diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11d4830cc..e4a4c60b8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- Fixed `omp setup python` to validate the same configured or discovered interpreter used by the Python eval runtime. + + ## [17.2.4] - 2026-08-01 ### Added diff --git a/packages/coding-agent/src/cli/setup-cli.ts b/packages/coding-agent/src/cli/setup-cli.ts index 4f3be8f50..24d9cbb73 100644 --- a/packages/coding-agent/src/cli/setup-cli.ts +++ b/packages/coding-agent/src/cli/setup-cli.ts @@ -4,10 +4,10 @@ * Handles `omp setup` for onboarding and `omp setup ` for optional dependencies. */ import * as path from "node:path"; -import { $which, APP_NAME, getProjectDir, getPythonEnvDir } from "@oh-my-pi/pi-utils"; -import { $ } from "bun"; +import { APP_NAME, getProjectDir, getPythonEnvDir } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { Settings, settings } from "../config/settings"; +import { checkPythonKernelAvailability } from "../eval/py/kernel"; import { theme } from "../modes/theme/theme"; import { downloadSttModel, isSttModelCached } from "../stt/downloader"; import { isSttModelKey, STT_MODEL_OPTIONS } from "../stt/models"; @@ -82,25 +82,14 @@ function managedPythonPath(): string { /** * Check Python environment and kernel dependencies. */ -async function checkPythonSetup(): Promise { - const result: PythonCheckResult = { - available: false, +async function checkPythonSetup(cwd: string, interpreter?: string): Promise { + const availability = await checkPythonKernelAvailability(cwd, interpreter); + return { + available: availability.ok, + pythonPath: availability.pythonPath, + usingManagedEnv: availability.pythonPath === managedPythonPath(), managedEnvPath: MANAGED_PYTHON_ENV, }; - - const systemPythonPath = $which("python") ?? $which("python3"); - const managedPath = managedPythonPath(); - const hasManagedEnv = await Bun.file(managedPath).exists(); - - const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined); - if (!pythonPath) { - return result; - } - const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow(); - result.pythonPath = pythonPath; - result.available = probe.exitCode === 0; - result.usingManagedEnv = pythonPath === managedPath; - return result; } /** @@ -126,7 +115,10 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise { } async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Promise { - const check = await checkPythonSetup(); + const cwd = getProjectDir(); + const projectSettings = await Settings.init({ cwd }); + const interpreter = projectSettings.get("python.interpreter")?.trim() || undefined; + const check = await checkPythonSetup(cwd, interpreter); if (flags.json) { console.log(JSON.stringify(check, null, 2)); @@ -136,7 +128,7 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr if (!check.pythonPath) { console.error(chalk.red(`${theme.status.error} Python not found`)); - console.error(chalk.dim("Install Python 3.8+ and ensure it's in your PATH")); + console.error(chalk.dim("Install Python 3.8+ or set python.interpreter to its executable path")); process.exit(1); } diff --git a/packages/coding-agent/test/setup-cli.test.ts b/packages/coding-agent/test/setup-cli.test.ts new file mode 100644 index 000000000..92834855a --- /dev/null +++ b/packages/coding-agent/test/setup-cli.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { TempDir } from "@oh-my-pi/pi-utils"; + +const cliEntry = path.join(import.meta.dir, "..", "src", "cli.ts"); + +interface CliProcessResult { + exitCode: number; + output: string; + error: string; +} + +async function runSetupPython(cwd: string): Promise { + const env: NodeJS.ProcessEnv = { + ...process.env, + NO_COLOR: "1", + PI_CODING_AGENT_DIR: path.join(cwd, "agent"), + }; + delete env.VIRTUAL_ENV; + delete env.CONDA_DEFAULT_ENV; + delete env.CONDA_PREFIX; + const proc = Bun.spawn([process.execPath, cliEntry, "setup", "python", "--json"], { + cwd, + stdout: "pipe", + stderr: "pipe", + env, + }); + const output = new Response(proc.stdout).text(); + const error = new Response(proc.stderr).text(); + const [exitCode, stdout, stderr] = await Promise.all([proc.exited, output, error]); + return { exitCode, output: stdout, error: stderr }; +} + +describe("omp setup python", () => { + let projectDir: TempDir | undefined; + + afterEach(async () => { + await projectDir?.remove(); + projectDir = undefined; + }); + + it.skipIf(process.platform === "win32")( + "probes the project-configured interpreter instead of the PATH interpreter", + async () => { + projectDir = TempDir.createSync("@omp-setup-python-"); + const cwd = projectDir.path(); + const interpreter = path.join(cwd, "configured-python"); + await Bun.write(interpreter, "#!/bin/sh\nexit 0\n"); + await fs.chmod(interpreter, 0o755); + await Bun.write(path.join(cwd, ".omp", "config.yml"), `python:\n interpreter: ${interpreter}\n`); + + const result = await runSetupPython(cwd); + + expect(result.error).toBe(""); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.output)).toMatchObject({ + available: true, + pythonPath: interpreter, + usingManagedEnv: false, + }); + }, + ); + it.skipIf(process.platform === "win32")("prefers the project venv over the PATH interpreter", async () => { + projectDir = TempDir.createSync("@omp-setup-python-"); + const cwd = projectDir.path(); + const interpreter = path.join(cwd, ".venv", "bin", "python"); + await Bun.write(interpreter, "#!/bin/sh\nexit 0\n"); + await fs.chmod(interpreter, 0o755); + + const result = await runSetupPython(cwd); + + expect(result.error).toBe(""); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.output)).toMatchObject({ + available: true, + pythonPath: interpreter, + usingManagedEnv: false, + }); + }); +});