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