@nitra/cursor 1.6.5 → 1.6.12
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/mdc/bun.mdc +4 -0
- package/mdc/docker.mdc +39 -0
- package/mdc/ga.mdc +1 -0
- package/mdc/js-lint.mdc +6 -0
- package/mdc/k8s.mdc +59 -0
- package/mdc/text.mdc +14 -4
- package/package.json +1 -1
- package/schemas/n-cursor.json +9 -4
- package/schemas/v8r-catalog.json +18 -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
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
17
|
"$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
|
|
18
|
-
"rules": ["js-format", "npm-module", "text"]
|
|
18
|
+
"rules": ["js-format", "npm-module", "text"],
|
|
19
|
+
"skills": ["fix"]
|
|
19
20
|
}
|
|
20
21
|
```
|
|
21
22
|
|
|
@@ -33,10 +34,15 @@
|
|
|
33
34
|
{
|
|
34
35
|
"$schema": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
|
|
35
36
|
"version": "2.5.0",
|
|
36
|
-
"rules": ["js-format", "text"]
|
|
37
|
+
"rules": ["js-format", "text"],
|
|
38
|
+
"skills": ["fix"]
|
|
37
39
|
}
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
### v8r і власний каталог схем
|
|
43
|
+
|
|
44
|
+
Для перевірки `.n-cursor.json` через [v8r](https://chris48s.github.io/v8r/) передайте каталог пакета (`-c` / `--catalogs`): локально `node_modules/@nitra/cursor/schemas/v8r-catalog.json`, після публікації також [unpkg](https://unpkg.com/@nitra/cursor/schemas/v8r-catalog.json). JSON Schema конфігурації: [n-cursor.json](https://unpkg.com/@nitra/cursor/schemas/n-cursor.json).
|
|
45
|
+
|
|
40
46
|
## Запуск
|
|
41
47
|
|
|
42
48
|
```bash
|
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"
|
|
@@ -118,6 +119,9 @@ jobs:
|
|
|
118
119
|
import { getConfig } from '@nitra/eslint-config'
|
|
119
120
|
|
|
120
121
|
export default [
|
|
122
|
+
{
|
|
123
|
+
ignores: ['**/auto-imports.d.ts']
|
|
124
|
+
},
|
|
121
125
|
...getConfig({
|
|
122
126
|
node: ['npm']
|
|
123
127
|
})
|
|
@@ -125,6 +129,7 @@ export default [
|
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
|
|
132
|
+
|
|
128
133
|
## Додаткові js правила
|
|
129
134
|
|
|
130
135
|
Завжди додавай до package.json що підтримується 24+ версія node:
|
|
@@ -134,6 +139,7 @@ export default [
|
|
|
134
139
|
"node": ">=24"
|
|
135
140
|
}
|
|
136
141
|
```
|
|
142
|
+
|
|
137
143
|
**Код:** синтаксис Node **24+**, **top level await**.
|
|
138
144
|
|
|
139
145
|
## Перевірка
|
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.18'
|
|
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,15 @@ 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`. Опційно для глобу `**/*.json` додай **власний каталог схем** [`--catalogs`](https://chris48s.github.io/v8r/usage-examples/#using-a-custom-catlog) (наприклад `-c npm/schemas/v8r-catalog.json` у репозиторії пакета `@nitra/cursor` або `-c https://unpkg.com/@nitra/cursor/schemas/v8r-catalog.json`), щоб перевіряти **`.n-cursor.json`** за схемою з [unpkg](https://unpkg.com/@nitra/cursor/schemas/n-cursor.json).
|
|
35
|
+
|
|
36
|
+
У корені проєкту має бути **`.v8rignore`**: **v8r** шукає схему в [Schema Store](https://www.schemastore.org/); для JSON без запису там перевірка завершується помилкою. Мінімум виключи **`.vscode/extensions.json`** і **`.vscode/settings.json`** (немає стабільної схеми в каталозі). Інші файли без схеми — за тією ж логікою; не розширюй ignore, щоб приховати проблеми там, де схема є. За потреби додай **`.git/**`** (службові JSON під `.git`) та **`npm/schemas/n-cursor.json`** у репозиторії `@nitra/cursor`, якщо для цього файлу немає стабільної схеми в ланцюжку v8r.
|
|
37
|
+
|
|
38
|
+
```text title=".v8rignore"
|
|
39
|
+
.vscode/extensions.json
|
|
40
|
+
.vscode/settings.json
|
|
41
|
+
.git/**
|
|
42
|
+
```
|
|
34
43
|
|
|
35
44
|
```json title=".markdownlint-cli2.jsonc"
|
|
36
45
|
{
|
|
@@ -52,6 +61,7 @@ version: '1.16'
|
|
|
52
61
|
У корені проєкту має бути `.cspell.json` і залежності для cspell у кореневому `package.json` (зазвичай `devDependencies`).
|
|
53
62
|
|
|
54
63
|
Додай workflow `.github/workflows/lint-text.yml`:
|
|
64
|
+
|
|
55
65
|
```yaml title=".github/workflows/lint-text.yml"
|
|
56
66
|
name: Lint Text
|
|
57
67
|
|
|
@@ -63,6 +73,7 @@ on:
|
|
|
63
73
|
- '.cspell.json'
|
|
64
74
|
- '.gitignore'
|
|
65
75
|
- '.markdownlint-cli2.jsonc'
|
|
76
|
+
- '.v8rignore'
|
|
66
77
|
- '**/*.js'
|
|
67
78
|
- '**/*.ts'
|
|
68
79
|
- '**/*.vue'
|
|
@@ -202,7 +213,6 @@ jobs:
|
|
|
202
213
|
|
|
203
214
|
Для іншої мови встанови відповідний пакет `@cspell/dict-*`, додай його `cspell-ext.json` у `import` і код мови в `language`. Огляд словників: [streetsidesoftware/cspell-dicts](https://github.com/streetsidesoftware/cspell-dicts).
|
|
204
215
|
|
|
205
|
-
|
|
206
216
|
## Перевірка
|
|
207
217
|
|
|
208
218
|
`npx @nitra/cursor check text`
|
package/package.json
CHANGED
package/schemas/n-cursor.json
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
"title": "n-cursor project config",
|
|
5
5
|
"description": "Конфігурація правил і skills для CLI @nitra/cursor (файл .n-cursor.json у корені репозиторію).",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"additionalProperties":
|
|
7
|
+
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"
|
|
11
|
+
"format": "uri",
|
|
12
|
+
"description": "Посилання на JSON Schema для автодоповнення та валідації в IDE; очікувано https://unpkg.com/@nitra/cursor/schemas/n-cursor.json"
|
|
12
13
|
},
|
|
13
14
|
"rules": {
|
|
14
15
|
"type": "array",
|
|
@@ -20,12 +21,16 @@
|
|
|
20
21
|
},
|
|
21
22
|
"skills": {
|
|
22
23
|
"type": "array",
|
|
23
|
-
"description": "Ідентифікатори skills без префікса n- (каталог .cursor/skills).",
|
|
24
|
+
"description": "Ідентифікатори skills без префікса n- (каталог .cursor/skills). Якщо відсутній, CLI доповнить списком skills з пакету.",
|
|
24
25
|
"items": {
|
|
25
26
|
"type": "string",
|
|
26
27
|
"minLength": 1
|
|
27
28
|
}
|
|
29
|
+
},
|
|
30
|
+
"version": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Версія пакету @nitra/cursor на unpkg для завантаження правил (необов'язково, інакше latest)."
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
|
-
"required": ["rules"
|
|
35
|
+
"required": ["rules"]
|
|
31
36
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/schema-catalog.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"schemas": [
|
|
5
|
+
{
|
|
6
|
+
"name": "n-cursor.json",
|
|
7
|
+
"description": "Конфігурація @nitra/cursor (.n-cursor.json у корені репозиторію)",
|
|
8
|
+
"url": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
|
|
9
|
+
"fileMatch": [".n-cursor.json", "**/.n-cursor.json", ".n-cursor.example.json", "**/.n-cursor.example.json"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "schema-catalog",
|
|
13
|
+
"description": "Каталог схем Schema Store (v8r-catalog.json у пакеті)",
|
|
14
|
+
"url": "https://json.schemastore.org/schema-catalog.json",
|
|
15
|
+
"fileMatch": ["npm/schemas/v8r-catalog.json"]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
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
|
+
}
|