@shardflux/cli 0.1.0 → 0.3.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 +44 -0
- package/README.md +119 -10
- package/dist/args.d.ts +10 -1
- package/dist/args.js +348 -15
- package/dist/commands.d.ts +14 -1
- package/dist/commands.js +622 -28
- package/dist/format.d.ts +20 -1
- package/dist/format.js +132 -1
- package/dist/http.d.ts +6 -1
- package/dist/http.js +14 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/main.js +28 -3
- package/dist/timing.d.ts +17 -0
- package/dist/timing.js +65 -0
- package/package.json +3 -2
package/dist/format.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `--json` output is the API's own objects (see README "JSON output"), so
|
|
4
4
|
* these functions only shape what people read.
|
|
5
5
|
*/
|
|
6
|
-
import type { AgentSession, FileList, Operation, WorkspaceView } from '@shardflux/sdk';
|
|
6
|
+
import type { AgentSession, DraftState, FileList, Operation, Secret, SecretAccessEvent, SecretVersion, TemplateBuild, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
7
7
|
import type { ErrorInfo } from './errors.js';
|
|
8
8
|
export declare function json(value: unknown): string;
|
|
9
9
|
export declare function table(headers: readonly string[], rows: readonly (readonly string[])[]): string;
|
|
@@ -14,6 +14,8 @@ export declare function time(iso: string | null | undefined): string;
|
|
|
14
14
|
export declare function pairs(obj: Record<string, unknown> | null | undefined): string;
|
|
15
15
|
export declare function operationSummary(op: Operation | null | undefined): string;
|
|
16
16
|
export declare const WORKSPACE_HEADERS: readonly ["ID", "KEY", "STATE", "DESIRED", "TEMPLATE", "OPERATION", "CREATED"];
|
|
17
|
+
/** Observed state plus what sets the workspace apart: session, draft or test instance, and how it ended. */
|
|
18
|
+
export declare function workspaceState(w: WorkspaceView): string;
|
|
17
19
|
export declare function workspaceRow(w: WorkspaceView): string[];
|
|
18
20
|
export declare function workspaceTable(ws: readonly WorkspaceView[]): string;
|
|
19
21
|
export declare function isReady(w: WorkspaceView): boolean;
|
|
@@ -23,6 +25,12 @@ export declare function operationTable(ops: readonly Operation[]): string;
|
|
|
23
25
|
export declare function operationDetail(op: Operation): string;
|
|
24
26
|
export declare function sessionTable(sessions: readonly AgentSession[]): string;
|
|
25
27
|
export declare function fileTable(list: FileList): string;
|
|
28
|
+
export declare function secretTable(rows: readonly Secret[]): string;
|
|
29
|
+
/** Secret metadata; there is no value to show (values are write-only). */
|
|
30
|
+
export declare function secretDetail(s: Secret): string;
|
|
31
|
+
export declare function secretVersionTable(rows: readonly SecretVersion[]): string;
|
|
32
|
+
export declare function secretAccessTable(rows: readonly SecretAccessEvent[]): string;
|
|
33
|
+
export declare function bindingsText(b: WorkspaceSecretBindings): string;
|
|
26
34
|
/**
|
|
27
35
|
* The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
|
|
28
36
|
* freshness, then meters and allowances as tables. Columns absent from the response are omitted.
|
|
@@ -30,3 +38,14 @@ export declare function fileTable(list: FileList): string;
|
|
|
30
38
|
export declare function usageText(summary: unknown): string;
|
|
31
39
|
/** One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one. */
|
|
32
40
|
export declare function errorText(e: ErrorInfo, hint?: string): string;
|
|
41
|
+
export declare function templateTable(rows: readonly TemplateSummary[]): string;
|
|
42
|
+
/** Permission bits as the four-digit octal `ls -l` users read (e.g. 0755). */
|
|
43
|
+
export declare function octal(mode: number): string;
|
|
44
|
+
export declare function templateFileTable(rows: readonly TemplateFileEntry[]): string;
|
|
45
|
+
export declare function templateEntryText(e: TemplateFileEntry): string;
|
|
46
|
+
export declare function diffTable(rows: readonly TemplateDiffEntry[]): string;
|
|
47
|
+
export declare function diffSummaryText(summary: unknown): string;
|
|
48
|
+
export declare function changesTable(rows: readonly WorkspaceChange[]): string;
|
|
49
|
+
export declare function draftText(d: TemplateDraft): string;
|
|
50
|
+
export declare function draftStateTable(rows: readonly DraftState[]): string;
|
|
51
|
+
export declare function buildText(b: TemplateBuild): string;
|
package/dist/format.js
CHANGED
|
@@ -44,8 +44,21 @@ export function operationSummary(op) {
|
|
|
44
44
|
return `${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}`;
|
|
45
45
|
}
|
|
46
46
|
export const WORKSPACE_HEADERS = ['ID', 'KEY', 'STATE', 'DESIRED', 'TEMPLATE', 'OPERATION', 'CREATED'];
|
|
47
|
+
/** Observed state plus what sets the workspace apart: session, draft or test instance, and how it ended. */
|
|
48
|
+
export function workspaceState(w) {
|
|
49
|
+
const tags = [];
|
|
50
|
+
if (w.purpose === 'template_draft')
|
|
51
|
+
tags.push('draft');
|
|
52
|
+
else if (w.purpose === 'template_test')
|
|
53
|
+
tags.push('test instance');
|
|
54
|
+
else if (w.lifetime === 'session')
|
|
55
|
+
tags.push('session');
|
|
56
|
+
if (w.deleted_at)
|
|
57
|
+
tags.push(w.ended_reason ? `ended: ${w.ended_reason}` : 'deleted');
|
|
58
|
+
return tags.length ? `${w.observed_state} (${tags.join(', ')})` : w.observed_state;
|
|
59
|
+
}
|
|
47
60
|
export function workspaceRow(w) {
|
|
48
|
-
const state = w
|
|
61
|
+
const state = workspaceState(w);
|
|
49
62
|
return [w.id, w.workspace_key, state, w.desired_state, `${w.template.slug}@${w.template.version}`, operationSummary(w.active_operation), time(w.created_at)];
|
|
50
63
|
}
|
|
51
64
|
export function workspaceTable(ws) {
|
|
@@ -69,6 +82,15 @@ export function workspaceDetail(w) {
|
|
|
69
82
|
['operation', w.active_operation ? `${operationSummary(w.active_operation)} ${w.active_operation.id}` : '-'],
|
|
70
83
|
['pending', w.pending_reason ?? '-'],
|
|
71
84
|
['forked from', w.forked_from_workspace_id ?? '-'],
|
|
85
|
+
...(w.lifetime !== undefined
|
|
86
|
+
? [
|
|
87
|
+
['lifetime', w.lifetime === 'session' ? `session (ends after ${w.idle_timeout_seconds ?? '-'} s idle, or on close)` : 'persistent'],
|
|
88
|
+
['purpose', w.purpose],
|
|
89
|
+
['disk layout', w.disk_layout],
|
|
90
|
+
...(w.origin ? [['origin', w.origin.kind === 'fork' ? `fork of ${w.origin.workspace_id}` : `draft state ${w.origin.checkpoint_id ?? '(capturing)'} of ${w.origin.draft_workspace_id}`]] : []),
|
|
91
|
+
...(w.ended_reason ? [['ended', w.ended_reason]] : []),
|
|
92
|
+
]
|
|
93
|
+
: []),
|
|
72
94
|
['created', time(w.created_at)],
|
|
73
95
|
['deleted', time(w.deleted_at)],
|
|
74
96
|
]);
|
|
@@ -97,6 +119,38 @@ export function sessionTable(sessions) {
|
|
|
97
119
|
export function fileTable(list) {
|
|
98
120
|
return table(['TYPE', 'SIZE', 'MODIFIED', 'NAME'], list.entries.map((e) => [e.type, String(e.size), time(e.modified_at), e.name]));
|
|
99
121
|
}
|
|
122
|
+
export function secretTable(rows) {
|
|
123
|
+
return table(['ID', 'NAME', 'SCOPE', 'VERSION', 'TOOLS', 'UPDATED', 'DELETED'], rows.map((s) => [s.id, s.name, s.scope, String(s.current_version), s.permissions.allowed_tools.join(','), time(s.updated_at), time(s.deleted_at)]));
|
|
124
|
+
}
|
|
125
|
+
const idList = (ids, all) => (ids === null ? all : ids.length ? ids.join(', ') : '(none)');
|
|
126
|
+
/** Secret metadata; there is no value to show (values are write-only). */
|
|
127
|
+
export function secretDetail(s) {
|
|
128
|
+
return kv([
|
|
129
|
+
['id', s.id],
|
|
130
|
+
['name', s.name],
|
|
131
|
+
['scope', s.scope === 'project' ? `project ${s.project_id ?? '-'}` : `organization ${s.organization_id}`],
|
|
132
|
+
['description', s.description || '-'],
|
|
133
|
+
['version', String(s.current_version)],
|
|
134
|
+
['tools', s.permissions.allowed_tools.join(', ')],
|
|
135
|
+
...(s.scope === 'organization' ? [['projects', idList(s.permissions.allowed_project_ids, '(every project)')]] : []),
|
|
136
|
+
['workspaces', idList(s.permissions.allowed_workspace_ids, '(any)')],
|
|
137
|
+
['created', `${time(s.created_at)} by ${s.created_by.type}:${s.created_by.id}`],
|
|
138
|
+
['updated', time(s.updated_at)],
|
|
139
|
+
['rotated', time(s.rotated_at)],
|
|
140
|
+
['deleted', time(s.deleted_at)],
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
export function secretVersionTable(rows) {
|
|
144
|
+
return table(['VERSION', 'STATE', 'CREATED', 'BY', 'DESTROYED', 'REASON'], rows.map((v) => [String(v.version), v.state, time(v.created_at), `${v.created_by.type}:${v.created_by.id}`, time(v.destroyed_at), v.destroyed_reason ?? '-']));
|
|
145
|
+
}
|
|
146
|
+
export function secretAccessTable(rows) {
|
|
147
|
+
return table(['TIME', 'OUTCOME', 'REASON', 'TOOL', 'VERSION', 'WORKSPACE', 'PRINCIPAL', 'REQUEST'], rows.map((e) => [time(e.created_at), e.outcome, e.reason ?? '-', e.tool, e.secret_version === null ? '-' : String(e.secret_version), e.workspace_id, `${e.principal.type}:${e.principal.id}`, e.request_id ?? '-']));
|
|
148
|
+
}
|
|
149
|
+
export function bindingsText(b) {
|
|
150
|
+
if (b.secrets.length === 0)
|
|
151
|
+
return 'No secrets are bound to this workspace.\n';
|
|
152
|
+
return table(['NAME', 'STATUS', 'SCOPE', 'SECRET'], b.secrets.map((s) => [s.name, s.status, s.scope ?? '-', s.secret_id ?? '-']));
|
|
153
|
+
}
|
|
100
154
|
function isObject(v) {
|
|
101
155
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
102
156
|
}
|
|
@@ -175,3 +229,80 @@ export function errorText(e, hint) {
|
|
|
175
229
|
: '';
|
|
176
230
|
return `error: ${e.message}${reason} [${meta.join(' ')}]\n${issues}${hint ? `hint: ${hint}\n` : ''}`;
|
|
177
231
|
}
|
|
232
|
+
// ---- Templates v2 (contracts §19) ---------------------------------------------------------------
|
|
233
|
+
export function templateTable(rows) {
|
|
234
|
+
return table(['SLUG', 'OWNER', 'OPEN VERSION', 'LAYOUTS', 'FILES', 'DRAFT', 'ARCHIVED'], rows.map((t) => {
|
|
235
|
+
const v = t.open_version;
|
|
236
|
+
return [
|
|
237
|
+
t.slug,
|
|
238
|
+
t.owner,
|
|
239
|
+
v ? `v${v.version}` : '-',
|
|
240
|
+
v?.disk_layouts?.join(',') ?? '-',
|
|
241
|
+
v?.files ? v.files.state : '-',
|
|
242
|
+
t.draft ? `${t.draft.workspace_key} (${t.draft.observed_state})` : '-',
|
|
243
|
+
time(t.archived_at),
|
|
244
|
+
];
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
/** Permission bits as the four-digit octal `ls -l` users read (e.g. 0755). */
|
|
248
|
+
export function octal(mode) {
|
|
249
|
+
return mode.toString(8).padStart(4, '0');
|
|
250
|
+
}
|
|
251
|
+
export function templateFileTable(rows) {
|
|
252
|
+
return table(['TYPE', 'MODE', 'OWNER', 'SIZE', 'NAME'], rows.map((e) => [e.type, octal(e.mode), `${e.uid}:${e.gid}`, e.type === 'dir' ? `${e.child_count ?? 0} entries` : String(e.size_bytes), e.link_target ? `${e.name} -> ${e.link_target}` : e.name]));
|
|
253
|
+
}
|
|
254
|
+
export function templateEntryText(e) {
|
|
255
|
+
return kv([
|
|
256
|
+
['path', e.path],
|
|
257
|
+
['type', e.type],
|
|
258
|
+
['size', `${e.size_bytes} bytes`],
|
|
259
|
+
['mode', octal(e.mode)],
|
|
260
|
+
['owner', `${e.uid}:${e.gid}`],
|
|
261
|
+
['sha256', e.sha256 ?? '-'],
|
|
262
|
+
['link target', e.link_target ?? '-'],
|
|
263
|
+
['hardlink of', e.hardlink_of ?? '-'],
|
|
264
|
+
...(e.type === 'dir' ? [['entries', String(e.child_count ?? 0)]] : []),
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
function side(x) {
|
|
268
|
+
if (!x)
|
|
269
|
+
return '-';
|
|
270
|
+
return `${x.type} ${x.type === 'symlink' ? `-> ${x.link_target ?? '?'}` : `${x.size_bytes} B`} ${octal(x.mode)} ${x.uid}:${x.gid}`;
|
|
271
|
+
}
|
|
272
|
+
export function diffTable(rows) {
|
|
273
|
+
return table(['CHANGE', 'BEFORE', 'AFTER', 'PATH'], rows.map((d) => [d.change, side(d.before), side(d.after), d.path]));
|
|
274
|
+
}
|
|
275
|
+
export function diffSummaryText(summary) {
|
|
276
|
+
if (typeof summary !== 'object' || summary === null)
|
|
277
|
+
return '';
|
|
278
|
+
const s = summary;
|
|
279
|
+
return `added ${s.added}, removed ${s.removed}, changed ${s.changed}, type changed ${s.type_changed}, metadata ${s.metadata}; +${s.bytes_added} / -${s.bytes_removed} bytes\n`;
|
|
280
|
+
}
|
|
281
|
+
export function changesTable(rows) {
|
|
282
|
+
return table(['CHANGE', 'TYPE', 'SIZE', 'MODE', 'PATH'], rows.map((c) => [c.change, c.type, String(c.size_bytes), c.mode, c.link_target ? `${c.path} -> ${c.link_target}` : c.path]));
|
|
283
|
+
}
|
|
284
|
+
export function draftText(d) {
|
|
285
|
+
const w = d.workspace;
|
|
286
|
+
return kv([
|
|
287
|
+
['workspace', `${w.workspace_key} (${w.id})`],
|
|
288
|
+
['state', `${w.observed_state} (desired ${w.desired_state})`],
|
|
289
|
+
['base version', `${d.base_version.slug} v${d.base_version.version} (${d.base_version.id})`],
|
|
290
|
+
['states', String(d.states_count)],
|
|
291
|
+
['latest state', d.latest_state ? `${d.latest_state.checkpoint_id}${d.latest_state.label ? ` "${d.latest_state.label}"` : ''} ${time(d.latest_state.captured_at)}` : '-'],
|
|
292
|
+
['test instances', `${d.test_instances_live} live`],
|
|
293
|
+
['created', `${time(d.created_at)} by ${d.created_by.type}${d.created_by.id ? `:${d.created_by.id}` : ''}`],
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
export function draftStateTable(rows) {
|
|
297
|
+
return table(['STATE', 'LABEL', 'CAPTURED', 'BYTES', 'TEST INSTANCES'], rows.map((st) => [st.checkpoint_id, st.label ?? '-', time(st.captured_at), st.bytes === null ? '-' : String(st.bytes), String(st.test_instances)]));
|
|
298
|
+
}
|
|
299
|
+
export function buildText(b) {
|
|
300
|
+
return kv([
|
|
301
|
+
['build', b.id],
|
|
302
|
+
['template', `${b.template.slug} v${b.target_version ?? '?'}`],
|
|
303
|
+
['state', `${b.state}${b.failure ? ` (${b.failure.code}: ${b.failure.message})` : ''}`],
|
|
304
|
+
['registration', b.registration.state],
|
|
305
|
+
['source', b.source_kind === 'workspace' ? `workspace ${b.source_workspace_id ?? '-'}${b.source_checkpoint_id ? ` checkpoint ${b.source_checkpoint_id}` : ''}` : 'recipe'],
|
|
306
|
+
['version', b.template_version ? `${b.template_version.id}${b.template_version.published ? ' (published)' : ' (unpublished)'}` : '-'],
|
|
307
|
+
]) + (b.state === 'published' || b.state === 'failed' || b.state === 'canceled' ? '' : `the build continues server side (--wait follows it; GET /v1/organizations/${b.organization_id}/template-builds/${b.id} reports it)\n`);
|
|
308
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -9,5 +9,10 @@
|
|
|
9
9
|
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
10
|
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
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.
|
|
12
17
|
*/
|
|
13
|
-
export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch): typeof fetch;
|
|
18
|
+
export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch, signal?: AbortSignal): typeof fetch;
|
package/dist/http.js
CHANGED
|
@@ -9,13 +9,22 @@
|
|
|
9
9
|
* 6.28) and `Connection: close` do not stall. A fresh connection per request
|
|
10
10
|
* costs one TCP/TLS handshake, which is negligible for these call rates.
|
|
11
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.
|
|
12
17
|
*/
|
|
13
|
-
export function makeFetch(env, base = fetch) {
|
|
14
|
-
|
|
15
|
-
return base;
|
|
18
|
+
export function makeFetch(env, base = fetch, signal) {
|
|
19
|
+
const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
|
|
16
20
|
return (input, init) => {
|
|
17
21
|
const headers = new Headers(init?.headers);
|
|
18
|
-
headers.
|
|
19
|
-
|
|
22
|
+
const held = signal !== undefined && /\bwait\s*=/i.test(headers.get('prefer') ?? '');
|
|
23
|
+
if (keepAlive && !held)
|
|
24
|
+
return base(input, init);
|
|
25
|
+
if (!keepAlive)
|
|
26
|
+
headers.set('connection', 'close');
|
|
27
|
+
const s = held && signal ? (init?.signal ? AbortSignal.any([init.signal, signal]) : signal) : init?.signal;
|
|
28
|
+
return base(input, { ...init, headers, ...(s ? { signal: s } : {}) });
|
|
20
29
|
};
|
|
21
30
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* The executable is src/bin.ts; `run()` drives the same code in-process.
|
|
4
4
|
*/
|
|
5
5
|
export { run, abortableSleep } from './main.js';
|
|
6
|
-
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, topHelp } from './args.js';
|
|
6
|
+
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, resolveWake, resolveWakeTimeout, topHelp } from './args.js';
|
|
7
7
|
export type { CommandSpec, OptionSpec, Parsed, PositionalSpec, Values } from './args.js';
|
|
8
8
|
export { HANDLERS, execExitCode, resolveWorkspace } from './commands.js';
|
|
9
9
|
export type { Ctx, Handler, Io, Writer } from './commands.js';
|
|
10
10
|
export { EXIT, AuthConfigError, InterruptedError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
|
|
11
11
|
export type { ErrorInfo } from './errors.js';
|
|
12
12
|
export { errorText, json, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
13
|
+
export { TimingRecorder, timingJson, timingText } from './timing.js';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* The executable is src/bin.ts; `run()` drives the same code in-process.
|
|
4
4
|
*/
|
|
5
5
|
export { run, abortableSleep } from "./main.js";
|
|
6
|
-
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, topHelp } from "./args.js";
|
|
6
|
+
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, commandHelp, findCommand, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, resolveWake, resolveWakeTimeout, topHelp } from "./args.js";
|
|
7
7
|
export { HANDLERS, execExitCode, resolveWorkspace } from "./commands.js";
|
|
8
8
|
export { EXIT, AuthConfigError, InterruptedError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
9
9
|
export { errorText, json, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
10
|
+
export { TimingRecorder, timingJson, timingText } from "./timing.js";
|
package/dist/main.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* the whole CLI can be driven by tests; bin.ts wires it to the real process.
|
|
6
6
|
*/
|
|
7
7
|
import { SDK_VERSION, Shardflux } from '@shardflux/sdk';
|
|
8
|
-
import { CLI_VERSION, parseCommandLine, resolveApiUrl } from "./args.js";
|
|
8
|
+
import { CLI_VERSION, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
9
9
|
import { HANDLERS } from "./commands.js";
|
|
10
10
|
import { AuthConfigError, EXIT, InterruptedError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
11
11
|
import { errorText, json } from "./format.js";
|
|
12
12
|
import { makeFetch } from "./http.js";
|
|
13
|
+
import { TimingRecorder, timingJson, timingText } from "./timing.js";
|
|
13
14
|
const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
|
|
14
15
|
// Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
|
|
15
16
|
const isLabel = (s) => s.length >= 1 && s.length <= 100 && ![...s].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f);
|
|
@@ -44,6 +45,9 @@ export async function run(argv, io, opts = {}) {
|
|
|
44
45
|
const signal = opts.signal ?? new AbortController().signal;
|
|
45
46
|
const rawKey = io.env.SHARDFLUX_API_KEY;
|
|
46
47
|
const wantsJson = argv.slice(0, argv.includes('--') ? argv.indexOf('--') : argv.length).includes('--json');
|
|
48
|
+
// Every traced SDK call of the command (open, waited lifecycle calls, waits, wakes) reports here; --timing prints it.
|
|
49
|
+
const traces = new TimingRecorder();
|
|
50
|
+
let showTiming = false;
|
|
47
51
|
const report = (err) => {
|
|
48
52
|
const code = exitCodeFor(err, signal.aborted);
|
|
49
53
|
const info = describeError(err);
|
|
@@ -52,7 +56,11 @@ export async function run(argv, io, opts = {}) {
|
|
|
52
56
|
info.message = 'Interrupted.';
|
|
53
57
|
}
|
|
54
58
|
info.message = redact(info.message, rawKey);
|
|
55
|
-
|
|
59
|
+
// --timing on a failure: the failed call's timing (err.timing, else the last recorded), before the error; with
|
|
60
|
+
// --json a `timing` field next to `error`.
|
|
61
|
+
const timing = showTiming ? traces.forError(err) : null;
|
|
62
|
+
const text = wantsJson ? json(showTiming ? { error: info, timing: timingJson(timing) } : { error: info }) : `${timing ? timingText(timing) : ''}${errorText(info, hintFor(info))}`;
|
|
63
|
+
io.stderr.write(redact(text, rawKey));
|
|
56
64
|
return code;
|
|
57
65
|
};
|
|
58
66
|
let parsed;
|
|
@@ -76,13 +84,20 @@ export async function run(argv, io, opts = {}) {
|
|
|
76
84
|
const agentLabel = parsed.global.agentLabel ?? io.env.SHARDFLUX_AGENT_LABEL ?? 'cli';
|
|
77
85
|
if (!isLabel(agentLabel))
|
|
78
86
|
throw new UsageError('--agent-label must be 1-100 printable characters');
|
|
87
|
+
const wake = resolveWake(parsed.global.wake, io.env.SHARDFLUX_NO_WAKE);
|
|
88
|
+
const wakeTimeoutMs = resolveWakeTimeout(parsed.global.wakeTimeout, io.env.SHARDFLUX_WAKE_TIMEOUT_MS);
|
|
89
|
+
showTiming = parsed.values.timing === true;
|
|
79
90
|
let cloud;
|
|
80
91
|
ctx = {
|
|
81
92
|
io,
|
|
82
93
|
json: parsed.global.json,
|
|
83
94
|
apiUrl,
|
|
84
95
|
agentLabel,
|
|
96
|
+
wake,
|
|
97
|
+
wakeTimeoutMs,
|
|
85
98
|
signal,
|
|
99
|
+
timing: showTiming,
|
|
100
|
+
traces,
|
|
86
101
|
cloud() {
|
|
87
102
|
if (cloud)
|
|
88
103
|
return cloud;
|
|
@@ -91,7 +106,14 @@ export async function run(argv, io, opts = {}) {
|
|
|
91
106
|
throw new AuthConfigError('SHARDFLUX_API_KEY is not set.');
|
|
92
107
|
if (!KEY_SHAPE.test(key))
|
|
93
108
|
throw new AuthConfigError('SHARDFLUX_API_KEY is not a Shardflux project key (expected sfk_<key id>_<secret>).');
|
|
94
|
-
cloud = new Shardflux({
|
|
109
|
+
cloud = new Shardflux({
|
|
110
|
+
apiKey: key,
|
|
111
|
+
baseUrl: apiUrl,
|
|
112
|
+
userAgent: `shard-cli/${CLI_VERSION} shardflux-sdk-ts/${SDK_VERSION}`,
|
|
113
|
+
fetch: makeFetch(io.env, fetch, signal),
|
|
114
|
+
sleep: abortableSleep(signal),
|
|
115
|
+
onProgress: traces.listener,
|
|
116
|
+
});
|
|
95
117
|
return cloud;
|
|
96
118
|
},
|
|
97
119
|
};
|
|
@@ -116,5 +138,8 @@ export async function run(argv, io, opts = {}) {
|
|
|
116
138
|
return report(new InterruptedError('Interrupted.'));
|
|
117
139
|
if ('err' in outcome)
|
|
118
140
|
return report(outcome.err);
|
|
141
|
+
// With --json the handler put the timing into its output; otherwise it follows the output, on stderr.
|
|
142
|
+
if (showTiming && !ctx.json && traces.last)
|
|
143
|
+
io.stderr.write(timingText(traces.last));
|
|
119
144
|
return outcome.code;
|
|
120
145
|
}
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LifecycleTiming, ProgressListener } from '@shardflux/sdk';
|
|
2
|
+
export declare class TimingRecorder {
|
|
3
|
+
/** The timing of the last traced call that ended (token fetches are not lifecycle calls and are skipped). */
|
|
4
|
+
last: LifecycleTiming | null;
|
|
5
|
+
/** The operation the command's traced calls last observed: named when Ctrl-C interrupts a wait. */
|
|
6
|
+
operationId: string | null;
|
|
7
|
+
readonly listener: ProgressListener;
|
|
8
|
+
/**
|
|
9
|
+
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
10
|
+
* carries the token fetch's timing, which is not a lifecycle call: then only a wake (if any) counts.
|
|
11
|
+
*/
|
|
12
|
+
forError(err: unknown): LifecycleTiming | null;
|
|
13
|
+
}
|
|
14
|
+
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
15
|
+
export declare function timingText(t: LifecycleTiming): string;
|
|
16
|
+
/** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
|
|
17
|
+
export declare function timingJson(t: LifecycleTiming | null): Record<string, unknown> | null;
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--timing`: where the time of a command's lifecycle call went, as @shardflux/sdk measures it (0.6.0+). The CLI
|
|
3
|
+
* does no timing of its own: the SDK traces open(), waited lifecycle calls, waitForOperation() and wakes, and emits
|
|
4
|
+
* each trace's LifecycleTiming in a `done` progress event (also on `err.timing` when the call fails). The recorder
|
|
5
|
+
* listens on the client (every traced call of the command), so a timing exists for failures too, including ones that
|
|
6
|
+
* carry no `timing` (network errors, Ctrl-C).
|
|
7
|
+
*/
|
|
8
|
+
import { formatTiming } from '@shardflux/sdk';
|
|
9
|
+
export class TimingRecorder {
|
|
10
|
+
/** The timing of the last traced call that ended (token fetches are not lifecycle calls and are skipped). */
|
|
11
|
+
last = null;
|
|
12
|
+
/** The operation the command's traced calls last observed: named when Ctrl-C interrupts a wait. */
|
|
13
|
+
operationId = null;
|
|
14
|
+
listener = (e) => {
|
|
15
|
+
if (e.action === 'token' || e.action === 'tool')
|
|
16
|
+
return;
|
|
17
|
+
if (e.operationId)
|
|
18
|
+
this.operationId = e.operationId;
|
|
19
|
+
if (e.type === 'done')
|
|
20
|
+
this.last = e.timing;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
24
|
+
* carries the token fetch's timing, which is not a lifecycle call: then only a wake (if any) counts.
|
|
25
|
+
*/
|
|
26
|
+
forError(err) {
|
|
27
|
+
const own = typeof err === 'object' && err !== null ? err.timing : undefined;
|
|
28
|
+
return own && own.action !== 'token' ? own : this.last;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
32
|
+
export function timingText(t) {
|
|
33
|
+
return `${formatTiming(t)}\n`;
|
|
34
|
+
}
|
|
35
|
+
/** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
|
|
36
|
+
export function timingJson(t) {
|
|
37
|
+
if (!t)
|
|
38
|
+
return null;
|
|
39
|
+
const s = t.server;
|
|
40
|
+
return {
|
|
41
|
+
action: t.action,
|
|
42
|
+
outcome: t.outcome,
|
|
43
|
+
workspace_id: t.workspaceId,
|
|
44
|
+
operation_id: t.operationId,
|
|
45
|
+
total_ms: t.totalMs,
|
|
46
|
+
phases: t.phases.map((p) => ({ phase: p.phase, reason: p.reason, operation_id: p.operationId, start_ms: p.startMs, duration_ms: p.durationMs })),
|
|
47
|
+
retries: t.retries.map((r) => ({ at_ms: r.atMs, request: r.request, attempt: r.attempt, cause: r.cause, delay_ms: r.delayMs })),
|
|
48
|
+
server: s
|
|
49
|
+
? {
|
|
50
|
+
operation_id: s.operationId,
|
|
51
|
+
kind: s.kind,
|
|
52
|
+
state: s.state,
|
|
53
|
+
queued_ms: s.queuedMs,
|
|
54
|
+
run_ms: s.runMs,
|
|
55
|
+
total_ms: s.totalMs,
|
|
56
|
+
start_path: s.startPath,
|
|
57
|
+
warm_fallback: s.warmFallback,
|
|
58
|
+
resume_path: s.resumePath,
|
|
59
|
+
boot_to_ready_ms: s.bootToReadyMs,
|
|
60
|
+
host_timings_ms: s.hostTimingsMs,
|
|
61
|
+
}
|
|
62
|
+
: null,
|
|
63
|
+
outside_server_ms: t.outsideServerMs,
|
|
64
|
+
};
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|
|
37
38
|
"publishConfig": {
|
|
38
39
|
"access": "public"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@shardflux/sdk": "^0.
|
|
42
|
+
"@shardflux/sdk": "^0.6.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@eslint/js": "10.0.1",
|