@nitra/cursor 1.11.1 → 1.11.3

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,18 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.11.3] - 2026-05-15
8
+
9
+ ### Fixed
10
+
11
+ - **`npm/bin/n-cursor.js`** — `detectAutoSkills` тепер отримує **ефективний** список правил (опт-ін вручну з `.n-cursor.json:rules` ∪ auto-detected, мінус `disable-rules`), а не лише auto-detected. Без цього скіли із залежністю на правило, додане вручну (наприклад, `adr` без `auto.md`-умови), не активувалися — у репо з `"rules": ["adr", …]` скіл `adr-normalize` залишався відсутнім, попри `[adr]` у його `skills/adr-normalize/auto.md`. Тепер `adr-normalize`, `abie-clean`, `abie-kustomize`, `taze` авто-додаються коректно як при auto-detected, так і при manual-opt-in відповідних правил.
12
+
13
+ ## [1.11.2] - 2026-05-15
14
+
15
+ ### Fixed
16
+
17
+ - **`npm/scripts/auto-skills.mjs`** — джерело правди для автоактивації скілів тепер `skills/<skill>/auto.md`, а не hardcoded мапа в JS. Парсер розпізнає три формати: `завжди` (always-on), `[rule, rule, …]` (умова на правила), відсутній/нерозпізнаний файл (opt-in). Експортовані константи `AUTO_SKILL_ORDER` та `AUTO_SKILL_RULE_DEPENDENCIES` тепер похідні від сканування `npm/skills/` під час завантаження модуля (зберігаються для зворотної сумісності). Побічно виправлено пропуск `abie-clean` у hardcoded мапі попри `[abie]` у його `auto.md` — тепер скіл коректно автоактивується разом з правилом `abie`.
18
+
7
19
  ## [1.11.1] - 2026-05-15
8
20
 
9
21
  ### Fixed
@@ -45,9 +57,6 @@
45
57
  - **Walk-glob правила** (6): `js-mssql`, `js-bun-db`, `js-bun-redis`, `js-run` (package_json + configmap), `vue`, `image-avif` — `walkGlob: "**/package.json"` або відповідний патерн.
46
58
  - **k8s.* концерни** (8): `manifest`, `gateway`, `hpa_pdb`, `kustomization`, `svc_yaml`, `svc_hl_yaml`, `base_kustomization`, `base_manifest` — `walkGlob` по YAML під сегментом `k8s/`; `base_manifest` використовує негативний glob для виключення `kustomization.yaml`.
47
59
  - **abie концерни** (4): `clean_merged_ignore_branches` (single), `health_check_policy` (walkGlob `**/k8s/**/hc.yaml`), `http_route_base` (walkGlob `**/k8s/**/base/**/hr.yaml`), `base_deployment_preem` (walkGlob `**/k8s/**/base/**/*.{yaml,yml}` з виключенням `kustomization.yaml`).
48
-
49
- ### Changed
50
-
51
60
  - **`capture-decisions.sh` тепер пише чернетки напряму в `docs/adr/<timestamp>-<sid>.md`** (раніше — у `docs/adr/_inbox/`). Сам каталог `_inbox/` більше не створюється, але `normalize-decisions.sh` бачить його рекурсивно — старі чернетки з `_inbox/` поступово розчищаються нормалізацією. Можна також одноразово `git mv docs/adr/_inbox/*.md docs/adr/` і прибрати порожній каталог.
52
61
  - **Правило `adr` (`npm/rules/adr/adr.mdc`)**: повне переписування під дві фази (capture + normalize). Видалено згадки `_inbox/`. Версія `version: '2.0'`.
53
62
  - **`npm/rules/adr/js/check.mjs`**: перевірка обох hook-скриптів (canonicity), обох log-файлів у `.gitignore`.
package/bin/n-cursor.js CHANGED
@@ -261,9 +261,16 @@ async function readConfig(paths = {}) {
261
261
  packageJsonParsed: rootPkg,
262
262
  disableRules
263
263
  })
264
+ // Skills залежать від ефективного списку правил, який буде у конфізі після merge:
265
+ // вже існуючі (опт-ін вручну) + auto-detected, мінус `disable-rules`. Без цього
266
+ // правило, додане вручну (напр. `adr` без auto.md-умови), не активувало б залежні
267
+ // скіли (`adr-normalize`).
268
+ const disableRulesSet = new Set(disableRules)
269
+ const effectiveRulesForSkills = [...new Set([...normalizeIdList(parsedConfig.rules), ...autoDetectedRules.rules])]
270
+ .filter(id => !disableRulesSet.has(id))
264
271
  const autoDetectedSkills = detectAutoSkills({
265
272
  availableSkills,
266
- detectedRules: autoDetectedRules.rules,
273
+ detectedRules: effectiveRulesForSkills,
267
274
  disableSkills
268
275
  })
