@replayio/app-building 1.7.0 → 1.8.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/README.md +16 -20
- package/dist/container.d.ts +0 -2
- package/dist/container.js +0 -127
- package/dist/fly.d.ts +9 -5
- package/dist/fly.js +47 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ npm install @replayio/app-building
|
|
|
14
14
|
import {
|
|
15
15
|
loadDotEnv,
|
|
16
16
|
FileContainerRegistry,
|
|
17
|
+
createMachine,
|
|
18
|
+
destroyMachine,
|
|
17
19
|
type ContainerConfig,
|
|
18
20
|
type RepoOptions,
|
|
19
|
-
startRemoteContainer,
|
|
20
|
-
stopRemoteContainer,
|
|
21
21
|
httpGet,
|
|
22
22
|
httpPost,
|
|
23
23
|
httpOptsFor,
|
|
@@ -33,20 +33,19 @@ const config: ContainerConfig = {
|
|
|
33
33
|
flyApp: envVars.FLY_APP_NAME,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
config.
|
|
39
|
-
|
|
40
|
-
const state = await startRemoteContainer(config, repo);
|
|
36
|
+
// Create a Fly machine (automatically provisions a volume)
|
|
37
|
+
const { machineId, volumeId } = await createMachine(
|
|
38
|
+
config.flyApp, config.flyToken, imageRef, containerEnv, machineName,
|
|
39
|
+
);
|
|
41
40
|
|
|
42
41
|
// Check status
|
|
43
|
-
const status = await httpGet(
|
|
42
|
+
const status = await httpGet(`https://${config.flyApp}.fly.dev/status`);
|
|
44
43
|
|
|
45
44
|
// Query the registry
|
|
46
45
|
const alive = await config.registry.findAlive();
|
|
47
46
|
|
|
48
|
-
// Clean up
|
|
49
|
-
await
|
|
47
|
+
// Clean up (destroys machine and its volume)
|
|
48
|
+
await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
## Exported API
|
|
@@ -65,9 +64,7 @@ await stopRemoteContainer(config, state);
|
|
|
65
64
|
| Export | Description |
|
|
66
65
|
|---|---|
|
|
67
66
|
| `startContainer(config, repo)` | Build the Docker image locally and start a container with `--network host`. Returns `AgentState`. |
|
|
68
|
-
| `startRemoteContainer(config, repo)` | Create a Fly.io machine using the GHCR image. Requires `config.flyToken` and `config.flyApp`. Returns `AgentState`. |
|
|
69
67
|
| `stopContainer(config, containerName)` | Stop a local Docker container by name. |
|
|
70
|
-
| `stopRemoteContainer(config, state)` | Destroy the Fly.io machine for a remote container. Requires `config.flyToken`. |
|
|
71
68
|
| `buildImage(config)` | Build the Docker image locally (called automatically by `startContainer`). |
|
|
72
69
|
| `spawnTestContainer(config)` | Start an interactive (`-it`) container with the repo mounted at `/repo`. |
|
|
73
70
|
| `loadDotEnv(projectRoot)` | Parse a `.env` file and return key-value pairs. |
|
|
@@ -103,26 +100,25 @@ await stopRemoteContainer(config, state);
|
|
|
103
100
|
| `httpOptsFor(state)` | Return `HttpOptions` for a container (adds `fly-force-instance-id` header for remote containers). |
|
|
104
101
|
| `probeAlive(entry)` | Check if a container is responding to `/status`. |
|
|
105
102
|
|
|
106
|
-
### Fly.io
|
|
103
|
+
### Fly.io utilities
|
|
107
104
|
|
|
108
105
|
| Export | Description |
|
|
109
106
|
|---|---|
|
|
110
107
|
| `createApp(token, name, org?)` | Create a Fly app and allocate IPs. |
|
|
111
|
-
| `createMachine(app, token, image, env, name
|
|
112
|
-
| `waitForMachine(app, token, machineId
|
|
113
|
-
| `destroyMachine(app, token, machineId)` | Force-destroy a machine. |
|
|
108
|
+
| `createMachine(app, token, image, env, name)` | Create a Fly machine with a 50GB volume mounted at `/repo`. Returns `{ machineId, volumeId }`. |
|
|
109
|
+
| `waitForMachine(app, token, machineId)` | Poll until a machine reaches `started` state. |
|
|
114
110
|
| `listMachines(app, token)` | List all machines for an app. |
|
|
115
|
-
| `
|
|
116
|
-
| `deleteVolume(app, token, volumeId)` | Delete a Fly volume. |
|
|
111
|
+
| `destroyMachine(app, token, machineId, volumeId?)` | Force-destroy a machine and optionally its volume. |
|
|
117
112
|
| `listVolumes(app, token)` | List all volumes for an app. |
|
|
113
|
+
| `deleteVolume(app, token, volumeId)` | Delete a Fly volume. |
|
|
118
114
|
|
|
119
|
-
**Types:** `FlyMachineInfo`, `FlyVolumeInfo`
|
|
115
|
+
**Types:** `FlyMachineInfo`, `FlyVolumeInfo`, `CreateMachineResult`
|
|
120
116
|
|
|
121
117
|
### Image ref
|
|
122
118
|
|
|
123
119
|
| Export | Description |
|
|
124
120
|
|---|---|
|
|
125
|
-
| `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default.
|
|
121
|
+
| `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default. |
|
|
126
122
|
|
|
127
123
|
## Container HTTP API
|
|
128
124
|
|
package/dist/container.d.ts
CHANGED
|
@@ -29,7 +29,5 @@ export interface RepoOptions {
|
|
|
29
29
|
export declare function loadDotEnv(projectRoot: string): Record<string, string>;
|
|
30
30
|
export declare function buildImage(config: ContainerConfig): void;
|
|
31
31
|
export declare function startContainer(config: ContainerConfig, repo: RepoOptions): Promise<AgentState>;
|
|
32
|
-
export declare function startRemoteContainer(config: ContainerConfig, repo: RepoOptions): Promise<AgentState>;
|
|
33
|
-
export declare function stopRemoteContainer(config: ContainerConfig, state: AgentState): Promise<void>;
|
|
34
32
|
export declare function stopContainer(config: ContainerConfig, containerName: string): void;
|
|
35
33
|
export declare function spawnTestContainer(config: ContainerConfig): Promise<void>;
|
package/dist/container.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "child_process";
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import { resolve } from "path";
|
|
4
|
-
import { createMachine, waitForMachine, destroyMachine, listMachines, createVolume, deleteVolume } from "./fly";
|
|
5
|
-
import { getImageRef } from "./image-ref";
|
|
6
4
|
const IMAGE_NAME = "app-building";
|
|
7
5
|
function debugLog(...args) {
|
|
8
6
|
if (process.env.DEBUG)
|
|
@@ -177,131 +175,6 @@ export async function startContainer(config, repo) {
|
|
|
177
175
|
config.registry.log(agentState);
|
|
178
176
|
return agentState;
|
|
179
177
|
}
|
|
180
|
-
export async function startRemoteContainer(config, repo) {
|
|
181
|
-
debugLog("startRemoteContainer config:", {
|
|
182
|
-
projectRoot: config.projectRoot,
|
|
183
|
-
flyApp: config.flyApp,
|
|
184
|
-
imageRef: config.imageRef,
|
|
185
|
-
webhookUrl: config.webhookUrl,
|
|
186
|
-
detached: config.detached,
|
|
187
|
-
initialPrompt: config.initialPrompt ? `${config.initialPrompt.slice(0, 100)}...` : undefined,
|
|
188
|
-
envVarKeys: Object.keys(config.envVars),
|
|
189
|
-
});
|
|
190
|
-
debugLog("startRemoteContainer repo:", repo);
|
|
191
|
-
if (!config.flyToken)
|
|
192
|
-
throw new Error("flyToken is required for remote containers");
|
|
193
|
-
if (!config.flyApp)
|
|
194
|
-
throw new Error("flyApp is required for remote containers");
|
|
195
|
-
const imageRef = config.imageRef ?? getImageRef();
|
|
196
|
-
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
197
|
-
const machineName = `app-building-${uniqueId}`;
|
|
198
|
-
// Build env vars for the machine
|
|
199
|
-
const remoteExtra = {
|
|
200
|
-
PORT: "3000",
|
|
201
|
-
CONTAINER_NAME: machineName,
|
|
202
|
-
};
|
|
203
|
-
if (config.webhookUrl)
|
|
204
|
-
remoteExtra.WEBHOOK_URL = config.webhookUrl;
|
|
205
|
-
if (config.detached)
|
|
206
|
-
remoteExtra.DETACHED = "1";
|
|
207
|
-
if (config.initialPrompt)
|
|
208
|
-
remoteExtra.INITIAL_PROMPT = config.initialPrompt;
|
|
209
|
-
const containerEnv = buildContainerEnv(repo, config.envVars, remoteExtra);
|
|
210
|
-
// Log existing machines (but don't destroy — multiple containers may run concurrently)
|
|
211
|
-
const existing = await listMachines(config.flyApp, config.flyToken);
|
|
212
|
-
if (existing.length > 0) {
|
|
213
|
-
console.log(`${existing.length} existing machine(s) in ${config.flyApp}:`);
|
|
214
|
-
for (const m of existing) {
|
|
215
|
-
console.log(` ${m.id} (${m.name}) — ${m.state}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// Create a volume for /repo storage
|
|
219
|
-
console.log("Creating Fly volume...");
|
|
220
|
-
const volumeId = await createVolume(config.flyApp, config.flyToken, `repo-${uniqueId}`);
|
|
221
|
-
console.log(`Volume created: ${volumeId}`);
|
|
222
|
-
// Retry machine creation — the registry tag may take a moment to propagate
|
|
223
|
-
console.log("Creating Fly machine...");
|
|
224
|
-
let machineId = "";
|
|
225
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
226
|
-
try {
|
|
227
|
-
machineId = await createMachine(config.flyApp, config.flyToken, imageRef, containerEnv, machineName, volumeId);
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
232
|
-
if (msg.includes("MANIFEST_UNKNOWN") && attempt < 4) {
|
|
233
|
-
console.log("Image not yet available in registry, retrying in 5s...");
|
|
234
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
// Clean up volume if machine creation fails
|
|
238
|
-
await deleteVolume(config.flyApp, config.flyToken, volumeId).catch(() => { });
|
|
239
|
-
throw err;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
console.log(`Machine created: ${machineId}`);
|
|
243
|
-
// Register immediately so the container is tracked even if startup times out
|
|
244
|
-
const baseUrl = `https://${config.flyApp}.fly.dev`;
|
|
245
|
-
const agentState = {
|
|
246
|
-
type: "remote",
|
|
247
|
-
containerName: machineName,
|
|
248
|
-
port: 443,
|
|
249
|
-
baseUrl,
|
|
250
|
-
flyApp: config.flyApp,
|
|
251
|
-
flyMachineId: machineId,
|
|
252
|
-
flyVolumeId: volumeId,
|
|
253
|
-
};
|
|
254
|
-
config.registry.log(agentState);
|
|
255
|
-
console.log("Waiting for machine to start...");
|
|
256
|
-
await waitForMachine(config.flyApp, config.flyToken, machineId);
|
|
257
|
-
console.log("Machine started.");
|
|
258
|
-
// Poll the public URL until the HTTP server is ready, targeting this specific machine
|
|
259
|
-
const maxWait = 180000;
|
|
260
|
-
const interval = 2000;
|
|
261
|
-
const start = Date.now();
|
|
262
|
-
let ready = false;
|
|
263
|
-
while (Date.now() - start < maxWait) {
|
|
264
|
-
try {
|
|
265
|
-
const res = await fetch(`${baseUrl}/status`, {
|
|
266
|
-
headers: { "fly-force-instance-id": machineId },
|
|
267
|
-
});
|
|
268
|
-
if (res.ok) {
|
|
269
|
-
ready = true;
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Not ready yet
|
|
275
|
-
}
|
|
276
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
277
|
-
}
|
|
278
|
-
if (!ready) {
|
|
279
|
-
// Clean up machine and volume if we can't reach it
|
|
280
|
-
console.log("Timed out waiting for machine, destroying...");
|
|
281
|
-
await destroyMachine(config.flyApp, config.flyToken, machineId).catch(() => { });
|
|
282
|
-
await deleteVolume(config.flyApp, config.flyToken, volumeId).catch(() => { });
|
|
283
|
-
throw new Error("Remote container did not become ready within timeout");
|
|
284
|
-
}
|
|
285
|
-
return agentState;
|
|
286
|
-
}
|
|
287
|
-
export async function stopRemoteContainer(config, state) {
|
|
288
|
-
if (!state.flyApp || !state.flyMachineId) {
|
|
289
|
-
throw new Error("Missing flyApp or flyMachineId in agent state");
|
|
290
|
-
}
|
|
291
|
-
if (!config.flyToken)
|
|
292
|
-
throw new Error("flyToken is required to stop remote container");
|
|
293
|
-
console.log(`Destroying Fly machine ${state.flyMachineId}...`);
|
|
294
|
-
await destroyMachine(state.flyApp, config.flyToken, state.flyMachineId);
|
|
295
|
-
console.log("Machine destroyed.");
|
|
296
|
-
if (state.flyVolumeId) {
|
|
297
|
-
console.log(`Deleting Fly volume ${state.flyVolumeId}...`);
|
|
298
|
-
await deleteVolume(state.flyApp, config.flyToken, state.flyVolumeId).catch((err) => {
|
|
299
|
-
console.log(`Warning: failed to delete volume: ${err instanceof Error ? err.message : err}`);
|
|
300
|
-
});
|
|
301
|
-
console.log("Volume deleted.");
|
|
302
|
-
}
|
|
303
|
-
config.registry.markStopped(state.containerName);
|
|
304
|
-
}
|
|
305
178
|
export function stopContainer(config, containerName) {
|
|
306
179
|
try {
|
|
307
180
|
execFileSync("docker", ["stop", containerName], { stdio: "ignore", timeout: 30000 });
|
package/dist/fly.d.ts
CHANGED
|
@@ -11,20 +11,24 @@ export declare function createVolume(app: string, token: string, name: string, s
|
|
|
11
11
|
* Delete a Fly Volume.
|
|
12
12
|
*/
|
|
13
13
|
export declare function deleteVolume(app: string, token: string, volumeId: string): Promise<void>;
|
|
14
|
+
export interface CreateMachineResult {
|
|
15
|
+
machineId: string;
|
|
16
|
+
volumeId: string;
|
|
17
|
+
}
|
|
14
18
|
/**
|
|
15
19
|
* Create a Fly Machine with the given image and env vars.
|
|
16
|
-
*
|
|
17
|
-
* Returns the machine ID.
|
|
20
|
+
* Creates a volume mounted at /repo for storage.
|
|
21
|
+
* Returns the machine ID and volume ID.
|
|
18
22
|
*/
|
|
19
|
-
export declare function createMachine(app: string, token: string, image: string, env: Record<string, string>, name: string
|
|
23
|
+
export declare function createMachine(app: string, token: string, image: string, env: Record<string, string>, name: string): Promise<CreateMachineResult>;
|
|
20
24
|
/**
|
|
21
25
|
* Wait for a Fly Machine to reach the "started" state.
|
|
22
26
|
*/
|
|
23
27
|
export declare function waitForMachine(app: string, token: string, machineId: string, timeoutMs?: number): Promise<void>;
|
|
24
28
|
/**
|
|
25
|
-
* Destroy a Fly Machine (force).
|
|
29
|
+
* Destroy a Fly Machine (force) and its attached volume.
|
|
26
30
|
*/
|
|
27
|
-
export declare function destroyMachine(app: string, token: string, machineId: string): Promise<void>;
|
|
31
|
+
export declare function destroyMachine(app: string, token: string, machineId: string, volumeId?: string): Promise<void>;
|
|
28
32
|
export interface FlyMachineInfo {
|
|
29
33
|
id: string;
|
|
30
34
|
name: string;
|
package/dist/fly.js
CHANGED
|
@@ -78,39 +78,48 @@ export async function deleteVolume(app, token, volumeId) {
|
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Create a Fly Machine with the given image and env vars.
|
|
81
|
-
*
|
|
82
|
-
* Returns the machine ID.
|
|
81
|
+
* Creates a volume mounted at /repo for storage.
|
|
82
|
+
* Returns the machine ID and volume ID.
|
|
83
83
|
*/
|
|
84
|
-
export async function createMachine(app, token, image, env, name
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
84
|
+
export async function createMachine(app, token, image, env, name) {
|
|
85
|
+
// Create a volume for /repo storage
|
|
86
|
+
const volumeId = await createVolume(app, token, `repo-${name}`, 50);
|
|
87
|
+
try {
|
|
88
|
+
const res = await flyFetch(`/apps/${app}/machines`, token, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
name,
|
|
92
|
+
config: {
|
|
93
|
+
image,
|
|
94
|
+
env,
|
|
95
|
+
auto_destroy: true,
|
|
96
|
+
restart: { policy: "no" },
|
|
97
|
+
guest: {
|
|
98
|
+
cpu_kind: "performance",
|
|
99
|
+
cpus: 16,
|
|
100
|
+
memory_mb: 32768,
|
|
101
|
+
},
|
|
102
|
+
mounts: [{ volume: volumeId, path: "/repo" }],
|
|
103
|
+
services: [
|
|
104
|
+
{
|
|
105
|
+
ports: [{ port: 443, handlers: ["tls", "http"] }],
|
|
106
|
+
protocol: "tcp",
|
|
107
|
+
internal_port: 3000,
|
|
108
|
+
autostart: false,
|
|
109
|
+
autostop: "off",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
const data = (await res.json());
|
|
116
|
+
return { machineId: data.id, volumeId };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// Clean up volume if machine creation fails
|
|
120
|
+
await deleteVolume(app, token, volumeId).catch(() => { });
|
|
121
|
+
throw err;
|
|
107
122
|
}
|
|
108
|
-
const res = await flyFetch(`/apps/${app}/machines`, token, {
|
|
109
|
-
method: "POST",
|
|
110
|
-
body: JSON.stringify({ name, config }),
|
|
111
|
-
});
|
|
112
|
-
const data = (await res.json());
|
|
113
|
-
return data.id;
|
|
114
123
|
}
|
|
115
124
|
/**
|
|
116
125
|
* Wait for a Fly Machine to reach the "started" state.
|
|
@@ -130,12 +139,17 @@ export async function waitForMachine(app, token, machineId, timeoutMs = 180000)
|
|
|
130
139
|
throw new Error(`Machine ${machineId} did not reach started state within ${timeoutMs / 1000}s`);
|
|
131
140
|
}
|
|
132
141
|
/**
|
|
133
|
-
* Destroy a Fly Machine (force).
|
|
142
|
+
* Destroy a Fly Machine (force) and its attached volume.
|
|
134
143
|
*/
|
|
135
|
-
export async function destroyMachine(app, token, machineId) {
|
|
144
|
+
export async function destroyMachine(app, token, machineId, volumeId) {
|
|
136
145
|
await flyFetch(`/apps/${app}/machines/${machineId}?force=true`, token, {
|
|
137
146
|
method: "DELETE",
|
|
138
147
|
});
|
|
148
|
+
if (volumeId) {
|
|
149
|
+
await deleteVolume(app, token, volumeId).catch((err) => {
|
|
150
|
+
console.log(`Warning: failed to delete volume ${volumeId}: ${err instanceof Error ? err.message : err}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
139
153
|
}
|
|
140
154
|
/**
|
|
141
155
|
* List all machines for a Fly app.
|