@nitra/cursor 1.13.84 → 1.13.85

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.85] - 2026-05-23
8
+
9
+ ### Changed
10
+
11
+ - **`withLock` розгорнуто на всі важкі CLI-команди:** додано серіалізацію + дедуп у `lint-rego`, `lint-text`, `lint-k8s`, `lint-docker` за тим самим зразком, що `lint-ga` (приватна `runLint<Foo>Steps()` + публічна `runLint<Foo>Cli = () => withLock('lint-<rule>', …)`).
12
+ - **`fix`-лок переїхав у `runStandardRule`:** замість зовнішньої обгортки навколо `runFixCommand` у `bin/n-cursor.js`, `withLock('fix-<ruleId>')` тепер всередині `scripts/utils/run-standard-rule.mjs`. Кожен `rules/<id>/fix.mjs` отримує лок «безкоштовно» через делегацію; `npx @nitra/cursor fix`, прямий `bun rules/<id>/fix.mjs` і `run(ctx)`-композиція проходять через одну точку. Per-rule гранулярність — різні правила паралельно, однакові серіалізуються.
13
+ - **`runLintRego` тепер async** (наслідок обгортки), додано окремий export `runLintRegoSteps(cwd)` для тестів — щоб не дедупувати проти попереднього прогону, який лишив cached result у `node_modules/.cache/n-cursor/lint-rego/`.
14
+ - **`.cursor/rules/scripts.mdc` 1.8 → 1.9:** додано канонічну секцію «Серіалізація важких CLI-команд: `withLock`» з патерном інтеграції, таблицею ключів і red flags.
15
+
16
+ ### Fixed
17
+
18
+ - Тест `withLock integration > serializes parallel calls` падав через дедуп (обидва виклики бачили однаковий fingerprint і другий пропускав). Тест явно вимикає дедуп через `getFingerprint: () => null` — окремо тестується серіалізація, окремо дедуп.
19
+ - Тест `runLintTextCli` після обгортки повертає Promise; `withIsolatedPath` тепер `await fn()`.
20
+ - JSDoc-тип `withLock` opts розширено `getFingerprint?` (вже використовувався у runtime, але був відсутній у сигнатурі — TS видавав error 2353).
21
+
7
22
  ## [1.13.84] - 2026-05-23
8
23
 
9
24
  ### Changed
