Files
pi-agent-integrated/scripts/profile.mjs
T

188 lines
6.6 KiB
JavaScript

import { spawnSync } from "node:child_process";
import { copyFileSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
import { platform } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";
import { loadEnvFile } from "node:process";
import { fileURLToPath } from "node:url";
export const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
const projectEnvPath = join(rootDir, ".env");
if (existsSync(projectEnvPath)) loadEnvFile(projectEnvPath);
export const configDir = join(rootDir, "config");
export const resourcesDir = join(rootDir, "resources");
export const dataDir = resolve(process.env.PI_AGENT_DATA_DIR ?? join(rootDir, "data"));
export const agentDir = join(dataDir, "agent");
export const homeDir = join(dataDir, "home");
export const memoryDir = join(agentDir, "memory");
export const skillsHomeDir = join(dataDir, "skills-home");
export const stateDir = join(dataDir, "state");
const managedDirectories = [
agentDir,
homeDir,
join(agentDir, "sessions"),
join(agentDir, "npm"),
join(agentDir, "git"),
join(agentDir, "bin"),
join(agentDir, "extensions", "subagent"),
memoryDir,
join(memoryDir, "daily"),
join(memoryDir, "recovery"),
join(agentDir, "tools"),
join(agentDir, "tmp"),
join(dataDir, "logs"),
join(dataDir, "cache", "npm"),
join(dataDir, "mcp", "playwright", "output"),
join(dataDir, "workspaces", "default"),
join(skillsHomeDir, ".agents", "skills"),
join(skillsHomeDir, ".pi", "agent", "skills"),
stateDir,
join(resourcesDir, "skills"),
join(resourcesDir, "extensions"),
join(resourcesDir, "prompts"),
join(resourcesDir, "themes"),
];
const mcpDefaultsFile = platform() === "darwin" ? "mcp.macos.default.json" : "mcp.default.json";
const seedFiles = [
[join(configDir, "settings.default.json"), join(agentDir, "settings.json")],
[join(configDir, mcpDefaultsFile), join(agentDir, "mcp.json")],
[join(configDir, "models.example.json"), join(agentDir, "models.json")],
[join(configDir, "subagents.default.json"), join(agentDir, "extensions", "subagent", "config.json")],
[join(configDir, "subagent-tool-description.default.md"), join(agentDir, "subagent-tool-description.md")],
];
const persistedWindowsEnvironmentKeys = ["SEARXNG_TOKEN", "SEARXNG_URL"];
const piWebBinDir = join(rootDir, "pi-web", "node_modules", ".bin");
const piCodingAgentPackageDir = join(
rootDir,
"pi-web",
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
const managedPeerPackageDir = join(
agentDir,
"npm",
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
function ensurePiSubagentRuntime() {
// pi-subagents resolves the host CLI through its optional coding-agent peer.
// The managed profile is installed separately from pi-web, so expose the
// existing local package instead of installing a second coding-agent copy.
if (!existsSync(piCodingAgentPackageDir)) return;
mkdirSync(dirname(managedPeerPackageDir), { recursive: true });
if (existsSync(managedPeerPackageDir)) return;
try {
if (lstatSync(managedPeerPackageDir)) return;
} catch {
// The peer link is absent; create it below.
}
try {
symlinkSync(
piCodingAgentPackageDir,
managedPeerPackageDir,
platform() === "win32" ? "junction" : "dir",
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[subagents] could not link the local Pi CLI package: ${message}`);
}
}
function prependPiWebBinToPath(baseEnv) {
if (!existsSync(piWebBinDir)) return baseEnv.PATH ?? baseEnv.Path;
const pathKey = baseEnv.PATH !== undefined || baseEnv.Path === undefined ? "PATH" : "Path";
const currentPath = baseEnv[pathKey] ?? "";
const entries = currentPath.split(delimiter).filter(Boolean);
if (!entries.includes(piWebBinDir)) entries.unshift(piWebBinDir);
return entries.join(delimiter);
}
function readPersistedWindowsEnvironment(baseEnv) {
if (platform() !== "win32") return {};
const missingKeys = persistedWindowsEnvironmentKeys.filter((key) => !baseEnv[key]?.trim());
if (missingKeys.length === 0) return {};
const keyLiterals = missingKeys.map((key) => `'${key.replaceAll("'", "''")}'`).join(", ");
const script = `
$result = @{}
foreach ($name in @(${keyLiterals})) {
$value = [Environment]::GetEnvironmentVariable($name, 'User')
if ([string]::IsNullOrWhiteSpace($value)) {
$value = [Environment]::GetEnvironmentVariable($name, 'Machine')
}
if (-not [string]::IsNullOrWhiteSpace($value)) {
$result[$name] = $value
}
}
ConvertTo-Json -Compress -InputObject $result
`;
const result = spawnSync(
"powershell.exe",
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
{ encoding: "utf8", windowsHide: true },
);
if (result.status !== 0 || !result.stdout.trim()) return {};
try {
const values = JSON.parse(result.stdout);
return values && typeof values === "object" && !Array.isArray(values) ? values : {};
} catch {
return {};
}
}
export function ensureProfile({ quiet = false } = {}) {
for (const path of managedDirectories) mkdirSync(path, { recursive: true });
const created = [];
for (const [source, destination] of seedFiles) {
if (existsSync(destination)) continue;
copyFileSync(source, destination);
created.push(destination);
}
if (!quiet) {
console.log(`Managed Pi profile: ${agentDir}`);
if (created.length > 0) console.log(`Initialized ${created.length} profile file(s).`);
}
return { rootDir, configDir, resourcesDir, dataDir, agentDir, homeDir, skillsHomeDir, stateDir, created };
}
export function managedEnvironment(baseEnv = process.env) {
ensureProfile({ quiet: true });
ensurePiSubagentRuntime();
const persistedEnvironment = readPersistedWindowsEnvironment(baseEnv);
return {
...baseEnv,
...persistedEnvironment,
PATH: prependPiWebBinToPath({ ...baseEnv, ...persistedEnvironment }),
PI_AGENT_MANAGED_RUNTIME: "1",
PI_AGENT_APP_ROOT: rootDir,
PI_AGENT_DATA_DIR: dataDir,
PI_AGENT_RESOURCES_DIR: resourcesDir,
PI_AGENT_SKILLS_HOME: skillsHomeDir,
PI_CODING_AGENT_DIR: agentDir,
HOME: homeDir,
USERPROFILE: homeDir,
PI_MEMORY_DIR: baseEnv.PI_MEMORY_DIR?.trim() || memoryDir,
PI_MEMORY_SNAPSHOT: baseEnv.PI_MEMORY_SNAPSHOT?.trim() || "stable",
PI_MEMORY_NO_SEARCH: baseEnv.PI_MEMORY_NO_SEARCH?.trim() || "1",
PI_MEMORY_QMD_UPDATE: baseEnv.PI_MEMORY_QMD_UPDATE?.trim() || "off",
PI_RETRY_STALL_TIMEOUT_MS: baseEnv.PI_RETRY_STALL_TIMEOUT_MS?.trim() || "180000",
npm_config_cache: baseEnv.npm_config_cache?.trim() || join(dataDir, "cache", "npm"),
XDG_STATE_HOME: stateDir,
};
}