diff --git a/packages/runtime-node/src/cli.ts b/packages/runtime-node/src/cli.ts index dbddefa..fdf21fa 100644 --- a/packages/runtime-node/src/cli.ts +++ b/packages/runtime-node/src/cli.ts @@ -2,10 +2,20 @@ /** * `npx @autter/runtime-node doctor` — see doctor.ts. */ +import { SDK_IDENTITY } from "./compat.js"; import { DOCTOR_USAGE, formatDoctorReport, parseDoctorArgs, runDoctor } from "./doctor.js"; +/** `autter-runtime --version` / `-v` / `version`: the installed SDK's name and version. */ +export function versionLine(): string { + return `${SDK_IDENTITY.name} ${SDK_IDENTITY.version}`; +} + async function main(argv: string[]): Promise { const [command, ...rest] = argv; + if (command === "--version" || command === "-v" || command === "version") { + console.log(versionLine()); + return 0; + } if (command !== "doctor") { console.log(DOCTOR_USAGE); return command === undefined || command === "--help" || command === "-h" ? 0 : 64; @@ -20,7 +30,8 @@ async function main(argv: string[]): Promise { return 64; } const report = await runDoctor(parsed); - console.log(parsed.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report)); + const output = parsed.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report, { quiet: parsed.quiet }); + if (output) (report.exitCode === 0 ? console.log : console.error)(output); return report.exitCode; } diff --git a/packages/runtime-node/src/doctor.ts b/packages/runtime-node/src/doctor.ts index 330a888..71c7355 100644 --- a/packages/runtime-node/src/doctor.ts +++ b/packages/runtime-node/src/doctor.ts @@ -25,6 +25,8 @@ export interface DoctorOptions { key?: string; features?: string[]; json?: boolean; + /** Print nothing when compatible, and only the problems otherwise (CI). */ + quiet?: boolean; cwd?: string; timeoutMs?: number; } @@ -161,7 +163,8 @@ export async function runDoctor(options: DoctorOptions = {}): Promise Also verify an ingest key (default: $AUTTER_RUNTIME_KEY) --features Only check these features (${COMPAT_MANIFEST.features.map((f) => f.id).join(", ")}) --json Machine-readable output + -q, --quiet Print nothing when compatible, only the problems otherwise --timeout Per-request timeout (default 5000) Exit codes: 0 compatible, 1 mismatch or rejected key, 2 ingester unreachable.`; @@ -246,6 +268,10 @@ export function parseDoctorArgs(argv: string[], env: NodeJS.ProcessEnv = process case "--json": options.json = true; break; + case "-q": + case "--quiet": + options.quiet = true; + break; case "--timeout": { const ms = Number(value()); if (!Number.isFinite(ms) || ms <= 0) return { error: "--timeout needs a positive number of ms" }; @@ -256,6 +282,7 @@ export function parseDoctorArgs(argv: string[], env: NodeJS.ProcessEnv = process return { error: `unknown option ${arg}` }; } } + if (options.json && options.quiet) return { error: "--json and --quiet cannot be combined" }; if (!options.key && env.AUTTER_RUNTIME_KEY) options.key = env.AUTTER_RUNTIME_KEY; return options; } diff --git a/packages/runtime-node/src/index.ts b/packages/runtime-node/src/index.ts index 6a1fd7c..42baeed 100644 --- a/packages/runtime-node/src/index.ts +++ b/packages/runtime-node/src/index.ts @@ -102,4 +102,10 @@ export { type IngesterCompatInfo, type SdkIdentity, } from "./compat.js"; -export { runDoctor, type DoctorOptions, type DoctorReport } from "./doctor.js"; +export { + formatDoctorReport, + parseDoctorArgs, + runDoctor, + type DoctorOptions, + type DoctorReport, +} from "./doctor.js"; diff --git a/packages/runtime-node/test/doctor-quiet.test.mjs b/packages/runtime-node/test/doctor-quiet.test.mjs new file mode 100644 index 0000000..06ba05d --- /dev/null +++ b/packages/runtime-node/test/doctor-quiet.test.mjs @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { formatDoctorReport, parseDoctorArgs } from "../dist/index.js"; + +const compatible = { + endpoint: "https://otlp.example.test", + sdks: [{ name: "@autter/runtime-node", version: "1.5.0" }], + ingester: { version: "1.5.0", schema: null }, + features: [{ id: "logs", title: "Logs", status: "ok" }], + issues: [], + exitCode: 0, +}; + +test("--quiet and -q are parsed, and cannot be combined with --json", () => { + assert.deepEqual(parseDoctorArgs(["--quiet"], {}), { quiet: true }); + assert.deepEqual(parseDoctorArgs(["-q", "--endpoint", "http://x"], {}), { quiet: true, endpoint: "http://x" }); + assert.deepEqual(parseDoctorArgs(["--json", "--quiet"], {}), { error: "--json and --quiet cannot be combined" }); +}); + +test("a quiet report is empty when everything is compatible", () => { + assert.equal(formatDoctorReport(compatible, { quiet: true }), ""); + assert.match(formatDoctorReport(compatible), /Compatible\./); +}); + +test("a quiet report lists only the problems", () => { + const broken = { + ...compatible, + key: "rejected", + features: [{ id: "traces", title: "Traces", status: "incompatible" }], + issues: [{ message: "ingester 1.3.0 is older than the SDK expects" }], + exitCode: 1, + }; + assert.equal( + formatDoctorReport(broken, { quiet: true }), + [ + "autter doctor: ingest key rejected (401)", + "autter doctor: feature traces is incompatible", + "autter doctor: ingester 1.3.0 is older than the SDK expects", + ].join("\n"), + ); + const unreachable = { ...compatible, ingester: null, ingesterError: "ECONNREFUSED", features: [], exitCode: 2 }; + assert.equal( + formatDoctorReport(unreachable, { quiet: true }), + "autter doctor: ingester unreachable at https://otlp.example.test (ECONNREFUSED)", + ); +}); + +test("the CLI prints its version and exits 0", async () => { + const { spawnSync } = await import("node:child_process"); + const { fileURLToPath } = await import("node:url"); + const { readFileSync } = await import("node:fs"); + const cli = fileURLToPath(new URL("../dist/cli.js", import.meta.url)); + const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + for (const flag of ["--version", "-v", "version"]) { + const run = spawnSync(process.execPath, [cli, flag], { encoding: "utf8" }); + assert.equal(run.status, 0, flag); + assert.equal(run.stdout.trim(), `@autter/runtime-node ${version}`); + } +});