@nitra/cursor 1.8.132 → 1.8.135

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/bin/auto-rules.md CHANGED
@@ -8,6 +8,8 @@ abie - якщо в кореневому package.json в секції "repository
8
8
 
9
9
  bun - якщо в корені проекту є package.json
10
10
 
11
+ capacitor - якщо в проекті є хоч один файл capacitor.config.json
12
+
11
13
  docker - якщо в проекті є хоч один Dockerfile
12
14
 
13
15
  ga - якщо присутня директорія .github/workflows
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Правила для проєктів Capacitor
3
+ alwaysApply: true
4
+ version: '1.0'
5
+ ---
6
+
7
+ ## Версія Capacitor
8
+
9
+ У `package.json` (у **корені** репозиторію чи **workspace**-пакеті) оголошення **`@capacitor/core`**
10
+ має вказувати діапазон, **сумісний лише з мажорною версією 8 і вище** (наприклад `^8.0.0`).
11
+ **`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні. Програма перевірки — **`check-capacitor.mjs`**
12
+ (репозиторій **@nitra/cursor**).
13
+
14
+ ## iOS: лише SPM
15
+
16
+ Нативний iOS-шар **не** повинен використовувати **CocoaPods** (наприклад файл **`Podfile`**
17
+ поза каталогом `Pods/`) — **Swift Package Manager (SPM)**. Якщо каталога **`ios/`** немає, перевірка iOS
18
+ у цьому кроці пропускається.
package/mdc/k8s.mdc CHANGED
@@ -416,6 +416,8 @@ spec:
416
416
 
417
417
  У маніфестах під **`k8s`** заборонено **`apiVersion: autoscaling/v1`** (legacy HPA з єдиною метрикою CPU). Мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**: поле **`spec.metrics`** (замість **`spec.targetCPUUtilizationPercentage`**) з **`type: Resource`** і **`target.type: Utilization`** / **`AverageUtilization`** — підтримує декілька метрик і зовнішні метрики. `check k8s` падає на будь-якому документі з **`apiVersion: autoscaling/v1`**.
418
418
 
419
+ Ресурси **batch** (наприклад **CronJob**, **Job**): застаріле **`apiVersion: batch/v1beta1`** у файлах під **`k8s` під час `check k8s` переписується** на **`apiVersion: batch/v1`**.
420
+
419
421
  ```yaml
420
422
  # yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
421
423
  apiVersion: autoscaling/v2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.132",
