@nitra/cursor 1.8.75 → 1.8.80

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/mdc/abie.mdc CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.7'
4
+ version: '1.8'
5
5
  ---
6
6
 
7
7
  Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), а також гілки **dev**, **ua**, **ru** у **clean-merged-branch**.
8
8
 
9
9
  **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
10
 
11
- **Канон перевірки** — **`npm/scripts/check-abie.mjs`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
11
+ **Канон перевірки** — **`npm/scripts/check-abie.mjs`** у пакеті **`@nitra/cursor`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
12
12
 
13
13
  ## k8s: `hc.yaml` поруч із Deployment
14
14
 
@@ -36,7 +36,7 @@ spec:
36
36
 
37
37
  ## k8s: overlay **HTTPRoute** (**ua** / **ru**)
38
38
 
39
- За наявності **Deployment** під **k8s** у **кожному** **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**. Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
39
+ За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**. Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
40
40
 
41
41
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
42
42
  - target:
@@ -95,7 +95,7 @@ patches:
95
95
 
96
96
  ## k8s: overlay **ua** / **ru** і nodeSelector
97
97
 
98
- За наявності **Deployment** під **k8s** у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** patch на **`kind: Deployment`**: **ua** — **`spec.template.spec.nodeSelector`** з **`preem: false`**; **ru** — **`spec.template.spec.nodeSelector`** з **`yandex.cloud/preemptible: false`**. Форму **JSON6902** (шлях **`/spec/template/spec/nodeSelector`**, **`op`**) див. **k8s.mdc**.
98
+ У **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** того пакета, у дереві **`k8s`** якого є **Deployment**, потрібен patch на **`kind: Deployment`**: **ua** — **`spec.template.spec.nodeSelector`** з **`preem: false`**; **ru** — **`spec.template.spec.nodeSelector`** з **`yandex.cloud/preemptible: false`**. Форму **JSON6902** (шлях **`/spec/template/spec/nodeSelector`**, **`op`**) див. **k8s.mdc**.
99
99
 
100
100
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
101
101
  patches:
package/mdc/docker.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Dockerfile — lint-docker / hadolint; перевірка check-docker
3
- version: '1.6'
3
+ version: '1.7'
4
4
  globs: "**/Dockerfile*"
5
5
  alwaysApply: false
6
6
  ---
@@ -103,14 +103,19 @@ jobs:
103
103
 
104
104
  1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
105
105
  2. **`npx @nitra/cursor check docker`** — **`check-docker.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`PATH`** або **`docker run`** з **`hadolint/hadolint:v2.12.0`**).
106
- 3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure).
106
+ 3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
107
+
108
+ ```yaml title=".hadolint.yaml"
109
+ ignored:
110
+ - DL3007
111
+ ```
107
112
 
108
113
  Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
109
114
 
110
115
  ## Агентам
111
116
 
112
117
  - Після правок у Dockerfile проганяй **`bun run lint-docker`** і/або **`check docker`**.
113
- - Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або правила в **`.hadolint.yaml`**.
118
+ - Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
114
119
  - Образи на базі Bun — див. **`n-bun.mdc`**.
115
120
 
116
121
  ## Редактор
package/mdc/k8s.mdc CHANGED
@@ -226,8 +226,6 @@ patches:
226
226
 
227
227
  **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
228
228
 
229
-
230
-
231
229
  ## Коли застосовувати (агентам)
232
230
 
233
231
  - Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
@@ -8,7 +8,7 @@ version: '1.2'
8
8
 
9
9
  - **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
10
10
  - **Форматування** узгоджуй з **`n-js-format.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
11
- - **Запуск stylelint:** лише **`npx stylelint`** (у **`lint-style`**, у CI, вручну). Не використовуй **`bunx stylelint`** і не запускай **stylelint** напряму без **`npx`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
11
+ - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
12
12
  - **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
13
13
 
14
14
  **VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
@@ -44,7 +44,7 @@ version: '1.2'
44
44
  },
45
45
  ```
46
46
 
47
- Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі **`bun run lint-style`** (у скрипті вже **`npx stylelint`**). **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
47
+ Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі `npx stylelint '**/*.{css,scss,vue}' --fix` у кроці **`run`**. **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
48
48
 
49
49
  ```yaml title=".github/workflows/lint-style.yml"
50
50
  name: StyleLint
@@ -85,7 +85,7 @@ jobs:
85
85
  - uses: ./.github/actions/setup-bun-deps
86
86
 
87
87
  - name: StyleLint
