@nitra/cursor
Advanced tools
| /** | ||
| * Конформність-детект (колишній subcommand `_fix-check`) як ПРЯМА функція — без subprocess-обгортки | ||
| * `bun n-cursor.js _fix-check`. Викликають конформність-фаза `lint` (read-only), движок | ||
| * (`orchestrator.mjs`, `t0.mjs`) і PostToolUse-хук. | ||
| * | ||
| * Per-rule ізоляція зберігається: кожне `rules/<id>/fix.mjs` усе ще запускається окремим | ||
| * процесом `bun` (config-loading + whitelist + crash-isolation). Прибрано лише зовнішній | ||
| * wrapper-subprocess, що його раніше шелили оркестратор/хук. | ||
| */ | ||
| import { spawnSync } from 'node:child_process' | ||
| import { dirname, join } from 'node:path' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { cwd as processCwd } from 'node:process' | ||
| import { listRuleIds } from '../list-rule-ids.mjs' | ||
| import { ensureTool } from '../ensure-tool.mjs' | ||
| import { discoverCheckRulesFromCursorRules } from '../discover-check-rules-from-cursor.mjs' | ||
| import { listProjectRulesMdcFiles } from '../list-project-rules-mdc.mjs' | ||
| // Цей файл: npm/scripts/lib/fix/run-fix-check.mjs → npm/rules (чотири dirname угору + rules). | ||
| const BUNDLED_RULES_DIR = join(dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url))))), 'rules') | ||
| /** | ||
| * Визначає id правил для прогону: явні (з валідацією) або discovery з `.cursor/rules/*.mdc`. | ||
| * @param {string[]} requestedRules запитані (порожній → discovery) | ||
| * @param {string[]} available доступні rule-id у пакеті | ||
| * @param {string} cwd корінь | ||
| * @returns {Promise<string[]>} id для прогону (можливо порожній) | ||
| * @throws {Error} на невідомих явно заданих правилах | ||
| */ | ||
| async function resolveCheckRuleIds(requestedRules, available, cwd) { | ||
| if (requestedRules.length > 0) { | ||
| const unknown = requestedRules.filter(id => !available.includes(id)) | ||
| if (unknown.length > 0) throw new Error(`Unknown rules: ${unknown.join(', ')}`) | ||
| return requestedRules | ||
| } | ||
| const mdcFiles = await listProjectRulesMdcFiles(cwd) | ||
| if (mdcFiles.length === 0) return [] | ||
| return discoverCheckRulesFromCursorRules(available, mdcFiles) | ||
| } | ||
| /** | ||
| * Прогоняє `fix.mjs` кожного правила окремим процесом, захоплюючи output. | ||
| * @param {string[]} idsToRun правила | ||
| * @param {string} cwd корінь | ||
| * @returns {{ totalFailed:number, rules:Array<{ruleId:string, ok:boolean, output:string}> }} результат | ||
| */ | ||
| function runRuleFixProcesses(idsToRun, cwd) { | ||
| let totalFailed = 0 | ||
| const rules = [] | ||
| for (const id of idsToRun) { | ||
| const r = spawnSync('bun', [join(BUNDLED_RULES_DIR, id, 'fix.mjs')], { cwd, encoding: 'utf8' }) | ||
| const ok = r.status === 0 | ||
| rules.push({ ruleId: id, ok, output: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() }) | ||
| if (!ok) totalFailed++ | ||
| } | ||
| return { totalFailed, rules } | ||
| } | ||
| /** | ||
| * Конформність-детект: per-rule `fix.mjs run()` (= перевірка, без мутацій). | ||
| * @param {string[]} [requestedRules] фільтр (порожній → discovery з `.cursor/rules/`) | ||
| * @param {string} [cwd] корінь | ||
| * @returns {Promise<{ total:number, failed:number, rules:Array<{ruleId:string, ok:boolean, output:string}> }>} результат | ||
| */ | ||
| export async function runFixCheck(requestedRules = [], cwd = processCwd()) { | ||
| ensureTool('conftest') | ||
| const available = await listRuleIds(BUNDLED_RULES_DIR) | ||
| if (available.length === 0) return { total: 0, failed: 0, rules: [] } | ||
| const idsToRun = await resolveCheckRuleIds(requestedRules, available, cwd) | ||
| if (idsToRun.length === 0) return { total: 0, failed: 0, rules: [] } | ||
| const { totalFailed, rules } = runRuleFixProcesses(idsToRun, cwd) | ||
| return { total: idsToRun.length, failed: totalFailed, rules } | ||
| } |
| /** | ||
| * Список `.mdc`-файлів правил у `.cursor/rules/` проєкту-споживача (відсортований). | ||
| * Винесено зі `bin/n-cursor.js`, щоб ділити між CLI-dispatch і `run-fix-check` (конформність-детект). | ||
| */ | ||
| import { existsSync } from 'node:fs' | ||
| import { readdir } from 'node:fs/promises' | ||
| import { join } from 'node:path' | ||
| import { cwd as processCwd } from 'node:process' | ||
| /** Каталог правил у проєкті-споживачі (відносно кореня). */ | ||
| export const CURSOR_RULES_DIR = '.cursor/rules' | ||
| /** | ||
| * @param {string} [cwd] корінь проєкту | ||
| * @returns {Promise<string[]>} імена `*.mdc` (відсортовані), або `[]` якщо каталогу немає | ||
| */ | ||
| export async function listProjectRulesMdcFiles(cwd = processCwd()) { | ||
| const dir = join(cwd, CURSOR_RULES_DIR) | ||
| if (!existsSync(dir)) return [] | ||
| const names = await readdir(dir) | ||
| return names.filter(n => n.endsWith('.mdc')).toSorted((a, b) => a.localeCompare(b)) | ||
| } |
+1
-1
| { | ||
| "name": "@nitra/cursor", | ||
| "version": "9.4.0", | ||
| "version": "10.0.0", | ||
| "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -17,3 +17,2 @@ /** | ||
| import { fileURLToPath } from 'node:url' | ||
| import { spawnSync } from 'node:child_process' | ||
| import { cwd as processCwd } from 'node:process' | ||
@@ -27,3 +26,2 @@ | ||
| const RULES_DIR = join(PACKAGE_ROOT, 'rules') | ||
| const N_CURSOR_BIN = join(PACKAGE_ROOT, 'bin', 'n-cursor.js') | ||
@@ -46,16 +44,7 @@ /** | ||
| } | ||
| const r = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...filter], { cwd, encoding: 'utf8', timeout: 600_000 }) | ||
| let parsed = null | ||
| try { | ||
| parsed = JSON.parse((r.stdout ?? '').trim()) | ||
| } catch { | ||
| parsed = null | ||
| } | ||
| if (!parsed) { | ||
| log('❌ lint: конформність — помилка перевірки (_fix-check не повернув JSON)\n') | ||
| return 1 | ||
| } | ||
| const failed = parsed.rules.filter(/** @param {{ok:boolean}} x */ x => !x.ok) | ||
| const { runFixCheck } = await import('../../../scripts/lib/fix/run-fix-check.mjs') | ||
| const { rules } = await runFixCheck(filter, cwd) | ||
| const failed = rules.filter(x => !x.ok) | ||
| if (failed.length === 0) return 0 | ||
| log(`❌ lint: конформність — ${failed.length} порушень: ${failed.map(/** @param {{ruleId:string}} x */ x => x.ruleId).join(', ')}\n`) | ||
| log(`❌ lint: конформність — ${failed.length} порушень: ${failed.map(x => x.ruleId).join(', ')}\n`) | ||
| for (const f of failed) if (f.output) log(`${f.output}\n`) | ||
@@ -62,0 +51,0 @@ return 1 |
| /** @see ./docs/orchestrator.md */ | ||
| import { spawnSync } from 'node:child_process' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { join } from 'node:path' | ||
| import { runFixCheck } from './run-fix-check.mjs' | ||
| import { runT0AutoCli } from './t0.mjs' | ||
| const HERE = fileURLToPath(new URL('.', import.meta.url)) | ||
| const N_CURSOR_BIN = join(HERE, '../../../bin/n-cursor.js') | ||
| const DEFAULT_MAX_ITER = 3 | ||
@@ -32,9 +28,9 @@ const ESCALATE_AFTER = 2 | ||
| * @param {Array<{ ruleId: string }>} failed правила перед кроком | ||
| * @returns {Array<{ ruleId: string, ok: boolean, output: string }>} правила після T0 | ||
| * @returns {Promise<Array<{ ruleId: string, ok: boolean, output: string }>>} правила після T0 | ||
| */ | ||
| function runT0Step(cwd, ruleFilter, failed) { | ||
| spawnSync('bun', [N_CURSOR_BIN, 'fix-t0', ...ruleFilter], { cwd, stdio: 'pipe' }) | ||
| async function runT0Step(cwd, ruleFilter, failed) { | ||
| await runT0AutoCli([...ruleFilter], cwd) | ||
| const afterT0 = runFixCheck(cwd, ruleFilter) | ||
| const failedAfterT0 = afterT0?.rules.filter(r => !r.ok) ?? failed | ||
| const afterT0 = await runFixCheck(ruleFilter, cwd) | ||
| const failedAfterT0 = afterT0.rules.filter(r => !r.ok) | ||
| const t0Fixed = failed.filter(r => !failedAfterT0.some(f => f.ruleId === r.ruleId)) | ||
@@ -88,8 +84,3 @@ | ||
| // ── Перша перевірка (тихо) ── | ||
| const initial = runFixCheck(cwd, ruleFilter) | ||
| if (!initial) { | ||
| console.error(`❌ fix: помилка перевірки`) | ||
| return 1 | ||
| } | ||
| const initial = await runFixCheck(ruleFilter, cwd) | ||
| let failed = initial.rules.filter(r => !r.ok) | ||
@@ -109,3 +100,3 @@ const total = initial.total | ||
| for (let iter = 1; iter <= maxIter; iter++) { | ||
| failed = runT0Step(cwd, ruleFilter, failed) | ||
| failed = await runT0Step(cwd, ruleFilter, failed) | ||
| if (failed.length === 0) break | ||
@@ -116,4 +107,4 @@ | ||
| // Перевірка після LLM | ||
| const afterLLM = runFixCheck(cwd, ruleFilter) | ||
| failed = afterLLM?.rules.filter(r => !r.ok) ?? failed | ||
| const afterLLM = await runFixCheck(ruleFilter, cwd) | ||
| failed = afterLLM.rules.filter(r => !r.ok) | ||
| if (failed.length === 0) break | ||
@@ -130,23 +121,1 @@ } | ||
| } | ||
| /** | ||
| * Внутрішня check-gate: запускає fix-перевірки і повертає структурований результат. | ||
| * Не є публічним CLI — викликається лише оркестратором. | ||
| * @param {string} cwd корінь проєкту | ||
| * @param {string[]} ruleFilter список ID правил (порожній — усі) | ||
| * @returns {{ total: number, failed: number, rules: Array<{ ruleId: string, ok: boolean, output: string }> } | null} JSON-результат або null якщо stdout порожній/невалідний | ||
| */ | ||
| function runFixCheck(cwd, ruleFilter = []) { | ||
| const r = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...ruleFilter], { | ||
| cwd, | ||
| encoding: 'utf8', | ||
| timeout: 120_000 | ||
| }) | ||
| const stdout = r.stdout?.trim() | ||
| if (!stdout) return null | ||
| try { | ||
| return JSON.parse(stdout) | ||
| } catch { | ||
| return null | ||
| } | ||
| } |
| /** @see ./docs/t0.md */ | ||
| import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs' | ||
| import { dirname, join } from 'node:path' | ||
| import { spawnSync } from 'node:child_process' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { join } from 'node:path' | ||
| import { runFixCheck } from './run-fix-check.mjs' | ||
| const REC_REQUIRE_RE = /recommendations має містити "[^"]+"/ | ||
@@ -112,24 +112,3 @@ const REC_MATCH_ALL_RE = /recommendations має містити "([^"]+)"/g | ||
| const HERE = dirname(fileURLToPath(import.meta.url)) | ||
| /** Абсолютний шлях до npm/bin/n-cursor.js відносно цього файлу */ | ||
| const N_CURSOR_BIN = join(HERE, '../../../bin/n-cursor.js') | ||
| /** | ||
| * Запускає `_fix-check` і парсить JSON-результат. | ||
| * @param {string[]} ruleFilter список rule-ids (порожній — усі) | ||
| * @param {string} cwd корінь проєкту | ||
| * @returns {{ rules: Array<{ ruleId: string, ok: boolean, output: string }> } | { _empty: true } | { _badJson: true }} JSON або маркер помилки | ||
| */ | ||
| function fixCheck(ruleFilter, cwd) { | ||
| const r = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...ruleFilter], { cwd, encoding: 'utf8', timeout: 120_000 }) | ||
| const raw = r.stdout?.trim() | ||
| if (!raw) return { _empty: true, stderr: r.stderr } | ||
| try { | ||
| return JSON.parse(raw) | ||
| } catch { | ||
| return { _badJson: true } | ||
| } | ||
| } | ||
| /** | ||
| * Застосовує T0-auto до кожного провального правила, розділяючи на applied/skipped. | ||
@@ -158,22 +137,12 @@ * @param {Array<{ ruleId: string, output: string }>} failed провальні правила | ||
| * повторно перевіряє check-gate, виводить підсумок. | ||
| * @param {string[]} args аргументи підкоманди (опційний список rule-ids) | ||
| * @param {string[]} args аргументи (опційний список rule-ids) | ||
| * @param {string} cwd корінь проєкту | ||
| * @returns {Promise<number>} 0 — T0-auto закрив всі або немає порушень; 1 — лишились | ||
| */ | ||
| export function runT0AutoCli(args, cwd) { | ||
| export async function runT0AutoCli(args, cwd) { | ||
| const ruleFilter = args.filter(a => !a.startsWith('--')) | ||
| const verbose = args.includes('--verbose') || args.includes('-v') | ||
| // 1. Запустити fix --json | ||
| const fixJson = fixCheck(ruleFilter, cwd) | ||
| if (fixJson._empty) { | ||
| console.error(`n-cursor fix-t0: fix --json повернув порожній stdout`) | ||
| console.error(fixJson.stderr?.slice(0, 300) ?? '') | ||
| return 1 | ||
| } | ||
| if (fixJson._badJson) { | ||
| console.error(`n-cursor fix-t0: fix --json повернув невалідний JSON`) | ||
| return 1 | ||
| } | ||
| // 1. Конформність-детект (пряма функція, без subprocess) | ||
| const fixJson = await runFixCheck(ruleFilter, cwd) | ||
| const failed = fixJson.rules.filter(r => !r.ok) | ||
@@ -200,7 +169,3 @@ if (failed.length === 0) { | ||
| // 4. Check-gate: перевірити лише ті правила, що ми чіпали | ||
| const recheckJson = fixCheck(applied.map(a => a.ruleId), cwd) | ||
| if (recheckJson._empty) { | ||
| console.error(`fix-t0: check-gate: fix --json повернув порожній stdout`) | ||
| return 1 | ||
| } | ||
| const recheckJson = await runFixCheck(applied.map(a => a.ruleId), cwd) | ||
| const stillFailed = recheckJson.rules.filter(r => !r.ok) | ||
@@ -207,0 +172,0 @@ |
@@ -11,8 +11,10 @@ /** | ||
| * - stdin Claude Code: JSON із `tool_input.file_path`; якщо файлу немає (напр. Bash) — exit 0 (skip); | ||
| * - інакше spawn `_fix-check` (детект усіх правил), exit-код прозоро пробрасуємо (PostToolUse | ||
| * не блокує turn, але код лишаємо інформативним: 1 — є порушення конформності). | ||
| * - інакше пряма `runFixCheck` (детект усіх правил, без subprocess-обгортки), exit-код прозоро: | ||
| * 1 — є порушення конформності (PostToolUse не блокує turn, але код лишаємо інформативним). | ||
| */ | ||
| import { spawn } from 'node:child_process' | ||
| import { once } from 'node:events' | ||
| import { cwd as processCwd } from 'node:process' | ||
| import { runFixCheck } from './lib/fix/run-fix-check.mjs' | ||
| /** | ||
@@ -61,5 +63,5 @@ * Зчитує stdin до EOF як utf8 рядок. На TTY — повертає `''` одразу. | ||
| * Параметри доступні для інʼєкції для тестів: `stdinJson` обходить read від `process.stdin`, | ||
| * `spawnFn` — заміна `node:child_process.spawn`. | ||
| * @param {{ stdinJson?: string, spawnFn?: typeof spawn }} [options] параметри для тестів | ||
| * @returns {Promise<number>} exit code (0 — пропущено / конформність ОК; інше — є порушення) | ||
| * `runFixCheckFn` — заміна `runFixCheck`. | ||
| * @param {{ stdinJson?: string, runFixCheckFn?: typeof runFixCheck }} [options] параметри для тестів | ||
| * @returns {Promise<number>} exit code (0 — пропущено / конформність ОК; 1 — є порушення) | ||
| */ | ||
@@ -73,8 +75,11 @@ export async function runPostToolUseFixCli(options = {}) { | ||
| } | ||
| const spawnFn = options.spawnFn ?? spawn | ||
| // Один read-only виклик: детект конформності всіх активованих правил, без роутингу. | ||
| const child = spawnFn('npx', ['--no', '@nitra/cursor', '_fix-check'], { stdio: 'inherit' }) | ||
| const check = options.runFixCheckFn ?? runFixCheck | ||
| // Один read-only детект конформності всіх активованих правил (пряма функція, без subprocess). | ||
| try { | ||
| const [code] = await once(child, 'exit') | ||
| return code ?? 1 | ||
| const { failed, rules } = await check([], processCwd()) | ||
| if (failed === 0) return 0 | ||
| for (const r of rules.filter(x => !x.ok)) { | ||
| if (r.output) process.stderr.write(`${r.output}\n`) | ||
| } | ||
| return 1 | ||
| } catch (error) { | ||
@@ -81,0 +86,0 @@ process.stderr.write(`post-tool-use-fix: не вдалося запустити детект конформності — ${error.message}\n`) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
810
0.25%25
-3.85%4485301
-0.12%36232
-0.32%