@shardflux/cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,7 +3,25 @@
3
3
  Every command and option the README shows is available from the version named here. Below 1.0, a minor release may
4
4
  break compatibility; breaking changes are marked **Breaking**.
5
5
 
6
- ## 0.3.0 (unreleased)
6
+ ## 0.3.1 (not yet published; npm `latest` is 0.3.0)
7
+
8
+ Needs `@shardflux/sdk` 0.6.2 or later.
9
+
10
+ The API no longer lets a start (open, resume, fork) wait in `capacity_pending` forever: one that no host could admit
11
+ 15 minutes after it began fails with `capacity_unavailable` (retryable; nothing was started, a suspended workspace
12
+ stays suspended).
13
+
14
+ - A failed operation's error has `retryable` (the operation error's own flag): `retryable=true|false` in the error line,
15
+ `"retryable"` in the `--json` error document, next to `details.error_code`. `details.reason` carries the operation
16
+ error's reason (for `capacity_unavailable`, why no host fit, e.g. `no_ready_hosts`), printed after the message.
17
+ - A retryable failure gets a hint to retry later. Exit codes are unchanged (6 for any failed operation); `shard` does
18
+ not retry.
19
+ - A `--timeout` that ends while the start waits for capacity names the time it gives up (the SDK's message).
20
+ - Help and README: `--timeout` and exit codes 5 and 6 describe the 15-minute limit.
21
+ - `shard workspaces get` names the origin of a template version test instance (`template <id> version <n>`, contracts
22
+ §24.6) instead of reading it as a draft state.
23
+
24
+ ## 0.3.0 (2026-09-28)
7
25
 
8
26
  Needs `@shardflux/sdk` 0.6.0 or later.
9
27
 
package/README.md CHANGED
@@ -106,6 +106,21 @@ never runs twice, because the workspace refused it before doing anything.
106
106
  is still queued after 120000 ms`). The operation continues server side:
107
107
  `shard operations wait <id>` keeps waiting.
108
108
  - A resume or open that fails exits 6.
109
+
110
+ ## Starts that wait for capacity
111
+
112
+ An open, resume or fork that no host can admit yet waits in `capacity_pending` for at most 15
113
+ minutes. If `--timeout` ends first, the timeout error names the time it gives up. A start still
114
+ pending then fails with `capacity_unavailable`: nothing was started, and a suspended
115
+ workspace stays suspended. `shard` exits 6 as for any failed operation, and the error says
116
+ `retryable=true` (with `--json`, `"retryable": true`) with a hint to retry later:
117
+
118
+ ```text
119
+ error: Operation 01a0df37-b3a9-7a14-a3e7-11395c349c39 (open) failed: capacity_unavailable (no_ready_hosts) [code=operation_failed operation_id=01a0df37-b3a9-7a14-a3e7-11395c349c39 retryable=true]
120
+ hint: no host could admit it before its deadline; nothing was started. Retry later.
121
+ ```
122
+
123
+ `shard` does not retry it for you. A definitive failure has `retryable=false`.
109
124
  - `--no-wake` (or `SHARDFLUX_NO_WAKE=1`) turns this off. A workspace that is not running is then
110
125
  refused at once (exit 1, `workspace_not_running`).
111
126
 
@@ -179,7 +194,7 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
179
194
 
180
195
  - **Human (default):** aligned tables and key/value blocks on stdout. Errors go to stderr as
181
196
  `error: <message> (<reason>) [code=... status=... request_id=... operation_id=...]`, with a
182
- hint when there is one.
197
+ hint when there is one. A failed operation adds `retryable=true|false` (0.3.1+).
183
198
  - **`--json`:** stdout carries the API's own objects:
184
199
  - `workspaces open`/`get` print the workspace view; `list` prints `{data, next_cursor}`.
185
200
  - Lifecycle commands print the operation; `fork` prints `{operation, workspace}`.
@@ -190,7 +205,9 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
190
205
  print `{data, next_cursor}`; `rotate` prints `{secret, version}`; `delete` prints
191
206
  `{deleted, id, name, scope}`; `workspaces secrets` prints `{workspace_id, names, secrets}`.
192
207
 
193
- Errors go to stderr as `{"error": {code, message, status, request_id, ...}}`.
208
+ Errors go to stderr as `{"error": {code, message, status, request_id, ...}}`. A failed operation is
209
+ `{"error": {"code": "operation_failed", "message", "operation_id", "retryable", "details": {"error_code",
210
+ "reason"}, "operation"}}`; `retryable` (0.3.1+) is the operation error's own flag.
194
211
  - **`--timing`:** the timing goes to stderr after the output. With `--json` it is a `timing` field instead
195
212
  (see "Timing").
196
213
 
@@ -203,8 +220,8 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
203
220
  | 2 | usage error: bad arguments, or input the API rejected as `validation_failed` |
204
221
  | 3 | not found: workspace, operation or route |
205
222
  | 4 | authentication or authorization: missing, malformed, revoked or insufficient key |
206
- | 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`) |
207
- | 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace) |
223
+ | 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`), a start waiting for capacity for at most 15 minutes |
224
+ | 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace). `retryable=true` in the error means it may succeed later (`capacity_unavailable`) |
208
225
  | 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. |
209
226
  | exec | the command's own code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |
210
227
 
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.3.0";
1
+ export declare const CLI_VERSION = "0.3.1";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
package/dist/args.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { parseArgs } from 'node:util';
12
12
  import { UsageError } from "./errors.js";
13
- export const CLI_VERSION = '0.3.0';
13
+ export const CLI_VERSION = '0.3.1';
14
14
  export const GLOBAL_OPTIONS = {
15
15
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
16
16
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -38,7 +38,7 @@ const WAKE_TIMING = {
38
38
  };
39
39
  const WAIT = {
40
40
  wait: { type: 'boolean', description: 'Wait until the operation finishes.' },
41
- timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m). The operation continues server side.' },
41
+ timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m). The operation continues server side (a start waiting for capacity until its deadline, 15 minutes after it began; then it fails with capacity_unavailable).' },
42
42
  ...TIMING,
43
43
  };
44
44
  const REF = { name: 'id|key', required: true };
@@ -111,7 +111,7 @@ export const COMMANDS = [
111
111
  },
112
112
  ...CAPS,
113
113
  wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
114
- timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m). The start continues server side.' },
114
+ timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m). The start continues server side; one waiting for capacity gives up 15 minutes after it began (capacity_unavailable, retryable).' },
115
115
  ...TIMING,
116
116
  },
117
117
  examples: [
@@ -691,7 +691,7 @@ export const EXIT_CODE_HELP = [
691
691
  ' 3 not found (workspace, operation or route)',
692
692
  ' 4 authentication/authorization (missing, malformed, revoked or insufficient key)',
693
693
  ' 5 timeout (waiting gave up; the operation continues server side)',
694
- ' 6 operation failed or canceled',
694
+ ' 6 operation failed or canceled (retryable=true in the error: it may succeed later, e.g. capacity_unavailable)',
695
695
  ' 130 interrupted (Ctrl-C)',
696
696
  ' exec: the command’s own exit code (124 if it timed out, 128+N if killed by signal N)',
697
697
  ];
package/dist/errors.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare const EXIT: {
10
10
  readonly auth: 4;
11
11
  /** Waiting ran out of time (the operation continues server side), or a request timed out. */
12
12
  readonly timeout: 5;
13
- /** The awaited operation ended `failed` or `canceled`. */
13
+ /** The awaited operation ended `failed` or `canceled` (the error's `retryable` says whether a retry may succeed). */
14
14
  readonly operationFailed: 6;
15
15
  /** SIGINT (Ctrl-C). */
16
16
  readonly interrupted: 130;
package/dist/errors.js CHANGED
@@ -16,7 +16,7 @@ export const EXIT = {
16
16
  auth: 4,
17
17
  /** Waiting ran out of time (the operation continues server side), or a request timed out. */
18
18
  timeout: 5,
19
- /** The awaited operation ended `failed` or `canceled`. */
19
+ /** The awaited operation ended `failed` or `canceled` (the error's `retryable` says whether a retry may succeed). */
20
20
  operationFailed: 6,
21
21
  /** SIGINT (Ctrl-C). */
22
22
  interrupted: 130,
@@ -69,7 +69,12 @@ export function describeError(err) {
69
69
  if (err instanceof OperationTimeoutError)
70
70
  return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState };
71
71
  if (err instanceof OperationFailedError) {
72
- return { code: 'operation_failed', message: err.message, operation_id: err.operationId, ...(err.errorCode ? { details: { error_code: err.errorCode } } : {}), operation: err.operation };
72
+ // retryable: the operation error's own flag (true for capacity_unavailable: nothing was started, retry later).
73
+ // details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
74
+ const opDetails = err.operation.error?.details;
75
+ const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
76
+ const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
77
+ return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
73
78
  }
74
79
  if (err instanceof UsageError)
75
80
  return { code: 'usage', message: err.message };
package/dist/format.d.ts CHANGED
@@ -36,7 +36,10 @@ export declare function bindingsText(b: WorkspaceSecretBindings): string;
36
36
  * freshness, then meters and allowances as tables. Columns absent from the response are omitted.
37
37
  */
38
38
  export declare function usageText(summary: unknown): string;
39
- /** One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. */
39
+ /**
40
+ * One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. A failed
41
+ * operation also shows `retryable=true|false`.
42
+ */
40
43
  export declare function errorText(e: ErrorInfo, hint?: string): string;
41
44
  export declare function templateTable(rows: readonly TemplateSummary[]): string;
42
45
  /** Permission bits as the four-digit octal `ls -l` users read (e.g. 0755). */
package/dist/format.js CHANGED
@@ -67,6 +67,17 @@ export function workspaceTable(ws) {
67
67
  export function isReady(w) {
68
68
  return w.observed_state === 'running' && w.desired_state === 'running' && w.deleted_at === null;
69
69
  }
70
+ /** A workspace's origin: a fork, a draft state (draft test instance) or a template version (version test instance). */
71
+ function originText(o) {
72
+ switch (o.kind) {
73
+ case 'fork':
74
+ return `fork of ${o.workspace_id}`;
75
+ case 'draft_state':
76
+ return `draft state ${o.checkpoint_id ?? '(capturing)'} of ${o.draft_workspace_id}`;
77
+ default:
78
+ return `template ${o.template_id} version ${o.version}`;
79
+ }
80
+ }
70
81
  export function workspaceDetail(w) {
71
82
  const c = w.ceilings;
72
83
  return kv([
@@ -87,7 +98,7 @@ export function workspaceDetail(w) {
87
98
  ['lifetime', w.lifetime === 'session' ? `session (ends after ${w.idle_timeout_seconds ?? '-'} s idle, or on close)` : 'persistent'],
88
99
  ['purpose', w.purpose],
89
100
  ['disk layout', w.disk_layout],
90
- ...(w.origin ? [['origin', w.origin.kind === 'fork' ? `fork of ${w.origin.workspace_id}` : `draft state ${w.origin.checkpoint_id ?? '(capturing)'} of ${w.origin.draft_workspace_id}`]] : []),
101
+ ...(w.origin ? [['origin', originText(w.origin)]] : []),
91
102
  ...(w.ended_reason ? [['ended', w.ended_reason]] : []),
92
103
  ]
93
104
  : []),
@@ -212,7 +223,10 @@ export function usageText(summary) {
212
223
  out += `\n${allowances}`;
213
224
  return out || json(summary);
214
225
  }
215
- /** One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. */
226
+ /**
227
+ * One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. A failed
228
+ * operation also shows `retryable=true|false`.
229
+ */
216
230
  export function errorText(e, hint) {
217
231
  const meta = [`code=${e.code}`];
218
232
  if (e.status !== undefined)
@@ -223,6 +237,8 @@ export function errorText(e, hint) {
223
237
  meta.push(`operation_id=${e.operation_id}`);
224
238
  if (e.source === 'cell')
225
239
  meta.push('source=cell');
240
+ if (e.code === 'operation_failed' && e.retryable !== undefined)
241
+ meta.push(`retryable=${e.retryable}`);
226
242
  const reason = e.details && typeof e.details.reason === 'string' ? ` (${e.details.reason})` : '';
227
243
  const issues = Array.isArray(e.details?.issues)
228
244
  ? e.details.issues.filter(isObject).map((i) => ` ${scalar(i.path)}: ${scalar(i.message)}\n`).join('')
package/dist/main.js CHANGED
@@ -31,6 +31,9 @@ export function abortableSleep(signal) {
31
31
  function hintFor(info) {
32
32
  if (info.code === 'timeout' && info.operation_id)
33
33
  return `the operation continues server side; keep waiting with: shard operations wait ${info.operation_id}`;
34
+ if (info.code === 'operation_failed' && info.retryable) {
35
+ return info.details?.error_code === 'capacity_unavailable' ? 'no host could admit it before its deadline; nothing was started. Retry later.' : 'the operation may succeed if you retry it later.';
36
+ }
34
37
  if (info.code === 'interrupted' && info.operation_id)
35
38
  return `keep waiting with: shard operations wait ${info.operation_id}`;
36
39
  if (info.code === 'unauthenticated' && info.status === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@shardflux/sdk": "^0.6.1"
42
+ "@shardflux/sdk": "^0.6.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@eslint/js": "10.0.1",