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.
@@ -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 miss fails with the missing names. 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.
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>` | URLs to mint after create. Omit to mint the manifest’s URLs; when given, exactly this list is minted and must include every required port. TCP URLs are always public. (JSON array) |
765
- | `--mcps <array>` | MCP exposures to mint after create. Omit to apply manifest MCPs. When provided, exactly this list is minted; required manifest names (requiredMcps) must be included. (JSON array) |
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 access.
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 or public setting. TCP cannot be private.
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 access. |
605
- | `ghostdock sandboxes urls update <url-id> [flags]` | Change a sandbox URL’s port, protocol, autoWake or public setting. |
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 miss fails with the missing names. 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.",
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: "URLs to mint after create. Omit to mint the manifest’s URLs; when given, exactly this list is minted and must include every required port. TCP URLs are always public.",
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
- type: "object",
848
- properties: {
849
- port: {
850
- type: "integer",
851
- minimum: 1,
852
- maximum: 65535
853
- },
854
- protocol: {
855
- type: "string",
856
- enum: [
857
- "http",
858
- "tcp"
859
- ]
860
- },
861
- autoWake: {
862
- type: "boolean"
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
- public: {
865
- type: "boolean"
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 to mint after create. Omit to apply manifest MCPs. When provided, exactly this list is minted; required manifest names (requiredMcps) must be included.",
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
- type: "object",
882
- properties: {
883
- name: {
884
- type: "string",
885
- maxLength: 64,
886
- pattern: "^[A-Za-z][A-Za-z0-9_-]*$"
887
- },
888
- port: {
889
- type: "integer",
890
- minimum: 1,
891
- maximum: 65535
892
- },
893
- path: {
894
- type: "string"
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
- autoWake: {
897
- type: "boolean"
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 access.",
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 or public setting. TCP cannot be private.",
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.8";
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 readFile3, stat } from "node:fs/promises";
9758
- import { basename as basename2, join as join3, relative, sep } from "node:path";
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 readFile3(path) }];
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 = join3(currentPath, entry.name);
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 readFile3(entryPath)
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 readFile3(options.zip)]), basename2(options.zip));
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 readFile4, stat as stat2 } from "node:fs/promises";
10018
- import { basename as basename3, join as join4, relative as relative2, sep as sep2 } from "node:path";
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 readFile4(path) }];
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 = join4(currentPath, entry.name);
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 readFile4(entryPath)
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 readFile4(options.zip)]), basename3(options.zip));
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 = spawn("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
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 readFile5, stat as stat3 } from "node:fs/promises";
10231
- import { basename as basename4, join as join5, relative as relative3, sep as sep3 } from "node:path";
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 = join5(currentPath, entry.name);
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 readFile5(entryPath)
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 readFile5(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
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 readFile6, stat as stat4 } from "node:fs/promises";
11188
- import { basename as basename5, join as join6, relative as relative4, sep as sep4 } from "node:path";
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 readFile6(path) }];
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 = join6(currentPath, entry.name);
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 readFile6(entryPath)
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 readFile6(options.zip)]), basename5(options.zip));
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghostdock",
3
3
  "description": "Ghostdock CLI — device login and agent tool invocation from the command line",
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ghostdock": "dist/index.js",