@nitra/cursor 1.6.4 → 1.6.11
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 +8 -2
- package/bin/n-cursor.js +28 -4
- package/mdc/bun.mdc +4 -0
- package/mdc/docker.mdc +39 -0
- package/mdc/ga.mdc +1 -0
- package/mdc/js-lint.mdc +3 -0
- package/mdc/k8s.mdc +59 -0
- package/mdc/text.mdc +13 -4
- package/package.json +1 -1
- package/schemas/n-cursor.json +4 -0
- package/scripts/check-bun.mjs +23 -0
- package/scripts/check-docker.mjs +145 -0
- package/scripts/check-k8s.mjs +292 -0
- package/scripts/check-text.mjs +20 -2
- package/scripts/utils/walkDir.mjs +41 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
|
+
"$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
|
|
17
18
|
"rules": ["js-format", "npm-module", "text"]
|
|
18
19
|
}
|
|
19
20
|
```
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
|
|
31
32
|
```json
|
|
32
33
|
{
|
|
34
|
+
"$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
|
|
33
35
|
"version": "2.5.0",
|
|
34
36
|
"rules": ["js-format", "text"]
|
|
35
37
|
}
|
|
@@ -39,11 +41,15 @@
|
|
|
39
41
|
|
|
40
42
|
```bash
|
|
41
43
|
npx @nitra/cursor
|
|
44
|
+
npx @nitra/cursor check
|
|
45
|
+
npx @nitra/cursor check bun ga
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
Команда `check` запускає programmatic перевірки з каталогу `scripts/` пакету. Якщо в корені репозиторію вже є `.n-cursor.json`, перед перевірками виконується зчитування конфігу — зокрема додається або виправляється поле `$schema`, якщо воно відсутнє або не збігається з очікуваним URL.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
CLI автоматично (команда завантаження правил без підкоманди `check`):
|
|
51
|
+
|
|
52
|
+
1. Знайде або створить `.n-cursor.json` у поточній директорії (із полем `$schema` на JSON Schema пакету; якщо файл уже є без коректного `$schema`, поле буде додано або оновлено при зчитуванні конфігу)
|
|
47
53
|
2. Створить директорію `.cursor/rules/`, якщо її ще немає
|
|
48
54
|
3. Завантажить кожне з перелічених у конфігу правило з unpkg.com і збереже файли з префіксом `n-`
|
|
49
55
|
4. Після оновлення файлів на диску згенерує в корені проєкту **`AGENTS.md`**: повний вміст береться з шаблону пакету `AGENTS.template.md`, а список правил у шаблоні формується з **усіх наявних файлів `*.mdc`** у `.cursor/rules/` (відсортовано за ім’ям)
|
package/bin/n-cursor.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Використання:
|
|
7
7
|
* `npx \@nitra/cursor` — завантажити cursor-правила
|
|
8
|
-
* `npx \@nitra/cursor check` — перевірити правила, перелічені в AGENTS.md (якщо є check-*.mjs)
|
|
8
|
+
* `npx \@nitra/cursor check` — перевірити правила, перелічені в AGENTS.md (якщо є check-*.mjs);
|
|
9
|
+
* якщо в корені вже є `.n-cursor.json`, спочатку зчитується конфіг і за потреби дописується `$schema`
|
|
9
10
|
* `npx \@nitra/cursor check bun` — перевірити лише вказані правила (ігнорує AGENTS.md)
|
|
10
11
|
*
|
|
11
12
|
* Якщо у корені репозиторію немає .n-cursor.json, спочатку перейменовується за наявності nitra-cursor.json;
|
|
12
13
|
* у `.cursor/rules` файли `nitra-*.mdc` перейменовуються на `n-*.mdc`; інакше конфіг створюється автоматично
|
|
13
|
-
* з усіма правилами з каталогу mdc пакету (їх можна відредагувати після створення).
|
|
14
|
+
* з усіма правилами з каталогу mdc пакету (їх можна відредагувати після створення). У файлі завжди має бути
|
|
15
|
+
* поле `$schema` з посиланням на JSON Schema пакету; при зчитуванні конфігу воно додається або виправляється на диску, якщо відсутнє або некоректне.
|
|
14
16
|
*
|
|
15
17
|
* Файл AGENTS.md у корені: щоразу повністю перезаписується змістом з AGENTS.template.md
|
|
16
18
|
* пакету; список правил у шаблоні будується з файлів *.mdc у .cursor/rules поточного проєкту.
|
|
@@ -33,6 +35,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
33
35
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
34
36
|
const UNPKG_BASE = 'https://unpkg.com'
|
|
35
37
|
const CONFIG_FILE = '.n-cursor.json'
|
|
38
|
+
/** URL JSON Schema для `.n-cursor.json` (поле `$schema` у файлі конфігурації) */
|
|
39
|
+
const CONFIG_SCHEMA_URL = `${UNPKG_BASE}/${PACKAGE_NAME}/schemas/n-cursor.json`
|
|
36
40
|
const AGENTS_FILE = 'AGENTS.md'
|
|
37
41
|
const AGENTS_TEMPLATE_FILE = 'AGENTS.template.md'
|
|
38
42
|
const RULES_DIR = '.cursor/rules'
|
|
@@ -144,7 +148,7 @@ async function migrateLegacyConfigIfNeeded() {
|
|
|
144
148
|
|
|
145
149
|
/**
|
|
146
150
|
* Зчитує конфіг .n-cursor.json з поточної директорії
|
|
147
|
-
* @returns {Promise<{rules: string[], skills: string[], version?: string}
|
|
151
|
+
* @returns {Promise<{ $schema: string, rules: string[], skills: string[], version?: string } & Record<string, unknown>>} rules, skills (id без префікса n-), опційно version; при відсутності файлу створює дефолтний конфіг
|
|
148
152
|
*/
|
|
149
153
|
async function readConfig() {
|
|
150
154
|
await migrateLegacyConfigIfNeeded()
|
|
@@ -152,7 +156,7 @@ async function readConfig() {
|
|
|
152
156
|
if (!existsSync(configPath)) {
|
|
153
157
|
const rules = await discoverBundledRuleNames()
|
|
154
158
|
const skills = await discoverBundledSkillNames()
|
|
155
|
-
const defaultConfig = { rules, skills }
|
|
159
|
+
const defaultConfig = { $schema: CONFIG_SCHEMA_URL, rules, skills }
|
|
156
160
|
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
|
|
157
161
|
console.log(
|
|
158
162
|
`📝 Створено ${CONFIG_FILE} з усіма правилами (${rules.length}) і skills (${skills.length}) з пакету. За потреби відредагуйте списки.\n`
|
|
@@ -175,6 +179,15 @@ async function readConfig() {
|
|
|
175
179
|
}
|
|
176
180
|
config.skills = await discoverBundledSkillNames()
|
|
177
181
|
}
|
|
182
|
+
|
|
183
|
+
if (config.$schema !== CONFIG_SCHEMA_URL) {
|
|
184
|
+
const { $schema: _omit, ...rest } = config
|
|
185
|
+
const normalized = { $schema: CONFIG_SCHEMA_URL, ...rest }
|
|
186
|
+
await writeFile(configPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
|
187
|
+
console.log(`📝 Оновлено поле $schema у ${CONFIG_FILE}\n`)
|
|
188
|
+
return normalized
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
return config
|
|
179
192
|
}
|
|
180
193
|
|
|
@@ -546,6 +559,17 @@ async function runChecks(requestedRules) {
|
|
|
546
559
|
throw new Error('No check scripts found')
|
|
547
560
|
}
|
|
548
561
|
|
|
562
|
+
const root = cwd()
|
|
563
|
+
const legacyConfigPath = join(root, 'nitra-cursor.json')
|
|
564
|
+
if (existsSync(join(root, CONFIG_FILE)) || existsSync(legacyConfigPath)) {
|
|
565
|
+
try {
|
|
566
|
+
await readConfig()
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error(`❌ ${error.message}`)
|
|
569
|
+
throw error
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
549
573
|
let rulesToCheck
|
|
550
574
|
if (requestedRules.length > 0) {
|
|
551
575
|
rulesToCheck = requestedRules
|
package/mdc/bun.mdc
CHANGED
package/mdc/docker.mdc
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Dockerfile — лінт через hadolint (локально або Docker); перевірка check-docker
|
|
3
|
+
version: '1.0'
|
|
4
|
+
globs: "**/Dockerfile*"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Docker — hadolint
|
|
9
|
+
|
|
10
|
+
[hadolint](https://github.com/hadolint/hadolint) перевіряє Dockerfile на типові помилки та рекомендації (`FROM`, `RUN`, `COPY`, shell form тощо).
|
|
11
|
+
|
|
12
|
+
## Область
|
|
13
|
+
|
|
14
|
+
- Усі файли з іменем **`Dockerfile`** або **`Dockerfile.*`** (наприклад `Dockerfile.prod`) у репозиторії, крім ігнорованих каталогів (`node_modules`, `.git`, `dist`, …) — як у **`check-docker.mjs`**.
|
|
15
|
+
- Також скрипт перевірки обробляє **`Containerfile`** та **`Containerfile.*`** (Podman / альтернативні імена), навіть якщо glob правила спрацьовує переважно на `Dockerfile*`.
|
|
16
|
+
|
|
17
|
+
## Запуск
|
|
18
|
+
|
|
19
|
+
1. **`npx @nitra/cursor check docker`** — те саме, що рекомендовано після змін у Dockerfile.
|
|
20
|
+
2. Спочатку викликається бінарник **`hadolint`** з `PATH`; якщо його немає — **`docker run`** з образом **`hadolint/hadolint:v2.12.0`** (тег узгоджуй з **`HADOLINT_IMAGE`** у `npm/scripts/check-docker.mjs`).
|
|
21
|
+
3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure).
|
|
22
|
+
|
|
23
|
+
Якщо в репозиторії немає жодного відповідного файлу — перевірка пропускається (exit 0).
|
|
24
|
+
|
|
25
|
+
## Агентам
|
|
26
|
+
|
|
27
|
+
- Після правок у Dockerfile проганяй **`check docker`**.
|
|
28
|
+
- Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або правила в **`.hadolint.yaml`**.
|
|
29
|
+
- Образи на базі Bun — див. **`n-bun.mdc`**.
|
|
30
|
+
|
|
31
|
+
## Редактор
|
|
32
|
+
|
|
33
|
+
Розширення VS Code / Cursor: **`exiasr.hadolint`** (потрібен бінарник hadolint) — за потреби в **`.vscode/extensions.json`**.
|
|
34
|
+
|
|
35
|
+
## Перевірка
|
|
36
|
+
|
|
37
|
+
`npx @nitra/cursor check docker`
|
|
38
|
+
|
|
39
|
+
Kubernetes YAML і `$schema` у `k8s/` — окреме правило **`k8s.mdc`**, **`check k8s`**.
|
package/mdc/ga.mdc
CHANGED
package/mdc/js-lint.mdc
CHANGED
|
@@ -29,6 +29,7 @@ version: '1.7'
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
|
+
|
|
32
33
|
У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт. За потреби додай `ignore` (дзеркальні каталоги, шаблони) та `minLines`, щоб відсікти дрібні збіги:
|
|
33
34
|
|
|
34
35
|
```json title=".jscpd.json"
|
|
@@ -125,6 +126,7 @@ export default [
|
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
|
|
129
|
+
|
|
128
130
|
## Додаткові js правила
|
|
129
131
|
|
|
130
132
|
Завжди додавай до package.json що підтримується 24+ версія node:
|
|
@@ -134,6 +136,7 @@ export default [
|
|
|
134
136
|
"node": ">=24"
|
|
135
137
|
}
|
|
136
138
|
```
|
|
139
|
+
|
|
137
140
|
**Код:** синтаксис Node **24+**, **top level await**.
|
|
138
141
|
|
|
139
142
|
## Перевірка
|
package/mdc/k8s.mdc
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: K8s YAML — перший рядок yaml-language-server $schema; перевірка check-k8s за apiVersion/kind
|
|
3
|
+
version: '1.3'
|
|
4
|
+
globs: "**/k8s/**/*.{yaml,yml}"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Kubernetes YAML у шляхах з `k8s`
|
|
9
|
+
|
|
10
|
+
Для кожного файлу `*.yaml` або `*.yml`, у шляху якого є сегмент директорії **`k8s`** (наприклад `site/k8s/base/deployment.yaml`), **перший рядок** — коментар-директива для [YAML Language Server](https://github.com/redhat-developer/yaml-language-server):
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
# yaml-language-server: $schema=https://...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Далі — вміст маніфесту. Зайвий порожній рядок між коментарем і YAML не додавай, якщо в проєкті не прийнято інше.
|
|
17
|
+
|
|
18
|
+
**Розширення:** усі маніфести — **`.yaml`**. Виняток: **`kustomization.yml`** (і `kustomization.yaml`) дозволені за іменем.
|
|
19
|
+
|
|
20
|
+
**Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
|
|
21
|
+
|
|
22
|
+
## Перевірка
|
|
23
|
+
|
|
24
|
+
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче. Якщо під `k8s` немає yaml/yml — перевірку пропущено. Синтаксис YAML і зміст маніфесту скрипт не перевіряє — вручну.
|
|
25
|
+
|
|
26
|
+
## Що закодовано в `check-k8s.mjs`
|
|
27
|
+
|
|
28
|
+
При зміні правил синхронно оновлюй **`YANNH_PIN`**, **`YANNH_GROUPS`**.
|
|
29
|
+
|
|
30
|
+
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
31
|
+
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree.
|
|
32
|
+
|
|
33
|
+
## Коли застосовувати (агентам)
|
|
34
|
+
|
|
35
|
+
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
36
|
+
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
37
|
+
|
|
38
|
+
## Визначення схеми YAML (канон)
|
|
39
|
+
|
|
40
|
+
Орієнтир — **перший документ** (до наступного `---`).
|
|
41
|
+
|
|
42
|
+
1. **Ім’я** `kustomization.yaml` або `kustomization.yml` → `https://json.schemastore.org/kustomization.json`.
|
|
43
|
+
2. **`apiVersion: v1`** → yannh, PIN **`v1.29.1-standalone-strict`**:
|
|
44
|
+
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/<PIN>/<kind>-v1.json`
|
|
45
|
+
`<kind>`: літери в нижньому регістрі без роздільників між CamelCase (наприклад `Service` → `service`).
|
|
46
|
+
3. **`apiVersion: group/version`** і **group** у **`YANNH_GROUPS`** у скрипті → yannh:
|
|
47
|
+
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/<PIN>/<kind>-<group-з-крапками-як-дефіси>-<version>.json`
|
|
48
|
+
Приклади: `apps/v1` + `Deployment` → `deployment-apps-v1.json`; `networking.k8s.io/v1` + `Ingress` → `ingress-networking-k8s-io-v1.json`.
|
|
49
|
+
4. **Інакше** (CRD тощо) → [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog):
|
|
50
|
+
`https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/CRDs/<group>/<kind>_<version>.json`
|
|
51
|
+
5. **Немає надійного публічного URL** — не вигадуй: залиш коректний `$schema` або `file:` за узгодженням.
|
|
52
|
+
|
|
53
|
+
## Багатодокументні YAML
|
|
54
|
+
|
|
55
|
+
Одна схема на файл; скрипт звіряє **перший** документ. Інші `kind` у тому ж файлі — розділи файли або узгодь у рев’ю.
|
|
56
|
+
|
|
57
|
+
## Редактор
|
|
58
|
+
|
|
59
|
+
Для `$schema` у VS Code / Cursor: **Red Hat YAML** (`redhat.vscode-yaml`) — за потреби в **`.vscode/extensions.json`**.
|
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.17'
|
|
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`**.
|
|
@@ -12,7 +12,8 @@ version: '1.16'
|
|
|
12
12
|
"dbaeumer.vscode-eslint",
|
|
13
13
|
"github.vscode-github-actions",
|
|
14
14
|
"oxc.oxc-vscode",
|
|
15
|
-
"DavidAnson.vscode-markdownlint"
|
|
15
|
+
"DavidAnson.vscode-markdownlint",
|
|
16
|
+
"redhat.vscode-yaml"
|
|
16
17
|
]
|
|
17
18
|
}
|
|
18
19
|
```
|
|
@@ -30,7 +31,14 @@ version: '1.16'
|
|
|
30
31
|
}
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
**v8r:** чотири виклики `(bunx v8r "<glob>" || [ $? -eq 98 ])` — exit **98**, якщо glob порожній.
|
|
34
|
+
**v8r:** чотири виклики `(bunx v8r "<glob>" || [ $? -eq 98 ])` — exit **98**, якщо glob порожній. Враховує `.gitignore`.
|
|
35
|
+
|
|
36
|
+
У корені проєкту має бути **`.v8rignore`**: **v8r** шукає схему в [Schema Store](https://www.schemastore.org/); для JSON без запису там перевірка завершується помилкою. Мінімум виключи **`.vscode/extensions.json`** і **`.vscode/settings.json`** (немає стабільної схеми в каталозі). Інші файли без схеми — за тією ж логікою; не розширюй ignore, щоб приховати проблеми там, де схема є.
|
|
37
|
+
|
|
38
|
+
```text title=".v8rignore"
|
|
39
|
+
.vscode/extensions.json
|
|
40
|
+
.vscode/settings.json
|
|
41
|
+
```
|
|
34
42
|
|
|
35
43
|
```json title=".markdownlint-cli2.jsonc"
|
|
36
44
|
{
|
|
@@ -52,6 +60,7 @@ version: '1.16'
|
|
|
52
60
|
У корені проєкту має бути `.cspell.json` і залежності для cspell у кореневому `package.json` (зазвичай `devDependencies`).
|
|
53
61
|
|
|
54
62
|
Додай workflow `.github/workflows/lint-text.yml`:
|
|
63
|
+
|
|
55
64
|
```yaml title=".github/workflows/lint-text.yml"
|
|
56
65
|
name: Lint Text
|
|
57
66
|
|
|
@@ -63,6 +72,7 @@ on:
|
|
|
63
72
|
- '.cspell.json'
|
|
64
73
|
- '.gitignore'
|
|
65
74
|
- '.markdownlint-cli2.jsonc'
|
|
75
|
+
- '.v8rignore'
|
|
66
76
|
- '**/*.js'
|
|
67
77
|
- '**/*.ts'
|
|
68
78
|
- '**/*.vue'
|
|
@@ -202,7 +212,6 @@ jobs:
|
|
|
202
212
|
|
|
203
213
|
Для іншої мови встанови відповідний пакет `@cspell/dict-*`, додай його `cspell-ext.json` у `import` і код мови в `language`. Огляд словників: [streetsidesoftware/cspell-dicts](https://github.com/streetsidesoftware/cspell-dicts).
|
|
204
214
|
|
|
205
|
-
|
|
206
215
|
## Перевірка
|
|
207
216
|
|
|
208
217
|
`npx @nitra/cursor check text`
|
package/package.json
CHANGED
package/schemas/n-cursor.json
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": true,
|
|
8
8
|
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Посилання на JSON Schema для автодоповнення та валідації в IDE; має збігатися з $id цієї схеми (рекомендовано завжди вказувати)."
|
|
12
|
+
},
|
|
9
13
|
"rules": {
|
|
10
14
|
"type": "array",
|
|
11
15
|
"description": "Ідентифікатори правил без префікса n- (відповідають файлам n-<id>.mdc).",
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Очікує наявність `bun.lock`, забороняє lockfile та артефакти yarn/pnpm, директорію `.yarn`
|
|
5
5
|
* і поле `packageManager` у кореневому `package.json`.
|
|
6
|
+
*
|
|
7
|
+
* Якщо в кореневому `package.json` є скрипти з префіксом `lint-`, перевіряє наявність агрегованого
|
|
8
|
+
* скрипта `lint`, у якому через `bun run <ім’я>` викликаються всі такі скрипти.
|
|
6
9
|
*/
|
|
7
10
|
import { existsSync } from 'node:fs'
|
|
8
11
|
import { readFile } from 'node:fs/promises'
|
|
@@ -47,6 +50,26 @@ export async function check() {
|
|
|
47
50
|
} else {
|
|
48
51
|
pass('package.json не містить packageManager')
|
|
49
52
|
}
|
|
53
|
+
|
|
54
|
+
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
55
|
+
const lintPrefixed = Object.keys(scripts).filter(name => name.startsWith('lint-'))
|
|
56
|
+
if (lintPrefixed.length > 0) {
|
|
57
|
+
const aggregate = typeof scripts.lint === 'string' ? scripts.lint : ''
|
|
58
|
+
if (aggregate.trim()) {
|
|
59
|
+
const missing = lintPrefixed.filter(name => !aggregate.includes(`bun run ${name}`))
|
|
60
|
+
if (missing.length > 0) {
|
|
61
|
+
fail(
|
|
62
|
+
`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missing.map(s => `\`${s}\``).join(', ')}`
|
|
63
|
+
)
|
|
64
|
+
} else {
|
|
65
|
+
pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
fail(
|
|
69
|
+
`У package.json є скрипти ${lintPrefixed.map(s => `\`${s}\``).join(', ')}, але немає агрегованого \`lint\` — додай скрипт, який запускає їх через \`bun run\``
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
return exitCode
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запускає hadolint для Dockerfile / Containerfile у всьому репозиторії (див. docker.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
|
|
5
|
+
* тощо. Спочатку бінарник hadolint з PATH, інакше docker run з образом hadolint/hadolint.
|
|
6
|
+
* Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { basename, relative, sep } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { pass } from './utils/pass.mjs'
|
|
12
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
13
|
+
|
|
14
|
+
/** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
|
|
15
|
+
const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Чи є basename Dockerfile / Containerfile (у т.ч. Dockerfile.prod).
|
|
19
|
+
* @param {string} name basename шляху
|
|
20
|
+
* @returns {boolean} true для Dockerfile / Dockerfile.* / Containerfile / Containerfile.*
|
|
21
|
+
*/
|
|
22
|
+
function isDockerfileName(name) {
|
|
23
|
+
const n = name.toLowerCase()
|
|
24
|
+
if (n === 'dockerfile' || n === 'containerfile') return true
|
|
25
|
+
if (n.startsWith('dockerfile.') || n.startsWith('containerfile.')) return true
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Збирає абсолютні шляхи до Dockerfile / Containerfile від кореня cwd.
|
|
31
|
+
* @param {string} root корінь репозиторію
|
|
32
|
+
* @returns {Promise<string[]>} відсортовані абсолютні шляхи
|
|
33
|
+
*/
|
|
34
|
+
async function findDockerfilePaths(root) {
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const out = []
|
|
37
|
+
await walkDir(root, p => {
|
|
38
|
+
if (isDockerfileName(basename(p))) out.push(p)
|
|
39
|
+
})
|
|
40
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Відносний шлях від root з прямими слешами (hadolint у контейнері).
|
|
45
|
+
* @param {string} root корінь
|
|
46
|
+
* @param {string} absPath абсолютний шлях
|
|
47
|
+
* @returns {string} відносний шлях з прямими слешами
|
|
48
|
+
*/
|
|
49
|
+
function posixRel(root, absPath) {
|
|
50
|
+
return relative(root, absPath).split(sep).join('/')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Запуск hadolint: спочатку PATH, інакше Docker.
|
|
55
|
+
* @param {string} root корінь репозиторію
|
|
56
|
+
* @param {string} absPath абсолютний шлях до Dockerfile
|
|
57
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint та канал запуску
|
|
58
|
+
*/
|
|
59
|
+
function lintDockerfileWithHadolint(root, absPath) {
|
|
60
|
+
const rel = posixRel(root, absPath)
|
|
61
|
+
const local = spawnSync('hadolint', [rel], {
|
|
62
|
+
cwd: root,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
maxBuffer: 10 * 1024 * 1024
|
|
65
|
+
})
|
|
66
|
+
if (!local.error) {
|
|
67
|
+
const ok = local.status === 0
|
|
68
|
+
return {
|
|
69
|
+
ok,
|
|
70
|
+
stdout: local.stdout ?? '',
|
|
71
|
+
stderr: local.stderr ?? '',
|
|
72
|
+
via: 'hadolint'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (local.error.code !== 'ENOENT') {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
stdout: '',
|
|
79
|
+
stderr: local.error.message,
|
|
80
|
+
via: 'hadolint'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const docker = spawnSync(
|
|
85
|
+
'docker',
|
|
86
|
+
['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel],
|
|
87
|
+
{
|
|
88
|
+
cwd: root,
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
maxBuffer: 10 * 1024 * 1024
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
if (docker.error) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
stdout: '',
|
|
97
|
+
stderr:
|
|
98
|
+
`Не знайдено hadolint у PATH і не вдалося запустити Docker (${docker.error.message}). ` +
|
|
99
|
+
`Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).`,
|
|
100
|
+
via: 'docker'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const ok = docker.status === 0
|
|
104
|
+
return {
|
|
105
|
+
ok,
|
|
106
|
+
stdout: docker.stdout ?? '',
|
|
107
|
+
stderr: docker.stderr ?? '',
|
|
108
|
+
via: 'docker'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Перевіряє Dockerfile / Containerfile через hadolint (docker.mdc).
|
|
114
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є зауваження або помилка запуску
|
|
115
|
+
*/
|
|
116
|
+
export async function check() {
|
|
117
|
+
let exitCode = 0
|
|
118
|
+
const fail = msg => {
|
|
119
|
+
console.log(` ❌ ${msg}`)
|
|
120
|
+
exitCode = 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const root = process.cwd()
|
|
124
|
+
const files = await findDockerfilePaths(root)
|
|
125
|
+
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
pass('Немає Dockerfile / Containerfile — перевірку hadolint пропущено')
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pass(`Знайдено файлів для hadolint: ${files.length}`)
|
|
132
|
+
|
|
133
|
+
for (const abs of files) {
|
|
134
|
+
const rel = posixRel(root, abs) || basename(abs)
|
|
135
|
+
const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
|
|
136
|
+
const tail = (stdout + stderr).trim()
|
|
137
|
+
if (ok) {
|
|
138
|
+
pass(`${rel} (${via})`)
|
|
139
|
+
} else {
|
|
140
|
+
fail(`${rel} (${via})${tail ? `:\n${tail}` : ''}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return exitCode
|
|
145
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє Kubernetes YAML у шляхах з сегментом `k8s` (див. k8s.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Перший рядок `# yaml-language-server: $schema=…`, без дублікатів, розширення `.yaml`
|
|
5
|
+
* (окрім `kustomization.yml`); URL схеми за першим документом — kustomization / yannh / datree.
|
|
6
|
+
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { basename, relative } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { pass } from './utils/pass.mjs'
|
|
12
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
13
|
+
|
|
14
|
+
/** Версія набору схем yannh — узгоджено з k8s.mdc */
|
|
15
|
+
const YANNH_PIN = 'v1.29.1-standalone-strict'
|
|
16
|
+
|
|
17
|
+
const KUSTOMIZATION_SCHEMA = 'https://json.schemastore.org/kustomization.json'
|
|
18
|
+
|
|
19
|
+
const YANNH_BASE = `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/${YANNH_PIN}/`
|
|
20
|
+
|
|
21
|
+
const DATREE_CRD_BASE = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/CRDs/'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Групи API Kubernetes, для яких у перевірці очікується схема yannh (не datree CRD catalog).
|
|
25
|
+
* `gateway.networking.k8s.io` та інші розширення поза цим списком — datree.
|
|
26
|
+
*/
|
|
27
|
+
const YANNH_GROUPS = new Set([
|
|
28
|
+
'admissionregistration.k8s.io',
|
|
29
|
+
'apiextensions.k8s.io',
|
|
30
|
+
'apiregistration.k8s.io',
|
|
31
|
+
'apps',
|
|
32
|
+
'authentication.k8s.io',
|
|
33
|
+
'authorization.k8s.io',
|
|
34
|
+
'autoscaling',
|
|
35
|
+
'batch',
|
|
36
|
+
'certificates.k8s.io',
|
|
37
|
+
'coordination.k8s.io',
|
|
38
|
+
'discovery.k8s.io',
|
|
39
|
+
'events.k8s.io',
|
|
40
|
+
'flowcontrol.apiserver.k8s.io',
|
|
41
|
+
'internal.apiserver.k8s.io',
|
|
42
|
+
'networking.k8s.io',
|
|
43
|
+
'node.k8s.io',
|
|
44
|
+
'policy',
|
|
45
|
+
'rbac.authorization.k8s.io',
|
|
46
|
+
'resource.k8s.io',
|
|
47
|
+
'scheduling.k8s.io',
|
|
48
|
+
'storage.k8s.io',
|
|
49
|
+
'storagemigration.k8s.io'
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Чи містить шлях сегмент директорії `k8s` (рівно ця назва компонента).
|
|
56
|
+
* @param {string} filePath шлях до файлу
|
|
57
|
+
* @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
|
|
58
|
+
*/
|
|
59
|
+
function pathHasK8sSegment(filePath) {
|
|
60
|
+
const parts = filePath.split(/[/\\]/u)
|
|
61
|
+
return parts.includes('k8s')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Збирає всі yaml/yml під деревом від кореня cwd, якщо шлях містить сегмент `k8s`.
|
|
66
|
+
* @param {string} root корінь репозиторію (cwd)
|
|
67
|
+
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
|
|
68
|
+
*/
|
|
69
|
+
async function findK8sYamlFiles(root) {
|
|
70
|
+
/** @type {string[]} */
|
|
71
|
+
const out = []
|
|
72
|
+
await walkDir(root, p => {
|
|
73
|
+
if (!pathHasK8sSegment(p)) return
|
|
74
|
+
if (!/\.ya?ml$/iu.test(p)) return
|
|
75
|
+
out.push(p)
|
|
76
|
+
})
|
|
77
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Прибирає BOM і ділить на рядки.
|
|
82
|
+
* @param {string} content вміст файлу
|
|
83
|
+
* @returns {string[]} рядки без BOM на початку
|
|
84
|
+
*/
|
|
85
|
+
function toLines(content) {
|
|
86
|
+
const body = content.startsWith('\uFEFF') ? content.slice(1) : content
|
|
87
|
+
return body.split(/\r?\n/u)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Вміст після першого рядка (modeline), без провідних порожніх рядків.
|
|
92
|
+
* @param {string[]} lines рядки файлу
|
|
93
|
+
* @returns {string} тіло для парсингу першого YAML-документа
|
|
94
|
+
*/
|
|
95
|
+
function yamlBodyAfterModeline(lines) {
|
|
96
|
+
let i = 1
|
|
97
|
+
while (i < lines.length && lines[i].trim() === '') i++
|
|
98
|
+
return lines.slice(i).join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Перший YAML-документ (до наступного `---` на окремому рядку).
|
|
103
|
+
* @param {string} body фрагмент YAML
|
|
104
|
+
* @returns {string} перший документ без зайвих пробілів по краях
|
|
105
|
+
*/
|
|
106
|
+
function firstYamlDocument(body) {
|
|
107
|
+
const parts = body.split(/^---\s*$/mu)
|
|
108
|
+
return (parts[0] ?? body).trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Витягує `apiVersion` та `kind` з тексту документа (без повного YAML-парсера).
|
|
113
|
+
* @param {string} doc фрагмент YAML одного документа
|
|
114
|
+
* @returns {{ apiVersion?: string, kind?: string }} знайдені поля або властивості відсутні
|
|
115
|
+
*/
|
|
116
|
+
function extractApiVersionAndKind(doc) {
|
|
117
|
+
const av = doc.match(/^\s*apiVersion:\s*(\S+)\s*$/mu)
|
|
118
|
+
const k = doc.match(/^\s*kind:\s*(\S+)\s*$/mu)
|
|
119
|
+
return {
|
|
120
|
+
apiVersion: av?.[1]?.replaceAll(/^["']|["']$/gu, ''),
|
|
121
|
+
kind: k?.[1]?.replaceAll(/^["']|["']$/gu, '')
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Kind для імен файлів yannh/datree: лише літери та цифри, нижній регістр (Service → service, HTTPRoute → httproute).
|
|
127
|
+
* @param {string} kind значення поля kind
|
|
128
|
+
* @returns {string} рядок для шаблону імені файлу схеми
|
|
129
|
+
*/
|
|
130
|
+
function kindToSchemaFilePart(kind) {
|
|
131
|
+
return kind.replaceAll(/[^a-zA-Z0-9]/gu, '').toLowerCase()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Очікуваний $schema для маніфесту згідно з k8s.mdc.
|
|
136
|
+
* @param {string} filePath шлях до файлу (для імені kustomization)
|
|
137
|
+
* @param {string} doc перший YAML-документ після modeline
|
|
138
|
+
* @returns {{ expected: string | null, reason: string }} reason — для повідомлень про помилку
|
|
139
|
+
*/
|
|
140
|
+
function expectedSchemaUrl(filePath, doc) {
|
|
141
|
+
const base = basename(filePath)
|
|
142
|
+
const baseLower = base.toLowerCase()
|
|
143
|
+
|
|
144
|
+
if (baseLower === 'kustomization.yaml' || baseLower === 'kustomization.yml') {
|
|
145
|
+
return { expected: KUSTOMIZATION_SCHEMA, reason: 'kustomization (ім’я файлу)' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { apiVersion, kind } = extractApiVersionAndKind(doc)
|
|
149
|
+
if (!apiVersion || !kind) {
|
|
150
|
+
return {
|
|
151
|
+
expected: null,
|
|
152
|
+
reason: 'не знайдено apiVersion/kind у першому документі (потрібні для перевірки $schema)'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (apiVersion === 'v1') {
|
|
157
|
+
const k = kindToSchemaFilePart(kind)
|
|
158
|
+
return { expected: `${YANNH_BASE}${k}-v1.json`, reason: 'core v1 (yannh)' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!apiVersion.includes('/')) {
|
|
162
|
+
return {
|
|
163
|
+
expected: null,
|
|
164
|
+
reason: `нестандартний apiVersion "${apiVersion}" — очікується v1 або group/version`
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const slash = apiVersion.indexOf('/')
|
|
169
|
+
const group = apiVersion.slice(0, Math.max(0, slash))
|
|
170
|
+
const version = apiVersion.slice(slash + 1)
|
|
171
|
+
const kindPart = kindToSchemaFilePart(kind)
|
|
172
|
+
const groupDash = group.replaceAll('.', '-')
|
|
173
|
+
|
|
174
|
+
if (YANNH_GROUPS.has(group)) {
|
|
175
|
+
const url = `${YANNH_BASE}${kindPart}-${groupDash}-${version}.json`
|
|
176
|
+
return { expected: url, reason: 'вбудований API Kubernetes (yannh)' }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const datreeKind = kindToSchemaFilePart(kind)
|
|
180
|
+
const url = `${DATREE_CRD_BASE}${group}/${datreeKind}_${version}.json`
|
|
181
|
+
return { expected: url, reason: 'CRD / група поза yannh (datree CRDs-catalog)' }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Підраховує рядки з modeline $schema у файлі.
|
|
186
|
+
* @param {string[]} lines рядки файлу
|
|
187
|
+
* @returns {number} скільки рядків містять modeline `$schema`
|
|
188
|
+
*/
|
|
189
|
+
function countSchemaModelines(lines) {
|
|
190
|
+
return lines.filter(l => /^\s*#\s*yaml-language-server:\s*\$schema=\S+/u.test(l.trim())).length
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Перевіряє один YAML у дереві k8s (modeline, схема).
|
|
195
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
196
|
+
* @param {string} root корінь репозиторію
|
|
197
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
198
|
+
* @param {(msg: string) => void} pass реєстрація успіху
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
*/
|
|
201
|
+
async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
202
|
+
const rel = relative(root, abs) || abs
|
|
203
|
+
const base = basename(abs)
|
|
204
|
+
const baseLower = base.toLowerCase()
|
|
205
|
+
|
|
206
|
+
if (baseLower.endsWith('.yml') && baseLower !== 'kustomization.yml') {
|
|
207
|
+
fail(`${rel}: розширення .yml — перейменуй на .yaml (див. k8s.mdc)`)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let raw
|
|
212
|
+
try {
|
|
213
|
+
raw = await readFile(abs, 'utf8')
|
|
214
|
+
} catch (error) {
|
|
215
|
+
fail(`${rel}: не вдалося прочитати (${error.message})`)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lines = toLines(raw)
|
|
220
|
+
if (lines.length === 0 || lines[0].trim() === '') {
|
|
221
|
+
fail(`${rel}: перший рядок порожній — потрібен # yaml-language-server: $schema=…`)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const m = lines[0].match(MODELINE_RE)
|
|
226
|
+
if (!m) {
|
|
227
|
+
fail(
|
|
228
|
+
`${rel}: перший рядок має бути коментарем # yaml-language-server: $schema=<url> (без префіксів перед #)`
|
|
229
|
+
)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const schemaUrl = m[1]
|
|
234
|
+
if (countSchemaModelines(lines) > 1) {
|
|
235
|
+
fail(`${rel}: кілька рядків yaml-language-server $schema — лиш один modeline на файл (див. k8s.mdc)`)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (schemaUrl.startsWith('file:')) {
|
|
240
|
+
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!/^https:/iu.test(schemaUrl)) {
|
|
245
|
+
fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const body = yamlBodyAfterModeline(lines)
|
|
250
|
+
const doc = firstYamlDocument(body)
|
|
251
|
+
const { expected, reason } = expectedSchemaUrl(abs, doc)
|
|
252
|
+
|
|
253
|
+
if (expected === null) {
|
|
254
|
+
fail(`${rel}: ${reason}`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (schemaUrl !== expected) {
|
|
259
|
+
fail(`${rel}: $schema не відповідає правилу (${reason}). Очікується:\n ${expected}\n Зараз: ${schemaUrl}`)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
pass(`${rel}: $schema узгоджено (${reason})`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Перевіряє відповідність проєкту правилам k8s.mdc.
|
|
268
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
269
|
+
*/
|
|
270
|
+
export async function check() {
|
|
271
|
+
let exitCode = 0
|
|
272
|
+
const fail = msg => {
|
|
273
|
+
console.log(` ❌ ${msg}`)
|
|
274
|
+
exitCode = 1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const root = process.cwd()
|
|
278
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
279
|
+
|
|
280
|
+
if (yamlFiles.length === 0) {
|
|
281
|
+
pass('Немає yaml/yml під k8s — перевірку $schema пропущено')
|
|
282
|
+
return 0
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
|
|
286
|
+
|
|
287
|
+
for (const abs of yamlFiles) {
|
|
288
|
+
await checkK8sYamlFile(abs, root, fail, pass)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return exitCode
|
|
292
|
+
}
|
package/scripts/check-text.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє текстовий стек за правилом text.mdc.
|
|
3
3
|
*
|
|
4
|
-
* cspell, markdownlint-cli2, скрипт `lint-text` з чотирма викликами v8r,
|
|
5
|
-
* розширення VSCode для markdownlint.
|
|
4
|
+
* cspell, markdownlint-cli2, скрипт `lint-text` з чотирма викликами v8r, `.v8rignore` (vscode JSON),
|
|
5
|
+
* workflow `lint-text.yml`, розширення VSCode для markdownlint.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import { readFile } from 'node:fs/promises'
|
|
@@ -20,6 +20,24 @@ export async function check() {
|
|
|
20
20
|
exitCode = 1
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const v8rIgnoreRequired = ['.vscode/extensions.json', '.vscode/settings.json']
|
|
24
|
+
if (existsSync('.v8rignore')) {
|
|
25
|
+
const raw = await readFile('.v8rignore', 'utf8')
|
|
26
|
+
const lines = new Set(raw
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map(l => l.trim())
|
|
29
|
+
.filter(l => l.length > 0 && !l.startsWith('#')))
|
|
30
|
+
for (const path of v8rIgnoreRequired) {
|
|
31
|
+
if (lines.has(path)) {
|
|
32
|
+
pass(`.v8rignore містить ${path}`)
|
|
33
|
+
} else {
|
|
34
|
+
fail(`.v8rignore: додай рядок "${path}" (JSON без схеми в Schema Store — див. n-text.mdc)`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
fail('.v8rignore не існує — створи згідно n-text.mdc (мінімум .vscode/extensions.json і .vscode/settings.json)')
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
if (existsSync('.vscode/extensions.json')) {
|
|
24
42
|
try {
|
|
25
43
|
const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Рекурсивний обхід каталогів для скриптів перевірки (Dockerfile, k8s YAML тощо).
|
|
3
|
+
*
|
|
4
|
+
* Обходить дерево від заданого кореня; для кожного звичайного файлу викликає переданий callback.
|
|
5
|
+
* Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться. Якщо readdir для
|
|
6
|
+
* каталогу не вдається — тихо виходить без throw.
|
|
7
|
+
*/
|
|
8
|
+
import { readdir } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Рекурсивно обходить каталог, пропускає типові артефакти збірки та залежностей.
|
|
13
|
+
* @param {string} dir абсолютний шлях
|
|
14
|
+
* @param {(filePath: string) => void} onFile виклик для кожного файлу
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
export async function walkDir(dir, onFile) {
|
|
18
|
+
let entries
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
21
|
+
} catch {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
const p = join(dir, e.name)
|
|
26
|
+
if (e.isDirectory()) {
|
|
27
|
+
const skipDir =
|
|
28
|
+
e.name === 'node_modules' ||
|
|
29
|
+
e.name === '.git' ||
|
|
30
|
+
e.name === 'dist' ||
|
|
31
|
+
e.name === 'coverage' ||
|
|
32
|
+
e.name === '.turbo' ||
|
|
33
|
+
e.name === '.next'
|
|
34
|
+
if (!skipDir) {
|
|
35
|
+
await walkDir(p, onFile)
|
|
36
|
+
}
|
|
37
|
+
} else if (e.isFile()) {
|
|
38
|
+
onFile(p)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|