@shardflux/cli 0.4.1 → 0.5.1
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 +226 -0
- package/README.md +517 -34
- package/dist/account-args.d.ts +13 -0
- package/dist/account-args.js +425 -0
- package/dist/account-commands.d.ts +18 -0
- package/dist/account-commands.js +1657 -0
- package/dist/args.d.ts +15 -3
- package/dist/args.js +292 -41
- package/dist/bin.js +5 -16
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +39 -0
- package/dist/commands.d.ts +69 -5
- package/dist/commands.js +440 -30
- package/dist/credentials.d.ts +77 -0
- package/dist/credentials.js +217 -0
- package/dist/errors.d.ts +63 -11
- package/dist/errors.js +114 -21
- package/dist/format.d.ts +89 -6
- package/dist/format.js +479 -10
- 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 +477 -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, suspendRequestText, suspendWhenIdleText, 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,17 @@ 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
|
-
|
|
69
|
+
/** `--when-idle <duration>`: whole seconds from 30 to 3600 (the API's after_seconds bounds, contracts §20.6). */
|
|
70
|
+
export function whenIdleSeconds(text) {
|
|
71
|
+
const ms = parseDuration(text, 'when-idle');
|
|
72
|
+
if (ms % 1000 !== 0)
|
|
73
|
+
throw new UsageError('--when-idle must be a whole number of seconds (e.g. 30s, 90s, 2m)');
|
|
74
|
+
const s = ms / 1000;
|
|
75
|
+
if (s < 30 || s > 3600)
|
|
76
|
+
throw new UsageError(`--when-idle must be between 30s and 1h (got "${text}")`);
|
|
77
|
+
return s;
|
|
78
|
+
}
|
|
79
|
+
export function waitTimeout(values) {
|
|
61
80
|
const t = str(values.timeout);
|
|
62
81
|
return t === undefined ? DEFAULT_WAIT_MS : parseDuration(t, 'timeout');
|
|
63
82
|
}
|
|
@@ -96,7 +115,7 @@ const PURPOSE_FILTERS = ['standard', 'template_draft', 'template_test', 'any'];
|
|
|
96
115
|
const DIFF_CHANGES = ['added', 'removed', 'changed', 'type_changed', 'metadata'];
|
|
97
116
|
const OWNERS = ['platform', 'organization'];
|
|
98
117
|
const DEFAULT_BUILD_WAIT_MS = 1_800_000;
|
|
99
|
-
function oneOf(values, option, allowed) {
|
|
118
|
+
export function oneOf(values, option, allowed) {
|
|
100
119
|
const v = str(values[option]);
|
|
101
120
|
if (v === undefined)
|
|
102
121
|
return undefined;
|
|
@@ -104,7 +123,7 @@ function oneOf(values, option, allowed) {
|
|
|
104
123
|
throw new UsageError(`--${option} must be one of ${allowed.join(', ')} (got "${v}")`);
|
|
105
124
|
return v;
|
|
106
125
|
}
|
|
107
|
-
function listLimit(values, max) {
|
|
126
|
+
export function listLimit(values, max) {
|
|
108
127
|
const t = str(values.limit);
|
|
109
128
|
return t === undefined ? undefined : parsePositiveInt(t, 'limit', max);
|
|
110
129
|
}
|
|
@@ -160,7 +179,7 @@ function parseVersion(text, what) {
|
|
|
160
179
|
}
|
|
161
180
|
const SLUG = /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
|
|
162
181
|
/** `<slug>@<version>`. */
|
|
163
|
-
function parseSlugVersion(text, what) {
|
|
182
|
+
export function parseSlugVersion(text, what) {
|
|
164
183
|
const at = text.lastIndexOf('@');
|
|
165
184
|
if (at <= 0)
|
|
166
185
|
throw new UsageError(`${what} must be <slug>@<version>, e.g. acme-dev@3 (got "${text}")`);
|
|
@@ -230,7 +249,7 @@ const BUILD_STATES = ['queued', 'building', 'testing', 'publishing', 'published'
|
|
|
230
249
|
function buildExitCode(b) {
|
|
231
250
|
return b.state === 'failed' || b.state === 'canceled' || b.registration.state === 'failed' ? 6 : 0;
|
|
232
251
|
}
|
|
233
|
-
async function fileExists(path) {
|
|
252
|
+
export async function fileExists(path) {
|
|
234
253
|
return access(path).then(() => true, () => false);
|
|
235
254
|
}
|
|
236
255
|
/** Writes `text` to `path`, refusing to overwrite an existing file without --force. */
|
|
@@ -283,7 +302,7 @@ async function readAll(stream, max, what) {
|
|
|
283
302
|
}
|
|
284
303
|
return new Uint8Array(Buffer.concat(chunks));
|
|
285
304
|
}
|
|
286
|
-
async function readInput(ctx, source, max, what) {
|
|
305
|
+
export async function readInput(ctx, source, max, what) {
|
|
287
306
|
if (source === '-') {
|
|
288
307
|
if (ctx.io.stdin.isTTY)
|
|
289
308
|
throw new UsageError(`${what}: standard input is a terminal; pipe data in or pass a file`);
|
|
@@ -296,7 +315,7 @@ async function readInput(ctx, source, max, what) {
|
|
|
296
315
|
throw new UsageError(`${what} exceeds ${max} bytes`);
|
|
297
316
|
return new Uint8Array(data);
|
|
298
317
|
}
|
|
299
|
-
async function principal(ctx) {
|
|
318
|
+
export async function principal(ctx) {
|
|
300
319
|
const me = await ctx.cloud().me();
|
|
301
320
|
if (me.type !== 'api_key' || !me.api_key)
|
|
302
321
|
throw new UsageError('SHARDFLUX_API_KEY did not authenticate as an API key');
|
|
@@ -317,6 +336,72 @@ function principalText(p) {
|
|
|
317
336
|
['tools', p.tool_permissions.length ? p.tool_permissions.join(', ') : '(none: read-only key)'],
|
|
318
337
|
]);
|
|
319
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Who sends the feedback: the project API key when one is configured (SHARDFLUX_API_KEY or saved by shard setup),
|
|
341
|
+
* else the signed-in session (SHARDFLUX_SESSION_TOKEN or saved by shard auth login), so an agent that is still setting
|
|
342
|
+
* up can report what got in its way. With neither, the key's own error (its hint names both ways, plus email).
|
|
343
|
+
*/
|
|
344
|
+
function feedbackSender(ctx) {
|
|
345
|
+
const keyConfigured = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
|
|
346
|
+
const sessionConfigured = Boolean(ctx.io.env.SHARDFLUX_SESSION_TOKEN?.trim()) || ctx.store.profile(ctx.apiUrl).session !== undefined;
|
|
347
|
+
return keyConfigured || !sessionConfigured ? ctx.cloud() : ctx.account();
|
|
348
|
+
}
|
|
349
|
+
/** Contract bounds of the feedback context fields shard sends (characters). */
|
|
350
|
+
const FEEDBACK_CONTEXT_MAX = { workspace: 200, 'request-id': 200, 'error-code': 100, command: 2000 };
|
|
351
|
+
/**
|
|
352
|
+
* The feedback message: the positional words joined with spaces, or --from-file (a file, or - for standard input),
|
|
353
|
+
* trimmed, 1-8000 characters (the API's bound, counted in UTF-16 units so nothing the API would refuse is sent).
|
|
354
|
+
*/
|
|
355
|
+
async function feedbackMessage(ctx, values, words) {
|
|
356
|
+
const file = str(values['from-file']);
|
|
357
|
+
if (file !== undefined && words.length > 0)
|
|
358
|
+
throw new UsageError('give the message as arguments or with --from-file, not both');
|
|
359
|
+
let text;
|
|
360
|
+
if (file !== undefined) {
|
|
361
|
+
const bytes = await readInput(ctx, file, MAX_STDIN, '--from-file');
|
|
362
|
+
try {
|
|
363
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
throw new UsageError('--from-file: the message must be UTF-8 text');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (words.length > 0) {
|
|
370
|
+
text = words.join(' ');
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
throw new UsageError('missing <message> for "shard feedback": shard feedback "what happened and what you expected" (or --from-file <file|->)');
|
|
374
|
+
}
|
|
375
|
+
text = text.trim();
|
|
376
|
+
if (text.length === 0)
|
|
377
|
+
throw new UsageError('the feedback message is empty');
|
|
378
|
+
if (text.length > FEEDBACK_MESSAGE_MAX_LENGTH) {
|
|
379
|
+
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)`);
|
|
380
|
+
}
|
|
381
|
+
return text;
|
|
382
|
+
}
|
|
383
|
+
/** The context shard sends: its own client, the agent label when one is set, and the given references. */
|
|
384
|
+
function feedbackContext(ctx, values) {
|
|
385
|
+
const c = { client: `shard-cli/${CLI_VERSION}` };
|
|
386
|
+
if (ctx.agentLabel !== 'cli')
|
|
387
|
+
c.agent = ctx.agentLabel;
|
|
388
|
+
for (const [option, max] of Object.entries(FEEDBACK_CONTEXT_MAX)) {
|
|
389
|
+
const v = str(values[option])?.trim();
|
|
390
|
+
if (v === undefined)
|
|
391
|
+
continue;
|
|
392
|
+
if (v.length === 0 || v.length > max)
|
|
393
|
+
throw new UsageError(`--${option} must be 1-${max} characters`);
|
|
394
|
+
if (option === 'workspace')
|
|
395
|
+
c.workspace = v;
|
|
396
|
+
else if (option === 'request-id')
|
|
397
|
+
c.requestId = v;
|
|
398
|
+
else if (option === 'error-code')
|
|
399
|
+
c.errorCode = v;
|
|
400
|
+
else
|
|
401
|
+
c.command = v;
|
|
402
|
+
}
|
|
403
|
+
return c;
|
|
404
|
+
}
|
|
320
405
|
/** The exit code `shard exec` returns for a finished command. */
|
|
321
406
|
export function execExitCode(r) {
|
|
322
407
|
if (r.timedOut)
|
|
@@ -330,14 +415,95 @@ export function execExitCode(r) {
|
|
|
330
415
|
async function bestEffortCancel(cell, sessionId) {
|
|
331
416
|
await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
|
|
332
417
|
}
|
|
418
|
+
// ---- File-first workspaces (0.5.0; contracts §29) ----------------------------------------------------------------------
|
|
419
|
+
const MODES = { processful: 'processful', 'file-first': 'file_first', file_first: 'file_first' };
|
|
420
|
+
/** `--mode processful|file-first` (`file_first` too). */
|
|
421
|
+
function modeFlag(values) {
|
|
422
|
+
const v = str(values.mode);
|
|
423
|
+
if (v === undefined)
|
|
424
|
+
return undefined;
|
|
425
|
+
const m = MODES[v];
|
|
426
|
+
if (!m)
|
|
427
|
+
throw new UsageError(`--mode must be processful or file-first (got "${v}")`);
|
|
428
|
+
return m;
|
|
429
|
+
}
|
|
430
|
+
const EXECUTION_ID_TEXT = '8-128 characters of A-Z a-z 0-9 . _ : -, starting with a letter or digit';
|
|
431
|
+
/** `--if-revision <n>`: a tree revision, a non-negative integer (0 is the empty tree a file-first workspace starts with). */
|
|
432
|
+
function ifRevision(values) {
|
|
433
|
+
const t = str(values['if-revision']);
|
|
434
|
+
if (t === undefined)
|
|
435
|
+
return undefined;
|
|
436
|
+
if (!/^\d{1,15}$/.test(t.trim()))
|
|
437
|
+
throw new UsageError(`--if-revision must be a tree revision, a non-negative integer (got "${t}")`);
|
|
438
|
+
return Number(t.trim());
|
|
439
|
+
}
|
|
440
|
+
/** Options that exist only for file-first workspaces are a usage error on a processful one (known once resolved). */
|
|
441
|
+
function fileFirstOnly(ws, flags, note = '') {
|
|
442
|
+
if (flags.length === 0 || ws.mode === 'file_first')
|
|
443
|
+
return;
|
|
444
|
+
throw new UsageError(`${flags.join(' and ')} ${flags.length > 1 ? 'apply' : 'applies'} to file-first workspaces only; ${ws.key} is ${modeText(ws.mode)}${note}`);
|
|
445
|
+
}
|
|
446
|
+
/** The tree revision after a mutating files call on a file-first workspace (the newest its responses reported); else null. */
|
|
447
|
+
function treeRevisionAfter(ws, cell) {
|
|
448
|
+
if (ws.mode !== 'file_first')
|
|
449
|
+
return null;
|
|
450
|
+
return cell.treeRevision ?? ws.treeRevision;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* A lifecycle call a file-first workspace does not have (contracts §29.7: suspend, resume, fork, reset,
|
|
454
|
+
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
|
455
|
+
* with its hint like the API's own refusal.
|
|
456
|
+
*/
|
|
457
|
+
function requireProcessful(ws, operation, what) {
|
|
458
|
+
if (ws.mode !== 'file_first')
|
|
459
|
+
return;
|
|
460
|
+
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}).`);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Prints an execution's result and returns the exit code. Human: the command's stdout and stderr bytes as they are
|
|
464
|
+
* (binary safe), a note when output was cut at the output limit, and with `summary` what ran (stderr; see
|
|
465
|
+
* executionSummary). --json: one document (executionJson). The exit code is the command's once it succeeded (like a
|
|
466
|
+
* processful exec: 124 timed out, 128+N killed by signal N); a failed or lost execution throws ExecutionFailedError
|
|
467
|
+
* (exit 6), one still queued or running ExecutionPendingError (exit 5, naming the command that waits for it).
|
|
468
|
+
*/
|
|
469
|
+
function reportExecution(ctx, ref, r, opts) {
|
|
470
|
+
if (ctx.json)
|
|
471
|
+
ctx.io.stdout.write(json(withTiming(ctx, executionJson(r))));
|
|
472
|
+
else if (!r.pending) {
|
|
473
|
+
if (r.stdout.length)
|
|
474
|
+
ctx.io.stdout.write(r.stdout);
|
|
475
|
+
if (r.stderr.length)
|
|
476
|
+
ctx.io.stderr.write(r.stderr);
|
|
477
|
+
const cut = [r.stdoutTruncated ? 'stdout' : '', r.stderrTruncated ? 'stderr' : ''].filter(Boolean).join(' and ');
|
|
478
|
+
if (opts.summary)
|
|
479
|
+
ctx.io.stderr.write(executionSummary(r, undefined, opts.fetched ? { replayed: false } : {}));
|
|
480
|
+
else {
|
|
481
|
+
if (cut)
|
|
482
|
+
ctx.io.stderr.write(`truncated: ${cut} reached the output limit (--output-limit, default 1 MiB, at most 16 MiB)\n`);
|
|
483
|
+
if (opts.timings && Object.keys(r.timings).length)
|
|
484
|
+
ctx.io.stderr.write(`execution ${r.executionId} timings: ${pairs(r.timings)}\n`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (r.pending) {
|
|
488
|
+
const when = opts.waited ? `still ${r.state} after ${opts.waited}` : `${r.state} (on tree revision ${r.baseRevision})`;
|
|
489
|
+
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`);
|
|
490
|
+
}
|
|
491
|
+
if (r.state === 'failed' || r.state === 'lost')
|
|
492
|
+
throw new ExecutionFailedError(r);
|
|
493
|
+
return execExitCode(r);
|
|
494
|
+
}
|
|
333
495
|
function lifecycleText(verb, w, op) {
|
|
334
496
|
const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
|
|
335
497
|
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
498
|
}
|
|
337
|
-
|
|
499
|
+
/** The project-key commands; handlers.ts adds the account plane's. */
|
|
500
|
+
export const KEY_HANDLERS = {
|
|
338
501
|
async login(ctx) {
|
|
339
502
|
const p = await principal(ctx);
|
|
340
|
-
|
|
503
|
+
const where = ctx.apiKey().source === 'env'
|
|
504
|
+
? 'The key comes from SHARDFLUX_API_KEY; shard stores nothing for it.'
|
|
505
|
+
: `The key is the one saved in ${ctx.store.file} (shard setup).`;
|
|
506
|
+
out(ctx, `Authenticated.\n${principalText(p)}${where} To sign in to your account instead: shard auth login.\n`, p);
|
|
341
507
|
return 0;
|
|
342
508
|
},
|
|
343
509
|
async whoami(ctx) {
|
|
@@ -345,14 +511,54 @@ export const HANDLERS = {
|
|
|
345
511
|
out(ctx, principalText(p), p);
|
|
346
512
|
return 0;
|
|
347
513
|
},
|
|
348
|
-
version(ctx) {
|
|
349
|
-
|
|
350
|
-
|
|
514
|
+
async version(ctx, values) {
|
|
515
|
+
const update = await ctx.versionStatus(values.check === true);
|
|
516
|
+
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);
|
|
517
|
+
out(ctx, `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n${line}`, { cli: CLI_VERSION, sdk: SDK_VERSION, update });
|
|
518
|
+
return 0;
|
|
519
|
+
},
|
|
520
|
+
// Feedback (0.5.0+): POST /v1/feedback, emailed to the founder. Validated here first so a bad call costs no request.
|
|
521
|
+
async feedback(ctx, values, words) {
|
|
522
|
+
const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
|
|
523
|
+
const context = feedbackContext(ctx, values);
|
|
524
|
+
const message = await feedbackMessage(ctx, values, words);
|
|
525
|
+
const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
|
|
526
|
+
const human = r.duplicate
|
|
527
|
+
? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
|
|
528
|
+
: `Sent to the Shardflux founder (feedback ${r.id}). Thank you.\n`;
|
|
529
|
+
out(ctx, human, { id: r.id, received_at: r.receivedAt, duplicate: r.duplicate });
|
|
530
|
+
return 0;
|
|
351
531
|
},
|
|
352
532
|
async usage(ctx, values) {
|
|
533
|
+
const views = [values.estimate === true, values.series === true, str(values.workspace) !== undefined].filter(Boolean).length;
|
|
534
|
+
if (views > 1)
|
|
535
|
+
throw new UsageError('use one of --estimate, --series and --workspace');
|
|
536
|
+
const seriesOnly = ['from', 'to', 'granularity', 'meter'].filter((k) => values[k] !== undefined);
|
|
537
|
+
if (seriesOnly.length > 0 && values.series !== true && str(values.workspace) === undefined) {
|
|
538
|
+
throw new UsageError(`--${seriesOnly[0]} needs --series or --workspace`);
|
|
539
|
+
}
|
|
540
|
+
const granularity = values.granularity === undefined ? undefined : oneOf(values, 'granularity', ['hour', 'day']);
|
|
541
|
+
const range = { from: str(values.from), to: str(values.to), granularity, meter: str(values.meter) };
|
|
542
|
+
const workspaceRef = str(values.workspace);
|
|
543
|
+
if (workspaceRef !== undefined) {
|
|
544
|
+
const ws = await resolveWorkspace(ctx, workspaceRef);
|
|
545
|
+
const series = await ctx.cloud().usage.workspace(ws.id, range);
|
|
546
|
+
out(ctx, usageSeriesText(series), series);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
353
549
|
const org = str(values.org) ?? (await principal(ctx)).organization_id;
|
|
354
550
|
if (!UUID.test(org))
|
|
355
551
|
throw new UsageError(`--org must be an organization id (UUID)`);
|
|
552
|
+
if (values.estimate === true) {
|
|
553
|
+
const estimate = await ctx.cloud().usage.estimate(org);
|
|
554
|
+
out(ctx, usageEstimateText(estimate), estimate);
|
|
555
|
+
return 0;
|
|
556
|
+
}
|
|
557
|
+
if (values.series === true) {
|
|
558
|
+
const series = await ctx.cloud().usage.series(org, range);
|
|
559
|
+
out(ctx, usageSeriesText(series), series);
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
356
562
|
const summary = await ctx.cloud().request('GET', `/v1/organizations/${encodeURIComponent(org)}/usage/summary`);
|
|
357
563
|
out(ctx, usageText(summary), summary);
|
|
358
564
|
return 0;
|
|
@@ -364,6 +570,9 @@ export const HANDLERS = {
|
|
|
364
570
|
const c = caps(values);
|
|
365
571
|
const bind = secretNames(strs(values.secret), '--secret');
|
|
366
572
|
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
573
|
+
const mode = modeFlag(values);
|
|
574
|
+
if (mode === 'file_first' && lifetime === 'session')
|
|
575
|
+
throw new UsageError('a file-first workspace is persistent: --lifetime session is for processful workspaces');
|
|
367
576
|
const inputs = await inputsFrom(ctx, values);
|
|
368
577
|
const timeoutMs = waitTimeout(values);
|
|
369
578
|
const cloud = ctx.cloud();
|
|
@@ -376,6 +585,7 @@ export const HANDLERS = {
|
|
|
376
585
|
...(bind.length ? { secrets: bind } : {}),
|
|
377
586
|
...(inputs ? { inputs } : {}),
|
|
378
587
|
...(lifetime ? { lifetime } : {}),
|
|
588
|
+
...(mode ? { mode } : {}),
|
|
379
589
|
agentLabel: ctx.agentLabel,
|
|
380
590
|
wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
|
|
381
591
|
}));
|
|
@@ -438,16 +648,49 @@ export const HANDLERS = {
|
|
|
438
648
|
const env = parseEnvPairs(values.env ?? []);
|
|
439
649
|
const timeout = str(values.timeout);
|
|
440
650
|
const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
|
|
651
|
+
const executionId = str(values['execution-id']);
|
|
652
|
+
if (executionId !== undefined && !EXECUTION_ID.test(executionId))
|
|
653
|
+
throw new UsageError(`--execution-id must be ${EXECUTION_ID_TEXT} (got "${executionId}")`);
|
|
654
|
+
const limitText = str(values['output-limit']);
|
|
655
|
+
const outputLimit = limitText === undefined ? undefined : parsePositiveInt(limitText, 'output-limit', MAX_OUTPUT_LIMIT);
|
|
441
656
|
const stdinSource = str(values.stdin);
|
|
442
657
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
443
658
|
const ws = await resolveWorkspace(ctx, ref);
|
|
444
659
|
const cell = cellOf(ctx, ws);
|
|
660
|
+
const cwd = str(values.cwd);
|
|
661
|
+
if (ws.mode === 'file_first') {
|
|
662
|
+
// An execution (contracts §29.8): answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
663
|
+
const id = executionId ?? newExecutionId();
|
|
664
|
+
let r;
|
|
665
|
+
try {
|
|
666
|
+
r = await cell.executions.run(argv, {
|
|
667
|
+
executionId: id,
|
|
668
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
669
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
670
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
671
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
672
|
+
...(outputLimit !== undefined ? { outputLimitBytes: outputLimit } : {}),
|
|
673
|
+
signal: ctx.signal,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
if (ctx.signal.aborted) {
|
|
678
|
+
throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution cannot be canceled).`, undefined, {
|
|
679
|
+
executionId: id,
|
|
680
|
+
hint: `fetch its result later with: shard executions get ${ref} ${id}`,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
throw err;
|
|
684
|
+
}
|
|
685
|
+
return reportExecution(ctx, ref, r, { summary: values.verbose === true, timings: ctx.timing });
|
|
686
|
+
}
|
|
687
|
+
fileFirstOnly(ws, [...(executionId !== undefined ? ['--execution-id'] : []), ...(outputLimit !== undefined ? ['--output-limit'] : [])]);
|
|
445
688
|
const sessionId = `cli-${randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
446
689
|
let r;
|
|
447
690
|
try {
|
|
448
691
|
r = await cell.exec.run(argv, {
|
|
449
692
|
sessionId,
|
|
450
|
-
...(
|
|
693
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
451
694
|
...(Object.keys(env).length ? { env } : {}),
|
|
452
695
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
453
696
|
...(stdin !== undefined ? { stdin } : {}),
|
|
@@ -464,8 +707,30 @@ export const HANDLERS = {
|
|
|
464
707
|
throw err;
|
|
465
708
|
}
|
|
466
709
|
const code = execExitCode(r);
|
|
710
|
+
// No exit code, no timeout, no signal: the session ended without the command's own ending (e.g. lost when its host
|
|
711
|
+
// restarted). Say so rather than exit 1 in silence (0.5.1); a command that could not start threw ExecStartError.
|
|
712
|
+
const unexplained = r.exitCode === null && !r.timedOut && !(r.termSignal !== null && r.termSignal > 0);
|
|
713
|
+
if (values.verbose === true && !ctx.json) {
|
|
714
|
+
const status = r.timedOut ? 'timed out' : r.canceled ? 'canceled' : r.termSignal ? `killed by signal ${r.termSignal}` : unexplained ? r.session.state : `exit code ${r.exitCode ?? '-'}`;
|
|
715
|
+
ctx.io.stderr.write(kv([['session', r.sessionId], ['status', status], ['output', `${r.stdoutBytes} bytes stdout, ${r.stderrBytes} bytes stderr`], ['reconnects', String(r.reconnects)]]));
|
|
716
|
+
}
|
|
467
717
|
if (ctx.json) {
|
|
468
|
-
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
718
|
+
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
719
|
+
session_id: r.sessionId,
|
|
720
|
+
state: r.session.state,
|
|
721
|
+
exit_code: r.exitCode,
|
|
722
|
+
term_signal: r.termSignal,
|
|
723
|
+
timed_out: r.timedOut,
|
|
724
|
+
canceled: r.canceled,
|
|
725
|
+
stdout: r.stdout,
|
|
726
|
+
stderr: r.stderr,
|
|
727
|
+
truncated: r.truncated,
|
|
728
|
+
reconnects: r.reconnects,
|
|
729
|
+
...(r.session.error ? { error: r.session.error } : {}),
|
|
730
|
+
})));
|
|
731
|
+
}
|
|
732
|
+
else if (unexplained) {
|
|
733
|
+
ctx.io.stderr.write(`exec session ${r.sessionId} ended ${r.session.state} without an exit code${r.session.error ? `: ${r.session.error}` : ''}\n`);
|
|
469
734
|
}
|
|
470
735
|
return code;
|
|
471
736
|
},
|
|
@@ -486,10 +751,15 @@ export const HANDLERS = {
|
|
|
486
751
|
return 0;
|
|
487
752
|
},
|
|
488
753
|
async 'files write'(ctx, values, [ref, path]) {
|
|
754
|
+
const ifRev = ifRevision(values);
|
|
489
755
|
const data = await readInput(ctx, str(values.from) ?? '-', 1024 * 1024 * 1024, 'input');
|
|
490
756
|
const ws = await resolveWorkspace(ctx, ref);
|
|
491
|
-
|
|
492
|
-
|
|
757
|
+
fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : []);
|
|
758
|
+
const cell = cellOf(ctx, ws);
|
|
759
|
+
const r = await cell.files.write(path, data, { append: values.append === true, createParents: values.parents !== false, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) });
|
|
760
|
+
// File-first: the tree revision the write published (or the current one when nothing changed), to chain --if-revision.
|
|
761
|
+
const tree = treeRevisionAfter(ws, cell);
|
|
762
|
+
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
763
|
return 0;
|
|
494
764
|
},
|
|
495
765
|
async 'files ls'(ctx, values, [ref, path]) {
|
|
@@ -499,11 +769,113 @@ export const HANDLERS = {
|
|
|
499
769
|
out(ctx, `${fileTable(list)}${list.truncated ? '(truncated)\n' : ''}`, list);
|
|
500
770
|
return 0;
|
|
501
771
|
},
|
|
772
|
+
async 'files stat'(ctx, values, [ref, path]) {
|
|
773
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
774
|
+
const info = await cellOf(ctx, ws).files.stat(path, values.revision === true ? { revision: true } : {});
|
|
775
|
+
out(ctx, fileInfoText(info), info);
|
|
776
|
+
return 0;
|
|
777
|
+
},
|
|
778
|
+
async 'files search'(ctx, values, [ref, path, pattern]) {
|
|
779
|
+
const maxText = str(values.max);
|
|
780
|
+
const include = strs(values.include);
|
|
781
|
+
const exclude = strs(values.exclude);
|
|
782
|
+
for (const g of [...include, ...exclude])
|
|
783
|
+
if (g.length === 0 || g.length > 256)
|
|
784
|
+
throw new UsageError('--include/--exclude globs must be 1-256 characters');
|
|
785
|
+
if (include.length > 32 || exclude.length > 32)
|
|
786
|
+
throw new UsageError('at most 32 --include and 32 --exclude globs');
|
|
787
|
+
if (pattern.length === 0 || pattern.length > 1000)
|
|
788
|
+
throw new UsageError('<pattern> must be 1-1000 characters');
|
|
789
|
+
const maxMatches = maxText === undefined ? undefined : parsePositiveInt(maxText, 'max', 5000);
|
|
790
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
791
|
+
const r = await cellOf(ctx, ws).files.search(path, pattern, {
|
|
792
|
+
...(values.regex === true ? { regex: true } : {}),
|
|
793
|
+
...(values['ignore-case'] === true ? { caseInsensitive: true } : {}),
|
|
794
|
+
...(include.length ? { include } : {}),
|
|
795
|
+
...(exclude.length ? { exclude } : {}),
|
|
796
|
+
...(maxMatches !== undefined ? { maxMatches } : {}),
|
|
797
|
+
signal: ctx.signal,
|
|
798
|
+
});
|
|
799
|
+
out(ctx, searchText(r), r);
|
|
800
|
+
if (!ctx.json && r.truncated) {
|
|
801
|
+
const at = r.stop_reason ? ` at ${STOPPED_AT[r.stop_reason] ?? r.stop_reason}` : '';
|
|
802
|
+
const raise = r.stop_reason === undefined || r.stop_reason === 'max_matches' ? ' or raise --max' : '';
|
|
803
|
+
ctx.io.stderr.write(`truncated: the search stopped${at} after ${r.matches.length} matches (${r.files_scanned} files scanned); narrow it${raise}\n`);
|
|
804
|
+
}
|
|
805
|
+
if (!ctx.json && r.served_from === 'disk')
|
|
806
|
+
ctx.io.stderr.write('note: searched the suspended workspace’s disk (its state at suspension)\n');
|
|
807
|
+
return 0;
|
|
808
|
+
},
|
|
809
|
+
async 'files patch'(ctx, values, [ref, path]) {
|
|
810
|
+
const olds = strs(values.old);
|
|
811
|
+
const news = strs(values.new);
|
|
812
|
+
const contentFile = str(values['content-file']);
|
|
813
|
+
const expected = str(values['expected-revision']);
|
|
814
|
+
if (contentFile !== undefined && (olds.length || news.length))
|
|
815
|
+
throw new UsageError('use either --old/--new or --content-file, not both');
|
|
816
|
+
if (contentFile === undefined && olds.length === 0)
|
|
817
|
+
throw new UsageError('give --old <text> --new <text> (repeatable), or --content-file <file|->');
|
|
818
|
+
if (olds.length !== news.length)
|
|
819
|
+
throw new UsageError(`every --old needs a --new (got ${olds.length} --old and ${news.length} --new)`);
|
|
820
|
+
if (olds.some((o) => o.length === 0))
|
|
821
|
+
throw new UsageError('--old must not be empty');
|
|
822
|
+
if (olds.length > 100)
|
|
823
|
+
throw new UsageError('at most 100 --old/--new pairs');
|
|
824
|
+
if (contentFile !== undefined && values['replace-all'] === true)
|
|
825
|
+
throw new UsageError('--replace-all applies to --old/--new, not --content-file');
|
|
826
|
+
if (expected !== undefined && !REVISION.test(expected))
|
|
827
|
+
throw new UsageError('--expected-revision must be a 64-character lowercase SHA-256 hex revision or "absent"');
|
|
828
|
+
const ifRev = ifRevision(values);
|
|
829
|
+
let content;
|
|
830
|
+
if (contentFile !== undefined) {
|
|
831
|
+
const bytes = await readInput(ctx, contentFile, MAX_PATCH_CONTENT, '--content-file');
|
|
832
|
+
try {
|
|
833
|
+
content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
throw new UsageError('--content-file must be UTF-8 text (files patch edits text; use files write for binary data)');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
840
|
+
fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : [], ' (--expected-revision checks the file’s own revision in any workspace)');
|
|
841
|
+
const cell = cellOf(ctx, ws);
|
|
842
|
+
const common = { path: path, ...(expected !== undefined ? { expectedRevision: expected } : {}) };
|
|
843
|
+
const opts = { signal: ctx.signal, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) };
|
|
844
|
+
const r = content !== undefined
|
|
845
|
+
? await cell.files.patch({ ...common, content }, opts)
|
|
846
|
+
: await cell.files.patch({ ...common, edits: olds.map((o, i) => ({ oldText: o, newText: news[i], ...(values['replace-all'] === true ? { replaceAll: true } : {}) })) }, opts);
|
|
847
|
+
const what = r.replacements !== undefined ? `${r.replacements} replacement${r.replacements === 1 ? '' : 's'}` : `${r.bytes_written} bytes`;
|
|
848
|
+
const tree = treeRevisionAfter(ws, cell);
|
|
849
|
+
out(ctx, `patched ${r.path}: ${what}, revision ${r.revision}${tree !== null ? `, tree revision ${tree}` : ''}\n`, tree !== null ? { ...r, tree_revision: tree } : r);
|
|
850
|
+
return 0;
|
|
851
|
+
},
|
|
502
852
|
async 'workspaces suspend'(ctx, values, [ref]) {
|
|
503
|
-
|
|
853
|
+
const whenIdle = str(values['when-idle']);
|
|
854
|
+
const cancel = values['cancel-when-idle'] === true;
|
|
855
|
+
if (whenIdle === undefined && !cancel)
|
|
856
|
+
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o), ['suspend', 'Suspend']);
|
|
857
|
+
if (whenIdle !== undefined && cancel)
|
|
858
|
+
throw new UsageError('--when-idle and --cancel-when-idle cannot be combined');
|
|
859
|
+
const flag = cancel ? '--cancel-when-idle' : '--when-idle';
|
|
860
|
+
if (values.wait === true || str(values.timeout) !== undefined)
|
|
861
|
+
throw new UsageError(`${flag} cannot be combined with --wait or --timeout (nothing is suspended now)`);
|
|
862
|
+
if (cancel) {
|
|
863
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
864
|
+
const pending = ws.suspendRequest;
|
|
865
|
+
const after = await ctx.cloud().workspaces.cancelSuspendWhenIdle(ws.id);
|
|
866
|
+
out(ctx, pending ? `Canceled the pending suspend-when-idle request of ${ws.key} (${ws.id}): ${suspendRequestText(pending)}.\n` : `No suspend-when-idle request was pending for ${ws.key} (${ws.id}).\n`, after.data);
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
const afterSeconds = whenIdleSeconds(whenIdle);
|
|
870
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
871
|
+
requireProcessful(ws, 'suspend_when_idle', 'Suspend when idle');
|
|
872
|
+
const r = await ctx.cloud().workspaces.suspendWhenIdle(ws.id, { afterSeconds });
|
|
873
|
+
out(ctx, suspendWhenIdleText(ws, { suspend_request: r.suspendRequest, operation: r.operation }), { workspace: r.workspace.data, operation: r.operation, suspend_request: r.suspendRequest });
|
|
874
|
+
return 0;
|
|
504
875
|
},
|
|
505
876
|
async 'workspaces resume'(ctx, values, [ref]) {
|
|
506
|
-
|
|
877
|
+
// With --wait the resume is held until the workspace runs (contracts §22.6); its tool token is the CLI's label.
|
|
878
|
+
return lifecycle(ctx, values, ref, 'Resuming', (ws, o) => ctx.cloud().workspaces.resume(ws.id, { ...o, agentLabel: ctx.agentLabel }), ['resume', 'Resume']);
|
|
507
879
|
},
|
|
508
880
|
async 'workspaces delete'(ctx, values, [ref]) {
|
|
509
881
|
if (values.yes !== true)
|
|
@@ -516,7 +888,7 @@ export const HANDLERS = {
|
|
|
516
888
|
async 'workspaces reset'(ctx, values, [ref]) {
|
|
517
889
|
if (values.yes !== true)
|
|
518
890
|
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));
|
|
891
|
+
return lifecycle(ctx, values, ref, 'Resetting', (ws, o) => ctx.cloud().workspaces.reset(ws.id, o), ['reset', 'Reset']);
|
|
520
892
|
},
|
|
521
893
|
async 'workspaces save-as-template'(ctx, values, [ref]) {
|
|
522
894
|
const template = str(values.template);
|
|
@@ -528,6 +900,7 @@ export const HANDLERS = {
|
|
|
528
900
|
const defaults = saveDefaults(values);
|
|
529
901
|
const ack = acknowledged(values);
|
|
530
902
|
const ws = await resolveWorkspace(ctx, ref);
|
|
903
|
+
requireProcessful(ws, 'save_as_template', 'Saving as a template');
|
|
531
904
|
const res = await ctx.cloud().workspaces.saveAsTemplate(ws.id, {
|
|
532
905
|
templateSlug: template,
|
|
533
906
|
...(str(values['display-name']) !== undefined ? { displayName: str(values['display-name']) } : {}),
|
|
@@ -1004,6 +1377,7 @@ export const HANDLERS = {
|
|
|
1004
1377
|
const c = caps(values);
|
|
1005
1378
|
const timeoutMs = waitTimeout(values);
|
|
1006
1379
|
const source = await resolveWorkspace(ctx, ref);
|
|
1380
|
+
requireProcessful(source, 'fork', 'A fork');
|
|
1007
1381
|
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
1382
|
const op = res.operation;
|
|
1009
1383
|
out(ctx, lifecycleText(`Forking ${source.key} into`, res.workspace, op), { operation: op, workspace: res.workspace.data });
|
|
@@ -1154,7 +1528,41 @@ export const HANDLERS = {
|
|
|
1154
1528
|
out(ctx, operationDetail(op), op);
|
|
1155
1529
|
return 0;
|
|
1156
1530
|
},
|
|
1531
|
+
async 'executions get'(ctx, values, [ref, id]) {
|
|
1532
|
+
if (!EXECUTION_ID.test(id))
|
|
1533
|
+
throw new UsageError(`<execution-id> must be ${EXECUTION_ID_TEXT} (got "${id}")`);
|
|
1534
|
+
const waitText = str(values.wait);
|
|
1535
|
+
const waitMs = waitText === undefined ? 0 : parseDuration(waitText, 'wait');
|
|
1536
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
1537
|
+
let r;
|
|
1538
|
+
try {
|
|
1539
|
+
r = await cellOf(ctx, ws).executions.get(id, { waitMs, signal: ctx.signal });
|
|
1540
|
+
}
|
|
1541
|
+
catch (err) {
|
|
1542
|
+
if (ctx.signal.aborted)
|
|
1543
|
+
throw new InterruptedError(`Interrupted; execution ${id} continues server side.`, undefined, { executionId: id, hint: `fetch its result later with: shard executions get ${ref} ${id}` });
|
|
1544
|
+
throw err;
|
|
1545
|
+
}
|
|
1546
|
+
return reportExecution(ctx, ref, r, { summary: true, timings: false, fetched: true, ...(waitText !== undefined ? { waited: waitText } : {}) });
|
|
1547
|
+
},
|
|
1157
1548
|
};
|
|
1549
|
+
/** The update line of `shard version`. */
|
|
1550
|
+
function updateLine(u, d) {
|
|
1551
|
+
const pkg = d.package === '@shardflux/cli' ? '' : ` (${d.package} ${d.version})`;
|
|
1552
|
+
if (!u)
|
|
1553
|
+
return `update check: not done yet${pkg} (shard version --check asks now)\n`;
|
|
1554
|
+
const when = u.checked_at ? `, checked ${u.checked_at.slice(0, 19).replace('T', ' ')}Z` : '';
|
|
1555
|
+
switch (u.status) {
|
|
1556
|
+
case 'current':
|
|
1557
|
+
return `up to date${pkg}: latest ${u.latest ?? '-'}${when}\n`;
|
|
1558
|
+
case 'outdated':
|
|
1559
|
+
return `update available${pkg}: ${u.latest} (you have ${u.current}). Update: ${u.upgrade_command ?? '-'}\n`;
|
|
1560
|
+
case 'unsupported':
|
|
1561
|
+
return `no longer supported${pkg}: the API needs ${u.minimum_supported} or later (latest ${u.latest ?? '-'}). Update: ${u.upgrade_command ?? '-'}\n`;
|
|
1562
|
+
default:
|
|
1563
|
+
return `update status unknown${pkg}${when} (the API could not be asked, or has no release of ${u.package} yet)\n`;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1158
1566
|
/** `--org <id>`: organization scope; null: the API key's project. */
|
|
1159
1567
|
function scopeFlag(values) {
|
|
1160
1568
|
const org = str(values.org);
|
|
@@ -1262,9 +1670,11 @@ async function resolveSecret(ctx, values, ref) {
|
|
|
1262
1670
|
function lifecycleOptions(ctx, values, timeoutMs) {
|
|
1263
1671
|
return values.wait === true ? { wait: { timeoutMs, signal: ctx.signal } } : {};
|
|
1264
1672
|
}
|
|
1265
|
-
async function lifecycle(ctx, values, ref, verb, start) {
|
|
1673
|
+
async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
|
|
1266
1674
|
const timeoutMs = waitTimeout(values);
|
|
1267
1675
|
const ws = await resolveWorkspace(ctx, ref);
|
|
1676
|
+
if (processfulOnly)
|
|
1677
|
+
requireProcessful(ws, processfulOnly[0], processfulOnly[1]);
|
|
1268
1678
|
const op = await waited(ctx, 'the operation continues server side.', () => start(ws, lifecycleOptions(ctx, values, timeoutMs)));
|
|
1269
1679
|
out(ctx, lifecycleText(verb, ws, op), op);
|
|
1270
1680
|
return 0;
|