@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
@@ -7,6 +7,7 @@
7
7
  * Library-mode виклик з CLI orchestration — інше: див. `runStandardRule` + `fix.mjs::run(ctx)`.
8
8
  */
9
9
  import { basename } from 'node:path'
10
+ import { pathToFileURL } from 'node:url'
10
11
 
11
12
  import { isRuleEnabled, readNCursorConfigLite } from './read-n-cursor-config-lite.mjs'
12
13
  import { runStandardRule } from './run-standard-rule.mjs'
@@ -14,6 +15,16 @@ import { getOrCreateWalkCache } from '../utils/walk-cache.mjs'
14
15
 
15
16
  const PACKAGE_NAME = '@nitra/cursor'
16
17
 
18
+ /**
19
+ * Чи поточний модуль запущено як CLI entry-point (`bun rules/<id>/fix.mjs`).
20
+ * @returns {boolean} true, коли `import.meta.url` збігається з `process.argv[1]`
21
+ */
22
+ export function isRunAsCli() {
23
+ const entry = process.argv[1]
24
+ if (!entry) return false
25
+ return import.meta.url === pathToFileURL(entry).href
26
+ }
27
+
17
28
  /**
18
29
  * @param {string} ruleDir абсолютний шлях до `rules/<id>/`
19
30
  * @returns {Promise<number>} 0 — OK або правило не enabled; 1 — порушення
@@ -32,7 +32,7 @@ import { withLock } from '../utils/with-lock.mjs'
32
32
  * @param {RuleContext} [ctx] контекст прогону (walkCache тощо)
33
33
  * @returns {Promise<number>} 0 OK, 1 violations
34
34
  */
