import { accessSync, constants } from "node:fs"; import { delimiter, join } from "node:path"; import { createRequire } from "node:module"; export type C2cNpmResolver = (opts?: { executable?: "c2c"; env?: NodeJS.ProcessEnv | Record; }) => string; export interface ResolveC2cCommandOptions { /** Explicit constructor override. Highest priority. */ explicitBin?: string; /** Environment to inspect; defaults to process.env. */ env?: NodeJS.ProcessEnv | Record; /** Test hook for PATH executable checks. */ pathExists?: (file: string) => boolean; /** Test hook / override for @clanker-code/c2c resolution. */ npmResolver?: C2cNpmResolver | null; /** Platform override for tests. */ platform?: NodeJS.Platform; } const require = createRequire(import.meta.url); function canExecute(file: string): boolean { try { accessSync(file, constants.X_OK); return true; } catch { return false; } } function executableNames(platform: NodeJS.Platform): string[] { return platform === "win32" ? ["c2c.exe", "c2c.cmd", "c2c.bat", "c2c"] : ["c2c"]; } function isNodeModulesBin(candidate: string): boolean { return candidate.split(/[\\/]+/).slice(-3, -1).join("/") === "node_modules/.bin"; } function findOnPath( env: NodeJS.ProcessEnv | Record, pathExists: (file: string) => boolean, platform: NodeJS.Platform, ): string | null { const pathValue = env.PATH ?? ""; if (!pathValue) return null; for (const dir of pathValue.split(delimiter).filter(Boolean)) { for (const name of executableNames(platform)) { const candidate = join(dir, name); // The @clanker-code/c2c package exposes a JS shim in node_modules/.bin. // That shim is fine for humans, but c2c registration records process // liveness from the native process context; the short-lived Node shim can // make freshly registered peers look dead. Skip shims and prefer either a // system native binary or the package resolver's native platform binary. if (!isNodeModulesBin(candidate) && pathExists(candidate)) return candidate; } } return null; } function loadBundledResolver(): C2cNpmResolver | null { try { const mod = require("@clanker-code/c2c") as { resolveC2cBinary?: unknown }; return typeof mod.resolveC2cBinary === "function" ? (mod.resolveC2cBinary as C2cNpmResolver) : null; } catch { return null; } } /** * Resolve the c2c command for pi-c2c. * * Priority: * explicit bin → C2C_BIN → bundled @clanker-code/c2c → PATH → literal "c2c" * * Bundled package is preferred over PATH so: * - I001 multi-key ships with @clanker-code/c2c ≥ 0.14.3 even if a stale * system install is on PATH; * - first bootstrap never synchronously probes arbitrary PATH binaries. * * Override with C2C_BIN for local experimental builds. */ export function resolveC2cCommand(opts: ResolveC2cCommandOptions = {}): string { const explicit = opts.explicitBin?.trim(); if (explicit) return explicit; const env = opts.env ?? process.env; const envBin = env.C2C_BIN?.trim(); if (envBin) return envBin; const resolver = opts.npmResolver === undefined ? loadBundledResolver() : opts.npmResolver; if (resolver) { try { return resolver({ executable: "c2c", env }); } catch { // Fall through to PATH / literal. } } const pathHit = findOnPath( env, opts.pathExists ?? canExecute, opts.platform ?? process.platform, ); if (pathHit) return pathHit; return "c2c"; } /** * Resolve only the bundled `@clanker-code/c2c` native binary (ignore PATH). * Used for packaged multi-key integration tests. */ export function resolveBundledC2cCommand( opts: Pick = {}, ): string | null { const resolver = opts.npmResolver === undefined ? loadBundledResolver() : opts.npmResolver; if (!resolver) return null; try { return resolver({ executable: "c2c", env: opts.env ?? process.env }); } catch { return null; } }