@treeseed/core 0.8.9 → 0.8.11

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,52 @@ function browserHost(host) {
150
169
  function webUrlFor(host, port) {
151
170
  return `http://${browserHost(host)}:${port}`;
152
171
  }
172
+ const CANONICAL_COMMAND_IDS = ["web", "api", "manager", "worker", "agents"];
173
+ function surfaceCommandIds(surface) {
174
+ switch (surface) {
175
+ case "web":
176
+ return ["web"];
177
+ case "api":
178
+ return ["api"];
179
+ case "manager":
180
+ return ["manager"];
181
+ case "worker":
182
+ return ["worker"];
183
+ case "agents":
184
+ return ["agents"];
185
+ case "services":
186
+ return ["api", "manager", "worker", "agents"];
187
+ case "all":
188
+ return ["web", "api", "manager", "worker"];
189
+ case "integrated":
190
+ default:
191
+ return ["web", "api", "manager", "worker"];
192
+ }
193
+ }
194
+ function parseSurfaceValue(value) {
195
+ return value === "web" || value === "api" || value === "manager" || value === "worker" || value === "agents" || value === "services" || value === "all" || value === "integrated" ? value : null;
196
+ }
197
+ function selectedSurfaceCommandIds(options) {
198
+ const values = (options.surfaces?.trim() || options.surface || "integrated").split(",").map((entry) => entry.trim()).filter(Boolean);
199
+ const selected = /* @__PURE__ */ new Set();
200
+ for (const value of values.length > 0 ? values : ["integrated"]) {
201
+ const surface = parseSurfaceValue(value);
202
+ if (!surface) continue;
203
+ for (const id of surfaceCommandIds(surface)) {
204
+ selected.add(id);
205
+ }
206
+ }
207
+ const selectedIds = CANONICAL_COMMAND_IDS.filter((id) => selected.has(id));
208
+ return selectedIds.length > 0 ? selectedIds : surfaceCommandIds("integrated");
209
+ }
210
+ function nodeLocalRuntime(label) {
211
+ return {
212
+ requested: "local",
213
+ provider: "local",
214
+ selected: "node-local",
215
+ reason: `${label} runs as a local Node.js process.`
216
+ };
217
+ }
153
218
  function dockerComposeIsAvailable(env) {
154
219
  const docker = resolveTreeseedToolBinary("docker", { env });
155
220
  if (!docker) return false;
@@ -204,7 +269,7 @@ function createTreeseedIntegratedDevResetPlan(options) {
204
269
  ]
205
270
  };
206
271
  }
207
- function createWatchEntries(tenantRoot, sdkPackageRoot) {
272
+ function createWatchEntries(tenantRoot, roots) {
208
273
  const entries = [
209
274
  { kind: "tenant", root: resolve(tenantRoot, "src") },
210
275
  { kind: "tenant", root: resolve(tenantRoot, "content") },
@@ -218,20 +283,34 @@ function createWatchEntries(tenantRoot, sdkPackageRoot) {
218
283
  ];
219
284
  if (!packageRoot.split(sep).includes("node_modules")) {
220
285
  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") }
286
+ { kind: "core", root: resolve(packageRoot, "src") },
287
+ { kind: "core", root: resolve(packageRoot, "scripts", "build-tenant-worker.ts") },
288
+ { kind: "core", root: resolve(packageRoot, "scripts", "run-ts.mjs") },
289
+ { kind: "core", root: resolve(packageRoot, "package.json") },
290
+ { kind: "core", root: resolve(packageRoot, "src", "dev.ts"), restartRequired: true },
291
+ { kind: "core", root: resolve(packageRoot, "scripts", "dev-platform.ts"), restartRequired: true }
292
+ );
293
+ }
294
+ if (!roots.sdkPackageRoot.split(sep).includes("node_modules")) {
295
+ entries.push(
296
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "src") },
297
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "tenant-astro-command.ts") },
298
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "tenant-d1-migrate-local.ts") },
299
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "scripts", "run-ts.mjs") },
300
+ { kind: "sdk", root: resolve(roots.sdkPackageRoot, "package.json") }
226
301
  );
227
302
  }
