@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.
Files changed (36) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +7 -2
  2. package/.claude-template/hooks/normalize-decisions.sh +7 -1
  3. package/CHANGELOG.md +46 -0
  4. package/bin/n-cursor.js +3 -1
  5. package/package.json +1 -1
  6. package/rules/abie/abie.mdc +1 -9
  7. package/rules/adr/adr.mdc +25 -16
  8. package/rules/adr/fix/hooks/check.mjs +70 -0
  9. package/rules/bun/bun.mdc +3 -20
  10. package/rules/capacitor/capacitor.mdc +4 -8
  11. package/rules/changelog/changelog.mdc +1 -5
  12. package/rules/ci4/ci4.mdc +0 -4
  13. package/rules/docker/docker.mdc +1 -19
  14. package/rules/ga/ga.mdc +1 -26
  15. package/rules/graphql/graphql.mdc +0 -8
  16. package/rules/hasura/hasura.mdc +0 -6
  17. package/rules/image-avif/image-avif.mdc +1 -13
  18. package/rules/image-compress/image-compress.mdc +7 -33
  19. package/rules/js-bun-db/js-bun-db.mdc +3 -6
  20. package/rules/js-bun-redis/js-bun-redis.mdc +3 -6
  21. package/rules/js-lint/js-lint.mdc +3 -16
  22. package/rules/js-mssql/js-mssql.mdc +3 -4
  23. package/rules/js-run/js-run.mdc +3 -10
  24. package/rules/k8s/k8s.mdc +2 -21
  25. package/rules/nginx-default-tpl/nginx-default-tpl.mdc +0 -25
  26. package/rules/npm-module/npm-module.mdc +4 -9
  27. package/rules/rego/rego.mdc +2 -38
  28. package/rules/security/auto.md +1 -0
  29. package/rules/security/fix/gitleaks/check.mjs +62 -0
  30. package/rules/security/policy/package_json/package_json.rego +75 -0
  31. package/rules/security/policy/package_json/target.json +4 -0
  32. package/rules/security/security.mdc +77 -0
  33. package/rules/style-lint/style-lint.mdc +0 -23
  34. package/rules/tauri/tauri.mdc +3 -6
  35. package/scripts/auto-rules.mjs +2 -0
  36. 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 { settings: false, npmClaudeMd: false, commands: [], adrHook: false, adrNormalizeHook: false }
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 { settings: false, npmClaudeMd: false, commands: [], adrHook: false, adrNormalizeHook: false }
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,