@treeseed/core 0.8.9 → 0.8.10

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/dev.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, resolve, sep } from "node:path";
4
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { setTimeout as delay } from "node:timers/promises";
7
7
  import {
@@ -37,6 +37,9 @@ const DEFAULT_SETUP_STEP_TIMEOUT_MS = 3e5;
37
37
  const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
38
38
  const DEFAULT_SHUTDOWN_GRACE_MS = 2500;
39
39
  const DEFAULT_KILL_GRACE_MS = 500;
40
+ const INITIAL_RESTART_BACKOFF_MS = 1e3;
41
+ const MAX_RESTART_BACKOFF_MS = 15e3;
42
+ const SETUP_RETRY_BACKOFF_MS = 3e3;
40
43
  function resolvePackageRoot(packageName, tenantRoot) {
41
44
  const resolvedPath = require2.resolve(packageName, {
42
45
  paths: [tenantRoot, packageRoot, process.cwd()]
@@ -51,6 +54,22 @@ function resolvePackageRoot(packageName, tenantRoot) {
51
54
  }
52
55
  return currentDir;
53
56
  }
57
+ function resolveOptionalPackageRoot(packageName, tenantRoot) {
58
+ try {
59
+ return resolvePackageRoot(packageName, tenantRoot);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function resolvePackageRootEnvOverride(env, envName, tenantRoot) {
65
+ const value = env[envName]?.trim();
66
+ if (!value) return null;
67
+ const root = isAbsolute(value) ? value : resolve(tenantRoot, value);
68
+ if (!existsSync(resolve(root, "package.json"))) {
69
+ throw new Error(`${envName} must point to a package root containing package.json.`);
70
+ }
71
+ return root;
72
+ }
54
73
  function resolveNodeEntrypoint(packageDir, sourceRelativePath, distRelativePath) {
55
74
  const sourcePath = resolve(packageDir, sourceRelativePath);
56
75
  const runTsPath = resolve(packageDir, "scripts", "run-ts.mjs");
@@ -150,6 +169,33 @@ function browserHost(host) {
150
169
  function webUrlFor(host, port) {
151
170
  return `http://${browserHost(host)}:${port}`;
152
171
  }
172
+ function surfaceCommandIds(surface) {
173
+ switch (surface) {
174
+ case "web":
175
+ return ["web"];
176
+ case "api":
177
+ return ["api"];
178
+ case "manager":
179
+ return ["manager"];
180
+ case "worker":
181
+ return ["worker"];
182
+ case "agents":
183
+ return ["agents"];
184
+ case "services":
185
+ return ["api", "manager", "worker", "agents"];
186
+ case "integrated":
187
+ default:
188
+ return ["web", "api"];
189
+ }
190
+ }
191
+ function nodeLocalRuntime(label) {
192
+ return {
193
+ requested: "local",
194
+ provider: "local",
195
+ selected: "node-local",
196
+ reason: `${label} runs as a local Node.js process.`
197
+ };
198
+ }
153
199
  function dockerComposeIsAvailable(env) {
154
200
  const docker = resolveTreeseedToolBinary("docker", { env });
155
201
  if (!docker) return false;
@@ -204,7 +250,7 @@ function createTreeseedIntegratedDevResetPlan(options) {
204
250
  ]
205
251
  };
206
252
  }
207
- function createWatchEntries(tenantRoot, sdkPackageRoot) {
253
+ function createWatchEntries(tenantRoot, roots) {
208
254
  const entries = [
209
255
  { kind: "tenant", root: resolve(tenantRoot, "src") },
210
256
  { kind: "tenant", root: resolve(tenantRoot, "content") },
@@ -218,20 +264,34 @@ function createWatchEntries(tenantRoot, sdkPackageRoot) {
218
264
  ];
219
265
  if (!packageRoot.split(sep).includes("node_modules")) {
220
266
  entries.push(
221
- { kind: "package", root: resolve(packageRoot, "src") },
222
- { kind: "package", root: resolve(packageRoot, "scripts", "dev-platform.ts") },
223
- { kind: "package", root: resolve(packageRoot, "scripts", "build-tenant-worker.ts") },
224
- { kind: "package", root: resolve(packageRoot, "scripts", "run-ts.mjs") },
225
- { kind: "package", root: resolve(packageRoot, "package.json") }
267
+ { kind: "core", root: resolve(packageRoot, "src") },
268
+ { kind: "core", root: resolve(packageRoot, "scripts", "build-tenant-worker.ts") },
269
+ { kind: "core", root: resolve(packageRoot, "scripts", "run-ts.mjs") },
270
+ { kind: "core", root: resolve(packageRoot, "package.json") },
271
+ { kind: "core", root: resolve(packageRoot, "src", "dev.ts"), restartRequired: true },
272
+ { kind: "core", root: resolve(packageRoot, "scripts", "dev-platform.ts"), restartRequired: true }
226
273
  );
227
274
  }
228
- if (!sdkPackageRoot.split(sep).includes("node_modules")) {
275
+ if (!roots.sdkPackageRoot.split(sep).includes("node_modules")) {
229
276
  entries.push(
230
- { kind: "sdk", root: resolve(sdkPackageRoot, "src") },
231
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "tenant-astro-command.ts") },
232
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "tenant-d1-migrate-local.ts") },
233
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "run-ts.mjs") },
234
- { kind: "sdk", root: resolve(sdkPackageRoot, "package.json") }
277
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "src") },
278
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "tenant-astro-command.ts") },
279
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "tenant-d1-migrate-local.ts") },
280
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "run-ts.mjs") },
281
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "package.json") }
282
+ );
283
+ }
284
+ if (roots.agentPackageRoot && !roots.agentPackageRoot.split(sep).includes("node_modules")) {
285
+ entries.push(
286
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "src") },
287
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "package.json") },
288
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "scripts", "run-ts.mjs") }
289
+ );
290
+ }
291
+ if (roots.cliPackageRoot && !roots.cliPackageRoot.split(sep).includes("node_modules")) {
292
+ entries.push(
293
+ { kind: "cli", root: resolve(roots.cliPackageRoot, "src", "cli", "handlers", "dev.ts"), restartRequired: true },
294
+ { kind: "cli", root: resolve(roots.cliPackageRoot, "dist", "cli", "handlers", "dev.js"), restartRequired: true }
235
295
  );
236
296
  }
