@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 +12 -3
- package/bin/n-cursor.js +8 -1
- package/package.json +1 -1
- package/scripts/auto-skills.mjs +92 -51
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:
|
|
273
|
+
detectedRules: effectiveRulesForSkills,
|
|
267
274
|
disableSkills
|
|
268
275
|
})
|
|
269
276
|
|
package/package.json
CHANGED
package/scripts/auto-skills.mjs
CHANGED
|
@@ -1,81 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Автовизначення skills для `.n-cursor.json` за умовами
|
|
2
|
+
* Автовизначення skills для `.n-cursor.json` за умовами зі `npm/skills/<skill>/auto.md`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* умови, які вже формалізовані для відповідного правила. Наприклад:
|
|
4
|
+
* `auto.md` — джерело правди (а не hardcoded мапа). Підтримуються три варіанти:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
*
|
|
27
|
-
* Ключ варто автододати, коли всі правила-залежності вже додані до конфігу автодетектом.
|
|
26
|
+
* @typedef {{ always: true } | { rules: readonly string[] }} SkillAutoSpec
|
|
28
27
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
/**
|
|
38
|
-
|
|
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 у стабільному порядку
|
|
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,
|
|
75
|
-
if (
|
|
76
|
-
|
|
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.
|
|
121
|
+
return { skills: AUTO_SKILL_ORDER.filter(id => detected.has(id)) }
|
|
81
122
|
}
|