@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.
- package/bin/auto-rules.md +45 -0
- package/bin/n-cursor.js +270 -149
- package/mdc/graphql.mdc +15 -1
- package/mdc/k8s.mdc +11 -10
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +558 -553
- package/scripts/check-bun.mjs +106 -82
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +267 -186
- package/scripts/check-k8s.mjs +1148 -673
- package/scripts/check-nginx-default-tpl.mjs +125 -100
- package/scripts/check-npm-module.mjs +165 -118
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +288 -210
- package/scripts/check-vue.mjs +110 -69
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +92 -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,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
|
|
174
|
-
const
|
|
175
|
-
|
|
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} з
|
|
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
|
-
|
|
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 }
|
|
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(`📝 Оновлено
|
|
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
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
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
|
|
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
|
-
*
|
|
382
|
-
*
|
|
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(
|
|
491
|
+
async function buildSkillBulletItems() {
|
|
386
492
|
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
493
|
+
const skillDirNames = await listProjectSkillDirNames()
|
|
387
494
|
const items = []
|
|
388
|
-
for (const
|
|
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(
|
|
574
|
+
async function syncClaudeMd() {
|
|
438
575
|
const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
|
|
576
|
+
const mdcFiles = await listProjectRulesMdcFiles()
|
|
439
577
|
|
|
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
|
-
}
|
|
578
|
+
for (const mdcFile of mdcFiles) {
|
|
579
|
+
lines.push(`@${RULES_DIR}/${mdcFile}`)
|
|
461
580
|
}
|
|
462
581
|
|
|
463
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1000
|
+
logRemovedManagedItems('правила', RULES_DIR, removed)
|
|
1001
|
+
})
|
|
854
1002
|
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
871
|
-
console.error(`❌ Skills: ${error.message}`)
|
|
872
|
-
throw error
|
|
873
|
-
}
|
|
1013
|
+
})
|
|
874
1014
|
|
|
875
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
903
|
-
|
|
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
|
|
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
|
|