mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): remove npm shrinkwrap and recommend the managed installer
npm installs no longer pin transitive dependencies. Pinned installs come from the pi.dev installer lockfile. pi update on global npm installs now recommends migrating with the installer. closes #5653
This commit is contained in:
parent
221cbcb02e
commit
581e7ba781
15 changed files with 37 additions and 2357 deletions
|
|
@ -100,7 +100,7 @@ console.error("Review lockfile changes before committing:");
|
|||
console.error(" - confirm every new/updated package is intentional");
|
||||
console.error(" - confirm npm age gates were active for resolution");
|
||||
console.error(" - review any new lifecycle scripts in the dependency tree");
|
||||
console.error(" - regenerate/check coding-agent shrinkwrap if release deps changed");
|
||||
console.error(" - regenerate/check coding-agent install lock if release deps changed");
|
||||
|
||||
const summary = changes ? summarizeLockfileChange(changes) : [];
|
||||
if (summary.length > 0) {
|
||||
|
|
|
|||
|
|
@ -1,386 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, posix, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(scriptDir, "..");
|
||||
const codingAgentDir = join(repoRoot, "packages/coding-agent");
|
||||
const rootLockfilePath = join(repoRoot, "package-lock.json");
|
||||
const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json");
|
||||
const internalPackagePrefix = "@earendil-works/pi-";
|
||||
const internalPackageNames = new Set(["@earendil-works/chord"]);
|
||||
const allowedInstallScriptPackages = new Map([
|
||||
["@google/genai@2.21.0", "preinstall is a no-op in the published package"],
|
||||
["esbuild@0.28.2", "postinstall selects and verifies the platform-specific esbuild binary"],
|
||||
["protobufjs@7.6.6", "postinstall only warns about protobufjs version scheme mismatches"],
|
||||
]);
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const checkOnly = args.has("--check");
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg !== "--check") {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function packageDependencies(entry) {
|
||||
return {
|
||||
...(entry.dependencies ?? {}),
|
||||
...(entry.optionalDependencies ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
function sortedObject(object) {
|
||||
return Object.fromEntries(Object.entries(object).sort(([a], [b]) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
function sortedPackageEntry(entry) {
|
||||
const fieldOrder = [
|
||||
"name",
|
||||
"version",
|
||||
"resolved",
|
||||
"integrity",
|
||||
"license",
|
||||
"dependencies",
|
||||
"optionalDependencies",
|
||||
"peerDependencies",
|
||||
"peerDependenciesMeta",
|
||||
"bin",
|
||||
"engines",
|
||||
"os",
|
||||
"cpu",
|
||||
"libc",
|
||||
"optional",
|
||||
"hasInstallScript",
|
||||
"deprecated",
|
||||
"funding",
|
||||
];
|
||||
const sorted = {};
|
||||
|
||||
for (const field of fieldOrder) {
|
||||
if (entry[field] !== undefined) {
|
||||
sorted[field] = entry[field];
|
||||
}
|
||||
}
|
||||
for (const [field, value] of Object.entries(entry).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
if (sorted[field] === undefined) {
|
||||
sorted[field] = value;
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function copyLockEntry(entry) {
|
||||
const copied = { ...entry };
|
||||
delete copied.dev;
|
||||
delete copied.devOptional;
|
||||
delete copied.extraneous;
|
||||
delete copied.link;
|
||||
return sortedPackageEntry(copied);
|
||||
}
|
||||
|
||||
function copyPackageJsonEntry(packageJson, options) {
|
||||
const entry = options.includeName
|
||||
? { name: packageJson.name, version: packageJson.version }
|
||||
: { version: packageJson.version };
|
||||
|
||||
for (const field of [
|
||||
"license",
|
||||
"dependencies",
|
||||
"optionalDependencies",
|
||||
"peerDependencies",
|
||||
"peerDependenciesMeta",
|
||||
"bin",
|
||||
"engines",
|
||||
"os",
|
||||
"cpu",
|
||||
"libc",
|
||||
]) {
|
||||
if (packageJson[field] !== undefined) {
|
||||
entry[field] = packageJson[field];
|
||||
}
|
||||
}
|
||||
|
||||
return sortedPackageEntry(entry);
|
||||
}
|
||||
|
||||
function packageNameFromLockPath(lockPath) {
|
||||
const marker = "node_modules/";
|
||||
const index = lockPath.lastIndexOf(marker);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts = lockPath.slice(index + marker.length).split("/");
|
||||
if (parts[0]?.startsWith("@")) {
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
function registryTarballUrl(packageName, version) {
|
||||
const tarballName = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
||||
return `https://registry.npmjs.org/${packageName}/-/${tarballName}-${version}.tgz`;
|
||||
}
|
||||
|
||||
function getInternalWorkspaces(lockPackages) {
|
||||
const workspaces = new Map();
|
||||
|
||||
for (const [lockPath, entry] of Object.entries(lockPackages)) {
|
||||
if (!lockPath.startsWith("packages/") || lockPath.includes("/node_modules/") || !entry.name || !entry.version) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.startsWith(internalPackagePrefix) && !internalPackageNames.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
workspaces.set(entry.name, {
|
||||
lockPath,
|
||||
packageJson: readJson(join(repoRoot, lockPath, "package.json")),
|
||||
});
|
||||
}
|
||||
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
function resolveExternalDependency(lockPackages, packageName, fromLockPath) {
|
||||
const candidateDirs = [];
|
||||
let current = fromLockPath;
|
||||
|
||||
while (current) {
|
||||
candidateDirs.push(current);
|
||||
const parent = posix.dirname(current);
|
||||
if (parent === "." || parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
candidateDirs.push("");
|
||||
|
||||
const tried = new Set();
|
||||
for (const directory of candidateDirs) {
|
||||
const candidate = directory ? `${directory}/node_modules/${packageName}` : `node_modules/${packageName}`;
|
||||
if (tried.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
tried.add(candidate);
|
||||
|
||||
const entry = lockPackages[candidate];
|
||||
if (entry && !entry.link) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const suffix = `node_modules/${packageName}`;
|
||||
const matches = Object.entries(lockPackages)
|
||||
.filter(([lockPath, entry]) => !entry.link && (lockPath === suffix || lockPath.endsWith(`/${suffix}`)))
|
||||
.map(([lockPath]) => lockPath);
|
||||
|
||||
if (matches.length === 1) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Cannot resolve ${packageName} from ${fromLockPath || "root"}. ` +
|
||||
(matches.length > 1 ? `Matches: ${matches.join(", ")}` : "No matching lockfile entry found."),
|
||||
);
|
||||
}
|
||||
|
||||
function addInternalWorkspace(shrinkwrapPackages, addedPaths, queue, name, workspace) {
|
||||
const packageJson = workspace.packageJson;
|
||||
const outputPath = `node_modules/${name}`;
|
||||
const entry = copyPackageJsonEntry(packageJson, { includeName: false });
|
||||
entry.resolved = registryTarballUrl(name, packageJson.version);
|
||||
|
||||
shrinkwrapPackages[outputPath] = sortedPackageEntry(entry);
|
||||
addedPaths.add(outputPath);
|
||||
|
||||
for (const dependencyName of Object.keys(packageDependencies(packageJson))) {
|
||||
queue.push({
|
||||
name: dependencyName,
|
||||
sourceFrom: workspace.lockPath,
|
||||
sourceBase: workspace.lockPath,
|
||||
outputBase: outputPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addExternalPackage(lockPackages, shrinkwrapPackages, addedPaths, queue, item) {
|
||||
const sourceLockPath = resolveExternalDependency(lockPackages, item.name, item.sourceFrom);
|
||||
const outputLockPath =
|
||||
item.sourceBase && sourceLockPath.startsWith(`${item.sourceBase}/`)
|
||||
? [item.outputBase, sourceLockPath.slice(item.sourceBase.length + 1)].filter(Boolean).join("/")
|
||||
: sourceLockPath;
|
||||
if (addedPaths.has(outputLockPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = lockPackages[sourceLockPath];
|
||||
shrinkwrapPackages[outputLockPath] = copyLockEntry(entry);
|
||||
addedPaths.add(outputLockPath);
|
||||
|
||||
for (const dependencyName of Object.keys(packageDependencies(entry))) {
|
||||
queue.push({
|
||||
name: dependencyName,
|
||||
sourceFrom: sourceLockPath,
|
||||
sourceBase: item.sourceBase,
|
||||
outputBase: item.outputBase,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateShrinkwrap(shrinkwrap, internalNames) {
|
||||
const errors = [];
|
||||
const includedPaths = new Set(Object.keys(shrinkwrap.packages));
|
||||
const includedPackageNames = new Set();
|
||||
const seenAllowedInstallScriptPackages = new Set();
|
||||
|
||||
for (const [lockPath, entry] of Object.entries(shrinkwrap.packages)) {
|
||||
const packageName = packageNameFromLockPath(lockPath);
|
||||
if (packageName) {
|
||||
includedPackageNames.add(packageName);
|
||||
}
|
||||
if (entry.link) {
|
||||
errors.push(`${lockPath} is a link entry`);
|
||||
}
|
||||
if (typeof entry.resolved === "string" && /^(file:|link:|workspace:|\.\.?\/|\/)/.test(entry.resolved)) {
|
||||
errors.push(`${lockPath} has a local resolved value: ${entry.resolved}`);
|
||||
}
|
||||
if (entry.hasInstallScript) {
|
||||
if (!packageName || !entry.version) {
|
||||
errors.push(`${lockPath || "root"} has install scripts but no package name/version`);
|
||||
} else {
|
||||
const packageId = `${packageName}@${entry.version}`;
|
||||
if (allowedInstallScriptPackages.has(packageId)) {
|
||||
seenAllowedInstallScriptPackages.add(packageId);
|
||||
} else {
|
||||
errors.push(
|
||||
`${lockPath} has install scripts (${packageId}). Review it and add it to allowedInstallScriptPackages if intentional.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const packageId of allowedInstallScriptPackages.keys()) {
|
||||
if (!seenAllowedInstallScriptPackages.has(packageId)) {
|
||||
errors.push(`allowed install-script package ${packageId} is no longer present; remove it from the allowlist`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of internalNames) {
|
||||
if (!includedPackageNames.has(name)) {
|
||||
errors.push(`internal dependency ${name} is missing`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [lockPath, entry] of Object.entries(shrinkwrap.packages)) {
|
||||
for (const dependencyName of Object.keys(packageDependencies(entry))) {
|
||||
const dependencyIncluded = [...includedPaths].some(
|
||||
(candidate) => candidate === `node_modules/${dependencyName}` || candidate.endsWith(`/node_modules/${dependencyName}`),
|
||||
);
|
||||
if (!dependencyIncluded) {
|
||||
errors.push(`${lockPath || "root"} dependency ${dependencyName} is missing`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const platformPackageCount = Object.values(shrinkwrap.packages).filter((entry) => entry.os || entry.cpu || entry.libc).length;
|
||||
if (platformPackageCount === 0) {
|
||||
errors.push("no platform-specific optional dependency entries found");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Generated shrinkwrap failed validation:\n${errors.map((error) => ` - ${error}`).join("\n")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateShrinkwrap() {
|
||||
const rootLock = readJson(rootLockfilePath);
|
||||
if (rootLock.lockfileVersion !== 3 || !rootLock.packages) {
|
||||
throw new Error("package-lock.json must be lockfileVersion 3 and contain a packages map");
|
||||
}
|
||||
|
||||
const lockPackages = rootLock.packages;
|
||||
const codingAgentPackage = readJson(join(codingAgentDir, "package.json"));
|
||||
const internalWorkspaces = getInternalWorkspaces(lockPackages);
|
||||
const shrinkwrapPackages = {
|
||||
"": copyPackageJsonEntry(codingAgentPackage, { includeName: true }),
|
||||
};
|
||||
const addedPaths = new Set([""]);
|
||||
const internalNames = new Set();
|
||||
const queue = Object.keys(packageDependencies(codingAgentPackage)).map((name) => ({
|
||||
name,
|
||||
sourceFrom: "packages/coding-agent",
|
||||
sourceBase: "packages/coding-agent",
|
||||
outputBase: "",
|
||||
}));
|
||||
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) {
|
||||
break;
|
||||
}
|
||||
|
||||
const workspace = internalWorkspaces.get(item.name);
|
||||
if (workspace) {
|
||||
const outputPath = `node_modules/${item.name}`;
|
||||
internalNames.add(item.name);
|
||||
if (!addedPaths.has(outputPath)) {
|
||||
addInternalWorkspace(shrinkwrapPackages, addedPaths, queue, item.name, workspace);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
addExternalPackage(lockPackages, shrinkwrapPackages, addedPaths, queue, item);
|
||||
}
|
||||
|
||||
const shrinkwrap = {
|
||||
name: codingAgentPackage.name,
|
||||
version: codingAgentPackage.version,
|
||||
lockfileVersion: 3,
|
||||
requires: true,
|
||||
packages: sortedObject(shrinkwrapPackages),
|
||||
};
|
||||
|
||||
validateShrinkwrap(shrinkwrap, internalNames);
|
||||
return shrinkwrap;
|
||||
}
|
||||
|
||||
try {
|
||||
const shrinkwrap = generateShrinkwrap();
|
||||
const content = `${JSON.stringify(shrinkwrap, null, "\t")}\n`;
|
||||
|
||||
if (checkOnly) {
|
||||
if (!existsSync(shrinkwrapPath)) {
|
||||
console.error("packages/coding-agent/npm-shrinkwrap.json is missing.");
|
||||
console.error("Run: npm run shrinkwrap:coding-agent");
|
||||
process.exit(1);
|
||||
}
|
||||
const current = readFileSync(shrinkwrapPath, "utf8");
|
||||
if (current !== content) {
|
||||
console.error("packages/coding-agent/npm-shrinkwrap.json is out of date.");
|
||||
console.error("Run: npm run shrinkwrap:coding-agent");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("packages/coding-agent/npm-shrinkwrap.json is up to date.");
|
||||
} else {
|
||||
writeFileSync(shrinkwrapPath, content);
|
||||
const packageCount = Object.keys(shrinkwrap.packages).length - 1;
|
||||
const platformPackageCount = Object.values(shrinkwrap.packages).filter((entry) => entry.os || entry.cpu || entry.libc).length;
|
||||
console.log(
|
||||
`Wrote packages/coding-agent/npm-shrinkwrap.json (${packageCount} packages, ${platformPackageCount} platform-specific).`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -237,7 +237,6 @@ console.log();
|
|||
console.log("Regenerating release artifacts...");
|
||||
run("npm run generate:models");
|
||||
run("npm run check:model-data");
|
||||
run("npm run shrinkwrap:coding-agent");
|
||||
run("npm run install-lock:coding-agent");
|
||||
console.log();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue