@shardflux/cli 0.7.0 → 0.8.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 +109 -0
- package/README.md +91 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +50 -5
- package/dist/commands.js +219 -9
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +16 -1
- package/dist/format.d.ts +11 -1
- package/dist/format.js +68 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +57 -0
- package/dist/template-yaml.js +3 -0
- package/dist/timing.d.ts +14 -2
- package/dist/timing.js +39 -5
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
11
|
-
import { EXECUTION_ID, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, newExecutionId } from '@shardflux/sdk';
|
|
11
|
+
import { EXECUTION_ID, ExecStartError, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, ShardfluxProtocolError, newExecutionId } from '@shardflux/sdk';
|
|
12
12
|
import { parse as parseYaml } from 'yaml';
|
|
13
13
|
import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
14
|
-
import { ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
14
|
+
import { ExecSessionRunningError, ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
15
15
|
import { updateCheckDisabled } from "./update-check.js";
|
|
16
16
|
import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
|
|
17
17
|
import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
|
|
18
18
|
import { timingJson } from "./timing.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, durableText, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
19
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
20
20
|
const DEFAULT_WAIT_MS = 300_000;
|
|
21
21
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
22
22
|
const MAX_STDIN = 1024 * 1024;
|
|
@@ -66,6 +66,47 @@ function caps(values) {
|
|
|
66
66
|
c.disk_gib = parsePositiveInt(disk, 'disk-gib', 1_000_000);
|
|
67
67
|
return Object.keys(c).length ? c : undefined;
|
|
68
68
|
}
|
|
69
|
+
const ALLOCATION_MODES = ['fixed', 'elastic'];
|
|
70
|
+
/**
|
|
71
|
+
* The caps of `workspaces open` and `workspaces fork` (0.8.0): the resource caps plus the memory allocation. Given caps
|
|
72
|
+
* replace the stored ones, so they are sent only when a flag was given.
|
|
73
|
+
*/
|
|
74
|
+
function workspaceCaps(values) {
|
|
75
|
+
const c = caps(values) ?? {};
|
|
76
|
+
const mode = oneOf(values, 'allocation-mode', ALLOCATION_MODES);
|
|
77
|
+
const held = str(values['memory-held']);
|
|
78
|
+
if (held !== undefined && mode !== 'elastic')
|
|
79
|
+
throw new UsageError('--memory-held needs --allocation-mode elastic (a fixed workspace holds all of --memory-mib)');
|
|
80
|
+
if (mode !== undefined)
|
|
81
|
+
c.allocation_mode = mode;
|
|
82
|
+
if (held !== undefined)
|
|
83
|
+
c.memory_mib_held = parsePositiveInt(held, 'memory-held', 16_777_216);
|
|
84
|
+
return Object.keys(c).length ? c : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `--burst [--burst-vcpus N] [--burst-memory MIB]` of `workspaces exec` (0.8.0): the exec.run options,
|
|
88
|
+
* or undefined without --burst.
|
|
89
|
+
*/
|
|
90
|
+
function burstOptions(values, withStdin) {
|
|
91
|
+
const vcpus = str(values['burst-vcpus']);
|
|
92
|
+
const mem = str(values['burst-memory']);
|
|
93
|
+
if (values.burst !== true) {
|
|
94
|
+
if (vcpus !== undefined || mem !== undefined)
|
|
95
|
+
throw new UsageError('--burst-vcpus and --burst-memory need --burst');
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (withStdin)
|
|
99
|
+
throw new UsageError('--burst runs the command to completion without stdin; drop --stdin');
|
|
100
|
+
const o = { burst: 'always' };
|
|
101
|
+
if (vcpus !== undefined)
|
|
102
|
+
o.burstVcpus = parsePositiveInt(vcpus, 'burst-vcpus', 32);
|
|
103
|
+
if (mem !== undefined) {
|
|
104
|
+
o.burstMemoryMib = parsePositiveInt(mem, 'burst-memory', 65_536);
|
|
105
|
+
if (o.burstMemoryMib < 512)
|
|
106
|
+
throw new UsageError('--burst-memory must be between 512 and 65536');
|
|
107
|
+
}
|
|
108
|
+
return o;
|
|
109
|
+
}
|
|
69
110
|
/** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
|
|
70
111
|
export function whenIdleSeconds(text) {
|
|
71
112
|
if (/^0+(?:\.0+)?(?:ms|s|m|h)?$/.test(text.trim()))
|
|
@@ -414,6 +455,75 @@ export function execExitCode(r) {
|
|
|
414
455
|
return 128 + r.termSignal;
|
|
415
456
|
return 1;
|
|
416
457
|
}
|
|
458
|
+
/** The longest cancel grace the workspace applies (it waits 5 s without one). */
|
|
459
|
+
const MAX_CANCEL_GRACE_MS = 60_000;
|
|
460
|
+
/** Error codes of a burst's own failure (an output stream's final error event; the session carries it too). */
|
|
461
|
+
const BURST_FAILURES = new Set(['burst_unavailable', 'burst_apply_failed']);
|
|
462
|
+
/**
|
|
463
|
+
* `shard ws exec-read` (0.8.0): an exec session's output from offset 0, and the session. Without `follow`, the output
|
|
464
|
+
* that exists now; with it, until the session ends, resuming from the byte offsets read after a dropped stream.
|
|
465
|
+
* `maxBytes` of each stream are kept for the result (the rest is counted); `onOutput` gets every new chunk.
|
|
466
|
+
*/
|
|
467
|
+
async function readSession(cell, sessionId, opts) {
|
|
468
|
+
let session = await cell.exec.get(sessionId);
|
|
469
|
+
const kept = { stdout: [], stderr: [] };
|
|
470
|
+
const size = { stdout: 0, stderr: 0 };
|
|
471
|
+
let total = { stdout: 0, stderr: 0 };
|
|
472
|
+
let reconnects = 0;
|
|
473
|
+
const live = (s) => s.state === 'starting' || s.state === 'running';
|
|
474
|
+
for (;;) {
|
|
475
|
+
let ended;
|
|
476
|
+
try {
|
|
477
|
+
const events = await cell.exec.output(sessionId, { stdoutOffset: total.stdout, stderrOffset: total.stderr, follow: opts.follow && live(session), signal: opts.signal });
|
|
478
|
+
for await (const ev of events) {
|
|
479
|
+
if (ev.type === 'output' && ev.data !== undefined) {
|
|
480
|
+
const stream = ev.stream === 'stderr' ? 'stderr' : 'stdout';
|
|
481
|
+
const bytes = Buffer.from(ev.data, 'base64');
|
|
482
|
+
const start = ev.offset ?? total[stream];
|
|
483
|
+
// A reconnect may resend bytes already printed.
|
|
484
|
+
const fresh = start < total[stream] ? bytes.subarray(Math.min(total[stream] - start, bytes.length)) : bytes;
|
|
485
|
+
if (fresh.length === 0)
|
|
486
|
+
continue;
|
|
487
|
+
total = { ...total, [stream]: Math.max(total[stream], start + bytes.length) };
|
|
488
|
+
const room = opts.maxBytes - size[stream];
|
|
489
|
+
if (room > 0) {
|
|
490
|
+
kept[stream].push(fresh.subarray(0, room));
|
|
491
|
+
size[stream] += Math.min(room, fresh.length);
|
|
492
|
+
}
|
|
493
|
+
opts.onOutput?.(stream, fresh);
|
|
494
|
+
}
|
|
495
|
+
else if (ev.type === 'exit') {
|
|
496
|
+
ended = ev.session ?? (await cell.exec.get(sessionId));
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
else if (ev.type === 'error' && ev.error && !BURST_FAILURES.has(ev.error.error.code)) {
|
|
500
|
+
throw new ShardfluxApiError(502, ev.error, 'cell');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
if (opts.signal.aborted)
|
|
506
|
+
throw err;
|
|
507
|
+
const transient = !(err instanceof ShardfluxApiError) || err.retryable;
|
|
508
|
+
if (!opts.follow || !transient || reconnects >= 10)
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
if (ended)
|
|
512
|
+
session = ended;
|
|
513
|
+
else if (opts.follow) {
|
|
514
|
+
// The stream ended without the exit (a dropped connection, a restarted gateway): continue from the offsets.
|
|
515
|
+
reconnects += 1;
|
|
516
|
+
if (reconnects > 10)
|
|
517
|
+
throw new ShardfluxProtocolError(`exec ${sessionId}: output stream kept dropping`, 0, 'cell');
|
|
518
|
+
await new Promise((r) => setTimeout(r, Math.min(2_000, 100 * 2 ** reconnects)).unref());
|
|
519
|
+
session = await cell.exec.get(sessionId);
|
|
520
|
+
if (live(session) || total.stdout < session.stdout_size || total.stderr < session.stderr_size)
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const text = (b) => new TextDecoder().decode(Buffer.concat(b));
|
|
524
|
+
return { session, stdout: text(kept.stdout), stderr: text(kept.stderr), truncated: total.stdout > size.stdout || total.stderr > size.stderr, reconnects };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
417
527
|
async function bestEffortCancel(cell, sessionId) {
|
|
418
528
|
await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
|
|
419
529
|
}
|
|
@@ -496,7 +606,7 @@ function reportExecution(ctx, ref, r, opts) {
|
|
|
496
606
|
}
|
|
497
607
|
function lifecycleText(verb, w, op) {
|
|
498
608
|
const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
|
|
499
|
-
return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
|
|
609
|
+
return `${verb} ${w.key !== undefined ? `${w.key} (${w.id})` : w.id}: operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
|
|
500
610
|
}
|
|
501
611
|
/** The project-key commands; handlers.ts adds the account plane's. */
|
|
502
612
|
export const KEY_HANDLERS = {
|
|
@@ -590,10 +700,78 @@ export const KEY_HANDLERS = {
|
|
|
590
700
|
return 0;
|
|
591
701
|
},
|
|
592
702
|
async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
|
|
703
|
+
const timeout = str(values.timeout);
|
|
704
|
+
const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
|
|
593
705
|
const ws = await resolveWorkspace(ctx, ref);
|
|
594
706
|
const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
|
|
595
|
-
...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {})
|
|
707
|
+
...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}),
|
|
708
|
+
...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}) }, ctx.signal);
|
|
596
709
|
out(ctx, JSON.stringify(session, null, 2) + '\n', session);
|
|
710
|
+
// 0.8.0: what to do next (stderr, so stdout stays the session's JSON).
|
|
711
|
+
if (!ctx.json && session.state !== 'failed_to_start')
|
|
712
|
+
ctx.io.stderr.write(`read it with: shard ws exec-read ${ref} ${session.session_id} --follow; stop it with: shard ws exec-cancel ${ref} ${session.session_id}\n`);
|
|
713
|
+
return 0;
|
|
714
|
+
},
|
|
715
|
+
async 'workspaces exec-read'(ctx, values, [ref, sessionId]) {
|
|
716
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
717
|
+
const cell = cellOf(ctx, ws);
|
|
718
|
+
const follow = values.follow === true;
|
|
719
|
+
let r;
|
|
720
|
+
try {
|
|
721
|
+
r = await readSession(cell, sessionId, {
|
|
722
|
+
follow,
|
|
723
|
+
signal: ctx.signal,
|
|
724
|
+
maxBytes: ctx.json ? MAX_JSON_OUTPUT : 0,
|
|
725
|
+
...(ctx.json ? {} : { onOutput: (stream, chunk) => void (stream === 'stderr' ? ctx.io.stderr : ctx.io.stdout).write(chunk) }),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
if (ctx.signal.aborted && follow) {
|
|
730
|
+
throw new InterruptedError(`Stopped following exec session ${sessionId}; the command keeps running.`, undefined, { hint: `stop it with: shard ws exec-cancel ${ref} ${sessionId}` });
|
|
731
|
+
}
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
734
|
+
const s = r.session;
|
|
735
|
+
// Nothing ran (a cwd that is not a directory, a program not on PATH): exit 6 with the workspace's reason, as ws exec.
|
|
736
|
+
if (s.burst?.error)
|
|
737
|
+
throw new ShardfluxApiError(409, { error: { code: s.burst.error.code, message: s.burst.error.message, request_id: '', retryable: s.burst.error.retryable, ...(s.burst.error.details ? { details: s.burst.error.details } : {}) } }, 'cell');
|
|
738
|
+
if (s.state === 'failed_to_start')
|
|
739
|
+
throw new ExecStartError(s);
|
|
740
|
+
if (ctx.json) {
|
|
741
|
+
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
742
|
+
session_id: s.session_id,
|
|
743
|
+
state: s.state,
|
|
744
|
+
exit_code: s.exit_code ?? null,
|
|
745
|
+
term_signal: s.term_signal ?? null,
|
|
746
|
+
timed_out: s.timed_out ?? false,
|
|
747
|
+
canceled: s.canceled ?? false,
|
|
748
|
+
stdout: r.stdout,
|
|
749
|
+
stderr: r.stderr,
|
|
750
|
+
truncated: r.truncated,
|
|
751
|
+
reconnects: r.reconnects,
|
|
752
|
+
...(s.error ? { error: s.error } : {}),
|
|
753
|
+
...(s.burst ? { burst: s.burst } : {}),
|
|
754
|
+
})));
|
|
755
|
+
}
|
|
756
|
+
if (s.state === 'starting' || s.state === 'running') {
|
|
757
|
+
throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
|
|
758
|
+
}
|
|
759
|
+
const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false };
|
|
760
|
+
if (!ctx.json && result.exitCode === null && !result.timedOut && !(result.termSignal !== null && result.termSignal > 0)) {
|
|
761
|
+
ctx.io.stderr.write(`exec session ${s.session_id} ended ${s.state} without an exit code${s.error ? `: ${s.error}` : ''}\n`);
|
|
762
|
+
}
|
|
763
|
+
return execExitCode(result);
|
|
764
|
+
},
|
|
765
|
+
async 'workspaces exec-cancel'(ctx, values, [ref, sessionId]) {
|
|
766
|
+
const grace = str(values.grace);
|
|
767
|
+
const graceMs = grace === undefined ? undefined : parseDuration(grace, 'grace');
|
|
768
|
+
if (graceMs !== undefined && graceMs > MAX_CANCEL_GRACE_MS)
|
|
769
|
+
throw new UsageError(`--grace must be at most 60s (got "${grace}")`);
|
|
770
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
771
|
+
const s = await cellOf(ctx, ws).exec.cancel(sessionId, graceMs);
|
|
772
|
+
const ended = s.state !== 'starting' && s.state !== 'running';
|
|
773
|
+
const status = !ended ? `${s.state} (cancel sent)` : s.canceled ? `canceled${s.term_signal ? ` (signal ${s.term_signal})` : ''}` : `already ${s.state}${typeof s.exit_code === 'number' ? ` (exit code ${s.exit_code})` : ''}`;
|
|
774
|
+
out(ctx, `exec session ${s.session_id}: ${status}\n`, s);
|
|
597
775
|
return 0;
|
|
598
776
|
},
|
|
599
777
|
async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
|
|
@@ -609,7 +787,7 @@ export const KEY_HANDLERS = {
|
|
|
609
787
|
const template = str(values.template);
|
|
610
788
|
if (!template)
|
|
611
789
|
throw new UsageError('--template <slug> is required for "shard workspaces open"');
|
|
612
|
-
const c =
|
|
790
|
+
const c = workspaceCaps(values);
|
|
613
791
|
const bind = secretNames(strs(values.secret), '--secret');
|
|
614
792
|
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
615
793
|
const mode = modeFlag(values);
|
|
@@ -699,11 +877,14 @@ export const KEY_HANDLERS = {
|
|
|
699
877
|
const limitText = str(values['output-limit']);
|
|
700
878
|
const outputLimit = limitText === undefined ? undefined : parsePositiveInt(limitText, 'output-limit', MAX_OUTPUT_LIMIT);
|
|
701
879
|
const stdinSource = str(values.stdin);
|
|
880
|
+
const burst = burstOptions(values, stdinSource !== undefined);
|
|
702
881
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
703
882
|
const ws = await resolveWorkspace(ctx, ref);
|
|
704
883
|
const cell = cellOf(ctx, ws);
|
|
705
884
|
const cwd = str(values.cwd);
|
|
706
885
|
if (ws.mode === 'file_first') {
|
|
886
|
+
if (burst)
|
|
887
|
+
throw new UsageError(`--burst applies to processful workspaces only; ${ws.key} is ${modeText(ws.mode)}`);
|
|
707
888
|
// An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
708
889
|
const id = executionId ?? newExecutionId();
|
|
709
890
|
let r;
|
|
@@ -739,6 +920,7 @@ export const KEY_HANDLERS = {
|
|
|
739
920
|
...(Object.keys(env).length ? { env } : {}),
|
|
740
921
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
741
922
|
...(stdin !== undefined ? { stdin } : {}),
|
|
923
|
+
...(burst ?? {}),
|
|
742
924
|
maxOutputBytes: ctx.json ? MAX_JSON_OUTPUT : 0,
|
|
743
925
|
...(ctx.json ? {} : { onOutput: (stream, chunk) => void (stream === 'stderr' ? ctx.io.stderr : ctx.io.stdout).write(chunk) }),
|
|
744
926
|
signal: ctx.signal,
|
|
@@ -746,9 +928,14 @@ export const KEY_HANDLERS = {
|
|
|
746
928
|
}
|
|
747
929
|
catch (err) {
|
|
748
930
|
if (ctx.signal.aborted) {
|
|
931
|
+
if (burst)
|
|
932
|
+
throw new InterruptedError(`Interrupted; burst exec session ${sessionId} keeps running server side (a burst cannot be canceled).`);
|
|
749
933
|
await bestEffortCancel(cell, sessionId);
|
|
750
934
|
throw new InterruptedError(`Interrupted; sent cancel to exec session ${sessionId}.`);
|
|
751
935
|
}
|
|
936
|
+
// A burst's apply that did not complete is finished by a retry of the same session (SDK exec.run / the API).
|
|
937
|
+
if (burst && err instanceof ShardfluxApiError && err.code === 'burst_apply_failed')
|
|
938
|
+
ctx.io.stderr.write(`burst exec session ${sessionId}\n`);
|
|
752
939
|
throw err;
|
|
753
940
|
}
|
|
754
941
|
const code = execExitCode(r);
|
|
@@ -757,7 +944,14 @@ export const KEY_HANDLERS = {
|
|
|
757
944
|
const unexplained = r.exitCode === null && !r.timedOut && !(r.termSignal !== null && r.termSignal > 0);
|
|
758
945
|
if (values.verbose === true && !ctx.json) {
|
|
759
946
|
const status = r.timedOut ? 'timed out' : r.canceled ? 'canceled' : r.termSignal ? `killed by signal ${r.termSignal}` : unexplained ? r.session.state : `exit code ${r.exitCode ?? '-'}`;
|
|
760
|
-
ctx.io.stderr.write(kv([
|
|
947
|
+
ctx.io.stderr.write(kv([
|
|
948
|
+
['session', r.sessionId],
|
|
949
|
+
['status', status],
|
|
950
|
+
['output', `${r.stdoutBytes} bytes stdout, ${r.stderrBytes} bytes stderr`],
|
|
951
|
+
['reconnects', String(r.reconnects)],
|
|
952
|
+
...(r.memoryGrow ? [['memory grow', memoryGrowText(r.memoryGrow)]] : []),
|
|
953
|
+
...(r.burst ? [['burst', burstText(r.burst)]] : []),
|
|
954
|
+
]));
|
|
761
955
|
}
|
|
762
956
|
if (ctx.json) {
|
|
763
957
|
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
@@ -772,6 +966,8 @@ export const KEY_HANDLERS = {
|
|
|
772
966
|
truncated: r.truncated,
|
|
773
967
|
reconnects: r.reconnects,
|
|
774
968
|
...(r.session.error ? { error: r.session.error } : {}),
|
|
969
|
+
...(r.memoryGrow ? { memory_grow: r.memoryGrow } : {}),
|
|
970
|
+
...(r.burst ? { burst: r.burst } : {}),
|
|
775
971
|
})));
|
|
776
972
|
}
|
|
777
973
|
else if (unexplained) {
|
|
@@ -899,7 +1095,21 @@ export const KEY_HANDLERS = {
|
|
|
899
1095
|
const cancel = values['cancel-when-idle'] === true;
|
|
900
1096
|
const durable = values.durable === true;
|
|
901
1097
|
if (whenIdle === undefined && !cancel) {
|
|
902
|
-
|
|
1098
|
+
const suspend = (id, o) => ctx.cloud().workspaces.suspend(id, durable ? { ...o, durable: true, wait: o.wait ?? { timeoutMs: waitTimeout(values), signal: ctx.signal } } : o);
|
|
1099
|
+
if (UUID.test(ref)) {
|
|
1100
|
+
// An id needs no resolve request: the suspend goes straight to it (the API refuses a file-first workspace
|
|
1101
|
+
// itself). Only a UUID that is no workspace id here (404, nothing started) is looked up as a key.
|
|
1102
|
+
try {
|
|
1103
|
+
const op = await waited(ctx, 'the operation continues server side.', () => suspend(ref, lifecycleOptions(ctx, values, waitTimeout(values))));
|
|
1104
|
+
out(ctx, lifecycleText('Suspending', { id: ref }, op), op);
|
|
1105
|
+
return 0;
|
|
1106
|
+
}
|
|
1107
|
+
catch (err) {
|
|
1108
|
+
if (!(err instanceof ShardfluxApiError && err.status === 404 && err.operationId === undefined))
|
|
1109
|
+
throw err;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => suspend(ws.id, o), ['suspend', 'Suspend']);
|
|
903
1113
|
}
|
|
904
1114
|
if (durable)
|
|
905
1115
|
throw new UsageError(`${whenIdle !== undefined ? '--when-idle' : '--cancel-when-idle'} cannot be combined with --durable (nothing is suspended now)`);
|
|
@@ -1423,7 +1633,7 @@ export const KEY_HANDLERS = {
|
|
|
1423
1633
|
return 0;
|
|
1424
1634
|
},
|
|
1425
1635
|
async 'workspaces fork'(ctx, values, [ref, newKey]) {
|
|
1426
|
-
const c =
|
|
1636
|
+
const c = workspaceCaps(values);
|
|
1427
1637
|
const timeoutMs = waitTimeout(values);
|
|
1428
1638
|
const source = await resolveWorkspace(ctx, ref);
|
|
1429
1639
|
requireProcessful(source, 'fork', 'A fork');
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckoutStatus, ExecutionResult } from '@shardflux/sdk';
|
|
1
|
+
import type { CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
2
2
|
export declare const EXIT: {
|
|
3
3
|
readonly ok: 0;
|
|
4
4
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -76,6 +76,15 @@ export declare class ExecutionPendingError extends Error {
|
|
|
76
76
|
readonly hint: string;
|
|
77
77
|
constructor(message: string, result: ExecutionResult, hint: string);
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `shard ws exec-read` without --follow (0.8.0): the background command still runs (exit 5, as a file-first execution
|
|
81
|
+
* that still runs); `hint` names the command that follows it to its end.
|
|
82
|
+
*/
|
|
83
|
+
export declare class ExecSessionRunningError extends HintedError {
|
|
84
|
+
name: string;
|
|
85
|
+
readonly session: ExecSession;
|
|
86
|
+
constructor(message: string, session: ExecSession, hint: string);
|
|
87
|
+
}
|
|
79
88
|
export interface ErrorInfo {
|
|
80
89
|
code: string;
|
|
81
90
|
message: string;
|
|
@@ -89,6 +98,8 @@ export interface ErrorInfo {
|
|
|
89
98
|
operation?: unknown;
|
|
90
99
|
/** A file-first execution the error is about (failed, lost, still running, or left running by Ctrl-C). */
|
|
91
100
|
execution_id?: string;
|
|
101
|
+
/** The refusal's Retry-After in seconds (0.9.0+), e.g. 429 quota_exceeded from the cell gateway. */
|
|
102
|
+
retry_after_seconds?: number;
|
|
92
103
|
}
|
|
93
104
|
/**
|
|
94
105
|
* Removes anything shaped like a Shardflux API key or user session token from text, and each of the given secrets
|
package/dist/errors.js
CHANGED
|
@@ -97,6 +97,18 @@ export class ExecutionPendingError extends Error {
|
|
|
97
97
|
this.hint = hint;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* `shard ws exec-read` without --follow (0.8.0): the background command still runs (exit 5, as a file-first execution
|
|
102
|
+
* that still runs); `hint` names the command that follows it to its end.
|
|
103
|
+
*/
|
|
104
|
+
export class ExecSessionRunningError extends HintedError {
|
|
105
|
+
name = 'ExecSessionRunningError';
|
|
106
|
+
session;
|
|
107
|
+
constructor(message, session, hint) {
|
|
108
|
+
super(message, hint);
|
|
109
|
+
this.session = session;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
100
112
|
const AUTH_CODES = new Set(['unauthenticated', 'invalid_credentials', 'token_invalid', 'forbidden', 'mfa_required', 'email_unverified', 'step_up_required', 'csrf_failed']);
|
|
101
113
|
const KEY_PATTERN = /sfk_[A-Za-z0-9]{8,}_[A-Za-z0-9]+/g;
|
|
102
114
|
/** User session tokens (`sfu_` + 43 base64url characters), also cut short. */
|
|
@@ -135,6 +147,7 @@ export function describeError(err) {
|
|
|
135
147
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
136
148
|
...(err.details ? { details: err.details } : {}),
|
|
137
149
|
source: err.source,
|
|
150
|
+
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
138
151
|
};
|
|
139
152
|
}
|
|
140
153
|
if (err instanceof OperationTimeoutError) {
|
|
@@ -163,6 +176,8 @@ export function describeError(err) {
|
|
|
163
176
|
}
|
|
164
177
|
if (err instanceof ExecutionPendingError)
|
|
165
178
|
return { code: 'timeout', message: err.message, execution_id: err.result.executionId, last_state: err.result.state };
|
|
179
|
+
if (err instanceof ExecSessionRunningError)
|
|
180
|
+
return { code: 'timeout', message: err.message, last_state: err.session.state, details: { session_id: err.session.session_id } };
|
|
166
181
|
if (err instanceof UsageError)
|
|
167
182
|
return { code: 'usage', message: err.message };
|
|
168
183
|
if (err instanceof CheckoutTimeoutError) {
|
|
@@ -214,7 +229,7 @@ export function exitCodeFor(err, interrupted = false) {
|
|
|
214
229
|
return EXIT.timeout;
|
|
215
230
|
if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
|
|
216
231
|
return EXIT.operationFailed;
|
|
217
|
-
if (err instanceof ExecutionPendingError)
|
|
232
|
+
if (err instanceof ExecutionPendingError || err instanceof ExecSessionRunningError)
|
|
218
233
|
return EXIT.timeout;
|
|
219
234
|
// A command that could not start exits 6 in both modes, as a failed execution does (0.5.1).
|
|
220
235
|
if (err instanceof ExecStartError)
|
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
1
|
+
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary } from '@shardflux/sdk';
|
|
2
2
|
import type { ErrorInfo } from './errors.js';
|
|
3
|
+
/** A size for people: `512 B`, `10.0 KiB`, `128.0 MiB`, `1.50 GiB`. */
|
|
4
|
+
export declare function byteSize(n: number): string;
|
|
3
5
|
export declare function json(value: unknown): string;
|
|
4
6
|
export declare function table(headers: readonly string[], rows: readonly (readonly string[])[]): string;
|
|
5
7
|
export declare function kv(pairs: readonly (readonly [string, string])[]): string;
|
|
@@ -33,6 +35,12 @@ export declare function suspendWhenIdleText(w: {
|
|
|
33
35
|
suspend_request: SuspendRequest | null;
|
|
34
36
|
operation: Operation | null;
|
|
35
37
|
}): string;
|
|
38
|
+
/** `fixed 4096 MiB`, or `elastic: promised 8192 MiB, held 1024 MiB, plugged +2048 MiB` (plugged: above the held floor now). */
|
|
39
|
+
export declare function memoryText(m: WorkspaceMemory): string;
|
|
40
|
+
/** The grow an exec's start waited for: `delivered 1024 -> 4096 MiB in 175 ms`, `partial 1024 -> 3072 of 4096 MiB in 175 ms`. */
|
|
41
|
+
/** The burst line of `ws exec -v` (0.8.0). */
|
|
42
|
+
export declare function burstText(b: BurstSummary): string;
|
|
43
|
+
export declare function memoryGrowText(g: MemoryGrow): string;
|
|
36
44
|
export declare function workspaceDetail(w: WorkspaceView): string;
|
|
37
45
|
export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
|
|
38
46
|
export declare function operationTable(ops: readonly Operation[]): string;
|
|
@@ -90,6 +98,8 @@ export declare function languageTable(l: TemplateLanguages): string;
|
|
|
90
98
|
export declare function packageTable(page: TemplatePackagePage): string;
|
|
91
99
|
/** A template: identity, what open resolves to, its versions, and the open version's settings. */
|
|
92
100
|
export declare function templateDetailText(t: TemplateDetail): string;
|
|
101
|
+
/** A version's immutable paths and the size of their image: `/opt/acme, /srv/tools (128.0 MiB)`, or `-`. */
|
|
102
|
+
export declare function immutablePathsText(i: TemplateVersion['immutable'] | undefined): string;
|
|
93
103
|
/** A version's settings, one line per part. */
|
|
94
104
|
export declare function settingsText(s: TemplateSettings): string;
|
|
95
105
|
/** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
|
package/dist/format.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* these functions only shape what people read.
|
|
5
5
|
*/
|
|
6
6
|
import { durabilityOf, isDurable } from '@shardflux/sdk';
|
|
7
|
+
/** A size for people: `512 B`, `10.0 KiB`, `128.0 MiB`, `1.50 GiB`. */
|
|
8
|
+
export function byteSize(n) {
|
|
9
|
+
return n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(2)} GiB`;
|
|
10
|
+
}
|
|
7
11
|
export function json(value) {
|
|
8
12
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
9
13
|
}
|
|
@@ -128,6 +132,32 @@ export function suspendWhenIdleText(w, r) {
|
|
|
128
132
|
const op = r.operation;
|
|
129
133
|
return `A suspend of ${w.key} (${w.id}) is already in progress (operation ${op.id} ${op.kind} ${op.state}); nothing was scheduled.\nwait with: shard operations wait ${op.id}\n`;
|
|
130
134
|
}
|
|
135
|
+
const mib = (n) => (n === null ? '-' : `${n} MiB`);
|
|
136
|
+
/** `fixed 4096 MiB`, or `elastic: promised 8192 MiB, held 1024 MiB, plugged +2048 MiB` (plugged: above the held floor now). */
|
|
137
|
+
export function memoryText(m) {
|
|
138
|
+
if (m.allocation_mode !== 'elastic')
|
|
139
|
+
return `fixed ${mib(m.promised_mib)}`;
|
|
140
|
+
return `elastic: promised ${mib(m.promised_mib)}, held ${mib(m.held_mib)}, plugged ${m.plugged_mib === null ? '- (not running)' : `+${m.plugged_mib} MiB`}`;
|
|
141
|
+
}
|
|
142
|
+
/** The grow an exec's start waited for: `delivered 1024 -> 4096 MiB in 175 ms`, `partial 1024 -> 3072 of 4096 MiB in 175 ms`. */
|
|
143
|
+
/** The burst line of `ws exec -v` (0.8.0). */
|
|
144
|
+
export function burstText(b) {
|
|
145
|
+
const size = [b.vcpus !== undefined ? `${b.vcpus} vCPU` : null, b.memory_mib !== undefined ? `${b.memory_mib} MiB` : null].filter(Boolean).join(' / ');
|
|
146
|
+
const head = `${b.host}${size ? ` ${size}` : ''}${b.method ? ` (${b.method})` : ''}`;
|
|
147
|
+
const changes = b.applied
|
|
148
|
+
? `applied ${b.written_files ?? 0} files, ${b.written_dirs ?? 0} dirs, ${b.removed ?? 0} removed, ${b.written_bytes ?? 0} bytes`
|
|
149
|
+
: 'changes not applied';
|
|
150
|
+
const extra = [
|
|
151
|
+
b.leftover_killed ? `${b.leftover_killed} leftover processes killed` : null,
|
|
152
|
+
b.overhead_ms !== undefined ? `overhead ${b.overhead_ms} ms` : null,
|
|
153
|
+
b.replayed ? 'replayed' : null,
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
return [head, changes, ...extra].join('; ');
|
|
156
|
+
}
|
|
157
|
+
export function memoryGrowText(g) {
|
|
158
|
+
const to = g.got_mib === g.want_mib ? `${g.got_mib} MiB` : `${g.got_mib} of ${g.want_mib} MiB`;
|
|
159
|
+
return `${g.outcome} ${g.from_mib} -> ${to} in ${g.deliver_ms} ms`;
|
|
160
|
+
}
|
|
131
161
|
export function workspaceDetail(w) {
|
|
132
162
|
const c = w.ceilings;
|
|
133
163
|
const startup = startupText(w.startup);
|
|
@@ -138,6 +168,8 @@ export function workspaceDetail(w) {
|
|
|
138
168
|
['state', `${w.observed_state} (desired ${w.desired_state})`],
|
|
139
169
|
['ready', isReady(w) ? 'yes' : 'no'],
|
|
140
170
|
['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
|
|
171
|
+
// Immutable paths (0.8.0): the template version whose immutable paths the workspace has mounted, when it has one.
|
|
172
|
+
...(typeof w.template.immutable_version === 'number' ? [['immutable paths', `v${w.template.immutable_version} (mounted read-only)`]] : []),
|
|
141
173
|
// The mode (0.5.0) when the API reports it; a file-first workspace's state is its tree revision.
|
|
142
174
|
...(w.mode !== undefined ? [['mode', modeText(w.mode)]] : []),
|
|
143
175
|
...(w.mode === 'file_first' ? [['tree revision', String(w.tree_revision)]] : []),
|
|
@@ -145,6 +177,8 @@ export function workspaceDetail(w) {
|
|
|
145
177
|
['ceilings', c ? `cpu ${c.cpu_millis}m (${c.sources.cpu_millis}), memory ${c.memory_mib} MiB (${c.sources.memory_mib}), disk ${c.disk_gib} GiB (${c.sources.disk_gib})` : '-'],
|
|
146
178
|
['grants', pairs(w.grants)],
|
|
147
179
|
['caps', pairs(w.caps)],
|
|
180
|
+
// Elastic memory (0.8.0) when the API reports it.
|
|
181
|
+
...(w.memory ? [['memory', memoryText(w.memory)]] : []),
|
|
148
182
|
['operation', w.active_operation ? `${operationSummary(w.active_operation)} ${w.active_operation.id}` : '-'],
|
|
149
183
|
['pending', w.pending_reason ?? '-'],
|
|
150
184
|
...(suspendRequest ? [['suspend when idle', suspendRequestText(suspendRequest)]] : []),
|
|
@@ -362,6 +396,10 @@ export function usageText(summary) {
|
|
|
362
396
|
const trialEnded = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
363
397
|
head.push(['allowance exhausted', s.allowance_exhausted ? (trialEnded ? `yes (${reason}the trial ends; the organization is back on free)` : `yes (${reason}new opens/resumes are refused)`) : 'no']);
|
|
364
398
|
}
|
|
399
|
+
// cap_state storage_blocked (0.9.0): the Retained state allowance is used up; existing workspaces keep working.
|
|
400
|
+
if (Array.isArray(s.allowances) && s.allowances.some((a) => isObject(a) && a.cap_state === 'storage_blocked')) {
|
|
401
|
+
head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
|
|
402
|
+
}
|
|
365
403
|
for (const [k, v] of Object.entries(s))
|
|
366
404
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
367
405
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
|
@@ -509,15 +547,30 @@ function buildSource(b) {
|
|
|
509
547
|
const v2 = b.provenance.recipe_schema === 'shardflux.template-recipe.v2';
|
|
510
548
|
return `recipe${v2 ? ' v2' : ''}${b.provenance.recipe_sha256 ? ` (sha256 ${b.provenance.recipe_sha256.slice(0, 12)})` : ''}`;
|
|
511
549
|
}
|
|
550
|
+
/** What to do about a build that failed on its immutable paths (0.8.0; contracts §34.2), from failure.code and details. */
|
|
551
|
+
function immutableFailureFix(f) {
|
|
552
|
+
if (f.code === 'immutable_path_missing') {
|
|
553
|
+
const path = typeof f.details?.path === 'string' ? f.details.path : 'the path';
|
|
554
|
+
return `make ${path} a directory in the build (a step: mkdir -p ${path}), then build again`;
|
|
555
|
+
}
|
|
556
|
+
if (f.code === 'immutable_image_too_large')
|
|
557
|
+
return 'keep large data outside the immutable paths, then build again';
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
512
560
|
export function buildText(b) {
|
|
513
561
|
const settled = b.state === 'published' || b.state === 'failed' || b.state === 'canceled' || b.state === 'succeeded';
|
|
514
562
|
const denied = b.denied_hosts ?? [];
|
|
563
|
+
const immutable = b.immutable_paths ?? [];
|
|
564
|
+
const fix = b.failure ? immutableFailureFix(b.failure) : null;
|
|
515
565
|
return (kv([
|
|
516
566
|
['build', b.id],
|
|
517
567
|
['template', `${b.template.slug} v${b.target_version ?? '?'}`],
|
|
518
568
|
['state', `${b.state}${b.failure ? ` (${b.failure.code}: ${b.failure.message})` : ''}`],
|
|
569
|
+
...(fix ? [['fix', fix]] : []),
|
|
519
570
|
['registration', `${b.registration.state}${b.registration.error ? ` (${b.registration.error.code}: ${b.registration.error.message})` : ''}`],
|
|
520
571
|
['source', buildSource(b)],
|
|
572
|
+
// Immutable paths (0.8.0): the list the produced version declares.
|
|
573
|
+
...(immutable.length ? [['immutable paths', immutable.join(', ')]] : []),
|
|
521
574
|
['network', b.network.mode === 'none' ? 'none' : `allowlist ${b.network.allow_hosts.join(', ') || '(empty)'}`],
|
|
522
575
|
...(denied.length ? [['denied hosts', `${denied.join(', ')} (allow one: add it to build.network.extra_hosts and build again)`]] : []),
|
|
523
576
|
['version', b.template_version ? `${b.template_version.id}${b.template_version.published ? ' (published)' : ' (unpublished)'}` : '-'],
|
|
@@ -543,11 +596,25 @@ export function templateDetailText(t) {
|
|
|
543
596
|
['draft', t.draft ? `${t.draft.workspace_key} (${t.draft.observed_state})` : '-'],
|
|
544
597
|
['archived', time(t.archived_at)],
|
|
545
598
|
]);
|
|
599
|
+
// Immutable paths (0.8.0): the column appears when a version declares any.
|
|
600
|
+
const immutable = t.versions.some((v) => v.immutable);
|
|
546
601
|
const versions = t.versions.length
|
|
547
|
-
? table(['VERSION', 'STATE', 'SOURCE', 'LAYOUTS',
|
|
602
|
+
? table(['VERSION', 'STATE', 'SOURCE', 'LAYOUTS', ...(immutable ? ['IMMUTABLE PATHS'] : []), 'CREATED', 'DESCRIPTION'], t.versions.map((v) => [
|
|
603
|
+
`v${v.version}${v.is_open_version ? ' *' : ''}`,
|
|
604
|
+
v.state,
|
|
605
|
+
v.source?.kind ?? '-',
|
|
606
|
+
v.disk_layouts.join(',') || '-',
|
|
607
|
+
...(immutable ? [immutablePathsText(v.immutable)] : []),
|
|
608
|
+
time(v.created_at),
|
|
609
|
+
v.description ?? '-',
|
|
610
|
+
]))
|
|
548
611
|
: 'No versions.\n';
|
|
549
612
|
return `${head}\n${versions}${open?.settings ? `\nsettings of v${open.version}:\n${settingsText(open.settings)}` : ''}`;
|
|
550
613
|
}
|
|
614
|
+
/** A version's immutable paths and the size of their image: `/opt/acme, /srv/tools (128.0 MiB)`, or `-`. */
|
|
615
|
+
export function immutablePathsText(i) {
|
|
616
|
+
return i ? `${i.paths.join(', ')} (${byteSize(i.bytes)})` : '-';
|
|
617
|
+
}
|
|
551
618
|
/** A version's settings, one line per part. */
|
|
552
619
|
export function settingsText(s) {
|
|
553
620
|
const inputs = Object.entries(s.inputs).map(([n, i]) => `${n} (${i.kind}${i.required ? ', required' : ''}${i.default !== null ? `, default ${JSON.stringify(i.default)}` : ''})`);
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,4 @@ export { openUrl, openerFor } from './browser.js';
|
|
|
22
22
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
|
|
23
23
|
export type { ErrorInfo } from './errors.js';
|
|
24
24
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
25
|
-
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
|
|
25
|
+
export { TimingRecorder, coldBootNotice, hostLostNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,4 @@ export { CHECK_INTERVAL_MS, CLI_DISTRIBUTION, NOTICE_WAIT_MS, UPDATE_CHECK_FILE,
|
|
|
14
14
|
export { openUrl, openerFor } from "./browser.js";
|
|
15
15
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
16
16
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
17
|
-
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
|
|
17
|
+
export { TimingRecorder, coldBootNotice, hostLostNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
|