@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 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
@@ -85,3 +85,5 @@ FROM oven/bun:alpine AS build-env
85
85
  ## lint
86
86
 
87
87
  Якщо в кореневому @package.json існують скрипти з префіксом `lint-`, обов'язково створюй `lint` скрипт, який буде запускати всі ці скрипти з лінт-префіксом.
88
+
89
+ У кінці скрипта `lint` додай `&& oxfmt .`.
package/mdc/docker.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Dockerfile — лінт через hadolint (локально або Docker); перевірка check-docker
3
- version: '1.0'
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. **`npx @nitra/cursor check docker`** — те саме, що рекомендовано після змін у Dockerfile.
20
- 2. Спочатку викликається бінарник **`hadolint`** з `PATH`; якщо його немає **`docker run`** з образом **`hadolint/hadolint:v2.12.0`** (тег узгоджуй з **`HADOLINT_IMAGE`** у `npm/scripts/check-docker.mjs`).
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
- Якщо в репозиторії немає жодного відповідного файлу — перевірка пропускається (exit 0).
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
@@ -119,6 +119,9 @@ jobs:
119
119
  import { getConfig } from '@nitra/eslint-config'
120
120
 
121
121
  export default [
122
+ {
123
+ ignores: ['**/auto-imports.d.ts']
124
+ },
122
125
  ...getConfig({
123
126
  node: ['npm']
124
127
  })
package/mdc/k8s.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: K8s YAML — перший рядок yaml-language-server $schema; перевірка check-k8s за apiVersion/kind
3
- version: '1.3'
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.17'
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`**, не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
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\" && (bunx v8r \"**/*.json\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yaml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.toml\" || [ $? -eq 98 ])"
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 ])` exit **98**, якщо glob порожній. Враховує `.gitignore`.
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\" && (bunx v8r \"**/*.json\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yaml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.toml\" || [ $? -eq 98 ])"
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\" && (bunx v8r \"**/*.json\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yaml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.toml\" || [ $? -eq 98 ])"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.6.11",
3
+ "version": "1.6.23",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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": true,
7
+ "additionalProperties": false,
8
8
  "properties": {
9
9
  "$schema": {
10
10
  "type": "string",
11
- "description": "Посилання на JSON Schema для автодоповнення та валідації в IDE; має збігатися з $id цієї схеми (рекомендовано завжди вказувати)."
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", "skills"]
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
+ }
@@ -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(
@@ -5,15 +5,12 @@
5
5
  * тощо. Спочатку бінарник hadolint з PATH, інакше docker run з образом hadolint/hadolint.
6
6
  * Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
7
7
  */
8
- import { spawnSync } from 'node:child_process'
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 — є зауваження або помилка запуску
@@ -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
 
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Перевіряє текстовий стек за правилом text.mdc.
3
3
  *
4
- * cspell, markdownlint-cli2, скрипт `lint-text` з чотирма викликами v8r, `.v8rignore` (vscode JSON),
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(raw
27
- .split('\n')
28
- .map(l => l.trim())
29
- .filter(l => l.length > 0 && !l.startsWith('#')))
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
- if (
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 — чотири виклики v8r з || [ $? -eq 98 ] для json/yml/yaml/toml')
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 ]) для **/*.json **/*.yml **/*.yaml **/*.toml (див. n-text.mdc)'
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
+ }