@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.28.0] - 2026-05-27
8
+
9
+ ### BREAKING
10
+
11
+ - **`scripts/utils/test-helpers.mjs#withTmpCwd` видалено**. Замість нього — `withTmpDir(async dir => …)`, що **НЕ** мутує `process.cwd()`, а передає абсолютний шлях `dir` у callback. Усі callers (43 тестових файли у пакеті) переписано: `await writeJson(join(dir, …), …)`, `await ensureDir(join(dir, …))`, `execFile('git', […], { cwd: dir })`, `await check(dir)`. `writeJson` і `ensureDir` тепер вимагають абсолютних шляхів (кидають помилку на relative). Споживачі пакета — мігруйте за конвенцією з `test.mdc` (секція "Заборона `process.chdir` у тестах").
12
+ - **Production-API: 24 `check()`/`applies()` JS concerns приймають `cwd = process.cwd()` параметром** у `rules/{abie,adr,bun,capacitor,changelog,docker,ga,graphql,hasura,image-avif,image-compress,js-bun-db,js-lint,js-run,k8s,nginx-default-tpl,npm-module,rego,rust,security,style-lint,test,text,vue}/js/*.mjs`. Default зберігає CLI-сумісність (runner викликає без аргументів — default `process.cwd()` спрацює).
13
+
14
+ ### Fixed
15
+
16
+ - **Race у `process.cwd()` між паралельними vitest workers** (root cause). У default `pool: 'threads'` усі workers ділять один процес. Паралельні `withTmpCwd` ламали один одному cwd: `git init`+`git commit` із фікстури `rules/changelog/.../check.test.mjs` (з `user.name=test`, `user.email=test@test`, `-m 'init'`) потрапляли в реальний робочий репозиторій, де відбувався vitest run, і знищували `npm/package.json` (зменшували до `{"name":"mono"}`) + `npm/CHANGELOG.md` (зрізали до плейсхолдера). Race множив rogue commits/branches (`feat/x`, `feat/docs`, `feat/sync`) щоразу при запуску `bun run coverage`/Stryker. Усунено повним переписуванням `withTmpCwd` → `withTmpDir` (без `chdir`), `cwd` параметром у production функціях і defense-in-depth `pool: 'forks'`.
17
+ - **`rules/test/coverage/coverage.mjs:192`** — dynamic import шлях `../../scripts/coverage-fix.mjs` резолвив у неіснуючий `npm/rules/scripts/coverage-fix.mjs`. Виправлено на `../../../scripts/coverage-fix.mjs` (реальний файл — у `npm/scripts/`). Усуває ERR_MODULE_NOT_FOUND у `n-cursor coverage --fix` і прибирає потребу у stub-стратегії, що створювала race-небезпечні fs-артефакти у production tree `rules/scripts/`.
18
+
19
+ ### Added
20
+
21
+ - **`rules/test/js/no-process-chdir.mjs`** — JS concern: сканує `**/*.test.{js,mjs}` і падає на `process.chdir(`. Token-based regex (`/process\.chdir\s*\(/u`) — не зачіпає згадки у JSDoc. 8 unit-тестів.
22
+ - **`rules/test/js/vitest-config-pool-forks.mjs`** — JS concern: substring-перевірка `pool: 'forks'` у `vitest.config.js`. Defense-in-depth. 6 unit-тестів.
23
+ - **`rules/test/js/data/vitest_config/vitest.config.baseline.js`** — canonical baseline тепер містить `pool: 'forks'` з обґрунтуванням race-bug у docstring.
24
+ - **`rules/test/test.mdc` — секція "Заборона `process.chdir` у тестах"** із описом інциденту, контрактом `withTmpDir`, посиланнями на нові concern'и.
25
+
26
+ ### Changed
27
+
28
+ - **`scripts/utils/test-helpers.mjs`** — `withTmpDir(fn)` без `chdir`; `writeJson(absPath, data)` і `ensureDir(absPath)` валідують `isAbsolute`. Docstring описує інцидент.
29
+ - **`vitest.config.js`** — `pool: 'forks'` як defense-in-depth з повним коментарем.
30
+
7
31
  ## [1.27.9] - 2026-05-27
8
32
 
9
33
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.27.9",
3
+ "version": "1.28.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -8,10 +8,11 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
8
8
  import { isAbieRuleEnabled } from '../lib/enabled.mjs'
9
9
 
10
10
  /**
11
+ * @param {string} [cwd] корінь репозиторію
11
12
  * @returns {Promise<boolean>} `true` — правило застосовне; `false` — пропустити
12
13
  */
