@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.
- package/CHANGELOG.md +24 -0
- package/bin/n-cursor.js +12 -0
- package/package.json +1 -1
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/js/lint.mjs +1 -1
- package/rules/docker/lib/docker-hadolint.mjs +27 -55
- package/rules/ga/lint/lint.mjs +18 -54
- package/rules/image-compress/meta.json +1 -1
- package/rules/k8s/lint/lint.mjs +3 -10
- package/rules/nginx-default-tpl/js/template.mjs +39 -1
- package/rules/nginx-default-tpl/nginx-default-tpl.mdc +3 -1
- package/rules/npm-module/js/package_structure.mjs +40 -9
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/target.json +1 -0
- package/rules/rego/lint/lint.mjs +10 -55
- package/rules/text/lint/lint.mjs +11 -40
- package/rules/worktree/policy/vscode_settings/target.json +5 -0
- package/rules/worktree/policy/vscode_settings/template/settings.json.snippet.json +8 -0
- package/rules/worktree/policy/zed_settings/target.json +5 -0
- package/rules/worktree/policy/zed_settings/template/settings.json.snippet.json +12 -0
- package/rules/worktree/worktree.mdc +52 -0
- package/schemas/target.json +5 -0
- package/scripts/lib/assert-project-root.mjs +74 -0
- package/scripts/lib/ensure-tool.mjs +352 -0
- package/scripts/lib/run-conftest-batch.mjs +6 -28
- package/scripts/lib/run-rule.mjs +61 -5
- package/scripts/lib/template.mjs +29 -3
- package/scripts/lib/worktree-notice.mjs +52 -1
- package/skills/fix/SKILL.md +4 -4
- package/types/bin/n-cursor.d.ts +1 -1
- 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
package/rules/docker/docker.mdc
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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`** (**`
|
|
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"
|
package/rules/docker/js/lint.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `USER` у Dockerfile — перевірка non-root для нього пропускається.
|
|
30
30
|
*
|
|
31
31
|
* Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
|
|
32
|
-
* тощо.
|
|
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
|
-
* Відносні шляхи з прямими
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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 з прямими слешами (
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
85
|
-
stderr:
|
|
86
|
-
via: '
|
|
55
|
+
ok: local.status === 0,
|
|
56
|
+
stdout: local.stdout ?? '',
|
|
57
|
+
stderr: local.stderr ?? '',
|
|
58
|
+
via: 'hadolint'
|
|
87
59
|
}
|
|
88
60
|
}
|
package/rules/ga/lint/lint.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-обгортка над канонічним `lint-ga` (ga.mdc):
|
|
3
|
-
*
|
|
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 передвстановлений) падає.
|
|
16
|
+
* на ubuntu-latest (де shellcheck передвстановлений) падає. ensureTool('shellcheck') усуває цю різницю.
|
|
17
17
|
*
|
|
18
|
-
* `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає
|
|
19
|
-
*
|
|
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`
|
|
105
|
+
* Виконує канонічний `lint-ga` — авто-встановлює shellcheck/conftest, перевіряє uv, запускає actionlint/zizmor/check-ga.
|
|
138
106
|
*
|
|
139
107
|
* Послідовність:
|
|
140
|
-
* 1)
|
|
141
|
-
* 2) `
|
|
142
|
-
* 3) `
|
|
143
|
-
* 4) `
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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":
|
|
1
|
+
{ "auto": { "glob": "**/*.{png,jpg,jpeg,gif,svg}" } }
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
{
|
|
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
|
-
* (один символ без `/`)
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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`)
|
|
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
|
|