mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
CLI: turn the impeccable npm package into a platform-binary shim
cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN, the @impeccable/cli-<os>-<arch> optional dependency (templates under cli/platform-packages/, published by the engine release), the ~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and execs it. package.json drops the engine dependencies and the library exports; puppeteer moves to devDependencies for the icon scripts. README.npm.md describes the shim. Prepared with AI assistance (Claude Code).
This commit is contained in:
+65
-95
@@ -1,102 +1,72 @@
|
||||
#!/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';
|
||||
|
||||
/**
|
||||
* Impeccable CLI
|
||||
*
|
||||
* Usage:
|
||||
* npx impeccable detect [file-or-dir-or-url...]
|
||||
* npx impeccable ignores <list|add-file|add-value|remove-...>
|
||||
* npx impeccable help|install|update
|
||||
* npx impeccable --help
|
||||
*/
|
||||
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/renaissance-geek-inc/impeccable-dist/releases/download').replace(/\/$/, '');
|
||||
const URL = `${BASE}/v${VERSION}/impeccable-${TARGET}${OS === 'windows' ? '.exe' : ''}`;
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SKILL_COMMANDS = new Set(['help', 'install', 'link', 'update', 'check']);
|
||||
|
||||
// Is this a detect target (the `npx impeccable src/` shorthand) or a mistyped
|
||||
// command? Flags, URLs, path-shaped args, and real files/dirs (e.g. an
|
||||
// extension-less `Dockerfile`) are targets; anything else is an unknown command.
|
||||
function looksLikeDetectTarget(arg) {
|
||||
const isFlag = arg.startsWith('-');
|
||||
const isUrl = /^https?:\/\//i.test(arg);
|
||||
const isPathShaped = arg.includes('/') || arg.includes('\\') || arg.includes('.');
|
||||
const isExistingPath = existsSync(resolve(arg));
|
||||
return isFlag || isUrl || isPathShaped || isExistingPath;
|
||||
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());
|
||||
const sum = await fetch(`${URL}.sha256`, { redirect: 'follow' }).then(r => (r.ok ? r.text() : ''), () => '');
|
||||
const expected = sum.trim().split(/\s+/)[0];
|
||||
if (expected && 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}`;
|
||||
fs.writeFileSync(tmp, buf, { mode: 0o755 });
|
||||
fs.renameSync(tmp, CACHED);
|
||||
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; });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === '--help' || command === '-h') {
|
||||
console.log(`Usage: impeccable <command> [options]
|
||||
|
||||
Commands:
|
||||
detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues
|
||||
ignores Manage detector ignore rules, files, and values
|
||||
help List all available skills and commands
|
||||
install Install impeccable skills into your project or global harness
|
||||
link Symlink skills from a local checkout or submodule
|
||||
update Update skills to the latest version
|
||||
check Check if skill updates are available
|
||||
|
||||
Options:
|
||||
--help Show this help message
|
||||
--version Show version number
|
||||
|
||||
Compatibility:
|
||||
impeccable skills <command> Legacy namespace; still supported.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (command === '--version' || command === '-v') {
|
||||
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
|
||||
console.log(pkg.version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (command === 'detect') {
|
||||
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
||||
const { detectCli } = await import('../engine/detect-antipatterns.mjs');
|
||||
await detectCli();
|
||||
} else if (command === 'ignores' || command === 'ignore') {
|
||||
const { run } = await import('./commands/ignores.mjs');
|
||||
await run(args.slice(1));
|
||||
} else if (command === 'skills') {
|
||||
const { run } = await import('./commands/skills.mjs');
|
||||
await run(args.slice(1));
|
||||
} else if (SKILL_COMMANDS.has(command)) {
|
||||
const { run } = await import('./commands/skills.mjs');
|
||||
await run(args);
|
||||
} else if (looksLikeDetectTarget(command)) {
|
||||
// Default: treat as detect arguments (allow `npx impeccable src/` shorthand)
|
||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
||||
const { detectCli } = await import('../engine/detect-antipatterns.mjs');
|
||||
await detectCli();
|
||||
} else if (command === 'init') {
|
||||
// The follow-up mistake from issue #472: `/impeccable init` belongs in an AI
|
||||
// coding agent's chat, and a user who typed it into their shell is likely to
|
||||
// retry it here as `npx impeccable init`.
|
||||
console.error(`"init" is not a CLI command. Type /impeccable init in your AI coding agent's chat (Claude Code, Cursor, Codex, ...), not in this terminal.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
// An unknown bareword: a mistyped command (or an old cached version run
|
||||
// against newer docs). Fail loudly instead of silently statting it as a path.
|
||||
console.error(`Unknown command: "${command}"\n\nTo see a list of supported commands, run:\n impeccable --help`);
|
||||
process.exit(1);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
if (error?.code === 'IMPECCABLE_PROMPT_ABORT') {
|
||||
console.log('\nAborted.');
|
||||
process.exit(130);
|
||||
}
|
||||
|
||||
console.error(error?.message || error);
|
||||
process.exit(1);
|
||||
const result = spawnSync(bin, process.argv.slice(2), {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user