237
297
  return entries;
@@ -248,6 +308,9 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
248
308
  }
249
309
  ];
250
310
  }
311
+ const hasWebCommand = planLike.commands.some((command) => command.id === "web");
312
+ const hasLocalRuntimeCommand = planLike.commands.some((command) => command.id !== "web");
313
+ const needsCloudflareLocalRuntime = usesCloudflareWebRuntime || hasLocalRuntimeCommand;
251
314
  const coreScripts = [
252
315
  ["starlight-patch", "Patch Starlight content path", "scripts/patch-starlight-content-path.ts", "dist/scripts/patch-starlight-content-path.js"],
253
316
  ["books", "Generate book/public artifacts", "scripts/aggregate-book.ts", "dist/scripts/aggregate-book.js"],
@@ -272,18 +335,20 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
272
335
  {
273
336
  id: "wrangler",
274
337
  label: "Verify Wrangler executable",
275
- required: usesCloudflareWebRuntime,
338
+ required: needsCloudflareLocalRuntime,
276
339
  status: "planned",
277
340
  detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
278
341
  },
279
- ...usesCloudflareWebRuntime ? [
342
+ ...needsCloudflareLocalRuntime ? [
280
343
  {
281
344
  id: "wrangler-config",
282
345
  label: "Generate local Wrangler config",
283
346
  required: true,
284
347
  status: "planned",
285
348
  detail: generatedLocalWranglerPath(tenantRoot)
286
- },
349
+ }
350
+ ] : [],
351
+ ...usesCloudflareWebRuntime && hasWebCommand ? [
287
352
  {
288
353
  id: "web-build",
289
354
  label: "Build local Cloudflare web runtime",
@@ -293,7 +358,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
293
358
  status: tenantBuild ? "planned" : "failed",
294
359
  detail: tenantBuild ? void 0 : "Unable to resolve the tenant build script."
295
360
  }
296
- ] : coreScripts.map(([id, label, source, dist]) => {
361
+ ] : hasWebCommand ? coreScripts.map(([id, label, source, dist]) => {
297
362
  const script = resolveOptionalScriptEntrypoint(packageRoot, source, dist);
298
363
  return {
299
364
  id,
@@ -304,7 +369,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
304
369
  status: script ? "planned" : "skipped",
305
370
  detail: script ? void 0 : `Script not found at ${source}.`
306
371
  };
307
- }),
372
+ }) : [],
308
373
  {
309
374
  id: "mailpit",
310
375
  label: mailpitEnabled ? "Start Mailpit email runtime" : "Disable Mailpit email runtime",
@@ -315,7 +380,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
315
380
  detail: mailpitEnabled ? mailpit ? "Mailpit SMTP will listen on 127.0.0.1:1025 and the web UI on http://127.0.0.1:8025." : "Unable to resolve the Mailpit startup script." : "Docker Compose is unavailable, so Mailpit is disabled for this local dev run."
316
381
  }
