orizu 0.4.0 → 0.5.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 +22 -13
- package/dist/artifact-archive.js +152 -0
- package/dist/connectors-cli.js +80 -0
- package/dist/credentials.js +4 -1
- package/dist/daytona-slice-rehearsal.js +255 -0
- package/dist/daytona-slice.js +441 -0
- package/dist/egress-allowlist-cli.js +132 -0
- package/dist/egress-policy.js +159 -0
- package/dist/git-credential.js +131 -0
- package/dist/github-setup.js +226 -0
- package/dist/help.js +276 -21
- package/dist/hosted-bootstrap-rehearsal.js +235 -0
- package/dist/hosted-bootstrap.js +365 -0
- package/dist/hosted-broker-server.js +220 -0
- package/dist/hosted-git-http-server.js +136 -0
- package/dist/hosted-harness-claude.js +493 -0
- package/dist/hosted-harness-opencode.js +729 -0
- package/dist/hosted-harness.js +60 -0
- package/dist/hosted-loop.js +477 -0
- package/dist/hosted-run-event-sink.js +499 -0
- package/dist/hosted-runtime-assets.js +216 -0
- package/dist/hosted-session-cli.js +642 -0
- package/dist/index.js +945 -258
- package/dist/kill-switch-cli.js +89 -0
- package/dist/manifests-cli.js +143 -0
- package/dist/preview-runtime.js +4 -1
- package/dist/sandbox-provider.js +238 -0
- package/dist/secret-redaction.js +112 -0
- package/dist/session-timeline.js +103 -0
- package/dist/setup-prompt.js +4 -2
- package/dist/skill-installer.js +9 -1
- package/dist/smoke.js +250 -0
- package/dist/task-assignment-manifest.js +96 -0
- package/dist/vercel-sandbox-provider.js +260 -0
- package/dist/workbench-cli.js +493 -0
- package/dist/workspace-sync.js +924 -0
- package/dist/workspace.js +868 -57
- package/package.json +4 -1
- package/src/artifact-archive.ts +182 -0
- package/src/connectors-cli.ts +115 -0
- package/src/credentials.ts +6 -2
- package/src/daytona-slice-rehearsal.ts +320 -0
- package/src/daytona-slice.ts +598 -0
- package/src/egress-allowlist-cli.ts +147 -0
- package/src/egress-policy.ts +188 -0
- package/src/git-credential.ts +155 -0
- package/src/github-setup.ts +366 -0
- package/src/help.ts +277 -21
- package/src/hosted-bootstrap-rehearsal.ts +312 -0
- package/src/hosted-bootstrap.ts +571 -0
- package/src/hosted-broker-server.ts +278 -0
- package/src/hosted-git-http-server.ts +164 -0
- package/src/hosted-harness-claude.ts +621 -0
- package/src/hosted-harness-opencode.ts +823 -0
- package/src/hosted-harness.ts +124 -0
- package/src/hosted-loop.ts +680 -0
- package/src/hosted-run-event-sink.ts +632 -0
- package/src/hosted-runtime-assets.ts +272 -0
- package/src/hosted-session-cli.ts +844 -0
- package/src/index.ts +1102 -317
- package/src/kill-switch-cli.ts +113 -0
- package/src/manifests-cli.ts +168 -0
- package/src/preview-runtime.ts +14 -9
- package/src/sandbox-provider.ts +440 -0
- package/src/secret-redaction.ts +124 -0
- package/src/session-timeline.ts +152 -0
- package/src/setup-prompt.ts +4 -2
- package/src/skill-installer.ts +13 -1
- package/src/smoke.ts +342 -0
- package/src/task-assignment-manifest.ts +164 -0
- package/src/vercel-sandbox-provider.ts +388 -0
- package/src/workbench-cli.ts +701 -0
- package/src/workspace-sync.ts +1148 -0
- package/src/workspace.ts +1051 -64
- package/vendor/skills/orizu-cli/SKILL.md +8 -4
- package/vendor/skills/orizu-cli/references/cli-reference.md +12 -4
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace session/run visibility commands (ALI-902 CLI half).
|
|
3
|
+
*
|
|
4
|
+
* Pure logic + injected HTTP: command functions do not parse process.argv and
|
|
5
|
+
* do not print. The thin `workbenchCommand` entry point owns CLI argument
|
|
6
|
+
* parsing and human/JSON output.
|
|
7
|
+
*/
|
|
8
|
+
import { authedFetch } from './http.js';
|
|
9
|
+
import { getWorkspaceRoot } from './workspace.js';
|
|
10
|
+
import { attachedWorkspaceId, stringOrNull } from './workspace-sync.js';
|
|
11
|
+
const TERMINAL_RUN_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
|
|
12
|
+
const SESSION_STATUSES = new Set(['active', 'ended']);
|
|
13
|
+
const DEFAULT_TAIL_LIMIT = 100;
|
|
14
|
+
const MAX_TAIL_LIMIT = 500;
|
|
15
|
+
const DEFAULT_TAIL_INTERVAL_SECONDS = 2;
|
|
16
|
+
// Floor for the continuous poll sleep so `--interval 0` (or negatives) cannot
|
|
17
|
+
// hot-spin against the API while a non-terminal run is quiet. Draining full
|
|
18
|
+
// pages intentionally skips the sleep — that is catch-up, not polling.
|
|
19
|
+
const MIN_TAIL_INTERVAL_MS = 500;
|
|
20
|
+
function defaultSleep(ms) {
|
|
21
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
function numberOrFallback(value, fallback) {
|
|
24
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
25
|
+
}
|
|
26
|
+
function normalizeTailLimit(limit) {
|
|
27
|
+
return typeof limit === 'number' && Number.isFinite(limit) && limit > 0
|
|
28
|
+
? Math.min(Math.floor(limit), MAX_TAIL_LIMIT)
|
|
29
|
+
: DEFAULT_TAIL_LIMIT;
|
|
30
|
+
}
|
|
31
|
+
function normalizeProjectSlug(value) {
|
|
32
|
+
const raw = stringOrNull(value);
|
|
33
|
+
if (!raw) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const segments = raw.split('/').map(segment => segment.trim()).filter(Boolean);
|
|
37
|
+
return segments.length > 0 ? segments[segments.length - 1] : null;
|
|
38
|
+
}
|
|
39
|
+
function resolveAttachedWorkspaceId(opts) {
|
|
40
|
+
const explicit = stringOrNull(opts.workspaceId ?? undefined);
|
|
41
|
+
if (explicit) {
|
|
42
|
+
return explicit;
|
|
43
|
+
}
|
|
44
|
+
const root = getWorkspaceRoot(opts.cwd);
|
|
45
|
+
const attached = attachedWorkspaceId(root);
|
|
46
|
+
if (!attached) {
|
|
47
|
+
throw new Error('This workspace is not attached yet; run `orizu workspace sync` first to attach it.');
|
|
48
|
+
}
|
|
49
|
+
return attached;
|
|
50
|
+
}
|
|
51
|
+
async function readJson(response) {
|
|
52
|
+
try {
|
|
53
|
+
return await response.json();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function responseMessage(response) {
|
|
60
|
+
try {
|
|
61
|
+
const data = await response.clone().json();
|
|
62
|
+
return stringOrNull(data.error) || stringOrNull(data.message) || response.statusText || String(response.status);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
try {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
return text.trim() || response.statusText || String(response.status);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return response.statusText || String(response.status);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function assertOk(response, action) {
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`${action} failed (${response.status}): ${await responseMessage(response)}`);
|
|
79
|
+
}
|
|
80
|
+
function fetcherFrom(opts) {
|
|
81
|
+
return opts.fetcher ?? authedFetch;
|
|
82
|
+
}
|
|
83
|
+
function requiredBodyObject(data, key, action) {
|
|
84
|
+
const value = data[key];
|
|
85
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
86
|
+
throw new Error(`${action} response did not include ${key}`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function sessionFrom(data, action) {
|
|
91
|
+
return requiredBodyObject(data, 'session', action);
|
|
92
|
+
}
|
|
93
|
+
function runFrom(data, action) {
|
|
94
|
+
return requiredBodyObject(data, 'run', action);
|
|
95
|
+
}
|
|
96
|
+
function sessionsFrom(data) {
|
|
97
|
+
return Array.isArray(data.sessions) ? data.sessions : [];
|
|
98
|
+
}
|
|
99
|
+
function eventsFrom(data) {
|
|
100
|
+
return Array.isArray(data.events) ? data.events : [];
|
|
101
|
+
}
|
|
102
|
+
export async function runSessionStart(opts = {}) {
|
|
103
|
+
const fetcher = fetcherFrom(opts);
|
|
104
|
+
const workspaceId = resolveAttachedWorkspaceId(opts);
|
|
105
|
+
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
106
|
+
const body = {};
|
|
107
|
+
if (projectSlug) {
|
|
108
|
+
body.projectSlug = projectSlug;
|
|
109
|
+
}
|
|
110
|
+
if (opts.clientInfo) {
|
|
111
|
+
body.clientInfo = opts.clientInfo;
|
|
112
|
+
}
|
|
113
|
+
const response = await fetcher(`/api/cli/workspaces/${encodeURIComponent(workspaceId)}/sessions`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify(body),
|
|
117
|
+
});
|
|
118
|
+
await assertOk(response, 'Session start');
|
|
119
|
+
const data = await readJson(response);
|
|
120
|
+
return { session: sessionFrom(data, 'Session start') };
|
|
121
|
+
}
|
|
122
|
+
export async function runSessionStatus(opts = {}) {
|
|
123
|
+
const fetcher = fetcherFrom(opts);
|
|
124
|
+
const sessionId = stringOrNull(opts.sessionId ?? undefined);
|
|
125
|
+
if (sessionId) {
|
|
126
|
+
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(sessionId)}`);
|
|
127
|
+
await assertOk(response, 'Session status');
|
|
128
|
+
const data = await readJson(response);
|
|
129
|
+
return { session: sessionFrom(data, 'Session status') };
|
|
130
|
+
}
|
|
131
|
+
const workspaceId = resolveAttachedWorkspaceId(opts);
|
|
132
|
+
const status = stringOrNull(opts.status ?? undefined);
|
|
133
|
+
const query = status ? `?status=${encodeURIComponent(status)}` : '';
|
|
134
|
+
const response = await fetcher(`/api/cli/workspaces/${encodeURIComponent(workspaceId)}/sessions${query}`);
|
|
135
|
+
await assertOk(response, 'Session status');
|
|
136
|
+
const data = await readJson(response);
|
|
137
|
+
return { sessions: sessionsFrom(data) };
|
|
138
|
+
}
|
|
139
|
+
export async function runSessionEnd(opts) {
|
|
140
|
+
const fetcher = fetcherFrom(opts);
|
|
141
|
+
if (!stringOrNull(opts.sessionId)) {
|
|
142
|
+
throw new Error('Usage: orizu session end --session <id> [--json]');
|
|
143
|
+
}
|
|
144
|
+
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, {
|
|
145
|
+
method: 'PATCH',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ status: 'ended' }),
|
|
148
|
+
});
|
|
149
|
+
await assertOk(response, 'Session end');
|
|
150
|
+
const data = await readJson(response);
|
|
151
|
+
return {
|
|
152
|
+
session: sessionFrom(data, 'Session end'),
|
|
153
|
+
branchStatus: stringOrNull(data.branchStatus) ?? undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export async function runSessionFinish(opts) {
|
|
157
|
+
const fetcher = fetcherFrom(opts);
|
|
158
|
+
if (!stringOrNull(opts.sessionId)) {
|
|
159
|
+
throw new Error('Usage: orizu session finish --session <id> [--project <team/project>] [--json]');
|
|
160
|
+
}
|
|
161
|
+
const body = {};
|
|
162
|
+
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
163
|
+
if (projectSlug) {
|
|
164
|
+
body.projectSlug = projectSlug;
|
|
165
|
+
}
|
|
166
|
+
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(opts.sessionId)}/finish-branch`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
});
|
|
171
|
+
await assertOk(response, 'Session finish');
|
|
172
|
+
const data = await readJson(response);
|
|
173
|
+
const manifest = data.manifest && typeof data.manifest === 'object' && !Array.isArray(data.manifest)
|
|
174
|
+
? data.manifest
|
|
175
|
+
: undefined;
|
|
176
|
+
return {
|
|
177
|
+
outcome: stringOrNull(data.outcome) || 'unknown',
|
|
178
|
+
branch: stringOrNull(data.branch) ?? undefined,
|
|
179
|
+
branchDeleted: typeof data.branchDeleted === 'boolean' ? data.branchDeleted : undefined,
|
|
180
|
+
manifest,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function runWorkbenchRunStart(opts) {
|
|
184
|
+
const fetcher = fetcherFrom(opts);
|
|
185
|
+
if (!stringOrNull(opts.sessionId) || !stringOrNull(opts.title)) {
|
|
186
|
+
throw new Error('Usage: orizu run start --session <id> --title <title> [--project <team/project>] [--json]');
|
|
187
|
+
}
|
|
188
|
+
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
189
|
+
const body = {
|
|
190
|
+
workspaceSessionId: opts.sessionId,
|
|
191
|
+
title: opts.title,
|
|
192
|
+
};
|
|
193
|
+
if (projectSlug) {
|
|
194
|
+
body.projectSlug = projectSlug;
|
|
195
|
+
}
|
|
196
|
+
const response = await fetcher('/api/cli/workbench-runs', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
});
|
|
201
|
+
if (response.status === 409) {
|
|
202
|
+
// PR #1284 contract: starting a run on an ended session is a 409.
|
|
203
|
+
const message = await responseMessage(response);
|
|
204
|
+
const hint = /ended/i.test(message) ? ' Run `orizu session start` to open a new session.' : '';
|
|
205
|
+
throw new Error(`Run start failed (409): ${message}${hint}`);
|
|
206
|
+
}
|
|
207
|
+
await assertOk(response, 'Run start');
|
|
208
|
+
const data = await readJson(response);
|
|
209
|
+
return { run: runFrom(data, 'Run start') };
|
|
210
|
+
}
|
|
211
|
+
export async function runWorkbenchRunStatus(opts) {
|
|
212
|
+
const fetcher = fetcherFrom(opts);
|
|
213
|
+
if (!stringOrNull(opts.runId)) {
|
|
214
|
+
throw new Error('Usage: orizu run status --run <id> [--json]');
|
|
215
|
+
}
|
|
216
|
+
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(opts.runId)}`);
|
|
217
|
+
await assertOk(response, 'Run status');
|
|
218
|
+
const data = await readJson(response);
|
|
219
|
+
return { run: runFrom(data, 'Run status') };
|
|
220
|
+
}
|
|
221
|
+
export async function runWorkbenchRunTerminal(opts) {
|
|
222
|
+
const fetcher = fetcherFrom(opts);
|
|
223
|
+
if (!stringOrNull(opts.runId) || !TERMINAL_RUN_STATUSES.has(opts.status)) {
|
|
224
|
+
throw new Error('Usage: orizu run <complete|fail|cancel> --run <id> [--summary <text>] [--json]');
|
|
225
|
+
}
|
|
226
|
+
const body = { status: opts.status };
|
|
227
|
+
if (opts.summary) {
|
|
228
|
+
body.summary = { note: opts.summary };
|
|
229
|
+
}
|
|
230
|
+
if (opts.evidence) {
|
|
231
|
+
body.evidence = opts.evidence;
|
|
232
|
+
}
|
|
233
|
+
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(opts.runId)}`, {
|
|
234
|
+
method: 'PATCH',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify(body),
|
|
237
|
+
});
|
|
238
|
+
if (response.status === 409) {
|
|
239
|
+
// PR #1284 contract: "Run already <status>" names the STORED status when a
|
|
240
|
+
// different terminal status was requested — report it plus a status hint.
|
|
241
|
+
const message = await responseMessage(response);
|
|
242
|
+
const alreadyMatch = /^Run already (\S+)/.exec(message);
|
|
243
|
+
if (alreadyMatch) {
|
|
244
|
+
throw new Error(`Run update rejected (409): run is already '${alreadyMatch[1]}' (requested '${opts.status}'). Check \`orizu run status --run ${opts.runId}\`.`);
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Run update failed (409): ${message}`);
|
|
247
|
+
}
|
|
248
|
+
await assertOk(response, 'Run update');
|
|
249
|
+
const data = await readJson(response);
|
|
250
|
+
return { run: runFrom(data, 'Run update') };
|
|
251
|
+
}
|
|
252
|
+
export async function tailOnce(fetcher, runId, cursor, limit = DEFAULT_TAIL_LIMIT) {
|
|
253
|
+
// The API requires an integer cursor: only send back whole non-negative
|
|
254
|
+
// sequences (PR #1284 contract — non-numeric/negative `after` is a 400).
|
|
255
|
+
const safeCursor = Number.isFinite(cursor) && cursor > 0 ? Math.floor(cursor) : 0;
|
|
256
|
+
const safeLimit = normalizeTailLimit(limit);
|
|
257
|
+
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(runId)}/events?after=${safeCursor}&limit=${safeLimit}`);
|
|
258
|
+
await assertOk(response, 'Run tail');
|
|
259
|
+
const data = await readJson(response);
|
|
260
|
+
return {
|
|
261
|
+
events: eventsFrom(data),
|
|
262
|
+
cursor: numberOrFallback(data.cursor, safeCursor),
|
|
263
|
+
runStatus: stringOrNull(data.runStatus) || 'unknown',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
export async function tailWorkbenchRun(opts) {
|
|
267
|
+
const fetcher = fetcherFrom(opts);
|
|
268
|
+
if (!stringOrNull(opts.runId)) {
|
|
269
|
+
throw new Error('Usage: orizu run tail --run <id> [--after <seq>] [--interval <seconds>] [--once] [--json] (--once drains all currently persisted events, then exits without following)');
|
|
270
|
+
}
|
|
271
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
272
|
+
let cursor = typeof opts.after === 'number' && Number.isFinite(opts.after) && opts.after > 0 ? Math.floor(opts.after) : 0;
|
|
273
|
+
const intervalMs = Math.max(MIN_TAIL_INTERVAL_MS, Math.round((opts.intervalSeconds ?? DEFAULT_TAIL_INTERVAL_SECONDS) * 1000));
|
|
274
|
+
const pageLimit = normalizeTailLimit(opts.limit);
|
|
275
|
+
while (true) {
|
|
276
|
+
const previousCursor = cursor;
|
|
277
|
+
const result = await tailOnce(fetcher, opts.runId, cursor, opts.limit);
|
|
278
|
+
cursor = result.cursor;
|
|
279
|
+
for (const event of result.events) {
|
|
280
|
+
opts.onEvent?.(event);
|
|
281
|
+
}
|
|
282
|
+
// A full page means more events may already be persisted — keep draining
|
|
283
|
+
// back-to-back (no sleep). Exit conditions are only evaluated on a SHORT
|
|
284
|
+
// page: a terminal runStatus on a full page would otherwise truncate a
|
|
285
|
+
// completed run that has more unread pages.
|
|
286
|
+
if (result.events.length >= pageLimit) {
|
|
287
|
+
// Protocol invariant: a full page always implies the server advanced
|
|
288
|
+
// the cursor past what we asked for. If it didn't, draining back-to-back
|
|
289
|
+
// would spin hot forever on a misbehaving/stalled server. We pick the
|
|
290
|
+
// simpler of the two acceptable fixes (apply the normal poll sleep
|
|
291
|
+
// before retrying, rather than failing after one retry) because it
|
|
292
|
+
// matches this loop's existing backoff-on-no-progress shape (see the
|
|
293
|
+
// MIN_TAIL_INTERVAL_MS floor above) without introducing a new
|
|
294
|
+
// retry-count/error path.
|
|
295
|
+
if (cursor > previousCursor) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
await sleep(intervalMs);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (opts.once || TERMINAL_RUN_STATUSES.has(result.runStatus)) {
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
await sleep(intervalMs);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function hasFlag(args, flag) {
|
|
308
|
+
return args.includes(flag);
|
|
309
|
+
}
|
|
310
|
+
function argValue(args, flag) {
|
|
311
|
+
const index = args.indexOf(flag);
|
|
312
|
+
if (index === -1 || index + 1 >= args.length || args[index + 1].startsWith('--')) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return args[index + 1];
|
|
316
|
+
}
|
|
317
|
+
function numberArg(args, flag) {
|
|
318
|
+
const raw = argValue(args, flag);
|
|
319
|
+
if (!raw) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const parsed = Number(raw);
|
|
323
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
324
|
+
}
|
|
325
|
+
function positionalArgs(args) {
|
|
326
|
+
const values = [];
|
|
327
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
328
|
+
const arg = args[i];
|
|
329
|
+
if (arg.startsWith('--')) {
|
|
330
|
+
if (argValue(args, arg) !== null) {
|
|
331
|
+
i += 1;
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
values.push(arg);
|
|
336
|
+
}
|
|
337
|
+
return values;
|
|
338
|
+
}
|
|
339
|
+
function emit(io, human, payload) {
|
|
340
|
+
io.print(io.json ? JSON.stringify(payload) : human);
|
|
341
|
+
}
|
|
342
|
+
function formatSessionStatus(result) {
|
|
343
|
+
if (result.session) {
|
|
344
|
+
const lines = [`session ${result.session.id} ${result.session.status}`];
|
|
345
|
+
for (const run of result.session.runs ?? []) {
|
|
346
|
+
lines.push(` run ${run.id} ${run.status}`);
|
|
347
|
+
}
|
|
348
|
+
return lines.join('\n');
|
|
349
|
+
}
|
|
350
|
+
const sessions = result.sessions ?? [];
|
|
351
|
+
return ['workspace sessions', ...sessions.map(session => `${session.id} ${session.status}`)].join('\n');
|
|
352
|
+
}
|
|
353
|
+
function formatSessionFinish(result) {
|
|
354
|
+
if (result.outcome === 'no-changes') {
|
|
355
|
+
const branch = result.branch ?? 'the session branch';
|
|
356
|
+
return result.branchDeleted === false
|
|
357
|
+
? `no changes on ${branch}; branch NOT deleted (delete it manually or retry \`orizu session finish\`)`
|
|
358
|
+
: `no changes on ${branch}; branch deleted`;
|
|
359
|
+
}
|
|
360
|
+
const manifest = result.manifest ?? {};
|
|
361
|
+
const id = typeof manifest.id === 'string' ? manifest.id : '(unknown)';
|
|
362
|
+
const status = typeof manifest.status === 'string' ? manifest.status : '(unknown)';
|
|
363
|
+
const proposed = (manifest.proposedState ?? {});
|
|
364
|
+
const files = typeof proposed.filesChanged === 'number' ? proposed.filesChanged : 0;
|
|
365
|
+
const additions = typeof proposed.additions === 'number' ? proposed.additions : 0;
|
|
366
|
+
const deletions = typeof proposed.deletions === 'number' ? proposed.deletions : 0;
|
|
367
|
+
return `manifest ${id} ${status} repo_merge (${files} files, +${additions}/-${deletions})`;
|
|
368
|
+
}
|
|
369
|
+
function formatRunStatus(run) {
|
|
370
|
+
const sequence = typeof run.latestSequence === 'number' ? ` latestSequence=${run.latestSequence}` : '';
|
|
371
|
+
return `run ${run.id} ${run.status}${sequence}`;
|
|
372
|
+
}
|
|
373
|
+
function formatEventLine(event) {
|
|
374
|
+
const payload = event.payload && Object.keys(event.payload).length > 0 ? ` ${JSON.stringify(event.payload)}` : '';
|
|
375
|
+
return `${event.sequence} ${event.eventType}${payload}`;
|
|
376
|
+
}
|
|
377
|
+
function terminalStatusForCommand(command) {
|
|
378
|
+
if (command === 'complete')
|
|
379
|
+
return 'succeeded';
|
|
380
|
+
if (command === 'fail')
|
|
381
|
+
return 'failed';
|
|
382
|
+
if (command === 'cancel')
|
|
383
|
+
return 'cancelled';
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const USAGE_LINE = 'Usage: orizu session <start|status|end|finish> ... | orizu run <start|status|tail|complete|fail|cancel> ... (run tail --once drains all currently persisted events, then exits without following)';
|
|
387
|
+
// In --json mode ANY command failure — bad usage, or an operational failure
|
|
388
|
+
// such as a 401/403/404/409/network error surfaced by assertOk — must still
|
|
389
|
+
// land as a single machine-readable document (`{"error": ...}`) on stdout,
|
|
390
|
+
// never human help text and never a plain-text message on stderr. An agent
|
|
391
|
+
// parsing --json stdout should never have to also watch stderr for the most
|
|
392
|
+
// common failure modes. Non-JSON mode is unchanged: the message prints as-is.
|
|
393
|
+
// Stack traces are never printed either way (only `error.message`).
|
|
394
|
+
function emitJsonError(io, message) {
|
|
395
|
+
io.print(io.json ? JSON.stringify({ error: message }) : message);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
export async function workbenchCommand(args, io) {
|
|
399
|
+
try {
|
|
400
|
+
return await dispatchWorkbenchCommand(args, io);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
404
|
+
if (io.json) {
|
|
405
|
+
return emitJsonError(io, message);
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function dispatchWorkbenchCommand(args, io) {
|
|
411
|
+
const positional = positionalArgs(args);
|
|
412
|
+
const group = positional[0];
|
|
413
|
+
const subcommand = positional[1];
|
|
414
|
+
const fetcher = io.fetcher;
|
|
415
|
+
if (group === 'session') {
|
|
416
|
+
if (subcommand === 'start') {
|
|
417
|
+
const result = await runSessionStart({
|
|
418
|
+
fetcher,
|
|
419
|
+
projectSlug: argValue(args, '--project'),
|
|
420
|
+
clientInfo: { source: 'orizu-cli' },
|
|
421
|
+
});
|
|
422
|
+
emit(io, `session started: ${result.session.id}`, result);
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
if (subcommand === 'status') {
|
|
426
|
+
const status = argValue(args, '--status');
|
|
427
|
+
if (status && !SESSION_STATUSES.has(status)) {
|
|
428
|
+
throw new Error('Usage: orizu session status [--session <id> | --status active|ended] [--json]');
|
|
429
|
+
}
|
|
430
|
+
const result = await runSessionStatus({ fetcher, sessionId: argValue(args, '--session'), status });
|
|
431
|
+
emit(io, formatSessionStatus(result), result);
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
if (subcommand === 'end') {
|
|
435
|
+
const result = await runSessionEnd({ fetcher, sessionId: argValue(args, '--session') || '' });
|
|
436
|
+
const hint = result.branchStatus === 'has-changes'
|
|
437
|
+
? '\nsession branch has changes — run `orizu session finish --session ' + result.session.id + '` to promote them'
|
|
438
|
+
: '';
|
|
439
|
+
emit(io, `session ended: ${result.session.id}${hint}`, result);
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
if (subcommand === 'finish') {
|
|
443
|
+
const result = await runSessionFinish({
|
|
444
|
+
fetcher,
|
|
445
|
+
sessionId: argValue(args, '--session') || '',
|
|
446
|
+
projectSlug: argValue(args, '--project'),
|
|
447
|
+
});
|
|
448
|
+
emit(io, formatSessionFinish(result), result);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (group === 'run') {
|
|
453
|
+
if (subcommand === 'start') {
|
|
454
|
+
const result = await runWorkbenchRunStart({
|
|
455
|
+
fetcher,
|
|
456
|
+
sessionId: argValue(args, '--session') || '',
|
|
457
|
+
title: argValue(args, '--title') || '',
|
|
458
|
+
projectSlug: argValue(args, '--project'),
|
|
459
|
+
});
|
|
460
|
+
emit(io, `run started: ${result.run.id}`, result);
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
if (subcommand === 'status') {
|
|
464
|
+
const result = await runWorkbenchRunStatus({ fetcher, runId: argValue(args, '--run') || '' });
|
|
465
|
+
emit(io, formatRunStatus(result.run), result);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
if (subcommand === 'tail') {
|
|
469
|
+
await tailWorkbenchRun({
|
|
470
|
+
fetcher,
|
|
471
|
+
runId: argValue(args, '--run') || '',
|
|
472
|
+
after: numberArg(args, '--after'),
|
|
473
|
+
intervalSeconds: numberArg(args, '--interval'),
|
|
474
|
+
once: hasFlag(args, '--once'),
|
|
475
|
+
sleep: io.sleep,
|
|
476
|
+
onEvent: event => io.print(io.json ? JSON.stringify(event) : formatEventLine(event)),
|
|
477
|
+
});
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
const status = terminalStatusForCommand(subcommand || '');
|
|
481
|
+
if (status) {
|
|
482
|
+
const result = await runWorkbenchRunTerminal({
|
|
483
|
+
fetcher,
|
|
484
|
+
runId: argValue(args, '--run') || '',
|
|
485
|
+
status,
|
|
486
|
+
summary: argValue(args, '--summary'),
|
|
487
|
+
});
|
|
488
|
+
emit(io, formatRunStatus(result.run), result);
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return emitJsonError(io, USAGE_LINE);
|
|
493
|
+
}
|