singularity-forge/src/tool-bootstrap.ts
Jeremy McSpadden 2c926c12e3 fix: Phase 1 quick wins — bug fixes, security hardening, and performance
- Fix loadStoredEnvKeys divergent provider lists: add telegram_bot and
  custom-openai to wizard.ts (the canonical copy used by CLI), remove
  dead duplicate from onboarding.ts
- Security: add SAFE_COMMAND_PREFIXES allowlist to resolveConfigValue
  to prevent arbitrary RCE via settings.json shell commands
- Security: add TOFU (Trust On First Use) model for project-local
  extensions — skip untrusted .pi/extensions/ with stderr warning
- Performance: debounce sql.js MemoryStorage persistence (500ms window)
  so rapid mutations coalesce into a single db.export()+writeFileSync
- Fix double lstatSync call in tool-bootstrap.ts isRegularFile
- Add 26 new tests covering all changes
2026-03-16 13:18:02 -05:00

86 lines
2.7 KiB
TypeScript

import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
import { delimiter, join } from "node:path";
type ManagedTool = "fd" | "rg";
interface ToolSpec {
targetName: string;
candidates: string[];
}
const TOOL_SPECS: Record<ManagedTool, ToolSpec> = {
fd: {
targetName: process.platform === "win32" ? "fd.exe" : "fd",
candidates: process.platform === "win32" ? ["fd.exe", "fd", "fdfind.exe", "fdfind"] : ["fd", "fdfind"],
},
rg: {
targetName: process.platform === "win32" ? "rg.exe" : "rg",
candidates: process.platform === "win32" ? ["rg.exe", "rg"] : ["rg"],
},
};
function splitPath(pathValue: string | undefined): string[] {
if (!pathValue) return [];
return pathValue.split(delimiter).map((segment) => segment.trim()).filter(Boolean);
}
function getCandidateNames(name: string): string[] {
if (process.platform !== "win32") return [name];
const lower = name.toLowerCase();
if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat")) return [name];
return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`];
}
function isRegularFile(path: string): boolean {
try {
const stat = lstatSync(path);
return stat.isFile() || stat.isSymbolicLink();
} catch {
return false;
}
}
export function resolveToolFromPath(tool: ManagedTool, pathValue: string | undefined = process.env.PATH): string | null {
const spec = TOOL_SPECS[tool];
for (const dir of splitPath(pathValue)) {
for (const candidate of spec.candidates) {
for (const name of getCandidateNames(candidate)) {
const fullPath = join(dir, name);
if (existsSync(fullPath) && isRegularFile(fullPath)) {
return fullPath;
}
}
}
}
return null;
}
function provisionTool(targetDir: string, tool: ManagedTool, sourcePath: string): string {
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
if (existsSync(targetPath)) return targetPath;
mkdirSync(targetDir, { recursive: true });
try {
symlinkSync(sourcePath, targetPath);
} catch {
rmSync(targetPath, { force: true });
copyFileSync(sourcePath, targetPath);
chmodSync(targetPath, 0o755);
}
return targetPath;
}
export function ensureManagedTools(targetDir: string, pathValue: string | undefined = process.env.PATH): string[] {
const provisioned: string[] = [];
for (const tool of Object.keys(TOOL_SPECS) as ManagedTool[]) {
if (existsSync(join(targetDir, TOOL_SPECS[tool].targetName))) continue;
const sourcePath = resolveToolFromPath(tool, pathValue);
if (!sourcePath) continue;
provisioned.push(provisionTool(targetDir, tool, sourcePath));
}
return provisioned;
}