228
- if (!sdkPackageRoot.split(sep).includes("node_modules")) {
303
+ if (roots.agentPackageRoot && !roots.agentPackageRoot.split(sep).includes("node_modules")) {
229
304
  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") }
305
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "src") },
306
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "package.json") },
307
+ { kind: "agent", root: resolve(roots.agentPackageRoot, "scripts", "run-ts.mjs") }
308
+ );
309
+ }
310
+ if (roots.cliPackageRoot && !roots.cliPackageRoot.split(sep).includes("node_modules")) {
311
+ entries.push(
312
+ { kind: "cli", root: resolve(roots.cliPackageRoot, "src", "cli", "handlers", "dev.ts"), restartRequired: true },
313
+ { kind: "cli", root: resolve(roots.cliPackageRoot, "dist", "cli", "handlers", "dev.js"), restartRequired: true }
235
314
  );
236
315
  }
237
316
  return entries;
@@ -248,6 +327,9 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
248
327
  }
249
328
  ];
250
329
  }
330
+ const hasWebCommand = planLike.commands.some((command) => command.id === "web");
331
+ const hasLocalRuntimeCommand = planLike.commands.some((command) => command.id !== "web");
332
+ const needsCloudflareLocalRuntime = usesCloudflareWebRuntime || hasLocalRuntimeCommand;
251
333
  const coreScripts = [
252
334
  ["starlight-patch", "Patch Starlight content path", "scripts/patch-starlight-content-path.ts", "dist/scripts/patch-starlight-content-path.js"],
253
335
  ["books", "Generate book/public artifacts", "scripts/aggregate-book.ts", "dist/scripts/aggregate-book.js"],
@@ -272,18 +354,20 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
272
354
  {
273
355
  id: "wrangler",
274
356
  label: "Verify Wrangler executable",
275
- required: usesCloudflareWebRuntime,
357
+ required: needsCloudflareLocalRuntime,
276
358
  status: "planned",
277
359
  detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
278
360
  },
279
- ...usesCloudflareWebRuntime ? [
361
+ ...needsCloudflareLocalRuntime ? [
280
362
  {
281
363
  id: "wrangler-config",
282
364
  label: "Generate local Wrangler config",
283
365
  required: true,
284
366
  status: "planned",
285
367
  detail: generatedLocalWranglerPath(tenantRoot)
286
- },
368
+ }
369
+ ] : [],
370
+ ...usesCloudflareWebRuntime && hasWebCommand ? [
287
371
  {
288
372
  id: "web-build",
289
373
  label: "Build local Cloudflare web runtime",
@@ -293,7 +377,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
293
377
  status: tenantBuild ? "planned" : "failed",
294
378
  detail: tenantBuild ? void 0 : "Unable to resolve the tenant build script."
295
379
  }
296
- ] : coreScripts.map(([id, label, source, dist]) => {
380
+ ] : hasWebCommand ? coreScripts.map(([id, label, source, dist]) => {
297
381
  const script = resolveOptionalScriptEntrypoint(packageRoot, source, dist);
298
382
  return {
299
383
  id,
@@ -304,7 +388,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
304
388
  status: script ? "planned" : "skipped",
305
389
  detail: script ? void 0 : `Script not found at ${source}.`
306
390
  };
307
- }),
391
+ }) : [],
308
392
  {
309
393
  id: "mailpit",
310
394
  label: mailpitEnabled ? "Start Mailpit email runtime" : "Disable Mailpit email runtime",
@@ -315,7 +399,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
315
399
  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
400
  }
317
401
  ];
