@shardflux/cli 0.1.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/dist/format.js ADDED
@@ -0,0 +1,177 @@
1
+ export function json(value) {
2
+ return `${JSON.stringify(value, null, 2)}\n`;
3
+ }
4
+ export function table(headers, rows) {
5
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
6
+ const line = (cells) => cells
7
+ .map((c, i) => (i === cells.length - 1 ? c : c.padEnd(widths[i])))
8
+ .join(' ')
9
+ .trimEnd();
10
+ return `${[line(headers), ...rows.map(line)].join('\n')}\n`;
11
+ }
12
+ export function kv(pairs) {
13
+ const w = Math.max(...pairs.map(([k]) => k.length));
14
+ return `${pairs.map(([k, v]) => `${k.padEnd(w)} ${v}`).join('\n')}\n`;
15
+ }
16
+ /** ISO timestamp to second precision ("2026-09-26 12:00:00Z"), "-" when absent. */
17
+ export function time(iso) {
18
+ if (!iso)
19
+ return '-';
20
+ const d = new Date(iso);
21
+ if (Number.isNaN(d.getTime()))
22
+ return iso;
23
+ return `${d.toISOString().slice(0, 19).replace('T', ' ')}Z`;
24
+ }
25
+ function scalar(v) {
26
+ if (v === null || v === undefined)
27
+ return '-';
28
+ if (typeof v === 'string')
29
+ return v;
30
+ if (typeof v === 'number' || typeof v === 'boolean')
31
+ return String(v);
32
+ return JSON.stringify(v);
33
+ }
34
+ /** { a: 1, b: 2 } -> "a=1 b=2" */
35
+ export function pairs(obj) {
36
+ if (!obj)
37
+ return '-';
38
+ const entries = Object.entries(obj).filter(([, v]) => typeof v !== 'object' || v === null);
39
+ return entries.length ? entries.map(([k, v]) => `${k}=${scalar(v)}`).join(' ') : '-';
40
+ }
41
+ export function operationSummary(op) {
42
+ if (!op)
43
+ return '-';
44
+ return `${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}`;
45
+ }
46
+ export const WORKSPACE_HEADERS = ['ID', 'KEY', 'STATE', 'DESIRED', 'TEMPLATE', 'OPERATION', 'CREATED'];
47
+ export function workspaceRow(w) {
48
+ const state = w.deleted_at ? `${w.observed_state} (deleted)` : w.observed_state;
49
+ return [w.id, w.workspace_key, state, w.desired_state, `${w.template.slug}@${w.template.version}`, operationSummary(w.active_operation), time(w.created_at)];
50
+ }
51
+ export function workspaceTable(ws) {
52
+ return table(WORKSPACE_HEADERS, ws.map(workspaceRow));
53
+ }
54
+ export function isReady(w) {
55
+ return w.observed_state === 'running' && w.desired_state === 'running' && w.deleted_at === null;
56
+ }
57
+ export function workspaceDetail(w) {
58
+ const c = w.ceilings;
59
+ return kv([
60
+ ['id', w.id],
61
+ ['key', w.workspace_key],
62
+ ['state', `${w.observed_state} (desired ${w.desired_state})`],
63
+ ['ready', isReady(w) ? 'yes' : 'no'],
64
+ ['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
65
+ ['cell', w.cell_id ? `${w.cell_id} ${w.cell_endpoint ?? '(endpoint unresolved)'}` : '-'],
66
+ ['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})` : '-'],
67
+ ['grants', pairs(w.grants)],
68
+ ['caps', pairs(w.caps)],
69
+ ['operation', w.active_operation ? `${operationSummary(w.active_operation)} ${w.active_operation.id}` : '-'],
70
+ ['pending', w.pending_reason ?? '-'],
71
+ ['forked from', w.forked_from_workspace_id ?? '-'],
72
+ ['created', time(w.created_at)],
73
+ ['deleted', time(w.deleted_at)],
74
+ ]);
75
+ }
76
+ export const OPERATION_HEADERS = ['ID', 'KIND', 'STATE', 'REASON', 'CREATED', 'COMPLETED'];
77
+ export function operationTable(ops) {
78
+ return table(OPERATION_HEADERS, ops.map((o) => [o.id, o.kind, o.state, o.state_reason ?? '-', time(o.created_at), time(o.completed_at)]));
79
+ }
80
+ export function operationDetail(op) {
81
+ const err = op.error;
82
+ return kv([
83
+ ['id', op.id],
84
+ ['kind', op.kind],
85
+ ['state', op.state],
86
+ ['reason', op.state_reason ?? '-'],
87
+ ['workspace', op.workspace_id ?? '-'],
88
+ ['created', time(op.created_at)],
89
+ ['updated', time(op.updated_at)],
90
+ ['completed', time(op.completed_at)],
91
+ ['error', err ? `${scalar(err.code)}${err.message ? `: ${scalar(err.message)}` : ''}` : '-'],
92
+ ]);
93
+ }
94
+ export function sessionTable(sessions) {
95
+ return table(['ID', 'LABEL', 'PRINCIPAL', 'TOKENS', 'LAST TOKEN', 'REVOKED'], sessions.map((s) => [s.id, s.agent_label, `${s.principal_type}:${s.principal_id}`, String(s.tokens_issued), time(s.last_token_issued_at), time(s.revoked_at)]));
96
+ }
97
+ export function fileTable(list) {
98
+ return table(['TYPE', 'SIZE', 'MODIFIED', 'NAME'], list.entries.map((e) => [e.type, String(e.size), time(e.modified_at), e.name]));
99
+ }
100
+ function isObject(v) {
101
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
102
+ }
103
+ /** A table of the listed columns that at least one row actually has. */
104
+ function objectTable(rows, columns) {
105
+ if (!Array.isArray(rows) || rows.length === 0)
106
+ return '';
107
+ const objs = rows.filter(isObject);
108
+ const cols = columns.filter(([k]) => objs.some((o) => k in o));
109
+ return table(cols.map(([, h]) => h), objs.map((o) => cols.map(([k]) => (k === 'percent_used' && typeof o[k] === 'number' ? `${o[k]}%` : scalar(o[k])))));
110
+ }
111
+ const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'meters', 'allowances', 'exhausted', 'alert_thresholds']);
112
+ /**
113
+ * The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
114
+ * freshness, then meters and allowances as tables. Columns absent from the response are omitted.
115
+ */
116
+ export function usageText(summary) {
117
+ if (!isObject(summary))
118
+ return json(summary);
119
+ const s = summary;
120
+ const head = [];
121
+ if (isObject(s.plan))
122
+ head.push(['plan', `${scalar(s.plan.plan_key)}${s.plan.catalog_version !== undefined ? ` (catalog ${scalar(s.plan.catalog_version)})` : ''}`]);
123
+ if (isObject(s.period))
124
+ head.push(['period', `${time(scalar(s.period.start))} .. ${time(scalar(s.period.end))}`]);
125
+ const m = isObject(s.measurement) ? s.measurement : s;
126
+ if (m.measured_through !== undefined)
127
+ head.push(['measured through', time(m.measured_through)]);
128
+ if (isObject(s.measurement) && s.measurement.estimate_status !== undefined)
129
+ head.push(['estimate', scalar(s.measurement.estimate_status)]);
130
+ if (typeof s.allowance_exhausted === 'boolean')
131
+ head.push(['allowance exhausted', s.allowance_exhausted ? 'yes (new opens/resumes are refused)' : 'no']);
132
+ for (const [k, v] of Object.entries(s))
133
+ if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
134
+ head.push([k.replace(/_/g, ' '), scalar(v)]);
135
+ let out = head.length ? kv(head) : '';
136
+ const meters = objectTable(s.meters, [
137
+ ['meter', 'METER'],
138
+ ['unit', 'UNIT'],
139
+ ['raw_quantity', 'RAW'],
140
+ ['billable_quantity', 'BILLABLE'],
141
+ ['included_quantity', 'INCLUDED'],
142
+ ['percent_used', 'USED'],
143
+ ['cap_state', 'CAP'],
144
+ ]);
145
+ if (meters)
146
+ out += `\n${meters}`;
147
+ const allowances = objectTable(s.allowances, [
148
+ ['key', 'ALLOWANCE'],
149
+ ['unit', 'UNIT'],
150
+ ['included', 'INCLUDED'],
151
+ ['used', 'USED'],
152
+ ['remaining', 'REMAINING'],
153
+ ['percent_used', 'USED %'],
154
+ ['enforcement', 'ENFORCEMENT'],
155
+ ['cap_state', 'CAP'],
156
+ ]);
157
+ if (allowances)
158
+ out += `\n${allowances}`;
159
+ return out || json(summary);
160
+ }
161
+ /** One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. */
162
+ export function errorText(e, hint) {
163
+ const meta = [`code=${e.code}`];
164
+ if (e.status !== undefined)
165
+ meta.push(`status=${e.status}`);
166
+ if (e.request_id)
167
+ meta.push(`request_id=${e.request_id}`);
168
+ if (e.operation_id)
169
+ meta.push(`operation_id=${e.operation_id}`);
170
+ if (e.source === 'cell')
171
+ meta.push('source=cell');
172
+ const reason = e.details && typeof e.details.reason === 'string' ? ` (${e.details.reason})` : '';
173
+ const issues = Array.isArray(e.details?.issues)
174
+ ? e.details.issues.filter(isObject).map((i) => ` ${scalar(i.path)}: ${scalar(i.message)}\n`).join('')
175
+ : '';
176
+ return `error: ${e.message}${reason} [${meta.join(' ')}]\n${issues}${hint ? `hint: ${hint}\n` : ''}`;
177
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The fetch given to the SDK: the runtime's fetch with `Connection: close` on
3
+ * every request (API and cell gateway).
4
+ *
5
+ * Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
6
+ * sent on a reused keep-alive connection until an unrelated timer fires (up to
7
+ * ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
8
+ * with bare fetch against both node:http and Fastify servers; Node 22 (undici
9
+ * 6.28) and `Connection: close` do not stall. A fresh connection per request
10
+ * costs one TCP/TLS handshake, which is negligible for these call rates.
11
+ * Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
12
+ */
13
+ export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch): typeof fetch;
package/dist/http.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The fetch given to the SDK: the runtime's fetch with `Connection: close` on
3
+ * every request (API and cell gateway).
4
+ *
5
+ * Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
6
+ * sent on a reused keep-alive connection until an unrelated timer fires (up to
7
+ * ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
8
+ * with bare fetch against both node:http and Fastify servers; Node 22 (undici
9
+ * 6.28) and `Connection: close` do not stall. A fresh connection per request
10
+ * costs one TCP/TLS handshake, which is negligible for these call rates.
11
+ * Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
12
+ */
13
+ export function makeFetch(env, base = fetch) {
14
+ if (env.SHARDFLUX_HTTP_KEEPALIVE === '1')
15
+ return base;
16
+ return (input, init) => {
17
+ const headers = new Headers(init?.headers);
18
+ headers.set('connection', 'close');
19
+ return base(input, { ...init, headers });
20
+ };
21
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @shardflux/cli — `shard`, the Shardflux command line on @shardflux/sdk.
3
+ * The executable is src/bin.ts; `run()` drives the same code in-process.
4
+ */
5
+ export { run, abortableSleep } from './main.js';
6
+ export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, topHelp } from './args.js';
7
+ export type { CommandSpec, OptionSpec, Parsed, PositionalSpec, Values } from './args.js';
8
+ export { HANDLERS, execExitCode, resolveWorkspace } from './commands.js';
9
+ export type { Ctx, Handler, Io, Writer } from './commands.js';
10
+ export { EXIT, AuthConfigError, InterruptedError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
11
+ export type { ErrorInfo } from './errors.js';
12
+ export { errorText, json, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @shardflux/cli — `shard`, the Shardflux command line on @shardflux/sdk.
3
+ * The executable is src/bin.ts; `run()` drives the same code in-process.
4
+ */
5
+ export { run, abortableSleep } from "./main.js";
6
+ export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, topHelp } from "./args.js";
7
+ export { HANDLERS, execExitCode, resolveWorkspace } from "./commands.js";
8
+ export { EXIT, AuthConfigError, InterruptedError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
9
+ export { errorText, json, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
package/dist/main.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Io } from './commands.js';
2
+ /** A sleep that ends early when the signal aborts (so Ctrl-C never waits out a poll interval). */
3
+ export declare function abortableSleep(signal: AbortSignal): (ms: number) => Promise<void>;
4
+ export declare function run(argv: readonly string[], io: Io, opts?: {
5
+ signal?: AbortSignal;
6
+ }): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `shard` entry: parse, build the SDK client from the environment, run the
3
+ * command, report errors (stderr, with the API error code and request id) and
4
+ * return the exit code. In-process and side-effect free apart from `io`, so
5
+ * the whole CLI can be driven by tests; bin.ts wires it to the real process.
6
+ */
7
+ import { SDK_VERSION, Shardflux } from '@shardflux/sdk';
8
+ import { CLI_VERSION, parseCommandLine, resolveApiUrl } from "./args.js";
9
+ import { HANDLERS } from "./commands.js";
10
+ import { AuthConfigError, EXIT, InterruptedError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
11
+ import { errorText, json } from "./format.js";
12
+ import { makeFetch } from "./http.js";
13
+ const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
14
+ // Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
15
+ const isLabel = (s) => s.length >= 1 && s.length <= 100 && ![...s].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f);
16
+ /** A sleep that ends early when the signal aborts (so Ctrl-C never waits out a poll interval). */
17
+ export function abortableSleep(signal) {
18
+ return (ms) => new Promise((resolve) => {
19
+ if (signal.aborted)
20
+ return resolve();
21
+ const done = () => {
22
+ clearTimeout(timer);
23
+ signal.removeEventListener('abort', done);
24
+ resolve();
25
+ };
26
+ const timer = setTimeout(done, ms);
27
+ signal.addEventListener('abort', done, { once: true });
28
+ });
29
+ }
30
+ function hintFor(info) {
31
+ if (info.code === 'timeout' && info.operation_id)
32
+ return `the operation continues server side; keep waiting with: shard operations wait ${info.operation_id}`;
33
+ if (info.code === 'interrupted' && info.operation_id)
34
+ return `keep waiting with: shard operations wait ${info.operation_id}`;
35
+ if (info.code === 'unauthenticated' && info.status === undefined)
36
+ return 'export SHARDFLUX_API_KEY=<project API key> (create one in the console under Project > API keys)';
37
+ if (info.code === 'usage')
38
+ return 'run "shard --help" or "shard <command> --help"';
39
+ if (info.source === 'cell' && info.code === 'workspace_not_running')
40
+ return 'open or resume the workspace first: shard workspaces open <key> --template <slug>';
41
+ return undefined;
42
+ }
43
+ export async function run(argv, io, opts = {}) {
44
+ const signal = opts.signal ?? new AbortController().signal;
45
+ const rawKey = io.env.SHARDFLUX_API_KEY;
46
+ const wantsJson = argv.slice(0, argv.includes('--') ? argv.indexOf('--') : argv.length).includes('--json');
47
+ const report = (err) => {
48
+ const code = exitCodeFor(err, signal.aborted);
49
+ const info = describeError(err);
50
+ if (code === EXIT.interrupted && info.code !== 'interrupted') {
51
+ info.code = 'interrupted';
52
+ info.message = 'Interrupted.';
53
+ }
54
+ info.message = redact(info.message, rawKey);
55
+ io.stderr.write(redact(wantsJson ? json({ error: info }) : errorText(info, hintFor(info)), rawKey));
56
+ return code;
57
+ };
58
+ let parsed;
59
+ try {
60
+ parsed = parseCommandLine(argv);
61
+ }
62
+ catch (err) {
63
+ return report(err);
64
+ }
65
+ if (parsed.kind === 'help') {
66
+ io.stdout.write(parsed.text);
67
+ return EXIT.ok;
68
+ }
69
+ if (parsed.kind === 'version') {
70
+ io.stdout.write(wantsJson ? json({ cli: CLI_VERSION, sdk: SDK_VERSION }) : `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n`);
71
+ return EXIT.ok;
72
+ }
73
+ let ctx;
74
+ try {
75
+ const apiUrl = resolveApiUrl(parsed.global.apiUrl, io.env.SHARDFLUX_API_URL);
76
+ const agentLabel = parsed.global.agentLabel ?? io.env.SHARDFLUX_AGENT_LABEL ?? 'cli';
77
+ if (!isLabel(agentLabel))
78
+ throw new UsageError('--agent-label must be 1-100 printable characters');
79
+ let cloud;
80
+ ctx = {
81
+ io,
82
+ json: parsed.global.json,
83
+ apiUrl,
84
+ agentLabel,
85
+ signal,
86
+ cloud() {
87
+ if (cloud)
88
+ return cloud;
89
+ const key = rawKey?.trim();
90
+ if (!key)
91
+ throw new AuthConfigError('SHARDFLUX_API_KEY is not set.');
92
+ if (!KEY_SHAPE.test(key))
93
+ throw new AuthConfigError('SHARDFLUX_API_KEY is not a Shardflux project key (expected sfk_<key id>_<secret>).');
94
+ cloud = new Shardflux({ apiKey: key, baseUrl: apiUrl, userAgent: `shard-cli/${CLI_VERSION} shardflux-sdk-ts/${SDK_VERSION}`, fetch: makeFetch(io.env), sleep: abortableSleep(signal) });
95
+ return cloud;
96
+ },
97
+ };
98
+ }
99
+ catch (err) {
100
+ return report(err);
101
+ }
102
+ const handler = HANDLERS[parsed.command.path.join(' ')];
103
+ if (!handler)
104
+ return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
105
+ const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
106
+ // On Ctrl-C give the command a moment to clean up (exec sends a cancel), then stop regardless.
107
+ const interrupted = new Promise((resolve) => {
108
+ const onAbort = () => setTimeout(() => resolve({ interrupted: true }), 4_000).unref();
109
+ if (signal.aborted)
110
+ onAbort();
111
+ else
112
+ signal.addEventListener('abort', onAbort, { once: true });
113
+ });
114
+ const outcome = await Promise.race([running, interrupted]);
115
+ if ('interrupted' in outcome)
116
+ return report(new InterruptedError('Interrupted.'));
117
+ if ('err' in outcome)
118
+ return report(outcome.err);
119
+ return outcome.code;
120
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@shardflux/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "shard: the Shardflux command line. Open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://shardflux.dev",
8
+ "keywords": [
9
+ "shardflux",
10
+ "cli",
11
+ "shard",
12
+ "agents",
13
+ "ai-agents",
14
+ "sandbox",
15
+ "workspace",
16
+ "microvm"
17
+ ],
18
+ "engines": {
19
+ "node": ">=24"
20
+ },
21
+ "bin": {
22
+ "shard": "./dist/bin.js"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./bin": "./dist/bin.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "@shardflux/sdk": "^0.5.0"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/js": "10.0.1",
45
+ "@types/node": "24.13.6",
46
+ "eslint": "10.11.0",
47
+ "typescript": "5.9.3",
48
+ "typescript-eslint": "8.70.1"
49
+ },
50
+ "scripts": {
51
+ "build": "node scripts/build.mjs",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "lint": "eslint .",
54
+ "test": "node --test test/*.test.ts"
55
+ },
56
+ "main": "./dist/index.js",
57
+ "types": "./dist/index.d.ts"
58
+ }