orizu 0.5.52 → 0.5.53
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/dist/auth-commands.js +2 -2
- package/dist/cli-console-output.d.ts +7 -1
- package/dist/cli-console-output.d.ts.map +1 -1
- package/dist/cli-console-output.js +16 -3
- package/dist/cli-stops.d.ts +14 -0
- package/dist/cli-stops.d.ts.map +1 -0
- package/dist/cli-stops.js +22 -0
- package/dist/egress-allowlist-cli.d.ts +29 -2
- package/dist/egress-allowlist-cli.d.ts.map +1 -1
- package/dist/egress-allowlist-cli.js +16 -89
- package/dist/egress-allowlist-workflow.d.ts +17 -0
- package/dist/egress-allowlist-workflow.d.ts.map +1 -0
- package/dist/egress-allowlist-workflow.js +183 -0
- package/dist/instruction-delivery.d.ts +4 -4
- package/dist/instruction-delivery.d.ts.map +1 -1
- package/dist/instruction-delivery.js +32 -46
- package/dist/instruction-prune.d.ts +24 -0
- package/dist/instruction-prune.d.ts.map +1 -0
- package/dist/instruction-prune.js +33 -0
- package/dist/instruction-set-sync/index.d.ts +0 -2
- package/dist/instruction-set-sync/index.d.ts.map +1 -1
- package/dist/instruction-set-sync/index.js +3 -36
- package/dist/instruction-set-update/index.d.ts +41 -3
- package/dist/instruction-set-update/index.d.ts.map +1 -1
- package/dist/instruction-set-update/index.js +156 -132
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +21 -2
- package/dist/instruction-sync-lock.d.ts +9 -0
- package/dist/instruction-sync-lock.d.ts.map +1 -0
- package/dist/instruction-sync-lock.js +24 -0
- package/dist/json-output-scrub.d.ts +3 -0
- package/dist/json-output-scrub.d.ts.map +1 -0
- package/dist/json-output-scrub.js +70 -0
- package/dist/one-request-write.d.ts.map +1 -1
- package/dist/one-request-write.js +2 -9
- package/dist/workbench-cli.d.ts +2 -1
- package/dist/workbench-cli.d.ts.map +1 -1
- package/dist/workbench-cli.js +79 -593
- package/dist/workbench-workflows.d.ts +56 -0
- package/dist/workbench-workflows.d.ts.map +1 -0
- package/dist/workbench-workflows.js +665 -0
- package/dist/workspace-sync-workflow.d.ts +115 -0
- package/dist/workspace-sync-workflow.d.ts.map +1 -0
- package/dist/workspace-sync-workflow.js +642 -0
- package/dist/workspace-sync.d.ts +45 -97
- package/dist/workspace-sync.d.ts.map +1 -1
- package/dist/workspace-sync.js +53 -494
- package/package.json +1 -1
- package/src/auth-commands.ts +2 -2
- package/src/cli-console-output.ts +16 -2
- package/src/cli-stops.ts +27 -0
- package/src/egress-allowlist-cli.ts +18 -90
- package/src/egress-allowlist-workflow.ts +198 -0
- package/src/instruction-delivery.ts +31 -47
- package/src/instruction-prune.ts +59 -0
- package/src/instruction-set-sync/index.ts +3 -43
- package/src/instruction-set-update/index.ts +173 -122
- package/src/instruction-sets-cli.ts +23 -2
- package/src/instruction-sync-lock.ts +27 -0
- package/src/json-output-scrub.ts +73 -0
- package/src/one-request-write.ts +2 -10
- package/src/workbench-cli.ts +83 -684
- package/src/workbench-workflows.ts +815 -0
- package/src/workspace-sync-workflow.ts +838 -0
- package/src/workspace-sync.ts +63 -630
package/dist/workbench-cli.js
CHANGED
|
@@ -5,37 +5,8 @@
|
|
|
5
5
|
* do not print. The thin `workbenchCommand` entry point owns CLI argument
|
|
6
6
|
* parsing and human/JSON output.
|
|
7
7
|
*/
|
|
8
|
-
import { spawnSync } from 'child_process';
|
|
9
|
-
import { authedFetch } from './http.js';
|
|
10
8
|
import { findUnknownOption } from './option-validation.js';
|
|
11
|
-
import { hasStagedChanges, NOTHING_STAGED_ERROR, unstageOrizuSkillLinks } from './orizu-skill-links.js';
|
|
12
9
|
import { formatRunEventDigest } from './run-event-digest.js';
|
|
13
|
-
import { createStartIntentId, isAmbiguousStartResponseStatus, isStartIntentId, START_RESPONSE_LOSS_MAX_RETRIES, } from './start-intent.js';
|
|
14
|
-
import { getWorkspaceRoot, workspaceExists } from './workspace.js';
|
|
15
|
-
import { attachedWorkspaceId, stringOrNull } from './workspace-sync.js';
|
|
16
|
-
function defaultSessionGit(args, opts) {
|
|
17
|
-
try {
|
|
18
|
-
// Strip repo-context env (set by git when running inside hooks) so the
|
|
19
|
-
// child git is scoped strictly to cwd/args (same pattern as github-setup).
|
|
20
|
-
const env = { ...process.env };
|
|
21
|
-
delete env.GIT_DIR;
|
|
22
|
-
delete env.GIT_WORK_TREE;
|
|
23
|
-
delete env.GIT_INDEX_FILE;
|
|
24
|
-
const result = spawnSync('git', args, { cwd: opts?.cwd, encoding: 'utf8', env });
|
|
25
|
-
return {
|
|
26
|
-
status: result.status ?? 1,
|
|
27
|
-
stdout: typeof result.stdout === 'string' ? result.stdout : '',
|
|
28
|
-
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return { status: 1, stdout: '', stderr: 'git is not available' };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/** True when `root` is the workbench clone: contract marker + a git repo. */
|
|
36
|
-
function isWorkbenchClone(root, git) {
|
|
37
|
-
return workspaceExists(root) && git(['rev-parse', '--git-dir'], { cwd: root }).status === 0;
|
|
38
|
-
}
|
|
39
10
|
/** A bounded repository wait exhausted without losing the immutable identity
|
|
40
11
|
* needed to resume safely. CLI callers can show the command; library callers
|
|
41
12
|
* can use the structured fields directly. */
|
|
@@ -61,15 +32,7 @@ export class SessionRepositoryPendingError extends Error {
|
|
|
61
32
|
this.resumeCommand = resumeCommand;
|
|
62
33
|
}
|
|
63
34
|
}
|
|
64
|
-
const TERMINAL_RUN_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
|
|
65
35
|
const SESSION_STATUSES = new Set(['active', 'ended']);
|
|
66
|
-
const DEFAULT_TAIL_LIMIT = 100;
|
|
67
|
-
const MAX_TAIL_LIMIT = 500;
|
|
68
|
-
const DEFAULT_TAIL_INTERVAL_SECONDS = 2;
|
|
69
|
-
// Floor for the continuous poll sleep so `--interval 0` (or negatives) cannot
|
|
70
|
-
// hot-spin against the API while a non-terminal run is quiet. Draining full
|
|
71
|
-
// pages intentionally skips the sleep — that is catch-up, not polling.
|
|
72
|
-
const MIN_TAIL_INTERVAL_MS = 500;
|
|
73
36
|
const SESSION_START_OPTIONS = new Set([
|
|
74
37
|
'--project',
|
|
75
38
|
'--workspace',
|
|
@@ -91,547 +54,48 @@ const WORKBENCH_VALUE_OPTIONS = new Set([
|
|
|
91
54
|
'--title',
|
|
92
55
|
'--workspace',
|
|
93
56
|
]);
|
|
94
|
-
function defaultSleep(ms) {
|
|
95
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
96
|
-
}
|
|
97
57
|
export const SESSION_REPOSITORY_PENDING_RETRY_MS = 2_000;
|
|
98
58
|
export const SESSION_REPOSITORY_PENDING_MAX_RETRIES = 30;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return typeof limit === 'number' && Number.isFinite(limit) && limit > 0
|
|
104
|
-
? Math.min(Math.floor(limit), MAX_TAIL_LIMIT)
|
|
105
|
-
: DEFAULT_TAIL_LIMIT;
|
|
106
|
-
}
|
|
107
|
-
function normalizeProjectSlug(value) {
|
|
108
|
-
const raw = stringOrNull(value);
|
|
109
|
-
if (!raw) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const segments = raw.split('/').map(segment => segment.trim()).filter(Boolean);
|
|
113
|
-
return segments.length > 0 ? segments[segments.length - 1] : null;
|
|
114
|
-
}
|
|
115
|
-
function resolveAttachedWorkspaceId(opts) {
|
|
116
|
-
const explicit = stringOrNull(opts.workspaceId ?? undefined);
|
|
117
|
-
if (explicit) {
|
|
118
|
-
return explicit;
|
|
119
|
-
}
|
|
120
|
-
const root = getWorkspaceRoot(opts.cwd);
|
|
121
|
-
const attached = attachedWorkspaceId(root);
|
|
122
|
-
if (!attached) {
|
|
123
|
-
throw new Error('This workspace is not attached yet; run `orizu workspace sync` first to attach it.');
|
|
124
|
-
}
|
|
125
|
-
return attached;
|
|
126
|
-
}
|
|
127
|
-
async function readJson(response) {
|
|
128
|
-
try {
|
|
129
|
-
return await response.json();
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return {};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async function responseMessage(response) {
|
|
136
|
-
try {
|
|
137
|
-
const data = await response.clone().json();
|
|
138
|
-
return stringOrNull(data.error) || stringOrNull(data.message) || response.statusText || String(response.status);
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
try {
|
|
142
|
-
const text = await response.text();
|
|
143
|
-
return text.trim() || response.statusText || String(response.status);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return response.statusText || String(response.status);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async function assertOk(response, action) {
|
|
151
|
-
if (response.ok) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
throw new Error(`${action} failed (${response.status}): ${await responseMessage(response)}`);
|
|
155
|
-
}
|
|
156
|
-
function fetcherFrom(opts) {
|
|
157
|
-
return opts.fetcher ?? authedFetch;
|
|
158
|
-
}
|
|
159
|
-
function requiredBodyObject(data, key, action) {
|
|
160
|
-
const value = data[key];
|
|
161
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
162
|
-
throw new Error(`${action} response did not include ${key}`);
|
|
163
|
-
}
|
|
164
|
-
return value;
|
|
165
|
-
}
|
|
166
|
-
function sessionFrom(data, action) {
|
|
167
|
-
return requiredBodyObject(data, 'session', action);
|
|
168
|
-
}
|
|
169
|
-
function runFrom(data, action) {
|
|
170
|
-
return requiredBodyObject(data, 'run', action);
|
|
171
|
-
}
|
|
172
|
-
function sessionsFrom(data) {
|
|
173
|
-
return Array.isArray(data.sessions) ? data.sessions : [];
|
|
174
|
-
}
|
|
175
|
-
function eventsFrom(data) {
|
|
176
|
-
return Array.isArray(data.events) ? data.events : [];
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Fetch the freshly-cut remote session branch and check out a local tracking
|
|
180
|
-
* branch, so a human can start committing immediately. Best-effort by design:
|
|
181
|
-
* every failure (not the clone, no git, fetch/checkout error) returns a
|
|
182
|
-
* 'skipped' result with a human-usable detail — it NEVER throws, because
|
|
183
|
-
* checkout ergonomics must never fail a successfully started session.
|
|
184
|
-
*/
|
|
185
|
-
function checkoutSessionBranchLocally(cwd, branch, git) {
|
|
186
|
-
const root = getWorkspaceRoot(cwd);
|
|
187
|
-
try {
|
|
188
|
-
if (!isWorkbenchClone(root, git)) {
|
|
189
|
-
return { branch, state: 'skipped', detail: 'not inside the workbench clone' };
|
|
190
|
-
}
|
|
191
|
-
if (git(['fetch', 'origin', branch], { cwd: root }).status !== 0) {
|
|
192
|
-
return { branch, state: 'skipped', detail: `could not fetch origin/${branch}` };
|
|
193
|
-
}
|
|
194
|
-
const haveLocal = git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: root }).status === 0;
|
|
195
|
-
const checkout = haveLocal
|
|
196
|
-
? git(['checkout', branch], { cwd: root })
|
|
197
|
-
: git(['checkout', '-b', branch, '--track', `origin/${branch}`], { cwd: root });
|
|
198
|
-
if (checkout.status !== 0) {
|
|
199
|
-
const reason = checkout.stderr.trim() || `exit ${checkout.status}`;
|
|
200
|
-
return { branch, state: 'skipped', detail: `checkout failed: ${reason}` };
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
branch,
|
|
204
|
-
state: 'checked-out',
|
|
205
|
-
detail: haveLocal ? `switched to existing local branch ${branch}` : `tracking origin/${branch}`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
210
|
-
return { branch, state: 'skipped', detail: reason };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/** Local ahead/behind vs origin/<branch>; counts are null when not computable. */
|
|
214
|
-
function computeSessionBranchSync(cwd, branch, git) {
|
|
215
|
-
const root = getWorkspaceRoot(cwd);
|
|
216
|
-
try {
|
|
217
|
-
if (!isWorkbenchClone(root, git)) {
|
|
218
|
-
return { branch, ahead: null, behind: null };
|
|
219
|
-
}
|
|
220
|
-
const counts = git(['rev-list', '--left-right', '--count', `origin/${branch}...${branch}`], { cwd: root });
|
|
221
|
-
if (counts.status !== 0) {
|
|
222
|
-
return { branch, ahead: null, behind: null };
|
|
223
|
-
}
|
|
224
|
-
// `--left-right --count origin/b...b` prints "<origin-only>\t<local-only>".
|
|
225
|
-
const match = /^\s*(\d+)\s+(\d+)\s*$/.exec(counts.stdout);
|
|
226
|
-
if (!match) {
|
|
227
|
-
return { branch, ahead: null, behind: null };
|
|
228
|
-
}
|
|
229
|
-
return { branch, ahead: Number(match[2]), behind: Number(match[1]) };
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
return { branch, ahead: null, behind: null };
|
|
233
|
-
}
|
|
234
|
-
}
|
|
59
|
+
// The workflows run on Effect in workbench-workflows (ORI-2734), loaded only
|
|
60
|
+
// when a command runs, so CLI startup and the hosted session CLI never load
|
|
61
|
+
// Effect. These exports keep their signatures and add no stop handlers.
|
|
62
|
+
const workflows = () => import('./workbench-workflows.js');
|
|
235
63
|
export async function runSessionStart(opts = {}) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
239
|
-
const suppliedStartIntentId = stringOrNull(opts.startIntentId);
|
|
240
|
-
if (suppliedStartIntentId &&
|
|
241
|
-
!isStartIntentId(suppliedStartIntentId)) {
|
|
242
|
-
throw new Error('session start --start-intent must be a UUIDv4');
|
|
243
|
-
}
|
|
244
|
-
const startIntentId = suppliedStartIntentId ??
|
|
245
|
-
createStartIntentId(opts.generateStartIntentId);
|
|
246
|
-
const suppliedReconcileSessionId = stringOrNull(opts.reconcileSessionId);
|
|
247
|
-
if (suppliedReconcileSessionId && !suppliedStartIntentId) {
|
|
248
|
-
throw new Error('session start --session requires --start-intent');
|
|
249
|
-
}
|
|
250
|
-
const body = { startIntentId };
|
|
251
|
-
if (projectSlug) {
|
|
252
|
-
body.projectSlug = projectSlug;
|
|
253
|
-
}
|
|
254
|
-
if (opts.clientInfo) {
|
|
255
|
-
body.clientInfo = opts.clientInfo;
|
|
256
|
-
}
|
|
257
|
-
let response;
|
|
258
|
-
let data;
|
|
259
|
-
let reconcileSessionId = suppliedReconcileSessionId;
|
|
260
|
-
let repositoryPendingAttempts = 0;
|
|
261
|
-
let ambiguousResponseAttempts = 0;
|
|
262
|
-
for (;;) {
|
|
263
|
-
try {
|
|
264
|
-
response = await fetcher(`/api/cli/workspaces/${encodeURIComponent(workspaceId)}/sessions`, {
|
|
265
|
-
method: 'POST',
|
|
266
|
-
headers: { 'Content-Type': 'application/json' },
|
|
267
|
-
body: JSON.stringify({
|
|
268
|
-
...body,
|
|
269
|
-
...(reconcileSessionId
|
|
270
|
-
? { sessionId: reconcileSessionId }
|
|
271
|
-
: {}),
|
|
272
|
-
}),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
if (ambiguousResponseAttempts <
|
|
277
|
-
START_RESPONSE_LOSS_MAX_RETRIES) {
|
|
278
|
-
ambiguousResponseAttempts += 1;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
throw error;
|
|
282
|
-
}
|
|
283
|
-
data = await readJson(response.clone());
|
|
284
|
-
if (isAmbiguousStartResponseStatus(response.status) &&
|
|
285
|
-
ambiguousResponseAttempts <
|
|
286
|
-
START_RESPONSE_LOSS_MAX_RETRIES) {
|
|
287
|
-
ambiguousResponseAttempts += 1;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
const repository = data.repository &&
|
|
291
|
-
typeof data.repository === 'object' &&
|
|
292
|
-
!Array.isArray(data.repository)
|
|
293
|
-
? data.repository
|
|
294
|
-
: null;
|
|
295
|
-
const pendingSession = response.status === 202 &&
|
|
296
|
-
repository?.state === 'pending'
|
|
297
|
-
? sessionFrom(data, 'Session start')
|
|
298
|
-
: null;
|
|
299
|
-
const pendingSessionId = pendingSession
|
|
300
|
-
? stringOrNull(pendingSession.id)
|
|
301
|
-
: null;
|
|
302
|
-
if (pendingSessionId) {
|
|
303
|
-
if (repositoryPendingAttempts >=
|
|
304
|
-
SESSION_REPOSITORY_PENDING_MAX_RETRIES) {
|
|
305
|
-
throw new SessionRepositoryPendingError({
|
|
306
|
-
sessionId: pendingSessionId,
|
|
307
|
-
startIntentId,
|
|
308
|
-
projectSlug,
|
|
309
|
-
workspaceDirectory: stringOrNull(opts.cwd),
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
repositoryPendingAttempts += 1;
|
|
313
|
-
ambiguousResponseAttempts = 0;
|
|
314
|
-
reconcileSessionId = pendingSessionId;
|
|
315
|
-
const retryAfterSeconds = typeof repository?.retryAfterSeconds === 'number' &&
|
|
316
|
-
Number.isInteger(repository.retryAfterSeconds) &&
|
|
317
|
-
repository.retryAfterSeconds >= 1 &&
|
|
318
|
-
repository.retryAfterSeconds <= 60
|
|
319
|
-
? repository.retryAfterSeconds
|
|
320
|
-
: SESSION_REPOSITORY_PENDING_RETRY_MS / 1_000;
|
|
321
|
-
await (opts.sleep ?? defaultSleep)(retryAfterSeconds * 1_000);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
await assertOk(response, 'Session start');
|
|
327
|
-
const session = sessionFrom(data, 'Session start');
|
|
328
|
-
// The session is started; local checkout is best-effort ergonomics only,
|
|
329
|
-
// and only when explicitly requested (the CLI human path). The temporary
|
|
330
|
-
// Artifacts compatibility branch is `main` in a separate remote; never run
|
|
331
|
-
// the legacy `git fetch origin/main` flow against the caller's current
|
|
332
|
-
// checkout.
|
|
333
|
-
const branch = stringOrNull(session.repoBranch);
|
|
334
|
-
const artifactsRepositoryId = stringOrNull(session.workingRepositoryId);
|
|
335
|
-
const checkout = artifactsRepositoryId
|
|
336
|
-
? null
|
|
337
|
-
: branch && opts.localCheckout
|
|
338
|
-
? checkoutSessionBranchLocally(opts.cwd, branch, opts.git ?? defaultSessionGit)
|
|
339
|
-
: branch
|
|
340
|
-
? { branch, state: 'skipped', detail: 'local checkout not requested' }
|
|
341
|
-
: null;
|
|
342
|
-
return { session, checkout };
|
|
64
|
+
const { runPlain, sessionStart } = await workflows();
|
|
65
|
+
return runPlain(sessionStart(opts));
|
|
343
66
|
}
|
|
344
67
|
export async function runSessionStatus(opts = {}) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
if (sessionId) {
|
|
348
|
-
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(sessionId)}`);
|
|
349
|
-
await assertOk(response, 'Session status');
|
|
350
|
-
const data = await readJson(response);
|
|
351
|
-
const session = sessionFrom(data, 'Session status');
|
|
352
|
-
const branch = stringOrNull(session.repoBranch);
|
|
353
|
-
const artifactsRepositoryId = stringOrNull(session.workingRepositoryId);
|
|
354
|
-
const branchSync = branch && !artifactsRepositoryId
|
|
355
|
-
? computeSessionBranchSync(opts.cwd, branch, opts.git ?? defaultSessionGit)
|
|
356
|
-
: null;
|
|
357
|
-
return { session, branchSync };
|
|
358
|
-
}
|
|
359
|
-
const workspaceId = resolveAttachedWorkspaceId(opts);
|
|
360
|
-
const status = stringOrNull(opts.status ?? undefined);
|
|
361
|
-
const query = status ? `?status=${encodeURIComponent(status)}` : '';
|
|
362
|
-
const response = await fetcher(`/api/cli/workspaces/${encodeURIComponent(workspaceId)}/sessions${query}`);
|
|
363
|
-
await assertOk(response, 'Session status');
|
|
364
|
-
const data = await readJson(response);
|
|
365
|
-
return { sessions: sessionsFrom(data) };
|
|
68
|
+
const { runPlain, sessionStatus } = await workflows();
|
|
69
|
+
return runPlain(sessionStatus(opts));
|
|
366
70
|
}
|
|
367
71
|
export async function runSessionEnd(opts) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
throw new Error('Usage: orizu session end --session <id> [--json]');
|
|
371
|
-
}
|
|
372
|
-
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`, {
|
|
373
|
-
method: 'PATCH',
|
|
374
|
-
headers: { 'Content-Type': 'application/json' },
|
|
375
|
-
body: JSON.stringify({ status: 'ended' }),
|
|
376
|
-
});
|
|
377
|
-
await assertOk(response, 'Session end');
|
|
378
|
-
const data = await readJson(response);
|
|
379
|
-
return {
|
|
380
|
-
session: sessionFrom(data, 'Session end'),
|
|
381
|
-
branchStatus: stringOrNull(data.branchStatus) ?? undefined,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Finish-time local checks (ALI-1028): when run inside the workbench clone
|
|
386
|
-
* with the session branch checked out, uncommitted or unpushed local work is
|
|
387
|
-
* reported (it would silently miss the manifest otherwise), and the opt-in
|
|
388
|
-
* `--push` stages/commits/pushes it first. Raw git remains the default
|
|
389
|
-
* workflow — without --push nothing local is mutated.
|
|
390
|
-
*/
|
|
391
|
-
async function prepareLocalSessionBranch(opts, fetcher) {
|
|
392
|
-
const git = opts.git ?? defaultSessionGit;
|
|
393
|
-
const localWarnings = [];
|
|
394
|
-
// Resolve the server-owned repository mode before invoking Git at all.
|
|
395
|
-
// Artifacts temporarily exposes repoBranch=main for hosted compatibility,
|
|
396
|
-
// but that branch belongs to the private working repository. Treating it as
|
|
397
|
-
// a legacy branch in the caller's current checkout can stage, commit, and
|
|
398
|
-
// push origin/main before the server has a chance to reject finish-branch.
|
|
399
|
-
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(opts.sessionId)}`);
|
|
400
|
-
if (!response.ok) {
|
|
401
|
-
// Best-effort: the finish call right after will surface the real error.
|
|
402
|
-
return { localWarnings, pushed: false };
|
|
403
|
-
}
|
|
404
|
-
const session = sessionFrom(await readJson(response), 'Session status');
|
|
405
|
-
if (stringOrNull(session.workingRepositoryId)) {
|
|
406
|
-
if (opts.push) {
|
|
407
|
-
throw new Error('session finish --push is not supported for an Artifacts-backed session; the hosted session owns its private repository');
|
|
408
|
-
}
|
|
409
|
-
return { localWarnings, pushed: false };
|
|
410
|
-
}
|
|
411
|
-
const root = getWorkspaceRoot(opts.cwd);
|
|
412
|
-
if (!isWorkbenchClone(root, git)) {
|
|
413
|
-
if (opts.push) {
|
|
414
|
-
throw new Error('session finish --push must run inside the workbench clone (it stages, commits, and pushes the session branch checkout)');
|
|
415
|
-
}
|
|
416
|
-
return { localWarnings, pushed: false };
|
|
417
|
-
}
|
|
418
|
-
const branch = stringOrNull(session.repoBranch);
|
|
419
|
-
if (!branch) {
|
|
420
|
-
return { localWarnings, pushed: false };
|
|
421
|
-
}
|
|
422
|
-
const head = git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
|
|
423
|
-
const onSessionBranch = head.status === 0 && head.stdout.trim() === branch;
|
|
424
|
-
if (!onSessionBranch) {
|
|
425
|
-
if (opts.push) {
|
|
426
|
-
const current = head.status === 0 ? head.stdout.trim() : '(unknown)';
|
|
427
|
-
throw new Error(`session finish --push requires the session branch checkout: HEAD is ${current}, not ${branch}`);
|
|
428
|
-
}
|
|
429
|
-
return { localWarnings, pushed: false };
|
|
430
|
-
}
|
|
431
|
-
const status = git(['status', '--porcelain'], { cwd: root });
|
|
432
|
-
const hasUncommitted = status.status === 0 && status.stdout.trim() !== '';
|
|
433
|
-
if (opts.push) {
|
|
434
|
-
if (hasUncommitted) {
|
|
435
|
-
const add = git(['add', '-A'], { cwd: root });
|
|
436
|
-
if (add.status !== 0) {
|
|
437
|
-
throw new Error(`session finish --push: git add failed: ${add.stderr.trim() || `exit ${add.status}`}`);
|
|
438
|
-
}
|
|
439
|
-
// A hosted session's Orizu skill links are sandbox-local; never ship
|
|
440
|
-
// them (ORI-2442). Commit only if something else is still staged.
|
|
441
|
-
const skillGit = (args) => {
|
|
442
|
-
const result = git([...args], { cwd: root });
|
|
443
|
-
return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
444
|
-
};
|
|
445
|
-
const unstage = unstageOrizuSkillLinks(skillGit);
|
|
446
|
-
if (!unstage.ok) {
|
|
447
|
-
throw new Error(`session finish --push: ${unstage.error}`);
|
|
448
|
-
}
|
|
449
|
-
const staged = hasStagedChanges(skillGit);
|
|
450
|
-
if (typeof staged !== 'boolean') {
|
|
451
|
-
throw new Error(`session finish --push: ${staged.error}`);
|
|
452
|
-
}
|
|
453
|
-
if (!staged && unstage.unstaged.length === 0) {
|
|
454
|
-
throw new Error(`session finish --push: ${NOTHING_STAGED_ERROR}`);
|
|
455
|
-
}
|
|
456
|
-
if (staged) {
|
|
457
|
-
const message = stringOrNull(opts.message) ?? `Session ${opts.sessionId} work (orizu session finish --push)`;
|
|
458
|
-
const commit = git(['commit', '-m', message], { cwd: root });
|
|
459
|
-
if (commit.status !== 0) {
|
|
460
|
-
throw new Error(`session finish --push: git commit failed: ${commit.stderr.trim() || `exit ${commit.status}`}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const push = git(['push', 'origin', branch], { cwd: root });
|
|
465
|
-
if (push.status !== 0) {
|
|
466
|
-
throw new Error(`session finish --push: git push failed: ${push.stderr.trim() || `exit ${push.status}`}`);
|
|
467
|
-
}
|
|
468
|
-
return { localWarnings, pushed: true };
|
|
469
|
-
}
|
|
470
|
-
if (hasUncommitted) {
|
|
471
|
-
localWarnings.push(`uncommitted changes in your ${branch} checkout — they won't be in the manifest. ` +
|
|
472
|
-
`Commit and push them first (git add/commit/push), or re-run with --push.`);
|
|
473
|
-
}
|
|
474
|
-
const counts = git(['rev-list', '--count', `origin/${branch}..${branch}`], { cwd: root });
|
|
475
|
-
const unpushed = counts.status === 0 ? Number.parseInt(counts.stdout.trim(), 10) : 0;
|
|
476
|
-
if (Number.isFinite(unpushed) && unpushed > 0) {
|
|
477
|
-
localWarnings.push(`${unpushed} unpushed commit${unpushed === 1 ? '' : 's'} on ${branch} — they won't be in the manifest. ` +
|
|
478
|
-
`Run \`git push origin ${branch}\` first, or re-run with --push.`);
|
|
479
|
-
}
|
|
480
|
-
return { localWarnings, pushed: false };
|
|
72
|
+
const { runPlain, sessionEnd } = await workflows();
|
|
73
|
+
return runPlain(sessionEnd(opts));
|
|
481
74
|
}
|
|
482
75
|
export async function runSessionFinish(opts) {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
throw new Error('Usage: orizu session finish --session <id> [--project <team/project>] [--push [--message <text>]] [--json]');
|
|
486
|
-
}
|
|
487
|
-
const { localWarnings, pushed } = await prepareLocalSessionBranch(opts, fetcher);
|
|
488
|
-
const body = {};
|
|
489
|
-
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
490
|
-
if (projectSlug) {
|
|
491
|
-
body.projectSlug = projectSlug;
|
|
492
|
-
}
|
|
493
|
-
const response = await fetcher(`/api/cli/sessions/${encodeURIComponent(opts.sessionId)}/finish-branch`, {
|
|
494
|
-
method: 'POST',
|
|
495
|
-
headers: { 'Content-Type': 'application/json' },
|
|
496
|
-
body: JSON.stringify(body),
|
|
497
|
-
});
|
|
498
|
-
await assertOk(response, 'Session finish');
|
|
499
|
-
const data = await readJson(response);
|
|
500
|
-
const manifest = data.manifest && typeof data.manifest === 'object' && !Array.isArray(data.manifest)
|
|
501
|
-
? data.manifest
|
|
502
|
-
: undefined;
|
|
503
|
-
return {
|
|
504
|
-
outcome: stringOrNull(data.outcome) || 'unknown',
|
|
505
|
-
branch: stringOrNull(data.branch) ?? undefined,
|
|
506
|
-
branchDeleted: typeof data.branchDeleted === 'boolean' ? data.branchDeleted : undefined,
|
|
507
|
-
manifest,
|
|
508
|
-
localWarnings: localWarnings.length > 0 ? localWarnings : undefined,
|
|
509
|
-
pushed: pushed || undefined,
|
|
510
|
-
};
|
|
76
|
+
const { runPlain, sessionFinish } = await workflows();
|
|
77
|
+
return runPlain(sessionFinish(opts));
|
|
511
78
|
}
|
|
512
79
|
export async function runWorkbenchRunStart(opts) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
throw new Error('Usage: orizu run start --session <id> --title <title> [--project <team/project>] [--json]');
|
|
516
|
-
}
|
|
517
|
-
const projectSlug = normalizeProjectSlug(opts.projectSlug);
|
|
518
|
-
const body = {
|
|
519
|
-
workspaceSessionId: opts.sessionId,
|
|
520
|
-
title: opts.title,
|
|
521
|
-
};
|
|
522
|
-
if (projectSlug) {
|
|
523
|
-
body.projectSlug = projectSlug;
|
|
524
|
-
}
|
|
525
|
-
const response = await fetcher('/api/cli/workbench-runs', {
|
|
526
|
-
method: 'POST',
|
|
527
|
-
headers: { 'Content-Type': 'application/json' },
|
|
528
|
-
body: JSON.stringify(body),
|
|
529
|
-
});
|
|
530
|
-
if (response.status === 409) {
|
|
531
|
-
// PR #1284 contract: starting a run on an ended session is a 409.
|
|
532
|
-
const message = await responseMessage(response);
|
|
533
|
-
const hint = /ended/i.test(message) ? ' Run `orizu session start` to open a new session.' : '';
|
|
534
|
-
throw new Error(`Run start failed (409): ${message}${hint}`);
|
|
535
|
-
}
|
|
536
|
-
await assertOk(response, 'Run start');
|
|
537
|
-
const data = await readJson(response);
|
|
538
|
-
return { run: runFrom(data, 'Run start') };
|
|
80
|
+
const { runPlain, runStart } = await workflows();
|
|
81
|
+
return runPlain(runStart(opts));
|
|
539
82
|
}
|
|
540
83
|
export async function runWorkbenchRunStatus(opts) {
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
throw new Error('Usage: orizu run status --run <id> [--json]');
|
|
544
|
-
}
|
|
545
|
-
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(opts.runId)}`);
|
|
546
|
-
await assertOk(response, 'Run status');
|
|
547
|
-
const data = await readJson(response);
|
|
548
|
-
return { run: runFrom(data, 'Run status') };
|
|
84
|
+
const { runPlain, runStatus } = await workflows();
|
|
85
|
+
return runPlain(runStatus(opts));
|
|
549
86
|
}
|
|
550
87
|
export async function runWorkbenchRunTerminal(opts) {
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
body.summary = { note: opts.summary };
|
|
558
|
-
}
|
|
559
|
-
if (opts.evidence) {
|
|
560
|
-
body.evidence = opts.evidence;
|
|
561
|
-
}
|
|
562
|
-
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(opts.runId)}`, {
|
|
563
|
-
method: 'PATCH',
|
|
564
|
-
headers: { 'Content-Type': 'application/json' },
|
|
565
|
-
body: JSON.stringify(body),
|
|
566
|
-
});
|
|
567
|
-
if (response.status === 409) {
|
|
568
|
-
// PR #1284 contract: "Run already <status>" names the STORED status when a
|
|
569
|
-
// different terminal status was requested — report it plus a status hint.
|
|
570
|
-
const message = await responseMessage(response);
|
|
571
|
-
const alreadyMatch = /^Run already (\S+)/.exec(message);
|
|
572
|
-
if (alreadyMatch) {
|
|
573
|
-
throw new Error(`Run update rejected (409): run is already '${alreadyMatch[1]}' (requested '${opts.status}'). Check \`orizu run status --run ${opts.runId}\`.`);
|
|
574
|
-
}
|
|
575
|
-
throw new Error(`Run update failed (409): ${message}`);
|
|
576
|
-
}
|
|
577
|
-
await assertOk(response, 'Run update');
|
|
578
|
-
const data = await readJson(response);
|
|
579
|
-
return { run: runFrom(data, 'Run update') };
|
|
580
|
-
}
|
|
581
|
-
export async function tailOnce(fetcher, runId, cursor, limit = DEFAULT_TAIL_LIMIT) {
|
|
582
|
-
// The API requires an integer cursor: only send back whole non-negative
|
|
583
|
-
// sequences (PR #1284 contract — non-numeric/negative `after` is a 400).
|
|
584
|
-
const safeCursor = Number.isFinite(cursor) && cursor > 0 ? Math.floor(cursor) : 0;
|
|
585
|
-
const safeLimit = normalizeTailLimit(limit);
|
|
586
|
-
const response = await fetcher(`/api/cli/workbench-runs/${encodeURIComponent(runId)}/events?after=${safeCursor}&limit=${safeLimit}`);
|
|
587
|
-
await assertOk(response, 'Run tail');
|
|
588
|
-
const data = await readJson(response);
|
|
589
|
-
return {
|
|
590
|
-
events: eventsFrom(data),
|
|
591
|
-
cursor: numberOrFallback(data.cursor, safeCursor),
|
|
592
|
-
runStatus: stringOrNull(data.runStatus) || 'unknown',
|
|
593
|
-
};
|
|
88
|
+
const { runPlain, runTerminal } = await workflows();
|
|
89
|
+
return runPlain(runTerminal(opts));
|
|
90
|
+
}
|
|
91
|
+
export async function tailOnce(fetcher, runId, cursor, limit) {
|
|
92
|
+
const workflow = await workflows();
|
|
93
|
+
return workflow.runPlain(workflow.tailOnce(fetcher, runId, cursor, limit));
|
|
594
94
|
}
|
|
95
|
+
/** Also the hosted session CLI's attached tail: same signature, no stop handlers. */
|
|
595
96
|
export async function tailWorkbenchRun(opts) {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
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)');
|
|
599
|
-
}
|
|
600
|
-
const sleep = opts.sleep ?? defaultSleep;
|
|
601
|
-
let cursor = typeof opts.after === 'number' && Number.isFinite(opts.after) && opts.after > 0 ? Math.floor(opts.after) : 0;
|
|
602
|
-
const intervalMs = Math.max(MIN_TAIL_INTERVAL_MS, Math.round((opts.intervalSeconds ?? DEFAULT_TAIL_INTERVAL_SECONDS) * 1000));
|
|
603
|
-
const pageLimit = normalizeTailLimit(opts.limit);
|
|
604
|
-
while (true) {
|
|
605
|
-
const previousCursor = cursor;
|
|
606
|
-
const result = await tailOnce(fetcher, opts.runId, cursor, opts.limit);
|
|
607
|
-
cursor = result.cursor;
|
|
608
|
-
for (const event of result.events) {
|
|
609
|
-
opts.onEvent?.(event);
|
|
610
|
-
}
|
|
611
|
-
// A full page means more events may already be persisted — keep draining
|
|
612
|
-
// back-to-back (no sleep). Exit conditions are only evaluated on a SHORT
|
|
613
|
-
// page: a terminal runStatus on a full page would otherwise truncate a
|
|
614
|
-
// completed run that has more unread pages.
|
|
615
|
-
if (result.events.length >= pageLimit) {
|
|
616
|
-
// Protocol invariant: a full page always implies the server advanced
|
|
617
|
-
// the cursor past what we asked for. If it didn't, draining back-to-back
|
|
618
|
-
// would spin hot forever on a misbehaving/stalled server. We pick the
|
|
619
|
-
// simpler of the two acceptable fixes (apply the normal poll sleep
|
|
620
|
-
// before retrying, rather than failing after one retry) because it
|
|
621
|
-
// matches this loop's existing backoff-on-no-progress shape (see the
|
|
622
|
-
// MIN_TAIL_INTERVAL_MS floor above) without introducing a new
|
|
623
|
-
// retry-count/error path.
|
|
624
|
-
if (cursor > previousCursor) {
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
await sleep(intervalMs);
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
if (opts.once || TERMINAL_RUN_STATUSES.has(result.runStatus)) {
|
|
631
|
-
return result;
|
|
632
|
-
}
|
|
633
|
-
await sleep(intervalMs);
|
|
634
|
-
}
|
|
97
|
+
const { runPlain, tail } = await workflows();
|
|
98
|
+
return runPlain(tail(opts));
|
|
635
99
|
}
|
|
636
100
|
function hasFlag(args, flag) {
|
|
637
101
|
return args.includes(flag);
|
|
@@ -880,6 +344,25 @@ export async function workbenchCommand(args, io) {
|
|
|
880
344
|
throw error;
|
|
881
345
|
}
|
|
882
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Ctrl-C (130) and SIGTERM (143) end a local command's waits and reads. A
|
|
349
|
+
* write already sent finishes and its reply prints before the stop's exit
|
|
350
|
+
* code. A session start stopped while its repository is pending prints how to
|
|
351
|
+
* resume it.
|
|
352
|
+
*/
|
|
353
|
+
function finishStoppable(io, outcome, print) {
|
|
354
|
+
if ('value' in outcome) {
|
|
355
|
+
print(outcome.value);
|
|
356
|
+
return outcome.interrupted ?? 0;
|
|
357
|
+
}
|
|
358
|
+
if (outcome.pending) {
|
|
359
|
+
const { sessionId, startIntentId, resumeCommand } = new SessionRepositoryPendingError(outcome.pending);
|
|
360
|
+
const message = 'Session start interrupted while its repository is still provisioning. ' +
|
|
361
|
+
`Resume the same session without creating a duplicate: ${resumeCommand}`;
|
|
362
|
+
io.print(io.json ? JSON.stringify({ error: message, sessionId, startIntentId, resumeCommand }) : message);
|
|
363
|
+
}
|
|
364
|
+
return outcome.interrupted;
|
|
365
|
+
}
|
|
883
366
|
async function dispatchWorkbenchCommand(args, io) {
|
|
884
367
|
const positional = positionalArgs(args);
|
|
885
368
|
const group = positional[0];
|
|
@@ -906,7 +389,8 @@ async function dispatchWorkbenchCommand(args, io) {
|
|
|
906
389
|
if (unknownOption) {
|
|
907
390
|
return emitJsonError(io, `unknown option ${unknownOption}\nUsage: orizu session start [--project <team/project>] [--workspace <dir>] [--start-intent <uuid> --session <uuid>] [--json]`);
|
|
908
391
|
}
|
|
909
|
-
const
|
|
392
|
+
const { runStoppable, sessionStart } = await workflows();
|
|
393
|
+
const outcome = await runStoppable((settled, stop) => sessionStart({
|
|
910
394
|
fetcher,
|
|
911
395
|
cwd: workspaceDir,
|
|
912
396
|
git: io.git,
|
|
@@ -917,32 +401,33 @@ async function dispatchWorkbenchCommand(args, io) {
|
|
|
917
401
|
clientInfo: { source: 'orizu-cli' },
|
|
918
402
|
generateStartIntentId: io.generateStartIntentId,
|
|
919
403
|
sleep: io.sleep,
|
|
920
|
-
});
|
|
921
|
-
emit(io, formatSessionStart(result), result);
|
|
922
|
-
return 0;
|
|
404
|
+
}, settled, stop));
|
|
405
|
+
return finishStoppable(io, outcome, result => emit(io, formatSessionStart(result), result));
|
|
923
406
|
}
|
|
924
407
|
if (subcommand === 'status') {
|
|
925
408
|
const status = argValue(args, '--status');
|
|
926
409
|
if (status && !SESSION_STATUSES.has(status)) {
|
|
927
410
|
throw new Error('Usage: orizu session status [--session <id> | --status active|ended] [--json]');
|
|
928
411
|
}
|
|
929
|
-
const
|
|
412
|
+
const { runStoppable, sessionStatus } = await workflows();
|
|
413
|
+
const outcome = await runStoppable(() => sessionStatus({
|
|
930
414
|
fetcher,
|
|
931
415
|
cwd: workspaceDir,
|
|
932
416
|
git: io.git,
|
|
933
417
|
sessionId: argValue(args, '--session'),
|
|
934
418
|
status,
|
|
935
|
-
});
|
|
936
|
-
emit(io, formatSessionStatus(result), result);
|
|
937
|
-
return 0;
|
|
419
|
+
}, true));
|
|
420
|
+
return finishStoppable(io, outcome, result => emit(io, formatSessionStatus(result), result));
|
|
938
421
|
}
|
|
939
422
|
if (subcommand === 'end') {
|
|
940
|
-
const
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
423
|
+
const { runStoppable, sessionEnd } = await workflows();
|
|
424
|
+
const outcome = await runStoppable(settled => sessionEnd({ fetcher, sessionId: argValue(args, '--session') || '' }, settled));
|
|
425
|
+
return finishStoppable(io, outcome, result => {
|
|
426
|
+
const hint = result.branchStatus === 'has-changes'
|
|
427
|
+
? '\nsession branch has changes — run `orizu session finish --session ' + result.session.id + '` to promote them'
|
|
428
|
+
: '';
|
|
429
|
+
emit(io, `session ended: ${result.session.id}${hint}`, result);
|
|
430
|
+
});
|
|
946
431
|
}
|
|
947
432
|
if (subcommand === 'finish') {
|
|
948
433
|
const message = argValue(args, '--message');
|
|
@@ -950,7 +435,8 @@ async function dispatchWorkbenchCommand(args, io) {
|
|
|
950
435
|
if (message && !push) {
|
|
951
436
|
throw new Error('`--message` only applies with `--push` (orizu session finish --session <id> --push --message <text>)');
|
|
952
437
|
}
|
|
953
|
-
const
|
|
438
|
+
const { runStoppable, sessionFinish } = await workflows();
|
|
439
|
+
const outcome = await runStoppable(settled => sessionFinish({
|
|
954
440
|
fetcher,
|
|
955
441
|
cwd: workspaceDir,
|
|
956
442
|
git: io.git,
|
|
@@ -958,29 +444,29 @@ async function dispatchWorkbenchCommand(args, io) {
|
|
|
958
444
|
projectSlug: argValue(args, '--project'),
|
|
959
445
|
push,
|
|
960
446
|
message,
|
|
961
|
-
});
|
|
962
|
-
emit(io, formatSessionFinish(result), result);
|
|
963
|
-
return 0;
|
|
447
|
+
}, settled, true));
|
|
448
|
+
return finishStoppable(io, outcome, result => emit(io, formatSessionFinish(result), result));
|
|
964
449
|
}
|
|
965
450
|
}
|
|
966
451
|
if (group === 'run') {
|
|
967
452
|
if (subcommand === 'start') {
|
|
968
|
-
const
|
|
453
|
+
const { runStoppable, runStart } = await workflows();
|
|
454
|
+
const outcome = await runStoppable(settled => runStart({
|
|
969
455
|
fetcher,
|
|
970
456
|
sessionId: argValue(args, '--session') || '',
|
|
971
457
|
title: argValue(args, '--title') || '',
|
|
972
458
|
projectSlug: argValue(args, '--project'),
|
|
973
|
-
});
|
|
974
|
-
emit(io, `run started: ${result.run.id}`, result);
|
|
975
|
-
return 0;
|
|
459
|
+
}, settled));
|
|
460
|
+
return finishStoppable(io, outcome, result => emit(io, `run started: ${result.run.id}`, result));
|
|
976
461
|
}
|
|
977
462
|
if (subcommand === 'status') {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
return
|
|
463
|
+
const { runStoppable, runStatus } = await workflows();
|
|
464
|
+
const outcome = await runStoppable(() => runStatus({ fetcher, runId: argValue(args, '--run') || '' }, true));
|
|
465
|
+
return finishStoppable(io, outcome, result => emit(io, formatRunStatus(result.run), result));
|
|
981
466
|
}
|
|
982
467
|
if (subcommand === 'tail') {
|
|
983
|
-
await
|
|
468
|
+
const { runStoppable, tail } = await workflows();
|
|
469
|
+
const outcome = await runStoppable(() => tail({
|
|
984
470
|
fetcher,
|
|
985
471
|
runId: argValue(args, '--run') || '',
|
|
986
472
|
after: numberArg(args, '--after'),
|
|
@@ -988,19 +474,19 @@ async function dispatchWorkbenchCommand(args, io) {
|
|
|
988
474
|
once: hasFlag(args, '--once'),
|
|
989
475
|
sleep: io.sleep,
|
|
990
476
|
onEvent: event => io.print(io.json ? JSON.stringify(event) : formatEventLine(event)),
|
|
991
|
-
});
|
|
992
|
-
return
|
|
477
|
+
}, true));
|
|
478
|
+
return finishStoppable(io, outcome, () => { });
|
|
993
479
|
}
|
|
994
480
|
const status = terminalStatusForCommand(subcommand || '');
|
|
995
481
|
if (status) {
|
|
996
|
-
const
|
|
482
|
+
const { runStoppable, runTerminal } = await workflows();
|
|
483
|
+
const outcome = await runStoppable(settled => runTerminal({
|
|
997
484
|
fetcher,
|
|
998
485
|
runId: argValue(args, '--run') || '',
|
|
999
486
|
status,
|
|
1000
487
|
summary: argValue(args, '--summary'),
|
|
1001
|
-
});
|
|
1002
|
-
emit(io, formatRunStatus(result.run), result);
|
|
1003
|
-
return 0;
|
|
488
|
+
}, settled));
|
|
489
|
+
return finishStoppable(io, outcome, result => emit(io, formatRunStatus(result.run), result));
|
|
1004
490
|
}
|
|
1005
491
|
}
|
|
1006
492
|
return emitJsonError(io, USAGE_LINE);
|