@cardelli/ambit 0.1.2 → 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 {};
@@ -11,6 +11,7 @@ import { createFlyProvider, FlyDeployError } from "../../providers/fly.js";
11
11
  import { findRouterApp } from "../../discovery.js";
12
12
  import { resolveOrg } from "../../resolve.js";
13
13
  import { assertNotRouter, auditDeploy, scanFlyToml } from "../../guard.js";
14
+ import { fetchTemplate, parseTemplateRef } from "../../template.js";
14
15
  // =============================================================================
15
16
  // Image Mode
16
17
  // =============================================================================
@@ -91,11 +92,77 @@ const resolveConfigMode = async (explicitConfig, out) => {
91
92
  };
92
93
  };
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
+ };
152
+ // =============================================================================
94
153
  // Deploy Command
95
154
  // =============================================================================
96
155
  const deploy = async (argv) => {
97
156
  const args = parseArgs(argv, {
98
- string: ["network", "org", "region", "image", "config", "main-port"],
157
+ string: [
158
+ "network",
159
+ "org",
160
+ "region",
161
+ "image",
162
+ "config",
163
+ "main-port",
164
+ "template",
165
+ ],
99
166
  boolean: ["help", "yes", "json"],
100
167
  alias: { y: "yes" },
101
168
  default: { "main-port": "80" },
@@ -109,22 +176,38 @@ ${bold("USAGE")}
109
176
 
110
177
  ${bold("MODES")}
111
178
  Config mode (default):
112
- ambit deploy <app> --network <name> Uses ./fly.toml
113
- 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
114
181
 
115
182
  Image mode:
116
- 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
117
187
 
118
188
  ${bold("OPTIONS")}
119
189
  --network <name> Custom 6PN network to target (required)
120
190
  --org <org> Fly.io organization slug
121
191
  --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
192
  -y, --yes Skip confirmation prompts
126
193
  --json Output as JSON
127
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
210
+
128
211
  ${bold("SAFETY")}
129
212
  Always deploys with --no-public-ips and --flycast.
130
213
  Post-deploy audit releases any public IPs and verifies Flycast allocation.
@@ -134,6 +217,8 @@ ${bold("EXAMPLES")}
134
217
  ambit deploy my-app --network browsers
135
218
  ambit deploy my-app --network browsers --image registry/img:latest
136
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
137
222
  `);
138
223
  return;
139
224
  }
@@ -155,8 +240,9 @@ ${bold("EXAMPLES")}
155
240
  catch (e) {
156
241
  return out.die(e instanceof Error ? e.message : String(e));
157
242
  }
158
- if (args.image && args.config) {
159
- 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");
160
246
  }
161
247
  const network = args.network;
162
248
  out.blank()
@@ -214,9 +300,16 @@ ${bold("EXAMPLES")}
214
300
  // Phase 4: Resolve Deploy Mode
215
301
  // ==========================================================================
216
302
  out.header("Step 4: Pre-flight Check").blank();
217
- const deployConfig = args.image
218
- ? resolveImageMode(args.image, String(args["main-port"]), out)
219
- : await resolveConfigMode(args.config, out);
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);
309
+ }
310
+ else {
311
+ deployConfig = await resolveConfigMode(args.config, out);
312
+ }
220
313
  if (!deployConfig)
221
314
  return; // mode resolver already called out.die()
222
315
  out.blank();
@@ -306,6 +399,6 @@ ${bold("EXAMPLES")}
306
399
  registerCommand({
307
400
  name: "deploy",
308
401
  description: "Deploy an app safely on a custom private network",
309
- 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>]",
310
403
  run: deploy,
311
404
  });
@@ -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
@@ -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.2",
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": {