@nitra/cursor 1.20.0 → 1.21.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/settings.template.json +4 -4
- package/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +10 -8
- package/package.json +1 -1
- package/rules/adr/adr.mdc +2 -12
- package/scripts/post-tool-use-fix.mjs +129 -0
- package/scripts/sync-claude-config.mjs +23 -14
- package/scripts/claude-stop-hook.mjs +0 -74
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"hooks": {
|
|
13
|
-
"
|
|
13
|
+
"PostToolUse": [
|
|
14
14
|
{
|
|
15
|
-
"matcher": "",
|
|
15
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
16
16
|
"hooks": [
|
|
17
17
|
{
|
|
18
18
|
"type": "command",
|
|
19
|
-
"command": "npx --no @nitra/cursor
|
|
20
|
-
"timeout":
|
|
19
|
+
"command": "npx --no @nitra/cursor post-tool-use-fix",
|
|
20
|
+
"timeout": 300
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
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.21.0] - 2026-05-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Stop-hook → PostToolUse з маршрутизацією за типом файла** (BREAKING для консьюмерів із кастомним `stop-hook` записом). `.claude-template/settings.template.json` тепер реєструє `PostToolUse` (matcher `Edit|Write|MultiEdit`, timeout 300) із командою `npx --no @nitra/cursor post-tool-use-fix` замість попереднього синхронного `Stop`-хука, що ганяв повний `fix` усіх правил на кожному turn-і. Новий хук читає `tool_input.file_path` зі stdin і запускає `fix` **лише** з релевантними правилами: `*.{mjs,js,cjs,ts,tsx,jsx}` → `js-lint`; `*.vue` → `js-lint style-lint vue`; `*.{css,scss,sass}` → `style-lint`; `**/k8s/**/*.{yaml,yml}` → `k8s`; `*.rego` → `rego`; `Dockerfile`/`*.Dockerfile` → `docker`; `.github/workflows/*.{yml,yaml}` → `ga`; `package.json` → `npm-module bun`; `*.sh` → `security`; `*.md` → `text` (поза `docs/adr/**` — там покриває async `normalize-decisions.sh`).
|
|
12
|
+
- **CLI**: підкоманду `npx @nitra/cursor stop-hook` видалено; замість неї — `npx @nitra/cursor post-tool-use-fix`. `MANAGED_HOOK_COMMAND_MARKER` у `sync-claude-config.mjs` змінено на `@nitra/cursor post-tool-use-fix`; legacy-маркер `@nitra/cursor stop-hook` лишається у `MANAGED_HOOK_COMMAND_MARKERS` для автоматичного cleanup-у старих entries при наступному `npx @nitra/cursor`. `mergeHooks` тепер обходить union usually template+existing events, тому застарілі managed-групи у вже-непотрібних подіях (`Stop` у даному випадку) теж зачищаються.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `npm/scripts/post-tool-use-fix.mjs` — реалізація `routeFilePathToRules(filePath)` (чиста функція, picomatch) і `runPostToolUseFixCli({ stdinJson, spawnFn })` (DI-friendly для тестів). 21 тест у `npm/scripts/tests/post-tool-use-fix.test.mjs`.
|
|
17
|
+
- `LEGACY_STOP_HOOK_COMMAND_MARKER` — публічний export для тестів і потенційних консьюмерів, які перевіряють відсутність застарілого хука.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- `npm/scripts/claude-stop-hook.mjs` — більше не потрібен.
|
|
22
|
+
|
|
7
23
|
## [1.20.0] - 2026-05-25
|
|
8
24
|
|
|
9
25
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* якщо в корені вже є `.n-cursor.json`, спочатку зчитується конфіг і за потреби дописується `$schema`
|
|
10
10
|
* `npx \@nitra/cursor fix bun` — перевірити лише вказані правила (ігнорує `.cursor/rules/`)
|
|
11
11
|
* `npx \@nitra/cursor rename-yaml-extensions` — k8s `*.yml` → `*.yaml`, `.github` `*.yaml` → `*.yml` (опції: `--dry-run`, `--root=…`; див. bin/rename-yaml-extensions.mjs)
|
|
12
|
-
* `npx \@nitra/cursor
|
|
13
|
-
*
|
|
12
|
+
* `npx \@nitra/cursor post-tool-use-fix` — точка входу PostToolUse hook Claude Code: читає stdin JSON,
|
|
13
|
+
* дістає `tool_input.file_path`, маршрутизує його у відповідні правила
|
|
14
|
+
* (`*.mjs` → `js-lint`, `*.vue` → `js-lint style-lint vue` тощо) і викликає
|
|
15
|
+
* `fix` лише з ними. Прописується автоматично в `.claude/settings.json`.
|
|
14
16
|
* `npx \@nitra/cursor lint-ga` — канонічний lint-ga (ga.mdc): preflight на `shellcheck` →
|
|
15
17
|
* `bunx github-actionlint` → `uvx zizmor --offline --collect=workflows .`
|
|
16
18
|
* `npx \@nitra/cursor lint-rego` — канонічний lint-rego (conftest.mdc + rego.mdc):
|
|
@@ -83,7 +85,7 @@ import {
|
|
|
83
85
|
RULE_MIGRATIONS
|
|
84
86
|
} from '../scripts/auto-rules.mjs'
|
|
85
87
|
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
86
|
-
import {
|
|
88
|
+
import { runPostToolUseFixCli } from '../scripts/post-tool-use-fix.mjs'
|
|
87
89
|
import { discoverCheckRulesFromCursorRules } from '../scripts/lib/discover-check-rules-from-cursor.mjs'
|
|
88
90
|
import { listRuleIds } from '../scripts/lib/list-rule-ids.mjs'
|
|
89
91
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
@@ -1427,10 +1429,10 @@ try {
|
|
|
1427
1429
|
|
|
1428
1430
|
break
|
|
1429
1431
|
}
|
|
1430
|
-
case '
|
|
1431
|
-
// Викликається з .claude/settings.json як
|
|
1432
|
-
//
|
|
1433
|
-
const code = await
|
|
1432
|
+
case 'post-tool-use-fix': {
|
|
1433
|
+
// Викликається з .claude/settings.json як PostToolUse hook Claude Code.
|
|
1434
|
+
// Маршрутизує змінений файл у релевантні правила і прокидає `fix` лише з ними.
|
|
1435
|
+
const code = await runPostToolUseFixCli()
|
|
1434
1436
|
process.exitCode = code
|
|
1435
1437
|
|
|
1436
1438
|
break
|
|
@@ -1488,7 +1490,7 @@ try {
|
|
|
1488
1490
|
default: {
|
|
1489
1491
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1490
1492
|
console.error(
|
|
1491
|
-
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions,
|
|
1493
|
+
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, skill`
|
|
1492
1494
|
)
|
|
1493
1495
|
process.exitCode = 1
|
|
1494
1496
|
}
|
package/package.json
CHANGED
package/rules/adr/adr.mdc
CHANGED
|
@@ -103,22 +103,12 @@ docs/adr/
|
|
|
103
103
|
|
|
104
104
|
## Stop-hook у `.claude/settings.json`
|
|
105
105
|
|
|
106
|
-
Канонічний запис, який вставляє sync (поряд із
|
|
106
|
+
Канонічний запис, який вставляє sync (поряд із PostToolUse fix-хуком — той живе в іншій події, тут не показаний):
|
|
107
107
|
|
|
108
108
|
```json title=".claude/settings.json"
|
|
109
109
|
{
|
|
110
110
|
"hooks": {
|
|
111
111
|
"Stop": [
|
|
112
|
-
{
|
|
113
|
-
"matcher": "",
|
|
114
|
-
"hooks": [
|
|
115
|
-
{
|
|
116
|
-
"type": "command",
|
|
117
|
-
"command": "npx --no @nitra/cursor stop-hook",
|
|
118
|
-
"timeout": 60
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
},
|
|
122
112
|
{
|
|
123
113
|
"matcher": "",
|
|
124
114
|
"hooks": [
|
|
@@ -146,7 +136,7 @@ docs/adr/
|
|
|
146
136
|
}
|
|
147
137
|
```
|
|
148
138
|
|
|
149
|
-
|
|
139
|
+
Обидві ADR-групи ідентифікуються пакетом за маркером у `command` (`.claude/hooks/capture-decisions.sh`, `.claude/hooks/normalize-decisions.sh`) — користувацькі hook-групи поряд не чіпаються. Якщо `adr` прибрати з `rules`, обидві ADR-групи автоматично видаляються на наступному `npx @nitra/cursor`.
|
|
150
140
|
|
|
151
141
|
## Stop-hook у `.cursor/hooks.json`
|
|
152
142
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse hook для Claude Code: точкова маршрутизація `npx @nitra/cursor fix`
|
|
3
|
+
* за типом зміненого файла. Запускається після кожного `Edit` / `Write` / `MultiEdit`;
|
|
4
|
+
* замінює дорогий синхронний `Stop`-хук, що ганяв повний `fix` усіх правил на кожному
|
|
5
|
+
* turn-і.
|
|
6
|
+
*
|
|
7
|
+
* Контракт:
|
|
8
|
+
* - stdin Claude Code: JSON із `tool_input.file_path` (відносний шлях зміненого файла);
|
|
9
|
+
* - exit 0, якщо файл не маршрутизується (PostToolUse не блокує turn у будь-якому випадку,
|
|
10
|
+
* але ми лишаємо exit-код прозорим — для діагностики);
|
|
11
|
+
* - інакше spawn `npx --no @nitra/cursor fix <rules…>` із пробрасуванням exit-коду.
|
|
12
|
+
*
|
|
13
|
+
* Маршрути впорядковані від найбільш специфічного до загального; перший збіг — переможець.
|
|
14
|
+
* `docs/adr/**\/*.md` свідомо повертає `[]`: ADR-нормалізація вже покривається async
|
|
15
|
+
* Stop-hook'ом `normalize-decisions.sh` — повторний `fix adr` тут лише сповільнював би turn.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
import { once } from 'node:events'
|
|
19
|
+
|
|
20
|
+
import picomatch from 'picomatch'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} Route
|
|
24
|
+
* @property {string} pattern picomatch glob (з підтримкою `**` і `{a,b}`)
|
|
25
|
+
* @property {string[]} rules ID правил `npm/rules/<id>` (бо `fix.mjs` обов'язковий)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Порядок важливий: специфічні маршрути (`.github/workflows/*`, `**\/k8s/**`) — перед загальними. */
|
|
29
|
+
/** @type {readonly Route[]} */
|
|
30
|
+
const ROUTES = Object.freeze([
|
|
31
|
+
{ pattern: 'docs/adr/**/*.md', rules: [] },
|
|
32
|
+
{ pattern: '.github/workflows/*.{yml,yaml}', rules: ['ga'] },
|
|
33
|
+
{ pattern: '**/k8s/**/*.{yaml,yml}', rules: ['k8s'] },
|
|
34
|
+
{ pattern: '**/*.vue', rules: ['js-lint', 'style-lint', 'vue'] },
|
|
35
|
+
{ pattern: '**/*.{mjs,js,cjs,ts,tsx,jsx}', rules: ['js-lint'] },
|
|
36
|
+
{ pattern: '**/*.{css,scss,sass}', rules: ['style-lint'] },
|
|
37
|
+
{ pattern: '**/*.rego', rules: ['rego'] },
|
|
38
|
+
{ pattern: '{**/,}Dockerfile', rules: ['docker'] },
|
|
39
|
+
{ pattern: '**/*.Dockerfile', rules: ['docker'] },
|
|
40
|
+
{ pattern: '**/*.sh', rules: ['security'] },
|
|
41
|
+
{ pattern: '{**/,}package.json', rules: ['npm-module', 'bun'] },
|
|
42
|
+
{ pattern: '**/*.md', rules: ['text'] }
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Повертає список правил, які слід прогнати для зміненого `filePath`.
|
|
47
|
+
* Перший збіг із `ROUTES` — переможець; невідомі шляхи / некоректні входи → `[]`.
|
|
48
|
+
* @param {unknown} filePath відносний шлях зміненого файла зі stdin Claude Code
|
|
49
|
+
* @returns {string[]} ID правил для `npx @nitra/cursor fix`
|
|
50
|
+
*/
|
|
51
|
+
export function routeFilePathToRules(filePath) {
|
|
52
|
+
if (typeof filePath !== 'string' || filePath === '') {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
for (const { pattern, rules } of ROUTES) {
|
|
56
|
+
if (picomatch.isMatch(filePath, pattern, { dot: true })) {
|
|
57
|
+
return [...rules]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Зчитує stdin до EOF як utf8 рядок. На TTY — повертає `''` одразу.
|
|
65
|
+
* @returns {Promise<string>} вміст stdin
|
|
66
|
+
*/
|
|
67
|
+
async function readStdin() {
|
|
68
|
+
if (process.stdin.isTTY) {
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
process.stdin.setEncoding('utf8')
|
|
72
|
+
const chunks = []
|
|
73
|
+
process.stdin.on('data', chunk => {
|
|
74
|
+
chunks.push(chunk)
|
|
75
|
+
})
|
|
76
|
+
try {
|
|
77
|
+
await once(process.stdin, 'end')
|
|
78
|
+
} catch {
|
|
79
|
+
// 'error' на stdin — повертаємо те, що встигли зібрати
|
|
80
|
+
}
|
|
81
|
+
return chunks.join('')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Дістає `tool_input.file_path` зі stdin JSON Claude Code. Невалідний JSON
|
|
86
|
+
* або відсутнє поле → `null` (не помилка: дехто з тулів — напр. Bash — не пише `file_path`).
|
|
87
|
+
* @param {string} stdinJson сирий вміст stdin
|
|
88
|
+
* @returns {string | null} відносний шлях або `null`
|
|
89
|
+
*/
|
|
90
|
+
function extractFilePath(stdinJson) {
|
|
91
|
+
if (!stdinJson) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const obj = JSON.parse(stdinJson)
|
|
96
|
+
const fp = obj?.tool_input?.file_path
|
|
97
|
+
return typeof fp === 'string' && fp !== '' ? fp : null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Точка входу. Викликається з `bin/n-cursor.js` коли argv[0] === `post-tool-use-fix`.
|
|
105
|
+
* Параметри ін'єктовні для тестів: `stdinJson` обходить read від `process.stdin`,
|
|
106
|
+
* `spawnFn` — заміна `node:child_process.spawn` (повертає EventEmitter-сумісний об'єкт).
|
|
107
|
+
* @param {{ stdinJson?: string, spawnFn?: typeof spawn }} [options]
|
|
108
|
+
* @returns {Promise<number>} exit code (0 — пропущено / fix ОК; інше — exit-код `fix`)
|
|
109
|
+
*/
|
|
110
|
+
export async function runPostToolUseFixCli(options = {}) {
|
|
111
|
+
const stdinJson = options.stdinJson ?? (await readStdin())
|
|
112
|
+
const filePath = extractFilePath(stdinJson)
|
|
113
|
+
if (filePath === null) {
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
const rules = routeFilePathToRules(filePath)
|
|
117
|
+
if (rules.length === 0) {
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
const spawnFn = options.spawnFn ?? spawn
|
|
121
|
+
const child = spawnFn('npx', ['--no', '@nitra/cursor', 'fix', ...rules], { stdio: 'inherit' })
|
|
122
|
+
try {
|
|
123
|
+
const [code] = await once(child, 'exit')
|
|
124
|
+
return code ?? 1
|
|
125
|
+
} catch (error) {
|
|
126
|
+
process.stderr.write(`post-tool-use-fix: не вдалося запустити npx @nitra/cursor fix — ${error.message}\n`)
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -30,8 +30,10 @@ import { existsSync } from 'node:fs'
|
|
|
30
30
|
import { chmod, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
|
31
31
|
import { join } from 'node:path'
|
|
32
32
|
|
|
33
|
-
/** Маркер
|
|
34
|
-
export const MANAGED_HOOK_COMMAND_MARKER = '@nitra/cursor
|
|
33
|
+
/** Маркер PostToolUse fix-hook'а (`npx --no \@nitra/cursor post-tool-use-fix`). */
|
|
34
|
+
export const MANAGED_HOOK_COMMAND_MARKER = '@nitra/cursor post-tool-use-fix'
|
|
35
|
+
/** Legacy-маркер старого Stop-hook'а — лишаємо для cleanup-у при оновленні існуючих інсталяцій. */
|
|
36
|
+
export const LEGACY_STOP_HOOK_COMMAND_MARKER = '@nitra/cursor stop-hook'
|
|
35
37
|
/** Маркер ADR Stop-hook'а — підрядок шляху до bash-скрипта capture-decisions. */
|
|
36
38
|
export const ADR_HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
|
|
37
39
|
/** Маркер ADR Stop-hook'а — підрядок шляху до bash-скрипта normalize-decisions. */
|
|
@@ -40,9 +42,13 @@ export const ADR_NORMALIZE_HOOK_COMMAND_MARKER = '.claude/hooks/normalize-decisi
|
|
|
40
42
|
export const CURSOR_ADR_HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
|
|
41
43
|
/** Маркер Cursor ADR Normalize Stop-hook'а — той самий script path, але в `.cursor/hooks.json`. */
|
|
42
44
|
export const CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER = '.claude/hooks/normalize-decisions.sh'
|
|
43
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Усі маркери managed-hook'ів пакета — за ними відрізняємо свої записи від користувацьких.
|
|
47
|
+
* Legacy stop-hook включений сюди, щоб старі entries автоматично видалялись при наступному sync-у.
|
|
48
|
+
*/
|
|
44
49
|
export const MANAGED_HOOK_COMMAND_MARKERS = Object.freeze([
|
|
45
50
|
MANAGED_HOOK_COMMAND_MARKER,
|
|
51
|
+
LEGACY_STOP_HOOK_COMMAND_MARKER,
|
|
46
52
|
ADR_HOOK_COMMAND_MARKER,
|
|
47
53
|
ADR_NORMALIZE_HOOK_COMMAND_MARKER
|
|
48
54
|
])
|
|
@@ -189,9 +195,13 @@ export function mergeAllowList(existing, fromTemplate) {
|
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
/**
|
|
192
|
-
* Зливає hooks
|
|
193
|
-
* з існуючої конфігурації
|
|
194
|
-
*
|
|
198
|
+
* Зливає hooks-секцію. Для **кожної події** з обох сторін:
|
|
199
|
+
* 1) видаляємо managed-групи з існуючої конфігурації (їх ідентифікують маркери з
|
|
200
|
+
* `MANAGED_HOOK_COMMAND_MARKERS`, включно з legacy-маркерами — це автоматично
|
|
201
|
+
* прибирає застарілі hook'и при переході на нову версію темплейту);
|
|
202
|
+
* 2) дописуємо managed-групи з темплейту.
|
|
203
|
+
* Перебір union-у подій важливий: коли пакет переносить hook між подіями (напр. `Stop`
|
|
204
|
+
* → `PostToolUse`), старі managed entries у вже-непотрібній події теж мають піти.
|
|
195
205
|
* @param {Record<string, HookGroup[]> | undefined} existing поточна `hooks`-секція з .claude/settings.json
|
|
196
206
|
* @param {Record<string, HookGroup[]> | undefined} fromTemplate цільова `hooks`-секція з темплейту
|
|
197
207
|
* @returns {Record<string, HookGroup[]>} результат злиття (порожні події видаляються)
|
|
@@ -199,14 +209,13 @@ export function mergeAllowList(existing, fromTemplate) {
|
|
|
199
209
|
export function mergeHooks(existing, fromTemplate) {
|
|
200
210
|
/** @type {Record<string, HookGroup[]>} */
|
|
201
211
|
const out = {}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
delete out[event]
|
|
212
|
+
const allEvents = new Set([...Object.keys(existing ?? {}), ...Object.keys(fromTemplate ?? {})])
|
|
213
|
+
for (const event of allEvents) {
|
|
214
|
+
const existingClean = (existing?.[event] ?? []).filter(g => !isManagedHookGroup(g))
|
|
215
|
+
const templateGroups = fromTemplate?.[event] ?? []
|
|
216
|
+
const combined = [...existingClean, ...templateGroups]
|
|
217
|
+
if (combined.length > 0) {
|
|
218
|
+
out[event] = combined
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
return out
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stop-hook для Claude Code: запускається hook'ом із `.claude/settings.json` після того,
|
|
3
|
-
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor fix`
|
|
4
|
-
* і повертає його exit code, щоб помилки правил блокували завершення.
|
|
5
|
-
*
|
|
6
|
-
* Захист від нескінченної рекурсії: якщо stdin містить `"stop_hook_active": true`
|
|
7
|
-
* (Claude Code позначає цей прапорець, коли hook сам спричинив повторний Stop),
|
|
8
|
-
* виходимо з кодом 0 без повторного запуску перевірок.
|
|
9
|
-
*
|
|
10
|
-
* Виклик з `bin/n-cursor.js`:
|
|
11
|
-
* `npx --no \@nitra/cursor stop-hook`
|
|
12
|
-
*/
|
|
13
|
-
import { spawn } from 'node:child_process'
|
|
14
|
-
import { once } from 'node:events'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Зчитує stdin до EOF як utf8 рядок. Якщо stdin порожній (TTY) — повертає '' одразу.
|
|
18
|
-
* @returns {Promise<string>} вміст stdin
|
|
19
|
-
*/
|
|
20
|
-
async function readStdin() {
|
|
21
|
-
if (process.stdin.isTTY) {
|
|
22
|
-
return ''
|
|
23
|
-
}
|
|
24
|
-
process.stdin.setEncoding('utf8')
|
|
25
|
-
const chunks = []
|
|
26
|
-
process.stdin.on('data', chunk => {
|
|
27
|
-
chunks.push(chunk)
|
|
28
|
-
})
|
|
29
|
-
try {
|
|
30
|
-
await once(process.stdin, 'end')
|
|
31
|
-
} catch {
|
|
32
|
-
// 'error' на stdin — повертаємо те, що встигли зібрати
|
|
33
|
-
}
|
|
34
|
-
return chunks.join('')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Чи stdin вказує, що поточний Stop вже виник через попередній Stop hook
|
|
39
|
-
* (Claude Code передає `stop_hook_active: true`). У такому випадку повторний
|
|
40
|
-
* запуск перевірок створив би нескінченний цикл — пропускаємо.
|
|
41
|
-
* @param {string} stdin сирий вміст stdin
|
|
42
|
-
* @returns {boolean} true, якщо рекурсивний виклик
|
|
43
|
-
*/
|
|
44
|
-
export function isRecursiveStopHookCall(stdin) {
|
|
45
|
-
if (!stdin) {
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const obj = JSON.parse(stdin)
|
|
50
|
-
return obj?.stop_hook_active === true
|
|
51
|
-
} catch {
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Точка входу. Викликається з `bin/n-cursor.js` коли argv[0] === 'stop-hook'.
|
|
58
|
-
* @returns {Promise<number>} exit code (0 — OK / пропуск, 1 — помилки правил)
|
|
59
|
-
*/
|
|
60
|
-
export async function runStopHookCli() {
|
|
61
|
-
const stdin = await readStdin()
|
|
62
|
-
if (isRecursiveStopHookCall(stdin)) {
|
|
63
|
-
return 0
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const child = spawn('npx', ['--no', '@nitra/cursor', 'fix'], { stdio: 'inherit' })
|
|
67
|
-
try {
|
|
68
|
-
const [code] = await once(child, 'exit')
|
|
69
|
-
return code ?? 1
|
|
70
|
-
} catch (error) {
|
|
71
|
-
process.stderr.write(`stop-hook: не вдалося запустити npx @nitra/cursor fix — ${error.message}\n`)
|
|
72
|
-
return 1
|
|
73
|
-
}
|
|
74
|
-
}
|