@nitra/cursor 1.8.100 → 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`**, у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`** (не додавай **`remove`** на **`/spec/clusterIPs`**: у статичному YAML ключа часто немає **`kubectl kustomize`** падає з *Unable to remove nonexistent key*). Деталі — **`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
@@ -130,6 +130,8 @@ patches:
130
130
  path: /spec/clusterIP
131
131
  ```
132
132
 
133
+ Якщо в base у цього **Service** уже є **`spec.clusterIPs`**, той самий **patch** розшир **remove** на **`/spec/clusterIPs`** (див. **`check-abie.mjs`**).
134
+
133
135
  ## k8s: overlay **ru** і HealthCheckPolicy
134
136
 
135
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.100",
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`** — у тому ж patch додай **`op: remove`** для **`/spec/clusterIP`** (поле **`clusterIPs`** у статичному YAML часто відсутнє **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
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'
@@ -387,20 +387,41 @@ export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
387
387
  return sp.clusterIP === 'None'
388
388
  }
389
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
398
+ }
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
403
+ }
404
+ const sp = /** @type {Record<string, unknown>} */ (spec)
405
+ return Object.hasOwn(sp, 'clusterIPs')
406
+ }
407
+
390
408
  /**
391
409
  * Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
392
410
  * @param {string} patchText поле **patch** у kustomization
393
- * @param {string} posixPath наприклад **`/spec/clusterIP`**
411
+ * @param {string} posixPath **`/spec/clusterIP`** або **`/spec/clusterIPs`**
394
412
  * @returns {boolean} true, якщо знайдено пару **remove** + **path**
395
413
  */
396
414
  export function jsonPatchRemovesPath(patchText, posixPath) {
397
415
  if (typeof patchText !== 'string' || patchText.trim() === '') {
398
416
  return false
399
417
  }
400
- if (posixPath !== '/spec/clusterIP') {
418
+ if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
401
419
  return false
402
420
  }
403
- const pathRe = String.raw`path:\s*\/spec\/clusterIP\b`
421
+ const pathRe =
422
+ posixPath === '/spec/clusterIP'
423
+ ? String.raw`path:\s*\/spec\/clusterIP\b`
424
+ : String.raw`path:\s*\/spec\/clusterIPs\b`
404
425
  const opRe = String.raw`op:\s*remove\b`
405
426
  return new RegExp(`${opRe}[\\s\\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(`${pathRe}[\\s\\S]{0,200}?${opRe}`, 'mu').test(patchText)
406
427
  }
@@ -506,7 +527,7 @@ function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
506
527
  /**
507
528
  * Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
508
529
  * @param {string} raw повний текст **kustomization.yaml**
509
- * @param {Map<string, { requiresClusterIPNoneClear: boolean }>} targetsByName ім’я **Service** → чи треба прибрати **None**
530
+ * @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім’я **Service** → прапорці patch
510
531
  * @returns {string[]} порожньо, якщо все OK
511
532
  */
512
533
  export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
@@ -518,7 +539,8 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
518
539
  const errors = []
519
540
  for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
520
541
  const flags = targetsByName.get(name)
521
- const requiresClear = flags?.requiresClusterIPNoneClear === true
542
+ const requiresClusterIPRemove = flags?.requiresClusterIPNoneClear === true
543
+ const requiresClusterIPsRemove = flags?.requiresClusterIPsRemove === true
522
544
  const pt = byName.get(name)
523
545
  if (pt === undefined || String(pt).trim() === '') {
524
546
  errors.push(`${name}: немає inline patch для kind: Service`)
@@ -526,11 +548,16 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
526
548
  if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
527
549
  errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
528
550
  }
529
- if (requiresClear && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
551
+ if (requiresClusterIPRemove && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
530
552
  errors.push(
531
553
  `${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
532
554
  )
533
555
  }
556
+ if (requiresClusterIPsRemove && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
557
+ errors.push(
558
+ `${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
559
+ )
560
+ }
534
561
  }
535
562
  }
536
563
  return errors
@@ -541,10 +568,10 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
541
568
  * @param {string} root корінь репозиторію
542
569
  * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
543
570
  * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
544
- * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
571
+ * @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
545
572
  */
546
573
  async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
547
- /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>} */
574
+ /** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
548
575
  const map = new Map()
549
576
  for (const abs of yamlAbs) {
550
577
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
@@ -590,9 +617,11 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
590
617
  map.set(pkgAbs, inner)
591
618
  }
592
619
  const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
620
+ const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
593
621
  const prev = inner.get(n)
594
622
  inner.set(n, {
595
- requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear
623
+ requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear,
624
+ requiresClusterIPsRemove: (prev?.requiresClusterIPsRemove === true) || needClusterIPsRemove
596
625
  })
597
626
  }
598
627
  }
@@ -607,7 +636,7 @@ async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail)
607
636
  }
608
637
 
609
638
  /**
610
- * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; якщо в base було **`clusterIP: None`** — також **`op: remove`** на **`/spec/clusterIP`** (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).
611
640
  * @param {string} root корінь
612
641
  * @param {string[]} yamlFilesAbs yaml під **k8s**
613
642
  * @param {(msg: string) => void} fail callback
@@ -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`**.