@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
@@ -9,6 +9,10 @@
9
9
  # (default: claude-4.6-sonnet-medium)
10
10
  # neither — exit 0 silently
11
11
  #
12
+ # Hook payloads:
13
+ # - Claude Code Stop: `transcript_path`, `session_id`, `CLAUDE_PROJECT_DIR`
14
+ # - Cursor stop: `transcript_path`, `conversation_id` / `generation_id`, `workspace_roots[]`
15
+ #
12
16
  # Bundled with @nitra/cursor; project copy is auto-synced by the `adr` rule.
13
17
  set -euo pipefail
14
18
 
@@ -19,9 +23,10 @@ export CAPTURE_DECISIONS_RUNNING=1
19
23
 
20
24
  INPUT=$(cat)
21
25
  TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
22
- SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"')
26
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // .conversation_id // .generation_id // "unknown"')
27
+ CURSOR_WORKSPACE_ROOT=$(printf '%s' "$INPUT" | jq -r '.workspace_roots[0] // empty')
23
28
 
24
- PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
29
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-${CURSOR_WORKSPACE_ROOT:-$PWD}}"
25
30
  ADR_DIR="$PROJECT_ROOT/docs/adr"
26
31
  LOG_DIR="$PROJECT_ROOT/.claude/hooks"
27
32
  LOG="$LOG_DIR/capture-decisions.log"
@@ -12,6 +12,10 @@
12
12
  # (default: claude-4.6-sonnet-medium)
13
13
  # neither — exit 0 silently
14
14
  #
15
+ # Hook payloads:
16
+ # - Claude Code Stop: `CLAUDE_PROJECT_DIR`
17
+ # - Cursor stop: `workspace_roots[]`
18
+ #
15
19
  # Portable bash 3.2 (macOS /bin/bash): no `mapfile`, no associative arrays.
16
20
  #
17
21
  # Bundled with @nitra/cursor; project copy is auto-synced by the `adr` rule.
@@ -23,7 +27,9 @@ if [ -n "${ADR_NORMALIZE_RUNNING:-}" ]; then
23
27
  fi
24
28
  export ADR_NORMALIZE_RUNNING=1
25
29
 
26
- PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
30
+ INPUT=$(cat || true)
31
+ CURSOR_WORKSPACE_ROOT=$(printf '%s' "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null || true)
32
+ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-${CURSOR_WORKSPACE_ROOT:-$PWD}}"
27
33
  ADR_DIR="$PROJECT_ROOT/docs/adr"
28
34
  LOG_DIR="$PROJECT_ROOT/.claude/hooks"
29
35
  LOG="$LOG_DIR/normalize-decisions.log"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.1] - 2026-05-17
