@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
@@ -53,13 +53,14 @@ const REGISTRY_TIMEOUT_MS = 10_000
53
53
  const LEADING_DOTSLASH_RE = /^\.\//
54
54
 
55
55
  /**
56
- * Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
56
+ * Тихо запускає `git` у заданому `cwd` і повертає stdout або `null` при будь-якій помилці.
57
57
  * @param {string[]} args аргументи `git`
58
+ * @param {string} cwd робочий каталог процесу
58
59
  * @returns {Promise<string | null>} результат
59
60
  */
60
- async function gitOrNull(args) {
61
+ async function gitOrNull(args, cwd) {
61
62
  try {
62
- const { stdout } = await execFileAsync('git', args)
63
+ const { stdout } = await execFileAsync('git', args, { cwd })
63
64
  return stdout
64
65
  } catch {
65
66
  return null
@@ -67,18 +68,20 @@ async function gitOrNull(args) {
67
68
  }
68
69
 
69
70
  /**
71
+ * @param {string} cwd робочий каталог
70
72
  * @returns {Promise<boolean>} результат
71
73
  */
72
- async function isInsideGitRepo() {
73
- const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
74
+ async function isInsideGitRepo(cwd) {
75
+ const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'], cwd)
74
76
  return typeof out === 'string' && out.trim() === 'true'
75
77
  }
76
78
 
77
79
  /**
80
+ * @param {string} cwd робочий каталог
78
81
  * @returns {Promise<string | null>} результат
79
82
  */
80
- async function currentBranchName() {
81
- const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
83
+ async function currentBranchName(cwd) {
84
+ const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'], cwd)
82
85
  return typeof out === 'string' ? out.trim() : null
83
86
  }
84
87
 
@@ -93,20 +96,22 @@ function baseRefLabel(ref) {
93
96
  /**
94
97
  * @param {string} ancestor предок
95
98
  * @param {string} descendant нащадок
99
+ * @param {string} cwd робочий каталог
96
100
  * @returns {Promise<boolean>} результат
97
101
  */
98
- async function isGitAncestor(ancestor, descendant) {
99
- const out = await gitOrNull(['merge-base', '--is-ancestor', ancestor, descendant])
102
+ async function isGitAncestor(ancestor, descendant, cwd) {
103
+ const out = await gitOrNull(['merge-base', '--is-ancestor', ancestor, descendant], cwd)
100
104
  return typeof out === 'string' && out.trim() === 'true'
101
105
  }
102
106
 
103
107
  /**
104
108
  * @param {string} branchName локальна або remote-tracking гілка
109
+ * @param {string} cwd робочий каталог
105
110
  * @returns {Promise<string | null>} ref для git або null
106
111
  */
107
- async function resolveBranchRef(branchName) {
112
+ async function resolveBranchRef(branchName, cwd) {
108
113
  for (const ref of [branchName, `origin/${branchName}`]) {
109
- const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
114
+ const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref], cwd)
110
115
  if (typeof out === 'string' && out.trim().length > 0) {
111
116
  return ref
112
117
  }
@@ -125,11 +130,12 @@ function isChangelogIgnoredPath(relPath) {
125
130
 
126
131
  /**
127
132
  * @param {string} relPath параметр
133
+ * @param {string} cwd робочий каталог
128
134
  * @returns {Promise<boolean>} результат
129
135
  */
130
- async function isPathGitIgnored(relPath) {
136
+ async function isPathGitIgnored(relPath, cwd) {
131
137
  try {
132
- await execFileAsync('git', ['check-ignore', '-q', '--', relPath])
138
+ await execFileAsync('git', ['check-ignore', '-q', '--', relPath], { cwd })
133
139
  return true
134
140
  } catch {
135
141
  return false
@@ -138,10 +144,11 @@ async function isPathGitIgnored(relPath) {
138
144
 
139
145
  /**
140
146
  * @param {string} baseRef параметр
147
+ * @param {string} cwd робочий каталог
141
148
  * @returns {Promise<string | null>} результат
142
149
  */
143
- async function resolveMergeBase(baseRef) {
144
- const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
150
+ async function resolveMergeBase(baseRef, cwd) {
151
+ const out = await gitOrNull(['merge-base', baseRef, 'HEAD'], cwd)
145
152
  if (typeof out !== 'string') return null
146
153
  const sha = out.trim()
147
154
  return sha.length > 0 ? sha : null
@@ -150,22 +157,23 @@ async function resolveMergeBase(baseRef) {
150
157
  /**
151
158
  * Точка порівняння git для changelog (ref або SHA для `git diff` / `git show`).
152
159
  * @param {string | null} branch поточна гілка
160
+ * @param {string} cwd робочий каталог
153
161
  * @returns {Promise<{ ref: string, label: string } | null>} результат
154
162
  */
155
- async function resolveChangelogComparisonPoint(branch) {
163
+ async function resolveChangelogComparisonPoint(branch, cwd) {
156
164
  if (branch === LOCAL_ONLY_SKIP_BRANCH) {
157
165
  return null
158
166
  }
159
167
 
160
168
  if (branch === 'main') {
161
- const originMainRaw = await gitOrNull(['rev-parse', '--verify', '--quiet', 'origin/main'])
169
+ const originMainRaw = await gitOrNull(['rev-parse', '--verify', '--quiet', 'origin/main'], cwd)
162
170
  const originMainSha = originMainRaw?.trim()
163
- const headRaw = await gitOrNull(['rev-parse', 'HEAD'])
171
+ const headRaw = await gitOrNull(['rev-parse', 'HEAD'], cwd)
164
172
  const headSha = headRaw?.trim()
165
- if (originMainSha && headSha && (originMainSha === headSha || (await isGitAncestor('origin/main', 'HEAD')))) {
173
+ if (originMainSha && headSha && (originMainSha === headSha || (await isGitAncestor('origin/main', 'HEAD', cwd)))) {
166
174
  return { ref: 'origin/main', label: 'main' }
167
175
  }
168
- const parent = await gitOrNull(['rev-parse', '--verify', '--quiet', 'HEAD~1'])
176
+ const parent = await gitOrNull(['rev-parse', '--verify', '--quiet', 'HEAD~1'], cwd)
169
177
  if (typeof parent === 'string' && parent.trim().length > 0) {
170
178
  return { ref: parent.trim(), label: 'main~1' }
171
179
  }
@@ -173,11 +181,11 @@ async function resolveChangelogComparisonPoint(branch) {
173
181
  }
174
182
 
175
183
  for (const name of FEATURE_BASE_BRANCH_CANDIDATES) {
176
- const baseRef = await resolveBranchRef(name)
184
+ const baseRef = await resolveBranchRef(name, cwd)
177
185
  if (!baseRef) {
178
186
  continue
179
187
  }
180
- const mergeBase = await resolveMergeBase(baseRef)
188
+ const mergeBase = await resolveMergeBase(baseRef, cwd)
181
189
  if (!mergeBase) {
182
190
  continue
183
191
  }
@@ -215,11 +223,12 @@ function splitNulPaths(nulSeparated) {
215
223
  /**
216
224
  * @param {string} baseRef параметр
217
225
  * @param {string[]} pathspec параметр
226
+ * @param {string} cwd робочий каталог
218
227
  * @returns {Promise<string[]>} результат
219
228
  */
220
- async function listChangedPathsAgainstBase(baseRef, pathspec) {
221
- const diffOut = await gitOrNull(['diff', '--name-only', '-z', baseRef, '--', ...pathspec])
222
- const untrackedOut = await gitOrNull(['ls-files', '--others', '--exclude-standard', '-z', '--', ...pathspec])
229
+ async function listChangedPathsAgainstBase(baseRef, pathspec, cwd) {
230
+ const diffOut = await gitOrNull(['diff', '--name-only', '-z', baseRef, '--', ...pathspec], cwd)
231
+ const untrackedOut = await gitOrNull(['ls-files', '--others', '--exclude-standard', '-z', '--', ...pathspec], cwd)
223
232
  return [...new Set([...splitNulPaths(diffOut), ...splitNulPaths(untrackedOut)])]
224
233
  }
225
234
 
@@ -227,16 +236,17 @@ async function listChangedPathsAgainstBase(baseRef, pathspec) {
227
236
  * @param {string} baseRef параметр
228
237
  * @param {string} ws параметр
229
238
  * @param {string[]} subWorkspaces параметр
239
+ * @param {string} cwd робочий каталог
230
240
  * @returns {Promise<boolean>} результат
231
241
  */
232
- async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces) {
242
+ async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces, cwd) {
233
243
  const pathspec = pathspecForWorkspace(ws, subWorkspaces)
234
- const paths = await listChangedPathsAgainstBase(baseRef, pathspec)
244
+ const paths = await listChangedPathsAgainstBase(baseRef, pathspec, cwd)
235
245
  for (const p of paths) {
236
246
  if (isChangelogIgnoredPath(p)) {
237
247
  continue
238
248
  }
239
- if (await isPathGitIgnored(p)) {
249
+ if (await isPathGitIgnored(p, cwd)) {
240
250
  continue
241
251
  }
242
252
  return true
@@ -248,11 +258,12 @@ async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces
248
258
  * Версія з маніфесту на `baseRef`.
249
259
  * @param {string} baseRef параметр
250
260
  * @param {import('../lib/package-manifest.mjs').PackageManifest} manifest параметр
261
+ * @param {string} cwd робочий каталог
251
262
  * @returns {Promise<string | null>} результат
252
263
  */
253
- async function readBaseVersion(baseRef, manifest) {
264
+ async function readBaseVersion(baseRef, manifest, cwd) {
254
265
  const wsPath = manifest.ws === '.' ? manifest.manifestRel : `${manifest.ws}/${manifest.manifestRel}`
255
- const out = await gitOrNull(['show', `${baseRef}:${wsPath}`])
266
+ const out = await gitOrNull(['show', `${baseRef}:${wsPath}`], cwd)
256
267
  if (out === null) return null
257
268
  if (manifest.kind === 'npm') {
258
269
  try {
@@ -356,21 +367,23 @@ function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
356
367
  * @param {string} version параметр
357
368
  * @param {(msg: string) => void} pass параметр
358
369
  * @param {(msg: string) => void} fail параметр
370
+ * @param {string} cwd робочий каталог
359
371
  * @returns {Promise<boolean>} результат
360
372
  */
361
- async function verifyChangelogEntry(ws, version, pass, fail) {
373
+ async function verifyChangelogEntry(ws, version, pass, fail, cwd) {
362
374
  const label = ws === '.' ? '<root>' : ws
363
- const changelogPath = join(ws, 'CHANGELOG.md')
364
- if (!existsSync(changelogPath)) {
365
- fail(`${label}: відсутній ${changelogPath} (Keep a Changelog, див. n-changelog.mdc)`)
375
+ const changelogRel = join(ws, 'CHANGELOG.md')
376
+ const changelogAbs = join(cwd, changelogRel)
377
+ if (!existsSync(changelogAbs)) {
378
+ fail(`${label}: відсутній ${changelogRel} (Keep a Changelog, див. n-changelog.mdc)`)
366
379
  return false
367
380
  }
368
- const text = await readFile(changelogPath, 'utf8')
381
+ const text = await readFile(changelogAbs, 'utf8')
369
382
  if (changelogHasVersionEntry(text, version)) {
370
- pass(`${changelogPath}: знайдено запис для версії ${version}`)
383
+ pass(`${changelogRel}: знайдено запис для версії ${version}`)
371
384
  return true
372
385
  }
373
- fail(`${changelogPath}: відсутній запис для ${version} (формат "## [${version}] - YYYY-MM-DD")`)
386
+ fail(`${changelogRel}: відсутній запис для ${version} (формат "## [${version}] - YYYY-MM-DD")`)
374
387
  return false
375
388
  }
376
389
 
@@ -388,19 +401,20 @@ function workspaceLabel(manifest) {
388
401
  * @param {string[]} subWorkspaces параметр
389
402
  * @param {(msg: string) => void} pass параметр
390
403
  * @param {(msg: string) => void} fail параметр
404
+ * @param {string} cwd робочий каталог
391
405
  * @returns {Promise<void>} результат
392
406
  */
393
- async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail) {
407
+ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail, cwd) {
394
408
  const label = workspaceLabel(manifest)
395
409
  const mf = manifestFilePath(manifest.ws, manifest)
396
- if (!(await isInsideGitRepo())) {
410
+ if (!(await isInsideGitRepo(cwd))) {
397
411
  return
398
412
  }
399
413
 
400
- const branch = await currentBranchName()
414
+ const branch = await currentBranchName(cwd)
401
415
 
402
416
  if (branch === LOCAL_ONLY_SKIP_BRANCH) {
403
- if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces)) {
417
+ if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd)) {
404
418
  fail(
405
419
  `${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
406
420
  `Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
@@ -409,15 +423,18 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
409
423
  return
410
424
  }
411
425
 
412
- const comparison = await resolveChangelogComparisonPoint(branch)
413
- if (comparison && (await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) {
414
- const Vbase = await readBaseVersion(comparison.ref, manifest)
426
+ const comparison = await resolveChangelogComparisonPoint(branch, cwd)
427
+ if (
428
+ comparison &&
429
+ (await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces, cwd))
430
+ ) {
431
+ const Vbase = await readBaseVersion(comparison.ref, manifest, cwd)
415
432
  const baseLabel = comparison.label
416
433
  if (Vbase === null) {
417
434
  pass(
418
435
  `${label}: новий registry-published воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`
419
436
  )
420
- await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail)
437
+ await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd)
421
438
  checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
422
439
  } else if (Vbase === Vcurrent) {
423
440
  fail(
@@ -429,7 +446,7 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
429
446
  }
430
447
  }
431
448
 
432
- if (branch === 'main' && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces))) {
449
+ if (branch === 'main' && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd))) {
433
450
  fail(
434
451
  `${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
435
452
  `Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
@@ -443,9 +460,10 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
443
460
  * @param {(name: string, kind?: import('../lib/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion параметр
444
461
  * @param {(msg: string) => void} pass параметр
445
462
  * @param {(msg: string) => void} fail параметр
463
+ * @param {string} cwd робочий каталог
446
464
  * @returns {Promise<void>} результат
447
465
  */
448
- async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail) {
466
+ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail, cwd) {
449
467
  const label = workspaceLabel(manifest)
450
468
  const mf = manifestFilePath(manifest.ws, manifest)
451
469
  const Vcurrent = manifest.version
@@ -465,11 +483,11 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
465
483
  }
466
484
  if (Vpublished === Vcurrent) {
467
485
  pass(`${label}: ${name}@${Vcurrent} збігається з реєстром — перевіряємо git на незрелізні зміни`)
468
- await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail)
486
+ await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail, cwd)
469
487
  return
470
488
  }
471
489
  pass(`${label}: ${name} — нова локальна версія (${Vpublished} → ${Vcurrent})`)
472
- await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail)
490
+ await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd)
473
491
  checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
474
492
  }
475
493
 
@@ -479,8 +497,9 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
479
497
  * @param {string} baseLabel параметр
480
498
  * @param {(msg: string) => void} pass параметр
481
499
  * @param {(msg: string) => void} fail параметр
500
+ * @param {string} cwd робочий каталог
482
501
  */
483
- async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail) {
502
+ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail, cwd) {
484
503
  const label = workspaceLabel(manifest)
485
504
  const mf = manifestFilePath(manifest.ws, manifest)
486
505
  const Vcurrent = manifest.version
@@ -488,10 +507,10 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
488
507
  fail(`${label}: у ${mf} відсутнє поле version (потрібне для запису в CHANGELOG)`)
489
508
  return
490
509
  }
491
- const Vbase = await readBaseVersion(comparisonRef, manifest)
510
+ const Vbase = await readBaseVersion(comparisonRef, manifest, cwd)
492
511
  if (Vbase === null) {
493
512
  pass(`${label}: новий воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`)
494
- if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail))) return
513
+ if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd))) return
495
514
  checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
496
515
  return
497
516
  }
@@ -502,7 +521,7 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
502
521
  return
503
522
  }
504
523
  pass(`${label}: version підвищено (${Vbase} → ${Vcurrent})`)
505
- if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail))) return
524
+ if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd))) return
506
525
  checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
507
526
  }
508
527
 
@@ -511,20 +530,21 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
511
530
  * @param {string[]} subWorkspaces параметр
512
531
  * @param {(msg: string) => void} pass параметр
513
532
  * @param {(msg: string) => void} fail параметр
533
+ * @param {string} cwd робочий каталог
514
534
  */
515
- async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
535
+ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail, cwd) {
516
536
  if (localOnly.length === 0) return
517
537
 
518
- if (!(await isInsideGitRepo())) {
538
+ if (!(await isInsideGitRepo(cwd))) {
519
539
  pass('changelog: не git-репозиторій — local-only перевірку пропущено')
520
540
  return
521
541
  }
522
- const branch = await currentBranchName()
542
+ const branch = await currentBranchName(cwd)
523
543
  if (branch === LOCAL_ONLY_SKIP_BRANCH) {
524
544
  pass('changelog: поточна гілка = dev — local-only перевірку пропущено')
525
545
  return
526
546
  }
527
- const comparison = await resolveChangelogComparisonPoint(branch)
547
+ const comparison = await resolveChangelogComparisonPoint(branch, cwd)
528
548
  if (!comparison) {
529
549
  pass('changelog: ref dev/main (та origin/*) не знайдено — local-only перевірку пропущено')
530
550
  return
@@ -532,9 +552,9 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
532
552
 
533
553
  let checkedAny = false
534
554
  for (const manifest of localOnly) {
535
- if (!(await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) continue
555
+ if (!(await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces, cwd))) continue
536
556
  checkedAny = true
537
- await checkLocalOnlyChangedWorkspace(comparison.ref, manifest, comparison.label, pass, fail)
557
+ await checkLocalOnlyChangedWorkspace(comparison.ref, manifest, comparison.label, pass, fail, cwd)
538
558
  }
539
559
  if (!checkedAny) {
540
560
  pass(`changelog: local-only воркспейси без змін відносно ${comparison.label}`)
@@ -544,14 +564,16 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
544
564
  /**
545
565
  * @param {object} [opts] опції перевірки
546
566
  * @param {(name: string, kind?: import('../lib/package-manifest.mjs').PackageKind) => Promise<string | null>} [opts.getPublishedVersion] перевизначення npm/PyPI у тестах
567
+ * @param {string} [opts.cwd] корінь репозиторію (за замовчуванням `process.cwd()`)
547
568
  * @returns {Promise<number>} exit-код перевірки
548
569
  */
549
570
  export async function check(opts = {}) {
550
571
  const reporter = createCheckReporter()
551
572
  const { pass, fail } = reporter
552
573
  const getPublishedVersion = opts.getPublishedVersion ?? createDefaultGetPublishedVersion()
574
+ const cwd = opts.cwd ?? process.cwd()
553
575
 
554
- const workspaces = await getMonorepoProjectRootDirs(process.cwd())
576
+ const workspaces = await getMonorepoProjectRootDirs(cwd)
555
577
  const subWorkspaces = workspaces.filter(w => w !== '.')
556
578
  // Корінь монорепо (`.` за наявності підпакетів) — це glue/конфіг/tooling, а не логіка
557
579
  // продукту: власного CHANGELOG він не веде, помітні зміни документують підпакети.
@@ -573,7 +595,7 @@ export async function check(opts = {}) {
573
595
  )
574
596
  continue
575
597
  }
576
- const manifest = await readPackageManifest(ws)
598
+ const manifest = await readPackageManifest(ws, cwd)
577
599
  if (!manifest) {
578
600
  continue
579
601
  }
@@ -585,10 +607,10 @@ export async function check(opts = {}) {
585
607
  }
586
608
 
587
609
  for (const manifest of published) {
588
- await checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail)
610
+ await checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail, cwd)
589
611
  }
590
612
 
591
- await runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail)
613
+ await runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail, cwd)
592
614
 
593
615
  return reporter.getExitCode()
594
616
  }
@@ -71,11 +71,12 @@ export function parsePyprojectFields(text) {
71
71
  }
72
72
 
73
73
  /**
74
- * @param {string} ws шлях воркспейсу
74
+ * @param {string} ws шлях воркспейсу (відносно `cwd`)
75
+ * @param {string} [cwd] корінь репозиторію (за замовчуванням `process.cwd()`)
75
76
  * @returns {Promise<PackageManifest | null>} маніфест пакета або null
76
77
  */
77
- export async function readPackageManifest(ws) {
78
- const pkgPath = join(ws, 'package.json')
78
+ export async function readPackageManifest(ws, cwd = process.cwd()) {
79
+ const pkgPath = join(cwd, ws, 'package.json')
79
80
  if (existsSync(pkgPath)) {
80
81
  try {
81
82
  const parsed = JSON.parse(await readFile(pkgPath, 'utf8'))
@@ -99,7 +100,7 @@ export async function readPackageManifest(ws) {
99
100
  }
100
101
  }
101
102
 
102
- const pyPath = join(ws, 'pyproject.toml')
103
+ const pyPath = join(cwd, ws, 'pyproject.toml')
103
104
  if (!existsSync(pyPath)) {
104
105
  return null
105
106
  }
@@ -284,13 +284,14 @@ export function getNonRootRuntimeHint(fileContent) {
284
284
 
285
285
  /**
286
286
  * Перевіряє Dockerfile / Containerfile через hadolint (docker.mdc).
287
+ * @param {string} [cwd] корінь репозиторію
287
288
  * @returns {Promise<number>} 0 — все OK, 1 — є зауваження або помилка запуску
288
289
  */
289
- export async function check() {
290
+ export async function check(cwd = process.cwd()) {
290
291
  const reporter = createCheckReporter()
291
292
  const { pass } = reporter
292
293
 
293
- const root = process.cwd()
294
+ const root = cwd
294
295
  const ignorePaths = await loadCursorIgnorePaths(root)
295
296
  const files = await findDockerfilePaths(root, ignorePaths)
296
297