mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
Small cleanup items from upstream review (Ark0N/Codeman#353): - OMP_SEARCH_DIRS now leads with ~/.local/bin, matching omp.sh's real installer target (~/.omp/bin was an earlier unverified guess, confirmed wrong against a real --no-cache Docker build). - docs/omp-integration.md: fixed the dead GitHub URL (can1357/omp -> can1357/oh-my-pi), corrected the CLI count (ninth backend, tenth SessionMode incl. shell -- not eighth), matched the install-path guidance to the resolver fix, updated the version example to the actually-tested 18.0.8, and added a Docker-section caveat: --resume pinning does not currently reach an in-container omp process, since Docker panes never see ompConfig. - docs/architecture-invariants.md: fixed a heading missing ", OMP" (CLAUDE.md already linked to the -omp anchor, so the link was dead) and added an OMP specifics paragraph -- the one external CLI missing an entry in this doc. - .changeset/omp-backend.md: corrected the sibling-CLI list (was missing Pi, Grok, and DeepSeek Harness) and the backend count. - Removed a stray orphaned comment fragment in the quick-start docker branch and split two CSS lines that had two declarations jammed onto one line.
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
/**
|
|
* @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. `~/.local/bin`
|
|
* leads: omp.sh's installer targets `$HOME/.local/bin` with no `--dir`
|
|
* override (verified against a real `--no-cache` Docker build — see
|
|
* docker/agent.Dockerfile); `~/.omp/bin` was an unverified guess that turned
|
|
* out wrong, kept after `~/.local/bin` only as a defensive fallback.
|
|
*/
|
|
const OMP_SEARCH_DIRS = [
|
|
join(homedir(), '.local', 'bin'),
|
|
join(homedir(), '.omp', '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;
|
|
}
|