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:
Paul Bakaus
2026-08-31 19:57:42 -07:00
parent 11a1ea64a6
commit 6dd16d21ef
34 changed files with 197 additions and 27746 deletions
+65 -95
View File
@@ -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);
-355
View File
@@ -1,355 +0,0 @@
import path from 'node:path';
import {
getConfigPath,
getLocalConfigPath,
normalizeIgnoreValue,
readDetectionConfig,
readRawDetectionConfig,
writeDetectionConfig,
extractFindingIgnoreValue,
} from '../../lib/impeccable-config.mjs';
const ACTION_ALIASES = new Map([
['status', 'list'],
['ls', 'list'],
['list', 'list'],
['add-rule', 'add-rule'],
['ignore-rule', 'add-rule'],
['add-file', 'add-file'],
['ignore-file', 'add-file'],
['add-value', 'add-value'],
['ignore-value', 'add-value'],
['update-value', 'add-value'],
['remove-rule', 'remove-rule'],
['rm-rule', 'remove-rule'],
['remove-file', 'remove-file'],
['rm-file', 'remove-file'],
['remove-value', 'remove-value'],
['rm-value', 'remove-value'],
['clear', 'clear'],
]);
function printUsage() {
console.log(`Usage: impeccable ignores <action> [options]
Manage detector ignores in .impeccable config.
Actions:
list Show merged, shared, and local ignores
add-rule <rule> [--all-values] Ignore a rule
add-file <glob> Ignore files by glob
add-value <rule> <value> Ignore one rule/value pair
remove-rule <rule> Remove a rule ignore
remove-file <glob> Remove a file ignore
remove-value <rule> <value> Remove a rule/value ignore
clear Clear detector ignores in the selected scope
Scope:
--shared Write .impeccable/config.json (default)
--local Write .impeccable/config.local.json
--all For remove/clear, apply to shared and local
Value options:
--file <glob> Scope add-value/remove-value to a file glob
--reason <text> Store or update a reason on add-value
Examples:
impeccable ignores add-file "src/legacy/**"
impeccable ignores add-value overused-font Inter --reason "Brand font"
impeccable ignores add-value design-system-color "*" --file "src/demo.css"
impeccable ignores remove-value overused-font Inter`);
}
function parseScope(args, { allowAll = false } = {}) {
const rest = [];
let local = false;
let shared = false;
let all = false;
for (const arg of args) {
if (arg === '--local') local = true;
else if (arg === '--shared') shared = true;
else if (arg === '--all') all = true;
else rest.push(arg);
}
if ([local, shared, all].filter(Boolean).length > 1) {
throw new Error(`Pass only one scope flag: --shared${allowAll ? ', --local, or --all' : ' or --local'}`);
}
if (all && !allowAll) throw new Error('--all is only supported for remove and clear actions');
return { local, all, rest };
}
// An empty glob used to be dropped by filter(Boolean), so `--file=` reported
// success and wrote an entry with no files: the user asked to scope a rule to one
// file and silently got the project-wide suppression instead. Refuse it.
function requireGlob(raw, flag) {
const glob = String(raw ?? '').trim();
if (!glob) throw new Error(`${flag} requires a non-empty glob`);
// A following flag is not a glob. `--file --reason "why"` consumed `--reason`
// as the scope and left the reason text to fold into the value, storing
// value="* why" files=["--reason"] and reporting success. Same silent-no-op
// class as an unknown flag folding into the value; refuse it the same way.
if (glob.startsWith('--')) throw new Error(`${flag} requires a glob, got the flag ${glob}`);
return glob;
}
function parseValueArgs(args, { allowUnscopedWildcard = false } = {}) {
const positionals = [];
const files = [];
let reason = '';
for (let i = 0; i < args.length; i++) {
const arg = String(args[i] || '');
if (arg === '--reason') {
const chunks = [];
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) chunks.push(args[++i]);
reason = chunks.join(' ').trim();
} else if (arg.startsWith('--reason=')) {
reason = arg.slice('--reason='.length).trim();
} else if (arg === '--file' || arg === '--files') {
if (i + 1 >= args.length) throw new Error(`${arg} requires a glob`);
files.push(requireGlob(args[++i], arg));
} else if (arg.startsWith('--file=')) {
files.push(requireGlob(arg.slice('--file='.length), '--file'));
} else if (arg.startsWith('--files=')) {
files.push(requireGlob(arg.slice('--files='.length), '--files'));
} else if (arg.startsWith('--')) {
throw new Error(`Unknown add-value flag: ${arg}`);
} else {
positionals.push(arg);
}
}
const [rule, ...valueParts] = positionals;
const value = normalizeIgnoreValue(valueParts.join(' '));
if (!rule || !value) throw new Error('Pass a rule id and value, e.g. impeccable ignores add-value overused-font Inter');
// Sorted: the dedup key compares the files array, so an unsorted scope made
// `--file b.css --file a.css` a different entry from `--file a.css --file b.css`.
const scopedFiles = Array.from(new Set(files.filter(Boolean))).sort();
if (value === '*' && scopedFiles.length === 0 && !allowUnscopedWildcard) {
throw new Error('Wildcard value ignores must be scoped with --file <glob>.');
}
return {
rule: String(rule).trim().toLowerCase(),
value,
files: scopedFiles,
reason,
};
}
function formatValues(values) {
if (!values.length) return '(none)';
return values
.map((entry) => {
const fileSuffix = Array.isArray(entry.files) && entry.files.length
? ` [${entry.files.join(', ')}]`
: '';
const reasonSuffix = entry.reason ? ` - ${entry.reason}` : '';
return `${entry.rule}=${entry.value}${fileSuffix}${reasonSuffix}`;
})
.join(', ');
}
function formatConfig(label, config) {
return [
`${label}:`,
` ignoreRules: ${config.ignoreRules.length ? config.ignoreRules.join(', ') : '(none)'}`,
` ignoreFiles: ${config.ignoreFiles.length ? config.ignoreFiles.join(', ') : '(none)'}`,
` ignoreValues: ${formatValues(config.ignoreValues)}`,
` designSystem: ${config.designSystem?.enabled === false ? 'disabled' : 'enabled'}`,
].join('\n');
}
function list(cwd) {
const merged = readDetectionConfig(cwd);
const shared = readRawDetectionConfig(cwd);
const local = readRawDetectionConfig(cwd, { local: true });
return [
'Impeccable detector ignores',
` shared file: ${path.relative(cwd, getConfigPath(cwd)) || getConfigPath(cwd)}`,
` local file: ${path.relative(cwd, getLocalConfigPath(cwd)) || getLocalConfigPath(cwd)}`,
'',
formatConfig('Merged', merged),
'',
formatConfig('Shared', shared),
'',
formatConfig('Local', local),
].join('\n');
}
function readScopeConfig(cwd, local) {
return readRawDetectionConfig(cwd, { local });
}
function writeScopeConfig(cwd, config, local) {
return writeDetectionConfig(cwd, config, { local });
}
function parseRuleArgs(args) {
const positionals = [];
let allValues = false;
for (let i = 0; i < args.length; i++) {
const arg = String(args[i] || '');
if (arg === '--all-values') {
allValues = true;
} else if (arg === '--reason') {
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) i++;
} else if (arg.startsWith('--reason=')) {
// Accepted for symmetry with add-value; ignoreRules stores ids only.
} else if (arg.startsWith('--')) {
throw new Error(`Unknown add-rule flag: ${arg}`);
} else {
positionals.push(arg);
}
}
return {
rule: String(positionals[0] || '').trim().toLowerCase(),
allValues,
};
}
function addRule(cwd, args) {
const { local, rest } = parseScope(args);
const { rule, allValues } = parseRuleArgs(rest);
if (!rule) throw new Error('Pass a rule id, e.g. impeccable ignores add-rule side-tab');
if (rule === 'overused-font' && !allValues) {
throw new Error('overused-font is value-specific by default. Use add-value overused-font <font>, or add-rule overused-font --all-values for broad suppression.');
}
const config = readScopeConfig(cwd, local);
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
const target = writeScopeConfig(cwd, config, local);
return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target) || target}).`;
}
function addFile(cwd, args) {
const { local, rest } = parseScope(args);
const glob = String(rest[0] || '').trim();
if (!glob) throw new Error('Pass a glob, e.g. impeccable ignores add-file "src/legacy/**"');
const config = readScopeConfig(cwd, local);
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
const target = writeScopeConfig(cwd, config, local);
return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target) || target}).`;
}
function addValue(cwd, args) {
const { local, rest } = parseScope(args);
const parsed = parseValueArgs(rest);
if (parsed.value !== '*' && !extractFindingIgnoreValue({ antipattern: parsed.rule, ignoreValue: parsed.value })) {
throw new Error(`${parsed.rule} has no extractable ignore value. Use impeccable ignores add-value ${parsed.rule} "*" --file <glob> to suppress it in matching files.`);
}
const config = readScopeConfig(cwd, local);
const key = ignoreValueKey(parsed);
const existing = config.ignoreValues.find((entry) => ignoreValueKey(entry) === key);
if (existing) {
if (parsed.reason) existing.reason = parsed.reason;
if (parsed.files.length) existing.files = parsed.files;
} else {
// rule, value, files, createdAt, reason — the same order the normalizers emit,
// so a fresh entry survives the next write untouched.
const entry = {
rule: parsed.rule,
value: parsed.value,
};
if (parsed.files.length) entry.files = parsed.files;
entry.createdAt = new Date().toISOString();
if (parsed.reason) entry.reason = parsed.reason;
config.ignoreValues.push(entry);
}
const target = writeScopeConfig(cwd, config, local);
return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target) || target}).`;
}
function removeFromScopes(cwd, args, remover) {
const { local, all, rest } = parseScope(args, { allowAll: true });
const scopes = all ? [false, true] : [local];
const removed = [];
for (const isLocal of scopes) {
const config = readScopeConfig(cwd, isLocal);
const count = remover(config, rest);
if (count > 0) {
const target = writeScopeConfig(cwd, config, isLocal);
removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target) || target})`);
}
}
return removed.length ? `Removed ${removed.join(', ')}.` : 'No matching detector ignore found.';
}
function removeRule(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const rule = String(rest[0] || '').trim().toLowerCase();
if (!rule) throw new Error('Pass a rule id, e.g. impeccable ignores remove-rule side-tab');
const before = config.ignoreRules.length;
config.ignoreRules = config.ignoreRules.filter((entry) => entry !== rule);
return before - config.ignoreRules.length;
});
}
function removeFile(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const glob = String(rest[0] || '').trim();
if (!glob) throw new Error('Pass a glob, e.g. impeccable ignores remove-file "src/legacy/**"');
const before = config.ignoreFiles.length;
config.ignoreFiles = config.ignoreFiles.filter((entry) => entry !== glob);
return before - config.ignoreFiles.length;
});
}
function removeValue(cwd, args) {
return removeFromScopes(cwd, args, (config, rest) => {
const parsed = parseValueArgs(rest, { allowUnscopedWildcard: true });
const key = ignoreValueKey(parsed);
const before = config.ignoreValues.length;
config.ignoreValues = config.ignoreValues.filter((entry) => ignoreValueKey(entry) !== key);
return before - config.ignoreValues.length;
});
}
function clear(cwd, args) {
const { local, all, rest } = parseScope(args, { allowAll: true });
if (rest.length > 0) throw new Error('clear does not take positional arguments');
const scopes = all ? [false, true] : [local];
for (const isLocal of scopes) {
const config = readScopeConfig(cwd, isLocal);
config.ignoreRules = [];
config.ignoreFiles = [];
config.ignoreValues = [];
writeScopeConfig(cwd, config, isLocal);
}
return `Cleared detector ignores in ${all ? 'shared and local config' : local ? 'local config' : 'shared config'}.`;
}
function ignoreValueKey(entry) {
// Sorted: a file scope is a set. Comparing stored order made an on-disk scope
// miss the sorted argv form, so a re-add duplicated the entry and a remove
// silently failed. Every key that hashes `files` must sort — there are four.
const files = Array.isArray(entry.files) && entry.files.length ? [...entry.files].sort().join('\x1f') : '';
return `${String(entry.rule || '').trim().toLowerCase()}\0${normalizeIgnoreValue(entry.value)}\0${files}`;
}
export async function run(args = [], opts = {}) {
const cwd = opts.cwd || process.cwd();
const actionArg = args[0] || 'list';
if (actionArg === '--help' || actionArg === '-h') {
printUsage();
return;
}
const action = ACTION_ALIASES.get(String(actionArg).toLowerCase());
if (!action) {
throw new Error(`Unknown ignores action: ${actionArg}. Run "impeccable ignores --help".`);
}
const rest = args.slice(1);
let out;
switch (action) {
case 'list': out = list(cwd); break;
case 'add-rule': out = addRule(cwd, rest); break;
case 'add-file': out = addFile(cwd, rest); break;
case 'add-value': out = addValue(cwd, rest); break;
case 'remove-rule': out = removeRule(cwd, rest); break;
case 'remove-file': out = removeFile(cwd, rest); break;
case 'remove-value': out = removeValue(cwd, rest); break;
case 'clear': out = clear(cwd, rest); break;
}
if (out) console.log(out);
}
File diff suppressed because it is too large Load Diff