#!/usr/bin/env node /** * cli:support-report — entry point. * * Verifies a captured CLI failure against the evidence rule and, ONLY when it * is a proven CLI-internal defect, an envelope contradicting itself (a * `dedupOf` mirror finding in err while its primary is ok — mechanical), a * substantively disputed refusal, or an argued verdict dispute anchored on * rules the envelope carries, writes the support bundle under * `/.smartstack/support/` for transmission to * support@atlashub.ch. Everything else is refused with guidance — this CLI is * the fail-closed gate between "a CLI printed a scary error" and "AtlasHub * receives a bug report". * * Invocation: * npx --prefer-offline tsx skills/support-report/cli/create/index.ts \ * --spec-file # or --spec '' * * Spec: * { * "projectPath": "D:/…/client-project", * "command": "npx tsx skills/…/index.ts --spec-file …", // verbatim * "runs": [ { "exitCode": 1, "stdout": "…", "stderr": "…" } ], * "spec": { … }, // the spec passed to the FAILING cli (optional) * "context": "…", // what the run was doing (optional) * "analysis": "…", // why cli-internal (optional) * "dispute": "…", // ≥ 20 chars against a controlled refusal; * // ≥ 80 chars WITH disputedRuleIds against a verdict * "disputedRuleIds": ["XD-005"], // rules PRESENT in the envelope findings (optional) * "inputs": [".smartstack/ba"] // what the CLI READ, relative to projectPath — * // bundled scrubbed + zipped for reproduction (optional) * } * * Exit codes: * 0 report written (confirmed / pending-retest-after-update / duplicate) * 1 refused — not reportable (usage-error / environment / unverified / * flaky / coherent no-failure / rejected dispute / input escaping the * project); the envelope's nextSteps say what to do instead * 4 usage / invalid spec */ import { parseArgs } from 'node:util' import { z } from 'zod' import { failExecute, printEnvelope } from '../../../lib/output.js' import { readSpecArg } from '../../../lib/spec-arg.js' import { COMMAND, run, type SupportReportSpec, type SupportRunReport } from './run.js' const RunSchema = z.object({ exitCode: z.number().int(), stdout: z.string(), stderr: z.string(), }) // Audit rule ids: `XD-005`, `RBAC-008`, `DEV-API-021` (2 or 3 upper segments + 3 digits). const RULE_ID_RE = /^[A-Z]{2,6}(-[A-Z]{2,6})?-\d{3}[a-z]?$/ // An input is a path INSIDE the project — never absolute, never a drive. const ABSOLUTE_PATH_RE = /^[\\/]|^[a-zA-Z]:/ const SpecSchema = z .object({ projectPath: z.string().min(1), command: z.string().min(1), runs: z.array(RunSchema).max(10), spec: z.unknown().optional(), context: z.string().optional(), analysis: z.string().optional(), // A dispute must argue, not just insist — one word is not a justification. dispute: z.string().min(20).optional(), disputedRuleIds: z.array(z.string().regex(RULE_ID_RE, 'not an audit rule id')).min(1).max(20).optional(), inputs: z.array(z.string().min(1).refine((p) => !ABSOLUTE_PATH_RE.test(p), 'must be relative to projectPath')).max(50).optional(), }) .superRefine((s, ctx) => { // A verdict dispute argues in ≥ 80 chars — the usage-error dispute keeps its 20. if (s.disputedRuleIds !== undefined && (s.dispute === undefined || s.dispute.length < 80)) { ctx.addIssue({ path: ['dispute'], code: 'custom', message: 'a verdict dispute (disputedRuleIds) needs an argued dispute of ≥ 80 chars' }) } }) async function main(): Promise { let values: { spec?: string; 'spec-file'?: string } try { ;({ values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' } }, strict: true, })) } catch (e) { printEnvelope(failExecute(COMMAND, [e instanceof Error ? e.message : String(e)])) process.exit(4) } const specArg = readSpecArg(values) if ('error' in specArg) { printEnvelope(failExecute(COMMAND, [specArg.error])) process.exit(4) } let raw: unknown try { raw = JSON.parse(specArg.raw) } catch { printEnvelope(failExecute(COMMAND, ['spec must be valid JSON'])) process.exit(4) } const parsed = SpecSchema.safeParse(raw) if (!parsed.success) { printEnvelope( failExecute( COMMAND, parsed.error.issues.map((i) => `spec.${i.path.join('.')}: ${i.message}`), ), ) process.exit(4) } try { const envelope = await run(parsed.data as SupportReportSpec) printEnvelope(envelope) process.exit(envelope.success ? 0 : 1) } catch (e) { printEnvelope(failExecute(COMMAND, [e instanceof Error ? (e.stack ?? e.message) : String(e)])) process.exit(4) } } await main()