@treeseed/core 0.10.20 → 0.10.21
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/README.md +16 -0
- package/dist/dev.d.ts +35 -0
- package/dist/dev.js +548 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/platform.d.ts +1 -1
- package/dist/platform.js +3 -1
- package/dist/scripts/dev-platform.js +52 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,6 +71,22 @@ What they do:
|
|
|
71
71
|
- `build`: builds the internal fixture app in production-like mode
|
|
72
72
|
- `test:smoke`: runs the packed-install smoke test
|
|
73
73
|
|
|
74
|
+
### Integrated managed dev
|
|
75
|
+
|
|
76
|
+
The published Core runtime also owns the integrated Treeseed dev supervisor used by the installable CLI:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx trsd dev
|
|
80
|
+
npx trsd dev start --web-runtime local --json
|
|
81
|
+
npx trsd dev status --all --json
|
|
82
|
+
npx trsd dev logs --follow
|
|
83
|
+
npx trsd dev stop --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`trsd dev` delegates to Core and runs the foreground supervisor. `trsd dev start` launches the same runtime as a worktree-scoped managed background instance, writing authoritative instance state under `.treeseed/dev/instances`, PID files under `.treeseed/dev/pids`, and logs under `.treeseed/logs`. The repository-family index under the git common dir is discovery-only and points back to those worktree-local records.
|
|
87
|
+
|
|
88
|
+
Core should keep this runtime reusable by the CLI and by the root Market workspace. Do not duplicate process, port, PID, or log management in package-local callers.
|
|
89
|
+
|
|
74
90
|
### Full verification
|
|
75
91
|
|
|
76
92
|
```bash
|
package/dist/dev.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type TreeseedIntegratedDevOptions = {
|
|
|
43
43
|
plan?: boolean;
|
|
44
44
|
reset?: boolean;
|
|
45
45
|
force?: boolean;
|
|
46
|
+
forceConflicts?: boolean;
|
|
46
47
|
json?: boolean;
|
|
47
48
|
includeServices?: boolean;
|
|
48
49
|
projectId?: string;
|
|
@@ -51,6 +52,12 @@ export type TreeseedIntegratedDevOptions = {
|
|
|
51
52
|
processReadyGraceMs?: number;
|
|
52
53
|
shutdownGraceMs?: number;
|
|
53
54
|
};
|
|
55
|
+
export type TreeseedManagedDevAction = 'start' | 'status' | 'logs' | 'stop' | 'restart';
|
|
56
|
+
export type TreeseedManagedDevOptions = TreeseedIntegratedDevOptions & {
|
|
57
|
+
action: TreeseedManagedDevAction;
|
|
58
|
+
all?: boolean;
|
|
59
|
+
follow?: boolean;
|
|
60
|
+
};
|
|
54
61
|
export type TreeseedIntegratedDevCommand = {
|
|
55
62
|
id: TreeseedIntegratedDevCommandId;
|
|
56
63
|
label: string;
|
|
@@ -114,6 +121,29 @@ export type TreeseedIntegratedDevPlan = {
|
|
|
114
121
|
};
|
|
115
122
|
reset: TreeseedIntegratedDevResetPlan | null;
|
|
116
123
|
};
|
|
124
|
+
export type TreeseedDevInstanceStatus = 'starting' | 'ready' | 'degraded' | 'stopped' | 'stale';
|
|
125
|
+
export type TreeseedDevInstanceRecord = {
|
|
126
|
+
schemaVersion: 1;
|
|
127
|
+
kind: 'treeseed.dev.instance';
|
|
128
|
+
projectRoot: string;
|
|
129
|
+
worktreeRoot: string;
|
|
130
|
+
branch: string | null;
|
|
131
|
+
gitCommonDir: string | null;
|
|
132
|
+
status: TreeseedDevInstanceStatus;
|
|
133
|
+
pid: number | null;
|
|
134
|
+
processGroupId: number | null;
|
|
135
|
+
startedAt: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
ports: Record<string, number>;
|
|
138
|
+
urls: Record<string, string>;
|
|
139
|
+
logPath: string;
|
|
140
|
+
runtimeScope: string;
|
|
141
|
+
surfaces: TreeseedIntegratedDevCommandId[];
|
|
142
|
+
readyChecks: TreeseedIntegratedDevReadinessCheck[];
|
|
143
|
+
instancePath: string;
|
|
144
|
+
pidPath: string;
|
|
145
|
+
staleReason?: string;
|
|
146
|
+
};
|
|
117
147
|
type SpawnLike = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
118
148
|
type SpawnSyncLike = typeof spawnSync;
|
|
119
149
|
type SignalRegistrar = (signal: NodeJS.Signals, handler: () => void) => () => void;
|
|
@@ -138,6 +168,7 @@ type TreeseedIntegratedDevDependencies = {
|
|
|
138
168
|
stopMarketPostgres: () => boolean;
|
|
139
169
|
inspectPortOwners: (ports: readonly number[]) => TreeseedDevPortOwner[];
|
|
140
170
|
};
|
|
171
|
+
type ManagedStartDependencies = Pick<TreeseedIntegratedDevDependencies, 'spawn' | 'write' | 'fetch' | 'processIsAlive' | 'killProcess' | 'inspectPortOwners'>;
|
|
141
172
|
export type TreeseedDevPortOwner = {
|
|
142
173
|
port: number;
|
|
143
174
|
pid: number | null;
|
|
@@ -151,6 +182,10 @@ export declare function createTreeseedIntegratedDevResetPlan(options: {
|
|
|
151
182
|
enabled?: boolean;
|
|
152
183
|
}): TreeseedIntegratedDevResetPlan | null;
|
|
153
184
|
export declare function createTreeseedIntegratedDevPlan(options?: TreeseedIntegratedDevOptions): TreeseedIntegratedDevPlan;
|
|
185
|
+
export declare function runTreeseedManagedDev(options: TreeseedManagedDevOptions, deps?: Partial<ManagedStartDependencies> & {
|
|
186
|
+
supervisorCommand?: string;
|
|
187
|
+
supervisorArgs?: string[];
|
|
188
|
+
}): Promise<number>;
|
|
154
189
|
export declare function runTreeseedIntegratedDevReset(reset: TreeseedIntegratedDevResetPlan | null, options: Pick<TreeseedIntegratedDevOptions, 'json'>, deps: Pick<TreeseedIntegratedDevDependencies, 'write' | 'removePath' | 'stopMailpitContainers' | 'resetMarketPostgres'>): {
|
|
155
190
|
actions: TreeseedIntegratedDevResetAction[];
|
|
156
191
|
enabled: boolean;
|
package/dist/dev.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { homedir } from "node:os";
|
|
4
6
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -38,6 +40,9 @@ const TREESEED_DEFAULT_MARKET_POSTGRES_URL = `postgres://treeseed:treeseed@127.0
|
|
|
38
40
|
const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
|
|
39
41
|
const DEV_RUNTIME_DIR = ".treeseed/generated/dev";
|
|
40
42
|
const DEV_RUNTIME_LEGACY_FILE = ".treeseed/generated/dev/runtime.json";
|
|
43
|
+
const DEV_INSTANCE_DIR = ".treeseed/dev/instances";
|
|
44
|
+
const DEV_PID_DIR = ".treeseed/dev/pids";
|
|
45
|
+
const DEV_REPO_INDEX_RELATIVE_PATH = "treeseed/dev-index.json";
|
|
41
46
|
const DEFAULT_READINESS_TIMEOUT_MS = 9e4;
|
|
42
47
|
const DEFAULT_SETUP_STEP_TIMEOUT_MS = 3e5;
|
|
43
48
|
const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
|
|
@@ -176,6 +181,7 @@ function webUrlFor(host, port) {
|
|
|
176
181
|
return `http://${browserHost(host)}:${port}`;
|
|
177
182
|
}
|
|
178
183
|
const CANONICAL_COMMAND_IDS = ["web", "api", "manager", "worker", "agents"];
|
|
184
|
+
const ALL_COMMAND_IDS = ["web", "api", "manager", "worker", "agents", "market-runner"];
|
|
179
185
|
const MARKET_DEV_COMMAND_IDS = ["web", "api", "market-runner"];
|
|
180
186
|
function isMarketWorkspace(tenantRoot) {
|
|
181
187
|
try {
|
|
@@ -726,8 +732,11 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
726
732
|
const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID ?? resolveSeededLocalProjectId(localD1PersistTo);
|
|
727
733
|
const resolvedHostingTeamId = teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
|
|
728
734
|
const resolvedTeamId = mergedEnv.TREESEED_TEAM_ID ?? resolvedHostingTeamId ?? resolveSeededLocalTeamId(localD1PersistTo, projectId ?? null);
|
|
729
|
-
const
|
|
735
|
+
const marketPostgresPort = mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_PORT ?? String(TREESEED_DEFAULT_MARKET_POSTGRES_PORT);
|
|
736
|
+
const marketDatabaseUrl = mergedEnv.TREESEED_MARKET_DATABASE_URL ?? `postgres://treeseed:treeseed@127.0.0.1:${marketPostgresPort}/market_local`;
|
|
730
737
|
const managedMarketPostgres = marketWorkspace && isTreeseedManagedMarketPostgresUrl(marketDatabaseUrl);
|
|
738
|
+
const mailpitSmtpPort = mergedEnv.TREESEED_MAILPIT_SMTP_PORT ?? String(TREESEED_DEFAULT_LOCAL_SMTP_PORT);
|
|
739
|
+
const mailpitUiPort = mergedEnv.TREESEED_MAILPIT_UI_PORT ?? String(TREESEED_DEFAULT_MAILPIT_UI_PORT);
|
|
731
740
|
const webEntrypoint = resolveNodeEntrypoint(
|
|
732
741
|
sdkPackageRoot,
|
|
733
742
|
"scripts/tenant-astro-command.ts",
|
|
@@ -762,7 +771,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
762
771
|
TREESEED_MARKET_DATABASE_URL: marketDatabaseUrl,
|
|
763
772
|
TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER ?? TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER,
|
|
764
773
|
TREESEED_MARKET_LOCAL_POSTGRES_VOLUME: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_VOLUME ?? TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME,
|
|
765
|
-
TREESEED_MARKET_LOCAL_POSTGRES_PORT:
|
|
774
|
+
TREESEED_MARKET_LOCAL_POSTGRES_PORT: marketPostgresPort,
|
|
766
775
|
TREESEED_MARKET_LOCAL_POSTGRES_MANAGED: managedMarketPostgres ? "true" : "false"
|
|
767
776
|
} : {},
|
|
768
777
|
TREESEED_PROJECT_ID: projectId ?? mergedEnv.TREESEED_PROJECT_ID,
|
|
@@ -780,12 +789,13 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
780
789
|
TREESEED_BETTER_AUTH_SECRET: mergedEnv.TREESEED_BETTER_AUTH_SECRET ?? "treeseed-local-better-auth-secret-minimum-32-characters",
|
|
781
790
|
...devResetId ? { TREESEED_DEV_RESET_ID: devResetId } : {},
|
|
782
791
|
TREESEED_SMTP_HOST: TREESEED_DEFAULT_LOCAL_SMTP_HOST,
|
|
783
|
-
TREESEED_SMTP_PORT:
|
|
792
|
+
TREESEED_SMTP_PORT: mailpitSmtpPort,
|
|
784
793
|
TREESEED_SMTP_USERNAME: "",
|
|
785
794
|
TREESEED_SMTP_PASSWORD: "",
|
|
786
795
|
TREESEED_MAILPIT_SMTP_HOST: TREESEED_DEFAULT_LOCAL_SMTP_HOST,
|
|
787
|
-
TREESEED_MAILPIT_SMTP_PORT:
|
|
788
|
-
TREESEED_MAILPIT_UI_PORT:
|
|
796
|
+
TREESEED_MAILPIT_SMTP_PORT: mailpitSmtpPort,
|
|
797
|
+
TREESEED_MAILPIT_UI_PORT: mailpitUiPort,
|
|
798
|
+
TREESEED_MAILPIT_CONTAINER_NAME: mergedEnv.TREESEED_MAILPIT_CONTAINER_NAME,
|
|
789
799
|
TREESEED_AUTH_EMAIL_FROM: mergedEnv.TREESEED_AUTH_EMAIL_FROM ?? "Treeseed Market <auth@treeseed.local>"
|
|
790
800
|
};
|
|
791
801
|
const reset = createTreeseedIntegratedDevResetPlan({
|
|
@@ -1033,6 +1043,505 @@ function resolveLocalMachineEnv(tenantRoot) {
|
|
|
1033
1043
|
return {};
|
|
1034
1044
|
}
|
|
1035
1045
|
}
|
|
1046
|
+
function atomicWriteJson(path, value) {
|
|
1047
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1048
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
1049
|
+
writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}
|
|
1050
|
+
`, "utf8");
|
|
1051
|
+
renameSync(tmpPath, path);
|
|
1052
|
+
}
|
|
1053
|
+
function runGitText(cwd, args) {
|
|
1054
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
1055
|
+
return (result.status ?? 1) === 0 ? String(result.stdout ?? "").trim() : null;
|
|
1056
|
+
}
|
|
1057
|
+
function resolveGitWorktreeInfo(tenantRoot) {
|
|
1058
|
+
const worktreeRoot = runGitText(tenantRoot, ["rev-parse", "--show-toplevel"]) ?? tenantRoot;
|
|
1059
|
+
const rawCommonDir = runGitText(tenantRoot, ["rev-parse", "--git-common-dir"]);
|
|
1060
|
+
const gitCommonDir = rawCommonDir ? isAbsolute(rawCommonDir) ? rawCommonDir : resolve(tenantRoot, rawCommonDir) : null;
|
|
1061
|
+
const rawBranch = runGitText(tenantRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1062
|
+
const branch = rawBranch && rawBranch !== "HEAD" ? rawBranch : null;
|
|
1063
|
+
return { worktreeRoot, gitCommonDir, branch };
|
|
1064
|
+
}
|
|
1065
|
+
function repositoryIndexId(tenantRoot, gitCommonDir) {
|
|
1066
|
+
const source = gitCommonDir ?? tenantRoot;
|
|
1067
|
+
return createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
1068
|
+
}
|
|
1069
|
+
function worktreeInstanceSuffix(tenantRoot) {
|
|
1070
|
+
return createHash("sha256").update(resolve(tenantRoot)).digest("hex").slice(0, 10);
|
|
1071
|
+
}
|
|
1072
|
+
function repoFamilyIndexPath(tenantRoot, gitCommonDir) {
|
|
1073
|
+
if (gitCommonDir) {
|
|
1074
|
+
return resolve(gitCommonDir, DEV_REPO_INDEX_RELATIVE_PATH);
|
|
1075
|
+
}
|
|
1076
|
+
const cacheRoot = process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "treeseed", "dev-instances") : resolve(homedir(), ".cache", "treeseed", "dev-instances");
|
|
1077
|
+
return resolve(cacheRoot, `${repositoryIndexId(tenantRoot, null)}.json`);
|
|
1078
|
+
}
|
|
1079
|
+
function instanceRuntimeScope(plan) {
|
|
1080
|
+
return runtimeScopeKey(plan.commands.map((command) => command.id));
|
|
1081
|
+
}
|
|
1082
|
+
function devInstanceDir(tenantRoot) {
|
|
1083
|
+
return resolve(tenantRoot, DEV_INSTANCE_DIR);
|
|
1084
|
+
}
|
|
1085
|
+
function devPidDir(tenantRoot) {
|
|
1086
|
+
return resolve(tenantRoot, DEV_PID_DIR);
|
|
1087
|
+
}
|
|
1088
|
+
function devInstancePath(tenantRoot, runtimeScope) {
|
|
1089
|
+
return resolve(devInstanceDir(tenantRoot), `${runtimeScope}.json`);
|
|
1090
|
+
}
|
|
1091
|
+
function devPidPath(tenantRoot, runtimeScope) {
|
|
1092
|
+
return resolve(devPidDir(tenantRoot), `${runtimeScope}.pid`);
|
|
1093
|
+
}
|
|
1094
|
+
function portFromReadyCheck(checks, id) {
|
|
1095
|
+
const check = checks.find((entry) => entry.id === id);
|
|
1096
|
+
return parsePortFromUrl(check?.url);
|
|
1097
|
+
}
|
|
1098
|
+
function portsFromPlan(plan) {
|
|
1099
|
+
return Object.fromEntries(
|
|
1100
|
+
Object.entries({
|
|
1101
|
+
web: parsePortFromUrl(plan.webUrl ?? void 0),
|
|
1102
|
+
api: parsePortFromUrl(plan.apiBaseUrl),
|
|
1103
|
+
mailpit: portFromReadyCheck(plan.readyChecks, "mailpit"),
|
|
1104
|
+
mailpitSmtp: Number(plan.commands[0]?.env.TREESEED_MAILPIT_SMTP_PORT ?? "") || null,
|
|
1105
|
+
postgres: Number(plan.commands[0]?.env.TREESEED_MARKET_LOCAL_POSTGRES_PORT ?? "") || null
|
|
1106
|
+
}).filter((entry) => Number.isInteger(entry[1]) && entry[1] > 0)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
function urlsFromPlan(plan) {
|
|
1110
|
+
return Object.fromEntries(
|
|
1111
|
+
Object.entries({
|
|
1112
|
+
web: plan.webUrl,
|
|
1113
|
+
api: plan.apiBaseUrl,
|
|
1114
|
+
apiHealth: `${plan.apiBaseUrl.replace(/\/+$/u, "")}/healthz`,
|
|
1115
|
+
mailpit: plan.readyChecks.find((check) => check.id === "mailpit")?.url ?? null
|
|
1116
|
+
}).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0)
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
function createDevInstanceRecord(plan, status, pid, processGroupId = null, startedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1120
|
+
const runtimeScope = instanceRuntimeScope(plan);
|
|
1121
|
+
const git = resolveGitWorktreeInfo(plan.tenantRoot);
|
|
1122
|
+
return {
|
|
1123
|
+
schemaVersion: 1,
|
|
1124
|
+
kind: "treeseed.dev.instance",
|
|
1125
|
+
projectRoot: plan.tenantRoot,
|
|
1126
|
+
worktreeRoot: git.worktreeRoot,
|
|
1127
|
+
branch: git.branch,
|
|
1128
|
+
gitCommonDir: git.gitCommonDir,
|
|
1129
|
+
status,
|
|
1130
|
+
pid,
|
|
1131
|
+
processGroupId,
|
|
1132
|
+
startedAt,
|
|
1133
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1134
|
+
ports: portsFromPlan(plan),
|
|
1135
|
+
urls: urlsFromPlan(plan),
|
|
1136
|
+
logPath: plan.logPath,
|
|
1137
|
+
runtimeScope,
|
|
1138
|
+
surfaces: plan.commands.map((command) => command.id),
|
|
1139
|
+
readyChecks: plan.readyChecks,
|
|
1140
|
+
instancePath: devInstancePath(plan.tenantRoot, runtimeScope),
|
|
1141
|
+
pidPath: devPidPath(plan.tenantRoot, runtimeScope)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function readDevInstanceFile(path) {
|
|
1145
|
+
try {
|
|
1146
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1147
|
+
if (parsed.kind !== "treeseed.dev.instance" || typeof parsed.projectRoot !== "string" || typeof parsed.instancePath !== "string") {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
return parsed;
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function writeDevInstance(record) {
|
|
1156
|
+
const next = { ...record, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1157
|
+
atomicWriteJson(next.instancePath, next);
|
|
1158
|
+
mkdirSync(dirname(next.pidPath), { recursive: true });
|
|
1159
|
+
if (next.pid) {
|
|
1160
|
+
writeFileSync(next.pidPath, `${next.pid}
|
|
1161
|
+
`, "utf8");
|
|
1162
|
+
}
|
|
1163
|
+
writeRepoFamilyIndexEntry(next);
|
|
1164
|
+
return next;
|
|
1165
|
+
}
|
|
1166
|
+
function readRepoFamilyIndex(tenantRoot, gitCommonDir) {
|
|
1167
|
+
const path = repoFamilyIndexPath(tenantRoot, gitCommonDir);
|
|
1168
|
+
try {
|
|
1169
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1170
|
+
if (parsed.kind === "treeseed.dev.index" && Array.isArray(parsed.instances)) {
|
|
1171
|
+
return parsed;
|
|
1172
|
+
}
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
schemaVersion: 1,
|
|
1177
|
+
kind: "treeseed.dev.index",
|
|
1178
|
+
repositoryId: repositoryIndexId(tenantRoot, gitCommonDir),
|
|
1179
|
+
gitCommonDir,
|
|
1180
|
+
instances: []
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function writeRepoFamilyIndex(index, tenantRoot, gitCommonDir) {
|
|
1184
|
+
atomicWriteJson(repoFamilyIndexPath(tenantRoot, gitCommonDir), index);
|
|
1185
|
+
}
|
|
1186
|
+
function writeRepoFamilyIndexEntry(record) {
|
|
1187
|
+
const index = readRepoFamilyIndex(record.projectRoot, record.gitCommonDir);
|
|
1188
|
+
const entry = {
|
|
1189
|
+
worktreeRoot: record.worktreeRoot,
|
|
1190
|
+
instancePath: record.instancePath,
|
|
1191
|
+
branch: record.branch,
|
|
1192
|
+
pid: record.pid,
|
|
1193
|
+
runtimeScope: record.runtimeScope,
|
|
1194
|
+
updatedAt: record.updatedAt
|
|
1195
|
+
};
|
|
1196
|
+
const instances = index.instances.filter(
|
|
1197
|
+
(candidate) => !(candidate.worktreeRoot === entry.worktreeRoot && candidate.runtimeScope === entry.runtimeScope)
|
|
1198
|
+
);
|
|
1199
|
+
instances.push(entry);
|
|
1200
|
+
writeRepoFamilyIndex({ ...index, instances }, record.projectRoot, record.gitCommonDir);
|
|
1201
|
+
}
|
|
1202
|
+
function removeRepoFamilyIndexEntry(record) {
|
|
1203
|
+
const index = readRepoFamilyIndex(record.projectRoot, record.gitCommonDir);
|
|
1204
|
+
writeRepoFamilyIndex({
|
|
1205
|
+
...index,
|
|
1206
|
+
instances: index.instances.filter(
|
|
1207
|
+
(candidate) => !(candidate.worktreeRoot === record.worktreeRoot && candidate.runtimeScope === record.runtimeScope)
|
|
1208
|
+
)
|
|
1209
|
+
}, record.projectRoot, record.gitCommonDir);
|
|
1210
|
+
}
|
|
1211
|
+
function listWorktreeDevInstances(tenantRoot) {
|
|
1212
|
+
try {
|
|
1213
|
+
return readdirSync(devInstanceDir(tenantRoot)).filter((entry) => entry.endsWith(".json")).map((entry) => readDevInstanceFile(resolve(devInstanceDir(tenantRoot), entry))).filter((entry) => Boolean(entry));
|
|
1214
|
+
} catch {
|
|
1215
|
+
return [];
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function listRepoFamilyDevInstances(tenantRoot) {
|
|
1219
|
+
const git = resolveGitWorktreeInfo(tenantRoot);
|
|
1220
|
+
const index = readRepoFamilyIndex(tenantRoot, git.gitCommonDir);
|
|
1221
|
+
return index.instances.map((entry) => readDevInstanceFile(entry.instancePath)).filter((entry) => Boolean(entry));
|
|
1222
|
+
}
|
|
1223
|
+
function evaluateDevInstance(record, deps) {
|
|
1224
|
+
if (!record.pid || !deps.processIsAlive(record.pid)) {
|
|
1225
|
+
return { ...record, status: "stale", staleReason: record.pid ? `Process ${record.pid} is not running.` : "No supervisor pid is recorded." };
|
|
1226
|
+
}
|
|
1227
|
+
return record;
|
|
1228
|
+
}
|
|
1229
|
+
function removeDevInstanceRecord(record) {
|
|
1230
|
+
rmSync(record.instancePath, { force: true });
|
|
1231
|
+
rmSync(record.pidPath, { force: true });
|
|
1232
|
+
removeRepoFamilyIndexEntry(record);
|
|
1233
|
+
}
|
|
1234
|
+
function usedPortsFromInstances(records, processIsAlive) {
|
|
1235
|
+
const ports = /* @__PURE__ */ new Set();
|
|
1236
|
+
for (const record of records) {
|
|
1237
|
+
const evaluated = evaluateDevInstance(record, { processIsAlive });
|
|
1238
|
+
if (evaluated.status === "stale") continue;
|
|
1239
|
+
for (const port of Object.values(record.ports)) {
|
|
1240
|
+
if (Number.isInteger(port) && port > 0) ports.add(port);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return ports;
|
|
1244
|
+
}
|
|
1245
|
+
function managedPortBlock(blockIndex) {
|
|
1246
|
+
const offset = blockIndex * 10;
|
|
1247
|
+
return {
|
|
1248
|
+
web: TREESEED_DEFAULT_WEB_PORT + offset,
|
|
1249
|
+
api: TREESEED_DEFAULT_API_PORT + offset,
|
|
1250
|
+
postgres: TREESEED_DEFAULT_MARKET_POSTGRES_PORT + offset,
|
|
1251
|
+
mailpitSmtp: TREESEED_DEFAULT_LOCAL_SMTP_PORT + offset,
|
|
1252
|
+
mailpitUi: TREESEED_DEFAULT_MAILPIT_UI_PORT + offset
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function resolveManagedPortOverrides(tenantRoot, options, deps) {
|
|
1256
|
+
const existing = listWorktreeDevInstances(tenantRoot).map((record) => evaluateDevInstance(record, deps)).find((record) => record.status !== "stale");
|
|
1257
|
+
if (existing && options.webPort == null && options.apiPort == null) {
|
|
1258
|
+
return {
|
|
1259
|
+
webPort: existing.ports.web,
|
|
1260
|
+
apiPort: existing.ports.api,
|
|
1261
|
+
env: {
|
|
1262
|
+
TREESEED_MARKET_LOCAL_POSTGRES_PORT: existing.ports.postgres ? String(existing.ports.postgres) : void 0,
|
|
1263
|
+
TREESEED_MAILPIT_SMTP_PORT: existing.ports.mailpitSmtp ? String(existing.ports.mailpitSmtp) : void 0,
|
|
1264
|
+
TREESEED_MAILPIT_UI_PORT: existing.ports.mailpit ? String(existing.ports.mailpit) : void 0,
|
|
1265
|
+
TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER: `treeseed-market-local-postgres-${worktreeInstanceSuffix(tenantRoot)}`,
|
|
1266
|
+
TREESEED_MARKET_LOCAL_POSTGRES_VOLUME: `treeseed-market-local-postgres-data-${worktreeInstanceSuffix(tenantRoot)}`,
|
|
1267
|
+
TREESEED_MAILPIT_CONTAINER_NAME: `treeseed-mailpit-${worktreeInstanceSuffix(tenantRoot)}`
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
const repoInstances = listRepoFamilyDevInstances(tenantRoot);
|
|
1272
|
+
const usedPorts = usedPortsFromInstances(repoInstances, deps.processIsAlive);
|
|
1273
|
+
for (const owner of deps.inspectPortOwners([
|
|
1274
|
+
TREESEED_DEFAULT_WEB_PORT,
|
|
1275
|
+
TREESEED_DEFAULT_API_PORT,
|
|
1276
|
+
TREESEED_DEFAULT_MARKET_POSTGRES_PORT,
|
|
1277
|
+
TREESEED_DEFAULT_LOCAL_SMTP_PORT,
|
|
1278
|
+
TREESEED_DEFAULT_MAILPIT_UI_PORT
|
|
1279
|
+
])) {
|
|
1280
|
+
if (owner.pid) usedPorts.add(owner.port);
|
|
1281
|
+
}
|
|
1282
|
+
for (let block = 0; block < 100; block += 1) {
|
|
1283
|
+
const candidate = managedPortBlock(block);
|
|
1284
|
+
const requestedWeb = options.webPort ?? candidate.web;
|
|
1285
|
+
const requestedApi = options.apiPort ?? candidate.api;
|
|
1286
|
+
const candidatePorts = [requestedWeb, requestedApi, candidate.postgres, candidate.mailpitSmtp, candidate.mailpitUi];
|
|
1287
|
+
const liveOwners = deps.inspectPortOwners(candidatePorts).filter((owner) => owner.pid && owner.pid !== process.pid);
|
|
1288
|
+
const blocked = candidatePorts.some((port) => usedPorts.has(port)) || liveOwners.length > 0;
|
|
1289
|
+
if (!blocked || options.forceConflicts === true) {
|
|
1290
|
+
return {
|
|
1291
|
+
webPort: requestedWeb,
|
|
1292
|
+
apiPort: requestedApi,
|
|
1293
|
+
env: {
|
|
1294
|
+
TREESEED_MARKET_LOCAL_POSTGRES_PORT: String(candidate.postgres),
|
|
1295
|
+
TREESEED_MAILPIT_SMTP_PORT: String(candidate.mailpitSmtp),
|
|
1296
|
+
TREESEED_MAILPIT_UI_PORT: String(candidate.mailpitUi),
|
|
1297
|
+
TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER: `treeseed-market-local-postgres-${worktreeInstanceSuffix(tenantRoot)}`,
|
|
1298
|
+
TREESEED_MARKET_LOCAL_POSTGRES_VOLUME: `treeseed-market-local-postgres-data-${worktreeInstanceSuffix(tenantRoot)}`,
|
|
1299
|
+
TREESEED_MAILPIT_CONTAINER_NAME: `treeseed-mailpit-${worktreeInstanceSuffix(tenantRoot)}`
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
throw new Error("Unable to allocate a free Treeseed dev port block for this worktree.");
|
|
1305
|
+
}
|
|
1306
|
+
function renderManagedDevStatus(records) {
|
|
1307
|
+
if (records.length === 0) return "No managed Treeseed dev instances found.";
|
|
1308
|
+
return records.map((record) => {
|
|
1309
|
+
const url = record.urls.web ?? record.urls.api ?? "(no url)";
|
|
1310
|
+
const branch = record.branch ? ` ${record.branch}` : "";
|
|
1311
|
+
return `${record.status.padEnd(8)} pid ${record.pid ?? "-"}${branch} ${url} log ${record.logPath}`;
|
|
1312
|
+
}).join("\n");
|
|
1313
|
+
}
|
|
1314
|
+
function renderDevLogJsonEventForHuman(parsed) {
|
|
1315
|
+
if (parsed.kind === "treeseed.dev.log" && parsed.type === "start") {
|
|
1316
|
+
const startedAt = typeof parsed.startedAt === "string" ? ` at ${parsed.startedAt}` : "";
|
|
1317
|
+
return `[dev] Log session started${startedAt}.`;
|
|
1318
|
+
}
|
|
1319
|
+
if (parsed.kind !== "treeseed.dev.event") {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
const surface = typeof parsed.surface === "string" ? `[${parsed.surface}]` : parsed.type === "setup" ? "[setup]" : "[dev]";
|
|
1323
|
+
const message = typeof parsed.message === "string" ? parsed.message : typeof parsed.status === "string" ? parsed.status : "";
|
|
1324
|
+
if (!message) {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
if (surface === "[market-runner]") {
|
|
1328
|
+
try {
|
|
1329
|
+
const runner = JSON.parse(message);
|
|
1330
|
+
if (runner.ok === true && runner.claimed === false && runner.operation == null) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
} catch {
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return `${surface} ${message}`;
|
|
1337
|
+
}
|
|
1338
|
+
function renderDevLogForHuman(raw) {
|
|
1339
|
+
const rendered = [];
|
|
1340
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
1341
|
+
if (!line) {
|
|
1342
|
+
rendered.push(line);
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (line.trimStart().startsWith("{")) {
|
|
1346
|
+
try {
|
|
1347
|
+
const parsed = JSON.parse(line);
|
|
1348
|
+
const human = renderDevLogJsonEventForHuman(parsed);
|
|
1349
|
+
if (human) rendered.push(human);
|
|
1350
|
+
continue;
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
rendered.push(line);
|
|
1355
|
+
}
|
|
1356
|
+
return rendered.join("\n");
|
|
1357
|
+
}
|
|
1358
|
+
function readRecentDevLog(path, maxLines = 300, maxBytes = 256 * 1024) {
|
|
1359
|
+
const stats = statSync(path);
|
|
1360
|
+
const start = Math.max(0, stats.size - maxBytes);
|
|
1361
|
+
const fd = openSync(path, "r");
|
|
1362
|
+
try {
|
|
1363
|
+
const buffer = Buffer.alloc(stats.size - start);
|
|
1364
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
1365
|
+
const lines = buffer.toString("utf8").split(/\r?\n/u);
|
|
1366
|
+
return lines.slice(Math.max(0, lines.length - maxLines)).join("\n");
|
|
1367
|
+
} finally {
|
|
1368
|
+
closeSync(fd);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
async function waitForManagedInstanceReady(instancePath, options, deps) {
|
|
1372
|
+
const startedAt = Date.now();
|
|
1373
|
+
const timeoutMs = options.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
|
|
1374
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1375
|
+
const record2 = readDevInstanceFile(instancePath);
|
|
1376
|
+
if (record2) {
|
|
1377
|
+
const evaluated = evaluateDevInstance(record2, deps);
|
|
1378
|
+
if (evaluated.status === "ready" || evaluated.status === "degraded" || evaluated.status === "stale") {
|
|
1379
|
+
return evaluated;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
await delay(500);
|
|
1383
|
+
}
|
|
1384
|
+
const record = readDevInstanceFile(instancePath);
|
|
1385
|
+
return record ? { ...record, status: "degraded", staleReason: "Timed out waiting for readiness." } : null;
|
|
1386
|
+
}
|
|
1387
|
+
function managedDevResult(kind, ok, payload, options, write) {
|
|
1388
|
+
if (options.json) {
|
|
1389
|
+
write(`${JSON.stringify({ schemaVersion: 1, kind, ok, payload }, null, 2)}
|
|
1390
|
+
`, "stdout");
|
|
1391
|
+
} else if (typeof payload === "string") {
|
|
1392
|
+
write(`${payload}
|
|
1393
|
+
`, "stdout");
|
|
1394
|
+
} else if (Array.isArray(payload)) {
|
|
1395
|
+
write(`${renderManagedDevStatus(payload)}
|
|
1396
|
+
`, "stdout");
|
|
1397
|
+
} else {
|
|
1398
|
+
write(`${renderManagedDevStatus([payload])}
|
|
1399
|
+
`, "stdout");
|
|
1400
|
+
}
|
|
1401
|
+
return ok ? 0 : 1;
|
|
1402
|
+
}
|
|
1403
|
+
async function stopDevInstance(record, options, deps) {
|
|
1404
|
+
if (!record.pid || !deps.processIsAlive(record.pid)) {
|
|
1405
|
+
removeDevInstanceRecord(record);
|
|
1406
|
+
return { ...record, status: "stale", staleReason: "Process was not running." };
|
|
1407
|
+
}
|
|
1408
|
+
const targetPid = record.processGroupId && process.platform !== "win32" ? -record.processGroupId : record.pid;
|
|
1409
|
+
try {
|
|
1410
|
+
deps.killProcess(targetPid, "SIGTERM");
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
1413
|
+
if (!await waitForProcessExit(record.pid, deps.processIsAlive, options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS)) {
|
|
1414
|
+
try {
|
|
1415
|
+
deps.killProcess(targetPid, "SIGKILL");
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
await waitForProcessExit(record.pid, deps.processIsAlive, DEFAULT_KILL_GRACE_MS);
|
|
1419
|
+
}
|
|
1420
|
+
removeDevInstanceRecord(record);
|
|
1421
|
+
return { ...record, status: "stopped", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1422
|
+
}
|
|
1423
|
+
async function runTreeseedManagedDev(options, deps = {}) {
|
|
1424
|
+
const tenantRoot = resolve(options.cwd ?? process.cwd());
|
|
1425
|
+
const write = deps.write ?? defaultWrite;
|
|
1426
|
+
const spawnProcess = deps.spawn ?? spawn;
|
|
1427
|
+
const fetchFn = deps.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1428
|
+
const processIsAlive = deps.processIsAlive ?? defaultProcessIsAlive;
|
|
1429
|
+
const killProcess = deps.killProcess ?? defaultKillProcess;
|
|
1430
|
+
const inspectPortOwners = deps.inspectPortOwners ?? defaultInspectPortOwners;
|
|
1431
|
+
const baseDeps = { processIsAlive, inspectPortOwners };
|
|
1432
|
+
if (options.action === "status") {
|
|
1433
|
+
const records = (options.all ? listRepoFamilyDevInstances(tenantRoot) : listWorktreeDevInstances(tenantRoot)).map((record) => evaluateDevInstance(record, { processIsAlive }));
|
|
1434
|
+
return managedDevResult("treeseed.dev.status", true, records, options, write);
|
|
1435
|
+
}
|
|
1436
|
+
if (options.action === "logs") {
|
|
1437
|
+
const record = listWorktreeDevInstances(tenantRoot).map((entry) => evaluateDevInstance(entry, { processIsAlive })).find((entry) => entry.status !== "stale") ?? listWorktreeDevInstances(tenantRoot)[0];
|
|
1438
|
+
if (!record) {
|
|
1439
|
+
return managedDevResult("treeseed.dev.logs", false, "No managed Treeseed dev instance found for this worktree.", options, write);
|
|
1440
|
+
}
|
|
1441
|
+
if (options.json) {
|
|
1442
|
+
return managedDevResult("treeseed.dev.logs", true, { logPath: record.logPath, exists: existsSync(record.logPath) }, options, write);
|
|
1443
|
+
}
|
|
1444
|
+
if (!existsSync(record.logPath)) {
|
|
1445
|
+
write(`Log file does not exist yet: ${record.logPath}
|
|
1446
|
+
`, "stderr");
|
|
1447
|
+
return 1;
|
|
1448
|
+
}
|
|
1449
|
+
if (options.follow) {
|
|
1450
|
+
const tail = spawnProcess("tail", ["-f", record.logPath], { cwd: record.projectRoot, stdio: "inherit" });
|
|
1451
|
+
return await new Promise((resolvePromise) => {
|
|
1452
|
+
tail.on("exit", (code) => resolvePromise(code ?? 0));
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
write(renderDevLogForHuman(readRecentDevLog(record.logPath)), "stdout");
|
|
1456
|
+
return 0;
|
|
1457
|
+
}
|
|
1458
|
+
if (options.action === "stop") {
|
|
1459
|
+
const records = options.all ? listRepoFamilyDevInstances(tenantRoot) : listWorktreeDevInstances(tenantRoot);
|
|
1460
|
+
const stopped = [];
|
|
1461
|
+
for (const record of records) {
|
|
1462
|
+
stopped.push(await stopDevInstance(record, options, { killProcess, processIsAlive }));
|
|
1463
|
+
}
|
|
1464
|
+
return managedDevResult("treeseed.dev.stop", true, stopped, options, write);
|
|
1465
|
+
}
|
|
1466
|
+
if (options.action === "restart") {
|
|
1467
|
+
await runTreeseedManagedDev({ ...options, action: "stop" }, { ...deps, write: () => {
|
|
1468
|
+
} });
|
|
1469
|
+
return runTreeseedManagedDev({ ...options, action: "start" }, deps);
|
|
1470
|
+
}
|
|
1471
|
+
const allocated = resolveManagedPortOverrides(tenantRoot, options, baseDeps);
|
|
1472
|
+
const effectiveEnv = {
|
|
1473
|
+
...options.env ?? {},
|
|
1474
|
+
...allocated.env
|
|
1475
|
+
};
|
|
1476
|
+
const plan = createTreeseedIntegratedDevPlan({
|
|
1477
|
+
...options,
|
|
1478
|
+
cwd: tenantRoot,
|
|
1479
|
+
webPort: allocated.webPort,
|
|
1480
|
+
apiPort: allocated.apiPort,
|
|
1481
|
+
env: effectiveEnv
|
|
1482
|
+
});
|
|
1483
|
+
const runtimeScope = instanceRuntimeScope(plan);
|
|
1484
|
+
const instancePath = devInstancePath(tenantRoot, runtimeScope);
|
|
1485
|
+
const logPath = plan.logPath;
|
|
1486
|
+
const existing = readDevInstanceFile(instancePath);
|
|
1487
|
+
if (existing && evaluateDevInstance(existing, { processIsAlive }).status !== "stale" && options.force !== true) {
|
|
1488
|
+
return managedDevResult("treeseed.dev.start", true, evaluateDevInstance(existing, { processIsAlive }), options, write);
|
|
1489
|
+
}
|
|
1490
|
+
if (existing && options.force === true) {
|
|
1491
|
+
await stopDevInstance(existing, options, { killProcess, processIsAlive });
|
|
1492
|
+
}
|
|
1493
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
1494
|
+
writeFileSync(logPath, "", { flag: "a" });
|
|
1495
|
+
const supervisorCommand = deps.supervisorCommand ?? process.execPath;
|
|
1496
|
+
const supervisorArgs = [
|
|
1497
|
+
...deps.supervisorArgs ?? process.argv.slice(1).filter((arg) => !["start", "restart", "status", "stop", "logs"].includes(arg)),
|
|
1498
|
+
"--port",
|
|
1499
|
+
String(allocated.webPort),
|
|
1500
|
+
"--api-port",
|
|
1501
|
+
String(allocated.apiPort),
|
|
1502
|
+
...options.webHost ? ["--host", options.webHost] : [],
|
|
1503
|
+
...options.apiHost ? ["--api-host", options.apiHost] : [],
|
|
1504
|
+
...options.webRuntime ? ["--web-runtime", options.webRuntime] : [],
|
|
1505
|
+
...options.surfaces ? ["--surfaces", options.surfaces] : options.surface ? ["--surface", options.surface] : [],
|
|
1506
|
+
...options.setupMode ? ["--setup", options.setupMode] : [],
|
|
1507
|
+
...options.feedbackMode ? ["--feedback", options.feedbackMode] : [],
|
|
1508
|
+
...options.openMode ? ["--open", options.openMode] : [],
|
|
1509
|
+
...options.reset ? ["--reset"] : [],
|
|
1510
|
+
...options.forceConflicts ? ["--force"] : [],
|
|
1511
|
+
...options.projectId ? ["--project-id", options.projectId] : [],
|
|
1512
|
+
...options.teamId ? ["--team-id", options.teamId] : []
|
|
1513
|
+
];
|
|
1514
|
+
const logFd = openSync(logPath, "a");
|
|
1515
|
+
const child = spawnProcess(supervisorCommand, supervisorArgs, {
|
|
1516
|
+
cwd: tenantRoot,
|
|
1517
|
+
env: {
|
|
1518
|
+
...process.env,
|
|
1519
|
+
...effectiveEnv,
|
|
1520
|
+
TREESEED_MANAGED_DEV_INSTANCE: "1",
|
|
1521
|
+
TREESEED_MANAGED_DEV_SUPPRESS_STDIO: "1"
|
|
1522
|
+
},
|
|
1523
|
+
stdio: ["ignore", logFd, logFd],
|
|
1524
|
+
detached: true
|
|
1525
|
+
});
|
|
1526
|
+
closeSync(logFd);
|
|
1527
|
+
child.unref?.();
|
|
1528
|
+
const childPid = typeof child.pid === "number" ? child.pid : null;
|
|
1529
|
+
const starting = writeDevInstance(createDevInstanceRecord(plan, "starting", childPid, childPid && process.platform !== "win32" ? childPid : null));
|
|
1530
|
+
const ready = await waitForManagedInstanceReady(instancePath, options, { processIsAlive });
|
|
1531
|
+
if (!ready) {
|
|
1532
|
+
return managedDevResult("treeseed.dev.start", false, { ...starting, status: "degraded", staleReason: "Supervisor did not publish an instance record." }, options, write);
|
|
1533
|
+
}
|
|
1534
|
+
if (ready.status === "stale") {
|
|
1535
|
+
return managedDevResult("treeseed.dev.start", false, ready, options, write);
|
|
1536
|
+
}
|
|
1537
|
+
for (const check of ready.readyChecks.filter((entry) => entry.required && entry.strategy === "http" && entry.url)) {
|
|
1538
|
+
if (!await fetchOk(fetchFn, check.url, 2e3)) {
|
|
1539
|
+
const degraded = writeDevInstance({ ...ready, status: "degraded", staleReason: `${check.label} is not reachable at ${check.url}.` });
|
|
1540
|
+
return managedDevResult("treeseed.dev.start", false, degraded, options, write);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return managedDevResult("treeseed.dev.start", ready.status === "ready", ready, options, write);
|
|
1544
|
+
}
|
|
1036
1545
|
function devRuntimeStateDir(tenantRoot) {
|
|
1037
1546
|
return resolve(tenantRoot, DEV_RUNTIME_DIR);
|
|
1038
1547
|
}
|
|
@@ -1052,7 +1561,7 @@ function readDevRuntimeStateFile(path) {
|
|
|
1052
1561
|
if (!Number.isInteger(parsed.pid) || typeof parsed.tenantRoot !== "string" || typeof parsed.startedAt !== "string") {
|
|
1053
1562
|
return null;
|
|
1054
1563
|
}
|
|
1055
|
-
const commandIds = Array.isArray(parsed.commandIds) ? parsed.commandIds.filter((id) =>
|
|
1564
|
+
const commandIds = Array.isArray(parsed.commandIds) ? parsed.commandIds.filter((id) => ALL_COMMAND_IDS.includes(id)) : void 0;
|
|
1056
1565
|
return {
|
|
1057
1566
|
pid: parsed.pid,
|
|
1058
1567
|
tenantRoot: parsed.tenantRoot,
|
|
@@ -1135,7 +1644,9 @@ function requiredDevPorts(plan) {
|
|
|
1135
1644
|
function formatPortOwner(owner) {
|
|
1136
1645
|
return `port ${owner.port}${owner.pid ? ` pid ${owner.pid}` : ""}${owner.processName ? ` (${owner.processName})` : ""}`;
|
|
1137
1646
|
}
|
|
1138
|
-
function writeCurrentDevRuntimeState(
|
|
1647
|
+
function writeCurrentDevRuntimeState(plan, status = "starting") {
|
|
1648
|
+
const tenantRoot = plan.tenantRoot;
|
|
1649
|
+
const commandIds = plan.commands.map((command) => command.id);
|
|
1139
1650
|
const outputPath = devRuntimeStatePath(tenantRoot, runtimeScopeKey(commandIds));
|
|
1140
1651
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1141
1652
|
writeFileSync(
|
|
@@ -1149,15 +1660,30 @@ function writeCurrentDevRuntimeState(tenantRoot, commandIds) {
|
|
|
1149
1660
|
`,
|
|
1150
1661
|
"utf8"
|
|
1151
1662
|
);
|
|
1663
|
+
const managedProcessGroupId = process.env.TREESEED_MANAGED_DEV_INSTANCE === "1" && process.platform !== "win32" ? process.pid : null;
|
|
1664
|
+
writeDevInstance(createDevInstanceRecord(plan, status, process.pid, managedProcessGroupId));
|
|
1152
1665
|
return outputPath;
|
|
1153
1666
|
}
|
|
1154
|
-
function
|
|
1667
|
+
function updateCurrentDevRuntimeState(plan, status, staleReason) {
|
|
1668
|
+
const existing = readDevInstanceFile(devInstancePath(plan.tenantRoot, instanceRuntimeScope(plan)));
|
|
1669
|
+
if (!existing || existing.pid !== process.pid) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
writeDevInstance({ ...existing, status, staleReason });
|
|
1673
|
+
}
|
|
1674
|
+
function removeCurrentDevRuntimeState(plan) {
|
|
1675
|
+
const tenantRoot = plan.tenantRoot;
|
|
1676
|
+
const commandIds = plan.commands.map((command) => command.id);
|
|
1155
1677
|
const statePath = devRuntimeStatePath(tenantRoot, runtimeScopeKey(commandIds));
|
|
1156
1678
|
const state = readDevRuntimeStateFile(statePath);
|
|
1157
1679
|
if (!state || state.pid !== process.pid) {
|
|
1158
|
-
|
|
1680
|
+
} else {
|
|
1681
|
+
rmSync(statePath, { force: true });
|
|
1682
|
+
}
|
|
1683
|
+
const instance = readDevInstanceFile(devInstancePath(tenantRoot, instanceRuntimeScope(plan)));
|
|
1684
|
+
if (instance?.pid === process.pid) {
|
|
1685
|
+
removeDevInstanceRecord(instance);
|
|
1159
1686
|
}
|
|
1160
|
-
rmSync(statePath, { force: true });
|
|
1161
1687
|
}
|
|
1162
1688
|
async function waitForProcessExit(pid, processIsAlive, timeoutMs) {
|
|
1163
1689
|
const startedAt = Date.now();
|
|
@@ -1283,15 +1809,13 @@ function emitEvent(options, write, event, stream = event.type === "error" ? "std
|
|
|
1283
1809
|
}
|
|
1284
1810
|
function createDevLogWrite(baseWrite, logPath) {
|
|
1285
1811
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
1286
|
-
appendFileSync(logPath,
|
|
1287
|
-
schemaVersion: 1,
|
|
1288
|
-
kind: "treeseed.dev.log",
|
|
1289
|
-
type: "start",
|
|
1290
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1291
|
-
})}
|
|
1812
|
+
appendFileSync(logPath, `[dev] Log session started at ${(/* @__PURE__ */ new Date()).toISOString()}.
|
|
1292
1813
|
`, "utf8");
|
|
1814
|
+
const suppressBaseWrite = process.env.TREESEED_MANAGED_DEV_SUPPRESS_STDIO === "1";
|
|
1293
1815
|
return (line, stream) => {
|
|
1294
|
-
|
|
1816
|
+
if (!suppressBaseWrite) {
|
|
1817
|
+
baseWrite(line, stream);
|
|
1818
|
+
}
|
|
1295
1819
|
appendFileSync(logPath, line, "utf8");
|
|
1296
1820
|
};
|
|
1297
1821
|
}
|
|
@@ -1870,7 +2394,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1870
2394
|
});
|
|
1871
2395
|
return 1;
|
|
1872
2396
|
}
|
|
1873
|
-
writeCurrentDevRuntimeState(
|
|
2397
|
+
writeCurrentDevRuntimeState(plan, "starting");
|
|
1874
2398
|
const children = /* @__PURE__ */ new Map();
|
|
1875
2399
|
const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
|
|
1876
2400
|
const requiredSurfaceIds = new Set(plan.readyChecks.filter((check) => check.required).map((check) => check.id));
|
|
@@ -1935,7 +2459,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1935
2459
|
for (const dispose of disposers) {
|
|
1936
2460
|
dispose();
|
|
1937
2461
|
}
|
|
1938
|
-
removeCurrentDevRuntimeState(
|
|
2462
|
+
removeCurrentDevRuntimeState(plan);
|
|
1939
2463
|
emitEvent(
|
|
1940
2464
|
options,
|
|
1941
2465
|
write,
|
|
@@ -2212,10 +2736,12 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
2212
2736
|
}
|
|
2213
2737
|
readinessInProgress = false;
|
|
2214
2738
|
if (!allRequiredReady) {
|
|
2739
|
+
updateCurrentDevRuntimeState(plan, "degraded", "One or more required readiness checks failed.");
|
|
2215
2740
|
startLiveWatch();
|
|
2216
2741
|
return;
|
|
2217
2742
|
}
|
|
2218
2743
|
readinessComplete = true;
|
|
2744
|
+
updateCurrentDevRuntimeState(plan, "ready");
|
|
2219
2745
|
if (plan.webUrl) {
|
|
2220
2746
|
emitEvent(options, write, { type: "ready", url: plan.webUrl, message: `Treeseed dev ready at ${plan.webUrl}.` });
|
|
2221
2747
|
}
|
|
@@ -2269,5 +2795,6 @@ export {
|
|
|
2269
2795
|
createTreeseedIntegratedDevPlan,
|
|
2270
2796
|
createTreeseedIntegratedDevResetPlan,
|
|
2271
2797
|
runTreeseedIntegratedDev,
|
|
2272
|
-
runTreeseedIntegratedDevReset
|
|
2798
|
+
runTreeseedIntegratedDevReset,
|
|
2799
|
+
runTreeseedManagedDev
|
|
2273
2800
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { buildTreeseedSiteLayers, resolveTreeseedPageEntrypoint, resolveTreeseed
|
|
|
2
2
|
export { buildTreeseedPlatformLayers, resolveTreeseedPlatformResource, TREESEED_PLATFORM_RESOURCE_KINDS, } from './platform-resources';
|
|
3
3
|
export { parseSiteConfig } from './utils/site-config-schema.js';
|
|
4
4
|
export { executeKnowledgeHubProviderLaunch, validateKnowledgeHubProviderLaunchPrerequisites, } from './launch';
|
|
5
|
-
export { createTreeseedIntegratedDevPlan, runTreeseedIntegratedDev, type TreeseedIntegratedDevCommand, type TreeseedIntegratedDevOptions, type TreeseedIntegratedDevPlan, type TreeseedIntegratedDevSurface, } from './dev';
|
|
5
|
+
export { createTreeseedIntegratedDevPlan, runTreeseedManagedDev, runTreeseedIntegratedDev, type TreeseedIntegratedDevCommand, type TreeseedIntegratedDevOptions, type TreeseedIntegratedDevPlan, type TreeseedIntegratedDevSurface, type TreeseedManagedDevOptions, type TreeseedDevInstanceRecord, } from './dev';
|
|
6
6
|
export { filterSiteRenderedModels, isSiteRenderedModel, siteModelRendered, } from './utils/site-models.ts';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./launch.js";
|
|
18
18
|
import {
|
|
19
19
|
createTreeseedIntegratedDevPlan,
|
|
20
|
+
runTreeseedManagedDev,
|
|
20
21
|
runTreeseedIntegratedDev
|
|
21
22
|
} from "./dev.js";
|
|
22
23
|
import {
|
|
@@ -39,6 +40,7 @@ export {
|
|
|
39
40
|
resolveTreeseedSiteResource,
|
|
40
41
|
resolveTreeseedStyleEntrypoint,
|
|
41
42
|
runTreeseedIntegratedDev,
|
|
43
|
+
runTreeseedManagedDev,
|
|
42
44
|
siteModelRendered,
|
|
43
45
|
validateKnowledgeHubProviderLaunchPrerequisites
|
|
44
46
|
};
|
package/dist/platform.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { buildTreeseedPlatformLayers, resolveTreeseedPlatformResource, TREESEED_PLATFORM_RESOURCE_KINDS, type TreeseedPlatformLayer, } from './platform-resources';
|
|
2
|
-
export { createTreeseedIntegratedDevPlan, runTreeseedIntegratedDev, type TreeseedIntegratedDevCommand, type TreeseedIntegratedDevOptions, type TreeseedIntegratedDevPlan, type TreeseedIntegratedDevSurface, } from './dev';
|
|
2
|
+
export { createTreeseedIntegratedDevPlan, runTreeseedManagedDev, runTreeseedIntegratedDev, type TreeseedIntegratedDevCommand, type TreeseedIntegratedDevOptions, type TreeseedIntegratedDevPlan, type TreeseedIntegratedDevSurface, type TreeseedManagedDevOptions, type TreeseedDevInstanceRecord, } from './dev';
|
package/dist/platform.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./platform-resources.js";
|
|
6
6
|
import {
|
|
7
7
|
createTreeseedIntegratedDevPlan,
|
|
8
|
+
runTreeseedManagedDev,
|
|
8
9
|
runTreeseedIntegratedDev
|
|
9
10
|
} from "./dev.js";
|
|
10
11
|
export {
|
|
@@ -12,5 +13,6 @@ export {
|
|
|
12
13
|
buildTreeseedPlatformLayers,
|
|
13
14
|
createTreeseedIntegratedDevPlan,
|
|
14
15
|
resolveTreeseedPlatformResource,
|
|
15
|
-
runTreeseedIntegratedDev
|
|
16
|
+
runTreeseedIntegratedDev,
|
|
17
|
+
runTreeseedManagedDev
|
|
16
18
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { runTreeseedManagedDev, runTreeseedIntegratedDev, } from '../dev.js';
|
|
6
|
+
const rawArgs = process.argv.slice(2);
|
|
7
|
+
const managedActions = new Set(['start', 'status', 'logs', 'stop', 'restart']);
|
|
8
|
+
const action = managedActions.has(rawArgs[0] ?? '') ? rawArgs[0] : null;
|
|
9
|
+
const args = action ? rawArgs.slice(1) : rawArgs;
|
|
4
10
|
function readFlag(name) {
|
|
5
11
|
return args.includes(name);
|
|
6
12
|
}
|
|
@@ -70,24 +76,47 @@ function readForwardedEnvironment() {
|
|
|
70
76
|
.map((key) => [key, process.env[key]])
|
|
71
77
|
.filter((entry) => typeof entry[1] === 'string' && entry[1].length > 0));
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
function resolveSupervisorArgs() {
|
|
80
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
81
|
+
if (scriptPath.endsWith('.ts')) {
|
|
82
|
+
const runnerPath = resolve(dirname(scriptPath), 'run-ts.mjs');
|
|
83
|
+
return existsSync(runnerPath) ? [runnerPath, scriptPath] : [scriptPath];
|
|
84
|
+
}
|
|
85
|
+
return [scriptPath];
|
|
86
|
+
}
|
|
87
|
+
async function main() {
|
|
88
|
+
const common = {
|
|
89
|
+
surface: parseSurface(readOption('--surface')),
|
|
90
|
+
surfaces: readOption('--surfaces'),
|
|
91
|
+
watch: readFlag('--watch'),
|
|
92
|
+
webHost: readOption('--host'),
|
|
93
|
+
webPort: readNumberOption('--port'),
|
|
94
|
+
apiHost: readOption('--api-host'),
|
|
95
|
+
apiPort: readNumberOption('--api-port'),
|
|
96
|
+
webRuntime: parseLocalRuntimeMode(readOption('--web-runtime')),
|
|
97
|
+
setupMode: parseSetupMode(readOption('--setup')),
|
|
98
|
+
feedbackMode: parseFeedbackMode(readOption('--feedback')),
|
|
99
|
+
openMode: parseOpenMode(readOption('--open')),
|
|
100
|
+
plan: readFlag('--plan'),
|
|
101
|
+
reset: readFlag('--reset'),
|
|
102
|
+
force: readFlag('--force'),
|
|
103
|
+
forceConflicts: readFlag('--force-conflicts'),
|
|
104
|
+
json: readFlag('--json'),
|
|
105
|
+
projectId: readOption('--project-id'),
|
|
106
|
+
teamId: readOption('--team-id'),
|
|
107
|
+
env: readForwardedEnvironment(),
|
|
108
|
+
};
|
|
109
|
+
if (!action) {
|
|
110
|
+
return runTreeseedIntegratedDev(common);
|
|
111
|
+
}
|
|
112
|
+
return runTreeseedManagedDev({
|
|
113
|
+
...common,
|
|
114
|
+
action,
|
|
115
|
+
all: readFlag('--all'),
|
|
116
|
+
follow: readFlag('--follow'),
|
|
117
|
+
}, {
|
|
118
|
+
supervisorCommand: process.execPath,
|
|
119
|
+
supervisorArgs: resolveSupervisorArgs(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
process.exit(await main());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.21",
|
|
4
4
|
"description": "Treeseed web framework package for Astro/Starlight site runtimes.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@astrojs/sitemap": "3.7.0",
|
|
71
71
|
"@astrojs/starlight": "0.37.6",
|
|
72
72
|
"@tailwindcss/vite": "^4.1.4",
|
|
73
|
-
"@treeseed/sdk": "github:treeseed-ai/sdk#0.10.
|
|
73
|
+
"@treeseed/sdk": "github:treeseed-ai/sdk#0.10.27",
|
|
74
74
|
"astro": "^5.6.1",
|
|
75
75
|
"esbuild": "^0.28.0",
|
|
76
76
|
"katex": "^0.16.22",
|