@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.
Files changed (141) hide show
  1. package/esm/cli/commands/create/index.d.ts +2 -0
  2. package/esm/cli/commands/create/index.d.ts.map +1 -0
  3. package/esm/cli/commands/create/index.js +292 -0
  4. package/esm/cli/commands/create/machine.d.ts +33 -0
  5. package/esm/cli/commands/create/machine.d.ts.map +1 -0
  6. package/esm/cli/commands/create/machine.js +162 -0
  7. package/esm/cli/commands/deploy/index.d.ts +2 -0
  8. package/esm/cli/commands/deploy/index.d.ts.map +1 -0
  9. package/esm/cli/commands/deploy/index.js +290 -0
  10. package/esm/cli/commands/deploy/machine.d.ts +52 -0
  11. package/esm/cli/commands/deploy/machine.d.ts.map +1 -0
  12. package/esm/cli/commands/deploy/machine.js +116 -0
  13. package/esm/cli/commands/deploy/modes.d.ts +18 -0
  14. package/esm/cli/commands/deploy/modes.d.ts.map +1 -0
  15. package/esm/cli/commands/deploy/modes.js +152 -0
  16. package/esm/cli/commands/destroy/app.d.ts +2 -0
  17. package/esm/cli/commands/destroy/app.d.ts.map +1 -0
  18. package/esm/cli/commands/destroy/app.js +173 -0
  19. package/esm/cli/commands/destroy/index.d.ts +2 -0
  20. package/esm/cli/commands/destroy/index.d.ts.map +1 -0
  21. package/esm/cli/commands/destroy/index.js +63 -0
  22. package/esm/cli/commands/destroy/network.d.ts +2 -0
  23. package/esm/cli/commands/destroy/network.d.ts.map +1 -0
  24. package/esm/cli/commands/destroy/network.js +210 -0
  25. package/esm/cli/commands/doctor.d.ts.map +1 -0
  26. package/esm/cli/commands/doctor.js +295 -0
  27. package/esm/{src/cli → cli}/commands/list.d.ts.map +1 -1
  28. package/esm/{src/cli → cli}/commands/list.js +39 -54
  29. package/esm/cli/commands/status.d.ts.map +1 -0
  30. package/esm/cli/commands/status.js +331 -0
  31. package/esm/cli/mod.d.ts.map +1 -0
  32. package/esm/{src/cli → cli}/mod.js +4 -4
  33. package/esm/deno.d.ts +4 -18
  34. package/esm/deno.js +5 -19
  35. package/esm/deps/jsr.io/@std/path/1.1.4/constants.d.ts +1 -1
  36. package/esm/deps/jsr.io/@zod/zod/4.3.6/src/v4/core/json-schema-generator.d.ts +1 -1
  37. package/esm/lib/args.d.ts +11 -0
  38. package/esm/lib/args.d.ts.map +1 -0
  39. package/esm/lib/args.js +28 -0
  40. package/esm/lib/cli.d.ts +0 -2
  41. package/esm/lib/cli.d.ts.map +1 -1
  42. package/esm/lib/cli.js +41 -27
  43. package/esm/lib/command.d.ts +21 -49
  44. package/esm/lib/command.d.ts.map +1 -1
  45. package/esm/lib/command.js +55 -95
  46. package/esm/lib/machine.d.ts +11 -0
  47. package/esm/lib/machine.d.ts.map +1 -0
  48. package/esm/lib/machine.js +15 -0
  49. package/esm/lib/output.d.ts +3 -2
  50. package/esm/lib/output.d.ts.map +1 -1
  51. package/esm/lib/output.js +25 -11
  52. package/esm/lib/result.d.ts +18 -7
  53. package/esm/lib/result.d.ts.map +1 -1
  54. package/esm/lib/result.js +46 -1
  55. package/esm/main.d.ts +6 -6
  56. package/esm/main.d.ts.map +1 -1
  57. package/esm/main.js +7 -9
  58. package/esm/providers/fly.d.ts +81 -0
  59. package/esm/providers/fly.d.ts.map +1 -0
  60. package/esm/providers/fly.js +372 -0
  61. package/esm/providers/tailscale.d.ts +31 -0
  62. package/esm/providers/tailscale.d.ts.map +1 -0
  63. package/esm/providers/tailscale.js +150 -0
  64. package/esm/{src/schemas → schemas}/fly.d.ts +1 -11
  65. package/esm/schemas/fly.d.ts.map +1 -0
  66. package/esm/{src/schemas → schemas}/fly.js +14 -56
  67. package/esm/{src/schemas → schemas}/tailscale.d.ts +1 -2
  68. package/esm/schemas/tailscale.d.ts.map +1 -0
  69. package/esm/{src/schemas → schemas}/tailscale.js +2 -3
  70. package/esm/src/{docker/router → router}/Dockerfile +0 -11
  71. package/esm/src/router/start.sh +101 -0
  72. package/esm/util/constants.d.ts +13 -0
  73. package/esm/util/constants.d.ts.map +1 -0
  74. package/esm/util/constants.js +34 -0
  75. package/esm/{src → util}/credentials.d.ts +0 -1
  76. package/esm/util/credentials.d.ts.map +1 -0
  77. package/esm/{src → util}/credentials.js +3 -5
  78. package/esm/{src → util}/discovery.d.ts +20 -4
  79. package/esm/util/discovery.d.ts.map +1 -0
  80. package/esm/{src → util}/discovery.js +38 -15
  81. package/esm/util/fly-transforms.d.ts +27 -0
  82. package/esm/util/fly-transforms.d.ts.map +1 -0
  83. package/esm/util/fly-transforms.js +87 -0
  84. package/esm/{src → util}/guard.d.ts +1 -2
  85. package/esm/util/guard.d.ts.map +1 -0
  86. package/esm/{src → util}/guard.js +27 -27
  87. package/esm/util/naming.d.ts +5 -0
  88. package/esm/util/naming.d.ts.map +1 -0
  89. package/esm/util/naming.js +12 -0
  90. package/esm/{src → util}/resolve.d.ts +2 -3
  91. package/esm/util/resolve.d.ts.map +1 -0
  92. package/esm/{src → util}/resolve.js +1 -2
  93. package/esm/util/session.d.ts +16 -0
  94. package/esm/util/session.d.ts.map +1 -0
  95. package/esm/util/session.js +19 -0
  96. package/esm/util/tailscale-local.d.ts +13 -0
  97. package/esm/util/tailscale-local.d.ts.map +1 -0
  98. package/esm/util/tailscale-local.js +63 -0
  99. package/esm/{src → util}/template.d.ts +2 -4
  100. package/esm/util/template.d.ts.map +1 -0
  101. package/esm/{src → util}/template.js +14 -17
  102. package/package.json +1 -43
  103. package/esm/lib/paths.d.ts +0 -3
  104. package/esm/lib/paths.d.ts.map +0 -1
  105. package/esm/lib/paths.js +0 -5
  106. package/esm/src/cli/commands/create.d.ts +0 -2
  107. package/esm/src/cli/commands/create.d.ts.map +0 -1
  108. package/esm/src/cli/commands/create.js +0 -294
  109. package/esm/src/cli/commands/deploy.d.ts +0 -2
  110. package/esm/src/cli/commands/deploy.d.ts.map +0 -1
  111. package/esm/src/cli/commands/deploy.js +0 -426
  112. package/esm/src/cli/commands/destroy.d.ts +0 -2
  113. package/esm/src/cli/commands/destroy.d.ts.map +0 -1
  114. package/esm/src/cli/commands/destroy.js +0 -340
  115. package/esm/src/cli/commands/doctor.d.ts.map +0 -1
  116. package/esm/src/cli/commands/doctor.js +0 -141
  117. package/esm/src/cli/commands/status.d.ts.map +0 -1
  118. package/esm/src/cli/commands/status.js +0 -152
  119. package/esm/src/cli/mod.d.ts.map +0 -1
  120. package/esm/src/credentials.d.ts.map +0 -1
  121. package/esm/src/discovery.d.ts.map +0 -1
  122. package/esm/src/docker/router/start.sh +0 -146
  123. package/esm/src/guard.d.ts.map +0 -1
  124. package/esm/src/providers/fly.d.ts +0 -70
  125. package/esm/src/providers/fly.d.ts.map +0 -1
  126. package/esm/src/providers/fly.js +0 -411
  127. package/esm/src/providers/tailscale.d.ts +0 -31
  128. package/esm/src/providers/tailscale.d.ts.map +0 -1
  129. package/esm/src/providers/tailscale.js +0 -195
  130. package/esm/src/resolve.d.ts.map +0 -1
  131. package/esm/src/schemas/config.d.ts +0 -5
  132. package/esm/src/schemas/config.d.ts.map +0 -1
  133. package/esm/src/schemas/config.js +0 -22
  134. package/esm/src/schemas/fly.d.ts.map +0 -1
  135. package/esm/src/schemas/tailscale.d.ts.map +0 -1
  136. package/esm/src/template.d.ts.map +0 -1
  137. /package/esm/{src/cli → cli}/commands/doctor.d.ts +0 -0
  138. /package/esm/{src/cli → cli}/commands/list.d.ts +0 -0
  139. /package/esm/{src/cli → cli}/commands/status.d.ts +0 -0
  140. /package/esm/{src/cli → cli}/mod.d.ts +0 -0
  141. /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 "../../_dnt.polyfills.js";
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"}