@nitra/cursor 1.6.11 → 1.6.23
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 +2 -0
- package/mdc/docker.mdc +91 -6
- package/mdc/js-lint.mdc +3 -0
- package/mdc/k8s.mdc +98 -8
- package/mdc/text.mdc +10 -7
- package/package.json +1 -1
- package/schemas/n-cursor.json +9 -4
- package/schemas/v8r-catalog.json +18 -0
- package/scripts/check-bun.mjs +7 -1
- package/scripts/check-docker.mjs +2 -74
- package/scripts/check-k8s.mjs +1 -3
- package/scripts/check-text.mjs +24 -13
- package/scripts/run-docker.mjs +76 -0
- package/scripts/run-k8s.mjs +137 -0
- package/scripts/run-v8r.mjs +62 -0
- package/scripts/utils/docker-hadolint.mjs +77 -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
|
+
Скрипт `scripts/run-v8r.mjs` передає в v8r каталог **`schemas/v8r-catalog.json`** пакета автоматично (у репозиторії той самий файл, що й `npm/schemas/v8r-catalog.json` від кореня монорепо). Якщо викликаєш `bunx v8r` напряму, передай `-c`: локально `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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Dockerfile —
|
|
3
|
-
version: '1.
|
|
2
|
+
description: Dockerfile — lint-docker / hadolint; перевірка check-docker
|
|
3
|
+
version: '1.5'
|
|
4
4
|
globs: "**/Dockerfile*"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -14,17 +14,100 @@ alwaysApply: false
|
|
|
14
14
|
- Усі файли з іменем **`Dockerfile`** або **`Dockerfile.*`** (наприклад `Dockerfile.prod`) у репозиторії, крім ігнорованих каталогів (`node_modules`, `.git`, `dist`, …) — як у **`check-docker.mjs`**.
|
|
15
15
|
- Також скрипт перевірки обробляє **`Containerfile`** та **`Containerfile.*`** (Podman / альтернативні імена), навіть якщо glob правила спрацьовує переважно на `Dockerfile*`.
|
|
16
16
|
|
|
17
|
+
## lint-docker
|
|
18
|
+
|
|
19
|
+
CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить скрипт **`npm/scripts/run-docker.mjs`**.
|
|
20
|
+
|
|
21
|
+
**Область lint-docker (вужча, ніж `check docker`):** лише файли з іменем **`Dockerfile`** та **`*.Dockerfile`** (суфікс **`.dockerfile`** без урахування регістру, наприклад **`api.Dockerfile`**). Файли **`Dockerfile.prod`**, **`Containerfile`** тощо **не** входять у **`lint-docker`**; їх ловить **`check docker`** (`check-docker.mjs`).
|
|
22
|
+
|
|
23
|
+
Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`check-docker.mjs`**. Виклик **`hadolint`**: **`PATH`**, інакше **`docker run`** — спільна логіка **`npm/scripts/utils/docker-hadolint.mjs`**.
|
|
24
|
+
|
|
25
|
+
```json title="package.json"
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"lint-docker": "bun ./npm/scripts/run-docker.mjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Додай workflow **`.github/workflows/lint-docker.yml`** (гілка **`dev`**, лише **`.yml`**, узгоджено з **`ga.mdc`**):
|
|
34
|
+
|
|
35
|
+
```yaml title=".github/workflows/lint-docker.yml"
|
|
36
|
+
name: Lint Docker
|
|
37
|
+
|
|
38
|
+
on:
|
|
39
|
+
push:
|
|
40
|
+
branches:
|
|
41
|
+
- dev
|
|
42
|
+
paths:
|
|
43
|
+
- '**/Dockerfile'
|
|
44
|
+
- '**/*.Dockerfile'
|
|
45
|
+
- '**/*.dockerfile'
|
|
46
|
+
- '.hadolint.yaml'
|
|
47
|
+
- 'npm/scripts/run-docker.mjs'
|
|
48
|
+
- 'npm/scripts/utils/docker-hadolint.mjs'
|
|
49
|
+
- 'npm/scripts/check-docker.mjs'
|
|
50
|
+
- 'npm/mdc/docker.mdc'
|
|
51
|
+
- 'package.json'
|
|
52
|
+
|
|
53
|
+
pull_request:
|
|
54
|
+
branches:
|
|
55
|
+
- dev
|
|
56
|
+
|
|
57
|
+
concurrency:
|
|
58
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
59
|
+
cancel-in-progress: true
|
|
60
|
+
|
|
61
|
+
jobs:
|
|
62
|
+
lint-docker:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
permissions:
|
|
65
|
+
contents: read
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
with:
|
|
69
|
+
persist-credentials: false
|
|
70
|
+
|
|
71
|
+
- name: Install hadolint
|
|
72
|
+
run: |
|
|
73
|
+
curl -sSL -o /tmp/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64
|
|
74
|
+
chmod +x /tmp/hadolint
|
|
75
|
+
sudo mv /tmp/hadolint /usr/local/bin/hadolint
|
|
76
|
+
|
|
77
|
+
- uses: oven-sh/setup-bun@v2
|
|
78
|
+
|
|
79
|
+
- name: Cache Bun dependencies
|
|
80
|
+
uses: actions/cache@v4
|
|
81
|
+
with:
|
|
82
|
+
path: |
|
|
83
|
+
~/.bun/install/cache
|
|
84
|
+
node_modules
|
|
85
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
86
|
+
restore-keys: |
|
|
87
|
+
${{ runner.os }}-bun-
|
|
88
|
+
|
|
89
|
+
- name: Install dependencies
|
|
90
|
+
run: bun install --frozen-lockfile
|
|
91
|
+
|
|
92
|
+
- name: Lint Docker
|
|
93
|
+
run: bun run lint-docker
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Версія бінарника **v2.12.0** узгоджуй з **`HADOLINT_IMAGE`** у **`npm/scripts/utils/docker-hadolint.mjs`**.
|
|
97
|
+
|
|
98
|
+
Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) має викликати **`lint-docker`**, якщо він існує.
|
|
99
|
+
|
|
17
100
|
## Запуск
|
|
18
101
|
|
|
19
|
-
1. **`
|
|
20
|
-
2.
|
|
102
|
+
1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови бінарник (приклад у workflow).
|
|
103
|
+
2. **`npx @nitra/cursor check docker`** — **`check-docker.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`PATH`** або **`docker run`** з **`hadolint/hadolint:v2.12.0`**).
|
|
21
104
|
3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure).
|
|
22
105
|
|
|
23
|
-
Якщо
|
|
106
|
+
Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
|
|
24
107
|
|
|
25
108
|
## Агентам
|
|
26
109
|
|
|
27
|
-
- Після правок у Dockerfile проганяй **`check docker`**.
|
|
110
|
+
- Після правок у Dockerfile проганяй **`bun run lint-docker`** і/або **`check docker`**.
|
|
28
111
|
- Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або правила в **`.hadolint.yaml`**.
|
|
29
112
|
- Образи на базі Bun — див. **`n-bun.mdc`**.
|
|
30
113
|
|
|
@@ -36,4 +119,6 @@ alwaysApply: false
|
|
|
36
119
|
|
|
37
120
|
`npx @nitra/cursor check docker`
|
|
38
121
|
|
|
122
|
+
Після змін у Dockerfile: **`bun run lint-docker`**, якщо скрипт додано в проєкт.
|
|
123
|
+
|
|
39
124
|
Kubernetes YAML і `$schema` у `k8s/` — окреме правило **`k8s.mdc`**, **`check k8s`**.
|
package/mdc/js-lint.mdc
CHANGED
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: K8s YAML —
|
|
3
|
-
version: '1.
|
|
2
|
+
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
+
version: '1.5'
|
|
4
4
|
globs: "**/k8s/**/*.{yaml,yml}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -19,13 +19,103 @@ alwaysApply: false
|
|
|
19
19
|
|
|
20
20
|
**Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
|
|
21
21
|
|
|
22
|
+
## lint-k8s: kubeconform і kubescape
|
|
23
|
+
|
|
24
|
+
Окремо від modeline `$schema` у редакторі варто ганяти CLI-лінтери по тих самих дерев’ях **`…/k8s`**.
|
|
25
|
+
|
|
26
|
+
- **[kubeconform](https://github.com/yannh/kubeconform#readme)** — швидка валідація маніфестів проти OpenAPI-схем Kubernetes (аналог kubeval, з підтримкою власних реєстрів схем і CRD). Не покриває серверні перевірки кластера; для gap див. README проєкту ([`kubectl --dry-run=server`](https://github.com/yannh/kubeconform#readme) тощо).
|
|
27
|
+
- **[kubescape](https://github.com/kubescape/kubescape#readme)** — сканування misconfiguration / compliance (NSA-CISA, MITRE ATT&CK, CIS тощо) по файлах, Helm, Kustomize або кластеру.
|
|
28
|
+
|
|
29
|
+
**Залежності:** виконувані файли kubeconform і kubescape у **PATH**; не додавай їх у **devDependencies** npm (аналогія до `v8r` у `n-text.mdc`). Локально: наприклад `brew install kubeconform kubescape` або релізи з GitHub.
|
|
30
|
+
|
|
31
|
+
**Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`check-k8s.mjs`** (зараз **`-kubernetes-version 1.29.1`** для набору **`v1.29.1-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
|
|
32
|
+
|
|
33
|
+
**kubescape:** типово **`kubescape scan <каталог-k8s>`**; поріг серйозності підлаштуй під проєкт (наприклад **`--severity-threshold high`**). Перший запуск може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme).
|
|
34
|
+
|
|
35
|
+
У репозиторії пакета **`@nitra/cursor`** скрипт **`lint-k8s`** делегує обхід дерев і виклики **`npm/scripts/run-k8s.mjs`**. У інших проєктах можна скопіювати цей скрипт або зібрати еквівалентні команди в **`package.json`**.
|
|
36
|
+
|
|
37
|
+
```json title="package.json"
|
|
38
|
+
{
|
|
39
|
+
"scripts": {
|
|
40
|
+
"lint-k8s": "bun ./npm/scripts/run-k8s.mjs"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Шлях до скрипта підстав свій (`./scripts/…` після копіювання, `node_modules/@nitra/cursor/scripts/…` якщо пакет у залежностях).
|
|
46
|
+
|
|
47
|
+
Додай workflow **`.github/workflows/lint-k8s.yml`** (гілка **`dev`**, лише **`.yml`**, узгоджено з **`ga.mdc`**):
|
|
48
|
+
|
|
49
|
+
```yaml title=".github/workflows/lint-k8s.yml"
|
|
50
|
+
name: Lint K8s
|
|
51
|
+
|
|
52
|
+
on:
|
|
53
|
+
push:
|
|
54
|
+
branches:
|
|
55
|
+
- dev
|
|
56
|
+
paths:
|
|
57
|
+
- '**/k8s/**/*.yml'
|
|
58
|
+
|
|
59
|
+
pull_request:
|
|
60
|
+
branches:
|
|
61
|
+
- dev
|
|
62
|
+
|
|
63
|
+
concurrency:
|
|
64
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
65
|
+
cancel-in-progress: true
|
|
66
|
+
|
|
67
|
+
jobs:
|
|
68
|
+
lint-k8s:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v4
|
|
74
|
+
with:
|
|
75
|
+
persist-credentials: false
|
|
76
|
+
|
|
77
|
+
- name: Install kubeconform
|
|
78
|
+
run: |
|
|
79
|
+
curl -sSL "https://github.com/yannh/kubeconform/releases/download/v0.7.0/kubeconform-linux-amd64.tar.gz" | tar xz
|
|
80
|
+
sudo mv kubeconform /usr/local/bin/
|
|
81
|
+
|
|
82
|
+
- name: Install kubescape
|
|
83
|
+
run: |
|
|
84
|
+
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
|
85
|
+
echo "$HOME/.kubescape/bin" >> $GITHUB_PATH
|
|
86
|
+
|
|
87
|
+
- uses: oven-sh/setup-bun@v2
|
|
88
|
+
|
|
89
|
+
- name: Cache Bun dependencies
|
|
90
|
+
uses: actions/cache@v4
|
|
91
|
+
with:
|
|
92
|
+
path: |
|
|
93
|
+
~/.bun/install/cache
|
|
94
|
+
node_modules
|
|
95
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
96
|
+
restore-keys: |
|
|
97
|
+
${{ runner.os }}-bun-
|
|
98
|
+
|
|
99
|
+
- name: Install dependencies
|
|
100
|
+
run: bun install --frozen-lockfile
|
|
101
|
+
|
|
102
|
+
- name: Lint K8s
|
|
103
|
+
run: bun run lint-k8s
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Після **`install.sh`** kubescape може опинитися поза стандартним PATH; за потреби додай крок **`echo "$HOME/.kubescape/bin" >> $GITHUB_PATH`** (див. документацію [kubescape](https://github.com/kubescape/kubescape#readme)).
|
|
107
|
+
|
|
108
|
+
Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) має викликати **`lint-k8s`**, якщо він існує.
|
|
109
|
+
|
|
22
110
|
## Перевірка
|
|
23
111
|
|
|
24
112
|
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче. Якщо під `k8s` немає yaml/yml — перевірку пропущено. Синтаксис YAML і зміст маніфесту скрипт не перевіряє — вручну.
|
|
25
113
|
|
|
114
|
+
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape), якщо скрипт додано в проєкт.
|
|
115
|
+
|
|
26
116
|
## Що закодовано в `check-k8s.mjs`
|
|
27
117
|
|
|
28
|
-
При зміні правил синхронно оновлюй **`YANNH_PIN`**, **`YANNH_GROUPS
|
|
118
|
+
При зміні правил синхронно оновлюй **`YANNH_PIN`**, **`YANNH_GROUPS`**, а в **`run-k8s.mjs`** — константу **`KUBERNETES_VERSION`** (число з PIN, наприклад `v1.29.1-standalone-strict` → **`1.29.1`**).
|
|
29
119
|
|
|
30
120
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
31
121
|
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree.
|
|
@@ -40,13 +130,13 @@ alwaysApply: false
|
|
|
40
130
|
Орієнтир — **перший документ** (до наступного `---`).
|
|
41
131
|
|
|
42
132
|
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`
|
|
133
|
+
2. **`apiVersion: v1`** → yannh, PIN **`v1.29.1-standalone-strict`**:
|
|
134
|
+
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/<PIN>/<kind>-v1.json`
|
|
45
135
|
`<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`
|
|
136
|
+
3. **`apiVersion: group/version`** і **group** у **`YANNH_GROUPS`** у скрипті → yannh:
|
|
137
|
+
`https://raw.githubusercontent.com/yannh/kubernetes-json-schema/<PIN>/<kind>-<group-з-крапками-як-дефіси>-<version>.json`
|
|
48
138
|
Приклади: `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):
|
|
139
|
+
4. **Інакше** (CRD тощо) → [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog):
|
|
50
140
|
`https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/CRDs/<group>/<kind>_<version>.json`
|
|
51
141
|
5. **Немає надійного публічного URL** — не вигадуй: залиш коректний `$schema` або `file:` за узгодженням.
|
|
52
142
|
|
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.22'
|
|
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`**.
|
|
@@ -18,12 +18,14 @@ version: '1.17'
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**, **`markdownlint-cli2`**. Для української додай **`@cspell/dict-uk-ua`**. **`v8r`** лише через **`bunx
|
|
21
|
+
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**, **`markdownlint-cli2`**. Для української додай **`@cspell/dict-uk-ua`**. **`v8r`** лише через **`bun x v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
|
|
22
|
+
|
|
23
|
+
У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`bun x v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` скрипт підставляє в v8r сам. За бажання можна передати власні glob-и аргументами скрипта. Шлях до скрипта: `./npm/scripts/…`, `./scripts/…` після копіювання, або `node_modules/@nitra/cursor/scripts/…`.
|
|
22
24
|
|
|
23
25
|
```json title="package.json"
|
|
24
26
|
{
|
|
25
27
|
"scripts": {
|
|
26
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" &&
|
|
28
|
+
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@nitra/cspell-dict": "^1.0.185"
|
|
@@ -31,13 +33,14 @@ version: '1.17'
|
|
|
31
33
|
}
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
**v8r:** чотири виклики `(bunx v8r "<glob>" || [ $? -eq 98 ])`
|
|
36
|
+
**v8r:** без обгортки — чотири виклики `(bunx v8r "<glob>" || [ $? -eq 98 ])` для **`**/*.json`**, **`**/*.yml`**, **`**/*.yaml`**, **`**/*.toml`** (за потреби окремо **json5**). Враховує `.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
37
|
|
|
36
|
-
У корені проєкту має бути **`.v8rignore`**: **v8r** шукає схему в [Schema Store](https://www.schemastore.org/); для JSON без запису там перевірка завершується помилкою. Мінімум виключи **`.vscode/extensions.json`** і **`.vscode/settings.json`** (немає стабільної схеми в каталозі). Інші файли без схеми — за тією ж логікою; не розширюй ignore, щоб приховати проблеми там, де схема є.
|
|
38
|
+
У корені проєкту має бути **`.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
39
|
|
|
38
40
|
```text title=".v8rignore"
|
|
39
41
|
.vscode/extensions.json
|
|
40
42
|
.vscode/settings.json
|
|
43
|
+
.git/**
|
|
41
44
|
```
|
|
42
45
|
|
|
43
46
|
```json title=".markdownlint-cli2.jsonc"
|
|
@@ -150,7 +153,7 @@ jobs:
|
|
|
150
153
|
```json title="package.json"
|
|
151
154
|
{
|
|
152
155
|
"scripts": {
|
|
153
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" &&
|
|
156
|
+
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
154
157
|
},
|
|
155
158
|
"devDependencies": {
|
|
156
159
|
"@nitra/cspell-dict": "^1.0.185",
|
|
@@ -168,7 +171,7 @@ jobs:
|
|
|
168
171
|
```json title="package.json"
|
|
169
172
|
{
|
|
170
173
|
"scripts": {
|
|
171
|
-
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" &&
|
|
174
|
+
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
172
175
|
},
|
|
173
176
|
"devDependencies": {
|
|
174
177
|
"@nitra/cspell-dict": "^1.0.185",
|
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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* і поле `packageManager` у кореневому `package.json`.
|
|
6
6
|
*
|
|
7
7
|
* Якщо в кореневому `package.json` є скрипти з префіксом `lint-`, перевіряє наявність агрегованого
|
|
8
|
-
* скрипта `lint`, у якому через `bun run <ім’я>` викликаються всі такі
|
|
8
|
+
* скрипта `lint`, у якому через `bun run <ім’я>` викликаються всі такі скрипти, і що рядок `lint`
|
|
9
|
+
* закінчується на `&& oxfmt .`.
|
|
9
10
|
*/
|
|
10
11
|
import { existsSync } from 'node:fs'
|
|
11
12
|
import { readFile } from 'node:fs/promises'
|
|
@@ -63,6 +64,11 @@ export async function check() {
|
|
|
63
64
|
)
|
|
64
65
|
} else {
|
|
65
66
|
pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
|
|
67
|
+
if (/\s*&&\s+oxfmt\s+\.\s*$/.test(aggregate.trim())) {
|
|
68
|
+
pass('package.json: `lint` завершується `&& oxfmt .`')
|
|
69
|
+
} else {
|
|
70
|
+
fail('Скрипт `lint` має закінчуватися на `&& oxfmt .`')
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
} else {
|
|
68
74
|
fail(
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* тощо. Спочатку бінарник hadolint з PATH, інакше docker run з образом hadolint/hadolint.
|
|
6
6
|
* Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { basename, relative, sep } from 'node:path'
|
|
8
|
+
import { basename } from 'node:path'
|
|
10
9
|
|
|
10
|
+
import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
|
|
11
11
|
import { pass } from './utils/pass.mjs'
|
|
12
12
|
import { walkDir } from './utils/walkDir.mjs'
|
|
13
13
|
|
|
14
|
-
/** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
|
|
15
|
-
const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
|
|
16
|
-
|
|
17
14
|
/**
|
|
18
15
|
* Чи є basename Dockerfile / Containerfile (у т.ч. Dockerfile.prod).
|
|
19
16
|
* @param {string} name basename шляху
|
|
@@ -40,75 +37,6 @@ async function findDockerfilePaths(root) {
|
|
|
40
37
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
41
38
|
}
|
|
42
39
|
|
|
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
40
|
/**
|
|
113
41
|
* Перевіряє Dockerfile / Containerfile через hadolint (docker.mdc).
|
|
114
42
|
* @returns {Promise<number>} 0 — все OK, 1 — є зауваження або помилка запуску
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -224,9 +224,7 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
224
224
|
|
|
225
225
|
const m = lines[0].match(MODELINE_RE)
|
|
226
226
|
if (!m) {
|
|
227
|
-
fail(
|
|
228
|
-
`${rel}: перший рядок має бути коментарем # yaml-language-server: $schema=<url> (без префіксів перед #)`
|
|
229
|
-
)
|
|
227
|
+
fail(`${rel}: перший рядок має бути коментарем # yaml-language-server: $schema=<url> (без префіксів перед #)`)
|
|
230
228
|
return
|
|
231
229
|
}
|
|
232
230
|
|
package/scripts/check-text.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє текстовий стек за правилом text.mdc.
|
|
3
3
|
*
|
|
4
|
-
* cspell, markdownlint-cli2, скрипт `lint-text` з
|
|
4
|
+
* cspell, markdownlint-cli2, скрипт `lint-text` з v8r (`run-v8r.mjs` або чотири `bunx v8r`),
|
|
5
|
+
* `.v8rignore` (vscode JSON),
|
|
5
6
|
* workflow `lint-text.yml`, розширення VSCode для markdownlint.
|
|
6
7
|
*/
|
|
7
8
|
import { existsSync } from 'node:fs'
|
|
@@ -23,10 +24,12 @@ export async function check() {
|
|
|
23
24
|
const v8rIgnoreRequired = ['.vscode/extensions.json', '.vscode/settings.json']
|
|
24
25
|
if (existsSync('.v8rignore')) {
|
|
25
26
|
const raw = await readFile('.v8rignore', 'utf8')
|
|
26
|
-
const lines = new Set(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const lines = new Set(
|
|
28
|
+
raw
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map(l => l.trim())
|
|
31
|
+
.filter(l => l.length > 0 && !l.startsWith('#'))
|
|
32
|
+
)
|
|
30
33
|
for (const path of v8rIgnoreRequired) {
|
|
31
34
|
if (lines.has(path)) {
|
|
32
35
|
pass(`.v8rignore містить ${path}`)
|
|
@@ -119,23 +122,31 @@ export async function check() {
|
|
|
119
122
|
|
|
120
123
|
const lintText = pkg.scripts?.['lint-text']
|
|
121
124
|
const v8rCalls = typeof lintText === 'string' ? (lintText.match(/bunx v8r/g) || []).length : 0
|
|
125
|
+
const quietCalls = typeof lintText === 'string' ? (lintText.match(/run-v8r?\.mjs/g) || []).length : 0
|
|
122
126
|
const eq98Hints = typeof lintText === 'string' ? (lintText.match(/eq 98/g) || []).length : 0
|
|
123
|
-
|
|
127
|
+
const globsOk =
|
|
124
128
|
typeof lintText === 'string' &&
|
|
125
|
-
lintText.includes('cspell') &&
|
|
126
|
-
lintText.includes('bunx markdownlint-cli2') &&
|
|
127
|
-
lintText.includes('**/*.mdc') &&
|
|
128
|
-
v8rCalls >= 4 &&
|
|
129
|
-
eq98Hints >= 4 &&
|
|
130
129
|
lintText.includes('**/*.json') &&
|
|
131
130
|
lintText.includes('**/*.yml') &&
|
|
132
131
|
lintText.includes('**/*.yaml') &&
|
|
133
132
|
lintText.includes('**/*.toml')
|
|
133
|
+
const legacyV8r = v8rCalls >= 4 && eq98Hints >= 4
|
|
134
|
+
const quietBundled = quietCalls === 1
|
|
135
|
+
const quietLegacy4x = quietCalls >= 4
|
|
136
|
+
const v8rTextOk = legacyV8r || quietBundled || quietLegacy4x
|
|
137
|
+
const globsRequired = legacyV8r || quietLegacy4x
|
|
138
|
+
if (
|
|
139
|
+
typeof lintText === 'string' &&
|
|
140
|
+
lintText.includes('cspell') &&
|
|
141
|
+
lintText.includes('bunx markdownlint-cli2') &&
|
|
142
|
+
lintText.includes('**/*.mdc') &&
|
|
143
|
+
v8rTextOk &&
|
|
144
|
+
(!globsRequired || globsOk)
|
|
134
145
|
) {
|
|
135
|
-
pass('package.json: lint-text — чотири
|
|
146
|
+
pass('package.json: lint-text — v8r: run-v8r.mjs (один виклик або чотири) або чотири bunx v8r з || [ $? -eq 98 ]')
|
|
136
147
|
} else {
|
|
137
148
|
fail(
|
|
138
|
-
'package.json: lint-text — чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) для
|
|
149
|
+
'package.json: lint-text — v8r: bun ./…/run-v8r.mjs або чотири (bunx v8r "<glob>" || [ $? -eq 98 ]) для json/yml/yaml/toml (див. n-text.mdc)'
|
|
139
150
|
)
|
|
140
151
|
}
|
|
141
152
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-docker (скрипт lint-docker): hadolint лише для файлів з іменем Dockerfile та суфіксом .dockerfile (див. docker.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Обхід дерева як у check-docker (walkDir, ті самі пропуски каталогів). На відміну від
|
|
5
|
+
* check docker, не обробляються Dockerfile.*, Containerfile тощо — лише канонічне ім’я
|
|
6
|
+
* Dockerfile та варіанти виду app.Dockerfile (регістр суфікса не важливий).
|
|
7
|
+
*
|
|
8
|
+
* Виклик hadolint — через utils/docker-hadolint.mjs (PATH або docker run).
|
|
9
|
+
*/
|
|
10
|
+
import { basename } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
|
|
13
|
+
import { pass } from './utils/pass.mjs'
|
|
14
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Чи входить файл до набору lint-docker: Dockerfile або *.Dockerfile (*.dockerfile).
|
|
18
|
+
* @param {string} name basename шляху
|
|
19
|
+
* @returns {boolean} true, якщо ім’я підходить під lint-docker
|
|
20
|
+
*/
|
|
21
|
+
function isLintDockerfileName(name) {
|
|
22
|
+
const n = name.toLowerCase()
|
|
23
|
+
if (n === 'dockerfile') return true
|
|
24
|
+
return n.endsWith('.dockerfile') && n.length > '.dockerfile'.length
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Збирає абсолютні шляхи для lint-docker.
|
|
29
|
+
* @param {string} root корінь репозиторію
|
|
30
|
+
* @returns {Promise<string[]>} відсортовані абсолютні шляхи
|
|
31
|
+
*/
|
|
32
|
+
async function findLintDockerfilePaths(root) {
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const out = []
|
|
35
|
+
await walkDir(root, p => {
|
|
36
|
+
if (isLintDockerfileName(basename(p))) out.push(p)
|
|
37
|
+
})
|
|
38
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Запуск hadolint по Dockerfile та *.Dockerfile.
|
|
43
|
+
* @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
|
|
44
|
+
*/
|
|
45
|
+
async function main() {
|
|
46
|
+
let exitCode = 0
|
|
47
|
+
const fail = msg => {
|
|
48
|
+
console.log(` ❌ ${msg}`)
|
|
49
|
+
exitCode = 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const root = process.cwd()
|
|
53
|
+
const files = await findLintDockerfilePaths(root)
|
|
54
|
+
|
|
55
|
+
if (files.length === 0) {
|
|
56
|
+
pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pass(`lint-docker: файлів для hadolint: ${files.length}`)
|
|
61
|
+
|
|
62
|
+
for (const abs of files) {
|
|
63
|
+
const rel = posixRel(root, abs) || basename(abs)
|
|
64
|
+
const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
|
|
65
|
+
const tail = (stdout + stderr).trim()
|
|
66
|
+
if (ok) {
|
|
67
|
+
pass(`${rel} (${via})`)
|
|
68
|
+
} else {
|
|
69
|
+
fail(`${rel} (${via})${tail ? `:\n${tail}` : ''}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return exitCode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process.exitCode = await main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запуск kubeconform та kubescape для каталогів `…/k8s`, де є YAML-маніфести (див. k8s.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Знаходить унікальні корені каталогів із іменем `k8s` за шляхами файлів `*.yaml` / `*.yml`
|
|
5
|
+
* (той самий принцип сегмента `k8s`, що й у check-k8s.mjs). Якщо таких файлів немає — вихід 0
|
|
6
|
+
* без виклику зовнішніх CLI.
|
|
7
|
+
*
|
|
8
|
+
* kubeconform перевіряє маніфести проти OpenAPI-схем Kubernetes; kubescape — сканування на
|
|
9
|
+
* misconfiguration / compliance (NSA, MITRE, CIS тощо). Обидва бінарники очікуються в PATH
|
|
10
|
+
* (локально: Homebrew, релізи GitHub; у CI — крок установки з k8s.mdc).
|
|
11
|
+
*
|
|
12
|
+
* Версія `-kubernetes-version` для kubeconform узгоджена з PIN yannh у check-k8s.mjs / k8s.mdc.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { basename, dirname } from 'node:path'
|
|
16
|
+
|
|
17
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
18
|
+
|
|
19
|
+
/** Версія Kubernetes для kubeconform — синхронно з YANNH_PIN (без префікса v і суфікса -standalone-strict). */
|
|
20
|
+
const KUBERNETES_VERSION = '1.29.1'
|
|
21
|
+
|
|
22
|
+
/** Додатковий реєстр схем для CRD (як у README kubeconform). */
|
|
23
|
+
const DATREE_CRD_SCHEMA_LOCATION =
|
|
24
|
+
'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Чи містить шлях сегмент директорії `k8s`.
|
|
28
|
+
* @param {string} filePath шлях до файлу
|
|
29
|
+
* @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
|
|
30
|
+
*/
|
|
31
|
+
function pathHasK8sSegment(filePath) {
|
|
32
|
+
const parts = filePath.split(/[/\\]/u)
|
|
33
|
+
return parts.includes('k8s')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Каталог `…/k8s`, що містить маніфест (йдемо вгору від файлу до компонента `k8s`).
|
|
38
|
+
* @param {string} absFile абсолютний шлях до yaml
|
|
39
|
+
* @returns {string | null} абсолютний шлях до `…/k8s` або null, якщо сегмента `k8s` у ланцюжку немає
|
|
40
|
+
*/
|
|
41
|
+
function k8sRootFromFile(absFile) {
|
|
42
|
+
let dir = dirname(absFile)
|
|
43
|
+
for (let i = 0; i < 64; i++) {
|
|
44
|
+
if (basename(dir) === 'k8s') return dir
|
|
45
|
+
const parent = dirname(dir)
|
|
46
|
+
if (parent === dir) break
|
|
47
|
+
dir = parent
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Унікальні корені `k8s` з yaml/yml під деревом cwd.
|
|
54
|
+
* @param {string} root корінь репозиторію
|
|
55
|
+
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
|
|
56
|
+
*/
|
|
57
|
+
async function findK8sRoots(root) {
|
|
58
|
+
/** @type {Set<string>} */
|
|
59
|
+
const roots = new Set()
|
|
60
|
+
await walkDir(root, p => {
|
|
61
|
+
if (!pathHasK8sSegment(p)) return
|
|
62
|
+
if (!/\.ya?ml$/iu.test(p)) return
|
|
63
|
+
const k8sRoot = k8sRootFromFile(p)
|
|
64
|
+
if (k8sRoot) roots.add(k8sRoot)
|
|
65
|
+
})
|
|
66
|
+
return [...roots].toSorted((a, b) => a.localeCompare(b))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Запускає kubeconform для переліку каталогів.
|
|
71
|
+
* @param {string[]} dirs абсолютні шляхи до `…/k8s`
|
|
72
|
+
* @returns {number} код виходу процесу kubeconform (127, якщо бінарник не знайдено)
|
|
73
|
+
*/
|
|
74
|
+
function runKubeconform(dirs) {
|
|
75
|
+
const args = [
|
|
76
|
+
'-summary',
|
|
77
|
+
'-kubernetes-version',
|
|
78
|
+
KUBERNETES_VERSION,
|
|
79
|
+
'-schema-location',
|
|
80
|
+
'default',
|
|
81
|
+
'-schema-location',
|
|
82
|
+
DATREE_CRD_SCHEMA_LOCATION,
|
|
83
|
+
'-ignore-missing-schemas',
|
|
84
|
+
...dirs
|
|
85
|
+
]
|
|
86
|
+
const r = spawnSync('kubeconform', args, { stdio: 'inherit', shell: false })
|
|
87
|
+
if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
|
|
88
|
+
console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
|
|
89
|
+
return 127
|
|
90
|
+
}
|
|
91
|
+
return r.status ?? 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Запускає kubescape scan для кожного каталогу окремо (узгоджено з прикладами CLI).
|
|
96
|
+
* @param {string[]} dirs абсолютні шляхи до `…/k8s`
|
|
97
|
+
* @returns {number} 0 при успіху, інакше код останнього невдалого scan або 127, якщо бінарник не знайдено
|
|
98
|
+
*/
|
|
99
|
+
function runKubescape(dirs) {
|
|
100
|
+
for (const d of dirs) {
|
|
101
|
+
const r = spawnSync('kubescape', ['scan', d, '--severity-threshold', 'high'], {
|
|
102
|
+
stdio: 'inherit',
|
|
103
|
+
shell: false
|
|
104
|
+
})
|
|
105
|
+
if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
|
|
106
|
+
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
107
|
+
return 127
|
|
108
|
+
}
|
|
109
|
+
if (r.status !== 0) return r.status ?? 1
|
|
110
|
+
}
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Головна точка входу: kubeconform + kubescape для усіх знайдених дерев `k8s`.
|
|
116
|
+
* @returns {Promise<number>} код виходу для `process.exitCode` (0 — успіх або пропуск)
|
|
117
|
+
*/
|
|
118
|
+
async function main() {
|
|
119
|
+
const root = process.cwd()
|
|
120
|
+
const dirs = await findK8sRoots(root)
|
|
121
|
+
|
|
122
|
+
if (dirs.length === 0) {
|
|
123
|
+
console.log('run-k8s: немає yaml/yml під k8s — kubeconform і kubescape пропущено')
|
|
124
|
+
return 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(`run-k8s: каталоги k8s (${dirs.length}):`)
|
|
128
|
+
for (const d of dirs) console.log(` ${d}`)
|
|
129
|
+
|
|
130
|
+
const kc = runKubeconform(dirs)
|
|
131
|
+
if (kc !== 0) return kc
|
|
132
|
+
|
|
133
|
+
const ks = runKubescape(dirs)
|
|
134
|
+
return ks
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.exitCode = await main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тихий запуск v8r для усіх типів файлів, які підтримує v8r (json, json5, yaml, yml, toml).
|
|
3
|
+
*
|
|
4
|
+
* Один виклик цього скрипта з `lint-text` замість чотирьох окремих викликів v8r: під капотом для
|
|
5
|
+
* кожного glob окремий `bun x v8r`, бо v8r у одному процесі падає з кодом 98, якщо хоч один із
|
|
6
|
+
* переданих глобів не знаходить файлів — тоді решта розширень не перевіряються.
|
|
7
|
+
*
|
|
8
|
+
* Каталог схем `@nitra/cursor` (`v8r-catalog.json` у каталозі `schemas` пакета) передається в v8r
|
|
9
|
+
* як `-c` автоматично (те саме, що в репозиторії шлях `npm/schemas/v8r-catalog.json` від кореня).
|
|
10
|
+
* Опційно можна передати власні glob-и як аргументи; якщо їх немає — типові для `.json`, `.json5`,
|
|
11
|
+
* `.yml`, `.yaml`, `.toml` у дереві проєкту.
|
|
12
|
+
*
|
|
13
|
+
* Якщо код виходу 0 або 98 (успіх або порожній glob), вивід v8r не показується; інакше
|
|
14
|
+
* вивід друкується, процес завершується з тим самим кодом, що й перший невдалий v8r.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
17
|
+
import { existsSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
/** Типові glob-и для форматів, які обробляє v8r (див. опис CLI v8r). */
|
|
22
|
+
const DEFAULT_GLOBS = ['**/*.json', '**/*.json5', '**/*.yml', '**/*.yaml', '**/*.toml']
|
|
23
|
+
|
|
24
|
+
/** Абсолютний шлях до `schemas/v8r-catalog.json` поруч з цим скриптом у пакеті `@nitra/cursor`. */
|
|
25
|
+
const V8R_CATALOG_PATH = join(dirname(fileURLToPath(import.meta.url)), '../schemas/v8r-catalog.json')
|
|
26
|
+
|
|
27
|
+
if (existsSync(V8R_CATALOG_PATH)) {
|
|
28
|
+
const globs = process.argv.length > 2 ? process.argv.slice(2) : DEFAULT_GLOBS
|
|
29
|
+
|
|
30
|
+
for (const pattern of globs) {
|
|
31
|
+
// Порядок важливий: glob має бути перед -c, інакше yargs у v8r не отримує позиційні patterns.
|
|
32
|
+
const result = spawnSync('bun', ['x', 'v8r', pattern, '-c', V8R_CATALOG_PATH], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
35
|
+
shell: false,
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (result.error) {
|
|
40
|
+
process.stderr.write(`${result.error.message}\n`)
|
|
41
|
+
process.exitCode = 1
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const exitCode = result.status ?? 1
|
|
46
|
+
if (exitCode !== 0 && exitCode !== 98) {
|
|
47
|
+
if (result.stdout?.length) {
|
|
48
|
+
process.stdout.write(result.stdout)
|
|
49
|
+
}
|
|
50
|
+
if (result.stderr?.length) {
|
|
51
|
+
process.stderr.write(result.stderr)
|
|
52
|
+
}
|
|
53
|
+
process.exitCode = exitCode
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
`run-v8r: не знайдено каталог схем за шляхом ${V8R_CATALOG_PATH} (очікується npm/schemas/v8r-catalog.json у пакеті)\n`
|
|
60
|
+
)
|
|
61
|
+
process.exitCode = 2
|
|
62
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільна логіка виклику hadolint для шляхів до Dockerfile (див. docker.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Відносні шляхи з прямими слешами для контейнера; спочатку бінарник hadolint з PATH,
|
|
5
|
+
* інакше docker run з образом HADOLINT_IMAGE. Використовується check-docker.mjs та
|
|
6
|
+
* run-docker.mjs.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import { relative, sep } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
|
|
12
|
+
export const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Відносний шлях від root з прямими слешами (hadolint у контейнері).
|
|
16
|
+
* @param {string} root корінь
|
|
17
|
+
* @param {string} absPath абсолютний шлях
|
|
18
|
+
* @returns {string} відносний шлях з прямими слешами
|
|
19
|
+
*/
|
|
20
|
+
export function posixRel(root, absPath) {
|
|
21
|
+
return relative(root, absPath).split(sep).join('/')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Запуск hadolint: спочатку PATH, інакше Docker.
|
|
26
|
+
* @param {string} root корінь репозиторію
|
|
27
|
+
* @param {string} absPath абсолютний шлях до Dockerfile
|
|
28
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint та канал запуску
|
|
29
|
+
*/
|
|
30
|
+
export function lintDockerfileWithHadolint(root, absPath) {
|
|
31
|
+
const rel = posixRel(root, absPath)
|
|
32
|
+
const local = spawnSync('hadolint', [rel], {
|
|
33
|
+
cwd: root,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
maxBuffer: 10 * 1024 * 1024
|
|
36
|
+
})
|
|
37
|
+
if (!local.error) {
|
|
38
|
+
const ok = local.status === 0
|
|
39
|
+
return {
|
|
40
|
+
ok,
|
|
41
|
+
stdout: local.stdout ?? '',
|
|
42
|
+
stderr: local.stderr ?? '',
|
|
43
|
+
via: 'hadolint'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (local.error.code !== 'ENOENT') {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
stdout: '',
|
|
50
|
+
stderr: local.error.message,
|
|
51
|
+
via: 'hadolint'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const docker = spawnSync('docker', ['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel], {
|
|
56
|
+
cwd: root,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
maxBuffer: 10 * 1024 * 1024
|
|
59
|
+
})
|
|
60
|
+
if (docker.error) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
stdout: '',
|
|
64
|
+
stderr:
|
|
65
|
+
`Не знайдено hadolint у PATH і не вдалося запустити Docker (${docker.error.message}). ` +
|
|
66
|
+
`Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).`,
|
|
67
|
+
via: 'docker'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const ok = docker.status === 0
|
|
71
|
+
return {
|
|
72
|
+
ok,
|
|
73
|
+
stdout: docker.stdout ?? '',
|
|
74
|
+
stderr: docker.stderr ?? '',
|
|
75
|
+
via: 'docker'
|
|
76
|
+
}
|
|
77
|
+
}
|