@shardflux/cli 0.10.0 → 0.11.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 +40 -23
- package/README.md +58 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +34 -1
- package/dist/commands.d.ts +3 -1
- package/dist/commands.js +84 -11
- package/dist/computer-stream-cache.d.ts +8 -0
- package/dist/computer-stream-cache.js +34 -0
- package/dist/format.js +3 -0
- package/dist/main.js +3 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,29 @@
|
|
|
3
3
|
Every command and option the README shows is available from the version named here. Breaking changes ship in minor
|
|
4
4
|
releases and are marked **Breaking**.
|
|
5
5
|
|
|
6
|
-
## 0.
|
|
6
|
+
## 0.11.1 (not yet published)
|
|
7
|
+
|
|
8
|
+
- Batch reverse-tunnel writes and retry after stalled acknowledgements, keeping active transfers moving without duplicate traffic.
|
|
9
|
+
|
|
10
|
+
## 0.11.0 — 2026-10-05
|
|
11
|
+
|
|
12
|
+
- `shard ws resize --at-least` grows each named resource only (a smaller disk stays); `--no-wait` returns the held answer and its operation.
|
|
13
|
+
- Add opt-in persistent workspace idle deletion retention, workspace/project setters and projected deletion times.
|
|
14
|
+
|
|
15
|
+
- Accelerate reverse tunnels with a bounded, acknowledged binary WebSocket stream, 64 KiB frames and batched credits. Select `auto`, `pty` or `exec` transport; reconnect preserves byte offsets. Opt-in binary PTY output accepts larger blocks for file transfers.
|
|
16
|
+
|
|
17
|
+
- Add opt-in resize controls to grow named resources only and return the held server answer without polling a pending operation.
|
|
18
|
+
|
|
19
|
+
- `shard ws computer stream` reuses a private cached link by API/workspace/credential and kind with more than 60 seconds left. `--fresh` remints; `--stop` clears cached links.
|
|
20
|
+
- `SHARDFLUX_BASE_URL` aliases `SHARDFLUX_API_URL` (the latter wins). Canceled exec results exit 130; timeout, normal exit and signal statuses follow the shared POSIX status helper.
|
|
21
|
+
- Shared SDK proxy support, missing-file classification and computer batch screenshot deduplication.
|
|
22
|
+
- Embed live desktop viewers and port previews in a web app with an explicit HTTPS origin allowlist and partitioned browser sessions.
|
|
23
|
+
|
|
24
|
+
- Add foreground `shard ws tunnel <key> --remote <port> --to <host:port> [--bind 127.0.0.1|0.0.0.0]`. Ctrl-C closes the reverse tunnel and its guest process.
|
|
25
|
+
|
|
26
|
+
- Add `shard ws upgrade <key> [--at next-resume]` with operation waiting and JSON output.
|
|
27
|
+
|
|
28
|
+
## 0.10.0 — 2026-10-04
|
|
7
29
|
|
|
8
30
|
Needs `@shardflux/sdk` 0.15.0 or later.
|
|
9
31
|
|
|
@@ -30,12 +52,10 @@ Additive (contracts §46), with `@shardflux/sdk` 0.15.0.
|
|
|
30
52
|
- `shard ws computer enable|disable|inherit <key>` and `shard ws open --computer-use on|off|inherit`: the workspace's
|
|
31
53
|
computer use switch; `shard templates computer-use <slug> on|off`: the switch for every workspace of a template.
|
|
32
54
|
|
|
33
|
-
## 0.9.0 — 2026-10-04
|
|
55
|
+
## 0.9.0 — 2026-10-04
|
|
34
56
|
|
|
35
57
|
- `shard ws hint <workspace>` explicitly stages a parked workspace ahead of a VM tool call, without waiting for restore or resuming a suspended workspace.
|
|
36
58
|
|
|
37
|
-
## 0.9.0 (not yet published)
|
|
38
|
-
|
|
39
59
|
Needs `@shardflux/sdk` 0.14.0 or later.
|
|
40
60
|
|
|
41
61
|
### Sign up from an agent
|
|
@@ -115,7 +135,7 @@ workspace.
|
|
|
115
135
|
API decides (with elastic-by-default enabled for the organization a new workspace is elastic; otherwise
|
|
116
136
|
`requires_elastic`). With `--allocation-mode fixed` it stays a usage error.
|
|
117
137
|
|
|
118
|
-
## 0.8.1
|
|
138
|
+
## 0.8.1 — 2026-10-03
|
|
119
139
|
|
|
120
140
|
Needs `@shardflux/sdk` 0.13.1 or later.
|
|
121
141
|
|
|
@@ -133,7 +153,7 @@ When the machine a workspace runs on fails, the workspace is suspended, and the
|
|
|
133
153
|
`--timing` prints the SDK's `restored checkpoint <id> (host_lost; state as of <time>)`.
|
|
134
154
|
- `hostLostNotice()` is exported next to `coldBootNotice()`.
|
|
135
155
|
|
|
136
|
-
## 0.8.0
|
|
156
|
+
## 0.8.0 — 2026-10-02
|
|
137
157
|
|
|
138
158
|
Needs `@shardflux/sdk` 0.13.0 or later.
|
|
139
159
|
|
|
@@ -206,7 +226,7 @@ newest published version.
|
|
|
206
226
|
- **Breaking:** `--json` output no longer carries `update_policy`: the API removed the update policy (it was always
|
|
207
227
|
`pinned`).
|
|
208
228
|
|
|
209
|
-
## 0.6.2
|
|
229
|
+
## 0.6.2 — 2026-09-30
|
|
210
230
|
|
|
211
231
|
Needs `@shardflux/sdk` 0.11.1 or later.
|
|
212
232
|
|
|
@@ -241,14 +261,12 @@ New organizations can start on a trial: 60 days at Developer limits, no card. Th
|
|
|
241
261
|
- An interrupted file-first `ws exec` says `(an execution runs to completion)`; `mode_not_available` drops "yet";
|
|
242
262
|
`--open` no longer says "best effort"; `templates init --help` no longer names an internal document.
|
|
243
263
|
|
|
244
|
-
## 0.7.0
|
|
264
|
+
## 0.7.0 — 2026-09-30
|
|
245
265
|
|
|
246
266
|
Completed exec output streams are drained before releasing their HTTP connections, with a bounded cleanup if a peer does not close.
|
|
247
267
|
|
|
248
268
|
Uses SDK 0.12.0 and its pooled transport. Adds --labels/--idle-policy to open, --labels to list, ws labels, ws idle, ws exec-start --stdin-open and ws exec-input --offset. Protocol-error JSON includes source. Resume recovers failed workspaces; completed deletion permits key reuse.
|
|
249
269
|
|
|
250
|
-
Requires the QM integration backend release for labels, failed recovery, key reuse and pipe stdin. No production deployment has occurred from this branch.
|
|
251
|
-
|
|
252
270
|
### Instant suspend: durable storage
|
|
253
271
|
|
|
254
272
|
- `shard ws suspend --wait` still returns when the suspend succeeds (sealed on its host); a finished suspend or fork
|
|
@@ -264,7 +282,7 @@ Requires the QM integration backend release for labels, failed recovery, key reu
|
|
|
264
282
|
- A suspend-when-idle canceled `workspace_active` says the workspace was in use and keeps running.
|
|
265
283
|
- The `ws suspend` summary no longer says "durable checkpoint".
|
|
266
284
|
|
|
267
|
-
## 0.6.1
|
|
285
|
+
## 0.6.1 — 2026-09-30
|
|
268
286
|
|
|
269
287
|
Needs `@shardflux/sdk` 0.11.0 or later.
|
|
270
288
|
|
|
@@ -276,7 +294,7 @@ flush grace after its last work), and a running command, an attached stream or a
|
|
|
276
294
|
it ends. The human output says `is suspended as soon as it is idle`, and `ws get` shows `as soon as idle`. Out of
|
|
277
295
|
range is still exit 2 before any request: `--when-idle must be between 0s and 1h`.
|
|
278
296
|
|
|
279
|
-
## 0.6.0
|
|
297
|
+
## 0.6.0 — 2026-09-29
|
|
280
298
|
|
|
281
299
|
Needs `@shardflux/sdk` 0.11.0 or later.
|
|
282
300
|
|
|
@@ -296,17 +314,17 @@ suspend; every process was restarted.
|
|
|
296
314
|
processes restarted (runtime_changed)` (the SDK's `formatTiming()`).
|
|
297
315
|
- `coldBootNotice()` is exported next to `TimingRecorder`, which records the call as `coldBoot`.
|
|
298
316
|
|
|
299
|
-
## 0.5.3
|
|
317
|
+
## 0.5.3 — 2026-09-29
|
|
300
318
|
|
|
301
319
|
Fix: `shard --version`, the User-Agent and the update check reports 0.5.3. 0.5.2 was published reporting 0.5.1, its previous version; the build now
|
|
302
320
|
fails when `CLI_VERSION` differs from package.json.
|
|
303
321
|
|
|
304
|
-
## 0.5.2
|
|
322
|
+
## 0.5.2 — 2026-09-29
|
|
305
323
|
|
|
306
324
|
Public support: package metadata and the README now link to the [shared bug tracker](https://github.com/shardfluxdev/community/issues),
|
|
307
325
|
feature requests, and private support/security reporting.
|
|
308
326
|
|
|
309
|
-
## 0.5.1
|
|
327
|
+
## 0.5.1 — 2026-09-29
|
|
310
328
|
|
|
311
329
|
### A command that could not start is never a silent exit
|
|
312
330
|
|
|
@@ -356,7 +374,7 @@ cell had ended the session `failed_to_start` (`working directory "app" is not a
|
|
|
356
374
|
`--cancel-when-idle` cancels it. `--json` prints `{workspace, operation, suspend_request}`.
|
|
357
375
|
- `shard workspaces get` shows a pending request (`suspend when idle after N s idle, not before ...`).
|
|
358
376
|
|
|
359
|
-
## 0.5.0
|
|
377
|
+
## 0.5.0 — 2026-09-29
|
|
360
378
|
|
|
361
379
|
Needs `@shardflux/sdk` 0.9.0 or later.
|
|
362
380
|
|
|
@@ -529,13 +547,12 @@ unchanged and still stores nothing.
|
|
|
529
547
|
edit that does not match, a mode the workspace lacks, a reused execution id), and `shard feedback` itself.
|
|
530
548
|
- The positional usage of `shard feedback` is shown as `<message...>`; `--agent-label` also names the reporting agent.
|
|
531
549
|
|
|
532
|
-
|
|
533
|
-
## 0.4.1 (2026-09-28)
|
|
550
|
+
## 0.4.1 — 2026-09-28
|
|
534
551
|
|
|
535
552
|
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
|
|
536
553
|
exports type-check without casts), and the CLI moves to it so `shardflux` installs one copy of the SDK.
|
|
537
554
|
|
|
538
|
-
## 0.4.0
|
|
555
|
+
## 0.4.0 — 2026-09-28
|
|
539
556
|
|
|
540
557
|
Needs `@shardflux/sdk` 0.7.0 or later. New dependency: `yaml` (template.yaml).
|
|
541
558
|
|
|
@@ -563,7 +580,7 @@ Needs `@shardflux/sdk` 0.7.0 or later. New dependency: `yaml` (template.yaml).
|
|
|
563
580
|
- `buildText` names a recipe v2 build's recipe sha256, its build network and denied hosts, and points at
|
|
564
581
|
`shard templates builds get`.
|
|
565
582
|
|
|
566
|
-
## 0.3.1
|
|
583
|
+
## 0.3.1 — 2026-09-28
|
|
567
584
|
|
|
568
585
|
Needs `@shardflux/sdk` 0.6.2 or later.
|
|
569
586
|
|
|
@@ -580,7 +597,7 @@ stays suspended).
|
|
|
580
597
|
- `shard workspaces get` names the origin of a template version test instance (`template <id> version <n>`, contracts
|
|
581
598
|
§24.6) instead of reading it as a draft state.
|
|
582
599
|
|
|
583
|
-
## 0.3.0
|
|
600
|
+
## 0.3.0 — 2026-09-28
|
|
584
601
|
|
|
585
602
|
Needs `@shardflux/sdk` 0.6.0 or later.
|
|
586
603
|
|
|
@@ -609,13 +626,13 @@ Needs `@shardflux/sdk` 0.6.0 or later.
|
|
|
609
626
|
- Ctrl-C ends a held open at once (exit 130); the open continues server side, and opening the key again keeps
|
|
610
627
|
waiting.
|
|
611
628
|
|
|
612
|
-
## 0.2.0
|
|
629
|
+
## 0.2.0 — 2026-09-28
|
|
613
630
|
|
|
614
631
|
Secrets (`secrets ...`, `workspaces open --secret`, `workspaces secrets`), session workspaces (`--lifetime session`,
|
|
615
632
|
`workspaces close`), layered workspaces (`workspaces reset|save-as-template|changes`), templates
|
|
616
633
|
(`templates list|files|diff|draft ...`), and waking suspended workspaces on use (`--no-wake`, `--wake-timeout`).
|
|
617
634
|
|
|
618
|
-
## 0.1.0
|
|
635
|
+
## 0.1.0 — 2026-09-26
|
|
619
636
|
|
|
620
637
|
First release: `login`, `whoami`, `usage`, `workspaces open|list|get|exec|suspend|resume|fork|delete|sessions`,
|
|
621
638
|
`files read|write|ls`, `operations list|get|wait`, `--json`, and documented exit codes.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @shardflux/cli: `shard`
|
|
2
2
|
|
|
3
|
-
The command line for [Shardflux](https://shardflux.dev):
|
|
3
|
+
The command line for [Shardflux](https://shardflux.dev): serverless VMs for AI agents. Open a
|
|
4
4
|
persistent workspace by key, run commands in it, move files in and out, and suspend, resume, fork
|
|
5
5
|
or delete it. From 0.5.0 it also does everything the web app does: create an account, sign in
|
|
6
6
|
(with two-factor authentication), set up an organization, project and API key, manage members,
|
|
@@ -24,6 +24,22 @@ For usage, account, billing, or private support, email [shardflux@heliosone.fi](
|
|
|
24
24
|
Report vulnerabilities through [private security reporting](https://github.com/shardfluxdev/community/security/advisories/new).
|
|
25
25
|
See the [support guide](https://github.com/shardfluxdev/community/blob/main/SUPPORT.md) for all reporting options.
|
|
26
26
|
|
|
27
|
+
## Reverse tunnels (0.11.0+)
|
|
28
|
+
|
|
29
|
+
Run a foreground TCP tunnel from a workspace to a service on this machine:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
shard ws tunnel customer-42/main --remote 8081 --to localhost:8081
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Commands in the workspace can then call `http://127.0.0.1:8081`. The target is dialed on the machine running
|
|
36
|
+
`shard`. Ctrl-C closes the sockets and guest process. `--bind 0.0.0.0` selects all guest interfaces; loopback is the
|
|
37
|
+
default. An open tunnel keeps the workspace awake, and persistent workspaces preserve it across explicit suspend/resume.
|
|
38
|
+
|
|
39
|
+
The tunnel selects a streaming transport automatically when `pty` permission is granted. Choose `transport: 'pty'` or `transport: 'exec'` in the SDK, or `--transport pty|exec` in the CLI, to select it explicitly. The handle reports `transport`.
|
|
40
|
+
|
|
41
|
+
Bulk transfers batch writes automatically. Active transfers retain their byte position through network reconnection.
|
|
42
|
+
|
|
27
43
|
## Install
|
|
28
44
|
|
|
29
45
|
```sh
|
|
@@ -1022,3 +1038,44 @@ Use the byte offset in each acknowledgement, including partial writes. `ws resum
|
|
|
1022
1038
|
### Prepare an imminent command (0.9.0+)
|
|
1023
1039
|
|
|
1024
1040
|
Call `shard ws hint <workspace>` ahead of a tool call to stage a parked workspace while the input is prepared. It returns without waiting for restore. Resume a suspended workspace first. The hint is explicit; ordinary commands add no early hint.
|
|
1041
|
+
|
|
1042
|
+
Resize controls (0.11.0+): `shard ws resize <workspace> --memory 6144 --disk 2 --at-least --no-wait` grows only
|
|
1043
|
+
resources that need more capacity and returns the held server answer with the operation. The normal command waits
|
|
1044
|
+
for completion. Fixed memory without `--memory-mib` uses the template default.
|
|
1045
|
+
|
|
1046
|
+
## New in 0.11.0
|
|
1047
|
+
|
|
1048
|
+
`shard ws computer stream <workspace>` reuses a private viewer link with more than 60 seconds left across CLI invocations. `--fresh` mints a new link, and `--stop` clears it. Canceled exec commands exit 130, timeouts exit 124, and signal exits use 128 + signal. `SHARDFLUX_BASE_URL` aliases `SHARDFLUX_API_URL`; the latter and `--api-url` take precedence. The SDK transport honors environment HTTP(S)_PROXY and NO_PROXY.
|
|
1049
|
+
|
|
1050
|
+
## Embed a live viewer (0.11.0+)
|
|
1051
|
+
|
|
1052
|
+
Watch the desktop or a port preview inside your web app. Choose 1–10 distinct HTTPS origins, then use the returned URL as the iframe source.
|
|
1053
|
+
|
|
1054
|
+
```bash
|
|
1055
|
+
shard ws computer stream acme/demo --embed-origin https://app.example.com
|
|
1056
|
+
shard ws port link acme/demo 3000 --embed-origin https://app.example.com --embed-origin https://admin.example.com
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
```html
|
|
1060
|
+
<iframe src="RETURNED_LINK_URL" title="Watch the agent" style="width:100%;height:600px;border:0"></iframe>
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
The browser session is partitioned by the top-level site. Each origin is exactly a scheme and host with an optional port, such as `https://app.example.com:8443`. The viewer permits framing by those origins; interactive viewing uses the same allowlist.
|
|
1064
|
+
|
|
1065
|
+
## Idle deletion retention (0.11.0+)
|
|
1066
|
+
|
|
1067
|
+
```sh
|
|
1068
|
+
shard ws open chat/42 --template python-node-browser --delete-after-idle 14d
|
|
1069
|
+
shard ws retention chat/42 30d
|
|
1070
|
+
shard projects retention PROJECT_ID 14d --labels '{"app":"chat"}'
|
|
1071
|
+
shard projects retention PROJECT_ID
|
|
1072
|
+
shard ws retention chat/42 off
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
Use 1d through 3650d with persistent workspaces. `off` removes the workspace's own policy and follows its
|
|
1076
|
+
project default; `shard projects retention PROJECT_ID off` clears that default. All selector labels match
|
|
1077
|
+
exactly, and a workspace's own setting wins. With no policy, workspaces stay until you delete them.
|
|
1078
|
+
|
|
1079
|
+
## Workspace upgrade (0.11.0+)
|
|
1080
|
+
|
|
1081
|
+
`shard ws upgrade <key>` keeps files and installed package files through one cold start. Memory and processes end. Use `--at next-resume` to schedule it without stopping the VM; `--json` returns the operation and its applied/scheduled result.
|
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.
|
|
15
|
+
export const CLI_VERSION = '0.11.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).' },
|
|
@@ -200,6 +200,17 @@ const KEY_COMMANDS = [
|
|
|
200
200
|
},
|
|
201
201
|
examples: ['shard usage', 'shard usage --estimate', 'shard usage --series --granularity day', 'shard usage --workspace acme/demo --meter cpu_seconds'],
|
|
202
202
|
},
|
|
203
|
+
{
|
|
204
|
+
path: ['workspaces', 'retention'], summary: 'Set or clear persistent workspace idle deletion',
|
|
205
|
+
positionals: [{ name: 'key', required: true }, { name: 'policy', required: true }], options: {},
|
|
206
|
+
examples: ['shard ws retention acme/thread-42 14d', 'shard ws retention acme/thread-42 off'],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
path: ['projects', 'retention'], summary: 'Read or set the project default idle deletion policy (project key)',
|
|
210
|
+
positionals: [{ name: 'project-id', required: true }, { name: 'policy', required: false }],
|
|
211
|
+
options: { labels: { type: 'string', value: '<json>', description: 'Exact-match label selectors.' } },
|
|
212
|
+
examples: ["shard projects retention <project-id> 14d --labels '{\"app\":\"chat\"}'"],
|
|
213
|
+
},
|
|
203
214
|
{
|
|
204
215
|
path: ['workspaces', 'open'],
|
|
205
216
|
summary: 'Open a workspace by key (create on first use, reconnect or resume afterwards)',
|
|
@@ -208,6 +219,7 @@ const KEY_COMMANDS = [
|
|
|
208
219
|
options: {
|
|
209
220
|
template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
|
|
210
221
|
labels: { type: 'string', value: '<json>', description: 'Replace workspace labels with a JSON object of strings.' },
|
|
222
|
+
'delete-after-idle': { type: 'string', value: '<days>d', description: 'Delete this persistent workspace after 1..3650 idle days (e.g. 14d).' },
|
|
211
223
|
'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
|
|
212
224
|
secret: {
|
|
213
225
|
type: 'string',
|
|
@@ -294,6 +306,17 @@ const KEY_COMMANDS = [
|
|
|
294
306
|
},
|
|
295
307
|
examples: ['shard ws exec-start acme/demo --session-id build-1 --timeout 2h -- bash -lc "make -C /home/user/app"', 'shard ws exec-read acme/demo build-1 --follow'],
|
|
296
308
|
},
|
|
309
|
+
{
|
|
310
|
+
path: ['workspaces', 'tunnel'], summary: 'Reach this machine from a workspace TCP port',
|
|
311
|
+
description: 'Runs in the foreground. The guest listener defaults to loopback; --to is dialed on this machine. Ctrl-C closes the tunnel. An open tunnel keeps the workspace awake.',
|
|
312
|
+
positionals: [REF], options: {
|
|
313
|
+
transport: { type: 'string', value: '<auto|pty|exec>', description: 'Tunnel transport (default auto).' },
|
|
314
|
+
remote: { type: 'string', value: '<port>', description: 'Required guest listener port (1..65535).' },
|
|
315
|
+
to: { type: 'string', value: '<host:port>', description: 'Required TCP target on this machine (host:port or [IPv6]:port).' },
|
|
316
|
+
bind: { type: 'string', value: '<127.0.0.1|0.0.0.0>', description: 'Guest bind address (default 127.0.0.1).' },
|
|
317
|
+
},
|
|
318
|
+
examples: ['shard ws tunnel acme/demo --remote 8081 --to localhost:8081'],
|
|
319
|
+
},
|
|
297
320
|
{
|
|
298
321
|
path: ['workspaces', 'exec-read'],
|
|
299
322
|
summary: 'Print a background exec session’s output and state; --follow streams it until the command exits',
|
|
@@ -387,6 +410,11 @@ const KEY_COMMANDS = [
|
|
|
387
410
|
positionals: [REF, { name: 'new-key', required: true }],
|
|
388
411
|
options: { ...CAPS, ...MEMORY_ALLOCATION, ...WAIT },
|
|
389
412
|
},
|
|
413
|
+
{
|
|
414
|
+
path: ['workspaces', 'upgrade'], summary: 'Upgrade in place, keeping files and installed packages',
|
|
415
|
+
description: 'One cold start drops memory and processes. --at next-resume schedules it without stopping the VM.',
|
|
416
|
+
positionals: [REF], options: { ...WAIT, at: { type: 'string', value: '<now|next-resume>', description: 'When to cold start (default now).' } },
|
|
417
|
+
},
|
|
390
418
|
{
|
|
391
419
|
path: ['workspaces', 'resize'],
|
|
392
420
|
summary: 'Change a workspace’s memory, CPU or disk, running or suspended, without a restart (0.9.0+)',
|
|
@@ -399,6 +427,8 @@ const KEY_COMMANDS = [
|
|
|
399
427
|
elastic: { type: 'boolean', description: 'Switch to elastic memory: the workspace holds what its commands use and grows up to the memory size.' },
|
|
400
428
|
cpu: { type: 'string', value: '<millis>', description: 'CPU in millicores (1000 = one vCPU).' },
|
|
401
429
|
disk: { type: 'string', value: '<gib>', description: 'Disk in GiB (disks grow only).' },
|
|
430
|
+
'at-least': { type: 'boolean', description: 'Only grow each named resource; smaller or equal values stay unchanged.' },
|
|
431
|
+
wait: { type: 'boolean', description: 'Wait for the operation (default); --no-wait returns the held server answer.' },
|
|
402
432
|
timeout: { type: 'string', value: '<duration>', description: 'Give up waiting after this long (default 5m); the resize continues server side.' },
|
|
403
433
|
...TIMING,
|
|
404
434
|
},
|
|
@@ -493,6 +523,7 @@ const KEY_COMMANDS = [
|
|
|
493
523
|
description: 'Opening the link starts a browser session for the port (until it expires) and lands on --path. Anyone with the link can open the port until then: share it like a password. --json prints {url, expires_at}.',
|
|
494
524
|
positionals: [REF, PORT],
|
|
495
525
|
options: {
|
|
526
|
+
'embed-origin': { type: 'string', multiple: true, value: '<https-origin>', description: 'Allow this HTTPS origin to embed the viewer (repeatable, 1–10 distinct origins).' },
|
|
496
527
|
ttl: { type: 'string', value: '<duration>', description: 'Lifetime of the link and its browser session, 1m to 7d (default 24h).' },
|
|
497
528
|
path: { type: 'string', value: '<path>', description: 'Where the link lands: a path starting with one / (query allowed; default /).' },
|
|
498
529
|
},
|
|
@@ -660,7 +691,9 @@ const KEY_COMMANDS = [
|
|
|
660
691
|
description: 'Starts the viewer in the workspace, exposes its port and signs a link to it (inbound ports). View only unless --interactive. Anyone with the link can open it until it expires: share it like a password. --stop ends every view instead. --json prints {url, expires_at, port, interactive}.',
|
|
661
692
|
positionals: [REF],
|
|
662
693
|
options: {
|
|
694
|
+
fresh: { type: 'boolean', description: 'Mint a new viewer link instead of reusing a link with more than 60 seconds left.' },
|
|
663
695
|
interactive: { type: 'boolean', description: 'Let the viewer use the mouse and keyboard.' },
|
|
696
|
+
'embed-origin': { type: 'string', multiple: true, value: '<https-origin>', description: 'Allow this HTTPS origin to embed the viewer (repeatable, 1–10 distinct origins).' },
|
|
664
697
|
ttl: { type: 'string', value: '<duration>', description: 'Lifetime of the link, 1m to 7d (default 24h).' },
|
|
665
698
|
stop: { type: 'boolean', description: 'End every view and close the viewer ports.' },
|
|
666
699
|
},
|
package/dist/commands.d.ts
CHANGED
|
@@ -99,7 +99,9 @@ export declare function principal(ctx: Ctx): Promise<{
|
|
|
99
99
|
tool_permissions: ("git" | "exec" | "files" | "pty" | "process" | "browser" | "computer")[];
|
|
100
100
|
}>;
|
|
101
101
|
/** The exit code `shard exec` returns for a finished command. */
|
|
102
|
-
export declare function execExitCode(r: Pick<RunResult, 'exitCode' | 'termSignal' | 'timedOut'>
|
|
102
|
+
export declare function execExitCode(r: Pick<RunResult, 'exitCode' | 'termSignal' | 'timedOut'> & {
|
|
103
|
+
canceled?: boolean;
|
|
104
|
+
}): number;
|
|
103
105
|
/**
|
|
104
106
|
* A lifecycle call a file-first workspace does not have (suspend, resume, fork, reset,
|
|
105
107
|
* save-as-template), refused before any request as the SDK's workspace handle does: NotSupportedForModeError, printed
|
package/dist/commands.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* for yet (GET /v1/operations/{id}, usage summary). The account plane (0.5.0) is in account-commands.ts; handlers.ts
|
|
6
6
|
* joins both tables.
|
|
7
7
|
*/
|
|
8
|
+
import { computerStreamCache } from "./computer-stream-cache.js";
|
|
8
9
|
import { randomUUID } from 'node:crypto';
|
|
9
10
|
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
10
11
|
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
11
|
-
import { EXECUTION_ID, ExecStartError, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, ShardfluxProtocolError, newExecutionId } from '@shardflux/sdk';
|
|
12
|
+
import { EXECUTION_ID, ExecStartError, FEEDBACK_CATEGORIES, FEEDBACK_MESSAGE_MAX_LENGTH, NotSupportedForModeError, SDK_VERSION, ShardfluxApiError, ShardfluxProtocolError, newExecutionId, exitCodePosix } from '@shardflux/sdk';
|
|
12
13
|
import { parse as parseYaml } from 'yaml';
|
|
13
14
|
import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePositiveInt } from "./args.js";
|
|
14
15
|
import { ExecSessionRunningError, ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
|
|
@@ -94,6 +95,10 @@ function resizeParams(values) {
|
|
|
94
95
|
p.diskGib = parsePositiveInt(disk, 'disk', 1_000_000);
|
|
95
96
|
if (Object.keys(p).length === 0)
|
|
96
97
|
throw new UsageError('nothing to resize: give --memory, --held, --fixed, --elastic, --cpu or --disk');
|
|
98
|
+
if (values['at-least'] === true)
|
|
99
|
+
p.atLeast = true;
|
|
100
|
+
if (values.wait === false)
|
|
101
|
+
p.wait = false;
|
|
97
102
|
return p;
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
@@ -493,13 +498,7 @@ function feedbackContext(ctx, values) {
|
|
|
493
498
|
}
|
|
494
499
|
/** The exit code `shard exec` returns for a finished command. */
|
|
495
500
|
export function execExitCode(r) {
|
|
496
|
-
|
|
497
|
-
return 124;
|
|
498
|
-
if (typeof r.exitCode === 'number' && r.exitCode >= 0)
|
|
499
|
-
return Math.min(r.exitCode, 255);
|
|
500
|
-
if (typeof r.termSignal === 'number' && r.termSignal > 0)
|
|
501
|
-
return 128 + r.termSignal;
|
|
502
|
-
return 1;
|
|
501
|
+
return Math.min(exitCodePosix(r), 255);
|
|
503
502
|
}
|
|
504
503
|
/** The longest cancel grace the workspace applies (it waits 5 s without one). */
|
|
505
504
|
const MAX_CANCEL_GRACE_MS = 60_000;
|
|
@@ -860,6 +859,35 @@ export const KEY_HANDLERS = {
|
|
|
860
859
|
ctx.io.stderr.write(`read it with: shard ws exec-read ${ref} ${session.session_id} --follow; stop it with: shard ws exec-cancel ${ref} ${session.session_id}\n`);
|
|
861
860
|
return 0;
|
|
862
861
|
},
|
|
862
|
+
async 'workspaces tunnel'(ctx, values, [ref]) {
|
|
863
|
+
const remote = str(values.remote);
|
|
864
|
+
const target = str(values.to);
|
|
865
|
+
if (!remote || !target)
|
|
866
|
+
throw new UsageError('tunnel requires --remote <port> and --to <host:port>');
|
|
867
|
+
const remotePort = parsePositiveInt(remote, 'remote');
|
|
868
|
+
if (remotePort > 65535)
|
|
869
|
+
throw new UsageError('--remote must be 1..65535');
|
|
870
|
+
const bindAddress = oneOf(values, 'bind', ['127.0.0.1', '0.0.0.0']);
|
|
871
|
+
const transport = oneOf(values, 'transport', ['auto', 'pty', 'exec']);
|
|
872
|
+
const terminated = new AbortController();
|
|
873
|
+
const onTerm = () => terminated.abort(new Error('terminated'));
|
|
874
|
+
process.once('SIGTERM', onTerm);
|
|
875
|
+
try {
|
|
876
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
877
|
+
const tunnel = await ws.tunnels.reverse({ remotePort, target, ...(bindAddress ? { bindAddress } : {}), ...(transport ? { transport } : {}), signal: AbortSignal.any([ctx.signal, terminated.signal]) });
|
|
878
|
+
out(ctx, `${bindAddress ?? '127.0.0.1'}:${remotePort} in ${ws.key} → ${target} on this machine; Ctrl-C closes the tunnel\n`, { remote_port: remotePort, target, session_id: tunnel.sessionId, transport: tunnel.transport });
|
|
879
|
+
try {
|
|
880
|
+
await tunnel.closed;
|
|
881
|
+
}
|
|
882
|
+
finally {
|
|
883
|
+
await tunnel.close();
|
|
884
|
+
}
|
|
885
|
+
return terminated.signal.aborted ? 143 : ctx.signal.aborted ? 130 : 0;
|
|
886
|
+
}
|
|
887
|
+
finally {
|
|
888
|
+
process.removeListener('SIGTERM', onTerm);
|
|
889
|
+
}
|
|
890
|
+
},
|
|
863
891
|
async 'workspaces exec-read'(ctx, values, [ref, sessionId]) {
|
|
864
892
|
const ws = await resolveWorkspace(ctx, ref);
|
|
865
893
|
const cell = cellOf(ctx, ws);
|
|
@@ -904,7 +932,7 @@ export const KEY_HANDLERS = {
|
|
|
904
932
|
if (s.state === 'starting' || s.state === 'running') {
|
|
905
933
|
throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
|
|
906
934
|
}
|
|
907
|
-
const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false };
|
|
935
|
+
const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false, canceled: s.canceled ?? false };
|
|
908
936
|
if (!ctx.json && result.exitCode === null && !result.timedOut && !(result.termSignal !== null && result.termSignal > 0)) {
|
|
909
937
|
ctx.io.stderr.write(`exec session ${s.session_id} ended ${s.state} without an exit code${s.error ? `: ${s.error}` : ''}\n`);
|
|
910
938
|
}
|
|
@@ -931,6 +959,22 @@ export const KEY_HANDLERS = {
|
|
|
931
959
|
out(ctx, JSON.stringify(ack) + '\n', ack);
|
|
932
960
|
return 0;
|
|
933
961
|
},
|
|
962
|
+
async 'workspaces retention'(ctx, _values, [ref, policy]) {
|
|
963
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
964
|
+
await ws.setRetention(retentionDays(policy));
|
|
965
|
+
out(ctx, `${JSON.stringify(ws.retention)}\n`, ws.data);
|
|
966
|
+
return 0;
|
|
967
|
+
},
|
|
968
|
+
async 'projects retention'(ctx, values, [projectId, policy]) {
|
|
969
|
+
const api = ctx.cloud().projects;
|
|
970
|
+
if (policy === 'off' && values.labels !== undefined)
|
|
971
|
+
throw new UsageError('--labels cannot be combined with off');
|
|
972
|
+
if (policy === undefined && values.labels !== undefined)
|
|
973
|
+
throw new UsageError('--labels requires a retention policy');
|
|
974
|
+
const retention = policy === undefined ? await api.getRetention(projectId) : await api.setRetention(projectId, policy === 'off' ? null : { ...openRetentionDays(policy), ...(str(values.labels) === undefined ? {} : { labels: labelMap(str(values.labels)) }) });
|
|
975
|
+
out(ctx, `${JSON.stringify(retention)}\n`, retention);
|
|
976
|
+
return 0;
|
|
977
|
+
},
|
|
934
978
|
async 'workspaces open'(ctx, values, [key]) {
|
|
935
979
|
const template = str(values.template);
|
|
936
980
|
if (!template)
|
|
@@ -954,6 +998,7 @@ export const KEY_HANDLERS = {
|
|
|
954
998
|
key: key,
|
|
955
999
|
template,
|
|
956
1000
|
...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
|
|
1001
|
+
...(str(values['delete-after-idle']) !== undefined ? { retention: openRetentionDays(str(values['delete-after-idle'])) } : {}),
|
|
957
1002
|
...(str(values['idle-policy']) ? { idlePolicy: str(values['idle-policy']) } : {}),
|
|
958
1003
|
...(c ? { caps: c } : {}),
|
|
959
1004
|
...(bind.length ? { secrets: bind } : {}),
|
|
@@ -1301,6 +1346,12 @@ export const KEY_HANDLERS = {
|
|
|
1301
1346
|
async 'workspaces close'(ctx, values, [ref]) {
|
|
1302
1347
|
return lifecycle(ctx, values, ref, 'Closing', (ws, o) => ctx.cloud().workspaces.close(ws.id, o));
|
|
1303
1348
|
},
|
|
1349
|
+
async 'workspaces upgrade'(ctx, values, [ref]) {
|
|
1350
|
+
const at = str(values.at) ?? 'now';
|
|
1351
|
+
if (at !== 'now' && at !== 'next-resume')
|
|
1352
|
+
throw new UsageError('--at must be now or next-resume');
|
|
1353
|
+
return lifecycle(ctx, values, ref, at === 'now' ? 'Upgrading' : 'Scheduling upgrade for', (ws, o) => ctx.cloud().workspaces.upgrade(ws.id, { ...o, at: at === 'next-resume' ? 'next_resume' : 'now' }), ['upgrade', 'Upgrade']);
|
|
1354
|
+
},
|
|
1304
1355
|
async 'workspaces reset'(ctx, values, [ref]) {
|
|
1305
1356
|
if (values.yes !== true)
|
|
1306
1357
|
throw new UsageError('refusing to reset without --yes (reset wipes every change in the workspace and restarts it on its template)');
|
|
@@ -1860,7 +1911,7 @@ export const KEY_HANDLERS = {
|
|
|
1860
1911
|
if (path !== undefined && !path.startsWith('/'))
|
|
1861
1912
|
throw new UsageError(`--path must start with / (got "${path}")`);
|
|
1862
1913
|
const ws = await portWorkspace(ctx, ref);
|
|
1863
|
-
const l = await ws.ports.link(port, { ...(ttl !== undefined ? { ttlSeconds: ttl } : {}), ...(path !== undefined ? { path } : {}) });
|
|
1914
|
+
const l = await ws.ports.link(port, { ...(values['embed-origin'] !== undefined ? { embed: { origins: values['embed-origin'] } } : {}), ...(ttl !== undefined ? { ttlSeconds: ttl } : {}), ...(path !== undefined ? { path } : {}) });
|
|
1864
1915
|
out(ctx, `${l.url}\n`, { url: l.url, expires_at: l.expiresAt });
|
|
1865
1916
|
if (!ctx.json)
|
|
1866
1917
|
ctx.io.stderr.write(`Opens port ${port} of ${ws.key} in a browser until ${time(l.expiresAt)}; anyone with the link can open it until then.\n`);
|
|
@@ -2004,13 +2055,20 @@ export const KEY_HANDLERS = {
|
|
|
2004
2055
|
},
|
|
2005
2056
|
async 'workspaces computer stream'(ctx, values, [ref]) {
|
|
2006
2057
|
const ws = await computerWorkspace(ctx, ref);
|
|
2058
|
+
const credential = ctx.io.env.SHARDFLUX_API_KEY?.trim() ?? ctx.store.profile(ctx.apiUrl).api_key?.token ?? '';
|
|
2059
|
+
const cache = computerStreamCache(ctx.io.env, ctx.apiUrl, ws.id, credential);
|
|
2007
2060
|
if (values.stop === true) {
|
|
2061
|
+
cache.clear();
|
|
2008
2062
|
await ws.computer.stopStream();
|
|
2009
2063
|
out(ctx, `Ended every view of the desktop of ${ws.key}.\n`, { workspace_id: ws.id, stopped: true });
|
|
2010
2064
|
return 0;
|
|
2011
2065
|
}
|
|
2012
2066
|
const ttl = ttlSeconds(values, PORT_LINK_TTL);
|
|
2013
|
-
const
|
|
2067
|
+
const streamOptions = { fresh: values.fresh === true, interactive: values.interactive === true,
|
|
2068
|
+
...(values['embed-origin'] !== undefined ? { embed: { origins: values['embed-origin'] } } : {}),
|
|
2069
|
+
...(ttl !== undefined ? { ttlSeconds: ttl } : {}) };
|
|
2070
|
+
const s = (values.fresh !== true ? cache.get(values.interactive === true, streamOptions) : undefined) ?? await ws.computer.stream(streamOptions);
|
|
2071
|
+
cache.set(s, streamOptions);
|
|
2014
2072
|
out(ctx, `${s.url}\n`, { url: s.url, expires_at: s.expiresAt, port: s.port, interactive: s.interactive });
|
|
2015
2073
|
if (!ctx.json) {
|
|
2016
2074
|
ctx.io.stderr.write(`${s.interactive ? 'Uses' : 'Watches'} the desktop of ${ws.key} until ${time(s.expiresAt)}; anyone with the link can open it until then.\n`);
|
|
@@ -2334,3 +2392,18 @@ function labelMap(value) {
|
|
|
2334
2392
|
throw new UsageError('labels must be a JSON object of strings');
|
|
2335
2393
|
return parsed;
|
|
2336
2394
|
}
|
|
2395
|
+
function retentionDays(value) {
|
|
2396
|
+
if (value === 'off')
|
|
2397
|
+
return null;
|
|
2398
|
+
const match = /^([0-9]+)d$/.exec(value);
|
|
2399
|
+
const days = match ? Number(match[1]) : NaN;
|
|
2400
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650)
|
|
2401
|
+
throw new UsageError('retention must be 1d..3650d or off');
|
|
2402
|
+
return { delete_after_idle_days: days };
|
|
2403
|
+
}
|
|
2404
|
+
function openRetentionDays(value) {
|
|
2405
|
+
const policy = retentionDays(value);
|
|
2406
|
+
if (policy === null)
|
|
2407
|
+
throw new UsageError('--delete-after-idle requires 1d..3650d; clear an own policy with shard ws retention KEY off');
|
|
2408
|
+
return policy;
|
|
2409
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComputerStream, ComputerStreamOptions } from '@shardflux/sdk';
|
|
2
|
+
type StreamOptions = Pick<ComputerStreamOptions, 'embed' | 'ttlSeconds'>;
|
|
3
|
+
export declare function computerStreamCache(env: Record<string, string | undefined>, apiUrl: string, workspaceId: string, credential: string): {
|
|
4
|
+
get(interactive: boolean, options?: StreamOptions): ComputerStream | undefined;
|
|
5
|
+
set(link: ComputerStream, options?: StreamOptions): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
};
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Viewer links survive CLI invocations in the private config directory, scoped to API/workspace/credential. */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { configDir } from "./credentials.js";
|
|
6
|
+
export function computerStreamCache(env, apiUrl, workspaceId, credential) {
|
|
7
|
+
const digest = (text) => createHash('sha256').update(text).digest('hex');
|
|
8
|
+
const dir = join(configDir(env), 'computer-streams', digest(`${apiUrl}\0${workspaceId}`));
|
|
9
|
+
const path = (interactive, options = {}) => join(dir, `${digest(credential)}-${interactive ? 'interactive' : 'view'}-${digest(JSON.stringify({ ttlSeconds: options.ttlSeconds, embed: options.embed }))}.json`);
|
|
10
|
+
return {
|
|
11
|
+
get(interactive, options = {}) {
|
|
12
|
+
try {
|
|
13
|
+
const link = JSON.parse(readFileSync(path(interactive, options), 'utf8'));
|
|
14
|
+
if (typeof link.url === 'string' && typeof link.expiresAt === 'string' && Number.isInteger(link.port) && link.interactive === interactive && Date.parse(link.expiresAt) - Date.now() > 60_000)
|
|
15
|
+
return link;
|
|
16
|
+
}
|
|
17
|
+
catch { /* cache misses are ordinary; mint through the SDK */ }
|
|
18
|
+
return undefined;
|
|
19
|
+
},
|
|
20
|
+
set(link, options = {}) {
|
|
21
|
+
const temp = `${path(link.interactive, options)}.${randomUUID()}.tmp`;
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
writeFileSync(temp, JSON.stringify(link), { mode: 0o600, flag: 'wx' });
|
|
25
|
+
renameSync(temp, path(link.interactive, options));
|
|
26
|
+
}
|
|
27
|
+
catch { /* caching is optional; the minted link remains usable */ }
|
|
28
|
+
finally {
|
|
29
|
+
rmSync(temp, { force: true });
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
clear() { rmSync(dir, { recursive: true, force: true }); },
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/format.js
CHANGED
|
@@ -155,6 +155,9 @@ const deferText = (r) => (r === null || r === 'suspended' || r === 'stopped' ? '
|
|
|
155
155
|
* applied in part (requested above a limit, a partial grow, a shrink the guest did not finish).
|
|
156
156
|
*/
|
|
157
157
|
export function resizeText(w, r) {
|
|
158
|
+
if (r.operation && !['succeeded', 'failed', 'canceled'].includes(r.operation.state)) {
|
|
159
|
+
return `Resize accepted for ${w.key} (${w.id}); operation ${r.operation.id} is ${r.operation.state}.\n`;
|
|
160
|
+
}
|
|
158
161
|
const rows = [];
|
|
159
162
|
const row = (name, unit, x, extra = '', converged = null) => {
|
|
160
163
|
const notes = [];
|
package/dist/main.js
CHANGED
|
@@ -415,7 +415,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
415
415
|
let ctx;
|
|
416
416
|
let updates = null;
|
|
417
417
|
try {
|
|
418
|
-
const apiUrl = resolveApiUrl(parsed.global.apiUrl, io.env.SHARDFLUX_API_URL);
|
|
418
|
+
const apiUrl = resolveApiUrl(parsed.global.apiUrl, io.env.SHARDFLUX_API_URL ?? io.env.SHARDFLUX_BASE_URL);
|
|
419
419
|
apiUrlForCleanup = apiUrl;
|
|
420
420
|
const agentLabel = parsed.global.agentLabel ?? io.env.SHARDFLUX_AGENT_LABEL ?? 'cli';
|
|
421
421
|
if (!isLabel(agentLabel))
|
|
@@ -609,7 +609,8 @@ export async function run(argv, io, opts = {}) {
|
|
|
609
609
|
const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
|
|
610
610
|
// On Ctrl-C give the command a moment to clean up (exec sends a cancel), then stop regardless.
|
|
611
611
|
const interrupted = new Promise((resolve) => {
|
|
612
|
-
const
|
|
612
|
+
const cleanupMs = parsed.command.path.join(' ') === 'workspaces tunnel' ? 90_000 : 4_000;
|
|
613
|
+
const onAbort = () => setTimeout(() => resolve({ interrupted: true }), cleanupMs).unref();
|
|
613
614
|
if (signal.aborted)
|
|
614
615
|
onAbort();
|
|
615
616
|
else
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"type": "module",
|
|
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
|
|
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 serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"homepage": "https://shardflux.dev",
|
|
8
8
|
"bugs": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"yaml": "2.9.1",
|
|
47
|
-
"@shardflux/sdk": "^0.
|
|
47
|
+
"@shardflux/sdk": "^0.16.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|