diff --git a/packages/core/src/evaluation/providers/pi-coding-agent.ts b/packages/core/src/evaluation/providers/pi-coding-agent.ts index b900ac016..e3f03c428 100644 --- a/packages/core/src/evaluation/providers/pi-coding-agent.ts +++ b/packages/core/src/evaluation/providers/pi-coding-agent.ts @@ -10,13 +10,14 @@ import { execSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; -import { accessSync, createWriteStream } from 'node:fs'; +import { accessSync, createWriteStream, mkdirSync } from 'node:fs'; import type { WriteStream } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import path from 'node:path'; import { createInterface } from 'node:readline'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { getAgentvHome } from '../../paths.js'; import { recordPiLogEntry } from './pi-log-tracker.js'; import { normalizeAzureSdkBaseUrl, @@ -57,7 +58,7 @@ async function promptInstall(): Promise { } } -/** Resolve agentv's own package root (where bun add should install peer deps). */ +/** Resolve agentv's own package root (where npm install should add peer deps). */ function findAgentvRoot(): string { const thisFile = fileURLToPath(import.meta.url); let dir = path.dirname(thisFile); @@ -78,30 +79,144 @@ function findAgentvRoot(): string { return path.dirname(thisFile); } -async function doLoadSdkModules(): Promise { +function findManagedSdkInstallRoot(): string { + return path.join(getAgentvHome(), 'deps', 'pi-sdk'); +} + +function resolveGlobalNpmRoot(): string | undefined { + try { + const root = execSync('npm root -g', { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + return root.length > 0 ? root : undefined; + } catch { + return undefined; + } +} + +function buildGlobalModuleEntry(moduleName: string, globalNpmRoot: string): string { + return path.join(globalNpmRoot, ...moduleName.split('/'), 'dist', 'index.js'); +} + +function findAccessiblePath(paths: readonly string[]): string | undefined { + for (const candidate of paths) { + try { + accessSync(candidate); + return candidate; + } catch {} + } + return undefined; +} + +async function tryImportLocalSdkModules(): Promise { try { [piCodingAgentModule, piAiModule] = await Promise.all([ import('@mariozechner/pi-coding-agent'), import('@mariozechner/pi-ai'), ]); + return true; } catch { - if (await promptInstall()) { - const installDir = findAgentvRoot(); - console.error(`Installing @mariozechner/pi-coding-agent into ${installDir}...`); - execSync('bun add @mariozechner/pi-coding-agent', { - cwd: installDir, - stdio: 'inherit', - }); - [piCodingAgentModule, piAiModule] = await Promise.all([ - import('@mariozechner/pi-coding-agent'), - import('@mariozechner/pi-ai'), - ]); - } else { - throw new Error( - 'pi-coding-agent SDK is not installed. Install it with:\n bun add @mariozechner/pi-coding-agent', - ); + return false; + } +} + +async function tryImportManagedSdkModules(): Promise { + const managedRoot = findManagedSdkInstallRoot(); + const piCodingAgentEntry = findAccessiblePath([ + path.join(managedRoot, 'node_modules', '@mariozechner', 'pi-coding-agent', 'dist', 'index.js'), + ]); + const piAiEntry = findAccessiblePath([ + path.join(managedRoot, 'node_modules', '@mariozechner', 'pi-ai', 'dist', 'index.js'), + path.join( + managedRoot, + 'node_modules', + '@mariozechner', + 'pi-coding-agent', + 'node_modules', + '@mariozechner', + 'pi-ai', + 'dist', + 'index.js', + ), + ]); + + if (!piCodingAgentEntry || !piAiEntry) return false; + + try { + [piCodingAgentModule, piAiModule] = await Promise.all([ + import(pathToFileURL(piCodingAgentEntry).href), + import(pathToFileURL(piAiEntry).href), + ]); + return true; + } catch { + return false; + } +} + +async function tryImportGlobalSdkModules(): Promise { + const globalNpmRoot = resolveGlobalNpmRoot(); + if (!globalNpmRoot) return false; + + const piCodingAgentEntry = findAccessiblePath([ + buildGlobalModuleEntry('@mariozechner/pi-coding-agent', globalNpmRoot), + ]); + const piAiEntry = findAccessiblePath([ + buildGlobalModuleEntry('@mariozechner/pi-ai', globalNpmRoot), + path.join( + globalNpmRoot, + '@mariozechner', + 'pi-coding-agent', + 'node_modules', + '@mariozechner', + 'pi-ai', + 'dist', + 'index.js', + ), + ]); + + if (!piCodingAgentEntry || !piAiEntry) return false; + + try { + [piCodingAgentModule, piAiModule] = await Promise.all([ + import(pathToFileURL(piCodingAgentEntry).href), + import(pathToFileURL(piAiEntry).href), + ]); + return true; + } catch { + return false; + } +} + +function installSdkModules(installDir: string): void { + console.error(`Installing @mariozechner/pi-coding-agent into ${installDir} via npm...`); + mkdirSync(installDir, { recursive: true }); + execSync('npm install --no-save --no-package-lock @mariozechner/pi-coding-agent', { + cwd: installDir, + stdio: 'inherit', + }); +} + +async function doLoadSdkModules(): Promise { + if ( + (await tryImportLocalSdkModules()) || + (await tryImportManagedSdkModules()) || + (await tryImportGlobalSdkModules()) + ) { + return; + } + + if (await promptInstall()) { + const installDir = findManagedSdkInstallRoot(); + installSdkModules(installDir); + if (await tryImportManagedSdkModules()) { + return; } } + + throw new Error( + 'pi-coding-agent SDK is not installed. Install it with:\n npm install @mariozechner/pi-coding-agent', + ); } async function loadSdkModules() { @@ -716,3 +831,11 @@ function extractToolCalls( return toolCalls; } + +/** @internal Exported for testing only. */ +export const _internal = { + buildGlobalModuleEntry, + findAgentvRoot, + findManagedSdkInstallRoot, + resolveGlobalNpmRoot, +}; diff --git a/packages/core/test/evaluation/providers/pi-coding-agent.test.ts b/packages/core/test/evaluation/providers/pi-coding-agent.test.ts index 0a264f1ff..0fb5041ad 100644 --- a/packages/core/test/evaluation/providers/pi-coding-agent.test.ts +++ b/packages/core/test/evaluation/providers/pi-coding-agent.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'bun:test'; -import { PiCodingAgentProvider } from '../../../src/evaluation/providers/pi-coding-agent.js'; +import { + PiCodingAgentProvider, + _internal, +} from '../../../src/evaluation/providers/pi-coding-agent.js'; describe('PiCodingAgentProvider', () => { it('has the correct kind and id', () => { @@ -44,4 +47,20 @@ describe('PiCodingAgentProvider', () => { process.env.OPENAI_BASE_URL = original; } }); + + it('builds the expected global npm module entry path', () => { + expect( + _internal.buildGlobalModuleEntry( + '@mariozechner/pi-coding-agent', + 'C:\\npm-global\\node_modules', + ), + ).toBe('C:\\npm-global\\node_modules\\@mariozechner\\pi-coding-agent\\dist\\index.js'); + expect( + _internal.buildGlobalModuleEntry('@mariozechner/pi-ai', 'C:\\npm-global\\node_modules'), + ).toBe('C:\\npm-global\\node_modules\\@mariozechner\\pi-ai\\dist\\index.js'); + }); + + it('finds the agentv package root', () => { + expect(_internal.findAgentvRoot().endsWith('packages\\core')).toBe(true); + }); });