@usezombie/zombiectl 0.3.0
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.
- package/README.md +76 -0
- package/bin/zombiectl.js +11 -0
- package/bun.lock +29 -0
- package/package.json +28 -0
- package/scripts/run-tests.mjs +38 -0
- package/src/cli.js +275 -0
- package/src/commands/admin.js +39 -0
- package/src/commands/agent.js +98 -0
- package/src/commands/agent_harness.js +43 -0
- package/src/commands/agent_improvement_report.js +42 -0
- package/src/commands/agent_profile.js +39 -0
- package/src/commands/agent_proposals.js +158 -0
- package/src/commands/agent_scores.js +44 -0
- package/src/commands/core-ops.js +108 -0
- package/src/commands/core.js +537 -0
- package/src/commands/harness.js +35 -0
- package/src/commands/harness_activate.js +53 -0
- package/src/commands/harness_active.js +32 -0
- package/src/commands/harness_compile.js +40 -0
- package/src/commands/harness_source.js +72 -0
- package/src/commands/run_preview.js +212 -0
- package/src/commands/run_preview_walk.js +1 -0
- package/src/commands/runs.js +35 -0
- package/src/commands/spec_init.js +287 -0
- package/src/commands/workspace_billing.js +26 -0
- package/src/constants/error-codes.js +1 -0
- package/src/lib/agent-loop.js +106 -0
- package/src/lib/analytics.js +114 -0
- package/src/lib/api-paths.js +2 -0
- package/src/lib/browser.js +96 -0
- package/src/lib/http.js +149 -0
- package/src/lib/sse-parser.js +50 -0
- package/src/lib/state.js +67 -0
- package/src/lib/tool-executors.js +110 -0
- package/src/lib/walk-dir.js +41 -0
- package/src/program/args.js +95 -0
- package/src/program/auth-guard.js +12 -0
- package/src/program/auth-token.js +44 -0
- package/src/program/banner.js +46 -0
- package/src/program/command-registry.js +17 -0
- package/src/program/http-client.js +38 -0
- package/src/program/io.js +83 -0
- package/src/program/routes.js +20 -0
- package/src/program/suggest.js +76 -0
- package/src/program/validate.js +24 -0
- package/src/ui-progress.js +59 -0
- package/src/ui-theme.js +62 -0
- package/test/admin_config.unit.test.js +25 -0
- package/test/agent-loop.unit.test.js +497 -0
- package/test/agent_harness.unit.test.js +52 -0
- package/test/agent_improvement_report.unit.test.js +74 -0
- package/test/agent_profile.unit.test.js +156 -0
- package/test/agent_proposals.unit.test.js +167 -0
- package/test/agent_scores.unit.test.js +220 -0
- package/test/analytics.unit.test.js +41 -0
- package/test/args.unit.test.js +69 -0
- package/test/auth-guard.test.js +33 -0
- package/test/auth-token.unit.test.js +112 -0
- package/test/banner.unit.test.js +442 -0
- package/test/browser.unit.test.js +16 -0
- package/test/cli-analytics.unit.test.js +296 -0
- package/test/did-you-mean.integration.test.js +76 -0
- package/test/doctor-json.test.js +81 -0
- package/test/error-codes.unit.test.js +7 -0
- package/test/harness-command.unit.test.js +180 -0
- package/test/harness-compile.test.js +81 -0
- package/test/harness-lifecycle.integration.test.js +339 -0
- package/test/harness-source-put.test.js +72 -0
- package/test/harness_activate.unit.test.js +48 -0
- package/test/harness_active.unit.test.js +53 -0
- package/test/harness_compile.unit.test.js +54 -0
- package/test/harness_source.unit.test.js +59 -0
- package/test/help.test.js +276 -0
- package/test/helpers-fs.js +32 -0
- package/test/helpers.js +31 -0
- package/test/io.unit.test.js +57 -0
- package/test/login.unit.test.js +115 -0
- package/test/logout.unit.test.js +65 -0
- package/test/parse.test.js +16 -0
- package/test/run-preview.edge.test.js +422 -0
- package/test/run-preview.integration.test.js +135 -0
- package/test/run-preview.security.test.js +246 -0
- package/test/run-preview.unit.test.js +131 -0
- package/test/run.unit.test.js +149 -0
- package/test/runs-cancel.unit.test.js +288 -0
- package/test/runs-list.unit.test.js +105 -0
- package/test/skill-secret.unit.test.js +94 -0
- package/test/spec-init.edge.test.js +232 -0
- package/test/spec-init.integration.test.js +128 -0
- package/test/spec-init.security.test.js +285 -0
- package/test/spec-init.unit.test.js +160 -0
- package/test/specs-sync.unit.test.js +164 -0
- package/test/sse-parser.unit.test.js +54 -0
- package/test/state.unit.test.js +34 -0
- package/test/streamfetch.unit.test.js +211 -0
- package/test/suggest.test.js +75 -0
- package/test/tool-executors.unit.test.js +165 -0
- package/test/validate.test.js +81 -0
- package/test/workspace-add.test.js +106 -0
- package/test/workspace.unit.test.js +230 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { createCoreOpsHandlers } from "./core-ops.js";
|
|
2
|
+
import { commandWorkspaceUpgradeScale } from "./workspace_billing.js";
|
|
3
|
+
import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
|
|
4
|
+
import { validateRequiredId } from "../program/validate.js";
|
|
5
|
+
import { ERR_BILLING_CREDIT_EXHAUSTED } from "../constants/error-codes.js";
|
|
6
|
+
import { ApiError } from "../lib/http.js";
|
|
7
|
+
import { commandSpecInit } from "./spec_init.js";
|
|
8
|
+
import { runPreview } from "./run_preview.js";
|
|
9
|
+
|
|
10
|
+
function createCoreHandlers(ctx, workspaces, deps) {
|
|
11
|
+
const {
|
|
12
|
+
clearCredentials,
|
|
13
|
+
createSpinner,
|
|
14
|
+
newIdempotencyKey,
|
|
15
|
+
openUrl,
|
|
16
|
+
parseFlags,
|
|
17
|
+
printJson,
|
|
18
|
+
printKeyValue,
|
|
19
|
+
printSection = () => {},
|
|
20
|
+
printTable,
|
|
21
|
+
request,
|
|
22
|
+
saveCredentials,
|
|
23
|
+
saveWorkspaces,
|
|
24
|
+
ui,
|
|
25
|
+
writeLine,
|
|
26
|
+
apiHeaders,
|
|
27
|
+
} = deps;
|
|
28
|
+
|
|
29
|
+
const ops = createCoreOpsHandlers(ctx, workspaces, {
|
|
30
|
+
apiHeaders,
|
|
31
|
+
parseFlags,
|
|
32
|
+
printJson,
|
|
33
|
+
request,
|
|
34
|
+
ui,
|
|
35
|
+
writeLine,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function ensureWorkspaceId(explicit) {
|
|
39
|
+
if (explicit) return explicit;
|
|
40
|
+
return workspaces.current_workspace_id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function commandLogin(args) {
|
|
44
|
+
const { options } = parseFlags(args);
|
|
45
|
+
const timeoutSec = Number.parseInt(String(options["timeout-sec"] || "300"), 10);
|
|
46
|
+
const pollMs = Number.parseInt(String(options["poll-ms"] || "2000"), 10);
|
|
47
|
+
|
|
48
|
+
const created = await request(ctx, "/v1/auth/sessions", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: "{}",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const loginUrl = created.login_url;
|
|
55
|
+
const sessionId = created.session_id;
|
|
56
|
+
setCliAnalyticsContext(ctx, { session_id: sessionId });
|
|
57
|
+
|
|
58
|
+
if (!ctx.jsonMode) {
|
|
59
|
+
printSection(ctx.stdout, "Login session");
|
|
60
|
+
printKeyValue(ctx.stdout, {
|
|
61
|
+
session_id: sessionId,
|
|
62
|
+
login_url: loginUrl,
|
|
63
|
+
});
|
|
64
|
+
writeLine(ctx.stdout);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const shouldOpen = options["no-open"] ? false : !ctx.noOpen;
|
|
68
|
+
const opened = shouldOpen ? await openUrl(loginUrl, { env: ctx.env }) : false;
|
|
69
|
+
|
|
70
|
+
if (!ctx.jsonMode) {
|
|
71
|
+
if (shouldOpen && opened) writeLine(ctx.stdout, "browser: opened");
|
|
72
|
+
if (shouldOpen && !opened) writeLine(ctx.stdout, "browser: not opened (open URL manually)");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const deadline = Date.now() + Math.max(1, timeoutSec) * 1000;
|
|
76
|
+
let last = { status: "pending", token: null };
|
|
77
|
+
const spinner = createSpinner({
|
|
78
|
+
enabled: !ctx.jsonMode && Boolean(ctx.stderr.isTTY),
|
|
79
|
+
stream: ctx.stderr,
|
|
80
|
+
label: "waiting for browser login",
|
|
81
|
+
});
|
|
82
|
+
spinner.start();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
last = await request(ctx, `/v1/auth/sessions/${encodeURIComponent(sessionId)}`, {
|
|
87
|
+
method: "GET",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (last.status === "complete" && last.token) {
|
|
92
|
+
const saved = {
|
|
93
|
+
token: last.token,
|
|
94
|
+
saved_at: Date.now(),
|
|
95
|
+
session_id: sessionId,
|
|
96
|
+
api_url: ctx.apiUrl,
|
|
97
|
+
};
|
|
98
|
+
await saveCredentials(saved);
|
|
99
|
+
|
|
100
|
+
const result = {
|
|
101
|
+
status: "complete",
|
|
102
|
+
session_id: sessionId,
|
|
103
|
+
token_saved: true,
|
|
104
|
+
api_url: ctx.apiUrl,
|
|
105
|
+
};
|
|
106
|
+
queueCliAnalyticsEvent(ctx, "login_completed", { session_id: sessionId });
|
|
107
|
+
if (ctx.jsonMode) printJson(ctx.stdout, result);
|
|
108
|
+
else writeLine(ctx.stdout, ui.ok("login complete"));
|
|
109
|
+
spinner.succeed();
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (last.status === "expired") {
|
|
114
|
+
const result = { status: "expired", session_id: sessionId };
|
|
115
|
+
if (ctx.jsonMode) printJson(ctx.stdout, result);
|
|
116
|
+
else writeLine(ctx.stderr, ui.err("login session expired"));
|
|
117
|
+
spinner.fail();
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(500, pollMs)));
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
spinner.fail();
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
spinner.fail();
|
|
129
|
+
const timeoutResult = { status: "timeout", session_id: sessionId };
|
|
130
|
+
if (ctx.jsonMode) printJson(ctx.stdout, timeoutResult);
|
|
131
|
+
else writeLine(ctx.stderr, ui.err("login timed out"));
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function commandLogout() {
|
|
136
|
+
await clearCredentials();
|
|
137
|
+
queueCliAnalyticsEvent(ctx, "logout_completed");
|
|
138
|
+
if (ctx.jsonMode) printJson(ctx.stdout, { status: "ok", logged_out: true });
|
|
139
|
+
else writeLine(ctx.stdout, ui.ok("logout complete"));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function commandWorkspace(args) {
|
|
144
|
+
const action = args[0];
|
|
145
|
+
const tail = args.slice(1);
|
|
146
|
+
|
|
147
|
+
if (action === "add") {
|
|
148
|
+
const parsed = parseFlags(tail);
|
|
149
|
+
const repoUrl = parsed.positionals[0];
|
|
150
|
+
if (!repoUrl) {
|
|
151
|
+
writeLine(ctx.stderr, ui.err("workspace add requires <repo_url>"));
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const branch = parsed.options["default-branch"] || "main";
|
|
156
|
+
const created = await request(ctx, "/v1/workspaces", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: apiHeaders(ctx),
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
repo_url: repoUrl,
|
|
161
|
+
default_branch: branch,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
const workspaceId = created.workspace_id;
|
|
165
|
+
const installUrl = created.install_url;
|
|
166
|
+
|
|
167
|
+
const existing = workspaces.items.find((x) => x.workspace_id === workspaceId);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
workspaces.items.push({
|
|
170
|
+
workspace_id: workspaceId,
|
|
171
|
+
repo_url: repoUrl,
|
|
172
|
+
default_branch: branch,
|
|
173
|
+
created_at: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
workspaces.current_workspace_id = workspaceId;
|
|
177
|
+
await saveWorkspaces(workspaces);
|
|
178
|
+
|
|
179
|
+
const out = {
|
|
180
|
+
workspace_id: workspaceId,
|
|
181
|
+
repo_url: repoUrl,
|
|
182
|
+
install_url: installUrl,
|
|
183
|
+
next_step: "open install_url and complete GitHub App install to bind server-side",
|
|
184
|
+
};
|
|
185
|
+
setCliAnalyticsContext(ctx, {
|
|
186
|
+
workspace_id: workspaceId,
|
|
187
|
+
repo_url: repoUrl,
|
|
188
|
+
branch,
|
|
189
|
+
});
|
|
190
|
+
queueCliAnalyticsEvent(ctx, "workspace_add_completed", {
|
|
191
|
+
workspace_id: workspaceId,
|
|
192
|
+
});
|
|
193
|
+
if (ctx.jsonMode) {
|
|
194
|
+
printJson(ctx.stdout, out);
|
|
195
|
+
} else {
|
|
196
|
+
printSection(ctx.stdout, "Workspace added");
|
|
197
|
+
printKeyValue(ctx.stdout, {
|
|
198
|
+
workspace_id: workspaceId,
|
|
199
|
+
repo_url: repoUrl,
|
|
200
|
+
branch,
|
|
201
|
+
});
|
|
202
|
+
writeLine(ctx.stdout);
|
|
203
|
+
const opened = ctx.noOpen ? false : await openUrl(installUrl, { env: ctx.env });
|
|
204
|
+
writeLine(ctx.stdout, ui.info(`github_app_install_url: ${installUrl}`));
|
|
205
|
+
if (opened) {
|
|
206
|
+
writeLine(ctx.stdout, ui.ok("opened GitHub App install page in browser"));
|
|
207
|
+
} else {
|
|
208
|
+
writeLine(ctx.stdout, ui.warn("could not auto-open browser; open URL above manually"));
|
|
209
|
+
}
|
|
210
|
+
writeLine(ctx.stdout, ui.dim("After install, GitHub calls /v1/github/callback and binds workspace automatically."));
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (action === "list") {
|
|
216
|
+
setCliAnalyticsContext(ctx, {
|
|
217
|
+
workspace_id: workspaces.current_workspace_id,
|
|
218
|
+
workspace_count: workspaces.items.length,
|
|
219
|
+
});
|
|
220
|
+
queueCliAnalyticsEvent(ctx, "workspace_list_viewed", {
|
|
221
|
+
workspace_count: workspaces.items.length,
|
|
222
|
+
});
|
|
223
|
+
if (ctx.jsonMode) {
|
|
224
|
+
printJson(ctx.stdout, {
|
|
225
|
+
current_workspace_id: workspaces.current_workspace_id,
|
|
226
|
+
workspaces: workspaces.items,
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
if (workspaces.items.length === 0) {
|
|
230
|
+
writeLine(ctx.stdout, ui.info("no workspaces"));
|
|
231
|
+
}
|
|
232
|
+
printTable(
|
|
233
|
+
ctx.stdout,
|
|
234
|
+
[
|
|
235
|
+
{ key: "active", label: "ACTIVE" },
|
|
236
|
+
{ key: "workspace_id", label: "WORKSPACE" },
|
|
237
|
+
{ key: "repo_url", label: "REPO" },
|
|
238
|
+
],
|
|
239
|
+
workspaces.items.map((item) => ({
|
|
240
|
+
active: item.workspace_id === workspaces.current_workspace_id ? "*" : "",
|
|
241
|
+
workspace_id: item.workspace_id,
|
|
242
|
+
repo_url: item.repo_url,
|
|
243
|
+
})),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (action === "upgrade-scale") {
|
|
250
|
+
const parsed = parseFlags(tail);
|
|
251
|
+
const workspaceId = await ensureWorkspaceId(parsed.options["workspace-id"] || parsed.positionals[0]);
|
|
252
|
+
if (!workspaceId) {
|
|
253
|
+
writeLine(ctx.stderr, ui.err("workspace upgrade-scale requires --workspace-id"));
|
|
254
|
+
return 2;
|
|
255
|
+
}
|
|
256
|
+
const check = validateRequiredId(workspaceId, "workspace_id");
|
|
257
|
+
if (!check.ok) {
|
|
258
|
+
writeLine(ctx.stderr, ui.err(check.message));
|
|
259
|
+
return 2;
|
|
260
|
+
}
|
|
261
|
+
setCliAnalyticsContext(ctx, { workspace_id: workspaceId });
|
|
262
|
+
return commandWorkspaceUpgradeScale(ctx, parsed, workspaceId, {
|
|
263
|
+
request, apiHeaders, ui, printJson, writeLine,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (action === "remove") {
|
|
268
|
+
const parsed = parseFlags(tail);
|
|
269
|
+
const workspaceId = parsed.positionals[0] || parsed.options["workspace-id"];
|
|
270
|
+
if (!workspaceId) {
|
|
271
|
+
writeLine(ctx.stderr, ui.err("workspace remove requires <workspace_id>"));
|
|
272
|
+
return 2;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const check = validateRequiredId(workspaceId, "workspace_id");
|
|
276
|
+
if (!check.ok) {
|
|
277
|
+
writeLine(ctx.stderr, ui.err(check.message));
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
workspaces.items = workspaces.items.filter((x) => x.workspace_id !== workspaceId);
|
|
282
|
+
if (workspaces.current_workspace_id === workspaceId) {
|
|
283
|
+
workspaces.current_workspace_id = workspaces.items[0]?.workspace_id || null;
|
|
284
|
+
}
|
|
285
|
+
await saveWorkspaces(workspaces);
|
|
286
|
+
|
|
287
|
+
setCliAnalyticsContext(ctx, { workspace_id: workspaceId });
|
|
288
|
+
queueCliAnalyticsEvent(ctx, "workspace_removed", { workspace_id: workspaceId });
|
|
289
|
+
if (ctx.jsonMode) printJson(ctx.stdout, { removed: workspaceId });
|
|
290
|
+
else writeLine(ctx.stdout, ui.ok(`workspace removed: ${workspaceId}`));
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
writeLine(ctx.stderr, ui.err("usage: workspace add|list|remove"));
|
|
295
|
+
return 2;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function commandSpecsSync(args) {
|
|
299
|
+
const parsed = parseFlags(args);
|
|
300
|
+
const workspaceId = await ensureWorkspaceId(parsed.options["workspace-id"]);
|
|
301
|
+
if (!workspaceId) {
|
|
302
|
+
writeLine(ctx.stderr, ui.err("workspace_id required (set one with workspace add or pass --workspace-id)"));
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let res;
|
|
307
|
+
try {
|
|
308
|
+
res = await request(ctx, `/v1/workspaces/${encodeURIComponent(workspaceId)}:sync`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: apiHeaders(ctx),
|
|
311
|
+
body: "{}",
|
|
312
|
+
});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (!ctx.jsonMode && err instanceof ApiError && err.code === ERR_BILLING_CREDIT_EXHAUSTED) {
|
|
315
|
+
writeLine(ctx.stderr, ui.info(`Upgrade path: zombiectl workspace upgrade-scale --workspace-id ${workspaceId} --subscription-id <SUBSCRIPTION_ID>`));
|
|
316
|
+
}
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setCliAnalyticsContext(ctx, {
|
|
321
|
+
workspace_id: workspaceId,
|
|
322
|
+
synced_count: res.synced_count ?? 0,
|
|
323
|
+
total_pending: res.total_pending ?? 0,
|
|
324
|
+
});
|
|
325
|
+
queueCliAnalyticsEvent(ctx, "specs_synced", {
|
|
326
|
+
workspace_id: workspaceId,
|
|
327
|
+
synced_count: res.synced_count ?? 0,
|
|
328
|
+
});
|
|
329
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
330
|
+
else {
|
|
331
|
+
printSection(ctx.stdout, "Specs synced");
|
|
332
|
+
printKeyValue(ctx.stdout, {
|
|
333
|
+
workspace_id: workspaceId,
|
|
334
|
+
synced_count: res.synced_count ?? 0,
|
|
335
|
+
total_pending: res.total_pending ?? 0,
|
|
336
|
+
plan_tier: res.plan_tier ?? "unknown",
|
|
337
|
+
credit_remaining_cents: res.credit_remaining_cents ?? "unknown",
|
|
338
|
+
credit_currency: res.credit_currency ?? "USD",
|
|
339
|
+
});
|
|
340
|
+
if (typeof res.credit_remaining_cents === "number" && res.credit_remaining_cents <= 0) {
|
|
341
|
+
writeLine(ctx.stdout);
|
|
342
|
+
writeLine(ctx.stdout, ui.info(`Upgrade path: zombiectl workspace upgrade-scale --workspace-id ${workspaceId} --subscription-id <SUBSCRIPTION_ID>`));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function commandRun(args) {
|
|
349
|
+
if (args[0] === "status") {
|
|
350
|
+
const runId = args[1];
|
|
351
|
+
if (!runId) {
|
|
352
|
+
writeLine(ctx.stderr, ui.err("run status requires <run_id>"));
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const check = validateRequiredId(runId, "run_id");
|
|
357
|
+
if (!check.ok) {
|
|
358
|
+
writeLine(ctx.stderr, ui.err(check.message));
|
|
359
|
+
return 2;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const res = await request(ctx, `/v1/runs/${encodeURIComponent(runId)}`, {
|
|
363
|
+
method: "GET",
|
|
364
|
+
headers: apiHeaders(ctx),
|
|
365
|
+
});
|
|
366
|
+
setCliAnalyticsContext(ctx, {
|
|
367
|
+
run_id: res.run_id,
|
|
368
|
+
run_state: res.current_state ?? res.state ?? "unknown",
|
|
369
|
+
run_attempt: res.attempt,
|
|
370
|
+
run_snapshot_version: res.run_snapshot_version ?? "default-v1",
|
|
371
|
+
});
|
|
372
|
+
queueCliAnalyticsEvent(ctx, "run_status_viewed", {
|
|
373
|
+
run_id: res.run_id,
|
|
374
|
+
run_state: res.current_state ?? res.state ?? "unknown",
|
|
375
|
+
});
|
|
376
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
377
|
+
else {
|
|
378
|
+
printSection(ctx.stdout, "Run status");
|
|
379
|
+
printKeyValue(ctx.stdout, {
|
|
380
|
+
run_id: res.run_id,
|
|
381
|
+
state: res.current_state ?? res.state ?? "unknown",
|
|
382
|
+
attempt: res.attempt,
|
|
383
|
+
run_snapshot_version: res.run_snapshot_version ?? "default-v1",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const parsed = parseFlags(args);
|
|
390
|
+
|
|
391
|
+
// Preview: parse spec file, show predicted file impact
|
|
392
|
+
const specFile = parsed.options["spec"];
|
|
393
|
+
const previewOnly = Boolean(parsed.options["preview-only"]);
|
|
394
|
+
const preview = previewOnly || Boolean(parsed.options["preview"]);
|
|
395
|
+
|
|
396
|
+
if (preview) {
|
|
397
|
+
if (!specFile) {
|
|
398
|
+
writeLine(ctx.stderr, ui.err("--preview requires --spec <file>"));
|
|
399
|
+
return 2;
|
|
400
|
+
}
|
|
401
|
+
const repoPath = parsed.options["path"] || ".";
|
|
402
|
+
const result = await runPreview(specFile, repoPath, ctx, {
|
|
403
|
+
writeLine,
|
|
404
|
+
ui,
|
|
405
|
+
parseFlags,
|
|
406
|
+
printJson,
|
|
407
|
+
printSection,
|
|
408
|
+
printKeyValue,
|
|
409
|
+
printTable,
|
|
410
|
+
request,
|
|
411
|
+
apiHeaders,
|
|
412
|
+
});
|
|
413
|
+
if (!result) return 1;
|
|
414
|
+
if (previewOnly) return 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const workspaceId = await ensureWorkspaceId(parsed.options["workspace-id"]);
|
|
418
|
+
if (!workspaceId) {
|
|
419
|
+
writeLine(ctx.stderr, ui.err("workspace_id required (set one with workspace add or pass --workspace-id)"));
|
|
420
|
+
return 2;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let specId = parsed.options["spec-id"];
|
|
424
|
+
if (!specId) {
|
|
425
|
+
const listed = await request(
|
|
426
|
+
ctx,
|
|
427
|
+
`/v1/specs?workspace_id=${encodeURIComponent(workspaceId)}&limit=1`,
|
|
428
|
+
{
|
|
429
|
+
method: "GET",
|
|
430
|
+
headers: apiHeaders(ctx),
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
const first = Array.isArray(listed.specs) ? listed.specs[0] : null;
|
|
434
|
+
specId = first?.spec_id;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!specId) {
|
|
438
|
+
writeLine(ctx.stderr, ui.err("spec_id required (no specs found)"));
|
|
439
|
+
return 1;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const payload = {
|
|
443
|
+
workspace_id: workspaceId,
|
|
444
|
+
spec_id: specId,
|
|
445
|
+
mode: parsed.options.mode || "api",
|
|
446
|
+
requested_by: parsed.options["requested-by"] || "zombiectl",
|
|
447
|
+
idempotency_key: parsed.options["idempotency-key"] || newIdempotencyKey(),
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const res = await request(ctx, "/v1/runs", {
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: apiHeaders(ctx),
|
|
453
|
+
body: JSON.stringify(payload),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
setCliAnalyticsContext(ctx, {
|
|
457
|
+
workspace_id: workspaceId,
|
|
458
|
+
spec_id: specId,
|
|
459
|
+
run_id: res.run_id,
|
|
460
|
+
run_state: res.state,
|
|
461
|
+
run_mode: payload.mode,
|
|
462
|
+
requested_by: payload.requested_by,
|
|
463
|
+
});
|
|
464
|
+
queueCliAnalyticsEvent(ctx, "run_queued", {
|
|
465
|
+
workspace_id: workspaceId,
|
|
466
|
+
run_id: res.run_id,
|
|
467
|
+
spec_id: specId,
|
|
468
|
+
});
|
|
469
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
470
|
+
else {
|
|
471
|
+
printSection(ctx.stdout, "Run queued");
|
|
472
|
+
printKeyValue(ctx.stdout, {
|
|
473
|
+
workspace_id: workspaceId,
|
|
474
|
+
spec_id: specId,
|
|
475
|
+
run_id: res.run_id,
|
|
476
|
+
state: res.state,
|
|
477
|
+
mode: payload.mode,
|
|
478
|
+
plan_tier: res.plan_tier ?? "unknown",
|
|
479
|
+
credit_remaining_cents: res.credit_remaining_cents ?? "unknown",
|
|
480
|
+
credit_currency: res.credit_currency ?? "USD",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function commandRunsList(args) {
|
|
487
|
+
const parsed = parseFlags(args);
|
|
488
|
+
const workspaceId = parsed.options["workspace-id"] || workspaces.current_workspace_id;
|
|
489
|
+
|
|
490
|
+
let url = "/v1/runs";
|
|
491
|
+
if (workspaceId) url += `?workspace_id=${encodeURIComponent(workspaceId)}`;
|
|
492
|
+
|
|
493
|
+
const res = await request(ctx, url, {
|
|
494
|
+
method: "GET",
|
|
495
|
+
headers: apiHeaders(ctx),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const items = Array.isArray(res.runs) ? res.runs : [];
|
|
499
|
+
setCliAnalyticsContext(ctx, {
|
|
500
|
+
workspace_id: workspaceId,
|
|
501
|
+
run_count: items.length,
|
|
502
|
+
});
|
|
503
|
+
queueCliAnalyticsEvent(ctx, "runs_list_viewed", {
|
|
504
|
+
workspace_id: workspaceId,
|
|
505
|
+
run_count: items.length,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (ctx.jsonMode) printJson(ctx.stdout, { runs: items, total: items.length });
|
|
509
|
+
else {
|
|
510
|
+
if (items.length === 0) writeLine(ctx.stdout, ui.info("no runs"));
|
|
511
|
+
printTable(
|
|
512
|
+
ctx.stdout,
|
|
513
|
+
[
|
|
514
|
+
{ key: "run_id", label: "RUN" },
|
|
515
|
+
{ key: "workspace_id", label: "WORKSPACE" },
|
|
516
|
+
{ key: "state", label: "STATE" },
|
|
517
|
+
],
|
|
518
|
+
items,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
commandDoctor: ops.commandDoctor,
|
|
526
|
+
commandLogin,
|
|
527
|
+
commandLogout,
|
|
528
|
+
commandRun,
|
|
529
|
+
commandRunsList,
|
|
530
|
+
commandSkillSecret: ops.commandSkillSecret,
|
|
531
|
+
commandSpecInit: (args) => commandSpecInit(args, ctx, { parseFlags, writeLine, ui, printJson }),
|
|
532
|
+
commandSpecsSync,
|
|
533
|
+
commandWorkspace,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export { createCoreHandlers };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { setCliAnalyticsContext } from "../lib/analytics.js";
|
|
2
|
+
import { commandHarnessSourcePut } from "./harness_source.js";
|
|
3
|
+
import { commandHarnessCompile } from "./harness_compile.js";
|
|
4
|
+
import { commandHarnessActivate } from "./harness_activate.js";
|
|
5
|
+
import { commandHarnessActive } from "./harness_active.js";
|
|
6
|
+
import { validateRequiredId } from "../program/validate.js";
|
|
7
|
+
|
|
8
|
+
export async function commandHarness(ctx, args, workspaces, deps) {
|
|
9
|
+
const { parseFlags, ui, writeLine } = deps;
|
|
10
|
+
|
|
11
|
+
const group = args[0];
|
|
12
|
+
const action = group === "source" ? args[1] : null;
|
|
13
|
+
const parsed = parseFlags(group === "source" ? args.slice(2) : args.slice(1));
|
|
14
|
+
|
|
15
|
+
const workspaceId = parsed.options["workspace-id"] || workspaces.current_workspace_id;
|
|
16
|
+
if (!workspaceId) {
|
|
17
|
+
writeLine(ctx.stderr, ui.err("workspace_id required"));
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const wsCheck = validateRequiredId(workspaceId, "workspace_id");
|
|
22
|
+
if (!wsCheck.ok) {
|
|
23
|
+
writeLine(ctx.stderr, ui.err(wsCheck.message));
|
|
24
|
+
return 2;
|
|
25
|
+
}
|
|
26
|
+
setCliAnalyticsContext(ctx, { workspace_id: workspaceId });
|
|
27
|
+
|
|
28
|
+
if (group === "source" && action === "put") return commandHarnessSourcePut(ctx, parsed, workspaceId, deps);
|
|
29
|
+
if (group === "compile") return commandHarnessCompile(ctx, parsed, workspaceId, deps);
|
|
30
|
+
if (group === "activate") return commandHarnessActivate(ctx, parsed, workspaceId, deps);
|
|
31
|
+
if (group === "active") return commandHarnessActive(ctx, parsed, workspaceId, deps);
|
|
32
|
+
|
|
33
|
+
writeLine(ctx.stderr, ui.err("usage: harness source put|compile|activate|active"));
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
|
|
2
|
+
|
|
3
|
+
export async function commandHarnessActivate(ctx, parsed, workspaceId, deps) {
|
|
4
|
+
const {
|
|
5
|
+
request,
|
|
6
|
+
apiHeaders,
|
|
7
|
+
ui,
|
|
8
|
+
printJson,
|
|
9
|
+
printKeyValue = () => {},
|
|
10
|
+
printSection = () => {},
|
|
11
|
+
writeLine,
|
|
12
|
+
} = deps;
|
|
13
|
+
|
|
14
|
+
const profileVersionId = parsed.options["config-version-id"];
|
|
15
|
+
if (!profileVersionId) {
|
|
16
|
+
writeLine(ctx.stderr, ui.err("harness activate requires --config-version-id"));
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const body = {
|
|
21
|
+
config_version_id: profileVersionId,
|
|
22
|
+
activated_by: parsed.options["activated-by"] || "zombiectl",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const res = await request(ctx, `/v1/workspaces/${encodeURIComponent(workspaceId)}/harness/activate`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: apiHeaders(ctx),
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
setCliAnalyticsContext(ctx, {
|
|
32
|
+
workspace_id: workspaceId,
|
|
33
|
+
agent_id: res.agent_id,
|
|
34
|
+
harness_config_version_id: res.config_version_id,
|
|
35
|
+
run_snapshot_version: res.run_snapshot_version,
|
|
36
|
+
});
|
|
37
|
+
queueCliAnalyticsEvent(ctx, "harness_activated", {
|
|
38
|
+
workspace_id: workspaceId,
|
|
39
|
+
agent_id: res.agent_id,
|
|
40
|
+
harness_config_version_id: res.config_version_id,
|
|
41
|
+
});
|
|
42
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
43
|
+
else {
|
|
44
|
+
printSection(ctx.stdout, "Harness activated");
|
|
45
|
+
printKeyValue(ctx.stdout, {
|
|
46
|
+
workspace_id: workspaceId,
|
|
47
|
+
agent_id: res.agent_id,
|
|
48
|
+
config_version_id: res.config_version_id,
|
|
49
|
+
run_snapshot_version: res.run_snapshot_version,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
|
|
2
|
+
|
|
3
|
+
export async function commandHarnessActive(ctx, parsed, workspaceId, deps) {
|
|
4
|
+
const { request, apiHeaders, printJson, printKeyValue = () => {}, printSection = () => {} } = deps;
|
|
5
|
+
|
|
6
|
+
const res = await request(ctx, `/v1/workspaces/${encodeURIComponent(workspaceId)}/harness/active`, {
|
|
7
|
+
method: "GET",
|
|
8
|
+
headers: apiHeaders(ctx),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
setCliAnalyticsContext(ctx, {
|
|
12
|
+
workspace_id: workspaceId,
|
|
13
|
+
agent_id: res.agent_id ?? "default-v1",
|
|
14
|
+
harness_config_version_id: res.config_version_id ?? "default-v1",
|
|
15
|
+
run_snapshot_version: res.run_snapshot_version ?? "default-v1",
|
|
16
|
+
});
|
|
17
|
+
queueCliAnalyticsEvent(ctx, "harness_active_viewed", {
|
|
18
|
+
workspace_id: workspaceId,
|
|
19
|
+
agent_id: res.agent_id ?? "default-v1",
|
|
20
|
+
});
|
|
21
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
22
|
+
else {
|
|
23
|
+
printSection(ctx.stdout, "Active harness");
|
|
24
|
+
printKeyValue(ctx.stdout, {
|
|
25
|
+
workspace_id: workspaceId,
|
|
26
|
+
agent_id: res.agent_id ?? "default-v1",
|
|
27
|
+
config_version_id: res.config_version_id ?? "default-v1",
|
|
28
|
+
run_snapshot_version: res.run_snapshot_version ?? "default-v1",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { queueCliAnalyticsEvent, setCliAnalyticsContext } from "../lib/analytics.js";
|
|
2
|
+
|
|
3
|
+
export async function commandHarnessCompile(ctx, parsed, workspaceId, deps) {
|
|
4
|
+
const { request, apiHeaders, printJson, printKeyValue = () => {}, printSection = () => {} } = deps;
|
|
5
|
+
|
|
6
|
+
const body = {
|
|
7
|
+
agent_id: parsed.options["agent-id"] || null,
|
|
8
|
+
config_version_id: parsed.options["config-version-id"] || null,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const res = await request(ctx, `/v1/workspaces/${encodeURIComponent(workspaceId)}/harness/compile`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: apiHeaders(ctx),
|
|
14
|
+
body: JSON.stringify(body),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
setCliAnalyticsContext(ctx, {
|
|
18
|
+
workspace_id: workspaceId,
|
|
19
|
+
agent_id: body.agent_id,
|
|
20
|
+
harness_config_version_id: body.config_version_id,
|
|
21
|
+
compile_job_id: res.compile_job_id,
|
|
22
|
+
harness_valid: res.is_valid,
|
|
23
|
+
});
|
|
24
|
+
queueCliAnalyticsEvent(ctx, "harness_compiled", {
|
|
25
|
+
workspace_id: workspaceId,
|
|
26
|
+
compile_job_id: res.compile_job_id,
|
|
27
|
+
harness_valid: res.is_valid,
|
|
28
|
+
});
|
|
29
|
+
if (ctx.jsonMode) printJson(ctx.stdout, res);
|
|
30
|
+
else {
|
|
31
|
+
printSection(ctx.stdout, "Harness compile");
|
|
32
|
+
printKeyValue(ctx.stdout, {
|
|
33
|
+
workspace_id: workspaceId,
|
|
34
|
+
compile_job_id: res.compile_job_id,
|
|
35
|
+
valid: res.is_valid,
|
|
36
|
+
config_version_id: body.config_version_id ?? "latest",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|