Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
657 lines
23 KiB
TypeScript
657 lines
23 KiB
TypeScript
import { accessSync, constants, existsSync, readFileSync, realpathSync } from "fs";
|
|
import { createRequire } from "module";
|
|
import { homedir } from "os";
|
|
import { basename, dirname, join, resolve, sep, win32 } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { spawnProcessSync } from "./utils/child-process.ts";
|
|
import { normalizePath } from "./utils/paths.ts";
|
|
import { stripBom } from "./utils/text.ts";
|
|
|
|
// =============================================================================
|
|
// 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;
|
|
|
|
/** Detect the esbuild-bundled Node.js distribution. */
|
|
declare const PI_BUNDLED_NODE: boolean;
|
|
export const isBundledNode = typeof PI_BUNDLED_NODE !== "undefined" && PI_BUNDLED_NODE;
|
|
|
|
// =============================================================================
|
|
// Install Method Detection
|
|
// =============================================================================
|
|
|
|
export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
|
|
|
interface SelfUpdateCommandStep {
|
|
command: string;
|
|
args: string[];
|
|
display: string;
|
|
}
|
|
|
|
export interface SelfUpdateCommand extends SelfUpdateCommandStep {
|
|
steps?: SelfUpdateCommandStep[];
|
|
}
|
|
|
|
export type SelfUpdatePackageTarget = string | { packageName: string; installSpec?: string };
|
|
|
|
function normalizeSelfUpdatePackageTarget(target: SelfUpdatePackageTarget): {
|
|
packageName: string;
|
|
installSpec: string;
|
|
} {
|
|
if (typeof target === "string") {
|
|
return { packageName: target, installSpec: target };
|
|
}
|
|
return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName };
|
|
}
|
|
|
|
function makeSelfUpdateCommand(
|
|
installStep: SelfUpdateCommandStep,
|
|
uninstallStep?: SelfUpdateCommandStep,
|
|
): SelfUpdateCommand {
|
|
if (!uninstallStep) return installStep;
|
|
return {
|
|
...installStep,
|
|
display: `${uninstallStep.display} && ${installStep.display}`,
|
|
steps: [uninstallStep, installStep],
|
|
};
|
|
}
|
|
|
|
function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep {
|
|
return {
|
|
command,
|
|
args,
|
|
display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "),
|
|
};
|
|
}
|
|
|
|
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 || resolvedPath.includes("/install/global/node_modules/")) {
|
|
return "bun";
|
|
}
|
|
if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) {
|
|
return "npm";
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
function getInferredNpmInstall(): { root: string; prefix: string } | undefined {
|
|
const packageDir = getPackageDir();
|
|
const path = process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname };
|
|
const parent = path.dirname(packageDir);
|
|
let root: string | undefined;
|
|
if (path.basename(parent).startsWith("@") && path.basename(path.dirname(parent)) === "node_modules") {
|
|
root = path.dirname(parent);
|
|
} else if (path.basename(parent) === "node_modules") {
|
|
root = parent;
|
|
}
|
|
if (!root) return undefined;
|
|
const rootParent = path.dirname(root);
|
|
if (path.basename(rootParent) === "lib") return { root, prefix: path.dirname(rootParent) };
|
|
// Windows global npm prefixes use `<prefix>\\node_modules`, which is
|
|
// indistinguishable from local project installs by path shape alone. Do not
|
|
// infer unsupported Windows custom prefixes without `npm root -g` evidence.
|
|
return undefined;
|
|
}
|
|
|
|
function getSelfUpdateCommandForMethod(
|
|
method: InstallMethod,
|
|
installedPackageName: string,
|
|
updatePackageTarget: SelfUpdatePackageTarget = installedPackageName,
|
|
npmCommand?: string[],
|
|
): SelfUpdateCommand | undefined {
|
|
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
|
switch (method) {
|
|
case "bun-binary":
|
|
return undefined;
|
|
case "pnpm": {
|
|
const match = readCommandOutput("pnpm", ["root", "-g"])
|
|
? undefined
|
|
: /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir());
|
|
const binDirArgs = match
|
|
? [`--config.global-bin-dir=${process.env.PNPM_HOME || dirname(dirname(match[1]))}`]
|
|
: [];
|
|
return makeSelfUpdateCommand(
|
|
makeSelfUpdateCommandStep("pnpm", [
|
|
"install",
|
|
"-g",
|
|
"--ignore-scripts",
|
|
"--config.minimumReleaseAge=0",
|
|
...binDirArgs,
|
|
target.installSpec,
|
|
]),
|
|
target.packageName === installedPackageName
|
|
? undefined
|
|
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]),
|
|
);
|
|
}
|
|
case "yarn":
|
|
return makeSelfUpdateCommand(
|
|
makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]),
|
|
target.packageName === installedPackageName
|
|
? undefined
|
|
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]),
|
|
);
|
|
case "bun":
|
|
return makeSelfUpdateCommand(
|
|
makeSelfUpdateCommandStep("bun", [
|
|
"install",
|
|
"-g",
|
|
"--ignore-scripts",
|
|
"--minimum-release-age=0",
|
|
target.installSpec,
|
|
]),
|
|
target.packageName === installedPackageName
|
|
? undefined
|
|
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
|
|
);
|
|
case "npm": {
|
|
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
|
const inferred = npmCommand?.length ? undefined : getInferredNpmInstall();
|
|
const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])];
|
|
// pi.dev advertises releases immediately, so a configured npm age gate would
|
|
// block the update. npm has no per-package age gate, so this also lets new
|
|
// transitive dependency releases through. Managed installs avoid this.
|
|
const installStep = makeSelfUpdateCommandStep(command, [
|
|
...prefixArgs,
|
|
"install",
|
|
"-g",
|
|
"--ignore-scripts",
|
|
"--min-release-age=0",
|
|
target.installSpec,
|
|
]);
|
|
const uninstallStep =
|
|
target.packageName === installedPackageName
|
|
? undefined
|
|
: makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
|
|
return makeSelfUpdateCommand(installStep, uninstallStep);
|
|
}
|
|
case "unknown":
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function readCommandOutput(
|
|
command: string,
|
|
args: string[],
|
|
options: { requireSuccess?: boolean } = {},
|
|
): string | undefined {
|
|
const result = spawnProcessSync(command, args, {
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (result.status === 0) return result.stdout.trim() || undefined;
|
|
if (options.requireSuccess) {
|
|
const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`;
|
|
throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmCommand?: string[]): string[] {
|
|
switch (method) {
|
|
case "npm": {
|
|
const configured = !!npmCommand?.length;
|
|
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
|
if (configured && command === "bun") {
|
|
const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], {
|
|
requireSuccess: true,
|
|
});
|
|
const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
|
|
if (bunBin) {
|
|
roots.push(join(dirname(bunBin), "install", "global", "node_modules"));
|
|
}
|
|
return roots;
|
|
}
|
|
const root = readCommandOutput(command, [...npmArgs, "root", "-g"], {
|
|
requireSuccess: configured,
|
|
});
|
|
const inferred = configured ? undefined : getInferredNpmInstall();
|
|
return [root, inferred?.root].filter((x): x is string => !!x);
|
|
}
|
|
case "pnpm": {
|
|
const root = readCommandOutput("pnpm", ["root", "-g"]);
|
|
if (root) return [root, dirname(root)];
|
|
const match = /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir());
|
|
return match ? [match[1]] : [];
|
|
}
|
|
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(bunBin), "install", "global", "node_modules"));
|
|
}
|
|
return roots;
|
|
}
|
|
case "bun-binary":
|
|
case "unknown":
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined {
|
|
const resolvedPath = resolve(path);
|
|
if (!existsSync(resolvedPath)) {
|
|
return undefined;
|
|
}
|
|
let normalizedPath = resolvedPath;
|
|
if (resolveSymlinks) {
|
|
try {
|
|
normalizedPath = realpathSync(resolvedPath);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
if (process.platform === "win32") {
|
|
normalizedPath = normalizedPath.toLowerCase();
|
|
}
|
|
return normalizedPath;
|
|
}
|
|
|
|
function getPathComparisonCandidates(path: string): string[] {
|
|
return Array.from(
|
|
new Set(
|
|
[normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter(
|
|
(candidate): candidate is string => !!candidate,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function getEntrypointPackageDir(): string | undefined {
|
|
const entrypoint = process.argv[1];
|
|
if (!entrypoint) return undefined;
|
|
let dir = dirname(entrypoint);
|
|
while (dir !== dirname(dir)) {
|
|
if (existsSync(join(dir, "package.json"))) {
|
|
return dir;
|
|
}
|
|
dir = dirname(dir);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isSelfUpdatePathWritable(): boolean {
|
|
const packageDir = getPackageDir();
|
|
try {
|
|
accessSync(packageDir, constants.W_OK);
|
|
accessSync(dirname(packageDir), constants.W_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean {
|
|
const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!dir);
|
|
const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir));
|
|
return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
|
return getPathComparisonCandidates(root).some((normalizedRoot) => {
|
|
const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
|
return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix));
|
|
});
|
|
});
|
|
}
|
|
|
|
export function getSelfUpdateCommand(
|
|
packageName: string,
|
|
npmCommand?: string[],
|
|
updatePackageTarget: SelfUpdatePackageTarget = packageName,
|
|
): SelfUpdateCommand | undefined {
|
|
const method = detectInstallMethod();
|
|
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand);
|
|
if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) {
|
|
return undefined;
|
|
}
|
|
return command;
|
|
}
|
|
|
|
export function getSelfUpdateUnavailableInstruction(
|
|
packageName: string,
|
|
npmCommand?: string[],
|
|
updatePackageTarget: SelfUpdatePackageTarget = packageName,
|
|
): string {
|
|
const method = detectInstallMethod();
|
|
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
|
if (method === "bun-binary") {
|
|
return `Download from: https://github.com/earendil-works/pi/releases/latest`;
|
|
}
|
|
const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand);
|
|
if (command) {
|
|
if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
|
|
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
|
|
}
|
|
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 ${target.installSpec} 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: returns the package root containing package.json
|
|
* - Ignores Bun binary metadata copied into dist/ when the package root is available
|
|
*/
|
|
export function findNodePackageDir(startDir: string): string {
|
|
let dir = startDir;
|
|
while (dir !== dirname(dir)) {
|
|
if (existsSync(join(dir, "package.json"))) {
|
|
const parent = dirname(dir);
|
|
// build:binary places Bun's metadata inside dist/. Node still needs the
|
|
// package root so its dist-relative asset paths do not become dist/dist/.
|
|
if (basename(dir) === "dist" && existsSync(join(parent, "package.json"))) {
|
|
return parent;
|
|
}
|
|
return dir;
|
|
}
|
|
dir = dirname(dir);
|
|
}
|
|
return startDir;
|
|
}
|
|
|
|
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) {
|
|
return normalizePath(envDir);
|
|
}
|
|
|
|
if (isBunBinary) {
|
|
// Bun binary: process.execPath points to the compiled executable
|
|
return dirname(process.execPath);
|
|
}
|
|
return findNodePackageDir(__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 source (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 source (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 source (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);
|
|
}
|
|
|
|
let quickJSWasmPath: string | undefined;
|
|
|
|
/** Called by the Bun entry with the path of the QuickJS wasm file embedded in the compiled executable. */
|
|
export function setEmbeddedQuickJSWasmPath(path: string): void {
|
|
quickJSWasmPath = path;
|
|
}
|
|
|
|
/**
|
|
* Get path to `quickjs-wasi/quickjs.wasm`, the VM that runs codemode scripts. Resolved once so the
|
|
* compiled module cached per path keeps working after an update removes this install (#10439).
|
|
*/
|
|
export function getQuickJSWasmPath(): string {
|
|
quickJSWasmPath ??= createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
|
|
return quickJSWasmPath;
|
|
}
|
|
|
|
/** Resolve the codemode worker entry for a release runtime. */
|
|
export function resolveCodemodeWorkerSpecifier(
|
|
runtime: "bun-binary" | "bundled-node" | "unbundled",
|
|
moduleUrl: string,
|
|
): string | URL | undefined {
|
|
// Bun embeds explicit source entrypoints, but on Windows Bun 1.3 cannot map an absolute
|
|
// B:\~BUN URL back to one. A relative string with the original source extension works on
|
|
// every Bun platform.
|
|
if (runtime === "bun-binary") return "./src/extensions/codemode/worker.ts";
|
|
if (runtime === "bundled-node") return new URL("./codemode-worker.js", moduleUrl);
|
|
return undefined;
|
|
}
|
|
|
|
let codemodeWorkerDataUrl: URL | undefined;
|
|
|
|
/**
|
|
* Get the codemode worker entry, or undefined to use the worker that ships next to pi-codemode.
|
|
* The Bun and Node release builds both pass the worker as an extra entrypoint.
|
|
*/
|
|
export function getCodemodeWorkerSpecifier(): string | URL | undefined {
|
|
const runtime = isBunBinary ? "bun-binary" : isBundledNode ? "bundled-node" : "unbundled";
|
|
const specifier = resolveCodemodeWorkerSpecifier(runtime, import.meta.url);
|
|
if (runtime !== "bundled-node" || !(specifier instanceof URL)) return specifier;
|
|
// Spawn workers from an in-memory copy. An update replaces or deletes the file while this
|
|
// process keeps running (#10439). The bundle build keeps the worker free of relative imports
|
|
// and import.meta, so it runs from a data: URL.
|
|
codemodeWorkerDataUrl ??= new URL(`data:text/javascript;base64,${readFileSync(specifier).toString("base64")}`);
|
|
return codemodeWorkerDataUrl;
|
|
}
|
|
|
|
export type InstallChange = { kind: "updated"; version: string } | { kind: "removed" };
|
|
|
|
/**
|
|
* Detect that the package this process runs from changed on disk, for example after `pi update`
|
|
* in another terminal. Code loaded on demand can then be missing or from another version.
|
|
*
|
|
* Checks the package.json read at startup. Resolving it again would walk up past a deleted install
|
|
* and could find an unrelated package.json, such as one in the home directory.
|
|
*/
|
|
export function detectInstallChange(packageJsonPath = startupPackageJsonPath): InstallChange | undefined {
|
|
// The Bun binary embeds its code, so replacing the executable does not affect this process.
|
|
if (isBunBinary || !packageJsonPath) return undefined;
|
|
let installed: PackageJson;
|
|
try {
|
|
installed = JSON.parse(stripBom(readFileSync(packageJsonPath, "utf-8"))) as PackageJson;
|
|
} catch (error) {
|
|
return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "removed" } : undefined;
|
|
}
|
|
return installed.version && installed.version !== VERSION
|
|
? { kind: "updated", version: installed.version }
|
|
: undefined;
|
|
}
|
|
|
|
// =============================================================================
|
|
// App Config (from package.json piConfig)
|
|
// =============================================================================
|
|
|
|
interface PackageJson {
|
|
name?: string;
|
|
version?: string;
|
|
piConfig?: {
|
|
name?: string;
|
|
configDir?: string;
|
|
};
|
|
}
|
|
|
|
let pkg: PackageJson = {};
|
|
/** The package.json this process started from, if one existed. */
|
|
let startupPackageJsonPath: string | undefined;
|
|
try {
|
|
const packageJsonPath = getPackageJsonPath();
|
|
pkg = JSON.parse(stripBom(readFileSync(packageJsonPath, "utf-8"))) as PackageJson;
|
|
startupPackageJsonPath = packageJsonPath;
|
|
} catch (e: unknown) {
|
|
const err = e as NodeJS.ErrnoException;
|
|
if (err.code !== "ENOENT") throw e;
|
|
}
|
|
|
|
const piConfigName: string | undefined = pkg.piConfig?.name;
|
|
export const PACKAGE_NAME: string = pkg.name || "@earendil-works/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 {
|
|
return normalizePath(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`);
|
|
}
|