feat(omp): rebase OMP backend onto master (merge Pi + OMP modes)

This commit is contained in:
timkjr
2026-08-26 20:05:48 -05:00
parent 7dfb4acf24
commit 4f5678fac4
29 changed files with 579 additions and 64 deletions
+134
View File
@@ -0,0 +1,134 @@
/**
* @fileoverview Resolve the OMP CLI binary across common install paths.
*
* Uses the shared `createCliExecutableResolver` (cli-executable-resolver.ts),
* same as the sibling claude/opencode/codex/gemini/antigravity/pi resolvers:
* server process PATH first, then common install directories, then — last,
* because it is the only step that spawns anything — an interactive login
* shell, which is what finds nvm/Homebrew/user-npm installs when Codeman runs
* as a systemd/launchd service with a minimal PATH.
*
* Provides an augmented PATH directory for tmux sessions.
*
* @module utils/omp-cli-resolver
*/
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js';
import {
createCliExecutableResolver,
formatCliNotFoundMessage,
type CliResolverHost,
} from './cli-executable-resolver.js';
/** Common directories where the OMP CLI binary may be installed */
const OMP_SEARCH_DIRS = [
join(homedir(), '.omp', 'bin'),
join(homedir(), '.local', 'bin'),
'/usr/local/bin',
join(homedir(), '.bun', 'bin'),
join(homedir(), '.npm-global', 'bin'),
join(homedir(), 'bin'),
];
/**
* A real `omp --version` prints `omp/<semver>` (e.g. `omp/17.4.0`).
*
* Shape mirrors PI_VERSION_REGEX: a capturing group and a leading boundary so
* `omp/17.4.0` matches while an unrelated `omp` (some other program) does not.
*/
export const OMP_VERSION_REGEX = /(?:^|\s)omp\/(\d+\.\d+\.\d+)/;
const OMP_NOT_FOUND = 'OMP CLI not found. Install with: curl -fsSL https://omp.sh/install | sh';
/**
* Run `omp --version` on a candidate path and return the trimmed version when
* it looks like the coding agent. Returns null for anything else — a missing
* binary, a non-zero exit, a hang (timeout), or output that is not
* `omp/<semver>`-shaped (which is how an unrelated `omp` on PATH gets rejected).
*
* Never runs under vitest: the suites must stay hermetic and must not depend on
* whether the dev box happens to have omp installed. The shared resolver host
* is already inert under vitest, so this gate is defense in depth for any
* opted-in host that still carries the default probe.
*/
function probeOmpVersion(binPath: string): string | null {
if (process.env.VITEST) return null;
try {
const out = execFileSync(binPath, ['--version'], {
encoding: 'utf-8',
timeout: EXEC_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore'],
// A stuck or hostile `omp` that ignores SIGTERM would survive the timeout
// and block the server (execFileSync keeps waiting after the signal).
killSignal: 'SIGKILL',
}).trim();
const candidate = OMP_VERSION_REGEX.exec(out)?.[1];
if (candidate) return candidate;
console.warn(`[OmpResolver] Ignoring ${binPath}: "omp --version" printed ${JSON.stringify(out.slice(0, 80))}`);
} catch (err) {
console.warn(`[OmpResolver] Ignoring ${binPath}: "omp --version" failed (${(err as Error).message})`);
}
return null;
}
type OmpVersionProbe = (binPath: string) => string | null;
function createOmpResolver(host?: CliResolverHost, versionProbe: OmpVersionProbe = probeOmpVersion, now?: () => number) {
return createCliExecutableResolver<string>(
{
binary: 'omp',
searchDirs: OMP_SEARCH_DIRS,
validateCandidate: (binPath) => {
const version = versionProbe(binPath);
return version ? { accepted: true, metadata: version } : { accepted: false };
},
now,
},
host
);
}
/**
* Creates an isolated OMP wrapper around an injected host, version probe and
* clock. Omitting `versionProbe` keeps the ambient (VITEST-gated) probe, which
* is exactly what the hermeticity test exercises.
*/
export function createOmpResolverForTest(host: CliResolverHost, versionProbe?: OmpVersionProbe, now?: () => number) {
return createOmpResolver(host, versionProbe ?? probeOmpVersion, now);
}
const ompResolver = createOmpResolver();
/**
* Finds the directory containing a verified `omp` binary.
* Checks `which omp` first, then falls back to common install locations. Every
* candidate must pass the `omp --version` sanity probe before it is accepted.
*
* @returns Directory path, or null if not found
*/
export function resolveOmpDir(): string | null {
return ompResolver.resolve()?.directory ?? null;
}
/**
* Check if the OMP CLI is available on the system.
*/
export function isOmpAvailable(): boolean {
return resolveOmpDir() !== null;
}
export function getOmpNotFoundMessage(): string {
return formatCliNotFoundMessage(OMP_NOT_FOUND, ompResolver.diagnostics());
}
/**
* Version reported by the resolved `omp` binary, or null when omp is
* unavailable. Surfaced through `GET /api/omp/status` so a misresolution is
* diagnosable from the UI.
*/
export function getOmpCliVersion(): string | null {
return ompResolver.resolve()?.metadata ?? null;
}