@nitra/cursor 1.27.9 → 1.28.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/rules/abie/js/applies.mjs +3 -2
  4. package/rules/abie/js/env_dns.mjs +4 -2
  5. package/rules/abie/js/firebase_hosting.mjs +3 -2
  6. package/rules/abie/js/hc_pairing.mjs +3 -2
  7. package/rules/abie/js/ua_http_route.mjs +4 -2
  8. package/rules/abie/js/ua_node_selector.mjs +4 -2
  9. package/rules/adr/js/hooks.mjs +36 -28
  10. package/rules/bun/js/layout.mjs +16 -11
  11. package/rules/capacitor/js/platforms.mjs +3 -2
  12. package/rules/changelog/js/consistency.mjs +85 -63
  13. package/rules/changelog/lib/package-manifest.mjs +5 -4
  14. package/rules/docker/js/lint.mjs +3 -2
  15. package/rules/ga/js/workflows.mjs +41 -32
  16. package/rules/graphql/js/tooling.mjs +15 -11
  17. package/rules/hasura/js/internal_urls.mjs +14 -10
  18. package/rules/image-avif/js/avif_generation.mjs +36 -23
  19. package/rules/image-compress/js/package_setup.mjs +18 -12
  20. package/rules/js-bun-db/js/safety.mjs +3 -2
  21. package/rules/js-lint/js/tooling.mjs +45 -32
  22. package/rules/js-run/js/runtime.mjs +21 -15
  23. package/rules/k8s/js/manifests.mjs +3 -2
  24. package/rules/nginx-default-tpl/js/template.mjs +3 -2
  25. package/rules/npm-module/js/package_structure.mjs +82 -57
  26. package/rules/rego/js/applies.mjs +4 -4
  27. package/rules/rust/js/applies.mjs +5 -3
  28. package/rules/security/js/sample_secret.mjs +2 -2
  29. package/rules/security/js/trufflehog.mjs +6 -4
  30. package/rules/style-lint/js/tooling.mjs +15 -8
  31. package/rules/test/coverage/coverage.mjs +1 -1
  32. package/rules/test/js/data/vitest_config/vitest.config.baseline.js +7 -0
  33. package/rules/test/js/location.mjs +3 -2
  34. package/rules/test/js/no-process-chdir.mjs +89 -0
  35. package/rules/test/js/vitest-config-pool-forks.mjs +52 -0
  36. package/rules/test/test.mdc +17 -0
  37. package/rules/text/js/forbidden-prettier.mjs +4 -2
  38. package/rules/text/js/formatting.mjs +25 -16
  39. package/rules/vue/js/packages.mjs +33 -25
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `vitest.config.js` має ставити `pool: 'forks'` — defense-in-depth для
3
+ * race-bug у `process.cwd()` (test.mdc, секція "Заборона `process.chdir` у тестах").
4
+ *
5
+ * Чому не достатньо самої заборони `process.chdir(`: third-party код у залежностях
6
+ * може робити chdir всередині vitest worker'а. У `pool: 'threads'` (default) усі
7
+ * workers ділять один процес → race на `process.cwd()` між паралельними test
8
+ * files. `pool: 'forks'` ізолює кожен test file у власному child-процесі.
9
+ *
10
+ * Перевірка — substring у source-тексті `vitest.config.js`. Не парсимо JS AST,
11
+ * бо це може бути будь-який export-формат (ESM default, named, CommonJS).
12
+ * Достатньо знайти `pool:` із значенням `'forks'`/`"forks"` (whitespace дозволений).
13
+ *
14
+ * Скіпи: правило не застосовне, якщо `vitest.config.js` не існує (нема vitest
15
+ * у проєкті) — це не помилка, лише skip. Якщо файл є — `pool: 'forks'`
16
+ * обов'язковий.
17
+ */
18
+ import { existsSync } from 'node:fs'
19
+ import { readFile } from 'node:fs/promises'
20
+ import { join } from 'node:path'
21
+
22
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
23
+
24
+ /** Subтring-pattern: `pool: 'forks'` або `pool: "forks"` (з опційним whitespace). */
25
+ const POOL_FORKS_RE = /pool\s*:\s*['"]forks['"]/u
26
+
27
+ /**
28
+ * Перевіряє, що `vitest.config.js` (якщо існує) містить `pool: 'forks'`.
29
+ * @param {string} [cwdParam] корінь репозиторію
30
+ * @returns {Promise<number>} 0 — OK або skip, 1 — config без `pool: 'forks'`
31
+ */
32
+ export async function check(cwdParam = process.cwd()) {
33
+ const reporter = createCheckReporter()
34
+ const { pass, fail } = reporter
35
+
36
+ const configPath = join(cwdParam, 'vitest.config.js')
37
+ if (!existsSync(configPath)) {
38
+ pass('vitest.config.js відсутній — pool-перевірку пропущено')
39
+ return reporter.getExitCode()
40
+ }
41
+
42
+ const body = await readFile(configPath, 'utf8')
43
+ if (POOL_FORKS_RE.test(body)) {
44
+ pass("vitest.config.js містить pool: 'forks' (test.mdc)")
45
+ } else {
46
+ fail(
47
+ "vitest.config.js має містити pool: 'forks' — defense-in-depth для race у process.cwd() між паралельними test files (test.mdc)"
48
+ )
49
+ }
50
+
51
+ return reporter.getExitCode()
52
+ }
@@ -60,6 +60,23 @@ Recursive globs ловлять файли всередині `tests/` так с
60
60
 
61
61
  Пропускаються: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`, шляхи з `.n-cursor.json:ignore`.
62
62
 
63
+ ## Заборона `process.chdir` у тестах
64
+
65
+ `process.chdir(dir)` — **process-wide** мутація. Vitest за замовчуванням ставить `pool: 'threads'`, і всі workers ділять один процес: паралельний test file може перехопити cwd сусіда посеред FS- або `git`-операції. У реальному інциденті це призвело до того, що `git init`+`git commit` із tmp-фікстури потрапив у реальний робочий репозиторій і створив rogue commit з автором `test <test@test>`, знищивши `npm/package.json` / `CHANGELOG.md`.
66
+
67
+ Тому:
68
+
69
+ - **У тестах заборонено** `process.chdir(...)` напряму та будь-які хелпери, що його викликають (історичний `withTmpCwd` видалений у `1.28.0`).
70
+ - Канон: `withTmpDir(async dir => { ... })` зі `scripts/utils/test-helpers.mjs` — створює tmpdir і передає **абсолютний** `dir` у callback **без** `process.chdir`.
71
+ - Усі FS-операції у тесті — через `join(dir, …)` і `writeJson(join(dir, …), …)` / `ensureDir(join(dir, …))` (хелпери валідують `isAbsolute`).
72
+ - Усі child-процеси — `execFile(bin, args, { cwd: dir })`, `spawnSync(bin, args, { cwd: dir })`.
73
+ - Concern-функції правил — `await check(dir)`, `await applies(dir)`, `await fix(dir)`; усі production функції приймають перший параметр `cwd = process.cwd()` (default зберігає CLI-сумісність).
74
+ - `vitest.config.js` додатково ставить `pool: 'forks'` як defense-in-depth: навіть якщо хтось пропустить правило вище, fork-ізоляція не дасть race у production tree.
75
+
76
+ Це **обов'язково** і для тестів пакета `@nitra/cursor`, і для кожного проєкту-споживача. Перевіряє concern `no-process-chdir` (`rules/test/js/no-process-chdir.mjs`): сканує `**/*.test.{js,mjs}` і падає з ❌ на будь-яке вживання `process.chdir(`.
77
+
78
+ Канон `vitest.config.js` (substring requirement `pool: 'forks'`): [vitest.config.snippet.js](./policy/vitest_config/template/vitest.config.snippet.js)
79
+
63
80
  ## Покриття + мутаційне тестування
64
81
 
65
82
  Канонічна команда — `n-cursor coverage`: збирає метрики покриття (`vitest run --coverage`, `cargo llvm-cov` тощо) і мутаційного тестування (Stryker з vitest-runner + `coverageAnalysis: 'perTest'`, `cargo-mutants`) з усіх активних провайдерів у `.n-cursor.json#rules` і пише `COVERAGE.md` у корінь проєкту. Лок і дедуп — `withLock('coverage', ...)`.
@@ -9,6 +9,7 @@
9
9
  * (https://prettier.io/docs/configuration). Якщо Prettier додасть новий формат — додай рядок.
10
10
  */
11
11
  import { existsSync } from 'node:fs'
12
+ import { join } from 'node:path'
12
13
 
13
14
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
14
15
 
@@ -38,15 +39,16 @@ const FORBIDDEN_PRETTIER_FILES = [
38
39
 
39
40
  /**
40
41
  * Перевіряє, що жоден Prettier-конфіг чи ignore-файл не лежить у корені проєкту.
42
+ * @param {string} [cwd] корінь репозиторію
41
43
  * @returns {number} 0 — все OK, 1 — знайдено заборонений файл
42
44
  */
43
- export function check() {
45
+ export function check(cwd = process.cwd()) {
44
46
  const reporter = createCheckReporter()
45
47
  const { pass, fail } = reporter
46
48
 
47
49
  let anyFound = false
48
50
  for (const file of FORBIDDEN_PRETTIER_FILES) {
49
- if (existsSync(file)) {
51
+ if (existsSync(join(cwd, file))) {
50
52
  fail(`${file} заборонено — Prettier не використовуємо, перейди на oxfmt (text.mdc)`)
51
53
  anyFound = true
52
54
  }
@@ -27,9 +27,11 @@
27
27
  * у `devDependencies`, заборона `markdownlint-cli2` у залежностях.
28
28
  * - `npm/policy/bun/package_json/` — у `devDependencies` лише `@nitra/*`
29
29
  * (раніше дублювалося тут).
30
+ * @param {string} cwd корінь репозиторію
30
31
  */
31
32
  import { existsSync } from 'node:fs'
32
33
  import { readFile } from 'node:fs/promises'
34
+ import { join } from 'node:path'
33
35
 
34
36
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
35
37
  import { anyRunStepIncludes, parseWorkflowYaml } from '../../../scripts/lib/gha-workflow.mjs'
@@ -65,14 +67,16 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
65
67
  * Перевіряє .v8rignore.
66
68
  * @param {(msg: string) => void} passFn callback при успішній перевірці
67
69
  * @param {(msg: string) => void} failFn callback при помилці
70
+ * @param {string} cwd корінь репозиторію
68
71
  */
69
- async function checkV8rIgnore(passFn, failFn) {
72
+ async function checkV8rIgnore(passFn, failFn, cwd) {
70
73
  const required = ['.vscode/extensions.json', '.vscode/settings.json']
71
- if (!existsSync('.v8rignore')) {
74
+ const v8rPath = join(cwd, '.v8rignore')
75
+ if (!existsSync(v8rPath)) {
72
76
  failFn('.v8rignore не існує — створи згідно n-text.mdc (мінімум .vscode/extensions.json і .vscode/settings.json)')
73
77
  return
74
78
  }
75
- const raw = await readFile('.v8rignore', 'utf8')
79
+ const raw = await readFile(v8rPath, 'utf8')
76
80
  const lines = new Set(
77
81
  raw
78
82
  .split('\n')
@@ -100,8 +104,9 @@ async function checkV8rIgnore(passFn, failFn) {
100
104
  * @param {(msg: string) => void} passFn callback при успішній перевірці
101
105
  * @param {(msg: string) => void} failFn callback при помилці
102
106
  * @returns {Promise<void>}
107
+ * @param {string} cwd корінь репозиторію
103
108
  */
104
- function checkTextConfigsExistence(passFn, failFn) {
109
+ function checkTextConfigsExistence(passFn, failFn, cwd) {
105
110
  for (const [path, mdcRef] of [
106
111
  ['.oxfmtrc.json', 'text.oxfmtrc'],
107
112
  ['.cspell.json', 'text.cspell'],
@@ -109,7 +114,7 @@ function checkTextConfigsExistence(passFn, failFn) {
109
114
  ['.vscode/extensions.json', 'text.vscode_extensions'],
110
115
  ['.vscode/settings.json', 'text.vscode_settings']
111
116
  ]) {
112
- if (existsSync(path)) {
117
+ if (existsSync(join(cwd, path))) {
113
118
  passFn(`${path} є (структуру перевіряє npx @nitra/cursor fix → ${mdcRef})`)
114
119
  } else {
115
120
  failFn(`${path} не існує — створи згідно n-text.mdc`)
@@ -125,14 +130,17 @@ function checkTextConfigsExistence(passFn, failFn) {
125
130
  * `@nitra/*` гейт) — у Rego (`text.package_json`, `bun.package_json`).
126
131
  * @param {(msg: string) => void} passFn callback при успішній перевірці
127
132
  * @param {(msg: string) => void} failFn callback при помилці
133
+ * @param {string} cwd корінь репозиторію
128
134
  */
129
- async function checkPackageJsonText(passFn, failFn) {
130
- if (!existsSync('package.json')) return
131
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
135
+ async function checkPackageJsonText(passFn, failFn, cwd) {
136
+ const pkgPath = join(cwd, 'package.json')
137
+ if (!existsSync(pkgPath)) return
138
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
132
139
  checkLintTextScript(pkg.scripts?.['lint-text'], passFn, failFn)
133
140
 
134
- if (existsSync('.github/workflows/lint-text.yml')) {
135
- const wf = await readFile('.github/workflows/lint-text.yml', 'utf8')
141
+ const lintTextWf = join(cwd, '.github/workflows/lint-text.yml')
142
+ if (existsSync(lintTextWf)) {
143
+ const wf = await readFile(lintTextWf, 'utf8')
136
144
  const root = parseWorkflowYaml(wf)
137
145
  const ok = root ? anyRunStepIncludes(root, 'bun run lint-text') : wf.includes('bun run lint-text')
138
146
  if (ok) {
@@ -166,27 +174,28 @@ function checkLintTextScript(lintText, passFn, failFn) {
166
174
 
167
175
  /**
168
176
  * Перевіряє відповідність проєкту правилам text.mdc.
177
+ * @param {string} [cwd] корінь репозиторію
169
178
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
170
179
  */
171
- export async function check() {
180
+ export async function check(cwd = process.cwd()) {
172
181
  const reporter = createCheckReporter()
173
182
  const { pass, fail } = reporter
174
183
 
175
- await checkV8rIgnore(pass, fail)
176
- await checkTextConfigsExistence(pass, fail)
184
+ await checkV8rIgnore(pass, fail, cwd)
185
+ await checkTextConfigsExistence(pass, fail, cwd)
177
186
 
178
187
  // Prettier-конфіги/ignore — окремий concern `text.forbidden-prettier` (rules/text/js/forbidden-prettier.mjs).
179
188
 
180
- const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
189
+ const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(join(cwd, p)))
181
190
  if (textRulePaths.length === 0) {
182
191
  pass('n-text.mdc / npm/mdc/text.mdc відсутні — перевірку абзацу про апостроф пропущено')
183
192
  } else {
184
193
  for (const p of textRulePaths) {
185
- verifyUkApostropheRuleParagraph(p, await readFile(p, 'utf8'), fail, pass)
194
+ verifyUkApostropheRuleParagraph(p, await readFile(join(cwd, p), 'utf8'), fail, pass)
186
195
  }
187
196
  }
188
197
 
189
- await checkPackageJsonText(pass, fail)
198
+ await checkPackageJsonText(pass, fail, cwd)
190
199
 
191
200
  return reporter.getExitCode()
192
201
  }
@@ -20,6 +20,7 @@
20
20
  * Окремо в `.vue` SFC заборонено імпорти Node-нативних модулів — `node:*` префікс або bare-ім’я
21
21
  * вбудованого модуля Node (`fs`, `path`, `timers/promises` тощо). Vue SFC виконується у браузері,
22
22
  * де Node API недоступне; такий код треба тримати у server-side утілітах.
23
+ * @param {string} cwd корінь репозиторію
23
24
  */
24
25
  import { existsSync } from 'node:fs'
25
26
  import { readFile } from 'node:fs/promises'
@@ -194,17 +195,18 @@ function ukFilesCountPhrase(n) {
194
195
  * @param {(msg: string) => void} passFn успіх
195
196
  * @param {(msg: string) => void} fail помилка
196
197
  * @returns {Promise<void>}
198
+ * @param {string} cwd корінь репозиторію
197
199
  */
198
- async function checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail) {
199
- const envRel = join(rootDir, 'src/vite-env.d.ts')
200
- if (!existsSync(envRel)) {
200
+ async function checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail, cwd) {
201
+ const envAbs = join(cwd, rootDir, 'src/vite-env.d.ts')
202
+ if (!existsSync(envAbs)) {
201
203
  fail(
202
204
  `${prefix}немає src/vite-env.d.ts — додай файл з рядком /// <reference types="vite/client" /> ` +
203
205
  `(інакше TS/Volar не бачать типів для імпортів асетів: png, avif, css як URL).`
204
206
  )
205
207
  return
206
208
  }
207
- const envContent = await readFile(envRel, 'utf8')
209
+ const envContent = await readFile(envAbs, 'utf8')
208
210
  if (!VITE_CLIENT_REFERENCE_RE.test(envContent)) {
209
211
  fail(
210
212
  `${prefix}src/vite-env.d.ts має містити /// <reference types="vite/client" /> ` +
@@ -214,7 +216,7 @@ async function checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail)
214
216
  }
215
217
  passFn(`${prefix}src/vite-env.d.ts посилається на vite/client`)
216
218
 
217
- if (!existsSync(join(rootDir, 'jsconfig.json'))) {
219
+ if (!existsSync(join(cwd, rootDir, 'jsconfig.json'))) {
218
220
  fail(
219
221
  `${prefix}немає jsconfig.json у корені пакета — додай файл з "include": ["src/**/*"] тощо, ` +
220
222
  `щоб IDE підхопила vite-env.d.ts і .vue.`
@@ -268,15 +270,16 @@ function viteConfigHasVueInAutoImports(content) {
268
270
  * @param {(msg: string) => void} passFn callback при успішній перевірці
269
271
  * @param {(msg: string) => void} fail callback при помилці
270
272
  * @returns {Promise<{ hasVueAutoImport: boolean }>} ознака успішно сконфігурованого vue-auto-import (для checkVueImportViolations)
273
+ * @param {string} cwd корінь репозиторію
271
274
  */
272
- async function checkViteConfig(rootDir, prefix, passFn, fail) {
275
+ async function checkViteConfig(rootDir, prefix, passFn, fail, cwd) {
273
276
  const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
274
- const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
277
+ const viteConfig = configFiles.find(f => existsSync(join(cwd, rootDir, f)))
275
278
  if (!viteConfig) {
276
279
  fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
277
280
  return { hasVueAutoImport: false }
278
281
  }
279
- const content = await readFile(join(rootDir, viteConfig), 'utf8')
282
+ const content = await readFile(join(cwd, rootDir, viteConfig), 'utf8')
280
283
  if (ESBUILD_RE.test(content)) {
281
284
  fail(`${prefix}${viteConfig} містить 'esbuild' — заміни на 'rolldown'`)
282
285
  }
@@ -410,37 +413,39 @@ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, ha
410
413
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
411
414
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
412
415
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
416
+ * @param {string} cwd корінь репозиторію
413
417
  */
414
- async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
418
+ async function checkVuePackage(rootDir, ignorePaths, fail, passFn, cwd) {
415
419
  const prefix = `[${packageLabel(rootDir)}] `
416
420
  passFn(`${prefix}package.json залежності перевіряє npx @nitra/cursor fix → vue.package_json`)
417
421
 
418
- await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail)
422
+ await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail, cwd)
419
423
 
420
- const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
424
+ const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail, cwd)
421
425
  await checkVueImportViolations(
422
426
  rootDir,
423
- join(process.cwd(), rootDir),
427
+ join(cwd, rootDir),
424
428
  ignorePaths,
425
429
  hasVueAutoImport,
426
430
  prefix,
427
431
  passFn,
428
432
  fail
429
433
  )
430
- await checkVueNodeImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
431
- await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
434
+ await checkVueNodeImportViolations(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
435
+ await checkEsbuildMentions(rootDir, join(cwd, rootDir), ignorePaths, prefix, passFn, fail)
432
436
  }
433
437
 
434
438
  /**
435
439
  * Збирає корені пакетів, у яких у `dependencies` є `vue`.
436
440
  * @param {string[]} roots усі корені пакетів monorepo
437
441
  * @returns {Promise<string[]>} перелік пакетів з vue у dependencies
442
+ * @param {string} cwd корінь репозиторію
438
443
  */
439
- async function collectVueRoots(roots) {
444
+ async function collectVueRoots(roots, cwd) {
440
445
  /** @type {string[]} */
441
446
  const vueRoots = []
442
447
  for (const r of roots) {
443
- const p = join(r, 'package.json')
448
+ const p = join(cwd, r, 'package.json')
444
449
  if (!existsSync(p)) continue
445
450
  const pkg = JSON.parse(await readFile(p, 'utf8'))
446
451
  if (pkg.dependencies?.vue) vueRoots.push(r)
@@ -453,13 +458,15 @@ async function collectVueRoots(roots) {
453
458
  * @param {(msg: string) => void} pass pass callback
454
459
  * @param {(msg: string) => void} fail fail callback
455
460
  * @returns {Promise<void>}
461
+ * @param {string} cwd корінь репозиторію
456
462
  */
457
- async function checkVueVolarRecommendation(pass, fail) {
458
- if (!existsSync('.vscode/extensions.json')) {
463
+ async function checkVueVolarRecommendation(pass, fail, cwd) {
464
+ const extPath = join(cwd, '.vscode/extensions.json')
465
+ if (!existsSync(extPath)) {
459
466
  fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
460
467
  return
461
468
  }
462
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
469
+ const ext = JSON.parse(await readFile(extPath, 'utf8'))
463
470
  if (ext.recommendations?.includes('Vue.volar')) {
464
471
  pass('extensions.json містить Vue.volar')
465
472
  } else {
@@ -469,14 +476,15 @@ async function checkVueVolarRecommendation(pass, fail) {
469
476
 
470
477
  /**
471
478
  * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
479
+ * @param {string} [cwd] корінь репозиторію
472
480
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
473
481
  */
474
- export async function check() {
482
+ export async function check(cwd = process.cwd()) {
475
483
  const reporter = createCheckReporter()
476
484
  const { pass, fail } = reporter
477
485
 
478
- const roots = await getMonorepoPackageRootDirs()
479
- const vueRoots = await collectVueRoots(roots)
486
+ const roots = await getMonorepoPackageRootDirs(cwd)
487
+ const vueRoots = await collectVueRoots(roots, cwd)
480
488
 
481
489
  if (vueRoots.length === 0) {
482
490
  pass('Vue.volar: пропущено (у repo немає пакетів з vue у dependencies)')
@@ -484,11 +492,11 @@ export async function check() {
484
492
  return reporter.getExitCode()
485
493
  }
486
494
 
487
- await checkVueVolarRecommendation(pass, fail)
495
+ await checkVueVolarRecommendation(pass, fail, cwd)
488
496
 
489
- const ignorePaths = await loadCursorIgnorePaths(process.cwd())
497
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
490
498
  for (const r of vueRoots) {
491
- await checkVuePackage(r, ignorePaths, fail, pass)
499
+ await checkVuePackage(r, ignorePaths, fail, pass, cwd)
492
500
  }
493
501
 
494
502
  return reporter.getExitCode()