@cardelli/ambit 0.1.0 → 0.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAKA,OAAO,sBAAsB,CAAC;AAiB9B,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AACvE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAAiC,CAAC;AACrE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAAiC,CAAC;AACrE,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,KAAG,MAAmC,CAAC;AACzE,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,KAAG,MAAoC,CAAC;AAC3E,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AACvE,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AAMvE,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,KAAG,IAE1C,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,KAAG,IAE5C,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,KAAG,IAE3C,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,KAAG,IAE5C,CAAC;AAMF,eAAO,MAAM,GAAG,GAAI,SAAS,MAAM,KAAG,KAGrC,CAAC;AAQF,qBAAa,OAAO;IAClB,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAM;IAErB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAe5B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI7B,IAAI,IAAI,IAAI;IASZ,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAI5B;AAMD,eAAO,MAAM,MAAM,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,CAW5D,CAAC;AAEF,eAAO,MAAM,OAAO,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CAG9D,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,CAIhE,CAAC;AAMF,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,OAAO,CAO9D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAO7C,CAAC;AAMF,eAAO,MAAM,YAAY,QAAO,MAG/B,CAAC;AAEF,eAAO,MAAM,aAAa,QAAO,MAEhC,CAAC;AAEF,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,IAAI,CAOpD,CAAC;AAMF,eAAO,MAAM,QAAQ,GAAI,SAAQ,MAAU,KAAG,MAO7C,CAAC;AAMF,eAAO,MAAM,aAAa,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CAYpE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAKA,OAAO,sBAAsB,CAAC;AAiB9B,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AACvE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAAiC,CAAC;AACrE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,KAAG,MAAiC,CAAC;AACrE,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,KAAG,MAAmC,CAAC;AACzE,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,KAAG,MAAoC,CAAC;AAC3E,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AACvE,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAC;AAMvE,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,KAAG,IAE1C,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,KAAG,IAE5C,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,KAAG,IAE3C,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,KAAG,IAE5C,CAAC;AAMF,eAAO,MAAM,GAAG,GAAI,SAAS,MAAM,KAAG,KAGrC,CAAC;AAQF,qBAAa,OAAO;IAClB,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAM;IAErB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAe5B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI7B,IAAI,IAAI,IAAI;IASZ,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAI5B;AAMD,eAAO,MAAM,MAAM,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,CAW5D,CAAC;AAEF,eAAO,MAAM,OAAO,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CAG9D,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,CAIhE,CAAC;AAMF,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,OAAO,CAO9D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAO7C,CAAC;AAMF,eAAO,MAAM,YAAY,QAAO,MAG/B,CAAC;AAEF,eAAO,MAAM,aAAa,QAAO,MAEhC,CAAC;AAEF,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,IAAI,CAOpD,CAAC;AAMF,eAAO,MAAM,QAAQ,GAAI,SAAQ,MAAU,KAAG,MAO7C,CAAC;AAMF,eAAO,MAAM,aAAa,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CASpE,CAAC"}
package/esm/lib/cli.js CHANGED
@@ -161,16 +161,12 @@ export const randomId = (length = 6) => {
161
161
  // Command Exists Check
162
162
  // =============================================================================
163
163
  export const commandExists = async (command) => {
164
- try {
165
- const cmd = new dntShim.Deno.Command("which", {
166
- args: [command],
167
- stdout: "null",
168
- stderr: "null",
164
+ const { spawn } = await import("node:child_process");
165
+ return new Promise((resolve) => {
166
+ const child = spawn("which", [command], {
167
+ stdio: "ignore",
169
168
  });
170
- const { success } = await cmd.output();
171
- return success;
172
- }
173
- catch {
174
- return false;
175
- }
169
+ child.on("error", () => resolve(false));
170
+ child.on("close", (code) => resolve(code === 0));
171
+ });
176
172
  };
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/lib/command.ts"],"names":[],"mappings":"AAGA,OAAO,sBAAsB,CAAC;AAW9B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;GAEG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;CACtC,KACA,OAAO,CAAC,aAAa,CA8BvB,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EACpC,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAmBxD,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC,aAAa,CAavB,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,QAAQ,GACnB,OAAO,MAAM,EACb,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM9C,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAkB5C,CAAC"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/lib/command.ts"],"names":[],"mappings":"AAGA,OAAO,sBAAsB,CAAC;AAU9B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;GAEG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;CACtC,KACA,OAAO,CAAC,aAAa,CAwCvB,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EACpC,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAmBxD,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC,aAAa,CAavB,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,QAAQ,GACnB,OAAO,MAAM,EACb,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM9C,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,UAAU;IACR,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,KACA,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAkB5C,CAAC"}
@@ -2,7 +2,7 @@
2
2
  // Shell Command Helpers
3
3
  // =============================================================================
4
4
  import "../_dnt.polyfills.js";
5
- import * as dntShim from "../_dnt.shims.js";
5
+ import { spawn } from "node:child_process";
6
6
  import { Spinner } from "./cli.js";
7
7
  // =============================================================================
8
8
  // Run Command
@@ -10,34 +10,41 @@ import { Spinner } from "./cli.js";
10
10
  /**
11
11
  * Run a command and capture output.
12
12
  */
13
- export const runCommand = async (args, options) => {
13
+ export const runCommand = (args, options) => {
14
14
  const [cmd, ...cmdArgs] = args;
15
- try {
16
- const command = new dntShim.Deno.Command(cmd, {
17
- args: cmdArgs,
15
+ return new Promise((resolve) => {
16
+ const child = spawn(cmd, cmdArgs, {
18
17
  cwd: options?.cwd,
19
- env: options?.env,
20
- stdin: options?.stdin ?? "null",
21
- stdout: "piped",
22
- stderr: "piped",
18
+ env: options?.env ? { ...process.env, ...options.env } : undefined,
19
+ stdio: [
20
+ options?.stdin === "inherit" ? "inherit" : "ignore",
21
+ "pipe",
22
+ "pipe",
23
+ ],
23
24
  });
24
- const { code, stdout, stderr } = await command.output();
25
- const decoder = new TextDecoder();
26
- return {
27
- success: code === 0,
28
- code,
29
- stdout: decoder.decode(stdout),
30
- stderr: decoder.decode(stderr),
31
- };
32
- }
33
- catch (error) {
34
- return {
35
- success: false,
36
- code: -1,
37
- stdout: "",
38
- stderr: error instanceof Error ? error.message : String(error),
39
- };
40
- }
25
+ const stdout = [];
26
+ const stderr = [];
27
+ child.stdout.setEncoding("utf8");
28
+ child.stderr.setEncoding("utf8");
29
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
30
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
31
+ child.on("error", (error) => {
32
+ resolve({
33
+ success: false,
34
+ code: -1,
35
+ stdout: "",
36
+ stderr: error.message,
37
+ });
38
+ });
39
+ child.on("close", (code) => {
40
+ resolve({
41
+ success: code === 0,
42
+ code: code ?? 1,
43
+ stdout: stdout.join(""),
44
+ stderr: stderr.join(""),
45
+ });
46
+ });
47
+ });
41
48
  };
42
49
  // =============================================================================
43
50
  // Run Command with JSON Output
@@ -101,21 +108,19 @@ export const runQuiet = async (label, args, options) => {
101
108
  /**
102
109
  * Run a command interactively (inherits stdio).
103
110
  */
104
- export const runInteractive = async (args, options) => {
111
+ export const runInteractive = (args, options) => {
105
112
  const [cmd, ...cmdArgs] = args;
106
- try {
107
- const command = new dntShim.Deno.Command(cmd, {
108
- args: cmdArgs,
113
+ return new Promise((resolve) => {
114
+ const child = spawn(cmd, cmdArgs, {
109
115
  cwd: options?.cwd,
110
- env: options?.env,
111
- stdin: "inherit",
112
- stdout: "inherit",
113
- stderr: "inherit",
116
+ env: options?.env ? { ...process.env, ...options.env } : undefined,
117
+ stdio: "inherit",
114
118
  });
115
- const { code } = await command.output();
116
- return { success: code === 0, code };
117
- }
118
- catch {
119
- return { success: false, code: -1 };
120
- }
119
+ child.on("error", () => {
120
+ resolve({ success: false, code: -1 });
121
+ });
122
+ child.on("close", (code) => {
123
+ resolve({ success: (code ?? 1) === 0, code: code ?? 1 });
124
+ });
125
+ });
121
126
  };
@@ -6,7 +6,8 @@ import { bold, randomId, readSecret } from "../../../lib/cli.js";
6
6
  import { createOutput } from "../../../lib/output.js";
7
7
  import { registerCommand } from "../mod.js";
8
8
  import { extractSubnet, getRouterTag } from "../../schemas/config.js";
9
- import { createFlyProvider, getRouterAppName } from "../../providers/fly.js";
9
+ import { isPublicTld } from "../../guard.js";
10
+ import { createFlyProvider, FlyDeployError, getRouterAppName, } from "../../providers/fly.js";
10
11
  import { createTailscaleProvider, enableAcceptRoutes, isAcceptRoutesEnabled, isTailscaleInstalled, waitForDevice, } from "../../providers/tailscale.js";
11
12
  import { getCredentialStore } from "../../credentials.js";
12
13
  import { resolveOrg } from "../../resolve.js";
@@ -49,10 +50,14 @@ ${bold("EXAMPLES")}
49
50
  return;
50
51
  }
51
52
  const out = createOutput(args.json);
52
- const network = args._[0];
53
- if (!network) {
53
+ const networkArg = args._[0];
54
+ if (!networkArg || typeof networkArg !== "string") {
54
55
  return out.die("Network Name Required. Usage: ambit create <network>");
55
56
  }
57
+ const network = networkArg;
58
+ if (isPublicTld(network)) {
59
+ return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
60
+ }
56
61
  const tag = args.tag || getRouterTag(network);
57
62
  const selfApprove = args["self-approve"] ?? false;
58
63
  out.blank()
@@ -164,7 +169,16 @@ ${bold("EXAMPLES")}
164
169
  out.ok("Set Router Secrets");
165
170
  const dockerDir = new URL("../../docker/router", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).pathname;
166
171
  out.blank().dim("Deploying Router...");
167
- await fly.routerDeploy(routerAppName, dockerDir, { region });
172
+ try {
173
+ await fly.routerDeploy(routerAppName, dockerDir, { region });
174
+ }
175
+ catch (e) {
176
+ if (e instanceof FlyDeployError) {
177
+ out.dim(` ${e.detail}`);
178
+ return out.die(e.message);
179
+ }
180
+ throw e;
181
+ }
168
182
  out.ok("Router Deployed");
169
183
  out.blank();
170
184
  const joinSpinner = out.spinner("Waiting for Router to Join Tailnet");
@@ -243,7 +257,6 @@ ${bold("EXAMPLES")}
243
257
  .dim("Control their access:")
244
258
  .dim(" https://login.tailscale.com/admin/acls/visual/general-access-rules")
245
259
  .blank();
246
- // Print recommended ACL policy
247
260
  if (subnet && selfApprove) {
248
261
  out.header("Recommended Tailscale ACL Policy:")
249
262
  .blank()
@@ -3,21 +3,102 @@
3
3
  // =============================================================================
4
4
  import * as dntShim from "../../../_dnt.shims.js";
5
5
  import { parseArgs } from "../../../deps/jsr.io/@std/cli/1.0.27/mod.js";
6
+ import { join } from "../../../deps/jsr.io/@std/path/1.1.4/mod.js";
6
7
  import { bold, confirm, fileExists } from "../../../lib/cli.js";
7
8
  import { createOutput } from "../../../lib/output.js";
8
9
  import { registerCommand } from "../mod.js";
9
- import { createFlyProvider } from "../../providers/fly.js";
10
+ import { createFlyProvider, FlyDeployError } from "../../providers/fly.js";
10
11
  import { findRouterApp } from "../../discovery.js";
11
12
  import { resolveOrg } from "../../resolve.js";
12
13
  import { assertNotRouter, auditDeploy, scanFlyToml } from "../../guard.js";
13
14
  // =============================================================================
15
+ // Image Mode
16
+ // =============================================================================
17
+ /**
18
+ * Generate a minimal fly.toml with http_service config for auto start/stop.
19
+ * Written to a temp directory and cleaned up after deploy.
20
+ */
21
+ const generateServiceToml = (port) => `[http_service]\n` +
22
+ ` internal_port = ${port}\n` +
23
+ ` auto_stop_machines = "stop"\n` +
24
+ ` auto_start_machines = true\n` +
25
+ ` min_machines_running = 0\n`;
26
+ /**
27
+ * Parse --main-port value. Returns the port number, or null if "none".
28
+ * Dies on invalid input.
29
+ */
30
+ const parseMainPort = (raw, out) => {
31
+ if (raw === "none")
32
+ return null;
33
+ const port = Number(raw);
34
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
35
+ out.die(`Invalid --main-port: "${raw}". Use a port number (1-65535) or "none".`);
36
+ return "error";
37
+ }
38
+ return port;
39
+ };
40
+ /** Resolve deploy config for image mode (--image). */
41
+ const resolveImageMode = (image, mainPortRaw, out) => {
42
+ const mainPort = parseMainPort(mainPortRaw, out);
43
+ if (mainPort === "error")
44
+ return null;
45
+ const preflight = { scanned: false, warnings: [] };
46
+ if (mainPort !== null) {
47
+ const tempDir = dntShim.Deno.makeTempDirSync();
48
+ const configPath = join(tempDir, "fly.toml");
49
+ dntShim.Deno.writeTextFileSync(configPath, generateServiceToml(mainPort));
50
+ out.ok(`HTTP Service on Port ${mainPort} (auto start/stop)`);
51
+ return { image, configPath, preflight, tempDir };
52
+ }
53
+ out.info("Image Mode — No Service Config");
54
+ return { image, preflight };
55
+ };
56
+ // =============================================================================
57
+ // Config Mode
58
+ // =============================================================================
59
+ /** Resolve deploy config for config mode (default — uses fly.toml). */
60
+ const resolveConfigMode = async (explicitConfig, out) => {
61
+ // Determine config path: explicit --config, or auto-detect ./fly.toml
62
+ let configPath = explicitConfig;
63
+ if (!configPath && await fileExists("./fly.toml")) {
64
+ configPath = "./fly.toml";
65
+ }
66
+ if (!configPath) {
67
+ out.info("No fly.toml Found — Deploying Without Config Scan");
68
+ return { preflight: { scanned: false, warnings: [] } };
69
+ }
70
+ if (!(await fileExists(configPath))) {
71
+ out.die(`Config File Not Found: ${configPath}`);
72
+ return null;
73
+ }
74
+ // Pre-flight scan
75
+ const tomlContent = await dntShim.Deno.readTextFile(configPath);
76
+ const scan = scanFlyToml(tomlContent);
77
+ if (scan.errors.length > 0) {
78
+ for (const err of scan.errors) {
79
+ out.err(err);
80
+ }
81
+ out.die("Pre-flight Check Failed. Fix fly.toml Before Deploying.");
82
+ return null;
83
+ }
84
+ for (const warn of scan.warnings) {
85
+ out.warn(warn);
86
+ }
87
+ out.ok(`Scanned ${configPath}`);
88
+ return {
89
+ configPath,
90
+ preflight: { scanned: scan.scanned, warnings: scan.warnings },
91
+ };
92
+ };
93
+ // =============================================================================
14
94
  // Deploy Command
15
95
  // =============================================================================
16
96
  const deploy = async (argv) => {
17
97
  const args = parseArgs(argv, {
18
- string: ["network", "org", "region", "image", "config"],
98
+ string: ["network", "org", "region", "image", "config", "main-port"],
19
99
  boolean: ["help", "yes", "json"],
20
100
  alias: { y: "yes" },
101
+ default: { "main-port": "80" },
21
102
  });
22
103
  if (args.help) {
23
104
  console.log(`
@@ -35,13 +116,14 @@ ${bold("MODES")}
35
116
  ambit deploy <app> --network <name> --image <img> Docker image, no toml
36
117
 
37
118
  ${bold("OPTIONS")}
38
- --network <name> Custom 6PN network to target (required)
39
- --org <org> Fly.io organization slug
40
- --region <region> Primary deployment region
41
- --image <img> Docker image (mutually exclusive with --config)
42
- --config <path> fly.toml path (mutually exclusive with --image)
43
- -y, --yes Skip confirmation prompts
44
- --json Output as JSON
119
+ --network <name> Custom 6PN network to target (required)
120
+ --org <org> Fly.io organization slug
121
+ --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
+ -y, --yes Skip confirmation prompts
126
+ --json Output as JSON
45
127
 
46
128
  ${bold("SAFETY")}
47
129
  Always deploys with --no-public-ips and --flycast.
@@ -59,10 +141,11 @@ ${bold("EXAMPLES")}
59
141
  // ==========================================================================
60
142
  // Phase 0: Parse & Validate
61
143
  // ==========================================================================
62
- const app = args._[0];
63
- if (!app) {
144
+ const appArg = args._[0];
145
+ if (!appArg || typeof appArg !== "string") {
64
146
  return out.die("App Name Required. Usage: ambit deploy <app> --network <name>");
65
147
  }
148
+ const app = appArg;
66
149
  if (!args.network) {
67
150
  return out.die("--network Is Required");
68
151
  }
@@ -70,7 +153,7 @@ ${bold("EXAMPLES")}
70
153
  assertNotRouter(app);
71
154
  }
72
155
  catch (e) {
73
- return out.die(e.message);
156
+ return out.die(e instanceof Error ? e.message : String(e));
74
157
  }
75
158
  if (args.image && args.config) {
76
159
  return out.die("--image and --config Are Mutually Exclusive");
@@ -128,55 +211,42 @@ ${bold("EXAMPLES")}
128
211
  }
129
212
  out.blank();
130
213
  // ==========================================================================
131
- // Phase 4: Pre-flight TOML Scan
214
+ // Phase 4: Resolve Deploy Mode
132
215
  // ==========================================================================
133
216
  out.header("Step 4: Pre-flight Check").blank();
134
- let preflight = { scanned: false, warnings: [] };
135
- // Determine config path for config mode (not image mode)
136
- let configPath = args.config;
137
- if (!args.image) {
138
- if (!configPath) {
139
- // Auto-detect ./fly.toml
140
- if (await fileExists("./fly.toml")) {
141
- configPath = "./fly.toml";
142
- }
143
- }
144
- if (configPath) {
145
- if (!(await fileExists(configPath))) {
146
- return out.die(`Config File Not Found: ${configPath}`);
147
- }
148
- const tomlContent = await dntShim.Deno.readTextFile(configPath);
149
- const scan = scanFlyToml(tomlContent);
150
- preflight = { scanned: scan.scanned, warnings: scan.warnings };
151
- if (scan.errors.length > 0) {
152
- for (const err of scan.errors) {
153
- out.err(err);
154
- }
155
- return out.die("Pre-flight Check Failed. Fix fly.toml Before Deploying.");
156
- }
157
- for (const warn of scan.warnings) {
158
- out.warn(warn);
159
- }
160
- out.ok(`Scanned ${configPath}`);
161
- }
162
- else {
163
- out.info("No fly.toml Found — Deploying Without Config Scan");
164
- }
165
- }
166
- else {
167
- out.info("Image Mode — Skipping TOML Scan");
168
- }
217
+ const deployConfig = args.image
218
+ ? resolveImageMode(args.image, String(args["main-port"]), out)
219
+ : await resolveConfigMode(args.config, out);
220
+ if (!deployConfig)
221
+ return; // mode resolver already called out.die()
169
222
  out.blank();
170
223
  // ==========================================================================
171
224
  // Phase 5: Deploy with Enforced Flags
172
225
  // ==========================================================================
173
226
  out.header("Step 5: Deploy").blank();
174
227
  out.dim("Deploying with --no-public-ips --flycast ...");
175
- await fly.deploySafe(app, {
176
- image: args.image,
177
- config: configPath,
178
- region: args.region,
179
- });
228
+ try {
229
+ await fly.deploySafe(app, {
230
+ image: deployConfig.image,
231
+ config: deployConfig.configPath,
232
+ region: args.region,
233
+ });
234
+ }
235
+ catch (e) {
236
+ if (e instanceof FlyDeployError) {
237
+ out.dim(` ${e.detail}`);
238
+ return out.die(e.message);
239
+ }
240
+ throw e;
241
+ }
242
+ finally {
243
+ if (deployConfig.tempDir) {
244
+ try {
245
+ dntShim.Deno.removeSync(deployConfig.tempDir, { recursive: true });
246
+ }
247
+ catch { /* ignore */ }
248
+ }
249
+ }
180
250
  out.ok("Deploy Succeeded");
181
251
  out.blank();
182
252
  // ==========================================================================
@@ -202,33 +272,23 @@ ${bold("EXAMPLES")}
202
272
  // Phase 7: Result
203
273
  // ==========================================================================
204
274
  const hasIssues = audit.public_ips_released > 0 || audit.warnings.length > 0;
275
+ const result = {
276
+ app,
277
+ network,
278
+ created,
279
+ audit: {
280
+ public_ips_released: audit.public_ips_released,
281
+ certs_removed: audit.certs_removed,
282
+ flycast_allocations: audit.flycast_allocations,
283
+ warnings: audit.warnings,
284
+ },
285
+ preflight: deployConfig.preflight,
286
+ };
205
287
  if (hasIssues) {
206
- out.fail("Deploy Completed with Issues", {
207
- app,
208
- network,
209
- created,
210
- audit: {
211
- public_ips_released: audit.public_ips_released,
212
- certs_removed: audit.certs_removed,
213
- flycast_allocations: audit.flycast_allocations,
214
- warnings: audit.warnings,
215
- },
216
- preflight,
217
- });
288
+ out.fail("Deploy Completed with Issues", result);
218
289
  }
219
290
  else {
220
- out.done({
221
- app,
222
- network,
223
- created,
224
- audit: {
225
- public_ips_released: audit.public_ips_released,
226
- certs_removed: audit.certs_removed,
227
- flycast_allocations: audit.flycast_allocations,
228
- warnings: audit.warnings,
229
- },
230
- preflight,
231
- });
291
+ out.done(result);
232
292
  }
233
293
  out.blank()
234
294
  .header("=".repeat(50))
@@ -6,7 +6,8 @@ import { bold, confirm } from "../../../lib/cli.js";
6
6
  import { createOutput } from "../../../lib/output.js";
7
7
  import { registerCommand } from "../mod.js";
8
8
  import { createFlyProvider } from "../../providers/fly.js";
9
- import { requireTailscaleProvider } from "../../credentials.js";
9
+ import { createTailscaleProvider } from "../../providers/tailscale.js";
10
+ import { checkDependencies } from "../../credentials.js";
10
11
  import { findRouterApp } from "../../discovery.js";
11
12
  import { resolveOrg } from "../../resolve.js";
12
13
  // =============================================================================
@@ -37,12 +38,17 @@ ${bold("OPTIONS")}
37
38
  if (!args.network) {
38
39
  return out.die("--network Is Required");
39
40
  }
41
+ // =========================================================================
42
+ // Prerequisites
43
+ // =========================================================================
44
+ const { tailscaleKey } = await checkDependencies(out);
40
45
  const fly = createFlyProvider();
41
- await fly.ensureInstalled();
42
46
  await fly.ensureAuth({ interactive: !args.json });
43
- const tailscale = await requireTailscaleProvider(out);
47
+ const tailscale = createTailscaleProvider("-", tailscaleKey);
44
48
  const org = await resolveOrg(fly, args, out);
45
- // 1. Find the router app
49
+ // =========================================================================
50
+ // Discover Router
51
+ // =========================================================================
46
52
  const spinner = out.spinner("Discovering Router");
47
53
  const app = await findRouterApp(fly, org, args.network);
48
54
  if (!app) {
@@ -50,13 +56,15 @@ ${bold("OPTIONS")}
50
56
  return out.die(`No Router Found for Network '${args.network}'`);
51
57
  }
52
58
  spinner.success(`Found Router: ${app.appName}`);
53
- // Get tailscale device to read the actual tag
54
59
  let tsDevice = null;
55
60
  try {
56
61
  tsDevice = await tailscale.getDeviceByHostname(app.appName);
57
62
  }
58
63
  catch { /* device may not exist */ }
59
64
  const tag = tsDevice?.tags?.[0] ?? null;
65
+ // =========================================================================
66
+ // Confirm
67
+ // =========================================================================
60
68
  out.blank()
61
69
  .header("ambit Destroy")
62
70
  .blank()
@@ -72,7 +80,9 @@ ${bold("OPTIONS")}
72
80
  }
73
81
  out.blank();
74
82
  }
75
- // 2. Clean up tailscale
83
+ // =========================================================================
84
+ // Tear Down
85
+ // =========================================================================
76
86
  const dnsSpinner = out.spinner("Clearing Split DNS");
77
87
  try {
78
88
  await tailscale.clearSplitDns(app.network);
@@ -95,7 +105,6 @@ ${bold("OPTIONS")}
95
105
  catch {
96
106
  deviceSpinner.fail("Could Not Remove Tailscale Device");
97
107
  }
98
- // 3. Destroy Fly app
99
108
  const appSpinner = out.spinner("Destroying Fly App");
100
109
  try {
101
110
  await fly.deleteApp(app.appName);
@@ -104,6 +113,9 @@ ${bold("OPTIONS")}
104
113
  catch {
105
114
  appSpinner.fail("Could Not Destroy Fly App");
106
115
  }
116
+ // =========================================================================
117
+ // Done
118
+ // =========================================================================
107
119
  out.done({ destroyed: true, appName: app.appName });
108
120
  out.ok("Router Destroyed");
109
121
  if (tag) {
@@ -7,8 +7,8 @@ import { createOutput } from "../../../lib/output.js";
7
7
  import { runCommand } from "../../../lib/command.js";
8
8
  import { registerCommand } from "../mod.js";
9
9
  import { createFlyProvider } from "../../providers/fly.js";
10
- import { isAcceptRoutesEnabled, isTailscaleInstalled, } from "../../providers/tailscale.js";
11
- import { requireTailscaleProvider } from "../../credentials.js";
10
+ import { createTailscaleProvider, isAcceptRoutesEnabled, isTailscaleInstalled, } from "../../providers/tailscale.js";
11
+ import { checkDependencies } from "../../credentials.js";
12
12
  import { findRouterApp, getRouterMachineInfo, getRouterTailscaleInfo, listRouterApps, } from "../../discovery.js";
13
13
  import { resolveOrg } from "../../resolve.js";
14
14
  // =============================================================================
@@ -54,15 +54,15 @@ ${bold("CHECKS")}
54
54
  }
55
55
  };
56
56
  // =========================================================================
57
- // Prerequisites (fail fast)
57
+ // Prerequisites
58
58
  // =========================================================================
59
+ const { tailscaleKey } = await checkDependencies(out);
59
60
  const fly = createFlyProvider();
60
- await fly.ensureInstalled();
61
61
  await fly.ensureAuth({ interactive: !args.json });
62
- const tailscale = await requireTailscaleProvider(out);
62
+ const tailscale = createTailscaleProvider("-", tailscaleKey);
63
63
  const org = await resolveOrg(fly, args, out);
64
64
  // =========================================================================
65
- // Local checks
65
+ // Local Checks
66
66
  // =========================================================================
67
67
  report("Tailscale Installed", await isTailscaleInstalled(), "Install from https://tailscale.com/download");
68
68
  const tsStatus = await runCommand(["tailscale", "status", "--json"]);
@@ -77,7 +77,7 @@ ${bold("CHECKS")}
77
77
  report("Tailscale Connected", tsConnected, "Run: tailscale up");
78
78
  report("Accept Routes Enabled", await isAcceptRoutesEnabled(), "Run: sudo tailscale set --accept-routes");
79
79
  // =========================================================================
80
- // Router checks
80
+ // Router Checks
81
81
  // =========================================================================
82
82
  if (args.network) {
83
83
  const app = await findRouterApp(fly, org, args.network);
@@ -7,7 +7,8 @@ import { bold } from "../../../lib/cli.js";
7
7
  import { createOutput } from "../../../lib/output.js";
8
8
  import { registerCommand } from "../mod.js";
9
9
  import { createFlyProvider } from "../../providers/fly.js";
10
- import { requireTailscaleProvider } from "../../credentials.js";
10
+ import { createTailscaleProvider } from "../../providers/tailscale.js";
11
+ import { checkDependencies } from "../../credentials.js";
11
12
  import { getRouterMachineInfo, getRouterTailscaleInfo, listRouterApps, } from "../../discovery.js";
12
13
  import { resolveOrg } from "../../resolve.js";
13
14
  // =============================================================================
@@ -32,12 +33,17 @@ ${bold("OPTIONS")}
32
33
  return;
33
34
  }
34
35
  const out = createOutput(args.json);
36
+ // =========================================================================
37
+ // Prerequisites
38
+ // =========================================================================
39
+ const { tailscaleKey } = await checkDependencies(out);
35
40
  const fly = createFlyProvider();
36
- await fly.ensureInstalled();
37
41
  await fly.ensureAuth({ interactive: !args.json });
38
- const tailscale = await requireTailscaleProvider(out);
42
+ const tailscale = createTailscaleProvider("-", tailscaleKey);
39
43
  const org = await resolveOrg(fly, args, out);
40
- // 1. Find all router apps
44
+ // =========================================================================
45
+ // Discover Routers
46
+ // =========================================================================
41
47
  const spinner = out.spinner("Discovering Routers");
42
48
  const routerApps = await listRouterApps(fly, org);
43
49
  spinner.success(`Found ${routerApps.length} Router${routerApps.length !== 1 ? "s" : ""}`);
@@ -50,14 +56,15 @@ ${bold("OPTIONS")}
50
56
  out.print();
51
57
  return;
52
58
  }
53
- // 2. Get machine + tailscale state for each
54
59
  const routers = [];
55
60
  for (const app of routerApps) {
56
61
  const machine = await getRouterMachineInfo(fly, app.appName);
57
62
  const ts = await getRouterTailscaleInfo(tailscale, app.appName);
58
63
  routers.push({ ...app, machine, tailscale: ts });
59
64
  }
60
- // 3. Render
65
+ // =========================================================================
66
+ // Render
67
+ // =========================================================================
61
68
  out.blank().header("Routers").blank();
62
69
  const rows = routers.map((r) => {
63
70
  const tsStatus = r.tailscale