@nitra/cursor 1.13.85 → 1.13.89
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 +38 -0
- package/bin/n-cursor.js +5 -6
- package/package.json +1 -1
- package/rules/docker/lint/lint.mjs +5 -2
- package/rules/ga/lint/lint.mjs +5 -2
- package/rules/image-compress/js/package_setup/check.mjs +2 -2
- package/rules/k8s/lint/lint.mjs +5 -2
- package/rules/rego/lint/lint.mjs +5 -2
- package/rules/text/lint/lint.mjs +5 -2
- package/scripts/claude-stop-hook.mjs +2 -2
- package/scripts/sync-claude-config.mjs +4 -3
- package/scripts/utils/run-standard-lint.mjs +35 -0
- package/scripts/utils/with-lock.mjs +24 -12
- package/scripts/utils/worktree-fingerprint.mjs +4 -2
- package/.claude-template/commands/n-check.md +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@
|
|
|
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.89] - 2026-05-23
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Stop-hook кличе `fix` замість deprecated `check`:** `scripts/claude-stop-hook.mjs` тепер спавнить `npx --no @nitra/cursor fix` — без deprecation-warning'а на кожен Stop event Claude Code.
|
|
12
|
+
- **`.claude-template/commands/n-check.md` видалено** (разом з локальним `.claude/commands/n-check.md`). Після CLI-перейменування `check` → `fix` slash-команда `/n-check` вказувала на застарілу команду. У `syncClaudeConfig` логіка sync `commands/*.md` залишилась; зараз темплейт порожній. Тест `створює settings.json + slash-команди` переписано на «без slash-команд, коли темплейт порожній».
|
|
13
|
+
- **JSDoc/docstring чистка:** `bin/n-cursor.js` (CLI usage header), `scripts/claude-stop-hook.mjs`, `scripts/sync-claude-config.mjs`, `rules/image-compress/js/package_setup/check.mjs` — згадки `npx @nitra/cursor check`, `/n-check`, `npm/scripts/check-*.mjs` оновлено на актуальну CLI (`fix`) і шляхи (`rules/<id>/fix.mjs`, `rules/<id>/js/<concern>/check.mjs`).
|
|
14
|
+
- **`.cursor/rules/conftest.mdc`** — алгоритм рішення / патерн Rego-authoritative / Workflow / Red-flags переписано під фактичну структуру `rules/<rule>/js/<concern>/check.mjs` + `rules/<rule>/policy/<name>/`. Прибрано згадки `npm/scripts/check-<rule>.mjs` та `npm/policy/<rule>/` (legacy шляхи); приклади `check abie`, `check ga` → `fix abie`, `fix ga`.
|
|
15
|
+
- **`docs/fix-cursor-skill.md`** — ASCII-діаграми, workflow-кроки та таблиця "Анатомія Skill-файлу" → `npx @nitra/cursor fix`; згадка `check-*.mjs скрипти` → `rules/<id>/fix.mjs правил`.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- Споживачі: після оновлення вручну видалити `.claude/commands/n-check.md` (sync не вичищає orphan slash-команди з темплейту). Активна команда — `/n-fix` (зі скілу `n-fix`).
|
|
20
|
+
- В `.claude/settings.json` permission `Bash(npx @nitra/cursor check)` видалено як redundant — вайлдкард `Bash(npx @nitra/cursor *)` нижче вже покриває обидві команди.
|
|
21
|
+
|
|
22
|
+
## [1.13.88] - 2026-05-23
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`scripts/utils/with-lock.mjs` + тести:** локальна `sleep(ms)` через `new Promise(r => setTimeout(r, ms))` замінена на іменований імпорт `setTimeout as sleep` із `node:timers/promises`. Відповідає правилу `js-run` (без ручних `setTimeout`-промісів) — перевірка `npx @nitra/cursor fix js-run` стала зеленою.
|
|
27
|
+
|
|
28
|
+
## [1.13.87] - 2026-05-23
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`scripts/utils/run-standard-lint.mjs`** — спільна точка входу для всіх `lint-<rule>` підкоманд, дзеркально до `runStandardRule` для `fix-<id>`. Виводить ключ локу зі шляху (`basename(dirname(lintDir))`) і прокидає `opts` у `withLock`. Місце для майбутніх крос-cutting розширень (телеметрія, env-toggle вимкнення локу, common preflight-логування) — патчиш одне місце, не 5 файлів.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **5 `rules/<rule>/lint/lint.mjs` (ga, rego, text, k8s, docker)** більше не імпортують `withLock` напряму — використовують `runStandardLint(import.meta.dirname, runLint<Foo>Steps)`. Ім'я правила в одному місці — у каталозі.
|
|
37
|
+
- **`.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`».
|
|
38
|
+
|
|
39
|
+
## [1.13.86] - 2026-05-23
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`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`-розбиття дає сирий байтовий шлях.
|
|
44
|
+
|
|
7
45
|
## [1.13.85] - 2026-05-23
|
|
8
46
|
|
|
9
47
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Використання:
|
|
7
7
|
* `npx \@nitra/cursor` — завантажити cursor-правила
|
|
8
|
-
* `npx \@nitra/cursor
|
|
8
|
+
* `npx \@nitra/cursor fix` — перевірити правила з `.cursor/rules/*.mdc`, для яких у пакеті є `fix.mjs`/policy;
|
|
9
9
|
* якщо в корені вже є `.n-cursor.json`, спочатку зчитується конфіг і за потреби дописується `$schema`
|
|
10
|
-
* `npx \@nitra/cursor
|
|
10
|
+
* `npx \@nitra/cursor fix bun` — перевірити лише вказані правила (ігнорує `.cursor/rules/`)
|
|
11
11
|
* `npx \@nitra/cursor rename-yaml-extensions` — k8s `*.yml` → `*.yaml`, `.github` `*.yaml` → `*.yml` (опції: `--dry-run`, `--root=…`; див. bin/rename-yaml-extensions.mjs)
|
|
12
12
|
* `npx \@nitra/cursor stop-hook` — точка входу Stop hook Claude Code (читає stdin, виходить 0 при `stop_hook_active`,
|
|
13
|
-
* інакше викликає `
|
|
13
|
+
* інакше викликає `fix`); прописується автоматично в `.claude/settings.json`
|
|
14
14
|
* `npx \@nitra/cursor lint-ga` — канонічний lint-ga (ga.mdc): preflight на `shellcheck` →
|
|
15
15
|
* `bunx github-actionlint` → `uvx zizmor --offline --collect=workflows .`
|
|
16
16
|
* `npx \@nitra/cursor lint-rego` — канонічний lint-rego (conftest.mdc + rego.mdc):
|
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
* `npx \@nitra/cursor skill claude taze ["task"]` — Claude Code CLI (`claude -p`)
|
|
26
26
|
*
|
|
27
27
|
* Agent інтеграція: під час синку, окрім `.cursor/rules` і `.claude/commands` (з skills), CLI ще раз
|
|
28
|
-
* синхронізує `.claude/settings.json` (hooks + permissions; merge — користувацькі поля зберігаються)
|
|
29
|
-
* `.cursor/hooks.json` (Cursor Agent hooks; merge — користувацькі hooks зберігаються)
|
|
30
|
-
* і slash-команди checks (`/n-check`).
|
|
28
|
+
* синхронізує `.claude/settings.json` (hooks + permissions; merge — користувацькі поля зберігаються)
|
|
29
|
+
* і `.cursor/hooks.json` (Cursor Agent hooks; merge — користувацькі hooks зберігаються).
|
|
31
30
|
* Опт-аут — поле `claude-config: false` у `.n-cursor.json`.
|
|
32
31
|
*
|
|
33
32
|
* Якщо у корені репозиторію немає .n-cursor.json, спочатку перейменовується за наявності nitra-cursor.json;
|
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,7 +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'
|
|
17
|
-
import {
|
|
20
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Чи входить файл до набору lint-docker: Dockerfile або *.Dockerfile (*.dockerfile).
|
|
@@ -85,7 +88,7 @@ async function runLintDockerSteps() {
|
|
|
85
88
|
* Експортовано як `runLintDocker` — використовується з `bin/n-cursor.js` як підкоманда `lint-docker`.
|
|
86
89
|
* @returns {Promise<number>} код виходу
|
|
87
90
|
*/
|
|
88
|
-
export const runLintDocker = () =>
|
|
91
|
+
export const runLintDocker = () => runStandardLint(import.meta.dirname, runLintDockerSteps)
|
|
89
92
|
|
|
90
93
|
if (isRunAsCli()) {
|
|
91
94
|
process.exitCode = await runLintDocker()
|
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)
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня та
|
|
10
10
|
* з `.gitignore`.
|
|
11
11
|
*
|
|
12
|
-
* **Що покрила Rego** (`npx \@nitra/cursor
|
|
13
|
-
* `npm/policy/
|
|
12
|
+
* **Що покрила Rego** (`npx \@nitra/cursor fix`,
|
|
13
|
+
* `npm/rules/image-compress/policy/package_json/`):
|
|
14
14
|
* - `scripts.lint-image` викликає `npx \@nitra/minify-image --src=. --write`
|
|
15
15
|
* без `--avif` (AVIF — окреме правило `image-avif`);
|
|
16
16
|
* - агрегований `lint` (якщо є) містить `bun run lint-image`;
|
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,7 +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'
|
|
27
|
-
import {
|
|
30
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
28
31
|
|
|
29
32
|
/** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
|
|
30
33
|
const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
|
|
@@ -349,7 +352,7 @@ async function runLintK8sSteps() {
|
|
|
349
352
|
* Експортовано як `runLintK8s` — використовується з `bin/n-cursor.js` як підкоманда `lint-k8s`.
|
|
350
353
|
* @returns {Promise<number>} код виходу
|
|
351
354
|
*/
|
|
352
|
-
export const runLintK8s = () =>
|
|
355
|
+
export const runLintK8s = () => runStandardLint(import.meta.dirname, runLintK8sSteps)
|
|
353
356
|
|
|
354
357
|
if (isRunAsCli()) {
|
|
355
358
|
process.exitCode = await runLintK8s()
|
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,7 +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'
|
|
32
|
-
import {
|
|
35
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
33
36
|
|
|
34
37
|
/** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
|
|
35
38
|
const LINT_TARGETS = ['npm/rules']
|
|
@@ -138,7 +141,7 @@ export function runLintRegoSteps(cwd = process.cwd()) {
|
|
|
138
141
|
* Публічна CLI-форма: серіалізує через `withLock('lint-rego')` + дедуп за станом git-дерева.
|
|
139
142
|
* @returns {Promise<number>} код виходу
|
|
140
143
|
*/
|
|
141
|
-
export const runLintRego = () =>
|
|
144
|
+
export const runLintRego = () => runStandardLint(import.meta.dirname, () => runLintRegoSteps())
|
|
142
145
|
|
|
143
146
|
if (isRunAsCli()) {
|
|
144
147
|
process.exitCode = await runLintRego()
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -13,12 +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'
|
|
21
|
-
import {
|
|
24
|
+
import { runStandardLint } from '../../../scripts/utils/run-standard-lint.mjs'
|
|
22
25
|
import { runDotenvLinter } from './run-dotenv-linter.mjs'
|
|
23
26
|
import { runShellcheckText } from './run-shellcheck.mjs'
|
|
24
27
|
import { runV8rWithGlobs } from './run-v8r.mjs'
|
|
@@ -153,4 +156,4 @@ function runLintTextSteps() {
|
|
|
153
156
|
* Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
|
|
154
157
|
* @returns {Promise<number>} код виходу
|
|
155
158
|
*/
|
|
156
|
-
export const runLintTextCli = () =>
|
|
159
|
+
export const runLintTextCli = () => runStandardLint(import.meta.dirname, () => runLintTextSteps())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stop-hook для Claude Code: запускається hook'ом із `.claude/settings.json` після того,
|
|
3
|
-
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor
|
|
3
|
+
* як агент сигналізує завершення ходу. Прозоро прокидає `npx \@nitra/cursor fix`
|
|
4
4
|
* і повертає його exit code, щоб помилки правил блокували завершення.
|
|
5
5
|
*
|
|
6
6
|
* Захист від нескінченної рекурсії: якщо stdin містить `"stop_hook_active": true`
|
|
@@ -63,7 +63,7 @@ export async function runStopHookCli() {
|
|
|
63
63
|
return 0
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const child = spawn('npx', ['--no', '@nitra/cursor', '
|
|
66
|
+
const child = spawn('npx', ['--no', '@nitra/cursor', 'fix'], { stdio: 'inherit' })
|
|
67
67
|
try {
|
|
68
68
|
const [code] = await once(child, 'exit')
|
|
69
69
|
return code ?? 1
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Синхронізує конфігурацію Claude Code (`.claude/settings.json`,
|
|
3
|
-
* slash-команди
|
|
4
|
-
* у поточний проєкт із темплейтів пакету
|
|
3
|
+
* slash-команди з `commands/` темплейту, ADR Stop-hook) і Cursor hooks
|
|
4
|
+
* (`.cursor/hooks.json`) у поточний проєкт із темплейтів пакету
|
|
5
5
|
* `npm/.claude-template/`.
|
|
6
6
|
*
|
|
7
7
|
* Архітектура:
|
|
8
8
|
* - `settings.json` — **merge**: користувацькі поля зберігаються; наші hooks
|
|
9
9
|
* ідентифікуються командою-маркером (`MANAGED_HOOK_COMMAND_MARKERS`) і
|
|
10
10
|
* перезаписуються; permissions.allow зливається через union (із дедублікацією).
|
|
11
|
-
* - `.claude/commands
|
|
11
|
+
* - `.claude/commands/*.md` — fully owned slash-команди з темплейту
|
|
12
|
+
* `.claude-template/commands/` (зараз порожньо; sync no-op).
|
|
12
13
|
* - `.claude/hooks/capture-decisions.sh` — fully owned bash-скрипт ADR capture Stop-hook;
|
|
13
14
|
* копіюється з `.claude-template/hooks/`, лише коли в `.n-cursor.json` `rules`
|
|
14
15
|
* присутнє `adr` (правило увімкнене за замовчуванням; вимикається через
|
|
@@ -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
|
+
}
|
|
@@ -5,21 +5,23 @@
|
|
|
5
5
|
import * as fs from 'node:fs'
|
|
6
6
|
import * as path from 'node:path'
|
|
7
7
|
import * as os from 'node:os'
|
|
8
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
8
9
|
import { worktreeFingerprint } from './worktree-fingerprint.mjs'
|
|
9
10
|
|
|
10
11
|
const DEFAULTS = {
|
|
11
12
|
ttl: 600_000,
|
|
12
13
|
staleThreshold: 1_800_000,
|
|
13
14
|
waitTimeout: 1_200_000,
|
|
14
|
-
pollInterval: 1_500
|
|
15
|
+
pollInterval: 1_500
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function isAlive(pid) {
|
|
18
|
-
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0)
|
|
21
|
+
return true
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function makeRelease(lockDir) {
|
|
@@ -67,18 +69,23 @@ export async function withLock(key, runFn, opts = {}) {
|
|
|
67
69
|
}
|
|
68
70
|
try {
|
|
69
71
|
fs.mkdirSync(lockDir)
|
|
70
|
-
fs.writeFileSync(
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
ownerFile,
|
|
74
|
+
JSON.stringify({ pid: process.pid, host: os.hostname(), startedAt: Date.now(), fingerprint })
|
|
75
|
+
)
|
|
71
76
|
locked = true
|
|
72
77
|
break
|
|
73
78
|
} catch (error) {
|
|
74
79
|
if (error.code !== 'EEXIST') throw error
|
|
75
80
|
let owner
|
|
76
|
-
try {
|
|
81
|
+
try {
|
|
82
|
+
owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'))
|
|
83
|
+
} catch {
|
|
77
84
|
fs.rmSync(lockDir, { recursive: true, force: true })
|
|
78
85
|
continue
|
|
79
86
|
}
|
|
80
|
-
const stale =
|
|
81
|
-
(os.hostname() === owner.host && !isAlive(owner.pid))
|
|
87
|
+
const stale =
|
|
88
|
+
Date.now() - owner.startedAt > staleThreshold || (os.hostname() === owner.host && !isAlive(owner.pid))
|
|
82
89
|
if (stale) {
|
|
83
90
|
console.error(`🧹 ${key}: знайдено застарілий лок — очищаю`)
|
|
84
91
|
fs.rmSync(lockDir, { recursive: true, force: true })
|
|
@@ -100,9 +107,14 @@ export async function withLock(key, runFn, opts = {}) {
|
|
|
100
107
|
release()
|
|
101
108
|
return 0
|
|
102
109
|
}
|
|
103
|
-
} catch {
|
|
110
|
+
} catch {
|
|
111
|
+
/* result.json не існує або пошкоджений */
|
|
112
|
+
}
|
|
104
113
|
|
|
105
|
-
const onSignal = () => {
|
|
114
|
+
const onSignal = () => {
|
|
115
|
+
release()
|
|
116
|
+
process.exit(130)
|
|
117
|
+
}
|
|
106
118
|
process.once('SIGINT', onSignal)
|
|
107
119
|
process.once('SIGTERM', onSignal)
|
|
108
120
|
|
|
@@ -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()
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: >-
|
|
3
|
-
Запустити всі програмні перевірки правил (`npx @nitra/cursor fix`)
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# n-check
|
|
7
|
-
|
|
8
|
-
Запусти `npx @nitra/cursor fix` і пройдися по результатах.
|
|
9
|
-
|
|
10
|
-
- Якщо є помилки — виправи відповідно до правила, на яке вказує перевірка.
|
|
11
|
-
- Якщо все чисто — підтверди коротким повідомленням і переходь до наступного кроку.
|