@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.
@@ -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
+ ````
@@ -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
- }