ghostdock 0.0.8 → 0.0.10

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
  }
@@ -8441,12 +8555,16 @@ var BUILTIN_SKILL_NAMES = GENERATED_SKILL_NAMES;
8441
8555
  // src/cli-name.ts
8442
8556
  import { basename } from "node:path";
8443
8557
  var CLI_NAMES = ["ghostdock", "gdock"];
8558
+ var NPX_CACHE_SEGMENT = /[\\/]_npx[\\/]/;
8444
8559
  function getCliName(argv = process.argv) {
8445
- const invoked = basename(argv[1] ?? "");
8560
+ const entry = argv[1] ?? "";
8561
+ if (NPX_CACHE_SEGMENT.test(entry))
8562
+ return "npx ghostdock";
8563
+ const invoked = basename(entry);
8446
8564
  return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
8447
8565
  }
8448
8566
  // package.json
8449
- var version = "0.0.8";
8567
+ var version = "0.0.10";
8450
8568
  var package_default = {
8451
8569
  name: "ghostdock",
8452
8570
  description: "Ghostdock CLI — device login and agent tool invocation from the command line",
@@ -9683,6 +9801,380 @@ var logoutCommand = defineCommand({
9683
9801
  }
9684
9802
  });
9685
9803
 
9804
+ // src/commands/network.ts
9805
+ import { spawn, spawnSync } from "node:child_process";
9806
+ import { hostname as hostname2 } from "node:os";
9807
+
9808
+ // src/network/tunnel.ts
9809
+ import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
9810
+ import { existsSync } from "node:fs";
9811
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
9812
+ import { homedir } from "node:os";
9813
+ import { dirname as dirname3, join as join3 } from "node:path";
9814
+ var INTERFACE = "gdock";
9815
+ function networkDir() {
9816
+ const override = process.env.GHOSTDOCK_NETWORK_DIR?.trim();
9817
+ if (override)
9818
+ return override;
9819
+ const base = process.env.XDG_CONFIG_HOME?.trim() || join3(homedir(), ".config");
9820
+ return join3(base, "ghostdock", "network");
9821
+ }
9822
+ function configPath(dir = networkDir()) {
9823
+ return join3(dir, `${INTERFACE}.conf`);
9824
+ }
9825
+ function keyPath(dir) {
9826
+ return join3(dir, "private.key");
9827
+ }
9828
+ function statePath(dir) {
9829
+ return join3(dir, "device.json");
9830
+ }
9831
+ var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
9832
+ function keyPairFromPrivate(privateKey) {
9833
+ const raw = Buffer.from(privateKey, "base64");
9834
+ if (raw.length !== 32)
9835
+ throw new Error("The stored WireGuard key is not 32 bytes.");
9836
+ const key = createPrivateKey({
9837
+ key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
9838
+ format: "der",
9839
+ type: "pkcs8"
9840
+ });
9841
+ const spki = createPublicKey(key).export({ format: "der", type: "spki" });
9842
+ return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
9843
+ }
9844
+ async function loadOrCreateKey(dir = networkDir()) {
9845
+ try {
9846
+ return keyPairFromPrivate((await readFile3(keyPath(dir), "utf8")).trim());
9847
+ } catch (err) {
9848
+ if (err.code !== "ENOENT")
9849
+ throw err;
9850
+ }
9851
+ const { privateKey } = generateKeyPairSync("x25519");
9852
+ const der = privateKey.export({ format: "der", type: "pkcs8" });
9853
+ const raw = der.subarray(der.length - 32).toString("base64");
9854
+ await mkdir3(dir, { recursive: true, mode: 448 });
9855
+ await writeFile3(keyPath(dir), `${raw}
9856
+ `, { mode: 384 });
9857
+ return keyPairFromPrivate(raw);
9858
+ }
9859
+ async function readDeviceState(dir = networkDir()) {
9860
+ try {
9861
+ const parsed = JSON.parse(await readFile3(statePath(dir), "utf8"));
9862
+ if (typeof parsed.deviceId === "string" && typeof parsed.baseUrl === "string") {
9863
+ return {
9864
+ baseUrl: parsed.baseUrl,
9865
+ deviceId: parsed.deviceId,
9866
+ address: typeof parsed.address === "string" ? parsed.address : ""
9867
+ };
9868
+ }
9869
+ } catch {}
9870
+ return null;
9871
+ }
9872
+ async function writeDeviceState(state, dir = networkDir()) {
9873
+ await mkdir3(dir, { recursive: true, mode: 448 });
9874
+ await writeFile3(statePath(dir), `${JSON.stringify(state, null, 2)}
9875
+ `, { mode: 384 });
9876
+ }
9877
+ function detectDnsMode(platform = process.platform, exists = existsSync) {
9878
+ if (platform === "darwin")
9879
+ return "macos-resolver";
9880
+ if (platform === "linux" && exists("/run/systemd/resolve"))
9881
+ return "resolved";
9882
+ return "none";
9883
+ }
9884
+ function renderConfig(input) {
9885
+ const { device, network, gateway } = input.registered;
9886
+ const zones = [network.zones.canonical, network.zones.short];
9887
+ const lines = [
9888
+ "# Written by `ghostdock network connect`. Run it again rather than editing this file.",
9889
+ "[Interface]",
9890
+ `PrivateKey = ${input.privateKey}`,
9891
+ `Address = ${device.address}/128`,
9892
+ `MTU = ${network.mtu}`
9893
+ ];
9894
+ if (input.dns === "macos-resolver") {
9895
+ const files = zones.map((zone) => `/etc/resolver/${zone}`);
9896
+ lines.push(`PostUp = mkdir -p /etc/resolver && ${files.map((file) => `printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`).join(" && ")}`, `PostDown = rm -f ${files.join(" ")}`);
9897
+ } else if (input.dns === "resolved") {
9898
+ lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${zones.map((zone) => `'~${zone}'`).join(" ")}`);
9899
+ }
9900
+ lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.platformPrefix}`, "PersistentKeepalive = 25", "");
9901
+ return lines.join(`
9902
+ `);
9903
+ }
9904
+ async function writeConfig(contents, path = configPath()) {
9905
+ await mkdir3(dirname3(path), { recursive: true, mode: 448 });
9906
+ await writeFile3(path, contents, { mode: 384 });
9907
+ await chmod2(path, 384);
9908
+ }
9909
+ function tunnelUp(platform = process.platform, exists = existsSync) {
9910
+ if (platform === "darwin")
9911
+ return exists(`/var/run/wireguard/${INTERFACE}.name`);
9912
+ return exists(`/sys/class/net/${INTERFACE}`);
9913
+ }
9914
+ function installHint(platform = process.platform) {
9915
+ if (platform === "darwin")
9916
+ return "brew install wireguard-tools";
9917
+ if (platform === "linux")
9918
+ return "sudo apt install wireguard-tools (or your distribution’s package)";
9919
+ return "Install wireguard-tools. On Windows, run the CLI inside WSL.";
9920
+ }
9921
+
9922
+ // src/commands/network.ts
9923
+ function defaultDeps() {
9924
+ return {
9925
+ request: authorizedRequest,
9926
+ run: (command, args) => new Promise((resolve, reject) => {
9927
+ const child = spawn(command, args, { stdio: "inherit" });
9928
+ child.on("error", reject);
9929
+ child.on("close", (code) => resolve(code ?? 1));
9930
+ }),
9931
+ has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
9932
+ dir: networkDir(),
9933
+ up: () => tunnelUp(),
9934
+ log: console.log,
9935
+ error: printError,
9936
+ notice: printNotice
9937
+ };
9938
+ }
9939
+ function message(err) {
9940
+ return err instanceof Error ? err.message : String(err);
9941
+ }
9942
+ async function wgQuick(deps, verb) {
9943
+ const args = ["wg-quick", verb, configPath(deps.dir)];
9944
+ deps.notice(`Running: sudo ${args.join(" ")}`);
9945
+ return deps.run("sudo", args);
9946
+ }
9947
+ async function runNetworkConnect(args, deps = defaultDeps()) {
9948
+ if (!deps.has("wg-quick")) {
9949
+ deps.error(`wg-quick is not installed. ${installHint()}`);
9950
+ return 1;
9951
+ }
9952
+ let registered;
9953
+ let privateKey;
9954
+ try {
9955
+ const key = await loadOrCreateKey(deps.dir);
9956
+ privateKey = key.privateKey;
9957
+ const found = await findOrganizationRef(args.org);
9958
+ registered = await deps.request("/api/v1/network/devices", {
9959
+ method: "POST",
9960
+ body: JSON.stringify({
9961
+ publicKey: key.publicKey,
9962
+ name: hostname2().slice(0, 64) || "device",
9963
+ ...found ? { shortNameOrganization: found.ref } : {}
9964
+ })
9965
+ });
9966
+ } catch (err) {
9967
+ deps.error(message(err));
9968
+ return 1;
9969
+ }
9970
+ const dns = detectDnsMode();
9971
+ await writeConfig(renderConfig({ privateKey, registered, dns }), configPath(deps.dir));
9972
+ await writeDeviceState({ baseUrl: getBaseUrl(), deviceId: registered.device.id, address: registered.device.address }, deps.dir);
9973
+ if (deps.up()) {
9974
+ const down = await wgQuick(deps, "down");
9975
+ if (down !== 0) {
9976
+ deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
9977
+ return 1;
9978
+ }
9979
+ }
9980
+ const code = await wgQuick(deps, "up");
9981
+ if (code !== 0) {
9982
+ deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
9983
+ return 1;
9984
+ }
9985
+ const { device, network } = registered;
9986
+ printResult({ device, network, interface: INTERFACE, dns }, () => [
9987
+ eventLine("done", `Connected. This machine is ${device.address} in the private network.`),
9988
+ ` Names: <sandbox>.<org>.${network.zones.canonical}` + (device.shortNameOrganizationId ? `, and <sandbox>.${network.zones.short}` : ""),
9989
+ ...dns === "none" ? [
9990
+ " No split DNS on this machine (no systemd-resolved): use the addresses from",
9991
+ ` \`${getCliName()} network ls\`.`
9992
+ ] : [],
9993
+ ` → ${getCliName()} network ls`
9994
+ ], deps.log);
9995
+ return 0;
9996
+ }
9997
+ async function runNetworkDisconnect(deps = defaultDeps()) {
9998
+ if (!deps.up()) {
9999
+ printResult({ connected: false }, () => [eventLine("item", "Not connected.")], deps.log);
10000
+ return 0;
10001
+ }
10002
+ const code = await wgQuick(deps, "down");
10003
+ if (code !== 0) {
10004
+ deps.error(`wg-quick exited ${code}.`);
10005
+ return 1;
10006
+ }
10007
+ printResult({ connected: false }, () => [eventLine("done", "Disconnected.")], deps.log);
10008
+ return 0;
10009
+ }
10010
+ async function runNetworkStatus(deps = defaultDeps()) {
10011
+ const state = await readDeviceState(deps.dir);
10012
+ const connected = deps.up();
10013
+ let device = null;
10014
+ if (state) {
10015
+ try {
10016
+ const { devices } = await deps.request("/api/v1/network/devices");
10017
+ device = devices.find((d) => d.id === state.deviceId) ?? null;
10018
+ } catch (err) {
10019
+ deps.notice(`Could not ask the app about this device: ${message(err)}`);
10020
+ }
10021
+ }
10022
+ printResult({ connected, device }, () => {
10023
+ if (!state) {
10024
+ return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
10025
+ }
10026
+ const lines = [
10027
+ eventLine("item", `${connected ? "Connected" : "Not connected"}: ${state.address}`)
10028
+ ];
10029
+ if (!device) {
10030
+ lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
10031
+ } else {
10032
+ lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
10033
+ }
10034
+ return lines;
10035
+ }, deps.log);
10036
+ return 0;
10037
+ }
10038
+ async function runNetworkLs(args, deps = defaultDeps()) {
10039
+ try {
10040
+ const organization = await requireOrganizationRef(args.org, deps.notice);
10041
+ const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
10042
+ printResult(listed, () => {
10043
+ if (listed.sandboxes.length === 0) {
10044
+ return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
10045
+ }
10046
+ const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
10047
+ return listed.sandboxes.map((s) => ` ${s.name.padEnd(width)} ${s.address.padEnd(28)} ${s.canonicalName} ${s.status}`);
10048
+ }, deps.log);
10049
+ return 0;
10050
+ } catch (err) {
10051
+ deps.error(message(err));
10052
+ return 1;
10053
+ }
10054
+ }
10055
+ async function runNetworkUse(args, deps = defaultDeps()) {
10056
+ const state = await readDeviceState(deps.dir);
10057
+ if (!state) {
10058
+ deps.error(`Not set up. Run \`${getCliName()} network connect\` first.`);
10059
+ return 1;
10060
+ }
10061
+ const organization = args.clear ? null : args.org?.trim();
10062
+ if (organization === undefined || organization === "") {
10063
+ deps.error("Pass an organization id or slug, or --clear.");
10064
+ return 1;
10065
+ }
10066
+ try {
10067
+ const device = await deps.request(`/api/v1/network/devices/${encodeURIComponent(state.deviceId)}/short-name-organization`, { method: "PUT", body: JSON.stringify({ organization }) });
10068
+ printResult(device, () => [
10069
+ eventLine("done", organization === null ? "Short names are off for this machine." : `Short names (<sandbox>.gdock.internal) now resolve in ${organization}.`)
10070
+ ], deps.log);
10071
+ return 0;
10072
+ } catch (err) {
10073
+ deps.error(message(err));
10074
+ return 1;
10075
+ }
10076
+ }
10077
+ async function runNetworkDevices(deps = defaultDeps()) {
10078
+ try {
10079
+ const { devices } = await deps.request("/api/v1/network/devices");
10080
+ const own = (await readDeviceState(deps.dir))?.deviceId;
10081
+ 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);
10082
+ return 0;
10083
+ } catch (err) {
10084
+ deps.error(message(err));
10085
+ return 1;
10086
+ }
10087
+ }
10088
+ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
10089
+ try {
10090
+ await deps.request(`/api/v1/network/devices/${encodeURIComponent(args.id)}`, {
10091
+ method: "DELETE"
10092
+ });
10093
+ printResult({ removed: args.id }, () => [eventLine("done", `Removed device ${args.id}.`)], deps.log);
10094
+ return 0;
10095
+ } catch (err) {
10096
+ deps.error(message(err));
10097
+ return 1;
10098
+ }
10099
+ }
10100
+ var connectCommand = defineCommand({
10101
+ meta: {
10102
+ name: "connect",
10103
+ description: "Put this machine into the private network: WireGuard and split DNS. Asks for sudo once."
10104
+ },
10105
+ args: { org: orgArg },
10106
+ async run({ args }) {
10107
+ process.exitCode = await runNetworkConnect({ org: args.org });
10108
+ }
10109
+ });
10110
+ var disconnectCommand = defineCommand({
10111
+ meta: { name: "disconnect", description: "Take this machine out of the private network." },
10112
+ async run() {
10113
+ process.exitCode = await runNetworkDisconnect();
10114
+ }
10115
+ });
10116
+ var statusCommand = defineCommand({
10117
+ meta: { name: "status", description: "Whether this machine is connected, and as which device." },
10118
+ async run() {
10119
+ process.exitCode = await runNetworkStatus();
10120
+ }
10121
+ });
10122
+ var lsCommand = defineCommand({
10123
+ meta: {
10124
+ name: "ls",
10125
+ description: "Sandboxes of an organization with their private addresses and names."
10126
+ },
10127
+ args: { org: orgArg },
10128
+ async run({ args }) {
10129
+ process.exitCode = await runNetworkLs({ org: args.org });
10130
+ }
10131
+ });
10132
+ var useCommand = defineCommand({
10133
+ meta: {
10134
+ name: "use",
10135
+ description: "Choose the organization <sandbox>.gdock.internal names resolve in on this machine."
10136
+ },
10137
+ args: {
10138
+ org: { type: "positional", description: "Organization id or slug.", required: false },
10139
+ clear: { type: "boolean", description: "Turn short names off.", default: false }
10140
+ },
10141
+ async run({ args }) {
10142
+ process.exitCode = await runNetworkUse({ org: args.org, clear: args.clear });
10143
+ }
10144
+ });
10145
+ var devicesRemoveCommand = defineCommand({
10146
+ meta: { name: "remove", description: "Remove one of your devices from the private network." },
10147
+ args: {
10148
+ id: { type: "positional", description: "Device id, from `network devices`.", required: true }
10149
+ },
10150
+ async run({ args }) {
10151
+ process.exitCode = await runNetworkDevicesRemove({ id: args.id });
10152
+ }
10153
+ });
10154
+ var devicesCommand = defineCommand({
10155
+ meta: { name: "devices", description: "Your devices in the private network." },
10156
+ subCommands: { remove: devicesRemoveCommand },
10157
+ async run({ rawArgs }) {
10158
+ if (rawArgs[0] === "remove")
10159
+ return;
10160
+ process.exitCode = await runNetworkDevices();
10161
+ }
10162
+ });
10163
+ var networkCommand = defineCommand({
10164
+ meta: {
10165
+ name: "network",
10166
+ description: "The private network: reach your sandboxes by address and name from this machine."
10167
+ },
10168
+ subCommands: {
10169
+ connect: connectCommand,
10170
+ disconnect: disconnectCommand,
10171
+ status: statusCommand,
10172
+ ls: lsCommand,
10173
+ use: useCommand,
10174
+ devices: devicesCommand
10175
+ }
10176
+ });
10177
+
9686
10178
  // src/commands/organizations.ts