8
+
9
+ ### Added
10
+
11
+ - **`adr` rule: Cursor Agent Stop-hook support** — `npx @nitra/cursor` тепер merge-ить project-level `.cursor/hooks.json` і додає managed `hooks.stop` entries для `.claude/hooks/capture-decisions.sh` та `.claude/hooks/normalize-decisions.sh`. Hook-скрипти приймають Cursor payload (`transcript_path`, `conversation_id` / `generation_id`, `workspace_roots[]`) і використовують той самий ADR capture/normalize pipeline, що й Claude Code.
12
+
13
+ ## [1.13.0] - 2026-05-17
14
+
15
+ ### Changed
16
+
17
+ - **9 правил переведено з `alwaysApply: true` на `alwaysApply: false` + `globs:`** — AI-контекст у Cursor/Claude Code підвантажується лише при роботі з релевантними файлами; програмна валідація через `npx check <rule>` залишається повністю функціональною незалежно від AI-контексту. Економить контекстне вікно у сесіях, де редагують код, далекий від відповідних конфігів.
18
+ - **`bun`** (`1.7 → 1.8`) — `globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"`
19
+ - **`capacitor`** (`1.0 → 1.1`) — `globs: "**/capacitor.config.json,**/android/**,**/ios/**"`
20
+ - **`js-bun-db`** (`1.6 → 1.7`) — `globs: "**/package.json,**/src/conn/**"`
21
+ - **`js-bun-redis`** (`1.0 → 1.1`) — `globs: "**/package.json,**/src/conn/**"`
22
+ - **`js-mssql`** (`1.3 → 1.4`) — `globs: "**/package.json,**/src/conn/mssql-*"`
23
+ - **`npm-module`** (`1.12 → 1.13`) — `globs: "npm/**,**/package.json,**/hk.pkl,.github/workflows/npm-publish.yml,**/tsconfig*.json"`
24
+ - **`tauri`** (`1.0 → 1.1`) — `globs: "**/src-tauri/**,**/tauri.conf.json"`
25
+ - **`js-lint`** (`1.21 → 1.22`) — `globs: "**/{.oxlintrc.json,eslint.config.js,.jscpd.json,knip.json,package.json},**/*.{js,mjs,cjs,jsx,ts,tsx}"`
26
+ - **`js-run`** (`1.7 → 1.8`) — `globs: "**/package.json,**/jsconfig.json,**/src/**/*.{js,mjs,cjs,ts,tsx}"`
27
+ - **Мотивація:** усі 9 правил мають повне покриття `npx @nitra/cursor check <rule>` (JS-перевірка + Rego policy). Тримати їх `alwaysApply: true` без потреби палить контекст AI у сесіях, де редагується непов'язаний код. Помилка → check ловить → AI виправляє — той самий цикл, що для `security@1.12.1` і `changelog`/`image-compress`/`php`/`vue`.
28
+ - **Залишено `alwaysApply: true`** — `text` (cross-cutting cspell-словник, апостроф), `adr` (про процес capture/normalize hooks), `ci4` (0 програмних чекерів — без AI-контексту правило мертве), `abie` (має JS `applies`-гейт, у не-abie репо мовчить; файли розкидані).
29
+
30
+ ## [1.12.1] - 2026-05-17
31
+
32
+ ### Changed
33
+
34
+ - **`npm/rules/security/security.mdc`** — `alwaysApply: true` → `alwaysApply: false` + `globs: "**/.gitleaks.toml,**/package.json,**/.github/workflows/**/*.yml"`. AI-контекст правила тепер підвантажується лише при роботі з релевантними файлами (за зразком `changelog`/`image-compress`/`php`), а не на кожен турн. Програмна валідація через `npx @nitra/cursor check security` залишається завжди увімкненою (через `auto.md = завжди`) і ловить помилки незалежно від AI-контексту. Версія frontmatter `1.0` → `1.1`.
35
+ - **Мотивація:** правило має повне покриття перевіркою (Rego + JS-check); тримати його `alwaysApply: true` не дає AI додаткової цінності понад те, що `check security` ловить програмно — лише марно займає контекстне вікно при роботі з кодом, не повʼязаним з конфігурацією security.
36
+
37
+ ## [1.12.0] - 2026-05-16
38
+
39
+ ### Added
40
+
41
+ - **Нове правило `security`** (увімкнене за замовчуванням, як `text`/`adr`) — секрет-сканер на базі [gitleaks](https://github.com/gitleaks/gitleaks). Вимагає:
42
+ - `scripts.lint-security` у `package.json` з викликом `gitleaks detect` (або `gitleaks git`);
43
+ - `bun run lint-security` всередині агрегованого `scripts.lint` (якщо `lint` є);
44
+ - `.gitleaks.toml` у корені з `useDefault = true` у блоці `[extend]` (без перетирання вбудованих правил);
45
+ - `gitleaks` **не** у `dependencies`/`devDependencies` (інструмент глобальний, як `shellcheck`/`conftest`).
46
+ - **`npm/rules/security/{security.mdc,auto.md,fix/gitleaks/check.mjs,policy/package_json/{package_json.rego,target.json,package_json_test.rego}}`** — повна структура правила за зразком `image-compress` (Rego per-document валідація + JS-частина для FS). 9 rego-тестів + 5 JS-тестів.
47
+ - **`npm/scripts/auto-rules.mjs`** — `'security'` додано в `AUTO_RULE_ORDER` (alphabetical, між `rego` і `style-lint`) і викликається `addRule('security')` без умови. `auto-rules.test.mjs` оновлено.
48
+
49
+ ### Motivation
50
+
51
+ Команда вже виявляла секрети у public-репо вручну (gitleaks локально + GitHub Push Protection); правило виносить інструмент у канонічний `bun run lint`, щоб витоки ловилися ще до push. Default-on, бо «опт-ін на secret-scanning» — це anti-pattern: репо, що не вмикав security вручну, найімовірніше і є той, що зливає секрети.
52
+
7
53
  ## [1.11.17] - 2026-05-16
8
54
 
9
55
  ### Fixed
package/bin/n-cursor.js CHANGED
@@ -20,8 +20,9 @@
20
20
  * `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
21
21
  * `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
22
22
  *
23
- * Claude Code інтеграція: під час синку, окрім `.cursor/rules` і `.claude/commands` (з skills), CLI ще раз
23
+ * Agent інтеграція: під час синку, окрім `.cursor/rules` і `.claude/commands` (з skills), CLI ще раз
24
24
  * синхронізує `.claude/settings.json` (hooks + permissions; merge — користувацькі поля зберігаються),
25
+ * `.cursor/hooks.json` (Cursor Agent hooks; merge — користувацькі hooks зберігаються),
25
26
  * `npm/CLAUDE.md` (path-scoped нагадування для роботи в `npm/`) і slash-команди checks (`/n-check`).
26
27
  * Опт-аут — поле `claude-config: false` у `.n-cursor.json`.
27
28
  *
@@ -1311,6 +1312,7 @@ async function runSync() {
1311
1312
  }
1312
1313
  const parts = []
1313
1314
  if (result.settings) parts.push('.claude/settings.json')
1315
+ if (result.cursorHooks) parts.push('.cursor/hooks.json')
1314
1316
  if (result.npmClaudeMd) parts.push('npm/CLAUDE.md')
1315
1317
  if (result.commands.length > 0) parts.push(`${result.commands.length} slash-commands`)
1316
1318
  if (result.adrHook) parts.push('.claude/hooks/capture-decisions.sh')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.11.17",
3
+ "version": "1.13.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -6,10 +6,6 @@ version: '1.20'
6
6
 
7
7
  Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`file-link-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua**)), гілки **dev**, **ua** у **clean-merged-branch**, а також заборона тримати артефакти **Firebase Hosting** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; у самому корені ці імена не вимагаються до видалення).
8
8
 
9
- **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
-
11
- **Канон перевірки** — **`npm/scripts/check-abie.mjs`** у пакеті **`@nitra/cursor`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
12
-
13
9
  ## k8s: `hc.yaml` поруч із Deployment
14
10
 
15
11
  Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`httpHealthCheck.requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: канонічно **`/healthz`**, але також допустимі **`/IsAlive`**, **`/api/live`** тощо — узгоджується з реальним endpoint сервісу), порт **8080**, **`targetRef.name`** — **headless** **Service** з суфіксом **`-hl`** (узгоджено з парою **`svc.yaml`** / **`svc-hl.yaml`** у **k8s.mdc**): або **`${metadata.name}-hl`**, або те саме ім’я, якщо **`metadata.name`** уже з **`-hl`**.
