@nitra/cursor 1.10.0 → 1.11.1
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 +42 -1
- package/bin/n-cursor.js +31 -31
- package/package.json +2 -4
- package/rules/abie/js/applies/check.mjs +24 -0
- package/rules/abie/js/env_dns/check.mjs +53 -0
- package/rules/abie/js/firebase_hosting/check.mjs +49 -0
- package/rules/abie/js/hc_pairing/check.mjs +58 -0
- package/rules/abie/js/ua_http_route/check.mjs +86 -0
- package/rules/abie/js/ua_node_selector/check.mjs +65 -0
- package/rules/abie/policy/base_deployment_preem/target.json +10 -0
- package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
- package/rules/abie/policy/health_check_policy/target.json +4 -0
- package/rules/abie/policy/http_route_base/target.json +4 -0
- package/rules/abie/utils/enabled.mjs +35 -0
- package/rules/abie/utils/env-dns.mjs +81 -0
- package/rules/abie/utils/hc-yaml.mjs +27 -0
- package/rules/abie/utils/http-route.mjs +93 -0
- package/rules/abie/utils/k8s-tree.mjs +102 -0
- package/rules/abie/utils/kustomization-patches.mjs +224 -0
- package/rules/abie/utils/overlay-paths.mjs +97 -0
- package/rules/abie/utils/yaml.mjs +72 -0
- package/rules/adr/js/{check.mjs → hooks/check.mjs} +2 -2
- package/rules/adr/policy/settings_json/target.json +4 -0
- package/rules/adr/policy/settings_local_json/target.json +4 -0
- package/rules/bun/js/{check.mjs → layout/check.mjs} +1 -1
- package/rules/bun/policy/bunfig/target.json +4 -0
- package/rules/bun/policy/package_json/target.json +4 -0
- package/rules/capacitor/js/{check.mjs → platforms/check.mjs} +1 -1
- package/rules/capacitor/policy/package_json/target.json +4 -0
- package/rules/changelog/js/{check.mjs → consistency/check.mjs} +2 -2
- package/rules/docker/js/{check.mjs → lint/check.mjs} +5 -5
- package/rules/docker/policy/lint_docker_yml/target.json +4 -0
- package/rules/docker/policy/package_json/target.json +4 -0
- package/rules/ga/js/lint.mjs +1 -1
- package/rules/ga/js/{check.mjs → workflows/check.mjs} +4 -4
- package/rules/graphql/js/{check.mjs → tooling/check.mjs} +5 -5
- package/rules/hasura/js/{check.mjs → internal_urls/check.mjs} +4 -4
- package/rules/hasura/policy/svc_hl/target.json +4 -0
- package/rules/image-avif/js/{check.mjs → avif_generation/check.mjs} +5 -5
- package/rules/image-avif/policy/package_json/target.json +4 -0
- package/rules/image-compress/js/{check.mjs → package_setup/check.mjs} +1 -1
- package/rules/image-compress/policy/package_json/target.json +4 -0
- package/rules/js-bun-db/js/{check.mjs → safety/check.mjs} +5 -5
- package/rules/js-bun-db/policy/package_json/target.json +4 -0
- package/rules/js-bun-redis/js/{check.mjs → imports/check.mjs} +4 -4
- package/rules/js-bun-redis/policy/package_json/target.json +4 -0
- package/rules/js-lint/js/{check.mjs → tooling/check.mjs} +16 -2
- package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
- package/rules/js-lint/policy/package_json/target.json +4 -0
- package/rules/js-mssql/js/{check.mjs → deps/check.mjs} +5 -5
- package/rules/js-mssql/policy/package_json/target.json +4 -0
- package/rules/js-run/js/{check.mjs → runtime/check.mjs} +10 -10
- package/rules/js-run/policy/configmap/target.json +4 -0
- package/rules/js-run/policy/package_json/target.json +4 -0
- package/rules/k8s/js/{check.mjs → manifests/check.mjs} +4 -4
- package/rules/k8s/policy/base_kustomization/target.json +4 -0
- package/rules/k8s/policy/base_manifest/target.json +10 -0
- package/rules/k8s/policy/gateway/target.json +4 -0
- package/rules/k8s/policy/hpa_pdb/target.json +4 -0
- package/rules/k8s/policy/kustomization/target.json +4 -0
- package/rules/k8s/policy/manifest/target.json +4 -0
- package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
- package/rules/k8s/policy/svc_yaml/target.json +4 -0
- package/rules/nginx-default-tpl/js/{check.mjs → template/check.mjs} +5 -5
- package/rules/npm-module/js/{check.mjs → package_structure/check.mjs} +4 -4
- package/rules/npm-module/policy/emit_types_config/target.json +4 -0
- package/rules/npm-module/policy/npm_package_json/target.json +4 -0
- package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
- package/rules/npm-module/policy/root_package_json/target.json +4 -0
- package/rules/php/js/{check.mjs → tooling/check.mjs} +1 -1
- package/rules/php/policy/lint_php_yml/target.json +4 -0
- package/rules/php/policy/package_json/target.json +4 -0
- package/rules/rego/js/applies/check.mjs +54 -0
- package/rules/rego/policy/package_json/target.json +5 -0
- package/rules/rego/policy/vscode_extensions/target.json +5 -0
- package/rules/rego/policy/vscode_settings/target.json +5 -0
- package/rules/style-lint/js/{check.mjs → tooling/check.mjs} +1 -1
- package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
- package/rules/style-lint/policy/package_json/target.json +4 -0
- package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
- package/rules/style-lint/policy/vscode_settings/target.json +4 -0
- package/rules/tauri/js/{check.mjs → tooling/check.mjs} +2 -2
- package/rules/text/js/{check.mjs → formatting/check.mjs} +2 -2
- package/rules/text/policy/cspell/target.json +4 -0
- package/rules/text/policy/markdownlint/target.json +4 -0
- package/rules/text/policy/oxfmtrc/target.json +4 -0
- package/rules/text/policy/package_json/target.json +4 -0
- package/rules/text/policy/vscode_extensions/target.json +4 -0
- package/rules/text/policy/vscode_settings/target.json +4 -0
- package/rules/vue/js/{check.mjs → packages/check.mjs} +5 -5
- package/rules/vue/policy/package_json/target.json +4 -0
- package/schemas/target.json +58 -0
- package/scripts/lint-conftest.mjs +65 -414
- package/scripts/utils/discover-checkable-rules.mjs +106 -0
- package/scripts/utils/resolve-target-files.mjs +109 -0
- package/scripts/utils/run-rule.mjs +127 -0
- package/rules/abie/js/check.mjs +0 -1152
- package/rules/rego/js/check.mjs +0 -106
package/CHANGELOG.md
CHANGED
|
@@ -4,24 +4,65 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
-
## [1.
|
|
7
|
+
## [1.11.1] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`npm/bin/n-cursor.js`** — `runSync()` (entry для `npx @nitra/cursor` без аргументів) шукав
|
|
12
|
+
`<packageRoot>/mdc` після того, як phase 1-4 перейменував каталог у `rules/`. Виправлено: тепер
|
|
13
|
+
вказує на коректний шлях `<packageRoot>/rules` — більше не кидає «Не знайдено каталог правил пакету».
|
|
14
|
+
|
|
15
|
+
## [1.11.0] - 2026-05-15
|
|
8
16
|
|
|
9
17
|
### Added
|
|
10
18
|
|
|
19
|
+
- **Concern-based JS + per-policy `target.json`** — нова інфраструктура для CLI `check`:
|
|
20
|
+
- `npm/rules/<id>/js/<concern>/check*.mjs` — JS-концерни замість одного плаского `js/check.mjs`. Дзеркалить `policy/<name>/`: один `<name>` = одна одиниця відповідальності (rego, JS, або hybrid).
|
|
21
|
+
- `npm/rules/<id>/policy/<name>/target.json` — декларативний маніфест поруч із `<name>.rego` описує, які файли фідити в conftest (`{ "files": { "single": "..." | "walkGlob": [...] } }`). CLI читає сам і викликає `runConftestBatch` — JS не зобовʼязаний дублювати.
|
|
22
|
+
- **Pure-rego правила** працюють без жодного `.mjs`: CLI знаходить полісі за `target.json` і прогонить їх через `runConftestBatch`.
|
|
23
|
+
- **Applies-гейт**: `rules/<id>/js/applies/check.mjs` може експортувати `applies()`. Якщо повертає `false` — CLI пропускає правило цілком (включно з policy-концернами).
|
|
24
|
+
- **JSON Schema** у `npm/schemas/target.json` для IDE-валідації `target.json`.
|
|
25
|
+
- **picomatch@^4.0.4** — runtime dependency для `walkGlob`-резолверу.
|
|
26
|
+
- **Нові утиліти** в `npm/scripts/utils/`:
|
|
27
|
+
- `discover-checkable-rules.mjs` — обхід `rules/`, повертає `{ id, jsConcerns, policyConcerns }[]`. Legacy-fallback: плаский `js/check.mjs` маппиться у концерн `legacy`, щоб не ламати ще не мігровані правила під час переходу.
|
|
28
|
+
- `resolve-target-files.mjs` — резолвер `files.single` / `files.walkGlob` з спільним walk-кешем на check-прогон (повторні таргети з тим самим `ignorePaths` не роблять додаткового `walkDir`). Path-traversal у `single` блокується.
|
|
29
|
+
- `run-rule.mjs` — оркестратор одного правила: applies-гейт → JS-концерни → policy-концерни. Exit-код агрегується OR-ом.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`npm/bin/n-cursor.js`** — `discoverCheckScripts` тепер делегує у `discoverCheckableRules` і повертає `CheckableRule[]` замість `string[]`. `runChecks` запускає `runRule` для кожного id зі shared `walkCache` (один обхід дерева на унікальний `ignorePaths`-сигнатуру).
|
|
34
|
+
- **`npm/rules/rego/`** — пілот міграції на нову структуру:
|
|
35
|
+
- `js/check.mjs` → `js/applies/check.mjs` з експортами `applies()` (gate за наявністю `*.rego` у дереві) і `check()` (короткий context-pass). Виклики `runConftestBatch` прибрано — їх тепер виконує CLI через `target.json`.
|
|
36
|
+
- `policy/{package_json,vscode_extensions,vscode_settings}/target.json` додано (`single` + `required: true` + кастомні `missingMessage`).
|
|
37
|
+
- **`npm/package.json`** — додано `picomatch` у `dependencies`.
|
|
38
|
+
|
|
11
39
|
- **Правило `adr` — фаза 2 (Normalize).** Новий Stop-hook `.claude/hooks/normalize-decisions.sh` батчево нормалізує ADR-чернетки через LLM: коли кількість файлів з `session:` у frontmatter досягає `ADR_NORMALIZE_THRESHOLD` (default 30), бере до `ADR_NORMALIZE_BATCH` найстарших, отримує JSON-операції (`rewrite` / `delete` / `merge-into`) і застосовує їх до робочого дерева. **Жодних git-операцій** — розробник дивиться `git status`/`git diff` і вирішує сам. Recursion guard `ADR_NORMALIZE_RUNNING=1`, мінімальний інтервал між спробами `ADR_NORMALIZE_MIN_INTERVAL_HOURS=6`, lock-файл, skip при mid-rebase/mid-merge, `ADR_NORMALIZE_DRY=1` для dry-run. Slug-стиль — kebab-case українською; дата у фінальному ADR береться з `captured` чернетки.
|
|
12
40
|
- **Skill `adr-normalize`** (slash-команда `/n-adr-normalize`) — ручний запуск normalize поза порогом і поза Stop-hook (виставляє `ADR_NORMALIZE_THRESHOLD=0` і `ADR_NORMALIZE_MIN_INTERVAL_HOURS=0`, корисно для dry-run або разової чистки). Авто-додається при `adr` у `rules`.
|
|
13
41
|
- **`sync-claude-config`**: експортовано `ADR_NORMALIZE_HOOK_COMMAND_MARKER` і функцію `syncAdrNormalizeHookScript`; managed-група normalize додається до `hooks.Stop` поряд з capture-групою (`async: true`, `timeout: 600`) при `"adr"` у `rules`. Маркер `.claude/hooks/normalize-decisions.sh` додано в `MANAGED_HOOK_COMMAND_MARKERS`.
|
|
14
42
|
- **Rego-перевірка `adr.settings_json`** тепер вимагає Stop-hook групу і для capture, і для normalize; **`adr.settings_local_json`** забороняє дублі обох хуків.
|
|
43
|
+
- **Інкрементальна міграція правил на `target.json`** (декларативні маніфести поруч із кожним `<concern>.rego`):
|
|
44
|
+
- **Single-file правила** (11): `bun`, `text`, `style-lint`, `php`, `docker`, `npm-module`, `js-lint`, `image-compress`, `capacitor`, `hasura`, `adr` — `target.json` з `single` для кожного канонічного конфіг-файлу. Сумарно 27 нових маніфестів. `bun.bunfig`, `text.cspell`, `npm_module.npm_publish_yml` тощо тепер прогоняються через CLI `check <id>` без додаткового `bun run lint-conftest`.
|
|
45
|
+
- **Walk-glob правила** (6): `js-mssql`, `js-bun-db`, `js-bun-redis`, `js-run` (package_json + configmap), `vue`, `image-avif` — `walkGlob: "**/package.json"` або відповідний патерн.
|
|
46
|
+
- **k8s.* концерни** (8): `manifest`, `gateway`, `hpa_pdb`, `kustomization`, `svc_yaml`, `svc_hl_yaml`, `base_kustomization`, `base_manifest` — `walkGlob` по YAML під сегментом `k8s/`; `base_manifest` використовує негативний glob для виключення `kustomization.yaml`.
|
|
47
|
+
- **abie концерни** (4): `clean_merged_ignore_branches` (single), `health_check_policy` (walkGlob `**/k8s/**/hc.yaml`), `http_route_base` (walkGlob `**/k8s/**/base/**/hr.yaml`), `base_deployment_preem` (walkGlob `**/k8s/**/base/**/*.{yaml,yml}` з виключенням `kustomization.yaml`).
|
|
15
48
|
|
|
16
49
|
### Changed
|
|
17
50
|
|
|
18
51
|
- **`capture-decisions.sh` тепер пише чернетки напряму в `docs/adr/<timestamp>-<sid>.md`** (раніше — у `docs/adr/_inbox/`). Сам каталог `_inbox/` більше не створюється, але `normalize-decisions.sh` бачить його рекурсивно — старі чернетки з `_inbox/` поступово розчищаються нормалізацією. Можна також одноразово `git mv docs/adr/_inbox/*.md docs/adr/` і прибрати порожній каталог.
|
|
19
52
|
- **Правило `adr` (`npm/rules/adr/adr.mdc`)**: повне переписування під дві фази (capture + normalize). Видалено згадки `_inbox/`. Версія `version: '2.0'`.
|
|
20
53
|
- **`npm/rules/adr/js/check.mjs`**: перевірка обох hook-скриптів (canonicity), обох log-файлів у `.gitignore`.
|
|
54
|
+
- **`npm/scripts/lint-conftest.mjs`**: повне переписування. Замість hardcoded `TARGETS`-таблиці (~280 рядків з регексами та walker-предикатами) скрипт викликає `discoverCheckableRules` і читає `target.json` для кожного policy-концерну. Файл-резолвер — спільний з CLI `check` (`resolveTargetFiles` + walk-кеш). Поведінка для користувача однакова (`stdio: 'inherit'` зберігається для рідного форматування conftest), але джерело правди тепер — `target.json` поруч із `.rego`. Скрипт став `async` (для `await readFile`/`discoverCheckableRules`).
|
|
55
|
+
- **`npm/rules/abie/`** — завершено concern-split:
|
|
56
|
+
- `js/check.mjs` (1153 рядки) видалено, його логіка розпорошена по `js/{applies,firebase_hosting,hc_pairing,ua_node_selector,ua_http_route,env_dns}/check.mjs`.
|
|
57
|
+
- Спільні стан і утиліти у `utils/{enabled,k8s-tree,overlay-paths,kustomization-patches,http-route,hc-yaml,env-dns,yaml}.mjs`. `k8s-tree.mjs` тримає module-level кеш `findK8sYamlFiles` + `collectDeploymentDirs` — повторні виклики з різних концернів не роблять нового обходу дерева.
|
|
58
|
+
- Виклики `runConftestBatch` прибрано з JS — їх тепер виконує CLI через `target.json`.
|
|
21
59
|
|
|
22
60
|
### Fixed
|
|
23
61
|
|
|
24
62
|
- `npm/rules/adr/js/check.{mjs,test.mjs}`: виправлено `BUNDLED_HOOKS_DIR` (після phase 3 co-location шлях `'..'` указував у `npm/rules/adr/.claude-template/`, потрібно `'../../..'` — до `npm/.claude-template/`).
|
|
63
|
+
- **`scripts/utils/run-rule.mjs`**: kebab-id правила (`style-lint`, `image-compress`, `js-lint`, `npm-module`, `js-mssql`, `js-bun-db`, `js-bun-redis`, `js-run`, `image-avif`, `nginx-default-tpl`) тепер коректно мапиться у snake-namespace rego (`style_lint.<concern>` тощо). Раніше `namespace: <id>.<concern>` давав `style-lint.package_json`, що не збігалося з `package style_lint.package_json` у `.rego` → conftest повертав 0 violations попри реальні порушення.
|
|
64
|
+
- **`scripts/utils/resolve-target-files.mjs`**: виправлено інтерпретацію негативних glob-патернів. `picomatch(['pos', '!neg'])` за дефолтом трактує `!neg` як окремий позитивний матчер «не-neg» (OR-логіка), що матчило майже всі шляхи. Тепер позитивні/негативні розділяються вручну, негативи застосовуються через `!isExcluded`. Виправляє `k8s.base_manifest`-таргет, який мав виключати `kustomization.yaml`, але до фіксу матчив усе дерево.
|
|
65
|
+
- **`scripts/utils/discover-checkable-rules.mjs`**: коли правило має й плаский `js/check.mjs`, і concern-підкаталоги, CLI прогонить **тільки** концерни. Раніше додавалися обидва → дублюючий вивід для правил у стані часткової міграції.
|
|
25
66
|
|
|
26
67
|
## [1.9.23] - 2026-05-14
|
|
27
68
|
|
package/bin/n-cursor.js
CHANGED
|
@@ -66,8 +66,10 @@ import {
|
|
|
66
66
|
} from '../scripts/auto-rules.mjs'
|
|
67
67
|
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
68
68
|
import { runStopHookCli } from '../scripts/claude-stop-hook.mjs'
|
|
69
|
+
import { discoverCheckableRules } from '../scripts/utils/discover-checkable-rules.mjs'
|
|
69
70
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
70
71
|
import { runLintGaCli } from '../rules/ga/js/lint.mjs'
|
|
72
|
+
import { runRule } from '../scripts/utils/run-rule.mjs'
|
|
71
73
|
import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
|
|
72
74
|
import { upgradeNitraCursorToLatestAndBunInstall } from '../scripts/upgrade-nitra-cursor-and-install.mjs'
|
|
73
75
|
import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
@@ -943,7 +945,7 @@ async function runSyncStep(prefix, action) {
|
|
|
943
945
|
/**
|
|
944
946
|
* Копіює керовані `.mdc` файли з пакету до `.cursor/rules`.
|
|
945
947
|
* @param {string[]} rules список rules з конфігу
|
|
946
|
-
* @param {string} bundledRulesDir каталог `
|
|
948
|
+
* @param {string} bundledRulesDir каталог `rules` пакету-джерела
|
|
947
949
|
* @param {string} rulesDir абсолютний шлях до `.cursor/rules`
|
|
948
950
|
* @returns {Promise<{ successCount: number, failCount: number }>} статистика копіювання
|
|
949
951
|
*/
|
|
@@ -986,18 +988,14 @@ function logRemovedManagedItems(title, basePath, names) {
|
|
|
986
988
|
}
|
|
987
989
|
|
|
988
990
|
/**
|
|
989
|
-
* Знаходить
|
|
990
|
-
* `rules/<id>/js/check.mjs`
|
|
991
|
-
*
|
|
991
|
+
* Знаходить правила, для яких є хоча б щось прогонне: JS-концерн у `rules/<id>/js/<concern>/check*.mjs`
|
|
992
|
+
* (плюс legacy `rules/<id>/js/check.mjs` як концерн `legacy`) або policy-концерн у
|
|
993
|
+
* `rules/<id>/policy/<concern>/target.json`. Делегує у `discoverCheckableRules` —
|
|
994
|
+
* див. `scripts/utils/discover-checkable-rules.mjs`.
|
|
995
|
+
* @returns {Promise<import('../scripts/utils/discover-checkable-rules.mjs').CheckableRule[]>} опис правил у алфавітному порядку
|
|
992
996
|
*/
|
|
993
997
|
async function discoverCheckScripts() {
|
|
994
|
-
|
|
995
|
-
const entries = await readdir(BUNDLED_RULES_DIR, { withFileTypes: true })
|
|
996
|
-
return entries
|
|
997
|
-
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
998
|
-
.filter(e => existsSync(join(BUNDLED_RULES_DIR, e.name, 'js', 'check.mjs')))
|
|
999
|
-
.map(e => e.name)
|
|
1000
|
-
.toSorted((a, b) => a.localeCompare(b))
|
|
998
|
+
return discoverCheckableRules(BUNDLED_RULES_DIR)
|
|
1001
999
|
}
|
|
1002
1000
|
|
|
1003
1001
|
/**
|
|
@@ -1048,13 +1046,15 @@ async function discoverCheckRulesFromAgentsMd(available) {
|
|
|
1048
1046
|
}
|
|
1049
1047
|
|
|
1050
1048
|
/**
|
|
1051
|
-
* Запускає перевірки: без аргументів — за списком у AGENTS.md; з аргументами — лише вказані
|
|
1049
|
+
* Запускає перевірки: без аргументів — за списком у AGENTS.md; з аргументами — лише вказані правила.
|
|
1050
|
+
* Делегує оркестрацію concern-ів (JS-checks + policy через `runConftestBatch`) у `runRule`;
|
|
1051
|
+
* сам `runChecks` відповідає лише за фільтр id, агрегацію exit-кодів і shared walk-cache на прогон.
|
|
1052
1052
|
* @param {string[]} requestedRules імена правил; порожній масив — брати з AGENTS.md
|
|
1053
1053
|
* @returns {Promise<void>}
|
|
1054
1054
|
*/
|
|
1055
1055
|
async function runChecks(requestedRules) {
|
|
1056
|
-
const
|
|
1057
|
-
if (
|
|
1056
|
+
const allRules = await discoverCheckScripts()
|
|
1057
|
+
if (allRules.length === 0) {
|
|
1058
1058
|
console.error('❌ Не знайдено жодного check-скрипта у пакеті')
|
|
1059
1059
|
throw new Error('No check scripts found')
|
|
1060
1060
|
}
|
|
@@ -1070,38 +1070,38 @@ async function runChecks(requestedRules) {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
-
|
|
1073
|
+
const available = allRules.map(r => r.id)
|
|
1074
|
+
let idsToCheck
|
|
1074
1075
|
if (requestedRules.length > 0) {
|
|
1075
|
-
|
|
1076
|
+
idsToCheck = requestedRules
|
|
1076
1077
|
} else {
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1078
|
+
idsToCheck = await discoverCheckRulesFromAgentsMd(available)
|
|
1079
|
+
if (idsToCheck.length === 0) {
|
|
1079
1080
|
console.log(
|
|
1080
1081
|
`\n🔍 ${PACKAGE_NAME} check — у ${AGENTS_FILE} немає правил з programmatic перевіркою ` +
|
|
1081
|
-
`(відповідного check-*.mjs у пакеті). Нічого не запущено.\n`
|
|
1082
|
+
`(відповідного check-*.mjs або policy/<name>/target.json у пакеті). Нічого не запущено.\n`
|
|
1082
1083
|
)
|
|
1083
1084
|
return
|
|
1084
1085
|
}
|
|
1085
1086
|
}
|
|
1086
1087
|
|
|
1087
|
-
const unknown =
|
|
1088
|
+
const unknown = idsToCheck.filter(id => !available.includes(id))
|
|
1088
1089
|
if (unknown.length > 0) {
|
|
1089
1090
|
console.error(`❌ Невідомі правила: ${unknown.join(', ')}`)
|
|
1090
1091
|
console.log(` Доступні: ${available.join(', ')}`)
|
|
1091
1092
|
throw new Error(`Unknown rules: ${unknown.join(', ')}`)
|
|
1092
1093
|
}
|
|
1093
1094
|
|
|
1094
|
-
console.log(`\n🔍 ${PACKAGE_NAME} check — перевірка правил (${
|
|
1095
|
+
console.log(`\n🔍 ${PACKAGE_NAME} check — перевірка правил (${idsToCheck.length})\n`)
|
|
1095
1096
|
|
|
1097
|
+
const ruleMap = new Map(allRules.map(r => [r.id, r]))
|
|
1098
|
+
/** @type {Map<string, Promise<string[]>>} shared walk-cache (cross-concern, cross-rule у межах одного прогону) */
|
|
1099
|
+
const walkCache = new Map()
|
|
1096
1100
|
let totalFailed = 0
|
|
1097
1101
|
|
|
1098
|
-
for (const
|
|
1099
|
-
const scriptPath = join(BUNDLED_RULES_DIR, rule, 'js', 'check.mjs')
|
|
1100
|
-
console.log(`📋 ${rule}:`)
|
|
1102
|
+
for (const id of idsToCheck) {
|
|
1101
1103
|
try {
|
|
1102
|
-
|
|
1103
|
-
const { check } = await import(scriptPath)
|
|
1104
|
-
const code = await check()
|
|
1104
|
+
const code = await runRule(ruleMap.get(id), BUNDLED_RULES_DIR, walkCache)
|
|
1105
1105
|
if (code !== 0) totalFailed++
|
|
1106
1106
|
} catch (error) {
|
|
1107
1107
|
console.log(` ❌ Помилка виконання: ${error.message}`)
|
|
@@ -1110,11 +1110,11 @@ async function runChecks(requestedRules) {
|
|
|
1110
1110
|
console.log()
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
const passedCount =
|
|
1114
|
-
console.log(`✨ Результат: ${passedCount}/${
|
|
1113
|
+
const passedCount = idsToCheck.length - totalFailed
|
|
1114
|
+
console.log(`✨ Результат: ${passedCount}/${idsToCheck.length} правил без зауважень\n`)
|
|
1115
1115
|
|
|
1116
1116
|
if (totalFailed > 0) {
|
|
1117
|
-
throw new Error(`${totalFailed} з ${
|
|
1117
|
+
throw new Error(`${totalFailed} з ${idsToCheck.length} правил мають проблеми`)
|
|
1118
1118
|
}
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
@@ -1207,7 +1207,7 @@ async function runSync() {
|
|
|
1207
1207
|
|
|
1208
1208
|
await reexecIfPackageVersionChanged(effectivePackageRoot)
|
|
1209
1209
|
|
|
1210
|
-
const bundledRulesDir = join(effectivePackageRoot, '
|
|
1210
|
+
const bundledRulesDir = join(effectivePackageRoot, 'rules')
|
|
1211
1211
|
const bundledSkillsDir = join(effectivePackageRoot, 'skills')
|
|
1212
1212
|
const bundledAgentsTemplatePath = join(effectivePackageRoot, AGENTS_TEMPLATE_FILE)
|
|
1213
1213
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -45,13 +45,11 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"oxc-parser": "^0.128.0",
|
|
48
|
+
"picomatch": "^4.0.4",
|
|
48
49
|
"yaml": "^2.8.3"
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"bun": ">=1.3",
|
|
52
53
|
"node": ">=25"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@nitra/cursor": "^1.9.22"
|
|
56
54
|
}
|
|
57
55
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies-гейт правила abie: rule-level через `isAbieRuleEnabled` (поле `rules` у `.n-cursor.json`).
|
|
3
|
+
* Якщо повертає `false` — CLI пропускає всі концерни (JS і policy) цього правила.
|
|
4
|
+
* `check()` друкує тільки context-pass; решта концернів роблять справжню роботу.
|
|
5
|
+
*/
|
|
6
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
7
|
+
|
|
8
|
+
import { isAbieRuleEnabled } from '../../utils/enabled.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @returns {Promise<boolean>}
|
|
12
|
+
*/
|
|
13
|
+
export async function applies() {
|
|
14
|
+
return isAbieRuleEnabled(process.cwd())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {Promise<number>}
|
|
19
|
+
*/
|
|
20
|
+
export async function check() {
|
|
21
|
+
const reporter = createCheckReporter()
|
|
22
|
+
reporter.pass('Правило abie увімкнено — виконуємо перевірки')
|
|
23
|
+
return reporter.getExitCode()
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Скан env-файлів abie (`*.dev.env`, `*.ua.env`): кожен внутрішньокластерний URL
|
|
3
|
+
* `http://<svc>.<ns>.svc.<dns>` має відповідати кластеру за іменем файла:
|
|
4
|
+
* - `dev.env` → `abie-dev.internal` + `dev-*` namespace
|
|
5
|
+
* - `ua.env` → `abie-ua.internal` + `ua-*` namespace
|
|
6
|
+
*
|
|
7
|
+
* Файл `.env` без імені (локальний для розробника) — виключено.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from 'node:fs/promises'
|
|
10
|
+
import { basename, relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
13
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
14
|
+
|
|
15
|
+
import { abieEnvNameFromBasename, collectAbieEnvFiles, validateAbieEnvInternalUrls } from '../../utils/env-dns.mjs'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {Promise<number>}
|
|
19
|
+
*/
|
|
20
|
+
export async function check() {
|
|
21
|
+
const reporter = createCheckReporter()
|
|
22
|
+
const { pass, fail } = reporter
|
|
23
|
+
const root = process.cwd()
|
|
24
|
+
|
|
25
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
26
|
+
const envFiles = await collectAbieEnvFiles(root, ignorePaths)
|
|
27
|
+
if (envFiles.length === 0) {
|
|
28
|
+
pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
|
|
29
|
+
return reporter.getExitCode()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const abs of envFiles) {
|
|
33
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
34
|
+
const envName = abieEnvNameFromBasename(basename(abs))
|
|
35
|
+
if (envName === null) continue
|
|
36
|
+
let raw
|
|
37
|
+
try {
|
|
38
|
+
raw = await readFile(abs, 'utf8')
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
41
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
const errors = validateAbieEnvInternalUrls(raw, envName)
|
|
45
|
+
if (errors.length === 0) {
|
|
46
|
+
pass(`${rel}: усі внутрішні URL відповідають env "${envName}" (abie.mdc)`)
|
|
47
|
+
} else {
|
|
48
|
+
for (const err of errors) fail(`${rel}: ${err} (abie.mdc)`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return reporter.getExitCode()
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевірка abie: у **підкаталогах першого рівня** (без `.git`/`node_modules`) не має бути
|
|
3
|
+
* `.firebaserc`, `firebase.json`, `.firebase/` (abie.mdc — Firebase Hosting заборонено).
|
|
4
|
+
* У самому корені репозиторію ці імена не перевіряються (можуть бути від суміжних проєктів).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { readdir } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
11
|
+
|
|
12
|
+
const SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {Promise<number>}
|
|
16
|
+
*/
|
|
17
|
+
export async function check() {
|
|
18
|
+
const reporter = createCheckReporter()
|
|
19
|
+
const { pass, fail } = reporter
|
|
20
|
+
const root = process.cwd()
|
|
21
|
+
|
|
22
|
+
let entries
|
|
23
|
+
try {
|
|
24
|
+
entries = await readdir(root, { withFileTypes: true })
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
27
|
+
fail(`Не вдалося прочитати ${root} для перевірки Firebase Hosting: ${msg} (abie.mdc)`)
|
|
28
|
+
return reporter.getExitCode()
|
|
29
|
+
}
|
|
30
|
+
const topDirs = entries.filter(e => e.isDirectory() && !SKIP_TOP_DIR_NAMES.has(e.name))
|
|
31
|
+
let hasViolation = false
|
|
32
|
+
for (const e of topDirs) {
|
|
33
|
+
for (const name of ['.firebaserc', 'firebase.json']) {
|
|
34
|
+
const rel = join(e.name, name).replaceAll('\\', '/')
|
|
35
|
+
if (existsSync(join(root, e.name, name))) {
|
|
36
|
+
fail(`Знайдено заборонений файл Firebase Hosting: ${rel} — видали його (abie.mdc)`)
|
|
37
|
+
hasViolation = true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (existsSync(join(root, e.name, '.firebase'))) {
|
|
41
|
+
fail(`Знайдено заборонену директорію: ${e.name}/.firebase/ — видали її (abie.mdc)`)
|
|
42
|
+
hasViolation = true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!hasViolation) {
|
|
46
|
+
pass('Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)')
|
|
47
|
+
}
|
|
48
|
+
return reporter.getExitCode()
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевірка abie: для кожного каталогу з `kind: Deployment` під `k8s/` поруч має бути `hc.yaml`
|
|
3
|
+
* з коректним modeline (yaml-language-server $schema).
|
|
4
|
+
*
|
|
5
|
+
* Це JS-частина (FS-парність + modeline). Структурну валідацію `HealthCheckPolicy`
|
|
6
|
+
* (apiVersion, requestPath, port, targetRef з суфіксом `-hl`) робить CLI через
|
|
7
|
+
* `policy/health_check_policy/target.json` (walkGlob по hc.yaml у k8s-дереві).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { relative } from 'node:path'
|
|
12
|
+
|
|
13
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
14
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
15
|
+
|
|
16
|
+
import { validateAbieHcModeline } from '../../utils/hc-yaml.mjs'
|
|
17
|
+
import { collectDeploymentDirs, findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @returns {Promise<number>}
|
|
21
|
+
*/
|
|
22
|
+
export async function check() {
|
|
23
|
+
const reporter = createCheckReporter()
|
|
24
|
+
const { pass, fail } = reporter
|
|
25
|
+
const root = process.cwd()
|
|
26
|
+
|
|
27
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
28
|
+
const yamls = await findK8sYamlFiles(root, ignorePaths)
|
|
29
|
+
const deploymentDirs = await collectDeploymentDirs(root, yamls, fail)
|
|
30
|
+
|
|
31
|
+
if (deploymentDirs.size === 0) {
|
|
32
|
+
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
33
|
+
return reporter.getExitCode()
|
|
34
|
+
}
|
|
35
|
+
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml поруч`)
|
|
36
|
+
|
|
37
|
+
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
38
|
+
const hcAbs = `${dir}/hc.yaml`
|
|
39
|
+
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
40
|
+
if (!existsSync(hcAbs)) {
|
|
41
|
+
fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
let hcRaw
|
|
45
|
+
try {
|
|
46
|
+
hcRaw = await readFile(hcAbs, 'utf8')
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
49
|
+
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const modelineErr = validateAbieHcModeline(hcRaw, relHc)
|
|
53
|
+
if (modelineErr !== null) fail(modelineErr)
|
|
54
|
+
else pass(`${relHc}: modeline OK`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return reporter.getExitCode()
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Якщо в каталозі пакета (батько `k8s/`) є `vite.config.{js,mjs,ts}`, у `ua/kustomization.yaml`
|
|
3
|
+
* має бути inline-patch HTTPRoute (непорожній `target.name`): `/spec/hostnames` (домени abie),
|
|
4
|
+
* `/spec/parentRefs/0/namespace` (`ua` або `ua-*`).
|
|
5
|
+
*
|
|
6
|
+
* Для спільних сервісів (`auth-run-hl`, `file-link-hl`) у base-HTTPRoute пакета — кожен `backendRef`
|
|
7
|
+
* має `namespace: dev`; в overlay patch — JSON6902 на `/spec/rules/…/backendRefs/…/namespace` зі
|
|
8
|
+
* `value: ua`. Кількість patch-ів = кількість таких посилань у base.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { relative } from 'node:path'
|
|
12
|
+
|
|
13
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
14
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
15
|
+
|
|
16
|
+
import { analyzeAbieSharedBackendRefsInPackageK8s } from '../../utils/http-route.mjs'
|
|
17
|
+
import { findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
|
|
18
|
+
import {
|
|
19
|
+
getCombinedNginxRunPatchTextFromKustomization,
|
|
20
|
+
validateAbieNginxRunHttpRoutePatches
|
|
21
|
+
} from '../../utils/kustomization-patches.mjs'
|
|
22
|
+
import {
|
|
23
|
+
abiePackageDirFromK8sOverlay,
|
|
24
|
+
abieOverlayRequiresHttpRouteByVite,
|
|
25
|
+
isUaKustomizationPath
|
|
26
|
+
} from '../../utils/overlay-paths.mjs'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {Promise<number>}
|
|
30
|
+
*/
|
|
31
|
+
export async function check() {
|
|
32
|
+
const reporter = createCheckReporter()
|
|
33
|
+
const { pass, fail } = reporter
|
|
34
|
+
const root = process.cwd()
|
|
35
|
+
|
|
36
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
37
|
+
const yamls = await findK8sYamlFiles(root, ignorePaths)
|
|
38
|
+
|
|
39
|
+
const uaAbsList = yamls.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
40
|
+
if (uaAbsList.length === 0) {
|
|
41
|
+
pass('Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)')
|
|
42
|
+
return reporter.getExitCode()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
46
|
+
const cache = new Map()
|
|
47
|
+
|
|
48
|
+
for (const abs of uaAbsList) {
|
|
49
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
50
|
+
if (!abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
51
|
+
pass(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
|
|
55
|
+
if (!pkgAbs) {
|
|
56
|
+
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
let p = cache.get(pkgAbs)
|
|
60
|
+
if (!p) {
|
|
61
|
+
p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamls)
|
|
62
|
+
cache.set(pkgAbs, p)
|
|
63
|
+
}
|
|
64
|
+
const sharedAnalysis = await p
|
|
65
|
+
let hasBaseError = false
|
|
66
|
+
for (const err of sharedAnalysis.baseErrors) {
|
|
67
|
+
fail(err)
|
|
68
|
+
hasBaseError = true
|
|
69
|
+
}
|
|
70
|
+
if (hasBaseError) continue
|
|
71
|
+
let raw
|
|
72
|
+
try {
|
|
73
|
+
raw = await readFile(abs, 'utf8')
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
76
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
80
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
|
|
81
|
+
if (v !== null) fail(`${rel}: ${v}`)
|
|
82
|
+
else pass(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return reporter.getExitCode()
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Якщо в дереві `k8s/` пакета є `Deployment`, у `ua/kustomization.yaml` має бути inline-patch
|
|
3
|
+
* на `Deployment` з `path /spec/template/spec/nodeSelector` і `preem: false` (abie.mdc).
|
|
4
|
+
*
|
|
5
|
+
* Структурні обмеження JSON6902 (заборона `remove + add` на той самий path) перевіряє k8s.mdc /
|
|
6
|
+
* `k8s.kustomization` rego — тут лише abie-специфічне.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { relative } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
12
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
13
|
+
|
|
14
|
+
import { collectDeploymentDirs, findK8sYamlFiles } from '../../utils/k8s-tree.mjs'
|
|
15
|
+
import { kustomizationHasAbieDeploymentNodeSelectorPatch } from '../../utils/kustomization-patches.mjs'
|
|
16
|
+
import { abieOverlayK8sTreeHasDeployment, isUaKustomizationPath } from '../../utils/overlay-paths.mjs'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns {Promise<number>}
|
|
20
|
+
*/
|
|
21
|
+
export async function check() {
|
|
22
|
+
const reporter = createCheckReporter()
|
|
23
|
+
const { pass, fail } = reporter
|
|
24
|
+
const root = process.cwd()
|
|
25
|
+
|
|
26
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
27
|
+
const yamls = await findK8sYamlFiles(root, ignorePaths)
|
|
28
|
+
const deploymentDirs = await collectDeploymentDirs(root, yamls, fail)
|
|
29
|
+
|
|
30
|
+
if (deploymentDirs.size === 0) {
|
|
31
|
+
pass('Немає Deployment у дереві k8s — patch nodeSelector (ua) не вимагається')
|
|
32
|
+
return reporter.getExitCode()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const uaAbsList = yamls.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
36
|
+
if (uaAbsList.length === 0) {
|
|
37
|
+
fail(
|
|
38
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
39
|
+
)
|
|
40
|
+
return reporter.getExitCode()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const abs of uaAbsList) {
|
|
44
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
45
|
+
if (!abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
46
|
+
pass(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
let raw
|
|
50
|
+
try {
|
|
51
|
+
raw = await readFile(abs, 'utf8')
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
54
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
58
|
+
fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return reporter.getExitCode()
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-level applies-гейт abie: чи `.n-cursor.json:rules` містить `abie`.
|
|
3
|
+
* Використовується `js/applies/check.mjs` як `applies()`-експорт — якщо false,
|
|
4
|
+
* CLI пропускає всі концерни правила (включно з policy).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const CONFIG_FILE = '.n-cursor.json'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Чи увімкнено правило **abie** у `.n-cursor.json:rules`.
|
|
14
|
+
* @param {string} root корінь репозиторію (cwd)
|
|
15
|
+
* @returns {Promise<boolean>}
|
|
16
|
+
*/
|
|
17
|
+
export async function isAbieRuleEnabled(root) {
|
|
18
|
+
const p = join(root, CONFIG_FILE)
|
|
19
|
+
if (!existsSync(p)) return false
|
|
20
|
+
let raw
|
|
21
|
+
try {
|
|
22
|
+
raw = await readFile(p, 'utf8')
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
let cfg
|
|
27
|
+
try {
|
|
28
|
+
cfg = JSON.parse(raw)
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
const rules = cfg?.rules
|
|
33
|
+
if (!Array.isArray(rules)) return false
|
|
34
|
+
return rules.some(r => String(r).trim().toLowerCase() === 'abie')
|
|
35
|
+
}
|