#!/usr/bin/env node const assert = require("assert"); const fs = require("fs"); const path = require("path"); const repo = path.resolve(__dirname, ".."); function read(relativePath) { const fullPath = path.join(repo, relativePath); if (!fs.existsSync(fullPath)) { if (fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json"))) { console.log( `CLI-first contract smoke skipped: ${relativePath} is filtered from this public tree` ); process.exit(0); } throw new Error(`${relativePath} is missing`); } return fs.readFileSync(fullPath, "utf8"); } function criterionLines(source) { return source .split(/\r?\n/) .filter((line) => /^- \[[ x]\] \*\*/.test(line)); } function expect(source, name, pattern) { assert.match(source, pattern, `missing CLI-first contract evidence: ${name}`); } const criteria = read("cli_acceptance_criteria.md"); const cli = read("crates/disasmer-cli/src/main.rs"); const outputModeSmoke = read("scripts/cli-output-mode-smoke.js"); expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m); expect( criteria, "addendum status", /\*\*Status:\*\* CLI-first addendum to `acceptance_criteria\.md` and `acceptance_criteria_phase2\.md`/ ); expect( criteria, "website exception", /hosted account creation as the only intentional private website exception/ ); expect( criteria, "no duplicate work note", /does not automatically mean new product code, a new feature, or even actual implementation work is required/ ); expect( criteria, "future hosted business non-goal", /billing, full support tooling, broad moderation consoles, and durable account\/business-process management are intentionally outside this CLI-first MVP slice/ ); const lines = criterionLines(criteria); assert(lines.length > 0, "CLI-first criteria must contain criteria lines"); for (const line of lines) { assert.match( line, /^- \[[ x]\] \*\*(Passed|Partial|Open)(?: \([^)]+\))?:\*\*/, `CLI-first criterion lacks an explicit status prefix: ${line}` ); } const openCriteria = lines.filter((line) => /\*\*Open(?::| \()/.test(line)); assert.deepStrictEqual( openCriteria, [ "- [ ] **Open:** Every criterion in this document is validated end-to-end or with integration-style coverage, preferably against a realistic NixOS deployment that can pretend to be live hosted infrastructure.", ], "only the full CLI-first verification gate should remain Open while command/API facts are Partial" ); for (const [name, pattern] of [ ["top-level version metadata", /#\[command\(name = "disasmer", version, arg_required_else_help = true\)\]/], ["human report renderer", /fn human_report\(value: &Value\) -> String/], ["shared report emitter", /fn emit_report\(report: &T, json_output: bool\) -> Result<\(\)>/], ["doctor command", /Doctor\(DoctorArgs\)/], ["auth status command", /enum AuthCommands[\s\S]*Status\(AuthStatusArgs\)/], ["auth logout command", /enum AuthCommands[\s\S]*Logout\(AuthLogoutArgs\)/], ["key lifecycle commands", /enum KeyCommands[\s\S]*Add\(KeyAddArgs\)[\s\S]*List\(KeyListArgs\)[\s\S]*Revoke\(KeyRevokeArgs\)/], ["project commands", /enum ProjectCommands[\s\S]*Init\(ProjectInitArgs\)[\s\S]*Status\(ProjectStatusArgs\)[\s\S]*List\(ProjectListArgs\)[\s\S]*Select\(ProjectSelectArgs\)/], ["inspect command", /Inspect\(BundleInspectArgs\)/], ["build command", /Build\(BuildArgs\)/], ["node lifecycle commands", /enum NodeCommands[\s\S]*Attach\(AttachArgs\)[\s\S]*Enroll\(NodeEnrollArgs\)[\s\S]*List\(NodeListArgs\)[\s\S]*Status\(NodeStatusArgs\)[\s\S]*Revoke\(NodeRevokeArgs\)/], ["process commands", /enum ProcessCommands[\s\S]*Status\(ProcessStatusArgs\)[\s\S]*Restart\(ProcessRestartArgs\)[\s\S]*Cancel\(ProcessCancelArgs\)/], ["task list command", /enum TaskCommands[\s\S]*List\(TaskListArgs\)/], ["logs command", /Logs\(LogsArgs\)/], ["artifact commands", /enum ArtifactCommands[\s\S]*List\(ArtifactListArgs\)[\s\S]*Download\(ArtifactDownloadArgs\)[\s\S]*Export\(ArtifactExportArgs\)/], ["DAP command", /Dap\(DapArgs\)/], ["debug attach command", /enum DebugCommands[\s\S]*Attach\(DebugAttachArgs\)/], ["quota command", /enum QuotaCommands[\s\S]*Status\(QuotaStatusArgs\)/], ["admin commands", /enum AdminCommands[\s\S]*Status\(AdminStatusArgs\)[\s\S]*Bootstrap\(AdminBootstrapArgs\)[\s\S]*RevokeNode\(NodeRevokeArgs\)[\s\S]*StopProcess\(ProcessCancelArgs\)[\s\S]*SuspendTenant\(AdminSuspendTenantArgs\)/], ]) { expect(cli, name, pattern); } for (const [name, pattern] of [ ["CLI parse coverage", /fn cli_first_mvp_command_surface_parses\(\)/], ["CLI version coverage", /fn top_level_version_is_available\(\)/], ["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/], ["CLI human output coverage", /fn human_report_is_text_not_json\(\)/], ["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/], ["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/], ["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/], ]) { expect(cli, name, pattern); } for (const [name, pattern] of [ ["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["bundle inspect --json flag", /struct BundleInspectArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["build --json flag", /struct BuildArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["run --json flag", /struct RunArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["node attach --json flag", /struct AttachArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["DAP plan --json flag", /struct DapArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ["shared scope --json flag", /struct CliScopeArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/], ]) { expect(cli, name, pattern); } for (const [name, pattern] of [ ["human default assertion", /default output should be human-readable text, not JSON/], ["login JSON mode", /\["login", "--coordinator", "https:\/\/coord\.example\.test", "--json"\]/], ["doctor human mode", /\["doctor"\]/], ["bundle inspect JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/], ["auth expiry posture", /token_expiry_posture[\s\S]*expires_at/], ]) { expect(outputModeSmoke, name, pattern); } console.log("CLI-first contract smoke passed");