@shardflux/cli 0.8.1 → 0.10.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 +94 -0
- package/README.md +132 -27
- package/dist/account-args.js +32 -4
- package/dist/account-commands.js +234 -111
- package/dist/args.d.ts +1 -2
- package/dist/args.js +288 -6
- package/dist/commands.d.ts +15 -1
- package/dist/commands.js +384 -5
- package/dist/format.d.ts +21 -1
- package/dist/format.js +61 -0
- package/dist/main.js +8 -0
- 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)
|
|
@@ -316,6 +346,22 @@ async function waitOperation(ctx, operationId, timeoutMs) {
|
|
|
316
346
|
* Runs an SDK call that waits itself (a waited open or lifecycle call). Ctrl-C reports the operation it was waiting
|
|
317
347
|
* for, as the SDK observed it; a held open interrupted before the server answered has none yet (`unnamed`).
|
|
318
348
|
*/
|
|
349
|
+
/**
|
|
350
|
+
* The workspace of `ws exec` / `exec-start`: with `--template <slug>` (0.10.0+) the key is opened first (created on first
|
|
351
|
+
* use, resumed if suspended; stderr says when it was created), else `<id|key>` must name an existing workspace.
|
|
352
|
+
*/
|
|
353
|
+
async function execTarget(ctx, values, ref) {
|
|
354
|
+
const template = str(values.template);
|
|
355
|
+
if (template === undefined)
|
|
356
|
+
return resolveWorkspace(ctx, ref);
|
|
357
|
+
const ws = await waited(ctx, 'the open continues server side (run the command again to keep waiting).', () => ctx
|
|
358
|
+
.cloud()
|
|
359
|
+
.workspace(ref, { template, agentLabel: ctx.agentLabel, wait: { timeoutMs: DEFAULT_WAIT_MS, signal: ctx.signal } })
|
|
360
|
+
.open());
|
|
361
|
+
if (ws.created === true && !ctx.json)
|
|
362
|
+
ctx.io.stderr.write(`created workspace ${ws.key} (template ${template})\n`);
|
|
363
|
+
return ws;
|
|
364
|
+
}
|
|
319
365
|
async function waited(ctx, unnamed, call) {
|
|
320
366
|
try {
|
|
321
367
|
return await call();
|
|
@@ -566,6 +612,100 @@ function treeRevisionAfter(ws, cell) {
|
|
|
566
612
|
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
|
567
613
|
* with its hint like the API's own refusal.
|
|
568
614
|
*/
|
|
615
|
+
/** `<port>` of the port commands: a TCP port number, 1-65535. */
|
|
616
|
+
export function portArg(text) {
|
|
617
|
+
if (!/^\d{1,5}$/.test(text) || Number(text) < 1 || Number(text) > 65_535)
|
|
618
|
+
throw new UsageError(`<port> must be a TCP port number between 1 and 65535 (got "${text}")`);
|
|
619
|
+
return Number(text);
|
|
620
|
+
}
|
|
621
|
+
const PORT_TOKEN_TTL = { option: 'ttl', min: 60, max: 86_400, range: '1m and 24h' };
|
|
622
|
+
const PORT_LINK_TTL = { option: 'ttl', min: 60, max: 604_800, range: '1m and 7d' };
|
|
623
|
+
/** `--ttl <duration>` of port tokens and links: whole seconds within the API's bounds (undefined: the API's default). */
|
|
624
|
+
export function ttlSeconds(values, b) {
|
|
625
|
+
const text = str(values[b.option]);
|
|
626
|
+
if (text === undefined)
|
|
627
|
+
return undefined;
|
|
628
|
+
const ms = parseDuration(text, b.option);
|
|
629
|
+
if (ms % 1000 !== 0)
|
|
630
|
+
throw new UsageError(`--${b.option} must be a whole number of seconds (e.g. 15m, 2h)`);
|
|
631
|
+
const seconds = ms / 1000;
|
|
632
|
+
if (seconds < b.min || seconds > b.max)
|
|
633
|
+
throw new UsageError(`--${b.option} must be between ${b.range} (got "${text}")`);
|
|
634
|
+
return seconds;
|
|
635
|
+
}
|
|
636
|
+
/** The workspace of a port command; a file-first workspace has no ports (refused before any port request). */
|
|
637
|
+
async function portWorkspace(ctx, ref) {
|
|
638
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
639
|
+
requireProcessful(ws, 'inbound_ports', 'Serving a port');
|
|
640
|
+
return ws;
|
|
641
|
+
}
|
|
642
|
+
/** The workspace of a computer command; a file-first workspace has no desktop (refused before any request). */
|
|
643
|
+
/** --format/--quality of a desktop image (0.10.0+). */
|
|
644
|
+
function imageOpts(values) {
|
|
645
|
+
const format = str(values.format);
|
|
646
|
+
if (format !== undefined && format !== 'png' && format !== 'jpeg')
|
|
647
|
+
throw new UsageError(`--format must be png or jpeg (got "${format}")`);
|
|
648
|
+
const quality = str(values.quality);
|
|
649
|
+
return { ...(format !== undefined ? { format } : {}), ...(quality !== undefined ? { quality: parsePositiveInt(quality, 'quality', 100) } : {}) };
|
|
650
|
+
}
|
|
651
|
+
/** --screenshot <file>, --settle and the image options: the screenshot after a computer action or batch. */
|
|
652
|
+
function lookOpts(values) {
|
|
653
|
+
const shotTo = str(values.screenshot);
|
|
654
|
+
const settle = str(values.settle);
|
|
655
|
+
if (settle !== undefined && (!/^\d+$/.test(settle.trim()) || Number(settle) > 10_000))
|
|
656
|
+
throw new UsageError(`--settle must be 0 to 10000 milliseconds (got "${settle}")`);
|
|
657
|
+
return { shotTo, req: { screenshot: shotTo !== undefined, ...(settle !== undefined ? { settle_ms: Number(settle) } : {}), ...imageOpts(values) } };
|
|
658
|
+
}
|
|
659
|
+
/** A screen coordinate argument (pixels from the top-left). */
|
|
660
|
+
function pixel(text, name) {
|
|
661
|
+
if (!/^\d+$/.test(text.trim()))
|
|
662
|
+
throw new UsageError(`<${name}> must be a whole number of pixels (got "${text}")`);
|
|
663
|
+
return Number(text);
|
|
664
|
+
}
|
|
665
|
+
/** <x> <y> as a coordinate; neither means at the pointer. */
|
|
666
|
+
function pointAt(x, y) {
|
|
667
|
+
if (x === undefined)
|
|
668
|
+
return {};
|
|
669
|
+
if (y === undefined)
|
|
670
|
+
throw new UsageError('pass both <x> and <y>, or neither to act at the pointer');
|
|
671
|
+
return { coordinate: [pixel(x, 'x'), pixel(y, 'y')] };
|
|
672
|
+
}
|
|
673
|
+
const modsOf = (values) => (str(values.mods) !== undefined ? { text: str(values.mods) } : {});
|
|
674
|
+
function secondsOf(text, name) {
|
|
675
|
+
if (!/^\d+(\.\d+)?$/.test(text.trim()) || Number(text) > 300)
|
|
676
|
+
throw new UsageError(`${name} must be 0 to 300 seconds (got "${text}")`);
|
|
677
|
+
return Number(text);
|
|
678
|
+
}
|
|
679
|
+
/** Runs computer actions as one batch and prints one line per action; exit 1 when one failed. */
|
|
680
|
+
async function runComputer(ctx, ref, actions, look) {
|
|
681
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
682
|
+
const r = await cellOf(ctx, ws).computer.act({ actions, ...look.req });
|
|
683
|
+
if (look.shotTo !== undefined && r.screenshot)
|
|
684
|
+
await writeFile(look.shotTo, Buffer.from(r.screenshot.data, 'base64'));
|
|
685
|
+
const lines = r.results.map((x, i) => {
|
|
686
|
+
const state = x.skipped ? 'skipped' : x.ok ? 'ok' : `failed: ${x.error?.message ?? ''}`;
|
|
687
|
+
return `${String(i + 1).padStart(3)} ${x.action.padEnd(16)} ${state}${x.output ? ` ${x.output}` : ''}${x.image ? ' (image)' : ''} ${x.took_ms} ms`;
|
|
688
|
+
});
|
|
689
|
+
const tail = `cursor ${r.cursor[0]},${r.cursor[1]} on a ${r.display.width}x${r.display.height} screen${look.shotTo !== undefined && r.screenshot ? `; screenshot written to ${look.shotTo}` : ''}`;
|
|
690
|
+
out(ctx, `${lines.join('\n')}${lines.length ? '\n' : ''}${tail}\n`, r);
|
|
691
|
+
return r.results.every((x) => x.ok) ? 0 : 1;
|
|
692
|
+
}
|
|
693
|
+
async function computerWorkspace(ctx, ref) {
|
|
694
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
695
|
+
requireProcessful(ws, 'computer_use', 'Computer use');
|
|
696
|
+
return ws;
|
|
697
|
+
}
|
|
698
|
+
async function setComputerUse(ctx, ref, enabled) {
|
|
699
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
700
|
+
await ws.setComputerUse(enabled);
|
|
701
|
+
const cu = ws.computerUse;
|
|
702
|
+
const why = cu.workspace === null ? `following its template (${cu.template ? 'on' : 'off'})` : 'set on the workspace';
|
|
703
|
+
const note = !cu.available ? ' Its template version cannot run a desktop.' : '';
|
|
704
|
+
out(ctx, `Computer use is ${cu.enabled ? 'on' : 'off'} for ${ws.key}, ${why}.${note}\n`, cu);
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
/** A port as the API sends it (`--json`). */
|
|
708
|
+
const portJson = (p) => ({ port: p.port, url: p.url, created_at: p.createdAt, callback: p.callback ? { created_at: p.callback.createdAt } : null });
|
|
569
709
|
function requireProcessful(ws, operation, what) {
|
|
570
710
|
if (ws.mode !== 'file_first')
|
|
571
711
|
return;
|
|
@@ -683,6 +823,12 @@ export const KEY_HANDLERS = {
|
|
|
683
823
|
out(ctx, JSON.stringify(ws.labels, null, 2) + '\n', ws.labels);
|
|
684
824
|
return 0;
|
|
685
825
|
},
|
|
826
|
+
async 'workspaces hint'(ctx, _values, [ref]) {
|
|
827
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
828
|
+
const result = await ws.cell({ wake: null }).wakeHint(ctx.signal);
|
|
829
|
+
out(ctx, JSON.stringify(result, null, 2) + '\n', result);
|
|
830
|
+
return 0;
|
|
831
|
+
},
|
|
686
832
|
async 'workspaces idle'(ctx, values, [ref]) {
|
|
687
833
|
const ws = await resolveWorkspace(ctx, ref);
|
|
688
834
|
const policy = str(values.policy);
|
|
@@ -702,8 +848,10 @@ export const KEY_HANDLERS = {
|
|
|
702
848
|
async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
|
|
703
849
|
const timeout = str(values.timeout);
|
|
704
850
|
const timeoutMs = timeout === undefined ? undefined : parseDuration(timeout, 'timeout');
|
|
705
|
-
const
|
|
851
|
+
const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
|
|
852
|
+
const ws = await execTarget(ctx, values, ref);
|
|
706
853
|
const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
|
|
854
|
+
...(resourceHint !== undefined ? { resource_hint: resourceHint } : {}),
|
|
707
855
|
...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}),
|
|
708
856
|
...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}) }, ctx.signal);
|
|
709
857
|
out(ctx, JSON.stringify(session, null, 2) + '\n', session);
|
|
@@ -791,6 +939,10 @@ export const KEY_HANDLERS = {
|
|
|
791
939
|
const bind = secretNames(strs(values.secret), '--secret');
|
|
792
940
|
const lifetime = oneOf(values, 'lifetime', LIFETIMES);
|
|
793
941
|
const mode = modeFlag(values);
|
|
942
|
+
const cuText = str(values['computer-use']);
|
|
943
|
+
if (cuText !== undefined && !['on', 'off', 'inherit'].includes(cuText))
|
|
944
|
+
throw new UsageError(`--computer-use must be on, off or inherit (got "${cuText}")`);
|
|
945
|
+
const computerUse = cuText === undefined ? undefined : cuText === 'inherit' ? null : cuText === 'on';
|
|
794
946
|
if (mode === 'file_first' && lifetime === 'session')
|
|
795
947
|
throw new UsageError('a file-first workspace is persistent: --lifetime session is for processful workspaces');
|
|
796
948
|
const inputs = await inputsFrom(ctx, values);
|
|
@@ -808,6 +960,7 @@ export const KEY_HANDLERS = {
|
|
|
808
960
|
...(inputs ? { inputs } : {}),
|
|
809
961
|
...(lifetime ? { lifetime } : {}),
|
|
810
962
|
...(mode ? { mode } : {}),
|
|
963
|
+
...(computerUse !== undefined ? { computerUse } : {}),
|
|
811
964
|
agentLabel: ctx.agentLabel,
|
|
812
965
|
wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
|
|
813
966
|
}));
|
|
@@ -878,13 +1031,16 @@ export const KEY_HANDLERS = {
|
|
|
878
1031
|
const outputLimit = limitText === undefined ? undefined : parsePositiveInt(limitText, 'output-limit', MAX_OUTPUT_LIMIT);
|
|
879
1032
|
const stdinSource = str(values.stdin);
|
|
880
1033
|
const burst = burstOptions(values, stdinSource !== undefined);
|
|
1034
|
+
const resourceHint = oneOf(values, 'resource-hint', RESOURCE_HINTS);
|
|
881
1035
|
const stdin = stdinSource === undefined ? undefined : await readInput(ctx, stdinSource, MAX_STDIN, '--stdin');
|
|
882
|
-
const ws = await
|
|
1036
|
+
const ws = await execTarget(ctx, values, ref);
|
|
883
1037
|
const cell = cellOf(ctx, ws);
|
|
884
1038
|
const cwd = str(values.cwd);
|
|
885
1039
|
if (ws.mode === 'file_first') {
|
|
886
1040
|
if (burst)
|
|
887
1041
|
throw new UsageError(`--burst applies to processful workspaces only; ${ws.key} is ${modeText(ws.mode)}`);
|
|
1042
|
+
if (resourceHint)
|
|
1043
|
+
throw new UsageError(`--resource-hint applies to processful workspaces only; ${ws.key} is ${modeText(ws.mode)} (each execution runs in a fresh VM)`);
|
|
888
1044
|
// An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
889
1045
|
const id = executionId ?? newExecutionId();
|
|
890
1046
|
let r;
|
|
@@ -920,6 +1076,7 @@ export const KEY_HANDLERS = {
|
|
|
920
1076
|
...(Object.keys(env).length ? { env } : {}),
|
|
921
1077
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
922
1078
|
...(stdin !== undefined ? { stdin } : {}),
|
|
1079
|
+
...(resourceHint !== undefined ? { resourceHint } : {}),
|
|
923
1080
|
...(burst ?? {}),
|
|
924
1081
|
maxOutputBytes: ctx.json ? MAX_JSON_OUTPUT : 0,
|
|
925
1082
|
...(ctx.json ? {} : { onOutput: (stream, chunk) => void (stream === 'stderr' ? ctx.io.stderr : ctx.io.stdout).write(chunk) }),
|
|
@@ -1642,6 +1799,16 @@ export const KEY_HANDLERS = {
|
|
|
1642
1799
|
out(ctx, lifecycleText(`Forking ${source.key} into`, res.workspace, op), { operation: op, workspace: res.workspace.data });
|
|
1643
1800
|
return 0;
|
|
1644
1801
|
},
|
|
1802
|
+
// Resize (0.9.0+): PATCH /v1/workspaces/{id}/caps through the SDK, which waits until the resize has finished.
|
|
1803
|
+
async 'workspaces resize'(ctx, values, [ref]) {
|
|
1804
|
+
const params = resizeParams(values);
|
|
1805
|
+
const timeoutMs = waitTimeout(values);
|
|
1806
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
1807
|
+
requireProcessful(ws, 'resize', 'Resize');
|
|
1808
|
+
const r = await waited(ctx, 'the resize continues server side.', () => ws.resize({ ...params, timeoutMs, signal: ctx.signal }));
|
|
1809
|
+
out(ctx, resizeText(ws, r), resizeJson(r));
|
|
1810
|
+
return 0;
|
|
1811
|
+
},
|
|
1645
1812
|
async 'workspaces sessions'(ctx, _values, [ref]) {
|
|
1646
1813
|
const ws = await resolveWorkspace(ctx, ref);
|
|
1647
1814
|
const sessions = [];
|
|
@@ -1654,6 +1821,218 @@ export const KEY_HANDLERS = {
|
|
|
1654
1821
|
out(ctx, sessions.length ? sessionTable(sessions) : 'No agent sessions (no tool token was issued for this workspace yet).\n', { data: sessions });
|
|
1655
1822
|
return 0;
|
|
1656
1823
|
},
|
|
1824
|
+
// ---- inbound ports (0.9.0; contracts §39.3) ----
|
|
1825
|
+
async 'workspaces port expose'(ctx, _values, [ref, portText]) {
|
|
1826
|
+
const port = portArg(portText);
|
|
1827
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1828
|
+
const p = await ws.ports.expose(port);
|
|
1829
|
+
out(ctx, exposedPortText(ws, p), portJson(p));
|
|
1830
|
+
return 0;
|
|
1831
|
+
},
|
|
1832
|
+
async 'workspaces port ls'(ctx, _values, [ref]) {
|
|
1833
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1834
|
+
const ports = await ws.ports.list();
|
|
1835
|
+
out(ctx, portTable(ws, ports), { ports: ports.map(portJson) });
|
|
1836
|
+
return 0;
|
|
1837
|
+
},
|
|
1838
|
+
async 'workspaces port close'(ctx, _values, [ref, portText]) {
|
|
1839
|
+
const port = portArg(portText);
|
|
1840
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1841
|
+
await ws.ports.close(port);
|
|
1842
|
+
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 });
|
|
1843
|
+
return 0;
|
|
1844
|
+
},
|
|
1845
|
+
async 'workspaces port token'(ctx, values, [ref, portText]) {
|
|
1846
|
+
const port = portArg(portText);
|
|
1847
|
+
const ttl = ttlSeconds(values, PORT_TOKEN_TTL);
|
|
1848
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1849
|
+
const t = await ws.ports.token(port, ttl !== undefined ? { ttlSeconds: ttl } : {});
|
|
1850
|
+
out(ctx, `${t.token}\n`, { token: t.token, expires_at: t.expiresAt, url: t.url });
|
|
1851
|
+
// The token alone on stdout (for $(...)); how to use it on stderr.
|
|
1852
|
+
if (!ctx.json)
|
|
1853
|
+
ctx.io.stderr.write(`Port token for ${t.url}, valid until ${time(t.expiresAt)}: send it as "Authorization: Bearer <token>".\n`);
|
|
1854
|
+
return 0;
|
|
1855
|
+
},
|
|
1856
|
+
async 'workspaces port link'(ctx, values, [ref, portText]) {
|
|
1857
|
+
const port = portArg(portText);
|
|
1858
|
+
const ttl = ttlSeconds(values, PORT_LINK_TTL);
|
|
1859
|
+
const path = str(values.path);
|
|
1860
|
+
if (path !== undefined && !path.startsWith('/'))
|
|
1861
|
+
throw new UsageError(`--path must start with / (got "${path}")`);
|
|
1862
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1863
|
+
const l = await ws.ports.link(port, { ...(ttl !== undefined ? { ttlSeconds: ttl } : {}), ...(path !== undefined ? { path } : {}) });
|
|
1864
|
+
out(ctx, `${l.url}\n`, { url: l.url, expires_at: l.expiresAt });
|
|
1865
|
+
if (!ctx.json)
|
|
1866
|
+
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`);
|
|
1867
|
+
return 0;
|
|
1868
|
+
},
|
|
1869
|
+
async 'workspaces port callback'(ctx, values, [ref, portText]) {
|
|
1870
|
+
const port = portArg(portText);
|
|
1871
|
+
const ws = await portWorkspace(ctx, ref);
|
|
1872
|
+
if (values.revoke === true) {
|
|
1873
|
+
await ws.ports.revokeCallbackUrl(port);
|
|
1874
|
+
out(ctx, `Revoked the callback URL of port ${port} of ${ws.key} (${ws.id}).\n`, { workspace_id: ws.id, port, revoked: true });
|
|
1875
|
+
return 0;
|
|
1876
|
+
}
|
|
1877
|
+
const c = await ws.ports.createCallbackUrl(port);
|
|
1878
|
+
out(ctx, `${c.url}\n`, { url: c.url, created_at: c.createdAt });
|
|
1879
|
+
if (!ctx.json) {
|
|
1880
|
+
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`);
|
|
1881
|
+
}
|
|
1882
|
+
return 0;
|
|
1883
|
+
},
|
|
1884
|
+
// ---- computer use (0.10.0; contracts §45) ----
|
|
1885
|
+
async 'workspaces computer status'(ctx, _values, [ref]) {
|
|
1886
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
1887
|
+
const st = await cellOf(ctx, ws).computer.status();
|
|
1888
|
+
const views = [st.streams.view ? 'view only' : null, st.streams.interactive ? 'interactive' : null].filter(Boolean);
|
|
1889
|
+
out(ctx, st.running
|
|
1890
|
+
? `Desktop of ${ws.key}: running ${st.width}x${st.height} since ${time(st.started_at ?? '')}; viewers: ${views.length ? views.join(', ') : 'none'}.\n`
|
|
1891
|
+
: `Desktop of ${ws.key}: not running (the first computer call starts it).\n`, st);
|
|
1892
|
+
return 0;
|
|
1893
|
+
},
|
|
1894
|
+
async 'workspaces computer screenshot'(ctx, values, [ref]) {
|
|
1895
|
+
const img = imageOpts(values);
|
|
1896
|
+
const dest = str(values.out) ?? (img.format === 'jpeg' ? 'screenshot.jpg' : 'screenshot.png');
|
|
1897
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
1898
|
+
const r = await cellOf(ctx, ws).computer.act({ actions: [], screenshot: true, ...img });
|
|
1899
|
+
if (!r.screenshot)
|
|
1900
|
+
throw new ShardfluxProtocolError('the desktop returned no screenshot', 200, 'cell');
|
|
1901
|
+
const bytes = Buffer.from(r.screenshot.data, 'base64');
|
|
1902
|
+
await writeFile(dest, bytes);
|
|
1903
|
+
out(ctx, `Wrote a ${r.screenshot.width}x${r.screenshot.height} screenshot of ${ws.key} to ${dest}.\n`, {
|
|
1904
|
+
out: dest, format: r.screenshot.format, width: r.screenshot.width, height: r.screenshot.height, bytes: bytes.length,
|
|
1905
|
+
});
|
|
1906
|
+
return 0;
|
|
1907
|
+
},
|
|
1908
|
+
async 'workspaces computer act'(ctx, values, [ref, source]) {
|
|
1909
|
+
const text = source === '-' ? Buffer.from(await readInput(ctx, '-', 1 << 20, 'actions')).toString('utf8') : source;
|
|
1910
|
+
let actions;
|
|
1911
|
+
try {
|
|
1912
|
+
actions = JSON.parse(text);
|
|
1913
|
+
}
|
|
1914
|
+
catch {
|
|
1915
|
+
throw new UsageError('<actions-json> must be a JSON array of actions, e.g. [{"action":"screenshot"}]');
|
|
1916
|
+
}
|
|
1917
|
+
if (!Array.isArray(actions))
|
|
1918
|
+
throw new UsageError('<actions-json> must be a JSON array of actions');
|
|
1919
|
+
return runComputer(ctx, ref, actions, lookOpts(values));
|
|
1920
|
+
},
|
|
1921
|
+
async 'workspaces computer click'(ctx, values, [ref, x, y]) {
|
|
1922
|
+
const button = str(values.button) ?? 'left';
|
|
1923
|
+
if (button !== 'left' && button !== 'right' && button !== 'middle')
|
|
1924
|
+
throw new UsageError(`--button must be left, right or middle (got "${button}")`);
|
|
1925
|
+
const count = str(values.count) !== undefined ? parsePositiveInt(str(values.count), 'count', 3) : 1;
|
|
1926
|
+
if (count > 1 && button !== 'left')
|
|
1927
|
+
throw new UsageError('--count 2 or 3 is a double or triple click of the left button');
|
|
1928
|
+
const action = button === 'right' ? 'right_click' : button === 'middle' ? 'middle_click' : ['left_click', 'double_click', 'triple_click'][count - 1];
|
|
1929
|
+
return runComputer(ctx, ref, [{ action, ...pointAt(x, y), ...modsOf(values) }], lookOpts(values));
|
|
1930
|
+
},
|
|
1931
|
+
async 'workspaces computer type'(ctx, values, [ref, source]) {
|
|
1932
|
+
const text = source === '-' ? Buffer.from(await readInput(ctx, '-', 256 << 10, 'text')).toString('utf8') : source;
|
|
1933
|
+
if (text === '')
|
|
1934
|
+
throw new UsageError('<text> must not be empty');
|
|
1935
|
+
return runComputer(ctx, ref, [{ action: 'type', text }], lookOpts(values));
|
|
1936
|
+
},
|
|
1937
|
+
async 'workspaces computer key'(ctx, values, [ref, keys]) {
|
|
1938
|
+
const repeat = str(values.repeat) !== undefined ? parsePositiveInt(str(values.repeat), 'repeat', 100) : undefined;
|
|
1939
|
+
return runComputer(ctx, ref, [{ action: 'key', text: keys, ...(repeat !== undefined ? { repeat } : {}) }], lookOpts(values));
|
|
1940
|
+
},
|
|
1941
|
+
async 'workspaces computer scroll'(ctx, values, [ref, direction, x, y]) {
|
|
1942
|
+
if (direction !== 'up' && direction !== 'down' && direction !== 'left' && direction !== 'right') {
|
|
1943
|
+
throw new UsageError(`expected up, down, left or right (got "${direction}")`);
|
|
1944
|
+
}
|
|
1945
|
+
const amount = str(values.amount) !== undefined ? parsePositiveInt(str(values.amount), 'amount', 100) : 3;
|
|
1946
|
+
return runComputer(ctx, ref, [{ action: 'scroll', scroll_direction: direction, scroll_amount: amount, ...pointAt(x, y), ...modsOf(values) }], lookOpts(values));
|
|
1947
|
+
},
|
|
1948
|
+
async 'workspaces computer move'(ctx, values, [ref, x, y]) {
|
|
1949
|
+
return runComputer(ctx, ref, [{ action: 'mouse_move', ...pointAt(x, y) }], lookOpts(values));
|
|
1950
|
+
},
|
|
1951
|
+
async 'workspaces computer drag'(ctx, values, [ref, x1, y1, x2, y2]) {
|
|
1952
|
+
const start = pointAt(x1, y1).coordinate;
|
|
1953
|
+
return runComputer(ctx, ref, [{ action: 'left_click_drag', start_coordinate: start, ...pointAt(x2, y2), ...modsOf(values) }], lookOpts(values));
|
|
1954
|
+
},
|
|
1955
|
+
async 'workspaces computer mouse-down'(ctx, values, [ref]) {
|
|
1956
|
+
return runComputer(ctx, ref, [{ action: 'left_mouse_down' }], lookOpts(values));
|
|
1957
|
+
},
|
|
1958
|
+
async 'workspaces computer mouse-up'(ctx, values, [ref]) {
|
|
1959
|
+
return runComputer(ctx, ref, [{ action: 'left_mouse_up' }], lookOpts(values));
|
|
1960
|
+
},
|
|
1961
|
+
async 'workspaces computer hold-key'(ctx, values, [ref, keys, seconds]) {
|
|
1962
|
+
return runComputer(ctx, ref, [{ action: 'hold_key', text: keys, duration: secondsOf(seconds, '<seconds>') }], lookOpts(values));
|
|
1963
|
+
},
|
|
1964
|
+
async 'workspaces computer wait'(ctx, values, [ref, seconds]) {
|
|
1965
|
+
return runComputer(ctx, ref, [{ action: 'wait', duration: secondsOf(seconds, '<seconds>') }], lookOpts(values));
|
|
1966
|
+
},
|
|
1967
|
+
async 'workspaces computer zoom'(ctx, values, [ref, x0, y0, x1, y1]) {
|
|
1968
|
+
const img = imageOpts(values);
|
|
1969
|
+
const region = [pixel(x0, 'x0'), pixel(y0, 'y0'), pixel(x1, 'x1'), pixel(y1, 'y1')];
|
|
1970
|
+
const dest = str(values.out) ?? (img.format === 'jpeg' ? 'zoom.jpg' : 'zoom.png');
|
|
1971
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
1972
|
+
const r = await cellOf(ctx, ws).computer.act({ actions: [{ action: 'zoom', region }], ...img });
|
|
1973
|
+
const res = r.results[0];
|
|
1974
|
+
if (!res?.ok || !res.image) {
|
|
1975
|
+
out(ctx, `zoom failed: ${res?.error?.message ?? 'no image'}\n`, r);
|
|
1976
|
+
return 1;
|
|
1977
|
+
}
|
|
1978
|
+
const bytes = Buffer.from(res.image.data, 'base64');
|
|
1979
|
+
await writeFile(dest, bytes);
|
|
1980
|
+
out(ctx, `Wrote a ${res.image.width}x${res.image.height} view of [${region.join(', ')}] on ${ws.key} to ${dest}.\n`, {
|
|
1981
|
+
out: dest, format: res.image.format, width: res.image.width, height: res.image.height, bytes: bytes.length,
|
|
1982
|
+
});
|
|
1983
|
+
return 0;
|
|
1984
|
+
},
|
|
1985
|
+
async 'workspaces computer cursor'(ctx, _values, [ref]) {
|
|
1986
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
1987
|
+
const r = await cellOf(ctx, ws).computer.act({ actions: [{ action: 'cursor_position' }] });
|
|
1988
|
+
out(ctx, `${r.cursor[0]} ${r.cursor[1]}\n`, { x: r.cursor[0], y: r.cursor[1], display: r.display });
|
|
1989
|
+
return 0;
|
|
1990
|
+
},
|
|
1991
|
+
async 'workspaces computer start'(ctx, values, [ref]) {
|
|
1992
|
+
const width = str(values.width) !== undefined ? parsePositiveInt(str(values.width), '--width') : undefined;
|
|
1993
|
+
const height = str(values.height) !== undefined ? parsePositiveInt(str(values.height), '--height') : undefined;
|
|
1994
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
1995
|
+
const st = await cellOf(ctx, ws).computer.start({ ...(width !== undefined ? { width } : {}), ...(height !== undefined ? { height } : {}) });
|
|
1996
|
+
out(ctx, `Desktop of ${ws.key}: running ${st.width}x${st.height}.\n`, st);
|
|
1997
|
+
return 0;
|
|
1998
|
+
},
|
|
1999
|
+
async 'workspaces computer stop'(ctx, _values, [ref]) {
|
|
2000
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
2001
|
+
await cellOf(ctx, ws).computer.stop();
|
|
2002
|
+
out(ctx, `Stopped the desktop of ${ws.key}.\n`, { workspace_id: ws.id, stopped: true });
|
|
2003
|
+
return 0;
|
|
2004
|
+
},
|
|
2005
|
+
async 'workspaces computer stream'(ctx, values, [ref]) {
|
|
2006
|
+
const ws = await computerWorkspace(ctx, ref);
|
|
2007
|
+
if (values.stop === true) {
|
|
2008
|
+
await ws.computer.stopStream();
|
|
2009
|
+
out(ctx, `Ended every view of the desktop of ${ws.key}.\n`, { workspace_id: ws.id, stopped: true });
|
|
2010
|
+
return 0;
|
|
2011
|
+
}
|
|
2012
|
+
const ttl = ttlSeconds(values, PORT_LINK_TTL);
|
|
2013
|
+
const s = await ws.computer.stream({ interactive: values.interactive === true, ...(ttl !== undefined ? { ttlSeconds: ttl } : {}) });
|
|
2014
|
+
out(ctx, `${s.url}\n`, { url: s.url, expires_at: s.expiresAt, port: s.port, interactive: s.interactive });
|
|
2015
|
+
if (!ctx.json) {
|
|
2016
|
+
ctx.io.stderr.write(`${s.interactive ? 'Uses' : 'Watches'} the desktop of ${ws.key} until ${time(s.expiresAt)}; anyone with the link can open it until then.\n`);
|
|
2017
|
+
}
|
|
2018
|
+
return 0;
|
|
2019
|
+
},
|
|
2020
|
+
async 'workspaces computer enable'(ctx, _values, [ref]) {
|
|
2021
|
+
return setComputerUse(ctx, ref, true);
|
|
2022
|
+
},
|
|
2023
|
+
async 'workspaces computer disable'(ctx, _values, [ref]) {
|
|
2024
|
+
return setComputerUse(ctx, ref, false);
|
|
2025
|
+
},
|
|
2026
|
+
async 'workspaces computer inherit'(ctx, _values, [ref]) {
|
|
2027
|
+
return setComputerUse(ctx, ref, null);
|
|
2028
|
+
},
|
|
2029
|
+
async 'templates computer-use'(ctx, _values, [slug, state]) {
|
|
2030
|
+
if (state !== 'on' && state !== 'off')
|
|
2031
|
+
throw new UsageError(`expected on or off (got "${state}")`);
|
|
2032
|
+
const r = await ctx.cloud().templates.setComputerUse(slug, state === 'on');
|
|
2033
|
+
out(ctx, `Computer use is ${r.enabled ? 'on' : 'off'} for the workspaces of ${slug} (a workspace's own setting wins).\n`, r);
|
|
2034
|
+
return 0;
|
|
2035
|
+
},
|
|
1657
2036
|
async 'workspaces secrets'(ctx, values, [ref]) {
|
|
1658
2037
|
const set = secretNames(strs(values.set), '--set');
|
|
1659
2038
|
const clear = values.clear === true;
|
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')
|
package/dist/main.js
CHANGED
|
@@ -104,6 +104,11 @@ function hintFor(info, cred, file) {
|
|
|
104
104
|
return 'the workspace cannot be woken right now and the call did not run; retry in a few seconds';
|
|
105
105
|
if (reason === 'host_feature_unavailable')
|
|
106
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>)';
|
|
107
112
|
// A processful command that could not start (0.5.1): what to change, from the workspace's reason.
|
|
108
113
|
if (info.code === 'conflict' && reason === 'exec_failed_to_start')
|
|
109
114
|
return startFailureHint(typeof info.details?.error === 'string' ? info.details.error : info.message, false);
|
|
@@ -312,6 +317,9 @@ const NO_NUDGE_REASONS = new Set([
|
|
|
312
317
|
'version_mismatch',
|
|
313
318
|
// A write under an immutable path (0.8.0): the hint says where to change it.
|
|
314
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',
|
|
315
323
|
]);
|
|
316
324
|
/**
|
|
317
325
|
* Whether a failure ends with the `feedback:` line (0.5.0+): every failure except the expected flow above (feedback
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.15.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|