@nitra/cursor 1.18.2 → 1.19.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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.19.0] - 2026-05-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Pi.dev інтеграція** — CLI під час синку генерує `.pi/skills/<dir>/SKILL.md` для кожного скілу з `.cursor/skills/<dir>/` із frontmatter `name`+`description` (формат pi.dev: 1-64 chars, `[a-z0-9-]`). Тіло — делегат `Виконай інструкції зі скілу .cursor/skills/<dir>/SKILL.md.`, симетрично до `.claude/commands/<dir>.md`. Always-on, без флагу. Покриває керовані (з пакета) і локальні скіли; orphan-cleanup видаляє `.pi/skills/n-*` дири, яких немає у конфігу, і локальні дири, яких більше немає у `.cursor/skills/`.
|
|
12
|
+
- `npm/bin/n-cursor.js`: константа `PI_SKILLS_DIR='.pi/skills'`, функція `formatPiSkillFrontmatter(name, desc)`, синки `syncPiSkills`/`syncLocalOnlyPiSkills` + cleanups `removeOrphanManagedPiSkillDirs`/`removeOrphanLocalPiSkillDirs`. Новий `runSyncStep('❌ Pi skills: ', …)` після Commands-блоку у головному потоці.
|
|
13
|
+
|
|
14
|
+
## [1.18.3] - 2026-05-25
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Canonical `.cargo/mutants.toml` baseline: `additional_cargo_test_args = ["--lib", "--tests"]` — виключає `--bins` і `--doc` фази, які перебудовують Tauri-бінарник та doc-tests при кожному мутанті, збільшуючи час з секунд до хвилин.
|
|
19
|
+
|
|
7
20
|
## [1.18.2] - 2026-05-25
|
|
8
21
|
|
|
9
22
|
### Fixed
|
package/bin/n-cursor.js
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* синхронізує `.claude/settings.json` (hooks + permissions; merge — користувацькі поля зберігаються)
|
|
29
29
|
* і `.cursor/hooks.json` (Cursor Agent hooks; merge — користувацькі hooks зберігаються).
|
|
30
30
|
* Опт-аут — поле `claude-config: false` у `.n-cursor.json`.
|
|
31
|
+
* Pi.dev інтеграція: для кожного skill у `.cursor/skills/<dir>/` CLI генерує
|
|
32
|
+
* `.pi/skills/<dir>/SKILL.md` із frontmatter `name`+`description` (формат pi.dev). Тіло — делегат
|
|
33
|
+
* на джерельний `.cursor/skills/<dir>/SKILL.md`. Always-on, симетрично до `.claude/commands/`.
|
|
31
34
|
*
|
|
32
35
|
* Якщо у корені репозиторію немає .n-cursor.json, спочатку перейменовується за наявності nitra-cursor.json;
|
|
33
36
|
* у `.cursor/rules` файли `nitra-*.mdc` перейменовуються на `n-*.mdc`; інакше конфіг створюється автоматично
|
|
@@ -104,6 +107,7 @@ const AGENTS_TEMPLATE_FILE = 'AGENTS.template.md'
|
|
|
104
107
|
const RULES_DIR = '.cursor/rules'
|
|
105
108
|
const SKILLS_DIR = '.cursor/skills'
|
|
106
109
|
const COMMANDS_DIR = '.claude/commands'
|
|
110
|
+
const PI_SKILLS_DIR = '.pi/skills'
|
|
107
111
|
const RULE_PREFIX = 'n-'
|
|
108
112
|
|
|
109
113
|
const binDir = dirname(fileURLToPath(import.meta.url))
|
|
@@ -486,6 +490,22 @@ function formatClaudeCommandFrontmatter(descriptionRaw) {
|
|
|
486
490
|
return `---\ndescription: >-\n ${text}\n---\n\n`
|
|
487
491
|
}
|
|
488
492
|
|
|
493
|
+
/**
|
|
494
|
+
* YAML frontmatter для `.pi/skills/<dir>/SKILL.md` згідно зі специфікацією pi.dev:
|
|
495
|
+
* обов'язкові поля `name` (1-64, `[a-z0-9-]`) і `description` (≤ 1024). Текст description збігається
|
|
496
|
+
* з полем `description` у frontmatter джерельного `SKILL.md`.
|
|
497
|
+
* @param {string} skillName ім'я скілу (наприклад `n-fix`); має бути валідним pi-name
|
|
498
|
+
* @param {string} descriptionRaw значення з `extractSkillDescription` (може бути порожнім)
|
|
499
|
+
* @returns {string} блок `---` … `---` і порожній рядок після
|
|
500
|
+
*/
|
|
501
|
+
function formatPiSkillFrontmatter(skillName, descriptionRaw) {
|
|
502
|
+
let text = skillDescriptionSafeForMarkdownInline(String(descriptionRaw || '').trim())
|
|
503
|
+
if (!text) {
|
|
504
|
+
text = 'Див. SKILL.md у каталозі скілу в .cursor/skills.'
|
|
505
|
+
}
|
|
506
|
+
return `---\nname: ${skillName}\ndescription: >-\n ${text}\n---\n\n`
|
|
507
|
+
}
|
|
508
|
+
|
|
489
509
|
/**
|
|
490
510
|
* Повертає відсортовані імена *.mdc у .cursor/rules поточного проєкту
|
|
491
511
|
* @returns {Promise<string[]>} базові імена файлів (лише .mdc)
|
|
@@ -901,6 +921,149 @@ async function removeOrphanLocalSkillCommandFiles(commandsDir, configSkills) {
|
|
|
901
921
|
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
902
922
|
}
|
|
903
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Синхронізує .pi/skills/n-<id>/SKILL.md зі skills пакету для pi.dev-сумісності.
|
|
926
|
+
* Pi-skill — це директорія з SKILL.md (frontmatter `name`+`description`), тіло-делегат на джерельний
|
|
927
|
+
* `.cursor/skills/<dir>/SKILL.md`. Симетрично до `syncCommands`, але дир замість `.md`-файлу.
|
|
928
|
+
* @param {string[]} configSkills id без префікса n-
|
|
929
|
+
* @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету-джерела
|
|
930
|
+
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
|
|
931
|
+
*/
|
|
932
|
+
async function syncPiSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
933
|
+
if (configSkills.length === 0 || !existsSync(bundledSkillsDir)) {
|
|
934
|
+
return { success: 0, fail: 0 }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const piSkillsRoot = join(cwd(), PI_SKILLS_DIR)
|
|
938
|
+
await mkdir(piSkillsRoot, { recursive: true })
|
|
939
|
+
|
|
940
|
+
let success = 0
|
|
941
|
+
let fail = 0
|
|
942
|
+
|
|
943
|
+
for (const skillId of configSkills) {
|
|
944
|
+
const id = normalizeSkillId(skillId)
|
|
945
|
+
const srcSkillMd = join(bundledSkillsDir, id, 'SKILL.md')
|
|
946
|
+
const destDirName = managedSkillDirName(skillId)
|
|
947
|
+
const destDir = join(piSkillsRoot, destDirName)
|
|
948
|
+
const destFile = join(destDir, 'SKILL.md')
|
|
949
|
+
|
|
950
|
+
process.stdout.write(` ⬇ ${id} → ${PI_SKILLS_DIR}/${destDirName}/SKILL.md ... `)
|
|
951
|
+
if (existsSync(srcSkillMd)) {
|
|
952
|
+
try {
|
|
953
|
+
const raw = await readFile(srcSkillMd, 'utf8')
|
|
954
|
+
const descRaw = extractSkillDescription(raw)
|
|
955
|
+
await mkdir(destDir, { recursive: true })
|
|
956
|
+
const frontmatter = formatPiSkillFrontmatter(destDirName, descRaw || '')
|
|
957
|
+
const header = `# ${destDirName}\n\n`
|
|
958
|
+
const body = `${frontmatter}${header}Виконай інструкції зі скілу \`.cursor/skills/${destDirName}/SKILL.md\`.\n`
|
|
959
|
+
await writeFile(destFile, body, 'utf8')
|
|
960
|
+
console.log(`✅`)
|
|
961
|
+
success++
|
|
962
|
+
} catch (error) {
|
|
963
|
+
console.log(`❌`)
|
|
964
|
+
console.error(` Помилка: ${errorMessage(error)}`)
|
|
965
|
+
fail++
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
console.log(`❌`)
|
|
969
|
+
console.error(` Немає SKILL.md у пакеті: skills/${id}`)
|
|
970
|
+
fail++
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return { success, fail }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Синхронізує .pi/skills/{dirName}/SKILL.md для всіх локальних скілів з .cursor/skills/
|
|
978
|
+
* що не керуються пакетом. Симетрично до `syncLocalOnlySkillCommands`.
|
|
979
|
+
* @param {string[]} configSkills id керованих skills (уже оброблені syncPiSkills)
|
|
980
|
+
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
|
|
981
|
+
*/
|
|
982
|
+
async function syncLocalOnlyPiSkills(configSkills) {
|
|
983
|
+
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
984
|
+
if (!existsSync(skillsRoot)) return { success: 0, fail: 0 }
|
|
985
|
+
|
|
986
|
+
const piSkillsRoot = join(cwd(), PI_SKILLS_DIR)
|
|
987
|
+
await mkdir(piSkillsRoot, { recursive: true })
|
|
988
|
+
|
|
989
|
+
const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
|
|
990
|
+
const allDirNames = await listProjectSkillDirNames()
|
|
991
|
+
const localOnly = allDirNames.filter(d => !managedDirNames.has(d))
|
|
992
|
+
|
|
993
|
+
let success = 0
|
|
994
|
+
let fail = 0
|
|
995
|
+
|
|
996
|
+
for (const dirName of localOnly) {
|
|
997
|
+
const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
|
|
998
|
+
const destDir = join(piSkillsRoot, dirName)
|
|
999
|
+
const destFile = join(destDir, 'SKILL.md')
|
|
1000
|
+
|
|
1001
|
+
process.stdout.write(` ⬇ ${dirName} → ${PI_SKILLS_DIR}/${dirName}/SKILL.md ... `)
|
|
1002
|
+
try {
|
|
1003
|
+
let descRaw = ''
|
|
1004
|
+
if (existsSync(skillMdPath)) {
|
|
1005
|
+
const raw = await readFile(skillMdPath, 'utf8')
|
|
1006
|
+
const parsed = extractSkillDescription(raw)
|
|
1007
|
+
if (parsed) descRaw = parsed
|
|
1008
|
+
}
|
|
1009
|
+
await mkdir(destDir, { recursive: true })
|
|
1010
|
+
const frontmatter = formatPiSkillFrontmatter(dirName, descRaw)
|
|
1011
|
+
const header = `# ${dirName}\n\n`
|
|
1012
|
+
const body = `${frontmatter}${header}Виконай інструкції зі скілу \`${SKILLS_DIR}/${dirName}/SKILL.md\`.\n`
|
|
1013
|
+
await writeFile(destFile, body, 'utf8')
|
|
1014
|
+
console.log(`✅`)
|
|
1015
|
+
success++
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.log(`❌`)
|
|
1018
|
+
console.error(` Помилка: ${errorMessage(error)}`)
|
|
1019
|
+
fail++
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { success, fail }
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Видаляє n-* директорії у .pi/skills, яких немає у конфігурації skills.
|
|
1027
|
+
* @param {string} piSkillsDir абсолютний шлях до .pi/skills
|
|
1028
|
+
* @param {string[]} configSkills id без префікса n-
|
|
1029
|
+
* @returns {Promise<string[]>} імена видалених директорій
|
|
1030
|
+
*/
|
|
1031
|
+
async function removeOrphanManagedPiSkillDirs(piSkillsDir, configSkills) {
|
|
1032
|
+
if (!existsSync(piSkillsDir)) return []
|
|
1033
|
+
const expected = new Set(configSkills.map(s => managedSkillDirName(s)))
|
|
1034
|
+
const entries = await readdir(piSkillsDir, { withFileTypes: true })
|
|
1035
|
+
const removed = []
|
|
1036
|
+
for (const entry of entries) {
|
|
1037
|
+
if (entry.isDirectory() && entry.name.startsWith(RULE_PREFIX) && !expected.has(entry.name)) {
|
|
1038
|
+
await rm(join(piSkillsDir, entry.name), { recursive: true, force: true })
|
|
1039
|
+
removed.push(entry.name)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Видаляє .pi/skills/{dirName} директорії локальних скілів, яких більше немає в .cursor/skills/.
|
|
1047
|
+
* @param {string} piSkillsDir абсолютний шлях до .pi/skills
|
|
1048
|
+
* @param {string[]} configSkills id керованих skills
|
|
1049
|
+
* @returns {Promise<string[]>} імена видалених директорій
|
|
1050
|
+
*/
|
|
1051
|
+
async function removeOrphanLocalPiSkillDirs(piSkillsDir, configSkills) {
|
|
1052
|
+
if (!existsSync(piSkillsDir)) return []
|
|
1053
|
+
const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
|
|
1054
|
+
const allDirNames = new Set(await listProjectSkillDirNames())
|
|
1055
|
+
const entries = await readdir(piSkillsDir, { withFileTypes: true })
|
|
1056
|
+
const removed = []
|
|
1057
|
+
for (const entry of entries) {
|
|
1058
|
+
if (!entry.isDirectory() || entry.name.startsWith(RULE_PREFIX)) continue
|
|
1059
|
+
if (!managedDirNames.has(entry.name) && !allDirNames.has(entry.name)) {
|
|
1060
|
+
await rm(join(piSkillsDir, entry.name), { recursive: true, force: true })
|
|
1061
|
+
removed.push(entry.name)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
1065
|
+
}
|
|
1066
|
+
|
|
904
1067
|
/**
|
|
905
1068
|
* Людинозрозумілий текст винятку для логів.
|
|
906
1069
|
* @param {unknown} error виняток із catch
|
|
@@ -1184,6 +1347,24 @@ async function runSync() {
|
|
|
1184
1347
|
}
|
|
1185
1348
|
})
|
|
1186
1349
|
|
|
1350
|
+
await runSyncStep('❌ Pi skills: ', async () => {
|
|
1351
|
+
const { success: piOk, fail: piFail } = await syncPiSkills(skills, bundledSkillsDir)
|
|
1352
|
+
const { success: piLocalOk, fail: piLocalFail } = await syncLocalOnlyPiSkills(skills)
|
|
1353
|
+
const totalOk = piOk + piLocalOk
|
|
1354
|
+
const totalFail = piFail + piLocalFail
|
|
1355
|
+
if (totalOk + totalFail > 0) {
|
|
1356
|
+
console.log(`\n🥧 Pi skills: ${totalOk} скопійовано, ${totalFail} з помилками`)
|
|
1357
|
+
}
|
|
1358
|
+
const piSkillsDir = join(cwd(), PI_SKILLS_DIR)
|
|
1359
|
+
const removedPi = await removeOrphanManagedPiSkillDirs(piSkillsDir, skills)
|
|
1360
|
+
logRemovedManagedItems('pi skills', PI_SKILLS_DIR, removedPi)
|
|
1361
|
+
const removedLocalPi = await removeOrphanLocalPiSkillDirs(piSkillsDir, skills)
|
|
1362
|
+
logRemovedManagedItems('pi skills (local)', PI_SKILLS_DIR, removedLocalPi)
|
|
1363
|
+
if (totalFail > 0) {
|
|
1364
|
+
throw new Error(`Не вдалося скопіювати ${totalFail} pi skills`)
|
|
1365
|
+
}
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1187
1368
|
await runSyncStep(`❌ Не вдалося оновити ${AGENTS_FILE}: `, () => syncAgentsMd(bundledAgentsTemplatePath))
|
|
1188
1369
|
await runSyncStep('❌ Не вдалося оновити CLAUDE.md: ', () =>
|
|
1189
1370
|
syncClaudeMd(/** @type {string[] | undefined} */ (ignore))
|
package/package.json
CHANGED
|
@@ -2,3 +2,7 @@
|
|
|
2
2
|
# cargo-mutants має робочі defaults; цей файл — стартова точка для customization.
|
|
3
3
|
# Документація: https://mutants.rs/
|
|
4
4
|
# Канон постачає правило `test` (@nitra/cursor).
|
|
5
|
+
|
|
6
|
+
# Виключаємо --bins і --doc: бінарник Tauri та doc-tests щоразу перезбираються
|
|
7
|
+
# з нуля навіть з кешем, що збільшує час кожного мутанту з секунд до хвилин.
|
|
8
|
+
additional_cargo_test_args = ["--lib", "--tests"]
|