@@ -157,11 +153,7 @@ with:
157
153
  ignore_branches: main,dev,ua
158
154
  ```
159
155
 
160
- ## Перевірка
161
-
162
- **`npx @nitra/cursor check abie`**
163
-
164
- ### Швидкий gate через conftest (Rego)
156
+ ## Швидкий gate через conftest (Rego)
165
157
 
166
158
  Підмножину пер-документних правил продубльовано як rego-полісі у **`npm/rules/abie/policy/`** (запускається через **`bun run lint-rego`** для `*_test.rego` юніт-тестів і через **`npx @nitra/cursor check abie`** для прогону по реальних YAML — деталі в **conftest.mdc** / **n-rego.mdc**). JS у **`fix/<concern>/check.mjs`** authoritative — rego тільки швидкий gate для одиничного маніфеста (зокрема через IDE-розширення `tsandall.opa`).
167
159
 
package/rules/adr/adr.mdc CHANGED
@@ -1,18 +1,9 @@
1
1
  ---
2
- description: Автоматичний збір ADR/Runbook/Knowledge-чернеток і батч-нормалізація у `docs/adr/` через Stop-хук Claude Code
2
+ description: Автоматичний збір ADR/Runbook/Knowledge-чернеток і батч-нормалізація у `docs/adr/` через Stop-хуки Claude Code та Cursor Agent
3
3
  alwaysApply: true
4
4
  version: '2.0'
5
5
  ---
6
6
 
7
- Правило увімкнене **за замовчуванням** (як [[text]]) — `npx @nitra/cursor` сам додає `"adr"` у `rules` файлу `.n-cursor.json`. Щоб вимкнути для конкретного репо (не кожен проєкт хоче літати ADR-чернеткам у `docs/`), додай `"adr"` у `disable-rules`.
8
-
9
- Коли правило увімкнене (за замовчуванням), **`npx @nitra/cursor`** автоматично:
10
-
11
- - копіює канонічний bash-скрипт у **`.claude/hooks/capture-decisions.sh`** (executable, повністю керується пакетом — на кожен sync перезаписується);
12
- - копіює канонічний bash-скрипт у **`.claude/hooks/normalize-decisions.sh`** (батч-нормалізація чернеток через LLM);
13
- - додає у **`hooks.Stop`** в **`.claude/settings.json`** дві managed-групи: capture (`async: true`, `timeout: 180`) і normalize (`async: true`, `timeout: 600`);
14
- - ці зміни — додаткові до lint Stop-hook (`@nitra/cursor stop-hook`); усі три групи живуть поряд у `Stop`.
15
-
16
7
  ## Дві фази, один каталог
17
8
 
18
9
  ADR живуть у єдиному каталозі **`docs/adr/`**. Є два стани файлу, які відрізняються YAML frontmatter:
@@ -26,6 +17,8 @@ ADR живуть у єдиному каталозі **`docs/adr/`**. Є два
26
17
 
27
18
  Stop-hook `capture-decisions.sh` зчитує JSONL-транскрипт сесії (через `jq`), витягає текст, `thinking`-блоки та назви `tool_use`-викликів, передає компактний дайджест у LLM CLI з промптом українською і записує результат у **`docs/adr/<timestamp>-<session>.md`**, якщо модель повернула блок з шапкою `## ADR|Runbook|Knowledge …`. Якщо модель повернула `NONE` (тривіальна сесія) — нічого не пишеться. Рекурсію з внутрішнього виклику моделі блокує env-var `CAPTURE_DECISIONS_RUNNING=1`.
