@hua-labs/tap 0.1.0

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.
@@ -0,0 +1,272 @@
1
+ type RuntimeName = "claude" | "codex" | "gemini";
2
+ type BridgeMode = "native-push" | "app-server" | "polling";
3
+ type Platform = "win32" | "darwin" | "linux";
4
+ /** Unique, immutable identifier for a runtime instance. e.g. "codex", "codex-reviewer" */
5
+ type InstanceId = string;
6
+ interface AdapterContext {
7
+ commsDir: string;
8
+ repoRoot: string;
9
+ stateDir: string;
10
+ platform: Platform;
11
+ }
12
+ interface ProbeResult {
13
+ installed: boolean;
14
+ configPath: string | null;
15
+ configExists: boolean;
16
+ runtimeCommand: string | null;
17
+ version: string | null;
18
+ canWrite: boolean;
19
+ warnings: string[];
20
+ issues: string[];
21
+ }
22
+ type ArtifactKind = "json-path" | "toml-table" | "file";
23
+ interface OwnedArtifact {
24
+ kind: ArtifactKind;
25
+ path: string;
26
+ selector: string;
27
+ backupPath?: string;
28
+ }
29
+ type PatchOpType = "set" | "merge" | "append" | "create-file";
30
+ interface PatchOp {
31
+ type: PatchOpType;
32
+ path: string;
33
+ key?: string;
34
+ value?: unknown;
35
+ content?: string;
36
+ }
37
+ interface PatchPlan {
38
+ runtime: RuntimeName;
39
+ operations: PatchOp[];
40
+ ownedArtifacts: OwnedArtifact[];
41
+ backupDir: string;
42
+ restartRequired: boolean;
43
+ conflicts: string[];
44
+ warnings: string[];
45
+ }
46
+ interface ApplyResult {
47
+ success: boolean;
48
+ appliedOps: number;
49
+ backupCreated: boolean;
50
+ lastAppliedHash: string;
51
+ ownedArtifacts: OwnedArtifact[];
52
+ changedFiles: string[];
53
+ restartRequired: boolean;
54
+ warnings: string[];
55
+ }
56
+ interface VerifyCheck {
57
+ name: string;
58
+ passed: boolean;
59
+ message?: string;
60
+ }
61
+ interface VerifyResult {
62
+ ok: boolean;
63
+ checks: VerifyCheck[];
64
+ restartRequired: boolean;
65
+ warnings: string[];
66
+ }
67
+ interface RuntimeAdapter {
68
+ readonly runtime: RuntimeName;
69
+ probe(ctx: AdapterContext): Promise<ProbeResult>;
70
+ plan(ctx: AdapterContext, probe: ProbeResult): Promise<PatchPlan>;
71
+ apply(ctx: AdapterContext, plan: PatchPlan): Promise<ApplyResult>;
72
+ verify(ctx: AdapterContext, plan: PatchPlan): Promise<VerifyResult>;
73
+ bridgeMode(): BridgeMode;
74
+ /** Resolve the bridge script path. Only called for app-server mode. */
75
+ resolveBridgeScript?(ctx: AdapterContext): string | null;
76
+ }
77
+ type AgentRole = "reviewer" | "validator" | "long-running";
78
+ interface HeadlessConfig {
79
+ enabled: boolean;
80
+ role: AgentRole;
81
+ /** Max review rounds before forced termination. Default: 5 */
82
+ maxRounds: number;
83
+ /** Severity floor for quality-threshold strategy. Default: "high" */
84
+ qualitySeverityFloor: "critical" | "high" | "medium";
85
+ }
86
+ interface BridgeState {
87
+ pid: number;
88
+ statePath: string;
89
+ lastHeartbeat: string;
90
+ }
91
+ /** Runtime instance state. Supports multiple instances per runtime (e.g. codex-reviewer, codex-builder). */
92
+ interface InstanceState {
93
+ instanceId: InstanceId;
94
+ runtime: RuntimeName;
95
+ agentName: string | null;
96
+ port: number | null;
97
+ installed: boolean;
98
+ configPath: string;
99
+ bridgeMode: BridgeMode;
100
+ restartRequired: boolean;
101
+ ownedArtifacts: OwnedArtifact[];
102
+ backupPath: string;
103
+ lastAppliedHash: string;
104
+ lastVerifiedAt: string | null;
105
+ bridge: BridgeState | null;
106
+ /** Headless mode configuration. null = interactive (default). */
107
+ headless: HeadlessConfig | null;
108
+ warnings: string[];
109
+ }
110
+ /** @deprecated Use InstanceState. Kept for v1 state migration. */
111
+ interface RuntimeState {
112
+ installed: boolean;
113
+ configPath: string;
114
+ bridgeMode: BridgeMode;
115
+ restartRequired: boolean;
116
+ ownedArtifacts: OwnedArtifact[];
117
+ backupPath: string;
118
+ lastAppliedHash: string;
119
+ lastVerifiedAt: string | null;
120
+ bridge: BridgeState | null;
121
+ warnings: string[];
122
+ }
123
+ /** Schema v2: instances keyed by InstanceId */
124
+ interface TapState {
125
+ schemaVersion: number;
126
+ createdAt: string;
127
+ updatedAt: string;
128
+ commsDir: string;
129
+ repoRoot: string;
130
+ packageVersion: string;
131
+ instances: Record<InstanceId, InstanceState>;
132
+ }
133
+ /** Schema v1: runtimes keyed by RuntimeName. Used for migration only. */
134
+ interface TapStateV1 {
135
+ schemaVersion: 1;
136
+ createdAt: string;
137
+ updatedAt: string;
138
+ commsDir: string;
139
+ repoRoot: string;
140
+ packageVersion: string;
141
+ runtimes: Partial<Record<RuntimeName, RuntimeState>>;
142
+ }
143
+ type CommandName = "init" | "init-worktree" | "add" | "remove" | "status" | "serve" | "bridge" | "dashboard" | "unknown";
144
+ type CommandCode = "TAP_INIT_OK" | "TAP_ADD_OK" | "TAP_REMOVE_OK" | "TAP_STATUS_OK" | "TAP_SERVE_OK" | "TAP_NO_OP" | "TAP_ALREADY_INITIALIZED" | "TAP_NOT_INITIALIZED" | "TAP_RUNTIME_UNKNOWN" | "TAP_RUNTIME_NOT_FOUND" | "TAP_CONFIG_INVALID" | "TAP_LOCAL_SERVER_MISSING" | "TAP_INVALID_ARGUMENT" | "TAP_INSTANCE_NOT_FOUND" | "TAP_INSTANCE_AMBIGUOUS" | "TAP_PORT_CONFLICT" | "TAP_PATCH_FAILED" | "TAP_VERIFY_FAILED" | "TAP_ROLLBACK_FAILED" | "TAP_BRIDGE_START_OK" | "TAP_BRIDGE_START_FAILED" | "TAP_BRIDGE_STOP_OK" | "TAP_BRIDGE_STATUS_OK" | "TAP_BRIDGE_NOT_RUNNING" | "TAP_BRIDGE_SCRIPT_MISSING" | "TAP_SERVE_NO_SERVER" | "TAP_SERVE_BUN_REQUIRED" | "TAP_REVIEW_START_OK" | "TAP_REVIEW_TERMINATED" | "TAP_INTERNAL_ERROR";
145
+ interface CommandResult<T = Record<string, unknown>> {
146
+ ok: boolean;
147
+ command: CommandName;
148
+ runtime?: RuntimeName;
149
+ instanceId?: InstanceId;
150
+ code: CommandCode;
151
+ message: string;
152
+ warnings: string[];
153
+ data: T;
154
+ }
155
+
156
+ declare function stateExists(repoRoot: string): boolean;
157
+ declare function loadState(repoRoot: string): TapState | null;
158
+ declare function saveState(repoRoot: string, state: TapState): void;
159
+ declare function createInitialState(commsDir: string, repoRoot: string, packageVersion: string): TapState;
160
+
161
+ declare const version = "0.1.0";
162
+
163
+ /**
164
+ * Shared config (tap-config.json) — git tracked, repo-level defaults.
165
+ * All paths are repo-relative unless explicitly absolute.
166
+ */
167
+ interface TapSharedConfig {
168
+ /** Comms directory path. Repo-relative or absolute. */
169
+ commsDir?: string;
170
+ /** State directory path. Defaults to .tap-comms/ under repoRoot. */
171
+ stateDir?: string;
172
+ /** Runtime command: "bun" | "node". */
173
+ runtimeCommand?: string;
174
+ /** App server WebSocket URL for bridge connections. */
175
+ appServerUrl?: string;
176
+ }
177
+ /**
178
+ * Local config (tap-config.local.json) — gitignored, machine-specific overrides.
179
+ * Same shape as shared, overrides shared values.
180
+ */
181
+ type TapLocalConfig = TapSharedConfig;
182
+ /**
183
+ * Resolved config — all values populated, absolute paths.
184
+ */
185
+ interface TapResolvedConfig {
186
+ repoRoot: string;
187
+ commsDir: string;
188
+ stateDir: string;
189
+ runtimeCommand: string;
190
+ appServerUrl: string;
191
+ }
192
+ /** Config resolution source for diagnostics. */
193
+ type ConfigSource = "cli-flag" | "env" | "local-config" | "shared-config" | "auto";
194
+ interface ConfigResolution {
195
+ config: TapResolvedConfig;
196
+ sources: Record<keyof TapResolvedConfig, ConfigSource>;
197
+ }
198
+
199
+ declare const SHARED_CONFIG_FILE = "tap-config.json";
200
+ declare const LOCAL_CONFIG_FILE = "tap-config.local.json";
201
+ declare function loadSharedConfig(repoRoot: string): TapSharedConfig | null;
202
+ declare function loadLocalConfig(repoRoot: string): TapLocalConfig | null;
203
+ interface ConfigOverrides {
204
+ commsDir?: string;
205
+ stateDir?: string;
206
+ runtimeCommand?: string;
207
+ appServerUrl?: string;
208
+ }
209
+ /**
210
+ * Resolve config with priority: CLI flag > env > local config > shared config > auto.
211
+ */
212
+ declare function resolveConfig(overrides?: ConfigOverrides, startDir?: string): ConfigResolution;
213
+ declare function saveSharedConfig(repoRoot: string, config: TapSharedConfig): void;
214
+ declare function saveLocalConfig(repoRoot: string, config: TapLocalConfig): void;
215
+
216
+ declare function rotateLog(logPath: string): void;
217
+ /**
218
+ * Update the heartbeat timestamp for a running bridge.
219
+ * Bridge processes should call this periodically.
220
+ *
221
+ * Only the owning process (matching PID) can update the heartbeat.
222
+ * This prevents state dir collision when multiple writers exist.
223
+ * See: 묵 finding — bridge-heartbeat-state-dir-collision
224
+ */
225
+ declare function updateBridgeHeartbeat(stateDir: string, instanceId: InstanceId): void;
226
+ /**
227
+ * Get heartbeat age in seconds. Returns null if no state or no heartbeat.
228
+ */
229
+ declare function getHeartbeatAge(stateDir: string, instanceId: InstanceId): number | null;
230
+
231
+ /**
232
+ * Common Node.js runtime resolver for all tap-comms child processes.
233
+ *
234
+ * Resolution chain:
235
+ * .node-version + fnm probe → configured command → tsx fallback
236
+ *
237
+ * Extracted from codex-bridge-runner.ts (M69) to share across:
238
+ * - bridge engine spawn
239
+ * - bridge runner spawn
240
+ * - future CLI commands
241
+ */
242
+ type RuntimeSource = "fnm" | "config" | "path" | "tsx-fallback" | "bun";
243
+ interface ResolvedRuntime {
244
+ /** Absolute path or command name for the resolved runtime. */
245
+ command: string;
246
+ /** Whether --experimental-strip-types is supported and should be used. */
247
+ supportsStripTypes: boolean;
248
+ /** Where the runtime was resolved from (for diagnostics). */
249
+ source: RuntimeSource;
250
+ /** Detected major version, if available. */
251
+ majorVersion: number | null;
252
+ }
253
+ declare function readNodeVersion(repoRoot: string): string | null;
254
+ declare function probeFnmNode(desiredVersion: string): string | null;
255
+ /**
256
+ * Returns the directory containing the fnm-managed node binary,
257
+ * suitable for prepending to PATH in child processes.
258
+ */
259
+ declare function getFnmBinDir(repoRoot: string): string | null;
260
+ /**
261
+ * Resolve the Node.js runtime to use for spawning child processes.
262
+ *
263
+ * Priority: bun passthrough → .node-version + fnm → configured command → tsx fallback
264
+ */
265
+ declare function resolveNodeRuntime(configCommand: string, repoRoot: string): ResolvedRuntime;
266
+ /**
267
+ * Build an env object with fnm Node prepended to PATH.
268
+ * Use this when spawning child processes that need the correct Node.
269
+ */
270
+ declare function buildRuntimeEnv(repoRoot: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
271
+
272
+ export { type AdapterContext, type ApplyResult, type ArtifactKind, type BridgeMode, type BridgeState, type CommandCode, type CommandName, type CommandResult, type ConfigOverrides, type ConfigResolution, type ConfigSource, type InstanceId, type InstanceState, LOCAL_CONFIG_FILE, type OwnedArtifact, type PatchOp, type PatchOpType, type PatchPlan, type Platform, type ProbeResult, type ResolvedRuntime, type RuntimeAdapter, type RuntimeName, type RuntimeSource, type RuntimeState, SHARED_CONFIG_FILE, type TapLocalConfig, type TapResolvedConfig, type TapSharedConfig, type TapState, type TapStateV1, type VerifyCheck, type VerifyResult, buildRuntimeEnv, createInitialState, getFnmBinDir, getHeartbeatAge, loadLocalConfig, loadSharedConfig, loadState, probeFnmNode, readNodeVersion, resolveConfig, resolveNodeRuntime, rotateLog, saveLocalConfig, saveSharedConfig, saveState, stateExists, updateBridgeHeartbeat, version };
package/dist/index.mjs ADDED
@@ -0,0 +1,439 @@
1
+ // src/state.ts
2
+ import * as fs2 from "fs";
3
+ import * as path2 from "path";
4
+ import * as crypto from "crypto";
5
+
6
+ // src/config/resolve.ts
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ var SHARED_CONFIG_FILE = "tap-config.json";
10
+ var LOCAL_CONFIG_FILE = "tap-config.local.json";
11
+ var DEFAULT_RUNTIME_COMMAND = "node";
12
+ var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
13
+ function findRepoRoot(startDir = process.cwd()) {
14
+ let dir = path.resolve(startDir);
15
+ while (true) {
16
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
17
+ if (fs.existsSync(path.join(dir, "package.json"))) return dir;
18
+ const parent = path.dirname(dir);
19
+ if (parent === dir) break;
20
+ dir = parent;
21
+ }
22
+ return process.cwd();
23
+ }
24
+ function loadJsonFile(filePath) {
25
+ if (!fs.existsSync(filePath)) return null;
26
+ try {
27
+ const raw = fs.readFileSync(filePath, "utf-8");
28
+ return JSON.parse(raw);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function loadSharedConfig(repoRoot) {
34
+ return loadJsonFile(path.join(repoRoot, SHARED_CONFIG_FILE));
35
+ }
36
+ function loadLocalConfig(repoRoot) {
37
+ return loadJsonFile(path.join(repoRoot, LOCAL_CONFIG_FILE));
38
+ }
39
+ function resolveConfig(overrides = {}, startDir) {
40
+ const repoRoot = findRepoRoot(startDir);
41
+ const shared = loadSharedConfig(repoRoot) ?? {};
42
+ const local = loadLocalConfig(repoRoot) ?? {};
43
+ const sources = {
44
+ repoRoot: "auto",
45
+ commsDir: "auto",
46
+ stateDir: "auto",
47
+ runtimeCommand: "auto",
48
+ appServerUrl: "auto"
49
+ };
50
+ let commsDir;
51
+ if (overrides.commsDir) {
52
+ commsDir = path.resolve(overrides.commsDir);
53
+ sources.commsDir = "cli-flag";
54
+ } else if (process.env.TAP_COMMS_DIR) {
55
+ commsDir = path.resolve(process.env.TAP_COMMS_DIR);
56
+ sources.commsDir = "env";
57
+ } else if (local.commsDir) {
58
+ commsDir = resolvePath(repoRoot, local.commsDir);
59
+ sources.commsDir = "local-config";
60
+ } else if (shared.commsDir) {
61
+ commsDir = resolvePath(repoRoot, shared.commsDir);
62
+ sources.commsDir = "shared-config";
63
+ } else {
64
+ commsDir = path.join(path.dirname(repoRoot), "tap-comms");
65
+ }
66
+ let stateDir;
67
+ if (overrides.stateDir) {
68
+ stateDir = path.resolve(overrides.stateDir);
69
+ sources.stateDir = "cli-flag";
70
+ } else if (process.env.TAP_STATE_DIR) {
71
+ stateDir = path.resolve(process.env.TAP_STATE_DIR);
72
+ sources.stateDir = "env";
73
+ } else if (local.stateDir) {
74
+ stateDir = resolvePath(repoRoot, local.stateDir);
75
+ sources.stateDir = "local-config";
76
+ } else if (shared.stateDir) {
77
+ stateDir = resolvePath(repoRoot, shared.stateDir);
78
+ sources.stateDir = "shared-config";
79
+ } else {
80
+ stateDir = path.join(repoRoot, ".tap-comms");
81
+ }
82
+ let runtimeCommand;
83
+ if (overrides.runtimeCommand) {
84
+ runtimeCommand = overrides.runtimeCommand;
85
+ sources.runtimeCommand = "cli-flag";
86
+ } else if (process.env.TAP_RUNTIME_COMMAND) {
87
+ runtimeCommand = process.env.TAP_RUNTIME_COMMAND;
88
+ sources.runtimeCommand = "env";
89
+ } else if (local.runtimeCommand) {
90
+ runtimeCommand = local.runtimeCommand;
91
+ sources.runtimeCommand = "local-config";
92
+ } else if (shared.runtimeCommand) {
93
+ runtimeCommand = shared.runtimeCommand;
94
+ sources.runtimeCommand = "shared-config";
95
+ } else {
96
+ runtimeCommand = DEFAULT_RUNTIME_COMMAND;
97
+ }
98
+ let appServerUrl;
99
+ if (overrides.appServerUrl) {
100
+ appServerUrl = overrides.appServerUrl;
101
+ sources.appServerUrl = "cli-flag";
102
+ } else if (process.env.TAP_APP_SERVER_URL) {
103
+ appServerUrl = process.env.TAP_APP_SERVER_URL;
104
+ sources.appServerUrl = "env";
105
+ } else if (local.appServerUrl) {
106
+ appServerUrl = local.appServerUrl;
107
+ sources.appServerUrl = "local-config";
108
+ } else if (shared.appServerUrl) {
109
+ appServerUrl = shared.appServerUrl;
110
+ sources.appServerUrl = "shared-config";
111
+ } else {
112
+ appServerUrl = DEFAULT_APP_SERVER_URL;
113
+ }
114
+ return {
115
+ config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },
116
+ sources
117
+ };
118
+ }
119
+ function saveSharedConfig(repoRoot, config) {
120
+ const filePath = path.join(repoRoot, SHARED_CONFIG_FILE);
121
+ const tmp = `${filePath}.tmp.${process.pid}`;
122
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
123
+ fs.renameSync(tmp, filePath);
124
+ }
125
+ function saveLocalConfig(repoRoot, config) {
126
+ const filePath = path.join(repoRoot, LOCAL_CONFIG_FILE);
127
+ const tmp = `${filePath}.tmp.${process.pid}`;
128
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
129
+ fs.renameSync(tmp, filePath);
130
+ }
131
+ function resolvePath(repoRoot, p) {
132
+ return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);
133
+ }
134
+
135
+ // src/state.ts
136
+ var STATE_FILE = "state.json";
137
+ var SCHEMA_VERSION = 2;
138
+ function getStateDir(repoRoot) {
139
+ const { config } = resolveConfig({}, repoRoot);
140
+ return config.stateDir;
141
+ }
142
+ function getStatePath(repoRoot) {
143
+ return path2.join(getStateDir(repoRoot), STATE_FILE);
144
+ }
145
+ function stateExists(repoRoot) {
146
+ return fs2.existsSync(getStatePath(repoRoot));
147
+ }
148
+ function migrateStateV1toV2(v1) {
149
+ const instances = {};
150
+ for (const [runtime, rs] of Object.entries(v1.runtimes)) {
151
+ if (!rs) continue;
152
+ const instanceId = runtime;
153
+ instances[instanceId] = {
154
+ instanceId,
155
+ runtime,
156
+ agentName: null,
157
+ port: null,
158
+ headless: null,
159
+ ...rs
160
+ };
161
+ }
162
+ return {
163
+ schemaVersion: SCHEMA_VERSION,
164
+ createdAt: v1.createdAt,
165
+ updatedAt: v1.updatedAt,
166
+ commsDir: v1.commsDir,
167
+ repoRoot: v1.repoRoot,
168
+ packageVersion: v1.packageVersion,
169
+ instances
170
+ };
171
+ }
172
+ function loadState(repoRoot) {
173
+ const statePath = getStatePath(repoRoot);
174
+ if (!fs2.existsSync(statePath)) return null;
175
+ const raw = fs2.readFileSync(statePath, "utf-8");
176
+ const parsed = JSON.parse(raw);
177
+ if (parsed.schemaVersion === 1 || parsed.runtimes) {
178
+ const migrated = migrateStateV1toV2(parsed);
179
+ saveState(repoRoot, migrated);
180
+ return migrated;
181
+ }
182
+ return parsed;
183
+ }
184
+ function saveState(repoRoot, state) {
185
+ const stateDir = getStateDir(repoRoot);
186
+ fs2.mkdirSync(stateDir, { recursive: true });
187
+ const statePath = getStatePath(repoRoot);
188
+ const tmp = `${statePath}.tmp.${process.pid}`;
189
+ fs2.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
190
+ fs2.renameSync(tmp, statePath);
191
+ }
192
+ function createInitialState(commsDir, repoRoot, packageVersion) {
193
+ const now = (/* @__PURE__ */ new Date()).toISOString();
194
+ return {
195
+ schemaVersion: SCHEMA_VERSION,
196
+ createdAt: now,
197
+ updatedAt: now,
198
+ commsDir: path2.resolve(commsDir),
199
+ repoRoot: path2.resolve(repoRoot),
200
+ packageVersion,
201
+ instances: {}
202
+ };
203
+ }
204
+
205
+ // src/version.ts
206
+ var version = "0.1.0";
207
+
208
+ // src/engine/bridge.ts
209
+ import * as fs4 from "fs";
210
+ import * as path4 from "path";
211
+ import { spawn, execSync as execSync2 } from "child_process";
212
+
213
+ // src/runtime/resolve-node.ts
214
+ import * as fs3 from "fs";
215
+ import * as path3 from "path";
216
+ import { execSync } from "child_process";
217
+ function readNodeVersion(repoRoot) {
218
+ const nvFile = path3.join(repoRoot, ".node-version");
219
+ if (!fs3.existsSync(nvFile)) return null;
220
+ try {
221
+ const raw = fs3.readFileSync(nvFile, "utf-8").trim();
222
+ return raw.length > 0 ? raw.replace(/^v/, "") : null;
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+ function fnmCandidateDirs() {
228
+ if (process.platform === "win32") {
229
+ return [
230
+ process.env.FNM_DIR,
231
+ process.env.APPDATA ? path3.join(process.env.APPDATA, "fnm") : null,
232
+ process.env.LOCALAPPDATA ? path3.join(process.env.LOCALAPPDATA, "fnm") : null,
233
+ process.env.USERPROFILE ? path3.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
234
+ ].filter(Boolean);
235
+ }
236
+ return [
237
+ process.env.FNM_DIR,
238
+ process.env.HOME ? path3.join(process.env.HOME, ".local", "share", "fnm") : null,
239
+ process.env.HOME ? path3.join(process.env.HOME, ".fnm") : null,
240
+ process.env.XDG_DATA_HOME ? path3.join(process.env.XDG_DATA_HOME, "fnm") : null
241
+ ].filter(Boolean);
242
+ }
243
+ function nodeExecutableName() {
244
+ return process.platform === "win32" ? "node.exe" : "node";
245
+ }
246
+ function probeFnmNode(desiredVersion) {
247
+ const dirs = fnmCandidateDirs();
248
+ const exe = nodeExecutableName();
249
+ for (const baseDir of dirs) {
250
+ const candidate = path3.join(
251
+ baseDir,
252
+ "node-versions",
253
+ `v${desiredVersion}`,
254
+ "installation",
255
+ exe
256
+ );
257
+ if (!fs3.existsSync(candidate)) continue;
258
+ try {
259
+ const v = execSync(`"${candidate}" --version`, {
260
+ encoding: "utf-8",
261
+ timeout: 5e3
262
+ }).trim();
263
+ if (v.startsWith(`v${desiredVersion.split(".")[0]}.`)) {
264
+ return candidate;
265
+ }
266
+ } catch {
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+ function detectNodeMajorVersion(command) {
272
+ try {
273
+ const version2 = execSync(`"${command}" --version`, {
274
+ encoding: "utf-8",
275
+ timeout: 5e3
276
+ }).trim();
277
+ const match = version2.match(/^v?(\d+)\./);
278
+ return match ? parseInt(match[1], 10) : null;
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+ function checkStripTypesSupport(command) {
284
+ const major = detectNodeMajorVersion(command);
285
+ if (major !== null && major >= 22) return true;
286
+ try {
287
+ execSync(`"${command}" --experimental-strip-types -e ""`, {
288
+ timeout: 5e3,
289
+ stdio: "pipe"
290
+ });
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+ function findTsxFallback(repoRoot) {
297
+ const candidates = [
298
+ path3.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
299
+ path3.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
300
+ path3.join(repoRoot, "node_modules", ".bin", "tsx")
301
+ ];
302
+ for (const c of candidates) {
303
+ if (fs3.existsSync(c)) return c;
304
+ }
305
+ return null;
306
+ }
307
+ function getFnmBinDir(repoRoot) {
308
+ const desiredVersion = readNodeVersion(repoRoot);
309
+ if (!desiredVersion) return null;
310
+ const nodePath = probeFnmNode(desiredVersion);
311
+ if (!nodePath) return null;
312
+ return path3.dirname(nodePath);
313
+ }
314
+ function resolveNodeRuntime(configCommand, repoRoot) {
315
+ if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
316
+ return {
317
+ command: configCommand,
318
+ supportsStripTypes: false,
319
+ source: "bun",
320
+ majorVersion: null
321
+ };
322
+ }
323
+ const desiredVersion = readNodeVersion(repoRoot);
324
+ if (desiredVersion) {
325
+ const fnmNode = probeFnmNode(desiredVersion);
326
+ if (fnmNode) {
327
+ const major2 = detectNodeMajorVersion(fnmNode);
328
+ return {
329
+ command: fnmNode,
330
+ supportsStripTypes: checkStripTypesSupport(fnmNode),
331
+ source: "fnm",
332
+ majorVersion: major2
333
+ };
334
+ }
335
+ }
336
+ const major = detectNodeMajorVersion(configCommand);
337
+ if (major !== null) {
338
+ return {
339
+ command: configCommand,
340
+ supportsStripTypes: checkStripTypesSupport(configCommand),
341
+ source: major === detectNodeMajorVersion("node") ? "path" : "config",
342
+ majorVersion: major
343
+ };
344
+ }
345
+ const tsx = findTsxFallback(repoRoot);
346
+ if (tsx) {
347
+ return {
348
+ command: tsx,
349
+ supportsStripTypes: false,
350
+ source: "tsx-fallback",
351
+ majorVersion: null
352
+ };
353
+ }
354
+ return {
355
+ command: configCommand,
356
+ supportsStripTypes: false,
357
+ source: "path",
358
+ majorVersion: null
359
+ };
360
+ }
361
+ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
362
+ const fnmBin = getFnmBinDir(repoRoot);
363
+ if (!fnmBin) return { ...baseEnv };
364
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
365
+ const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
366
+ return {
367
+ ...baseEnv,
368
+ [pathKey]: `${fnmBin}${path3.delimiter}${currentPath}`
369
+ };
370
+ }
371
+
372
+ // src/engine/bridge.ts
373
+ function pidFilePath(stateDir, instanceId) {
374
+ return path4.join(stateDir, "pids", `bridge-${instanceId}.json`);
375
+ }
376
+ function loadBridgeState(stateDir, instanceId) {
377
+ const pidPath = pidFilePath(stateDir, instanceId);
378
+ if (!fs4.existsSync(pidPath)) return null;
379
+ try {
380
+ const raw = fs4.readFileSync(pidPath, "utf-8");
381
+ return JSON.parse(raw);
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+ function saveBridgeState(stateDir, instanceId, state) {
387
+ const pidPath = pidFilePath(stateDir, instanceId);
388
+ fs4.mkdirSync(path4.dirname(pidPath), { recursive: true });
389
+ const tmp = `${pidPath}.tmp.${process.pid}`;
390
+ fs4.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
391
+ fs4.renameSync(tmp, pidPath);
392
+ }
393
+ function rotateLog(logPath) {
394
+ if (!fs4.existsSync(logPath)) return;
395
+ try {
396
+ const stats = fs4.statSync(logPath);
397
+ if (stats.size === 0) return;
398
+ const prevPath = `${logPath}.prev`;
399
+ fs4.renameSync(logPath, prevPath);
400
+ } catch {
401
+ }
402
+ }
403
+ function updateBridgeHeartbeat(stateDir, instanceId) {
404
+ const state = loadBridgeState(stateDir, instanceId);
405
+ if (!state) return;
406
+ if (state.pid !== process.pid) return;
407
+ state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
408
+ saveBridgeState(stateDir, instanceId, state);
409
+ }
410
+ function getHeartbeatAge(stateDir, instanceId) {
411
+ const state = loadBridgeState(stateDir, instanceId);
412
+ if (!state?.lastHeartbeat) return null;
413
+ const heartbeatTime = new Date(state.lastHeartbeat).getTime();
414
+ if (isNaN(heartbeatTime)) return null;
415
+ return Math.floor((Date.now() - heartbeatTime) / 1e3);
416
+ }
417
+ export {
418
+ LOCAL_CONFIG_FILE,
419
+ SHARED_CONFIG_FILE,
420
+ buildRuntimeEnv,
421
+ createInitialState,
422
+ getFnmBinDir,
423
+ getHeartbeatAge,
424
+ loadLocalConfig,
425
+ loadSharedConfig,
426
+ loadState,
427
+ probeFnmNode,
428
+ readNodeVersion,
429
+ resolveConfig,
430
+ resolveNodeRuntime,
431
+ rotateLog,
432
+ saveLocalConfig,
433
+ saveSharedConfig,
434
+ saveState,
435
+ stateExists,
436
+ updateBridgeHeartbeat,
437
+ version
438
+ };
439
+ //# sourceMappingURL=index.mjs.map