269
276
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.11.1",
3
+ "version": "1.11.3",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,81 +1,122 @@
1
1
  /**
2
- * Автовизначення skills для `.n-cursor.json` за умовами з `npm/skills/<skill>/auto.md`.
2
+ * Автовизначення skills для `.n-cursor.json` за умовами зі `npm/skills/<skill>/auto.md`.
3
3
  *
4
- * Скіли автододаються залежно від уже виявлених правил (auto-rules) щоб не дублювати
5
- * умови, які вже формалізовані для відповідного правила. Наприклад:
4
+ * `auto.md` джерело правди не hardcoded мапа). Підтримуються три варіанти:
6
5
  *
7
- * - `abie-kustomize - [abie]` додається разом з правилом `abie`
8
- * - `taze - [bun]` — додається разом з правилом `bun`
6
+ * - `завжди` скіл активується незалежно від інших правил
7
+ * (приклади: `fix`, `lint`, `llm-patch`, `publish-telegram`).
8
+ * - `[rule, rule, …]` — скіл активується, якщо ВСІ перелічені правила вже виявлені
9
+ * auto-rules (приклади: `abie-clean - [abie]`, `taze - [bun]`).
10
+ * - файл відсутній або формат не розпізнано — скіл opt-in лише через `.n-cursor.json:skills`.
9
11
  *
10
- * Скіли без секції `[rules]` у `skills/<skill>/auto.md` (`fix`, `lint`, `llm-patch`, `publish-telegram`)
11
- * додаються завжди, якщо доступні в пакеті й не у `disable-skills`.
12
+ * Сканування `npm/skills/` sync під час завантаження модуля (детермінізм + sync API
13
+ * `auto-rules.mjs`-сусіда). Кеш на час процесу.
12
14
  */
15
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
16
+ import { dirname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
13
18
 
14
- /** Порядок автододавання skills відповідно до `skills/<skill>/auto.md`. */
15
- export const AUTO_SKILL_ORDER = Object.freeze([
16
- 'abie-kustomize',
17
- 'adr-normalize',
18
- 'fix',
19
- 'lint',
20
- 'llm-patch',
21
- 'publish-telegram',
22
- 'taze'
23
- ])
19
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
20
+ const SKILLS_DIR = join(PACKAGE_ROOT, 'skills')
21
+
22
+ const ALWAYS_LITERAL = 'завжди'
23
+ const BRACKET_LIST_RE = /^\[([^\]]+)\]$/u
24
24
 
25
25
  /**
26
- * Залежність скілів від правил (`skills/<skill>/auto.md` синтаксис `skill - [rules]`).
27
- * Ключ варто автододати, коли всі правила-залежності вже додані до конфігу автодетектом.
26
+ * @typedef {{ always: true } | { rules: readonly string[] }} SkillAutoSpec
28
27
  */
29
- export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
30
- /** @type {Record<string, readonly string[]>} */ ({
31
- 'abie-kustomize': Object.freeze(['abie']),
32
- 'adr-normalize': Object.freeze(['adr']),
33
- taze: Object.freeze(['bun'])
34
- })
28
+
29
+ /**
30
+ * Парсить тіло `auto.md` одного скіла.
31
+ * @param {string} text вміст файла (без `trim`)
32
+ * @returns {SkillAutoSpec | null} `null` — формат не розпізнано (= opt-in)
33
+ */
34
+ function parseSkillAutoSpec(text) {
35
+ const trimmed = text.trim()
36
+ if (trimmed === ALWAYS_LITERAL) {
37
+ return { always: true }
38
+ }
39
+ const m = trimmed.match(BRACKET_LIST_RE)
40
+ if (m) {
41
+ const rules = m[1]
42
+ .split(',')
43
+ .map(s => s.trim())
44
+ .filter(s => s.length > 0)
45
+ if (rules.length === 0) return null
46
+ return { rules: Object.freeze(rules) }
47
+ }
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Сканує `npm/skills/<id>/auto.md`. Скіли без `auto.md` або з нерозпізнаним
53
+ * вмістом не потрапляють у результат — їх можна вмикати лише вручну в конфізі.
54
+ * @param {string} [skillsDir] override для тестів
55
+ * @returns {Record<string, SkillAutoSpec>}
56
+ */
57
+ export function discoverSkillAutoActivation(skillsDir = SKILLS_DIR) {
58
+ if (!existsSync(skillsDir)) return {}
59
+ /** @type {Record<string, SkillAutoSpec>} */
60
+ const out = {}
61
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
62
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue
63
+ const autoMdPath = join(skillsDir, entry.name, 'auto.md')
64
+ if (!existsSync(autoMdPath)) continue
65
+ const spec = parseSkillAutoSpec(readFileSync(autoMdPath, 'utf8'))
66
+ if (spec) out[entry.name] = spec
67
+ }
68
+ return out
69
+ }
70
+
71
+ /** Cache на час процесу: один скан `npm/skills/` дає всю автоактивацію. */
72
+ const SKILL_AUTO_ACTIVATION = discoverSkillAutoActivation()
73
+
74
+ /**
75
+ * Стабільний алфавітний порядок скілів з автоактивацією. Експортовано для зворотної
76
+ * сумісності (попередня версія мала жорстко прописаний `AUTO_SKILL_ORDER`).
77
+ */
78
+ export const AUTO_SKILL_ORDER = Object.freeze(
79
+ Object.keys(SKILL_AUTO_ACTIVATION).toSorted((a, b) => a.localeCompare(b))
35
80
  )
