@nitra/cursor 1.24.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/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +5 -0
- package/package.json +1 -1
- package/scripts/sync-claude-config.mjs +55 -1
|
@@ -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() {
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
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
|
+
|
|
7
23
|
## [1.24.0] - 2026-05-26
|
|
8
24
|
|
|
9
25
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1401,6 +1401,11 @@ 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
1410
|
if (result.piExtension) parts.push('.pi/extensions/n-cursor-adr/')
|
|
1406
1411
|
if (parts.length > 0) {
|
package/package.json
CHANGED
|
@@ -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 артефактів у проєкті-споживачі. */
|
|
@@ -414,6 +415,51 @@ export function syncAdrNormalizeHookScript(projectRoot, templateDir) {
|
|
|
414
415
|
return syncHookScript(projectRoot, templateDir, ADR_NORMALIZE_HOOK_SCRIPT_NAME)
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
/**
|
|
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
|
+
|
|
417
463
|
/**
|
|
418
464
|
* Копіює bundled pi.dev TS-extension `npm/.pi-template/extensions/n-cursor-adr/` (усі файли —
|
|
419
465
|
* `index.ts`, `tsconfig.json`, потенційні `package.json`/`.gitignore` тощо) у
|
|
@@ -558,7 +604,7 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
558
604
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
559
605
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
560
606
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
561
|
-
* @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-команд
|
|
562
608
|
*/
|
|
563
609
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
564
610
|
if (!enabled) {
|
|
@@ -568,6 +614,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
568
614
|
commands: [],
|
|
569
615
|
adrHook: false,
|
|
570
616
|
adrNormalizeHook: false,
|
|
617
|
+
adrHookLib: [],
|
|
571
618
|
gitignoreAdr: false,
|
|
572
619
|
piExtension: false
|
|
573
620
|
}
|
|
@@ -580,6 +627,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
580
627
|
commands: [],
|
|
581
628
|
adrHook: false,
|
|
582
629
|
adrNormalizeHook: false,
|
|
630
|
+
adrHookLib: [],
|
|
583
631
|
gitignoreAdr: false,
|
|
584
632
|
piExtension: false
|
|
585
633
|
}
|
|
@@ -589,6 +637,11 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
589
637
|
const adrNormalizeHook = includeAdrHook
|
|
590
638
|
? await syncAdrNormalizeHookScript(projectRoot, templateDir)
|
|
591
639
|
: { written: false, path: '' }
|
|
640
|
+
// Lib-файли мають сенс лише з активним хоча б одним ADR-хуком — без caller'а
|
|
641
|
+
// нікому source-ити; при вимкненому правилі прибираємо орфан-теку.
|
|
642
|
+
const adrHookLibEntries = includeAdrHook
|
|
643
|
+
? await syncAdrHookLibScripts(projectRoot, templateDir)
|
|
644
|
+
: (await removeOrphanAdrHookLib(projectRoot), [])
|
|
592
645
|
const gitignoreAdr = includeAdrHook
|
|
593
646
|
? await syncGitignoreAdrFragment(projectRoot, bundledPackageRoot)
|
|
594
647
|
: { written: false, path: '' }
|
|
@@ -604,6 +657,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
604
657
|
commands,
|
|
605
658
|
adrHook: adrHook.written,
|
|
606
659
|
adrNormalizeHook: adrNormalizeHook.written,
|
|
660
|
+
adrHookLib: adrHookLibEntries.map(e => e.path),
|
|
607
661
|
gitignoreAdr: gitignoreAdr.written,
|
|
608
662
|
piExtension: piExtension.written
|
|
609
663
|
}
|