@nitra/cursor 1.13.0 → 1.13.2

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.
@@ -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 }` пропускаються).
@@ -5,11 +5,9 @@ globs: "**/*.{png,jpg,jpeg,gif,svg}"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
- CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.3.1**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує окреме правило `image-avif` (`npx @nitra/cursor check image-avif`), яке заодно прибирає AVIF-сироти. Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
8
+ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.3.1**) запускається через `npx` і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує окреме правило `image-avif`.
9
9
 
10
- Мінімум `3.3.1` важливий для правила `image-avif`: починаючи з цієї версії, CLI порівнює sha1 raster-сорсу зі збереженим у `.n-minify-image.tsv` і **перегенеровує `<source>.avif`** при будь-якій зміні контенту оригіналу (раніше stale `.avif` лишався, поки розробник не видаляв його вручну).
11
-
12
- Перевірка лише локальна — у CI `lint-image` не запускаємо (sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону). Окремий workflow `lint-image.yml` створювати не треба.
10
+ Перевірка лише локальна у CI `lint-image` не запускаємо. Окремий workflow `lint-image.yml` створювати не треба.
13
11
 
14
12
  ## `package.json`
15
13
 
@@ -24,36 +22,12 @@ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (
24
22
 
25
23
  Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення.
26
24
 
27
- ## Split-cache
28
-
29
- Починаючи з `@nitra/minify-image` **3.2.0** кеш розбитий на два файли з різною семантикою:
30
-
31
- ### `.n-minify-image.tsv` — source of truth у git
32
-
33
- У корені сканованого каталогу. Формат: `<rel-path>\t<sha1-hex>\t<originalSize>\t<size>`.
34
-
35
- Slow-path і джерело даних для `Project lifetime savings`. **Має бути в git** — після `git clone` чи `git checkout` (mtime скидається на час checkout-у) CLI читає файл, рахує SHA-1 і порівнює зі збереженим у TSV хешем; на match локальний mtime-кеш зігрівається без reprocess. Рядки відсортовані алфавітно, hash і size змінюються лише при реальній зміні контенту — diff чистий.
36
-
37
- ### `node_modules/.cache/@nitra/minify-image/mtime.tsv` — локальний fast-path
38
-
39
- Формат: `<rel-path>\t<mtime>\t<size>`. При збігу `(size, mtime)` CLI пропускає файл без читання — константа per-file.
25
+ ## Кеш
40
26
 
41
- Лежить під `node_modules/`, тож **авто-gitignored** за конвенцією JS-tooling-у (так кешуються ESLint, Babel, webpack, Turbo). Окремий рядок у `.gitignore` не потрібен. `rm -rf node_modules` зносить — наступний запуск відновлює його через slow-path проти `.n-minify-image.tsv`, без reprocess.
42
-
43
- ### Міграція з versions < 3.2
44
-
45
- Старий єдиний `.minify-image-cache.tsv` (4 колонки `path\tmtime\toriginalSize\tsize`, зазвичай у `.gitignore`) автоматично читається при першому запуску для seed-у `originalSize` у `.n-minify-image.tsv` (lifetime savings не скидається). Після цього старий файл видаляють вручну:
46
-
47
- ```bash
48
- git rm --cached .minify-image-cache.tsv 2>/dev/null || true
49
- rm -f .minify-image-cache.tsv
50
- # прибери відповідний рядок з .gitignore, якщо був
51
- ```
27
+ - **`.n-minify-image.tsv`** у корені сканованого каталогу **має бути в git** (source of truth для sha1-перевірок і lifetime savings). У `.gitignore` його не додавай.
28
+ - **`node_modules/.cache/@nitra/minify-image/mtime.tsv`** — локальний fast-path; авто-gitignored через `node_modules/`.
29
+ - Застарілий `.minify-image-cache.tsv` у корені — видали (`git rm --cached`, прибери рядок з `.gitignore`).
52
30
 
53
31
  ## Заборонені залежності
54
32
 
55
- `@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write`).
56
-
57
- ## Перевірка
58
-
59
- `npx @nitra/cursor check image-compress` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write` і **забороненим** `--avif`; агрегований `lint`; заборону `@nitra/minify-image` у залежностях; `.n-minify-image.tsv` НЕ в `.gitignore` — має бути в git; відсутність застарілого `.minify-image-cache.tsv` у корені).
33
+ `@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` — CLI запускається лише через `npx`. Якщо потрібен явний пін — у самому виклику (`npx @nitra/minify-image@^3 --src=. --write`).
@@ -251,7 +251,3 @@ function getUser(id) {
251
251
  Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
252
252
 
253
253
  Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (див. `## pg-format: повне видалення, без шимів`).
254
-
255
- ## Перевірка
256
-
257
- `npx @nitra/cursor check js-bun-db`.
@@ -16,7 +16,3 @@ Redis 7.2+
16
16
  - Видалити з `dependencies`: `ioredis`, `node-redis`.
17
17
  - Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
18
18
  - Замінити на `import { redis } from 'bun'`
19
-
20
- ## Перевірка
21
-
22
- `npx @nitra/cursor check js-bun-redis`.
@@ -7,16 +7,6 @@ version: '1.22'
7
7
 
8
8
  **oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd/knip не додавай без потреби монорепо.
9
9
 
10
- ```json title=".vscode/extensions.json"
11
- {
12
- "recommendations": [
13
- "dbaeumer.vscode-eslint",
14
- "github.vscode-github-actions",
15
- "oxc.oxc-vscode"
16
- ]
17
- }
18
- ```
19
-
20
10
  У кожному **`package.json`** проєкту (корінь і всі workspace-пакети) має бути **`"type": "module"`** — весь код у ESM.
21
11
 
22
12
  ```json title="package.json"
@@ -190,7 +180,3 @@ for (const item of arr) {
190
180
  ## Тести
191
181
 
192
182
  Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
193
-
194
- ## Перевірка
195
-
196
- `npx @nitra/cursor check js-lint`
@@ -212,5 +212,3 @@ await pool.request().query`
212
212
  Допустимі парсери: `parseInt(...)`, `parseFloat(...)`, `Number(...)`, `BigInt(...)` або унарний `+x`. Літеральні масиви чисел (`[1, 2, 3]`) теж безпечні — без парсера, але без жодних рядків.
213
213
 
214
214
  Це правило діє і для безпечного `pool.request().query\`...\`` (де mssql сам параметризує масив), і поготів для `pool.query(String.raw\`...\`)` чи `pool.query(\`...\`)`, де такий парсинг — єдиний бар'єр.
215
-
216
- Перевірка: `npx @nitra/cursor check js-mssql`.
@@ -210,12 +210,4 @@ await setTimeout(500)
210
210
 
211
211
  Імпорт `setTimeout` з `node:timers/promises` затіняє глобальний таймер у файлі — якщо в тому ж файлі потрібен callback-варіант, імпортуй його під іншим іменем (наприклад, `import { setTimeout as setTimeoutCb } from 'node:timers'`).
212
212
 
213
- ## Перевірка
214
-
215
- `npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище. Додатково для файлів у каталозі `#conn/` (за замовчуванням `src/conn/`) перевіряється:
216
-
217
- - **basename файла** відповідає канону: `ql-<id>` (GraphQL) / `(pg|mysql|mssql)-(read|write)[-<id>]` (БД), kebab-case `[a-z0-9-]`;
218
- - **відсутній `export default`** — лише іменований експорт;
219
- - **імʼя експорту** дорівнює camelCase від basename (`pg-write-contract.js` → `export const pgWriteContract`).
220
-
221
213
  Файли `index.*` у conn-каталозі пропускаються як можливий reexport-барель.
package/rules/k8s/k8s.mdc CHANGED
@@ -27,12 +27,9 @@ alwaysApply: false
27
27
 
28
28
  ## lint-k8s: kubeconform і kubescape
29
29
 
30
- Окремо від modeline `$schema` у редакторі варто ганяти CLI-лінтери по тих самих дерев’ях **`…/k8s`**.
30
+ Окремо від modeline `$schema` у редакторі варто ганяти CLI-лінтери (**kubeconform** і **kubescape**) по тих самих дерев’ях **`…/k8s`**.
31
31
 
32
- - **[kubeconform](https://github.com/yannh/kubeconform#readme)** швидка валідація маніфестів проти OpenAPI-схем Kubernetes (аналог kubeval, з підтримкою власних реєстрів схем і CRD). Не покриває серверні перевірки кластера; для gap див. README проєкту ([`kubectl --dry-run=server`](https://github.com/yannh/kubeconform#readme) тощо).
33
- - **[kubescape](https://github.com/kubescape/kubescape#readme)** — сканування misconfiguration / compliance (NSA-CISA, MITRE ATT&CK, CIS тощо) по файлах, Helm, Kustomize або кластеру.
34
-
35
- **Залежності:** виконувані файли kubeconform і kubescape у **PATH**; не додавай їх у **devDependencies** npm (аналогія до `v8r` у `n-text.mdc`). Локально: наприклад `brew install kubeconform kubescape` або релізи з GitHub.
32
+ **Залежності:** виконувані файли kubeconform і kubescape у **PATH**; не додавай їх у **devDependencies**.
36
33
 
37
34
  **Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`check-k8s.mjs`** (зараз **`-kubernetes-version 1.33.9`** — semver без префікса `v`, еквівалент релізу **v1.33.9**; набір схем **`v1.33.9-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
38
35
 
@@ -645,18 +642,6 @@ patch: |-
645
642
  value: 2
646
643
  ```
647
644
 
648
- ## Перевірка
649
-
650
- **`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
651
-
652
- **Не входить у check k8s:** **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
653
-
654
- ## Коли застосовувати (агентам)
655
-
656
- - Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
657
- - Оновив **`apiVersion` / `kind`** — підправ **перший** рядок **`$schema`** (див. **Визначення схеми YAML**).
658
- - Дотримуйся **Kustomize** з цього правила; деталі **namespace** / графа ресурсів — **check k8s** + підказки в JSDoc скрипта.
659
-
660
645
  ## Визначення схеми YAML (канон)
661
646
 
662
647
  Орієнтир — **перший документ** (до наступного `---`).
@@ -702,7 +687,3 @@ patch: |-
702
687
  ## Багатодокументні YAML
703
688
 
704
689
  Одна схема на файл; скрипт звіряє **перший** документ. Інші `kind` у тому ж файлі — розділи файли або узгодь у рев’ю.
705
-
706
- ## Редактор
707
-
708
- Для `$schema` у VS Code / Cursor: **Red Hat YAML** (`redhat.vscode-yaml`) — за потреби в **`.vscode/extensions.json`**.
@@ -5,8 +5,6 @@ globs: "**/default.{conf.template,tpl.conf}"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
- > **Автоматична міграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
9
-
10
8
  default.conf.template повинен виглядати так:
11
9
 
12
10
  ```nginx
@@ -122,26 +120,3 @@ RUN NAMES=$(sed -nE '/^\s*[#;]/d; /^\s*$/d; s/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=.*
122
120
  ```
123
121
 
124
122
  Якщо у конфігураційних файлах *.ini є змінні які відсутні в default.conf.template, то їх потрібно вилучити.
125
-
126
- в файлі .vscode/extensions.json є налаштування для NGINX Configuration Language Support:
127
-
128
- ```json title=".vscode/extensions.json"
129
- {
130
- "recommendations": ["ahmadalli.vscode-nginx-conf"]
131
- }
132
- ```
133
-
134
- в файлі .vscode/settings.json є налаштування для NGINX Configuration Language Support:
135
-
136
- ```json title=".vscode/settings.json"
137
- {
138
- "editor.formatOnSave": true,
139
- "[nginx]": {
140
- "editor.defaultFormatter": "ahmadalli.vscode-nginx-conf"
141
- }
142
- }
143
- ```
144
-
145
- ## Перевірка
146
-
147
- `npx @nitra/cursor check nginx-default-tpl`
@@ -69,9 +69,7 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
69
69
 
70
70
  ## CHANGELOG
71
71
 
72
- Окреме правило **`changelog`** ([changelog.mdc](changelog.mdc)) вимагає `npm/CHANGELOG.md` із записом для поточної версії (Keep a Changelog) і присутність `"CHANGELOG.md"` у масиві `files` у `npm/package.json`. Логіка — PR-scoped (сума по гілці vs `dev`).
73
-
74
- Найновіша версія — **перша** секція **`## [version]`** у файлі (зверху після заголовка). Вона **має збігатися** з полем **`version`** у **`npm/package.json`** — це перевіряє **`npx @nitra/cursor check npm-module`**.
72
+ Найновіша версія **перша** секція **`## [version]`** у файлі (зверху після заголовка). Вона **має збігатися** з полем **`version`** у **`npm/package.json`**.
75
73
 
76
74
  ## npm publish
77
75
 
@@ -113,7 +111,3 @@ jobs:
113
111
  with:
114
112
  package: npm/package.json
115
113
  ```
116
-
117
- ## Перевірка
118
-
119
- `npx @nitra/cursor check npm-module` — зокрема узгодженість першої секції **`npm/CHANGELOG.md`** з **`version`** у **`npm/package.json`** і нагадування про bump при незакомічених змінах під **`npm/`** (через `git`).
@@ -9,49 +9,15 @@ alwaysApply: false
9
9
 
10
10
  Синтаксичні правила (`rego.v1`, `import rego.v1`, заборона legacy v0) — у `conftest.mdc` (alwaysApply). Цей файл — про **інструментарій**: VS Code, лінтери, форматування.
11
11
 
12
- ## VS Code
13
-
14
- Розширення `tsandall.opa` (від автора OPA): підсвічування, hover, go-to-definition, оцінка виразів і `format-on-save` через `opa fmt`. Працює лише за наявності `opa` у `PATH` — встановити нижче.
15
-
16
- ```json title=".vscode/extensions.json"
17
- {
18
- "recommendations": ["tsandall.opa"]
19
- }
20
- ```
21
-
22
- ```json title=".vscode/settings.json"
23
- {
24
- "[rego]": {
25
- "editor.defaultFormatter": "tsandall.opa",
26
- "editor.formatOnSave": true
27
- }
28
- }
29
- ```
30
-
31
- `opa.checkOnSave` за замовчуванням увімкнено в розширенні — діагностика від `opa check` показується в редакторі, тож синтаксичні/типові помилки видно одразу, без запуску `lint-rego`.
32
-
33
12
  ## Перевірка
34
13
 
35
14
  ```bash
36
15
  bun run lint-rego
37
16
  ```
38
17
 
39
- Скрипт делегує до CLI `n-cursor lint-rego` (бінарка з `node_modules/.bin/` пакету `@nitra/cursor`). Послідовність:
40
-
41
- 1. **preflight** — наявність `opa` і `regal` у `PATH`; якщо хоча б одного нема — exit 1 з підказкою встановлення;
42
- 2. `opa check --strict <targets>` — компіляція з типами та `--strict` (мертвий код, неоднозначні правила, незадекларовані змінні);
43
- 3. `regal lint <targets>` — статичний лінтер Rego ([Styra Regal](https://docs.styra.com/regal)): ловить v0-синтаксис, неявні set-rules і відхилення від `rego.v1`, плюс bugs/idiomatic/style-правила.
18
+ Цілі `npm/policy/`. Інші *.rego поза деревом додай у `LINT_TARGETS` у `npm/rules/rego/js/lint.mjs`.
44
19
 
45
- Цілі `npm/policy/` (де живуть Rego-полісі пакета). Інші *.rego поза деревом додай у `LINT_TARGETS` у `npm/rules/rego/js/lint.mjs`.
46
-
47
- ### Встановлення інструментів
48
-
49
- - macOS: `brew install opa regal`
50
- - Linux/Windows:
51
- - opa — <https://www.openpolicyagent.org/docs/latest/#1-download-opa>
52
- - regal — <https://docs.styra.com/regal#installation>
53
-
54
- Обидва — лише в `PATH`, **не** додавай у `dependencies` / `devDependencies` (як `shellcheck` у `text.mdc`).
20
+ `opa` і `regal` лише у `PATH`, **не** додавай у `dependencies` / `devDependencies`.
55
21
 
56
22
  ### `package.json`
57
23
 
@@ -63,8 +29,6 @@ bun run lint-rego
63
29
  }
64
30
  ```
65
31
 
66
- У кореневому `lint` (з `text.mdc` і дотичних) включай `bun run lint-rego` — щоб локальний прогін співпадав з CI.
67
-
68
32
  ## Конфіг regal
69
33
 
70
34
  У корені — `.regal/config.yaml`. Дозволено вимикати окремі правила під специфіку репо (наприклад, conftest-полісі — `deny`-правила як де-факто entrypoint-и):
@@ -12,25 +12,6 @@ alwaysApply: false
12
12
  - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний `bun run lint` (навичка **`/n-lint`**).
13
13
  - **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
14
14
 
15
- **VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
16
-
17
- ```json title=".vscode/extensions.json"
18
- {
19
- "recommendations": ["stylelint.vscode-stylelint"]
20
- }
21
- ```
22
-
23
- ```json title=".vscode/settings.json"
24
- {
25
- "css.validate": false,
26
- "less.validate": false,
27
- "scss.validate": false,
28
- "editor.codeActionsOnSave": {
29
- "source.fixAll": "explicit"
30
- }
31
- }
32
- ```
33
-
34
15
  **`package.json`:**
35
16
 
36
17
  ```json title="package.json"
@@ -94,7 +75,3 @@ jobs:
94
75
  ```text title=".stylelintignore"
95
76
  dist/
96
77
  ```
97
-
98
- ## Перевірка
99
-
100
- `npx @nitra/cursor check style-lint`
@@ -14,7 +14,3 @@ version: '1.1'
14
14
  "rust-lang.rust-analyzer"]
15
15
  }
16
16
  ```
17
-
18
- ## Перевірка
19
-
20
- `npx @nitra/cursor check tauri`
@@ -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,