@replayio/app-building 1.0.2 → 1.1.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 +146 -63
- package/dist/{package/container-registry.d.ts → container-registry.d.ts} +9 -1
- package/dist/{package/container-registry.js → container-registry.js} +1 -1
- package/dist/{package/container.d.ts → container.d.ts} +2 -1
- package/dist/{package/container.js → container.js} +14 -4
- package/package.json +6 -23
- /package/dist/{package/container-utils.d.ts → container-utils.d.ts} +0 -0
- /package/dist/{package/container-utils.js → container-utils.js} +0 -0
- /package/dist/{package/fly.d.ts → fly.d.ts} +0 -0
- /package/dist/{package/fly.js → fly.js} +0 -0
- /package/dist/{package/http-client.d.ts → http-client.d.ts} +0 -0
- /package/dist/{package/http-client.js → http-client.js} +0 -0
- /package/dist/{package/image-ref.d.ts → image-ref.d.ts} +0 -0
- /package/dist/{package/image-ref.js → image-ref.js} +0 -0
- /package/dist/{package/index.d.ts → index.d.ts} +0 -0
- /package/dist/{package/index.js → index.js} +0 -0
package/README.md
CHANGED
|
@@ -1,92 +1,175 @@
|
|
|
1
|
-
# app-building
|
|
1
|
+
# @replayio/app-building
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
according to a spec without human involvement along the way. Example use cases:
|
|
3
|
+
Library for managing agentic app-building containers. Start and stop containers locally (Docker) or remotely (Fly.io), communicate with the in-container HTTP server, and track container state via a local registry.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
* `npm run agent -- -p "Continue maintaining app XYZ and fix these bugs: ..."`
|
|
8
|
-
* `npm run agent -- -i` for interactive access to the agent.
|
|
5
|
+
## Install
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @replayio/app-building
|
|
9
|
+
```
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
loadDotEnv,
|
|
16
|
+
FileContainerRegistry,
|
|
17
|
+
type ContainerConfig,
|
|
18
|
+
type RepoOptions,
|
|
19
|
+
startRemoteContainer,
|
|
20
|
+
stopRemoteContainer,
|
|
21
|
+
httpGet,
|
|
22
|
+
httpPost,
|
|
23
|
+
httpOptsFor,
|
|
24
|
+
} from "@replayio/app-building";
|
|
25
|
+
|
|
26
|
+
// Assemble config once at startup
|
|
27
|
+
const envVars = loadDotEnv("/path/to/project");
|
|
28
|
+
const config: ContainerConfig = {
|
|
29
|
+
projectRoot: "/path/to/project", // optional — only needed for local Docker operations
|
|
30
|
+
envVars,
|
|
31
|
+
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
32
|
+
flyToken: envVars.FLY_API_TOKEN,
|
|
33
|
+
flyApp: envVars.FLY_APP_NAME,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Start a remote container
|
|
37
|
+
const repo: RepoOptions = { repoUrl: "https://github.com/...", cloneBranch: "main", pushBranch: "main" };
|
|
38
|
+
const state = await startRemoteContainer(config, repo);
|
|
39
|
+
|
|
40
|
+
// Send a prompt and wait for completion
|
|
41
|
+
const httpOpts = httpOptsFor(state);
|
|
42
|
+
const { id } = await httpPost(`${state.baseUrl}/message`, { prompt: "Build the app" }, httpOpts);
|
|
43
|
+
|
|
44
|
+
// Check status
|
|
45
|
+
const status = await httpGet(`${state.baseUrl}/status`, httpOpts);
|
|
46
|
+
|
|
47
|
+
// Query the registry
|
|
48
|
+
const alive = await config.registry.findAlive();
|
|
49
|
+
|
|
50
|
+
// Clean up
|
|
51
|
+
await stopRemoteContainer(config, state);
|
|
52
|
+
```
|
|
18
53
|
|
|
19
|
-
|
|
54
|
+
## Exported API
|
|
20
55
|
|
|
21
|
-
|
|
56
|
+
### Domain objects
|
|
22
57
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
58
|
+
| Export | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars`, `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`. See [Webhooks](#webhooks) below. |
|
|
61
|
+
| `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
|
|
62
|
+
| `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
|
|
63
|
+
| `FileContainerRegistry` | Built-in file-backed implementation of `ContainerRegistry`, backed by a `.jsonl` file. |
|
|
26
64
|
|
|
27
|
-
|
|
65
|
+
### Container lifecycle
|
|
28
66
|
|
|
29
|
-
|
|
67
|
+
| Export | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `startContainer(config, repo)` | Build the Docker image locally and start a container with `--network host`. Returns `AgentState`. |
|
|
70
|
+
| `startRemoteContainer(config, repo)` | Create a Fly.io machine using the GHCR image. Requires `config.flyToken` and `config.flyApp`. Returns `AgentState`. |
|
|
71
|
+
| `stopContainer(config, containerName)` | Stop a local Docker container by name. |
|
|
72
|
+
| `stopRemoteContainer(config, state)` | Destroy the Fly.io machine for a remote container. Requires `config.flyToken`. |
|
|
73
|
+
| `buildImage(config)` | Build the Docker image locally (called automatically by `startContainer`). |
|
|
74
|
+
| `spawnTestContainer(config)` | Start an interactive (`-it`) container with the repo mounted at `/repo`. |
|
|
75
|
+
| `loadDotEnv(projectRoot)` | Parse a `.env` file and return key-value pairs. |
|
|
30
76
|
|
|
31
|
-
`
|
|
77
|
+
**Types:** `AgentState`, `ContainerConfig`, `RepoOptions`
|
|
32
78
|
|
|
33
|
-
###
|
|
79
|
+
### Container registry (`ContainerRegistry` interface / `FileContainerRegistry` class)
|
|
34
80
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
| Method | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `log(state)` | Append a new entry to the registry. |
|
|
84
|
+
| `markStopped(name?)` | Mark a container as stopped. |
|
|
85
|
+
| `clearStopped(name)` | Clear the stopped flag (container came back alive). |
|
|
86
|
+
| `getRecent(limit?)` | Read the most recent registry entries. |
|
|
87
|
+
| `find(name)` | Find a specific container by name. |
|
|
88
|
+
| `findAlive()` | Probe recent entries and return those that are alive. Reconciles stopped flags. |
|
|
40
89
|
|
|
41
|
-
|
|
90
|
+
**Types:** `RegistryEntry`
|
|
42
91
|
|
|
43
|
-
###
|
|
92
|
+
### HTTP client
|
|
44
93
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
94
|
+
| Export | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `httpGet(url, opts?)` | GET with retries and timeout. Returns parsed JSON. |
|
|
97
|
+
| `httpPost(url, body?, opts?)` | POST with retries and timeout. Returns parsed JSON. |
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
**Types:** `HttpOptions`
|
|
50
100
|
|
|
51
|
-
###
|
|
101
|
+
### Container utilities
|
|
52
102
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
103
|
+
| Export | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `httpOptsFor(state)` | Return `HttpOptions` for a container (adds `fly-force-instance-id` header for remote containers). |
|
|
106
|
+
| `probeAlive(entry)` | Check if a container is responding to `/status`. |
|
|
56
107
|
|
|
57
|
-
|
|
108
|
+
### Fly.io machines
|
|
58
109
|
|
|
59
|
-
|
|
110
|
+
| Export | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `createApp(token, name, org?)` | Create a Fly app and allocate IPs. |
|
|
113
|
+
| `createMachine(app, token, image, env, name)` | Create a Fly machine. Returns machine ID. |
|
|
114
|
+
| `waitForMachine(app, token, machineId, timeout?)` | Wait for a machine to reach `started` state. |
|
|
115
|
+
| `destroyMachine(app, token, machineId)` | Force-destroy a machine. |
|
|
116
|
+
| `listMachines(app, token)` | List all machines for an app. |
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
npm run stop
|
|
63
|
-
npm run stop -- <containerName>
|
|
64
|
-
```
|
|
118
|
+
**Types:** `FlyMachineInfo`
|
|
65
119
|
|
|
66
|
-
|
|
120
|
+
### Image ref
|
|
67
121
|
|
|
68
|
-
|
|
122
|
+
| Export | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default. Used by `startRemoteContainer`. |
|
|
69
125
|
|
|
70
|
-
|
|
71
|
-
high quality, well tested apps from the initial spec. During the initial app build
|
|
72
|
-
it does the following:
|
|
126
|
+
## Webhooks
|
|
73
127
|
|
|
74
|
-
|
|
75
|
-
2. Builds the app and writes tests to match the spec.
|
|
76
|
-
3. Gets all the tests to pass, deploys to production, and does further testing.
|
|
128
|
+
Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs fire-and-forget JSON to that URL on key events (no retries, failures are silently ignored).
|
|
77
129
|
|
|
78
|
-
|
|
79
|
-
where it checks to make sure it closely followed the spec and skill directives and fixes
|
|
80
|
-
any issues it finds. It will also fix reported bugs and update the skills to avoid
|
|
81
|
-
similar problems in the future.
|
|
130
|
+
### Payload format
|
|
82
131
|
|
|
83
|
-
|
|
84
|
-
do it but not always) the agent will converge on an app that follows the initial spec
|
|
85
|
-
and skill directives.
|
|
132
|
+
Every POST body has this shape:
|
|
86
133
|
|
|
87
|
-
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"type": "container.started",
|
|
137
|
+
"containerName": "app-building-abc123",
|
|
138
|
+
"timestamp": "2026-02-28T12:00:00.000Z",
|
|
139
|
+
"data": { ... }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
88
142
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
143
|
+
| Field | Type | Description |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `type` | `string` | Event type (see table below). |
|
|
146
|
+
| `containerName` | `string` | Name of the container that emitted the event. |
|
|
147
|
+
| `timestamp` | `string` | ISO-8601 timestamp. |
|
|
148
|
+
| `data` | `object` | Event-specific payload. Present on all events; contents vary by type. |
|
|
149
|
+
|
|
150
|
+
### Events
|
|
151
|
+
|
|
152
|
+
| Type | When | `data` fields |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `container.started` | HTTP server is listening | `pushBranch`, `revision` |
|
|
155
|
+
| `container.idle` | State transitions to idle | `pendingTasks`, `queueLength` |
|
|
156
|
+
| `container.processing` | State transitions to processing | `iteration` |
|
|
157
|
+
| `container.stopping` | State transitions to stopping | _(empty)_ |
|
|
158
|
+
| `container.stopped` | State transitions to stopped | _(empty)_ |
|
|
159
|
+
| `message.queued` | `POST /message` received | `messageId`, `prompt` |
|
|
160
|
+
| `message.done` | Message processing complete | `messageId`, `cost_usd`, `duration_ms`, `num_turns` |
|
|
161
|
+
| `message.error` | Message processing failed | `messageId`, `error` |
|
|
162
|
+
| `task.started` | Task processing begins | `pendingTasks` |
|
|
163
|
+
| `task.done` | Task processing complete | `tasksProcessed`, `totalCost` |
|
|
164
|
+
| `log` | Each log line | `line` |
|
|
165
|
+
|
|
166
|
+
### Example
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const config: ContainerConfig = {
|
|
170
|
+
projectRoot: "/path/to/project",
|
|
171
|
+
envVars: loadDotEnv("/path/to/project"),
|
|
172
|
+
registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
|
|
173
|
+
webhookUrl: "https://example.com/hooks/container-events",
|
|
174
|
+
};
|
|
175
|
+
```
|
|
@@ -3,7 +3,15 @@ export interface RegistryEntry extends AgentState {
|
|
|
3
3
|
startedAt: string;
|
|
4
4
|
stoppedAt?: string;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
6
|
+
export interface ContainerRegistry {
|
|
7
|
+
log(state: AgentState): void;
|
|
8
|
+
markStopped(containerName?: string): void;
|
|
9
|
+
clearStopped(containerName: string): void;
|
|
10
|
+
getRecent(limit?: number): RegistryEntry[];
|
|
11
|
+
find(containerName: string): RegistryEntry | null;
|
|
12
|
+
findAlive(): Promise<RegistryEntry[]>;
|
|
13
|
+
}
|
|
14
|
+
export declare class FileContainerRegistry implements ContainerRegistry {
|
|
7
15
|
private filePath;
|
|
8
16
|
constructor(filePath: string);
|
|
9
17
|
private readRegistry;
|
|
@@ -8,12 +8,13 @@ export interface AgentState {
|
|
|
8
8
|
flyMachineId?: string;
|
|
9
9
|
}
|
|
10
10
|
export interface ContainerConfig {
|
|
11
|
-
projectRoot
|
|
11
|
+
projectRoot?: string;
|
|
12
12
|
envVars: Record<string, string>;
|
|
13
13
|
registry: ContainerRegistry;
|
|
14
14
|
flyToken?: string;
|
|
15
15
|
flyApp?: string;
|
|
16
16
|
imageRef?: string;
|
|
17
|
+
webhookUrl?: string;
|
|
17
18
|
}
|
|
18
19
|
export interface RepoOptions {
|
|
19
20
|
repoUrl: string;
|
|
@@ -28,6 +28,8 @@ export function loadDotEnv(projectRoot) {
|
|
|
28
28
|
return vars;
|
|
29
29
|
}
|
|
30
30
|
export function buildImage(config) {
|
|
31
|
+
if (!config.projectRoot)
|
|
32
|
+
throw new Error("projectRoot is required for local Docker operations");
|
|
31
33
|
console.log("Building Docker image...");
|
|
32
34
|
execFileSync("docker", ["build", "--network", "host", "-t", IMAGE_NAME, config.projectRoot], {
|
|
33
35
|
stdio: "inherit",
|
|
@@ -92,10 +94,13 @@ export async function startContainer(config, repo) {
|
|
|
92
94
|
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
93
95
|
const containerName = `app-building-${uniqueId}`;
|
|
94
96
|
const hostPort = findFreePort();
|
|
95
|
-
const
|
|
97
|
+
const extra = {
|
|
96
98
|
PORT: String(hostPort),
|
|
97
99
|
CONTAINER_NAME: containerName,
|
|
98
|
-
}
|
|
100
|
+
};
|
|
101
|
+
if (config.webhookUrl)
|
|
102
|
+
extra.WEBHOOK_URL = config.webhookUrl;
|
|
103
|
+
const containerEnv = buildContainerEnv(repo, config.envVars, extra);
|
|
99
104
|
// Build docker run args
|
|
100
105
|
const args = ["run", "-d", "--rm", "--name", containerName];
|
|
101
106
|
// --network host: container shares host network stack (no -p needed)
|
|
@@ -163,10 +168,13 @@ export async function startRemoteContainer(config, repo) {
|
|
|
163
168
|
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
164
169
|
const machineName = `app-building-${uniqueId}`;
|
|
165
170
|
// Build env vars for the machine
|
|
166
|
-
const
|
|
171
|
+
const remoteExtra = {
|
|
167
172
|
PORT: "3000",
|
|
168
173
|
CONTAINER_NAME: machineName,
|
|
169
|
-
}
|
|
174
|
+
};
|
|
175
|
+
if (config.webhookUrl)
|
|
176
|
+
remoteExtra.WEBHOOK_URL = config.webhookUrl;
|
|
177
|
+
const containerEnv = buildContainerEnv(repo, config.envVars, remoteExtra);
|
|
170
178
|
// Remove Fly-specific vars from container env (not needed inside)
|
|
171
179
|
delete containerEnv.FLY_API_TOKEN;
|
|
172
180
|
delete containerEnv.FLY_APP_NAME;
|
|
@@ -260,6 +268,8 @@ export function stopContainer(config, containerName) {
|
|
|
260
268
|
config.registry.markStopped(containerName);
|
|
261
269
|
}
|
|
262
270
|
export function spawnTestContainer(config) {
|
|
271
|
+
if (!config.projectRoot)
|
|
272
|
+
throw new Error("projectRoot is required for local Docker operations");
|
|
263
273
|
ensureImageExists(config.projectRoot);
|
|
264
274
|
const uniqueId = Math.random().toString(36).slice(2, 8);
|
|
265
275
|
const containerName = `app-building-test-${uniqueId}`;
|
package/package.json
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replayio/app-building",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Library for managing agentic app-building containers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"import": "./dist/
|
|
9
|
-
"types": "./dist/
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist
|
|
13
|
+
"dist"
|
|
14
14
|
],
|
|
15
|
-
"readme": "src/package/README.md",
|
|
16
15
|
"scripts": {
|
|
17
|
-
"
|
|
18
|
-
"status": "tsx src/status.ts",
|
|
19
|
-
"read-log": "tsx src/read-log.ts",
|
|
20
|
-
"reset-app": "tsx src/reset-app.ts",
|
|
21
|
-
"docker:build": "docker build --no-cache --network=host -t app-building .",
|
|
22
|
-
"test-container": "tsx src/test-container.ts",
|
|
23
|
-
"stop": "tsx src/stop.ts",
|
|
24
|
-
"fly-machines": "tsx src/fly-machines.ts",
|
|
25
|
-
"add-task": "tsx scripts/add-task.ts"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/node": "^25.3.0",
|
|
29
|
-
"tsx": "^4.19.0",
|
|
30
|
-
"typescript": "^5.5.0"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"commander": "^14.0.3"
|
|
16
|
+
"prepublishOnly": "tsc -p tsconfig.json"
|
|
34
17
|
}
|
|
35
18
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|