This commit captures uncommitted modifications that accumulated in the working tree across multiple in-progress workstreams. It is a snapshot to clear the deck before sf v3 work begins; individual workstreams should land separately on top of this. Notable additions: - trace-collector.ts, traces.ts, src/tests/trace-export.test.ts — trace export plumbing - biome.json — Biome linter configuration - .gitignore — exclude native/npm/**/*.node compiled binaries The bulk of the diff is across src/resources/extensions/sf/ (301 files) and src/resources/extensions/sf/tests/ (277 files), reflecting the ongoing sf extension work. Specific feature commits should follow this snapshot rather than being archaeology'd out of it. The 76MB native/npm/linux-x64-gnu/forge_engine.node compiled binary was left out of the commit — it's now gitignored and built locally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
3.6 KiB
TypeScript
168 lines
3.6 KiB
TypeScript
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
unlinkSync,
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
function pathExistsIncludingBrokenSymlink(path: string): boolean {
|
|
try {
|
|
lstatSync(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isBrokenSymlink(path: string): boolean {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
if (!stat.isSymbolicLink()) return false;
|
|
try {
|
|
statSync(path);
|
|
return false;
|
|
} catch {
|
|
return true;
|
|
}
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function removeTargetPath(path: string): void {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
if (stat.isSymbolicLink()) {
|
|
unlinkSync(path);
|
|
return;
|
|
}
|
|
rmSync(path, { force: true });
|
|
} catch {
|
|
// Path already absent.
|
|
}
|
|
}
|
|
|
|
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);
|
|
const brokenTarget = isBrokenSymlink(targetPath);
|
|
if (pathExistsIncludingBrokenSymlink(targetPath)) {
|
|
if (!brokenTarget) return targetPath;
|
|
removeTargetPath(targetPath);
|
|
}
|
|
|
|
mkdirSync(targetDir, { recursive: true });
|
|
|
|
if (!brokenTarget) {
|
|
try {
|
|
symlinkSync(sourcePath, targetPath);
|
|
return targetPath;
|
|
} catch {
|
|
// Fall back to copying below.
|
|
}
|
|
}
|
|
|
|
removeTargetPath(targetPath);
|
|
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[]) {
|
|
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
if (
|
|
pathExistsIncludingBrokenSymlink(targetPath) &&
|
|
!isBrokenSymlink(targetPath)
|
|
)
|
|
continue;
|
|
const sourcePath = resolveToolFromPath(tool, pathValue);
|
|
if (!sourcePath) continue;
|
|
provisioned.push(provisionTool(targetDir, tool, sourcePath));
|
|
}
|
|
|
|
return provisioned;
|
|
}
|