mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 09:06:53 +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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user