317
382
  ];
318
- if (usesCloudflareWebRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
383
+ if (needsCloudflareLocalRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
319
384
  const migrate = resolveOptionalScriptEntrypoint(
320
385
  sdkPackageRoot,
321
386
  "scripts/tenant-d1-migrate-local.ts",
@@ -333,6 +398,56 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
333
398
  }
334
399
  return steps;
335
400
  }
401
+ function createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost, apiPort) {
402
+ const configs = {
403
+ api: {
404
+ label: "Treeseed API",
405
+ source: "src/api/server.ts",
406
+ dist: "dist/api/server.js",
407
+ extraEnv: {
408
+ HOST: apiHost,
409
+ PORT: String(apiPort),
410
+ TREESEED_API_REPO_ROOT: tenantRoot
411
+ }
412
+ },
413
+ manager: {
414
+ label: "Workday Manager",
415
+ source: "src/services/workday-manager.ts",
416
+ dist: "dist/services/workday-manager.js",
417
+ extraEnv: {}
418
+ },
419
+ worker: {
420
+ label: "Worker Runner",
421
+ source: "src/services/worker.ts",
422
+ dist: "dist/services/worker.js",
423
+ extraEnv: {}
424
+ },
425
+ agents: {
426
+ label: "Agents Loop",
427
+ source: "src/services/agents.ts",
428
+ dist: "dist/services/agents.js",
429
+ extraEnv: {}
430
+ }
431
+ };
432
+ const config = configs[id];
433
+ const entrypoint = resolveNodeEntrypoint(agentPackageRoot, config.source, config.dist);
434
+ return {
435
+ id,
436
+ label: config.label,
437
+ command: entrypoint.command,
438
+ args: entrypoint.args,
439
+ cwd: tenantRoot,
440
+ env: {
441
+ ...sharedEnv,
442
+ TREESEED_AGENT_REPO_ROOT: tenantRoot,
443
+ TREESEED_AGENT_D1_DATABASE: sharedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
444
+ TREESEED_AGENT_D1_PERSIST_TO: sharedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO,
445
+ TREESEED_ENVIRONMENT: sharedEnv.TREESEED_ENVIRONMENT ?? "local",
446
+ ...config.extraEnv
447
+ },
448
+ localRuntime: nodeLocalRuntime(config.label)
449
+ };
450
+ }
336
451
  function createTreeseedIntegratedDevPlan(options = {}) {
337
452
  const tenantRoot = resolve(options.cwd ?? process.cwd());
338
453
  const surface = options.surface ?? "integrated";
@@ -349,8 +464,11 @@ function createTreeseedIntegratedDevPlan(options = {}) {
349
464
  const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID;
350
465
  const teamId = options.teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
351
466
  const apiBaseUrl = options.apiHost != null || options.apiPort != null ? `http://${apiHost}:${apiPort}` : mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
352
- const webUrl = surface === "integrated" || surface === "web" ? webUrlFor(webHost, webPort) : null;
467
+ const selectedCommandIds = surfaceCommandIds(surface);
468
+ const webUrl = selectedCommandIds.includes("web") ? webUrlFor(webHost, webPort) : null;
353
469
  const sdkPackageRoot = resolvePackageRoot("@treeseed/sdk", tenantRoot);
470
+ const agentPackageRoot = resolvePackageRootEnvOverride(mergedEnv, "TREESEED_AGENT_PACKAGE_ROOT", tenantRoot) ?? resolveOptionalPackageRoot("@treeseed/agent", tenantRoot);
471
+ const cliPackageRoot = resolveOptionalPackageRoot("@treeseed/cli", tenantRoot);
354
472
  const deployConfig = loadDevDeployConfig(tenantRoot);
355
473
  const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig));
