@cardelli/ambit 0.1.3 → 0.1.5
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/esm/deno.d.ts +1 -0
- package/esm/deno.js +3 -2
- package/esm/deps/jsr.io/@std/cli/1.0.28/_data.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.js +17 -3
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/unicode_width.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.d.ts +2 -2
- package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.js +30 -20
- package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.js +1 -1
- package/esm/deps/jsr.io/@zod/zod/4.3.6/src/v4/core/json-schema-generator.d.ts +1 -1
- package/esm/lib/cli.d.ts +0 -1
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +41 -26
- package/esm/lib/command.d.ts +23 -48
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +60 -89
- package/esm/lib/output.d.ts +1 -1
- package/esm/lib/output.d.ts.map +1 -1
- package/esm/lib/output.js +5 -9
- package/esm/lib/result.d.ts +19 -7
- package/esm/lib/result.d.ts.map +1 -1
- package/esm/lib/result.js +47 -1
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +3 -2
- package/esm/src/cli/commands/create.js +111 -97
- package/esm/src/cli/commands/deploy.js +61 -35
- package/esm/src/cli/commands/destroy.js +224 -29
- package/esm/src/cli/commands/doctor.js +2 -2
- package/esm/src/cli/commands/list.js +1 -1
- package/esm/src/cli/commands/status.js +1 -1
- package/esm/src/cli/mod.d.ts.map +1 -1
- package/esm/src/cli/mod.js +4 -3
- package/esm/src/discovery.d.ts +13 -0
- package/esm/src/discovery.d.ts.map +1 -1
- package/esm/src/discovery.js +42 -0
- package/esm/src/docker/router/start.sh +18 -72
- package/esm/src/providers/fly.d.ts +6 -0
- package/esm/src/providers/fly.d.ts.map +1 -1
- package/esm/src/providers/fly.js +77 -81
- package/esm/src/providers/tailscale.d.ts.map +1 -1
- package/esm/src/providers/tailscale.js +5 -11
- package/esm/src/template.d.ts +8 -7
- package/esm/src/template.d.ts.map +1 -1
- package/esm/src/template.js +32 -26
- package/package.json +7 -1
- package/esm/deps/jsr.io/@std/cli/1.0.27/_data.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.d.ts +0 -6
- package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.js +0 -18
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_data.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_data.js +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.js +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.js +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.js +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/unicode_width.d.ts +0 -0
- /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/unicode_width.js +0 -0
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// Destroy Command -
|
|
2
|
+
// Destroy Command - Destroy Networks or Apps
|
|
3
3
|
// =============================================================================
|
|
4
|
-
import
|
|
4
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
5
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
6
|
import { bold, confirm } from "../../../lib/cli.js";
|
|
6
7
|
import { createOutput } from "../../../lib/output.js";
|
|
7
8
|
import { registerCommand } from "../mod.js";
|
|
8
9
|
import { createFlyProvider } from "../../providers/fly.js";
|
|
9
10
|
import { createTailscaleProvider } from "../../providers/tailscale.js";
|
|
10
11
|
import { checkDependencies } from "../../credentials.js";
|
|
11
|
-
import { findRouterApp } from "../../discovery.js";
|
|
12
|
+
import { findRouterApp, findWorkloadApp, listWorkloadAppsOnNetwork, } from "../../discovery.js";
|
|
12
13
|
import { resolveOrg } from "../../resolve.js";
|
|
14
|
+
import { assertNotRouter } from "../../guard.js";
|
|
13
15
|
// =============================================================================
|
|
14
|
-
//
|
|
16
|
+
// Top-Level Help
|
|
15
17
|
// =============================================================================
|
|
16
|
-
const
|
|
18
|
+
const showDestroyHelp = () => {
|
|
19
|
+
console.log(`
|
|
20
|
+
${bold("ambit destroy")} - Destroy Networks or Apps
|
|
21
|
+
|
|
22
|
+
${bold("USAGE")}
|
|
23
|
+
ambit destroy network <name> [options]
|
|
24
|
+
ambit destroy app <app>.<network> [options]
|
|
25
|
+
|
|
26
|
+
${bold("SUBCOMMANDS")}
|
|
27
|
+
network Tear down a router, clean up DNS and tailnet device
|
|
28
|
+
app Destroy a workload app on a network
|
|
29
|
+
|
|
30
|
+
${bold("OPTIONS")}
|
|
31
|
+
--help Show help for a subcommand
|
|
32
|
+
|
|
33
|
+
${bold("EXAMPLES")}
|
|
34
|
+
ambit destroy network browsers
|
|
35
|
+
ambit destroy app my-app.browsers
|
|
36
|
+
ambit destroy app my-app --network browsers
|
|
37
|
+
|
|
38
|
+
Run 'ambit destroy network --help' or 'ambit destroy app --help' for details.
|
|
39
|
+
`);
|
|
40
|
+
};
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Destroy Network
|
|
43
|
+
// =============================================================================
|
|
44
|
+
const destroyNetwork = async (argv) => {
|
|
17
45
|
const args = parseArgs(argv, {
|
|
18
46
|
string: ["network", "org"],
|
|
19
47
|
boolean: ["help", "yes", "json"],
|
|
@@ -21,57 +49,78 @@ const destroy = async (argv) => {
|
|
|
21
49
|
});
|
|
22
50
|
if (args.help) {
|
|
23
51
|
console.log(`
|
|
24
|
-
${bold("ambit destroy")} - Tear Down Router
|
|
52
|
+
${bold("ambit destroy network")} - Tear Down Router
|
|
25
53
|
|
|
26
54
|
${bold("USAGE")}
|
|
27
|
-
ambit destroy
|
|
55
|
+
ambit destroy network <name> [--org <org>] [--yes] [--json]
|
|
28
56
|
|
|
29
57
|
${bold("OPTIONS")}
|
|
30
|
-
--network <name> Network of the router to destroy (required)
|
|
31
58
|
--org <org> Fly.io organization slug
|
|
32
59
|
-y, --yes Skip confirmation prompts
|
|
33
60
|
--json Output as JSON
|
|
61
|
+
|
|
62
|
+
${bold("EXAMPLES")}
|
|
63
|
+
ambit destroy network browsers
|
|
64
|
+
ambit destroy network browsers --org my-org --yes
|
|
34
65
|
`);
|
|
35
66
|
return;
|
|
36
67
|
}
|
|
37
68
|
const out = createOutput(args.json);
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
// Accept network as positional or --network flag (backward compat)
|
|
70
|
+
const network = (typeof args._[0] === "string" ? args._[0] : undefined) || args.network;
|
|
71
|
+
if (!network) {
|
|
72
|
+
return out.die("Network name required. Usage: ambit destroy network <name>");
|
|
40
73
|
}
|
|
41
|
-
//
|
|
74
|
+
// ===========================================================================
|
|
42
75
|
// Prerequisites
|
|
43
|
-
//
|
|
76
|
+
// ===========================================================================
|
|
44
77
|
const { tailscaleKey } = await checkDependencies(out);
|
|
45
78
|
const fly = createFlyProvider();
|
|
46
79
|
await fly.ensureAuth({ interactive: !args.json });
|
|
47
80
|
const tailscale = createTailscaleProvider("-", tailscaleKey);
|
|
48
81
|
const org = await resolveOrg(fly, args, out);
|
|
49
|
-
//
|
|
82
|
+
// ===========================================================================
|
|
50
83
|
// Discover Router
|
|
51
|
-
//
|
|
84
|
+
// ===========================================================================
|
|
52
85
|
const spinner = out.spinner("Discovering Router");
|
|
53
|
-
const app = await findRouterApp(fly, org,
|
|
86
|
+
const app = await findRouterApp(fly, org, network);
|
|
54
87
|
if (!app) {
|
|
55
88
|
spinner.fail("Router Not Found");
|
|
56
|
-
return out.die(`No Router Found for Network '${
|
|
89
|
+
return out.die(`No Router Found for Network '${network}'`);
|
|
57
90
|
}
|
|
58
91
|
spinner.success(`Found Router: ${app.appName}`);
|
|
59
92
|
let tsDevice = null;
|
|
60
93
|
try {
|
|
61
94
|
tsDevice = await tailscale.getDeviceByHostname(app.appName);
|
|
62
95
|
}
|
|
63
|
-
catch {
|
|
96
|
+
catch {
|
|
97
|
+
/* device may not exist */
|
|
98
|
+
}
|
|
64
99
|
const tag = tsDevice?.tags?.[0] ?? null;
|
|
65
|
-
//
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
// Check for Workload Apps on This Network
|
|
102
|
+
// ===========================================================================
|
|
103
|
+
const workloadApps = await listWorkloadAppsOnNetwork(fly, org, network);
|
|
104
|
+
// ===========================================================================
|
|
66
105
|
// Confirm
|
|
67
|
-
//
|
|
106
|
+
// ===========================================================================
|
|
68
107
|
out.blank()
|
|
69
|
-
.header("ambit Destroy")
|
|
108
|
+
.header("ambit Destroy Network")
|
|
70
109
|
.blank()
|
|
71
|
-
.text(` Network: ${
|
|
110
|
+
.text(` Network: ${network}`)
|
|
72
111
|
.text(` Router App: ${app.appName}`)
|
|
73
112
|
.text(` Tag: ${tag ?? "unknown"}`)
|
|
74
113
|
.blank();
|
|
114
|
+
if (workloadApps.length > 0) {
|
|
115
|
+
out.warn(`${workloadApps.length} Workload App(s) Still on Network '${network}':`);
|
|
116
|
+
for (const wa of workloadApps) {
|
|
117
|
+
out.text(` - ${wa.appName}`);
|
|
118
|
+
}
|
|
119
|
+
out.blank();
|
|
120
|
+
out.dim("These apps will lose connectivity when the router is destroyed.");
|
|
121
|
+
out.dim(`Consider destroying them first with: ambit destroy app <name>.${network}`);
|
|
122
|
+
out.blank();
|
|
123
|
+
}
|
|
75
124
|
if (!args.yes && !args.json) {
|
|
76
125
|
const confirmed = await confirm("Destroy this router?");
|
|
77
126
|
if (!confirmed) {
|
|
@@ -80,12 +129,12 @@ ${bold("OPTIONS")}
|
|
|
80
129
|
}
|
|
81
130
|
out.blank();
|
|
82
131
|
}
|
|
83
|
-
//
|
|
132
|
+
// ===========================================================================
|
|
84
133
|
// Tear Down
|
|
85
|
-
//
|
|
134
|
+
// ===========================================================================
|
|
86
135
|
const dnsSpinner = out.spinner("Clearing Split DNS");
|
|
87
136
|
try {
|
|
88
|
-
await tailscale.clearSplitDns(
|
|
137
|
+
await tailscale.clearSplitDns(network);
|
|
89
138
|
dnsSpinner.success("Split DNS Cleared");
|
|
90
139
|
}
|
|
91
140
|
catch {
|
|
@@ -113,10 +162,14 @@ ${bold("OPTIONS")}
|
|
|
113
162
|
catch {
|
|
114
163
|
appSpinner.fail("Could Not Destroy Fly App");
|
|
115
164
|
}
|
|
116
|
-
//
|
|
165
|
+
// ===========================================================================
|
|
117
166
|
// Done
|
|
118
|
-
//
|
|
119
|
-
out.done({
|
|
167
|
+
// ===========================================================================
|
|
168
|
+
out.done({
|
|
169
|
+
destroyed: true,
|
|
170
|
+
appName: app.appName,
|
|
171
|
+
workloadAppsWarned: workloadApps.length,
|
|
172
|
+
});
|
|
120
173
|
out.ok("Router Destroyed");
|
|
121
174
|
if (tag) {
|
|
122
175
|
out.blank()
|
|
@@ -135,11 +188,153 @@ ${bold("OPTIONS")}
|
|
|
135
188
|
out.print();
|
|
136
189
|
};
|
|
137
190
|
// =============================================================================
|
|
191
|
+
// Destroy App
|
|
192
|
+
// =============================================================================
|
|
193
|
+
const destroyApp = async (argv) => {
|
|
194
|
+
const args = parseArgs(argv, {
|
|
195
|
+
string: ["network", "org"],
|
|
196
|
+
boolean: ["help", "yes", "json"],
|
|
197
|
+
alias: { y: "yes" },
|
|
198
|
+
});
|
|
199
|
+
if (args.help) {
|
|
200
|
+
console.log(`
|
|
201
|
+
${bold("ambit destroy app")} - Destroy a Workload App
|
|
202
|
+
|
|
203
|
+
${bold("USAGE")}
|
|
204
|
+
ambit destroy app <app>.<network> [--org <org>] [--yes] [--json]
|
|
205
|
+
ambit destroy app <app> --network <name> [--org <org>] [--yes] [--json]
|
|
206
|
+
|
|
207
|
+
${bold("OPTIONS")}
|
|
208
|
+
--network <name> Target network (if not using dot syntax)
|
|
209
|
+
--org <org> Fly.io organization slug
|
|
210
|
+
-y, --yes Skip confirmation prompts
|
|
211
|
+
--json Output as JSON
|
|
212
|
+
|
|
213
|
+
${bold("EXAMPLES")}
|
|
214
|
+
ambit destroy app my-app.browsers
|
|
215
|
+
ambit destroy app my-app --network browsers --yes
|
|
216
|
+
`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const out = createOutput(args.json);
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
// Parse App & Network
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
const appArg = args._[0];
|
|
224
|
+
if (!appArg || typeof appArg !== "string") {
|
|
225
|
+
return out.die("Missing app name. Usage: ambit destroy app <app>.<network>");
|
|
226
|
+
}
|
|
227
|
+
let app;
|
|
228
|
+
let network;
|
|
229
|
+
if (appArg.includes(".")) {
|
|
230
|
+
const parts = appArg.split(".");
|
|
231
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
232
|
+
return out.die(`'${appArg}' should have exactly one dot, like my-app.my-network`);
|
|
233
|
+
}
|
|
234
|
+
if (args.network) {
|
|
235
|
+
return out.die(`Network is already part of the name ('${appArg}'), --network is not needed`);
|
|
236
|
+
}
|
|
237
|
+
app = parts[0];
|
|
238
|
+
network = parts[1];
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
app = appArg;
|
|
242
|
+
if (!args.network) {
|
|
243
|
+
return out.die(`Missing network. Use: ambit destroy app ${app}.<network>`);
|
|
244
|
+
}
|
|
245
|
+
network = args.network;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
assertNotRouter(app);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
return out.die(e instanceof Error ? e.message : String(e));
|
|
252
|
+
}
|
|
253
|
+
// ===========================================================================
|
|
254
|
+
// Prerequisites
|
|
255
|
+
// ===========================================================================
|
|
256
|
+
const { tailscaleKey: _tailscaleKey } = await checkDependencies(out);
|
|
257
|
+
const fly = createFlyProvider();
|
|
258
|
+
await fly.ensureAuth({ interactive: !args.json });
|
|
259
|
+
const org = await resolveOrg(fly, args, out);
|
|
260
|
+
// ===========================================================================
|
|
261
|
+
// Discover App
|
|
262
|
+
// ===========================================================================
|
|
263
|
+
const spinner = out.spinner("Discovering App");
|
|
264
|
+
const workloadApp = await findWorkloadApp(fly, org, app, network);
|
|
265
|
+
if (!workloadApp) {
|
|
266
|
+
spinner.fail("App Not Found");
|
|
267
|
+
// Check if app exists on a different network
|
|
268
|
+
const anyApp = await findWorkloadApp(fly, org, app);
|
|
269
|
+
if (anyApp) {
|
|
270
|
+
return out.die(`App '${app}' exists on network '${anyApp.network}', not '${network}'`);
|
|
271
|
+
}
|
|
272
|
+
return out.die(`No app '${app}' found on network '${network}'`);
|
|
273
|
+
}
|
|
274
|
+
spinner.success(`Found App: ${workloadApp.appName} (network: ${workloadApp.network})`);
|
|
275
|
+
// ===========================================================================
|
|
276
|
+
// Confirm
|
|
277
|
+
// ===========================================================================
|
|
278
|
+
out.blank()
|
|
279
|
+
.header("ambit Destroy App")
|
|
280
|
+
.blank()
|
|
281
|
+
.text(` App: ${workloadApp.appName}`)
|
|
282
|
+
.text(` Network: ${workloadApp.network}`)
|
|
283
|
+
.blank();
|
|
284
|
+
if (!args.yes && !args.json) {
|
|
285
|
+
const confirmed = await confirm(`Destroy app '${app}' on network '${network}'?`);
|
|
286
|
+
if (!confirmed) {
|
|
287
|
+
out.text("Cancelled.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
out.blank();
|
|
291
|
+
}
|
|
292
|
+
// ===========================================================================
|
|
293
|
+
// Destroy
|
|
294
|
+
// ===========================================================================
|
|
295
|
+
const appSpinner = out.spinner("Destroying Fly App");
|
|
296
|
+
try {
|
|
297
|
+
await fly.deleteApp(workloadApp.appName);
|
|
298
|
+
appSpinner.success("Fly App Destroyed");
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
appSpinner.fail("Could Not Destroy Fly App");
|
|
302
|
+
}
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
// Done
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
out.done({ destroyed: true, appName: workloadApp.appName, network });
|
|
307
|
+
out.ok("App Destroyed");
|
|
308
|
+
out.blank();
|
|
309
|
+
out.print();
|
|
310
|
+
};
|
|
311
|
+
// =============================================================================
|
|
312
|
+
// Dispatcher
|
|
313
|
+
// =============================================================================
|
|
314
|
+
const destroy = async (argv) => {
|
|
315
|
+
const subcommand = typeof argv[0] === "string" ? argv[0] : undefined;
|
|
316
|
+
if (subcommand === "network") {
|
|
317
|
+
return destroyNetwork(argv.slice(1));
|
|
318
|
+
}
|
|
319
|
+
if (subcommand === "app") {
|
|
320
|
+
return destroyApp(argv.slice(1));
|
|
321
|
+
}
|
|
322
|
+
// Handle --help at the top level
|
|
323
|
+
const args = parseArgs(argv, { boolean: ["help"] });
|
|
324
|
+
if (args.help) {
|
|
325
|
+
showDestroyHelp();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// No valid subcommand
|
|
329
|
+
showDestroyHelp();
|
|
330
|
+
dntShim.Deno.exit(1);
|
|
331
|
+
};
|
|
332
|
+
// =============================================================================
|
|
138
333
|
// Register Command
|
|
139
334
|
// =============================================================================
|
|
140
335
|
registerCommand({
|
|
141
336
|
name: "destroy",
|
|
142
|
-
description: "
|
|
143
|
-
usage: "ambit destroy
|
|
337
|
+
description: "Destroy a network (router) or a workload app",
|
|
338
|
+
usage: "ambit destroy network|app <name> [options]",
|
|
144
339
|
run: destroy,
|
|
145
340
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// Doctor Command - Verify Environment and Infrastructure Health
|
|
3
3
|
// =============================================================================
|
|
4
|
-
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
5
|
import { bold } from "../../../lib/cli.js";
|
|
6
6
|
import { createOutput } from "../../../lib/output.js";
|
|
7
7
|
import { runCommand } from "../../../lib/command.js";
|
|
@@ -67,7 +67,7 @@ ${bold("CHECKS")}
|
|
|
67
67
|
report("Tailscale Installed", await isTailscaleInstalled(), "Install from https://tailscale.com/download");
|
|
68
68
|
const tsStatus = await runCommand(["tailscale", "status", "--json"]);
|
|
69
69
|
let tsConnected = false;
|
|
70
|
-
if (tsStatus.
|
|
70
|
+
if (tsStatus.ok) {
|
|
71
71
|
try {
|
|
72
72
|
const parsed = JSON.parse(tsStatus.stdout);
|
|
73
73
|
tsConnected = parsed.BackendState === "Running";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// List Command - List All Discovered Routers
|
|
3
3
|
// =============================================================================
|
|
4
|
-
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
5
|
import { Table } from "../../../deps/jsr.io/@cliffy/table/1.0.0/mod.js";
|
|
6
6
|
import { bold } from "../../../lib/cli.js";
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// Status Command - Show Router Status
|
|
3
3
|
// =============================================================================
|
|
4
|
-
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.
|
|
4
|
+
import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
5
5
|
import { Table } from "../../../deps/jsr.io/@cliffy/table/1.0.0/mod.js";
|
|
6
6
|
import { bold } from "../../../lib/cli.js";
|
|
7
7
|
import { createOutput } from "../../../lib/output.js";
|
package/esm/src/cli/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAQD,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,IAElD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAO,GAAG,SAEnD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,OAAO,EAExC,CAAC;AAQF,eAAO,MAAM,QAAQ,QAAO,
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAQD,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,IAElD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAO,GAAG,SAEnD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,OAAO,EAExC,CAAC;AAQF,eAAO,MAAM,QAAQ,QAAO,IA6B3B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAC;AAMF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAuCzD,CAAC"}
|
package/esm/src/cli/mod.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// CLI Framework - Command Parser and Router
|
|
3
3
|
// =============================================================================
|
|
4
4
|
import * as dntShim from "../../_dnt.shims.js";
|
|
5
|
-
import { parseArgs } from "../../deps/jsr.io/@std/cli/1.0.
|
|
5
|
+
import { parseArgs } from "../../deps/jsr.io/@std/cli/1.0.28/mod.js";
|
|
6
6
|
import { bold, dim } from "../../lib/cli.js";
|
|
7
7
|
import denoConfig from "../../deno.js";
|
|
8
8
|
// =============================================================================
|
|
@@ -34,7 +34,7 @@ ${bold("COMMANDS")}
|
|
|
34
34
|
deploy Deploy an app safely on a custom private network
|
|
35
35
|
list List all discovered routers across networks
|
|
36
36
|
status Show router status, network, and tailnet info
|
|
37
|
-
destroy
|
|
37
|
+
destroy Destroy a network (router) or a workload app
|
|
38
38
|
doctor Check that Tailscale and the router are working correctly
|
|
39
39
|
|
|
40
40
|
${bold("OPTIONS")}
|
|
@@ -45,7 +45,8 @@ ${bold("EXAMPLES")}
|
|
|
45
45
|
ambit create browsers
|
|
46
46
|
ambit list
|
|
47
47
|
ambit status --network browsers
|
|
48
|
-
ambit destroy
|
|
48
|
+
ambit destroy network browsers
|
|
49
|
+
ambit destroy app my-app.browsers
|
|
49
50
|
ambit doctor
|
|
50
51
|
|
|
51
52
|
${dim("Run 'ambit <command> --help' for command-specific help.")}
|
package/esm/src/discovery.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface RouterApp {
|
|
|
6
6
|
appName: string;
|
|
7
7
|
network: string;
|
|
8
8
|
org: string;
|
|
9
|
+
routerId: string;
|
|
9
10
|
}
|
|
10
11
|
/** Machine state for a router, from the Fly Machines API. */
|
|
11
12
|
export interface RouterMachineInfo {
|
|
@@ -25,6 +26,18 @@ export interface RouterTailscaleInfo {
|
|
|
25
26
|
export declare const listRouterApps: (fly: FlyProvider, org: string) => Promise<RouterApp[]>;
|
|
26
27
|
/** Find the router app for a specific network. */
|
|
27
28
|
export declare const findRouterApp: (fly: FlyProvider, org: string, network: string) => Promise<RouterApp | null>;
|
|
29
|
+
/** A workload (non-router) app discovered from the Fly REST API. */
|
|
30
|
+
export interface WorkloadApp {
|
|
31
|
+
appName: string;
|
|
32
|
+
network: string;
|
|
33
|
+
org: string;
|
|
34
|
+
}
|
|
35
|
+
/** List all non-router apps on a specific custom network in an org. */
|
|
36
|
+
export declare const listWorkloadAppsOnNetwork: (fly: FlyProvider, org: string, network: string) => Promise<WorkloadApp[]>;
|
|
37
|
+
/** Find a specific workload app by logical name, optionally scoped to a network.
|
|
38
|
+
* When a network is provided, resolves the router-suffixed Fly app name
|
|
39
|
+
* (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
|
|
40
|
+
export declare const findWorkloadApp: (fly: FlyProvider, org: string, appName: string, network?: string) => Promise<WorkloadApp | null>;
|
|
28
41
|
/** Get machine info for a router app. Returns null if no machines exist. */
|
|
29
42
|
export declare const getRouterMachineInfo: (fly: FlyProvider, appName: string) => Promise<RouterMachineInfo | null>;
|
|
30
43
|
/** Get tailscale device info for a router. Returns null if not found. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/src/discovery.ts"],"names":[],"mappings":"AAcA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/src/discovery.ts"],"names":[],"mappings":"AAcA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAOlE,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAaD,wDAAwD;AACxD,eAAO,MAAM,cAAc,GACzB,KAAK,WAAW,EAChB,KAAK,MAAM,KACV,OAAO,CAAC,SAAS,EAAE,CAerB,CAAC;AAEF,kDAAkD;AAClD,eAAO,MAAM,aAAa,GACxB,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,SAAS,GAAG,IAAI,CAG1B,CAAC;AAMF,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,GACpC,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,KACd,OAAO,CAAC,WAAW,EAAE,CAcvB,CAAC;AAEF;;+EAE+E;AAC/E,eAAO,MAAM,eAAe,GAC1B,KAAK,WAAW,EAChB,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAU,MAAM,KACf,OAAO,CAAC,WAAW,GAAG,IAAI,CA4B5B,CAAC;AAMF,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,GAC/B,KAAK,WAAW,EAChB,SAAS,MAAM,KACd,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAclC,CAAC;AAMF,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GACjC,WAAW,iBAAiB,EAC5B,SAAS,MAAM,KACd,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAcpC,CAAC"}
|
package/esm/src/discovery.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// =============================================================================
|
|
15
15
|
import "../_dnt.polyfills.js";
|
|
16
|
+
import { getRouterSuffix, getWorkloadAppName } from "./providers/fly.js";
|
|
16
17
|
import { extractSubnet } from "./schemas/config.js";
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// Constants
|
|
@@ -32,6 +33,7 @@ export const listRouterApps = async (fly, org) => {
|
|
|
32
33
|
appName: app.name,
|
|
33
34
|
network: app.network,
|
|
34
35
|
org: app.organization?.slug ?? org,
|
|
36
|
+
routerId: getRouterSuffix(app.name, app.network),
|
|
35
37
|
}));
|
|
36
38
|
};
|
|
37
39
|
/** Find the router app for a specific network. */
|
|
@@ -39,6 +41,46 @@ export const findRouterApp = async (fly, org, network) => {
|
|
|
39
41
|
const apps = await listRouterApps(fly, org);
|
|
40
42
|
return apps.find((a) => a.network === network) ?? null;
|
|
41
43
|
};
|
|
44
|
+
/** List all non-router apps on a specific custom network in an org. */
|
|
45
|
+
export const listWorkloadAppsOnNetwork = async (fly, org, network) => {
|
|
46
|
+
const apps = await fly.listAppsWithNetwork(org);
|
|
47
|
+
return apps
|
|
48
|
+
.filter((app) => !app.name.startsWith(ROUTER_APP_PREFIX) &&
|
|
49
|
+
app.network === network)
|
|
50
|
+
.map((app) => ({
|
|
51
|
+
appName: app.name,
|
|
52
|
+
network: app.network,
|
|
53
|
+
org: app.organization?.slug ?? org,
|
|
54
|
+
}));
|
|
55
|
+
};
|
|
56
|
+
/** Find a specific workload app by logical name, optionally scoped to a network.
|
|
57
|
+
* When a network is provided, resolves the router-suffixed Fly app name
|
|
58
|
+
* (e.g. "thing" on network "lab" with routerId "abc123" → "thing-abc123"). */
|
|
59
|
+
export const findWorkloadApp = async (fly, org, appName, network) => {
|
|
60
|
+
const apps = await fly.listAppsWithNetwork(org);
|
|
61
|
+
const workloads = apps
|
|
62
|
+
.filter((app) => !app.name.startsWith(ROUTER_APP_PREFIX) &&
|
|
63
|
+
app.network !== DEFAULT_NETWORK)
|
|
64
|
+
.map((app) => ({
|
|
65
|
+
appName: app.name,
|
|
66
|
+
network: app.network,
|
|
67
|
+
org: app.organization?.slug ?? org,
|
|
68
|
+
}));
|
|
69
|
+
if (network) {
|
|
70
|
+
// Resolve the router's suffix so we can match the suffixed Fly app name
|
|
71
|
+
const router = await findRouterApp(fly, org, network);
|
|
72
|
+
if (router) {
|
|
73
|
+
const suffixedName = getWorkloadAppName(appName, router.routerId);
|
|
74
|
+
const found = workloads.find((a) => a.appName === suffixedName && a.network === network);
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
// Fallback: try exact match (for pre-suffix apps or direct Fly name)
|
|
79
|
+
return workloads.find((a) => a.appName === appName && a.network === network) ?? null;
|
|
80
|
+
}
|
|
81
|
+
// Without network, try exact match only
|
|
82
|
+
return workloads.find((a) => a.appName === appName) ?? null;
|
|
83
|
+
};
|
|
42
84
|
// =============================================================================
|
|
43
85
|
// 2. What's the machine state? (Fly Machines API)
|
|
44
86
|
// =============================================================================
|
|
@@ -5,8 +5,10 @@ set -e
|
|
|
5
5
|
# ambit - Self-Configuring Tailscale Subnet Router
|
|
6
6
|
# =============================================================================
|
|
7
7
|
# State is persisted to /var/lib/tailscale via Fly volume.
|
|
8
|
-
# On first run:
|
|
8
|
+
# On first run: authenticates with a pre-minted auth key, advertises routes.
|
|
9
9
|
# On restart: reuses existing state, no new device created.
|
|
10
|
+
# The router never receives the user's API token — only a single-use,
|
|
11
|
+
# tag-scoped auth key that expires after 5 minutes.
|
|
10
12
|
# =============================================================================
|
|
11
13
|
|
|
12
14
|
echo "Router: Enabling IP Forwarding"
|
|
@@ -33,42 +35,9 @@ if /usr/local/bin/tailscale status --json 2>/dev/null | jq -e '.BackendState ==
|
|
|
33
35
|
--hostname="${FLY_APP_NAME:-ambit}" \
|
|
34
36
|
--advertise-routes="${SUBNET}"
|
|
35
37
|
else
|
|
36
|
-
# First run -
|
|
37
|
-
if [ -
|
|
38
|
-
echo "Router:
|
|
39
|
-
|
|
40
|
-
TAGS_JSON="[]"
|
|
41
|
-
if [ -n "${TAILSCALE_TAGS}" ]; then
|
|
42
|
-
TAGS_JSON=$(echo "${TAILSCALE_TAGS}" | jq -R 'split(",")')
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
AUTH_KEY_RESPONSE=$(curl -s -X POST \
|
|
46
|
-
-u "${TAILSCALE_API_TOKEN}:" \
|
|
47
|
-
-H "Content-Type: application/json" \
|
|
48
|
-
-d "$(jq -n \
|
|
49
|
-
--argjson tags "${TAGS_JSON}" \
|
|
50
|
-
'{
|
|
51
|
-
capabilities: { devices: { create: {
|
|
52
|
-
reusable: false,
|
|
53
|
-
ephemeral: false,
|
|
54
|
-
preauthorized: true,
|
|
55
|
-
tags: $tags
|
|
56
|
-
}}},
|
|
57
|
-
expirySeconds: 300
|
|
58
|
-
}' | jq 'if .capabilities.devices.create.tags == [] then .capabilities.devices.create |= del(.tags) else . end')" \
|
|
59
|
-
"https://api.tailscale.com/api/v2/tailnet/-/keys")
|
|
60
|
-
|
|
61
|
-
TAILSCALE_AUTHKEY=$(echo "${AUTH_KEY_RESPONSE}" | jq -r '.key')
|
|
62
|
-
|
|
63
|
-
if [ -z "${TAILSCALE_AUTHKEY}" ] || [ "${TAILSCALE_AUTHKEY}" = "null" ]; then
|
|
64
|
-
echo "Router: ERROR - Failed to Create Auth Key"
|
|
65
|
-
echo "${AUTH_KEY_RESPONSE}"
|
|
66
|
-
exit 1
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
echo "Router: Auth Key Created"
|
|
70
|
-
elif [ -z "${TAILSCALE_AUTHKEY}" ]; then
|
|
71
|
-
echo "Router: ERROR - No TAILSCALE_API_TOKEN or TAILSCALE_AUTHKEY Provided"
|
|
38
|
+
# First run - authenticate with pre-minted auth key
|
|
39
|
+
if [ -z "${TAILSCALE_AUTHKEY}" ]; then
|
|
40
|
+
echo "Router: ERROR - No TAILSCALE_AUTHKEY Provided"
|
|
72
41
|
exit 1
|
|
73
42
|
fi
|
|
74
43
|
|
|
@@ -79,40 +48,6 @@ else
|
|
|
79
48
|
--advertise-routes="${SUBNET}"
|
|
80
49
|
fi
|
|
81
50
|
|
|
82
|
-
echo "Router: Getting Node Key"
|
|
83
|
-
NODE_KEY=$(/usr/local/bin/tailscale status --json | jq -r '.Self.PublicKey')
|
|
84
|
-
echo "Router: Node Key ${NODE_KEY}"
|
|
85
|
-
|
|
86
|
-
# Self-approve routes if we have API access
|
|
87
|
-
# This is a fallback — if the user has autoApprovers configured in their
|
|
88
|
-
# Tailscale policy file, routes are approved automatically and this block
|
|
89
|
-
# is a no-op (routes are already enabled).
|
|
90
|
-
if [ -n "${TAILSCALE_API_TOKEN}" ]; then
|
|
91
|
-
echo "Router: Finding Device ID"
|
|
92
|
-
|
|
93
|
-
DEVICES_RESPONSE=$(curl -s \
|
|
94
|
-
-u "${TAILSCALE_API_TOKEN}:" \
|
|
95
|
-
"https://api.tailscale.com/api/v2/tailnet/-/devices")
|
|
96
|
-
|
|
97
|
-
DEVICE_ID=$(echo "${DEVICES_RESPONSE}" | jq -r ".devices[] | select(.nodeKey == \"${NODE_KEY}\") | .id")
|
|
98
|
-
|
|
99
|
-
if [ -n "${DEVICE_ID}" ] && [ "${DEVICE_ID}" != "null" ]; then
|
|
100
|
-
echo "Router: Device ID ${DEVICE_ID}"
|
|
101
|
-
echo "Router: Approving Subnet Routes"
|
|
102
|
-
|
|
103
|
-
curl -s -X POST \
|
|
104
|
-
-u "${TAILSCALE_API_TOKEN}:" \
|
|
105
|
-
-H "Content-Type: application/json" \
|
|
106
|
-
-d "{\"routes\": [\"${SUBNET}\"]}" \
|
|
107
|
-
"https://api.tailscale.com/api/v2/device/${DEVICE_ID}/routes" > /dev/null
|
|
108
|
-
|
|
109
|
-
echo "Router: Routes Approved"
|
|
110
|
-
else
|
|
111
|
-
echo "Router: WARNING - Could Not Find Device ID"
|
|
112
|
-
echo "Router: Routes May Need Manual Approval"
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
51
|
echo "Router: Fully Configured"
|
|
117
52
|
|
|
118
53
|
# Start SOCKS5 proxy for bidirectional tailnet access
|
|
@@ -124,10 +59,21 @@ echo "Router: Starting DNS Proxy"
|
|
|
124
59
|
|
|
125
60
|
# Generate Corefile for CoreDNS
|
|
126
61
|
# Rewrites NETWORK_NAME TLD to .flycast before forwarding to Fly DNS.
|
|
62
|
+
# When ROUTER_ID is set, workload app names are suffixed: app.network ->
|
|
63
|
+
# app-ROUTER_ID.flycast. This ties workloads to their router and avoids
|
|
64
|
+
# name collisions across networks.
|
|
127
65
|
# .flycast resolves to the Flycast address (private_v6) which routes through
|
|
128
66
|
# Fly Proxy — enabling autostart/autostop and load balancing. The Flycast IP
|
|
129
67
|
# is within the network's /48 subnet so it's routable through the tailnet.
|
|
130
|
-
if [ -n "${NETWORK_NAME}" ]; then
|
|
68
|
+
if [ -n "${NETWORK_NAME}" ] && [ -n "${ROUTER_ID}" ]; then
|
|
69
|
+
echo "Router: DNS Rewrite *.${NETWORK_NAME} -> *-${ROUTER_ID}.flycast"
|
|
70
|
+
cat > /etc/coredns/Corefile <<EOF
|
|
71
|
+
.:53 {
|
|
72
|
+
rewrite name regex (.+)\.${NETWORK_NAME}\. {1}-${ROUTER_ID}.flycast. answer auto
|
|
73
|
+
forward . fdaa::3
|
|
74
|
+
}
|
|
75
|
+
EOF
|
|
76
|
+
elif [ -n "${NETWORK_NAME}" ]; then
|
|
131
77
|
echo "Router: DNS Rewrite ${NETWORK_NAME} -> flycast"
|
|
132
78
|
cat > /etc/coredns/Corefile <<EOF
|
|
133
79
|
.:53 {
|
|
@@ -32,6 +32,7 @@ export interface SafeDeployOptions {
|
|
|
32
32
|
image?: string;
|
|
33
33
|
config?: string;
|
|
34
34
|
region?: string;
|
|
35
|
+
routerId?: string;
|
|
35
36
|
}
|
|
36
37
|
export interface FlyProvider {
|
|
37
38
|
ensureInstalled(): Promise<void>;
|
|
@@ -41,6 +42,7 @@ export interface FlyProvider {
|
|
|
41
42
|
listOrgs(): Promise<Record<string, string>>;
|
|
42
43
|
createApp(name: string, org: string, options?: {
|
|
43
44
|
network?: string;
|
|
45
|
+
routerId?: string;
|
|
44
46
|
}): Promise<void>;
|
|
45
47
|
deleteApp(name: string): Promise<void>;
|
|
46
48
|
listApps(org?: string): Promise<FlyApp[]>;
|
|
@@ -67,4 +69,8 @@ export interface FlyProvider {
|
|
|
67
69
|
}
|
|
68
70
|
export declare const createFlyProvider: () => FlyProvider;
|
|
69
71
|
export declare const getRouterAppName: (network: string, randomSuffix: string) => string;
|
|
72
|
+
/** Extract the routerId suffix from a router app name. */
|
|
73
|
+
export declare const getRouterSuffix: (routerAppName: string, network: string) => string;
|
|
74
|
+
/** Build the physical Fly app name for a workload. */
|
|
75
|
+
export declare const getWorkloadAppName: (name: string, routerId: string) => string;
|
|
70
76
|
//# sourceMappingURL=fly.d.ts.map
|