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