package/bin/n-cursor.js CHANGED
@@ -978,6 +978,10 @@ function logRemovedManagedItems(title, basePath, names) {
978
978
  * перевіряє whitelist (`runRuleCli`) і друкує per-rule summary.
979
979
  *
980
980
  * Без аргументів — discover з `.cursor/rules/*.mdc` у проекті-споживачі.
981
+ *
982
+ * Серіалізація паралельних запусків — per-rule, всередині `runStandardRule` (`withLock('fix-<id>')`).
983
+ * На рівні `runFixCommand` локу нема: різні набори правил можуть прогресувати незалежно,
984
+ * а однакові правила серіалізуються в spawn'ах нижче.
981
985
  * @param {string[]} requestedRules імена правил; порожній масив — discovery з `.cursor/rules/`
982
986
  * @returns {Promise<void>}
983
987
  */
@@ -1258,7 +1262,7 @@ try {
1258
1262
  }
1259
1263
  case 'lint-rego': {
1260
1264
  // Канонічний lint-rego: preflight opa/regal → opa check --strict → regal lint → conftest verify (опц.).
1261
- process.exitCode = runLintRego()
1265
+ process.exitCode = await runLintRego()
1262
1266
 
1263
1267
  break
1264
1268
  }
@@ -1276,7 +1280,7 @@ try {
1276
1280
  }
1277
1281
  case 'lint-text': {
1278
1282
  // Канонічний lint-text: cspell → run-shellcheck → markdownlint-cli2 --fix → run-v8r (text.mdc).
1279
- process.exitCode = runLintTextCli()
1283
+ process.exitCode = await runLintTextCli()
1280
1284
 
1281
1285
  break
1282
1286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.84",
3
+ "version": "1.13.85",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -14,6 +14,7 @@ import { lintDockerfileWithHadolint, posixRel } from '../js/lint/docker-hadolint
14
14
  import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
15
15
  import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
16
16
  import { walkDir } from '../../../scripts/utils/walkDir.mjs'
17
+ import { withLock } from '../../../scripts/utils/with-lock.mjs'
17
18
 
18
19
  /**
19
20
  * Чи входить файл до набору lint-docker: Dockerfile або *.Dockerfile (*.dockerfile).
@@ -46,11 +47,10 @@ export async function findLintDockerfilePaths(root, ignorePaths = []) {
46
47
  }
47
48
 
48
49
  /**
49
- * Запуск hadolint по Dockerfile та *.Dockerfile.
50
- * Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
50
+ * Внутрішні кроки `lint-docker` без локу: hadolint по Dockerfile та *.Dockerfile.
51
51
  * @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
52
52
  */
53
- export async function runLintDocker() {
53
+ async function runLintDockerSteps() {
54
54
  const reporter = createCheckReporter()
55
55
  const { pass, fail } = reporter
56
56
 
@@ -80,6 +80,13 @@ export async function runLintDocker() {
80
80
  return reporter.getExitCode()
81
81
  }
82
82
 
83
+ /**
84
+ * Публічна CLI-форма: серіалізує через `withLock('lint-docker')` + дедуп за станом git-дерева.
85
+ * Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
86
+ * @returns {Promise<number>} код виходу
87
+ */
88
+ export const runLintDocker = () => withLock('lint-docker', runLintDockerSteps)
89
+
83
90
  if (isRunAsCli()) {
84
91
  process.exitCode = await runLintDocker()
85
92
  }
@@ -24,6 +24,7 @@ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
24
24
  import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
25
25
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
26
26
  import { walkDir } from '../../../scripts/utils/walkDir.mjs'
27
+ import { withLock } from '../../../scripts/utils/with-lock.mjs'
27
28
 
28
29
  /** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
29
30
  const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
@@ -320,11 +321,10 @@ async function runKubescape(dirs, root) {
320
321
  }
321
322
 
322
323
  /**
323
- * Головна точка входу: kubeconform + kubescape для усіх знайдених дерев `k8s`.
324
- * Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
324
+ * Внутрішні кроки `lint-k8s` без локу: kubeconform + kubescape для усіх знайдених дерев `k8s`.
325
325
  * @returns {Promise<number>} код виходу для `process.exitCode` (0 — успіх або пропуск)
326
326
  */
327
- export async function runLintK8s() {
327
+ async function runLintK8sSteps() {
328
328
  const root = process.cwd()
329
329
  const ignorePaths = await loadCursorIgnorePaths(root)
330
330
  const dirs = await findK8sRoots(root, ignorePaths)
@@ -344,6 +344,13 @@ export async function runLintK8s() {
344
344
  return ks
345
345
  }
346
346
 
347
+ /**
348
+ * Публічна CLI-форма: серіалізує через `withLock('lint-k8s')` + дедуп за станом git-дерева.
349
+ * Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
350
+ * @returns {Promise<number>} код виходу
351
+ */
352
+ export const runLintK8s = () => withLock('lint-k8s', runLintK8sSteps)
353
+
347
354
  if (isRunAsCli()) {
348
355
  process.exitCode = await runLintK8s()
349
356
  }
@@ -29,6 +29,7 @@ import { resolve } from 'node:path'
29
29
 
30
30
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
31
31
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
32
+ import { withLock } from '../../../scripts/utils/with-lock.mjs'
32
33
 
33
34
  /** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
34
35
  const LINT_TARGETS = ['npm/rules']
@@ -89,10 +90,13 @@ function runStep(bin, args, cwd) {
89
90
  /**
90
91
  * Запускає `opa check --strict` і `regal lint` по існуючих цілях. Якщо жодної цілі немає —
91
92
  * пропускає лінт із кодом 0. Якщо хоча б один preflight не пройшов — exit 1 ще до запусків.
93
+ *
94
+ * Внутрішня форма без локу — для тестів, які працюють у тимчасових каталогах і мають
95
+ * можливість запускати fresh без дедуплікації проти попереднього прогону.
92
96
  * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
93
97
  * @returns {number} 0 — OK або skip; інакше код виходу першого кроку, що впав
94
98
  */
95
- export function runLintRego(cwd = process.cwd()) {
99
+ export function runLintRegoSteps(cwd = process.cwd()) {
96
100
  const root = resolve(cwd)
97
101
  const opa = resolveCmd('opa')
98
102
  const regal = resolveCmd('regal')
@@ -130,6 +134,12 @@ export function runLintRego(cwd = process.cwd()) {
130
134
  return runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
131
135
  }
132
136
 
137
+ /**
138
+ * Публічна CLI-форма: серіалізує через `withLock('lint-rego')` + дедуп за станом git-дерева.
139
+ * @returns {Promise<number>} код виходу
140
+ */
141
+ export const runLintRego = () => withLock('lint-rego', () => runLintRegoSteps())
142
+
133
143
  if (isRunAsCli()) {
134
- process.exitCode = runLintRego()
144
+ process.exitCode = await runLintRego()
135
145
  }
@@ -18,6 +18,7 @@ import { platform } from 'node:process'
18
18
 
19
19
  import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
20
20
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
21
+ import { withLock } from '../../../scripts/utils/with-lock.mjs'
21
22
  import { runDotenvLinter } from './run-dotenv-linter.mjs'
22
23
  import { runShellcheckText } from './run-shellcheck.mjs'
23
24
  import { runV8rWithGlobs } from './run-v8r.mjs'
@@ -120,10 +121,10 @@ function preflight(dep) {
120
121
  }
121
122
 
122
123
  /**
123
- * Виконує канонічний `lint-text` з preflight і ланцюжком cspell → shellcheck → dotenv → markdownlint → v8r.
124
+ * Внутрішні кроки `lint-text` без локу.
124
125
  * @returns {number} 0 — все OK, інакше — код першого кроку, що впав
125
126
  */
126
- export function runLintTextCli() {
127
+ function runLintTextSteps() {
127
128
  let preflightOk = true
128
129
  for (const dep of [SHELLCHECK_PREFLIGHT, PATCH_PREFLIGHT, DOTENV_LINTER_PREFLIGHT]) {
129
130
  if (!preflight(dep)) preflightOk = false
@@ -147,3 +148,9 @@ export function runLintTextCli() {
147
148
  console.log('\n▶ v8r (schema-валідація json/json5/yaml/yml/toml)')
148
149
  return runV8rWithGlobs()
149
150
  }
151
+
152
+ /**
153
+ * Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
154
+ * @returns {Promise<number>} код виходу
155
+ */
156
+ export const runLintTextCli = () => withLock('lint-text', () => runLintTextSteps())
@@ -3,12 +3,19 @@
3
3
  *
4
4
  * Інкапсулює: `discoverOneRule` → `runRule(applies → JS → policy → mdc-refs)`.
5
5
  * Локальна логіка в правилах заборонена; розширення поведінки — через `ctx`-опції.
6
+ *
7
+ * Серіалізація: загортає виконання у `withLock('fix-<ruleId>')` — паралельні запуски
8
+ * того самого правила (через `npx @nitra/cursor fix`, прямий `bun rules/<id>/fix.mjs`
9
+ * чи `run(ctx)`-композицію) дедупляться за станом git-дерева; різні правила можуть
10
+ * виконуватись паралельно. Точка інтеграції — тут, щоб не дублювати лок у кожному
11
+ * `fix.mjs`.
6
12
  */
7
13
  import { basename, dirname } from 'node:path'
8
14
 
9
15
  import { discoverOneRule } from './discover-checkable-rules.mjs'
10
16
  import { runRule } from './run-rule.mjs'
11
17
  import { getOrCreateWalkCache } from './walk-cache.mjs'
18
+ import { withLock } from './with-lock.mjs'
12
19
 
13
20
  /**
14
21
  * @typedef {object} RuleContext
@@ -28,7 +35,9 @@ import { getOrCreateWalkCache } from './walk-cache.mjs'
28
35
  export async function runStandardRule(ruleDir, ctx = {}) {
29
36
  const ruleId = basename(ruleDir)
30
37
  const bundledRulesDir = dirname(ruleDir)
31
- const rule = await discoverOneRule(ruleDir, ruleId)
32
- const walkCache = ctx.walkCache ?? getOrCreateWalkCache()
33
- return runRule(rule, bundledRulesDir, walkCache)
38
+ return withLock(`fix-${ruleId}`, async () => {
39
+ const rule = await discoverOneRule(ruleDir, ruleId)
40
+ const walkCache = ctx.walkCache ?? getOrCreateWalkCache()
41
+ return runRule(rule, bundledRulesDir, walkCache)
42
+ })
34
43
  }
@@ -40,8 +40,8 @@ export function shouldDedup(result, fingerprint, ttl) {
40
40
 
41
41
  /**
42
42
  * @param {string} key
43
- * @param {() => Promise<number>} runFn
44
- * @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string}} [opts]
43
+ * @param {() => number | Promise<number>} runFn
44
+ * @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string, getFingerprint?:() => string | null}} [opts]
45
45
  * @returns {Promise<number>}
46
46
  */
47
47
  export async function withLock(key, runFn, opts = {}) {