@nitra/cursor 1.17.0 → 1.17.1

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 (46) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/bin/n-cursor.js +16 -16
  3. package/package.json +2 -1
  4. package/rules/abie/fix.mjs +3 -3
  5. package/rules/adr/fix.mjs +3 -3
  6. package/rules/adr/js/hooks.mjs +6 -6
  7. package/rules/bun/fix.mjs +3 -3
  8. package/rules/capacitor/fix.mjs +3 -3
  9. package/rules/changelog/fix.mjs +3 -3
  10. package/rules/changelog/js/consistency.mjs +15 -15
  11. package/rules/ci4/fix.mjs +3 -3
  12. package/rules/docker/fix.mjs +3 -3
  13. package/rules/efes/fix.mjs +3 -3
  14. package/rules/feedback/fix.mjs +3 -3
  15. package/rules/ga/fix.mjs +3 -3
  16. package/rules/graphql/fix.mjs +3 -3
  17. package/rules/hasura/fix.mjs +3 -3
  18. package/rules/image-avif/fix.mjs +3 -3
  19. package/rules/image-compress/fix.mjs +3 -3
  20. package/rules/js-bun-db/fix.mjs +3 -3
  21. package/rules/js-bun-redis/fix.mjs +3 -3
  22. package/rules/js-lint/coverage/coverage.mjs +22 -20
  23. package/rules/js-lint/fix.mjs +3 -3
  24. package/rules/js-mssql/fix.mjs +3 -3
  25. package/rules/js-run/fix.mjs +3 -3
  26. package/rules/k8s/fix.mjs +3 -3
  27. package/rules/nginx-default-tpl/fix.mjs +3 -3
  28. package/rules/npm-module/fix.mjs +3 -3
  29. package/rules/php/fix.mjs +3 -3
  30. package/rules/rego/fix.mjs +3 -3
  31. package/rules/rust/coverage/coverage.mjs +19 -20
  32. package/rules/rust/fix.mjs +3 -3
  33. package/rules/rust/lib/has-cargo-toml.mjs +1 -3
  34. package/rules/security/fix.mjs +3 -3
  35. package/rules/style-lint/fix.mjs +3 -3
  36. package/rules/style-lint/js/tooling.mjs +1 -1
  37. package/rules/tauri/fix.mjs +3 -3
  38. package/rules/test/coverage/coverage.mjs +27 -25
  39. package/rules/test/fix.mjs +3 -3
  40. package/rules/test/js/location.mjs +1 -1
  41. package/rules/text/fix.mjs +3 -3
  42. package/rules/vue/fix.mjs +3 -3
  43. package/scripts/lib/run-rule-cli.mjs +11 -0
  44. package/scripts/lib/run-standard-rule.mjs +1 -1
  45. package/scripts/utils/with-lock.mjs +27 -16
  46. package/scripts/utils/worktree-fingerprint.mjs +10 -7
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
7
7
  */
8
+ import { spawnSync } from 'node:child_process'
8
9
  import { existsSync } from 'node:fs'
9
10
  import { mkdtemp, readFile, rm } from 'node:fs/promises'
10
11
  import { tmpdir } from 'node:os'
@@ -12,9 +13,9 @@ import { join } from 'node:path'
12
13
 
13
14
  /**
14
15
  * Резолвить cwd, у якому стоять JS-тести. Workspace-проєкти — перший workspace
15
- * (mlmail: app/), single-package — корінь.
16
+ * (наприклад: app/), single-package — корінь.
16
17
  * @param {string} cwd корінь проєкту
17
- * @returns {Promise<string|null>} абсолютний шлях або null якщо package.json відсутній
18
+ * @returns {Promise<string|null>} абсолютний шлях до JS-root або null без package.json
18
19
  */
