@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/args.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseArgs } from 'node:util';
|
|
12
12
|
import { UsageError } from "./errors.js";
|
|
13
|
-
export const CLI_VERSION = '0.
|
|
13
|
+
export const CLI_VERSION = '0.3.0';
|
|
14
14
|
export const GLOBAL_OPTIONS = {
|
|
15
15
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
16
16
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
17
17
|
'agent-label': { type: 'string', value: '<label>', description: 'Attribution label for workspace tool tokens (default $SHARDFLUX_AGENT_LABEL, else "cli").' },
|
|
18
|
+
wake: { type: 'boolean', description: 'Resume a suspended workspace when exec/files/changes use it (default; --no-wake or SHARDFLUX_NO_WAKE=1 fails with workspace_not_running instead).' },
|
|
19
|
+
'wake-timeout': { type: 'string', value: '<ms>', description: 'Longest wait in milliseconds for the workspace to wake or finish a transition (default $SHARDFLUX_WAKE_TIMEOUT_MS, else 120000); then exit 5.' },
|
|
18
20
|
help: { type: 'boolean', short: 'h', description: 'Show help.' },
|
|
19
21
|
version: { type: 'boolean', short: 'V', description: 'Print the version.' },
|
|
20
22
|
};
|
|
@@ -23,11 +25,56 @@ const CAPS = {
|
|
|
23
25
|
'memory-mib': { type: 'string', value: '<n>', description: 'Memory cap in MiB.' },
|
|
24
26
|
'disk-gib': { type: 'string', value: '<n>', description: 'Disk cap in GiB.' },
|
|
25
27
|
};
|
|
28
|
+
/** `--timing` (0.3.0+): the SDK's lifecycle timing of the command's open, wait or wake (README "Timing"). */
|
|
29
|
+
const TIMING = {
|
|
30
|
+
timing: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Print where the time went on stderr after the call, also when it fails: client phases, the operation’s server timing, retries (with --json: a "timing" field instead).',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** `--timing` of the commands that can wake a suspended workspace: the wake's timing, when there was one. */
|
|
36
|
+
const WAKE_TIMING = {
|
|
37
|
+
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).' },
|
|
38
|
+
};
|
|
26
39
|
const WAIT = {
|
|
27
40
|
wait: { type: 'boolean', description: 'Wait until the operation finishes.' },
|
|
28
41
|
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m). The operation continues server side.' },
|
|
42
|
+
...TIMING,
|
|
29
43
|
};
|
|
30
44
|
const REF = { name: 'id|key', required: true };
|
|
45
|
+
const SLUG = { name: 'slug', required: true };
|
|
46
|
+
const PAGE = {
|
|
47
|
+
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
48
|
+
limit: { type: 'string', value: '<n>', description: 'Page size.' },
|
|
49
|
+
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
50
|
+
};
|
|
51
|
+
/** Options of a version produced by save-as-template or a draft publish (contracts §19.8). */
|
|
52
|
+
const SAVE = {
|
|
53
|
+
description: { type: 'string', value: '<text>', description: 'Version description (default: the source version’s).' },
|
|
54
|
+
'default-lifetime': { type: 'string', value: '<persistent|session>', description: 'Default lifetime of workspaces opened from the new version.' },
|
|
55
|
+
'default-idle-timeout': { type: 'string', value: '<duration>', description: 'Idle timeout of its session workspaces (60s-24h; default: the platform’s 10m).' },
|
|
56
|
+
acknowledge: { type: 'string', multiple: true, value: '<path>', description: 'Absolute path the credential scan may report without failing the build (repeatable, up to 200).' },
|
|
57
|
+
'auto-publish': { type: 'boolean', description: 'Publish the version once registered (default; --no-auto-publish leaves it for an owner/admin).' },
|
|
58
|
+
wait: { type: 'boolean', description: 'Wait until the build is published and registered (or fails).' },
|
|
59
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 30m). The build continues server side.' },
|
|
60
|
+
};
|
|
61
|
+
const LIFETIME_FILTER = {
|
|
62
|
+
lifetime: { type: 'string', value: '<persistent|session|any>', description: 'Lifetime filter (default persistent: sessions are hidden).' },
|
|
63
|
+
purpose: { type: 'string', value: '<standard|template_draft|template_test|any>', description: 'Purpose filter (default standard: template drafts and test instances are hidden).' },
|
|
64
|
+
};
|
|
65
|
+
const SECRET_REF = { name: 'id|name', required: true };
|
|
66
|
+
const SECRET_SCOPE = {
|
|
67
|
+
org: { type: 'string', value: '<organization-id>', description: 'Organization-wide secrets of this organization (owners/admins; project API keys get 403). Default: the API key’s project.' },
|
|
68
|
+
};
|
|
69
|
+
const SECRET_VALUE = {
|
|
70
|
+
'from-file': { type: 'string', value: '<file>', description: 'Read the value from this file. Default: standard input (pipe it in). One trailing newline is removed. Values are never accepted as arguments.' },
|
|
71
|
+
};
|
|
72
|
+
const SECRET_PERMISSIONS = {
|
|
73
|
+
tool: { type: 'string', multiple: true, value: '<tool>', description: 'Tool that receives the value at session start (repeatable; exec, pty, files, process, git, browser). Default exec and pty.' },
|
|
74
|
+
'allow-workspace': { type: 'string', multiple: true, value: '<workspace-id>', description: 'Only these workspaces may use it (repeatable). Default: any workspace of the allowed projects.' },
|
|
75
|
+
'allow-project': { type: 'string', multiple: true, value: '<project-id>', description: 'Organization secrets: projects whose workspaces may use it (repeatable). Default: none until granted.' },
|
|
76
|
+
'all-projects': { type: 'boolean', description: 'Organization secrets: every project of the organization may use it.' },
|
|
77
|
+
};
|
|
31
78
|
export const COMMANDS = [
|
|
32
79
|
{
|
|
33
80
|
path: ['login'],
|
|
@@ -51,11 +98,29 @@ export const COMMANDS = [
|
|
|
51
98
|
positionals: [{ name: 'key', required: true }],
|
|
52
99
|
options: {
|
|
53
100
|
template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
|
|
101
|
+
secret: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
multiple: true,
|
|
104
|
+
value: '<NAME>',
|
|
105
|
+
description: 'Bind this secret name (repeatable): injected as an environment variable into every exec and terminal. Replaces the binding of an existing workspace; omit to leave it unchanged.',
|
|
106
|
+
},
|
|
107
|
+
lifetime: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
value: '<persistent|session>',
|
|
110
|
+
description: 'session: the workspace is discarded when the session ends ("shard ws close" or the idle timeout) and the key then opens a new one. Default: the template’s default, else persistent.',
|
|
111
|
+
},
|
|
54
112
|
...CAPS,
|
|
55
113
|
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
56
114
|
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m). The start continues server side.' },
|
|
115
|
+
...TIMING,
|
|
57
116
|
},
|
|
58
|
-
examples: [
|
|
117
|
+
examples: [
|
|
118
|
+
'shard workspaces open acme/demo --template python-node-browser',
|
|
119
|
+
'shard ws open job-42 --template python-node-browser --lifetime session',
|
|
120
|
+
'shard ws open acme/demo --template python-node-browser --secret OPENAI_API_KEY --secret DATABASE_URL',
|
|
121
|
+
'shard ws open acme/demo --template python-node-browser --no-wait --json',
|
|
122
|
+
'shard ws open acme/demo --template python-node-browser --timing',
|
|
123
|
+
],
|
|
59
124
|
},
|
|
60
125
|
{
|
|
61
126
|
path: ['workspaces', 'list'],
|
|
@@ -64,7 +129,8 @@ export const COMMANDS = [
|
|
|
64
129
|
options: {
|
|
65
130
|
state: { type: 'string', value: '<state>', description: 'Observed state filter (running, suspended, creating, ...).' },
|
|
66
131
|
prefix: { type: 'string', value: '<key-prefix>', description: 'Only keys starting with this prefix.' },
|
|
67
|
-
|
|
132
|
+
...LIFETIME_FILTER,
|
|
133
|
+
'include-deleted': { type: 'boolean', description: 'Include deleted workspaces and ended sessions (tombstones).' },
|
|
68
134
|
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
69
135
|
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
70
136
|
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
@@ -81,6 +147,7 @@ export const COMMANDS = [
|
|
|
81
147
|
env: { type: 'string', multiple: true, value: '<K=V>', description: 'Environment variable (repeatable).' },
|
|
82
148
|
timeout: { type: 'string', value: '<duration>', description: 'Kill the command after this long (exit code 124).' },
|
|
83
149
|
stdin: { type: 'string', value: '<file|->', description: 'Send this file (or - for standard input) to the command’s stdin (max 1 MiB).' },
|
|
150
|
+
...WAKE_TIMING,
|
|
84
151
|
},
|
|
85
152
|
examples: ['shard ws exec acme/demo -- python3 -V', 'shard ws exec acme/demo --cwd /home/user/app --env CI=1 -- bash -lc "npm test"'],
|
|
86
153
|
},
|
|
@@ -98,12 +165,64 @@ export const COMMANDS = [
|
|
|
98
165
|
positionals: [REF],
|
|
99
166
|
options: { yes: { type: 'boolean', description: 'Required: confirm the deletion.' }, ...WAIT },
|
|
100
167
|
},
|
|
168
|
+
{
|
|
169
|
+
path: ['workspaces', 'close'],
|
|
170
|
+
summary: 'End a session workspace now (it is deleted; the key then opens a new workspace)',
|
|
171
|
+
description: 'Sessions only: a persistent workspace is refused with conflict (not_session); suspend or delete it instead. Repeating returns the same delete operation.',
|
|
172
|
+
positionals: [REF],
|
|
173
|
+
options: { ...WAIT },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
path: ['workspaces', 'reset'],
|
|
177
|
+
summary: 'Wipe every change in a layered workspace and restart it on its template',
|
|
178
|
+
description: 'Keeps the key, id, template version, caps, secret bindings and volume attachments; processes are gone. A suspended workspace stays suspended and boots blank on its next resume. The previous state stays restorable for 7 days (the operation result names the recovery checkpoint).',
|
|
179
|
+
positionals: [REF],
|
|
180
|
+
options: { yes: { type: 'boolean', description: 'Required: confirm that every change is wiped.' }, ...WAIT },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: ['workspaces', 'save-as-template'],
|
|
184
|
+
summary: 'Save a layered workspace as the next version of an organization template',
|
|
185
|
+
description: 'Everything in the workspace becomes template content (minus the sf-scrub.v1 list: machine identity, histories, credentials, .env files, caches), stored as one new layer on the workspace’s template. A running workspace is captured briefly and keeps running.',
|
|
186
|
+
positionals: [REF],
|
|
187
|
+
options: {
|
|
188
|
+
template: { type: 'string', value: '<slug>', description: 'Organization template to save into (required; created when absent).' },
|
|
189
|
+
'display-name': { type: 'string', value: '<name>', description: 'Template name when this save creates the template.' },
|
|
190
|
+
checkpoint: { type: 'string', value: '<checkpoint-id>', description: 'Save this committed checkpoint of the workspace instead of its current state.' },
|
|
191
|
+
...SAVE,
|
|
192
|
+
},
|
|
193
|
+
examples: ['shard ws save-as-template acme/demo --template acme-dev --description "node 22 + repo deps" --wait'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: ['workspaces', 'changes'],
|
|
197
|
+
summary: 'List what a layered workspace changed against its template',
|
|
198
|
+
description: 'Served by the cell gateway from the running workspace (needs the files tool). added, modified, metadata (with --hash), deleted, replaced (opaque directory).',
|
|
199
|
+
positionals: [REF],
|
|
200
|
+
options: {
|
|
201
|
+
path: { type: 'string', value: '<prefix>', description: 'Only entries at or below this absolute path (default /).' },
|
|
202
|
+
hash: { type: 'boolean', description: 'Hash regular files up to 16 MiB (reports metadata-only changes).' },
|
|
203
|
+
summary: { type: 'boolean', description: 'Also print totals over everything under --path.' },
|
|
204
|
+
...PAGE,
|
|
205
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 1000).' },
|
|
206
|
+
...WAKE_TIMING,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
101
209
|
{ path: ['workspaces', 'sessions'], summary: 'List the attributed agent sessions (tool-token principals and labels) of a workspace', positionals: [REF], options: {} },
|
|
210
|
+
{
|
|
211
|
+
path: ['workspaces', 'secrets'],
|
|
212
|
+
summary: 'Show (or replace) the secret names bound to a workspace',
|
|
213
|
+
description: 'Bound secrets are injected as environment variables into every exec and terminal of the workspace. Status: available, not_allowed (starts are refused until the secret’s permissions allow this workspace) or deleted. Values are never shown.',
|
|
214
|
+
positionals: [REF],
|
|
215
|
+
options: {
|
|
216
|
+
set: { type: 'string', multiple: true, value: '<NAME>', description: 'Replace the binding with these names (repeatable).' },
|
|
217
|
+
clear: { type: 'boolean', description: 'Remove every bound name.' },
|
|
218
|
+
},
|
|
219
|
+
examples: ['shard ws secrets acme/demo', 'shard ws secrets acme/demo --set OPENAI_API_KEY --set DATABASE_URL', 'shard ws secrets acme/demo --clear'],
|
|
220
|
+
},
|
|
102
221
|
{
|
|
103
222
|
path: ['files', 'read'],
|
|
104
223
|
summary: 'Print (or save) a file from a workspace',
|
|
105
224
|
positionals: [REF, { name: 'path', required: true }],
|
|
106
|
-
options: { out: { type: 'string', value: '<file>', description: 'Write to this local file instead of stdout.' } },
|
|
225
|
+
options: { out: { type: 'string', value: '<file>', description: 'Write to this local file instead of stdout.' }, ...WAKE_TIMING },
|
|
107
226
|
},
|
|
108
227
|
{
|
|
109
228
|
path: ['files', 'write'],
|
|
@@ -113,13 +232,14 @@ export const COMMANDS = [
|
|
|
113
232
|
from: { type: 'string', value: '<file|->', description: 'Local file to upload, or - for standard input (default -).' },
|
|
114
233
|
append: { type: 'boolean', description: 'Append instead of replacing.' },
|
|
115
234
|
parents: { type: 'boolean', description: 'Create missing parent directories (default; --no-parents to refuse).' },
|
|
235
|
+
...WAKE_TIMING,
|
|
116
236
|
},
|
|
117
237
|
},
|
|
118
238
|
{
|
|
119
239
|
path: ['files', 'ls'],
|
|
120
240
|
summary: 'List a directory in a workspace',
|
|
121
241
|
positionals: [REF, { name: 'path', required: true }],
|
|
122
|
-
options: { limit: { type: 'string', value: '<n>', description: 'Maximum entries (default 1000).' } },
|
|
242
|
+
options: { limit: { type: 'string', value: '<n>', description: 'Maximum entries (default 1000).' }, ...WAKE_TIMING },
|
|
123
243
|
},
|
|
124
244
|
{
|
|
125
245
|
path: ['operations', 'list'],
|
|
@@ -132,19 +252,177 @@ export const COMMANDS = [
|
|
|
132
252
|
},
|
|
133
253
|
},
|
|
134
254
|
{ path: ['operations', 'get'], summary: 'Show one operation', positionals: [{ name: 'operation-id', required: true }], options: {} },
|
|
255
|
+
{
|
|
256
|
+
path: ['secrets', 'create'],
|
|
257
|
+
summary: 'Create a secret; the value is read from standard input or --from-file, never from arguments',
|
|
258
|
+
description: 'Project secret of the API key’s project by default, or an organization-wide secret with --org. The value (UTF-8 text, up to 64 KiB) is encrypted and never shown again. Bind it to workspaces with "shard ws open --secret NAME" or "shard ws secrets <ws> --set NAME".',
|
|
259
|
+
positionals: [{ name: 'name', required: true }],
|
|
260
|
+
options: { ...SECRET_SCOPE, ...SECRET_VALUE, description: { type: 'string', value: '<text>', description: 'Description (up to 500 characters).' }, ...SECRET_PERMISSIONS },
|
|
261
|
+
examples: ['printf %s "$OPENAI_API_KEY" | shard secrets create OPENAI_API_KEY', 'shard secrets create DATABASE_URL --from-file ./database-url.txt --description "Staging database"'],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
path: ['secrets', 'list'],
|
|
265
|
+
summary: 'List secrets (metadata only; never values)',
|
|
266
|
+
positionals: [],
|
|
267
|
+
options: {
|
|
268
|
+
...SECRET_SCOPE,
|
|
269
|
+
'include-deleted': { type: 'boolean', description: 'Include deleted secrets.' },
|
|
270
|
+
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
271
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
272
|
+
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{ path: ['secrets', 'get'], summary: 'Show one secret’s metadata (never the value)', positionals: [SECRET_REF], options: { ...SECRET_SCOPE } },
|
|
276
|
+
{
|
|
277
|
+
path: ['secrets', 'update'],
|
|
278
|
+
summary: 'Change a secret’s description or usage permissions (applies from the next session start)',
|
|
279
|
+
positionals: [SECRET_REF],
|
|
280
|
+
options: {
|
|
281
|
+
...SECRET_SCOPE,
|
|
282
|
+
description: { type: 'string', value: '<text>', description: 'New description.' },
|
|
283
|
+
...SECRET_PERMISSIONS,
|
|
284
|
+
'any-workspace': { type: 'boolean', description: 'Remove the workspace restriction (any workspace of the allowed projects).' },
|
|
285
|
+
'clear-projects': { type: 'boolean', description: 'Organization secrets: no project may use it (until granted again).' },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
path: ['secrets', 'rotate'],
|
|
290
|
+
summary: 'Store a new value as the next version (standard input or --from-file); older values are erased',
|
|
291
|
+
positionals: [SECRET_REF],
|
|
292
|
+
options: { ...SECRET_SCOPE, ...SECRET_VALUE },
|
|
293
|
+
examples: ['printf %s "$NEW_TOKEN" | shard secrets rotate API_TOKEN'],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
path: ['secrets', 'versions'],
|
|
297
|
+
summary: 'List a secret’s versions, newest first (metadata only)',
|
|
298
|
+
positionals: [SECRET_REF],
|
|
299
|
+
options: { ...SECRET_SCOPE, limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' } },
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: ['secrets', 'delete'],
|
|
303
|
+
summary: 'Delete a secret: its values are erased and it is removed from every workspace binding',
|
|
304
|
+
positionals: [SECRET_REF],
|
|
305
|
+
options: { ...SECRET_SCOPE, yes: { type: 'boolean', description: 'Required: confirm the deletion.' } },
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
path: ['secrets', 'access-log'],
|
|
309
|
+
summary: 'Show which sessions resolved a secret (owners/admins; project API keys get 403)',
|
|
310
|
+
positionals: [SECRET_REF],
|
|
311
|
+
options: {
|
|
312
|
+
...SECRET_SCOPE,
|
|
313
|
+
outcome: { type: 'string', value: '<outcome>', description: 'granted, denied or failed.' },
|
|
314
|
+
workspace: { type: 'string', value: '<workspace-id>', description: 'Only this workspace.' },
|
|
315
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
path: ['templates', 'list'],
|
|
320
|
+
summary: 'List the templates this project can open (platform and organization)',
|
|
321
|
+
positionals: [],
|
|
322
|
+
options: {
|
|
323
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Only platform or only organization templates.' },
|
|
324
|
+
'include-archived': { type: 'boolean', description: 'Include archived templates.' },
|
|
325
|
+
...PAGE,
|
|
326
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
path: ['templates', 'files'],
|
|
331
|
+
summary: 'List one directory of a template version’s file tree (or show one entry with --stat)',
|
|
332
|
+
description: 'Needs a version with a file list (versions from before file lists answer conflict file_list_unavailable). File contents are not served: open a draft or test instance for that.',
|
|
333
|
+
positionals: [SLUG, { name: 'version', required: true }, { name: 'path' }],
|
|
334
|
+
options: {
|
|
335
|
+
stat: { type: 'boolean', description: 'Show the entry at <path> itself instead of listing it.' },
|
|
336
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Pick the platform template even when an organization template shadows its slug.' },
|
|
337
|
+
...PAGE,
|
|
338
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 200).' },
|
|
339
|
+
},
|
|
340
|
+
examples: ['shard templates files python-node-browser 5 /usr/local/bin', 'shard templates files acme-dev 3 /etc/hostname --stat'],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
path: ['templates', 'diff'],
|
|
344
|
+
summary: 'Diff two versions of a template (added, removed, changed, type_changed, metadata)',
|
|
345
|
+
positionals: [SLUG],
|
|
346
|
+
options: {
|
|
347
|
+
from: { type: 'string', value: '<version|base>', description: 'Older version, or "base" (the --to version’s build base). Required.' },
|
|
348
|
+
to: { type: 'string', value: '<version>', description: 'Newer version (required).' },
|
|
349
|
+
prefix: { type: 'string', value: '<path>', description: 'Only paths starting with this absolute prefix.' },
|
|
350
|
+
change: { type: 'string', value: '<kind>', description: 'Only added, removed, changed, type_changed or metadata.' },
|
|
351
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Pick the platform template even when an organization template shadows its slug.' },
|
|
352
|
+
...PAGE,
|
|
353
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 200).' },
|
|
354
|
+
},
|
|
355
|
+
examples: ['shard templates diff acme-dev --from 2 --to 3', 'shard templates diff acme-dev --from base --to 3 --prefix /home/user'],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
path: ['templates', 'draft', 'open'],
|
|
359
|
+
summary: 'Open the draft of an organization template (a layered workspace to edit live)',
|
|
360
|
+
description: 'One live draft per template. Edit it with "shard ws exec|files ... <draft key>", capture states, open test instances, then publish it as the next version.',
|
|
361
|
+
positionals: [SLUG],
|
|
362
|
+
options: {
|
|
363
|
+
base: { type: 'string', value: '<slug@version>', description: 'Version to start from (default: the template’s latest published version; required for a new template).' },
|
|
364
|
+
...CAPS,
|
|
365
|
+
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
366
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m).' },
|
|
367
|
+
...TIMING,
|
|
368
|
+
},
|
|
369
|
+
examples: ['shard templates draft open acme-dev --base python-node-browser@5'],
|
|
370
|
+
},
|
|
371
|
+
{ path: ['templates', 'draft', 'status'], summary: 'Show a template’s draft: workspace, base version, states and live test instances', positionals: [SLUG], options: {} },
|
|
372
|
+
{
|
|
373
|
+
path: ['templates', 'draft', 'state'],
|
|
374
|
+
summary: 'Capture a state of the running draft (or list its states with --list)',
|
|
375
|
+
positionals: [SLUG],
|
|
376
|
+
options: {
|
|
377
|
+
label: { type: 'string', value: '<text>', description: 'Label of the captured state.' },
|
|
378
|
+
list: { type: 'boolean', description: 'List the draft’s states instead of capturing one.' },
|
|
379
|
+
...WAIT,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
path: ['templates', 'draft', 'test'],
|
|
384
|
+
summary: 'Open a test instance of a draft state (a disposable session workspace), or list them with --list',
|
|
385
|
+
description: 'Its writes never reach the draft. It ends with "shard ws close <key>", when idle, or when the draft is discarded.',
|
|
386
|
+
positionals: [SLUG],
|
|
387
|
+
options: {
|
|
388
|
+
state: { type: 'string', value: '<state-id>', description: 'Draft state to test (default: a fresh capture of the running draft).' },
|
|
389
|
+
'instance-key': { type: 'string', value: '<key>', description: 'Workspace key of the test instance (default sf:test:<slug>:<random>).' },
|
|
390
|
+
list: { type: 'boolean', description: 'List the draft’s test instances instead of opening one.' },
|
|
391
|
+
'include-ended': { type: 'boolean', description: 'With --list: include ended test instances.' },
|
|
392
|
+
...CAPS,
|
|
393
|
+
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
394
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m).' },
|
|
395
|
+
...TIMING,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
path: ['templates', 'draft', 'publish'],
|
|
400
|
+
summary: 'Publish the draft as the template’s next version',
|
|
401
|
+
description: 'Refused (conflict draft_stale) when the template got a version from elsewhere since the draft was opened. The draft stays open afterwards.',
|
|
402
|
+
positionals: [SLUG],
|
|
403
|
+
options: { state: { type: 'string', value: '<state-id>', description: 'State to publish (default: the draft’s current state).' }, ...SAVE },
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
path: ['templates', 'draft', 'discard'],
|
|
407
|
+
summary: 'Discard the draft (it is deleted and its live test instances end)',
|
|
408
|
+
positionals: [SLUG],
|
|
409
|
+
options: { yes: { type: 'boolean', description: 'Required: confirm the discard.' }, ...WAIT },
|
|
410
|
+
},
|
|
135
411
|
{
|
|
136
412
|
path: ['operations', 'wait'],
|
|
137
413
|
summary: 'Wait for an operation to finish (exit 0 succeeded, 6 failed/canceled, 5 timeout)',
|
|
138
414
|
positionals: [{ name: 'operation-id', required: true }],
|
|
139
|
-
options: { timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' } },
|
|
415
|
+
options: { timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' }, ...TIMING },
|
|
140
416
|
},
|
|
141
417
|
];
|
|
142
418
|
const GROUPS = {
|
|
143
419
|
workspaces: 'Open, inspect and manage workspaces',
|
|
144
420
|
files: 'Read, write and list files in a workspace',
|
|
145
421
|
operations: 'Inspect and wait for lifecycle operations',
|
|
422
|
+
secrets: 'Manage secrets (values are write-only) and bind them to workspaces',
|
|
423
|
+
templates: 'Browse templates (file tree, diff) and develop organization templates in a draft',
|
|
146
424
|
};
|
|
147
|
-
const ALIASES = { ws: 'workspaces', workspace: 'workspaces', ops: 'operations', operation: 'operations', file: 'files' };
|
|
425
|
+
const ALIASES = { ws: 'workspaces', workspace: 'workspaces', ops: 'operations', operation: 'operations', file: 'files', secret: 'secrets', template: 'templates' };
|
|
148
426
|
/** Canonical command words: aliases resolved, `workspaces files|operations` folded to the top-level group. */
|
|
149
427
|
export function normalizeWords(words) {
|
|
150
428
|
const w = words.map((x) => ALIASES[x] ?? x);
|
|
@@ -202,6 +480,8 @@ export function parseEnvPairs(pairs) {
|
|
|
202
480
|
return out;
|
|
203
481
|
}
|
|
204
482
|
export const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
483
|
+
/** Secret names (environment-variable safe; the API's SECRET_NAME_PATTERN). */
|
|
484
|
+
export const SECRET_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
205
485
|
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
206
486
|
/** Validates the API base URL: https, or http only to a loopback address (the key is a bearer credential). */
|
|
207
487
|
export function resolveApiUrl(flag, env) {
|
|
@@ -220,8 +500,37 @@ export function resolveApiUrl(flag, env) {
|
|
|
220
500
|
throw new UsageError('API URL must not contain credentials, a query or a fragment');
|
|
221
501
|
return u.toString().replace(/\/+$/, '');
|
|
222
502
|
}
|
|
223
|
-
const
|
|
224
|
-
const
|
|
503
|
+
export const DEFAULT_WAKE_TIMEOUT_MS = 120_000;
|
|
504
|
+
const MAX_WAKE_TIMEOUT_MS = 7 * 24 * 3600 * 1000;
|
|
505
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
506
|
+
const FALSY = new Set(['', '0', 'false', 'no', 'off']);
|
|
507
|
+
/** Wake on use: --wake / --no-wake, else SHARDFLUX_NO_WAKE (1/true/yes/on disables), else on. */
|
|
508
|
+
export function resolveWake(flag, noWakeEnv) {
|
|
509
|
+
if (flag !== undefined)
|
|
510
|
+
return flag;
|
|
511
|
+
const v = (noWakeEnv ?? '').trim().toLowerCase();
|
|
512
|
+
if (TRUTHY.has(v))
|
|
513
|
+
return false;
|
|
514
|
+
if (FALSY.has(v))
|
|
515
|
+
return true;
|
|
516
|
+
throw new UsageError(`SHARDFLUX_NO_WAKE must be 1 (or true) to turn wake on use off, or 0/empty (got "${noWakeEnv}")`);
|
|
517
|
+
}
|
|
518
|
+
/** The wake/transition budget per call in ms: --wake-timeout, else SHARDFLUX_WAKE_TIMEOUT_MS, else 120000. */
|
|
519
|
+
export function resolveWakeTimeout(flag, env) {
|
|
520
|
+
if (flag !== undefined)
|
|
521
|
+
return parsePositiveInt(flag, 'wake-timeout', MAX_WAKE_TIMEOUT_MS);
|
|
522
|
+
const v = env?.trim();
|
|
523
|
+
if (!v)
|
|
524
|
+
return DEFAULT_WAKE_TIMEOUT_MS;
|
|
525
|
+
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > MAX_WAKE_TIMEOUT_MS) {
|
|
526
|
+
throw new UsageError(`SHARDFLUX_WAKE_TIMEOUT_MS must be an integer number of milliseconds between 1 and ${MAX_WAKE_TIMEOUT_MS} (got "${env}")`);
|
|
527
|
+
}
|
|
528
|
+
return Number(v);
|
|
529
|
+
}
|
|
530
|
+
const FORBIDDEN_KEY_FLAG = /^--?(api-?key|apikey|key|token|secret|value|secret-value|password)(=.*)?$/i;
|
|
531
|
+
/** `--secret NAME` binds a secret by name on `workspaces open`; anything else shaped like --secret is refused. */
|
|
532
|
+
const SECRET_NAME_FLAG = /^--secret(?:=(.*))?$/;
|
|
533
|
+
const GLOBAL_VALUE_FLAGS = new Set(['--api-url', '--agent-label', '--wake-timeout']);
|
|
225
534
|
function toParseArgsOptions(opts) {
|
|
226
535
|
const out = {};
|
|
227
536
|
for (const [name, o] of Object.entries(opts))
|
|
@@ -235,10 +544,30 @@ function toParseArgsOptions(opts) {
|
|
|
235
544
|
export function parseCommandLine(argv) {
|
|
236
545
|
const terminator = argv.indexOf('--');
|
|
237
546
|
const head = terminator < 0 ? argv : argv.slice(0, terminator);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
547
|
+
const leading = [];
|
|
548
|
+
for (let i = 0; i < head.length && leading.length < 2; i += 1) {
|
|
549
|
+
const a = head[i];
|
|
550
|
+
if (!a.startsWith('-'))
|
|
551
|
+
leading.push(a);
|
|
552
|
+
else if (GLOBAL_VALUE_FLAGS.has(a))
|
|
553
|
+
i += 1;
|
|
554
|
+
}
|
|
555
|
+
const opensWorkspace = key(normalizeWords(leading)) === 'workspaces open';
|
|
556
|
+
for (let i = 0; i < head.length; i += 1) {
|
|
557
|
+
const a = head[i];
|
|
558
|
+
if (!FORBIDDEN_KEY_FLAG.test(a))
|
|
559
|
+
continue;
|
|
560
|
+
const named = SECRET_NAME_FLAG.exec(a);
|
|
561
|
+
if (named && opensWorkspace) {
|
|
562
|
+
const name = named[1] ?? head[i + 1];
|
|
563
|
+
if (name !== undefined && SECRET_NAME.test(name))
|
|
564
|
+
continue;
|
|
565
|
+
throw new UsageError('--secret takes a secret NAME (^[A-Z_][A-Z0-9_]*$) to bind, never a value; create the secret with "shard secrets create NAME" (value on standard input)');
|
|
566
|
+
}
|
|
567
|
+
if (/^--?(value|secret-value|password)(=.*)?$/i.test(a)) {
|
|
568
|
+
throw new UsageError('shard never accepts secret values on the command line (shell history and process listings would expose them); pipe the value on standard input or use --from-file');
|
|
241
569
|
}
|
|
570
|
+
throw new UsageError('shard reads the API key only from the SHARDFLUX_API_KEY environment variable; never pass it on the command line (shell history and process listings would expose it)');
|
|
242
571
|
}
|
|
243
572
|
// Pick the command words (the leading non-option words that form a known command path).
|
|
244
573
|
const words = [];
|
|
@@ -323,6 +652,8 @@ export function parseCommandLine(argv) {
|
|
|
323
652
|
apiUrl: typeof values['api-url'] === 'string' ? values['api-url'] : undefined,
|
|
324
653
|
json: values.json === true,
|
|
325
654
|
agentLabel: typeof values['agent-label'] === 'string' ? values['agent-label'] : undefined,
|
|
655
|
+
wake: typeof values.wake === 'boolean' ? values.wake : undefined,
|
|
656
|
+
wakeTimeout: typeof values['wake-timeout'] === 'string' ? values['wake-timeout'] : undefined,
|
|
326
657
|
},
|
|
327
658
|
};
|
|
328
659
|
}
|
|
@@ -380,9 +711,11 @@ export function topHelp() {
|
|
|
380
711
|
...optionLines(GLOBAL_OPTIONS),
|
|
381
712
|
'',
|
|
382
713
|
'Environment:',
|
|
383
|
-
' SHARDFLUX_API_KEY
|
|
384
|
-
' SHARDFLUX_API_URL
|
|
385
|
-
' SHARDFLUX_AGENT_LABEL
|
|
714
|
+
' SHARDFLUX_API_KEY project API key (sfk_...), required; never pass it as an argument',
|
|
715
|
+
' SHARDFLUX_API_URL API base URL (default https://api.shardflux.dev)',
|
|
716
|
+
' SHARDFLUX_AGENT_LABEL attribution label for tool tokens (default cli)',
|
|
717
|
+
' SHARDFLUX_NO_WAKE 1: do not resume a suspended workspace on use (same as --no-wake)',
|
|
718
|
+
' SHARDFLUX_WAKE_TIMEOUT_MS longest wait in ms for a workspace to wake or finish a transition (default 120000; --wake-timeout overrides)',
|
|
386
719
|
'',
|
|
387
720
|
'Exit codes:',
|
|
388
721
|
...EXIT_CODE_HELP,
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RunResult, Shardflux, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Values } from './args.js';
|
|
3
|
+
import type { TimingRecorder } from './timing.js';
|
|
3
4
|
export interface Writer {
|
|
4
5
|
write(chunk: string | Uint8Array): boolean;
|
|
5
6
|
}
|
|
@@ -16,11 +17,23 @@ export interface Ctx {
|
|
|
16
17
|
json: boolean;
|
|
17
18
|
apiUrl: string;
|
|
18
19
|
agentLabel: string;
|
|
20
|
+
/** Wake a suspended workspace on use (--no-wake / SHARDFLUX_NO_WAKE=1 turn it off). */
|
|
21
|
+
wake: boolean;
|
|
22
|
+
/** Bound on lifecycle waits per cell call, wakes included (--wake-timeout / SHARDFLUX_WAKE_TIMEOUT_MS). */
|
|
23
|
+
wakeTimeoutMs: number;
|
|
19
24
|
signal: AbortSignal;
|
|
25
|
+
/** --timing: print the SDK's timing of the command's open, wait or wake (stderr; a `timing` field with --json). */
|
|
26
|
+
timing: boolean;
|
|
27
|
+
/** Timings and the last observed operation of the traced SDK calls this command made (the client's onProgress). */
|
|
28
|
+
traces: TimingRecorder;
|
|
20
29
|
cloud(): Shardflux;
|
|
21
30
|
}
|
|
22
31
|
export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promise<number>;
|
|
23
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
|
|
34
|
+
* lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
|
|
35
|
+
* workspace over tombstones of ended sessions that had the same key (contracts §19.11).
|
|
36
|
+
*/
|
|
24
37
|
export declare function resolveWorkspace(ctx: Ctx, ref: string): Promise<Workspace>;
|
|
25
38
|
/** The exit code `shard exec` returns for a finished command. */
|
|
26
39
|
export declare function execExitCode(r: Pick<RunResult, 'exitCode' | 'termSignal' | 'timedOut'>): number;
|