@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 @@
1
+ {"version":3,"sources":["../src/state.ts","../src/config/resolve.ts","../src/version.ts","../src/engine/bridge.ts","../src/runtime/resolve-node.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n TapState,\n TapStateV1,\n InstanceState,\n InstanceId,\n RuntimeName,\n OwnedArtifact,\n} from \"./types.js\";\nimport { resolveConfig } from \"./config/index.js\";\n\nconst STATE_FILE = \"state.json\";\nconst SCHEMA_VERSION = 2;\n\nexport function getStateDir(repoRoot: string): string {\n const { config } = resolveConfig({}, repoRoot);\n return config.stateDir;\n}\n\nexport function getStatePath(repoRoot: string): string {\n return path.join(getStateDir(repoRoot), STATE_FILE);\n}\n\nexport function stateExists(repoRoot: string): boolean {\n return fs.existsSync(getStatePath(repoRoot));\n}\n\n// ─── v1 → v2 Migration ────────────────────────────────────────\n\nexport function migrateStateV1toV2(v1: TapStateV1): TapState {\n const instances: Record<InstanceId, InstanceState> = {};\n\n for (const [runtime, rs] of Object.entries(v1.runtimes)) {\n if (!rs) continue;\n const instanceId = runtime as InstanceId; // default instance = runtime name\n instances[instanceId] = {\n instanceId,\n runtime: runtime as RuntimeName,\n agentName: null,\n port: null,\n headless: null,\n ...rs,\n };\n }\n\n return {\n schemaVersion: SCHEMA_VERSION,\n createdAt: v1.createdAt,\n updatedAt: v1.updatedAt,\n commsDir: v1.commsDir,\n repoRoot: v1.repoRoot,\n packageVersion: v1.packageVersion,\n instances,\n };\n}\n\n// ─── Load / Save ───────────────────────────────────────────────\n\nexport function loadState(repoRoot: string): TapState | null {\n const statePath = getStatePath(repoRoot);\n if (!fs.existsSync(statePath)) return null;\n\n const raw = fs.readFileSync(statePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n\n // Auto-migrate v1 → v2\n if (parsed.schemaVersion === 1 || parsed.runtimes) {\n const migrated = migrateStateV1toV2(parsed as TapStateV1);\n saveState(repoRoot, migrated);\n return migrated;\n }\n\n return parsed as TapState;\n}\n\nexport function saveState(repoRoot: string, state: TapState): void {\n const stateDir = getStateDir(repoRoot);\n fs.mkdirSync(stateDir, { recursive: true });\n const statePath = getStatePath(repoRoot);\n const tmp = `${statePath}.tmp.${process.pid}`;\n fs.writeFileSync(tmp, JSON.stringify(state, null, 2), \"utf-8\");\n fs.renameSync(tmp, statePath);\n}\n\nexport function createInitialState(\n commsDir: string,\n repoRoot: string,\n packageVersion: string,\n): TapState {\n const now = new Date().toISOString();\n return {\n schemaVersion: SCHEMA_VERSION,\n createdAt: now,\n updatedAt: now,\n commsDir: path.resolve(commsDir),\n repoRoot: path.resolve(repoRoot),\n packageVersion,\n instances: {},\n };\n}\n\n// ─── Instance CRUD ─────────────────────────────────────────────\n\nexport function updateInstanceState(\n state: TapState,\n instanceId: InstanceId,\n instanceState: InstanceState,\n): TapState {\n return {\n ...state,\n updatedAt: new Date().toISOString(),\n instances: {\n ...state.instances,\n [instanceId]: instanceState,\n },\n };\n}\n\nexport function removeInstanceState(\n state: TapState,\n instanceId: InstanceId,\n): TapState {\n const { [instanceId]: _removed, ...remaining } = state.instances;\n return {\n ...state,\n updatedAt: new Date().toISOString(),\n instances: remaining,\n };\n}\n\nexport function getInstalledInstances(state: TapState): InstanceId[] {\n return (Object.keys(state.instances) as InstanceId[]).filter(\n (id) => state.instances[id]?.installed,\n );\n}\n\nexport function getInstanceArtifacts(\n state: TapState,\n instanceId: InstanceId,\n): OwnedArtifact[] {\n return state.instances[instanceId]?.ownedArtifacts ?? [];\n}\n\n// ─── Backup ────────────────────────────────────────────────────\n\nexport function ensureBackupDir(\n stateDir: string,\n instanceId: InstanceId,\n): string {\n const backupDir = path.join(stateDir, \"backups\", instanceId);\n fs.mkdirSync(backupDir, { recursive: true });\n return backupDir;\n}\n\nexport function backupFile(filePath: string, backupDir: string): string {\n const basename = path.basename(filePath);\n const hash = fileHash(filePath);\n const backupPath = path.join(backupDir, `${basename}.${hash}.bak`);\n fs.copyFileSync(filePath, backupPath);\n return backupPath;\n}\n\nexport function fileHash(filePath: string): string {\n if (!fs.existsSync(filePath)) return \"\";\n const content = fs.readFileSync(filePath);\n return crypto.createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 16);\n}\n\n// ─── Deprecated (v1 compat wrappers) ───────────────────────────\n\n/** @deprecated Use updateInstanceState */\nexport function updateRuntimeState(\n state: TapState,\n runtime: RuntimeName,\n runtimeState: InstanceState,\n): TapState {\n return updateInstanceState(state, runtime, runtimeState);\n}\n\n/** @deprecated Use removeInstanceState */\nexport function removeRuntimeState(\n state: TapState,\n runtime: RuntimeName,\n): TapState {\n return removeInstanceState(state, runtime);\n}\n\n/** @deprecated Use getInstalledInstances */\nexport function getInstalledRuntimes(state: TapState): InstanceId[] {\n return getInstalledInstances(state);\n}\n\n/** @deprecated Use getInstanceArtifacts */\nexport function getRuntimeArtifacts(\n state: TapState,\n instanceId: InstanceId,\n): OwnedArtifact[] {\n return getInstanceArtifacts(state, instanceId);\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type {\n TapSharedConfig,\n TapLocalConfig,\n TapResolvedConfig,\n ConfigSource,\n ConfigResolution,\n} from \"./types.js\";\n\n// ─── File names ────────────────────────────────────────────────\n\nexport const SHARED_CONFIG_FILE = \"tap-config.json\";\nexport const LOCAL_CONFIG_FILE = \"tap-config.local.json\";\n\n// ─── Defaults ──────────────────────────────────────────────────\n\nconst DEFAULT_RUNTIME_COMMAND = \"node\";\nconst DEFAULT_APP_SERVER_URL = \"ws://127.0.0.1:4501\";\n\n// ─── Repo root discovery ───────────────────────────────────────\n\nexport function findRepoRoot(startDir: string = process.cwd()): string {\n let dir = path.resolve(startDir);\n while (true) {\n if (fs.existsSync(path.join(dir, \".git\"))) return dir;\n if (fs.existsSync(path.join(dir, \"package.json\"))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n}\n\n// ─── JSON file loading ─────────────────────────────────────────\n\nfunction loadJsonFile<T>(filePath: string): T | null {\n if (!fs.existsSync(filePath)) return null;\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\nexport function loadSharedConfig(repoRoot: string): TapSharedConfig | null {\n return loadJsonFile<TapSharedConfig>(path.join(repoRoot, SHARED_CONFIG_FILE));\n}\n\nexport function loadLocalConfig(repoRoot: string): TapLocalConfig | null {\n return loadJsonFile<TapLocalConfig>(path.join(repoRoot, LOCAL_CONFIG_FILE));\n}\n\n// ─── CLI overrides ─────────────────────────────────────────────\n\nexport interface ConfigOverrides {\n commsDir?: string;\n stateDir?: string;\n runtimeCommand?: string;\n appServerUrl?: string;\n}\n\n// ─── Resolution ────────────────────────────────────────────────\n\n/**\n * Resolve config with priority: CLI flag > env > local config > shared config > auto.\n */\nexport function resolveConfig(\n overrides: ConfigOverrides = {},\n startDir?: string,\n): ConfigResolution {\n const repoRoot = findRepoRoot(startDir);\n const shared = loadSharedConfig(repoRoot) ?? {};\n const local = loadLocalConfig(repoRoot) ?? {};\n\n const sources: Record<keyof TapResolvedConfig, ConfigSource> = {\n repoRoot: \"auto\",\n commsDir: \"auto\",\n stateDir: \"auto\",\n runtimeCommand: \"auto\",\n appServerUrl: \"auto\",\n };\n\n // ─── commsDir ──────────────────────────────────────────────\n let commsDir: string;\n if (overrides.commsDir) {\n commsDir = path.resolve(overrides.commsDir);\n sources.commsDir = \"cli-flag\";\n } else if (process.env.TAP_COMMS_DIR) {\n commsDir = path.resolve(process.env.TAP_COMMS_DIR);\n sources.commsDir = \"env\";\n } else if (local.commsDir) {\n commsDir = resolvePath(repoRoot, local.commsDir);\n sources.commsDir = \"local-config\";\n } else if (shared.commsDir) {\n commsDir = resolvePath(repoRoot, shared.commsDir);\n sources.commsDir = \"shared-config\";\n } else {\n commsDir = path.join(path.dirname(repoRoot), \"tap-comms\");\n }\n\n // ─── stateDir ──────────────────────────────────────────────\n let stateDir: string;\n if (overrides.stateDir) {\n stateDir = path.resolve(overrides.stateDir);\n sources.stateDir = \"cli-flag\";\n } else if (process.env.TAP_STATE_DIR) {\n stateDir = path.resolve(process.env.TAP_STATE_DIR);\n sources.stateDir = \"env\";\n } else if (local.stateDir) {\n stateDir = resolvePath(repoRoot, local.stateDir);\n sources.stateDir = \"local-config\";\n } else if (shared.stateDir) {\n stateDir = resolvePath(repoRoot, shared.stateDir);\n sources.stateDir = \"shared-config\";\n } else {\n stateDir = path.join(repoRoot, \".tap-comms\");\n }\n\n // ─── runtimeCommand ────────────────────────────────────────\n let runtimeCommand: string;\n if (overrides.runtimeCommand) {\n runtimeCommand = overrides.runtimeCommand;\n sources.runtimeCommand = \"cli-flag\";\n } else if (process.env.TAP_RUNTIME_COMMAND) {\n runtimeCommand = process.env.TAP_RUNTIME_COMMAND;\n sources.runtimeCommand = \"env\";\n } else if (local.runtimeCommand) {\n runtimeCommand = local.runtimeCommand;\n sources.runtimeCommand = \"local-config\";\n } else if (shared.runtimeCommand) {\n runtimeCommand = shared.runtimeCommand;\n sources.runtimeCommand = \"shared-config\";\n } else {\n runtimeCommand = DEFAULT_RUNTIME_COMMAND;\n }\n\n // ─── appServerUrl ──────────────────────────────────────────\n let appServerUrl: string;\n if (overrides.appServerUrl) {\n appServerUrl = overrides.appServerUrl;\n sources.appServerUrl = \"cli-flag\";\n } else if (process.env.TAP_APP_SERVER_URL) {\n appServerUrl = process.env.TAP_APP_SERVER_URL;\n sources.appServerUrl = \"env\";\n } else if (local.appServerUrl) {\n appServerUrl = local.appServerUrl;\n sources.appServerUrl = \"local-config\";\n } else if (shared.appServerUrl) {\n appServerUrl = shared.appServerUrl;\n sources.appServerUrl = \"shared-config\";\n } else {\n appServerUrl = DEFAULT_APP_SERVER_URL;\n }\n\n return {\n config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },\n sources,\n };\n}\n\n// ─── Save helpers ──────────────────────────────────────────────\n\nexport function saveSharedConfig(\n repoRoot: string,\n config: TapSharedConfig,\n): void {\n const filePath = path.join(repoRoot, SHARED_CONFIG_FILE);\n const tmp = `${filePath}.tmp.${process.pid}`;\n fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n fs.renameSync(tmp, filePath);\n}\n\nexport function saveLocalConfig(\n repoRoot: string,\n config: TapLocalConfig,\n): void {\n const filePath = path.join(repoRoot, LOCAL_CONFIG_FILE);\n const tmp = `${filePath}.tmp.${process.pid}`;\n fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n fs.renameSync(tmp, filePath);\n}\n\n// ─── Helpers ───────────────────────────────────────────────────\n\n/** Resolve a path relative to repoRoot, or keep absolute as-is. */\nfunction resolvePath(repoRoot: string, p: string): string {\n return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);\n}\n","export const version = \"0.1.0\";\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { spawn, execSync } from \"node:child_process\";\nimport type {\n RuntimeName,\n InstanceId,\n BridgeState,\n HeadlessConfig,\n Platform,\n} from \"../types.js\";\nimport { resolveNodeRuntime, buildRuntimeEnv } from \"../runtime/index.js\";\n\nexport interface BridgeStartOptions {\n instanceId: InstanceId;\n runtime: RuntimeName;\n stateDir: string;\n commsDir: string;\n bridgeScript: string;\n platform: Platform;\n agentName?: string;\n runtimeCommand?: string;\n appServerUrl?: string;\n repoRoot?: string;\n port?: number;\n /** Headless configuration. Passed as env vars to the bridge process. */\n headless?: HeadlessConfig | null;\n /** Bridge script operational flags (forwarded to codex-app-server-bridge.ts) */\n busyMode?: \"steer\" | \"wait\";\n pollSeconds?: number;\n reconnectSeconds?: number;\n messageLookbackMinutes?: number;\n threadId?: string;\n ephemeral?: boolean;\n processExistingMessages?: boolean;\n}\n\nexport interface BridgeStopOptions {\n instanceId: InstanceId;\n stateDir: string;\n platform: Platform;\n}\n\nfunction pidFilePath(stateDir: string, instanceId: InstanceId): string {\n return path.join(stateDir, \"pids\", `bridge-${instanceId}.json`);\n}\n\nfunction logFilePath(stateDir: string, instanceId: InstanceId): string {\n return path.join(stateDir, \"logs\", `bridge-${instanceId}.log`);\n}\n\nexport function loadBridgeState(\n stateDir: string,\n instanceId: InstanceId,\n): BridgeState | null {\n const pidPath = pidFilePath(stateDir, instanceId);\n if (!fs.existsSync(pidPath)) return null;\n\n try {\n const raw = fs.readFileSync(pidPath, \"utf-8\");\n return JSON.parse(raw) as BridgeState;\n } catch {\n return null;\n }\n}\n\nexport function saveBridgeState(\n stateDir: string,\n instanceId: InstanceId,\n state: BridgeState,\n): void {\n const pidPath = pidFilePath(stateDir, instanceId);\n fs.mkdirSync(path.dirname(pidPath), { recursive: true });\n const tmp = `${pidPath}.tmp.${process.pid}`;\n fs.writeFileSync(tmp, JSON.stringify(state, null, 2), \"utf-8\");\n fs.renameSync(tmp, pidPath);\n}\n\nexport function clearBridgeState(\n stateDir: string,\n instanceId: InstanceId,\n): void {\n const pidPath = pidFilePath(stateDir, instanceId);\n if (fs.existsSync(pidPath)) {\n fs.unlinkSync(pidPath);\n }\n}\n\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function isBridgeRunning(\n stateDir: string,\n instanceId: InstanceId,\n): boolean {\n const state = loadBridgeState(stateDir, instanceId);\n if (!state) return false;\n return isProcessAlive(state.pid);\n}\n\nexport async function startBridge(\n options: BridgeStartOptions,\n): Promise<BridgeState> {\n const {\n instanceId,\n runtime,\n stateDir,\n commsDir,\n bridgeScript,\n agentName,\n port,\n } = options;\n\n // Resolve agent name: explicit > env > error\n const resolvedAgent =\n agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;\n\n if (!resolvedAgent) {\n throw new Error(\n `No agent name for ${instanceId} bridge. ` +\n `Set TAP_AGENT_NAME env var or pass --agent-name flag.`,\n );\n }\n\n // Check if already running\n if (isBridgeRunning(stateDir, instanceId)) {\n const existing = loadBridgeState(stateDir, instanceId)!;\n throw new Error(\n `Bridge for ${instanceId} is already running (PID: ${existing.pid})`,\n );\n }\n\n // Clear stale PID\n clearBridgeState(stateDir, instanceId);\n\n const logPath = logFilePath(stateDir, instanceId);\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n\n // Log rotation: rename existing log to .prev\n rotateLog(logPath);\n\n const logFd = fs.openSync(logPath, \"a\");\n\n // Use explicit repoRoot (not derived from stateDir — stateDir may be external)\n const repoRoot = options.repoRoot ?? path.resolve(stateDir, \"..\");\n const resolved = resolveNodeRuntime(\n options.runtimeCommand ?? \"node\",\n repoRoot,\n );\n const command = resolved.command;\n\n // Build env with fnm Node prepended to PATH so the bridge runner's\n // 2nd-stage spawn also finds the correct Node (결 finding: 2-stage spawn)\n const runtimeEnv = buildRuntimeEnv(repoRoot);\n\n // Spawn detached process — pass both command and strip-types metadata\n // so the runner doesn't re-guess (avoids bun + --experimental-strip-types)\n const child = spawn(command, [bridgeScript], {\n detached: true,\n stdio: [\"ignore\", logFd, logFd],\n env: {\n ...runtimeEnv,\n TAP_COMMS_DIR: commsDir,\n TAP_BRIDGE_RUNTIME: runtime,\n TAP_BRIDGE_INSTANCE_ID: instanceId,\n TAP_AGENT_NAME: resolvedAgent,\n CODEX_TAP_AGENT_NAME: resolvedAgent,\n TAP_RESOLVED_NODE: resolved.command,\n TAP_STRIP_TYPES: resolved.supportsStripTypes ? \"1\" : \"0\",\n ...(options.appServerUrl\n ? { CODEX_APP_SERVER_URL: options.appServerUrl }\n : {}),\n ...(port != null ? { TAP_BRIDGE_PORT: String(port) } : {}),\n ...(options.headless?.enabled\n ? {\n TAP_HEADLESS: \"true\",\n TAP_AGENT_ROLE: options.headless.role,\n TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),\n TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor,\n }\n : {}),\n // Bridge script operational flags\n ...(options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {}),\n ...(options.pollSeconds != null\n ? { TAP_POLL_SECONDS: String(options.pollSeconds) }\n : {}),\n ...(options.reconnectSeconds != null\n ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) }\n : {}),\n ...(options.messageLookbackMinutes != null\n ? {\n TAP_MESSAGE_LOOKBACK_MINUTES: String(\n options.messageLookbackMinutes,\n ),\n }\n : {}),\n ...(options.threadId ? { TAP_THREAD_ID: options.threadId } : {}),\n ...(options.ephemeral ? { TAP_EPHEMERAL: \"true\" } : {}),\n ...(options.processExistingMessages\n ? { TAP_PROCESS_EXISTING: \"true\" }\n : {}),\n },\n });\n\n child.unref();\n fs.closeSync(logFd);\n\n if (!child.pid) {\n throw new Error(`Failed to spawn bridge process for ${instanceId}`);\n }\n\n const state: BridgeState = {\n pid: child.pid,\n statePath: pidFilePath(stateDir, instanceId),\n lastHeartbeat: new Date().toISOString(),\n };\n\n saveBridgeState(stateDir, instanceId, state);\n\n // NOTE: Heartbeat updates are the bridge process's responsibility.\n // The bridge script should periodically write to the PID file's lastHeartbeat field.\n // CLI only records the initial heartbeat at spawn time.\n\n return state;\n}\n\nexport async function stopBridge(options: BridgeStopOptions): Promise<boolean> {\n const { instanceId, stateDir, platform } = options;\n const state = loadBridgeState(stateDir, instanceId);\n\n if (!state) {\n return false; // No PID file\n }\n\n if (!isProcessAlive(state.pid)) {\n clearBridgeState(stateDir, instanceId);\n return false; // Already dead\n }\n\n try {\n if (platform === \"win32\") {\n // Windows: use taskkill\n execSync(`taskkill /PID ${state.pid} /F /T`, { stdio: \"pipe\" });\n } else {\n // Unix: SIGTERM\n process.kill(state.pid, \"SIGTERM\");\n\n // Give it a moment, then SIGKILL if needed\n await new Promise((resolve) => setTimeout(resolve, 2000));\n if (isProcessAlive(state.pid)) {\n process.kill(state.pid, \"SIGKILL\");\n }\n }\n } catch {\n // Process may have already exited\n }\n\n clearBridgeState(stateDir, instanceId);\n return true;\n}\n\n// ─── Log rotation ──────────────────────────────────────────────\n\nexport function rotateLog(logPath: string): void {\n if (!fs.existsSync(logPath)) return;\n try {\n const stats = fs.statSync(logPath);\n if (stats.size === 0) return;\n const prevPath = `${logPath}.prev`;\n fs.renameSync(logPath, prevPath);\n } catch {\n // Best-effort: don't fail bridge start if rotation fails\n }\n}\n\n// ─── Heartbeat ─────────────────────────────────────────────────\n\n/**\n * Update the heartbeat timestamp for a running bridge.\n * Bridge processes should call this periodically.\n *\n * Only the owning process (matching PID) can update the heartbeat.\n * This prevents state dir collision when multiple writers exist.\n * See: 묵 finding — bridge-heartbeat-state-dir-collision\n */\nexport function updateBridgeHeartbeat(\n stateDir: string,\n instanceId: InstanceId,\n): void {\n const state = loadBridgeState(stateDir, instanceId);\n if (!state) return;\n\n // Guard: only the owning process may update heartbeat\n if (state.pid !== process.pid) return;\n\n state.lastHeartbeat = new Date().toISOString();\n saveBridgeState(stateDir, instanceId, state);\n}\n\n/**\n * Get heartbeat age in seconds. Returns null if no state or no heartbeat.\n */\nexport function getHeartbeatAge(\n stateDir: string,\n instanceId: InstanceId,\n): number | null {\n const state = loadBridgeState(stateDir, instanceId);\n if (!state?.lastHeartbeat) return null;\n const heartbeatTime = new Date(state.lastHeartbeat).getTime();\n if (isNaN(heartbeatTime)) return null;\n return Math.floor((Date.now() - heartbeatTime) / 1000);\n}\n\nexport function getBridgeStatus(\n stateDir: string,\n instanceId: InstanceId,\n): \"running\" | \"stopped\" | \"stale\" {\n const state = loadBridgeState(stateDir, instanceId);\n if (!state) return \"stopped\";\n\n // Primary check: is the process actually alive?\n if (!isProcessAlive(state.pid)) {\n clearBridgeState(stateDir, instanceId);\n return \"stale\";\n }\n\n // Process is alive → running.\n // Heartbeat staleness is informational only — the bridge process\n // is responsible for updating lastHeartbeat. If it doesn't,\n // PID alive is still the authoritative signal.\n return \"running\";\n}\n","/**\n * Common Node.js runtime resolver for all tap-comms child processes.\n *\n * Resolution chain:\n * .node-version + fnm probe → configured command → tsx fallback\n *\n * Extracted from codex-bridge-runner.ts (M69) to share across:\n * - bridge engine spawn\n * - bridge runner spawn\n * - future CLI commands\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { execSync } from \"node:child_process\";\n\n// ─── Types ─────────────────────────────────────────────────────\n\nexport type RuntimeSource = \"fnm\" | \"config\" | \"path\" | \"tsx-fallback\" | \"bun\";\n\nexport interface ResolvedRuntime {\n /** Absolute path or command name for the resolved runtime. */\n command: string;\n /** Whether --experimental-strip-types is supported and should be used. */\n supportsStripTypes: boolean;\n /** Where the runtime was resolved from (for diagnostics). */\n source: RuntimeSource;\n /** Detected major version, if available. */\n majorVersion: number | null;\n}\n\n// ─── .node-version ─────────────────────────────────────────────\n\nexport function readNodeVersion(repoRoot: string): string | null {\n const nvFile = path.join(repoRoot, \".node-version\");\n if (!fs.existsSync(nvFile)) return null;\n try {\n const raw = fs.readFileSync(nvFile, \"utf-8\").trim();\n return raw.length > 0 ? raw.replace(/^v/, \"\") : null;\n } catch {\n return null;\n }\n}\n\n// ─── fnm probe ─────────────────────────────────────────────────\n\nfunction fnmCandidateDirs(): string[] {\n if (process.platform === \"win32\") {\n return [\n process.env.FNM_DIR,\n process.env.APPDATA ? path.join(process.env.APPDATA, \"fnm\") : null,\n process.env.LOCALAPPDATA\n ? path.join(process.env.LOCALAPPDATA, \"fnm\")\n : null,\n process.env.USERPROFILE\n ? path.join(process.env.USERPROFILE, \"scoop\", \"persist\", \"fnm\")\n : null,\n ].filter(Boolean) as string[];\n }\n // macOS / Linux\n return [\n process.env.FNM_DIR,\n process.env.HOME\n ? path.join(process.env.HOME, \".local\", \"share\", \"fnm\")\n : null,\n process.env.HOME ? path.join(process.env.HOME, \".fnm\") : null,\n process.env.XDG_DATA_HOME\n ? path.join(process.env.XDG_DATA_HOME, \"fnm\")\n : null,\n ].filter(Boolean) as string[];\n}\n\nfunction nodeExecutableName(): string {\n return process.platform === \"win32\" ? \"node.exe\" : \"node\";\n}\n\nexport function probeFnmNode(desiredVersion: string): string | null {\n const dirs = fnmCandidateDirs();\n const exe = nodeExecutableName();\n\n for (const baseDir of dirs) {\n const candidate = path.join(\n baseDir,\n \"node-versions\",\n `v${desiredVersion}`,\n \"installation\",\n exe,\n );\n if (!fs.existsSync(candidate)) continue;\n\n try {\n const v = execSync(`\"${candidate}\" --version`, {\n encoding: \"utf-8\",\n timeout: 5000,\n }).trim();\n if (v.startsWith(`v${desiredVersion.split(\".\")[0]}.`)) {\n return candidate;\n }\n } catch {\n // candidate exists but doesn't work — skip\n }\n }\n\n return null;\n}\n\n// ─── Version detection ─────────────────────────────────────────\n\nexport function detectNodeMajorVersion(command: string): number | null {\n try {\n const version = execSync(`\"${command}\" --version`, {\n encoding: \"utf-8\",\n timeout: 5000,\n }).trim();\n const match = version.match(/^v?(\\d+)\\./);\n return match ? parseInt(match[1], 10) : null;\n } catch {\n return null;\n }\n}\n\nexport function checkStripTypesSupport(command: string): boolean {\n const major = detectNodeMajorVersion(command);\n if (major !== null && major >= 22) return true;\n try {\n execSync(`\"${command}\" --experimental-strip-types -e \"\"`, {\n timeout: 5000,\n stdio: \"pipe\",\n });\n return true;\n } catch {\n return false;\n }\n}\n\n// ─── tsx fallback ──────────────────────────────────────────────\n\nexport function findTsxFallback(repoRoot: string): string | null {\n const candidates = [\n path.join(repoRoot, \"node_modules\", \".bin\", \"tsx.exe\"),\n path.join(repoRoot, \"node_modules\", \".bin\", \"tsx.CMD\"),\n path.join(repoRoot, \"node_modules\", \".bin\", \"tsx\"),\n ];\n for (const c of candidates) {\n if (fs.existsSync(c)) return c;\n }\n return null;\n}\n\n// ─── fnm bin directory (for PATH prepending) ───────────────────\n\n/**\n * Returns the directory containing the fnm-managed node binary,\n * suitable for prepending to PATH in child processes.\n */\nexport function getFnmBinDir(repoRoot: string): string | null {\n const desiredVersion = readNodeVersion(repoRoot);\n if (!desiredVersion) return null;\n\n const nodePath = probeFnmNode(desiredVersion);\n if (!nodePath) return null;\n\n return path.dirname(nodePath);\n}\n\n// ─── Main resolver ─────────────────────────────────────────────\n\n/**\n * Resolve the Node.js runtime to use for spawning child processes.\n *\n * Priority: bun passthrough → .node-version + fnm → configured command → tsx fallback\n */\nexport function resolveNodeRuntime(\n configCommand: string,\n repoRoot: string,\n): ResolvedRuntime {\n // Bun: native TS support, no strip-types needed\n if (configCommand === \"bun\" || configCommand.endsWith(\"bun.exe\")) {\n return {\n command: configCommand,\n supportsStripTypes: false,\n source: \"bun\",\n majorVersion: null,\n };\n }\n\n // .node-version + fnm discovery\n const desiredVersion = readNodeVersion(repoRoot);\n if (desiredVersion) {\n const fnmNode = probeFnmNode(desiredVersion);\n if (fnmNode) {\n const major = detectNodeMajorVersion(fnmNode);\n return {\n command: fnmNode,\n supportsStripTypes: checkStripTypesSupport(fnmNode),\n source: \"fnm\",\n majorVersion: major,\n };\n }\n }\n\n // Configured command (from config or PATH)\n const major = detectNodeMajorVersion(configCommand);\n if (major !== null) {\n return {\n command: configCommand,\n supportsStripTypes: checkStripTypesSupport(configCommand),\n source: major === detectNodeMajorVersion(\"node\") ? \"path\" : \"config\",\n majorVersion: major,\n };\n }\n\n // tsx fallback\n const tsx = findTsxFallback(repoRoot);\n if (tsx) {\n return {\n command: tsx,\n supportsStripTypes: false,\n source: \"tsx-fallback\",\n majorVersion: null,\n };\n }\n\n // Last resort\n return {\n command: configCommand,\n supportsStripTypes: false,\n source: \"path\",\n majorVersion: null,\n };\n}\n\n// ─── Env builder for child processes ───────────────────────────\n\n/**\n * Build an env object with fnm Node prepended to PATH.\n * Use this when spawning child processes that need the correct Node.\n */\nexport function buildRuntimeEnv(\n repoRoot: string,\n baseEnv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n const fnmBin = getFnmBinDir(repoRoot);\n if (!fnmBin) return { ...baseEnv };\n\n const pathKey = process.platform === \"win32\" ? \"Path\" : \"PATH\";\n const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? \"\";\n\n return {\n ...baseEnv,\n [pathKey]: `${fnmBin}${path.delimiter}${currentPath}`,\n };\n}\n"],"mappings":"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","names":["fs","path","fs","path","execSync","fs","path","version","major"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@hua-labs/tap",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency CLI for cross-model AI agent communication setup",
5
+ "bin": {
6
+ "tap-comms": "./bin/tap-comms.mjs"
7
+ },
8
+ "main": "./dist/index.mjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.mts",
14
+ "import": "./dist/index.mjs",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "bin"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "clean": "rm -rf dist",
26
+ "type-check": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.6.0",
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "keywords": [
37
+ "tap",
38
+ "comms",
39
+ "ai-agent",
40
+ "cross-model",
41
+ "mcp",
42
+ "claude",
43
+ "codex",
44
+ "gemini",
45
+ "cli"
46
+ ],
47
+ "author": "HUA Labs",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/HUA-Labs/hua-packages.git"
52
+ },
53
+ "sideEffects": false,
54
+ "engines": {
55
+ "node": ">=22.6.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public",
59
+ "provenance": false
60
+ }
61
+ }