@nitra/cursor 1.8.99 → 1.8.102

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
@@ -115,9 +115,9 @@ spec:
115
115
 
116
116
  ## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
117
117
 
118
- Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** (типово **headless**), у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** — інакше API відхилить **NodePort** з помилкою на **`clusterIPs`**. Деталі — **`check-abie.mjs`**.
118
+ Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`**, у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`**. Якщо в base **явно** задано **`spec.clusterIPs`** (зокрема **`['None']`**), додай **`op: remove`** і для **`/spec/clusterIPs`** — інакше **API** може відхилити **NodePort** (*`spec.clusterIPs[0]: Invalid value: "None"`*). **Не** додавай **`remove`** на **`/spec/clusterIPs`**, якщо ключа **немає** в base: **`kubectl kustomize`** тоді падає (*Unable to remove nonexistent key*). Якщо в base лише **`clusterIP: None`**, а помилка лишається після **`kubectl apply -k`** (злиття з уже існуючим **Service** у кластері), тимчасово **видали** **`Service`** у **`ru`** і застосуй знову, або додай у base поле **`clusterIPs`**, щоб **`remove`** у patch був валідний для **kustomize**. Деталі — **`check-abie.mjs`**.
119
119
 
120
- ```yaml title="…/ru/kustomization.yaml (фрагмент, headless → NodePort)"
120
+ ```yaml title="…/ru/kustomization.yaml (фрагмент, headless → NodePort, без clusterIPs у base)"
121
121
  patches:
122
122
  - target:
123
123
  kind: Service
@@ -128,10 +128,10 @@ patches:
128
128
  value: NodePort
129
129
  - op: remove
130
130
  path: /spec/clusterIP
