@nitra/cursor 1.8.151 → 1.8.153
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/AGENTS.template.md +8 -0
- package/README.md +7 -2
- package/bin/n-cursor.js +8 -3
- package/mdc/k8s.mdc +21 -1
- package/package.json +1 -1
- package/scripts/build-agents-commands.mjs +88 -0
- package/scripts/check-k8s.mjs +458 -1
package/AGENTS.template.md
CHANGED
|
@@ -18,6 +18,14 @@ The primary development rules are stored in the Cursor rules directory:
|
|
|
18
18
|
{{name}}
|
|
19
19
|
{{/skills}}
|
|
20
20
|
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
Generated from the root `package.json` on each `npx @nitra/cursor` sync. Prefer `bun run <script>` for project scripts.
|
|
24
|
+
|
|
25
|
+
{{#commands}}
|
|
26
|
+
{{name}}
|
|
27
|
+
{{/commands}}
|
|
28
|
+
|
|
21
29
|
## Instructions for all agents
|
|
22
30
|
|
|
23
31
|
Before making changes, read the relevant rule files for the area you are working on.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ CLI автоматично (команда завантаження правил
|
|
|
63
63
|
1. Знайде або створить `.n-cursor.json` у поточній директорії (із полем `$schema` на JSON Schema пакету; якщо файл уже є без коректного `$schema`, поле буде додано або оновлено при зчитуванні конфігу)
|
|
64
64
|
2. Створить директорію `.cursor/rules/`, якщо її ще немає
|
|
65
65
|
3. Скопіює кожне з перелічених у конфігу правило з `mdc/` установленого пакету і збереже файли з префіксом `n-`
|
|
66
|
-
4. Після оновлення файлів на диску згенерує в корені проєкту **`AGENTS.md`**: повний вміст береться з шаблону пакету `AGENTS.template.md`, а список правил у шаблоні формується з **усіх наявних файлів `*.mdc`** у `.cursor/rules/` (відсортовано за ім’ям)
|
|
66
|
+
4. Після оновлення файлів на диску згенерує в корені проєкту **`AGENTS.md`**: повний вміст береться з шаблону пакету `AGENTS.template.md`, а список правил у шаблоні формується з **усіх наявних файлів `*.mdc`** у `.cursor/rules/` (відсортовано за ім’ям); секція команд — з **`package.json`** кореня (див. `{{#commands}}` у шаблоні).
|
|
67
67
|
|
|
68
68
|
## Приклад виводу
|
|
69
69
|
|
|
@@ -114,13 +114,18 @@ npm/
|
|
|
114
114
|
|
|
115
115
|
Під час запуску CLI тіло між `{{#services}}` і `{{/services}}` повторюється для кожного `*.mdc` у `.cursor/rules/`; у `{{name}}` підставляється вже готовий markdown-рядок (наприклад `- .cursor/rules/n-text.mdc`).
|
|
116
116
|
|
|
117
|
-
3.
|
|
117
|
+
3. Для секції **Skills** використовуйте блок **`{{#skills}}` … `{{/skills}}`** з тим самим `{{name}}`: рядки формуються з каталогів у `.cursor/skills/` (див. також `buildSkillBulletItems` у `bin/n-cursor.js`).
|
|
118
|
+
|
|
119
|
+
4. Для секції **Commands** використовуйте **`{{#commands}}` … `{{/commands}}`**: список генерується з кореневого **`package.json`** (поле `scripts` — відомі ключі у фіксованому порядку, плюс додаткові `lint-*`) та завжди доповнюється рядками про **`npx @nitra/cursor`** і **`npx @nitra/cursor check`**. Логіка винесена в **`npm/scripts/build-agents-commands.mjs`**.
|
|
120
|
+
|
|
121
|
+
5. Після змін у шаблоні перевірте локально: у тестовому репозиторії з `.n-cursor.json` виконайте `npx`/`bunx` на зібраному пакеті або `node npm/bin/n-cursor.js` з кореня того репозиторію і переконайтеся, що **`AGENTS.md`** виглядає як очікується.
|
|
118
122
|
|
|
119
123
|
### Логіка в коді CLI
|
|
120
124
|
|
|
121
125
|
- Шлях до шаблону: поруч із `mdc/`, тобто `…/node_modules/@nitra/cursor/AGENTS.template.md` після встановлення пакету.
|
|
122
126
|
- Оновлення **`AGENTS.md`** виконується **після** циклу завантаження правил, щоб список відображав актуальний вміст `.cursor/rules/` на диску.
|
|
123
127
|
- Якщо каталогу `.cursor/rules/` немає або в ньому немає `*.mdc`, блок `{{#services}}` стає порожнім; решта шаблону все одно записується в **`AGENTS.md`**.
|
|
128
|
+
- Секція **`commands`** залежить лише від **`package.json` у корені cwd**; якщо файлу немає або `scripts` відсутній, у блоці лишаються мінімальні рядки (`bun i`, виклики CLI).
|
|
124
129
|
|
|
125
130
|
## Мета проекту
|
|
126
131
|
|
package/bin/n-cursor.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Файл AGENTS.md у корені: щоразу повністю перезаписується змістом з AGENTS.template.md
|
|
20
20
|
* пакету; список правил у шаблоні будується з файлів *.mdc у .cursor/rules поточного проєкту.
|
|
21
|
+
* Секція команд — з кореневого package.json (scripts) та фіксовані рядки про CLI синхрону/перевірок.
|
|
21
22
|
*
|
|
22
23
|
* Після завантаження: у .cursor/rules видаляються файли *.mdc з префіксом «n-» (керовані
|
|
23
24
|
* пакетом), яких немає у списку rules у .n-cursor.json. Інші .mdc у цій директорії залишаються.
|
|
@@ -45,6 +46,7 @@ import { basename, dirname, join } from 'node:path'
|
|
|
45
46
|
import { cwd } from 'node:process'
|
|
46
47
|
import { fileURLToPath } from 'node:url'
|
|
47
48
|
|
|
49
|
+
import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
|
|
48
50
|
import { detectAutoRulesAndSkills, mergeConfigWithAutoDetected, normalizeIdList } from '../scripts/auto-rules.mjs'
|
|
49
51
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
50
52
|
import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
|
|
@@ -424,19 +426,21 @@ function expandMustacheSection(template, section, items, prop) {
|
|
|
424
426
|
}
|
|
425
427
|
|
|
426
428
|
/**
|
|
427
|
-
* Підставляє у вміст AGENTS.template.md список шляхів до файлів
|
|
429
|
+
* Підставляє у вміст AGENTS.template.md список шляхів до файлів правил, skills і команд з package.json
|
|
428
430
|
* @param {string} templateText вміст AGENTS.template.md
|
|
429
431
|
* @param {string[]} mdcBasenames імена файлів (*.mdc) з .cursor/rules
|
|
430
432
|
* @param {{ name: string }[]} skillItems рядки для секції Skills
|
|
433
|
+
* @param {{ name: string }[]} commandItems рядки для секції commands
|
|
431
434
|
* @returns {string} готовий markdown для AGENTS.md
|
|
432
435
|
*/
|
|
433
|
-
function renderAgentsTemplate(templateText, mdcBasenames, skillItems) {
|
|
436
|
+
function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems) {
|
|
434
437
|
let result = templateText
|
|
435
438
|
const serviceItems = mdcBasenames.map(mdcName => ({
|
|
436
439
|
name: `- ${RULES_DIR}/${mdcName}`
|
|
437
440
|
}))
|
|
438
441
|
result = expandMustacheSection(result, 'services', serviceItems, 'name')
|
|
439
442
|
result = expandMustacheSection(result, 'skills', skillItems, 'name')
|
|
443
|
+
result = expandMustacheSection(result, 'commands', commandItems, 'name')
|
|
440
444
|
return result
|
|
441
445
|
}
|
|
442
446
|
|
|
@@ -650,7 +654,8 @@ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
|
|
|
650
654
|
const templateText = await readFile(agentsTemplatePath, 'utf8')
|
|
651
655
|
const mdcFiles = await listProjectRulesMdcFiles()
|
|
652
656
|
const skillItems = await buildSkillBulletItems()
|
|
653
|
-
const
|
|
657
|
+
const commandItems = await buildAgentsCommandBulletItems(cwd())
|
|
658
|
+
const body = renderAgentsTemplate(templateText, mdcFiles, skillItems, commandItems)
|
|
654
659
|
const agentsPath = join(cwd(), AGENTS_FILE)
|
|
655
660
|
const hadFile = existsSync(agentsPath)
|
|
656
661
|
const out = body.endsWith('\n') ? body : `${body}\n`
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.26'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -266,6 +266,26 @@ data:
|
|
|
266
266
|
|
|
267
267
|
- Після перенесення маніфестів у **`base`** та overlays і перевірки (**`check k8s`**, **`lint-k8s`**) **видали** застарілі файли та директорії, які замінені новою схемою (дубльовані копії, колишні шляхи без Kustomize), щоб у репозиторії не залишалося зайвих або суперечливих маніфестів.
|
|
268
268
|
|
|
269
|
+
### Зміна image — через `images:`, не через `patches[]`
|
|
270
|
+
|
|
271
|
+
Підміну image у Pod-шаблоні Deployment в overlay роби директивою `images:`, а не JSON6902-патчем `op: replace` на `/spec/template/spec/containers/<N>/image`. `images:` — канонічний механізм Kustomize (матчить за іменем образу, не за індексом контейнера, тож стійкий до перестановки).
|
|
272
|
+
|
|
273
|
+
- У **`name`** — те, що **дослівно** стоїть у `image:` у base (або в попередньому шарі) **без тегу**: інакше підміна не спрацює. Тег у `name` зайвий — Kustomize матчить лише за іменем.
|
|
274
|
+
- **`newName`** — кінцеве ім'я образу (як правило, збігається з `name`); `**newTag**` — тег для прода. Якщо `newTag` дорівнює тегу, який вже в base, його можна не вказувати.
|
|
275
|
+
- **`digest`** (`@sha256:…`) у `name` / `newName` не чіпай — це не тег.
|
|
276
|
+
|
|
277
|
+
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
278
|
+
images:
|
|
279
|
+
- name: europe-west4-docker.pkg.dev/abie-ua/c/apply-on-invoice-discount
|
|
280
|
+
newName: europe-west4-docker.pkg.dev/abie-ua/c/apply-on-invoice-discount
|
|
281
|
+
newTag: v2025-04-29
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**`check k8s` автоматично** для кожного `kustomization.yaml`:
|
|
285
|
+
|
|
286
|
+
1. конвертує JSON6902-патч `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:` (резолвить оригінальний image у base через `resources:` / `bases` / `components` / `crds`); якщо в `patches[]` залишається лише ця операція — патч прибирається повністю;
|
|
287
|
+
2. чистить існуючий блок `images:` — зрізає `:tag` з `name` і видаляє `newTag`, який збігається з відрізаним тегом.
|
|
288
|
+
|
|
269
289
|
## Ingress → Gateway API (GKE)
|
|
270
290
|
|
|
271
291
|
Якщо в дереві **`k8s`** трапляється маніфест з **`kind: Ingress`**, його потрібно **замінити на Gateway API**, а не залишати Ingress.
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Формує markdown-рядки для секції «Команди» у AGENTS.md.
|
|
3
|
+
*
|
|
4
|
+
* Джерело істини — `package.json` у корені цільового репозиторію: з поля `scripts` беруться відомі ключі
|
|
5
|
+
* у стабільному порядку, додатково — усі `lint-*`, яких не було в основному списку.
|
|
6
|
+
*
|
|
7
|
+
* Наприкінці завжди додаються рядки про CLI `@nitra/cursor` (синхрон правил / programmatic check),
|
|
8
|
+
* на початку — рекомендована команда `bun i` за конвенціями monorepo.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { readFile } from 'node:fs/promises'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
const PACKAGE_NAME = '@nitra/cursor'
|
|
15
|
+
const AGENTS_MD = 'AGENTS.md'
|
|
16
|
+
|
|
17
|
+
/** Порядок виводу скриптів із `package.json` (лише ті, що реально існують). */
|
|
18
|
+
const SCRIPT_KEYS_ORDER = /** @type {const} */ ([
|
|
19
|
+
'test',
|
|
20
|
+
'lint',
|
|
21
|
+
'lint-js',
|
|
22
|
+
'lint-text',
|
|
23
|
+
'lint-ga',
|
|
24
|
+
'lint-k8s',
|
|
25
|
+
'lint-docker',
|
|
26
|
+
'start',
|
|
27
|
+
'dev',
|
|
28
|
+
'build'
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Зчитує `scripts` з `package.json` у `projectRoot` або повертає порожній об'єкт.
|
|
33
|
+
* @param {string} projectRoot абсолютний шлях до кореня репозиторію
|
|
34
|
+
* @returns {Promise<Record<string, string>>} об'єкт скриптів
|
|
35
|
+
*/
|
|
36
|
+
async function readPackageScripts(projectRoot) {
|
|
37
|
+
const pkgPath = join(projectRoot, 'package.json')
|
|
38
|
+
if (!existsSync(pkgPath)) {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(pkgPath, 'utf8')
|
|
43
|
+
const pkg = JSON.parse(raw)
|
|
44
|
+
if (pkg && typeof pkg === 'object' && pkg.scripts && typeof pkg.scripts === 'object') {
|
|
45
|
+
return /** @type {Record<string, string>} */ (pkg.scripts)
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// некоректний JSON або IO — секція команд лишиться з мінімумом (bun i + npx)
|
|
49
|
+
}
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Повертає елементи для Mustache-секції `commands` у AGENTS.template.md.
|
|
55
|
+
* @param {string} projectRoot абсолютний шлях до кореня репозиторію (зазвичай `process.cwd()`)
|
|
56
|
+
* @returns {Promise<{ name: string }[]>} рядки з полем `name` для `expandMustacheSection`
|
|
57
|
+
*/
|
|
58
|
+
export async function buildAgentsCommandBulletItems(projectRoot) {
|
|
59
|
+
const scripts = await readPackageScripts(projectRoot)
|
|
60
|
+
const items = /** @type {{ name: string }[]} */ ([])
|
|
61
|
+
|
|
62
|
+
items.push({ name: `- **Залежності**: \`bun i\`` })
|
|
63
|
+
|
|
64
|
+
const added = new Set()
|
|
65
|
+
|
|
66
|
+
for (const key of SCRIPT_KEYS_ORDER) {
|
|
67
|
+
if (typeof scripts[key] === 'string' && scripts[key].length > 0) {
|
|
68
|
+
items.push({ name: `- **${key}**: \`bun run ${key}\`` })
|
|
69
|
+
added.add(key)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const lintExtraKeys = Object.keys(scripts)
|
|
74
|
+
.filter(k => k.startsWith('lint-') && !added.has(k) && typeof scripts[k] === 'string')
|
|
75
|
+
.toSorted((a, b) => a.localeCompare(b))
|
|
76
|
+
|
|
77
|
+
for (const key of lintExtraKeys) {
|
|
78
|
+
items.push({ name: `- **${key}**: \`bun run ${key}\`` })
|
|
79
|
+
added.add(key)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
items.push({
|
|
83
|
+
name: `- **Оновити правила та ${AGENTS_MD}** (після змін у правилах/шаблоні CLI): \`npx ${PACKAGE_NAME}\``
|
|
84
|
+
})
|
|
85
|
+
items.push({ name: `- **Перевірки правил (programmatic)**: \`npx ${PACKAGE_NAME} check\`` })
|
|
86
|
+
|
|
87
|
+
return items
|
|
88
|
+
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -89,6 +89,13 @@
|
|
|
89
89
|
* `replacements[].path` має вказувати на наявний у репозиторії файл (`.yaml` / `.yml`) або каталог; інакше
|
|
90
90
|
* помилка `check k8s` (k8s.mdc).
|
|
91
91
|
*
|
|
92
|
+
* **Images у Kustomize — `images:`, не patch:** для кожного `kustomization.yaml` автоматично:
|
|
93
|
+
* (а) конвертує JSON6902 `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у
|
|
94
|
+
* запис **`images:`** — `name` береться з оригінального `image:` у base (без тегу), `newName` — з patch.value (без тегу),
|
|
95
|
+
* `newTag` — лише якщо тег у patch.value відрізняється від тега в base; якщо `patches[]` після цього порожній — ключ
|
|
96
|
+
* прибирається; (б) чистить існуючий блок **`images:`** — зрізає `:tag` з `name` (digest `@…` не чіпає) і видаляє
|
|
97
|
+
* `newTag`, який збігається з відрізаним тегом.
|
|
98
|
+
*
|
|
92
99
|
* **HPA / PDB тільки з Deployment у шарі base:** у дереві Kustomize з `…/k8s/…/base/kustomization.yaml` не
|
|
93
100
|
* дозволяти `HorizontalPodAutoscaler` / `PodDisruptionBudget` у `resources` / `bases` / `components` / `crds`
|
|
94
101
|
* (рекурсивно), якщо в цьому ж дереві немає документа **`Deployment`** у жодному YAML під **`…/k8s/…/base/`**. У
|
|
@@ -99,7 +106,7 @@ import { existsSync } from 'node:fs'
|
|
|
99
106
|
import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
100
107
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
101
108
|
|
|
102
|
-
import { parseAllDocuments } from 'yaml'
|
|
109
|
+
import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
|
|
103
110
|
|
|
104
111
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
105
112
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -5040,6 +5047,454 @@ async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, pas
|
|
|
5040
5047
|
}
|
|
5041
5048
|
}
|
|
5042
5049
|
|
|
5050
|
+
/**
|
|
5051
|
+
* Розбирає рядок image на ім'я і тег, з виявленням digest.
|
|
5052
|
+
*
|
|
5053
|
+
* - `foo@sha256:…` — `hasDigest: true`, тег не виділяється;
|
|
5054
|
+
* - `localhost:5000/foo` (порт без тегу) — теж без виділення;
|
|
5055
|
+
* - `localhost:5000/foo:tag` — `name: 'localhost:5000/foo'`, `tag: 'tag'`.
|
|
5056
|
+
*
|
|
5057
|
+
* Тег визначається лише по **останній** двокрапці; якщо після неї є `/` — це порт реєстру, не тег.
|
|
5058
|
+
* @param {string} image рядок image
|
|
5059
|
+
* @returns {{ name: string, tag: string | null, hasDigest: boolean }}
|
|
5060
|
+
*/
|
|
5061
|
+
export function splitImageNameTagDigest(image) {
|
|
5062
|
+
if (image.includes('@')) {
|
|
5063
|
+
return { name: image, tag: null, hasDigest: true }
|
|
5064
|
+
}
|
|
5065
|
+
const lastColon = image.lastIndexOf(':')
|
|
5066
|
+
if (lastColon === -1) {
|
|
5067
|
+
return { name: image, tag: null, hasDigest: false }
|
|
5068
|
+
}
|
|
5069
|
+
const after = image.slice(lastColon + 1)
|
|
5070
|
+
if (after === '' || after.includes('/')) {
|
|
5071
|
+
return { name: image, tag: null, hasDigest: false }
|
|
5072
|
+
}
|
|
5073
|
+
return { name: image.slice(0, lastColon), tag: after, hasDigest: false }
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
/**
|
|
5077
|
+
* Розпаковує YAML-скаляр з оточуючими лапками (single або double). Інші стилі (block scalar) — повертає як є.
|
|
5078
|
+
* @param {string} raw сирий рядок-значення без trailing whitespace/comment
|
|
5079
|
+
* @returns {{ unquoted: string, quote: '' | "'" | '"' }}
|
|
5080
|
+
*/
|
|
5081
|
+
function parseQuotedYamlScalar(raw) {
|
|
5082
|
+
if (raw.length >= 2) {
|
|
5083
|
+
const first = raw.charAt(0)
|
|
5084
|
+
const last = raw.charAt(raw.length - 1)
|
|
5085
|
+
if (first === '"' && last === '"') {
|
|
5086
|
+
return { unquoted: raw.slice(1, -1), quote: '"' }
|
|
5087
|
+
}
|
|
5088
|
+
if (first === "'" && last === "'") {
|
|
5089
|
+
return { unquoted: raw.slice(1, -1).replaceAll("''", "'"), quote: "'" }
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
return { unquoted: raw, quote: '' }
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
/**
|
|
5096
|
+
* Загортає скаляр у лапки, повертаючи оригінальний стиль.
|
|
5097
|
+
* @param {string} value значення без оточуючих лапок
|
|
5098
|
+
* @param {'' | "'" | '"'} quote стиль лапок
|
|
5099
|
+
* @returns {string} рядок-скаляр для запису назад у YAML
|
|
5100
|
+
*/
|
|
5101
|
+
function requoteYamlScalar(value, quote) {
|
|
5102
|
+
if (quote === '"') return `"${value}"`
|
|
5103
|
+
if (quote === "'") return `'${value.replaceAll("'", "''")}'`
|
|
5104
|
+
return value
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5107
|
+
/** Regex: рядок верхнього рівня з ключем `images:` (без значення в тому ж рядку). */
|
|
5108
|
+
const KUSTOMIZATION_IMAGES_KEY_RE = /^images:\s*(?:#.*)?$/u
|
|
5109
|
+
/** Regex: початок елемента масиву (`-` з відступом). Групує сам відступ перед `-`. */
|
|
5110
|
+
const KUSTOMIZATION_LIST_ITEM_RE = /^(\s*)-\s/u
|
|
5111
|
+
/** Regex: значення поля (name / newName / newTag) у рядку, з опційним `- ` префіксом. */
|
|
5112
|
+
const KUSTOMIZATION_IMAGE_FIELD_RE = /^(\s*(?:-\s+)?)(name|newName|newTag):(\s+)(\S.*?)(\s*(?:#.*)?)$/u
|
|
5113
|
+
|
|
5114
|
+
/**
|
|
5115
|
+
* Автофікс блоку `images:` у kustomization.yaml: зрізає `:tag` з `name` (digest `@…` не чіпає)
|
|
5116
|
+
* і видаляє `newTag`, який збігається зі зрізаним тегом. Працює рядково, зберігаючи коментарі
|
|
5117
|
+
* й форматування.
|
|
5118
|
+
* @param {string} raw вміст файлу
|
|
5119
|
+
* @returns {{ changed: boolean, content: string }}
|
|
5120
|
+
*/
|
|
5121
|
+
export function cleanupKustomizationImagesInYamlText(raw) {
|
|
5122
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
5123
|
+
const lines = raw.split(YAML_LINE_SPLIT_RE)
|
|
5124
|
+
|
|
5125
|
+
let imagesStart = -1
|
|
5126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5127
|
+
if (KUSTOMIZATION_IMAGES_KEY_RE.test(lines[i])) {
|
|
5128
|
+
imagesStart = i + 1
|
|
5129
|
+
break
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
if (imagesStart === -1) return { changed: false, content: raw }
|
|
5133
|
+
|
|
5134
|
+
let imagesEnd = lines.length
|
|
5135
|
+
for (let i = imagesStart; i < lines.length; i++) {
|
|
5136
|
+
const l = lines[i]
|
|
5137
|
+
if (l === '' || /^\s/u.test(l) || /^#/u.test(l)) continue
|
|
5138
|
+
imagesEnd = i
|
|
5139
|
+
break
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
/** @type {Array<{ start: number, end: number }>} */
|
|
5143
|
+
const entries = []
|
|
5144
|
+
let curStart = -1
|
|
5145
|
+
for (let i = imagesStart; i < imagesEnd; i++) {
|
|
5146
|
+
if (KUSTOMIZATION_LIST_ITEM_RE.test(lines[i])) {
|
|
5147
|
+
if (curStart >= 0) entries.push({ start: curStart, end: i })
|
|
5148
|
+
curStart = i
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
if (curStart >= 0) entries.push({ start: curStart, end: imagesEnd })
|
|
5152
|
+
|
|
5153
|
+
/** @type {Map<number, string>} */
|
|
5154
|
+
const replacements = new Map()
|
|
5155
|
+
/** @type {Set<number>} */
|
|
5156
|
+
const removals = new Set()
|
|
5157
|
+
let changed = false
|
|
5158
|
+
|
|
5159
|
+
for (const { start, end } of entries) {
|
|
5160
|
+
/** @type {string | null} */
|
|
5161
|
+
let strippedTag = null
|
|
5162
|
+
let nameProcessed = false
|
|
5163
|
+
/** @type {{ lineIdx: number, value: string } | null} */
|
|
5164
|
+
let newTagInfo = null
|
|
5165
|
+
let newTagProcessed = false
|
|
5166
|
+
|
|
5167
|
+
for (let i = start; i < end; i++) {
|
|
5168
|
+
const m = lines[i].match(KUSTOMIZATION_IMAGE_FIELD_RE)
|
|
5169
|
+
if (m === null) continue
|
|
5170
|
+
const [, prefix, key, sep, valueRaw, trailing] = m
|
|
5171
|
+
if (key === 'name' && !nameProcessed) {
|
|
5172
|
+
nameProcessed = true
|
|
5173
|
+
const { unquoted, quote } = parseQuotedYamlScalar(valueRaw)
|
|
5174
|
+
const split = splitImageNameTagDigest(unquoted)
|
|
5175
|
+
if (split.tag !== null) {
|
|
5176
|
+
const newLine = `${prefix}name:${sep}${requoteYamlScalar(split.name, quote)}${trailing}`
|
|
5177
|
+
if (newLine !== lines[i]) {
|
|
5178
|
+
replacements.set(i, newLine)
|
|
5179
|
+
changed = true
|
|
5180
|
+
}
|
|
5181
|
+
strippedTag = split.tag
|
|
5182
|
+
}
|
|
5183
|
+
} else if (key === 'newTag' && !newTagProcessed) {
|
|
5184
|
+
newTagProcessed = true
|
|
5185
|
+
const { unquoted } = parseQuotedYamlScalar(valueRaw)
|
|
5186
|
+
newTagInfo = { lineIdx: i, value: unquoted }
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
if (newTagInfo !== null && strippedTag !== null && newTagInfo.value === strippedTag) {
|
|
5191
|
+
removals.add(newTagInfo.lineIdx)
|
|
5192
|
+
changed = true
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
if (!changed) return { changed: false, content: raw }
|
|
5197
|
+
|
|
5198
|
+
/** @type {string[]} */
|
|
5199
|
+
const out = []
|
|
5200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5201
|
+
if (removals.has(i)) continue
|
|
5202
|
+
out.push(replacements.has(i) ? replacements.get(i) : lines[i])
|
|
5203
|
+
}
|
|
5204
|
+
return { changed: true, content: out.join(eol) }
|
|
5205
|
+
}
|
|
5206
|
+
|
|
5207
|
+
/** Regex: JSON6902 path для image окремого контейнера у Pod-шаблоні Deployment. */
|
|
5208
|
+
const KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE = /^\/spec\/template\/spec\/containers\/(\d+)\/image$/u
|
|
5209
|
+
|
|
5210
|
+
/**
|
|
5211
|
+
* Якщо `patchObj` — JSON6902 з єдиною операцією `replace` на шляху image-контейнера у `Deployment`,
|
|
5212
|
+
* повертає `{ deployName, containerIndex, newImage }`. Інакше null.
|
|
5213
|
+
* @param {unknown} patchObj елемент масиву `patches[]`
|
|
5214
|
+
* @returns {{ deployName: string, containerIndex: number, newImage: string } | null}
|
|
5215
|
+
*/
|
|
5216
|
+
export function imageReplaceDeploymentPatchInfo(patchObj) {
|
|
5217
|
+
if (patchObj === null || typeof patchObj !== 'object' || Array.isArray(patchObj)) return null
|
|
5218
|
+
const pr = /** @type {Record<string, unknown>} */ (patchObj)
|
|
5219
|
+
const target = pr.target
|
|
5220
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
5221
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
5222
|
+
if (t.kind !== 'Deployment') return null
|
|
5223
|
+
if (typeof t.name !== 'string' || t.name.trim() === '') return null
|
|
5224
|
+
if (typeof pr.patch !== 'string') return null
|
|
5225
|
+
|
|
5226
|
+
let parsedArr
|
|
5227
|
+
try {
|
|
5228
|
+
for (const d of parseAllDocuments(pr.patch.trim())) {
|
|
5229
|
+
if (d.errors.length === 0) {
|
|
5230
|
+
const j = d.toJSON()
|
|
5231
|
+
if (Array.isArray(j)) {
|
|
5232
|
+
parsedArr = j
|
|
5233
|
+
break
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
} catch {
|
|
5238
|
+
return null
|
|
5239
|
+
}
|
|
5240
|
+
if (!Array.isArray(parsedArr) || parsedArr.length !== 1) return null
|
|
5241
|
+
const op = parsedArr[0]
|
|
5242
|
+
if (op === null || typeof op !== 'object' || Array.isArray(op)) return null
|
|
5243
|
+
const oo = /** @type {Record<string, unknown>} */ (op)
|
|
5244
|
+
if (typeof oo.op !== 'string' || oo.op.toLowerCase() !== 'replace') return null
|
|
5245
|
+
if (typeof oo.path !== 'string') return null
|
|
5246
|
+
const m = oo.path.match(KUSTOMIZATION_DEPLOYMENT_CONTAINER_IMAGE_PATH_RE)
|
|
5247
|
+
if (m === null) return null
|
|
5248
|
+
if (typeof oo.value !== 'string' || oo.value.trim() === '') return null
|
|
5249
|
+
|
|
5250
|
+
return {
|
|
5251
|
+
deployName: t.name.trim(),
|
|
5252
|
+
containerIndex: Number(m[1]),
|
|
5253
|
+
newImage: oo.value.trim()
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
/**
|
|
5258
|
+
* Шукає `Deployment.spec.template.spec.containers[N].image` у YAML-файлі.
|
|
5259
|
+
* @param {string} absPath абсолютний шлях до YAML-файлу
|
|
5260
|
+
* @param {string} deployName ім'я Deployment
|
|
5261
|
+
* @param {number} containerIndex індекс контейнера
|
|
5262
|
+
* @returns {Promise<string | null>} рядок image або null
|
|
5263
|
+
*/
|
|
5264
|
+
async function findDeploymentContainerImageInFile(absPath, deployName, containerIndex) {
|
|
5265
|
+
const raw = await tryReadFileUtf8(absPath)
|
|
5266
|
+
if (raw === undefined) return null
|
|
5267
|
+
const docs = tryParseAllYamlDocs(raw)
|
|
5268
|
+
if (docs === undefined) return null
|
|
5269
|
+
for (const d of docs) {
|
|
5270
|
+
if (d.errors.length !== 0) continue
|
|
5271
|
+
const o = d.toJSON()
|
|
5272
|
+
if (o === null || typeof o !== 'object' || Array.isArray(o)) continue
|
|
5273
|
+
const oo = /** @type {Record<string, unknown>} */ (o)
|
|
5274
|
+
if (oo.kind !== 'Deployment') continue
|
|
5275
|
+
const meta = oo.metadata
|
|
5276
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) continue
|
|
5277
|
+
if (/** @type {Record<string, unknown>} */ (meta).name !== deployName) continue
|
|
5278
|
+
const spec = oo.spec
|
|
5279
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) continue
|
|
5280
|
+
const tmpl = /** @type {Record<string, unknown>} */ (spec).template
|
|
5281
|
+
if (tmpl === null || typeof tmpl !== 'object' || Array.isArray(tmpl)) continue
|
|
5282
|
+
const podSpec = /** @type {Record<string, unknown>} */ (tmpl).spec
|
|
5283
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) continue
|
|
5284
|
+
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
5285
|
+
if (!Array.isArray(containers) || containerIndex < 0 || containerIndex >= containers.length) continue
|
|
5286
|
+
const c = containers[containerIndex]
|
|
5287
|
+
if (c === null || typeof c !== 'object' || Array.isArray(c)) continue
|
|
5288
|
+
const img = /** @type {Record<string, unknown>} */ (c).image
|
|
5289
|
+
if (typeof img === 'string' && img.trim() !== '') return img.trim()
|
|
5290
|
+
}
|
|
5291
|
+
return null
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
/**
|
|
5295
|
+
* Рекурсивно проходить дерево kustomization (resources / bases / components / crds), шукаючи
|
|
5296
|
+
* `Deployment` із заданим іменем; повертає image потрібного контейнера або null, якщо не знайдено.
|
|
5297
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml (поточний шар)
|
|
5298
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5299
|
+
* @param {string} deployName ім'я Deployment
|
|
5300
|
+
* @param {number} containerIndex індекс контейнера
|
|
5301
|
+
* @param {Set<string>} visited нормалізовані відвідані kustomization.yaml
|
|
5302
|
+
* @returns {Promise<string | null>} image або null
|
|
5303
|
+
*/
|
|
5304
|
+
async function walkKustomizationForDeploymentImage(kustAbs, rootNorm, deployName, containerIndex, visited) {
|
|
5305
|
+
const norm = resolve(kustAbs)
|
|
5306
|
+
if (visited.has(norm)) return null
|
|
5307
|
+
visited.add(norm)
|
|
5308
|
+
|
|
5309
|
+
const obj = await readFirstYamlObject(norm)
|
|
5310
|
+
if (obj === null) return null
|
|
5311
|
+
const kustDir = dirname(norm)
|
|
5312
|
+
const refs = pathsFromKustomizationObject(obj)
|
|
5313
|
+
|
|
5314
|
+
for (const ref of refs) {
|
|
5315
|
+
if (typeof ref !== 'string' || ref.includes('://') || ref.trim() === '') continue
|
|
5316
|
+
const resolved = resolve(kustDir, ref.trim())
|
|
5317
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) continue
|
|
5318
|
+
let st
|
|
5319
|
+
try {
|
|
5320
|
+
st = await stat(resolved)
|
|
5321
|
+
} catch {
|
|
5322
|
+
continue
|
|
5323
|
+
}
|
|
5324
|
+
if (st.isFile() && YAML_EXTENSION_RE.test(resolved)) {
|
|
5325
|
+
const img = await findDeploymentContainerImageInFile(resolved, deployName, containerIndex)
|
|
5326
|
+
if (img !== null) return img
|
|
5327
|
+
} else if (st.isDirectory()) {
|
|
5328
|
+
const childK = join(resolved, 'kustomization.yaml')
|
|
5329
|
+
if (existsSync(childK)) {
|
|
5330
|
+
const img = await walkKustomizationForDeploymentImage(childK, rootNorm, deployName, containerIndex, visited)
|
|
5331
|
+
if (img !== null) return img
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
return null
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
/**
|
|
5339
|
+
* Конвертує JSON6902 image-replace patches у `images:` для одного kustomization.yaml.
|
|
5340
|
+
*
|
|
5341
|
+
* Алгоритм:
|
|
5342
|
+
*
|
|
5343
|
+
* 1. Читає файл, парсить як **Document** (yaml lib), щоб максимально зберегти форматування.
|
|
5344
|
+
* 2. Для кожного `patches[i]` з `target.kind: Deployment` і єдиною операцією
|
|
5345
|
+
* `op: replace` на `path: /spec/template/spec/containers/N/image` шукає оригінальний image
|
|
5346
|
+
* через `walkKustomizationForDeploymentImage` (resources → recursively).
|
|
5347
|
+
* 3. Будує `images:` запис: `name = base_image_без_тегу/digest`, `newName = patch_value_без_тегу`,
|
|
5348
|
+
* `newTag = patch_value_тег`, **якщо** він відрізняється від тега base.
|
|
5349
|
+
* 4. Видаляє відповідні patches; якщо `patches:` стає порожнім — видаляє ключ.
|
|
5350
|
+
* 5. Записує файл назад через `Document.toString()`.
|
|
5351
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5352
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
5353
|
+
* @returns {Promise<{ changed: boolean, content?: string, errors: string[] }>}
|
|
5354
|
+
*/
|
|
5355
|
+
export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNorm) {
|
|
5356
|
+
const raw = await tryReadFileUtf8(kustAbs)
|
|
5357
|
+
if (raw === undefined) return { changed: false, errors: [] }
|
|
5358
|
+
|
|
5359
|
+
let doc
|
|
5360
|
+
try {
|
|
5361
|
+
doc = parseDocument(raw)
|
|
5362
|
+
} catch {
|
|
5363
|
+
return { changed: false, errors: [] }
|
|
5364
|
+
}
|
|
5365
|
+
if (doc.errors.length > 0) return { changed: false, errors: [] }
|
|
5366
|
+
|
|
5367
|
+
const obj = doc.toJSON()
|
|
5368
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
5369
|
+
return { changed: false, errors: [] }
|
|
5370
|
+
}
|
|
5371
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
5372
|
+
if (rec.kind !== 'Kustomization') return { changed: false, errors: [] }
|
|
5373
|
+
if (typeof rec.apiVersion !== 'string' || !rec.apiVersion.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) {
|
|
5374
|
+
return { changed: false, errors: [] }
|
|
5375
|
+
}
|
|
5376
|
+
if (!Array.isArray(rec.patches)) return { changed: false, errors: [] }
|
|
5377
|
+
|
|
5378
|
+
/** @type {Array<{ index: number, info: { deployName: string, containerIndex: number, newImage: string } }>} */
|
|
5379
|
+
const candidates = []
|
|
5380
|
+
for (const [i, p] of rec.patches.entries()) {
|
|
5381
|
+
const info = imageReplaceDeploymentPatchInfo(p)
|
|
5382
|
+
if (info !== null) candidates.push({ index: i, info })
|
|
5383
|
+
}
|
|
5384
|
+
if (candidates.length === 0) return { changed: false, errors: [] }
|
|
5385
|
+
|
|
5386
|
+
/** @type {Array<{ index: number, name: string, newName: string, newTag: string | null }>} */
|
|
5387
|
+
const conversions = []
|
|
5388
|
+
/** @type {string[]} */
|
|
5389
|
+
const errors = []
|
|
5390
|
+
|
|
5391
|
+
for (const { index, info } of candidates) {
|
|
5392
|
+
/** @type {Set<string>} */
|
|
5393
|
+
const visited = new Set()
|
|
5394
|
+
const baseImage = await walkKustomizationForDeploymentImage(
|
|
5395
|
+
kustAbs,
|
|
5396
|
+
rootNorm,
|
|
5397
|
+
info.deployName,
|
|
5398
|
+
info.containerIndex,
|
|
5399
|
+
visited
|
|
5400
|
+
)
|
|
5401
|
+
if (baseImage === null) {
|
|
5402
|
+
errors.push(
|
|
5403
|
+
`patches[${index}]: не знайдено Deployment ${info.deployName}.containers[${info.containerIndex}].image у дереві resources — конвертацію патча в images: пропущено (k8s.mdc)`
|
|
5404
|
+
)
|
|
5405
|
+
continue
|
|
5406
|
+
}
|
|
5407
|
+
const baseSplit = splitImageNameTagDigest(baseImage)
|
|
5408
|
+
if (baseSplit.hasDigest) {
|
|
5409
|
+
errors.push(
|
|
5410
|
+
`patches[${index}]: base image для ${info.deployName} містить digest (${baseImage}) — автоконвертацію патча пропущено (k8s.mdc)`
|
|
5411
|
+
)
|
|
5412
|
+
continue
|
|
5413
|
+
}
|
|
5414
|
+
const newSplit = splitImageNameTagDigest(info.newImage)
|
|
5415
|
+
if (newSplit.hasDigest) {
|
|
5416
|
+
errors.push(
|
|
5417
|
+
`patches[${index}]: значення патча для ${info.deployName} містить digest (${info.newImage}) — автоконвертацію пропущено (k8s.mdc)`
|
|
5418
|
+
)
|
|
5419
|
+
continue
|
|
5420
|
+
}
|
|
5421
|
+
const finalNewTag = newSplit.tag !== null && newSplit.tag !== baseSplit.tag ? newSplit.tag : null
|
|
5422
|
+
conversions.push({
|
|
5423
|
+
index,
|
|
5424
|
+
name: baseSplit.name,
|
|
5425
|
+
newName: newSplit.name,
|
|
5426
|
+
newTag: finalNewTag
|
|
5427
|
+
})
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
if (conversions.length === 0) return { changed: false, errors }
|
|
5431
|
+
|
|
5432
|
+
const patchesNode = doc.get('patches', true)
|
|
5433
|
+
if (!isSeq(patchesNode)) return { changed: false, errors }
|
|
5434
|
+
|
|
5435
|
+
const removeIdx = new Set(conversions.map(c => c.index))
|
|
5436
|
+
for (let i = patchesNode.items.length - 1; i >= 0; i--) {
|
|
5437
|
+
if (removeIdx.has(i)) patchesNode.delete(i)
|
|
5438
|
+
}
|
|
5439
|
+
if (patchesNode.items.length === 0) {
|
|
5440
|
+
doc.delete('patches')
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
let imagesNode = doc.get('images', true)
|
|
5444
|
+
if (!isSeq(imagesNode)) {
|
|
5445
|
+
imagesNode = doc.createNode([])
|
|
5446
|
+
doc.set('images', imagesNode)
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
for (const { name, newName, newTag } of conversions) {
|
|
5450
|
+
const entry = newTag === null ? { name, newName } : { name, newName, newTag }
|
|
5451
|
+
imagesNode.add(doc.createNode(entry))
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
const content = doc.toString()
|
|
5455
|
+
if (content === raw) return { changed: false, errors }
|
|
5456
|
+
return { changed: true, content, errors }
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
/**
|
|
5460
|
+
* Прохід для всіх `kustomization.yaml`: конвертує image-replace patches у `images:`,
|
|
5461
|
+
* потім чистить `images:` (зрізає теги в `name`, видаляє надлишкові `newTag`).
|
|
5462
|
+
* @param {string} root корінь репо
|
|
5463
|
+
* @param {string[]} yamlFilesAbs всі yaml під k8s
|
|
5464
|
+
* @param {(msg: string) => void} fail колбек повідомлення про помилку
|
|
5465
|
+
* @param {(msg: string) => void} pass колбек успішного повідомлення
|
|
5466
|
+
* @returns {Promise<void>}
|
|
5467
|
+
*/
|
|
5468
|
+
async function autofixKustomizationImagesYaml(root, yamlFilesAbs, fail, pass) {
|
|
5469
|
+
const rootNorm = resolve(root)
|
|
5470
|
+
const kusts = yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')
|
|
5471
|
+
for (const kustAbs of kusts) {
|
|
5472
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
5473
|
+
try {
|
|
5474
|
+
const r = await convertImagePatchesToImagesInKustomization(kustAbs, rootNorm)
|
|
5475
|
+
for (const err of r.errors) fail(`${rel}: ${err}`)
|
|
5476
|
+
if (r.changed && r.content !== undefined) {
|
|
5477
|
+
await writeFile(kustAbs, r.content, 'utf8')
|
|
5478
|
+
pass(`${rel}: image-replace patch(es) конвертовано в images: (k8s.mdc)`)
|
|
5479
|
+
}
|
|
5480
|
+
} catch (error) {
|
|
5481
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
5482
|
+
fail(`${rel}: не вдалося конвертувати image-replace patches → images: (${msg})`)
|
|
5483
|
+
}
|
|
5484
|
+
try {
|
|
5485
|
+
const raw = await readFile(kustAbs, 'utf8')
|
|
5486
|
+
const r = cleanupKustomizationImagesInYamlText(raw)
|
|
5487
|
+
if (r.changed) {
|
|
5488
|
+
await writeFile(kustAbs, r.content, 'utf8')
|
|
5489
|
+
pass(`${rel}: images: cleanup — зрізано :tag з name й видалено надлишкове newTag (k8s.mdc)`)
|
|
5490
|
+
}
|
|
5491
|
+
} catch (error) {
|
|
5492
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
5493
|
+
fail(`${rel}: не вдалося очистити images: (${msg})`)
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5043
5498
|
/**
|
|
5044
5499
|
* Перевіряє відповідність проєкту правилам k8s.mdc.
|
|
5045
5500
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -5063,6 +5518,8 @@ export async function check() {
|
|
|
5063
5518
|
|
|
5064
5519
|
pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
|
|
5065
5520
|
|
|
5521
|
+
await autofixKustomizationImagesYaml(root, yamlFiles, fail, pass)
|
|
5522
|
+
|
|
5066
5523
|
assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
|
|
5067
5524
|
|
|
5068
5525
|
const kustomizeManagedRel = await collectKustomizeManagedRelPaths(root, yamlFiles)
|