Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bin/crawlproof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@ async function main(argv: string[]): Promise<number> {
const result = await install(spec);
if (!result.ok) {
process.stderr.write('crawlproof: could not install the dashboard.\n');
// An install that exited 0 and left the wrong version behind is the
// confusing case, so the reason goes out rather than just the failure.
if (result.note) process.stderr.write(` ${result.note}\n`);
return 1;
}
process.stdout.write(`crawlproof: installed with ${result.manager}\n`);
process.stdout.write(
`crawlproof: installed ${result.version ?? ''} with ${result.manager}\n`.replace(' ', ' '),
);
return 0;
}

Expand Down
7 changes: 6 additions & 1 deletion bin/hqtui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,14 @@ async function main(argv: string[]): Promise<number> {
const result = await install(spec);
if (!result.ok) {
process.stderr.write('hqtui: could not install the dashboard.\n');
// An install that exited 0 and left the wrong version behind is the
// confusing case, so the reason goes out rather than just the failure.
if (result.note) process.stderr.write(` ${result.note}\n`);
return 1;
}
process.stdout.write(`hqtui: installed with ${result.manager}\n`);
process.stdout.write(
`hqtui: installed ${result.version ?? ''} with ${result.manager}\n`.replace(' ', ' '),
);
return 0;
}

Expand Down
23 changes: 21 additions & 2 deletions src/crawlproof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { homedir } from 'node:os';
import { join } from 'node:path';
import { onPath, resolveCommand } from './registry.ts';
import { spawnInherit } from './codeburn.ts';
import { delivered, heldBackNote, installedVersion, wantedVersion } from './vendor-verify.ts';

/** The published package, and the executable it installs. */
export const PACKAGE = '@profullstack/crawlproof';
Expand Down Expand Up @@ -161,6 +162,10 @@ export interface InstallResult {
ok: boolean;
manager?: PackageManager;
code?: number | null;
/** What actually landed, when it could be read. */
version?: string;
/** Why an install that exited 0 was not accepted. */
note?: string;
}

/** Install (or refresh) the dashboard in the private prefix. */
Expand All @@ -172,12 +177,26 @@ export async function install(
const root = vendorRoot(env);
prepareVendorDir(root);

// What this install is supposed to produce, asked once rather than per
// manager. Null means the registry was unreachable, and an unverifiable
// install is allowed through: an offline box should still be able to
// reinstall what it already has.
const wanted = await wantedVersion(spec, PACKAGE);
let lastNote: string | undefined;

for (const manager of managers(env)) {
const plan = installPlan(manager, spec);
const code = await run(plan.file, plan.args, root);
if (code === 0) return { ok: true, manager, code };
if (code !== 0) continue;

// Exit 0 is not proof. See src/vendor-verify.ts: pnpm's release-age
// cooldown installs the previous version and reports success.
const got = installedVersion(root, PACKAGE);
if (delivered(got, wanted)) return { ok: true, manager, code, ...(got ? { version: got } : {}) };
lastNote = heldBackNote(manager, got, wanted);
}
return { ok: false };

return { ok: false, ...(lastNote ? { note: lastNote } : {}) };
}

/**
Expand Down
23 changes: 21 additions & 2 deletions src/hqtui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { homedir } from 'node:os';
import { join } from 'node:path';
import { onPath, resolveCommand } from './registry.ts';
import { spawnInherit } from './codeburn.ts';
import { delivered, heldBackNote, installedVersion, wantedVersion } from './vendor-verify.ts';

/** The published package, and the executable it installs. */
export const PACKAGE = '@profullstack/hqtui-demo';
Expand Down Expand Up @@ -173,6 +174,10 @@ export interface InstallResult {
ok: boolean;
manager?: PackageManager;
code?: number | null;
/** What actually landed, when it could be read. */
version?: string;
/** Why an install that exited 0 was not accepted. */
note?: string;
}

