@ponderbot/cli 0.1.0-alpha.21

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/main.js ADDED
@@ -0,0 +1,2533 @@
1
+ #!/usr/bin/env bun
2
+ import { builtinModules } from "node:module";
3
+ import { constants } from "node:fs";
4
+ import { access, chmod, copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
5
+ import { dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { RUN_TYPES, createClient, isPonderbotError, isRunType, ponderbotError, resolveEnvironment } from "@ponderbot/sdk";
8
+ import { agentInstructionBudgets, buildManifest, checkSource, emitManifest, hasWorkerEntryHandlers, isManifestBuildError, loadProjectDefinition, loadSource, manifestBuildError, sourceWarnings } from "@ponderbot/sdk/authoring";
9
+ import { Command } from "commander";
10
+ import { cancelledResult, isLifecycleEvent, parseActionExecutionSnapshot, parseApplicationOrigin, parseSerializedExecutionBoundary, parseWorkerExecutionResult } from "@ponderbot/sdk/worker";
11
+ import { watch } from "chokidar";
12
+ import { createHash, randomBytes } from "node:crypto";
13
+ import { spawn } from "node:child_process";
14
+ import { tmpdir } from "node:os";
15
+ import { build } from "esbuild";
16
+ import { createServer } from "node:http";
17
+ import { createInterface } from "node:readline/promises";
18
+ import { stderr, stdin } from "node:process";
19
+ //#region src/config.ts
20
+ const projectDir = () => resolve(process.cwd(), process.env.PONDERBOT_PROJECT ?? "./ponderbot");
21
+ const exitConfig = (message) => {
22
+ console.error(`ponder: ${message}`);
23
+ process.exit(2);
24
+ };
25
+ const resolveProject = async () => {
26
+ const project = projectDir();
27
+ return {
28
+ project,
29
+ projectDefinition: await loadProjectDefinition(resolve(project, "config"))
30
+ };
31
+ };
32
+ const resolveConfig = async (envName) => requireExecutionSpace(await resolveEnvironmentConfig(envName));
33
+ const resolveProjectReleaseConfig = async (envName) => resolveEnvironmentConfig(envName, { resolveApplicationOrigin: true });
34
+ const resolveDevConfig = async (envName) => requireExecutionSpace(await resolveEnvironmentConfig(envName ?? "development", {
35
+ requireDevAllowed: true,
36
+ resolveApplicationOrigin: true,
37
+ useProjectEndpoint: true
38
+ }));
39
+ const resolveEnvironmentConfig = async (envName, options = {}) => {
40
+ let resolved;
41
+ const project = projectDir();
42
+ let projectDefinition;
43
+ try {
44
+ projectDefinition = await loadProjectDefinition(resolve(project, "config"));
45
+ resolved = resolveEnvironment(projectDefinition, envName);
46
+ } catch (error) {
47
+ return exitConfig(error instanceof Error ? error.message : String(error));
48
+ }
49
+ if (options.requireDevAllowed && !(resolved.projectEnvironment.allowDev ?? resolved.env === "development")) return exitConfig(`environment "${resolved.env}" does not allow ponder manifest dev`);
50
+ const endpoint = options.useProjectEndpoint ? {
51
+ baseUrl: resolved.projectEnvironment.url,
52
+ spaceId: resolved.projectEnvironment.spaceId
53
+ } : resolved.client;
54
+ let applicationOrigin;
55
+ if (options.resolveApplicationOrigin) try {
56
+ applicationOrigin = resolveProjectApplicationOrigin(projectDefinition);
57
+ } catch (error) {
58
+ return exitConfig(error instanceof Error ? error.message : String(error));
59
+ }
60
+ return {
61
+ project,
62
+ projectDefinition,
63
+ environment: resolved.env,
64
+ environmentRole: resolved.projectEnvironment.role ?? "execution",
65
+ allowDev: resolved.projectEnvironment.allowDev ?? resolved.env === "development",
66
+ sourceName: `${resolved.env}:${projectDefinition.name}`,
67
+ url: endpoint.baseUrl,
68
+ spaceId: endpoint.spaceId,
69
+ token: resolved.client.token,
70
+ applicationOrigin
71
+ };
72
+ };
73
+ const requireExecutionSpace = (config) => {
74
+ if (config.spaceId === void 0 || config.spaceId.length === 0) return exitConfig(`set spaceId for environment "${config.environment}" in ponderbot/config.ts or PONDERBOT_SPACE_ID for this command`);
75
+ return {
76
+ ...config,
77
+ spaceId: config.spaceId
78
+ };
79
+ };
80
+ const resolveProjectApplicationOrigin = (projectDefinition) => {
81
+ const origin = projectDefinition.applicationOrigin;
82
+ if (origin === void 0) return;
83
+ const value = process.env[origin.env];
84
+ if (value === void 0 || value.length === 0) throw new Error(`application origin environment variable "${origin.env}" is not set`);
85
+ return parseApplicationOrigin(value);
86
+ };
87
+ const clientFor = (config) => createClient({
88
+ baseUrl: config.url,
89
+ token: config.token,
90
+ spaceId: config.spaceId
91
+ });
92
+ //#endregion
93
+ //#region src/deployment-environment.ts
94
+ const INERT_SOURCE_SETTINGS = {
95
+ allow_schedules: false,
96
+ allow_webhooks: false,
97
+ allow_manual_triggers: false
98
+ };
99
+ const reconcileDeploymentEnvironment = async (client, config) => {
100
+ if (config.environmentRole === "release-source") {
101
+ const { settings } = await client.spaces.settings.update(INERT_SOURCE_SETTINGS);
102
+ if (settings.allow_schedules !== false || settings.allow_webhooks !== false || settings.allow_manual_triggers !== false) throw new Error("release-source Space settings did not become execution-inert");
103
+ }
104
+ };
105
+ //#endregion
106
+ //#region src/dev-manifest.ts
107
+ const staticProjectManifest = (manifest) => ({
108
+ ...manifest,
109
+ version: staticProjectVersion(manifest)
110
+ });
111
+ const staticProjectVersion = (manifest) => {
112
+ const sourceHash = createHash("sha256").update(JSON.stringify(manifest)).digest("hex").slice(0, 12);
113
+ return `${manifest.version ?? "dev"}-dev.${sourceHash}`;
114
+ };
115
+ //#endregion
116
+ //#region src/dev-install.ts
117
+ const INSTALL_CONCURRENCY = 8;
118
+ /**
119
+ * Installs the current Project Release for this environment into every Space
120
+ * associated with the manifest, except the Space the dev loop deploys into.
121
+ *
122
+ * Every `ponder manifest dev` run calls this, whatever the environment role.
123
+ * The `release-source` role only adds inert Space settings and skips the local
124
+ * event tail; it does not own the install. A project with no associated Space
125
+ * is normal, so an empty target list logs nothing.
126
+ */
127
+ const installDevelopmentProjectRelease = async (client, config, manifestName) => {
128
+ const { space_ids: spaceIds } = await client.dev.manifests.targets(manifestName, config.environment);
129
+ const targets = spaceIds.filter((spaceId) => spaceId !== config.spaceId);
130
+ if (targets.length === 0) return targets;
131
+ await eachConcurrent(targets, INSTALL_CONCURRENCY, async (spaceId) => {
132
+ await client.projects.releases.install(manifestName, config.environment, spaceId);
133
+ });
134
+ console.log(`installed project release in ${targets.length} associated Spaces: ${targets.join(", ")}`);
135
+ return targets;
136
+ };
137
+ const eachConcurrent = async (values, concurrency, run) => {
138
+ let index = 0;
139
+ let failure;
140
+ const worker = async () => {
141
+ while (index < values.length) {
142
+ const value = values[index];
143
+ index += 1;
144
+ if (value !== void 0) try {
145
+ await run(value);
146
+ } catch (error) {
147
+ failure ??= error;
148
+ }
149
+ }
150
+ };
151
+ await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
152
+ if (failure !== void 0) throw failure;
153
+ };
154
+ //#endregion
155
+ //#region src/node-worker-runner.ts
156
+ const MAXIMUM_WORKER_RESPONSE_BYTES = 1e6;
157
+ const MAXIMUM_WORKER_STDERR_BYTES = 65536;
158
+ const MAXIMUM_TIMER_DELAY = 2147483647;
159
+ var NodeWorkerFailure = class extends Error {
160
+ reason;
161
+ constructor(reason, message) {
162
+ super(message);
163
+ this.name = "NodeWorkerFailure";
164
+ this.reason = reason;
165
+ }
166
+ };
167
+ const createWorkerTermination = (input) => {
168
+ let aborted = false;
169
+ let forceKillTimer;
170
+ const abort = () => {
171
+ if (aborted) return;
172
+ aborted = true;
173
+ input.kill("SIGTERM");
174
+ forceKillTimer = input.schedule(() => {
175
+ if (input.isRunning()) input.kill("SIGKILL");
176
+ }, input.completionGraceMilliseconds);
177
+ };
178
+ const settle = () => {
179
+ if (forceKillTimer === void 0) return;
180
+ input.cancel(forceKillTimer);
181
+ forceKillTimer = void 0;
182
+ };
183
+ return {
184
+ abort,
185
+ settle,
186
+ wasAborted: () => aborted
187
+ };
188
+ };
189
+ const executePonderbotWorkerInNode = (async (options) => {
190
+ const { completionGraceMilliseconds, nodeBinary, signal, environment, stderr: stderrTarget, ...requestOptions } = options;
191
+ if (!Number.isSafeInteger(completionGraceMilliseconds) || completionGraceMilliseconds < 0 || completionGraceMilliseconds > MAXIMUM_TIMER_DELAY) throw new Error("invalid_worker_completion_grace");
192
+ const execution = parseSerializedExecutionBoundary(requestOptions.execution);
193
+ if (signal?.aborted) return cancellationResult(requestOptions.invocation, "caller_cancelled");
194
+ if (Date.parse(execution.deadlineAt) <= Date.now()) return cancellationResult(requestOptions.invocation, "deadline_exceeded");
195
+ const request = {
196
+ ...requestOptions,
197
+ execution
198
+ };
199
+ const directory = await mkdtemp(join(tmpdir(), "ponderbot-node-worker-"));
200
+ const requestPath = join(directory, "request.json");
201
+ const responsePath = join(directory, "response.json");
202
+ try {
203
+ await writeFile(requestPath, `${JSON.stringify(request)}\n`, { mode: 384 });
204
+ const childResult = await runNodeWorker({
205
+ nodeBinary: nodeBinary ?? process.env.PONDERBOT_NODE_BINARY ?? "node",
206
+ requestPath,
207
+ responsePath,
208
+ execution: request.execution,
209
+ completionGraceMilliseconds,
210
+ signal,
211
+ environment
212
+ });
213
+ if (childResult.stderr.length > 0) (stderrTarget ?? process.stderr).write(childResult.stderr);
214
+ if (childResult.cancellationReason !== void 0) return cancellationResult(request.invocation, childResult.cancellationReason);
215
+ try {
216
+ if ((await stat(responsePath)).size > MAXIMUM_WORKER_RESPONSE_BYTES) throw new NodeWorkerFailure("runner_contract_invalid_result", "node_worker_response_too_large");
217
+ return parseWorkerExecutionResult(JSON.parse(await readFile(responsePath, "utf8")), { kind: request.invocation.kind });
218
+ } catch (error) {
219
+ if (error instanceof NodeWorkerFailure) throw error;
220
+ throw new NodeWorkerFailure(isRecord$2(error) && error.code === "ENOENT" ? "runner_contract_missing_result" : "runner_contract_invalid_result", errorMessage$1(error));
221
+ }
222
+ } finally {
223
+ await rm(directory, {
224
+ recursive: true,
225
+ force: true
226
+ });
227
+ }
228
+ });
229
+ const actionCheckpoint = (invocation) => invocation.kind === "action" ? { onceRecords: invocation.execution.onceRecords } : void 0;
230
+ const cancellationResult = (invocation, reason) => invocation.kind === "action" ? cancelledResult("action", reason, actionCheckpoint(invocation)) : cancelledResult("hook", reason);
231
+ const runNodeWorker = async (input) => {
232
+ const workerExecutable = await resolveWorkerExecutable();
233
+ return new Promise((resolveRun, rejectRun) => {
234
+ const child = spawn(input.nodeBinary, [
235
+ workerExecutable,
236
+ "--request",
237
+ input.requestPath,
238
+ "--response",
239
+ input.responsePath
240
+ ], {
241
+ stdio: [
242
+ "ignore",
243
+ "inherit",
244
+ "pipe"
245
+ ],
246
+ ...input.environment === void 0 ? {} : { env: {
247
+ ...process.env,
248
+ ...input.environment
249
+ } }
250
+ });
251
+ let stderr = "";
252
+ const deadlineAt = Date.parse(input.execution.deadlineAt);
253
+ const termination = createWorkerTermination({
254
+ completionGraceMilliseconds: input.completionGraceMilliseconds,
255
+ isRunning: () => child.exitCode === null && child.signalCode === null,
256
+ kill: (signal) => {
257
+ child.kill(signal);
258
+ },
259
+ schedule: setTimeout,
260
+ cancel: clearTimeout
261
+ });
262
+ const cancellation = createWorkerCancellation(termination);
263
+ let deadlineTimer;
264
+ const abortFromDeadline = () => {
265
+ cancellation.abort("deadline_exceeded");
266
+ };
267
+ const abortFromCaller = () => {
268
+ cancellation.abort("caller_cancelled");
269
+ };
270
+ const scheduleDeadline = () => {
271
+ const remaining = Math.max(0, deadlineAt - Date.now());
272
+ deadlineTimer = setTimeout(remaining > MAXIMUM_TIMER_DELAY ? scheduleDeadline : abortFromDeadline, Math.min(remaining, MAXIMUM_TIMER_DELAY));
273
+ };
274
+ scheduleDeadline();
275
+ const settle = () => {
276
+ clearTimeout(deadlineTimer);
277
+ termination.settle();
278
+ input.signal?.removeEventListener("abort", abortFromCaller);
279
+ };
280
+ child.stderr.setEncoding("utf8");
281
+ child.stderr.on("data", (chunk) => {
282
+ if (stderr.length < MAXIMUM_WORKER_STDERR_BYTES) stderr += chunk.slice(0, MAXIMUM_WORKER_STDERR_BYTES - stderr.length);
283
+ });
284
+ if (input.signal?.aborted) abortFromCaller();
285
+ else input.signal?.addEventListener("abort", abortFromCaller, { once: true });
286
+ child.on("error", (error) => {
287
+ settle();
288
+ rejectRun(error);
289
+ });
290
+ child.on("exit", (code) => {
291
+ settle();
292
+ const cancellationReason = cancellation.reason();
293
+ if (cancellationReason !== void 0) {
294
+ resolveRun({
295
+ stderr,
296
+ cancellationReason
297
+ });
298
+ return;
299
+ }
300
+ if (code === 0) {
301
+ resolveRun({ stderr });
302
+ return;
303
+ }
304
+ rejectRun(new NodeWorkerFailure(stderr.trim() === "invalid_node_worker_request" ? "runner_dispatch_failed" : "runner_failed", stderr.trim() || `node_worker_exited: ${code ?? "signal"}`));
305
+ });
306
+ });
307
+ };
308
+ const createWorkerCancellation = (termination) => {
309
+ let cancellationReason;
310
+ return {
311
+ abort: (reason) => {
312
+ if (cancellationReason !== void 0) return;
313
+ cancellationReason = reason;
314
+ termination.abort();
315
+ },
316
+ reason: () => cancellationReason
317
+ };
318
+ };
319
+ const resolveWorkerExecutable = async () => {
320
+ const sdkDirectory = dirname(dirname(fileURLToPath(import.meta.resolve("@ponderbot/sdk/worker"))));
321
+ const packageJson = JSON.parse(await readFile(join(sdkDirectory, "package.json"), "utf8"));
322
+ if (!isRecord$2(packageJson) || !isRecord$2(packageJson.bin) || typeof packageJson.bin["ponderbot-worker"] !== "string") throw new Error("ponderbot_worker_executable_missing");
323
+ return resolve(sdkDirectory, packageJson.bin["ponderbot-worker"]);
324
+ };
325
+ const isRecord$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
326
+ const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
327
+ //#endregion
328
+ //#region src/output.ts
329
+ const print = (data, options = {}) => {
330
+ if (options.json) {
331
+ console.log(JSON.stringify(jsonData(data)));
332
+ return;
333
+ }
334
+ for (const line of lines(data)) console.log(line);
335
+ };
336
+ const fail = (error, code = 1) => {
337
+ console.error(formatError(error));
338
+ process.exit(code);
339
+ };
340
+ const formatError = (error) => {
341
+ if (isManifestBuildError(error)) return error.message;
342
+ if (isPonderbotError(error)) {
343
+ const conflictVersion = manifestReleaseVersionConflict(error.detail);
344
+ if (conflictVersion !== void 0) return `version ${conflictVersion} is already released with different content — bump version in ponderbot/config.ts or rerun with --bump patch`;
345
+ return typeof error.detail === "undefined" ? error.code : JSON.stringify(error.detail);
346
+ }
347
+ if (error instanceof Error) return error.message;
348
+ return String(error);
349
+ };
350
+ const lines = (data) => {
351
+ if (isStartedDevStack(data)) return [
352
+ "Ponderbot dev stack ready.",
353
+ ` PONDERBOT_URL=${data.dev_environment.url}`,
354
+ ` PONDERBOT_SPACE_ID=${data.dev_environment.space_id}`,
355
+ ` PONDERBOT_TOKEN=${data.dev_environment.token}`,
356
+ "Put these in your app's .env - createClient() reads them."
357
+ ];
358
+ if (isRecord$1(data) && isRecord$1(data.release) && typeof data.release.id === "string") return [
359
+ ...versionBumpLines(data.version_bump),
360
+ `release ${data.release.id}`,
361
+ ...recordLines(data.registered)
362
+ ];
363
+ if (isRecord$1(data) && data.ok === true && isRecord$1(data.counts)) return ["manifest valid", ...recordLines(data.counts)];
364
+ if (isRecord$1(data) && Array.isArray(data.spaces)) return ["id name status", ...data.spaces.map((space) => spaceLine(space))];
365
+ if (isRecord$1(data) && isRecord$1(data.space)) {
366
+ if (isRecord$1(data.budget) && isRecord$1(data.usage)) return spaceShowLines(data.space, data.budget, data.usage);
367
+ return [spaceLine(data.space)];
368
+ }
369
+ if (isRecord$1(data) && isRecord$1(data.budget)) return recordLines(data.budget);
370
+ if (isRecord$1(data) && Array.isArray(data.models)) return ["ref provider model", ...data.models.map((model) => modelLine(model))];
371
+ if (isRecord$1(data) && isRecord$1(data.model)) return [modelLine(data.model)];
372
+ if (isRecord$1(data) && isRecord$1(data.agent_model_refs)) return ["agent model", ...Object.entries(data.agent_model_refs).map(([agentRef, modelRef]) => `${agentRef} ${String(modelRef)}`)];
373
+ if (isRecord$1(data) && isRecord$1(data.run)) return lines(data.run);
374
+ if (isRecord$1(data) && data.type === "provider_auth" && typeof data.provider === "string" && typeof data.method === "string" && typeof data.space_id === "string") {
375
+ if (typeof data.binding_id === "string") {
376
+ const target = data.all_spaces === true ? "all Spaces" : `Spaces ${Array.isArray(data.space_ids) ? data.space_ids.join(", ") : data.space_id}`;
377
+ return [
378
+ `provider ${data.provider} ${data.method} shared credentials saved for ${target}`,
379
+ `binding ${data.binding_id}`,
380
+ `smoke test: ${String(data.smoke_test ?? "ponder spaces show")}`
381
+ ];
382
+ }
383
+ return [`provider ${data.provider} ${data.method} credentials saved for space ${data.space_id}`, `smoke test: ${String(data.smoke_test ?? "ponder spaces show")}`];
384
+ }
385
+ if (Array.isArray(data)) return data.flatMap((item) => lines(item));
386
+ if (isRecord$1(data)) {
387
+ const arrayEntry = Object.entries(data).find((entry) => Array.isArray(entry[1]));
388
+ if (arrayEntry) return arrayEntry[1].flatMap((item) => lines(item));
389
+ return [recordSummary(data)];
390
+ }
391
+ return [String(data)];
392
+ };
393
+ const versionBumpLines = (data) => {
394
+ if (!isRecord$1(data) || typeof data.previousVersion !== "string" || typeof data.nextVersion !== "string") return [];
395
+ return [`version ${data.previousVersion} → ${data.nextVersion}`];
396
+ };
397
+ const jsonData = (data) => {
398
+ if (!isStartedDevStack(data)) return data;
399
+ return {
400
+ ...data,
401
+ dev_environment: {
402
+ url: data.dev_environment.url,
403
+ space_id: data.dev_environment.space_id,
404
+ token_env: "PONDERBOT_TOKEN"
405
+ }
406
+ };
407
+ };
408
+ function isStartedDevStack(data) {
409
+ return isRecord$1(data) && data.type === "stack" && data.status === "started" && isRecord$1(data.dev_environment) && typeof data.dev_environment.url === "string" && typeof data.dev_environment.space_id === "string" && typeof data.dev_environment.token === "string";
410
+ }
411
+ function isRecord$1(value) {
412
+ return value !== null && typeof value === "object" && !Array.isArray(value);
413
+ }
414
+ const manifestReleaseVersionConflict = (detail) => {
415
+ const details = isRecord$1(detail) ? detail.details : detail;
416
+ if (Array.isArray(details) && (details[0] === "manifest_release_version_conflict" || details[0] === "manifest_project_release_version_conflict") && typeof details[3] === "string") return details[3];
417
+ };
418
+ const recordLines = (data) => {
419
+ if (!isRecord$1(data)) return [];
420
+ return Object.entries(data).map(([key, value]) => `${key} ${String(value)}`);
421
+ };
422
+ function spaceShowLines(space, budget, usage) {
423
+ return [
424
+ spaceLine(space),
425
+ "budget",
426
+ ...recordLines(budget),
427
+ ...spaceUsageLines(usage)
428
+ ];
429
+ }
430
+ function spaceUsageLines(usage) {
431
+ const period = isRecord$1(usage.period) ? usage.period : {};
432
+ const resources = isRecord$1(usage.usage) ? usage.usage : {};
433
+ return [`usage ${String(period.type ?? "unknown")} ${String(period.starts_at ?? "")} ${String(period.ends_at ?? "")}`.trim(), ...Object.entries(resources).map(([key, value]) => {
434
+ if (!isRecord$1(value)) return `${key} ${JSON.stringify(value)}`;
435
+ return [
436
+ key,
437
+ `used=${String(value.used ?? "")}`,
438
+ `limit=${limitValue(value.limit)}`,
439
+ `remaining=${limitValue(value.remaining)}`
440
+ ].join(" ");
441
+ })];
442
+ }
443
+ const limitValue = (value) => {
444
+ return value === null ? "unlimited" : String(value);
445
+ };
446
+ const spaceLine = (space) => {
447
+ if (!isRecord$1(space)) return String(space);
448
+ return [
449
+ field(space, "id"),
450
+ field(space, "name"),
451
+ field(space, "status")
452
+ ].filter((value) => value !== void 0).join(" ");
453
+ };
454
+ const modelLine = (model) => {
455
+ if (!isRecord$1(model)) return String(model);
456
+ return [
457
+ field(model, "ref"),
458
+ field(model, "provider"),
459
+ field(model, "model")
460
+ ].filter((value) => value !== void 0).join(" ");
461
+ };
462
+ function recordSummary(record) {
463
+ const fields = [
464
+ field(record, "type") ?? field(record, "run_type") ?? field(record, "target_type"),
465
+ field(record, "id") ?? field(record, "run_id") ?? field(record, "record_id") ?? field(record, "receipt_id"),
466
+ field(record, "ref") ?? field(record, "target_ref") ?? field(record, "action"),
467
+ field(record, "status")
468
+ ].filter((value) => value !== void 0);
469
+ return fields.length === 0 ? JSON.stringify(record) : fields.join(" ");
470
+ }
471
+ function field(record, name) {
472
+ const value = record[name];
473
+ return typeof value === "string" ? value : void 0;
474
+ }
475
+ //#endregion
476
+ //#region src/dev-runner.ts
477
+ const DEFAULT_DEV_ACTION_CONCURRENCY = 4;
478
+ const CLAIM_FAILURE_BACKOFF_MILLISECONDS = 3e4;
479
+ const runKey = (run) => "actionRunId" in run ? `action:${run.spaceId}:${run.actionRunId}` : `hook:${run.hookDelivery.id}`;
480
+ const claimRequestId = (run) => "actionRunId" in run ? void 0 : run.hookDelivery.dev_runner.request_id;
481
+ const blockClaim = (queue, run) => {
482
+ queue.claimBackoff.set(runKey(run), {
483
+ requestId: claimRequestId(run),
484
+ blockedUntil: Date.now() + CLAIM_FAILURE_BACKOFF_MILLISECONDS
485
+ });
486
+ };
487
+ const claimBlocked = (queue, run) => {
488
+ const key = runKey(run);
489
+ const backoff = queue.claimBackoff.get(key);
490
+ if (backoff === void 0) return false;
491
+ if (backoff.requestId === claimRequestId(run) && Date.now() < backoff.blockedUntil) return true;
492
+ queue.claimBackoff.delete(key);
493
+ return false;
494
+ };
495
+ const createDevActionRunner = (client, config, options = {}) => {
496
+ const state = {
497
+ stopped: false,
498
+ concurrency: Math.max(1, Math.floor(options.concurrency ?? DEFAULT_DEV_ACTION_CONCURRENCY)),
499
+ active: /* @__PURE__ */ new Map(),
500
+ idlePromise: Promise.resolve(),
501
+ pending: [],
502
+ scheduledIds: /* @__PURE__ */ new Set(),
503
+ claimBackoff: /* @__PURE__ */ new Map()
504
+ };
505
+ const enqueue = (queue, run) => {
506
+ const key = runKey(run);
507
+ if (queue.stopped || queue.scheduledIds.has(key)) return;
508
+ if (claimBlocked(queue, run)) return;
509
+ if (queue.scheduledIds.size === 0) queue.idlePromise = new Promise((resolve) => {
510
+ queue.resolveIdle = resolve;
511
+ });
512
+ queue.pending.push(run);
513
+ queue.scheduledIds.add(key);
514
+ drain(queue);
515
+ };
516
+ const drain = (queue) => {
517
+ while (!queue.stopped && queue.active.size < queue.concurrency && queue.pending.length > 0) {
518
+ const run = queue.pending.shift();
519
+ if (run === void 0) continue;
520
+ const key = runKey(run);
521
+ const active = {};
522
+ queue.active.set(key, active);
523
+ executeDevRun(run, active).finally(() => {
524
+ queue.active.delete(key);
525
+ queue.scheduledIds.delete(key);
526
+ drain(queue);
527
+ settleIdle(queue);
528
+ });
529
+ }
530
+ };
531
+ const settleIdle = (queue) => {
532
+ if (queue.scheduledIds.size > 0) return;
533
+ queue.resolveIdle?.();
534
+ queue.resolveIdle = void 0;
535
+ };
536
+ const executeDevRun = async (run, active) => {
537
+ if ("hookDelivery" in run) {
538
+ await executeDevHook(run.hookDelivery, active);
539
+ return;
540
+ }
541
+ await executeDevAction(run, active);
542
+ };
543
+ const executeDevAction = async ({ actionRunId, spaceId }, active) => {
544
+ let callback;
545
+ try {
546
+ let { run } = await client.runs.show("action", actionRunId, spaceId);
547
+ if (state.stopped || run.runner !== "dev" || run.status !== "running") return;
548
+ if (typeof run.dev_runner?.secret_grant === "string" && run.dev_runner.secret_grant.length > 0) {
549
+ try {
550
+ await client.dev.actionRuns.claim(actionRunId, spaceId);
551
+ } catch (claimError) {
552
+ if (!(isPonderbotError(claimError) && claimError.status === 409)) throw claimError;
553
+ ({run} = await client.runs.show("action", actionRunId, spaceId));
554
+ if (state.stopped || run.runner !== "dev" || run.status !== "running") return;
555
+ blockClaim(state, {
556
+ actionRunId,
557
+ spaceId
558
+ });
559
+ console.error(formatError(claimError));
560
+ return;
561
+ }
562
+ ({run} = await client.runs.show("action", actionRunId, spaceId));
563
+ if (state.stopped || run.runner !== "dev" || run.status !== "running") return;
564
+ }
565
+ const context = devCallContext(run, spaceId);
566
+ const actionExecution = devActionExecution(run);
567
+ const activeCallback = {
568
+ ...devRunnerCallback(run),
569
+ checkpoint: { onceRecords: actionExecution.onceRecords }
570
+ };
571
+ callback = activeCallback;
572
+ const execution = devExecutionBoundary(run);
573
+ const controller = new AbortController();
574
+ active.requestId = activeCallback.requestId;
575
+ active.controller = controller;
576
+ const environment = await redeemActionSecretGrant(run, spaceId, config);
577
+ const result = await executePonderbotWorkerInNode({
578
+ dir: config.project,
579
+ dev: true,
580
+ invocation: {
581
+ kind: "action",
582
+ name: run.ref ?? actionRunId,
583
+ input: devInput(run),
584
+ execution: actionExecution
585
+ },
586
+ context,
587
+ execution,
588
+ applicationOrigin: config.applicationOrigin ?? devApplicationOrigin(run),
589
+ completionGraceMilliseconds: devCompletionGraceMilliseconds(run),
590
+ environment,
591
+ reload: true,
592
+ signal: controller.signal
593
+ });
594
+ if (state.stopped) return;
595
+ try {
596
+ await postRunnerCallback(activeCallback, devRunnerCallbackBody(result));
597
+ } catch (callbackError) {
598
+ logCallbackDeliveryError(callbackError);
599
+ return;
600
+ }
601
+ } catch (error) {
602
+ if (state.stopped) return;
603
+ if (callback === void 0) {
604
+ console.error(formatError(error));
605
+ return;
606
+ }
607
+ try {
608
+ await postRunnerCallback(callback, devRunnerFailureBody(error, callback.checkpoint));
609
+ } catch (callbackError) {
610
+ logCallbackDeliveryError(callbackError);
611
+ }
612
+ console.error(formatError(error));
613
+ }
614
+ };
615
+ const executeDevHook = async (offered, active) => {
616
+ let delivery;
617
+ try {
618
+ ({delivery} = await client.dev.hookDeliveries.claim(offered.id, offered.space_id));
619
+ } catch (claimError) {
620
+ if (state.stopped) return;
621
+ if (isPonderbotError(claimError) && claimError.status === 404) return;
622
+ if (isPonderbotError(claimError) && claimError.status === 409) blockClaim(state, { hookDelivery: offered });
623
+ console.error(formatError(claimError));
624
+ return;
625
+ }
626
+ const callback = devHookCallback(delivery);
627
+ try {
628
+ const context = devHookContext(delivery);
629
+ const execution = devHookExecution(delivery);
630
+ const controller = new AbortController();
631
+ active.requestId = callback.requestId;
632
+ active.controller = controller;
633
+ let environment;
634
+ try {
635
+ environment = await redeemSecretGrant({
636
+ token: delivery.dev_runner.secret_grant,
637
+ kind: "hook",
638
+ ownerId: delivery.id,
639
+ spaceId: context.space_id,
640
+ releaseId: context.release_id,
641
+ runId: context.run_id,
642
+ attempt: context.attempt,
643
+ requestId: delivery.dev_runner.request_id,
644
+ runnerId: delivery.dev_runner.runner_id
645
+ }, config, `dev_hook_secret_delivery_failed: ${delivery.id}`, `dev_hook_secret_delivery_invalid: ${delivery.id}`);
646
+ } catch (redeemError) {
647
+ if (state.stopped) return;
648
+ if (!isSecretGrantContention(redeemError)) throw redeemError;
649
+ blockClaim(state, { hookDelivery: offered });
650
+ console.error(formatError(redeemError));
651
+ return;
652
+ }
653
+ const event = delivery.dev_runner.worker_command.event;
654
+ if (!isLifecycleEvent(event)) throw new Error(`dev_hook_event_invalid: ${delivery.id}`);
655
+ const result = await executePonderbotWorkerInNode({
656
+ dir: config.project,
657
+ dev: true,
658
+ invocation: {
659
+ kind: "hook",
660
+ name: delivery.dev_runner.worker_command.name,
661
+ event
662
+ },
663
+ context,
664
+ execution,
665
+ applicationOrigin: config.applicationOrigin,
666
+ completionGraceMilliseconds: 0,
667
+ environment,
668
+ reload: true,
669
+ signal: controller.signal
670
+ });
671
+ if (state.stopped) return;
672
+ try {
673
+ await postHookRunnerCallback(callback, devHookCallbackBody(result));
674
+ } catch (callbackError) {
675
+ logCallbackDeliveryError(callbackError);
676
+ }
677
+ } catch (error) {
678
+ if (state.stopped) return;
679
+ try {
680
+ await postHookRunnerCallback(callback, devHookFailureBody(error));
681
+ } catch (callbackError) {
682
+ logCallbackDeliveryError(callbackError);
683
+ }
684
+ console.error(formatError(error));
685
+ }
686
+ };
687
+ return {
688
+ async catchUp() {
689
+ const response = await client.runs.list({
690
+ type: "action",
691
+ status: "running"
692
+ });
693
+ for (const run of response.runs) if (run.runner === "dev") enqueue(state, {
694
+ actionRunId: run.id,
695
+ spaceId: config.spaceId
696
+ });
697
+ },
698
+ async catchUpManifest(manifestName) {
699
+ const response = await client.dev.manifests.actionRuns(manifestName, config.environment);
700
+ const runningKeys = new Set(response.runs.map((run) => runKey({
701
+ actionRunId: run.id,
702
+ spaceId: run.space_id
703
+ })));
704
+ const hookResponse = await client.dev.manifests.hookDeliveries(manifestName, config.environment);
705
+ const hookKeys = new Set(hookResponse.deliveries.map((delivery) => runKey({ hookDelivery: delivery })));
706
+ for (const [key, active] of state.active) if (!runningKeys.has(key) && !hookKeys.has(key)) active.controller?.abort(new DOMException("Runtime cancelled execution", "AbortError"));
707
+ for (const run of response.runs) enqueue(state, {
708
+ actionRunId: run.id,
709
+ spaceId: run.space_id
710
+ });
711
+ for (const delivery of hookResponse.deliveries) enqueue(state, { hookDelivery: delivery });
712
+ },
713
+ handleEvent(event) {
714
+ if (isDevActionDispatchHint(event)) {
715
+ enqueue(state, {
716
+ actionRunId: event.data.run_id,
717
+ spaceId: eventSpaceId(event, config.spaceId)
718
+ });
719
+ return;
720
+ }
721
+ if (isDevActionTerminalHint(event)) {
722
+ const active = state.active.get(runKey({
723
+ actionRunId: event.data.run_id,
724
+ spaceId: eventSpaceId(event, config.spaceId)
725
+ }));
726
+ if (active?.requestId === event.data.metadata.request_id) active.controller?.abort(new DOMException("Runtime cancelled execution", "AbortError"));
727
+ }
728
+ },
729
+ idle() {
730
+ return state.idlePromise;
731
+ },
732
+ stop() {
733
+ if (!state.stopped) {
734
+ state.stopped = true;
735
+ for (const run of state.pending) state.scheduledIds.delete(runKey(run));
736
+ state.pending.length = 0;
737
+ for (const active of state.active.values()) active.controller?.abort(new DOMException("Dev runner stopped", "AbortError"));
738
+ settleIdle(state);
739
+ }
740
+ return state.idlePromise;
741
+ }
742
+ };
743
+ };
744
+ const isDevActionTerminalHint = (event) => event.event === "run_updated" && isRecord(event.data) && event.data.type === "run_updated" && event.data.run_type === "action" && typeof event.data.run_id === "string" && isRecord(event.data.metadata) && event.data.metadata.runner === "dev" && typeof event.data.metadata.request_id === "string" && [
745
+ "cancelled",
746
+ "failed",
747
+ "completed"
748
+ ].includes(String(event.data.metadata.status));
749
+ const isDevActionDispatchHint = (event) => event.event === "run_updated" && isRecord(event.data) && event.data.type === "run_updated" && event.data.run_type === "action" && typeof event.data.run_id === "string" && isRecord(event.data.metadata) && event.data.metadata.runner === "dev" && event.data.metadata.status === "running";
750
+ const eventSpaceId = (event, fallback) => isRecord(event.data) && typeof event.data.space_id === "string" ? event.data.space_id : fallback;
751
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
752
+ const isJsonObject = (value) => isRecord(value) && Object.values(value).every(isJsonValue);
753
+ const isJsonValue = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value) && value.every(isJsonValue) || isJsonObject(value);
754
+ const devInput = (run) => {
755
+ const input = run.input ?? {};
756
+ if (!isJsonObject(input)) throw new Error(`dev_runner_input_invalid: ${run.id}`);
757
+ return input;
758
+ };
759
+ const devCallContext = (run, spaceId) => {
760
+ const devRunner = run.dev_runner;
761
+ if (devRunner === null || devRunner === void 0 || typeof devRunner.runner_id !== "string") throw new Error(`dev_runner_context_missing: ${run.id}`);
762
+ if (devRunner.schedule_ref === void 0 !== (devRunner.scheduled_at === void 0)) throw new Error(`dev_runner_schedule_context_invalid: ${run.id}`);
763
+ const context = {
764
+ space_id: spaceId,
765
+ action_run_id: run.id,
766
+ run_type: "action",
767
+ run_id: run.id,
768
+ release_id: devReleaseId(run),
769
+ attempt: devAttempt(run),
770
+ runner_id: devRunner.runner_id,
771
+ metadata: { request_id: devRequestId(run) }
772
+ };
773
+ if (devRunner.principal !== void 0) context.principal = devRunner.principal;
774
+ if (devRunner.workflow_run_id !== void 0) context.workflow_run_id = devRunner.workflow_run_id;
775
+ if (devRunner.schedule_ref !== void 0 && devRunner.scheduled_at !== void 0) {
776
+ context.schedule_ref = devRunner.schedule_ref;
777
+ context.scheduled_at = devRunner.scheduled_at;
778
+ }
779
+ return context;
780
+ };
781
+ const devReleaseId = (run) => {
782
+ if (typeof run.release_id !== "string" || run.release_id.length === 0) throw new Error(`dev_runner_release_id_invalid: ${run.id}`);
783
+ return run.release_id;
784
+ };
785
+ const devApplicationOrigin = (run) => {
786
+ const origin = run.dev_runner?.application_origin;
787
+ if (origin === void 0) return;
788
+ if (typeof origin !== "string" || origin.length === 0) throw new Error(`dev_runner_application_origin_invalid: ${run.id}`);
789
+ return origin;
790
+ };
791
+ const devRunnerCallback = (run) => {
792
+ const devRunner = run.dev_runner;
793
+ if (devRunner === null || devRunner === void 0 || typeof devRunner.callback_url !== "string" || typeof devRunner.callback_token !== "string" || typeof devRunner.runner_id !== "string") throw new Error(`dev_runner_context_missing: ${run.id}`);
794
+ return {
795
+ url: devRunner.callback_url,
796
+ token: devRunner.callback_token,
797
+ runnerId: devRunner.runner_id,
798
+ requestId: devRequestId(run),
799
+ actionRunId: run.id,
800
+ attempt: devAttempt(run)
801
+ };
802
+ };
803
+ const devExecutionBoundary = (run) => {
804
+ const execution = run.dev_runner?.execution;
805
+ try {
806
+ const parsed = parseSerializedExecutionBoundary(execution);
807
+ if (parsed.cancellationId !== devRequestId(run)) throw new Error("execution_request_identity_mismatch");
808
+ return parsed;
809
+ } catch {
810
+ throw new Error(`dev_runner_execution_boundary_invalid: ${run.id}`);
811
+ }
812
+ };
813
+ const devActionExecution = (run) => {
814
+ try {
815
+ return parseActionExecutionSnapshot(run.dev_runner?.action_execution);
816
+ } catch {
817
+ throw new Error(`dev_runner_action_execution_invalid: ${run.id}`);
818
+ }
819
+ };
820
+ const devRequestId = (run) => {
821
+ const requestId = run.dev_runner?.request_id;
822
+ if (typeof requestId !== "string" || requestId.length === 0) throw new Error(`dev_runner_request_id_invalid: ${run.id}`);
823
+ return requestId;
824
+ };
825
+ const devAttempt = (run) => {
826
+ if (!Number.isSafeInteger(run.attempt) || Number(run.attempt) <= 0) throw new Error(`dev_runner_attempt_invalid: ${run.id}`);
827
+ return Number(run.attempt);
828
+ };
829
+ const devCompletionGraceMilliseconds = (run) => {
830
+ const seconds = run.dev_runner?.completion_grace_seconds;
831
+ if (!Number.isSafeInteger(seconds) || Number(seconds) < 0) throw new Error(`dev_runner_completion_grace_invalid: ${run.id}`);
832
+ const milliseconds = Number(seconds) * 1e3;
833
+ if (!Number.isSafeInteger(milliseconds)) throw new Error(`dev_runner_completion_grace_invalid: ${run.id}`);
834
+ return milliseconds;
835
+ };
836
+ const postRunnerCallback = async (callback, body) => {
837
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
838
+ const response = await fetch(callback.url, {
839
+ method: "POST",
840
+ headers: {
841
+ authorization: `Bearer ${callback.token}`,
842
+ "content-type": "application/json",
843
+ "x-ponderbot-runner-event-id": [
844
+ "dev-action",
845
+ callback.actionRunId,
846
+ callback.attempt,
847
+ body.status,
848
+ globalThis.crypto.randomUUID()
849
+ ].join(":"),
850
+ "x-ponderbot-runner-timestamp": occurredAt
851
+ },
852
+ body: JSON.stringify({
853
+ runner_id: callback.runnerId,
854
+ status: body.status,
855
+ exit_code: body.exit_code,
856
+ result: body.result,
857
+ reason: body.reason,
858
+ checkpoint: body.checkpoint,
859
+ suspension: body.suspension,
860
+ occurred_at: occurredAt
861
+ })
862
+ });
863
+ const responseBody = await response.json().catch(() => ({}));
864
+ if (!response.ok) {
865
+ const error = isRecord(responseBody) && responseBody.error !== void 0 ? responseBody.error : response.statusText;
866
+ throw ponderbotError(response.status, String(error), responseBody);
867
+ }
868
+ };
869
+ const devRunnerCallbackBody = (result) => {
870
+ switch (result.status) {
871
+ case "completed": return {
872
+ status: "completed",
873
+ exit_code: 0,
874
+ result: result.output,
875
+ reason: null,
876
+ checkpoint: result.checkpoint
877
+ };
878
+ case "suspended": return {
879
+ status: "suspended",
880
+ exit_code: 0,
881
+ result: {},
882
+ reason: null,
883
+ checkpoint: result.checkpoint,
884
+ suspension: result.suspension
885
+ };
886
+ case "failed": return {
887
+ status: "failed",
888
+ exit_code: 1,
889
+ result: { error: result.error.detail === void 0 ? {
890
+ code: result.error.code,
891
+ message: result.error.message
892
+ } : {
893
+ code: result.error.code,
894
+ message: result.error.message,
895
+ detail: result.error.detail
896
+ } },
897
+ reason: result.error.code,
898
+ checkpoint: result.checkpoint
899
+ };
900
+ case "cancelled": return {
901
+ status: "cancelled",
902
+ exit_code: result.reason === "deadline_exceeded" ? 124 : 130,
903
+ result: {},
904
+ reason: result.reason,
905
+ checkpoint: result.checkpoint
906
+ };
907
+ }
908
+ };
909
+ const devRunnerFailureBody = (error, checkpoint) => {
910
+ const message = errorMessage(error);
911
+ const reason = error instanceof NodeWorkerFailure ? error.reason : "runner_failed";
912
+ return {
913
+ status: "failed",
914
+ exit_code: 1,
915
+ result: { error: {
916
+ code: reason,
917
+ message
918
+ } },
919
+ reason,
920
+ checkpoint
921
+ };
922
+ };
923
+ const devHookCallback = (delivery) => ({
924
+ url: delivery.dev_runner.callback_url,
925
+ token: delivery.dev_runner.callback_token,
926
+ runnerId: delivery.dev_runner.runner_id,
927
+ hookDeliveryId: delivery.id,
928
+ requestId: delivery.dev_runner.request_id
929
+ });
930
+ const devHookContext = (delivery) => {
931
+ const context = delivery.dev_runner.context;
932
+ const spaceId = context.space_id;
933
+ const runId = context.run_id;
934
+ const releaseId = context.release_id;
935
+ const hookDeliveryId = context.hook_delivery_id;
936
+ const attempt = context.attempt;
937
+ if (typeof spaceId !== "string" || typeof runId !== "string" || typeof releaseId !== "string" || typeof hookDeliveryId !== "string" || typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt <= 0) throw new Error(`dev_hook_context_invalid: ${delivery.id}`);
938
+ return {
939
+ space_id: spaceId,
940
+ run_id: runId,
941
+ release_id: releaseId,
942
+ hook_delivery_id: hookDeliveryId,
943
+ attempt,
944
+ runner_id: delivery.dev_runner.runner_id,
945
+ metadata: { request_id: delivery.dev_runner.request_id }
946
+ };
947
+ };
948
+ const devHookExecution = (delivery) => {
949
+ try {
950
+ const execution = delivery.dev_runner.context.execution;
951
+ const parsed = parseSerializedExecutionBoundary(execution);
952
+ if (parsed.cancellationId !== delivery.dev_runner.request_id) throw new Error("execution_request_identity_mismatch");
953
+ return parsed;
954
+ } catch {
955
+ throw new Error(`dev_hook_execution_boundary_invalid: ${delivery.id}`);
956
+ }
957
+ };
958
+ const redeemActionSecretGrant = async (run, spaceId, config) => {
959
+ const grant = run.dev_runner?.secret_grant;
960
+ if (grant === void 0) return;
961
+ if (typeof grant !== "string" || grant.length === 0) throw new Error(`dev_runner_secret_grant_invalid: ${run.id}`);
962
+ return redeemSecretGrant({
963
+ token: grant,
964
+ kind: "action",
965
+ ownerId: run.id,
966
+ spaceId,
967
+ releaseId: devReleaseId(run),
968
+ runId: run.id,
969
+ attempt: devAttempt(run),
970
+ requestId: devRequestId(run),
971
+ runnerId: devRunnerCallback(run).runnerId
972
+ }, config, `dev_action_secret_delivery_failed: ${run.id}`, `dev_action_secret_delivery_invalid: ${run.id}`);
973
+ };
974
+ const redeemSecretGrant = async (identity, config, failed, invalid) => {
975
+ const response = await fetch(`${config.url}/api/internal/secret-delivery/redeem`, {
976
+ method: "POST",
977
+ headers: { "content-type": "application/json" },
978
+ body: JSON.stringify({
979
+ token: identity.token,
980
+ kind: identity.kind,
981
+ owner_id: identity.ownerId,
982
+ space_id: identity.spaceId,
983
+ release_id: identity.releaseId,
984
+ run_id: identity.runId,
985
+ attempt: identity.attempt,
986
+ request_id: identity.requestId,
987
+ runner_id: identity.runnerId
988
+ })
989
+ });
990
+ const body = await response.json().catch(() => void 0);
991
+ if (!response.ok) {
992
+ const code = isRecord(body) && typeof body.error === "string" ? body.error : "unknown_error";
993
+ throw Object.assign(ponderbotError(response.status, code, body), { message: `${failed}: HTTP ${response.status} ${code}` });
994
+ }
995
+ if (!isRecord(body) || !isRecord(body.env)) throw new Error(invalid);
996
+ const environment = {};
997
+ for (const [name, value] of Object.entries(body.env)) {
998
+ if (typeof value !== "string") throw new Error(invalid);
999
+ environment[name] = value;
1000
+ }
1001
+ return environment;
1002
+ };
1003
+ const postHookRunnerCallback = async (callback, body) => {
1004
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
1005
+ const response = await fetch(callback.url, {
1006
+ method: "POST",
1007
+ headers: {
1008
+ authorization: `Bearer ${callback.token}`,
1009
+ "content-type": "application/json",
1010
+ "x-ponderbot-runner-event-id": [
1011
+ "dev-hook",
1012
+ callback.hookDeliveryId,
1013
+ callback.requestId,
1014
+ body.status,
1015
+ globalThis.crypto.randomUUID()
1016
+ ].join(":"),
1017
+ "x-ponderbot-runner-timestamp": occurredAt
1018
+ },
1019
+ body: JSON.stringify({
1020
+ runner_id: callback.runnerId,
1021
+ status: body.status,
1022
+ exit_code: body.exit_code,
1023
+ result: body.result,
1024
+ reason: body.reason,
1025
+ occurred_at: occurredAt
1026
+ })
1027
+ });
1028
+ const responseBody = await response.json().catch(() => ({}));
1029
+ if (!response.ok) {
1030
+ const error = isRecord(responseBody) && responseBody.error !== void 0 ? responseBody.error : response.statusText;
1031
+ throw ponderbotError(response.status, String(error), responseBody);
1032
+ }
1033
+ };
1034
+ const devHookCallbackBody = (result) => {
1035
+ switch (result.status) {
1036
+ case "completed": return {
1037
+ status: "completed",
1038
+ exit_code: 0,
1039
+ result: result.output,
1040
+ reason: null
1041
+ };
1042
+ case "failed": return {
1043
+ status: "failed",
1044
+ exit_code: 1,
1045
+ result: { error: result.error },
1046
+ reason: result.error.code
1047
+ };
1048
+ case "cancelled": return {
1049
+ status: "cancelled",
1050
+ exit_code: result.reason === "deadline_exceeded" ? 124 : 130,
1051
+ result: {},
1052
+ reason: result.reason
1053
+ };
1054
+ }
1055
+ };
1056
+ const devHookFailureBody = (error) => ({
1057
+ status: "failed",
1058
+ exit_code: 1,
1059
+ result: { error: {
1060
+ code: error instanceof NodeWorkerFailure ? error.reason : "runner_failed",
1061
+ message: errorMessage(error)
1062
+ } },
1063
+ reason: error instanceof NodeWorkerFailure ? error.reason : "runner_failed"
1064
+ });
1065
+ const errorMessage = (error) => {
1066
+ return error instanceof Error ? error.message : String(error);
1067
+ };
1068
+ const logCallbackDeliveryError = (error) => {
1069
+ if (isAttemptSupersededCallbackError(error)) return;
1070
+ console.error(formatError(error));
1071
+ };
1072
+ const SECRET_GRANT_CONTENTION_CODES = ["secret_delivery_grant_redeemed", "secret_delivery_grant_not_active"];
1073
+ const isSecretGrantContention = (error) => isPonderbotError(error) && error.status === 409 && SECRET_GRANT_CONTENTION_CODES.includes(error.code);
1074
+ const isAttemptSupersededCallbackError = (error) => {
1075
+ return isPonderbotError(error) && [401, 409].includes(error.status);
1076
+ };
1077
+ //#endregion
1078
+ //#region src/events.ts
1079
+ function followEvents(client, onEvent, options = {}) {
1080
+ const controller = new AbortController();
1081
+ return {
1082
+ stop: () => controller.abort(),
1083
+ done: runEventTail(client, onEvent, controller, options)
1084
+ };
1085
+ }
1086
+ async function runEventTail(client, onEvent, controller, options) {
1087
+ try {
1088
+ for await (const event of client.events.streamEvents({
1089
+ signal: controller.signal,
1090
+ initialRetryMs: options.retryMs,
1091
+ onReconnect: options.onReconnect
1092
+ })) onEvent(event);
1093
+ } catch (error) {
1094
+ if (!controller.signal.aborted) {
1095
+ if (options.onError) {
1096
+ options.onError(error);
1097
+ return;
1098
+ }
1099
+ throw error;
1100
+ }
1101
+ }
1102
+ }
1103
+ //#endregion
1104
+ //#region src/manifest-diagnostics.ts
1105
+ const printManifestDiagnostics = (budgets, warnings) => {
1106
+ if (budgets.length > 0) {
1107
+ console.error("agent instruction sizes:");
1108
+ for (const budget of budgets) console.error(` ${budget.ref}: ${instructionBudgetSummary(budget)}`);
1109
+ }
1110
+ for (const warning of warnings) console.error(`${warning.file}: warning: ${warning.message}`);
1111
+ };
1112
+ const instructionBudgetSummary = (budget) => {
1113
+ const suffix = budget.overWarning ? " over budget" : "";
1114
+ return `${formatCount(budget.chars)} chars (~${formatCount(budget.approxTokens)} tokens)${suffix}`;
1115
+ };
1116
+ const formatCount = (value) => value >= 1e3 ? `${(value / 1e3).toFixed(1)}k` : `${value}`;
1117
+ //#endregion
1118
+ //#region src/dev.ts
1119
+ const DEBOUNCE_MS = 300;
1120
+ const DEV_RUN_POLL_MS = 750;
1121
+ const SHUTDOWN_GRACE_MS = 2e3;
1122
+ async function devLoop(config) {
1123
+ const client = clientFor(config);
1124
+ let timer;
1125
+ let building = false;
1126
+ let queued = false;
1127
+ let stopped = false;
1128
+ let watcher;
1129
+ let tail;
1130
+ let runPoll;
1131
+ let pollingRuns = false;
1132
+ let manifestName;
1133
+ await ensureDevSpace$1(client, config);
1134
+ await reconcileDeploymentEnvironment(client, config);
1135
+ const devRunner = createDevActionRunner(client, config);
1136
+ const stop = async () => {
1137
+ if (stopped) return;
1138
+ stopped = true;
1139
+ queued = false;
1140
+ if (timer) clearTimeout(timer);
1141
+ if (runPoll) clearInterval(runPoll);
1142
+ tail?.stop();
1143
+ const cleanup = Promise.allSettled([watcher?.close(), devRunner.stop()]);
1144
+ await Promise.race([cleanup, new Promise((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_MS))]);
1145
+ process.exit(0);
1146
+ };
1147
+ const handleSignal = () => {
1148
+ stop().catch((error) => {
1149
+ console.error(formatError(error));
1150
+ process.exit(1);
1151
+ });
1152
+ };
1153
+ process.once("SIGINT", handleSignal);
1154
+ process.once("SIGTERM", handleSignal);
1155
+ async function rebuild(reason) {
1156
+ if (stopped) return;
1157
+ if (building) {
1158
+ queued = true;
1159
+ return;
1160
+ }
1161
+ building = true;
1162
+ try {
1163
+ const { manifest, warnings, instructionBudgets } = await buildManifest(config.project, {
1164
+ dev: true,
1165
+ reload: true
1166
+ });
1167
+ printManifestDiagnostics(instructionBudgets, warnings);
1168
+ const developmentManifest = staticProjectManifest(manifest);
1169
+ const release = await client.manifests.deploy(developmentManifest, `dev:${reason}`);
1170
+ await client.projects.releases.deploy(developmentManifest.name, config.environment, developmentManifest, `dev:${reason}`, void 0, config.applicationOrigin);
1171
+ console.log(deployLine(release, reason));
1172
+ manifestName = developmentManifest.name;
1173
+ await installDevelopmentProjectRelease(client, config, developmentManifest.name);
1174
+ } catch (error) {
1175
+ console.error(formatError(error));
1176
+ } finally {
1177
+ building = false;
1178
+ if (queued && !stopped) {
1179
+ queued = false;
1180
+ rebuild("queued-changes");
1181
+ }
1182
+ }
1183
+ }
1184
+ watcher = watch(config.project, { ignoreInitial: true });
1185
+ watcher.on("all", (_event, file) => {
1186
+ if (stopped) return;
1187
+ if (timer) clearTimeout(timer);
1188
+ timer = setTimeout(() => void rebuild(file || "change"), DEBOUNCE_MS);
1189
+ });
1190
+ await new Promise((resolve, reject) => {
1191
+ watcher?.once("ready", resolve);
1192
+ watcher?.once("error", reject);
1193
+ });
1194
+ await rebuild("startup");
1195
+ const catchUpManifest = async () => {
1196
+ if (pollingRuns || manifestName === void 0 || stopped) return;
1197
+ pollingRuns = true;
1198
+ try {
1199
+ await devRunner.catchUpManifest(manifestName);
1200
+ } catch (error) {
1201
+ console.error(formatError(error));
1202
+ } finally {
1203
+ pollingRuns = false;
1204
+ }
1205
+ };
1206
+ await catchUpManifest();
1207
+ runPoll = setInterval(() => void catchUpManifest(), DEV_RUN_POLL_MS);
1208
+ if (config.environmentRole === "release-source") return;
1209
+ await devRunner.catchUp().catch((error) => console.error(formatError(error)));
1210
+ tail = followEvents(client, (event) => {
1211
+ print(event);
1212
+ devRunner.handleEvent(event);
1213
+ }, {
1214
+ onError: (error) => console.error(formatError(error)),
1215
+ onReconnect: () => {
1216
+ devRunner.catchUp().catch((error) => console.error(formatError(error)));
1217
+ }
1218
+ });
1219
+ }
1220
+ async function ensureDevSpace$1(client, config) {
1221
+ try {
1222
+ await client.spaces.get();
1223
+ } catch (error) {
1224
+ if (isPonderbotError(error) && error.status === 404 && error.code === "space_not_found") throw new Error(`space "${config.spaceId}" not found; run: ponder spaces create ${config.spaceId}`);
1225
+ throw error;
1226
+ }
1227
+ }
1228
+ const deployLine = (response, reason) => {
1229
+ return [`deployed ${response.release.id} (${reason})`, ...Object.entries(response.registered).map(([key, value]) => `${key} ${value}`)].join(" ");
1230
+ };
1231
+ //#endregion
1232
+ //#region src/worker-artifact-dependencies.ts
1233
+ const NATIVE_FILE_PATTERN = /\.(?:wasm|node|gyp|c|cc|cpp|cxx|h|hpp|hxx)$/i;
1234
+ const builtins = new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
1235
+ const workerArtifactDependencyCollector = (forcedExternal) => {
1236
+ const forced = new Set(forcedExternal);
1237
+ const dependencies = /* @__PURE__ */ new Map();
1238
+ const packageCache = /* @__PURE__ */ new Map();
1239
+ const nativeCache = /* @__PURE__ */ new Map();
1240
+ return {
1241
+ dependencies: () => Object.fromEntries([...dependencies].sort()),
1242
+ plugin: {
1243
+ name: "ponderbot-worker-artifact-dependencies",
1244
+ setup(build) {
1245
+ build.onResolve({ filter: /.*/ }, async (args) => {
1246
+ if (!isPackageImport(args.path) || builtins.has(args.path) || args.pluginData?.ponderbotDependencyProbe === true) return;
1247
+ const importName = packageName(args.path);
1248
+ if (dependencies.has(importName)) return {
1249
+ path: args.path,
1250
+ external: true
1251
+ };
1252
+ const resolved = await build.resolve(args.path, {
1253
+ importer: args.importer,
1254
+ kind: args.kind,
1255
+ namespace: args.namespace,
1256
+ resolveDir: args.resolveDir,
1257
+ pluginData: { ponderbotDependencyProbe: true }
1258
+ });
1259
+ if (resolved.errors.length > 0 || resolved.path === "") return;
1260
+ const identity = await packageIdentity(resolved.path, packageCache);
1261
+ if (identity === null) {
1262
+ if (forced.has(importName)) return { errors: [{ text: `External Worker dependency ${importName} has no package name and version` }] };
1263
+ return;
1264
+ }
1265
+ if (!(forced.has(importName) || await packageUsesNativeCode(identity, resolved.path, nativeCache))) return;
1266
+ dependencies.set(importName, identity.name === importName ? identity.version : `npm:${identity.name}@${identity.version}`);
1267
+ return {
1268
+ path: args.path,
1269
+ external: true
1270
+ };
1271
+ });
1272
+ }
1273
+ }
1274
+ };
1275
+ };
1276
+ const isPackageImport = (specifier) => ![
1277
+ ".",
1278
+ "/",
1279
+ "~",
1280
+ "#",
1281
+ "file:",
1282
+ "data:"
1283
+ ].some((prefix) => specifier.startsWith(prefix));
1284
+ const packageName = (specifier) => {
1285
+ const parts = specifier.split("/");
1286
+ return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0] ?? specifier;
1287
+ };
1288
+ const packageIdentity = async (resolvedPath, cache) => {
1289
+ let directory = dirname(resolvedPath);
1290
+ const root = parse(directory).root;
1291
+ while (directory !== root) {
1292
+ if (cache.has(directory)) return cache.get(directory) ?? null;
1293
+ try {
1294
+ const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
1295
+ if (typeof packageJson.name === "string" && packageJson.name !== "" && typeof packageJson.version === "string" && packageJson.version !== "") {
1296
+ const identity = {
1297
+ name: packageJson.name,
1298
+ root: directory,
1299
+ version: packageJson.version,
1300
+ packageJson
1301
+ };
1302
+ cache.set(directory, identity);
1303
+ return identity;
1304
+ }
1305
+ } catch (error) {
1306
+ if (error.code !== "ENOENT") throw error;
1307
+ }
1308
+ directory = dirname(directory);
1309
+ }
1310
+ cache.set(dirname(resolvedPath), null);
1311
+ return null;
1312
+ };
1313
+ const packageUsesNativeCode = async (identity, resolvedPath, cache) => {
1314
+ if (NATIVE_FILE_PATTERN.test(resolvedPath)) return true;
1315
+ const cached = cache.get(identity.root);
1316
+ if (cached !== void 0) return cached;
1317
+ const declaredFiles = [...Array.isArray(identity.packageJson.files) ? identity.packageJson.files.filter((file) => typeof file === "string") : [], ...[
1318
+ "main",
1319
+ "module",
1320
+ "browser"
1321
+ ].flatMap((field) => {
1322
+ const value = identity.packageJson[field];
1323
+ return typeof value === "string" ? [value] : [];
1324
+ })];
1325
+ let bindingGyp = false;
1326
+ try {
1327
+ await access(join(identity.root, "binding.gyp"));
1328
+ bindingGyp = true;
1329
+ } catch (error) {
1330
+ if (error.code !== "ENOENT") throw error;
1331
+ }
1332
+ const native = identity.packageJson.gypfile === true || bindingGyp || declaredFiles.some((file) => NATIVE_FILE_PATTERN.test(file));
1333
+ cache.set(identity.root, native);
1334
+ return native;
1335
+ };
1336
+ //#endregion
1337
+ //#region src/worker-artifact.ts
1338
+ const ARTIFACT_FORMAT = "ponderbot-worker-artifact/v1";
1339
+ const PROJECT_DIRECTORY = "project";
1340
+ const WORKER_MAIN = "worker-main.ts";
1341
+ var WorkerArtifactError = class extends Error {
1342
+ constructor(message) {
1343
+ super(message);
1344
+ this.name = "WorkerArtifactError";
1345
+ }
1346
+ };
1347
+ const containerfile = (hasDependencies) => `# syntax=docker/dockerfile:1.7
1348
+ ${hasDependencies ? `FROM node:24-bookworm-slim AS dependencies
1349
+ WORKDIR /opt/ponderbot
1350
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
1351
+ COPY package.json ./
1352
+ RUN npm install --omit=dev --no-package-lock --no-audit --no-fund
1353
+ ` : ""}FROM node:24-bookworm-slim
1354
+ ENV NODE_ENV=production
1355
+ ENV PONDERBOT_PROJECT_DIR=/opt/ponderbot/project
1356
+ WORKDIR /workspace
1357
+ ${hasDependencies ? "COPY --from=dependencies --chown=node:node /opt/ponderbot/node_modules /opt/ponderbot/node_modules\n" : ""}COPY --chown=node:node . /opt/ponderbot
1358
+ USER node
1359
+ ENTRYPOINT ["node", "/opt/ponderbot/worker-main.js"]
1360
+ `;
1361
+ const buildWorkerArtifact = async (source, projectDir, options = {}) => {
1362
+ const projectRoot = resolve(projectDir);
1363
+ const root = await mkdtemp(join(tmpdir(), "ponderbot-worker-artifact-"));
1364
+ const entries = join(root, "entries");
1365
+ const context = join(root, "artifact");
1366
+ try {
1367
+ await mkdir(entries, { recursive: true });
1368
+ await mkdir(context, { recursive: true });
1369
+ const definitionFiles = [
1370
+ await projectConfigFile(projectRoot),
1371
+ ...source.actions.map(({ file }) => file),
1372
+ ...source.agents.map(({ file }) => file),
1373
+ ...source.groups.map(({ file }) => file),
1374
+ ...source.workflows.map(({ file }) => file)
1375
+ ];
1376
+ const entrypoints = [];
1377
+ const entryPaths = /* @__PURE__ */ new Set();
1378
+ for (const file of [...new Set(definitionFiles)].sort()) {
1379
+ const projectPath = artifactProjectPath(projectRoot, file);
1380
+ const entryPath = join(entries, PROJECT_DIRECTORY, projectPath.slice(0, -extname(projectPath).length) + ".ts");
1381
+ if (entryPaths.has(entryPath)) throw new WorkerArtifactError(`Worker artifact entry path collision for ${projectPath}`);
1382
+ entryPaths.add(entryPath);
1383
+ await mkdir(dirname(entryPath), { recursive: true });
1384
+ await writeFile(entryPath, definitionWrapper(resolve(file)), { mode: 384 });
1385
+ entrypoints.push(entryPath);
1386
+ }
1387
+ const workerEntry = join(entries, WORKER_MAIN);
1388
+ await writeFile(workerEntry, workerMainWrapper(), { mode: 384 });
1389
+ entrypoints.push(workerEntry);
1390
+ console.error(`bundling ${entrypoints.length - 1} Ponderbot definition modules`);
1391
+ const dependencies = await bundleEntries(entries, context, entrypoints, options.external ?? []);
1392
+ const assetFiles = /* @__PURE__ */ new Set();
1393
+ for (const skill of source.skills) assetFiles.add(skill.file);
1394
+ for (const agent of source.agents) {
1395
+ assetFiles.add(agent.instructionsFile);
1396
+ if (agent.soulFile !== void 0) assetFiles.add(agent.soulFile);
1397
+ }
1398
+ for (const file of [...assetFiles].sort()) {
1399
+ const destination = join(context, PROJECT_DIRECTORY, artifactProjectPath(projectRoot, file));
1400
+ await mkdir(dirname(destination), { recursive: true });
1401
+ await copyFile(file, destination);
1402
+ }
1403
+ await writeFile(join(context, "package.json"), `${JSON.stringify({
1404
+ name: "ponderbot-worker-artifact",
1405
+ private: true,
1406
+ type: "module",
1407
+ engines: { node: ">=22.4.0" },
1408
+ dependencies
1409
+ }, null, 2)}\n`, { mode: 384 });
1410
+ const dockerfile = join(context, "Containerfile");
1411
+ await writeFile(dockerfile, containerfile(Object.keys(dependencies).length > 0), { mode: 384 });
1412
+ const contentHash = await artifactContentHash(context);
1413
+ await writeFile(join(context, "ponderbot-build.json"), `${JSON.stringify({
1414
+ format: ARTIFACT_FORMAT,
1415
+ content_hash: contentHash,
1416
+ project: {
1417
+ name: source.project.name,
1418
+ version: source.project.version ?? null
1419
+ },
1420
+ definition_modules: entrypoints.length - 1,
1421
+ external_dependencies: Object.keys(dependencies).length
1422
+ }, null, 2)}\n`, { mode: 384 });
1423
+ return {
1424
+ context,
1425
+ dockerfile,
1426
+ dispose: async () => rm(root, {
1427
+ recursive: true,
1428
+ force: true
1429
+ })
1430
+ };
1431
+ } catch (error) {
1432
+ await rm(root, {
1433
+ recursive: true,
1434
+ force: true
1435
+ });
1436
+ if (error instanceof WorkerArtifactError) throw error;
1437
+ throw new WorkerArtifactError(error instanceof Error ? error.message : String(error));
1438
+ }
1439
+ };
1440
+ const projectConfigFile = async (projectRoot) => {
1441
+ for (const extension of [
1442
+ ".ts",
1443
+ ".js",
1444
+ ".mjs"
1445
+ ]) {
1446
+ const path = join(projectRoot, `config${extension}`);
1447
+ try {
1448
+ await access(path);
1449
+ return path;
1450
+ } catch (error) {
1451
+ if (error.code !== "ENOENT") throw error;
1452
+ }
1453
+ }
1454
+ throw new WorkerArtifactError(`Ponderbot project config was not found under ${projectRoot}`);
1455
+ };
1456
+ const artifactProjectPath = (projectRoot, file) => {
1457
+ const projectPath = relative(projectRoot, resolve(file));
1458
+ if (projectPath === ".." || projectPath.startsWith(`..${sep}`) || isAbsolute(projectPath)) throw new WorkerArtifactError(`Ponderbot definition ${file} must be inside project ${projectRoot}`);
1459
+ return projectPath;
1460
+ };
1461
+ const definitionWrapper = (file) => {
1462
+ const specifier = JSON.stringify(file);
1463
+ return `export * from ${specifier};
1464
+ import * as loaded from ${specifier};
1465
+ export default loaded.default;
1466
+ `;
1467
+ };
1468
+ const workerMainWrapper = () => {
1469
+ const workerModule = fileURLToPath(import.meta.resolve("@ponderbot/sdk/worker"));
1470
+ return `import { runNodeWorkerMain } from ${JSON.stringify(workerModule)};
1471
+ await runNodeWorkerMain();
1472
+ `;
1473
+ };
1474
+ const bundleEntries = async (root, outdir, entrypoints, forcedExternal) => {
1475
+ const collector = workerArtifactDependencyCollector(forcedExternal);
1476
+ try {
1477
+ await build({
1478
+ entryPoints: entrypoints,
1479
+ outbase: root,
1480
+ outdir,
1481
+ bundle: true,
1482
+ splitting: true,
1483
+ packages: "bundle",
1484
+ platform: "node",
1485
+ target: "node22.4",
1486
+ format: "esm",
1487
+ sourcemap: false,
1488
+ minifySyntax: true,
1489
+ minifyWhitespace: true,
1490
+ keepNames: true,
1491
+ legalComments: "none",
1492
+ entryNames: "[dir]/[name]",
1493
+ chunkNames: "chunks/[name]-[hash]",
1494
+ assetNames: "assets/[name]-[hash]",
1495
+ plugins: [collector.plugin],
1496
+ logLevel: "silent"
1497
+ });
1498
+ return collector.dependencies();
1499
+ } catch (error) {
1500
+ throw new WorkerArtifactError(`Ponderbot Worker artifact bundle failed: ${error instanceof Error ? error.message : String(error)}`);
1501
+ }
1502
+ };
1503
+ const artifactContentHash = async (context) => {
1504
+ const files = [];
1505
+ const pending = [context];
1506
+ while (pending.length > 0) {
1507
+ const directory = pending.pop();
1508
+ if (directory === void 0) break;
1509
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1510
+ const path = join(directory, entry.name);
1511
+ if (entry.isDirectory()) pending.push(path);
1512
+ else if (entry.isFile()) files.push(path);
1513
+ }
1514
+ }
1515
+ const hash = createHash("sha256");
1516
+ for (const file of files.sort()) {
1517
+ hash.update(relative(context, file).split(sep).join("/"));
1518
+ hash.update("\0");
1519
+ hash.update(await readFile(file));
1520
+ hash.update("\0");
1521
+ }
1522
+ return `sha256:${hash.digest("hex")}`;
1523
+ };
1524
+ //#endregion
1525
+ //#region src/worker-image.ts
1526
+ const WORKER_IMAGE_TAG_ENV = "PONDERBOT_WORKER_IMAGE_TAG";
1527
+ const WORKER_IMAGE_TAG_TOKEN = "${PONDERBOT_WORKER_IMAGE_TAG}";
1528
+ const WORKER_IMAGE_TAG_ERROR = "PONDERBOT_WORKER_IMAGE_TAG must be a tag or sha256 digest; pass full refs via --image";
1529
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
1530
+ var WorkerImageError = class extends Error {
1531
+ constructor(message) {
1532
+ super(message);
1533
+ this.name = "WorkerImageError";
1534
+ }
1535
+ };
1536
+ async function deployManifestPlan(projectDir, options = {}) {
1537
+ const source = await loadSource(projectDir, { reload: true });
1538
+ const staticRelease = options.staticRelease === true && options.image === void 0;
1539
+ const explicit = staticRelease ? void 0 : configuredWorkerImage(source.project, options.image);
1540
+ const hasHandlers = hasWorkerEntryHandlers(source);
1541
+ const errors = checkSource(setWorkerImage(source, explicit), { requireWorkerImage: hasHandlers && explicit === void 0 && !staticRelease && source.project.worker?.build === void 0 });
1542
+ if (errors.length > 0) throw manifestBuildError(errors);
1543
+ const plan = await resolveBuiltWorkerImage(source, projectDir, explicit, options, staticRelease);
1544
+ const finalSource = setWorkerImage(source, plan.workerImage ?? explicit);
1545
+ const manifest = emitManifest(finalSource, {
1546
+ dev: staticRelease,
1547
+ workerImage: finalSource.project.workerImage
1548
+ });
1549
+ return {
1550
+ manifest: staticRelease ? staticProjectManifest(manifest) : manifest,
1551
+ workerImage: plan.workerImage,
1552
+ warnings: sourceWarnings(finalSource),
1553
+ instructionBudgets: agentInstructionBudgets(finalSource)
1554
+ };
1555
+ }
1556
+ async function resolveBuiltWorkerImage(source, projectDir, explicit, options, staticRelease) {
1557
+ if (!hasWorkerEntryHandlers(source)) return {};
1558
+ if (explicit !== void 0) return { workerImage: explicit };
1559
+ if (staticRelease) return {};
1560
+ const build = source.project.worker?.build;
1561
+ if (build === void 0) return {};
1562
+ return { workerImage: await buildAndPushWorkerImage(source, build, projectDir, options.platform) };
1563
+ }
1564
+ const setWorkerImage = (source, workerImage) => {
1565
+ return {
1566
+ ...source,
1567
+ project: {
1568
+ ...source.project,
1569
+ workerImage
1570
+ }
1571
+ };
1572
+ };
1573
+ function configuredWorkerImage(project, override) {
1574
+ const envValue = process.env[WORKER_IMAGE_TAG_ENV];
1575
+ return resolveWorkerImage(envValue === void 0 || envValue.length === 0 ? project.workerImage : project.workerImage ?? project.worker?.build?.image, override, envValue);
1576
+ }
1577
+ function resolveWorkerImage(configured, override, envTag) {
1578
+ if (override !== void 0) return override;
1579
+ const tagOrDigest = envTag === "" ? void 0 : envTag;
1580
+ if (tagOrDigest === void 0) return configured?.includes("${PONDERBOT_WORKER_IMAGE_TAG}") ? void 0 : configured;
1581
+ if (configured === void 0) return;
1582
+ if (DIGEST_PATTERN.test(tagOrDigest)) return `${repository(configured)}@${tagOrDigest}`;
1583
+ if (tagOrDigest.includes("/") || tagOrDigest.includes("@") || tagOrDigest.includes(":")) throw new WorkerImageError(WORKER_IMAGE_TAG_ERROR);
1584
+ if (configured.includes("${PONDERBOT_WORKER_IMAGE_TAG}")) return configured.split(WORKER_IMAGE_TAG_TOKEN).join(tagOrDigest);
1585
+ return `${repository(configured)}:${tagOrDigest}`;
1586
+ }
1587
+ async function buildAndPushWorkerImage(source, build, projectDir, platform) {
1588
+ const tag = build.image;
1589
+ const repo = repository(tag);
1590
+ const configuredContext = resolve(process.cwd(), build.context ?? ".");
1591
+ const metadataDirectory = await mkdtemp(join(tmpdir(), "ponderbot-worker-buildx-"));
1592
+ const metadataFile = join(metadataDirectory, "metadata.json");
1593
+ let artifact;
1594
+ try {
1595
+ let context = configuredContext;
1596
+ let dockerfile = resolve(process.cwd(), build.dockerfile ?? "Containerfile");
1597
+ if (build.dockerfile === void 0) {
1598
+ const resolvedContext = await realpath(configuredContext);
1599
+ projectDirectoryInContext(await realpath(projectDir), resolvedContext);
1600
+ try {
1601
+ artifact = await buildWorkerArtifact(source, projectDir, { external: build.external });
1602
+ } catch (error) {
1603
+ if (error instanceof WorkerArtifactError) throw new WorkerImageError(error.message);
1604
+ throw error;
1605
+ }
1606
+ context = artifact.context;
1607
+ dockerfile = artifact.dockerfile;
1608
+ }
1609
+ const cacheReference = `${repo}:buildcache`;
1610
+ const buildArgs = [
1611
+ "buildx",
1612
+ "build",
1613
+ "--file",
1614
+ dockerfile,
1615
+ "--tag",
1616
+ tag,
1617
+ "--push",
1618
+ "--provenance",
1619
+ "false",
1620
+ "--metadata-file",
1621
+ metadataFile,
1622
+ "--progress",
1623
+ "plain",
1624
+ "--cache-from",
1625
+ `type=registry,ref=${cacheReference}`,
1626
+ "--cache-to",
1627
+ `type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=${cacheReference}`,
1628
+ ...platform === void 0 ? [] : ["--platform", platform],
1629
+ ...Object.entries(build.args ?? {}).flatMap(([name, value]) => ["--build-arg", `${name}=${value}`]),
1630
+ context
1631
+ ];
1632
+ console.error(`building worker image ${tag}`);
1633
+ await runDocker(buildArgs, repo);
1634
+ const digest = await buildxDigest(metadataFile);
1635
+ if (digest === void 0) throw new WorkerImageError(`docker buildx did not report a registry digest for ${tag}`);
1636
+ const pinned = `${repo}@${digest}`;
1637
+ console.error(`pinned worker image ${pinned}`);
1638
+ return pinned;
1639
+ } finally {
1640
+ if (artifact !== void 0) await artifact.dispose();
1641
+ await rm(metadataDirectory, {
1642
+ recursive: true,
1643
+ force: true
1644
+ });
1645
+ }
1646
+ }
1647
+ const projectDirectoryInContext = (projectDir, context) => {
1648
+ const projectRelativeToContext = relative(context, projectDir);
1649
+ if (projectRelativeToContext === ".." || projectRelativeToContext.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(projectRelativeToContext)) throw new WorkerImageError(`Ponderbot project ${projectDir} must be inside Worker Image build context ${context}`);
1650
+ const dockerPath = process.platform === "win32" ? projectRelativeToContext.split("\\").join("/") : projectRelativeToContext;
1651
+ return dockerPath.length === 0 ? "." : `./${dockerPath}`;
1652
+ };
1653
+ async function runDocker(args, registryHint) {
1654
+ return new Promise((resolveRun, rejectRun) => {
1655
+ const child = spawn("docker", args, { stdio: [
1656
+ "ignore",
1657
+ "pipe",
1658
+ "pipe"
1659
+ ] });
1660
+ let stdout = "";
1661
+ let stderr = "";
1662
+ child.stdout.setEncoding("utf8");
1663
+ child.stderr.setEncoding("utf8");
1664
+ child.stdout.on("data", (chunk) => {
1665
+ stdout += chunk;
1666
+ process.stderr.write(chunk);
1667
+ });
1668
+ child.stderr.on("data", (chunk) => {
1669
+ stderr += chunk;
1670
+ process.stderr.write(chunk);
1671
+ });
1672
+ child.on("error", (error) => {
1673
+ if (error.code === "ENOENT") {
1674
+ rejectRun(new WorkerImageError("docker not found; install Docker and make sure docker is on PATH"));
1675
+ return;
1676
+ }
1677
+ rejectRun(error);
1678
+ });
1679
+ child.on("exit", (code) => {
1680
+ if (code === 0) {
1681
+ resolveRun({
1682
+ stdout,
1683
+ stderr
1684
+ });
1685
+ return;
1686
+ }
1687
+ rejectRun(new WorkerImageError(`docker buildx build failed with exit code ${code}; run docker login for ${registryHint} if the registry requires authentication`));
1688
+ });
1689
+ });
1690
+ }
1691
+ const buildxDigest = async (metadataFile) => {
1692
+ try {
1693
+ const digest = JSON.parse(await readFile(metadataFile, "utf8"))["containerimage.digest"];
1694
+ return typeof digest === "string" && DIGEST_PATTERN.test(digest) ? digest : void 0;
1695
+ } catch {
1696
+ return;
1697
+ }
1698
+ };
1699
+ const repository = (image) => {
1700
+ const withoutDigest = image.split("@", 1)[0] ?? image;
1701
+ const slash = withoutDigest.lastIndexOf("/");
1702
+ const colon = withoutDigest.lastIndexOf(":");
1703
+ if (colon > slash) return withoutDigest.slice(0, colon);
1704
+ return withoutDigest;
1705
+ };
1706
+ //#endregion
1707
+ //#region src/model-policy.ts
1708
+ const modelDefinitionInput = (options) => ({
1709
+ provider: modelProvider(options.provider),
1710
+ model: options.model,
1711
+ capabilities: Object.fromEntries((options.capability ?? []).map((capability) => [capability, true])),
1712
+ fallback_model_refs: options.fallback ?? []
1713
+ });
1714
+ const modelProvider = (provider) => {
1715
+ switch (provider) {
1716
+ case "anthropic":
1717
+ case "fake":
1718
+ case "mistral":
1719
+ case "openai": return provider;
1720
+ default: throw new Error("--provider must be anthropic, fake, mistral, or openai");
1721
+ }
1722
+ };
1723
+ //#endregion
1724
+ //#region src/provider-auth.ts
1725
+ const providerAuth = async (options) => {
1726
+ const config = await resolveConfig(options.env);
1727
+ const spaceId = options.space ?? config.spaceId;
1728
+ const client = clientFor(config);
1729
+ const registry = await client.modelProviders.list(spaceId);
1730
+ const provider = await resolveProvider(options.provider, registry.providers);
1731
+ const providerEntry = registry.providers.find((entry) => entry.id === provider);
1732
+ if (providerEntry === void 0) throw new Error(`provider ${provider} is not available`);
1733
+ const method = await resolveMethod(providerEntry, options.method);
1734
+ if (options.allSpaces === true && options.shared !== true) throw new Error("--all-spaces requires --shared");
1735
+ if (options.spaces !== void 0 && options.shared !== true) throw new Error("--spaces requires --shared");
1736
+ if (options.allSpaces === true && options.spaces !== void 0) throw new Error("use either --all-spaces or --spaces, not both");
1737
+ const sharedSpaceIds = options.spaces ?? [spaceId];
1738
+ if (options.shared === true) {
1739
+ const { binding } = await client.providerBindings.create(provider);
1740
+ if (method === "api-key") {
1741
+ const apiKey = await readApiKey(options);
1742
+ await client.providerBindings.setApiKey(binding.id, { apiKey });
1743
+ } else {
1744
+ if (!canPrompt()) throw new Error("OAuth login needs an interactive terminal. Run locally with a browser, or rerun with --no-browser to paste the callback URL.");
1745
+ await authorizeOAuthLogin(() => client.providerBindings.startOAuthLogin(binding.id), (authorizationResponse, state) => client.providerBindings.completeOAuthLogin(binding.id, {
1746
+ authorizationResponse,
1747
+ state
1748
+ }), options.browser !== false);
1749
+ }
1750
+ if (options.allSpaces === true) await client.providerBindings.authorize(binding.id, { allSpaces: true });
1751
+ else await client.providerBindings.authorize(binding.id, { spaceIds: sharedSpaceIds });
1752
+ return providerAuthResult(provider, method, spaceId, {
1753
+ bindingId: binding.id,
1754
+ allSpaces: options.allSpaces === true,
1755
+ spaceIds: options.allSpaces === true ? void 0 : sharedSpaceIds
1756
+ });
1757
+ }
1758
+ if (method === "api-key") {
1759
+ const apiKey = await readApiKey(options);
1760
+ await client.modelProviders.setApiKey(spaceId, {
1761
+ provider,
1762
+ apiKey
1763
+ });
1764
+ return providerAuthResult(provider, method, spaceId);
1765
+ }
1766
+ if (!canPrompt()) throw new Error("OAuth login needs an interactive terminal. Run locally with a browser, or rerun with --no-browser to paste the callback URL.");
1767
+ await authorizeOAuthLogin(() => client.modelProviders.startOAuthLogin(spaceId, provider), (authorizationResponse, state) => client.modelProviders.completeOAuthLogin(spaceId, provider, {
1768
+ authorizationResponse,
1769
+ state
1770
+ }), options.browser !== false);
1771
+ return providerAuthResult(provider, method, spaceId);
1772
+ };
1773
+ const providerAuthResult = (provider, method, spaceId, shared) => ({
1774
+ type: "provider_auth",
1775
+ status: "saved",
1776
+ provider,
1777
+ method,
1778
+ space_id: spaceId,
1779
+ binding_id: shared?.bindingId,
1780
+ all_spaces: shared?.allSpaces,
1781
+ space_ids: shared?.spaceIds,
1782
+ smoke_test: "ponder spaces show"
1783
+ });
1784
+ const resolveProvider = async (value, entries) => {
1785
+ const providers = entries.map((entry) => entry.id);
1786
+ if (value !== void 0) {
1787
+ const provider = providers.find((candidate) => candidate === value);
1788
+ if (provider !== void 0) return provider;
1789
+ throw new Error(`--provider must be one of ${providers.join(", ")}`);
1790
+ }
1791
+ return promptChoice("Provider", providers);
1792
+ };
1793
+ const resolveMethod = async (provider, value) => {
1794
+ const supportsApiKey = provider.auth_methods.some((method) => method.id === "api_key");
1795
+ const supportsOAuth = provider.auth_methods.some((method) => method.id !== "api_key");
1796
+ if (value !== void 0) {
1797
+ if (value === "api-key" && supportsApiKey) return "api-key";
1798
+ if ((value === "oauth" || value === "codex") && supportsOAuth) return "oauth";
1799
+ throw new Error(`--method is not supported by --provider ${provider.id}`);
1800
+ }
1801
+ if (!supportsOAuth) return "api-key";
1802
+ if (!supportsApiKey) return "oauth";
1803
+ return promptChoice("Authentication method", ["api-key", "oauth"]);
1804
+ };
1805
+ const readApiKey = async (options) => {
1806
+ const envValue = process.env.PONDER_PROVIDER_API_KEY;
1807
+ if (envValue !== void 0 && envValue !== "") return envValue;
1808
+ if (!process.stdin.isTTY) {
1809
+ const piped = (await readStdin()).trim();
1810
+ if (piped !== "") return piped;
1811
+ throw new Error("set PONDER_PROVIDER_API_KEY or pipe the API key on stdin");
1812
+ }
1813
+ if (options.provider !== void 0 && options.method !== void 0 && !canPrompt()) throw new Error("set PONDER_PROVIDER_API_KEY or pipe the API key on stdin");
1814
+ return promptSecret("API key");
1815
+ };
1816
+ const authorizeOAuthLogin = async (start, complete, openBrowser) => {
1817
+ const { login } = await start();
1818
+ const authorization = {
1819
+ authUrl: login.authUrl,
1820
+ callbackUrl: login.callbackUrl,
1821
+ state: login.state
1822
+ };
1823
+ let authorizationResponse;
1824
+ if (openBrowser) {
1825
+ const callback = listenForCallback(authorization);
1826
+ console.error(`Open this URL if your browser does not open automatically:\n${authorization.authUrl}`);
1827
+ if (await loopbackReady(callback) && await openSystemBrowser(authorization.authUrl)) authorizationResponse = await callback.code;
1828
+ else {
1829
+ callback.close();
1830
+ console.error(`Open this URL and paste the redirected callback URL:\n${authorization.authUrl}`);
1831
+ authorizationResponse = await promptLine("Callback URL or code");
1832
+ }
1833
+ } else {
1834
+ console.error(`Open this URL and paste the redirected callback URL:\n${authorization.authUrl}`);
1835
+ authorizationResponse = await promptLine("Callback URL or code");
1836
+ }
1837
+ await complete(authorizationResponse, authorization.state);
1838
+ };
1839
+ const listenForCallback = (authorization) => {
1840
+ const callback = new URL(authorization.callbackUrl);
1841
+ const server = createServer((request, response) => {
1842
+ try {
1843
+ const code = authorizationCodeFromInput(new URL(request.url ?? "/", callback).toString(), authorization.state);
1844
+ response.writeHead(200, { "content-type": "text/html" });
1845
+ response.end("<html><body>OpenAI login complete. You can close this window.</body></html>");
1846
+ server.close();
1847
+ resolve(code);
1848
+ } catch (error) {
1849
+ response.writeHead(400, { "content-type": "text/plain" });
1850
+ response.end(error instanceof Error ? error.message : "invalid callback");
1851
+ reject(error);
1852
+ }
1853
+ });
1854
+ let resolve;
1855
+ let reject;
1856
+ let readyResolve;
1857
+ let readyReject;
1858
+ const code = new Promise((promiseResolve, promiseReject) => {
1859
+ resolve = promiseResolve;
1860
+ reject = promiseReject;
1861
+ });
1862
+ const ready = new Promise((promiseResolve, promiseReject) => {
1863
+ readyResolve = promiseResolve;
1864
+ readyReject = promiseReject;
1865
+ });
1866
+ server.once("listening", readyResolve);
1867
+ server.once("error", (error) => {
1868
+ const normalized = error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error("port 1455 is already in use") : error;
1869
+ readyReject(normalized);
1870
+ reject(normalized);
1871
+ });
1872
+ server.listen(Number(callback.port), callback.hostname);
1873
+ return {
1874
+ code,
1875
+ ready,
1876
+ close: () => server.close()
1877
+ };
1878
+ };
1879
+ const loopbackReady = async (callback) => {
1880
+ try {
1881
+ await callback.ready;
1882
+ return true;
1883
+ } catch (error) {
1884
+ console.error(error instanceof Error ? `${error.message}; falling back to paste mode` : "falling back to paste mode");
1885
+ callback.close();
1886
+ return false;
1887
+ }
1888
+ };
1889
+ const authorizationCodeFromInput = (inputValue, expectedState) => {
1890
+ const trimmed = inputValue.trim();
1891
+ if (trimmed === "") throw new Error("missing authorization code");
1892
+ const params = trimmed.includes("://") ? new URL(trimmed).searchParams : new URLSearchParams(trimmed.startsWith("?") ? trimmed.slice(1) : trimmed);
1893
+ const returnedState = params.get("state");
1894
+ const code = params.get("code") ?? (trimmed.includes("=") ? null : trimmed);
1895
+ if (returnedState !== null && returnedState !== expectedState) throw new Error("OAuth state did not match");
1896
+ if (code === null || code === "") throw new Error("missing authorization code");
1897
+ return code;
1898
+ };
1899
+ const promptChoice = async (label, choices) => {
1900
+ const answer = await promptLine(`${label}\n${choices.map((choice, index) => `${index + 1}) ${choice}`).join("\n")}\nChoose`);
1901
+ const index = Number(answer.trim());
1902
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1];
1903
+ if (choices.includes(answer.trim())) return answer.trim();
1904
+ throw new Error(`invalid ${label.toLowerCase()}`);
1905
+ };
1906
+ const promptLine = async (label) => {
1907
+ const rl = createInterface({
1908
+ input: stdin,
1909
+ output: stderr
1910
+ });
1911
+ try {
1912
+ return await rl.question(`${label}: `);
1913
+ } finally {
1914
+ rl.close();
1915
+ }
1916
+ };
1917
+ const promptSecret = async (label) => {
1918
+ process.stderr.write(`${label}: `);
1919
+ const chunks = [];
1920
+ const stdin = process.stdin;
1921
+ const wasRaw = stdin.isRaw;
1922
+ stdin.setRawMode?.(true);
1923
+ stdin.resume();
1924
+ return await new Promise((resolve, reject) => {
1925
+ const cleanup = () => {
1926
+ stdin.setRawMode?.(wasRaw);
1927
+ stdin.pause();
1928
+ stdin.off("data", onData);
1929
+ process.stderr.write("\n");
1930
+ };
1931
+ const onData = (chunk) => {
1932
+ const value = chunk.toString("utf8");
1933
+ if (value === "") {
1934
+ cleanup();
1935
+ reject(/* @__PURE__ */ new Error("cancelled"));
1936
+ return;
1937
+ }
1938
+ if (value === "\r" || value === "\n") {
1939
+ cleanup();
1940
+ resolve(chunks.join(""));
1941
+ return;
1942
+ }
1943
+ if (value === "") {
1944
+ chunks.pop();
1945
+ return;
1946
+ }
1947
+ chunks.push(value);
1948
+ };
1949
+ stdin.on("data", onData);
1950
+ });
1951
+ };
1952
+ const openSystemBrowser = (url) => {
1953
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
1954
+ const args = process.platform === "win32" ? [
1955
+ "/c",
1956
+ "start",
1957
+ "",
1958
+ url
1959
+ ] : [url];
1960
+ return new Promise((resolve) => {
1961
+ const child = spawn(command, args, {
1962
+ stdio: "ignore",
1963
+ detached: true
1964
+ });
1965
+ child.once("error", () => resolve(false));
1966
+ child.once("spawn", () => {
1967
+ child.unref();
1968
+ resolve(true);
1969
+ });
1970
+ });
1971
+ };
1972
+ const readStdin = async () => {
1973
+ const chunks = [];
1974
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
1975
+ return Buffer.concat(chunks).toString("utf8");
1976
+ };
1977
+ const canPrompt = () => {
1978
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
1979
+ };
1980
+ //#endregion
1981
+ //#region src/spaces.ts
1982
+ const spacePageLimit = 100;
1983
+ const listSpaces = async (client) => {
1984
+ const spaces = [];
1985
+ let cursor;
1986
+ do {
1987
+ const page = await client.spaces.list({
1988
+ cursor,
1989
+ limit: spacePageLimit
1990
+ });
1991
+ spaces.push(...page.spaces);
1992
+ cursor = page.next_cursor ?? void 0;
1993
+ } while (cursor !== void 0);
1994
+ return spaces;
1995
+ };
1996
+ //#endregion
1997
+ //#region src/stack.ts
1998
+ const DEFAULT_TARGET = "core";
1999
+ const DEFAULT_DEV_RUNTIME_IMAGE = "ponderbot-runtime:dev";
2000
+ const DEFAULT_DEV_SPACE_ID = "default";
2001
+ const DEFAULT_DEV_URL = "http://127.0.0.1:4100";
2002
+ const DEV_BOOTSTRAP_TOKEN_FILE = "dev-bootstrap-token";
2003
+ const OUTPUT_TAIL_LIMIT = 16384;
2004
+ const WAIT_TIMEOUT_SECONDS = "180";
2005
+ var StackError = class extends Error {
2006
+ constructor(message) {
2007
+ super(message);
2008
+ this.name = "StackError";
2009
+ }
2010
+ };
2011
+ async function findRepoRoot(start = process.cwd()) {
2012
+ let current = resolve(start);
2013
+ while (true) {
2014
+ try {
2015
+ await access(join(current, "docker", "compose", "targets"));
2016
+ return current;
2017
+ } catch (error) {
2018
+ if (error.code !== "ENOENT") throw error;
2019
+ }
2020
+ const parent = dirname(current);
2021
+ if (parent === current) throw new StackError(`could not find docker/compose/targets from ${resolve(start)}`);
2022
+ current = parent;
2023
+ }
2024
+ }
2025
+ async function startStack(options = {}) {
2026
+ const stack = await resolveStack(options.target, options.dev);
2027
+ const bootstrapToken = await bootstrapTokenForHome(stack.ponderbotHome);
2028
+ const devEnvironment = devStackEnvironment(stack, options.space, bootstrapToken);
2029
+ await runDockerCompose([
2030
+ ...composeArgs(stack),
2031
+ "up",
2032
+ ...options.dev ? ["--watch"] : [],
2033
+ ...options.attach || options.dev ? [] : [
2034
+ "-d",
2035
+ "--wait",
2036
+ "--wait-timeout",
2037
+ WAIT_TIMEOUT_SECONDS
2038
+ ],
2039
+ ...options.build ? ["--build"] : []
2040
+ ], stack, bootstrapToken);
2041
+ await ensureDevSpace(devEnvironment);
2042
+ return result(stack, "started", devEnvironment);
2043
+ }
2044
+ async function stopStack(options = {}) {
2045
+ const stack = await resolveStack(options.target, false);
2046
+ await runDockerCompose([
2047
+ ...composeArgs(stack),
2048
+ "down",
2049
+ "--remove-orphans"
2050
+ ], stack);
2051
+ return result(stack, "stopped");
2052
+ }
2053
+ async function resolveStack(target, dev) {
2054
+ const repoRoot = await findRepoRoot();
2055
+ const resolvedTarget = targetName(target);
2056
+ const baseImagesEnvFile = await baseImagesEnvFileForRepo(repoRoot);
2057
+ const environmentFile = await environmentFileForRepo(repoRoot);
2058
+ const composeFile = await composeFileForRepo(repoRoot, resolvedTarget);
2059
+ const devComposeFile = dev ? await devComposeFileForRepo(repoRoot) : void 0;
2060
+ const ponderbotHome = await ensurePonderbotHome(repoRoot);
2061
+ const configuredUrl = process.env.PONDERBOT_URL;
2062
+ return {
2063
+ baseImagesEnvFile,
2064
+ composeFile,
2065
+ devComposeFile,
2066
+ devUrl: configuredUrl === void 0 || configuredUrl.length === 0 ? DEFAULT_DEV_URL : configuredUrl,
2067
+ environmentFile,
2068
+ ponderbotHome,
2069
+ repoRoot,
2070
+ spaceId: DEFAULT_DEV_SPACE_ID,
2071
+ target: resolvedTarget
2072
+ };
2073
+ }
2074
+ const composeArgs = (stack) => {
2075
+ return [
2076
+ "compose",
2077
+ "--env-file",
2078
+ stack.baseImagesEnvFile,
2079
+ ...stack.environmentFile === void 0 ? [] : ["--env-file", stack.environmentFile],
2080
+ "-f",
2081
+ stack.composeFile,
2082
+ ...stack.devComposeFile === void 0 ? [] : ["-f", stack.devComposeFile]
2083
+ ];
2084
+ };
2085
+ const environmentFileForRepo = async (repoRoot) => {
2086
+ const environmentFile = join(repoRoot, ".env");
2087
+ try {
2088
+ await access(environmentFile);
2089
+ return environmentFile;
2090
+ } catch (error) {
2091
+ if (error.code === "ENOENT") return;
2092
+ throw error;
2093
+ }
2094
+ };
2095
+ async function baseImagesEnvFileForRepo(repoRoot) {
2096
+ const envFile = join(repoRoot, "docker", "env", "base-images.env");
2097
+ try {
2098
+ await access(envFile);
2099
+ } catch (error) {
2100
+ if (error.code === "ENOENT") throw new StackError(`base image env file not found at ${envFile}`);
2101
+ throw error;
2102
+ }
2103
+ return envFile;
2104
+ }
2105
+ async function composeFileForRepo(repoRoot, target) {
2106
+ if (target === "dev") throw new StackError("compose target \"dev\" is an overlay; use ponder start --dev");
2107
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(target)) throw new StackError(`invalid compose target "${target}"`);
2108
+ const composeFile = join(repoRoot, "docker", "compose", "targets", `${target}.yml`);
2109
+ try {
2110
+ await access(composeFile);
2111
+ } catch (error) {
2112
+ if (error.code === "ENOENT") throw new StackError(`compose target "${target}" not found at ${composeFile}`);
2113
+ throw error;
2114
+ }
2115
+ return composeFile;
2116
+ }
2117
+ async function devComposeFileForRepo(repoRoot) {
2118
+ const devComposeFile = join(repoRoot, "docker", "compose", "targets", "dev.yml");
2119
+ try {
2120
+ await access(devComposeFile);
2121
+ } catch (error) {
2122
+ if (error.code === "ENOENT") throw new StackError(`dev compose overlay not found at ${devComposeFile}`);
2123
+ throw error;
2124
+ }
2125
+ return devComposeFile;
2126
+ }
2127
+ async function ensurePonderbotHome(repoRoot) {
2128
+ const configured = process.env.PONDERBOT_HOME;
2129
+ const ponderbotHome = configured === void 0 || configured.length === 0 ? join(repoRoot, ".ponderbot") : configured;
2130
+ await mkdir(ponderbotHome, { recursive: true });
2131
+ return ponderbotHome;
2132
+ }
2133
+ const bootstrapTokenForHome = async (ponderbotHome) => {
2134
+ const configured = process.env.PONDERBOT_BOOTSTRAP_TOKEN;
2135
+ if (configured !== void 0 && configured.trim().length > 0) return configured;
2136
+ const tokenPath = join(ponderbotHome, DEV_BOOTSTRAP_TOKEN_FILE);
2137
+ try {
2138
+ const token = (await readFile(tokenPath, "utf8")).trim();
2139
+ if (token.length > 0) return token;
2140
+ } catch (error) {
2141
+ if (error.code !== "ENOENT") throw error;
2142
+ }
2143
+ const token = `pb_dev_${randomBytes(32).toString("base64url")}`;
2144
+ await writeFile(tokenPath, `${token}\n`, { mode: 384 });
2145
+ await chmod(tokenPath, 384);
2146
+ return token;
2147
+ };
2148
+ const devStackEnvironment = (stack, space, token) => ({
2149
+ url: stack.devUrl,
2150
+ space_id: space === void 0 || space.length === 0 ? stack.spaceId : space,
2151
+ token
2152
+ });
2153
+ const ensureDevSpace = async (environment) => {
2154
+ const client = createClient({
2155
+ baseUrl: environment.url,
2156
+ spaceId: environment.space_id,
2157
+ token: environment.token
2158
+ });
2159
+ const space = (await listSpaces(client)).find((space) => space.id === environment.space_id);
2160
+ if (space === void 0) {
2161
+ await client.spaces.create({
2162
+ id: environment.space_id,
2163
+ name: environment.space_id
2164
+ });
2165
+ return;
2166
+ }
2167
+ if (space.status !== "active") await client.spaces.update({ status: "active" }, environment.space_id);
2168
+ };
2169
+ async function runDockerCompose(args, stack, bootstrapToken) {
2170
+ await new Promise((resolveRun, rejectRun) => {
2171
+ const child = spawn("docker", args, {
2172
+ cwd: stack.repoRoot,
2173
+ env: {
2174
+ ...process.env,
2175
+ ...stack.devComposeFile === void 0 ? {} : { PONDERBOT_RUNTIME_IMAGE: process.env.PONDERBOT_RUNTIME_IMAGE || DEFAULT_DEV_RUNTIME_IMAGE },
2176
+ ...bootstrapToken === void 0 ? {} : { PONDERBOT_BOOTSTRAP_TOKEN: bootstrapToken },
2177
+ PONDERBOT_DATA_MOUNT_SOURCE: stack.ponderbotHome,
2178
+ PONDERBOT_HOME: stack.ponderbotHome
2179
+ },
2180
+ stdio: [
2181
+ "ignore",
2182
+ "pipe",
2183
+ "pipe"
2184
+ ]
2185
+ });
2186
+ let outputTail = "";
2187
+ child.stdout.setEncoding("utf8");
2188
+ child.stderr.setEncoding("utf8");
2189
+ child.stdout.on("data", (chunk) => {
2190
+ outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT);
2191
+ process.stderr.write(chunk);
2192
+ });
2193
+ child.stderr.on("data", (chunk) => {
2194
+ outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT);
2195
+ process.stderr.write(chunk);
2196
+ });
2197
+ child.on("error", (error) => {
2198
+ if (error.code === "ENOENT") {
2199
+ rejectRun(new StackError("docker not found; install Docker and make sure docker is on PATH"));
2200
+ return;
2201
+ }
2202
+ rejectRun(error);
2203
+ });
2204
+ child.on("close", (code) => {
2205
+ if (code === 0) {
2206
+ resolveRun();
2207
+ return;
2208
+ }
2209
+ rejectRun(new StackError(composeFailureMessage(args, code, outputTail)));
2210
+ });
2211
+ });
2212
+ }
2213
+ const composeFailureMessage = (args, code, output) => {
2214
+ if (/Cannot connect to the Docker daemon|Is the docker daemon running|docker daemon is not running/i.test(output)) return "docker daemon unavailable; start Docker and try again";
2215
+ return `docker compose ${args.includes("up") ? "up" : args.includes("down") ? "down" : "compose"} failed with exit code ${code ?? "unknown"}`;
2216
+ };
2217
+ function result(stack, status, devEnvironment) {
2218
+ const base = {
2219
+ type: "stack",
2220
+ id: stack.target,
2221
+ status,
2222
+ compose_file: stack.composeFile,
2223
+ ponderbot_home: stack.ponderbotHome
2224
+ };
2225
+ return devEnvironment === void 0 ? base : {
2226
+ ...base,
2227
+ dev_environment: devEnvironment
2228
+ };
2229
+ }
2230
+ const targetName = (target) => {
2231
+ return target === void 0 || target.length === 0 ? DEFAULT_TARGET : target;
2232
+ };
2233
+ //#endregion
2234
+ //#region src/version-bump.ts
2235
+ const VERSION_LITERAL_PATTERN = /\bversion:\s*"([^"\r\n]+)"/g;
2236
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
2237
+ const bumpProjectVersion = async (projectDir, currentVersion, level) => {
2238
+ const bumpLevel = parseBumpLevel(level);
2239
+ const file = join(projectDir, "config.ts");
2240
+ const source = await readFile(file, "utf8");
2241
+ const matches = [...source.matchAll(VERSION_LITERAL_PATTERN)];
2242
+ if (matches.length !== 1) throw manualBumpError(file);
2243
+ const match = matches[0];
2244
+ const literalVersion = match?.[1];
2245
+ if (match?.index === void 0 || literalVersion === void 0 || currentVersion === void 0 || literalVersion !== currentVersion) throw manualBumpError(file);
2246
+ const nextVersion = bumpedVersion(currentVersion, bumpLevel);
2247
+ const replacement = match[0].replace(`"${literalVersion}"`, `"${nextVersion}"`);
2248
+ await writeFile(file, `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`);
2249
+ return {
2250
+ previousVersion: currentVersion,
2251
+ nextVersion
2252
+ };
2253
+ };
2254
+ const parseBumpLevel = (level) => {
2255
+ switch (level) {
2256
+ case "patch":
2257
+ case "minor":
2258
+ case "major": return level;
2259
+ default: throw new Error("--bump must be patch, minor, or major");
2260
+ }
2261
+ };
2262
+ const bumpedVersion = (version, level) => {
2263
+ const match = SEMVER_PATTERN.exec(version);
2264
+ if (match === null) throw new Error(`project version ${JSON.stringify(version)} is not x.y.z semver; bump the version manually`);
2265
+ const major = Number(match[1]);
2266
+ const minor = Number(match[2]);
2267
+ const patch = Number(match[3]);
2268
+ switch (level) {
2269
+ case "patch": return `${major}.${minor}.${patch + 1}`;
2270
+ case "minor": return `${major}.${minor + 1}.0`;
2271
+ case "major": return `${major + 1}.0.0`;
2272
+ }
2273
+ };
2274
+ const manualBumpError = (file) => /* @__PURE__ */ new Error(`${file} does not contain exactly one version string literal; bump the version manually`);
2275
+ //#endregion
2276
+ //#region src/main.ts
2277
+ const program = new Command("ponder").option("--json", "machine output").showHelpAfterError();
2278
+ program.command("start").option("--target <name>", "compose target", "core").option("--attach", "run compose in the foreground and stream logs until stopped").option("--build", "build compose images before starting").option("--dev", "run the runtime with Docker Compose Watch and Phoenix code reloading").option("--space <id>", "space id to create and print for local app credentials").description("start the local Docker compose stack").action(async (options) => {
2279
+ try {
2280
+ print(await startStack({
2281
+ attach: options.attach,
2282
+ build: options.build,
2283
+ dev: options.dev,
2284
+ space: options.space,
2285
+ target: options.target
2286
+ }), program.opts());
2287
+ } catch (error) {
2288
+ fail(error, error instanceof StackError ? 2 : 1);
2289
+ }
2290
+ });
2291
+ program.command("stop").option("--target <name>", "compose target", "core").description("stop the local Docker compose stack").action(async (options) => {
2292
+ try {
2293
+ print(await stopStack(options), program.opts());
2294
+ } catch (error) {
2295
+ fail(error, error instanceof StackError ? 2 : 1);
2296
+ }
2297
+ });
2298
+ program.command("init").description("scaffold ponderbot/config.ts").action(async () => {
2299
+ await scaffold();
2300
+ print({
2301
+ ok: true,
2302
+ project: "ponderbot"
2303
+ }, program.opts());
2304
+ });
2305
+ const manifest = program.command("manifest").description("author, validate, and deploy the manifest");
2306
+ manifest.command("validate").description("build the manifest and report structural errors").action(async () => {
2307
+ try {
2308
+ const config = await resolveProject();
2309
+ const { manifest, warnings, instructionBudgets } = await buildManifest(config.project, {
2310
+ workerImage: configuredWorkerImage(config.projectDefinition),
2311
+ requireWorkerImage: false
2312
+ });
2313
+ printManifestDiagnostics(instructionBudgets, warnings);
2314
+ print({
2315
+ ok: true,
2316
+ counts: countByKind(manifest)
2317
+ }, program.opts());
2318
+ } catch (error) {
2319
+ fail(error, error instanceof WorkerImageError ? 2 : 1);
2320
+ }
2321
+ });
2322
+ manifest.command("deploy").option("--env <name>", "environment").option("--image <ref>", "prebuilt worker image ref").option("--platform <platform>", "docker build platform").option("--bump <patch|minor|major>", "bump ponderbot/config.ts version before deploy").description("build and deploy the manifest").action(async (options) => {
2323
+ const config = await resolveProjectReleaseConfig(options.env);
2324
+ const client = clientFor(config);
2325
+ try {
2326
+ const versionBump = options.bump === void 0 ? void 0 : await bumpProjectVersion(config.project, config.projectDefinition.version, options.bump);
2327
+ const plan = await deployManifestPlan(config.project, {
2328
+ ...options,
2329
+ staticRelease: options.image === void 0 && config.allowDev
2330
+ });
2331
+ printManifestDiagnostics(plan.instructionBudgets, plan.warnings);
2332
+ const release = await client.projects.releases.deploy(config.projectDefinition.name, config.environment, plan.manifest, config.sourceName, void 0, config.applicationOrigin);
2333
+ print(versionBump === void 0 ? release : {
2334
+ ...release,
2335
+ version_bump: versionBump
2336
+ }, program.opts());
2337
+ } catch (error) {
2338
+ fail(error, error instanceof WorkerImageError ? 2 : 1);
2339
+ }
2340
+ });
2341
+ manifest.command("dev").option("--env <name>", "environment").description("watch, deploy to a dev environment, and follow runtime events").action(async (options) => {
2342
+ try {
2343
+ await devLoop(await resolveDevConfig(options.env));
2344
+ } catch (error) {
2345
+ fail(error, 1);
2346
+ }
2347
+ });
2348
+ const spaces = program.command("spaces").description("manage runtime spaces");
2349
+ spaces.command("list").option("--env <name>", "environment").description("list spaces").action(async (options) => {
2350
+ const config = await resolveConfig(options.env);
2351
+ await runSpacesCommand("spaces:read", "*", async () => {
2352
+ print({ spaces: await listSpaces(clientFor(config)) }, program.opts());
2353
+ });
2354
+ });
2355
+ spaces.command("create <spaceId>").option("--name <name>", "space name").option("--env <name>", "environment").description("create a space").action(async (spaceId, options) => {
2356
+ const config = await resolveConfig(options.env);
2357
+ await runSpacesCommand("spaces:write", "*", async () => {
2358
+ try {
2359
+ print(await clientFor(config).spaces.create({
2360
+ id: spaceId,
2361
+ name: options.name ?? spaceId
2362
+ }), program.opts());
2363
+ } catch (error) {
2364
+ throw spaceCreateError(error, spaceId);
2365
+ }
2366
+ });
2367
+ });
2368
+ spaces.command("show [spaceId]").option("--env <name>", "environment").description("show a space with budget and usage").action(async (spaceId, options) => {
2369
+ const config = await resolveConfig(options.env);
2370
+ const client = clientFor(config);
2371
+ const resolvedSpaceId = spaceId ?? config.spaceId;
2372
+ await runSpacesCommand("spaces:read", resolvedSpaceId, async () => {
2373
+ const [space, budget, usage] = await Promise.all([
2374
+ client.spaces.get(resolvedSpaceId),
2375
+ client.spaces.budget.get(resolvedSpaceId),
2376
+ client.spaces.usage(resolvedSpaceId)
2377
+ ]);
2378
+ print({
2379
+ space: space.space,
2380
+ budget: budget.budget,
2381
+ usage
2382
+ }, program.opts());
2383
+ });
2384
+ });
2385
+ spaces.command("budget [spaceId]").option("--set <file>", "replace budget from a JSON file").option("--env <name>", "environment").description("show or replace a space budget").action(async (spaceId, options) => {
2386
+ const config = await resolveConfig(options.env);
2387
+ const client = clientFor(config);
2388
+ const resolvedSpaceId = spaceId ?? config.spaceId;
2389
+ await runSpacesCommand(options.set === void 0 ? "spaces:read" : "spaces:write", resolvedSpaceId, async () => {
2390
+ if (options.set === void 0) {
2391
+ print(await client.spaces.budget.get(resolvedSpaceId), program.opts());
2392
+ return;
2393
+ }
2394
+ print(await client.spaces.budget.set(await readJsonFile(options.set), resolvedSpaceId), program.opts());
2395
+ });
2396
+ });
2397
+ program.command("provider").description("configure model provider credentials").command("auth").description("authenticate a model provider for a space").option("--env <name>", "environment").option("--space <spaceId>", "target space (defaults to the configured space)").option("--provider <name>", "anthropic | mistral | openai (skips the picker)").option("--method <method>", "api-key | oauth").option("--shared", "store one Runtime credential binding instead of a Space credential").option("--all-spaces", "authorize the shared binding for every Space").option("--spaces <spaceIds...>", "authorize the shared binding for selected Spaces").option("--no-browser", "print the Codex authorize URL and paste the callback URL").action(async (options) => {
2398
+ print(await providerAuth(options), program.opts());
2399
+ });
2400
+ const model = program.command("model").description("manage Space model policy");
2401
+ model.command("add <modelRef>").requiredOption("--provider <name>", "anthropic | fake | mistral | openai").requiredOption("--model <name>", "provider model name").option("--capability <name...>", "declared model capabilities").option("--fallback <modelRef...>", "ordered fallback model refs").option("--space <spaceId>", "target space (defaults to the configured space)").option("--env <name>", "environment").description("add or update a model in the Space catalog").action(async (modelRef, options) => {
2402
+ const config = await resolveConfig(options.env);
2403
+ const spaceId = options.space ?? config.spaceId;
2404
+ await runSpacesCommand("spaces:write", spaceId, async () => {
2405
+ print(await clientFor(config).models.upsert(modelRef, modelDefinitionInput(options), spaceId), program.opts());
2406
+ });
2407
+ });
2408
+ model.command("list").option("--space <spaceId>", "target space (defaults to the configured space)").option("--env <name>", "environment").description("list models in the Space catalog").action(async (options) => {
2409
+ const config = await resolveConfig(options.env);
2410
+ const spaceId = options.space ?? config.spaceId;
2411
+ await runSpacesCommand("spaces:read", spaceId, async () => {
2412
+ print(await clientFor(config).models.list(spaceId), program.opts());
2413
+ });
2414
+ });
2415
+ model.command("assign <agentRef> <modelRef>").option("--space <spaceId>", "target space (defaults to the configured space)").option("--env <name>", "environment").description("assign a Space catalog model to an exact Agent ref").action(async (agentRef, modelRef, options) => {
2416
+ const config = await resolveConfig(options.env);
2417
+ const spaceId = options.space ?? config.spaceId;
2418
+ await runSpacesCommand("spaces:write", spaceId, async () => {
2419
+ print(await clientFor(config).models.assign(agentRef, modelRef, spaceId), program.opts());
2420
+ });
2421
+ });
2422
+ model.command("assignments").option("--space <spaceId>", "target space (defaults to the configured space)").option("--env <name>", "environment").description("list exact Agent model assignments for a Space").action(async (options) => {
2423
+ const config = await resolveConfig(options.env);
2424
+ const spaceId = options.space ?? config.spaceId;
2425
+ await runSpacesCommand("spaces:read", spaceId, async () => {
2426
+ print(await clientFor(config).models.assignments(spaceId), program.opts());
2427
+ });
2428
+ });
2429
+ program.command("releases").option("--env <name>", "environment").description("list releases").action(async (options) => {
2430
+ print(await clientFor(await resolveConfig(options.env)).releases.list(), program.opts());
2431
+ });
2432
+ program.command("rollback <releaseId>").option("--env <name>", "environment").description("roll back to a release").action(async (releaseId, options) => {
2433
+ print(await clientFor(await resolveConfig(options.env)).releases.rollback(releaseId), program.opts());
2434
+ });
2435
+ program.command("runs").option("--type <type>", "run type").option("--status <status>", "run status").option("--env <name>", "environment").description("list runs").action(async (options) => {
2436
+ print(await clientFor(await resolveConfig(options.env)).runs.list(Object.fromEntries([["type", options.type], ["status", options.status]].filter((entry) => typeof entry[1] === "string"))), program.opts());
2437
+ });
2438
+ program.command("run <type> <id>").option("--export", "export run bundle").option("--env <name>", "environment").description("show a run or export bundle").action(async (type, id, options) => {
2439
+ const runType = parseRunType(type);
2440
+ const client = clientFor(await resolveConfig(options.env));
2441
+ print(options.export ? await client.runs.export(runType, id) : await client.runs.show(runType, id), program.opts());
2442
+ });
2443
+ program.command("cancel <type> <id>").option("--env <name>", "environment").description("cancel a run").action(async (type, id, options) => {
2444
+ const runType = parseRunType(type);
2445
+ print(await clientFor(await resolveConfig(options.env)).runs.cancel(runType, id), program.opts());
2446
+ });
2447
+ program.command("action").command("run <ref>").option("--input <json>", "input payload", "{}").option("--env <name>", "environment").option("--release-id <id>", "pinned release id").option("--manifest-name <name>", "current release selector").description("start an Action Run").action(async (ref, options) => {
2448
+ print(await clientFor(await resolveConfig(options.env)).actions.run(ref, JSON.parse(options.input), {
2449
+ releaseId: options.releaseId,
2450
+ manifestName: options.manifestName
2451
+ }), program.opts());
2452
+ });
2453
+ program.command("workflow").command("trigger <ref>").option("--input <json>", "input payload", "{}").option("--env <name>", "environment").option("--release-id <id>", "pinned release id").option("--manifest-name <name>", "current release selector").description("start a Workflow Run").action(async (ref, options) => {
2454
+ print(await clientFor(await resolveConfig(options.env)).workflows.trigger(ref, JSON.parse(options.input), {
2455
+ releaseId: options.releaseId,
2456
+ manifestName: options.manifestName
2457
+ }), program.opts());
2458
+ });
2459
+ program.command("decisions").option("--env <name>", "environment").option("--actionable", "only generations this actor could answer").description("list pending decisions").action(async (options) => {
2460
+ print(await clientFor(await resolveConfig(options.env)).decisions.list({
2461
+ status: "pending",
2462
+ ...options.actionable ? { actionable: true } : {}
2463
+ }), program.opts());
2464
+ });
2465
+ program.command("approve <id>").option("--env <name>", "environment").option("--reason <reason>", "bounded reason recorded on the response").description("approve a decision").action(async (id, options) => {
2466
+ print(await clientFor(await resolveConfig(options.env)).decisions.approve(id, { ...options.reason === void 0 ? {} : { reason: options.reason } }), program.opts());
2467
+ });
2468
+ program.command("reject <id>").option("--env <name>", "environment").option("--reason <reason>", "bounded reason recorded on the response").description("reject a decision").action(async (id, options) => {
2469
+ print(await clientFor(await resolveConfig(options.env)).decisions.reject(id, { ...options.reason === void 0 ? {} : { reason: options.reason } }), program.opts());
2470
+ });
2471
+ program.command("events").option("--follow", "follow runtime events").option("--env <name>", "environment").description("show recent events or follow the event stream").action(async (options) => {
2472
+ const client = clientFor(await resolveConfig(options.env));
2473
+ if (!options.follow) {
2474
+ print(await client.events.recent(), program.opts());
2475
+ return;
2476
+ }
2477
+ const tail = followEvents(client, (event) => print(event, program.opts()));
2478
+ process.once("SIGINT", () => tail.stop());
2479
+ await tail.done;
2480
+ });
2481
+ program.parseAsync().catch((error) => fail(error, 1));
2482
+ async function scaffold() {
2483
+ const root = process.cwd();
2484
+ const templateRoot = fileURLToPath(new URL("../templates", import.meta.url));
2485
+ const projectFile = join(root, "ponderbot", "config.ts");
2486
+ await assertNewProjectFiles([projectFile]);
2487
+ await mkdir(dirname(projectFile), { recursive: true });
2488
+ await copyFile(join(templateRoot, "ponderbot", "config.ts"), projectFile, constants.COPYFILE_EXCL);
2489
+ }
2490
+ async function assertNewProjectFiles(paths) {
2491
+ for (const path of paths) try {
2492
+ await access(path);
2493
+ throw new Error(`${path} already exists`);
2494
+ } catch (error) {
2495
+ if (error.code !== "ENOENT") throw error;
2496
+ }
2497
+ }
2498
+ function countByKind(manifest) {
2499
+ return {
2500
+ actions: manifest.actions.length,
2501
+ skills: manifest.skills.length,
2502
+ schedules: manifest.schedules.length,
2503
+ workflows: manifest.workflows.length,
2504
+ agents: manifest.agents.length,
2505
+ agent_groups: manifest.agent_groups.length
2506
+ };
2507
+ }
2508
+ function parseRunType(value) {
2509
+ if (!isRunType(value)) throw new TypeError(`run type must be one of ${RUN_TYPES.join(", ")}`);
2510
+ return value;
2511
+ }
2512
+ async function readJsonFile(path) {
2513
+ return JSON.parse(await readFile(path, "utf8"));
2514
+ }
2515
+ async function runSpacesCommand(scope, space, run) {
2516
+ try {
2517
+ await run();
2518
+ } catch (error) {
2519
+ fail(spaceScopeError(error, scope, space), 1);
2520
+ }
2521
+ }
2522
+ const spaceScopeError = (error, scope, space) => {
2523
+ if (isPonderbotError(error) && error.status === 403) return /* @__PURE__ */ new Error(`your token lacks ${scope} on ${space} - mint one with that grant`);
2524
+ return error;
2525
+ };
2526
+ const spaceCreateError = (error, spaceId) => {
2527
+ if (isPonderbotError(error) && error.status === 409) return /* @__PURE__ */ new Error(`space "${spaceId}" already exists`);
2528
+ return error;
2529
+ };
2530
+ //#endregion
2531
+ export {};
2532
+
2533
+ //# sourceMappingURL=main.js.map