@t3x-dev/local 0.1.2

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 ADDED
@@ -0,0 +1,12 @@
1
+ # `@t3x-dev/local`
2
+
3
+ Minimal local entry package for T3X.
4
+
5
+ Current PR2 scope:
6
+
7
+ - `t3x-local start` launches the locally built API + Web
8
+ - `t3x` forwards to `@t3x-dev/cli`
9
+ - `t3x-mcp` forwards to `@t3x-dev/mcp`
10
+
11
+ This phase uses local build artifacts already present in the monorepo.
12
+ It does not download runtime assets yet.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ checkHttpHealth,
4
+ getPortStatus,
5
+ getVersionLockReport,
6
+ getVersionSnapshot,
7
+ resolveStartOptions,
8
+ runStartCommand
9
+ } from "../chunk-KNAXB6AU.js";
10
+ import {
11
+ clearRuntimeState,
12
+ getLocalPaths,
13
+ getMissingStartArtifacts,
14
+ getRuntimeMetadataPaths,
15
+ getRuntimeProcessStatus,
16
+ readRuntimeState,
17
+ terminatePid
18
+ } from "../chunk-E4LL4ESG.js";
19
+
20
+ // src/bin/t3x-local.ts
21
+ import { Command } from "commander";
22
+
23
+ // src/commands/doctor.ts
24
+ import fs from "fs";
25
+ async function runDoctorCommand(input = {}) {
26
+ const paths = getLocalPaths();
27
+ const metadataPaths = getRuntimeMetadataPaths(paths);
28
+ const configured = resolveStartOptions(input, paths, process.env);
29
+ const state = await readRuntimeState(paths);
30
+ const versions = getVersionSnapshot(paths);
31
+ const versionLock = getVersionLockReport(paths);
32
+ const effective = {
33
+ dataDir: input.dataDir ? configured.dataDir : state?.dataDir ?? configured.dataDir,
34
+ apiPort: input.apiPort ?? state?.apiPort ?? configured.apiPort,
35
+ webPort: input.webPort ?? state?.webPort ?? configured.webPort
36
+ };
37
+ const missingArtifacts = getMissingStartArtifacts(paths);
38
+ const processStatus = state ? getRuntimeProcessStatus(state) : null;
39
+ const [apiPortStatus, webPortStatus, apiHealth, webHealth] = await Promise.all([
40
+ getPortStatus(effective.apiPort),
41
+ getPortStatus(effective.webPort),
42
+ checkHttpHealth(`http://127.0.0.1:${effective.apiPort}/health`),
43
+ checkHttpHealth(`http://127.0.0.1:${effective.webPort}/health`)
44
+ ]);
45
+ const problems = [];
46
+ if (missingArtifacts.length > 0) {
47
+ problems.push(
48
+ ...missingArtifacts.map((item) => `Missing build artifact: ${item.label} (${item.path})`)
49
+ );
50
+ }
51
+ if (!fs.existsSync(paths.cliEntryPath)) {
52
+ problems.push(`Missing CLI shim target: ${paths.cliEntryPath}`);
53
+ }
54
+ if (!fs.existsSync(paths.mcpEntryPath)) {
55
+ problems.push(`Missing MCP shim target: ${paths.mcpEntryPath}`);
56
+ }
57
+ if (state && processStatus && (!processStatus.apiRunning || !processStatus.webRunning)) {
58
+ problems.push("Runtime state file exists but one or more recorded PIDs are not running");
59
+ }
60
+ if (!state && !apiPortStatus.available) {
61
+ problems.push(`API port ${effective.apiPort} is occupied outside the local runtime`);
62
+ }
63
+ if (!state && !webPortStatus.available) {
64
+ problems.push(`Web port ${effective.webPort} is occupied outside the local runtime`);
65
+ }
66
+ if (state && processStatus?.apiRunning && !apiHealth.ok) {
67
+ problems.push(`API process is running but health check failed: ${apiHealth.details}`);
68
+ }
69
+ if (state && processStatus?.webRunning && !webHealth.ok) {
70
+ problems.push(`Web process is running but health check failed: ${webHealth.details}`);
71
+ }
72
+ if (versionLock.problems.length > 0) {
73
+ problems.push(...versionLock.problems);
74
+ }
75
+ const status = getDoctorStatus(state, processStatus, apiHealth.ok, webHealth.ok);
76
+ console.log("[t3x-local] Doctor");
77
+ console.log(`Status: ${status}`);
78
+ console.log(
79
+ `Versions: local=${versions.local} api=${versions.api} web=${versions.web} cli=${versions.cli} mcp=${versions.mcp}`
80
+ );
81
+ console.log(
82
+ `Fixed version: ${versions.fixedVersion} (${versionLock.problems.length === 0 ? "ok" : "mismatch"})`
83
+ );
84
+ console.log(`Node: ${versions.node} (${versions.platform})`);
85
+ console.log(`Repo root: ${paths.repoRoot}`);
86
+ console.log(`Runtime source: ${paths.runtimeSource}`);
87
+ console.log(`Installed runtime dir: ${paths.installedRuntimeDir}`);
88
+ console.log(
89
+ `Configured data dir: ${effective.dataDir} (${fs.existsSync(effective.dataDir) ? "exists" : "missing"})`
90
+ );
91
+ console.log(
92
+ `State file: ${metadataPaths.stateFilePath} (${fs.existsSync(metadataPaths.stateFilePath) ? "present" : "absent"})`
93
+ );
94
+ console.log(`API log: ${metadataPaths.apiLogPath}`);
95
+ console.log(`Web log: ${metadataPaths.webLogPath}`);
96
+ console.log(
97
+ `CLI target: ${paths.cliEntryPath} (${fs.existsSync(paths.cliEntryPath) ? "ok" : "missing"})`
98
+ );
99
+ console.log(
100
+ `MCP target: ${paths.mcpEntryPath} (${fs.existsSync(paths.mcpEntryPath) ? "ok" : "missing"})`
101
+ );
102
+ console.log(
103
+ `API: port=${effective.apiPort} pid=${state?.apiPid ?? "n/a"} portStatus=${apiPortStatus.details} health=${formatHealth(apiHealth)}`
104
+ );
105
+ console.log(
106
+ `Web: port=${effective.webPort} pid=${state?.webPid ?? "n/a"} portStatus=${webPortStatus.details} health=${formatHealth(webHealth)}`
107
+ );
108
+ if (missingArtifacts.length === 0) {
109
+ console.log("Artifacts: all required local build artifacts are present");
110
+ } else {
111
+ console.log("Artifacts: missing required build outputs");
112
+ }
113
+ if (problems.length > 0) {
114
+ console.log("Problems:");
115
+ for (const problem of problems) {
116
+ console.log(`- ${problem}`);
117
+ }
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ console.log("Problems: none");
122
+ }
123
+ function getDoctorStatus(state, processStatus, apiHealthy, webHealthy) {
124
+ if (!state) {
125
+ return "stopped";
126
+ }
127
+ if (!processStatus?.apiRunning && !processStatus?.webRunning) {
128
+ return "stale-state";
129
+ }
130
+ if (processStatus.apiRunning && processStatus.webRunning && apiHealthy && webHealthy) {
131
+ return "running";
132
+ }
133
+ return "degraded";
134
+ }
135
+ function formatHealth(result) {
136
+ return result.ok ? `ok (${result.details})` : `failed (${result.details})`;
137
+ }
138
+
139
+ // src/commands/reset.ts
140
+ import fs2 from "fs/promises";
141
+
142
+ // src/commands/stop.ts
143
+ async function runStopCommand() {
144
+ const paths = getLocalPaths();
145
+ const state = await readRuntimeState(paths);
146
+ if (!state) {
147
+ console.log("[t3x-local] No local runtime state file found. Nothing to stop.");
148
+ return;
149
+ }
150
+ const before = getRuntimeProcessStatus(state);
151
+ if (!before.apiRunning && !before.webRunning) {
152
+ await clearRuntimeState(paths);
153
+ console.log("[t3x-local] Runtime state was stale. Cleared local state files.");
154
+ return;
155
+ }
156
+ const webResult = await terminatePid(state.webPid);
157
+ const apiResult = await terminatePid(state.apiPid);
158
+ await clearRuntimeState(paths);
159
+ console.log(
160
+ `[t3x-local] Web process ${state.webPid}: ${webResult === "stopped" ? "stopped" : "not running"}`
161
+ );
162
+ console.log(
163
+ `[t3x-local] API process ${state.apiPid}: ${apiResult === "stopped" ? "stopped" : "not running"}`
164
+ );
165
+ console.log("[t3x-local] Cleared local runtime state.");
166
+ }
167
+
168
+ // src/commands/reset.ts
169
+ async function runResetCommand(input = {}) {
170
+ const paths = getLocalPaths();
171
+ const state = await readRuntimeState(paths);
172
+ const configured = resolveStartOptions({ dataDir: input.dataDir }, paths, process.env);
173
+ const dataDir = input.dataDir ? configured.dataDir : state?.dataDir ?? configured.dataDir;
174
+ const processStatus = state ? getRuntimeProcessStatus(state) : null;
175
+ if (processStatus && (processStatus.apiRunning || processStatus.webRunning)) {
176
+ if (!input.force) {
177
+ throw new Error(
178
+ "[t3x-local] Local runtime is still running. Run `t3x-local stop` first or use `t3x-local reset --force`."
179
+ );
180
+ }
181
+ await runStopCommand();
182
+ }
183
+ await Promise.all([
184
+ fs2.rm(dataDir, { recursive: true, force: true }),
185
+ fs2.rm(paths.localRuntimeRoot, { recursive: true, force: true })
186
+ ]);
187
+ console.log(`[t3x-local] Removed data dir: ${dataDir}`);
188
+ console.log(`[t3x-local] Removed runtime dir: ${paths.localRuntimeRoot}`);
189
+ }
190
+
191
+ // src/bin/t3x-local.ts
192
+ var program = new Command();
193
+ program.name("t3x-local").description("T3X local runtime entrypoint").version("0.1.1");
194
+ program.command("start").description("Start the local API and Web runtimes in the background").option("--data-dir <path>", "Embedded PostgreSQL data directory").option("--api-port <port>", "API port (default: 8000)", parseInteger).option("--web-port <port>", "Web port (default: 3000)", parseInteger).action(async (options) => {
195
+ await runStartCommand(options);
196
+ });
197
+ program.command("stop").description("Stop the local API and Web runtimes").action(async () => {
198
+ await runStopCommand();
199
+ });
200
+ program.command("doctor").description("Print local runtime diagnostics").option("--data-dir <path>", "Embedded PostgreSQL data directory").option("--api-port <port>", "API port override", parseInteger).option("--web-port <port>", "Web port override", parseInteger).action(async (options) => {
201
+ await runDoctorCommand(options);
202
+ });
203
+ program.command("reset").description("Remove local runtime state and data").option("--data-dir <path>", "Embedded PostgreSQL data directory").option("--force", "Stop the runtime before clearing local data").action(async (options) => {
204
+ await runResetCommand(options);
205
+ });
206
+ program.parseAsync().catch((error) => {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ console.error(`[t3x-local] ${message}`);
209
+ process.exit(1);
210
+ });
211
+ function parseInteger(value) {
212
+ const parsed = Number.parseInt(value, 10);
213
+ if (!Number.isFinite(parsed)) {
214
+ throw new Error(`Invalid integer: ${value}`);
215
+ }
216
+ return parsed;
217
+ }
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getLocalPaths,
4
+ readRuntimeState,
5
+ runNodeScript
6
+ } from "../chunk-E4LL4ESG.js";
7
+
8
+ // src/bin/t3x-mcp.ts
9
+ var paths = getLocalPaths();
10
+ var runtimeState = await readRuntimeState(paths);
11
+ if (runtimeState) {
12
+ process.env.T3X_DATA_DIR ??= runtimeState.dataDir;
13
+ }
14
+ await runNodeScript({
15
+ name: "t3x-mcp",
16
+ entryPath: paths.mcpEntryPath,
17
+ args: process.argv.slice(2),
18
+ env: process.env
19
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getLocalPaths,
4
+ readRuntimeState,
5
+ runNodeScript
6
+ } from "../chunk-E4LL4ESG.js";
7
+
8
+ // src/bin/t3x.ts
9
+ var paths = getLocalPaths();
10
+ var runtimeState = await readRuntimeState(paths);
11
+ if (runtimeState) {
12
+ process.env.T3X_API_URL ??= `${runtimeState.apiUrl}/api`;
13
+ process.env.T3X_WEB_URL ??= runtimeState.webUrl;
14
+ }
15
+ await runNodeScript({
16
+ name: "t3x",
17
+ entryPath: paths.cliEntryPath,
18
+ args: process.argv.slice(2),
19
+ env: process.env
20
+ });
@@ -0,0 +1,381 @@
1
+ // src/runtime/paths.ts
2
+ import fs from "fs";
3
+ import { createRequire } from "module";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ var require2 = createRequire(import.meta.url);
8
+ function getLocalPaths() {
9
+ const packageDir = findLocalPackageDir(path.dirname(fileURLToPath(import.meta.url)));
10
+ const repoRoot = findRepoRoot(packageDir);
11
+ const appHomeDir = resolveAppHomeDir(repoRoot);
12
+ const runtimeManifestPath = path.join(packageDir, "runtime-manifest.json");
13
+ const installedRuntimeDir = resolveInstalledRuntimeDir(packageDir);
14
+ const runtimeSource = resolveRuntimeSource(repoRoot, installedRuntimeDir);
15
+ return {
16
+ packageDir,
17
+ repoRoot,
18
+ appHomeDir,
19
+ defaultDataDir: path.join(appHomeDir, "pg-data"),
20
+ localRuntimeRoot: path.join(appHomeDir, "local-runtime"),
21
+ runtimeManifestPath,
22
+ installedRuntimeDir,
23
+ runtimeSource,
24
+ apiEntryPath: runtimeSource === "installed" ? path.join(installedRuntimeDir, "api", "dist", "index.js") : path.join(assertRepoRoot(repoRoot), "apps", "api", "dist", "index.js"),
25
+ webStandaloneDir: runtimeSource === "installed" ? path.join(installedRuntimeDir, "web", "standalone") : path.join(assertRepoRoot(repoRoot), "apps", "web", ".next", "standalone"),
26
+ webStandaloneServerPath: runtimeSource === "installed" ? path.join(installedRuntimeDir, "web", "standalone", "apps", "web", "server.js") : path.join(
27
+ assertRepoRoot(repoRoot),
28
+ "apps",
29
+ "web",
30
+ ".next",
31
+ "standalone",
32
+ "apps",
33
+ "web",
34
+ "server.js"
35
+ ),
36
+ webStaticDir: runtimeSource === "installed" ? path.join(installedRuntimeDir, "web", "static") : path.join(assertRepoRoot(repoRoot), "apps", "web", ".next", "static"),
37
+ webPublicDir: runtimeSource === "installed" ? path.join(installedRuntimeDir, "web", "public") : path.join(assertRepoRoot(repoRoot), "apps", "web", "public"),
38
+ cliEntryPath: resolvePackageBinEntry({
39
+ packageName: "@t3x-dev/cli",
40
+ binName: "t3x",
41
+ repoRoot,
42
+ workspaceRelativeDir: path.join("apps", "cli")
43
+ }),
44
+ mcpEntryPath: resolvePackageBinEntry({
45
+ packageName: "@t3x-dev/mcp",
46
+ binName: "t3x-mcp",
47
+ repoRoot,
48
+ workspaceRelativeDir: path.join("apps", "mcp")
49
+ })
50
+ };
51
+ }
52
+ function findLocalPackageDir(startDir) {
53
+ let current = startDir;
54
+ while (current !== path.dirname(current)) {
55
+ const packageJsonPath = path.join(current, "package.json");
56
+ if (fs.existsSync(packageJsonPath)) {
57
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
58
+ if (packageJson.name === "@t3x-dev/local") {
59
+ return current;
60
+ }
61
+ }
62
+ current = path.dirname(current);
63
+ }
64
+ throw new Error(`Could not locate @t3x-dev/local package root above ${startDir}`);
65
+ }
66
+ function getMissingStartArtifacts(paths) {
67
+ const required = [
68
+ { label: "API runtime entry", path: paths.apiEntryPath },
69
+ { label: "Web standalone server", path: paths.webStandaloneServerPath },
70
+ { label: "Web static assets", path: paths.webStaticDir },
71
+ { label: "Web public assets", path: paths.webPublicDir }
72
+ ];
73
+ return required.filter((item) => !fs.existsSync(item.path));
74
+ }
75
+ function formatMissingArtifacts(items, paths) {
76
+ const details = items.map((item) => `- ${item.label}: ${item.path}`).join("\n");
77
+ const hint = paths.runtimeSource === "installed" ? "Reinstall `@t3x-dev/local`, or rerun the postinstall download with `T3X_LOCAL_RUNTIME_MIRROR` configured." : "Run `pnpm build:api-server` and `pnpm build:webui` from the repo root first.";
78
+ return `Missing local runtime artifacts required by \`t3x-local start\`.
79
+ ${details}
80
+ ` + hint;
81
+ }
82
+ function resolvePlatformKey() {
83
+ return `${process.platform}-${process.arch}`;
84
+ }
85
+ function resolveRuntimeSource(repoRoot, installedRuntimeDir) {
86
+ if (process.env.T3X_LOCAL_RUNTIME_DIR) {
87
+ return "installed";
88
+ }
89
+ if (!repoRoot) {
90
+ return "installed";
91
+ }
92
+ return fs.existsSync(path.join(installedRuntimeDir, "api", "dist", "index.js")) ? "installed" : "workspace";
93
+ }
94
+ function resolveAppHomeDir(repoRoot) {
95
+ if (repoRoot) {
96
+ return path.join(repoRoot, ".t3x");
97
+ }
98
+ return path.join(os.homedir(), ".t3x");
99
+ }
100
+ function resolveInstalledRuntimeDir(packageDir) {
101
+ if (process.env.T3X_LOCAL_RUNTIME_DIR) {
102
+ return path.resolve(process.env.T3X_LOCAL_RUNTIME_DIR);
103
+ }
104
+ return path.join(packageDir, "runtime", resolvePlatformKey());
105
+ }
106
+ function findRepoRoot(startDir) {
107
+ let current = startDir;
108
+ while (current !== path.dirname(current)) {
109
+ if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) && fs.existsSync(path.join(current, "apps", "api", "package.json")) && fs.existsSync(path.join(current, "apps", "web", "package.json")) && fs.existsSync(path.join(current, "apps", "local", "package.json"))) {
110
+ return current;
111
+ }
112
+ current = path.dirname(current);
113
+ }
114
+ return null;
115
+ }
116
+ function resolvePackageBinEntry(options) {
117
+ const installedPackageJsonPath = resolveInstalledPackageJson(options.packageName);
118
+ if (installedPackageJsonPath) {
119
+ return resolveBinFromPackageJson(installedPackageJsonPath, options.binName);
120
+ }
121
+ if (!options.repoRoot) {
122
+ throw new Error(`Could not resolve installed package ${options.packageName}`);
123
+ }
124
+ return resolveBinFromPackageJson(
125
+ path.join(options.repoRoot, options.workspaceRelativeDir, "package.json"),
126
+ options.binName
127
+ );
128
+ }
129
+ function resolveInstalledPackageJson(packageName) {
130
+ try {
131
+ const entryPath = require2.resolve(packageName);
132
+ return findNearestPackageJson(entryPath);
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function resolveBinFromPackageJson(packageJsonPath, binName) {
138
+ if (!fs.existsSync(packageJsonPath)) {
139
+ throw new Error(`Missing package.json for ${binName}: ${packageJsonPath}`);
140
+ }
141
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
142
+ const relativeEntry = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
143
+ if (!relativeEntry) {
144
+ const packageName = packageJson.name ?? packageJsonPath;
145
+ throw new Error(`Package ${packageName} does not declare a \`${binName}\` bin entry`);
146
+ }
147
+ return path.resolve(path.dirname(packageJsonPath), relativeEntry);
148
+ }
149
+ function findNearestPackageJson(startPath) {
150
+ let current = path.dirname(startPath);
151
+ while (current !== path.dirname(current)) {
152
+ const packageJsonPath = path.join(current, "package.json");
153
+ if (fs.existsSync(packageJsonPath)) {
154
+ return packageJsonPath;
155
+ }
156
+ current = path.dirname(current);
157
+ }
158
+ throw new Error(`Could not locate package.json above ${startPath}`);
159
+ }
160
+ function assertRepoRoot(repoRoot) {
161
+ if (!repoRoot) {
162
+ throw new Error("Workspace runtime source requested outside the monorepo");
163
+ }
164
+ return repoRoot;
165
+ }
166
+
167
+ // src/runtime/pid.ts
168
+ import fs2 from "fs/promises";
169
+ import path2 from "path";
170
+ var STATE_SCHEMA_VERSION = 1;
171
+ function getRuntimeMetadataPaths(paths) {
172
+ return {
173
+ runtimeRoot: paths.localRuntimeRoot,
174
+ logsDir: path2.join(paths.localRuntimeRoot, "logs"),
175
+ stateFilePath: path2.join(paths.localRuntimeRoot, "state.json"),
176
+ apiPidFilePath: path2.join(paths.localRuntimeRoot, "api.pid"),
177
+ webPidFilePath: path2.join(paths.localRuntimeRoot, "web.pid"),
178
+ apiLogPath: path2.join(paths.localRuntimeRoot, "logs", "api.log"),
179
+ webLogPath: path2.join(paths.localRuntimeRoot, "logs", "web.log")
180
+ };
181
+ }
182
+ async function ensureRuntimeMetadataDirs(paths) {
183
+ const metadataPaths = getRuntimeMetadataPaths(paths);
184
+ await fs2.mkdir(metadataPaths.runtimeRoot, { recursive: true });
185
+ await fs2.mkdir(metadataPaths.logsDir, { recursive: true });
186
+ return metadataPaths;
187
+ }
188
+ async function readRuntimeState(paths) {
189
+ const { stateFilePath } = getRuntimeMetadataPaths(paths);
190
+ try {
191
+ const raw = await fs2.readFile(stateFilePath, "utf8");
192
+ const parsed = JSON.parse(raw);
193
+ if (parsed.schemaVersion !== STATE_SCHEMA_VERSION) {
194
+ return null;
195
+ }
196
+ return parsed;
197
+ } catch (error) {
198
+ if (isFileNotFoundError(error)) {
199
+ return null;
200
+ }
201
+ throw error;
202
+ }
203
+ }
204
+ async function writeRuntimeState(paths, state) {
205
+ const metadataPaths = await ensureRuntimeMetadataDirs(paths);
206
+ await Promise.all([
207
+ fs2.writeFile(metadataPaths.stateFilePath, `${JSON.stringify(state, null, 2)}
208
+ `, "utf8"),
209
+ fs2.writeFile(metadataPaths.apiPidFilePath, `${state.apiPid}
210
+ `, "utf8"),
211
+ fs2.writeFile(metadataPaths.webPidFilePath, `${state.webPid}
212
+ `, "utf8")
213
+ ]);
214
+ }
215
+ async function clearRuntimeState(paths) {
216
+ const metadataPaths = getRuntimeMetadataPaths(paths);
217
+ await Promise.all([
218
+ fs2.rm(metadataPaths.stateFilePath, { force: true }),
219
+ fs2.rm(metadataPaths.apiPidFilePath, { force: true }),
220
+ fs2.rm(metadataPaths.webPidFilePath, { force: true })
221
+ ]);
222
+ }
223
+ function getRuntimeProcessStatus(state) {
224
+ return {
225
+ apiRunning: isProcessRunning(state.apiPid),
226
+ webRunning: isProcessRunning(state.webPid)
227
+ };
228
+ }
229
+ function isProcessRunning(pid) {
230
+ try {
231
+ process.kill(pid, 0);
232
+ return true;
233
+ } catch (error) {
234
+ return isPermissionError(error);
235
+ }
236
+ }
237
+ async function terminatePid(pid, signal = "SIGTERM", timeoutMs = 5e3) {
238
+ if (!isProcessRunning(pid)) {
239
+ return "not-running";
240
+ }
241
+ process.kill(pid, signal);
242
+ const stopped = await waitForPidExit(pid, timeoutMs);
243
+ if (stopped) {
244
+ return "stopped";
245
+ }
246
+ process.kill(pid, "SIGKILL");
247
+ await waitForPidExit(pid, 2e3);
248
+ return "stopped";
249
+ }
250
+ async function waitForPidExit(pid, timeoutMs) {
251
+ const startedAt = Date.now();
252
+ while (Date.now() - startedAt < timeoutMs) {
253
+ if (!isProcessRunning(pid)) {
254
+ return true;
255
+ }
256
+ await sleep(200);
257
+ }
258
+ return !isProcessRunning(pid);
259
+ }
260
+ function sleep(ms) {
261
+ return new Promise((resolve) => {
262
+ setTimeout(resolve, ms);
263
+ });
264
+ }
265
+ function isFileNotFoundError(error) {
266
+ return Boolean(
267
+ error && typeof error === "object" && "code" in error && error.code === "ENOENT"
268
+ );
269
+ }
270
+ function isPermissionError(error) {
271
+ return Boolean(
272
+ error && typeof error === "object" && "code" in error && error.code === "EPERM"
273
+ );
274
+ }
275
+
276
+ // src/runtime/spawn.ts
277
+ import { spawn } from "child_process";
278
+ import fs3 from "fs";
279
+ import path3 from "path";
280
+ function spawnNodeScript(options) {
281
+ if (!fs3.existsSync(options.entryPath)) {
282
+ throw new Error(`[t3x-local] Missing ${options.name} entrypoint: ${options.entryPath}`);
283
+ }
284
+ const { stdio, openDescriptors } = buildStdio(options);
285
+ const child = spawn(process.execPath, [options.entryPath, ...options.args ?? []], {
286
+ cwd: options.cwd,
287
+ detached: options.detached,
288
+ env: options.env,
289
+ stdio
290
+ });
291
+ for (const descriptor of openDescriptors) {
292
+ fs3.closeSync(descriptor);
293
+ }
294
+ if (options.detached) {
295
+ child.unref();
296
+ }
297
+ child.on("error", (error) => {
298
+ console.error(`[t3x-local] Failed to start ${options.name}: ${error.message}`);
299
+ });
300
+ return {
301
+ name: options.name,
302
+ child
303
+ };
304
+ }
305
+ async function runNodeScript(options) {
306
+ const spawned = spawnNodeScript(options);
307
+ const exit = await onProcessExit(spawned);
308
+ if (exit.signal) {
309
+ process.kill(process.pid, exit.signal);
310
+ return new Promise(() => {
311
+ });
312
+ }
313
+ process.exit(exit.code ?? 1);
314
+ }
315
+ function buildStdio(options) {
316
+ if (!options.stdoutPath && !options.stderrPath) {
317
+ return {
318
+ stdio: "inherit",
319
+ openDescriptors: []
320
+ };
321
+ }
322
+ const stdoutPath = options.stdoutPath ?? options.stderrPath;
323
+ const stderrPath = options.stderrPath ?? options.stdoutPath;
324
+ if (!stdoutPath || !stderrPath) {
325
+ throw new Error(
326
+ `[t3x-local] Both stdout and stderr log paths must be provided for ${options.name}`
327
+ );
328
+ }
329
+ fs3.mkdirSync(path3.dirname(stdoutPath), { recursive: true });
330
+ fs3.mkdirSync(path3.dirname(stderrPath), { recursive: true });
331
+ const stdoutDescriptor = fs3.openSync(stdoutPath, "a");
332
+ const stderrDescriptor = fs3.openSync(stderrPath, "a");
333
+ return {
334
+ stdio: ["ignore", stdoutDescriptor, stderrDescriptor],
335
+ openDescriptors: [stdoutDescriptor, stderrDescriptor]
336
+ };
337
+ }
338
+ function onProcessExit(processInfo) {
339
+ return new Promise((resolve) => {
340
+ processInfo.child.once("exit", (code, signal) => {
341
+ resolve({
342
+ name: processInfo.name,
343
+ code,
344
+ signal
345
+ });
346
+ });
347
+ });
348
+ }
349
+ async function terminateProcess(processInfo) {
350
+ if (processInfo.child.exitCode !== null || processInfo.child.signalCode !== null) {
351
+ return;
352
+ }
353
+ processInfo.child.kill("SIGTERM");
354
+ await Promise.race([
355
+ onProcessExit(processInfo).then(() => void 0),
356
+ new Promise((resolve) => {
357
+ setTimeout(() => {
358
+ if (processInfo.child.exitCode === null && processInfo.child.signalCode === null) {
359
+ processInfo.child.kill("SIGKILL");
360
+ }
361
+ resolve();
362
+ }, 5e3);
363
+ })
364
+ ]);
365
+ }
366
+
367
+ export {
368
+ getLocalPaths,
369
+ getMissingStartArtifacts,
370
+ formatMissingArtifacts,
371
+ getRuntimeMetadataPaths,
372
+ ensureRuntimeMetadataDirs,
373
+ readRuntimeState,
374
+ writeRuntimeState,
375
+ clearRuntimeState,
376
+ getRuntimeProcessStatus,
377
+ terminatePid,
378
+ spawnNodeScript,
379
+ runNodeScript,
380
+ terminateProcess
381
+ };