318
- if (usesCloudflareWebRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
402
+ if (needsCloudflareLocalRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
319
403
  const migrate = resolveOptionalScriptEntrypoint(
320
404
  sdkPackageRoot,
321
405
  "scripts/tenant-d1-migrate-local.ts",
@@ -333,6 +417,59 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
333
417
  }
334
418
  return steps;
335
419
  }
420
+ function createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost, apiPort) {
421
+ const configs = {
422
+ api: {
423
+ label: "Treeseed API",
424
+ source: "src/api/server.ts",
425
+ dist: "dist/api/server.js",
426
+ extraEnv: {
427
+ HOST: apiHost,
428
+ PORT: String(apiPort),
429
+ TREESEED_API_REPO_ROOT: tenantRoot
430
+ }
431
+ },
432
+ manager: {
433
+ label: "Manager",
434
+ source: "src/services/manager.ts",
435
+ dist: "dist/services/manager.js",
436
+ extraArgs: ["--mode", "loop"],
437
+ extraEnv: {
438
+ TREESEED_MANAGER_MODE: "loop"
439
+ }
440
+ },
441
+ worker: {
442
+ label: "Worker Runner",
443
+ source: "src/services/worker.ts",
444
+ dist: "dist/services/worker.js",
445
+ extraEnv: {}
446
+ },
447
+ agents: {
448
+ label: "Agents Loop",
449
+ source: "src/services/agents.ts",
450
+ dist: "dist/services/agents.js",
451
+ extraEnv: {}
452
+ }
453
+ };
454
+ const config = configs[id];
455
+ const entrypoint = resolveNodeEntrypoint(agentPackageRoot, config.source, config.dist);
456
+ return {
457
+ id,
458
+ label: config.label,
459
+ command: entrypoint.command,
460
+ args: [...entrypoint.args, ...config.extraArgs ?? []],
461
+ cwd: tenantRoot,
462
+ env: {
463
+ ...sharedEnv,
464
+ TREESEED_AGENT_REPO_ROOT: tenantRoot,
465
+ TREESEED_AGENT_D1_DATABASE: sharedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
466
+ TREESEED_AGENT_D1_PERSIST_TO: sharedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO,
467
+ TREESEED_ENVIRONMENT: sharedEnv.TREESEED_ENVIRONMENT ?? "local",
468
+ ...config.extraEnv
469
+ },
470
+ localRuntime: nodeLocalRuntime(config.label)
471
+ };
472
+ }
336
473
  function createTreeseedIntegratedDevPlan(options = {}) {
337
474
  const tenantRoot = resolve(options.cwd ?? process.cwd());
338
475
  const surface = options.surface ?? "integrated";
@@ -349,8 +486,11 @@ function createTreeseedIntegratedDevPlan(options = {}) {
349
486
  const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID;
350
487
  const teamId = options.teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
351
488
  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;
489
+ const selectedCommandIds = selectedSurfaceCommandIds(options);
490
+ const webUrl = selectedCommandIds.includes("web") ? webUrlFor(webHost, webPort) : null;
353
491
  const sdkPackageRoot = resolvePackageRoot("@treeseed/sdk", tenantRoot);
492
+ const agentPackageRoot = resolvePackageRootEnvOverride(mergedEnv, "TREESEED_AGENT_PACKAGE_ROOT", tenantRoot) ?? resolveOptionalPackageRoot("@treeseed/agent", tenantRoot);
493
+ const cliPackageRoot = resolveOptionalPackageRoot("@treeseed/cli", tenantRoot);
354
494
  const deployConfig = loadDevDeployConfig(tenantRoot);
355
495
  const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig));
356
496
  const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
@@ -373,7 +513,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
373
513
  String(webPort)
374
514
  ]
375
515
  };
376
- const watchEntries = watch ? createWatchEntries(tenantRoot, sdkPackageRoot) : [];
516
+ const watchEntries = watch ? createWatchEntries(tenantRoot, { sdkPackageRoot, agentPackageRoot, cliPackageRoot }) : [];
377
517
  const mailpitEnabled = dockerComposeIsAvailable(mergedEnv);
378
518
  const resetRequested = options.reset === true;
379
519
  const sharedEnv = {
@@ -407,7 +547,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
407
547
  sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
408
548
  }
409
549
  const commands = [];