356
474
  const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
@@ -373,7 +491,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
373
491
  String(webPort)
374
492
  ]
375
493
  };
376
- const watchEntries = watch ? createWatchEntries(tenantRoot, sdkPackageRoot) : [];
494
+ const watchEntries = watch ? createWatchEntries(tenantRoot, { sdkPackageRoot, agentPackageRoot, cliPackageRoot }) : [];
377
495
  const mailpitEnabled = dockerComposeIsAvailable(mergedEnv);
378
496
  const resetRequested = options.reset === true;
379
497
  const sharedEnv = {
@@ -407,7 +525,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
407
525
  sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
408
526
  }
409
527
  const commands = [];
410
- if (surface === "integrated" || surface === "web") {
528
+ if (selectedCommandIds.includes("web")) {
411
529
  commands.push({
412
530
  id: "web",
413
531
  label: usesCloudflareWebRuntime ? "Cloudflare Wrangler UI" : "Astro UI",
@@ -418,14 +536,21 @@ function createTreeseedIntegratedDevPlan(options = {}) {
418
536
  localRuntime: webLocalRuntime
419
537
  });
420
538
  }
539
+ if (selectedCommandIds.some((id) => id !== "web") && !agentPackageRoot) {
540
+ throw new Error("Unable to resolve @treeseed/agent for local API or agent service surfaces.");
541
+ }
542
+ for (const id of selectedCommandIds) {
543
+ if (id === "web") continue;
544
+ commands.push(createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost, apiPort));
545
+ }
421
546
  const readyChecks = commands.map((command) => {
422
- if (command.id === "web") {
547
+ if (command.id === "web" || command.id === "api") {
423
548
  return {
424
549
  id: command.id,
425
550
  label: command.label,
426
551
  required: true,
427
552
  strategy: "http",
428
- url: webUrl ?? void 0
553
+ url: command.id === "web" ? webUrl ?? void 0 : `${apiBaseUrl.replace(/\/+$/u, "")}/healthz`
429
554
  };
430
555
  }
431
556
  return {
@@ -458,7 +583,18 @@ function createTreeseedIntegratedDevPlan(options = {}) {
458
583
  watchEntries,
459
584
  commands,
460
585
  localRuntimes: {
461
- web: webLocalRuntime
586
+ ...commands.some((command) => command.id === "web") ? { web: webLocalRuntime } : {},
587
+ ...commands.some((command) => command.id === "api") ? { api: nodeLocalRuntime("Treeseed API") } : {},
588
+ ...commands.some((command) => command.id === "manager") ? { manager: nodeLocalRuntime("Workday Manager") } : {},
589
+ ...commands.some((command) => command.id === "worker") ? { worker: nodeLocalRuntime("Worker Runner") } : {},
590
+ ...commands.some((command) => command.id === "agents") ? { agents: nodeLocalRuntime("Agents Loop") } : {}
591
+ },
592
+ restartPolicy: {
593
+ initialBackoffMs: INITIAL_RESTART_BACKOFF_MS,
594
+ maxBackoffMs: MAX_RESTART_BACKOFF_MS,
595
+ setupRetryBackoffMs: SETUP_RETRY_BACKOFF_MS,
596
+ commandImplementationChangesRequireRestart: true,
597
+ agentChanges: "defer"
462
598
  },
463
599
  reset
464
600
  };
@@ -559,6 +695,23 @@ function defaultWrite(line, stream) {
559
695
  const target = stream === "stderr" ? process.stderr : process.stdout;
560
696
  target.write(line);
561
697
  }
698
+ function shouldRedactEnvValue(key) {
699
+ return /(TOKEN|SECRET|PASSWORD|PASSPHRASE|PRIVATE|CREDENTIAL|AUTH)/iu.test(key);
700
+ }
701
+ function redactEnvironment(env) {
702
+ return Object.fromEntries(
703
+ Object.entries(env).map(([key, value]) => [key, value == null || !shouldRedactEnvValue(key) ? value : "[redacted]"])
704
+ );
705
+ }
706
+ function serializeDevPlanForOutput(plan) {
707
+ return {
708
+ ...plan,
709
+ commands: plan.commands.map((command) => ({
710
+ ...command,
711
+ env: redactEnvironment(command.env)
712
+ }))
713
+ };
714
+ }
562
715
  function resolveLocalMachineEnv(tenantRoot) {
563
716
  try {
564
717
  return resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
@@ -738,7 +891,7 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
738
891
  }
739
892
  function writePlan(plan, options, write) {
740
893
  if (options.json) {
741
- write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: plan }, null, 2)}
894
+ write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: serializeDevPlanForOutput(plan) }, null, 2)}
742
895
  `, "stdout");
743
896
  return;
744
897
  }
@@ -1016,17 +1169,15 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1016
1169
  });
1017
1170
  return 1;
1018
1171
  }
1019
- const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1020
- const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
1021
- if (failedSetup) {
1022
- emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup), detail: failedSetup });
1023
- return 1;
1024
- }
1025
1172
  writeCurrentDevRuntimeState(tenantRoot);
1026
1173
  const children = /* @__PURE__ */ new Map();
1027
1174
  const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
1028
1175
  const requiredSurfaceIds = new Set(plan.readyChecks.filter((check) => check.required).map((check) => check.id));
1029
1176
  const exited = /* @__PURE__ */ new Map();
1177
+ const restartAttempts = /* @__PURE__ */ new Map();
1178
+ const restartTimers = /* @__PURE__ */ new Map();
1179
+ let setupRetryTimer = null;
1180
+ let readinessInProgress = false;
1030
1181
  let watchController = null;
1031
1182
  let settled = false;
1032
1183
  let readinessComplete = false;
@@ -1044,6 +1195,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1044
1195
  watchController.stop();
1045
1196
  watchController = null;
1046
1197
  }
1198
+ function clearTimers() {
1199
+ if (setupRetryTimer) {
1200
+ clearTimeout(setupRetryTimer);
1201
+ setupRetryTimer = null;
1202
+ }
1203
+ for (const timer of restartTimers.values()) {
1204
+ clearTimeout(timer);
1205
+ }
1206
+ restartTimers.clear();
1207
+ }
1047
1208
  function finalize(exitCode) {
1048
1209
  if (settled) {
1049
1210
  return;
@@ -1053,6 +1214,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1053
1214
  }
1054
1215
  async function finalizeAsync(exitCode) {
1055
1216
  stopWatching();
1217
+ clearTimers();
1056
1218
  await Promise.all(
1057
1219
  [...children.values()].map((managed) => stopManagedProcess(managed, "SIGTERM", killProcess, shutdownGraceMs))
1058
1220
  );
@@ -1069,6 +1231,47 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1069
1231
  );
1070
1232
  resolveExitCode(exitCode);
1071
1233
  }
1234
+ function restartDelayFor(id) {
1235
+ const attempts = restartAttempts.get(id) ?? 0;
1236
+ return Math.min(MAX_RESTART_BACKOFF_MS, INITIAL_RESTART_BACKOFF_MS * 2 ** attempts);
1237
+ }
1238
+ function markRestartAttempt(id) {
1239
+ const attempts = restartAttempts.get(id) ?? 0;
1240
+ restartAttempts.set(id, attempts + 1);
1241
+ }
1242
+ function runSetupOnce() {
1243
+ const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1244
+ const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
1245
+ if (failedSetup) {
1246
+ emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup), detail: failedSetup });
1247
+ return false;
1248
+ }
1249
+ return true;
1250
+ }
1251
+ function scheduleSetupRetry(reason) {
1252
+ if (setupRetryTimer || settled) {
1253
+ return;
1254
+ }
1255
+ emitEvent(options, write, {
1256
+ type: "restart",
1257
+ status: "retrying",
1258
+ message: `${reason} Retrying local setup in ${Math.round(SETUP_RETRY_BACKOFF_MS / 1e3)}s.`
1259
+ }, "stderr");
1260
+ setupRetryTimer = setTimeout(() => {
1261
+ setupRetryTimer = null;
1262
+ if (settled) return;
1263
+ if (!runSetupOnce()) {
1264
+ scheduleSetupRetry("Local setup is still failing.");
1265
+ return;
1266
+ }
1267
+ for (const command of plan.commands) {
1268
+ if (!children.has(command.id)) {
1269
+ spawnCommand(command);
1270
+ }
1271
+ }
1272
+ void waitForReadiness();
1273
+ }, SETUP_RETRY_BACKOFF_MS);
1274
+ }
1072
1275
  function spawnCommand(command) {
1073
1276
  emitEvent(options, write, {
1074
1277
  type: "spawn",
@@ -1103,9 +1306,10 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1103
1306
  surface: command.id,
1104
1307
  exitCode,
1105
1308
  signal,
1106
- message: `${command.label} exited unexpectedly during ${readinessComplete ? "supervision" : "startup"} with ${signal ?? exitCode}.`
1309
+ message: `${command.label} exited unexpectedly during ${readinessComplete ? "supervision" : "startup"} with ${signal ?? exitCode}; restarting.`
1107
1310
  });
1108
- finalize(exitCode === 0 ? 1 : exitCode);
1311
+ children.delete(command.id);
1312
+ scheduleCommandRestart(command.id);
1109
1313
  return;
1110
1314
  }
1111
1315
  const status = exitCode === 0 ? "idle" : "degraded";
@@ -1123,6 +1327,25 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1123
1327
  });
1124
1328
  return child;
1125
1329
  }
1330
+ function scheduleCommandRestart(id) {
1331
+ const command = commandsById.get(id);
1332
+ if (!command || settled || restartTimers.has(id)) {
1333
+ return;
1334
+ }
1335
+ const delayMs = restartDelayFor(id);
1336
+ markRestartAttempt(id);
1337
+ emitEvent(options, write, {
1338
+ type: "restart",
1339
+ surface: id,
1340
+ status: "scheduled",
1341
+ message: `Restarting ${command.label} in ${Math.round(delayMs / 1e3)}s.`
1342
+ }, "stderr");
1343
+ const timer = setTimeout(() => {
1344
+ restartTimers.delete(id);
1345
+ void restartCommand(id);
1346
+ }, delayMs);
1347
+ restartTimers.set(id, timer);
1348
+ }
1126
1349
  async function restartCommand(id) {
1127
1350
  const command = commandsById.get(id);
1128
1351
  if (!command || settled) {
@@ -1139,6 +1362,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1139
1362
  }
1140
1363
  spawnCommand(command);
1141
1364
  emitEvent(options, write, { type: "restart", surface: id, message: `Restarted ${command.label}.` });
1365
+ void waitForReadiness();
1142
1366
  }
1143
1367
  function startLiveWatch() {
1144
1368
  if (watchController || plan.watchEntries.length === 0 || plan.feedbackMode === "off" || settled) {
@@ -1162,28 +1386,50 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1162
1386
  detail: {
1163
1387
  tenantChanged: change.tenantChanged,
1164
1388
  tenantApiChanged: change.tenantApiChanged,
1165
- packageChanged: change.packageChanged,
1166
- sdkChanged: change.sdkChanged
1389
+ coreChanged: change.coreChanged,
1390
+ sdkChanged: change.sdkChanged,
1391
+ agentChanged: change.agentChanged,
1392
+ cliChanged: change.cliChanged,
1393
+ commandImplementationChanged: change.commandImplementationChanged
1167
1394
  }
1168
1395
  });
1169
- if (commandsById.get("web")?.localRuntime?.selected === "cloudflare-wrangler-local" && (change.tenantChanged || change.packageChanged || change.sdkChanged)) {
1170
- const web = children.get("web");
1171
- if (web) {
1172
- await stopManagedProcess(web, "SIGTERM", killProcess, Math.min(shutdownGraceMs, 500));
1173
- children.delete("web");
1174
- exited.delete("web");
1175
- }
1176
- const setupResults2 = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1177
- const failedSetup2 = setupResults2.find((step) => step.status === "failed" && step.required);
1178
- if (failedSetup2) {
1179
- emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup2), detail: failedSetup2 });
1180
- finalize(1);
1396
+ if (change.commandImplementationChanged) {
1397
+ emitEvent(options, write, {
1398
+ type: "replace",
1399
+ status: "restart-required",
1400
+ message: "The dev command implementation changed. Stop and rerun `npx trsd dev` to load the new supervisor.",
1401
+ detail: change.changedPaths
1402
+ }, "stderr");
1403
+ return;
1404
+ }
1405
+ if (change.agentChanged) {
1406
+ emitEvent(options, write, {
1407
+ type: "restart",
1408
+ status: "deferred",
1409
+ message: "Agent service changes detected; running agent services will keep their current code until the next workday or a manual restart.",
1410
+ detail: change.changedPaths
1411
+ });
1412
+ }
1413
+ if (change.tenantChanged || change.tenantApiChanged || change.coreChanged || change.sdkChanged) {
1414
+ if (!runSetupOnce()) {
1415
+ scheduleSetupRetry("Local setup failed after a development change.");
1181
1416
  return;
1182
1417
  }
1183
- await restartCommand("web");
1184
1418
  }
1185
- if (change.packageChanged || change.sdkChanged) {
1186
- await restartCommand("web");
1419
+ const restartIds = /* @__PURE__ */ new Set();
1420
+ if ((change.tenantChanged || change.coreChanged || change.sdkChanged) && commandsById.has("web")) {
1421
+ restartIds.add("web");
1422
+ }
1423
+ if ((change.tenantApiChanged || change.sdkChanged) && commandsById.has("api")) {
1424
+ restartIds.add("api");
1425
+ }
1426
+ if (change.sdkChanged) {
1427
+ for (const id of commandsById.keys()) {
1428
+ restartIds.add(id);
1429
+ }
1430
+ }
1431
+ for (const id of restartIds) {
1432
+ await restartCommand(id);
1187
1433
  }
1188
1434
  if (plan.feedbackMode === "live") {
1189
1435
  writeDevReloadStamp(plan.tenantRoot);
@@ -1198,10 +1444,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1198
1444
  watchController.rebaseline();
1199
1445
  }
1200
1446
  async function waitForReadiness() {
1447
+ if (readinessInProgress) {
1448
+ return;
1449
+ }
1450
+ readinessInProgress = true;
1201
1451
  const readinessTimeoutMs = options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
1202
1452
  const processReadyGraceMs = options.processReadyGraceMs ?? DEFAULT_PROCESS_READY_GRACE_MS;
1453
+ let allRequiredReady = true;
1203
1454
  for (const check of plan.readyChecks) {
1204
1455
  if (settled) {
1456
+ readinessInProgress = false;
1205
1457
  return;
1206
1458
  }
1207
1459
  let ready = false;
@@ -1213,17 +1465,26 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1213
1465
  ready = !exited.has(commandId) && children.has(commandId);
1214
1466
  }
1215
1467
  if (settled) {
1468
+ readinessInProgress = false;
1216
1469
  return;
1217
1470
  }
1218
1471
  if (!ready && check.required) {
1472
+ allRequiredReady = false;
1219
1473
  emitEvent(options, write, {
1220
1474
  type: "error",
1221
1475
  surface: check.id,
1222
1476
  url: check.url,
1223
- message: `${check.label} did not become ready${check.url ? ` at ${check.url}` : ""}.`
1477
+ message: `${check.label} did not become ready${check.url ? ` at ${check.url}` : ""}; keeping dev alive and retrying.`
1224
1478
  });
1225
- finalize(1);
1226
- return;
1479
+ if (check.id !== "mailpit") {
1480
+ scheduleCommandRestart(check.id);
1481
+ } else {
1482
+ scheduleSetupRetry("Mailpit readiness failed.");
1483
+ }
1484
+ continue;
1485
+ }
1486
+ if (ready && check.id !== "mailpit") {
1487
+ restartAttempts.set(check.id, 0);
1227
1488
  }
1228
1489
  emitEvent(options, write, {
1229
1490
  type: "ready",
@@ -1233,6 +1494,11 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1233
1494
  message: `${check.label} is ${ready ? "ready" : "degraded"}.`
1234
1495
  });
1235
1496
  }
1497
+ readinessInProgress = false;
1498
+ if (!allRequiredReady) {
1499
+ startLiveWatch();
1500
+ return;
1501
+ }
1236
1502
  readinessComplete = true;
1237
1503
  if (plan.webUrl) {
1238
1504
  emitEvent(options, write, { type: "ready", url: plan.webUrl, message: `Treeseed dev ready at ${plan.webUrl}.` });
@@ -1240,12 +1506,12 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1240
1506
  if (shouldOpenBrowser(plan)) {
1241
1507
  try {
1242
1508
  await openBrowser(plan.webUrl);
1243
- emitEvent(options, write, { type: "open", url: plan.webUrl, message: `Opened ${plan.webUrl}.` });
1509
+ emitEvent(options, write, { type: "open", url: plan.webUrl ?? void 0, message: `Opened ${plan.webUrl}.` });
1244
1510
  } catch (error) {
1245
1511
  emitEvent(options, write, {
1246
1512
  type: "open",
1247
1513
  status: "degraded",
1248
- url: plan.webUrl,
1514
+ url: plan.webUrl ?? void 0,
1249
1515
  message: `Could not open ${plan.webUrl}.`,
1250
1516
  detail: error instanceof Error ? error.message : String(error)
1251
1517
  });
@@ -1253,17 +1519,23 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1253
1519
  }
1254
1520
  startLiveWatch();
1255
1521
  }
1256
- for (const command of plan.commands) {
1257
- spawnCommand(command);
1258
- }
1259
- void waitForReadiness().catch((error) => {
1260
- emitEvent(options, write, {
1261
- type: "error",
1262
- message: "Dev readiness failed.",
1263
- detail: error instanceof Error ? error.message : String(error)
1522
+ startLiveWatch();
1523
+ if (runSetupOnce()) {
1524
+ for (const command of plan.commands) {
1525
+ spawnCommand(command);
1526
+ }
1527
+ void waitForReadiness().catch((error) => {
1528
+ readinessInProgress = false;
1529
+ emitEvent(options, write, {
1530
+ type: "error",
1531
+ message: "Dev readiness failed; keeping supervisor alive.",
1532
+ detail: error instanceof Error ? error.message : String(error)
1533
+ });
1534
+ scheduleSetupRetry("Readiness failed unexpectedly.");
1264
1535
  });
1265
- finalize(1);
1266
- });
1536
+ } else {
1537
+ scheduleSetupRetry("Initial local setup failed.");
1538
+ }
1267
1539
  });
1268
1540
  }
1269
1541
  export {