13
- export function applies() {
14
- return isAbieRuleEnabled(process.cwd())
14
+ export function applies(cwd = process.cwd()) {
15
+ return isAbieRuleEnabled(cwd)
15
16
  }
16
17
 
17
18
  /**
@@ -5,6 +5,7 @@
5
5
  * - `ua.env` → `abie-ua.internal` + `ua-*` namespace
6
6
  *
7
7
  * Файл `.env` без імені (локальний для розробника) — виключено.
8
+ * @param {string} [cwd] корінь репозиторію
8
9
  */
9
10
  import { readFile } from 'node:fs/promises'
10
11
  import { basename, relative } from 'node:path'
@@ -16,11 +17,12 @@ import { abieEnvNameFromBasename, collectAbieEnvFiles, validateAbieEnvInternalUr
16
17
 
17
18
  /**
18
19
  * @returns {Promise<number>} результат
20
+ * @param {string} [cwd] корінь репозиторію
19
21
  */
20
- export async function check() {
22
+ export async function check(cwd = process.cwd()) {
21
23
  const reporter = createCheckReporter()
22
24
  const { pass, fail } = reporter
23
- const root = process.cwd()
25
+ const root = cwd
24
26
 
25
27
  const ignorePaths = await loadCursorIgnorePaths(root)
26
28
  const envFiles = await collectAbieEnvFiles(root, ignorePaths)
@@ -12,12 +12,13 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
12
12
  const SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
13
13
 
14
14
  /**
15
+ * @param {string} [cwd] корінь репозиторію
15
16
  * @returns {Promise<number>} результат
16
17
  */
17
- export async function check() {
18
+ export async function check(cwd = process.cwd()) {
18
19
  const reporter = createCheckReporter()
19
20
  const { pass, fail } = reporter
20
- const root = process.cwd()
21
+ const root = cwd
21
22
 
22
23
  let entries
23
24
  try {
@@ -17,12 +17,13 @@ import { validateAbieHcModeline } from '../lib/hc-yaml.mjs'
17
17
  import { collectDeploymentDirs, findK8sYamlFiles } from '../lib/k8s-tree.mjs'
18
18
 
19
19
  /**
20
+ * @param {string} [cwd] корінь репозиторію
20
21
  * @returns {Promise<number>} результат
21
22
  */
22
- export async function check() {
23
+ export async function check(cwd = process.cwd()) {
23
24
  const reporter = createCheckReporter()
24
25
  const { pass, fail } = reporter
25
- const root = process.cwd()
26
+ const root = cwd
26
27
 
27
28
  const ignorePaths = await loadCursorIgnorePaths(root)
28
29
  const yamls = await findK8sYamlFiles(root, ignorePaths)
@@ -6,6 +6,7 @@
6
6
  * Для спільних сервісів (`auth-run-hl`, `file-link-hl`) у base-HTTPRoute пакета — кожен `backendRef`
7
7
  * має `namespace: dev`; в overlay patch — JSON6902 на `/spec/rules/…/backendRefs/…/namespace` зі
8
8
  * `value: ua`. Кількість patch-ів = кількість таких посилань у base.
9
+ * @param {string} [cwd] корінь репозиторію
9
10
  */
10
11
  import { readFile } from 'node:fs/promises'
11
12
  import { relative } from 'node:path'
@@ -27,11 +28,12 @@ import {
27
28
 
28
29
  /**
29
30
  * @returns {Promise<number>} результат
31
+ * @param {string} [cwd] корінь репозиторію
30
32
  */
31
- export async function check() {
33
+ export async function check(cwd = process.cwd()) {
32
34
  const reporter = createCheckReporter()
33
35
  const { pass, fail } = reporter
34
- const root = process.cwd()
36
+ const root = cwd
35
37
 
36
38
  const ignorePaths = await loadCursorIgnorePaths(root)
37
39
  const yamls = await findK8sYamlFiles(root, ignorePaths)
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Структурні обмеження JSON6902 (заборона `remove + add` на той самий path) перевіряє k8s.mdc /
6
6
  * `k8s.kustomization` rego — тут лише abie-специфічне.
7
+ * @param {string} [cwd] корінь репозиторію
7
8
  */
8
9
  import { readFile } from 'node:fs/promises'
9
10
  import { relative } from 'node:path'
@@ -17,11 +18,12 @@ import { abieOverlayK8sTreeHasDeployment, isUaKustomizationPath } from '../lib/o
17
18
 
18
19
  /**
19
20
  * @returns {Promise<number>} результат
21
+ * @param {string} [cwd] корінь репозиторію
20
22
  */
21
- export async function check() {
23
+ export async function check(cwd = process.cwd()) {
22
24
  const reporter = createCheckReporter()
23
25
  const { pass, fail } = reporter
24
- const root = process.cwd()
26
+ const root = cwd
25
27
 
26
28
  const ignorePaths = await loadCursorIgnorePaths(root)
27
29
  const yamls = await findK8sYamlFiles(root, ignorePaths)
@@ -32,8 +32,8 @@ const HOOK_ARTIFACTS = /** @type {const} */ ([
32
32
  { scriptName: 'normalize-decisions.sh', logName: 'normalize-decisions.log' }
33
33
  ])
34
34
 
35
- const PROJECT_SETTINGS_PATH = '.claude/settings.json'
36
- const CURSOR_HOOKS_PATH = '.cursor/hooks.json'
35
+ const PROJECT_SETTINGS_REL = '.claude/settings.json'
36
+ const CURSOR_HOOKS_REL = '.cursor/hooks.json'
37
37
  const EOL_RE = /\r?\n/u
38
38
 
39
39
  const here = dirname(fileURLToPath(import.meta.url))
@@ -83,26 +83,28 @@ function gitignoreLineCoversHookLog(line, logPath) {
83
83
  /**
84
84
  * Перевіряє наявність і канонічність одного hook-скрипта.
85
85
  * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
86
+ * @param {string} cwd корінь репозиторію
86
87
  * @param {string} scriptName базове ім'я скрипта (наприклад `capture-decisions.sh`)
87
88
  * @returns {Promise<void>}
88
89
  */
89
- async function checkHookScript(reporter, scriptName) {
90
+ async function checkHookScript(reporter, cwd, scriptName) {
90
91
  const { pass, fail } = reporter
91
- const projectPath = projectHookPath(scriptName)
92
+ const projectRel = projectHookPath(scriptName)
93
+ const projectAbs = join(cwd, projectRel)
92
94
  const bundledPath = join(BUNDLED_HOOKS_DIR, scriptName)
93
- if (!existsSync(projectPath)) {
94
- fail(`${projectPath} не існує — запусти \`npx @nitra/cursor\` (правило adr копіює канонічний скрипт)`)
95
+ if (!existsSync(projectAbs)) {
96
+ fail(`${projectRel} не існує — запусти \`npx @nitra/cursor\` (правило adr копіює канонічний скрипт)`)
95
97
  return
96
98
  }
97
99
  if (!existsSync(bundledPath)) {
98
100
  fail(`канонічний скрипт у пакеті не знайдено: ${bundledPath} — перевстанови @nitra/cursor`)
99
101
  return
100
102
  }
101
- const [project, bundled] = await Promise.all([readFile(projectPath, 'utf8'), readFile(bundledPath, 'utf8')])
103
+ const [project, bundled] = await Promise.all([readFile(projectAbs, 'utf8'), readFile(bundledPath, 'utf8')])
102
104
  if (project === bundled) {
103
- pass(`${projectPath} збігається з канонічним`)
105
+ pass(`${projectRel} збігається з канонічним`)
104
106
  } else {
105
- fail(`${projectPath} відрізняється від канонічного — запусти \`npx @nitra/cursor\` для повторного синку`)
107
+ fail(`${projectRel} відрізняється від канонічного — запусти \`npx @nitra/cursor\` для повторного синку`)
106
108
  }
107
109
  }
108
110
 
@@ -112,13 +114,14 @@ async function checkHookScript(reporter, scriptName) {
112
114
  * `capture-decisions.sh`; `settings.local.json` не дублює) валідують
113
115
  * `npm/policy/adr/settings_json/` і `npm/policy/adr/settings_local_json/`.
114
116
  * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
117
+ * @param {string} cwd корінь репозиторію
115
118
  */
116
- function checkProjectSettings(reporter) {
119
+ function checkProjectSettings(reporter, cwd) {
117
120
  const { pass, fail } = reporter
118
- if (existsSync(PROJECT_SETTINGS_PATH)) {
119
- pass(`${PROJECT_SETTINGS_PATH} є (Stop-hook перевіряє npx @nitra/cursor fix → adr.settings_json)`)
121
+ if (existsSync(join(cwd, PROJECT_SETTINGS_REL))) {
122
+ pass(`${PROJECT_SETTINGS_REL} є (Stop-hook перевіряє npx @nitra/cursor fix → adr.settings_json)`)
120
123
  } else {
121
- fail(`${PROJECT_SETTINGS_PATH} не існує — запусти \`npx @nitra/cursor\``)
124
+ fail(`${PROJECT_SETTINGS_REL} не існує — запусти \`npx @nitra/cursor\``)
122
125
  }
123
126
  }
124
127
 
@@ -165,25 +168,27 @@ function cursorConfigHasStopHook(config, marker) {
165
168
  /**
166
169
  * Перевіряє project-level Cursor hooks config для ADR stop-hooks.
167
170
  * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
171
+ * @param {string} cwd корінь репозиторію
168
172
  * @returns {Promise<void>}
169
173
  */
170
- async function checkCursorHooks(reporter) {
174
+ async function checkCursorHooks(reporter, cwd) {
171
175
  const { pass, fail } = reporter
172
- if (!existsSync(CURSOR_HOOKS_PATH)) {
173
- fail(`${CURSOR_HOOKS_PATH} не існує — запусти \`npx @nitra/cursor\``)
176
+ const cursorHooksAbs = join(cwd, CURSOR_HOOKS_REL)
177
+ if (!existsSync(cursorHooksAbs)) {
178
+ fail(`${CURSOR_HOOKS_REL} не існує — запусти \`npx @nitra/cursor\``)
174
179
  return
175
180
  }
176
- const config = await readJsonSafe(CURSOR_HOOKS_PATH)
181
+ const config = await readJsonSafe(cursorHooksAbs)
177
182
  if (config === null) {
178
- fail(`${CURSOR_HOOKS_PATH} не парситься як JSON — запусти \`npx @nitra/cursor\` або виправ файл`)
183
+ fail(`${CURSOR_HOOKS_REL} не парситься як JSON — запусти \`npx @nitra/cursor\` або виправ файл`)
179
184
  return
180
185
  }
181
186
  for (const { scriptName } of HOOK_ARTIFACTS) {
182
187
  const marker = projectHookPath(scriptName)
183
188
  if (cursorConfigHasStopHook(config, marker)) {
184
- pass(`${CURSOR_HOOKS_PATH} має stop-hook для ${marker}`)
189
+ pass(`${CURSOR_HOOKS_REL} має stop-hook для ${marker}`)
185
190
  } else {
186
- fail(`${CURSOR_HOOKS_PATH}: відсутній stop-hook для \`${marker}\` (adr.mdc)`)
191
+ fail(`${CURSOR_HOOKS_REL}: відсутній stop-hook для \`${marker}\` (adr.mdc)`)
187
192
  }
188
193
  }
189
194
  }
@@ -212,17 +217,19 @@ function checkGitignoreForLog(reporter, logName, gitignoreContent) {
212
217
  /**
213
218
  * Перевіряє `.gitignore` для всіх hook-логів одним проходом.
214
219
  * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
220
+ * @param {string} cwd корінь репозиторію
215
221
  * @returns {Promise<void>}
216
222
  */
217
- async function checkGitignore(reporter) {
223
+ async function checkGitignore(reporter, cwd) {
218
224
  const { fail } = reporter
219
- if (!existsSync('.gitignore')) {
225
+ const gitignoreAbs = join(cwd, '.gitignore')
226
+ if (!existsSync(gitignoreAbs)) {
220
227
  for (const { logName } of HOOK_ARTIFACTS) {
221
228
  fail(`.gitignore не існує — додай рядок \`${projectLogPath(logName)}\``)
222
229
  }
223
230
  return
224
231
  }
225
- const content = await readFile('.gitignore', 'utf8')
232
+ const content = await readFile(gitignoreAbs, 'utf8')
226
233
  for (const { logName } of HOOK_ARTIFACTS) {
227
234
  checkGitignoreForLog(reporter, logName, content)
228
235
  }
@@ -273,16 +280,17 @@ function checkLlmCliAvailable(reporter) {
273
280
 
274
281
  /**
275
282
  * Перевіряє відповідність проєкту правилам adr.mdc.
283
+ * @param {string} [cwd] корінь репозиторію
276
284
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
277
285
  */
278
- export async function check() {
286
+ export async function check(cwd = process.cwd()) {
279
287
  const reporter = createCheckReporter()
280
288
  for (const { scriptName } of HOOK_ARTIFACTS) {
281
- await checkHookScript(reporter, scriptName)
289
+ await checkHookScript(reporter, cwd, scriptName)
282
290
  }
283
- checkProjectSettings(reporter)
284
- await checkCursorHooks(reporter)
285
- await checkGitignore(reporter)
291
+ checkProjectSettings(reporter, cwd)
292
+ await checkCursorHooks(reporter, cwd)
293
+ await checkGitignore(reporter, cwd)
286
294
  checkLlmCliAvailable(reporter)
287
295
  return reporter.getExitCode()
288
296
  }
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { existsSync } from 'node:fs'
21
21
  import { readFile } from 'node:fs/promises'
22
+ import { join } from 'node:path'
22
23
 
23
24
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
24
25
 
@@ -31,13 +32,15 @@ const WHITESPACE_RE = /\s+/u
31
32
 
32
33
  /**
33
34
  * Зчитує `rules` та `disable-rules` з `.n-cursor.json`.
35
+ * @param {string} cwd корінь репозиторію
34
36
  * @returns {Promise<{ rules: Set<string>, disabled: Set<string> }>} активні правила і явно вимкнені
35
37
  */
36
- async function loadNCursorRules() {
38
+ async function loadNCursorRules(cwd) {
37
39
  const empty = { rules: new Set(), disabled: new Set() }
38
- if (!existsSync('.n-cursor.json')) return empty
40
+ const cfgPath = join(cwd, '.n-cursor.json')
41
+ if (!existsSync(cfgPath)) return empty
39
42
  try {
40
- const raw = JSON.parse(await readFile('.n-cursor.json', 'utf8'))
43
+ const raw = JSON.parse(await readFile(cfgPath, 'utf8'))
41
44
  const list = Array.isArray(raw?.rules) ? raw.rules.map(String) : []
42
45
  const disabled = Array.isArray(raw?.['disable-rules']) ? raw['disable-rules'].map(String) : []
43
46
  return { rules: new Set(list), disabled: new Set(disabled) }
@@ -157,45 +160,47 @@ function checkCursorRuleScripts(reporter, scripts, cursorRules) {
157
160
 
158
161
  /**
159
162
  * Перевіряє відповідність проєкту правилам bun.mdc
163
+ * @param {string} [cwd] корінь репозиторію
160
164
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
161
165
  */
162
- export async function check() {
166
+ export async function check(cwd = process.cwd()) {
163
167
  const reporter = createCheckReporter()
164
168
  const { pass, fail } = reporter
165
169
 
166
170
  for (const f of ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.yarnrc.yml']) {
167
- if (existsSync(f)) {
171
+ if (existsSync(join(cwd, f))) {
168
172
  fail(`Знайдено заборонений файл: ${f} — видали його`)
169
173
  } else {
170
174
  pass(`Немає ${f}`)
171
175
  }
172
176
  }
173
177
 
174
- if (existsSync('.yarn')) {
178
+ if (existsSync(join(cwd, '.yarn'))) {
175
179
  fail('Знайдено директорію .yarn — видали її')
176
180
  } else {
177
181
  pass('Немає .yarn/')
178
182
  }
179
- if (existsSync('bun.lock')) {
183
+ if (existsSync(join(cwd, 'bun.lock'))) {
180
184
  pass('bun.lock є')
181
185
  } else {
182
186
  fail('Відсутній bun.lock — запусти bun i')
183
187
  }
184
188
 
185
- if (existsSync('bunfig.toml')) {
189
+ if (existsSync(join(cwd, 'bunfig.toml'))) {
186
190
  pass('bunfig.toml є (структуру перевіряє npx @nitra/cursor fix → bun.bunfig)')
187
191
  } else {
188
192
  fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
189
193
  }
190
194
 
191
- const cursorRules = await loadNCursorRules()
195
+ const cursorRules = await loadNCursorRules(cwd)
192
196
 
193
- if (!existsSync('package.json')) {
197
+ const pkgPath = join(cwd, 'package.json')
198
+ if (!existsSync(pkgPath)) {
194
199
  fail('Відсутній package.json у корені')
195
200
  return reporter.getExitCode()
196
201
  }
197
202
 
198
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
203
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
199
204
  const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
200
205
  checkCursorRuleScripts(reporter, scripts, cursorRules)
201
206
 
@@ -429,12 +429,13 @@ async function isIosCocoaPodsExemptByNitraConfig(root) {
429
429
  }
430
430
 
431
431
  /**
432
+ * @param {string} [cwd] корінь репозиторію
432
433
  * @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
433
434
  */
434
- export async function check() {
435
+ export async function check(cwd = process.cwd()) {
435
436
  const reporter = createCheckReporter()
436
437
  const { pass, fail, getExitCode } = reporter
437
- const root = process.cwd()
438
+ const root = cwd
438
439
 
439
440
  const acc = { byPath: new Map(), anyCapacitor: false }
440
441
  await collectCapacitorDataFromAllPackageJson(root, acc)