@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 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,8 @@
1
+ export type Result<T, K extends string = string> = ({
2
+ ok: true;
3
+ } & T) | {
4
+ ok: false;
5
+ kind: K;
6
+ message: string;
7
+ };
8
+ //# sourceMappingURL=result.d.ts.map
@@ -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 network = args._[0];
54
- if (!network) {
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: ["network", "org", "region", "image", "config"],
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> Uses ./fly.toml
32
- ambit deploy <app> --network <name> --config path Explicit fly.toml
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> Docker image, no toml
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> 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
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 app = args._[0];
63
- if (!app) {
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
- if (args.image && args.config) {
76
- return out.die("--image and --config Are Mutually Exclusive");
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: Pre-flight TOML Scan
300
+ // Phase 4: Resolve Deploy Mode
132
301
  // ==========================================================================
133
302
  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
- }
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
- out.info("Image Mode Skipping TOML Scan");
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: args.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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAQD,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,IAElD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAO,GAAG,SAEnD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,OAAO,EAExC,CAAC;AAQF,eAAO,MAAM,QAAQ,QAAO,IA4B3B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAC;AAMF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAuCzD,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../src/src/cli/mod.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAQD,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,IAElD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAO,GAAG,SAEnD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,OAAO,EAExC,CAAC;AAQF,eAAO,MAAM,QAAQ,QAAO,IA4B3B,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,IAE9B,CAAC;AAMF,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAuCzD,CAAC"}
@@ -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 = "0.3.0";
24
+ const VERSION = denoConfig.version;
24
25
  export const showHelp = () => {
25
26
  console.log(`
26
27
  ${bold("ambit")} - Tailscale Subnet Router for Fly.io Custom Networks
@@ -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,MACvB,CAAC;AAEzB,eAAO,MAAM,aAAa,GAAI,WAAW,MAAM,KAAG,MAKjD,CAAC;AAMF,eAAO,MAAM,YAAY,QAAO,MAG/B,CAAC"}
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.1",
4
- "description": "Tailscale subnet router manager for Fly.io custom networks",
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": {