@nitra/cursor 1.13.62 → 1.13.64
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 +24 -0
- package/bin/n-cursor.js +1 -0
- package/package.json +1 -1
- package/rules/adr/adr.mdc +1 -1
- package/rules/adr/fix/hooks/template/.gitignore.snippet +8 -0
- package/rules/changelog/changelog.mdc +26 -14
- package/rules/changelog/fix/consistency/check.mjs +110 -64
- package/scripts/sync-claude-config.mjs +72 -4
- package/skills/lint/SKILL.md +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.64] - 2026-05-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`sync-claude-config`**: при увімкненому правилі `adr` `npx @nitra/cursor` дописує в кореневий `.gitignore` канонічний фрагмент `rules/adr/fix/hooks/template/.gitignore.snippet` (`.claude/hooks/*.log`, `.normalize-state`, `.normalize.lock`) — логи ADR Stop-hook більше не потрапляють у git status.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Правило **`adr`**: посилання на канон `.gitignore.snippet` і згадка автоматичного дописування під час sync.
|
|
16
|
+
- **`.gitignore.snippet`**: додано базові рядки `node_modules/`, `dist/`, `*.secret` (як у кореневому `.gitignore` пакета).
|
|
17
|
+
|
|
18
|
+
## [1.13.63] - 2026-05-20
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`check changelog`**: новий local-only воркспейс (маніфест відсутній на merge-base з `dev`/`main`, напр. `demo/` на `main`) більше не вимагає штучного bump — достатньо початкової `version` і запису в `CHANGELOG.md` (раніше `Vbase === ∅` помилково трактувалось як «version не підвищено»).
|
|
23
|
+
- **`check changelog`**: на гілці **`main`** база порівняння — **`origin/main`** (або `HEAD~1` без remote), не `dev`; коли `origin/main` збігається з `HEAD`, diff порожній (не fallback на `HEAD~1`); feature-гілки — `merge-base` з `dev`, інакше з `main` (репо без `dev`).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Правило **`changelog`** ([changelog.mdc](rules/changelog/changelog.mdc) `2.5`): блок **STOP** перенесено на початок (тригер шляхів, інверсія, три кроки до завершення відповіді) — щоб агент не пропускав bump після правок у `npm/skills/` тощо, коли чеклист губився внизу довгого alwaysApply-правила.
|
|
28
|
+
- **`.cursor/rules/scripts.mdc`**: секція «Завершення задачі після правок у пакетному workspace» — cross-STOP з **n-changelog** (останні кроки сесії перед відповіддю).
|
|
29
|
+
- **`hk.pkl`**: pre-commit крок **`npm-changelog`** (`glob: npm/**`, `bun ./npm/bin/n-cursor.js check changelog`) — програмний стоп-кран при commit, якщо агент забув bump.
|
|
30
|
+
|
|
7
31
|
## [1.13.62] - 2026-05-20
|
|
8
32
|
|
|
9
33
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -1274,6 +1274,7 @@ async function runSync() {
|
|
|
1274
1274
|
if (result.commands.length > 0) parts.push(`${result.commands.length} slash-commands`)
|
|
1275
1275
|
if (result.adrHook) parts.push('.claude/hooks/capture-decisions.sh')
|
|
1276
1276
|
if (result.adrNormalizeHook) parts.push('.claude/hooks/normalize-decisions.sh')
|
|
1277
|
+
if (result.gitignoreAdr) parts.push('.gitignore (adr fragment)')
|
|
1277
1278
|
if (parts.length > 0) {
|
|
1278
1279
|
console.log(`🤖 Claude-конфіг: ${parts.join(', ')}`)
|
|
1279
1280
|
}
|
package/package.json
CHANGED
package/rules/adr/adr.mdc
CHANGED
|
@@ -95,7 +95,7 @@ docs/adr/
|
|
|
95
95
|
└── hooks.json # Cursor Agent stop-hooks для тих самих скриптів
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
`.gitignore` повинен містити
|
|
98
|
+
`.gitignore` у корені проєкту повинен містити базові рядки (`node_modules/`, `dist/`, `*.secret`) і патерни для ADR Stop-hook (**`.claude/hooks/*.log`**, `.claude/hooks/.normalize-state`, `.claude/hooks/.normalize.lock`). Канонічний фрагмент (дописується `npx @nitra/cursor`, коли правило `adr` увімкнене): [.gitignore.snippet](./fix/hooks/template/.gitignore.snippet).
|
|
99
99
|
|
|
100
100
|
## Stop-hook у `.claude/settings.json`
|
|
101
101
|
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
|
|
3
|
-
version: '2.
|
|
3
|
+
version: '2.5'
|
|
4
4
|
alwaysApply: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## STOP — перед завершенням відповіді агента
|
|
8
|
+
|
|
9
|
+
> **Якщо в цій сесії ти змінив(ла) файли в пакетному workspace** (код, rego, правила, скіли, скрипти, конфіги, тести — **не** лише `docs/` / `doc/`) — **не завершуй задачу**, поки не виконаєш **усі три** кроки нижче в **тому ж** наборі змін. Це не «опційно після синку» — це частина PR.
|
|
10
|
+
|
|
11
|
+
1. **`version`** у `<ws>/package.json` (або `[project].version` у `pyproject.toml`) → **patch +1** відносно `git show HEAD:<ws>/package.json`, якщо ще не піднято.
|
|
12
|
+
2. **`CHANGELOG.md`** того workspace → **нова** секція `## [версія] - YYYY-MM-DD` **зверху** (не bullet-и в стару версію).
|
|
13
|
+
3. **`npx @nitra/cursor check changelog`** (у репо `@nitra/cursor`: `bun ./npm/bin/n-cursor.js check changelog`) → exit **`0`**.
|
|
14
|
+
|
|
15
|
+
**Тригер шляхів (приклади):** `npm/**`, `packages/foo/**`, будь-який каталог з власним `package.json` / `pyproject.toml`, куди потрапили правки.
|
|
16
|
+
|
|
17
|
+
**Інверсія (bump не потрібен):** лише `docs/` / `doc/`; лише `.gitignore`; лише сам релізний крок (`CHANGELOG.md` + `version`).
|
|
18
|
+
|
|
19
|
+
**Pre-commit (людина):** `hk` у цьому репо також запускає `check changelog` при змінах під `npm/**` — агент не покладайся лише на commit hook; виконай кроки 1–3 **до** фінальної відповіді.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
7
23
|
У кожному **пакетному** workspace (каталог із `package.json` або `pyproject.toml`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій.
|
|
8
24
|
|
|
9
25
|
**Маніфест версії:**
|
|
@@ -13,7 +29,9 @@ alwaysApply: true
|
|
|
13
29
|
|
|
14
30
|
Каталоги лише з `pyproject.toml` (без `package.json`) теж враховуються; `node_modules/`, `.venv/`, `venv/` при пошуку ігноруються.
|
|
15
31
|
|
|
16
|
-
## Чеклист агента (
|
|
32
|
+
## Чеклист агента (деталі)
|
|
33
|
+
|
|
34
|
+
Повний алгоритм — у блоці **STOP** вище; тут лише уточнення.
|
|
17
35
|
|
|
18
36
|
**Інверсія (за замовчуванням не вимагають bump/CHANGELOG):**
|
|
19
37
|
|
|
@@ -21,13 +39,7 @@ alwaysApply: true
|
|
|
21
39
|
- файли під **`.gitignore`**;
|
|
22
40
|
- правки **лише** `CHANGELOG.md` або поля `version` у маніфесті як сам релізний крок.
|
|
23
41
|
|
|
24
|
-
**Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, конфіги, тести тощо).
|
|
25
|
-
|
|
26
|
-
Перед завершенням задачі:
|
|
27
|
-
|
|
28
|
-
1. **`version`** у `<ws>/package.json` або `[project].version` у `<ws>/pyproject.toml` підвищено (build/patch +1 на PR), якщо зміни входять у реліз.
|
|
29
|
-
2. **`CHANGELOG.md`** — **нова** секція `## [версія] - YYYY-MM-DD` зверху (не дописуй bullet-и в стару версію).
|
|
30
|
-
3. **`npx @nitra/cursor check changelog`** — exit `0`.
|
|
42
|
+
**Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, скіли, конфіги, тести тощо).
|
|
31
43
|
|
|
32
44
|
Перевірка програмна (`changelog/fix/consistency/check.mjs`).
|
|
33
45
|
|
|
@@ -48,14 +60,14 @@ alwaysApply: true
|
|
|
48
60
|
|
|
49
61
|
### local-only
|
|
50
62
|
|
|
51
|
-
**npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. База
|
|
63
|
+
**npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. База залежить від гілки:
|
|
52
64
|
|
|
53
65
|
1. На **`dev`** local-only не активний (крім незакомічених registry-published).
|
|
54
|
-
2. На
|
|
55
|
-
3.
|
|
56
|
-
4.
|
|
66
|
+
2. На **`main`** — diff від **`origin/main`** (попередній опублікований `main`); без remote — від `HEAD~1`. **`dev` не використовується** як база на `main`.
|
|
67
|
+
3. На **feature-гілці** — `merge-base` з **`dev`**, якщо є; інакше з **`main`** (репо без `dev`).
|
|
68
|
+
4. Bump + CHANGELOG **раз на PR** / direct-commit на `main`.
|
|
57
69
|
|
|
58
|
-
Якщо немає git або немає `dev`/`main` — local-only пропускається.
|
|
70
|
+
Якщо немає git або немає `dev`/`main`/`origin/main` — local-only пропускається.
|
|
59
71
|
|
|
60
72
|
## Формат CHANGELOG.md
|
|
61
73
|
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Якщо локальна версія відрізняється — потрібен CHANGELOG; для npm також `"CHANGELOG.md"`
|
|
11
11
|
* у `files`. Якщо версії збігаються, але в git є релевантні зміни без bump — fail.
|
|
12
12
|
*
|
|
13
|
-
* 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру):
|
|
14
|
-
*
|
|
13
|
+
* 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру):
|
|
14
|
+
* feature-гілка — `merge-base` з `dev`, інакше з `main`; на `main` — diff від
|
|
15
|
+
* `origin/main` (попередній опублікований main) або `HEAD~1` без remote.
|
|
15
16
|
*
|
|
16
17
|
* Усі `git` і зовнішні виклики — через `execFile` / `fetch`, без shell-інтерполяції.
|
|
17
18
|
*/
|
|
@@ -31,11 +32,11 @@ import {
|
|
|
31
32
|
|
|
32
33
|
const execFileAsync = promisify(execFile)
|
|
33
34
|
|
|
34
|
-
/** Кандидати інтеграційної гілки (перша
|
|
35
|
-
const
|
|
35
|
+
/** Кандидати інтеграційної гілки для feature-гілок (перша наявна; див. n-changelog.mdc). */
|
|
36
|
+
const FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
-
const
|
|
38
|
+
/** Гілка `dev`: local-only не активний (крім незакомічених registry-published). */
|
|
39
|
+
const LOCAL_ONLY_SKIP_BRANCH = 'dev'
|
|
39
40
|
|
|
40
41
|
/** Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc). */
|
|
41
42
|
const CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/'])
|
|
@@ -78,14 +79,6 @@ async function currentBranchName() {
|
|
|
78
79
|
return typeof out === 'string' ? out.trim() : null
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/**
|
|
82
|
-
* @param {string | null} branch параметр
|
|
83
|
-
* @returns {boolean} результат
|
|
84
|
-
*/
|
|
85
|
-
function isIntegrationBranch(branch) {
|
|
86
|
-
return branch !== null && INTEGRATION_BRANCHES.includes(branch)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
82
|
/**
|
|
90
83
|
* @param {string} ref параметр
|
|
91
84
|
* @returns {string} результат
|
|
@@ -94,6 +87,30 @@ function baseRefLabel(ref) {
|
|
|
94
87
|
return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
|
|
95
88
|
}
|
|
96
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} ancestor предок
|
|
92
|
+
* @param {string} descendant нащадок
|
|
93
|
+
* @returns {Promise<boolean>} результат
|
|
94
|
+
*/
|
|
95
|
+
async function isGitAncestor(ancestor, descendant) {
|
|
96
|
+
const out = await gitOrNull(['merge-base', '--is-ancestor', ancestor, descendant])
|
|
97
|
+
return typeof out === 'string' && out.trim() === 'true'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} branchName локальна або remote-tracking гілка
|
|
102
|
+
* @returns {Promise<string | null>} ref для git або null
|
|
103
|
+
*/
|
|
104
|
+
async function resolveBranchRef(branchName) {
|
|
105
|
+
for (const ref of [branchName, `origin/${branchName}`]) {
|
|
106
|
+
const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
|
|
107
|
+
if (typeof out === 'string' && out.trim().length > 0) {
|
|
108
|
+
return ref
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
/**
|
|
98
115
|
* @param {string} relPath параметр
|
|
99
116
|
* @returns {boolean} результат
|
|
@@ -119,21 +136,6 @@ async function isPathGitIgnored(relPath) {
|
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
/**
|
|
123
|
-
* @returns {Promise<string | null>} результат
|
|
124
|
-
*/
|
|
125
|
-
async function resolveBaseRef() {
|
|
126
|
-
for (const name of BASE_BRANCH_CANDIDATES) {
|
|
127
|
-
for (const ref of [name, `origin/${name}`]) {
|
|
128
|
-
const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
|
|
129
|
-
if (typeof out === 'string' && out.trim().length > 0) {
|
|
130
|
-
return ref
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return null
|
|
135
|
-
}
|
|
136
|
-
|
|
137
139
|
/**
|
|
138
140
|
* @param {string} baseRef параметр
|
|
139
141
|
* @returns {Promise<string | null>} результат
|
|
@@ -145,6 +147,45 @@ async function resolveMergeBase(baseRef) {
|
|
|
145
147
|
return sha.length > 0 ? sha : null
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Точка порівняння git для changelog (ref або SHA для `git diff` / `git show`).
|
|
152
|
+
* @param {string | null} branch поточна гілка
|
|
153
|
+
* @returns {Promise<{ ref: string, label: string } | null>} результат
|
|
154
|
+
*/
|
|
155
|
+
async function resolveChangelogComparisonPoint(branch) {
|
|
156
|
+
if (branch === LOCAL_ONLY_SKIP_BRANCH) {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (branch === 'main') {
|
|
161
|
+
const originMainRaw = await gitOrNull(['rev-parse', '--verify', '--quiet', 'origin/main'])
|
|
162
|
+
const originMainSha = originMainRaw?.trim()
|
|
163
|
+
const headRaw = await gitOrNull(['rev-parse', 'HEAD'])
|
|
164
|
+
const headSha = headRaw?.trim()
|
|
165
|
+
if (originMainSha && headSha && (originMainSha === headSha || (await isGitAncestor('origin/main', 'HEAD')))) {
|
|
166
|
+
return { ref: 'origin/main', label: 'main' }
|
|
167
|
+
}
|
|
168
|
+
const parent = await gitOrNull(['rev-parse', '--verify', '--quiet', 'HEAD~1'])
|
|
169
|
+
if (typeof parent === 'string' && parent.trim().length > 0) {
|
|
170
|
+
return { ref: parent.trim(), label: 'main~1' }
|
|
171
|
+
}
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const name of FEATURE_BASE_BRANCH_CANDIDATES) {
|
|
176
|
+
const baseRef = await resolveBranchRef(name)
|
|
177
|
+
if (!baseRef) {
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
const mergeBase = await resolveMergeBase(baseRef)
|
|
181
|
+
if (!mergeBase) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
return { ref: mergeBase, label: baseRefLabel(baseRef) }
|
|
185
|
+
}
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
148
189
|
/**
|
|
149
190
|
* @param {string} ws параметр
|
|
150
191
|
* @param {string[]} subWorkspaces параметр
|
|
@@ -355,7 +396,8 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
|
|
|
355
396
|
}
|
|
356
397
|
|
|
357
398
|
const branch = await currentBranchName()
|
|
358
|
-
|
|
399
|
+
|
|
400
|
+
if (branch === LOCAL_ONLY_SKIP_BRANCH) {
|
|
359
401
|
if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces)) {
|
|
360
402
|
fail(
|
|
361
403
|
`${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
|
|
@@ -365,28 +407,32 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
|
|
|
365
407
|
return
|
|
366
408
|
}
|
|
367
409
|
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
410
|
+
const comparison = await resolveChangelogComparisonPoint(branch)
|
|
411
|
+
if (comparison && (await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) {
|
|
412
|
+
const Vbase = await readBaseVersion(comparison.ref, manifest)
|
|
413
|
+
const baseLabel = comparison.label
|
|
414
|
+
if (Vbase === null) {
|
|
415
|
+
pass(
|
|
416
|
+
`${label}: новий registry-published воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`
|
|
417
|
+
)
|
|
418
|
+
await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail)
|
|
419
|
+
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
420
|
+
} else if (Vbase === Vcurrent) {
|
|
421
|
+
fail(
|
|
422
|
+
`${label}: у цій гілці є зміни в registry-published пакеті, але version у ${mf} ` +
|
|
423
|
+
`не підвищено (на ${baseLabel} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові (n-changelog.mdc)`
|
|
424
|
+
)
|
|
425
|
+
} else {
|
|
426
|
+
pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
|
|
427
|
+
}
|
|
378
428
|
}
|
|
379
429
|
|
|
380
|
-
|
|
381
|
-
const baseLabel = baseRefLabel(baseRef)
|
|
382
|
-
if (Vbase === null || Vbase === Vcurrent) {
|
|
430
|
+
if (branch === 'main' && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces))) {
|
|
383
431
|
fail(
|
|
384
|
-
`${label}: у
|
|
385
|
-
|
|
432
|
+
`${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
|
|
433
|
+
`Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
|
|
386
434
|
)
|
|
387
|
-
return
|
|
388
435
|
}
|
|
389
|
-
pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
|
|
390
436
|
}
|
|
391
437
|
|
|
392
438
|
/**
|
|
@@ -426,13 +472,13 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
|
|
|
426
472
|
}
|
|
427
473
|
|
|
428
474
|
/**
|
|
429
|
-
* @param {string}
|
|
475
|
+
* @param {string} comparisonRef ref/SHA для `git diff` / `git show`
|
|
430
476
|
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
|
|
431
477
|
* @param {string} baseLabel параметр
|
|
432
478
|
* @param {(msg: string) => void} pass параметр
|
|
433
479
|
* @param {(msg: string) => void} fail параметр
|
|
434
480
|
*/
|
|
435
|
-
async function checkLocalOnlyChangedWorkspace(
|
|
481
|
+
async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail) {
|
|
436
482
|
const label = workspaceLabel(manifest)
|
|
437
483
|
const mf = manifestFilePath(manifest.ws, manifest)
|
|
438
484
|
const Vcurrent = manifest.version
|
|
@@ -440,10 +486,16 @@ async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pa
|
|
|
440
486
|
fail(`${label}: у ${mf} відсутнє поле version (потрібне для запису в CHANGELOG)`)
|
|
441
487
|
return
|
|
442
488
|
}
|
|
443
|
-
const Vbase = await readBaseVersion(
|
|
444
|
-
if (Vbase === null
|
|
489
|
+
const Vbase = await readBaseVersion(comparisonRef, manifest)
|
|
490
|
+
if (Vbase === null) {
|
|
491
|
+
pass(`${label}: новий воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`)
|
|
492
|
+
if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail))) return
|
|
493
|
+
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
if (Vbase === Vcurrent) {
|
|
445
497
|
fail(
|
|
446
|
-
`${label}: у цій гілці є зміни, але version у ${mf} не підвищено (на ${baseLabel} — ${Vbase
|
|
498
|
+
`${label}: у цій гілці є зміни, але version у ${mf} не підвищено (на ${baseLabel} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові на PR`
|
|
447
499
|
)
|
|
448
500
|
return
|
|
449
501
|
}
|
|
@@ -466,30 +518,24 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
|
|
|
466
518
|
return
|
|
467
519
|
}
|
|
468
520
|
const branch = await currentBranchName()
|
|
469
|
-
if (branch ===
|
|
521
|
+
if (branch === LOCAL_ONLY_SKIP_BRANCH) {
|
|
470
522
|
pass('changelog: поточна гілка = dev — local-only перевірку пропущено')
|
|
471
523
|
return
|
|
472
524
|
}
|
|
473
|
-
const
|
|
474
|
-
if (!
|
|
525
|
+
const comparison = await resolveChangelogComparisonPoint(branch)
|
|
526
|
+
if (!comparison) {
|
|
475
527
|
pass('changelog: ref dev/main (та origin/*) не знайдено — local-only перевірку пропущено')
|
|
476
528
|
return
|
|
477
529
|
}
|
|
478
|
-
const mergeBase = await resolveMergeBase(baseRef)
|
|
479
|
-
if (!mergeBase) {
|
|
480
|
-
pass(`changelog: merge-base з ${baseRef} не знайдено — local-only перевірку пропущено`)
|
|
481
|
-
return
|
|
482
|
-
}
|
|
483
530
|
|
|
484
|
-
const baseLabel = baseRefLabel(baseRef)
|
|
485
531
|
let checkedAny = false
|
|
486
532
|
for (const manifest of localOnly) {
|
|
487
|
-
if (!(await workspaceHasRelevantChangesAgainstBase(
|
|
533
|
+
if (!(await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) continue
|
|
488
534
|
checkedAny = true
|
|
489
|
-
await checkLocalOnlyChangedWorkspace(
|
|
535
|
+
await checkLocalOnlyChangedWorkspace(comparison.ref, manifest, comparison.label, pass, fail)
|
|
490
536
|
}
|
|
491
537
|
if (!checkedAny) {
|
|
492
|
-
pass(`changelog: local-only воркспейси без змін відносно
|
|
538
|
+
pass(`changelog: local-only воркспейси без змін відносно ${comparison.label}`)
|
|
493
539
|
}
|
|
494
540
|
}
|
|
495
541
|
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* Stop-hook (батч-нормалізація чернеток); умови — ті самі, що для `capture`.
|
|
19
19
|
* - `.cursor/hooks.json` — **merge**: користувацькі hooks зберігаються; ADR stop
|
|
20
20
|
* entries додаються, коли правило `adr` увімкнене, і видаляються, коли вимкнене.
|
|
21
|
+
* - `.gitignore` — **merge** (лише з `adr`): дописує відсутні рядки з канонічного
|
|
22
|
+
* фрагмента `rules/adr/fix/hooks/template/.gitignore.snippet` (`node_modules/`, `dist/`,
|
|
23
|
+
* `*.secret`, логи capture/normalize, `.normalize-state`, `.normalize.lock`); існуючі
|
|
24
|
+
* рядки не перезаписуються.
|
|
21
25
|
*
|
|
22
26
|
* Опт-аут — `claude-config: false` у `.n-cursor.json`.
|
|
23
27
|
*/
|
|
@@ -51,6 +55,10 @@ const CURSOR_HOOKS_FILE = `${CURSOR_DIR}/hooks.json`
|
|
|
51
55
|
const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
52
56
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
53
57
|
const TEMPLATE_DIR_NAME = '.claude-template'
|
|
58
|
+
/** Відносний шлях до канонічного фрагмента `.gitignore` для ADR Stop-hook'ів у tarball пакета. */
|
|
59
|
+
export const ADR_GITIGNORE_SNIPPET_REL = 'rules/adr/fix/hooks/template/.gitignore.snippet'
|
|
60
|
+
const GITIGNORE_FILE = '.gitignore'
|
|
61
|
+
const EOL_RE = /\r?\n/u
|
|
54
62
|
|
|
55
63
|
/** Канонічна група hooks для ADR capture Stop-hook'а — додається в settings, коли `adr` у `rules`. */
|
|
56
64
|
const ADR_STOP_HOOK_GROUP = Object.freeze({
|
|
@@ -387,6 +395,60 @@ export function syncAdrNormalizeHookScript(projectRoot, templateDir) {
|
|
|
387
395
|
return syncHookScript(projectRoot, templateDir, ADR_NORMALIZE_HOOK_SCRIPT_NAME)
|
|
388
396
|
}
|
|
389
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Повертає змістовні (не коментар, не порожній) рядки з text-фрагмента `.gitignore`.
|
|
400
|
+
* @param {string} raw вміст snippet-файлу
|
|
401
|
+
* @returns {string[]} нормалізовані рядки патернів
|
|
402
|
+
*/
|
|
403
|
+
function parseGitignoreFragmentLines(raw) {
|
|
404
|
+
return raw
|
|
405
|
+
.split(EOL_RE)
|
|
406
|
+
.map(l => l.trim())
|
|
407
|
+
.filter(l => l !== '' && !l.startsWith('#'))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Дописує в кореневий `.gitignore` проєкту відсутні рядки з канонічного ADR-фрагмента.
|
|
412
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
413
|
+
* @param {string} bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
414
|
+
* @returns {Promise<{ written: boolean, path: string }>} чи змінено файл і відносний шлях
|
|
415
|
+
*/
|
|
416
|
+
export async function syncGitignoreAdrFragment(projectRoot, bundledPackageRoot) {
|
|
417
|
+
const snippetPath = join(bundledPackageRoot, ADR_GITIGNORE_SNIPPET_REL)
|
|
418
|
+
if (!existsSync(snippetPath)) {
|
|
419
|
+
return { written: false, path: '' }
|
|
420
|
+
}
|
|
421
|
+
const fragment = await readFile(snippetPath, 'utf8')
|
|
422
|
+
const required = parseGitignoreFragmentLines(fragment)
|
|
423
|
+
if (required.length === 0) {
|
|
424
|
+
return { written: false, path: '' }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const destPath = join(projectRoot, GITIGNORE_FILE)
|
|
428
|
+
const existing = existsSync(destPath) ? await readFile(destPath, 'utf8') : ''
|
|
429
|
+
const existingLines = new Set(
|
|
430
|
+
existing
|
|
431
|
+
.split(EOL_RE)
|
|
432
|
+
.map(l => l.trim())
|
|
433
|
+
.filter(l => l !== '' && !l.startsWith('#'))
|
|
434
|
+
)
|
|
435
|
+
const missing = required.filter(l => !existingLines.has(l))
|
|
436
|
+
if (missing.length === 0) {
|
|
437
|
+
return { written: false, path: GITIGNORE_FILE }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const sectionHeader = '# @nitra/cursor (adr) — локальні артефакти Stop-hook, не коміти'
|
|
441
|
+
const hasHeader = existing.split(EOL_RE).some(l => l.trim() === sectionHeader)
|
|
442
|
+
const block = hasHeader ? missing.join('\n') : [sectionHeader, ...missing].join('\n')
|
|
443
|
+
let prefix = ''
|
|
444
|
+
if (existing.length > 0) {
|
|
445
|
+
prefix = existing.endsWith('\n') ? existing : `${existing}\n`
|
|
446
|
+
}
|
|
447
|
+
const next = `${prefix}${block}\n`
|
|
448
|
+
await writeFile(destPath, next, 'utf8')
|
|
449
|
+
return { written: true, path: GITIGNORE_FILE }
|
|
450
|
+
}
|
|
451
|
+
|
|
390
452
|
/**
|
|
391
453
|
* Копіює всі slash-команди з `templateDir/commands/` у `.claude/commands/`.
|
|
392
454
|
* Команди ідентифікуються тим, що вони лежать у темплейті — не перетинаються
|
|
@@ -422,7 +484,7 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
422
484
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
423
485
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
424
486
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
425
|
-
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s) та список
|
|
487
|
+
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, gitignoreAdr: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore` та список slash-команд
|
|
426
488
|
*/
|
|
427
489
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
428
490
|
if (!enabled) {
|
|
@@ -431,7 +493,8 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
431
493
|
cursorHooks: false,
|
|
432
494
|
commands: [],
|
|
433
495
|
adrHook: false,
|
|
434
|
-
adrNormalizeHook: false
|
|
496
|
+
adrNormalizeHook: false,
|
|
497
|
+
gitignoreAdr: false
|
|
435
498
|
}
|
|
436
499
|
}
|
|
437
500
|
const templateDir = join(bundledPackageRoot, TEMPLATE_DIR_NAME)
|
|
@@ -441,7 +504,8 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
441
504
|
cursorHooks: false,
|
|
442
505
|
commands: [],
|
|
443
506
|
adrHook: false,
|
|
444
|
-
adrNormalizeHook: false
|
|
507
|
+
adrNormalizeHook: false,
|
|
508
|
+
gitignoreAdr: false
|
|
445
509
|
}
|
|
446
510
|
}
|
|
447
511
|
const includeAdrHook = Array.isArray(rules) && rules.includes('adr')
|
|
@@ -449,6 +513,9 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
449
513
|
const adrNormalizeHook = includeAdrHook
|
|
450
514
|
? await syncAdrNormalizeHookScript(projectRoot, templateDir)
|
|
451
515
|
: { written: false, path: '' }
|
|
516
|
+
const gitignoreAdr = includeAdrHook
|
|
517
|
+
? await syncGitignoreAdrFragment(projectRoot, bundledPackageRoot)
|
|
518
|
+
: { written: false, path: '' }
|
|
452
519
|
const settings = await syncClaudeSettings(projectRoot, templateDir, { includeAdrHook })
|
|
453
520
|
const cursorHooks = await syncCursorHooksConfig(projectRoot, { includeAdrHook })
|
|
454
521
|
const commands = await syncClaudeCommands(projectRoot, templateDir)
|
|
@@ -457,6 +524,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
457
524
|
cursorHooks: cursorHooks.written,
|
|
458
525
|
commands,
|
|
459
526
|
adrHook: adrHook.written,
|
|
460
|
-
adrNormalizeHook: adrNormalizeHook.written
|
|
527
|
+
adrNormalizeHook: adrNormalizeHook.written,
|
|
528
|
+
gitignoreAdr: gitignoreAdr.written
|
|
461
529
|
}
|
|
462
530
|
}
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -44,13 +44,13 @@ bun run lint
|
|
|
44
44
|
|
|
45
45
|
**Конфіги і коментарі, які потребують зупинки** (неповний список — будь-який аналог):
|
|
46
46
|
|
|
47
|
-
| Інструмент
|
|
48
|
-
|
|
|
49
|
-
| **jscpd**
|
|
50
|
-
| **cspell**
|
|
51
|
-
| **knip**
|
|
47
|
+
| Інструмент | Типові файли / зміни |
|
|
48
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| **jscpd** | `.jscpd.json` → `ignore`, `minLines` |
|
|
50
|
+
| **cspell** | `.cspell.json` → `words`, `ignorePaths`; `.cspellignore` |
|
|
51
|
+
| **knip** | `knip.json` → `ignore`, `ignoreDependencies`, `ignoreBinaries`, `entry` |
|
|
52
52
|
| **oxlint / ESLint** | `.oxlintrc.json` → `ignorePatterns`; `eslint.config.js` → `ignores`; `eslint-disable` / `oxlint-disable` у коді |
|
|
53
|
-
| **інше**
|
|
53
|
+
| **інше** | `.v8rignore`, `.stylelintignore`, `.trufflehog-exclude`, розширення `ignores` у workflow-конфігах |
|
|
54
54
|
|
|
55
55
|
Політика узгоджена з **`.cursor/rules/`** (зокрема **n-js-lint**, **n-text**): виняток допустимий лише з **обґрунтованою** причиною, не як заміна рефакторингу для справжніх клонів / дублікатів.
|
|
56
56
|
|
|
@@ -64,12 +64,12 @@ bun run lint
|
|
|
64
64
|
|
|
65
65
|
**Варіанти відповіді** (мінімум такі; `allow_multiple: false`):
|
|
66
66
|
|
|
67
|
-
| id
|
|
68
|
-
|
|
|
69
|
-
| `refactor`
|
|
70
|
-
| `ignore-once` | **Точковий виняток у конфігу** — додати ignore/words/minLines з обґрунтуванням у коментарі PR/відповіді | Після вибору — мінімальна зміна конфігу + 1 речення **чому** це не рефакторинг
|
|
71
|
-
| `skip`
|
|
72
|
-
| `explain`
|
|
67
|
+
| id | label (українською) | Дія агента |
|
|
68
|
+
| ------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
69
|
+
| `refactor` | **Рефакторинг коду** — усунути дублікат / помилку в коді (рекомендовано за замовчуванням) | Рефакторинг; конфіг **не** чіпати |
|
|
70
|
+
| `ignore-once` | **Точковий виняток у конфігу** — додати ignore/words/minLines з обґрунтуванням у коментарі PR/відповіді | Після вибору — мінімальна зміна конфігу + 1 речення **чому** це не рефакторинг |
|
|
71
|
+
| `skip` | **Залишити як є** — не чіпати ні код, ні конфіг зараз | Не змінювати; у фінальному резюме — що лишилось червоним |
|
|
72
|
+
| `explain` | **Потрібні деталі** — поясни варіанти глибше | Розгорнути порівняння refactor vs ignore; **знову** запитати той самий набір варіантів |
|
|
73
73
|
|
|
74
74
|
Якщо користувач обрав **`ignore-once`** — у відповіді після зміни зафіксуй: який ключ конфігу змінено, який glob/слово додано, чому рефакторинг був недоречний (генерований код, формальний шаблон, легітимний термін без перекладу тощо).
|
|
75
75
|
|