131
- - op: remove
132
- path: /spec/clusterIPs
133
131
  ```
134
132
 
133
+ Якщо в base у цього **Service** уже є **`spec.clusterIPs`**, той самий **patch** розшир **remove** на **`/spec/clusterIPs`** (див. **`check-abie.mjs`**).
134
+
135
135
  ## k8s: overlay **ru** і HealthCheckPolicy
136
136
 
137
137
  Якщо в дереві **k8s** є **HealthCheckPolicy**, у **`ru/kustomization.yaml`** має бути patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc**; перевірка в **`check-k8s.mjs`**, **`ruKustomizationHasHealthCheckDeletePatch`**). Підстав реальне ім’я замість **`СЕРВІС`**:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.99",
3
+ "version": "1.8.102",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -34,7 +34,7 @@
34
34
  * Вибір **`op`** — **k8s.mdc**.
35
35
  *
36
36
  * **Service (overlay ru):** для кожного **Service**, оголошеного в YAML під **`…/k8s/…`**, де шлях **не** проходить через **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару, у т. ч. **headless** з **`clusterIP: None`** і **`-hl`**), якщо ще не **NodePort** / **LoadBalancer** / **ExternalName**,
37
- * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** у тому ж patch додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** (інакше **NodePort** з **`None`** відхиляє API).
37
+ * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** **`op: remove`** для **`/spec/clusterIP`**; якщо в base **явно** задано **`spec.clusterIPs`** — також **`remove`** для **`/spec/clusterIPs`** (інакше **API** може залишити **`None`** для **NodePort**; без ключа **`clusterIPs`** у base **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
38
38
  */
39
39
  import { existsSync } from 'node:fs'
40
40
  import { readFile } from 'node:fs/promises'
@@ -370,9 +370,9 @@ export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
370
370
  }
371
371
 
372
372
  /**
373
- * Чи в base-**Service** є **headless** **`None`**, який треба прибрати в **ru** перед **NodePort** (**clusterIP** / **clusterIPs**).
373
+ * Чи в base-**Service** задано **headless** через **`spec.clusterIP: None`**, який треба прибрати в **ru** перед **NodePort**.
374
374
  * @param {unknown} obj корінь YAML (**Service**)
375
- * @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`** або **`spec.clusterIPs`** містить **`'None'`**
375
+ * @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`**
376
376
  */
377
377
  export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
378
378
  if (!isServiceDoc(obj)) {
@@ -384,20 +384,31 @@ export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
384
384
  return false
385
385
  }
386
386
  const sp = /** @type {Record<string, unknown>} */ (spec)
387
- if (sp.clusterIP === 'None') {
388
- return true
387
+ return sp.clusterIP === 'None'
388
+ }
389
+
390
+ /**
391
+ * Чи в base-**Service** у **`spec`** явно задано поле **`clusterIPs`** (тоді **`remove`** на **`/spec/clusterIPs`** безпечний для **`kubectl kustomize`**).
392
+ * @param {unknown} obj корінь YAML (**Service**)
393
+ * @returns {boolean} **true**, якщо **`Object.hasOwn(spec, 'clusterIPs')`**
394
+ */
395
+ export function serviceDocumentBaseDeclaresClusterIPsField(obj) {
396
+ if (!isServiceDoc(obj)) {
397
+ return false
389
398
  }
390
- const cips = sp.clusterIPs
391
- if (Array.isArray(cips) && cips.includes('None')) {
392
- return true
399
+ const rec = /** @type {Record<string, unknown>} */ (obj)
400
+ const spec = rec.spec
401
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
402
+ return false
393
403
  }
394
- return false
404
+ const sp = /** @type {Record<string, unknown>} */ (spec)
405
+ return Object.hasOwn(sp, 'clusterIPs')
395
406
  }
396
407
 
397
408
  /**
398
409
  * Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
399
410
  * @param {string} patchText поле **patch** у kustomization
400
- * @param {string} posixPath наприклад **`/spec/clusterIP`**
411
+ * @param {string} posixPath **`/spec/clusterIP`** або **`/spec/clusterIPs`**
401
412
  * @returns {boolean} true, якщо знайдено пару **remove** + **path**
402
413
  */
403
414
  export function jsonPatchRemovesPath(patchText, posixPath) {
@@ -416,12 +427,12 @@ export function jsonPatchRemovesPath(patchText, posixPath) {
416
427
  }
417
428
 
418
429
  /**
419
- * Чи patch прибирає **headless** поля **`clusterIP`** / **`clusterIPs`**, щоб **NodePort** пройшов валідацію API.
430
+ * Чи patch містить **`op: remove`** для **`/spec/clusterIP`**, щоб прибрати **headless** перед **NodePort**.
420
431
  * @param {string} patchText поле **patch** у kustomization
421
- * @returns {boolean} true, якщо є **remove** і для **`/spec/clusterIP`**, і для **`/spec/clusterIPs`**
432
+ * @returns {boolean} true, якщо є **remove** для **`/spec/clusterIP`**
422
433
  */
423
434
  export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
424
- return jsonPatchRemovesPath(patchText, '/spec/clusterIP') && jsonPatchRemovesPath(patchText, '/spec/clusterIPs')
435
+ return jsonPatchRemovesPath(patchText, '/spec/clusterIP')
425
436
  }
426
437
 
427
438
  /**
@@ -516,7 +527,7 @@ function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
516
527
  /**
517
528
  * Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
518
529
  * @param {string} raw повний текст **kustomization.yaml**
519
- * @param {Map<string, { requiresClusterIPNoneClear: boolean }>} targetsByName ім’я **Service** → чи треба прибрати **None**
530
+ * @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім’я **Service** → прапорці patch
520
531
  * @returns {string[]} порожньо, якщо все OK
521
532
  */
522
533
  export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
@@ -528,7 +539,8 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
528
539
  const errors = []
529
540
  for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
530
541
  const flags = targetsByName.get(name)
531
- const requiresClear = flags?.requiresClusterIPNoneClear === true
542
+ const requiresClusterIPRemove = flags?.requiresClusterIPNoneClear === true
543
+ const requiresClusterIPsRemove = flags?.requiresClusterIPsRemove === true
532
544
  const pt = byName.get(name)
533
545
  if (pt === undefined || String(pt).trim() === '') {
534
546
  errors.push(`${name}: немає inline patch для kind: Service`)
@@ -536,9 +548,14 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
536
548
  if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
537
549
  errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
538
550
  }
539
- if (requiresClear && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
551
+ if (requiresClusterIPRemove && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
552
+ errors.push(
553
+ `${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
554
+ )
555
+ }
556
+ if (requiresClusterIPsRemove && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
540
557
  errors.push(
541
- `${name}: для spec.clusterIP/spec.clusterIPs: None додай у той самий patch op: remove для path /spec/clusterIP та /spec/clusterIPs (abie.mdc)`
558
+ `${name}: у base задано spec.clusterIPs додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
542
559
  )
543
560
  }
544
561
  }
@@ -551,10 +568,10 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
551
568
  * @param {string} root корінь репозиторію
552
569
  * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
553
570
  * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
554
- * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
571
+ * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
555
572
  */
556
573
  async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
557
- /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>} */
574
+ /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
558
575
  const map = new Map()
559
576
  for (const abs of yamlAbs) {
560
577
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
@@ -600,9 +617,11 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
600
617
  map.set(pkgAbs, inner)
601
618
  }
602
619
  const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
620
+ const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
603
621
  const prev = inner.get(n)
604
622
  inner.set(n, {
605
- requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear
623
+ requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear,
624
+ requiresClusterIPsRemove: (prev?.requiresClusterIPsRemove === true) || needClusterIPsRemove
606
625
  })
607
626
  }
608
627
  }
@@ -617,7 +636,7 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
617
636
  }
618
637
 
619
638
  /**
620
- * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; якщо в base було **`clusterIP: None`** / **`clusterIPs: None`** також **`op: remove`** на **`/spec/clusterIP`** та **`/spec/clusterIPs`** (abie.mdc).
639
+ * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; при **`clusterIP: None`** — **`op: remove`** на **`/spec/clusterIP`**; якщо в base є **`spec.clusterIPs`** ще **`remove`** на **`/spec/clusterIPs`** (abie.mdc).
621
640
  * @param {string} root корінь
622
641
  * @param {string[]} yamlFilesAbs yaml під **k8s**
623
642
  * @param {(msg: string) => void} fail callback
@@ -637,7 +656,7 @@ async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, pass
637
656
  const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
638
657
  if (!existsSync(ruAbs)) {
639
658
  fail(
640
- `${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove clusterIP/clusterIPs): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
659
+ `${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove /spec/clusterIP): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
641
660
  )
642
661
  return
643
662
  }
@@ -25,7 +25,7 @@ bun run lint
25
25
 
26
26
  2. **Якщо exit code не 0** — проаналізуй вивід (останній упавший крок у ланцюжку **`lint`** часто видно з stderr / логів):
27
27
  - Де скрипт уже робить **auto-fix** (**`--fix`**, **`markdownlint-cli2 --fix`**, **`oxfmt`** тощо) — перезапусти **`bun run lint`** після змін файлів.
28
- - Де auto-fix **немає** (наприклад, **jscpd**, **cspell**, **zizmor**, перевірки без прапорця fix) — виправ код, конфіги або винятки **узгоджено з** `.cursor/rules/` (не розширюй ignore лише щоб приховати проблему без причини).
28
+ - Де auto-fix **немає** (наприклад, **jscpd**, **cspell**, **zizmor**, перевірки без прапорця fix) — **рефактори код проєкту**, щоб усунути порушення: перейменуй ідентифікатори, перепиши логіку, видали дублікати тощо. **Заборонено** додавати `eslint-disable`, `// @ts-ignore`, розширювати `ignores`, **`.cspellignore`**, **`.jscpdignore`** або інші винятки **лише** щоб приховати порушення без обґрунтованої причини — політика узгоджена з **`.cursor/rules/`** (зокрема **n-js-lint** для **jscpd**, **n-text** для **cspell** тощо).
29
29
  - Якщо спрацьовує **`sonarjs/cognitive-complexity`** — див. окремий блок нижче.
30
30
 
31
31
  3. **Цикл** — повторюй кроки 1–2, доки **`bun run lint`** не завершиться успішно. Після суттєвих правок за потреби ще раз **`bun run lint`**, щоб переконатися, що не зламав наступний крок у скрипті **`lint`**.