@shardflux/cli 0.8.0 → 0.9.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 +104 -1
- package/README.md +99 -29
- package/dist/account-args.js +32 -4
- package/dist/account-commands.js +234 -111
- package/dist/args.d.ts +1 -1
- package/dist/args.js +80 -4
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +144 -3
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -0
- package/dist/format.d.ts +21 -1
- package/dist/format.js +65 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +31 -0
- package/dist/timing.d.ts +14 -2
- package/dist/timing.js +37 -4
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -16,7 +16,7 @@ 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, 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";
|
|
19
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, pairs, resizeJson, resizeText, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, exposedPortText, portTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, time, 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;
|
|
@@ -67,6 +67,35 @@ function caps(values) {
|
|
|
67
67
|
return Object.keys(c).length ? c : undefined;
|
|
68
68
|
}
|
|
69
69
|
const ALLOCATION_MODES = ['fixed', 'elastic'];
|
|
70
|
+
/** `--resource-hint` of `workspaces exec` / `exec-start` (0.9.0): the exec's memory hint. */
|
|
71
|
+
const RESOURCE_HINTS = ['auto', 'light', 'heavy'];
|
|
72
|
+
/** The resources of `workspaces resize` (0.9.0): at least one, validated before any request. */
|
|
73
|
+
function resizeParams(values) {
|
|
74
|
+
const p = {};
|
|
75
|
+
const memory = str(values.memory);
|
|
76
|
+
const held = str(values.held);
|
|
77
|
+
const cpu = str(values.cpu);
|
|
78
|
+
const disk = str(values.disk);
|
|
79
|
+
if (values.fixed === true && values.elastic === true)
|
|
80
|
+
throw new UsageError('--fixed and --elastic cannot be combined');
|
|
81
|
+
if (held !== undefined && values.fixed === true)
|
|
82
|
+
throw new UsageError('--held applies to elastic memory (a fixed workspace holds all of its memory)');
|
|
83
|
+
if (memory !== undefined)
|
|
84
|
+
p.memoryMib = parsePositiveInt(memory, 'memory', 16_777_216);
|
|
85
|
+
if (held !== undefined)
|
|
86
|
+
p.memoryMibHeld = parsePositiveInt(held, 'held', 16_777_216);
|
|
87
|
+
if (values.fixed === true)
|
|
88
|
+
p.allocationMode = 'fixed';
|
|
89
|
+
if (values.elastic === true)
|
|
90
|
+
p.allocationMode = 'elastic';
|
|
91
|
+
if (cpu !== undefined)
|
|
92
|
+
p.cpuMillis = parsePositiveInt(cpu, 'cpu', 1_000_000);
|
|
93
|
+
if (disk !== undefined)
|
|
94
|
+
p.diskGib = parsePositiveInt(disk, 'disk', 1_000_000);
|
|
95
|
+
if (Object.keys(p).length === 0)
|
|
96
|
+
throw new UsageError('nothing to resize: give --memory, --held, --fixed, --elastic, --cpu or --disk');
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
70
99
|
/**
|
|
71
100
|
* The caps of `workspaces open` and `workspaces fork` (0.8.0): the resource caps plus the memory allocation. Given caps
|
|
72
101
|
* replace the stored ones, so they are sent only when a flag was given.
|
|
@@ -75,8 +104,9 @@ function workspaceCaps(values) {
|
|
|
75
104
|
const c = caps(values) ?? {};
|
|
76
105
|
const mode = oneOf(values, 'allocation-mode', ALLOCATION_MODES);
|
|
77
106
|
const held = str(values['memory-held']);
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
// Without --allocation-mode the API decides whether the workspace is elastic (requires_elastic when it is not).
|
|
108
|
+
if (held !== undefined && mode === 'fixed')
|
|
109
|
+
throw new UsageError('--memory-held applies to elastic memory (a fixed workspace holds all of --memory-mib)');
|
|
80
110
|
if (mode !== undefined)
|
|
81
111
|
c.allocation_mode = mode;
|
|
82
112
|
if (held !== undefined)
|
|
@@ -566,6 +596,35 @@ function treeRevisionAfter(ws, cell) {
|
|
|
566
596
|
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
|
567
597
|
* with its hint like the API's own refusal.
|
|
568
598
|
*/
|
|
599
|
+
/** `<port>` of the port commands: a TCP port number, 1-65535. */
|
|
600
|
+
export function portArg(text) {
|
|
601
|
+
if (!/^\d{1,5}$/.test(text) || Number(text) < 1 || Number(text) > 65_535)
|
|
602
|
+
throw new UsageError(`<port> must be a TCP port number between 1 and 65535 (got "${text}")`);
|
|
603
|
+
return Number(text);
|
|
604
|
+
}
|
|
605
|
+
const PORT_TOKEN_TTL = { option: 'ttl', min: 60, max: 86_400, range: '1m and 24h' };
|
|
606
|
+
const PORT_LINK_TTL = { option: 'ttl', min: 60, max: 604_800, range: '1m and 7d' };
|
|
607
|
+
/** `--ttl <duration>` of port tokens and links: whole seconds within the API's bounds (undefined: the API's default). */
|
|
608
|
+
export function ttlSeconds(values, b) {
|
|
609
|
+
const text = str(values[b.option]);
|
|
610
|
+
if (text === undefined)
|
|
611
|
+
return undefined;
|
|
612
|
+
const ms = parseDuration(text, b.option);
|
|
613
|
+
if (ms % 1000 !== 0)
|
|
614
|
+
throw new UsageError(`--${b.option} must be a whole number of seconds (e.g. 15m, 2h)`);
|
|
615
|
+
const seconds = ms / 1000;
|
|
616
|
+
if (seconds < b.min || seconds > b.max)
|
|
617
|
+
throw new UsageError(`--${b.option} must be between ${b.range} (got "${text}")`);
|
|
618
|
+
return seconds;
|
|
619
|
+
}
|
|
620
|
+
/** The workspace of a port command; a file-first workspace has no ports (refused before any port request). */
|
|
621
|
+
async function portWorkspace(ctx, ref) {
|
|
622
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
623
|
+
requireProcessful(ws, 'inbound_ports', 'Serving a port');
|
|
624
|
+
return ws;
|
|
625
|
+
}
|
|
626
|
+
/** A port as the API sends it (`--json`). */
|
|
627
|
+
const portJson = (p) => ({ port: p.port, url: p.url, created_at: p.createdAt, callback: p.callback ? { created_at: p.callback.createdAt } : null });
|
|
569
628
|
function requireProcessful(ws, operation, what) {
|
|
570
629
|
if (ws.mode !== 'file_first')
|
|
571
630
|
return;
|
|
@@ -683,6 +742,12 @@ export const KEY_HANDLERS = {
|
|
|
683
742
|
out(ctx, JSON.stringify(ws.labels, null, 2) + '\n', ws.labels);
|
|
684
743
|
return 0;
|
|
685
744
|
},
|
|
745
|
+
async 'workspaces hint'(ctx, _values, [ref]) {
|
|
746
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
747
|
+
const result = await ws.cell({ wake: null }).wakeHint(ctx.signal);
|
|
748
|
+
out(ctx, JSON.stringify(result, null, 2) + '\n', result);
|
|
749
|
+
return 0;
|
|
750
|
+
},
|
|
686
751
|
async 'workspaces idle'(ctx, values, [ref]) {
|
|
687
752
|
const ws = await resolveWorkspace(ctx, ref);
|
|
688
753
|
const policy = str(values.policy);
|
|
@@ -702,8 +767,10 @@ export const KEY_HANDLERS = {
|
|
|
702
767
|
async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
|
|
703
768
|
const timeout = str(values.timeout);
|
|
704
769
|
const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
|
|
770
|
+
const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
|
|
705
771
|
const ws = await resolveWorkspace(ctx, ref);
|
|
706
772
|
const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
|
|
773
|
+
...(resourceHint !== undefined ? { resource_hint: resourceHint } : {}),
|
|
707
774
|
...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}),
|
|
708
775
|
...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}) }, ctx.signal);
|
|
709
776
|
out(ctx, JSON.stringify(session, null, 2) + '\n', session);
|
|
@@ -878,6 +945,7 @@ export const KEY_HANDLERS = {
|
|
|
878
945
|
const outputLimit = limitText === undefined ? undefined : parsePositiveInt(limitText, 'output-limit', MAX_OUTPUT_LIMIT);
|
|
879
946
|
const stdinSource = str(values.stdin);
|
|
880
947
|
const burst = burstOptions(values, stdinSource !== undefined);
|
|
948
|
+
const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
|
|
881
949
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
882
950
|
const ws = await resolveWorkspace(ctx, ref);
|
|
883
951
|
const cell = cellOf(ctx, ws);
|
|
@@ -885,6 +953,8 @@ export const KEY_HANDLERS = {
|
|
|
885
953
|
if (ws.mode === 'file_first') {
|
|
886
954
|
if (burst)
|
|
887
955
|
throw new UsageError(`--burst applies to processful workspaces only; ${ws.key} is ${modeText(ws.mode)}`);
|
|
956
|
+
if (resourceHint)
|
|
957
|
+
throw new UsageError(`--resource-hint applies to processful workspaces only; ${ws.key} is ${modeText(ws.mode)} (each execution runs in a fresh VM)`);
|
|
888
958
|
// An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
889
959
|
const id = executionId ?? newExecutionId();
|
|
890
960
|
let r;
|
|
@@ -920,6 +990,7 @@ export const KEY_HANDLERS = {
|
|
|
920
990
|
...(Object.keys(env).length ? { env } : {}),
|
|
921
991
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
922
992
|
...(stdin !== undefined ? { stdin } : {}),
|
|
993
|
+
...(resourceHint !== undefined ? { resourceHint } : {}),
|
|
923
994
|
...(burst ?? {}),
|
|
924
995
|
maxOutputBytes: ctx.json ? MAX_JSON_OUTPUT : 0,
|
|
925
996
|
...(ctx.json ? {} : { onOutput: (stream, chunk) => void (stream === 'stderr' ? ctx.io.stderr : ctx.io.stdout).write(chunk) }),
|
|
@@ -1642,6 +1713,16 @@ export const KEY_HANDLERS = {
|
|
|
1642
1713
|
out(ctx, lifecycleText(`Forking ${source.key} into`, res.workspace, op), { operation: op, workspace: res.workspace.data });
|
|
1643
1714
|
return 0;
|
|
1644
1715
|
},
|
|
1716
|
+
// Resize (0.9.0+): PATCH /v1/workspaces/{id}/caps through the SDK, which waits until the resize has finished.
|
|
1717
|
+
async 'workspaces resize'(ctx, values, [ref]) {
|
|
1718
|
+
const params = resizeParams(values);
|
|
1719
|
+
const timeoutMs = waitTimeout(values);
|
|
1720
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
1721
|
+
requireProcessful(ws, 'resize', 'Resize');
|
|
1722
|
+
const r = await waited(ctx, 'the resize continues server side.', () => ws.resize({ ...params, timeoutMs, signal: ctx.signal }));
|
|
1723
|
+
out(ctx, resizeText(ws, r), resizeJson(r));
|
|
1724
|
+
return 0;
|
|
1725
|
+
},
|
|
1645
1726
|
async 'workspaces sessions'(ctx, _values, [ref]) {
|
|
1646
1727
|
const ws = await resolveWorkspace(ctx, ref);
|
|
1647
1728
|
const sessions = [];
|
|
@@ -1654,6 +1735,66 @@ export const KEY_HANDLERS = {
|
|
|
1654
1735
|
out(ctx, sessions.length ? sessionTable(sessions) : 'No agent sessions (no tool token was issued for this workspace yet).\n', { data: sessions });
|
|
1655
1736
|
return 0;
|
|
1656
1737
|
},
|
|
1738
|
+
// ---- inbound ports (0.9.0; contracts §39.3) ----
|
|
1739
|
+
async 'workspaces port expose'(ctx, _values, [ref, portText]) {
|
|
1740
|
+
const port = portArg(portText);
|
|
1741
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1742
|
+
const p = await ws.ports.expose(port);
|
|
1743
|
+
out(ctx, exposedPortText(ws, p), portJson(p));
|
|
1744
|
+
return 0;
|
|
1745
|
+
},
|
|
1746
|
+
async 'workspaces port ls'(ctx, _values, [ref]) {
|
|
1747
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1748
|
+
const ports = await ws.ports.list();
|
|
1749
|
+
out(ctx, portTable(ws, ports), { ports: ports.map(portJson) });
|
|
1750
|
+
return 0;
|
|
1751
|
+
},
|
|
1752
|
+
async 'workspaces port close'(ctx, _values, [ref, portText]) {
|
|
1753
|
+
const port = portArg(portText);
|
|
1754
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1755
|
+
await ws.ports.close(port);
|
|
1756
|
+
out(ctx, `Closed port ${port} of ${ws.key} (${ws.id}): its tokens, links and callback URL no longer work.\n`, { workspace_id: ws.id, port, closed: true });
|
|
1757
|
+
return 0;
|
|
1758
|
+
},
|
|
1759
|
+
async 'workspaces port token'(ctx, values, [ref, portText]) {
|
|
1760
|
+
const port = portArg(portText);
|
|
1761
|
+
const ttl = ttlSeconds(values, PORT_TOKEN_TTL);
|
|
1762
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1763
|
+
const t = await ws.ports.token(port, ttl !== undefined ? { ttlSeconds: ttl } : {});
|
|
1764
|
+
out(ctx, `${t.token}\n`, { token: t.token, expires_at: t.expiresAt, url: t.url });
|
|
1765
|
+
// The token alone on stdout (for $(...)); how to use it on stderr.
|
|
1766
|
+
if (!ctx.json)
|
|
1767
|
+
ctx.io.stderr.write(`Port token for ${t.url}, valid until ${time(t.expiresAt)}: send it as "Authorization: Bearer <token>".\n`);
|
|
1768
|
+
return 0;
|
|
1769
|
+
},
|
|
1770
|
+
async 'workspaces port link'(ctx, values, [ref, portText]) {
|
|
1771
|
+
const port = portArg(portText);
|
|
1772
|
+
const ttl = ttlSeconds(values, PORT_LINK_TTL);
|
|
1773
|
+
const path = str(values.path);
|
|
1774
|
+
if (path !== undefined && !path.startsWith('/'))
|
|
1775
|
+
throw new UsageError(`--path must start with / (got "${path}")`);
|
|
1776
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1777
|
+
const l = await ws.ports.link(port, { ...(ttl !== undefined ? { ttlSeconds: ttl } : {}), ...(path !== undefined ? { path } : {}) });
|
|
1778
|
+
out(ctx, `${l.url}\n`, { url: l.url, expires_at: l.expiresAt });
|
|
1779
|
+
if (!ctx.json)
|
|
1780
|
+
ctx.io.stderr.write(`Opens port ${port} of ${ws.key} in a browser until ${time(l.expiresAt)}; anyone with the link can open it until then.\n`);
|
|
1781
|
+
return 0;
|
|
1782
|
+
},
|
|
1783
|
+
async 'workspaces port callback'(ctx, values, [ref, portText]) {
|
|
1784
|
+
const port = portArg(portText);
|
|
1785
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1786
|
+
if (values.revoke === true) {
|
|
1787
|
+
await ws.ports.revokeCallbackUrl(port);
|
|
1788
|
+
out(ctx, `Revoked the callback URL of port ${port} of ${ws.key} (${ws.id}).\n`, { workspace_id: ws.id, port, revoked: true });
|
|
1789
|
+
return 0;
|
|
1790
|
+
}
|
|
1791
|
+
const c = await ws.ports.createCallbackUrl(port);
|
|
1792
|
+
out(ctx, `${c.url}\n`, { url: c.url, created_at: c.createdAt });
|
|
1793
|
+
if (!ctx.json) {
|
|
1794
|
+
ctx.io.stderr.write(`Callback URL of port ${port} of ${ws.key}: register it plus your own path (e.g. ${c.url}webhooks/github) with your webhook or OAuth provider; a request to it reaches /<path> of the port without a token. It is shown only now; running this again replaces it, --revoke removes it.\n`);
|
|
1795
|
+
}
|
|
1796
|
+
return 0;
|
|
1797
|
+
},
|
|
1657
1798
|
async 'workspaces secrets'(ctx, values, [ref]) {
|
|
1658
1799
|
const set = secretNames(strs(values.set), '--set');
|
|
1659
1800
|
const clear = values.clear === true;
|
package/dist/errors.d.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface ErrorInfo {
|
|
|
98
98
|
operation?: unknown;
|
|
99
99
|
/** A file-first execution the error is about (failed, lost, still running, or left running by Ctrl-C). */
|
|
100
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;
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
103
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
|
@@ -147,6 +147,7 @@ export function describeError(err) {
|
|
|
147
147
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
148
148
|
...(err.details ? { details: err.details } : {}),
|
|
149
149
|
source: err.source,
|
|
150
|
+
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
153
|
if (err instanceof OperationTimeoutError) {
|
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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';
|
|
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, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
|
|
2
2
|
import type { ErrorInfo } from './errors.js';
|
|
3
|
+
/** A workspace as the port commands name it. */
|
|
4
|
+
interface PortWorkspace {
|
|
5
|
+
key: string;
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
/** `shard ws port expose` (0.9.0): the URL, and how a request gets in. */
|
|
9
|
+
export declare function exposedPortText(w: PortWorkspace, p: ExposePortResult): string;
|
|
10
|
+
/** `shard ws port ls` (0.9.0). */
|
|
11
|
+
export declare function portTable(w: PortWorkspace, ports: readonly ExposedPort[]): string;
|
|
3
12
|
/** A size for people: `512 B`, `10.0 KiB`, `128.0 MiB`, `1.50 GiB`. */
|
|
4
13
|
export declare function byteSize(n: number): string;
|
|
5
14
|
export declare function json(value: unknown): string;
|
|
@@ -35,6 +44,17 @@ export declare function suspendWhenIdleText(w: {
|
|
|
35
44
|
suspend_request: SuspendRequest | null;
|
|
36
45
|
operation: Operation | null;
|
|
37
46
|
}): string;
|
|
47
|
+
/**
|
|
48
|
+
* `shard ws resize` (0.9.0): per resource the change and when it applies, e.g.
|
|
49
|
+
* `memory 2048 → 6144 MiB now` / `cpu 2000 → 4000 millis next start (boot vCPUs)`, plus what was clamped or
|
|
50
|
+
* applied in part (requested above a limit, a partial grow, a shrink the guest did not finish).
|
|
51
|
+
*/
|
|
52
|
+
export declare function resizeText(w: {
|
|
53
|
+
key: string;
|
|
54
|
+
id: string;
|
|
55
|
+
}, r: ResizeResult): string;
|
|
56
|
+
/** `shard ws resize --json`: the API's result (only the resources the request named), the stored caps, and the operation when one was waited for. */
|
|
57
|
+
export declare function resizeJson(r: ResizeResult): Record<string, unknown>;
|
|
38
58
|
/** `fixed 4096 MiB`, or `elastic: promised 8192 MiB, held 1024 MiB, plugged +2048 MiB` (plugged: above the held floor now). */
|
|
39
59
|
export declare function memoryText(m: WorkspaceMemory): string;
|
|
40
60
|
/** The grow an exec's start waited for: `delivered 1024 -> 4096 MiB in 175 ms`, `partial 1024 -> 3072 of 4096 MiB in 175 ms`. */
|
package/dist/format.js
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* these functions only shape what people read.
|
|
5
5
|
*/
|
|
6
6
|
import { durabilityOf, isDurable } from '@shardflux/sdk';
|
|
7
|
+
/** `shard ws port expose` (0.9.0): the URL, and how a request gets in. */
|
|
8
|
+
export function exposedPortText(w, p) {
|
|
9
|
+
return (`${p.created ? `Exposed port ${p.port} of ${w.key} (${w.id})` : `Port ${p.port} of ${w.key} (${w.id}) is exposed`}: ${p.url}\n` +
|
|
10
|
+
`Requests need a token (shard ws port token ${w.key} ${p.port}), a browser link (shard ws port link ${w.key} ${p.port}) or the callback URL (shard ws port callback ${w.key} ${p.port}).\n` +
|
|
11
|
+
`The server in the workspace listens on 0.0.0.0:${p.port}.\n`);
|
|
12
|
+
}
|
|
13
|
+
/** `shard ws port ls` (0.9.0). */
|
|
14
|
+
export function portTable(w, ports) {
|
|
15
|
+
if (ports.length === 0)
|
|
16
|
+
return `No ports of ${w.key} (${w.id}) are exposed; expose one with: shard ws port expose ${w.key} <port>\n`;
|
|
17
|
+
return table(['PORT', 'URL', 'EXPOSED', 'CALLBACK URL'], ports.map((p) => [String(p.port), p.url, time(p.createdAt), p.callback ? `created ${time(p.callback.createdAt)}` : '-']));
|
|
18
|
+
}
|
|
7
19
|
/** A size for people: `512 B`, `10.0 KiB`, `128.0 MiB`, `1.50 GiB`. */
|
|
8
20
|
export function byteSize(n) {
|
|
9
21
|
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`;
|
|
@@ -133,6 +145,55 @@ export function suspendWhenIdleText(w, r) {
|
|
|
133
145
|
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`;
|
|
134
146
|
}
|
|
135
147
|
const mib = (n) => (n === null ? '-' : `${n} MiB`);
|
|
148
|
+
const APPLIES_AT = { now: 'now', resume: 'on resume', next_start: 'next start' };
|
|
149
|
+
const LIMIT_TEXT = { plan: 'plan limit', template: 'template limit', host_capacity: 'host capacity', shrink_stalled: 'the guest kept memory in use' };
|
|
150
|
+
/** Why a resource applies later; `suspended` and `stopped` are what `on resume` / `next start` already say. */
|
|
151
|
+
const deferText = (r) => (r === null || r === 'suspended' || r === 'stopped' ? '' : r === 'boot_vcpus' ? ' (boot vCPUs)' : ` (${r.replace(/_/g, ' ')})`);
|
|
152
|
+
/**
|
|
153
|
+
* `shard ws resize` (0.9.0): per resource the change and when it applies, e.g.
|
|
154
|
+
* `memory 2048 → 6144 MiB now` / `cpu 2000 → 4000 millis next start (boot vCPUs)`, plus what was clamped or
|
|
155
|
+
* applied in part (requested above a limit, a partial grow, a shrink the guest did not finish).
|
|
156
|
+
*/
|
|
157
|
+
export function resizeText(w, r) {
|
|
158
|
+
const rows = [];
|
|
159
|
+
const row = (name, unit, x, extra = '', converged = null) => {
|
|
160
|
+
const notes = [];
|
|
161
|
+
if (x.requested !== null && x.target !== null && x.requested !== x.target)
|
|
162
|
+
notes.push(`requested ${x.requested} ${unit}${x.limit === 'plan' || x.limit === 'template' ? `, ${LIMIT_TEXT[x.limit]}` : ''}`);
|
|
163
|
+
if (x.applies_at === 'now' && x.applied !== null && x.applied !== x.target)
|
|
164
|
+
notes.push(`${x.applied} ${unit} applied${x.limit === 'host_capacity' || x.limit === 'shrink_stalled' ? `: ${LIMIT_TEXT[x.limit]}` : ''}`);
|
|
165
|
+
else if (converged === false)
|
|
166
|
+
notes.push('not converged');
|
|
167
|
+
rows.push([name, `${x.previous ?? '-'} → ${x.target ?? '-'} ${unit}${extra}`, `${APPLIES_AT[x.applies_at]}${deferText(x.reason)}${notes.length ? `; ${notes.join('; ')}` : ''}`]);
|
|
168
|
+
};
|
|
169
|
+
const m = r.memory;
|
|
170
|
+
if (m) {
|
|
171
|
+
const extra = m.allocation_mode === 'elastic' ? ` elastic${m.held_mib !== null ? `, held ${m.held_mib} MiB` : ''}` : '';
|
|
172
|
+
row('memory', 'MiB', { applies_at: m.applies_at, requested: m.requested_mib, target: m.target_mib, previous: m.previous_mib, applied: m.applied_mib, limit: m.limit_reason, reason: m.reason }, extra, m.converged);
|
|
173
|
+
}
|
|
174
|
+
const c = r.cpu;
|
|
175
|
+
if (c)
|
|
176
|
+
row('cpu', 'millis', { applies_at: c.applies_at, requested: c.requested_millis, target: c.target_millis, previous: c.previous_millis, applied: c.applied_millis, limit: c.limit_reason, reason: c.reason });
|
|
177
|
+
const d = r.disk;
|
|
178
|
+
if (d)
|
|
179
|
+
row('disk', 'GiB', { applies_at: d.applies_at, requested: d.requested_gib, target: d.target_gib, previous: d.previous_gib, applied: d.applied_gib, limit: d.limit_reason, reason: d.reason });
|
|
180
|
+
const width = Math.max(0, ...rows.map(([, change]) => change.length));
|
|
181
|
+
const lines = rows.map(([name, change, when]) => `${name.padEnd(6)} ${change.padEnd(width)} ${when}`);
|
|
182
|
+
return `Resized ${w.key} (${w.id}), ${r.state}:\n${lines.map((l) => `${l}\n`).join('')}`;
|
|
183
|
+
}
|
|
184
|
+
/** `shard ws resize --json`: the API's result (only the resources the request named), the stored caps, and the operation when one was waited for. */
|
|
185
|
+
export function resizeJson(r) {
|
|
186
|
+
return {
|
|
187
|
+
workspace_id: r.workspaceId,
|
|
188
|
+
operation_id: r.operationId,
|
|
189
|
+
state: r.state,
|
|
190
|
+
...(r.memory ? { memory: r.memory } : {}),
|
|
191
|
+
...(r.cpu ? { cpu: r.cpu } : {}),
|
|
192
|
+
...(r.disk ? { disk: r.disk } : {}),
|
|
193
|
+
caps: r.caps,
|
|
194
|
+
...(r.operation ? { operation: r.operation } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
136
197
|
/** `fixed 4096 MiB`, or `elastic: promised 8192 MiB, held 1024 MiB, plugged +2048 MiB` (plugged: above the held floor now). */
|
|
137
198
|
export function memoryText(m) {
|
|
138
199
|
if (m.allocation_mode !== 'elastic')
|
|
@@ -396,6 +457,10 @@ export function usageText(summary) {
|
|
|
396
457
|
const trialEnded = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
397
458
|
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']);
|
|
398
459
|
}
|
|
460
|
+
// cap_state storage_blocked (0.9.0): the Retained state allowance is used up; existing workspaces keep working.
|
|
461
|
+
if (Array.isArray(s.allowances) && s.allowances.some((a) => isObject(a) && a.cap_state === 'storage_blocked')) {
|
|
462
|
+
head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
|
|
463
|
+
}
|
|
399
464
|
for (const [k, v] of Object.entries(s))
|
|
400
465
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
401
466
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
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";
|
package/dist/main.js
CHANGED
|
@@ -76,6 +76,11 @@ function hintFor(info, cred, file) {
|
|
|
76
76
|
return 'this API has no billing configured: plans cannot be bought here';
|
|
77
77
|
if (info.code === 'allowance_exhausted')
|
|
78
78
|
return allowanceHint(info);
|
|
79
|
+
if (info.code === 'quota_exceeded') {
|
|
80
|
+
const quota = quotaHint(info);
|
|
81
|
+
if (quota)
|
|
82
|
+
return quota;
|
|
83
|
+
}
|
|
79
84
|
const policyHint = spendPolicyHint(info);
|
|
80
85
|
if (policyHint)
|
|
81
86
|
return policyHint;
|
|
@@ -99,6 +104,11 @@ function hintFor(info, cred, file) {
|
|
|
99
104
|
return 'the workspace cannot be woken right now and the call did not run; retry in a few seconds';
|
|
100
105
|
if (reason === 'host_feature_unavailable')
|
|
101
106
|
return hostFeatureHint(info.details?.feature);
|
|
107
|
+
// Inbound ports (0.9.0): the next step names the command.
|
|
108
|
+
if (reason === 'port_not_exposed')
|
|
109
|
+
return `expose it first: shard ws port expose <id|key> ${typeof info.details?.port === 'number' ? info.details.port : '<port>'}`;
|
|
110
|
+
if (reason === 'port_limit')
|
|
111
|
+
return 'close a port first (shard ws port ls <id|key>, then shard ws port close <id|key> <port>)';
|
|
102
112
|
// A processful command that could not start (0.5.1): what to change, from the workspace's reason.
|
|
103
113
|
if (info.code === 'conflict' && reason === 'exec_failed_to_start')
|
|
104
114
|
return startFailureHint(typeof info.details?.error === 'string' ? info.details.error : info.message, false);
|
|
@@ -135,6 +145,21 @@ function allowanceHint(info) {
|
|
|
135
145
|
return undefined;
|
|
136
146
|
}
|
|
137
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* quota_exceeded (0.9.0): the cell gateway's 429 when every workspace the plan allows to work at once is busy (the SDK
|
|
150
|
+
* already retried it after Retry-After), and the API's 403 when the plan's Retained state is used up (new keys and
|
|
151
|
+
* forks are refused). Other limits keep the API's message alone.
|
|
152
|
+
*/
|
|
153
|
+
function quotaHint(info) {
|
|
154
|
+
const limit = info.details?.limit;
|
|
155
|
+
if (limit === 'concurrent_workspaces' && info.status === 429) {
|
|
156
|
+
const s = info.retry_after_seconds;
|
|
157
|
+
return `every workspace your plan allows to work at once is busy and the call did not run (it was already retried); retry ${typeof s === 'number' ? `in ${s} s` : 'in a few seconds'}, or upgrade: shard billing plans`;
|
|
158
|
+
}
|
|
159
|
+
if (limit === 'retained_state')
|
|
160
|
+
return 'delete workspaces you no longer need (shard ws delete <key> --yes), or upgrade: shard billing plans';
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
138
163
|
/** Dollars for a hint: `9` for 900 minor units, `9.50` for 950. */
|
|
139
164
|
const dollars = (minor) => (typeof minor === 'number' ? (minor % 100 === 0 ? String(minor / 100) : (minor / 100).toFixed(2)) : undefined);
|
|
140
165
|
/**
|
|
@@ -292,6 +317,9 @@ const NO_NUDGE_REASONS = new Set([
|
|
|
292
317
|
'version_mismatch',
|
|
293
318
|
// A write under an immutable path (0.8.0): the hint says where to change it.
|
|
294
319
|
'read_only_path',
|
|
320
|
+
// Inbound ports (0.9.0): the hint names the command to expose or close a port.
|
|
321
|
+
'port_not_exposed',
|
|
322
|
+
'port_limit',
|
|
295
323
|
]);
|
|
296
324
|
/**
|
|
297
325
|
* Whether a failure ends with the `feedback:` line (0.5.0+): every failure except the expected flow above (feedback
|
|
@@ -300,6 +328,9 @@ const NO_NUDGE_REASONS = new Set([
|
|
|
300
328
|
function nudges(info, command) {
|
|
301
329
|
if (command === undefined || command === 'feedback')
|
|
302
330
|
return false;
|
|
331
|
+
// A plan limit whose hint names the next step (0.9.0): busy working slots, or Retained state used up.
|
|
332
|
+
if (info.code === 'quota_exceeded' && quotaHint(info) !== undefined)
|
|
333
|
+
return false;
|
|
303
334
|
const reason = reasonOf(info);
|
|
304
335
|
return !NO_NUDGE_CODES.has(info.code) && !(reason !== undefined && NO_NUDGE_REASONS.has(reason));
|
|
305
336
|
}
|
package/dist/timing.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export declare class TimingRecorder {
|
|
|
15
15
|
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
16
16
|
*/
|
|
17
17
|
lostSuspend: LifecycleTiming | null;
|
|
18
|
+
/**
|
|
19
|
+
* (0.8.1+) The last traced resume that restored the workspace's newest checkpoint after the machine it ran on failed
|
|
20
|
+
* (`host_lost.restored_from` `checkpoint`): a `ws resume --wait`, an `operations wait` of that resume, or a command's
|
|
21
|
+
* wake. `hostLostNotice()` says it. A restore from the disk is a cold boot (`coldBootNotice()`).
|
|
22
|
+
*/
|
|
23
|
+
hostLost: LifecycleTiming | null;
|
|
18
24
|
readonly listener: ProgressListener;
|
|
19
25
|
/**
|
|
20
26
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -24,15 +30,21 @@ export declare class TimingRecorder {
|
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* The one-line stderr notice after a command whose resume booted the workspace's saved disk (0.6.0+), with or without
|
|
27
|
-
* `--timing`: the workspace runs
|
|
33
|
+
* `--timing`: the workspace runs with its files kept, but running processes were restarted. The reason follows in
|
|
34
|
+
* parentheses: `runtime_changed`, or in words for `host_lost` (0.8.1+).
|
|
28
35
|
*/
|
|
29
36
|
export declare function coldBootNotice(t: LifecycleTiming): string;
|
|
37
|
+
/**
|
|
38
|
+
* The one-line stderr notice after a command whose resume restored the workspace's newest checkpoint after the machine
|
|
39
|
+
* it ran on failed (0.8.1+; `host_lost.restored_from` `checkpoint`): the state is as of `state_as_of`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hostLostNotice(t: LifecycleTiming): string;
|
|
30
42
|
/**
|
|
31
43
|
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
32
44
|
* could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
|
|
33
45
|
*/
|
|
34
46
|
export declare function lostSuspendNotice(t: LifecycleTiming): string;
|
|
35
|
-
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
47
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend, host loss), in that order. */
|
|
36
48
|
export declare function notices(r: TimingRecorder): string;
|
|
37
49
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
38
50
|
export declare function timingText(t: LifecycleTiming): string;
|
package/dist/timing.js
CHANGED
|
@@ -23,6 +23,12 @@ export class TimingRecorder {
|
|
|
23
23
|
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
24
24
|
*/
|
|
25
25
|
lostSuspend = null;
|
|
26
|
+
/**
|
|
27
|
+
* (0.8.1+) The last traced resume that restored the workspace's newest checkpoint after the machine it ran on failed
|
|
28
|
+
* (`host_lost.restored_from` `checkpoint`): a `ws resume --wait`, an `operations wait` of that resume, or a command's
|
|
29
|
+
* wake. `hostLostNotice()` says it. A restore from the disk is a cold boot (`coldBootNotice()`).
|
|
30
|
+
*/
|
|
31
|
+
hostLost = null;
|
|
26
32
|
listener = (e) => {
|
|
27
33
|
if (e.action === 'token' || e.action === 'tool')
|
|
28
34
|
return;
|
|
@@ -35,6 +41,8 @@ export class TimingRecorder {
|
|
|
35
41
|
this.coldBoot = e.timing;
|
|
36
42
|
if (e.timing.server?.lostSuspend)
|
|
37
43
|
this.lostSuspend = e.timing;
|
|
44
|
+
if (e.timing.server?.hostLost?.restoredFrom === 'checkpoint')
|
|
45
|
+
this.hostLost = e.timing;
|
|
38
46
|
};
|
|
39
47
|
/**
|
|
40
48
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -49,13 +57,29 @@ export class TimingRecorder {
|
|
|
49
57
|
function restartedProcesses(t) {
|
|
50
58
|
return t.server?.resumePath === 'cold_boot' && t.server.memoryRestored === false;
|
|
51
59
|
}
|
|
60
|
+
/** Cold boot reasons the notice says in words (0.8.1+); any other reason is printed as the API sent it. */
|
|
61
|
+
const COLD_BOOT_WORDS = {
|
|
62
|
+
host_lost: 'the machine it ran on failed',
|
|
63
|
+
};
|
|
52
64
|
/**
|
|
53
65
|
* The one-line stderr notice after a command whose resume booted the workspace's saved disk (0.6.0+), with or without
|
|
54
|
-
* `--timing`: the workspace runs
|
|
66
|
+
* `--timing`: the workspace runs with its files kept, but running processes were restarted. The reason follows in
|
|
67
|
+
* parentheses: `runtime_changed`, or in words for `host_lost` (0.8.1+).
|
|
55
68
|
*/
|
|
56
69
|
export function coldBootNotice(t) {
|
|
57
70
|
const reason = t.server?.coldBootReason;
|
|
58
|
-
|
|
71
|
+
const why = reason ? (COLD_BOOT_WORDS[reason] ?? reason) : null;
|
|
72
|
+
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from its saved disk; running processes were restarted${why ? ` (${why})` : ''}\n`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The one-line stderr notice after a command whose resume restored the workspace's newest checkpoint after the machine
|
|
76
|
+
* it ran on failed (0.8.1+; `host_lost.restored_from` `checkpoint`): the state is as of `state_as_of`.
|
|
77
|
+
*/
|
|
78
|
+
export function hostLostNotice(t) {
|
|
79
|
+
const h = t.server?.hostLost;
|
|
80
|
+
if (h?.restoredFrom !== 'checkpoint')
|
|
81
|
+
return '';
|
|
82
|
+
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from checkpoint ${h.restoredCheckpointId ?? '-'}${h.stateAsOf ? ` (state as of ${h.stateAsOf})` : ''} after the machine it ran on failed; changes after that time are not in it\n`;
|
|
59
83
|
}
|
|
60
84
|
/**
|
|
61
85
|
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
@@ -67,9 +91,9 @@ export function lostSuspendNotice(t) {
|
|
|
67
91
|
return '';
|
|
68
92
|
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from checkpoint ${l.restoredCheckpointId ?? '-'}${l.stateAsOf ? ` (state as of ${l.stateAsOf})` : ''}; the suspend of ${l.suspendedAt ?? l.checkpointId} was not kept${l.reason ? ` (${l.reason})` : ''}\n`;
|
|
69
93
|
}
|
|
70
|
-
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
94
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend, host loss), in that order. */
|
|
71
95
|
export function notices(r) {
|
|
72
|
-
return `${r.coldBoot ? coldBootNotice(r.coldBoot) : ''}${r.lostSuspend ? lostSuspendNotice(r.lostSuspend) : ''}`;
|
|
96
|
+
return `${r.coldBoot ? coldBootNotice(r.coldBoot) : ''}${r.lostSuspend ? lostSuspendNotice(r.lostSuspend) : ''}${r.hostLost ? hostLostNotice(r.hostLost) : ''}`;
|
|
73
97
|
}
|
|
74
98
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
75
99
|
export function timingText(t) {
|
|
@@ -126,6 +150,15 @@ export function timingJson(t) {
|
|
|
126
150
|
state_as_of: s.lostSuspend.stateAsOf,
|
|
127
151
|
}
|
|
128
152
|
: null,
|
|
153
|
+
// 0.8.1+: the machine the workspace ran on failed (resume: what it restored; suspend: detected_at only).
|
|
154
|
+
host_lost: s.hostLost
|
|
155
|
+
? {
|
|
156
|
+
detected_at: s.hostLost.detectedAt,
|
|
157
|
+
restored_from: s.hostLost.restoredFrom,
|
|
158
|
+
restored_checkpoint_id: s.hostLost.restoredCheckpointId,
|
|
159
|
+
state_as_of: s.hostLost.stateAsOf,
|
|
160
|
+
}
|
|
161
|
+
: null,
|
|
129
162
|
boot_to_ready_ms: s.bootToReadyMs,
|
|
130
163
|
host_timings_ms: s.hostTimingsMs,
|
|
131
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"yaml": "2.9.1",
|
|
47
|
-
"@shardflux/sdk": "^0.
|
|
47
|
+
"@shardflux/sdk": "^0.14.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|