ghostdock 0.0.10 → 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 +203 -96
- 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
|
},
|
|
@@ -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.11";
|
|
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,16 @@ 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 } = 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>`] : [],
|
|
9989
10073
|
...dns === "none" ? [
|
|
9990
10074
|
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
9991
10075
|
` \`${getCliName()} network ls\`.`
|
|
@@ -10007,44 +10091,89 @@ async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
|
10007
10091
|
printResult({ connected: false }, () => [eventLine("done", "Disconnected.")], deps.log);
|
|
10008
10092
|
return 0;
|
|
10009
10093
|
}
|
|
10010
|
-
async function
|
|
10094
|
+
async function readConnection(deps) {
|
|
10011
10095
|
const state = await readDeviceState(deps.dir);
|
|
10012
|
-
const
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
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 };
|
|
10021
10112
|
}
|
|
10022
|
-
|
|
10113
|
+
}
|
|
10114
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10115
|
+
const { state, up, device, asked, provider } = await readConnection(deps);
|
|
10116
|
+
printResult({ connected: up, device, provider }, () => {
|
|
10023
10117
|
if (!state) {
|
|
10024
10118
|
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10025
10119
|
}
|
|
10120
|
+
const through = provider ? ` through ${provider.name}` : "";
|
|
10026
10121
|
const lines = [
|
|
10027
|
-
eventLine("item", `${
|
|
10122
|
+
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
10028
10123
|
];
|
|
10029
|
-
if (
|
|
10030
|
-
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10031
|
-
} else {
|
|
10124
|
+
if (device) {
|
|
10032
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\`.`);
|
|
10033
10131
|
}
|
|
10132
|
+
if (provider)
|
|
10133
|
+
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
10034
10134
|
return lines;
|
|
10035
10135
|
}, deps.log);
|
|
10036
10136
|
return 0;
|
|
10037
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
|
+
}
|
|
10038
10146
|
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10039
10147
|
try {
|
|
10040
10148
|
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10041
10149
|
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
|
|
10042
|
-
|
|
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 }, () => {
|
|
10043
10156
|
if (listed.sandboxes.length === 0) {
|
|
10044
10157
|
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10045
10158
|
}
|
|
10046
10159
|
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10047
|
-
|
|
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;
|
|
10048
10177
|
}, deps.log);
|
|
10049
10178
|
return 0;
|
|
10050
10179
|
} catch (err) {
|
|
@@ -10052,28 +10181,6 @@ async function runNetworkLs(args, deps = defaultDeps()) {
|
|
|
10052
10181
|
return 1;
|
|
10053
10182
|
}
|
|
10054
10183
|
}
|
|
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
10184
|
async function runNetworkDevices(deps = defaultDeps()) {
|
|
10078
10185
|
try {
|
|
10079
10186
|
const { devices } = await deps.request("/api/v1/network/devices");
|
|
@@ -10100,11 +10207,18 @@ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
|
10100
10207
|
var connectCommand = defineCommand({
|
|
10101
10208
|
meta: {
|
|
10102
10209
|
name: "connect",
|
|
10103
|
-
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
|
|
10104
10219
|
},
|
|
10105
|
-
args: { org: orgArg },
|
|
10106
10220
|
async run({ args }) {
|
|
10107
|
-
process.exitCode = await runNetworkConnect({ org: args.org });
|
|
10221
|
+
process.exitCode = await runNetworkConnect({ provider: args.provider, org: args.org });
|
|
10108
10222
|
}
|
|
10109
10223
|
});
|
|
10110
10224
|
var disconnectCommand = defineCommand({
|
|
@@ -10114,7 +10228,10 @@ var disconnectCommand = defineCommand({
|
|
|
10114
10228
|
}
|
|
10115
10229
|
});
|
|
10116
10230
|
var statusCommand = defineCommand({
|
|
10117
|
-
meta: {
|
|
10231
|
+
meta: {
|
|
10232
|
+
name: "status",
|
|
10233
|
+
description: "Whether this machine is connected, through which provider, and as which device."
|
|
10234
|
+
},
|
|
10118
10235
|
async run() {
|
|
10119
10236
|
process.exitCode = await runNetworkStatus();
|
|
10120
10237
|
}
|
|
@@ -10122,26 +10239,13 @@ var statusCommand = defineCommand({
|
|
|
10122
10239
|
var lsCommand = defineCommand({
|
|
10123
10240
|
meta: {
|
|
10124
10241
|
name: "ls",
|
|
10125
|
-
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."
|
|
10126
10243
|
},
|
|
10127
10244
|
args: { org: orgArg },
|
|
10128
10245
|
async run({ args }) {
|
|
10129
10246
|
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10130
10247
|
}
|
|
10131
10248
|
});
|
|
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
10249
|
var devicesRemoveCommand = defineCommand({
|
|
10146
10250
|
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10147
10251
|
args: {
|
|
@@ -10170,7 +10274,6 @@ var networkCommand = defineCommand({
|
|
|
10170
10274
|
disconnect: disconnectCommand,
|
|
10171
10275
|
status: statusCommand,
|
|
10172
10276
|
ls: lsCommand,
|
|
10173
|
-
use: useCommand,
|
|
10174
10277
|
devices: devicesCommand
|
|
10175
10278
|
}
|
|
10176
10279
|
});
|
|
@@ -10199,26 +10302,30 @@ async function runOrganizationsUse(args, log = console.log, error = printError)
|
|
|
10199
10302
|
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
10200
10303
|
await writeSettings(settings);
|
|
10201
10304
|
log(eventLine("done", `Default organization: ${organization.name} (${organization.slug ?? organization.id}).`));
|
|
10202
|
-
log(` Stored in ${getSettingsPath()}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10305
|
+
log(` Stored in ${displayPath(getSettingsPath())}. --org and GHOSTDOCK_ORG still take precedence.`);
|
|
10203
10306
|
return 0;
|
|
10204
10307
|
}
|
|
10205
10308
|
async function runOrganizationsCurrent(args, log = console.log) {
|
|
10206
10309
|
const found = await findOrganizationRef(args.org);
|
|
10207
10310
|
if (args.json) {
|
|
10208
|
-
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));
|
|
10209
10316
|
return 0;
|
|
10210
10317
|
}
|
|
10211
10318
|
if (!found) {
|
|
10212
10319
|
log(eventLine("item", `No organization set. Pass --org, set GHOSTDOCK_ORG, or run \`${getCliName()} organizations use <org>\`.`));
|
|
10213
10320
|
return 0;
|
|
10214
10321
|
}
|
|
10215
|
-
log(eventLine("item", `${found.label} (${describeSource(found
|
|
10322
|
+
log(eventLine("item", `${found.label} (${describeSource(found)})`));
|
|
10216
10323
|
return 0;
|
|
10217
10324
|
}
|
|
10218
10325
|
var organizationsUseCommand = defineCommand({
|
|
10219
10326
|
meta: {
|
|
10220
10327
|
name: "use",
|
|
10221
|
-
description: "Set the default organization for commands run in
|
|
10328
|
+
description: "Set the default organization for commands run without --org (in every directory)."
|
|
10222
10329
|
},
|
|
10223
10330
|
args: {
|
|
10224
10331
|
org: {
|