/** Install (or refresh) the dashboard in the private prefix. */
Expand All @@ -184,10 +189,24 @@ export async function install(
const root = vendorRoot(env);
prepareVendorDir(root);

// What this install is supposed to produce, asked once rather than per
// manager. Null means the registry was unreachable, and an unverifiable
// install is allowed through: an offline box should still be able to
// reinstall what it already has.
const wanted = await wantedVersion(spec, PACKAGE);
let lastNote: string | undefined;

for (const manager of managers(env)) {
const plan = installPlan(manager, spec);
const code = await run(plan.file, plan.args, root);
if (code === 0) return { ok: true, manager, code };
if (code !== 0) continue;

// Exit 0 is not proof. See src/vendor-verify.ts: pnpm's release-age
// cooldown installs the previous version and reports success.
const got = installedVersion(root, PACKAGE);
if (delivered(got, wanted)) return { ok: true, manager, code, ...(got ? { version: got } : {}) };
lastNote = heldBackNote(manager, got, wanted);
}
return { ok: false };

return { ok: false, ...(lastNote ? { note: lastNote } : {}) };
}
98 changes: 98 additions & 0 deletions src/vendor-verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Did the install actually install anything?
*
* Exit code 0 is not proof. pnpm 11 ships a `minimumReleaseAge` cooldown that
* refuses versions published in the last little while, and it does not fail
* when it refuses one: it resolves to the newest version old enough to pass,
* prints a note about an exclude list, and exits 0. So `pnpm add pkg@latest`
* against a package published ten minutes ago installs the previous release and
* reports success.
*
* Reproduced on 2026-09-06 in an empty directory, pnpm 11.18.0:
*
* registry latest: 0.2.0
* pnpm add @profullstack/crawlproof@latest -> 0.1.0, exit 0
* npm install @profullstack/crawlproof@latest -> 0.2.0
*
* The failure mode is the bad one. Someone runs `update`, is told it worked,
* and keeps hitting the bug it was supposed to fix. So the wrappers ask what
* landed instead of trusting the exit code, and move to the next package
* manager when the answer is the wrong version.
*
* Deliberately not `--config.minimumReleaseAge=0`. The cooldown is a real
* supply-chain protection and turning it off wholesale in a tool that installs
* on other people's machines is a bigger decision than fixing an update.
* Falling through to npm keeps the protection as pnpm's default behaviour and
* still lets a deliberate `update` finish.
*/

import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';

import { run } from './exec.ts';

/** The version actually present in a vendor prefix, or null when nothing is. */
export function installedVersion(root: string, pkg: string): string | null {
const manifest = path.join(root, 'node_modules', ...pkg.split('/'), 'package.json');
if (!existsSync(manifest)) return null;
try {
const version = (JSON.parse(readFileSync(manifest, 'utf8')) as { version?: string }).version;
return typeof version === 'string' && version ? version : null;
} catch {
return null;
}
}

/**
* The exact version a spec asks for, when it names one.
*
* `pkg@1.2.3` is answerable here; `pkg@latest` and `pkg@^1` are not, and
* return null so the caller asks the registry instead of guessing.
*/
export function pinnedVersion(spec: string): string | null {
const at = spec.lastIndexOf('@');
if (at <= 0) return null;
const tag = spec.slice(at + 1);
return /^\d+\.\d+\.\d+/.test(tag) ? tag : null;
}

/** What the registry calls latest, or null when it cannot be reached. */
export async function registryLatest(
pkg: string,
exec: typeof run = run,
): Promise<string | null> {
const result = await exec('npm', ['view', pkg, 'version'], { timeoutMs: 60_000 });
if (result.code !== 0) return null;
const version = result.stdout.trim().split('\n').pop()?.trim() ?? '';
return /^\d+\.\d+\.\d+/.test(version) ? version : null;
}

/**
* The version this install was supposed to produce.
*
* A pinned spec answers itself. `@latest` has to be asked, and when the
* registry cannot be reached the answer is null, which callers must read as
* "cannot verify" rather than as "wrong version" — an offline box should still
* be able to reinstall what it already has.
*/
export async function wantedVersion(
spec: string,
pkg: string,
exec: typeof run = run,
): Promise<string | null> {
return pinnedVersion(spec) ?? (await registryLatest(pkg, exec));
}

