ghostdock 0.0.9 → 0.0.11
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 +7 -19
- package/dist/cli.md +83 -33
- package/dist/index.js +208 -97
- package/package.json +1 -1
package/dist/cli-reference.md
CHANGED
|
@@ -228,7 +228,7 @@ MCP tool: `images_update_dockerfile`.
|
|
|
228
228
|
|
|
229
229
|
## `ghostdock login`
|
|
230
230
|
|
|
231
|
-
Sign in and store a device access token for this
|
|
231
|
+
Sign in and store a device access token for this machine (~/.config/ghostdock/credentials.json).
|
|
232
232
|
|
|
233
233
|
```
|
|
234
234
|
ghostdock login [flags]
|
|
@@ -255,14 +255,15 @@ The private network: reach your sandboxes by address and name from this machine.
|
|
|
255
255
|
|
|
256
256
|
### `ghostdock network connect`
|
|
257
257
|
|
|
258
|
-
Put this machine into the private network: WireGuard and split DNS. Asks for sudo once.
|
|
258
|
+
Put this machine into the private network through one provider: WireGuard and split DNS. Asks for sudo once.
|
|
259
259
|
|
|
260
260
|
```
|
|
261
|
-
ghostdock network connect [flags]
|
|
261
|
+
ghostdock network connect [provider] [flags]
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
| Argument | Description |
|
|
265
265
|
| --- | --- |
|
|
266
|
+
| `<provider>` | Sandbox provider name or id. Omit for the organization's default provider (else the platform default). Only its sandboxes are reachable; connecting through another replaces the connection. |
|
|
266
267
|
| `--org <value>` | Organization id or slug, from `ghostdock organizations list` (default: GHOSTDOCK_ORG, then `ghostdock organizations use`). |
|
|
267
268
|
|
|
268
269
|
### `ghostdock network devices`
|
|
@@ -295,7 +296,7 @@ ghostdock network disconnect
|
|
|
295
296
|
|
|
296
297
|
### `ghostdock network ls`
|
|
297
298
|
|
|
298
|
-
Sandboxes of an organization with their private addresses and names.
|
|
299
|
+
Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection.
|
|
299
300
|
|
|
300
301
|
```
|
|
301
302
|
ghostdock network ls [flags]
|
|
@@ -307,25 +308,12 @@ ghostdock network ls [flags]
|
|
|
307
308
|
|
|
308
309
|
### `ghostdock network status`
|
|
309
310
|
|
|
310
|
-
Whether this machine is connected, and as which device.
|
|
311
|
+
Whether this machine is connected, through which provider, and as which device.
|
|
311
312
|
|
|
312
313
|
```
|
|
313
314
|
ghostdock network status
|
|
314
315
|
```
|
|
315
316
|
|
|
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
|
-
|
|
329
317
|
## `ghostdock organizations`
|
|
330
318
|
|
|
331
319
|
Organizations you belong to.
|
|
@@ -354,7 +342,7 @@ MCP tool: `organizations_list`.
|
|
|
354
342
|
|
|
355
343
|
### `ghostdock organizations use`
|
|
356
344
|
|
|
357
|
-
Set the default organization for commands run in
|
|
345
|
+
Set the default organization for commands run without --org (in every directory).
|
|
358
346
|
|
|
359
347
|
```
|
|
360
348
|
ghostdock organizations use [org] [flags]
|
package/dist/cli.md
CHANGED
|
@@ -99,7 +99,7 @@ Commands are grouped by resource, like `gh` or `kubectl`: `ghostdock <resource>
|
|
|
99
99
|
ghostdock apps Published apps of an organization.
|
|
100
100
|
ghostdock env Manage organization environment variables and secrets.
|
|
101
101
|
ghostdock images Organization image catalog: OCI bookmarks, Dockerfiles and Compose files.
|
|
102
|
-
ghostdock login Sign in and store a device access token for this
|
|
102
|
+
ghostdock login Sign in and store a device access token for this machine…
|
|
103
103
|
ghostdock logout Revoke the device session and remove stored credentials.
|
|
104
104
|
ghostdock network The private network: reach your sandboxes by address and name from this machine.
|
|
105
105
|
ghostdock organizations Organizations you belong to.
|
|
@@ -128,21 +128,25 @@ Every organization-scoped command acts on one organization, taken from the first
|
|
|
128
128
|
|
|
129
129
|
1. `--org <id-or-slug>` on the command
|
|
130
130
|
2. the `GHOSTDOCK_ORG` environment variable
|
|
131
|
-
3. the default set with `ghostdock organizations use <id-or-slug
|
|
131
|
+
3. the default set with `ghostdock organizations use <id-or-slug>`, stored in
|
|
132
|
+
`~/.config/ghostdock/settings.json`
|
|
133
|
+
|
|
134
|
+
With one organization there is nothing to do: `login` makes it the default, and every command,
|
|
135
|
+
in any directory, uses it.
|
|
132
136
|
|
|
133
137
|
```bash
|
|
134
|
-
ghostdock organizations use acme # default
|
|
135
|
-
ghostdock sandboxes list # stderr: Using organization acme (default
|
|
138
|
+
ghostdock organizations use acme # the default (login sets one when it can)
|
|
139
|
+
ghostdock sandboxes list # stderr: Using organization acme (default from ~/.config/ghostdock/settings.json)
|
|
136
140
|
ghostdock sandboxes list --org beta # --org always wins
|
|
137
141
|
ghostdock organizations current # which organization, and where it comes from
|
|
138
142
|
ghostdock organizations use --clear
|
|
139
143
|
```
|
|
140
144
|
|
|
141
145
|
When the organization does not come from `--org`, the command says which one it uses on stderr,
|
|
142
|
-
so a default never acts silently. The default is stored next to the credentials
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
so a default never acts silently. The default is stored next to the credentials, so it belongs to
|
|
147
|
+
the same login. **Scripts and CI should pass `--org` or set `GHOSTDOCK_ORG`** rather than depend on
|
|
148
|
+
(or change) a stored default, and use the organization id: renaming an organization changes its
|
|
149
|
+
slug, not its id.
|
|
146
150
|
|
|
147
151
|
### `ghostdock login`
|
|
148
152
|
|
|
@@ -187,6 +191,9 @@ ghostdock login --code <paste-code>
|
|
|
187
191
|
On success, credentials are written and the command exits `0`. If the code hasn't been approved
|
|
188
192
|
yet, it exits `1` — approve it in the browser first.
|
|
189
193
|
|
|
194
|
+
Credentials are stored per machine, in `~/.config/ghostdock/credentials.json` (see
|
|
195
|
+
[Credentials file](#credentials-file)), so one login works in every directory.
|
|
196
|
+
|
|
190
197
|
**Default organization after login.** Once logged in (with `--wait` or `--code`), `login` sets the
|
|
191
198
|
default organization when that is unambiguous:
|
|
192
199
|
|
|
@@ -207,7 +214,8 @@ ghostdock logout
|
|
|
207
214
|
```
|
|
208
215
|
|
|
209
216
|
Revokes the device session on the server (best-effort — still clears local credentials even if the
|
|
210
|
-
server call fails, e.g. when offline) and deletes the
|
|
217
|
+
server call fails, e.g. when offline) and deletes the credentials file. The default organization
|
|
218
|
+
stays.
|
|
211
219
|
|
|
212
220
|
### `ghostdock whoami`
|
|
213
221
|
|
|
@@ -478,31 +486,75 @@ secret is public.
|
|
|
478
486
|
|
|
479
487
|
### `ghostdock network`
|
|
480
488
|
|
|
481
|
-
Reach your
|
|
489
|
+
Reach your organizations' sandboxes from this machine by private address and name, over
|
|
482
490
|
WireGuard ([ADR 0027](../../docs/adr/0027-private-sandbox-network.md)). Everything on a
|
|
483
491
|
sandbox's address is reachable: a container that publishes port 80 answers on
|
|
484
|
-
`http://quiet-harbor.
|
|
492
|
+
`http://quiet-harbor.acme.ghostdock.internal/`, as it would on `localhost` inside the sandbox.
|
|
485
493
|
Nothing reaches your machine from a sandbox.
|
|
486
494
|
|
|
487
495
|
```bash
|
|
488
|
-
ghostdock network connect
|
|
489
|
-
ghostdock network
|
|
496
|
+
ghostdock network connect # your organization's default provider; sudo once
|
|
497
|
+
ghostdock network connect helsinki # or name a provider (name or id)
|
|
498
|
+
ghostdock network ls --org acme # sandboxes, addresses, names, and what is reachable
|
|
490
499
|
curl http://quiet-harbor.acme.ghostdock.internal:8080/
|
|
491
|
-
|
|
492
|
-
ghostdock network use beta # short names in another organization, no sudo
|
|
500
|
+
open https://quiet-harbor--acme.ghostdock.net:8080/ # in a browser: HTTPS, any port
|
|
493
501
|
ghostdock network status
|
|
494
502
|
ghostdock network disconnect
|
|
495
503
|
```
|
|
496
504
|
|
|
505
|
+
- **One provider at a time.** Each sandbox provider is its own gateway, and a device reaches
|
|
506
|
+
only the sandboxes on the provider it connected through. Without an argument, `connect` takes
|
|
507
|
+
the organization (`--org`, `GHOSTDOCK_ORG`, the default from `organizations use`) and its
|
|
508
|
+
default sandbox provider, else the platform default. Connecting through another provider
|
|
509
|
+
replaces the connection: same interface and address, new gateway. `network status` says which
|
|
510
|
+
provider you are connected through; `network ls` marks sandboxes elsewhere as
|
|
511
|
+
`not reachable: on <provider>` with the `connect` command that reaches them. A provider
|
|
512
|
+
without a private network (Incus, for now) is refused by the server; one your organization
|
|
513
|
+
may not use answers 403.
|
|
497
514
|
- **Needs `wg-quick`** (`brew install wireguard-tools`, `apt install wireguard-tools`). macOS
|
|
498
515
|
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
|
|
516
|
+
- **One key per machine**, kept in `~/.config/ghostdock/network/`.
|
|
500
517
|
Only the public key is sent. The device belongs to the login it was connected with:
|
|
501
518
|
`ghostdock logout` takes it out of the network, and so does not connecting for 90 days.
|
|
502
|
-
- **Names**: `<sandbox>.<org>.ghostdock.internal
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
519
|
+
- **Names**: `<sandbox>.<org>.ghostdock.internal`, for every organization you are a member
|
|
520
|
+
of. A name always says its organization, so a browser origin never changes hands between
|
|
521
|
+
organizations. Only AAAA records exist: the network is IPv6, and the sandbox sees IPv4.
|
|
522
|
+
- **In a browser**, use the HTTPS name `https://<sandbox>--<org>.ghostdock.net:<port>`
|
|
523
|
+
(`network ls` shows it): any port, a real certificate, so the page is a secure context
|
|
524
|
+
(clipboard, `Secure` cookies, service workers). The host's `gdo-ingress` terminates TLS and
|
|
525
|
+
forwards to the sandbox's port; it serves only devices of the sandbox's organization, and
|
|
526
|
+
refuses background requests (fetch, forms, iframes, websockets) from pages outside that
|
|
527
|
+
organization; pages on another HTTPS name of the same organization, any port, may call it. The
|
|
528
|
+
sandbox sees the request from `198.18.0.0`, and your device's address in
|
|
529
|
+
`X-Forwarded-For`. The desktop image's Desktop opens only this way, never on the raw
|
|
530
|
+
address.
|
|
531
|
+
- **Browsers use the HTTPS name; tools keep the raw address.** A browser that opens
|
|
532
|
+
`http://quiet-harbor.acme.ghostdock.internal:3000/app` (or the raw IPv6 address) gets a
|
|
533
|
+
`308` to `https://quiet-harbor--acme.ghostdock.net:3000/app`. Any other browser request on
|
|
534
|
+
the raw address (`fetch`, images, scripts, forms, websockets) gets `403` with
|
|
535
|
+
`X-Ghostdock-Blocked: browser-https-only` and the HTTPS address in the body. This keeps
|
|
536
|
+
websites you visit while connected from using your connection against your sandboxes,
|
|
537
|
+
including through DNS rebinding.
|
|
538
|
+
- **What counts as a browser:** a `User-Agent` that starts with `Mozilla/`, which every
|
|
539
|
+
browser sends on its own requests. Browsers send no `Sec-Fetch-*` headers over plain
|
|
540
|
+
`http://` to an address like this, so those cannot be the signal. (Firefox lets a page
|
|
541
|
+
set its own `User-Agent` on a `fetch`; the `Host` check below stops that one.) Tools that send a
|
|
542
|
+
browser's `User-Agent` (Playwright, Puppeteer, their request APIs) count as browsers too:
|
|
543
|
+
point them at the HTTPS name. A frontend outside the sandbox (a dev server on your laptop)
|
|
544
|
+
calls the sandbox's API by its HTTPS name as well.
|
|
545
|
+
- **Tools** (`curl`, SDKs, test runners, Prometheus) pass untouched, and every other
|
|
546
|
+
protocol does too, as long as they address the sandbox by its IP address, its
|
|
547
|
+
`.internal` name (or a container's, `web.quiet-harbor.acme.ghostdock.internal`), or
|
|
548
|
+
`localhost`: any other `Host` gets `421` (`X-Ghostdock-Blocked: host-not-allowed`). That
|
|
549
|
+
check is what stops DNS rebinding for a page that sets its own `User-Agent`. A name of
|
|
550
|
+
your own in `/etc/hosts` gets 421 too.
|
|
551
|
+
- **One difference for tools:** a port where nothing listens accepts the connection and
|
|
552
|
+
then resets it, so port checks such as `nc -z` report it open; check with a real request
|
|
553
|
+
instead.
|
|
554
|
+
- **In the sandbox,** firewall rules for these clients belong in the input path
|
|
555
|
+
(`iptables -A INPUT`, an nft `hook input` chain): `FORWARD` and `DOCKER-USER` rules no
|
|
556
|
+
longer see them, because the guard connects to a published container port from inside
|
|
557
|
+
the VM.
|
|
506
558
|
- `ghostdock network devices` lists your devices, `ghostdock network devices remove <id>`
|
|
507
559
|
removes one.
|
|
508
560
|
|
|
@@ -544,7 +596,7 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
544
596
|
|
|
545
597
|
| Command | What it does |
|
|
546
598
|
| --- | --- |
|
|
547
|
-
| `ghostdock login [flags]` | Sign in and store a device access token for this
|
|
599
|
+
| `ghostdock login [flags]` | Sign in and store a device access token for this machine… |
|
|
548
600
|
|
|
549
601
|
#### logout
|
|
550
602
|
|
|
@@ -556,13 +608,12 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
556
608
|
|
|
557
609
|
| Command | What it does |
|
|
558
610
|
| --- | --- |
|
|
559
|
-
| `ghostdock network connect [flags]` | Put this machine into the private network: WireGuard and
|
|
611
|
+
| `ghostdock network connect [provider] [flags]` | Put this machine into the private network through one provider: WireGuard and… |
|
|
560
612
|
| `ghostdock network devices <command>` | Your devices in the private network. |
|
|
561
613
|
| `ghostdock network devices remove <id>` | Remove one of your devices from the private network. |
|
|
562
614
|
| `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… |
|
|
615
|
+
| `ghostdock network ls [flags]` | Sandboxes of an organization with their private addresses and names, and which… |
|
|
616
|
+
| `ghostdock network status` | Whether this machine is connected, through which provider, and as which device. |
|
|
566
617
|
|
|
567
618
|
#### organizations
|
|
568
619
|
|
|
@@ -570,7 +621,7 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
570
621
|
| --- | --- |
|
|
571
622
|
| `ghostdock organizations current [flags]` | Show which organization commands use, and where it comes from. |
|
|
572
623
|
| `ghostdock organizations list` | List the organizations you belong to, with their ids and slugs. |
|
|
573
|
-
| `ghostdock organizations use [org] [flags]` | Set the default organization for commands run in
|
|
624
|
+
| `ghostdock organizations use [org] [flags]` | Set the default organization for commands run without --org (in every… |
|
|
574
625
|
|
|
575
626
|
#### projects
|
|
576
627
|
|
|
@@ -694,7 +745,8 @@ and secrets, see [`ghostdock env`](#ghostdock-env).
|
|
|
694
745
|
| ------------------------------- | ------------------------------------------------------------- | --------------------------------- |
|
|
695
746
|
| `GHOSTDOCK_BASE_URL` | API origin the CLI talks to. Override for local dev / self-hosted instances. | Local/dev builds default to `http://localhost:3000`. npm-published builds default to the production API origin baked at publish time. Override with `--base-url` or this env var. |
|
|
696
747
|
| `GHOSTDOCK_ORG` | Organization (id or slug) for commands run without `--org`. Takes precedence over the default from `organizations use`. | unset |
|
|
697
|
-
| `
|
|
748
|
+
| `XDG_CONFIG_HOME` | Base directory for the CLI's per-machine state (`ghostdock/credentials.json`, `ghostdock/settings.json`, `ghostdock/network/`). | `~/.config` (native Windows: `%APPDATA%`) |
|
|
749
|
+
| `GHOSTDOCK_CREDENTIALS_PATH` | Absolute path to the credentials file, overriding the default. `settings.json` (the default organization) sits in the same directory, so each credentials file keeps its own default — handy for a second account. | `~/.config/ghostdock/credentials.json` |
|
|
698
750
|
|
|
699
751
|
Example for local development against a Nuxt dev server:
|
|
700
752
|
|
|
@@ -706,8 +758,10 @@ GHOSTDOCK_BASE_URL=http://localhost:3000 ghostdock login --wait
|
|
|
706
758
|
|
|
707
759
|
## Credentials file
|
|
708
760
|
|
|
709
|
-
Credentials are stored as JSON
|
|
710
|
-
|
|
761
|
+
Credentials are stored per machine as JSON in `~/.config/ghostdock/credentials.json`
|
|
762
|
+
(`$XDG_CONFIG_HOME/ghostdock/` when that is set, `%APPDATA%\ghostdock\` on native Windows; WSL
|
|
763
|
+
counts as Linux), with file permissions restricted to the owner (`0600`). `GHOSTDOCK_CREDENTIALS_PATH`
|
|
764
|
+
points the CLI at another file, e.g. for a second account. The file looks like this:
|
|
711
765
|
|
|
712
766
|
```json
|
|
713
767
|
{
|
|
@@ -717,10 +771,6 @@ CLI is run from), with file permissions restricted to the owner (`0600`):
|
|
|
717
771
|
}
|
|
718
772
|
```
|
|
719
773
|
|
|
720
|
-
**Add `.ghostdock/` to your project's `.gitignore`.** These are live tokens — never commit them.
|
|
721
|
-
This repository's own `.gitignore` already ignores `.ghostdock/` for local development/testing of
|
|
722
|
-
the CLI itself.
|
|
723
|
-
|
|
724
774
|
Access tokens are short-lived (1 hour); the CLI refreshes them automatically using the refresh token
|
|
725
775
|
(30-day rolling expiry) whenever a request gets a `401`. If both tokens are invalid or revoked (e.g.
|
|
726
776
|
after `ghostdock logout` from another session), re-run `ghostdock login`.
|
package/dist/index.js
CHANGED
|
@@ -1121,6 +1121,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
1121
1121
|
},
|
|
1122
1122
|
mcp: {
|
|
1123
1123
|
type: "boolean"
|
|
1124
|
+
},
|
|
1125
|
+
private_network: {
|
|
1126
|
+
type: "boolean"
|
|
1124
1127
|
}
|
|
1125
1128
|
},
|
|
1126
1129
|
required: [
|
|
@@ -1135,7 +1138,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
1135
1138
|
"compose",
|
|
1136
1139
|
"kubernetes",
|
|
1137
1140
|
"env",
|
|
1138
|
-
"mcp"
|
|
1141
|
+
"mcp",
|
|
1142
|
+
"private_network"
|
|
1139
1143
|
],
|
|
1140
1144
|
additionalProperties: false
|
|
1141
1145
|
},
|
|
@@ -1563,6 +1567,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
1563
1567
|
},
|
|
1564
1568
|
mcp: {
|
|
1565
1569
|
type: "boolean"
|
|
1570
|
+
},
|
|
1571
|
+
private_network: {
|
|
1572
|
+
type: "boolean"
|
|
1566
1573
|
}
|
|
1567
1574
|
},
|
|
1568
1575
|
required: [
|
|
@@ -1577,7 +1584,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
1577
1584
|
"compose",
|
|
1578
1585
|
"kubernetes",
|
|
1579
1586
|
"env",
|
|
1580
|
-
"mcp"
|
|
1587
|
+
"mcp",
|
|
1588
|
+
"private_network"
|
|
1581
1589
|
],
|
|
1582
1590
|
additionalProperties: false
|
|
1583
1591
|
},
|
|
@@ -2102,6 +2110,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2102
2110
|
},
|
|
2103
2111
|
mcp: {
|
|
2104
2112
|
type: "boolean"
|
|
2113
|
+
},
|
|
2114
|
+
private_network: {
|
|
2115
|
+
type: "boolean"
|
|
2105
2116
|
}
|
|
2106
2117
|
},
|
|
2107
2118
|
required: [
|
|
@@ -2116,7 +2127,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2116
2127
|
"compose",
|
|
2117
2128
|
"kubernetes",
|
|
2118
2129
|
"env",
|
|
2119
|
-
"mcp"
|
|
2130
|
+
"mcp",
|
|
2131
|
+
"private_network"
|
|
2120
2132
|
],
|
|
2121
2133
|
additionalProperties: false
|
|
2122
2134
|
},
|
|
@@ -2495,6 +2507,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2495
2507
|
},
|
|
2496
2508
|
mcp: {
|
|
2497
2509
|
type: "boolean"
|
|
2510
|
+
},
|
|
2511
|
+
private_network: {
|
|
2512
|
+
type: "boolean"
|
|
2498
2513
|
}
|
|
2499
2514
|
},
|
|
2500
2515
|
required: [
|
|
@@ -2509,7 +2524,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2509
2524
|
"compose",
|
|
2510
2525
|
"kubernetes",
|
|
2511
2526
|
"env",
|
|
2512
|
-
"mcp"
|
|
2527
|
+
"mcp",
|
|
2528
|
+
"private_network"
|
|
2513
2529
|
],
|
|
2514
2530
|
additionalProperties: false
|
|
2515
2531
|
},
|
|
@@ -2949,6 +2965,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2949
2965
|
},
|
|
2950
2966
|
mcp: {
|
|
2951
2967
|
type: "boolean"
|
|
2968
|
+
},
|
|
2969
|
+
private_network: {
|
|
2970
|
+
type: "boolean"
|
|
2952
2971
|
}
|
|
2953
2972
|
},
|
|
2954
2973
|
required: [
|
|
@@ -2963,7 +2982,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
2963
2982
|
"compose",
|
|
2964
2983
|
"kubernetes",
|
|
2965
2984
|
"env",
|
|
2966
|
-
"mcp"
|
|
2985
|
+
"mcp",
|
|
2986
|
+
"private_network"
|
|
2967
2987
|
],
|
|
2968
2988
|
additionalProperties: false
|
|
2969
2989
|
},
|
|
@@ -3339,6 +3359,9 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
3339
3359
|
},
|
|
3340
3360
|
mcp: {
|
|
3341
3361
|
type: "boolean"
|
|
3362
|
+
},
|
|
3363
|
+
private_network: {
|
|
3364
|
+
type: "boolean"
|
|
3342
3365
|
}
|
|
3343
3366
|
},
|
|
3344
3367
|
required: [
|
|
@@ -3353,7 +3376,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
3353
3376
|
"compose",
|
|
3354
3377
|
"kubernetes",
|
|
3355
3378
|
"env",
|
|
3356
|
-
"mcp"
|
|
3379
|
+
"mcp",
|
|
3380
|
+
"private_network"
|
|
3357
3381
|
],
|
|
3358
3382
|
additionalProperties: false
|
|
3359
3383
|
},
|
|
@@ -8555,12 +8579,16 @@ var BUILTIN_SKILL_NAMES = GENERATED_SKILL_NAMES;
|
|
|
8555
8579
|
// src/cli-name.ts
|
|
8556
8580
|
import { basename } from "node:path";
|
|
8557
8581
|
var CLI_NAMES = ["ghostdock", "gdock"];
|
|
8582
|
+
var NPX_CACHE_SEGMENT = /[\\/]_npx[\\/]/;
|
|
8558
8583
|
function getCliName(argv = process.argv) {
|
|
8559
|
-
const
|
|
8584
|
+
const entry = argv[1] ?? "";
|
|
8585
|
+
if (NPX_CACHE_SEGMENT.test(entry))
|
|
8586
|
+
return "npx ghostdock";
|
|
8587
|
+
const invoked = basename(entry);
|
|
8560
8588
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
8561
8589
|
}
|
|
8562
8590
|
// package.json
|
|
8563
|
-
var version = "0.0.
|
|
8591
|
+
var version = "0.0.11";
|
|
8564
8592
|
var package_default = {
|
|
8565
8593
|
name: "ghostdock",
|
|
8566
8594
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -8597,6 +8625,7 @@ var package_default = {
|
|
|
8597
8625
|
};
|
|
8598
8626
|
|
|
8599
8627
|
// src/config.ts
|
|
8628
|
+
import { homedir } from "node:os";
|
|
8600
8629
|
import { join } from "node:path";
|
|
8601
8630
|
var FALLBACK_DEFAULT_BASE_URL = "http://localhost:3000";
|
|
8602
8631
|
var cliBaseUrlOverride;
|
|
@@ -8618,9 +8647,22 @@ function getBaseUrl() {
|
|
|
8618
8647
|
return fromEnv.replace(/\/+$/, "");
|
|
8619
8648
|
return resolveDefaultBaseUrl();
|
|
8620
8649
|
}
|
|
8650
|
+
function getConfigDir() {
|
|
8651
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
8652
|
+
if (xdg)
|
|
8653
|
+
return join(xdg, "ghostdock");
|
|
8654
|
+
const appData = process.env.APPDATA?.trim();
|
|
8655
|
+
if (process.platform === "win32" && appData)
|
|
8656
|
+
return join(appData, "ghostdock");
|
|
8657
|
+
return join(homedir(), ".config", "ghostdock");
|
|
8658
|
+
}
|
|
8621
8659
|
function getCredentialsPath() {
|
|
8622
8660
|
const override = process.env.GHOSTDOCK_CREDENTIALS_PATH?.trim();
|
|
8623
|
-
return override || join(
|
|
8661
|
+
return override || join(getConfigDir(), "credentials.json");
|
|
8662
|
+
}
|
|
8663
|
+
function displayPath(path) {
|
|
8664
|
+
const home = homedir();
|
|
8665
|
+
return home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
8624
8666
|
}
|
|
8625
8667
|
|
|
8626
8668
|
// src/credentials.ts
|
|
@@ -8882,7 +8924,8 @@ async function findOrganizationRef(flag) {
|
|
|
8882
8924
|
return {
|
|
8883
8925
|
ref: defaultOrganization.id,
|
|
8884
8926
|
source: "default",
|
|
8885
|
-
label: defaultOrganization.slug ?? defaultOrganization.id
|
|
8927
|
+
label: defaultOrganization.slug ?? defaultOrganization.id,
|
|
8928
|
+
path: getSettingsPath()
|
|
8886
8929
|
};
|
|
8887
8930
|
}
|
|
8888
8931
|
return null;
|
|
@@ -8893,15 +8936,15 @@ async function requireOrganizationRef(flag, notice = printNotice) {
|
|
|
8893
8936
|
throw new Error(`No organization. Pass --org <id-or-slug>, set GHOSTDOCK_ORG, or run \`${getCliName()} organizations use <org>\`.`);
|
|
8894
8937
|
}
|
|
8895
8938
|
if (found.source !== "flag")
|
|
8896
|
-
notice(`Using organization ${found.label} (${describeSource(found
|
|
8939
|
+
notice(`Using organization ${found.label} (${describeSource(found)}).`);
|
|
8897
8940
|
return found.ref;
|
|
8898
8941
|
}
|
|
8899
|
-
function describeSource(source) {
|
|
8942
|
+
function describeSource({ source, path }) {
|
|
8900
8943
|
if (source === "flag")
|
|
8901
8944
|
return "--org";
|
|
8902
8945
|
if (source === "env")
|
|
8903
8946
|
return "GHOSTDOCK_ORG";
|
|
8904
|
-
return `default
|
|
8947
|
+
return `default from ${displayPath(path ?? getSettingsPath())}`;
|
|
8905
8948
|
}
|
|
8906
8949
|
async function withOrganization(flag, run) {
|
|
8907
8950
|
let organization;
|
|
@@ -9746,7 +9789,7 @@ async function runLogin(args, log = console.log, choose = defaultChooser(), erro
|
|
|
9746
9789
|
var loginCommand = defineCommand({
|
|
9747
9790
|
meta: {
|
|
9748
9791
|
name: "login",
|
|
9749
|
-
description: "Sign in and store a device access token for this
|
|
9792
|
+
description: "Sign in and store a device access token for this machine (~/.config/ghostdock/credentials.json)."
|
|
9750
9793
|
},
|
|
9751
9794
|
args: {
|
|
9752
9795
|
wait: {
|
|
@@ -9805,15 +9848,10 @@ import { hostname as hostname2 } from "node:os";
|
|
|
9805
9848
|
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
9806
9849
|
import { existsSync } from "node:fs";
|
|
9807
9850
|
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
9808
|
-
import { homedir } from "node:os";
|
|
9809
9851
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9810
9852
|
var INTERFACE = "gdock";
|
|
9811
9853
|
function networkDir() {
|
|
9812
|
-
|
|
9813
|
-
if (override)
|
|
9814
|
-
return override;
|
|
9815
|
-
const base = process.env.XDG_CONFIG_HOME?.trim() || join3(homedir(), ".config");
|
|
9816
|
-
return join3(base, "ghostdock", "network");
|
|
9854
|
+
return join3(getConfigDir(), "network");
|
|
9817
9855
|
}
|
|
9818
9856
|
function configPath(dir = networkDir()) {
|
|
9819
9857
|
return join3(dir, `${INTERFACE}.conf`);
|
|
@@ -9824,6 +9862,10 @@ function keyPath(dir) {
|
|
|
9824
9862
|
function statePath(dir) {
|
|
9825
9863
|
return join3(dir, "device.json");
|
|
9826
9864
|
}
|
|
9865
|
+
function isProvider(value) {
|
|
9866
|
+
const provider = value;
|
|
9867
|
+
return typeof provider?.id === "string" && typeof provider.name === "string";
|
|
9868
|
+
}
|
|
9827
9869
|
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
9828
9870
|
function keyPairFromPrivate(privateKey) {
|
|
9829
9871
|
const raw = Buffer.from(privateKey, "base64");
|
|
@@ -9859,7 +9901,8 @@ async function readDeviceState(dir = networkDir()) {
|
|
|
9859
9901
|
return {
|
|
9860
9902
|
baseUrl: parsed.baseUrl,
|
|
9861
9903
|
deviceId: parsed.deviceId,
|
|
9862
|
-
address: typeof parsed.address === "string" ? parsed.address : ""
|
|
9904
|
+
address: typeof parsed.address === "string" ? parsed.address : "",
|
|
9905
|
+
gatewayProvider: isProvider(parsed.gatewayProvider) ? { id: parsed.gatewayProvider.id, name: parsed.gatewayProvider.name } : null
|
|
9863
9906
|
};
|
|
9864
9907
|
}
|
|
9865
9908
|
} catch {}
|
|
@@ -9879,7 +9922,14 @@ function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
|
9879
9922
|
}
|
|
9880
9923
|
function renderConfig(input) {
|
|
9881
9924
|
const { device, network, gateway } = input.registered;
|
|
9882
|
-
const zones = [network.
|
|
9925
|
+
const zones = [network.zone, ...network.netZone ? [network.netZone] : []];
|
|
9926
|
+
for (const zone of zones) {
|
|
9927
|
+
if (!isDnsZone(zone))
|
|
9928
|
+
throw new Error(`The server sent a zone that is not a DNS name: ${zone}`);
|
|
9929
|
+
}
|
|
9930
|
+
if (!isIPv6Address(network.gatewayAddress)) {
|
|
9931
|
+
throw new Error(`The server sent a gateway that is not an IPv6 address: ${network.gatewayAddress}`);
|
|
9932
|
+
}
|
|
9883
9933
|
const lines = [
|
|
9884
9934
|
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9885
9935
|
"[Interface]",
|
|
@@ -9888,15 +9938,26 @@ function renderConfig(input) {
|
|
|
9888
9938
|
`MTU = ${network.mtu}`
|
|
9889
9939
|
];
|
|
9890
9940
|
if (input.dns === "macos-resolver") {
|
|
9891
|
-
const
|
|
9892
|
-
|
|
9941
|
+
for (const zone of zones) {
|
|
9942
|
+
const file = `/etc/resolver/${zone}`;
|
|
9943
|
+
lines.push(`PostUp = mkdir -p /etc/resolver && printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`, `PostDown = rm -f ${file}`);
|
|
9944
|
+
}
|
|
9893
9945
|
} else if (input.dns === "resolved") {
|
|
9894
|
-
|
|
9946
|
+
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
9947
|
+
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
9895
9948
|
}
|
|
9896
9949
|
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.platformPrefix}`, "PersistentKeepalive = 25", "");
|
|
9897
9950
|
return lines.join(`
|
|
9898
9951
|
`);
|
|
9899
9952
|
}
|
|
9953
|
+
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
9954
|
+
function isDnsZone(zone) {
|
|
9955
|
+
const labels = zone.split(".");
|
|
9956
|
+
return zone.length <= 253 && labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
9957
|
+
}
|
|
9958
|
+
function isIPv6Address(text) {
|
|
9959
|
+
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
9960
|
+
}
|
|
9900
9961
|
async function writeConfig(contents, path = configPath()) {
|
|
9901
9962
|
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9902
9963
|
await writeFile3(path, contents, { mode: 384 });
|
|
@@ -9935,28 +9996,36 @@ function defaultDeps() {
|
|
|
9935
9996
|
function message(err) {
|
|
9936
9997
|
return err instanceof Error ? err.message : String(err);
|
|
9937
9998
|
}
|
|
9999
|
+
function connectCommandFor(provider) {
|
|
10000
|
+
const ref = provider.connectRef;
|
|
10001
|
+
const arg = /^[A-Za-z0-9._-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
10002
|
+
return `${getCliName()} network connect ${arg}`;
|
|
10003
|
+
}
|
|
9938
10004
|
async function wgQuick(deps, verb) {
|
|
9939
10005
|
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
9940
10006
|
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
9941
10007
|
return deps.run("sudo", args);
|
|
9942
10008
|
}
|
|
9943
|
-
async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
10009
|
+
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
9944
10010
|
if (!deps.has("wg-quick")) {
|
|
9945
10011
|
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
9946
10012
|
return 1;
|
|
9947
10013
|
}
|
|
9948
10014
|
let registered;
|
|
9949
10015
|
let privateKey;
|
|
10016
|
+
const previous = await readDeviceState(deps.dir);
|
|
9950
10017
|
try {
|
|
10018
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10019
|
+
const provider = args.provider?.trim();
|
|
9951
10020
|
const key = await loadOrCreateKey(deps.dir);
|
|
9952
10021
|
privateKey = key.privateKey;
|
|
9953
|
-
const found = await findOrganizationRef(args.org);
|
|
9954
10022
|
registered = await deps.request("/api/v1/network/devices", {
|
|
9955
10023
|
method: "POST",
|
|
9956
10024
|
body: JSON.stringify({
|
|
9957
10025
|
publicKey: key.publicKey,
|
|
9958
10026
|
name: hostname2().slice(0, 64) || "device",
|
|
9959
|
-
|
|
10027
|
+
organization,
|
|
10028
|
+
...provider ? { provider } : {}
|
|
9960
10029
|
})
|
|
9961
10030
|
});
|
|
9962
10031
|
} catch (err) {
|
|
@@ -9964,9 +10033,22 @@ async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
|
9964
10033
|
return 1;
|
|
9965
10034
|
}
|
|
9966
10035
|
const dns = detectDnsMode();
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
10036
|
+
let config;
|
|
10037
|
+
try {
|
|
10038
|
+
config = renderConfig({ privateKey, registered, dns });
|
|
10039
|
+
} catch (err) {
|
|
10040
|
+
deps.error(message(err));
|
|
10041
|
+
return 1;
|
|
10042
|
+
}
|
|
10043
|
+
await writeConfig(config, configPath(deps.dir));
|
|
10044
|
+
await writeDeviceState({
|
|
10045
|
+
baseUrl: getBaseUrl(),
|
|
10046
|
+
deviceId: registered.device.id,
|
|
10047
|
+
address: registered.device.address,
|
|
10048
|
+
gatewayProvider: registered.provider
|
|
10049
|
+
}, deps.dir);
|
|
10050
|
+
const wasUp = deps.up();
|
|
10051
|
+
if (wasUp) {
|
|
9970
10052
|
const down = await wgQuick(deps, "down");
|
|
9971
10053
|
if (down !== 0) {
|
|
9972
10054
|
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
@@ -9978,10 +10060,16 @@ async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
|
9978
10060
|
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
9979
10061
|
return 1;
|
|
9980
10062
|
}
|
|
9981
|
-
const { device, network } = registered;
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
`
|
|
10063
|
+
const { device, network, provider } = registered;
|
|
10064
|
+
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10065
|
+
printResult({ device, network, provider, interface: INTERFACE, dns }, () => [
|
|
10066
|
+
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10067
|
+
...switched ? [
|
|
10068
|
+
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
10069
|
+
] : [],
|
|
10070
|
+
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
10071
|
+
` Names: <sandbox>.<org>.${network.zone}`,
|
|
10072
|
+
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
9985
10073
|
...dns === "none" ? [
|
|
9986
10074
|
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
9987
10075
|
` \`${getCliName()} network ls\`.`
|
|
@@ -10003,44 +10091,89 @@ async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
|
10003
10091
|
printResult({ connected: false }, () => [eventLine("done", "Disconnected.")], deps.log);
|
|
10004
10092
|
return 0;
|
|
10005
10093
|
}
|
|
10006
|
-
async function
|
|
10094
|
+
async function readConnection(deps) {
|
|
10007
10095
|
const state = await readDeviceState(deps.dir);
|
|
10008
|
-
const
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10096
|
+
const up = deps.up();
|
|
10097
|
+
if (!state)
|
|
10098
|
+
return { state, up, device: null, asked: true, provider: null };
|
|
10099
|
+
try {
|
|
10100
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10101
|
+
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10102
|
+
return {
|
|
10103
|
+
state,
|
|
10104
|
+
up,
|
|
10105
|
+
device,
|
|
10106
|
+
asked: true,
|
|
10107
|
+
provider: up ? device?.gatewayProvider ?? null : null
|
|
10108
|
+
};
|
|
10109
|
+
} catch (err) {
|
|
10110
|
+
deps.notice(`Could not ask the app about this device: ${message(err)}`);
|
|
10111
|
+
return { state, up, device: null, asked: false, provider: up ? state.gatewayProvider : null };
|
|
10017
10112
|
}
|
|
10018
|
-
|
|
10113
|
+
}
|
|
10114
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10115
|
+
const { state, up, device, asked, provider } = await readConnection(deps);
|
|
10116
|
+
printResult({ connected: up, device, provider }, () => {
|
|
10019
10117
|
if (!state) {
|
|
10020
10118
|
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10021
10119
|
}
|
|
10120
|
+
const through = provider ? ` through ${provider.name}` : "";
|
|
10022
10121
|
const lines = [
|
|
10023
|
-
eventLine("item", `${
|
|
10122
|
+
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
10024
10123
|
];
|
|
10025
|
-
if (
|
|
10026
|
-
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10027
|
-
} else {
|
|
10124
|
+
if (device) {
|
|
10028
10125
|
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
10126
|
+
if (!device.gatewayProvider) {
|
|
10127
|
+
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
10128
|
+
}
|
|
10129
|
+
} else if (asked) {
|
|
10130
|
+
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10029
10131
|
}
|
|
10132
|
+
if (provider)
|
|
10133
|
+
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
10030
10134
|
return lines;
|
|
10031
10135
|
}, deps.log);
|
|
10032
10136
|
return 0;
|
|
10033
10137
|
}
|
|
10138
|
+
function unreachableNote(sandbox, connected) {
|
|
10139
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10140
|
+
return `not reachable: ${sandbox.provider.name} has no private network yet`;
|
|
10141
|
+
}
|
|
10142
|
+
if (connected?.id === sandbox.provider.id)
|
|
10143
|
+
return null;
|
|
10144
|
+
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
10145
|
+
}
|
|
10034
10146
|
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10035
10147
|
try {
|
|
10036
10148
|
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10037
10149
|
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
|
|
10038
|
-
|
|
10150
|
+
const connected = (await readConnection(deps)).provider;
|
|
10151
|
+
const sandboxes = listed.sandboxes.map((sandbox) => ({
|
|
10152
|
+
...sandbox,
|
|
10153
|
+
reachable: unreachableNote(sandbox, connected) === null
|
|
10154
|
+
}));
|
|
10155
|
+
printResult({ ...listed, connectedProvider: connected, sandboxes }, () => {
|
|
10039
10156
|
if (listed.sandboxes.length === 0) {
|
|
10040
10157
|
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10041
10158
|
}
|
|
10042
10159
|
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10043
|
-
|
|
10160
|
+
const canonicalWidth = Math.max(...listed.sandboxes.map((s) => s.canonicalName.length));
|
|
10161
|
+
const httpsWidth = Math.max(...listed.sandboxes.map((s) => (s.httpsName?.length ?? 0) + 8));
|
|
10162
|
+
const lines = [
|
|
10163
|
+
connected ? eventLine("item", `Connected through ${connected.name}.`) : eventLine("item", `Not connected: nothing is reachable. Run \`${getCliName()} network connect [provider]\`.`)
|
|
10164
|
+
];
|
|
10165
|
+
for (const s of listed.sandboxes) {
|
|
10166
|
+
const note = unreachableNote(s, connected);
|
|
10167
|
+
lines.push([
|
|
10168
|
+
` ${s.name.padEnd(width)}`,
|
|
10169
|
+
s.address.padEnd(28),
|
|
10170
|
+
s.canonicalName.padEnd(canonicalWidth),
|
|
10171
|
+
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
10172
|
+
s.status,
|
|
10173
|
+
...note ? [`(${note})`] : []
|
|
10174
|
+
].join(" "));
|
|
10175
|
+
}
|
|
10176
|
+
return lines;
|
|
10044
10177
|
}, deps.log);
|
|
10045
10178
|
return 0;
|
|
10046
10179
|
} catch (err) {
|
|
@@ -10048,28 +10181,6 @@ async function runNetworkLs(args, deps = defaultDeps()) {
|
|
|
10048
10181
|
return 1;
|
|
10049
10182
|
}
|
|
10050
10183
|
}
|
|
10051
|
-
async function runNetworkUse(args, deps = defaultDeps()) {
|
|
10052
|
-
const state = await readDeviceState(deps.dir);
|
|
10053
|
-
if (!state) {
|
|
10054
|
-
deps.error(`Not set up. Run \`${getCliName()} network connect\` first.`);
|
|
10055
|
-
return 1;
|
|
10056
|
-
}
|
|
10057
|
-
const organization = args.clear ? null : args.org?.trim();
|
|
10058
|
-
if (organization === undefined || organization === "") {
|
|
10059
|
-
deps.error("Pass an organization id or slug, or --clear.");
|
|
10060
|
-
return 1;
|
|
10061
|
-
}
|
|
10062
|
-
try {
|
|
10063
|
-
const device = await deps.request(`/api/v1/network/devices/${encodeURIComponent(state.deviceId)}/short-name-organization`, { method: "PUT", body: JSON.stringify({ organization }) });
|
|
10064
|
-
printResult(device, () => [
|
|
10065
|
-
eventLine("done", organization === null ? "Short names are off for this machine." : `Short names (<sandbox>.gdock.internal) now resolve in ${organization}.`)
|
|
10066
|
-
], deps.log);
|
|
10067
|
-
return 0;
|
|
10068
|
-
} catch (err) {
|
|
10069
|
-
deps.error(message(err));
|
|
10070
|
-
return 1;
|
|
10071
|
-
}
|
|
10072
|
-
}
|
|
10073
10184
|
async function runNetworkDevices(deps = defaultDeps()) {
|
|
10074
10185
|
try {
|
|
10075
10186
|
const { devices } = await deps.request("/api/v1/network/devices");
|
|
@@ -10096,11 +10207,18 @@ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
|
10096
10207
|
var connectCommand = defineCommand({
|
|
10097
10208
|
meta: {
|
|
10098
10209
|
name: "connect",
|
|
10099
|
-
description: "Put this machine into the private network: WireGuard and split DNS. Asks for sudo once."
|
|
10210
|
+
description: "Put this machine into the private network through one provider: WireGuard and split DNS. Asks for sudo once."
|
|
10211
|
+
},
|
|
10212
|
+
args: {
|
|
10213
|
+
provider: {
|
|
10214
|
+
type: "positional",
|
|
10215
|
+
required: false,
|
|
10216
|
+
description: "Sandbox provider name or id. Omit for the organization's default provider (else the platform default). Only its sandboxes are reachable; connecting through another replaces the connection."
|
|
10217
|
+
},
|
|
10218
|
+
org: orgArg
|
|
10100
10219
|
},
|
|
10101
|
-
args: { org: orgArg },
|
|
10102
10220
|
async run({ args }) {
|
|
10103
|
-
process.exitCode = await runNetworkConnect({ org: args.org });
|
|
10221
|
+
process.exitCode = await runNetworkConnect({ provider: args.provider, org: args.org });
|
|
10104
10222
|
}
|
|
10105
10223
|
});
|
|
10106
10224
|
var disconnectCommand = defineCommand({
|
|
@@ -10110,7 +10228,10 @@ var disconnectCommand = defineCommand({
|
|
|
10110
10228
|
}
|
|
10111
10229
|
});
|
|
10112
10230
|
var statusCommand = defineCommand({
|
|
10113
|
-
meta: {
|
|
10231
|
+
meta: {
|
|
10232
|
+
name: "status",
|
|
10233
|
+
description: "Whether this machine is connected, through which provider, and as which device."
|
|
10234
|
+
},
|
|
10114
10235
|
async run() {
|
|
10115
10236
|
process.exitCode = await runNetworkStatus();
|
|
10116
10237
|
}
|
|
@@ -10118,26 +10239,13 @@ var statusCommand = defineCommand({
|
|
|
10118
10239
|
var lsCommand = defineCommand({
|
|
10119
10240
|
meta: {
|
|
10120
10241
|
name: "ls",
|
|
10121
|
-
description: "Sandboxes of an organization with their private addresses and names."
|
|
10242
|
+
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
10122
10243
|
},
|
|
10123
10244
|
args: { org: orgArg },
|
|
10124
10245
|
async run({ args }) {
|
|
10125
10246
|
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10126
10247
|
}
|
|
10127
10248
|
});
|
|
10128
|
-
var useCommand = defineCommand({
|
|
10129
|
-
meta: {
|
|
10130
|
-
name: "use",
|
|
10131
|
-
description: "Choose the organization <sandbox>.gdock.internal names resolve in on this machine."
|
|
10132
|
-
},
|
|
10133
|
-
args: {
|
|
10134
|
-
org: { type: "positional", description: "Organization id or slug.", required: false },
|
|
10135
|
-
clear: { type: "boolean", description: "Turn short names off.", default: false }
|
|
10136
|
-
},
|
|
10137
|
-
async run({ args }) {
|
|
10138
|
-
process.exitCode = await runNetworkUse({ org: args.org, clear: args.clear });
|
|
10139
|
-
}
|
|
10140
|
-
});
|
|
10141
10249
|
var devicesRemoveCommand = defineCommand({
|
|
10142
10250
|
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10143
10251
|
args: {
|
|
@@ -10166,7 +10274,6 @@ var networkCommand = defineCommand({
|
|
|
10166
10274
|
disconnect: disconnectCommand,
|
|
10167
10275
|
status: statusCommand,
|
|
10168
10276
|
ls: lsCommand,
|
|
10169
|
-
use: useCommand,
|
|
10170
10277
|
devices: devicesCommand
|
|
10171
10278
|
}
|
|
10172
10279
|
});
|
|
@@ -10195,26 +10302,30 @@ async function runOrganizationsUse(args, log = console.log, error = printError)
|
|
|
10195
10302
|
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
10196
10303
|
await writeSettings(settings);
|
|
10197
10304
|
log(eventLine("done", `Default organization: ${organization.name} (${organization.slug ?? organization.id}).`));
|
|
10198
|
-
log(` Stored in ${getSettingsPath()}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10305
|
+
log(` Stored in ${displayPath(getSettingsPath())}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10199
10306
|
return 0;
|
|
10200
10307
|
}
|
|
10201
10308
|
async function runOrganizationsCurrent(args, log = console.log) {
|
|
10202
10309
|
const found = await findOrganizationRef(args.org);
|
|
10203
10310
|
if (args.json) {
|
|
10204
|
-
log(JSON.stringify(found ? {
|
|
10311
|
+
log(JSON.stringify(found ? {
|
|
10312
|
+
organization: found.ref,
|
|
10313
|
+
source: found.source,
|
|
10314
|
+
...found.path && { path: found.path }
|
|
10315
|
+
} : null, null, 2));
|
|
10205
10316
|
return 0;
|
|
10206
10317
|
}
|
|
10207
10318
|
if (!found) {
|
|
10208
10319
|
log(eventLine("item", `No organization set. Pass --org, set GHOSTDOCK_ORG, or run \`${getCliName()} organizations use <org>\`.`));
|
|
10209
10320
|
return 0;
|
|
10210
10321
|
}
|
|
10211
|
-
log(eventLine("item", `${found.label} (${describeSource(found
|
|
10322
|
+
log(eventLine("item", `${found.label} (${describeSource(found)})`));
|
|
10212
10323
|
return 0;
|
|
10213
10324
|
}
|
|
10214
10325
|
var organizationsUseCommand = defineCommand({
|
|
10215
10326
|
meta: {
|
|
10216
10327
|
name: "use",
|
|
10217
|
-
description: "Set the default organization for commands run in
|
|
10328
|
+
description: "Set the default organization for commands run without --org (in every directory)."
|
|
10218
10329
|
},
|
|
10219
10330
|
args: {
|
|
10220
10331
|
org: {
|