@nitra/cursor 3.18.2 → 3.20.0

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 (31) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/n-cursor.js +12 -0
  3. package/package.json +1 -1
  4. package/rules/docker/docker.mdc +3 -3
  5. package/rules/docker/js/lint.mjs +1 -1
  6. package/rules/docker/lib/docker-hadolint.mjs +27 -55
  7. package/rules/ga/lint/lint.mjs +18 -54
  8. package/rules/image-compress/meta.json +1 -1
  9. package/rules/k8s/lint/lint.mjs +3 -10
  10. package/rules/nginx-default-tpl/js/template.mjs +39 -1
  11. package/rules/nginx-default-tpl/nginx-default-tpl.mdc +3 -1
  12. package/rules/npm-module/js/package_structure.mjs +40 -9
  13. package/rules/npm-module/npm-module.mdc +1 -1
  14. package/rules/npm-module/policy/npm_publish_yml/target.json +1 -0
  15. package/rules/rego/lint/lint.mjs +10 -55
  16. package/rules/text/lint/lint.mjs +11 -40
  17. package/rules/worktree/policy/vscode_settings/target.json +5 -0
  18. package/rules/worktree/policy/vscode_settings/template/settings.json.snippet.json +8 -0
  19. package/rules/worktree/policy/zed_settings/target.json +5 -0
  20. package/rules/worktree/policy/zed_settings/template/settings.json.snippet.json +12 -0
  21. package/rules/worktree/worktree.mdc +52 -0
  22. package/schemas/target.json +5 -0
  23. package/scripts/lib/assert-project-root.mjs +74 -0
  24. package/scripts/lib/ensure-tool.mjs +352 -0
  25. package/scripts/lib/run-conftest-batch.mjs +6 -28
  26. package/scripts/lib/run-rule.mjs +61 -5
  27. package/scripts/lib/template.mjs +29 -3
  28. package/scripts/lib/worktree-notice.mjs +52 -1
  29. package/skills/fix/SKILL.md +4 -4
  30. package/types/bin/n-cursor.d.ts +1 -1
  31. package/rules/npm-module/policy/npm_publish_yml/npm_publish_yml.rego +0 -87
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): preflight на `opa` і `regal`,
3
- * далі послідовно `opa check --strict`, `regal lint` і опційний
4
- * `conftest verify` (для `*_test.rego`-файлів) якщо conftest у PATH.
2
+ * Лінт Rego-полісі (`conftest.mdc` + `rego.mdc`): `ensureTool` на `opa` і `regal`
3
+ * (авто-install per-platform або hard-fail), далі послідовно `opa check --strict`,
4
+ * `regal lint` і опційний `conftest verify` (для `*_test.rego`-файлів) якщо conftest у PATH.
5
5
  *
6
6
  * Чому два-три інструменти:
7
7
  * - `opa check --strict` — компіляція з типами і строгим режимом (мертвий код, неоднозначні
@@ -13,10 +13,10 @@
13
13
  * Якщо conftest відсутній у PATH — пропускаємо без помилки (тести опційні в локальному середовищі;
14
14
  * у CI потрібно встановити conftest).
15
15
  *
16
- * Без preflight-у на бінарники лінт мовчки злетить з невиразним повідомленням від shell —
17
- * друкуємо явні install-hints (як це робить `lint-ga.mjs` для shellcheck/uv). `opa` додатково
18
- * потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через `opa fmt`) — деталі в
19
- * `mdc/rego.mdc`.
16
+ * `opa`/`regal` резолвляться через `ensureTool` (PATH кеш авто-install brew/scoop/GitHub
17
+ * Release hard-fail) без них лінт мовчки злетів би з невиразним повідомленням від shell.
18
+ * `opa` додатково потрібен VS Code-розширенню `tsandall.opa` (LSP, format-on-save через
19
+ * `opa fmt`) — деталі в `mdc/rego.mdc`.
20
20
  *
21
21
  * Цілі лінту: `npm/rules/` (де живуть Rego-полісі пакета `@nitra/cursor` — у
22
22
  * `npm/rules/<id>/policy/<concern>/`). Усі три інструменти приймають один шлях
