@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 +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/deploy.js +106 -13
- package/esm/src/cli/mod.d.ts.map +1 -1
- package/esm/src/cli/mod.js +2 -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 {};
|
|
@@ -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: [
|
|
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>
|
|
113
|
-
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
|
|
114
181
|
|
|
115
182
|
Image mode:
|
|
116
|
-
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
});
|
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
|
|
@@ -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": {
|