@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/components/SiteTitle.astro +2 -2
- package/dist/components/docs/Footer.astro +6 -90
- package/dist/components/docs/Header.astro +9 -2
- package/dist/components/site/RouteNotFound.astro +2 -2
- package/dist/components/ui/shell/AppShell.astro +2 -0
- package/dist/components/ui/shell/PublicFooter.astro +39 -0
- package/dist/components/ui/shell/PublicShell.astro +80 -9
- package/dist/components/ui/shell/TopBar.astro +1 -1
- package/dist/components/ui/theme/ThemeScript.astro +8 -2
- package/dist/dev-watch.d.ts +6 -2
- package/dist/dev-watch.js +12 -3
- package/dist/dev.d.ts +11 -2
- package/dist/dev.js +360 -66
- package/dist/layouts/MainLayout.astro +66 -195
- package/dist/middleware/starlightRouteData.js +20 -14
- package/dist/pages/404.astro +3 -3
- package/dist/pages/[slug].astro +1 -1
- package/dist/pages/books/[slug].astro +5 -5
- package/dist/pages/docs-runtime/[...slug].astro +1 -1
- package/dist/scripts/dev-platform.js +10 -2
- package/dist/site.js +50 -3
- package/dist/styles/app-shell.css +208 -9
- package/dist/styles/global.css +6 -1
- package/dist/utils/color-schemes/cedar.js +53 -0
- package/dist/utils/color-schemes/fern.js +53 -0
- package/dist/utils/color-schemes/index.js +13 -0
- package/dist/utils/color-schemes/lichen.js +53 -0
- package/dist/utils/color-schemes/shared.js +33 -0
- package/dist/utils/color-schemes/tidepool.js +53 -0
- package/dist/utils/starlight-nav.js +13 -7
- package/dist/utils/theme.js +10 -230
- package/package.json +3 -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 {
|
|
@@ -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,
|
|
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: "
|
|
222
|
-
{ kind: "
|
|
223
|
-
{ kind: "
|
|
224
|
-
{ kind: "
|
|
225
|
-
{ kind: "
|
|
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 (!
|
|
303
|
+
if (roots.agentPackageRoot && !roots.agentPackageRoot.split(sep).includes("node_modules")) {
|
|
229
304
|
entries.push(
|
|
230
|
-
{ kind: "
|
|
231
|
-
{ kind: "
|
|
232
|
-
{ kind: "
|
|
233
|
-
|
|
234
|
-
|
|
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:
|
|
357
|
+
required: needsCloudflareLocalRuntime,
|
|
276
358
|
status: "planned",
|
|
277
359
|
detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
|
|
278
360
|
},
|
|
279
|
-
...
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1558
|
+
} else {
|
|
1559
|
+
scheduleSetupRetry("Initial local setup failed.");
|
|
1560
|
+
}
|
|
1267
1561
|
});
|
|
1268
1562
|
}
|
|
1269
1563
|
export {
|