@zivis/cli 0.1.0-alpha.40 → 0.1.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/commands/app/index.js +4 -2
  2. package/dist/commands/assurance/index.d.ts +33 -0
  3. package/dist/commands/assurance/index.js +149 -0
  4. package/dist/commands/auth/index.js +1 -0
  5. package/dist/commands/auth/init.d.ts +1 -0
  6. package/dist/commands/auth/init.js +12 -8
  7. package/dist/commands/gate/index.d.ts +2 -0
  8. package/dist/commands/gate/index.js +171 -0
  9. package/dist/commands/mcp/index.js +33 -1
  10. package/dist/commands/run/index.d.ts +2 -0
  11. package/dist/commands/run/index.js +111 -0
  12. package/dist/commands/sync/index.d.ts +2 -0
  13. package/dist/commands/sync/index.js +187 -0
  14. package/dist/commands/test/index.d.ts +2 -0
  15. package/dist/commands/test/index.js +115 -0
  16. package/dist/commands/tm/index.js +4 -0
  17. package/dist/index.js +11 -0
  18. package/dist/internal/application-binding.d.ts +8 -0
  19. package/dist/internal/application-binding.js +167 -0
  20. package/dist/internal/cli-output.d.ts +16 -0
  21. package/dist/internal/cli-output.js +39 -0
  22. package/dist/internal/devx-run.d.ts +47 -0
  23. package/dist/internal/devx-run.js +36 -0
  24. package/dist/internal/gate-evaluate.d.ts +50 -0
  25. package/dist/internal/gate-evaluate.js +111 -0
  26. package/dist/internal/gate-policy.d.ts +38 -0
  27. package/dist/internal/gate-policy.js +167 -0
  28. package/dist/internal/git-metadata.d.ts +8 -0
  29. package/dist/internal/git-metadata.js +38 -0
  30. package/dist/internal/git.d.ts +8 -0
  31. package/dist/internal/git.js +30 -0
  32. package/dist/internal/ide-setup.d.ts +1 -0
  33. package/dist/internal/ide-setup.js +76 -19
  34. package/dist/internal/inventory-sync.d.ts +74 -0
  35. package/dist/internal/inventory-sync.js +189 -0
  36. package/dist/internal/packs/cache.d.ts +8 -0
  37. package/dist/internal/packs/cache.js +60 -0
  38. package/dist/internal/packs/index.d.ts +10 -0
  39. package/dist/internal/packs/index.js +7 -0
  40. package/dist/internal/packs/integrity.d.ts +9 -0
  41. package/dist/internal/packs/integrity.js +24 -0
  42. package/dist/internal/packs/jcs.d.ts +2 -0
  43. package/dist/internal/packs/jcs.js +57 -0
  44. package/dist/internal/packs/local-paths.d.ts +4 -0
  45. package/dist/internal/packs/local-paths.js +26 -0
  46. package/dist/internal/packs/registry-client.d.ts +20 -0
  47. package/dist/internal/packs/registry-client.js +40 -0
  48. package/dist/internal/packs/resolve.d.ts +8 -0
  49. package/dist/internal/packs/resolve.js +89 -0
  50. package/dist/internal/packs/semver.d.ts +9 -0
  51. package/dist/internal/packs/semver.js +57 -0
  52. package/dist/internal/packs/signing.d.ts +5 -0
  53. package/dist/internal/packs/signing.js +49 -0
  54. package/dist/internal/packs/types.d.ts +70 -0
  55. package/dist/internal/packs/types.js +20 -0
  56. package/dist/internal/run-workdir.d.ts +1 -0
  57. package/dist/internal/run-workdir.js +11 -0
  58. package/dist/internal/security-context.d.ts +62 -0
  59. package/dist/internal/security-context.js +50 -0
  60. package/dist/internal/sync-outbox.d.ts +40 -0
  61. package/dist/internal/sync-outbox.js +66 -0
  62. package/dist/internal/test-scope.d.ts +68 -0
  63. package/dist/internal/test-scope.js +69 -0
  64. package/dist/internal/zivis-local-state.d.ts +1 -0
  65. package/dist/internal/zivis-local-state.js +22 -0
  66. package/package.json +3 -2
