@treeseed/core 0.8.19 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/shell/AppShell.astro +16 -4
- package/dist/components/ui/shell/BottomNav.astro +10 -3
- package/dist/components/ui/shell/RailNav.astro +10 -3
- package/dist/components/ui/shell/ShellIconLink.astro +30 -0
- package/dist/content-config.d.ts +1 -0
- package/dist/content.js +19 -2
- package/dist/dev.d.ts +10 -0
- package/dist/dev.js +283 -51
- package/dist/layouts/AuthoredEntryLayout.astro +136 -76
- package/dist/layouts/MainLayout.astro +2 -0
- package/dist/pages/decisions/[slug].astro +9 -1
- package/dist/pages/notes/[slug].astro +17 -4
- package/dist/pages/objectives/[slug].astro +4 -0
- package/dist/pages/proposals/[slug].astro +6 -0
- package/dist/pages/questions/[slug].astro +4 -0
- package/dist/scripts/build-dist.js +1 -1
- package/dist/scripts/dev-platform.js +23 -0
- package/dist/scripts/publish-package.js +5 -1
- package/dist/site.js +3 -0
- package/dist/styles/app-shell.css +26 -1
- package/dist/styles/theme.css +8 -8
- package/package.json +2 -2
package/dist/dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
@@ -32,7 +32,8 @@ const TREESEED_DEFAULT_LOCAL_SMTP_HOST = "127.0.0.1";
|
|
|
32
32
|
const TREESEED_DEFAULT_LOCAL_SMTP_PORT = 1025;
|
|
33
33
|
const TREESEED_DEFAULT_MAILPIT_UI_PORT = 8025;
|
|
34
34
|
const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
|
|
35
|
-
const
|
|
35
|
+
const DEV_RUNTIME_DIR = ".treeseed/generated/dev";
|
|
36
|
+
const DEV_RUNTIME_LEGACY_FILE = ".treeseed/generated/dev/runtime.json";
|
|
36
37
|
const DEFAULT_READINESS_TIMEOUT_MS = 9e4;
|
|
37
38
|
const DEFAULT_SETUP_STEP_TIMEOUT_MS = 3e5;
|
|
38
39
|
const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
|
|
@@ -132,10 +133,10 @@ function fallbackWebProviderFromDeployConfig(deployConfig) {
|
|
|
132
133
|
const record = deployConfig && typeof deployConfig === "object" ? deployConfig : {};
|
|
133
134
|
return normalizeProvider(record.providers?.deploy, "local");
|
|
134
135
|
}
|
|
135
|
-
function selectWebLocalRuntime(surfaceConfig, providerFallback = "local") {
|
|
136
|
+
function selectWebLocalRuntime(surfaceConfig, providerFallback = "local", overrideRuntime) {
|
|
136
137
|
const record = surfaceConfig && typeof surfaceConfig === "object" ? surfaceConfig : {};
|
|
137
138
|
const provider = normalizeProvider(record.provider, providerFallback);
|
|
138
|
-
const requested = normalizeLocalRuntimeMode(record.local?.runtime);
|
|
139
|
+
const requested = overrideRuntime ?? normalizeLocalRuntimeMode(record.local?.runtime);
|
|
139
140
|
if (provider === "cloudflare" && requested !== "local") {
|
|
140
141
|
return {
|
|
141
142
|
requested,
|
|
@@ -151,7 +152,7 @@ function selectWebLocalRuntime(surfaceConfig, providerFallback = "local") {
|
|
|
151
152
|
requested,
|
|
152
153
|
provider,
|
|
153
154
|
selected: "astro-local",
|
|
154
|
-
reason: requested === "local" ? "Configured to use the full local Astro runtime." : `Provider "${provider}" has no provider-local web runtime; using Astro local.`
|
|
155
|
+
reason: overrideRuntime === "local" ? "CLI override selected the full local Astro runtime for faster UI development." : requested === "local" ? "Configured to use the full local Astro runtime." : `Provider "${provider}" has no provider-local web runtime; using Astro local.`
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
158
|
function loadDevDeployConfig(tenantRoot) {
|
|
@@ -271,6 +272,30 @@ function resolveSeededLocalProjectId(persistTo, projectSlug = "market") {
|
|
|
271
272
|
db?.close();
|
|
272
273
|
}
|
|
273
274
|
}
|
|
275
|
+
function resolveSeededLocalTeamId(persistTo, projectId, teamSlug = "treeseed") {
|
|
276
|
+
const sqlitePath = resolveLocalD1SqlitePath(persistTo);
|
|
277
|
+
if (!sqlitePath) return null;
|
|
278
|
+
let db = null;
|
|
279
|
+
try {
|
|
280
|
+
db = new DatabaseSync(sqlitePath, { readOnly: true });
|
|
281
|
+
if (projectId) {
|
|
282
|
+
const projectRow = db.prepare(
|
|
283
|
+
`SELECT team_id FROM projects WHERE id = ? LIMIT 1`
|
|
284
|
+
).get(projectId);
|
|
285
|
+
if (typeof projectRow?.team_id === "string" && projectRow.team_id.trim()) {
|
|
286
|
+
return projectRow.team_id.trim();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const teamRow = db.prepare(
|
|
290
|
+
`SELECT id FROM teams WHERE LOWER(slug) = LOWER(?) ORDER BY created_at ASC LIMIT 1`
|
|
291
|
+
).get(teamSlug);
|
|
292
|
+
return typeof teamRow?.id === "string" && teamRow.id.trim() ? teamRow.id.trim() : null;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
} finally {
|
|
296
|
+
db?.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
274
299
|
function createTreeseedIntegratedDevResetPlan(options) {
|
|
275
300
|
if (!options.enabled) {
|
|
276
301
|
return null;
|
|
@@ -529,10 +554,13 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
529
554
|
const agentPackageRoot = resolvePackageRootEnvOverride(mergedEnv, "TREESEED_AGENT_PACKAGE_ROOT", tenantRoot) ?? resolveOptionalPackageRoot("@treeseed/agent", tenantRoot);
|
|
530
555
|
const cliPackageRoot = resolveOptionalPackageRoot("@treeseed/cli", tenantRoot);
|
|
531
556
|
const deployConfig = loadDevDeployConfig(tenantRoot);
|
|
532
|
-
const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig));
|
|
557
|
+
const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig), options.webRuntime);
|
|
533
558
|
const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
|
|
534
|
-
const
|
|
559
|
+
const usesGeneratedLocalD1State = usesCloudflareWebRuntime || webLocalRuntime.provider === "cloudflare" || selectedCommandIds.some((id) => id !== "web");
|
|
560
|
+
const localD1PersistTo = mergedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO ?? (usesGeneratedLocalD1State ? resolve(tenantRoot, ".treeseed", "generated", "environments", "local", ".wrangler", "state", "v3", "d1") : resolve(tenantRoot, ".wrangler", "state", "v3", "d1"));
|
|
535
561
|
const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID ?? resolveSeededLocalProjectId(localD1PersistTo);
|
|
562
|
+
const resolvedHostingTeamId = teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
|
|
563
|
+
const resolvedTeamId = mergedEnv.TREESEED_TEAM_ID ?? resolvedHostingTeamId ?? resolveSeededLocalTeamId(localD1PersistTo, projectId ?? null);
|
|
536
564
|
const webEntrypoint = resolveNodeEntrypoint(
|
|
537
565
|
sdkPackageRoot,
|
|
538
566
|
"scripts/tenant-astro-command.ts",
|
|
@@ -557,13 +585,14 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
557
585
|
const resetRequested = options.reset === true;
|
|
558
586
|
const sharedEnv = {
|
|
559
587
|
...mergedEnv,
|
|
560
|
-
TREESEED_LOCAL_DEV_MODE: mergedEnv.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
|
|
588
|
+
TREESEED_LOCAL_DEV_MODE: usesCloudflareWebRuntime ? mergedEnv.TREESEED_LOCAL_DEV_MODE ?? "cloudflare" : void 0,
|
|
561
589
|
TREESEED_SITE_URL: mergedEnv.TREESEED_SITE_URL ?? webUrl,
|
|
562
590
|
BETTER_AUTH_URL: mergedEnv.BETTER_AUTH_URL ?? webUrl,
|
|
563
591
|
TREESEED_API_BASE_URL: apiBaseUrl,
|
|
564
592
|
TREESEED_MARKET_API_BASE_URL: mergedEnv.TREESEED_MARKET_API_BASE_URL ?? apiBaseUrl,
|
|
565
593
|
TREESEED_PROJECT_ID: projectId ?? mergedEnv.TREESEED_PROJECT_ID,
|
|
566
|
-
|
|
594
|
+
TREESEED_TEAM_ID: resolvedTeamId ?? mergedEnv.TREESEED_TEAM_ID,
|
|
595
|
+
TREESEED_HOSTING_TEAM_ID: resolvedHostingTeamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID,
|
|
567
596
|
TREESEED_API_D1_DATABASE_NAME: mergedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
|
|
568
597
|
SITE_DATA_DB: mergedEnv.SITE_DATA_DB ?? "SITE_DATA_DB",
|
|
569
598
|
TREESEED_API_D1_LOCAL_PERSIST_TO: localD1PersistTo,
|
|
@@ -645,6 +674,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
|
|
|
645
674
|
readyChecks,
|
|
646
675
|
watchEntries,
|
|
647
676
|
commands,
|
|
677
|
+
logPath: resolve(tenantRoot, ".treeseed", "logs", `dev-${runtimeScopeKey(commands.map((command) => command.id))}.jsonl`),
|
|
648
678
|
localRuntimes: {
|
|
649
679
|
...commands.some((command) => command.id === "web") ? { web: webLocalRuntime } : {},
|
|
650
680
|
...commands.some((command) => command.id === "api") ? { api: nodeLocalRuntime("Treeseed API") } : {},
|
|
@@ -686,6 +716,29 @@ function defaultProcessIsAlive(pid) {
|
|
|
686
716
|
return false;
|
|
687
717
|
}
|
|
688
718
|
}
|
|
719
|
+
function defaultInspectPortOwners(ports) {
|
|
720
|
+
const uniquePorts = [...new Set(ports.filter((port) => Number.isInteger(port) && port > 0))];
|
|
721
|
+
if (uniquePorts.length === 0) return [];
|
|
722
|
+
const result = spawnSync("ss", ["-ltnp"], { encoding: "utf8" });
|
|
723
|
+
if ((result.status ?? 1) !== 0) return [];
|
|
724
|
+
const lines = String(result.stdout ?? "").split(/\r?\n/u);
|
|
725
|
+
const owners = [];
|
|
726
|
+
for (const port of uniquePorts) {
|
|
727
|
+
const portPattern = new RegExp(`:${port}\\b`, "u");
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
if (!portPattern.test(line)) continue;
|
|
730
|
+
const pidMatch = line.match(/pid=(\d+)/u);
|
|
731
|
+
const nameMatch = line.match(/users:\(\("([^"]+)"/u);
|
|
732
|
+
owners.push({
|
|
733
|
+
port,
|
|
734
|
+
pid: pidMatch ? Number(pidMatch[1]) : null,
|
|
735
|
+
processName: nameMatch?.[1],
|
|
736
|
+
detail: line.trim()
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return owners;
|
|
741
|
+
}
|
|
689
742
|
function defaultRemovePath(path) {
|
|
690
743
|
rmSync(path, { recursive: true, force: true });
|
|
691
744
|
}
|
|
@@ -782,44 +835,131 @@ function resolveLocalMachineEnv(tenantRoot) {
|
|
|
782
835
|
return {};
|
|
783
836
|
}
|
|
784
837
|
}
|
|
785
|
-
function
|
|
786
|
-
return resolve(tenantRoot,
|
|
838
|
+
function devRuntimeStateDir(tenantRoot) {
|
|
839
|
+
return resolve(tenantRoot, DEV_RUNTIME_DIR);
|
|
840
|
+
}
|
|
841
|
+
function devRuntimeStatePath(tenantRoot, key) {
|
|
842
|
+
return resolve(devRuntimeStateDir(tenantRoot), `runtime-${key}.json`);
|
|
843
|
+
}
|
|
844
|
+
function legacyDevRuntimeStatePath(tenantRoot) {
|
|
845
|
+
return resolve(tenantRoot, DEV_RUNTIME_LEGACY_FILE);
|
|
787
846
|
}
|
|
788
|
-
function
|
|
847
|
+
function runtimeScopeKey(commandIds) {
|
|
848
|
+
const selected = CANONICAL_COMMAND_IDS.filter((id) => commandIds.includes(id));
|
|
849
|
+
return selected.length > 0 ? selected.join("-") : "integrated";
|
|
850
|
+
}
|
|
851
|
+
function readDevRuntimeStateFile(path) {
|
|
789
852
|
try {
|
|
790
|
-
const parsed = JSON.parse(readFileSync(
|
|
853
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
791
854
|
if (!Number.isInteger(parsed.pid) || typeof parsed.tenantRoot !== "string" || typeof parsed.startedAt !== "string") {
|
|
792
855
|
return null;
|
|
793
856
|
}
|
|
857
|
+
const commandIds = Array.isArray(parsed.commandIds) ? parsed.commandIds.filter((id) => CANONICAL_COMMAND_IDS.includes(id)) : void 0;
|
|
794
858
|
return {
|
|
795
859
|
pid: parsed.pid,
|
|
796
860
|
tenantRoot: parsed.tenantRoot,
|
|
797
|
-
startedAt: parsed.startedAt
|
|
861
|
+
startedAt: parsed.startedAt,
|
|
862
|
+
...commandIds ? { commandIds } : {},
|
|
863
|
+
statePath: path
|
|
798
864
|
};
|
|
799
865
|
} catch {
|
|
800
866
|
return null;
|
|
801
867
|
}
|
|
802
868
|
}
|
|
803
|
-
function
|
|
804
|
-
const
|
|
869
|
+
function listDevRuntimeStates(tenantRoot) {
|
|
870
|
+
const states = [];
|
|
871
|
+
const legacy = readDevRuntimeStateFile(legacyDevRuntimeStatePath(tenantRoot));
|
|
872
|
+
if (legacy) {
|
|
873
|
+
states.push(legacy);
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
for (const entry of readdirSync(devRuntimeStateDir(tenantRoot))) {
|
|
877
|
+
if (!entry.startsWith("runtime-") || !entry.endsWith(".json")) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const state = readDevRuntimeStateFile(resolve(devRuntimeStateDir(tenantRoot), entry));
|
|
881
|
+
if (state) {
|
|
882
|
+
states.push(state);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
return states;
|
|
888
|
+
}
|
|
889
|
+
function runtimeStateOverlaps(state, commandIds) {
|
|
890
|
+
if (!state.commandIds || state.commandIds.length === 0) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
return state.commandIds.some((id) => commandIds.includes(id));
|
|
894
|
+
}
|
|
895
|
+
function listLiveOverlappingDevRuntimeStates(tenantRoot, commandIds, processIsAlive) {
|
|
896
|
+
const live = [];
|
|
897
|
+
for (const state of listDevRuntimeStates(tenantRoot)) {
|
|
898
|
+
const statePath = state.statePath;
|
|
899
|
+
if (!statePath || !runtimeStateOverlaps(state, commandIds)) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (state.pid === process.pid) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (!processIsAlive(state.pid)) {
|
|
906
|
+
rmSync(statePath, { force: true });
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
live.push(state);
|
|
910
|
+
}
|
|
911
|
+
return live;
|
|
912
|
+
}
|
|
913
|
+
function parsePortFromUrl(value) {
|
|
914
|
+
if (!value) return null;
|
|
915
|
+
try {
|
|
916
|
+
const url = new URL(value);
|
|
917
|
+
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
918
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function requiredDevPorts(plan) {
|
|
924
|
+
const ports = [];
|
|
925
|
+
for (const command of plan.commands) {
|
|
926
|
+
if (command.id === "web") {
|
|
927
|
+
const port = parsePortFromUrl(plan.webUrl ?? void 0);
|
|
928
|
+
if (port) ports.push(port);
|
|
929
|
+
}
|
|
930
|
+
if (command.id === "api") {
|
|
931
|
+
const port = parsePortFromUrl(plan.apiBaseUrl);
|
|
932
|
+
if (port) ports.push(port);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return [...new Set(ports)];
|
|
936
|
+
}
|
|
937
|
+
function formatPortOwner(owner) {
|
|
938
|
+
return `port ${owner.port}${owner.pid ? ` pid ${owner.pid}` : ""}${owner.processName ? ` (${owner.processName})` : ""}`;
|
|
939
|
+
}
|
|
940
|
+
function writeCurrentDevRuntimeState(tenantRoot, commandIds) {
|
|
941
|
+
const outputPath = devRuntimeStatePath(tenantRoot, runtimeScopeKey(commandIds));
|
|
805
942
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
806
943
|
writeFileSync(
|
|
807
944
|
outputPath,
|
|
808
945
|
`${JSON.stringify({
|
|
809
946
|
pid: process.pid,
|
|
810
947
|
tenantRoot,
|
|
948
|
+
commandIds,
|
|
811
949
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
812
950
|
}, null, 2)}
|
|
813
951
|
`,
|
|
814
952
|
"utf8"
|
|
815
953
|
);
|
|
954
|
+
return outputPath;
|
|
816
955
|
}
|
|
817
|
-
function removeCurrentDevRuntimeState(tenantRoot) {
|
|
818
|
-
const
|
|
956
|
+
function removeCurrentDevRuntimeState(tenantRoot, commandIds) {
|
|
957
|
+
const statePath = devRuntimeStatePath(tenantRoot, runtimeScopeKey(commandIds));
|
|
958
|
+
const state = readDevRuntimeStateFile(statePath);
|
|
819
959
|
if (!state || state.pid !== process.pid) {
|
|
820
960
|
return;
|
|
821
961
|
}
|
|
822
|
-
rmSync(
|
|
962
|
+
rmSync(statePath, { force: true });
|
|
823
963
|
}
|
|
824
964
|
async function waitForProcessExit(pid, processIsAlive, timeoutMs) {
|
|
825
965
|
const startedAt = Date.now();
|
|
@@ -831,38 +971,106 @@ async function waitForProcessExit(pid, processIsAlive, timeoutMs) {
|
|
|
831
971
|
}
|
|
832
972
|
return !processIsAlive(pid);
|
|
833
973
|
}
|
|
834
|
-
async function
|
|
835
|
-
const state
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
974
|
+
async function stopPreviousDevRuntimes(tenantRoot, commandIds, options, deps) {
|
|
975
|
+
for (const state of listLiveOverlappingDevRuntimeStates(tenantRoot, commandIds, deps.processIsAlive)) {
|
|
976
|
+
const statePath = state.statePath;
|
|
977
|
+
if (!statePath) continue;
|
|
978
|
+
emitEvent(options, deps.write, {
|
|
979
|
+
type: "replace",
|
|
980
|
+
message: `Stopping previous Treeseed dev runtime (${state.pid}) before starting overlapping surfaces.`,
|
|
981
|
+
detail: { pid: state.pid, startedAt: state.startedAt, commandIds: state.commandIds ?? null }
|
|
982
|
+
});
|
|
983
|
+
try {
|
|
984
|
+
deps.killProcess(state.pid, "SIGTERM");
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
987
|
+
if (await waitForProcessExit(state.pid, deps.processIsAlive, options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS)) {
|
|
988
|
+
rmSync(statePath, { force: true });
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
deps.killProcess(state.pid, "SIGKILL");
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
await waitForProcessExit(state.pid, deps.processIsAlive, DEFAULT_KILL_GRACE_MS);
|
|
844
996
|
rmSync(statePath, { force: true });
|
|
845
|
-
return;
|
|
846
997
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
998
|
+
}
|
|
999
|
+
async function stopPortOwners(owners, options, deps) {
|
|
1000
|
+
const pids = [...new Set(owners.map((owner) => owner.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid))];
|
|
1001
|
+
for (const pid of pids) {
|
|
1002
|
+
emitEvent(options, deps.write, {
|
|
1003
|
+
type: "replace",
|
|
1004
|
+
message: `Stopping service on required dev port (pid ${pid}).`,
|
|
1005
|
+
detail: owners.filter((owner) => owner.pid === pid)
|
|
1006
|
+
});
|
|
1007
|
+
try {
|
|
1008
|
+
deps.killProcess(pid, "SIGTERM");
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
if (await waitForProcessExit(pid, deps.processIsAlive, options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS)) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
deps.killProcess(pid, "SIGKILL");
|
|
1016
|
+
} catch {
|
|
1017
|
+
}
|
|
1018
|
+
await waitForProcessExit(pid, deps.processIsAlive, DEFAULT_KILL_GRACE_MS);
|
|
855
1019
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1020
|
+
}
|
|
1021
|
+
async function prepareDevRuntimeSlots(plan, options, deps) {
|
|
1022
|
+
const commandIds = plan.commands.map((command) => command.id);
|
|
1023
|
+
const liveRuntimeStates = listLiveOverlappingDevRuntimeStates(plan.tenantRoot, commandIds, deps.processIsAlive);
|
|
1024
|
+
const ports = requiredDevPorts(plan);
|
|
1025
|
+
const portOwners = deps.inspectPortOwners(ports).filter((owner) => owner.pid !== process.pid);
|
|
1026
|
+
if (options.force !== true) {
|
|
1027
|
+
if (liveRuntimeStates.length > 0 || portOwners.length > 0) {
|
|
1028
|
+
emitEvent(options, deps.write, {
|
|
1029
|
+
type: "error",
|
|
1030
|
+
status: "existing-service",
|
|
1031
|
+
message: [
|
|
1032
|
+
"Treeseed dev found an existing runtime or service on a required port.",
|
|
1033
|
+
"Stop it first, or rerun with --force to terminate overlapping Treeseed dev services and port owners."
|
|
1034
|
+
].join(" "),
|
|
1035
|
+
detail: {
|
|
1036
|
+
runtimes: liveRuntimeStates.map((state) => ({
|
|
1037
|
+
pid: state.pid,
|
|
1038
|
+
startedAt: state.startedAt,
|
|
1039
|
+
commandIds: state.commandIds ?? null,
|
|
1040
|
+
statePath: state.statePath ?? null
|
|
1041
|
+
})),
|
|
1042
|
+
ports: portOwners.map((owner) => ({ ...owner, label: formatPortOwner(owner) }))
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
return true;
|
|
859
1048
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1049
|
+
await stopPreviousDevRuntimes(plan.tenantRoot, commandIds, options, deps);
|
|
1050
|
+
if (portOwners.length > 0) {
|
|
1051
|
+
const ownersWithoutPid = portOwners.filter((owner) => owner.pid == null);
|
|
1052
|
+
if (ownersWithoutPid.length > 0) {
|
|
1053
|
+
emitEvent(options, deps.write, {
|
|
1054
|
+
type: "error",
|
|
1055
|
+
status: "existing-service",
|
|
1056
|
+
message: `Cannot force-stop required dev ports because some listeners did not expose process ids: ${ownersWithoutPid.map(formatPortOwner).join(", ")}.`,
|
|
1057
|
+
detail: ownersWithoutPid
|
|
1058
|
+
});
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
await stopPortOwners(portOwners, options, deps);
|
|
863
1062
|
}
|
|
864
|
-
|
|
865
|
-
|
|
1063
|
+
const remainingPortOwners = deps.inspectPortOwners(ports).filter((owner) => owner.pid !== process.pid);
|
|
1064
|
+
if (remainingPortOwners.length > 0) {
|
|
1065
|
+
emitEvent(options, deps.write, {
|
|
1066
|
+
type: "error",
|
|
1067
|
+
status: "existing-service",
|
|
1068
|
+
message: `Required dev ports are still occupied after --force: ${remainingPortOwners.map(formatPortOwner).join(", ")}.`,
|
|
1069
|
+
detail: remainingPortOwners
|
|
1070
|
+
});
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
866
1074
|
}
|
|
867
1075
|
function emitEvent(options, write, event, stream = event.type === "error" ? "stderr" : "stdout") {
|
|
868
1076
|
if (options.json) {
|
|
@@ -875,6 +1083,20 @@ function emitEvent(options, write, event, stream = event.type === "error" ? "std
|
|
|
875
1083
|
write(`${surface} ${String(message)}
|
|
876
1084
|
`, stream);
|
|
877
1085
|
}
|
|
1086
|
+
function createDevLogWrite(baseWrite, logPath) {
|
|
1087
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
1088
|
+
appendFileSync(logPath, `${JSON.stringify({
|
|
1089
|
+
schemaVersion: 1,
|
|
1090
|
+
kind: "treeseed.dev.log",
|
|
1091
|
+
type: "start",
|
|
1092
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1093
|
+
})}
|
|
1094
|
+
`, "utf8");
|
|
1095
|
+
return (line, stream) => {
|
|
1096
|
+
baseWrite(line, stream);
|
|
1097
|
+
appendFileSync(logPath, line, "utf8");
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
878
1100
|
function runTreeseedIntegratedDevReset(reset, options, deps) {
|
|
879
1101
|
if (!reset?.enabled) {
|
|
880
1102
|
return null;
|
|
@@ -979,6 +1201,8 @@ function writePlan(plan, options, write) {
|
|
|
979
1201
|
`, "stdout");
|
|
980
1202
|
}
|
|
981
1203
|
write(`api: ${plan.apiBaseUrl}
|
|
1204
|
+
`, "stdout");
|
|
1205
|
+
write(`log: ${plan.logPath}
|
|
982
1206
|
`, "stdout");
|
|
983
1207
|
for (const [name, runtime] of Object.entries(plan.localRuntimes)) {
|
|
984
1208
|
write(`runtime ${name}: ${runtime.selected} (${runtime.provider}, requested ${runtime.requested})${runtime.reason ? ` - ${runtime.reason}` : ""}
|
|
@@ -1216,7 +1440,7 @@ function failedSetupMessage(failed) {
|
|
|
1216
1440
|
}
|
|
1217
1441
|
async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
1218
1442
|
const tenantRoot = resolve(options.cwd ?? process.cwd());
|
|
1219
|
-
|
|
1443
|
+
let write = deps.write ?? defaultWrite;
|
|
1220
1444
|
const spawnProcess = deps.spawn ?? spawn;
|
|
1221
1445
|
const spawnSyncProcess = deps.spawnSync ?? spawnSync;
|
|
1222
1446
|
const onSignal = deps.onSignal ?? defaultSignalRegistrar;
|
|
@@ -1228,6 +1452,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1228
1452
|
const prepareEnvironment = deps.prepareEnvironment ?? defaultPrepareEnvironment;
|
|
1229
1453
|
const removePath = deps.removePath ?? defaultRemovePath;
|
|
1230
1454
|
const stopMailpit = deps.stopMailpitContainers ?? stopKnownMailpitContainers;
|
|
1455
|
+
const inspectPortOwners = deps.inspectPortOwners ?? defaultInspectPortOwners;
|
|
1231
1456
|
prepareEnvironment(tenantRoot);
|
|
1232
1457
|
const plan = createTreeseedIntegratedDevPlan({
|
|
1233
1458
|
...options,
|
|
@@ -1241,8 +1466,15 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1241
1466
|
writePlan(plan, options, write);
|
|
1242
1467
|
return 0;
|
|
1243
1468
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1469
|
+
const commandIds = plan.commands.map((command) => command.id);
|
|
1470
|
+
write = createDevLogWrite(write, plan.logPath);
|
|
1471
|
+
emitEvent(options, write, {
|
|
1472
|
+
type: "log",
|
|
1473
|
+
message: `Writing Treeseed dev logs to ${plan.logPath}.`,
|
|
1474
|
+
detail: { logPath: plan.logPath }
|
|
1475
|
+
});
|
|
1476
|
+
if (!await prepareDevRuntimeSlots(plan, options, { write, killProcess, processIsAlive, inspectPortOwners })) {
|
|
1477
|
+
return 1;
|
|
1246
1478
|
}
|
|
1247
1479
|
const resetResults = runTreeseedIntegratedDevReset(plan.reset, options, {
|
|
1248
1480
|
write,
|
|
@@ -1258,7 +1490,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1258
1490
|
});
|
|
1259
1491
|
return 1;
|
|
1260
1492
|
}
|
|
1261
|
-
writeCurrentDevRuntimeState(tenantRoot);
|
|
1493
|
+
writeCurrentDevRuntimeState(tenantRoot, commandIds);
|
|
1262
1494
|
const children = /* @__PURE__ */ new Map();
|
|
1263
1495
|
const commandsById = new Map(plan.commands.map((command) => [command.id, command]));
|
|
1264
1496
|
const requiredSurfaceIds = new Set(plan.readyChecks.filter((check) => check.required).map((check) => check.id));
|
|
@@ -1311,7 +1543,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
1311
1543
|
for (const dispose of disposers) {
|
|
1312
1544
|
dispose();
|
|
1313
1545
|
}
|
|
1314
|
-
removeCurrentDevRuntimeState(tenantRoot);
|
|
1546
|
+
removeCurrentDevRuntimeState(tenantRoot, commandIds);
|
|
1315
1547
|
emitEvent(
|
|
1316
1548
|
options,
|
|
1317
1549
|
write,
|