2026-05-19 06:31:18 +03:00
|
|
|
|
#!/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;
|
|
|
|
|
|
|
2026-05-19 07:12:05 +03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 07:36:21 +03:00
|
|
|
|
// 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`).
|
2026-05-19 07:12:05 +03:00
|
|
|
|
export function detectDrift(settings, toolingText, aliases = {}) {
|
2026-05-19 06:31:18 +03:00
|
|
|
|
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]);
|
2026-05-19 07:12:05 +03:00
|
|
|
|
const missingInTooling = enabled.filter((p) => {
|
|
|
|
|
|
if (found.has(p)) return false;
|
|
|
|
|
|
const alias = aliases[p];
|
|
|
|
|
|
if (alias && toolingText.includes(alias)) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
2026-05-19 07:36:21 +03:00
|
|
|
|
return { missingInTooling };
|
2026-05-19 06:31:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadFileMaybe(path) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function loadInputs(projectRoot = process.cwd()) {
|
|
|
|
|
|
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 tooling = loadFileMaybe(join(projectRoot, 'docs', 'Tooling_v8_3.md')) || '';
|
2026-05-19 07:12:05 +03:00
|
|
|
|
const aliasesRaw = loadFileMaybe(join(projectRoot, 'tools', '.l1-watcher-aliases.txt'));
|
|
|
|
|
|
const aliases = parseAliases(aliasesRaw);
|
|
|
|
|
|
return { settings: merged, tooling, aliases };
|
2026-05-19 06:31:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/l1-watcher.mjs')) {
|
2026-05-19 07:12:05 +03:00
|
|
|
|
const { settings, tooling, aliases } = loadInputs();
|
|
|
|
|
|
const drift = detectDrift(settings, tooling, aliases);
|
2026-05-19 06:31:18 +03:00
|
|
|
|
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.`);
|
2026-05-19 07:12:05 +03:00
|
|
|
|
console.error(`If the plugin is referenced in Tooling under a group/human name, add an alias to tools/.l1-watcher-aliases.txt.`);
|
2026-05-19 06:31:18 +03:00
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(`[l1-watcher] OK — 0 drift`);
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|