9687
10179
  async function runOrganizationsUse(args, log = console.log, error = printError) {
9688
10180
  const settings = await readSettings();
@@ -9754,8 +10246,8 @@ var organizationsCurrentCommand = defineCommand({
9754
10246
  });
9755
10247
 
9756
10248
  // 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";
10249
+ import { readdir, readFile as readFile4, stat } from "node:fs/promises";
10250
+ import { basename as basename2, join as join4, relative, sep } from "node:path";
9759
10251
  function publishAutoLines(result) {
9760
10252
  const [summaryLine, ...rest] = formatPublishAutoResult(result).split(`
9761
10253
  `);
@@ -9832,7 +10324,7 @@ function formatAttemptedSpec(spec) {
9832
10324
  async function collectPath(path) {
9833
10325
  const pathStats = await stat(path);
9834
10326
  if (pathStats.isFile()) {
9835
- return [{ path: basename2(path), data: await readFile3(path) }];
10327
+ return [{ path: basename2(path), data: await readFile4(path) }];
9836
10328
  }
9837
10329
  if (!pathStats.isDirectory()) {
9838
10330
  throw new Error(`Publish path must be a file or directory: ${path}`);
@@ -9842,13 +10334,13 @@ async function collectPath(path) {
9842
10334
  const entries = await readdir(currentPath, { withFileTypes: true });
9843
10335
  entries.sort((a, b) => a.name.localeCompare(b.name));
9844
10336
  for (const entry of entries) {
9845
- const entryPath = join3(currentPath, entry.name);
10337
+ const entryPath = join4(currentPath, entry.name);
9846
10338
  if (entry.isDirectory()) {
9847
10339
  await visit(entryPath);
9848
10340
  } else if (entry.isFile()) {
9849
10341
  files.push({
9850
10342
  path: relative(path, entryPath).split(sep).join("/"),
9851
- data: await readFile3(entryPath)
10343
+ data: await readFile4(entryPath)
9852
10344
  });
9853
10345
  }
9854
10346
  }
@@ -9891,7 +10383,7 @@ async function runPublishAuto(options, log = console.log, error = printError) {
9891
10383
  }
9892
10384
  }
9893
10385
  if (options.zip) {
9894
- form.append("zip", new Blob([await readFile3(options.zip)]), basename2(options.zip));
10386
+ form.append("zip", new Blob([await readFile4(options.zip)]), basename2(options.zip));
9895
10387
  }
9896
10388
  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
10389
  printResult(result, () => publishAutoLines(result), log);
@@ -10014,12 +10506,12 @@ var publishAutoCommand = defineCommand({
10014
10506
  });
10015
10507
 
10016
10508
  // 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";
10509
+ import { readdir as readdir2, readFile as readFile5, stat as stat2 } from "node:fs/promises";
10510
+ import { basename as basename3, join as join5, relative as relative2, sep as sep2 } from "node:path";
10019
10511
  async function collectPath2(path) {
10020
10512
  const pathStats = await stat2(path);
10021
10513
  if (pathStats.isFile()) {
10022
- return [{ path: basename3(path), data: await readFile4(path) }];
10514
+ return [{ path: basename3(path), data: await readFile5(path) }];
10023
10515
  }
10024
10516
  if (!pathStats.isDirectory()) {
10025
10517
  throw new Error(`Publish path must be a file or directory: ${path}`);
@@ -10029,13 +10521,13 @@ async function collectPath2(path) {
10029
10521
  const entries = await readdir2(currentPath, { withFileTypes: true });
10030
10522
  entries.sort((a, b) => a.name.localeCompare(b.name));
10031
10523
  for (const entry of entries) {
10032
- const entryPath = join4(currentPath, entry.name);
10524
+ const entryPath = join5(currentPath, entry.name);
10033
10525
  if (entry.isDirectory()) {
10034
10526
  await visit(entryPath);
10035
10527
  } else if (entry.isFile()) {
10036
10528
  files.push({
10037
10529
  path: relative2(path, entryPath).split(sep2).join("/"),
10038
- data: await readFile4(entryPath)
10530
+ data: await readFile5(entryPath)
10039
10531
  });
10040
10532
  }
10041
10533
  }
@@ -10058,7 +10550,7 @@ async function runPublishStatic(options, log = console.log, error = printError)
10058
10550
  }
10059
10551
  }
10060
10552
  if (options.zip) {
10061
- form.append("zip", new Blob([await readFile4(options.zip)]), basename3(options.zip));
10553
+ form.append("zip", new Blob([await readFile5(options.zip)]), basename3(options.zip));
10062
10554
  }
10063
10555
  const { value: result } = await withProgress("Publishing…", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/static-sites`, { method: "POST", body: form }));
10064
10556
  printResult(result, () => [eventLine("url", result.url), eventLine("done", `Published site ${result.siteId}.`)], log);
@@ -10112,7 +10604,7 @@ var publishStaticCommand = defineCommand({
10112
10604
  });
10113
10605
 
10114
10606
  // src/commands/registry.ts
10115
- import { spawn } from "node:child_process";
10607
+ import { spawn as spawn2 } from "node:child_process";
10116
10608
  function reportError2(err, error) {
10117
10609
  if (err instanceof AuthRequiredError || err instanceof Error) {
10118
10610
  error(err.message);
@@ -10129,7 +10621,7 @@ function defaultInvoke(input) {
10129
10621
  }
10130
10622
  function dockerLogin(input) {
10131
10623
  return new Promise((resolve, reject) => {
10132
- const child = spawn("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
10624
+ const child = spawn2("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
10133
10625
  stdio: ["pipe", "inherit", "pipe"]
10134
10626
  });
10135
10627
  let stderr = "";
@@ -10227,8 +10719,8 @@ var registryCommand = defineCommand({
10227
10719
  });
10228
10720
 
10229
10721
  // 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";
10722
+ import { readdir as readdir3, readFile as readFile6, stat as stat3 } from "node:fs/promises";
10723
+ import { basename as basename4, join as join6, relative as relative3, sep as sep3 } from "node:path";
10232
10724
 
10233
10725
  // ../../node_modules/fflate/esm/index.mjs
10234
10726
  import { createRequire } from "module";
@@ -10884,13 +11376,13 @@ async function collectDirectory(path) {
10884
11376
  const entries = await readdir3(currentPath, { withFileTypes: true });
10885
11377
  entries.sort((a, b) => a.name.localeCompare(b.name));
10886
11378
  for (const entry of entries) {
10887
- const entryPath = join5(currentPath, entry.name);
11379
+ const entryPath = join6(currentPath, entry.name);
10888
11380
  if (entry.isDirectory()) {
10889
11381
  await visit(entryPath);
10890
11382
  } else if (entry.isFile()) {
10891
11383
  files.push({
10892
11384
  path: relative3(path, entryPath).split(sep3).join("/"),
10893
- data: await readFile5(entryPath)
11385
+ data: await readFile6(entryPath)
10894
11386
  });
10895
11387
  }
10896
11388
  }
@@ -10923,7 +11415,7 @@ async function runSandboxCreate(options, log = console.log, error = printError)
10923
11415
  return 1;
10924
11416
  }
10925
11417
  try {
10926
- const zipBytes = options.zip ? new Uint8Array(await readFile5(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
11418
+ const zipBytes = options.zip ? new Uint8Array(await readFile6(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
10927
11419
  const form = new FormData;
10928
11420
  form.append("kind", options.kind);
10929
11421
  if (options.displayName)
@@ -11184,12 +11676,12 @@ var toolsCommand = defineCommand({
11184
11676
  });
11185
11677
 
11186
11678
  // 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";
11679
+ import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
11680
+ import { basename as basename5, join as join7, relative as relative4, sep as sep4 } from "node:path";
11189
11681
  async function collectPath3(path) {
11190
11682
  const pathStats = await stat4(path);
11191
11683
  if (pathStats.isFile()) {
11192
- return [{ path: basename5(path), data: await readFile6(path) }];
11684
+ return [{ path: basename5(path), data: await readFile7(path) }];
11193
11685
  }
11194
11686
  if (!pathStats.isDirectory()) {
11195
11687
  throw new Error(`Upload path must be a file or directory: ${path}`);
@@ -11199,13 +11691,13 @@ async function collectPath3(path) {
11199
11691
  const entries = await readdir4(currentPath, { withFileTypes: true });
11200
11692
  entries.sort((a, b) => a.name.localeCompare(b.name));
11201
11693
  for (const entry of entries) {
11202
- const entryPath = join6(currentPath, entry.name);
11694
+ const entryPath = join7(currentPath, entry.name);
11203
11695
  if (entry.isDirectory()) {
11204
11696
  await visit(entryPath);
11205
11697
  } else if (entry.isFile()) {
11206
11698
  files.push({
11207
11699
  path: relative4(path, entryPath).split(sep4).join("/"),
11208
- data: await readFile6(entryPath)
11700
+ data: await readFile7(entryPath)
11209
11701
  });
11210
11702
  }
11211
11703
  }
@@ -11227,7 +11719,7 @@ async function runSandboxUpload(options, log = console.log, error = printError)
11227
11719
  }
11228
11720
  }
11229
11721
  if (options.zip) {
11230
- form.append("zip", new Blob([await readFile6(options.zip)]), basename5(options.zip));
11722
+ form.append("zip", new Blob([await readFile7(options.zip)]), basename5(options.zip));
11231
11723
  }
11232
11724
  const { value: result } = await withProgress("Uploading…", () => authorizedRequest(`/api/v1/sandboxes/${encodeURIComponent(options.sandboxId)}/files?organization=${encodeURIComponent(organization)}`, { method: "POST", body: form }));
11233
11725
  printResult(result, () => [
@@ -11340,7 +11832,8 @@ var HANDWRITTEN_COMMANDS = [
11340
11832
  [["sandboxes", "build"], sandboxCreateCommand],
11341
11833
  [["sandboxes", "files", "upload"], sandboxUploadCommand],
11342
11834
  [["sites", "publish"], publishStaticCommand],
11343
- [["projects", "publish"], publishAutoCommand]
11835
+ [["projects", "publish"], publishAutoCommand],
11836
+ [["network"], networkCommand]
11344
11837
  ];
11345
11838
  var HANDWRITTEN_PATHS = new Set(HANDWRITTEN_COMMANDS.map(([path]) => path.join(" ")));
11346
11839
  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.10",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ghostdock": "dist/index.js",