@shardflux/cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -0
- package/README.md +442 -34
- package/dist/account-args.d.ts +13 -0
- package/dist/account-args.js +413 -0
- package/dist/account-commands.d.ts +16 -0
- package/dist/account-commands.js +1616 -0
- package/dist/args.d.ts +15 -3
- package/dist/args.js +280 -40
- package/dist/bin.js +5 -16
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +39 -0
- package/dist/commands.d.ts +67 -5
- package/dist/commands.js +390 -29
- package/dist/credentials.d.ts +77 -0
- package/dist/credentials.js +217 -0
- package/dist/errors.d.ts +63 -11
- package/dist/errors.js +107 -21
- package/dist/format.d.ts +70 -5
- package/dist/format.js +365 -6
- package/dist/handlers.d.ts +2 -0
- package/dist/handlers.js +7 -0
- package/dist/index.d.ts +19 -7
- package/dist/index.js +13 -6
- package/dist/input.d.ts +25 -0
- package/dist/input.js +167 -0
- package/dist/main.d.ts +16 -2
- package/dist/main.js +399 -31
- package/dist/update-check.d.ts +64 -0
- package/dist/update-check.js +128 -0
- package/package.json +3 -3
package/dist/commands.js
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Command implementations
|
|
3
|
-
* client, retries, idempotency keys, operation polling and
|
|
4
|
-
* management as SDK users get); `cloud.request()` covers the two endpoints
|
|
5
|
-
*
|
|
2
|
+
* Command implementations of the project-key commands (workspaces, files, operations, secrets, templates, usage).
|
|
3
|
+
* Everything goes through @shardflux/sdk (the same client, retries, idempotency keys, operation polling and
|
|
4
|
+
* tool-token management as SDK users get); `cloud.request()` covers the two endpoints the SDK has no typed method
|
|
5
|
+
* for yet (GET /v1/operations/{id}, usage summary). The account plane (0.5.0) is in account-commands.ts; handlers.ts
|
|
6
|
+
* joins both tables.
|
|
6
7
|
*/
|
|
7
8
|
import { randomUUID } from 'node:crypto';
|
|
8
9
|
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
9
10
|
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
10
|
-
import { SDK_VERSION, ShardfluxApiError } from '@shardflux/sdk';
|
|
11
|
+
import { EXECUTION_ID, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, newExecutionId } from '@shardflux/sdk';
|
|
11
12
|
import { parse as parseYaml } from 'yaml';
|
|
12
13
|
import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
13
|
-
import { InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
14
|
+
import { ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
15
|
+
import { updateCheckDisabled } from "./update-check.js";
|
|
14
16
|
import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
|
|
15
17
|
import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
|
|
16
18
|
import { timingJson } from "./timing.js";
|
|
17
|
-
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, fileTable, languageTable, json, kv, operationDetail, operationTable, packageTable, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateDetailText, templateEntryText, templateFileTable, templateTable, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
19
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
18
20
|
const DEFAULT_WAIT_MS = 300_000;
|
|
19
21
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
20
22
|
const MAX_STDIN = 1024 * 1024;
|
|
21
|
-
|
|
23
|
+
/** `--output-limit` of a file-first execution: 1 byte to 16 MiB per stream (cell-api.yaml output_limit_bytes). */
|
|
24
|
+
const MAX_OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
25
|
+
export const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
22
26
|
/**
|
|
23
27
|
* The workspace's cell client as this invocation configures it: the CLI's agent label, wake on use unless turned off,
|
|
24
28
|
* and the wake timeout (an OperationTimeoutError past it exits 5).
|
|
25
29
|
*/
|
|
26
30
|
const cellOf = (ctx, ws) => ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs, ...(ctx.wake ? {} : { wake: null }) });
|
|
27
|
-
const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
|
|
31
|
+
export const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
|
|
28
32
|
const MAX_SECRET_VALUE_BYTES = 65_536;
|
|
33
|
+
/** `files patch --content-file`: a patch request is at most 7 MiB (contracts §26.2); larger files go through `files write`. */
|
|
34
|
+
const MAX_PATCH_CONTENT = 7 * 1024 * 1024;
|
|
35
|
+
const STOPPED_AT = { max_matches: '--max matches', budget: 'its 10 s budget', max_bytes: '4 MiB of results' };
|
|
36
|
+
/** A file revision (SHA-256 of the content) or `absent` (contracts §26.2). */
|
|
37
|
+
const REVISION = /^([0-9a-f]{64}|absent)$/;
|
|
29
38
|
const TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
|
|
30
39
|
function secretNames(names, option) {
|
|
31
40
|
for (const n of names)
|
|
@@ -41,7 +50,7 @@ function withTiming(ctx, machine) {
|
|
|
41
50
|
return machine;
|
|
42
51
|
return { ...machine, timing: timingJson(ctx.traces.last) };
|
|
43
52
|
}
|
|
44
|
-
function out(ctx, human, machine) {
|
|
53
|
+
export function out(ctx, human, machine) {
|
|
45
54
|
ctx.io.stdout.write(ctx.json ? json(withTiming(ctx, machine)) : human);
|
|
46
55
|
}
|
|
47
56
|
function caps(values) {
|
|
@@ -57,7 +66,7 @@ function caps(values) {
|
|
|
57
66
|
c.disk_gib = parsePositiveInt(disk, 'disk-gib', 1_000_000);
|
|
58
67
|
return Object.keys(c).length ? c : undefined;
|
|
59
68
|
}
|
|
60
|
-
function waitTimeout(values) {
|
|
69
|
+
export function waitTimeout(values) {
|
|
61
70
|
const t = str(values.timeout);
|
|
62
71
|
return t === undefined ? DEFAULT_WAIT_MS : parseDuration(t, 'timeout');
|
|
63
72
|
}
|
|
@@ -96,7 +105,7 @@ const PURPOSE_FILTERS = ['standard', 'template_draft', 'template_test', 'any'];
|
|
|
96
105
|
const DIFF_CHANGES = ['added', 'removed', 'changed', 'type_changed', 'metadata'];
|
|
97
106
|
const OWNERS = ['platform', 'organization'];
|
|
98
107
|
const DEFAULT_BUILD_WAIT_MS = 1_800_000;
|
|
99
|
-
function oneOf(values, option, allowed) {
|
|
108
|
+
export function oneOf(values, option, allowed) {
|
|
100
109
|
const v = str(values[option]);
|
|
101
110
|
if (v === undefined)
|
|
102
111
|
return undefined;
|
|
@@ -104,7 +113,7 @@ function oneOf(values, option, allowed) {
|
|
|
104
113
|
throw new UsageError(`--${option} must be one of ${allowed.join(', ')} (got "${v}")`);
|
|
105
114
|
return v;
|
|
106
115
|
}
|
|
107
|
-
function listLimit(values, max) {
|
|
116
|
+
export function listLimit(values, max) {
|
|
108
117
|
const t = str(values.limit);
|
|
109
118
|
return t === undefined ? undefined : parsePositiveInt(t, 'limit', max);
|
|
110
119
|
}
|
|
@@ -160,7 +169,7 @@ function parseVersion(text, what) {
|
|
|
160
169
|
}
|
|
161
170
|
const SLUG = /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
|
|
162
171
|
/** `<slug>@<version>`. */
|
|
163
|
-
function parseSlugVersion(text, what) {
|
|
172
|
+
export function parseSlugVersion(text, what) {
|
|
164
173
|
const at = text.lastIndexOf('@');
|
|
165
174
|
if (at <= 0)
|
|
166
175
|
throw new UsageError(`${what} must be <slug>@<version>, e.g. acme-dev@3 (got "${text}")`);
|
|
@@ -230,7 +239,7 @@ const BUILD_STATES = ['queued', 'building', 'testing', 'publishing', 'published'
|
|
|
230
239
|
function buildExitCode(b) {
|
|
231
240
|
return b.state === 'failed' || b.state === 'canceled' || b.registration.state === 'failed' ? 6 : 0;
|
|
232
241
|
}
|
|
233
|
-
async function fileExists(path) {
|
|
242
|
+
export async function fileExists(path) {
|
|
234
243
|
return access(path).then(() => true, () => false);
|
|
235
244
|
}
|
|
236
245
|
/** Writes `text` to `path`, refusing to overwrite an existing file without --force. */
|
|
@@ -283,7 +292,7 @@ async function readAll(stream, max, what) {
|
|
|
283
292
|
}
|
|
284
293
|
return new Uint8Array(Buffer.concat(chunks));
|
|
285
294
|
}
|
|
286
|
-
async function readInput(ctx, source, max, what) {
|
|
295
|
+
export async function readInput(ctx, source, max, what) {
|
|
287
296
|
if (source === '-') {
|
|
288
297
|
if (ctx.io.stdin.isTTY)
|
|
289
298
|
throw new UsageError(`${what}: standard input is a terminal; pipe data in or pass a file`);
|
|
@@ -296,7 +305,7 @@ async function readInput(ctx, source, max, what) {
|
|
|
296
305
|
throw new UsageError(`${what} exceeds ${max} bytes`);
|
|
297
306
|
return new Uint8Array(data);
|
|
298
307
|
}
|
|
299
|
-
async function principal(ctx) {
|
|
308
|
+
export async function principal(ctx) {
|
|
300
309
|
const me = await ctx.cloud().me();
|
|
301
310
|
if (me.type !== 'api_key' || !me.api_key)
|
|
302
311
|
throw new UsageError('SHARDFLUX_API_KEY did not authenticate as an API key');
|
|
@@ -317,6 +326,72 @@ function principalText(p) {
|
|
|
317
326
|
['tools', p.tool_permissions.length ? p.tool_permissions.join(', ') : '(none: read-only key)'],
|
|
318
327
|
]);
|
|
319
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Who sends the feedback: the project API key when one is configured (SHARDFLUX_API_KEY or saved by shard setup),
|
|
331
|
+
* else the signed-in session (SHARDFLUX_SESSION_TOKEN or saved by shard auth login), so an agent that is still setting
|
|
332
|
+
* up can report what got in its way. With neither, the key's own error (its hint names both ways, plus email).
|
|
333
|
+
*/
|
|
334
|
+
function feedbackSender(ctx) {
|
|
335
|
+
const keyConfigured = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
|
|
336
|
+
const sessionConfigured = Boolean(ctx.io.env.SHARDFLUX_SESSION_TOKEN?.trim()) || ctx.store.profile(ctx.apiUrl).session !== undefined;
|
|
337
|
+
return keyConfigured || !sessionConfigured ? ctx.cloud() : ctx.account();
|
|
338
|
+
}
|
|
339
|
+
/** Contract bounds of the feedback context fields shard sends (characters). */
|
|
340
|
+
const FEEDBACK_CONTEXT_MAX = { workspace: 200, 'request-id': 200, 'error-code': 100, command: 2000 };
|
|
341
|
+
/**
|
|
342
|
+
* The feedback message: the positional words joined with spaces, or --from-file (a file, or - for standard input),
|
|
343
|
+
* trimmed, 1-8000 characters (the API's bound, counted in UTF-16 units so nothing the API would refuse is sent).
|
|
344
|
+
*/
|
|
345
|
+
async function feedbackMessage(ctx, values, words) {
|
|
346
|
+
const file = str(values['from-file']);
|
|
347
|
+
if (file !== undefined && words.length > 0)
|
|
348
|
+
throw new UsageError('give the message as arguments or with --from-file, not both');
|
|
349
|
+
let text;
|
|
350
|
+
if (file !== undefined) {
|
|
351
|
+
const bytes = await readInput(ctx, file, MAX_STDIN, '--from-file');
|
|
352
|
+
try {
|
|
353
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
throw new UsageError('--from-file: the message must be UTF-8 text');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (words.length > 0) {
|
|
360
|
+
text = words.join(' ');
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
throw new UsageError('missing <message> for "shard feedback": shard feedback "what happened and what you expected" (or --from-file <file|->)');
|
|
364
|
+
}
|
|
365
|
+
text = text.trim();
|
|
366
|
+
if (text.length === 0)
|
|
367
|
+
throw new UsageError('the feedback message is empty');
|
|
368
|
+
if (text.length > FEEDBACK_MESSAGE_MAX_LENGTH) {
|
|
369
|
+
throw new UsageError(`the feedback message is ${text.length} characters; the limit is ${FEEDBACK_MESSAGE_MAX_LENGTH} (keep the relevant part, e.g. tail -n 100 of a log, or send it in two messages)`);
|
|
370
|
+
}
|
|
371
|
+
return text;
|
|
372
|
+
}
|
|
373
|
+
/** The context shard sends: its own client, the agent label when one is set, and the given references. */
|
|
374
|
+
function feedbackContext(ctx, values) {
|
|
375
|
+
const c = { client: `shard-cli/${CLI_VERSION}` };
|
|
376
|
+
if (ctx.agentLabel !== 'cli')
|
|
377
|
+
c.agent = ctx.agentLabel;
|
|
378
|
+
for (const [option, max] of Object.entries(FEEDBACK_CONTEXT_MAX)) {
|
|
379
|
+
const v = str(values[option])?.trim();
|
|
380
|
+
if (v === undefined)
|
|
381
|
+
continue;
|
|
382
|
+
if (v.length === 0 || v.length > max)
|
|
383
|
+
throw new UsageError(`--${option} must be 1-${max} characters`);
|
|
384
|
+
if (option === 'workspace')
|
|
385
|
+
c.workspace = v;
|
|
386
|
+
else if (option === 'request-id')
|
|
387
|
+
c.requestId = v;
|
|
388
|
+
else if (option === 'error-code')
|
|
389
|
+
c.errorCode = v;
|
|
390
|
+
else
|
|
391
|
+
c.command = v;
|
|
392
|
+
}
|
|
393
|
+
return c;
|
|
394
|
+
}
|
|
320
395
|
/** The exit code `shard exec` returns for a finished command. */
|
|
321
396
|
export function execExitCode(r) {
|
|
322
397
|
if (r.timedOut)
|
|
@@ -330,14 +405,95 @@ export function execExitCode(r) {
|
|
|
330
405
|
async function bestEffortCancel(cell, sessionId) {
|
|
331
406
|
await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
|
|
332
407
|
}
|
|
408
|
+
// ---- File-first workspaces (0.5.0; contracts §29) ----------------------------------------------------------------------
|
|
409
|
+
const MODES = { processful: 'processful', 'file-first': 'file_first', file_first: 'file_first' };
|
|
410
|
+
/** `--mode processful|file-first` (`file_first` too). */
|
|
411
|
+
function modeFlag(values) {
|
|
412
|
+
const v = str(values.mode);
|
|
413
|
+
if (v === undefined)
|
|
414
|
+
return undefined;
|
|
415
|
+
const m = MODES[v];
|
|
416
|
+
if (!m)
|
|
417
|
+
throw new UsageError(`--mode must be processful or file-first (got "${v}")`);
|
|
418
|
+
return m;
|
|
419
|
+
}
|
|
420
|
+
const EXECUTION_ID_TEXT = '8-128 characters of A-Z a-z 0-9 . _ : -, starting with a letter or digit';
|
|
421
|
+
/** `--if-revision <n>`: a tree revision, a non-negative integer (0 is the empty tree a file-first workspace starts with). */
|
|
422
|
+
function ifRevision(values) {
|
|
423
|
+
const t = str(values['if-revision']);
|
|
424
|
+
if (t === undefined)
|
|
425
|
+
return undefined;
|
|
426
|
+
if (!/^\d{1,15}$/.test(t.trim()))
|
|
427
|
+
throw new UsageError(`--if-revision must be a tree revision, a non-negative integer (got "${t}")`);
|
|
428
|
+
return Number(t.trim());
|
|
429
|
+
}
|
|
430
|
+
/** Options that exist only for file-first workspaces are a usage error on a processful one (known once resolved). */
|
|
431
|
+
function fileFirstOnly(ws, flags, note = '') {
|
|
432
|
+
if (flags.length === 0 || ws.mode === 'file_first')
|
|
433
|
+
return;
|
|
434
|
+
throw new UsageError(`${flags.join(' and ')} ${flags.length > 1 ? 'apply' : 'applies'} to file-first workspaces only; ${ws.key} is ${modeText(ws.mode)}${note}`);
|
|
435
|
+
}
|
|
436
|
+
/** The tree revision after a mutating files call on a file-first workspace (the newest its responses reported); else null. */
|
|
437
|
+
function treeRevisionAfter(ws, cell) {
|
|
438
|
+
if (ws.mode !== 'file_first')
|
|
439
|
+
return null;
|
|
440
|
+
return cell.treeRevision ?? ws.treeRevision;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* A lifecycle call a file-first workspace does not have (contracts §29.7: suspend, resume, fork, reset,
|
|
444
|
+
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
|
445
|
+
* with its hint like the API's own refusal.
|
|
446
|
+
*/
|
|
447
|
+
function requireProcessful(ws, operation, what) {
|
|
448
|
+
if (ws.mode !== 'file_first')
|
|
449
|
+
return;
|
|
450
|
+
throw NotSupportedForModeError.local('file_first', operation, 'api', `${what} is not available for a file-first workspace: it has no VM between executions and is never suspended; its state is its file tree (revision ${ws.treeRevision ?? 0}).`);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Prints an execution's result and returns the exit code. Human: the command's stdout and stderr bytes as they are
|
|
454
|
+
* (binary safe), a note when output was cut at the output limit, and with `summary` what ran (stderr; see
|
|
455
|
+
* executionSummary). --json: one document (executionJson). The exit code is the command's once it succeeded (like a
|
|
456
|
+
* processful exec: 124 timed out, 128+N killed by signal N); a failed or lost execution throws ExecutionFailedError
|
|
457
|
+
* (exit 6), one still queued or running ExecutionPendingError (exit 5, naming the command that waits for it).
|
|
458
|
+
*/
|
|
459
|
+
function reportExecution(ctx, ref, r, opts) {
|
|
460
|
+
if (ctx.json)
|
|
461
|
+
ctx.io.stdout.write(json(withTiming(ctx, executionJson(r))));
|
|
462
|
+
else if (!r.pending) {
|
|
463
|
+
if (r.stdout.length)
|
|
464
|
+
ctx.io.stdout.write(r.stdout);
|
|
465
|
+
if (r.stderr.length)
|
|
466
|
+
ctx.io.stderr.write(r.stderr);
|
|
467
|
+
const cut = [r.stdoutTruncated ? 'stdout' : '', r.stderrTruncated ? 'stderr' : ''].filter(Boolean).join(' and ');
|
|
468
|
+
if (opts.summary)
|
|
469
|
+
ctx.io.stderr.write(executionSummary(r, undefined, opts.fetched ? { replayed: false } : {}));
|
|
470
|
+
else {
|
|
471
|
+
if (cut)
|
|
472
|
+
ctx.io.stderr.write(`truncated: ${cut} reached the output limit (--output-limit, default 1 MiB, at most 16 MiB)\n`);
|
|
473
|
+
if (opts.timings && Object.keys(r.timings).length)
|
|
474
|
+
ctx.io.stderr.write(`execution ${r.executionId} timings: ${pairs(r.timings)}\n`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (r.pending) {
|
|
478
|
+
const when = opts.waited ? `still ${r.state} after ${opts.waited}` : `${r.state} (on tree revision ${r.baseRevision})`;
|
|
479
|
+
throw new ExecutionPendingError(`Execution ${r.executionId} is ${when}; it continues server side.`, r, `wait for its result with: shard executions get ${ref} ${r.executionId} --wait 10m`);
|
|
480
|
+
}
|
|
481
|
+
if (r.state === 'failed' || r.state === 'lost')
|
|
482
|
+
throw new ExecutionFailedError(r);
|
|
483
|
+
return execExitCode(r);
|
|
484
|
+
}
|
|
333
485
|
function lifecycleText(verb, w, op) {
|
|
334
486
|
const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
|
|
335
487
|
return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? '' : `wait with: shard operations wait ${op.id}\n`}`;
|
|
336
488
|
}
|
|
337
|
-
|
|
489
|
+
/** The project-key commands; handlers.ts adds the account plane's. */
|
|
490
|
+
export const KEY_HANDLERS = {
|
|
338
491
|
async login(ctx) {
|
|
339
492
|
const p = await principal(ctx);
|
|
340
|
-
|
|
493
|
+
const where = ctx.apiKey().source === 'env'
|
|
494
|
+
? 'The key comes from SHARDFLUX_API_KEY; shard stores nothing for it.'
|
|
495
|
+
: `The key is the one saved in ${ctx.store.file} (shard setup).`;
|
|
496
|
+
out(ctx, `Authenticated.\n${principalText(p)}${where} To sign in to your account instead: shard auth login.\n`, p);
|
|
341
497
|
return 0;
|
|
342
498
|
},
|
|
343
499
|
async whoami(ctx) {
|
|
@@ -345,14 +501,54 @@ export const HANDLERS = {
|
|
|
345
501
|
out(ctx, principalText(p), p);
|
|
346
502
|
return 0;
|
|
347
503
|
},
|
|
348
|
-
version(ctx) {
|
|
349
|
-
|
|
350
|
-
|
|
504
|
+
async version(ctx, values) {
|
|
505
|
+
const update = await ctx.versionStatus(values.check === true);
|
|
506
|
+
const line = update === null && updateCheckDisabled(ctx.io.env) ? 'update check: off (SHARDFLUX_NO_UPDATE_CHECK or NO_UPDATE_NOTIFIER; shard version --check asks anyway)\n' : updateLine(update, ctx.distribution);
|
|
507
|
+
out(ctx, `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n${line}`, { cli: CLI_VERSION, sdk: SDK_VERSION, update });
|
|
508
|
+
return 0;
|
|
509
|
+
},
|
|
510
|
+
// Feedback (0.5.0+): POST /v1/feedback, emailed to the founder. Validated here first so a bad call costs no request.
|
|
511
|
+
async feedback(ctx, values, words) {
|
|
512
|
+
const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
|
|
513
|
+
const context = feedbackContext(ctx, values);
|
|
514
|
+
const message = await feedbackMessage(ctx, values, words);
|
|
515
|
+
const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
|
|
516
|
+
const human = r.duplicate
|
|
517
|
+
? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
|
|
518
|
+
: `Sent to the Shardflux founder (feedback ${r.id}). Thank you.\n`;
|
|
519
|
+
out(ctx, human, { id: r.id, received_at: r.receivedAt, duplicate: r.duplicate });
|
|
520
|
+
return 0;
|
|
351
521
|
},
|
|
352
522
|
async usage(ctx, values) {
|
|
523
|
+
const views = [values.estimate === true, values.series === true, str(values.workspace) !== undefined].filter(Boolean).length;
|
|
524
|
+
if (views > 1)
|
|
525
|
+
throw new UsageError('use one of --estimate, --series and --workspace');
|
|
526
|
+
const seriesOnly = ['from', 'to', 'granularity', 'meter'].filter((k) => values[k] !== undefined);
|
|
527
|
+
if (seriesOnly.length > 0 && values.series !== true && str(values.workspace) === undefined) {
|
|
528
|
+
throw new UsageError(`--${seriesOnly[0]} needs --series or --workspace`);
|
|
529
|
+
}
|
|
530
|
+
const granularity = values.granularity === undefined ? undefined : oneOf(values, 'granularity', ['hour', 'day']);
|
|
531
|
+
const range = { from: str(values.from), to: str(values.to), granularity, meter: str(values.meter) };
|
|
532
|
+
const workspaceRef = str(values.workspace);
|
|
533
|
+
if (workspaceRef !== undefined) {
|
|
534
|
+
const ws = await resolveWorkspace(ctx, workspaceRef);
|
|
535
|
+
const series = await ctx.cloud().usage.workspace(ws.id, range);
|
|
536
|
+
out(ctx, usageSeriesText(series), series);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
353
539
|
const org = str(values.org) ?? (await principal(ctx)).organization_id;
|
|
354
540
|
if (!UUID.test(org))
|
|
355
541
|
throw new UsageError(`--org must be an organization id (UUID)`);
|
|
542
|
+
if (values.estimate === true) {
|
|
543
|
+
const estimate = await ctx.cloud().usage.estimate(org);
|
|
544
|
+
out(ctx, usageEstimateText(estimate), estimate);
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
if (values.series === true) {
|
|
548
|
+
const series = await ctx.cloud().usage.series(org, range);
|
|
549
|
+
out(ctx, usageSeriesText(series), series);
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
356
552
|
const summary = await ctx.cloud().request('GET', `/v1/organizations/${encodeURIComponent(org)}/usage/summary`);
|
|
357
553
|
out(ctx, usageText(summary), summary);
|
|
358
554
|
return 0;
|
|
@@ -364,6 +560,9 @@ export const HANDLERS = {
|
|
|
364
560
|
const c = caps(values);
|
|
365
561
|
const bind = secretNames(strs(values.secret), '--secret');
|
|
366
562
|
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
563
|
+
const mode = modeFlag(values);
|
|
564
|
+
if (mode === 'file_first' && lifetime === 'session')
|
|
565
|
+
throw new UsageError('a file-first workspace is persistent: --lifetime session is for processful workspaces');
|
|
367
566
|
const inputs = await inputsFrom(ctx, values);
|
|
368
567
|
const timeoutMs = waitTimeout(values);
|
|
369
568
|
const cloud = ctx.cloud();
|
|
@@ -376,6 +575,7 @@ export const HANDLERS = {
|
|
|
376
575
|
...(bind.length ? { secrets: bind } : {}),
|
|
377
576
|
...(inputs ? { inputs } : {}),
|
|
378
577
|
...(lifetime ? { lifetime } : {}),
|
|
578
|
+
...(mode ? { mode } : {}),
|
|
379
579
|
agentLabel: ctx.agentLabel,
|
|
380
580
|
wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
|
|
381
581
|
}));
|
|
@@ -438,16 +638,49 @@ export const HANDLERS = {
|
|
|
438
638
|
const env = parseEnvPairs(values.env ?? []);
|
|
439
639
|
const timeout = str(values.timeout);
|
|
440
640
|
const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
|
|
641
|
+
const executionId = str(values['execution-id']);
|
|
642
|
+
if (executionId !== undefined && !EXECUTION_ID.test(executionId))
|
|
643
|
+
throw new UsageError(`--execution-id must be ${EXECUTION_ID_TEXT} (got "${executionId}")`);
|
|
644
|
+
const limitText = str(values['output-limit']);
|
|
645
|
+
const outputLimit = limitText === undefined ? undefined : parsePositiveInt(limitText, 'output-limit', MAX_OUTPUT_LIMIT);
|
|
441
646
|
const stdinSource = str(values.stdin);
|
|
442
647
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
443
648
|
const ws = await resolveWorkspace(ctx, ref);
|
|
444
649
|
const cell = cellOf(ctx, ws);
|
|
650
|
+
const cwd = str(values.cwd);
|
|
651
|
+
if (ws.mode === 'file_first') {
|
|
652
|
+
// An execution (contracts §29.8): answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
653
|
+
const id = executionId ?? newExecutionId();
|
|
654
|
+
let r;
|
|
655
|
+
try {
|
|
656
|
+
r = await cell.executions.run(argv, {
|
|
657
|
+
executionId: id,
|
|
658
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
659
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
660
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
661
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
662
|
+
...(outputLimit !== undefined ? { outputLimitBytes: outputLimit } : {}),
|
|
663
|
+
signal: ctx.signal,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
if (ctx.signal.aborted) {
|
|
668
|
+
throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution cannot be canceled).`, undefined, {
|
|
669
|
+
executionId: id,
|
|
670
|
+
hint: `fetch its result later with: shard executions get ${ref} ${id}`,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
throw err;
|
|
674
|
+
}
|
|
675
|
+
return reportExecution(ctx, ref, r, { summary: values.verbose === true, timings: ctx.timing });
|
|
676
|
+
}
|
|
677
|
+
fileFirstOnly(ws, [...(executionId !== undefined ? ['--execution-id'] : []), ...(outputLimit !== undefined ? ['--output-limit'] : [])]);
|
|
445
678
|
const sessionId = `cli-${randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
446
679
|
let r;
|
|
447
680
|
try {
|
|
448
681
|
r = await cell.exec.run(argv, {
|
|
449
682
|
sessionId,
|
|
450
|
-
...(
|
|
683
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
451
684
|
...(Object.keys(env).length ? { env } : {}),
|
|
452
685
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
453
686
|
...(stdin !== undefined ? { stdin } : {}),
|
|
@@ -464,6 +697,10 @@ export const HANDLERS = {
|
|
|
464
697
|
throw err;
|
|
465
698
|
}
|
|
466
699
|
const code = execExitCode(r);
|
|
700
|
+
if (values.verbose === true && !ctx.json) {
|
|
701
|
+
const status = r.timedOut ? 'timed out' : r.canceled ? 'canceled' : r.termSignal ? `killed by signal ${r.termSignal}` : `exit code ${r.exitCode ?? '-'}`;
|
|
702
|
+
ctx.io.stderr.write(kv([['session', r.sessionId], ['status', status], ['output', `${r.stdoutBytes} bytes stdout, ${r.stderrBytes} bytes stderr`], ['reconnects', String(r.reconnects)]]));
|
|
703
|
+
}
|
|
467
704
|
if (ctx.json) {
|
|
468
705
|
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 })));
|
|
469
706
|
}
|
|
@@ -486,10 +723,15 @@ export const HANDLERS = {
|
|
|
486
723
|
return 0;
|
|
487
724
|
},
|
|
488
725
|
async 'files write'(ctx, values, [ref, path]) {
|
|
726
|
+
const ifRev = ifRevision(values);
|
|
489
727
|
const data = await readInput(ctx, str(values.from) ?? '-', 1024 * 1024 * 1024, 'input');
|
|
490
728
|
const ws = await resolveWorkspace(ctx, ref);
|
|
491
|
-
|
|
492
|
-
|
|
729
|
+
fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : []);
|
|
730
|
+
const cell = cellOf(ctx, ws);
|
|
731
|
+
const r = await cell.files.write(path, data, { append: values.append === true, createParents: values.parents !== false, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) });
|
|
732
|
+
// File-first: the tree revision the write published (or the current one when nothing changed), to chain --if-revision.
|
|
733
|
+
const tree = treeRevisionAfter(ws, cell);
|
|
734
|
+
out(ctx, `wrote ${r.bytes_written} bytes to ${r.path}${r.sha256 ? ` (sha256 ${r.sha256})` : ''}${tree !== null ? `, tree revision ${tree}` : ''}\n`, tree !== null ? { ...r, tree_revision: tree } : r);
|
|
493
735
|
return 0;
|
|
494
736
|
},
|
|
495
737
|
async 'files ls'(ctx, values, [ref, path]) {
|
|
@@ -499,11 +741,92 @@ export const HANDLERS = {
|
|
|
499
741
|
out(ctx, `${fileTable(list)}${list.truncated ? '(truncated)\n' : ''}`, list);
|
|
500
742
|
return 0;
|
|
501
743
|
},
|
|
744
|
+
async 'files stat'(ctx, values, [ref, path]) {
|
|
745
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
746
|
+
const info = await cellOf(ctx, ws).files.stat(path, values.revision === true ? { revision: true } : {});
|
|
747
|
+
out(ctx, fileInfoText(info), info);
|
|
748
|
+
return 0;
|
|
749
|
+
},
|
|
750
|
+
async 'files search'(ctx, values, [ref, path, pattern]) {
|
|
751
|
+
const maxText = str(values.max);
|
|
752
|
+
const include = strs(values.include);
|
|
753
|
+
const exclude = strs(values.exclude);
|
|
754
|
+
for (const g of [...include, ...exclude])
|
|
755
|
+
if (g.length === 0 || g.length > 256)
|
|
756
|
+
throw new UsageError('--include/--exclude globs must be 1-256 characters');
|
|
757
|
+
if (include.length > 32 || exclude.length > 32)
|
|
758
|
+
throw new UsageError('at most 32 --include and 32 --exclude globs');
|
|
759
|
+
if (pattern.length === 0 || pattern.length > 1000)
|
|
760
|
+
throw new UsageError('<pattern> must be 1-1000 characters');
|
|
761
|
+
const maxMatches = maxText === undefined ? undefined : parsePositiveInt(maxText, 'max', 5000);
|
|
762
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
763
|
+
const r = await cellOf(ctx, ws).files.search(path, pattern, {
|
|
764
|
+
...(values.regex === true ? { regex: true } : {}),
|
|
765
|
+
...(values['ignore-case'] === true ? { caseInsensitive: true } : {}),
|
|
766
|
+
...(include.length ? { include } : {}),
|
|
767
|
+
...(exclude.length ? { exclude } : {}),
|
|
768
|
+
...(maxMatches !== undefined ? { maxMatches } : {}),
|
|
769
|
+
signal: ctx.signal,
|
|
770
|
+
});
|
|
771
|
+
out(ctx, searchText(r), r);
|
|
772
|
+
if (!ctx.json && r.truncated) {
|
|
773
|
+
const at = r.stop_reason ? ` at ${STOPPED_AT[r.stop_reason] ?? r.stop_reason}` : '';
|
|
774
|
+
const raise = r.stop_reason === undefined || r.stop_reason === 'max_matches' ? ' or raise --max' : '';
|
|
775
|
+
ctx.io.stderr.write(`truncated: the search stopped${at} after ${r.matches.length} matches (${r.files_scanned} files scanned); narrow it${raise}\n`);
|
|
776
|
+
}
|
|
777
|
+
if (!ctx.json && r.served_from === 'disk')
|
|
778
|
+
ctx.io.stderr.write('note: searched the suspended workspace’s disk (its state at suspension)\n');
|
|
779
|
+
return 0;
|
|
780
|
+
},
|
|
781
|
+
async 'files patch'(ctx, values, [ref, path]) {
|
|
782
|
+
const olds = strs(values.old);
|
|
783
|
+
const news = strs(values.new);
|
|
784
|
+
const contentFile = str(values['content-file']);
|
|
785
|
+
const expected = str(values['expected-revision']);
|
|
786
|
+
if (contentFile !== undefined && (olds.length || news.length))
|
|
787
|
+
throw new UsageError('use either --old/--new or --content-file, not both');
|
|
788
|
+
if (contentFile === undefined && olds.length === 0)
|
|
789
|
+
throw new UsageError('give --old <text> --new <text> (repeatable), or --content-file <file|->');
|
|
790
|
+
if (olds.length !== news.length)
|
|
791
|
+
throw new UsageError(`every --old needs a --new (got ${olds.length} --old and ${news.length} --new)`);
|
|
792
|
+
if (olds.some((o) => o.length === 0))
|
|
793
|
+
throw new UsageError('--old must not be empty');
|
|
794
|
+
if (olds.length > 100)
|
|
795
|
+
throw new UsageError('at most 100 --old/--new pairs');
|
|
796
|
+
if (contentFile !== undefined && values['replace-all'] === true)
|
|
797
|
+
throw new UsageError('--replace-all applies to --old/--new, not --content-file');
|
|
798
|
+
if (expected !== undefined && !REVISION.test(expected))
|
|
799
|
+
throw new UsageError('--expected-revision must be a 64-character lowercase SHA-256 hex revision or "absent"');
|
|
800
|
+
const ifRev = ifRevision(values);
|
|
801
|
+
let content;
|
|
802
|
+
if (contentFile !== undefined) {
|
|
803
|
+
const bytes = await readInput(ctx, contentFile, MAX_PATCH_CONTENT, '--content-file');
|
|
804
|
+
try {
|
|
805
|
+
content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
throw new UsageError('--content-file must be UTF-8 text (files patch edits text; use files write for binary data)');
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
812
|
+
fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : [], ' (--expected-revision checks the file’s own revision in any workspace)');
|
|
813
|
+
const cell = cellOf(ctx, ws);
|
|
814
|
+
const common = { path: path, ...(expected !== undefined ? { expectedRevision: expected } : {}) };
|
|
815
|
+
const opts = { signal: ctx.signal, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) };
|
|
816
|
+
const r = content !== undefined
|
|
817
|
+
? await cell.files.patch({ ...common, content }, opts)
|
|
818
|
+
: await cell.files.patch({ ...common, edits: olds.map((o, i) => ({ oldText: o, newText: news[i], ...(values['replace-all'] === true ? { replaceAll: true } : {}) })) }, opts);
|
|
819
|
+
const what = r.replacements !== undefined ? `${r.replacements} replacement${r.replacements === 1 ? '' : 's'}` : `${r.bytes_written} bytes`;
|
|
820
|
+
const tree = treeRevisionAfter(ws, cell);
|
|
821
|
+
out(ctx, `patched ${r.path}: ${what}, revision ${r.revision}${tree !== null ? `, tree revision ${tree}` : ''}\n`, tree !== null ? { ...r, tree_revision: tree } : r);
|
|
822
|
+
return 0;
|
|
823
|
+
},
|
|
502
824
|
async 'workspaces suspend'(ctx, values, [ref]) {
|
|
503
|
-
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o));
|
|
825
|
+
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o), ['suspend', 'Suspend']);
|
|
504
826
|
},
|
|
505
827
|
async 'workspaces resume'(ctx, values, [ref]) {
|
|
506
|
-
|
|
828
|
+
// With --wait the resume is held until the workspace runs (contracts §22.6); its tool token is the CLI's label.
|
|
829
|
+
return lifecycle(ctx, values, ref, 'Resuming', (ws, o) => ctx.cloud().workspaces.resume(ws.id, { ...o, agentLabel: ctx.agentLabel }), ['resume', 'Resume']);
|
|
507
830
|
},
|
|
508
831
|
async 'workspaces delete'(ctx, values, [ref]) {
|
|
509
832
|
if (values.yes !== true)
|
|
@@ -516,7 +839,7 @@ export const HANDLERS = {
|
|
|
516
839
|
async 'workspaces reset'(ctx, values, [ref]) {
|
|
517
840
|
if (values.yes !== true)
|
|
518
841
|
throw new UsageError('refusing to reset without --yes (reset wipes every change in the workspace and restarts it on its template)');
|
|
519
|
-
return lifecycle(ctx, values, ref, 'Resetting', (ws, o) => ctx.cloud().workspaces.reset(ws.id, o));
|
|
842
|
+
return lifecycle(ctx, values, ref, 'Resetting', (ws, o) => ctx.cloud().workspaces.reset(ws.id, o), ['reset', 'Reset']);
|
|
520
843
|
},
|
|
521
844
|
async 'workspaces save-as-template'(ctx, values, [ref]) {
|
|
522
845
|
const template = str(values.template);
|
|
@@ -528,6 +851,7 @@ export const HANDLERS = {
|
|
|
528
851
|
const defaults = saveDefaults(values);
|
|
529
852
|
const ack = acknowledged(values);
|
|
530
853
|
const ws = await resolveWorkspace(ctx, ref);
|
|
854
|
+
requireProcessful(ws, 'save_as_template', 'Saving as a template');
|
|
531
855
|
const res = await ctx.cloud().workspaces.saveAsTemplate(ws.id, {
|
|
532
856
|
templateSlug: template,
|
|
533
857
|
...(str(values['display-name']) !== undefined ? { displayName: str(values['display-name']) } : {}),
|
|
@@ -1004,6 +1328,7 @@ export const HANDLERS = {
|
|
|
1004
1328
|
const c = caps(values);
|
|
1005
1329
|
const timeoutMs = waitTimeout(values);
|
|
1006
1330
|
const source = await resolveWorkspace(ctx, ref);
|
|
1331
|
+
requireProcessful(source, 'fork', 'A fork');
|
|
1007
1332
|
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)));
|
|
1008
1333
|
const op = res.operation;
|
|
1009
1334
|
out(ctx, lifecycleText(`Forking ${source.key} into`, res.workspace, op), { operation: op, workspace: res.workspace.data });
|
|
@@ -1154,7 +1479,41 @@ export const HANDLERS = {
|
|
|
1154
1479
|
out(ctx, operationDetail(op), op);
|
|
1155
1480
|
return 0;
|
|
1156
1481
|
},
|
|
1482
|
+
async 'executions get'(ctx, values, [ref, id]) {
|
|
1483
|
+
if (!EXECUTION_ID.test(id))
|
|
1484
|
+
throw new UsageError(`<execution-id> must be ${EXECUTION_ID_TEXT} (got "${id}")`);
|
|
1485
|
+
const waitText = str(values.wait);
|
|
1486
|
+
const waitMs = waitText === undefined ? 0 : parseDuration(waitText, 'wait');
|
|
1487
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
1488
|
+
let r;
|
|
1489
|
+
try {
|
|
1490
|
+
r = await cellOf(ctx, ws).executions.get(id, { waitMs, signal: ctx.signal });
|
|
1491
|
+
}
|
|
1492
|
+
catch (err) {
|
|
1493
|
+
if (ctx.signal.aborted)
|
|
1494
|
+
throw new InterruptedError(`Interrupted; execution ${id} continues server side.`, undefined, { executionId: id, hint: `fetch its result later with: shard executions get ${ref} ${id}` });
|
|
1495
|
+
throw err;
|
|
1496
|
+
}
|
|
1497
|
+
return reportExecution(ctx, ref, r, { summary: true, timings: false, fetched: true, ...(waitText !== undefined ? { waited: waitText } : {}) });
|
|
1498
|
+
},
|
|
1157
1499
|
};
|
|
1500
|
+
/** The update line of `shard version`. */
|
|
1501
|
+
function updateLine(u, d) {
|
|
1502
|
+
const pkg = d.package === '@shardflux/cli' ? '' : ` (${d.package} ${d.version})`;
|
|
1503
|
+
if (!u)
|
|
1504
|
+
return `update check: not done yet${pkg} (shard version --check asks now)\n`;
|
|
1505
|
+
const when = u.checked_at ? `, checked ${u.checked_at.slice(0, 19).replace('T', ' ')}Z` : '';
|
|
1506
|
+
switch (u.status) {
|
|
1507
|
+
case 'current':
|
|
1508
|
+
return `up to date${pkg}: latest ${u.latest ?? '-'}${when}\n`;
|
|
1509
|
+
case 'outdated':
|
|
1510
|
+
return `update available${pkg}: ${u.latest} (you have ${u.current}). Update: ${u.upgrade_command ?? '-'}\n`;
|
|
1511
|
+
case 'unsupported':
|
|
1512
|
+
return `no longer supported${pkg}: the API needs ${u.minimum_supported} or later (latest ${u.latest ?? '-'}). Update: ${u.upgrade_command ?? '-'}\n`;
|
|
1513
|
+
default:
|
|
1514
|
+
return `update status unknown${pkg}${when} (the API could not be asked, or has no release of ${u.package} yet)\n`;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1158
1517
|
/** `--org <id>`: organization scope; null: the API key's project. */
|
|
1159
1518
|
function scopeFlag(values) {
|
|
1160
1519
|
const org = str(values.org);
|
|
@@ -1262,9 +1621,11 @@ async function resolveSecret(ctx, values, ref) {
|
|
|
1262
1621
|
function lifecycleOptions(ctx, values, timeoutMs) {
|
|
1263
1622
|
return values.wait === true ? { wait: { timeoutMs, signal: ctx.signal } } : {};
|
|
1264
1623
|
}
|
|
1265
|
-
async function lifecycle(ctx, values, ref, verb, start) {
|
|
1624
|
+
async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
|
|
1266
1625
|
const timeoutMs = waitTimeout(values);
|
|
1267
1626
|
const ws = await resolveWorkspace(ctx, ref);
|
|
1627
|
+
if (processfulOnly)
|
|
1628
|
+
requireProcessful(ws, processfulOnly[0], processfulOnly[1]);
|
|
1268
1629
|
const op = await waited(ctx, 'the operation continues server side.', () => start(ws, lifecycleOptions(ctx, values, timeoutMs)));
|
|
1269
1630
|
out(ctx, lifecycleText(verb, ws, op), op);
|
|
1270
1631
|
return 0;
|