@nitra/cursor 1.8.79 → 1.8.81

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`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
40
40
 
41
41
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
42
42
  - target:
@@ -66,7 +66,9 @@ spec:
66
66
  value: ru
67
67
  ```
68
68
 
69
- ```yaml title="…/ru/kustomization.yaml (фрагмент)"
69
+ Якщо в цьому ж файлі є **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura з JWT), додай окремий patch на **HTTPRoute** з анотацією для WebSocket:
70
+
71
+ ```yaml title="…/ru/kustomization.yaml (фрагмент, після patch на ConfigMap з HASURA_GRAPHQL_JWT_SECRET)"
70
72
  - target:
71
73
  kind: HTTPRoute
72
74
  name: my-httproute
@@ -95,7 +97,7 @@ patches:
95
97
 
96
98
  ## k8s: overlay **ua** / **ru** і nodeSelector
97
99
 
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**.
100
+ У **`…/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
101
 
100
102
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
101
103
  patches:
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`**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.79",
3
+ "version": "1.8.81",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -17,12 +17,14 @@
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`**
25
- * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
24
+ * **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
25
+ * тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
26
+ * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
27
+ * якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
26
28
  * Вибір **`op`** — **k8s.mdc**.
27
29
  */
28
30
  import { existsSync } from 'node:fs'
@@ -38,6 +40,9 @@ import { walkDir } from './utils/walkDir.mjs'
38
40
 
39
41
  const CONFIG_FILE = '.n-cursor.json'
40
42
 
43
+ /** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
44
+ const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
45
+
41
46
  /** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
42
47
  export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
43
48
 
@@ -66,6 +71,58 @@ export function isUaKustomizationPath(rel) {
66
71
  return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
67
72
  }
68
73
 
74
+ /**
75
+ * Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
76
+ * @param {string} root корінь репозиторію
77
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
78
+ * @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
79
+ */
80
+ export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
81
+ const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
82
+ const m = rel.match(/^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u)
83
+ return m ? join(root, m[1]) : null
84
+ }
85
+
86
+ /**
87
+ * Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
88
+ * @param {string} root корінь репозиторію
89
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
90
+ * @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
91
+ */
92
+ export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
93
+ const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
94
+ if (!pkg) {
95
+ return false
96
+ }
97
+ return (
98
+ existsSync(join(pkg, 'vite.config.js')) ||
99
+ existsSync(join(pkg, 'vite.config.mjs')) ||
100
+ existsSync(join(pkg, 'vite.config.ts'))
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
106
+ * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
107
+ * @param {string} root корінь репозиторію
108
+ * @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
109
+ * @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
110
+ */
111
+ export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
112
+ const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
113
+ if (!pkg) {
114
+ return false
115
+ }
116
+ const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
117
+ for (const dir of deploymentDirs) {
118
+ const norm = dir.replaceAll('\\', '/')
119
+ if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
120
+ return true
121
+ }
122
+ }
123
+ return false
124
+ }
125
+
69
126
  /**
70
127
  * Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
71
128
  * @param {string} rel шлях від кореня репозиторію
@@ -549,11 +606,12 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
549
606
  * Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
550
607
  * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
551
608
  * @param {'ua' | 'ru'} mode **ua** або **ru**
609
+ * @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
552
610
  * @returns {string | null} повідомлення про помилку або **null**
553
611
  */
554
- export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
612
+ export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomizationRaw) {
555
613
  if (typeof combined !== 'string' || combined.trim() === '') {
556
- return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — також gwin… websocket) — abie.mdc`
614
+ return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
557
615
  }
558
616
  if (!/path:\s*\/spec\/hostnames\b/m.test(combined)) {
559
617
  return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
@@ -569,8 +627,12 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
569
627
  if (!namespaceOk) {
570
628
  return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
571
629
  }
572
- if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
573
- return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
630
+ const ruNeedsWebsocket =
631
+ mode === 'ru' &&
632
+ typeof fullKustomizationRaw === 'string' &&
633
+ fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
634
+ if (ruNeedsWebsocket && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
635
+ return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
574
636
  }
575
637
  return null
576
638
  }
@@ -583,7 +645,7 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
583
645
  */
584
646
  export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
585
647
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
586
- return validateAbieNginxRunHttpRoutePatches(combined, mode) === null
648
+ return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
587
649
  }
588
650
 