@@ -31,47 +31,13 @@ import { existsSync } from 'node:fs'
31
31
  import { resolve } from 'node:path'
32
32
 
33
33
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
34
+ import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
34
35
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
35
36
  import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
36
37
 
37
38
  /** Шляхи з Rego-полісі (відносно cwd). Існують не всі на ранніх стадіях — фільтруємо нижче. */
38
39
  const LINT_TARGETS = ['npm/rules']
39
40
 
40
- /**
41
- * Друкує підказку зі встановлення `opa` (потрібен для `opa check --strict` і VS Code LSP).
42
- * @returns {void}
43
- */
44
- function printOpaInstallHints() {
45
- process.stderr.write(
46
- [
47
- '❌ opa не знайдено в PATH.',
48
- ' Без нього не запускається `opa check --strict` (типи + мертвий код у *.rego),',
49
- ' і не працює VS Code-розширення `tsandall.opa` (LSP, format-on-save через opa fmt).',
50
- ' Встанови:',
51
- ' macOS: brew install opa',
52
- ' Universal: https://www.openpolicyagent.org/docs/latest/#1-download-opa',
53
- ''
54
- ].join('\n')
55
- )
56
- }
57
-
58
- /**
59
- * Друкує підказку зі встановлення `regal`.
60
- * @returns {void}
61
- */
62
- function printRegalInstallHints() {
63
- process.stderr.write(
64
- [
65
- '❌ regal не знайдено в PATH.',
66
- ' Без нього не перевіряється rego.v1 синтаксис у *.rego (правило `conftest`).',
67
- ' Встанови:',
68
- ' macOS: brew install regal',
69
- ' Universal: https://docs.styra.com/regal#installation',
70
- ''
71
- ].join('\n')
72
- )
73
- }
74
-
75
41
  /**
76
42
  * Запускає крок з відображенням команди користувачу. Stdout/stderr передаємо як є
77
43
  * (`stdio: 'inherit'`), щоб виглядало як прямий виклик у shell.
@@ -101,19 +67,8 @@ function runStep(bin, args, cwd) {
101
67
  */
