ghostdock 0.0.10 → 0.0.12
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 +107 -33
- package/dist/index.js +226 -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. The organization (--org, else the default) becomes this machine's short-name organization. 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,99 @@ 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
|
|
500
|
+
open https://quiet-harbor--acme.ghostdock.net:8080/ # in a browser: HTTPS, any port
|
|
501
|
+
open https://quiet-harbor.ghostdock.net:8080/ # the short name, for the connect organization
|
|
502
|
+
ghostdock network connect --org beta # short names now mean beta's sandboxes
|
|
493
503
|
ghostdock network status
|
|
494
504
|
ghostdock network disconnect
|
|
495
505
|
```
|
|
496
506
|
|
|
507
|
+
- **One provider at a time.** Each sandbox provider is its own gateway, and a device reaches
|
|
508
|
+
only the sandboxes on the provider it connected through. Without an argument, `connect` takes
|
|
509
|
+
the organization (`--org`, `GHOSTDOCK_ORG`, the default from `organizations use`) and its
|
|
510
|
+
default sandbox provider, else the platform default. Connecting through another provider
|
|
511
|
+
replaces the connection: same interface and address, new gateway. `network status` says which
|
|
512
|
+
provider you are connected through; `network ls` marks sandboxes elsewhere as
|
|
513
|
+
`not reachable: on <provider>` with the `connect` command that reaches them. A provider
|
|
514
|
+
without a private network (Incus, for now) is refused by the server; one your organization
|
|
515
|
+
may not use answers 403.
|
|
497
516
|
- **Needs `wg-quick`** (`brew install wireguard-tools`, `apt install wireguard-tools`). macOS
|
|
498
517
|
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
|
|
518
|
+
- **One key per machine**, kept in `~/.config/ghostdock/network/`.
|
|
500
519
|
Only the public key is sent. The device belongs to the login it was connected with:
|
|
501
520
|
`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
|
-
|
|
521
|
+
- **Names**: `<sandbox>.<org>.ghostdock.internal`, for every organization you are a member
|
|
522
|
+
of. A name always says its organization, so a browser origin never changes hands between
|
|
523
|
+
organizations. Only AAAA records exist: the network is IPv6, and the sandbox sees IPv4.
|
|
524
|
+
- **In a browser**, use the HTTPS name `https://<sandbox>--<org>.ghostdock.net:<port>`
|
|
525
|
+
(`network ls` shows it): any port, a real certificate, so the page is a secure context
|
|
526
|
+
(clipboard, `Secure` cookies, service workers). The host's `gdo-ingress` terminates TLS and
|
|
527
|
+
forwards to the sandbox's port; it serves only devices of the sandbox's organization, and
|
|
528
|
+
refuses background requests (fetch, forms, iframes, websockets) from pages outside that
|
|
529
|
+
organization; pages on another HTTPS name of the same organization, any port, may call it. The
|
|
530
|
+
sandbox sees the request from `198.18.0.0`, and your device's address in
|
|
531
|
+
`X-Forwarded-For`. The desktop image's Desktop opens only this way, never on the raw
|
|
532
|
+
address.
|
|
533
|
+
- **Short names** in a browser: `https://<sandbox>.ghostdock.net:<port>` means the sandbox of
|
|
534
|
+
this machine's organization: the one `network connect` resolved (`--org`, `GHOSTDOCK_ORG`,
|
|
535
|
+
the default from `organizations use`). `connect` says which; `network status` too, and
|
|
536
|
+
`network ls` shows a sandbox's short name next to its full one when it is that
|
|
537
|
+
organization's. With one organization you need nothing else. To use another organization's
|
|
538
|
+
short names, connect again with `--org <other>`: there is no other way to change it, so a
|
|
539
|
+
short name never changes meaning behind your back (`cd` into another project does not
|
|
540
|
+
change it). Full names (`<sandbox>--<org>`) keep working for every organization and never
|
|
541
|
+
change meaning: use them in scripts, CI, `.env` files and links you share.
|
|
542
|
+
- **When the organization changes,** each short-name site clears its browser data once
|
|
543
|
+
(local storage, cache, cookies) before the other organization's sandbox sees a request, so
|
|
544
|
+
it never reads the first one's data. Cookies are cleared for the whole site, not only the
|
|
545
|
+
short name: every site under the net domain's registrable domain (`ghostdock.net`: every
|
|
546
|
+
sandbox app there, full names included; a net domain under the app's domain, as on staging:
|
|
547
|
+
the Ghostdock app too). `connect` names the site when the organization changes. A tab opened on a short name before the
|
|
548
|
+
change keeps the old page until you reload it: reload or close those tabs (`connect` says
|
|
549
|
+
so). The page *Switching organization* is that clearing and reloads by itself; *Clear this
|
|
550
|
+
site's data* means your browser could not clear it (Safari before 17): clear the site's
|
|
551
|
+
data in its settings, or use the full name.
|
|
552
|
+
- Tools may use short names too (`curl https://quiet-harbor.ghostdock.net:8080/`), but a
|
|
553
|
+
short name in a script means whatever this machine is connected with: prefer the full
|
|
554
|
+
name.
|
|
555
|
+
- **Browsers use the HTTPS name; tools keep the raw address.** A browser that opens
|
|
556
|
+
`http://quiet-harbor.acme.ghostdock.internal:3000/app` (or the raw IPv6 address) gets a
|
|
557
|
+
`308` to `https://quiet-harbor--acme.ghostdock.net:3000/app`. Any other browser request on
|
|
558
|
+
the raw address (`fetch`, images, scripts, forms, websockets) gets `403` with
|
|
559
|
+
`X-Ghostdock-Blocked: browser-https-only` and the HTTPS address in the body. This keeps
|
|
560
|
+
websites you visit while connected from using your connection against your sandboxes,
|
|
561
|
+
including through DNS rebinding.
|
|
562
|
+
- **What counts as a browser:** a `User-Agent` that starts with `Mozilla/`, which every
|
|
563
|
+
browser sends on its own requests. Browsers send no `Sec-Fetch-*` headers over plain
|
|
564
|
+
`http://` to an address like this, so those cannot be the signal. (Firefox lets a page
|
|
565
|
+
set its own `User-Agent` on a `fetch`; the `Host` check below stops that one.) Tools that send a
|
|
566
|
+
browser's `User-Agent` (Playwright, Puppeteer, their request APIs) count as browsers too:
|
|
567
|
+
point them at the HTTPS name. A frontend outside the sandbox (a dev server on your laptop)
|
|
568
|
+
calls the sandbox's API by its HTTPS name as well.
|
|
569
|
+
- **Tools** (`curl`, SDKs, test runners, Prometheus) pass untouched, and every other
|
|
570
|
+
protocol does too, as long as they address the sandbox by its IP address, its
|
|
571
|
+
`.internal` name (or a container's, `web.quiet-harbor.acme.ghostdock.internal`), or
|
|
572
|
+
`localhost`: any other `Host` gets `421` (`X-Ghostdock-Blocked: host-not-allowed`). That
|
|
573
|
+
check is what stops DNS rebinding for a page that sets its own `User-Agent`. A name of
|
|
574
|
+
your own in `/etc/hosts` gets 421 too.
|
|
575
|
+
- **One difference for tools:** a port where nothing listens accepts the connection and
|
|
576
|
+
then resets it, so port checks such as `nc -z` report it open; check with a real request
|
|
577
|
+
instead.
|
|
578
|
+
- **In the sandbox,** firewall rules for these clients belong in the input path
|
|
579
|
+
(`iptables -A INPUT`, an nft `hook input` chain): `FORWARD` and `DOCKER-USER` rules no
|
|
580
|
+
longer see them, because the guard connects to a published container port from inside
|
|
581
|
+
the VM.
|
|
506
582
|
- `ghostdock network devices` lists your devices, `ghostdock network devices remove <id>`
|
|
507
583
|
removes one.
|
|
508
584
|
|
|
@@ -544,7 +620,7 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
544
620
|
|
|
545
621
|
| Command | What it does |
|
|
546
622
|
| --- | --- |
|
|
547
|
-
| `ghostdock login [flags]` | Sign in and store a device access token for this
|
|
623
|
+
| `ghostdock login [flags]` | Sign in and store a device access token for this machine… |
|
|
548
624
|
|
|
549
625
|
#### logout
|
|
550
626
|
|
|
@@ -556,13 +632,12 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
556
632
|
|
|
557
633
|
| Command | What it does |
|
|
558
634
|
| --- | --- |
|
|
559
|
-
| `ghostdock network connect [flags]` | Put this machine into the private network: WireGuard and
|
|
635
|
+
| `ghostdock network connect [provider] [flags]` | Put this machine into the private network through one provider: WireGuard and… |
|
|
560
636
|
| `ghostdock network devices <command>` | Your devices in the private network. |
|
|
561
637
|
| `ghostdock network devices remove <id>` | Remove one of your devices from the private network. |
|
|
562
638
|
| `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… |
|
|
639
|
+
| `ghostdock network ls [flags]` | Sandboxes of an organization with their private addresses and names, and which… |
|
|
640
|
+
| `ghostdock network status` | Whether this machine is connected, through which provider, and as which device. |
|
|
566
641
|
|
|
567
642
|
#### organizations
|
|
568
643
|
|
|
@@ -570,7 +645,7 @@ or read [`cli-reference.md`](cli-reference.md) for every command's full descript
|
|
|
570
645
|
| --- | --- |
|
|
571
646
|
| `ghostdock organizations current [flags]` | Show which organization commands use, and where it comes from. |
|
|
572
647
|
| `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
|
|
648
|
+
| `ghostdock organizations use [org] [flags]` | Set the default organization for commands run without --org (in every… |
|
|
574
649
|
|
|
575
650
|
#### projects
|
|
576
651
|
|
|
@@ -694,7 +769,8 @@ and secrets, see [`ghostdock env`](#ghostdock-env).
|
|
|
694
769
|
| ------------------------------- | ------------------------------------------------------------- | --------------------------------- |
|
|
695
770
|
| `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
771
|
| `GHOSTDOCK_ORG` | Organization (id or slug) for commands run without `--org`. Takes precedence over the default from `organizations use`. | unset |
|
|
697
|
-
| `
|
|
772
|
+
| `XDG_CONFIG_HOME` | Base directory for the CLI's per-machine state (`ghostdock/credentials.json`, `ghostdock/settings.json`, `ghostdock/network/`). | `~/.config` (native Windows: `%APPDATA%`) |
|
|
773
|
+
| `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
774
|
|
|
699
775
|
Example for local development against a Nuxt dev server:
|
|
700
776
|
|
|
@@ -706,8 +782,10 @@ GHOSTDOCK_BASE_URL=http://localhost:3000 ghostdock login --wait
|
|
|
706
782
|
|
|
707
783
|
## Credentials file
|
|
708
784
|
|
|
709
|
-
Credentials are stored as JSON
|
|
710
|
-
|
|
785
|
+
Credentials are stored per machine as JSON in `~/.config/ghostdock/credentials.json`
|
|
786
|
+
(`$XDG_CONFIG_HOME/ghostdock/` when that is set, `%APPDATA%\ghostdock\` on native Windows; WSL
|
|
787
|
+
counts as Linux), with file permissions restricted to the owner (`0600`). `GHOSTDOCK_CREDENTIALS_PATH`
|
|
788
|
+
points the CLI at another file, e.g. for a second account. The file looks like this:
|
|
711
789
|
|
|
712
790
|
```json
|
|
713
791
|
{
|
|
@@ -717,10 +795,6 @@ CLI is run from), with file permissions restricted to the owner (`0600`):
|
|
|
717
795
|
}
|
|
718
796
|
```
|
|
719
797
|
|
|
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
798
|
Access tokens are short-lived (1 hour); the CLI refreshes them automatically using the refresh token
|
|
725
799
|
(30-day rolling expiry) whenever a request gets a `401`. If both tokens are invalid or revoked (e.g.
|
|
726
800
|
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
|
},
|
|
@@ -8564,7 +8588,7 @@ function getCliName(argv = process.argv) {
|
|
|
8564
8588
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
8565
8589
|
}
|
|
8566
8590
|
// package.json
|
|
8567
|
-
var version = "0.0.
|
|
8591
|
+
var version = "0.0.12";
|
|
8568
8592
|
var package_default = {
|
|
8569
8593
|
name: "ghostdock",
|
|
8570
8594
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -8601,6 +8625,7 @@ var package_default = {
|
|
|
8601
8625
|
};
|
|
8602
8626
|
|
|
8603
8627
|
// src/config.ts
|
|
8628
|
+
import { homedir } from "node:os";
|
|
8604
8629
|
import { join } from "node:path";
|
|
8605
8630
|
var FALLBACK_DEFAULT_BASE_URL = "http://localhost:3000";
|
|
8606
8631
|
var cliBaseUrlOverride;
|
|
@@ -8622,9 +8647,22 @@ function getBaseUrl() {
|
|
|
8622
8647
|
return fromEnv.replace(/\/+$/, "");
|
|
8623
8648
|
return resolveDefaultBaseUrl();
|
|
8624
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
|
+
}
|
|
8625
8659
|
function getCredentialsPath() {
|
|
8626
8660
|
const override = process.env.GHOSTDOCK_CREDENTIALS_PATH?.trim();
|
|
8627
|
-
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;
|
|
8628
8666
|
}
|
|
8629
8667
|
|
|
8630
8668
|
// src/credentials.ts
|
|
@@ -8886,7 +8924,8 @@ async function findOrganizationRef(flag) {
|
|
|
8886
8924
|
return {
|
|
8887
8925
|
ref: defaultOrganization.id,
|
|
8888
8926
|
source: "default",
|
|
8889
|
-
label: defaultOrganization.slug ?? defaultOrganization.id
|
|
8927
|
+
label: defaultOrganization.slug ?? defaultOrganization.id,
|
|
8928
|
+
path: getSettingsPath()
|
|
8890
8929
|
};
|
|
8891
8930
|
}
|
|
8892
8931
|
return null;
|
|
@@ -8897,15 +8936,15 @@ async function requireOrganizationRef(flag, notice = printNotice) {
|
|
|
8897
8936
|
throw new Error(`No organization. Pass --org <id-or-slug>, set GHOSTDOCK_ORG, or run \`${getCliName()} organizations use <org>\`.`);
|
|
8898
8937
|
}
|
|
8899
8938
|
if (found.source !== "flag")
|
|
8900
|
-
notice(`Using organization ${found.label} (${describeSource(found
|
|
8939
|
+
notice(`Using organization ${found.label} (${describeSource(found)}).`);
|
|
8901
8940
|
return found.ref;
|
|
8902
8941
|
}
|
|
8903
|
-
function describeSource(source) {
|
|
8942
|
+
function describeSource({ source, path }) {
|
|
8904
8943
|
if (source === "flag")
|
|
8905
8944
|
return "--org";
|
|
8906
8945
|
if (source === "env")
|
|
8907
8946
|
return "GHOSTDOCK_ORG";
|
|
8908
|
-
return `default
|
|
8947
|
+
return `default from ${displayPath(path ?? getSettingsPath())}`;
|
|
8909
8948
|
}
|
|
8910
8949
|
async function withOrganization(flag, run) {
|
|
8911
8950
|
let organization;
|
|
@@ -9750,7 +9789,7 @@ async function runLogin(args, log = console.log, choose = defaultChooser(), erro
|
|
|
9750
9789
|
var loginCommand = defineCommand({
|
|
9751
9790
|
meta: {
|
|
9752
9791
|
name: "login",
|
|
9753
|
-
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)."
|
|
9754
9793
|
},
|
|
9755
9794
|
args: {
|
|
9756
9795
|
wait: {
|
|
@@ -9809,15 +9848,10 @@ import { hostname as hostname2 } from "node:os";
|
|
|
9809
9848
|
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
9810
9849
|
import { existsSync } from "node:fs";
|
|
9811
9850
|
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
9812
|
-
import { homedir } from "node:os";
|
|
9813
9851
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9814
9852
|
var INTERFACE = "gdock";
|
|
9815
9853
|
function networkDir() {
|
|
9816
|
-
|
|
9817
|
-
if (override)
|
|
9818
|
-
return override;
|
|
9819
|
-
const base = process.env.XDG_CONFIG_HOME?.trim() || join3(homedir(), ".config");
|
|
9820
|
-
return join3(base, "ghostdock", "network");
|
|
9854
|
+
return join3(getConfigDir(), "network");
|
|
9821
9855
|
}
|
|
9822
9856
|
function configPath(dir = networkDir()) {
|
|
9823
9857
|
return join3(dir, `${INTERFACE}.conf`);
|
|
@@ -9828,6 +9862,10 @@ function keyPath(dir) {
|
|
|
9828
9862
|
function statePath(dir) {
|
|
9829
9863
|
return join3(dir, "device.json");
|
|
9830
9864
|
}
|
|
9865
|
+
function isProvider(value) {
|
|
9866
|
+
const provider = value;
|
|
9867
|
+
return typeof provider?.id === "string" && typeof provider.name === "string";
|
|
9868
|
+
}
|
|
9831
9869
|
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
9832
9870
|
function keyPairFromPrivate(privateKey) {
|
|
9833
9871
|
const raw = Buffer.from(privateKey, "base64");
|
|
@@ -9863,7 +9901,8 @@ async function readDeviceState(dir = networkDir()) {
|
|
|
9863
9901
|
return {
|
|
9864
9902
|
baseUrl: parsed.baseUrl,
|
|
9865
9903
|
deviceId: parsed.deviceId,
|
|
9866
|
-
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
|
|
9867
9906
|
};
|
|
9868
9907
|
}
|
|
9869
9908
|
} catch {}
|
|
@@ -9883,7 +9922,14 @@ function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
|
9883
9922
|
}
|
|
9884
9923
|
function renderConfig(input) {
|
|
9885
9924
|
const { device, network, gateway } = input.registered;
|
|
9886
|
-
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
|
+
}
|
|
9887
9933
|
const lines = [
|
|
9888
9934
|
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9889
9935
|
"[Interface]",
|
|
@@ -9892,15 +9938,26 @@ function renderConfig(input) {
|
|
|
9892
9938
|
`MTU = ${network.mtu}`
|
|
9893
9939
|
];
|
|
9894
9940
|
if (input.dns === "macos-resolver") {
|
|
9895
|
-
const
|
|
9896
|
-
|
|
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
|
+
}
|
|
9897
9945
|
} else if (input.dns === "resolved") {
|
|
9898
|
-
|
|
9946
|
+
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
9947
|
+
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
9899
9948
|
}
|
|
9900
9949
|
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.platformPrefix}`, "PersistentKeepalive = 25", "");
|
|
9901
9950
|
return lines.join(`
|
|
9902
9951
|
`);
|
|
9903
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
|
+
}
|
|
9904
9961
|
async function writeConfig(contents, path = configPath()) {
|
|
9905
9962
|
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9906
9963
|
await writeFile3(path, contents, { mode: 384 });
|
|
@@ -9939,28 +9996,36 @@ function defaultDeps() {
|
|
|
9939
9996
|
function message(err) {
|
|
9940
9997
|
return err instanceof Error ? err.message : String(err);
|
|
9941
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
|
+
}
|
|
9942
10004
|
async function wgQuick(deps, verb) {
|
|
9943
10005
|
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
9944
10006
|
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
9945
10007
|
return deps.run("sudo", args);
|
|
9946
10008
|
}
|
|
9947
|
-
async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
10009
|
+
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
9948
10010
|
if (!deps.has("wg-quick")) {
|
|
9949
10011
|
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
9950
10012
|
return 1;
|
|
9951
10013
|
}
|
|
9952
10014
|
let registered;
|
|
9953
10015
|
let privateKey;
|
|
10016
|
+
const previous = await readDeviceState(deps.dir);
|
|
9954
10017
|
try {
|
|
10018
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10019
|
+
const provider = args.provider?.trim();
|
|
9955
10020
|
const key = await loadOrCreateKey(deps.dir);
|
|
9956
10021
|
privateKey = key.privateKey;
|
|
9957
|
-
const found = await findOrganizationRef(args.org);
|
|
9958
10022
|
registered = await deps.request("/api/v1/network/devices", {
|
|
9959
10023
|
method: "POST",
|
|
9960
10024
|
body: JSON.stringify({
|
|
9961
10025
|
publicKey: key.publicKey,
|
|
9962
10026
|
name: hostname2().slice(0, 64) || "device",
|
|
9963
|
-
|
|
10027
|
+
organization,
|
|
10028
|
+
...provider ? { provider } : {}
|
|
9964
10029
|
})
|
|
9965
10030
|
});
|
|
9966
10031
|
} catch (err) {
|
|
@@ -9968,9 +10033,22 @@ async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
|
9968
10033
|
return 1;
|
|
9969
10034
|
}
|
|
9970
10035
|
const dns = detectDnsMode();
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
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) {
|
|
9974
10052
|
const down = await wgQuick(deps, "down");
|
|
9975
10053
|
if (down !== 0) {
|
|
9976
10054
|
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
@@ -9982,10 +10060,22 @@ async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
|
9982
10060
|
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
9983
10061
|
return 1;
|
|
9984
10062
|
}
|
|
9985
|
-
const { device, network } = registered;
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
10063
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10064
|
+
const pinned = device.shortNameOrganization;
|
|
10065
|
+
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10066
|
+
printResult({ device, network, provider, shortNameOrganizationChanged, interface: INTERFACE, dns }, () => [
|
|
10067
|
+
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10068
|
+
...switched ? [
|
|
10069
|
+
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
10070
|
+
] : [],
|
|
10071
|
+
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
10072
|
+
` Names: <sandbox>.<org>.${network.zone}`,
|
|
10073
|
+
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
10074
|
+
...network.netZone && pinned ? [
|
|
10075
|
+
` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
|
|
10076
|
+
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
10077
|
+
] : [],
|
|
10078
|
+
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
9989
10079
|
...dns === "none" ? [
|
|
9990
10080
|
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
9991
10081
|
` \`${getCliName()} network ls\`.`
|
|
@@ -9994,6 +10084,15 @@ async function runNetworkConnect(args, deps = defaultDeps()) {
|
|
|
9994
10084
|
], deps.log);
|
|
9995
10085
|
return 0;
|
|
9996
10086
|
}
|
|
10087
|
+
function clearingNotice(cookieSite) {
|
|
10088
|
+
const site = cookieSite?.site ?? "the net domain";
|
|
10089
|
+
return [
|
|
10090
|
+
" Short names meant another organization's sandboxes before. The first short-name site each",
|
|
10091
|
+
` browser opens now clears its data once: the site's storage, and every cookie of ${site}.`,
|
|
10092
|
+
` That signs the browser out of every site under ${site}${cookieSite?.includesApp ? ", the Ghostdock app included" : ""}.`,
|
|
10093
|
+
" Reload or close browser tabs opened on short names before this connect."
|
|
10094
|
+
];
|
|
10095
|
+
}
|
|
9997
10096
|
async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
9998
10097
|
if (!deps.up()) {
|
|
9999
10098
|
printResult({ connected: false }, () => [eventLine("item", "Not connected.")], deps.log);
|
|
@@ -10007,44 +10106,96 @@ async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
|
10007
10106
|
printResult({ connected: false }, () => [eventLine("done", "Disconnected.")], deps.log);
|
|
10008
10107
|
return 0;
|
|
10009
10108
|
}
|
|
10010
|
-
async function
|
|
10109
|
+
async function readConnection(deps) {
|
|
10011
10110
|
const state = await readDeviceState(deps.dir);
|
|
10012
|
-
const
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10111
|
+
const up = deps.up();
|
|
10112
|
+
if (!state)
|
|
10113
|
+
return { state, up, device: null, asked: true, provider: null };
|
|
10114
|
+
try {
|
|
10115
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10116
|
+
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10117
|
+
return {
|
|
10118
|
+
state,
|
|
10119
|
+
up,
|
|
10120
|
+
device,
|
|
10121
|
+
asked: true,
|
|
10122
|
+
provider: up ? device?.gatewayProvider ?? null : null
|
|
10123
|
+
};
|
|
10124
|
+
} catch (err) {
|
|
10125
|
+
deps.notice(`Could not ask the app about this device: ${message(err)}`);
|
|
10126
|
+
return { state, up, device: null, asked: false, provider: up ? state.gatewayProvider : null };
|
|
10021
10127
|
}
|
|
10022
|
-
|
|
10128
|
+
}
|
|
10129
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10130
|
+
const { state, up, device, asked, provider } = await readConnection(deps);
|
|
10131
|
+
printResult({ connected: up, device, provider }, () => {
|
|
10023
10132
|
if (!state) {
|
|
10024
10133
|
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10025
10134
|
}
|
|
10135
|
+
const through = provider ? ` through ${provider.name}` : "";
|
|
10026
10136
|
const lines = [
|
|
10027
|
-
eventLine("item", `${
|
|
10137
|
+
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
10028
10138
|
];
|
|
10029
|
-
if (
|
|
10030
|
-
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10031
|
-
} else {
|
|
10139
|
+
if (device) {
|
|
10032
10140
|
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
10141
|
+
if (device.shortNameOrganization) {
|
|
10142
|
+
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
10143
|
+
}
|
|
10144
|
+
if (!device.gatewayProvider) {
|
|
10145
|
+
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
10146
|
+
}
|
|
10147
|
+
} else if (asked) {
|
|
10148
|
+
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10033
10149
|
}
|
|
10150
|
+
if (provider)
|
|
10151
|
+
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
10034
10152
|
return lines;
|
|
10035
10153
|
}, deps.log);
|
|
10036
10154
|
return 0;
|
|
10037
10155
|
}
|
|
10156
|
+
function unreachableNote(sandbox, connected) {
|
|
10157
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10158
|
+
return `not reachable: ${sandbox.provider.name} has no private network yet`;
|
|
10159
|
+
}
|
|
10160
|
+
if (connected?.id === sandbox.provider.id)
|
|
10161
|
+
return null;
|
|
10162
|
+
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
10163
|
+
}
|
|
10038
10164
|
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10039
10165
|
try {
|
|
10040
10166
|
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10041
|
-
const
|
|
10042
|
-
|
|
10167
|
+
const connection = await readConnection(deps);
|
|
10168
|
+
const deviceId = connection.device?.id;
|
|
10169
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes${deviceId ? `?device=${encodeURIComponent(deviceId)}` : ""}`);
|
|
10170
|
+
const connected = connection.provider;
|
|
10171
|
+
const sandboxes = listed.sandboxes.map((sandbox) => ({
|
|
10172
|
+
...sandbox,
|
|
10173
|
+
reachable: unreachableNote(sandbox, connected) === null
|
|
10174
|
+
}));
|
|
10175
|
+
printResult({ ...listed, connectedProvider: connected, sandboxes }, () => {
|
|
10043
10176
|
if (listed.sandboxes.length === 0) {
|
|
10044
10177
|
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10045
10178
|
}
|
|
10046
10179
|
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10047
|
-
|
|
10180
|
+
const canonicalWidth = Math.max(...listed.sandboxes.map((s) => s.canonicalName.length));
|
|
10181
|
+
const httpsWidth = Math.max(...listed.sandboxes.map((s) => (s.httpsName?.length ?? 0) + 8));
|
|
10182
|
+
const shortWidth = Math.max(...listed.sandboxes.map((s) => (s.shortName?.length ?? 0) + 8));
|
|
10183
|
+
const lines = [
|
|
10184
|
+
connected ? eventLine("item", `Connected through ${connected.name}.`) : eventLine("item", `Not connected: nothing is reachable. Run \`${getCliName()} network connect [provider]\`.`)
|
|
10185
|
+
];
|
|
10186
|
+
for (const s of listed.sandboxes) {
|
|
10187
|
+
const note = unreachableNote(s, connected);
|
|
10188
|
+
lines.push([
|
|
10189
|
+
` ${s.name.padEnd(width)}`,
|
|
10190
|
+
s.address.padEnd(28),
|
|
10191
|
+
s.canonicalName.padEnd(canonicalWidth),
|
|
10192
|
+
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
10193
|
+
...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
|
|
10194
|
+
s.status,
|
|
10195
|
+
...note ? [`(${note})`] : []
|
|
10196
|
+
].join(" "));
|
|
10197
|
+
}
|
|
10198
|
+
return lines;
|
|
10048
10199
|
}, deps.log);
|
|
10049
10200
|
return 0;
|
|
10050
10201
|
} catch (err) {
|
|
@@ -10052,28 +10203,6 @@ async function runNetworkLs(args, deps = defaultDeps()) {
|
|
|
10052
10203
|
return 1;
|
|
10053
10204
|
}
|
|
10054
10205
|
}
|
|
10055
|
-
async function runNetworkUse(args, deps = defaultDeps()) {
|
|
10056
|
-
const state = await readDeviceState(deps.dir);
|
|
10057
|
-
if (!state) {
|
|
10058
|
-
deps.error(`Not set up. Run \`${getCliName()} network connect\` first.`);
|
|
10059
|
-
return 1;
|
|
10060
|
-
}
|
|
10061
|
-
const organization = args.clear ? null : args.org?.trim();
|
|
10062
|
-
if (organization === undefined || organization === "") {
|
|
10063
|
-
deps.error("Pass an organization id or slug, or --clear.");
|
|
10064
|
-
return 1;
|
|
10065
|
-
}
|
|
10066
|
-
try {
|
|
10067
|
-
const device = await deps.request(`/api/v1/network/devices/${encodeURIComponent(state.deviceId)}/short-name-organization`, { method: "PUT", body: JSON.stringify({ organization }) });
|
|
10068
|
-
printResult(device, () => [
|
|
10069
|
-
eventLine("done", organization === null ? "Short names are off for this machine." : `Short names (<sandbox>.gdock.internal) now resolve in ${organization}.`)
|
|
10070
|
-
], deps.log);
|
|
10071
|
-
return 0;
|
|
10072
|
-
} catch (err) {
|
|
10073
|
-
deps.error(message(err));
|
|
10074
|
-
return 1;
|
|
10075
|
-
}
|
|
10076
|
-
}
|
|
10077
10206
|
async function runNetworkDevices(deps = defaultDeps()) {
|
|
10078
10207
|
try {
|
|
10079
10208
|
const { devices } = await deps.request("/api/v1/network/devices");
|
|
@@ -10100,11 +10229,18 @@ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
|
10100
10229
|
var connectCommand = defineCommand({
|
|
10101
10230
|
meta: {
|
|
10102
10231
|
name: "connect",
|
|
10103
|
-
description: "Put this machine into the private network: WireGuard and split DNS. Asks for sudo once."
|
|
10232
|
+
description: "Put this machine into the private network through one provider: WireGuard and split DNS. The organization (--org, else the default) becomes this machine's short-name organization. Asks for sudo once."
|
|
10233
|
+
},
|
|
10234
|
+
args: {
|
|
10235
|
+
provider: {
|
|
10236
|
+
type: "positional",
|
|
10237
|
+
required: false,
|
|
10238
|
+
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."
|
|
10239
|
+
},
|
|
10240
|
+
org: orgArg
|
|
10104
10241
|
},
|
|
10105
|
-
args: { org: orgArg },
|
|
10106
10242
|
async run({ args }) {
|
|
10107
|
-
process.exitCode = await runNetworkConnect({ org: args.org });
|
|
10243
|
+
process.exitCode = await runNetworkConnect({ provider: args.provider, org: args.org });
|
|
10108
10244
|
}
|
|
10109
10245
|
});
|
|
10110
10246
|
var disconnectCommand = defineCommand({
|
|
@@ -10114,7 +10250,10 @@ var disconnectCommand = defineCommand({
|
|
|
10114
10250
|
}
|
|
10115
10251
|
});
|
|
10116
10252
|
var statusCommand = defineCommand({
|
|
10117
|
-
meta: {
|
|
10253
|
+
meta: {
|
|
10254
|
+
name: "status",
|
|
10255
|
+
description: "Whether this machine is connected, through which provider, and as which device."
|
|
10256
|
+
},
|
|
10118
10257
|
async run() {
|
|
10119
10258
|
process.exitCode = await runNetworkStatus();
|
|
10120
10259
|
}
|
|
@@ -10122,26 +10261,13 @@ var statusCommand = defineCommand({
|
|
|
10122
10261
|
var lsCommand = defineCommand({
|
|
10123
10262
|
meta: {
|
|
10124
10263
|
name: "ls",
|
|
10125
|
-
description: "Sandboxes of an organization with their private addresses and names."
|
|
10264
|
+
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
10126
10265
|
},
|
|
10127
10266
|
args: { org: orgArg },
|
|
10128
10267
|
async run({ args }) {
|
|
10129
10268
|
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10130
10269
|
}
|
|
10131
10270
|
});
|
|
10132
|
-
var useCommand = defineCommand({
|
|
10133
|
-
meta: {
|
|
10134
|
-
name: "use",
|
|
10135
|
-
description: "Choose the organization <sandbox>.gdock.internal names resolve in on this machine."
|
|
10136
|
-
},
|
|
10137
|
-
args: {
|
|
10138
|
-
org: { type: "positional", description: "Organization id or slug.", required: false },
|
|
10139
|
-
clear: { type: "boolean", description: "Turn short names off.", default: false }
|
|
10140
|
-
},
|
|
10141
|
-
async run({ args }) {
|
|
10142
|
-
process.exitCode = await runNetworkUse({ org: args.org, clear: args.clear });
|
|
10143
|
-
}
|
|
10144
|
-
});
|
|
10145
10271
|
var devicesRemoveCommand = defineCommand({
|
|
10146
10272
|
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10147
10273
|
args: {
|
|
@@ -10170,7 +10296,6 @@ var networkCommand = defineCommand({
|
|
|
10170
10296
|
disconnect: disconnectCommand,
|
|
10171
10297
|
status: statusCommand,
|
|
10172
10298
|
ls: lsCommand,
|
|
10173
|
-
use: useCommand,
|
|
10174
10299
|
devices: devicesCommand
|
|
10175
10300
|
}
|
|
10176
10301
|
});
|
|
@@ -10199,26 +10324,30 @@ async function runOrganizationsUse(args, log = console.log, error = printError)
|
|
|
10199
10324
|
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
10200
10325
|
await writeSettings(settings);
|
|
10201
10326
|
log(eventLine("done", `Default organization: ${organization.name} (${organization.slug ?? organization.id}).`));
|
|
10202
|
-
log(` Stored in ${getSettingsPath()}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10327
|
+
log(` Stored in ${displayPath(getSettingsPath())}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10203
10328
|
return 0;
|
|
10204
10329
|
}
|
|
10205
10330
|
async function runOrganizationsCurrent(args, log = console.log) {
|
|
10206
10331
|
const found = await findOrganizationRef(args.org);
|
|
10207
10332
|
if (args.json) {
|
|
10208
|
-
log(JSON.stringify(found ? {
|
|
10333
|
+
log(JSON.stringify(found ? {
|
|
10334
|
+
organization: found.ref,
|
|
10335
|
+
source: found.source,
|
|
10336
|
+
...found.path && { path: found.path }
|
|
10337
|
+
} : null, null, 2));
|
|
10209
10338
|
return 0;
|
|
10210
10339
|
}
|
|
10211
10340
|
if (!found) {
|
|
10212
10341
|
log(eventLine("item", `No organization set. Pass --org, set GHOSTDOCK_ORG, or run \`${getCliName()} organizations use <org>\`.`));
|
|
10213
10342
|
return 0;
|
|
10214
10343
|
}
|
|
10215
|
-
log(eventLine("item", `${found.label} (${describeSource(found
|
|
10344
|
+
log(eventLine("item", `${found.label} (${describeSource(found)})`));
|
|
10216
10345
|
return 0;
|
|
10217
10346
|
}
|
|
10218
10347
|
var organizationsUseCommand = defineCommand({
|
|
10219
10348
|
meta: {
|
|
10220
10349
|
name: "use",
|
|
10221
|
-
description: "Set the default organization for commands run in
|
|
10350
|
+
description: "Set the default organization for commands run without --org (in every directory)."
|
|
10222
10351
|
},
|
|
10223
10352
|
args: {
|
|
10224
10353
|
org: {
|