@treeseed/core 0.6.15 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev.d.ts +56 -0
- package/dist/dev.js +704 -15
- package/dist/scripts/dev-platform.js +37 -1
- package/package.json +2 -2
package/dist/dev.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
export declare const TREESEED_DEFAULT_WEB_HOST = "127.0.0.1";
|
|
3
4
|
export declare const TREESEED_DEFAULT_WEB_PORT = 4321;
|
|
4
5
|
export declare const TREESEED_DEFAULT_API_HOST = "127.0.0.1";
|
|
5
6
|
export declare const TREESEED_DEFAULT_API_PORT = 3000;
|
|
6
7
|
export declare const TREESEED_DEFAULT_MANAGER_PORT = 3100;
|
|
7
8
|
export type TreeseedIntegratedDevSurface = 'integrated' | 'services' | 'web' | 'api' | 'manager' | 'worker';
|
|
9
|
+
export type TreeseedIntegratedDevSetupMode = 'auto' | 'check' | 'off';
|
|
10
|
+
export type TreeseedIntegratedDevFeedbackMode = 'live' | 'restart' | 'off';
|
|
11
|
+
export type TreeseedIntegratedDevOpenMode = 'auto' | 'on' | 'off';
|
|
8
12
|
export type TreeseedIntegratedDevOptions = {
|
|
9
13
|
surface?: TreeseedIntegratedDevSurface;
|
|
10
14
|
watch?: boolean;
|
|
@@ -16,9 +20,16 @@ export type TreeseedIntegratedDevOptions = {
|
|
|
16
20
|
apiHost?: string;
|
|
17
21
|
apiPort?: number;
|
|
18
22
|
managerPort?: number;
|
|
23
|
+
setupMode?: TreeseedIntegratedDevSetupMode;
|
|
24
|
+
feedbackMode?: TreeseedIntegratedDevFeedbackMode;
|
|
25
|
+
openMode?: TreeseedIntegratedDevOpenMode;
|
|
26
|
+
plan?: boolean;
|
|
27
|
+
json?: boolean;
|
|
19
28
|
includeServices?: boolean;
|
|
20
29
|
projectId?: string;
|
|
21
30
|
teamId?: string;
|
|
31
|
+
readinessTimeoutMs?: number;
|
|
32
|
+
processReadyGraceMs?: number;
|
|
22
33
|
};
|
|
23
34
|
export type TreeseedIntegratedDevCommand = {
|
|
24
35
|
id: 'web' | 'api' | 'manager' | 'worker';
|
|
@@ -28,18 +39,63 @@ export type TreeseedIntegratedDevCommand = {
|
|
|
28
39
|
cwd: string;
|
|
29
40
|
env: NodeJS.ProcessEnv;
|
|
30
41
|
};
|
|
42
|
+
export type TreeseedIntegratedDevWatchEntry = {
|
|
43
|
+
kind: 'tenant' | 'package' | 'sdk';
|
|
44
|
+
root: string;
|
|
45
|
+
};
|
|
46
|
+
export type TreeseedIntegratedDevSetupStep = {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
required: boolean;
|
|
50
|
+
command?: string;
|
|
51
|
+
args?: string[];
|
|
52
|
+
status: 'planned' | 'completed' | 'skipped' | 'degraded' | 'failed';
|
|
53
|
+
detail?: string;
|
|
54
|
+
};
|
|
55
|
+
export type TreeseedIntegratedDevReadinessCheck = {
|
|
56
|
+
id: TreeseedIntegratedDevCommand['id'];
|
|
57
|
+
label: string;
|
|
58
|
+
required: boolean;
|
|
59
|
+
strategy: 'http' | 'process';
|
|
60
|
+
url?: string;
|
|
61
|
+
};
|
|
31
62
|
export type TreeseedIntegratedDevPlan = {
|
|
32
63
|
surface: TreeseedIntegratedDevSurface;
|
|
64
|
+
setupMode: TreeseedIntegratedDevSetupMode;
|
|
65
|
+
feedbackMode: TreeseedIntegratedDevFeedbackMode;
|
|
66
|
+
openMode: TreeseedIntegratedDevOpenMode;
|
|
67
|
+
watch: boolean;
|
|
33
68
|
tenantRoot: string;
|
|
34
69
|
apiBaseUrl: string;
|
|
70
|
+
webUrl: string | null;
|
|
71
|
+
setupSteps: TreeseedIntegratedDevSetupStep[];
|
|
72
|
+
readyChecks: TreeseedIntegratedDevReadinessCheck[];
|
|
73
|
+
watchEntries: TreeseedIntegratedDevWatchEntry[];
|
|
35
74
|
commands: TreeseedIntegratedDevCommand[];
|
|
36
75
|
};
|
|
37
76
|
type SpawnLike = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
77
|
+
type SpawnSyncLike = typeof spawnSync;
|
|
38
78
|
type SignalRegistrar = (signal: NodeJS.Signals, handler: () => void) => () => void;
|
|
79
|
+
type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
80
|
+
type WatchChange = {
|
|
81
|
+
changedPaths: string[];
|
|
82
|
+
tenantChanged: boolean;
|
|
83
|
+
packageChanged: boolean;
|
|
84
|
+
sdkChanged: boolean;
|
|
85
|
+
};
|
|
86
|
+
type WatchStarter = (input: {
|
|
87
|
+
watchEntries: TreeseedIntegratedDevWatchEntry[];
|
|
88
|
+
onChange: (change: WatchChange) => void | Promise<void>;
|
|
89
|
+
}) => () => void;
|
|
39
90
|
type TreeseedIntegratedDevDependencies = {
|
|
40
91
|
spawn: SpawnLike;
|
|
92
|
+
spawnSync: SpawnSyncLike;
|
|
41
93
|
onSignal: SignalRegistrar;
|
|
42
94
|
prepareEnvironment: (tenantRoot: string) => void;
|
|
95
|
+
fetch: FetchLike;
|
|
96
|
+
write: (line: string, stream: 'stdout' | 'stderr') => void;
|
|
97
|
+
openBrowser: (url: string) => void | Promise<void>;
|
|
98
|
+
startWatch: WatchStarter;
|
|
43
99
|
};
|
|
44
100
|
export declare function createTreeseedIntegratedDevPlan(options?: TreeseedIntegratedDevOptions): TreeseedIntegratedDevPlan;
|
|
45
101
|
export declare function runTreeseedIntegratedDev(options?: TreeseedIntegratedDevOptions, deps?: Partial<TreeseedIntegratedDevDependencies>): Promise<number>;
|
package/dist/dev.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import {
|
|
8
|
+
applyTreeseedEnvironmentToProcess,
|
|
9
|
+
assertTreeseedCommandEnvironment,
|
|
10
|
+
ensureLocalWorkspaceLinks,
|
|
11
|
+
findNearestTreeseedWorkspaceRoot,
|
|
12
|
+
resolveTreeseedToolBinary
|
|
13
|
+
} from "@treeseed/sdk/workflow-support";
|
|
7
14
|
const require2 = createRequire(import.meta.url);
|
|
8
15
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
16
|
const TREESEED_DEFAULT_WEB_HOST = "127.0.0.1";
|
|
@@ -11,6 +18,11 @@ const TREESEED_DEFAULT_WEB_PORT = 4321;
|
|
|
11
18
|
const TREESEED_DEFAULT_API_HOST = "127.0.0.1";
|
|
12
19
|
const TREESEED_DEFAULT_API_PORT = 3e3;
|
|
13
20
|
const TREESEED_DEFAULT_MANAGER_PORT = 3100;
|
|
21
|
+
const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
|
|
22
|
+
const WATCH_INTERVAL_MS = 900;
|
|
23
|
+
const WATCH_DEBOUNCE_MS = 350;
|
|
24
|
+
const DEFAULT_READINESS_TIMEOUT_MS = 9e4;
|
|
25
|
+
const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
|
|
14
26
|
function resolvePackageRoot(packageName, tenantRoot) {
|
|
15
27
|
const resolvedPath = require2.resolve(packageName, {
|
|
16
28
|
paths: [tenantRoot, packageRoot, process.cwd()]
|
|
@@ -39,6 +51,24 @@ function resolveNodeEntrypoint(packageDir, sourceRelativePath, distRelativePath)
|
|
|
39
51
|
args: [resolve(packageDir, distRelativePath)]
|
|
40
52
|
};
|
|
41
53
|
}
|
|
54
|
+
function resolveOptionalScriptEntrypoint(packageDir, sourceRelativePath, distRelativePath) {
|
|
55
|
+
const sourcePath = resolve(packageDir, sourceRelativePath);
|
|
56
|
+
const runTsPath = resolve(packageDir, "scripts", "run-ts.mjs");
|
|
57
|
+
if (existsSync(sourcePath) && existsSync(runTsPath)) {
|
|
58
|
+
return {
|
|
59
|
+
command: process.execPath,
|
|
60
|
+
args: [runTsPath, sourcePath]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const distPath = resolve(packageDir, distRelativePath);
|
|
64
|
+
if (existsSync(distPath)) {
|
|
65
|
+
return {
|
|
66
|
+
command: process.execPath,
|
|
67
|
+
args: [distPath]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
42
72
|
function resolveTenantApiEntrypoint(tenantRoot, runTsPath) {
|
|
43
73
|
const javascriptCandidates = [
|
|
44
74
|
resolve(tenantRoot, "src", "api", "server.js"),
|
|
@@ -67,10 +97,122 @@ function withWatchArgs(args, watchPaths) {
|
|
|
67
97
|
function normalizePort(value, fallback) {
|
|
68
98
|
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
69
99
|
}
|
|
100
|
+
function normalizeSetupMode(value) {
|
|
101
|
+
return value ?? "auto";
|
|
102
|
+
}
|
|
103
|
+
function normalizeFeedbackMode(value) {
|
|
104
|
+
return value ?? "live";
|
|
105
|
+
}
|
|
106
|
+
function normalizeOpenMode(value) {
|
|
107
|
+
return value ?? "auto";
|
|
108
|
+
}
|
|
109
|
+
function browserHost(host) {
|
|
110
|
+
return host === "0.0.0.0" || host === "::" || host === "[::]" ? "127.0.0.1" : host;
|
|
111
|
+
}
|
|
112
|
+
function webUrlFor(host, port) {
|
|
113
|
+
return `http://${browserHost(host)}:${port}`;
|
|
114
|
+
}
|
|
115
|
+
function createWatchEntries(tenantRoot, sdkPackageRoot) {
|
|
116
|
+
const entries = [
|
|
117
|
+
{ kind: "tenant", root: resolve(tenantRoot, "src") },
|
|
118
|
+
{ kind: "tenant", root: resolve(tenantRoot, "public") },
|
|
119
|
+
{ kind: "tenant", root: resolve(tenantRoot, "astro.config.ts") },
|
|
120
|
+
{ kind: "tenant", root: resolve(tenantRoot, "treeseed.site.yaml") }
|
|
121
|
+
];
|
|
122
|
+
if (!packageRoot.split(sep).includes("node_modules")) {
|
|
123
|
+
entries.push(
|
|
124
|
+
{ kind: "package", root: resolve(packageRoot, "src") },
|
|
125
|
+
{ kind: "package", root: resolve(packageRoot, "scripts") },
|
|
126
|
+
{ kind: "package", root: resolve(packageRoot, "package.json") }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (!sdkPackageRoot.split(sep).includes("node_modules")) {
|
|
130
|
+
entries.push(
|
|
131
|
+
{ kind: "sdk", root: resolve(sdkPackageRoot, "src") },
|
|
132
|
+
{ kind: "sdk", root: resolve(sdkPackageRoot, "scripts") },
|
|
133
|
+
{ kind: "sdk", root: resolve(sdkPackageRoot, "package.json") }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
function isSurfaceIncluded(plan, id) {
|
|
139
|
+
return plan.commands.some((command) => command.id === id);
|
|
140
|
+
}
|
|
141
|
+
function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env) {
|
|
142
|
+
if (setupMode === "off") {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
id: "setup-disabled",
|
|
146
|
+
label: "Local setup disabled",
|
|
147
|
+
required: false,
|
|
148
|
+
status: "skipped",
|
|
149
|
+
detail: "Run without --setup off to prepare workspace links, local D1 state, and generated dev artifacts."
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
const coreScripts = [
|
|
154
|
+
["starlight-patch", "Patch Starlight content path", "scripts/patch-starlight-content-path.ts", "dist/scripts/patch-starlight-content-path.js"],
|
|
155
|
+
["books", "Generate book/public artifacts", "scripts/aggregate-book.ts", "dist/scripts/aggregate-book.js"],
|
|
156
|
+
["worker-bundle", "Generate local worker bundle", "scripts/build-tenant-worker.ts", "dist/scripts/build-tenant-worker.js"]
|
|
157
|
+
];
|
|
158
|
+
const steps = [
|
|
159
|
+
{
|
|
160
|
+
id: "workspace-links",
|
|
161
|
+
label: "Ensure local workspace links",
|
|
162
|
+
required: setupMode === "auto",
|
|
163
|
+
status: "planned"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "wrangler",
|
|
167
|
+
label: "Verify Wrangler executable",
|
|
168
|
+
required: isSurfaceIncluded(planLike, "api"),
|
|
169
|
+
status: "planned",
|
|
170
|
+
detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
|
|
171
|
+
},
|
|
172
|
+
...coreScripts.map(([id, label, source, dist]) => {
|
|
173
|
+
const script = resolveOptionalScriptEntrypoint(packageRoot, source, dist);
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
label,
|
|
177
|
+
required: true,
|
|
178
|
+
command: script?.command,
|
|
179
|
+
args: script?.args,
|
|
180
|
+
status: script ? "planned" : "skipped",
|
|
181
|
+
detail: script ? void 0 : `Script not found at ${source}.`
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
184
|
+
{
|
|
185
|
+
id: "mailpit",
|
|
186
|
+
label: "Check optional Mailpit email runtime",
|
|
187
|
+
required: false,
|
|
188
|
+
status: "planned"
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
if (isSurfaceIncluded(planLike, "api") && existsSync(resolve(tenantRoot, "migrations"))) {
|
|
192
|
+
const migrate = resolveOptionalScriptEntrypoint(
|
|
193
|
+
sdkPackageRoot,
|
|
194
|
+
"scripts/tenant-d1-migrate-local.ts",
|
|
195
|
+
"dist/scripts/tenant-d1-migrate-local.js"
|
|
196
|
+
);
|
|
197
|
+
steps.push({
|
|
198
|
+
id: "d1-migrations",
|
|
199
|
+
label: "Run local D1 migrations",
|
|
200
|
+
required: true,
|
|
201
|
+
command: migrate?.command,
|
|
202
|
+
args: migrate?.args,
|
|
203
|
+
status: migrate ? "planned" : "failed",
|
|
204
|
+
detail: migrate ? void 0 : "Unable to resolve the local D1 migration script."
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return steps;
|
|
208
|
+
}
|
|
70
209
|
function createTreeseedIntegratedDevPlan(options = {}) {
|
|
71
210
|
const tenantRoot = resolve(options.cwd ?? process.cwd());
|
|
72
211
|
const surface = options.surface ?? "integrated";
|
|
73
|
-
const
|
|
212
|
+
const setupMode = normalizeSetupMode(options.setupMode);
|
|
213
|
+
const feedbackMode = normalizeFeedbackMode(options.feedbackMode);
|
|
214
|
+
const openMode = normalizeOpenMode(options.openMode);
|
|
215
|
+
const watch = feedbackMode !== "off" || options.watch === true;
|
|
74
216
|
const webHost = options.webHost ?? TREESEED_DEFAULT_WEB_HOST;
|
|
75
217
|
const webPort = normalizePort(options.webPort, TREESEED_DEFAULT_WEB_PORT);
|
|
76
218
|
const apiHost = options.apiHost ?? TREESEED_DEFAULT_API_HOST;
|
|
@@ -80,7 +222,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
80
222
|
const projectId = options.projectId ?? process.env.TREESEED_PROJECT_ID;
|
|
81
223
|
const teamId = options.teamId ?? process.env.TREESEED_HOSTING_TEAM_ID;
|
|
82
224
|
const mergedEnv = { ...process.env, ...options.env ?? {} };
|
|
83
|
-
const apiBaseUrl = mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
|
|
225
|
+
const apiBaseUrl = options.apiHost != null || options.apiPort != null ? `http://${apiHost}:${apiPort}` : mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
|
|
226
|
+
const webUrl = surface === "integrated" || surface === "web" ? webUrlFor(webHost, webPort) : null;
|
|
84
227
|
const sdkPackageRoot = resolvePackageRoot("@treeseed/sdk", tenantRoot);
|
|
85
228
|
const coreRunTsPath = resolve(packageRoot, "scripts", "run-ts.mjs");
|
|
86
229
|
const webEntrypoint = resolveNodeEntrypoint(
|
|
@@ -103,9 +246,11 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
103
246
|
"src/services/worker.ts",
|
|
104
247
|
"dist/services/worker.js"
|
|
105
248
|
);
|
|
249
|
+
const watchEntries = watch ? createWatchEntries(tenantRoot, sdkPackageRoot) : [];
|
|
106
250
|
const watchPaths = [
|
|
107
251
|
resolve(packageRoot, existsSync(resolve(packageRoot, "src")) ? "src" : "dist"),
|
|
108
252
|
resolve(tenantRoot, "src"),
|
|
253
|
+
resolve(tenantRoot, "public"),
|
|
109
254
|
resolve(tenantRoot, "treeseed.site.yaml"),
|
|
110
255
|
resolve(tenantRoot, "astro.config.ts")
|
|
111
256
|
];
|
|
@@ -115,9 +260,12 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
115
260
|
TREESEED_API_BASE_URL: apiBaseUrl,
|
|
116
261
|
TREESEED_MARKET_API_BASE_URL: mergedEnv.TREESEED_MARKET_API_BASE_URL ?? apiBaseUrl,
|
|
117
262
|
TREESEED_PROJECT_ID: projectId ?? mergedEnv.TREESEED_PROJECT_ID,
|
|
118
|
-
TREESEED_HOSTING_TEAM_ID: teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID
|
|
263
|
+
TREESEED_HOSTING_TEAM_ID: teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID,
|
|
264
|
+
TREESEED_API_D1_DATABASE_NAME: mergedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
|
|
265
|
+
SITE_DATA_DB: mergedEnv.SITE_DATA_DB ?? "SITE_DATA_DB",
|
|
266
|
+
TREESEED_API_D1_LOCAL_PERSIST_TO: mergedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO ?? resolve(tenantRoot, ".wrangler", "state", "v3", "d1")
|
|
119
267
|
};
|
|
120
|
-
if (watch) {
|
|
268
|
+
if (watch && feedbackMode === "live") {
|
|
121
269
|
sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
|
|
122
270
|
}
|
|
123
271
|
const commands = [];
|
|
@@ -140,7 +288,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
140
288
|
cwd: tenantRoot,
|
|
141
289
|
env: {
|
|
142
290
|
...sharedEnv,
|
|
143
|
-
PORT: sharedEnv.PORT ?? String(apiPort)
|
|
291
|
+
PORT: options.apiPort != null ? String(apiPort) : sharedEnv.PORT ?? String(apiPort)
|
|
144
292
|
}
|
|
145
293
|
});
|
|
146
294
|
}
|
|
@@ -153,8 +301,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
153
301
|
cwd: tenantRoot,
|
|
154
302
|
env: {
|
|
155
303
|
...sharedEnv,
|
|
156
|
-
PORT: sharedEnv.PORT ?? String(managerPort),
|
|
157
|
-
TREESEED_MANAGER_BASE_URL: sharedEnv.TREESEED_MANAGER_BASE_URL ?? `http://${apiHost}:${managerPort}`
|
|
304
|
+
PORT: options.managerPort != null ? String(managerPort) : sharedEnv.PORT ?? String(managerPort),
|
|
305
|
+
TREESEED_MANAGER_BASE_URL: options.managerPort != null ? `http://${apiHost}:${managerPort}` : sharedEnv.TREESEED_MANAGER_BASE_URL ?? `http://${apiHost}:${managerPort}`
|
|
158
306
|
}
|
|
159
307
|
});
|
|
160
308
|
}
|
|
@@ -168,10 +316,44 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
168
316
|
env: sharedEnv
|
|
169
317
|
});
|
|
170
318
|
}
|
|
319
|
+
const readyChecks = commands.map((command) => {
|
|
320
|
+
if (command.id === "web") {
|
|
321
|
+
return {
|
|
322
|
+
id: command.id,
|
|
323
|
+
label: command.label,
|
|
324
|
+
required: true,
|
|
325
|
+
strategy: "http",
|
|
326
|
+
url: webUrl ?? void 0
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (command.id === "api") {
|
|
330
|
+
return {
|
|
331
|
+
id: command.id,
|
|
332
|
+
label: command.label,
|
|
333
|
+
required: true,
|
|
334
|
+
strategy: "http",
|
|
335
|
+
url: `${apiBaseUrl.replace(/\/$/u, "")}/readyz`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
id: command.id,
|
|
340
|
+
label: command.label,
|
|
341
|
+
required: false,
|
|
342
|
+
strategy: "process"
|
|
343
|
+
};
|
|
344
|
+
});
|
|
171
345
|
return {
|
|
172
346
|
surface,
|
|
347
|
+
setupMode,
|
|
348
|
+
feedbackMode,
|
|
349
|
+
openMode,
|
|
350
|
+
watch,
|
|
173
351
|
tenantRoot,
|
|
174
352
|
apiBaseUrl,
|
|
353
|
+
webUrl,
|
|
354
|
+
setupSteps: createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, { commands }, sharedEnv),
|
|
355
|
+
readyChecks,
|
|
356
|
+
watchEntries,
|
|
175
357
|
commands
|
|
176
358
|
};
|
|
177
359
|
}
|
|
@@ -194,8 +376,370 @@ function stopChildProcess(child, signal = "SIGTERM") {
|
|
|
194
376
|
} catch {
|
|
195
377
|
}
|
|
196
378
|
}
|
|
379
|
+
function writeDevReloadStamp(projectRoot) {
|
|
380
|
+
const outputPath = resolve(projectRoot, DEV_RELOAD_FILE);
|
|
381
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
382
|
+
writeFileSync(
|
|
383
|
+
outputPath,
|
|
384
|
+
`${JSON.stringify(
|
|
385
|
+
{
|
|
386
|
+
buildId: `${Date.now()}`,
|
|
387
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
388
|
+
},
|
|
389
|
+
null,
|
|
390
|
+
2
|
|
391
|
+
)}
|
|
392
|
+
`,
|
|
393
|
+
"utf8"
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
function shouldIgnoreWatchPath(filePath, rootPath) {
|
|
397
|
+
const rel = relative(rootPath, filePath);
|
|
398
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === "..") {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
const normalized = rel.split(sep).join("/");
|
|
402
|
+
return normalized === ".git" || normalized.startsWith(".git/") || normalized === "node_modules" || normalized.startsWith("node_modules/") || normalized === ".astro" || normalized.startsWith(".astro/") || normalized === ".wrangler" || normalized.startsWith(".wrangler/") || normalized === ".local" || normalized.startsWith(".local/") || normalized === ".treeseed" || normalized.startsWith(".treeseed/") || normalized === "dist" || normalized.startsWith("dist/") || normalized === "coverage" || normalized.startsWith("coverage/") || normalized.startsWith("public/books/") || normalized.startsWith("public/__treeseed/");
|
|
403
|
+
}
|
|
404
|
+
function collectRootSnapshot(rootPath, snapshot) {
|
|
405
|
+
if (!existsSync(rootPath)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const stats = statSync(rootPath);
|
|
409
|
+
if (stats.isFile()) {
|
|
410
|
+
snapshot.set(rootPath, `${stats.mtimeMs}:${stats.size}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
for (const entry of readdirSync(rootPath, { withFileTypes: true })) {
|
|
414
|
+
const fullPath = resolve(rootPath, entry.name);
|
|
415
|
+
if (shouldIgnoreWatchPath(fullPath, rootPath)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (entry.isDirectory()) {
|
|
419
|
+
collectDirectorySnapshot(fullPath, rootPath, snapshot);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const entryStats = statSync(fullPath);
|
|
423
|
+
snapshot.set(fullPath, `${entryStats.mtimeMs}:${entryStats.size}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function collectDirectorySnapshot(directoryPath, rootPath, snapshot) {
|
|
427
|
+
if (shouldIgnoreWatchPath(directoryPath, rootPath)) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
|
|
431
|
+
const fullPath = resolve(directoryPath, entry.name);
|
|
432
|
+
if (shouldIgnoreWatchPath(fullPath, rootPath)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (entry.isDirectory()) {
|
|
436
|
+
collectDirectorySnapshot(fullPath, rootPath, snapshot);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const stats = statSync(fullPath);
|
|
440
|
+
snapshot.set(fullPath, `${stats.mtimeMs}:${stats.size}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function collectSnapshot(entries) {
|
|
444
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
collectRootSnapshot(entry.root, snapshot);
|
|
447
|
+
}
|
|
448
|
+
return snapshot;
|
|
449
|
+
}
|
|
450
|
+
function diffSnapshots(previousSnapshot, nextSnapshot) {
|
|
451
|
+
const changed = /* @__PURE__ */ new Set();
|
|
452
|
+
for (const [filePath, signature] of nextSnapshot.entries()) {
|
|
453
|
+
if (previousSnapshot.get(filePath) !== signature) {
|
|
454
|
+
changed.add(filePath);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const filePath of previousSnapshot.keys()) {
|
|
458
|
+
if (!nextSnapshot.has(filePath)) {
|
|
459
|
+
changed.add(filePath);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return [...changed];
|
|
463
|
+
}
|
|
464
|
+
function classifyChanges(changedPaths, watchEntries) {
|
|
465
|
+
function matchesEntry(filePath, entry) {
|
|
466
|
+
return filePath === entry.root || filePath.startsWith(`${entry.root}${sep}`);
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
changedPaths,
|
|
470
|
+
sdkChanged: changedPaths.some(
|
|
471
|
+
(filePath) => watchEntries.some((entry) => entry.kind === "sdk" && matchesEntry(filePath, entry))
|
|
472
|
+
),
|
|
473
|
+
packageChanged: changedPaths.some(
|
|
474
|
+
(filePath) => watchEntries.some((entry) => entry.kind === "package" && matchesEntry(filePath, entry))
|
|
475
|
+
),
|
|
476
|
+
tenantChanged: changedPaths.some(
|
|
477
|
+
(filePath) => watchEntries.some((entry) => entry.kind === "tenant" && matchesEntry(filePath, entry))
|
|
478
|
+
)
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function startPollingWatch({ watchEntries, onChange }) {
|
|
482
|
+
let previousSnapshot = collectSnapshot(watchEntries);
|
|
483
|
+
let queuedPaths = [];
|
|
484
|
+
let debounceTimer = null;
|
|
485
|
+
let running = false;
|
|
486
|
+
const intervalId = setInterval(() => {
|
|
487
|
+
const nextSnapshot = collectSnapshot(watchEntries);
|
|
488
|
+
const changedPaths = diffSnapshots(previousSnapshot, nextSnapshot);
|
|
489
|
+
previousSnapshot = nextSnapshot;
|
|
490
|
+
if (changedPaths.length === 0) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
queuedPaths.push(...changedPaths);
|
|
494
|
+
if (debounceTimer) {
|
|
495
|
+
clearTimeout(debounceTimer);
|
|
496
|
+
}
|
|
497
|
+
debounceTimer = setTimeout(() => {
|
|
498
|
+
void flush();
|
|
499
|
+
}, WATCH_DEBOUNCE_MS);
|
|
500
|
+
}, WATCH_INTERVAL_MS);
|
|
501
|
+
async function flush() {
|
|
502
|
+
if (running || queuedPaths.length === 0) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const changedPaths = [...new Set(queuedPaths)];
|
|
506
|
+
queuedPaths = [];
|
|
507
|
+
running = true;
|
|
508
|
+
try {
|
|
509
|
+
await onChange(classifyChanges(changedPaths, watchEntries));
|
|
510
|
+
} finally {
|
|
511
|
+
running = false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return () => {
|
|
515
|
+
if (debounceTimer) {
|
|
516
|
+
clearTimeout(debounceTimer);
|
|
517
|
+
}
|
|
518
|
+
clearInterval(intervalId);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function defaultWrite(line, stream) {
|
|
522
|
+
const target = stream === "stderr" ? process.stderr : process.stdout;
|
|
523
|
+
target.write(line);
|
|
524
|
+
}
|
|
525
|
+
function emitEvent(options, write, event, stream = event.type === "error" ? "stderr" : "stdout") {
|
|
526
|
+
if (options.json) {
|
|
527
|
+
write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.event", ...event })}
|
|
528
|
+
`, stream);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const surface = event.surface ? `[${event.surface}]` : event.type === "setup" ? "[setup]" : "[dev]";
|
|
532
|
+
const message = event.message ?? event.detail ?? event.status ?? "";
|
|
533
|
+
write(`${surface} ${String(message)}
|
|
534
|
+
`, stream);
|
|
535
|
+
}
|
|
536
|
+
function writePlan(plan, options, write) {
|
|
537
|
+
if (options.json) {
|
|
538
|
+
write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: plan }, null, 2)}
|
|
539
|
+
`, "stdout");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
write(`Treeseed dev plan
|
|
543
|
+
`, "stdout");
|
|
544
|
+
write(`surface: ${plan.surface}
|
|
545
|
+
`, "stdout");
|
|
546
|
+
write(`setup: ${plan.setupMode}
|
|
547
|
+
`, "stdout");
|
|
548
|
+
write(`feedback: ${plan.feedbackMode}
|
|
549
|
+
`, "stdout");
|
|
550
|
+
if (plan.webUrl) {
|
|
551
|
+
write(`web: ${plan.webUrl}
|
|
552
|
+
`, "stdout");
|
|
553
|
+
}
|
|
554
|
+
write(`api: ${plan.apiBaseUrl}
|
|
555
|
+
`, "stdout");
|
|
556
|
+
for (const command of plan.commands) {
|
|
557
|
+
write(`- ${command.id}: ${command.command} ${command.args.join(" ")}
|
|
558
|
+
`, "stdout");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function attachPrefixedLogReader(child, surface, options, write) {
|
|
562
|
+
function attach(stream, name) {
|
|
563
|
+
if (!stream || typeof stream.on !== "function") {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
let buffer = "";
|
|
567
|
+
stream.on("data", (chunk) => {
|
|
568
|
+
buffer += chunk.toString();
|
|
569
|
+
for (; ; ) {
|
|
570
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
571
|
+
if (newlineIndex < 0) {
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
const line = buffer.slice(0, newlineIndex);
|
|
575
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
576
|
+
if (options.json) {
|
|
577
|
+
emitEvent(options, write, { type: "log", surface, message: line, detail: { stream: name } }, name);
|
|
578
|
+
} else {
|
|
579
|
+
write(`[${surface}] ${line}
|
|
580
|
+
`, name);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
stream.on("end", () => {
|
|
585
|
+
if (buffer.length > 0) {
|
|
586
|
+
if (options.json) {
|
|
587
|
+
emitEvent(options, write, { type: "log", surface, message: buffer, detail: { stream: name } }, name);
|
|
588
|
+
} else {
|
|
589
|
+
write(`[${surface}] ${buffer}
|
|
590
|
+
`, name);
|
|
591
|
+
}
|
|
592
|
+
buffer = "";
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
attach(child.stdout ?? null, "stdout");
|
|
597
|
+
attach(child.stderr ?? null, "stderr");
|
|
598
|
+
}
|
|
599
|
+
function runSetupStep(step, plan, deps) {
|
|
600
|
+
if (!step.command || !step.args) {
|
|
601
|
+
return {
|
|
602
|
+
...step,
|
|
603
|
+
status: step.status === "failed" ? "failed" : "skipped"
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const result = deps.spawnSync(step.command, step.args, {
|
|
607
|
+
cwd: plan.tenantRoot,
|
|
608
|
+
env: {
|
|
609
|
+
...process.env,
|
|
610
|
+
...plan.commands[0]?.env,
|
|
611
|
+
TREESEED_LOCAL_DEV_MODE: "cloudflare",
|
|
612
|
+
TREESEED_PUBLIC_DEV_WATCH_RELOAD: plan.feedbackMode === "live" ? "true" : process.env.TREESEED_PUBLIC_DEV_WATCH_RELOAD
|
|
613
|
+
},
|
|
614
|
+
encoding: "utf8"
|
|
615
|
+
});
|
|
616
|
+
if ((result.status ?? 1) === 0) {
|
|
617
|
+
return {
|
|
618
|
+
...step,
|
|
619
|
+
status: "completed",
|
|
620
|
+
detail: [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || step.detail
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
...step,
|
|
625
|
+
status: step.required ? "failed" : "degraded",
|
|
626
|
+
detail: [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || `Exited with ${result.status ?? 1}.`
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function runLocalSetup(plan, options, deps) {
|
|
630
|
+
const results = [];
|
|
631
|
+
if (plan.setupMode === "off") {
|
|
632
|
+
for (const step of plan.setupSteps) {
|
|
633
|
+
results.push(step);
|
|
634
|
+
emitEvent(options, deps.write, { type: "setup", status: step.status, message: `${step.label}: ${step.status}`, detail: step.detail });
|
|
635
|
+
}
|
|
636
|
+
return results;
|
|
637
|
+
}
|
|
638
|
+
for (const step of plan.setupSteps) {
|
|
639
|
+
let result = step;
|
|
640
|
+
if (step.id === "workspace-links") {
|
|
641
|
+
if (plan.setupMode === "check") {
|
|
642
|
+
result = { ...step, status: "skipped", detail: "Workspace links were checked in non-mutating mode." };
|
|
643
|
+
} else {
|
|
644
|
+
const workspaceRoot = findNearestTreeseedWorkspaceRoot(plan.tenantRoot);
|
|
645
|
+
if (workspaceRoot) {
|
|
646
|
+
const links = ensureLocalWorkspaceLinks(workspaceRoot, {
|
|
647
|
+
mode: "auto",
|
|
648
|
+
env: { ...process.env, ...plan.commands[0]?.env }
|
|
649
|
+
});
|
|
650
|
+
result = {
|
|
651
|
+
...step,
|
|
652
|
+
status: links.issues.length > 0 ? "failed" : "completed",
|
|
653
|
+
detail: links.issues.length > 0 ? links.issues.join("; ") : `Verified ${links.links.length} workspace link${links.links.length === 1 ? "" : "s"}.`
|
|
654
|
+
};
|
|
655
|
+
} else {
|
|
656
|
+
result = { ...step, status: "skipped", detail: "No Treeseed workspace root found." };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} else if (step.id === "wrangler") {
|
|
660
|
+
const wrangler = resolveTreeseedToolBinary("wrangler", { env: { ...process.env, ...plan.commands[0]?.env } });
|
|
661
|
+
result = wrangler ? { ...step, status: "completed", detail: wrangler } : {
|
|
662
|
+
...step,
|
|
663
|
+
status: step.required ? "failed" : "degraded",
|
|
664
|
+
detail: "Wrangler was not found. Run `npx trsd install --json` and retry `npx trsd dev`."
|
|
665
|
+
};
|
|
666
|
+
} else if (step.id === "mailpit") {
|
|
667
|
+
const docker = resolveTreeseedToolBinary("docker", { env: { ...process.env, ...plan.commands[0]?.env } });
|
|
668
|
+
result = docker ? { ...step, status: "completed", detail: `Docker detected at ${docker}; Mailpit remains optional for local dev.` } : { ...step, status: "degraded", detail: "Docker is unavailable, so Mailpit email previews are disabled." };
|
|
669
|
+
} else if (plan.setupMode === "check") {
|
|
670
|
+
result = { ...step, status: step.status === "failed" ? "failed" : "skipped", detail: step.detail ?? "Skipped in setup check mode." };
|
|
671
|
+
} else {
|
|
672
|
+
result = runSetupStep(step, plan, deps);
|
|
673
|
+
}
|
|
674
|
+
results.push(result);
|
|
675
|
+
emitEvent(options, deps.write, {
|
|
676
|
+
type: "setup",
|
|
677
|
+
status: result.status,
|
|
678
|
+
message: `${result.label}: ${result.status}`,
|
|
679
|
+
detail: result.detail
|
|
680
|
+
}, result.status === "failed" ? "stderr" : "stdout");
|
|
681
|
+
}
|
|
682
|
+
const failedRequired = results.some((step) => step.required && step.status === "failed");
|
|
683
|
+
if (plan.feedbackMode === "live" && plan.setupMode === "auto" && !failedRequired) {
|
|
684
|
+
writeDevReloadStamp(plan.tenantRoot);
|
|
685
|
+
emitEvent(options, deps.write, { type: "reload", message: "Wrote initial browser reload stamp." });
|
|
686
|
+
}
|
|
687
|
+
return results;
|
|
688
|
+
}
|
|
689
|
+
async function fetchOk(fetchFn, url, timeoutMs) {
|
|
690
|
+
const controller = new AbortController();
|
|
691
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
692
|
+
try {
|
|
693
|
+
const response = await fetchFn(url, { signal: controller.signal });
|
|
694
|
+
return response.ok;
|
|
695
|
+
} catch {
|
|
696
|
+
return false;
|
|
697
|
+
} finally {
|
|
698
|
+
clearTimeout(timeout);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function waitForHttpReady(fetchFn, url, timeoutMs) {
|
|
702
|
+
const startedAt = Date.now();
|
|
703
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
704
|
+
if (await fetchOk(fetchFn, url, 2e3)) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
await delay(500);
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
async function defaultOpenBrowser(url) {
|
|
712
|
+
const platform = process.platform;
|
|
713
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
714
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
715
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
716
|
+
child.unref();
|
|
717
|
+
}
|
|
718
|
+
function shouldOpenBrowser(plan) {
|
|
719
|
+
if (!plan.webUrl || plan.openMode === "off") {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
if (plan.openMode === "on") {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
return process.stdout.isTTY === true && process.env.CI !== "true";
|
|
726
|
+
}
|
|
727
|
+
function failedSetupMessage(failed) {
|
|
728
|
+
return [
|
|
729
|
+
`${failed.label} failed.`,
|
|
730
|
+
failed.detail ? String(failed.detail) : null,
|
|
731
|
+
"Run `npx trsd install --json` if a managed executable is missing, then retry `npx trsd dev --setup auto`."
|
|
732
|
+
].filter(Boolean).join(" ");
|
|
733
|
+
}
|
|
197
734
|
async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
198
735
|
const tenantRoot = resolve(options.cwd ?? process.cwd());
|
|
736
|
+
const write = deps.write ?? defaultWrite;
|
|
737
|
+
const spawnProcess = deps.spawn ?? spawn;
|
|
738
|
+
const spawnSyncProcess = deps.spawnSync ?? spawnSync;
|
|
739
|
+
const onSignal = deps.onSignal ?? defaultSignalRegistrar;
|
|
740
|
+
const fetchFn = deps.fetch ?? globalThis.fetch.bind(globalThis);
|
|
741
|
+
const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
|
|
742
|
+
const startWatch = deps.startWatch ?? startPollingWatch;
|
|
199
743
|
const prepareEnvironment = deps.prepareEnvironment ?? defaultPrepareEnvironment;
|
|
200
744
|
prepareEnvironment(tenantRoot);
|
|
201
745
|
const plan = createTreeseedIntegratedDevPlan({
|
|
@@ -206,10 +750,22 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
206
750
|
...options.env ?? {}
|
|
207
751
|
}
|
|
208
752
|
});
|
|
209
|
-
|
|
210
|
-
|
|
753
|
+
if (options.plan) {
|
|
754
|
+
writePlan(plan, options, write);
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
|
|
758
|
+
const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
|
|
759
|
+
if (failedSetup) {
|
|
760
|
+
emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup), detail: failedSetup });
|
|
761
|
+
return 1;
|
|
762
|
+
}
|
|
211
763
|
const children = /* @__PURE__ */ new Map();
|
|
764
|
+
const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
|
|
765
|
+
const exited = /* @__PURE__ */ new Map();
|
|
766
|
+
let stopWatching = null;
|
|
212
767
|
let settled = false;
|
|
768
|
+
let restarting = false;
|
|
213
769
|
return await new Promise((resolveExitCode) => {
|
|
214
770
|
const disposers = [
|
|
215
771
|
onSignal("SIGINT", () => finalize(130)),
|
|
@@ -220,6 +776,10 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
220
776
|
return;
|
|
221
777
|
}
|
|
222
778
|
settled = true;
|
|
779
|
+
if (stopWatching) {
|
|
780
|
+
stopWatching();
|
|
781
|
+
stopWatching = null;
|
|
782
|
+
}
|
|
223
783
|
for (const [childId, child] of children.entries()) {
|
|
224
784
|
if (childId !== originId) {
|
|
225
785
|
stopChildProcess(child);
|
|
@@ -228,20 +788,149 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
228
788
|
for (const dispose of disposers) {
|
|
229
789
|
dispose();
|
|
230
790
|
}
|
|
791
|
+
emitEvent(options, write, { type: "shutdown", exitCode, message: `Dev runtime stopped with exit code ${exitCode}.` }, exitCode === 0 ? "stdout" : "stderr");
|
|
231
792
|
resolveExitCode(exitCode);
|
|
232
793
|
}
|
|
233
|
-
|
|
794
|
+
function spawnCommand(command) {
|
|
795
|
+
emitEvent(options, write, {
|
|
796
|
+
type: "spawn",
|
|
797
|
+
surface: command.id,
|
|
798
|
+
command: command.command,
|
|
799
|
+
args: command.args,
|
|
800
|
+
message: `Starting ${command.label}.`
|
|
801
|
+
});
|
|
234
802
|
const child = spawnProcess(command.command, command.args, {
|
|
235
803
|
cwd: command.cwd,
|
|
236
804
|
env: command.env,
|
|
237
|
-
stdio: options.stdio ?? "inherit"
|
|
805
|
+
stdio: options.stdio ?? ["inherit", "pipe", "pipe"],
|
|
806
|
+
detached: false
|
|
238
807
|
});
|
|
239
808
|
children.set(command.id, child);
|
|
809
|
+
attachPrefixedLogReader(child, command.id, options, write);
|
|
240
810
|
child.on("exit", (code, signal) => {
|
|
811
|
+
children.delete(command.id);
|
|
812
|
+
exited.set(command.id, { code, signal });
|
|
813
|
+
if (restarting || settled) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
241
816
|
const exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : code ?? 0;
|
|
817
|
+
emitEvent(options, write, {
|
|
818
|
+
type: exitCode === 0 ? "shutdown" : "error",
|
|
819
|
+
surface: command.id,
|
|
820
|
+
exitCode,
|
|
821
|
+
signal,
|
|
822
|
+
message: `${command.label} exited with ${signal ?? exitCode}.`
|
|
823
|
+
}, exitCode === 0 ? "stdout" : "stderr");
|
|
242
824
|
finalize(exitCode, command.id);
|
|
243
825
|
});
|
|
826
|
+
return child;
|
|
827
|
+
}
|
|
828
|
+
async function restartCommand(id) {
|
|
829
|
+
const command = commandsById.get(id);
|
|
830
|
+
if (!command || settled) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const current = children.get(id);
|
|
834
|
+
restarting = true;
|
|
835
|
+
if (current) {
|
|
836
|
+
stopChildProcess(current);
|
|
837
|
+
await delay(350);
|
|
838
|
+
}
|
|
839
|
+
children.delete(id);
|
|
840
|
+
exited.delete(id);
|
|
841
|
+
restarting = false;
|
|
842
|
+
spawnCommand(command);
|
|
843
|
+
emitEvent(options, write, { type: "restart", surface: id, message: `Restarted ${command.label}.` });
|
|
844
|
+
}
|
|
845
|
+
async function waitForReadiness() {
|
|
846
|
+
const readinessTimeoutMs = options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
|
|
847
|
+
const processReadyGraceMs = options.processReadyGraceMs ?? DEFAULT_PROCESS_READY_GRACE_MS;
|
|
848
|
+
for (const check of plan.readyChecks) {
|
|
849
|
+
if (settled) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
let ready = false;
|
|
853
|
+
if (check.strategy === "http" && check.url) {
|
|
854
|
+
ready = await waitForHttpReady(fetchFn, check.url, readinessTimeoutMs);
|
|
855
|
+
} else {
|
|
856
|
+
await delay(processReadyGraceMs);
|
|
857
|
+
ready = !exited.has(check.id);
|
|
858
|
+
}
|
|
859
|
+
if (settled) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (!ready && check.required) {
|
|
863
|
+
emitEvent(options, write, {
|
|
864
|
+
type: "error",
|
|
865
|
+
surface: check.id,
|
|
866
|
+
url: check.url,
|
|
867
|
+
message: `${check.label} did not become ready${check.url ? ` at ${check.url}` : ""}.`
|
|
868
|
+
});
|
|
869
|
+
finalize(1, check.id);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
emitEvent(options, write, {
|
|
873
|
+
type: "ready",
|
|
874
|
+
surface: check.id,
|
|
875
|
+
status: ready ? "ready" : "degraded",
|
|
876
|
+
url: check.url,
|
|
877
|
+
message: `${check.label} is ${ready ? "ready" : "degraded"}.`
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (plan.webUrl) {
|
|
881
|
+
emitEvent(options, write, { type: "ready", url: plan.webUrl, message: `Treeseed dev ready at ${plan.webUrl}.` });
|
|
882
|
+
}
|
|
883
|
+
if (shouldOpenBrowser(plan)) {
|
|
884
|
+
try {
|
|
885
|
+
await openBrowser(plan.webUrl);
|
|
886
|
+
emitEvent(options, write, { type: "open", url: plan.webUrl, message: `Opened ${plan.webUrl}.` });
|
|
887
|
+
} catch (error) {
|
|
888
|
+
emitEvent(options, write, {
|
|
889
|
+
type: "open",
|
|
890
|
+
status: "degraded",
|
|
891
|
+
url: plan.webUrl,
|
|
892
|
+
message: `Could not open ${plan.webUrl}.`,
|
|
893
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
for (const command of plan.commands) {
|
|
899
|
+
spawnCommand(command);
|
|
900
|
+
}
|
|
901
|
+
if (plan.watchEntries.length > 0 && plan.feedbackMode !== "off") {
|
|
902
|
+
stopWatching = startWatch({
|
|
903
|
+
watchEntries: plan.watchEntries,
|
|
904
|
+
onChange: async (change) => {
|
|
905
|
+
if (settled) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
emitEvent(options, write, {
|
|
909
|
+
type: "restart",
|
|
910
|
+
message: `Detected ${change.changedPaths.length} change${change.changedPaths.length === 1 ? "" : "s"}.`,
|
|
911
|
+
detail: {
|
|
912
|
+
tenantChanged: change.tenantChanged,
|
|
913
|
+
packageChanged: change.packageChanged,
|
|
914
|
+
sdkChanged: change.sdkChanged
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
if (change.packageChanged || change.sdkChanged) {
|
|
918
|
+
await Promise.all([
|
|
919
|
+
restartCommand("api"),
|
|
920
|
+
restartCommand("manager"),
|
|
921
|
+
restartCommand("worker")
|
|
922
|
+
]);
|
|
923
|
+
} else if (change.tenantChanged) {
|
|
924
|
+
await restartCommand("api");
|
|
925
|
+
}
|
|
926
|
+
if (plan.feedbackMode === "live") {
|
|
927
|
+
writeDevReloadStamp(plan.tenantRoot);
|
|
928
|
+
emitEvent(options, write, { type: "reload", message: "Wrote browser reload stamp." });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
});
|
|
244
932
|
}
|
|
933
|
+
void waitForReadiness();
|
|
245
934
|
});
|
|
246
935
|
}
|
|
247
936
|
export {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runTreeseedIntegratedDev } from '../dev.js';
|
|
2
|
+
import { runTreeseedIntegratedDev, } from '../dev.js';
|
|
3
3
|
const args = process.argv.slice(2);
|
|
4
4
|
function readFlag(name) {
|
|
5
5
|
return args.includes(name);
|
|
@@ -11,6 +11,14 @@ function readOption(name) {
|
|
|
11
11
|
}
|
|
12
12
|
return args[index + 1];
|
|
13
13
|
}
|
|
14
|
+
function readNumberOption(name) {
|
|
15
|
+
const value = readOption(name);
|
|
16
|
+
if (!value) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
21
|
+
}
|
|
14
22
|
function parseSurface(value) {
|
|
15
23
|
if (value === 'web'
|
|
16
24
|
|| value === 'api'
|
|
@@ -22,9 +30,37 @@ function parseSurface(value) {
|
|
|
22
30
|
}
|
|
23
31
|
return 'integrated';
|
|
24
32
|
}
|
|
33
|
+
function parseSetupMode(value) {
|
|
34
|
+
if (value === 'auto' || value === 'check' || value === 'off') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function parseFeedbackMode(value) {
|
|
40
|
+
if (value === 'live' || value === 'restart' || value === 'off') {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function parseOpenMode(value) {
|
|
46
|
+
if (value === 'auto' || value === 'on' || value === 'off') {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
25
51
|
const exitCode = await runTreeseedIntegratedDev({
|
|
26
52
|
surface: parseSurface(readOption('--surface')),
|
|
27
53
|
watch: readFlag('--watch'),
|
|
54
|
+
webHost: readOption('--host'),
|
|
55
|
+
webPort: readNumberOption('--port'),
|
|
56
|
+
apiHost: readOption('--api-host'),
|
|
57
|
+
apiPort: readNumberOption('--api-port'),
|
|
58
|
+
managerPort: readNumberOption('--manager-port'),
|
|
59
|
+
setupMode: parseSetupMode(readOption('--setup')),
|
|
60
|
+
feedbackMode: parseFeedbackMode(readOption('--feedback')),
|
|
61
|
+
openMode: parseOpenMode(readOption('--open')),
|
|
62
|
+
plan: readFlag('--plan'),
|
|
63
|
+
json: readFlag('--json'),
|
|
28
64
|
projectId: readOption('--project-id'),
|
|
29
65
|
teamId: readOption('--team-id'),
|
|
30
66
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.16",
|
|
4
4
|
"description": "Treeseed integrated platform starter for Astro/Starlight web runtimes and Hono API runtimes.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@astrojs/sitemap": "3.7.0",
|
|
77
77
|
"@astrojs/starlight": "0.37.6",
|
|
78
78
|
"@tailwindcss/vite": "^4.1.4",
|
|
79
|
-
"@treeseed/sdk": "0.6.
|
|
79
|
+
"@treeseed/sdk": "0.6.14",
|
|
80
80
|
"astro": "^5.6.1",
|
|
81
81
|
"esbuild": "^0.28.0",
|
|
82
82
|
"hono": "^4.8.2",
|