@nitra/cursor 1.8.2 → 1.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/n-cursor.js +140 -0
- package/github-actions/setup-bun-deps/action.yml +3 -5
- package/mdc/ga.mdc +6 -2
- package/mdc/js-lint.mdc +6 -2
- package/mdc/k8s.mdc +15 -3
- package/mdc/text.mdc +6 -2
- package/package.json +1 -1
- package/scripts/check-ga.mjs +41 -1
- package/scripts/check-k8s.mjs +80 -1
- package/scripts/sync-setup-bun-deps-action.mjs +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -52,6 +52,7 @@ const AGENTS_FILE = 'AGENTS.md'
|
|
|
52
52
|
const AGENTS_TEMPLATE_FILE = 'AGENTS.template.md'
|
|
53
53
|
const RULES_DIR = '.cursor/rules'
|
|
54
54
|
const SKILLS_DIR = '.cursor/skills'
|
|
55
|
+
const COMMANDS_DIR = '.claude/commands'
|
|
55
56
|
const RULE_PREFIX = 'n-'
|
|
56
57
|
|
|
57
58
|
const binDir = dirname(fileURLToPath(import.meta.url))
|
|
@@ -415,6 +416,49 @@ async function removeOrphanManagedSkillDirs(skillsRoot, configSkills) {
|
|
|
415
416
|
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Генерує CLAUDE.md у корені cwd з at-імпортами всіх .mdc-правил та посиланнями на skills.
|
|
421
|
+
* Завдяки цьому Claude Code автоматично завантажує вміст кожного правила при старті.
|
|
422
|
+
* @param {string[]} configRules елементи масиву rules з .n-cursor.json
|
|
423
|
+
* @param {string[]} configSkills id skills з конфігу
|
|
424
|
+
* @returns {Promise<void>}
|
|
425
|
+
*/
|
|
426
|
+
async function syncClaudeMd(configRules, configSkills) {
|
|
427
|
+
const lines = [
|
|
428
|
+
`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`,
|
|
429
|
+
'',
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
for (const rule of configRules) {
|
|
433
|
+
const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
|
|
434
|
+
lines.push(`@${RULES_DIR}/${fileName}`)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (configSkills.length > 0) {
|
|
438
|
+
lines.push('', '## Skills', '')
|
|
439
|
+
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
440
|
+
for (const skillId of configSkills) {
|
|
441
|
+
const id = canonicalSkillId(skillId)
|
|
442
|
+
const dirName = managedSkillDirName(skillId)
|
|
443
|
+
const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
|
|
444
|
+
let desc = ''
|
|
445
|
+
if (existsSync(skillMdPath)) {
|
|
446
|
+
const text = await readFile(skillMdPath, 'utf8')
|
|
447
|
+
const parsed = extractSkillDescription(text)
|
|
448
|
+
if (parsed) desc = parsed
|
|
449
|
+
}
|
|
450
|
+
const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
451
|
+
lines.push(desc ? `${ref} — ${desc}` : ref, ` Команда: \`/${RULE_PREFIX}${id}\``)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
lines.push('')
|
|
456
|
+
const claudeMdPath = join(cwd(), 'CLAUDE.md')
|
|
457
|
+
const hadFile = existsSync(claudeMdPath)
|
|
458
|
+
await writeFile(claudeMdPath, lines.join('\n'), 'utf8')
|
|
459
|
+
console.log(hadFile ? `📝 Оновлено CLAUDE.md` : `📝 Створено CLAUDE.md`)
|
|
460
|
+
}
|
|
461
|
+
|
|
418
462
|
/**
|
|
419
463
|
* Повністю перезаписує AGENTS.md у корені cwd з npm/AGENTS.template.md
|
|
420
464
|
* @param {string[]} configSkills id skills з конфігу
|
|
@@ -491,6 +535,75 @@ async function syncSkills(configSkills) {
|
|
|
491
535
|
return { success, fail }
|
|
492
536
|
}
|
|
493
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Синхронізує .claude/commands/n-<id>.md зі skills пакету.
|
|
540
|
+
* Кожен файл містить посилання на відповідний cursor skill, а не копію інструкцій.
|
|
541
|
+
* @param {string[]} configSkills id без префікса n-
|
|
542
|
+
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
|
|
543
|
+
*/
|
|
544
|
+
async function syncCommands(configSkills) {
|
|
545
|
+
if (configSkills.length === 0 || !existsSync(BUNDLED_SKILLS_DIR)) {
|
|
546
|
+
return { success: 0, fail: 0 }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const commandsDir = join(cwd(), COMMANDS_DIR)
|
|
550
|
+
await mkdir(commandsDir, { recursive: true })
|
|
551
|
+
|
|
552
|
+
let success = 0
|
|
553
|
+
let fail = 0
|
|
554
|
+
|
|
555
|
+
for (const skillId of configSkills) {
|
|
556
|
+
const id = canonicalSkillId(skillId)
|
|
557
|
+
const dirName = managedSkillDirName(skillId)
|
|
558
|
+
const srcSkillMd = join(BUNDLED_SKILLS_DIR, dirName, 'SKILL.md')
|
|
559
|
+
const destFile = join(commandsDir, `${RULE_PREFIX}${id}.md`)
|
|
560
|
+
|
|
561
|
+
process.stdout.write(` ⬇ ${id} → ${COMMANDS_DIR}/${RULE_PREFIX}${id}.md ... `)
|
|
562
|
+
if (existsSync(srcSkillMd)) {
|
|
563
|
+
try {
|
|
564
|
+
const raw = await readFile(srcSkillMd, 'utf8')
|
|
565
|
+
const desc = extractSkillDescription(raw)
|
|
566
|
+
const header = desc ? `# ${RULE_PREFIX}${id} — ${desc}\n\n` : ''
|
|
567
|
+
const body = `${header}Виконай інструкції зі скілу \`.cursor/skills/${dirName}/SKILL.md\`.\n`
|
|
568
|
+
await writeFile(destFile, body, 'utf8')
|
|
569
|
+
console.log(`✅`)
|
|
570
|
+
success++
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.log(`❌`)
|
|
573
|
+
console.error(` Помилка: ${error.message}`)
|
|
574
|
+
fail++
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
console.log(`❌`)
|
|
578
|
+
console.error(` Немає SKILL.md у пакеті: ${dirName}`)
|
|
579
|
+
fail++
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { success, fail }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Видаляє файли n-*.md у .claude/commands, яких немає у конфігурації skills
|
|
587
|
+
* @param {string} commandsDir абсолютний шлях до .claude/commands
|
|
588
|
+
* @param {string[]} configSkills id без префікса n-
|
|
589
|
+
* @returns {Promise<string[]>} імена видалених файлів
|
|
590
|
+
*/
|
|
591
|
+
async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
|
|
592
|
+
if (!existsSync(commandsDir)) {
|
|
593
|
+
return []
|
|
594
|
+
}
|
|
595
|
+
const expected = new Set(configSkills.map(s => `${RULE_PREFIX}${canonicalSkillId(s)}.md`))
|
|
596
|
+
const names = await readdir(commandsDir)
|
|
597
|
+
const removed = []
|
|
598
|
+
for (const name of names) {
|
|
599
|
+
if (name.endsWith('.md') && name.startsWith(RULE_PREFIX) && !expected.has(name)) {
|
|
600
|
+
await unlink(join(commandsDir, name))
|
|
601
|
+
removed.push(name)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return removed.toSorted((a, b) => a.localeCompare(b))
|
|
605
|
+
}
|
|
606
|
+
|
|
494
607
|
/**
|
|
495
608
|
* Знаходить доступні check-скрипти у каталозі scripts пакету
|
|
496
609
|
* @returns {Promise<string[]>} відсортовані імена правил (наприклад ['bun', 'ga', 'js-lint'])
|
|
@@ -711,6 +824,26 @@ async function runSync() {
|
|
|
711
824
|
throw error
|
|
712
825
|
}
|
|
713
826
|
|
|
827
|
+
try {
|
|
828
|
+
const { success: cmdOk, fail: cmdFail } = await syncCommands(skills)
|
|
829
|
+
if (skills.length > 0) {
|
|
830
|
+
console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
|
|
831
|
+
}
|
|
832
|
+
const removedCmds = await removeOrphanManagedCommandFiles(join(cwd(), COMMANDS_DIR), skills)
|
|
833
|
+
if (removedCmds.length > 0) {
|
|
834
|
+
console.log(`\n🧹 Видалено commands поза списком ${CONFIG_FILE} (${removedCmds.length}):`)
|
|
835
|
+
for (const name of removedCmds) {
|
|
836
|
+
console.log(` − ${COMMANDS_DIR}/${name}`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (cmdFail > 0) {
|
|
840
|
+
throw new Error(`Не вдалося скопіювати ${cmdFail} з ${skills.length} commands`)
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.error(`❌ Commands: ${error instanceof Error ? error.message : String(error)}`)
|
|
844
|
+
throw error
|
|
845
|
+
}
|
|
846
|
+
|
|
714
847
|
try {
|
|
715
848
|
await syncAgentsMd(skills)
|
|
716
849
|
} catch (error) {
|
|
@@ -718,6 +851,13 @@ async function runSync() {
|
|
|
718
851
|
throw error
|
|
719
852
|
}
|
|
720
853
|
|
|
854
|
+
try {
|
|
855
|
+
await syncClaudeMd(rules, skills)
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error(`❌ Не вдалося оновити CLAUDE.md: ${error instanceof Error ? error.message : String(error)}`)
|
|
858
|
+
throw error
|
|
859
|
+
}
|
|
860
|
+
|
|
721
861
|
console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
|
|
722
862
|
if (failCount > 0) {
|
|
723
863
|
throw new Error(`Не вдалося завантажити ${failCount} з ${rules.length} правил`)
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
|
2
|
+
|
|
1
3
|
name: Setup Bun dependencies
|
|
2
|
-
description:
|
|
4
|
+
description: Node 24, Bun, cache та bun install --frozen-lockfile (checkout — окремим кроком у workflow)
|
|
3
5
|
|
|
4
6
|
runs:
|
|
5
7
|
using: composite
|
|
6
8
|
steps:
|
|
7
|
-
- uses: actions/checkout@v6
|
|
8
|
-
with:
|
|
9
|
-
persist-credentials: false
|
|
10
|
-
|
|
11
9
|
- uses: actions/setup-node@v6
|
|
12
10
|
with:
|
|
13
11
|
node-version: '24'
|
package/mdc/ga.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила форматів для .github/workflows
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.3'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
|
|
@@ -96,6 +96,10 @@ jobs:
|
|
|
96
96
|
permissions:
|
|
97
97
|
contents: read
|
|
98
98
|
steps:
|
|
99
|
+
- uses: actions/checkout@v6
|
|
100
|
+
with:
|
|
101
|
+
persist-credentials: false
|
|
102
|
+
|
|
99
103
|
- uses: ./.github/actions/setup-bun-deps
|
|
100
104
|
|
|
101
105
|
- uses: astral-sh/setup-uv@v8.0.0
|
|
@@ -104,7 +108,7 @@ jobs:
|
|
|
104
108
|
run: bun run lint-ga
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
**Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
|
|
108
112
|
|
|
109
113
|
```json title=".vscode/extensions.json"
|
|
110
114
|
{
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.8'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`:** `oxlint` (без `bunx`), **`bunx eslint`**, **`bunx jscpd`**; у CI — `bunx oxlint` / `bunx eslint` / `bunx jscpd`. Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
@@ -86,6 +86,10 @@ jobs:
|
|
|
86
86
|
permissions:
|
|
87
87
|
contents: read
|
|
88
88
|
steps:
|
|
89
|
+
- uses: actions/checkout@v6
|
|
90
|
+
with:
|
|
91
|
+
persist-credentials: false
|
|
92
|
+
|
|
89
93
|
- uses: ./.github/actions/setup-bun-deps
|
|
90
94
|
|
|
91
95
|
- name: Eslint
|
|
@@ -95,7 +99,7 @@ jobs:
|
|
|
95
99
|
bunx jscpd .
|
|
96
100
|
```
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
99
103
|
|
|
100
104
|
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
|
|
101
105
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.12'
|
|
4
4
|
globs: "**/k8s/**/*.{yaml,yml}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
При зміні правил синхронно оновлюй **`YANNH_PIN`**, **`YANNH_REF`** (якщо зміниться гілка за замовчуванням у репо yannh), **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** (GitHub Pages каталогу CRD), а в **`run-k8s.mjs`** — константу **`KUBERNETES_VERSION`** і **`DATREE_CRD_SCHEMA_LOCATION`** (узгоджено з базою datree у цьому правилі).
|
|
125
125
|
|
|
126
126
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
127
|
-
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree (GitHub Pages).
|
|
127
|
+
- **`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`).
|
|
128
128
|
|
|
129
129
|
## Коли застосовувати (агентам)
|
|
130
130
|
|
|
@@ -139,13 +139,25 @@ jobs:
|
|
|
139
139
|
2. **`apiVersion: v1`** → yannh, PIN набору схем **`v1.33.9-standalone-strict`**, ref репозиторію для raw URL — **`master`** (каталог версії не є коренем репо на GitHub):
|
|
140
140
|
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/<PIN>/<kind>-v1.json`
|
|
141
141
|
`<kind>`: літери в нижньому регістрі без роздільників між CamelCase (наприклад `Service` → `service`).
|
|
142
|
+
|
|
143
|
+
**`kind: Secret`** і **`type: kubernetes.io/basic-auth`** — той самий шаблон, **`secret-v1.json`**:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/secret-v1.json
|
|
147
|
+
```
|
|
142
148
|
3. **`apiVersion: group/version`** і **group** у **`YANNH_GROUPS`** у скрипті → yannh:
|
|
143
149
|
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/<PIN>/<kind>-<group-з-крапками-як-дефіси>-<version>.json`
|
|
144
150
|
Приклади: `apps/v1` + `Deployment` → `deployment-apps-v1.json`; `networking.k8s.io/v1` + `Ingress` → `ingress-networking-k8s-io-v1.json`.
|
|
145
|
-
4. **Інакше** (CRD тощо) → [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog)
|
|
151
|
+
4. **Інакше** (CRD тощо) → [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog). Типово для `$schema` у редакторі — **GitHub Pages**:
|
|
146
152
|
`https://datreeio.github.io/CRDs-catalog/<group>/<kind>_<version>.json`
|
|
147
153
|
(`<kind>` — лише літери та цифри в нижньому регістрі, без роздільників між CamelCase, як для yannh.)
|
|
148
154
|
|
|
155
|
+
**Виняток — `InfisicalSecret`:** `apiVersion: secrets.infisical.com/v1alpha1`, `kind: InfisicalSecret` — канонічний modeline через **raw** на гілці **`main`** (не GitHub Pages):
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/secrets.infisical.com/infisicalsecret_v1alpha1.json
|
|
159
|
+
```
|
|
160
|
+
|
|
149
161
|
**Приклад (Gateway API):** `apiVersion: gateway.networking.k8s.io/v1beta1`, `kind: HTTPRoute`:
|
|
150
162
|
|
|
151
163
|
```yaml
|
package/mdc/text.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Обробка та перевірка текстових файлів (cspell, markdownlint-cli2, v8r, CI)
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.24'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**cspell**, **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint**, workflow **`lint-text`**.
|
|
@@ -107,13 +107,17 @@ jobs:
|
|
|
107
107
|
permissions:
|
|
108
108
|
contents: read
|
|
109
109
|
steps:
|
|
110
|
+
- uses: actions/checkout@v6
|
|
111
|
+
with:
|
|
112
|
+
persist-credentials: false
|
|
113
|
+
|
|
110
114
|
- uses: ./.github/actions/setup-bun-deps
|
|
111
115
|
|
|
112
116
|
- name: Lint text
|
|
113
117
|
run: bun run lint-text
|
|
114
118
|
```
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
117
121
|
|
|
118
122
|
Не дублюй окремий workflow з тими самими кроками cspell/markdownlint.
|
|
119
123
|
|
package/package.json
CHANGED
package/scripts/check-ga.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Workflows лише з розширенням `.yml`, наявність clean/lint workflow, конфіг zizmor з ref-pin,
|
|
5
5
|
* відсутність MegaLinter, коректний скрипт `lint-ga` у `package.json`, виклик у `lint-ga.yml`,
|
|
6
|
-
* наявність composite `.github/actions/setup-bun-deps/action.yml` (його записує `npx
|
|
6
|
+
* наявність composite `.github/actions/setup-bun-deps/action.yml` (його записує `npx \@nitra/cursor`),
|
|
7
|
+
* перед `uses: ./…/setup-bun-deps` у workflow — `actions/checkout` (runner інакше не бачить локальний action).
|
|
7
8
|
*/
|
|
8
9
|
import { existsSync } from 'node:fs'
|
|
9
10
|
import { readdir, readFile } from 'node:fs/promises'
|
|
@@ -17,6 +18,36 @@ const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/
|
|
|
17
18
|
/** Типові конфіги MegaLinter у корені репо */
|
|
18
19
|
const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-linter.yaml']
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Якщо workflow викликає локальний setup-bun-deps, раніше у файлі має бути `actions/checkout@v…` (ga.mdc).
|
|
23
|
+
* @param {string} relPath шлях для повідомлень
|
|
24
|
+
* @param {string} content вміст YAML
|
|
25
|
+
* @param {(msg: string) => void} failFn реєструє порушення (exit 1)
|
|
26
|
+
* @param {(msg: string) => void} passFn реєструє успішну перевірку
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
function verifyCheckoutBeforeLocalSetupBunDeps(relPath, content, failFn, passFn) {
|
|
30
|
+
const patterns = ['./.github/actions/setup-bun-deps', './npm/github-actions/setup-bun-deps']
|
|
31
|
+
let idxSetup = -1
|
|
32
|
+
for (const p of patterns) {
|
|
33
|
+
const i = content.indexOf(p)
|
|
34
|
+
if (i !== -1 && (idxSetup === -1 || i < idxSetup)) {
|
|
35
|
+
idxSetup = i
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (idxSetup === -1) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const idxCheckout = content.indexOf('actions/checkout@v')
|
|
42
|
+
if (idxCheckout === -1 || idxCheckout > idxSetup) {
|
|
43
|
+
failFn(
|
|
44
|
+
`${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
|
|
45
|
+
)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
passFn(`${relPath}: перед setup-bun-deps є checkout`)
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
/**
|
|
21
52
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
22
53
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -159,6 +190,15 @@ export async function check() {
|
|
|
159
190
|
} else {
|
|
160
191
|
fail('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
|
|
161
192
|
}
|
|
193
|
+
verifyCheckoutBeforeLocalSetupBunDeps(`${wfDir}/lint-ga.yml`, lgContent, fail, pass)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const wfName of ['lint-js.yml', 'lint-text.yml']) {
|
|
197
|
+
const p = join(wfDir, wfName)
|
|
198
|
+
if (existsSync(p)) {
|
|
199
|
+
const body = await readFile(p, 'utf8')
|
|
200
|
+
verifyCheckoutBeforeLocalSetupBunDeps(`${wfDir}/${wfName}`, body, fail, pass)
|
|
201
|
+
}
|
|
162
202
|
}
|
|
163
203
|
|
|
164
204
|
return exitCode
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Перший рядок `# yaml-language-server: $schema=…`, без дублікатів, розширення `.yaml`
|
|
5
5
|
* (окрім `kustomization.yml`); URL схеми за першим документом — kustomization / yannh / datree
|
|
6
|
-
* (datree
|
|
6
|
+
* (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
|
|
7
|
+
*
|
|
8
|
+
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
9
|
+
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
10
|
+
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
7
11
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
8
12
|
*/
|
|
9
13
|
import { readFile } from 'node:fs/promises'
|
|
@@ -25,6 +29,74 @@ const YANNH_BASE = `https://raw.githubusercontent.com/yannh/kubernetes-json-sche
|
|
|
25
29
|
/** Публікація [CRDs-catalog](https://github.com/datreeio/CRDs-catalog) на GitHub Pages (те саме дерево, що й raw на `main`). */
|
|
26
30
|
const DATREE_CRD_BASE = 'https://datreeio.github.io/CRDs-catalog/'
|
|
27
31
|
|
|
32
|
+
/** Raw URL для окремих CRD, де в редакторі канон — `raw.githubusercontent.com` (див. k8s.mdc). */
|
|
33
|
+
const DATREE_CRD_RAW_REF = 'main'
|
|
34
|
+
|
|
35
|
+
const DATREE_CRD_RAW_BASE = `https://raw.githubusercontent.com/datreeio/CRDs-catalog/${DATREE_CRD_RAW_REF}/`
|
|
36
|
+
|
|
37
|
+
/** У ключі `Map` означає «будь-який / відсутній `type`» (наприклад CRD без кореневого `type:`). */
|
|
38
|
+
const K8S_EXPLICIT_SCHEMA_TYPE_ANY = '*'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ключ запису в **`EXPLICIT_K8S_SCHEMAS`**: `apiVersion`, **`kind` як у YAML** (регістр як у маніфесті),
|
|
42
|
+
* `typeKey` — значення поля **`type:`** або **`K8S_EXPLICIT_SCHEMA_TYPE_ANY`**.
|
|
43
|
+
* @param {string} apiVersion повне значення `apiVersion` з маніфесту
|
|
44
|
+
* @param {string} kind значення `kind` з маніфесту (як у YAML)
|
|
45
|
+
* @param {string} typeKey значення кореневого `type:` або `K8S_EXPLICIT_SCHEMA_TYPE_ANY`
|
|
46
|
+
* @returns {string} внутрішній ключ для `Map`
|
|
47
|
+
*/
|
|
48
|
+
function k8sExplicitSchemaMapKey(apiVersion, kind, typeKey) {
|
|
49
|
+
return `${apiVersion}\0${kind}\0${typeKey}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Таблиця явних `$schema` для поєднань **`apiVersion` + `kind` + `type`** (див. k8s.mdc).
|
|
54
|
+
* Щоб додати рядок: визнач **`apiVersion`**, **`kind`**, при потребі **`type`**, вкажи **URL** і **reason**.
|
|
55
|
+
* @type {Map<string, { schema: string, reason: string }>}
|
|
56
|
+
*/
|
|
57
|
+
const EXPLICIT_K8S_SCHEMAS = new Map([
|
|
58
|
+
[
|
|
59
|
+
k8sExplicitSchemaMapKey('secrets.infisical.com/v1alpha1', 'InfisicalSecret', K8S_EXPLICIT_SCHEMA_TYPE_ANY),
|
|
60
|
+
{
|
|
61
|
+
schema: `${DATREE_CRD_RAW_BASE}secrets.infisical.com/infisicalsecret_v1alpha1.json`,
|
|
62
|
+
reason: 'InfisicalSecret v1alpha1 (явна таблиця схем, datree CRDs-catalog raw)'
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
k8sExplicitSchemaMapKey('v1', 'Secret', 'kubernetes.io/basic-auth'),
|
|
67
|
+
{
|
|
68
|
+
schema: `${YANNH_BASE}secret-v1.json`,
|
|
69
|
+
reason: 'Secret type kubernetes.io/basic-auth (явна таблиця схем, yannh secret-v1.json)'
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Витягує кореневе поле **`type:`** з документа (без повного YAML-парсера).
|
|
76
|
+
* @param {string} doc фрагмент YAML одного документа
|
|
77
|
+
* @returns {string | undefined} значення без лапок або undefined, якщо поля немає
|
|
78
|
+
*/
|
|
79
|
+
function extractTopLevelManifestType(doc) {
|
|
80
|
+
const m = doc.match(/^\s*type:\s*(\S+)\s*$/mu)
|
|
81
|
+
const raw = m?.[1]?.replaceAll(/^["']|["']$/gu, '')
|
|
82
|
+
return raw === undefined || raw === '' ? undefined : raw
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Шукає схему в **`EXPLICIT_K8S_SCHEMAS`**: спочатку за точним **`type`**, потім за **`*`**.
|
|
87
|
+
* @param {string} apiVersion повне значення `apiVersion` з маніфесту
|
|
88
|
+
* @param {string} kind значення `kind` з маніфесту (як у YAML)
|
|
89
|
+
* @param {string | undefined} manifestType кореневе поле `type` або undefined, якщо відсутнє
|
|
90
|
+
* @returns {{ schema: string, reason: string } | null} запис таблиці або null, якщо збігу немає
|
|
91
|
+
*/
|
|
92
|
+
function lookupExplicitK8sSchema(apiVersion, kind, manifestType) {
|
|
93
|
+
if (manifestType !== undefined) {
|
|
94
|
+
const exact = EXPLICIT_K8S_SCHEMAS.get(k8sExplicitSchemaMapKey(apiVersion, kind, manifestType))
|
|
95
|
+
if (exact) return exact
|
|
96
|
+
}
|
|
97
|
+
return EXPLICIT_K8S_SCHEMAS.get(k8sExplicitSchemaMapKey(apiVersion, kind, K8S_EXPLICIT_SCHEMA_TYPE_ANY)) ?? null
|
|
98
|
+
}
|
|
99
|
+
|
|
28
100
|
/**
|
|
29
101
|
* Групи API Kubernetes, для яких у перевірці очікується схема yannh (не datree CRD catalog).
|
|
30
102
|
* `gateway.networking.k8s.io` та інші розширення поза цим списком — datree.
|
|
@@ -158,6 +230,12 @@ export function expectedSchemaUrl(filePath, doc) {
|
|
|
158
230
|
}
|
|
159
231
|
}
|
|
160
232
|
|
|
233
|
+
const manifestType = extractTopLevelManifestType(doc)
|
|
234
|
+
const explicit = lookupExplicitK8sSchema(apiVersion, kind, manifestType)
|
|
235
|
+
if (explicit) {
|
|
236
|
+
return { expected: explicit.schema, reason: explicit.reason }
|
|
237
|
+
}
|
|
238
|
+
|
|
161
239
|
if (apiVersion === 'v1') {
|
|
162
240
|
const k = kindToSchemaFilePart(kind)
|
|
163
241
|
return { expected: `${YANNH_BASE}${k}-v1.json`, reason: 'core v1 (yannh)' }
|
|
@@ -182,6 +260,7 @@ export function expectedSchemaUrl(filePath, doc) {
|
|
|
182
260
|
}
|
|
183
261
|
|
|
184
262
|
const datreeKind = kindToSchemaFilePart(kind)
|
|
263
|
+
|
|
185
264
|
const url = `${DATREE_CRD_BASE}${group}/${datreeKind}_${version}.json`
|
|
186
265
|
return { expected: url, reason: 'CRD / група поза yannh (datree CRDs-catalog)' }
|
|
187
266
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* у цільовий репозиторій (`.github/actions/setup-bun-deps/action.yml`).
|
|
4
4
|
*
|
|
5
5
|
* Використовується CLI `npx \@nitra/cursor`, щоб workflows з правил `ga` / `js-lint` / `text`
|
|
6
|
-
* могли одразу викликати `uses: ./.github/actions/setup-bun-deps` без
|
|
6
|
+
* могли одразу викликати `uses: ./.github/actions/setup-bun-deps` після кроку `actions/checkout@v6` (без checkout runner не знайде action.yml).
|
|
7
7
|
*
|
|
8
8
|
* Джерело: каталог `github-actions/setup-bun-deps/` у корені tarball пакету (поруч із `mdc/`, `bin/`).
|
|
9
9
|
*/
|