@shardflux/cli 0.6.0 → 0.6.2
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 +69 -25
- package/README.md +68 -67
- package/dist/account-args.js +1 -1
- package/dist/account-commands.js +10 -3
- package/dist/args.d.ts +1 -1
- package/dist/args.js +18 -19
- package/dist/browser.js +1 -1
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +16 -14
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/format.d.ts +17 -5
- package/dist/format.js +37 -15
- package/dist/http.d.ts +3 -10
- package/dist/http.js +4 -10
- package/dist/main.js +13 -15
- package/dist/template-yaml.js +1 -1
- package/dist/update-check.js +1 -1
- package/package.json +2 -2
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.
|
|
15
|
+
export const CLI_VERSION = '0.6.2';
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
100
|
-
'
|
|
101
|
-
'- your user
|
|
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
|
|
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
|
|
158
|
-
'error-code': { type: 'string', value: '<code>', description: 'The error code you saw, e.g.
|
|
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
|
|
164
|
-
'shard feedback --category
|
|
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
|
},
|
|
@@ -241,7 +240,7 @@ const KEY_COMMANDS = [
|
|
|
241
240
|
{
|
|
242
241
|
path: ['workspaces', 'exec'],
|
|
243
242
|
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
|
|
243
|
+
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
244
|
positionals: [REF, { name: 'command', required: true, variadic: true }],
|
|
246
245
|
options: {
|
|
247
246
|
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).' },
|
|
@@ -274,7 +273,7 @@ const KEY_COMMANDS = [
|
|
|
274
273
|
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.',
|
|
275
274
|
positionals: [REF],
|
|
276
275
|
options: {
|
|
277
|
-
'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (
|
|
276
|
+
'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
277
|
'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
|
|
279
278
|
...WAIT,
|
|
280
279
|
},
|
|
@@ -519,7 +518,7 @@ const KEY_COMMANDS = [
|
|
|
519
518
|
{
|
|
520
519
|
path: ['templates', 'init'],
|
|
521
520
|
summary: 'Write a template.yaml that starts from a base template',
|
|
522
|
-
description: 'The file is the recipe v2 document
|
|
521
|
+
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
522
|
positionals: [],
|
|
524
523
|
options: {
|
|
525
524
|
base: { type: 'string', value: '<slug[@version]>', description: 'Base template (required), e.g. ubuntu-24.04; without @version, the version open resolves to now.' },
|
|
@@ -1058,8 +1057,8 @@ export function topHelp() {
|
|
|
1058
1057
|
'',
|
|
1059
1058
|
'Feedback: shard feedback [options] <message...>',
|
|
1060
1059
|
...FEEDBACK_WHY.map((l) => ` ${l}`),
|
|
1061
|
-
' shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 "ws open
|
|
1062
|
-
' shard feedback --category
|
|
1060
|
+
' 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"',
|
|
1061
|
+
' shard feedback --category idea "user would like shard ws open to remember the last template they used"',
|
|
1063
1062
|
'',
|
|
1064
1063
|
'Start here (everything the web app does, from a terminal; a person only opens the emailed link and pays in Checkout):',
|
|
1065
1064
|
` 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
|
|
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
|
*/
|
package/dist/commands.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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,14 +66,16 @@ 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
|
|
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
|
+
if (/^0+(?:\.0+)?(?:ms|s|m|h)?$/.test(text.trim()))
|
|
72
|
+
return 0;
|
|
71
73
|
const ms = parseDuration(text, 'when-idle');
|
|
72
74
|
if (ms % 1000 !== 0)
|
|
73
75
|
throw new UsageError('--when-idle must be a whole number of seconds (e.g. 30s, 90s, 2m)');
|
|
74
76
|
const s = ms / 1000;
|
|
75
|
-
if (s
|
|
76
|
-
throw new UsageError(`--when-idle must be between
|
|
77
|
+
if (s > 3600)
|
|
78
|
+
throw new UsageError(`--when-idle must be between 0s and 1h (got "${text}")`);
|
|
77
79
|
return s;
|
|
78
80
|
}
|
|
79
81
|
export function waitTimeout(values) {
|
|
@@ -83,7 +85,7 @@ export function waitTimeout(values) {
|
|
|
83
85
|
/**
|
|
84
86
|
* `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
|
|
85
87
|
* lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
|
|
86
|
-
* workspace over tombstones of ended sessions that had the same key
|
|
88
|
+
* workspace over tombstones of ended sessions that had the same key.
|
|
87
89
|
*/
|
|
88
90
|
export async function resolveWorkspace(ctx, ref) {
|
|
89
91
|
const cloud = ctx.cloud();
|
|
@@ -190,7 +192,7 @@ export function parseSlugVersion(text, what) {
|
|
|
190
192
|
}
|
|
191
193
|
const INPUT_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
192
194
|
/**
|
|
193
|
-
* Open-time text inputs
|
|
195
|
+
* Open-time text inputs from --input-file (NAME=value lines with # comments, or a JSON object of
|
|
194
196
|
* strings), then each --input NAME=value (a later one wins). Undefined when neither option is given.
|
|
195
197
|
*/
|
|
196
198
|
async function inputsFrom(ctx, values) {
|
|
@@ -415,7 +417,7 @@ export function execExitCode(r) {
|
|
|
415
417
|
async function bestEffortCancel(cell, sessionId) {
|
|
416
418
|
await Promise.race([cell.exec.cancel(sessionId).catch(() => undefined), new Promise((r) => setTimeout(r, 3_000).unref())]);
|
|
417
419
|
}
|
|
418
|
-
// ---- File-first workspaces (0.5.0
|
|
420
|
+
// ---- File-first workspaces (0.5.0) ----------------------------------------------------------------------
|
|
419
421
|
const MODES = { processful: 'processful', 'file-first': 'file_first', file_first: 'file_first' };
|
|
420
422
|
/** `--mode processful|file-first` (`file_first` too). */
|
|
421
423
|
function modeFlag(values) {
|
|
@@ -450,7 +452,7 @@ function treeRevisionAfter(ws, cell) {
|
|
|
450
452
|
return cell.treeRevision ?? ws.treeRevision;
|
|
451
453
|
}
|
|
452
454
|
/**
|
|
453
|
-
* A lifecycle call a file-first workspace does not have (
|
|
455
|
+
* A lifecycle call a file-first workspace does not have (suspend, resume, fork, reset,
|
|
454
456
|
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
|
455
457
|
* with its hint like the API's own refusal.
|
|
456
458
|
*/
|
|
@@ -517,7 +519,7 @@ export const KEY_HANDLERS = {
|
|
|
517
519
|
out(ctx, `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n${line}`, { cli: CLI_VERSION, sdk: SDK_VERSION, update });
|
|
518
520
|
return 0;
|
|
519
521
|
},
|
|
520
|
-
// Feedback (0.5.0+): POST /v1/feedback, emailed to the
|
|
522
|
+
// Feedback (0.5.0+): POST /v1/feedback, emailed to the Shardflux team. Validated here first so a bad call costs no request.
|
|
521
523
|
async feedback(ctx, values, words) {
|
|
522
524
|
const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
|
|
523
525
|
const context = feedbackContext(ctx, values);
|
|
@@ -525,7 +527,7 @@ export const KEY_HANDLERS = {
|
|
|
525
527
|
const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
|
|
526
528
|
const human = r.duplicate
|
|
527
529
|
? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
|
|
528
|
-
: `Sent to the Shardflux
|
|
530
|
+
: `Sent to the Shardflux team (feedback ${r.id}). Thank you.\n`;
|
|
529
531
|
out(ctx, human, { id: r.id, received_at: r.receivedAt, duplicate: r.duplicate });
|
|
530
532
|
return 0;
|
|
531
533
|
},
|
|
@@ -659,7 +661,7 @@ export const KEY_HANDLERS = {
|
|
|
659
661
|
const cell = cellOf(ctx, ws);
|
|
660
662
|
const cwd = str(values.cwd);
|
|
661
663
|
if (ws.mode === 'file_first') {
|
|
662
|
-
// An execution
|
|
664
|
+
// An execution: answered when the command ended. The id is ours, so Ctrl-C can name it.
|
|
663
665
|
const id = executionId ?? newExecutionId();
|
|
664
666
|
let r;
|
|
665
667
|
try {
|
|
@@ -675,7 +677,7 @@ export const KEY_HANDLERS = {
|
|
|
675
677
|
}
|
|
676
678
|
catch (err) {
|
|
677
679
|
if (ctx.signal.aborted) {
|
|
678
|
-
throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution
|
|
680
|
+
throw new InterruptedError(`Interrupted; execution ${id} keeps running server side if it started (an execution runs to completion).`, undefined, {
|
|
679
681
|
executionId: id,
|
|
680
682
|
hint: `fetch its result later with: shard executions get ${ref} ${id}`,
|
|
681
683
|
});
|
|
@@ -874,7 +876,7 @@ export const KEY_HANDLERS = {
|
|
|
874
876
|
return 0;
|
|
875
877
|
},
|
|
876
878
|
async 'workspaces resume'(ctx, values, [ref]) {
|
|
877
|
-
// With --wait the resume is held until the workspace runs
|
|
879
|
+
// With --wait the resume is held until the workspace runs; its tool token is the CLI's label.
|
|
878
880
|
return lifecycle(ctx, values, ref, 'Resuming', (ws, o) => ctx.cloud().workspaces.resume(ws.id, { ...o, agentLabel: ctx.agentLabel }), ['resume', 'Resume']);
|
|
879
881
|
},
|
|
880
882
|
async 'workspaces delete'(ctx, values, [ref]) {
|
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 (
|
|
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
|
|
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;
|
package/dist/errors.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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;
|
|
@@ -147,7 +147,7 @@ export function describeError(err) {
|
|
|
147
147
|
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
|
|
148
148
|
return { code: 'operation_failed', message: err.message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
|
|
149
149
|
}
|
|
150
|
-
// File-first executions (0.5.0
|
|
150
|
+
// File-first executions (0.5.0): failed or lost like a failed operation (details.reason says why;
|
|
151
151
|
// retryable: a new execution id may succeed), still running like a wait that gave up.
|
|
152
152
|
if (err instanceof ExecutionFailedError) {
|
|
153
153
|
const e = err.result.error;
|
package/dist/format.d.ts
CHANGED
|
@@ -22,13 +22,13 @@ export declare function workspaceRow(w: WorkspaceView): string[];
|
|
|
22
22
|
export declare function workspaceTable(ws: readonly WorkspaceView[]): string;
|
|
23
23
|
export declare function isReady(w: WorkspaceView): boolean;
|
|
24
24
|
/**
|
|
25
|
-
* The startup of a workspace's template version
|
|
25
|
+
* The startup of a workspace's template version in one line: state, trigger, and for a failure the
|
|
26
26
|
* step or service, exit code and reason. Null when the version has no start commands or services.
|
|
27
27
|
*/
|
|
28
28
|
export declare function startupText(s: WorkspaceView['startup'] | undefined): string | null;
|
|
29
29
|
/** The failed step's output tail, indented, after the detail block (empty otherwise). */
|
|
30
30
|
export declare function startupTail(s: WorkspaceView['startup'] | undefined): string;
|
|
31
|
-
/** A pending suspend-when-idle request in one line (0.5.1
|
|
31
|
+
/** A pending suspend-when-idle request in one line (0.5.1). */
|
|
32
32
|
export declare function suspendRequestText(r: SuspendRequest): string;
|
|
33
33
|
/** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
|
|
34
34
|
export declare function suspendWhenIdleText(w: {
|
|
@@ -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
|
|
67
|
-
* can find the logs (a failed operation's or execution's own error code, e.g.
|
|
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
|
|
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 {};
|
package/dist/format.js
CHANGED
|
@@ -85,7 +85,7 @@ function originText(o) {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* The startup of a workspace's template version
|
|
88
|
+
* The startup of a workspace's template version in one line: state, trigger, and for a failure the
|
|
89
89
|
* step or service, exit code and reason. Null when the version has no start commands or services.
|
|
90
90
|
*/
|
|
91
91
|
export function startupText(s) {
|
|
@@ -107,15 +107,15 @@ export function startupTail(s) {
|
|
|
107
107
|
const lines = s.output_tail.replace(/\n$/, '').split('\n');
|
|
108
108
|
return `startup output (last ${Buffer.byteLength(s.output_tail)} bytes):\n${lines.map((l) => ` | ${l}`).join('\n')}\n`;
|
|
109
109
|
}
|
|
110
|
-
/** A pending suspend-when-idle request in one line (0.5.1
|
|
110
|
+
/** A pending suspend-when-idle request in one line (0.5.1). */
|
|
111
111
|
export function suspendRequestText(r) {
|
|
112
|
-
return `after ${r.after_seconds} s idle, not before ${time(r.not_before)} (requested ${time(r.requested_at)})`;
|
|
112
|
+
return `${r.after_seconds === 0 ? 'as soon as idle' : `after ${r.after_seconds} s idle`}, not before ${time(r.not_before)} (requested ${time(r.requested_at)})`;
|
|
113
113
|
}
|
|
114
114
|
/** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
|
|
115
115
|
export function suspendWhenIdleText(w, r) {
|
|
116
116
|
if (r.suspend_request) {
|
|
117
117
|
const s = r.suspend_request;
|
|
118
|
-
return (`Suspend when idle: ${w.key} (${w.id}) is suspended once it has been idle for ${s.after_seconds} s, not before ${time(s.not_before)}.\n` +
|
|
118
|
+
return (`Suspend when idle: ${w.key} (${w.id}) is suspended ${s.after_seconds === 0 ? 'as soon as it is idle' : `once it has been idle for ${s.after_seconds} s`}, not before ${time(s.not_before)}.\n` +
|
|
119
119
|
'A running command, an attached stream or a keepalive postpones it; the next tool call or a resume cancels it.\n' +
|
|
120
120
|
`cancel with: shard workspaces suspend ${w.key} --cancel-when-idle\n`);
|
|
121
121
|
}
|
|
@@ -132,7 +132,7 @@ export function workspaceDetail(w) {
|
|
|
132
132
|
['state', `${w.observed_state} (desired ${w.desired_state})`],
|
|
133
133
|
['ready', isReady(w) ? 'yes' : 'no'],
|
|
134
134
|
['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
|
|
135
|
-
// The mode (0.5.0
|
|
135
|
+
// The mode (0.5.0) when the API reports it; a file-first workspace's state is its tree revision.
|
|
136
136
|
...(w.mode !== undefined ? [['mode', modeText(w.mode)]] : []),
|
|
137
137
|
...(w.mode === 'file_first' ? [['tree revision', String(w.tree_revision)]] : []),
|
|
138
138
|
['cell', w.cell_id ? `${w.cell_id} ${w.cell_endpoint ?? '(endpoint unresolved)'}` : '-'],
|
|
@@ -317,8 +317,12 @@ export function usageText(summary) {
|
|
|
317
317
|
const head = [];
|
|
318
318
|
if (isObject(s.plan))
|
|
319
319
|
head.push(['plan', `${scalar(s.plan.plan_key)}${s.plan.catalog_version !== undefined ? ` (catalog ${scalar(s.plan.catalog_version)})` : ''}`]);
|
|
320
|
-
if (isObject(s.period))
|
|
321
|
-
|
|
320
|
+
if (isObject(s.period)) {
|
|
321
|
+
// A plan grant's own window (source `grant`, 0.6.2+): the trial's 60 days rather than the calendar month.
|
|
322
|
+
const trial = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
323
|
+
const grant = s.period.source === 'grant' ? (trial ? ' (the trial; then free)' : ' (the plan grant; it ends then)') : '';
|
|
324
|
+
head.push(['period', `${time(scalar(s.period.start))} .. ${time(scalar(s.period.end))}${grant}`]);
|
|
325
|
+
}
|
|
322
326
|
const m = isObject(s.measurement) ? s.measurement : s;
|
|
323
327
|
if (m.measured_through !== undefined)
|
|
324
328
|
head.push(['measured through', time(m.measured_through)]);
|
|
@@ -326,7 +330,8 @@ export function usageText(summary) {
|
|
|
326
330
|
head.push(['estimate', scalar(s.measurement.estimate_status)]);
|
|
327
331
|
if (typeof s.allowance_exhausted === 'boolean') {
|
|
328
332
|
const reason = typeof s.exhausted_reason === 'string' ? `${s.exhausted_reason}: ` : '';
|
|
329
|
-
|
|
333
|
+
const trialEnded = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
334
|
+
head.push(['allowance exhausted', s.allowance_exhausted ? (trialEnded ? `yes (${reason}the trial ends; the organization is back on free)` : `yes (${reason}new opens/resumes are refused)`) : 'no']);
|
|
330
335
|
}
|
|
331
336
|
for (const [k, v] of Object.entries(s))
|
|
332
337
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
@@ -363,8 +368,8 @@ export function usageText(summary) {
|
|
|
363
368
|
/** Request ids and error codes pasted unquoted into the suggested command must be shell-inert. */
|
|
364
369
|
const SHELL_SAFE = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
365
370
|
/**
|
|
366
|
-
* The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the
|
|
367
|
-
* can find the logs (a failed operation's or execution's own error code, e.g.
|
|
371
|
+
* The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the team
|
|
372
|
+
* can find the logs (a failed operation's or execution's own error code, e.g. template_not_found); the agent fills in
|
|
368
373
|
* the text.
|
|
369
374
|
*/
|
|
370
375
|
export function feedbackCommand(e) {
|
|
@@ -402,7 +407,7 @@ export function errorText(e, hint, feedback) {
|
|
|
402
407
|
: '';
|
|
403
408
|
return `error: ${e.message}${reason} [${meta.join(' ')}]\n${issues}${hint ? `hint: ${hint}\n` : ''}${feedback ? `feedback: ${feedback}\n` : ''}`;
|
|
404
409
|
}
|
|
405
|
-
// ---- Templates v2
|
|
410
|
+
// ---- Templates v2 ---------------------------------------------------------------
|
|
406
411
|
export function templateTable(rows) {
|
|
407
412
|
return table(['SLUG', 'OWNER', 'OPEN VERSION', 'LAYOUTS', 'FILES', 'DRAFT', 'ARCHIVED'], rows.map((t) => {
|
|
408
413
|
const v = t.open_version;
|
|
@@ -514,7 +519,7 @@ export function templateDetailText(t) {
|
|
|
514
519
|
: 'No versions.\n';
|
|
515
520
|
return `${head}\n${versions}${open?.settings ? `\nsettings of v${open.version}:\n${settingsText(open.settings)}` : ''}`;
|
|
516
521
|
}
|
|
517
|
-
/** A version's settings
|
|
522
|
+
/** A version's settings, one line per part. */
|
|
518
523
|
export function settingsText(s) {
|
|
519
524
|
const inputs = Object.entries(s.inputs).map(([n, i]) => `${n} (${i.kind}${i.required ? ', required' : ''}${i.default !== null ? `, default ${JSON.stringify(i.default)}` : ''})`);
|
|
520
525
|
const d = s.defaults;
|
|
@@ -528,7 +533,7 @@ export function settingsText(s) {
|
|
|
528
533
|
['egress', egress],
|
|
529
534
|
]).replace(/^/gm, ' ');
|
|
530
535
|
}
|
|
531
|
-
// ---- File-first executions (0.5.0
|
|
536
|
+
// ---- File-first executions (0.5.0) ----------------------------------------------------------------------
|
|
532
537
|
const CHANGE_LETTER = { added: 'A', modified: 'M', deleted: 'D' };
|
|
533
538
|
/** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
|
|
534
539
|
export const MAX_CHANGED_SHOWN = 100;
|
|
@@ -730,8 +735,25 @@ export function money(amountMinor, currency) {
|
|
|
730
735
|
const c = currency.toLowerCase();
|
|
731
736
|
return `${zeroDecimal.has(c) ? String(amountMinor) : (amountMinor / 100).toFixed(2)} ${c.toUpperCase()}`;
|
|
732
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Plans that are only ever granted, never bought: the signup trial and the design-partner plan (0.6.2+). The API's
|
|
740
|
+
* catalog marks them with `grant_days`; the keys cover an API that lists them without it.
|
|
741
|
+
*/
|
|
742
|
+
const GRANT_PLANS = new Set(['trial', 'partner']);
|
|
743
|
+
export function isGrantPlan(plan) {
|
|
744
|
+
return GRANT_PLANS.has(plan.plan_key) || (typeof plan.grant_days === 'number' && plan.grant_days > 0);
|
|
745
|
+
}
|
|
746
|
+
/** "trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog live-v7)"; a plan without an end as before. */
|
|
747
|
+
export function planInForceText(plan) {
|
|
748
|
+
if (!plan)
|
|
749
|
+
return '-';
|
|
750
|
+
const endsAt = typeof plan.ends_at === 'string' && plan.ends_at !== '' ? plan.ends_at : null;
|
|
751
|
+
const ends = endsAt ? `, ends ${time(endsAt)}${plan.plan_key === 'trial' ? ', then free' : ''}` : '';
|
|
752
|
+
return `${plan.plan_key}${ends} (${plan.source}, catalog ${plan.catalog_version})`;
|
|
753
|
+
}
|
|
754
|
+
/** The public plans: granted plans (the trial, design partners) are never listed or offered. */
|
|
733
755
|
export function planTable(catalog) {
|
|
734
|
-
const rows = catalog.plans.map((p) => {
|
|
756
|
+
const rows = catalog.plans.filter((p) => !isGrantPlan(p)).map((p) => {
|
|
735
757
|
const prices = p.prices.length ? p.prices.map((x) => `${money(x.amount_minor, x.currency)}/${x.interval}`).join(', ') : 'free';
|
|
736
758
|
const buy = p.prices.some((x) => x.purchasable) ? 'yes' : '-';
|
|
737
759
|
return [p.plan_key, p.name, prices, buy];
|
|
@@ -743,7 +765,7 @@ export function billingStatusText(s, org) {
|
|
|
743
765
|
const pending = s.pending_checkout;
|
|
744
766
|
return kv([
|
|
745
767
|
['organization', `${org.name} (${org.slug})`],
|
|
746
|
-
['plan',
|
|
768
|
+
['plan', planInForceText(s.plan)],
|
|
747
769
|
['billing state', s.billing_state],
|
|
748
770
|
['subscription', sub ? `${sub.status}${sub.plan_key ? ` ${sub.plan_key}` : ''}${sub.cancel_at_period_end ? ', cancels at period end' : ''}` : 'none'],
|
|
749
771
|
['period end', sub ? time(sub.current_period_end) : '-'],
|
package/dist/http.d.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The fetch given to the SDK: the runtime's fetch
|
|
3
|
-
* every request
|
|
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.
|
|
2
|
+
* The fetch given to the SDK: the runtime's fetch (API and cell gateway). By
|
|
3
|
+
* default every request uses a fresh connection (`Connection: close`);
|
|
4
|
+
* SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
|
|
12
5
|
*
|
|
13
6
|
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
14
7
|
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
package/dist/http.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The fetch given to the SDK: the runtime's fetch
|
|
3
|
-
* every request
|
|
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.
|
|
2
|
+
* The fetch given to the SDK: the runtime's fetch (API and cell gateway). By
|
|
3
|
+
* default every request uses a fresh connection (`Connection: close`);
|
|
4
|
+
* SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
|
|
12
5
|
*
|
|
13
6
|
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
14
7
|
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
|
@@ -16,6 +9,7 @@
|
|
|
16
9
|
* sends its POST without one), so Ctrl-C would otherwise wait the hold out.
|
|
17
10
|
*/
|
|
18
11
|
export function makeFetch(env, base = fetch, signal) {
|
|
12
|
+
// Why a fresh connection by default (Node 26 keep-alive measurements): docs/progress/startup-latency.md.
|
|
19
13
|
const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
|
|
20
14
|
return (input, init) => {
|
|
21
15
|
const headers = new Headers(init?.headers);
|