@cardelli/ambit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/deno.d.ts +51 -0
- package/esm/deno.d.ts.map +1 -0
- package/esm/deno.js +49 -0
- package/esm/lib/result.d.ts +8 -0
- package/esm/lib/result.d.ts.map +1 -0
- package/esm/lib/result.js +1 -0
- package/esm/src/cli/commands/create.js +4 -3
- package/esm/src/cli/commands/deploy.js +220 -76
- package/esm/src/cli/commands/status.js +1 -3
- package/esm/src/cli/mod.d.ts.map +1 -1
- package/esm/src/cli/mod.js +3 -2
- package/esm/src/schemas/config.d.ts.map +1 -1
- package/esm/src/template.d.ts +38 -0
- package/esm/src/template.d.ts.map +1 -0
- package/esm/src/template.js +159 -0
- package/package.json +2 -2
package/esm/deno.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
declare namespace _default {
|
|
2
|
+
let name: string;
|
|
3
|
+
let version: string;
|
|
4
|
+
let description: string;
|
|
5
|
+
let license: string;
|
|
6
|
+
let exports: {
|
|
7
|
+
".": string;
|
|
8
|
+
"./providers/fly": string;
|
|
9
|
+
"./providers/tailscale": string;
|
|
10
|
+
"./schemas/config": string;
|
|
11
|
+
"./schemas/fly": string;
|
|
12
|
+
"./schemas/tailscale": string;
|
|
13
|
+
"./lib/cli": string;
|
|
14
|
+
"./lib/command": string;
|
|
15
|
+
"./lib/output": string;
|
|
16
|
+
"./lib/result": string;
|
|
17
|
+
"./lib/paths": string;
|
|
18
|
+
"./src/credentials": string;
|
|
19
|
+
"./src/discovery": string;
|
|
20
|
+
"./src/guard": string;
|
|
21
|
+
"./src/resolve": string;
|
|
22
|
+
};
|
|
23
|
+
let tasks: {
|
|
24
|
+
dev: string;
|
|
25
|
+
run: string;
|
|
26
|
+
check: string;
|
|
27
|
+
test: string;
|
|
28
|
+
"check-versions": string;
|
|
29
|
+
build: string;
|
|
30
|
+
};
|
|
31
|
+
let imports: {
|
|
32
|
+
"@/": string;
|
|
33
|
+
"@cliffy/table": string;
|
|
34
|
+
"@std/assert": string;
|
|
35
|
+
"@std/cli": string;
|
|
36
|
+
"@std/json": string;
|
|
37
|
+
"@std/path": string;
|
|
38
|
+
"@std/fs": string;
|
|
39
|
+
"@std/toml": string;
|
|
40
|
+
zod: string;
|
|
41
|
+
};
|
|
42
|
+
namespace compilerOptions {
|
|
43
|
+
let strict: boolean;
|
|
44
|
+
let baseUrl: string;
|
|
45
|
+
let paths: {
|
|
46
|
+
"@/*": string[];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export default _default;
|
|
51
|
+
//# sourceMappingURL=deno.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deno.d.ts","sourceRoot":"","sources":["../src/deno.js"],"names":[],"mappings":""}
|
package/esm/deno.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"name": "@cardelli/ambit",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Deploy apps to the cloud that only you and your AI agents can reach",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./main.ts",
|
|
8
|
+
"./providers/fly": "./src/providers/fly.ts",
|
|
9
|
+
"./providers/tailscale": "./src/providers/tailscale.ts",
|
|
10
|
+
"./schemas/config": "./src/schemas/config.ts",
|
|
11
|
+
"./schemas/fly": "./src/schemas/fly.ts",
|
|
12
|
+
"./schemas/tailscale": "./src/schemas/tailscale.ts",
|
|
13
|
+
"./lib/cli": "./lib/cli.ts",
|
|
14
|
+
"./lib/command": "./lib/command.ts",
|
|
15
|
+
"./lib/output": "./lib/output.ts",
|
|
16
|
+
"./lib/result": "./lib/result.ts",
|
|
17
|
+
"./lib/paths": "./lib/paths.ts",
|
|
18
|
+
"./src/credentials": "./src/credentials.ts",
|
|
19
|
+
"./src/discovery": "./src/discovery.ts",
|
|
20
|
+
"./src/guard": "./src/guard.ts",
|
|
21
|
+
"./src/resolve": "./src/resolve.ts"
|
|
22
|
+
},
|
|
23
|
+
"tasks": {
|
|
24
|
+
"dev": "deno run --watch main.ts",
|
|
25
|
+
"run": "deno run -A main.ts",
|
|
26
|
+
"check": "deno check main.ts",
|
|
27
|
+
"test": "deno test -A",
|
|
28
|
+
"check-versions": "deno run --allow-net --allow-run=git scripts/check-versions.ts",
|
|
29
|
+
"build": "deno run -A scripts/build_npm.ts"
|
|
30
|
+
},
|
|
31
|
+
"imports": {
|
|
32
|
+
"@/": "./",
|
|
33
|
+
"@cliffy/table": "jsr:@cliffy/table@^1.0.0",
|
|
34
|
+
"@std/assert": "jsr:@std/assert@1",
|
|
35
|
+
"@std/cli": "jsr:@std/cli@^1.0.27",
|
|
36
|
+
"@std/json": "jsr:@std/json@^1.0.2",
|
|
37
|
+
"@std/path": "jsr:@std/path@^1.1.4",
|
|
38
|
+
"@std/fs": "jsr:@std/fs@^1.0.22",
|
|
39
|
+
"@std/toml": "jsr:@std/toml@^1",
|
|
40
|
+
"zod": "jsr:@zod/zod@^4.3.6"
|
|
41
|
+
},
|
|
42
|
+
"compilerOptions": {
|
|
43
|
+
"strict": true,
|
|
44
|
+
"baseUrl": ".",
|
|
45
|
+
"paths": {
|
|
46
|
+
"@/*": ["./*"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/lib/result.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAC3C,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,CAAC,CAAC,GAClB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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";
|
|
@@ -10,14 +11,161 @@ 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";
|
|
14
|
+
import { fetchTemplate, parseTemplateRef } from "../../template.js";
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Image Mode
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Generate a minimal fly.toml with http_service config for auto start/stop.
|
|
20
|
+
* Written to a temp directory and cleaned up after deploy.
|
|
21
|
+
*/
|
|
22
|
+
const generateServiceToml = (port) => `[http_service]\n` +
|
|
23
|
+
` internal_port = ${port}\n` +
|
|
24
|
+
` auto_stop_machines = "stop"\n` +
|
|
25
|
+
` auto_start_machines = true\n` +
|
|
26
|
+
` min_machines_running = 0\n`;
|
|
27
|
+
/**
|
|
28
|
+
* Parse --main-port value. Returns the port number, or null if "none".
|
|
29
|
+
* Dies on invalid input.
|
|
30
|
+
*/
|
|
31
|
+
const parseMainPort = (raw, out) => {
|
|
32
|
+
if (raw === "none")
|
|
33
|
+
return null;
|
|
34
|
+
const port = Number(raw);
|
|
35
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
36
|
+
out.die(`Invalid --main-port: "${raw}". Use a port number (1-65535) or "none".`);
|
|
37
|
+
return "error";
|
|
38
|
+
}
|
|
39
|
+
return port;
|
|
40
|
+
};
|
|
41
|
+
/** Resolve deploy config for image mode (--image). */
|
|
42
|
+
const resolveImageMode = (image, mainPortRaw, out) => {
|
|
43
|
+
const mainPort = parseMainPort(mainPortRaw, out);
|
|
44
|
+
if (mainPort === "error")
|
|
45
|
+
return null;
|
|
46
|
+
const preflight = { scanned: false, warnings: [] };
|
|
47
|
+
if (mainPort !== null) {
|
|
48
|
+
const tempDir = dntShim.Deno.makeTempDirSync();
|
|
49
|
+
const configPath = join(tempDir, "fly.toml");
|
|
50
|
+
dntShim.Deno.writeTextFileSync(configPath, generateServiceToml(mainPort));
|
|
51
|
+
out.ok(`HTTP Service on Port ${mainPort} (auto start/stop)`);
|
|
52
|
+
return { image, configPath, preflight, tempDir };
|
|
53
|
+
}
|
|
54
|
+
out.info("Image Mode — No Service Config");
|
|
55
|
+
return { image, preflight };
|
|
56
|
+
};
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Config Mode
|
|
59
|
+
// =============================================================================
|
|
60
|
+
/** Resolve deploy config for config mode (default — uses fly.toml). */
|
|
61
|
+
const resolveConfigMode = async (explicitConfig, out) => {
|
|
62
|
+
// Determine config path: explicit --config, or auto-detect ./fly.toml
|
|
63
|
+
let configPath = explicitConfig;
|
|
64
|
+
if (!configPath && await fileExists("./fly.toml")) {
|
|
65
|
+
configPath = "./fly.toml";
|
|
66
|
+
}
|
|
67
|
+
if (!configPath) {
|
|
68
|
+
out.info("No fly.toml Found — Deploying Without Config Scan");
|
|
69
|
+
return { preflight: { scanned: false, warnings: [] } };
|
|
70
|
+
}
|
|
71
|
+
if (!(await fileExists(configPath))) {
|
|
72
|
+
out.die(`Config File Not Found: ${configPath}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// Pre-flight scan
|
|
76
|
+
const tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
77
|
+
const scan = scanFlyToml(tomlContent);
|
|
78
|
+
if (scan.errors.length > 0) {
|
|
79
|
+
for (const err of scan.errors) {
|
|
80
|
+
out.err(err);
|
|
81
|
+
}
|
|
82
|
+
out.die("Pre-flight Check Failed. Fix fly.toml Before Deploying.");
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
for (const warn of scan.warnings) {
|
|
86
|
+
out.warn(warn);
|
|
87
|
+
}
|
|
88
|
+
out.ok(`Scanned ${configPath}`);
|
|
89
|
+
return {
|
|
90
|
+
configPath,
|
|
91
|
+
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
92
|
+
};
|
|
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.owner}/${ref.repo}/${ref.path}` +
|
|
106
|
+
(ref.ref ? `@${ref.ref}` : "");
|
|
107
|
+
out.info(`Template: ${label}`);
|
|
108
|
+
const fetchSpinner = out.spinner("Fetching Template from GitHub");
|
|
109
|
+
const result = await fetchTemplate(ref);
|
|
110
|
+
if (!result.ok) {
|
|
111
|
+
fetchSpinner.fail("Template Fetch Failed");
|
|
112
|
+
out.die(result.message);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
fetchSpinner.success("Template Fetched");
|
|
116
|
+
// Find and scan the template's fly.toml
|
|
117
|
+
const configPath = join(result.templateDir, "fly.toml");
|
|
118
|
+
let tomlContent;
|
|
119
|
+
try {
|
|
120
|
+
tomlContent = await dntShim.Deno.readTextFile(configPath);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
try {
|
|
124
|
+
dntShim.Deno.removeSync(result.tempDir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
out.die(`Template '${ref.path}' Has No fly.toml`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const scan = scanFlyToml(tomlContent);
|
|
131
|
+
if (scan.errors.length > 0) {
|
|
132
|
+
try {
|
|
133
|
+
dntShim.Deno.removeSync(result.tempDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
catch { /* ignore */ }
|
|
136
|
+
for (const err of scan.errors) {
|
|
137
|
+
out.err(err);
|
|
138
|
+
}
|
|
139
|
+
out.die("Pre-flight Check Failed for Template fly.toml");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
for (const warn of scan.warnings) {
|
|
143
|
+
out.warn(warn);
|
|
144
|
+
}
|
|
145
|
+
out.ok(`Scanned ${ref.path}/fly.toml`);
|
|
146
|
+
return {
|
|
147
|
+
configPath,
|
|
148
|
+
preflight: { scanned: scan.scanned, warnings: scan.warnings },
|
|
149
|
+
tempDir: result.tempDir,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
13
152
|
// =============================================================================
|
|
14
153
|
// Deploy Command
|
|
15
154
|
// =============================================================================
|
|
16
155
|
const deploy = async (argv) => {
|
|
17
156
|
const args = parseArgs(argv, {
|
|
18
|
-
string: [
|
|
157
|
+
string: [
|
|
158
|
+
"network",
|
|
159
|
+
"org",
|
|
160
|
+
"region",
|
|
161
|
+
"image",
|
|
162
|
+
"config",
|
|
163
|
+
"main-port",
|
|
164
|
+
"template",
|
|
165
|
+
],
|
|
19
166
|
boolean: ["help", "yes", "json"],
|
|
20
167
|
alias: { y: "yes" },
|
|
168
|
+
default: { "main-port": "80" },
|
|
21
169
|
});
|
|
22
170
|
if (args.help) {
|
|
23
171
|
console.log(`
|
|
@@ -28,20 +176,37 @@ ${bold("USAGE")}
|
|
|
28
176
|
|
|
29
177
|
${bold("MODES")}
|
|
30
178
|
Config mode (default):
|
|
31
|
-
ambit deploy <app> --network <name>
|
|
32
|
-
ambit deploy <app> --network <name> --config path
|
|
179
|
+
ambit deploy <app> --network <name> Uses ./fly.toml
|
|
180
|
+
ambit deploy <app> --network <name> --config path Explicit fly.toml
|
|
33
181
|
|
|
34
182
|
Image mode:
|
|
35
|
-
ambit deploy <app> --network <name> --image <img>
|
|
183
|
+
ambit deploy <app> --network <name> --image <img> Docker image, no toml
|
|
184
|
+
|
|
185
|
+
Template mode:
|
|
186
|
+
ambit deploy <app> --network <name> --template <ref> GitHub template
|
|
36
187
|
|
|
37
188
|
${bold("OPTIONS")}
|
|
38
|
-
--network <name>
|
|
39
|
-
--org <org>
|
|
40
|
-
--region <region>
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
|
|
44
|
-
|
|
189
|
+
--network <name> Custom 6PN network to target (required)
|
|
190
|
+
--org <org> Fly.io organization slug
|
|
191
|
+
--region <region> Primary deployment region
|
|
192
|
+
-y, --yes Skip confirmation prompts
|
|
193
|
+
--json Output as JSON
|
|
194
|
+
|
|
195
|
+
${bold("CONFIG MODE")} (default)
|
|
196
|
+
--config <path> Explicit fly.toml path (auto-detects ./fly.toml if omitted)
|
|
197
|
+
|
|
198
|
+
${bold("IMAGE MODE")}
|
|
199
|
+
--image <img> Docker image to deploy (no fly.toml needed)
|
|
200
|
+
--main-port <port> Internal port for HTTP service (default: 80, "none" to skip)
|
|
201
|
+
|
|
202
|
+
${bold("TEMPLATE MODE")}
|
|
203
|
+
--template <ref> GitHub template as owner/repo/path[@ref]
|
|
204
|
+
|
|
205
|
+
Reference format:
|
|
206
|
+
owner/repo/path Fetch from the default branch
|
|
207
|
+
owner/repo/path@tag Fetch a tagged release
|
|
208
|
+
owner/repo/path@branch Fetch a specific branch
|
|
209
|
+
owner/repo/path@commit Fetch a specific commit
|
|
45
210
|
|
|
46
211
|
${bold("SAFETY")}
|
|
47
212
|
Always deploys with --no-public-ips and --flycast.
|
|
@@ -52,6 +217,8 @@ ${bold("EXAMPLES")}
|
|
|
52
217
|
ambit deploy my-app --network browsers
|
|
53
218
|
ambit deploy my-app --network browsers --image registry/img:latest
|
|
54
219
|
ambit deploy my-app --network browsers --config ./fly.toml --region sea
|
|
220
|
+
ambit deploy my-browser --network lab --template ToxicPine/ambit-templates/cdp
|
|
221
|
+
ambit deploy my-browser --network lab --template ToxicPine/ambit-templates/cdp@v1.0
|
|
55
222
|
`);
|
|
56
223
|
return;
|
|
57
224
|
}
|
|
@@ -59,10 +226,11 @@ ${bold("EXAMPLES")}
|
|
|
59
226
|
// ==========================================================================
|
|
60
227
|
// Phase 0: Parse & Validate
|
|
61
228
|
// ==========================================================================
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
229
|
+
const appArg = args._[0];
|
|
230
|
+
if (!appArg || typeof appArg !== "string") {
|
|
64
231
|
return out.die("App Name Required. Usage: ambit deploy <app> --network <name>");
|
|
65
232
|
}
|
|
233
|
+
const app = appArg;
|
|
66
234
|
if (!args.network) {
|
|
67
235
|
return out.die("--network Is Required");
|
|
68
236
|
}
|
|
@@ -70,10 +238,11 @@ ${bold("EXAMPLES")}
|
|
|
70
238
|
assertNotRouter(app);
|
|
71
239
|
}
|
|
72
240
|
catch (e) {
|
|
73
|
-
return out.die(e.message);
|
|
241
|
+
return out.die(e instanceof Error ? e.message : String(e));
|
|
74
242
|
}
|
|
75
|
-
|
|
76
|
-
|
|
243
|
+
const modeFlags = [args.image, args.config, args.template].filter(Boolean);
|
|
244
|
+
if (modeFlags.length > 1) {
|
|
245
|
+
return out.die("--image, --config, and --template Are Mutually Exclusive");
|
|
77
246
|
}
|
|
78
247
|
const network = args.network;
|
|
79
248
|
out.blank()
|
|
@@ -128,44 +297,21 @@ ${bold("EXAMPLES")}
|
|
|
128
297
|
}
|
|
129
298
|
out.blank();
|
|
130
299
|
// ==========================================================================
|
|
131
|
-
// Phase 4:
|
|
300
|
+
// Phase 4: Resolve Deploy Mode
|
|
132
301
|
// ==========================================================================
|
|
133
302
|
out.header("Step 4: Pre-flight Check").blank();
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
}
|
|
303
|
+
let deployConfig;
|
|
304
|
+
if (args.template) {
|
|
305
|
+
deployConfig = await resolveTemplateMode(args.template, out);
|
|
306
|
+
}
|
|
307
|
+
else if (args.image) {
|
|
308
|
+
deployConfig = resolveImageMode(args.image, String(args["main-port"]), out);
|
|
165
309
|
}
|
|
166
310
|
else {
|
|
167
|
-
|
|
311
|
+
deployConfig = await resolveConfigMode(args.config, out);
|
|
168
312
|
}
|
|
313
|
+
if (!deployConfig)
|
|
314
|
+
return; // mode resolver already called out.die()
|
|
169
315
|
out.blank();
|
|
170
316
|
// ==========================================================================
|
|
171
317
|
// Phase 5: Deploy with Enforced Flags
|
|
@@ -174,8 +320,8 @@ ${bold("EXAMPLES")}
|
|
|
174
320
|
out.dim("Deploying with --no-public-ips --flycast ...");
|
|
175
321
|
try {
|
|
176
322
|
await fly.deploySafe(app, {
|
|
177
|
-
image:
|
|
178
|
-
config: configPath,
|
|
323
|
+
image: deployConfig.image,
|
|
324
|
+
config: deployConfig.configPath,
|
|
179
325
|
region: args.region,
|
|
180
326
|
});
|
|
181
327
|
}
|
|
@@ -186,6 +332,14 @@ ${bold("EXAMPLES")}
|
|
|
186
332
|
}
|
|
187
333
|
throw e;
|
|
188
334
|
}
|
|
335
|
+
finally {
|
|
336
|
+
if (deployConfig.tempDir) {
|
|
337
|
+
try {
|
|
338
|
+
dntShim.Deno.removeSync(deployConfig.tempDir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
catch { /* ignore */ }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
189
343
|
out.ok("Deploy Succeeded");
|
|
190
344
|
out.blank();
|
|
191
345
|
// ==========================================================================
|
|
@@ -211,33 +365,23 @@ ${bold("EXAMPLES")}
|
|
|
211
365
|
// Phase 7: Result
|
|
212
366
|
// ==========================================================================
|
|
213
367
|
const hasIssues = audit.public_ips_released > 0 || audit.warnings.length > 0;
|
|
368
|
+
const result = {
|
|
369
|
+
app,
|
|
370
|
+
network,
|
|
371
|
+
created,
|
|
372
|
+
audit: {
|
|
373
|
+
public_ips_released: audit.public_ips_released,
|
|
374
|
+
certs_removed: audit.certs_removed,
|
|
375
|
+
flycast_allocations: audit.flycast_allocations,
|
|
376
|
+
warnings: audit.warnings,
|
|
377
|
+
},
|
|
378
|
+
preflight: deployConfig.preflight,
|
|
379
|
+
};
|
|
214
380
|
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
|
-
});
|
|
381
|
+
out.fail("Deploy Completed with Issues", result);
|
|
227
382
|
}
|
|
228
383
|
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
|
-
});
|
|
384
|
+
out.done(result);
|
|
241
385
|
}
|
|
242
386
|
out.blank()
|
|
243
387
|
.header("=".repeat(50))
|
|
@@ -255,6 +399,6 @@ ${bold("EXAMPLES")}
|
|
|
255
399
|
registerCommand({
|
|
256
400
|
name: "deploy",
|
|
257
401
|
description: "Deploy an app safely on a custom private network",
|
|
258
|
-
usage: "ambit deploy <app> --network <name> [--image <img>] [--org <org>] [--region <region>]",
|
|
402
|
+
usage: "ambit deploy <app> --network <name> [--image <img>] [--template <ref>] [--org <org>] [--region <region>]",
|
|
259
403
|
run: deploy,
|
|
260
404
|
});
|
|
@@ -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.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAQD,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,IAElD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAO,GAAG,SAEnD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,OAAO,EAExC,CAAC;AAQF,eAAO,MAAM,QAAQ,QAAO,IA4B3B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAC;AAMF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAuCzD,CAAC"}
|
package/esm/src/cli/mod.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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
6
|
import { bold, dim } from "../../lib/cli.js";
|
|
7
|
+
import denoConfig from "../../deno.js";
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Commands Registry
|
|
9
10
|
// =============================================================================
|
|
@@ -20,7 +21,7 @@ export const getAllCommands = () => {
|
|
|
20
21
|
// =============================================================================
|
|
21
22
|
// Help Text
|
|
22
23
|
// =============================================================================
|
|
23
|
-
const VERSION =
|
|
24
|
+
const VERSION = denoConfig.version;
|
|
24
25
|
export const showHelp = () => {
|
|
25
26
|
console.log(`
|
|
26
27
|
${bold("ambit")} - Tailscale Subnet Router for Fly.io Custom Networks
|
|
@@ -65,7 +66,7 @@ export const runCli = async (argv) => {
|
|
|
65
66
|
showVersion();
|
|
66
67
|
return;
|
|
67
68
|
}
|
|
68
|
-
const commandName = args._[0];
|
|
69
|
+
const commandName = typeof args._[0] === "string" ? args._[0] : undefined;
|
|
69
70
|
if (!commandName || args.help) {
|
|
70
71
|
if (commandName) {
|
|
71
72
|
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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Result } from "../lib/result.js";
|
|
2
|
+
/** Kinds of errors that can occur when fetching a template from GitHub. */
|
|
3
|
+
export type TemplateErrorKind = "NotFound" | "RateLimited" | "HttpError" | "ExtractionFailed" | "PathNotFound" | "PathNotDirectory" | "EmptyArchive" | "NetworkError";
|
|
4
|
+
/** Parsed GitHub template reference. */
|
|
5
|
+
export interface TemplateRef {
|
|
6
|
+
owner: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
path: string;
|
|
9
|
+
ref: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Result of fetching a template from GitHub. */
|
|
12
|
+
export type TemplateFetchResult = Result<{
|
|
13
|
+
tempDir: string;
|
|
14
|
+
templateDir: string;
|
|
15
|
+
}, TemplateErrorKind>;
|
|
16
|
+
/**
|
|
17
|
+
* Parse a template reference string into its components.
|
|
18
|
+
* Returns null if the format is invalid.
|
|
19
|
+
*
|
|
20
|
+
* Format: owner/repo/path[@ref]
|
|
21
|
+
*
|
|
22
|
+
* The first two segments are always owner/repo. Everything after the second
|
|
23
|
+
* slash up to an optional @ref is the path within the repository. This is
|
|
24
|
+
* unambiguous because GitHub owner and repo names cannot contain slashes.
|
|
25
|
+
*/
|
|
26
|
+
export declare const parseTemplateRef: (raw: string) => TemplateRef | null;
|
|
27
|
+
/**
|
|
28
|
+
* Download a template from GitHub and extract the target subdirectory
|
|
29
|
+
* to a temp directory.
|
|
30
|
+
*
|
|
31
|
+
* On success, returns the temp dir (for cleanup) and the path to the
|
|
32
|
+
* extracted template directory. The caller is responsible for removing
|
|
33
|
+
* tempDir when done.
|
|
34
|
+
*
|
|
35
|
+
* On failure, cleans up the temp dir and returns a typed error.
|
|
36
|
+
*/
|
|
37
|
+
export declare const fetchTemplate: (ref: TemplateRef) => Promise<TemplateFetchResult>;
|
|
38
|
+
//# sourceMappingURL=template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/src/template.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAM/C,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,aAAa,GACb,WAAW,GACX,kBAAkB,GAClB,cAAc,GACd,kBAAkB,GAClB,cAAc,GACd,cAAc,CAAC;AAMnB,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED,iDAAiD;AACjD,MAAM,MAAM,mBAAmB,GAAG,MAAM,CACtC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EACxC,iBAAiB,CAClB,CAAC;AA4BF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,WAAW,GAAG,IAyB5D,CAAC;AAMF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,WAAW,KACf,OAAO,CAAC,mBAAmB,CAsH7B,CAAC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Template - Fetch Deploy Templates from GitHub Repositories
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Downloads a subdirectory from a GitHub repository for use as a deploy
|
|
6
|
+
// template. Templates are expected to contain at least a fly.toml and
|
|
7
|
+
// typically a Dockerfile.
|
|
8
|
+
//
|
|
9
|
+
// Reference format: owner/repo/path[@ref]
|
|
10
|
+
//
|
|
11
|
+
// ToxicPine/ambit-templates/cdp → default branch
|
|
12
|
+
// ToxicPine/ambit-templates/cdp@v1.0 → tagged release
|
|
13
|
+
// ToxicPine/ambit-templates/cdp@main → explicit branch
|
|
14
|
+
//
|
|
15
|
+
// =============================================================================
|
|
16
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
17
|
+
import { join } from "../deps/jsr.io/@std/path/1.1.4/mod.js";
|
|
18
|
+
import { runCommand } from "../lib/command.js";
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Internal Helpers
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/** Shorthand for returning a typed fetch error. */
|
|
23
|
+
const fail = (kind, message) => ({
|
|
24
|
+
ok: false,
|
|
25
|
+
kind,
|
|
26
|
+
message,
|
|
27
|
+
});
|
|
28
|
+
/** Format a template reference for display. */
|
|
29
|
+
const formatRef = (ref) => `${ref.owner}/${ref.repo}/${ref.path}` + (ref.ref ? `@${ref.ref}` : "");
|
|
30
|
+
/** Format owner/repo with optional ref for display. */
|
|
31
|
+
const formatRepo = (ref) => `${ref.owner}/${ref.repo}` + (ref.ref ? `@${ref.ref}` : "");
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Parse Template Reference
|
|
34
|
+
// =============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* Parse a template reference string into its components.
|
|
37
|
+
* Returns null if the format is invalid.
|
|
38
|
+
*
|
|
39
|
+
* Format: owner/repo/path[@ref]
|
|
40
|
+
*
|
|
41
|
+
* The first two segments are always owner/repo. Everything after the second
|
|
42
|
+
* slash up to an optional @ref is the path within the repository. This is
|
|
43
|
+
* unambiguous because GitHub owner and repo names cannot contain slashes.
|
|
44
|
+
*/
|
|
45
|
+
export const parseTemplateRef = (raw) => {
|
|
46
|
+
// Split off @ref suffix
|
|
47
|
+
const atIndex = raw.lastIndexOf("@");
|
|
48
|
+
let body;
|
|
49
|
+
let ref;
|
|
50
|
+
if (atIndex > 0) {
|
|
51
|
+
body = raw.slice(0, atIndex);
|
|
52
|
+
ref = raw.slice(atIndex + 1);
|
|
53
|
+
if (!ref)
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
body = raw;
|
|
58
|
+
}
|
|
59
|
+
// Need at least owner/repo/path
|
|
60
|
+
const parts = body.split("/");
|
|
61
|
+
if (parts.length < 3)
|
|
62
|
+
return null;
|
|
63
|
+
const owner = parts[0];
|
|
64
|
+
const repo = parts[1];
|
|
65
|
+
const path = parts.slice(2).join("/");
|
|
66
|
+
if (!owner || !repo || !path)
|
|
67
|
+
return null;
|
|
68
|
+
return { owner, repo, path, ref };
|
|
69
|
+
};
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Fetch Template
|
|
72
|
+
// =============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Download a template from GitHub and extract the target subdirectory
|
|
75
|
+
* to a temp directory.
|
|
76
|
+
*
|
|
77
|
+
* On success, returns the temp dir (for cleanup) and the path to the
|
|
78
|
+
* extracted template directory. The caller is responsible for removing
|
|
79
|
+
* tempDir when done.
|
|
80
|
+
*
|
|
81
|
+
* On failure, cleans up the temp dir and returns a typed error.
|
|
82
|
+
*/
|
|
83
|
+
export const fetchTemplate = async (ref) => {
|
|
84
|
+
const tempDir = dntShim.Deno.makeTempDirSync({ prefix: "ambit-template-" });
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
try {
|
|
87
|
+
dntShim.Deno.removeSync(tempDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
catch { /* ignore */ }
|
|
90
|
+
};
|
|
91
|
+
const cleanFail = (kind, message) => {
|
|
92
|
+
cleanup();
|
|
93
|
+
return fail(kind, message);
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const url = ref.ref
|
|
97
|
+
? `https://api.github.com/repos/${ref.owner}/${ref.repo}/tarball/${ref.ref}`
|
|
98
|
+
: `https://api.github.com/repos/${ref.owner}/${ref.repo}/tarball`;
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
headers: {
|
|
101
|
+
Accept: "application/vnd.github+json",
|
|
102
|
+
"User-Agent": "ambit-cli",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const repo = formatRepo(ref);
|
|
107
|
+
if (response.status === 404) {
|
|
108
|
+
return cleanFail("NotFound", `Template Repository Not Found: ${repo}. ` +
|
|
109
|
+
"Check that the repository exists and is public.");
|
|
110
|
+
}
|
|
111
|
+
if (response.status === 403) {
|
|
112
|
+
return cleanFail("RateLimited", "GitHub API Rate Limit Exceeded. Try again later.");
|
|
113
|
+
}
|
|
114
|
+
return cleanFail("HttpError", `GitHub API Returned HTTP ${response.status} for ${repo}`);
|
|
115
|
+
}
|
|
116
|
+
// Write tarball to disk
|
|
117
|
+
const tarballPath = join(tempDir, "template.tar.gz");
|
|
118
|
+
const tarball = new Uint8Array(await response.arrayBuffer());
|
|
119
|
+
dntShim.Deno.writeFileSync(tarballPath, tarball);
|
|
120
|
+
// Extract
|
|
121
|
+
const extractDir = join(tempDir, "extract");
|
|
122
|
+
dntShim.Deno.mkdirSync(extractDir);
|
|
123
|
+
const extractResult = await runCommand([
|
|
124
|
+
"tar",
|
|
125
|
+
"xzf",
|
|
126
|
+
tarballPath,
|
|
127
|
+
"-C",
|
|
128
|
+
extractDir,
|
|
129
|
+
]);
|
|
130
|
+
if (!extractResult.success) {
|
|
131
|
+
return cleanFail("ExtractionFailed", "Failed to Extract Template Archive");
|
|
132
|
+
}
|
|
133
|
+
// GitHub tarballs have a single top-level dir (owner-repo-commitish/)
|
|
134
|
+
const entries = [...dntShim.Deno.readDirSync(extractDir)];
|
|
135
|
+
const topLevel = entries.find((e) => e.isDirectory);
|
|
136
|
+
if (!topLevel) {
|
|
137
|
+
return cleanFail("EmptyArchive", "Template Archive Has No Top-Level Directory");
|
|
138
|
+
}
|
|
139
|
+
// Locate the template subdirectory
|
|
140
|
+
const templateDir = join(extractDir, topLevel.name, ref.path);
|
|
141
|
+
try {
|
|
142
|
+
const stat = dntShim.Deno.statSync(templateDir);
|
|
143
|
+
if (!stat.isDirectory) {
|
|
144
|
+
return cleanFail("PathNotDirectory", `Template Path '${ref.path}' Is Not a Directory in ${formatRepo(ref)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return cleanFail("PathNotFound", `Template Path '${ref.path}' Not Found in ${formatRepo(ref)}`);
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, tempDir, templateDir };
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (e instanceof TypeError) {
|
|
154
|
+
return cleanFail("NetworkError", "Network Error: Could Not Reach GitHub");
|
|
155
|
+
}
|
|
156
|
+
cleanup();
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardelli/ambit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Deploy apps to the cloud that only you and your AI agents can reach",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "./esm/src/providers/fly.js",
|
|
7
7
|
"exports": {
|