@nitra/cursor 1.8.30 → 1.8.40
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/README.md +2 -1
- package/bin/n-cursor.js +37 -40
- package/mdc/abie.mdc +56 -0
- package/mdc/bun.mdc +7 -20
- package/mdc/js-lint.mdc +3 -3
- package/mdc/k8s.mdc +1 -1
- package/mdc/text.mdc +2 -2
- package/package.json +1 -1
- package/schemas/n-cursor.json +1 -1
- package/scripts/check-abie.mjs +468 -0
- package/scripts/check-js-lint.mjs +4 -6
- package/scripts/run-v8r.mjs +1 -1
- package/skills/abie-kustomize/SKILL.md +26 -0
- package/skills/{n-fix → fix}/SKILL.md +1 -1
- package/skills/{n-publish-telegram → publish-telegram}/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,11 @@ CLI автоматично (команда завантаження правил
|
|
|
85
85
|
```
|
|
86
86
|
npm/
|
|
87
87
|
├── AGENTS.template.md # шаблон AGENTS.md для цільових репозиторіїв (потрапляє в npm-архів)
|
|
88
|
-
├── mdc/ # cursor-правила
|
|
88
|
+
├── mdc/ # cursor-правила (без префікса n-; після синку — .cursor/rules/n-<id>.mdc)
|
|
89
89
|
│ ├── js-format.mdc
|
|
90
90
|
│ ├── npm-module.mdc
|
|
91
91
|
│ └── text.mdc
|
|
92
|
+
├── skills/ # skills (каталоги <id>/; після синку — .cursor/skills/n-<id>/)
|
|
92
93
|
└── bin/
|
|
93
94
|
└── n-cursor.js # CLI-скрипт
|
|
94
95
|
```
|
package/bin/n-cursor.js
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* `github-actions/` пакету при кожному успішному синку (workflows з правил ga / js-lint / text).
|
|
25
25
|
*
|
|
26
26
|
* Skills копіюються з npm/skills пакету лише для id з масиву «skills» у .n-cursor.json
|
|
27
|
-
* (у JSON — без префікса,
|
|
28
|
-
*
|
|
27
|
+
* (у JSON — без префікса, як імена файлів у mdc/ без n-). У пакеті джерело — каталоги
|
|
28
|
+
* skills/<id>/ (без префікса); у проєкті — .cursor/skills/n-<id>/ (префікс n-, як n-*.mdc).
|
|
29
|
+
* Якщо ключа skills немає, за замовчуванням підтягуються всі підкаталоги skills/ (лише імена без префікса n-).
|
|
29
30
|
* Зайві каталоги n-* у .cursor/skills, яких немає у списку, видаляються.
|
|
30
31
|
*
|
|
31
32
|
* Якщо в корені є package.json і в ньому ще немає \@nitra/cursor у devDependencies (і не оголошено
|
|
@@ -87,7 +88,7 @@ async function discoverBundledRuleNames() {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
* Імена skills (без префікса n-) з каталогу skills пакету — лише
|
|
91
|
+
* Імена skills (id без префікса n-) з каталогу skills пакету — лише підкаталоги `<id>/` без префікса n-
|
|
91
92
|
* @returns {Promise<string[]>} відсортовані id
|
|
92
93
|
*/
|
|
93
94
|
async function discoverBundledSkillNames() {
|
|
@@ -96,8 +97,8 @@ async function discoverBundledSkillNames() {
|
|
|
96
97
|
}
|
|
97
98
|
const entries = await readdir(BUNDLED_SKILLS_DIR, { withFileTypes: true })
|
|
98
99
|
return entries
|
|
99
|
-
.filter(e => e.isDirectory() && e.name.startsWith(RULE_PREFIX))
|
|
100
|
-
.map(e => e.name
|
|
100
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith(RULE_PREFIX))
|
|
101
|
+
.map(e => e.name)
|
|
101
102
|
.toSorted((a, b) => a.localeCompare(b))
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -233,28 +234,13 @@ function normalizeSkillId(skillName) {
|
|
|
233
234
|
return s
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
/** Legacy id у `.n-cursor.json` → поточний bundled id (каталог `n-<id>` у пакеті) */
|
|
237
|
-
const LEGACY_SKILL_ID_MAP = {
|
|
238
|
-
'fix-cursor': 'fix'
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Поточний id skill для шляхів у пакеті та `.cursor/skills`
|
|
243
|
-
* @param {string} skillName елемент масиву skills або ім'я каталогу
|
|
244
|
-
* @returns {string} canonical id без префікса n-
|
|
245
|
-
*/
|
|
246
|
-
function canonicalSkillId(skillName) {
|
|
247
|
-
const id = normalizeSkillId(skillName)
|
|
248
|
-
return LEGACY_SKILL_ID_MAP[id] ?? id
|
|
249
|
-
}
|
|
250
|
-
|
|
251
237
|
/**
|
|
252
238
|
* Ім'я керованого каталогу skill у .cursor/skills (префікс n-)
|
|
253
|
-
* @param {string} skillId id без префікса
|
|
239
|
+
* @param {string} skillId id без префікса (або з префіксом n- у конфігу — нормалізується)
|
|
254
240
|
* @returns {string} наприклад n-fix
|
|
255
241
|
*/
|
|
256
242
|
function managedSkillDirName(skillId) {
|
|
257
|
-
return `${RULE_PREFIX}${
|
|
243
|
+
return `${RULE_PREFIX}${normalizeSkillId(skillId)}`
|
|
258
244
|
}
|
|
259
245
|
|
|
260
246
|
/**
|
|
@@ -279,6 +265,16 @@ function extractSkillDescription(text) {
|
|
|
279
265
|
.trim()
|
|
280
266
|
}
|
|
281
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Підготовка опису skill для вставки в звичайний markdown (заголовок H1, bullet без code fence).
|
|
270
|
+
* Послідовність `<id>` сприймається markdownlint (MD033) як inline HTML — замінюємо на `{id}`.
|
|
271
|
+
* @param {string} desc один рядок з YAML frontmatter SKILL.md
|
|
272
|
+
* @returns {string} той самий рядок після заміни літералу з кутовими дужками навколо id на плейсхолдер у фігурних дужках (MD033).
|
|
273
|
+
*/
|
|
274
|
+
function skillDescriptionSafeForMarkdownInline(desc) {
|
|
275
|
+
return desc.replaceAll('<id>', '{id}')
|
|
276
|
+
}
|
|
277
|
+
|
|
282
278
|
/**
|
|
283
279
|
* Розгортає в шаблоні блок Mustache {{#section}} … {{/section}} для масиву елементів
|
|
284
280
|
* @param {string} template вихідний текст шаблону
|
|
@@ -382,7 +378,7 @@ async function buildSkillBulletItems(skillIds) {
|
|
|
382
378
|
const text = await readFile(skillMdPath, 'utf8')
|
|
383
379
|
const parsed = extractSkillDescription(text)
|
|
384
380
|
if (parsed) {
|
|
385
|
-
desc = parsed
|
|
381
|
+
desc = skillDescriptionSafeForMarkdownInline(parsed)
|
|
386
382
|
}
|
|
387
383
|
}
|
|
388
384
|
const pathLine = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
@@ -435,14 +431,14 @@ async function syncClaudeMd(configRules, configSkills) {
|
|
|
435
431
|
lines.push('', '## Skills', '')
|
|
436
432
|
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
437
433
|
for (const skillId of configSkills) {
|
|
438
|
-
const id =
|
|
434
|
+
const id = normalizeSkillId(skillId)
|
|
439
435
|
const dirName = managedSkillDirName(skillId)
|
|
440
436
|
const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
|
|
441
437
|
let desc = ''
|
|
442
438
|
if (existsSync(skillMdPath)) {
|
|
443
439
|
const text = await readFile(skillMdPath, 'utf8')
|
|
444
440
|
const parsed = extractSkillDescription(text)
|
|
445
|
-
if (parsed) desc = parsed
|
|
441
|
+
if (parsed) desc = skillDescriptionSafeForMarkdownInline(parsed)
|
|
446
442
|
}
|
|
447
443
|
const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
448
444
|
lines.push(desc ? `${ref} — ${desc}` : ref, ` Команда: \`/${RULE_PREFIX}${id}\``)
|
|
@@ -485,7 +481,7 @@ async function syncAgentsMd(configSkills) {
|
|
|
485
481
|
}
|
|
486
482
|
|
|
487
483
|
/**
|
|
488
|
-
* Копіює лише skills зі списку configSkills (
|
|
484
|
+
* Копіює лише skills зі списку configSkills (джерело: skills/<id>/ у пакеті)
|
|
489
485
|
* @param {string[]} configSkills id без префікса n-
|
|
490
486
|
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих копіювань
|
|
491
487
|
*/
|
|
@@ -501,13 +497,13 @@ async function syncSkills(configSkills) {
|
|
|
501
497
|
let fail = 0
|
|
502
498
|
|
|
503
499
|
for (const skillId of configSkills) {
|
|
504
|
-
const id =
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const destDir = join(skillsRoot,
|
|
500
|
+
const id = normalizeSkillId(skillId)
|
|
501
|
+
const srcDir = join(BUNDLED_SKILLS_DIR, id)
|
|
502
|
+
const destDirName = managedSkillDirName(skillId)
|
|
503
|
+
const destDir = join(skillsRoot, destDirName)
|
|
508
504
|
|
|
509
505
|
if (existsSync(srcDir)) {
|
|
510
|
-
process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${
|
|
506
|
+
process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${destDirName} ... `)
|
|
511
507
|
try {
|
|
512
508
|
await mkdir(destDir, { recursive: true })
|
|
513
509
|
const files = await readdir(srcDir)
|
|
@@ -523,9 +519,9 @@ async function syncSkills(configSkills) {
|
|
|
523
519
|
fail++
|
|
524
520
|
}
|
|
525
521
|
} else {
|
|
526
|
-
process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${
|
|
522
|
+
process.stdout.write(` ⬇ ${id} → ${SKILLS_DIR}/${destDirName} ... `)
|
|
527
523
|
console.log(`❌`)
|
|
528
|
-
console.error(` Немає каталогу в пакеті:
|
|
524
|
+
console.error(` Немає каталогу в пакеті: skills/${id}`)
|
|
529
525
|
fail++
|
|
530
526
|
}
|
|
531
527
|
}
|
|
@@ -550,18 +546,19 @@ async function syncCommands(configSkills) {
|
|
|
550
546
|
let fail = 0
|
|
551
547
|
|
|
552
548
|
for (const skillId of configSkills) {
|
|
553
|
-
const id =
|
|
554
|
-
const
|
|
555
|
-
const
|
|
549
|
+
const id = normalizeSkillId(skillId)
|
|
550
|
+
const srcSkillMd = join(BUNDLED_SKILLS_DIR, id, 'SKILL.md')
|
|
551
|
+
const destDirName = managedSkillDirName(skillId)
|
|
556
552
|
const destFile = join(commandsDir, `${RULE_PREFIX}${id}.md`)
|
|
557
553
|
|
|
558
554
|
process.stdout.write(` ⬇ ${id} → ${COMMANDS_DIR}/${RULE_PREFIX}${id}.md ... `)
|
|
559
555
|
if (existsSync(srcSkillMd)) {
|
|
560
556
|
try {
|
|
561
557
|
const raw = await readFile(srcSkillMd, 'utf8')
|
|
562
|
-
const
|
|
558
|
+
const descRaw = extractSkillDescription(raw)
|
|
559
|
+
const desc = descRaw ? skillDescriptionSafeForMarkdownInline(descRaw) : ''
|
|
563
560
|
const header = desc ? `# ${RULE_PREFIX}${id} — ${desc}\n\n` : ''
|
|
564
|
-
const body = `${header}Виконай інструкції зі скілу \`.cursor/skills/${
|
|
561
|
+
const body = `${header}Виконай інструкції зі скілу \`.cursor/skills/${destDirName}/SKILL.md\`.\n`
|
|
565
562
|
await writeFile(destFile, body, 'utf8')
|
|
566
563
|
console.log(`✅`)
|
|
567
564
|
success++
|
|
@@ -572,7 +569,7 @@ async function syncCommands(configSkills) {
|
|
|
572
569
|
}
|
|
573
570
|
} else {
|
|
574
571
|
console.log(`❌`)
|
|
575
|
-
console.error(` Немає SKILL.md у пакеті:
|
|
572
|
+
console.error(` Немає SKILL.md у пакеті: skills/${id}`)
|
|
576
573
|
fail++
|
|
577
574
|
}
|
|
578
575
|
}
|
|
@@ -589,7 +586,7 @@ async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
|
|
|
589
586
|
if (!existsSync(commandsDir)) {
|
|
590
587
|
return []
|
|
591
588
|
}
|
|
592
|
-
const expected = new Set(configSkills.map(s => `${RULE_PREFIX}${
|
|
589
|
+
const expected = new Set(configSkills.map(s => `${RULE_PREFIX}${normalizeSkillId(s)}.md`))
|
|
593
590
|
const names = await readdir(commandsDir)
|
|
594
591
|
const removed = []
|
|
595
592
|
for (const name of names) {
|
package/mdc/abie.mdc
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Правила для проєктів abinbevefes
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## k8s
|
|
8
|
+
|
|
9
|
+
Якщо в проекті є k8s deployment, рядом з ним повинен бути:
|
|
10
|
+
|
|
11
|
+
```yaml title="hc.yaml"
|
|
12
|
+
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
|
13
|
+
apiVersion: networking.gke.io/v1
|
|
14
|
+
kind: HealthCheckPolicy
|
|
15
|
+
metadata:
|
|
16
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
17
|
+
namespace: dev # буде замінено через kustomize
|
|
18
|
+
spec:
|
|
19
|
+
default:
|
|
20
|
+
config:
|
|
21
|
+
type: HTTP
|
|
22
|
+
httpHealthCheck:
|
|
23
|
+
requestPath: /healthz
|
|
24
|
+
port: 8080
|
|
25
|
+
targetRef:
|
|
26
|
+
group: ''
|
|
27
|
+
kind: Service
|
|
28
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
і підключення до kustomize. Але у директорії ru повинен бути файл kustomization.yaml з patch, що видаляє ресурс **HealthCheckPolicy**.
|
|
32
|
+
|
|
33
|
+
```yaml title="kustomization.yaml"
|
|
34
|
+
patches:
|
|
35
|
+
- target:
|
|
36
|
+
kind: HealthCheckPolicy
|
|
37
|
+
patch: |-
|
|
38
|
+
kind: HealthCheckPolicy
|
|
39
|
+
metadata:
|
|
40
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
41
|
+
$patch: delete
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## branch
|
|
45
|
+
|
|
46
|
+
В lean-merged-branch.yml список гілок, які повинні бути:
|
|
47
|
+
|
|
48
|
+
```yaml title=".github/workflows/clean-merged-branch.yml"
|
|
49
|
+
ignore_branches: dev,ua,ru
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Перевірка
|
|
53
|
+
|
|
54
|
+
`npx @nitra/cursor check abie`
|
|
55
|
+
|
|
56
|
+
Програмна перевірка (**`check-abie.mjs`**) виконується лише якщо у **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень (щоб не вимагати **ua**/**ru** у репозиторіях без цього правила).
|
package/mdc/bun.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Bun як єдиний package manager у монорепо
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.6'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Проект використовує тільки Bun для керування залежностями та запуску скриптів.
|
|
@@ -63,29 +63,16 @@ FROM oven/bun:alpine AS build-env
|
|
|
63
63
|
|
|
64
64
|
замість образу node
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
У **GitHub Actions** не вставляй у workflow окремі кроки **`actions/setup-node`**, **`oven-sh/setup-bun`**, **`actions/cache`** та **`bun install`** — їх **заборонено** дублювати в кожному job; завжди використовуй **локальний composite** (деталі й заборона дублювання — **ga.mdc**). Під капотом composite уже містить Node **24**, Bun, кеш і **`bun install --frozen-lockfile`**.
|
|
67
|
+
|
|
68
|
+
Після **`actions/checkout@v6`** (`persist-credentials: false`):
|
|
67
69
|
|
|
68
70
|
```yaml
|
|
69
|
-
- uses: actions/setup-
|
|
70
|
-
with:
|
|
71
|
-
node-version: '24'
|
|
72
|
-
|
|
73
|
-
- uses: oven-sh/setup-bun@v2
|
|
74
|
-
|
|
75
|
-
- name: Cache Bun dependencies
|
|
76
|
-
uses: actions/cache@v5
|
|
77
|
-
with:
|
|
78
|
-
path: |
|
|
79
|
-
~/.bun/install/cache
|
|
80
|
-
node_modules
|
|
81
|
-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
82
|
-
restore-keys: |
|
|
83
|
-
${{ runner.os }}-bun-
|
|
84
|
-
|
|
85
|
-
- name: Install dependencies
|
|
86
|
-
run: bun install --frozen-lockfile
|
|
71
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
87
72
|
```
|
|
88
73
|
|
|
74
|
+
Якщо в репозиторії action збережено під **`./npm/github-actions/setup-bun-deps`**, у `uses:` вкажи цей шлях замість `.github/actions/…` (**ga.mdc**).
|
|
75
|
+
|
|
89
76
|
## Перевірка
|
|
90
77
|
|
|
91
78
|
`npx @nitra/cursor check bun`
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.11'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -38,7 +38,7 @@ version: '1.9'
|
|
|
38
38
|
"exitCode": 1,
|
|
39
39
|
"reporters": ["console"],
|
|
40
40
|
"minLines": 25,
|
|
41
|
-
"ignore": ["**/dist/**"
|
|
41
|
+
"ignore": ["**/dist/**"]
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -206,7 +206,7 @@ resources: {}
|
|
|
206
206
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
207
207
|
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; далі перевіряється **`EXPLICIT_K8S_SCHEMAS`** (`Map`: `apiVersion` + `kind` + `type`, для записів без `type` у маніфесті — третій компонент **`*`**); потім `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree (GitHub Pages), крім рядків явної таблиці (наприклад **InfisicalSecret** — raw на `main`).
|
|
208
208
|
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
209
|
-
- У файлах, ім’я яких **не** `kustomization.yaml
|
|
209
|
+
- У файлах, ім’я яких **не** `kustomization.yaml`.
|
|
210
210
|
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
211
211
|
- Якщо в будь-якому файлі під **`k8s`** є **`kind: HealthCheckPolicy`**, серед файлів має бути **`ru/kustomization.yaml`** (сегмент шляху **`ru`** перед іменем файлу), а його вміст — patch видалення **HealthCheckPolicy** з **`$patch: delete`** (див. той самий розділ).
|
|
212
212
|
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
package/mdc/text.mdc
CHANGED
|
@@ -28,9 +28,9 @@ version: '1.24'
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**. Для української додай **`@cspell/dict-uk-ua`**. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`
|
|
31
|
+
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**. Для української додай **`@cspell/dict-uk-ua`**. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`bunx v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
|
|
32
32
|
|
|
33
|
-
У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`
|
|
33
|
+
У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` скрипт підставляє в v8r сам. За бажання можна передати власні glob-и аргументами скрипта. Шлях до скрипта: `./npm/scripts/…`, `./scripts/…` після копіювання, або `node_modules/@nitra/cursor/scripts/…`.
|
|
34
34
|
|
|
35
35
|
```json title="package.json"
|
|
36
36
|
{
|
package/package.json
CHANGED
package/schemas/n-cursor.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"skills": {
|
|
23
23
|
"type": "array",
|
|
24
|
-
"description": "Ідентифікатори skills без префікса n- (
|
|
24
|
+
"description": "Ідентифікатори skills без префікса n- (у пакеті — підкаталог skills/<id>/, у проєкті після синку — .cursor/skills/n-<id>/). Якщо відсутній, CLI доповнить списком skills з пакету.",
|
|
25
25
|
"items": {
|
|
26
26
|
"type": "string",
|
|
27
27
|
"minLength": 1
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє відповідність проєкту правилу abie.mdc (проєкти abinbevefes).
|
|
3
|
+
*
|
|
4
|
+
* Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
|
|
5
|
+
* без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
|
|
6
|
+
*
|
|
7
|
+
* **Гілки:** у **`.github/workflows/clean-merged-branch.yml`** у кроці з
|
|
8
|
+
* **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
|
|
9
|
+
* **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно).
|
|
10
|
+
*
|
|
11
|
+
* **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
|
|
12
|
+
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
13
|
+
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
|
|
14
|
+
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
15
|
+
* (узгоджено з **k8s.mdc** / **check-k8s.mjs**).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync } from 'node:fs'
|
|
18
|
+
import { readFile } from 'node:fs/promises'
|
|
19
|
+
import { dirname, join, relative } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { parseAllDocuments } from 'yaml'
|
|
22
|
+
|
|
23
|
+
import { isRuKustomizationPath, pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
|
|
24
|
+
import { pass } from './utils/pass.mjs'
|
|
25
|
+
import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
26
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
27
|
+
|
|
28
|
+
const CONFIG_FILE = '.n-cursor.json'
|
|
29
|
+
|
|
30
|
+
/** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
|
|
31
|
+
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
32
|
+
|
|
33
|
+
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
34
|
+
|
|
35
|
+
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
36
|
+
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
40
|
+
* @param {string} root корінь репозиторію (cwd)
|
|
41
|
+
* @returns {Promise<boolean>} true, якщо **rules** містить **abie**
|
|
42
|
+
*/
|
|
43
|
+
export async function isAbieRuleEnabled(root) {
|
|
44
|
+
const p = join(root, CONFIG_FILE)
|
|
45
|
+
if (!existsSync(p)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
let raw
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(p, 'utf8')
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
let cfg
|
|
55
|
+
try {
|
|
56
|
+
cfg = JSON.parse(raw)
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
const rules = cfg?.rules
|
|
61
|
+
if (!Array.isArray(rules)) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
return rules.some(r => String(r).trim().toLowerCase() === 'abie')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Розбирає **`ignore_branches`** з workflow **clean-merged-branch** (крок delete-abandoned-branches).
|
|
69
|
+
* @param {string} content вміст **.yml**
|
|
70
|
+
* @returns {string | null} рядок **ignore_branches** або **null**
|
|
71
|
+
*/
|
|
72
|
+
export function parseCleanMergedIgnoreBranches(content) {
|
|
73
|
+
const root = parseWorkflowYaml(content)
|
|
74
|
+
if (!root) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
for (const { step } of flattenWorkflowSteps(root)) {
|
|
78
|
+
const uses = getStepUses(step)
|
|
79
|
+
if (uses.includes('phpdocker-io/github-actions-delete-abandoned-branches')) {
|
|
80
|
+
const w = step.with
|
|
81
|
+
if (w && typeof w === 'object' && !Array.isArray(w)) {
|
|
82
|
+
const ib = /** @type {Record<string, unknown>} */ (w).ignore_branches
|
|
83
|
+
if (typeof ib === 'string') {
|
|
84
|
+
return ib
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Чи рядок **ignore_branches** містить усі гілки з **required** (для abie — dev, ua, ru).
|
|
94
|
+
* @param {string} ignoreBranches значення **ignore_branches**
|
|
95
|
+
* @param {string[]} required імена гілок (нижній регістр для порівняння)
|
|
96
|
+
* @returns {boolean} true, якщо всі **required** присутні як окремі токени
|
|
97
|
+
*/
|
|
98
|
+
export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
|
|
99
|
+
const parts = new Set(
|
|
100
|
+
ignoreBranches
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim().toLowerCase())
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
)
|
|
105
|
+
return required.every(r => parts.has(r.toLowerCase()))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
|
|
110
|
+
* @param {string} root корінь репозиторію
|
|
111
|
+
* @returns {Promise<string[]>} відсортовані шляхи
|
|
112
|
+
*/
|
|
113
|
+
async function findK8sYamlFiles(root) {
|
|
114
|
+
/** @type {string[]} */
|
|
115
|
+
const out = []
|
|
116
|
+
await walkDir(root, p => {
|
|
117
|
+
if (!pathHasK8sSegment(p)) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (!/\.ya?ml$/iu.test(p)) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
out.push(p)
|
|
124
|
+
})
|
|
125
|
+
return [...out].toSorted((a, b) => a.localeCompare(b))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Чи документ — **Deployment**.
|
|
130
|
+
* @param {unknown} obj корінь YAML-документа
|
|
131
|
+
* @returns {boolean} true, якщо **kind** документа — **Deployment**
|
|
132
|
+
*/
|
|
133
|
+
function isDeploymentDoc(obj) {
|
|
134
|
+
return (
|
|
135
|
+
obj !== null &&
|
|
136
|
+
typeof obj === 'object' &&
|
|
137
|
+
!Array.isArray(obj) &&
|
|
138
|
+
/** @type {Record<string, unknown>} */ (obj).kind === 'Deployment'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Директорії, де є хоча б один **Deployment** у файлах **k8s**.
|
|
144
|
+
* @param {string} root корінь cwd
|
|
145
|
+
* @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
|
|
146
|
+
* @param {(msg: string) => void} fail реєстрація помилки парсингу
|
|
147
|
+
* @returns {Promise<Set<string>>} абсолютні шляхи директорій
|
|
148
|
+
*/
|
|
149
|
+
async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
150
|
+
/** @type {Set<string>} */
|
|
151
|
+
const dirs = new Set()
|
|
152
|
+
for (const abs of yamlAbs) {
|
|
153
|
+
let raw
|
|
154
|
+
let readOk = false
|
|
155
|
+
try {
|
|
156
|
+
raw = await readFile(abs, 'utf8')
|
|
157
|
+
readOk = true
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
160
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
161
|
+
}
|
|
162
|
+
if (readOk) {
|
|
163
|
+
const body = stripBom(raw)
|
|
164
|
+
const lines = body.split(/\r?\n/u)
|
|
165
|
+
const first = lines[0] ?? ''
|
|
166
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
167
|
+
/** @type {import('yaml').Document[]} */
|
|
168
|
+
let docs
|
|
169
|
+
let parseOk = false
|
|
170
|
+
try {
|
|
171
|
+
docs = parseAllDocuments(rest)
|
|
172
|
+
parseOk = true
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
175
|
+
fail(`${relative(root, abs) || abs}: YAML (${msg})`)
|
|
176
|
+
}
|
|
177
|
+
if (parseOk) {
|
|
178
|
+
for (const doc of docs) {
|
|
179
|
+
if (doc.errors.length === 0) {
|
|
180
|
+
const obj = doc.toJSON()
|
|
181
|
+
if (isDeploymentDoc(obj)) {
|
|
182
|
+
dirs.add(dirname(abs))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return dirs
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Прибирає BOM на початку файлу.
|
|
194
|
+
* @param {string} s вміст
|
|
195
|
+
* @returns {string} той самий рядок без BOM (U+FEFF) на початку
|
|
196
|
+
*/
|
|
197
|
+
function stripBom(s) {
|
|
198
|
+
return s.startsWith('\uFEFF') ? s.slice(1) : s
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Перевіряє **hc.yaml** на відповідність abie.mdc.
|
|
203
|
+
* @param {string} raw повний текст файлу
|
|
204
|
+
* @param {string} relPath відносний шлях для повідомлень
|
|
205
|
+
* @returns {string | null} текст помилки або **null**
|
|
206
|
+
*/
|
|
207
|
+
export function validateAbieHcYaml(raw, relPath) {
|
|
208
|
+
const body = stripBom(raw)
|
|
209
|
+
const lines = body.split(/\r?\n/u)
|
|
210
|
+
if (lines.length === 0 || lines[0].trim() === '') {
|
|
211
|
+
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
212
|
+
}
|
|
213
|
+
const m = lines[0].match(MODELINE_RE)
|
|
214
|
+
if (!m) {
|
|
215
|
+
return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
216
|
+
}
|
|
217
|
+
if (m[1] !== ABIE_HC_SCHEMA_URL) {
|
|
218
|
+
return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
219
|
+
}
|
|
220
|
+
const yamlBody = lines
|
|
221
|
+
.slice(1)
|
|
222
|
+
.join('\n')
|
|
223
|
+
.replace(/^\s*\n/u, '')
|
|
224
|
+
/** @type {import('yaml').Document[]} */
|
|
225
|
+
let docs
|
|
226
|
+
try {
|
|
227
|
+
docs = parseAllDocuments(yamlBody)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
230
|
+
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
231
|
+
}
|
|
232
|
+
/** @type {Record<string, unknown> | null} */
|
|
233
|
+
let policy = null
|
|
234
|
+
for (const doc of docs) {
|
|
235
|
+
if (doc.errors.length > 0) {
|
|
236
|
+
return `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}`
|
|
237
|
+
}
|
|
238
|
+
const obj = doc.toJSON()
|
|
239
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
240
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
241
|
+
if (rec.kind === 'HealthCheckPolicy') {
|
|
242
|
+
policy = rec
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!policy) {
|
|
248
|
+
return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
249
|
+
}
|
|
250
|
+
if (policy.apiVersion !== 'networking.gke.io/v1') {
|
|
251
|
+
return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
|
|
252
|
+
}
|
|
253
|
+
const meta = policy.metadata
|
|
254
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
255
|
+
return `${relPath}: відсутній metadata (abie.mdc)`
|
|
256
|
+
}
|
|
257
|
+
const name = /** @type {Record<string, unknown>} */ (meta).name
|
|
258
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
259
|
+
return `${relPath}: metadata.name має бути непорожнім рядком (abie.mdc)`
|
|
260
|
+
}
|
|
261
|
+
const spec = policy.spec
|
|
262
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
263
|
+
return `${relPath}: відсутній spec (abie.mdc)`
|
|
264
|
+
}
|
|
265
|
+
const def = /** @type {Record<string, unknown>} */ (spec).default
|
|
266
|
+
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
267
|
+
return `${relPath}: відсутній spec.default (abie.mdc)`
|
|
268
|
+
}
|
|
269
|
+
const config = /** @type {Record<string, unknown>} */ (def).config
|
|
270
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
271
|
+
return `${relPath}: відсутній spec.default.config (abie.mdc)`
|
|
272
|
+
}
|
|
273
|
+
if (config.type !== 'HTTP') {
|
|
274
|
+
return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
275
|
+
}
|
|
276
|
+
const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
|
|
277
|
+
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
278
|
+
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
279
|
+
}
|
|
280
|
+
if (httpHc.requestPath !== '/healthz') {
|
|
281
|
+
return `${relPath}: httpHealthCheck.requestPath має бути /healthz (abie.mdc)`
|
|
282
|
+
}
|
|
283
|
+
if (httpHc.port !== 8080) {
|
|
284
|
+
return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
285
|
+
}
|
|
286
|
+
const targetRef = /** @type {Record<string, unknown>} */ (spec).targetRef
|
|
287
|
+
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
288
|
+
return `${relPath}: відсутній targetRef (abie.mdc)`
|
|
289
|
+
}
|
|
290
|
+
if (targetRef.kind !== 'Service') {
|
|
291
|
+
return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
|
|
292
|
+
}
|
|
293
|
+
const svcName = targetRef.name
|
|
294
|
+
if (typeof svcName !== 'string' || svcName !== name) {
|
|
295
|
+
return `${relPath}: targetRef.name має збігатися з metadata.name (${name}) (abie.mdc)`
|
|
296
|
+
}
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
|
|
302
|
+
* @param {string} root корінь
|
|
303
|
+
* @param {string[]} yamlAbs абсолютні шляхи
|
|
304
|
+
* @returns {Promise<string[]>} унікальні відносні шляхи yaml із **HealthCheckPolicy**
|
|
305
|
+
*/
|
|
306
|
+
async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
307
|
+
/** @type {string[]} */
|
|
308
|
+
const out = []
|
|
309
|
+
for (const abs of yamlAbs) {
|
|
310
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
311
|
+
let raw
|
|
312
|
+
try {
|
|
313
|
+
raw = await readFile(abs, 'utf8')
|
|
314
|
+
} catch {
|
|
315
|
+
raw = null
|
|
316
|
+
}
|
|
317
|
+
if (raw !== null) {
|
|
318
|
+
const body = stripBom(raw)
|
|
319
|
+
const lines = body.split(/\r?\n/u)
|
|
320
|
+
const first = lines[0] ?? ''
|
|
321
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
322
|
+
try {
|
|
323
|
+
const docs = parseAllDocuments(rest)
|
|
324
|
+
for (const doc of docs) {
|
|
325
|
+
if (doc.errors.length === 0) {
|
|
326
|
+
const obj = doc.toJSON()
|
|
327
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
328
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
329
|
+
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
330
|
+
out.push(rel)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
/* пропускаємо пошкоджені файли — їх ловить check-k8s */
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (як **check-k8s**).
|
|
345
|
+
* @param {string} root корінь
|
|
346
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
|
|
347
|
+
* @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
|
|
348
|
+
* @param {(msg: string) => void} fail callback
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, healthCheckPolicyRelativePaths, fail) {
|
|
352
|
+
if (healthCheckPolicyRelativePaths.length === 0) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
356
|
+
if (ruAbsList.length === 0) {
|
|
357
|
+
fail(
|
|
358
|
+
`Знайдено HealthCheckPolicy у ${healthCheckPolicyRelativePaths.join(', ')} — додай ru/kustomization.yaml з patch видалення (abie.mdc)`
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
for (const abs of ruAbsList) {
|
|
363
|
+
let raw
|
|
364
|
+
try {
|
|
365
|
+
raw = await readFile(abs, 'utf8')
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
368
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
fail(
|
|
376
|
+
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — abie.mdc'
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
382
|
+
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
383
|
+
*/
|
|
384
|
+
export async function check() {
|
|
385
|
+
let exitCode = 0
|
|
386
|
+
const fail = msg => {
|
|
387
|
+
console.log(` ❌ ${msg}`)
|
|
388
|
+
exitCode = 1
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const root = process.cwd()
|
|
392
|
+
const enabled = await isAbieRuleEnabled(root)
|
|
393
|
+
if (!enabled) {
|
|
394
|
+
pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
|
|
395
|
+
return 0
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
399
|
+
|
|
400
|
+
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
401
|
+
if (existsSync(cleanMergedPath)) {
|
|
402
|
+
/** @type {string | undefined} */
|
|
403
|
+
let wfRaw
|
|
404
|
+
try {
|
|
405
|
+
wfRaw = await readFile(cleanMergedPath, 'utf8')
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
408
|
+
fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
|
|
409
|
+
}
|
|
410
|
+
if (wfRaw !== undefined) {
|
|
411
|
+
const ib = parseCleanMergedIgnoreBranches(wfRaw)
|
|
412
|
+
if (ib === null || ib.trim() === '') {
|
|
413
|
+
fail(
|
|
414
|
+
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
415
|
+
)
|
|
416
|
+
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
417
|
+
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
418
|
+
} else {
|
|
419
|
+
fail(
|
|
420
|
+
`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
429
|
+
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
|
430
|
+
|
|
431
|
+
if (deploymentDirs.size > 0) {
|
|
432
|
+
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml`)
|
|
433
|
+
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
434
|
+
const hcAbs = join(dir, 'hc.yaml')
|
|
435
|
+
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
436
|
+
if (existsSync(hcAbs)) {
|
|
437
|
+
let hcRaw
|
|
438
|
+
let hcReadOk = false
|
|
439
|
+
try {
|
|
440
|
+
hcRaw = await readFile(hcAbs, 'utf8')
|
|
441
|
+
hcReadOk = true
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
444
|
+
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
445
|
+
}
|
|
446
|
+
if (hcReadOk) {
|
|
447
|
+
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
448
|
+
if (v === null) {
|
|
449
|
+
pass(`${relHc}: відповідає abie.mdc`)
|
|
450
|
+
} else {
|
|
451
|
+
fail(v)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
fail(
|
|
456
|
+
`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
|
|
465
|
+
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
|
|
466
|
+
|
|
467
|
+
return exitCode
|
|
468
|
+
}
|
|
@@ -12,7 +12,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
12
12
|
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
|
|
13
13
|
import { pass } from './utils/pass.mjs'
|
|
14
14
|
|
|
15
|
-
/** Очікуваний локальний
|
|
15
|
+
/** Очікуваний локальний скрипт. */
|
|
16
16
|
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
17
17
|
|
|
18
18
|
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
@@ -28,14 +28,12 @@ export function normalizeLintJsScript(s) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Чи рядок `lint-js` збігається з каноном
|
|
31
|
+
* Чи рядок `lint-js` збігається з каноном (`bunx oxlint`, `bunx eslint`, `bunx jscpd`).
|
|
32
32
|
* @param {string} script значення `scripts.lint-js` з package.json
|
|
33
33
|
* @returns {boolean} true, якщо рядок канонічний
|
|
34
34
|
*/
|
|
35
35
|
export function isCanonicalLintJs(script) {
|
|
36
|
-
|
|
37
|
-
if (n.includes('bunx oxlint')) return false
|
|
38
|
-
return n === CANONICAL_LINT_JS
|
|
36
|
+
return normalizeLintJsScript(script) === CANONICAL_LINT_JS
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
/**
|
|
@@ -89,7 +87,7 @@ export async function check() {
|
|
|
89
87
|
pass(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
90
88
|
} else {
|
|
91
89
|
fail(
|
|
92
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (
|
|
90
|
+
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
|
|
93
91
|
)
|
|
94
92
|
}
|
|
95
93
|
} else {
|
package/scripts/run-v8r.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Тихий запуск v8r для усіх типів файлів, які підтримує v8r (json, json5, yaml, yml, toml).
|
|
3
3
|
*
|
|
4
4
|
* Один виклик цього скрипта з `lint-text` замість чотирьох окремих викликів v8r: під капотом для
|
|
5
|
-
* кожного glob окремий `
|
|
5
|
+
* кожного glob окремий `bunx v8r`, бо v8r у одному процесі падає з кодом 98, якщо хоч один із
|
|
6
6
|
* переданих глобів не знаходить файлів — тоді решта розширень не перевіряються.
|
|
7
7
|
*
|
|
8
8
|
* Каталог схем `@nitra/cursor` (`v8r-catalog.json` у каталозі `schemas` пакета) передається в v8r
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: abie-kustomize
|
|
3
|
+
description: >-
|
|
4
|
+
Трансформація дерев k8s у структуру Kustomize (base + overlays): dev → base, без окремої dev/
|
|
5
|
+
version: '1.0'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Трансформуй директорії, щоб виділити спільне за допомогою kustomize. За основу беремо все, що в середовищі dev, і саме в такому вигляді з dev воно має стати **base**; якщо вже є base і немає dev — це нормально, рухайся далі.
|
|
9
|
+
|
|
10
|
+
У інших середовищах має бути лише `kustomization.yaml` і зміни через оверрайди.
|
|
11
|
+
|
|
12
|
+
У **base** у всіх ресурсів (окрім `base/kustomization.yaml`) має бути namespace **dev**.
|
|
13
|
+
|
|
14
|
+
Окремої директорії **dev** не має бути — за середовище dev відповідає **base**.
|
|
15
|
+
|
|
16
|
+
README має бути в директорії **k8s**.
|
|
17
|
+
|
|
18
|
+
Рядки в маніфестах у **base**, які змінюватимуться в інших середовищах, позначай коментарем на тому самому рядку: `# буде замінено через kustomize`.
|
|
19
|
+
|
|
20
|
+
Патчів лише на namespace не роби — namespace задається в `kustomization.yaml`.
|
|
21
|
+
|
|
22
|
+
Застарілі файли прибирай.
|
|
23
|
+
|
|
24
|
+
У всіх Deployment має бути `imagePullPolicy: Always`.
|
|
25
|
+
|
|
26
|
+
Для overlays **ru** та **ua** `namespace` задавай у `kustomization.yaml` (без окремих patch лише на зміну namespace). Деталі — **n-k8s** / **abie** у `.cursor/rules/`, якщо ці правила увімкнені в проєкті.
|