@nitra/cursor 1.23.0 → 1.25.0
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/.claude-template/hooks/capture-decisions.sh +4 -55
- package/.claude-template/hooks/lib/tooling-only.sh +61 -0
- package/.claude-template/hooks/normalize-decisions.sh +4 -49
- package/.pi-template/extensions/n-cursor-adr/index.ts +7 -7
- package/.pi-template/extensions/n-cursor-adr/tsconfig.json +16 -0
- package/CHANGELOG.md +29 -0
- package/bin/n-cursor.js +6 -1
- package/package.json +7 -2
- package/scripts/sync-claude-config.mjs +82 -19
- package/skills/coverage-fix/SKILL.md +114 -0
- package/skills/coverage-fix/auto.md +1 -0
|
@@ -34,61 +34,10 @@ mkdir -p "$ADR_DIR" "$LOG_DIR"
|
|
|
34
34
|
|
|
35
35
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
# Bash 3.2: без mapfile/асоц. масивів.
|
|
42
|
-
is_tooling_only_change() {
|
|
43
|
-
local proj="$1"
|
|
44
|
-
local had_file=0
|
|
45
|
-
local f rel
|
|
46
|
-
while IFS= read -r f; do
|
|
47
|
-
[ -z "$f" ] && continue
|
|
48
|
-
had_file=1
|
|
49
|
-
case "$f" in
|
|
50
|
-
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
51
|
-
/*) return 1 ;;
|
|
52
|
-
*) rel="$f" ;;
|
|
53
|
-
esac
|
|
54
|
-
case "$rel" in
|
|
55
|
-
.cspell.json) ;;
|
|
56
|
-
docs/adr/*.md) ;;
|
|
57
|
-
AGENTS.md|CLAUDE.md) ;;
|
|
58
|
-
CHANGELOG.md) ;;
|
|
59
|
-
*/CHANGELOG.md) ;;
|
|
60
|
-
package.json|*/package.json)
|
|
61
|
-
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
62
|
-
return 1
|
|
63
|
-
fi
|
|
64
|
-
;;
|
|
65
|
-
*) return 1 ;;
|
|
66
|
-
esac
|
|
67
|
-
done
|
|
68
|
-
[ "$had_file" = "1" ] && return 0
|
|
69
|
-
return 1
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
73
|
-
# Поза git-репо або при помилці — вертаємо 1 (не tooling).
|
|
74
|
-
git_diff_only_version_field() {
|
|
75
|
-
local proj="$1" path="$2"
|
|
76
|
-
[ -d "$proj/.git" ] || return 1
|
|
77
|
-
local diff
|
|
78
|
-
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
79
|
-
[ -z "$diff" ] && return 1
|
|
80
|
-
local line
|
|
81
|
-
while IFS= read -r line; do
|
|
82
|
-
case "$line" in
|
|
83
|
-
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
84
|
-
[+-]*'"version":'*) continue ;;
|
|
85
|
-
[+-]*) return 1 ;;
|
|
86
|
-
esac
|
|
87
|
-
done <<EOF
|
|
88
|
-
$diff
|
|
89
|
-
EOF
|
|
90
|
-
return 0
|
|
91
|
-
}
|
|
37
|
+
# Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
|
|
38
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
39
|
+
# shellcheck source=lib/tooling-only.sh
|
|
40
|
+
. "$SCRIPT_DIR/lib/tooling-only.sh"
|
|
92
41
|
|
|
93
42
|
log "fired: $SESSION_ID"
|
|
94
43
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Спільний helper для ADR Stop-hook'ів: розпізнавання "tooling-only" сесій.
|
|
3
|
+
# Source'ається з capture-decisions.sh і normalize-decisions.sh — функції стають
|
|
4
|
+
# видимими caller'у, який успадковує `set` опції.
|
|
5
|
+
# Bash 3.2 (macOS /bin/bash) сумісний: без mapfile, без асоц. масивів,
|
|
6
|
+
# без process substitution.
|
|
7
|
+
|
|
8
|
+
# Структурний скіп ADR-генерації для "tooling-only" сесій.
|
|
9
|
+
# Вхід: рядки-шляхи у stdin (один шлях на лінію), відносні до $PROJECT_ROOT
|
|
10
|
+
# або абсолютні з префіксом $PROJECT_ROOT (нормалізуємо тут).
|
|
11
|
+
# Вихід: 0 — усі шляхи в allowlist; 1 — є хоч один змістовний шлях.
|
|
12
|
+
is_tooling_only_change() {
|
|
13
|
+
local proj="$1"
|
|
14
|
+
local had_file=0
|
|
15
|
+
local f rel
|
|
16
|
+
while IFS= read -r f; do
|
|
17
|
+
[ -z "$f" ] && continue
|
|
18
|
+
had_file=1
|
|
19
|
+
case "$f" in
|
|
20
|
+
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
21
|
+
/*) return 1 ;;
|
|
22
|
+
*) rel="$f" ;;
|
|
23
|
+
esac
|
|
24
|
+
case "$rel" in
|
|
25
|
+
.cspell.json) ;;
|
|
26
|
+
docs/adr/*.md) ;;
|
|
27
|
+
AGENTS.md|CLAUDE.md) ;;
|
|
28
|
+
CHANGELOG.md) ;;
|
|
29
|
+
*/CHANGELOG.md) ;;
|
|
30
|
+
package.json|*/package.json)
|
|
31
|
+
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*) return 1 ;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
[ "$had_file" = "1" ] && return 0
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
43
|
+
# Поза git-репо або при помилці — вертаємо 1 (не tooling).
|
|
44
|
+
git_diff_only_version_field() {
|
|
45
|
+
local proj="$1" path="$2"
|
|
46
|
+
[ -d "$proj/.git" ] || return 1
|
|
47
|
+
local diff
|
|
48
|
+
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
49
|
+
[ -z "$diff" ] && return 1
|
|
50
|
+
local line
|
|
51
|
+
while IFS= read -r line; do
|
|
52
|
+
case "$line" in
|
|
53
|
+
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
54
|
+
[+-]*'"version":'*) continue ;;
|
|
55
|
+
[+-]*) return 1 ;;
|
|
56
|
+
esac
|
|
57
|
+
done <<EOF
|
|
58
|
+
$diff
|
|
59
|
+
EOF
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
@@ -39,55 +39,10 @@ mkdir -p "$LOG_DIR"
|
|
|
39
39
|
|
|
40
40
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
proj="$1"
|
|
47
|
-
had_file=0
|
|
48
|
-
while IFS= read -r f; do
|
|
49
|
-
[ -z "$f" ] && continue
|
|
50
|
-
had_file=1
|
|
51
|
-
case "$f" in
|
|
52
|
-
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
53
|
-
/*) return 1 ;;
|
|
54
|
-
*) rel="$f" ;;
|
|
55
|
-
esac
|
|
56
|
-
case "$rel" in
|
|
57
|
-
.cspell.json) ;;
|
|
58
|
-
docs/adr/*.md) ;;
|
|
59
|
-
AGENTS.md|CLAUDE.md) ;;
|
|
60
|
-
CHANGELOG.md) ;;
|
|
61
|
-
*/CHANGELOG.md) ;;
|
|
62
|
-
package.json|*/package.json)
|
|
63
|
-
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
64
|
-
return 1
|
|
65
|
-
fi
|
|
66
|
-
;;
|
|
67
|
-
*) return 1 ;;
|
|
68
|
-
esac
|
|
69
|
-
done
|
|
70
|
-
[ "$had_file" = "1" ] && return 0
|
|
71
|
-
return 1
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
75
|
-
git_diff_only_version_field() {
|
|
76
|
-
proj="$1"; path="$2"
|
|
77
|
-
[ -d "$proj/.git" ] || return 1
|
|
78
|
-
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
79
|
-
[ -z "$diff" ] && return 1
|
|
80
|
-
while IFS= read -r line; do
|
|
81
|
-
case "$line" in
|
|
82
|
-
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
83
|
-
[+-]*'"version":'*) continue ;;
|
|
84
|
-
[+-]*) return 1 ;;
|
|
85
|
-
esac
|
|
86
|
-
done <<EOF
|
|
87
|
-
$diff
|
|
88
|
-
EOF
|
|
89
|
-
return 0
|
|
90
|
-
}
|
|
42
|
+
# Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
# shellcheck source=lib/tooling-only.sh
|
|
45
|
+
. "$SCRIPT_DIR/lib/tooling-only.sh"
|
|
91
46
|
|
|
92
47
|
# Витягає поле `transcript:` з YAML frontmatter ADR-чернетки.
|
|
93
48
|
draft_transcript_path() {
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* перед спавном LLM CLI.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
14
|
+
import { writeFileSync } from 'node:fs'
|
|
15
|
+
import { tmpdir } from 'node:os'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { env } from 'node:process'
|
|
18
|
+
|
|
13
19
|
interface PiContext {
|
|
14
20
|
cwd: string
|
|
15
21
|
sessionId?: string
|
|
@@ -36,18 +42,12 @@ interface PiExec {
|
|
|
36
42
|
) => void
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
import { writeFileSync } from 'node:fs'
|
|
40
|
-
import { tmpdir } from 'node:os'
|
|
41
|
-
import { join } from 'node:path'
|
|
42
|
-
import { randomUUID } from 'node:crypto'
|
|
43
|
-
import { env } from 'node:process'
|
|
44
|
-
|
|
45
45
|
const CAPTURE_HOOK = '.claude/hooks/capture-decisions.sh'
|
|
46
46
|
const NORMALIZE_HOOK = '.claude/hooks/normalize-decisions.sh'
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Pi extension entry point.
|
|
50
|
-
* @param pi pi.dev extension API
|
|
50
|
+
* @param {PiExec} pi pi.dev extension API
|
|
51
51
|
*/
|
|
52
52
|
export default function (pi: PiExec): void {
|
|
53
53
|
pi.on('agent_end', async (_event, ctx) => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "TS-конфіг для pi.dev extension. Не компілюється сам пакетом (синкається як є у .pi/extensions/<name>/), потрібен лише для IDE/TS-сервера у проєкті-споживачі, щоб резолвити node:* модулі. Споживачу треба мати @types/node у devDependencies (зазвичай уже є транзитивно).",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"lib": ["ES2022"],
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts"]
|
|
16
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.25.0] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`.claude-template/hooks/lib/tooling-only.sh`** — спільний bash-helper із функціями `is_tooling_only_change` і `git_diff_only_version_field`. Source'ається з `capture-decisions.sh` і `normalize-decisions.sh` через `. "$SCRIPT_DIR/lib/tooling-only.sh"` — caller'и більше не дублюють ~27 рядків логіки розпізнавання "tooling-only" сесій. Bash 3.2 (macOS `/bin/bash`) сумісний.
|
|
12
|
+
- **`syncAdrHookLibScripts(projectRoot, templateDir)`** у `npm/scripts/sync-claude-config.mjs` — копіює всі `*.sh` із `.claude-template/hooks/lib/` у `.claude/hooks/lib/` проєкту-споживача без exec-біта (source-only). Експортується разом із новою `removeOrphanAdrHookLib(projectRoot)` для cleanup-у при вимкненому `adr` правилі.
|
|
13
|
+
- Поле `adrHookLib: string[]` у відповіді `syncClaudeConfig` — список синкнутих lib-файлів. `npm/bin/n-cursor.js` виводить їх у `🤖 Claude-конфіг` логу окремими записами.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`.claude-template/hooks/{capture,normalize}-decisions.sh`** — обидва скрипти `source`-ять спільний `lib/tooling-only.sh` замість локальних копій функцій. Заголовок `# shellcheck source=lib/tooling-only.sh` тримає shellcheck-аналіз чистим.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Усунено jscpd-дублікат `is_tooling_only_change`/`git_diff_only_version_field` між `capture-decisions.sh` і `normalize-decisions.sh` (~27 рядків / ~194 токени). Споживачам більше не потрібен ignore-запис для цих файлів у `.jscpd.json`.
|
|
22
|
+
|
|
23
|
+
## [1.24.0] - 2026-05-26
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`.pi-template/extensions/n-cursor-adr/tsconfig.json`** — мінімальний TS-конфіг шаблону pi.dev extension, який синкається у `.pi/extensions/n-cursor-adr/` разом із `index.ts`. Дозволяє IDE/TS-серверу резолвити `node:*` модулі без project-wide tsconfig у проєкті-споживачі (`types: ["node"]`, `module/target: ES2022 + NodeNext`, `noEmit`, `isolatedModules`). Споживачу потрібен `@types/node` у devDeps (зазвичай уже є транзитивно).
|
|
28
|
+
- **`pi` manifest у `npm/package.json`** — `{"skills":"skills","extensions":".pi-template/extensions"}`. Pi.dev під час `pi install npm:@nitra/cursor` тепер бачить explicit-шляхи до bundled-ресурсів, замість convention-auto-discovery. `extensions: ".pi-template/extensions"` критичний — pi за замовч. шукає `extensions/` у корені пакета, а у нас шлях нестандартний.
|
|
29
|
+
- **`"pi-package"` keyword** у `npm/package.json` — пакет з’являється у pi-gallery для discoverability.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`syncPiExtensions`** тепер копіює **всі файли** з теки `.pi-template/extensions/<name>/`, а не лише `index.ts`. Контракт повернення: `{ written, path, files }` — `path` тепер тека (а не файл), `files` — відсортований список базових імен. У `🤖 Claude-конфіг` логу та у `result.piExtension` caller-стороні виводиться тека.
|
|
34
|
+
- **`.pi-template/extensions/n-cursor-adr/index.ts`** — порядок імпортів виправлено (всі `node:*` імпорти підняті на самий верх, перед `interface PiContext`). Усувало `import/first` ESLint-помилку; функціонально без змін.
|
|
35
|
+
|
|
7
36
|
## [1.23.0] - 2026-05-25
|
|
8
37
|
|
|
9
38
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1401,8 +1401,13 @@ async function runSync() {
|
|
|
1401
1401
|
if (result.commands.length > 0) parts.push(`${result.commands.length} slash-commands`)
|
|
1402
1402
|
if (result.adrHook) parts.push('.claude/hooks/capture-decisions.sh')
|
|
1403
1403
|
if (result.adrNormalizeHook) parts.push('.claude/hooks/normalize-decisions.sh')
|
|
1404
|
+
if (result.adrHookLib?.length > 0) {
|
|
1405
|
+
for (const libPath of result.adrHookLib) {
|
|
1406
|
+
parts.push(libPath)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1404
1409
|
if (result.gitignoreAdr) parts.push('.gitignore (adr fragment)')
|
|
1405
|
-
if (result.piExtension) parts.push('.pi/extensions/n-cursor-adr/
|
|
1410
|
+
if (result.piExtension) parts.push('.pi/extensions/n-cursor-adr/')
|
|
1406
1411
|
if (parts.length > 0) {
|
|
1407
1412
|
console.log(`🤖 Claude-конфіг: ${parts.join(', ')}`)
|
|
1408
1413
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"cursor",
|
|
8
8
|
"cursor-rules",
|
|
9
9
|
"mdc",
|
|
10
|
-
"n"
|
|
10
|
+
"n",
|
|
11
|
+
"pi-package"
|
|
11
12
|
],
|
|
13
|
+
"pi": {
|
|
14
|
+
"skills": "skills",
|
|
15
|
+
"extensions": ".pi-template/extensions"
|
|
16
|
+
},
|
|
12
17
|
"homepage": "https://github.com/n/cursor#readme",
|
|
13
18
|
"bugs": {
|
|
14
19
|
"url": "https://github.com/n/cursor/issues"
|
|
@@ -61,6 +61,7 @@ const CURSOR_DIR = '.cursor'
|
|
|
61
61
|
const CURSOR_HOOKS_FILE = `${CURSOR_DIR}/hooks.json`
|
|
62
62
|
const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
63
63
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
64
|
+
const ADR_HOOK_LIB_DIR = 'lib'
|
|
64
65
|
const TEMPLATE_DIR_NAME = '.claude-template'
|
|
65
66
|
|
|
66
67
|
/** Корінь pi.dev артефактів у проєкті-споживачі. */
|
|
@@ -415,32 +416,86 @@ export function syncAdrNormalizeHookScript(projectRoot, templateDir) {
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
/**
|
|
418
|
-
* Копіює
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
419
|
+
* Копіює всі `.sh`-файли з `.claude-template/hooks/lib/` у `.claude/hooks/lib/` проєкту.
|
|
420
|
+
* Файли source-only (без exec bit) — їх `source`-ять capture/normalize-decisions.sh,
|
|
421
|
+
* щоб не дублювати спільну bash-логіку (`is_tooling_only_change`,
|
|
422
|
+
* `git_diff_only_version_field`).
|
|
423
|
+
* Тека fully-owned: при кожному sync-у перезаписується.
|
|
424
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
425
|
+
* @param {string} templateDir каталог `.claude-template/` усередині пакету
|
|
426
|
+
* @returns {Promise<Array<{ written: boolean, path: string }>>} перелік записаних файлів (порожній, якщо темплейту нема)
|
|
427
|
+
*/
|
|
428
|
+
export async function syncAdrHookLibScripts(projectRoot, templateDir) {
|
|
429
|
+
const libTemplateDir = join(templateDir, 'hooks', ADR_HOOK_LIB_DIR)
|
|
430
|
+
if (!existsSync(libTemplateDir)) {
|
|
431
|
+
return []
|
|
432
|
+
}
|
|
433
|
+
const entries = await readdir(libTemplateDir, { withFileTypes: true })
|
|
434
|
+
const libDestDir = join(projectRoot, CLAUDE_HOOKS_DIR, ADR_HOOK_LIB_DIR)
|
|
435
|
+
await mkdir(libDestDir, { recursive: true })
|
|
436
|
+
const written = []
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
if (!entry.isFile() || !entry.name.endsWith('.sh')) continue
|
|
439
|
+
const content = await readFile(join(libTemplateDir, entry.name), 'utf8')
|
|
440
|
+
// НЕ chmod 755 — source-файли не виконувані (їх лише `.`-ять caller-скрипти).
|
|
441
|
+
await writeFile(join(libDestDir, entry.name), content, 'utf8')
|
|
442
|
+
written.push({ written: true, path: `${CLAUDE_HOOKS_DIR}/${ADR_HOOK_LIB_DIR}/${entry.name}` })
|
|
443
|
+
}
|
|
444
|
+
return written
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Видаляє `.claude/hooks/lib/` директорію з проєкту-споживача.
|
|
449
|
+
* Викликається коли правило `adr` вимкнено — lib-файли не самостійні, без хуків,
|
|
450
|
+
* що їх source-ять, вони нікому не потрібні (симетрично до `removeOrphanPiExtension`).
|
|
451
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
452
|
+
* @returns {Promise<{ removed: boolean, path: string }>} чи було щось видалено та відносний шлях
|
|
453
|
+
*/
|
|
454
|
+
export async function removeOrphanAdrHookLib(projectRoot) {
|
|
455
|
+
const libDir = join(projectRoot, CLAUDE_HOOKS_DIR, ADR_HOOK_LIB_DIR)
|
|
456
|
+
if (!existsSync(libDir)) {
|
|
457
|
+
return { removed: false, path: '' }
|
|
458
|
+
}
|
|
459
|
+
await rm(libDir, { recursive: true, force: true })
|
|
460
|
+
return { removed: true, path: `${CLAUDE_HOOKS_DIR}/${ADR_HOOK_LIB_DIR}` }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Копіює bundled pi.dev TS-extension `npm/.pi-template/extensions/n-cursor-adr/` (усі файли —
|
|
465
|
+
* `index.ts`, `tsconfig.json`, потенційні `package.json`/`.gitignore` тощо) у
|
|
466
|
+
* `.pi/extensions/n-cursor-adr/` проєкту-споживача. Тека fully-owned: при кожному sync-у
|
|
467
|
+
* перезаписується. Якщо bundled template відсутній (legacy-версії пакета без `.pi-template/`)
|
|
468
|
+
* або в ньому немає `index.ts` — повертаємо `{written: false}` без помилки.
|
|
422
469
|
*
|
|
470
|
+
* Розширення поверх `index.ts` (tsconfig тощо) потрібні, бо `.pi/extensions/` синкається як є
|
|
471
|
+
* у проєкти-споживачі, а IDE/TS-сервер мусить резолвити `node:*` модулі без додаткових
|
|
472
|
+
* project-wide конфігів.
|
|
423
473
|
* @param {string} projectRoot корінь проєкту-споживача
|
|
424
474
|
* @param {string} bundledPackageRoot корінь установленого `@nitra/cursor` (із `.pi-template/`)
|
|
425
|
-
* @returns {Promise<{ written: boolean, path: string }>} чи
|
|
475
|
+
* @returns {Promise<{ written: boolean, path: string, files: string[] }>} чи писали; відносний шлях до теки розширення; список скопійованих базових імен (відсортований)
|
|
426
476
|
*/
|
|
427
477
|
export async function syncPiExtensions(projectRoot, bundledPackageRoot) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
'
|
|
432
|
-
PI_EXTENSION_NAME,
|
|
433
|
-
'index.ts'
|
|
434
|
-
)
|
|
435
|
-
if (!existsSync(srcPath)) {
|
|
436
|
-
return { written: false, path: '' }
|
|
478
|
+
const srcDir = join(bundledPackageRoot, PI_TEMPLATE_DIR_NAME, 'extensions', PI_EXTENSION_NAME)
|
|
479
|
+
const indexPath = join(srcDir, 'index.ts')
|
|
480
|
+
if (!existsSync(indexPath)) {
|
|
481
|
+
return { written: false, path: '', files: [] }
|
|
437
482
|
}
|
|
438
|
-
const content = await readFile(srcPath, 'utf8')
|
|
439
483
|
const destDir = join(projectRoot, PI_EXTENSIONS_DIR, PI_EXTENSION_NAME)
|
|
440
484
|
await mkdir(destDir, { recursive: true })
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
485
|
+
const entries = await readdir(srcDir, { withFileTypes: true })
|
|
486
|
+
const copied = []
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
if (!entry.isFile()) continue
|
|
489
|
+
const name = entry.name
|
|
490
|
+
const content = await readFile(join(srcDir, name), 'utf8')
|
|
491
|
+
await writeFile(join(destDir, name), content, 'utf8')
|
|
492
|
+
copied.push(name)
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
written: true,
|
|
496
|
+
path: `${PI_EXTENSIONS_DIR}/${PI_EXTENSION_NAME}`,
|
|
497
|
+
files: copied.toSorted((a, b) => a.localeCompare(b))
|
|
498
|
+
}
|
|
444
499
|
}
|
|
445
500
|
|
|
446
501
|
/**
|
|
@@ -549,7 +604,7 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
549
604
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
550
605
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
551
606
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
552
|
-
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, gitignoreAdr: boolean, piExtension: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore`/pi-extension
|
|
607
|
+
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, adrHookLib: string[], gitignoreAdr: boolean, piExtension: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore`/pi-extension, перелік lib-файлів і список slash-команд
|
|
553
608
|
*/
|
|
554
609
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
555
610
|
if (!enabled) {
|
|
@@ -559,6 +614,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
559
614
|
commands: [],
|
|
560
615
|
adrHook: false,
|
|
561
616
|
adrNormalizeHook: false,
|
|
617
|
+
adrHookLib: [],
|
|
562
618
|
gitignoreAdr: false,
|
|
563
619
|
piExtension: false
|
|
564
620
|
}
|
|
@@ -571,6 +627,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
571
627
|
commands: [],
|
|
572
628
|
adrHook: false,
|
|
573
629
|
adrNormalizeHook: false,
|
|
630
|
+
adrHookLib: [],
|
|
574
631
|
gitignoreAdr: false,
|
|
575
632
|
piExtension: false
|
|
576
633
|
}
|
|
@@ -580,6 +637,11 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
580
637
|
const adrNormalizeHook = includeAdrHook
|
|
581
638
|
? await syncAdrNormalizeHookScript(projectRoot, templateDir)
|
|
582
639
|
: { written: false, path: '' }
|
|
640
|
+
// Lib-файли мають сенс лише з активним хоча б одним ADR-хуком — без caller'а
|
|
641
|
+
// нікому source-ити; при вимкненому правилі прибираємо орфан-теку.
|
|
642
|
+
const adrHookLibEntries = includeAdrHook
|
|
643
|
+
? await syncAdrHookLibScripts(projectRoot, templateDir)
|
|
644
|
+
: (await removeOrphanAdrHookLib(projectRoot), [])
|
|
583
645
|
const gitignoreAdr = includeAdrHook
|
|
584
646
|
? await syncGitignoreAdrFragment(projectRoot, bundledPackageRoot)
|
|
585
647
|
: { written: false, path: '' }
|
|
@@ -595,6 +657,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
595
657
|
commands,
|
|
596
658
|
adrHook: adrHook.written,
|
|
597
659
|
adrNormalizeHook: adrNormalizeHook.written,
|
|
660
|
+
adrHookLib: adrHookLibEntries.map(e => e.path),
|
|
598
661
|
gitignoreAdr: gitignoreAdr.written,
|
|
599
662
|
piExtension: piExtension.written
|
|
600
663
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-coverage-fix
|
|
3
|
+
description: >-
|
|
4
|
+
Автономна команда: запускає coverage, читає ## Recommendations у COVERAGE.md, ітеративно пише тести для вижилих мутантів до конвергенції
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /n-coverage-fix — ітеративне підвищення mutation score
|
|
8
|
+
|
|
9
|
+
## Важливо
|
|
10
|
+
|
|
11
|
+
⚠️ Не запускати паралельно з іншим `/n-coverage-fix` або `bun coverage` — Stryker пише `mutation.json` і `incremental.json` в одну директорію. `n-cursor coverage` всередині вже серіалізований через `withLock('coverage')`, але паралельний запуск двох ітерацій скілу зіпсує дані.
|
|
12
|
+
|
|
13
|
+
## Мета
|
|
14
|
+
|
|
15
|
+
Автономно підвищити mutation score: запустити `bun coverage`, записати тести для вижилих мутантів, повторити до конвергенції (score перестав зростати).
|
|
16
|
+
|
|
17
|
+
## Алгоритм
|
|
18
|
+
|
|
19
|
+
### Крок 1: Запусти coverage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun coverage
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
(або `bun run coverage` якщо команда у `package.json`)
|
|
26
|
+
|
|
27
|
+
Чекай завершення. Якщо команди немає у `package.json` — запусти `n-cursor coverage` з кореня.
|
|
28
|
+
|
|
29
|
+
### Крок 2: Прочитай вижилих мутантів
|
|
30
|
+
|
|
31
|
+
Прочитай `COVERAGE.md` — знайди секцію `## Recommendations`.
|
|
32
|
+
|
|
33
|
+
Якщо секції немає або вона порожня:
|
|
34
|
+
```
|
|
35
|
+
✓ Нема вижилих мутантів — mutation score повний
|
|
36
|
+
```
|
|
37
|
+
→ DONE
|
|
38
|
+
|
|
39
|
+
Запам'ятай поточний mutation score як `baseline_score` (рядок `| **Разом** |` з таблиці у COVERAGE.md).
|
|
40
|
+
|
|
41
|
+
### Крок 3: Для кожного файлу з Recommendations — пиши тести
|
|
42
|
+
|
|
43
|
+
Для кожного `### <file>` у секції:
|
|
44
|
+
|
|
45
|
+
**3a. Читай контекст:**
|
|
46
|
+
- Source-файл (`<file>` від кореня проєкту)
|
|
47
|
+
- Таблицю вижилих мутантів: рядок, оригінал, заміна, тип
|
|
48
|
+
- Блок `**Приклад наявного тесту:**` — style guide для нових тестів
|
|
49
|
+
|
|
50
|
+
**3b. Знайди тестовий файл:**
|
|
51
|
+
Перший що існує:
|
|
52
|
+
1. `<dir>/<basename>.test.js` — поруч із source
|
|
53
|
+
2. `<dir>/<basename>.spec.js`
|
|
54
|
+
3. `test/<basename>.test.js` від кореня
|
|
55
|
+
4. `tests/<basename>.test.js` від кореня
|
|
56
|
+
|
|
57
|
+
Якщо жоден не знайдено — створи `<dir>/<basename>.test.js` з правильними imports (орієнтуйся на сусідні файли).
|
|
58
|
+
|
|
59
|
+
**3c. Напиши тести що вбивають кожен мутант:**
|
|
60
|
+
|
|
61
|
+
Керуйся типом мутації:
|
|
62
|
+
- `ConditionalExpression` (`→ false` / `→ true`): протестуй обидва branch явно — значення що робить умову `true` і значення що робить її `false`
|
|
63
|
+
- `BooleanLiteral` (`true → false`): перевір початковий стан — `initialValue === false`
|
|
64
|
+
- `LogicalOperator` (`&&` ↔ `||`): передай `null` та `undefined` **окремо**, перевір що результат різний для кожного
|
|
65
|
+
- `StringLiteral` / `EqualityOperator`: перевір точний рядок/значення, а не лише happy-path
|
|
66
|
+
|
|
67
|
+
Правила:
|
|
68
|
+
- НЕ видаляй і НЕ змінюй наявні тести
|
|
69
|
+
- Стиль: той самий `describe`/`it`/`expect`, мова коментарів — як у прикладі тесту
|
|
70
|
+
- Якщо `**Приклад наявного тесту:**` відсутній — орієнтуйся на інші test-файли у тій самій директорії
|
|
71
|
+
|
|
72
|
+
**3d. Після написання тестів:**
|
|
73
|
+
```bash
|
|
74
|
+
bun test <testFile>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Якщо FAIL — виправи саме ті тести що впали (до 2 спроб). Якщо не вдалося — логуй і переходь до наступного файлу.
|
|
78
|
+
|
|
79
|
+
### Крок 4: Перевір що весь suite проходить
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun test
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Якщо FAIL:
|
|
86
|
+
- Не відкочувати зміни
|
|
87
|
+
- Показати: яка помилка, які файли змінені, що вже покращено
|
|
88
|
+
- Очікувати рішення від user: [виправити вручну → продовжити] / [пропустити файл] / [зупинити]
|
|
89
|
+
|
|
90
|
+
### Крок 5: Запусти coverage і перевір конвергенцію
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bun coverage
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Якщо CRASH (SIGURG, memory pressure): нагадати user — Stryker incremental зберіг прогрес, перезапустити `bun coverage`.
|
|
97
|
+
|
|
98
|
+
Прочитай новий COVERAGE.md. Візьми `new_score` з рядка `| **Разом** |`.
|
|
99
|
+
|
|
100
|
+
**Рішення:**
|
|
101
|
+
- Якщо `new_score > baseline_score` → `baseline_score = new_score` → перейти до Кроку 2 (наступна ітерація)
|
|
102
|
+
- Якщо `new_score <= baseline_score` → конвергенція:
|
|
103
|
+
```
|
|
104
|
+
✓ Конвергенція: mutation score більше не покращується.
|
|
105
|
+
Baseline: <baseline_score> → Фінал: <new_score>
|
|
106
|
+
```
|
|
107
|
+
→ DONE
|
|
108
|
+
|
|
109
|
+
## Нотатки
|
|
110
|
+
|
|
111
|
+
- Stryker `incremental` (`incrementalFile`) зберігає прогрес між запусками — crash ≠ перезапуск з нуля
|
|
112
|
+
- Не комітити зміни автоматично — user вирішує коли комітити
|
|
113
|
+
- Пріоритет файлів: більше вижилих мутантів = важливіший (першим у Recommendations = найважливіший)
|
|
114
|
+
- Якщо `COVERAGE.md` відсутній — запусти `bun coverage` спочатку
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[js-lint]
|