@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
190 lines (189 loc) • 8.4 kB
JavaScript
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);
}