3
+ "version": "1.8.135",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,6 +21,7 @@ import {
21
21
  export const AUTO_RULE_ORDER = Object.freeze([
22
22
  'abie',
23
23
  'bun',
24
+ 'capacitor',
24
25
  'docker',
25
26
  'ga',
26
27
  'graphql',
@@ -119,6 +120,7 @@ function updateDirFacts(dirName, facts) {
119
120
  * @param {string} fileName базове імʼя файлу
120
121
  * @param {string} relPath шлях відносно кореня
121
122
  * @param {{
123
+ * hasCapacitorConfig: boolean,
122
124
  * hasDockerfile: boolean,
123
125
  * hasJsLikeSource: boolean,
124
126
  * hasNginxDefaultTplFile: boolean,
@@ -129,6 +131,9 @@ function updateDirFacts(dirName, facts) {
129
131
  * @returns {void}
130
132
  */
131
133
  function updateFileFacts(fileName, relPath, facts) {
134
+ if (fileName === 'capacitor.config.json') {
135
+ facts.hasCapacitorConfig = true
136
+ }
132
137
  if (fileName === 'Dockerfile' || fileName.startsWith('Dockerfile.')) {
133
138
  facts.hasDockerfile = true
134
139
  }
@@ -182,6 +187,7 @@ async function updateGqlFactFromFile(absPath, relPath, facts) {
182
187
  * @param {string} absPath абсолютний шлях до файлу
183
188
  * @param {string} root абсолютний шлях кореня
184
189
  * @param {{
190
+ * hasCapacitorConfig: boolean,
185
191
  * hasDockerfile: boolean,
186
192
  * hasGqlTaggedTemplates: boolean,
187
193
  * hasJsLikeSource: boolean,
@@ -261,6 +267,7 @@ export function isMonorepoPackage(packageJson) {
261
267
  * Обходить дерево проєкту, збираючи факти для автоувімкнення правил.
262
268
  * @param {string} root абсолютний шлях кореня репозиторію
263
269
  * @returns {Promise<{
270
+ * hasCapacitorConfig: boolean,
264
271
  * hasDockerfile: boolean,
265
272
  * hasGaWorkflowsDir: boolean,
266
273
  * hasGqlTaggedTemplates: boolean,
@@ -275,6 +282,7 @@ export function isMonorepoPackage(packageJson) {
275
282
  */
276
283
  export async function collectAutoRuleFacts(root) {
277
284
  const facts = {
285
+ hasCapacitorConfig: false,
278
286
  hasDockerfile: false,
279
287
  hasGaWorkflowsDir: existsSync(join(root, '.github', 'workflows')),
280
288
  hasGqlTaggedTemplates: false,
@@ -386,6 +394,7 @@ export async function detectAutoRulesAndSkills({
386
394
  const autoRuleChecks = [
387
395
  { enabled: isAbie, id: 'abie' },
388
396
  { enabled: packageJsonExists, id: 'bun' },
397
+ { enabled: facts.hasCapacitorConfig, id: 'capacitor' },
389
398
  { enabled: facts.hasDockerfile, id: 'docker' },
390
399
  { enabled: facts.hasGaWorkflowsDir, id: 'ga' },
391
400
  { enabled: facts.hasGqlTaggedTemplates, id: 'graphql' },
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Перевіряє відповідність проєкту правилам capacitor.mdc для застосунків **Capacitor**.
3
+ *
4
+ * Якщо у репозиторії **немає** ознак Capacitor (див. наведене) — вихід **0**, перевірка не застосовується.
5
+ *
6
+ * **Ознака Capacitor:** наявні **`capacitor.config.json`**, **`capacitor.config.ts`**, **`capacitor.config.mjs`**
7
+ * (у корені) **або** у будь-якому `package.json` (рекурсивно, з пропуском типових каталогів) оголошено
8
+ * хоча б одну залежність **`@capacitor/…`** (у **`dependencies`**, **`devDependencies`**, опційно
9
+ * **`optionalDependencies`**, **`peerDependencies`**).
10
+ *
11
+ * **Версія мінімум 8:** у кожному `package.json`, де вказано **`@capacitor/core`**, діапазон версії
12
+ * мусить допускати лише **Capacitor 8+** (оцінка мінімального **major** з рядка діапазону npm, зокрема
13
+ * `||` і діапазонів через `-` у спрощеному вигляді). **`*`**, **latest** та нерозпізнані випадки — **порушення**:
14
+ * варто задати явний діапазон, наприклад **`^8.0.0`**. Якщо оголошено `capacitor.config.*` без жодного
15
+ * **`@capacitor/core`** у дереві `package.json` — також помилка.
16
+ *
17
+ * **iOS лише через SPM (Swift Package Manager):** якщо в корні є каталог **`ios/`** — у ньому **не** має
18
+ * бути файлів **Podfile** (CocoaPods) **поза** каталогом **Pods** (тобто не використовувати **Podfile**
19
+ * у вихідному iOS-шарі; присутній **Podfile** — порушення). Якщо **немає** `ios/` — вимогу iOS у цьому
20
+ * прогоні пропущено.
21
+ */
22
+ import { existsSync } from 'node:fs'
23
+ import { readdir, readFile } from 'node:fs/promises'
24
+ import { join, relative } from 'node:path'
25
+
26
+ import { createCheckReporter } from './utils/check-reporter.mjs'
27
+
28
+ /** Мінімальна допустима мажорна версія Capacitor (capacitor.mdc) */
29
+ const MIN_CAPACITOR_MAJOR = 8
30
+
31
+ /** @type {Set<string>} */
32
+ const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
33
+ 'node_modules',
34
+ '.git',
35
+ 'dist',
36
+ 'coverage',
37
+ 'Pods',
38
+ '.turbo',
39
+ '.next',
40
+ 'build'
41
+ ])
42
+
43
+ /** `||` у діапазоні npm-версій */
44
+ const NPM_OR_PARTS_RE = /\s*\|\|\s*/
45
+
46
+ /** `a - b` (діапазон діапазонів) */
47
+ const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
48
+
49
+ const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
50
+
51
+ const PREFIX_GEQ_RE = /^>=\s*/u
52
+ const PREFIX_GT_RE = /^>\s*/u
53
+ const STRIP_CARET_TILDE_EQ_RE = /^[=^~]+\s*/u
54
+
55
+ /**
56
+ * Мінімальний **major** (нижня межа) для **однієї** OR-частини діапазону npm (без `||` всередині).
57
+ * @param {string} segment одна частина після `||` або весь рядок
58
+ * @returns {number | null} null, якщо **`*` / `x` / `latest`**, або **major** **нижньої** межі
59
+ */
60
+ export function capacitorSegmentMinMajor(segment) {
61
+ if (typeof segment !== 'string') {
62
+ return null
63
+ }
64
+ const s0 = segment.trim()
65
+ if (!s0) {
66
+ return null
67
+ }
68
+ const low = s0.toLowerCase()
69
+ if (s0 === '*' || low === 'x' || low === 'latest') {
70
+ return null
71
+ }
72
+ if (s0.startsWith('<') || s0.startsWith('<=')) {
73
+ return 0
74
+ }
75
+ if (s0.startsWith('>') && !s0.startsWith('>=')) {
76
+ return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GT_RE, ''))
77
+ }
78
+ const rangeHyphen = s0.match(NPM_HYPHEN_RANGE_RE)
79
+ if (rangeHyphen) {
80
+ return firstVersionMajorFromNpmValue(rangeHyphen[1].trim())
81
+ }
82
+ if (s0.startsWith('^') || s0.startsWith('~') || s0.startsWith('=')) {
83
+ return firstVersionMajorFromNpmValue(s0.replace(STRIP_CARET_TILDE_EQ_RE, ''))
84
+ }
85
+ if (s0.startsWith('>=')) {
86
+ return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GEQ_RE, ''))
87
+ }
88
+ return firstVersionMajorFromNpmValue(s0)
89
+ }
90
+
91
+ /**
92
+ * Витягує **major** з першого числа у вигляді **X** або **X.Y** / **X.Y.Z** (опційно **v**).
93
+ * @param {string} t рядок ділянки **версії** (без префікса **операторів**)
94
+ * @returns {number | null} перше **ціле** (major) або **null**
95
+ */
96
+ function firstVersionMajorFromNpmValue(t) {
97
+ const s = t.trim()
98
+ if (!s) {
99
+ return null
100
+ }
101
+ const m = s.match(FIRST_VERSION_NUM_RE)
102
+ if (!m) {
103
+ return null
104
+ }
105
+ return Number.parseInt(m[1], 10)
106
+ }
107
+
108
+ /**
109
+ * Мінімальна можлива (нижня) **major**-версія для повного діапазону npm, у т. ч. з `||`.
110
+ * @param {string} versionRange повне поле `package.json` для **@capacitor/core**
111
+ * @returns {number | null} **null** якщо **`*` / latest** в одній з частин
112
+ */
113
+ export function capacitorVersionRangeMinMajor(versionRange) {
114
+ if (typeof versionRange !== 'string') {
115
+ return null
116
+ }
117
+ const parts = versionRange.split(NPM_OR_PARTS_RE)
118
+ let overallMin = /** @type {number | null} */ (null)
119
+ for (const p of parts) {
120
+ const m = capacitorSegmentMinMajor(p)
121
+ if (m === null) {
122
+ return null
123
+ }
124
+ if (overallMin === null || m < overallMin) {
125
+ overallMin = m
126
+ }
127
+ }
128
+ return overallMin
129
+ }
130
+
131
+ /**
132
+ * @param {string} versionRange рядок **версії** з `package.json`
133
+ * @param {number} [min] мінімальний **major** (за замовчуванням `MIN_CAPACITOR_MAJOR`)
134
+ * @returns {boolean} **true**, якщо нижня межа **≥** **min**
135
+ */
136
+ export function isCapacitorCoreVersionAtLeast8(versionRange, min = MIN_CAPACITOR_MAJOR) {
137
+ const low = capacitorVersionRangeMinMajor(versionRange)
138
+ if (low === null) {
139
+ return false
140
+ }
141
+ return low >= min
142
+ }
143
+
144
+ /**
145
+ * @param {(m: string) => void} fail друк помилки
146
+ * @param {(m: string) => void} pass друк успіху
147
+ * @param {string} rel відносний **posix**-шлях `package.json`
148
+ * @param {string} range поле `version` для **@capacitor/core**
149
+ * @returns {void}
150
+ */
151
+ function reportOneCapacitorCoreRange(fail, pass, rel, range) {
152
+ if (isCapacitorCoreVersionAtLeast8(range)) {
153
+ pass(`«${rel}»: @capacitor/core — діапазон сумісний з ${MIN_CAPACITOR_MAJOR}+`)
154
+ } else {
155
+ fail(
156
+ `«${rel}»: @capacitor/core «${range}» — мінімальна допустима мажорна версія Capacitor ${MIN_CAPACITOR_MAJOR} (capacitor.mdc). Вкажи, наприклад, ^${MIN_CAPACITOR_MAJOR}.0.0`
157
+ )
158
+ }
159
+ }
160
+
161
+ /**
162
+ * @param {string} absPath шлях до `package.json`
163
+ * @param {string} root корінь репозиторію
164
+ * @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач **byPath** і **anyCapacitor**
165
+ * @returns {Promise<void>}
166
+ */
167
+ export async function recordCapacitorFromOnePackageJson(absPath, root, out) {
168
+ let raw
169
+ try {
170
+ raw = await readFile(absPath, 'utf8')
171
+ } catch {
172
+ return
173
+ }
174
+ let pkg
175
+ try {
176
+ pkg = JSON.parse(raw)
177
+ } catch {
178
+ return
179
+ }
180
+ const rel = (relative(root, absPath) || absPath).replaceAll('\\', '/')
181
+ for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
182
+ const rec = pkg?.[block]
183
+ if (rec !== null && rec !== undefined && typeof rec === 'object' && !Array.isArray(rec)) {
184
+ const obj = /** @type {Record<string, unknown>} */ (rec)
185
+ for (const [name, val] of Object.entries(obj)) {
186
+ if (typeof name === 'string' && name.startsWith('@capacitor/')) {
187
+ out.anyCapacitor = true
188
+ }
189
+ if (name === '@capacitor/core' && typeof val === 'string' && val !== '') {
190
+ out.byPath.set(rel, val)
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Зчитує всі `package.json` з дерева, накопичує `byPath` і `anyCapacitor`.
199
+ * @param {string} root корінь репозиторію
200
+ * @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач
201
+ * @returns {Promise<void>}
202
+ */
203
+ export async function collectCapacitorDataFromAllPackageJson(root, out) {
204
+ out.anyCapacitor = false
205
+ if (out.byPath) {
206
+ out.byPath.clear()
207
+ } else {
208
+ out.byPath = new Map()
209
+ }
210
+
211
+ /**
212
+ * @param {string} dir абсолютний **каталог** для `readdir`
213
+ * @returns {Promise<void>}
214
+ */
215
+ async function walk(dir) {
216
+ let entries
217
+ try {
218
+ entries = await readdir(dir, { withFileTypes: true })
219
+ } catch {
220
+ return
221
+ }
222
+ for (const entry of entries) {
223
+ const absPath = join(dir, entry.name)
224
+ if (entry.isDirectory() && !IGNORED_DIRS_FOR_PACKAGE_JSON.has(entry.name)) {
225
+ await walk(absPath)
226
+ } else if (entry.isFile() && entry.name === 'package.json') {
227
+ await recordCapacitorFromOnePackageJson(absPath, root, out)
228
+ }
229
+ }
230
+ }
231
+
232
+ await walk(root)
233
+ }
234
+
235
+ /**
236
+ * @param {string} root абсолютний або **cwd**-відносний **корінь** репозиторію
237
+ * @returns {boolean} **true** якщо `capacitor.config.{json,ts,mjs}` існує
238
+ */
239
+ export function hasCapacitorConfigInRoot(root) {
240
+ return (
241
+ existsSync(join(root, 'capacitor.config.json')) ||
242
+ existsSync(join(root, 'capacitor.config.ts')) ||
243
+ existsSync(join(root, 'capacitor.config.mjs'))
244
+ )
245
+ }
246
+
247
+ /**
248
+ * Чи варто застосовувати правила: конфіг **або** **@capacitor/** у залежностях.
249
+ * @param {string} root корінь
250
+ * @param {boolean} anyCapacitor чи зустрілось **@capacitor/** у **package.json**
251
+ * @returns {boolean} **true** якщо застосовуємо **check capacitor**
252
+ */
253
+ export function isCapacitorRelevantForCheck(root, anyCapacitor) {
254
+ return hasCapacitorConfigInRoot(root) || anyCapacitor
255
+ }
256
+
257
+ /**
258
+ * Рекурсивно шукає `Podfile` у **ios/**, **не** заходячи в **Pods** (кеш CocoaPods) і типові build-каталоги.
259
+ * @param {string} root корінь репозиторію
260
+ * @param {string} dir абсолютний каталог
261
+ * @param {(rel: string) => void} onPodfileRelative **callback** з **posix**-шляхом `Podfile` від **root**
262
+ * @returns {Promise<boolean>} **true** — знайдено **хоча б один** `Podfile`
263
+ */
264
+ export async function walkIosForPodfileSkipPods(root, dir, onPodfileRelative) {
265
+ let entries
266
+ try {
267
+ entries = await readdir(dir, { withFileTypes: true })
268
+ } catch {
269
+ return false
270
+ }
271
+ for (const e of entries) {
272
+ if (e.name !== 'Pods' && e.name !== 'build' && e.name !== 'DerivedData') {
273
+ const abs = join(dir, e.name)
274
+ if (e.isFile() && e.name === 'Podfile') {
275
+ onPodfileRelative((relative(root, abs) || abs).replaceAll('\\', '/'))
276
+ return true
277
+ }
278
+ if (e.isDirectory()) {
279
+ const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
280
+ if (found) {
281
+ return true
282
+ }
283
+ }
284
+ }
285
+ }
286
+ return false
287
+ }
288
+
289
+ /**
290
+ * @param {string} root корінь
291
+ * @returns {Promise<string | null>} **relative**-шлях `Podfile` (або **null**)
292
+ */
293
+ export async function findFirstPodfileUnderIosExcludingPods(root) {
294
+ const iosDir = join(root, 'ios')
295
+ if (!existsSync(iosDir)) {
296
+ return null
297
+ }
298
+ let first = /** @type {string | null} */ (null)
299
+ await walkIosForPodfileSkipPods(root, iosDir, rel => {
300
+ if (first === null || rel.length < first.length) {
301
+ first = rel
302
+ }
303
+ })
304
+ return first
305
+ }
306
+
307
+ /**
308
+ * @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
309
+ */
310
+ export async function check() {
311
+ const reporter = createCheckReporter()
312
+ const { pass, fail, getExitCode } = reporter
313
+ const root = process.cwd()
314
+
315
+ const acc = { byPath: new Map(), anyCapacitor: false }
316
+ await collectCapacitorDataFromAllPackageJson(root, acc)
317
+ const { byPath, anyCapacitor } = acc
318
+
319
+ if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
320
+ pass('Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено')
321
+ return getExitCode()
322
+ }
323
+
324
+ pass('Проєкт з ознаками Capacitor — застосовую capacitor.mdc')
325
+
326
+ if (byPath.size === 0) {
327
+ fail(
328
+ `додай залежність @capacitor/core з діапазоном ^${MIN_CAPACITOR_MAJOR}.0.0 (або іншим, сумісним лише з ${MIN_CAPACITOR_MAJOR}+) у package.json (capacitor.mdc)`
329
+ )
330
+ } else {
331
+ for (const [rel, range] of byPath) {
332
+ reportOneCapacitorCoreRange(fail, pass, rel, range)
333
+ }
334
+ }
335
+
336
+ const podfileRel = await findFirstPodfileUnderIosExcludingPods(root)
337
+ if (podfileRel === null) {
338
+ if (existsSync(join(root, 'ios'))) {
339
+ pass('ios/ без Podfile поза Pods/ (лише SPM, capacitor.mdc)')
340
+ } else {
341
+ pass('каталог ios/ не знайдено — вимогу iOS/SPM пропущено')
342
+ }
343
+ } else {
344
+ fail(
345
+ `iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods (прибери Podfile, capacitor.mdc)`
346
+ )
347
+ }
348
+
349
+ return getExitCode()
350
+ }
@@ -23,6 +23,8 @@
23
23
  *
24
24
  * **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
25
25
  * **`apiVersion: autoscaling/v1`** заборонено (мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**).
26
+ * Рядок **`apiVersion: batch/v1beta1`** (CronJob, Job) **автоматично** переписується на **`apiVersion: batch/v1`**
27
+ * (окрім рядків-коментарів і рядків, де після значення йде наприклад `# …`).
26
28
  *
27
29
  * Файли під **`k8s`**, де всі YAML-документи — лише **`kind: BackendConfig`**, **видаляються** автоматично.
28
30
  * Якщо **BackendConfig** змішано з іншими ресурсами в одному файлі — перевірка завершується помилкою (розділи маніфести).
@@ -93,7 +95,7 @@
93
95
  * не з’явиться `Deployment` (k8s.mdc).
94
96
  */
95
97
  import { existsSync } from 'node:fs'
96
- import { readFile, readdir, stat, unlink } from 'node:fs/promises'
98
+ import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
97
99
  import { basename, dirname, join, relative, resolve } from 'node:path'
98
100
 
99
101
  import { parseAllDocuments } from 'yaml'
@@ -1561,6 +1563,73 @@ async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
1561
1563
  }
1562
1564
  }
1563
1565
 
1566
+ /**
1567
+ * Один рядок YAML: якщо це `apiVersion` зі значенням **`batch/v1beta1`**, повертає той самий рядок із **`batch/v1`**
1568
+ * (з тими самими відступами/пробілами після `apiVersion:`, крім випадків з лапками — нормалізується до `apiVersion: batch/v1`).
1569
+ * Рядки, що після trim починаються з `#`, не змінюються.
1570
+ * @param {string} line
1571
+ * @returns {string}
1572
+ */
1573
+ function rewriteLineBatchV1beta1ApiVersion(line) {
1574
+ const t = line.trimStart()
1575
+ if (t.startsWith('#')) {
1576
+ return line
1577
+ }
1578
+ const m = line.match(/^(\s*apiVersion:\s*)(?:"|')?batch\/v1beta1(?:"|')?(\s*)$/)
1579
+ if (m) {
1580
+ return `${m[1]}batch/v1${m[2]}`
1581
+ }
1582
+ return line
1583
+ }
1584
+
1585
+ /**
1586
+ * У повному тексті YAML замінює всі **цілі** рядки `apiVersion: batch/v1beta1` (за потреби в лапках) на `apiVersion: batch/v1`.
1587
+ * Зберігає **CRLF** / **LF** як у вихідному рядку.
1588
+ * @param {string} raw вміст файлу
1589
+ * @returns {{ changed: boolean, content: string }}
1590
+ */
1591
+ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
1592
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n'
1593
+ const lines = raw.split(/\r?\n/)
1594
+ let changed = false
1595
+ const out = lines.map(line => {
1596
+ const n = rewriteLineBatchV1beta1ApiVersion(line)
1597
+ if (n !== line) {
1598
+ changed = true
1599
+ }
1600
+ return n
1601
+ })
1602
+ if (!changed) {
1603
+ return { changed: false, content: raw }
1604
+ }
1605
+ return { changed: true, content: out.join(eol) }
1606
+ }
1607
+
1608
+ /**
1609
+ * Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
1610
+ * @param {string} root корінь репозиторію
1611
+ * @param {(msg: string) => void} fail
1612
+ * @param {(msg: string) => void} pass
1613
+ * @returns {Promise<void>}
1614
+ */
1615
+ async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
1616
+ const yamlFiles = await findK8sYamlFiles(root)
1617
+ for (const abs of yamlFiles) {
1618
+ const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
1619
+ try {
1620
+ const raw = await readFile(abs, 'utf8')
1621
+ const { changed, content } = replaceBatchV1beta1ApiVersionInYamlText(raw)
1622
+ if (changed) {
1623
+ await writeFile(abs, content, 'utf8')
1624
+ pass(`${rel}: оновлено apiVersion batch/v1beta1 → batch/v1 (k8s.mdc)`)
1625
+ }
1626
+ } catch (error) {
1627
+ const msg = error instanceof Error ? error.message : String(error)
1628
+ fail(`${rel}: не вдалося прочитати/записати при заміні batch/v1beta1 → batch/v1 (${msg})`)
1629
+ }
1630
+ }
1631
+ }
1632
+
1564
1633
  /**
1565
1634
  * Прибирає BOM і ділить на рядки.
1566
1635
  * @param {string} content вміст файлу
@@ -4819,6 +4888,8 @@ export async function check() {
4819
4888
 
4820
4889
  const root = process.cwd()
4821
4890
 
4891
+ await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass)
4892
+
4822
4893
  await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
4823
4894
 
4824
4895
  const yamlFiles = await findK8sYamlFiles(root)