@nitra/cursor 1.13.51 → 1.13.52

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/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.52] - 2026-05-19
8
+
9
+ ### Added
10
+
11
+ - `check bun`: **зворотній інваріант** для `lint-<id>`-скриптів. Раніше `checkCursorRuleScripts` ([npm/rules/bun/fix/layout/check.mjs](rules/bun/fix/layout/check.mjs)) перевіряв лише пряму імплікацію — «правило в `.n-cursor.json:rules` → скрипт у `package.json`». Тепер також fail-имо, коли правило **відсутнє** в `rules` (або явно перенесене в **`disable-rules`**), але в кореневому `package.json` залишилися: (а) сам скрипт `lint-<id>`, або (б) виклик `bun run lint-<id>` у агрегованому `scripts.lint`. Причина: `n-cursor lint-<id>` запускається напряму й **ігнорує** `.n-cursor.json`, тож `bun run lint` падає на вимкненому правилі (як було з `disable-rules: ["k8s"]` у cursor-репо, де `lint-k8s` обходив template-сорці власного правила). Покриті скрипти і їхні правила-власники: `lint-docker` ← `docker`, `lint-k8s` ← `k8s`, `lint-image` ← `image-avif`/`image-compress` (multi-owner — скрипт лишається дозволеним, поки активний **хоч один** власник). Розпізнавання згадки `bun run lint-<id>` у chain'і — через токен-границі (regex `\\bbun run <script>\\b`), щоб не матчити префікси (`lint-k8s-foo` ≠ `lint-k8s`). Bump `bun.mdc` `1.8` → `1.9`.
12
+
7
13
  ## [1.13.51] - 2026-05-19
8
14
 
9
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.51",
3
+ "version": "1.13.52",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/bun/bun.mdc CHANGED
@@ -2,7 +2,7 @@
2
2
  description: Bun як єдиний package manager у монорепо
3
3
  globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"
4
4
  alwaysApply: false
5
- version: '1.8'
5
+ version: '1.9'
6
6
  ---
7
7
 
8
8
  Проект використовує тільки Bun для керування залежностями та запуску скриптів.
@@ -70,4 +70,6 @@ FROM oven/bun:alpine AS build-env
70
70
 
71
71
  У кінці скрипта `lint` додай `&& oxfmt .`.
72
72
 
