@cardelli/ambit 0.1.2 → 0.1.4
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 +52 -0
- package/esm/deno.d.ts.map +1 -0
- package/esm/deno.js +50 -0
- 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/lib/result.d.ts +8 -0
- package/esm/lib/result.d.ts.map +1 -0
- package/esm/lib/result.js +1 -0
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +3 -2
- package/esm/src/cli/commands/create.js +1 -1
- package/esm/src/cli/commands/deploy.js +138 -23
- package/esm/src/cli/commands/destroy.js +224 -29
- package/esm/src/cli/commands/doctor.js +1 -1
- 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 +6 -4
- package/esm/src/discovery.d.ts +10 -0
- package/esm/src/discovery.d.ts.map +1 -1
- package/esm/src/discovery.js +28 -0
- package/esm/src/template.d.ts +40 -0
- package/esm/src/template.d.ts.map +1 -0
- package/esm/src/template.js +167 -0
- package/package.json +2 -2
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Deploy Command - Safely Deploy a Workload App on a Custom Private Network
|
|
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 { join } from "../../../deps/jsr.io/@std/path/1.1.4/mod.js";
|
|
7
7
|
import { bold, confirm, fileExists } from "../../../lib/cli.js";
|
|
8
8
|
import { createOutput } from "../../../lib/output.js";
|
|
@@ -11,6 +11,7 @@ import { createFlyProvider, FlyDeployError } from "../../providers/fly.js";
|
|
|
11
11
|
import { findRouterApp } from "../../discovery.js";
|
|
12
12
|
import { resolveOrg } from "../../resolve.js";
|
|
13
13
|
import { assertNotRouter, auditDeploy, scanFlyToml } from "../../guard.js";
|
|
14
|
+
import { fetchTemplate, parseTemplateRef } from "../../template.js";
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// Image Mode
|
|
16
17
|
// =============================================================================
|
|
@@ -91,11 +92,79 @@ const resolveConfigMode = async (explicitConfig, out) => {
|
|
|
91
92
|
};
|
|
92
93
|
};
|
|
93
94
|
// =============================================================================
|
|
95
|
+
// Template Mode
|
|
96
|
+
// =============================================================================
|
|
97
|
+
/** Resolve deploy config for template mode (--template). */
|
|
98
|
+
const resolveTemplateMode = async (templateRaw, out) => {
|
|
99
|
+
const ref = parseTemplateRef(templateRaw);
|
|
100
|
+
if (!ref) {
|
|
101
|
+
out.die(`Invalid Template Reference: "${templateRaw}". ` +
|
|
102
|
+
`Format: owner/repo[/path][@ref]`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const label = (ref.path === "."
|
|
106
|
+
? `${ref.owner}/${ref.repo}`
|
|
107
|
+
: `${ref.owner}/${ref.repo}/${ref.path}`) +
|
|
108
|
+
(ref.ref ? `@${ref.ref}` : "");
|
|
109
|
+
out.info(`Template: ${label}`);
|
|
110
|
+
const fetchSpinner = out.spinner("Fetching Template from GitHub");
|
|
111
|
+
const result = await fetchTemplate(ref);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
fetchSpinner.fail("Template Fetch Failed");
|
|
114
|
+
out.die(result.message);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
fetchSpinner.success("Template Fetched");
|
|
118
|
+
// Find and scan the template's fly.toml
|
|
119
|
+
const configPath = join(result.templateDir, "fly.toml");
|
|
120
|
+
let tomlContent;
|
|
121
|
+
try {
|
|
122
|
+
tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
try {
|
|
126
|
+
dntShim.Deno.removeSync(result.tempDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore */ }
|
|
129
|
+
out.die(`Template '${ref.path === "." ? ref.repo : ref.path}' Has No fly.toml`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const scan = scanFlyToml(tomlContent);
|
|
133
|
+
if (scan.errors.length > 0) {
|
|
134
|
+
try {
|
|
135
|
+
dntShim.Deno.removeSync(result.tempDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
for (const err of scan.errors) {
|
|
139
|
+
out.err(err);
|
|
140
|
+
}
|
|
141
|
+
out.die("Pre-flight Check Failed for Template fly.toml");
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
for (const warn of scan.warnings) {
|
|
145
|
+
out.warn(warn);
|
|
146
|
+
}
|
|
147
|
+
out.ok(`Scanned ${ref.path === "." ? "" : ref.path + "/"}fly.toml`);
|
|
148
|
+
return {
|
|
149
|
+
configPath,
|
|
150
|
+
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
151
|
+
tempDir: result.tempDir,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
// =============================================================================
|
|
94
155
|
// Deploy Command
|
|
95
156
|
// =============================================================================
|
|
96
157
|
const deploy = async (argv) => {
|
|
97
158
|
const args = parseArgs(argv, {
|
|
98
|
-
string: [
|
|
159
|
+
string: [
|
|
160
|
+
"network",
|
|
161
|
+
"org",
|
|
162
|
+
"region",
|
|
163
|
+
"image",
|
|
164
|
+
"config",
|
|
165
|
+
"main-port",
|
|
166
|
+
"template",
|
|
167
|
+
],
|
|
99
168
|
boolean: ["help", "yes", "json"],
|
|
100
169
|
alias: { y: "yes" },
|
|
101
170
|
default: { "main-port": "80" },
|
|
@@ -105,35 +174,58 @@ const deploy = async (argv) => {
|
|
|
105
174
|
${bold("ambit deploy")} - Deploy an App Safely on a Custom Private Network
|
|
106
175
|
|
|
107
176
|
${bold("USAGE")}
|
|
177
|
+
ambit deploy <app>.<network> [options]
|
|
108
178
|
ambit deploy <app> --network <name> [options]
|
|
109
179
|
|
|
180
|
+
The network can be specified as part of the name (app.network) or with --network.
|
|
181
|
+
|
|
110
182
|
${bold("MODES")}
|
|
111
183
|
Config mode (default):
|
|
112
|
-
ambit deploy
|
|
113
|
-
ambit deploy
|
|
184
|
+
ambit deploy my-app.lab Uses ./fly.toml
|
|
185
|
+
ambit deploy my-app.lab --config path Explicit fly.toml
|
|
114
186
|
|
|
115
187
|
Image mode:
|
|
116
|
-
ambit deploy
|
|
188
|
+
ambit deploy my-app.lab --image <img> Docker image, no toml
|
|
189
|
+
|
|
190
|
+
Template mode:
|
|
191
|
+
ambit deploy my-app.lab --template <ref> GitHub template
|
|
117
192
|
|
|
118
193
|
${bold("OPTIONS")}
|
|
119
|
-
--network <name>
|
|
194
|
+
--network <name> Target network
|
|
120
195
|
--org <org> Fly.io organization slug
|
|
121
196
|
--region <region> Primary deployment region
|
|
122
|
-
--image <img> Docker image (mutually exclusive with --config)
|
|
123
|
-
--config <path> fly.toml path (mutually exclusive with --image)
|
|
124
|
-
--main-port <port> Internal port for HTTP service in image mode (default: 80, "none" to skip)
|
|
125
197
|
-y, --yes Skip confirmation prompts
|
|
126
198
|
--json Output as JSON
|
|
127
199
|
|
|
200
|
+
${bold("CONFIG MODE")} (default)
|
|
201
|
+
--config <path> Explicit fly.toml path (auto-detects ./fly.toml if omitted)
|
|
202
|
+
|
|
203
|
+
${bold("IMAGE MODE")}
|
|
204
|
+
--image <img> Docker image to deploy (no fly.toml needed)
|
|
205
|
+
--main-port <port> Internal port for HTTP service (default: 80, "none" to skip)
|
|
206
|
+
|
|
207
|
+
${bold("TEMPLATE MODE")}
|
|
208
|
+
--template <ref> GitHub template as owner/repo[/path][@ref]
|
|
209
|
+
|
|
210
|
+
Reference format:
|
|
211
|
+
owner/repo Fetch repo root from the default branch
|
|
212
|
+
owner/repo/path Fetch subdirectory from the default branch
|
|
213
|
+
owner/repo/path@tag Fetch a tagged release
|
|
214
|
+
owner/repo/path@branch Fetch a specific branch
|
|
215
|
+
owner/repo/path@commit Fetch a specific commit
|
|
216
|
+
|
|
128
217
|
${bold("SAFETY")}
|
|
129
218
|
Always deploys with --no-public-ips and --flycast.
|
|
130
219
|
Post-deploy audit releases any public IPs and verifies Flycast allocation.
|
|
131
220
|
Pre-flight scan rejects fly.toml with force_https or TLS on 443.
|
|
132
221
|
|
|
133
222
|
${bold("EXAMPLES")}
|
|
134
|
-
ambit deploy my-app
|
|
135
|
-
ambit deploy my-app --
|
|
136
|
-
ambit deploy my-app --
|
|
223
|
+
ambit deploy my-app.lab
|
|
224
|
+
ambit deploy my-app.lab --image registry/img:latest
|
|
225
|
+
ambit deploy my-app.lab --config ./fly.toml --region sea
|
|
226
|
+
ambit deploy my-claw.lab --template ToxicPine/ambit-openclaw
|
|
227
|
+
ambit deploy my-browser.lab --template ToxicPine/ambit-templates/chromatic
|
|
228
|
+
ambit deploy my-browser --network lab --template ToxicPine/ambit-templates/chromatic@v1.0
|
|
137
229
|
`);
|
|
138
230
|
return;
|
|
139
231
|
}
|
|
@@ -143,11 +235,27 @@ ${bold("EXAMPLES")}
|
|
|
143
235
|
// ==========================================================================
|
|
144
236
|
const appArg = args._[0];
|
|
145
237
|
if (!appArg || typeof appArg !== "string") {
|
|
146
|
-
return out.die("
|
|
238
|
+
return out.die("Missing app name. Usage: ambit deploy <app>.<network>");
|
|
147
239
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
240
|
+
let app;
|
|
241
|
+
let network;
|
|
242
|
+
if (appArg.includes(".")) {
|
|
243
|
+
const parts = appArg.split(".");
|
|
244
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
245
|
+
return out.die(`'${appArg}' should have exactly one dot, like my-app.my-network`);
|
|
246
|
+
}
|
|
247
|
+
if (args.network) {
|
|
248
|
+
return out.die(`Network is already part of the name ('${appArg}'), --network is not needed`);
|
|
249
|
+
}
|
|
250
|
+
app = parts[0];
|
|
251
|
+
network = parts[1];
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
app = appArg;
|
|
255
|
+
if (!args.network) {
|
|
256
|
+
return out.die(`Missing network. Use: ambit deploy ${app}.<network>`);
|
|
257
|
+
}
|
|
258
|
+
network = args.network;
|
|
151
259
|
}
|
|
152
260
|
try {
|
|
153
261
|
assertNotRouter(app);
|
|
@@ -155,10 +263,10 @@ ${bold("EXAMPLES")}
|
|
|
155
263
|
catch (e) {
|
|
156
264
|
return out.die(e instanceof Error ? e.message : String(e));
|
|
157
265
|
}
|
|
158
|
-
|
|
159
|
-
|
|
266
|
+
const modeFlags = [args.image, args.config, args.template].filter(Boolean);
|
|
267
|
+
if (modeFlags.length > 1) {
|
|
268
|
+
return out.die("--image, --config, and --template Are Mutually Exclusive");
|
|
160
269
|
}
|
|
161
|
-
const network = args.network;
|
|
162
270
|
out.blank()
|
|
163
271
|
.header("=".repeat(50))
|
|
164
272
|
.header(` ambit Deploy: ${app}`)
|
|
@@ -214,9 +322,16 @@ ${bold("EXAMPLES")}
|
|
|
214
322
|
// Phase 4: Resolve Deploy Mode
|
|
215
323
|
// ==========================================================================
|
|
216
324
|
out.header("Step 4: Pre-flight Check").blank();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
325
|
+
let deployConfig;
|
|
326
|
+
if (args.template) {
|
|
327
|
+
deployConfig = await resolveTemplateMode(args.template, out);
|
|
328
|
+
}
|
|
329
|
+
else if (args.image) {
|
|
330
|
+
deployConfig = resolveImageMode(args.image, String(args["main-port"]), out);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
deployConfig = await resolveConfigMode(args.config, out);
|
|
334
|
+
}
|
|
220
335
|
if (!deployConfig)
|
|
221
336
|
return; // mode resolver already called out.die()
|
|
222
337
|
out.blank();
|
|
@@ -306,6 +421,6 @@ ${bold("EXAMPLES")}
|
|
|
306
421
|
registerCommand({
|
|
307
422
|
name: "deploy",
|
|
308
423
|
description: "Deploy an app safely on a custom private network",
|
|
309
|
-
usage: "ambit deploy <app> --network <name> [--image <img>] [--org <org>] [--region <region>]",
|
|
424
|
+
usage: "ambit deploy <app> --network <name> [--image <img>] [--template <ref>] [--org <org>] [--region <region>]",
|
|
310
425
|
run: deploy,
|
|
311
426
|
});
|
|
@@ -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(app);
|
|
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: app, 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";
|
|
@@ -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":"
|
|
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,8 +2,9 @@
|
|
|
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
|
+
import denoConfig from "../../deno.js";
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Commands Registry
|
|
9
10
|
// =============================================================================
|
|
@@ -20,7 +21,7 @@ export const getAllCommands = () => {
|
|
|
20
21
|
// =============================================================================
|
|
21
22
|
// Help Text
|
|
22
23
|
// =============================================================================
|
|
23
|
-
const VERSION =
|
|
24
|
+
const VERSION = denoConfig.version;
|
|
24
25
|
export const showHelp = () => {
|
|
25
26
|
console.log(`
|
|
26
27
|
${bold("ambit")} - Tailscale Subnet Router for Fly.io Custom Networks
|
|
@@ -33,7 +34,7 @@ ${bold("COMMANDS")}
|
|
|
33
34
|
deploy Deploy an app safely on a custom private network
|
|
34
35
|
list List all discovered routers across networks
|
|
35
36
|
status Show router status, network, and tailnet info
|
|
36
|
-
destroy
|
|
37
|
+
destroy Destroy a network (router) or a workload app
|
|
37
38
|
doctor Check that Tailscale and the router are working correctly
|
|
38
39
|
|
|
39
40
|
${bold("OPTIONS")}
|
|
@@ -44,7 +45,8 @@ ${bold("EXAMPLES")}
|
|
|
44
45
|
ambit create browsers
|
|
45
46
|
ambit list
|
|
46
47
|
ambit status --network browsers
|
|
47
|
-
ambit destroy
|
|
48
|
+
ambit destroy network browsers
|
|
49
|
+
ambit destroy app my-app.browsers
|
|
48
50
|
ambit doctor
|
|
49
51
|
|
|
50
52
|
${dim("Run 'ambit <command> --help' for command-specific help.")}
|