Files
callstack__agent-device/scripts/check-affected/lockfile-install-sync.ts
Michał Pierzchała 04e4c23b95 dx(check): fail fast when node_modules lags the lockfile (#1967)
* dx(doctor): flag a worktree whose node_modules lags the lockfile

Add a doctor probe and a check:affected preflight that compare
node_modules/.pnpm/lock.yaml (the exact lockfile snapshot pnpm installed
from) against pnpm-lock.yaml via a content hash — no subprocess. On
mismatch both surfaces report the same one-liner: "node_modules was
installed from a different lockfile; run pnpm install", so a stale
install names its own cause instead of surfacing as bogus format diffs
on files a change never touched (the #1956 incident).

Closes #1963

* fix(doctor): scope the node-modules probe to a local source checkout

The probe ran unconditionally from findProjectRoot(), so it fired in two
contexts it cannot diagnose:

- Packaged installs. Published packages ship neither pnpm-lock.yaml (not
  in the package.json `files` allowlist) nor an installed snapshot, so
  every end user's `doctor` gained a spurious node-modules line and a
  degraded overall status.
- `--remote`, where the daemon's own root describes the server
  deployment rather than the caller's worktree, so the answer could not
  address #1963 at all.

Whether a root is a source checkout is now decided by the presence of
pnpm-lock.yaml itself rather than a heuristic about install location, and
'no-source-checkout' is a distinct result rather than a warning, so the
packaged case cannot be represented as a defect. The probe returns
undefined there and the route appends no check, matching how doctor
already models an out-of-scope question (the device family is likewise
absent under --remote). The fresh-worktree catch is preserved: a lockfile
with no installed snapshot is still a hard failure.

The check:affected preflight is unchanged in behavior.

Route-level assertions cover all three contexts (source, packaged,
remote); each was verified to fail against the pre-fix wiring.

* refactor(check): keep stale-install probe worktree-local
2026-08-22 17:01:04 +02:00

40 lines
1.4 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
const LOCKFILE_BASENAME = 'pnpm-lock.yaml';
const INSTALLED_SNAPSHOT_RELATIVE_PATH = ['node_modules', '.pnpm', 'lock.yaml'];
export const STALE_NODE_MODULES_MESSAGE =
"node_modules does not match this worktree's pnpm-lock.yaml";
export type LockfileInstallSyncResult =
| { readonly status: 'no-source-checkout' }
| { readonly status: 'in-sync' }
| {
readonly status: 'out-of-sync';
readonly reason: 'install-missing' | 'stale';
};
/**
* Compares pnpm's installed lockfile snapshot with this worktree's lockfile.
* This is intentionally synchronous and subprocess-free: it runs before any gate.
*/
export function checkLockfileInstallSync(repoRoot: string): LockfileInstallSyncResult {
const lockfile = readFileIfExists(path.join(repoRoot, LOCKFILE_BASENAME));
if (!lockfile) return { status: 'no-source-checkout' };
const installedSnapshot = readFileIfExists(
path.join(repoRoot, ...INSTALLED_SNAPSHOT_RELATIVE_PATH),
);
if (!installedSnapshot) return { status: 'out-of-sync', reason: 'install-missing' };
return lockfile.equals(installedSnapshot)
? { status: 'in-sync' }
: { status: 'out-of-sync', reason: 'stale' };
}
function readFileIfExists(filePath: string): Buffer | undefined {
if (!fs.existsSync(filePath)) return undefined;
return fs.readFileSync(filePath);
}