88
- run: bun run lint-style
88
+ run: npx stylelint '**/*.{css,scss,vue}' --fix
89
89
  ```
90
90
 
91
91
  У корені проєкту має бути **`.stylelintignore`**:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.75",
3
+ "version": "1.8.80",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -17,11 +17,12 @@
17
17
  * **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
18
18
  * у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
19
19
  *
20
- * **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у **`ua`/`ru` kustomization** — inline patch на **`kind: Deployment`**
20
+ * **nodeSelector (overlay):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua`/`ru` kustomization** цього пакета — inline patch на **`kind: Deployment`**
21
21
  * з **`path: /spec/template/spec/nodeSelector`**: **ua** — **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
22
22
  * Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
23
23
  *
24
- * **HTTPRoute (overlay):** тієї ж умови patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
24
+ * **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
25
+ * — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
25
26
  * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
26
27
  * Вибір **`op`** — **k8s.mdc**.
27
28
  */
@@ -66,6 +67,58 @@ export function isUaKustomizationPath(rel) {
66
67
  return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
67
68
  }
68
69
 
70
+ /**
71
+ * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
72
+ * @param {string} root корінь репозиторію
73
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
74
+ * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
75
+ */
76
+ export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
77
+ const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
78
+ const m = rel.match(/^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u)
79
+ return m ? join(root, m[1]) : null
80
+ }
81
+
82
+ /**
83
+ * Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
84
+ * @param {string} root корінь репозиторію
85
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
86
+ * @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
87
+ */
88
+ export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
89
+ const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
90
+ if (!pkg) {
91
+ return false
92
+ }
93
+ return (
94
+ existsSync(join(pkg, 'vite.config.js')) ||
95
+ existsSync(join(pkg, 'vite.config.mjs')) ||
96
+ existsSync(join(pkg, 'vite.config.ts'))
97
+ )
98
+ }
99
+
100
+ /**
101
+ * Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
102
+ * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
103
+ * @param {string} root корінь репозиторію
104
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
105
+ * @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
106
+ */
107
+ export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
108
+ const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
109
+ if (!pkg) {
110
+ return false
111
+ }
112
+ const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
113
+ for (const dir of deploymentDirs) {
114
+ const norm = dir.replaceAll('\\', '/')
115
+ if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
116
+ return true
117
+ }
118
+ }
119
+ return false
120
+ }
121
+
69
122
  /**
70
123
  * Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
71
124
  * @param {string} rel шлях від кореня репозиторію
@@ -766,14 +819,16 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
766
819
  }
767
820
 
768
821
  /**
769
- * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc).
822
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc)
823
+ * лише для kustomization того пакета, у дереві **k8s** якого є **Deployment**.
770
824
  * @param {string} root корінь репозиторію
771
825
  * @param {string[]} yamlFilesAbs yaml під k8s
826
+ * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
772
827
  * @param {(msg: string) => void} fail callback
773
828
  * @param {(msg: string) => void} passFn успішне повідомлення
774
829
  * @returns {Promise<void>}
775
830
  */
776
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
831
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
777
832
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
778
833
  if (uaAbsList.length === 0) {
779
834
  fail(
@@ -783,21 +838,25 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
783
838
  }
784
839
  for (const abs of uaAbsList) {
785
840
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
786
- let raw
787
- try {
788
- raw = await readFile(abs, 'utf8')
789
- } catch (error) {
790
- const msg = error instanceof Error ? error.message : String(error)
791
- fail(`${rel}: не вдалося прочитати (${msg})`)
792
- return
793
- }
794
- if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
795
- fail(
796
- `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
797
- )
798
- return
841
+ if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
842
+ let raw
843
+ try {
844
+ raw = await readFile(abs, 'utf8')
845
+ } catch (error) {
846
+ const msg = error instanceof Error ? error.message : String(error)
847
+ fail(`${rel}: не вдалося прочитати (${msg})`)
848
+ return
849
+ }
850
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
851
+ fail(
852
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
853
+ )
854
+ return
855
+ }
856
+ passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
857
+ } else {
858
+ passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
799
859
  }
800
- passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
801
860
  }
802
861
 
803
862
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
@@ -809,26 +868,31 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
809
868
  }
810
869
  for (const abs of ruAbsList) {
811
870
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
812
- let raw
813
- try {
814
- raw = await readFile(abs, 'utf8')
815
- } catch (error) {
816
- const msg = error instanceof Error ? error.message : String(error)
817
- fail(`${rel}: не вдалося прочитати (${msg})`)
818
- return
819
- }
820
- if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
821
- fail(
822
- `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
823
- )
824
- return
871
+ if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
872
+ let raw
873
+ try {
874
+ raw = await readFile(abs, 'utf8')
875
+ } catch (error) {
876
+ const msg = error instanceof Error ? error.message : String(error)
877
+ fail(`${rel}: не вдалося прочитати (${msg})`)
878
+ return
879
+ }
880
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
881
+ fail(
882
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
883
+ )
884
+ return
885
+ }
886
+ passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
887
+ } else {
888
+ passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
825
889
  }
