fix: align diagnostics cli with yargs

This commit is contained in:
Artur Do Lago
2026-01-13 13:42:25 +01:00
parent c1cce80b0b
commit 06ddafce1c
12 changed files with 234 additions and 137 deletions
+91 -57
View File
@@ -3,70 +3,104 @@
* @description CLI entry point for crash report generation
*/
import { Command } from "commander";
import type { Argv } from "yargs";
import * as readline from "readline";
import { ReportGenerator } from "../../crash-report/report-generator";
import { cmd } from "./cmd";
export function createBugReportCommand(): Command {
return new Command("bug-report")
.alias("report")
.description("Generate a crash report for debugging")
.option("--include-session", "Include session conversation data (requires consent)", false)
.option("--log-lines <count>", "Number of log lines to include", "500")
.option("-o, --output <path>", "Output path for the report archive")
.option("--skip-diagnostics", "Skip running health checks", false)
.option(
"--anonymization <level>",
"Anonymization level: minimal, standard, aggressive",
"standard"
)
.option("-y, --non-interactive", "Skip interactive prompts", false)
.action(async (options) => {
try {
// Validate anonymization level
const validLevels = ["minimal", "standard", "aggressive"];
if (!validLevels.includes(options.anonymization)) {
console.error(`Invalid anonymization level. Choose: ${validLevels.join(", ")}`);
process.exit(2);
}
const ANONYMIZATION_LEVELS = ["minimal", "standard", "aggressive"] as const;
// Session consent prompt
let includeSession = options.includeSession;
if (options.includeSession && !options.nonInteractive) {
includeSession = await promptConsent(
"Include session data? This may contain conversation content. (y/N): "
);
}
type BugReportArgs = {
includeSession?: boolean;
logLines?: number;
output?: string;
skipDiagnostics?: boolean;
anonymization?: (typeof ANONYMIZATION_LEVELS)[number];
nonInteractive?: boolean;
};
// Generate report
const generator = new ReportGenerator({
includeSession,
logLines: parseInt(options.logLines, 10),
outputPath: options.output,
skipDiagnostics: options.skipDiagnostics,
anonymization: options.anonymization as "minimal" | "standard" | "aggressive",
nonInteractive: options.nonInteractive,
});
const { archivePath } = await generator.generate();
// Success message
console.log("\n📎 Next steps:");
console.log(" 1. Review the report contents for any remaining sensitive data");
console.log(" 2. Create a GitHub issue at https://github.com/your-org/agent-core/issues");
console.log(" 3. Attach the report archive to the issue");
console.log(`\n Archive: ${archivePath}\n`);
process.exit(0);
} catch (error) {
console.error(
"Report generation failed:",
error instanceof Error ? error.message : String(error)
);
export const BugReportCommand = cmd({
command: "bug-report",
aliases: ["report"],
describe: "Generate a crash report for debugging",
builder: (yargs: Argv) => {
return yargs
.option("include-session", {
type: "boolean",
default: false,
describe: "Include session conversation data (requires consent)",
})
.option("log-lines", {
type: "number",
default: 500,
describe: "Number of log lines to include",
})
.option("output", {
alias: "o",
type: "string",
describe: "Output path for the report archive",
})
.option("skip-diagnostics", {
type: "boolean",
default: false,
describe: "Skip running health checks",
})
.option("anonymization", {
type: "string",
choices: ANONYMIZATION_LEVELS,
default: "standard",
describe: "Anonymization level: minimal, standard, aggressive",
})
.option("non-interactive", {
alias: "y",
type: "boolean",
default: false,
describe: "Skip interactive prompts",
});
},
handler: async (args) => {
try {
const typedArgs = args as BugReportArgs;
const anonymization = typedArgs.anonymization ?? "standard";
if (!ANONYMIZATION_LEVELS.includes(anonymization)) {
console.error(`Invalid anonymization level. Choose: ${ANONYMIZATION_LEVELS.join(", ")}`);
process.exit(2);
}
});
}
let includeSession = typedArgs.includeSession ?? false;
if (includeSession && !typedArgs.nonInteractive) {
includeSession = await promptConsent(
"Include session data? This may contain conversation content. (y/N): "
);
}
const generator = new ReportGenerator({
includeSession,
logLines: typedArgs.logLines ?? 500,
outputPath: typedArgs.output,
skipDiagnostics: typedArgs.skipDiagnostics ?? false,
anonymization,
nonInteractive: typedArgs.nonInteractive ?? false,
});
const { archivePath } = await generator.generate();
console.log("\n📎 Next steps:");
console.log(" 1. Review the report contents for any remaining sensitive data");
console.log(" 2. Create a GitHub issue at https://github.com/your-org/agent-core/issues");
console.log(" 3. Attach the report archive to the issue");
console.log(`\n Archive: ${archivePath}\n`);
process.exit(0);
} catch (error) {
console.error(
"Report generation failed:",
error instanceof Error ? error.message : String(error)
);
process.exit(2);
}
},
});
async function promptConsent(question: string): Promise<boolean> {
const rl = readline.createInterface({
+132 -71
View File
@@ -3,87 +3,148 @@
* @description CLI entry point for health checks
*/
import { Command } from "commander";
import type { Argv } from "yargs";
import { CheckEngine } from "../../diagnostics/check-engine";
import { InteractiveReporter } from "../../diagnostics/reporters/interactive";
import { JsonReporter } from "../../diagnostics/reporters/json";
import { MinimalReporter } from "../../diagnostics/reporters/minimal";
import type { CheckCategory } from "../../diagnostics/types";
import { cmd } from "./cmd";
export function createCheckCommand(): Command {
return new Command("check")
.description("Run diagnostic health checks")
.option("--full", "Run extended checks (slower but more thorough)", false)
.option("--fix", "Attempt to auto-fix detected issues", false)
.option("--json", "Output in JSON format for scripting", false)
.option("--minimal", "Single-line output for scripts", false)
.option("-v, --verbose", "Show detailed output including check details", false)
.option("--timeout <ms>", "Timeout per category in milliseconds", "10000")
.option(
"--category <names...>",
"Run only specific categories (runtime, config, providers, integrity)"
)
.option("--skip <ids...>", "Skip specific check IDs")
.option("--no-color", "Disable colored output")
.action(async (options) => {
try {
// Parse categories
let categories: CheckCategory[] | undefined;
if (options.category) {
const validCategories = ["runtime", "config", "providers", "integrity"];
categories = options.category.filter((c: string) =>
validCategories.includes(c)
) as CheckCategory[];
const VALID_CATEGORIES: CheckCategory[] = ["runtime", "config", "providers", "integrity"];
if (categories.length === 0) {
console.error(
`Invalid categories. Valid options: ${validCategories.join(", ")}`
);
process.exit(2);
}
}
type CheckArgs = {
full?: boolean;
fix?: boolean;
json?: boolean;
minimal?: boolean;
verbose?: boolean;
timeout?: number;
category?: string[];
skip?: string[];
color?: boolean;
};
// Create engine with options
const engine = new CheckEngine({
full: options.full,
fix: options.fix,
verbose: options.verbose,
timeout: parseInt(options.timeout, 10),
categories,
skip: options.skip,
});
// Run checks
const report = await engine.runAll();
// Output based on format
if (options.json) {
const reporter = new JsonReporter();
console.log(reporter.format(report));
} else if (options.minimal) {
const reporter = new MinimalReporter();
console.log(reporter.format(report));
} else {
const reporter = new InteractiveReporter({
verbose: options.verbose,
colors: options.color !== false,
});
console.log(reporter.format(report));
}
// Exit with appropriate code
if (report.summary.failed > 0) {
process.exit(1);
}
process.exit(0);
} catch (error) {
console.error(
"Health check failed:",
error instanceof Error ? error.message : String(error)
export const CheckCommand = cmd({
command: "check",
describe: "Run diagnostic health checks",
builder: (yargs: Argv) => {
return yargs
.option("full", {
type: "boolean",
default: false,
describe: "Run extended checks (slower but more thorough)",
})
.option("fix", {
type: "boolean",
default: false,
describe: "Attempt to auto-fix detected issues",
})
.option("json", {
type: "boolean",
default: false,
describe: "Output in JSON format for scripting",
})
.option("minimal", {
type: "boolean",
default: false,
describe: "Single-line output for scripts",
})
.option("verbose", {
alias: "v",
type: "boolean",
default: false,
describe: "Show detailed output including check details",
})
.option("timeout", {
type: "number",
default: 10000,
describe: "Timeout per category in milliseconds",
})
.option("category", {
type: "string",
array: true,
choices: VALID_CATEGORIES,
describe: "Run only specific categories (runtime, config, providers, integrity)",
})
.option("skip", {
type: "string",
array: true,
describe: "Skip specific check IDs",
})
.option("color", {
type: "boolean",
default: true,
describe: "Enable colored output",
});
},
handler: async (args: CheckArgs) => {
try {
let categories: CheckCategory[] | undefined;
if (args.category?.length) {
categories = args.category.filter((c): c is CheckCategory =>
VALID_CATEGORIES.includes(c as CheckCategory)
);
process.exit(2);
if (categories.length === 0) {
console.error(`Invalid categories. Valid options: ${VALID_CATEGORIES.join(", ")}`);
process.exit(2);
}
}
});
const engine = new CheckEngine({
full: Boolean(args.full),
fix: Boolean(args.fix),
verbose: Boolean(args.verbose),
timeout: args.timeout ?? 10000,
categories,
skip: args.skip,
});
const report = await engine.runAll();
if (args.json) {
const reporter = new JsonReporter();
console.log(reporter.format(report));
} else if (args.minimal) {
const reporter = new MinimalReporter();
console.log(reporter.format(report));
} else {
const reporter = new InteractiveReporter({
verbose: args.verbose,
colors: args.color !== false,
});
console.log(reporter.format(report));
}
if (report.summary.failed > 0) {
process.exit(1);
}
process.exit(0);
} catch (error) {
console.error(
"Health check failed:",
error instanceof Error ? error.message : String(error)
);
process.exit(2);
}
},
});
export async function checkEnvironment(): Promise<void> {
const engine = new CheckEngine({
full: false,
fix: false,
verbose: false,
timeout: 5000,
categories: ["runtime", "config"],
});
const report = await engine.runAll();
if (report.summary.failed > 0) {
const reporter = new MinimalReporter();
console.error(reporter.format(report));
throw new Error("Environment checks failed");
}
}
// Help examples
@@ -4,7 +4,7 @@
*/
import * as os from "os";
import {
import type {
CheckResult,
CheckOptions,
CheckReport,
@@ -6,7 +6,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import { CheckResult, CheckOptions } from "../types";
import type { CheckResult, CheckOptions } from "../types";
/** Deprecated configuration options that should be migrated */
const DEPRECATED_OPTIONS = [
@@ -6,7 +6,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import { CheckResult, CheckOptions } from "../types";
import type { CheckResult, CheckOptions } from "../types";
const STALE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
@@ -3,7 +3,7 @@
* @description Network and AI provider connectivity checks
*/
import { CheckResult, CheckOptions } from "../types";
import type { CheckResult, CheckOptions } from "../types";
interface ProviderConfig {
name: string;
@@ -7,7 +7,7 @@ import { execSync } from "child_process";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import { CheckResult, CheckOptions } from "../types";
import type { CheckResult, CheckOptions } from "../types";
/** Minimum required Bun version */
const MIN_BUN_VERSION = "1.0.0";
@@ -3,7 +3,7 @@
* @description TTY-friendly colored output for health check results
*/
import { CheckReport, CheckResult, CheckCategory } from "../types";
import type { CheckReport, CheckResult, CheckCategory } from "../types";
// ANSI color codes
const colors = {
@@ -3,7 +3,7 @@
* @description Machine-readable JSON output for CI/scripting
*/
import { CheckReport } from "../types";
import type { CheckReport } from "../types";
export class JsonReporter {
private pretty: boolean;
@@ -3,7 +3,7 @@
* @description Single-line output for scripts and CI
*/
import { CheckReport } from "../types";
import type { CheckReport } from "../types";
export class MinimalReporter {
format(report: CheckReport): string {
+2
View File
@@ -30,6 +30,7 @@ import { DaemonCommand, DaemonStatusCommand, DaemonStopCommand } from "./cli/cmd
import { PluginCommand } from "./cli/cmd/plugin"
import { SetupCommand } from "./cli/cmd/setup"
import { BugReportCommand } from "./cli/cmd/bug-report"
import { CheckCommand } from "./cli/cmd/check"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -87,6 +88,7 @@ const cli = yargs(hideBin(process.argv))
.command(TuiThreadCommand)
.command(AttachCommand)
.command(RunCommand)
.command(CheckCommand)
.command(GenerateCommand)
.command(DebugCommand)
.command(AuthCommand)
+1 -1
View File
@@ -78,6 +78,6 @@ export function setContextValue<K extends keyof LogContext>(
): void {
const current = asyncLocalStorage.getStore();
if (current) {
(current as Record<string, unknown>)[key] = value;
current[key] = value;
}
}