@replayio/app-building 1.25.0 → 1.26.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/dist/index.js CHANGED
@@ -1,7 +1,799 @@
1
- export * from "./container";
2
- export * from "./container-registry";
3
- export * from "./container-utils";
4
- export * from "./http-client";
5
- export * from "./image-ref";
6
- export * from "./secrets";
7
- export * from "./tasks";
1
+ // container.ts
2
+ import { execFileSync, spawn } from "child_process";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { resolve } from "path";
5
+
6
+ // fly.ts
7
+ var API_BASE = "https://api.machines.dev/v1";
8
+ async function flyFetch(path, token, opts = {}) {
9
+ const res = await fetch(`${API_BASE}${path}`, {
10
+ ...opts,
11
+ headers: {
12
+ Authorization: `Bearer ${token}`,
13
+ "Content-Type": "application/json",
14
+ ...opts.headers ?? {}
15
+ }
16
+ });
17
+ if (!res.ok) {
18
+ const body = await res.text().catch(() => "");
19
+ throw new Error(`Fly API ${opts.method ?? "GET"} ${path} \u2192 ${res.status}: ${body}`);
20
+ }
21
+ return res;
22
+ }
23
+ async function createVolume(app, token, name, region, sizeGb = 50) {
24
+ const res = await flyFetch(`/apps/${app}/volumes`, token, {
25
+ method: "POST",
26
+ body: JSON.stringify({
27
+ name,
28
+ region,
29
+ size_gb: sizeGb,
30
+ encrypted: true,
31
+ require_unique_zone: false
32
+ })
33
+ });
34
+ const data = await res.json();
35
+ return data.id;
36
+ }
37
+ async function deleteVolume(app, token, volumeId) {
38
+ await flyFetch(`/apps/${app}/volumes/${volumeId}`, token, {
39
+ method: "DELETE"
40
+ });
41
+ }
42
+ async function createMachine(app, token, image, env, name) {
43
+ const volumeName = `repo_${name.replace(/-/g, "_")}`.slice(0, 30);
44
+ const regions = ["dfw", "iad", "ord", "sjc"];
45
+ let cleanupDone;
46
+ for (const region of regions) {
47
+ console.log(`Creating machine in region ${region}...`);
48
+ const volumeId = await createVolume(app, token, volumeName, region, 50);
49
+ if (!cleanupDone) {
50
+ cleanupDone = listVolumes(app, token).then((vols) => Promise.all(
51
+ vols.map(async ({ id, attached_machine_id }) => {
52
+ if (attached_machine_id || id === volumeId)
53
+ return;
54
+ await deleteVolume(app, token, id).catch(() => {
55
+ });
56
+ })
57
+ ));
58
+ }
59
+ try {
60
+ const res = await flyFetch(`/apps/${app}/machines`, token, {
61
+ method: "POST",
62
+ body: JSON.stringify({
63
+ name,
64
+ region,
65
+ config: {
66
+ image,
67
+ env,
68
+ auto_destroy: true,
69
+ restart: { policy: "on-failure", max_retries: 3 },
70
+ guest: {
71
+ cpu_kind: "performance",
72
+ cpus: 16,
73
+ memory_mb: 32768
74
+ },
75
+ mounts: [{ volume: volumeId, path: "/repo" }],
76
+ services: [
77
+ {
78
+ ports: [{ port: 443, handlers: ["tls", "http"] }],
79
+ protocol: "tcp",
80
+ internal_port: 3e3,
81
+ autostart: false,
82
+ autostop: "off"
83
+ }
84
+ ]
85
+ }
86
+ })
87
+ });
88
+ const data = await res.json();
89
+ await cleanupDone;
90
+ return { machineId: data.id, volumeId };
91
+ } catch (err) {
92
+ await deleteVolume(app, token, volumeId).catch(() => {
93
+ });
94
+ const msg = err instanceof Error ? err.message : String(err);
95
+ if (msg.includes("412") || msg.includes("insufficient")) {
96
+ console.log(`Insufficient resources in ${region}, trying next region...`);
97
+ continue;
98
+ }
99
+ throw new Error(`Failed to create machine in region ${region}: ${msg}`);
100
+ }
101
+ }
102
+ throw new Error(`Failed to create machine in any region (tried ${regions.join(", ")}): insufficient resources`);
103
+ }
104
+ async function waitForMachine(app, token, machineId, timeoutMs = 18e4) {
105
+ const start = Date.now();
106
+ let lastLogTime = 0;
107
+ while (Date.now() - start < timeoutMs) {
108
+ try {
109
+ await flyFetch(
110
+ `/apps/${app}/machines/${machineId}/wait?state=started&timeout=60`,
111
+ token
112
+ );
113
+ return;
114
+ } catch (e) {
115
+ const now = Date.now();
116
+ const elapsed = Math.round((now - start) / 1e3);
117
+ if (now - lastLogTime >= 1e4) {
118
+ console.log(`Still waiting for machine to start (${elapsed}s elapsed): ${e instanceof Error ? e.message : e}`);
119
+ lastLogTime = now;
120
+ }
121
+ await new Promise((r) => setTimeout(r, 5e3));
122
+ }
123
+ }
124
+ throw new Error(`Machine ${machineId} did not reach started state within ${timeoutMs / 1e3}s`);
125
+ }
126
+ async function destroyMachine(app, token, machineId, volumeId) {
127
+ await flyFetch(`/apps/${app}/machines/${machineId}?force=true`, token, {
128
+ method: "DELETE"
129
+ });
130
+ if (volumeId) {
131
+ await deleteVolume(app, token, volumeId).catch((err) => {
132
+ console.log(`Warning: failed to delete volume ${volumeId}: ${err instanceof Error ? err.message : err}`);
133
+ });
134
+ }
135
+ }
136
+ async function listMachines(app, token) {
137
+ const res = await flyFetch(`/apps/${app}/machines`, token);
138
+ return await res.json();
139
+ }
140
+ async function listVolumes(app, token) {
141
+ const res = await flyFetch(`/apps/${app}/volumes`, token);
142
+ return await res.json();
143
+ }
144
+
145
+ // image-ref.ts
146
+ var DEFAULT_IMAGE_REF = "ghcr.io/replayio/app-building:latest";
147
+ function getImageRef() {
148
+ return process.env.CONTAINER_IMAGE_REF ?? DEFAULT_IMAGE_REF;
149
+ }
150
+
151
+ // container.ts
152
+ var IMAGE_NAME = "app-building";
153
+ function debugLog(...args) {
154
+ if (process.env.DEBUG) console.log("[container]", ...args);
155
+ }
156
+ function loadDotEnv(projectRoot) {
157
+ const envPath = resolve(projectRoot, ".env");
158
+ if (!existsSync(envPath)) {
159
+ return {};
160
+ }
161
+ const content = readFileSync(envPath, "utf-8");
162
+ const vars = {};
163
+ for (const line of content.split("\n")) {
164
+ const trimmed = line.trim();
165
+ if (!trimmed || trimmed.startsWith("#")) continue;
166
+ const eqIndex = trimmed.indexOf("=");
167
+ if (eqIndex === -1) continue;
168
+ const key = trimmed.slice(0, eqIndex).trim();
169
+ let value = trimmed.slice(eqIndex + 1).trim();
170
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
171
+ value = value.slice(1, -1);
172
+ }
173
+ vars[key] = value;
174
+ }
175
+ return vars;
176
+ }
177
+ function buildImage(config) {
178
+ if (!config.projectRoot) throw new Error("projectRoot is required for local Docker operations");
179
+ console.log("Building Docker image...");
180
+ execFileSync("docker", ["build", "--platform", "linux/amd64", "--network", "host", "-t", IMAGE_NAME, config.projectRoot], {
181
+ stdio: "inherit",
182
+ timeout: 6e5
183
+ });
184
+ console.log("Docker image built successfully.");
185
+ }
186
+ function ensureImageExists(projectRoot) {
187
+ try {
188
+ execFileSync("docker", ["image", "inspect", IMAGE_NAME], {
189
+ stdio: "ignore"
190
+ });
191
+ } catch {
192
+ console.log("Building Docker image...");
193
+ execFileSync("docker", ["build", "--platform", "linux/amd64", "--network", "host", "-t", IMAGE_NAME, projectRoot], {
194
+ stdio: "inherit",
195
+ timeout: 6e5
196
+ });
197
+ console.log("Docker image built successfully.");
198
+ }
199
+ }
200
+ function findFreePort() {
201
+ let port = 3100;
202
+ try {
203
+ const out = execFileSync("ss", ["-tlnH"], {
204
+ encoding: "utf-8",
205
+ timeout: 5e3
206
+ });
207
+ const usedPorts = /* @__PURE__ */ new Set();
208
+ for (const match of out.matchAll(/:(\d+)\s/g)) {
209
+ usedPorts.add(parseInt(match[1], 10));
210
+ }
211
+ while (usedPorts.has(port)) port++;
212
+ } catch {
213
+ }
214
+ return port;
215
+ }
216
+ function buildContainerEnv(repo, infisical, extra = {}) {
217
+ const env = {
218
+ REPO_URL: repo.repoUrl,
219
+ CLONE_BRANCH: repo.cloneBranch,
220
+ PUSH_BRANCH: repo.pushBranch,
221
+ GIT_AUTHOR_NAME: "App Builder",
222
+ GIT_AUTHOR_EMAIL: "app-builder@localhost",
223
+ GIT_COMMITTER_NAME: "App Builder",
224
+ GIT_COMMITTER_EMAIL: "app-builder@localhost",
225
+ PLAYWRIGHT_BROWSERS_PATH: "/opt/playwright",
226
+ INFISICAL_TOKEN: infisical.token,
227
+ INFISICAL_PROJECT_ID: infisical.projectId,
228
+ INFISICAL_ENVIRONMENT: infisical.environment,
229
+ ...extra
230
+ };
231
+ if (process.env.DEBUG) {
232
+ env.DEBUG = process.env.DEBUG;
233
+ }
234
+ return env;
235
+ }
236
+ function buildExtraEnv(config, containerName) {
237
+ const extra = {
238
+ PORT: "3000",
239
+ CONTAINER_NAME: containerName
240
+ };
241
+ if (config.webhookUrl) extra.WEBHOOK_URL = config.webhookUrl;
242
+ if (config.taskWebhookUrl) extra.TASK_WEBHOOK_URL = config.taskWebhookUrl;
243
+ if (config.addTaskWebhookUrl) extra.ADD_TASK_WEBHOOK_URL = config.addTaskWebhookUrl;
244
+ if (config.webhookSecret) extra.WEBHOOK_SECRET = config.webhookSecret;
245
+ if (config.detached) extra.DETACHED = "1";
246
+ if (config.initialPrompt) extra.INITIAL_PROMPT = config.initialPrompt;
247
+ if (config.absorbTasks) extra.ABSORB_TASKS = "1";
248
+ if (config.agentCommand) extra.AGENT_COMMAND = config.agentCommand;
249
+ return extra;
250
+ }
251
+ function isRemote(config) {
252
+ return !!(config.flyToken && config.flyApp);
253
+ }
254
+ async function startLocalContainer(config, repo) {
255
+ buildImage(config);
256
+ const uniqueId = Math.random().toString(36).slice(2, 8);
257
+ const prefix = config.namePrefix ?? "app-building";
258
+ const containerName = `${prefix}-${uniqueId}`;
259
+ const containerPort = 3e3;
260
+ const hostPort = config.localPort ?? findFreePort();
261
+ const extra = buildExtraEnv(config, containerName);
262
+ extra.PORT = String(containerPort);
263
+ const containerEnv = buildContainerEnv(repo, config.infisical, extra);
264
+ const args = ["run", "--platform", "linux/amd64", "-d", "--rm", "--name", containerName];
265
+ args.push("-p", `${hostPort}:${containerPort}`);
266
+ for (const [k, v] of Object.entries(containerEnv)) {
267
+ args.push("--env", `${k}=${v}`);
268
+ }
269
+ args.push(IMAGE_NAME);
270
+ const containerId = execFileSync("docker", args, {
271
+ encoding: "utf-8",
272
+ timeout: 3e4
273
+ }).trim();
274
+ console.log(`Container started: ${containerId.slice(0, 12)} (${containerName})`);
275
+ const baseUrl = `http://127.0.0.1:${hostPort}`;
276
+ const maxWait = 12e4;
277
+ const interval = 1e3;
278
+ const start = Date.now();
279
+ let ready = false;
280
+ while (Date.now() - start < maxWait) {
281
+ try {
282
+ execFileSync(
283
+ "docker",
284
+ ["inspect", "--format", "{{.State.Running}}", containerName],
285
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
286
+ );
287
+ } catch {
288
+ let logs = "";
289
+ try {
290
+ logs = execFileSync("docker", ["logs", "--tail", "30", containerName], {
291
+ encoding: "utf-8",
292
+ timeout: 5e3
293
+ });
294
+ } catch {
295
+ }
296
+ throw new Error(
297
+ `Container exited during startup.${logs ? `
298
+
299
+ --- container logs ---
300
+ ${logs}` : " (no logs available, container was removed)"}`
301
+ );
302
+ }
303
+ try {
304
+ const res = await fetch(`${baseUrl}/status`);
305
+ if (res.ok) {
306
+ ready = true;
307
+ break;
308
+ }
309
+ } catch {
310
+ }
311
+ await new Promise((r) => setTimeout(r, interval));
312
+ }
313
+ if (!ready) {
314
+ throw new Error("Container did not become ready within timeout");
315
+ }
316
+ return { type: "local", containerName, port: hostPort, baseUrl };
317
+ }
318
+ function stopLocalContainer(containerName) {
319
+ try {
320
+ execFileSync("docker", ["stop", containerName], { stdio: "ignore", timeout: 3e4 });
321
+ } catch {
322
+ }
323
+ }
324
+ async function startRemoteContainerImpl(config, repo) {
325
+ if (!config.flyToken) throw new Error("flyToken is required for remote containers");
326
+ if (!config.flyApp) throw new Error("flyApp is required for remote containers");
327
+ const imageRef = config.imageRef ?? getImageRef();
328
+ const uniqueId = Math.random().toString(36).slice(2, 8);
329
+ const prefix = config.namePrefix ?? "app-building";
330
+ const machineName = `${prefix}-${uniqueId}`;
331
+ const extra = buildExtraEnv(config, machineName);
332
+ const containerEnv = buildContainerEnv(repo, config.infisical, extra);
333
+ const existing = await listMachines(config.flyApp, config.flyToken);
334
+ if (existing.length > 0) {
335
+ console.log(`${existing.length} existing machine(s) in ${config.flyApp}:`);
336
+ for (const m of existing) {
337
+ console.log(` ${m.id} (${m.name}) \u2014 ${m.state}`);
338
+ }
339
+ }
340
+ console.log("Creating Fly machine (with volume)...");
341
+ let machineId = "";
342
+ let volumeId = "";
343
+ for (let attempt = 0; attempt < 5; attempt++) {
344
+ try {
345
+ const result = await createMachine(config.flyApp, config.flyToken, imageRef, containerEnv, machineName);
346
+ machineId = result.machineId;
347
+ volumeId = result.volumeId;
348
+ break;
349
+ } catch (err) {
350
+ const msg = err instanceof Error ? err.message : String(err);
351
+ if (msg.includes("MANIFEST_UNKNOWN") && attempt < 4) {
352
+ console.log("Image not yet available in registry, retrying in 5s...");
353
+ await new Promise((r) => setTimeout(r, 5e3));
354
+ continue;
355
+ }
356
+ throw err;
357
+ }
358
+ }
359
+ console.log(`Machine created: ${machineId} (volume: ${volumeId})`);
360
+ const baseUrl = `https://${config.flyApp}.fly.dev`;
361
+ const agentState = {
362
+ type: "remote",
363
+ containerName: machineName,
364
+ port: 443,
365
+ baseUrl,
366
+ flyApp: config.flyApp,
367
+ flyMachineId: machineId,
368
+ flyVolumeId: volumeId
369
+ };
370
+ console.log("Waiting for machine to start...");
371
+ await waitForMachine(config.flyApp, config.flyToken, machineId);
372
+ console.log("Machine started.");
373
+ const maxWait = 18e4;
374
+ const interval = 2e3;
375
+ const start = Date.now();
376
+ let ready = false;
377
+ while (Date.now() - start < maxWait) {
378
+ try {
379
+ const res = await fetch(`${baseUrl}/status`, {
380
+ headers: { "fly-force-instance-id": machineId }
381
+ });
382
+ if (res.ok) {
383
+ ready = true;
384
+ break;
385
+ }
386
+ } catch {
387
+ }
388
+ await new Promise((r) => setTimeout(r, interval));
389
+ }
390
+ if (!ready) {
391
+ console.log("Timed out waiting for machine, destroying...");
392
+ await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId).catch(() => {
393
+ });
394
+ throw new Error("Remote container did not become ready within timeout");
395
+ }
396
+ return agentState;
397
+ }
398
+ async function stopRemoteContainerImpl(config, state) {
399
+ if (!state.flyApp || !state.flyMachineId) {
400
+ throw new Error("Missing flyApp or flyMachineId in agent state");
401
+ }
402
+ if (!config.flyToken) throw new Error("flyToken is required to stop remote container");
403
+ console.log(`Destroying Fly machine ${state.flyMachineId}...`);
404
+ await destroyMachine(state.flyApp, config.flyToken, state.flyMachineId, state.flyVolumeId);
405
+ console.log("Machine destroyed.");
406
+ }
407
+ async function startContainer(config, repo) {
408
+ const { token, projectId, environment } = config.infisical;
409
+ if (!token || !projectId || !environment) {
410
+ const missing = [
411
+ !token && "token",
412
+ !projectId && "projectId",
413
+ !environment && "environment"
414
+ ].filter(Boolean);
415
+ throw new Error(`Missing Infisical credentials: ${missing.join(", ")}. Containers cannot start without Infisical.`);
416
+ }
417
+ debugLog("startContainer config:", {
418
+ projectRoot: config.projectRoot,
419
+ flyApp: config.flyApp,
420
+ imageRef: config.imageRef,
421
+ webhookUrl: config.webhookUrl,
422
+ detached: config.detached,
423
+ remote: isRemote(config),
424
+ initialPrompt: config.initialPrompt ? `${config.initialPrompt.slice(0, 100)}...` : void 0
425
+ });
426
+ debugLog("startContainer repo:", repo);
427
+ const state = isRemote(config) ? await startRemoteContainerImpl(config, repo) : await startLocalContainer(config, repo);
428
+ config.registry.log(state);
429
+ return state;
430
+ }
431
+ async function stopContainer(config, state) {
432
+ if (state.type === "remote") {
433
+ await stopRemoteContainerImpl(config, state);
434
+ } else {
435
+ stopLocalContainer(state.containerName);
436
+ }
437
+ config.registry.markStopped(state.containerName);
438
+ }
439
+ function spawnTestContainer(config) {
440
+ if (!config.projectRoot) throw new Error("projectRoot is required for local Docker operations");
441
+ ensureImageExists(config.projectRoot);
442
+ const uniqueId = Math.random().toString(36).slice(2, 8);
443
+ const containerName = `app-building-test-${uniqueId}`;
444
+ const args = ["run", "--platform", "linux/amd64", "-it", "--rm", "--name", containerName];
445
+ args.push("-v", `${config.projectRoot}:/repo`);
446
+ args.push("-w", "/repo");
447
+ args.push("--network", "host");
448
+ args.push("--user", `${process.getuid()}:${process.getgid()}`);
449
+ args.push("--env", "HOME=/repo/.agent-home");
450
+ args.push("--env", "PLAYWRIGHT_BROWSERS_PATH=/opt/playwright");
451
+ args.push("--env", `INFISICAL_TOKEN=${config.infisical.token}`);
452
+ args.push("--env", `INFISICAL_PROJECT_ID=${config.infisical.projectId}`);
453
+ args.push("--env", `INFISICAL_ENVIRONMENT=${config.infisical.environment}`);
454
+ args.push(IMAGE_NAME, "bash");
455
+ return new Promise((resolvePromise, reject) => {
456
+ const child = spawn("docker", args, { stdio: "inherit" });
457
+ child.on("close", (code) => {
458
+ if (code === 0) resolvePromise();
459
+ else reject(new Error(`Container exited with code ${code}`));
460
+ });
461
+ child.on("error", reject);
462
+ });
463
+ }
464
+
465
+ // container-registry.ts
466
+ import { readFileSync as readFileSync2, writeFileSync, appendFileSync, existsSync as existsSync2 } from "fs";
467
+
468
+ // container-utils.ts
469
+ function httpOptsFor(state) {
470
+ if (state.type === "remote" && state.flyMachineId) {
471
+ return { headers: { "fly-force-instance-id": state.flyMachineId } };
472
+ }
473
+ return {};
474
+ }
475
+ async function probeAlive(entry) {
476
+ try {
477
+ const headers = {};
478
+ if (entry.type === "remote" && entry.flyMachineId) {
479
+ headers["fly-force-instance-id"] = entry.flyMachineId;
480
+ }
481
+ const res = await fetch(`${entry.baseUrl}/status`, {
482
+ headers,
483
+ signal: AbortSignal.timeout(5e3)
484
+ });
485
+ if (!res.ok) return false;
486
+ const body = await res.json();
487
+ if (body.containerName && body.containerName !== entry.containerName) {
488
+ return false;
489
+ }
490
+ return true;
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+
496
+ // container-registry.ts
497
+ var FileContainerRegistry = class {
498
+ constructor(filePath) {
499
+ this.filePath = filePath;
500
+ }
501
+ readRegistry() {
502
+ if (!existsSync2(this.filePath)) return { lines: [], entries: [] };
503
+ const lines = readFileSync2(this.filePath, "utf-8").split("\n").filter((l) => l.trim());
504
+ const entries = lines.map((line) => {
505
+ try {
506
+ return JSON.parse(line);
507
+ } catch {
508
+ return null;
509
+ }
510
+ });
511
+ return { lines, entries };
512
+ }
513
+ updateEntry(match, update) {
514
+ const { lines, entries } = this.readRegistry();
515
+ for (let i = entries.length - 1; i >= 0; i--) {
516
+ const entry = entries[i];
517
+ if (entry && match(entry)) {
518
+ update(entry);
519
+ lines[i] = JSON.stringify(entry);
520
+ writeFileSync(this.filePath, lines.join("\n") + "\n");
521
+ return;
522
+ }
523
+ }
524
+ }
525
+ log(state) {
526
+ const entry = {
527
+ ...state,
528
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
529
+ };
530
+ appendFileSync(this.filePath, JSON.stringify(entry) + "\n");
531
+ }
532
+ markStopped(containerName) {
533
+ this.updateEntry(
534
+ (e) => !e.stoppedAt && (!containerName || e.containerName === containerName),
535
+ (e) => {
536
+ e.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
537
+ }
538
+ );
539
+ }
540
+ clearStopped(containerName) {
541
+ this.updateEntry(
542
+ (e) => e.containerName === containerName && !!e.stoppedAt,
543
+ (e) => {
544
+ delete e.stoppedAt;
545
+ }
546
+ );
547
+ }
548
+ getRecent(limit = 20) {
549
+ const { entries } = this.readRegistry();
550
+ return entries.filter((e) => e !== null).slice(-limit);
551
+ }
552
+ find(containerName) {
553
+ const entries = this.getRecent(100);
554
+ for (let i = entries.length - 1; i >= 0; i--) {
555
+ if (entries[i].containerName === containerName) {
556
+ return entries[i];
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+ async findAlive() {
562
+ const entries = this.getRecent();
563
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1e3;
564
+ const candidates = entries.filter((e) => new Date(e.startedAt).getTime() > oneDayAgo);
565
+ const aliveResults = await Promise.all(
566
+ candidates.map(async (entry) => ({
567
+ entry,
568
+ alive: await probeAlive(entry)
569
+ }))
570
+ );
571
+ for (const r of aliveResults) {
572
+ if (r.alive && r.entry.stoppedAt) {
573
+ this.clearStopped(r.entry.containerName);
574
+ } else if (!r.alive && !r.entry.stoppedAt) {
575
+ this.markStopped(r.entry.containerName);
576
+ }
577
+ }
578
+ return aliveResults.filter((r) => r.alive).map((r) => r.entry);
579
+ }
580
+ };
581
+
582
+ // http-client.ts
583
+ var DEFAULT_TIMEOUT = 3e4;
584
+ var MAX_RETRIES = 4;
585
+ var RETRY_DELAY_MS = 2e3;
586
+ async function fetchWithRetry(url, init, timeout) {
587
+ for (let attempt = 0; ; attempt++) {
588
+ try {
589
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeout) });
590
+ if (!res.ok) throw new Error(`${init.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`);
591
+ return res;
592
+ } catch (err) {
593
+ if (attempt >= MAX_RETRIES) throw err;
594
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
595
+ }
596
+ }
597
+ }
598
+ async function httpGet(url, opts = {}) {
599
+ const res = await fetchWithRetry(url, { headers: opts.headers }, opts.timeout ?? DEFAULT_TIMEOUT);
600
+ return res.json();
601
+ }
602
+ async function httpPost(url, body, opts = {}) {
603
+ const res = await fetchWithRetry(
604
+ url,
605
+ {
606
+ method: "POST",
607
+ headers: { "Content-Type": "application/json", ...opts.headers },
608
+ body: body !== void 0 ? JSON.stringify(body) : void 0
609
+ },
610
+ opts.timeout ?? DEFAULT_TIMEOUT
611
+ );
612
+ return res.json();
613
+ }
614
+
615
+ // secrets.ts
616
+ var INFISICAL_API_BASE = "https://app.infisical.com";
617
+ async function infisicalLogin(clientId, clientSecret) {
618
+ const res = await fetch(`${INFISICAL_API_BASE}/api/v1/auth/universal-auth/login`, {
619
+ method: "POST",
620
+ headers: { "Content-Type": "application/json" },
621
+ body: JSON.stringify({ clientId, clientSecret })
622
+ });
623
+ if (!res.ok) {
624
+ const body = await res.text().catch(() => "");
625
+ throw new Error(`Infisical login failed \u2192 ${res.status}: ${body}`);
626
+ }
627
+ const data = await res.json();
628
+ return data.accessToken;
629
+ }
630
+ function authHeaders(config) {
631
+ return {
632
+ Authorization: `Bearer ${config.token}`,
633
+ "Content-Type": "application/json"
634
+ };
635
+ }
636
+ async function fetchInfisicalSecrets(config, secretPath) {
637
+ const params = new URLSearchParams({
638
+ projectId: config.projectId,
639
+ environment: config.environment,
640
+ secretPath
641
+ });
642
+ const res = await fetch(`${INFISICAL_API_BASE}/api/v4/secrets?${params}`, {
643
+ headers: authHeaders(config)
644
+ });
645
+ if (!res.ok) {
646
+ const body = await res.text().catch(() => "");
647
+ throw new Error(`Infisical GET secrets ${secretPath} \u2192 ${res.status}: ${body}`);
648
+ }
649
+ const data = await res.json();
650
+ const secrets = {};
651
+ for (const s of data.secrets) {
652
+ secrets[s.secretKey] = s.secretValue;
653
+ }
654
+ return secrets;
655
+ }
656
+ async function fetchGlobalSecrets(config) {
657
+ return fetchInfisicalSecrets(config, "/global/");
658
+ }
659
+ async function fetchBranchSecrets(config, branch) {
660
+ return fetchInfisicalSecrets(config, `/branches/${branch}/`);
661
+ }
662
+ async function ensureFolder(config, folderPath) {
663
+ const segments = folderPath.split("/").filter(Boolean);
664
+ let parentPath = "/";
665
+ for (const segment of segments) {
666
+ const res = await fetch(`${INFISICAL_API_BASE}/api/v2/folders`, {
667
+ method: "POST",
668
+ headers: authHeaders(config),
669
+ body: JSON.stringify({
670
+ projectId: config.projectId,
671
+ environment: config.environment,
672
+ name: segment,
673
+ path: parentPath
674
+ })
675
+ });
676
+ if (res.ok || res.status === 400) {
677
+ parentPath += segment + "/";
678
+ continue;
679
+ }
680
+ const text = await res.text().catch(() => "");
681
+ throw new Error(`Infisical create folder ${parentPath}${segment} \u2192 ${res.status}: ${text}`);
682
+ }
683
+ }
684
+ async function createBranchSecret(config, branch, name, value) {
685
+ const secretPath = `/branches/${branch}/`;
686
+ const url = `${INFISICAL_API_BASE}/api/v4/secrets/${encodeURIComponent(name)}`;
687
+ const body = {
688
+ projectId: config.projectId,
689
+ environment: config.environment,
690
+ secretPath,
691
+ secretValue: value,
692
+ type: "shared"
693
+ };
694
+ const headers = authHeaders(config);
695
+ const res = await fetch(url, {
696
+ method: "POST",
697
+ headers,
698
+ body: JSON.stringify(body)
699
+ });
700
+ if (res.ok) return;
701
+ if (res.status === 400) {
702
+ const patchRes = await fetch(url, {
703
+ method: "PATCH",
704
+ headers,
705
+ body: JSON.stringify(body)
706
+ });
707
+ if (patchRes.ok) return;
708
+ const text2 = await patchRes.text().catch(() => "");
709
+ throw new Error(`Infisical PATCH ${name} \u2192 ${patchRes.status}: ${text2}`);
710
+ }
711
+ if (res.status === 404) {
712
+ await ensureFolder(config, secretPath);
713
+ const retryRes = await fetch(url, {
714
+ method: "POST",
715
+ headers,
716
+ body: JSON.stringify(body)
717
+ });
718
+ if (retryRes.ok) return;
719
+ if (retryRes.status === 400) {
720
+ const patchRes = await fetch(url, {
721
+ method: "PATCH",
722
+ headers,
723
+ body: JSON.stringify(body)
724
+ });
725
+ if (patchRes.ok) return;
726
+ const text3 = await patchRes.text().catch(() => "");
727
+ throw new Error(`Infisical PATCH ${name} (after folder creation) \u2192 ${patchRes.status}: ${text3}`);
728
+ }
729
+ const text2 = await retryRes.text().catch(() => "");
730
+ throw new Error(`Infisical POST ${name} (after folder creation) \u2192 ${retryRes.status}: ${text2}`);
731
+ }
732
+ const text = await res.text().catch(() => "");
733
+ throw new Error(`Infisical POST ${name} \u2192 ${res.status}: ${text}`);
734
+ }
735
+ async function getInfisicalConfig(envVars) {
736
+ const clientId = envVars.INFISICAL_CLIENT_ID;
737
+ const clientSecret = envVars.INFISICAL_CLIENT_SECRET;
738
+ const projectId = envVars.INFISICAL_PROJECT_ID;
739
+ const environment = envVars.INFISICAL_ENVIRONMENT;
740
+ const missing = [
741
+ !clientId && "INFISICAL_CLIENT_ID",
742
+ !clientSecret && "INFISICAL_CLIENT_SECRET",
743
+ !projectId && "INFISICAL_PROJECT_ID",
744
+ !environment && "INFISICAL_ENVIRONMENT"
745
+ ].filter(Boolean);
746
+ if (missing.length > 0) {
747
+ throw new Error(`Missing Infisical config in .env: ${missing.join(", ")}`);
748
+ }
749
+ const token = await infisicalLogin(clientId, clientSecret);
750
+ return { token, projectId, environment };
751
+ }
752
+
753
+ // tasks.ts
754
+ function findReadyTask(pendingTasks, completedTasks) {
755
+ const completedIds = new Set(completedTasks.map((t) => t.id).filter(Boolean));
756
+ const pendingChildParents = /* @__PURE__ */ new Set();
757
+ for (const t of pendingTasks) {
758
+ if (t.parentTaskId) pendingChildParents.add(t.parentTaskId);
759
+ }
760
+ const cache = /* @__PURE__ */ new Map();
761
+ function isFullyComplete(taskId) {
762
+ if (cache.has(taskId)) return cache.get(taskId);
763
+ cache.set(taskId, false);
764
+ if (!completedIds.has(taskId)) return false;
765
+ if (pendingChildParents.has(taskId)) return false;
766
+ for (const child of completedTasks) {
767
+ if (child.parentTaskId === taskId && child.id && !isFullyComplete(child.id)) {
768
+ return false;
769
+ }
770
+ }
771
+ cache.set(taskId, true);
772
+ return true;
773
+ }
774
+ const idx = pendingTasks.findIndex((task) => {
775
+ if (!task.requiredTaskIds || task.requiredTaskIds.length === 0) return true;
776
+ return task.requiredTaskIds.every((id) => isFullyComplete(id));
777
+ });
778
+ return idx === -1 ? null : pendingTasks[idx];
779
+ }
780
+ export {
781
+ FileContainerRegistry,
782
+ buildImage,
783
+ createBranchSecret,
784
+ fetchBranchSecrets,
785
+ fetchGlobalSecrets,
786
+ fetchInfisicalSecrets,
787
+ findReadyTask,
788
+ getImageRef,
789
+ getInfisicalConfig,
790
+ httpGet,
791
+ httpOptsFor,
792
+ httpPost,
793
+ infisicalLogin,
794
+ loadDotEnv,
795
+ probeAlive,
796
+ spawnTestContainer,
797
+ startContainer,
798
+ stopContainer
799
+ };