## Summary CLI commands, daemon/dashboard startup, packaged desktop startup, and live-data maintenance scripts now share the mandatory PostgreSQL lifecycle. Operators no longer risk a command silently reading or writing a disconnected SQLite shadow when PostgreSQL setup fails. ## Design decisions - Every startup owner retains and awaits its PostgreSQL shutdown callback, including partial-startup failure paths. - CLI project context and lock-retry flows resolve through asynchronous project stores. - Maintenance scripts use the shared backend helper; explicit database migration/inspection remains the only CLI surface allowed to read legacy SQLite sources. ## Validation - CLI and Desktop typechecks pass on the stacked branch. - `pnpm test:gate` passes all 478 gate tests. - This PR changes 54 files. ## Stack - Depends on #2109, which depends on #2108. - Bundled plugins and docs/release follow in later PRs. Related: #2105 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * PostgreSQL is now the authoritative store for structured project and task metadata. * Projects can be recognized and initialized using `.fusion/project.json`, without creating a legacy SQLite database. * CLI commands now retry transient PostgreSQL contention errors. * **Bug Fixes** * Improved cleanup when commands complete, fail, or run in the background, preventing lingering resources. * Improved desktop, server, and session shutdown reliability. * **Documentation** * Updated storage and standalone binary guidance to reflect PostgreSQL and legacy SQLite compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
/**
|
|
* CLI command for exporting agents to Agent Companies packages.
|
|
*
|
|
* Usage:
|
|
* fn agent export <dir> [--company-name <name>] [--company-slug <slug>] [--project <name>]
|
|
*
|
|
* @module agent-export
|
|
*/
|
|
|
|
import { resolve } from "node:path";
|
|
|
|
import { AgentStore, exportAgentsToDirectory } from "@fusion/core";
|
|
|
|
import { resolveAgentStoreBase } from "../project-context.js";
|
|
|
|
/**
|
|
* FNXC:CliAgentControl 2026-07-09-00:00:
|
|
* Mirrors `agent.ts`'s private `closeAgentStoreSafely` (FN-7704) — kept as
|
|
* a tiny local copy here per FN-7740 File Scope (do NOT edit `agent.ts`,
|
|
* and do NOT fork the `TaskStore` retry/teardown logic; this only closes
|
|
* the `AgentStore` this file itself opens). Best-effort: an already-closed
|
|
* store must never throw here.
|
|
*/
|
|
function closeAgentStoreSafely(agentStore: AgentStore): void {
|
|
try {
|
|
agentStore.close();
|
|
} catch {
|
|
// Best-effort teardown — never let a close failure block exit.
|
|
}
|
|
}
|
|
|
|
function printSummary(result: {
|
|
outputDir: string;
|
|
agentsExported: number;
|
|
skillsExported: number;
|
|
filesWritten: string[];
|
|
errors: Array<{ agentId: string; error: string }>;
|
|
}): void {
|
|
console.log();
|
|
console.log(` Output directory: ${result.outputDir}`);
|
|
console.log(` Agents exported: ${result.agentsExported}`);
|
|
console.log(` Skills exported: ${result.skillsExported}`);
|
|
console.log(` Files written: ${result.filesWritten.length}`);
|
|
|
|
if (result.errors.length > 0) {
|
|
console.log(` Errors: ${result.errors.length}`);
|
|
for (const err of result.errors) {
|
|
console.log(` ✗ ${err.agentId}: ${err.error}`);
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Run the agent export command.
|
|
*/
|
|
export async function runAgentExport(
|
|
outputDir: string,
|
|
options?: {
|
|
project?: string;
|
|
companyName?: string;
|
|
companySlug?: string;
|
|
agentIds?: string[];
|
|
},
|
|
): Promise<void> {
|
|
// FNXC:PostgresCutover 2026-07-04: construct AgentStore in backend mode by
|
|
// borrowing the asyncLayer from the resolved project store (SQLite runtime
|
|
// removed under VAL-REMOVAL-005), mirroring extension.ts getAgentStore.
|
|
const base = await resolveAgentStoreBase(options?.project);
|
|
const agentStore = new AgentStore({ rootDir: base.rootDir + "/.fusion", asyncLayer: base.asyncLayer });
|
|
|
|
try {
|
|
await agentStore.init();
|
|
const allAgents = await agentStore.listAgents();
|
|
const filterIds = options?.agentIds?.filter((id) => id.trim().length > 0);
|
|
const agents = filterIds && filterIds.length > 0
|
|
? allAgents.filter((agent) => filterIds.includes(agent.id))
|
|
: allAgents;
|
|
|
|
if (agents.length === 0) {
|
|
console.error("No agents found to export");
|
|
closeAgentStoreSafely(agentStore);
|
|
await base.cleanup();
|
|
process.exit(1);
|
|
}
|
|
|
|
const result = await exportAgentsToDirectory(agents, resolve(outputDir), {
|
|
companyName: options?.companyName,
|
|
companySlug: options?.companySlug,
|
|
});
|
|
|
|
printSummary(result);
|
|
} finally {
|
|
closeAgentStoreSafely(agentStore);
|
|
await base.cleanup();
|
|
}
|
|
}
|