Files
fusion/packages/cli/src/commands/agent-export.ts
gsxdsm 97172fdcf2 fix(FN-7952): require PostgreSQL in CLI and desktop (#2110)
## 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 -->
2026-07-14 23:18:55 -07:00

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();
}
}