@nitra/cursor 1.8.104 → 1.8.106

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,28 @@ 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} */ (['rules', 'skills', 'disable-rules', 'disable-skills'])
79
+
80
+ /**
81
+ * Сортує масиви id у конфігу за алфавітом (`localeCompare`), щоб порядок у файлі був стабільним після синку.
82
+ * @param {Record<string, unknown>} config об'єкт конфігу перед записом на диск
83
+ * @returns {Record<string, unknown>} копія з відсортованими масивами для відомих ключів
84
+ */
85
+ function sortConfigIdArrays(config) {
86
+ const out = { ...config }
87
+ for (const key of CONFIG_SORTED_ARRAY_KEYS) {
88
+ const v = out[key]
89
+ if (key in out && Array.isArray(v)) {
90
+ out[key] = v.map(String).toSorted((a, b) => a.localeCompare(b))
91
+ }
92
+ }
93
+ return out
94
+ }
95
+
76
96
  /**
77
97
  * Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
78
98
  * @param {string} [bundledMdcDir] каталог `mdc/` у корені пакету (за замовчуванням — з поточного процесу)
@@ -169,42 +189,106 @@ async function readConfig(paths = {}) {
169
189
  const bundledSkillsDir = paths.bundledSkillsDir ?? BUNDLED_SKILLS_DIR
170
190
  await migrateLegacyConfigIfNeeded()
171
191
  const configPath = join(cwd(), CONFIG_FILE)
192
+ const availableRules = await discoverBundledRuleNames(bundledMdcDir)
193
+ const availableSkills = await discoverBundledSkillNames(bundledSkillsDir)
194
+
195
+ /**
196
+ * Повертає розпарсений package.json з кореня або null, якщо файл відсутній/некоректний.
197
+ * @returns {Promise<unknown | null>} Обʼєкт package.json або null, якщо файл недоступний чи JSON невалідний.
198
+ */
199
+ async function readRootPackageJsonSafe() {
200
+ const packageJsonPath = join(cwd(), 'package.json')
201
+ if (!existsSync(packageJsonPath)) {
202
+ return null
203
+ }
204
+ try {
205
+ return JSON.parse(await readFile(packageJsonPath, 'utf8'))
206
+ } catch {
207
+ return null
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Автодописує правила/skills за `auto-rules.md` і синхронізує `$schema`.
213
+ * @param {Record<string, unknown>} parsedConfig сирий обʼєкт конфігу
214
+ * @returns {Promise<Record<string, unknown>>} нормалізований конфіг
215
+ */
216
+ async function normalizeConfigWithAutoRules(parsedConfig) {
217
+ const currentRules = parsedConfig.rules
218
+ if (!Array.isArray(currentRules)) {
219
+ throw new TypeError(`У ${CONFIG_FILE} поле "rules" має бути масивом рядків`)
220
+ }
221
+ if ('skills' in parsedConfig && !Array.isArray(parsedConfig.skills)) {
222
+ throw new Error(`У ${CONFIG_FILE} поле "skills" має бути масивом рядків`)
223
+ }
224
+
225
+ const rootPkg = await readRootPackageJsonSafe()
226
+ const disableRules = normalizeIdList(parsedConfig['disable-rules'])
227
+ const disableSkills = normalizeIdList(parsedConfig['disable-skills'])
228
+ const autoDetected = await detectAutoRulesAndSkills({
229
+ root: cwd(),
230
+ availableRules,
231
+ availableSkills,
232
+ packageJsonParsed: rootPkg,
233
+ disableRules,
234
+ disableSkills
235
+ })
236
+
237
+ const merged = mergeConfigWithAutoDetected({
238
+ config: parsedConfig,
239
+ detectedRules: autoDetected.rules,
240
+ detectedSkills: autoDetected.skills
241
+ })
242
+
243
+ const rest = Object.fromEntries(Object.entries(parsedConfig).filter(([k]) => k !== '$schema'))
244
+ const normalized = {
245
+ $schema: CONFIG_SCHEMA_URL,
246
+ ...rest,
247
+ rules: merged.rules,
248
+ skills: merged.skills
249
+ }
250
+ if (merged['disable-rules']?.length) {
251
+ normalized['disable-rules'] = merged['disable-rules']
252
+ }
253
+ if (merged['disable-skills']?.length) {
254
+ normalized['disable-skills'] = merged['disable-skills']
255
+ }
256
+ return sortConfigIdArrays(normalized)
257
+ }
258
+
172
259
  if (!existsSync(configPath)) {
173
- const rules = await discoverBundledRuleNames(bundledMdcDir)
174
- const skills = await discoverBundledSkillNames(bundledSkillsDir)
175
- const defaultConfig = { $schema: CONFIG_SCHEMA_URL, rules, skills }
260
+ const rootPkg = await readRootPackageJsonSafe()
261
+ const autoDetected = await detectAutoRulesAndSkills({
262
+ root: cwd(),
263
+ availableRules,
264
+ availableSkills,
265
+ packageJsonParsed: rootPkg
266
+ })
267
+ const defaultConfig = sortConfigIdArrays({
268
+ $schema: CONFIG_SCHEMA_URL,
269
+ rules: autoDetected.rules,
270
+ skills: autoDetected.skills
271
+ })
176
272
  await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
177
273
  console.log(
178
- `📝 Створено ${CONFIG_FILE} з усіма правилами (${rules.length}) і skills (${skills.length}) з пакету. За потреби відредагуйте списки.\n`
274
+ `📝 Створено ${CONFIG_FILE} з автоаналізом правил (${defaultConfig.rules.length}) і skills (${defaultConfig.skills.length}).\n`
179
275
  )
180
276
  return defaultConfig
181
277
  }
182
278
  const raw = await readFile(configPath, 'utf8')
279
+ /** @type {Record<string, unknown>} */
183
280
  let config
184
281
  try {
185
282
  config = JSON.parse(raw)
186
283
  } catch {
187
284
  throw new Error(`Невірний JSON у файлі ${CONFIG_FILE}`)
188
285
  }
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 }
286
+ const normalized = await normalizeConfigWithAutoRules(config)
287
+ if (JSON.stringify(normalized) !== JSON.stringify(config)) {
202
288
  await writeFile(configPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
203
- console.log(`📝 Оновлено поле $schema у ${CONFIG_FILE}\n`)
204
- return normalized
289
+ console.log(`📝 Оновлено ${CONFIG_FILE}: синхронізовано $schema та авто-додані rules/skills\n`)
205
290
  }
206
-
207
- return config
291
+ return normalized
208
292
  }
209
293
 
210
294
  /**
@@ -268,16 +352,22 @@ function extractSkillDescription(text) {
268
352
  if (!fm) {
269
353
  return null
270
354
  }
271
- const block = fm[1]
272
- const desc = block.match(SKILL_DESCRIPTION_RE)
273
- if (!desc) {
355
+ const lines = fm[1].split(NEWLINE_RE)
356
+ const start = lines.findIndex(line => line.trim() === 'description: >-')
357
+ if (start === -1) {
358
+ return null
359
+ }
360
+ const descLines = []
361
+ for (const line of lines.slice(start + 1)) {
362
+ if (!LEADING_SPACES_RE.test(line)) {
363
+ break
364
+ }
365
+ descLines.push(line.replace(LEADING_SPACES_RE, '').trimEnd())
366
+ }
367
+ if (descLines.length === 0) {
274
368
  return null
275
369
  }
276
- return desc[1]
277
- .split(NEWLINE_RE)
278
- .map(line => line.replace(LEADING_SPACES_RE, '').trimEnd())
279
- .join(' ')
280
- .trim()
370
+ return descLines.join(' ').trim()
281
371
  }
282
372
 
283
373
  /**
@@ -378,15 +468,31 @@ async function removeOrphanManagedRuleFiles(rulesDir, configRules) {
378
468
  }
379
469
 
380
470
  /**
381
- * Формує markdown-рядки для секції Skills у AGENTS.md з SKILL.md на диску
382
- * @param {string[]} skillIds id з конфігу (без префікса n-)
471
+ * Повертає відсортований список директорій skills у `.cursor/skills`.
472
+ * Директорія вважається skill-каталогом, якщо це підкаталог (без префікса `.`).
473
+ * @returns {Promise<string[]>} імена директорій (наприклад `n-fix`, `custom-skill`)
474
+ */
475
+ async function listProjectSkillDirNames() {
476
+ const skillsRoot = join(cwd(), SKILLS_DIR)
477
+ if (!existsSync(skillsRoot)) {
478
+ return []
479
+ }
480
+ const entries = await readdir(skillsRoot, { withFileTypes: true })
481
+ return entries
482
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
483
+ .map(entry => entry.name)
484
+ .toSorted((a, b) => a.localeCompare(b))
485
+ }
486
+
487
+ /**
488
+ * Формує markdown-рядки для секції Skills у AGENTS.md з усіх skill-директорій на диску.
383
489
  * @returns {Promise<{ name: string }[]>} елементи з полем name для Mustache-секції skills
384
490
  */
385
- async function buildSkillBulletItems(skillIds) {
491
+ async function buildSkillBulletItems() {
386
492
  const skillsRoot = join(cwd(), SKILLS_DIR)
493
+ const skillDirNames = await listProjectSkillDirNames()
387
494
  const items = []
388
- for (const id of skillIds) {
389
- const dirName = managedSkillDirName(id)
495
+ for (const dirName of skillDirNames) {
390
496
  const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
391
497
  let desc = ''
392
498
  if (existsSync(skillMdPath)) {
@@ -427,40 +533,54 @@ async function removeOrphanManagedSkillDirs(skillsRoot, configSkills) {
427
533
  return removed.toSorted((a, b) => a.localeCompare(b))
428
534
  }
429
535
 
536
+ /**
537
+ * Рендерить секцію Skills для CLAUDE.md з урахуванням наявних slash-команд.
538
+ * @returns {Promise<string[]>} готові рядки секції (або порожній масив)
539
+ */
540
+ async function buildClaudeSkillsSectionLines() {
541
+ const skillDirNames = await listProjectSkillDirNames()
542
+ if (skillDirNames.length === 0) {
543
+ return []
544
+ }
545
+
546
+ const lines = ['', '## Skills', '']
547
+ const skillsRoot = join(cwd(), SKILLS_DIR)
548
+ const commandsRoot = join(cwd(), COMMANDS_DIR)
549
+ for (const dirName of skillDirNames) {
550
+ const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
551
+ const commandPath = join(commandsRoot, `${dirName}.md`)
552
+ let desc = ''
553
+ if (existsSync(skillMdPath)) {
554
+ const text = await readFile(skillMdPath, 'utf8')
555
+ const parsed = extractSkillDescription(text)
556
+ if (parsed) {
557
+ desc = skillDescriptionSafeForMarkdownInline(parsed)
558
+ }
559
+ }
560
+ const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
561
+ lines.push(desc ? `${ref} — ${desc}` : ref)
562
+ if (existsSync(commandPath)) {
563
+ lines.push(` Команда: \`/${dirName}\``)
564
+ }
565
+ }
566
+ return lines
567
+ }
568
+
430
569
  /**
431
570
  * Генерує CLAUDE.md у корені cwd з at-імпортами всіх .mdc-правил та посиланнями на skills.
432
571
  * Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
433
- * @param {string[]} configRules елементи масиву rules з .n-cursor.json
434
- * @param {string[]} configSkills id skills з конфігу
435
572
  * @returns {Promise<void>}
436
573
  */
437
- async function syncClaudeMd(configRules, configSkills) {
574
+ async function syncClaudeMd() {
438
575
  const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
576
+ const mdcFiles = await listProjectRulesMdcFiles()
439
577
 
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
- }
578
+ for (const mdcFile of mdcFiles) {
579
+ lines.push(`@${RULES_DIR}/${mdcFile}`)
461
580
  }
462
581
 
463
- lines.push('')
582
+ const skillsSectionLines = await buildClaudeSkillsSectionLines()
583
+ lines.push(...skillsSectionLines, '')
464
584
  const claudeMdPath = join(cwd(), 'CLAUDE.md')
465
585
  const hadFile = existsSync(claudeMdPath)
466
586
  await writeFile(claudeMdPath, lines.join('\n'), 'utf8')
@@ -469,11 +589,10 @@ async function syncClaudeMd(configRules, configSkills) {
469
589
 
470
590
  /**
471
591
  * Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
472
- * @param {string[]} configSkills id skills з конфігу
473
592
  * @param {string} [agentsTemplatePath] шлях до AGENTS.template.md у корені пакету-джерела
474
593
  * @returns {Promise<void>} завершення запису файлу
475
594
  */
476
- async function syncAgentsMd(configSkills, agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
595
+ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
477
596
  if (!existsSync(agentsTemplatePath)) {
478
597
  throw new Error(
479
598
  `Не знайдено шаблон ${AGENTS_TEMPLATE_FILE} у пакеті.\n` +
@@ -483,7 +602,7 @@ async function syncAgentsMd(configSkills, agentsTemplatePath = BUNDLED_AGENTS_TE
483
602
  }
484
603
  const templateText = await readFile(agentsTemplatePath, 'utf8')
485
604
  const mdcFiles = await listProjectRulesMdcFiles()
486
- const skillItems = await buildSkillBulletItems(configSkills)
605
+ const skillItems = await buildSkillBulletItems()
487
606
  const body = renderAgentsTemplate(templateText, mdcFiles, skillItems)
488
607
  const agentsPath = join(cwd(), AGENTS_FILE)
489
608
  const hadFile = existsSync(agentsPath)
@@ -616,6 +735,76 @@ async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
616
735
  return removed.toSorted((a, b) => a.localeCompare(b))
617
736
  }
618
737
 
738
+ /**
739
+ * Людинозрозумілий текст винятку для логів.
740
+ * @param {unknown} error виняток із catch
741
+ * @returns {string} текст повідомлення
742
+ */
743
+ function errorMessage(error) {
744
+ return error instanceof Error ? error.message : String(error)
745
+ }
746
+
747
+ /**
748
+ * Виконує крок синхронізації з уніфікованим логуванням помилки.
749
+ * @template T
750
+ * @param {string} prefix префікс повідомлення про помилку
751
+ * @param {() => Promise<T>} action операція
752
+ * @returns {Promise<T>} результат операції
753
+ */
754
+ async function runSyncStep(prefix, action) {
755
+ try {
756
+ return await action()
757
+ } catch (error) {
758
+ console.error(`${prefix}${errorMessage(error)}`)
759
+ throw error
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Копіює керовані `.mdc` файли з пакету до `.cursor/rules`.
765
+ * @param {string[]} rules список rules з конфігу
766
+ * @param {string} bundledMdcDir каталог `mdc` пакету-джерела
767
+ * @param {string} rulesDir абсолютний шлях до `.cursor/rules`
768
+ * @returns {Promise<{ successCount: number, failCount: number }>} статистика копіювання
769
+ */
770
+ async function syncManagedRuleFiles(rules, bundledMdcDir, rulesDir) {
771
+ let successCount = 0
772
+ let failCount = 0
773
+ for (const rule of rules) {
774
+ const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
775
+ const destPath = join(rulesDir, fileName)
776
+ try {
777
+ process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
778
+ const content = await readBundledRuleContent(rule, bundledMdcDir)
779
+ await writeFile(destPath, content, 'utf8')
780
+ console.log(`✅`)
781
+ successCount++
782
+ } catch (error) {
783
+ console.log(`❌`)
784
+ console.error(` Помилка: ${errorMessage(error)}`)
785
+ failCount++
786
+ }
787
+ }
788
+ return { successCount, failCount }
789
+ }
790
+
791
+ /**
792
+ * Логує видалені керовані правила/skills/commands у єдиному форматі.
793
+ * @param {string} title назва сутностей
794
+ * @param {string} basePath базовий шлях для виводу
795
+ * @param {string[]} names перелік елементів
796
+ * @returns {void}
797
+ */
798
+ function logRemovedManagedItems(title, basePath, names) {
799
+ if (names.length === 0) {
800
+ return
801
+ }
802
+ console.log(`\n🧹 Видалено ${title} поза списком ${CONFIG_FILE} (${names.length}):`)
803
+ for (const name of names) {
804
+ console.log(` − ${basePath}/${name}`)
805
+ }
806
+ }
807
+
619
808
  /**
620
809
  * Знаходить доступні check-скрипти у каталозі scripts пакету
621
810
  * @returns {Promise<string[]>} відсортовані імена правил (наприклад ['bun', 'ga', 'js-lint'])
@@ -772,26 +961,15 @@ async function runSync() {
772
961
  console.log(`\n🔧 ${PACKAGE_NAME} — завантаження cursor-правил\n`)
773
962
 
774
963
  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
- }
964
+ const effectivePackageRoot = await runSyncStep(`❌ Не вдалося оновити ${PACKAGE_NAME} або виконати bun i: `, () =>
965
+ upgradeNitraCursorToLatestAndBunInstall(projectRoot, BUNDLED_PACKAGE_ROOT)
966
+ )
783
967
 
784
968
  const bundledMdcDir = join(effectivePackageRoot, 'mdc')
785
969
  const bundledSkillsDir = join(effectivePackageRoot, 'skills')
786
970
  const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
787
971
 
788
- let config
789
- try {
790
- config = await readConfig({ bundledMdcDir, bundledSkillsDir })
791
- } catch (error) {
792
- console.error(`❌ ${error.message}`)
793
- throw error
794
- }
972
+ const config = await runSyncStep('❌ ', () => readConfig({ bundledMdcDir, bundledSkillsDir }))
795
973
 
796
974
  const { rules, skills, version } = config
797
975
  const bundledVer = await readBundledVersionAt(effectivePackageRoot)
@@ -808,103 +986,46 @@ async function runSync() {
808
986
  console.log(`📋 Правил до завантаження: ${rules.length}`)
809
987
  console.log(`📋 Skills до синхронізації: ${skills.length}`)
810
988
 
811
- try {
989
+ await runSyncStep('❌ Не вдалося записати setup-bun-deps action: ', async () => {
812
990
  const { destPath } = await syncSetupBunDepsAction(cwd(), effectivePackageRoot)
813
991
  console.log(`📝 Оновлено ${destPath} (composite setup-bun-deps з пакету)\n`)
814
- } catch (error) {
815
- console.error(`❌ Не вдалося записати setup-bun-deps action: ${error.message}`)
816
- throw error
817
- }
992
+ })
818
993
 
819
994
  const rulesDir = join(cwd(), RULES_DIR)
820
995
  await mkdir(rulesDir, { recursive: true })
996
+ const { successCount, failCount } = await syncManagedRuleFiles(rules, bundledMdcDir, rulesDir)
821
997
 
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 {
998
+ await runSyncStep(`❌ Не вдалося прибрати зайві файли в ${RULES_DIR}: `, async () => {
843
999
  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
- }
1000
+ logRemovedManagedItems('правила', RULES_DIR, removed)
1001
+ })
854
1002
 
855
- try {
1003
+ await runSyncStep('❌ Skills: ', async () => {
856
1004
  const { success: skillOk, fail: skillFail } = await syncSkills(skills, bundledSkillsDir)
857
1005
  if (skills.length > 0) {
858
1006
  console.log(`\n🧩 Skills: ${skillOk} скопійовано, ${skillFail} з помилками`)
859
1007
  }
860
1008
  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
- }
1009
+ logRemovedManagedItems('skills', SKILLS_DIR, removedSkills)
867
1010
  if (skillFail > 0) {
868
1011
  throw new Error(`Не вдалося скопіювати ${skillFail} з ${skills.length} skills`)
869
1012
  }
870
- } catch (error) {
871
- console.error(`❌ Skills: ${error.message}`)
872
- throw error
873
- }
1013
+ })
874
1014
 
875
- try {
1015
+ await runSyncStep('❌ Commands: ', async () => {
876
1016
  const { success: cmdOk, fail: cmdFail } = await syncCommands(skills, bundledSkillsDir)
877
1017
  if (skills.length > 0) {
878
1018
  console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
879
1019
  }
880
1020
  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
- }
1021
+ logRemovedManagedItems('commands', COMMANDS_DIR, removedCmds)
887
1022
  if (cmdFail > 0) {
888
1023
  throw new Error(`Не вдалося скопіювати ${cmdFail} з ${skills.length} commands`)
889
1024
  }
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
- }
1025
+ })
901
1026
 
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
- }
1027
+ await runSyncStep(`❌ Не вдалося оновити ${AGENTS_FILE}: `, () => syncAgentsMd(bundledAgentsTemplatePath))
1028
+ await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () => syncClaudeMd())
908
1029
 
909
1030
  console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
910
1031
  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