@@ -16,7 +16,8 @@ export function registerAppCommands(program) {
16
16
  .option("--json", "emit raw JSON")
17
17
  .action(async (opts) => {
18
18
  const client = buildClient(opts);
19
- const apps = await client.request("GET", "/api/rt/applications");
19
+ const result = await client.request("GET", "/api/rt/applications");
20
+ const apps = result.applications;
20
21
  if (opts.json) {
21
22
  process.stdout.write(JSON.stringify(apps, null, 2) + "\n");
22
23
  return;
@@ -85,7 +86,8 @@ export function registerAppCommands(program) {
85
86
  }
86
87
  const client = buildClient(opts);
87
88
  try {
88
- const created = await client.request("POST", "/api/rt/applications", body);
89
+ const result = await client.request("POST", "/api/rt/applications", body);
90
+ const created = result.application;
89
91
  if (opts.json) {
90
92
  process.stdout.write(JSON.stringify(created, null, 2) + "\n");
91
93
  return;
@@ -0,0 +1,33 @@
1
+ import type { Command } from "commander";
2
+ export interface CategoryPosture {
3
+ categoryId: string;
4
+ categoryName: string;
5
+ scoreRaw: number | null;
6
+ coverage: number | null;
7
+ determinedCount: number;
8
+ totalCount: number;
9
+ }
10
+ export interface AssuranceStatusResponse {
11
+ applicationId: string;
12
+ model: {
13
+ key: string;
14
+ displayName: string;
15
+ frameworkId: string | null;
16
+ version: string | null;
17
+ };
18
+ evaluationJobId: string | null;
19
+ evaluatedAt: string | null;
20
+ lastEvaluatedGitSha: string | null;
21
+ overall: {
22
+ scoreRaw: number | null;
23
+ coverage: number | null;
24
+ determinedCount: number;
25
+ totalCount: number;
26
+ };
27
+ categories: CategoryPosture[];
28
+ risk: {
29
+ openCritical: number;
30
+ openHigh: number;
31
+ };
32
+ }
33
+ export declare function registerAssuranceCommands(program: Command): void;
@@ -0,0 +1,149 @@
1
+ import { ApiClient, ApiError } from "@zivis/mcp/api-client";
2
+ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
3
+ import { requireApplicationId } from "@zivis/mcp/resolve-application-id";
4
+ import { jsonEnvelope, printJson, failCommand, EXIT } from "../../internal/cli-output.js";
5
+ function buildClient(opts) {
6
+ const config = opts.session
7
+ ? { ...resolveConfigFromBinding(process.cwd()), session: opts.session }
8
+ : resolveConfigFromBinding(process.cwd());
9
+ return new ApiClient(config);
10
+ }
11
+ function resolveApp(opts, command, json) {
12
+ const resolved = requireApplicationId(opts.app, process.cwd());
13
+ if (!resolved.ok) {
14
+ if (json) {
15
+ printJson(jsonEnvelope(command, { result: "error", error: resolved.message }));
16
+ }
17
+ else {
18
+ console.error(`zivis ${command}: ${resolved.message}`);
19
+ }
20
+ process.exit(EXIT.INVALID_OR_INDETERMINATE);
21
+ }
22
+ return resolved.id;
23
+ }
24
+ function fmtScore(v) {
25
+ return v === null ? "—" : v.toFixed(2);
26
+ }
27
+ function fmtPct(v) {
28
+ return v === null ? "—" : `${Math.round(v * 100)}%`;
29
+ }
30
+ export function registerAssuranceCommands(program) {
31
+ const assurance = program.command("assurance").description("read current assurance/ZAM posture and issued Trust Marks");
32
+ assurance
33
+ .command("status")
34
+ .description("current Application/ZAM posture — live working state, not a signed claim")
35
+ .option("--app <id>", "Application id (defaults to the bound Application)")
36
+ .option("--model <zamId>", "assurance model key — required when more than one model is bound to the Application")
37
+ .option("--category <categoryId>", "show only this category")
38
+ .option("--provenance", "include evaluation/provenance detail in human-readable output")
39
+ .option("-s, --session <name>", "named session")
40
+ .option("--json", "emit JSON only on stdout")
41
+ .action(async (opts) => {
42
+ const json = !!opts.json;
43
+ const appId = resolveApp(opts, "assurance status", json);
44
+ const client = buildClient(opts);
45
+ try {
46
+ const query = new URLSearchParams({ applicationId: appId });
47
+ if (opts.model)
48
+ query.set("model", opts.model);
49
+ if (opts.category)
50
+ query.set("category", opts.category);
51
+ const status = await client.get(`/api/assurance/status?${query.toString()}`);
52
+ if (json) {
53
+ printJson(jsonEnvelope("assurance status", { result: "ok", ...status }));
54
+ return;
55
+ }
56
+ console.log(`Model: ${status.model.displayName} (${status.model.key})${status.model.version ? ` v${status.model.version}` : ""}`);
57
+ if (!status.evaluatedAt) {
58
+ console.log("No completed evaluation for this Application/model yet.");
59
+ return;
60
+ }
61
+ console.log(`Overall: score_raw=${fmtScore(status.overall.scoreRaw)} coverage=${fmtPct(status.overall.coverage)} (${status.overall.determinedCount}/${status.overall.totalCount} determined)`);
62
+ for (const c of status.categories) {
63
+ console.log(` ${c.categoryId.padEnd(16)} score_raw=${fmtScore(c.scoreRaw)} coverage=${fmtPct(c.coverage)}`);
64
+ }
65
+ console.log(`Risk: ${status.risk.openCritical} open critical, ${status.risk.openHigh} open high`);
66
+ console.log(`Evaluated: ${status.evaluatedAt}`);
67
+ if (opts.provenance) {
68
+ console.log(`Evaluation job: ${status.evaluationJobId ?? "—"}`);
69
+ console.log(`Last evaluated git SHA: ${status.lastEvaluatedGitSha ?? "unknown"}`);
70
+ console.log(`Framework id: ${status.model.frameworkId ?? "—"}`);
71
+ }
72
+ }
73
+ catch (err) {
74
+ if (isAmbiguousModelError(err)) {
75
+ return handleAmbiguous("assurance status", err, json);
76
+ }
77
+ failCommand("assurance status", err, json);
78
+ }
79
+ });
80
+ assurance
81
+ .command("mark [markId]")
82
+ .description("read an issued Trust Mark/ZAT — a signed snapshot, distinct from live status")
83
+ .option("--latest", "resolve the latest mark for the bound Application")
84
+ .option("--app <id>", "Application id (with --latest; defaults to the bound Application)")
85
+ .option("--model <zamId>", "assurance model key — required when more than one model has a mark for the Application")
86
+ .option("--raw", "include the full signed token in the output")
87
+ .option("-s, --session <name>", "named session")
88
+ .option("--json", "emit JSON only on stdout")
89
+ .action(async (markId, opts) => {
90
+ const json = !!opts.json;
91
+ const client = buildClient(opts);
92
+ if (!markId && !opts.latest) {
93
+ const message = "Pass a markId, or use --latest to resolve the current Application's latest mark.";
94
+ if (json)
95
+ printJson(jsonEnvelope("assurance mark", { result: "error", error: message }));
96
+ else
97
+ console.error(`zivis assurance mark: ${message}`);
98
+ process.exit(EXIT.INVALID_OR_INDETERMINATE);
99
+ }
100
+ try {
101
+ let mark;
102
+ if (markId) {
103
+ const query = opts.raw ? "?raw=true" : "";
104
+ mark = await client.get(`/api/zat/marks/${encodeURIComponent(markId)}${query}`);
105
+ }
106
+ else {
107
+ const appId = resolveApp(opts, "assurance mark", json);
108
+ const query = new URLSearchParams({ applicationId: appId });
109
+ if (opts.model)
110
+ query.set("model", opts.model);
111
+ if (opts.raw)
112
+ query.set("raw", "true");
113
+ mark = await client.get(`/api/zat/marks/latest?${query.toString()}`);
114
+ }
115
+ if (json) {
116
+ printJson(jsonEnvelope("assurance mark", { result: "ok", ...mark }));
117
+ return;
118
+ }
119
+ console.log(`Mark: ${mark.mark_id}`);
120
+ console.log(`Issuer: ${mark.issuer} Subject: ${mark.subject}`);
121
+ const fw = mark.framework;
122
+ if (fw) {
123
+ console.log(`Framework: ${fw.id} v${fw.version} basis=${fw.basis}`);
124
+ }
125
+ console.log(`Score: ${fmtScore(mark.score_raw)} (display ${mark.score_display ?? "—"}) tier=${mark.tier_label ?? "—"} scoring_profile=${mark.scoring_profile ?? "—"}`);
126
+ const sig = mark.signature;
127
+ console.log(`Signature: ${sig?.alg ?? "—"} — ${sig?.verified ? "VERIFIED" : "NOT VERIFIED"}`);
128
+ console.log(`Issued: ${mark.issued_at} Expires: ${mark.expires_at}${mark.expired ? " (EXPIRED)" : ""}${mark.revoked ? " (REVOKED)" : ""}`);
129
+ }
130
+ catch (err) {
131
+ if (isAmbiguousModelError(err)) {
132
+ return handleAmbiguous("assurance mark", err, json);
133
+ }
134
+ failCommand("assurance mark", err, json);
135
+ }
136
+ });
137
+ }
138
+ function isAmbiguousModelError(err) {
139
+ return err instanceof ApiError && err.status === 409;
140
+ }
141
+ function handleAmbiguous(command, err, json) {
142
+ if (json) {
143
+ printJson(jsonEnvelope(command, { result: "error", error: "ambiguous_model", message: err.message }));
144
+ }
145
+ else {
146
+ console.error(`zivis ${command}: ${err.message}`);
147
+ }
148
+ process.exit(EXIT.INVALID_OR_INDETERMINATE);
149
+ }
@@ -19,6 +19,7 @@ export function registerAuthCommands(program) {
19
19
  .description("interactive setup: authenticate, pick org, and bind this repo")
20
20
  .option("-s, --session <name>", "named session for multi-tenant")
21
21
  .option("--skip-mint-key", "skip MCP API key mint after successful OAuth")
22
+ .option("--application <name-or-id>", "select an existing Application by id or exact name (fails if not found — never creates one)")
22
23
  .action(async (opts) => {
23
24
  await initCommand(opts);
24
25
  });
@@ -1,5 +1,6 @@
1
1
  export interface InitCmdOpts {
2
2
  session?: string;
3
3
  skipMintKey?: boolean;
4
+ application?: string;
4
5
  }
5
6
  export declare function initCommand(opts: InitCmdOpts): Promise<void>;
@@ -4,6 +4,7 @@ import { credentialStorageFromConfig, DEFAULT_CONFIG } from "@zivis/mcp/types";
4
4
  import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
5
5
  import { fetchUserOrgs, extractEmail } from "../../internal/orgs.js";
6
6
  import { ensureProjectBinding } from "../../internal/project-binding.js";
7
+ import { ensureApplicationBinding } from "../../internal/application-binding.js";
7
8
  import { discardOauthAfterLogin, tryAutoMintBindingTokenAfterLogin, } from "../../internal/binding-token-mint.js";
8
9
  import { decodeJwtPayload } from "@zivis/mcp/auth/jwt";
9
10
  import { detectIdesInRepo, writeGuidance } from "../../internal/ide-setup.js";
@@ -28,26 +29,26 @@ async function prompt(question) {
28
29
  }
29
30
  async function offerGuidanceInstall(cwd) {
30
31
  const detectedIdes = detectIdesInRepo(cwd);
31
- if (detectedIdes.length === 0)
32
- return;
32
+ const otherIdes = detectedIdes.filter((ide) => ide !== "claude-code-cli");
33
+ const targets = ["claude-code-cli", ...otherIdes];
33
34
  const ideLabels = {
34
35
  cursor: "Cursor",
35
- "claude-code-cli": "Claude Code (CLAUDE.md)",
36
+ "claude-code-cli": "Claude Code (CLAUDE.md) / Codex (AGENTS.md)",
36
37
  "vscode-copilot": "VS Code (Copilot)",
37
38
  "vscode-claude": "VS Code (Claude)",
38
39
  cline: "Cline",
39
40
  windsurf: "Windsurf",
40
41
  };
41
- const ideNames = detectedIdes.map((ide) => ideLabels[ide] ?? ide).join(", ");
42
- console.log(`\nDetected IDE configs in this repo: ${ideNames}`);
43
- const ans = await prompt("Add ZIVIS guidance so your AI assistant suggests security checks at the right moments?\n" +
44
- "(Adds a rule/instruction file. You can edit or remove it anytime.) [Y/n] ");
42
+ const ideNames = targets.map((ide) => ideLabels[ide] ?? ide).join(", ");
43
+ console.log(`\nSet up AI agent guidance for: ${ideNames}`);
44
+ const ans = await prompt("Add ZIVIS guidance so your AI agent knows to run `zivis test` / `zivis threatmodel` when asked?\n" +
45
+ "(Adds a small managed block your own content is preserved, remove anytime with `zivis mcp uninstall`.) [Y/n] ");
45
46
  if (ans.toLowerCase() === "n" || ans.toLowerCase() === "no") {
46
47
  console.log(" Skipped. Run 'zivis mcp install --client <ide> --with-guidance' anytime to add later.");
47
48
  return;
48
49
  }
49
50
  const written = [];
50
- for (const ide of detectedIdes) {
51
+ for (const ide of targets) {
51
52
  try {
52
53
  const files = writeGuidance({ cwd, client: ide });
53
54
  written.push(...files);
@@ -148,6 +149,7 @@ export async function initCommand(opts) {
148
149
  config, session, accessToken: retryTokens.access_token,
149
150
  organizationId: retryOrgId, cwd: process.cwd(), autoBind: true, skipBind: false, force: true,
150
151
  });
152
+ await ensureApplicationBinding({ config, accessToken: retryTokens.access_token, cwd: process.cwd(), explicitApplication: opts.application });
151
153
  await tryAutoMintBindingTokenAfterLogin({
152
154
  cwd: process.cwd(),
153
155
  sessionName: session,
@@ -177,6 +179,7 @@ export async function initCommand(opts) {
177
179
  skipBind: false,
178
180
  force: true,
179
181
  });
182
+ await ensureApplicationBinding({ config, accessToken: tokens.access_token, cwd: process.cwd(), explicitApplication: opts.application });
180
183
  await tryAutoMintBindingTokenAfterLogin({
181
184
  cwd: process.cwd(),
182
185
  sessionName: session,
@@ -241,6 +244,7 @@ export async function initCommand(opts) {
241
244
  skipBind: false,
242
245
  force: true,
243
246
  });
247
+ await ensureApplicationBinding({ config, accessToken: scopedTokens.access_token, cwd: process.cwd(), explicitApplication: opts.application });
244
248
  await tryAutoMintBindingTokenAfterLogin({
245
249
  cwd: process.cwd(),
246
250
  sessionName: session,
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerGateCommand(program: Command): void;
@@ -0,0 +1,171 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+ import { ApiClient, ApiError } from "@zivis/mcp/api-client";
5
+ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
6
+ import { requireApplicationId } from "@zivis/mcp/resolve-application-id";
7
+ import { getCurrentGitSha } from "../../internal/git.js";
8
+ import { parsePolicyFile, PolicyValidationError } from "../../internal/gate-policy.js";
9
+ import { evaluateGate } from "../../internal/gate-evaluate.js";
10
+ import { jsonEnvelope, printJson, EXIT, exitCodeForError, errorMessage } from "../../internal/cli-output.js";
11
+ const DEFAULT_POLICY_PATH = ".zivis/policy.yaml";
12
+ export function registerGateCommand(program) {
13
+ program
14
+ .command("gate [name]")
15
+ .description("evaluate a committed release/ship policy deterministically against current Zivis state")
16
+ .option("--file <path>", `policy file path (default: ${DEFAULT_POLICY_PATH})`)
17
+ .option("--app <id>", "Application id (defaults to the bound Application)")
18
+ .option("--model <zamId>", "ad-hoc form: assurance model key")
19
+ .option("--category <categoryId>", "ad-hoc form: category id to threshold")
20
+ .option("--min-score <0..1>", "ad-hoc form: minimum normalized score_raw")
21
+ .option("--min-coverage <0..1>", "ad-hoc form: minimum coverage")
22
+ .option("-s, --session <name>", "named session")
23
+ .option("--json", "emit JSON only on stdout")
24
+ .action(async (name, opts) => {
25
+ const json = !!opts.json;
26
+ try {
27
+ const { gateName, policy, policyHash } = await resolvePolicy(name, opts);
28
+ const config = opts.session
29
+ ? { ...resolveConfigFromBinding(process.cwd()), session: opts.session }
30
+ : resolveConfigFromBinding(process.cwd());
31
+ const client = new ApiClient(config);
32
+ const resolvedApp = requireApplicationId(opts.app, process.cwd());
33
+ if (!resolvedApp.ok) {
34
+ throw new PolicyValidationError(resolvedApp.message);
35
+ }
36
+ const applicationId = resolvedApp.id;
37
+ const [currentGitSha, statusResult] = await Promise.all([
38
+ getCurrentGitSha(process.cwd()),
39
+ fetchStatus(client, applicationId, policy),
40
+ ]);
41
+ let mark = null;
42
+ let markLookupFailed = false;
43
+ let markId = null;
44
+ let assessorVersion = null;
45
+ if (policy.provenance) {
46
+ try {
47
+ const query = new URLSearchParams({ applicationId, model: policy.model });
48
+ const raw = await client.get(`/api/zat/marks/latest?${query.toString()}`);
49
+ markId = raw.mark_id ?? null;
50
+ const methodology = raw.methodology;
51
+ assessorVersion = methodology?.assessor_version ?? null;
52
+ mark = {
53
+ basis: raw.framework?.basis,
54
+ expired: !!raw.expired,
55
+ revoked: !!raw.revoked,
56
+ signature: { verified: !!raw.signature?.verified },
57
+ };
58
+ }
59
+ catch (err) {
60
+ if (!(err instanceof ApiError && err.status === 404)) {
61
+ markLookupFailed = true;
62
+ }
63
+ }
64
+ }
65
+ const evaluation = evaluateGate({
66
+ policy,
67
+ status: statusResult,
68
+ mark,
69
+ markLookupFailed,
70
+ currentGitSha,
71
+ now: new Date(),
72
+ });
73
+ const output = jsonEnvelope("gate", {
74
+ gate: gateName,
75
+ result: evaluation.result,
76
+ application_id: applicationId,
77
+ git_sha: currentGitSha ?? null,
78
+ policy_hash: policyHash,
79
+ evaluated_at: new Date().toISOString(),
80
+ scoring_profile: policy.scoring_profile ?? null,
81
+ checks: evaluation.checks,
82
+ provenance: {
83
+ source: policy.source,
84
+ latest_mark_id: markId,
85
+ basis: mark?.basis ?? null,
86
+ assessor_version: assessorVersion,
87
+ last_evaluated_sha: statusResult?.lastEvaluatedGitSha ?? null,
88
+ },
89
+ });
90
+ if (json) {
91
+ printJson(output);
92
+ }
93
+ else {
94
+ printHuman(gateName, evaluation, output);
95
+ }
96
+ process.exit(evaluation.result === "pass" ? EXIT.OK : evaluation.result === "fail" ? EXIT.POLICY_FAIL : EXIT.INVALID_OR_INDETERMINATE);
97
+ }
98
+ catch (err) {
99
+ const code = err instanceof PolicyValidationError ? EXIT.INVALID_OR_INDETERMINATE : exitCodeForError(err);
100
+ const message = errorMessage(err);
101
+ if (json) {
102
+ printJson(jsonEnvelope("gate", { gate: name ?? null, result: "indeterminate", error: message }));
103
+ }
104
+ else {
105
+ console.error(`zivis gate: ${message}`);
106
+ }
107
+ process.exit(code);
108
+ }
109
+ });
110
+ }
111
+ async function resolvePolicy(name, opts) {
112
+ const isAdHoc = !!(opts.model && opts.category && opts.minScore !== undefined);
113
+ if (isAdHoc) {
114
+ const minScore = Number(opts.minScore);
115
+ if (Number.isNaN(minScore) || minScore < 0 || minScore > 1) {
116
+ throw new PolicyValidationError("--min-score must be a number between 0.0 and 1.0.");
117
+ }
118
+ const policy = {
119
+ source: "current",
120
+ model: opts.model,
121
+ assurance: { categories: [{ id: opts.category, min_score_raw: minScore }] },
122
+ };
123
+ if (opts.minCoverage !== undefined) {
124
+ const minCoverage = Number(opts.minCoverage);
125
+ if (Number.isNaN(minCoverage) || minCoverage < 0 || minCoverage > 1) {
126
+ throw new PolicyValidationError("--min-coverage must be a number between 0.0 and 1.0.");
127
+ }
128
+ policy.assurance.categories[0].min_coverage = minCoverage;
129
+ }
130
+ return { gateName: "ad-hoc", policy, policyHash: "sha256:" + crypto.createHash("sha256").update(JSON.stringify(policy)).digest("hex") };
131
+ }
132
+ if (!name) {
133
+ throw new PolicyValidationError("A gate name is required (e.g. `zivis gate ship`), or pass --model/--category/--min-score for the ad-hoc debugging form.");
134
+ }
135
+ const filePath = path.resolve(process.cwd(), opts.file ?? DEFAULT_POLICY_PATH);
136
+ let raw;
137
+ try {
138
+ raw = await fs.readFile(filePath, "utf8");
139
+ }
140
+ catch {
141
+ throw new PolicyValidationError(`Policy file not found: ${filePath}`);
142
+ }
143
+ const parsed = parsePolicyFile(raw);
144
+ const policy = parsed.gates[name];
145
+ if (!policy) {
146
+ const available = Object.keys(parsed.gates).join(", ") || "(none defined)";
147
+ throw new PolicyValidationError(`Gate '${name}' is not defined in ${filePath}. Available gates: ${available}`);
148
+ }
149
+ const policyHash = "sha256:" + crypto.createHash("sha256").update(raw).digest("hex");
150
+ return { gateName: name, policy, policyHash };
151
+ }
152
+ async function fetchStatus(client, applicationId, policy) {
153
+ const query = new URLSearchParams({ applicationId, model: policy.model });
154
+ try {
155
+ return await client.get(`/api/assurance/status?${query.toString()}`);
156
+ }
157
+ catch (err) {
158
+ if (err instanceof ApiError && err.status === 404)
159
+ return null;
160
+ throw err;
161
+ }
162
+ }
163
+ function printHuman(gateName, evaluation, _full) {
164
+ const symbol = evaluation.result === "pass" ? "✓" : evaluation.result === "fail" ? "✗" : "?";
165
+ console.log(`Gate '${gateName}': ${symbol} ${evaluation.result.toUpperCase()}`);
166
+ for (const c of evaluation.checks) {
167
+ const label = c.category_id ? `${c.type}[${c.category_id}]` : c.type;
168
+ const mark = c.result === "pass" ? "✓" : c.result === "fail" ? "✗" : "?";
169
+ console.log(` ${mark} ${label}: actual=${JSON.stringify(c.actual)} required=${JSON.stringify(c.required)}`);
170
+ }
171
+ }
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import * as readline from "node:readline/promises";
3
3
  import { startServer } from "@zivis/mcp/server";
4
4
  import { DEFAULT_CONFIG } from "@zivis/mcp/types";
5
- import { setupCursorWorkspace, setupVsCodeCopilot, setupVsCodeClaude, setupClaudeCodeCli, setupCline, setupWindsurf, printClaudeCodeManual, writeGuidance, } from "../../internal/ide-setup.js";
5
+ import { setupCursorWorkspace, setupVsCodeCopilot, setupVsCodeClaude, setupClaudeCodeCli, setupCline, setupWindsurf, printClaudeCodeManual, writeGuidance, removeGuidance, } from "../../internal/ide-setup.js";
6
6
  const KNOWN_CLIENTS = [
7
7
  "cursor",
8
8
  "claude-code",
@@ -134,4 +134,36 @@ export function registerMcpCommands(program) {
134
134
  }
135
135
  }
136
136
  });
137
+ mcp
138
+ .command("uninstall")
139
+ .description("remove ONLY the ZIVIS-managed guidance block/files for a client (run from your project root)")
140
+ .option("--client <name>", `target client (${KNOWN_CLIENTS.join(" | ")})`)
141
+ .action(async (opts) => {
142
+ const client = opts.client?.toLowerCase();
143
+ if (!client) {
144
+ console.error("Specify a client with --client <name>:");
145
+ for (const c of KNOWN_CLIENTS)
146
+ console.error(` zivis mcp uninstall --client ${c}`);
147
+ process.exit(1);
148
+ }
149
+ const guidanceClient = toGuidanceClient(client);
150
+ if (!guidanceClient) {
151
+ console.log(`Nothing to remove for "${client}" — no guidance files are written for this client.`);
152
+ return;
153
+ }
154
+ try {
155
+ const removed = removeGuidance({ cwd: process.cwd(), client: guidanceClient });
156
+ if (removed.length === 0) {
157
+ console.log("No ZIVIS-managed guidance found — nothing to remove.");
158
+ return;
159
+ }
160
+ console.log(`✓ Removed ZIVIS guidance:`);
161
+ for (const f of removed)
162
+ console.log(` ${f}`);
163
+ }
164
+ catch (err) {
165
+ console.error("Uninstall failed:", err instanceof Error ? err.message : err);
166
+ process.exit(1);
167
+ }
168
+ });
137
169
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerRunCommands(program: Command): void;
@@ -0,0 +1,111 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { ApiClient } from "@zivis/mcp/api-client";
3
+ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
4
+ import { DEVX_RUN_CONTRACT_VERSION, completeDevXRunRemote, cancelDevXRunRemote, isRetryableDevXRunError, } from "../../internal/devx-run.js";
5
+ import { writeDevXRunCompleteOutboxEntry } from "../../internal/sync-outbox.js";
6
+ import { jsonEnvelope, printJson, EXIT, errorMessage, failCommand } from "../../internal/cli-output.js";
7
+ async function readInput(inputArg) {
8
+ const raw = inputArg === "-" ? await readStdin() : await fs.readFile(inputArg, "utf-8");
9
+ try {
10
+ return JSON.parse(raw);
11
+ }
12
+ catch (err) {
13
+ throw new Error(`--input is not valid JSON: ${errorMessage(err)}`);
14
+ }
15
+ }
16
+ async function readStdin() {
17
+ let data = "";
18
+ process.stdin.setEncoding("utf-8");
19
+ for await (const chunk of process.stdin) {
20
+ data += chunk;
21
+ }
22
+ return data;
23
+ }
24
+ export function registerRunCommands(program) {
25
+ const run = program.command("run").description("resolve a DevX run started by a run producer (e.g. `zivis test`)");
26
+ run
27
+ .command("complete <runId>")
28
+ .description("complete a pending run with its result envelope")
29
+ .requiredOption("--input <path>", "path to a JSON result file, or '-' for stdin")
30
+ .option("--json", "emit JSON only on stdout")
31
+ .action(async (runId, opts) => {
32
+ const json = !!opts.json;
33
+ let result;
34
+ try {
35
+ result = await readInput(opts.input);
36
+ }
37
+ catch (err) {
38
+ failCommand("run complete", err, json);
39
+ }
40
+ const config = resolveConfigFromBinding(process.cwd());
41
+ const client = new ApiClient(config);
42
+ try {
43
+ const response = await completeDevXRunRemote(client, {
44
+ runId,
45
+ contractVersion: DEVX_RUN_CONTRACT_VERSION,
46
+ result,
47
+ });
48
+ const output = jsonEnvelope("run complete", {
49
+ run_id: runId,
50
+ outcome: response.outcome,
51
+ status: response.run.status,
52
+ completed_at: response.run.completedAt,
53
+ });
54
+ if (json) {
55
+ printJson(output);
56
+ }
57
+ else {
58
+ console.log(`zivis run complete: ${response.outcome} (run ${runId} is now ${response.run.status})`);
59
+ }
60
+ process.exit(EXIT.OK);
61
+ }
62
+ catch (err) {
63
+ if (isRetryableDevXRunError(err)) {
64
+ const message = errorMessage(err);
65
+ const outboxPath = writeDevXRunCompleteOutboxEntry(process.cwd(), {
66
+ runId,
67
+ contractVersion: DEVX_RUN_CONTRACT_VERSION,
68
+ result,
69
+ error: message,
70
+ });
71
+ if (json) {
72
+ printJson(jsonEnvelope("run complete", { run_id: runId, outcome: "queued", queued_at: outboxPath, error: message }));
73
+ }
74
+ else {
75
+ console.error(`zivis run complete: Cloud unreachable (${message})\nQueued for retry: ${outboxPath}\nRun \`zivis sync --flush\` once the issue is resolved.`);
76
+ }
77
+ process.exit(EXIT.CLOUD_UNAVAILABLE);
78
+ }
79
+ failCommand("run complete", err, json);
80
+ }
81
+ });
82
+ run
83
+ .command("cancel <runId>")
84
+ .description("cancel a pending run")
85
+ .option("--reason <text>", "optional human-readable cancellation reason")
86
+ .option("--json", "emit JSON only on stdout")
87
+ .action(async (runId, opts) => {
88
+ const json = !!opts.json;
89
+ const config = resolveConfigFromBinding(process.cwd());
90
+ const client = new ApiClient(config);
91
+ try {
92
+ const response = await cancelDevXRunRemote(client, { runId, reason: opts.reason ?? null });
93
+ const output = jsonEnvelope("run cancel", {
94
+ run_id: runId,
95
+ outcome: response.outcome,
96
+ status: response.run.status,
97
+ cancelled_at: response.run.cancelledAt,
98
+ });
99
+ if (json) {
100
+ printJson(output);
101
+ }
102
+ else {
103
+ console.log(`zivis run cancel: ${response.outcome} (run ${runId} is now ${response.run.status})`);
104
+ }
105
+ process.exit(EXIT.OK);
106
+ }
107
+ catch (err) {
108
+ failCommand("run cancel", err, json);
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerSyncCommands(program: Command): void;