singularity-forge/scripts/link-workspace-packages.cjs

157 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* link-workspace-packages.cjs
*
* Creates node_modules/@singularity-forge/* symlinks pointing to shipped
* packages/* directories.
*
* During development, npm workspaces creates these automatically. But in the
* published tarball, workspace packages are shipped under packages/ (via the
* "files" field) and the @singularity-forge/* imports in compiled code need node_modules/@singularity-forge/*
* to resolve. This script bridges the gap.
*
* Runs as part of postinstall (before any ESM code that imports @singularity-forge/*).
*
* On Windows without Developer Mode or administrator rights, creating symlinks
* (even NTFS junctions) can fail with EPERM. In that case we fall back to
* cpSync (directory copy) which works universally.
*/
const {
existsSync,
mkdirSync,
symlinkSync,
cpSync,
lstatSync,
readlinkSync,
unlinkSync,
} = require("node:fs");
const { resolve, join } = require("node:path");
const root = resolve(__dirname, "..");
const packagesDir = join(root, "packages");
const scope = "@singularity-forge";
const scopeDir = join(root, "node_modules", scope);
// Directory names under packages/ that should be linked as @singularity-forge/<dir>
const packageDirs = [
"native",
"pi-agent-core",
"google-gemini-cli-provider",
"pi-ai",
"pi-coding-agent",
"pi-tui",
"rpc-client",
"daemon",
];
if (!existsSync(scopeDir)) {
mkdirSync(scopeDir, { recursive: true });
}
let linked = 0;
let copied = 0;
for (const dir of packageDirs) {
const source = join(packagesDir, dir);
const target = join(scopeDir, dir);
if (!existsSync(source)) continue;
// Skip if already correctly linked or is a real directory (bundled)
if (existsSync(target)) {
try {
const stat = lstatSync(target);
if (stat.isSymbolicLink()) {
const linkTarget = readlinkSync(target);
if (
resolve(join(scopeDir, linkTarget)) === source ||
linkTarget === source
) {
continue; // Already correct
}
unlinkSync(target); // Wrong target, relink
} else {
continue; // Real directory (e.g., copied or from bundleDependencies), don't touch
}
} catch {
continue;
}
}
let symlinkOk = false;
try {
symlinkSync(source, target, "junction"); // junction works on Windows too
symlinkOk = true;
linked++;
} catch {
// Symlink failed — common on Windows without Developer Mode or admin rights.
// Fall back to a directory copy so the package is still resolvable.
}
if (!symlinkOk) {
try {
cpSync(source, target, { recursive: true });
copied++;
} catch {
// Non-fatal — loader.ts will emit a clearer error if resolution still fails
}
}
}
if (linked > 0)
process.stderr.write(
` Linked ${linked} workspace package${linked !== 1 ? "s" : ""}\n`,
);
if (copied > 0)
process.stderr.write(
` Copied ${copied} workspace package${copied !== 1 ? "s" : ""} (symlinks unavailable)\n`,
);
// Platform-specific native engine packages live under rust-engine/npm/<suffix>/, not packages/.
// Wire them into node_modules/@singularity-forge/ so native.ts can require() them without
// a registry install. Only link platforms where the binary (forge_engine.node) is present.
const nativeNpmDir = join(root, "native", "npm");
const engineSuffixes = [
"darwin-arm64",
"darwin-x64",
"linux-x64-gnu",
"linux-arm64-gnu",
"win32-x64-msvc",
];
for (const suffix of engineSuffixes) {
const source = join(nativeNpmDir, suffix);
const binaryPath = join(source, "forge_engine.node");
if (!existsSync(source) || !existsSync(binaryPath)) continue;
const target = join(scopeDir, `engine-${suffix}`);
if (existsSync(target)) {
try {
const stat = lstatSync(target);
if (stat.isSymbolicLink()) {
const linkTarget = readlinkSync(target);
if (
resolve(join(scopeDir, linkTarget)) === source ||
linkTarget === source
)
continue;
unlinkSync(target);
} else {
continue;
}
} catch {
continue;
}
}
try {
symlinkSync(source, target, "junction");
process.stderr.write(
` Linked native engine: @singularity-forge/engine-${suffix}\n`,
);
} catch {
try {
cpSync(source, target, { recursive: true });
} catch {
/* non-fatal */
}
}
}