@shardflux/cli 0.5.0 → 0.5.1
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 +52 -1
- package/README.md +81 -6
- package/dist/account-args.js +12 -0
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +42 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +16 -5
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +53 -4
- package/dist/errors.js +8 -1
- package/dist/format.d.ts +20 -2
- package/dist/format.js +114 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/main.js +81 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,56 @@
|
|
|
3
3
|
Every command and option the README shows is available from the version named here. Below 1.0, a minor release may
|
|
4
4
|
break compatibility; breaking changes are marked **Breaking**.
|
|
5
5
|
|
|
6
|
+
## 0.5.1 (not yet published; npm `latest` is 0.5.0)
|
|
7
|
+
|
|
8
|
+
### A command that could not start is never a silent exit
|
|
9
|
+
|
|
10
|
+
Production (2026-09-29): `shard ws exec <key> --cwd app -- ls` exited 1 and printed nothing on stdout or stderr. The
|
|
11
|
+
cell had ended the session `failed_to_start` (`working directory "app" is not a directory`).
|
|
12
|
+
|
|
13
|
+
- `shard ws exec` on a processful workspace: a command that could not start (a `--cwd` that is not a directory, a
|
|
14
|
+
program not on `PATH`, an unknown user) exits 6, as a failed file-first execution does, with the workspace's reason
|
|
15
|
+
and a hint on stderr: `error: The command could not start: working directory "/home/user/app" is not a directory
|
|
16
|
+
(exec_failed_to_start) [code=conflict source=cell]`, then `hint: --cwd must name a directory that exists in the
|
|
17
|
+
workspace, ...` (or the `PATH` hint). It exited 1 with no output. With `--json` the error document has
|
|
18
|
+
`details.reason` `exec_failed_to_start`, `details.error` and `details.session_id`. Through `@shardflux/sdk` 0.10.0
|
|
19
|
+
(`ExecStartError`).
|
|
20
|
+
- A relative `--cwd` is refused by the API before anything runs: 422 `invalid_cwd`, exit 2, and the message names the
|
|
21
|
+
absolute path it likely means (`use "/home/user/app"`). That holds for every CLI version once the cell refuses it.
|
|
22
|
+
- A session that ended without an exit code, timeout or signal (`lost`, when its host restarted) prints `exec session
|
|
23
|
+
<id> ended lost without an exit code: <reason>` on stderr; the exit code stays 1. `--json` adds `state` (and `error`
|
|
24
|
+
when the session has one) to the result.
|
|
25
|
+
- The file-first hint for `exec_failed_to_start` names `--cwd` when the reason is a working directory, instead of
|
|
26
|
+
always asking whether argv[0] is on `PATH`. `--cwd` help: an absolute path, default `/home/user`.
|
|
27
|
+
|
|
28
|
+
- A 402 `allowance_exhausted` and a spend-policy `version_mismatch` end without the `feedback:` line: they are billing
|
|
29
|
+
states whose hint names the next step, not problems to report.
|
|
30
|
+
### Opt-in overage with a spend cap
|
|
31
|
+
|
|
32
|
+
- `shard usage` prints an overage block from the summary's `spend_cap`: the state (`off`, `paused`, `accruing`,
|
|
33
|
+
`warning`, `reached`, ...), the spend cap, the charges of the cap with what is left, the date the cap is projected
|
|
34
|
+
to be reached, and a table of the units past each allowance (CPU-hours, RAM GiB-hours) with the billed units, rate
|
|
35
|
+
and amount. Allowances past `included` while overage is on show cap state `overage`; `allowance exhausted` names the
|
|
36
|
+
reason (`allowance_used`, `overage_paused`, `spend_cap_reached`). An API without overage prints no block.
|
|
37
|
+
- `shard billing overage` (signed in; owners and billing members change it): `status` (the default: the state, the
|
|
38
|
+
spend cap and its range up to the plan price, the rates), `on [--cap <dollars>]`, `cap <dollars>` (e.g. `9` or
|
|
39
|
+
`9.50`) and `off`, with `--if-version <n>` and `--json` (the API's spend policy). A refusal exits 2 with a hint that
|
|
40
|
+
names the bound (`spend_cap_above_plan_price`: the plan price; `spend_cap_below_minimum`; `spend_cap_below_charges`:
|
|
41
|
+
what overage already charged) or the next step (`overage_unavailable`, `spend_cap_required`); a 409
|
|
42
|
+
`version_mismatch` (also from `egress set --if-version`) exits 1 and names the current version.
|
|
43
|
+
- 402 `allowance_exhausted` gets a hint per `details.reason`: `allowance_used` (upgrade, or `shard billing overage on
|
|
44
|
+
--cap <dollars>`), `spend_cap_reached` (raise the cap with `shard billing overage cap <dollars>`, or upgrade),
|
|
45
|
+
`overage_paused` (a payment is past due: update the payment method, `shard billing portal --open`). The exit code
|
|
46
|
+
stays 1.
|
|
47
|
+
|
|
48
|
+
### Suspend when idle (contracts §20.6)
|
|
49
|
+
|
|
50
|
+
- `shard workspaces suspend <id|key> --when-idle <duration>` (30s to 1h): the workspace is suspended once it has been
|
|
51
|
+
idle that long, counted from the later of its last work and the request (suspend when idle, for the end of an agent
|
|
52
|
+
turn). A running command, an attached stream or a keepalive postpones it; the next tool call or a resume cancels it.
|
|
53
|
+
`--cancel-when-idle` cancels it. `--json` prints `{workspace, operation, suspend_request}`.
|
|
54
|
+
- `shard workspaces get` shows a pending request (`suspend when idle after N s idle, not before ...`).
|
|
55
|
+
|
|
6
56
|
## 0.5.0 (not yet published; npm `latest` is 0.4.1)
|
|
7
57
|
|
|
8
58
|
Needs `@shardflux/sdk` 0.9.0 or later.
|
|
@@ -75,7 +125,7 @@ Needs `@shardflux/sdk` 0.9.0 or later.
|
|
|
75
125
|
|
|
76
126
|
### Waking a suspended workspace is one request (contracts §22.6)
|
|
77
127
|
|
|
78
|
-
- Through `@shardflux/sdk` 0.
|
|
128
|
+
- Through `@shardflux/sdk` 0.9.0: a command on a suspended workspace (`ws exec`, `files ...`, `ws changes`) wakes it
|
|
79
129
|
with one held resume that returns the running workspace and the CLI's tool token, then runs (refused call, resume,
|
|
80
130
|
the command; before: also a wait, a view and a token request). An API without the held resume works as before.
|
|
81
131
|
- `--timing` shows the wake as one `request` phase `(held)`; a wake that runs out of time shows the hold, then the
|
|
@@ -178,6 +228,7 @@ unchanged and still stores nothing.
|
|
|
178
228
|
edit that does not match, a mode the workspace lacks, a reused execution id), and `shard feedback` itself.
|
|
179
229
|
- The positional usage of `shard feedback` is shown as `<message...>`; `--agent-label` also names the reporting agent.
|
|
180
230
|
|
|
231
|
+
|
|
181
232
|
## 0.4.1 (2026-09-28)
|
|
182
233
|
|
|
183
234
|
Needs `@shardflux/sdk` 0.8.0 or later. No command changes: the SDK's 0.8.0 is a types-only release (its provider tool
|
package/README.md
CHANGED
|
@@ -222,6 +222,10 @@ shard billing upgrade <plan> [--open] [--wait] [--timeout 15m]
|
|
|
222
222
|
shard billing portal [--open] # plan changes, payment methods, invoices, cancellation
|
|
223
223
|
shard billing invoices [--all]
|
|
224
224
|
shard billing alerts [--set 50,80,100 | --clear] # usage alert emails, percent of each allowance
|
|
225
|
+
shard billing overage [status] # opt-in overage: state, spend cap and its range, rates
|
|
226
|
+
shard billing overage on [--cap <dollars>] # turn it on (a cap is needed the first time)
|
|
227
|
+
shard billing overage cap <dollars> # change the spend cap, e.g. 9 or 9.50
|
|
228
|
+
shard billing overage off # turn it off (always allowed)
|
|
225
229
|
```
|
|
226
230
|
|
|
227
231
|
`billing upgrade` checks the plan against the public catalog (an unknown one lists the purchasable
|
|
@@ -232,6 +236,24 @@ plan only when Stripe confirms the payment, so `--wait` polls until the subscrip
|
|
|
232
236
|
open). An organization that already has a subscription changes plans in the billing portal: the
|
|
233
237
|
command prints the portal URL instead (exit 0).
|
|
234
238
|
|
|
239
|
+
`billing overage` shows and changes opt-in overage (owners and billing members). With it on,
|
|
240
|
+
workspaces keep opening and running past the CPU-hours and RAM GiB-hours allowances, and the usage
|
|
241
|
+
past them is charged on the next invoice until the charges reach the spend cap. The cap is per
|
|
242
|
+
billing period, from $1 up to the plan price; `shard usage` shows this period's charges. Every owner
|
|
243
|
+
and billing member gets an email when overage is turned on or off or the cap changes. `--json`
|
|
244
|
+
prints the API's spend policy; `--if-version <n>` (its `version`) refuses the change if someone
|
|
245
|
+
changed the policy since (exit 1, `version_mismatch`). A refused cap exits 2 and the hint names the
|
|
246
|
+
bound: the plan price (`spend_cap_above_plan_price`), $1 (`spend_cap_below_minimum`) or what overage
|
|
247
|
+
already charged this period (`spend_cap_below_charges`); a plan without overage answers
|
|
248
|
+
`overage_unavailable`, and turning it on without a cap `spend_cap_required`.
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
$ shard billing overage on --cap 9
|
|
252
|
+
Acme: overage is on, spend cap $9.00 per billing period ($1.00 to $9.00, the plan price).
|
|
253
|
+
Rates past the allowances: $0.04 per RAM GiB-hour, $0.12 per CPU-hour, charged on the next invoice (this period: shard usage).
|
|
254
|
+
Saved (policy version 1).
|
|
255
|
+
```
|
|
256
|
+
|
|
235
257
|
## Audit, account export and deletion (0.5.0+)
|
|
236
258
|
|
|
237
259
|
```sh
|
|
@@ -330,26 +352,29 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
|
|
|
330
352
|
| `auth register\|verify-email\|resend-verification\|login\|mfa\|status\|logout\|sessions\|step-up`, `auth password change\|reset\|reset-confirm`, `auth email change\|confirm`, `auth totp enroll\|confirm\|disable\|recovery-codes` | The account and its sign-in (0.5.0+): see [Accounts and sign-in](#accounts-and-sign-in-050). |
|
|
331
353
|
| `setup [--org O] [--project P] [--org-name N] [--project-name N] [--key-name N] [--tool T]... [--new-key]`, `env [--shell sh\|fish\|powershell]` | Organization, project and a saved API key in one step; the saved key as shell exports (0.5.0+). See [Setup](#setup-050). |
|
|
332
354
|
| `orgs`, `projects`, `api-keys`, `members`, `invitations`, `templates publish\|archive` | Organizations, projects, API keys, members and invitations with the session (0.5.0+). See [Organizations, projects, keys, members](#organizations-projects-keys-members-050). |
|
|
333
|
-
| `billing plans\|status\|upgrade\|portal\|invoices\|alerts` | Plans, Stripe Checkout, the billing portal, invoices
|
|
355
|
+
| `billing plans\|status\|upgrade\|portal\|invoices\|alerts\|overage` | Plans, Stripe Checkout, the billing portal, invoices, usage alerts (0.5.0+) and opt-in overage (`overage`, 0.5.1+). See [Billing](#billing-050). |
|
|
334
356
|
| `audit list\|export`, `account status\|export\|delete\|cancel-deletion` | The audit log, data exports and account deletion (0.5.0+). |
|
|
335
357
|
| `egress get\|set\|history\|clear`, `volumes list\|create\|get\|delete\|attachments\|attach\|detach` | Outbound network policies and shared volumes with the project key (0.5.0+). |
|
|
336
358
|
| `login`, `whoami` | Show the principal behind the project key (`SHARDFLUX_API_KEY`, else the saved one). |
|
|
337
359
|
| `version [--check]` | Print the CLI and SDK versions and the update status (`--check` asks the API now). |
|
|
360
|
+
| `usage [--org <id>] [--estimate \| --series \| --workspace <id\|key>] [--from T] [--to T] [--granularity hour\|day] [--meter M]` | Usage summary for the current period: plan, meters and allowances. `--estimate`: the bill so far and projected allowance use; `--series`: usage over time; `--workspace`: one workspace's usage over time (0.5.0+). The summary includes the [overage](#usage-and-overage) block (0.5.1+). |
|
|
361
|
+
|
|
338
362
|
| `feedback [--category C] [--workspace W] [--request-id ID] [--error-code CODE] [--command TEXT] [--from-file F\|-] <message...>` | Send feedback straight to the Shardflux founder (0.5.0+). See [Feedback](#feedback-agents-use-this-while-you-work). |
|
|
339
363
|
| `usage [--org <id>] [--estimate \| --series \| --workspace <id\|key>] [--from T] [--to T] [--granularity hour\|day] [--meter M]` | Usage summary for the current period: plan, meters and allowances. `--estimate`: the bill so far and projected allowance use; `--series`: usage over time; `--workspace`: one workspace's usage over time (0.5.0+). |
|
|
340
364
|
| `workspaces open <key> --template <slug> [--secret NAME]... [--input NAME=value]... [--input-file F] [--lifetime persistent\|session] [--mode processful\|file-first] [--cpu-millis N --memory-mib N --disk-gib N] [--no-wait] [--timeout 5m] [--timing]` | Open by key. Creates the workspace on first use and reconnects or resumes it afterwards, never resetting it. Waits until it is ready (the template's start commands and services included) unless you pass `--no-wait`. `--secret` binds secret names (replacing the binding of an existing workspace). `--input` (0.4.0+) passes the template's text inputs (replacing those of an existing workspace); `--input-file` reads `NAME=value` lines or a JSON object. `--lifetime session`: the workspace is discarded when the session ends (`workspaces close` or the idle timeout), and the key then opens a new workspace. `--mode file-first` (0.5.0+) opens a [file-first workspace](#file-first-workspaces); the default is processful for a new key and the stored mode for an existing one. |
|
|
341
365
|
| `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. A file-first workspace's state reads `running (file-first)` (0.5.0+). |
|
|
342
366
|
| `workspaces get <id\|key>` | One workspace (deleted ones included), with the startup of its template's start commands and services (0.4.0+): a failed step shows its exit code, reason and output tail. Its mode, and a file-first workspace's tree revision (0.5.0+). |
|
|
343
367
|
| `workspaces inputs <id\|key>` | The workspace's text inputs (0.4.0+). |
|
|
344
|
-
| `workspaces exec <id\|key> [--cwd D] [--env K=V]... [--timeout T] [--stdin F\|-] [-v] [--execution-id ID] [--output-limit N] -- <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. `-v` (0.5.0+) then prints the session id and exit status on stderr. On a file-first workspace (0.5.0+) the command runs as an execution instead: see [File-first workspaces](#file-first-workspaces). |
|
|
368
|
+
| `workspaces exec <id\|key> [--cwd D] [--env K=V]... [--timeout T] [--stdin F\|-] [-v] [--execution-id ID] [--output-limit N] -- <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. `-v` (0.5.0+) then prints the session id and exit status on stderr. `--cwd` is an absolute path (default `/home/user`); a relative one is refused (`invalid_cwd`, exit 2). A command that could not start (a `--cwd` that is not a directory, a program not on `PATH`) exits 6 with the workspace's reason and a hint (0.5.1+; before, it exited 1 with no output). On a file-first workspace (0.5.0+) the command runs as an execution instead: see [File-first workspaces](#file-first-workspaces). |
|
|
345
369
|
| `executions get <id\|key> <execution-id> [--wait D]` | The result of a file-first workspace's execution (0.5.0+): its output, then what ran. Exits with the command's code, 6 if the execution failed or was lost, 5 while it still runs. |
|
|
346
370
|
| `files read <id\|key> <path> [--out F]` | Raw bytes to stdout, or to a local file. |
|
|
347
371
|
| `files write <id\|key> <path> [--from F\|-] [--append] [--no-parents] [--if-revision N]` | Atomic write. The default source is stdin. `--if-revision` (0.5.0+): file-first workspaces only, see [Tree revisions](#tree-revisions). |
|
|
348
372
|
| `files ls <id\|key> <path> [--limit N]` | Directory listing. |
|
|
349
|
-
| `files stat <id\|key> <path> [--revision]` | One path's metadata; `--revision` adds the content's SHA-256 (0.
|
|
350
|
-
| `files search <id\|key> <path> <pattern> [--regex] [-i\|--ignore-case] [--include G]... [--exclude G]... [--max N]` | Search file contents under a directory (or in one file; gitignore-style globs): `path:line:column: text` per match (0.
|
|
351
|
-
| `files patch <id\|key> <path> (--old T --new T)... [--replace-all] \| --content-file F\|- [--expected-revision R] [--if-revision N]` | Replace exact text (each `--old` must occur once unless `--replace-all`), or the whole content, atomically; `R` is a revision from `files stat --revision` or `absent` (0.
|
|
373
|
+
| `files stat <id\|key> <path> [--revision]` | One path's metadata; `--revision` adds the content's SHA-256 (0.5.0+). |
|
|
374
|
+
| `files search <id\|key> <path> <pattern> [--regex] [-i\|--ignore-case] [--include G]... [--exclude G]... [--max N]` | Search file contents under a directory (or in one file; gitignore-style globs): `path:line:column: text` per match (0.5.0+). |
|
|
375
|
+
| `files patch <id\|key> <path> (--old T --new T)... [--replace-all] \| --content-file F\|- [--expected-revision R] [--if-revision N]` | Replace exact text (each `--old` must occur once unless `--replace-all`), or the whole content, atomically; `R` is a revision from `files stat --revision` or `absent` (0.5.0+). `--if-revision` (0.5.0+): file-first workspaces only. |
|
|
352
376
|
| `workspaces suspend\|resume <id\|key> [--wait] [--timeout T] [--timing]` | Lifecycle operation. Returns immediately unless you pass `--wait`. |
|
|
377
|
+
| `workspaces suspend <id\|key> --when-idle D` / `--cancel-when-idle` | (0.5.1+) Suspend once the workspace has been idle for `D` (30s to 1h), or cancel that. See "Suspend when idle". |
|
|
353
378
|
| `workspaces fork <id\|key> <new-key> [caps] [--wait]` | Fork into a new key. |
|
|
354
379
|
| `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once. The key of a persistent workspace is never reused. |
|
|
355
380
|
| `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`). |
|
|
@@ -540,6 +565,26 @@ without waking it (the state at suspension); `files search` then notes it on std
|
|
|
540
565
|
`shard operations wait <id>` keeps waiting.
|
|
541
566
|
- A resume or open that fails exits 6.
|
|
542
567
|
|
|
568
|
+
## Suspend when idle (0.5.1+)
|
|
569
|
+
|
|
570
|
+
A running workspace is billed while it is awake. When an agent's turn ends, ask for a suspend once the workspace has
|
|
571
|
+
been idle for a short time, instead of waiting for its idle policy:
|
|
572
|
+
|
|
573
|
+
```sh
|
|
574
|
+
shard ws suspend acme/demo --when-idle 2m # 30s to 1h; whole seconds
|
|
575
|
+
shard ws get acme/demo # "suspend when idle after 120 s idle, not before ..."
|
|
576
|
+
shard ws suspend acme/demo --cancel-when-idle
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
- The idle time counts from the later of the workspace's last work and the request.
|
|
580
|
+
- A command still running, an attached stream or a keepalive postpones the suspend until the duration after it ends.
|
|
581
|
+
- The next tool call on the workspace (the next turn) or a resume cancels the request. Repeating replaces it.
|
|
582
|
+
- If a suspend is already in progress, `shard` names its operation and schedules nothing.
|
|
583
|
+
- `--json` prints `{workspace, operation, suspend_request}`; with `--cancel-when-idle`, the workspace.
|
|
584
|
+
- `--when-idle` and `--cancel-when-idle` cannot be combined with each other, `--wait` or `--timeout` (exit 2). A
|
|
585
|
+
workspace that is not running is refused with `conflict` (`not_running`, exit 1), and a file-first workspace (never
|
|
586
|
+
suspended) with `not_supported_for_mode` (exit 1).
|
|
587
|
+
|
|
543
588
|
## Starts that wait for capacity
|
|
544
589
|
|
|
545
590
|
An open, resume or fork that no host can admit yet waits in `capacity_pending` for at most 15
|
|
@@ -557,6 +602,36 @@ hint: no host could admit it before its deadline; nothing was started. Retry lat
|
|
|
557
602
|
- `--no-wake` (or `SHARDFLUX_NO_WAKE=1`) turns this off. A workspace that is not running is then
|
|
558
603
|
refused at once (exit 1, `workspace_not_running`).
|
|
559
604
|
|
|
605
|
+
## Usage and overage
|
|
606
|
+
|
|
607
|
+
`shard usage` prints the current period's plan, meters and allowances. With opt-in overage (0.5.1+), an allowance past
|
|
608
|
+
`included` while overage is on shows cap state `overage`, and a block follows with the overage state, the spend cap,
|
|
609
|
+
the charges of the cap, the date the cap is projected to be reached, and per allowance the units past it and their
|
|
610
|
+
amount:
|
|
611
|
+
|
|
612
|
+
```text
|
|
613
|
+
overage accruing (usage past the allowances is charged on the next invoice)
|
|
614
|
+
spend cap $9.00 (at most $9.00, the plan price)
|
|
615
|
+
overage charges $3.00 of $9.00 (33.33%), $6.00 left; billed on the next invoice
|
|
616
|
+
cap reached 2026-09-29 15:02:46Z (projected at this period's average use)
|
|
617
|
+
|
|
618
|
+
OVERAGE UNIT PAST ALLOWANCE BILLED RATE AMOUNT
|
|
619
|
+
ram_gib_hours gib_hours 70 60 $0.04 $2.40
|
|
620
|
+
cpu_hours hours 5 5 $0.12 $0.60
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`--json` prints the API's summary, with `spend_cap` and `exhausted_reason`. Owners and billing members turn overage on
|
|
624
|
+
and set the cap with [`shard billing overage`](#billing-050) or under **Usage & billing** in the console.
|
|
625
|
+
|
|
626
|
+
While an allowance is used up, opens, resumes and forks are refused with 402 `allowance_exhausted` (exit 1). The error
|
|
627
|
+
names `details.reason`, and a hint says what to do (0.5.0+):
|
|
628
|
+
|
|
629
|
+
| Reason | Hint |
|
|
630
|
+
| --- | --- |
|
|
631
|
+
| `allowance_used` | Upgrade (`shard billing plans`), or turn on overage: `shard billing overage on --cap <dollars>`. |
|
|
632
|
+
| `spend_cap_reached` | The spend cap is reached: raise it (`shard billing overage cap <dollars>`, at most the plan price) or upgrade. |
|
|
633
|
+
| `overage_paused` | A payment is past due: update the payment method (`shard billing portal --open`). |
|
|
634
|
+
|
|
560
635
|
## Timing
|
|
561
636
|
|
|
562
637
|
`--timing` (0.3.0+) says where the time of an open or a wait went. After the call, `shard` prints the lifecycle
|
|
@@ -684,7 +759,7 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
|
|
|
684
759
|
| 3 | not found: workspace, operation or route |
|
|
685
760
|
| 4 | authentication or authorization: missing, malformed, revoked or insufficient key or session; `step_up_required` (run `shard auth step-up`), `mfa_required` (a sign-in waiting for its code), `email_unverified`, `invalid_credentials` |
|
|
686
761
|
| 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`), a start waiting for capacity for at most 15 minutes. `executions get` (0.5.0+): the execution is still queued or running. `billing upgrade --wait` (0.5.0+): nobody paid in time (the checkout stays open) |
|
|
687
|
-
| 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace). `retryable=true` in the error means it may succeed later (`capacity_unavailable`). Also a file-first execution that ended `failed` or `lost` (0.5.0+): nothing was published. `billing upgrade --wait` (0.5.0+): the checkout expired or was canceled (`checkout_ended`) |
|
|
762
|
+
| 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace). `retryable=true` in the error means it may succeed later (`capacity_unavailable`). Also a file-first execution that ended `failed` or `lost` (0.5.0+): nothing was published. `exec` (0.5.1+): the command could not start (`exec_failed_to_start`). `billing upgrade --wait` (0.5.0+): the checkout expired or was canceled (`checkout_ended`) |
|
|
688
763
|
| 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. A file-first execution cannot be canceled: it keeps running, and the error names the `shard executions get` command for its result. |
|
|
689
764
|
| exec, executions get | 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. |
|
|
690
765
|
|
package/dist/account-args.js
CHANGED
|
@@ -303,6 +303,18 @@ export const ACCOUNT_COMMANDS = [
|
|
|
303
303
|
},
|
|
304
304
|
examples: ['shard billing alerts --set 50,80,100'],
|
|
305
305
|
},
|
|
306
|
+
{
|
|
307
|
+
path: ['billing', 'overage'],
|
|
308
|
+
summary: 'Show or change opt-in overage and its spend cap: status (default), on --cap <dollars>, off, cap <dollars>',
|
|
309
|
+
description: 'With overage on, workspaces keep opening and running past the CPU-hours and RAM GiB-hours allowances; the usage past them is charged on the next invoice until the charges reach the spend cap (per billing period, $1 up to the plan price). Off by default; turning it off is always allowed and what it charged stays on the invoice. Owners and billing members change it, and each of them gets an email about the change. This period\'s charges: shard usage.',
|
|
310
|
+
positionals: [{ name: 'status|on|off|cap' }, { name: 'dollars' }],
|
|
311
|
+
options: {
|
|
312
|
+
...ORG,
|
|
313
|
+
cap: { type: 'string', value: '<dollars>', description: 'With on: the spend cap per billing period in dollars, e.g. 9 or 9.50 (needed unless a cap is set already).' },
|
|
314
|
+
'if-version': { type: 'string', value: '<n>', description: 'Only if the policy version is still n (shard billing overage --json shows it): refuses a concurrent change.' },
|
|
315
|
+
},
|
|
316
|
+
examples: ['shard billing overage', 'shard billing overage on --cap 9', 'shard billing overage cap 25.50', 'shard billing overage off'],
|
|
317
|
+
},
|
|
306
318
|
{
|
|
307
319
|
path: ['audit', 'list'],
|
|
308
320
|
summary: 'List the organization’s audit log, newest first (owners and admins)',
|
|
@@ -13,4 +13,6 @@ export type ShellKind = 'sh' | 'fish' | 'powershell';
|
|
|
13
13
|
export declare function exportLine(shell: ShellKind, name: string, value: string): string;
|
|
14
14
|
/** An RFC 3339 time, or a duration ago (`90m`, `24h`, `7d`, `2w`). */
|
|
15
15
|
export declare function parseTime(text: string, option: string, now?: number): string;
|
|
16
|
+
/** A spend cap in dollars ("9", "9.5", "9.50", "$9") as minor units (cents); at most two decimals. */
|
|
17
|
+
export declare function dollarsToMinor(text: string, what: string): number;
|
|
16
18
|
export declare const ACCOUNT_HANDLERS: Record<string, Handler>;
|
package/dist/account-commands.js
CHANGED
|
@@ -18,7 +18,7 @@ import { UUID, parseDuration, parsePositiveInt } from "./args.js";
|
|
|
18
18
|
import { openUrl } from "./browser.js";
|
|
19
19
|
import { fileExists, oneOf, out, parseSlugVersion, principal, readInput, resolveWorkspace, str, strs, waitTimeout } from "./commands.js";
|
|
20
20
|
import { AuthConfigError, CheckoutEndedError, InterruptedError, MfaPendingError, NotFoundError, UsageError } from "./errors.js";
|
|
21
|
-
import { apiKeyTable, attachmentTable, auditTable, authSessionTable, billingStatusText, dataExportText, egressVersionTable, invitationTable, invoiceTable, kv, memberTable, money, orgTable, planTable, projectEgressText, projectTable, table, time, toolsText, volumeDetail, volumeTable, workspaceEgressText, WORKSPACE_HEADERS, workspaceRow, } from "./format.js";
|
|
21
|
+
import { apiKeyTable, attachmentTable, auditTable, authSessionTable, billingStatusText, dataExportText, egressVersionTable, invitationTable, invoiceTable, kv, memberTable, money, orgTable, overagePolicyText, planTable, projectEgressText, projectTable, table, time, toolsText, volumeDetail, volumeTable, workspaceEgressText, WORKSPACE_HEADERS, workspaceRow, } from "./format.js";
|
|
22
22
|
export const DEFAULT_API_URL = 'https://api.shardflux.dev';
|
|
23
23
|
const TOTP = /^\d{6}$/;
|
|
24
24
|
const ROLES = ['owner', 'admin', 'member', 'billing'];
|
|
@@ -409,6 +409,14 @@ function auditFilters(values) {
|
|
|
409
409
|
return f;
|
|
410
410
|
}
|
|
411
411
|
// ---- billing ---------------------------------------------------------------------------------------------------
|
|
412
|
+
const OVERAGE_ACTIONS = ['status', 'on', 'off', 'cap'];
|
|
413
|
+
/** A spend cap in dollars ("9", "9.5", "9.50", "$9") as minor units (cents); at most two decimals. */
|
|
414
|
+
export function dollarsToMinor(text, what) {
|
|
415
|
+
const m = /^\$?(\d{1,7})(?:\.(\d{1,2}))?$/.exec(text.trim());
|
|
416
|
+
if (!m)
|
|
417
|
+
throw new UsageError(`${what} must be an amount in dollars such as 9 or 9.50 (got "${text}")`);
|
|
418
|
+
return Number(m[1]) * 100 + Number((m[2] ?? '').padEnd(2, '0'));
|
|
419
|
+
}
|
|
412
420
|
function purchasablePlans(catalog) {
|
|
413
421
|
return catalog.plans.filter((p) => p.prices.some((x) => x.purchasable)).map((p) => p.plan_key);
|
|
414
422
|
}
|
|
@@ -1377,6 +1385,39 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1377
1385
|
out(ctx, `${org.name}: ${now}${thresholds === null ? '' : ' (saved)'}.\n`, policy);
|
|
1378
1386
|
return 0;
|
|
1379
1387
|
},
|
|
1388
|
+
async 'billing overage'(ctx, values, [action = 'status', amount]) {
|
|
1389
|
+
if (!OVERAGE_ACTIONS.includes(action))
|
|
1390
|
+
throw new UsageError(`unknown action "${action}"; use status, on, off or cap (shard billing overage --help)`);
|
|
1391
|
+
const capText = str(values.cap);
|
|
1392
|
+
if (capText !== undefined && action !== 'on')
|
|
1393
|
+
throw new UsageError(action === 'cap' ? 'give the cap as an argument: shard billing overage cap <dollars>' : `--cap goes with on: shard billing overage on --cap <dollars>`);
|
|
1394
|
+
if (action === 'cap' && amount === undefined)
|
|
1395
|
+
throw new UsageError('missing <dollars> for "shard billing overage cap" (e.g. shard billing overage cap 9)');
|
|
1396
|
+
if (action !== 'cap' && amount !== undefined)
|
|
1397
|
+
throw new UsageError(`unexpected argument "${amount}"${action === 'on' ? '; give the cap with --cap <dollars>' : ''}`);
|
|
1398
|
+
const update = {};
|
|
1399
|
+
if (action === 'on')
|
|
1400
|
+
update.overageEnabled = true;
|
|
1401
|
+
if (action === 'off')
|
|
1402
|
+
update.overageEnabled = false;
|
|
1403
|
+
if (action === 'on' && capText !== undefined)
|
|
1404
|
+
update.spendCapMinor = dollarsToMinor(capText, '--cap');
|
|
1405
|
+
if (action === 'cap')
|
|
1406
|
+
update.spendCapMinor = dollarsToMinor(amount, 'the cap');
|
|
1407
|
+
const version = str(values['if-version']);
|
|
1408
|
+
if (version !== undefined) {
|
|
1409
|
+
if (action === 'status')
|
|
1410
|
+
throw new UsageError('--if-version goes with on, off or cap');
|
|
1411
|
+
if (!/^\d+$/.test(version))
|
|
1412
|
+
throw new UsageError('--if-version must be a policy version number (shard billing overage --json shows it)');
|
|
1413
|
+
update.ifMatch = Number(version);
|
|
1414
|
+
}
|
|
1415
|
+
const account = ctx.account();
|
|
1416
|
+
const org = await resolveOrg(ctx, account, values);
|
|
1417
|
+
const policy = action === 'status' ? await account.billing.spendPolicy(org.id) : await account.billing.setSpendPolicy(org.id, update);
|
|
1418
|
+
out(ctx, overagePolicyText(policy, org.name, action !== 'status'), policy);
|
|
1419
|
+
return 0;
|
|
1420
|
+
},
|
|
1380
1421
|
async 'audit list'(ctx, values) {
|
|
1381
1422
|
const filters = auditFilters(values);
|
|
1382
1423
|
const limitText = str(values.limit);
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.5.
|
|
15
|
+
export const CLI_VERSION = '0.5.1';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -241,10 +241,10 @@ const KEY_COMMANDS = [
|
|
|
241
241
|
{
|
|
242
242
|
path: ['workspaces', 'exec'],
|
|
243
243
|
summary: 'Run a command in a workspace; prints its output and exits with its exit code',
|
|
244
|
-
description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution cannot be canceled: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6.',
|
|
244
|
+
description: 'Runs argv directly (no shell; use `-- bash -lc "..."` for shell syntax). Processful workspaces: output is streamed as it arrives and resumes from byte offsets after dropped connections; Ctrl-C stops waiting and cancels the command. File-first workspaces: the command runs as an execution in a fresh VM on the latest tree revision; its output is printed when it ended, and the files it changed under /home/user become the next tree revision (-v lists them). An execution cannot be canceled: Ctrl-C stops waiting and names the command that fetches its result later. A failed or lost execution (nothing published) exits 6, as does a command that could not start (a --cwd that is not a directory, a program not on PATH), with the workspace’s reason.',
|
|
245
245
|
positionals: [REF, { name: 'command', required: true, variadic: true }],
|
|
246
246
|
options: {
|
|
247
|
-
cwd: { type: 'string', value: '<dir>', description: 'Working directory (
|
|
247
|
+
cwd: { type: 'string', value: '<dir>', description: 'Working directory, an absolute path (default /home/user; under /home/user on a file-first workspace). A relative path is refused (invalid_cwd).' },
|
|
248
248
|
env: { type: 'string', multiple: true, value: '<K=V>', description: 'Environment variable (repeatable).' },
|
|
249
249
|
timeout: { type: 'string', value: '<duration>', description: 'Kill the command after this long (exit code 124).' },
|
|
250
250
|
stdin: { type: 'string', value: '<file|->', description: 'Send this file (or - for standard input) to the command’s stdin (max 1 MiB).' },
|
|
@@ -268,7 +268,18 @@ const KEY_COMMANDS = [
|
|
|
268
268
|
'shard ws exec acme/agent-7 --json -- python3 /home/user/report.py > result.json',
|
|
269
269
|
],
|
|
270
270
|
},
|
|
271
|
-
{
|
|
271
|
+
{
|
|
272
|
+
path: ['workspaces', 'suspend'],
|
|
273
|
+
summary: 'Suspend a running workspace (durable checkpoint), now or once it has been idle',
|
|
274
|
+
description: 'Without options the suspend is requested now. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
|
|
275
|
+
positionals: [REF],
|
|
276
|
+
options: {
|
|
277
|
+
'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (30s to 1h, e.g. 30s, 2m; 0.5.1+). Cannot be combined with --wait.' },
|
|
278
|
+
'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
|
|
279
|
+
...WAIT,
|
|
280
|
+
},
|
|
281
|
+
examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
|
|
282
|
+
},
|
|
272
283
|
{ path: ['workspaces', 'resume'], summary: 'Resume a suspended workspace', positionals: [REF], options: { ...WAIT } },
|
|
273
284
|
{
|
|
274
285
|
path: ['workspaces', 'fork'],
|
|
@@ -700,7 +711,7 @@ const GROUPS = {
|
|
|
700
711
|
'api-keys': 'Project API keys (signed in; secrets are shown once)',
|
|
701
712
|
members: 'Members of an organization and their roles (signed in)',
|
|
702
713
|
invitations: 'Invite people to an organization and accept invitations (signed in)',
|
|
703
|
-
billing: 'Plans, upgrades (Stripe Checkout), the billing portal, invoices
|
|
714
|
+
billing: 'Plans, upgrades (Stripe Checkout), the billing portal, invoices, usage alerts and overage (signed in; plans is public)',
|
|
704
715
|
audit: 'The organization audit log and its export (signed in; owners and admins)',
|
|
705
716
|
account: 'Your account: data export and deletion (signed in)',
|
|
706
717
|
egress: 'Outbound network policies of the project and its workspaces (project API key)',
|
package/dist/commands.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export type Handler = (ctx: Ctx, values: Values, positionals: string[]) => Promi
|
|
|
70
70
|
export declare const str: (v: Values[string]) => string | undefined;
|
|
71
71
|
export declare const strs: (v: Values[string]) => string[];
|
|
72
72
|
export declare function out(ctx: Ctx, human: string, machine: unknown): void;
|
|
73
|
+
/** `--when-idle <duration>`: whole seconds from 30 to 3600 (the API's after_seconds bounds, contracts §20.6). */
|
|
74
|
+
export declare function whenIdleSeconds(text: string): number;
|
|
73
75
|
export declare function waitTimeout(values: Values): number;
|
|
74
76
|
/**
|
|
75
77
|
* `<id|key>`: a UUID is tried as an id first; anything else (or an unknown UUID) as an exact workspace key, across every
|
package/dist/commands.js
CHANGED
|
@@ -16,7 +16,7 @@ import { updateCheckDisabled } from "./update-check.js";
|
|
|
16
16
|
import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
|
|
17
17
|
import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
|
|
18
18
|
import { timingJson } from "./timing.js";
|
|
19
|
-
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
19
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
20
20
|
const DEFAULT_WAIT_MS = 300_000;
|
|
21
21
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
22
22
|
const MAX_STDIN = 1024 * 1024;
|
|
@@ -66,6 +66,16 @@ function caps(values) {
|
|
|
66
66
|
c.disk_gib = parsePositiveInt(disk, 'disk-gib', 1_000_000);
|
|
67
67
|
return Object.keys(c).length ? c : undefined;
|
|
68
68
|
}
|
|
69
|
+
/** `--when-idle <duration>`: whole seconds from 30 to 3600 (the API's after_seconds bounds, contracts §20.6). */
|
|
70
|
+
export function whenIdleSeconds(text) {
|
|
71
|
+
const ms = parseDuration(text, 'when-idle');
|
|
72
|
+
if (ms % 1000 !== 0)
|
|
73
|
+
throw new UsageError('--when-idle must be a whole number of seconds (e.g. 30s, 90s, 2m)');
|
|
74
|
+
const s = ms / 1000;
|
|
75
|
+
if (s < 30 || s > 3600)
|
|
76
|
+
throw new UsageError(`--when-idle must be between 30s and 1h (got "${text}")`);
|
|
77
|
+
return s;
|
|
78
|
+
}
|
|
69
79
|
export function waitTimeout(values) {
|
|
70
80
|
const t = str(values.timeout);
|
|
71
81
|
return t === undefined ? DEFAULT_WAIT_MS : parseDuration(t, 'timeout');
|
|
@@ -697,12 +707,30 @@ export const KEY_HANDLERS = {
|
|
|
697
707
|
throw err;
|
|
698
708
|
}
|
|
699
709
|
const code = execExitCode(r);
|
|
710
|
+
// No exit code, no timeout, no signal: the session ended without the command's own ending (e.g. lost when its host
|
|
711
|
+
// restarted). Say so rather than exit 1 in silence (0.5.1); a command that could not start threw ExecStartError.
|
|
712
|
+
const unexplained = r.exitCode === null && !r.timedOut && !(r.termSignal !== null && r.termSignal > 0);
|
|
700
713
|
if (values.verbose === true && !ctx.json) {
|
|
701
|
-
const status = r.timedOut ? 'timed out' : r.canceled ? 'canceled' : r.termSignal ? `killed by signal ${r.termSignal}` : `exit code ${r.exitCode ?? '-'}`;
|
|
714
|
+
const status = r.timedOut ? 'timed out' : r.canceled ? 'canceled' : r.termSignal ? `killed by signal ${r.termSignal}` : unexplained ? r.session.state : `exit code ${r.exitCode ?? '-'}`;
|
|
702
715
|
ctx.io.stderr.write(kv([['session', r.sessionId], ['status', status], ['output', `${r.stdoutBytes} bytes stdout, ${r.stderrBytes} bytes stderr`], ['reconnects', String(r.reconnects)]]));
|
|
703
716
|
}
|
|
704
717
|
if (ctx.json) {
|
|
705
|
-
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
718
|
+
ctx.io.stdout.write(json(withTiming(ctx, {
|
|
719
|
+
session_id: r.sessionId,
|
|
720
|
+
state: r.session.state,
|
|
721
|
+
exit_code: r.exitCode,
|
|
722
|
+
term_signal: r.termSignal,
|
|
723
|
+
timed_out: r.timedOut,
|
|
724
|
+
canceled: r.canceled,
|
|
725
|
+
stdout: r.stdout,
|
|
726
|
+
stderr: r.stderr,
|
|
727
|
+
truncated: r.truncated,
|
|
728
|
+
reconnects: r.reconnects,
|
|
729
|
+
...(r.session.error ? { error: r.session.error } : {}),
|
|
730
|
+
})));
|
|
731
|
+
}
|
|
732
|
+
else if (unexplained) {
|
|
733
|
+
ctx.io.stderr.write(`exec session ${r.sessionId} ended ${r.session.state} without an exit code${r.session.error ? `: ${r.session.error}` : ''}\n`);
|
|
706
734
|
}
|
|
707
735
|
return code;
|
|
708
736
|
},
|
|
@@ -822,7 +850,28 @@ export const KEY_HANDLERS = {
|
|
|
822
850
|
return 0;
|
|
823
851
|
},
|
|
824
852
|
async 'workspaces suspend'(ctx, values, [ref]) {
|
|
825
|
-
|
|
853
|
+
const whenIdle = str(values['when-idle']);
|
|
854
|
+
const cancel = values['cancel-when-idle'] === true;
|
|
855
|
+
if (whenIdle === undefined && !cancel)
|
|
856
|
+
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, o), ['suspend', 'Suspend']);
|
|
857
|
+
if (whenIdle !== undefined && cancel)
|
|
858
|
+
throw new UsageError('--when-idle and --cancel-when-idle cannot be combined');
|
|
859
|
+
const flag = cancel ? '--cancel-when-idle' : '--when-idle';
|
|
860
|
+
if (values.wait === true || str(values.timeout) !== undefined)
|
|
861
|
+
throw new UsageError(`${flag} cannot be combined with --wait or --timeout (nothing is suspended now)`);
|
|
862
|
+
if (cancel) {
|
|
863
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
864
|
+
const pending = ws.suspendRequest;
|
|
865
|
+
const after = await ctx.cloud().workspaces.cancelSuspendWhenIdle(ws.id);
|
|
866
|
+
out(ctx, pending ? `Canceled the pending suspend-when-idle request of ${ws.key} (${ws.id}): ${suspendRequestText(pending)}.\n` : `No suspend-when-idle request was pending for ${ws.key} (${ws.id}).\n`, after.data);
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
const afterSeconds = whenIdleSeconds(whenIdle);
|
|
870
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
871
|
+
requireProcessful(ws, 'suspend_when_idle', 'Suspend when idle');
|
|
872
|
+
const r = await ctx.cloud().workspaces.suspendWhenIdle(ws.id, { afterSeconds });
|
|
873
|
+
out(ctx, suspendWhenIdleText(ws, { suspend_request: r.suspendRequest, operation: r.operation }), { workspace: r.workspace.data, operation: r.operation, suspend_request: r.suspendRequest });
|
|
874
|
+
return 0;
|
|
826
875
|
},
|
|
827
876
|
async 'workspaces resume'(ctx, values, [ref]) {
|
|
828
877
|
// With --wait the resume is held until the workspace runs (contracts §22.6); its tool token is the CLI's label.
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { CheckoutTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
6
|
+
import { CheckoutTimeoutError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -120,6 +120,10 @@ function isNamed(err, name) {
|
|
|
120
120
|
return typeof err === 'object' && err !== null && err.name === name;
|
|
121
121
|
}
|
|
122
122
|
export function describeError(err) {
|
|
123
|
+
// A processful command that could not start (0.5.1; the SDK's ExecStartError): the workspace's reason, no HTTP status
|
|
124
|
+
// (the start itself was answered 201) and details.error, details.session_id.
|
|
125
|
+
if (err instanceof ExecStartError)
|
|
126
|
+
return { code: err.code, message: err.message, retryable: false, ...(err.details ? { details: err.details } : {}), source: err.source };
|
|
123
127
|
if (err instanceof ShardfluxApiError) {
|
|
124
128
|
return {
|
|
125
129
|
code: err.code,
|
|
@@ -205,6 +209,9 @@ export function exitCodeFor(err, interrupted = false) {
|
|
|
205
209
|
return EXIT.operationFailed;
|
|
206
210
|
if (err instanceof ExecutionPendingError)
|
|
207
211
|
return EXIT.timeout;
|
|
212
|
+
// A command that could not start exits 6 in both modes, as a failed execution does (0.5.1).
|
|
213
|
+
if (err instanceof ExecStartError)
|
|
214
|
+
return EXIT.operationFailed;
|
|
208
215
|
if (err instanceof ShardfluxApiError) {
|
|
209
216
|
if (AUTH_CODES.has(err.code) || err.status === 401)
|
|
210
217
|
return EXIT.auth;
|
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, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
6
|
+
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, 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;
|
|
@@ -28,6 +28,16 @@ export declare function isReady(w: WorkspaceView): boolean;
|
|
|
28
28
|
export declare function startupText(s: WorkspaceView['startup'] | undefined): string | null;
|
|
29
29
|
/** The failed step's output tail, indented, after the detail block (empty otherwise). */
|
|
30
30
|
export declare function startupTail(s: WorkspaceView['startup'] | undefined): string;
|
|
31
|
+
/** A pending suspend-when-idle request in one line (0.5.1; contracts §20.6). */
|
|
32
|
+
export declare function suspendRequestText(r: SuspendRequest): string;
|
|
33
|
+
/** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
|
|
34
|
+
export declare function suspendWhenIdleText(w: {
|
|
35
|
+
key: string;
|
|
36
|
+
id: string;
|
|
37
|
+
}, r: {
|
|
38
|
+
suspend_request: SuspendRequest | null;
|
|
39
|
+
operation: Operation | null;
|
|
40
|
+
}): string;
|
|
31
41
|
export declare function workspaceDetail(w: WorkspaceView): string;
|
|
32
42
|
export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
|
|
33
43
|
export declare function operationTable(ops: readonly Operation[]): string;
|
|
@@ -44,9 +54,12 @@ export declare function secretDetail(s: Secret): string;
|
|
|
44
54
|
export declare function secretVersionTable(rows: readonly SecretVersion[]): string;
|
|
45
55
|
export declare function secretAccessTable(rows: readonly SecretAccessEvent[]): string;
|
|
46
56
|
export declare function bindingsText(b: WorkspaceSecretBindings): string;
|
|
57
|
+
/** Minor units as money (the overage block): `$3.00` for usd, else `3.00 EUR`. A fractional minor amount (a sub-cent rate) keeps its digits. */
|
|
58
|
+
export declare function capMoney(minor: unknown, currency: string): string;
|
|
47
59
|
/**
|
|
48
60
|
* The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
|
|
49
|
-
* freshness, then meters and allowances as tables
|
|
61
|
+
* freshness, then meters and allowances as tables (cap state `overage` past an allowance while opt-in overage is on),
|
|
62
|
+
* then the overage block. Columns absent from the response are omitted.
|
|
50
63
|
*/
|
|
51
64
|
export declare function usageText(summary: unknown): string;
|
|
52
65
|
/**
|
|
@@ -131,3 +144,8 @@ export declare function attachmentTable(rows: readonly VolumeAttachment[]): stri
|
|
|
131
144
|
export declare function projectEgressText(p: ProjectEgressPolicy): string;
|
|
132
145
|
export declare function workspaceEgressText(w: WorkspaceEgressPolicy): string;
|
|
133
146
|
export declare function egressVersionTable(rows: readonly EgressPolicyVersion[]): string;
|
|
147
|
+
/**
|
|
148
|
+
* The spend policy's overage settings for people (GET/PUT /v1/organizations/{id}/spend-policy): the state, the spend
|
|
149
|
+
* cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
|
|
150
|
+
*/
|
|
151
|
+
export declare function overagePolicyText(p: SpendPolicy, orgName: string, saved?: boolean): string;
|
package/dist/format.js
CHANGED
|
@@ -107,9 +107,25 @@ export function startupTail(s) {
|
|
|
107
107
|
const lines = s.output_tail.replace(/\n$/, '').split('\n');
|
|
108
108
|
return `startup output (last ${Buffer.byteLength(s.output_tail)} bytes):\n${lines.map((l) => ` | ${l}`).join('\n')}\n`;
|
|
109
109
|
}
|
|
110
|
+
/** A pending suspend-when-idle request in one line (0.5.1; contracts §20.6). */
|
|
111
|
+
export function suspendRequestText(r) {
|
|
112
|
+
return `after ${r.after_seconds} s idle, not before ${time(r.not_before)} (requested ${time(r.requested_at)})`;
|
|
113
|
+
}
|
|
114
|
+
/** `shard ws suspend --when-idle`: the recorded request, or the suspend already in progress. */
|
|
115
|
+
export function suspendWhenIdleText(w, r) {
|
|
116
|
+
if (r.suspend_request) {
|
|
117
|
+
const s = r.suspend_request;
|
|
118
|
+
return (`Suspend when idle: ${w.key} (${w.id}) is suspended once it has been idle for ${s.after_seconds} s, not before ${time(s.not_before)}.\n` +
|
|
119
|
+
'A running command, an attached stream or a keepalive postpones it; the next tool call or a resume cancels it.\n' +
|
|
120
|
+
`cancel with: shard workspaces suspend ${w.key} --cancel-when-idle\n`);
|
|
121
|
+
}
|
|
122
|
+
const op = r.operation;
|
|
123
|
+
return `A suspend of ${w.key} (${w.id}) is already in progress (operation ${op.id} ${op.kind} ${op.state}); nothing was scheduled.\nwait with: shard operations wait ${op.id}\n`;
|
|
124
|
+
}
|
|
110
125
|
export function workspaceDetail(w) {
|
|
111
126
|
const c = w.ceilings;
|
|
112
127
|
const startup = startupText(w.startup);
|
|
128
|
+
const suspendRequest = w.idle?.suspend_request;
|
|
113
129
|
return kv([
|
|
114
130
|
['id', w.id],
|
|
115
131
|
['key', w.workspace_key],
|
|
@@ -125,6 +141,7 @@ export function workspaceDetail(w) {
|
|
|
125
141
|
['caps', pairs(w.caps)],
|
|
126
142
|
['operation', w.active_operation ? `${operationSummary(w.active_operation)} ${w.active_operation.id}` : '-'],
|
|
127
143
|
['pending', w.pending_reason ?? '-'],
|
|
144
|
+
...(suspendRequest ? [['suspend when idle', suspendRequestText(suspendRequest)]] : []),
|
|
128
145
|
...(startup ? [['startup', startup]] : []),
|
|
129
146
|
['forked from', w.forked_from_workspace_id ?? '-'],
|
|
130
147
|
...(w.lifetime !== undefined
|
|
@@ -232,10 +249,66 @@ function objectTable(rows, columns) {
|
|
|
232
249
|
const cols = columns.filter(([k]) => objs.some((o) => k in o));
|
|
233
250
|
return table(cols.map(([, h]) => h), objs.map((o) => cols.map(([k]) => (k === 'percent_used' && typeof o[k] === 'number' ? `${o[k]}%` : scalar(o[k])))));
|
|
234
251
|
}
|
|
235
|
-
const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'meters', 'allowances', 'exhausted', 'alert_thresholds']);
|
|
252
|
+
const USAGE_KNOWN = new Set(['plan', 'period', 'measurement', 'allowance_exhausted', 'exhausted_reason', 'egress_override', 'meters', 'allowances', 'exhausted', 'alert_thresholds', 'spend_cap']);
|
|
253
|
+
/** Minor units as money (the overage block): `$3.00` for usd, else `3.00 EUR`. A fractional minor amount (a sub-cent rate) keeps its digits. */
|
|
254
|
+
export function capMoney(minor, currency) {
|
|
255
|
+
if (typeof minor !== 'number')
|
|
256
|
+
return '-';
|
|
257
|
+
const major = minor / 100;
|
|
258
|
+
const digits = Number.isInteger(minor) ? 2 : Math.min(6, Math.max(2, String(major).split('.')[1]?.length ?? 2));
|
|
259
|
+
const n = major.toFixed(digits);
|
|
260
|
+
return currency.toLowerCase() === 'usd' ? `$${n}` : `${n} ${currency.toUpperCase()}`;
|
|
261
|
+
}
|
|
262
|
+
/** What each spend_cap.state means for the reader (API contract: SpendCap.state). */
|
|
263
|
+
const OVERAGE_STATES = {
|
|
264
|
+
unavailable: 'not available on this plan',
|
|
265
|
+
off: 'starts are refused once an allowance is used up; an owner or billing member can turn overage on under Usage & billing',
|
|
266
|
+
paused: 'a plan payment is past due; overage resumes once it is paid',
|
|
267
|
+
within_allowance: 'on, no usage past the allowances yet',
|
|
268
|
+
accruing: 'usage past the allowances is charged on the next invoice',
|
|
269
|
+
warning: '80% or more of the spend cap is used',
|
|
270
|
+
reached: 'the spend cap is reached: new opens/resumes are refused and running workspaces are suspended',
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* The opt-in overage block (0.5.1; `spend_cap` of the summary, SDK 0.10.0): state, cap, charges of the cap, the
|
|
274
|
+
* projected date the cap is reached, and per allowance the units past it and their amount. Empty without spend_cap
|
|
275
|
+
* (an API older than overage).
|
|
276
|
+
*/
|
|
277
|
+
function spendCapText(cap) {
|
|
278
|
+
if (!isObject(cap) || typeof cap.state !== 'string')
|
|
279
|
+
return '';
|
|
280
|
+
const cur = typeof cap.currency === 'string' ? cap.currency : 'usd';
|
|
281
|
+
const rows = [['overage', `${cap.state}${OVERAGE_STATES[cap.state] ? ` (${OVERAGE_STATES[cap.state]})` : ''}`]];
|
|
282
|
+
if (cap.state === 'unavailable')
|
|
283
|
+
return kv(rows);
|
|
284
|
+
const charges = typeof cap.charges_minor === 'number' ? cap.charges_minor : 0;
|
|
285
|
+
if (cap.state !== 'off') {
|
|
286
|
+
const plan = typeof cap.max_cap_minor === 'number' ? capMoney(cap.max_cap_minor, cur) : undefined;
|
|
287
|
+
if (cap.cap_minor === null || cap.cap_minor === undefined)
|
|
288
|
+
rows.push(['spend cap', `not set: ${capMoney(cap.effective_cap_minor, cur)}${plan ? ' (the plan price)' : ''} applies`]);
|
|
289
|
+
else if (typeof cap.effective_cap_minor === 'number' && typeof cap.cap_minor === 'number' && cap.effective_cap_minor < cap.cap_minor) {
|
|
290
|
+
rows.push(['spend cap', `${capMoney(cap.cap_minor, cur)}, lowered to ${capMoney(cap.effective_cap_minor, cur)}${plan ? ' (the plan price)' : ''}`]);
|
|
291
|
+
}
|
|
292
|
+
else
|
|
293
|
+
rows.push(['spend cap', `${capMoney(cap.cap_minor, cur)}${plan ? ` (at most ${plan}, the plan price)` : ''}`]);
|
|
294
|
+
}
|
|
295
|
+
if (cap.state !== 'off' || charges > 0) {
|
|
296
|
+
const percent = typeof cap.percent_of_cap === 'number' ? ` (${cap.percent_of_cap}%)` : '';
|
|
297
|
+
rows.push(['overage charges', `${capMoney(charges, cur)} of ${capMoney(cap.effective_cap_minor, cur)}${percent}, ${capMoney(cap.remaining_minor, cur)} left; billed on the next invoice`]);
|
|
298
|
+
}
|
|
299
|
+
if (typeof cap.projected_reached_at === 'string')
|
|
300
|
+
rows.push(['cap reached', `${time(cap.projected_reached_at)} (projected at this period's average use)`]);
|
|
301
|
+
let out = kv(rows);
|
|
302
|
+
const lines = Array.isArray(cap.lines) ? cap.lines.filter(isObject) : [];
|
|
303
|
+
if (lines.some((l) => (typeof l.units_over === 'number' && l.units_over > 0) || (typeof l.amount_minor === 'number' && l.amount_minor > 0))) {
|
|
304
|
+
out += `\n${table(['OVERAGE', 'UNIT', 'PAST ALLOWANCE', 'BILLED', 'RATE', 'AMOUNT'], lines.map((l) => [scalar(l.allowance), scalar(l.unit), scalar(l.units_over), scalar(l.billed_units), capMoney(l.rate_minor, cur), capMoney(l.amount_minor, cur)]))}`;
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
236
308
|
/**
|
|
237
309
|
* The usage summary (GET /v1/organizations/{id}/usage/summary): plan, period, measurement
|
|
238
|
-
* freshness, then meters and allowances as tables
|
|
310
|
+
* freshness, then meters and allowances as tables (cap state `overage` past an allowance while opt-in overage is on),
|
|
311
|
+
* then the overage block. Columns absent from the response are omitted.
|
|
239
312
|
*/
|
|
240
313
|
export function usageText(summary) {
|
|
241
314
|
if (!isObject(summary))
|
|
@@ -251,8 +324,10 @@ export function usageText(summary) {
|
|
|
251
324
|
head.push(['measured through', time(m.measured_through)]);
|
|
252
325
|
if (isObject(s.measurement) && s.measurement.estimate_status !== undefined)
|
|
253
326
|
head.push(['estimate', scalar(s.measurement.estimate_status)]);
|
|
254
|
-
if (typeof s.allowance_exhausted === 'boolean')
|
|
255
|
-
|
|
327
|
+
if (typeof s.allowance_exhausted === 'boolean') {
|
|
328
|
+
const reason = typeof s.exhausted_reason === 'string' ? `${s.exhausted_reason}: ` : '';
|
|
329
|
+
head.push(['allowance exhausted', s.allowance_exhausted ? `yes (${reason}new opens/resumes are refused)` : 'no']);
|
|
330
|
+
}
|
|
256
331
|
for (const [k, v] of Object.entries(s))
|
|
257
332
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
258
333
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
|
@@ -280,6 +355,9 @@ export function usageText(summary) {
|
|
|
280
355
|
]);
|
|
281
356
|
if (allowances)
|
|
282
357
|
out += `\n${allowances}`;
|
|
358
|
+
const overage = spendCapText(s.spend_cap);
|
|
359
|
+
if (overage)
|
|
360
|
+
out += `\n${overage}`;
|
|
283
361
|
return out || json(summary);
|
|
284
362
|
}
|
|
285
363
|
/** Request ids and error codes pasted unquoted into the suggested command must be shell-inert. */
|
|
@@ -755,3 +833,35 @@ export function workspaceEgressText(w) {
|
|
|
755
833
|
export function egressVersionTable(rows) {
|
|
756
834
|
return table(['VERSION', 'KIND', 'POLICY', 'BY', 'CREATED'], rows.map((v) => [String(v.version), v.kind, v.kind === 'cleared' ? '-' : egressPolicyLine({ mode: v.mode ?? 'allow_all', rules: v.rules, cidrs: v.cidrs }), `${v.created_by.type}:${v.created_by.id}`, time(v.created_at)]));
|
|
757
835
|
}
|
|
836
|
+
// ---- Overage settings (0.5.1; shard billing overage) -----------------------------------------------------------
|
|
837
|
+
const RATE_UNITS = { ram_gib_hours: 'RAM GiB-hour', cpu_hours: 'CPU-hour' };
|
|
838
|
+
/**
|
|
839
|
+
* The spend policy's overage settings for people (GET/PUT /v1/organizations/{id}/spend-policy): the state, the spend
|
|
840
|
+
* cap and its bounds, the rates, and the next step. `saved` adds the version the change was saved as.
|
|
841
|
+
*/
|
|
842
|
+
export function overagePolicyText(p, orgName, saved = false) {
|
|
843
|
+
const m = (minor) => capMoney(minor, p.currency);
|
|
844
|
+
const bound = p.spend_cap_max_minor === null ? '' : `${m(p.spend_cap_min_minor)} to ${m(p.spend_cap_max_minor)}, the plan price`;
|
|
845
|
+
const cap = p.spend_cap_minor === null ? 'no spend cap set' : `spend cap ${m(p.spend_cap_minor)} per billing period`;
|
|
846
|
+
const lines = [];
|
|
847
|
+
switch (p.overage_state) {
|
|
848
|
+
case 'unavailable':
|
|
849
|
+
lines.push(`${orgName}: overage is not available on this plan: it needs a paid plan with opt-in overage (shard billing plans).`);
|
|
850
|
+
break;
|
|
851
|
+
case 'off':
|
|
852
|
+
lines.push(`${orgName}: overage is off (${cap}): new opens and resumes are refused once an allowance is used up.`);
|
|
853
|
+
lines.push(`Turn it on${p.spend_cap_minor === null ? ` with a spend cap${bound ? ` of ${bound}` : ''}: shard billing overage on --cap <dollars>` : ': shard billing overage on'}`);
|
|
854
|
+
break;
|
|
855
|
+
case 'paused':
|
|
856
|
+
lines.push(`${orgName}: overage is on but paused while a plan payment is past due (update the payment method: shard billing portal --open); ${cap}.`);
|
|
857
|
+
break;
|
|
858
|
+
default:
|
|
859
|
+
lines.push(`${orgName}: overage is on, ${cap}${bound ? ` (${bound})` : ''}.`);
|
|
860
|
+
}
|
|
861
|
+
if (p.overage_state !== 'unavailable' && p.rates.length > 0) {
|
|
862
|
+
lines.push(`Rates past the allowances: ${p.rates.map((r) => `${m(r.amount_minor_per_unit)} per ${RATE_UNITS[r.allowance] ?? r.unit}`).join(', ')}, charged on the next invoice (this period: shard usage).`);
|
|
863
|
+
}
|
|
864
|
+
if (saved)
|
|
865
|
+
lines.push(`Saved (policy version ${p.version}).`);
|
|
866
|
+
return `${lines.join('\n')}\n`;
|
|
867
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { MainOptions, RunOptions } from './main.js';
|
|
|
8
8
|
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, HELP_SECTIONS, commandHelp, findCommand, groupHelp, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, resolveWake, resolveWakeTimeout, topHelp } from './args.js';
|
|
9
9
|
export type { CommandSpec, OptionSpec, Parsed, PositionalSpec, Values } from './args.js';
|
|
10
10
|
export { HANDLERS } from './handlers.js';
|
|
11
|
-
export { KEY_HANDLERS, execExitCode, resolveWorkspace } from './commands.js';
|
|
11
|
+
export { KEY_HANDLERS, execExitCode, resolveWorkspace, whenIdleSeconds } from './commands.js';
|
|
12
12
|
export type { Ctx, Handler, Io, ResolvedApiKey, Writer } from './commands.js';
|
|
13
13
|
export { ACCOUNT_HANDLERS, DEFAULT_API_URL, defaultOrgName, exportLine, parseTime } from './account-commands.js';
|
|
14
14
|
export type { ShellKind } from './account-commands.js';
|
|
@@ -21,5 +21,5 @@ export type { Distribution, UpdateView } from './update-check.js';
|
|
|
21
21
|
export { openUrl, openerFor } from './browser.js';
|
|
22
22
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
|
|
23
23
|
export type { ErrorInfo } from './errors.js';
|
|
24
|
-
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
24
|
+
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
25
25
|
export { TimingRecorder, timingJson, timingText } from './timing.js';
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
export { run, main, abortableSleep } from "./main.js";
|
|
7
7
|
export { COMMANDS, GLOBAL_OPTIONS, CLI_VERSION, HELP_SECTIONS, commandHelp, findCommand, groupHelp, normalizeWords, parseCommandLine, parseDuration, parseEnvPairs, parsePositiveInt, resolveApiUrl, resolveWake, resolveWakeTimeout, topHelp } from "./args.js";
|
|
8
8
|
export { HANDLERS } from "./handlers.js";
|
|
9
|
-
export { KEY_HANDLERS, execExitCode, resolveWorkspace } from "./commands.js";
|
|
9
|
+
export { KEY_HANDLERS, execExitCode, resolveWorkspace, whenIdleSeconds } from "./commands.js";
|
|
10
10
|
export { ACCOUNT_HANDLERS, DEFAULT_API_URL, defaultOrgName, exportLine, parseTime } from "./account-commands.js";
|
|
11
11
|
export { CREDENTIALS_FILE, CredentialStore, CredentialsFileError, configDir, ensurePrivateDir, parseCredentials, writePrivateFile } from "./credentials.js";
|
|
12
12
|
export { Input } from "./input.js";
|
|
13
13
|
export { CHECK_INTERVAL_MS, CLI_DISTRIBUTION, NOTICE_WAIT_MS, UPDATE_CHECK_FILE, UpdateCheck, noticeOf, statusFromEntry, updateCheckDisabled, updateView } from "./update-check.js";
|
|
14
14
|
export { openUrl, openerFor } from "./browser.js";
|
|
15
15
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
16
|
-
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
16
|
+
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
17
17
|
export { TimingRecorder, timingJson, timingText } from "./timing.js";
|
package/dist/main.js
CHANGED
|
@@ -15,7 +15,7 @@ import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shard
|
|
|
15
15
|
import { CLI_VERSION, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
16
16
|
import { CredentialStore, configDir } from "./credentials.js";
|
|
17
17
|
import { AuthConfigError, EXIT, ExecutionPendingError, InterruptedError, UsageError, describeError, exitCodeFor, ownHint, redact } from "./errors.js";
|
|
18
|
-
import { errorText, feedbackCommand, json } from "./format.js";
|
|
18
|
+
import { errorText, feedbackCommand, json, time } from "./format.js";
|
|
19
19
|
import { HANDLERS } from "./handlers.js";
|
|
20
20
|
import { makeFetch } from "./http.js";
|
|
21
21
|
import { Input } from "./input.js";
|
|
@@ -74,6 +74,11 @@ function hintFor(info, cred, file) {
|
|
|
74
74
|
return 'change plans in the billing portal: shard billing portal --open';
|
|
75
75
|
if (info.code === 'dependency_unavailable' && reasonOf(info) === 'billing_disabled')
|
|
76
76
|
return 'this API has no billing configured: plans cannot be bought here';
|
|
77
|
+
if (info.code === 'allowance_exhausted')
|
|
78
|
+
return allowanceHint(info);
|
|
79
|
+
const policyHint = spendPolicyHint(info);
|
|
80
|
+
if (policyHint)
|
|
81
|
+
return policyHint;
|
|
77
82
|
// File patches (contracts §26.2): what to do about each refusal. details.index is the 0-based edit (--old) index.
|
|
78
83
|
const reason = info.details?.reason;
|
|
79
84
|
const index = typeof info.details?.index === 'number' ? ` #${info.details.index + 1}` : '';
|
|
@@ -91,8 +96,76 @@ function hintFor(info, cred, file) {
|
|
|
91
96
|
return 'the workspace’s host has no room to run it right now; retry in a few seconds';
|
|
92
97
|
if (reason === 'host_feature_unavailable')
|
|
93
98
|
return hostFeatureHint(info.details?.feature);
|
|
99
|
+
// A processful command that could not start (0.5.1): what to change, from the workspace's reason.
|
|
100
|
+
if (info.code === 'conflict' && reason === 'exec_failed_to_start')
|
|
101
|
+
return startFailureHint(typeof info.details?.error === 'string' ? info.details.error : info.message, false);
|
|
94
102
|
return fileFirstHint(info);
|
|
95
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* What to do about a command that could not start (exec_failed_to_start), from the workspace's reason: a working
|
|
106
|
+
* directory that is not a directory, else a program that is not on PATH. An unknown user needs no more than the reason.
|
|
107
|
+
*/
|
|
108
|
+
function startFailureHint(why, fileFirst) {
|
|
109
|
+
if (/working directory|chdir|\bcwd\b/i.test(why)) {
|
|
110
|
+
return fileFirst
|
|
111
|
+
? '--cwd must name a directory that exists in the workspace’s files, as an absolute path under /home/user'
|
|
112
|
+
: '--cwd must name a directory that exists in the workspace, as an absolute path (commands start in /home/user without --cwd)';
|
|
113
|
+
}
|
|
114
|
+
if (/unknown user/i.test(why))
|
|
115
|
+
return undefined;
|
|
116
|
+
return 'is argv[0] installed and on PATH? shell syntax needs -- bash -lc "..."';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 402 allowance_exhausted (0.5.1): what to do per details.reason (opt-in overage with a spend cap). An API older than
|
|
120
|
+
* overage sends no reason: no hint.
|
|
121
|
+
*/
|
|
122
|
+
function allowanceHint(info) {
|
|
123
|
+
const resets = typeof info.details?.resets_at === 'string' ? ` (the period resets ${time(info.details.resets_at)})` : '';
|
|
124
|
+
switch (info.details?.reason) {
|
|
125
|
+
case 'allowance_used':
|
|
126
|
+
return `upgrade (shard billing plans), or turn on overage: shard billing overage on --cap <dollars>${resets}`;
|
|
127
|
+
case 'spend_cap_reached':
|
|
128
|
+
return `the spend cap is reached: raise it (shard billing overage cap <dollars>, at most the plan price) or upgrade${resets}`;
|
|
129
|
+
case 'overage_paused':
|
|
130
|
+
return 'a payment is past due: update the payment method (shard billing portal --open); overage resumes once the invoice is paid';
|
|
131
|
+
default:
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Dollars for a hint: `9` for 900 minor units, `9.50` for 950. */
|
|
136
|
+
const dollars = (minor) => (typeof minor === 'number' ? (minor % 100 === 0 ? String(minor / 100) : (minor / 100).toFixed(2)) : undefined);
|
|
137
|
+
/**
|
|
138
|
+
* The spend-policy refusals (0.5.1; shard billing overage): 422 validation_failed per details.reason, with the bound the
|
|
139
|
+
* API names (min_minor, max_minor, charges_minor), and a 409 version_mismatch (--if-version, here or on egress set).
|
|
140
|
+
*/
|
|
141
|
+
function spendPolicyHint(info) {
|
|
142
|
+
const d = info.details ?? {};
|
|
143
|
+
const reason = reasonOf(info);
|
|
144
|
+
if (info.code === 'conflict' && reason === 'version_mismatch') {
|
|
145
|
+
const now = typeof d.current_version === 'number' ? d.current_version : undefined;
|
|
146
|
+
return `it changed since that version${now === undefined ? '' : ` (it is at version ${now} now)`}; nothing was saved. Look at it again, then retry${now === undefined ? '' : ` with --if-version ${now}`}`;
|
|
147
|
+
}
|
|
148
|
+
if (info.code !== 'validation_failed')
|
|
149
|
+
return undefined;
|
|
150
|
+
switch (reason) {
|
|
151
|
+
case 'overage_unavailable':
|
|
152
|
+
return 'overage needs a paid plan with opt-in overage: shard billing plans, then shard billing upgrade <plan>';
|
|
153
|
+
case 'spend_cap_required':
|
|
154
|
+
return 'give a spend cap: shard billing overage on --cap <dollars> (shard billing overage shows the range)';
|
|
155
|
+
case 'spend_cap_below_minimum':
|
|
156
|
+
return `the smallest spend cap is $${dollars(d.min_minor) ?? '1'}: shard billing overage cap ${dollars(d.min_minor) ?? '1'}`;
|
|
157
|
+
case 'spend_cap_above_plan_price': {
|
|
158
|
+
const max = dollars(d.max_minor);
|
|
159
|
+
return `the spend cap is at most the plan price${max ? `, $${max}: shard billing overage cap ${max}` : ''}; a higher cap needs a larger plan (shard billing plans)`;
|
|
160
|
+
}
|
|
161
|
+
case 'spend_cap_below_charges': {
|
|
162
|
+
const charged = dollars(d.charges_minor);
|
|
163
|
+
return `overage already charged ${charged ? `$${charged}` : 'more than that'} this period: set a cap of at least that${charged ? ` (shard billing overage cap ${charged})` : ''}, or turn overage off (shard billing overage off)`;
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
96
169
|
/**
|
|
97
170
|
* 409 conflict host_feature_unavailable (contracts §26.7; 0.5.0): the workspace runs on a host agent that predates the
|
|
98
171
|
* call. Not retryable: it lasts until the workspace runs on an upgraded host (minutes to hours), so the hint names
|
|
@@ -121,7 +194,8 @@ function fileFirstHint(info) {
|
|
|
121
194
|
const d = info.details ?? {};
|
|
122
195
|
const reason = d.reason;
|
|
123
196
|
if (info.code === 'execution_failed') {
|
|
124
|
-
const
|
|
197
|
+
const startHint = reason === 'exec_failed_to_start' ? startFailureHint(info.message, true) : undefined;
|
|
198
|
+
const cause = startHint ? `the command could not start (${startHint})` : typeof reason === 'string' ? EXECUTION_CAUSES[reason] : undefined;
|
|
125
199
|
return `${cause ? `${cause}; ` : ''}nothing was published (the tree is unchanged). Running it again needs a new --execution-id (omit it for a fresh one)${info.retryable ? '; a retry may succeed' : ''}.`;
|
|
126
200
|
}
|
|
127
201
|
switch (reason) {
|
|
@@ -166,7 +240,9 @@ const FEEDBACK_EMAIL = 'shardflux@heliosone.fi';
|
|
|
166
240
|
* or step-up to finish, a wait that gave up while the work continues, a checkout nobody paid, and the refusals whose
|
|
167
241
|
* hint names the exact next step (a stale revision, an edit that does not match, a mode the workspace lacks).
|
|
168
242
|
*/
|
|
169
|
-
|
|
243
|
+
// allowance_exhausted (0.5.1): a used-up allowance, a reached spend cap or a paused overage is a billing state whose hint
|
|
244
|
+
// names the next step (upgrade, overage, the cap, the payment method), not a problem to report.
|
|
245
|
+
const NO_NUDGE_CODES = new Set(['usage', 'interrupted', 'unauthenticated', 'validation_failed', 'mfa_required', 'step_up_required', 'email_unverified', 'timeout', 'checkout_ended', 'allowance_exhausted']);
|
|
170
246
|
const NO_NUDGE_REASONS = new Set([
|
|
171
247
|
'revision_mismatch',
|
|
172
248
|
'tree_revision_mismatch',
|
|
@@ -182,6 +258,8 @@ const NO_NUDGE_REASONS = new Set([
|
|
|
182
258
|
'layout_unsupported',
|
|
183
259
|
'host_feature_unavailable',
|
|
184
260
|
'subscription_exists',
|
|
261
|
+
// A stale spend policy (If-Match): the hint names the version to retry with (0.5.1).
|
|
262
|
+
'version_mismatch',
|
|
185
263
|
]);
|
|
186
264
|
/**
|
|
187
265
|
* Whether a failure ends with the `feedback:` line (0.5.0+): every failure except the expected flow above (feedback
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"yaml": "2.9.1",
|
|
43
|
-
"@shardflux/sdk": "^0.
|
|
43
|
+
"@shardflux/sdk": "^0.10.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/js": "10.0.1",
|