Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/hook-admin.mjs
1#!/usr/bin/env node2/**3* `/impeccable hooks <on|off|status|reset>` — manage the design hook runtime4* via the `hook` key and shared detector ignores via the `detector` key in5* .impeccable/config.json / .impeccable/config.local.json.6*7* Usage:8* node hook-admin.mjs status # print current state9* node hook-admin.mjs on # set enabled: true10* node hook-admin.mjs off # set enabled: false11* node hook-admin.mjs ignore-rule <rule-id> # append to ignoreRules12* node hook-admin.mjs ignore-rule overused-font --all-values13* node hook-admin.mjs ignore-file <glob> # append to ignoreFiles14* node hook-admin.mjs ignore-value <rule> <value> # append to shared ignoreValues15* node hook-admin.mjs ignore-value <rule> <value> --local16* node hook-admin.mjs reset # remove all config + cache17*18* Designed to be invoked by the LLM from the reference/hooks.md flow.19* Output is human-readable; the harness will pass it back to the user.20*/2122import fs from 'node:fs';23import path from 'node:path';2425import {26getConfigPath,27getLocalConfigPath,28getCachePath,29getPendingPath,30readConfig,31DEFAULT_CONFIG,32ensureHookGitExcludes,33normalizeIgnoreValue,34normalizeIgnoreValueEntries,35} from './hook-lib.mjs';3637const ACTIONS = new Set(['status', 'on', 'off', 'ignore-rule', 'ignore-file', 'ignore-value', 'reset']);38const IMPECCABLE_HOOK_COMMAND_MARKERS = [39'skills/impeccable/scripts/hook-probe.mjs',40'skills/impeccable/scripts/hook.mjs',41'skills/impeccable/scripts/hook-before-edit.mjs',42'skills/impeccable/scripts/hook-after-edit.mjs',43'skills/impeccable/scripts/hook-stop.mjs',44];45const TIMEOUT_SECONDS = 5;46const STATUS_MESSAGE = 'Checking UI changes';4748const HOOK_MANIFEST_TARGETS = [49{50provider: '.claude',51skillRel: '.claude/skills/impeccable',52destRel: '.claude/settings.local.json',53sharedDestRel: '.claude/settings.json',54manifest: () => ({55description: 'Impeccable design detector: runs after Edit/Write/MultiEdit on UI files and surfaces findings as system reminders.',56hooks: {57PostToolUse: [58{59matcher: 'Edit|Write|MultiEdit',60hooks: [61{62type: 'command',63command: 'node "${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"',64timeout: TIMEOUT_SECONDS,65statusMessage: STATUS_MESSAGE,66},67],68},69],70},71}),72},73{74provider: '.agents',75skillRel: '.agents/skills/impeccable',76destRel: '.codex/hooks.json',77manifest: () => ({78description: 'Impeccable design detector: runs after Edit/Write/apply_patch on UI files and surfaces findings as system reminders.',79hooks: {80PostToolUse: [81{82matcher: 'Edit|Write|apply_patch',83hooks: [84{85type: 'command',86command: 'node "$(git rev-parse --show-toplevel)/.agents/skills/impeccable/scripts/hook.mjs"',87timeout: TIMEOUT_SECONDS,88statusMessage: STATUS_MESSAGE,89},90],91},92],93},94}),95},96{97provider: '.cursor',98skillRel: '.cursor/skills/impeccable',99destRel: '.cursor/hooks.json',100manifest: () => ({101version: 1,102hooks: {103preToolUse: [104{105command: 'node ".cursor/skills/impeccable/scripts/hook-before-edit.mjs"',106timeout: TIMEOUT_SECONDS,107},108],109},110}),111},112{113// GitHub Copilot reads repo-level hooks from `.github/hooks/*.json`. The same114// manifest is honored by the CLI (once committed to the default branch) and115// the cloud/app agent. Schema differs: lowercase `postToolUse`, flat entries,116// `bash`/`timeoutSec`, and a `matcher` regex against the `edit`/`create` tools.117provider: '.github',118skillRel: '.github/skills/impeccable',119destRel: '.github/hooks/impeccable.json',120manifest: () => ({121version: 1,122hooks: {123postToolUse: [124{125type: 'command',126matcher: 'edit|create|apply_patch',127bash: 'node "$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs"',128timeoutSec: TIMEOUT_SECONDS,129},130],131},132}),133},134];135136function readRawConfigFile(filePath) {137if (!fs.existsSync(filePath)) return { exists: false, malformed: false, raw: null };138try {139return { exists: true, malformed: false, raw: JSON.parse(fs.readFileSync(filePath, 'utf-8')) };140} catch {141return { exists: true, malformed: true, raw: null };142}143}144145const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem']);146147function hookSection(unified) {148return unified && typeof unified === 'object' && !Array.isArray(unified) && unified.hook && typeof unified.hook === 'object' && !Array.isArray(unified.hook)149? unified.hook150: null;151}152153function detectorSection(unified) {154return unified && typeof unified === 'object' && !Array.isArray(unified) && unified.detector && typeof unified.detector === 'object' && !Array.isArray(unified.detector)155? unified.detector156: null;157}158159function readRawHookConfig(cwd, opts = {}) {160const unified = readRawConfigFile(opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd)).raw;161return hookSection(unified);162}163164function readRawDetectorConfig(cwd, opts = {}) {165const unified = readRawConfigFile(opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd)).raw;166const merged = mergeDetectorConfig(hookSection(unified));167return mergeDetectorConfig(detectorSection(unified), merged);168}169170function stripDetectorKeys(raw) {171if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};172const out = {};173for (const [key, value] of Object.entries(raw)) {174if (!DETECTOR_CONFIG_KEYS.has(key)) out[key] = value;175}176return out;177}178179// Write hook runtime config under `hook`, leaving detector filters in180// `detector` and preserving sibling keys such as updateCheck.181function writeHookConfig(cwd, hookConfig, opts = {}) {182const filePath = opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd);183if (opts.local) ensureHookGitExcludes(cwd);184const existingRaw = readRawConfigFile(filePath).raw;185const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};186const existingHook = stripDetectorKeys(hookSection(existing));187// Merge over the existing hook object so fields the merge helpers don't manage188// (consent, quiet, auditLog) survive a `/impeccable hooks` edit.189const next = { ...existing, hook: { ...existingHook, ...hookConfig } };190fs.mkdirSync(path.dirname(filePath), { recursive: true });191fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');192return filePath;193}194195function writeDetectorConfig(cwd, detectorConfig, opts = {}) {196const filePath = opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd);197if (opts.local) ensureHookGitExcludes(cwd);198const existingRaw = readRawConfigFile(filePath).raw;199const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};200const nextHook = stripDetectorKeys(hookSection(existing));201const existingDetector = mergeDetectorConfig(detectorSection(existing));202const next = {203...existing,204detector: mergeDetectorConfig(detectorConfig, existingDetector),205};206if (Object.keys(nextHook).length > 0) next.hook = nextHook;207else delete next.hook;208fs.mkdirSync(path.dirname(filePath), { recursive: true });209fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');210return filePath;211}212213function mergeHookConfig(existing) {214const base = existing && typeof existing === 'object' ? existing : {};215return {216enabled: base.enabled === false ? false : true,217limits: {218maxFindings: Number.isFinite(base?.limits?.maxFindings) ? base.limits.maxFindings : DEFAULT_CONFIG.limits.maxFindings,219maxChars: Number.isFinite(base?.limits?.maxChars) ? base.limits.maxChars : DEFAULT_CONFIG.limits.maxChars,220},221};222}223224function mergeDetectorConfig(existing, seed = null) {225const base = existing && typeof existing === 'object' ? existing : {};226const out = seed ? {227ignoreRules: [...seed.ignoreRules],228ignoreFiles: [...seed.ignoreFiles],229ignoreValues: normalizeIgnoreValueEntries(seed.ignoreValues),230} : {231ignoreRules: [],232ignoreFiles: [],233ignoreValues: [],234};235if (seed?.designSystem && typeof seed.designSystem === 'object' && !Array.isArray(seed.designSystem)) {236out.designSystem = { ...seed.designSystem };237}238if (base.designSystem && typeof base.designSystem === 'object' && !Array.isArray(base.designSystem)) {239out.designSystem = {240...(out.designSystem || {}),241enabled: base.designSystem.enabled === false ? false : true,242};243}244if (Array.isArray(base.ignoreRules)) {245out.ignoreRules = Array.from(new Set([...out.ignoreRules, ...base.ignoreRules.map(String)]));246}247if (Array.isArray(base.ignoreFiles)) {248out.ignoreFiles = Array.from(new Set([...out.ignoreFiles, ...base.ignoreFiles.map(String)]));249}250if (Array.isArray(base.ignoreValues)) {251out.ignoreValues = mergeIgnoreValueEntries(out.ignoreValues, base.ignoreValues);252}253return out;254}255256function mergeIgnoreValueEntries(existing, incoming) {257const map = new Map();258for (const entry of normalizeIgnoreValueEntries(existing)) {259map.set(ignoreValueEntryKey(entry), entry);260}261for (const entry of normalizeIgnoreValueEntries(incoming)) {262map.set(ignoreValueEntryKey(entry), entry);263}264return Array.from(map.values());265}266267function ignoreValueEntryKey(entry) {268const files = Array.isArray(entry.files) && entry.files.length > 0 ? entry.files.join('\x1f') : '';269return `${entry.rule}\0${entry.value}\0${files}`;270}271272function statusReport(cwd) {273const shared = readRawConfigFile(getConfigPath(cwd));274const local = readRawConfigFile(getLocalConfigPath(cwd));275const cfg = readConfig(cwd);276const envKill = process.env.IMPECCABLE_HOOK_DISABLED;277const envState = envKill ? `IMPECCABLE_HOOK_DISABLED=${envKill}` : 'unset';278const cfgPath = path.relative(cwd, getConfigPath(cwd)) || '.impeccable/config.json';279const localPath = path.relative(cwd, getLocalConfigPath(cwd)) || '.impeccable/config.local.json';280const cachePath = path.relative(cwd, getCachePath(cwd)) || '.impeccable/hook.cache.json';281const fileState = (info, relPath, absent) => {282if (info.malformed) return `${relPath} (malformed; ignored)`;283if (info.exists) return relPath;284return `${relPath} (${absent})`;285};286const ignoreValues = cfg.ignoreValues.map((entry) => `${entry.rule}=${entry.value}`);287288const lines = [289`Impeccable design hook`,290` state: ${cfg.enabled ? 'enabled' : 'disabled'}`,291` shared file: ${fileState(shared, cfgPath, 'using defaults; file not present')}`,292` local file: ${fileState(local, localPath, 'not present')}`,293` ignoreRules: ${cfg.ignoreRules.length ? cfg.ignoreRules.join(', ') : '(none)'}`,294` ignoreFiles: ${cfg.ignoreFiles.length ? cfg.ignoreFiles.join(', ') : '(none)'}`,295` ignoreValues: ${ignoreValues.length ? ignoreValues.join(', ') : '(none)'}`,296` maxFindings: ${cfg.limits.maxFindings}`,297` maxChars: ${cfg.limits.maxChars}`,298` env override: ${envState}`,299` cache file: ${fs.existsSync(getCachePath(cwd)) ? cachePath : `${cachePath} (not present)`}`,300];301return lines.join('\n');302}303304function setEnabled(cwd, value) {305const config = mergeHookConfig(readRawHookConfig(cwd));306config.enabled = value;307const target = writeHookConfig(cwd, config);308if (!value) {309return `Design hook disabled for this project (wrote ${path.relative(cwd, target) || target}).`;310}311312const localTarget = writeHookConfig(cwd, { consent: 'accepted' }, { local: true });313const repaired = repairHookManifests(cwd);314const parts = [315`Design hook enabled for this project (wrote ${path.relative(cwd, target) || target}).`,316`Recorded local hook consent in ${path.relative(cwd, localTarget) || localTarget}.`,317];318if (repaired.written.length > 0) {319parts.push(`Installed or repaired hook manifests for: ${repaired.written.join(', ')}.`);320} else if (repaired.already.length > 0) {321parts.push(`Hook manifests already installed for: ${repaired.already.join(', ')}.`);322} else {323parts.push('No installed provider skill folders found to repair.');324}325if (repaired.backups.length > 0) {326parts.push(`Backed up malformed manifest(s): ${repaired.backups.map((filePath) => path.relative(cwd, filePath) || filePath).join(', ')}.`);327}328return parts.join(' ');329}330331function repairHookManifests(cwd) {332const result = { written: [], already: [], backups: [] };333for (const target of HOOK_MANIFEST_TARGETS) {334if (!fs.existsSync(path.join(cwd, target.skillRel))) continue;335const dest = path.join(cwd, target.destRel);336const sharedDest = target.sharedDestRel ? path.join(cwd, target.sharedDestRel) : null;337338if (sharedDest && fileHasImpeccableHookMarker(sharedDest)) {339pruneImpeccableHookFromManifest(dest);340result.already.push(target.provider);341continue;342}343344const fresh = target.manifest();345let next = fresh;346if (fs.existsSync(dest)) {347try {348next = mergeHookManifests(JSON.parse(fs.readFileSync(dest, 'utf-8')), fresh);349} catch {350const backup = `${dest}.bak`;351fs.copyFileSync(dest, backup);352result.backups.push(backup);353}354}355356const serialized = `${JSON.stringify(next, null, 2)}\n`;357const current = fs.existsSync(dest) ? safeReadText(dest) : null;358if (current === serialized) {359result.already.push(target.provider);360continue;361}362fs.mkdirSync(path.dirname(dest), { recursive: true });363fs.writeFileSync(dest, serialized);364result.written.push(target.provider);365}366return result;367}368369function safeReadText(filePath) {370try {371return fs.readFileSync(filePath, 'utf-8');372} catch {373return null;374}375}376377function mergeHookManifests(existing, fresh) {378const existingObject = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {};379const freshObject = fresh && typeof fresh === 'object' && !Array.isArray(fresh) ? fresh : {};380const existingHooks = existingObject.hooks && typeof existingObject.hooks === 'object' && !Array.isArray(existingObject.hooks)381? existingObject.hooks382: {};383const freshHooks = freshObject.hooks && typeof freshObject.hooks === 'object' && !Array.isArray(freshObject.hooks)384? freshObject.hooks385: {};386387const merged = { ...existingObject, hooks: {} };388if (freshObject.version !== undefined) merged.version = freshObject.version;389if (freshObject.description !== undefined) merged.description = freshObject.description;390391const hookEvents = new Set([...Object.keys(existingHooks), ...Object.keys(freshHooks)]);392for (const event of hookEvents) {393const preserved = stripImpeccableHookEntries(existingHooks[event]);394const added = Array.isArray(freshHooks[event]) ? freshHooks[event] : [];395const mergedEntries = [...preserved, ...added];396if (mergedEntries.length > 0) merged.hooks[event] = mergedEntries;397}398return merged;399}400401function fileHasImpeccableHookMarker(filePath) {402if (!fs.existsSync(filePath)) return false;403let parsed;404try {405parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));406} catch {407return false;408}409if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;410if (!parsed.hooks || typeof parsed.hooks !== 'object') return false;411return valueHasImpeccableHookMarker(parsed.hooks);412}413414function valueHasImpeccableHookMarker(value) {415if (typeof value === 'string') {416return IMPECCABLE_HOOK_COMMAND_MARKERS.some((marker) => value.includes(marker));417}418if (Array.isArray(value)) return value.some(valueHasImpeccableHookMarker);419if (value && typeof value === 'object') return Object.values(value).some(valueHasImpeccableHookMarker);420return false;421}422423function stripImpeccableHookEntry(entry) {424if (!entry || typeof entry !== 'object') return entry;425// `command`/`args`: Claude/Codex/Cursor. `bash`/`powershell`: GitHub Copilot's426// flat entry shape, where the marker lives under the shell-command keys.427if (valueHasImpeccableHookMarker(entry.command) || valueHasImpeccableHookMarker(entry.args)428|| valueHasImpeccableHookMarker(entry.bash) || valueHasImpeccableHookMarker(entry.powershell)) {429return null;430}431if (!Array.isArray(entry.hooks)) return entry;432433const strippedHooks = entry.hooks434.map(stripImpeccableHookEntry)435.filter(Boolean);436437if (strippedHooks.length === 0 && entry.hooks.some(valueHasImpeccableHookMarker)) {438return null;439}440return { ...entry, hooks: strippedHooks };441}442443function stripImpeccableHookEntries(entries) {444if (!Array.isArray(entries)) return [];445return entries446.map(stripImpeccableHookEntry)447.filter(Boolean);448}449450function pruneImpeccableHookFromManifest(manifestPath) {451if (!fileHasImpeccableHookMarker(manifestPath)) return false;452let parsed;453try {454parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));455} catch {456return false;457}458459const existingHooks = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)460? parsed.hooks461: {};462const cleanedHooks = {};463for (const [event, entries] of Object.entries(existingHooks)) {464const kept = stripImpeccableHookEntries(entries);465if (kept.length > 0) cleanedHooks[event] = kept;466}467468const next = { ...parsed };469if (Object.keys(cleanedHooks).length > 0) {470next.hooks = cleanedHooks;471} else {472delete next.hooks;473delete next.description;474delete next.version;475}476477if (Object.keys(next).length === 0) {478fs.rmSync(manifestPath, { force: true });479} else {480fs.writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}\n`);481}482return true;483}484485function normalizeRuleId(rule) {486return String(rule || '').trim().toLowerCase();487}488489function parseIgnoreRuleArgs(args) {490const positionals = [];491let allValues = false;492493for (let i = 0; i < args.length; i++) {494const arg = String(args[i] || '');495if (arg === '--all-values') {496allValues = true;497} else if (arg === '--reason') {498while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) i++;499} else if (arg.startsWith('--reason=')) {500// Accepted for command symmetry; ignoreRules stores rule ids only.501} else if (arg.startsWith('--')) {502throw new Error(`Unknown ignore-rule flag: ${arg}`);503} else {504positionals.push(arg);505}506}507508return {509rule: normalizeRuleId(positionals[0]),510allValues,511};512}513514function addIgnoreRule(cwd, args) {515const parsed = parseIgnoreRuleArgs(args);516const rule = parsed.rule;517if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab');518if (rule === 'overused-font' && !parsed.allValues) {519throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font <font> for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.');520}521const config = mergeDetectorConfig(readRawDetectorConfig(cwd));522if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);523writeDetectorConfig(cwd, config);524return `Added "${rule}" to detector.ignoreRules. Current: ${config.ignoreRules.join(', ')}`;525}526527function addIgnoreFile(cwd, glob) {528if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"');529const config = mergeDetectorConfig(readRawDetectorConfig(cwd));530if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);531writeDetectorConfig(cwd, config);532return `Added "${glob}" to detector.ignoreFiles. Current: ${config.ignoreFiles.join(', ')}`;533}534535function parseIgnoreValueArgs(args) {536const positionals = [];537let shared = false;538let local = false;539let reason = '';540541for (let i = 0; i < args.length; i++) {542const arg = args[i];543if (arg === '--shared') {544shared = true;545} else if (arg === '--local') {546local = true;547} else if (arg === '--reason') {548const chunks = [];549while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) {550chunks.push(args[++i]);551}552reason = chunks.join(' ').trim();553} else if (String(arg).startsWith('--reason=')) {554reason = String(arg).slice('--reason='.length).trim();555} else {556positionals.push(arg);557}558}559560const [rule, ...valueParts] = positionals;561return {562rule: String(rule || '').trim().toLowerCase(),563value: normalizeIgnoreValue(valueParts.join(' ')),564shared,565local,566reason,567};568}569570function addIgnoreValue(cwd, args) {571const parsed = parseIgnoreValueArgs(args);572if (!parsed.rule || !parsed.value) {573throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter');574}575576if (parsed.shared && parsed.local) {577throw new Error('Pass only one scope flag: --shared or --local');578}579580const local = parsed.local;581const config = mergeDetectorConfig(readRawDetectorConfig(cwd, { local }));582const key = `${parsed.rule}\0${parsed.value}`;583const existing = config.ignoreValues.find((entry) => `${entry.rule}\0${entry.value}` === key);584585if (existing) {586if (parsed.reason) existing.reason = parsed.reason;587} else {588const entry = {589rule: parsed.rule,590value: parsed.value,591createdAt: new Date().toISOString(),592};593if (parsed.reason) entry.reason = parsed.reason;594config.ignoreValues.push(entry);595}596597const target = writeDetectorConfig(cwd, config, { local });598const scope = local ? 'local detector.ignoreValues' : 'shared detector.ignoreValues';599return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target) || target}).`;600}601602function reset(cwd) {603const removed = [];604// Unified files may hold non-hook keys (e.g. updateCheck); strip only the605// hook/detector subtrees and keep the rest, deleting the file only if nothing remains.606for (const filePath of [getConfigPath(cwd), getLocalConfigPath(cwd)]) {607try {608const raw = readRawConfigFile(filePath).raw;609if (!raw || typeof raw !== 'object' || Array.isArray(raw) || (!('hook' in raw) && !('detector' in raw))) continue;610const { hook, detector, ...rest } = raw;611if (Object.keys(rest).length === 0) {612fs.unlinkSync(filePath);613} else {614fs.writeFileSync(filePath, JSON.stringify(rest, null, 2) + '\n');615}616removed.push(path.relative(cwd, filePath) || filePath);617} catch { /* ignore */ }618}619// State files are wholly ours; delete outright.620for (const filePath of [getCachePath(cwd), getPendingPath(cwd)]) {621try {622if (fs.existsSync(filePath)) {623fs.unlinkSync(filePath);624removed.push(path.relative(cwd, filePath) || filePath);625}626} catch { /* ignore */ }627}628return removed.length629? `Reset design hook config and cache (removed: ${removed.join(', ')}).`630: 'No hook config or cache to remove. Already at defaults.';631}632633function main() {634const [, , actionArg, ...rest] = process.argv;635const action = (actionArg || 'status').toLowerCase();636const cwd = process.cwd();637638if (!ACTIONS.has(action)) {639process.stderr.write(`Unknown action: ${action}\nValid: ${Array.from(ACTIONS).join(', ')}\n`);640process.exit(1);641}642643try {644let out = '';645switch (action) {646case 'status': out = statusReport(cwd); break;647case 'on': out = setEnabled(cwd, true); break;648case 'off': out = setEnabled(cwd, false); break;649case 'ignore-rule': out = addIgnoreRule(cwd, rest); break;650case 'ignore-file': out = addIgnoreFile(cwd, rest[0]); break;651case 'ignore-value': out = addIgnoreValue(cwd, rest); break;652case 'reset': out = reset(cwd); break;653}654process.stdout.write(out + '\n');655} catch (err) {656process.stderr.write(`Error: ${err.message || err}\n`);657process.exit(1);658}659}660661main();662