@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.
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +7 -11
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +45 -40
- package/esm/src/cli/commands/create.js +18 -5
- package/esm/src/cli/commands/deploy.js +137 -77
- package/esm/src/cli/commands/destroy.js +19 -7
- package/esm/src/cli/commands/doctor.js +7 -7
- package/esm/src/cli/commands/list.js +13 -6
- package/esm/src/cli/commands/status.js +98 -94
- package/esm/src/cli/mod.js +1 -1
- package/esm/src/credentials.d.ts +13 -4
- package/esm/src/credentials.d.ts.map +1 -1
- package/esm/src/credentials.js +26 -9
- package/esm/src/guard.d.ts +6 -0
- package/esm/src/guard.d.ts.map +1 -1
- package/esm/src/guard.js +183 -0
- package/esm/src/providers/fly.d.ts +10 -0
- package/esm/src/providers/fly.d.ts.map +1 -1
- package/esm/src/providers/fly.js +31 -4
- package/esm/src/schemas/config.d.ts.map +1 -1
- package/package.json +4 -1
package/esm/lib/cli.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
169
|
+
child.on("error", () => resolve(false));
|
|
170
|
+
child.on("close", (code) => resolve(code === 0));
|
|
171
|
+
});
|
|
176
172
|
};
|
package/esm/lib/command.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/lib/command.ts"],"names":[],"mappings":"AAGA,OAAO,sBAAsB,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"}
|
package/esm/lib/command.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Shell Command Helpers
|
|
3
3
|
// =============================================================================
|
|
4
4
|
import "../_dnt.polyfills.js";
|
|
5
|
-
import
|
|
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 =
|
|
13
|
+
export const runCommand = (args, options) => {
|
|
14
14
|
const [cmd, ...cmdArgs] = args;
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
args: cmdArgs,
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = spawn(cmd, cmdArgs, {
|
|
18
17
|
cwd: options?.cwd,
|
|
19
|
-
env: options?.env,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
111
|
+
export const runInteractive = (args, options) => {
|
|
105
112
|
const [cmd, ...cmdArgs] = args;
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
args: cmdArgs,
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const child = spawn(cmd, cmdArgs, {
|
|
109
115
|
cwd: options?.cwd,
|
|
110
|
-
env: options?.env,
|
|
111
|
-
|
|
112
|
-
stdout: "inherit",
|
|
113
|
-
stderr: "inherit",
|
|
116
|
+
env: options?.env ? { ...process.env, ...options.env } : undefined,
|
|
117
|
+
stdio: "inherit",
|
|
114
118
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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
|
|
53
|
-
if (!
|
|
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
|
-
|
|
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>
|
|
39
|
-
--org <org>
|
|
40
|
-
--region <region>
|
|
41
|
-
--image <img>
|
|
42
|
-
--config <path>
|
|
43
|
-
-
|
|
44
|
-
--
|
|
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
|
|
63
|
-
if (!
|
|
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:
|
|
214
|
+
// Phase 4: Resolve Deploy Mode
|
|
132
215
|
// ==========================================================================
|
|
133
216
|
out.header("Step 4: Pre-flight Check").blank();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!
|
|
138
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 {
|
|
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 =
|
|
47
|
+
const tailscale = createTailscaleProvider("-", tailscaleKey);
|
|
44
48
|
const org = await resolveOrg(fly, args, out);
|
|
45
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
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 =
|
|
62
|
+
const tailscale = createTailscaleProvider("-", tailscaleKey);
|
|
63
63
|
const org = await resolveOrg(fly, args, out);
|
|
64
64
|
// =========================================================================
|
|
65
|
-
// Local
|
|
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
|
|
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 {
|
|
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 =
|
|
42
|
+
const tailscale = createTailscaleProvider("-", tailscaleKey);
|
|
39
43
|
const org = await resolveOrg(fly, args, out);
|
|
40
|
-
//
|
|
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
|
-
//
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// Render
|
|
67
|
+
// =========================================================================
|
|
61
68
|
out.blank().header("Routers").blank();
|
|
62
69
|
const rows = routers.map((r) => {
|
|
63
70
|
const tsStatus = r.tailscale
|