workplane 0.1.1
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/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/cli.js +340 -0
- package/dist/cli.js.map +1 -0
- package/dist/migrate.js +120 -0
- package/dist/migrate.js.map +1 -0
- package/dist/node.js +606 -0
- package/dist/node.js.map +1 -0
- package/dist/schema.sql +62 -0
- package/dist/server.js +851 -0
- package/dist/server.js.map +1 -0
- package/package.json +54 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../node/src/index.ts
|
|
4
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
5
|
+
import { join as join6, resolve } from "path";
|
|
6
|
+
|
|
7
|
+
// ../core/src/config.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// ../core/src/env.ts
|
|
11
|
+
import { config as dotenvConfig } from "dotenv";
|
|
12
|
+
var envLoaded = false;
|
|
13
|
+
function loadLocalEnv() {
|
|
14
|
+
if (envLoaded) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
dotenvConfig({ path: ".env" });
|
|
18
|
+
dotenvConfig({ path: ".env.local", override: true });
|
|
19
|
+
envLoaded = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../core/src/config.ts
|
|
23
|
+
loadLocalEnv();
|
|
24
|
+
var serverEnvSchema = z.object({
|
|
25
|
+
DATABASE_URL: z.string().min(1),
|
|
26
|
+
WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
|
|
27
|
+
WORKPLANE_NODE_TOKEN: z.string().optional(),
|
|
28
|
+
WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
|
|
29
|
+
WORKPLANE_ENV_ALLOWLIST: z.string().optional()
|
|
30
|
+
});
|
|
31
|
+
var nodeEnvSchema = z.object({
|
|
32
|
+
WORKPLANE_SERVER_URL: z.string().url().optional().default("http://localhost:8787"),
|
|
33
|
+
WORKPLANE_NODE_NAME: z.string().optional().default("local-node-1"),
|
|
34
|
+
WORKPLANE_NODE_ID: z.string().optional(),
|
|
35
|
+
WORKPLANE_NODE_CAPABILITIES: z.string().optional().default("shell,git,node,typescript,aider,ollama,codex,claude-code"),
|
|
36
|
+
WORKPLANE_NODE_TOKEN: z.string().optional(),
|
|
37
|
+
WORKPLANE_POLL_INTERVAL_MS: z.string().optional().default("3000").transform((value) => Number.parseInt(value, 10)),
|
|
38
|
+
WORKPLANE_ENV_ALLOWLIST: z.string().optional()
|
|
39
|
+
});
|
|
40
|
+
function loadServerConfig(env = process.env) {
|
|
41
|
+
const parsed = serverEnvSchema.parse(env);
|
|
42
|
+
return {
|
|
43
|
+
databaseUrl: parsed.DATABASE_URL,
|
|
44
|
+
port: parsed.WORKPLANE_SERVER_PORT,
|
|
45
|
+
nodeToken: parsed.WORKPLANE_NODE_TOKEN,
|
|
46
|
+
operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,
|
|
47
|
+
envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? "")
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function loadNodeConfig(env = process.env) {
|
|
51
|
+
const parsed = nodeEnvSchema.parse(env);
|
|
52
|
+
return {
|
|
53
|
+
serverUrl: parsed.WORKPLANE_SERVER_URL,
|
|
54
|
+
nodeName: parsed.WORKPLANE_NODE_NAME,
|
|
55
|
+
nodeId: parsed.WORKPLANE_NODE_ID,
|
|
56
|
+
nodeCapabilities: parseCsv(parsed.WORKPLANE_NODE_CAPABILITIES),
|
|
57
|
+
nodeToken: parsed.WORKPLANE_NODE_TOKEN,
|
|
58
|
+
pollIntervalMs: parsed.WORKPLANE_POLL_INTERVAL_MS,
|
|
59
|
+
envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? "")
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseCsv(value) {
|
|
63
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../core/src/auth.ts
|
|
67
|
+
function authHeaders(token) {
|
|
68
|
+
if (!token) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
return { authorization: `Bearer ${token}` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ../core/src/http-client.ts
|
|
75
|
+
function buildWorkplaneHeaders(options = {}) {
|
|
76
|
+
const token = options.nodeToken ?? options.operatorToken;
|
|
77
|
+
return {
|
|
78
|
+
"content-type": "application/json",
|
|
79
|
+
...authHeaders(token)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function workplaneFetch(serverUrl, path, options = {}) {
|
|
83
|
+
const response = await fetch(`${serverUrl}${path}`, {
|
|
84
|
+
method: options.method ?? "GET",
|
|
85
|
+
headers: buildWorkplaneHeaders(options),
|
|
86
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
throw new Error(`${response.status} ${response.statusText} from ${path}: ${text}`);
|
|
91
|
+
}
|
|
92
|
+
return await response.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ../core/src/git.ts
|
|
96
|
+
import { join } from "path";
|
|
97
|
+
function repoPath(workspacePath) {
|
|
98
|
+
return join(workspacePath, "repo");
|
|
99
|
+
}
|
|
100
|
+
async function createTaskBranch(context, branchName) {
|
|
101
|
+
const cwd = repoPath(context.workspacePath);
|
|
102
|
+
await context.log("system", `creating branch ${branchName}`, "git-branch");
|
|
103
|
+
const checkout = await context.exec("git", ["checkout", "-b", branchName], { cwd });
|
|
104
|
+
if (checkout.exitCode !== 0) {
|
|
105
|
+
throw new Error(`git checkout -b failed with exit code ${checkout.exitCode}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function defaultTaskBranchName(runId) {
|
|
109
|
+
return `workplane/${runId}`;
|
|
110
|
+
}
|
|
111
|
+
async function captureGitDiff(context, adapterName, artifactName = "changes.diff") {
|
|
112
|
+
const cwd = repoPath(context.workspacePath);
|
|
113
|
+
const diffResult = await context.exec("git", ["diff"], { cwd });
|
|
114
|
+
const artifactsPath = await context.ensureWorkspace("artifacts");
|
|
115
|
+
const diffPath = join(artifactsPath, artifactName);
|
|
116
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
117
|
+
await writeFile4(diffPath, diffResult.stdout, "utf8");
|
|
118
|
+
await context.emitArtifact({
|
|
119
|
+
type: "diff",
|
|
120
|
+
name: artifactName,
|
|
121
|
+
path: diffPath,
|
|
122
|
+
metadata: { adapter: adapterName }
|
|
123
|
+
});
|
|
124
|
+
return diffPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ../adapter-sdk/src/index.ts
|
|
128
|
+
import { spawn } from "child_process";
|
|
129
|
+
import { mkdir } from "fs/promises";
|
|
130
|
+
import { join as join2 } from "path";
|
|
131
|
+
function pickEnv(allowlist) {
|
|
132
|
+
if (!allowlist || allowlist.length === 0) {
|
|
133
|
+
return { ...process.env };
|
|
134
|
+
}
|
|
135
|
+
const picked = {};
|
|
136
|
+
for (const key of allowlist) {
|
|
137
|
+
const value = process.env[key];
|
|
138
|
+
if (value !== void 0) {
|
|
139
|
+
picked[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
picked.PATH = process.env.PATH ?? "";
|
|
143
|
+
picked.HOME = process.env.HOME ?? "";
|
|
144
|
+
return picked;
|
|
145
|
+
}
|
|
146
|
+
function createCancellableExec(context) {
|
|
147
|
+
let activeChild;
|
|
148
|
+
const exec = async (command, args = [], options = {}) => new Promise((resolve2) => {
|
|
149
|
+
let childError;
|
|
150
|
+
const child = spawn(command, args, {
|
|
151
|
+
cwd: options.cwd ?? context.workspacePath,
|
|
152
|
+
env: {
|
|
153
|
+
...pickEnv(context.envAllowlist),
|
|
154
|
+
...options.env ?? {}
|
|
155
|
+
},
|
|
156
|
+
signal: options.signal
|
|
157
|
+
});
|
|
158
|
+
activeChild = child;
|
|
159
|
+
let stdout = "";
|
|
160
|
+
let stderr = "";
|
|
161
|
+
child.stdout.on("data", (chunk) => {
|
|
162
|
+
const text = chunk.toString();
|
|
163
|
+
stdout += text;
|
|
164
|
+
void context.log("stdout", text);
|
|
165
|
+
});
|
|
166
|
+
child.stderr.on("data", (chunk) => {
|
|
167
|
+
const text = chunk.toString();
|
|
168
|
+
stderr += text;
|
|
169
|
+
void context.log("stderr", text);
|
|
170
|
+
});
|
|
171
|
+
child.on("error", (error) => {
|
|
172
|
+
childError = error;
|
|
173
|
+
});
|
|
174
|
+
child.on("close", (exitCode, signal) => {
|
|
175
|
+
if (activeChild === child) {
|
|
176
|
+
activeChild = void 0;
|
|
177
|
+
}
|
|
178
|
+
if (signal === "SIGTERM" || signal === "SIGKILL") {
|
|
179
|
+
resolve2({
|
|
180
|
+
exitCode: exitCode ?? 1,
|
|
181
|
+
stdout,
|
|
182
|
+
stderr: `${stderr}
|
|
183
|
+
process terminated (${signal})`.trim()
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
resolve2({
|
|
188
|
+
exitCode: exitCode ?? 1,
|
|
189
|
+
stdout,
|
|
190
|
+
stderr: childError ? `${stderr}
|
|
191
|
+
${childError.message}`.trim() : stderr
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
const kill = () => {
|
|
196
|
+
if (activeChild && !activeChild.killed) {
|
|
197
|
+
activeChild.kill("SIGTERM");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
return { exec, kill };
|
|
201
|
+
}
|
|
202
|
+
async function ensureWorkspacePath(workspacePath, ...segments) {
|
|
203
|
+
const fullPath = join2(workspacePath, ...segments);
|
|
204
|
+
await mkdir(fullPath, { recursive: true });
|
|
205
|
+
return fullPath;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ../adapter-shell/src/index.ts
|
|
209
|
+
import { join as join3 } from "path";
|
|
210
|
+
import { writeFile } from "fs/promises";
|
|
211
|
+
var shellAdapter = {
|
|
212
|
+
name: "shell",
|
|
213
|
+
kind: "shell.exec",
|
|
214
|
+
async run(context, payload) {
|
|
215
|
+
const renderedCommand = payload.args && payload.args.length > 0 ? `${payload.command} ${payload.args.join(" ")}` : payload.command;
|
|
216
|
+
const commandCwd = payload.cwd ? payload.cwd.startsWith("/") ? payload.cwd : join3(context.workspacePath, payload.cwd) : context.workspacePath;
|
|
217
|
+
const result = await context.exec("/bin/sh", ["-lc", renderedCommand], { cwd: commandCwd });
|
|
218
|
+
const artifactsDir = await context.ensureWorkspace("artifacts");
|
|
219
|
+
const resultPath = join3(artifactsDir, "shell-result.json");
|
|
220
|
+
await writeFile(
|
|
221
|
+
resultPath,
|
|
222
|
+
JSON.stringify(
|
|
223
|
+
{
|
|
224
|
+
command: renderedCommand,
|
|
225
|
+
cwd: commandCwd,
|
|
226
|
+
exitCode: result.exitCode
|
|
227
|
+
},
|
|
228
|
+
null,
|
|
229
|
+
2
|
|
230
|
+
),
|
|
231
|
+
"utf8"
|
|
232
|
+
);
|
|
233
|
+
await context.emitArtifact({
|
|
234
|
+
type: "shell.result",
|
|
235
|
+
name: "shell-result.json",
|
|
236
|
+
path: resultPath,
|
|
237
|
+
metadata: { exitCode: result.exitCode }
|
|
238
|
+
});
|
|
239
|
+
if (result.exitCode !== 0) {
|
|
240
|
+
throw new Error(`shell command failed with exit code ${result.exitCode}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// ../adapter-aider/src/index.ts
|
|
246
|
+
var aiderAdapter = {
|
|
247
|
+
name: "aider",
|
|
248
|
+
kind: "agent.run",
|
|
249
|
+
async run(context, payload) {
|
|
250
|
+
const cwd = repoPath(context.workspacePath);
|
|
251
|
+
await createTaskBranch(context, defaultTaskBranchName(context.runId));
|
|
252
|
+
const args = ["--yes", "--message", payload.prompt];
|
|
253
|
+
if (payload.model) {
|
|
254
|
+
args.unshift("--model", payload.model);
|
|
255
|
+
}
|
|
256
|
+
const result = await context.exec("aider", args, { cwd });
|
|
257
|
+
if (result.exitCode !== 0) {
|
|
258
|
+
throw new Error(`aider failed with exit code ${result.exitCode}`);
|
|
259
|
+
}
|
|
260
|
+
await captureGitDiff(context, "aider");
|
|
261
|
+
if (payload.testCommand) {
|
|
262
|
+
await context.log("system", `running test command: ${payload.testCommand}`, "run-tests");
|
|
263
|
+
const testResult = await context.exec("/bin/sh", ["-lc", payload.testCommand], { cwd });
|
|
264
|
+
if (testResult.exitCode !== 0) {
|
|
265
|
+
throw new Error(`test command failed with exit code ${testResult.exitCode}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// ../adapter-ollama/src/index.ts
|
|
272
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
273
|
+
import { join as join4 } from "path";
|
|
274
|
+
var ollamaAdapter = {
|
|
275
|
+
name: "ollama",
|
|
276
|
+
kind: "inference.batch",
|
|
277
|
+
async run(context, payload) {
|
|
278
|
+
const args = ["run", payload.model, payload.prompt];
|
|
279
|
+
const result = await context.exec("ollama", args, { cwd: context.workspacePath });
|
|
280
|
+
if (result.exitCode !== 0) {
|
|
281
|
+
throw new Error(`ollama failed with exit code ${result.exitCode}`);
|
|
282
|
+
}
|
|
283
|
+
const artifactsPath = await context.ensureWorkspace("artifacts");
|
|
284
|
+
const outputPath = join4(artifactsPath, "inference-output.txt");
|
|
285
|
+
const output = `${result.stdout}${result.stderr}`.trim();
|
|
286
|
+
await writeFile2(outputPath, output, "utf8");
|
|
287
|
+
await context.emitArtifact({
|
|
288
|
+
type: "inference.output",
|
|
289
|
+
name: "inference-output.txt",
|
|
290
|
+
path: outputPath,
|
|
291
|
+
metadata: { model: payload.model, adapter: "ollama" }
|
|
292
|
+
});
|
|
293
|
+
if (payload.inputFile) {
|
|
294
|
+
await context.log("system", `inputFile ${payload.inputFile} noted (batch file expansion not implemented)`, "inference");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// ../adapter-harness/src/index.ts
|
|
300
|
+
import { join as join5 } from "path";
|
|
301
|
+
function resolveBinary(binary) {
|
|
302
|
+
return typeof binary === "function" ? binary() : binary;
|
|
303
|
+
}
|
|
304
|
+
function createHarnessAdapter(options) {
|
|
305
|
+
return {
|
|
306
|
+
name: options.name,
|
|
307
|
+
kind: options.kind,
|
|
308
|
+
async run(context, payload) {
|
|
309
|
+
const cwd = repoPath(context.workspacePath);
|
|
310
|
+
const branchName = payload.taskBranch ?? defaultTaskBranchName(context.runId);
|
|
311
|
+
await createTaskBranch(context, branchName);
|
|
312
|
+
const extra = payload.extraArgs ?? options.defaultArgs ?? [];
|
|
313
|
+
const promptArgs = options.buildArgs ? options.buildArgs(payload) : [...extra, payload.prompt];
|
|
314
|
+
const binary = resolveBinary(options.binary);
|
|
315
|
+
const result = await context.exec(binary, promptArgs, { cwd });
|
|
316
|
+
if (result.exitCode !== 0) {
|
|
317
|
+
throw new Error(`${binary} failed with exit code ${result.exitCode}`);
|
|
318
|
+
}
|
|
319
|
+
await captureGitDiff(context, options.name);
|
|
320
|
+
if (payload.testCommand) {
|
|
321
|
+
await context.log("system", `running test command: ${payload.testCommand}`, "run-tests");
|
|
322
|
+
const testResult = await context.exec("/bin/sh", ["-lc", payload.testCommand], { cwd });
|
|
323
|
+
const testArtifactPath = join5(await context.ensureWorkspace("artifacts"), "test-output.txt");
|
|
324
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
325
|
+
await writeFile4(testArtifactPath, `${testResult.stdout}
|
|
326
|
+
${testResult.stderr}`, "utf8");
|
|
327
|
+
await context.emitArtifact({
|
|
328
|
+
type: "test.output",
|
|
329
|
+
name: "test-output.txt",
|
|
330
|
+
path: testArtifactPath,
|
|
331
|
+
metadata: { exitCode: testResult.exitCode }
|
|
332
|
+
});
|
|
333
|
+
if (testResult.exitCode !== 0) {
|
|
334
|
+
throw new Error(`test command failed with exit code ${testResult.exitCode}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ../adapter-codex/src/index.ts
|
|
342
|
+
var codexAdapter = createHarnessAdapter({
|
|
343
|
+
name: "codex",
|
|
344
|
+
kind: "agent.run",
|
|
345
|
+
binary: () => process.env.WORKPLANE_CODEX_BIN ?? "codex",
|
|
346
|
+
defaultArgs: ["--approval-mode", "full-auto"],
|
|
347
|
+
buildArgs: (payload) => {
|
|
348
|
+
const args = [...payload.extraArgs ?? ["--approval-mode", "full-auto"]];
|
|
349
|
+
if (payload.model) {
|
|
350
|
+
args.push("--model", payload.model);
|
|
351
|
+
}
|
|
352
|
+
args.push(payload.prompt);
|
|
353
|
+
return args;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ../adapter-claude-code/src/index.ts
|
|
358
|
+
var claudeCodeAdapter = createHarnessAdapter({
|
|
359
|
+
name: "claude-code",
|
|
360
|
+
kind: "agent.run",
|
|
361
|
+
binary: () => process.env.WORKPLANE_CLAUDE_CODE_BIN ?? "claude",
|
|
362
|
+
buildArgs: (payload) => {
|
|
363
|
+
const args = ["-p", payload.prompt, "--output-format", "text"];
|
|
364
|
+
if (payload.model) {
|
|
365
|
+
args.push("--model", payload.model);
|
|
366
|
+
}
|
|
367
|
+
const extra = payload.extraArgs ?? [];
|
|
368
|
+
return [...args, ...extra];
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ../node/src/index.ts
|
|
373
|
+
var REPO_ADAPTERS = /* @__PURE__ */ new Set(["aider", "codex", "claude-code"]);
|
|
374
|
+
var adapters = /* @__PURE__ */ new Map([
|
|
375
|
+
["shell", shellAdapter],
|
|
376
|
+
["aider", aiderAdapter],
|
|
377
|
+
["ollama", ollamaAdapter],
|
|
378
|
+
["codex", codexAdapter],
|
|
379
|
+
["claude-code", claudeCodeAdapter]
|
|
380
|
+
]);
|
|
381
|
+
function nowStamp() {
|
|
382
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
383
|
+
}
|
|
384
|
+
function workspaceRoot() {
|
|
385
|
+
return resolve(process.env.WORKPLANE_WORKSPACE_ROOT ?? ".workplane/workspaces");
|
|
386
|
+
}
|
|
387
|
+
function shouldMirrorNodeLogs() {
|
|
388
|
+
const value = process.env.WORKPLANE_NODE_LOG_MIRROR;
|
|
389
|
+
if (!value) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
|
|
393
|
+
}
|
|
394
|
+
function stringPayloadValue(payload, key) {
|
|
395
|
+
const value = payload[key];
|
|
396
|
+
if (typeof value !== "string") {
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
const trimmed = value.trim();
|
|
400
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
401
|
+
}
|
|
402
|
+
function nodeHttpOptions(config) {
|
|
403
|
+
return { nodeToken: config.nodeToken };
|
|
404
|
+
}
|
|
405
|
+
async function updateStatus(config, runId, status, error) {
|
|
406
|
+
await workplaneFetch(config.serverUrl, `/runs/${runId}/status`, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
body: { status, error },
|
|
409
|
+
...nodeHttpOptions(config)
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function appendLogs(config, runId, logs) {
|
|
413
|
+
await workplaneFetch(config.serverUrl, `/runs/${runId}/logs`, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
body: { logs },
|
|
416
|
+
...nodeHttpOptions(config)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async function emitArtifact(config, runId, artifact) {
|
|
420
|
+
await workplaneFetch(config.serverUrl, `/runs/${runId}/artifacts`, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
body: artifact,
|
|
423
|
+
...nodeHttpOptions(config)
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async function isCancelled(config, runId) {
|
|
427
|
+
const run = await workplaneFetch(config.serverUrl, `/runs/${runId}`, nodeHttpOptions(config));
|
|
428
|
+
if (run.status === "cancelled") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
const task = await workplaneFetch(
|
|
432
|
+
config.serverUrl,
|
|
433
|
+
`/tasks/${run.taskId}`,
|
|
434
|
+
nodeHttpOptions(config)
|
|
435
|
+
);
|
|
436
|
+
return task.status === "cancelled";
|
|
437
|
+
}
|
|
438
|
+
async function checkoutRepoIfPresent(context, payload) {
|
|
439
|
+
const repo = stringPayloadValue(payload, "repo");
|
|
440
|
+
if (!repo) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
await context.log("system", `cloning repository ${repo}`, "checkout-repo");
|
|
444
|
+
const cloneResult = await context.exec("git", ["clone", repo, "repo"], {
|
|
445
|
+
cwd: context.workspacePath
|
|
446
|
+
});
|
|
447
|
+
if (cloneResult.exitCode !== 0) {
|
|
448
|
+
throw new Error(`git clone failed with exit code ${cloneResult.exitCode}`);
|
|
449
|
+
}
|
|
450
|
+
const branch = stringPayloadValue(payload, "branch");
|
|
451
|
+
if (branch) {
|
|
452
|
+
await context.log("system", `checking out branch ${branch}`, "checkout-repo");
|
|
453
|
+
const checkoutResult = await context.exec("git", ["checkout", branch], {
|
|
454
|
+
cwd: repoPath(context.workspacePath)
|
|
455
|
+
});
|
|
456
|
+
if (checkoutResult.exitCode !== 0) {
|
|
457
|
+
throw new Error(`git checkout failed with exit code ${checkoutResult.exitCode}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
await context.emitArtifact({
|
|
461
|
+
type: "workspace.repo",
|
|
462
|
+
name: "repo-checkout",
|
|
463
|
+
path: repoPath(context.workspacePath),
|
|
464
|
+
metadata: { repo, branch: branch ?? null }
|
|
465
|
+
});
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
async function executeAssignment(config, assignment) {
|
|
469
|
+
const runId = assignment.run.id;
|
|
470
|
+
const task = assignment.task;
|
|
471
|
+
const adapter = adapters.get(task.adapter);
|
|
472
|
+
if (!adapter) {
|
|
473
|
+
await updateStatus(config, runId, "failed", `adapter not registered: ${task.adapter}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const runWorkspace = join6(workspaceRoot(), `${runId}_${nowStamp()}`);
|
|
477
|
+
const mirrorLogs = shouldMirrorNodeLogs();
|
|
478
|
+
await mkdir2(runWorkspace, { recursive: true });
|
|
479
|
+
await ensureWorkspacePath(runWorkspace, "logs");
|
|
480
|
+
await ensureWorkspacePath(runWorkspace, "artifacts");
|
|
481
|
+
await ensureWorkspacePath(runWorkspace, "tmp");
|
|
482
|
+
await writeFile3(
|
|
483
|
+
join6(runWorkspace, "metadata.json"),
|
|
484
|
+
JSON.stringify({ runId, taskId: task.id, adapter: task.adapter, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
485
|
+
"utf8"
|
|
486
|
+
);
|
|
487
|
+
await updateStatus(config, runId, "running");
|
|
488
|
+
const serverEnvAllowlist = config.envAllowlist.length > 0 ? config.envAllowlist : loadServerConfig().envAllowlist.length > 0 ? loadServerConfig().envAllowlist : void 0;
|
|
489
|
+
const { exec, kill } = createCancellableExec({
|
|
490
|
+
runId,
|
|
491
|
+
taskId: task.id,
|
|
492
|
+
workspacePath: runWorkspace,
|
|
493
|
+
log: async () => {
|
|
494
|
+
},
|
|
495
|
+
exec: async () => ({ exitCode: 1, stdout: "", stderr: "not initialized" }),
|
|
496
|
+
ensureWorkspace: async (...segments) => ensureWorkspacePath(runWorkspace, ...segments),
|
|
497
|
+
emitArtifact: async () => {
|
|
498
|
+
},
|
|
499
|
+
envAllowlist: serverEnvAllowlist
|
|
500
|
+
});
|
|
501
|
+
const context = {
|
|
502
|
+
runId,
|
|
503
|
+
taskId: task.id,
|
|
504
|
+
workspacePath: runWorkspace,
|
|
505
|
+
envAllowlist: serverEnvAllowlist,
|
|
506
|
+
log: async (stream, message, stepName) => {
|
|
507
|
+
if (mirrorLogs) {
|
|
508
|
+
const prefix = `[run:${runId}] [${stream}]${stepName ? ` [${stepName}]` : ""} `;
|
|
509
|
+
if (stream === "stderr") {
|
|
510
|
+
process.stderr.write(`${prefix}${message}`);
|
|
511
|
+
} else {
|
|
512
|
+
process.stdout.write(`${prefix}${message}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
await appendLogs(config, runId, [{ stream, message, stepName }]);
|
|
516
|
+
},
|
|
517
|
+
exec,
|
|
518
|
+
ensureWorkspace: async (...segments) => ensureWorkspacePath(runWorkspace, ...segments),
|
|
519
|
+
emitArtifact: async (input) => {
|
|
520
|
+
await emitArtifact(config, runId, input);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const cancelInterval = setInterval(() => {
|
|
524
|
+
void isCancelled(config, runId).then((cancelled) => {
|
|
525
|
+
if (cancelled) {
|
|
526
|
+
kill();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}, 2e3);
|
|
530
|
+
try {
|
|
531
|
+
await context.log("system", `starting adapter ${adapter.name}`, "run-adapter");
|
|
532
|
+
const hasRepo = await checkoutRepoIfPresent(context, task.payload);
|
|
533
|
+
if (REPO_ADAPTERS.has(adapter.name) && !hasRepo) {
|
|
534
|
+
throw new Error(`${adapter.name} adapter requires payload.repo`);
|
|
535
|
+
}
|
|
536
|
+
const adapterPayload = { ...task.payload };
|
|
537
|
+
if (adapter.name === "shell" && hasRepo && typeof adapterPayload.cwd !== "string") {
|
|
538
|
+
adapterPayload.cwd = "repo";
|
|
539
|
+
}
|
|
540
|
+
if (REPO_ADAPTERS.has(adapter.name) && adapter.name !== "aider" && !stringPayloadValue(adapterPayload, "taskBranch")) {
|
|
541
|
+
adapterPayload.taskBranch = defaultTaskBranchName(runId);
|
|
542
|
+
}
|
|
543
|
+
await adapter.run(context, adapterPayload);
|
|
544
|
+
if (await isCancelled(config, runId)) {
|
|
545
|
+
await context.log("system", "run cancelled", "run-adapter");
|
|
546
|
+
await updateStatus(config, runId, "cancelled", "task cancelled");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
await context.log("system", "adapter execution finished", "run-adapter");
|
|
550
|
+
await updateStatus(config, runId, "succeeded");
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (await isCancelled(config, runId)) {
|
|
553
|
+
await updateStatus(config, runId, "cancelled", "task cancelled");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
557
|
+
await context.log("stderr", message, "run-adapter");
|
|
558
|
+
await updateStatus(config, runId, "failed", message);
|
|
559
|
+
} finally {
|
|
560
|
+
clearInterval(cancelInterval);
|
|
561
|
+
kill();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function main() {
|
|
565
|
+
const config = loadNodeConfig();
|
|
566
|
+
const register = await workplaneFetch(config.serverUrl, "/nodes/register", {
|
|
567
|
+
method: "POST",
|
|
568
|
+
body: {
|
|
569
|
+
name: config.nodeName,
|
|
570
|
+
capabilities: config.nodeCapabilities,
|
|
571
|
+
nodeId: config.nodeId
|
|
572
|
+
},
|
|
573
|
+
...nodeHttpOptions(config)
|
|
574
|
+
});
|
|
575
|
+
const nodeId = register.id;
|
|
576
|
+
process.stdout.write(`node registered: ${nodeId}
|
|
577
|
+
`);
|
|
578
|
+
while (true) {
|
|
579
|
+
try {
|
|
580
|
+
const poll = await workplaneFetch(
|
|
581
|
+
config.serverUrl,
|
|
582
|
+
`/nodes/${nodeId}/poll`,
|
|
583
|
+
{
|
|
584
|
+
method: "POST",
|
|
585
|
+
body: {
|
|
586
|
+
capabilities: config.nodeCapabilities
|
|
587
|
+
},
|
|
588
|
+
...nodeHttpOptions(config)
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
if (poll.assignment) {
|
|
592
|
+
await executeAssignment(config, poll.assignment);
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
process.stderr.write(`poll error: ${String(error)}
|
|
596
|
+
`);
|
|
597
|
+
}
|
|
598
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, config.pollIntervalMs));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
main().catch((error) => {
|
|
602
|
+
process.stderr.write(`${String(error)}
|
|
603
|
+
`);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
});
|
|
606
|
+
//# sourceMappingURL=node.js.map
|