35
- export async function runStandardRule(ruleDir, ctx = {}) {
35
+ export function runStandardRule(ruleDir, ctx = {}) {
36
36
  const ruleId = basename(ruleDir)
37
37
  const bundledRulesDir = dirname(ruleDir)
38
38
  return withLock(`fix-${ruleId}`, async () => {
@@ -3,7 +3,7 @@
3
3
  * Алгоритм: mkdirSync-based lock, перевірка живості PID, sha256-dedup з TTL.
4
4
  */
5
5
  import * as fs from 'node:fs'
6
- import * as path from 'node:path'
6
+ import { join } from 'node:path'
7
7
  import * as os from 'node:os'
8
8
  import { setTimeout as sleep } from 'node:timers/promises'
9
9
  import { worktreeFingerprint } from './worktree-fingerprint.mjs'
@@ -12,9 +12,14 @@ const DEFAULTS = {
12
12
  ttl: 600_000,
13
13
  staleThreshold: 1_800_000,
14
14
  waitTimeout: 1_200_000,
15
- pollInterval: 1_500
15
+ pollInterval: 1500
16
16
  }
17
17
 
18
+ /**
19
+ * Чи процес із заданим PID ще живий на поточному host.
20
+ * @param {number} pid ідентифікатор процесу з owner.json
21
+ * @returns {boolean} true, якщо process.kill(pid, 0) не кинув помилку
22
+ */
18
23
  function isAlive(pid) {
19
24
  try {
20
25
  process.kill(pid, 0)
@@ -24,14 +29,21 @@ function isAlive(pid) {
24
29
  }
25
30
  }
26
31
 
32
+ /**
33
+ * Повертає функцію, що знімає lock-директорію.
34
+ * @param {string} lockDir абсолютний шлях до lock-директорії
35
+ * @returns {() => void} release-колбек для finally/signal handler
36
+ */
27
37
  function makeRelease(lockDir) {
28
38
  return () => fs.rmSync(lockDir, { recursive: true, force: true })
29
39
  }
30
40
 
31
41
  /**
32
- * @param {{exitCode:number, fingerprint:string|null, finishedAt:number}} result
33
- * @param {string|null} fingerprint
34
- * @param {number} ttl
42
+ * Чи можна пропустити повторний прогін за кешованим result.json.
43
+ * @param {{exitCode:number, fingerprint:string|null, finishedAt:number}} result попередній результат з result.json
44
+ * @param {string|null} fingerprint поточний fingerprint робочого дерева
45
+ * @param {number} ttl TTL дедуплікації в мілісекундах
46
+ * @returns {boolean} true, якщо попередній успішний прогін можна повторно використати
35
47
  */
36
48
  export function shouldDedup(result, fingerprint, ttl) {
37
49
  if (result.exitCode !== 0) return false
@@ -41,27 +53,26 @@ export function shouldDedup(result, fingerprint, ttl) {
41
53
  }
42
54
 
43
55
  /**
44
- * @param {string} key
45
- * @param {() => number | Promise<number>} runFn
46
- * @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string, getFingerprint?:() => string | null}} [opts]
47
- * @returns {Promise<number>}
56
+ * Серіалізує важку команду через атомарний lock і dedup за fingerprint.
57
+ * @param {string} key ключ локу (наприклад `lint-ga`, `fix-bun`)
58
+ * @param {() => number | Promise<number>} runFn основна робота; повертає exit code
59
+ * @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string, getFingerprint?:() => string | null}} [opts] TTL, шлях кешу та override fingerprint
60
+ * @returns {Promise<number>} exit code виконаної або дедуплікованої команди
48
61
  */
49
62
  export async function withLock(key, runFn, opts = {}) {
50
63
  const { ttl, staleThreshold, waitTimeout, pollInterval } = { ...DEFAULTS, ...opts }
51
64
  const getFingerprint = opts.getFingerprint ?? worktreeFingerprint
52
- const cacheDir = opts.cacheDir ?? path.join(process.cwd(), 'node_modules/.cache/n-cursor', key)
53
- const lockDir = path.join(cacheDir, 'lock')
54
- const ownerFile = path.join(lockDir, 'owner.json')
55
- const resultFile = path.join(cacheDir, 'result.json')
65
+ const cacheDir = opts.cacheDir ?? join(process.cwd(), 'node_modules/.cache/n-cursor', key)
66
+ const lockDir = join(cacheDir, 'lock')
67
+ const ownerFile = join(lockDir, 'owner.json')
68
+ const resultFile = join(cacheDir, 'result.json')
56
69
  const release = makeRelease(lockDir)
57
70
 
58
71
  const fingerprint = getFingerprint()
59
72
  fs.mkdirSync(cacheDir, { recursive: true })
60
73
 
61
74
  const loopStart = Date.now()
62
- let locked = false
63
75
 
64
- // eslint-disable-next-line no-constant-condition
65
76
  while (true) {
66
77
  if (Date.now() - loopStart >= waitTimeout) {
67
78
  console.error(`⚠️ ${key}: чекав ${waitTimeout / 60_000} хв — запускаю без локу`)
@@ -73,7 +84,6 @@ export async function withLock(key, runFn, opts = {}) {
73
84
  ownerFile,
74
85
  JSON.stringify({ pid: process.pid, host: os.hostname(), startedAt: Date.now(), fingerprint })
75
86
  )
76
- locked = true
77
87
  break
78
88
  } catch (error) {
79
89
  if (error.code !== 'EEXIST') throw error
@@ -113,6 +123,7 @@ export async function withLock(key, runFn, opts = {}) {
113
123
 
114
124
  const onSignal = () => {
115
125
  release()
126
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- SIGINT/SIGTERM мають завершити процес із кодом 130
116
127
  process.exit(130)
117
128
  }
118
129
  process.once('SIGINT', onSignal)
@@ -1,13 +1,16 @@
1
- /**
2
- * Fingerprint поточного стану git-робочого дерева.
3
- * Повертає sha256-hex (64 символи) або null, якщо не в git-репо.
4
- * @param {typeof import('child_process').spawnSync} spawn
5
- */
6
1
  import { spawnSync } from 'node:child_process'
7
2
  import { createHash } from 'node:crypto'
8
3
 
4
+ /**
5
+ * Fingerprint поточного стану git-робочого дерева.
6
+ * @param {typeof import('child_process').spawnSync} [spawn] sync-виклик git (ін'єкція для тестів)
7
+ * @returns {string|null} sha256-hex (64 символи) або null, якщо не в git-репо
8
+ */
9
9
  export function worktreeFingerprint(spawn = spawnSync) {
10
- /** @param {string[]} args */
10
+ /**
11
+ * @param {string[]} args аргументи підкоманди git
12
+ * @returns {string} stdout git-команди
13
+ */
11
14
  function git(args) {
12
15
  const r = spawn('git', args, { encoding: 'utf8' })
13
16
  if (r.status !== 0 || r.error) throw new Error(`git ${args[0]} failed`)
@@ -21,7 +24,7 @@ export function worktreeFingerprint(spawn = spawnSync) {
21
24
  // повертаються у `"..."` формі, і `git hash-object` не знаходить файл → throw → fingerprint=null.
22
25
  const untrackedRaw = git(['ls-files', '-z', '--others', '--exclude-standard'])
23
26
  const untrackedFiles = untrackedRaw.split('\0').filter(Boolean)
24
- const pairs = untrackedFiles.map(f => `${f}:${git(['hash-object', f]).trim()}`).sort()
27
+ const pairs = untrackedFiles.map(f => `${f}:${git(['hash-object', f]).trim()}`).toSorted()
25
28
  const raw = [commitHash, diffText, ...pairs].join('\n')
26
29
  return createHash('sha256').update(raw).digest('hex')
27
30
  } catch {