826
- passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
827
890
  }
828
891
  }
829
892
 
830
893
  /**
831
- * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc.
894
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
895
+ * лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
832
896
  * @param {string} root корінь репозиторію
833
897
  * @param {string[]} yamlFilesAbs yaml під k8s
834
898
  * @param {(msg: string) => void} fail callback
@@ -838,54 +902,60 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
838
902
  async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
839
903
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
840
904
  if (uaAbsList.length === 0) {
841
- fail(
842
- 'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, parentRefs namespace ua) abie.mdc'
905
+ passFn(
906
+ 'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
843
907
  )
844
- return
845
908
  }
846
909
  for (const abs of uaAbsList) {
847
910
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
848
- let raw
849
- try {
850
- raw = await readFile(abs, 'utf8')
851
- } catch (error) {
852
- const msg = error instanceof Error ? error.message : String(error)
853
- fail(`${rel}: не вдалося прочитати (${msg})`)
854
- return
855
- }
856
- const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
857
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
858
- if (v !== null) {
859
- fail(`${rel}: ${v}`)
860
- return
911
+ if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
912
+ let raw
913
+ try {
914
+ raw = await readFile(abs, 'utf8')
915
+ } catch (error) {
916
+ const msg = error instanceof Error ? error.message : String(error)
917
+ fail(`${rel}: не вдалося прочитати (${msg})`)
918
+ return
919
+ }
920
+ const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
921
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
922
+ if (v !== null) {
923
+ fail(`${rel}: ${v}`)
924
+ return
925
+ }
926
+ passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
927
+ } else {
928
+ passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
861
929
  }
862
- passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
863
930
  }
864
931
 
865
932
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
866
933
  if (ruAbsList.length === 0) {
867
- fail(
868
- 'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, namespace ru, gwin websocket) — abie.mdc'
934
+ passFn(
935
+ 'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
869
936
  )
870
- return
871
937
  }
872
938
  for (const abs of ruAbsList) {
873
939
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
874
- let raw
875
- try {
876
- raw = await readFile(abs, 'utf8')
877
- } catch (error) {
878
- const msg = error instanceof Error ? error.message : String(error)
879
- fail(`${rel}: не вдалося прочитати (${msg})`)
880
- return
881
- }
882
- const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
883
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru')
884
- if (v !== null) {
885
- fail(`${rel}: ${v}`)
886
- return
940
+ if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
941
+ let raw
942
+ try {
943
+ raw = await readFile(abs, 'utf8')
944
+ } catch (error) {
945
+ const msg = error instanceof Error ? error.message : String(error)
946
+ fail(`${rel}: не вдалося прочитати (${msg})`)
947
+ return
948
+ }
949
+ const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
950
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru')
951
+ if (v !== null) {
952
+ fail(`${rel}: ${v}`)
953
+ return
954
+ }
955
+ passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
956
+ } else {
957
+ passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
887
958
  }
888
- passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
889
959
  }
890
960
  }
891
961
 
@@ -977,7 +1047,7 @@ export async function check() {
977
1047
 
978
1048
  if (deploymentDirs.size > 0) {
979
1049
  pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
980
- await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
1050
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
981
1051
  pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
982
1052
  await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
983
1053
  }
@@ -735,7 +735,7 @@ function extractJson6902OpsFromArray(arr) {
735
735
  * Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
736
736
  * Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
737
737
  * @param {string} patchText вміст поля **patch** або файлу
738
- * @returns {Array<{ op: string, path: string }>}
738
+ * @returns {Array<{ op: string, path: string }>} нормалізовані **op** / **path** або порожній масив, якщо не JSON6902-масив
739
739
  */