410
- if (surface === "integrated" || surface === "web") {
550
+ if (selectedCommandIds.includes("web")) {
411
551
  commands.push({
412
552
  id: "web",
413
553
  label: usesCloudflareWebRuntime ? "Cloudflare Wrangler UI" : "Astro UI",
@@ -418,14 +558,21 @@ function createTreeseedIntegratedDevPlan(options = {}) {
418
558
  localRuntime: webLocalRuntime
419
559
  });
420
560
  }
561
+ if (selectedCommandIds.some((id) => id !== "web") && !agentPackageRoot) {
562
+ throw new Error("Unable to resolve @treeseed/agent for local API or agent service surfaces.");
563
+ }
564
+ for (const id of selectedCommandIds) {
565
+ if (id === "web") continue;
566
+ commands.push(createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost, apiPort));
567
+ }
421
568
  const readyChecks = commands.map((command) => {
422
- if (command.id === "web") {
569
+ if (command.id === "web" || command.id === "api") {
423
570
  return {
424
571
  id: command.id,
425
572
  label: command.label,
426
573
  required: true,
427
574
  strategy: "http",
428
- url: webUrl ?? void 0
575
+ url: command.id === "web" ? webUrl ?? void 0 : `${apiBaseUrl.replace(/\/+$/u, "")}/healthz`
429
576
  };
430
577
  }
431
578
  return {
@@ -458,7 +605,18 @@ function createTreeseedIntegratedDevPlan(options = {}) {
458
605
  watchEntries,
459
606
  commands,
460
607
  localRuntimes: {
461
- web: webLocalRuntime
608
+ ...commands.some((command) => command.id === "web") ? { web: webLocalRuntime } : {},
609
+ ...commands.some((command) => command.id === "api") ? { api: nodeLocalRuntime("Treeseed API") } : {},
610
+ ...commands.some((command) => command.id === "manager") ? { manager: nodeLocalRuntime("Manager") } : {},
611
+ ...commands.some((command) => command.id === "worker") ? { worker: nodeLocalRuntime("Worker Runner") } : {},
612
+ ...commands.some((command) => command.id === "agents") ? { agents: nodeLocalRuntime("Agents Loop") } : {}
613
+ },
614
+ restartPolicy: {
615
+ initialBackoffMs: INITIAL_RESTART_BACKOFF_MS,
616
+ maxBackoffMs: MAX_RESTART_BACKOFF_MS,
617
+ setupRetryBackoffMs: SETUP_RETRY_BACKOFF_MS,
618
+ commandImplementationChangesRequireRestart: true,
619
+ agentChanges: "defer"
462
620
  },
463
621
  reset
464
622
  };
@@ -559,6 +717,23 @@ function defaultWrite(line, stream) {
559
717
  const target = stream === "stderr" ? process.stderr : process.stdout;
560
718
  target.write(line);
561
719
  }
720
+ function shouldRedactEnvValue(key) {
721
+ return /(TOKEN|SECRET|PASSWORD|PASSPHRASE|PRIVATE|CREDENTIAL|AUTH)/iu.test(key);
722
+ }
723
+ function redactEnvironment(env) {
724
+ return Object.fromEntries(
725
+ Object.entries(env).map(([key, value]) => [key, value == null || !shouldRedactEnvValue(key) ? value : "[redacted]"])
726
+ );
727
+ }
728
+ function serializeDevPlanForOutput(plan) {
729
+ return {
730
+ ...plan,
731
+ commands: plan.commands.map((command) => ({
732
+ ...command,
733
+ env: redactEnvironment(command.env)
734
+ }))
735
+ };
736
+ }
562
737
  function resolveLocalMachineEnv(tenantRoot) {
563
738
  try {
564
739
  return resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
@@ -738,7 +913,7 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
738
913
  }
739
914
  function writePlan(plan, options, write) {
740
915
  if (options.json) {
741
- write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: plan }, null, 2)}
916
+ write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: serializeDevPlanForOutput(plan) }, null, 2)}
742
917
  `, "stdout");
743
918
  return;
744
919
  }
@@ -1016,17 +1191,15 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1016
1191
  });
1017
1192
  return 1;
1018
1193
  }
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
1194
  writeCurrentDevRuntimeState(tenantRoot);
1026
1195
  const children = /* @__PURE__ */ new Map();
1027
1196
  const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
1028
1197
  const requiredSurfaceIds = new Set(plan.readyChecks.filter((check) => check.required).map((check) => check.id));
1029
1198
  const exited = /* @__PURE__ */ new Map();
1199
+ const restartAttempts = /* @__PURE__ */ new Map();
1200
+ const restartTimers = /* @__PURE__ */ new Map();
1201
+ let setupRetryTimer = null;
1202
+ let readinessInProgress = false;
1030
1203
  let watchController = null;
1031
1204
  let settled = false;
1032
1205
  let readinessComplete = false;
@@ -1044,6 +1217,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1044
1217
  watchController.stop();
1045
1218
  watchController = null;
1046
1219
  }
1220
+ function clearTimers() {
1221
+ if (setupRetryTimer) {
1222
+ clearTimeout(setupRetryTimer);
1223
+ setupRetryTimer = null;
1224
+ }
1225
+ for (const timer of restartTimers.values()) {
1226
+ clearTimeout(timer);
1227
+ }
1228
+ restartTimers.clear();
1229
+ }
1047
1230
  function finalize(exitCode) {
1048
1231
  if (settled) {
1049
1232
  return;
@@ -1053,6 +1236,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1053
1236
  }
1054
1237
  async function finalizeAsync(exitCode) {
1055
1238
  stopWatching();
1239
+ clearTimers();
1056
1240
  await Promise.all(
1057
1241
  [...children.values()].map((managed) => stopManagedProcess(managed, "SIGTERM", killProcess, shutdownGraceMs))
1058
1242
  );
@@ -1069,6 +1253,47 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1069
1253
  );
1070
1254
  resolveExitCode(exitCode);
1071
1255
  }
1256
+ function restartDelayFor(id) {
1257
+ const attempts = restartAttempts.get(id) ?? 0;
1258
+ return Math.min(MAX_RESTART_BACKOFF_MS, INITIAL_RESTART_BACKOFF_MS * 2 ** attempts);
1259
+ }
1260
+ function markRestartAttempt(id) {
1261
+ const attempts = restartAttempts.get(id) ?? 0;
1262
+ restartAttempts.set(id, attempts + 1);
1263
+ }
1264
+ function runSetupOnce() {
1265
+ const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1266
+ const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
1267
+ if (failedSetup) {
1268
+ emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup), detail: failedSetup });
1269
+ return false;
1270
+ }
1271
+ return true;
1272
+ }
1273
+ function scheduleSetupRetry(reason) {
1274
+ if (setupRetryTimer || settled) {
1275
+ return;
1276
+ }
1277
+ emitEvent(options, write, {
1278
+ type: "restart",
1279
+ status: "retrying",
1280
+ message: `${reason} Retrying local setup in ${Math.round(SETUP_RETRY_BACKOFF_MS / 1e3)}s.`
1281
+ }, "stderr");
1282
+ setupRetryTimer = setTimeout(() => {
1283
+ setupRetryTimer = null;
1284
+ if (settled) return;
1285
+ if (!runSetupOnce()) {
1286
+ scheduleSetupRetry("Local setup is still failing.");
1287
+ return;
1288
+ }
1289
+ for (const command of plan.commands) {
1290
+ if (!children.has(command.id)) {
1291
+ spawnCommand(command);
1292
+ }
1293
+ }
1294
+ void waitForReadiness();
1295
+ }, SETUP_RETRY_BACKOFF_MS);
1296
+ }
1072
1297
  function spawnCommand(command) {
1073
1298
  emitEvent(options, write, {
1074
1299
  type: "spawn",
@@ -1103,9 +1328,10 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1103
1328
  surface: command.id,
1104
1329
  exitCode,
1105
1330
  signal,
1106
- message: `${command.label} exited unexpectedly during ${readinessComplete ? "supervision" : "startup"} with ${signal ?? exitCode}.`
1331
+ message: `${command.label} exited unexpectedly during ${readinessComplete ? "supervision" : "startup"} with ${signal ?? exitCode}; restarting.`
1107
1332
  });
1108
- finalize(exitCode === 0 ? 1 : exitCode);
1333
+ children.delete(command.id);
1334
+ scheduleCommandRestart(command.id);
1109
1335
  return;
1110
1336
  }
1111
1337
  const status = exitCode === 0 ? "idle" : "degraded";
@@ -1123,6 +1349,25 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1123
1349
  });
1124
1350
  return child;
1125
1351
  }
1352
+ function scheduleCommandRestart(id) {
1353
+ const command = commandsById.get(id);
1354
+ if (!command || settled || restartTimers.has(id)) {
1355
+ return;
1356
+ }
1357
+ const delayMs = restartDelayFor(id);
1358
+ markRestartAttempt(id);
1359
+ emitEvent(options, write, {
1360
+ type: "restart",
1361
+ surface: id,
1362
+ status: "scheduled",
1363
+ message: `Restarting ${command.label} in ${Math.round(delayMs / 1e3)}s.`
1364
+ }, "stderr");
1365
+ const timer = setTimeout(() => {
1366
+ restartTimers.delete(id);
1367
+ void restartCommand(id);
1368
+ }, delayMs);
1369
+ restartTimers.set(id, timer);
1370
+ }
1126
1371
  async function restartCommand(id) {
1127
1372
  const command = commandsById.get(id);
1128
1373
  if (!command || settled) {
@@ -1139,6 +1384,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1139
1384
  }
1140
1385
  spawnCommand(command);
1141
1386
  emitEvent(options, write, { type: "restart", surface: id, message: `Restarted ${command.label}.` });
1387
+ void waitForReadiness();
1142
1388
  }
1143
1389
  function startLiveWatch() {
1144
1390
  if (watchController || plan.watchEntries.length === 0 || plan.feedbackMode === "off" || settled) {
@@ -1162,28 +1408,50 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1162
1408
  detail: {
1163
1409
  tenantChanged: change.tenantChanged,
1164
1410
  tenantApiChanged: change.tenantApiChanged,
1165
- packageChanged: change.packageChanged,
1166
- sdkChanged: change.sdkChanged
1411
+ coreChanged: change.coreChanged,
1412
+ sdkChanged: change.sdkChanged,
1413
+ agentChanged: change.agentChanged,
1414
+ cliChanged: change.cliChanged,
1415
+ commandImplementationChanged: change.commandImplementationChanged
1167
1416
  }
1168
1417
  });
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);
1418
+ if (change.commandImplementationChanged) {
1419
+ emitEvent(options, write, {
1420
+ type: "replace",
1421
+ status: "restart-required",
1422
+ message: "The dev command implementation changed. Stop and rerun `npx trsd dev` to load the new supervisor.",
1423
+ detail: change.changedPaths
1424
+ }, "stderr");
1425
+ return;
1426
+ }
1427
+ if (change.agentChanged) {
1428
+ emitEvent(options, write, {
1429
+ type: "restart",
1430
+ status: "deferred",
1431
+ message: "Agent service changes detected; running agent services will keep their current code until the next workday or a manual restart.",
1432
+ detail: change.changedPaths
1433
+ });
1434
+ }
1435
+ if (change.tenantChanged || change.tenantApiChanged || change.coreChanged || change.sdkChanged) {
1436
+ if (!runSetupOnce()) {
1437
+ scheduleSetupRetry("Local setup failed after a development change.");
1181
1438
  return;
1182
1439
  }
1183
- await restartCommand("web");
1184
1440
  }
1185
- if (change.packageChanged || change.sdkChanged) {
1186
- await restartCommand("web");
1441
+ const restartIds = /* @__PURE__ */ new Set();
1442
+ if ((change.tenantChanged || change.coreChanged || change.sdkChanged) && commandsById.has("web")) {
1443
+ restartIds.add("web");
1444
+ }
1445
+ if ((change.tenantApiChanged || change.sdkChanged) && commandsById.has("api")) {
1446
+ restartIds.add("api");
1447
+ }
1448
+ if (change.sdkChanged) {
1449
+ for (const id of commandsById.keys()) {
1450
+ restartIds.add(id);
1451
+ }
1452
+ }
1453
+ for (const id of restartIds) {
1454
+ await restartCommand(id);
1187
1455
  }
1188
1456
  if (plan.feedbackMode === "live") {
1189
1457
  writeDevReloadStamp(plan.tenantRoot);
@@ -1198,10 +1466,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1198
1466
  watchController.rebaseline();
1199
1467
  }
1200
1468
  async function waitForReadiness() {
1469
+ if (readinessInProgress) {
1470
+ return;
1471
+ }
1472
+ readinessInProgress = true;
1201
1473
  const readinessTimeoutMs = options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
1202
1474
  const processReadyGraceMs = options.processReadyGraceMs ?? DEFAULT_PROCESS_READY_GRACE_MS;
1475
+ let allRequiredReady = true;
1203
1476
  for (const check of plan.readyChecks) {
1204
1477
  if (settled) {
1478
+ readinessInProgress = false;
1205
1479
  return;
1206
1480
  }
1207
1481
  let ready = false;
@@ -1213,17 +1487,26 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1213
1487
  ready = !exited.has(commandId) && children.has(commandId);
1214
1488
  }
1215
1489
  if (settled) {
1490
+ readinessInProgress = false;
1216
1491
  return;
1217
1492
  }
1218
1493
  if (!ready && check.required) {
1494
+ allRequiredReady = false;
1219
1495
  emitEvent(options, write, {
1220
1496
  type: "error",
1221
1497
  surface: check.id,
1222
1498
  url: check.url,
1223
- message: `${check.label} did not become ready${check.url ? ` at ${check.url}` : ""}.`
1499
+ message: `${check.label} did not become ready${check.url ? ` at ${check.url}` : ""}; keeping dev alive and retrying.`
1224
1500
  });
1225
- finalize(1);
1226
- return;
1501
+ if (check.id !== "mailpit") {
1502
+ scheduleCommandRestart(check.id);
1503
+ } else {
1504
+ scheduleSetupRetry("Mailpit readiness failed.");
1505
+ }
1506
+ continue;
1507
+ }
1508
+ if (ready && check.id !== "mailpit") {
1509
+ restartAttempts.set(check.id, 0);
1227
1510
  }
1228
1511
  emitEvent(options, write, {
1229
1512
  type: "ready",
@@ -1233,6 +1516,11 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1233
1516
  message: `${check.label} is ${ready ? "ready" : "degraded"}.`
1234
1517
  });
1235
1518
  }
1519
+ readinessInProgress = false;
1520
+ if (!allRequiredReady) {
1521
+ startLiveWatch();
1522
+ return;
1523
+ }
1236
1524
  readinessComplete = true;
1237
1525
  if (plan.webUrl) {
1238
1526
  emitEvent(options, write, { type: "ready", url: plan.webUrl, message: `Treeseed dev ready at ${plan.webUrl}.` });
@@ -1240,12 +1528,12 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1240
1528
  if (shouldOpenBrowser(plan)) {
1241
1529
  try {
1242
1530
  await openBrowser(plan.webUrl);
1243
- emitEvent(options, write, { type: "open", url: plan.webUrl, message: `Opened ${plan.webUrl}.` });
1531
+ emitEvent(options, write, { type: "open", url: plan.webUrl ?? void 0, message: `Opened ${plan.webUrl}.` });
1244
1532
  } catch (error) {
1245
1533
  emitEvent(options, write, {
1246
1534
  type: "open",
1247
1535
  status: "degraded",
1248
- url: plan.webUrl,
1536
+ url: plan.webUrl ?? void 0,
1249
1537
  message: `Could not open ${plan.webUrl}.`,
1250
1538
  detail: error instanceof Error ? error.message : String(error)
1251
1539
  });
@@ -1253,17 +1541,23 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1253
1541
  }
1254
1542
  startLiveWatch();
1255
1543
  }
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)
1544
+ startLiveWatch();
1545
+ if (runSetupOnce()) {
1546
+ for (const command of plan.commands) {
1547
+ spawnCommand(command);
1548
+ }
1549
+ void waitForReadiness().catch((error) => {
1550
+ readinessInProgress = false;
1551
+ emitEvent(options, write, {
1552
+ type: "error",
1553
+ message: "Dev readiness failed; keeping supervisor alive.",
1554
+ detail: error instanceof Error ? error.message : String(error)
1555
+ });
1556
+ scheduleSetupRetry("Readiness failed unexpectedly.");
1264
1557
  });
1265
- finalize(1);
1266
- });
1558
+ } else {
1559
+ scheduleSetupRetry("Initial local setup failed.");
1560
+ }
1267
1561
  });
1268
1562
  }
1269
1563
  export {