28
19
 
20
+ Для Cursor payload скрипт бере `transcript_path`, `conversation_id` / `generation_id` і `workspace_roots[0]`; для Claude Code — `transcript_path`, `session_id` і `CLAUDE_PROJECT_DIR`.
21
+
29
22
  ### Фаза 2 — Normalize
30
23
 
31
24
  Stop-hook `normalize-decisions.sh` спрацьовує на тому самому `Stop`-евенті, але:
@@ -88,12 +81,12 @@ docs/adr/
88
81
  ├── normalize-decisions.log # лог запусків normalize (НЕ коміти)
89
82
  ├── .normalize-state # timestamp останнього normalize-запуску (НЕ коміти)
90
83
  └── .normalize.lock # lock-файл (НЕ коміти)
84
+ .cursor/
85
+ └── hooks.json # Cursor Agent stop-hooks для тих самих скриптів
91
86
  ```
92
87
 
93
88
  `.gitignore` повинен містити рядки, що покривають **`.claude/hooks/*.log`** і службові файли normalize (`.claude/hooks/.normalize-*`).
94
89
 
95
- > Якщо в репозиторії лишився старий каталог **`docs/adr/_inbox/`** з попередньої версії правила — `normalize-decisions.sh` бачить його рекурсивно й поступово розчистить. Можна також одразу `git mv docs/adr/_inbox/*.md docs/adr/` і прибрати порожній каталог.
96
-
97
90
  ## Stop-hook у `.claude/settings.json`
98
91
 
99
92
  Канонічний запис, який вставляє sync (поряд із lint stop-hook):
@@ -141,10 +134,26 @@ docs/adr/
141
134
 
142
135
  Усі три групи ідентифікуються пакетом за маркером у `command` (`@nitra/cursor stop-hook`, `.claude/hooks/capture-decisions.sh`, `.claude/hooks/normalize-decisions.sh`) — користувацькі hook-групи поряд не чіпаються. Якщо `adr` прибрати з `rules`, обидві ADR-групи автоматично видаляються на наступному `npx @nitra/cursor`.
143
136
 
144
- ## Локальні vs project-shared налаштування
137
+ ## Stop-hook у `.cursor/hooks.json`
145
138
 
146
- Обидва Stop-hook'и ADR живуть у **project-shared** `.claude/settings.json` (закомічений), щоб механізм працював у всіх членів команди. Якщо хук колись був у `.claude/settings.local.json` прибери дубль вручну: project-shared і local-копія створили б два запуски на одну подію.
139
+ Cursor Agent читає project-level **`.cursor/hooks.json`**. `npx @nitra/cursor` merge-ить файл: користувацькі hooks лишаються, managed entries для `capture-decisions.sh` і `normalize-decisions.sh` перезаписуються або видаляються разом із правилом `adr`.
147
140
 
148
- ## Перевірка
141
+ ```json title=".cursor/hooks.json"
142
+ {
143
+ "version": 1,
144
+ "hooks": {
145
+ "stop": [
146
+ {
147
+ "command": "bash -lc 'root=\"$PWD\"; if [ ! -f \"$root/.claude/hooks/capture-decisions.sh\" ] && [ -f \"$root/../.claude/hooks/capture-decisions.sh\" ]; then root=\"$root/..\"; fi; bash \"$root/.claude/hooks/capture-decisions.sh\"'",
148
+ "timeout": 180
149
+ },
150
+ {
151
+ "command": "bash -lc 'root=\"$PWD\"; if [ ! -f \"$root/.claude/hooks/normalize-decisions.sh\" ] && [ -f \"$root/../.claude/hooks/normalize-decisions.sh\" ]; then root=\"$root/..\"; fi; bash \"$root/.claude/hooks/normalize-decisions.sh\"'",
152
+ "timeout": 600
153
+ }
154
+ ]
155
+ }
156
+ }
157
+ ```
149
158
 
150
- `npx @nitra/cursor check adr`
159
+ Обидва Stop-hook'и ADR живуть у **project-shared** `.claude/settings.json` (закомічений), щоб механізм працював у всіх членів команди. Якщо хук колись був у `.claude/settings.local.json` прибери дубль вручну: project-shared і local-копія створили б два запуски на одну подію.
@@ -8,6 +8,8 @@
8
8
  * пакета (sync керує файлами повністю).
9
9
  * - `.claude/settings.json` (project-shared) має managed-групи у `hooks.Stop` для
10
10
  * обох скриптів (маркери у `command` — самі шляхи до скриптів).
11
+ * - `.cursor/hooks.json` має managed entries у `hooks.stop` для обох скриптів, щоб
12
+ * Cursor Agent теж запускав ADR capture/normalize після завершення відповіді.
11
13
  * - `.claude/settings.local.json` (якщо існує) НЕ має дублів цих managed-груп —
12
14
  * після переходу на project-shared такі записи створили б два запуски на одну подію.
13
15
  * - `.gitignore` у корені містить шаблон, який покриває
@@ -31,6 +33,7 @@ const HOOK_ARTIFACTS = /** @type {const} */ ([
31
33
  ])
