@nitra/cursor 1.3.6 → 1.4.0
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/AGENTS.template.md +6 -0
- package/README.md +14 -14
- package/bin/n-cursor.js +684 -0
- package/mdc/bun.mdc +4 -0
- package/mdc/ga.mdc +4 -0
- package/mdc/js-format.mdc +4 -0
- package/mdc/js-lint.mdc +19 -1
- package/mdc/js-pino.mdc +7 -1
- package/mdc/nginx-default-tpl.mdc +4 -0
- package/mdc/npm-module.mdc +4 -0
- package/mdc/spell.mdc +5 -1
- package/mdc/style-lint.mdc +4 -0
- package/mdc/vue.mdc +4 -0
- package/package.json +9 -7
- package/scripts/check-bun.mjs +32 -0
- package/scripts/check-ga.mjs +60 -0
- package/scripts/check-js-format.mjs +80 -0
- package/scripts/check-js-lint.mjs +60 -0
- package/scripts/check-js-pino.mjs +39 -0
- package/scripts/check-nginx-default-tpl.mjs +59 -0
- package/scripts/check-npm-module.mjs +44 -0
- package/scripts/check-spell.mjs +57 -0
- package/scripts/check-style-lint.mjs +67 -0
- package/scripts/check-vue.mjs +72 -0
- package/skills/n-fix-cursor/SKILL.md +52 -0
- package/skills/n-publish-telegram/SKILL.md +93 -0
- package/bin/nitra-cursor.js +0 -301
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Перевіряє відповідність проєкту правилам vue.mdc
|
|
6
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
7
|
+
*/
|
|
8
|
+
export async function check() {
|
|
9
|
+
let exitCode = 0
|
|
10
|
+
const pass = msg => console.log(` ✅ ${msg}`)
|
|
11
|
+
const fail = msg => {
|
|
12
|
+
console.log(` ❌ ${msg}`)
|
|
13
|
+
exitCode = 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (existsSync('.vscode/extensions.json')) {
|
|
17
|
+
const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
18
|
+
ext.recommendations?.includes('Vue.volar')
|
|
19
|
+
? pass('extensions.json містить Vue.volar')
|
|
20
|
+
: fail('extensions.json не містить Vue.volar — додай до recommendations')
|
|
21
|
+
} else {
|
|
22
|
+
fail('.vscode/extensions.json не існує')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (existsSync('package.json')) {
|
|
26
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
27
|
+
const deps = pkg.dependencies || {}
|
|
28
|
+
const devDeps = pkg.devDependencies || {}
|
|
29
|
+
const allDeps = { ...deps, ...devDeps }
|
|
30
|
+
|
|
31
|
+
deps.vue ? pass(`vue в dependencies: ${deps.vue}`) : fail('vue відсутній в dependencies')
|
|
32
|
+
|
|
33
|
+
if (devDeps.vite) {
|
|
34
|
+
const match = devDeps.vite.match(/(\d+)/)
|
|
35
|
+
if (match && Number(match[1]) >= 8) {
|
|
36
|
+
pass(`vite >= 8: ${devDeps.vite}`)
|
|
37
|
+
} else {
|
|
38
|
+
fail(`vite має бути >= 8, знайдено: ${devDeps.vite}`)
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
fail('vite відсутній в devDependencies')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
devDeps['@vitejs/plugin-vue']
|
|
45
|
+
? pass(`@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
|
|
46
|
+
: fail('@vitejs/plugin-vue відсутній в devDependencies')
|
|
47
|
+
|
|
48
|
+
allDeps['vue-macros']
|
|
49
|
+
? pass(`vue-macros: ${allDeps['vue-macros']}`)
|
|
50
|
+
: fail('vue-macros відсутній — bun add -d vue-macros')
|
|
51
|
+
|
|
52
|
+
allDeps['unplugin-auto-import']
|
|
53
|
+
? pass('unplugin-auto-import присутній')
|
|
54
|
+
: fail('unplugin-auto-import відсутній — bun add -d unplugin-auto-import')
|
|
55
|
+
|
|
56
|
+
allDeps['vite-plugin-vue-layouts-next']
|
|
57
|
+
? pass('vite-plugin-vue-layouts-next присутній')
|
|
58
|
+
: fail('vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
62
|
+
const viteConfig = configFiles.find(f => existsSync(f))
|
|
63
|
+
if (viteConfig) {
|
|
64
|
+
const content = await readFile(viteConfig, 'utf8')
|
|
65
|
+
content.includes('VueMacros') ? pass('vite.config використовує VueMacros') : fail(`${viteConfig} не містить VueMacros`)
|
|
66
|
+
content.includes('AutoImport') ? pass('vite.config використовує AutoImport') : fail(`${viteConfig} не містить AutoImport`)
|
|
67
|
+
} else {
|
|
68
|
+
fail('vite.config.js не існує')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return exitCode
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-fix-cursor
|
|
3
|
+
description: >-
|
|
4
|
+
Виправити проєкт відповідно до всіх правил в .cursor/rules/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Fix Cursor — автоматичне виправлення проєкту
|
|
8
|
+
|
|
9
|
+
## Workflow
|
|
10
|
+
|
|
11
|
+
1. **Діагностика** — запусти перевірку (за замовчуванням лише правила з `AGENTS.md`, для яких є `check-*.mjs`; повний набір — явні аргументи: `npx @nitra/cursor check bun ga …`):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @nitra/cursor check
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. **Аналіз** — зчитай вивід, знайди всі `❌` та визнач які правила порушено
|
|
18
|
+
|
|
19
|
+
3. **Виправлення** — для кожного `❌` відкрий відповідне правило з `.cursor/rules/` і виправ:
|
|
20
|
+
- Створи відсутні конфігураційні файли (`.cspell.json`, `.oxfmtrc.json`, `eslint.config.js`, тощо)
|
|
21
|
+
- Додай відсутні залежності до `package.json`
|
|
22
|
+
- Створи або оновити `.vscode/settings.json` та `extensions.json`
|
|
23
|
+
- Створи відсутні GitHub Actions workflows у `.github/workflows/`
|
|
24
|
+
- Видали заборонені файли та залежності (`package-lock.json`, `yarn.lock`, prettier, тощо)
|
|
25
|
+
- Оновити скрипти в `package.json`
|
|
26
|
+
|
|
27
|
+
4. **Встановлення** — якщо були змінені залежності:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun i
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
5. **Форматування** — відформатуй змінені файли:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
oxfmt .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
6. **Лінтери** — знайди в кореневому `package.json` всі скрипти з префіксом `lint-` і запусти кожен:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun run lint-js
|
|
43
|
+
bun run lint-style
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
7. **Верифікація** — перевір що все виправлено:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @nitra/cursor check
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
8. **Результат** — всі `❌` мають стати `✅`. Якщо залишились `❌` — повтори кроки 3-7.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-publish-telegram
|
|
3
|
+
description: >-
|
|
4
|
+
Підготовка матеріалу з поточного контексту для публікації в Telegram-каналі команди
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Публікація в Telegram
|
|
8
|
+
|
|
9
|
+
Підготовка матеріалу для публікації в Telegram-каналі команди (розробники + менеджери).
|
|
10
|
+
|
|
11
|
+
## Формат Telegram
|
|
12
|
+
|
|
13
|
+
Telegram підтримує моноширний шрифт через markdown-розмітку:
|
|
14
|
+
|
|
15
|
+
- `` `inline code` `` — для inline
|
|
16
|
+
- ` ```pre``` ` (потрійні backticks) — для блоку коду / моноширного тексту
|
|
17
|
+
|
|
18
|
+
Весь пост оформлюй як **один блок** потрійних backticks, щоб текст був моноширним.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
1. **Визнач контекст** — проаналізуй поточну розмову: яка проблема вирішувалась, що було зроблено, який результат.
|
|
23
|
+
|
|
24
|
+
2. **Сформуй пост** за шаблоном нижче.
|
|
25
|
+
|
|
26
|
+
3. **Виведи готовий текст** — користувач копіює його в Telegram.
|
|
27
|
+
|
|
28
|
+
## Шаблон посту
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
#тег
|
|
32
|
+
|
|
33
|
+
📌 <Короткий заголовок>
|
|
34
|
+
|
|
35
|
+
Проблема:
|
|
36
|
+
<1-3 речення — що було не так / яке завдання>
|
|
37
|
+
|
|
38
|
+
Рішення:
|
|
39
|
+
<2-5 речень — що зробили, який підхід>
|
|
40
|
+
|
|
41
|
+
Деталі:
|
|
42
|
+
• <ключовий момент 1>
|
|
43
|
+
• <ключовий момент 2>
|
|
44
|
+
• <ключовий момент 3>
|
|
45
|
+
|
|
46
|
+
Результат:
|
|
47
|
+
<1-2 речення — що отримали, яка вигода>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#тег використовуються наступні:
|
|
51
|
+
якщо публікація стосовно AI, то #ai
|
|
52
|
+
якщо публікація стосовно npm модуля, то #npm
|
|
53
|
+
якщо публікація стосовно розробки, то #dev
|
|
54
|
+
якщо публікація стосовно інфраструктури, скриптів, налаштувань, ефективності коду чи безпеки то #sre
|
|
55
|
+
|
|
56
|
+
## Правила
|
|
57
|
+
|
|
58
|
+
- Мова: українська, технічні терміни — англійською
|
|
59
|
+
- Обсяг: 500–1500 символів (оптимально для Telegram)
|
|
60
|
+
- Без зайвих вступів і водянистих фраз
|
|
61
|
+
- Конкретика: назви файлів, бібліотек, команд — якщо доречно
|
|
62
|
+
- Теги: 2–4 хештеги в кінці (#devops, #frontend, #bugfix, #refactoring, #CI, #performance тощо)
|
|
63
|
+
- Emoji: лише структурні (📌 для заголовка, • для списку), не перевантажувати
|
|
64
|
+
- Цільова аудиторія: розробники та менеджери — тому пояснюй простою мовою, без надмірного жаргону
|
|
65
|
+
- Весь текст повинен бути всередині потрійних backticks для моноширного відображення в Telegram
|
|
66
|
+
|
|
67
|
+
## Приклад
|
|
68
|
+
|
|
69
|
+
````
|
|
70
|
+
```
|
|
71
|
+
📌 Міграція з Prettier на oxfmt
|
|
72
|
+
|
|
73
|
+
Проблема:
|
|
74
|
+
Prettier повільно форматував великі файли і
|
|
75
|
+
конфліктував з ESLint при роботі з Vue SFC.
|
|
76
|
+
|
|
77
|
+
Рішення:
|
|
78
|
+
Замінили Prettier на oxfmt — нативний
|
|
79
|
+
форматер від OXC. Оновили VS Code settings,
|
|
80
|
+
видалили prettier-конфіги, додали .oxfmtrc.json.
|
|
81
|
+
|
|
82
|
+
Деталі:
|
|
83
|
+
• oxfmt працює в 10-50x швидше за Prettier
|
|
84
|
+
• Єдиний конфіг .oxfmtrc.json у корені
|
|
85
|
+
• CI перевіряє форматування через lint-js
|
|
86
|
+
|
|
87
|
+
Результат:
|
|
88
|
+
Форматування працює миттєво при збереженні,
|
|
89
|
+
зникли конфлікти між форматером і лінтером.
|
|
90
|
+
|
|
91
|
+
#dx #tooling #oxfmt
|
|
92
|
+
```
|
|
93
|
+
````
|
package/bin/nitra-cursor.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* nitra-cursor — CLI завантаження правил
|
|
5
|
-
*
|
|
6
|
-
* Завантажує cursor-правила з npm-пакету nitra-cursor у локальний репозиторій.
|
|
7
|
-
*
|
|
8
|
-
* Використання:
|
|
9
|
-
* `npx @nitra/cursor`
|
|
10
|
-
*
|
|
11
|
-
* Якщо у корені репозиторію немає nitra-cursor.json, він створюється автоматично
|
|
12
|
-
* з усіма правилами з каталогу mdc пакету (їх можна відредагувати після створення).
|
|
13
|
-
*
|
|
14
|
-
* Файл AGENTS.md у корені: щоразу повністю перезаписується змістом з AGENTS.template.md
|
|
15
|
-
* пакету; список правил у шаблоні будується з файлів *.mdc у .cursor/rules поточного проєкту.
|
|
16
|
-
*
|
|
17
|
-
* Після завантаження: у .cursor/rules видаляються файли *.mdc з префіксом «nitra-» (керовані
|
|
18
|
-
* пакетом), яких немає у списку rules у nitra-cursor.json. Інші .mdc у цій директорії залишаються.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { existsSync } from 'node:fs'
|
|
22
|
-
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
23
|
-
import { basename, dirname, join } from 'node:path'
|
|
24
|
-
import { cwd } from 'node:process'
|
|
25
|
-
import { fileURLToPath } from 'node:url'
|
|
26
|
-
|
|
27
|
-
const PACKAGE_NAME = '@nitra/cursor'
|
|
28
|
-
const UNPKG_BASE = 'https://unpkg.com'
|
|
29
|
-
const CONFIG_FILE = 'nitra-cursor.json'
|
|
30
|
-
const AGENTS_FILE = 'AGENTS.md'
|
|
31
|
-
const AGENTS_TEMPLATE_FILE = 'AGENTS.template.md'
|
|
32
|
-
const RULES_DIR = '.cursor/rules'
|
|
33
|
-
const RULE_PREFIX = 'nitra-'
|
|
34
|
-
|
|
35
|
-
const binDir = dirname(fileURLToPath(import.meta.url))
|
|
36
|
-
const BUNDLED_MDC_DIR = join(binDir, '..', 'mdc')
|
|
37
|
-
const BUNDLED_AGENTS_TEMPLATE_PATH = join(binDir, '..', AGENTS_TEMPLATE_FILE)
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
|
|
41
|
-
* @returns {Promise<string[]>} відсортовані імена файлів правил без суфікса .mdc
|
|
42
|
-
*/
|
|
43
|
-
async function discoverBundledRuleNames() {
|
|
44
|
-
if (!existsSync(BUNDLED_MDC_DIR)) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Не знайдено каталог правил пакету.\n` +
|
|
47
|
-
`Очікуваний шлях: ${BUNDLED_MDC_DIR}\n` +
|
|
48
|
-
`Перевстановіть ${PACKAGE_NAME} або створіть ${CONFIG_FILE} вручну.`
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
const names = await readdir(BUNDLED_MDC_DIR)
|
|
52
|
-
const rules = names
|
|
53
|
-
.filter(n => n.endsWith('.mdc'))
|
|
54
|
-
.map(n => n.slice(0, -'.mdc'.length))
|
|
55
|
-
.sort((a, b) => a.localeCompare(b))
|
|
56
|
-
if (rules.length === 0) {
|
|
57
|
-
throw new Error(`У каталозі mdc пакету немає файлів .mdc. Створіть ${CONFIG_FILE} вручну.`)
|
|
58
|
-
}
|
|
59
|
-
return rules
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Завантажує текст з URL
|
|
64
|
-
* @param {string} url адреса HTTP(S)
|
|
65
|
-
* @returns {Promise<string>} тіло відповіді як UTF-8 текст
|
|
66
|
-
*/
|
|
67
|
-
async function fetchText(url) {
|
|
68
|
-
const response = await fetch(url)
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
throw new Error(`HTTP ${response.status} — не вдалося завантажити: ${url}`)
|
|
71
|
-
}
|
|
72
|
-
return response.text()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Зчитує конфіг nitra-cursor.json з поточної директорії
|
|
77
|
-
* @returns {Promise<{rules: string[], version?: string}>} об'єкт з масивом rules і опційно version; при відсутності файлу створює дефолтний конфіг
|
|
78
|
-
*/
|
|
79
|
-
async function readConfig() {
|
|
80
|
-
const configPath = join(cwd(), CONFIG_FILE)
|
|
81
|
-
if (!existsSync(configPath)) {
|
|
82
|
-
const rules = await discoverBundledRuleNames()
|
|
83
|
-
const defaultConfig = { rules }
|
|
84
|
-
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
|
|
85
|
-
console.log(
|
|
86
|
-
`📝 Створено ${CONFIG_FILE} з усіма правилами з пакету (${rules.length}). За потреби відредагуйте список.\n`
|
|
87
|
-
)
|
|
88
|
-
return defaultConfig
|
|
89
|
-
}
|
|
90
|
-
const raw = await readFile(configPath, 'utf8')
|
|
91
|
-
let config
|
|
92
|
-
try {
|
|
93
|
-
config = JSON.parse(raw)
|
|
94
|
-
} catch {
|
|
95
|
-
throw new Error(`Невірний JSON у файлі ${CONFIG_FILE}`)
|
|
96
|
-
}
|
|
97
|
-
if (!Array.isArray(config.rules) || config.rules.length === 0) {
|
|
98
|
-
throw new Error(`У ${CONFIG_FILE} має бути непорожній масив "rules"`)
|
|
99
|
-
}
|
|
100
|
-
return config
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Повертає URL для завантаження правила з unpkg
|
|
105
|
-
* @param {string} ruleName - ім'я без розширення, наприклад "js-format"
|
|
106
|
-
* @param {string} [version] - версія пакету (необов'язково, за замовчуванням "latest")
|
|
107
|
-
* @returns {string} повний URL файлу правила на unpkg
|
|
108
|
-
*/
|
|
109
|
-
function buildUrl(ruleName, version) {
|
|
110
|
-
const name = ruleName.endsWith('.mdc') ? ruleName : `${ruleName}.mdc`
|
|
111
|
-
const ver = version ? `@${version}` : '@latest'
|
|
112
|
-
return `${UNPKG_BASE}/${PACKAGE_NAME}${ver}/mdc/${name}`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Витягує чисте ім'я файлу правила (без шляху, але зберігає .mdc)
|
|
117
|
-
* "npm/mdc/js-format.mdc" → "js-format.mdc"
|
|
118
|
-
* "js-format" → "js-format.mdc"
|
|
119
|
-
* @param {string} ruleName шлях або базове ім'я, з суфіксом .mdc або без
|
|
120
|
-
* @returns {string} лише ім'я файлу з суфіксом .mdc
|
|
121
|
-
*/
|
|
122
|
-
function normalizeRuleName(ruleName) {
|
|
123
|
-
const name = ruleName.endsWith('.mdc') ? ruleName : `${ruleName}.mdc`
|
|
124
|
-
return basename(name)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Розгортає в шаблоні блок Mustache {{#section}} … {{/section}} для масиву елементів
|
|
129
|
-
* @param {string} template вихідний текст шаблону
|
|
130
|
-
* @param {string} section ім'я секції (наприклад services)
|
|
131
|
-
* @param {Record<string, string>[]} items елементи для повторення тіла секції
|
|
132
|
-
* @param {string} prop ключ поля для підстановки замість {{prop}}
|
|
133
|
-
* @returns {string} текст після розгортання усіх входжень блоку
|
|
134
|
-
*/
|
|
135
|
-
function expandMustacheSection(template, section, items, prop) {
|
|
136
|
-
const open = `{{#${section}}}`
|
|
137
|
-
const close = `{{/${section}}}`
|
|
138
|
-
const placeholder = `{{${prop}}}`
|
|
139
|
-
let result = template
|
|
140
|
-
let start = result.indexOf(open)
|
|
141
|
-
let end = result.indexOf(close)
|
|
142
|
-
while (start !== -1 && end !== -1 && end > start) {
|
|
143
|
-
const inner = result.slice(start + open.length, end)
|
|
144
|
-
const rendered = items.map(item => inner.split(placeholder).join(String(item[prop]))).join('')
|
|
145
|
-
result = result.slice(0, start) + rendered + result.slice(end + close.length)
|
|
146
|
-
start = result.indexOf(open)
|
|
147
|
-
end = result.indexOf(close)
|
|
148
|
-
}
|
|
149
|
-
return result
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Підставляє у вміст AGENTS.template.md список шляхів до файлів правил
|
|
154
|
-
* @param {string} templateText вміст AGENTS.template.md
|
|
155
|
-
* @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
|
|
156
|
-
* @returns {string} готовий markdown для AGENTS.md
|
|
157
|
-
*/
|
|
158
|
-
function renderAgentsTemplate(templateText, mdcBasenames) {
|
|
159
|
-
const items = mdcBasenames.map(mdcName => ({
|
|
160
|
-
name: `- ${RULES_DIR}/${mdcName}`
|
|
161
|
-
}))
|
|
162
|
-
return expandMustacheSection(templateText, 'services', items, 'name')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Повертає відсортовані імена *.mdc у .cursor/rules поточного проєкту
|
|
167
|
-
* @returns {Promise<string[]>} базові імена файлів (лише .mdc)
|
|
168
|
-
*/
|
|
169
|
-
async function listProjectRulesMdcFiles() {
|
|
170
|
-
const rulesDir = join(cwd(), RULES_DIR)
|
|
171
|
-
if (!existsSync(rulesDir)) {
|
|
172
|
-
return []
|
|
173
|
-
}
|
|
174
|
-
const names = await readdir(rulesDir)
|
|
175
|
-
return names.filter(n => n.endsWith('.mdc')).sort((a, b) => a.localeCompare(b))
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Базові імена файлів .mdc, які очікуються згідно з nitra-cursor.json (префікс nitra-).
|
|
180
|
-
* @param {string[]} configRules елементи масиву rules з конфігу
|
|
181
|
-
* @returns {Set<string>} множина очікуваних імен файлів (наприклад nitra-bun.mdc)
|
|
182
|
-
*/
|
|
183
|
-
function expectedManagedRuleBasenames(configRules) {
|
|
184
|
-
return new Set(configRules.map(rule => `${RULE_PREFIX}${normalizeRuleName(rule)}`))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Видаляє з каталогу правил файли *.mdc з префіксом nitra-, яких немає у конфігурації.
|
|
189
|
-
* Файли без префікса nitra- не змінює.
|
|
190
|
-
* @param {string} rulesDir абсолютний шлях до .cursor/rules
|
|
191
|
-
* @param {string[]} configRules елементи масиву rules з nitra-cursor.json
|
|
192
|
-
* @returns {Promise<string[]>} відсортовані імена видалених файлів
|
|
193
|
-
*/
|
|
194
|
-
async function removeOrphanManagedRuleFiles(rulesDir, configRules) {
|
|
195
|
-
if (!existsSync(rulesDir)) {
|
|
196
|
-
return []
|
|
197
|
-
}
|
|
198
|
-
const expected = expectedManagedRuleBasenames(configRules)
|
|
199
|
-
const names = await readdir(rulesDir)
|
|
200
|
-
const removed = []
|
|
201
|
-
for (const name of names) {
|
|
202
|
-
if (name.endsWith('.mdc') && name.startsWith(RULE_PREFIX) && !expected.has(name)) {
|
|
203
|
-
await unlink(join(rulesDir, name))
|
|
204
|
-
removed.push(name)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return removed.sort((a, b) => a.localeCompare(b))
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
|
|
212
|
-
* @returns {Promise<void>} завершення запису файлу
|
|
213
|
-
*/
|
|
214
|
-
async function syncAgentsMd() {
|
|
215
|
-
if (!existsSync(BUNDLED_AGENTS_TEMPLATE_PATH)) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
`Не знайдено шаблон ${AGENTS_TEMPLATE_FILE} у пакеті.\n` +
|
|
218
|
-
`Очікуваний шлях: ${BUNDLED_AGENTS_TEMPLATE_PATH}\n` +
|
|
219
|
-
`Перевстановіть ${PACKAGE_NAME}.`
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
|
-
const templateText = await readFile(BUNDLED_AGENTS_TEMPLATE_PATH, 'utf8')
|
|
223
|
-
const mdcFiles = await listProjectRulesMdcFiles()
|
|
224
|
-
const body = renderAgentsTemplate(templateText, mdcFiles)
|
|
225
|
-
const agentsPath = join(cwd(), AGENTS_FILE)
|
|
226
|
-
const hadFile = existsSync(agentsPath)
|
|
227
|
-
const out = body.endsWith('\n') ? body : `${body}\n`
|
|
228
|
-
await writeFile(agentsPath, out, 'utf8')
|
|
229
|
-
console.log(hadFile ? `📝 Оновлено ${AGENTS_FILE} з ${AGENTS_TEMPLATE_FILE}` : `📝 Створено ${AGENTS_FILE} з ${AGENTS_TEMPLATE_FILE}`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.log(`\n🔧 @nitra/cursor — завантаження cursor-правил\n`)
|
|
233
|
-
|
|
234
|
-
// 1. Зчитуємо конфіг
|
|
235
|
-
let config
|
|
236
|
-
try {
|
|
237
|
-
config = await readConfig()
|
|
238
|
-
} catch (error) {
|
|
239
|
-
console.error(`❌ ${error.message}`)
|
|
240
|
-
process.exit(1)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const { rules, version } = config
|
|
244
|
-
if (version) {
|
|
245
|
-
console.log(`📦 Версія пакету: ${version}`)
|
|
246
|
-
}
|
|
247
|
-
console.log(`📋 Правил до завантаження: ${rules.length}`)
|
|
248
|
-
|
|
249
|
-
// 2. Створюємо директорію .cursor/rules якщо не існує
|
|
250
|
-
const rulesDir = join(cwd(), RULES_DIR)
|
|
251
|
-
await mkdir(rulesDir, { recursive: true })
|
|
252
|
-
|
|
253
|
-
// 3. Завантажуємо та зберігаємо кожне правило
|
|
254
|
-
let successCount = 0
|
|
255
|
-
let failCount = 0
|
|
256
|
-
|
|
257
|
-
for (const rule of rules) {
|
|
258
|
-
const url = buildUrl(rule, version)
|
|
259
|
-
const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
|
|
260
|
-
const destPath = join(rulesDir, fileName)
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
|
|
264
|
-
const content = await fetchText(url)
|
|
265
|
-
await writeFile(destPath, content, 'utf8')
|
|
266
|
-
console.log(`✅`)
|
|
267
|
-
successCount++
|
|
268
|
-
} catch (error) {
|
|
269
|
-
console.log(`❌`)
|
|
270
|
-
console.error(` Помилка: ${error.message}`)
|
|
271
|
-
failCount++
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 4. Прибираємо керовані nitra-*.mdc, яких немає у nitra-cursor.json
|
|
276
|
-
try {
|
|
277
|
-
const removed = await removeOrphanManagedRuleFiles(rulesDir, rules)
|
|
278
|
-
if (removed.length > 0) {
|
|
279
|
-
console.log(`\n🧹 Видалено правила поза списком ${CONFIG_FILE} (${removed.length}):`)
|
|
280
|
-
for (const name of removed) {
|
|
281
|
-
console.log(` − ${RULES_DIR}/${name}`)
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.error(`❌ Не вдалося прибрати зайві файли в ${RULES_DIR}: ${error.message}`)
|
|
286
|
-
process.exit(1)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 5. AGENTS.md зі списком файлів *.mdc у .cursor/rules (після оновлення на диску)
|
|
290
|
-
try {
|
|
291
|
-
await syncAgentsMd()
|
|
292
|
-
} catch (error) {
|
|
293
|
-
console.error(`❌ Не вдалося оновити ${AGENTS_FILE}: ${error.message}`)
|
|
294
|
-
process.exit(1)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 6. Підсумок
|
|
298
|
-
console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
|
|
299
|
-
if (failCount > 0) {
|
|
300
|
-
process.exit(1)
|
|
301
|
-
}
|