@zivis/cli 0.1.0-alpha.42 → 0.1.0-alpha.43

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.
@@ -9,6 +9,7 @@ import { discardOauthAfterLogin, tryAutoMintBindingTokenAfterLogin, } from "../.
9
9
  import { decodeJwtPayload } from "@zivis/mcp/auth/jwt";
10
10
  import { detectIdesInRepo, writeGuidance } from "../../internal/ide-setup.js";
11
11
  import { buildLinkagePrompt } from "../../internal/github-linkage-prompt.js";
12
+ import { resolveExistingSessionTokens } from "../../internal/resolve-init-session.js";
12
13
  async function maybePromptGitHubLinkage(config) {
13
14
  try {
14
15
  const msg = await buildLinkagePrompt({ config });
@@ -72,15 +73,23 @@ export async function initCommand(opts) {
72
73
  const envLabel = config.apiBaseUrl.includes("staging") ? "staging" : "production";
73
74
  console.log(`Environment: ${envLabel} (${config.apiBaseUrl})`);
74
75
  console.log("");
75
- console.log("Opening browser for authentication...");
76
- console.log("Log into the org you want to bind this project to.\n");
77
76
  let tokens;
78
- try {
79
- tokens = await login(config, session, undefined, { skipConfirm: true });
77
+ const existing = await resolveExistingSessionTokens(config, session);
78
+ if (existing) {
79
+ console.log("Existing session found — reusing stored credentials.");
80
+ console.log("(Run `zivis auth logout` first to switch accounts.)\n");
81
+ tokens = existing;
80
82
  }
81
- catch (err) {
82
- console.error("Login failed:", err instanceof Error ? err.message : err);
83
- process.exit(1);
83
+ else {
84
+ console.log("Opening browser for authentication...");
85
+ console.log("Log into the org you want to bind this project to.\n");
86
+ try {
87
+ tokens = await login(config, session, undefined, { skipConfirm: true });
88
+ }
89
+ catch (err) {
90
+ console.error("Login failed:", err instanceof Error ? err.message : err);
91
+ process.exit(1);
92
+ }
84
93
  }
85
94
  const email = extractEmail(tokens.id_token);
86
95
  const claims = decodeJwtPayload(tokens.access_token);
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerThreatmodelCommand(program: Command): void;
@@ -0,0 +1,166 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as fsSync from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ApiClient } from "@zivis/mcp/api-client";
6
+ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
7
+ import { requireApplicationId } from "@zivis/mcp/resolve-application-id";
8
+ import { renderSecurityContext } from "../../internal/security-context.js";
9
+ import { startThreatModelRun, syncThreatModel, } from "../../internal/threatmodel-run.js";
10
+ import { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride } from "../../internal/packs/registry-client.js";
11
+ import { PackUnavailableError, PackIntegrityError } from "../../internal/packs/types.js";
12
+ import { jsonEnvelope, printJson, EXIT, exitCodeForError, errorMessage, failCommand } from "../../internal/cli-output.js";
13
+ class UsageError extends Error {
14
+ }
15
+ function buildRegistryClient(apiClient) {
16
+ const override = registryDirOverride();
17
+ if (override)
18
+ return new LocalPackRegistryClient(override);
19
+ return new HttpPackRegistryClient(apiClient);
20
+ }
21
+ function exitCodeForThreatModelError(err) {
22
+ if (err instanceof PackIntegrityError)
23
+ return EXIT.INVALID_OR_INDETERMINATE;
24
+ if (err instanceof PackUnavailableError)
25
+ return EXIT.CLOUD_UNAVAILABLE;
26
+ return exitCodeForError(err);
27
+ }
28
+ async function readMarkdownInput(inputArg) {
29
+ if (inputArg === "-")
30
+ return readStdin();
31
+ const content = await fs.readFile(inputArg, "utf-8");
32
+ if (content.trim().length === 0) {
33
+ throw new UsageError(`--input file "${inputArg}" is empty.`);
34
+ }
35
+ return content;
36
+ }
37
+ async function readStdin() {
38
+ let data = "";
39
+ process.stdin.setEncoding("utf-8");
40
+ for await (const chunk of process.stdin) {
41
+ data += chunk;
42
+ }
43
+ if (data.trim().length === 0) {
44
+ throw new UsageError("--input - read no content from stdin.");
45
+ }
46
+ return data;
47
+ }
48
+ export function registerThreatmodelCommand(program) {
49
+ const threatmodel = program
50
+ .command("threatmodel")
51
+ .description("start (or continue) a Zivis-guided living threat model for the bound Application")
52
+ .option("--app <id>", "Application id (defaults to the bound Application)")
53
+ .option("--json", "emit JSON only on stdout")
54
+ .action(async (opts) => {
55
+ const json = !!opts.json;
56
+ const cwd = process.cwd();
57
+ const config = resolveConfigFromBinding(cwd);
58
+ const apiClient = new ApiClient(config);
59
+ const registryClient = buildRegistryClient(apiClient);
60
+ const cliVersion = getCliVersion();
61
+ try {
62
+ const resolvedApp = requireApplicationId(opts.app, cwd);
63
+ if (!resolvedApp.ok) {
64
+ throw new UsageError(resolvedApp.message);
65
+ }
66
+ const { envelope, contextOutcome } = await startThreatModelRun({ apiClient, registryClient }, { applicationId: resolvedApp.id, cwd, cliVersion });
67
+ const output = jsonEnvelope("threatmodel", { ...envelope });
68
+ if (json) {
69
+ printJson(output);
70
+ }
71
+ else {
72
+ printStartHuman(envelope, contextOutcome);
73
+ }
74
+ process.exit(EXIT.OK);
75
+ }
76
+ catch (err) {
77
+ const code = err instanceof UsageError ? EXIT.INVALID_OR_INDETERMINATE : exitCodeForThreatModelError(err);
78
+ const message = errorMessage(err);
79
+ if (json) {
80
+ printJson(jsonEnvelope("threatmodel", { result: "error", error: message }));
81
+ }
82
+ else {
83
+ console.error(`zivis threatmodel: ${message}`);
84
+ }
85
+ process.exit(code);
86
+ }
87
+ });
88
+ threatmodel
89
+ .command("sync")
90
+ .description("persist the finished Markdown threat model as a new Artifact revision and complete the run")
91
+ .requiredOption("--input <path>", "path to the finished Markdown threat model, or '-' for stdin")
92
+ .option("--run <runId>", "override the auto-resolved active threatmodel run")
93
+ .option("--change-note <text>", "optional human-readable summary of what changed")
94
+ .option("--app <id>", "Application id (defaults to the bound Application)")
95
+ .option("--json", "emit JSON only on stdout")
96
+ .action(async (opts) => {
97
+ const json = !!opts.json;
98
+ const cwd = process.cwd();
99
+ let content;
100
+ try {
101
+ content = await readMarkdownInput(opts.input);
102
+ }
103
+ catch (err) {
104
+ failCommand("threatmodel sync", err, json);
105
+ }
106
+ const config = resolveConfigFromBinding(cwd);
107
+ const apiClient = new ApiClient(config);
108
+ const registryClient = buildRegistryClient(apiClient);
109
+ try {
110
+ const resolvedApp = requireApplicationId(opts.app, cwd);
111
+ if (!resolvedApp.ok) {
112
+ throw new UsageError(resolvedApp.message);
113
+ }
114
+ const result = await syncThreatModel({ apiClient, registryClient }, { applicationId: resolvedApp.id, runId: opts.run, cwd, content, changeNote: opts.changeNote });
115
+ const output = jsonEnvelope("threatmodel sync", {
116
+ run_id: result.runId,
117
+ run_outcome: result.runOutcome,
118
+ evidence_id: result.evidenceId,
119
+ version: result.version,
120
+ unchanged: result.unchanged,
121
+ });
122
+ if (json) {
123
+ printJson(output);
124
+ }
125
+ else {
126
+ console.log(result.unchanged
127
+ ? `zivis threatmodel sync: no change (content identical to revision ${result.version}); run ${result.runId} ${result.runOutcome}.`
128
+ : `zivis threatmodel sync: created revision ${result.version} (evidence ${result.evidenceId}); run ${result.runId} ${result.runOutcome}.`);
129
+ }
130
+ process.exit(EXIT.OK);
131
+ }
132
+ catch (err) {
133
+ const code = err instanceof UsageError ? EXIT.INVALID_OR_INDETERMINATE : exitCodeForThreatModelError(err);
134
+ const message = errorMessage(err);
135
+ if (json) {
136
+ printJson(jsonEnvelope("threatmodel sync", { result: "error", error: message }));
137
+ }
138
+ else {
139
+ console.error(`zivis threatmodel sync: ${message}`);
140
+ }
141
+ process.exit(code);
142
+ }
143
+ });
144
+ }
145
+ function getCliVersion() {
146
+ try {
147
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
148
+ const pkg = JSON.parse(fsSync.readFileSync(path.join(__dirname, "../../../package.json"), "utf-8"));
149
+ return pkg.version;
150
+ }
151
+ catch {
152
+ return "0.0.0";
153
+ }
154
+ }
155
+ function printStartHuman(envelope, contextOutcome) {
156
+ console.log(`zivis threatmodel: started run ${envelope.run_id}`);
157
+ console.log(renderSecurityContext(contextOutcome, "human"));
158
+ console.log(`\nMethodology: ${envelope.methodology.path}`);
159
+ if (envelope.prior_model?.exists) {
160
+ console.log(`Prior model: ${envelope.prior_model.path} (revision ${envelope.prior_model.version}, last modeled at ${envelope.prior_model.git_commit_sha ?? "unknown"})`);
161
+ }
162
+ else {
163
+ console.log("Prior model: none on record — this will be the first threat model for this Application.");
164
+ }
165
+ console.log(`\nWhen finished: ${envelope.result_contract.complete_command}`);
166
+ }
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { registerAssuranceCommands } from "./commands/assurance/index.js";
19
19
  import { registerGateCommand } from "./commands/gate/index.js";
20
20
  import { registerRunCommands } from "./commands/run/index.js";
21
21
  import { registerTestCommand } from "./commands/test/index.js";
22
+ import { registerThreatmodelCommand } from "./commands/threatmodel/index.js";
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
24
  function getVersion() {
24
25
  try {
@@ -58,6 +59,7 @@ registerAssuranceCommands(program);
58
59
  registerGateCommand(program);
59
60
  registerRunCommands(program);
60
61
  registerTestCommand(program);
62
+ registerThreatmodelCommand(program);
61
63
  program.parseAsync(process.argv).catch((err) => {
62
64
  console.error(err instanceof Error ? err.message : err);
63
65
  process.exit(1);
@@ -0,0 +1,6 @@
1
+ import { type ZivisConfig } from "@zivis/mcp/types";
2
+ export interface ExistingSessionTokens {
3
+ access_token: string;
4
+ id_token?: string;
5
+ }
6
+ export declare function resolveExistingSessionTokens(config: ZivisConfig, session: string): Promise<ExistingSessionTokens | null>;
@@ -0,0 +1,10 @@
1
+ import { loadTokens } from "@zivis/mcp/auth";
2
+ import { getValidAccessToken } from "@zivis/mcp/auth/token-refresh";
3
+ import { credentialStorageFromConfig } from "@zivis/mcp/types";
4
+ export async function resolveExistingSessionTokens(config, session) {
5
+ const accessToken = await getValidAccessToken({ ...config, session });
6
+ if (!accessToken)
7
+ return null;
8
+ const stored = await loadTokens(session, credentialStorageFromConfig(config));
9
+ return { access_token: accessToken, id_token: stored?.id_token };
10
+ }
@@ -1 +1,2 @@
1
1
  export declare function writeRunWorkFile(cwd: string, runId: string, filename: string, data: unknown): string;
2
+ export declare function writeRunWorkTextFile(cwd: string, runId: string, filename: string, text: string): string;
@@ -9,3 +9,11 @@ export function writeRunWorkFile(cwd, runId, filename, data) {
9
9
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
10
10
  return filePath;
11
11
  }
12
+ export function writeRunWorkTextFile(cwd, runId, filename, text) {
13
+ ensureZivisGitignore(cwd);
14
+ const dir = path.join(cwd, ".zivis", "work", runId);
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ const filePath = path.join(dir, filename);
17
+ fs.writeFileSync(filePath, text);
18
+ return filePath;
19
+ }
@@ -0,0 +1,15 @@
1
+ export declare const THREATMODEL_RUN_STATE_FILENAME = "threatmodel-run.json";
2
+ export interface ThreatModelRunState {
3
+ runId: string;
4
+ runType: "threatmodel";
5
+ applicationId: string;
6
+ startedAt: string;
7
+ status: "pending" | "completed";
8
+ packId: string;
9
+ packType: string;
10
+ packVersion: string;
11
+ }
12
+ export declare function recordThreatModelRunState(cwd: string, state: ThreatModelRunState): string;
13
+ export declare function loadThreatModelRunState(cwd: string, runId: string): ThreatModelRunState | null;
14
+ export declare function markThreatModelRunCompleted(cwd: string, runId: string): void;
15
+ export declare function findActiveThreatModelRun(cwd: string): ThreatModelRunState | null;
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { writeRunWorkFile } from "./run-workdir.js";
4
+ export const THREATMODEL_RUN_STATE_FILENAME = "threatmodel-run.json";
5
+ export function recordThreatModelRunState(cwd, state) {
6
+ return writeRunWorkFile(cwd, state.runId, THREATMODEL_RUN_STATE_FILENAME, state);
7
+ }
8
+ function runStatePath(cwd, runId) {
9
+ return path.join(cwd, ".zivis", "work", runId, THREATMODEL_RUN_STATE_FILENAME);
10
+ }
11
+ export function loadThreatModelRunState(cwd, runId) {
12
+ const filePath = runStatePath(cwd, runId);
13
+ if (!fs.existsSync(filePath))
14
+ return null;
15
+ try {
16
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function markThreatModelRunCompleted(cwd, runId) {
23
+ const filePath = runStatePath(cwd, runId);
24
+ const existing = loadThreatModelRunState(cwd, runId);
25
+ if (!existing)
26
+ return;
27
+ fs.writeFileSync(filePath, JSON.stringify({ ...existing, status: "completed" }, null, 2) + "\n");
28
+ }
29
+ export function findActiveThreatModelRun(cwd) {
30
+ const workDir = path.join(cwd, ".zivis", "work");
31
+ if (!fs.existsSync(workDir))
32
+ return null;
33
+ const candidates = [];
34
+ for (const runId of fs.readdirSync(workDir)) {
35
+ const state = loadThreatModelRunState(cwd, runId);
36
+ if (state && state.status === "pending")
37
+ candidates.push(state);
38
+ }
39
+ if (candidates.length === 0)
40
+ return null;
41
+ candidates.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
42
+ return candidates[0];
43
+ }
@@ -0,0 +1,85 @@
1
+ import { type DevXRunApi } from "./devx-run.js";
2
+ import { type SecurityContextApi, type SecurityContextOutcome } from "./security-context.js";
3
+ import type { PackRegistryClient } from "./packs/types.js";
4
+ export interface ThreatModelArtifactApi {
5
+ get<T>(path: string): Promise<T>;
6
+ put<T>(path: string, body: unknown): Promise<T>;
7
+ }
8
+ export interface ThreatModelRunDeps {
9
+ apiClient: DevXRunApi & SecurityContextApi & ThreatModelArtifactApi;
10
+ registryClient: PackRegistryClient;
11
+ }
12
+ export interface StartThreatModelRunParams {
13
+ applicationId: string;
14
+ cwd: string;
15
+ cliVersion: string;
16
+ }
17
+ export type PriorThreatModelOutcome = {
18
+ status: "found";
19
+ content: string;
20
+ version: number;
21
+ gitCommitSha: string | null;
22
+ updatedAt: string;
23
+ updatedBy: string;
24
+ } | {
25
+ status: "none";
26
+ } | {
27
+ status: "unavailable";
28
+ reason: string;
29
+ };
30
+ export interface ThreatModelRunEnvelope {
31
+ run_id: string;
32
+ application: {
33
+ id: string;
34
+ };
35
+ connection: {
36
+ mode: "connected";
37
+ };
38
+ git: {
39
+ sha: string | null;
40
+ dirty: boolean | null;
41
+ };
42
+ methodology: {
43
+ pack: string;
44
+ version: string;
45
+ path: string;
46
+ };
47
+ context: {
48
+ path: string;
49
+ };
50
+ prior_model: {
51
+ exists: true;
52
+ version: number;
53
+ git_commit_sha: string | null;
54
+ path: string;
55
+ } | {
56
+ exists: false;
57
+ reason?: string;
58
+ } | null;
59
+ result_contract: {
60
+ schema_version: string;
61
+ complete_command: string;
62
+ };
63
+ agent_instructions: string[];
64
+ }
65
+ export interface StartThreatModelRunResult {
66
+ envelope: ThreatModelRunEnvelope;
67
+ contextOutcome: SecurityContextOutcome;
68
+ priorModel: PriorThreatModelOutcome;
69
+ }
70
+ export declare function startThreatModelRun(deps: ThreatModelRunDeps, params: StartThreatModelRunParams): Promise<StartThreatModelRunResult>;
71
+ export interface SyncThreatModelParams {
72
+ applicationId: string;
73
+ runId?: string;
74
+ cwd: string;
75
+ content: string;
76
+ changeNote?: string;
77
+ }
78
+ export interface SyncThreatModelResult {
79
+ evidenceId: string;
80
+ version: number;
81
+ unchanged: boolean;
82
+ runId: string;
83
+ runOutcome: "completed" | "cancelled" | "idempotent-replay" | "queued";
84
+ }
85
+ export declare function syncThreatModel(deps: ThreatModelRunDeps, params: SyncThreatModelParams): Promise<SyncThreatModelResult>;
@@ -0,0 +1,159 @@
1
+ import { ApiError } from "@zivis/mcp/api-client";
2
+ import { readGitMetadata } from "./git.js";
3
+ import { startDevXRun, completeDevXRunRemote, isRetryableDevXRunError, DEVX_RUN_CONTRACT_VERSION, } from "./devx-run.js";
4
+ import { fetchSecurityContext } from "./security-context.js";
5
+ import { resolveCompatiblePack } from "./packs/resolve.js";
6
+ import { writeRunWorkFile, writeRunWorkTextFile } from "./run-workdir.js";
7
+ import { writeDevXRunCompleteOutboxEntry } from "./sync-outbox.js";
8
+ import { recordThreatModelRunState, loadThreatModelRunState, findActiveThreatModelRun, markThreatModelRunCompleted, } from "./threatmodel-run-state.js";
9
+ async function fetchPriorThreatModel(client, applicationId) {
10
+ try {
11
+ const res = await client.get(`/api/rt/applications/${encodeURIComponent(applicationId)}/artifacts/threat_model`);
12
+ const evidence = res.evidence;
13
+ return {
14
+ status: "found",
15
+ content: res.content,
16
+ version: evidence.version,
17
+ gitCommitSha: evidence.gitCommitSha ?? null,
18
+ updatedAt: evidence.updatedAt,
19
+ updatedBy: evidence.updatedBy,
20
+ };
21
+ }
22
+ catch (err) {
23
+ if (err instanceof ApiError && err.status === 404)
24
+ return { status: "none" };
25
+ return { status: "unavailable", reason: err instanceof Error ? err.message : String(err) };
26
+ }
27
+ }
28
+ function buildAgentInstructions(prior) {
29
+ const common = [
30
+ "Use your own repository understanding, git, shell, and tools to perform the analysis — this pack describes the methodology, not the storage format.",
31
+ "When finished, write the Markdown threat model to a local file and run `zivis threatmodel sync --input <file>` to persist it. Do not run the artifact API or `zivis run complete` yourself — `sync` composes both internally.",
32
+ ];
33
+ if (prior.status === "found") {
34
+ return [
35
+ `Read the methodology at methodology.path, then the prior threat model at prior_model.path (last modeled at commit ${prior.gitCommitSha ?? "unknown"}).`,
36
+ `Inspect what materially changed between ${prior.gitCommitSha ?? "the prior commit"} and HEAD (git.sha) using git log/diff — architecture, components, data flows, trust boundaries, assumptions, attack paths, and test recommendations.`,
37
+ "Preserve valid existing analysis. Update only the sections materially affected by what changed — do not regenerate the model from scratch merely because a new run occurred.",
38
+ ...common,
39
+ ];
40
+ }
41
+ const noPriorReason = prior.status === "unavailable" ? ` (could not confirm: ${prior.reason} — proceed as if none exists, but note this in your output if relevant)` : "";
42
+ return [
43
+ `Read the methodology at methodology.path — no prior threat model is on record for this Application${noPriorReason}.`,
44
+ "Model actors, assets, trust boundaries, and data flows before enumerating threats, then identify assumptions, attack paths, and test recommendations.",
45
+ ...common,
46
+ ];
47
+ }
48
+ export async function startThreatModelRun(deps, params) {
49
+ const [git, contextOutcome, resolvedPack, priorModel] = await Promise.all([
50
+ readGitMetadata(params.cwd),
51
+ fetchSecurityContext(deps.apiClient, params.applicationId),
52
+ resolveCompatiblePack("threatmodel", { registryClient: deps.registryClient, cliVersion: params.cliVersion }),
53
+ fetchPriorThreatModel(deps.apiClient, params.applicationId),
54
+ ]);
55
+ const started = await startDevXRun(deps.apiClient, {
56
+ applicationId: params.applicationId,
57
+ runType: "threatmodel",
58
+ pack: { packId: resolvedPack.packId, packType: resolvedPack.packType, version: resolvedPack.version },
59
+ cwd: params.cwd,
60
+ });
61
+ const contextPath = writeRunWorkFile(params.cwd, started.runId, "context.json", contextOutcome);
62
+ let priorModelField;
63
+ if (priorModel.status === "found") {
64
+ const priorModelPath = writeRunWorkTextFile(params.cwd, started.runId, "prior-threat-model.md", priorModel.content);
65
+ priorModelField = { exists: true, version: priorModel.version, git_commit_sha: priorModel.gitCommitSha, path: priorModelPath };
66
+ }
67
+ else if (priorModel.status === "none") {
68
+ priorModelField = { exists: false };
69
+ }
70
+ else {
71
+ priorModelField = { exists: false, reason: priorModel.reason };
72
+ }
73
+ const runState = {
74
+ runId: started.runId,
75
+ runType: "threatmodel",
76
+ applicationId: params.applicationId,
77
+ startedAt: started.startedAt,
78
+ status: "pending",
79
+ packId: resolvedPack.packId,
80
+ packType: resolvedPack.packType,
81
+ packVersion: resolvedPack.version,
82
+ };
83
+ recordThreatModelRunState(params.cwd, runState);
84
+ const envelope = {
85
+ run_id: started.runId,
86
+ application: { id: params.applicationId },
87
+ connection: { mode: "connected" },
88
+ git: { sha: git.commitSha ?? null, dirty: git.dirty ?? null },
89
+ methodology: { pack: resolvedPack.packType, version: resolvedPack.version, path: resolvedPack.basePath },
90
+ context: { path: contextPath },
91
+ prior_model: priorModelField,
92
+ result_contract: {
93
+ schema_version: DEVX_RUN_CONTRACT_VERSION,
94
+ complete_command: "zivis threatmodel sync --input <file>",
95
+ },
96
+ agent_instructions: buildAgentInstructions(priorModel),
97
+ };
98
+ return { envelope, contextOutcome, priorModel };
99
+ }
100
+ function resolveRunState(cwd, explicitRunId) {
101
+ const state = explicitRunId ? loadThreatModelRunState(cwd, explicitRunId) : findActiveThreatModelRun(cwd);
102
+ if (!state) {
103
+ throw new Error(explicitRunId
104
+ ? `No local threatmodel run state found for run ${explicitRunId}. Start one with \`zivis threatmodel\`.`
105
+ : "No active `zivis threatmodel` run found under .zivis/work. Start one with `zivis threatmodel`, or pass --run <runId> explicitly.");
106
+ }
107
+ if (state.status !== "pending") {
108
+ throw new Error(`Run ${state.runId} has already been synced. Start a new run with \`zivis threatmodel\`.`);
109
+ }
110
+ return state;
111
+ }
112
+ export async function syncThreatModel(deps, params) {
113
+ const runState = resolveRunState(params.cwd, params.runId);
114
+ const git = await readGitMetadata(params.cwd);
115
+ const putResult = await deps.apiClient.put(`/api/rt/applications/${encodeURIComponent(params.applicationId)}/artifacts/threat_model`, {
116
+ content: params.content,
117
+ changeNote: params.changeNote,
118
+ gitCommitSha: git.commitSha ?? null,
119
+ methodology: `${runState.packId}@${runState.packVersion}`,
120
+ methodologyPackId: runState.packId,
121
+ methodologyVersion: runState.packVersion,
122
+ runId: runState.runId,
123
+ source: "cli",
124
+ });
125
+ const evidence = putResult.evidence;
126
+ const resultEnvelope = {
127
+ generatedArtifacts: {
128
+ threatModel: { evidenceId: evidence.id, version: evidence.version, unchanged: putResult.unchanged },
129
+ },
130
+ };
131
+ let runOutcome;
132
+ try {
133
+ const completion = await completeDevXRunRemote(deps.apiClient, {
134
+ runId: runState.runId,
135
+ contractVersion: DEVX_RUN_CONTRACT_VERSION,
136
+ result: resultEnvelope,
137
+ });
138
+ runOutcome = completion.outcome;
139
+ }
140
+ catch (err) {
141
+ if (!isRetryableDevXRunError(err))
142
+ throw err;
143
+ writeDevXRunCompleteOutboxEntry(params.cwd, {
144
+ runId: runState.runId,
145
+ contractVersion: DEVX_RUN_CONTRACT_VERSION,
146
+ result: resultEnvelope,
147
+ error: err instanceof Error ? err.message : String(err),
148
+ });
149
+ runOutcome = "queued";
150
+ }
151
+ markThreatModelRunCompleted(params.cwd, runState.runId);
152
+ return {
153
+ evidenceId: evidence.id,
154
+ version: evidence.version,
155
+ unchanged: putResult.unchanged,
156
+ runId: runState.runId,
157
+ runOutcome,
158
+ };
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zivis/cli",
3
- "version": "0.1.0-alpha.42",
3
+ "version": "0.1.0-alpha.43",
4
4
  "description": "ZIVIS CLI — threat modeling, scans, and MCP server for IDE integration",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://zivis.ai",