@shardflux/cli 0.1.0 → 0.2.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 +70 -8
- package/dist/args.d.ts +10 -1
- package/dist/args.js +326 -12
- package/dist/commands.d.ts +9 -1
- package/dist/commands.js +585 -11
- package/dist/format.d.ts +20 -1
- package/dist/format.js +132 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +5 -1
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -7,13 +7,29 @@
|
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { SDK_VERSION, ShardfluxApiError } from '@shardflux/sdk';
|
|
10
|
-
import { CLI_VERSION, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
10
|
+
import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
11
11
|
import { InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
12
|
-
import { fileTable, json, kv, operationDetail, operationTable, sessionTable, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
12
|
+
import { bindingsText, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, fileTable, json, kv, operationDetail, operationTable, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateEntryText, templateFileTable, templateTable, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
13
13
|
const DEFAULT_WAIT_MS = 300_000;
|
|
14
14
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
15
15
|
const MAX_STDIN = 1024 * 1024;
|
|
16
16
|
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
17
|
+
/**
|
|
18
|
+
* The workspace's cell client as this invocation configures it: the CLI's agent label, wake on use unless turned off,
|
|
19
|
+
* and the wake timeout (an OperationTimeoutError past it exits 5).
|
|
20
|
+
*/
|
|
21
|
+
const cellOf = (ctx, ws) => ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs, ...(ctx.wake ? {} : { wake: null }) });
|
|
22
|
+
const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
|
|
23
|
+
const MAX_SECRET_VALUE_BYTES = 65_536;
|
|
24
|
+
const TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
|
|
25
|
+
function secretNames(names, option) {
|
|
26
|
+
for (const n of names)
|
|
27
|
+
if (!SECRET_NAME.test(n))
|
|
28
|
+
throw new UsageError(`${option} expects a secret name like OPENAI_API_KEY (^[A-Z_][A-Z0-9_]*$), got "${n}"`);
|
|
29
|
+
if (new Set(names).size !== names.length)
|
|
30
|
+
throw new UsageError(`${option} names a secret twice`);
|
|
31
|
+
return [...names];
|
|
32
|
+
}
|
|
17
33
|
function out(ctx, human, machine) {
|
|
18
34
|
ctx.io.stdout.write(ctx.json ? json(machine) : human);
|
|
19
35
|
}
|
|
@@ -34,7 +50,11 @@ function waitTimeout(values) {
|
|
|
34
50
|
const t = str(values.timeout);
|
|
35
51
|
return t === undefined ? DEFAULT_WAIT_MS : parseDuration(t, 'timeout');
|
|
36
52
|
}
|
|
37
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
|
|
55
|
+
* lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
|
|
56
|
+
* workspace over tombstones of ended sessions that had the same key (contracts §19.11).
|
|
57
|
+
*/
|
|
38
58
|
export async function resolveWorkspace(ctx, ref) {
|
|
39
59
|
const cloud = ctx.cloud();
|
|
40
60
|
if (UUID.test(ref)) {
|
|
@@ -46,14 +66,87 @@ export async function resolveWorkspace(ctx, ref) {
|
|
|
46
66
|
throw err;
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
let found;
|
|
70
|
+
try {
|
|
71
|
+
found = await cloud.workspaces.findByKey(ref, { signal: ctx.signal });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
52
74
|
if (ctx.signal.aborted)
|
|
53
75
|
throw new InterruptedError('interrupted');
|
|
76
|
+
throw err;
|
|
54
77
|
}
|
|
78
|
+
if (found)
|
|
79
|
+
return found;
|
|
55
80
|
throw new NotFoundError(`No workspace with id or key "${ref}" in this project.`);
|
|
56
81
|
}
|
|
82
|
+
const LIFETIMES = ['persistent', 'session'];
|
|
83
|
+
const LIFETIME_FILTERS = ['persistent', 'session', 'any'];
|
|
84
|
+
const PURPOSE_FILTERS = ['standard', 'template_draft', 'template_test', 'any'];
|
|
85
|
+
const DIFF_CHANGES = ['added', 'removed', 'changed', 'type_changed', 'metadata'];
|
|
86
|
+
const OWNERS = ['platform', 'organization'];
|
|
87
|
+
const DEFAULT_BUILD_WAIT_MS = 1_800_000;
|
|
88
|
+
function oneOf(values, option, allowed) {
|
|
89
|
+
const v = str(values[option]);
|
|
90
|
+
if (v === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!allowed.includes(v))
|
|
93
|
+
throw new UsageError(`--${option} must be one of ${allowed.join(', ')} (got "${v}")`);
|
|
94
|
+
return v;
|
|
95
|
+
}
|
|
96
|
+
function listLimit(values, max) {
|
|
97
|
+
const t = str(values.limit);
|
|
98
|
+
return t === undefined ? undefined : parsePositiveInt(t, 'limit', max);
|
|
99
|
+
}
|
|
100
|
+
/** `--default-lifetime` / `--default-idle-timeout` as the new version's `defaults` (undefined when neither is given). */
|
|
101
|
+
function saveDefaults(values) {
|
|
102
|
+
const lifetime = oneOf(values, 'default-lifetime', LIFETIMES);
|
|
103
|
+
const idle = str(values['default-idle-timeout']);
|
|
104
|
+
if (lifetime === undefined && idle === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const d = {};
|
|
107
|
+
if (lifetime !== undefined)
|
|
108
|
+
d.lifetime = lifetime;
|
|
109
|
+
if (idle !== undefined) {
|
|
110
|
+
const seconds = Math.round(parseDuration(idle, 'default-idle-timeout') / 1000);
|
|
111
|
+
if (seconds < 60 || seconds > 86_400)
|
|
112
|
+
throw new UsageError('--default-idle-timeout must be between 60s and 24h');
|
|
113
|
+
d.idle_timeout_seconds = seconds;
|
|
114
|
+
}
|
|
115
|
+
return d;
|
|
116
|
+
}
|
|
117
|
+
function acknowledged(values) {
|
|
118
|
+
const paths = strs(values.acknowledge);
|
|
119
|
+
for (const p of paths)
|
|
120
|
+
if (!p.startsWith('/'))
|
|
121
|
+
throw new UsageError(`--acknowledge expects an absolute path, got "${p}"`);
|
|
122
|
+
if (paths.length > 200)
|
|
123
|
+
throw new UsageError('--acknowledge takes at most 200 paths');
|
|
124
|
+
return paths.length ? paths : undefined;
|
|
125
|
+
}
|
|
126
|
+
/** Prints a save-as-template / draft publish result; `--wait` follows the build until it settles. */
|
|
127
|
+
async function reportSave(ctx, values, res, what) {
|
|
128
|
+
let build = res.build;
|
|
129
|
+
if (values.wait === true) {
|
|
130
|
+
const t = str(values.timeout);
|
|
131
|
+
const timeoutMs = t === undefined ? DEFAULT_BUILD_WAIT_MS : parseDuration(t, 'timeout');
|
|
132
|
+
try {
|
|
133
|
+
build = await ctx.cloud().templates.builds.waitForBuild(build.organization_id, build.id, { timeoutMs, signal: ctx.signal });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (ctx.signal.aborted)
|
|
137
|
+
throw new InterruptedError(`Interrupted; build ${build.id} continues server side.`);
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const capture = res.operation ? `capture: operation ${res.operation.id} ${res.operation.kind} ${res.operation.state}\n` : '';
|
|
142
|
+
out(ctx, `${what}\n${capture}${buildText(build)}`, { operation: res.operation, build });
|
|
143
|
+
return build.state === 'failed' || build.state === 'canceled' || build.registration.state === 'failed' ? 6 : 0;
|
|
144
|
+
}
|
|
145
|
+
function parseVersion(text, what) {
|
|
146
|
+
if (!/^\d+$/.test(text) || Number(text) < 1)
|
|
147
|
+
throw new UsageError(`${what} must be a version number (got "${text}")`);
|
|
148
|
+
return Number(text);
|
|
149
|
+
}
|
|
57
150
|
/** Waits with the SDK's backoff; Ctrl-C reports the operation that keeps running. */
|
|
58
151
|
async function waitOperation(ctx, operationId, timeoutMs) {
|
|
59
152
|
try {
|
|
@@ -162,9 +255,19 @@ export const HANDLERS = {
|
|
|
162
255
|
if (!template)
|
|
163
256
|
throw new UsageError('--template <slug> is required for "shard workspaces open"');
|
|
164
257
|
const c = caps(values);
|
|
258
|
+
const bind = secretNames(strs(values.secret), '--secret');
|
|
259
|
+
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
165
260
|
const timeoutMs = waitTimeout(values);
|
|
166
261
|
const cloud = ctx.cloud();
|
|
167
|
-
let ws = await cloud.workspaces.open({
|
|
262
|
+
let ws = await cloud.workspaces.open({
|
|
263
|
+
key: key,
|
|
264
|
+
template,
|
|
265
|
+
...(c ? { caps: c } : {}),
|
|
266
|
+
...(bind.length ? { secrets: bind } : {}),
|
|
267
|
+
...(lifetime ? { lifetime } : {}),
|
|
268
|
+
agentLabel: ctx.agentLabel,
|
|
269
|
+
wait: false,
|
|
270
|
+
});
|
|
168
271
|
const op = ws.activeOperation;
|
|
169
272
|
if (values.wait !== false && !ws.ready && op) {
|
|
170
273
|
await waitOperation(ctx, op.id, timeoutMs);
|
|
@@ -182,9 +285,13 @@ export const HANDLERS = {
|
|
|
182
285
|
const state = str(values.state);
|
|
183
286
|
const prefix = str(values.prefix);
|
|
184
287
|
const limitText = str(values.limit);
|
|
288
|
+
const lifetime = oneOf(values, 'lifetime', LIFETIME_FILTERS);
|
|
289
|
+
const purpose = oneOf(values, 'purpose', PURPOSE_FILTERS);
|
|
185
290
|
const params = {
|
|
186
291
|
...(state ? { state: state } : {}),
|
|
187
292
|
...(prefix ? { keyPrefix: prefix } : {}),
|
|
293
|
+
...(lifetime ? { lifetime } : {}),
|
|
294
|
+
...(purpose ? { purpose } : {}),
|
|
188
295
|
...(values['include-deleted'] === true ? { includeDeleted: true } : {}),
|
|
189
296
|
...(limitText ? { limit: parsePositiveInt(limitText, 'limit', 200) } : {}),
|
|
190
297
|
};
|
|
@@ -220,7 +327,7 @@ export const HANDLERS = {
|
|
|
220
327
|
const stdinSource = str(values.stdin);
|
|
221
328
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
222
329
|
const ws = await resolveWorkspace(ctx, ref);
|
|
223
|
-
const cell =
|
|
330
|
+
const cell = cellOf(ctx, ws);
|
|
224
331
|
const sessionId = `cli-${randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
225
332
|
let r;
|
|
226
333
|
try {
|
|
@@ -250,7 +357,7 @@ export const HANDLERS = {
|
|
|
250
357
|
},
|
|
251
358
|
async 'files read'(ctx, values, [ref, path]) {
|
|
252
359
|
const ws = await resolveWorkspace(ctx, ref);
|
|
253
|
-
const bytes = await
|
|
360
|
+
const bytes = await cellOf(ctx, ws).files.read(path);
|
|
254
361
|
const dest = str(values.out);
|
|
255
362
|
if (dest !== undefined) {
|
|
256
363
|
await writeFile(dest, bytes);
|
|
@@ -267,14 +374,14 @@ export const HANDLERS = {
|
|
|
267
374
|
async 'files write'(ctx, values, [ref, path]) {
|
|
268
375
|
const data = await readInput(ctx, str(values.from) ?? '-', 1024 * 1024 * 1024, 'input');
|
|
269
376
|
const ws = await resolveWorkspace(ctx, ref);
|
|
270
|
-
const r = await
|
|
377
|
+
const r = await cellOf(ctx, ws).files.write(path, data, { append: values.append === true, createParents: values.parents !== false });
|
|
271
378
|
out(ctx, `wrote ${r.bytes_written} bytes to ${r.path}${r.sha256 ? ` (sha256 ${r.sha256})` : ''}\n`, r);
|
|
272
379
|
return 0;
|
|
273
380
|
},
|
|
274
381
|
async 'files ls'(ctx, values, [ref, path]) {
|
|
275
382
|
const limitText = str(values.limit);
|
|
276
383
|
const ws = await resolveWorkspace(ctx, ref);
|
|
277
|
-
const list = await
|
|
384
|
+
const list = await cellOf(ctx, ws).files.list(path, { limit: limitText ? parsePositiveInt(limitText, 'limit', 10_000) : 1000 });
|
|
278
385
|
out(ctx, `${fileTable(list)}${list.truncated ? '(truncated)\n' : ''}`, list);
|
|
279
386
|
return 0;
|
|
280
387
|
},
|
|
@@ -289,6 +396,262 @@ export const HANDLERS = {
|
|
|
289
396
|
throw new UsageError('refusing to delete without --yes (deletion revokes tool access immediately and cannot be undone)');
|
|
290
397
|
return lifecycle(ctx, values, ref, 'Deleting', (ws) => ctx.cloud().workspaces.delete(ws.id));
|
|
291
398
|
},
|
|
399
|
+
async 'workspaces close'(ctx, values, [ref]) {
|
|
400
|
+
return lifecycle(ctx, values, ref, 'Closing', (ws) => ctx.cloud().workspaces.close(ws.id));
|
|
401
|
+
},
|
|
402
|
+
async 'workspaces reset'(ctx, values, [ref]) {
|
|
403
|
+
if (values.yes !== true)
|
|
404
|
+
throw new UsageError('refusing to reset without --yes (reset wipes every change in the workspace and restarts it on its template)');
|
|
405
|
+
return lifecycle(ctx, values, ref, 'Resetting', (ws) => ctx.cloud().workspaces.reset(ws.id));
|
|
406
|
+
},
|
|
407
|
+
async 'workspaces save-as-template'(ctx, values, [ref]) {
|
|
408
|
+
const template = str(values.template);
|
|
409
|
+
if (!template)
|
|
410
|
+
throw new UsageError('--template <slug> is required for "shard workspaces save-as-template"');
|
|
411
|
+
const checkpoint = str(values.checkpoint);
|
|
412
|
+
if (checkpoint !== undefined && !UUID.test(checkpoint))
|
|
413
|
+
throw new UsageError('--checkpoint must be a checkpoint id (UUID)');
|
|
414
|
+
const defaults = saveDefaults(values);
|
|
415
|
+
const ack = acknowledged(values);
|
|
416
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
417
|
+
const res = await ctx.cloud().workspaces.saveAsTemplate(ws.id, {
|
|
418
|
+
templateSlug: template,
|
|
419
|
+
...(str(values['display-name']) !== undefined ? { displayName: str(values['display-name']) } : {}),
|
|
420
|
+
...(str(values.description) !== undefined ? { description: str(values.description) } : {}),
|
|
421
|
+
...(defaults ? { defaults } : {}),
|
|
422
|
+
...(checkpoint ? { checkpointId: checkpoint } : {}),
|
|
423
|
+
...(values['auto-publish'] === false ? { autoPublish: false } : {}),
|
|
424
|
+
...(ack ? { acknowledgedScanFindings: ack } : {}),
|
|
425
|
+
});
|
|
426
|
+
return reportSave(ctx, values, res, `Saving ${ws.key} (${ws.id}) as ${template} v${res.build.target_version ?? '?'}.`);
|
|
427
|
+
},
|
|
428
|
+
async 'workspaces changes'(ctx, values, [ref]) {
|
|
429
|
+
const path = str(values.path);
|
|
430
|
+
if (path !== undefined && !path.startsWith('/'))
|
|
431
|
+
throw new UsageError('--path must be an absolute path');
|
|
432
|
+
const limit = listLimit(values, 1000);
|
|
433
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
434
|
+
const cell = cellOf(ctx, ws);
|
|
435
|
+
const base = { ...(path ? { pathPrefix: path } : {}), ...(values.hash === true ? { hash: true } : {}), ...(limit ? { limit } : {}) };
|
|
436
|
+
let rows = [];
|
|
437
|
+
let next = null;
|
|
438
|
+
let summary;
|
|
439
|
+
if (values.all === true) {
|
|
440
|
+
let cursor;
|
|
441
|
+
do {
|
|
442
|
+
const page = await cell.changes({ ...base, ...(cursor ? { cursor } : {}), ...(cursor === undefined && values.summary === true ? { summary: true } : {}) });
|
|
443
|
+
if (page.summary)
|
|
444
|
+
summary = page.summary;
|
|
445
|
+
rows.push(...page.data);
|
|
446
|
+
cursor = page.next_cursor ?? undefined;
|
|
447
|
+
if (ctx.signal.aborted)
|
|
448
|
+
throw new InterruptedError('Interrupted.');
|
|
449
|
+
} while (cursor);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const cursor = str(values.cursor);
|
|
453
|
+
const page = await cell.changes({ ...base, ...(cursor ? { cursor } : {}), ...(values.summary === true ? { summary: true } : {}) });
|
|
454
|
+
rows = page.data;
|
|
455
|
+
next = page.next_cursor;
|
|
456
|
+
summary = page.summary;
|
|
457
|
+
}
|
|
458
|
+
const s = summary;
|
|
459
|
+
const head = s ? `added ${s.added}, modified ${s.modified}, deleted ${s.deleted}, replaced ${s.replaced} (${s.bytes} bytes)\n` : '';
|
|
460
|
+
out(ctx, `${head}${rows.length ? changesTable(rows) : 'No changes against the template.\n'}`, { data: rows, next_cursor: next, ...(summary ? { summary } : {}) });
|
|
461
|
+
if (!ctx.json && next)
|
|
462
|
+
ctx.io.stderr.write(`more: shard workspaces changes ${ref} --cursor ${next}\n`);
|
|
463
|
+
return 0;
|
|
464
|
+
},
|
|
465
|
+
async 'templates list'(ctx, values) {
|
|
466
|
+
const owner = oneOf(values, 'owner', OWNERS);
|
|
467
|
+
const limit = listLimit(values, 200);
|
|
468
|
+
const params = { ...(owner ? { owner } : {}), ...(values['include-archived'] === true ? { includeArchived: true } : {}) };
|
|
469
|
+
const templates = ctx.cloud().templates;
|
|
470
|
+
let rows = [];
|
|
471
|
+
let next = null;
|
|
472
|
+
if (values.all === true) {
|
|
473
|
+
for await (const t of templates.listAll(params))
|
|
474
|
+
rows.push(t);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const cursor = str(values.cursor);
|
|
478
|
+
const page = await templates.list({ ...params, ...(limit ? { limit } : {}), ...(cursor ? { cursor } : {}) });
|
|
479
|
+
rows = page.data;
|
|
480
|
+
next = page.nextCursor;
|
|
481
|
+
}
|
|
482
|
+
out(ctx, rows.length ? templateTable(rows) : 'No templates.\n', { data: rows, next_cursor: next });
|
|
483
|
+
if (!ctx.json && next)
|
|
484
|
+
ctx.io.stderr.write(`more: shard templates list --cursor ${next}\n`);
|
|
485
|
+
return 0;
|
|
486
|
+
},
|
|
487
|
+
async 'templates files'(ctx, values, [slug, versionText, path]) {
|
|
488
|
+
const version = parseVersion(versionText, '<version>');
|
|
489
|
+
const owner = oneOf(values, 'owner', OWNERS);
|
|
490
|
+
const templates = ctx.cloud().templates;
|
|
491
|
+
const dir = path ?? '/';
|
|
492
|
+
if (values.stat === true) {
|
|
493
|
+
const entry = await templates.fileEntry(slug, version, dir, owner ? { owner } : {});
|
|
494
|
+
out(ctx, templateEntryText(entry), entry);
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
497
|
+
const limit = listLimit(values, 1000);
|
|
498
|
+
let rows = [];
|
|
499
|
+
let next = null;
|
|
500
|
+
if (values.all === true) {
|
|
501
|
+
for await (const e of templates.filesAll(slug, version, { path: dir, ...(owner ? { owner } : {}), ...(limit ? { limit } : {}) })) {
|
|
502
|
+
rows.push(e);
|
|
503
|
+
if (ctx.signal.aborted)
|
|
504
|
+
throw new InterruptedError('Interrupted.');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
const cursor = str(values.cursor);
|
|
509
|
+
const page = await templates.files(slug, version, { path: dir, ...(owner ? { owner } : {}), ...(limit ? { limit } : {}), ...(cursor ? { cursor } : {}) });
|
|
510
|
+
rows = page.data;
|
|
511
|
+
next = page.next_cursor;
|
|
512
|
+
}
|
|
513
|
+
out(ctx, rows.length ? templateFileTable(rows) : `${dir} is empty.\n`, { path: dir, data: rows, next_cursor: next });
|
|
514
|
+
if (!ctx.json && next)
|
|
515
|
+
ctx.io.stderr.write(`more: shard templates files ${slug} ${version} ${dir} --cursor ${next}\n`);
|
|
516
|
+
return 0;
|
|
517
|
+
},
|
|
518
|
+
async 'templates diff'(ctx, values, [slug]) {
|
|
519
|
+
const fromText = str(values.from);
|
|
520
|
+
const toText = str(values.to);
|
|
521
|
+
if (fromText === undefined || toText === undefined)
|
|
522
|
+
throw new UsageError('--from <version|base> and --to <version> are required for "shard templates diff"');
|
|
523
|
+
const from = fromText === 'base' ? 'base' : parseVersion(fromText, '--from');
|
|
524
|
+
const to = parseVersion(toText, '--to');
|
|
525
|
+
const change = oneOf(values, 'change', DIFF_CHANGES);
|
|
526
|
+
const owner = oneOf(values, 'owner', OWNERS);
|
|
527
|
+
const prefix = str(values.prefix);
|
|
528
|
+
if (prefix !== undefined && !prefix.startsWith('/'))
|
|
529
|
+
throw new UsageError('--prefix must be an absolute path');
|
|
530
|
+
const limit = listLimit(values, 1000);
|
|
531
|
+
const base = { from, to, ...(prefix ? { pathPrefix: prefix } : {}), ...(change ? { change } : {}), ...(owner ? { owner } : {}), ...(limit ? { limit } : {}) };
|
|
532
|
+
const templates = ctx.cloud().templates;
|
|
533
|
+
let rows = [];
|
|
534
|
+
let next = null;
|
|
535
|
+
let summary = null;
|
|
536
|
+
if (values.all === true) {
|
|
537
|
+
let cursor;
|
|
538
|
+
do {
|
|
539
|
+
const page = await templates.diff(slug, { ...base, ...(cursor ? { cursor } : {}) });
|
|
540
|
+
if (page.summary)
|
|
541
|
+
summary = page.summary;
|
|
542
|
+
rows.push(...page.data);
|
|
543
|
+
cursor = page.next_cursor ?? undefined;
|
|
544
|
+
if (ctx.signal.aborted)
|
|
545
|
+
throw new InterruptedError('Interrupted.');
|
|
546
|
+
} while (cursor);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
const cursor = str(values.cursor);
|
|
550
|
+
const page = await templates.diff(slug, { ...base, ...(cursor ? { cursor } : {}) });
|
|
551
|
+
rows = page.data;
|
|
552
|
+
next = page.next_cursor;
|
|
553
|
+
summary = page.summary;
|
|
554
|
+
}
|
|
555
|
+
out(ctx, `${diffSummaryText(summary)}${rows.length ? diffTable(rows) : 'No differences.\n'}`, { data: rows, next_cursor: next, summary });
|
|
556
|
+
if (!ctx.json && next)
|
|
557
|
+
ctx.io.stderr.write(`more: shard templates diff ${slug} --from ${fromText} --to ${toText} --cursor ${next}\n`);
|
|
558
|
+
return 0;
|
|
559
|
+
},
|
|
560
|
+
async 'templates draft open'(ctx, values, [slug]) {
|
|
561
|
+
const c = caps(values);
|
|
562
|
+
const timeoutMs = waitTimeout(values);
|
|
563
|
+
const base = str(values.base);
|
|
564
|
+
const cloud = ctx.cloud();
|
|
565
|
+
const opened = await cloud.templates.draft(slug).create({ ...(base ? { base } : {}), ...(c ? { caps: c } : {}), agentLabel: ctx.agentLabel, wait: false });
|
|
566
|
+
let ws = opened.workspace;
|
|
567
|
+
const op = opened.operation;
|
|
568
|
+
if (values.wait !== false && !ws.ready && op) {
|
|
569
|
+
await waitOperation(ctx, op.id, timeoutMs);
|
|
570
|
+
ws = await cloud.workspaces.get(ws.id);
|
|
571
|
+
}
|
|
572
|
+
const human = ws.ready
|
|
573
|
+
? `Draft of ${slug} is ready: workspace ${ws.key} (${ws.id}) on ${opened.draft.base_version.slug} v${opened.draft.base_version.version}.\nedit it with: shard ws exec ${ws.key} -- <command>\n`
|
|
574
|
+
: `Draft of ${slug}: workspace ${ws.key} (${ws.id}) is ${ws.state}${op ? `; operation ${op.id} ${op.kind} ${op.state}` : ''}.\n${op ? `wait with: shard operations wait ${op.id}\n` : ''}`;
|
|
575
|
+
out(ctx, human, { draft: { ...opened.draft, workspace: ws.data }, operation: op });
|
|
576
|
+
return 0;
|
|
577
|
+
},
|
|
578
|
+
async 'templates draft status'(ctx, _values, [slug]) {
|
|
579
|
+
const draft = await ctx.cloud().templates.draft(slug).get();
|
|
580
|
+
out(ctx, draftText(draft), draft);
|
|
581
|
+
return 0;
|
|
582
|
+
},
|
|
583
|
+
async 'templates draft state'(ctx, values, [slug]) {
|
|
584
|
+
const draft = ctx.cloud().templates.draft(slug);
|
|
585
|
+
if (values.list === true) {
|
|
586
|
+
const states = [];
|
|
587
|
+
for await (const st of draft.statesAll({ limit: 200 }))
|
|
588
|
+
states.push(st);
|
|
589
|
+
out(ctx, states.length ? draftStateTable(states) : 'No states captured yet.\n', { data: states });
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
const label = str(values.label);
|
|
593
|
+
let op = await draft.captureState(label === undefined ? {} : { label });
|
|
594
|
+
if (values.wait === true)
|
|
595
|
+
op = await waitOperation(ctx, op.id, waitTimeout(values));
|
|
596
|
+
const checkpoint = typeof op.result?.checkpoint_id === 'string' ? `state ${op.result.checkpoint_id}\n` : '';
|
|
597
|
+
out(ctx, `Capturing a state of the ${slug} draft: operation ${op.id} ${op.kind} ${op.state}\n${checkpoint}${op.state === 'succeeded' ? '' : `wait with: shard operations wait ${op.id}\n`}`, op);
|
|
598
|
+
return 0;
|
|
599
|
+
},
|
|
600
|
+
async 'templates draft test'(ctx, values, [slug]) {
|
|
601
|
+
const draft = ctx.cloud().templates.draft(slug);
|
|
602
|
+
if (values.list === true) {
|
|
603
|
+
const views = [];
|
|
604
|
+
let cursor;
|
|
605
|
+
do {
|
|
606
|
+
const page = await draft.testInstances({ limit: 200, ...(values['include-ended'] === true ? { includeEnded: true } : {}), ...(cursor ? { cursor } : {}) });
|
|
607
|
+
views.push(...page.data.map((w) => w.data));
|
|
608
|
+
cursor = page.nextCursor ?? undefined;
|
|
609
|
+
} while (cursor);
|
|
610
|
+
out(ctx, views.length ? workspaceTable(views) : 'No test instances.\n', { data: views });
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
const state = str(values.state);
|
|
614
|
+
if (state !== undefined && !UUID.test(state))
|
|
615
|
+
throw new UsageError('--state must be a draft state id (UUID)');
|
|
616
|
+
const c = caps(values);
|
|
617
|
+
const timeoutMs = waitTimeout(values);
|
|
618
|
+
const instanceKey = str(values['instance-key']);
|
|
619
|
+
let ws = await draft.openTestInstance({ ...(state ? { stateId: state } : {}), ...(instanceKey ? { key: instanceKey } : {}), ...(c ? { caps: c } : {}), agentLabel: ctx.agentLabel, wait: false });
|
|
620
|
+
const op = ws.activeOperation;
|
|
621
|
+
if (values.wait !== false && !ws.ready && op) {
|
|
622
|
+
await waitOperation(ctx, op.id, timeoutMs);
|
|
623
|
+
ws = await ctx.cloud().workspaces.get(ws.id);
|
|
624
|
+
}
|
|
625
|
+
const human = ws.ready
|
|
626
|
+
? `Test instance ${ws.key} (${ws.id}) is ready. It ends with: shard ws close ${ws.key}\n`
|
|
627
|
+
: `Test instance ${ws.key} (${ws.id}) is ${ws.state}${op ? `; operation ${op.id} ${op.kind} ${op.state}` : ''}.\n${op ? `wait with: shard operations wait ${op.id}\n` : ''}`;
|
|
628
|
+
out(ctx, human, ws.data);
|
|
629
|
+
return 0;
|
|
630
|
+
},
|
|
631
|
+
async 'templates draft publish'(ctx, values, [slug]) {
|
|
632
|
+
const state = str(values.state);
|
|
633
|
+
if (state !== undefined && !UUID.test(state))
|
|
634
|
+
throw new UsageError('--state must be a draft state id (UUID)');
|
|
635
|
+
const defaults = saveDefaults(values);
|
|
636
|
+
const ack = acknowledged(values);
|
|
637
|
+
const res = await ctx.cloud().templates.draft(slug).publish({
|
|
638
|
+
...(state ? { stateId: state } : {}),
|
|
639
|
+
...(str(values.description) !== undefined ? { description: str(values.description) } : {}),
|
|
640
|
+
...(defaults ? { defaults } : {}),
|
|
641
|
+
...(values['auto-publish'] === false ? { autoPublish: false } : {}),
|
|
642
|
+
...(ack ? { acknowledgedScanFindings: ack } : {}),
|
|
643
|
+
});
|
|
644
|
+
return reportSave(ctx, values, res, `Publishing the ${slug} draft as v${res.build.target_version ?? '?'}.`);
|
|
645
|
+
},
|
|
646
|
+
async 'templates draft discard'(ctx, values, [slug]) {
|
|
647
|
+
if (values.yes !== true)
|
|
648
|
+
throw new UsageError('refusing to discard without --yes (the draft is deleted and its live test instances end)');
|
|
649
|
+
let op = await ctx.cloud().templates.draft(slug).discard();
|
|
650
|
+
if (values.wait === true)
|
|
651
|
+
op = await waitOperation(ctx, op.id, waitTimeout(values));
|
|
652
|
+
out(ctx, `Discarding the ${slug} draft: operation ${op.id} ${op.kind} ${op.state}\n`, op);
|
|
653
|
+
return 0;
|
|
654
|
+
},
|
|
292
655
|
async 'workspaces fork'(ctx, values, [ref, newKey]) {
|
|
293
656
|
const c = caps(values);
|
|
294
657
|
const timeoutMs = waitTimeout(values);
|
|
@@ -312,6 +675,114 @@ export const HANDLERS = {
|
|
|
312
675
|
out(ctx, sessions.length ? sessionTable(sessions) : 'No agent sessions (no tool token was issued for this workspace yet).\n', { data: sessions });
|
|
313
676
|
return 0;
|
|
314
677
|
},
|
|
678
|
+
async 'workspaces secrets'(ctx, values, [ref]) {
|
|
679
|
+
const set = secretNames(strs(values.set), '--set');
|
|
680
|
+
const clear = values.clear === true;
|
|
681
|
+
if (clear && set.length > 0)
|
|
682
|
+
throw new UsageError('use either --set NAME... or --clear, not both');
|
|
683
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
684
|
+
const b = clear || set.length > 0 ? await ws.secrets.set(clear ? [] : set) : await ws.secrets.get();
|
|
685
|
+
out(ctx, bindingsText(b), b);
|
|
686
|
+
return 0;
|
|
687
|
+
},
|
|
688
|
+
async 'secrets create'(ctx, values, [name]) {
|
|
689
|
+
if (!SECRET_NAME.test(name))
|
|
690
|
+
throw new UsageError(`<name> must be an environment variable name like OPENAI_API_KEY (^[A-Z_][A-Z0-9_]*$), got "${name}"`);
|
|
691
|
+
const scope = scopeFlag(values);
|
|
692
|
+
const perms = permissionParams(values, scope === null ? 'project' : 'organization', 'create');
|
|
693
|
+
const description = str(values.description);
|
|
694
|
+
const value = await readSecretValue(ctx, values);
|
|
695
|
+
const cloud = ctx.cloud();
|
|
696
|
+
const base = { name: name, value, ...(description !== undefined ? { description } : {}) };
|
|
697
|
+
const s = scope === null
|
|
698
|
+
? await cloud.secrets.create((await principal(ctx)).project_id, { ...base, ...perms })
|
|
699
|
+
: await cloud.secrets.createOrganization(scope, { ...base, ...perms });
|
|
700
|
+
out(ctx, `Created ${s.scope} secret ${s.name} (${s.id}), version ${s.current_version}. The value is stored encrypted and is never shown.\n${secretDetail(s)}`, s);
|
|
701
|
+
return 0;
|
|
702
|
+
},
|
|
703
|
+
async 'secrets list'(ctx, values) {
|
|
704
|
+
const cloud = ctx.cloud();
|
|
705
|
+
const org = scopeFlag(values);
|
|
706
|
+
const project = org === null ? (await principal(ctx)).project_id : null;
|
|
707
|
+
const limitText = str(values.limit);
|
|
708
|
+
const opts = { ...(values['include-deleted'] === true ? { includeDeleted: true } : {}), ...(limitText ? { limit: parsePositiveInt(limitText, 'limit', 200) } : {}) };
|
|
709
|
+
const page = (cursor) => (project !== null ? cloud.secrets.list(project, { ...opts, ...(cursor ? { cursor } : {}) }) : cloud.secrets.listOrganization(org, { ...opts, ...(cursor ? { cursor } : {}) }));
|
|
710
|
+
let rows = [];
|
|
711
|
+
let next = null;
|
|
712
|
+
if (values.all === true) {
|
|
713
|
+
let cursor;
|
|
714
|
+
do {
|
|
715
|
+
const p = await page(cursor);
|
|
716
|
+
rows.push(...p.data);
|
|
717
|
+
cursor = p.nextCursor ?? undefined;
|
|
718
|
+
} while (cursor);
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
const p = await page(str(values.cursor));
|
|
722
|
+
rows = p.data;
|
|
723
|
+
next = p.nextCursor;
|
|
724
|
+
}
|
|
725
|
+
out(ctx, rows.length ? secretTable(rows) : 'No secrets.\n', { data: rows, next_cursor: next });
|
|
726
|
+
if (!ctx.json && next)
|
|
727
|
+
ctx.io.stderr.write(`more: shard secrets list${org ? ` --org ${org}` : ''} --cursor ${next}\n`);
|
|
728
|
+
return 0;
|
|
729
|
+
},
|
|
730
|
+
async 'secrets get'(ctx, values, [ref]) {
|
|
731
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
732
|
+
out(ctx, secretDetail(s), s);
|
|
733
|
+
return 0;
|
|
734
|
+
},
|
|
735
|
+
async 'secrets update'(ctx, values, [ref]) {
|
|
736
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
737
|
+
const params = { ...permissionParams(values, s.scope, 'update') };
|
|
738
|
+
const description = str(values.description);
|
|
739
|
+
if (description !== undefined)
|
|
740
|
+
params.description = description;
|
|
741
|
+
if (Object.keys(params).length === 0)
|
|
742
|
+
throw new UsageError('nothing to update: pass --description, --tool, --allow-workspace, --any-workspace, --allow-project, --all-projects or --clear-projects');
|
|
743
|
+
const updated = await ctx.cloud().secrets.update(s.id, params);
|
|
744
|
+
out(ctx, `Updated ${updated.name}; applies from the next session start.\n${secretDetail(updated)}`, updated);
|
|
745
|
+
return 0;
|
|
746
|
+
},
|
|
747
|
+
async 'secrets rotate'(ctx, values, [ref]) {
|
|
748
|
+
const value = await readSecretValue(ctx, values);
|
|
749
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
750
|
+
const r = await ctx.cloud().secrets.rotate(s.id, value);
|
|
751
|
+
out(ctx, `Rotated ${r.secret.name} to version ${r.version.version}; older values were erased. Processes that already read the old value keep it.\n`, r);
|
|
752
|
+
return 0;
|
|
753
|
+
},
|
|
754
|
+
async 'secrets versions'(ctx, values, [ref]) {
|
|
755
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
756
|
+
const limitText = str(values.limit);
|
|
757
|
+
const page = await ctx.cloud().secrets.versions(s.id, limitText ? { limit: parsePositiveInt(limitText, 'limit', 200) } : {});
|
|
758
|
+
out(ctx, secretVersionTable(page.data), { data: page.data, next_cursor: page.nextCursor });
|
|
759
|
+
return 0;
|
|
760
|
+
},
|
|
761
|
+
async 'secrets delete'(ctx, values, [ref]) {
|
|
762
|
+
if (values.yes !== true)
|
|
763
|
+
throw new UsageError('refusing to delete without --yes (the values are erased and the name is removed from every workspace binding)');
|
|
764
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
765
|
+
await ctx.cloud().secrets.delete(s.id);
|
|
766
|
+
out(ctx, `Deleted ${s.scope} secret ${s.name} (${s.id}): its values were erased and it was removed from every workspace binding.\n`, { deleted: true, id: s.id, name: s.name, scope: s.scope });
|
|
767
|
+
return 0;
|
|
768
|
+
},
|
|
769
|
+
async 'secrets access-log'(ctx, values, [ref]) {
|
|
770
|
+
const outcome = str(values.outcome);
|
|
771
|
+
if (outcome !== undefined && !['granted', 'denied', 'failed'].includes(outcome))
|
|
772
|
+
throw new UsageError('--outcome must be granted, denied or failed');
|
|
773
|
+
const workspace = str(values.workspace);
|
|
774
|
+
if (workspace !== undefined && !UUID.test(workspace))
|
|
775
|
+
throw new UsageError('--workspace must be a workspace id (UUID)');
|
|
776
|
+
const limitText = str(values.limit);
|
|
777
|
+
const s = await resolveSecret(ctx, values, ref);
|
|
778
|
+
const page = await ctx.cloud().secrets.accessEvents(s.id, {
|
|
779
|
+
...(outcome ? { outcome: outcome } : {}),
|
|
780
|
+
...(workspace ? { workspaceId: workspace } : {}),
|
|
781
|
+
...(limitText ? { limit: parsePositiveInt(limitText, 'limit', 200) } : {}),
|
|
782
|
+
});
|
|
783
|
+
out(ctx, page.data.length ? secretAccessTable(page.data) : 'No session has resolved this secret yet.\n', { data: page.data, next_cursor: page.nextCursor });
|
|
784
|
+
return 0;
|
|
785
|
+
},
|
|
315
786
|
async 'operations list'(ctx, values, [ref]) {
|
|
316
787
|
const ws = await resolveWorkspace(ctx, ref);
|
|
317
788
|
const limitText = str(values.limit);
|
|
@@ -338,6 +809,109 @@ export const HANDLERS = {
|
|
|
338
809
|
return 0;
|
|
339
810
|
},
|
|
340
811
|
};
|
|
812
|
+
/** `--org <id>`: organization scope; null: the API key's project. */
|
|
813
|
+
function scopeFlag(values) {
|
|
814
|
+
const org = str(values.org);
|
|
815
|
+
if (org === undefined)
|
|
816
|
+
return null;
|
|
817
|
+
if (!UUID.test(org))
|
|
818
|
+
throw new UsageError('--org must be an organization id (UUID)');
|
|
819
|
+
return org;
|
|
820
|
+
}
|
|
821
|
+
/** Usage permissions from the flags (create: defaults left to the API; update: only what was passed). */
|
|
822
|
+
function permissionParams(values, scope, mode) {
|
|
823
|
+
const out = {};
|
|
824
|
+
const tools = strs(values.tool);
|
|
825
|
+
for (const t of tools)
|
|
826
|
+
if (!TOOLS.includes(t))
|
|
827
|
+
throw new UsageError(`--tool must be one of ${TOOLS.join(', ')} (got "${t}")`);
|
|
828
|
+
if (tools.length)
|
|
829
|
+
out.allowedTools = [...new Set(tools)];
|
|
830
|
+
const workspaces = strs(values['allow-workspace']);
|
|
831
|
+
for (const w of workspaces)
|
|
832
|
+
if (!UUID.test(w))
|
|
833
|
+
throw new UsageError(`--allow-workspace expects a workspace id (UUID), got "${w}"`);
|
|
834
|
+
if (workspaces.length && values['any-workspace'] === true)
|
|
835
|
+
throw new UsageError('use either --allow-workspace or --any-workspace');
|
|
836
|
+
if (workspaces.length)
|
|
837
|
+
out.allowedWorkspaceIds = workspaces;
|
|
838
|
+
if (mode === 'update' && values['any-workspace'] === true)
|
|
839
|
+
out.allowedWorkspaceIds = null;
|
|
840
|
+
const projects = strs(values['allow-project']);
|
|
841
|
+
const all = values['all-projects'] === true;
|
|
842
|
+
const none = mode === 'update' && values['clear-projects'] === true;
|
|
843
|
+
if ((projects.length ? 1 : 0) + (all ? 1 : 0) + (none ? 1 : 0) > 1)
|
|
844
|
+
throw new UsageError('use only one of --allow-project, --all-projects, --clear-projects');
|
|
845
|
+
if ((projects.length || all || none) && scope === 'project')
|
|
846
|
+
throw new UsageError('--allow-project/--all-projects/--clear-projects apply to organization secrets only (--org)');
|
|
847
|
+
for (const p of projects)
|
|
848
|
+
if (!UUID.test(p))
|
|
849
|
+
throw new UsageError(`--allow-project expects a project id (UUID), got "${p}"`);
|
|
850
|
+
if (projects.length)
|
|
851
|
+
out.allowedProjectIds = projects;
|
|
852
|
+
if (all)
|
|
853
|
+
out.allowedProjectIds = null;
|
|
854
|
+
if (none)
|
|
855
|
+
out.allowedProjectIds = [];
|
|
856
|
+
return out;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* The secret value from --from-file or standard input (never from arguments): UTF-8 text, one trailing
|
|
860
|
+
* newline removed, 1..65536 bytes. Never echoed.
|
|
861
|
+
*/
|
|
862
|
+
async function readSecretValue(ctx, values) {
|
|
863
|
+
const file = str(values['from-file']);
|
|
864
|
+
const bytes = await readInput(ctx, file ?? '-', MAX_SECRET_VALUE_BYTES + 2, file === undefined ? 'secret value (standard input)' : '--from-file');
|
|
865
|
+
let text;
|
|
866
|
+
try {
|
|
867
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
throw new UsageError('the secret value must be UTF-8 text');
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
bytes.fill(0);
|
|
874
|
+
}
|
|
875
|
+
if (text.endsWith('\r\n'))
|
|
876
|
+
text = text.slice(0, -2);
|
|
877
|
+
else if (text.endsWith('\n'))
|
|
878
|
+
text = text.slice(0, -1);
|
|
879
|
+
if (text.length === 0)
|
|
880
|
+
throw new UsageError('the secret value is empty: pipe it on standard input or pass --from-file <file>');
|
|
881
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_SECRET_VALUE_BYTES)
|
|
882
|
+
throw new UsageError(`the secret value exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
|
|
883
|
+
if (text.includes('\u0000'))
|
|
884
|
+
throw new UsageError('the secret value must not contain NUL characters');
|
|
885
|
+
return text;
|
|
886
|
+
}
|
|
887
|
+
/** `<id|name>`: a UUID is tried as an id first; otherwise the live secret of that name in the scope (project, or --org). */
|
|
888
|
+
async function resolveSecret(ctx, values, ref) {
|
|
889
|
+
const cloud = ctx.cloud();
|
|
890
|
+
if (UUID.test(ref)) {
|
|
891
|
+
try {
|
|
892
|
+
return await cloud.secrets.get(ref);
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
if (!(err instanceof ShardfluxApiError && err.status === 404))
|
|
896
|
+
throw err;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const org = scopeFlag(values);
|
|
900
|
+
if (!SECRET_NAME.test(ref))
|
|
901
|
+
throw new NotFoundError(`No secret with id or name "${ref}".`);
|
|
902
|
+
const project = org === null ? (await principal(ctx)).project_id : null;
|
|
903
|
+
let cursor;
|
|
904
|
+
do {
|
|
905
|
+
const page = project !== null ? await cloud.secrets.list(project, { limit: 200, ...(cursor ? { cursor } : {}) }) : await cloud.secrets.listOrganization(org, { limit: 200, ...(cursor ? { cursor } : {}) });
|
|
906
|
+
const hit = page.data.find((s) => s.name === ref && s.deleted_at === null);
|
|
907
|
+
if (hit)
|
|
908
|
+
return hit;
|
|
909
|
+
cursor = page.nextCursor ?? undefined;
|
|
910
|
+
if (ctx.signal.aborted)
|
|
911
|
+
throw new InterruptedError('Interrupted.');
|
|
912
|
+
} while (cursor);
|
|
913
|
+
throw new NotFoundError(project !== null ? `No secret named "${ref}" in this project (organization-wide secrets: add --org <organization-id>).` : `No organization secret named "${ref}".`);
|
|
914
|
+
}
|
|
341
915
|
async function lifecycle(ctx, values, ref, verb, start) {
|
|
342
916
|
const timeoutMs = waitTimeout(values);
|
|
343
917
|
const ws = await resolveWorkspace(ctx, ref);
|