#!/usr/bin/env bun // The CLI key: one hash of everything that decides how the packed npm CLI behaves in the acceptance matrix // (tools/acceptance.ts), with the version copies normalized. Two commits with the same key run the same CLI code; a // version bump (tools/desktop-version.ts --set) does not change it. Acceptance results carry the key, so the results // of a commit count for a version-only child (tools/release-candidate.ts) and the Mac reuses its cached macOS results // (tools/release.ts) until the CLI code really changes. // bun tools/cli-key.ts print the key of the working tree (tracked files only) // In the key: the npm package's files (package.json "files") without the prose that ships with it (CHANGELOG.md, // README.md, docs/*.md: the CLI never reads storebound's own docs), package.json, bun.lock and what the acceptance // run itself uses (tools/acceptance.ts, tools/build-node.ts, tests/helpers, tests/build-fixtures.ts, tests/fixtures). import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { existsSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { COPIES } from './desktop-version'; const ROOT = resolve(import.meta.dir, '..'); const PROSE = /^(CHANGELOG\.md|README\.md|docs\/[^/]+\.md)$/; const EXTRA = ['package.json', 'bun.lock', 'tools/acceptance.ts', 'tools/build-node.ts', 'tools/cli-key.ts', 'tools/desktop-version.ts', 'tests/helpers', 'tests/build-fixtures.ts', 'tests/fixtures']; const NORMAL = '0.0.0-cli-key'; /** The paths of the key (git pathspecs). Pure. */ export function keyPaths(pkgFiles: string[]): string[] { return [...new Set([...pkgFiles.filter(f => f !== 'dist/storebound.mjs'), ...EXTRA])].sort(); } /** One file's content in the key: the version copies with the version normalized. Pure. */ export function keyContent(file: string, text: Buffer): Buffer { const copy = COPIES.find(c => c.file === file); return copy ? Buffer.from(copy.write(text.toString('utf8'), NORMAL)) : text; } /** sha256 over (path, content) of every tracked file of the key, sorted. Pure given the reader. */ export function cliKeyOf(files: string[], read: (f: string) => Buffer): string { const h = createHash('sha256'); for (const f of [...files].filter(f => !PROSE.test(f)).sort()) { h.update(`${f}\0`); h.update(keyContent(f, read(f))); h.update('\0'); } return h.digest('hex').slice(0, 32); } export function cliKey(root = ROOT): string { const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { files: string[] }; const ls = spawnSync('git', ['ls-files', '-z', '--', ...keyPaths(pkg.files)], { cwd: root, encoding: 'utf8', maxBuffer: 64 << 20 }); if (ls.status !== 0) throw new Error(`git ls-files: ${ls.stderr}`); const files = ls.stdout.split('\0').filter(f => f && existsSync(join(root, f))); return cliKeyOf(files, f => readFileSync(join(root, f))); } if (import.meta.main) console.log(cliKey());