@nitra/cursor 1.8.220 → 1.8.222
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/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +21 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +25 -4
- package/mdc/ci4.mdc +51 -0
- package/mdc/tauri.mdc +20 -0
- package/package.json +1 -1
- package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
- package/scripts/auto-skills.mjs +8 -1
- package/scripts/check-bun.mjs +3 -3
- package/scripts/check-changelog.mjs +2 -3
- package/scripts/check-image-avif.mjs +14 -6
- package/scripts/check-image-compress.mjs +1 -1
- package/scripts/check-js-run.mjs +58 -47
- package/scripts/check-k8s.mjs +128 -51
- package/scripts/check-npm-module.mjs +1 -4
- package/scripts/check-php.mjs +5 -5
- package/scripts/claude-stop-hook.mjs +2 -2
- package/scripts/lint-conftest.mjs +88 -8
- package/scripts/lint-ga.mjs +1 -1
- package/scripts/lint-rego.mjs +19 -4
- package/scripts/run-shellcheck-text.mjs +94 -64
- package/scripts/sync-claude-config.mjs +1 -1
- package/scripts/utils/ast-scan-utils.mjs +28 -0
- package/scripts/utils/bun-sql-scan.mjs +53 -34
- package/scripts/utils/bunyan-imports.mjs +10 -61
- package/scripts/utils/conn-file-rules.mjs +76 -37
- package/scripts/utils/depcheck-workflow.mjs +27 -6
- package/scripts/utils/redis-imports.mjs +9 -51
- package/skills/llm-patch/SKILL.md +16 -5
package/scripts/auto-skills.mjs
CHANGED
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** Порядок автододавання skills відповідно до `auto-skills.md`. */
|
|
15
|
-
export const AUTO_SKILL_ORDER = Object.freeze([
|
|
15
|
+
export const AUTO_SKILL_ORDER = Object.freeze([
|
|
16
|
+
'abie-kustomize',
|
|
17
|
+
'fix',
|
|
18
|
+
'lint',
|
|
19
|
+
'llm-patch',
|
|
20
|
+
'publish-telegram',
|
|
21
|
+
'taze'
|
|
22
|
+
])
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Залежність скілів від правил (`auto-skills.md` синтаксис `skill - [rules]`).
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -104,10 +104,10 @@ export async function check() {
|
|
|
104
104
|
fail('Відсутній bun.lock — запусти bun i')
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (
|
|
108
|
-
fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
|
|
109
|
-
} else {
|
|
107
|
+
if (existsSync('bunfig.toml')) {
|
|
110
108
|
pass('bunfig.toml є (структуру перевіряє bun run lint-conftest → bun.bunfig)')
|
|
109
|
+
} else {
|
|
110
|
+
fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const cursorRules = await loadNCursorRules()
|
|
@@ -160,9 +160,8 @@ async function readBaseVersion(baseRef, ws) {
|
|
|
160
160
|
* @returns {boolean} `true`, якщо запис для `version` знайдено
|
|
161
161
|
*/
|
|
162
162
|
function changelogHasVersionEntry(text, version) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
return re.test(text)
|
|
163
|
+
const needle = `## [${version}]`
|
|
164
|
+
return text.startsWith(needle) || text.includes(`\n${needle}`)
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
/**
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* ув'язування `.avif`-двійників з посиланнями у `.vue`/`.html`.
|
|
4
4
|
*
|
|
5
5
|
* Дії під час `check image-avif`:
|
|
6
|
-
* 1. `npx
|
|
6
|
+
* 1. `npx \@nitra/minify-image --src=. --write --avif` — генерує AVIF-двійники.
|
|
7
7
|
* 2. У кожному workspace-пакеті переписує raster-посилання у `.vue`/`.html` на `.avif`
|
|
8
|
-
* (де AVIF-двійник реально існує на диску). Pakety з `"
|
|
8
|
+
* (де AVIF-двійник реально існує на диску). Pakety з `"\@nitra/minify-image": {
|
|
9
9
|
* "disable-avif": true }` у `package.json` пропускаються.
|
|
10
10
|
* 3. Прибирає AVIF-сироти — `<name>.<ext>.avif`, на які не лишилось жодного посилання
|
|
11
11
|
* у `.vue`/`.html` репозиторію, видаляються (умова правила: «AVIF лишається лише
|
|
@@ -27,13 +27,14 @@ import { env } from 'node:process'
|
|
|
27
27
|
|
|
28
28
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
29
29
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
30
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
30
31
|
import { walkDir } from './utils/walkDir.mjs'
|
|
31
32
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
32
33
|
|
|
33
34
|
/** Імʼя CLI-пакета, який генерує AVIF. */
|
|
34
35
|
const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
|
|
35
36
|
|
|
36
|
-
/** Поле в `package.json` для конфігу
|
|
37
|
+
/** Поле в `package.json` для конфігу `\@nitra/minify-image` (наприклад, `disable-avif`). */
|
|
37
38
|
const PKG_CONFIG_FIELD = '@nitra/minify-image'
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -279,7 +280,7 @@ async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail)
|
|
|
279
280
|
|
|
280
281
|
/**
|
|
281
282
|
* Чи є в репозиторії хоч один raster-файл, який мав би сенс конвертувати у AVIF.
|
|
282
|
-
* Якщо немає — `npx
|
|
283
|
+
* Якщо немає — `npx \@nitra/minify-image` нема що робити, тож зайвий запуск пропускаємо
|
|
283
284
|
* (важливо у тестах: фікстурні `.png`-імпорти посилаються на неіснуючі файли, тож
|
|
284
285
|
* minify-image все одно нічого не згенерує — а зайвий npx-спавн повільний і робить шум).
|
|
285
286
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
@@ -299,7 +300,7 @@ async function hasAnyRasterImage(ignorePaths) {
|
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
/**
|
|
302
|
-
* Запускає `npx
|
|
303
|
+
* Запускає `npx \@nitra/minify-image --src=. --write --avif` для генерації AVIF-двійників.
|
|
303
304
|
*
|
|
304
305
|
* Виклик best-effort: якщо мережа/кеш недоступні чи бінарника нема — лог-варн без падіння
|
|
305
306
|
* перевірки (валідації package.json і vue-refs все одно прогоняться, vue-refs на
|
|
@@ -309,7 +310,14 @@ async function hasAnyRasterImage(ignorePaths) {
|
|
|
309
310
|
*/
|
|
310
311
|
function runAvifGeneration() {
|
|
311
312
|
if (env.NITRA_CURSOR_NO_AVIF_RUN === '1') return
|
|
312
|
-
const
|
|
313
|
+
const npxPath = resolveCmd('npx')
|
|
314
|
+
if (!npxPath) {
|
|
315
|
+
console.log(
|
|
316
|
+
` ⚠️ 'npx' не знайдено в PATH — пропускаємо генерацію AVIF; vue/html-перевірка покаже файли, для яких не вистачає .avif`
|
|
317
|
+
)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
const result = spawnSync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--write', '--avif'], {
|
|
313
321
|
stdio: 'inherit',
|
|
314
322
|
env
|
|
315
323
|
})
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **Що покрила Rego** (`bun run lint-conftest`,
|
|
13
13
|
* `npm/policy/image_compress/package_json/`):
|
|
14
|
-
* - `scripts.lint-image` викликає `npx
|
|
14
|
+
* - `scripts.lint-image` викликає `npx \@nitra/minify-image --src=. --write`
|
|
15
15
|
* без `--avif` (AVIF — окреме правило `image-avif`);
|
|
16
16
|
* - агрегований `lint` (якщо є) містить `bun run lint-image`;
|
|
17
17
|
* - `@nitra/minify-image` НЕ у `dependencies` / `devDependencies` (через `npx`).
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -207,35 +207,56 @@ async function checkConnFileNamingAndExports(absPackageRoot, sourcePaths, pkgJso
|
|
|
207
207
|
let violations = 0
|
|
208
208
|
for (const absPath of sourcePaths) {
|
|
209
209
|
const rel = relPosix(absPackageRoot, absPath)
|
|
210
|
-
if (!
|
|
211
|
-
if (!isConnFileRulesSourceFile(rel)) continue
|
|
212
|
-
// пропускаємо реекспортний барель `index.*` (якщо знадобиться) і прихований .d.ts
|
|
213
|
-
const base = rel.slice(rel.lastIndexOf('/') + 1)
|
|
214
|
-
if (base.startsWith('index.')) continue
|
|
215
|
-
|
|
210
|
+
if (!isConnFileToCheck(rel, connDir)) continue
|
|
216
211
|
const content = await readFile(absPath, 'utf8')
|
|
217
212
|
for (const v of findConnFileRuleViolations(content, rel)) {
|
|
218
213
|
violations++
|
|
219
|
-
|
|
220
|
-
fail(
|
|
221
|
-
`${label}${rel} — назва файла в '${connDir}/' не відповідає канону js-run: ` +
|
|
222
|
-
`'ql-<id>', 'pg-{read|write}[-<id>]', 'mysql-{read|write}[-<id>]' або 'mssql-{read|write}[-<id>]' ` +
|
|
223
|
-
`(kebab-case, [a-z0-9-])`
|
|
224
|
-
)
|
|
225
|
-
} else if (v.kind === 'default-export') {
|
|
226
|
-
fail(`${label}${rel} — 'export default' заборонений у '${connDir}/'; зроби іменований експорт`)
|
|
227
|
-
} else {
|
|
228
|
-
const found = v.foundNames?.length ? v.foundNames.join(', ') : '—'
|
|
229
|
-
fail(
|
|
230
|
-
`${label}${rel} — очікується іменований експорт 'export const ${v.expectedName} = …' ` +
|
|
231
|
-
`(camelCase від назви файла); знайдено: ${found}`
|
|
232
|
-
)
|
|
233
|
-
}
|
|
214
|
+
fail(formatConnFileViolation(v, label, rel, connDir))
|
|
234
215
|
}
|
|
235
216
|
}
|
|
236
217
|
return violations
|
|
237
218
|
}
|
|
238
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Чи `rel` — це conn-файл, який треба валідувати: під `connDir/`, з JS/TS-розширенням,
|
|
222
|
+
* не `index.*` (який є реекспортним барелем).
|
|
223
|
+
* @param {string} rel відносний шлях у posix-форматі
|
|
224
|
+
* @param {string} connDir каталог conn-файлів (наприклад `src/conn`)
|
|
225
|
+
* @returns {boolean} true, якщо файл потрібно перевірити
|
|
226
|
+
*/
|
|
227
|
+
function isConnFileToCheck(rel, connDir) {
|
|
228
|
+
if (!isInsideConnDir(rel, connDir)) return false
|
|
229
|
+
if (!isConnFileRulesSourceFile(rel)) return false
|
|
230
|
+
const base = rel.slice(rel.lastIndexOf('/') + 1)
|
|
231
|
+
return !base.startsWith('index.')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Будує повідомлення про конкретне порушення canon-у файла з `connDir/`.
|
|
236
|
+
* @param {{ kind: 'name' | 'default-export' | 'export-name', expectedName?: string, foundNames?: string[] }} v опис порушення
|
|
237
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
238
|
+
* @param {string} rel відносний шлях файла
|
|
239
|
+
* @param {string} connDir каталог conn-файлів
|
|
240
|
+
* @returns {string} повний текст повідомлення для `fail(...)`
|
|
241
|
+
*/
|
|
242
|
+
function formatConnFileViolation(v, label, rel, connDir) {
|
|
243
|
+
if (v.kind === 'name') {
|
|
244
|
+
return (
|
|
245
|
+
`${label}${rel} — назва файла в '${connDir}/' не відповідає канону js-run: ` +
|
|
246
|
+
`'ql-<id>', 'pg-{read|write}[-<id>]', 'mysql-{read|write}[-<id>]' або 'mssql-{read|write}[-<id>]' ` +
|
|
247
|
+
`(kebab-case, [a-z0-9-])`
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
if (v.kind === 'default-export') {
|
|
251
|
+
return `${label}${rel} — 'export default' заборонений у '${connDir}/'; зроби іменований експорт`
|
|
252
|
+
}
|
|
253
|
+
const found = v.foundNames?.length ? v.foundNames.join(', ') : '—'
|
|
254
|
+
return (
|
|
255
|
+
`${label}${rel} — очікується іменований експорт 'export const ${v.expectedName} = …' ` +
|
|
256
|
+
`(camelCase від назви файла); знайдено: ${found}`
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
239
260
|
/**
|
|
240
261
|
* Перевіряє правило «CheckEnv» для пакета.
|
|
241
262
|
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
@@ -297,12 +318,12 @@ async function checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, f
|
|
|
297
318
|
async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, passFn) {
|
|
298
319
|
const label = `[${rootDir}] `
|
|
299
320
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
300
|
-
const pkgJson = await
|
|
321
|
+
const pkgJson = await loadPackageJson(rootDir)
|
|
301
322
|
|
|
302
323
|
// Frontend-пакети (vite у devDependencies) виходять за межі js-run:
|
|
303
324
|
// браузерний бандл не має `node:process`, а `process.env.*` бандлер
|
|
304
|
-
// обробляє самостійно. Перевірку process.env / conn-аліасів
|
|
305
|
-
// bunyan-залежність
|
|
325
|
+
// обробляє самостійно. Перевірку process.env / conn-аліасів пропускаємо;
|
|
326
|
+
// bunyan-залежність валідується в Rego (`bun run lint-conftest`).
|
|
306
327
|
if (packageJsonHasViteDevDependency(pkgJson)) {
|
|
307
328
|
passFn(`${label}vite-пакет (frontend) — js-run пропущено (process.env / conn-aliases / OTEL configmap)`)
|
|
308
329
|
return
|
|
@@ -343,7 +364,7 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
|
|
|
343
364
|
passFn(`${label}немає 'new Promise(r => setTimeout(r, ms))' — паузи через 'node:timers/promises'`)
|
|
344
365
|
}
|
|
345
366
|
|
|
346
|
-
|
|
367
|
+
checkOtelConfigmap(rootDir, passFn)
|
|
347
368
|
|
|
348
369
|
checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
|
|
349
370
|
}
|
|
@@ -388,41 +409,31 @@ function packageJsonHasViteDevDependency(pkgJson) {
|
|
|
388
409
|
}
|
|
389
410
|
|
|
390
411
|
/**
|
|
391
|
-
* Завантажує `package.json` пакета (якщо є)
|
|
412
|
+
* Завантажує `package.json` пакета (якщо є). Заборону `@nitra/bunyan` / `bunyan`
|
|
413
|
+
* у dependencies/devDependencies перенесено в Rego (`npm/policy/js_run/package_json/`);
|
|
414
|
+
* `bun run lint-conftest` запускає її по всіх workspace `package.json`. Тут лишилася
|
|
415
|
+
* лише AST-перевірка імпортів.
|
|
392
416
|
* @param {string} rootDir відносний шлях workspace
|
|
393
|
-
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
394
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
395
417
|
* @returns {Promise<unknown>} розпарсений package.json або null
|
|
396
418
|
*/
|
|
397
|
-
async function
|
|
419
|
+
async function loadPackageJson(rootDir) {
|
|
398
420
|
const pkgPath = join(rootDir, 'package.json')
|
|
399
421
|
if (!existsSync(pkgPath)) return null
|
|
400
|
-
|
|
401
|
-
// Заборону `@nitra/bunyan` / `bunyan` у dependencies/devDependencies перенесено
|
|
402
|
-
// в Rego (`npm/policy/js_run/package_json/`); `bun run lint-conftest` запускає
|
|
403
|
-
// її по всіх workspace `package.json`. Тут лишилася лише AST-перевірка імпортів.
|
|
404
|
-
void label
|
|
405
|
-
void fail
|
|
406
|
-
return pkgJson
|
|
422
|
+
return JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
407
423
|
}
|
|
408
424
|
|
|
409
425
|
/**
|
|
410
|
-
* Перевіряє
|
|
411
|
-
* з обов'язковими `service.name=`
|
|
426
|
+
* Перевіряє наявність `k8s/base/configmap.yaml` пакета. Структуру (наявність
|
|
427
|
+
* `OTEL_RESOURCE_ATTRIBUTES` з обов'язковими `service.name=` / `service.namespace=`)
|
|
428
|
+
* перенесено в Rego (`npm/policy/js_run/configmap/`); `bun run lint-conftest`
|
|
429
|
+
* запускає її на всіх `k8s/base/configmap.yaml`.
|
|
412
430
|
* @param {string} rootDir відносний шлях workspace
|
|
413
|
-
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
414
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
415
431
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
416
|
-
* @returns {
|
|
432
|
+
* @returns {void}
|
|
417
433
|
*/
|
|
418
|
-
function checkOtelConfigmap(rootDir,
|
|
434
|
+
function checkOtelConfigmap(rootDir, passFn) {
|
|
419
435
|
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
420
436
|
if (!existsSync(configmapPath)) return
|
|
421
|
-
// Перевірку `OTEL_RESOURCE_ATTRIBUTES` має містити `service.name=` /
|
|
422
|
-
// `service.namespace=` перенесено в Rego (`npm/policy/js_run/configmap/`);
|
|
423
|
-
// `bun run lint-conftest` запускає її на всіх `k8s/base/configmap.yaml`.
|
|
424
|
-
void label
|
|
425
|
-
void fail
|
|
426
437
|
passFn(`${rootDir}/k8s/base/configmap.yaml є (OTEL — bun run lint-conftest → js_run.configmap)`)
|
|
427
438
|
}
|
|
428
439
|
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -325,6 +325,18 @@ const K8S_BASE_SEGMENT_RE = /(^|\/)k8s\/base\//u
|
|
|
325
325
|
const OXLINT_SCHEMA_MODELINE_RE = /^\s*#\s*yaml-language-server:\s*\$schema=\S+/u
|
|
326
326
|
const HTTPS_SCHEMA_RE = /^https:/iu
|
|
327
327
|
const HASURA_GRAPHQL_ENGINE_RE = /(^|\/)hasura\/graphql-engine(?::|$)/u
|
|
328
|
+
const BASE_CANON_MEMORY_RE = /^128Mi$/iu
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Видаляє хвостові символи `\n` зі стрічки без regex (щоб не тригерити sonarjs/slow-regex).
|
|
332
|
+
* @param {string} s стрічка YAML/тексту
|
|
333
|
+
* @returns {string} стрічка без trailing newlines
|
|
334
|
+
*/
|
|
335
|
+
function stripTrailingNewlines(s) {
|
|
336
|
+
let end = s.length
|
|
337
|
+
while (end > 0 && s.codePointAt(end - 1) === 10) end--
|
|
338
|
+
return end === s.length ? s : s.slice(0, end)
|
|
339
|
+
}
|
|
328
340
|
const BATCH_V1BETA1_API_VERSION_LINE_RE = /^(\s*apiVersion:\s*)["']?batch\/v1beta1["']?(\s*)$/u
|
|
329
341
|
|
|
330
342
|
/**
|
|
@@ -665,15 +677,25 @@ async function validateKustomizationPatchesStructuralSort(root, yamlFilesAbs, fa
|
|
|
665
677
|
if (kust === null) continue
|
|
666
678
|
const outer = kustomizationPatchesSortedViolation(kust)
|
|
667
679
|
if (outer !== null) fail(`${rel}: ${outer}`)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
680
|
+
if (!Array.isArray(kust.patches)) continue
|
|
681
|
+
validateInlinePatchesSorted(rel, kust.patches, fail)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Перевіряє, що inline-`patch:` (рядок YAML/JSON) у кожному `patches[i]` має ops у канонічному порядку
|
|
687
|
+
* (`add`/`replace` за `path`). Чужі форми (без `patch`-стрічки, з `target` без inline-блока) пропускаються.
|
|
688
|
+
* @param {string} rel відносний шлях `kustomization.yaml` для повідомлень
|
|
689
|
+
* @param {unknown[]} patches масив `kust.patches` (рекордів)
|
|
690
|
+
* @param {(msg: string) => void} fail callback при порушенні
|
|
691
|
+
*/
|
|
692
|
+
function validateInlinePatchesSorted(rel, patches, fail) {
|
|
693
|
+
for (const [i, p] of patches.entries()) {
|
|
694
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
|
|
695
|
+
const rec = /** @type {Record<string, unknown>} */ (p)
|
|
696
|
+
if (typeof rec.patch !== 'string') continue
|
|
697
|
+
const v = kustomizationInlinePatchOpsSortedViolation(rec.patch)
|
|
698
|
+
if (v !== null) fail(`${rel}: patches[${i}] (${kustomizationPatchLabel(p, i)}): ${v}`)
|
|
677
699
|
}
|
|
678
700
|
}
|
|
679
701
|
|
|
@@ -2582,7 +2604,7 @@ function isBaseCanonCpuValue(cpu) {
|
|
|
2582
2604
|
*/
|
|
2583
2605
|
function isBaseCanonMemoryValue(mem) {
|
|
2584
2606
|
if (typeof mem !== 'string' || mem.trim() === '') return false
|
|
2585
|
-
return
|
|
2607
|
+
return BASE_CANON_MEMORY_RE.test(mem.trim())
|
|
2586
2608
|
}
|
|
2587
2609
|
|
|
2588
2610
|
/**
|
|
@@ -5212,13 +5234,11 @@ function checkProdOverridesInKustomization(kust, rel, fail, passFn, needs) {
|
|
|
5212
5234
|
ok = false
|
|
5213
5235
|
}
|
|
5214
5236
|
}
|
|
5215
|
-
if (needs.needsPdbMinAvailablePatch) {
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
ok = false
|
|
5221
|
-
}
|
|
5237
|
+
if (needs.needsPdbMinAvailablePatch && !pdbPaths.has('/spec/minAvailable')) {
|
|
5238
|
+
fail(
|
|
5239
|
+
`${rel}: прод-оверлей має перевизначати spec.minAvailable для PodDisruptionBudget (мінімум 1 у проді) (k8s.mdc)`
|
|
5240
|
+
)
|
|
5241
|
+
ok = false
|
|
5222
5242
|
}
|
|
5223
5243
|
if (ok) {
|
|
5224
5244
|
passFn(`${rel}: прод-оверрайди HPA/PDB за потреби присутні (k8s.mdc)`)
|
|
@@ -5573,18 +5593,7 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
|
|
|
5573
5593
|
const isK8sBaseLayer = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
|
|
5574
5594
|
const deployRel = relDir === '' ? '.' : relDir
|
|
5575
5595
|
if (isK8sBaseLayer && deployments.length > 0) {
|
|
5576
|
-
|
|
5577
|
-
if (existsSync(hpaAbs)) {
|
|
5578
|
-
fail(
|
|
5579
|
-
`${deployRel}/${HPA_FILENAME}: у шарі k8s/.../base не тримай локальний hpa.yaml — HPA живе у sibling components/ (k8s.mdc)`
|
|
5580
|
-
)
|
|
5581
|
-
}
|
|
5582
|
-
const pdbAbs = join(dir, PDB_FILENAME)
|
|
5583
|
-
if (existsSync(pdbAbs)) {
|
|
5584
|
-
fail(
|
|
5585
|
-
`${deployRel}/${PDB_FILENAME}: у шарі k8s/.../base не тримай локальний pdb.yaml — PDB живе у sibling components/ (k8s.mdc)`
|
|
5586
|
-
)
|
|
5587
|
-
}
|
|
5596
|
+
failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail)
|
|
5588
5597
|
}
|
|
5589
5598
|
const hpaDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
|
|
5590
5599
|
const pdbDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
|
|
@@ -5600,15 +5609,47 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
|
|
|
5600
5609
|
passFn
|
|
5601
5610
|
)
|
|
5602
5611
|
if (isK8sBaseLayer) {
|
|
5603
|
-
|
|
5604
|
-
const appLabel = deploymentAppLabel(deployment)
|
|
5605
|
-
if (deployName !== null && appLabel !== null) {
|
|
5606
|
-
await validateComponentsForBaseDeployment(dir, deployName, appLabel, root, fail, passFn)
|
|
5607
|
-
}
|
|
5612
|
+
await validateBaseLayerComponentsIfNamed(deployment, dir, root, fail, passFn)
|
|
5608
5613
|
}
|
|
5609
5614
|
}
|
|
5610
5615
|
}
|
|
5611
5616
|
|
|
5617
|
+
/**
|
|
5618
|
+
* У шарі `…/k8s/…/base/` забороняє локальні `hpa.yaml` / `pdb.yaml` (вони мають жити у sibling `components/`).
|
|
5619
|
+
* @param {string} dir абсолютний каталог Deployment-маніфесту
|
|
5620
|
+
* @param {string} deployRel відносний шлях для повідомлень (`.` якщо корінь репо)
|
|
5621
|
+
* @param {(msg: string) => void} fail callback при порушенні
|
|
5622
|
+
*/
|
|
5623
|
+
function failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail) {
|
|
5624
|
+
if (existsSync(join(dir, HPA_FILENAME))) {
|
|
5625
|
+
fail(
|
|
5626
|
+
`${deployRel}/${HPA_FILENAME}: у шарі k8s/.../base не тримай локальний hpa.yaml — HPA живе у sibling components/ (k8s.mdc)`
|
|
5627
|
+
)
|
|
5628
|
+
}
|
|
5629
|
+
if (existsSync(join(dir, PDB_FILENAME))) {
|
|
5630
|
+
fail(
|
|
5631
|
+
`${deployRel}/${PDB_FILENAME}: у шарі k8s/.../base не тримай локальний pdb.yaml — PDB живе у sibling components/ (k8s.mdc)`
|
|
5632
|
+
)
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
/**
|
|
5637
|
+
* Якщо у Deployment є `metadata.name` і `spec.selector.matchLabels.app` — викликає
|
|
5638
|
+
* `validateComponentsForBaseDeployment` для звірки sibling-`components/`. Без цих ключів
|
|
5639
|
+
* каталог `components/` неможливо звʼязати з конкретним Deployment, тож пропускаємо мовчки.
|
|
5640
|
+
* @param {Record<string, unknown>} deployment AST документа Deployment
|
|
5641
|
+
* @param {string} dir абсолютний каталог Deployment-маніфесту
|
|
5642
|
+
* @param {string} root абсолютний корінь репо
|
|
5643
|
+
* @param {(msg: string) => void} fail callback при порушенні
|
|
5644
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
5645
|
+
*/
|
|
5646
|
+
async function validateBaseLayerComponentsIfNamed(deployment, dir, root, fail, passFn) {
|
|
5647
|
+
const deployName = manifestMetadataName(deployment)
|
|
5648
|
+
const appLabel = deploymentAppLabel(deployment)
|
|
5649
|
+
if (deployName === null || appLabel === null) return
|
|
5650
|
+
await validateComponentsForBaseDeployment(dir, deployName, appLabel, root, fail, passFn)
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5612
5653
|
/**
|
|
5613
5654
|
* Витягує документи Deployment з YAML-файлу (повертає порожній масив, якщо файл недоступний або немає Deployment).
|
|
5614
5655
|
* @param {string} filePath абсолютний шлях до YAML-файлу
|
|
@@ -6302,6 +6343,20 @@ function applyConversionsToDoc(doc, conversions) {
|
|
|
6302
6343
|
const patchesNode = doc.get('patches', true)
|
|
6303
6344
|
if (!isSeq(patchesNode)) return false
|
|
6304
6345
|
|
|
6346
|
+
applyPatchConversionsToPatchesNode(patchesNode, groupConversionsByPatchIndex(conversions))
|
|
6347
|
+
if (patchesNode.items.length === 0) {
|
|
6348
|
+
doc.delete('patches')
|
|
6349
|
+
}
|
|
6350
|
+
appendConvertedImagesNode(doc, conversions)
|
|
6351
|
+
return true
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
/**
|
|
6355
|
+
* Згруповує конвертації за індексом `patches[i]` і збирає `opIdx`-список ops, які треба видалити.
|
|
6356
|
+
* @param {Array<{ index: number, opIndex: number, totalOps: number }>} conversions конвертації
|
|
6357
|
+
* @returns {Map<number, { totalOps: number, opIdx: number[] }>} згруповане
|
|
6358
|
+
*/
|
|
6359
|
+
function groupConversionsByPatchIndex(conversions) {
|
|
6305
6360
|
/** @type {Map<number, { totalOps: number, opIdx: number[] }>} */
|
|
6306
6361
|
const byPatch = new Map()
|
|
6307
6362
|
for (const c of conversions) {
|
|
@@ -6309,39 +6364,61 @@ function applyConversionsToDoc(doc, conversions) {
|
|
|
6309
6364
|
slot.opIdx.push(c.opIndex)
|
|
6310
6365
|
byPatch.set(c.index, slot)
|
|
6311
6366
|
}
|
|
6367
|
+
return byPatch
|
|
6368
|
+
}
|
|
6312
6369
|
|
|
6370
|
+
/**
|
|
6371
|
+
* Застосовує згруповані конвертації до `patches:` Sequence: видаляє повністю-конвертовані
|
|
6372
|
+
* patches або переписує inline `patch:` без конвертованих ops. Іде в порядку спадання
|
|
6373
|
+
* індексів, щоб зберегти стабільність вилучень з масиву.
|
|
6374
|
+
* @param {import('yaml').YAMLSeq & { get(i: number, keep: true): unknown, delete(i: number): void, items: unknown[] }} patchesNode YAML Seq (звужено через `isSeq` у caller-і)
|
|
6375
|
+
* @param {Map<number, { totalOps: number, opIdx: number[] }>} byPatch згруповані конвертації
|
|
6376
|
+
*/
|
|
6377
|
+
function applyPatchConversionsToPatchesNode(patchesNode, byPatch) {
|
|
6313
6378
|
const sortedIdx = [...byPatch.keys()].toSorted((a, b) => b - a)
|
|
6314
6379
|
for (const i of sortedIdx) {
|
|
6315
6380
|
const slot = byPatch.get(i)
|
|
6316
6381
|
if (slot === undefined) continue
|
|
6317
|
-
|
|
6318
|
-
if (opIdx.length === totalOps) {
|
|
6382
|
+
if (slot.opIdx.length === slot.totalOps) {
|
|
6319
6383
|
patchesNode.delete(i)
|
|
6320
6384
|
continue
|
|
6321
6385
|
}
|
|
6322
|
-
|
|
6323
|
-
if (patchEntry === undefined || patchEntry === null) continue
|
|
6324
|
-
const patchScalar = patchEntry.get('patch', true)
|
|
6325
|
-
if (patchScalar === undefined || patchScalar === null || typeof patchScalar.value !== 'string') continue
|
|
6326
|
-
const rewritten = rewriteInlinePatchWithoutOps(patchScalar.value, opIdx)
|
|
6327
|
-
if (rewritten === null) continue
|
|
6328
|
-
patchScalar.value = rewritten
|
|
6386
|
+
rewriteInlinePatchAtIndex(patchesNode, i, slot.opIdx)
|
|
6329
6387
|
}
|
|
6388
|
+
}
|
|
6330
6389
|
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6390
|
+
/**
|
|
6391
|
+
* Переписує inline `patch:` у `patches[i]`, видаляючи ops зі списку. Якщо вузол не знайдено
|
|
6392
|
+
* або переписування не вдалося — залишає Document без змін.
|
|
6393
|
+
* @param {import('yaml').YAMLSeq & { get(i: number, keep: true): unknown, delete(i: number): void, items: unknown[] }} patchesNode YAML Seq (звужено через `isSeq` у caller-і)
|
|
6394
|
+
* @param {number} i індекс у `patches:`
|
|
6395
|
+
* @param {number[]} opIdx індекси ops для видалення
|
|
6396
|
+
*/
|
|
6397
|
+
function rewriteInlinePatchAtIndex(patchesNode, i, opIdx) {
|
|
6398
|
+
const patchEntry = patchesNode.get(i, true)
|
|
6399
|
+
if (patchEntry === undefined || patchEntry === null) return
|
|
6400
|
+
const patchScalar = patchEntry.get('patch', true)
|
|
6401
|
+
if (patchScalar === undefined || patchScalar === null || typeof patchScalar.value !== 'string') return
|
|
6402
|
+
const rewritten = rewriteInlinePatchWithoutOps(patchScalar.value, opIdx)
|
|
6403
|
+
if (rewritten === null) return
|
|
6404
|
+
patchScalar.value = rewritten
|
|
6405
|
+
}
|
|
6334
6406
|
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6407
|
+
/**
|
|
6408
|
+
* Дописує `images:` Seq у Document результатами конвертацій (створює, якщо немає).
|
|
6409
|
+
* @param {import('yaml').Document} doc YAML Document
|
|
6410
|
+
* @param {Array<{ name: string, newName: string, newTag: string | null }>} conversions конвертації
|
|
6411
|
+
*/
|
|
6412
|
+
function appendConvertedImagesNode(doc, conversions) {
|
|
6413
|
+
const existing = doc.get('images', true)
|
|
6414
|
+
const imagesNode = isSeq(existing) ? existing : doc.createNode([])
|
|
6415
|
+
if (existing !== imagesNode) {
|
|
6338
6416
|
doc.set('images', imagesNode)
|
|
6339
6417
|
}
|
|
6340
6418
|
for (const { name, newName, newTag } of conversions) {
|
|
6341
6419
|
const entry = newTag === null ? { name, newName } : { name, newName, newTag }
|
|
6342
6420
|
imagesNode.add(doc.createNode(entry))
|
|
6343
6421
|
}
|
|
6344
|
-
return true
|
|
6345
6422
|
}
|
|
6346
6423
|
|
|
6347
6424
|
/**
|
|
@@ -6370,7 +6447,7 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
|
|
|
6370
6447
|
}
|
|
6371
6448
|
if (seq.items.length === 0) return null
|
|
6372
6449
|
seq.flow = false
|
|
6373
|
-
return inner.toString()
|
|
6450
|
+
return stripTrailingNewlines(inner.toString())
|
|
6374
6451
|
}
|
|
6375
6452
|
|
|
6376
6453
|
/**
|
|
@@ -33,9 +33,6 @@ const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
|
|
|
33
33
|
/** Поле `version` у текстовому зрізі `package.json` (для `git show HEAD:npm/package.json`). */
|
|
34
34
|
const PACKAGE_JSON_VERSION_RE = /"version":\s*"([^"]+)"/u
|
|
35
35
|
|
|
36
|
-
/** Канонічний entrypoint типів для пакетів із вихідним `.js` під каталогом `npm/src` */
|
|
37
|
-
const TYPES_INDEX = './types/index.d.ts'
|
|
38
|
-
|
|
39
36
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
40
37
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
41
38
|
|
|
@@ -198,7 +195,7 @@ async function gitDiffNameOnlyNpm() {
|
|
|
198
195
|
async function gitShowNpmPackageVersionAt(refPath) {
|
|
199
196
|
try {
|
|
200
197
|
const { stdout } = await execFileAsync('git', ['show', refPath], { encoding: 'utf8' })
|
|
201
|
-
const m = stdout.match(
|
|
198
|
+
const m = stdout.match(PACKAGE_JSON_VERSION_RE)
|
|
202
199
|
return m ? m[1] : null
|
|
203
200
|
} catch {
|
|
204
201
|
return null
|
package/scripts/check-php.mjs
CHANGED
|
@@ -15,9 +15,9 @@ import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Перевіряє відповідність проєкту правилам php.mdc.
|
|
18
|
-
* @returns {
|
|
18
|
+
* @returns {number} 0 — все OK, 1 — є проблеми
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
20
|
+
export function check() {
|
|
21
21
|
const reporter = createCheckReporter()
|
|
22
22
|
const { pass, fail } = reporter
|
|
23
23
|
|
|
@@ -27,10 +27,10 @@ export async function check() {
|
|
|
27
27
|
fail('composer.json не знайдено в корені — додай (php.mdc)')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
if (
|
|
31
|
-
fail('package.json не знайдено в корені — додай (php.mdc)')
|
|
32
|
-
} else {
|
|
30
|
+
if (existsSync('package.json')) {
|
|
33
31
|
pass('package.json є (наявність lint-php перевіряє bun run lint-conftest → php.package_json)')
|
|
32
|
+
} else {
|
|
33
|
+
fail('package.json не знайдено в корені — додай (php.mdc)')
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const wfPath = '.github/workflows/lint-php.yml'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stop-hook для Claude Code: запускається hook'ом із `.claude/settings.json` після того,
|
|
3
|
-
* як агент сигналізує завершення ходу. Прозоро прокидає `npx
|
|
3
|
+
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor check`
|
|
4
4
|
* і повертає його exit code, щоб помилки правил блокували завершення.
|
|
5
5
|
*
|
|
6
6
|
* Захист від нескінченної рекурсії: якщо stdin містить `"stop_hook_active": true`
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* виходимо з кодом 0 без повторного запуску перевірок.
|
|
9
9
|
*
|
|
10
10
|
* Виклик з `bin/n-cursor.js`:
|
|
11
|
-
* `npx --no
|
|
11
|
+
* `npx --no \@nitra/cursor stop-hook`
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from 'node:child_process'
|
|
14
14
|
import { once } from 'node:events'
|