FN-8201: pin pi dependency versions
Pin the pi runtime packages to a single exact version so global npm installs resolve a compatible set. - Pin pi-ai and pi-coding-agent declarations across workspace manifests - Add a guard and tests that reject ranged or mismatched pi versions - Document the source-install fallback and add a patch changeset Files changed: .changeset/fn-8201-pin-pi-versions.md | 7 ++ docs/getting-started.md | 3 + package.json | 6 +- packages/cli/package.json | 4 +- packages/cli/src/__tests__/package-config.test.ts | 18 +++- packages/core/package.json | 2 +- packages/dashboard/package.json | 2 +- packages/engine/package.json | 4 +- packages/pi-claude-cli/package.json | 8 +- .../__tests__/check-pi-versions-pinned.test.mjs | 45 ++++++++ scripts/check-pi-versions-pinned.mjs | 120 +++++++++++++++++++++ 11 files changed, 205 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8201 Fusion-Task-Lineage: bf0ac363-df5f-4445-835b-cfd2d4909659 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8201-pin-pi-versions.md
Normal file
7
.changeset/fn-8201-pin-pi-versions.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Make global npm installs reliable by pinning the @earendil-works/pi-* version set.
|
||||||
|
category: fix
|
||||||
|
dev: Pins pi-ai and pi-coding-agent to exact 0.80.10 and adds check-pi-versions-pinned.mjs.
|
||||||
@@ -41,9 +41,12 @@ npm install -g @runfusion/fusion
|
|||||||
fn dashboard # or: fusion dashboard
|
fn dashboard # or: fusion dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Fusion pins `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent` as a locked version pair. If a global npm install ever reports a `pi-*` export mismatch, build from source with `pnpm install`; it honors this repository's committed lockfile and is the reliable fallback while upstream pi-mono patch exports stabilize.
|
||||||
|
|
||||||
### From source (development)
|
### From source (development)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pnpm install
|
||||||
pnpm dev dashboard
|
pnpm dev dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.33.0",
|
"packageManager": "pnpm@10.33.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pretest": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs",
|
"pretest": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs",
|
||||||
"pretest:full": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs",
|
"pretest:full": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs",
|
||||||
"check:line-count": "node scripts/check-file-line-count.mjs",
|
"check:line-count": "node scripts/check-file-line-count.mjs",
|
||||||
"check:changesets": "node scripts/check-changeset-format.mjs",
|
"check:changesets": "node scripts/check-changeset-format.mjs",
|
||||||
"check:quarantine-ledger": "node scripts/check-quarantine-ledger.mjs",
|
"check:quarantine-ledger": "node scripts/check-quarantine-ledger.mjs",
|
||||||
"check:mock-completeness": "node scripts/check-mock-completeness.mjs",
|
"check:mock-completeness": "node scripts/check-mock-completeness.mjs",
|
||||||
"test:gate": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-mock-completeness.mjs && pnpm --filter @fusion/engine test:core && pnpm --filter @fusion/core test:pg-gate && pnpm --filter @runfusion/fusion test:ci-shape",
|
"test:gate": "node scripts/check-no-nohup.mjs && node scripts/check-no-kill-4040.mjs && node scripts/check-no-getdatabase.mjs && node scripts/check-pi-versions-pinned.mjs && node scripts/check-no-test-timeout-appeasement.mjs && node scripts/check-changeset-format.mjs && node scripts/check-mock-completeness.mjs && pnpm --filter @fusion/engine test:core && pnpm --filter @fusion/core test:pg-gate && pnpm --filter @runfusion/fusion test:ci-shape",
|
||||||
"smoke:boot": "node scripts/boot-smoke.mjs",
|
"smoke:boot": "node scripts/boot-smoke.mjs",
|
||||||
"local": "node scripts/start-local.mjs",
|
"local": "node scripts/start-local.mjs",
|
||||||
"dev": "node scripts/dev-with-memory.mjs",
|
"dev": "node scripts/dev-with-memory.mjs",
|
||||||
|
|||||||
@@ -60,8 +60,8 @@
|
|||||||
"test:pre-release": "pnpm test:slow-cli && pnpm test:build-exe"
|
"test:pre-release": "pnpm test:slow-cli && pnpm test:build-exe"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.10",
|
"@earendil-works/pi-ai": "0.80.10",
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10",
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
"dockerode": "^4.0.12",
|
"dockerode": "^4.0.12",
|
||||||
"electron": "^33.4.11",
|
"electron": "^33.4.11",
|
||||||
"embedded-postgres": "15.18.0-beta.17",
|
"embedded-postgres": "15.18.0-beta.17",
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ function hasProjectArg(script: string | undefined, project: string): boolean {
|
|||||||
return parts.some((part, index) => part === "--project" && parts[index + 1] === project);
|
return parts.some((part, index) => part === "--project" && parts[index + 1] === project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:DependencyPinning 2026-07-17-12:00:
|
||||||
|
FN-8201 requires source and prepack-transformed manifests to keep pi-ai and
|
||||||
|
pi-coding-agent as one exact version pair, because npm global installation does
|
||||||
|
not honor pnpm-lock.yaml when resolving package dependency ranges.
|
||||||
|
*/
|
||||||
|
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||||
|
|
||||||
function assertRuntimeDepsAreNotOptionalPeers(pkg: any, label: string): void {
|
function assertRuntimeDepsAreNotOptionalPeers(pkg: any, label: string): void {
|
||||||
const dependencies = pkg.dependencies ?? {};
|
const dependencies = pkg.dependencies ?? {};
|
||||||
const peerDependencies = pkg.peerDependencies ?? {};
|
const peerDependencies = pkg.peerDependencies ?? {};
|
||||||
@@ -48,7 +56,10 @@ function assertRuntimeDepsAreNotOptionalPeers(pkg: any, label: string): void {
|
|||||||
for (const dependencyName of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"]) {
|
for (const dependencyName of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"]) {
|
||||||
expect(dependencies, `${label}: ${dependencyName} must remain a required runtime dependency`).toHaveProperty(
|
expect(dependencies, `${label}: ${dependencyName} must remain a required runtime dependency`).toHaveProperty(
|
||||||
dependencyName,
|
dependencyName,
|
||||||
"^0.80.6",
|
"0.80.10",
|
||||||
|
);
|
||||||
|
expect(dependencies[dependencyName], `${label}: ${dependencyName} must be a clean exact semver`).toMatch(
|
||||||
|
EXACT_SEMVER,
|
||||||
);
|
);
|
||||||
expect(peerDependencies, `${label}: ${dependencyName} must not be a peer dependency`).not.toHaveProperty(
|
expect(peerDependencies, `${label}: ${dependencyName} must not be a peer dependency`).not.toHaveProperty(
|
||||||
dependencyName,
|
dependencyName,
|
||||||
@@ -58,6 +69,11 @@ function assertRuntimeDepsAreNotOptionalPeers(pkg: any, label: string): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
dependencies["@earendil-works/pi-ai"],
|
||||||
|
`${label}: pi-ai and pi-coding-agent must remain a matched exact version pair`,
|
||||||
|
).toBe(dependencies["@earendil-works/pi-coding-agent"]);
|
||||||
|
|
||||||
expect(dependencies, `${label}: typebox must not be promoted into runtime dependencies`).not.toHaveProperty(
|
expect(dependencies, `${label}: typebox must not be promoted into runtime dependencies`).not.toHaveProperty(
|
||||||
"typebox",
|
"typebox",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"test:pg-gate": "vitest run src/__tests__/postgres/handoff-to-review-atomicity.pg.test.ts src/__tests__/postgres/store-list.pg.test.ts src/__tests__/postgres/task-lifecycle-e2e.pg.test.ts src/__tests__/postgres/soft-delete-resurrection-FN-5233.pg.test.ts src/__tests__/postgres/agent-logs-and-monitor.pg.test.ts src/__tests__/postgres/todo-store.pg.test.ts src/__tests__/postgres/workflow-definitions.pg.test.ts src/__tests__/postgres/message-store.pg.test.ts src/__tests__/postgres/insight-store.pg.test.ts src/__tests__/postgres/insight-run-execution.pg.test.ts src/__tests__/postgres/research-store.pg.test.ts src/__tests__/postgres/mission-store.pg.test.ts src/__tests__/postgres/goal-store.pg.test.ts src/__tests__/postgres/artifacts-documents-evals.pg.test.ts src/__tests__/postgres/command-center-analytics.pg.test.ts src/__tests__/postgres/command-center-remaining-analytics.pg.test.ts src/__tests__/postgres/research-execution.pg.test.ts src/__tests__/postgres/async-store-events.pg.test.ts src/__tests__/postgres/signal-ingestion.pg.test.ts src/__tests__/postgres/mission-autopilot.pg.test.ts src/__tests__/postgres/workflow-create.pg.test.ts src/__tests__/postgres/monitor-trait-storm-guard.pg.test.ts src/__tests__/postgres/agent-wake-getagent.pg.test.ts --silent=passed-only --reporter=dot"
|
"test:pg-gate": "vitest run src/__tests__/postgres/handoff-to-review-atomicity.pg.test.ts src/__tests__/postgres/store-list.pg.test.ts src/__tests__/postgres/task-lifecycle-e2e.pg.test.ts src/__tests__/postgres/soft-delete-resurrection-FN-5233.pg.test.ts src/__tests__/postgres/agent-logs-and-monitor.pg.test.ts src/__tests__/postgres/todo-store.pg.test.ts src/__tests__/postgres/workflow-definitions.pg.test.ts src/__tests__/postgres/message-store.pg.test.ts src/__tests__/postgres/insight-store.pg.test.ts src/__tests__/postgres/insight-run-execution.pg.test.ts src/__tests__/postgres/research-store.pg.test.ts src/__tests__/postgres/mission-store.pg.test.ts src/__tests__/postgres/goal-store.pg.test.ts src/__tests__/postgres/artifacts-documents-evals.pg.test.ts src/__tests__/postgres/command-center-analytics.pg.test.ts src/__tests__/postgres/command-center-remaining-analytics.pg.test.ts src/__tests__/postgres/research-execution.pg.test.ts src/__tests__/postgres/async-store-events.pg.test.ts src/__tests__/postgres/signal-ingestion.pg.test.ts src/__tests__/postgres/mission-autopilot.pg.test.ts src/__tests__/postgres/workflow-create.pg.test.ts src/__tests__/postgres/monitor-trait-storm-guard.pg.test.ts src/__tests__/postgres/agent-wake-getagent.pg.test.ts --silent=passed-only --reporter=dot"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10",
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
"@types/dockerode": "^3.3.41",
|
"@types/dockerode": "^3.3.41",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@vitest/coverage-v8": "^4.1.0",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.36.4",
|
"@codemirror/view": "^6.36.4",
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10",
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
"@fusion-plugin-examples/cli-printing-press": "workspace:*",
|
"@fusion-plugin-examples/cli-printing-press": "workspace:*",
|
||||||
"@fusion-plugin-examples/compound-engineering": "workspace:*",
|
"@fusion-plugin-examples/compound-engineering": "workspace:*",
|
||||||
"@fusion-plugin-examples/cursor-runtime": "workspace:*",
|
"@fusion-plugin-examples/cursor-runtime": "workspace:*",
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
"test:watch": "vitest src/__tests__/executor-*.test.ts --watch"
|
"test:watch": "vitest src/__tests__/executor-*.test.ts --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.10",
|
"@earendil-works/pi-ai": "0.80.10",
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10",
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
"@fusion/core": "workspace:*",
|
"@fusion/core": "workspace:*",
|
||||||
"@fusion/pi-claude-cli": "workspace:*",
|
"@fusion/pi-claude-cli": "workspace:*",
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
"@agentclientprotocol/sdk": "0.24.0"
|
"@agentclientprotocol/sdk": "0.24.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.10",
|
"@earendil-works/pi-ai": "0.80.10",
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10"
|
"@earendil-works/pi-coding-agent": "0.80.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.10",
|
"@earendil-works/pi-ai": "0.80.10",
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.10",
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vitest": "^4.1.0"
|
"vitest": "^4.1.0"
|
||||||
|
|||||||
45
scripts/__tests__/check-pi-versions-pinned.test.mjs
Normal file
45
scripts/__tests__/check-pi-versions-pinned.test.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
scanTrackedManifests,
|
||||||
|
validateManifestSet,
|
||||||
|
} from "../check-pi-versions-pinned.mjs";
|
||||||
|
|
||||||
|
const pinnedManifest = {
|
||||||
|
dependencies: {
|
||||||
|
"@earendil-works/pi-ai": "0.80.10",
|
||||||
|
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function validate(manifest) {
|
||||||
|
return validateManifestSet([{ filePath: "packages/cli/package.json", manifest }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("check-pi-versions-pinned", () => {
|
||||||
|
it("passes against every guarded repository manifest", () => {
|
||||||
|
assert.deepEqual(scanTrackedManifests(), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects caret, tilde, wildcard, x, and comparator ranges", () => {
|
||||||
|
for (const version of ["^0.80.10", "~0.80.10", "*", "0.80.x", ">=0.80.10"]) {
|
||||||
|
const violations = validate({
|
||||||
|
...pinnedManifest,
|
||||||
|
dependencies: { ...pinnedManifest.dependencies, "@earendil-works/pi-ai": version },
|
||||||
|
});
|
||||||
|
assert.equal(violations.length > 0, true, `${version} must be rejected`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a matched-set version mismatch", () => {
|
||||||
|
const violations = validate({
|
||||||
|
...pinnedManifest,
|
||||||
|
dependencies: { ...pinnedManifest.dependencies, "@earendil-works/pi-coding-agent": "0.80.11" },
|
||||||
|
});
|
||||||
|
assert.equal(violations.some((violation) => violation.includes("same exact version")), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a clean exact matched pair", () => {
|
||||||
|
assert.deepEqual(validate(pinnedManifest), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
120
scripts/check-pi-versions-pinned.mjs
Normal file
120
scripts/check-pi-versions-pinned.mjs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
FNXC:DependencyPinning 2026-07-17-12:00:
|
||||||
|
FN-8201 / Runfusion/Fusion#2270 requires the pi-ai and pi-coding-agent runtime
|
||||||
|
set to remain version-locked. npm global installs ignore pnpm-lock.yaml and can
|
||||||
|
independently resolve ranges to incompatible pi-mono patches, so every guarded
|
||||||
|
manifest declaration must be an exact semver and all declarations must agree.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export const PI_DEPENDENCIES = [
|
||||||
|
"@earendil-works/pi-ai",
|
||||||
|
"@earendil-works/pi-coding-agent",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GUARDED_MANIFESTS = [
|
||||||
|
"packages/cli/package.json",
|
||||||
|
"packages/core/package.json",
|
||||||
|
"packages/engine/package.json",
|
||||||
|
"packages/dashboard/package.json",
|
||||||
|
"packages/pi-claude-cli/package.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
||||||
|
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
||||||
|
|
||||||
|
export function isExactSemver(version) {
|
||||||
|
return typeof version === "string" && EXACT_SEMVER.test(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTrackedManifests() {
|
||||||
|
const result = spawnSync("git", ["ls-files", "--", ...GUARDED_MANIFESTS], {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
if (result.status !== 0) throw new Error(result.stderr?.trim() || "git ls-files failed");
|
||||||
|
return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateManifestSet(manifests) {
|
||||||
|
const violations = [];
|
||||||
|
const declaredVersions = new Map();
|
||||||
|
|
||||||
|
for (const { filePath, manifest } of manifests) {
|
||||||
|
for (const blockName of DEPENDENCY_BLOCKS) {
|
||||||
|
const dependencies = manifest?.[blockName];
|
||||||
|
if (!dependencies || typeof dependencies !== "object") continue;
|
||||||
|
|
||||||
|
for (const packageName of PI_DEPENDENCIES) {
|
||||||
|
if (!(packageName in dependencies)) continue;
|
||||||
|
const version = dependencies[packageName];
|
||||||
|
if (!isExactSemver(version)) {
|
||||||
|
violations.push(`${filePath}: ${blockName}.${packageName} must be an exact semver, found ${JSON.stringify(version)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const declarations = declaredVersions.get(packageName) ?? [];
|
||||||
|
declarations.push({ filePath, blockName, version });
|
||||||
|
declaredVersions.set(packageName, declarations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const packageName of PI_DEPENDENCIES) {
|
||||||
|
const declarations = declaredVersions.get(packageName) ?? [];
|
||||||
|
const versions = [...new Set(declarations.map(({ version }) => version))];
|
||||||
|
if (versions.length > 1) {
|
||||||
|
violations.push(`${packageName} must use one exact version across guarded manifests; found ${versions.join(", ")} (${declarations.map(({ filePath, blockName, version }) => `${filePath}:${blockName}=${version}`).join(", ")})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allVersions = [...new Set(
|
||||||
|
PI_DEPENDENCIES.flatMap((packageName) =>
|
||||||
|
(declaredVersions.get(packageName) ?? []).map(({ version }) => version),
|
||||||
|
),
|
||||||
|
)];
|
||||||
|
if (allVersions.length > 1) {
|
||||||
|
violations.push(`${PI_DEPENDENCIES.join(" and ")} must resolve to the same exact version; found ${allVersions.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scanTrackedManifests(files = listTrackedManifests(), options = {}) {
|
||||||
|
const readFile = options.readFile ?? readFileSync;
|
||||||
|
const manifests = [];
|
||||||
|
for (const filePath of files) {
|
||||||
|
let source;
|
||||||
|
try {
|
||||||
|
source = readFile(filePath, "utf8");
|
||||||
|
} catch (error) {
|
||||||
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") continue;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
manifests.push({ filePath, manifest: JSON.parse(source) });
|
||||||
|
} catch (error) {
|
||||||
|
return [`${filePath}: invalid JSON (${error instanceof Error ? error.message : String(error)})`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validateManifestSet(manifests);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFailureMessage(violations) {
|
||||||
|
return [
|
||||||
|
"[check-pi-versions-pinned] pi runtime dependencies must be exact, matched versions.",
|
||||||
|
"npm global installs do not use pnpm-lock.yaml; ranges can resolve an incompatible pi-mono patch set.",
|
||||||
|
...violations.map((violation) => `- ${violation}`),
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
const violations = scanTrackedManifests();
|
||||||
|
if (!violations.length) return 0;
|
||||||
|
console.error(formatFailureMessage(violations));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exitCode = main();
|
||||||
Reference in New Issue
Block a user