ghostdock 0.0.8 → 0.0.9
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/dist/cli-reference.md +88 -9
- package/dist/cli.md +45 -2
- package/dist/index.js +573 -84
- package/package.json +1 -1
package/dist/cli-reference.md
CHANGED
|
@@ -150,7 +150,7 @@ MCP tool: `images_add_dockerfile`.
|
|
|
150
150
|
|
|
151
151
|
### `ghostdock images get`
|
|
152
152
|
|
|
153
|
-
Get one catalog image with its spec, manifest and requirements. Returns kind, spec and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps.
|
|
153
|
+
Get one catalog image with its spec, manifest and requirements. Returns kind, spec and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps. An OCI bookmark’s manifest is null: its labels are read at create time.
|
|
154
154
|
|
|
155
155
|
```
|
|
156
156
|
ghostdock images get <image-id> [flags]
|
|
@@ -165,7 +165,7 @@ MCP tool: `images_get`.
|
|
|
165
165
|
|
|
166
166
|
### `ghostdock images list`
|
|
167
167
|
|
|
168
|
-
List the images an organization can boot sandboxes from. OCI bookmarks, images add-dockerfile and images add-compose rows, and refs its sandboxes use. A discovery catalog, not an allowlist: sandboxes create accepts any OCI ref. Items carry sandboxCount and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps.
|
|
168
|
+
List the images an organization can boot sandboxes from. OCI bookmarks, images add-dockerfile and images add-compose rows, and refs its sandboxes use. A discovery catalog, not an allowlist: sandboxes create accepts any OCI ref. Items carry sandboxCount and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps. An OCI bookmark stores no labels (manifest null): sandboxes create reads them from the digest the ref names then.
|
|
169
169
|
|
|
170
170
|
```
|
|
171
171
|
ghostdock images list [flags]
|
|
@@ -249,6 +249,83 @@ Revoke the device session and remove stored credentials.
|
|
|
249
249
|
ghostdock logout
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
## `ghostdock network`
|
|
253
|
+
|
|
254
|
+
The private network: reach your sandboxes by address and name from this machine.
|
|
255
|
+
|
|
256
|
+
### `ghostdock network connect`
|
|
257
|
+
|
|
258
|
+
Put this machine into the private network: WireGuard and split DNS. Asks for sudo once.
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
ghostdock network connect [flags]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
| Argument | Description |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `--org <value>` | Organization id or slug, from `ghostdock organizations list` (default: GHOSTDOCK_ORG, then `ghostdock organizations use`). |
|
|
267
|
+
|
|
268
|
+
### `ghostdock network devices`
|
|
269
|
+
|
|
270
|
+
Your devices in the private network.
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
ghostdock network devices <command>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `ghostdock network devices remove`
|
|
277
|
+
|
|
278
|
+
Remove one of your devices from the private network.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
ghostdock network devices remove <id>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
| Argument | Description |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| `<id>` | Device id, from `network devices`. Required. |
|
|
287
|
+
|
|
288
|
+
### `ghostdock network disconnect`
|
|
289
|
+
|
|
290
|
+
Take this machine out of the private network.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
ghostdock network disconnect
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `ghostdock network ls`
|
|
297
|
+
|
|
298
|
+
Sandboxes of an organization with their private addresses and names.
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
ghostdock network ls [flags]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Argument | Description |
|
|
305
|
+
| --- | --- |
|
|
306
|
+
| `--org <value>` | Organization id or slug, from `ghostdock organizations list` (default: GHOSTDOCK_ORG, then `ghostdock organizations use`). |
|
|
307
|
+
|
|
308
|
+
### `ghostdock network status`
|
|
309
|
+
|
|
310
|
+
Whether this machine is connected, and as which device.
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
ghostdock network status
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### `ghostdock network use`
|
|
317
|
+
|
|
318
|
+
Choose the organization <sandbox>.gdock.internal names resolve in on this machine.
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
ghostdock network use [org] [flags]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
| Argument | Description |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| `<org>` | Organization id or slug. |
|
|
327
|
+
| `--clear` | Turn short names off. |
|
|
328
|
+
|
|
252
329
|
## `ghostdock organizations`
|
|
253
330
|
|
|
254
331
|
Organizations you belong to.
|
|
@@ -744,7 +821,7 @@ MCP tool: `sandboxes_commands_run`.
|
|
|
744
821
|
|
|
745
822
|
### `ghostdock sandboxes create`
|
|
746
823
|
|
|
747
|
-
Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a
|
|
824
|
+
Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a missing environment entry fails with its names (missingEnvironment), and removing a required URL or MCP fails with them (missingUrls / missingMcps). Returns once the sandbox is running, with `boot`: how long the sandbox (VM) and, when one was started, its container took to start, each in ms. Idle-stop and auto-delete apply (defaults 20 minutes / 14 days). Read the sandboxes skill (`ghostdock skills get sandboxes`) for sources, manifests, lifecycle and providers.
|
|
748
825
|
|
|
749
826
|
```
|
|
750
827
|
ghostdock sandboxes create --source <object> [flags]
|
|
@@ -761,8 +838,8 @@ ghostdock sandboxes create --source <object> [flags]
|
|
|
761
838
|
| `--sandbox-provider-id <string>` | Configured sandbox-provider instance id from providers list (not the provider kind). Omit to use the organization default (the org choice, else the platform default); if that provider lacks a capability this create needs, the first listed provider that accepts new sandboxes and has it is used. When no provider fits, the call fails with a not-configured error. |
|
|
762
839
|
| `--environment <object>` | Environment to give this sandbox. organization: names of organization entries (from env list) to export under their own names. bindings: remap org entries onto slot names ({ name: slot, organization: orgEntryName }). local: sandbox-only variables/secrets to create with the sandbox (kind required for each). Both land as ordinary process environment variables ($NAME / process.env.NAME); a local entry overrides an organization entry of the same name. When booting from an organization snapshot, omit organization/bindings/local to keep manifest env defaults, but still supply every name in requiredEnvironment (via organization, bindings, or local). Otherwise a sandbox gets nothing unless listed here. (JSON object) |
|
|
763
840
|
| `--ephemeral` | Mark the sandbox ephemeral: every stop destroys the machine and the next start boots fresh, while the record, URLs, and settings stay. Omit to use the image label ghostdock.ephemeral, or false when that label is absent. Does not change idle-stop or auto-delete. |
|
|
764
|
-
| `--urls <array>` |
|
|
765
|
-
| `--mcps <array>` | MCP exposures
|
|
841
|
+
| `--urls <array>` | Changes to the image’s or snapshot’s URLs, keyed by port. Omit for the image URLs as they are; otherwise you get the image URLs plus yours: an entry on the same port replaces that URL completely (omitted fields take their defaults), and { port, remove: true } drops it; removing a port the image does not declare does nothing. A required port cannot be removed. TCP URLs are always public. (JSON array) |
|
|
842
|
+
| `--mcps <array>` | Changes to the image’s or snapshot’s MCP exposures, keyed by name. Omit for the image MCPs as they are; otherwise you get the image MCPs plus yours: an entry with the same name replaces that MCP completely, and { name, remove: true } drops it; removing a name the image does not declare does nothing. A required MCP (requiredMcps) cannot be removed. (JSON array) |
|
|
766
843
|
|
|
767
844
|
MCP tool: `sandboxes_create`.
|
|
768
845
|
|
|
@@ -1075,7 +1152,7 @@ ghostdock sandboxes shells create <sandbox-id> [flags]
|
|
|
1075
1152
|
| --- | --- |
|
|
1076
1153
|
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
1077
1154
|
| `--org <string>` | Organization id or slug, from organizations list. Ids survive renames; slugs change when the organization is renamed. Defaults to GHOSTDOCK_ORG, then `organizations use`. |
|
|
1078
|
-
| `--session-id <string>` | Id for the new session. Omit to generate one. |
|
|
1155
|
+
| `--session-id <string>` | Id for the new session: a slug; user-cmd, image-cmd and compose are reserved. Omit to generate one. |
|
|
1079
1156
|
|
|
1080
1157
|
MCP tool: `sandboxes_shells_create`.
|
|
1081
1158
|
|
|
@@ -1205,7 +1282,7 @@ URLs that expose sandbox ports.
|
|
|
1205
1282
|
|
|
1206
1283
|
### `ghostdock sandboxes urls create`
|
|
1207
1284
|
|
|
1208
|
-
Expose a sandbox port as a URL. Idempotent by port. HTTP defaults to private with no wake-on-visit; TCP URLs use TLS (not HTTP) and are always public — for a private port use sandboxes ssh get and an SSH forward. Read the sandboxes skill (`ghostdock skills get sandboxes`) for TCP clients.
|
|
1285
|
+
Expose a sandbox port as a URL. Idempotent by port: when the sandbox already has a URL on that port, it is returned unchanged (autoWake, public and title are not applied); use sandboxes urls update to change it. HTTP defaults to private with no wake-on-visit; TCP URLs use TLS (not HTTP) and are always public — for a private port use sandboxes ssh get and an SSH forward. Read the sandboxes skill (`ghostdock skills get sandboxes`) for TCP clients.
|
|
1209
1286
|
|
|
1210
1287
|
```
|
|
1211
1288
|
ghostdock sandboxes urls create <sandbox-id> --port <integer> [flags]
|
|
@@ -1219,6 +1296,7 @@ ghostdock sandboxes urls create <sandbox-id> --port <integer> [flags]
|
|
|
1219
1296
|
| `--protocol <string>` | "http" (default) or "tcp". TCP URLs use TLS and are always public. |
|
|
1220
1297
|
| `--auto-wake` | Start the sandbox when the URL is visited while it is stopped. Default false. |
|
|
1221
1298
|
| `--public` | Reachable without signing in. Default false; TCP URLs are always public. |
|
|
1299
|
+
| `--title <string>` | Name shown for the URL, up to 40 characters. A titled HTTP URL gets its own button on the sandbox card; TCP titles are shown in URL lists. Untitled HTTP URLs are listed under "Open URL". |
|
|
1222
1300
|
|
|
1223
1301
|
MCP tool: `sandboxes_urls_create`.
|
|
1224
1302
|
|
|
@@ -1238,7 +1316,7 @@ MCP tool: `sandboxes_urls_delete`.
|
|
|
1238
1316
|
|
|
1239
1317
|
### `ghostdock sandboxes urls list`
|
|
1240
1318
|
|
|
1241
|
-
List the URLs that expose a sandbox’s ports, with protocol and
|
|
1319
|
+
List the URLs that expose a sandbox’s ports, with protocol, access and title. A titled URL (for example `Desktop`, the desktop image’s desktop in a browser) gets its own button in the app.
|
|
1242
1320
|
|
|
1243
1321
|
```
|
|
1244
1322
|
ghostdock sandboxes urls list <sandbox-id> [flags]
|
|
@@ -1253,7 +1331,7 @@ MCP tool: `sandboxes_urls_list`.
|
|
|
1253
1331
|
|
|
1254
1332
|
### `ghostdock sandboxes urls update`
|
|
1255
1333
|
|
|
1256
|
-
Change a sandbox URL’s port, protocol, autoWake
|
|
1334
|
+
Change a sandbox URL’s port, protocol, autoWake, public setting or title. TCP cannot be private.
|
|
1257
1335
|
|
|
1258
1336
|
```
|
|
1259
1337
|
ghostdock sandboxes urls update <url-id> [flags]
|
|
@@ -1266,6 +1344,7 @@ ghostdock sandboxes urls update <url-id> [flags]
|
|
|
1266
1344
|
| `--protocol <string>` | "http" or "tcp". Omit to keep the current protocol. |
|
|
1267
1345
|
| `--auto-wake` | Start the sandbox when the URL is visited while it is stopped. Omit to keep it. |
|
|
1268
1346
|
| `--public` | Reachable without signing in. Omit to keep it; TCP cannot be made private. |
|
|
1347
|
+
| `--title <string>` | Name shown for the URL, up to 40 characters. A titled HTTP URL gets its own button on the sandbox card; TCP titles are shown in URL lists. Untitled HTTP URLs are listed under "Open URL". Omit to keep it; null removes it. |
|
|
1269
1348
|
|
|
1270
1349
|
MCP tool: `sandboxes_urls_update`.
|
|
1271
1350
|
|
package/dist/cli.md
CHANGED
|
@@ -101,6 +101,7 @@ ghostdock env Manage organization environment variables and secrets.
|
|
|
101
101
|
ghostdock images Organization image catalog: OCI bookmarks, Dockerfiles and Compose files.
|
|
102
102
|
ghostdock login Sign in and store a device access token for this CLI.
|
|
103
103
|
ghostdock logout Revoke the device session and remove stored credentials.
|
|
104
|
+
ghostdock network The private network: reach your sandboxes by address and name from this machine.
|
|
104
105
|
ghostdock organizations Organizations you belong to.
|
|
105
106
|
ghostdock projects Deployable projects, their deployments and environment.
|
|
106
107
|
ghostdock providers Sandbox providers of an organization.
|
|
@@ -475,6 +476,36 @@ Names must be valid environment variable names, and names the sandbox runtime ow
|
|
|
475
476
|
only when browser code must read it: Vite inlines those into the published bundle, so a `VITE_`
|
|
476
477
|
secret is public.
|
|
477
478
|
|
|
479
|
+
### `ghostdock network`
|
|
480
|
+
|
|
481
|
+
Reach your organization's sandboxes from this machine by private address and name, over
|
|
482
|
+
WireGuard ([ADR 0027](../../docs/adr/0027-private-sandbox-network.md)). Everything on a
|
|
483
|
+
sandbox's address is reachable: a container that publishes port 80 answers on
|
|
484
|
+
`http://quiet-harbor.gdock.internal/`, as it would on `localhost` inside the sandbox.
|
|
485
|
+
Nothing reaches your machine from a sandbox.
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
ghostdock network connect --org acme # asks for sudo once: wg-quick up and split DNS
|
|
489
|
+
ghostdock network ls --org acme # sandboxes with their addresses and names
|
|
490
|
+
curl http://quiet-harbor.acme.ghostdock.internal:8080/
|
|
491
|
+
curl http://quiet-harbor.gdock.internal:8080/ # short name: the organization connect used
|
|
492
|
+
ghostdock network use beta # short names in another organization, no sudo
|
|
493
|
+
ghostdock network status
|
|
494
|
+
ghostdock network disconnect
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- **Needs `wg-quick`** (`brew install wireguard-tools`, `apt install wireguard-tools`). macOS
|
|
498
|
+
and Linux. On Windows, run the CLI in WSL: then only tools inside WSL reach sandboxes.
|
|
499
|
+
- **One key per machine**, kept in `~/.config/ghostdock/network/` (or `GHOSTDOCK_NETWORK_DIR`).
|
|
500
|
+
Only the public key is sent. The device belongs to the login it was connected with:
|
|
501
|
+
`ghostdock logout` takes it out of the network, and so does not connecting for 90 days.
|
|
502
|
+
- **Names**: `<sandbox>.<org>.ghostdock.internal` always, and `<sandbox>.gdock.internal` in the
|
|
503
|
+
organization this machine is pinned to (the one `connect` resolved, changed with
|
|
504
|
+
`network use`). The pin is per machine, not per directory. Only AAAA records exist: the
|
|
505
|
+
network is IPv6, and the sandbox sees IPv4.
|
|
506
|
+
- `ghostdock network devices` lists your devices, `ghostdock network devices remove <id>`
|
|
507
|
+
removes one.
|
|
508
|
+
|
|
478
509
|
## Command index
|
|
479
510
|
|
|
480
511
|
Every command with its arguments and what it does. Run `ghostdock <command> --help` for its flags,
|
|
@@ -521,6 +552,18 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
521
552
|
| --- | --- |
|
|
522
553
|
| `ghostdock logout` | Revoke the device session and remove stored credentials. |
|
|
523
554
|
|
|
555
|
+
#### network
|
|
556
|
+
|
|
557
|
+
| Command | What it does |
|
|
558
|
+
| --- | --- |
|
|
559
|
+
| `ghostdock network connect [flags]` | Put this machine into the private network: WireGuard and split DNS. |
|
|
560
|
+
| `ghostdock network devices <command>` | Your devices in the private network. |
|
|
561
|
+
| `ghostdock network devices remove <id>` | Remove one of your devices from the private network. |
|
|
562
|
+
| `ghostdock network disconnect` | Take this machine out of the private network. |
|
|
563
|
+
| `ghostdock network ls [flags]` | Sandboxes of an organization with their private addresses and names. |
|
|
564
|
+
| `ghostdock network status` | Whether this machine is connected, and as which device. |
|
|
565
|
+
| `ghostdock network use [org] [flags]` | Choose the organization <sandbox>.gdock.internal names resolve in on this… |
|
|
566
|
+
|
|
524
567
|
#### organizations
|
|
525
568
|
|
|
526
569
|
| Command | What it does |
|
|
@@ -601,8 +644,8 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
601
644
|
| `ghostdock sandboxes update <sandbox-id> [flags]` | Change a sandbox’s display name, idle-stop or auto-delete interval. |
|
|
602
645
|
| `ghostdock sandboxes urls create <sandbox-id> --port <integer> [flags]` | Expose a sandbox port as a URL. |
|
|
603
646
|
| `ghostdock sandboxes urls delete <url-id>` | Delete a sandbox URL. |
|
|
604
|
-
| `ghostdock sandboxes urls list <sandbox-id> [flags]` | List the URLs that expose a sandbox’s ports, with protocol and
|
|
605
|
-
| `ghostdock sandboxes urls update <url-id> [flags]` | Change a sandbox URL’s port, protocol, autoWake
|
|
647
|
+
| `ghostdock sandboxes urls list <sandbox-id> [flags]` | List the URLs that expose a sandbox’s ports, with protocol, access and title. |
|
|
648
|
+
| `ghostdock sandboxes urls update <url-id> [flags]` | Change a sandbox URL’s port, protocol, autoWake, public setting or title. |
|
|
606
649
|
| `ghostdock sandboxes wake <sandbox-id> [flags]` | Start a stopped sandbox, or reset the idle-stop countdown of a running one. |
|
|
607
650
|
|
|
608
651
|
#### sites
|
package/dist/index.js
CHANGED
|
@@ -606,7 +606,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
606
606
|
"sandboxes",
|
|
607
607
|
"create"
|
|
608
608
|
],
|
|
609
|
-
description: "Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a
|
|
609
|
+
description: "Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a missing environment entry fails with its names (missingEnvironment), and removing a required URL or MCP fails with them (missingUrls / missingMcps). Returns once the sandbox is running, with `boot`: how long the sandbox (VM) and, when one was started, its container took to start, each in ms. Idle-stop and auto-delete apply (defaults 20 minutes / 14 days). Read the sandboxes skill for sources, manifests, lifecycle and providers.",
|
|
610
610
|
inputSchema: {
|
|
611
611
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
612
612
|
type: "object",
|
|
@@ -840,69 +840,121 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
840
840
|
type: "boolean"
|
|
841
841
|
},
|
|
842
842
|
urls: {
|
|
843
|
-
description: "
|
|
843
|
+
description: "Changes to the image’s or snapshot’s URLs, keyed by port. Omit for the image URLs as they are; otherwise you get the image URLs plus yours: an entry on the same port replaces that URL completely (omitted fields take their defaults), and { port, remove: true } drops it; removing a port the image does not declare does nothing. A required port cannot be removed. TCP URLs are always public.",
|
|
844
844
|
maxItems: 100,
|
|
845
845
|
type: "array",
|
|
846
846
|
items: {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
847
|
+
anyOf: [
|
|
848
|
+
{
|
|
849
|
+
type: "object",
|
|
850
|
+
properties: {
|
|
851
|
+
port: {
|
|
852
|
+
type: "integer",
|
|
853
|
+
minimum: 1,
|
|
854
|
+
maximum: 65535
|
|
855
|
+
},
|
|
856
|
+
protocol: {
|
|
857
|
+
type: "string",
|
|
858
|
+
enum: [
|
|
859
|
+
"http",
|
|
860
|
+
"tcp"
|
|
861
|
+
]
|
|
862
|
+
},
|
|
863
|
+
autoWake: {
|
|
864
|
+
type: "boolean"
|
|
865
|
+
},
|
|
866
|
+
public: {
|
|
867
|
+
type: "boolean"
|
|
868
|
+
},
|
|
869
|
+
title: {
|
|
870
|
+
description: 'Name shown for the URL, up to 40 characters. A titled HTTP URL gets its own button on the sandbox card; TCP titles are shown in URL lists. Untitled HTTP URLs are listed under "Open URL".',
|
|
871
|
+
type: "string",
|
|
872
|
+
minLength: 1,
|
|
873
|
+
maxLength: 40
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
required: [
|
|
877
|
+
"port",
|
|
878
|
+
"autoWake",
|
|
879
|
+
"public"
|
|
880
|
+
],
|
|
881
|
+
additionalProperties: false
|
|
863
882
|
},
|
|
864
|
-
|
|
865
|
-
type: "
|
|
883
|
+
{
|
|
884
|
+
type: "object",
|
|
885
|
+
properties: {
|
|
886
|
+
port: {
|
|
887
|
+
type: "integer",
|
|
888
|
+
minimum: 1,
|
|
889
|
+
maximum: 65535
|
|
890
|
+
},
|
|
891
|
+
remove: {
|
|
892
|
+
type: "boolean",
|
|
893
|
+
const: true
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
required: [
|
|
897
|
+
"port",
|
|
898
|
+
"remove"
|
|
899
|
+
],
|
|
900
|
+
additionalProperties: false
|
|
866
901
|
}
|
|
867
|
-
|
|
868
|
-
required: [
|
|
869
|
-
"port",
|
|
870
|
-
"autoWake",
|
|
871
|
-
"public"
|
|
872
|
-
],
|
|
873
|
-
additionalProperties: false
|
|
902
|
+
]
|
|
874
903
|
}
|
|
875
904
|
},
|
|
876
905
|
mcps: {
|
|
877
|
-
description: "MCP exposures
|
|
906
|
+
description: "Changes to the image’s or snapshot’s MCP exposures, keyed by name. Omit for the image MCPs as they are; otherwise you get the image MCPs plus yours: an entry with the same name replaces that MCP completely, and { name, remove: true } drops it; removing a name the image does not declare does nothing. A required MCP (requiredMcps) cannot be removed.",
|
|
878
907
|
maxItems: 100,
|
|
879
908
|
type: "array",
|
|
880
909
|
items: {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
910
|
+
anyOf: [
|
|
911
|
+
{
|
|
912
|
+
type: "object",
|
|
913
|
+
properties: {
|
|
914
|
+
name: {
|
|
915
|
+
type: "string",
|
|
916
|
+
maxLength: 64,
|
|
917
|
+
pattern: "^[A-Za-z][A-Za-z0-9_-]*$"
|
|
918
|
+
},
|
|
919
|
+
port: {
|
|
920
|
+
type: "integer",
|
|
921
|
+
minimum: 1,
|
|
922
|
+
maximum: 65535
|
|
923
|
+
},
|
|
924
|
+
path: {
|
|
925
|
+
type: "string"
|
|
926
|
+
},
|
|
927
|
+
autoWake: {
|
|
928
|
+
type: "boolean"
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
required: [
|
|
932
|
+
"name",
|
|
933
|
+
"port",
|
|
934
|
+
"autoWake"
|
|
935
|
+
],
|
|
936
|
+
additionalProperties: false
|
|
895
937
|
},
|
|
896
|
-
|
|
897
|
-
type: "
|
|
938
|
+
{
|
|
939
|
+
type: "object",
|
|
940
|
+
properties: {
|
|
941
|
+
name: {
|
|
942
|
+
type: "string",
|
|
943
|
+
maxLength: 64,
|
|
944
|
+
pattern: "^[A-Za-z][A-Za-z0-9_-]*$"
|
|
945
|
+
},
|
|
946
|
+
remove: {
|
|
947
|
+
type: "boolean",
|
|
948
|
+
const: true
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
required: [
|
|
952
|
+
"name",
|
|
953
|
+
"remove"
|
|
954
|
+
],
|
|
955
|
+
additionalProperties: false
|
|
898
956
|
}
|
|
899
|
-
|
|
900
|
-
required: [
|
|
901
|
-
"name",
|
|
902
|
-
"port",
|
|
903
|
-
"autoWake"
|
|
904
|
-
],
|
|
905
|
-
additionalProperties: false
|
|
957
|
+
]
|
|
906
958
|
}
|
|
907
959
|
}
|
|
908
960
|
},
|
|
@@ -3710,6 +3762,11 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
3710
3762
|
"http",
|
|
3711
3763
|
"tcp"
|
|
3712
3764
|
]
|
|
3765
|
+
},
|
|
3766
|
+
title: {
|
|
3767
|
+
type: "string",
|
|
3768
|
+
minLength: 1,
|
|
3769
|
+
maxLength: 40
|
|
3713
3770
|
}
|
|
3714
3771
|
},
|
|
3715
3772
|
required: [
|
|
@@ -3995,6 +4052,11 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
3995
4052
|
"http",
|
|
3996
4053
|
"tcp"
|
|
3997
4054
|
]
|
|
4055
|
+
},
|
|
4056
|
+
title: {
|
|
4057
|
+
type: "string",
|
|
4058
|
+
minLength: 1,
|
|
4059
|
+
maxLength: 40
|
|
3998
4060
|
}
|
|
3999
4061
|
},
|
|
4000
4062
|
required: [
|
|
@@ -4151,7 +4213,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4151
4213
|
"images",
|
|
4152
4214
|
"list"
|
|
4153
4215
|
],
|
|
4154
|
-
description: "List the images an organization can boot sandboxes from. OCI bookmarks, images_add_dockerfile and images_add_compose rows, and refs its sandboxes use. A discovery catalog, not an allowlist: sandboxes_create accepts any OCI ref. Items carry sandboxCount and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps.",
|
|
4216
|
+
description: "List the images an organization can boot sandboxes from. OCI bookmarks, images_add_dockerfile and images_add_compose rows, and refs its sandboxes use. A discovery catalog, not an allowlist: sandboxes_create accepts any OCI ref. Items carry sandboxCount and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps. An OCI bookmark stores no labels (manifest null): sandboxes_create reads them from the digest the ref names then.",
|
|
4155
4217
|
inputSchema: {
|
|
4156
4218
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4157
4219
|
type: "object",
|
|
@@ -4186,7 +4248,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4186
4248
|
cliArgs: [
|
|
4187
4249
|
"imageId"
|
|
4188
4250
|
],
|
|
4189
|
-
description: "Get one catalog image with its spec, manifest and requirements. Returns kind, spec and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps.",
|
|
4251
|
+
description: "Get one catalog image with its spec, manifest and requirements. Returns kind, spec and a redacted manifest with requiredEnvironment / requiredUrls / requiredMcps. An OCI bookmark’s manifest is null: its labels are read at create time.",
|
|
4190
4252
|
inputSchema: {
|
|
4191
4253
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4192
4254
|
type: "object",
|
|
@@ -4766,7 +4828,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4766
4828
|
cliArgs: [
|
|
4767
4829
|
"sandboxId"
|
|
4768
4830
|
],
|
|
4769
|
-
description: "List the URLs that expose a sandbox’s ports, with protocol and
|
|
4831
|
+
description: "List the URLs that expose a sandbox’s ports, with protocol, access and title. A titled URL (for example `Desktop`, the desktop image’s desktop in a browser) gets its own button in the app.",
|
|
4770
4832
|
inputSchema: {
|
|
4771
4833
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4772
4834
|
type: "object",
|
|
@@ -4834,6 +4896,16 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4834
4896
|
public: {
|
|
4835
4897
|
type: "boolean"
|
|
4836
4898
|
},
|
|
4899
|
+
title: {
|
|
4900
|
+
anyOf: [
|
|
4901
|
+
{
|
|
4902
|
+
type: "string"
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
type: "null"
|
|
4906
|
+
}
|
|
4907
|
+
]
|
|
4908
|
+
},
|
|
4837
4909
|
sshTunnel: {
|
|
4838
4910
|
anyOf: [
|
|
4839
4911
|
{
|
|
@@ -4904,6 +4976,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4904
4976
|
"port",
|
|
4905
4977
|
"autoWake",
|
|
4906
4978
|
"public",
|
|
4979
|
+
"title",
|
|
4907
4980
|
"sshTunnel",
|
|
4908
4981
|
"connect"
|
|
4909
4982
|
],
|
|
@@ -4928,7 +5001,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4928
5001
|
cliArgs: [
|
|
4929
5002
|
"sandboxId"
|
|
4930
5003
|
],
|
|
4931
|
-
description: "Expose a sandbox port as a URL. Idempotent by port. HTTP defaults to private with no wake-on-visit; TCP URLs use TLS (not HTTP) and are always public — for a private port use sandboxes_ssh_get and an SSH forward. Read the sandboxes skill for TCP clients.",
|
|
5004
|
+
description: "Expose a sandbox port as a URL. Idempotent by port: when the sandbox already has a URL on that port, it is returned unchanged (autoWake, public and title are not applied); use sandboxes_urls_update to change it. HTTP defaults to private with no wake-on-visit; TCP URLs use TLS (not HTTP) and are always public — for a private port use sandboxes_ssh_get and an SSH forward. Read the sandboxes skill for TCP clients.",
|
|
4932
5005
|
inputSchema: {
|
|
4933
5006
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4934
5007
|
type: "object",
|
|
@@ -4964,6 +5037,12 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4964
5037
|
public: {
|
|
4965
5038
|
description: "Reachable without signing in. Default false; TCP URLs are always public.",
|
|
4966
5039
|
type: "boolean"
|
|
5040
|
+
},
|
|
5041
|
+
title: {
|
|
5042
|
+
description: 'Name shown for the URL, up to 40 characters. A titled HTTP URL gets its own button on the sandbox card; TCP titles are shown in URL lists. Untitled HTTP URLs are listed under "Open URL".',
|
|
5043
|
+
type: "string",
|
|
5044
|
+
minLength: 1,
|
|
5045
|
+
maxLength: 40
|
|
4967
5046
|
}
|
|
4968
5047
|
},
|
|
4969
5048
|
required: [
|
|
@@ -5014,6 +5093,16 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5014
5093
|
public: {
|
|
5015
5094
|
type: "boolean"
|
|
5016
5095
|
},
|
|
5096
|
+
title: {
|
|
5097
|
+
anyOf: [
|
|
5098
|
+
{
|
|
5099
|
+
type: "string"
|
|
5100
|
+
},
|
|
5101
|
+
{
|
|
5102
|
+
type: "null"
|
|
5103
|
+
}
|
|
5104
|
+
]
|
|
5105
|
+
},
|
|
5017
5106
|
sshTunnel: {
|
|
5018
5107
|
anyOf: [
|
|
5019
5108
|
{
|
|
@@ -5084,6 +5173,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5084
5173
|
"port",
|
|
5085
5174
|
"autoWake",
|
|
5086
5175
|
"public",
|
|
5176
|
+
"title",
|
|
5087
5177
|
"sshTunnel",
|
|
5088
5178
|
"connect"
|
|
5089
5179
|
],
|
|
@@ -5101,7 +5191,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5101
5191
|
cliArgs: [
|
|
5102
5192
|
"urlId"
|
|
5103
5193
|
],
|
|
5104
|
-
description: "Change a sandbox URL’s port, protocol, autoWake
|
|
5194
|
+
description: "Change a sandbox URL’s port, protocol, autoWake, public setting or title. TCP cannot be private.",
|
|
5105
5195
|
inputSchema: {
|
|
5106
5196
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
5107
5197
|
type: "object",
|
|
@@ -5132,6 +5222,19 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5132
5222
|
public: {
|
|
5133
5223
|
description: "Reachable without signing in. Omit to keep it; TCP cannot be made private.",
|
|
5134
5224
|
type: "boolean"
|
|
5225
|
+
},
|
|
5226
|
+
title: {
|
|
5227
|
+
description: 'Name shown for the URL, up to 40 characters. A titled HTTP URL gets its own button on the sandbox card; TCP titles are shown in URL lists. Untitled HTTP URLs are listed under "Open URL". Omit to keep it; null removes it.',
|
|
5228
|
+
anyOf: [
|
|
5229
|
+
{
|
|
5230
|
+
type: "string",
|
|
5231
|
+
minLength: 1,
|
|
5232
|
+
maxLength: 40
|
|
5233
|
+
},
|
|
5234
|
+
{
|
|
5235
|
+
type: "null"
|
|
5236
|
+
}
|
|
5237
|
+
]
|
|
5135
5238
|
}
|
|
5136
5239
|
},
|
|
5137
5240
|
required: [
|
|
@@ -5180,6 +5283,16 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5180
5283
|
public: {
|
|
5181
5284
|
type: "boolean"
|
|
5182
5285
|
},
|
|
5286
|
+
title: {
|
|
5287
|
+
anyOf: [
|
|
5288
|
+
{
|
|
5289
|
+
type: "string"
|
|
5290
|
+
},
|
|
5291
|
+
{
|
|
5292
|
+
type: "null"
|
|
5293
|
+
}
|
|
5294
|
+
]
|
|
5295
|
+
},
|
|
5183
5296
|
sshTunnel: {
|
|
5184
5297
|
anyOf: [
|
|
5185
5298
|
{
|
|
@@ -5250,6 +5363,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5250
5363
|
"port",
|
|
5251
5364
|
"autoWake",
|
|
5252
5365
|
"public",
|
|
5366
|
+
"title",
|
|
5253
5367
|
"sshTunnel",
|
|
5254
5368
|
"connect"
|
|
5255
5369
|
],
|
|
@@ -6100,7 +6214,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6100
6214
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6101
6215
|
},
|
|
6102
6216
|
sessionId: {
|
|
6103
|
-
description: "Id for the new session. Omit to generate one.",
|
|
6217
|
+
description: "Id for the new session: a slug; user-cmd, image-cmd and compose are reserved. Omit to generate one.",
|
|
6104
6218
|
type: "string",
|
|
6105
6219
|
minLength: 1
|
|
6106
6220
|
}
|
|
@@ -8446,7 +8560,7 @@ function getCliName(argv = process.argv) {
|
|
|
8446
8560
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
8447
8561
|
}
|
|
8448
8562
|
// package.json
|
|
8449
|
-
var version = "0.0.
|
|
8563
|
+
var version = "0.0.9";
|
|
8450
8564
|
var package_default = {
|
|
8451
8565
|
name: "ghostdock",
|
|
8452
8566
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -9683,6 +9797,380 @@ var logoutCommand = defineCommand({
|
|
|
9683
9797
|
}
|
|
9684
9798
|
});
|
|
9685
9799
|
|
|
9800
|
+
// src/commands/network.ts
|
|
9801
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9802
|
+
import { hostname as hostname2 } from "node:os";
|
|
9803
|
+
|
|
9804
|
+
// src/network/tunnel.ts
|
|
9805
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
9806
|
+
import { existsSync } from "node:fs";
|
|
9807
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
9808
|
+
import { homedir } from "node:os";
|
|
9809
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9810
|
+
var INTERFACE = "gdock";
|
|
9811
|
+
function networkDir() {
|
|
9812
|
+
const override = process.env.GHOSTDOCK_NETWORK_DIR?.trim();
|
|
9813
|
+
if (override)
|
|
9814
|
+
return override;
|
|
9815
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || join3(homedir(), ".config");
|
|
9816
|
+
return join3(base, "ghostdock", "network");
|
|
9817
|
+
}
|
|
9818
|
+
function configPath(dir = networkDir()) {
|
|
9819
|
+
return join3(dir, `${INTERFACE}.conf`);
|
|
9820
|
+
}
|
|
9821
|
+
function keyPath(dir) {
|
|
9822
|
+
return join3(dir, "private.key");
|
|
9823
|
+
}
|
|
9824
|
+
function statePath(dir) {
|
|
9825
|
+
return join3(dir, "device.json");
|
|
9826
|
+
}
|
|
9827
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
9828
|
+
function keyPairFromPrivate(privateKey) {
|
|
9829
|
+
const raw = Buffer.from(privateKey, "base64");
|
|
9830
|
+
if (raw.length !== 32)
|
|
9831
|
+
throw new Error("The stored WireGuard key is not 32 bytes.");
|
|
9832
|
+
const key = createPrivateKey({
|
|
9833
|
+
key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
|
|
9834
|
+
format: "der",
|
|
9835
|
+
type: "pkcs8"
|
|
9836
|
+
});
|
|
9837
|
+
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
9838
|
+
return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
|
|
9839
|
+
}
|
|
9840
|
+
async function loadOrCreateKey(dir = networkDir()) {
|
|
9841
|
+
try {
|
|
9842
|
+
return keyPairFromPrivate((await readFile3(keyPath(dir), "utf8")).trim());
|
|
9843
|
+
} catch (err) {
|
|
9844
|
+
if (err.code !== "ENOENT")
|
|
9845
|
+
throw err;
|
|
9846
|
+
}
|
|
9847
|
+
const { privateKey } = generateKeyPairSync("x25519");
|
|
9848
|
+
const der = privateKey.export({ format: "der", type: "pkcs8" });
|
|
9849
|
+
const raw = der.subarray(der.length - 32).toString("base64");
|
|
9850
|
+
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
9851
|
+
await writeFile3(keyPath(dir), `${raw}
|
|
9852
|
+
`, { mode: 384 });
|
|
9853
|
+
return keyPairFromPrivate(raw);
|
|
9854
|
+
}
|
|
9855
|
+
async function readDeviceState(dir = networkDir()) {
|
|
9856
|
+
try {
|
|
9857
|
+
const parsed = JSON.parse(await readFile3(statePath(dir), "utf8"));
|
|
9858
|
+
if (typeof parsed.deviceId === "string" && typeof parsed.baseUrl === "string") {
|
|
9859
|
+
return {
|
|
9860
|
+
baseUrl: parsed.baseUrl,
|
|
9861
|
+
deviceId: parsed.deviceId,
|
|
9862
|
+
address: typeof parsed.address === "string" ? parsed.address : ""
|
|
9863
|
+
};
|
|
9864
|
+
}
|
|
9865
|
+
} catch {}
|
|
9866
|
+
return null;
|
|
9867
|
+
}
|
|
9868
|
+
async function writeDeviceState(state, dir = networkDir()) {
|
|
9869
|
+
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
9870
|
+
await writeFile3(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
9871
|
+
`, { mode: 384 });
|
|
9872
|
+
}
|
|
9873
|
+
function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
9874
|
+
if (platform === "darwin")
|
|
9875
|
+
return "macos-resolver";
|
|
9876
|
+
if (platform === "linux" && exists("/run/systemd/resolve"))
|
|
9877
|
+
return "resolved";
|
|
9878
|
+
return "none";
|
|
9879
|
+
}
|
|
9880
|
+
function renderConfig(input) {
|
|
9881
|
+
const { device, network, gateway } = input.registered;
|
|
9882
|
+
const zones = [network.zones.canonical, network.zones.short];
|
|
9883
|
+
const lines = [
|
|
9884
|
+
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9885
|
+
"[Interface]",
|
|
9886
|
+
`PrivateKey = ${input.privateKey}`,
|
|
9887
|
+
`Address = ${device.address}/128`,
|
|
9888
|
+
`MTU = ${network.mtu}`
|
|
9889
|
+
];
|
|
9890
|
+
if (input.dns === "macos-resolver") {
|
|
9891
|
+
const files = zones.map((zone) => `/etc/resolver/${zone}`);
|
|
9892
|
+
lines.push(`PostUp = mkdir -p /etc/resolver && ${files.map((file) => `printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`).join(" && ")}`, `PostDown = rm -f ${files.join(" ")}`);
|
|
9893
|
+
} else if (input.dns === "resolved") {
|
|
9894
|
+
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${zones.map((zone) => `'~${zone}'`).join(" ")}`);
|
|
9895
|
+
}
|
|
9896
|
+
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.platformPrefix}`, "PersistentKeepalive = 25", "");
|
|
9897
|
+
return lines.join(`
|
|
9898
|
+
`);
|
|
9899
|
+
}
|
|
9900
|
+
async function writeConfig(contents, path = configPath()) {
|
|
9901
|
+
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9902
|
+
await writeFile3(path, contents, { mode: 384 });
|
|
9903
|
+
await chmod2(path, 384);
|
|
9904
|
+
}
|
|
9905
|
+
function tunnelUp(platform = process.platform, exists = existsSync) {
|
|
9906
|
+
if (platform === "darwin")
|
|
9907
|
+
return exists(`/var/run/wireguard/${INTERFACE}.name`);
|
|
9908
|
+
return exists(`/sys/class/net/${INTERFACE}`);
|
|
9909
|
+
}
|
|
9910
|
+
function installHint(platform = process.platform) {
|
|
9911
|
+
if (platform === "darwin")
|
|
9912
|
+
return "brew install wireguard-tools";
|
|
9913
|
+
if (platform === "linux")
|
|
9914
|
+
return "sudo apt install wireguard-tools (or your distribution’s package)";
|
|
9915
|
+
return "Install wireguard-tools. On Windows, run the CLI inside WSL.";
|
|
9916
|
+
}
|
|
9917
|
+
|
|
9918
|
+
// src/commands/network.ts
|
|
9919
|
+
function defaultDeps() {
|
|
9920
|
+
return {
|
|
9921
|
+
request: authorizedRequest,
|
|
9922
|
+
run: (command, args) => new Promise((resolve, reject) => {
|
|
9923
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
9924
|
+
child.on("error", reject);
|
|
9925
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
9926
|
+
}),
|
|
9927
|
+
has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
9928
|
+
dir: networkDir(),
|
|
9929
|
+
up: () => tunnelUp(),
|
|
9930
|
+
log: console.log,
|
|
9931
|
+
error: printError,
|
|
9932
|
+
notice: printNotice
|
|
9933
|
+
};
|
|
9934
|
+
}
|
|
9935
|
+
function message(err) {
|
|
9936
|
+
return err instanceof Error ? err.message : String(err);
|
|
9937
|
+
}
|
|
9938
|
+
async function wgQuick(deps, verb) {
|
|
9939
|
+
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
9940
|
+
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
9941
|
+
return deps.run("sudo", args);
|
|
9942
|
+
}
|
|
9943
|
+
async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
9944
|
+
if (!deps.has("wg-quick")) {
|
|
9945
|
+
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
9946
|
+
return 1;
|
|
9947
|
+
}
|
|
9948
|
+
let registered;
|
|
9949
|
+
let privateKey;
|
|
9950
|
+
try {
|
|
9951
|
+
const key = await loadOrCreateKey(deps.dir);
|
|
9952
|
+
privateKey = key.privateKey;
|
|
9953
|
+
const found = await findOrganizationRef(args.org);
|
|
9954
|
+
registered = await deps.request("/api/v1/network/devices", {
|
|
9955
|
+
method: "POST",
|
|
9956
|
+
body: JSON.stringify({
|
|
9957
|
+
publicKey: key.publicKey,
|
|
9958
|
+
name: hostname2().slice(0, 64) || "device",
|
|
9959
|
+
...found ? { shortNameOrganization: found.ref } : {}
|
|
9960
|
+
})
|
|
9961
|
+
});
|
|
9962
|
+
} catch (err) {
|
|
9963
|
+
deps.error(message(err));
|
|
9964
|
+
return 1;
|
|
9965
|
+
}
|
|
9966
|
+
const dns = detectDnsMode();
|
|
9967
|
+
await writeConfig(renderConfig({ privateKey, registered, dns }), configPath(deps.dir));
|
|
9968
|
+
await writeDeviceState({ baseUrl: getBaseUrl(), deviceId: registered.device.id, address: registered.device.address }, deps.dir);
|
|
9969
|
+
if (deps.up()) {
|
|
9970
|
+
const down = await wgQuick(deps, "down");
|
|
9971
|
+
if (down !== 0) {
|
|
9972
|
+
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
9973
|
+
return 1;
|
|
9974
|
+
}
|
|
9975
|
+
}
|
|
9976
|
+
const code = await wgQuick(deps, "up");
|
|
9977
|
+
if (code !== 0) {
|
|
9978
|
+
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
9979
|
+
return 1;
|
|
9980
|
+
}
|
|
9981
|
+
const { device, network } = registered;
|
|
9982
|
+
printResult({ device, network, interface: INTERFACE, dns }, () => [
|
|
9983
|
+
eventLine("done", `Connected. This machine is ${device.address} in the private network.`),
|
|
9984
|
+
` Names: <sandbox>.<org>.${network.zones.canonical}` + (device.shortNameOrganizationId ? `, and <sandbox>.${network.zones.short}` : ""),
|
|
9985
|
+
...dns === "none" ? [
|
|
9986
|
+
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
9987
|
+
` \`${getCliName()} network ls\`.`
|
|
9988
|
+
] : [],
|
|
9989
|
+
` → ${getCliName()} network ls`
|
|
9990
|
+
], deps.log);
|
|
9991
|
+
return 0;
|
|
9992
|
+
}
|
|
9993
|
+
async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
9994
|
+
if (!deps.up()) {
|
|
9995
|
+
printResult({ connected: false }, () => [eventLine("item", "Not connected.")], deps.log);
|
|
9996
|
+
return 0;
|
|
9997
|
+
}
|
|
9998
|
+
const code = await wgQuick(deps, "down");
|
|
9999
|
+
if (code !== 0) {
|
|
10000
|
+
deps.error(`wg-quick exited ${code}.`);
|
|
10001
|
+
return 1;
|
|
10002
|
+
}
|
|
10003
|
+
printResult({ connected: false }, () => [eventLine("done", "Disconnected.")], deps.log);
|
|
10004
|
+
return 0;
|
|
10005
|
+
}
|
|
10006
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10007
|
+
const state = await readDeviceState(deps.dir);
|
|
10008
|
+
const connected = deps.up();
|
|
10009
|
+
let device = null;
|
|
10010
|
+
if (state) {
|
|
10011
|
+
try {
|
|
10012
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10013
|
+
device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10014
|
+
} catch (err) {
|
|
10015
|
+
deps.notice(`Could not ask the app about this device: ${message(err)}`);
|
|
10016
|
+
}
|
|
10017
|
+
}
|
|
10018
|
+
printResult({ connected, device }, () => {
|
|
10019
|
+
if (!state) {
|
|
10020
|
+
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10021
|
+
}
|
|
10022
|
+
const lines = [
|
|
10023
|
+
eventLine("item", `${connected ? "Connected" : "Not connected"}: ${state.address}`)
|
|
10024
|
+
];
|
|
10025
|
+
if (!device) {
|
|
10026
|
+
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10027
|
+
} else {
|
|
10028
|
+
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
10029
|
+
}
|
|
10030
|
+
return lines;
|
|
10031
|
+
}, deps.log);
|
|
10032
|
+
return 0;
|
|
10033
|
+
}
|
|
10034
|
+
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10035
|
+
try {
|
|
10036
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10037
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
|
|
10038
|
+
printResult(listed, () => {
|
|
10039
|
+
if (listed.sandboxes.length === 0) {
|
|
10040
|
+
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10041
|
+
}
|
|
10042
|
+
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10043
|
+
return listed.sandboxes.map((s) => ` ${s.name.padEnd(width)} ${s.address.padEnd(28)} ${s.canonicalName} ${s.status}`);
|
|
10044
|
+
}, deps.log);
|
|
10045
|
+
return 0;
|
|
10046
|
+
} catch (err) {
|
|
10047
|
+
deps.error(message(err));
|
|
10048
|
+
return 1;
|
|
10049
|
+
}
|
|
10050
|
+
}
|
|
10051
|
+
async function runNetworkUse(args, deps = defaultDeps()) {
|
|
10052
|
+
const state = await readDeviceState(deps.dir);
|
|
10053
|
+
if (!state) {
|
|
10054
|
+
deps.error(`Not set up. Run \`${getCliName()} network connect\` first.`);
|
|
10055
|
+
return 1;
|
|
10056
|
+
}
|
|
10057
|
+
const organization = args.clear ? null : args.org?.trim();
|
|
10058
|
+
if (organization === undefined || organization === "") {
|
|
10059
|
+
deps.error("Pass an organization id or slug, or --clear.");
|
|
10060
|
+
return 1;
|
|
10061
|
+
}
|
|
10062
|
+
try {
|
|
10063
|
+
const device = await deps.request(`/api/v1/network/devices/${encodeURIComponent(state.deviceId)}/short-name-organization`, { method: "PUT", body: JSON.stringify({ organization }) });
|
|
10064
|
+
printResult(device, () => [
|
|
10065
|
+
eventLine("done", organization === null ? "Short names are off for this machine." : `Short names (<sandbox>.gdock.internal) now resolve in ${organization}.`)
|
|
10066
|
+
], deps.log);
|
|
10067
|
+
return 0;
|
|
10068
|
+
} catch (err) {
|
|
10069
|
+
deps.error(message(err));
|
|
10070
|
+
return 1;
|
|
10071
|
+
}
|
|
10072
|
+
}
|
|
10073
|
+
async function runNetworkDevices(deps = defaultDeps()) {
|
|
10074
|
+
try {
|
|
10075
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10076
|
+
const own = (await readDeviceState(deps.dir))?.deviceId;
|
|
10077
|
+
printResult({ devices }, () => devices.length === 0 ? [eventLine("item", "No devices.")] : devices.map((d) => ` ${d.id === own ? "*" : " "} ${d.id} ${d.name} ${d.address} until ${d.expiresAt.slice(0, 10)}`), deps.log);
|
|
10078
|
+
return 0;
|
|
10079
|
+
} catch (err) {
|
|
10080
|
+
deps.error(message(err));
|
|
10081
|
+
return 1;
|
|
10082
|
+
}
|
|
10083
|
+
}
|
|
10084
|
+
async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
10085
|
+
try {
|
|
10086
|
+
await deps.request(`/api/v1/network/devices/${encodeURIComponent(args.id)}`, {
|
|
10087
|
+
method: "DELETE"
|
|
10088
|
+
});
|
|
10089
|
+
printResult({ removed: args.id }, () => [eventLine("done", `Removed device ${args.id}.`)], deps.log);
|
|
10090
|
+
return 0;
|
|
10091
|
+
} catch (err) {
|
|
10092
|
+
deps.error(message(err));
|
|
10093
|
+
return 1;
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
var connectCommand = defineCommand({
|
|
10097
|
+
meta: {
|
|
10098
|
+
name: "connect",
|
|
10099
|
+
description: "Put this machine into the private network: WireGuard and split DNS. Asks for sudo once."
|
|
10100
|
+
},
|
|
10101
|
+
args: { org: orgArg },
|
|
10102
|
+
async run({ args }) {
|
|
10103
|
+
process.exitCode = await runNetworkConnect({ org: args.org });
|
|
10104
|
+
}
|
|
10105
|
+
});
|
|
10106
|
+
var disconnectCommand = defineCommand({
|
|
10107
|
+
meta: { name: "disconnect", description: "Take this machine out of the private network." },
|
|
10108
|
+
async run() {
|
|
10109
|
+
process.exitCode = await runNetworkDisconnect();
|
|
10110
|
+
}
|
|
10111
|
+
});
|
|
10112
|
+
var statusCommand = defineCommand({
|
|
10113
|
+
meta: { name: "status", description: "Whether this machine is connected, and as which device." },
|
|
10114
|
+
async run() {
|
|
10115
|
+
process.exitCode = await runNetworkStatus();
|
|
10116
|
+
}
|
|
10117
|
+
});
|
|
10118
|
+
var lsCommand = defineCommand({
|
|
10119
|
+
meta: {
|
|
10120
|
+
name: "ls",
|
|
10121
|
+
description: "Sandboxes of an organization with their private addresses and names."
|
|
10122
|
+
},
|
|
10123
|
+
args: { org: orgArg },
|
|
10124
|
+
async run({ args }) {
|
|
10125
|
+
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10126
|
+
}
|
|
10127
|
+
});
|
|
10128
|
+
var useCommand = defineCommand({
|
|
10129
|
+
meta: {
|
|
10130
|
+
name: "use",
|
|
10131
|
+
description: "Choose the organization <sandbox>.gdock.internal names resolve in on this machine."
|
|
10132
|
+
},
|
|
10133
|
+
args: {
|
|
10134
|
+
org: { type: "positional", description: "Organization id or slug.", required: false },
|
|
10135
|
+
clear: { type: "boolean", description: "Turn short names off.", default: false }
|
|
10136
|
+
},
|
|
10137
|
+
async run({ args }) {
|
|
10138
|
+
process.exitCode = await runNetworkUse({ org: args.org, clear: args.clear });
|
|
10139
|
+
}
|
|
10140
|
+
});
|
|
10141
|
+
var devicesRemoveCommand = defineCommand({
|
|
10142
|
+
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10143
|
+
args: {
|
|
10144
|
+
id: { type: "positional", description: "Device id, from `network devices`.", required: true }
|
|
10145
|
+
},
|
|
10146
|
+
async run({ args }) {
|
|
10147
|
+
process.exitCode = await runNetworkDevicesRemove({ id: args.id });
|
|
10148
|
+
}
|
|
10149
|
+
});
|
|
10150
|
+
var devicesCommand = defineCommand({
|
|
10151
|
+
meta: { name: "devices", description: "Your devices in the private network." },
|
|
10152
|
+
subCommands: { remove: devicesRemoveCommand },
|
|
10153
|
+
async run({ rawArgs }) {
|
|
10154
|
+
if (rawArgs[0] === "remove")
|
|
10155
|
+
return;
|
|
10156
|
+
process.exitCode = await runNetworkDevices();
|
|
10157
|
+
}
|
|
10158
|
+
});
|
|
10159
|
+
var networkCommand = defineCommand({
|
|
10160
|
+
meta: {
|
|
10161
|
+
name: "network",
|
|
10162
|
+
description: "The private network: reach your sandboxes by address and name from this machine."
|
|
10163
|
+
},
|
|
10164
|
+
subCommands: {
|
|
10165
|
+
connect: connectCommand,
|
|
10166
|
+
disconnect: disconnectCommand,
|
|
10167
|
+
status: statusCommand,
|
|
10168
|
+
ls: lsCommand,
|
|
10169
|
+
use: useCommand,
|
|
10170
|
+
devices: devicesCommand
|
|
10171
|
+
}
|
|
10172
|
+
});
|
|
10173
|
+
|
|
9686
10174
|
// src/commands/organizations.ts
|
|
9687
10175
|
async function runOrganizationsUse(args, log = console.log, error = printError) {
|
|
9688
10176
|
const settings = await readSettings();
|
|
@@ -9754,8 +10242,8 @@ var organizationsCurrentCommand = defineCommand({
|
|
|
9754
10242
|
});
|
|
9755
10243
|
|
|
9756
10244
|
// src/commands/publish-auto.ts
|
|
9757
|
-
import { readdir, readFile as
|
|
9758
|
-
import { basename as basename2, join as
|
|
10245
|
+
import { readdir, readFile as readFile4, stat } from "node:fs/promises";
|
|
10246
|
+
import { basename as basename2, join as join4, relative, sep } from "node:path";
|
|
9759
10247
|
function publishAutoLines(result) {
|
|
9760
10248
|
const [summaryLine, ...rest] = formatPublishAutoResult(result).split(`
|
|
9761
10249
|
`);
|
|
@@ -9832,7 +10320,7 @@ function formatAttemptedSpec(spec) {
|
|
|
9832
10320
|
async function collectPath(path) {
|
|
9833
10321
|
const pathStats = await stat(path);
|
|
9834
10322
|
if (pathStats.isFile()) {
|
|
9835
|
-
return [{ path: basename2(path), data: await
|
|
10323
|
+
return [{ path: basename2(path), data: await readFile4(path) }];
|
|
9836
10324
|
}
|
|
9837
10325
|
if (!pathStats.isDirectory()) {
|
|
9838
10326
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
@@ -9842,13 +10330,13 @@ async function collectPath(path) {
|
|
|
9842
10330
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
9843
10331
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
9844
10332
|
for (const entry of entries) {
|
|
9845
|
-
const entryPath =
|
|
10333
|
+
const entryPath = join4(currentPath, entry.name);
|
|
9846
10334
|
if (entry.isDirectory()) {
|
|
9847
10335
|
await visit(entryPath);
|
|
9848
10336
|
} else if (entry.isFile()) {
|
|
9849
10337
|
files.push({
|
|
9850
10338
|
path: relative(path, entryPath).split(sep).join("/"),
|
|
9851
|
-
data: await
|
|
10339
|
+
data: await readFile4(entryPath)
|
|
9852
10340
|
});
|
|
9853
10341
|
}
|
|
9854
10342
|
}
|
|
@@ -9891,7 +10379,7 @@ async function runPublishAuto(options, log = console.log, error = printError) {
|
|
|
9891
10379
|
}
|
|
9892
10380
|
}
|
|
9893
10381
|
if (options.zip) {
|
|
9894
|
-
form.append("zip", new Blob([await
|
|
10382
|
+
form.append("zip", new Blob([await readFile4(options.zip)]), basename2(options.zip));
|
|
9895
10383
|
}
|
|
9896
10384
|
const { value: result } = await withProgress("Planning and deploying… (this can take a few minutes)", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/publish-auto`, { method: "POST", body: form }));
|
|
9897
10385
|
printResult(result, () => publishAutoLines(result), log);
|
|
@@ -10014,12 +10502,12 @@ var publishAutoCommand = defineCommand({
|
|
|
10014
10502
|
});
|
|
10015
10503
|
|
|
10016
10504
|
// src/commands/publish-static.ts
|
|
10017
|
-
import { readdir as readdir2, readFile as
|
|
10018
|
-
import { basename as basename3, join as
|
|
10505
|
+
import { readdir as readdir2, readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
10506
|
+
import { basename as basename3, join as join5, relative as relative2, sep as sep2 } from "node:path";
|
|
10019
10507
|
async function collectPath2(path) {
|
|
10020
10508
|
const pathStats = await stat2(path);
|
|
10021
10509
|
if (pathStats.isFile()) {
|
|
10022
|
-
return [{ path: basename3(path), data: await
|
|
10510
|
+
return [{ path: basename3(path), data: await readFile5(path) }];
|
|
10023
10511
|
}
|
|
10024
10512
|
if (!pathStats.isDirectory()) {
|
|
10025
10513
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
@@ -10029,13 +10517,13 @@ async function collectPath2(path) {
|
|
|
10029
10517
|
const entries = await readdir2(currentPath, { withFileTypes: true });
|
|
10030
10518
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
10031
10519
|
for (const entry of entries) {
|
|
10032
|
-
const entryPath =
|
|
10520
|
+
const entryPath = join5(currentPath, entry.name);
|
|
10033
10521
|
if (entry.isDirectory()) {
|
|
10034
10522
|
await visit(entryPath);
|
|
10035
10523
|
} else if (entry.isFile()) {
|
|
10036
10524
|
files.push({
|
|
10037
10525
|
path: relative2(path, entryPath).split(sep2).join("/"),
|
|
10038
|
-
data: await
|
|
10526
|
+
data: await readFile5(entryPath)
|
|
10039
10527
|
});
|
|
10040
10528
|
}
|
|
10041
10529
|
}
|
|
@@ -10058,7 +10546,7 @@ async function runPublishStatic(options, log = console.log, error = printError)
|
|
|
10058
10546
|
}
|
|
10059
10547
|
}
|
|
10060
10548
|
if (options.zip) {
|
|
10061
|
-
form.append("zip", new Blob([await
|
|
10549
|
+
form.append("zip", new Blob([await readFile5(options.zip)]), basename3(options.zip));
|
|
10062
10550
|
}
|
|
10063
10551
|
const { value: result } = await withProgress("Publishing…", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/static-sites`, { method: "POST", body: form }));
|
|
10064
10552
|
printResult(result, () => [eventLine("url", result.url), eventLine("done", `Published site ${result.siteId}.`)], log);
|
|
@@ -10112,7 +10600,7 @@ var publishStaticCommand = defineCommand({
|
|
|
10112
10600
|
});
|
|
10113
10601
|
|
|
10114
10602
|
// src/commands/registry.ts
|
|
10115
|
-
import { spawn } from "node:child_process";
|
|
10603
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
10116
10604
|
function reportError2(err, error) {
|
|
10117
10605
|
if (err instanceof AuthRequiredError || err instanceof Error) {
|
|
10118
10606
|
error(err.message);
|
|
@@ -10129,7 +10617,7 @@ function defaultInvoke(input) {
|
|
|
10129
10617
|
}
|
|
10130
10618
|
function dockerLogin(input) {
|
|
10131
10619
|
return new Promise((resolve, reject) => {
|
|
10132
|
-
const child =
|
|
10620
|
+
const child = spawn2("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
|
|
10133
10621
|
stdio: ["pipe", "inherit", "pipe"]
|
|
10134
10622
|
});
|
|
10135
10623
|
let stderr = "";
|
|
@@ -10227,8 +10715,8 @@ var registryCommand = defineCommand({
|
|
|
10227
10715
|
});
|
|
10228
10716
|
|
|
10229
10717
|
// src/commands/sandbox-create.ts
|
|
10230
|
-
import { readdir as readdir3, readFile as
|
|
10231
|
-
import { basename as basename4, join as
|
|
10718
|
+
import { readdir as readdir3, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
10719
|
+
import { basename as basename4, join as join6, relative as relative3, sep as sep3 } from "node:path";
|
|
10232
10720
|
|
|
10233
10721
|
// ../../node_modules/fflate/esm/index.mjs
|
|
10234
10722
|
import { createRequire } from "module";
|
|
@@ -10884,13 +11372,13 @@ async function collectDirectory(path) {
|
|
|
10884
11372
|
const entries = await readdir3(currentPath, { withFileTypes: true });
|
|
10885
11373
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
10886
11374
|
for (const entry of entries) {
|
|
10887
|
-
const entryPath =
|
|
11375
|
+
const entryPath = join6(currentPath, entry.name);
|
|
10888
11376
|
if (entry.isDirectory()) {
|
|
10889
11377
|
await visit(entryPath);
|
|
10890
11378
|
} else if (entry.isFile()) {
|
|
10891
11379
|
files.push({
|
|
10892
11380
|
path: relative3(path, entryPath).split(sep3).join("/"),
|
|
10893
|
-
data: await
|
|
11381
|
+
data: await readFile6(entryPath)
|
|
10894
11382
|
});
|
|
10895
11383
|
}
|
|
10896
11384
|
}
|
|
@@ -10923,7 +11411,7 @@ async function runSandboxCreate(options, log = console.log, error = printError)
|
|
|
10923
11411
|
return 1;
|
|
10924
11412
|
}
|
|
10925
11413
|
try {
|
|
10926
|
-
const zipBytes = options.zip ? new Uint8Array(await
|
|
11414
|
+
const zipBytes = options.zip ? new Uint8Array(await readFile6(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
|
|
10927
11415
|
const form = new FormData;
|
|
10928
11416
|
form.append("kind", options.kind);
|
|
10929
11417
|
if (options.displayName)
|
|
@@ -11184,12 +11672,12 @@ var toolsCommand = defineCommand({
|
|
|
11184
11672
|
});
|
|
11185
11673
|
|
|
11186
11674
|
// src/commands/upload-files.ts
|
|
11187
|
-
import { readdir as readdir4, readFile as
|
|
11188
|
-
import { basename as basename5, join as
|
|
11675
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
11676
|
+
import { basename as basename5, join as join7, relative as relative4, sep as sep4 } from "node:path";
|
|
11189
11677
|
async function collectPath3(path) {
|
|
11190
11678
|
const pathStats = await stat4(path);
|
|
11191
11679
|
if (pathStats.isFile()) {
|
|
11192
|
-
return [{ path: basename5(path), data: await
|
|
11680
|
+
return [{ path: basename5(path), data: await readFile7(path) }];
|
|
11193
11681
|
}
|
|
11194
11682
|
if (!pathStats.isDirectory()) {
|
|
11195
11683
|
throw new Error(`Upload path must be a file or directory: ${path}`);
|
|
@@ -11199,13 +11687,13 @@ async function collectPath3(path) {
|
|
|
11199
11687
|
const entries = await readdir4(currentPath, { withFileTypes: true });
|
|
11200
11688
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11201
11689
|
for (const entry of entries) {
|
|
11202
|
-
const entryPath =
|
|
11690
|
+
const entryPath = join7(currentPath, entry.name);
|
|
11203
11691
|
if (entry.isDirectory()) {
|
|
11204
11692
|
await visit(entryPath);
|
|
11205
11693
|
} else if (entry.isFile()) {
|
|
11206
11694
|
files.push({
|
|
11207
11695
|
path: relative4(path, entryPath).split(sep4).join("/"),
|
|
11208
|
-
data: await
|
|
11696
|
+
data: await readFile7(entryPath)
|
|
11209
11697
|
});
|
|
11210
11698
|
}
|
|
11211
11699
|
}
|
|
@@ -11227,7 +11715,7 @@ async function runSandboxUpload(options, log = console.log, error = printError)
|
|
|
11227
11715
|
}
|
|
11228
11716
|
}
|
|
11229
11717
|
if (options.zip) {
|
|
11230
|
-
form.append("zip", new Blob([await
|
|
11718
|
+
form.append("zip", new Blob([await readFile7(options.zip)]), basename5(options.zip));
|
|
11231
11719
|
}
|
|
11232
11720
|
const { value: result } = await withProgress("Uploading…", () => authorizedRequest(`/api/v1/sandboxes/${encodeURIComponent(options.sandboxId)}/files?organization=${encodeURIComponent(organization)}`, { method: "POST", body: form }));
|
|
11233
11721
|
printResult(result, () => [
|
|
@@ -11340,7 +11828,8 @@ var HANDWRITTEN_COMMANDS = [
|
|
|
11340
11828
|
[["sandboxes", "build"], sandboxCreateCommand],
|
|
11341
11829
|
[["sandboxes", "files", "upload"], sandboxUploadCommand],
|
|
11342
11830
|
[["sites", "publish"], publishStaticCommand],
|
|
11343
|
-
[["projects", "publish"], publishAutoCommand]
|
|
11831
|
+
[["projects", "publish"], publishAutoCommand],
|
|
11832
|
+
[["network"], networkCommand]
|
|
11344
11833
|
];
|
|
11345
11834
|
var HANDWRITTEN_PATHS = new Set(HANDWRITTEN_COMMANDS.map(([path]) => path.join(" ")));
|
|
11346
11835
|
function createMainCommand(catalog = OFFLINE_TOOL_CATALOG) {
|