#!/usr/bin/env node import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; const PLUGIN_NAME_PATTERN = /[a-z][\w-]*(?:@[\w-]+)?/g; export function parseAliases(raw) { if (!raw) return {}; const out = {}; for (const line of raw.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 1) continue; const key = trimmed.slice(0, eq).trim(); const value = trimmed.slice(eq + 1).trim(); if (key && value) out[key] = value; } return out; } // Detects the L1 pattern (ADR-011 §6.1): a plugin enabled in settings.json // that has no formalization in Tooling Прил. Н. Only the settings→Tooling // direction is checked — the reverse ("documented but disabled") cannot be // computed reliably because Tooling lists tools by human/group name while // settings.json keys are machine IDs (`name@marketplace`). export function detectDrift(settings, toolingText, aliases = {}) { const enabled = Object.entries(settings.enabledPlugins || {}) .filter(([, v]) => v === true) .map(([k]) => k); const found = new Set(); for (const m of toolingText.matchAll(PLUGIN_NAME_PATTERN)) found.add(m[0]); const missingInTooling = enabled.filter((p) => { if (found.has(p)) return false; const alias = aliases[p]; if (alias && toolingText.includes(alias)) return false; return true; }); return { missingInTooling }; } function loadFileMaybe(path) { try { return existsSync(path) ? readFileSync(path, 'utf-8') : null; } catch { return null; } } export function loadInputs(projectRoot = process.cwd(), toolRegistryPath = 'docs/Tooling_v8_3.md') { const userSettings = JSON.parse(loadFileMaybe(join(homedir(), '.claude', 'settings.json')) || '{}'); const projectSettings = JSON.parse(loadFileMaybe(join(projectRoot, '.claude', 'settings.json')) || '{}'); const merged = { enabledPlugins: { ...(userSettings.enabledPlugins || {}), ...(projectSettings.enabledPlugins || {}) }, }; const toolingRaw = toolRegistryPath ? loadFileMaybe(join(projectRoot, toolRegistryPath)) : null; const tooling = toolingRaw || ''; const toolingPresent = toolingRaw !== null; const aliasesRaw = loadFileMaybe(join(projectRoot, 'tools', '.l1-watcher-aliases.txt')); const aliases = parseAliases(aliasesRaw); return { settings: merged, tooling, aliases, toolingPresent }; } if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/l1-watcher.mjs')) { let toolRegistryPath = 'docs/Tooling_v8_3.md'; try { const { loadConfig } = await import('./brain-config.mjs'); toolRegistryPath = loadConfig().tool_registry_path; } catch { /* дефолт */ } const { settings, tooling, aliases, toolingPresent } = loadInputs(process.cwd(), toolRegistryPath); if (!toolingPresent) { console.log('[l1-watcher] OK — справочник инструментов не задан/не найден (skip)'); process.exit(0); } const drift = detectDrift(settings, tooling, aliases); if (drift.missingInTooling.length > 0) { console.error(`[l1-watcher] FAIL — plugins in settings but not formalized in Tooling Прил. Н:`); drift.missingInTooling.forEach((p) => console.error(` - ${p}`)); console.error(`Run /claude-md-management:claude-md-improver to formalize.`); console.error(`If the plugin is referenced in Tooling under a group/human name, add an alias to tools/.l1-watcher-aliases.txt.`); process.exit(1); } console.log(`[l1-watcher] OK — 0 drift`); process.exit(0); }