@cardelli/ambit 0.1.1 → 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.
|
@@ -7,7 +7,7 @@ import { createOutput } from "../../../lib/output.js";
|
|
|
7
7
|
import { registerCommand } from "../mod.js";
|
|
8
8
|
import { extractSubnet, getRouterTag } from "../../schemas/config.js";
|
|
9
9
|
import { isPublicTld } from "../../guard.js";
|
|
10
|
-
import { createFlyProvider, FlyDeployError, getRouterAppName } from "../../providers/fly.js";
|
|
10
|
+
import { createFlyProvider, FlyDeployError, getRouterAppName, } from "../../providers/fly.js";
|
|
11
11
|
import { createTailscaleProvider, enableAcceptRoutes, isAcceptRoutesEnabled, isTailscaleInstalled, waitForDevice, } from "../../providers/tailscale.js";
|
|
12
12
|
import { getCredentialStore } from "../../credentials.js";
|
|
13
13
|
import { resolveOrg } from "../../resolve.js";
|
|
@@ -50,10 +50,11 @@ ${bold("EXAMPLES")}
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const out = createOutput(args.json);
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
53
|
+
const networkArg = args._[0];
|
|
54
|
+
if (!networkArg || typeof networkArg !== "string") {
|
|
55
55
|
return out.die("Network Name Required. Usage: ambit create <network>");
|
|
56
56
|
}
|
|
57
|
+
const network = networkArg;
|
|
57
58
|
if (isPublicTld(network)) {
|
|
58
59
|
return out.die(`"${network}" Is a Public TLD and Cannot Be Used as a Network Name`);
|
|
59
60
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
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";
|
|
@@ -11,13 +12,93 @@ 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,44 +211,14 @@ ${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
|
|
@@ -174,8 +227,8 @@ ${bold("EXAMPLES")}
|
|
|
174
227
|
out.dim("Deploying with --no-public-ips --flycast ...");
|
|
175
228
|
try {
|
|
176
229
|
await fly.deploySafe(app, {
|
|
177
|
-
image:
|
|
178
|
-
config: configPath,
|
|
230
|
+
image: deployConfig.image,
|
|
231
|
+
config: deployConfig.configPath,
|
|
179
232
|
region: args.region,
|
|
180
233
|
});
|
|
181
234
|
}
|
|
@@ -186,6 +239,14 @@ ${bold("EXAMPLES")}
|
|
|
186
239
|
}
|
|
187
240
|
throw e;
|
|
188
241
|
}
|
|
242
|
+
finally {
|
|
243
|
+
if (deployConfig.tempDir) {
|
|
244
|
+
try {
|
|
245
|
+
dntShim.Deno.removeSync(deployConfig.tempDir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
catch { /* ignore */ }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
189
250
|
out.ok("Deploy Succeeded");
|
|
190
251
|
out.blank();
|
|
191
252
|
// ==========================================================================
|
|
@@ -211,33 +272,23 @@ ${bold("EXAMPLES")}
|
|
|
211
272
|
// Phase 7: Result
|
|
212
273
|
// ==========================================================================
|
|
213
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
|
+
};
|
|
214
287
|
if (hasIssues) {
|
|
215
|
-
out.fail("Deploy Completed with Issues",
|
|
216
|
-
app,
|
|
217
|
-
network,
|
|
218
|
-
created,
|
|
219
|
-
audit: {
|
|
220
|
-
public_ips_released: audit.public_ips_released,
|
|
221
|
-
certs_removed: audit.certs_removed,
|
|
222
|
-
flycast_allocations: audit.flycast_allocations,
|
|
223
|
-
warnings: audit.warnings,
|
|
224
|
-
},
|
|
225
|
-
preflight,
|
|
226
|
-
});
|
|
288
|
+
out.fail("Deploy Completed with Issues", result);
|
|
227
289
|
}
|
|
228
290
|
else {
|
|
229
|
-
out.done(
|
|
230
|
-
app,
|
|
231
|
-
network,
|
|
232
|
-
created,
|
|
233
|
-
audit: {
|
|
234
|
-
public_ips_released: audit.public_ips_released,
|
|
235
|
-
certs_removed: audit.certs_removed,
|
|
236
|
-
flycast_allocations: audit.flycast_allocations,
|
|
237
|
-
warnings: audit.warnings,
|
|
238
|
-
},
|
|
239
|
-
preflight,
|
|
240
|
-
});
|
|
291
|
+
out.done(result);
|
|
241
292
|
}
|
|
242
293
|
out.blank()
|
|
243
294
|
.header("=".repeat(50))
|
|
@@ -78,9 +78,7 @@ const showNetworkStatus = async (fly, tailscale, args) => {
|
|
|
78
78
|
.text(` Region: ${machine?.region ?? "unknown"}`)
|
|
79
79
|
.text(` Machine State: ${machine?.state ?? "unknown"}`)
|
|
80
80
|
.text(` Private IP: ${machine?.privateIp ?? "unknown"}`)
|
|
81
|
-
.text(` SOCKS Proxy: ${machine?.privateIp
|
|
82
|
-
? `socks5://[${machine.privateIp}]:1080`
|
|
83
|
-
: "unknown"}`);
|
|
81
|
+
.text(` SOCKS Proxy: ${machine?.privateIp ? `socks5://[${machine.privateIp}]:1080` : "unknown"}`);
|
|
84
82
|
if (machine?.subnet) {
|
|
85
83
|
out.text(` Subnet: ${machine.subnet}`);
|
|
86
84
|
}
|
package/esm/src/cli/mod.js
CHANGED
|
@@ -65,7 +65,7 @@ export const runCli = async (argv) => {
|
|
|
65
65
|
showVersion();
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
const commandName = args._[0];
|
|
68
|
+
const commandName = typeof args._[0] === "string" ? args._[0] : undefined;
|
|
69
69
|
if (!commandName || args.help) {
|
|
70
70
|
if (commandName) {
|
|
71
71
|
const command = getCommand(commandName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/config.ts"],"names":[],"mappings":"AAOA,OAAO,yBAAyB,CAAC;AAKjC,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/src/schemas/config.ts"],"names":[],"mappings":"AAOA,OAAO,yBAAyB,CAAC;AAKjC,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAAgC,CAAC;AAEhF,eAAO,MAAM,aAAa,GAAI,WAAW,MAAM,KAAG,MAKjD,CAAC;AAMF,eAAO,MAAM,YAAY,QAAO,MAG/B,CAAC"}
|