@nitra/cursor 1.11.17 → 1.13.1
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 +7 -2
- package/.claude-template/hooks/normalize-decisions.sh +7 -1
- package/CHANGELOG.md +46 -0
- package/bin/n-cursor.js +3 -1
- package/package.json +1 -1
- package/rules/abie/abie.mdc +1 -9
- package/rules/adr/adr.mdc +25 -16
- package/rules/adr/fix/hooks/check.mjs +70 -0
- package/rules/bun/bun.mdc +3 -20
- package/rules/capacitor/capacitor.mdc +4 -8
- package/rules/changelog/changelog.mdc +1 -5
- package/rules/ci4/ci4.mdc +0 -4
- package/rules/docker/docker.mdc +1 -19
- package/rules/ga/ga.mdc +1 -26
- package/rules/graphql/graphql.mdc +0 -8
- package/rules/hasura/hasura.mdc +0 -6
- package/rules/image-avif/image-avif.mdc +1 -13
- package/rules/image-compress/image-compress.mdc +7 -33
- package/rules/js-bun-db/js-bun-db.mdc +3 -6
- package/rules/js-bun-redis/js-bun-redis.mdc +3 -6
- package/rules/js-lint/js-lint.mdc +3 -16
- package/rules/js-mssql/js-mssql.mdc +3 -4
- package/rules/js-run/js-run.mdc +3 -10
- package/rules/k8s/k8s.mdc +2 -21
- package/rules/nginx-default-tpl/nginx-default-tpl.mdc +0 -25
- package/rules/npm-module/npm-module.mdc +4 -9
- package/rules/rego/rego.mdc +2 -38
- package/rules/security/auto.md +1 -0
- package/rules/security/fix/gitleaks/check.mjs +62 -0
- package/rules/security/policy/package_json/package_json.rego +75 -0
- package/rules/security/policy/package_json/target.json +4 -0
- package/rules/security/security.mdc +77 -0
- package/rules/style-lint/style-lint.mdc +0 -23
- package/rules/tauri/tauri.mdc +3 -6
- package/scripts/auto-rules.mjs +2 -0
- package/scripts/sync-claude-config.mjs +133 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Синхронізує конфігурацію Claude Code (`.claude/settings.json`, `npm/CLAUDE.md`,
|
|
3
|
-
* slash-команди для checks, ADR Stop-hook)
|
|
3
|
+
* slash-команди для checks, ADR Stop-hook) і Cursor hooks (`.cursor/hooks.json`)
|
|
4
|
+
* у поточний проєкт із темплейтів пакету
|
|
4
5
|
* `npm/.claude-template/`.
|
|
5
6
|
*
|
|
6
7
|
* Архітектура:
|
|
@@ -17,6 +18,8 @@
|
|
|
17
18
|
* так само автоматично прибирається з settings.json.
|
|
18
19
|
* - `.claude/hooks/normalize-decisions.sh` — fully owned bash-скрипт ADR normalize
|
|
19
20
|
* Stop-hook (батч-нормалізація чернеток); умови — ті самі, що для `capture`.
|
|
21
|
+
* - `.cursor/hooks.json` — **merge**: користувацькі hooks зберігаються; ADR stop
|
|
22
|
+
* entries додаються, коли правило `adr` увімкнене, і видаляються, коли вимкнене.
|
|
20
23
|
*
|
|
21
24
|
* Опт-аут — `claude-config: false` у `.n-cursor.json`.
|
|
22
25
|
*/
|
|
@@ -30,6 +33,10 @@ export const MANAGED_HOOK_COMMAND_MARKER = '@nitra/cursor stop-hook'
|
|
|
30
33
|
export const ADR_HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
|
|
31
34
|
/** Маркер ADR Stop-hook'а — підрядок шляху до bash-скрипта normalize-decisions. */
|
|
32
35
|
export const ADR_NORMALIZE_HOOK_COMMAND_MARKER = '.claude/hooks/normalize-decisions.sh'
|
|
36
|
+
/** Маркер Cursor ADR Stop-hook'а — той самий script path, але в `.cursor/hooks.json`. */
|
|
37
|
+
export const CURSOR_ADR_HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
|
|
38
|
+
/** Маркер Cursor ADR Normalize Stop-hook'а — той самий script path, але в `.cursor/hooks.json`. */
|
|
39
|
+
export const CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER = '.claude/hooks/normalize-decisions.sh'
|
|
33
40
|
/** Усі маркери managed-hook'ів пакета — за ними відрізняємо свої записи від користувацьких. */
|
|
34
41
|
export const MANAGED_HOOK_COMMAND_MARKERS = Object.freeze([
|
|
35
42
|
MANAGED_HOOK_COMMAND_MARKER,
|
|
@@ -41,6 +48,8 @@ const CLAUDE_DIR = '.claude'
|
|
|
41
48
|
const CLAUDE_SETTINGS_FILE = `${CLAUDE_DIR}/settings.json`
|
|
42
49
|
const CLAUDE_COMMANDS_DIR = `${CLAUDE_DIR}/commands`
|
|
43
50
|
const CLAUDE_HOOKS_DIR = `${CLAUDE_DIR}/hooks`
|
|
51
|
+
const CURSOR_DIR = '.cursor'
|
|
52
|
+
const CURSOR_HOOKS_FILE = `${CURSOR_DIR}/hooks.json`
|
|
44
53
|
const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
45
54
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
46
55
|
const NPM_CLAUDE_MD_FILE = 'npm/CLAUDE.md'
|
|
@@ -72,6 +81,26 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
|
|
|
72
81
|
])
|
|
73
82
|
})
|
|
74
83
|
|
|
84
|
+
/** Канонічний Cursor stop-hook для ADR capture. Cursor передає payload через stdin JSON. */
|
|
85
|
+
const CURSOR_ADR_STOP_HOOK = Object.freeze({
|
|
86
|
+
command: [
|
|
87
|
+
"bash -lc 'root=\"$PWD\";",
|
|
88
|
+
`if [ ! -f "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
89
|
+
`bash "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}"'`
|
|
90
|
+
].join(' '),
|
|
91
|
+
timeout: 180
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/** Канонічний Cursor stop-hook для ADR normalize. */
|
|
95
|
+
const CURSOR_ADR_NORMALIZE_STOP_HOOK = Object.freeze({
|
|
96
|
+
command: [
|
|
97
|
+
"bash -lc 'root=\"$PWD\";",
|
|
98
|
+
`if [ ! -f "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
99
|
+
`bash "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}"'`
|
|
100
|
+
].join(' '),
|
|
101
|
+
timeout: 600
|
|
102
|
+
})
|
|
103
|
+
|
|
75
104
|
/**
|
|
76
105
|
* @typedef {object} HookEntry
|
|
77
106
|
* @property {string} type тип hook'а у форматі Claude Code (зазвичай `'command'`)
|
|
@@ -91,6 +120,18 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
|
|
|
91
120
|
* @property {Record<string, HookGroup[]>} [hooks] hooks за подіями (`Stop`, `PreToolUse`, ...)
|
|
92
121
|
*/
|
|
93
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @typedef {object} CursorHookEntry
|
|
125
|
+
* @property {string} command команда, яку виконує Cursor hook
|
|
126
|
+
* @property {number} [timeout] опційний таймаут у секундах
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @typedef {object} CursorHooksConfig
|
|
131
|
+
* @property {number} [version] версія Cursor hooks config
|
|
132
|
+
* @property {Record<string, CursorHookEntry[]>} [hooks] hooks за подіями (`stop`, `afterFileEdit`, ...)
|
|
133
|
+
*/
|
|
134
|
+
|
|
94
135
|
/**
|
|
95
136
|
* Чи hook-група містить лише наші managed-команди (за будь-яким із маркерів пакета).
|
|
96
137
|
* @param {HookGroup} group hook-група з .claude/settings.json
|
|
@@ -105,6 +146,20 @@ function isManagedHookGroup(group) {
|
|
|
105
146
|
)
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Чи Cursor hook entry належить пакету `@nitra/cursor`.
|
|
151
|
+
* @param {CursorHookEntry} entry один entry з `.cursor/hooks.json`
|
|
152
|
+
* @returns {boolean} `true`, якщо command містить managed ADR marker
|
|
153
|
+
*/
|
|
154
|
+
function isManagedCursorHookEntry(entry) {
|
|
155
|
+
return (
|
|
156
|
+
typeof entry?.command === 'string' &&
|
|
157
|
+
[CURSOR_ADR_HOOK_COMMAND_MARKER, CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER].some(marker =>
|
|
158
|
+
entry.command.includes(marker)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
108
163
|
/**
|
|
109
164
|
* Зливає список allow-permissions: union існуючого і темплейтного без дублікатів,
|
|
110
165
|
* порядок — спочатку існуючі (щоб не міняти користувацький порядок), потім нові.
|
|
@@ -196,6 +251,43 @@ export function mergeSettings(existing, template, options = {}) {
|
|
|
196
251
|
return merged
|
|
197
252
|
}
|
|
198
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Зливає `.cursor/hooks.json`: користувацькі entries зберігаються, managed ADR
|
|
256
|
+
* entries у `hooks.stop` перезаписуються або видаляються залежно від `includeAdrHook`.
|
|
257
|
+
* @param {CursorHooksConfig | undefined} existing поточний Cursor hooks config
|
|
258
|
+
* @param {object} [options] опції merge-у
|
|
259
|
+
* @param {boolean} [options.includeAdrHook] чи додати ADR stop entries
|
|
260
|
+
* @returns {CursorHooksConfig} результат злиття
|
|
261
|
+
*/
|
|
262
|
+
export function mergeCursorHooksConfig(existing, options = {}) {
|
|
263
|
+
/** @type {CursorHooksConfig} */
|
|
264
|
+
const merged = { ...existing }
|
|
265
|
+
/** @type {Record<string, CursorHookEntry[]>} */
|
|
266
|
+
const hooks = {}
|
|
267
|
+
for (const [event, entries] of Object.entries(existing?.hooks ?? {})) {
|
|
268
|
+
hooks[event] = Array.isArray(entries) ? [...entries] : []
|
|
269
|
+
}
|
|
270
|
+
const stop = (hooks.stop ?? []).filter(entry => !isManagedCursorHookEntry(entry))
|
|
271
|
+
if (options.includeAdrHook) {
|
|
272
|
+
stop.push(
|
|
273
|
+
/** @type {CursorHookEntry} */ (CURSOR_ADR_STOP_HOOK),
|
|
274
|
+
/** @type {CursorHookEntry} */ (CURSOR_ADR_NORMALIZE_STOP_HOOK)
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
if (stop.length > 0) {
|
|
278
|
+
hooks.stop = stop
|
|
279
|
+
} else {
|
|
280
|
+
delete hooks.stop
|
|
281
|
+
}
|
|
282
|
+
merged.version = typeof merged.version === 'number' ? merged.version : 1
|
|
283
|
+
if (Object.keys(hooks).length > 0) {
|
|
284
|
+
merged.hooks = hooks
|
|
285
|
+
} else {
|
|
286
|
+
delete merged.hooks
|
|
287
|
+
}
|
|
288
|
+
return merged
|
|
289
|
+
}
|
|
290
|
+
|
|
199
291
|
/**
|
|
200
292
|
* Читає JSON-файл; якщо файл відсутній або не валідний — повертає `undefined`.
|
|
201
293
|
* @param {string} path абсолютний шлях до JSON-файлу
|
|
@@ -212,6 +304,27 @@ async function readJsonOrUndefined(path) {
|
|
|
212
304
|
}
|
|
213
305
|
}
|
|
214
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Синхронізує `.cursor/hooks.json` для Cursor Agent stop-hooks. Cursor читає
|
|
309
|
+
* project-level config з `.cursor/hooks.json`; hook scripts лишаються спільними
|
|
310
|
+
* з Claude Code у `.claude/hooks/`.
|
|
311
|
+
* @param {string} projectRoot корінь проєкту, куди писати
|
|
312
|
+
* @param {object} [options] опції merge-у
|
|
313
|
+
* @param {boolean} [options.includeAdrHook] чи додавати ADR stop-hook entries
|
|
314
|
+
* @returns {Promise<{ written: boolean, path: string }>} результат: чи писали файл, та його відносний шлях
|
|
315
|
+
*/
|
|
316
|
+
export async function syncCursorHooksConfig(projectRoot, options = {}) {
|
|
317
|
+
const hooksPath = join(projectRoot, CURSOR_HOOKS_FILE)
|
|
318
|
+
if (!options.includeAdrHook && !existsSync(hooksPath)) {
|
|
319
|
+
return { written: false, path: '' }
|
|
320
|
+
}
|
|
321
|
+
const existing = /** @type {CursorHooksConfig | undefined} */ (await readJsonOrUndefined(hooksPath))
|
|
322
|
+
const merged = mergeCursorHooksConfig(existing, options)
|
|
323
|
+
await mkdir(join(projectRoot, CURSOR_DIR), { recursive: true })
|
|
324
|
+
await writeFile(hooksPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
|
|
325
|
+
return { written: true, path: CURSOR_HOOKS_FILE }
|
|
326
|
+
}
|
|
327
|
+
|
|
215
328
|
/**
|
|
216
329
|
* Синхронізує `.claude/settings.json` за темплейтом, зберігаючи решту
|
|
217
330
|
* користувацьких полів.
|
|
@@ -331,15 +444,29 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
331
444
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
332
445
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
333
446
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
334
|
-
* @returns {Promise<{ settings: boolean, npmClaudeMd: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean }>} прапорці записів settings/CLAUDE.md/ADR-hook(s) та список записаних slash-команд
|
|
447
|
+
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, npmClaudeMd: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean }>} прапорці записів settings/CLAUDE.md/Cursor hooks/ADR-hook(s) та список записаних slash-команд
|
|
335
448
|
*/
|
|
336
449
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
337
450
|
if (!enabled) {
|
|
338
|
-
return {
|
|
451
|
+
return {
|
|
452
|
+
settings: false,
|
|
453
|
+
cursorHooks: false,
|
|
454
|
+
npmClaudeMd: false,
|
|
455
|
+
commands: [],
|
|
456
|
+
adrHook: false,
|
|
457
|
+
adrNormalizeHook: false
|
|
458
|
+
}
|
|
339
459
|
}
|
|
340
460
|
const templateDir = join(bundledPackageRoot, TEMPLATE_DIR_NAME)
|
|
341
461
|
if (!existsSync(templateDir)) {
|
|
342
|
-
return {
|
|
462
|
+
return {
|
|
463
|
+
settings: false,
|
|
464
|
+
cursorHooks: false,
|
|
465
|
+
npmClaudeMd: false,
|
|
466
|
+
commands: [],
|
|
467
|
+
adrHook: false,
|
|
468
|
+
adrNormalizeHook: false
|
|
469
|
+
}
|
|
343
470
|
}
|
|
344
471
|
const includeAdrHook = Array.isArray(rules) && rules.includes('adr')
|
|
345
472
|
const adrHook = includeAdrHook ? await syncAdrHookScript(projectRoot, templateDir) : { written: false, path: '' }
|
|
@@ -347,10 +474,12 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
347
474
|
? await syncAdrNormalizeHookScript(projectRoot, templateDir)
|
|
348
475
|
: { written: false, path: '' }
|
|
349
476
|
const settings = await syncClaudeSettings(projectRoot, templateDir, { includeAdrHook })
|
|
477
|
+
const cursorHooks = await syncCursorHooksConfig(projectRoot, { includeAdrHook })
|
|
350
478
|
const npmClaudeMd = await syncNpmClaudeMd(projectRoot, templateDir)
|
|
351
479
|
const commands = await syncClaudeCommands(projectRoot, templateDir)
|
|
352
480
|
return {
|
|
353
481
|
settings: settings.written,
|
|
482
|
+
cursorHooks: cursorHooks.written,
|
|
354
483
|
npmClaudeMd: npmClaudeMd.written,
|
|
355
484
|
commands,
|
|
356
485
|
adrHook: adrHook.written,
|