@nitra/cursor 1.8.71 → 1.8.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mdc/k8s.mdc CHANGED
@@ -206,6 +206,30 @@ patches:
206
206
  value: dev
207
207
  ```
208
208
 
209
+ 4. Якщо в kustomization.yaml є remove разом з add на однаковий path:
210
+
211
+ ```yaml title="overlay/kustomization.yaml (фрагмент)"
212
+ - op: remove
213
+ path: /spec/template/spec/nodeSelector
214
+ - op: add
215
+ path: /spec/template/spec/nodeSelector
216
+ value:
217
+ preem: "false"
218
+ ```
219
+
220
+ заміняй на replace:
221
+
222
+ ```yaml title="overlay/kustomization.yaml (фрагмент)"
223
+ - target:
224
+ kind: Deployment
225
+ name: x
226
+ patch: |-
227
+ - op: replace
228
+ path: /spec/template/spec/nodeSelector
229
+ value:
230
+ preem: "false"
231
+ ```
232
+
209
233
  ## Перевірка
210
234
 
211
235
  **`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.71",
3
+ "version": "1.8.73",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -32,7 +32,7 @@ import { dirname, join, relative } from 'node:path'
32
32
  import { parseAllDocuments } from 'yaml'
33
33
 
34
34
  import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
35
- import { pass } from './utils/pass.mjs'
35
+ import { createCheckReporter } from './utils/check-reporter.mjs'
36
36
  import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
37
37
  import { walkDir } from './utils/walkDir.mjs'
38
38
 
