import { spawnSync } from "child_process"; import { existsSync, readFileSync, realpathSync } from "fs"; import { homedir } from "os"; import { dirname, join, resolve, sep } from "path"; import { fileURLToPath } from "url"; // ============================================================================= // Package Detection // ============================================================================= const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Detect if we're running as a Bun compiled binary. * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) */ export const isBunBinary = import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); /** Detect if Bun is the runtime (compiled binary or bun run) */ export const isBunRuntime = !!process.versions.bun; // ============================================================================= // Install Method Detection // ============================================================================= export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown"; export interface SelfUpdateCommand { command: string; args: string[]; display: string; } export function detectInstallMethod(): InstallMethod { if (isBunBinary) { return "bun-binary"; } const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/"); if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) { return "pnpm"; } if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) { return "yarn"; } if (isBunRuntime) { return "bun"; } if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) { return "npm"; } return "unknown"; } function getSelfUpdateCommandForMethod(method: InstallMethod, packageName: string): SelfUpdateCommand | undefined { switch (method) { case "bun-binary": return undefined; case "pnpm": return { command: "pnpm", args: ["install", "-g", packageName], display: `pnpm install -g ${packageName}`, }; case "yarn": return { command: "yarn", args: ["global", "add", packageName], display: `yarn global add ${packageName}`, }; case "bun": return { command: "bun", args: ["install", "-g", packageName], display: `bun install -g ${packageName}`, }; case "npm": return { command: "npm", args: ["install", "-g", packageName], display: `npm install -g ${packageName}`, }; case "unknown": return undefined; } } function readCommandOutput(command: string, args: string[]): string | undefined { const result = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000, // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them; // command and args are fixed literals from getGlobalPackageRoots(), not user input. shell: process.platform === "win32", }); if (result.status !== 0) return undefined; const stdout = result.stdout.trim(); return stdout || undefined; } function getGlobalPackageRoots(method: InstallMethod): string[] { switch (method) { case "npm": { const root = readCommandOutput("npm", ["root", "-g"]); return root ? [root] : []; } case "pnpm": { const root = readCommandOutput("pnpm", ["root", "-g"]); return root ? [root, dirname(root)] : []; } case "yarn": { const dir = readCommandOutput("yarn", ["global", "dir"]); return dir ? [dir, join(dir, "node_modules")] : []; } case "bun": { const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]); const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; if (bunBin) { roots.push(join(dirname(dirname(bunBin)), "install", "global", "node_modules")); } return roots; } case "bun-binary": case "unknown": return []; } } function normalizeExistingPathForComparison(path: string): string | undefined { const resolvedPath = resolve(path); if (!existsSync(resolvedPath)) { return undefined; } let normalizedPath: string; try { normalizedPath = realpathSync(resolvedPath); } catch { return undefined; } if (process.platform === "win32") { normalizedPath = normalizedPath.toLowerCase(); } return normalizedPath; } function isManagedByGlobalPackageManager(method: InstallMethod): boolean { const packageDir = normalizeExistingPathForComparison(getPackageDir()); if (!packageDir) { return false; } return getGlobalPackageRoots(method).some((root) => { const normalizedRoot = normalizeExistingPathForComparison(root); return ( normalizedRoot !== undefined && (packageDir === normalizedRoot || packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`)) ); }); } export function getSelfUpdateCommand(packageName: string): SelfUpdateCommand | undefined { const method = detectInstallMethod(); const command = getSelfUpdateCommandForMethod(method, packageName); if (!command || !isManagedByGlobalPackageManager(method)) { return undefined; } return command; } export function getSelfUpdateUnavailableInstruction(packageName: string): string { const method = detectInstallMethod(); if (method === "bun-binary") { return `Download from: https://github.com/badlogic/pi-mono/releases/latest`; } if (getSelfUpdateCommandForMethod(method, packageName)) { return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } return `Update ${packageName} using the package manager, wrapper, or source checkout that provides this installation.`; } export function getUpdateInstruction(packageName: string): string { const method = detectInstallMethod(); const command = getSelfUpdateCommandForMethod(method, packageName); if (command) { return `Run: ${command.display}`; } return getSelfUpdateUnavailableInstruction(packageName); } // ============================================================================= // Package Asset Paths (shipped with executable) // ============================================================================= /** * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). * - For Bun binary: returns the directory containing the executable * - For Node.js (dist/): returns __dirname (the dist/ directory) * - For tsx (src/): returns parent directory (the package root) */ export function getPackageDir(): string { // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) const envDir = process.env.PI_PACKAGE_DIR; if (envDir) { if (envDir === "~") return homedir(); if (envDir.startsWith("~/")) return homedir() + envDir.slice(1); return envDir; } if (isBunBinary) { // Bun binary: process.execPath points to the compiled executable return dirname(process.execPath); } // Node.js: walk up from __dirname until we find package.json let dir = __dirname; while (dir !== dirname(dir)) { if (existsSync(join(dir, "package.json"))) { return dir; } dir = dirname(dir); } // Fallback (shouldn't happen) return __dirname; } /** * Get path to built-in themes directory (shipped with package) * - For Bun binary: theme/ next to executable * - For Node.js (dist/): dist/modes/interactive/theme/ * - For tsx (src/): src/modes/interactive/theme/ */ export function getThemesDir(): string { if (isBunBinary) { return join(getPackageDir(), "theme"); } // Theme is in modes/interactive/theme/ relative to src/ or dist/ const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "modes", "interactive", "theme"); } /** * Get path to HTML export template directory (shipped with package) * - For Bun binary: export-html/ next to executable * - For Node.js (dist/): dist/core/export-html/ * - For tsx (src/): src/core/export-html/ */ export function getExportTemplateDir(): string { if (isBunBinary) { return join(getPackageDir(), "export-html"); } const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "core", "export-html"); } /** Get path to package.json */ export function getPackageJsonPath(): string { return join(getPackageDir(), "package.json"); } /** Get path to README.md */ export function getReadmePath(): string { return resolve(join(getPackageDir(), "README.md")); } /** Get path to docs directory */ export function getDocsPath(): string { return resolve(join(getPackageDir(), "docs")); } /** Get path to examples directory */ export function getExamplesPath(): string { return resolve(join(getPackageDir(), "examples")); } /** Get path to CHANGELOG.md */ export function getChangelogPath(): string { return resolve(join(getPackageDir(), "CHANGELOG.md")); } /** * Get path to built-in interactive assets directory. * - For Bun binary: assets/ next to executable * - For Node.js (dist/): dist/modes/interactive/assets/ * - For tsx (src/): src/modes/interactive/assets/ */ export function getInteractiveAssetsDir(): string { if (isBunBinary) { return join(getPackageDir(), "assets"); } const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "modes", "interactive", "assets"); } /** Get path to a bundled interactive asset */ export function getBundledInteractiveAssetPath(name: string): string { return join(getInteractiveAssetsDir(), name); } // ============================================================================= // App Config (from package.json piConfig) // ============================================================================= interface PackageJson { name?: string; version?: string; piConfig?: { name?: string; configDir?: string; }; } const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")) as PackageJson; const piConfigName: string | undefined = pkg.piConfig?.name; export const PACKAGE_NAME: string = pkg.name || "@mariozechner/pi-coding-agent"; export const APP_NAME: string = piConfigName || "pi"; export const APP_TITLE: string = piConfigName ? APP_NAME : "π"; export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi"; export const VERSION: string = pkg.version || "0.0.0"; // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`; export function expandTildePath(path: string): string { if (path === "~") return homedir(); if (path.startsWith("~/")) return homedir() + path.slice(1); return path; } const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/"; /** Get the share viewer URL for a gist ID */ export function getShareViewerUrl(gistId: string): string { const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL; return `${baseUrl}#${gistId}`; } // ============================================================================= // User Config Paths (~/.pi/agent/*) // ============================================================================= /** Get the agent config directory (e.g., ~/.pi/agent/) */ export function getAgentDir(): string { const envDir = process.env[ENV_AGENT_DIR]; if (envDir) { return expandTildePath(envDir); } return join(homedir(), CONFIG_DIR_NAME, "agent"); } /** Get path to user's custom themes directory */ export function getCustomThemesDir(): string { return join(getAgentDir(), "themes"); } /** Get path to models.json */ export function getModelsPath(): string { return join(getAgentDir(), "models.json"); } /** Get path to auth.json */ export function getAuthPath(): string { return join(getAgentDir(), "auth.json"); } /** Get path to settings.json */ export function getSettingsPath(): string { return join(getAgentDir(), "settings.json"); } /** Get path to tools directory */ export function getToolsDir(): string { return join(getAgentDir(), "tools"); } /** Get path to managed binaries directory (fd, rg) */ export function getBinDir(): string { return join(getAgentDir(), "bin"); } /** Get path to prompt templates directory */ export function getPromptsDir(): string { return join(getAgentDir(), "prompts"); } /** Get path to sessions directory */ export function getSessionsDir(): string { return join(getAgentDir(), "sessions"); } /** Get path to debug log file */ export function getDebugLogPath(): string { return join(getAgentDir(), `${APP_NAME}-debug.log`); }