36
81
 
37
- /** Скіли без залежностей — додаються завжди (рядок «завжди» в `skills/<skill>/auto.md`). */
38
- const ALWAYS_ON_SKILLS = Object.freeze(['fix', 'lint', 'llm-patch', 'publish-telegram'])
82
+ /**
83
+ * Похідна view на `SKILL_AUTO_ACTIVATION`: лише скіли з rule-залежностями.
84
+ * Експортовано для зворотної сумісності та автодоку.
85
+ */
86
+ export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
87
+ Object.fromEntries(
88
+ Object.entries(SKILL_AUTO_ACTIVATION)
89
+ .filter(([, spec]) => 'rules' in spec)
90
+ .map(([id, spec]) => [id, /** @type {{ rules: readonly string[] }} */ (spec).rules])
91
+ )
92
+ )
39
93
 
40
94
  const DEFAULT_DISABLED_LIST = Object.freeze([])
41
95
 
42
96
  /**
43
- * Визначає авто-skills згідно з `skills/<skill>/auto.md`.
97
+ * Визначає авто-skills згідно з вмістом `skills/<skill>/auto.md`.
44
98
  * @param {object} params параметри
45
99
  * @param {string[]} params.availableSkills перелік доступних skills із пакету (id без префікса n-)
46
100
  * @param {string[]} params.detectedRules id правил, виявлених auto-rules (вхідні залежності)
47
101
  * @param {string[]} [params.disableSkills] список `disable-skills` з конфігу
48
- * @returns {{ skills: string[] }} список id у стабільному порядку (за `AUTO_SKILL_ORDER`)
102
+ * @returns {{ skills: string[] }} список id у стабільному алфавітному порядку
49
103
  */
50
104
  export function detectAutoSkills({ availableSkills, detectedRules, disableSkills = DEFAULT_DISABLED_LIST }) {
51
105
  const normalizedSkills = new Set(availableSkills.map(s => s.trim().toLowerCase()))
52
106
  const disableSkillsSet = new Set(disableSkills)
53
107
  const detectedRulesSet = new Set(detectedRules)
54
108
 
55
- /** @type {string[]} */
56
- const detected = []
57
-
58
- /**
59
- * Додає skill до результату, якщо він доступний і не в disable-списку.
60
- * @param {string} skillId id skill
61
- * @returns {void}
62
- */
63
- function addSkill(skillId) {
64
- if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId) || detected.includes(skillId)) {
65
- return
66
- }
67
- detected.push(skillId)
68
- }
69
-
70
- for (const skillId of ALWAYS_ON_SKILLS) {
71
- addSkill(skillId)
72
- }
109
+ /** @type {Set<string>} */
110
+ const detected = new Set()
73
111
 
74
- for (const [skillId, deps] of Object.entries(AUTO_SKILL_RULE_DEPENDENCIES)) {
75
- if (deps.every(d => detectedRulesSet.has(d))) {
76
- addSkill(skillId)
112
+ for (const [skillId, spec] of Object.entries(SKILL_AUTO_ACTIVATION)) {
113
+ if (!normalizedSkills.has(skillId) || disableSkillsSet.has(skillId)) continue
114
+ if ('always' in spec) {
115
+ detected.add(skillId)
116
+ } else if (spec.rules.every(d => detectedRulesSet.has(d))) {
117
+ detected.add(skillId)
77
118
  }
78
119
  }
79
120
 
80
- return { skills: AUTO_SKILL_ORDER.filter(id => detected.includes(id)) }
121
+ return { skills: AUTO_SKILL_ORDER.filter(id => detected.has(id)) }
81
122
  }