740
740
  export function collectJson6902OperationsFromPatchText(patchText) {
741
741
  const t = typeof patchText === 'string' ? patchText.trim() : ''
@@ -745,12 +745,11 @@ export function collectJson6902OperationsFromPatchText(patchText) {
745
745
  try {
746
746
  const docs = parseAllDocuments(t)
747
747
  for (const d of docs) {
748
- if (d.errors.length > 0) {
749
- continue
750
- }
751
- const j = d.toJSON()
752
- if (Array.isArray(j)) {
753
- return extractJson6902OpsFromArray(j)
748
+ if (d.errors.length === 0) {
749
+ const j = d.toJSON()
750
+ if (Array.isArray(j)) {
751
+ return extractJson6902OpsFromArray(j)
752
+ }
754
753
  }
755
754
  }
756
755
  } catch {
@@ -778,13 +777,12 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
778
777
  /** @type {Map<string, Set<string>>} */
779
778
  const byPath = new Map()
780
779
  for (const { op, path } of ops) {
781
- if (!path) {
782
- continue
783
- }
784
- if (!byPath.has(path)) {
785
- byPath.set(path, new Set())
780
+ if (path) {
781
+ if (!byPath.has(path)) {
782
+ byPath.set(path, new Set())
783
+ }
784
+ byPath.get(path).add(op)
786
785
  }
787
- byPath.get(path).add(op)
788
786
  }
789
787
  /** @type {string[]} */
790
788
  const out = []
@@ -806,93 +804,91 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
806
804
  async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
807
805
  const rootNorm = resolve(root)
808
806
  for (const kustAbs of yamlFilesAbs) {
809
- if (basename(kustAbs).toLowerCase() !== 'kustomization.yaml') {
810
- continue
811
- }
812
- const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
813
- let raw
814
- try {
815
- raw = await readFile(kustAbs, 'utf8')
816
- } catch (error) {
817
- const msg = error instanceof Error ? error.message : String(error)
818
- fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
819
- continue
820
- }
821
- const lines = toLines(raw)
822
- const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
823
- /** @type {import('yaml').Document[]} */
824
- let docs
825
- try {
826
- docs = parseAllDocuments(body)
827
- } catch {
828
- continue
829
- }
830
- for (const doc of docs) {
831
- if (doc.errors.length > 0) {
832
- continue
833
- }
834
- const rootObj = doc.toJSON()
835
- if (rootObj === null || typeof rootObj !== 'object' || Array.isArray(rootObj)) {
836
- continue
837
- }
838
- const rec = /** @type {Record<string, unknown>} */ (rootObj)
839
- if (rec.kind !== 'Kustomization') {
840
- continue
841
- }
842
- const patches = rec.patches
843
- if (!Array.isArray(patches)) {
844
- continue
807
+ if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
808
+ const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
809
+ /** @type {string | undefined} */
810
+ let raw
811
+ let readOk = false
812
+ try {
813
+ raw = await readFile(kustAbs, 'utf8')
814
+ readOk = true
815
+ } catch (error) {
816
+ const msg = error instanceof Error ? error.message : String(error)
817
+ fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
845
818
  }
846
- let patchIdx = 0
847
- for (const p of patches) {
848
- patchIdx++
849
- if (p === null || typeof p !== 'object' || Array.isArray(p)) {
850
- continue
851
- }
852
- const pr = /** @type {Record<string, unknown>} */ (p)
853
- if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
854
- const ops = collectJson6902OperationsFromPatchText(pr.patch)
855
- const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
856
- if (bad.length > 0) {
857
- fail(
858
- `${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
859
- )
860
- }
819
+ if (readOk && raw !== undefined) {
820
+ const lines = toLines(raw)
821
+ const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
822
+ /** @type {import('yaml').Document[] | null} */
823
+ let docs = null
824
+ try {
825
+ docs = parseAllDocuments(body)
826
+ } catch {
827
+ docs = null
861
828
  }
862
- if (typeof pr.path === 'string' && pr.path.trim() !== '') {
863
- const patchRef = pr.path.trim()
864
- const resolved = resolve(dirname(kustAbs), patchRef)
865
- if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
866
- continue
867
- }
868
- if (!existsSync(resolved)) {
869
- continue
870
- }
871
- let st
872
- try {
873
- st = await stat(resolved)
874
- } catch {
875
- continue
876
- }
877
- if (!st.isFile()) {
878
- continue
879
- }
880
- let pRaw
881
- try {
882
- pRaw = await readFile(resolved, 'utf8')
883
- } catch {
884
- continue
885
- }
886
- const ops = collectJson6902OperationsFromPatchText(pRaw)
887
- if (ops.length === 0) {
888
- continue
889
- }
890
- const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
891
- if (bad.length > 0) {
892
- const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
893
- fail(
894
- `${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
895
- )
829
+ if (docs !== null) {
830
+ for (const doc of docs) {
831
+ if (doc.errors.length === 0) {
832
+ const rootObj = doc.toJSON()
833
+ if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
834
+ const rec = /** @type {Record<string, unknown>} */ (rootObj)
835
+ if (rec.kind === 'Kustomization') {
836
+ const patches = rec.patches
837
+ if (Array.isArray(patches)) {
838
+ let patchIdx = 0
839
+ for (const p of patches) {
840
+ patchIdx++
841
+ if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
842
+ const pr = /** @type {Record<string, unknown>} */ (p)
843
+ if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
844
+ const ops = collectJson6902OperationsFromPatchText(pr.patch)
845
+ const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
846
+ if (bad.length > 0) {
847
+ fail(
848
+ `${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
849
+ )
850
+ }
851
+ }
852
+ if (typeof pr.path === 'string' && pr.path.trim() !== '') {
853
+ const patchRef = pr.path.trim()
854
+ const resolved = resolve(dirname(kustAbs), patchRef)
855
+ if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
856
+ /** @type {import('node:fs').Stats | null} */
857
+ let st = null
858
+ try {
859
+ st = await stat(resolved)
860
+ } catch {
861
+ st = null
862
+ }
863
+ if (st !== null && st.isFile()) {
864
+ /** @type {string | undefined} */
865
+ let pRaw
866
+ try {
867
+ pRaw = await readFile(resolved, 'utf8')
868
+ } catch {
869
+ pRaw = undefined
870
+ }
871
+ if (pRaw !== undefined) {
872
+ const ops = collectJson6902OperationsFromPatchText(pRaw)
873
+ if (ops.length > 0) {
874
+ const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
875
+ if (bad.length > 0) {
876
+ const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
877
+ fail(
878
+ `${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
879
+ )
880
+ }
881
+ }
882
+ }
883
+ }
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+ }
890
+ }
891
+ }
896
892
  }
897
893
  }
898
894
  }
@@ -2,7 +2,7 @@
2
2
  * Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
3
3
  *
4
4
  * Очікування: `@nitra/stylelint-config`, `lint-style` через `npx stylelint`, `.stylelintignore`,
5
- * workflow `lint-style.yml` (у `run` — `npx stylelint` або `bun run lint-style`), VSCode stylelint,
5
+ * workflow `lint-style.yml` (у `run` — лише `npx stylelint`, не `bun run lint-style`), VSCode stylelint,
6
6
  * `css.validate` / `scss.validate` / `less.validate`: false.
7
7
  */
8
8
  import { existsSync } from 'node:fs'
@@ -60,13 +60,13 @@ export async function check() {
60
60
  const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
61
61
  pass('lint-style.yml існує')
62
62
  const root = parseWorkflowYaml(content)
63
- const ok = root
64
- ? anyRunStepIncludesStylelint(root)
65
- : content.includes('npx stylelint') || content.includes('bun run lint-style')
63
+ const ok = root ? anyRunStepIncludesStylelint(root) : content.includes('npx stylelint')
66
64
  if (ok) {
67
- pass('lint-style.yml містить npx stylelint або bun run lint-style у кроці run')
65
+ pass('lint-style.yml містить npx stylelint у кроці run')
68
66
  } else {
69
- fail('lint-style.yml має містити npx stylelint або bun run lint-style у кроці run')
67
+ fail(
68
+ "lint-style.yml має викликати stylelint у CI через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix"
69
+ )
70
70
  }
71
71
  } else {
72
72
  fail('.github/workflows/lint-style.yml не існує — створи його')
@@ -341,11 +341,10 @@ export function anyRunStepIncludes(root, needle) {
341
341
  }
342
342
 
343
343
  /**
344
- * Чи викликається stylelint коректно: `npx stylelint` у run або `bun run lint-style`
345
- * (скрипт `lint-style` у package.json має містити `npx stylelint`).
344
+ * Чи викликається stylelint у workflow через `npx stylelint` у кроці `run` (вимога для CI).
346
345
  * @param {Record<string, unknown>} root корінь workflow
347
346
  * @returns {boolean} `true`, якщо умова виконана
348
347
  */
349
348
  export function anyRunStepIncludesStylelint(root) {
350
- return anyRunStepIncludes(root, 'npx stylelint') || anyRunStepIncludes(root, 'bun run lint-style')
349
+ return anyRunStepIncludes(root, 'npx stylelint')
351
350
  }