@treeseed/core 0.8.8 → 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.
Files changed (90) hide show
  1. package/dist/components/SiteTitle.astro +2 -2
  2. package/dist/components/content/ContentStatusLegend.astro +4 -4
  3. package/dist/components/docs/BookFontControls.astro +9 -9
  4. package/dist/components/docs/DesktopSidebarToggle.astro +8 -8
  5. package/dist/components/docs/Footer.astro +7 -91
  6. package/dist/components/docs/Header.astro +9 -2
  7. package/dist/components/docs/PageTitle.astro +1 -1
  8. package/dist/components/docs/ThemeSelect.astro +3 -1
  9. package/dist/components/forms/ContactForm.astro +21 -21
  10. package/dist/components/forms/FooterSubscribeForm.astro +9 -9
  11. package/dist/components/site/BookList.astro +7 -7
  12. package/dist/components/site/CTASection.astro +4 -4
  13. package/dist/components/site/ChronicleList.astro +6 -6
  14. package/dist/components/site/Hero.astro +3 -3
  15. package/dist/components/site/PathCard.astro +5 -5
  16. package/dist/components/site/ProfileList.astro +5 -5
  17. package/dist/components/site/RouteNotFound.astro +6 -6
  18. package/dist/components/site/SectionIntro.astro +3 -3
  19. package/dist/components/site/StageBanner.astro +2 -2
  20. package/dist/components/site/TrustCallout.astro +3 -3
  21. package/dist/components/ui/data/ActionList.astro +51 -0
  22. package/dist/components/ui/data/Badge.astro +19 -0
  23. package/dist/components/ui/data/DataTable.astro +51 -0
  24. package/dist/components/ui/data/KeyValueList.astro +28 -0
  25. package/dist/components/ui/data/MetricCard.astro +25 -0
  26. package/dist/components/ui/data/MetricGrid.astro +27 -0
  27. package/dist/components/ui/data/StatusPill.astro +20 -0
  28. package/dist/components/ui/forms/Button.astro +52 -0
  29. package/dist/components/ui/forms/Field.astro +39 -0
  30. package/dist/components/ui/forms/FormActions.astro +12 -0
  31. package/dist/components/ui/forms/PasswordMeter.astro +80 -0
  32. package/dist/components/ui/forms/RadioGroup.astro +55 -0
  33. package/dist/components/ui/forms/Select.astro +44 -0
  34. package/dist/components/ui/forms/TextInput.astro +58 -0
  35. package/dist/components/ui/forms/Textarea.astro +45 -0
  36. package/dist/components/ui/layout/PageHeader.astro +45 -0
  37. package/dist/components/ui/shell/AppShell.astro +112 -0
  38. package/dist/components/ui/shell/BottomNav.astro +35 -0
  39. package/dist/components/ui/shell/ProjectHeader.astro +66 -0
  40. package/dist/components/ui/shell/PublicFooter.astro +39 -0
  41. package/dist/components/ui/shell/PublicShell.astro +179 -0
  42. package/dist/components/ui/shell/RailNav.astro +35 -0
  43. package/dist/components/ui/shell/TopBar.astro +52 -0
  44. package/dist/components/ui/surface/Card.astro +46 -0
  45. package/dist/components/ui/surface/EmptyState.astro +45 -0
  46. package/dist/components/ui/surface/Panel.astro +54 -0
  47. package/dist/components/ui/theme/ThemeMenu.astro +32 -0
  48. package/dist/components/ui/theme/ThemePreviewSwatch.astro +18 -0
  49. package/dist/components/ui/theme/ThemeScript.astro +111 -0
  50. package/dist/components/ui/theme/ThemeSelector.astro +202 -0
  51. package/dist/components/ui/types.js +0 -0
  52. package/dist/dev-watch.d.ts +6 -2
  53. package/dist/dev-watch.js +12 -3
  54. package/dist/dev.d.ts +10 -2
  55. package/dist/dev.js +352 -68
  56. package/dist/layouts/AuthoredEntryLayout.astro +27 -27
  57. package/dist/layouts/BookLayout.astro +10 -10
  58. package/dist/layouts/ContentLayout.astro +4 -4
  59. package/dist/layouts/MainLayout.astro +66 -193
  60. package/dist/layouts/NoteLayout.astro +6 -6
  61. package/dist/layouts/ProfileLayout.astro +17 -17
  62. package/dist/middleware/starlightRouteData.js +20 -14
  63. package/dist/pages/404.astro +8 -8
  64. package/dist/pages/[slug].astro +1 -1
  65. package/dist/pages/books/[slug].astro +5 -5
  66. package/dist/pages/contact.astro +4 -4
  67. package/dist/pages/docs-runtime/[...slug].astro +12 -12
  68. package/dist/pages/docs-runtime/index.astro +13 -13
  69. package/dist/pages/index.astro +28 -28
  70. package/dist/pages/ui/index.astro +216 -0
  71. package/dist/scripts/dev-platform.js +7 -1
  72. package/dist/site.js +53 -5
  73. package/dist/styles/app-shell.css +597 -0
  74. package/dist/styles/forms.css +258 -0
  75. package/dist/styles/global.css +125 -120
  76. package/dist/styles/prose.css +11 -11
  77. package/dist/styles/theme.css +177 -0
  78. package/dist/styles/tokens.css +62 -22
  79. package/dist/styles/ui.css +551 -0
  80. package/dist/utils/color-schemes/cedar.js +53 -0
  81. package/dist/utils/color-schemes/fern.js +53 -0
  82. package/dist/utils/color-schemes/index.js +13 -0
  83. package/dist/utils/color-schemes/lichen.js +53 -0
  84. package/dist/utils/color-schemes/shared.js +33 -0
  85. package/dist/utils/color-schemes/tidepool.js +53 -0
  86. package/dist/utils/content-status.js +5 -5
  87. package/dist/utils/site-config.js +2 -2
  88. package/dist/utils/starlight-nav.js +13 -7
  89. package/dist/utils/theme.js +133 -41
  90. package/package.json +36 -2
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 {
@@ -33,9 +33,13 @@ const TREESEED_DEFAULT_MAILPIT_UI_PORT = 8025;
33
33
  const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
34
34
  const DEV_RUNTIME_FILE = ".treeseed/generated/dev/runtime.json";
35
35
  const DEFAULT_READINESS_TIMEOUT_MS = 9e4;
36
+ const DEFAULT_SETUP_STEP_TIMEOUT_MS = 3e5;
36
37
  const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
37
38
  const DEFAULT_SHUTDOWN_GRACE_MS = 2500;
38
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;
39
43
  function resolvePackageRoot(packageName, tenantRoot) {
40
44
  const resolvedPath = require2.resolve(packageName, {
41
45
  paths: [tenantRoot, packageRoot, process.cwd()]
@@ -50,6 +54,22 @@ function resolvePackageRoot(packageName, tenantRoot) {
50
54
  }
51
55
  return currentDir;
52
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
+ }
53
73
  function resolveNodeEntrypoint(packageDir, sourceRelativePath, distRelativePath) {
54
74
  const sourcePath = resolve(packageDir, sourceRelativePath);
55
75
  const runTsPath = resolve(packageDir, "scripts", "run-ts.mjs");
@@ -149,6 +169,33 @@ function browserHost(host) {
149
169
  function webUrlFor(host, port) {
150
170
  return `http://${browserHost(host)}:${port}`;
151
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
+ }
152
199
  function dockerComposeIsAvailable(env) {
153
200
  const docker = resolveTreeseedToolBinary("docker", { env });
154
201
  if (!docker) return false;
@@ -203,7 +250,7 @@ function createTreeseedIntegratedDevResetPlan(options) {
203
250
  ]
204
251
  };
205
252
  }
206
- function createWatchEntries(tenantRoot, sdkPackageRoot) {
253
+ function createWatchEntries(tenantRoot, roots) {
207
254
  const entries = [
208
255
  { kind: "tenant", root: resolve(tenantRoot, "src") },
209
256
  { kind: "tenant", root: resolve(tenantRoot, "content") },
@@ -217,20 +264,34 @@ function createWatchEntries(tenantRoot, sdkPackageRoot) {
217
264
  ];
218
265
  if (!packageRoot.split(sep).includes("node_modules")) {
219
266
  entries.push(
220
- { kind: "package", root: resolve(packageRoot, "src") },
221
- { kind: "package", root: resolve(packageRoot, "scripts", "dev-platform.ts") },
222
- { kind: "package", root: resolve(packageRoot, "scripts", "build-tenant-worker.ts") },
223
- { kind: "package", root: resolve(packageRoot, "scripts", "run-ts.mjs") },
224
- { 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 }
225
273
  );
226
274
  }
227
- if (!sdkPackageRoot.split(sep).includes("node_modules")) {
275
+ if (!roots.sdkPackageRoot.split(sep).includes("node_modules")) {
228
276
  entries.push(
229
- { kind: "sdk", root: resolve(sdkPackageRoot, "src") },
230
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "tenant-astro-command.ts") },
231
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "tenant-d1-migrate-local.ts") },
232
- { kind: "sdk", root: resolve(sdkPackageRoot, "scripts", "run-ts.mjs") },
233
- { 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 }
234
295
  );
235
296
  }
236
297
  return entries;
@@ -247,6 +308,9 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
247
308
  }
248
309
  ];
249
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;
250
314
  const coreScripts = [
251
315
  ["starlight-patch", "Patch Starlight content path", "scripts/patch-starlight-content-path.ts", "dist/scripts/patch-starlight-content-path.js"],
252
316
  ["books", "Generate book/public artifacts", "scripts/aggregate-book.ts", "dist/scripts/aggregate-book.js"],
@@ -271,18 +335,20 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
271
335
  {
272
336
  id: "wrangler",
273
337
  label: "Verify Wrangler executable",
274
- required: usesCloudflareWebRuntime,
338
+ required: needsCloudflareLocalRuntime,
275
339
  status: "planned",
276
340
  detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
277
341
  },
278
- ...usesCloudflareWebRuntime ? [
342
+ ...needsCloudflareLocalRuntime ? [
279
343
  {
280
344
  id: "wrangler-config",
281
345
  label: "Generate local Wrangler config",
282
346
  required: true,
283
347
  status: "planned",
284
348
  detail: generatedLocalWranglerPath(tenantRoot)
285
- },
349
+ }
350
+ ] : [],
351
+ ...usesCloudflareWebRuntime && hasWebCommand ? [
286
352
  {
287
353
  id: "web-build",
288
354
  label: "Build local Cloudflare web runtime",
@@ -292,7 +358,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
292
358
  status: tenantBuild ? "planned" : "failed",
293
359
  detail: tenantBuild ? void 0 : "Unable to resolve the tenant build script."
294
360
  }
295
- ] : coreScripts.map(([id, label, source, dist]) => {
361
+ ] : hasWebCommand ? coreScripts.map(([id, label, source, dist]) => {
296
362
  const script = resolveOptionalScriptEntrypoint(packageRoot, source, dist);
297
363
  return {
298
364
  id,
@@ -303,7 +369,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
303
369
  status: script ? "planned" : "skipped",
304
370
  detail: script ? void 0 : `Script not found at ${source}.`
305
371
  };
306
- }),
372
+ }) : [],
307
373
  {
308
374
  id: "mailpit",
309
375
  label: mailpitEnabled ? "Start Mailpit email runtime" : "Disable Mailpit email runtime",
@@ -314,7 +380,7 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
314
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."
315
381
  }
316
382
  ];
317
- if (usesCloudflareWebRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
383
+ if (needsCloudflareLocalRuntime && existsSync(resolve(tenantRoot, "migrations"))) {
318
384
  const migrate = resolveOptionalScriptEntrypoint(
319
385
  sdkPackageRoot,
320
386
  "scripts/tenant-d1-migrate-local.ts",
@@ -332,6 +398,56 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
332
398
  }
333
399
  return steps;
334
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
+ }
335
451
  function createTreeseedIntegratedDevPlan(options = {}) {
336
452
  const tenantRoot = resolve(options.cwd ?? process.cwd());
337
453
  const surface = options.surface ?? "integrated";
@@ -348,8 +464,11 @@ function createTreeseedIntegratedDevPlan(options = {}) {
348
464
  const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID;
349
465
  const teamId = options.teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
350
466
  const apiBaseUrl = options.apiHost != null || options.apiPort != null ? `http://${apiHost}:${apiPort}` : mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
351
- 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;
352
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);
353
472
  const deployConfig = loadDevDeployConfig(tenantRoot);
354
473
  const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig));
355
474
  const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
@@ -372,7 +491,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
372
491
  String(webPort)
373
492
  ]