19
20
  async function resolveJsRoot(cwd) {
20
21
  const rootPkgPath = join(cwd, 'package.json')
@@ -30,8 +31,8 @@ async function resolveJsRoot(cwd) {
30
31
 
31
32
  /**
32
33
  * Чи `scripts` містить coverage-сумісну команду.
33
- * @param {Record<string, string> | undefined} scripts
34
- * @returns {boolean}
34
+ * @param {Record<string, string> | undefined} scripts секція scripts з package.json
35
+ * @returns {boolean} true, якщо є test:coverage або test з --coverage
35
36
  */
36
37
  function hasCoverageScript(scripts) {
37
38
  if (!scripts || typeof scripts !== 'object') return false
@@ -42,8 +43,8 @@ function hasCoverageScript(scripts) {
42
43
 
43
44
  /**
44
45
  * Чи провайдер застосовний у поточному cwd.
45
- * @param {string} cwd
46
- * @returns {Promise<boolean>}
46
+ * @param {string} cwd корінь проєкту
47
+ * @returns {Promise<boolean>} true, якщо знайдено coverage-сумісний test-скрипт
47
48
  */
48
49
  export async function detect(cwd) {
49
50
  const jsRoot = await resolveJsRoot(cwd)
@@ -56,8 +57,8 @@ export async function detect(cwd) {
56
57
 
57
58
  /**
58
59
  * Парс lcov.info: сумує LF/LH (рядки) і FNF/FNH (функції) по всіх records.
59
- * @param {string} text
60
- * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}}
60
+ * @param {string} text вміст lcov.info
61
+ * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} агреговані totals
61
62
  */
62
63
  function parseLcov(text) {
63
64
  const acc = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
@@ -73,8 +74,8 @@ function parseLcov(text) {
73
74
  /**
74
75
  * Парс Stryker mutation.json: Killed+Timeout → caught; Survived+NoCoverage → до total.
75
76
  * Compile/Runtime errors виключаються з total.
76
- * @param {{files:Record<string,{mutants:Array<{status:string}>}>}} report
77
- * @returns {{caught:number,total:number}}
77
+ * @param {{files:Record<string,{mutants:Array<{status:string}>}>}} report розпарсений mutation.json
78
+ * @returns {{caught:number,total:number}} агрегований mutation score
78
79
  */
79
80
  function parseStrykerReport(report) {
80
81
  let caught = 0
@@ -93,20 +94,21 @@ function parseStrykerReport(report) {
93
94
  }
94
95
 
95
96
  /**
96
- * Default runner — спавнить реальні bun-команди. Замінюється у тестах.
97
+ * Default runner — спавнить реальні bun-команди через `node:child_process.spawnSync`
98
+ * (працює і в Node-runtime через shebang `n-cursor`, і в Bun). Замінюється у тестах.
97
99
  */
98
100
  const defaultRunner = {
99
- async runJsCoverage({ cwd, lcovDir }) {
100
- const proc = Bun.spawn(['bun', 'run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
101
+ runJsCoverage({ cwd, lcovDir }) {
102
+ const r = spawnSync('bun', ['run', 'test:coverage', '--coverage-reporter=lcov', `--coverage-dir=${lcovDir}`], {
101
103
  cwd,
102
- stdout: 'inherit',
103
- stderr: 'inherit'
104
+ stdio: 'inherit',
105
+ env: process.env
104
106
  })
105
- return proc.exited
107
+ return r.status ?? 1
106
108
  },
107
- async runStryker({ cwd }) {
108
- const proc = Bun.spawn(['bunx', 'stryker', 'run'], { cwd, stdout: 'inherit', stderr: 'inherit' })
109
- return proc.exited
109
+ runStryker({ cwd }) {
110
+ const r = spawnSync('bunx', ['stryker', 'run'], { cwd, stdio: 'inherit', env: process.env })
111
+ return r.status ?? 1
110
112
  }
111
113
  }
112
114
 
@@ -114,7 +116,7 @@ const defaultRunner = {
114
116
  * Збирає JS-метрики покриття + мутаційного тестування.
115
117
  * @param {string} cwd корінь проєкту
116
118
  * @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція для тестів
117
- * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>}
119
+ * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
118
120
  */
119
121
  export async function collect(cwd, opts = {}) {
120
122
  const runner = opts.runner ?? defaultRunner
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
package/rules/k8s/fix.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
package/rules/php/fix.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Контракт провайдера — у docs/superpowers/specs/2026-05-24-coverage-rule-design.md.
8
8
  */
9
+ import { spawnSync } from 'node:child_process'
9
10
  import { existsSync } from 'node:fs'
10
11
  import { mkdtemp, readFile, rm } from 'node:fs/promises'
11
12
  import { tmpdir } from 'node:os'
@@ -17,17 +18,17 @@ const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo', 't
17
18
 
18
19
  /**
19
20
  * Чи провайдер застосовний у поточному cwd.
20
- * @param {string} cwd
21
- * @returns {Promise<boolean>}
21
+ * @param {string} cwd корінь проєкту
22
+ * @returns {Promise<boolean>} true, якщо знайдено Cargo.toml у cwd або workspace-піддереві
22
23
  */
23
- export async function detect(cwd) {
24
- if (existsSync(join(cwd, 'Cargo.toml'))) return true
25
- return hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES)
24
+ export function detect(cwd) {
25
+ if (existsSync(join(cwd, 'Cargo.toml'))) return Promise.resolve(true)
26
+ return Promise.resolve(hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES))
26
27
  }
27
28
 
28
29
  /**
29
30
  * Знайти Cargo.toml: cwd/Cargo.toml або в одному з workspace-підкаталогів.
30
- * @param {string} cwd
31
+ * @param {string} cwd корінь проєкту
31
32
  * @returns {Promise<string>} абсолютний шлях до Cargo.toml
32
33
  */
33
34
  async function resolveCargoManifest(cwd) {
@@ -50,29 +51,27 @@ async function resolveCargoManifest(cwd) {
50
51
  }
51
52
 
52
53
  const defaultRunner = {
53
- async runLlvmCov({ manifestPath }) {
54
- const proc = Bun.spawn(['cargo', 'llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
55
- stdout: 'pipe',
56
- stderr: 'inherit'
54
+ runLlvmCov({ manifestPath }) {
55
+ const r = spawnSync('cargo', ['llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
56
+ stdio: ['inherit', 'pipe', 'inherit'],
57
+ env: process.env
57
58
  })
58
- const stdout = await new Response(proc.stdout).text()
59
- const exitCode = await proc.exited
60
- return { exitCode, stdout }
59
+ return { exitCode: r.status ?? 1, stdout: r.stdout?.toString('utf8') ?? '' }
61
60
  },
62
- async runCargoMutants({ manifestPath, outDir }) {
63
- const proc = Bun.spawn(['cargo', 'mutants', '--in-place', '-o', outDir, '--manifest-path', manifestPath], {
64
- stdout: 'inherit',
65
- stderr: 'inherit'
61
+ runCargoMutants({ manifestPath, outDir }) {
62
+ const r = spawnSync('cargo', ['mutants', '--in-place', '-o', outDir, '--manifest-path', manifestPath], {
63
+ stdio: 'inherit',
64
+ env: process.env
66
65
  })
67
- return proc.exited
66
+ return r.status ?? 1
68
67
  }
69
68
  }
70
69
 
71
70
  /**
72
71
  * Збирає Rust-метрики покриття + мутаційного тестування.
73
72
  * @param {string} cwd корінь проєкту
74
- * @param {{runner?: typeof defaultRunner}} [opts]
75
- * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>}
73
+ * @param {{runner?: typeof defaultRunner}} [opts] ін'єкція runner-а для тестів
74
+ * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
76
75
  */
77
76
  export async function collect(cwd, opts = {}) {
78
77
  const runner = opts.runner ?? defaultRunner
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -29,9 +29,7 @@ export function hasCargoTomlInTree(root, ignoredDirNames) {
29
29
  }
30
30
  for (const entry of entries) {
31
31
  if (entry.isFile() && entry.name === 'Cargo.toml') return true
32
- if (entry.isDirectory() && !ignoredDirNames.has(entry.name)) {
33
- if (walk(join(dir, entry.name))) return true
34
- }
32
+ if (entry.isDirectory() && !ignoredDirNames.has(entry.name) && walk(join(dir, entry.name))) return true
35
33
  }
36
34
  return false
37
35
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -25,7 +25,7 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
25
25
  * Альтернатива полю `stylelint` у `package.json` — зовнішній файл конфігу. Якщо
26
26
  * поля немає і файлу немає, фейлимося; якщо є хоч щось — пропускаємо. Поле
27
27
  * `stylelint.extends == "@nitra/stylelint-config"` сам формат — у Rego.
28
- * @param {import('../../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
28
+ * @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
29
29
  */
30
30
  async function checkStylelintConfigPresence(reporter) {
31
31
  const { pass, fail } = reporter
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -15,7 +15,7 @@
15
15
  import { existsSync } from 'node:fs'
16
16
  import { writeFile } from 'node:fs/promises'
17
17
  import { dirname, join } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
18
+ import { fileURLToPath, pathToFileURL } from 'node:url'
19
19
 
20
20
  import { readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
21
21
  import { withLock } from '../../../scripts/utils/with-lock.mjs'
@@ -25,9 +25,9 @@ const RULES_DIR = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
25
25
 
26
26
  /**
27
27
  * Сума двох coverage-totals.
28
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} a
29
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} b
30
- * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}}
28
+ * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} a перший subtotal
29
+ * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} b другий subtotal
30
+ * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} сумарні lines/functions
31
31
  */
32
32
  export function addCoverage(a, b) {
33
33
  return {
@@ -41,9 +41,9 @@ export function addCoverage(a, b) {
41
41
 
42
42
  /**
43
43
  * Сума двох mutation-counts.
44
- * @param {{caught:number,total:number}} a
45
- * @param {{caught:number,total:number}} b
46
- * @returns {{caught:number,total:number}}
44
+ * @param {{caught:number,total:number}} a перший subtotal
45
+ * @param {{caught:number,total:number}} b другий subtotal
46
+ * @returns {{caught:number,total:number}} сумарні caught/total
47
47
  */
48
48
  export function addMutation(a, b) {
49
49
  return { caught: a.caught + b.caught, total: a.total + b.total }
@@ -51,8 +51,8 @@ export function addMutation(a, b) {
51
51
 
52
52
  /**
53
53
  * Форматує covered/total як `XX.XX% (covered/total)`.
54
- * @param {{covered:number,total:number}} metric
55
- * @returns {string}
54
+ * @param {{covered:number,total:number}} metric метрика lines або functions
55
+ * @returns {string} відформатований рядок для таблиці COVERAGE.md
56
56
  */
57
57
  export function formatCoverage({ covered, total }) {
58
58
  const percent = total === 0 ? '—' : `${((covered / total) * 100).toFixed(2)}%`
@@ -61,8 +61,8 @@ export function formatCoverage({ covered, total }) {
61
61
 
62
62
  /**
63
63
  * Форматує мутаційний score як `XX.XX%`.
64
- * @param {{caught:number,total:number}} metric
65
- * @returns {string}
64
+ * @param {{caught:number,total:number}} metric агрегований mutation score
65
+ * @returns {string} відформатований score або прочерк
66
66
  */
67
67
  export function formatScore({ caught, total }) {
68
68
  return total === 0 ? '—' : `${((caught / total) * 100).toFixed(2)}%`
@@ -71,8 +71,8 @@ export function formatScore({ caught, total }) {
71
71
  /**
72
72
  * Рендерить таблицю покриття + мутаційного тестування як Markdown.
73
73
  * Без timestamp, щоб git diff рухався лише при зміні метрик.
74
- * @param {Array<{area:string, coverage:{lines:{covered:number,total:number},functions:{covered:number,total:number}}, mutation:{caught:number,total:number}}>} rows
75
- * @returns {string}
74
+ * @param {Array<{area:string, coverage:{lines:{covered:number,total:number},functions:{covered:number,total:number}}, mutation:{caught:number,total:number}}>} rows рядки провайдерів
75
+ * @returns {string} Markdown-таблиця з заголовком `# Coverage`
76
76
  */
77
77
  export function renderMarkdown(rows) {
78
78
  const lines = [
@@ -96,36 +96,38 @@ export function renderMarkdown(rows) {
96
96
  * - файлу немає (rule без coverage-провайдера),
97
97
  * - файл існує, але не експортує `detect` + `collect` як функції (наприклад,
98
98
  * `rules/test/coverage/coverage.mjs` — сам оркестратор, не провайдер).
99
- * @param {string} rulesDir
100
- * @param {string} ruleId
101
- * @returns {Promise<{detect:Function, collect:Function}|null>}
99
+ * @param {string} rulesDir корінь `npm/rules/`
100
+ * @param {string} ruleId id правила з `.n-cursor.json#rules`
101
+ * @returns {Promise<{detect:(cwd:string)=>Promise<boolean>, collect:(cwd:string)=>Promise<Array<object>>}|null>} provider-модуль або null
102
102
  */
103
103
  async function loadProvider(rulesDir, ruleId) {
104
104
  const providerPath = join(rulesDir, ruleId, 'coverage', 'coverage.mjs')
105
105
  if (!existsSync(providerPath)) return null
106
- const mod = await import(providerPath)
106
+ // eslint-disable-next-line no-unsanitized/method -- providerPath з join(rulesDir, ruleId, …), ruleId з конфігу
107
+ const mod = await import(pathToFileURL(providerPath).href)
107
108
  if (typeof mod.detect !== 'function' || typeof mod.collect !== 'function') return null
108
109
  return mod
109
110
  }
110
111
 
111
112
  /**
112
113
  * Будує підсумковий рядок «Разом» через сумування всіх coverage/mutation.
113
- * @param {Array<{area:string, coverage:object, mutation:object}>} rows
114
- * @returns {{area:string, coverage:object, mutation:{caught:number,total:number}}}
114
+ * @param {Array<{area:string, coverage:object, mutation:object}>} rows рядки провайдерів без totals
115
+ * @returns {{area:string, coverage:object, mutation:{caught:number,total:number}}} агрегований рядок «Разом»
115
116
  */
116
117
  function buildTotalsRow(rows) {
117
- const totalCoverage = rows.reduce((acc, row) => addCoverage(acc, row.coverage), {
118
- lines: { covered: 0, total: 0 },
119
- functions: { covered: 0, total: 0 }
120
- })
121
- const totalMutation = rows.reduce((acc, row) => addMutation(acc, row.mutation), { caught: 0, total: 0 })
118
+ let totalCoverage = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
119
+ let totalMutation = { caught: 0, total: 0 }
120
+ for (const row of rows) {
121
+ totalCoverage = addCoverage(totalCoverage, row.coverage)
122
+ totalMutation = addMutation(totalMutation, row.mutation)
123
+ }
122
124
  return { area: '**Разом**', coverage: totalCoverage, mutation: totalMutation }
123
125
  }
124
126
 
125
127
  /**
126
128
  * Виконує coverage-pipeline: discovery провайдерів за `.n-cursor.json#rules`,
127
129
  * detect+collect для кожного, агрегація, запис COVERAGE.md.
128
- * @param {{cwd?:string, rulesDir?:string}} [opts] ін'єкція для тестів
130
+ * @param {{cwd?:string, rulesDir?:string}} [opts] ін'єкція cwd/rulesDir для тестів
129
131
  * @returns {Promise<number>} exit code (0 OK, 1 коли жоден провайдер не дав даних)
130
132
  */
131
133
  export async function runCoverageSteps(opts = {}) {
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
@@ -18,7 +18,7 @@ const TESTS_DIR_NAME = 'tests'
18
18
  /**
19
19
  * Чи файл є JS-тестом (`*.test.mjs`).
20
20
  * @param {string} absPath абсолютний шлях
21
- * @returns {boolean}
21
+ * @returns {boolean} true для шляхів із суфіксом `.test.mjs`
22
22
  */
23
23
  function isTestFile(absPath) {
24
24
  return basename(absPath).endsWith('.test.mjs')
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }
package/rules/vue/fix.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
1
2
  import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
2
3
 
3
4
  /**
@@ -10,10 +11,9 @@ export function run(ctx) {
10
11
  return runStandardRule(import.meta.dirname, ctx)
11
12
  }
12
13
 
13
- if (import.meta.main) {
14
+ if (isRunAsCli()) {
14
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
15
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
16
- const { runRuleCli } = await import('../../scripts/lib/run-rule-cli.mjs')
17
- // eslint-disable-next-line unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
17
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
18
18
  process.exit(await runRuleCli(import.meta.dirname))
19
19
  }