32
34
 
33
35
  const PROJECT_SETTINGS_PATH = '.claude/settings.json'
36
+ const CURSOR_HOOKS_PATH = '.cursor/hooks.json'
34
37
  const EOL_RE = /\r?\n/u
35
38
 
36
39
  const here = dirname(fileURLToPath(import.meta.url))
@@ -119,6 +122,72 @@ function checkProjectSettings(reporter) {
119
122
  }
120
123
  }
121
124
 
125
+ /**
126
+ * Читає JSON-файл із диска без винятку.
127
+ * @param {string} path відносний шлях до JSON-файлу
128
+ * @returns {Promise<unknown | null>} розпарсений JSON або null
129
+ */
130
+ async function readJsonSafe(path) {
131
+ try {
132
+ return JSON.parse(await readFile(path, 'utf8'))
133
+ } catch {
134
+ return null
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Чи має Cursor hooks config stop-entry з потрібним command marker.
140
+ * @param {unknown} config розпарсений `.cursor/hooks.json`
141
+ * @param {string} marker підрядок, який має бути в `command`
142
+ * @returns {boolean} true, якщо marker знайдено у `hooks.stop[]`
143
+ */
144
+ function cursorConfigHasStopHook(config, marker) {
145
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
146
+ return false
147
+ }
148
+ const hooks = /** @type {{ hooks?: unknown }} */ (config).hooks
149
+ if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) {
150
+ return false
151
+ }
152
+ const stop = /** @type {{ stop?: unknown }} */ (hooks).stop
153
+ if (!Array.isArray(stop)) {
154
+ return false
155
+ }
156
+ return stop.some(entry => {
157
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
158
+ return false
159
+ }
160
+ const command = /** @type {{ command?: unknown }} */ (entry).command
161
+ return typeof command === 'string' && command.includes(marker)
162
+ })
163
+ }
164
+
165
+ /**
166
+ * Перевіряє project-level Cursor hooks config для ADR stop-hooks.
167
+ * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер
168
+ * @returns {Promise<void>}
169
+ */
170
+ async function checkCursorHooks(reporter) {
171
+ const { pass, fail } = reporter
172
+ if (!existsSync(CURSOR_HOOKS_PATH)) {
173
+ fail(`${CURSOR_HOOKS_PATH} не існує — запусти \`npx @nitra/cursor\``)
174
+ return
175
+ }
176
+ const config = await readJsonSafe(CURSOR_HOOKS_PATH)
177
+ if (config === null) {
178
+ fail(`${CURSOR_HOOKS_PATH} не парситься як JSON — запусти \`npx @nitra/cursor\` або виправ файл`)
179
+ return
180
+ }
181
+ for (const { scriptName } of HOOK_ARTIFACTS) {
182
+ const marker = projectHookPath(scriptName)
183
+ if (cursorConfigHasStopHook(config, marker)) {
184
+ pass(`${CURSOR_HOOKS_PATH} має stop-hook для ${marker}`)
185
+ } else {
186
+ fail(`${CURSOR_HOOKS_PATH}: відсутній stop-hook для \`${marker}\` (adr.mdc)`)
187
+ }
188
+ }
189
+ }
190
+
122
191
  /**
123
192
  * Перевіряє `.gitignore` на ігнорування лог-файлу одного хука.
124
193
  * @param {import('../../../../scripts/utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
@@ -212,6 +281,7 @@ export async function check() {
212
281
  await checkHookScript(reporter, scriptName)
213
282
  }
214
283
  checkProjectSettings(reporter)
284
+ await checkCursorHooks(reporter)
215
285
  await checkGitignore(reporter)
216
286
  checkLlmCliAvailable(reporter)
217
287
  return reporter.getExitCode()
package/rules/bun/bun.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Bun як єдиний package manager у монорепо
3
- alwaysApply: true
4
- version: '1.7'
3
+ globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"
4
+ alwaysApply: false
5
+ version: '1.8'
5
6
  ---
6
7
 
7
8
  Проект використовує тільки Bun для керування залежностями та запуску скриптів.
@@ -25,21 +26,8 @@ version: '1.7'
25
26
  - `bunx <tool>`
26
27
  - `npx <tool>`
27
28
 
28
- - Для встановлення залежностей використовуй `bun i`.
29
- - Для запуску скриптів використовуй `bun run <script>`.
30
- - Для додавання залежностей:
31
- - `bun add <pkg>`
32
- - `bun add -d <pkg>` для devDependencies
33
- - Для одноразових CLI-команд використовуй `bunx <tool>`.
34
-
35
29
  **Не додавай** у `dependencies` / `devDependencies` пакети, які **використовуються лише як CLI** і їх достатньо викликати через **`bunx <pkg>`** (або **`npx`**, якщо в проєкті так прийнято): наприклад **oxlint**, **jscpd**, **eslint** у корені тощо. Виняток — пакет потрібен як **бібліотека** (імпорт у коді), peer для іншого пакета, або інструмент **не** покривається `bunx` у вашому CI.
36
30
 
37
- Заборонено використовувати:
38
-
39
- - `npm`
40
- - `yarn`
41
- - `pnpm`
42
-
43
31
  Lockfile у репозиторії: `bun.lock`.
44
32
  Не створювати `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`.
45
33
  Видалити якщо вони є. Видалити .yarn та .yarnrc.yml якщо вони є.
@@ -61,7 +49,6 @@ linker = "hoisted"
61
49
 
62
50
  Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn
63
51
 
64
- Якщо в проекті використовується npx, то не замінювати його на bunx, а використовувати npx.
65
52
  Коли зміна відбувається в Dockerfile, то використовувати
66
53
 
67
54
  ```dockerfile
@@ -80,10 +67,6 @@ FROM oven/bun:alpine AS build-env
80
67
 
81
68
  Якщо в репозиторії action збережено під **`./npm/github-actions/setup-bun-deps`**, у `uses:` вкажи цей шлях замість `.github/actions/…` (**ga.mdc**).
82
69
 
83
- ## Перевірка
84
-
85
- `npx @nitra/cursor check bun`
86
-
87
70
  ## lint
88
71
 
89
72
  Якщо в кореневому @package.json існують скрипти з префіксом `lint-`, обов'язково створюй `lint` скрипт, який буде запускати всі ці скрипти з лінт-префіксом.
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  description: Правила для проєктів Capacitor
3
- alwaysApply: true
4
- version: '1.0'
3
+ globs: "**/capacitor.config.json,**/android/**,**/ios/**"
4
+ alwaysApply: false
5
+ version: '1.1'
5
6
  ---
6
7
 
7
8
  ## Версія Capacitor
8
9
 
9
10
  У `package.json` (у **корені** репозиторію чи **workspace**-пакеті) оголошення **`@capacitor/core`**
10
11
  має вказувати діапазон, **сумісний лише з мажорною версією 8 і вище** (наприклад `^8.0.0`).
11
- **`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні. Програма перевірки — **`check-capacitor.mjs`**
12
- (репозиторій **@nitra/cursor**).
12
+ **`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні.
13
13
 
14
14
  ## iOS: зазвичай лише SPM, виняток Podfile
15
15
 
@@ -32,7 +32,3 @@ version: '1.0'
32
32
 
33
33
  - Перевірка читає **лише** кореневі файли: **`package.json`**, потім **capacitor-конфіги** у **корені** (див. вище).
34
34
  У **`.ts` / `.mjs`**: шукається блок **nitra** `{ ... }` і **на його тілі** перевіряються ці **boolean**-поля.
35
-
36
- ## Перевірка
37
-
38
- `npx @nitra/cursor check capacitor` (коли **check-скрипт** підключено до цієї **ruleset**).
@@ -5,7 +5,7 @@ globs: "**/{CHANGELOG.md,package.json}"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
- Bun monorepo: у кожному workspace із кореневого `package.json.workspaces` (плюс кореневий пакет, плюс `npm/`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій. Правило `npm-module` відповідає лише за публікацію типів і workflow, а CHANGELOG — за цим правилом.
8
+ Bun monorepo: у кожному workspace із кореневого `package.json.workspaces` (плюс кореневий пакет, плюс `npm/`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій.
9
9
 
10
10
  ## Дві моделі бази порівняння
11
11
 
@@ -59,7 +59,3 @@ Git у цьому режимі не використовується — пор
59
59
  ```
60
60
 
61
61
  Секції — підмножина `### Added`, `### Changed`, `### Fixed`, `### Removed` (одна або кілька).
62
-
63
- ## Перевірка
64
-
65
- `npx @nitra/cursor check changelog`
package/rules/ci4/ci4.mdc CHANGED
@@ -101,7 +101,3 @@ C4-схеми потрібно оновити, і робимо це у тому
101
101
  C4-схеми — частина **користувацької документації**, а не закритий артефакт для команди.
102
102
  Контекстна діаграма (рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у
103
103
  проєкт, а не у відокремленій теці «for-architects».
104
-
105
- ## Перевірка
106
-
107
- `npx @nitra/cursor check ci4`
@@ -7,8 +7,6 @@ alwaysApply: false
7
7
 
8
8
  # Docker — hadolint
9
9
 
10
- [hadolint](https://github.com/hadolint/hadolint) перевіряє Dockerfile на типові помилки та рекомендації (`FROM`, `RUN`, `COPY`, shell form тощо).
11
-
12
10
  Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginxinc/nginx-unprivileged`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub: **`mirror.gcr.io/oven/bun`**, **`mirror.gcr.io/library/alpine`**, **`mirror.gcr.io/nginxinc/nginx-unprivileged`**, **`mirror.gcr.io/library/node`**. Перевіряє **`check-docker.mjs`**, деталі в **`npm/scripts/utils/docker-mirror.mjs`**.
13
11
 
14
12
  Також Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
@@ -181,20 +179,4 @@ ignored:
181
179
 
182
180
  Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
183
181
 
184
- ## Агентам
185
-
186
- - Після правок у Dockerfile проганяй **`bun run lint-docker`** і/або **`check docker`**.
187
- - Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
188
- - Образи на базі Bun — див. **`n-bun.mdc`**.
189
-
190
- ## Редактор
191
-
192
- Розширення VS Code / Cursor: **`exiasr.hadolint`** (потрібна утиліта hadolint) — за потреби в **`.vscode/extensions.json`**.
193
-
194
- ## Перевірка
195
-
196
- `npx @nitra/cursor check docker`
197
-
198
- Після змін у Dockerfile: **`bun run lint-docker`** (обов'язково для проєктів з правилом **`docker`** у **`.n-cursor.json`**).
199
-
200
- Kubernetes YAML і `$schema` у `k8s/` — окреме правило **`k8s.mdc`**, **`check k8s`**.
182
+ Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
package/rules/ga/ga.mdc CHANGED
@@ -166,20 +166,6 @@ jobs:
166
166
 
167
167
  **Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
168
168
 
169
- ```json title=".vscode/extensions.json"
170
- {
171
- "recommendations": ["github.vscode-github-actions"]
172
- }
173
- ```
174
-
175
- У **`.vscode/settings.json`** для мови **`github-actions-workflow`** (workflow з розширення GitHub Actions) задай **oxc** як formatter:
176
-
177
- ```json
178
- "[github-actions-workflow]": {
179
- "editor.defaultFormatter": "oxc.oxc-vscode"
180
- }
181
- ```
182
-
183
169
  **ЗАБОРОНЕНО** дублювати кроки встановлення Bun та кешування безпосередньо у workflow файлах. Завжди використовуй локальний composite action.
184
170
 
185
171
  **Кроки `run`:** не розбивай команду shell-продовженням через зворотний сліш у кінці рядка (`… \` у `run: |`). Замість багаторядкового буквального блока з `\\` оформ довгу одну shell-команду як **folded block** `>-` (рядки з’єднаються в один рядок із пробілами).
@@ -264,13 +250,7 @@ jobs:
264
250
 
265
251
  > Не використовуй `npx --no @nitra/cursor lint-ga` — `bun run` автоматично транслює `npx` у `bun x`, а `bun x` для скоупованого пакету з одним bin-ім’ям повертає 0 без виконання. Виклик через bin-ім’я `n-cursor` працює і у `bun run`, і у `npm run`.
266
252
 
267
- CLI виконує:
268
-
269
- 1. Preflight на [`shellcheck`](https://www.shellcheck.net/) у `PATH` — без нього `actionlint` мовчки пропускає shell-перевірки в `run:` блоках, тож локальний прогін зеленіє, а CI на `ubuntu-latest` (де shellcheck передвстановлений) падає на тих самих workflow. Встановлення: `brew install shellcheck` (macOS), `sudo apt-get install -y shellcheck` (Debian/Ubuntu), `sudo pacman -S shellcheck` (Arch).
270
- 2. Preflight на [`uv`](https://docs.astral.sh/uv/) у `PATH` — постачає `uvx`, без якого не запуститься `uvx zizmor`. Встановлення: `brew install uv` (macOS), `curl -LsSf https://astral.sh/uv/install.sh | sh` (universal), `pip install uv`.
271
- 3. Якщо хоча б один preflight не пройшов — exit 1 (підказки для всіх відсутніх залежностей друкуються разом, а не лише для першої).
272
- 4. `bunx github-actionlint`.
273
- 5. `uvx zizmor --offline --collect=workflows .`.
253
+ CLI робить preflight на `shellcheck` і `uv` (`uvx`) у `PATH`, потім запускає `bunx github-actionlint` і `uvx zizmor --offline --collect=workflows .`.
274
254
 
275
255
  **`.github/zizmor.yml`:** для [unpinned-uses](https://docs.zizmor.sh/audits/#unpinned-uses) — політика **`ref-pin`**, якщо в `uses:` семантичні теги. За потреби вимкни [template-injection](https://docs.zizmor.sh/audits/#template-injection):
276
256
 
@@ -288,8 +268,3 @@ rules:
288
268
  **MegaLinter:** не використовувати; прибрати workflow, конфіги (`.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`), залежності та згадки в CI / pre-commit / документації.
289
269
 
290
270
  **`depcheck`:** не використовувати у `.github/workflows/*.yml` — мігровано на `knip` (див. `js-lint.mdc`). Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `lint-js`, окремий крок `npx depcheck` у workflow не потрібен і блокується полісі `ga.workflow_common`.
291
-
292
- ## Перевірка
293
-
294
- - `bun run lint-ga`
295
- - `npx @nitra/cursor check ga`
@@ -10,10 +10,6 @@ alwaysApply: false
10
10
  - файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs));
11
11
  - у **`.vscode/extensions.json`** в масиві **`recommendations`** — запис **`graphql.vscode-graphql`**.
12
12
 
13
- Це забезпечує підсвітку та діагностику GraphQL в редакторі.
14
-
15
- Деталі виявлення `gql` у скриптах (у т.ч. лише `<script>` у SFC) — **`npm/scripts/check-graphql.mjs`** / **`npm/scripts/utils/graphql-gql-scan.mjs`**.
16
-
17
13
  ## `.graphqlrc.yml`
18
14
 
19
15
  Підстав свої шляхи до схеми та до файлів з операціями; приклад орієнтиру:
@@ -33,7 +29,3 @@ documents:
33
29
  "recommendations": ["graphql.vscode-graphql"]
34
30
  }
35
31
  ```
36
-
37
- ## Перевірка
38
-
39
- `npx @nitra/cursor check graphql`
@@ -26,9 +26,3 @@ HASURA_GRAPHQL_ENDPOINT=http://contract-h.ua-contract.svc.abie-ua.internal:8080
26
26
  Правило застосовується для проєктів **nitra** (у кореневому `package.json` `"repository": "https://github.com/nitra/*"`) і **abie** (`"repository": "https://github.com/abinbevefes/*"`); для інших репозиторіїв перевірка пропускається.
27
27
 
28
28
  Файл .env це (без імені) це виключення з цього правила, його змінювати не потрібно
29
-
30
- ## Перевірка
31
-
32
- `npx @nitra/cursor check hasura`
33
-
34
- Деталі алгоритму — у `check-hasura.mjs`.
@@ -7,7 +7,7 @@ alwaysApply: false
7
7
 
8
8
  AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image-avif` — у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить три кроки в порядку:
9
9
 
10
- 1. Запускає `npx @nitra/minify-image --src=. --write --avif` (≥ **3.3.1**) — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF. **Перегенерація при оновленні оригіналу:** з 3.3.1 CLI порівнює sha1 кожного raster-сорсу зі збереженим у `.n-minify-image.tsv` і автоматично перезаписує `<source>.avif`, якщо оригінал відредагували після останнього прогону. `@nitra/cursor` цю логіку не дублює — sha1-кеш живе всередині `@nitra/minify-image`.
10
+ 1. Запускає `npx @nitra/minify-image --src=. --write --avif` (≥ **3.3.1**) — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF. CLI порівнює sha1 кожного raster-сорсу зі збереженим у `.n-minify-image.tsv` і перезаписує `<source>.avif` при зміні оригіналу.
11
11
  2. Сканує `.vue` (а також `.html`) файли в кожному workspace-пакеті (root + workspaces) і автоматично переписує raster-посилання на AVIF-двійник у двох формах:
12
12
  - **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні);
13
13
  - **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
@@ -29,12 +29,6 @@ import welcomeImage from './assets/welcome.png.avif'
29
29
 
30
30
  AVIF-двійники **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від AVIF втрачається на чистому checkout-і).