374
493
  };
375
- const watchEntries = watch ? createWatchEntries(tenantRoot, sdkPackageRoot) : [];
494
+ const watchEntries = watch ? createWatchEntries(tenantRoot, { sdkPackageRoot, agentPackageRoot, cliPackageRoot }) : [];
376
495
  const mailpitEnabled = dockerComposeIsAvailable(mergedEnv);
377
496
  const resetRequested = options.reset === true;
378
497
  const sharedEnv = {
@@ -406,7 +525,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
406
525
  sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
407
526
  }
408
527
  const commands = [];
409
- if (surface === "integrated" || surface === "web") {
528
+ if (selectedCommandIds.includes("web")) {
410
529
  commands.push({
411
530
  id: "web",
412
531
  label: usesCloudflareWebRuntime ? "Cloudflare Wrangler UI" : "Astro UI",
@@ -417,14 +536,21 @@ function createTreeseedIntegratedDevPlan(options = {}) {
417
536
  localRuntime: webLocalRuntime
418
537
  });
419
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
+ }
420
546
  const readyChecks = commands.map((command) => {
421
- if (command.id === "web") {
547
+ if (command.id === "web" || command.id === "api") {
422
548
  return {
423
549
  id: command.id,
424
550
  label: command.label,
425
551
  required: true,
426
552
  strategy: "http",
427
- url: webUrl ?? void 0
553
+ url: command.id === "web" ? webUrl ?? void 0 : `${apiBaseUrl.replace(/\/+$/u, "")}/healthz`
428
554
  };
429
555
  }
430
556
  return {
@@ -457,7 +583,18 @@ function createTreeseedIntegratedDevPlan(options = {}) {
457
583
  watchEntries,
458
584
  commands,
459
585
  localRuntimes: {
460
- 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"
461
598
  },
462
599
  reset
463
600
  };
@@ -558,6 +695,23 @@ function defaultWrite(line, stream) {
558
695
  const target = stream === "stderr" ? process.stderr : process.stdout;
559
696
  target.write(line);
560
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
+ }
561
715
  function resolveLocalMachineEnv(tenantRoot) {
562
716
  try {
563
717
  return resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
@@ -737,7 +891,7 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
737
891
  }
738
892
  function writePlan(plan, options, write) {
739
893
  if (options.json) {
740
- 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)}
741
895
  `, "stdout");
742
896
  return;
743
897
  }
@@ -825,7 +979,8 @@ function runSetupStep(step, plan, deps) {
825
979
  TREESEED_LOCAL_DEV_MODE: "cloudflare",
826
980
  TREESEED_PUBLIC_DEV_WATCH_RELOAD: plan.feedbackMode === "live" ? "true" : process.env.TREESEED_PUBLIC_DEV_WATCH_RELOAD
827
981
  },
828
- encoding: "utf8"
982
+ encoding: "utf8",
983
+ timeout: DEFAULT_SETUP_STEP_TIMEOUT_MS
829
984
  });
830
985
  if ((result.status ?? 1) === 0) {
831
986
  return {
@@ -834,10 +989,12 @@ function runSetupStep(step, plan, deps) {
834
989
  detail: [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || step.detail
835
990
  };
836
991
  }
992
+ const timedOut = result.error && "code" in result.error && result.error.code === "ETIMEDOUT";
993
+ const timeoutDetail = timedOut ? `${step.label} timed out after ${Math.round(DEFAULT_SETUP_STEP_TIMEOUT_MS / 1e3)} seconds.` : null;
837
994
  return {
838
995
  ...step,
839
996
  status: step.required ? "failed" : "degraded",
840
- detail: [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || `Exited with ${result.status ?? 1}.`
997
+ detail: [timeoutDetail, result.stdout, result.stderr].filter(Boolean).join("\n").trim() || `Exited with ${result.status ?? 1}.`
841
998
  };
842
999
  }
843
1000
  function runLocalSetup(plan, options, deps) {
@@ -851,6 +1008,14 @@ function runLocalSetup(plan, options, deps) {
851
1008
  }
852
1009
  for (const step of plan.setupSteps) {
853
1010
  let result = step;
1011
+ if (step.status === "planned") {
1012
+ emitEvent(options, deps.write, {
1013
+ type: "setup",
1014
+ status: "running",
1015
+ message: `${step.label}: running`,
1016
+ detail: step.detail
1017
+ });
1018
+ }
854
1019
  if (step.id === "workspace-links") {
855
1020
  if (plan.setupMode === "check") {
856
1021
  result = { ...step, status: "skipped", detail: "Workspace links were checked in non-mutating mode." };
@@ -1004,17 +1169,15 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1004
1169
  });
1005
1170
  return 1;
1006
1171
  }
1007
- const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1008
- const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
1009
- if (failedSetup) {
1010
- emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup), detail: failedSetup });
1011
- return 1;
1012
- }
1013
1172
  writeCurrentDevRuntimeState(tenantRoot);
1014
1173
  const children = /* @__PURE__ */ new Map();
1015
1174
  const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
1016
1175
  const requiredSurfaceIds = new Set(plan.readyChecks.filter((check) => check.required).map((check) => check.id));
1017
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;
1018
1181
  let watchController = null;
1019
1182
  let settled = false;
1020
1183
  let readinessComplete = false;
@@ -1032,6 +1195,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1032
1195
  watchController.stop();
1033
1196
  watchController = null;
1034
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
+ }
1035
1208
  function finalize(exitCode) {
1036
1209
  if (settled) {
1037
1210
  return;
@@ -1041,6 +1214,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1041
1214
  }
1042
1215
  async function finalizeAsync(exitCode) {
1043
1216
  stopWatching();
1217
+ clearTimers();
1044
1218
  await Promise.all(
1045
1219
  [...children.values()].map((managed) => stopManagedProcess(managed, "SIGTERM", killProcess, shutdownGraceMs))
1046
1220
  );
@@ -1057,6 +1231,47 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1057
1231
  );
1058
1232
  resolveExitCode(exitCode);
1059
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
+ }
1060
1275
  function spawnCommand(command) {
1061
1276
  emitEvent(options, write, {
1062
1277
  type: "spawn",
@@ -1091,9 +1306,10 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1091
1306
  surface: command.id,
1092
1307
  exitCode,
1093
1308
  signal,
1094
- 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.`
1095
1310
  });
1096
- finalize(exitCode === 0 ? 1 : exitCode);
1311
+ children.delete(command.id);
1312
+ scheduleCommandRestart(command.id);
1097
1313
  return;
1098
1314
  }
1099
1315
  const status = exitCode === 0 ? "idle" : "degraded";
@@ -1111,6 +1327,25 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1111
1327
  });
1112
1328
  return child;
1113
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
+ }
1114
1349
  async function restartCommand(id) {
1115
1350
  const command = commandsById.get(id);
1116
1351
  if (!command || settled) {
@@ -1127,6 +1362,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1127
1362
  }
1128
1363
  spawnCommand(command);
1129
1364
  emitEvent(options, write, { type: "restart", surface: id, message: `Restarted ${command.label}.` });
1365
+ void waitForReadiness();
1130
1366
  }
1131
1367
  function startLiveWatch() {
1132
1368
  if (watchController || plan.watchEntries.length === 0 || plan.feedbackMode === "off" || settled) {
@@ -1150,28 +1386,50 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1150
1386
  detail: {
1151
1387
  tenantChanged: change.tenantChanged,
1152
1388
  tenantApiChanged: change.tenantApiChanged,
1153
- packageChanged: change.packageChanged,
1154
- sdkChanged: change.sdkChanged
1389
+ coreChanged: change.coreChanged,
1390
+ sdkChanged: change.sdkChanged,
1391
+ agentChanged: change.agentChanged,
1392
+ cliChanged: change.cliChanged,
1393
+ commandImplementationChanged: change.commandImplementationChanged
1155
1394
  }
1156
1395
  });
1157
- if (commandsById.get("web")?.localRuntime?.selected === "cloudflare-wrangler-local" && (change.tenantChanged || change.packageChanged || change.sdkChanged)) {
1158
- const web = children.get("web");
1159
- if (web) {
1160
- await stopManagedProcess(web, "SIGTERM", killProcess, Math.min(shutdownGraceMs, 500));
1161
- children.delete("web");
1162
- exited.delete("web");
1163
- }
1164
- const setupResults2 = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1165
- const failedSetup2 = setupResults2.find((step) => step.status === "failed" && step.required);
1166
- if (failedSetup2) {
1167
- emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup2), detail: failedSetup2 });
1168
- 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.");
1169
1416
  return;
