@nitra/cursor 5.3.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/bin/n-cursor.js +72 -50
- package/lib/models.mjs +1 -1
- package/package.json +1 -1
- package/rules/k8s/js/manifests.mjs +144 -82
- package/rules/npm-module/js/header_doc_pointer.mjs +72 -27
- package/rules/npm-module/js/rule_meta.mjs +72 -36
- package/rules/npm-module/js/skill_meta.mjs +59 -35
- package/scripts/coverage-classify/index.mjs +2 -2
- package/scripts/coverage-classify/verdict-schema.mjs +1 -1
- package/scripts/lib/assert-project-root.mjs +1 -1
- package/scripts/lib/discover-check-rules-from-cursor.mjs +1 -1
- package/scripts/lib/rule-predicates.mjs +30 -18
- package/scripts/lib/run-rule-cli.mjs +1 -1
- package/scripts/lib/run-standard-rule.mjs +1 -1
- package/scripts/post-tool-use-fix.mjs +3 -3
- package/scripts/skills-cli.mjs +5 -5
- package/scripts/worktree-cli.mjs +5 -5
- package/skills/doc-files/js/docgen-extract.mjs +1 -1
- package/skills/doc-files/js/docgen-files-batch.mjs +65 -34
- package/skills/doc-files/js/docgen-gen.mjs +121 -36
- package/skills/doc-files/js/docgen-prompts.mjs +20 -5
- package/skills/fix/js/orchestrator.mjs +64 -35
- package/skills/fix/js/t0.mjs +44 -32
- package/skills/start-check/js/check.mjs +1 -1
|
@@ -6,6 +6,77 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
|
6
6
|
import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
|
|
7
7
|
import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Перевіряє поле `auto` у meta.json одного правила.
|
|
11
|
+
* @param {string} id ідентифікатор правила
|
|
12
|
+
* @param {Record<string, unknown>} raw сирий meta.json
|
|
13
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
14
|
+
* @returns {boolean} true, якщо поле валідне (або відсутнє)
|
|
15
|
+
*/
|
|
16
|
+
function checkAutoField(id, raw, reporter) {
|
|
17
|
+
if (raw.auto === undefined) return true
|
|
18
|
+
const spec = parseRuleAutoSpec(raw.auto)
|
|
19
|
+
if (spec === null) {
|
|
20
|
+
reporter.fail(`rules/${id}: meta.json.auto нерозпізнане (очікується "завжди" / масив / {glob} / {predicate})`)
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if ('predicate' in spec && !Object.hasOwn(RULE_PREDICATES, spec.predicate)) {
|
|
24
|
+
reporter.fail(`rules/${id}: невідомий predicate "${spec.predicate}" (немає в RULE_PREDICATES)`)
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Перевіряє поле `lint` у meta.json одного правила.
|
|
32
|
+
* @param {string} id ідентифікатор правила
|
|
33
|
+
* @param {string} ruleDir каталог правила
|
|
34
|
+
* @param {Record<string, unknown>} raw сирий meta.json
|
|
35
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
36
|
+
* @returns {boolean} true, якщо поле валідне (або відсутнє)
|
|
37
|
+
*/
|
|
38
|
+
function checkLintField(id, ruleDir, raw, reporter) {
|
|
39
|
+
if (raw.lint === undefined) return true
|
|
40
|
+
if (parseRuleLintPhase(raw.lint) === null) {
|
|
41
|
+
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
|
|
45
|
+
reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Валідує meta.json одного правила.
|
|
53
|
+
* @param {string} id ідентифікатор правила
|
|
54
|
+
* @param {string} ruleDir каталог правила
|
|
55
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function checkRule(id, ruleDir, reporter) {
|
|
59
|
+
let ruleOk = true
|
|
60
|
+
|
|
61
|
+
if (existsSync(join(ruleDir, 'auto.md'))) {
|
|
62
|
+
reporter.fail(`rules/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
63
|
+
ruleOk = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const raw = readRuleMetaRaw(ruleDir)
|
|
67
|
+
if (!raw) {
|
|
68
|
+
reporter.fail(`rules/${id}: відсутній або невалідний meta.json`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!checkAutoField(id, raw, reporter)) ruleOk = false
|
|
73
|
+
if (!checkLintField(id, ruleDir, raw, reporter)) ruleOk = false
|
|
74
|
+
|
|
75
|
+
if (ruleOk) {
|
|
76
|
+
reporter.pass(`rules/${id}: meta.json валідний`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
9
80
|
/**
|
|
10
81
|
* Валідує всі `npm/rules/<id>/meta.json`.
|
|
11
82
|
* @param {string} [cwd] корінь репозиторію
|
|
@@ -21,42 +92,7 @@ export function check(cwd = process.cwd()) {
|
|
|
21
92
|
|
|
22
93
|
for (const entry of readdirSync(rulesDir, { withFileTypes: true })) {
|
|
23
94
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
24
|
-
|
|
25
|
-
const ruleDir = join(rulesDir, id)
|
|
26
|
-
let ruleOk = true
|
|
27
|
-
|
|
28
|
-
if (existsSync(join(ruleDir, 'auto.md'))) {
|
|
29
|
-
reporter.fail(`rules/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
30
|
-
ruleOk = false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const raw = readRuleMetaRaw(ruleDir)
|
|
34
|
-
if (!raw) {
|
|
35
|
-
reporter.fail(`rules/${id}: відсутній або невалідний meta.json`)
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
if (raw.auto !== undefined) {
|
|
39
|
-
const spec = parseRuleAutoSpec(raw.auto)
|
|
40
|
-
if (spec === null) {
|
|
41
|
-
reporter.fail(`rules/${id}: meta.json.auto нерозпізнане (очікується "завжди" / масив / {glob} / {predicate})`)
|
|
42
|
-
ruleOk = false
|
|
43
|
-
} else if ('predicate' in spec && !Object.hasOwn(RULE_PREDICATES, spec.predicate)) {
|
|
44
|
-
reporter.fail(`rules/${id}: невідомий predicate "${spec.predicate}" (немає в RULE_PREDICATES)`)
|
|
45
|
-
ruleOk = false
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (raw.lint !== undefined) {
|
|
49
|
-
if (parseRuleLintPhase(raw.lint) === null) {
|
|
50
|
-
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
|
|
51
|
-
ruleOk = false
|
|
52
|
-
} else if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
|
|
53
|
-
reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
|
|
54
|
-
ruleOk = false
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (ruleOk) {
|
|
58
|
-
reporter.pass(`rules/${id}: meta.json валідний`)
|
|
59
|
-
}
|
|
95
|
+
checkRule(entry.name, join(rulesDir, entry.name), reporter)
|
|
60
96
|
}
|
|
61
97
|
|
|
62
98
|
return Promise.resolve(reporter.getExitCode())
|
|
@@ -5,6 +5,64 @@ import { join } from 'node:path'
|
|
|
5
5
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
6
6
|
import { parseSkillAutoSpec, readSkillMetaRaw } from '../../../scripts/lib/skill-meta.mjs'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Перевіряє поля сирого meta.json одного скіла (без auto.md / відсутності файлу).
|
|
10
|
+
* @param {string} id ідентифікатор скіла
|
|
11
|
+
* @param {Record<string, unknown>} raw сирий meta.json
|
|
12
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
13
|
+
* @returns {boolean} true, якщо всі поля валідні
|
|
14
|
+
*/
|
|
15
|
+
function checkSkillFields(id, raw, reporter) {
|
|
16
|
+
let ok = true
|
|
17
|
+
if (typeof raw.worktree !== 'boolean') {
|
|
18
|
+
reporter.fail(`skills/${id}: meta.json.worktree має бути boolean`)
|
|
19
|
+
ok = false
|
|
20
|
+
}
|
|
21
|
+
if (raw.auto !== undefined && parseSkillAutoSpec(raw.auto) === null) {
|
|
22
|
+
reporter.fail(`skills/${id}: meta.json.auto нерозпізнане — очікується "завжди" або непорожній масив правил`)
|
|
23
|
+
ok = false
|
|
24
|
+
}
|
|
25
|
+
if (raw.requireRoot !== undefined && typeof raw.requireRoot !== 'boolean') {
|
|
26
|
+
reporter.fail(`skills/${id}: meta.json.requireRoot має бути boolean`)
|
|
27
|
+
ok = false
|
|
28
|
+
}
|
|
29
|
+
if (raw.worktree === true && raw.requireRoot === false) {
|
|
30
|
+
reporter.fail(
|
|
31
|
+
`skills/${id}: requireRoot:false суперечить worktree:true (worktree вже вимагає кореня — прибери поле)`
|
|
32
|
+
)
|
|
33
|
+
ok = false
|
|
34
|
+
}
|
|
35
|
+
return ok
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Валідує meta.json одного скіла.
|
|
40
|
+
* @param {string} id ідентифікатор скіла
|
|
41
|
+
* @param {string} skillDir каталог скіла
|
|
42
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
function checkSkill(id, skillDir, reporter) {
|
|
46
|
+
let skillOk = true
|
|
47
|
+
|
|
48
|
+
if (existsSync(join(skillDir, 'auto.md'))) {
|
|
49
|
+
reporter.fail(`skills/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
50
|
+
skillOk = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const raw = readSkillMetaRaw(skillDir)
|
|
54
|
+
if (!raw) {
|
|
55
|
+
reporter.fail(`skills/${id}: відсутній або невалідний meta.json (очікується {"auto"?, "worktree": bool})`)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!checkSkillFields(id, raw, reporter)) skillOk = false
|
|
60
|
+
|
|
61
|
+
if (skillOk) {
|
|
62
|
+
reporter.pass(`skills/${id}: meta.json валідний`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
/**
|
|
9
67
|
* Валідує всі `npm/skills/<id>/meta.json`.
|
|
10
68
|
* @param {string} [cwd] корінь репозиторію
|
|
@@ -20,41 +78,7 @@ export function check(cwd = process.cwd()) {
|
|
|
20
78
|
|
|
21
79
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
22
80
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
23
|
-
|
|
24
|
-
const skillDir = join(skillsDir, id)
|
|
25
|
-
let skillOk = true
|
|
26
|
-
|
|
27
|
-
if (existsSync(join(skillDir, 'auto.md'))) {
|
|
28
|
-
reporter.fail(`skills/${id}: залишковий auto.md — видали (метадані тепер у meta.json)`)
|
|
29
|
-
skillOk = false
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const raw = readSkillMetaRaw(skillDir)
|
|
33
|
-
if (!raw) {
|
|
34
|
-
reporter.fail(`skills/${id}: відсутній або невалідний meta.json (очікується {"auto"?, "worktree": bool})`)
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
if (typeof raw.worktree !== 'boolean') {
|
|
38
|
-
reporter.fail(`skills/${id}: meta.json.worktree має бути boolean`)
|
|
39
|
-
skillOk = false
|
|
40
|
-
}
|
|
41
|
-
if (raw.auto !== undefined && parseSkillAutoSpec(raw.auto) === null) {
|
|
42
|
-
reporter.fail(`skills/${id}: meta.json.auto нерозпізнане — очікується "завжди" або непорожній масив правил`)
|
|
43
|
-
skillOk = false
|
|
44
|
-
}
|
|
45
|
-
if (raw.requireRoot !== undefined && typeof raw.requireRoot !== 'boolean') {
|
|
46
|
-
reporter.fail(`skills/${id}: meta.json.requireRoot має бути boolean`)
|
|
47
|
-
skillOk = false
|
|
48
|
-
}
|
|
49
|
-
if (raw.worktree === true && raw.requireRoot === false) {
|
|
50
|
-
reporter.fail(
|
|
51
|
-
`skills/${id}: requireRoot:false суперечить worktree:true (worktree вже вимагає кореня — прибери поле)`
|
|
52
|
-
)
|
|
53
|
-
skillOk = false
|
|
54
|
-
}
|
|
55
|
-
if (skillOk) {
|
|
56
|
-
reporter.pass(`skills/${id}: meta.json валідний`)
|
|
57
|
-
}
|
|
81
|
+
checkSkill(entry.name, join(skillsDir, entry.name), reporter)
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
return Promise.resolve(reporter.getExitCode())
|
|
@@ -30,7 +30,7 @@ const FALLBACK_VERDICT = {
|
|
|
30
30
|
* @param {string} prompt текст промпта
|
|
31
31
|
* @param {string} model provider/model-id, `omlx/...` або '' для pi-дефолту
|
|
32
32
|
* @returns {string} текст відповіді моделі
|
|
33
|
-
* @throws якщо backend недоступний або повертає помилку
|
|
33
|
+
* @throws {Error} якщо backend недоступний або повертає помилку
|
|
34
34
|
*/
|
|
35
35
|
function callModel(prompt, model) {
|
|
36
36
|
return callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 60_000, caller: 'coverage' })
|
|
@@ -68,7 +68,7 @@ function classifyOne(group, mutant, cwd, callModelFn) {
|
|
|
68
68
|
* Класифікує survived мутантів (resolveModel('min') → CLOUD_MIN → fallback).
|
|
69
69
|
* @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived список вцілілих мутантів
|
|
70
70
|
* @param {string} cwd корінь проєкту
|
|
71
|
-
* @param {{cachePath?: string, callModel?:
|
|
71
|
+
* @param {{cachePath?: string, callModel?: (prompt: string, model: string) => string}} [opts] ін'єкції для тестів
|
|
72
72
|
* @returns {Promise<Array<{key: string, verdict: object}>>} verdicts
|
|
73
73
|
*/
|
|
74
74
|
export function classify(survived, cwd, opts = {}) {
|
|
@@ -22,7 +22,7 @@ export const VerdictSchema = z.object({
|
|
|
22
22
|
* Витягує JSON-об'єкт з raw-text LLM-відповіді і валідує через VerdictSchema.
|
|
23
23
|
* @param {string} rawText raw-text відповідь LLM
|
|
24
24
|
* @returns {{verdict: string, confidence: number, reason: string, suggestedTest?: string}} verdict
|
|
25
|
-
* @throws якщо JSON не знайдено, не парситься, або не відповідає схемі
|
|
25
|
+
* @throws {Error} якщо JSON не знайдено, не парситься, або не відповідає схемі
|
|
26
26
|
*/
|
|
27
27
|
export function parseVerdict(rawText) {
|
|
28
28
|
const jsonStart = rawText.indexOf('{')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Guard для дефолтної синхронізації `npx
|
|
2
|
+
* Guard для дефолтної синхронізації `npx \@nitra/cursor` (гілка без підкоманди).
|
|
3
3
|
*
|
|
4
4
|
* Дефолтний sync (`runSync` у `bin/n-cursor.js`) скаффолдить у `cwd()` керовані
|
|
5
5
|
* артефакти — `.cursor/rules/`, `.cursor/skills/`, `.claude/`, `AGENTS.md`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Визначає список id правил для `npx
|
|
2
|
+
* Визначає список id правил для `npx \@nitra/cursor fix` без аргументів:
|
|
3
3
|
* зчитує базові імена `*.mdc` у `.cursor/rules/` і залишає лише ті id,
|
|
4
4
|
* для яких у пакеті є programmatic перевірка (JS-концерн або policy з target.json).
|
|
5
5
|
*/
|
|
@@ -23,35 +23,44 @@ const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
|
23
23
|
*/
|
|
24
24
|
async function anyDepInTree(root, keys) {
|
|
25
25
|
const wanted = new Set(keys)
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Чи package.json за `abs` оголошує будь-який пакет із `wanted` у `dependencies`.
|
|
28
|
+
* @param {string} abs шлях до package.json
|
|
29
|
+
* @returns {Promise<boolean>} true, якщо знайдено хоч один
|
|
30
|
+
*/
|
|
31
|
+
async function pkgDeclaresWanted(abs) {
|
|
32
|
+
try {
|
|
33
|
+
const deps = JSON.parse(await readFile(abs, 'utf8'))?.dependencies
|
|
34
|
+
if (deps && typeof deps === 'object' && !Array.isArray(deps)) {
|
|
35
|
+
for (const k of wanted) if (Object.hasOwn(deps, k)) return true
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
/* ігноруємо пошкоджені package.json */
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} dir каталог обходу
|
|
44
|
+
* @returns {Promise<boolean>} true, якщо знайдено хоч один пакет
|
|
45
|
+
*/
|
|
28
46
|
async function walk(dir) {
|
|
29
|
-
if (found) return
|
|
30
47
|
let entries
|
|
31
48
|
try {
|
|
32
49
|
entries = await readdir(dir, { withFileTypes: true })
|
|
33
50
|
} catch {
|
|
34
|
-
return
|
|
51
|
+
return false
|
|
35
52
|
}
|
|
36
53
|
for (const entry of entries) {
|
|
37
|
-
if (found) return
|
|
38
54
|
const abs = join(dir, entry.name)
|
|
39
55
|
if (entry.isDirectory()) {
|
|
40
|
-
if (!IGNORED_DIR_NAMES.has(entry.name)
|
|
41
|
-
} else if (entry.isFile() && entry.name === 'package.json') {
|
|
42
|
-
|
|
43
|
-
const deps = JSON.parse(await readFile(abs, 'utf8'))?.dependencies
|
|
44
|
-
if (deps && typeof deps === 'object' && !Array.isArray(deps)) {
|
|
45
|
-
for (const k of wanted) if (Object.hasOwn(deps, k)) found = true
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
/* ігноруємо пошкоджені package.json */
|
|
49
|
-
}
|
|
56
|
+
if (!IGNORED_DIR_NAMES.has(entry.name) && (await walk(abs))) return true
|
|
57
|
+
} else if (entry.isFile() && entry.name === 'package.json' && (await pkgDeclaresWanted(abs))) {
|
|
58
|
+
return true
|
|
50
59
|
}
|
|
51
60
|
}
|
|
61
|
+
return false
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
return found
|
|
63
|
+
return walk(root)
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -62,7 +71,10 @@ async function anyDepInTree(root, keys) {
|
|
|
62
71
|
async function nestedWithoutVite(root) {
|
|
63
72
|
const rootPkg = join(root, 'package.json')
|
|
64
73
|
let result = false
|
|
65
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} dir каталог
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
66
78
|
async function walk(dir) {
|
|
67
79
|
if (result) return
|
|
68
80
|
let entries
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standalone CLI runner для одного правила. Викликається з `rules/<id>/fix.mjs`
|
|
3
3
|
* у блоці `if (import.meta.main)` — це робить `bun rules/<id>/fix.mjs` повним
|
|
4
|
-
* еквівалентом старого `npx
|
|
4
|
+
* еквівалентом старого `npx \@nitra/cursor fix <id>`: читає `.n-cursor.json`,
|
|
5
5
|
* перевіряє whitelist, друкує summary, повертає aggregated exit-code.
|
|
6
6
|
*
|
|
7
7
|
* Library-mode виклик з CLI orchestration — інше: див. `runStandardRule` + `fix.mjs::run(ctx)`.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Локальна логіка в правилах заборонена; розширення поведінки — через `ctx`-опції.
|
|
6
6
|
*
|
|
7
7
|
* Серіалізація: загортає виконання у `withLock('fix-<ruleId>')` — паралельні запуски
|
|
8
|
-
* того самого правила (через `npx
|
|
8
|
+
* того самого правила (через `npx \@nitra/cursor fix`, прямий `bun rules/<id>/fix.mjs`
|
|
9
9
|
* чи `run(ctx)`-композицію) дедупляться за станом git-дерева; різні правила можуть
|
|
10
10
|
* виконуватись паралельно. Точка інтеграції — тут, щоб не дублювати лок у кожному
|
|
11
11
|
* `fix.mjs`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PostToolUse hook для Claude Code: точкова маршрутизація `npx
|
|
2
|
+
* PostToolUse hook для Claude Code: точкова маршрутизація `npx \@nitra/cursor fix`
|
|
3
3
|
* за типом зміненого файла. Запускається після кожного `Edit` / `Write` / `MultiEdit`;
|
|
4
4
|
* замінює дорогий синхронний `Stop`-хук, що ганяв повний `fix` усіх правил на кожному
|
|
5
5
|
* turn-і.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - stdin Claude Code: JSON із `tool_input.file_path` (відносний шлях зміненого файла);
|
|
9
9
|
* - exit 0, якщо файл не має маршрут (PostToolUse не блокує turn у будь-якому випадку,
|
|
10
10
|
* але ми лишаємо exit-код прозорим — для діагностики);
|
|
11
|
-
* - інакше spawn `npx --no
|
|
11
|
+
* - інакше spawn `npx --no \@nitra/cursor fix <rules…>` із передаванням exit-коду.
|
|
12
12
|
*
|
|
13
13
|
* Маршрути впорядковані від найбільш специфічного до загального; перший збіг — переможець.
|
|
14
14
|
* `docs/adr/**\/*.md` свідомо повертає `[]`: ADR-нормалізація вже покривається async
|
|
@@ -46,7 +46,7 @@ const ROUTES = Object.freeze([
|
|
|
46
46
|
* Повертає список правил, які слід прогнати для зміненого `filePath`.
|
|
47
47
|
* Перший збіг із `ROUTES` — переможець; невідомі шляхи / некоректні входи → `[]`.
|
|
48
48
|
* @param {unknown} filePath відносний шлях зміненого файла зі stdin Claude Code
|
|
49
|
-
* @returns {string[]} ID правил для `npx
|
|
49
|
+
* @returns {string[]} ID правил для `npx \@nitra/cursor fix`
|
|
50
50
|
*/
|
|
51
51
|
export function routeFilePathToRules(filePath) {
|
|
52
52
|
if (typeof filePath !== 'string' || filePath === '') {
|
package/scripts/skills-cli.mjs
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* `.n-cursor.json`) — далі stdout або делегування в `cursor-agent` / `claude`.
|
|
7
7
|
*
|
|
8
8
|
* Підтримувані формати:
|
|
9
|
-
* `npx
|
|
10
|
-
* `npx
|
|
11
|
-
* `npx
|
|
12
|
-
* `npx
|
|
13
|
-
* `npx
|
|
9
|
+
* `npx \@nitra/cursor skill list`
|
|
10
|
+
* `npx \@nitra/cursor skill taze`
|
|
11
|
+
* `npx \@nitra/cursor skill cursor taze`
|
|
12
|
+
* `npx \@nitra/cursor skill cursor taze "онови залежності"`
|
|
13
|
+
* `npx \@nitra/cursor skill claude taze` — те саме через Claude Code CLI
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { spawnSync } from 'node:child_process'
|
package/scripts/worktree-cli.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function listDescFiles(cwd) {
|
|
|
79
79
|
/**
|
|
80
80
|
* add: створити worktree від HEAD + .md-опис.
|
|
81
81
|
* @param {string[]} rest [branch, ...descParts]
|
|
82
|
-
* @param {{ cwd: string, log:
|
|
82
|
+
* @param {{ cwd: string, log: (line: string) => void, logError: (line: string) => void, now: () => Date }} ctx контекст
|
|
83
83
|
* @returns {number} exit code
|
|
84
84
|
*/
|
|
85
85
|
function cmdAdd(rest, ctx) {
|
|
@@ -135,7 +135,7 @@ function cmdAdd(rest, ctx) {
|
|
|
135
135
|
/**
|
|
136
136
|
* remove: прибрати checkout + .md (гілку лишає).
|
|
137
137
|
* @param {string[]} rest [branch, ...flags]
|
|
138
|
-
* @param {{ cwd: string, log:
|
|
138
|
+
* @param {{ cwd: string, log: (line: string) => void, logError: (line: string) => void }} ctx контекст
|
|
139
139
|
* @returns {number} exit code
|
|
140
140
|
*/
|
|
141
141
|
function cmdRemove(rest, ctx) {
|
|
@@ -167,7 +167,7 @@ function cmdRemove(rest, ctx) {
|
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
169
|
* list: git worktree list + вміст .md-описів.
|
|
170
|
-
* @param {{ cwd: string, log:
|
|
170
|
+
* @param {{ cwd: string, log: (line: string) => void }} ctx контекст
|
|
171
171
|
* @returns {number} exit code
|
|
172
172
|
*/
|
|
173
173
|
function cmdList(ctx) {
|
|
@@ -181,7 +181,7 @@ function cmdList(ctx) {
|
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
183
|
* prune: git worktree prune + видалити осиротілі .md.
|
|
184
|
-
* @param {{ cwd: string, log:
|
|
184
|
+
* @param {{ cwd: string, log: (line: string) => void }} ctx контекст
|
|
185
185
|
* @returns {number} exit code
|
|
186
186
|
*/
|
|
187
187
|
function cmdPrune(ctx) {
|
|
@@ -198,7 +198,7 @@ function cmdPrune(ctx) {
|
|
|
198
198
|
/**
|
|
199
199
|
* Точка входу підкоманди worktree.
|
|
200
200
|
* @param {string[]} argv аргументи після `worktree`
|
|
201
|
-
* @param {{ cwd?: string, log?:
|
|
201
|
+
* @param {{ cwd?: string, log?: (line: string) => void, logError?: (line: string) => void, now?: () => Date }} [options] ін'єкція для тестів
|
|
202
202
|
* @returns {Promise<number>} exit code
|
|
203
203
|
*/
|
|
204
204
|
export function runWorktreeCli(argv, options = {}) {
|
|
@@ -65,7 +65,7 @@ function cleanJsDoc(raw) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Опис (без @-тегів) + параметри з
|
|
68
|
+
* Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
69
69
|
* @param {string} raw сирий JSDoc-блок
|
|
70
70
|
* @returns {{desc:string, params:Array<{name:string, desc:string}>, ret:string}} розпарсений опис, параметри й опис повернення
|
|
71
71
|
*/
|
|
@@ -79,6 +79,68 @@ function preflightProblem() {
|
|
|
79
79
|
return `omlx помилка: ${hc.detail}`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Текст-суфікс режиму для прогрес-рядка.
|
|
84
|
+
* @param {{ overwrite: boolean, retryDegraded: boolean }} mode режими
|
|
85
|
+
* @returns {string} ` (--overwrite)` / ` (--retry-degraded)` / порожній рядок
|
|
86
|
+
*/
|
|
87
|
+
function modeSuffix({ overwrite, retryDegraded }) {
|
|
88
|
+
if (overwrite) return ' (--overwrite)'
|
|
89
|
+
if (retryDegraded) return ' (--retry-degraded)'
|
|
90
|
+
return ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Генерує й штампує доку для одного файлу, оновлюючи лічильники й прогрес.
|
|
95
|
+
* @param {object} file елемент scanForDocFiles
|
|
96
|
+
* @param {string} root абсолютний корінь
|
|
97
|
+
* @param {{ done: number, total: number }} progress позиція у прогресі
|
|
98
|
+
* @param {{ ok: number, degraded: number, err: number, errors: string[] }} stats акумулятор статистики
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
async function generateOne(file, root, progress, stats) {
|
|
102
|
+
const sourceAbs = join(root, file.sourcePath)
|
|
103
|
+
process.stdout.write(` [${progress.done}/${progress.total}] ${file.sourcePath} … `)
|
|
104
|
+
try {
|
|
105
|
+
const docAbs = join(root, file.docPath)
|
|
106
|
+
// Варіант B: передаємо наявну доку, щоб зберегти захищену секцію «Призначення»
|
|
107
|
+
const existingMd = existsSync(docAbs) ? readFileSync(docAbs, 'utf8') : null
|
|
108
|
+
const result = await generateDoc(sourceAbs, { existingMd })
|
|
109
|
+
const crc = crc32(readFileSync(sourceAbs))
|
|
110
|
+
mkdirSync(dirname(docAbs), { recursive: true })
|
|
111
|
+
const quality =
|
|
112
|
+
result.score === null ? null : { score: result.score, issues: result.degraded ? result.issues : [] }
|
|
113
|
+
writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality))
|
|
114
|
+
stats.ok++
|
|
115
|
+
if (result.degraded) {
|
|
116
|
+
stats.degraded++
|
|
117
|
+
process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`)
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`)
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
stats.err++
|
|
123
|
+
stats.errors.push(file.sourcePath)
|
|
124
|
+
process.stdout.write(`✗ ${error.message}\n`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Підсумковий звіт прогону у stdout.
|
|
130
|
+
* @param {{ ok: number, degraded: number, err: number, errors: string[] }} stats статистика
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
function reportStats(stats) {
|
|
134
|
+
console.log(`\n${'─'.repeat(50)}\n✓ OK: ${stats.ok} ⚠ degraded: ${stats.degraded} ✗ Err: ${stats.err}`)
|
|
135
|
+
if (stats.errors.length > 0) {
|
|
136
|
+
console.log('Помилки:')
|
|
137
|
+
for (const e of stats.errors) console.log(` - ${e}`)
|
|
138
|
+
}
|
|
139
|
+
if (stats.degraded > 0) {
|
|
140
|
+
console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
82
144
|
/**
|
|
83
145
|
* `doc-files gen` — згенерувати документацію для застарілих/відсутніх док.
|
|
84
146
|
* @param {string[]} argv аргументи після назви субкоманди
|
|
@@ -106,47 +168,16 @@ export async function runDocFilesGenCli(argv) {
|
|
|
106
168
|
return 1
|
|
107
169
|
}
|
|
108
170
|
|
|
109
|
-
|
|
110
|
-
if (overwrite) modeTxt = ' (--overwrite)'
|
|
111
|
-
else if (retryDegraded) modeTxt = ' (--retry-degraded)'
|
|
112
|
-
console.log(`📋 doc-files: до генерації ${targets.length} файл(ів)${modeTxt}`)
|
|
171
|
+
console.log(`📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite, retryDegraded })}`)
|
|
113
172
|
const stats = { ok: 0, degraded: 0, err: 0, errors: [] }
|
|
114
173
|
|
|
115
174
|
let done = 0
|
|
116
175
|
for (const file of targets) {
|
|
117
176
|
done++
|
|
118
|
-
|
|
119
|
-
process.stdout.write(` [${done}/${targets.length}] ${file.sourcePath} … `)
|
|
120
|
-
try {
|
|
121
|
-
const result = await generateDoc(sourceAbs)
|
|
122
|
-
const crc = crc32(readFileSync(sourceAbs))
|
|
123
|
-
const docAbs = join(root, file.docPath)
|
|
124
|
-
mkdirSync(dirname(docAbs), { recursive: true })
|
|
125
|
-
const quality =
|
|
126
|
-
result.score === null ? null : { score: result.score, issues: result.degraded ? result.issues : [] }
|
|
127
|
-
writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality))
|
|
128
|
-
stats.ok++
|
|
129
|
-
if (result.degraded) {
|
|
130
|
-
stats.degraded++
|
|
131
|
-
process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`)
|
|
132
|
-
} else {
|
|
133
|
-
process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`)
|
|
134
|
-
}
|
|
135
|
-
} catch (error) {
|
|
136
|
-
stats.err++
|
|
137
|
-
stats.errors.push(file.sourcePath)
|
|
138
|
-
process.stdout.write(`✗ ${error.message}\n`)
|
|
139
|
-
}
|
|
177
|
+
await generateOne(file, root, { done, total: targets.length }, stats)
|
|
140
178
|
}
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
if (stats.errors.length > 0) {
|
|
144
|
-
console.log('Помилки:')
|
|
145
|
-
for (const e of stats.errors) console.log(` - ${e}`)
|
|
146
|
-
}
|
|
147
|
-
if (stats.degraded > 0) {
|
|
148
|
-
console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`)
|
|
149
|
-
}
|
|
180
|
+
reportStats(stats)
|
|
150
181
|
return stats.err > 0 ? 1 : 0
|
|
151
182
|
}
|
|
152
183
|
|