102
68
  export function runLintRegoSteps(cwd = process.cwd()) {
103
69
  const root = resolve(cwd)
104
- const opa = resolveCmd('opa')
105
- const regal = resolveCmd('regal')
106
-
107
- let preflightOk = true
108
- if (!opa) {
109
- printOpaInstallHints()
110
- preflightOk = false
111
- }
112
- if (!regal) {
113
- printRegalInstallHints()
114
- preflightOk = false
115
- }
116
- if (!preflightOk) return 1
70
+ const opa = ensureTool('opa')
71
+ const regal = ensureTool('regal')
117
72
 
118
73
  const targets = LINT_TARGETS.filter(rel => existsSync(resolve(root, rel)))
119
74
  if (targets.length === 0) {
@@ -1,6 +1,7 @@
1
1
  /**
2
- * CLI-обгортка над канонічним `lint-text` (text.mdc): preflight на `shellcheck`, `patch`
3
- * (для авто-фіксу shellcheck) і `dotenv-linter`; далі послідовно
2
+ * CLI-обгортка над канонічним `lint-text` (text.mdc): авто-встановлює `shellcheck` і `dotenv-linter`
3
+ * через `ensureTool` (brew/scoop/GitHub Release per-platform), перевіряє наявність `patch`
4
+ * (для авто-фіксу shellcheck); далі послідовно
4
5
  * 1) `cspell .` — перевірка правопису з `@nitra/cspell-dict`;
5
6
  * 2) `runShellcheckText()` — авто-фікс і фінальна перевірка `*.sh` через `shellcheck`;
6
7
  * 3) `runDotenvLinter()` — авто-фікс і фінальна перевірка `.env*` через `dotenv-linter`;
@@ -9,7 +10,7 @@
9
10
  *
10
11
  * Без preflight локальний прогін може пройти cspell/markdownlint, а CI на ubuntu-latest
11
12
  * (де shellcheck передвстановлений, але dotenv-linter — ні) падає на кроці dotenv-linter
12
- * з неінформативним повідомленням. Preflight збирає всі відсутні бінарники до першого кроку.
13
+ * з неінформативним повідомленням. ensureTool збирає всі відсутні бінарники до першого кроку.
13
14
  *
14
15
  * Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
15
16
  * Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
@@ -22,6 +23,7 @@ import { platform } from 'node:process'
22
23
  import { runLintStep } from '../../../scripts/lib/run-lint-step.mjs'
23
24
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
24
25
  import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
26
+ import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
25
27
  import { runDotenvLinter } from './run-dotenv-linter.mjs'
26
28
  import { runShellcheckText } from './run-shellcheck.mjs'
27
29
  import { runV8rWithGlobs } from './run-v8r.mjs'
@@ -36,22 +38,6 @@ import { runV8rWithGlobs } from './run-v8r.mjs'
36
38
  * @property {string} successMsg повідомлення на pass-шлях
37
39
  */
38
40
 
39
- /** @type {PreflightDep} */
40
- const SHELLCHECK_PREFLIGHT = {
41
- bin: 'shellcheck',
42
- winBins: ['shellcheck.exe'],
43
- explanation: [
44
- 'Без нього `runShellcheckText()` пропускає перевірку tracked `*.sh` — локально lint-text',
45
- 'може бути зеленим, а CI (shellcheck + patch) падає на тих самих скриптах.'
46
- ].join('\n '),
47
- install: [
48
- 'macOS: brew install shellcheck',
49
- 'Debian/Ubuntu: sudo apt-get install -y shellcheck',
50
- 'Arch: sudo pacman -S shellcheck'
51
- ],
52
- successMsg: '✅ shellcheck знайдено в PATH — lint-text перевірить *.sh'
53
- }
54
-
55
41
  /** @type {PreflightDep} */
56
42
  const PATCH_PREFLIGHT = {
57
43
  bin: 'patch',
@@ -62,22 +48,6 @@ const PATCH_PREFLIGHT = {
62
48
  successMsg: '✅ patch знайдено в PATH — shellcheck auto-fix працюватиме'
63
49
  }
64
50
 
65
- /** @type {PreflightDep} */
66
- const DOTENV_LINTER_PREFLIGHT = {
67
- bin: 'dotenv-linter',
68
- winBins: ['dotenv-linter.exe'],
69
- explanation: [
70
- 'Без нього не виконається крок `.env*` у lint-text — локально cspell/markdownlint',
71
- 'пройдуть, а CI без Install dotenv-linter впаде неінформативно.'
72
- ].join('\n '),
73
- install: [
74
- 'macOS: brew install dotenv-linter',
75
- 'Linux: curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin',
76
- 'cargo: cargo install dotenv-linter'
77
- ],
78
- successMsg: '✅ dotenv-linter знайдено в PATH — lint-text перевірить .env*'
79
- }
80
-
81
51
  /**
82
52
  * Шукає шлях до бінарника `dep.bin` у `PATH`; на Windows додатково перебирає `dep.winBins`.
83
53
  * @param {PreflightDep} dep опис залежності з canon-списку preflight-перевірок
@@ -128,11 +98,12 @@ function preflight(dep) {
128
98
  * @returns {number} 0 — все OK, інакше — код першого кроку, що впав
129
99
  */
130
100
  function runLintTextSteps() {
131
- let preflightOk = true
132
- for (const dep of [SHELLCHECK_PREFLIGHT, PATCH_PREFLIGHT, DOTENV_LINTER_PREFLIGHT]) {
133
- if (!preflight(dep)) preflightOk = false
134
- }
135
- if (!preflightOk) return 1
101
+ // Auto-install: throws on failure → propagates as exit 1 from runStandardLint
102
+ ensureTool('shellcheck')
103
+ ensureTool('dotenv-linter')
104
+
105
+ // patch is hint-only (system tool)
106
+ if (!preflight(PATCH_PREFLIGHT)) return 1
136
107
 
137
108
  const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.'])
138
109
  if (cspellCode !== 0) return cspellCode
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "check": "template",
4
+ "files": { "single": ".vscode/settings.json" }
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "search.exclude": {
3
+ "**/.worktrees/**": true
4
+ },
5
+ "files.exclude": {
6
+ "**/.worktrees/**": true
7
+ }
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "check": "template",
4
+ "files": { "single": ".zed/settings.json" }
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "file_scan_exclusions": [
3
+ "**/.git",
4
+ "**/.svn",
5
+ "**/.hg",
6
+ "**/.DS_Store",
7
+ "**/Thumbs.db",
8
+ "**/node_modules",
9
+ "**/.worktrees",
10
+ "**/.claude/worktrees"
11
+ ]
12
+ }
@@ -20,6 +20,58 @@ alwaysApply: true
20
20
  Слеш у гілці перетворюється на дефіс: `feat/skill-meta` → `.worktrees/feat-skill-meta/`.
21
21
  Git-гілка лишається з оригінальним імʼям (`feat/skill-meta`).
22
22
 
23
+ ## Налаштування редакторів
24
+
25
+ Worktree-каталоги — це вкладені копії всього дерева. Якщо редактор індексує їх,
26
+ пошук дає дублі, а file-watcher вантажить диск. Тому редактор має **виключити**
27
+ `.worktrees/` (а для Zed ще й приватний `.claude/worktrees/`) з пошуку та сканування.
28
+
29
+ Канон перевіряється **лише** якщо файл налаштувань уже є — проєктам без VS Code / Zed
30
+ нічого не нав'язується.
31
+
32
+ ### VS Code
33
+
34
+ У `.vscode/settings.json` обовʼязково виключи `**/.worktrees/**` з пошуку і з дерева
35
+ файлів. `node_modules` додавати не треба — VS Code виключає його з пошуку дефолтно.
36
+
37
+ ```json title=".vscode/settings.json"
38
+ {
39
+ "search.exclude": {
40
+ "**/.worktrees/**": true
41
+ },
42
+ "files.exclude": {
43
+ "**/.worktrees/**": true
44
+ }
45
+ }
46
+ ```
47
+
48
+ Канон (subset — інші ключі дозволені): [settings.json.snippet.json](./policy/vscode_settings/template/settings.json.snippet.json)
49
+
50
+ ### Zed
51
+
52
+ Zed **замінює** масив `file_scan_exclusions` цілком (не зливає з дефолтами), тому в
53
+ `.zed/settings.json` перелічуй і дефолти Zed, і `**/.worktrees`, і `**/.claude/worktrees`.
54
+ Перевірка вимагає **всі** записи канону (бо інакше дефолти губляться).
55
+
56
+ ```json title=".zed/settings.json"
57
+ {
58
+ "file_scan_exclusions": [
59
+ "**/.git",
60
+ "**/.svn",
61
+ "**/.hg",
62
+ "**/.DS_Store",
63
+ "**/Thumbs.db",
64
+ "**/node_modules",
65
+ "**/.worktrees",
66
+ "**/.claude/worktrees"
67
+ ]
68
+ }
69
+ ```
70
+
71
+ Канон (усі елементи обовʼязкові, зайві дозволені): [settings.json.snippet.json](./policy/zed_settings/template/settings.json.snippet.json)
72
+
73
+ `.zed/settings.json` без стабільної схеми в Schema Store — додай його до `.v8rignore` (як `.vscode/*`, див. **text.mdc**).
74
+
23
75
  ## Команди
24
76
 
25
77
  - **Створити** (опис обовʼязковий): `npx @nitra/cursor worktree add <branch> "<навіщо>"`
@@ -12,6 +12,11 @@
12
12
  "format": "uri",
13
13
  "description": "Опційне посилання на JSON Schema для IDE-валідації; очікувано https://unpkg.com/@nitra/cursor/schemas/target.json"
14
14
  },
15
+ "check": {
16
+ "type": "string",
17
+ "enum": ["template"],
18
+ "description": "Режим перевірки концерну. 'template' → без власного <name>.rego: канон зі template/<target>.snippet|deny|contains.<ext> звіряється generic deep-subset-ом (checkSnippet/checkDeny/checkContains/checkTextSubset). Сніпет — єдине джерело істини: його зміна одразу змінює enforce, без правок rego й міграторів. Без поля → класичний rego-режим (пара з <name>.rego)."
19
+ },
15
20
  "files": {
16
21
  "oneOf": [
17
22
  {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Guard для дефолтної синхронізації `npx @nitra/cursor` (гілка без підкоманди).
3
+ *
4
+ * Дефолтний sync (`runSync` у `bin/n-cursor.js`) скаффолдить у `cwd()` керовані
5
+ * артефакти — `.cursor/rules/`, `.cursor/skills/`, `.claude/`, `AGENTS.md`,
6
+ * `CLAUDE.md`, `.n-cursor.json`, `.gitignore` — і запускає `bun install`. Усе це
7
+ * розраховане на **корінь** проєкту-споживача. Якщо бінар викликати напряму
8
+ * (`bun npm/bin/n-cursor.js`, `node …/n-cursor.js`) з піддиректорії git-репо,
9
+ * `cwd()` — та піддиректорія, і конфіг розкидається не туди.
10
+ *
11
+ * `bun run start`/`npm start` цей кейс не створює (менеджер скидає cwd на корінь
12
+ * пакета), але прямий виклик бінаря — створює. Тому guard прив'язаний до
13
+ * **git-кореня**, а не до конкретного монорепо: CLI публічний і легітимно
14
+ * запускається в корені будь-якого репо-споживача.
15
+ */
16
+ import { spawnSync } from 'node:child_process'
17
+ import { realpathSync } from 'node:fs'
18
+ import { cwd } from 'node:process'
19
+
20
+ /**
21
+ * Корінь git-репозиторію для `dir` через `git rev-parse --show-toplevel`.
22
+ * Повертає realpath-шлях кореня або `null`, якщо `dir` поза git-репо чи `git`
23
+ * недоступний — у такому разі визначити корінь неможливо, тож не блокуємо.
24
+ * @param {string} [dir] каталог, з якого питаємо git
25
+ * @returns {string | null} абсолютний (realpath) корінь репо або null
26
+ */
27
+ export function gitToplevel(dir = cwd()) {
28
+ const res = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: dir, encoding: 'utf8' })
29
+ if (res.status !== 0 || typeof res.stdout !== 'string') return null
30
+ const top = res.stdout.trim()
31
+ return top.length > 0 ? top : null
32
+ }
33
+
34
+ /**
35
+ * Безпечний realpath: повертає реальний шлях `dir`, а якщо його не існує —
36
+ * сам `dir` без змін (порівняння нижче все одно дасть коректний результат).
37
+ * Потрібен бо `git rev-parse --show-toplevel` віддає realpath (symlink'и
38
+ * розгорнуті), а `cwd()` — ні; без нормалізації корінь під symlink-шляхом
39
+ * (типово `/var` → `/private/var` на macOS) хибно вважався б піддиректорією.
40
+ * @param {string} dir шлях для нормалізації
41
+ * @returns {string} realpath або вихідний шлях
42
+ */
43
+ function safeRealpath(dir) {
44
+ try {
45
+ return realpathSync(dir)
46
+ } catch {
47
+ return dir
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Кидає помилку, якщо `dir` — піддиректорія git-репозиторію (тобто не його
53
+ * корінь). Поза git-репо (немає toplevel) — пропускає без помилки.
54
+ *
55
+ * Викликати лише перед дефолтним sync (до перших мутацій файлів), не для
56
+ * підкоманд із власним `--root` чи read-only-логікою.
57
+ * @param {string} [dir] каталог, що перевіряємо (типово `cwd()`)
58
+ * @throws {Error} коли `dir` всередині git-репо, але не його корінь
59
+ * @returns {void}
60
+ */
61
+ export function assertCwdIsProjectRoot(dir = cwd()) {
62
+ const top = gitToplevel(dir)
63
+ if (top === null) return
64
+ const here = safeRealpath(dir)
65
+ if (here === top) return
66
+ throw new Error(
67
+ `❌ @nitra/cursor запущено не в корені проєкту.\n` +
68
+ ` Поточний каталог: ${here}\n` +
69
+ ` Корінь git-репо: ${top}\n` +
70
+ ` Дефолтна синхронізація скаффолдить .cursor/, .claude/, CLAUDE.md, .n-cursor.json\n` +
71
+ ` і робить bun install у поточному каталозі — із піддиректорії це розкидало б\n` +
72
+ ` конфіг не туди. Перейдіть у корінь репозиторію: cd ${top}`
73
+ )
74
+ }