@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/CHANGELOG.md +79 -24
- package/README.md +105 -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 +45 -22
- package/dist/browser.js +1 -1
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +81 -17
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +16 -9
- package/dist/format.d.ts +22 -10
- package/dist/format.js +64 -13
- package/dist/http.d.ts +1 -17
- package/dist/http.js +6 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +21 -21
- package/dist/template-yaml.js +1 -1
- package/dist/timing.d.ts +12 -0
- package/dist/timing.js +46 -0
- package/dist/update-check.js +1 -1
- package/package.json +2 -2
package/dist/format.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
import { durabilityOf, isDurable } from '@shardflux/sdk';
|
|
1
7
|
export function json(value) {
|
|
2
8
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
3
9
|
}
|
|
@@ -85,7 +91,7 @@ function originText(o) {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
/**
|
|
88
|
-
* The startup of a workspace's template version
|
|
94
|
+
* The startup of a workspace's template version in one line: state, trigger, and for a failure the
|
|
89
95
|
* step or service, exit code and reason. Null when the version has no start commands or services.
|
|
90
96
|
*/
|
|
91
97
|
export function startupText(s) {
|
|
@@ -107,7 +113,7 @@ export function startupTail(s) {
|
|
|
107
113
|
const lines = s.output_tail.replace(/\n$/, '').split('\n');
|
|
108
114
|
return `startup output (last ${Buffer.byteLength(s.output_tail)} bytes):\n${lines.map((l) => ` | ${l}`).join('\n')}\n`;
|
|
109
115
|
}
|
|
110
|
-
/** A pending suspend-when-idle request in one line (0.5.1
|
|
116
|
+
/** A pending suspend-when-idle request in one line (0.5.1). */
|
|
111
117
|
export function suspendRequestText(r) {
|
|
112
118
|
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
119
|
}
|
|
@@ -132,7 +138,7 @@ export function workspaceDetail(w) {
|
|
|
132
138
|
['state', `${w.observed_state} (desired ${w.desired_state})`],
|
|
133
139
|
['ready', isReady(w) ? 'yes' : 'no'],
|
|
134
140
|
['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
|
|
135
|
-
// The mode (0.5.0
|
|
141
|
+
// The mode (0.5.0) when the API reports it; a file-first workspace's state is its tree revision.
|
|
136
142
|
...(w.mode !== undefined ? [['mode', modeText(w.mode)]] : []),
|
|
137
143
|
...(w.mode === 'file_first' ? [['tree revision', String(w.tree_revision)]] : []),
|
|
138
144
|
['cell', w.cell_id ? `${w.cell_id} ${w.cell_endpoint ?? '(endpoint unresolved)'}` : '-'],
|
|
@@ -161,6 +167,28 @@ export const OPERATION_HEADERS = ['ID', 'KIND', 'STATE', 'REASON', 'CREATED', 'C
|
|
|
161
167
|
export function operationTable(ops) {
|
|
162
168
|
return table(OPERATION_HEADERS, ops.map((o) => [o.id, o.kind, o.state, o.state_reason ?? '-', time(o.created_at), time(o.completed_at)]));
|
|
163
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
|
|
172
|
+
* `durability`). Empty when the result has no durability block (stored durably before the operation completed).
|
|
173
|
+
*/
|
|
174
|
+
export function durableText(op) {
|
|
175
|
+
const d = durabilityOf(op);
|
|
176
|
+
if (op.state !== 'succeeded' || !d)
|
|
177
|
+
return '';
|
|
178
|
+
if (d.state === 'durable')
|
|
179
|
+
return `durable: yes${d.localCommitToDurableMs !== null ? ` (${d.localCommitToDurableMs} ms after the suspend)` : ''}\n`;
|
|
180
|
+
if (d.state === 'pending')
|
|
181
|
+
return `durable: in progress, typically within a second (wait with: shard operations wait ${op.id} --durable)\n`;
|
|
182
|
+
return `durable: no (${d.reason ?? 'lost'}); see docs.shardflux.dev/concepts/lifecycle\n`;
|
|
183
|
+
}
|
|
184
|
+
/** `durable` of a finished suspend or fork for `operations get`/`wait`: yes, in progress, no, or '-' for other kinds. */
|
|
185
|
+
function durableField(op) {
|
|
186
|
+
const d = durabilityOf(op);
|
|
187
|
+
if (d)
|
|
188
|
+
return d.state === 'durable' ? 'yes' : d.state === 'pending' ? 'in progress' : `no (${d.reason ?? 'lost'})`;
|
|
189
|
+
const v = isDurable(op);
|
|
190
|
+
return v === null ? '-' : v ? 'yes' : 'no';
|
|
191
|
+
}
|
|
164
192
|
export function operationDetail(op) {
|
|
165
193
|
const err = op.error;
|
|
166
194
|
return kv([
|
|
@@ -168,6 +196,7 @@ export function operationDetail(op) {
|
|
|
168
196
|
['kind', op.kind],
|
|
169
197
|
['state', op.state],
|
|
170
198
|
['reason', op.state_reason ?? '-'],
|
|
199
|
+
...(op.kind === 'suspend' || op.kind === 'fork' ? [['durable', durableField(op)]] : []),
|
|
171
200
|
['workspace', op.workspace_id ?? '-'],
|
|
172
201
|
['created', time(op.created_at)],
|
|
173
202
|
['updated', time(op.updated_at)],
|
|
@@ -317,8 +346,12 @@ export function usageText(summary) {
|
|
|
317
346
|
const head = [];
|
|
318
347
|
if (isObject(s.plan))
|
|
319
348
|
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
|
-
|
|
349
|
+
if (isObject(s.period)) {
|
|
350
|
+
// A plan grant's own window (source `grant`, 0.6.2+): the trial's 60 days rather than the calendar month.
|
|
351
|
+
const trial = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
352
|
+
const grant = s.period.source === 'grant' ? (trial ? ' (the trial; then free)' : ' (the plan grant; it ends then)') : '';
|
|
353
|
+
head.push(['period', `${time(scalar(s.period.start))} .. ${time(scalar(s.period.end))}${grant}`]);
|
|
354
|
+
}
|
|
322
355
|
const m = isObject(s.measurement) ? s.measurement : s;
|
|
323
356
|
if (m.measured_through !== undefined)
|
|
324
357
|
head.push(['measured through', time(m.measured_through)]);
|
|
@@ -326,7 +359,8 @@ export function usageText(summary) {
|
|
|
326
359
|
head.push(['estimate', scalar(s.measurement.estimate_status)]);
|
|
327
360
|
if (typeof s.allowance_exhausted === 'boolean') {
|
|
328
361
|
const reason = typeof s.exhausted_reason === 'string' ? `${s.exhausted_reason}: ` : '';
|
|
329
|
-
|
|
362
|
+
const trialEnded = isObject(s.plan) && s.plan.plan_key === 'trial';
|
|
363
|
+
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
364
|
}
|
|
331
365
|
for (const [k, v] of Object.entries(s))
|
|
332
366
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
@@ -363,8 +397,8 @@ export function usageText(summary) {
|
|
|
363
397
|
/** Request ids and error codes pasted unquoted into the suggested command must be shell-inert. */
|
|
364
398
|
const SHELL_SAFE = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
365
399
|
/**
|
|
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.
|
|
400
|
+
* The `shard feedback` command a failed command suggests (0.5.0+), with the error's request id and code so the team
|
|
401
|
+
* can find the logs (a failed operation's or execution's own error code, e.g. template_not_found); the agent fills in
|
|
368
402
|
* the text.
|
|
369
403
|
*/
|
|
370
404
|
export function feedbackCommand(e) {
|
|
@@ -402,7 +436,7 @@ export function errorText(e, hint, feedback) {
|
|
|
402
436
|
: '';
|
|
403
437
|
return `error: ${e.message}${reason} [${meta.join(' ')}]\n${issues}${hint ? `hint: ${hint}\n` : ''}${feedback ? `feedback: ${feedback}\n` : ''}`;
|
|
404
438
|
}
|
|
405
|
-
// ---- Templates v2
|
|
439
|
+
// ---- Templates v2 ---------------------------------------------------------------
|
|
406
440
|
export function templateTable(rows) {
|
|
407
441
|
return table(['SLUG', 'OWNER', 'OPEN VERSION', 'LAYOUTS', 'FILES', 'DRAFT', 'ARCHIVED'], rows.map((t) => {
|
|
408
442
|
const v = t.open_version;
|
|
@@ -514,7 +548,7 @@ export function templateDetailText(t) {
|
|
|
514
548
|
: 'No versions.\n';
|
|
515
549
|
return `${head}\n${versions}${open?.settings ? `\nsettings of v${open.version}:\n${settingsText(open.settings)}` : ''}`;
|
|
516
550
|
}
|
|
517
|
-
/** A version's settings
|
|
551
|
+
/** A version's settings, one line per part. */
|
|
518
552
|
export function settingsText(s) {
|
|
519
553
|
const inputs = Object.entries(s.inputs).map(([n, i]) => `${n} (${i.kind}${i.required ? ', required' : ''}${i.default !== null ? `, default ${JSON.stringify(i.default)}` : ''})`);
|
|
520
554
|
const d = s.defaults;
|
|
@@ -528,7 +562,7 @@ export function settingsText(s) {
|
|
|
528
562
|
['egress', egress],
|
|
529
563
|
]).replace(/^/gm, ' ');
|
|
530
564
|
}
|
|
531
|
-
// ---- File-first executions (0.5.0
|
|
565
|
+
// ---- File-first executions (0.5.0) ----------------------------------------------------------------------
|
|
532
566
|
const CHANGE_LETTER = { added: 'A', modified: 'M', deleted: 'D' };
|
|
533
567
|
/** How many changed paths the human output lists before "... N more" (--json lists every one the result has). */
|
|
534
568
|
export const MAX_CHANGED_SHOWN = 100;
|
|
@@ -730,8 +764,25 @@ export function money(amountMinor, currency) {
|
|
|
730
764
|
const c = currency.toLowerCase();
|
|
731
765
|
return `${zeroDecimal.has(c) ? String(amountMinor) : (amountMinor / 100).toFixed(2)} ${c.toUpperCase()}`;
|
|
732
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Plans that are only ever granted, never bought: the signup trial and the design-partner plan (0.6.2+). The API's
|
|
769
|
+
* catalog marks them with `grant_days`; the keys cover an API that lists them without it.
|
|
770
|
+
*/
|
|
771
|
+
const GRANT_PLANS = new Set(['trial', 'partner']);
|
|
772
|
+
export function isGrantPlan(plan) {
|
|
773
|
+
return GRANT_PLANS.has(plan.plan_key) || (typeof plan.grant_days === 'number' && plan.grant_days > 0);
|
|
774
|
+
}
|
|
775
|
+
/** "trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog live-v7)"; a plan without an end as before. */
|
|
776
|
+
export function planInForceText(plan) {
|
|
777
|
+
if (!plan)
|
|
778
|
+
return '-';
|
|
779
|
+
const endsAt = typeof plan.ends_at === 'string' && plan.ends_at !== '' ? plan.ends_at : null;
|
|
780
|
+
const ends = endsAt ? `, ends ${time(endsAt)}${plan.plan_key === 'trial' ? ', then free' : ''}` : '';
|
|
781
|
+
return `${plan.plan_key}${ends} (${plan.source}, catalog ${plan.catalog_version})`;
|
|
782
|
+
}
|
|
783
|
+
/** The public plans: granted plans (the trial, design partners) are never listed or offered. */
|
|
733
784
|
export function planTable(catalog) {
|
|
734
|
-
const rows = catalog.plans.map((p) => {
|
|
785
|
+
const rows = catalog.plans.filter((p) => !isGrantPlan(p)).map((p) => {
|
|
735
786
|
const prices = p.prices.length ? p.prices.map((x) => `${money(x.amount_minor, x.currency)}/${x.interval}`).join(', ') : 'free';
|
|
736
787
|
const buy = p.prices.some((x) => x.purchasable) ? 'yes' : '-';
|
|
737
788
|
return [p.plan_key, p.name, prices, buy];
|
|
@@ -743,7 +794,7 @@ export function billingStatusText(s, org) {
|
|
|
743
794
|
const pending = s.pending_checkout;
|
|
744
795
|
return kv([
|
|
745
796
|
['organization', `${org.name} (${org.slug})`],
|
|
746
|
-
['plan',
|
|
797
|
+
['plan', planInForceText(s.plan)],
|
|
747
798
|
['billing state', s.billing_state],
|
|
748
799
|
['subscription', sub ? `${sub.status}${sub.plan_key ? ` ${sub.plan_key}` : ''}${sub.cancel_at_period_end ? ', cancels at period end' : ''}` : 'none'],
|
|
749
800
|
['period end', sub ? time(sub.current_period_end) : '-'],
|
package/dist/http.d.ts
CHANGED
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fetch given to the SDK: the runtime's fetch with `Connection: close` on
|
|
3
|
-
* every request (API and cell gateway).
|
|
4
|
-
*
|
|
5
|
-
* Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
|
|
6
|
-
* sent on a reused keep-alive connection until an unrelated timer fires (up to
|
|
7
|
-
* ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
|
|
8
|
-
* with bare fetch against both node:http and Fastify servers; Node 22 (undici
|
|
9
|
-
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
|
-
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
|
-
* Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
|
|
12
|
-
*
|
|
13
|
-
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
14
|
-
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
|
15
|
-
* held polls itself, but not to the held open (`workspaces.open()` with `wait`
|
|
16
|
-
* sends its POST without one), so Ctrl-C would otherwise wait the hold out.
|
|
17
|
-
*/
|
|
1
|
+
/** Share the SDK pool and attach Ctrl-C to held API requests. */
|
|
18
2
|
export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch, signal?: AbortSignal): typeof fetch;
|
package/dist/http.js
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* Why: Node 26.7's bundled undici (8.9.0) intermittently stalls a request
|
|
6
|
-
* sent on a reused keep-alive connection until an unrelated timer fires (up to
|
|
7
|
-
* ~30 s, the SDK's request timeout, after which the SDK retries). Reproduced
|
|
8
|
-
* with bare fetch against both node:http and Fastify servers; Node 22 (undici
|
|
9
|
-
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
|
-
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
|
-
* Set SHARDFLUX_HTTP_KEEPALIVE=1 to use the runtime's keep-alive pooling.
|
|
12
|
-
*
|
|
13
|
-
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
14
|
-
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
|
15
|
-
* held polls itself, but not to the held open (`workspaces.open()` with `wait`
|
|
16
|
-
* sends its POST without one), so Ctrl-C would otherwise wait the hold out.
|
|
17
|
-
*/
|
|
18
|
-
export function makeFetch(env, base = fetch, signal) {
|
|
19
|
-
const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
|
|
1
|
+
import { defaultFetch } from '@shardflux/sdk';
|
|
2
|
+
/** Share the SDK pool and attach Ctrl-C to held API requests. */
|
|
3
|
+
export function makeFetch(env, base = defaultFetch(env), signal) {
|
|
20
4
|
return (input, init) => {
|
|
21
5
|
const headers = new Headers(init?.headers);
|
|
22
6
|
const held = signal !== undefined && /\bwait\s*=/i.test(headers.get('prefer') ?? '');
|
|
23
|
-
if (
|
|
7
|
+
if (!held)
|
|
24
8
|
return base(input, init);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const s = held && signal ? (init?.signal ? AbortSignal.any([init.signal, signal]) : signal) : init?.signal;
|
|
28
|
-
return base(input, { ...init, headers, ...(s ? { signal: s } : {}) });
|
|
9
|
+
const s = init?.signal && signal ? AbortSignal.any([init.signal, signal]) : signal;
|
|
10
|
+
return base(input, { ...init, ...(s ? { signal: s } : {}) });
|
|
29
11
|
};
|
|
30
12
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,4 @@ export { openUrl, openerFor } from './browser.js';
|
|
|
22
22
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
|
|
23
23
|
export type { ErrorInfo } from './errors.js';
|
|
24
24
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
25
|
-
export { TimingRecorder, coldBootNotice, timingJson, timingText } from './timing.js';
|
|
25
|
+
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,4 @@ export { CHECK_INTERVAL_MS, CLI_DISTRIBUTION, NOTICE_WAIT_MS, UPDATE_CHECK_FILE,
|
|
|
14
14
|
export { openUrl, openerFor } from "./browser.js";
|
|
15
15
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
16
16
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
17
|
-
export { TimingRecorder, coldBootNotice, timingJson, timingText } from "./timing.js";
|
|
17
|
+
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
|
package/dist/main.js
CHANGED
|
@@ -19,7 +19,7 @@ import { errorText, feedbackCommand, json, time } from "./format.js";
|
|
|
19
19
|
import { HANDLERS } from "./handlers.js";
|
|
20
20
|
import { makeFetch } from "./http.js";
|
|
21
21
|
import { Input } from "./input.js";
|
|
22
|
-
import { TimingRecorder,
|
|
22
|
+
import { TimingRecorder, notices, timingJson, timingText } from "./timing.js";
|
|
23
23
|
import { CLI_DISTRIBUTION, NOTICE_WAIT_MS, UpdateCheck, noticeOf, updateCheckDisabled, updateView } from "./update-check.js";
|
|
24
24
|
const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
|
|
25
25
|
// Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
|
|
@@ -47,7 +47,7 @@ function hintFor(info, cred, file) {
|
|
|
47
47
|
if (info.code === 'timeout' && info.operation_id)
|
|
48
48
|
return `the operation continues server side; keep waiting with: shard operations wait ${info.operation_id}`;
|
|
49
49
|
if (info.code === 'operation_failed' && info.retryable) {
|
|
50
|
-
return info.details?.error_code === 'capacity_unavailable' ? '
|
|
50
|
+
return info.details?.error_code === 'capacity_unavailable' ? 'nothing was started; run the same command again' : 'the operation may succeed if you retry it later.';
|
|
51
51
|
}
|
|
52
52
|
if (info.code === 'interrupted' && info.operation_id)
|
|
53
53
|
return `keep waiting with: shard operations wait ${info.operation_id}`;
|
|
@@ -79,7 +79,7 @@ function hintFor(info, cred, file) {
|
|
|
79
79
|
const policyHint = spendPolicyHint(info);
|
|
80
80
|
if (policyHint)
|
|
81
81
|
return policyHint;
|
|
82
|
-
// File patches
|
|
82
|
+
// File patches: what to do about each refusal. details.index is the 0-based edit (--old) index.
|
|
83
83
|
const reason = info.details?.reason;
|
|
84
84
|
const index = typeof info.details?.index === 'number' ? ` #${info.details.index + 1}` : '';
|
|
85
85
|
if (reason === 'revision_mismatch') {
|
|
@@ -93,7 +93,7 @@ function hintFor(info, cred, file) {
|
|
|
93
93
|
if (reason === 'edit_not_text')
|
|
94
94
|
return 'the file is not UTF-8 text; use files write for binary data';
|
|
95
95
|
if (reason === 'host_capacity')
|
|
96
|
-
return 'the workspace
|
|
96
|
+
return 'the workspace cannot be woken right now and the call did not run; retry in a few seconds';
|
|
97
97
|
if (reason === 'host_feature_unavailable')
|
|
98
98
|
return hostFeatureHint(info.details?.feature);
|
|
99
99
|
// A processful command that could not start (0.5.1): what to change, from the workspace's reason.
|
|
@@ -167,17 +167,15 @@ function spendPolicyHint(info) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
170
|
-
* 409 conflict host_feature_unavailable (
|
|
171
|
-
*
|
|
172
|
-
* another way to do it now.
|
|
170
|
+
* 409 conflict host_feature_unavailable (0.5.0): the call is not available for this workspace. Not retryable, so the
|
|
171
|
+
* hint names the fallback that works now.
|
|
173
172
|
*/
|
|
174
173
|
function hostFeatureHint(feature) {
|
|
175
|
-
const later = 'it will once the workspace runs on an upgraded host (retrying now does not help)';
|
|
176
174
|
if (feature === 'file_search')
|
|
177
|
-
return
|
|
175
|
+
return 'files search is not available for this workspace (retrying does not help); search with a command instead: shard ws exec <id|key> -- grep -rn <pattern> <path>';
|
|
178
176
|
if (feature === 'file_patch')
|
|
179
|
-
return
|
|
180
|
-
return
|
|
177
|
+
return 'files patch is not available for this workspace (retrying does not help); rewrite the file instead: shard files read <id|key> <path> --out F, edit F, then shard files write <id|key> <path> --from F';
|
|
178
|
+
return 'this call is not available for this workspace (retrying does not help)';
|
|
181
179
|
}
|
|
182
180
|
/** What an execution that ended failed or lost ran into (error.details.reason), where the reason alone says little. */
|
|
183
181
|
const EXECUTION_CAUSES = {
|
|
@@ -186,10 +184,10 @@ const EXECUTION_CAUSES = {
|
|
|
186
184
|
host_unreachable: 'its host stopped answering',
|
|
187
185
|
host_restarted: 'its host restarted',
|
|
188
186
|
tree_moved: 'the tree changed while it ran',
|
|
189
|
-
no_execution_host: '
|
|
187
|
+
no_execution_host: 'the execution could not be placed',
|
|
190
188
|
workspace_deleted: 'the workspace was deleted',
|
|
191
189
|
};
|
|
192
|
-
/** Hints for file-first workspaces (0.5.0
|
|
190
|
+
/** Hints for file-first workspaces (0.5.0): the mode refusals, tree revisions and executions. */
|
|
193
191
|
function fileFirstHint(info) {
|
|
194
192
|
const d = info.details ?? {};
|
|
195
193
|
const reason = d.reason;
|
|
@@ -211,7 +209,7 @@ function fileFirstHint(info) {
|
|
|
211
209
|
return `this key already belongs to a ${has} workspace and a workspace’s mode never changes; use another key for a ${wants} workspace (or reopen this one without --mode)`;
|
|
212
210
|
}
|
|
213
211
|
case 'mode_not_available':
|
|
214
|
-
return 'this deployment does not offer file-first workspaces
|
|
212
|
+
return 'this deployment does not offer file-first workspaces; leave out --mode file-first to open a processful workspace';
|
|
215
213
|
case 'layout_unsupported':
|
|
216
214
|
return d.mode === 'file_first' ? 'file-first workspaces run on layered template versions only; open a template version whose disk layouts include layered (shard templates get <slug> lists them)' : undefined;
|
|
217
215
|
case 'tree_revision_mismatch': {
|
|
@@ -229,7 +227,7 @@ function fileFirstHint(info) {
|
|
|
229
227
|
case 'execution_id_reused':
|
|
230
228
|
return 'this --execution-id was already used for a different request (the same id with the same request returns its recorded result); pass a new --execution-id';
|
|
231
229
|
case 'no_execution_host':
|
|
232
|
-
return '
|
|
230
|
+
return 'the execution cannot be placed right now, also after retries with the same execution id; nothing ran. Retry';
|
|
233
231
|
default:
|
|
234
232
|
return undefined;
|
|
235
233
|
}
|
|
@@ -271,7 +269,7 @@ function nudges(info, command) {
|
|
|
271
269
|
const reason = reasonOf(info);
|
|
272
270
|
return !NO_NUDGE_CODES.has(info.code) && !(reason !== undefined && NO_NUDGE_REASONS.has(reason));
|
|
273
271
|
}
|
|
274
|
-
/** `shard feedback` could not send: how to fix it, and the way to still reach the
|
|
272
|
+
/** `shard feedback` could not send: how to fix it, and the way to still reach the Shardflux team. */
|
|
275
273
|
function feedbackHint(info, credentialHint) {
|
|
276
274
|
if (info.code === 'usage')
|
|
277
275
|
return 'run "shard feedback --help"';
|
|
@@ -329,7 +327,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
329
327
|
const feedback = nudges(info, command) ? feedbackCommand(info) : undefined;
|
|
330
328
|
const text = wantsJson
|
|
331
329
|
? json({ error: info, ...(showTiming ? { timing: timingJson(timing) } : {}), ...(feedback ? { feedback } : {}) })
|
|
332
|
-
: `${timing ? timingText(timing) : ''}${
|
|
330
|
+
: `${timing ? timingText(timing) : ''}${notices(traces)}${errorText(info, hint, feedback)}`;
|
|
333
331
|
io.stderr.write(redact(text, ...secrets));
|
|
334
332
|
return code;
|
|
335
333
|
};
|
|
@@ -361,7 +359,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
361
359
|
const wakeTimeoutMs = resolveWakeTimeout(parsed.global.wakeTimeout, io.env.SHARDFLUX_WAKE_TIMEOUT_MS);
|
|
362
360
|
showTiming = parsed.values.timing === true;
|
|
363
361
|
const userAgent = `shard-cli/${CLI_VERSION} shardflux-sdk-ts/${SDK_VERSION}${distribution.package === CLI_DISTRIBUTION.package ? '' : ` ${distribution.package}/${distribution.version}`}`;
|
|
364
|
-
const base = makeFetch(io.env,
|
|
362
|
+
const base = makeFetch(io.env, undefined, signal);
|
|
365
363
|
// Notes which credential a 401 unauthenticated answered (the hint, and removing a dead saved session).
|
|
366
364
|
const watchedFetch = async (input, init) => {
|
|
367
365
|
const res = await base(input, init);
|
|
@@ -562,9 +560,11 @@ export async function run(argv, io, opts = {}) {
|
|
|
562
560
|
// With --json the handler put the timing into its output; otherwise it follows the output, on stderr.
|
|
563
561
|
if (showTiming && !ctx.json && traces.last)
|
|
564
562
|
io.stderr.write(timingText(traces.last));
|
|
565
|
-
// A resume that booted the saved disk (0.6.0+): said on stderr with or
|
|
566
|
-
|
|
567
|
-
|
|
563
|
+
// A resume that booted the saved disk (0.6.0+), or restored an earlier checkpoint (0.7.0+): said on stderr with or
|
|
564
|
+
// without --timing, also with --json.
|
|
565
|
+
const notice = notices(traces);
|
|
566
|
+
if (notice)
|
|
567
|
+
io.stderr.write(notice);
|
|
568
568
|
code = outcome.code;
|
|
569
569
|
}
|
|
570
570
|
// The update notice: after the output, never after Ctrl-C or a --json failure (stderr then holds only the error).
|
package/dist/template-yaml.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* template.yaml text (
|
|
2
|
+
* template.yaml text (the recipe v2 document in YAML; only clients parse YAML): the starter file of
|
|
3
3
|
* `shard templates init` and the export of a version (`shard templates export`), written like the dashboard's
|
|
4
4
|
* Template file section (keys in the recipe's order, long scripts as block literals, no line folding).
|
|
5
5
|
*/
|
package/dist/timing.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare class TimingRecorder {
|
|
|
10
10
|
* the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
|
|
11
11
|
*/
|
|
12
12
|
coldBoot: LifecycleTiming | null;
|
|
13
|
+
/**
|
|
14
|
+
* (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
|
|
15
|
+
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
16
|
+
*/
|
|
17
|
+
lostSuspend: LifecycleTiming | null;
|
|
13
18
|
readonly listener: ProgressListener;
|
|
14
19
|
/**
|
|
15
20
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -22,6 +27,13 @@ export declare class TimingRecorder {
|
|
|
22
27
|
* `--timing`: the workspace runs and its files are as of the suspend, but running processes were restarted.
|
|
23
28
|
*/
|
|
24
29
|
export declare function coldBootNotice(t: LifecycleTiming): string;
|
|
30
|
+
/**
|
|
31
|
+
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
32
|
+
* could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function lostSuspendNotice(t: LifecycleTiming): string;
|
|
35
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
36
|
+
export declare function notices(r: TimingRecorder): string;
|
|
25
37
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
26
38
|
export declare function timingText(t: LifecycleTiming): string;
|
|
27
39
|
/** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
|
package/dist/timing.js
CHANGED
|
@@ -17,6 +17,11 @@ export class TimingRecorder {
|
|
|
17
17
|
* the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
|
|
18
18
|
*/
|
|
19
19
|
coldBoot = null;
|
|
20
|
+
/**
|
|
21
|
+
* (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
|
|
22
|
+
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
23
|
+
*/
|
|
24
|
+
lostSuspend = null;
|
|
20
25
|
listener = (e) => {
|
|
21
26
|
if (e.action === 'token' || e.action === 'tool')
|
|
22
27
|
return;
|
|
@@ -27,6 +32,8 @@ export class TimingRecorder {
|
|
|
27
32
|
this.last = e.timing;
|
|
28
33
|
if (restartedProcesses(e.timing))
|
|
29
34
|
this.coldBoot = e.timing;
|
|
35
|
+
if (e.timing.server?.lostSuspend)
|
|
36
|
+
this.lostSuspend = e.timing;
|
|
30
37
|
};
|
|
31
38
|
/**
|
|
32
39
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -49,6 +56,20 @@ export function coldBootNotice(t) {
|
|
|
49
56
|
const reason = t.server?.coldBootReason;
|
|
50
57
|
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from its saved disk; running processes were restarted${reason ? ` (${reason})` : ''}\n`;
|
|
51
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
61
|
+
* could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
|
|
62
|
+
*/
|
|
63
|
+
export function lostSuspendNotice(t) {
|
|
64
|
+
const l = t.server?.lostSuspend;
|
|
65
|
+
if (!l)
|
|
66
|
+
return '';
|
|
67
|
+
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from checkpoint ${l.restoredCheckpointId ?? '-'}${l.stateAsOf ? ` (state as of ${l.stateAsOf})` : ''}; the suspend of ${l.suspendedAt ?? l.checkpointId} was not kept${l.reason ? ` (${l.reason})` : ''}\n`;
|
|
68
|
+
}
|
|
69
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
70
|
+
export function notices(r) {
|
|
71
|
+
return `${r.coldBoot ? coldBootNotice(r.coldBoot) : ''}${r.lostSuspend ? lostSuspendNotice(r.lostSuspend) : ''}`;
|
|
72
|
+
}
|
|
52
73
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
53
74
|
export function timingText(t) {
|
|
54
75
|
return `${formatTiming(t)}\n`;
|
|
@@ -79,6 +100,31 @@ export function timingJson(t) {
|
|
|
79
100
|
resume_path: s.resumePath,
|
|
80
101
|
memory_restored: s.memoryRestored ?? null,
|
|
81
102
|
cold_boot_reason: s.coldBootReason ?? null,
|
|
103
|
+
durable: s.durable ?? null,
|
|
104
|
+
suspend_path: s.suspendPath ?? null,
|
|
105
|
+
durability: s.durability
|
|
106
|
+
? {
|
|
107
|
+
state: s.durability.state,
|
|
108
|
+
checkpoint_id: s.durability.checkpointId,
|
|
109
|
+
generation_id: s.durability.generationId,
|
|
110
|
+
local_commit_at: s.durability.localCommitAt,
|
|
111
|
+
durable_by: s.durability.durableBy,
|
|
112
|
+
durable_at: s.durability.durableAt,
|
|
113
|
+
local_commit_to_durable_ms: s.durability.localCommitToDurableMs,
|
|
114
|
+
overdue_at: s.durability.overdueAt,
|
|
115
|
+
reason: s.durability.reason,
|
|
116
|
+
}
|
|
117
|
+
: null,
|
|
118
|
+
lost_suspend: s.lostSuspend
|
|
119
|
+
? {
|
|
120
|
+
checkpoint_id: s.lostSuspend.checkpointId,
|
|
121
|
+
generation_id: s.lostSuspend.generationId,
|
|
122
|
+
reason: s.lostSuspend.reason,
|
|
123
|
+
suspended_at: s.lostSuspend.suspendedAt,
|
|
124
|
+
restored_checkpoint_id: s.lostSuspend.restoredCheckpointId,
|
|
125
|
+
state_as_of: s.lostSuspend.stateAsOf,
|
|
126
|
+
}
|
|
127
|
+
: null,
|
|
82
128
|
boot_to_ready_ms: s.bootToReadyMs,
|
|
83
129
|
host_timings_ms: s.hostTimingsMs,
|
|
84
130
|
}
|
package/dist/update-check.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `shard` tells its user when it is outdated or no longer supported (0.5.0
|
|
2
|
+
* `shard` tells its user when it is outdated or no longer supported (0.5.0+). The API's
|
|
3
3
|
* GET /v1/client-versions names the latest published version of every client and the oldest one it still serves.
|
|
4
4
|
*
|
|
5
5
|
* - At most one request per 24 hours per API URL and package: the answer is cached in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"yaml": "2.9.1",
|
|
47
|
-
"@shardflux/sdk": "^0.
|
|
47
|
+
"@shardflux/sdk": "^0.12.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|