/** Whether what landed is what was asked for. Unknown wants pass. */
export function delivered(installed: string | null, wanted: string | null): boolean {
if (wanted === null) return true;
return installed === wanted;
}

/** What to say when a manager reported success and delivered something older. */
export function heldBackNote(manager: string, installed: string | null, wanted: string | null): string {
return (
`${manager} reported success but left ${installed ?? 'nothing'} installed, not ${wanted}. ` +
`pnpm holds back very recent releases; trying the next package manager.`
);
}
113 changes: 113 additions & 0 deletions test/vendor-verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

import {
delivered,
heldBackNote,
installedVersion,
pinnedVersion,
registryLatest,
wantedVersion,
} from '../src/vendor-verify.ts';

function prefixWith(pkg: string, version: string | null): string {
const root = mkdtempSync(path.join(tmpdir(), 'vendor-verify-'));
if (version !== null) {
const dir = path.join(root, 'node_modules', ...pkg.split('/'));
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: pkg, version }));
}
return root;
}

const fakeExec = (stdout: string, code = 0) =>
(async () => ({ code, stdout, stderr: '' })) as never;

describe('installedVersion', () => {
it('reads what is actually on disk', () => {
const root = prefixWith('@profullstack/crawlproof', '0.1.0');
try {
expect(installedVersion(root, '@profullstack/crawlproof')).toBe('0.1.0');
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it('is null when nothing is installed, rather than throwing', () => {
const root = prefixWith('@profullstack/crawlproof', null);
try {
expect(installedVersion(root, '@profullstack/crawlproof')).toBeNull();
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});

describe('pinnedVersion', () => {
it('answers a spec that names a version', () => {
expect(pinnedVersion('@profullstack/crawlproof@0.2.0')).toBe('0.2.0');
expect(pinnedVersion('hqtui-demo@1.2.3')).toBe('1.2.3');
});

it('declines a tag or a range, so the registry is asked instead', () => {
expect(pinnedVersion('@profullstack/crawlproof@latest')).toBeNull();
expect(pinnedVersion('@profullstack/crawlproof@^0.1')).toBeNull();
expect(pinnedVersion('@profullstack/crawlproof')).toBeNull();
});
});

describe('registryLatest', () => {
it('reads the version npm prints', async () => {
expect(await registryLatest('pkg', fakeExec('0.2.0\n'))).toBe('0.2.0');
});

it('is null when npm fails, so an offline box is not told it is wrong', async () => {
expect(await registryLatest('pkg', fakeExec('', 1))).toBeNull();
expect(await registryLatest('pkg', fakeExec('not a version\n'))).toBeNull();
});
});

describe('wantedVersion', () => {
it('prefers the pin and never asks the registry for one', async () => {
const boom = (async () => {
throw new Error('should not be called');
}) as never;
expect(await wantedVersion('pkg@1.4.2', 'pkg', boom)).toBe('1.4.2');
});

it('asks the registry for a tag', async () => {
expect(await wantedVersion('pkg@latest', 'pkg', fakeExec('9.9.9\n'))).toBe('9.9.9');
});
});

describe('delivered', () => {
// The whole point: pnpm 11 exits 0 having installed the previous release.
it('rejects an install that left an older version behind', () => {
expect(delivered('0.1.0', '0.2.0')).toBe(false);
});

it('accepts the version that was asked for', () => {
expect(delivered('0.2.0', '0.2.0')).toBe(true);
});

it('accepts anything when the want could not be determined', () => {
// An unreachable registry must not make a working reinstall look broken.
expect(delivered('0.1.0', null)).toBe(true);
expect(delivered(null, null)).toBe(true);
});

it('rejects an install that produced nothing at all', () => {
expect(delivered(null, '0.2.0')).toBe(false);
});
});

describe('heldBackNote', () => {
it('names the version gap rather than saying it failed', () => {
const note = heldBackNote('pnpm', '0.1.0', '0.2.0');
expect(note).toContain('0.1.0');
expect(note).toContain('0.2.0');
expect(note).toContain('pnpm');
});
});
Loading