@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 +4 -2
- package/package.json +1 -1
- package/scripts/check-abie.mjs +40 -11
- package/skills/lint/SKILL.md +1 -1
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`** (
|
|
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
package/scripts/check-abie.mjs
CHANGED
|
@@ -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`** —
|
|
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
|
|
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 =
|
|
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** →
|
|
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
|
|
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 (
|
|
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**;
|
|
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
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -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) —
|
|
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`**.
|