73
- Якщо в **`.n-cursor.json`** у масиві **`rules`** є **`docker`**, у кореневому `package.json` **обов'язково** скрипт **`lint-docker`** (див. **`docker.mdc`**) і рядок **`bun run lint-docker`** у **`lint`**. Якщо є **`k8s`** — **обов'язково** **`lint-k8s`** і **`bun run lint-k8s`** у **`lint`** (див. **`k8s.mdc`**). Перевірка — **`npx @nitra/cursor check bun`**.
73
+ Якщо в **`.n-cursor.json`** у масиві **`rules`** є **`docker`**, у кореневому `package.json` **обов'язково** скрипт **`lint-docker`** (див. **`docker.mdc`**) і рядок **`bun run lint-docker`** у **`lint`**. Якщо є **`k8s`** — **обов'язково** **`lint-k8s`** і **`bun run lint-k8s`** у **`lint`** (див. **`k8s.mdc`**).
74
+
75
+ **Зворотній інваріант:** якщо правила **немає** в `rules` (або воно явно перенесене в **`disable-rules`**), скрипту **`lint-<id>`** у кореневому `package.json` бути **не може**, і ланцюжок агрегованого **`scripts.lint`** не має містити **`bun run lint-<id>`**. Інакше `bun run lint` падатиме на вимкненому правилі — `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево незалежно від `rules`/`disable-rules`. Для скриптів із кількома власниками (як **`lint-image`** — обслуговує і **`image-avif`**, і **`image-compress`**) скрипт лишається дозволеним, поки активний **хоч один** власник; зворотній інваріант тригериться лише коли в `rules` немає **жодного** з них. Перевірка — **`npx @nitra/cursor check bun`**.
@@ -5,9 +5,11 @@
5
5
  * - наявність `bun.lock`, `bunfig.toml`, `package.json` у корені (FS-existence);
6
6
  * - заборонені lockfile та артефакти yarn/pnpm (`package-lock.json`, `yarn.lock`,
7
7
  * `pnpm-lock.yaml`, `.yarnrc.yml`, директорія `.yarn/`);
8
- * - якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, у кореневому
9
- * `package.json` має бути відповідний скрипт `lint-docker` / `lint-k8s`
10
- * (cross-file: два JSON-файли).
8
+ * - двосторонній зв'язок `.n-cursor.json:rules` `package.json:scripts` для правил із
9
+ * `lint-<id>` (`docker`, `k8s`): rule увімкнено скрипт мусить існувати; rule
10
+ * відсутнє (або в `disable-rules`) скрипту та згадки `bun run lint-<id>` у
11
+ * агрегованому `scripts.lint` бути **не може** (інакше `bun run lint` падатиме
12
+ * на правилі, яке у конфізі вимкнено).
11
13
  *
12
14
  * **Що покрила Rego** (`npx \@nitra/cursor check`):
13
15
  * - `npm/policy/bun/bunfig/` — `[install].linker == "hoisted"` у `bunfig.toml`;
@@ -25,46 +27,116 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
25
27
  // видалено, щоб не було двох джерел істини.
26
28
 
27
29
  /**
28
- * Зчитує ідентифікатори правил з `.n-cursor.json` (поле `rules`).
29
- * @returns {Promise<Set<string>>} множина рядків id правил або порожня, якщо файлу/поля немає
30
+ * Зчитує `rules` та `disable-rules` з `.n-cursor.json`.
31
+ * @returns {Promise<{ rules: Set<string>, disabled: Set<string> }>} активні правила і явно вимкнені
30
32
  */
31
33
  async function loadNCursorRules() {
32
- if (!existsSync('.n-cursor.json')) {
33
- return new Set()
34
- }
34
+ const empty = { rules: new Set(), disabled: new Set() }
35
+ if (!existsSync('.n-cursor.json')) return empty
35
36
  try {
36
37
  const raw = JSON.parse(await readFile('.n-cursor.json', 'utf8'))
37
- const list = raw?.rules
38
- if (!Array.isArray(list)) {
39
- return new Set()
40
- }
41
- return new Set(list.map(String))
38
+ const list = Array.isArray(raw?.rules) ? raw.rules.map(String) : []
39
+ const disabled = Array.isArray(raw?.['disable-rules']) ? raw['disable-rules'].map(String) : []
40
+ return { rules: new Set(list), disabled: new Set(disabled) }
42
41
  } catch {
43
- return new Set()
42
+ return empty
44
43
  }
45
44
  }
46
45
 
47
46
  /**
48
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
47
+ * Чи містить `scripts.lint` виклик `bun run <script>` у chain'і. Шукаємо саме `bun run <script>`
48
+ * як окремий токен (між пробілами/`&&`), щоб уникнути false-positive на префіксах
49
+ * (`bun run lint-k8s-foo` не матчиться як `bun run lint-k8s`).
50
+ * @param {string} lintScript значення `scripts.lint` (порожній рядок — якщо нема)
51
+ * @param {string} target ім'я скрипта (без префіксів)
52
+ * @returns {boolean} true, якщо chain згадує `bun run <target>`
53
+ */
54
+ function lintChainHasScript(lintScript, target) {
55
+ if (!lintScript) return false
56
+ const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')
57
+ return new RegExp(`(?:^|\\s)bun\\s+run\\s+${escaped}(?:$|\\s)`, 'u').test(lintScript)
58
+ }
59
+
60
+ /**
61
+ * Описує `lint-<id>`-обгортку та правила, що нею володіють. Один скрипт може мати кілька
62
+ * власників (`lint-image` — обслуговує і `image-avif`, і `image-compress`); скрипт вважається
63
+ * «потрібним», якщо **хоч одне** з власних правил активне у `.n-cursor.json:rules`.
64
+ * @typedef {object} RuleScript
65
+ * @property {string[]} rules id правил-власників (>=1); скрипт зобов'язаний існувати, поки активне хоч одне з них
66
+ * @property {string} script ім'я скрипта в `package.json:scripts`
67
+ * @property {string} doc `.mdc`-файл (або кома-список), на який посилається повідомлення check-у
68
+ */
69
+
70
+ /** @type {RuleScript[]} */
71
+ const RULE_SCRIPTS = [
72
+ { rules: ['docker'], script: 'lint-docker', doc: 'docker.mdc' },
73
+ { rules: ['k8s'], script: 'lint-k8s', doc: 'k8s.mdc' },
74
+ { rules: ['image-avif', 'image-compress'], script: 'lint-image', doc: 'image-avif.mdc / image-compress.mdc' }
75
+ ]
76
+
77
+ /**
78
+ * Описує стан правил-власників скрипта для повідомлень про reason. Повертає або список увімкнених
79
+ * правил (для passing-кейсу «правило є»), або компактний опис, чому всі вимкнені (для inverse-fail).
80
+ * @param {string[]} owners id правил-власників (>=1)
81
+ * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
82
+ * @returns {{ enabled: string[], reason: string }} `enabled` — список з `cursorRules.rules`; `reason` — текст для лога
83
+ */
84
+ function ownerStatus(owners, cursorRules) {
85
+ const enabled = owners.filter(r => cursorRules.rules.has(r))
86
+ if (enabled.length > 0) {
87
+ return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${enabled.map(r => `\`${r}\``).join(', ')}` }
88
+ }
89
+ if (owners.length === 1) {
90
+ const [only] = owners
91
+ const where = cursorRules.disabled.has(only) ? 'в disable-rules' : 'відсутнє в rules'
92
+ return { enabled, reason: `правило \`${only}\` ${where}` }
93
+ }
94
+ const disabledCount = owners.filter(r => cursorRules.disabled.has(r)).length
95
+ const note = disabledCount === owners.length ? 'усі власники в disable-rules' : 'жоден власник не активний у rules'
96
+ return { enabled, reason: `${owners.map(r => `\`${r}\``).join('/')} — ${note}` }
97
+ }
98
+
99
+ /**
100
+ * Перевіряє двосторонній зв'язок `rules` ↔ `scripts.lint-<id>` для правил із `lint-<id>`-обгорткою
101
+ * (див. `RULE_SCRIPTS`). Якщо активне хоч одне правило-власник — скрипт мусить існувати; якщо
102
+ * жодне з власників не активне (відсутнє у `rules` або є в `disable-rules`), скрипту і згадки
103
+ * `bun run <script>` у `scripts.lint` бути **не може**. Інакше `bun run lint` падатиме на
104
+ * вимкненому правилі: `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево
105
+ * незалежно від конфігу (як було в cursor-репо: `disable-rules: ["k8s"]` + залишений `lint-k8s`
106
+ * ламав chain на template-сорцях власного правила).
107
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter
49
108
  * @param {Record<string, string>} scripts scripts з package.json
