60dc4d8264
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.6 KiB
JavaScript
87 lines
3.6 KiB
JavaScript
#!/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);
|
||
}
|