@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.20.0] - 2026-06-03
4
+
5
+ ### Added
6
+
7
+ - ensure-tool: авто-встановлення зовнішніх CLI-залежностей (hk, conftest, shellcheck, actionlint, dotenv-linter) — brew/scoop/GitHub Release per-platform; hk install після fix; conftest авто-встановлюється перед fix та lint-ga
8
+ - ensureTool: розширено на opa/regal/hadolint/kubeconform/kubescape (brew/scoop/GitHub Release per-platform) + підтримка сирих бінарників (archive:false → download+chmod, без tar). Мігровано call-sites rego-lint (opa/regal), docker (hadolint з docker-fallback), k8s-lint (kubeconform/kubescape). withBinRemovedFromPath виставляє N_CURSOR_NO_AUTO_INSTALL=1.
9
+ - Guard: дефолтний sync (`npx @nitra/cursor` без підкоманди) забороняє запуск із піддиректорії git-репо — STOP до мутацій, замість скаффолда .cursor/.claude/CLAUDE.md/.n-cursor.json не в той каталог
10
+
11
+ ### Changed
12
+
13
+ - worktree-only скіли: bootstrap-виклик npx @nitra/cursor у новоствореному worktree тепер ретраїться при транзитних помилках реєстру/CDN (ETARGET/notarget, ENOTFOUND, ETIMEDOUT, EAI_AGAIN, ECONNRESET, 5xx) кожні 30с до 5 хв (env N_CURSOR_NPX_RETRY_MAX_MIN, ceiling 10 хв); реальний nonzero CLI віддається одразу. worktree-notice додає bun install у дереві (локальна копія усуває гонку з CDN) і shell-обгортку n_cursor_npx; fix-скіл кроки 1/6 використовують її
14
+ - npm-module: npm_publish_yml тепер звіряє ВЕСЬ канонічний сніпет напряму (target.json "check":"template", generic deep-subset) замість bespoke subset-of rego — редагування сніпета одразу змінює enforce, без правок rego й міграторів; масиви (steps) матчаться структурним subset-ом за наявністю (order/key-insensitive, зайві кроки дозволені); legacy publish-only workflow тепер падає check (вимагає release-publish job). Новий режим check:template перевикористовний для будь-якого whole-file концерну зі сніпетом.
15
+ - docker hadolint: прибрано docker-run fallback — hadolint тепер лише нативний бінарник через ensureTool (brew/scoop/GitHub Release). Видалено HADOLINT_IMAGE; оновлено docker.mdc і тести.
16
+
17
+ ### Fixed
18
+
19
+ - nginx-default-tpl: error_log off → error_log /dev/null crit; (error_log off — НЕ валідний nginx, падає під readOnlyRootFilesystem); авто-заміна в шаблонах + оновлено канон .mdc/фікстуру
20
+
21
+ ## [3.19.0] - 2026-06-03
22
+
23
+ ### Changed
24
+
25
+ - image-compress переведено на glob-активацію (**/*.{png,jpg,jpeg,gif,svg}) замість залежності від bun — у проєктах без растрів/SVG правило більше не додається автоматично; globToRegex отримав підтримку brace-альтернатив {a,b,c}
26
+
3
27
  ## [3.18.2] - 2026-06-03
4
28
 
5
29
  ### Changed
package/bin/n-cursor.js CHANGED
@@ -96,6 +96,7 @@ import { runPostToolUseFixCli } from '../scripts/post-tool-use-fix.mjs'
96
96
  import { discoverCheckRulesFromCursorRules } from '../scripts/lib/discover-check-rules-from-cursor.mjs'
97
97
  import { listRuleIds } from '../scripts/lib/list-rule-ids.mjs'
98
98
  import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
99
+ import { assertCwdIsProjectRoot } from '../scripts/lib/assert-project-root.mjs'
99
100
  import { runLintDocker } from '../rules/docker/lint/lint.mjs'
100
101
  import { runLintGaCli } from '../rules/ga/lint/lint.mjs'
101
102
  import { runLintK8s } from '../rules/k8s/lint/lint.mjs'
@@ -110,6 +111,7 @@ import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
110
111
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
111
112
  import { runLint } from '../scripts/lint-cli.mjs'
112
113
  import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
114
+ import { ensureHkInstall, ensureTool } from '../scripts/lib/ensure-tool.mjs'
113
115
 
114
116
  const PACKAGE_NAME = '@nitra/cursor'
115
117
  const CONFIG_FILE = '.n-cursor.json'
@@ -1184,6 +1186,10 @@ function logRemovedManagedItems(title, basePath, names) {
1184
1186
  * @returns {Promise<void>}
1185
1187
  */
1186
1188
  async function runFixCommand(requestedRules) {
1189
+ const hkBin = ensureTool('hk')
1190
+ ensureHkInstall(hkBin)
1191
+ ensureTool('conftest')
1192
+
1187
1193
  const available = await listRuleIds(BUNDLED_RULES_DIR)
1188
1194
  if (available.length === 0) {
1189
1195
  console.error('❌ Не знайдено жодного правила у пакеті')
@@ -1457,6 +1463,12 @@ async function runSync() {
1457
1463
  const [command, ...args] = process.argv.slice(2)
1458
1464
 
1459
1465
  try {
1466
+ // Дефолтний sync (без підкоманди) скаффолдить .cursor/.claude/CLAUDE.md/.n-cursor.json
1467
+ // і робить bun install у cwd(). Guard до перших мутацій: заборона запуску з піддиректорії
1468
+ // git-репо (типово прямий `bun npm/bin/n-cursor.js` не з кореня). Підкоманди не зачіпає.
1469
+ if (command === undefined || command === '') {
1470
+ assertCwdIsProjectRoot()
1471
+ }
1460
1472
  await ensureNitraCursorInRootDevDependencies(cwd())
1461
1473
  switch (command) {
1462
1474
  case 'fix': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.18.2",
3
+ "version": "3.20.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -181,7 +181,7 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
181
181
 
182
182
  **Область lint-docker (вужча, ніж `check docker`):** лише файли з іменем **`Dockerfile`** та **`*.Dockerfile`** (суфікс **`.dockerfile`** без урахування регістру, наприклад **`api.Dockerfile`**). Файли **`Dockerfile.prod`**, **`Containerfile`** тощо **не** входять у **`lint-docker`**; їх ловить **`check docker`** (`rules/docker/fix.mjs`).
183
183
 
184
- Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`rules/docker/fix.mjs`**. Виклик **`hadolint`**: **`PATH`**, інакше **`docker run`** — спільна логіка **`npm/rules/docker/js/lint/docker-hadolint.mjs`**.
184
+ Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`rules/docker/fix.mjs`**. Виклик **`hadolint`** як **нативного бінарника** через **`ensureTool`** (PATH кеш → авто-install brew/scoop/GitHub Release; **без** `docker run`) — спільна логіка **`npm/rules/docker/lib/docker-hadolint.mjs`**.
185
185
 
186
186
  - Канон `package.json#scripts.lint-docker`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
187
187
 
@@ -191,14 +191,14 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
191
191
 
192
192
  - Канон: [lint-docker.yml.snippet.yml](./policy/lint_docker_yml/template/lint-docker.yml.snippet.yml)
193
193
 
194
- Узгоджуй версію hadolint **v2.12.0** з **`HADOLINT_IMAGE`** у **`npm/rules/docker/js/lint/docker-hadolint.mjs`**.
194
+ Локально hadolint авто-встановлюється через **`ensureTool`** (latest, без піну версії). У CI встанови його кроком із workflow-сніпета (curl-download бінарника — без `docker run`).
195
195
 
196
196
  Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) **обов'язково** містить **`bun run lint-docker`**, коли в проєкті підключено правило **`docker`**.
197
197
 
198
198
  ## Запуск
199
199
 
200
200
  1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
201
- 2. **`npx @nitra/cursor fix docker`** — **`rules/docker/fix.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`PATH`** або **`docker run`** з **`hadolint/hadolint:v2.12.0`**).
201
+ 2. **`npx @nitra/cursor fix docker`** — **`rules/docker/fix.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (нативний бінарник через **`ensureTool`**; **без** `docker run`).
202
202
  3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
203
203
 
204
204
  ```yaml title=".hadolint.yaml"
@@ -29,7 +29,7 @@
29
29
  * `USER` у Dockerfile — перевірка non-root для нього пропускається.
30
30
  *
31
31
  * Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
32
- * тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
32
+ * тощо. hadolint нативний бінарник через `ensureTool` (PATH/кеш/авто-install; без docker run).
33
33
  * Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
34
34
  */
35
35
  import { readFile } from 'node:fs/promises'
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Спільна логіка виклику hadolint для шляхів до Dockerfile (див. docker.mdc).
3
3
  *
4
- * Відносні шляхи з прямими слешами для контейнера; спочатку hadolint з PATH,
5
- * інакше docker run з образом HADOLINT_IMAGE. Використовується `./check.mjs`
6
- * (check-docker) та `../../lint/lint.mjs` (run-docker).
4
+ * Відносні шляхи з прямими слешами; hadolint резолвиться через `ensureTool`
5
+ * (PATH кеш авто-install brew/scoop/GitHub Release per-platform). Docker-fallback
6
+ * прибрано hadolint ставиться як **нативний бінарник**, без `docker run`.
7
+ * Використовується `./check.mjs` (check-docker) та `../../lint/lint.mjs` (run-docker).
7
8
  */
8
9
  import { spawnSync } from 'node:child_process'
9
10
  import { relative, sep } from 'node:path'
10
11
 
11
- import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
12
-
13
- /** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
14
- export const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
12
+ import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
15
13
 
16
14
  /**
17
- * Відносний шлях від root з прямими слешами (hadolint у контейнері).
15
+ * Відносний шлях від root з прямими слешами (стабільний вивід незалежно від OS).
18
16
  * @param {string} root корінь
19
17
  * @param {string} absPath абсолютний шлях
20
18
  * @returns {string} відносний шлях з прямими слешами
@@ -24,65 +22,39 @@ export function posixRel(root, absPath) {
24
22
  }
25
23
 
26
24
  /**
27
- * Запуск hadolint: спочатку PATH, інакше Docker.
25
+ * Запуск hadolint як нативного бінарника. hadolint резолвиться через `ensureTool`
26
+ * (PATH → кеш → авто-install); якщо авто-install відключено (`N_CURSOR_NO_AUTO_INSTALL`)
27
+ * чи не вдався — повертаємо `ok: false` з підказкою (без `docker run`).
28
28
  * @param {string} root корінь репозиторію
29
29
  * @param {string} absPath абсолютний шлях до Dockerfile
30
- * @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint та канал запуску
30
+ * @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint
31
31
  */
32
32
  export function lintDockerfileWithHadolint(root, absPath) {
33
33
  const rel = posixRel(root, absPath)
34
- const hadolintPath = resolveCmd('hadolint')
35
- if (hadolintPath) {
36
- const local = spawnSync(hadolintPath, [rel], {
37
- cwd: root,
38
- encoding: 'utf8',
39
- maxBuffer: 10 * 1024 * 1024
40
- })
41
- const ok = local.status === 0
42
- return {
43
- ok,
44
- stdout: local.stdout ?? '',
45
- stderr: local.stderr ?? '',
46
- via: 'hadolint'
47
- }
48
- }
49
-
50
- const dockerPath = resolveCmd('docker')
51
- if (!dockerPath) {
34
+ let hadolintPath
35
+ try {
36
+ hadolintPath = ensureTool('hadolint')
37
+ } catch (error) {
52
38
  return {
53
39
  ok: false,
54
40
  stdout: '',
55
41
  stderr:
56
- 'Не знайдено hadolint у PATH і не знайдено docker у PATH. ' +
57
- 'Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).',
58
- via: 'docker'
42
+ `Не вдалося отримати hadolint (${error.message}). ` +
43
+ 'Встанови: brew install hadolint (macOS) / scoop install hadolint (Windows) / ' +
44
+ 'https://github.com/hadolint/hadolint/releases (Linux).',
45
+ via: 'hadolint'
59
46
  }
60
47
  }
61
48
 
62
- const docker = spawnSync(
63
- dockerPath,
64
- ['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel],
65
- {
66
- cwd: root,
67
- encoding: 'utf8',
68
- maxBuffer: 10 * 1024 * 1024
69
- }
70
- )
71
- if (docker.error) {
72
- return {
73
- ok: false,
74
- stdout: '',
75
- stderr:
76
- `Не знайдено hadolint у PATH і не вдалося запустити Docker (${docker.error.message}). ` +
77
- `Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).`,
78
- via: 'docker'
79
- }
80
- }
81
- const ok = docker.status === 0
49
+ const local = spawnSync(hadolintPath, [rel], {
50
+ cwd: root,
51
+ encoding: 'utf8',
52
+ maxBuffer: 10 * 1024 * 1024
53
+ })
82
54
  return {
83
- ok,
84
- stdout: docker.stdout ?? '',
85
- stderr: docker.stderr ?? '',
86
- via: 'docker'
55
+ ok: local.status === 0,
56
+ stdout: local.stdout ?? '',
57
+ stderr: local.stderr ?? '',
58
+ via: 'hadolint'
87
59
  }
88
60
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * CLI-обгортка над канонічним `lint-ga` (ga.mdc): робить preflight на `shellcheck`, `uv` (для `uvx`)
3
- * і `conftest` (для rego-полісі у `check-ga`),
2
+ * CLI-обгортка над канонічним `lint-ga` (ga.mdc): авто-встановлює `shellcheck` і `conftest`
3
+ * через `ensureTool` (brew/scoop/GitHub Release per-platform), перевіряє наявність `uv` (для `uvx`),
4
4
  * тоді послідовно виконує `bunx github-actionlint`, `uvx zizmor --offline --collect=workflows .` і
5
5
  * делегує до `rules/ga/fix.mjs::check()` — там і Rego-частина (через `runConftestBatch`),
6
6
  * і JS cross-file перевірки правил `ga.mdc`.
@@ -13,14 +13,10 @@
13
13
  *
14
14
  * Без preflight `actionlint` (через `bunx github-actionlint`) мовчки пропускає shell-перевірки в
15
15
  * `run:` блоках, коли `shellcheck` відсутній у PATH; локально `bun lint-ga` лишається зеленим, а CI
16
- * на ubuntu-latest (де shellcheck передвстановлений) падає. Preflight робить цю різницю явною.
16
+ * на ubuntu-latest (де shellcheck передвстановлений) падає. ensureTool('shellcheck') усуває цю різницю.
17
17
  *
18
- * `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає неінформативно («command not
19
- * found»); підказка з командою встановлення коротша й корисніша.
20
- *
21
- * `conftest` потрібен для `rules/ga/fix.mjs::runAllGaRego` (`runConftestBatch`). Без preflight крок
22
- * check-ga кидає виняток, який глобальний `catch` у `bin/n-cursor.js` раніше ковтав без логу —
23
- * локально це виглядало як мовчазний exit 1.
18
+ * `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає неінформативно; підказка
19
+ * з командою встановлення коротша й корисніша. `uv` не в реєстрі ensureTool → hint-only.
24
20
  *
25
21
  * Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
26
22
  *
@@ -33,6 +29,7 @@ import { check as checkGa } from '../js/workflows.mjs'
33
29
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
34
30
  import { runLintStep } from '../../../scripts/lib/run-lint-step.mjs'
35
31
  import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
32
+ import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
36
33
 
37
34
  /**
38
35
  * Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
@@ -44,23 +41,6 @@ import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
44
41
  * @property {string} successMsg повідомлення на pass-шлях
45
42
  */
46
43
 
47
- /** @type {PreflightDep} */
48
- const SHELLCHECK_PREFLIGHT = {
49
- bin: 'shellcheck',
50
- winBins: ['shellcheck.exe'],
51
- explanation: [
52
- 'Без нього `actionlint` пропускає shell-перевірки в run: блоках,',
53
- 'тож локальний прогін зеленіє, а CI на ubuntu-latest (де shellcheck',
54
- 'передвстановлений) падає на тих самих workflow.'
55
- ].join('\n '),
56
- install: [
57
- 'macOS: brew install shellcheck',
58
- 'Debian/Ubuntu: sudo apt-get install -y shellcheck',
59
- 'Arch: sudo pacman -S shellcheck'
60
- ],
61
- successMsg: '✅ shellcheck знайдено в PATH — actionlint виконуватиме SC-правила, як у CI'
62
- }
63
-
64
44
  /** @type {PreflightDep} */
65
45
  const UV_PREFLIGHT = {
66
46
  bin: 'uv',
@@ -77,18 +57,6 @@ const UV_PREFLIGHT = {
77
57
  successMsg: '✅ uv знайдено в PATH — uvx zizmor запуститься'
78
58
  }
79
59
 
80
- /** @type {PreflightDep} */
81
- const CONFTEST_PREFLIGHT = {
82
- bin: 'conftest',
83
- winBins: ['conftest.exe'],
84
- explanation: [
85
- 'Без нього не запускається пер-документна валідація через rego-полісі (npm/rules/*/policy/)',
86
- 'у кроці check-ga — `runConftestBatch` завершується hard-fail.'
87
- ].join('\n '),
88
- install: ['macOS: brew install conftest', 'Universal: https://www.conftest.dev/install/'],
89
- successMsg: '✅ conftest знайдено в PATH — check-ga виконає rego-полісі через runConftestBatch'
90
- }
91
-
92
60
  /**
93
61
  * Шукає бінарник у PATH з урахуванням Windows: спершу `winBins`, потім `bin`.
94
62
  * @param {PreflightDep} dep опис залежності
@@ -134,29 +102,25 @@ function preflight(dep) {
134
102
  }
135
103
 
136
104
  /**
137
- * Виконує канонічний `lint-ga` з preflight-перевірками і делегує до `check-ga.check()`.
105
+ * Виконує канонічний `lint-ga` авто-встановлює shellcheck/conftest, перевіряє uv, запускає actionlint/zizmor/check-ga.
138
106
  *
139
107
  * Послідовність:
140
- * 1) preflight: `shellcheck`, `uv` (для `uvx zizmor`) і `conftest` (для check-ga); відсутній → exit 1;
141
- * 2) `bunx github-actionlint`;
142
- * 3) `uvx zizmor --offline --collect=workflows .`;
143
- * 4) `rules/ga/fix.mjs::check()` Rego-полісі (батч conftest з `npm/policy/ga/`) + JS cross-file
108
+ * 1) ensureTool: `shellcheck` і `conftest` (авто-install або hard-fail);
109
+ * 2) preflight: `uv` (для `uvx zizmor`) — hint-only, без авто-install;
110
+ * 3) `bunx github-actionlint`;
111
+ * 4) `uvx zizmor --offline --collect=workflows .`;
112
+ * 5) `rules/ga/fix.mjs::check()` — Rego-полісі (батч conftest з `npm/policy/ga/`) + JS cross-file
144
113
  * перевірки правил `ga.mdc`. Це **те саме**, що робить `npx \@nitra/cursor check ga`, тож
145
114
  * `lint-ga` тепер є суперсетом перевірки правила: external-tools + check.
146
- *
147
- * Якщо хоча б один preflight не пройшов — виходимо одразу з кодом 1, **до** запуску actionlint/zizmor,
148
- * бо їхні власні повідомлення про відсутність залежностей менш інформативні (особливо для shellcheck —
149
- * actionlint мовчки пропускає SC-правила; ця перевірка — головний сенс обгортки).
150
- *
151
- * Першу помилку від actionlint/zizmor/check повертаємо як код виходу; наступні кроки не запускаються.
152
115
  * @returns {Promise<number>} 0 — все OK, інакше — код першого кроку, що впав
153
116
  */
154
117
  async function runLintGaSteps() {
155
- let preflightOk = true
156
- for (const dep of [SHELLCHECK_PREFLIGHT, UV_PREFLIGHT, CONFTEST_PREFLIGHT]) {
157
- if (!preflight(dep)) preflightOk = false
158
- }
159
- if (!preflightOk) return 1
118
+ // Auto-install: throws on failure → propagates as exit 1 from runStandardLint
119
+ ensureTool('shellcheck')
120
+ ensureTool('conftest')
121
+
122
+ // uv is hint-only (not in auto-install registry)
123
+ if (!preflight(UV_PREFLIGHT)) return 1
160
124
 
161
125
  const actionlintCode = runLintStep('actionlint', 'bunx', ['github-actionlint'])
162
126
  if (actionlintCode !== 0) return actionlintCode
@@ -1 +1 @@
1
- { "auto": ["bun"] }
1
+ { "auto": { "glob": "**/*.{png,jpg,jpeg,gif,svg}" } }
@@ -24,6 +24,7 @@ import { basename, dirname, join, relative } from 'node:path'
24
24
  import { parse } from 'yaml'
25
25
 
26
26
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
27
+ import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
27
28
  import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
28
29
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
29
30
  import { walkDir } from '../../../scripts/utils/walkDir.mjs'
@@ -123,11 +124,7 @@ function runKubeconform(dirs) {
123
124
  '-ignore-missing-schemas',
124
125
  ...dirs
125
126
  ]
126
- const kubeconformPath = resolveCmd('kubeconform')
127
- if (!kubeconformPath) {
128
- console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
129
- return 127
130
- }
127
+ const kubeconformPath = ensureTool('kubeconform')
131
128
  const r = spawnSync(kubeconformPath, args, { stdio: 'inherit', shell: false })
132
129
  if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
133
130
  console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
@@ -297,11 +294,7 @@ async function runKubescape(dirs, root) {
297
294
  if (exceptionsArgs.length > 0) {
298
295
  console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
299
296
  }
300
- const kubescapePath = resolveCmd('kubescape')
301
- if (!kubescapePath) {
302
- console.error(KUBESCAPE_MISSING_HINT)
303
- return 127
304
- }
297
+ const kubescapePath = ensureTool('kubescape')
305
298
  let kubectlPath = null
306
299
  for (const d of dirs) {
307
300
  const kdirs = await findKustomizationDirs(d)
@@ -12,6 +12,9 @@
12
12
  * У дереві від **cwd** усі **default.tpl.conf** стають **default.conf.template**: перейменування, або
13
13
  * якщо **default.conf.template** уже є — він перезаписується вмістом **default.tpl.conf**, після чого
14
14
  * **default.tpl.conf** видаляється. Якщо після міграції шаблону немає — перевірка пропускається (0).
15
+ *
16
+ * Невалідна директива **`error_log off;`** (nginx трактує "off" як ім'я файлу `/etc/nginx/off` і падає під
17
+ * readOnlyRootFilesystem) автоматично замінюється на **`error_log /dev/null crit;`** у кожному шаблоні.
15
18
  */
16
19
  import { existsSync } from 'node:fs'
17
20
  import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
@@ -33,6 +36,11 @@ const FIND_CMD_RE = /\bfind\b/u
33
36
  const GZIP_CMD_RE = /\bgzip\b/u
34
37
  const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
35
38
 
39
+ // `error_log off;` — НЕ валідний nginx: "off" трактується як ім'я файлу (/etc/nginx/off)
40
+ // і падає під readOnlyRootFilesystem. /dev/null — writable device, тому канон — `error_log /dev/null crit;`.
41
+ const ERROR_LOG_OFF_RE = /error_log\s+off\s*;/gu
42
+ const ERROR_LOG_CANONICAL = 'error_log /dev/null crit;'
43
+
36
44
  /**
37
45
  * Збирає абсолютні шляхи до **default.conf.template** у репозиторії; будь-який сегмент
38
46
  * `fixtures/` у шляху виключається — це тестові артефакти (як `tests/fixtures/` так і
@@ -98,6 +106,28 @@ export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
98
106
  return { renamed, overwritten }
99
107
  }
100
108
 
109
+ /**
110
+ * Замінює невалідну директиву `error_log off;` на `error_log /dev/null crit;` у всіх
111
+ * **default.conf.template** від `root`. `error_log off;` — НЕ валідний nginx: "off" трактується
112
+ * як ім'я файлу (`/etc/nginx/off`) і падає під readOnlyRootFilesystem; `/dev/null` — writable device.
113
+ * @param {string} root корінь обходу (зазвичай cwd репозиторію)
114
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
115
+ * @returns {Promise<string[]>} відносні шляхи виправлених шаблонів (для звіту)
116
+ */
117
+ export async function migrateErrorLogOffDirective(root, ignorePaths = []) {
118
+ const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
119
+ /** @type {string[]} */
120
+ const fixed = []
121
+ for (const abs of templates) {
122
+ const body = await readFile(abs, 'utf8')
123
+ const next = body.replace(ERROR_LOG_OFF_RE, ERROR_LOG_CANONICAL)
124
+ if (next === body) continue
125
+ await writeFile(abs, next, 'utf8')
126
+ fixed.push(relative(root, abs).replaceAll('\\', '/') || abs)
127
+ }
128
+ return fixed
129
+ }
130
+
101
131
  /**
102
132
  * Імена змінних з ini (рядки KEY=value, без коментарів і порожніх).
103
133
  * @param {string} iniText вміст *.ini
@@ -131,7 +161,10 @@ export function nginxTemplateViolations(content) {
131
161
  { msg: 'відсутнє listen 8080', ok: c => c.includes('listen 8080') },
132
162
  { msg: 'відсутнє server_name _', ok: c => c.includes('server_name _') },
133
163
  { msg: 'відсутнє access_log off', ok: c => c.includes('access_log off') },
134
- { msg: 'відсутнє error_log off', ok: c => c.includes('error_log off') },
164
+ {
165
+ msg: 'відсутнє error_log /dev/null crit (error_log off — НЕ валідний nginx, падає під readOnlyRootFilesystem)',
166
+ ok: c => c.includes('error_log /dev/null crit')
167
+ },
135
168
  { msg: 'відсутнє root /usr/share/nginx/html', ok: c => c.includes('root /usr/share/nginx/html') },
136
169
  {
137
170
  msg: 'location /healthz має повертати healthy (див. nginx-default-tpl.mdc)',
@@ -416,6 +449,11 @@ export async function check(cwd = process.cwd()) {
416
449
  pass(`Перезаписано default.conf.template змістом default.tpl.conf: ${rel}`)
417
450
  }
418
451
 
452
+ const errorLogFixed = await migrateErrorLogOffDirective(root, ignorePaths)
453
+ for (const rel of errorLogFixed) {
454
+ pass(`Замінено невалідне error_log off; → error_log /dev/null crit; у ${rel}`)
455
+ }
456
+
419
457
  const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
420
458
 
421
459
  if (templates.length === 0) {
@@ -19,7 +19,9 @@ server {
19
19
 
20
20
  # disable all log
21
21
  access_log off;
22
- error_log off;
22
+ # `error_log off;` — НЕ валідний nginx: "off" трактується як ім'я файлу (/etc/nginx/off)
23
+ # і падає під readOnlyRootFilesystem. /dev/null — writable device.
24
+ error_log /dev/null crit;
23
25
 
24
26
  # This would be the directory where your Vite app's static files are stored at
25
27
  root /usr/share/nginx/html;
@@ -224,11 +224,11 @@ function checkPublishWorkflow(passFn, failFn, cwd) {
224
224
  }
225
225
 
226
226
  /**
227
- * Перетворює glob-патерн (як у npm `files`) у `RegExp` з якорями `^` / `$`.
228
- * Підтримує globstar (нуль або більше сегментів), `*` (символи без `/`) і `?`
229
- * (один символ без `/`). Не підтримує brace-expansion і class `[…]` — у
230
- * негативних патернах `files` цього достатньо для практичних випадків
231
- * (приклад: negation з префіксом `!` і двома зірочками поряд з `_test.rego`).
227
+ * Перетворює glob-патерн (як у npm `files` чи `meta.json:auto.glob`) у `RegExp`
228
+ * з якорями `^` / `$`. Підтримує globstar (нуль або більше сегментів), `*`
229
+ * (символи без `/`), `?` (один символ без `/`) і brace-альтернативи `{a,b,c}`
230
+ * (наприклад `*.{png,jpg,svg}` `(?:png|jpg|svg)`). Клас `[…]` не
231
+ * підтримується у негативних патернах `files` цього достатньо.
232
232
  * @param {string} glob posix-шлях у glob-нотації
233
233
  * @returns {RegExp} `RegExp` з якорями `^` / `$`
234
234
  */
@@ -237,11 +237,42 @@ export function globToRegex(glob) {
237
237
  const tokens = parts.map(p => {
238
238
  if (p === '**') return '__GLOBSTAR__'
239
239
  let out = ''
240
+ let braceDepth = 0
240
241
  for (const c of p) {
241
- if (c === '*') out += '[^/]*'
242
- else if (c === '?') out += '[^/]'
243
- else if (REGEX_SPECIAL_IN_GLOB.has(c)) out += `\\${c}`
244
- else out += c
242
+ switch (c) {
243
+ case '*': {
244
+ out += '[^/]*'
245
+ continue
246
+ }
247
+ case '?': {
248
+ out += '[^/]'
249
+ continue
250
+ }
251
+ case '{': {
252
+ out += '(?:'
253
+ braceDepth++
254
+ continue
255
+ }
256
+ case '}': {
257
+ if (braceDepth > 0) {
258
+ out += ')'
259
+ braceDepth--
260
+ continue
261
+ }
262
+ break
263
+ }
264
+ case ',': {
265
+ if (braceDepth > 0) {
266
+ out += '|'
267
+ continue
268
+ }
269
+ break
270
+ }
271
+ default: {
272
+ break
273
+ }
274
+ }
275
+ out += REGEX_SPECIAL_IN_GLOB.has(c) ? `\\${c}` : c
245
276
  }
246
277
  return out
247
278
  })
@@ -65,7 +65,7 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
65
65
 
66
66
  **`npm-publish.yml`:** push у **`main`**, **`on.push.paths`** з **`npm/**`**, **`JS-DevTools/npm-publish@v4.1.5`**, **`with.package: npm/package.json`**, **`permissions.id-token: write`** (OIDC на npm).
67
67
 
68
- Workflow робить **release + publish** одним job (`release-publish`): крок **`Release (bump + CHANGELOG + tag)`** (`node npm/bin/n-cursor.js release` — агрегує change-файли, bump `version`, генерує секцію `CHANGELOG.md`, ставить git-тег) виконується **перед** публікацією. Тому потрібні **`permissions.contents: write`** і **`persist-credentials: true`** з **`fetch-depth: 0`** на `checkout` (release пушить commit-back версії та тег), а також локальний composite **`./.github/actions/setup-bun-deps`** і крок `Configure git identity`. Це узгоджено з **`n-changelog`**: `version`/`CHANGELOG.md` змінює лише `n-cursor release` у CI на `main`. Програмна перевірка (`npm_module.npm_publish_yml`) лишається subset-of enforce-ить `on.push.paths`/`branches`, `id-token: write` і наявність publish-кроку; додаткові кроки (release, git identity) дозволені.
68
+ Workflow робить **release + publish** одним job (`release-publish`): крок **`Release (bump + CHANGELOG + tag)`** (`node npm/bin/n-cursor.js release` — агрегує change-файли, bump `version`, генерує секцію `CHANGELOG.md`, ставить git-тег) виконується **перед** публікацією. Тому потрібні **`permissions.contents: write`** і **`persist-credentials: true`** з **`fetch-depth: 0`** на `checkout` (release пушить commit-back версії та тег), а також локальний composite **`./.github/actions/setup-bun-deps`** і крок `Configure git identity`. Це узгоджено з **`n-changelog`**: `version`/`CHANGELOG.md` змінює лише `n-cursor release` у CI на `main`. Програмна перевірка (`npm_module.npm_publish_yml`) звіряє **весь канонічний сніпет** напряму (`target.json:"check":"template"`, generic deep-subset): усі поля й кроки сніпета (`on.push.paths`/`branches`, `concurrency`, `permissions.contents/id-token`, `checkout` з `persist-credentials/fetch-depth`, `setup-bun-deps`, `Configure git identity`, `Release`, publish-крок) **обовʼязкові**; зайві кроки/поля дозволені (subset-of), масиви матчаться за наявністю (порядок кроків не важить). Сніпет — єдине джерело істини: його редагування одразу змінює enforce, без правок rego й без міграторів.
69
69
 
70
70
  - Канон: [npm-publish.yml.snippet.yml](./policy/npm_publish_yml/template/npm-publish.yml.snippet.yml)
71
71
 
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "check": "template",
3
4
  "files": { "single": ".github/workflows/npm-publish.yml" }
4
5
  }