@shardflux/cli 0.6.1 → 0.7.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/args.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { parseArgs } from 'node:util';
13
13
  import { ACCOUNT_COMMANDS } from "./account-args.js";
14
14
  import { UsageError } from "./errors.js";
15
- export const CLI_VERSION = '0.6.1';
15
+ export const CLI_VERSION = '0.7.0';
16
16
  export const GLOBAL_OPTIONS = {
17
17
  'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
18
18
  json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
@@ -38,7 +38,7 @@ const TIMING = {
38
38
  const WAKE_TIMING = {
39
39
  timing: { type: 'boolean', description: 'Print where the time went when the command had to wake the workspace (stderr; with --json a "timing" field, null without a wake).' },
40
40
  };
41
- /** `--if-revision` (0.5.0) of the mutating files commands: the tree revision a file-first write applies to (contracts §29.8). */
41
+ /** `--if-revision` (0.5.0) of the mutating files commands: the tree revision a file-first write applies to. */
42
42
  const IF_REVISION = {
43
43
  'if-revision': {
44
44
  type: 'string',
@@ -58,7 +58,7 @@ const PAGE = {
58
58
  limit: { type: 'string', value: '<n>', description: 'Page size.' },
59
59
  cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
60
60
  };
61
- /** Options of a version produced by save-as-template or a draft publish (contracts §19.8). */
61
+ /** Options of a version produced by save-as-template or a draft publish. */
62
62
  const SAVE = {
63
63
  description: { type: 'string', value: '<text>', description: 'Version description (default: the source version’s).' },
64
64
  'default-lifetime': { type: 'string', value: '<persistent|session>', description: 'Default lifetime of workspaces opened from the new version.' },
@@ -68,7 +68,7 @@ const SAVE = {
68
68
  wait: { type: 'boolean', description: 'Wait until the build is published and registered (or fails).' },
69
69
  timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 30m). The build continues server side.' },
70
70
  };
71
- /** Open-time text inputs of a template version (0.4.0; contracts §24.3). */
71
+ /** Open-time text inputs of a template version (0.4.0). */
72
72
  const INPUTS = {
73
73
  input: {
74
74
  type: 'string',
@@ -93,12 +93,12 @@ const FEEDBACK_HELP = [
93
93
  * MCP server's send_feedback tool. It has to make an agent actually send feedback while it works.
94
94
  */
95
95
  const FEEDBACK_WHY = [
96
- 'Sends your message straight to the Shardflux founder, who reads every one. Agents: use it actively while you work,',
96
+ 'Sends your message straight to the Shardflux team, who read every one. Agents: use it actively while you work,',
97
97
  'not once at the end. Send a short note the moment:',
98
98
  '- something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround;',
99
- '- your user complains or is frustrated about Shardflux or the workflow around it (slowness, cost or limits,',
100
- ' confusing setup, a missing feature, a workaround): paraphrase it, with what they were trying to do;',
101
- '- your user asked for something Shardflux could not do, or made awkward.',
99
+ '- your user asks for a capability, an option or a smoother workflow: paraphrase it, with what they were trying',
100
+ ' to do;',
101
+ '- your user has an idea for how Shardflux could fit their work better.',
102
102
  'Short and frequent beats polished; include the request id and error code (a failed shard command prints a',
103
103
  'ready-made "feedback:" line to fill in). No need to ask first, but tell your user you sent it. Leave out private',
104
104
  'data about the user and secrets or code they did not mean to share; paraphrase, never paste transcripts.',
@@ -138,7 +138,7 @@ const KEY_COMMANDS = [
138
138
  },
139
139
  {
140
140
  path: ['feedback'],
141
- summary: 'Tell the Shardflux founder what failed, confused you or is missing (agents: as you work)',
141
+ summary: 'Tell the Shardflux team what failed, confused you or is missing (agents: as you work)',
142
142
  description: [
143
143
  ...FEEDBACK_WHY,
144
144
  '',
@@ -154,15 +154,14 @@ const KEY_COMMANDS = [
154
154
  options: {
155
155
  category: { type: 'string', value: '<bug|confusing|missing|idea|praise|other>', description: 'Kind of feedback (default other; see Categories).' },
156
156
  workspace: { type: 'string', value: '<id|key>', description: 'The workspace it is about.' },
157
- 'request-id': { type: 'string', value: '<id>', description: 'request_id from the error, so the founder can find the logs.' },
158
- 'error-code': { type: 'string', value: '<code>', description: 'The error code you saw, e.g. capacity_unavailable.' },
157
+ 'request-id': { type: 'string', value: '<id>', description: 'request_id from the error, so the team can find the logs.' },
158
+ 'error-code': { type: 'string', value: '<code>', description: 'The error code you saw, e.g. template_not_found.' },
159
159
  command: { type: 'string', value: '<text>', description: 'What you ran, e.g. "shard ws exec acme/demo -- npm test".' },
160
160
  'from-file': { type: 'string', value: '<file|->', description: 'Read the message from this file, or - for standard input (long reports, logs); instead of <message...>.' },
161
161
  },
162
162
  examples: [
163
- 'shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code capacity_unavailable "ws open failed twice in 10 min; expected a start within a minute"',
164
- 'shard feedback --category confusing --command "shard ws exec acme/demo -- npm test" exit 137 with no message, took a while to see it was the memory cap',
165
- 'shard feedback --category missing no way to copy a file between two workspaces, piped files read into files write instead',
163
+ 'shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code template_not_found "ws open --template python-node failed; expected a suggestion of the closest slug"',
164
+ 'shard feedback --category idea "user would like shard ws open to remember the last template they used"',
166
165
  'npm test 2>&1 | tail -n 60 | shard feedback --category bug --workspace acme/demo --from-file -',
167
166
  ],
168
167
  },
@@ -190,6 +189,8 @@ const KEY_COMMANDS = [
190
189
  positionals: [{ name: 'key', required: true }],
191
190
  options: {
192
191
  template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
192
+ labels: { type: 'string', value: '<json>', description: 'Replace workspace labels with a JSON object of strings.' },
193
+ 'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
193
194
  secret: {
194
195
  type: 'string',
195
196
  multiple: true,
@@ -229,6 +230,7 @@ const KEY_COMMANDS = [
229
230
  options: {
230
231
  state: { type: 'string', value: '<state>', description: 'Observed state filter (running, suspended, creating, ...).' },
231
232
  prefix: { type: 'string', value: '<key-prefix>', description: 'Only keys starting with this prefix.' },
233
+ labels: { type: 'string', value: '<json>', description: 'Exact label matches (JSON object; all pairs must match).' },
232
234
  ...LIFETIME_FILTER,
233
235
  'include-deleted': { type: 'boolean', description: 'Include deleted workspaces and ended sessions (tombstones).' },
234
236
  all: { type: 'boolean', description: 'Fetch every page.' },
@@ -236,12 +238,28 @@ const KEY_COMMANDS = [
236
238
  cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
237
239
  },
238
240
  },
241
+ { path: ['workspaces', 'labels'], summary: 'Read or replace workspace labels', positionals: [REF], options: {
242
+ set: { type: 'string', value: '<json>', description: 'Replace all labels; {} clears them.' },
243
+ } },
244
+ { path: ['workspaces', 'idle'], summary: 'Inspect idle status, set policy or declare a keepalive', positionals: [REF], options: {
245
+ policy: { type: 'string', value: '<policy>', description: 'adaptive, never, fixed:<seconds>, or default to clear the override.' },
246
+ keepalive: { type: 'string', value: '<seconds>', description: 'Declare ongoing work for this many seconds.' },
247
+ } },
248
+ { path: ['workspaces', 'exec-start'], summary: 'Start a background exec session with optional pipe stdin', positionals: [REF, { name: 'command', required: true, variadic: true }], options: {
249
+ 'stdin-open': { type: 'boolean', description: 'Keep pipe stdin open for exec-input.' },
250
+ 'session-id': { type: 'string', value: '<id>', description: 'Caller-chosen id to deduplicate starts.' },
251
+ cwd: { type: 'string', value: '<dir>', description: 'Absolute working directory.' },
252
+ } },
253
+ { path: ['workspaces', 'exec-input'], summary: 'Write pipe stdin at its acknowledged byte offset', positionals: [REF, { name: 'session-id', required: true }, { name: 'data' }], options: {
254
+ offset: { type: 'string', value: '<bytes>', description: 'Required acknowledged offset (0 initially).' },
255
+ close: { type: 'boolean', description: 'Send EOF after these bytes are accepted.' },
256
+ } },
239
257
  { path: ['workspaces', 'get'], summary: 'Show one workspace (state, template, startup)', positionals: [REF], options: {} },
240
258
  { path: ['workspaces', 'inputs'], summary: 'Show the text inputs of a workspace (secret inputs are bound secrets: shard ws secrets)', positionals: [REF], options: {} },
241
259
  {
242
260
  path: ['workspaces', 'exec'],
243
261
  summary: 'Run a command in a workspace; prints its output and exits with its exit code',
244
- description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution cannot be canceled: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
262
+ description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution runs to completion: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
245
263
  positionals: [REF, { name: 'command', required: true, variadic: true }],
246
264
  options: {
247
265
  cwd: { type: 'string', value: '<dir>', description: 'Working directory, an absolute path (default /home/user; under /home/user on a file-first workspace). A relative path is refused (invalid_cwd).' },
@@ -270,15 +288,16 @@ const KEY_COMMANDS = [
270
288
  },
271
289
  {
272
290
  path: ['workspaces', 'suspend'],
273
- summary: 'Suspend a running workspace (durable checkpoint), now or once it has been idle',
274
- description: 'Without options the suspend is requested now. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
291
+ summary: 'Suspend a running workspace (memory and processes checkpointed), now or once it has been idle',
292
+ description: 'Without options the suspend is requested now. With --wait it returns as soon as the workspace is sealed on its host, typically in a few hundred ms; the result\'s durable turns true when the copy lands in durable storage, typically within a second. --durable (0.7.0+) waits for that too. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
275
293
  positionals: [REF],
276
294
  options: {
277
295
  'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (0.5.1+; 0 to 1h, e.g. 0, 30s, 2m; 0 = as soon as it is idle, 0.6.1+). Cannot be combined with --wait.' },
278
296
  'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
297
+ durable: { type: 'boolean', description: 'Wait until the suspend is in durable storage (result durable: true; 0.7.0+). Implies --wait and shares its --timeout.' },
279
298
  ...WAIT,
280
299
  },
281
- examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
300
+ examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --durable', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
282
301
  },
283
302
  { path: ['workspaces', 'resume'], summary: 'Resume a suspended workspace', positionals: [REF], options: { ...WAIT } },
284
303
  {
@@ -519,7 +538,7 @@ const KEY_COMMANDS = [
519
538
  {
520
539
  path: ['templates', 'init'],
521
540
  summary: 'Write a template.yaml that starts from a base template',
522
- description: 'The file is the recipe v2 document (contracts §24.1): languages, packages, files (local "from" paths are uploaded by "shard templates build"), build steps, and the settings a workspace gets when it opens (env, inputs, start commands, services, defaults). Edit it, then build it.',
541
+ description: 'The file is the recipe v2 document: languages, packages, files (local "from" paths are uploaded by "shard templates build"), build steps, and the settings a workspace gets when it opens (env, inputs, start commands, services, defaults). Edit it, then build it.',
523
542
  positionals: [],
524
543
  options: {
525
544
  base: { type: 'string', value: '<slug[@version]>', description: 'Base template (required), e.g. ubuntu-24.04; without @version, the version open resolves to now.' },
@@ -696,7 +715,11 @@ const KEY_COMMANDS = [
696
715
  path: ['operations', 'wait'],
697
716
  summary: 'Wait for an operation to finish (exit 0 succeeded, 6 failed/canceled, 5 timeout)',
698
717
  positionals: [{ name: 'operation-id', required: true }],
699
- options: { timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' }, ...TIMING },
718
+ options: {
719
+ timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' },
720
+ durable: { type: 'boolean', description: 'A suspend or fork: also wait until its copy is in durable storage (result durable: true; 0.7.0+).' },
721
+ ...TIMING,
722
+ },
700
723
  },
701
724
  ];
702
725
  /** Every command: the account plane first (sign in, set up), then the project-key commands. */
@@ -1058,8 +1081,8 @@ export function topHelp() {
1058
1081
  '',
1059
1082
  'Feedback: shard feedback [options] <message...>',
1060
1083
  ...FEEDBACK_WHY.map((l) => ` ${l}`),
1061
- ' shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 "ws open waited 40 s in capacity_pending; expected under 5 s"',
1062
- ' shard feedback --category confusing "user was frustrated that ws open needs a template slug they had to look up; they expected a default"',
1084
+ ' shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code template_not_found "ws open --template python-node failed; expected a suggestion of the closest slug"',
1085
+ ' shard feedback --category idea "user would like shard ws open to remember the last template they used"',
1063
1086
  '',
1064
1087
  'Start here (everything the web app does, from a terminal; a person only opens the emailed link and pays in Checkout):',
1065
1088
  ` printf '%s\\n' "$PASSWORD" | shard auth register --email you@example.com`,
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `--open` (0.5.0+): opens a URL in the default browser, best effort. The URL is always printed as well, so a
2
+ * `--open` (0.5.0+): opens a URL in the default browser when it can. The URL is always printed as well, so a
3
3
  * failure to open changes nothing: no error, no exit code. $BROWSER (a program run with the URL as its only argument)
4
4
  * wins; otherwise `open` (macOS), `rundll32 url.dll,FileProtocolHandler` (Windows) or `xdg-open` (elsewhere).
5
5
  */
@@ -70,13 +70,13 @@ export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promi
70
70
  export declare const str: (v: Values[string]) => string | undefined;
71
71
  export declare const strs: (v: Values[string]) => string[];
72
72
  export declare function out(ctx: Ctx, human: string, machine: unknown): void;
73
- /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds, contracts §20.6). */
73
+ /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
74
74
  export declare function whenIdleSeconds(text: string): number;
75
75
  export declare function waitTimeout(values: Values): number;
76
76
  /**
77
77
  * `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
78
78
  * lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
79
- * workspace over tombstones of ended sessions that had the same key (contracts §19.11).
79
+ * workspace over tombstones of ended sessions that had the same key.
80
80
  */
81
81
  export declare function resolveWorkspace(ctx: Ctx, ref: string): Promise<Workspace>;
82
82
  export declare function oneOf<T extends string>(values: Values, option: string, allowed: readonly T[]): T | undefined;
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, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, 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, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
20
20
  const DEFAULT_WAIT_MS = 300_000;
21
21
  const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
22
22
  const MAX_STDIN = 1024 * 1024;
@@ -30,10 +30,10 @@ export const str = (v) => (typeof v === 'string' ? v : undefined);
30
30
  const cellOf = (ctx, ws) => ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs, ...(ctx.wake ? {} : { wake: null }) });
31
31
  export const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
32
32
  const MAX_SECRET_VALUE_BYTES = 65_536;
33
- /** `files patch --content-file`: a patch request is at most 7 MiB (contracts §26.2); larger files go through `files write`. */
33
+ /** `files patch --content-file`: a patch request is at most 7 MiB; larger files go through `files write`. */
34
34
  const MAX_PATCH_CONTENT = 7 * 1024 * 1024;
35
35
  const STOPPED_AT = { max_matches: '--max matches', budget: 'its 10 s budget', max_bytes: '4 MiB of results' };
36
- /** A file revision (SHA-256 of the content) or `absent` (contracts §26.2). */
36
+ /** A file revision (SHA-256 of the content) or `absent`. */
37
37
  const REVISION = /^([0-9a-f]{64}|absent)$/;
38
38
  const TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
39
39
  function secretNames(names, option) {
@@ -66,7 +66,7 @@ function caps(values) {
66
66
  c.disk_gib = parsePositiveInt(disk, 'disk-gib', 1_000_000);
67
67
  return Object.keys(c).length ? c : undefined;
68
68
  }
69
- /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds, contracts §20.6). */
69
+ /** `--when-idle <duration>`: whole seconds from 0 (as soon as it is idle; 0.6.1) to 3600 (the API's after_seconds bounds). */
70
70
  export function whenIdleSeconds(text) {
71
71
  if (/^0+(?:\.0+)?(?:ms|s|m|h)?$/.test(text.trim()))
72
72
  return 0;
@@ -85,7 +85,7 @@ export function waitTimeout(values) {
85
85
  /**
86
86
  * `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
87
87
  * lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
88
- * workspace over tombstones of ended sessions that had the same key (contracts §19.11).
88
+ * workspace over tombstones of ended sessions that had the same key.
89
89
  */
90
90
  export async function resolveWorkspace(ctx, ref) {
91
91
  const cloud = ctx.cloud();
@@ -192,7 +192,7 @@ export function parseSlugVersion(text, what) {
192
192
  }
193
193
  const INPUT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
194
194
  /**
195
- * Open-time text inputs (contracts §24.3) from --input-file (NAME=value lines with # comments, or a JSON object of
195
+ * Open-time text inputs from --input-file (NAME=value lines with # comments, or a JSON object of
196
196
  * strings), then each --input NAME=value (a later one wins). Undefined when neither option is given.
197
197
  */
198
198
  async function inputsFrom(ctx, values) {
@@ -417,7 +417,7 @@ export function execExitCode(r) {
417
417
  async function bestEffortCancel(cell, sessionId) {
418
418
  await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
419
419
  }
420
- // ---- File-first workspaces (0.5.0; contracts §29) ----------------------------------------------------------------------
420
+ // ---- File-first workspaces (0.5.0) ----------------------------------------------------------------------
421
421
  const MODES = { processful: 'processful', 'file-first': 'file_first', file_first: 'file_first' };
422
422
  /** `--mode processful|file-first` (`file_first` too). */
423
423
  function modeFlag(values) {
@@ -452,7 +452,7 @@ function treeRevisionAfter(ws, cell) {
452
452
  return cell.treeRevision ?? ws.treeRevision;
453
453
  }
454
454
  /**
455
- * A lifecycle call a file-first workspace does not have (contracts §29.7: suspend, resume, fork, reset,
455
+ * A lifecycle call a file-first workspace does not have (suspend, resume, fork, reset,
456
456
  * save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
457
457
  * with its hint like the API's own refusal.
458
458
  */
@@ -496,7 +496,7 @@ function reportExecution(ctx, ref, r, opts) {
496
496
  }
497
497
  function lifecycleText(verb, w, op) {
498
498
  const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
499
- return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? '' : `wait with: shard operations wait ${op.id}\n`}`;
499
+ return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
500
500
  }
501
501
  /** The project-key commands; handlers.ts adds the account plane's. */
502
502
  export const KEY_HANDLERS = {
@@ -519,7 +519,7 @@ export const KEY_HANDLERS = {
519
519
  out(ctx, `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n${line}`, { cli: CLI_VERSION, sdk: SDK_VERSION, update });
520
520
  return 0;
521
521
  },
522
- // Feedback (0.5.0+): POST /v1/feedback, emailed to the founder. Validated here first so a bad call costs no request.
522
+ // Feedback (0.5.0+): POST /v1/feedback, emailed to the Shardflux team. Validated here first so a bad call costs no request.
523
523
  async feedback(ctx, values, words) {
524
524
  const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
525
525
  const context = feedbackContext(ctx, values);
@@ -527,7 +527,7 @@ export const KEY_HANDLERS = {
527
527
  const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
528
528
  const human = r.duplicate
529
529
  ? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
530
- : `Sent to the Shardflux founder (feedback ${r.id}). Thank you.\n`;
530
+ : `Sent to the Shardflux team (feedback ${r.id}). Thank you.\n`;
531
531
  out(ctx, human, { id: r.id, received_at: r.receivedAt, duplicate: r.duplicate });
532
532
  return 0;
533
533
  },
@@ -565,6 +565,46 @@ export const KEY_HANDLERS = {
565
565
  out(ctx, usageText(summary), summary);
566
566
  return 0;
567
567
  },
568
+ async 'workspaces labels'(ctx, values, [ref]) {
569
+ const ws = await resolveWorkspace(ctx, ref);
570
+ const labels = labelMap(str(values.set));
571
+ if (labels !== undefined)
572
+ await ws.setLabels(labels);
573
+ out(ctx, JSON.stringify(ws.labels, null, 2) + '\n', ws.labels);
574
+ return 0;
575
+ },
576
+ async 'workspaces idle'(ctx, values, [ref]) {
577
+ const ws = await resolveWorkspace(ctx, ref);
578
+ const policy = str(values.policy);
579
+ const keepalive = str(values.keepalive);
580
+ if (policy !== undefined && keepalive !== undefined)
581
+ throw new UsageError('--policy and --keepalive cannot be combined');
582
+ let result;
583
+ if (policy !== undefined)
584
+ result = (await ws.setIdlePolicy(policy === 'default' ? null : policy)).data;
585
+ else if (keepalive !== undefined)
586
+ result = await ws.keepalive(parsePositiveInt(keepalive, 'keepalive', 86400));
587
+ else
588
+ result = await ws.idle(ctx.signal);
589
+ out(ctx, JSON.stringify(result, null, 2) + '\n', result);
590
+ return 0;
591
+ },
592
+ async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
593
+ const ws = await resolveWorkspace(ctx, ref);
594
+ const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
595
+ ...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}) }, ctx.signal);
596
+ out(ctx, JSON.stringify(session, null, 2) + '\n', session);
597
+ return 0;
598
+ },
599
+ async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
600
+ const offset = Number(str(values.offset));
601
+ if (str(values.offset) === undefined || !Number.isSafeInteger(offset) || offset < 0)
602
+ throw new UsageError('--offset must be a non-negative integer');
603
+ const ws = await resolveWorkspace(ctx, ref);
604
+ const ack = await cellOf(ctx, ws).exec.input(sessionId, data ?? '', { offset, close: values.close === true, signal: ctx.signal });
605
+ out(ctx, JSON.stringify(ack) + '\n', ack);
606
+ return 0;
607
+ },
568
608
  async 'workspaces open'(ctx, values, [key]) {
569
609
  const template = str(values.template);
570
610
  if (!template)
@@ -583,6 +623,8 @@ export const KEY_HANDLERS = {
583
623
  const ws = await waited(ctx, 'the open continues server side (open the key again to keep waiting).', () => cloud.workspaces.open({
584
624
  key: key,
585
625
  template,
626
+ ...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
627
+ ...(str(values['idle-policy']) ? { idlePolicy: str(values['idle-policy']) } : {}),
586
628
  ...(c ? { caps: c } : {}),
587
629
  ...(bind.length ? { secrets: bind } : {}),
588
630
  ...(inputs ? { inputs } : {}),
@@ -609,6 +651,7 @@ export const KEY_HANDLERS = {
609
651
  const params = {
610
652
  ...(state ? { state: state } : {}),
611
653
  ...(prefix ? { keyPrefix: prefix } : {}),
654
+ ...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
612
655
  ...(lifetime ? { lifetime } : {}),
613
656
  ...(purpose ? { purpose } : {}),
614
657
  ...(values['include-deleted'] === true ? { includeDeleted: true } : {}),
@@ -661,7 +704,7 @@ export const KEY_HANDLERS = {
661
704
  const cell = cellOf(ctx, ws);
662
705
  const cwd = str(values.cwd);
663
706
  if (ws.mode === 'file_first') {
664
- // An execution (contracts §29.8): answered when the command ended. The id is ours, so Ctrl-C can name it.
707
+ // An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
665
708
  const id = executionId ?? newExecutionId();
666
709
  let r;
667
710
  try {
@@ -677,7 +720,7 @@ export const KEY_HANDLERS = {
677
720
  }
678
721
  catch (err) {
679
722
  if (ctx.signal.aborted) {
680
- throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution cannot be canceled).`, undefined, {
723
+ throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution runs to completion).`, undefined, {
681
724
  executionId: id,
682
725
  hint: `fetch its result later with: shard executions get ${ref} ${id}`,
683
726
  });
@@ -854,8 +897,12 @@ export const KEY_HANDLERS = {
854
897
  async 'workspaces suspend'(ctx, values, [ref]) {
855
898
  const whenIdle = str(values['when-idle']);
856
899
  const cancel = values['cancel-when-idle'] === true;
857
- if (whenIdle === undefined && !cancel)
858
- return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o), ['suspend', 'Suspend']);
900
+ const durable = values.durable === true;
901
+ if (whenIdle === undefined && !cancel) {
902
+ return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, durable ? { ...o, durable: true, wait: o.wait ?? { timeoutMs: waitTimeout(values), signal: ctx.signal } } : o), ['suspend', 'Suspend']);
903
+ }
904
+ if (durable)
905
+ throw new UsageError(`${whenIdle !== undefined ? '--when-idle' : '--cancel-when-idle'} cannot be combined with --durable (nothing is suspended now)`);
859
906
  if (whenIdle !== undefined && cancel)
860
907
  throw new UsageError('--when-idle and --cancel-when-idle cannot be combined');
861
908
  const flag = cancel ? '--cancel-when-idle' : '--when-idle';
@@ -876,7 +923,7 @@ export const KEY_HANDLERS = {
876
923
  return 0;
877
924
  },
878
925
  async 'workspaces resume'(ctx, values, [ref]) {
879
- // With --wait the resume is held until the workspace runs (contracts §22.6); its tool token is the CLI's label.
926
+ // With --wait the resume is held until the workspace runs; its tool token is the CLI's label.
880
927
  return lifecycle(ctx, values, ref, 'Resuming', (ws, o) => ctx.cloud().workspaces.resume(ws.id, { ...o, agentLabel: ctx.agentLabel }), ['resume', 'Resume']);
881
928
  },
882
929
  async 'workspaces delete'(ctx, values, [ref]) {
@@ -1526,7 +1573,10 @@ export const KEY_HANDLERS = {
1526
1573
  async 'operations wait'(ctx, values, [id]) {
1527
1574
  if (!UUID.test(id))
1528
1575
  throw new UsageError(`<operation-id> must be a UUID (got "${id}")`);
1529
- const op = await waitOperation(ctx, id, waitTimeout(values));
1576
+ const timeoutMs = waitTimeout(values);
1577
+ const op = values.durable === true
1578
+ ? await waited(ctx, `operation ${id} continues server side.`, () => ctx.cloud().workspaces.waitForDurable(id, { timeoutMs, signal: ctx.signal }))
1579
+ : await waitOperation(ctx, id, timeoutMs);
1530
1580
  out(ctx, operationDetail(op), op);
1531
1581
  return 0;
1532
1582
  },
@@ -1681,3 +1731,17 @@ async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
1681
1731
  out(ctx, lifecycleText(verb, ws, op), op);
1682
1732
  return 0;
1683
1733
  }
1734
+ function labelMap(value) {
1735
+ if (value === undefined)
1736
+ return undefined;
1737
+ let parsed;
1738
+ try {
1739
+ parsed = JSON.parse(value);
1740
+ }
1741
+ catch {
1742
+ throw new UsageError('labels must be a JSON object of strings');
1743
+ }
1744
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.values(parsed).some((v) => typeof v !== 'string'))
1745
+ throw new UsageError('labels must be a JSON object of strings');
1746
+ return parsed;
1747
+ }
package/dist/errors.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare class CheckoutEndedError extends Error {
48
48
  }
49
49
  /**
50
50
  * The user pressed Ctrl-C; `operationId` is set when a server-side operation keeps running, `executionId` when a
51
- * file-first execution does (it cannot be canceled), with `hint` naming the command that fetches its result.
51
+ * file-first execution does (an execution runs to completion), with `hint` naming the command that fetches its result.
52
52
  */
53
53
  export declare class InterruptedError extends Error {
54
54
  name: string;
@@ -60,7 +60,7 @@ export declare class InterruptedError extends Error {
60
60
  hint?: string;
61
61
  });
62
62
  }
63
- /** A file-first execution that ended `failed` or `lost` (contracts §29.8): nothing was published. */
63
+ /** A file-first execution that ended `failed` or `lost`: nothing was published. */
64
64
  export declare class ExecutionFailedError extends Error {
65
65
  name: string;
66
66
  readonly result: ExecutionResult;
@@ -84,7 +84,7 @@ export interface ErrorInfo {
84
84
  retryable?: boolean;
85
85
  operation_id?: string;
86
86
  details?: Record<string, unknown>;
87
- source?: 'api' | 'cell';
87
+ source?: 'api' | 'cell' | 'unknown';
88
88
  last_state?: string;
89
89
  operation?: unknown;
90
90
  /** A file-first execution the error is about (failed, lost, still running, or left running by Ctrl-C). */
package/dist/errors.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * stderr with the API's error code, message and request id; the process exit
4
4
  * code tells scripts what kind of failure it was (README "Exit codes").
5
5
  */
6
- import { CheckoutTimeoutError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
6
+ import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
7
7
  export const EXIT = {
8
8
  ok: 0,
9
9
  /** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
@@ -59,7 +59,7 @@ export class CheckoutEndedError extends Error {
59
59
  }
60
60
  /**
61
61
  * The user pressed Ctrl-C; `operationId` is set when a server-side operation keeps running, `executionId` when a
62
- * file-first execution does (it cannot be canceled), with `hint` naming the command that fetches its result.
62
+ * file-first execution does (an execution runs to completion), with `hint` naming the command that fetches its result.
63
63
  */
64
64
  export class InterruptedError extends Error {
65
65
  name = 'InterruptedError';
@@ -73,7 +73,7 @@ export class InterruptedError extends Error {
73
73
  this.hint = more.hint;
74
74
  }
75
75
  }
76
- /** A file-first execution that ended `failed` or `lost` (contracts §29.8): nothing was published. */
76
+ /** A file-first execution that ended `failed` or `lost`: nothing was published. */
77
77
  export class ExecutionFailedError extends Error {
78
78
  name = 'ExecutionFailedError';
79
79
  result;
@@ -137,17 +137,24 @@ export function describeError(err) {
137
137
  source: err.source,
138
138
  };
139
139
  }
140
- if (err instanceof OperationTimeoutError)
141
- return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState };
140
+ if (err instanceof OperationTimeoutError) {
141
+ return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState, ...(err.durable ? { details: { durable: false, state: 'pending' } } : {}) };
142
+ }
143
+ // --durable (0.7.0+): the suspend succeeded; its durable copy could not be made (see the lifecycle reference).
144
+ if (err instanceof DurabilityLostError) {
145
+ return { code: 'durability_lost', message: err.message, operation_id: err.operationId, retryable: false, details: { state: 'lost', ...(err.durability.reason ? { reason: err.durability.reason } : {}), checkpoint_id: err.durability.checkpointId }, operation: err.operation };
146
+ }
142
147
  if (err instanceof OperationFailedError) {
143
148
  // retryable: the operation error's own flag (true for capacity_unavailable: nothing was started, retry later).
144
149
  // details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
145
150
  const opDetails = err.operation.error?.details;
146
151
  const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
147
152
  const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
148
- return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
153
+ // A suspend-when-idle canceled workspace_active (0.7.0+): the workspace was in use and keeps running.
154
+ const message = err.workspaceActive ? `${err.message}: the workspace was in use, so the suspend-when-idle was canceled; it keeps running` : err.message;
155
+ return { code: 'operation_failed', message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
149
156
  }
150
- // File-first executions (0.5.0; contracts §29.8): failed or lost like a failed operation (details.reason says why;
157
+ // File-first executions (0.5.0): failed or lost like a failed operation (details.reason says why;
151
158
  // retryable: a new execution id may succeed), still running like a wait that gave up.
152
159
  if (err instanceof ExecutionFailedError) {
153
160
  const e = err.result.error;
@@ -180,7 +187,7 @@ export function describeError(err) {
180
187
  return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
181
188
  }
182
189
  if (err instanceof ShardfluxProtocolError)
183
- return { code: 'protocol_error', message: err.message, status: err.status };
190
+ return { code: 'protocol_error', message: err.message, status: err.status, source: err.source };
184
191
  if (isNamed(err, 'TimeoutError'))
185
192
  return { code: 'timeout', message: 'The request timed out.' };
186
193
  if (isNamed(err, 'AbortError'))
@@ -205,7 +212,7 @@ export function exitCodeFor(err, interrupted = false) {
205
212
  return EXIT.notFound;
206
213
  if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
207
214
  return EXIT.timeout;
208
- if (err instanceof OperationFailedError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
215
+ if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
209
216
  return EXIT.operationFailed;
210
217
  if (err instanceof ExecutionPendingError)
211
218
  return EXIT.timeout;
package/dist/format.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Human-readable output (aligned tables, key/value blocks) and the error line.
3
- * `--json` output is the API's own objects (see README "JSON output"), so
4
- * these functions only shape what people read.
5
- */
6
1
  import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
7
2
  import type { ErrorInfo } from './errors.js';
8
3
  export declare function json(value: unknown): string;
@@ -22,13 +17,13 @@ export declare function workspaceRow(w: WorkspaceView): string[];
22
17
  export declare function workspaceTable(ws: readonly WorkspaceView[]): string;
23
18
  export declare function isReady(w: WorkspaceView): boolean;
24
19
  /**
25
- * The startup of a workspace's template version (contracts §24.4) in one line: state, trigger, and for a failure the
20
+ * The startup of a workspace's template version in one line: state, trigger, and for a failure the
26
21
  * step or service, exit code and reason. Null when the version has no start commands or services.
27
22
  */
28
23
  export declare function startupText(s: WorkspaceView['startup'] | undefined): string | null;
29
24
  /** The failed step's output tail, indented, after the detail block (empty otherwise). */
30
25
  export declare function startupTail(s: WorkspaceView['startup'] | undefined): string;
31
- /** A pending suspend-when-idle request in one line (0.5.1; contracts §20.6). */
26
+ /** A pending suspend-when-idle request in one line (0.5.1). */
32
27
  export declare function suspendRequestText(r: SuspendRequest): string;
33
28
  /** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
34
29
  export declare function suspendWhenIdleText(w: {
@@ -41,6 +36,11 @@ export declare function suspendWhenIdleText(w: {
41
36
  export declare function workspaceDetail(w: WorkspaceView): string;
42
37
  export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
43
38
  export declare function operationTable(ops: readonly Operation[]): string;
39
+ /**
40
+ * A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
41
+ * `durability`). Empty when the result has no durability block (stored durably before the operation completed).
42
+ */
43
+ export declare function durableText(op: Operation): string;
44
44
  export declare function operationDetail(op: Operation): string;
45
45
  export declare function sessionTable(sessions: readonly AgentSession[]): string;
46
46
  /** `files stat`: one path's metadata; the revision only when requested and returned. */
@@ -63,8 +63,8 @@ export declare function capMoney(minor: unknown, currency: string): string;
63
63
  */
64
64
  export declare function usageText(summary: unknown): string;
65
65
  /**
66
- * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the founder
67
- * can find the logs (a failed operation's or execution's own error code, e.g. capacity_unavailable); the agent fills in
66
+ * The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the team
67
+ * can find the logs (a failed operation's or execution's own error code, e.g. template_not_found); the agent fills in
68
68
  * the text.
69
69
  */
70
70
  export declare function feedbackCommand(e: ErrorInfo): string;
@@ -90,7 +90,7 @@ export declare function languageTable(l: TemplateLanguages): string;
90
90
  export declare function packageTable(page: TemplatePackagePage): string;
91
91
  /** A template: identity, what open resolves to, its versions, and the open version's settings. */
92
92
  export declare function templateDetailText(t: TemplateDetail): string;
93
- /** A version's settings (contracts §24.3), one line per part. */
93
+ /** A version's settings, one line per part. */
94
94
  export declare function settingsText(s: TemplateSettings): string;
95
95
  /** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
96
96
  export declare const MAX_CHANGED_SHOWN = 100;
@@ -133,6 +133,17 @@ export declare function usageSeriesText(series: unknown): string;
133
133
  /** The period estimate (GET .../usage/estimate): the bill so far and where each allowance is heading. */
134
134
  export declare function usageEstimateText(estimate: unknown): string;
135
135
  export declare function money(amountMinor: number, currency: string): string;
136
+ export declare function isGrantPlan(plan: {
137
+ plan_key: string;
138
+ grant_days?: unknown;
139
+ }): boolean;
140
+ /** The plan in force with the grant's end (`ends_at`, 0.6.2+), which the SDK's generated types add with the API change. */
141
+ type PlanInForce = NonNullable<BillingSubscription['plan']> & {
142
+ ends_at?: string | null;
143
+ };
144
+ /** "trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog live-v7)"; a plan without an end as before. */
145
+ export declare function planInForceText(plan: PlanInForce | null): string;
146
+ /** The public plans: granted plans (the trial, design partners) are never listed or offered. */
136
147
  export declare function planTable(catalog: BillingCatalog): string;
137
148
  export declare function billingStatusText(s: BillingSubscription, org: Pick<Organization, 'name' | 'slug'>): string;
138
149
  export declare function invoiceTable(rows: readonly BillingInvoicePage['data'][number][]): string;
@@ -149,3 +160,4 @@ export declare function egressVersionTable(rows: readonly EgressPolicyVersion[])
149
160
  * cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
150
161
  */
151
162
  export declare function overagePolicyText(p: SpendPolicy, orgName: string, saved?: boolean): string;
163
+ export {};