@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.
@@ -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
- # Структурний скіп ADR-генерації для "tooling-only" сесій.
38
- # Вхід: рядки-шляхи у stdin (один шлях на лінію), відносні до $PROJECT_ROOT
39
- # або абсолютні з префіксом $PROJECT_ROOT (нормалізуємо тут).
40
- # Вихід: 0 — усі шляхи в allowlist; 1 — є хоч один змістовний шлях.
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
- # Структурний скіп ADR-нормалізації для "tooling-only" сесій.
43
- # Дублікат із capture-decisions.sh: `.claude-template/hooks/` копіюється плоско,
44
- # спільний helper-файл туди не вписується.
45
- is_tooling_only_change() {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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 та список slash-команд
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
  }