@cardelli/ambit 0.1.4 → 0.2.0
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/cli/commands/create/index.d.ts +2 -0
- package/esm/cli/commands/create/index.d.ts.map +1 -0
- package/esm/cli/commands/create/index.js +292 -0
- package/esm/cli/commands/create/machine.d.ts +33 -0
- package/esm/cli/commands/create/machine.d.ts.map +1 -0
- package/esm/cli/commands/create/machine.js +162 -0
- package/esm/cli/commands/deploy/index.d.ts +2 -0
- package/esm/cli/commands/deploy/index.d.ts.map +1 -0
- package/esm/cli/commands/deploy/index.js +290 -0
- package/esm/cli/commands/deploy/machine.d.ts +52 -0
- package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
- package/esm/cli/commands/deploy/machine.js +116 -0
- package/esm/cli/commands/deploy/modes.d.ts +18 -0
- package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
- package/esm/cli/commands/deploy/modes.js +152 -0
- package/esm/cli/commands/destroy/app.d.ts +2 -0
- package/esm/cli/commands/destroy/app.d.ts.map +1 -0
- package/esm/cli/commands/destroy/app.js +173 -0
- package/esm/cli/commands/destroy/index.d.ts +2 -0
- package/esm/cli/commands/destroy/index.d.ts.map +1 -0
- package/esm/cli/commands/destroy/index.js +63 -0
- package/esm/cli/commands/destroy/network.d.ts +2 -0
- package/esm/cli/commands/destroy/network.d.ts.map +1 -0
- package/esm/cli/commands/destroy/network.js +210 -0
- package/esm/cli/commands/doctor.d.ts.map +1 -0
- package/esm/cli/commands/doctor.js +295 -0
- package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
- package/esm/{src/cli → cli}/commands/list.js +39 -54
- package/esm/cli/commands/status.d.ts.map +1 -0
- package/esm/cli/commands/status.js +331 -0
- package/esm/cli/mod.d.ts.map +1 -0
- package/esm/{src/cli → cli}/mod.js +4 -4
- package/esm/deno.d.ts +4 -18
- package/esm/deno.js +5 -19
- package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
- package/esm/deps/jsr.io/@zod/zod/4.3.6/src/v4/core/json-schema-generator.d.ts +1 -1
- package/esm/lib/args.d.ts +11 -0
- package/esm/lib/args.d.ts.map +1 -0
- package/esm/lib/args.js +28 -0
- package/esm/lib/cli.d.ts +0 -2
- package/esm/lib/cli.d.ts.map +1 -1
- package/esm/lib/cli.js +41 -27
- package/esm/lib/command.d.ts +21 -49
- package/esm/lib/command.d.ts.map +1 -1
- package/esm/lib/command.js +55 -95
- package/esm/lib/machine.d.ts +11 -0
- package/esm/lib/machine.d.ts.map +1 -0
- package/esm/lib/machine.js +15 -0
- package/esm/lib/output.d.ts +3 -2
- package/esm/lib/output.d.ts.map +1 -1
- package/esm/lib/output.js +25 -11
- package/esm/lib/result.d.ts +18 -7
- package/esm/lib/result.d.ts.map +1 -1
- package/esm/lib/result.js +46 -1
- package/esm/main.d.ts +6 -6
- package/esm/main.d.ts.map +1 -1
- package/esm/main.js +7 -9
- package/esm/providers/fly.d.ts +81 -0
- package/esm/providers/fly.d.ts.map +1 -0
- package/esm/providers/fly.js +372 -0
- package/esm/providers/tailscale.d.ts +31 -0
- package/esm/providers/tailscale.d.ts.map +1 -0
- package/esm/providers/tailscale.js +150 -0
- package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
- package/esm/schemas/fly.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/fly.js +14 -56
- package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
- package/esm/schemas/tailscale.d.ts.map +1 -0
- package/esm/{src/schemas → schemas}/tailscale.js +2 -3
- package/esm/src/{docker/router → router}/Dockerfile +0 -11
- package/esm/src/router/start.sh +101 -0
- package/esm/util/constants.d.ts +13 -0
- package/esm/util/constants.d.ts.map +1 -0
- package/esm/util/constants.js +34 -0
- package/esm/{src → util}/credentials.d.ts +0 -1
- package/esm/util/credentials.d.ts.map +1 -0
- package/esm/{src → util}/credentials.js +3 -5
- package/esm/{src → util}/discovery.d.ts +20 -4
- package/esm/util/discovery.d.ts.map +1 -0
- package/esm/{src → util}/discovery.js +38 -15
- package/esm/util/fly-transforms.d.ts +27 -0
- package/esm/util/fly-transforms.d.ts.map +1 -0
- package/esm/util/fly-transforms.js +87 -0
- package/esm/{src → util}/guard.d.ts +1 -2
- package/esm/util/guard.d.ts.map +1 -0
- package/esm/{src → util}/guard.js +27 -27
- package/esm/util/naming.d.ts +5 -0
- package/esm/util/naming.d.ts.map +1 -0
- package/esm/util/naming.js +12 -0
- package/esm/{src → util}/resolve.d.ts +2 -3
- package/esm/util/resolve.d.ts.map +1 -0
- package/esm/{src → util}/resolve.js +1 -2
- package/esm/util/session.d.ts +16 -0
- package/esm/util/session.d.ts.map +1 -0
- package/esm/util/session.js +19 -0
- package/esm/util/tailscale-local.d.ts +13 -0
- package/esm/util/tailscale-local.d.ts.map +1 -0
- package/esm/util/tailscale-local.js +63 -0
- package/esm/{src → util}/template.d.ts +2 -4
- package/esm/util/template.d.ts.map +1 -0
- package/esm/{src → util}/template.js +14 -17
- package/package.json +1 -43
- package/esm/lib/paths.d.ts +0 -3
- package/esm/lib/paths.d.ts.map +0 -1
- package/esm/lib/paths.js +0 -5
- package/esm/src/cli/commands/create.d.ts +0 -2
- package/esm/src/cli/commands/create.d.ts.map +0 -1
- package/esm/src/cli/commands/create.js +0 -294
- package/esm/src/cli/commands/deploy.d.ts +0 -2
- package/esm/src/cli/commands/deploy.d.ts.map +0 -1
- package/esm/src/cli/commands/deploy.js +0 -426
- package/esm/src/cli/commands/destroy.d.ts +0 -2
- package/esm/src/cli/commands/destroy.d.ts.map +0 -1
- package/esm/src/cli/commands/destroy.js +0 -340
- package/esm/src/cli/commands/doctor.d.ts.map +0 -1
- package/esm/src/cli/commands/doctor.js +0 -141
- package/esm/src/cli/commands/status.d.ts.map +0 -1
- package/esm/src/cli/commands/status.js +0 -152
- package/esm/src/cli/mod.d.ts.map +0 -1
- package/esm/src/credentials.d.ts.map +0 -1
- package/esm/src/discovery.d.ts.map +0 -1
- package/esm/src/docker/router/start.sh +0 -146
- package/esm/src/guard.d.ts.map +0 -1
- package/esm/src/providers/fly.d.ts +0 -70
- package/esm/src/providers/fly.d.ts.map +0 -1
- package/esm/src/providers/fly.js +0 -411
- package/esm/src/providers/tailscale.d.ts +0 -31
- package/esm/src/providers/tailscale.d.ts.map +0 -1
- package/esm/src/providers/tailscale.js +0 -195
- package/esm/src/resolve.d.ts.map +0 -1
- package/esm/src/schemas/config.d.ts +0 -5
- package/esm/src/schemas/config.d.ts.map +0 -1
- package/esm/src/schemas/config.js +0 -22
- package/esm/src/schemas/fly.d.ts.map +0 -1
- package/esm/src/schemas/tailscale.d.ts.map +0 -1
- package/esm/src/template.d.ts.map +0 -1
- /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
- /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
- /package/esm/{src/cli → cli}/mod.d.ts +0 -0
- /package/esm/src/{docker/router → router}/fly.toml +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Fly.io Provider - Wraps flyctl CLI
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
5
|
+
import { runCommand, runJson } from "../lib/command.js";
|
|
6
|
+
import { Result } from "../lib/result.js";
|
|
7
|
+
import { commandExists, die } from "../lib/cli.js";
|
|
8
|
+
import { dirname, resolve } from "../deps/jsr.io/@std/path/1.1.4/mod.js";
|
|
9
|
+
import { FlyAppInfoListSchema, FlyAppsListSchema, FlyAuthSchema, FlyIpListSchema, FlyMachinesListSchema, FlyOrgsSchema, FlyStatusSchema, } from "../schemas/fly.js";
|
|
10
|
+
import { fileExists } from "../lib/cli.js";
|
|
11
|
+
import { getWorkloadAppName } from "../util/naming.js";
|
|
12
|
+
import { extractErrorDetail, getSizeConfig, mapMachines } from "../util/fly-transforms.js";
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Deploy Error
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a `fly deploy` command fails. Carries the raw stderr so callers
|
|
18
|
+
* can surface it through `out` (respecting JSON mode) instead of printing
|
|
19
|
+
* directly.
|
|
20
|
+
*/
|
|
21
|
+
export class FlyDeployError extends Error {
|
|
22
|
+
/** Last meaningful line from flyctl stderr. */
|
|
23
|
+
detail;
|
|
24
|
+
constructor(app, stderr) {
|
|
25
|
+
const detail = extractErrorDetail(stderr);
|
|
26
|
+
super(`Deploy Failed for '${app}'`);
|
|
27
|
+
this.name = "FlyDeployError";
|
|
28
|
+
this.detail = detail;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Create Fly Provider
|
|
33
|
+
// =============================================================================
|
|
34
|
+
export const createFlyProvider = () => {
|
|
35
|
+
const provider = {
|
|
36
|
+
auth: {
|
|
37
|
+
async ensureInstalled() {
|
|
38
|
+
if (!(await commandExists("fly"))) {
|
|
39
|
+
return die("Flyctl Not Found. Install from https://fly.io/docs/flyctl/install/");
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async login(opts) {
|
|
43
|
+
const interactive = opts?.interactive ?? true;
|
|
44
|
+
const result = await runCommand(["fly", "auth", "whoami", "--json"]);
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
const auth = result.json();
|
|
47
|
+
if (auth.ok) {
|
|
48
|
+
const parsed = FlyAuthSchema.safeParse(auth.value);
|
|
49
|
+
if (parsed.success) {
|
|
50
|
+
return parsed.data.email;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!interactive) {
|
|
55
|
+
return die("Not Authenticated with Fly.io. Run 'fly auth login' First");
|
|
56
|
+
}
|
|
57
|
+
const loginResult = await runCommand(["fly", "auth", "login"], {
|
|
58
|
+
interactive: true,
|
|
59
|
+
});
|
|
60
|
+
if (!loginResult.ok) {
|
|
61
|
+
return die("Fly.io Authentication Failed");
|
|
62
|
+
}
|
|
63
|
+
const checkResult = await runCommand(["fly", "auth", "whoami", "--json"]);
|
|
64
|
+
if (!checkResult.ok) {
|
|
65
|
+
return die("Fly.io Authentication Verification Failed");
|
|
66
|
+
}
|
|
67
|
+
const parsed = FlyAuthSchema.safeParse(checkResult.json().unwrap());
|
|
68
|
+
if (!parsed.success || !parsed.data) {
|
|
69
|
+
return die("Fly.io Authentication Response Invalid");
|
|
70
|
+
}
|
|
71
|
+
return parsed.data.email;
|
|
72
|
+
},
|
|
73
|
+
async getToken() {
|
|
74
|
+
const home = dntShim.Deno.env.get("HOME") || dntShim.Deno.env.get("USERPROFILE") || "";
|
|
75
|
+
const configPath = `${home}/.fly/config.yml`;
|
|
76
|
+
if (!(await fileExists(configPath))) {
|
|
77
|
+
return die("Fly Config Not Found at ~/.fly/config.yml. Run 'fly auth login' First");
|
|
78
|
+
}
|
|
79
|
+
const content = await dntShim.Deno.readTextFile(configPath);
|
|
80
|
+
const match = content.match(/access_token:\s*(.+)/);
|
|
81
|
+
if (!match || !match[1]) {
|
|
82
|
+
return die("No Access Token Found in ~/.fly/config.yml. Run 'fly auth login' First");
|
|
83
|
+
}
|
|
84
|
+
return match[1].trim();
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
orgs: {
|
|
88
|
+
async list() {
|
|
89
|
+
const result = await runJson(["fly", "orgs", "list", "--json"]);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
return die("Failed to List Organizations");
|
|
92
|
+
}
|
|
93
|
+
const parsed = FlyOrgsSchema.safeParse(result.value);
|
|
94
|
+
if (!parsed.success) {
|
|
95
|
+
return die("Failed to Parse Organizations");
|
|
96
|
+
}
|
|
97
|
+
return parsed.data;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
apps: {
|
|
101
|
+
async list(org) {
|
|
102
|
+
const args = ["fly", "apps", "list", "--json"];
|
|
103
|
+
if (org) {
|
|
104
|
+
args.push("--org", org);
|
|
105
|
+
}
|
|
106
|
+
const result = await runCommand(args);
|
|
107
|
+
return result.json().flatMap((data) => {
|
|
108
|
+
const parsed = FlyAppsListSchema.safeParse(data);
|
|
109
|
+
return parsed.success
|
|
110
|
+
? Result.ok(parsed.data)
|
|
111
|
+
: Result.err("Parse Failed");
|
|
112
|
+
}).unwrapOr([]);
|
|
113
|
+
},
|
|
114
|
+
async listWithNetwork(org) {
|
|
115
|
+
const token = await provider.auth.getToken();
|
|
116
|
+
const response = await fetch(`https://api.machines.dev/v1/apps?org_slug=${encodeURIComponent(org)}`, {
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${token}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
return die(`Failed to List Apps via REST API: HTTP ${response.status}`);
|
|
124
|
+
}
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
const parsed = FlyAppInfoListSchema.safeParse(data);
|
|
127
|
+
if (!parsed.success) {
|
|
128
|
+
return die("Failed to Parse Apps REST API Response");
|
|
129
|
+
}
|
|
130
|
+
return parsed.data.apps;
|
|
131
|
+
},
|
|
132
|
+
async create(name, org, opts) {
|
|
133
|
+
const appName = opts?.routerId
|
|
134
|
+
? getWorkloadAppName(name, opts.routerId)
|
|
135
|
+
: name;
|
|
136
|
+
const args = ["fly", "apps", "create", appName, "--org", org, "--json"];
|
|
137
|
+
if (opts?.network) {
|
|
138
|
+
args.push("--network", opts.network);
|
|
139
|
+
}
|
|
140
|
+
const result = await runCommand(args);
|
|
141
|
+
if (!result.ok) {
|
|
142
|
+
return die(`Failed to Create App '${appName}'`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
async delete(name) {
|
|
146
|
+
const result = await runCommand([
|
|
147
|
+
"fly",
|
|
148
|
+
"apps",
|
|
149
|
+
"destroy",
|
|
150
|
+
name,
|
|
151
|
+
"--yes",
|
|
152
|
+
]);
|
|
153
|
+
if (!result.ok) {
|
|
154
|
+
return die(`Failed to Delete App '${name}'`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async exists(name) {
|
|
158
|
+
const result = await runCommand(["fly", "status", "-a", name, "--json"]);
|
|
159
|
+
return result.json().match({
|
|
160
|
+
ok: (data) => {
|
|
161
|
+
const parsed = FlyStatusSchema.safeParse(data);
|
|
162
|
+
return parsed.success && !!parsed.data.ID;
|
|
163
|
+
},
|
|
164
|
+
err: () => false,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
async getConfig(name) {
|
|
168
|
+
const result = await runCommand(["fly", "config", "show", "-a", name]);
|
|
169
|
+
return result.json().match({
|
|
170
|
+
ok: (v) => v,
|
|
171
|
+
err: () => null,
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
machines: {
|
|
176
|
+
async list(app) {
|
|
177
|
+
const result = await runJson(["fly", "machines", "list", "-a", app, "--json"]);
|
|
178
|
+
return result.flatMap((data) => {
|
|
179
|
+
const parsed = FlyMachinesListSchema.safeParse(data);
|
|
180
|
+
return parsed.success
|
|
181
|
+
? Result.ok(parsed.data)
|
|
182
|
+
: Result.err("Parse Failed");
|
|
183
|
+
}).unwrapOr([]);
|
|
184
|
+
},
|
|
185
|
+
async clone(app, config) {
|
|
186
|
+
const raw = await provider.machines.list(app);
|
|
187
|
+
const existingMachines = mapMachines(raw);
|
|
188
|
+
if (existingMachines.length === 0) {
|
|
189
|
+
return die("No Existing Machine to Clone. Run 'fly deploy' First");
|
|
190
|
+
}
|
|
191
|
+
const sourceMachine = existingMachines[0];
|
|
192
|
+
const sizeConfig = getSizeConfig(config.size);
|
|
193
|
+
const memoryMb = config.memoryMb ?? sizeConfig.memoryMb;
|
|
194
|
+
const args = [
|
|
195
|
+
"fly",
|
|
196
|
+
"machine",
|
|
197
|
+
"clone",
|
|
198
|
+
sourceMachine.id,
|
|
199
|
+
"-a",
|
|
200
|
+
app,
|
|
201
|
+
"--vm-cpus",
|
|
202
|
+
String(sizeConfig.cpus),
|
|
203
|
+
"--vm-memory",
|
|
204
|
+
String(memoryMb),
|
|
205
|
+
];
|
|
206
|
+
if (config.region) {
|
|
207
|
+
args.push("--region", config.region);
|
|
208
|
+
}
|
|
209
|
+
const result = await runCommand(args);
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
return die(result.stderr || "Unknown Error");
|
|
212
|
+
}
|
|
213
|
+
const rawAfter = await provider.machines.list(app);
|
|
214
|
+
const machines = mapMachines(rawAfter);
|
|
215
|
+
const newest = machines[machines.length - 1];
|
|
216
|
+
if (!newest) {
|
|
217
|
+
return die("Created Machine Not Found");
|
|
218
|
+
}
|
|
219
|
+
return newest;
|
|
220
|
+
},
|
|
221
|
+
async destroy(app, machineId) {
|
|
222
|
+
const shortId = machineId.slice(0, 8);
|
|
223
|
+
const result = await runCommand([
|
|
224
|
+
"fly",
|
|
225
|
+
"machines",
|
|
226
|
+
"destroy",
|
|
227
|
+
machineId,
|
|
228
|
+
"-a",
|
|
229
|
+
app,
|
|
230
|
+
"--force",
|
|
231
|
+
]);
|
|
232
|
+
if (!result.ok) {
|
|
233
|
+
return die(`Failed to Destroy Machine '${shortId}'`);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
secrets: {
|
|
238
|
+
async set(app, secrets, opts) {
|
|
239
|
+
const pairs = Object.entries(secrets)
|
|
240
|
+
.filter(([_, v]) => v !== undefined && v !== "")
|
|
241
|
+
.map(([k, v]) => `${k}=${v}`);
|
|
242
|
+
if (pairs.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
const args = ["fly", "secrets", "set", ...pairs, "-a", app];
|
|
245
|
+
if (opts?.stage) {
|
|
246
|
+
args.push("--stage");
|
|
247
|
+
}
|
|
248
|
+
const result = await runCommand(args);
|
|
249
|
+
if (!result.ok) {
|
|
250
|
+
return die("Failed to Set Secrets");
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
ips: {
|
|
255
|
+
async list(app) {
|
|
256
|
+
const result = await runCommand([
|
|
257
|
+
"fly",
|
|
258
|
+
"ips",
|
|
259
|
+
"list",
|
|
260
|
+
"-a",
|
|
261
|
+
app,
|
|
262
|
+
"--json",
|
|
263
|
+
]);
|
|
264
|
+
return result.json().flatMap((data) => {
|
|
265
|
+
const parsed = FlyIpListSchema.safeParse(data);
|
|
266
|
+
return parsed.success
|
|
267
|
+
? Result.ok(parsed.data)
|
|
268
|
+
: Result.err("Parse Failed");
|
|
269
|
+
}).unwrapOr([]);
|
|
270
|
+
},
|
|
271
|
+
async release(app, address) {
|
|
272
|
+
const result = await runCommand([
|
|
273
|
+
"fly",
|
|
274
|
+
"ips",
|
|
275
|
+
"release",
|
|
276
|
+
address,
|
|
277
|
+
"-a",
|
|
278
|
+
app,
|
|
279
|
+
]);
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
return die(`Failed to Release IP ${address} from '${app}'`);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
async allocateFlycast(app, network) {
|
|
285
|
+
const result = await runCommand([
|
|
286
|
+
"fly",
|
|
287
|
+
"ips",
|
|
288
|
+
"allocate-v6",
|
|
289
|
+
"--private",
|
|
290
|
+
"--network",
|
|
291
|
+
network,
|
|
292
|
+
"-a",
|
|
293
|
+
app,
|
|
294
|
+
]);
|
|
295
|
+
if (!result.ok) {
|
|
296
|
+
return die(`Failed to Allocate Flycast IP on Network '${network}'`);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
certs: {
|
|
301
|
+
async list(app) {
|
|
302
|
+
const result = await runCommand([
|
|
303
|
+
"fly",
|
|
304
|
+
"certs",
|
|
305
|
+
"list",
|
|
306
|
+
"-a",
|
|
307
|
+
app,
|
|
308
|
+
"--json",
|
|
309
|
+
]);
|
|
310
|
+
return result.json().map((certs) => certs
|
|
311
|
+
.map((c) => c.Hostname)
|
|
312
|
+
.filter((h) => typeof h === "string")).unwrapOr([]);
|
|
313
|
+
},
|
|
314
|
+
async remove(app, hostname) {
|
|
315
|
+
await runCommand([
|
|
316
|
+
"fly",
|
|
317
|
+
"certs",
|
|
318
|
+
"remove",
|
|
319
|
+
hostname,
|
|
320
|
+
"-a",
|
|
321
|
+
app,
|
|
322
|
+
"--yes",
|
|
323
|
+
]);
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
deploy: {
|
|
327
|
+
async router(app, dockerDir, config) {
|
|
328
|
+
const args = [
|
|
329
|
+
"fly",
|
|
330
|
+
"deploy",
|
|
331
|
+
dockerDir,
|
|
332
|
+
"-a",
|
|
333
|
+
app,
|
|
334
|
+
"--yes",
|
|
335
|
+
"--ha=false",
|
|
336
|
+
];
|
|
337
|
+
if (config?.region) {
|
|
338
|
+
args.push("--primary-region", config.region);
|
|
339
|
+
}
|
|
340
|
+
const result = await runCommand(args);
|
|
341
|
+
if (!result.ok) {
|
|
342
|
+
throw new FlyDeployError(app, result.stderr);
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
async app(appName, options) {
|
|
346
|
+
const flyAppName = options.routerId
|
|
347
|
+
? getWorkloadAppName(appName, options.routerId)
|
|
348
|
+
: appName;
|
|
349
|
+
const args = ["fly", "deploy"];
|
|
350
|
+
// When config is provided, use its parent directory as the build context
|
|
351
|
+
// so fly deploy finds the Dockerfile and COPY picks up the correct files
|
|
352
|
+
if (options.config) {
|
|
353
|
+
const configAbs = resolve(options.config);
|
|
354
|
+
args.push(dirname(configAbs));
|
|
355
|
+
args.push("--config", configAbs);
|
|
356
|
+
}
|
|
357
|
+
args.push("-a", flyAppName, "--yes", "--no-public-ips");
|
|
358
|
+
if (options.image) {
|
|
359
|
+
args.push("--image", options.image);
|
|
360
|
+
}
|
|
361
|
+
if (options.region) {
|
|
362
|
+
args.push("--primary-region", options.region);
|
|
363
|
+
}
|
|
364
|
+
const result = await runCommand(args);
|
|
365
|
+
if (!result.ok) {
|
|
366
|
+
throw new FlyDeployError(flyAppName, result.stderr);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
return provider;
|
|
372
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type AuthKeyCapabilities, type TailscaleDevice } from "../schemas/tailscale.js";
|
|
2
|
+
export interface DeviceRoutes {
|
|
3
|
+
advertised: string[];
|
|
4
|
+
enabled: string[];
|
|
5
|
+
unapproved: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface TailscaleProvider {
|
|
8
|
+
auth: {
|
|
9
|
+
validateKey(): Promise<boolean>;
|
|
10
|
+
createKey(opts?: AuthKeyCapabilities): Promise<string>;
|
|
11
|
+
};
|
|
12
|
+
devices: {
|
|
13
|
+
list(): Promise<TailscaleDevice[]>;
|
|
14
|
+
getByHostname(hostname: string): Promise<TailscaleDevice | null>;
|
|
15
|
+
delete(id: string): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
routes: {
|
|
18
|
+
get(deviceId: string): Promise<DeviceRoutes | null>;
|
|
19
|
+
approve(deviceId: string, routes: string[]): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
dns: {
|
|
22
|
+
getSplit(): Promise<Record<string, string[]>>;
|
|
23
|
+
setSplit(domain: string, nameservers: string[]): Promise<void>;
|
|
24
|
+
clearSplit(domain: string): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
acl: {
|
|
27
|
+
getPolicy(): Promise<Record<string, unknown> | null>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare const createTailscaleProvider: (apiKey: string, tailnet?: string) => TailscaleProvider;
|
|
31
|
+
//# sourceMappingURL=tailscale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailscale.d.ts","sourceRoot":"","sources":["../../src/providers/tailscale.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAYjC,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE;QACJ,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;KACxD,CAAC;IACF,OAAO,EAAE;QACP,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;QACnC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;QACjE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnC,CAAC;IACF,MAAM,EAAE;QACN,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;QACpD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5D,CAAC;IACF,GAAG,EAAE;QACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/D,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAC3C,CAAC;IACF,GAAG,EAAE;QACH,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;KACtD,CAAC;CACH;AAiBD,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,EACd,UAAS,MAAY,KACpB,iBAyMF,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Tailscale API Client
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { die } from "../lib/cli.js";
|
|
5
|
+
import { createAuthKeyPayload, TailscaleDevicesListSchema, } from "../schemas/tailscale.js";
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Constants
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const API_BASE = "https://api.tailscale.com/api/v2";
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Create Tailscale Provider
|
|
12
|
+
// =============================================================================
|
|
13
|
+
export const createTailscaleProvider = (apiKey, tailnet = "-") => {
|
|
14
|
+
const headers = () => ({
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
Authorization: `Basic ${btoa(apiKey + ":")}`,
|
|
18
|
+
});
|
|
19
|
+
const request = async (method, path, body) => {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`${API_BASE}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
headers: headers(),
|
|
24
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
status: response.status,
|
|
31
|
+
error: text || `HTTP ${response.status}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
if (!text) {
|
|
36
|
+
return { ok: true, status: response.status };
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, status: response.status, data: JSON.parse(text) };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 0,
|
|
44
|
+
error: error instanceof Error ? error.message : String(error),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const provider = {
|
|
49
|
+
auth: {
|
|
50
|
+
async validateKey() {
|
|
51
|
+
const result = await request("GET", `/tailnet/${tailnet}/devices`);
|
|
52
|
+
return result.ok;
|
|
53
|
+
},
|
|
54
|
+
async createKey(opts) {
|
|
55
|
+
const payload = createAuthKeyPayload(opts ?? {});
|
|
56
|
+
const result = await request("POST", `/tailnet/${tailnet}/keys`, payload);
|
|
57
|
+
if (!result.ok || !result.data?.key) {
|
|
58
|
+
return die(`Failed to Create Auth Key: ${result.error}`);
|
|
59
|
+
}
|
|
60
|
+
return result.data.key;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
devices: {
|
|
64
|
+
async list() {
|
|
65
|
+
const result = await request("GET", `/tailnet/${tailnet}/devices`);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
return die(`Failed to List Devices: ${result.error}`);
|
|
68
|
+
}
|
|
69
|
+
const parsed = TailscaleDevicesListSchema.safeParse(result.data);
|
|
70
|
+
return parsed.success ? parsed.data.devices : [];
|
|
71
|
+
},
|
|
72
|
+
async getByHostname(hostname) {
|
|
73
|
+
const devices = await provider.devices.list();
|
|
74
|
+
const exact = devices.find((d) => d.hostname === hostname);
|
|
75
|
+
if (exact)
|
|
76
|
+
return exact;
|
|
77
|
+
// Fallback: find by prefix if Tailscale added suffix (e.g., hostname-1)
|
|
78
|
+
// Prefer online devices, then most recently seen
|
|
79
|
+
const prefixMatches = devices
|
|
80
|
+
.filter((d) => d.hostname.startsWith(hostname + "-"))
|
|
81
|
+
.sort((a, b) => {
|
|
82
|
+
if (a.online && !b.online)
|
|
83
|
+
return -1;
|
|
84
|
+
if (!a.online && b.online)
|
|
85
|
+
return 1;
|
|
86
|
+
const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
|
|
87
|
+
const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
|
|
88
|
+
return bTime - aTime;
|
|
89
|
+
});
|
|
90
|
+
return prefixMatches[0] ?? null;
|
|
91
|
+
},
|
|
92
|
+
async delete(id) {
|
|
93
|
+
const result = await request("DELETE", `/device/${id}`);
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
return die(`Failed to Delete Device: ${result.error}`);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
routes: {
|
|
100
|
+
async get(deviceId) {
|
|
101
|
+
const result = await request("GET", `/device/${deviceId}/routes`);
|
|
102
|
+
if (!result.ok || !result.data)
|
|
103
|
+
return null;
|
|
104
|
+
const advertised = result.data.advertisedRoutes ?? [];
|
|
105
|
+
const enabled = result.data.enabledRoutes ?? [];
|
|
106
|
+
const enabledSet = new Set(enabled);
|
|
107
|
+
return {
|
|
108
|
+
advertised,
|
|
109
|
+
enabled,
|
|
110
|
+
unapproved: advertised.filter((r) => !enabledSet.has(r)),
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
async approve(deviceId, routes) {
|
|
114
|
+
const result = await request("POST", `/device/${deviceId}/routes`, { routes });
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
return die(`Failed to Approve Routes: ${result.error}`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
dns: {
|
|
121
|
+
async getSplit() {
|
|
122
|
+
const result = await request("GET", `/tailnet/${tailnet}/dns/split-dns`);
|
|
123
|
+
return result.ok && result.data ? result.data : {};
|
|
124
|
+
},
|
|
125
|
+
async setSplit(domain, nameservers) {
|
|
126
|
+
// PATCH performs partial update - only specified domains are modified
|
|
127
|
+
const result = await request("PATCH", `/tailnet/${tailnet}/dns/split-dns`, { [domain]: nameservers });
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
return die(`Failed to Configure Split DNS: ${result.error}`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async clearSplit(domain) {
|
|
133
|
+
const result = await request("PATCH", `/tailnet/${tailnet}/dns/split-dns`, { [domain]: null });
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
return die(`Failed to Clear Split DNS: ${result.error}`);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
acl: {
|
|
140
|
+
async getPolicy() {
|
|
141
|
+
const result = await request("GET", `/tailnet/${tailnet}/acl`);
|
|
142
|
+
if (!result.ok || !result.data) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return result.data;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
return provider;
|
|
150
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import "
|
|
2
|
-
import { z } from "../../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
|
|
1
|
+
import { z } from "../deps/jsr.io/@zod/zod/4.3.6/src/index.js";
|
|
3
2
|
export declare const FlyAuthSchema: z.ZodObject<{
|
|
4
3
|
email: z.ZodString;
|
|
5
4
|
}, z.core.$loose>;
|
|
@@ -106,15 +105,6 @@ export declare const FlyDeploySchema: z.ZodObject<{
|
|
|
106
105
|
ID: z.ZodOptional<z.ZodString>;
|
|
107
106
|
Status: z.ZodOptional<z.ZodString>;
|
|
108
107
|
}, z.core.$loose>;
|
|
109
|
-
/**
|
|
110
|
-
* Map Fly machine state to internal state.
|
|
111
|
-
* Fly states: created, starting, started, stopping, stopped, destroying, destroyed
|
|
112
|
-
*/
|
|
113
|
-
export declare const mapFlyMachineState: (flyState: string) => "creating" | "running" | "frozen" | "failed";
|
|
114
|
-
/**
|
|
115
|
-
* Map Fly guest config to machine size enum.
|
|
116
|
-
*/
|
|
117
|
-
export declare const mapFlyMachineSize: (guest?: z.infer<typeof FlyMachineGuestSchema>) => "shared-cpu-1x" | "shared-cpu-2x" | "shared-cpu-4x";
|
|
118
108
|
export type FlyOrgs = z.infer<typeof FlyOrgsSchema>;
|
|
119
109
|
export declare const FlyIpNetworkSchema: z.ZodObject<{
|
|
120
110
|
Name: z.ZodString;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fly.d.ts","sourceRoot":"","sources":["../../src/schemas/fly.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,4CAA4C,CAAC;AAM/D,eAAO,MAAM,aAAa;;iBAEhB,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAMpD,eAAO,MAAM,YAAY;;;;;;iBAMf,CAAC;AAEX,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD,eAAO,MAAM,iBAAiB;;;;;;kBAAwB,CAAC;AAMvD,eAAO,MAAM,eAAe;;;;;iBAKlB,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAMxD,eAAO,MAAM,qBAAqB;;;;iBAIxB,CAAC;AAEX,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;iBAgBzB,CAAC;AAEX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;iBASnB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAM/D,eAAO,MAAM,aAAa,uCAAmC,CAAC;AAM9D,eAAO,MAAM,eAAe;;;iBAGlB,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAMpD,eAAO,MAAM,kBAAkB;;;;;iBAGrB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;;;iBAOd,CAAC;AAEX,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,eAAO,MAAM,eAAe;;;;;;;;;;;;kBAAuB,CAAC;AAMpD,eAAO,MAAM,gBAAgB;;;;;;;iBAKnB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAGvB,CAAC"}
|