ghostdock 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-reference.md +62 -0
- package/dist/cli.md +26 -0
- package/dist/index.js +399 -19
- package/package.json +1 -1
package/dist/cli-reference.md
CHANGED
|
@@ -749,6 +749,7 @@ ghostdock sandboxes commands get <command-id> --sandbox-id <string> --session-id
|
|
|
749
749
|
| `<command-id>` | Command id returned by sandboxes commands run. Required. |
|
|
750
750
|
| `--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`. |
|
|
751
751
|
| `--sandbox-id <string>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
752
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
752
753
|
| `--session-id <string>` | Session id returned by sandboxes commands run together with the commandId. Required. |
|
|
753
754
|
|
|
754
755
|
MCP tool: `sandboxes_commands_get`.
|
|
@@ -766,6 +767,7 @@ ghostdock sandboxes commands kill <command-id> --sandbox-id <string> --session-i
|
|
|
766
767
|
| `<command-id>` | Command id returned by sandboxes commands run. Required. |
|
|
767
768
|
| `--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`. |
|
|
768
769
|
| `--sandbox-id <string>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
770
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
769
771
|
| `--session-id <string>` | Session id returned by sandboxes commands run together with the commandId. Required. |
|
|
770
772
|
|
|
771
773
|
MCP tool: `sandboxes_commands_kill`.
|
|
@@ -783,6 +785,7 @@ ghostdock sandboxes commands logs <command-id> --sandbox-id <string> --session-i
|
|
|
783
785
|
| `<command-id>` | Command id returned by sandboxes commands run. Required. |
|
|
784
786
|
| `--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`. |
|
|
785
787
|
| `--sandbox-id <string>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
788
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
786
789
|
| `--session-id <string>` | Session id returned by sandboxes commands run together with the commandId. Required. |
|
|
787
790
|
| `--offset <integer>` | Character offset to continue reading from. Default 0. |
|
|
788
791
|
|
|
@@ -800,6 +803,7 @@ ghostdock sandboxes commands run <sandbox-id> --command <string> [flags]
|
|
|
800
803
|
| --- | --- |
|
|
801
804
|
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
802
805
|
| `--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`. |
|
|
806
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
803
807
|
| `--command <string>` | Shell command to run. Never include secret values: commands are logged. Required. |
|
|
804
808
|
| `--session-id <string>` | Session from sandboxes shells create, to keep cwd and environment from earlier commands. Omit for a one-off session. |
|
|
805
809
|
| `--cwd <string>` | Directory to run the command in. |
|
|
@@ -807,6 +811,42 @@ ghostdock sandboxes commands run <sandbox-id> --command <string> [flags]
|
|
|
807
811
|
|
|
808
812
|
MCP tool: `sandboxes_commands_run`.
|
|
809
813
|
|
|
814
|
+
### `ghostdock sandboxes containers`
|
|
815
|
+
|
|
816
|
+
Containers of a sandbox’s Docker engine.
|
|
817
|
+
|
|
818
|
+
### `ghostdock sandboxes containers list`
|
|
819
|
+
|
|
820
|
+
List the containers of a sandbox, running or not. Each with state, health, restart policy, ports, Compose project and service, deletion protection, ghostdock.* labels, and which one is the default. A stopped sandbox lists none (it is not started for this).
|
|
821
|
+
|
|
822
|
+
```
|
|
823
|
+
ghostdock sandboxes containers list <sandbox-id> [flags]
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
| Argument | Description |
|
|
827
|
+
| --- | --- |
|
|
828
|
+
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
829
|
+
| `--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`. |
|
|
830
|
+
|
|
831
|
+
MCP tool: `sandboxes_containers_list`.
|
|
832
|
+
|
|
833
|
+
### `ghostdock sandboxes containers protection`
|
|
834
|
+
|
|
835
|
+
Switch a container’s deletion protection on or off. A protected container cannot be removed or recreated (docker rm, compose down) until its protection is off. Starts the sandbox when needed.
|
|
836
|
+
|
|
837
|
+
```
|
|
838
|
+
ghostdock sandboxes containers protection <sandbox-id> <container> --protected <value> [flags]
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
| Argument | Description |
|
|
842
|
+
| --- | --- |
|
|
843
|
+
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
844
|
+
| `<container>` | Container, by name. Required. |
|
|
845
|
+
| `--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`. |
|
|
846
|
+
| `--protected` | true: protect it; false: allow removing it. Required. |
|
|
847
|
+
|
|
848
|
+
MCP tool: `sandboxes_containers_protection`.
|
|
849
|
+
|
|
810
850
|
### `ghostdock sandboxes create`
|
|
811
851
|
|
|
812
852
|
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.
|
|
@@ -831,6 +871,23 @@ ghostdock sandboxes create --source <object> [flags]
|
|
|
831
871
|
|
|
832
872
|
MCP tool: `sandboxes_create`.
|
|
833
873
|
|
|
874
|
+
### `ghostdock sandboxes default-container`
|
|
875
|
+
|
|
876
|
+
Show, set or clear the sandbox’s default container. SSH, the terminal, files, commands and MCP land there when no container is named. Without container or clear it only shows the setting.
|
|
877
|
+
|
|
878
|
+
```
|
|
879
|
+
ghostdock sandboxes default-container <sandbox-id> [flags]
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
| Argument | Description |
|
|
883
|
+
| --- | --- |
|
|
884
|
+
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
885
|
+
| `--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`. |
|
|
886
|
+
| `--container <string>` | Container to make the default, by name. The running sandbox must have it. |
|
|
887
|
+
| `--clear` | Clear the default. |
|
|
888
|
+
|
|
889
|
+
MCP tool: `sandboxes_default_container`.
|
|
890
|
+
|
|
834
891
|
### `ghostdock sandboxes delete`
|
|
835
892
|
|
|
836
893
|
Permanently delete a sandbox and its files.
|
|
@@ -936,6 +993,7 @@ ghostdock sandboxes files list <sandbox-id> [flags]
|
|
|
936
993
|
| --- | --- |
|
|
937
994
|
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
938
995
|
| `--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`. |
|
|
996
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
939
997
|
| `--path <string>` | Directory to list. Absolute or relative to /. Defaults to /workspace. |
|
|
940
998
|
|
|
941
999
|
MCP tool: `sandboxes_files_list`.
|
|
@@ -953,6 +1011,7 @@ ghostdock sandboxes files read <path> --sandbox-id <string> [flags]
|
|
|
953
1011
|
| `<path>` | File path. Absolute or relative to / (for example /workspace/src/App.vue). Required. |
|
|
954
1012
|
| `--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`. |
|
|
955
1013
|
| `--sandbox-id <string>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
1014
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
956
1015
|
|
|
957
1016
|
MCP tool: `sandboxes_files_read`.
|
|
958
1017
|
|
|
@@ -1125,6 +1184,7 @@ ghostdock sandboxes shells close <session-id> --sandbox-id <string> [flags]
|
|
|
1125
1184
|
| `<session-id>` | Session id from sandboxes shells create. Required. |
|
|
1126
1185
|
| `--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`. |
|
|
1127
1186
|
| `--sandbox-id <string>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
1187
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
1128
1188
|
|
|
1129
1189
|
MCP tool: `sandboxes_shells_close`.
|
|
1130
1190
|
|
|
@@ -1140,6 +1200,7 @@ ghostdock sandboxes shells create <sandbox-id> [flags]
|
|
|
1140
1200
|
| --- | --- |
|
|
1141
1201
|
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
1142
1202
|
| `--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`. |
|
|
1203
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
1143
1204
|
| `--session-id <string>` | Id for the new session: a slug; user-cmd, image-cmd and compose are reserved. Omit to generate one. |
|
|
1144
1205
|
|
|
1145
1206
|
MCP tool: `sandboxes_shells_create`.
|
|
@@ -1160,6 +1221,7 @@ ghostdock sandboxes ssh get <sandbox-id> [flags]
|
|
|
1160
1221
|
| --- | --- |
|
|
1161
1222
|
| `<sandbox-id>` | Sandbox name (e.g. quiet-harbor) or id, from sandboxes list or sandboxes create. Names are unique within the organization. Required. |
|
|
1162
1223
|
| `--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`. |
|
|
1224
|
+
| `--container <string>` | Container to work in, by name, from sandboxes containers list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them. |
|
|
1163
1225
|
|
|
1164
1226
|
MCP tool: `sandboxes_ssh_get`.
|
|
1165
1227
|
|
package/dist/cli.md
CHANGED
|
@@ -314,6 +314,25 @@ ghostdock sandboxes create --org org_123 --display-name "Marketing site"
|
|
|
314
314
|
ghostdock sandboxes urls create quiet-harbor --port 8080 --auto-wake true --public true
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
+
#### Containers in a sandbox
|
|
318
|
+
|
|
319
|
+
On Ghostdock Cloud a sandbox runs one Docker engine; everything in it is an ordinary container.
|
|
320
|
+
`ssh get`, `files`, `commands` and `shells` land in one of them: `--container <name>`, else the
|
|
321
|
+
sandbox's default container, else the one running container. With several running and no default,
|
|
322
|
+
the CLI asks which one at the keyboard and offers to make it the default; without a terminal it
|
|
323
|
+
fails and lists the names. A command runs as `docker exec` does there: as the container's
|
|
324
|
+
`ghostdock.login.user`, else the image's `USER`, else root.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
ghostdock sandboxes containers list quiet-harbor
|
|
328
|
+
ghostdock sandboxes default-container quiet-harbor --container web # --clear true removes it
|
|
329
|
+
ghostdock sandboxes commands run quiet-harbor --container db --command 'pg_isready'
|
|
330
|
+
ghostdock sandboxes containers protection quiet-harbor workstation --protected false
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
A shell session lives in its container: pass the same `--container` to every `shells` and
|
|
334
|
+
`commands` call on it.
|
|
335
|
+
|
|
317
336
|
#### Environment in a sandbox
|
|
318
337
|
|
|
319
338
|
A sandbox exports **nothing** by default (unless you boot from an organization snapshot or catalog
|
|
@@ -515,6 +534,10 @@ ghostdock network disconnect
|
|
|
515
534
|
may not use answers 403.
|
|
516
535
|
- **Needs `wg-quick`** (`brew install wireguard-tools`, `apt install wireguard-tools`). macOS
|
|
517
536
|
and Linux. On Windows, run the CLI in WSL: then only tools inside WSL reach sandboxes.
|
|
537
|
+
- **Tunnel MTU 1280**, so the tunnel works on networks with a smaller path than 1500 bytes
|
|
538
|
+
(DS-Lite, VPNs, mobile). A config written before 2026-10-20 has 1420: `network status` and
|
|
539
|
+
`network ls` then say *This machine's tunnel has MTU 1420, the network now uses 1280*. Run
|
|
540
|
+
`ghostdock network connect` again; until then TCP works, other large packets can be lost.
|
|
518
541
|
- **One key per machine**, kept in `~/.config/ghostdock/network/`.
|
|
519
542
|
Only the public key is sent. The device belongs to the login it was connected with:
|
|
520
543
|
`ghostdock logout` takes it out of the network, and so does not connecting for 90 days.
|
|
@@ -691,7 +714,10 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
691
714
|
| `ghostdock sandboxes commands kill <command-id> --sandbox-id <string> --session-id <string> [flags]` | Interrupt a running shell command. |
|
|
692
715
|
| `ghostdock sandboxes commands logs <command-id> --sandbox-id <string> --session-id <string> [flags]` | Get a page of a shell command’s stdout and stderr. |
|
|
693
716
|
| `ghostdock sandboxes commands run <sandbox-id> --command <string> [flags]` | Run a shell command in a sandbox. |
|
|
717
|
+
| `ghostdock sandboxes containers list <sandbox-id> [flags]` | List the containers of a sandbox, running or not. |
|
|
718
|
+
| `ghostdock sandboxes containers protection <sandbox-id> <container> --protected <value> [flags]` | Switch a container’s deletion protection on or off. |
|
|
694
719
|
| `ghostdock sandboxes create --source <object> [flags]` | Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. |
|
|
720
|
+
| `ghostdock sandboxes default-container <sandbox-id> [flags]` | Show, set or clear the sandbox’s default container. |
|
|
695
721
|
| `ghostdock sandboxes delete <sandbox-id> [flags]` | Permanently delete a sandbox and its files. |
|
|
696
722
|
| `ghostdock sandboxes env delete <name> --sandbox-id <string> [flags]` | Delete one of a sandbox’s own variables or secrets. |
|
|
697
723
|
| `ghostdock sandboxes env get <sandbox-id> [flags]` | Get a sandbox’s environment: selections, bindings and local entries. |
|
package/dist/index.js
CHANGED
|
@@ -4625,6 +4625,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4625
4625
|
minLength: 1,
|
|
4626
4626
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
4627
4627
|
},
|
|
4628
|
+
container: {
|
|
4629
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
4630
|
+
type: "string",
|
|
4631
|
+
minLength: 1,
|
|
4632
|
+
maxLength: 255,
|
|
4633
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
4634
|
+
},
|
|
4628
4635
|
files: {
|
|
4629
4636
|
description: "Small inline files: { path, content } for utf8 text, or { path, contentBase64 } for binary.",
|
|
4630
4637
|
type: "array",
|
|
@@ -4701,6 +4708,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4701
4708
|
minLength: 1,
|
|
4702
4709
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
4703
4710
|
},
|
|
4711
|
+
container: {
|
|
4712
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
4713
|
+
type: "string",
|
|
4714
|
+
minLength: 1,
|
|
4715
|
+
maxLength: 255,
|
|
4716
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
4717
|
+
},
|
|
4704
4718
|
path: {
|
|
4705
4719
|
description: "Directory to list. Absolute or relative to /. Defaults to /workspace.",
|
|
4706
4720
|
type: "string",
|
|
@@ -4780,6 +4794,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4780
4794
|
minLength: 1,
|
|
4781
4795
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
4782
4796
|
},
|
|
4797
|
+
container: {
|
|
4798
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
4799
|
+
type: "string",
|
|
4800
|
+
minLength: 1,
|
|
4801
|
+
maxLength: 255,
|
|
4802
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
4803
|
+
},
|
|
4783
4804
|
path: {
|
|
4784
4805
|
type: "string",
|
|
4785
4806
|
minLength: 1,
|
|
@@ -5922,6 +5943,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5922
5943
|
type: "string",
|
|
5923
5944
|
minLength: 1,
|
|
5924
5945
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
5946
|
+
},
|
|
5947
|
+
container: {
|
|
5948
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
5949
|
+
type: "string",
|
|
5950
|
+
minLength: 1,
|
|
5951
|
+
maxLength: 255,
|
|
5952
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
5925
5953
|
}
|
|
5926
5954
|
},
|
|
5927
5955
|
required: [
|
|
@@ -5981,6 +6009,156 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
5981
6009
|
additionalProperties: {}
|
|
5982
6010
|
}
|
|
5983
6011
|
},
|
|
6012
|
+
{
|
|
6013
|
+
id: "sandboxes_containers_list",
|
|
6014
|
+
name: "sandboxes_containers_list",
|
|
6015
|
+
cli: [
|
|
6016
|
+
"sandboxes",
|
|
6017
|
+
"containers",
|
|
6018
|
+
"list"
|
|
6019
|
+
],
|
|
6020
|
+
cliArgs: [
|
|
6021
|
+
"sandboxId"
|
|
6022
|
+
],
|
|
6023
|
+
description: "List the containers of a sandbox, running or not. Each with state, health, restart policy, ports, Compose project and service, deletion protection, ghostdock.* labels, and which one is the default. A stopped sandbox lists none (it is not started for this).",
|
|
6024
|
+
inputSchema: {
|
|
6025
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6026
|
+
type: "object",
|
|
6027
|
+
properties: {
|
|
6028
|
+
organization: {
|
|
6029
|
+
type: "string",
|
|
6030
|
+
minLength: 1,
|
|
6031
|
+
description: "Organization id or slug, from organizations_list. Ids survive renames; slugs change when the organization is renamed."
|
|
6032
|
+
},
|
|
6033
|
+
sandboxId: {
|
|
6034
|
+
type: "string",
|
|
6035
|
+
minLength: 1,
|
|
6036
|
+
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6037
|
+
}
|
|
6038
|
+
},
|
|
6039
|
+
required: [
|
|
6040
|
+
"organization",
|
|
6041
|
+
"sandboxId"
|
|
6042
|
+
],
|
|
6043
|
+
additionalProperties: false
|
|
6044
|
+
},
|
|
6045
|
+
outputSchema: {
|
|
6046
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6047
|
+
type: "object",
|
|
6048
|
+
propertyNames: {
|
|
6049
|
+
type: "string"
|
|
6050
|
+
},
|
|
6051
|
+
additionalProperties: {}
|
|
6052
|
+
}
|
|
6053
|
+
},
|
|
6054
|
+
{
|
|
6055
|
+
id: "sandboxes_default_container",
|
|
6056
|
+
name: "sandboxes_default_container",
|
|
6057
|
+
cli: [
|
|
6058
|
+
"sandboxes",
|
|
6059
|
+
"default-container"
|
|
6060
|
+
],
|
|
6061
|
+
cliArgs: [
|
|
6062
|
+
"sandboxId"
|
|
6063
|
+
],
|
|
6064
|
+
description: "Show, set or clear the sandbox’s default container. SSH, the terminal, files, commands and MCP land there when no container is named. Without container or clear it only shows the setting.",
|
|
6065
|
+
inputSchema: {
|
|
6066
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6067
|
+
type: "object",
|
|
6068
|
+
properties: {
|
|
6069
|
+
organization: {
|
|
6070
|
+
type: "string",
|
|
6071
|
+
minLength: 1,
|
|
6072
|
+
description: "Organization id or slug, from organizations_list. Ids survive renames; slugs change when the organization is renamed."
|
|
6073
|
+
},
|
|
6074
|
+
sandboxId: {
|
|
6075
|
+
type: "string",
|
|
6076
|
+
minLength: 1,
|
|
6077
|
+
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6078
|
+
},
|
|
6079
|
+
container: {
|
|
6080
|
+
description: "Container to make the default, by name. The running sandbox must have it.",
|
|
6081
|
+
type: "string",
|
|
6082
|
+
minLength: 1,
|
|
6083
|
+
maxLength: 255,
|
|
6084
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6085
|
+
},
|
|
6086
|
+
clear: {
|
|
6087
|
+
description: "Clear the default.",
|
|
6088
|
+
type: "boolean"
|
|
6089
|
+
}
|
|
6090
|
+
},
|
|
6091
|
+
required: [
|
|
6092
|
+
"organization",
|
|
6093
|
+
"sandboxId"
|
|
6094
|
+
],
|
|
6095
|
+
additionalProperties: false
|
|
6096
|
+
},
|
|
6097
|
+
outputSchema: {
|
|
6098
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6099
|
+
type: "object",
|
|
6100
|
+
propertyNames: {
|
|
6101
|
+
type: "string"
|
|
6102
|
+
},
|
|
6103
|
+
additionalProperties: {}
|
|
6104
|
+
}
|
|
6105
|
+
},
|
|
6106
|
+
{
|
|
6107
|
+
id: "sandboxes_containers_protection",
|
|
6108
|
+
name: "sandboxes_containers_protection",
|
|
6109
|
+
cli: [
|
|
6110
|
+
"sandboxes",
|
|
6111
|
+
"containers",
|
|
6112
|
+
"protection"
|
|
6113
|
+
],
|
|
6114
|
+
cliArgs: [
|
|
6115
|
+
"sandboxId",
|
|
6116
|
+
"container"
|
|
6117
|
+
],
|
|
6118
|
+
description: "Switch a container’s deletion protection on or off. A protected container cannot be removed or recreated (docker rm, compose down) until its protection is off. Starts the sandbox when needed.",
|
|
6119
|
+
inputSchema: {
|
|
6120
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6121
|
+
type: "object",
|
|
6122
|
+
properties: {
|
|
6123
|
+
organization: {
|
|
6124
|
+
type: "string",
|
|
6125
|
+
minLength: 1,
|
|
6126
|
+
description: "Organization id or slug, from organizations_list. Ids survive renames; slugs change when the organization is renamed."
|
|
6127
|
+
},
|
|
6128
|
+
sandboxId: {
|
|
6129
|
+
type: "string",
|
|
6130
|
+
minLength: 1,
|
|
6131
|
+
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6132
|
+
},
|
|
6133
|
+
container: {
|
|
6134
|
+
type: "string",
|
|
6135
|
+
minLength: 1,
|
|
6136
|
+
maxLength: 255,
|
|
6137
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$",
|
|
6138
|
+
description: "Container, by name."
|
|
6139
|
+
},
|
|
6140
|
+
protected: {
|
|
6141
|
+
type: "boolean",
|
|
6142
|
+
description: "true: protect it; false: allow removing it."
|
|
6143
|
+
}
|
|
6144
|
+
},
|
|
6145
|
+
required: [
|
|
6146
|
+
"organization",
|
|
6147
|
+
"sandboxId",
|
|
6148
|
+
"container",
|
|
6149
|
+
"protected"
|
|
6150
|
+
],
|
|
6151
|
+
additionalProperties: false
|
|
6152
|
+
},
|
|
6153
|
+
outputSchema: {
|
|
6154
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6155
|
+
type: "object",
|
|
6156
|
+
propertyNames: {
|
|
6157
|
+
type: "string"
|
|
6158
|
+
},
|
|
6159
|
+
additionalProperties: {}
|
|
6160
|
+
}
|
|
6161
|
+
},
|
|
5984
6162
|
{
|
|
5985
6163
|
id: "sandboxes_commands_run",
|
|
5986
6164
|
name: "sandboxes_commands_run",
|
|
@@ -6007,6 +6185,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6007
6185
|
minLength: 1,
|
|
6008
6186
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6009
6187
|
},
|
|
6188
|
+
container: {
|
|
6189
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6190
|
+
type: "string",
|
|
6191
|
+
minLength: 1,
|
|
6192
|
+
maxLength: 255,
|
|
6193
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6194
|
+
},
|
|
6010
6195
|
command: {
|
|
6011
6196
|
type: "string",
|
|
6012
6197
|
minLength: 1,
|
|
@@ -6069,6 +6254,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6069
6254
|
minLength: 1,
|
|
6070
6255
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6071
6256
|
},
|
|
6257
|
+
container: {
|
|
6258
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6259
|
+
type: "string",
|
|
6260
|
+
minLength: 1,
|
|
6261
|
+
maxLength: 255,
|
|
6262
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6263
|
+
},
|
|
6072
6264
|
sessionId: {
|
|
6073
6265
|
type: "string",
|
|
6074
6266
|
minLength: 1,
|
|
@@ -6123,6 +6315,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6123
6315
|
minLength: 1,
|
|
6124
6316
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6125
6317
|
},
|
|
6318
|
+
container: {
|
|
6319
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6320
|
+
type: "string",
|
|
6321
|
+
minLength: 1,
|
|
6322
|
+
maxLength: 255,
|
|
6323
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6324
|
+
},
|
|
6126
6325
|
sessionId: {
|
|
6127
6326
|
type: "string",
|
|
6128
6327
|
minLength: 1,
|
|
@@ -6183,6 +6382,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6183
6382
|
minLength: 1,
|
|
6184
6383
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6185
6384
|
},
|
|
6385
|
+
container: {
|
|
6386
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6387
|
+
type: "string",
|
|
6388
|
+
minLength: 1,
|
|
6389
|
+
maxLength: 255,
|
|
6390
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6391
|
+
},
|
|
6186
6392
|
sessionId: {
|
|
6187
6393
|
type: "string",
|
|
6188
6394
|
minLength: 1,
|
|
@@ -6237,6 +6443,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6237
6443
|
minLength: 1,
|
|
6238
6444
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6239
6445
|
},
|
|
6446
|
+
container: {
|
|
6447
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6448
|
+
type: "string",
|
|
6449
|
+
minLength: 1,
|
|
6450
|
+
maxLength: 255,
|
|
6451
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6452
|
+
},
|
|
6240
6453
|
sessionId: {
|
|
6241
6454
|
description: "Id for the new session: a slug; user-cmd, image-cmd and compose are reserved. Omit to generate one.",
|
|
6242
6455
|
type: "string",
|
|
@@ -6284,6 +6497,13 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6284
6497
|
minLength: 1,
|
|
6285
6498
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6286
6499
|
},
|
|
6500
|
+
container: {
|
|
6501
|
+
description: "Container to work in, by name, from sandboxes_containers_list. Default: the sandbox’s default container, else the one running container. With several running and no default the call fails and lists them.",
|
|
6502
|
+
type: "string",
|
|
6503
|
+
minLength: 1,
|
|
6504
|
+
maxLength: 255,
|
|
6505
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6506
|
+
},
|
|
6287
6507
|
sessionId: {
|
|
6288
6508
|
type: "string",
|
|
6289
6509
|
minLength: 1,
|
|
@@ -8588,7 +8808,7 @@ function getCliName(argv = process.argv) {
|
|
|
8588
8808
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
8589
8809
|
}
|
|
8590
8810
|
// package.json
|
|
8591
|
-
var version = "0.0.
|
|
8811
|
+
var version = "0.0.14";
|
|
8592
8812
|
var package_default = {
|
|
8593
8813
|
name: "ghostdock",
|
|
8594
8814
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -9193,6 +9413,57 @@ var envCommand = defineCommand({
|
|
|
9193
9413
|
}
|
|
9194
9414
|
});
|
|
9195
9415
|
|
|
9416
|
+
// src/container-picker.ts
|
|
9417
|
+
import { createInterface } from "node:readline/promises";
|
|
9418
|
+
function containerQuestionOf(error) {
|
|
9419
|
+
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
9420
|
+
return null;
|
|
9421
|
+
const { code, params } = error.data;
|
|
9422
|
+
if (code !== "container_choice_required" && code !== "container_default_missing")
|
|
9423
|
+
return null;
|
|
9424
|
+
const record = params && typeof params === "object" ? params : {};
|
|
9425
|
+
const running = typeof record.containers === "string" ? record.containers.split(",").map((name) => name.trim()).filter(Boolean) : [];
|
|
9426
|
+
if (running.length === 0)
|
|
9427
|
+
return null;
|
|
9428
|
+
return {
|
|
9429
|
+
running,
|
|
9430
|
+
...code === "container_default_missing" && typeof record.container === "string" ? { missingDefault: record.container } : {}
|
|
9431
|
+
};
|
|
9432
|
+
}
|
|
9433
|
+
var say = (line) => process.stderr.write(`${line}
|
|
9434
|
+
`);
|
|
9435
|
+
var chooseContainerOnTerminal = async ({ running, missingDefault }) => {
|
|
9436
|
+
if (missingDefault) {
|
|
9437
|
+
say(`
|
|
9438
|
+
The default container ${missingDefault} is gone.`);
|
|
9439
|
+
}
|
|
9440
|
+
say(missingDefault ? "Which container?" : `
|
|
9441
|
+
Several containers are running. Which one?`);
|
|
9442
|
+
running.forEach((name, index) => {
|
|
9443
|
+
say(` ${index + 1}. ${name}`);
|
|
9444
|
+
});
|
|
9445
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9446
|
+
try {
|
|
9447
|
+
const answer = (await prompt.question("Number (Enter to cancel): ")).trim();
|
|
9448
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
9449
|
+
return Number.isInteger(index) && index >= 0 && index < running.length ? running[index] ?? null : null;
|
|
9450
|
+
} finally {
|
|
9451
|
+
prompt.close();
|
|
9452
|
+
}
|
|
9453
|
+
};
|
|
9454
|
+
var confirmDefaultOnTerminal = async (container) => {
|
|
9455
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9456
|
+
try {
|
|
9457
|
+
const answer = (await prompt.question(`Make ${container} the default? [y/N] `)).trim();
|
|
9458
|
+
return /^y(es)?$/i.test(answer);
|
|
9459
|
+
} finally {
|
|
9460
|
+
prompt.close();
|
|
9461
|
+
}
|
|
9462
|
+
};
|
|
9463
|
+
function defaultContainerPickers() {
|
|
9464
|
+
return process.stdin.isTTY && process.stderr.isTTY ? { choose: chooseContainerOnTerminal, confirmDefault: confirmDefaultOnTerminal } : undefined;
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9196
9467
|
// src/progress.ts
|
|
9197
9468
|
var PROGRESS_FRAMES = ["▂▄▆", "▄▆█", "▆█▆", "█▆▄", "▆▄▂", "▄▂▄"];
|
|
9198
9469
|
var FRAME_MS = 120;
|
|
@@ -9293,10 +9564,10 @@ function symbolForStatus(status) {
|
|
|
9293
9564
|
return "failed";
|
|
9294
9565
|
if (/^(running|active|ready|deployed|published|online|verified|ok)$/.test(s))
|
|
9295
9566
|
return "ready";
|
|
9296
|
-
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed)$/.test(s)) {
|
|
9567
|
+
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed|exited|dead|paused)$/.test(s)) {
|
|
9297
9568
|
return "stopped";
|
|
9298
9569
|
}
|
|
9299
|
-
if (/(pending|starting|spawning|stopping|building|progress|awaiting|queued|creating)/.test(s)) {
|
|
9570
|
+
if (/(pending|starting|spawning|stopping|building|progress|awaiting|queued|creating|restarting|removing)/.test(s)) {
|
|
9300
9571
|
return "progress";
|
|
9301
9572
|
}
|
|
9302
9573
|
return "item";
|
|
@@ -9311,6 +9582,9 @@ function lineKeys(item) {
|
|
|
9311
9582
|
const idKeys = "id" in item ? ID_KEYS.filter((key) => key !== "sandboxId") : ID_KEYS;
|
|
9312
9583
|
return { id: firstKey(item, idKeys), label: firstKey(item, LABEL_KEYS) };
|
|
9313
9584
|
}
|
|
9585
|
+
function shortId(id) {
|
|
9586
|
+
return /^[0-9a-f]{64}$/.test(id) ? id.slice(0, 12) : id;
|
|
9587
|
+
}
|
|
9314
9588
|
function itemLine(item, color) {
|
|
9315
9589
|
const keys = lineKeys(item);
|
|
9316
9590
|
const id = keys.id ? item[keys.id] : undefined;
|
|
@@ -9319,7 +9593,7 @@ function itemLine(item, color) {
|
|
|
9319
9593
|
const url = firstString(item, URL_KEYS);
|
|
9320
9594
|
const parts = [label ?? id ?? "(unnamed)"];
|
|
9321
9595
|
if (label && id && id !== label)
|
|
9322
|
-
parts.push(dim(id, color));
|
|
9596
|
+
parts.push(dim(shortId(id), color));
|
|
9323
9597
|
if (status)
|
|
9324
9598
|
parts.push(status);
|
|
9325
9599
|
if (url)
|
|
@@ -9552,7 +9826,7 @@ function buildToolInput(schema, rawArgs) {
|
|
|
9552
9826
|
}
|
|
9553
9827
|
|
|
9554
9828
|
// src/commands/invoke.ts
|
|
9555
|
-
async function runToolInvoke(tool, rawArgs, log = console.log, error = printError, notice = printNotice) {
|
|
9829
|
+
async function runToolInvoke(tool, rawArgs, log = console.log, error = printError, notice = printNotice, pickers = defaultContainerPickers()) {
|
|
9556
9830
|
let input;
|
|
9557
9831
|
try {
|
|
9558
9832
|
input = buildToolInput(tool.inputSchema, rawArgs);
|
|
@@ -9571,11 +9845,11 @@ async function runToolInvoke(tool, rawArgs, log = console.log, error = printErro
|
|
|
9571
9845
|
});
|
|
9572
9846
|
const waitMessage = PROGRESS_MESSAGES[tool.name];
|
|
9573
9847
|
if (!waitMessage) {
|
|
9574
|
-
const result = await request
|
|
9848
|
+
const result = await withContainerPick(tool, input, request, pickers, notice);
|
|
9575
9849
|
printResult(result, () => renderResult(result, colorEnabled(process.stdout)), log);
|
|
9576
9850
|
return 0;
|
|
9577
9851
|
}
|
|
9578
|
-
const { value: result, elapsed } = await withProgress(waitMessage, request);
|
|
9852
|
+
const { value: result, elapsed } = await withContainerPick(tool, input, () => withProgress(waitMessage, request), pickers, notice);
|
|
9579
9853
|
printResult(result, () => {
|
|
9580
9854
|
const color = colorEnabled(process.stdout);
|
|
9581
9855
|
const lines = renderResult(result, color);
|
|
@@ -9587,6 +9861,38 @@ async function runToolInvoke(tool, rawArgs, log = console.log, error = printErro
|
|
|
9587
9861
|
return 1;
|
|
9588
9862
|
}
|
|
9589
9863
|
}
|
|
9864
|
+
async function withContainerPick(tool, input, send, pickers, notice) {
|
|
9865
|
+
try {
|
|
9866
|
+
return await send();
|
|
9867
|
+
} catch (err) {
|
|
9868
|
+
const properties = tool.inputSchema.properties;
|
|
9869
|
+
const question = containerQuestionOf(err);
|
|
9870
|
+
if (!pickers || !question || !properties || !("container" in properties) || input.container) {
|
|
9871
|
+
throw err;
|
|
9872
|
+
}
|
|
9873
|
+
const container = await pickers.choose(question);
|
|
9874
|
+
if (!container)
|
|
9875
|
+
throw err;
|
|
9876
|
+
input.container = container;
|
|
9877
|
+
const result = await send();
|
|
9878
|
+
if (typeof input.sandboxId === "string" && await pickers.confirmDefault(container)) {
|
|
9879
|
+
try {
|
|
9880
|
+
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
9881
|
+
method: "POST",
|
|
9882
|
+
body: JSON.stringify({
|
|
9883
|
+
organization: input.organization,
|
|
9884
|
+
sandboxId: input.sandboxId,
|
|
9885
|
+
container
|
|
9886
|
+
})
|
|
9887
|
+
});
|
|
9888
|
+
notice(eventLine("done", `Default container: ${container}.`));
|
|
9889
|
+
} catch (err) {
|
|
9890
|
+
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
return result;
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9590
9896
|
var PROGRESS_MESSAGES = {
|
|
9591
9897
|
sandboxes_create: "Creating sandbox…",
|
|
9592
9898
|
sandboxes_wake: "Waking sandbox…",
|
|
@@ -9642,7 +9948,7 @@ function createToolCommands(catalog) {
|
|
|
9642
9948
|
import { hostname } from "node:os";
|
|
9643
9949
|
|
|
9644
9950
|
// src/commands/login-organization.ts
|
|
9645
|
-
import { createInterface } from "node:readline/promises";
|
|
9951
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
9646
9952
|
function label(organization) {
|
|
9647
9953
|
return `${organization.name} (${organization.slug ?? organization.id})`;
|
|
9648
9954
|
}
|
|
@@ -9652,7 +9958,7 @@ Which organization should commands use by default?`);
|
|
|
9652
9958
|
organizations.forEach((organization, index) => {
|
|
9653
9959
|
console.log(` ${index + 1}. ${label(organization)}`);
|
|
9654
9960
|
});
|
|
9655
|
-
const prompt =
|
|
9961
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
9656
9962
|
try {
|
|
9657
9963
|
const answer = (await prompt.question(`Number (Enter to skip): `)).trim();
|
|
9658
9964
|
const index = Number.parseInt(answer, 10) - 1;
|
|
@@ -9927,14 +10233,21 @@ function renderConfig(input) {
|
|
|
9927
10233
|
if (!isDnsZone(zone))
|
|
9928
10234
|
throw new Error(`The server sent a zone that is not a DNS name: ${zone}`);
|
|
9929
10235
|
}
|
|
10236
|
+
if (!device.address || !isIPv6Address(device.address)) {
|
|
10237
|
+
throw new Error(`The server sent no IPv6 address for this device: ${device.address}`);
|
|
10238
|
+
}
|
|
9930
10239
|
if (!isIPv6Address(network.gatewayAddress)) {
|
|
9931
10240
|
throw new Error(`The server sent a gateway that is not an IPv6 address: ${network.gatewayAddress}`);
|
|
9932
10241
|
}
|
|
10242
|
+
const ipv4 = device.address4 && network.ingress4 ? { address: device.address4, ingress: network.ingress4 } : null;
|
|
10243
|
+
if (ipv4 && (!isIPv4Address(ipv4.address) || !isIPv4Address(ipv4.ingress))) {
|
|
10244
|
+
throw new Error(`The server sent an IPv4 address that is not one: ${ipv4.address}, ${ipv4.ingress}`);
|
|
10245
|
+
}
|
|
9933
10246
|
const lines = [
|
|
9934
10247
|
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9935
10248
|
"[Interface]",
|
|
9936
10249
|
`PrivateKey = ${input.privateKey}`,
|
|
9937
|
-
`Address = ${device.address}/128`,
|
|
10250
|
+
`Address = ${device.address}/128${ipv4 ? `, ${ipv4.address}/32` : ""}`,
|
|
9938
10251
|
`MTU = ${network.mtu}`
|
|
9939
10252
|
];
|
|
9940
10253
|
if (input.dns === "macos-resolver") {
|
|
@@ -9946,7 +10259,7 @@ function renderConfig(input) {
|
|
|
9946
10259
|
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
9947
10260
|
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
9948
10261
|
}
|
|
9949
|
-
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.
|
|
10262
|
+
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.prefix}${ipv4 ? `, ${ipv4.ingress}/32` : ""}`, "PersistentKeepalive = 25", "");
|
|
9950
10263
|
return lines.join(`
|
|
9951
10264
|
`);
|
|
9952
10265
|
}
|
|
@@ -9955,9 +10268,39 @@ function isDnsZone(zone) {
|
|
|
9955
10268
|
const labels = zone.split(".");
|
|
9956
10269
|
return zone.length <= 253 && labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
9957
10270
|
}
|
|
10271
|
+
function isIPv4Address(text) {
|
|
10272
|
+
return /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/.test(text);
|
|
10273
|
+
}
|
|
10274
|
+
function ipv4RouteOwner(platform, output) {
|
|
10275
|
+
if (platform === "darwin") {
|
|
10276
|
+
if (/^\s*destination: default$/m.test(output))
|
|
10277
|
+
return null;
|
|
10278
|
+
return /^\s*interface: (\S+)$/m.exec(output)?.[1] ?? null;
|
|
10279
|
+
}
|
|
10280
|
+
for (const line of output.split(`
|
|
10281
|
+
`).map((l) => l.trim())) {
|
|
10282
|
+
if (!line || /^(default|broadcast)\b/.test(line))
|
|
10283
|
+
continue;
|
|
10284
|
+
return /\bdev (\S+)/.exec(line)?.[1] ?? "another route";
|
|
10285
|
+
}
|
|
10286
|
+
return null;
|
|
10287
|
+
}
|
|
10288
|
+
function ipv4RouteCommand(platform, address) {
|
|
10289
|
+
return platform === "darwin" ? ["route", ["-n", "get", address]] : ["ip", ["-4", "route", "show", "table", "all", "match", address]];
|
|
10290
|
+
}
|
|
9958
10291
|
function isIPv6Address(text) {
|
|
9959
10292
|
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
9960
10293
|
}
|
|
10294
|
+
async function readConfigMtu(path = configPath()) {
|
|
10295
|
+
let contents;
|
|
10296
|
+
try {
|
|
10297
|
+
contents = await readFile3(path, "utf8");
|
|
10298
|
+
} catch {
|
|
10299
|
+
return null;
|
|
10300
|
+
}
|
|
10301
|
+
const match = /^MTU = (\d+)$/m.exec(contents);
|
|
10302
|
+
return match ? Number(match[1]) : null;
|
|
10303
|
+
}
|
|
9961
10304
|
async function writeConfig(contents, path = configPath()) {
|
|
9962
10305
|
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9963
10306
|
await writeFile3(path, contents, { mode: 384 });
|
|
@@ -9985,6 +10328,11 @@ function defaultDeps() {
|
|
|
9985
10328
|
child.on("error", reject);
|
|
9986
10329
|
child.on("close", (code) => resolve(code ?? 1));
|
|
9987
10330
|
}),
|
|
10331
|
+
output: (command, args) => {
|
|
10332
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
10333
|
+
return result.status === 0 ? result.stdout : "";
|
|
10334
|
+
},
|
|
10335
|
+
platform: process.platform,
|
|
9988
10336
|
has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
9989
10337
|
dir: networkDir(),
|
|
9990
10338
|
up: () => tunnelUp(),
|
|
@@ -10044,7 +10392,7 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10044
10392
|
await writeDeviceState({
|
|
10045
10393
|
baseUrl: getBaseUrl(),
|
|
10046
10394
|
deviceId: registered.device.id,
|
|
10047
|
-
address: registered.device.address,
|
|
10395
|
+
address: registered.device.address ?? "",
|
|
10048
10396
|
gatewayProvider: registered.provider
|
|
10049
10397
|
}, deps.dir);
|
|
10050
10398
|
const wasUp = deps.up();
|
|
@@ -10055,6 +10403,7 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10055
10403
|
return 1;
|
|
10056
10404
|
}
|
|
10057
10405
|
}
|
|
10406
|
+
const ipv4Clashes = ipv4Clashes4(registered, deps);
|
|
10058
10407
|
const code = await wgQuick(deps, "up");
|
|
10059
10408
|
if (code !== 0) {
|
|
10060
10409
|
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
@@ -10076,6 +10425,12 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10076
10425
|
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
10077
10426
|
] : [],
|
|
10078
10427
|
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
10428
|
+
...network.ingress4 && !device.address4 ? [
|
|
10429
|
+
" No IPv4 address was free on this provider's host, so browser names work over IPv6 only:",
|
|
10430
|
+
" Chrome on a network without IPv6 cannot open them on this machine. Firefox and Safari",
|
|
10431
|
+
" can. Run connect again later: an address frees up when another device goes."
|
|
10432
|
+
] : [],
|
|
10433
|
+
...ipv4Clashes.map(({ address, owner }) => ` ${address} is already routed through ${owner} on this machine: browsers may not reach the private network's names over IPv4 while it is. Ask your Ghostdock admin about the IPv4 block.`),
|
|
10079
10434
|
...dns === "none" ? [
|
|
10080
10435
|
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
10081
10436
|
` \`${getCliName()} network ls\`.`
|
|
@@ -10084,6 +10439,17 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10084
10439
|
], deps.log);
|
|
10085
10440
|
return 0;
|
|
10086
10441
|
}
|
|
10442
|
+
function ipv4Clashes4(registered, deps) {
|
|
10443
|
+
const addresses = [registered.network.ingress4, registered.device.address4].filter((a) => typeof a === "string" && isIPv4Address(a));
|
|
10444
|
+
const clashes = [];
|
|
10445
|
+
for (const address of addresses) {
|
|
10446
|
+
const [command, args] = ipv4RouteCommand(deps.platform, address);
|
|
10447
|
+
const owner = ipv4RouteOwner(deps.platform, deps.output(command, args));
|
|
10448
|
+
if (owner)
|
|
10449
|
+
clashes.push({ address, owner });
|
|
10450
|
+
}
|
|
10451
|
+
return clashes;
|
|
10452
|
+
}
|
|
10087
10453
|
function clearingNotice(cookieSite) {
|
|
10088
10454
|
const site = cookieSite?.site ?? "the net domain";
|
|
10089
10455
|
return [
|
|
@@ -10110,25 +10476,38 @@ async function readConnection(deps) {
|
|
|
10110
10476
|
const state = await readDeviceState(deps.dir);
|
|
10111
10477
|
const up = deps.up();
|
|
10112
10478
|
if (!state)
|
|
10113
|
-
return { state, up, device: null, asked: true, provider: null };
|
|
10479
|
+
return { state, up, device: null, asked: true, provider: null, staleMtu: null };
|
|
10114
10480
|
try {
|
|
10115
|
-
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10481
|
+
const { devices, mtu } = await deps.request("/api/v1/network/devices");
|
|
10116
10482
|
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10483
|
+
const config = await readConfigMtu(configPath(deps.dir));
|
|
10484
|
+
const staleMtu = config !== null && config !== mtu ? { config, current: mtu } : null;
|
|
10485
|
+
if (staleMtu) {
|
|
10486
|
+
deps.notice(`This machine's tunnel has MTU ${staleMtu.config}, the network now uses ${staleMtu.current}. ` + `Run \`${getCliName()} network connect\` again: until then, large packets other than TCP ` + "can be lost on the way.");
|
|
10487
|
+
}
|
|
10117
10488
|
return {
|
|
10118
10489
|
state,
|
|
10119
10490
|
up,
|
|
10120
10491
|
device,
|
|
10121
10492
|
asked: true,
|
|
10122
|
-
provider: up ? device?.gatewayProvider ?? null : null
|
|
10493
|
+
provider: up ? device?.gatewayProvider ?? null : null,
|
|
10494
|
+
staleMtu
|
|
10123
10495
|
};
|
|
10124
10496
|
} catch (err) {
|
|
10125
10497
|
deps.notice(`Could not ask the app about this device: ${message(err)}`);
|
|
10126
|
-
return {
|
|
10498
|
+
return {
|
|
10499
|
+
state,
|
|
10500
|
+
up,
|
|
10501
|
+
device: null,
|
|
10502
|
+
asked: false,
|
|
10503
|
+
provider: up ? state.gatewayProvider : null,
|
|
10504
|
+
staleMtu: null
|
|
10505
|
+
};
|
|
10127
10506
|
}
|
|
10128
10507
|
}
|
|
10129
10508
|
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10130
|
-
const { state, up, device, asked, provider } = await readConnection(deps);
|
|
10131
|
-
printResult({ connected: up, device, provider }, () => {
|
|
10509
|
+
const { state, up, device, asked, provider, staleMtu } = await readConnection(deps);
|
|
10510
|
+
printResult({ connected: up, device, provider, staleMtu }, () => {
|
|
10132
10511
|
if (!state) {
|
|
10133
10512
|
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10134
10513
|
}
|
|
@@ -10187,7 +10566,7 @@ async function runNetworkLs(args, deps = defaultDeps()) {
|
|
|
10187
10566
|
const note = unreachableNote(s, connected);
|
|
10188
10567
|
lines.push([
|
|
10189
10568
|
` ${s.name.padEnd(width)}`,
|
|
10190
|
-
s.address.padEnd(28),
|
|
10569
|
+
(s.address ?? "-").padEnd(28),
|
|
10191
10570
|
s.canonicalName.padEnd(canonicalWidth),
|
|
10192
10571
|
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
10193
10572
|
...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
|
|
@@ -11938,6 +12317,7 @@ var GROUP_DESCRIPTIONS = {
|
|
|
11938
12317
|
providers: "Sandbox providers of an organization.",
|
|
11939
12318
|
sandboxes: "Create, run and manage sandboxes.",
|
|
11940
12319
|
"sandboxes commands": "Run commands in a sandbox and read their output.",
|
|
12320
|
+
"sandboxes containers": "Containers of a sandbox’s Docker engine.",
|
|
11941
12321
|
"sandboxes env": "Environment of one sandbox.",
|
|
11942
12322
|
"sandboxes files": "Files inside a sandbox.",
|
|
11943
12323
|
"sandboxes mcps": "MCP endpoints a sandbox exposes.",
|