@cardelli/ambit 0.2.2 → 0.3.0
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/README.md +38 -22
- package/esm/cli/commands/create/index.js +71 -20
- package/esm/cli/commands/create/machine.d.ts.map +1 -1
- package/esm/cli/commands/create/machine.js +4 -2
- package/esm/cli/commands/deploy/index.js +2 -4
- package/esm/cli/commands/deploy/machine.d.ts.map +1 -1
- package/esm/cli/commands/deploy/machine.js +4 -2
- package/esm/cli/commands/destroy/app.d.ts.map +1 -1
- package/esm/cli/commands/destroy/index.js +2 -0
- package/esm/cli/commands/destroy/network.d.ts.map +1 -1
- package/esm/cli/commands/destroy/network.js +66 -5
- package/esm/cli/commands/doctor.js +13 -4
- package/esm/cli/commands/list.js +1 -1
- package/esm/cli/commands/share.d.ts +2 -0
- package/esm/cli/commands/share.d.ts.map +1 -0
- package/esm/cli/commands/share.js +250 -0
- package/esm/cli/commands/status.js +4 -1
- package/esm/cli/mod.d.ts.map +1 -1
- package/esm/cli/mod.js +2 -0
- package/esm/deno.js +1 -1
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +5 -7
- package/esm/main.d.ts +1 -0
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +2 -0
- package/esm/providers/fly.d.ts.map +1 -1
- package/esm/providers/fly.js +14 -3
- package/esm/providers/tailscale.d.ts +7 -0
- package/esm/providers/tailscale.d.ts.map +1 -1
- package/esm/providers/tailscale.js +23 -1
- package/esm/util/credentials.d.ts.map +1 -1
- package/esm/util/credentials.js +1 -1
- package/esm/util/discovery.d.ts.map +1 -1
- package/esm/util/discovery.js +1 -1
- package/esm/util/tailscale-local.d.ts +41 -0
- package/esm/util/tailscale-local.d.ts.map +1 -1
- package/esm/util/tailscale-local.js +146 -0
- package/esm/util/template.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,17 +67,38 @@ Open `http://my-crazy-site.lab`. It works for you and nobody else.
|
|
|
67
67
|
|
|
68
68
|
### `ambit create <network>`
|
|
69
69
|
|
|
70
|
-
This is the first command you run, it sets up your private network: a named slice of the cloud that only your devices can reach. Under the hood it handles Fly.io and Tailscale authentication, deploys the router, sets up DNS,
|
|
71
|
-
|
|
72
|
-
| Flag
|
|
73
|
-
|
|
|
74
|
-
| `--org <org>`
|
|
75
|
-
| `--region <region>`
|
|
76
|
-
| `--api-key <key>`
|
|
77
|
-
| `--tag <tag>`
|
|
78
|
-
| `--
|
|
79
|
-
|
|
|
80
|
-
| `--
|
|
70
|
+
This is the first command you run, it sets up your private network: a named slice of the cloud that only your devices can reach. Under the hood it handles Fly.io and Tailscale authentication, deploys the router, sets up DNS, configures your local machine to accept the new routes, and automatically adds the router's tag to your Tailscale ACL policy.
|
|
71
|
+
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
| ------------------- | --------------------------------------------------------------------------- |
|
|
74
|
+
| `--org <org>` | Fly.io organization slug |
|
|
75
|
+
| `--region <region>` | Fly.io region (default: `iad`) |
|
|
76
|
+
| `--api-key <key>` | Tailscale API access token (tskey-api-...) |
|
|
77
|
+
| `--tag <tag>` | Tailscale ACL tag for the router (default: `tag:ambit-<network>`) |
|
|
78
|
+
| `--manual` | Skip automatic Tailscale ACL configuration (tagOwners + autoApprovers) |
|
|
79
|
+
| `--no-auto-approve` | Skip waiting for router and approving routes |
|
|
80
|
+
| `-y`, `--yes` | Skip confirmation prompts |
|
|
81
|
+
| `--json` | Machine-readable JSON output (implies `--no-auto-approve`) |
|
|
82
|
+
|
|
83
|
+
### `ambit share <network> <member> [<member>...]`
|
|
84
|
+
|
|
85
|
+
Grants one or more members access to a network by adding two ACL rules per member: one for DNS (so they can resolve `*.<network>` names) and one for the subnet (so they can reach apps). All members are validated before any changes are made. The command is idempotent — safe to re-run.
|
|
86
|
+
|
|
87
|
+
Each member must be one of:
|
|
88
|
+
- `group:<name>` — a Tailscale group
|
|
89
|
+
- `tag:<name>` — a device tag
|
|
90
|
+
- `autogroup:<name>` — a built-in Tailscale group (e.g. `autogroup:member`)
|
|
91
|
+
- A valid email address — a specific Tailscale user
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx @cardelli/ambit share browsers group:team
|
|
95
|
+
npx @cardelli/ambit share browsers group:team alice@example.com group:contractors
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
| ------------- | ------------------------ |
|
|
100
|
+
| `--org <org>` | Fly.io organization slug |
|
|
101
|
+
| `--json` | Machine-readable JSON |
|
|
81
102
|
|
|
82
103
|
### `ambit deploy <app>.<network>`
|
|
83
104
|
|
|
@@ -173,11 +194,12 @@ Lists all discovered routers across networks in a table showing the network name
|
|
|
173
194
|
|
|
174
195
|
### `ambit destroy network <name>`
|
|
175
196
|
|
|
176
|
-
Tears down a network: destroys the router, cleans up DNS, and removes the Tailscale
|
|
197
|
+
Tears down a network: destroys the router, cleans up DNS, removes the Tailscale device, and automatically removes the router's tag from your Tailscale ACL policy (tagOwners and autoApprovers). If there are workload apps still on the network, ambit warns you before proceeding.
|
|
177
198
|
|
|
178
199
|
| Flag | Description |
|
|
179
200
|
| ----------------- | ------------------------------ |
|
|
180
201
|
| `--org <org>` | Fly.io organization slug |
|
|
202
|
+
| `--manual` | Skip automatic Tailscale ACL cleanup (tagOwners + autoApprovers) |
|
|
181
203
|
| `-y`, `--yes` | Skip confirmation prompts |
|
|
182
204
|
| `--json` | Machine-readable JSON output |
|
|
183
205
|
|
|
@@ -218,20 +240,12 @@ Checks app health: verifies the app is deployed and running, then checks the rou
|
|
|
218
240
|
|
|
219
241
|
## Access Control
|
|
220
242
|
|
|
221
|
-
|
|
243
|
+
By default, `ambit create` automatically adds the router's tag to your Tailscale ACL policy (`tagOwners` and `autoApprovers`). `ambit destroy network` automatically removes them. Pass `--manual` to either command to skip this and manage the policy yourself — useful if your API token lacks ACL write permission (`policy_file` scope).
|
|
222
244
|
|
|
223
|
-
If you want to lock
|
|
245
|
+
If you want to lock down which users can reach which networks, two rules do the job — one for DNS queries and one for data traffic:
|
|
224
246
|
|
|
225
247
|
```jsonc
|
|
226
248
|
{
|
|
227
|
-
"tagOwners": {
|
|
228
|
-
"tag:ambit-infra": ["autogroup:admin"]
|
|
229
|
-
},
|
|
230
|
-
"autoApprovers": {
|
|
231
|
-
"routes": {
|
|
232
|
-
"fdaa:X:XXXX::/48": ["tag:ambit-infra"]
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
249
|
"acls": [
|
|
236
250
|
{
|
|
237
251
|
"action": "accept",
|
|
@@ -243,6 +257,8 @@ If you want to lock it down, two rules do the job — one for DNS queries and on
|
|
|
243
257
|
}
|
|
244
258
|
```
|
|
245
259
|
|
|
260
|
+
These `acls` entries are never touched automatically — ambit only manages `tagOwners` and `autoApprovers`.
|
|
261
|
+
|
|
246
262
|
## Multiple Networks
|
|
247
263
|
|
|
248
264
|
You can create as many networks as you want, and each one gets its own TLD on your tailnet. The SOCKS proxy on each router means containers on one network can reach services on another by going through the tailnet, so a browser on your `browsers` network can connect to a database on your `infra` network.
|
|
@@ -9,12 +9,12 @@ import { runMachine } from "../../../lib/machine.js";
|
|
|
9
9
|
import { registerCommand } from "../../mod.js";
|
|
10
10
|
import { getRouterTag } from "../../../util/naming.js";
|
|
11
11
|
import { isPublicTld } from "../../../util/guard.js";
|
|
12
|
-
import { createFlyProvider
|
|
12
|
+
import { createFlyProvider } from "../../../providers/fly.js";
|
|
13
13
|
import { createTailscaleProvider, } from "../../../providers/tailscale.js";
|
|
14
14
|
import { getCredentialStore } from "../../../util/credentials.js";
|
|
15
15
|
import { FLY_PRIVATE_SUBNET, TAILSCALE_API_KEY_PREFIX, } from "../../../util/constants.js";
|
|
16
16
|
import { resolveOrg } from "../../../util/resolve.js";
|
|
17
|
-
import { isAutoApproverConfigured, isTagOwnerConfigured } from "../../../util/tailscale-local.js";
|
|
17
|
+
import { assertAdditivePatch, isAutoApproverConfigured, isTagOwnerConfigured, patchAutoApprover, patchTagOwner, } from "../../../util/tailscale-local.js";
|
|
18
18
|
import { createTransition, hydrateCreate, reportSkipped, } from "./machine.js";
|
|
19
19
|
// =============================================================================
|
|
20
20
|
// Stage 1: Fly.io Configuration
|
|
@@ -34,6 +34,13 @@ const stageFlyConfig = async (out, opts) => {
|
|
|
34
34
|
// =============================================================================
|
|
35
35
|
// Stage 2: Tailscale Configuration
|
|
36
36
|
// =============================================================================
|
|
37
|
+
const handleAclSetFailure = (out, result, action) => {
|
|
38
|
+
if (result.status === 403) {
|
|
39
|
+
out.err(`${action}: Permission Denied (HTTP 403)`);
|
|
40
|
+
return out.die("Your API Token Lacks ACL Write Permission. Re-run with --manual to Skip ACL Changes");
|
|
41
|
+
}
|
|
42
|
+
return out.die(`${action}: ${result.error ?? `HTTP ${result.status}`}`);
|
|
43
|
+
};
|
|
37
44
|
const stageTailscaleConfig = async (out, opts) => {
|
|
38
45
|
out.header("Step 2: Tailscale Configuration").blank();
|
|
39
46
|
const credentials = getCredentialStore();
|
|
@@ -63,9 +70,26 @@ const stageTailscaleConfig = async (out, opts) => {
|
|
|
63
70
|
validateSpinner.success("API Access Token Validated");
|
|
64
71
|
await credentials.setTailscaleApiKey(apiKey);
|
|
65
72
|
const tagOwnerSpinner = out.spinner(`Checking tagOwners for ${opts.tag}`);
|
|
66
|
-
|
|
73
|
+
let policy = await tailscale.acl.getPolicy();
|
|
67
74
|
const hasTagOwner = isTagOwnerConfigured(policy, opts.tag);
|
|
68
|
-
if (!hasTagOwner) {
|
|
75
|
+
if (!hasTagOwner && !opts.manual && policy) {
|
|
76
|
+
tagOwnerSpinner.stop();
|
|
77
|
+
const beforeTagOwner = policy;
|
|
78
|
+
policy = patchTagOwner(policy, opts.tag);
|
|
79
|
+
assertAdditivePatch(beforeTagOwner, policy);
|
|
80
|
+
const validateTagOwner = await tailscale.acl.validatePolicy(policy);
|
|
81
|
+
if (!validateTagOwner.ok) {
|
|
82
|
+
return handleAclSetFailure(out, validateTagOwner, `Validating tagOwners patch for ${opts.tag}`);
|
|
83
|
+
}
|
|
84
|
+
const patchSpinner = out.spinner(`Adding ${opts.tag} to tagOwners`);
|
|
85
|
+
const result = await tailscale.acl.setPolicy(policy);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
patchSpinner.fail(`Adding ${opts.tag} to tagOwners`);
|
|
88
|
+
return handleAclSetFailure(out, result, `Adding ${opts.tag} to tagOwners`);
|
|
89
|
+
}
|
|
90
|
+
patchSpinner.success(`Added ${opts.tag} to tagOwners`);
|
|
91
|
+
}
|
|
92
|
+
else if (!hasTagOwner) {
|
|
69
93
|
tagOwnerSpinner.fail(`${opts.tag} Not Set Up Yet`);
|
|
70
94
|
out.blank()
|
|
71
95
|
.text(` You need to grant yourself permission to create the "${opts.network}"`)
|
|
@@ -75,28 +99,48 @@ const stageTailscaleConfig = async (out, opts) => {
|
|
|
75
99
|
.text(` you're a part of in the Tailscale dashboard:`)
|
|
76
100
|
.link(" https://login.tailscale.com/admin/acls/visual/tags")
|
|
77
101
|
.blank()
|
|
78
|
-
.dim(" Or you can do it manually
|
|
79
|
-
.link(" https://login.tailscale.com/admin/acls/file")
|
|
102
|
+
.dim(" Or you can do it manually with this JSON config:")
|
|
80
103
|
.dim(` "tagOwners": { "${opts.tag}": ["autogroup:admin"] }`)
|
|
81
104
|
.blank();
|
|
82
105
|
return out.die(`Set Up ${opts.tag} in Tailscale, Then Try Again`);
|
|
83
106
|
}
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
else {
|
|
108
|
+
tagOwnerSpinner.success(`${opts.tag} Found in Tailscale ACL`);
|
|
109
|
+
}
|
|
110
|
+
if (!opts.manual) {
|
|
111
|
+
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
112
|
+
if (!hasApprover && policy) {
|
|
113
|
+
const beforeApprover = policy;
|
|
114
|
+
policy = patchAutoApprover(policy, opts.tag, FLY_PRIVATE_SUBNET);
|
|
115
|
+
assertAdditivePatch(beforeApprover, policy);
|
|
116
|
+
const validateApprover = await tailscale.acl.validatePolicy(policy);
|
|
117
|
+
if (!validateApprover.ok) {
|
|
118
|
+
return handleAclSetFailure(out, validateApprover, `Validating autoApprover patch for ${opts.tag}`);
|
|
119
|
+
}
|
|
120
|
+
const approverSpinner = out.spinner(`Adding autoApprover for ${opts.tag}`);
|
|
121
|
+
const result = await tailscale.acl.setPolicy(policy);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
approverSpinner.fail(`Adding autoApprover for ${opts.tag}`);
|
|
124
|
+
return handleAclSetFailure(out, result, `Adding autoApprover for ${opts.tag}`);
|
|
125
|
+
}
|
|
126
|
+
approverSpinner.success(`Added autoApprover for ${opts.tag}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (opts.json) {
|
|
86
130
|
const approverSpinner = out.spinner(`Checking autoApprovers for ${opts.tag}`);
|
|
87
131
|
const hasApprover = isAutoApproverConfigured(policy, opts.tag);
|
|
88
132
|
if (!hasApprover) {
|
|
89
133
|
approverSpinner.fail(`Auto-approve Not Configured for ${opts.tag}`);
|
|
90
134
|
out.blank()
|
|
91
|
-
.text(" In JSON mode, ambit can't interactively approve
|
|
92
|
-
.text(`
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.dim(" https://login.tailscale.com/admin/acls/file")
|
|
135
|
+
.text(" In JSON mode, ambit can't interactively approve the router's")
|
|
136
|
+
.text(` network connections. You can set this up from the Tailscale dashboard:`)
|
|
137
|
+
.link(" https://login.tailscale.com/admin/acls/visual/auto-approvers")
|
|
138
|
+
.dim(` Route: ${FLY_PRIVATE_SUBNET} Owner: ${opts.tag}`)
|
|
96
139
|
.blank()
|
|
140
|
+
.dim(" Or you can do it manually with this JSON config:")
|
|
97
141
|
.dim(` "autoApprovers": { "routes": { "${FLY_PRIVATE_SUBNET}": ["${opts.tag}"] } }`)
|
|
98
142
|
.blank();
|
|
99
|
-
return out.die(`
|
|
143
|
+
return out.die(`Set Up Auto-approve for ${opts.tag} to Use --json`);
|
|
100
144
|
}
|
|
101
145
|
approverSpinner.success(`Auto-approve Configured for ${opts.tag}`);
|
|
102
146
|
}
|
|
@@ -184,7 +228,6 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
184
228
|
.dim(` Route: ${ctx.subnet} Owner: ${opts.tag}`)
|
|
185
229
|
.blank()
|
|
186
230
|
.dim(" Or you can do it manually with this JSON config:")
|
|
187
|
-
.link(" https://login.tailscale.com/admin/acls/file")
|
|
188
231
|
.dim(` "autoApprovers": { "routes": { "${ctx.subnet}": ["${opts.tag}"] } }`);
|
|
189
232
|
if (opts.shouldApprove) {
|
|
190
233
|
out.blank().dim(" Traffic Was Allowed via API for This Session.");
|
|
@@ -196,10 +239,10 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
196
239
|
.blank()
|
|
197
240
|
.dim(" You can do this from the Tailscale dashboard:")
|
|
198
241
|
.link(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
|
|
199
|
-
.dim(` Source: group:YOUR_GROUP
|
|
242
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${opts.tag}:53`)
|
|
243
|
+
.dim(` Source: group:YOUR_GROUP Destination: ${ctx.subnet}:*`)
|
|
200
244
|
.blank()
|
|
201
245
|
.dim(" Or you can do it manually with this JSON config:")
|
|
202
|
-
.link(" https://login.tailscale.com/admin/acls/file")
|
|
203
246
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${opts.tag}:53"]}`)
|
|
204
247
|
.dim(` {"action": "accept", "src": ["group:YOUR_GROUP"], "dst": ["${ctx.subnet}:*"]}`)
|
|
205
248
|
.blank();
|
|
@@ -217,7 +260,7 @@ const stageSummary = async (out, fly, tailscale, ctx, opts) => {
|
|
|
217
260
|
const create = async (argv) => {
|
|
218
261
|
const opts = {
|
|
219
262
|
string: ["org", "region", "api-key", "tag"],
|
|
220
|
-
boolean: ["help", "yes", "json", "no-auto-approve"],
|
|
263
|
+
boolean: ["help", "yes", "json", "no-auto-approve", "manual"],
|
|
221
264
|
alias: { y: "yes" },
|
|
222
265
|
};
|
|
223
266
|
const args = parseArgs(argv, opts);
|
|
@@ -234,7 +277,8 @@ ${bold("OPTIONS")}
|
|
|
234
277
|
--region <region> Fly.io region (default: iad)
|
|
235
278
|
--api-key <key> Tailscale API access token (tskey-api-...)
|
|
236
279
|
--tag <tag> Tailscale ACL tag for the router (default: tag:ambit-<network>)
|
|
237
|
-
--
|
|
280
|
+
--manual Skip automatic Tailscale ACL configuration (tagOwners + autoApprovers)
|
|
281
|
+
--no-auto-approve Skip waiting for router and approving routes
|
|
238
282
|
-y, --yes Skip confirmation prompts
|
|
239
283
|
--json Output as JSON (implies --no-auto-approve)
|
|
240
284
|
|
|
@@ -244,9 +288,14 @@ ${bold("DESCRIPTION")}
|
|
|
244
288
|
|
|
245
289
|
my-app.${args._[0] || "<network>"} resolves to my-app.flycast
|
|
246
290
|
|
|
291
|
+
By default, ambit auto-configures your Tailscale ACL policy (tagOwners
|
|
292
|
+
and autoApprovers). Use --manual if your API token lacks ACL write
|
|
293
|
+
permission or you prefer to manage the policy yourself.
|
|
294
|
+
|
|
247
295
|
${bold("EXAMPLES")}
|
|
248
296
|
ambit create browsers
|
|
249
297
|
ambit create browsers --org my-org --region sea
|
|
298
|
+
ambit create browsers --manual
|
|
250
299
|
`);
|
|
251
300
|
return;
|
|
252
301
|
}
|
|
@@ -260,7 +309,8 @@ ${bold("EXAMPLES")}
|
|
|
260
309
|
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
261
310
|
}
|
|
262
311
|
const tag = args.tag || getRouterTag(network);
|
|
263
|
-
const
|
|
312
|
+
const manual = !!args.manual;
|
|
313
|
+
const shouldApprove = !manual || !(args["no-auto-approve"] || args.json);
|
|
264
314
|
out.blank()
|
|
265
315
|
.header("=".repeat(50))
|
|
266
316
|
.header(` ambit Create: ${network}`)
|
|
@@ -273,6 +323,7 @@ ${bold("EXAMPLES")}
|
|
|
273
323
|
});
|
|
274
324
|
const tailscale = await stageTailscaleConfig(out, {
|
|
275
325
|
json: args.json,
|
|
326
|
+
manual,
|
|
276
327
|
apiKey: args["api-key"],
|
|
277
328
|
tag,
|
|
278
329
|
network,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/create/machine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQhD,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAS7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAOrE,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,eAAe,GACf,cAAc,GACd,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACxD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,eAAO,MAAM,aAAa,GACxB,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CA4BrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAoH7B,CAAC"}
|
|
@@ -5,7 +5,7 @@ import { randomId } from "../../../lib/cli.js";
|
|
|
5
5
|
import { Result } from "../../../lib/result.js";
|
|
6
6
|
import { extractSubnet } from "../../../util/fly-transforms.js";
|
|
7
7
|
import { ROUTER_DOCKER_DIR, SECRET_NETWORK_NAME, SECRET_ROUTER_ID, SECRET_TAILSCALE_AUTHKEY, } from "../../../util/constants.js";
|
|
8
|
-
import { FlyDeployError
|
|
8
|
+
import { FlyDeployError } from "../../../providers/fly.js";
|
|
9
9
|
import { getRouterAppName } from "../../../util/naming.js";
|
|
10
10
|
import { enableAcceptRoutes, isAcceptRoutesEnabled, isAutoApproverConfigured, isTailscaleInstalled, waitForDevice, } from "../../../util/tailscale-local.js";
|
|
11
11
|
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
@@ -83,19 +83,21 @@ export const createTransition = async (phase, ctx) => {
|
|
|
83
83
|
[SECRET_ROUTER_ID]: ctx.routerId,
|
|
84
84
|
}, { stage: true }));
|
|
85
85
|
const dockerDir = ROUTER_DOCKER_DIR;
|
|
86
|
+
const deploySpinner = ctx.out.spinner("Deploying Router to Fly.io");
|
|
86
87
|
try {
|
|
87
88
|
await ctx.fly.deploy.router(ctx.appName, dockerDir, {
|
|
88
89
|
region: ctx.region,
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
catch (e) {
|
|
93
|
+
deploySpinner.fail("Router Deploy Failed");
|
|
92
94
|
if (e instanceof FlyDeployError) {
|
|
93
95
|
ctx.out.dim(` ${e.detail}`);
|
|
94
96
|
return Result.err(e.message);
|
|
95
97
|
}
|
|
96
98
|
throw e;
|
|
97
99
|
}
|
|
98
|
-
|
|
100
|
+
deploySpinner.success("Router Deployed");
|
|
99
101
|
const machines = await ctx.fly.machines.list(ctx.appName);
|
|
100
102
|
const m = machines.find((m) => m.private_ip);
|
|
101
103
|
if (m?.private_ip)
|
|
@@ -7,7 +7,7 @@ import { checkArgs } from "../../../lib/args.js";
|
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
|
8
8
|
import { runMachine } from "../../../lib/machine.js";
|
|
9
9
|
import { registerCommand } from "../../mod.js";
|
|
10
|
-
import { createFlyProvider
|
|
10
|
+
import { createFlyProvider } from "../../../providers/fly.js";
|
|
11
11
|
import { getWorkloadAppName } from "../../../util/naming.js";
|
|
12
12
|
import { findRouterApp, getRouterMachineInfo } from "../../../util/discovery.js";
|
|
13
13
|
import { resolveOrg } from "../../../util/resolve.js";
|
|
@@ -126,9 +126,7 @@ const stageSummary = (out, ctx, deployConfig) => {
|
|
|
126
126
|
}
|
|
127
127
|
out.blank()
|
|
128
128
|
.header("=".repeat(50))
|
|
129
|
-
.header(hasIssues
|
|
130
|
-
? " Deploy Completed (with Warnings)"
|
|
131
|
-
: " Deploy Completed!")
|
|
129
|
+
.header(hasIssues ? " Deploy Completed (with Warnings)" : " Deploy Completed!")
|
|
132
130
|
.header("=".repeat(50))
|
|
133
131
|
.blank()
|
|
134
132
|
.text(`App '${ctx.app}' Is Reachable from Your Tailnet as:`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/machine.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,OAAO,GACP,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE;QACL,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,CAAC,EAAE;QACN,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAaD,eAAO,MAAM,mBAAmB,GAC9B,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CAMrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/machine.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,OAAO,GACP,UAAU,CAAC;AAMf,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE;QACL,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,CAAC,EAAE;QACN,mBAAmB,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjE,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAaD,eAAO,MAAM,mBAAmB,GAC9B,KAAK,MAAM,CAAC,YAAY,CAAC,EACzB,YAAY,WAAW,SAMxB,CAAC;AAMF,eAAO,MAAM,aAAa,GACxB,KAAK,SAAS,KACb,OAAO,CAAC,WAAW,CAMrB,CAAC;AAMF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,WAAW,EAClB,KAAK,SAAS,KACb,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CA6G7B,CAAC"}
|
|
@@ -67,11 +67,13 @@ export const deployTransition = async (phase, ctx) => {
|
|
|
67
67
|
return Result.ok("deploy");
|
|
68
68
|
}
|
|
69
69
|
case "deploy": {
|
|
70
|
-
ctx.out.blank()
|
|
70
|
+
ctx.out.blank();
|
|
71
|
+
const deploySpinner = ctx.out.spinner("Deploying to Fly.io");
|
|
71
72
|
try {
|
|
72
73
|
await ctx.fly.deploy.app(ctx.app, ctx.deployOptions);
|
|
73
74
|
}
|
|
74
75
|
catch (e) {
|
|
76
|
+
deploySpinner.fail("Deploy Failed");
|
|
75
77
|
if (e instanceof FlyDeployError) {
|
|
76
78
|
ctx.out.dim(` ${e.detail}`);
|
|
77
79
|
return Result.err(e.message);
|
|
@@ -88,7 +90,7 @@ export const deployTransition = async (phase, ctx) => {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
deploySpinner.success("Deploy Succeeded");
|
|
92
94
|
return Result.ok("audit");
|
|
93
95
|
}
|
|
94
96
|
case "audit": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/app.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/app.ts"],"names":[],"mappings":"AAkLA,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAoF7D,CAAC"}
|
|
@@ -23,10 +23,12 @@ ${bold("SUBCOMMANDS")}
|
|
|
23
23
|
app Destroy a workload app on a network
|
|
24
24
|
|
|
25
25
|
${bold("OPTIONS")}
|
|
26
|
+
--manual Skip automatic Tailscale ACL cleanup (network subcommand)
|
|
26
27
|
--help Show help for a subcommand
|
|
27
28
|
|
|
28
29
|
${bold("EXAMPLES")}
|
|
29
30
|
ambit destroy network browsers
|
|
31
|
+
ambit destroy network browsers --yes
|
|
30
32
|
ambit destroy app my-app.browsers
|
|
31
33
|
ambit destroy app my-app --network browsers
|
|
32
34
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/network.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/destroy/network.ts"],"names":[],"mappings":"AAuVA,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CA0DjE,CAAC"}
|
|
@@ -7,7 +7,9 @@ import { checkArgs } from "../../../lib/args.js";
|
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
|
8
8
|
import { Result } from "../../../lib/result.js";
|
|
9
9
|
import { runMachine } from "../../../lib/machine.js";
|
|
10
|
-
import { findRouterApp, listWorkloadAppsOnNetwork
|
|
10
|
+
import { findRouterApp, listWorkloadAppsOnNetwork } from "../../../util/discovery.js";
|
|
11
|
+
import { getRouterTag } from "../../../util/naming.js";
|
|
12
|
+
import { isAutoApproverConfigured, isTagOwnerConfigured, unpatchAutoApprover, unpatchTagOwner, } from "../../../util/tailscale-local.js";
|
|
11
13
|
import { initSession } from "../../../util/session.js";
|
|
12
14
|
// =============================================================================
|
|
13
15
|
// Phase Labels
|
|
@@ -17,6 +19,7 @@ const DESTROY_NETWORK_PHASES = [
|
|
|
17
19
|
{ phase: "clear_dns", label: "Split DNS Cleared" },
|
|
18
20
|
{ phase: "remove_device", label: "Tailscale Device Removed" },
|
|
19
21
|
{ phase: "delete_app", label: "Fly App Destroyed" },
|
|
22
|
+
{ phase: "clean_acl", label: "ACL Policy Cleaned" },
|
|
20
23
|
];
|
|
21
24
|
const reportSkipped = (out, startPhase) => {
|
|
22
25
|
for (const { phase, label } of DESTROY_NETWORK_PHASES) {
|
|
@@ -107,6 +110,48 @@ const destroyNetworkTransition = async (phase, ctx) => {
|
|
|
107
110
|
else {
|
|
108
111
|
ctx.out.skip("Fly App Already Destroyed");
|
|
109
112
|
}
|
|
113
|
+
return Result.ok(ctx.manual ? "complete" : "clean_acl");
|
|
114
|
+
}
|
|
115
|
+
case "clean_acl": {
|
|
116
|
+
const tag = ctx.tag || getRouterTag(ctx.network);
|
|
117
|
+
let policy = await ctx.tailscale.acl.getPolicy();
|
|
118
|
+
if (!policy) {
|
|
119
|
+
ctx.out.skip("Could Not Read ACL Policy");
|
|
120
|
+
return Result.ok("complete");
|
|
121
|
+
}
|
|
122
|
+
let changed = false;
|
|
123
|
+
if (isTagOwnerConfigured(policy, tag)) {
|
|
124
|
+
policy = unpatchTagOwner(policy, tag);
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
if (isAutoApproverConfigured(policy, tag)) {
|
|
128
|
+
policy = unpatchAutoApprover(policy, tag);
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
if (changed) {
|
|
132
|
+
const validateResult = await ctx.tailscale.acl
|
|
133
|
+
.validatePolicy(policy);
|
|
134
|
+
if (!validateResult.ok) {
|
|
135
|
+
ctx.out.warn(`ACL Policy Validation Failed — Skipping Cleanup: ${validateResult.error ?? `HTTP ${validateResult.status}`}`);
|
|
136
|
+
return Result.ok("complete");
|
|
137
|
+
}
|
|
138
|
+
const spinner = ctx.out.spinner(`Removing ${tag} from ACL policy`);
|
|
139
|
+
const result = await ctx.tailscale.acl.setPolicy(policy);
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
spinner.fail(`Removing ${tag} from ACL policy`);
|
|
142
|
+
if (result.status === 403) {
|
|
143
|
+
ctx.out.warn("Your API Token Lacks ACL Write Permission. Re-run with --manual to Skip ACL Changes");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
ctx.out.warn(`Failed to Update ACL Policy: ${result.error ?? `HTTP ${result.status}`}`);
|
|
147
|
+
}
|
|
148
|
+
return Result.ok("complete");
|
|
149
|
+
}
|
|
150
|
+
spinner.success(`Removed ${tag} from ACL policy`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
ctx.out.skip(`No ACL Entries Found for ${tag}`);
|
|
154
|
+
}
|
|
110
155
|
return Result.ok("complete");
|
|
111
156
|
}
|
|
112
157
|
default:
|
|
@@ -147,7 +192,14 @@ const stageSummary = (out, ctx) => {
|
|
|
147
192
|
workloadAppsWarned: 0,
|
|
148
193
|
});
|
|
149
194
|
out.ok("Router Destroyed");
|
|
150
|
-
|
|
195
|
+
const tag = ctx.tag || getRouterTag(ctx.network);
|
|
196
|
+
if (!ctx.manual) {
|
|
197
|
+
out.blank()
|
|
198
|
+
.dim("If You Added ACL Rules Referencing This Router, Remember to Remove:")
|
|
199
|
+
.dim(` acls: rules referencing ${tag}`)
|
|
200
|
+
.blank();
|
|
201
|
+
}
|
|
202
|
+
else if (ctx.tag) {
|
|
151
203
|
out.blank()
|
|
152
204
|
.dim("If You Added ACL Policy Entries for This Router, Remember to Remove:")
|
|
153
205
|
.dim(` tagOwners: ${ctx.tag}`)
|
|
@@ -169,7 +221,7 @@ const stageSummary = (out, ctx) => {
|
|
|
169
221
|
export const destroyNetwork = async (argv) => {
|
|
170
222
|
const opts = {
|
|
171
223
|
string: ["network", "org"],
|
|
172
|
-
boolean: ["help", "yes", "json"],
|
|
224
|
+
boolean: ["help", "yes", "json", "manual"],
|
|
173
225
|
alias: { y: "yes" },
|
|
174
226
|
};
|
|
175
227
|
const args = parseArgs(argv, opts);
|
|
@@ -183,17 +235,25 @@ ${bold("USAGE")}
|
|
|
183
235
|
|
|
184
236
|
${bold("OPTIONS")}
|
|
185
237
|
--org <org> Fly.io organization slug
|
|
238
|
+
--manual Skip automatic Tailscale ACL cleanup (tagOwners + autoApprovers)
|
|
186
239
|
-y, --yes Skip confirmation prompts
|
|
187
240
|
--json Output as JSON
|
|
188
241
|
|
|
242
|
+
${bold("DESCRIPTION")}
|
|
243
|
+
By default, ambit removes the router's tag from your Tailscale ACL
|
|
244
|
+
policy (tagOwners and autoApprovers). Use --manual if your API token
|
|
245
|
+
lacks ACL write permission or you prefer to manage the policy yourself.
|
|
246
|
+
|
|
189
247
|
${bold("EXAMPLES")}
|
|
190
248
|
ambit destroy network browsers
|
|
191
|
-
ambit destroy network browsers --
|
|
249
|
+
ambit destroy network browsers --yes
|
|
250
|
+
ambit destroy network browsers --manual --org my-org
|
|
192
251
|
`);
|
|
193
252
|
return;
|
|
194
253
|
}
|
|
195
254
|
const out = createOutput(args.json);
|
|
196
|
-
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
255
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
256
|
+
args.network;
|
|
197
257
|
if (!network) {
|
|
198
258
|
return out.die("Network Name Required. Usage: ambit destroy network <name>");
|
|
199
259
|
}
|
|
@@ -206,5 +266,6 @@ ${bold("EXAMPLES")}
|
|
|
206
266
|
org,
|
|
207
267
|
yes: args.yes,
|
|
208
268
|
json: args.json,
|
|
269
|
+
manual: !!args.manual,
|
|
209
270
|
});
|
|
210
271
|
};
|
|
@@ -130,7 +130,9 @@ const stageSummary = (out, results) => {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
out.blank();
|
|
133
|
-
out.text(issues === 0
|
|
133
|
+
out.text(issues === 0
|
|
134
|
+
? "All Checks Passed."
|
|
135
|
+
: `${issues} Issue${issues > 1 ? "s" : ""} Found.`);
|
|
134
136
|
out.blank();
|
|
135
137
|
out.print();
|
|
136
138
|
};
|
|
@@ -138,7 +140,10 @@ const stageSummary = (out, results) => {
|
|
|
138
140
|
// Doctor Network
|
|
139
141
|
// =============================================================================
|
|
140
142
|
const doctorNetwork = async (argv) => {
|
|
141
|
-
const opts = {
|
|
143
|
+
const opts = {
|
|
144
|
+
string: ["network", "org"],
|
|
145
|
+
boolean: ["help", "json"],
|
|
146
|
+
};
|
|
142
147
|
const args = parseArgs(argv, opts);
|
|
143
148
|
checkArgs(args, opts, "ambit doctor network");
|
|
144
149
|
if (args.help) {
|
|
@@ -168,7 +173,8 @@ ${bold("CHECKS")}
|
|
|
168
173
|
out.blank().header("ambit Doctor: Network").blank();
|
|
169
174
|
const results = [];
|
|
170
175
|
const report = makeReporter(results, out);
|
|
171
|
-
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
176
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) ||
|
|
177
|
+
args.network;
|
|
172
178
|
const { fly, tailscale, org } = await initSession(out, {
|
|
173
179
|
json: args.json,
|
|
174
180
|
org: args.org,
|
|
@@ -181,7 +187,10 @@ ${bold("CHECKS")}
|
|
|
181
187
|
// Doctor App
|
|
182
188
|
// =============================================================================
|
|
183
189
|
const doctorApp = async (argv) => {
|
|
184
|
-
const opts = {
|
|
190
|
+
const opts = {
|
|
191
|
+
string: ["network", "org"],
|
|
192
|
+
boolean: ["help", "json"],
|
|
193
|
+
};
|
|
185
194
|
const args = parseArgs(argv, opts);
|
|
186
195
|
checkArgs(args, opts, "ambit doctor app");
|
|
187
196
|
if (args.help) {
|
package/esm/cli/commands/list.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bold } from "../../lib/cli.js";
|
|
|
7
7
|
import { checkArgs } from "../../lib/args.js";
|
|
8
8
|
import { createOutput } from "../../lib/output.js";
|
|
9
9
|
import { registerCommand } from "../mod.js";
|
|
10
|
-
import { discoverRouters
|
|
10
|
+
import { discoverRouters } from "../../util/discovery.js";
|
|
11
11
|
import { initSession } from "../../util/session.js";
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Stage: Render
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/share.ts"],"names":[],"mappings":""}
|