@nitra/cursor 1.8.5 → 1.8.6
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/bin/n-cursor.js +140 -0
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +4 -5
package/bin/n-cursor.js
CHANGED
|
@@ -52,6 +52,7 @@ const AGENTS_FILE = 'AGENTS.md'
|
|
|
52
52
|
const AGENTS_TEMPLATE_FILE = 'AGENTS.template.md'
|
|
53
53
|
const RULES_DIR = '.cursor/rules'
|
|
54
54
|
const SKILLS_DIR = '.cursor/skills'
|
|
55
|
+
const COMMANDS_DIR = '.claude/commands'
|
|
55
56
|
const RULE_PREFIX = 'n-'
|
|
56
57
|
|
|
57
58
|
const binDir = dirname(fileURLToPath(import.meta.url))
|
|
@@ -415,6 +416,49 @@ async function removeOrphanManagedSkillDirs(skillsRoot, configSkills) {
|
|
|
415
416
|
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Генерує CLAUDE.md у корені cwd з at-імпортами всіх .mdc-правил та посиланнями на skills.
|
|
421
|
+
* Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
|
|
422
|
+
* @param {string[]} configRules елементи масиву rules з .n-cursor.json
|
|
423
|
+
* @param {string[]} configSkills id skills з конфігу
|
|
424
|
+
* @returns {Promise<void>}
|
|
425
|
+
*/
|
|
426
|
+
async function syncClaudeMd(configRules, configSkills) {
|
|
427
|
+
const lines = [
|
|
428
|
+
`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`,
|
|
429
|
+
'',
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
for (const rule of configRules) {
|
|
433
|
+
const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
|
|
434
|
+
lines.push(`@${RULES_DIR}/${fileName}`)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (configSkills.length > 0) {
|
|
438
|
+
lines.push('', '## Skills', '')
|
|
439
|
+
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
440
|
+
for (const skillId of configSkills) {
|
|
441
|
+
const id = canonicalSkillId(skillId)
|
|
442
|
+
const dirName = managedSkillDirName(skillId)
|
|
443
|
+
const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
|
|
444
|
+
let desc = ''
|
|
445
|
+
if (existsSync(skillMdPath)) {
|
|
446
|
+
const text = await readFile(skillMdPath, 'utf8')
|
|
447
|
+
const parsed = extractSkillDescription(text)
|
|
448
|
+
if (parsed) desc = parsed
|
|
449
|
+
}
|
|
450
|
+
const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
451
|
+
lines.push(desc ? `${ref} — ${desc}` : ref, ` Команда: \`/${RULE_PREFIX}${id}\``)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
lines.push('')
|
|
456
|
+
const claudeMdPath = join(cwd(), 'CLAUDE.md')
|
|
457
|
+
const hadFile = existsSync(claudeMdPath)
|
|
458
|
+
await writeFile(claudeMdPath, lines.join('\n'), 'utf8')
|
|
459
|
+
console.log(hadFile ? `📝 Оновлено CLAUDE.md` : `📝 Створено CLAUDE.md`)
|
|
460
|
+
}
|
|
461
|
+
|
|
418
462
|
/**
|
|
419
463
|
* Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
|
|
420
464
|
* @param {string[]} configSkills id skills з конфігу
|
|
@@ -491,6 +535,75 @@ async function syncSkills(configSkills) {
|
|
|
491
535
|
return { success, fail }
|
|
492
536
|
}
|
|
493
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Синхронізує .claude/commands/n-<id>.md зі skills пакету.
|
|
540
|
+
* Кожен файл містить посилання на відповідний cursor skill, а не копію інструкцій.
|
|
541
|
+
* @param {string[]} configSkills id без префікса n-
|
|
542
|
+
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
|
|
543
|
+
*/
|
|
544
|
+
async function syncCommands(configSkills) {
|
|
545
|
+
if (configSkills.length === 0 || !existsSync(BUNDLED_SKILLS_DIR)) {
|
|
546
|
+
return { success: 0, fail: 0 }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const commandsDir = join(cwd(), COMMANDS_DIR)
|
|
550
|
+
await mkdir(commandsDir, { recursive: true })
|
|
551
|
+
|
|
552
|
+
let success = 0
|
|
553
|
+
let fail = 0
|
|
554
|
+
|
|
555
|
+
for (const skillId of configSkills) {
|
|
556
|
+
const id = canonicalSkillId(skillId)
|
|
557
|
+
const dirName = managedSkillDirName(skillId)
|
|
558
|
+
const srcSkillMd = join(BUNDLED_SKILLS_DIR, dirName, 'SKILL.md')
|
|
559
|
+
const destFile = join(commandsDir, `${RULE_PREFIX}${id}.md`)
|
|
560
|
+
|
|
561
|
+
process.stdout.write(` ⬇ ${id} → ${COMMANDS_DIR}/${RULE_PREFIX}${id}.md ... `)
|
|
562
|
+
if (existsSync(srcSkillMd)) {
|
|
563
|
+
try {
|
|
564
|
+
const raw = await readFile(srcSkillMd, 'utf8')
|
|
565
|
+
const desc = extractSkillDescription(raw)
|
|
566
|
+
const header = desc ? `# ${RULE_PREFIX}${id} — ${desc}\n\n` : ''
|
|
567
|
+
const body = `${header}Виконай інструкції зі скілу \`.cursor/skills/${dirName}/SKILL.md\`.\n`
|
|
568
|
+
await writeFile(destFile, body, 'utf8')
|
|
569
|
+
console.log(`✅`)
|
|
570
|
+
success++
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.log(`❌`)
|
|
573
|
+
console.error(` Помилка: ${error.message}`)
|
|
574
|
+
fail++
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
console.log(`❌`)
|
|
578
|
+
console.error(` Немає SKILL.md у пакеті: ${dirName}`)
|
|
579
|
+
fail++
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { success, fail }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Видаляє файли n-*.md у .claude/commands, яких немає у конфігурації skills
|
|
587
|
+
* @param {string} commandsDir абсолютний шлях до .claude/commands
|
|
588
|
+
* @param {string[]} configSkills id без префікса n-
|
|
589
|
+
* @returns {Promise<string[]>} імена видалених файлів
|
|
590
|
+
*/
|
|
591
|
+
async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
|
|
592
|
+
if (!existsSync(commandsDir)) {
|
|
593
|
+
return []
|
|
594
|
+
}
|
|
595
|
+
const expected = new Set(configSkills.map(s => `${RULE_PREFIX}${canonicalSkillId(s)}.md`))
|
|
596
|
+
const names = await readdir(commandsDir)
|
|
597
|
+
const removed = []
|
|
598
|
+
for (const name of names) {
|
|
599
|
+
if (name.endsWith('.md') && name.startsWith(RULE_PREFIX) && !expected.has(name)) {
|
|
600
|
+
await unlink(join(commandsDir, name))
|
|
601
|
+
removed.push(name)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
605
|
+
}
|
|
606
|
+
|
|
494
607
|
/**
|
|
495
608
|
* Знаходить доступні check-скрипти у каталозі scripts пакету
|
|
496
609
|
* @returns {Promise<string[]>} відсортовані імена правил (наприклад ['bun', 'ga', 'js-lint'])
|
|
@@ -711,6 +824,26 @@ async function runSync() {
|
|
|
711
824
|
throw error
|
|
712
825
|
}
|
|
713
826
|
|
|
827
|
+
try {
|
|
828
|
+
const { success: cmdOk, fail: cmdFail } = await syncCommands(skills)
|
|
829
|
+
if (skills.length > 0) {
|
|
830
|
+
console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
|
|
831
|
+
}
|
|
832
|
+
const removedCmds = await removeOrphanManagedCommandFiles(join(cwd(), COMMANDS_DIR), skills)
|
|
833
|
+
if (removedCmds.length > 0) {
|
|
834
|
+
console.log(`\n🧹 Видалено commands поза списком ${CONFIG_FILE} (${removedCmds.length}):`)
|
|
835
|
+
for (const name of removedCmds) {
|
|
836
|
+
console.log(` − ${COMMANDS_DIR}/${name}`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (cmdFail > 0) {
|
|
840
|
+
throw new Error(`Не вдалося скопіювати ${cmdFail} з ${skills.length} commands`)
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.error(`❌ Commands: ${error instanceof Error ? error.message : String(error)}`)
|
|
844
|
+
throw error
|
|
845
|
+
}
|
|
846
|
+
|
|
714
847
|
try {
|
|
715
848
|
await syncAgentsMd(skills)
|
|
716
849
|
} catch (error) {
|
|
@@ -718,6 +851,13 @@ async function runSync() {
|
|
|
718
851
|
throw error
|
|
719
852
|
}
|
|
720
853
|
|
|
854
|
+
try {
|
|
855
|
+
await syncClaudeMd(rules, skills)
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error(`❌ Не вдалося оновити CLAUDE.md: ${error instanceof Error ? error.message : String(error)}`)
|
|
858
|
+
throw error
|
|
859
|
+
}
|
|
860
|
+
|
|
721
861
|
console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
|
|
722
862
|
if (failCount > 0) {
|
|
723
863
|
throw new Error(`Не вдалося завантажити ${failCount} з ${rules.length} правил`)
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const DATREE_CRD_RAW_REF = 'main'
|
|
|
34
34
|
|
|
35
35
|
const DATREE_CRD_RAW_BASE = `https://raw.githubusercontent.com/datreeio/CRDs-catalog/${DATREE_CRD_RAW_REF}/`
|
|
36
36
|
|
|
37
|
-
/** У ключі `Map` означає «будь-який / відсутній `type`» (наприклад CRD без
|
|
37
|
+
/** У ключі `Map` означає «будь-який / відсутній `type`» (наприклад CRD без кореневого `type:`). */
|
|
38
38
|
const K8S_EXPLICIT_SCHEMA_TYPE_ANY = '*'
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -42,7 +42,7 @@ const K8S_EXPLICIT_SCHEMA_TYPE_ANY = '*'
|
|
|
42
42
|
* `typeKey` — значення поля **`type:`** або **`K8S_EXPLICIT_SCHEMA_TYPE_ANY`**.
|
|
43
43
|
* @param {string} apiVersion повне значення `apiVersion` з маніфесту
|
|
44
44
|
* @param {string} kind значення `kind` з маніфесту (як у YAML)
|
|
45
|
-
* @param {string} typeKey значення
|
|
45
|
+
* @param {string} typeKey значення кореневого `type:` або `K8S_EXPLICIT_SCHEMA_TYPE_ANY`
|
|
46
46
|
* @returns {string} внутрішній ключ для `Map`
|
|
47
47
|
*/
|
|
48
48
|
function k8sExplicitSchemaMapKey(apiVersion, kind, typeKey) {
|
|
@@ -52,7 +52,6 @@ function k8sExplicitSchemaMapKey(apiVersion, kind, typeKey) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Таблиця явних `$schema` для поєднань **`apiVersion` + `kind` + `type`** (див. k8s.mdc).
|
|
54
54
|
* Щоб додати рядок: визнач **`apiVersion`**, **`kind`**, при потребі **`type`**, вкажи **URL** і **reason**.
|
|
55
|
-
*
|
|
56
55
|
* @type {Map<string, { schema: string, reason: string }>}
|
|
57
56
|
*/
|
|
58
57
|
const EXPLICIT_K8S_SCHEMAS = new Map([
|
|
@@ -73,7 +72,7 @@ const EXPLICIT_K8S_SCHEMAS = new Map([
|
|
|
73
72
|
])
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
|
-
* Витягує
|
|
75
|
+
* Витягує кореневе поле **`type:`** з документа (без повного YAML-парсера).
|
|
77
76
|
* @param {string} doc фрагмент YAML одного документа
|
|
78
77
|
* @returns {string | undefined} значення без лапок або undefined, якщо поля немає
|
|
79
78
|
*/
|
|
@@ -87,7 +86,7 @@ function extractTopLevelManifestType(doc) {
|
|
|
87
86
|
* Шукає схему в **`EXPLICIT_K8S_SCHEMAS`**: спочатку за точним **`type`**, потім за **`*`**.
|
|
88
87
|
* @param {string} apiVersion повне значення `apiVersion` з маніфесту
|
|
89
88
|
* @param {string} kind значення `kind` з маніфесту (як у YAML)
|
|
90
|
-
* @param {string | undefined} manifestType
|
|
89
|
+
* @param {string | undefined} manifestType кореневе поле `type` або undefined, якщо відсутнє
|
|
91
90
|
* @returns {{ schema: string, reason: string } | null} запис таблиці або null, якщо збігу немає
|
|
92
91
|
*/
|
|
93
92
|
function lookupExplicitK8sSchema(apiVersion, kind, manifestType) {
|