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