589
651
  /**
@@ -766,14 +828,16 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
766
828
  }
767
829
 
768
830
  /**
769
- * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc).
831
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc)
832
+ * лише для kustomization того пакета, у дереві **k8s** якого є **Deployment**.
770
833
  * @param {string} root корінь репозиторію
771
834
  * @param {string[]} yamlFilesAbs yaml під k8s
835
+ * @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
772
836
  * @param {(msg: string) => void} fail callback
773
837
  * @param {(msg: string) => void} passFn успішне повідомлення
774
838
  * @returns {Promise<void>}
775
839
  */
776
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
840
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
777
841
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
778
842
  if (uaAbsList.length === 0) {
779
843
  fail(
@@ -783,21 +847,25 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
783
847
  }
784
848
  for (const abs of uaAbsList) {
785
849
  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
850
+ if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
851
+ let raw
852
+ try {
853
+ raw = await readFile(abs, 'utf8')
854
+ } catch (error) {
855
+ const msg = error instanceof Error ? error.message : String(error)
856
+ fail(`${rel}: не вдалося прочитати (${msg})`)
857
+ return
858
+ }
859
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
860
+ fail(
861
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
862
+ )
863
+ return
864
+ }
865
+ passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
866
+ } else {
867
+ passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
799
868
  }
800
- passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
801
869
  }
802
870
 
803
871
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
@@ -809,26 +877,31 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
809
877
  }
810
878
  for (const abs of ruAbsList) {
811
879
  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
880
+ if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
881
+ let raw
882
+ try {
883
+ raw = await readFile(abs, 'utf8')
884
+ } catch (error) {
885
+ const msg = error instanceof Error ? error.message : String(error)
886
+ fail(`${rel}: не вдалося прочитати (${msg})`)
887
+ return
888
+ }
889
+ if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
890
+ fail(
891
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
892
+ )
893
+ return
894
+ }
895
+ passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
896
+ } else {
897
+ passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
825
898
  }
826
- passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
827
899
  }
828
900
  }
829
901
 
830
902
  /**
831
- * Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc.
903
+ * Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
904
+ * лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
832
905
  * @param {string} root корінь репозиторію
833
906
  * @param {string[]} yamlFilesAbs yaml під k8s
834
907
  * @param {(msg: string) => void} fail callback
@@ -838,54 +911,60 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
838
911
  async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
839
912
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
840
913
  if (uaAbsList.length === 0) {
841
- fail(
842
- 'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, parentRefs namespace ua) abie.mdc'
914
+ passFn(
915
+ 'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
843
916
  )
844
- return
845
917
  }
846
918
  for (const abs of uaAbsList) {
847
919
  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
920
+ if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
921
+ let raw
922
+ try {
923
+ raw = await readFile(abs, 'utf8')
924
+ } catch (error) {
925
+ const msg = error instanceof Error ? error.message : String(error)
926
+ fail(`${rel}: не вдалося прочитати (${msg})`)
927
+ return
928
+ }
929
+ const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
930
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
931
+ if (v !== null) {
932
+ fail(`${rel}: ${v}`)
933
+ return
934
+ }
935
+ passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
936
+ } else {
937
+ passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
861
938
  }
862
- passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
863
939
  }
864
940
 
865
941
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
866
942
  if (ruAbsList.length === 0) {
867
- fail(
868
- 'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, namespace ru, gwin websocket) — abie.mdc'
943
+ passFn(
944
+ 'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
869
945
  )
870
- return
871
946
  }
872
947
  for (const abs of ruAbsList) {
873
948
  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
949
+ if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
950
+ let raw
951
+ try {
952
+ raw = await readFile(abs, 'utf8')
953
+ } catch (error) {
954
+ const msg = error instanceof Error ? error.message : String(error)
955
+ fail(`${rel}: не вдалося прочитати (${msg})`)
956
+ return
957
+ }
958
+ const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
959
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw)
960
+ if (v !== null) {
961
+ fail(`${rel}: ${v}`)
962
+ return
963
+ }
964
+ passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
965
+ } else {
966
+ passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
887
967
  }
888
- passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
889
968
  }
890
969
  }
891
970
 
@@ -977,7 +1056,7 @@ export async function check() {
977
1056
 
978
1057
  if (deploymentDirs.size > 0) {
979
1058
  pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
980
- await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
1059
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
981
1060
  pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
982
1061
  await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
983
1062
  }
@@ -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
  }