31
31
 
32
- ## Коли НЕ вмикати правило
33
-
34
- AVIF ще не підтримується **усіма** браузерами: для публічного сайту, де серед користувачів можуть бути старі/нестандартні браузери, конвертація raster → AVIF як основного джерела ризикована. Для адмінок (де користувачі — співробітники з сучасними браузерами) AVIF безпечний.
35
-
36
- У монорепо з адмінкою + публічним сайтом стандартна стратегія така: правило `image-avif` присутнє у `.n-cursor.json`, але для пакета-сайту вмикається опт-аут (нижче).
37
-
38
32
  ## Опт-аут для конкретного пакета
39
33
 
40
34
  У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл або публічний сайт без гарантованої AVIF-підтримки), додай у `package.json` цього пакета:
@@ -48,9 +42,3 @@ AVIF ще не підтримується **усіма** браузерами:
48
42
  ```
49
43
 
50
44
  Тоді перевірка пропускає `.vue` файли цього пакета і не видаляє наявні `.avif` всередині як «сироти». У root-`package.json` опт-аут діє лише для файлів кореня (вкладені workspaces перевіряються незалежно — вмикай прапор у кожному пакеті, де треба).
51
-
52
- `image-compress` (раніший крок: lint-image, кеш, заборонені залежності) при цьому продовжує працювати — стиснення raster-зображень виконується незалежно від AVIF.
53
-
54
- ## Перевірка
55
-
56
- `npx @nitra/cursor check image-avif` (запуск AVIF-генерації + авто-заміна raster-посилань на `.avif` у `.vue`/`.html` кожного workspace-пакета + прибирання AVIF-сиріт; пакети з `"@nitra/minify-image": { "disable-avif": true }` пропускаються).