50
- * @param {Set<string>} cursorRules активні правила з .n-cursor.json
109
+ * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
51
110
  */
52
111
  function checkCursorRuleScripts(reporter, scripts, cursorRules) {
53
112
  const { pass, fail } = reporter
54
- /** @type {Array<{rule: string, script: string, doc: string}>} */
55
- const ruleScripts = [
56
- { rule: 'docker', script: 'lint-docker', doc: 'docker.mdc' },
57
- { rule: 'k8s', script: 'lint-k8s', doc: 'k8s.mdc' }
58
- ]
59
- for (const { rule, script, doc } of ruleScripts) {
60
- if (cursorRules.has(rule)) {
61
- if (scripts[script]) {
62
- pass(`package.json: є \`${script}\` (правило ${rule} у .n-cursor.json)`)
113
+ const lintScript = typeof scripts.lint === 'string' ? scripts.lint : ''
114
+ for (const { rules: owners, script, doc } of RULE_SCRIPTS) {
115
+ const status = ownerStatus(owners, cursorRules)
116
+ const present = Boolean(scripts[script])
117
+ const inChain = lintChainHasScript(lintScript, script)
118
+ if (status.enabled.length > 0) {
119
+ if (present) {
120
+ pass(`package.json: є \`${script}\` (${status.reason} у .n-cursor.json)`)
63
121
  } else {
64
122
  fail(
65
- `У .n-cursor.json є правило \`${rule}\` — додай скрипт \`${script}\` у кореневий package.json (див. ${doc})`
123
+ `У .n-cursor.json увімкнено ${status.reason} — додай скрипт \`${script}\` у кореневий package.json (див. ${doc})`
66
124
  )
67
125
  }
126
+ continue
127
+ }
128
+ if (present) {
129
+ fail(
130
+ `У .n-cursor.json немає активних власників ${owners.map(r => `\`${r}\``).join('/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
131
+ )
132
+ }
133
+ if (inChain) {
134
+ fail(
135
+ `У \`scripts.lint\` є \`bun run ${script}\`, але серед \`${owners.join('/')}\` жоден не активний у .n-cursor.json — прибери з ланцюжка lint (див. ${doc})`
136
+ )
137
+ }
138
+ if (!present && !inChain) {
139
+ pass(`package.json: \`${script}\` відсутній (${status.reason})`)
68
140
  }
69
141
  }
70
142
  }