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

179 lines
5.7 KiB
TypeScript

import chalk from "chalk";
import type { Command } from "commander";
import {
type Tracker,
type ProjectConfig,
type OrchestratorConfig,
type PluginRegistry,
loadConfig,
} from "@composio/ao-core";
/**
* Resolve the target project from config.
* If --project is given, use that. If there is exactly one project, use it.
* Otherwise, error out asking the user to specify --project.
*/
function resolveProject(
config: OrchestratorConfig,
projectOpt?: string,
): { projectId: string; project: ProjectConfig } {
if (projectOpt) {
const project = config.projects[projectOpt];
if (!project) {
console.error(chalk.red(`Unknown project: ${projectOpt}`));
process.exit(1);
}
return { projectId: projectOpt, project };
}
const ids = Object.keys(config.projects);
if (ids.length === 0) {
console.error(chalk.red("No projects configured. Run `ao init` first."));
process.exit(1);
}
if (ids.length > 1) {
console.error(
chalk.red(`Multiple projects found. Specify one with --project: ${ids.join(", ")}`),
);
process.exit(1);
}
return { projectId: ids[0], project: config.projects[ids[0]] };
}
/**
* Get a Tracker instance from the plugin registry for the given project.
*/
async function getTracker(
config: OrchestratorConfig,
project: ProjectConfig,
): Promise<{ tracker: Tracker; registry: PluginRegistry }> {
if (!project.tracker) {
console.error(chalk.red("No tracker configured for this project."));
process.exit(1);
}
// getSessionManager internally creates the registry; we need the registry
// directly, so we replicate the same pattern from create-session-manager.
const { createPluginRegistry } = await import("@composio/ao-core");
const registry = createPluginRegistry();
await registry.loadFromConfig(config, (pkg: string) => import(pkg));
const tracker = registry.get<Tracker>("tracker", project.tracker.plugin);
if (!tracker) {
console.error(chalk.red(`Tracker plugin "${project.tracker.plugin}" not found.`));
process.exit(1);
}
return { tracker, registry };
}
export function registerVerify(program: Command): void {
program
.command("verify")
.description("Mark an issue as verified (or failed) after checking the fix on staging")
.argument("[issue]", "Issue number or identifier to verify")
.option("-p, --project <id>", "Project ID (required if multiple projects)")
.option("--fail", "Mark verification as failed instead of passing")
.option("-c, --comment <msg>", "Custom comment to add")
.option("-l, --list", "List all issues with merged-unverified label")
.action(
async (
issue: string | undefined,
opts: { project?: string; fail?: boolean; comment?: string; list?: boolean },
) => {
let config: OrchestratorConfig;
try {
config = loadConfig();
} catch {
console.error(chalk.red("No config found. Run `ao init` first."));
process.exit(1);
return;
}
const { projectId, project } = resolveProject(config, opts.project);
const { tracker } = await getTracker(config, project);
// --list mode: show all merged-unverified issues
if (opts.list) {
if (!tracker.listIssues) {
console.error(chalk.red("Tracker does not support listing issues."));
process.exit(1);
}
const issues = await tracker.listIssues(
{ state: "open", labels: ["merged-unverified"] },
project,
);
if (issues.length === 0) {
console.log(chalk.dim("No merged-unverified issues found."));
return;
}
console.log(chalk.bold(`Merged-unverified issues in ${project.name || projectId}:\n`));
for (const i of issues) {
const labels = i.labels.length > 0 ? chalk.dim(` [${i.labels.join(", ")}]`) : "";
console.log(` ${chalk.cyan(i.id)} ${i.title}${labels}`);
if (i.url) {
console.log(` ${chalk.dim(i.url)}`);
}
}
console.log(
chalk.dim(
`\n ${issues.length} issue${issues.length !== 1 ? "s" : ""} awaiting verification`,
),
);
return;
}
// Verify/fail mode: requires an issue argument
if (!issue) {
console.error(chalk.red("Issue number is required. Usage: ao verify <issue>"));
process.exit(1);
}
if (!tracker.updateIssue) {
console.error(chalk.red("Tracker does not support updating issues."));
process.exit(1);
}
if (opts.fail) {
// Verification failed
const comment = opts.comment ?? "Verification failed — problem persists on staging.";
await tracker.updateIssue(
issue,
{
state: "open",
labels: ["verification-failed"],
removeLabels: ["merged-unverified"],
comment,
},
project,
);
console.log(chalk.red(`Issue ${issue} marked as verification-failed (kept open).`));
console.log(chalk.dim(` Comment: ${comment}`));
} else {
// Verification passed
const comment = opts.comment ?? "Verified — fix confirmed on staging.";
await tracker.updateIssue(
issue,
{
state: "closed",
labels: ["verified"],
removeLabels: ["merged-unverified"],
comment,
},
project,
);
console.log(chalk.green(`Issue ${issue} verified and closed.`));
console.log(chalk.dim(` Comment: ${comment}`));
}
},
);
}