mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-06 11:18:56 +03:00
feat(hooks): §1A гард поддержки + суфлёр навыков (node-хуки Claude Code)
Харнес-слой поверх пола §1B: ловит правки мимо навыков-мутаторов. - support-guard.mjs (PreToolUse Edit|Write|MultiEdit) — §1A: блокирует сырую правку объекта поставщика «на замке» / read-only конфы; реакция deny|warn|off из .v8-project.json editingAllowedCheck, идентично §1B. - skill-suggester.mjs (PostToolUse Read|Grep|Glob|Edit|Write|MultiEdit) — ненавязчивая подсказка профильного навыка, throttle 1×/сессия/группа, не блокирует; флаг skillSuggester (on|off). - common/: support-state.mjs (порт декодера bin 1:1 из Assert-EditAllowed), project.mjs (реакция из .v8-project.json), object-class.mjs (карта путь→навык с различением cf/cfe и mxl/скд по нюху корня). - test/run.mjs: 38 standalone-тестов на корпусе cfsrc + синтетике. - plugin.json: hooks → ./hooks/hooks.json (авто-загрузка в плагине). §1C (грубый Bash-гейт) отброшен — дублирует §1B, формат bin заморожен. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// project.mjs v1.0 — read reaction mode from .v8-project.json for Claude Code hooks
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
//
|
||||
// Canonical port of Get-EditMode / _sg_get_edit_mode
|
||||
// (reference: .claude/skills/meta-edit/scripts/meta-edit.ps1:181-201, meta-edit.py:50-68).
|
||||
// configSrc is matched here ONLY to fetch a per-database override — identically to the
|
||||
// in-skill guard §1B, so that a raw Edit and an edit-via-skill behave the same under the
|
||||
// same databases[].editingAllowedCheck. Never throws — falls back to the default.
|
||||
|
||||
import { readFileSync, existsSync, statSync } from 'node:fs';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
|
||||
const WIN = process.platform === 'win32';
|
||||
|
||||
function norm(p) {
|
||||
let s = resolve(p).replace(/[\\/]+$/, '');
|
||||
return WIN ? s.toLowerCase() : s;
|
||||
}
|
||||
|
||||
function findV8Project(startDir) {
|
||||
let d = startDir;
|
||||
for (let i = 0; i < 20 && d; i++) {
|
||||
const pj = join(d, '.v8-project.json');
|
||||
if (existsSync(pj)) return pj;
|
||||
const parent = dirname(d);
|
||||
if (parent === d) break;
|
||||
d = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generic reader: returns databases[].<key> for the matching configSrc, else global
|
||||
// proj.<key>, else fallback. cwd is the hook's stdin cwd; cfgDir is the resolved config root.
|
||||
export function getProjectSetting(key, cfgDir, cwd, fallback) {
|
||||
try {
|
||||
const pj = findV8Project(cwd) || (cfgDir ? findV8Project(cfgDir) : null);
|
||||
if (!pj) return fallback;
|
||||
let raw = readFileSync(pj, 'utf8');
|
||||
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1); // strip BOM
|
||||
const proj = JSON.parse(raw);
|
||||
if (cfgDir && Array.isArray(proj.databases)) {
|
||||
const cfgFull = norm(cfgDir);
|
||||
for (const db of proj.databases) {
|
||||
if (db && db.configSrc) {
|
||||
const src = norm(db.configSrc);
|
||||
if (cfgFull === src || cfgFull.startsWith(src + sep)) {
|
||||
if (db[key]) return db[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (proj[key]) return proj[key];
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Guard reaction: deny (default) | warn | off.
|
||||
export function getEditMode(cfgDir, cwd) {
|
||||
return getProjectSetting('editingAllowedCheck', cfgDir, cwd, 'deny');
|
||||
}
|
||||
|
||||
// Suggester switch: on (default) | off.
|
||||
export function getSuggesterMode(cfgDir, cwd) {
|
||||
return getProjectSetting('skillSuggester', cfgDir, cwd, 'on');
|
||||
}
|
||||
Reference in New Issue
Block a user