chore(coding-agent): add publish shrinkwrap

This commit is contained in:
Armin Ronacher
2026-05-12 23:09:56 +02:00
Unverified
parent 3d9e14d748
commit b9637cd622
6 changed files with 3296 additions and 13 deletions
+4 -2
View File
@@ -15,8 +15,10 @@
"build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build",
"dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"",
"dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"",
"check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check",
"check": "biome check --write --error-on-warnings . && npm run shrinkwrap:coding-agent:check && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check",
"check:browser-smoke": "node scripts/check-browser-smoke.mjs",
"shrinkwrap:coding-agent": "node scripts/generate-coding-agent-shrinkwrap.mjs",
"shrinkwrap:coding-agent:check": "node scripts/generate-coding-agent-shrinkwrap.mjs --check",
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
"test": "npm run test --workspaces --if-present",
@@ -24,7 +26,7 @@
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
"version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install",
"version:set": "npm version -ws",
"prepublishOnly": "npm run clean && npm run build && npm run check",
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap:coding-agent && npm run check",
"publish": "npm run prepublishOnly && npm publish -ws --access public",
"publish:dry": "npm run prepublishOnly && npm publish -ws --access public --dry-run",
"release:patch": "node scripts/release.mjs patch",
+4
View File
@@ -6,6 +6,10 @@
- Added Together AI to built-in provider setup, `/login` API-key auth, and default model resolution ([#3624](https://github.com/earendil-works/pi-mono/pull/3624) by [@Nutlope](https://github.com/Nutlope)).
### Changed
- Published npm installs now include an `npm-shrinkwrap.json` to lock transitive dependencies and platform-specific optional packages.
### Fixed
- Fixed theme sharing across package scopes so extensions do not crash with `Theme not initialized` ([#4333](https://github.com/earendil-works/pi/issues/4333)).
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -25,7 +25,8 @@
"dist",
"docs",
"examples",
"CHANGELOG.md"
"CHANGELOG.md",
"npm-shrinkwrap.json"
],
"scripts": {
"clean": "shx rm -rf dist",
@@ -34,8 +35,9 @@
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets",
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
"shrinkwrap": "node ../../scripts/generate-coding-agent-shrinkwrap.mjs",
"test": "vitest --run",
"prepublishOnly": "npm run clean && npm run build"
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
},
"dependencies": {
"@earendil-works/pi-agent-core": "^0.74.0",
@@ -0,0 +1,269 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs";
import { dirname, join, posix, resolve } from "path";
import { fileURLToPath } from "url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "..");
const codingAgentPackagePath = "packages/coding-agent";
const codingAgentDir = join(repoRoot, codingAgentPackagePath);
const lockfilePath = join(repoRoot, "package-lock.json");
const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json");
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 productionDependencies(pkg) {
return {
...(pkg.dependencies || {}),
...(pkg.optionalDependencies || {}),
};
}
function sortedObject(obj) {
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
}
function copyLockEntry(entry) {
const copied = { ...entry };
delete copied.dev;
delete copied.devOptional;
delete copied.extraneous;
delete copied.link;
return sortedObject(copied);
}
function copyPackageJsonEntry(pkg, { includeName }) {
const entry = includeName ? { name: pkg.name, version: pkg.version } : { version: pkg.version };
for (const field of [
"license",
"bin",
"engines",
"os",
"cpu",
"libc",
"dependencies",
"optionalDependencies",
"peerDependencies",
"peerDependenciesMeta",
]) {
if (pkg[field] !== undefined) {
entry[field] = pkg[field];
}
}
return sortedObject(entry);
}
function packageNameFromLockPath(lockPath) {
const marker = "node_modules/";
const index = lockPath.lastIndexOf(marker);
if (index === -1) {
throw new Error(`Cannot derive package name from lock path: ${lockPath}`);
}
const rest = lockPath.slice(index + marker.length).split("/");
if (rest[0]?.startsWith("@")) {
return `${rest[0]}/${rest[1]}`;
}
return rest[0];
}
function workspaceOutputPath(packageName) {
return `node_modules/${packageName}`;
}
function findWorkspacePackages(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;
}
workspaces.set(entry.name, {
lockPath,
pkg: readJson(join(repoRoot, lockPath, "package.json")),
});
}
return workspaces;
}
function createResolver(lockPackages) {
return function resolveExternalDependency(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 dir of candidateDirs) {
const candidate = dir ? `${dir}/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 validateShrinkwrapPackages(shrinkwrapPackages) {
const errors = [];
const includedPaths = new Set(Object.keys(shrinkwrapPackages));
for (const [lockPath, entry] of Object.entries(shrinkwrapPackages)) {
if (entry.link) {
errors.push(`${lockPath} is a link entry`);
}
if (typeof entry.resolved === "string" && /^(file:|\.\.?\/|\/)/.test(entry.resolved)) {
errors.push(`${lockPath} has a local resolved value: ${entry.resolved}`);
}
}
for (const [lockPath, entry] of Object.entries(shrinkwrapPackages)) {
for (const dependencyName of Object.keys(entry.optionalDependencies || {})) {
const dependencyIncluded = [...includedPaths].some(
(candidate) => candidate === `node_modules/${dependencyName}` || candidate.endsWith(`/node_modules/${dependencyName}`),
);
if (!dependencyIncluded) {
errors.push(`${lockPath} optional dependency ${dependencyName} is missing`);
}
}
}
const platformEntries = Object.entries(shrinkwrapPackages).filter(([, entry]) => entry.os || entry.cpu || entry.libc);
if (platformEntries.length === 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(lockfilePath);
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 workspaces = findWorkspacePackages(lockPackages);
const resolveExternalDependency = createResolver(lockPackages);
const shrinkwrapPackages = {
"": copyPackageJsonEntry(codingAgentPackage, { includeName: true }),
};
const addedPaths = new Set([""]);
const queue = Object.keys(productionDependencies(codingAgentPackage)).map((name) => ({ name, from: "" }));
while (queue.length > 0) {
const item = queue.shift();
if (!item) {
break;
}
const workspace = workspaces.get(item.name);
if (workspace) {
const outputPath = workspaceOutputPath(item.name);
if (addedPaths.has(outputPath)) {
continue;
}
shrinkwrapPackages[outputPath] = copyPackageJsonEntry(workspace.pkg, { includeName: false });
addedPaths.add(outputPath);
for (const dependencyName of Object.keys(productionDependencies(workspace.pkg))) {
queue.push({ name: dependencyName, from: outputPath });
}
continue;
}
const lockPath = resolveExternalDependency(item.name, item.from);
if (addedPaths.has(lockPath)) {
continue;
}
const entry = lockPackages[lockPath];
shrinkwrapPackages[lockPath] = copyLockEntry(entry);
addedPaths.add(lockPath);
for (const dependencyName of Object.keys(productionDependencies(entry))) {
queue.push({ name: dependencyName, from: lockPath });
}
}
validateShrinkwrapPackages(shrinkwrapPackages);
return {
name: codingAgentPackage.name,
version: codingAgentPackage.version,
lockfileVersion: 3,
requires: true,
packages: sortedObject(shrinkwrapPackages),
};
}
try {
const shrinkwrap = generateShrinkwrap();
const content = `${JSON.stringify(shrinkwrap, null, "\t")}\n`;
if (checkOnly) {
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);
}
+15 -9
View File
@@ -10,10 +10,11 @@
* 1. Check for uncommitted changes
* 2. Bump version via npm run version:xxx or set an explicit version
* 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date
* 4. Commit and tag
* 5. Publish to npm
* 6. Add new [Unreleased] section to changelogs
* 7. Commit
* 4. Generate the coding-agent npm-shrinkwrap.json
* 5. Commit and tag
* 6. Publish to npm
* 7. Add new [Unreleased] section to changelogs
* 8. Commit
*/
import { execSync } from "child_process";
@@ -164,30 +165,35 @@ console.log("Updating CHANGELOG.md files...");
updateChangelogsForRelease(version);
console.log();
// 4. Commit and tag
// 4. Generate publish shrinkwrap
console.log("Generating coding-agent shrinkwrap...");
run("npm run shrinkwrap:coding-agent");
console.log();
// 5. Commit and tag
console.log("Committing and tagging...");
stageChangedFiles();
run(`git commit -m "Release v${version}"`);
run(`git tag v${version}`);
console.log();
// 5. Publish
// 6. Publish
console.log("Publishing to npm...");
run("npm run publish");
console.log();
// 6. Add new [Unreleased] sections
// 7. Add new [Unreleased] sections
console.log("Adding [Unreleased] sections for next cycle...");
addUnreleasedSection();
console.log();
// 7. Commit
// 8. Commit
console.log("Committing changelog updates...");
stageChangedFiles();
run(`git commit -m "Add [Unreleased] section for next cycle"`);
console.log();
// 8. Push
// 9. Push
console.log("Pushing to remote...");
run("git push origin main");
run(`git push origin v${version}`);