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.
@@ -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 CLI.
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 this directory.
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 CLI.
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 for this directory (login sets one when it can)
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
- (`./.ghostdock/settings.json`), so it belongs to that directory's login. **Scripts and CI should
144
- pass `--org` or set `GHOSTDOCK_ORG`** rather than depend on (or change) a stored default, and use
145
- the organization id: renaming an organization changes its slug, not its id.
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 local credentials file.
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 organization's sandboxes from this machine by private address and name, over
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.gdock.internal/`, as it would on `localhost` inside the sandbox.
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 --org acme # asks for sudo once: wg-quick up and split DNS
489
- ghostdock network ls --org acme # sandboxes with their addresses and names
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
- curl http://quiet-harbor.gdock.internal:8080/ # short name: the organization connect used
492
- ghostdock network use beta # short names in another organization, no sudo
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/` (or `GHOSTDOCK_NETWORK_DIR`).
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` always, and `<sandbox>.gdock.internal` in the
503
- organization this machine is pinned to (the one `connect` resolved, changed with
504
- `network use`). The pin is per machine, not per directory. Only AAAA records exist: the
505
- network is IPv6, and the sandbox sees IPv4.
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 CLI. |
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 split DNS. |
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 this directory. |
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
- | `GHOSTDOCK_CREDENTIALS_PATH` | Absolute path to the credentials file, overriding the default. `settings.json` (the default organization) sits in the same directory. | `./.ghostdock/credentials.json` (relative to the current working directory) |
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 at `./.ghostdock/credentials.json` (relative to the directory the
710
- CLI is run from), with file permissions restricted to the owner (`0600`):
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.10";
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(process.cwd(), ".ghostdock", "credentials.json");
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.source)}).`);
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, set with \`${getCliName()} organizations use\``;
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 CLI."
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
- const override = process.env.GHOSTDOCK_NETWORK_DIR?.trim();
9817
- if (override)
9818
- return override;
9819
- const base = process.env.XDG_CONFIG_HOME?.trim() || join3(homedir(), ".config");
9820
- return join3(base, "ghostdock", "network");
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.zones.canonical, network.zones.short];
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 files = zones.map((zone) => `/etc/resolver/${zone}`);
9896
- lines.push(`PostUp = mkdir -p /etc/resolver && ${files.map((file) => `printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`).join(" && ")}`, `PostDown = rm -f ${files.join(" ")}`);
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
- lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${zones.map((zone) => `'~${zone}'`).join(" ")}`);
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
- ...found ? { shortNameOrganization: found.ref } : {}
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
- await writeConfig(renderConfig({ privateKey, registered, dns }), configPath(deps.dir));
9972
- await writeDeviceState({ baseUrl: getBaseUrl(), deviceId: registered.device.id, address: registered.device.address }, deps.dir);
9973
- if (deps.up()) {
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
- printResult({ device, network, interface: INTERFACE, dns }, () => [
9987
- eventLine("done", `Connected. This machine is ${device.address} in the private network.`),
9988
- ` Names: <sandbox>.<org>.${network.zones.canonical}` + (device.shortNameOrganizationId ? `, and <sandbox>.${network.zones.short}` : ""),
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 runNetworkStatus(deps = defaultDeps()) {
10109
+ async function readConnection(deps) {
10011
10110
  const state = await readDeviceState(deps.dir);
10012
- const connected = deps.up();
10013
- let device = null;
10014
- if (state) {
10015
- try {
10016
- const { devices } = await deps.request("/api/v1/network/devices");
10017
- device = devices.find((d) => d.id === state.deviceId) ?? null;
10018
- } catch (err) {
10019
- deps.notice(`Could not ask the app about this device: ${message(err)}`);
10020
- }
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
- printResult({ connected, device }, () => {
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", `${connected ? "Connected" : "Not connected"}: ${state.address}`)
10137
+ eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
10028
10138
  ];
10029
- if (!device) {
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 listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes`);
10042
- printResult(listed, () => {
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
- return listed.sandboxes.map((s) => ` ${s.name.padEnd(width)} ${s.address.padEnd(28)} ${s.canonicalName} ${s.status}`);
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: { name: "status", description: "Whether this machine is connected, and as which device." },
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 ? { organization: found.ref, source: found.source } : null, null, 2));
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.source)})`));
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 this directory."
10350
+ description: "Set the default organization for commands run without --org (in every directory)."
10222
10351
  },
10223
10352
  args: {
10224
10353
  org: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghostdock",
3
3
  "description": "Ghostdock CLI — device login and agent tool invocation from the command line",
4
- "version": "0.0.10",
4
+ "version": "0.0.12",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ghostdock": "dist/index.js",