@@ -79,9 +79,9 @@ export function isAbieK8sBaseYamlPath(rel) {
79
79
  /**
80
80
  * Чи значення **`preem`** у base **Deployment** вважається «істинним» за abie.mdc (**true** або рядок **`true`** без урахування регістру).
81
81
  * @param {unknown} v значення з YAML
82
- * @returns {boolean}
82
+ * @returns {boolean} **true**, якщо значення вважається істинним за abie.mdc
83
83
  */
84
- function isAbiePreemTrueish(v) {
84
+ function isAbiePreemTruthy(v) {
85
85
  if (v === true) {
86
86
  return true
87
87
  }
@@ -117,7 +117,7 @@ export function deploymentDocumentHasAbieBasePreemNodeSelector(obj) {
117
117
  if (nodeSelector === null || typeof nodeSelector !== 'object' || Array.isArray(nodeSelector)) {
118
118
  return false
119
119
  }
120
- return isAbiePreemTrueish(nodeSelector.preem)
120
+ return isAbiePreemTruthy(nodeSelector.preem)
121
121
  }
122
122
 
123
123
  /**
@@ -279,9 +279,10 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
279
279
  * @param {string} root корінь репозиторію
280
280
  * @param {string[]} yamlFilesAbs yaml під k8s
281
281
  * @param {(msg: string) => void} fail callback
282
+ * @param {(msg: string) => void} passFn успішне повідомлення
282
283
  * @returns {Promise<void>}
283
284
  */
284
- async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail) {
285
+ async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
285
286
  const baseFiles = yamlFilesAbs.filter(abs => {
286
287
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
287
288
  return isAbieK8sBaseYamlPath(rel)
@@ -311,24 +312,24 @@ async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fai
311
312
  return
312
313
  }
313
314
  for (const doc of docs) {
314
- if (doc.errors.length > 0) {
315
- continue
316
- }
317
- const obj = doc.toJSON()
318
- if (!isDeploymentDoc(obj)) {
319
- continue
320
- }
321
- anyBaseDeployment = true
322
- if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
323
- fail(`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`)
324
- return
315
+ if (doc.errors.length === 0) {
316
+ const obj = doc.toJSON()
317
+ if (isDeploymentDoc(obj)) {
318
+ anyBaseDeployment = true
319
+ if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
320
+ fail(
321
+ `${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
322
+ )
323
+ return
324
+ }
325
+ }
325
326
  }
326
327
  }
327
328
  }
328
329
  if (anyBaseDeployment) {
329
- pass('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
330
+ passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
330
331
  } else {
331
- pass('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
332
+ passFn('Немає Deployment у шляхах …/base/… — перевірку preem у base пропущено')
332
333
  }
333
334
  }
334
335
 
@@ -506,28 +507,25 @@ function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
506
507
  /** @type {string[]} */
507
508
  const out = []
508
509
  for (const p of patches) {
509
- if (p === null || typeof p !== 'object' || Array.isArray(p)) {
510
- continue
511
- }
512
- const pr = /** @type {Record<string, unknown>} */ (p)
513
- const target = pr.target
514
- if (target === null || typeof target !== 'object' || Array.isArray(target)) {
515
- continue
516
- }
517
- const tg = /** @type {Record<string, unknown>} */ (target)
518
- if (tg.kind !== 'HTTPRoute' || tg.name !== 'nginx-run') {
519
- continue
520
- }
521
- const patchStr = pr.patch
522
- if (typeof patchStr === 'string' && patchStr.trim() !== '') {
523
- out.push(patchStr)
510
+ if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
511
+ const pr = /** @type {Record<string, unknown>} */ (p)
512
+ const target = pr.target
513
+ if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
514
+ const tg = /** @type {Record<string, unknown>} */ (target)
515
+ if (tg.kind === 'HTTPRoute' && tg.name === 'nginx-run') {
516
+ const patchStr = pr.patch
517
+ if (typeof patchStr === 'string' && patchStr.trim() !== '') {
518
+ out.push(patchStr)
519
+ }
520
+ }
521
+ }
524
522
  }
525
523
  }
526
524
  return out
527
525
  }
528
526
 
529
527
  /**
530
- * Об’єднує всі inline **JSON6902**-фрагменти для **HTTPRoute/nginx-run** у **kustomization.yaml** (усі документи у файлі).
528
+ * Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute/nginx-run** у **kustomization.yaml** (усі документи у файлі).
531
529
  * @param {string} raw повний текст файлу
532
530
  * @returns {string} текст для **`validateAbieNginxRunHttpRoutePatches`** (може бути порожнім)
533
531
  */
@@ -552,8 +550,8 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
552
550
  }
553
551
 
554
552
  /**
555
- * Перевіряє об’єднаний текст patch(ів) **HTTPRoute/nginx-run** на відповідність abie.mdc.
556
- * @param {string} combined текст одного або кількох inline **patch** (з’єднаних перевідом рядка)
553
+ * Перевіряє сукупний текст patch(ів) **HTTPRoute/nginx-run** на відповідність abie.mdc.
554
+ * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
557
555
  * @param {'ua' | 'ru'} mode **ua** або **ru**
558
556
  * @returns {string | null} повідомлення про помилку або **null**
559
557
  */
@@ -569,24 +567,21 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
569
567
  if (!markers.some(m => combined.includes(m))) {
570
568
  return `HTTPRoute nginx-run: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
571
569
  }
572
- const ns = mode === 'ua' ? 'ua' : 'ru'
573
- const nsRe = new RegExp(
574
- String.raw`path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?${ns}['"]?(?:\s|$)`,
575
- 'mu'
576
- )
577
- if (!nsRe.test(combined)) {
578
- return `HTTPRoute nginx-run: потрібен replace path /spec/parentRefs/0/namespace з value ${ns} (abie.mdc)`
570
+ const namespaceOk =
571
+ mode === 'ua'
572
+ ? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
573
+ : /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
574
+ if (!namespaceOk) {
575
+ return `HTTPRoute nginx-run: потрібен replace path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
579
576
  }
580
- if (mode === 'ru') {
581
- if (!/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
582
- return 'HTTPRoute nginx-run (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
583
- }
577
+ if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
578
+ return 'HTTPRoute nginx-run (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
584
579
  }
585
580
  return null
586
581
  }
587
582
 
588
583
  /**
589
- * Чи **kustomization** містить валідні для abie патчі **HTTPRoute/nginx-run** (**ua** або **ru**).
584
+ * Чи **kustomization** містить валідні для abie записи **patch** для **HTTPRoute/nginx-run** (**ua** або **ru**).
590
585
  * @param {string} raw повний текст **kustomization.yaml**
591
586
  * @param {'ua' | 'ru'} mode overlay
592
587
  * @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
@@ -780,9 +775,10 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
780
775
  * @param {string} root корінь репозиторію
781
776
  * @param {string[]} yamlFilesAbs yaml під k8s
782
777
  * @param {(msg: string) => void} fail callback
778
+ * @param {(msg: string) => void} passFn успішне повідомлення
783
779
  * @returns {Promise<void>}
784
780
  */
785
- async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
781
+ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
786
782
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
787
783
  if (uaAbsList.length === 0) {
788
784
  fail(
@@ -806,7 +802,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
806
802
  )
807
803
  return
808
804
  }
809
- pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
805
+ passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
810
806
  }
811
807
 
812
808
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
@@ -832,7 +828,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
832
828
  )
833
829
  return
834
830
  }
835
- pass(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
831
+ passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
836
832
  }
837
833
  }
838
834
 
@@ -841,9 +837,10 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
841
837
  * @param {string} root корінь репозиторію
842
838
  * @param {string[]} yamlFilesAbs yaml під k8s
843
839
  * @param {(msg: string) => void} fail callback
840
+ * @param {(msg: string) => void} passFn успішне повідомлення
844
841
  * @returns {Promise<void>}
845
842
  */
846
- async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
843
+ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
847
844
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
848
845
  if (uaAbsList.length === 0) {
849
846
  fail(
@@ -867,7 +864,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
867
864
  fail(`${rel}: ${v}`)
868
865
  return
869
866
  }
870
- pass(`${rel}: HTTPRoute nginx-run (ua) відповідає abie.mdc`)
867
+ passFn(`${rel}: HTTPRoute nginx-run (ua) відповідає abie.mdc`)
871
868
  }
872
869
 
873
870
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
@@ -893,7 +890,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
893
890
  fail(`${rel}: ${v}`)
894
891
  return
895
892
  }
896
- pass(`${rel}: HTTPRoute nginx-run (ru) відповідає abie.mdc`)
893
+ passFn(`${rel}: HTTPRoute nginx-run (ru) відповідає abie.mdc`)
897
894
  }
898
895
  }
899
896
 
@@ -902,17 +899,14 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail) {
902
899
  * @returns {Promise<number>} 0 — OK, 1 — є порушення
903
900
  */
904
901
  export async function check() {
905
- let exitCode = 0
906
- const fail = msg => {
907
- console.log(` ❌ ${msg}`)
908
- exitCode = 1
909
- }
902
+ const reporter = createCheckReporter()
903
+ const { pass, fail } = reporter
910
904
 
911
905
  const root = process.cwd()
912
906
  const enabled = await isAbieRuleEnabled(root)
913
907
  if (!enabled) {
914
908
  pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
915
- return 0
909
+ return reporter.getExitCode()
916
910
  }
917
911
 
918
912
  pass('Правило abie увімкнено — виконуємо перевірки')
@@ -978,7 +972,7 @@ export async function check() {
978
972
  }
979
973
  }
980
974
  pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
981
- await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail)
975
+ await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
982
976
  } else {
983
977
  pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
984
978
  }
@@ -988,10 +982,10 @@ export async function check() {
988
982
 
989
983
  if (deploymentDirs.size > 0) {
990
984
  pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
991
- await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
985
+ await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
992
986
  pass('Є Deployment — перевіряємо HTTPRoute nginx-run у ua/ru kustomization (abie.mdc)')
993
- await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail)
987
+ await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
994
988
  }
995
989
 
996
- return exitCode
990
+ return reporter.getExitCode()
997
991
  }
@@ -17,7 +17,7 @@
17
17
  import { existsSync } from 'node:fs'
18
18
  import { readFile } from 'node:fs/promises'
19
19
 
20
- import { pass } from './utils/pass.mjs'
20
+ import { createCheckReporter } from './utils/check-reporter.mjs'
21
21
 
22
22
  /**
23
23
  * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише `@cspell/*` та `@nitra/*`).
@@ -53,11 +53,8 @@ async function loadNCursorRules() {
53
53
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
54
54
  */
55
55
  export async function check() {
56
- let exitCode = 0
57
- const fail = msg => {
58
- console.log(` ❌ ${msg}`)
59
- exitCode = 1
60
- }
56
+ const reporter = createCheckReporter()
57
+ const { pass, fail } = reporter
61
58
 
62
59
  const forbidden = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.yarnrc.yml']
63
60
  for (const f of forbidden) {
@@ -164,5 +161,5 @@ export async function check() {
164
161
  }
165
162
  }
166
163
 
167
- return exitCode
164
+ return reporter.getExitCode()
168
165
  }
@@ -8,7 +8,7 @@
8
8
  import { basename } from 'node:path'
9
9
 
10
10
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { walkDir } from './utils/walkDir.mjs'
13
13
 
14
14
  /**
@@ -42,18 +42,15 @@ export async function findDockerfilePaths(root) {
42
42
  * @returns {Promise<number>} 0 — все OK, 1 — є зауваження або помилка запуску
43
43
  */
44
44
  export async function check() {
45
- let exitCode = 0
46
- const fail = msg => {
47
- console.log(` ❌ ${msg}`)
48
- exitCode = 1
49
- }
45
+ const reporter = createCheckReporter()
46
+ const { pass, fail } = reporter
50
47
 
51
48
  const root = process.cwd()
52
49
  const files = await findDockerfilePaths(root)
53
50
 
54
51
  if (files.length === 0) {
55
52
  pass('Немає Dockerfile / Containerfile — перевірку hadolint пропущено')
56
- return 0
53
+ return reporter.getExitCode()
57
54
  }
58
55
 
59
56
  pass(`Знайдено файлів для hadolint: ${files.length}`)
@@ -69,5 +66,5 @@ export async function check() {
69
66
  }
70
67
  }
71
68
 
72
- return exitCode
69
+ return reporter.getExitCode()
73
70
  }
@@ -14,7 +14,7 @@ import { existsSync } from 'node:fs'
14
14
  import { readdir, readFile } from 'node:fs/promises'
15
15
  import { join } from 'node:path'
16
16
 
17
- import { pass } from './utils/pass.mjs'
17
+ import { createCheckReporter } from './utils/check-reporter.mjs'
18
18
  import {
19
19
  anyRunStepIncludes,
20
20
  eventPathsIncludeExact,
@@ -122,17 +122,14 @@ function verifyNoDirectBunOrCache(relPath, content, failFn, passFn) {
122
122
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
123
123
  */
124
124
  export async function check() {
125
- let exitCode = 0
126
- const fail = msg => {
127
- console.log(` ❌ ${msg}`)
128
- exitCode = 1
129
- }
125
+ const reporter = createCheckReporter()
126
+ const { pass, fail } = reporter
130
127
 
131
128
  const wfDir = '.github/workflows'
132
129
 
133
130
  if (!existsSync(wfDir)) {
134
131
  fail(`Директорія ${wfDir} не існує`)
135
- return exitCode
132
+ return reporter.getExitCode()
136
133
  }
137
134
 
138
135
  const setupBunDepsAction = '.github/actions/setup-bun-deps/action.yml'
@@ -290,5 +287,5 @@ export async function check() {
290
287
  }
291
288
  }
292
289
 
293
- return exitCode
290
+ return reporter.getExitCode()
294
291
  }
@@ -6,18 +6,15 @@
6
6
  import { existsSync } from 'node:fs'
7
7
  import { readFile } from 'node:fs/promises'
8
8
 
9
- import { pass } from './utils/pass.mjs'
9
+ import { createCheckReporter } from './utils/check-reporter.mjs'
10
10
 
11
11
  /**
12
12
  * Перевіряє відповідність проєкту правилам js-format.mdc
13
13
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
14
14
  */
15
15
  export async function check() {
16
- let exitCode = 0
17
- const fail = msg => {
18
- console.log(` ❌ ${msg}`)
19
- exitCode = 1
20
- }
16
+ const reporter = createCheckReporter()
17
+ const { pass, fail } = reporter
21
18
 
22
19
  const expectedKeys = [
23
20
  'arrowParens',
@@ -95,5 +92,5 @@ export async function check() {
95
92
  if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
96
93
  }
97
94
 
98
- return exitCode
95
+ return reporter.getExitCode()
99
96
  }
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
 
12
12
  import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
13
- import { pass } from './utils/pass.mjs'
13
+ import { createCheckReporter } from './utils/check-reporter.mjs'
14
14
 
15
15
  /** Очікуваний локальний скрипт. */
16
16
  export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
@@ -41,11 +41,8 @@ export function isCanonicalLintJs(script) {
41
41
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
42
42
  */
43
43
  export async function check() {
44
- let exitCode = 0
45
- const fail = msg => {
46
- console.log(` ❌ ${msg}`)
47
- exitCode = 1
48
- }
44
+ const reporter = createCheckReporter()
45
+ const { pass, fail } = reporter
49
46
 
50
47
  let eslintPath = ''
51
48
  if (existsSync('eslint.config.js')) {
@@ -242,5 +239,5 @@ export async function check() {
242
239
  if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
243
240
  }
244
241
 
245
- return exitCode
242
+ return reporter.getExitCode()
246
243
  }
@@ -8,16 +8,17 @@ import { existsSync } from 'node:fs'
8
8
  import { readFile } from 'node:fs/promises'
9
9
  import { join } from 'node:path'
10
10
 
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
13
13
 
14
14
  /**
15
15
  * Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
16
16
  * @param {string} rootDir відносний шлях workspace (не `'.'`)
17
17
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
18
+ * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
18
19
  * @returns {Promise<void>} завершується після перевірок цього пакета
19
20
  */
20
- async function checkWorkspacePackage(rootDir, fail) {
21
+ async function checkWorkspacePackage(rootDir, fail, passFn) {
21
22
  const label = `[${rootDir}] `
22
23
  const pkgPath = join(rootDir, 'package.json')
23
24
  if (existsSync(pkgPath)) {
@@ -36,9 +37,9 @@ async function checkWorkspacePackage(rootDir, fail) {
36
37
  if (existsSync(configmapPath)) {
37
38
  const content = await readFile(configmapPath, 'utf8')
38
39
  if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
39
- pass(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
40
+ passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
40
41
  if (content.includes('service.name=') && content.includes('service.namespace=')) {
41
- pass(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
42
+ passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
42
43
  } else {
43
44
  fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
44
45
  }
@@ -53,23 +54,20 @@ async function checkWorkspacePackage(rootDir, fail) {
53
54
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
54
55
  */
55
56
  export async function check() {
56
- let exitCode = 0
57
- const fail = msg => {
58
- console.log(` ❌ ${msg}`)
59
- exitCode = 1
60
- }
57
+ const reporter = createCheckReporter()
58
+ const { pass, fail } = reporter
61
59
 
62
60
  const roots = await getMonorepoPackageRootDirs()
63
61
  const workspaceRoots = roots.filter(r => r !== '.')
64
62
 
65
63
  if (workspaceRoots.length === 0) {
66
64
  pass('js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
67
- return exitCode
65
+ return reporter.getExitCode()
68
66
  }
69
67
 
70
68
  for (const r of workspaceRoots) {
71
- await checkWorkspacePackage(r, fail)
69
+ await checkWorkspacePackage(r, fail, pass)
72
70
  }
73
71
 
74
- return exitCode
72
+ return reporter.getExitCode()
75
73
  }
@@ -51,7 +51,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
51
51
 
52
52
  import { parseAllDocuments } from 'yaml'
53
53
 
54
- import { pass } from './utils/pass.mjs'
54
+ import { createCheckReporter } from './utils/check-reporter.mjs'
55
55
  import { walkDir } from './utils/walkDir.mjs'
56
56
 
57
57
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -1482,11 +1482,8 @@ async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
1482
1482
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
1483
1483
  */
1484
1484
  export async function check() {
1485
- let exitCode = 0
1486
- const fail = msg => {
1487
- console.log(` ❌ ${msg}`)
1488
- exitCode = 1
1489
- }
1485
+ const reporter = createCheckReporter()
1486
+ const { pass, fail } = reporter
1490
1487
 
1491
1488
  const root = process.cwd()
1492
1489
 
@@ -1496,7 +1493,7 @@ export async function check() {
1496
1493
 
1497
1494
  if (yamlFiles.length === 0) {
1498
1495
  pass('Немає *.yaml під k8s — перевірку $schema пропущено')
1499
- return 0
1496
+ return reporter.getExitCode()
1500
1497
  }
1501
1498
 
1502
1499
  pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
@@ -1515,5 +1512,5 @@ export async function check() {
1515
1512
 
1516
1513
  await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
1517
1514
 
1518
- return exitCode
1515
+ return reporter.getExitCode()
1519
1516
  }
@@ -18,7 +18,7 @@ import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
18
18
  import { basename, dirname, join, relative } from 'node:path'
19
19
 
20
20
  import { findDockerfilePaths } from './check-docker.mjs'
21
- import { pass } from './utils/pass.mjs'
21
+ import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
  import { walkDir } from './utils/walkDir.mjs'
23
23
 
24
24
  /**
@@ -264,11 +264,8 @@ function dockerfileHasEnvsSubstTemplate(dockerfileContent) {
264
264
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
265
265
  */
266
266
  export async function check() {
267
- let exitCode = 0
268
- const fail = msg => {
269
- console.log(` ❌ ${msg}`)
270
- exitCode = 1
271
- }
267
+ const reporter = createCheckReporter()
268
+ const { pass, fail } = reporter
272
269
 
273
270
  const root = process.cwd()
274
271
 
@@ -284,7 +281,7 @@ export async function check() {
284
281
 
285
282
  if (templates.length === 0) {
286
283
  pass('Немає default.conf.template — перевірку nginx-default-tpl пропущено')
287
- return 0
284
+ return reporter.getExitCode()
288
285
  }
289
286
 
290
287
  pass(`Знайдено default.conf.template: ${templates.length}`)
@@ -383,5 +380,5 @@ export async function check() {
383
380
  fail('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
384
381
  }
385
382
 
386
- return exitCode
383
+ return reporter.getExitCode()
387
384
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs'
8
8
  import { readFile, stat } from 'node:fs/promises'
9
9
 
10
- import { pass } from './utils/pass.mjs'
10
+ import { createCheckReporter } from './utils/check-reporter.mjs'
11
11
  import {
12
12
  hasIdTokenWritePermission,
13
13
  hasNpmPublishStepWithPackage,
@@ -21,11 +21,8 @@ import {
21
21
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
22
22
  */
23
23
  export async function check() {
24
- let exitCode = 0
25
- const fail = msg => {
26
- console.log(` ❌ ${msg}`)
27
- exitCode = 1
28
- }
24
+ const reporter = createCheckReporter()
25
+ const { pass, fail } = reporter
29
26
 
30
27
  if (existsSync('package.json')) {
31
28
  pass('package.json існує')
@@ -114,5 +111,5 @@ export async function check() {
114
111
  fail(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
115
112
  }
116
113
 
117
- return exitCode
114
+ return reporter.getExitCode()
118
115
  }
@@ -8,7 +8,7 @@
8
8
  import { existsSync } from 'node:fs'
9
9
  import { readFile } from 'node:fs/promises'
10
10
 
11
- import { pass } from './utils/pass.mjs'
11
+ import { createCheckReporter } from './utils/check-reporter.mjs'
12
12
  import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-workflow.mjs'
13
13
 
14
14
  /**
@@ -16,11 +16,8 @@ import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-work
16
16
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
17
17
  */
18
18
  export async function check() {
19
- let exitCode = 0
20
- const fail = msg => {
21
- console.log(` ❌ ${msg}`)
22
- exitCode = 1
23
- }
19
+ const reporter = createCheckReporter()
20
+ const { pass, fail } = reporter
24
21
 
25
22
  if (existsSync('package.json')) {
26
23
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
@@ -105,5 +102,5 @@ export async function check() {
105
102
  }
106
103
  }
107
104
 
108
- return exitCode
105
+ return reporter.getExitCode()
109
106
  }
@@ -12,7 +12,7 @@
12
12
  import { existsSync } from 'node:fs'
13
13
  import { readFile } from 'node:fs/promises'
14
14
 
15
- import { pass } from './utils/pass.mjs'
15
+ import { createCheckReporter } from './utils/check-reporter.mjs'
16
16
  import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
17
17
 
18
18
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
@@ -47,11 +47,8 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
47
47
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
48
48
  */
49
49
  export async function check() {
50
- let exitCode = 0
51
- const fail = msg => {
52
- console.log(` ❌ ${msg}`)
53
- exitCode = 1
54
- }
50
+ const reporter = createCheckReporter()
51
+ const { pass, fail } = reporter
55
52
 
56
53
  const v8rIgnoreRequired = ['.vscode/extensions.json', '.vscode/settings.json']
57
54
  if (existsSync('.v8rignore')) {
@@ -215,5 +212,5 @@ export async function check() {
215
212
  }
216
213
  }
217
214
 
218
- return exitCode
215
+ return reporter.getExitCode()
219
216
  }
@@ -11,7 +11,7 @@ import { existsSync } from 'node:fs'
11
11
  import { readFile } from 'node:fs/promises'
12
12
  import { join, relative } from 'node:path'
13
13
 
14
- import { pass } from './utils/pass.mjs'
14
+ import { createCheckReporter } from './utils/check-reporter.mjs'
15
15
  import {
16
16
  findForbiddenVueImportsInSourceFile,
17
17
  isVueImportScanSourceFile,
@@ -53,9 +53,10 @@ function ukFilesCountPhrase(n) {
53
53
  * Перевіряє залежності та vite.config одного Vue-пакета.
54
54
  * @param {string} rootDir відносний шлях до пакета
55
55
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
56
+ * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
56
57
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
57
58
  */
58
- async function checkVuePackage(rootDir, fail) {
59
+ async function checkVuePackage(rootDir, fail, passFn) {
59
60
  const label = packageLabel(rootDir)
60
61
  const prefix = `[${label}] `
61
62
 
@@ -66,7 +67,7 @@ async function checkVuePackage(rootDir, fail) {
66
67
  const allDeps = { ...deps, ...devDeps }
67
68
 
68
69
  if (deps.vue) {
69
- pass(`${prefix}vue в dependencies: ${deps.vue}`)
70
+ passFn(`${prefix}vue в dependencies: ${deps.vue}`)
70
71
  } else {
71
72
  fail(`${prefix}vue відсутній в dependencies`)
72
73
  }
@@ -74,7 +75,7 @@ async function checkVuePackage(rootDir, fail) {
74
75
  if (devDeps.vite) {
75
76
  const match = devDeps.vite.match(/(\d+)/)
76
77
  if (match && Number(match[1]) >= 8) {
77
- pass(`${prefix}vite >= 8: ${devDeps.vite}`)
78
+ passFn(`${prefix}vite >= 8: ${devDeps.vite}`)
78
79
  } else {
79
80
  fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
80
81
  }
@@ -83,25 +84,25 @@ async function checkVuePackage(rootDir, fail) {
83
84
  }
84
85
 
85
86
  if (devDeps['@vitejs/plugin-vue']) {
86
- pass(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
87
+ passFn(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
87
88
  } else {
88
89
  fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
89
90
  }
90
91
 
91
92
  if (allDeps['vue-macros']) {
92
- pass(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
93
+ passFn(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
93
94
  } else {
94
95
  fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
95
96
  }
96
97
 
97
98
  if (allDeps['unplugin-auto-import']) {
98
- pass(`${prefix}unplugin-auto-import присутній`)
99
+ passFn(`${prefix}unplugin-auto-import присутній`)
99
100
  } else {
100
101
  fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
101
102
  }
102
103
 
103
104
  if (allDeps['vite-plugin-vue-layouts-next']) {
104
- pass(`${prefix}vite-plugin-vue-layouts-next присутній`)
105
+ passFn(`${prefix}vite-plugin-vue-layouts-next присутній`)
105
106
  } else {
106
107
  fail(`${prefix}vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next`)
107
108
  }
@@ -112,12 +113,12 @@ async function checkVuePackage(rootDir, fail) {
112
113
  const relConfig = join(rootDir, viteConfig)
113
114
  const content = await readFile(relConfig, 'utf8')
114
115
  if (content.includes('VueMacros')) {
115
- pass(`${prefix}${viteConfig} використовує VueMacros`)
116
+ passFn(`${prefix}${viteConfig} використовує VueMacros`)
116
117
  } else {
117
118
  fail(`${prefix}${viteConfig} не містить VueMacros`)
118
119
  }
119
120
  if (content.includes('AutoImport')) {
120
- pass(`${prefix}${viteConfig} використовує AutoImport`)
121
+ passFn(`${prefix}${viteConfig} використовує AutoImport`)
121
122
  } else {
122
123
  fail(`${prefix}${viteConfig} не містить AutoImport`)
123
124
  }
@@ -147,7 +148,7 @@ async function checkVuePackage(rootDir, fail) {
147
148
  }
148
149
  }
149
150
  if (importViolations === 0) {
150
- pass(
151
+ passFn(
151
152
  `${prefix}немає заборонених value-імпортів з 'vue' у джерелах (проскановано ${ukFilesCountPhrase(sourcePaths.length)})`
152
153
  )
153
154
  }
@@ -158,11 +159,8 @@ async function checkVuePackage(rootDir, fail) {
158
159
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
159
160
  */
160
161
  export async function check() {
161
- let exitCode = 0
162
- const fail = msg => {
163
- console.log(` ❌ ${msg}`)
164
- exitCode = 1
165
- }
162
+ const reporter = createCheckReporter()
163
+ const { pass, fail } = reporter
166
164
 
167
165
  if (existsSync('.vscode/extensions.json')) {
168
166
  const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
@@ -188,12 +186,12 @@ export async function check() {
188
186
 
189
187
  if (vueRoots.length === 0) {
190
188
  fail('vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)')
191
- return exitCode
189
+ return reporter.getExitCode()
192
190
  }
193
191
 
194
192
  for (const r of vueRoots) {
195
- await checkVuePackage(r, fail)
193
+ await checkVuePackage(r, fail, pass)
196
194
  }
197
195
 
198
- return exitCode
196
+ return reporter.getExitCode()
199
197
  }
@@ -11,7 +11,7 @@ import { basename } from 'node:path'
11
11
 
12
12
  import { isRunAsCli } from './cli-entry.mjs'
13
13
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
14
- import { pass } from './utils/pass.mjs'
14
+ import { createCheckReporter } from './utils/check-reporter.mjs'
15
15
  import { walkDir } from './utils/walkDir.mjs'
16
16
 
17
17
  /**
@@ -44,18 +44,15 @@ export async function findLintDockerfilePaths(root) {
44
44
  * @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
45
45
  */
46
46
  async function main() {
47
- let exitCode = 0
48
- const fail = msg => {
49
- console.log(` ❌ ${msg}`)
50
- exitCode = 1
51
- }
47
+ const reporter = createCheckReporter()
48
+ const { pass, fail } = reporter
52
49
 
53
50
  const root = process.cwd()
54
51
  const files = await findLintDockerfilePaths(root)
55
52
 
56
53
  if (files.length === 0) {
57
54
  pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')
58
- return 0
55
+ return reporter.getExitCode()
59
56
  }
60
57
 
61
58
  pass(`lint-docker: файлів для hadolint: ${files.length}`)
@@ -71,7 +68,7 @@ async function main() {
71
68
  }
72
69
  }
73
70
 
74
- return exitCode
71
+ return reporter.getExitCode()
75
72
  }
76
73
 
77
74
  if (isRunAsCli()) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Спільний репортер для check-скриптів і lint-docker.
3
+ *
4
+ * Об’єднує вивід успіхів (`pass` з `pass.mjs`) і помилок з префіксом ❌; накопичує код виходу **1**,
5
+ * якщо хоча б раз викликано `fail`.
6
+ *
7
+ * Використовуй `getExitCode()` у `return`, а не деструктуризацію `exitCode` — геттер «знімається» один раз.
8
+ */
9
+ import { pass } from './pass.mjs'
10
+
11
+ /**
12
+ * Створює пару `pass` / `fail` з накопиченням ненульового коду виходу.
13
+ * @returns {{ pass: typeof pass, fail: (msg: string) => void, getExitCode: () => number }}
14
+ */
15
+ export function createCheckReporter() {
16
+ let exitCode = 0
17
+ return {
18
+ pass,
19
+ fail(msg) {
20
+ console.log(` ❌ ${msg}`)
21
+ exitCode = 1
22
+ },
23
+ getExitCode() {
24
+ return exitCode
25
+ }
26
+ }
27
+ }