agent-orchestrator/packages/cli/src/commands/project.ts

194 lines
6.3 KiB
TypeScript

import { basename, resolve } from "node:path";
import { existsSync } from "node:fs";
import chalk from "chalk";
import type { Command } from "commander";
import {
getPortfolio,
getPortfolioSessionCounts,
isPortfolioEnabled,
recordActivityEvent,
registerProject,
unregisterProject,
loadPreferences,
savePreferences,
loadLocalProjectConfig,
loadConfig,
} from "@aoagents/ao-core";
import {
formatPortfolioDegradedReason,
formatPortfolioProjectName,
formatPortfolioProjectStatus,
} from "../lib/portfolio-display.js";
function assertPortfolioEnabled(): void {
if (isPortfolioEnabled()) return;
console.error(
chalk.red(
"Portfolio mode is disabled. Unset AO_ENABLE_PORTFOLIO or set it to 1 to use `ao project`.",
),
);
process.exit(1);
}
export function registerProjectCommand(program: Command): void {
const project = program.command("project").description("Manage portfolio projects");
// ao project ls
project
.command("ls")
.description("List all portfolio projects")
.action(async () => {
assertPortfolioEnabled();
const portfolio = getPortfolio();
if (portfolio.length === 0) {
console.log(chalk.dim("No projects in portfolio."));
console.log(
chalk.dim("Run `ao start` in a project or `ao project add <path>` to register one."),
);
return;
}
const counts = await getPortfolioSessionCounts(portfolio);
const prefs = loadPreferences();
console.log(chalk.bold("\nPortfolio Projects\n"));
for (const p of portfolio) {
const count = counts[p.id] || { total: 0, active: 0 };
const isDefault = prefs.defaultProjectId === p.id;
const status = formatPortfolioProjectStatus(p, count);
const pin = p.pinned ? chalk.yellow("*") : " ";
const def = isDefault ? chalk.cyan(" (default)") : "";
const name = formatPortfolioProjectName(p);
const degradedReason = formatPortfolioDegradedReason(p);
console.log(` ${pin} ${chalk.bold(p.id)}${name}${def}`);
console.log(` ${status} | ${count.total} sessions | ${chalk.dim(p.source)}`);
if (degradedReason) {
console.log(` ${degradedReason}`);
}
}
console.log();
});
// ao project add <path>
project
.command("add <path>")
.description("Register a project path in the portfolio")
.option(
"-k, --key <key>",
"Legacy only: the project key under `projects:` in a wrapped agent-orchestrator.yaml. Omit for flat configs.",
)
.option("--default", "Use the default project ID, adding a numeric suffix if needed")
.action(async (path: string, opts: { key?: string; default?: boolean }) => {
assertPortfolioEnabled();
const resolvedPath = resolve(path);
const candidatePaths = [
resolve(resolvedPath, "agent-orchestrator.yaml"),
resolve(resolvedPath, "agent-orchestrator.yml"),
];
if (!loadLocalProjectConfig(resolvedPath)) {
const existingConfigPath = candidatePaths.find((candidate) => existsSync(candidate));
if (!existingConfigPath) {
recordActivityEvent({
source: "cli",
kind: "cli.project_register_failed",
level: "warn",
summary: `ao project add: no agent-orchestrator config found`,
data: { resolvedPath, reason: "no_config_found" },
});
console.error(chalk.red(`No agent-orchestrator.yaml found at ${resolvedPath}`));
process.exit(1);
}
try {
loadConfig(existingConfigPath);
recordActivityEvent({
source: "cli",
kind: "cli.project_register_failed",
level: "warn",
summary: `ao project add: found old-format config requiring migration`,
data: {
resolvedPath,
configPath: existingConfigPath,
reason: "old_format",
},
});
console.error(
chalk.red(
`Found old-format config at ${existingConfigPath}. Run \`ao start\` in that project to migrate it before using \`ao project add\`.`,
),
);
} catch (error) {
recordActivityEvent({
source: "cli",
kind: "cli.project_register_failed",
level: "error",
summary: `ao project add: config load failed`,
data: {
resolvedPath,
configPath: existingConfigPath,
reason: "load_error",
errorMessage: error instanceof Error ? error.message : String(error),
},
});
console.error(
chalk.red(
`Found agent-orchestrator config at ${existingConfigPath}, but it could not be loaded: ${error instanceof Error ? error.message : String(error)}`,
),
);
}
process.exit(1);
}
let projectId = opts.key;
if (!projectId) {
projectId = basename(resolvedPath) || "project";
}
const effectiveId = registerProject(resolvedPath, projectId, basename(resolvedPath) || projectId);
console.log(chalk.green(`Registered project "${effectiveId}" at ${resolvedPath}`));
});
// ao project rm <id>
project
.command("rm <id>")
.description("Remove a project from the portfolio")
.action((id: string) => {
assertPortfolioEnabled();
const portfolio = getPortfolio();
const found = portfolio.find((p) => p.id === id);
if (!found) {
console.error(chalk.red(`Project "${id}" not found in portfolio`));
process.exit(1);
}
unregisterProject(id);
console.log(chalk.green(`Removed project "${id}" from portfolio`));
});
// ao project set-default <id>
project
.command("set-default <id>")
.description("Set the default project for the portfolio")
.action((id: string) => {
assertPortfolioEnabled();
const portfolio = getPortfolio();
const found = portfolio.find((p) => p.id === id);
if (!found) {
console.error(chalk.red(`Project "${id}" not found in portfolio`));
process.exit(1);
}
const prefs = loadPreferences();
prefs.defaultProjectId = id;
savePreferences(prefs);
console.log(chalk.green(`Set default project to "${id}"`));
});
}