@nitra/cursor 1.13.0 → 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 +6 -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 +0 -18
- package/rules/capacitor/capacitor.mdc +1 -6
- 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 +0 -4
- package/rules/js-bun-redis/js-bun-redis.mdc +0 -4
- package/rules/js-lint/js-lint.mdc +0 -14
- package/rules/js-mssql/js-mssql.mdc +0 -2
- package/rules/js-run/js-run.mdc +0 -8
- 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 +1 -7
- package/rules/rego/rego.mdc +2 -38
- package/rules/style-lint/style-lint.mdc +0 -23
- package/rules/tauri/tauri.mdc +0 -4
- 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
|
-
|
|
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,12 @@
|
|
|
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
|
+
|
|
7
13
|
## [1.13.0] - 2026-05-17
|
|
8
14
|
|
|
9
15
|
### Changed
|
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
|
-
*
|
|
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
package/rules/abie/abie.mdc
CHANGED
|
@@ -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
|
|
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
|
-
##
|
|
137
|
+
## Stop-hook у `.cursor/hooks.json`
|
|
145
138
|
|
|
146
|
-
|
|
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
|
-
`
|
|
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
|
@@ -26,21 +26,8 @@ version: '1.8'
|
|
|
26
26
|
- `bunx <tool>`
|
|
27
27
|
- `npx <tool>`
|
|
28
28
|
|
|
29
|
-
- Для встановлення залежностей використовуй `bun i`.
|
|
30
|
-
- Для запуску скриптів використовуй `bun run <script>`.
|
|
31
|
-
- Для додавання залежностей:
|
|
32
|
-
- `bun add <pkg>`
|
|
33
|
-
- `bun add -d <pkg>` для devDependencies
|
|
34
|
-
- Для одноразових CLI-команд використовуй `bunx <tool>`.
|
|
35
|
-
|
|
36
29
|
**Не додавай** у `dependencies` / `devDependencies` пакети, які **використовуються лише як CLI** і їх достатньо викликати через **`bunx <pkg>`** (або **`npx`**, якщо в проєкті так прийнято): наприклад **oxlint**, **jscpd**, **eslint** у корені тощо. Виняток — пакет потрібен як **бібліотека** (імпорт у коді), peer для іншого пакета, або інструмент **не** покривається `bunx` у вашому CI.
|
|
37
30
|
|
|
38
|
-
Заборонено використовувати:
|
|
39
|
-
|
|
40
|
-
- `npm`
|
|
41
|
-
- `yarn`
|
|
42
|
-
- `pnpm`
|
|
43
|
-
|
|
44
31
|
Lockfile у репозиторії: `bun.lock`.
|
|
45
32
|
Не створювати `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`.
|
|
46
33
|
Видалити якщо вони є. Видалити .yarn та .yarnrc.yml якщо вони є.
|
|
@@ -62,7 +49,6 @@ linker = "hoisted"
|
|
|
62
49
|
|
|
63
50
|
Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn
|
|
64
51
|
|
|
65
|
-
Якщо в проекті використовується npx, то не замінювати його на bunx, а використовувати npx.
|
|
66
52
|
Коли зміна відбувається в Dockerfile, то використовувати
|
|
67
53
|
|
|
68
54
|
```dockerfile
|
|
@@ -81,10 +67,6 @@ FROM oven/bun:alpine AS build-env
|
|
|
81
67
|
|
|
82
68
|
Якщо в репозиторії action збережено під **`./npm/github-actions/setup-bun-deps`**, у `uses:` вкажи цей шлях замість `.github/actions/…` (**ga.mdc**).
|
|
83
69
|
|
|
84
|
-
## Перевірка
|
|
85
|
-
|
|
86
|
-
`npx @nitra/cursor check bun`
|
|
87
|
-
|
|
88
70
|
## lint
|
|
89
71
|
|
|
90
72
|
Якщо в кореневому @package.json існують скрипти з префіксом `lint-`, обов'язково створюй `lint` скрипт, який буде запускати всі ці скрипти з лінт-префіксом.
|
|
@@ -9,8 +9,7 @@ version: '1.1'
|
|
|
9
9
|
|
|
10
10
|
У `package.json` (у **корені** репозиторію чи **workspace**-пакеті) оголошення **`@capacitor/core`**
|
|
11
11
|
має вказувати діапазон, **сумісний лише з мажорною версією 8 і вище** (наприклад `^8.0.0`).
|
|
12
|
-
**`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні.
|
|
13
|
-
(репозиторій **@nitra/cursor**).
|
|
12
|
+
**`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні.
|
|
14
13
|
|
|
15
14
|
## iOS: зазвичай лише SPM, виняток Podfile
|
|
16
15
|
|
|
@@ -33,7 +32,3 @@ version: '1.1'
|
|
|
33
32
|
|
|
34
33
|
- Перевірка читає **лише** кореневі файли: **`package.json`**, потім **capacitor-конфіги** у **корені** (див. вище).
|
|
35
34
|
У **`.ts` / `.mjs`**: шукається блок **nitra** `{ ... }` і **на його тілі** перевіряються ці **boolean**-поля.
|
|
36
|
-
|
|
37
|
-
## Перевірка
|
|
38
|
-
|
|
39
|
-
`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`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій.
|
|
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`
|
package/rules/docker/docker.mdc
CHANGED
|
@@ -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`
|
package/rules/hasura/hasura.mdc
CHANGED
|
@@ -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.
|
|
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`
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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`
|
|
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`.
|
package/rules/js-run/js-run.mdc
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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`).
|
package/rules/rego/rego.mdc
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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`
|
package/rules/tauri/tauri.mdc
CHANGED
|
@@ -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,
|