@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.
- package/bin/auto-rules.md +45 -0
- package/bin/n-cursor.js +280 -149
- package/mdc/graphql.mdc +15 -1
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +54 -36
- package/scripts/check-bun.mjs +2 -6
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +15 -11
- package/scripts/check-k8s.mjs +1652 -627
- package/scripts/check-nginx-default-tpl.mjs +17 -10
- package/scripts/check-npm-module.mjs +3 -3
- package/scripts/check-text.mjs +1 -3
- package/scripts/check-vue.mjs +2 -2
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +90 -72
- package/scripts/utils/workspaces.mjs +39 -16
|
@@ -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
|
|
174
|
-
const
|
|
175
|
-
|
|
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} з
|
|
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
|
-
|
|
190
|
-
|
|
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(`📝 Оновлено
|
|
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
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
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
|
|
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
|
-
*
|
|
382
|
-
*
|
|
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(
|
|
500
|
+
async function buildSkillBulletItems() {
|
|
386
501
|
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
502
|
+
const skillDirNames = await listProjectSkillDirNames()
|
|
387
503
|
const items = []
|
|
388
|
-
for (const
|
|
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(
|
|
583
|
+
async function syncClaudeMd() {
|
|
438
584
|
const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
|
|
585
|
+
const mdcFiles = await listProjectRulesMdcFiles()
|
|
439
586
|
|
|
440
|
-
for (const
|
|
441
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
845
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
871
|
-
console.error(`❌ Skills: ${error.message}`)
|
|
872
|
-
throw error
|
|
873
|
-
}
|
|
1023
|
+
})
|
|
874
1024
|
|
|
875
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
903
|
-
|
|
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
|
|
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
|
|