UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

190 lines (189 loc) • 8.4 kB
import { execFileSync } from "node:child_process"; import * as fs from "node:fs"; import { homedir } from "node:os"; import * as path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; export const CODE_PACKAGE_HINT = "phren code needs @phren/code: run phren modules enable code"; let loaded; let resolvedFrom; /** Where the loaded copy came from: an explicit directory, a resolver step or * the bare specifier "@phren/code". Undefined until a load succeeds. */ export function loadedFrom() { return resolvedFrom; } /** * The Hook bundle runs from `<bridge>/versions/<v>` with no node_modules, so a * bare import fails there and the npm global root is invisible when the * daemon's PATH has no npm (mise shims under systemd). Resolve a local copy * before the bare import: an explicit directory, the bridge install, the * store's runtime packages, then a built workspace sibling. */ function bridgeRoot() { return process.env.PHREN_BRIDGE_HOME || path.join(homedir(), ".local/share/phren/bridge"); } /** The npm shims and Homebrew bin a service manager's PATH usually omits. */ const EXTRA_PATH = [path.join(homedir(), ".local/share/mise/shims"), "/opt/homebrew/bin"]; function npmPath() { const entries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); for (const entry of EXTRA_PATH) if (!entries.includes(entry) && fs.existsSync(entry)) entries.push(entry); return entries.join(path.delimiter); } /** Run npm. On Windows npm is `npm.cmd`, which only a shell can start (a * plain spawn of it fails with EINVAL), and the shell needs each argument quoted. */ function npm(args, options) { const env = { ...process.env, PATH: npmPath() }; if (process.platform !== "win32") return String(execFileSync("npm", args, { ...options, env }) ?? ""); return String(execFileSync("npm.cmd", args.map(arg => `"${arg}"`), { ...options, env, shell: true }) ?? ""); } /** The entry file an installed package declares, without executing anything. */ function packageEntry(directory) { const manifest = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8")); const exported = typeof manifest.exports === "object" && manifest.exports !== null && "." in manifest.exports ? manifest.exports["."] : manifest.exports; const nested = typeof exported === "object" && exported !== null ? exported.default ?? exported.import : exported; for (const candidate of [nested, manifest.module, manifest.main, "index.js", "index.mjs"]) { if (typeof candidate === "string") return path.join(directory, candidate); } throw new Error(`@phren/code has no entry point in ${directory}.`); } async function importDirectory(directory) { return await import(pathToFileURL(packageEntry(directory)).href); } function globalPackageDirectory() { try { const root = npm(["root", "-g"], { encoding: "utf8", timeout: 10_000 }).trim(); return root ? path.join(root, "@phren", "code") : undefined; } catch { return undefined; } } /** Resolution order for the optional package. First success wins and is remembered. */ export async function loadCodePackage(store) { if (loaded) return loaded; const candidates = []; if (process.env.PHREN_CODE_PACKAGE) candidates.push(process.env.PHREN_CODE_PACKAGE); candidates.push(path.join(bridgeRoot(), "node_modules", "@phren", "code")); const root = store || process.env.PHREN_PATH; if (root) candidates.push(path.join(root, ".runtime", "packages", "node_modules", "@phren", "code")); const workspace = workspaceCodeDirectory(); if (workspace && builtEntryExists(workspace)) candidates.push(workspace); for (const directory of candidates) { if (!fs.existsSync(path.join(directory, "package.json"))) continue; try { loaded = await importDirectory(directory); resolvedFrom = directory; return loaded; } catch { /* try the next candidate */ } } try { loaded = await import("@phren/code"); resolvedFrom = "@phren/code"; return loaded; } catch (error) { if (error.code !== "ERR_MODULE_NOT_FOUND") throw error; } const global = globalPackageDirectory(); if (global && fs.existsSync(path.join(global, "package.json"))) { try { loaded = await importDirectory(global); resolvedFrom = global; return loaded; } catch { /* not installed after all */ } } return undefined; } export async function requireCodePackage(store) { const code = await loadCodePackage(store); if (!code) throw new Error(codePackageHint(store)); return code; } /** This repository checked out as a workspace, if its packages/code is present. */ function workspaceCodeDirectory() { let directory = path.dirname(fileURLToPath(import.meta.url)); for (let depth = 0; depth < 8; depth++) { const candidate = path.join(directory, "packages", "code"); if (fs.existsSync(path.join(candidate, "package.json"))) return candidate; const parent = path.dirname(directory); if (parent === directory) break; directory = parent; } return undefined; } function builtEntryExists(directory) { try { return fs.existsSync(packageEntry(directory)); } catch { return false; } } export function codePackageHint(store) { const root = store || process.env.PHREN_PATH; const linked = root && path.join(root, ".runtime", "packages", "node_modules", "@phren", "code"); const workspace = workspaceCodeDirectory(); let workspaceLink; try { if (linked && fs.lstatSync(linked).isSymbolicLink()) workspaceLink = linked; } catch { /* no store link */ } for (const directory of [workspace, workspaceLink]) { if (!directory || !fs.existsSync(path.join(directory, "package.json")) || builtEntryExists(directory)) continue; return `${CODE_PACKAGE_HINT}. @phren/code at ${directory} has no built entry. From the source checkout root run: pnpm --filter @phren/code build`; } return `${CODE_PACKAGE_HINT}. For a source checkout, from its root run pnpm --filter @phren/code build, then node packages/cli/dist/index.js modules enable code and restart the Hook.`; } /** * Make @phren/code available to this store. A workspace checkout links its own * package; otherwise npm installs into the store's runtime packages, which the * Hook can resolve without a global npm. */ export async function installCodePackage(store) { const existing = await loadCodePackage(store); const workspace = workspaceCodeDirectory(); // The installed Hook bundle cannot find the workspace sibling after relocation. if (existing && resolvedFrom !== workspace) return existing; const packages = path.join(store, ".runtime", "packages"); const linked = path.join(packages, "node_modules", "@phren", "code"); if (workspace) { fs.mkdirSync(path.dirname(linked), { recursive: true }); fs.rmSync(linked, { recursive: true, force: true }); fs.symlinkSync(workspace, linked, "junction"); const code = existing ?? await loadCodePackage(store); if (!code) throw new Error(`${codePackageHint(store)}\nLinked ${workspace}, but it could not be loaded.`); console.log(`@phren/code linked from ${workspace} into ${linked}.`); return code; } try { npm(["install", "--prefix", packages, "@phren/code"], { stdio: "inherit", timeout: 120_000 }); } catch { throw new Error(`${CODE_PACKAGE_HINT}\nInstallation failed. Run: npm install --prefix ${packages} @phren/code`); } const installed = await loadCodePackage(store); if (!installed) throw new Error(`${CODE_PACKAGE_HINT}\nRun: npm install --prefix ${packages} @phren/code`); console.log(`@phren/code installed into ${packages}.`); return installed; } export function copyCodeSkill(store, code) { const destination = path.join(store, "global", "skills", "code", "SKILL.md"); fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.copyFileSync(code.codeSkill, destination); }