mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* CLI: --version reports the npm package version npx impeccable --version printed 3.6.0 after the 4.0.0 release because the shim handed every argument to the engine, whose baked-in CLI_VERSION still said 3.6.0. The shim now answers --version and -v from its own package.json, as docs/CLI-CONTRACT.md specifies, without locating or downloading a binary; a test covers both flags. The engine's CLI_VERSION moves to 4.0.0 for the next engine release, with the cli-version golden re-recorded and the delta noted. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * CLI shim: --version wins whenever it leads, trailing arguments or not Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Code <noreply@anthropic.com>
96 lines
4.2 KiB
JavaScript
Executable File
96 lines
4.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// `impeccable` npm shim: finds the platform binary and execs it with argv.
|
|
// Order: $IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency,
|
|
// the version-pinned user cache (~/.impeccable/bin/<version>/), then a
|
|
// download into that cache from the public release channel.
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const pkg = require('../../package.json');
|
|
const OS = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[process.platform] || process.platform;
|
|
const ARCH = { arm64: 'arm64', x64: 'x64' }[process.arch] || process.arch;
|
|
const TARGET = `${OS}-${ARCH}`;
|
|
const EXE = OS === 'windows' ? 'impeccable.exe' : 'impeccable';
|
|
const PLATFORM_PKG = `@impeccable/cli-${TARGET}`;
|
|
// The engine version travels as the pinned optionalDependency range.
|
|
const VERSION = String(pkg.optionalDependencies?.[PLATFORM_PKG] || Object.values(pkg.optionalDependencies || {})[0] || '').replace(/^[^\d]*/, '');
|
|
const CACHE_ROOT = process.env.IMPECCABLE_HOME || path.join(os.homedir(), '.impeccable');
|
|
const CACHED = path.join(CACHE_ROOT, 'bin', VERSION, EXE);
|
|
const BASE = (process.env.IMPECCABLE_DOWNLOAD_BASE || 'https://github.com/pbakaus/impeccable/releases/download').replace(/\/$/, '');
|
|
const URL = `${BASE}/engine-v${VERSION}/impeccable-${TARGET}${OS === 'windows' ? '.exe' : ''}`;
|
|
|
|
function exists(p) { try { return !!p && fs.statSync(p).isFile(); } catch { return false; } }
|
|
function fromPackage() {
|
|
try { return path.join(path.dirname(require.resolve(`${PLATFORM_PKG}/package.json`)), 'bin', EXE); } catch { return null; }
|
|
}
|
|
async function download() {
|
|
if (!VERSION) return null;
|
|
const res = await fetch(URL, { redirect: 'follow' });
|
|
if (!res.ok) return null;
|
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
// Fail closed, like the skill launcher and `impeccable install`: a sidecar
|
|
// that cannot be fetched, or that carries no hash, refuses the download
|
|
// instead of caching an unverified binary. Nothing is written until the
|
|
// hash matches, so a refusal leaves the cache dir untouched.
|
|
const sum = await fetch(`${URL}.sha256`, { redirect: 'follow' }).then(r => (r.ok ? r.text() : ''), () => '');
|
|
const expected = sum.trim().split(/\s+/)[0].toLowerCase();
|
|
if (!expected) {
|
|
throw new Error(
|
|
`cannot verify ${URL} against ${URL}.sha256 (sidecar unavailable or empty); `
|
|
+ 'refusing the unverified download',
|
|
);
|
|
}
|
|
if (createHash('sha256').update(buf).digest('hex') !== expected) {
|
|
throw new Error(`checksum mismatch downloading ${URL}`);
|
|
}
|
|
fs.mkdirSync(path.dirname(CACHED), { recursive: true });
|
|
const tmp = `${CACHED}.part.${process.pid}`;
|
|
try {
|
|
fs.writeFileSync(tmp, buf, { mode: 0o755 });
|
|
fs.renameSync(tmp, CACHED);
|
|
} catch (err) {
|
|
try { fs.rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
throw err;
|
|
}
|
|
return CACHED;
|
|
}
|
|
async function locate() {
|
|
const envBin = process.env.IMPECCABLE_BIN;
|
|
if (exists(envBin)) return envBin;
|
|
const fromPkg = fromPackage();
|
|
if (exists(fromPkg)) return fromPkg;
|
|
if (exists(CACHED)) return CACHED;
|
|
return download().catch((err) => { process.stderr.write(`impeccable: ${err.message}\n`); return null; });
|
|
}
|
|
|
|
// `--version` / `-v` is answered by the shim itself: the number users mean
|
|
// is this npm package's version, not the engine's (docs/CLI-CONTRACT.md).
|
|
const argv = process.argv.slice(2);
|
|
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
process.stdout.write(`${pkg.version}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const bin = await locate();
|
|
if (!bin) {
|
|
process.stderr.write(
|
|
`impeccable: no binary for ${TARGET}. Install ${PLATFORM_PKG}@${VERSION}, set IMPECCABLE_BIN, `
|
|
+ `or download impeccable-${TARGET} v${VERSION} from ${BASE} into ${CACHED}.\n`,
|
|
);
|
|
process.exit(127);
|
|
}
|
|
const result = spawnSync(bin, argv, {
|
|
stdio: 'inherit',
|
|
env: { IMPECCABLE_SELF: 'npx impeccable', ...process.env },
|
|
});
|
|
if (result.error) {
|
|
process.stderr.write(`impeccable: failed to run ${bin}: ${result.error.message}\n`);
|
|
process.exit(127);
|
|
}
|
|
process.exit(result.status === null ? 1 : result.status);
|