@nitra/cursor 1.13.84 → 1.13.87
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 +32 -0
- package/bin/n-cursor.js +6 -2
- package/package.json +1 -1
- package/rules/docker/lint/lint.mjs +13 -3
- package/rules/ga/lint/lint.mjs +5 -2
- package/rules/k8s/lint/lint.mjs +13 -3
- package/rules/rego/lint/lint.mjs +15 -2
- package/rules/text/lint/lint.mjs +12 -2
- package/scripts/utils/run-standard-lint.mjs +35 -0
- package/scripts/utils/run-standard-rule.mjs +12 -3
- package/scripts/utils/with-lock.mjs +2 -2
- package/scripts/utils/worktree-fingerprint.mjs +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
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.87] - 2026-05-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`scripts/utils/run-standard-lint.mjs`** — спільна точка входу для всіх `lint-<rule>` підкоманд, дзеркально до `runStandardRule` для `fix-<id>`. Виводить ключ локу зі шляху (`basename(dirname(lintDir))`) і прокидає `opts` у `withLock`. Місце для майбутніх крос-cutting розширень (телеметрія, env-toggle вимкнення локу, common preflight-логування) — патчиш одне місце, не 5 файлів.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **5 `rules/<rule>/lint/lint.mjs` (ga, rego, text, k8s, docker)** більше не імпортують `withLock` напряму — використовують `runStandardLint(import.meta.dirname, runLint<Foo>Steps)`. Ім'я правила в одному місці — у каталозі.
|
|
16
|
+
- **`.cursor/rules/scripts.mdc` 1.9 → 1.10:** канон патерну переписано на `runStandardLint` (а не прямий `withLock`); додано явну заборону імпортувати `withLock` у `rules/<rule>/lint/lint.mjs`. У кожному з 5 lint.mjs у top-JSDoc додано посилання «Канон патерну `lint-*` — `.cursor/rules/scripts.mdc`».
|
|
17
|
+
|
|
18
|
+
## [1.13.86] - 2026-05-23
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`worktreeFingerprint` повертав `null` при untracked-файлах з не-ASCII іменами:** `git ls-files --others --exclude-standard` без `-z` повертає такі шляхи у C-escape виді (`"docs/adr/20260523-...кирилиця..."` з `\321\201`-послідовностями), і наступний `git hash-object <escaped>` не знаходить файл — увесь fingerprint падав у `null`, через що дедуп ніколи не спрацьовував у репах з кирилицею в untracked-іменах. Перехід на `-z` + `\0`-розбиття дає сирий байтовий шлях.
|
|
23
|
+
|
|
24
|
+
## [1.13.85] - 2026-05-23
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **`withLock` розгорнуто на всі важкі CLI-команди:** додано серіалізацію + дедуп у `lint-rego`, `lint-text`, `lint-k8s`, `lint-docker` за тим самим зразком, що `lint-ga` (приватна `runLint<Foo>Steps()` + публічна `runLint<Foo>Cli = () => withLock('lint-<rule>', …)`).
|
|
29
|
+
- **`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 гранулярність — різні правила паралельно, однакові серіалізуються.
|
|
30
|
+
- **`runLintRego` тепер async** (наслідок обгортки), додано окремий export `runLintRegoSteps(cwd)` для тестів — щоб не дедупувати проти попереднього прогону, який лишив cached result у `node_modules/.cache/n-cursor/lint-rego/`.
|
|
31
|
+
- **`.cursor/rules/scripts.mdc` 1.8 → 1.9:** додано канонічну секцію «Серіалізація важких CLI-команд: `withLock`» з патерном інтеграції, таблицею ключів і red flags.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Тест `withLock integration > serializes parallel calls` падав через дедуп (обидва виклики бачили однаковий fingerprint і другий пропускав). Тест явно вимикає дедуп через `getFingerprint: () => null` — окремо тестується серіалізація, окремо дедуп.
|
|
36
|
+
- Тест `runLintTextCli` після обгортки повертає Promise; `withIsolatedPath` тепер `await fn()`.
|
|
37
|
+
- JSDoc-тип `withLock` opts розширено `getFingerprint?` (вже використовувався у runtime, але був відсутній у сигнатурі — TS видавав error 2353).
|
|
38
|
+
|
|
7
39
|
## [1.13.84] - 2026-05-23
|
|
8
40
|
|
|
9
41
|
### 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
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Dockerfile та варіанти виду app.Dockerfile (регістр суфікса не важливий).
|
|
7
7
|
*
|
|
8
8
|
* Виклик hadolint — через ../js/lint/docker-hadolint.mjs (PATH або docker run).
|
|
9
|
+
*
|
|
10
|
+
* Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) —
|
|
11
|
+
* `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
|
|
9
12
|
*/
|
|
10
13
|
import { basename } from 'node:path'
|
|
11
14
|
|
|
@@ -14,6 +17,7 @@ import { lintDockerfileWithHadolint, posixRel } from '../js/lint/docker-hadolint
|
|
|
14
17
|
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
15
18
|
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
16
19
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
20
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Чи входить файл до набору lint-docker: Dockerfile або *.Dockerfile (*.dockerfile).
|
|
@@ -46,11 +50,10 @@ export async function findLintDockerfilePaths(root, ignorePaths = []) {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
*
|
|
50
|
-
* Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
|
|
53
|
+
* Внутрішні кроки `lint-docker` без локу: hadolint по Dockerfile та *.Dockerfile.
|
|
51
54
|
* @returns {Promise<number>} 0 — OK, 1 — зауваження або помилка
|
|
52
55
|
*/
|
|
53
|
-
|
|
56
|
+
async function runLintDockerSteps() {
|
|
54
57
|
const reporter = createCheckReporter()
|
|
55
58
|
const { pass, fail } = reporter
|
|
56
59
|
|
|
@@ -80,6 +83,13 @@ export async function runLintDocker() {
|
|
|
80
83
|
return reporter.getExitCode()
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Публічна CLI-форма: серіалізує через `withLock('lint-docker')` + дедуп за станом git-дерева.
|
|
88
|
+
* Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
|
|
89
|
+
* @returns {Promise<number>} код виходу
|
|
90
|
+
*/
|
|
91
|
+
export const runLintDocker = () => runStandardLint(import.meta.dirname, runLintDockerSteps)
|
|
92
|
+
|
|
83
93
|
if (isRunAsCli()) {
|
|
84
94
|
process.exitCode = await runLintDocker()
|
|
85
95
|
}
|
package/rules/ga/lint/lint.mjs
CHANGED
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
* локально це виглядало як мовчазний exit 1.
|
|
24
24
|
*
|
|
25
25
|
* Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
|
|
26
|
+
*
|
|
27
|
+
* Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) —
|
|
28
|
+
* `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
|
|
26
29
|
*/
|
|
27
30
|
import { platform } from 'node:process'
|
|
28
31
|
|
|
29
32
|
import { check as checkGa } from '../js/workflows/check.mjs'
|
|
30
33
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
31
34
|
import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
|
|
32
|
-
import {
|
|
35
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
|
|
@@ -165,4 +168,4 @@ async function runLintGaSteps() {
|
|
|
165
168
|
return await checkGa()
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
export const runLintGaCli = () =>
|
|
171
|
+
export const runLintGaCli = () => runStandardLint(import.meta.dirname, runLintGaSteps)
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Версія `-kubernetes-version` для kubeconform узгоджена з PIN yannh у rules/k8s/fix.mjs / k8s.mdc.
|
|
13
13
|
* Kubescape не має аналога цього прапорця; орієнтир цільового кластера — та сама лінія релізу (див. k8s.mdc).
|
|
14
|
+
*
|
|
15
|
+
* Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) —
|
|
16
|
+
* `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
|
|
14
17
|
*/
|
|
15
18
|
import { spawnSync } from 'node:child_process'
|
|
16
19
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
@@ -24,6 +27,7 @@ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
|
24
27
|
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
25
28
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
26
29
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
30
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
27
31
|
|
|
28
32
|
/** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
|
|
29
33
|
const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
|
|
@@ -320,11 +324,10 @@ async function runKubescape(dirs, root) {
|
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
/**
|
|
323
|
-
*
|
|
324
|
-
* Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
|
|
327
|
+
* Внутрішні кроки `lint-k8s` без локу: kubeconform + kubescape для усіх знайдених дерев `k8s`.
|
|
325
328
|
* @returns {Promise<number>} код виходу для `process.exitCode` (0 — успіх або пропуск)
|
|
326
329
|
*/
|
|
327
|
-
|
|
330
|
+
async function runLintK8sSteps() {
|
|
328
331
|
const root = process.cwd()
|
|
329
332
|
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
330
333
|
const dirs = await findK8sRoots(root, ignorePaths)
|
|
@@ -344,6 +347,13 @@ export async function runLintK8s() {
|
|
|
344
347
|
return ks
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Публічна CLI-форма: серіалізує через `withLock('lint-k8s')` + дедуп за станом git-дерева.
|
|
352
|
+
* Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
|
|
353
|
+
* @returns {Promise<number>} код виходу
|
|
354
|
+
*/
|
|
355
|
+
export const runLintK8s = () => runStandardLint(import.meta.dirname, runLintK8sSteps)
|
|
356
|
+
|
|
347
357
|
if (isRunAsCli()) {
|
|
348
358
|
process.exitCode = await runLintK8s()
|
|
349
359
|
}
|
package/rules/rego/lint/lint.mjs
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* `npm/rules/<id>/policy/<concern>/`). Усі три інструменти приймають один шлях
|
|
23
23
|
* і самі рекурсивно знаходять `.rego` (ігноруючи інші розширення на кшталт
|
|
24
24
|
* `target.json` чи template-фіх).
|
|
25
|
+
*
|
|
26
|
+
* Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) —
|
|
27
|
+
* `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
|
|
25
28
|
*/
|
|
26
29
|
import { spawnSync } from 'node:child_process'
|
|
27
30
|
import { existsSync } from 'node:fs'
|
|
@@ -29,6 +32,7 @@ import { resolve } from 'node:path'
|
|
|
29
32
|
|
|
30
33
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
31
34
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
35
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
32
36
|
|
|
33
37
|
/** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
|
|
34
38
|
const LINT_TARGETS = ['npm/rules']
|
|
@@ -89,10 +93,13 @@ function runStep(bin, args, cwd) {
|
|
|
89
93
|
/**
|
|
90
94
|
* Запускає `opa check --strict` і `regal lint` по існуючих цілях. Якщо жодної цілі немає —
|
|
91
95
|
* пропускає лінт із кодом 0. Якщо хоча б один preflight не пройшов — exit 1 ще до запусків.
|
|
96
|
+
*
|
|
97
|
+
* Внутрішня форма без локу — для тестів, які працюють у тимчасових каталогах і мають
|
|
98
|
+
* можливість запускати fresh без дедуплікації проти попереднього прогону.
|
|
92
99
|
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
93
100
|
* @returns {number} 0 — OK або skip; інакше код виходу першого кроку, що впав
|
|
94
101
|
*/
|
|
95
|
-
export function
|
|
102
|
+
export function runLintRegoSteps(cwd = process.cwd()) {
|
|
96
103
|
const root = resolve(cwd)
|
|
97
104
|
const opa = resolveCmd('opa')
|
|
98
105
|
const regal = resolveCmd('regal')
|
|
@@ -130,6 +137,12 @@ export function runLintRego(cwd = process.cwd()) {
|
|
|
130
137
|
return runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Публічна CLI-форма: серіалізує через `withLock('lint-rego')` + дедуп за станом git-дерева.
|
|
142
|
+
* @returns {Promise<number>} код виходу
|
|
143
|
+
*/
|
|
144
|
+
export const runLintRego = () => runStandardLint(import.meta.dirname, () => runLintRegoSteps())
|
|
145
|
+
|
|
133
146
|
if (isRunAsCli()) {
|
|
134
|
-
process.exitCode = runLintRego()
|
|
147
|
+
process.exitCode = await runLintRego()
|
|
135
148
|
}
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
|
|
15
15
|
* Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
|
|
16
|
+
*
|
|
17
|
+
* Канон патерну `lint-*` (серіалізація через `runStandardLint`, без прямого `withLock`) —
|
|
18
|
+
* `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд».
|
|
16
19
|
*/
|
|
17
20
|
import { platform } from 'node:process'
|
|
18
21
|
|
|
19
22
|
import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
|
|
20
23
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
24
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
21
25
|
import { runDotenvLinter } from './run-dotenv-linter.mjs'
|
|
22
26
|
import { runShellcheckText } from './run-shellcheck.mjs'
|
|
23
27
|
import { runV8rWithGlobs } from './run-v8r.mjs'
|
|
@@ -120,10 +124,10 @@ function preflight(dep) {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/**
|
|
123
|
-
*
|
|
127
|
+
* Внутрішні кроки `lint-text` без локу.
|
|
124
128
|
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
125
129
|
*/
|
|
126
|
-
|
|
130
|
+
function runLintTextSteps() {
|
|
127
131
|
let preflightOk = true
|
|
128
132
|
for (const dep of [SHELLCHECK_PREFLIGHT, PATCH_PREFLIGHT, DOTENV_LINTER_PREFLIGHT]) {
|
|
129
133
|
if (!preflight(dep)) preflightOk = false
|
|
@@ -147,3 +151,9 @@ export function runLintTextCli() {
|
|
|
147
151
|
console.log('\n▶ v8r (schema-валідація json/json5/yaml/yml/toml)')
|
|
148
152
|
return runV8rWithGlobs()
|
|
149
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
|
|
157
|
+
* @returns {Promise<number>} код виходу
|
|
158
|
+
*/
|
|
159
|
+
export const runLintTextCli = () => runStandardLint(import.meta.dirname, () => runLintTextSteps())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільна точка входу для канонічних `lint-<rule>` підкоманд `@nitra/cursor`.
|
|
3
|
+
*
|
|
4
|
+
* Дзеркально до `runStandardRule` для `fix-<id>`: усі `lint-*` проходять через одну функцію,
|
|
5
|
+
* щоб майбутні крос-cutting концерни (телеметрія, env-toggle вимкнення локу для дебагу,
|
|
6
|
+
* dry-run-режим, common preflight-логування тощо) додавались **в одному** місці, а не
|
|
7
|
+
* патчилися в кожному `rules/<rule>/lint/lint.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Зараз робить рівно одне: серіалізує + дедуплікує запуски через `withLock('lint-<ruleId>')`.
|
|
10
|
+
* `ruleId` виводиться зі шляху: `import.meta.dirname` у `rules/<id>/lint/lint.mjs` → `<id>`.
|
|
11
|
+
*
|
|
12
|
+
* Інтеграція з боку правила:
|
|
13
|
+
*
|
|
14
|
+
* ```js
|
|
15
|
+
* import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
16
|
+
*
|
|
17
|
+
* async function runLintFooSteps() { ... }
|
|
18
|
+
*
|
|
19
|
+
* export const runLintFooCli = () => runStandardLint(import.meta.dirname, runLintFooSteps)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { basename, dirname } from 'node:path'
|
|
23
|
+
|
|
24
|
+
import { withLock } from './with-lock.mjs'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} lintDir абсолютний шлях до `rules/<id>/lint/` (передавай `import.meta.dirname`)
|
|
28
|
+
* @param {() => number | Promise<number>} stepsFn реальна робота лінту; повертає код виходу
|
|
29
|
+
* @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string, getFingerprint?:() => string | null}} [opts] прокидаються у `withLock`
|
|
30
|
+
* @returns {Promise<number>} код виходу
|
|
31
|
+
*/
|
|
32
|
+
export function runStandardLint(lintDir, stepsFn, opts) {
|
|
33
|
+
const ruleId = basename(dirname(lintDir))
|
|
34
|
+
return withLock(`lint-${ruleId}`, stepsFn, opts)
|
|
35
|
+
}
|
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 = {}) {
|
|
@@ -17,8 +17,10 @@ export function worktreeFingerprint(spawn = spawnSync) {
|
|
|
17
17
|
try {
|
|
18
18
|
const commitHash = git(['rev-parse', 'HEAD']).trim()
|
|
19
19
|
const diffText = git(['diff', 'HEAD'])
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// -z: NUL-розділення без C-екранування. Без нього імена з не-ASCII символами
|
|
21
|
+
// повертаються у `"..."` формі, і `git hash-object` не знаходить файл → throw → fingerprint=null.
|
|
22
|
+
const untrackedRaw = git(['ls-files', '-z', '--others', '--exclude-standard'])
|
|
23
|
+
const untrackedFiles = untrackedRaw.split('\0').filter(Boolean)
|
|
22
24
|
const pairs = untrackedFiles
|
|
23
25
|
.map(f => `${f}:${git(['hash-object', f]).trim()}`)
|
|
24
26
|
.sort()
|