@nitra/cursor 1.8.103 → 1.8.105

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,45 @@
1
+ # Авто вмикання правил та скілів
2
+
3
+ В цьому файлі описані умови, по яким повинні скіли та правила автододаватись в конфіг
4
+
5
+ ## Правила, які автоматично додається до .n-cursor.json
6
+
7
+ abie - якщо в кореневому package.json в секції "repository" присутній текст "https://github.com/abinbevefes/**/"
8
+
9
+ bun - якщо в корені проекту є package.json
10
+
11
+ docker - якщо в проекті є хоч один Dockerfile
12
+
13
+ ga - якщо присутня директорія .github/workflows
14
+
15
+ graphql - якщо хоч в одному js або vue файлі присутній gql` темплейт літерал
16
+
17
+ js-lint - якщо присутній хоч один js файл
18
+
19
+ js-pino - якщо присутній хоч один js файл, не в монорепо проекті з vue та директорії tempo
20
+
21
+ k8s - якщо присутня хоч одна директорія k8s
22
+
23
+ nginx-default-tpl - якщо присутній хоч один файл з переліку - default.conf.template, default.conf, nginx.conf
24
+
25
+ npm-module - якщо в корені присутня директорія npm
26
+
27
+ style-lint - якщо присутній хоч один vue або css файл
28
+
29
+ text - завжди
30
+
31
+ vue - якщо присутній хоч один vue файл
32
+
33
+ ## Скіли, які автоматично додається до .n-cursor.json
34
+
35
+ abie-kustomize - якщо в кореневому package.json в секції "repository" присутній текст "https://github.com/abinbevefes/**/"
36
+
37
+ fix - завжди
38
+
39
+ lint - завжди
40
+
41
+ ## Виключення
42
+
43
+ Якщо в .n-cursor.json задано в секції disable-rules правило, то воно автоматично додаватись не повинно.
44
+
45
+ Якщо в .n-cursor.json задано в секції disable-skills скіл, то він автоматично додаватись не повинен.
package/bin/n-cursor.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * у `.cursor/rules` файли `nitra-*.mdc` перейменовуються на `n-*.mdc`; інакше конфіг створюється автоматично
15
15
  * з усіма правилами з каталогу mdc пакету (їх можна відредагувати після створення). У файлі завжди має бути
16
16
  * поле `$schema` з посиланням на JSON Schema пакету (публічний URL для IDE); при зчитуванні конфігу воно додається або виправляється на диску, якщо відсутнє або некоректне.
17
+ * Масиви `rules`, `skills`, `disable-rules` і `disable-skills` при записі сортуються за алфавітом.
17
18
  *
18
19
  * Файл AGENTS.md у корені: щоразу повністю перезаписується змістом з AGENTS.template.md
19
20
  * пакету; список правил у шаблоні будується з файлів *.mdc у .cursor/rules поточного проєкту.
@@ -44,6 +45,7 @@ import { basename, dirname, join } from 'node:path'
44
45
  import { cwd } from 'node:process'
45
46
  import { fileURLToPath } from 'node:url'
46
47
 
48
+ import { detectAutoRulesAndSkills, mergeConfigWithAutoDetected, normalizeIdList } from '../scripts/auto-rules.mjs'
47
49
  import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
48
50
  import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
49
51
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
@@ -69,10 +71,37 @@ const BUNDLED_AGENTS_TEMPLATE_PATH = join(binDir, '..', AGENTS_TEMPLATE_FILE)
69
71
  const BUNDLED_PACKAGE_ROOT = join(binDir, '..')
70
72
 
71
73
  const YAML_FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
72
- const SKILL_DESCRIPTION_RE = /description:\s*>-\s*\r?\n((?:[ \t]+[^\n]*(?:\r?\n|$))+)/m
73
74
  const NEWLINE_RE = /\r?\n/
74
75
  const LEADING_SPACES_RE = /^\s+/
75
76
 
77
+ /** Ключі `.n-cursor.json`, де значення — масиви id; після запуску CLI сортуються за алфавітом */
78
+ const CONFIG_SORTED_ARRAY_KEYS = /** @type {const} */ ([
79
+ 'rules',
80
+ 'skills',
81
+ 'disable-rules',
82
+ 'disable-skills'
83
+ ])
84
+
85
+ /**
86
+ * Сортує масиви id у конфігу за алфавітом (`localeCompare`), щоб порядок у файлі був стабільним після синку.
87
+ * @param {Record<string, unknown>} config об'єкт конфігу перед записом на диск
88
+ * @returns {Record<string, unknown>} копія з відсортованими масивами для відомих ключів
89
+ */
90
+ function sortConfigIdArrays(config) {
91
+ const out = { ...config }
92
+ for (const key of CONFIG_SORTED_ARRAY_KEYS) {
93
+ if (!(key in out)) {
94
+ continue
95
+ }
96
+ const v = out[key]
97
+ if (!Array.isArray(v)) {
98
+ continue
99
+ }
100
+ out[key] = v.map(x => String(x)).toSorted((a, b) => a.localeCompare(b))
101
+ }
102
+ return out
103
+ }
104
+
76
105
  /**
77
106
  * Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
78
107
  * @param {string} [bundledMdcDir] каталог `mdc/` у корені пакету (за замовчуванням — з поточного процесу)
@@ -169,42 +198,106 @@ async function readConfig(paths = {}) {
169
198
  const bundledSkillsDir = paths.bundledSkillsDir ?? BUNDLED_SKILLS_DIR
170
199
  await migrateLegacyConfigIfNeeded()
171
200
  const configPath = join(cwd(), CONFIG_FILE)
201
+ const availableRules = await discoverBundledRuleNames(bundledMdcDir)
202
+ const availableSkills = await discoverBundledSkillNames(bundledSkillsDir)
203
+
204
+ /**
205
+ * Повертає розпарсений package.json з кореня або null, якщо файл відсутній/некоректний.
206
+ * @returns {Promise<unknown | null>} Обʼєкт package.json або null, якщо файл недоступний чи JSON невалідний.
207
+ */
208
+ async function readRootPackageJsonSafe() {
209
+ const packageJsonPath = join(cwd(), 'package.json')
210
+ if (!existsSync(packageJsonPath)) {
211
+ return null
212
+ }
213
+ try {
214
+ return JSON.parse(await readFile(packageJsonPath, 'utf8'))
215
+ } catch {
216
+ return null
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Автодописує правила/skills за `auto-rules.md` і синхронізує `$schema`.
222
+ * @param {Record<string, unknown>} parsedConfig сирий обʼєкт конфігу
223
+ * @returns {Promise<Record<string, unknown>>} нормалізований конфіг
224
+ */
225
+ async function normalizeConfigWithAutoRules(parsedConfig) {
226
+ const currentRules = parsedConfig.rules
227
+ if (!Array.isArray(currentRules)) {
228
+ throw new TypeError(`У ${CONFIG_FILE} поле "rules" має бути масивом рядків`)
229
+ }
230
+ if ('skills' in parsedConfig && !Array.isArray(parsedConfig.skills)) {
231
+ throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
232
+ }
233
+
234
+ const rootPkg = await readRootPackageJsonSafe()
235
+ const disableRules = normalizeIdList(parsedConfig['disable-rules'])
236
+ const disableSkills = normalizeIdList(parsedConfig['disable-skills'])
237
+ const autoDetected = await detectAutoRulesAndSkills({
238
+ root: cwd(),
239
+ availableRules,
240
+ availableSkills,
241
+ packageJsonParsed: rootPkg,
242
+ disableRules,
243
+ disableSkills
244
+ })
245
+
246
+ const merged = mergeConfigWithAutoDetected({
247
+ config: parsedConfig,
248
+ detectedRules: autoDetected.rules,
249
+ detectedSkills: autoDetected.skills
250
+ })
251
+
252
+ const rest = Object.fromEntries(Object.entries(parsedConfig).filter(([k]) => k !== '$schema'))
253
+ const normalized = {
254
+ $schema: CONFIG_SCHEMA_URL,
255
+ ...rest,
256
+ rules: merged.rules,
257
+ skills: merged.skills
258
+ }
259
+ if (merged['disable-rules']?.length) {
260
+ normalized['disable-rules'] = merged['disable-rules']
261
+ }
262
+ if (merged['disable-skills']?.length) {
263
+ normalized['disable-skills'] = merged['disable-skills']
264
+ }
265
+ return sortConfigIdArrays(normalized)
266
+ }
267
+
172
268
  if (!existsSync(configPath)) {
173
- const rules = await discoverBundledRuleNames(bundledMdcDir)
174
- const skills = await discoverBundledSkillNames(bundledSkillsDir)
175
- const defaultConfig = { $schema: CONFIG_SCHEMA_URL, rules, skills }
269
+ const rootPkg = await readRootPackageJsonSafe()
270
+ const autoDetected = await detectAutoRulesAndSkills({
271
+ root: cwd(),
272
+ availableRules,
273
+ availableSkills,
274
+ packageJsonParsed: rootPkg
275
+ })
276
+ const defaultConfig = sortConfigIdArrays({
277
+ $schema: CONFIG_SCHEMA_URL,
278
+ rules: autoDetected.rules,
279
+ skills: autoDetected.skills
280
+ })
176
281
  await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
177
282
  console.log(
178
- `📝 Створено ${CONFIG_FILE} з усіма правилами (${rules.length}) і skills (${skills.length}) з пакету. За потреби відредагуйте списки.\n`
283
+ `📝 Створено ${CONFIG_FILE} з автоаналізом правил (${defaultConfig.rules.length}) і skills (${defaultConfig.skills.length}).\n`
179
284
  )
180
285
  return defaultConfig
181
286
  }
182
287
  const raw = await readFile(configPath, 'utf8')
288
+ /** @type {Record<string, unknown>} */
183
289
  let config
184
290
  try {
185
291
  config = JSON.parse(raw)
186
292
  } catch {
187
293
  throw new Error(`Невірний JSON у файлі ${CONFIG_FILE}`)
188
294
  }
189
- if (!Array.isArray(config.rules) || config.rules.length === 0) {
190
- throw new Error( ${CONFIG_FILE} має бути непорожній масив "rules"`)
191
- }
192
- if (!Array.isArray(config.skills)) {
193
- if ('skills' in config) {
194
- throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
195
- }
196
- config.skills = await discoverBundledSkillNames(bundledSkillsDir)
197
- }
198
-
199
- if (config.$schema !== CONFIG_SCHEMA_URL) {
200
- const rest = Object.fromEntries(Object.entries(config).filter(([k]) => k !== '$schema'))
201
- const normalized = { $schema: CONFIG_SCHEMA_URL, ...rest }
295
+ const normalized = await normalizeConfigWithAutoRules(config)
296
+ if (JSON.stringify(normalized) !== JSON.stringify(config)) {
202
297
  await writeFile(configPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
203
- console.log(`📝 Оновлено поле $schema у ${CONFIG_FILE}\n`)
204
- return normalized
298
+ console.log(`📝 Оновлено ${CONFIG_FILE}: синхронізовано $schema та авто-додані rules/skills\n`)
205
299
  }
206
-
207
- return config
300
+ return normalized
208
301
  }
209
302
 
210
303
  /**
@@ -268,16 +361,22 @@ function extractSkillDescription(text) {
268
361
  if (!fm) {
269
362
  return null
270
363
  }
271
- const block = fm[1]
272
- const desc = block.match(SKILL_DESCRIPTION_RE)
273
- if (!desc) {
364
+ const lines = fm[1].split(NEWLINE_RE)
365
+ const start = lines.findIndex(line => line.trim() === 'description: >-')
366
+ if (start === -1) {
367
+ return null
368
+ }
369
+ const descLines = []
370
+ for (const line of lines.slice(start + 1)) {
371
+ if (!LEADING_SPACES_RE.test(line)) {
372
+ break
373
+ }
374
+ descLines.push(line.replace(LEADING_SPACES_RE, '').trimEnd())
375
+ }
376
+ if (descLines.length === 0) {
274
377
  return null
275
378
  }
276
- return desc[1]
277
- .split(NEWLINE_RE)
278
- .map(line => line.replace(LEADING_SPACES_RE, '').trimEnd())
279
- .join(' ')
280
- .trim()
379
+ return descLines.join(' ').trim()
281
380
  }
282
381
 
283
382
  /**
@@ -378,15 +477,31 @@ async function removeOrphanManagedRuleFiles(rulesDir, configRules) {
378
477
  }
379
478
 
380
479
  /**
381
- * Формує markdown-рядки для секції Skills у AGENTS.md з SKILL.md на диску
382
- * @param {string[]} skillIds id з конфігу (без префікса n-)
480
+ * Повертає відсортований список директорій skills у `.cursor/skills`.
481
+ * Директорія вважається skill-каталогом, якщо це підкаталог (без префікса `.`).
482
+ * @returns {Promise<string[]>} імена директорій (наприклад `n-fix`, `custom-skill`)
483
+ */
484
+ async function listProjectSkillDirNames() {
485
+ const skillsRoot = join(cwd(), SKILLS_DIR)
486
+ if (!existsSync(skillsRoot)) {
487
+ return []
488
+ }
489
+ const entries = await readdir(skillsRoot, { withFileTypes: true })
490
+ return entries
491
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
492
+ .map(entry => entry.name)
493
+ .toSorted((a, b) => a.localeCompare(b))
494
+ }
495
+
496
+ /**
497
+ * Формує markdown-рядки для секції Skills у AGENTS.md з усіх skill-директорій на диску.
383
498
  * @returns {Promise<{ name: string }[]>} елементи з полем name для Mustache-секції skills
384
499
  */
385
- async function buildSkillBulletItems(skillIds) {
500
+ async function buildSkillBulletItems() {
386
501
  const skillsRoot = join(cwd(), SKILLS_DIR)
502
+ const skillDirNames = await listProjectSkillDirNames()
387
503
  const items = []
388
- for (const id of skillIds) {
389
- const dirName = managedSkillDirName(id)
504
+ for (const dirName of skillDirNames) {
390
505
  const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
391
506
  let desc = ''
392
507
  if (existsSync(skillMdPath)) {
@@ -427,40 +542,54 @@ async function removeOrphanManagedSkillDirs(skillsRoot, configSkills) {
427
542
  return removed.toSorted((a, b) => a.localeCompare(b))
428
543
  }
429
544
 
545
+ /**
546
+ * Рендерить секцію Skills для CLAUDE.md з урахуванням наявних slash-команд.
547
+ * @returns {Promise<string[]>} готові рядки секції (або порожній масив)
548
+ */
549
+ async function buildClaudeSkillsSectionLines() {
550
+ const skillDirNames = await listProjectSkillDirNames()
551
+ if (skillDirNames.length === 0) {
552
+ return []
553
+ }
554
+
555
+ const lines = ['', '## Skills', '']
556
+ const skillsRoot = join(cwd(), SKILLS_DIR)
557
+ const commandsRoot = join(cwd(), COMMANDS_DIR)
558
+ for (const dirName of skillDirNames) {
559
+ const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
560
+ const commandPath = join(commandsRoot, `${dirName}.md`)
561
+ let desc = ''
562
+ if (existsSync(skillMdPath)) {
563
+ const text = await readFile(skillMdPath, 'utf8')
564
+ const parsed = extractSkillDescription(text)
565
+ if (parsed) {
566
+ desc = skillDescriptionSafeForMarkdownInline(parsed)
567
+ }
568
+ }
569
+ const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
570
+ lines.push(desc ? `${ref} — ${desc}` : ref)
571
+ if (existsSync(commandPath)) {
572
+ lines.push(` Команда: \`/${dirName}\``)
573
+ }
574
+ }
575
+ return lines
576
+ }
577
+
430
578
  /**
431
579
  * Генерує CLAUDE.md у корені cwd з at-імпортами всіх .mdc-правил та посиланнями на skills.
432
580
  * Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
433
- * @param {string[]} configRules елементи масиву rules з .n-cursor.json
434
- * @param {string[]} configSkills id skills з конфігу
435
581
  * @returns {Promise<void>}
436
582
  */
437
- async function syncClaudeMd(configRules, configSkills) {
583
+ async function syncClaudeMd() {
438
584
  const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
585
+ const mdcFiles = await listProjectRulesMdcFiles()
439
586
 
440
- for (const rule of configRules) {
441
- const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
442
- lines.push(`@${RULES_DIR}/${fileName}`)
443
- }
444
-
445
- if (configSkills.length > 0) {
446
- lines.push('', '## Skills', '')
447
- const skillsRoot = join(cwd(), SKILLS_DIR)
448
- for (const skillId of configSkills) {
449
- const id = normalizeSkillId(skillId)
450
- const dirName = managedSkillDirName(skillId)
451
- const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
452
- let desc = ''
453
- if (existsSync(skillMdPath)) {
454
- const text = await readFile(skillMdPath, 'utf8')
455
- const parsed = extractSkillDescription(text)
456
- if (parsed) desc = skillDescriptionSafeForMarkdownInline(parsed)
457
- }
458
- const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
459
- lines.push(desc ? `${ref} — ${desc}` : ref, ` Команда: \`/${RULE_PREFIX}${id}\``)
460
- }
587
+ for (const mdcFile of mdcFiles) {
588
+ lines.push(`@${RULES_DIR}/${mdcFile}`)
461
589
  }
462
590
 
463
- lines.push('')
591
+ const skillsSectionLines = await buildClaudeSkillsSectionLines()
592
+ lines.push(...skillsSectionLines, '')
464
593
  const claudeMdPath = join(cwd(), 'CLAUDE.md')
465
594
  const hadFile = existsSync(claudeMdPath)
466
595
  await writeFile(claudeMdPath, lines.join('\n'), 'utf8')
@@ -469,11 +598,10 @@ async function syncClaudeMd(configRules, configSkills) {
469
598
 
470
599
  /**
471
600
  * Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
472
- * @param {string[]} configSkills id skills з конфігу
473
601
  * @param {string} [agentsTemplatePath] шлях до AGENTS.template.md у корені пакету-джерела
474
602
  * @returns {Promise<void>} завершення запису файлу
475
603
  */
476
- async function syncAgentsMd(configSkills, agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
604
+ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
477
605
  if (!existsSync(agentsTemplatePath)) {
478
606
  throw new Error(
479
607
  `Не знайдено шаблон ${AGENTS_TEMPLATE_FILE} у пакеті.\n` +
@@ -483,7 +611,7 @@ async function syncAgentsMd(configSkills, agentsTemplatePath = BUNDLED_AGENTS_TE
483
611
  }
484
612
  const templateText = await readFile(agentsTemplatePath, 'utf8')
485
613
  const mdcFiles = await listProjectRulesMdcFiles()
486
- const skillItems = await buildSkillBulletItems(configSkills)
614
+ const skillItems = await buildSkillBulletItems()
487
615
  const body = renderAgentsTemplate(templateText, mdcFiles, skillItems)
488
616
  const agentsPath = join(cwd(), AGENTS_FILE)
489
617
  const hadFile = existsSync(agentsPath)
@@ -616,6 +744,76 @@ async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
616
744
  return removed.toSorted((a, b) => a.localeCompare(b))
617
745
  }
618
746
 
747
+ /**
748
+ * Людинозрозумілий текст винятку для логів.
749
+ * @param {unknown} error виняток із catch
750
+ * @returns {string} текст повідомлення
751
+ */
752
+ function errorMessage(error) {
753
+ return error instanceof Error ? error.message : String(error)
754
+ }
755
+
756
+ /**
757
+ * Виконує крок синхронізації з уніфікованим логуванням помилки.
758
+ * @template T
759
+ * @param {string} prefix префікс повідомлення про помилку
760
+ * @param {() => Promise<T>} action операція
761
+ * @returns {Promise<T>} результат операції
762
+ */
763
+ async function runSyncStep(prefix, action) {
764
+ try {
765
+ return await action()
766
+ } catch (error) {
767
+ console.error(`${prefix}${errorMessage(error)}`)
768
+ throw error
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Копіює керовані `.mdc` файли з пакету до `.cursor/rules`.
774
+ * @param {string[]} rules список rules з конфігу
775
+ * @param {string} bundledMdcDir каталог `mdc` пакету-джерела
776
+ * @param {string} rulesDir абсолютний шлях до `.cursor/rules`
777
+ * @returns {Promise<{ successCount: number, failCount: number }>} статистика копіювання
778
+ */
779
+ async function syncManagedRuleFiles(rules, bundledMdcDir, rulesDir) {
780
+ let successCount = 0
781
+ let failCount = 0
782
+ for (const rule of rules) {
783
+ const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
784
+ const destPath = join(rulesDir, fileName)
785
+ try {
786
+ process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
787
+ const content = await readBundledRuleContent(rule, bundledMdcDir)
788
+ await writeFile(destPath, content, 'utf8')
789
+ console.log(`✅`)
790
+ successCount++
791
+ } catch (error) {
792
+ console.log(`❌`)
793
+ console.error(` Помилка: ${errorMessage(error)}`)
794
+ failCount++
795
+ }
796
+ }
797
+ return { successCount, failCount }
798
+ }
799
+
800
+ /**
801
+ * Логує видалені керовані правила/skills/commands у єдиному форматі.
802
+ * @param {string} title назва сутностей
803
+ * @param {string} basePath базовий шлях для виводу
804
+ * @param {string[]} names перелік елементів
805
+ * @returns {void}
806
+ */
807
+ function logRemovedManagedItems(title, basePath, names) {
808
+ if (names.length === 0) {
809
+ return
810
+ }
811
+ console.log(`\n🧹 Видалено ${title} поза списком ${CONFIG_FILE} (${names.length}):`)
812
+ for (const name of names) {
813
+ console.log(` − ${basePath}/${name}`)
814
+ }
815
+ }
816
+
619
817
  /**
620
818
  * Знаходить доступні check-скрипти у каталозі scripts пакету
621
819
  * @returns {Promise<string[]>} відсортовані імена правил (наприклад ['bun', 'ga', 'js-lint'])
@@ -772,26 +970,16 @@ async function runSync() {
772
970
  console.log(`\n🔧 ${PACKAGE_NAME} — завантаження cursor-правил\n`)
773
971
 
774
972
  const projectRoot = cwd()
775
- let effectivePackageRoot
776
- try {
777
- effectivePackageRoot = await upgradeNitraCursorToLatestAndBunInstall(projectRoot, BUNDLED_PACKAGE_ROOT)
778
- } catch (error) {
779
- const msg = error instanceof Error ? error.message : String(error)
780
- console.error(`❌ Не вдалося оновити ${PACKAGE_NAME} або виконати bun i: ${msg}`)
781
- throw error
782
- }
973
+ const effectivePackageRoot = await runSyncStep(
974
+ `❌ Не вдалося оновити ${PACKAGE_NAME} або виконати bun i: `,
975
+ () => upgradeNitraCursorToLatestAndBunInstall(projectRoot, BUNDLED_PACKAGE_ROOT)
976
+ )
783
977
 
784
978
  const bundledMdcDir = join(effectivePackageRoot, 'mdc')
785
979
  const bundledSkillsDir = join(effectivePackageRoot, 'skills')
786
980
  const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
787
981
 
788
- let config
789
- try {
790
- config = await readConfig({ bundledMdcDir, bundledSkillsDir })
791
- } catch (error) {
792
- console.error(`❌ ${error.message}`)
793
- throw error
794
- }
982
+ const config = await runSyncStep('❌ ', () => readConfig({ bundledMdcDir, bundledSkillsDir }))
795
983
 
796
984
  const { rules, skills, version } = config
797
985
  const bundledVer = await readBundledVersionAt(effectivePackageRoot)
@@ -808,103 +996,46 @@ async function runSync() {
808
996
  console.log(`📋 Правил до завантаження: ${rules.length}`)
809
997
  console.log(`📋 Skills до синхронізації: ${skills.length}`)
810
998
 
811
- try {
999
+ await runSyncStep('❌ Не вдалося записати setup-bun-deps action: ', async () => {
812
1000
  const { destPath } = await syncSetupBunDepsAction(cwd(), effectivePackageRoot)
813
1001
  console.log(`📝 Оновлено ${destPath} (composite setup-bun-deps з пакету)\n`)
814
- } catch (error) {
815
- console.error(`❌ Не вдалося записати setup-bun-deps action: ${error.message}`)
816
- throw error
817
- }
1002
+ })
818
1003
 
819
1004
  const rulesDir = join(cwd(), RULES_DIR)
820
1005
  await mkdir(rulesDir, { recursive: true })
1006
+ const { successCount, failCount } = await syncManagedRuleFiles(rules, bundledMdcDir, rulesDir)
821
1007
 
822
- let successCount = 0
823
- let failCount = 0
824
-
825
- for (const rule of rules) {
826
- const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
827
- const destPath = join(rulesDir, fileName)
828
-
829
- try {
830
- process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
831
- const content = await readBundledRuleContent(rule, bundledMdcDir)
832
- await writeFile(destPath, content, 'utf8')
833
- console.log(`✅`)
834
- successCount++
835
- } catch (error) {
836
- console.log(`❌`)
837
- console.error(` Помилка: ${error.message}`)
838
- failCount++
839
- }
840
- }
841
-
842
- try {
1008
+ await runSyncStep(`❌ Не вдалося прибрати зайві файли в ${RULES_DIR}: `, async () => {
843
1009
  const removed = await removeOrphanManagedRuleFiles(rulesDir, rules)
844
- if (removed.length > 0) {
845
- console.log(`\n🧹 Видалено правила поза списком ${CONFIG_FILE} (${removed.length}):`)
846
- for (const name of removed) {
847
- console.log(` − ${RULES_DIR}/${name}`)
848
- }
849
- }
850
- } catch (error) {
851
- console.error(`❌ Не вдалося прибрати зайві файли в ${RULES_DIR}: ${error.message}`)
852
- throw error
853
- }
1010
+ logRemovedManagedItems('правила', RULES_DIR, removed)
1011
+ })
854
1012
 
855
- try {
1013
+ await runSyncStep('❌ Skills: ', async () => {
856
1014
  const { success: skillOk, fail: skillFail } = await syncSkills(skills, bundledSkillsDir)
857
1015
  if (skills.length > 0) {
858
1016
  console.log(`\n🧩 Skills: ${skillOk} скопійовано, ${skillFail} з помилками`)
859
1017
  }
860
1018
  const removedSkills = await removeOrphanManagedSkillDirs(join(cwd(), SKILLS_DIR), skills)
861
- if (removedSkills.length > 0) {
862
- console.log(`\n🧹 Видалено skills поза списком ${CONFIG_FILE} (${removedSkills.length}):`)
863
- for (const name of removedSkills) {
864
- console.log(` − ${SKILLS_DIR}/${name}`)
865
- }
866
- }
1019
+ logRemovedManagedItems('skills', SKILLS_DIR, removedSkills)
867
1020
  if (skillFail > 0) {
868
1021
  throw new Error(`Не вдалося скопіювати ${skillFail} з ${skills.length} skills`)
869
1022
  }
870
- } catch (error) {
871
- console.error(`❌ Skills: ${error.message}`)
872
- throw error
873
- }
1023
+ })
874
1024
 
875
- try {
1025
+ await runSyncStep('❌ Commands: ', async () => {
876
1026
  const { success: cmdOk, fail: cmdFail } = await syncCommands(skills, bundledSkillsDir)
877
1027
  if (skills.length > 0) {
878
1028
  console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
879
1029
  }
880
1030
  const removedCmds = await removeOrphanManagedCommandFiles(join(cwd(), COMMANDS_DIR), skills)
881
- if (removedCmds.length > 0) {
882
- console.log(`\n🧹 Видалено commands поза списком ${CONFIG_FILE} (${removedCmds.length}):`)
883
- for (const name of removedCmds) {
884
- console.log(` − ${COMMANDS_DIR}/${name}`)
885
- }
886
- }
1031
+ logRemovedManagedItems('commands', COMMANDS_DIR, removedCmds)
887
1032
  if (cmdFail > 0) {
888
1033
  throw new Error(`Не вдалося скопіювати ${cmdFail} з ${skills.length} commands`)
889
1034
  }
890
- } catch (error) {
891
- console.error(`❌ Commands: ${error instanceof Error ? error.message : String(error)}`)
892
- throw error
893
- }
894
-
895
- try {
896
- await syncAgentsMd(skills, bundledAgentsTemplatePath)
897
- } catch (error) {
898
- console.error(`❌ Не вдалося оновити ${AGENTS_FILE}: ${error.message}`)
899
- throw error
900
- }
1035
+ })
901
1036
 
902
- try {
903
- await syncClaudeMd(rules, skills)
904
- } catch (error) {
905
- console.error(`❌ Не вдалося оновити CLAUDE.md: ${error instanceof Error ? error.message : String(error)}`)
906
- throw error
907
- }
1037
+ await runSyncStep(`❌ Не вдалося оновити ${AGENTS_FILE}: `, () => syncAgentsMd(bundledAgentsTemplatePath))
1038
+ await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () => syncClaudeMd())
908
1039
 
909
1040
  console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
910
1041
  if (failCount > 0) {
package/mdc/graphql.mdc CHANGED
@@ -4,7 +4,21 @@ alwaysApply: true
4
4
  version: '1.0'
5
5
  ---
6
6
 
7
- Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\``для GraphQL-запиту), у **корені репозиторію** має бути файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs)), а в **`.vscode/extensions.json`** у масиві **`recommendations`** — запис **`graphql.vscode-graphql`**, щоб підсвітка, навігація до схеми й діагностика працювали в редакторі.
7
+ Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\`` для GraphQL-запиту), у **корені репозиторію** мають бути:
8
+
9
+ - файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs));
10
+ - у **`.vscode/extensions.json`** в масиві **`recommendations`** — запис **`graphql.vscode-graphql`**;
11
+ - у кореневому **`package.json`** скрипт:
12
+
13
+ ```json title="package.json (фрагмент)"
14
+ {
15
+ "scripts": {
16
+ "dump-schema": "bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql"
17
+ }
18
+ }
19
+ ```
20
+
21
+ Це забезпечує підсвітку та діагностику GraphQL в редакторі, а також стандартний спосіб оновлення локальної `schema.graphql`.
8
22
 
9
23
  Деталі виявлення `gql` у скриптах (у т.ч. лише `<script>` у SFC) — **`npm/scripts/check-graphql.mjs`** / **`npm/scripts/utils/graphql-gql-scan.mjs`**.
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.103",
3
+ "version": "1.8.105",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",