@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.
Files changed (49) hide show
  1. package/esm/deno.d.ts +52 -0
  2. package/esm/deno.d.ts.map +1 -0
  3. package/esm/deno.js +50 -0
  4. package/esm/deps/jsr.io/@std/cli/1.0.28/_data.d.ts.map +1 -0
  5. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.d.ts.map +1 -1
  6. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.d.ts.map +1 -1
  7. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.d.ts.map +1 -1
  8. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.js +17 -3
  9. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.d.ts.map +1 -1
  10. package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/unicode_width.d.ts.map +1 -1
  11. package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.d.ts +2 -2
  12. package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.d.ts.map +1 -1
  13. package/esm/deps/jsr.io/@std/collections/{1.1.3 → 1.1.6}/deep_merge.js +30 -20
  14. package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.js +1 -1
  15. package/esm/lib/result.d.ts +8 -0
  16. package/esm/lib/result.d.ts.map +1 -0
  17. package/esm/lib/result.js +1 -0
  18. package/esm/main.d.ts.map +1 -1
  19. package/esm/main.js +3 -2
  20. package/esm/src/cli/commands/create.js +1 -1
  21. package/esm/src/cli/commands/deploy.js +138 -23
  22. package/esm/src/cli/commands/destroy.js +224 -29
  23. package/esm/src/cli/commands/doctor.js +1 -1
  24. package/esm/src/cli/commands/list.js +1 -1
  25. package/esm/src/cli/commands/status.js +1 -1
  26. package/esm/src/cli/mod.d.ts.map +1 -1
  27. package/esm/src/cli/mod.js +6 -4
  28. package/esm/src/discovery.d.ts +10 -0
  29. package/esm/src/discovery.d.ts.map +1 -1
  30. package/esm/src/discovery.js +28 -0
  31. package/esm/src/template.d.ts +40 -0
  32. package/esm/src/template.d.ts.map +1 -0
  33. package/esm/src/template.js +167 -0
  34. package/package.json +2 -2
  35. package/esm/deps/jsr.io/@std/cli/1.0.27/_data.d.ts.map +0 -1
  36. package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.d.ts +0 -6
  37. package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.d.ts.map +0 -1
  38. package/esm/deps/jsr.io/@std/collections/1.1.3/_utils.js +0 -18
  39. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_data.d.ts +0 -0
  40. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_data.js +0 -0
  41. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.d.ts +0 -0
  42. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/_run_length.js +0 -0
  43. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.d.ts +0 -0
  44. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/mod.js +0 -0
  45. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/parse_args.d.ts +0 -0
  46. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.d.ts +0 -0
  47. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/prompt_secret.js +0 -0
  48. /package/esm/deps/jsr.io/@std/cli/{1.0.27 → 1.0.28}/unicode_width.d.ts +0 -0
  49. /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.27/mod.js";
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: ["network", "org", "region", "image", "config", "main-port"],
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 <app> --network <name> Uses ./fly.toml
113
- ambit deploy <app> --network <name> --config path Explicit fly.toml
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 <app> --network <name> --image <img> Docker image, no toml
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> Custom 6PN network to target (required)
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 --network browsers
135
- ambit deploy my-app --network browsers --image registry/img:latest
136
- ambit deploy my-app --network browsers --config ./fly.toml --region sea
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("App Name Required. Usage: ambit deploy <app> --network <name>");
238
+ return out.die("Missing app name. Usage: ambit deploy <app>.<network>");
147
239
  }
148
- const app = appArg;
149
- if (!args.network) {
150
- return out.die("--network Is Required");
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
- if (args.image && args.config) {
159
- return out.die("--image and --config Are Mutually Exclusive");
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
- const deployConfig = args.image
218
- ? resolveImageMode(args.image, String(args["main-port"]), out)
219
- : await resolveConfigMode(args.config, out);
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 - Tear Down Router and Clean Up
2
+ // Destroy Command - Destroy Networks or Apps
3
3
  // =============================================================================
4
- import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.27/mod.js";
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
- // Destroy Command
16
+ // Top-Level Help
15
17
  // =============================================================================
16
- const destroy = async (argv) => {
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 --network <name> [--org <org>] [--yes] [--json]
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
- if (!args.network) {
39
- return out.die("--network Is Required");
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, args.network);
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 '${args.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 { /* device may not exist */ }
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: ${app.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(app.network);
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({ destroyed: true, appName: app.appName });
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: "Tear down the router, clean up DNS and tailnet device",
143
- usage: "ambit destroy --network <name> [--org <org>] [--yes] [--json]",
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.27/mod.js";
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.27/mod.js";
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.27/mod.js";
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 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAaA,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,IA4B3B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAC;AAMF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAuCzD,CAAC"}
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"}
@@ -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.27/mod.js";
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 = "0.3.0";
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 Tear down the router, clean up DNS and tailnet device
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 --network browsers
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.")}