1170
1417
  }
1171
- await restartCommand("web");
1172
1418
  }
1173
- if (change.packageChanged || change.sdkChanged) {
1174
- 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);
1175
1433
  }
1176
1434
  if (plan.feedbackMode === "live") {
1177
1435
  writeDevReloadStamp(plan.tenantRoot);
@@ -1186,10 +1444,16 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1186
1444
  watchController.rebaseline();
1187
1445
  }
1188
1446
  async function waitForReadiness() {
1447
+ if (readinessInProgress) {
1448
+ return;
1449
+ }
1450
+ readinessInProgress = true;
1189
1451
  const readinessTimeoutMs = options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
1190
1452
  const processReadyGraceMs = options.processReadyGraceMs ?? DEFAULT_PROCESS_READY_GRACE_MS;
1453
+ let allRequiredReady = true;
1191
1454
  for (const check of plan.readyChecks) {
1192
1455
  if (settled) {
1456
+ readinessInProgress = false;
1193
1457
  return;
1194
1458
  }
1195
1459
  let ready = false;
@@ -1201,17 +1465,26 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1201
1465
  ready = !exited.has(commandId) && children.has(commandId);
1202
1466
  }
1203
1467
  if (settled) {
1468
+ readinessInProgress = false;
1204
1469
  return;
1205
1470
  }
1206
1471
  if (!ready && check.required) {
1472
+ allRequiredReady = false;
1207
1473
  emitEvent(options, write, {
1208
1474
  type: "error",
1209
1475
  surface: check.id,
1210
1476
  url: check.url,
1211
- 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.`
1212
1478
  });
1213
- finalize(1);
1214
- 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);
1215
1488
  }
1216
1489
  emitEvent(options, write, {
1217
1490
  type: "ready",
@@ -1221,6 +1494,11 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1221
1494
  message: `${check.label} is ${ready ? "ready" : "degraded"}.`
1222
1495
  });
1223
1496
  }
1497
+ readinessInProgress = false;
1498
+ if (!allRequiredReady) {
1499
+ startLiveWatch();
1500
+ return;
1501
+ }
1224
1502
  readinessComplete = true;
1225
1503
  if (plan.webUrl) {
1226
1504
  emitEvent(options, write, { type: "ready", url: plan.webUrl, message: `Treeseed dev ready at ${plan.webUrl}.` });
@@ -1228,12 +1506,12 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1228
1506
  if (shouldOpenBrowser(plan)) {
1229
1507
  try {
1230
1508
  await openBrowser(plan.webUrl);
1231
- 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}.` });
1232
1510
  } catch (error) {
1233
1511
  emitEvent(options, write, {
1234
1512
  type: "open",
1235
1513
  status: "degraded",
1236
- url: plan.webUrl,
1514
+ url: plan.webUrl ?? void 0,
1237
1515
  message: `Could not open ${plan.webUrl}.`,
1238
1516
  detail: error instanceof Error ? error.message : String(error)
1239
1517
  });
@@ -1241,17 +1519,23 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1241
1519
  }
1242
1520
  startLiveWatch();
1243
1521
  }
1244
- for (const command of plan.commands) {
1245
- spawnCommand(command);
1246
- }
1247
- void waitForReadiness().catch((error) => {
1248
- emitEvent(options, write, {
1249
- type: "error",
1250
- message: "Dev readiness failed.",
1251
- 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.");
1252
1535
  });
1253
- finalize(1);
1254
- });
1536
+ } else {
1537
+ scheduleSetupRetry("Initial local setup failed.");
1538
+ }
1255
1539
  });
1256
1540
  }
1257
1541
  export {