@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 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 marketDatabaseUrl = mergedEnv.TREESEED_MARKET_DATABASE_URL ?? TREESEED_DEFAULT_MARKET_POSTGRES_URL;
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: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_PORT ?? String(TREESEED_DEFAULT_MARKET_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: String(TREESEED_DEFAULT_LOCAL_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: String(TREESEED_DEFAULT_LOCAL_SMTP_PORT),
788
- TREESEED_MAILPIT_UI_PORT: mergedEnv.TREESEED_MAILPIT_UI_PORT ?? String(TREESEED_DEFAULT_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) => CANONICAL_COMMAND_IDS.includes(id)) : void 0;
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(tenantRoot, commandIds) {
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 removeCurrentDevRuntimeState(tenantRoot, commandIds) {
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
- return;
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, `${JSON.stringify({
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
- baseWrite(line, stream);
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(tenantRoot, commandIds);
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(tenantRoot, commandIds);
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
  };
@@ -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 { runTreeseedIntegratedDev, } from '../dev.js';
3
- const args = process.argv.slice(2);
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
- const exitCode = await runTreeseedIntegratedDev({
74
- surface: parseSurface(readOption('--surface')),
75
- surfaces: readOption('--surfaces'),
76
- watch: readFlag('--watch'),
77
- webHost: readOption('--host'),
78
- webPort: readNumberOption('--port'),
79
- apiHost: readOption('--api-host'),
80
- apiPort: readNumberOption('--api-port'),
81
- webRuntime: parseLocalRuntimeMode(readOption('--web-runtime')),
82
- setupMode: parseSetupMode(readOption('--setup')),
83
- feedbackMode: parseFeedbackMode(readOption('--feedback')),
84
- openMode: parseOpenMode(readOption('--open')),
85
- plan: readFlag('--plan'),
86
- reset: readFlag('--reset'),
87
- force: readFlag('--force'),
88
- json: readFlag('--json'),
89
- projectId: readOption('--project-id'),
90
- teamId: readOption('--team-id'),
91
- env: readForwardedEnvironment(),
92
- });
93
- process.exit(exitCode);
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.20",
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.26",
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",