From d9812cf4f26395af31ab3e7be002364be94c3ada Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Tue, 31 Mar 2026 09:18:05 -0700 Subject: [PATCH] Fix Pi package updates and merge feynman-model --- CHANGELOG.md | 9 + extensions/research-tools.ts | 2 + extensions/research-tools/feynman-model.ts | 309 +++++++++++++++++++++ metadata/commands.mjs | 1 + package-lock.json | 38 +-- package.json | 4 +- src/cli.ts | 3 + src/model/registry.ts | 3 +- src/pi/runtime.ts | 15 +- tests/pi-runtime.test.ts | 33 ++- 10 files changed, 392 insertions(+), 25 deletions(-) create mode 100644 extensions/research-tools/feynman-model.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b71fc..66e618f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,3 +95,12 @@ Use this file to track chronology, not release notes. Keep entries short, factua - Failed / learned: An initial local `build:native-bundle` check failed because `npm pack` and `build:native-bundle` were run in parallel, and `prepack` intentionally removes `dist/release`; rerunning `npm run build:native-bundle` sequentially succeeded. - Blockers: None in the repo; publishing still depends on the GitHub workflow running on the bumped version. - Next: Push the `0.2.16` release bump and monitor npm/GitHub release publication. + +### 2026-03-31 10:45 PDT — pi-maintenance-issues-prs + +- Objective: Triage open Pi-related issues/PRs, fix the concrete package update regression, and refresh Pi dependencies against current upstream releases. +- Changed: Pinned direct package-manager operations (`feynman update`, `feynman packages install`) to Feynman's npm prefix by exporting `FEYNMAN_NPM_PREFIX`, `NPM_CONFIG_PREFIX`, and `npm_config_prefix` before invoking Pi's `DefaultPackageManager`; bumped `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` from `0.62.0` to `0.64.0`; adapted `src/model/registry.ts` to the new `ModelRegistry.create(...)` factory; integrated PR #15's `/feynman-model` command on top of current `main`. +- Verified: Ran `npm test`, `npm run typecheck`, and `npm run build` successfully after the dependency bump and PR integration; confirmed upstream `pi-coding-agent@0.64.0` still uses `npm install -g` for user-scope package updates, so the Feynman-side prefix fix is still required. +- Failed / learned: PR #14 is a stale branch with no clean merge path against current `main`; the only user-facing delta is the ValiChord prompt/skill addition, and the branch also carries unrelated release churn plus demo-style material, so it was not merged in this pass. +- Blockers: None in the local repo state; remote merge/push still depends on repository credentials and branch policy. +- Next: If remote write access is available, commit and push the validated maintenance changes, then close issue #22 and resolve PR #15 as merged while leaving PR #14 unmerged pending a cleaned-up, non-promotional resubmission. diff --git a/extensions/research-tools.ts b/extensions/research-tools.ts index 45d47af..6e3553c 100644 --- a/extensions/research-tools.ts +++ b/extensions/research-tools.ts @@ -1,6 +1,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { registerAlphaTools } from "./research-tools/alpha.js"; +import { registerFeynmanModelCommand } from "./research-tools/feynman-model.js"; import { installFeynmanHeader } from "./research-tools/header.js"; import { registerHelpCommand } from "./research-tools/help.js"; import { registerInitCommand, registerOutputsCommand } from "./research-tools/project.js"; @@ -17,6 +18,7 @@ export default function researchTools(pi: ExtensionAPI): void { }); registerAlphaTools(pi); + registerFeynmanModelCommand(pi); registerHelpCommand(pi); registerInitCommand(pi); registerOutputsCommand(pi); diff --git a/extensions/research-tools/feynman-model.ts b/extensions/research-tools/feynman-model.ts new file mode 100644 index 0000000..9c83010 --- /dev/null +++ b/extensions/research-tools/feynman-model.ts @@ -0,0 +1,309 @@ +import { type Dirent, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join, resolve } from "node:path"; + +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +const FRONTMATTER_PATTERN = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/; +const INHERIT_MAIN = "__inherit_main__"; + +type FrontmatterDocument = { + lines: string[]; + body: string; + eol: string; + trailingNewline: boolean; +}; + +type SubagentModelConfig = { + agent: string; + model?: string; + filePath: string; +}; + +type SelectOption = { + label: string; + value: T; +}; + +type CommandContext = Parameters[1]["handler"]>[1]; + +type TargetChoice = + | { type: "main" } + | { type: "subagent"; agent: string; model?: string }; + +function expandHomePath(value: string): string { + if (value === "~") return homedir(); + if (value.startsWith("~/")) return resolve(homedir(), value.slice(2)); + return value; +} + +function resolveFeynmanAgentDir(): string { + const configured = process.env.PI_CODING_AGENT_DIR ?? process.env.FEYNMAN_CODING_AGENT_DIR; + if (configured?.trim()) { + return resolve(expandHomePath(configured.trim())); + } + return resolve(homedir(), ".feynman", "agent"); +} + +function formatModelSpec(model: { provider: string; id: string }): string { + return `${model.provider}/${model.id}`; +} + +function detectEol(text: string): string { + return text.includes("\r\n") ? "\r\n" : "\n"; +} + +function normalizeLineEndings(text: string): string { + return text.replace(/\r\n/g, "\n"); +} + +function parseFrontmatterDocument(text: string): FrontmatterDocument | null { + const normalized = normalizeLineEndings(text); + const match = normalized.match(FRONTMATTER_PATTERN); + if (!match) return null; + + return { + lines: match[1].split("\n"), + body: match[2] ?? "", + eol: detectEol(text), + trailingNewline: normalized.endsWith("\n"), + }; +} + +function serializeFrontmatterDocument(document: FrontmatterDocument): string { + const normalized = `---\n${document.lines.join("\n")}\n---\n${document.body}`; + const withTrailingNewline = + document.trailingNewline && !normalized.endsWith("\n") ? `${normalized}\n` : normalized; + return document.eol === "\n" ? withTrailingNewline : withTrailingNewline.replace(/\n/g, "\r\n"); +} + +function parseFrontmatterKey(line: string): string | undefined { + const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*:/); + return match?.[1]?.toLowerCase(); +} + +function getFrontmatterValue(lines: string[], key: string): string | undefined { + const normalizedKey = key.toLowerCase(); + for (const line of lines) { + const parsedKey = parseFrontmatterKey(line); + if (parsedKey !== normalizedKey) continue; + const separatorIndex = line.indexOf(":"); + if (separatorIndex === -1) return undefined; + const value = line.slice(separatorIndex + 1).trim(); + return value.length > 0 ? value : undefined; + } + return undefined; +} + +function upsertFrontmatterValue(lines: string[], key: string, value: string): string[] { + const normalizedKey = key.toLowerCase(); + const nextLines = [...lines]; + const existingIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === normalizedKey); + const serialized = `${key}: ${value}`; + + if (existingIndex !== -1) { + nextLines[existingIndex] = serialized; + return nextLines; + } + + const descriptionIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === "description"); + const nameIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === "name"); + const insertIndex = descriptionIndex !== -1 ? descriptionIndex + 1 : nameIndex !== -1 ? nameIndex + 1 : nextLines.length; + nextLines.splice(insertIndex, 0, serialized); + return nextLines; +} + +function removeFrontmatterKey(lines: string[], key: string): string[] { + const normalizedKey = key.toLowerCase(); + return lines.filter((line) => parseFrontmatterKey(line) !== normalizedKey); +} + +function normalizeAgentName(name: string): string { + return name.trim().toLowerCase(); +} + +function getAgentsDir(agentDir: string): string { + return join(agentDir, "agents"); +} + +function listAgentFiles(agentsDir: string): string[] { + if (!existsSync(agentsDir)) return []; + + return readdirSync(agentsDir, { withFileTypes: true }) + .filter((entry: Dirent) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md")) + .filter((entry) => !entry.name.endsWith(".chain.md")) + .map((entry) => join(agentsDir, entry.name)); +} + +function readAgentConfig(filePath: string): SubagentModelConfig { + const content = readFileSync(filePath, "utf8"); + const parsed = parseFrontmatterDocument(content); + const fallbackName = basename(filePath, ".md"); + if (!parsed) return { agent: fallbackName, filePath }; + + return { + agent: getFrontmatterValue(parsed.lines, "name") ?? fallbackName, + model: getFrontmatterValue(parsed.lines, "model"), + filePath, + }; +} + +function listSubagentModelConfigs(agentDir: string): SubagentModelConfig[] { + return listAgentFiles(getAgentsDir(agentDir)) + .map((filePath) => readAgentConfig(filePath)) + .sort((left, right) => left.agent.localeCompare(right.agent)); +} + +function findAgentConfig(configs: SubagentModelConfig[], agentName: string): SubagentModelConfig | undefined { + const normalized = normalizeAgentName(agentName); + return ( + configs.find((config) => normalizeAgentName(config.agent) === normalized) ?? + configs.find((config) => normalizeAgentName(basename(config.filePath, ".md")) === normalized) + ); +} + +function getAgentConfigOrThrow(agentDir: string, agentName: string): SubagentModelConfig { + const configs = listSubagentModelConfigs(agentDir); + const target = findAgentConfig(configs, agentName); + if (target) return target; + + if (configs.length === 0) { + throw new Error(`No subagent definitions found in ${getAgentsDir(agentDir)}.`); + } + + const availableAgents = configs.map((config) => config.agent).join(", "); + throw new Error(`Unknown subagent: ${agentName}. Available agents: ${availableAgents}`); +} + +function setSubagentModel(agentDir: string, agentName: string, modelSpec: string): void { + const normalizedModelSpec = modelSpec.trim(); + if (!normalizedModelSpec) throw new Error("Model spec cannot be empty."); + + const target = getAgentConfigOrThrow(agentDir, agentName); + const content = readFileSync(target.filePath, "utf8"); + const parsed = parseFrontmatterDocument(content); + + if (!parsed) { + const eol = detectEol(content); + const injected = `---${eol}name: ${target.agent}${eol}model: ${normalizedModelSpec}${eol}---${eol}${content}`; + writeFileSync(target.filePath, injected, "utf8"); + return; + } + + const nextLines = upsertFrontmatterValue(parsed.lines, "model", normalizedModelSpec); + if (nextLines.join("\n") !== parsed.lines.join("\n")) { + writeFileSync(target.filePath, serializeFrontmatterDocument({ ...parsed, lines: nextLines }), "utf8"); + } +} + +function unsetSubagentModel(agentDir: string, agentName: string): void { + const target = getAgentConfigOrThrow(agentDir, agentName); + const content = readFileSync(target.filePath, "utf8"); + const parsed = parseFrontmatterDocument(content); + if (!parsed) return; + + const nextLines = removeFrontmatterKey(parsed.lines, "model"); + if (nextLines.join("\n") !== parsed.lines.join("\n")) { + writeFileSync(target.filePath, serializeFrontmatterDocument({ ...parsed, lines: nextLines }), "utf8"); + } +} + +async function selectOption( + ctx: CommandContext, + title: string, + options: SelectOption[], +): Promise { + const selected = await ctx.ui.select( + title, + options.map((option) => option.label), + ); + if (!selected) return undefined; + return options.find((option) => option.label === selected)?.value; +} + +export function registerFeynmanModelCommand(pi: ExtensionAPI): void { + pi.registerCommand("feynman-model", { + description: "Open Feynman model menu (main + per-subagent overrides).", + handler: async (_args, ctx) => { + if (!ctx.hasUI) { + ctx.ui.notify("feynman-model requires interactive mode.", "error"); + return; + } + + try { + ctx.modelRegistry.refresh(); + const availableModels = [...ctx.modelRegistry.getAvailable()].sort((left, right) => + formatModelSpec(left).localeCompare(formatModelSpec(right)), + ); + if (availableModels.length === 0) { + ctx.ui.notify("No models available.", "error"); + return; + } + + const agentDir = resolveFeynmanAgentDir(); + const subagentConfigs = listSubagentModelConfigs(agentDir); + const currentMain = ctx.model ? formatModelSpec(ctx.model) : "(none)"; + + const targetOptions: SelectOption[] = [ + { label: `main (default): ${currentMain}`, value: { type: "main" } }, + ...subagentConfigs.map((config) => ({ + label: `${config.agent}: ${config.model ?? "default"}`, + value: { type: "subagent" as const, agent: config.agent, model: config.model }, + })), + ]; + + const target = await selectOption(ctx, "Choose target", targetOptions); + if (!target) return; + + if (target.type === "main") { + const selectedModel = await selectOption( + ctx, + "Select main model", + availableModels.map((model) => { + const spec = formatModelSpec(model); + const suffix = spec === currentMain ? " (current)" : ""; + return { label: `${spec}${suffix}`, value: model }; + }), + ); + if (!selectedModel) return; + + const success = await pi.setModel(selectedModel); + if (!success) { + ctx.ui.notify(`No API key found for ${selectedModel.provider}.`, "error"); + return; + } + ctx.ui.notify(`Main model set to ${formatModelSpec(selectedModel)}.`, "info"); + return; + } + + const selectedSubagentModel = await selectOption( + ctx, + `Select model for ${target.agent}`, + [ + { + label: target.model ? "(inherit main default)" : "(inherit main default) (current)", + value: INHERIT_MAIN, + }, + ...availableModels.map((model) => { + const spec = formatModelSpec(model); + const suffix = spec === target.model ? " (current)" : ""; + return { label: `${spec}${suffix}`, value: spec }; + }), + ], + ); + if (!selectedSubagentModel) return; + + if (selectedSubagentModel === INHERIT_MAIN) { + unsetSubagentModel(agentDir, target.agent); + ctx.ui.notify(`${target.agent} now inherits the main model.`, "info"); + return; + } + + setSubagentModel(agentDir, target.agent, selectedSubagentModel); + ctx.ui.notify(`${target.agent} model set to ${selectedSubagentModel}.`, "info"); + } catch (error) { + ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); + } + }, + }); +} diff --git a/metadata/commands.mjs b/metadata/commands.mjs index 8691155..bc92441 100644 --- a/metadata/commands.mjs +++ b/metadata/commands.mjs @@ -36,6 +36,7 @@ export function readPromptSpecs(appRoot) { export const extensionCommandSpecs = [ { name: "help", args: "", section: "Project & Session", description: "Show grouped Feynman commands and prefill the editor with a selected command.", publicDocs: true }, + { name: "feynman-model", args: "", section: "Project & Session", description: "Open Feynman model menu (main + per-subagent overrides).", publicDocs: true }, { name: "init", args: "", section: "Project & Session", description: "Bootstrap AGENTS.md and session-log folders for a research project.", publicDocs: true }, { name: "outputs", args: "", section: "Project & Session", description: "Browse all research artifacts (papers, outputs, experiments, notes).", publicDocs: true }, ]; diff --git a/package-lock.json b/package-lock.json index 99cd5aa..7793523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,12 @@ "": { "name": "@companion-ai/feynman", "version": "0.2.16", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@companion-ai/alpha-hub": "^0.1.2", - "@mariozechner/pi-ai": "^0.62.0", - "@mariozechner/pi-coding-agent": "^0.62.0", + "@mariozechner/pi-ai": "^0.64.0", + "@mariozechner/pi-coding-agent": "^0.64.0", "@sinclair/typebox": "^0.34.48", "dotenv": "^17.3.1" }, @@ -1468,21 +1469,21 @@ } }, "node_modules/@mariozechner/pi-agent-core": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.62.0.tgz", - "integrity": "sha512-SBjqgDrgKOaC+IGzFGB3jXQErv9H1QMYnWFvUg6ra6dG0ZgWFBUZb6unidngWLsmaxSDWes6KeKiVFMsr2VSEQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.64.0.tgz", + "integrity": "sha512-IN/sIxWOD0v1OFVXHB605SGiZhO5XdEWG5dO8EAV08n3jz/p12o4OuYGvhGXmHhU28WXa/FGWC+FO5xiIih8Uw==", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.62.0" + "@mariozechner/pi-ai": "^0.64.0" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@mariozechner/pi-ai": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.62.0.tgz", - "integrity": "sha512-mJgryZ5RgBQG++tiETMtCQQJoH2MAhKetCfqI98NMvGydu7L9x2qC2JekQlRaAgIlTgv4MRH1UXHMEs4UweE/Q==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.64.0.tgz", + "integrity": "sha512-Z/Jnf+JSVDPLRcxJsa8XhYTJKIqKekNueaCpBLGQHgizL1F9RQ1Rur3rIfZpfXkt2cLu/AIPtOs223ueuoWaWg==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -1507,16 +1508,17 @@ } }, "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.62.0.tgz", - "integrity": "sha512-f1NnExqsHuA6w8UVlBtPsvTBhdkMc0h1JD9SzGCdWTLou5GHJr2JIP6DlwV9IKWAnM+sAelaoFez+14wLP2zOQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.64.0.tgz", + "integrity": "sha512-Q4tcqSqFGQtOgCtRyIp1D80Nv2if13Q2pfbnrOlaT/mix90mLcZGML9jKVnT1jGSy5GMYudU1HsS7cx53kxb0g==", "license": "MIT", "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.62.0", - "@mariozechner/pi-ai": "^0.62.0", - "@mariozechner/pi-tui": "^0.62.0", + "@mariozechner/pi-agent-core": "^0.64.0", + "@mariozechner/pi-ai": "^0.64.0", + "@mariozechner/pi-tui": "^0.64.0", "@silvia-odwyer/photon-node": "^0.3.4", + "ajv": "^8.17.1", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", @@ -1543,9 +1545,9 @@ } }, "node_modules/@mariozechner/pi-tui": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.62.0.tgz", - "integrity": "sha512-/At11PPe8l319MnUoK4wN5L/uVCU6bDdiIUzH8Ez0stOkjSF6isRXScZ+RMM+6iCKsD4muBTX8Cmcif+3/UWHA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.64.0.tgz", + "integrity": "sha512-W1qLry9MAuN/V3YJmMv/BJa0VaYv721NkXPg/DGItdqWxuDc+1VdNbyAnRwxblNkIpXVUWL26x64BlyFXpxmkg==", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", diff --git a/package.json b/package.json index 25dd098..63ba846 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ }, "dependencies": { "@companion-ai/alpha-hub": "^0.1.2", - "@mariozechner/pi-ai": "^0.62.0", - "@mariozechner/pi-coding-agent": "^0.62.0", + "@mariozechner/pi-ai": "^0.64.0", + "@mariozechner/pi-coding-agent": "^0.64.0", "@sinclair/typebox": "^0.34.48", "dotenv": "^17.3.1" }, diff --git a/src/cli.ts b/src/cli.ts index e6fddfd..fd55a8c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,6 +18,7 @@ import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynman import { launchPiChat } from "./pi/launch.js"; import { CORE_PACKAGE_SOURCES, getOptionalPackagePresetSources, listOptionalPackagePresets } from "./pi/package-presets.js"; import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js"; +import { applyFeynmanPackageManagerEnv } from "./pi/runtime.js"; import { authenticateModelProvider, getCurrentModelSpec, @@ -154,6 +155,7 @@ async function handleModelCommand(subcommand: string | undefined, args: string[] } async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string, source?: string): Promise { + applyFeynmanPackageManagerEnv(feynmanAgentDir); const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir); const packageManager = new DefaultPackageManager({ cwd: workingDir, @@ -177,6 +179,7 @@ async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string, } async function handlePackagesCommand(subcommand: string | undefined, args: string[], workingDir: string, feynmanAgentDir: string): Promise { + applyFeynmanPackageManagerEnv(feynmanAgentDir); const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir); const configuredSources = new Set( settingsManager diff --git a/src/model/registry.ts b/src/model/registry.ts index 849696c..65ef860 100644 --- a/src/model/registry.ts +++ b/src/model/registry.ts @@ -7,6 +7,5 @@ export function getModelsJsonPath(authPath: string): string { } export function createModelRegistry(authPath: string): ModelRegistry { - return new ModelRegistry(AuthStorage.create(authPath), getModelsJsonPath(authPath)); + return ModelRegistry.create(AuthStorage.create(authPath), getModelsJsonPath(authPath)); } - diff --git a/src/pi/runtime.ts b/src/pi/runtime.ts index 4125e13..9ab3e54 100644 --- a/src/pi/runtime.ts +++ b/src/pi/runtime.ts @@ -20,6 +20,18 @@ export type PiRuntimeOptions = { initialPrompt?: string; }; +export function getFeynmanNpmPrefixPath(feynmanAgentDir: string): string { + return resolve(dirname(feynmanAgentDir), "npm-global"); +} + +export function applyFeynmanPackageManagerEnv(feynmanAgentDir: string): string { + const feynmanNpmPrefixPath = getFeynmanNpmPrefixPath(feynmanAgentDir); + process.env.FEYNMAN_NPM_PREFIX = feynmanNpmPrefixPath; + process.env.NPM_CONFIG_PREFIX = feynmanNpmPrefixPath; + process.env.npm_config_prefix = feynmanNpmPrefixPath; + return feynmanNpmPrefixPath; +} + export function resolvePiPaths(appRoot: string) { return { piPackageRoot: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"), @@ -83,8 +95,7 @@ export function buildPiArgs(options: PiRuntimeOptions): string[] { export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv { const paths = resolvePiPaths(options.appRoot); - const feynmanHome = dirname(options.feynmanAgentDir); - const feynmanNpmPrefixPath = resolve(feynmanHome, "npm-global"); + const feynmanNpmPrefixPath = getFeynmanNpmPrefixPath(options.feynmanAgentDir); const feynmanNpmBinPath = resolve(feynmanNpmPrefixPath, "bin"); const currentPath = process.env.PATH ?? ""; diff --git a/tests/pi-runtime.test.ts b/tests/pi-runtime.test.ts index 01a72a6..7190677 100644 --- a/tests/pi-runtime.test.ts +++ b/tests/pi-runtime.test.ts @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js"; +import { applyFeynmanPackageManagerEnv, buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js"; test("buildPiArgs includes configured runtime paths and prompt", () => { const args = buildPiArgs({ @@ -70,6 +70,37 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => { } }); +test("applyFeynmanPackageManagerEnv pins npm globals to the Feynman prefix", () => { + const previousFeynmanPrefix = process.env.FEYNMAN_NPM_PREFIX; + const previousUppercasePrefix = process.env.NPM_CONFIG_PREFIX; + const previousLowercasePrefix = process.env.npm_config_prefix; + + try { + const prefix = applyFeynmanPackageManagerEnv("/home/.feynman/agent"); + + assert.equal(prefix, "/home/.feynman/npm-global"); + assert.equal(process.env.FEYNMAN_NPM_PREFIX, "/home/.feynman/npm-global"); + assert.equal(process.env.NPM_CONFIG_PREFIX, "/home/.feynman/npm-global"); + assert.equal(process.env.npm_config_prefix, "/home/.feynman/npm-global"); + } finally { + if (previousFeynmanPrefix === undefined) { + delete process.env.FEYNMAN_NPM_PREFIX; + } else { + process.env.FEYNMAN_NPM_PREFIX = previousFeynmanPrefix; + } + if (previousUppercasePrefix === undefined) { + delete process.env.NPM_CONFIG_PREFIX; + } else { + process.env.NPM_CONFIG_PREFIX = previousUppercasePrefix; + } + if (previousLowercasePrefix === undefined) { + delete process.env.npm_config_prefix; + } else { + process.env.npm_config_prefix = previousLowercasePrefix; + } + } +}); + test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => { const paths = resolvePiPaths("/repo/feynman");