@shardflux/cli 0.1.0 → 0.2.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/README.md +70 -8
- package/dist/args.d.ts +10 -1
- package/dist/args.js +326 -12
- package/dist/commands.d.ts +9 -1
- package/dist/commands.js +585 -11
- package/dist/format.d.ts +20 -1
- package/dist/format.js +132 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +5 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ shard ws suspend acme/demo --wait
|
|
|
42
42
|
| `SHARDFLUX_API_KEY` | Project API key (`sfk_<key id>_<secret>`). Required. `shard` never accepts the key as an argument (`--api-key`, `--token` and similar flags are refused) and never prints it; anything shaped like a key is redacted from error output. |
|
|
43
43
|
| `SHARDFLUX_API_URL` | API base URL. Default `https://api.shardflux.dev`; `--api-url` overrides it. Plain `http://` is allowed only for loopback hosts. |
|
|
44
44
|
| `SHARDFLUX_AGENT_LABEL` | Attribution label for the tool tokens `shard` obtains for exec and files. Default `cli`; `--agent-label` overrides it. Each label is one agent session, listed by `shard ws sessions`. |
|
|
45
|
+
| `SHARDFLUX_NO_WAKE=1` | Do not resume a suspended workspace on use; same as `--no-wake` (see "Suspended workspaces" below). `--wake` overrides it. |
|
|
46
|
+
| `SHARDFLUX_WAKE_TIMEOUT_MS` | Longest wait, in milliseconds, for a workspace to wake or finish a transition. Default `120000`; `--wake-timeout <ms>` overrides it. |
|
|
45
47
|
| `SHARDFLUX_HTTP_KEEPALIVE=1` | Reuse HTTP connections (see "HTTP connections" below). |
|
|
46
48
|
|
|
47
49
|
`shard login` stores nothing. There is no config file: the key is read from the environment on
|
|
@@ -54,8 +56,8 @@ every run.
|
|
|
54
56
|
| `login`, `whoami` | Show the principal behind the key. |
|
|
55
57
|
| `version` | Print the CLI and SDK versions. |
|
|
56
58
|
| `usage [--org <id>]` | Usage summary for the current period: plan, meters and allowances. |
|
|
57
|
-
| `workspaces open <key> --template <slug> [--cpu-millis N --memory-mib N --disk-gib N] [--no-wait] [--timeout 5m]` | Open by key. Creates the workspace on first use and reconnects or resumes it afterwards, never resetting it. Waits until it is ready unless you pass `--no-wait`. |
|
|
58
|
-
| `workspaces list [--state S] [--prefix P] [--include-deleted] [--all] [--limit N] [--cursor C]` | One page, or every page with `--all`. The next cursor is printed on stderr. |
|
|
59
|
+
| `workspaces open <key> --template <slug> [--secret NAME]... [--lifetime persistent\|session] [--cpu-millis N --memory-mib N --disk-gib N] [--no-wait] [--timeout 5m]` | Open by key. Creates the workspace on first use and reconnects or resumes it afterwards, never resetting it. Waits until it is ready unless you pass `--no-wait`. `--secret` binds secret names (replacing the binding of an existing workspace). `--lifetime session`: the workspace is discarded when the session ends (`workspaces close` or the idle timeout), and the key then opens a new workspace. |
|
|
60
|
+
| `workspaces list [--state S] [--prefix P] [--lifetime persistent\|session\|any] [--purpose standard\|template_draft\|template_test\|any] [--include-deleted] [--all] [--limit N] [--cursor C]` | One page, or every page with `--all`. The next cursor is printed on stderr. By default only persistent standard workspaces are listed. |
|
|
59
61
|
| `workspaces get <id\|key>` | One workspace (deleted ones included). |
|
|
60
62
|
| `workspaces exec <id\|key> [--cwd D] [--env K=V]... [--timeout T] [--stdin F\|-] -- <cmd> [args...]` | Runs argv with no shell. Output is streamed and resumes from byte offsets after dropped connections. `shard` exits with the command's exit code. Ctrl-C cancels the command. |
|
|
61
63
|
| `files read <id\|key> <path> [--out F]` | Raw bytes to stdout, or to a local file. |
|
|
@@ -63,13 +65,70 @@ every run.
|
|
|
63
65
|
| `files ls <id\|key> <path> [--limit N]` | Directory listing. |
|
|
64
66
|
| `workspaces suspend\|resume <id\|key> [--wait] [--timeout T]` | Lifecycle operation. Returns immediately unless you pass `--wait`. |
|
|
65
67
|
| `workspaces fork <id\|key> <new-key> [caps] [--wait]` | Fork into a new key. |
|
|
66
|
-
| `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once
|
|
68
|
+
| `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once. The key of a persistent workspace is never reused. |
|
|
69
|
+
| `workspaces close <id\|key> [--wait]` | End a session workspace now: it is deleted, and the key then opens a new workspace. A persistent workspace is refused (`not_session`). |
|
|
70
|
+
| `workspaces reset <id\|key> --yes [--wait]` | Layered workspaces: wipe every change and restart on the template. The previous state stays restorable for 7 days. |
|
|
71
|
+
| `workspaces save-as-template <id\|key> --template <slug> [--display-name N] [--description T] [--checkpoint ID] [--default-lifetime L] [--default-idle-timeout D] [--acknowledge PATH]... [--no-auto-publish] [--wait]` | Save a layered workspace as the next version of an organization template. `--wait` follows the build. |
|
|
72
|
+
| `workspaces changes <id\|key> [--path P] [--hash] [--summary] [--all] [--limit N] [--cursor C]` | What a layered, running workspace changed against its template. |
|
|
67
73
|
| `workspaces sessions <id\|key>` | Attributed agent sessions: label, principal and tokens issued. |
|
|
74
|
+
| `workspaces secrets <id\|key> [--set NAME]... [--clear]` | Show the secret names bound to the workspace and their status, or replace them. |
|
|
75
|
+
| `secrets create <NAME> [--from-file F] [--description T] [--tool T]... [--allow-workspace ID]... [--org ID [--allow-project ID]... [--all-projects]]` | Create a secret. The value comes from standard input or `--from-file`, never from an argument. |
|
|
76
|
+
| `secrets list [--org ID] [--include-deleted] [--all]`, `secrets get <id\|NAME>` | Secret metadata. Values are never shown. |
|
|
77
|
+
| `secrets update <id\|NAME> [--description T] [--tool T]... [--allow-workspace ID]... [--any-workspace] [--allow-project ID]... [--all-projects] [--clear-projects]` | Change the description or usage permissions. |
|
|
78
|
+
| `secrets rotate <id\|NAME> [--from-file F]` | Store a new value (standard input or file); older values are erased. |
|
|
79
|
+
| `secrets versions <id\|NAME>`, `secrets delete <id\|NAME> --yes`, `secrets access-log <id\|NAME>` | Versions, deletion (also removes the name from every workspace binding), and the access log. |
|
|
68
80
|
| `operations list <id\|key> [--state S] [--kind K]`, `operations get <op>`, `operations wait <op> [--timeout T]` | Operations. |
|
|
81
|
+
| `templates list [--owner platform\|organization] [--include-archived] [--all]` | Templates the project can open, with their open version, layouts, file list state and draft. |
|
|
82
|
+
| `templates files <slug> <version> [path] [--stat] [--owner O] [--all] [--limit N]` | One directory of a version's file tree, or one entry with `--stat`. |
|
|
83
|
+
| `templates diff <slug> --from <version\|base> --to <version> [--prefix P] [--change K] [--all]` | Diff two versions: added, removed, changed, type_changed, metadata. The totals come first. |
|
|
84
|
+
| `templates draft open <slug> [--base slug@version] [caps] [--no-wait]` | Open the template's draft, a layered workspace you edit with `workspaces exec`/`files`. |
|
|
85
|
+
| `templates draft status <slug>` | The draft's workspace, base version, states and live test instances. |
|
|
86
|
+
| `templates draft state <slug> [--label T] [--wait]`, `... state <slug> --list` | Capture a state of the running draft, or list its states. |
|
|
87
|
+
| `templates draft test <slug> [--state ID] [--instance-key K] [--no-wait]`, `... test <slug> --list [--include-ended]` | Open a test instance (a session on a copy of a state), or list them. |
|
|
88
|
+
| `templates draft publish <slug> [--state ID] [--description T] [--default-lifetime L] [--acknowledge PATH]... [--no-auto-publish] [--wait]` | Publish the draft as the template's next version. |
|
|
89
|
+
| `templates draft discard <slug> --yes [--wait]` | Delete the draft and end its live test instances. |
|
|
90
|
+
|
|
91
|
+
Aliases: `ws` for `workspaces`, `ops` for `operations`, `secret` for `secrets`, `template` for `templates`.
|
|
92
|
+
`shard workspaces files ...` and `shard workspaces operations ...` also work. `<id|key>` is tried as an id when it
|
|
93
|
+
is a UUID. Otherwise, or if no workspace has that id, it is matched as an exact workspace key across every lifetime
|
|
94
|
+
and purpose, so sessions, drafts and test instances resolve too. The live workspace wins over tombstones that ended
|
|
95
|
+
sessions left with the same key.
|
|
96
|
+
|
|
97
|
+
## Suspended workspaces
|
|
98
|
+
|
|
99
|
+
`workspaces exec`, `files read|write|ls` and `workspaces changes` wake a suspended workspace: they
|
|
100
|
+
resume it (or join the resume or open already running), wait until it runs, then run the request.
|
|
101
|
+
A request made during a suspend or resume waits for the transition to finish. The request itself
|
|
102
|
+
never runs twice, because the workspace refused it before doing anything.
|
|
103
|
+
|
|
104
|
+
- `--wake-timeout <ms>` (or `SHARDFLUX_WAKE_TIMEOUT_MS`, default `120000`) bounds the wait. Past
|
|
105
|
+
it, `shard` exits 5 and names the operation and its state (for example `Operation <id> (open)
|
|
106
|
+
is still queued after 120000 ms`). The operation continues server side:
|
|
107
|
+
`shard operations wait <id>` keeps waiting.
|
|
108
|
+
- A resume or open that fails exits 6.
|
|
109
|
+
- `--no-wake` (or `SHARDFLUX_NO_WAKE=1`) turns this off. A workspace that is not running is then
|
|
110
|
+
refused at once (exit 1, `workspace_not_running`).
|
|
111
|
+
|
|
112
|
+
## Secrets
|
|
69
113
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
```sh
|
|
115
|
+
printf %s "$OPENAI_API_KEY" | shard secrets create OPENAI_API_KEY # value from standard input
|
|
116
|
+
shard secrets create DATABASE_URL --from-file ./database-url.txt # or from a file
|
|
117
|
+
shard ws open acme/demo --template python-node-browser --secret OPENAI_API_KEY --secret DATABASE_URL
|
|
118
|
+
shard ws exec acme/demo -- python3 agent.py # sees both as environment variables
|
|
119
|
+
shard ws secrets acme/demo # names and status, never values
|
|
120
|
+
printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- Secret values are read only from standard input or `--from-file` (one trailing newline is
|
|
124
|
+
removed). `shard` refuses `--value`, `--password` and similar flags, and never prints a value.
|
|
125
|
+
- Bound secrets reach every `exec` and terminal of the workspace. A name that is unknown, or a
|
|
126
|
+
secret the workspace may not use, is refused (exit 2, `secret_not_available` with the names).
|
|
127
|
+
- Secrets are project secrets of the API key's project by default. `--org <id>` addresses
|
|
128
|
+
organization-wide secrets; those, and `secrets access-log`, belong to organization owners and
|
|
129
|
+
admins in the console, so a project API key gets 403 (exit 4).
|
|
130
|
+
- `<id|NAME>` is tried as an id when it is a UUID; otherwise it is matched by name in the project
|
|
131
|
+
(or in the organization with `--org`).
|
|
73
132
|
|
|
74
133
|
## Output
|
|
75
134
|
|
|
@@ -82,6 +141,9 @@ otherwise, or if no workspace has that id, it is matched as an exact workspace k
|
|
|
82
141
|
- `login`/`whoami` print `{api_url, organization_id, project_id, api_key:{id,key_id}, tool_permissions}`.
|
|
83
142
|
- `exec` prints `{session_id, exit_code, term_signal, timed_out, canceled, stdout, stderr, truncated, reconnects}`.
|
|
84
143
|
- `files read` prints `{path, bytes, content_base64}`; `files write` prints the write result.
|
|
144
|
+
- `secrets create`/`get`/`update` print the secret's metadata; `list`, `versions` and `access-log`
|
|
145
|
+
print `{data, next_cursor}`; `rotate` prints `{secret, version}`; `delete` prints
|
|
146
|
+
`{deleted, id, name, scope}`; `workspaces secrets` prints `{workspace_id, names, secrets}`.
|
|
85
147
|
|
|
86
148
|
Errors go to stderr as `{"error": {code, message, status, request_id, ...}}`.
|
|
87
149
|
|
|
@@ -94,8 +156,8 @@ otherwise, or if no workspace has that id, it is matched as an exact workspace k
|
|
|
94
156
|
| 2 | usage error: bad arguments, or input the API rejected as `validation_failed` |
|
|
95
157
|
| 3 | not found: workspace, operation or route |
|
|
96
158
|
| 4 | authentication or authorization: missing, malformed, revoked or insufficient key |
|
|
97
|
-
| 5 | timeout: waiting gave up; the operation continues server side (`shard operations wait <id>`) |
|
|
98
|
-
| 6 | the awaited operation ended `failed` or `canceled` |
|
|
159
|
+
| 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`) |
|
|
160
|
+
| 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace) |
|
|
99
161
|
| 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. |
|
|
100
162
|
| exec | the command's own code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |
|
|
101
163
|
|
package/dist/args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.2.0";
|
|
2
2
|
export interface OptionSpec {
|
|
3
3
|
type: 'string' | 'boolean';
|
|
4
4
|
short?: string;
|
|
@@ -32,8 +32,15 @@ export declare function parsePositiveInt(text: string, option: string, max?: num
|
|
|
32
32
|
/** ["A=1", "B=x=y"] -> { A: "1", B: "x=y" }. */
|
|
33
33
|
export declare function parseEnvPairs(pairs: readonly string[]): Record<string, string>;
|
|
34
34
|
export declare const UUID: RegExp;
|
|
35
|
+
/** Secret names (environment-variable safe; the API's SECRET_NAME_PATTERN). */
|
|
36
|
+
export declare const SECRET_NAME: RegExp;
|
|
35
37
|
/** Validates the API base URL: https, or http only to a loopback address (the key is a bearer credential). */
|
|
36
38
|
export declare function resolveApiUrl(flag: string | undefined, env: string | undefined): string;
|
|
39
|
+
export declare const DEFAULT_WAKE_TIMEOUT_MS = 120000;
|
|
40
|
+
/** Wake on use: --wake / --no-wake, else SHARDFLUX_NO_WAKE (1/true/yes/on disables), else on. */
|
|
41
|
+
export declare function resolveWake(flag: boolean | undefined, noWakeEnv: string | undefined): boolean;
|
|
42
|
+
/** The wake/transition budget per call in ms: --wake-timeout, else SHARDFLUX_WAKE_TIMEOUT_MS, else 120000. */
|
|
43
|
+
export declare function resolveWakeTimeout(flag: string | undefined, env: string | undefined): number;
|
|
37
44
|
export type Values = Record<string, string | boolean | string[] | undefined>;
|
|
38
45
|
export type Parsed = {
|
|
39
46
|
kind: 'help';
|
|
@@ -49,6 +56,8 @@ export type Parsed = {
|
|
|
49
56
|
apiUrl: string | undefined;
|
|
50
57
|
json: boolean;
|
|
51
58
|
agentLabel: string | undefined;
|
|
59
|
+
wake: boolean | undefined;
|
|
60
|
+
wakeTimeout: string | undefined;
|
|
52
61
|
};
|
|
53
62
|
};
|
|
54
63
|
/**
|
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.2.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
|
};
|
|
@@ -28,6 +30,39 @@ const WAIT = {
|
|
|
28
30
|
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (e.g. 90s, 5m; default 5m). The operation continues server side.' },
|
|
29
31
|
};
|
|
30
32
|
const REF = { name: 'id|key', required: true };
|
|
33
|
+
const SLUG = { name: 'slug', required: true };
|
|
34
|
+
const PAGE = {
|
|
35
|
+
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
36
|
+
limit: { type: 'string', value: '<n>', description: 'Page size.' },
|
|
37
|
+
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
38
|
+
};
|
|
39
|
+
/** Options of a version produced by save-as-template or a draft publish (contracts §19.8). */
|
|
40
|
+
const SAVE = {
|
|
41
|
+
description: { type: 'string', value: '<text>', description: 'Version description (default: the source version’s).' },
|
|
42
|
+
'default-lifetime': { type: 'string', value: '<persistent|session>', description: 'Default lifetime of workspaces opened from the new version.' },
|
|
43
|
+
'default-idle-timeout': { type: 'string', value: '<duration>', description: 'Idle timeout of its session workspaces (60s-24h; default: the platform’s 10m).' },
|
|
44
|
+
acknowledge: { type: 'string', multiple: true, value: '<path>', description: 'Absolute path the credential scan may report without failing the build (repeatable, up to 200).' },
|
|
45
|
+
'auto-publish': { type: 'boolean', description: 'Publish the version once registered (default; --no-auto-publish leaves it for an owner/admin).' },
|
|
46
|
+
wait: { type: 'boolean', description: 'Wait until the build is published and registered (or fails).' },
|
|
47
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 30m). The build continues server side.' },
|
|
48
|
+
};
|
|
49
|
+
const LIFETIME_FILTER = {
|
|
50
|
+
lifetime: { type: 'string', value: '<persistent|session|any>', description: 'Lifetime filter (default persistent: sessions are hidden).' },
|
|
51
|
+
purpose: { type: 'string', value: '<standard|template_draft|template_test|any>', description: 'Purpose filter (default standard: template drafts and test instances are hidden).' },
|
|
52
|
+
};
|
|
53
|
+
const SECRET_REF = { name: 'id|name', required: true };
|
|
54
|
+
const SECRET_SCOPE = {
|
|
55
|
+
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.' },
|
|
56
|
+
};
|
|
57
|
+
const SECRET_VALUE = {
|
|
58
|
+
'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.' },
|
|
59
|
+
};
|
|
60
|
+
const SECRET_PERMISSIONS = {
|
|
61
|
+
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.' },
|
|
62
|
+
'allow-workspace': { type: 'string', multiple: true, value: '<workspace-id>', description: 'Only these workspaces may use it (repeatable). Default: any workspace of the allowed projects.' },
|
|
63
|
+
'allow-project': { type: 'string', multiple: true, value: '<project-id>', description: 'Organization secrets: projects whose workspaces may use it (repeatable). Default: none until granted.' },
|
|
64
|
+
'all-projects': { type: 'boolean', description: 'Organization secrets: every project of the organization may use it.' },
|
|
65
|
+
};
|
|
31
66
|
export const COMMANDS = [
|
|
32
67
|
{
|
|
33
68
|
path: ['login'],
|
|
@@ -51,11 +86,27 @@ export const COMMANDS = [
|
|
|
51
86
|
positionals: [{ name: 'key', required: true }],
|
|
52
87
|
options: {
|
|
53
88
|
template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
|
|
89
|
+
secret: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
multiple: true,
|
|
92
|
+
value: '<NAME>',
|
|
93
|
+
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.',
|
|
94
|
+
},
|
|
95
|
+
lifetime: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
value: '<persistent|session>',
|
|
98
|
+
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.',
|
|
99
|
+
},
|
|
54
100
|
...CAPS,
|
|
55
101
|
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
56
102
|
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m). The start continues server side.' },
|
|
57
103
|
},
|
|
58
|
-
examples: [
|
|
104
|
+
examples: [
|
|
105
|
+
'shard workspaces open acme/demo --template python-node-browser',
|
|
106
|
+
'shard ws open job-42 --template python-node-browser --lifetime session',
|
|
107
|
+
'shard ws open acme/demo --template python-node-browser --secret OPENAI_API_KEY --secret DATABASE_URL',
|
|
108
|
+
'shard ws open acme/demo --template python-node-browser --no-wait --json',
|
|
109
|
+
],
|
|
59
110
|
},
|
|
60
111
|
{
|
|
61
112
|
path: ['workspaces', 'list'],
|
|
@@ -64,7 +115,8 @@ export const COMMANDS = [
|
|
|
64
115
|
options: {
|
|
65
116
|
state: { type: 'string', value: '<state>', description: 'Observed state filter (running, suspended, creating, ...).' },
|
|
66
117
|
prefix: { type: 'string', value: '<key-prefix>', description: 'Only keys starting with this prefix.' },
|
|
67
|
-
|
|
118
|
+
...LIFETIME_FILTER,
|
|
119
|
+
'include-deleted': { type: 'boolean', description: 'Include deleted workspaces and ended sessions (tombstones).' },
|
|
68
120
|
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
69
121
|
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
70
122
|
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
@@ -98,7 +150,58 @@ export const COMMANDS = [
|
|
|
98
150
|
positionals: [REF],
|
|
99
151
|
options: { yes: { type: 'boolean', description: 'Required: confirm the deletion.' }, ...WAIT },
|
|
100
152
|
},
|
|
153
|
+
{
|
|
154
|
+
path: ['workspaces', 'close'],
|
|
155
|
+
summary: 'End a session workspace now (it is deleted; the key then opens a new workspace)',
|
|
156
|
+
description: 'Sessions only: a persistent workspace is refused with conflict (not_session); suspend or delete it instead. Repeating returns the same delete operation.',
|
|
157
|
+
positionals: [REF],
|
|
158
|
+
options: { ...WAIT },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
path: ['workspaces', 'reset'],
|
|
162
|
+
summary: 'Wipe every change in a layered workspace and restart it on its template',
|
|
163
|
+
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).',
|
|
164
|
+
positionals: [REF],
|
|
165
|
+
options: { yes: { type: 'boolean', description: 'Required: confirm that every change is wiped.' }, ...WAIT },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
path: ['workspaces', 'save-as-template'],
|
|
169
|
+
summary: 'Save a layered workspace as the next version of an organization template',
|
|
170
|
+
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.',
|
|
171
|
+
positionals: [REF],
|
|
172
|
+
options: {
|
|
173
|
+
template: { type: 'string', value: '<slug>', description: 'Organization template to save into (required; created when absent).' },
|
|
174
|
+
'display-name': { type: 'string', value: '<name>', description: 'Template name when this save creates the template.' },
|
|
175
|
+
checkpoint: { type: 'string', value: '<checkpoint-id>', description: 'Save this committed checkpoint of the workspace instead of its current state.' },
|
|
176
|
+
...SAVE,
|
|
177
|
+
},
|
|
178
|
+
examples: ['shard ws save-as-template acme/demo --template acme-dev --description "node 22 + repo deps" --wait'],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
path: ['workspaces', 'changes'],
|
|
182
|
+
summary: 'List what a layered workspace changed against its template',
|
|
183
|
+
description: 'Served by the cell gateway from the running workspace (needs the files tool). added, modified, metadata (with --hash), deleted, replaced (opaque directory).',
|
|
184
|
+
positionals: [REF],
|
|
185
|
+
options: {
|
|
186
|
+
path: { type: 'string', value: '<prefix>', description: 'Only entries at or below this absolute path (default /).' },
|
|
187
|
+
hash: { type: 'boolean', description: 'Hash regular files up to 16 MiB (reports metadata-only changes).' },
|
|
188
|
+
summary: { type: 'boolean', description: 'Also print totals over everything under --path.' },
|
|
189
|
+
...PAGE,
|
|
190
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 1000).' },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
101
193
|
{ path: ['workspaces', 'sessions'], summary: 'List the attributed agent sessions (tool-token principals and labels) of a workspace', positionals: [REF], options: {} },
|
|
194
|
+
{
|
|
195
|
+
path: ['workspaces', 'secrets'],
|
|
196
|
+
summary: 'Show (or replace) the secret names bound to a workspace',
|
|
197
|
+
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.',
|
|
198
|
+
positionals: [REF],
|
|
199
|
+
options: {
|
|
200
|
+
set: { type: 'string', multiple: true, value: '<NAME>', description: 'Replace the binding with these names (repeatable).' },
|
|
201
|
+
clear: { type: 'boolean', description: 'Remove every bound name.' },
|
|
202
|
+
},
|
|
203
|
+
examples: ['shard ws secrets acme/demo', 'shard ws secrets acme/demo --set OPENAI_API_KEY --set DATABASE_URL', 'shard ws secrets acme/demo --clear'],
|
|
204
|
+
},
|
|
102
205
|
{
|
|
103
206
|
path: ['files', 'read'],
|
|
104
207
|
summary: 'Print (or save) a file from a workspace',
|
|
@@ -132,6 +235,160 @@ export const COMMANDS = [
|
|
|
132
235
|
},
|
|
133
236
|
},
|
|
134
237
|
{ path: ['operations', 'get'], summary: 'Show one operation', positionals: [{ name: 'operation-id', required: true }], options: {} },
|
|
238
|
+
{
|
|
239
|
+
path: ['secrets', 'create'],
|
|
240
|
+
summary: 'Create a secret; the value is read from standard input or --from-file, never from arguments',
|
|
241
|
+
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".',
|
|
242
|
+
positionals: [{ name: 'name', required: true }],
|
|
243
|
+
options: { ...SECRET_SCOPE, ...SECRET_VALUE, description: { type: 'string', value: '<text>', description: 'Description (up to 500 characters).' }, ...SECRET_PERMISSIONS },
|
|
244
|
+
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"'],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
path: ['secrets', 'list'],
|
|
248
|
+
summary: 'List secrets (metadata only; never values)',
|
|
249
|
+
positionals: [],
|
|
250
|
+
options: {
|
|
251
|
+
...SECRET_SCOPE,
|
|
252
|
+
'include-deleted': { type: 'boolean', description: 'Include deleted secrets.' },
|
|
253
|
+
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
254
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
255
|
+
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{ path: ['secrets', 'get'], summary: 'Show one secret’s metadata (never the value)', positionals: [SECRET_REF], options: { ...SECRET_SCOPE } },
|
|
259
|
+
{
|
|
260
|
+
path: ['secrets', 'update'],
|
|
261
|
+
summary: 'Change a secret’s description or usage permissions (applies from the next session start)',
|
|
262
|
+
positionals: [SECRET_REF],
|
|
263
|
+
options: {
|
|
264
|
+
...SECRET_SCOPE,
|
|
265
|
+
description: { type: 'string', value: '<text>', description: 'New description.' },
|
|
266
|
+
...SECRET_PERMISSIONS,
|
|
267
|
+
'any-workspace': { type: 'boolean', description: 'Remove the workspace restriction (any workspace of the allowed projects).' },
|
|
268
|
+
'clear-projects': { type: 'boolean', description: 'Organization secrets: no project may use it (until granted again).' },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
path: ['secrets', 'rotate'],
|
|
273
|
+
summary: 'Store a new value as the next version (standard input or --from-file); older values are erased',
|
|
274
|
+
positionals: [SECRET_REF],
|
|
275
|
+
options: { ...SECRET_SCOPE, ...SECRET_VALUE },
|
|
276
|
+
examples: ['printf %s "$NEW_TOKEN" | shard secrets rotate API_TOKEN'],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
path: ['secrets', 'versions'],
|
|
280
|
+
summary: 'List a secret’s versions, newest first (metadata only)',
|
|
281
|
+
positionals: [SECRET_REF],
|
|
282
|
+
options: { ...SECRET_SCOPE, limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' } },
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
path: ['secrets', 'delete'],
|
|
286
|
+
summary: 'Delete a secret: its values are erased and it is removed from every workspace binding',
|
|
287
|
+
positionals: [SECRET_REF],
|
|
288
|
+
options: { ...SECRET_SCOPE, yes: { type: 'boolean', description: 'Required: confirm the deletion.' } },
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
path: ['secrets', 'access-log'],
|
|
292
|
+
summary: 'Show which sessions resolved a secret (owners/admins; project API keys get 403)',
|
|
293
|
+
positionals: [SECRET_REF],
|
|
294
|
+
options: {
|
|
295
|
+
...SECRET_SCOPE,
|
|
296
|
+
outcome: { type: 'string', value: '<outcome>', description: 'granted, denied or failed.' },
|
|
297
|
+
workspace: { type: 'string', value: '<workspace-id>', description: 'Only this workspace.' },
|
|
298
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: ['templates', 'list'],
|
|
303
|
+
summary: 'List the templates this project can open (platform and organization)',
|
|
304
|
+
positionals: [],
|
|
305
|
+
options: {
|
|
306
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Only platform or only organization templates.' },
|
|
307
|
+
'include-archived': { type: 'boolean', description: 'Include archived templates.' },
|
|
308
|
+
...PAGE,
|
|
309
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-200 (default 50).' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
path: ['templates', 'files'],
|
|
314
|
+
summary: 'List one directory of a template version’s file tree (or show one entry with --stat)',
|
|
315
|
+
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.',
|
|
316
|
+
positionals: [SLUG, { name: 'version', required: true }, { name: 'path' }],
|
|
317
|
+
options: {
|
|
318
|
+
stat: { type: 'boolean', description: 'Show the entry at <path> itself instead of listing it.' },
|
|
319
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Pick the platform template even when an organization template shadows its slug.' },
|
|
320
|
+
...PAGE,
|
|
321
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 200).' },
|
|
322
|
+
},
|
|
323
|
+
examples: ['shard templates files python-node-browser 5 /usr/local/bin', 'shard templates files acme-dev 3 /etc/hostname --stat'],
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
path: ['templates', 'diff'],
|
|
327
|
+
summary: 'Diff two versions of a template (added, removed, changed, type_changed, metadata)',
|
|
328
|
+
positionals: [SLUG],
|
|
329
|
+
options: {
|
|
330
|
+
from: { type: 'string', value: '<version|base>', description: 'Older version, or "base" (the --to version’s build base). Required.' },
|
|
331
|
+
to: { type: 'string', value: '<version>', description: 'Newer version (required).' },
|
|
332
|
+
prefix: { type: 'string', value: '<path>', description: 'Only paths starting with this absolute prefix.' },
|
|
333
|
+
change: { type: 'string', value: '<kind>', description: 'Only added, removed, changed, type_changed or metadata.' },
|
|
334
|
+
owner: { type: 'string', value: '<platform|organization>', description: 'Pick the platform template even when an organization template shadows its slug.' },
|
|
335
|
+
...PAGE,
|
|
336
|
+
limit: { type: 'string', value: '<n>', description: 'Page size, 1-1000 (default 200).' },
|
|
337
|
+
},
|
|
338
|
+
examples: ['shard templates diff acme-dev --from 2 --to 3', 'shard templates diff acme-dev --from base --to 3 --prefix /home/user'],
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
path: ['templates', 'draft', 'open'],
|
|
342
|
+
summary: 'Open the draft of an organization template (a layered workspace to edit live)',
|
|
343
|
+
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.',
|
|
344
|
+
positionals: [SLUG],
|
|
345
|
+
options: {
|
|
346
|
+
base: { type: 'string', value: '<slug@version>', description: 'Version to start from (default: the template’s latest published version; required for a new template).' },
|
|
347
|
+
...CAPS,
|
|
348
|
+
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
349
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m).' },
|
|
350
|
+
},
|
|
351
|
+
examples: ['shard templates draft open acme-dev --base python-node-browser@5'],
|
|
352
|
+
},
|
|
353
|
+
{ path: ['templates', 'draft', 'status'], summary: 'Show a template’s draft: workspace, base version, states and live test instances', positionals: [SLUG], options: {} },
|
|
354
|
+
{
|
|
355
|
+
path: ['templates', 'draft', 'state'],
|
|
356
|
+
summary: 'Capture a state of the running draft (or list its states with --list)',
|
|
357
|
+
positionals: [SLUG],
|
|
358
|
+
options: {
|
|
359
|
+
label: { type: 'string', value: '<text>', description: 'Label of the captured state.' },
|
|
360
|
+
list: { type: 'boolean', description: 'List the draft’s states instead of capturing one.' },
|
|
361
|
+
...WAIT,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
path: ['templates', 'draft', 'test'],
|
|
366
|
+
summary: 'Open a test instance of a draft state (a disposable session workspace), or list them with --list',
|
|
367
|
+
description: 'Its writes never reach the draft. It ends with "shard ws close <key>", when idle, or when the draft is discarded.',
|
|
368
|
+
positionals: [SLUG],
|
|
369
|
+
options: {
|
|
370
|
+
state: { type: 'string', value: '<state-id>', description: 'Draft state to test (default: a fresh capture of the running draft).' },
|
|
371
|
+
'instance-key': { type: 'string', value: '<key>', description: 'Workspace key of the test instance (default sf:test:<slug>:<random>).' },
|
|
372
|
+
list: { type: 'boolean', description: 'List the draft’s test instances instead of opening one.' },
|
|
373
|
+
'include-ended': { type: 'boolean', description: 'With --list: include ended test instances.' },
|
|
374
|
+
...CAPS,
|
|
375
|
+
wait: { type: 'boolean', description: 'Wait until ready (default; --no-wait returns at once).' },
|
|
376
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m).' },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
path: ['templates', 'draft', 'publish'],
|
|
381
|
+
summary: 'Publish the draft as the template’s next version',
|
|
382
|
+
description: 'Refused (conflict draft_stale) when the template got a version from elsewhere since the draft was opened. The draft stays open afterwards.',
|
|
383
|
+
positionals: [SLUG],
|
|
384
|
+
options: { state: { type: 'string', value: '<state-id>', description: 'State to publish (default: the draft’s current state).' }, ...SAVE },
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
path: ['templates', 'draft', 'discard'],
|
|
388
|
+
summary: 'Discard the draft (it is deleted and its live test instances end)',
|
|
389
|
+
positionals: [SLUG],
|
|
390
|
+
options: { yes: { type: 'boolean', description: 'Required: confirm the discard.' }, ...WAIT },
|
|
391
|
+
},
|
|
135
392
|
{
|
|
136
393
|
path: ['operations', 'wait'],
|
|
137
394
|
summary: 'Wait for an operation to finish (exit 0 succeeded, 6 failed/canceled, 5 timeout)',
|
|
@@ -143,8 +400,10 @@ const GROUPS = {
|
|
|
143
400
|
workspaces: 'Open, inspect and manage workspaces',
|
|
144
401
|
files: 'Read, write and list files in a workspace',
|
|
145
402
|
operations: 'Inspect and wait for lifecycle operations',
|
|
403
|
+
secrets: 'Manage secrets (values are write-only) and bind them to workspaces',
|
|
404
|
+
templates: 'Browse templates (file tree, diff) and develop organization templates in a draft',
|
|
146
405
|
};
|
|
147
|
-
const ALIASES = { ws: 'workspaces', workspace: 'workspaces', ops: 'operations', operation: 'operations', file: 'files' };
|
|
406
|
+
const ALIASES = { ws: 'workspaces', workspace: 'workspaces', ops: 'operations', operation: 'operations', file: 'files', secret: 'secrets', template: 'templates' };
|
|
148
407
|
/** Canonical command words: aliases resolved, `workspaces files|operations` folded to the top-level group. */
|
|
149
408
|
export function normalizeWords(words) {
|
|
150
409
|
const w = words.map((x) => ALIASES[x] ?? x);
|
|
@@ -202,6 +461,8 @@ export function parseEnvPairs(pairs) {
|
|
|
202
461
|
return out;
|
|
203
462
|
}
|
|
204
463
|
export const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
464
|
+
/** Secret names (environment-variable safe; the API's SECRET_NAME_PATTERN). */
|
|
465
|
+
export const SECRET_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
205
466
|
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
206
467
|
/** Validates the API base URL: https, or http only to a loopback address (the key is a bearer credential). */
|
|
207
468
|
export function resolveApiUrl(flag, env) {
|
|
@@ -220,8 +481,37 @@ export function resolveApiUrl(flag, env) {
|
|
|
220
481
|
throw new UsageError('API URL must not contain credentials, a query or a fragment');
|
|
221
482
|
return u.toString().replace(/\/+$/, '');
|
|
222
483
|
}
|
|
223
|
-
const
|
|
224
|
-
const
|
|
484
|
+
export const DEFAULT_WAKE_TIMEOUT_MS = 120_000;
|
|
485
|
+
const MAX_WAKE_TIMEOUT_MS = 7 * 24 * 3600 * 1000;
|
|
486
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
487
|
+
const FALSY = new Set(['', '0', 'false', 'no', 'off']);
|
|
488
|
+
/** Wake on use: --wake / --no-wake, else SHARDFLUX_NO_WAKE (1/true/yes/on disables), else on. */
|
|
489
|
+
export function resolveWake(flag, noWakeEnv) {
|
|
490
|
+
if (flag !== undefined)
|
|
491
|
+
return flag;
|
|
492
|
+
const v = (noWakeEnv ?? '').trim().toLowerCase();
|
|
493
|
+
if (TRUTHY.has(v))
|
|
494
|
+
return false;
|
|
495
|
+
if (FALSY.has(v))
|
|
496
|
+
return true;
|
|
497
|
+
throw new UsageError(`SHARDFLUX_NO_WAKE must be 1 (or true) to turn wake on use off, or 0/empty (got "${noWakeEnv}")`);
|
|
498
|
+
}
|
|
499
|
+
/** The wake/transition budget per call in ms: --wake-timeout, else SHARDFLUX_WAKE_TIMEOUT_MS, else 120000. */
|
|
500
|
+
export function resolveWakeTimeout(flag, env) {
|
|
501
|
+
if (flag !== undefined)
|
|
502
|
+
return parsePositiveInt(flag, 'wake-timeout', MAX_WAKE_TIMEOUT_MS);
|
|
503
|
+
const v = env?.trim();
|
|
504
|
+
if (!v)
|
|
505
|
+
return DEFAULT_WAKE_TIMEOUT_MS;
|
|
506
|
+
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > MAX_WAKE_TIMEOUT_MS) {
|
|
507
|
+
throw new UsageError(`SHARDFLUX_WAKE_TIMEOUT_MS must be an integer number of milliseconds between 1 and ${MAX_WAKE_TIMEOUT_MS} (got "${env}")`);
|
|
508
|
+
}
|
|
509
|
+
return Number(v);
|
|
510
|
+
}
|
|
511
|
+
const FORBIDDEN_KEY_FLAG = /^--?(api-?key|apikey|key|token|secret|value|secret-value|password)(=.*)?$/i;
|
|
512
|
+
/** `--secret NAME` binds a secret by name on `workspaces open`; anything else shaped like --secret is refused. */
|
|
513
|
+
const SECRET_NAME_FLAG = /^--secret(?:=(.*))?$/;
|
|
514
|
+
const GLOBAL_VALUE_FLAGS = new Set(['--api-url', '--agent-label', '--wake-timeout']);
|
|
225
515
|
function toParseArgsOptions(opts) {
|
|
226
516
|
const out = {};
|
|
227
517
|
for (const [name, o] of Object.entries(opts))
|
|
@@ -235,10 +525,30 @@ function toParseArgsOptions(opts) {
|
|
|
235
525
|
export function parseCommandLine(argv) {
|
|
236
526
|
const terminator = argv.indexOf('--');
|
|
237
527
|
const head = terminator < 0 ? argv : argv.slice(0, terminator);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
528
|
+
const leading = [];
|
|
529
|
+
for (let i = 0; i < head.length && leading.length < 2; i += 1) {
|
|
530
|
+
const a = head[i];
|
|
531
|
+
if (!a.startsWith('-'))
|
|
532
|
+
leading.push(a);
|
|
533
|
+
else if (GLOBAL_VALUE_FLAGS.has(a))
|
|
534
|
+
i += 1;
|
|
535
|
+
}
|
|
536
|
+
const opensWorkspace = key(normalizeWords(leading)) === 'workspaces open';
|
|
537
|
+
for (let i = 0; i < head.length; i += 1) {
|
|
538
|
+
const a = head[i];
|
|
539
|
+
if (!FORBIDDEN_KEY_FLAG.test(a))
|
|
540
|
+
continue;
|
|
541
|
+
const named = SECRET_NAME_FLAG.exec(a);
|
|
542
|
+
if (named && opensWorkspace) {
|
|
543
|
+
const name = named[1] ?? head[i + 1];
|
|
544
|
+
if (name !== undefined && SECRET_NAME.test(name))
|
|
545
|
+
continue;
|
|
546
|
+
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)');
|
|
547
|
+
}
|
|
548
|
+
if (/^--?(value|secret-value|password)(=.*)?$/i.test(a)) {
|
|
549
|
+
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
550
|
}
|
|
551
|
+
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
552
|
}
|
|
243
553
|
// Pick the command words (the leading non-option words that form a known command path).
|
|
244
554
|
const words = [];
|
|
@@ -323,6 +633,8 @@ export function parseCommandLine(argv) {
|
|
|
323
633
|
apiUrl: typeof values['api-url'] === 'string' ? values['api-url'] : undefined,
|
|
324
634
|
json: values.json === true,
|
|
325
635
|
agentLabel: typeof values['agent-label'] === 'string' ? values['agent-label'] : undefined,
|
|
636
|
+
wake: typeof values.wake === 'boolean' ? values.wake : undefined,
|
|
637
|
+
wakeTimeout: typeof values['wake-timeout'] === 'string' ? values['wake-timeout'] : undefined,
|
|
326
638
|
},
|
|
327
639
|
};
|
|
328
640
|
}
|
|
@@ -380,9 +692,11 @@ export function topHelp() {
|
|
|
380
692
|
...optionLines(GLOBAL_OPTIONS),
|
|
381
693
|
'',
|
|
382
694
|
'Environment:',
|
|
383
|
-
' SHARDFLUX_API_KEY
|
|
384
|
-
' SHARDFLUX_API_URL
|
|
385
|
-
' SHARDFLUX_AGENT_LABEL
|
|
695
|
+
' SHARDFLUX_API_KEY project API key (sfk_...), required; never pass it as an argument',
|
|
696
|
+
' SHARDFLUX_API_URL API base URL (default https://api.shardflux.dev)',
|
|
697
|
+
' SHARDFLUX_AGENT_LABEL attribution label for tool tokens (default cli)',
|
|
698
|
+
' SHARDFLUX_NO_WAKE 1: do not resume a suspended workspace on use (same as --no-wake)',
|
|
699
|
+
' SHARDFLUX_WAKE_TIMEOUT_MS longest wait in ms for a workspace to wake or finish a transition (default 120000; --wake-timeout overrides)',
|
|
386
700
|
'',
|
|
387
701
|
'Exit codes:',
|
|
388
702
|
...EXIT_CODE_HELP,
|
package/dist/commands.d.ts
CHANGED
|
@@ -16,11 +16,19 @@ export interface Ctx {
|
|
|
16
16
|
json: boolean;
|
|
17
17
|
apiUrl: string;
|
|
18
18
|
agentLabel: string;
|
|
19
|
+
/** Wake a suspended workspace on use (--no-wake / SHARDFLUX_NO_WAKE=1 turn it off). */
|
|
20
|
+
wake: boolean;
|
|
21
|
+
/** Bound on lifecycle waits per cell call, wakes included (--wake-timeout / SHARDFLUX_WAKE_TIMEOUT_MS). */
|
|
22
|
+
wakeTimeoutMs: number;
|
|
19
23
|
signal: AbortSignal;
|
|
20
24
|
cloud(): Shardflux;
|
|
21
25
|
}
|
|
22
26
|
export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promise<number>;
|
|
23
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
|
|
29
|
+
* lifetime and purpose (sessions, drafts and test instances are hidden from the default list), preferring the live
|
|
30
|
+
* workspace over tombstones of ended sessions that had the same key (contracts §19.11).
|
|
31
|
+
*/
|
|
24
32
|
export declare function resolveWorkspace(ctx: Ctx, ref: string): Promise<Workspace>;
|
|
25
33
|
/** The exit code `shard exec` returns for a finished command. */
|
|
26
34
|
export declare function execExitCode(r: Pick<RunResult, 'exitCode' | 'termSignal' | 'timedOut'>): number;
|