@7n/rules 1.29.3 → 1.31.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 +17 -0
- package/package.json +1 -1
- package/rules/k8s/dremio_logging/dremio_logging.mdc +1 -0
- package/scripts/docs/index.md +15 -14
- package/scripts/docs/tool-pins-refresh.md +28 -0
- package/scripts/docs/upgrade-n-rules-and-install.md +2 -1
- package/scripts/lib/docs/ensure-tool.md +69 -25
- package/scripts/lib/ensure-tool.mjs +150 -12
- package/scripts/lib/lint-surface/detect.mjs +22 -0
- package/scripts/lib/lint-surface/docs/detect.md +4 -4
- package/scripts/lib/tool-pins.json +15 -0
- package/scripts/tool-pins-refresh.mjs +54 -0
- package/scripts/upgrade-n-rules-and-install.mjs +86 -19
- package/skills/taze/js/docs/orchestrate.md +1 -1
- package/rules/style/admin_table/admin_table.mdc +0 -88
- package/rules/style/admin_table/concern.json +0 -7
- package/rules/style/admin_table/docs/index.md +0 -9
- package/rules/style/admin_table/docs/main.md +0 -14
- package/rules/style/admin_table/main.mjs +0 -46
- package/rules/style/colors/colors.mdc +0 -21
- package/rules/style/colors/concern.json +0 -3
- package/rules/style/docs/index.md +0 -11
- package/rules/style/gap/concern.json +0 -7
- package/rules/style/gap/docs/index.md +0 -9
- package/rules/style/gap/docs/main.md +0 -15
- package/rules/style/gap/gap.mdc +0 -22
- package/rules/style/gap/main.mjs +0 -51
- package/rules/style/lint/concern.json +0 -7
- package/rules/style/lint/docs/fix-lint.md +0 -29
- package/rules/style/lint/docs/index.md +0 -11
- package/rules/style/lint/docs/main.md +0 -29
- package/rules/style/lint/fix-lint.mjs +0 -66
- package/rules/style/lint/main.mjs +0 -68
- package/rules/style/main.json +0 -1
- package/rules/style/main.mdc +0 -12
- package/rules/style/package_json/concern.json +0 -9
- package/rules/style/package_json/docs/fix-package_json.md +0 -27
- package/rules/style/package_json/docs/index.md +0 -9
- package/rules/style/package_json/fix-package_json.mjs +0 -3
- package/rules/style/package_json/package_json.mdc +0 -18
- package/rules/style/package_json/package_json.rego +0 -31
- package/rules/style/package_json/template/package.json.snippet.json +0 -5
- package/rules/style/quasar/concern.json +0 -3
- package/rules/style/quasar/quasar.mdc +0 -7
- package/rules/style/quasar_fixes/concern.json +0 -7
- package/rules/style/quasar_fixes/docs/index.md +0 -9
- package/rules/style/quasar_fixes/docs/main.md +0 -16
- package/rules/style/quasar_fixes/main.mjs +0 -57
- package/rules/style/quasar_fixes/quasar_fixes.mdc +0 -32
- package/rules/style/tooling/concern.json +0 -14
- package/rules/style/tooling/docs/fix-tooling.md +0 -29
- package/rules/style/tooling/docs/index.md +0 -12
- package/rules/style/tooling/docs/main.md +0 -34
- package/rules/style/tooling/fix-tooling.mjs +0 -58
- package/rules/style/tooling/main.mjs +0 -73
- package/rules/style/tooling/tooling.mdc +0 -85
- package/rules/style/vscode_extensions/concern.json +0 -9
- package/rules/style/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
- package/rules/style/vscode_extensions/docs/index.md +0 -11
- package/rules/style/vscode_extensions/fix-vscode_extensions.mjs +0 -1
- package/rules/style/vscode_extensions/template/extensions.json.snippet.json +0 -1
- package/rules/style/vscode_extensions/vscode_extensions.mdc +0 -13
- package/rules/style/vscode_extensions/vscode_extensions.rego +0 -13
- package/rules/style/vscode_settings/concern.json +0 -9
- package/rules/style/vscode_settings/docs/fix-vscode_settings.md +0 -27
- package/rules/style/vscode_settings/docs/index.md +0 -9
- package/rules/style/vscode_settings/fix-vscode_settings.mjs +0 -5
- package/rules/style/vscode_settings/template/settings.json.snippet.json +0 -5
- package/rules/style/vscode_settings/vscode_settings.mdc +0 -19
- package/rules/style/vscode_settings/vscode_settings.rego +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.31.0] - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- sync: діапазони вже оголошених плагінів @7n/rules-* у package.json підіймаються до ^latest тим самим кроком, що й ядро — фікси плагінів доїжджають без ручного bun update (bun i сам по собі поважає lockfile); недоступний registry для окремого плагіна — warning і пропуск
|
|
8
|
+
- lint: правило style (stylelint/css/vue-стилі) переїхало з ядра у @7n/rules-lang-js — фінальний крок фази 5 spec lang-plugins-extraction по фронтенд-сімʼї
|
|
9
|
+
|
|
10
|
+
## [1.30.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- ensure-tool: закріплені версії зовнішніх CLI-тулів у tool-pins.json (без latest-lookup на звичайному install-шляху) + tool-pins-refresh.mjs для ручного рефрешу + tool-pins-freshness.test.mjs, що нагадує оновити піни після 30 днів
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- ensure-tool: GitHub-токен у lookup релізів, redirect-fallback без API і fail-open детекторів на транзієнтні збої авто-встановлення тулів (rate-limit без tag_name більше не валить lint exit 2)
|
|
19
|
+
|
|
3
20
|
## [1.29.3] - 2026-07-19
|
|
4
21
|
|
|
5
22
|
### Changed
|
package/package.json
CHANGED
|
@@ -39,6 +39,7 @@ JS-детектор (не rego): `main.mjs`, `lint(ctx)`, per-file, glob `**/dre
|
|
|
39
39
|
**Що перевіряється:** якщо файл визначає `ConfigMap` з `data["logback.xml"]` (bundled-конфіг образу `zookeeper:3.8.4-jre-17`, монтується поверх `/conf/logback.xml`), його `<root level="...">` має бути `warn`/`error`/`off` (case-insensitive) — не `info`/`debug`/`trace` (дефолт бандлованого файлу) і не відсутній.
|
|
40
40
|
|
|
41
41
|
**Чому:** readiness/liveness `ruok`-проби виконуються кожні ~10s на кожен zk-под; `NIOServerCnxn` логує це на INFO незалежно від навантаження. Керувати рівнем ззовні НЕ вдається для цього образу:
|
|
42
|
+
|
|
42
43
|
- `ZOO_LOG4J_PROP` — змінна Bitnami-образів, тут взагалі не читається жодним скриптом.
|
|
43
44
|
- `-Dzookeeper.console.threshold=WARN` через `JVMFLAGS` — перебивається `<property name="zookeeper.console.threshold" value="INFO"/>` у бандлованому файлі.
|
|
44
45
|
|
package/scripts/docs/index.md
CHANGED
|
@@ -4,19 +4,20 @@ title: npm/scripts
|
|
|
4
4
|
resource: npm/scripts/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
| Файл
|
|
8
|
-
|
|
|
9
|
-
| [auto-rules.mjs](auto-rules.md)
|
|
10
|
-
| [auto-skills.mjs](auto-skills.md)
|
|
11
|
-
| [build-agents-commands.mjs](build-agents-commands.md)
|
|
12
|
-
| [cli-entry.mjs](cli-entry.md)
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------------------------------------------------------- | --------- |
|
|
9
|
+
| [auto-rules.mjs](auto-rules.md) | JS Module |
|
|
10
|
+
| [auto-skills.mjs](auto-skills.md) | JS Module |
|
|
11
|
+
| [build-agents-commands.mjs](build-agents-commands.md) | JS Module |
|
|
12
|
+
| [cli-entry.mjs](cli-entry.md) | JS Module |
|
|
13
13
|
| [ensure-n-rules-dev-dependencies.mjs](ensure-n-rules-dev-dependencies.md) | JS Module |
|
|
14
|
-
| [hook.mjs](hook.md)
|
|
15
|
-
| [post-tool-use-check.mjs](post-tool-use-check.md)
|
|
16
|
-
| [rename-yaml-extensions.mjs](rename-yaml-extensions.md)
|
|
17
|
-
| [skills-cli.mjs](skills-cli.md)
|
|
18
|
-
| [smoke-check-imports.mjs](smoke-check-imports.md)
|
|
19
|
-
| [sync-claude-config.mjs](sync-claude-config.md)
|
|
20
|
-
| [sync-setup-bun-deps-action.mjs](sync-setup-bun-deps-action.md)
|
|
21
|
-
| [
|
|
14
|
+
| [hook.mjs](hook.md) | JS Module |
|
|
15
|
+
| [post-tool-use-check.mjs](post-tool-use-check.md) | JS Module |
|
|
16
|
+
| [rename-yaml-extensions.mjs](rename-yaml-extensions.md) | JS Module |
|
|
17
|
+
| [skills-cli.mjs](skills-cli.md) | JS Module |
|
|
18
|
+
| [smoke-check-imports.mjs](smoke-check-imports.md) | JS Module |
|
|
19
|
+
| [sync-claude-config.mjs](sync-claude-config.md) | JS Module |
|
|
20
|
+
| [sync-setup-bun-deps-action.mjs](sync-setup-bun-deps-action.md) | JS Module |
|
|
21
|
+
| [tool-pins-refresh.mjs](tool-pins-refresh.md) | JS Module |
|
|
22
|
+
| [update-blue-oak.mjs](update-blue-oak.md) | JS Module |
|
|
22
23
|
| [upgrade-n-rules-and-install.mjs](upgrade-n-rules-and-install.md) | JS Module |
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: tool-pins-refresh.mjs
|
|
4
|
+
resource: npm/scripts/tool-pins-refresh.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: f2d4811a
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
score: 80
|
|
9
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Огляд
|
|
13
|
+
|
|
14
|
+
Оновлює `scripts/lib/tool-pins.json` свіжими релізами закріплених CLI-тулів для `ensure-tool.mjs` і `Linux/Windows-fallback install-шляху`, а також ставить нову дату піна. Використовується як ручний refresh, коли `tool-pins-freshness.test.mjs` червоніє через піни старші за 30 днів, коли у закріпленому тулі відома вразливість або баг, або коли потрібно оновити закріплені версії до актуальних. Для кожного тула з реєстру `TOOLS` бере останній реліз через `fetchLatestVersion` і переписує `tool-pins.json` із новими версіями та сьогоднішнім `pinnedAt`; друкує diff лише для змінених тулів.
|
|
15
|
+
|
|
16
|
+
## Поведінка
|
|
17
|
+
|
|
18
|
+
1. Перевіряє, чи доступний `curl`, бо без нього оновлення піна не виконується.
|
|
19
|
+
2. Зчитує поточний стан `tool-pins.json` як джерело правди для закріплених версій і дати піна.
|
|
20
|
+
3. Для кожного закріпленого CLI-тула отримує актуальну версію з upstream-релізів через мережу.
|
|
21
|
+
4. Порівнює нову версію з уже закріпленою і фіксує лише ті позиції, де є зміна.
|
|
22
|
+
5. Перезаписує `tool-pins.json` новим набором версій і сьогоднішньою датою піна.
|
|
23
|
+
6. Показує короткий звіт про зміни: якщо версії не змінилися, окремо повідомляє, що оновилася лише дата піна; якщо зміни є, перелічує, які тули оновилися.
|
|
24
|
+
7. Не торкається звичайного install-потоку: мережеве оновлення версій виконується тільки цим скриптом, а штатне встановлення працює лише з уже закріпленими значеннями.
|
|
25
|
+
|
|
26
|
+
## Гарантії поведінки
|
|
27
|
+
|
|
28
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: upgrade-n-rules-and-install.mjs
|
|
4
4
|
resource: npm/scripts/upgrade-n-rules-and-install.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: ab3a4cc2
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -28,6 +28,7 @@ shouldSkipNpmVersionUpgrade — Визначає, чи можна замінит
|
|
|
28
28
|
fetchLatestNRulesVersionFromNpm — Отримує актуальну версію пакета `@7n/rules` з реєстру npm (через поле `version` у JSON dist-tag `latest`), що доступно на https://registry.npmjs.org/@7n/rules/latest.
|
|
29
29
|
resolveInstalledPackageRoot — Знаходить абсолютний шлях до встановленого пакета у директорії `node_modules`, або надає резервний шлях.
|
|
30
30
|
upgradeNRulesToLatestAndBunInstall — Оновлює `@7n/rules` до останньої версії з npm (якщо це дозволено конфігурацією), виконує встановлення залежностей за допомогою `bun i`, і повертає корінь пакета для подальшої синхронізації конфігураційних файлів, таких як `mdc/`.
|
|
31
|
+
upgradePluginRanges — Підіймає діапазони вже оголошених плагінів `@7n/rules-*` у dependencies/devDependencies до `^latest` з npm (мутує pkg; той самий крок, що й для ядра — бо `bun i` поважає lockfile і фікси плагінів інакше не доїжджають). Специфікатори поза npm-semver (workspace:, file:, git…) не чіпає; недоступний registry для окремого плагіна — warning і пропуск.
|
|
31
32
|
|
|
32
33
|
## Гарантії поведінки
|
|
33
34
|
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: ensure-tool.mjs
|
|
4
4
|
resource: npm/scripts/lib/ensure-tool.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: c77b4b65
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Модуль `ensure-tool.mjs` — єдина точка резолву зовнішніх CLI-залежностей пакета `@7n/rules`. Він гарантує, що потрібний бінарник (`hk`, `conftest`, `shellcheck`, `actionlint`, `dotenv-linter`, `opa`, `regal`, `hadolint`, `kubeconform`, `kubescape`) доступний у системі, виконуючи послідовний пошук:
|
|
@@ -15,6 +15,8 @@ docgen:
|
|
|
15
15
|
|
|
16
16
|
Така архітектура усуває дублювання install-логіки в кожному `lint.mjs` / `fix.mjs`: щоб додати нову зовнішню утиліту, достатньо одного запису в реєстрі `TOOLS`. Додатково модуль експортує `ensureHkInstall`, який реєструє git pre-commit hook через `hk install` (пропускається в CI).
|
|
17
17
|
|
|
18
|
+
Версії для GitHub Release install-шляху (Linux, і Windows-fallback коли `scoop` недоступний) — **закріплені** у `scripts/lib/tool-pins.json`, а не резолвляться як `latest` на кожен install. Причина: CI-runner-и ефемерні, локальний кеш бінарників порожній щоразу, і `latest`-lookup через GitHub API на кожен job означає постійний трафік зі спільних CI-IP — і впирається у rate-limit (60 запитів/год без токена). `fetchLatestVersion` (двоступеневий lookup: GitHub API з `GITHUB_TOKEN`/`GH_TOKEN` за наявності → redirect-fallback повз API) лишається в модулі, але викликається лише вручну — окремим скриптом `scripts/tool-pins-refresh.mjs`, не на звичайному install-шляху. `checkToolPinsFreshness()` рахує вік поточного піна (`tool-pins.json.pinnedAt`) у днях і сигналить `stale: true` після `TOOL_PINS_MAX_AGE_DAYS` (30) — на цьому побудований тест `tool-pins-freshness.test.mjs`, що навмисно червоніє, коли піни застаріли, і нагадує запустити рефреш.
|
|
19
|
+
|
|
18
20
|
Поруч із синхронною `ensureTool` (публічний API пакета, сигнатура не змінюється) модуль експортує async-варіант `ensureToolAsync(toolId)` для parallel lane `detectAll()` (ADR 260716-1354-внутрішній-паралелізм-lint-оркестратора): конкурентні виклики того самого `toolId` в одному Node-процесі колапсують в один install (in-process single-flight), а auto-install крок додатково серіалізується між процесами через `withLock` (ключ `ensure-tool/<toolId>`) — паралельні Node-процеси (різні CI-shard-и, кілька агентів) не тягнуть той самий бінарник конкурентно. Завантажений архів завжди пишеться в унікальний per-call temp-каталог і публікується атомарним `renameSync` під фіксованим flat-іменем `<toolId>` — цей hardened install-крок спільний для sync і async шляхів.
|
|
19
21
|
|
|
20
22
|
Файл написаний для Node.js (ESM), використовує лише стандартну бібліотеку, локальний хелпер `resolveCmd` і `withLock` (`../utils/with-lock.mjs`) для міжпроцесної серіалізації async-install-кроку.
|
|
@@ -26,12 +28,16 @@ docgen:
|
|
|
26
28
|
| `ensureTool(toolId)` | `function` | Резолвить і за потреби встановлює зовнішній CLI (sync). Повертає абсолютний шлях до бінарника або кидає `Error`. |
|
|
27
29
|
| `ensureToolAsync(toolId)` | `function` | Async-варіант для parallel lane `detectAll()`: single-flight (in-process) + `withLock` (cross-process) навколо auto-install кроку. Повертає `Promise<string>`. |
|
|
28
30
|
| `ensureHkInstall(hkBin)` | `function` | Виконує `hk install` для реєстрації git pre-commit hook. Жодного return value; на помилку лише `console.warn`. |
|
|
31
|
+
| `ToolProvisionError` | `class` | Транзієнтний збій авто-встановлення (мережа, обірваний download). Споживачі розпізнають за `name` і можуть спрацювати fail-open (див. `lint-surface/detect.mjs`). |
|
|
32
|
+
| `fetchLatestVersion(repo, curlBin)` | `function` | Резолвить останній тег релізу: GitHub API (з токеном за наявності) → redirect-fallback повз API. На звичайному install-шляху не викликається — лише зі `scripts/tool-pins-refresh.mjs` і юніт-тестів. |
|
|
33
|
+
| `TOOLS` | `object` | Реєстр `Record<string, ToolEntry>` із описом install-стратегії для кожного тула. Read-only; читається `tool-pins-refresh.mjs`. |
|
|
34
|
+
| `TOOL_PINS_MAX_AGE_DAYS` | `number` | Поріг «застарілості» піна версій у днях (30). |
|
|
35
|
+
| `checkToolPinsFreshness(now?)` | `function` | Вік поточного `tool-pins.json.pinnedAt` у днях відносно `now` (за замовчуванням `Date.now()`). Повертає `{ pinnedAt, ageDays, stale }`. |
|
|
29
36
|
|
|
30
37
|
Внутрішні (не експортуються, але формують контракт модуля):
|
|
31
38
|
|
|
32
|
-
- `TOOLS` — реєстр `Record<string, ToolEntry>` із описом install-стратегії для кожного тула.
|
|
33
39
|
- `ToolEntry` — JSDoc-тип, що описує поля одного запису реєстру.
|
|
34
|
-
- Допоміжні функції: `getCacheDir`, `mapArch`, `
|
|
40
|
+
- Допоміжні функції: `getCacheDir`, `mapArch`, `githubAuthArgs`, `fetchLatestVersionViaApi`, `fetchLatestVersionViaRedirect`, `readToolPins`, `resolvePinnedVersion`, `installFromGithub`, `installViaBrew`, `installViaScoop`, `autoInstall`, `buildHint`.
|
|
35
41
|
|
|
36
42
|
## Функції
|
|
37
43
|
|
|
@@ -64,13 +70,24 @@ docgen:
|
|
|
64
70
|
- `repo` — репозиторій у форматі `owner/repo`.
|
|
65
71
|
- `curlBin` — абсолютний шлях до бінарника `curl`.
|
|
66
72
|
- **Повертає:** версію останнього релізу без префікса `v` (наприклад `0.4.1`).
|
|
67
|
-
- **Поведінка:**
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
- **Поведінка:** двоступеневий lookup.
|
|
74
|
+
1. GitHub API: `curl -sSL https://api.github.com/repos/<repo>/releases/latest` з `Accept: application/vnd.github+json`; якщо в env є `GITHUB_TOKEN` (або `GH_TOKEN`) — додається `Authorization: Bearer <token>`, що піднімає rate-limit із 60/год (per-IP, вичерпується на shared CI-runner-ах) до 5000/год. Парсить JSON, бере `tag_name`, прибирає префікс `v`.
|
|
75
|
+
2. Redirect-fallback (при будь-якому збої API): `https://github.com/<repo>/releases/latest` переадресовує на `…/releases/tag/<tag>`; тег читається з фінального URL (`curl -sIL -w '%{url_effective}'`). Веб-endpoint не підпадає під API rate-limit.
|
|
76
|
+
- **Помилки:** кидає `ToolProvisionError` лише коли не вдались обидва шляхи; повідомлення містить обидві причини (для API-відповіді без `tag_name` — і її `message`, напр. «API rate limit exceeded»).
|
|
77
|
+
- **Side effects:** мережеві HTTP-запити до GitHub.
|
|
78
|
+
|
|
79
|
+
### `readToolPins()`
|
|
80
|
+
|
|
81
|
+
- **Сигнатура:** `readToolPins(): { pinnedAt: string, versions: Record<string, string> }`
|
|
82
|
+
- **Повертає:** розпарсений `tool-pins.json`. Читається синхронно на кожен виклик (без module-level кешу) — довгоживучий процес одразу бачить щойно застосований рефреш.
|
|
83
|
+
- **Side effects:** синхронне читання файлу `scripts/lib/tool-pins.json`.
|
|
84
|
+
|
|
85
|
+
### `resolvePinnedVersion(toolId)`
|
|
86
|
+
|
|
87
|
+
- **Сигнатура:** `resolvePinnedVersion(toolId: string): string`
|
|
88
|
+
- **Повертає:** закріплену версію тула з `tool-pins.json.versions[toolId]` (без префікса `v`).
|
|
89
|
+
- **Помилки:** якщо для `toolId` немає запису у `versions` — кидає звичайний (не `ToolProvisionError`) `Error`: це конфігураційна помилка (тул зареєстрували в `TOOLS`, але забули додати пін), а не транзієнтний збій.
|
|
90
|
+
- **Side effects:** делегує в `readToolPins()`.
|
|
74
91
|
|
|
75
92
|
### `installFromGithub(toolId, entry, cacheDir)`
|
|
76
93
|
|
|
@@ -82,7 +99,7 @@ docgen:
|
|
|
82
99
|
- **Повертає:** абсолютний шлях до встановленого бінарника.
|
|
83
100
|
- **Послідовність дій:**
|
|
84
101
|
1. Резолвить `curl` та `tar` у PATH; за відсутності — кидає `Error`.
|
|
85
|
-
2. Через `
|
|
102
|
+
2. Через `resolvePinnedVersion(toolId)` бере закріплену версію з `tool-pins.json` — жодного мережевого `latest`-lookup на цьому кроці.
|
|
86
103
|
3. Формує назву asset через `entry.asset(ver)` і URL `https://github.com/<github>/releases/download/v<ver>/<asset>`.
|
|
87
104
|
4. Створює `cacheDir` (`mkdirSync` з `recursive: true`) і унікальний per-call temp-каталог усередині нього (`mkdtempSync(join(cacheDir, '.tmp-<toolId>-'))`) — той самий filesystem гарантує, що фінальний `renameSync` не впаде з `EXDEV`.
|
|
88
105
|
5. Завантажує asset у temp-каталог через `curl -sSL -o <tmpDir>/<asset> <downloadUrl>`.
|
|
@@ -90,12 +107,20 @@ docgen:
|
|
|
90
107
|
7. Інакше викликає `tar` із прапорцем `-xJf` (для `.tar.xz`) або `-xzf` (для `.tar.gz`) для розпакування в temp-каталог, знаходить реальний шлях бінарника через `entry.binFinder(ver)` або просто `toolId`, перевіряє його існування — і так само атомарним `renameSync` публікує його у `<cacheDir>/<toolId>` (flat-ім'я, незалежно від вкладеної структури архіву).
|
|
91
108
|
8. У `finally` прибирає весь temp-каталог (`rmSync(tmpDir, { recursive: true, force: true })`) — і архів, і проміжні файли розпакування зникають одним викликом.
|
|
92
109
|
- **Помилки:**
|
|
93
|
-
- `curl не знайдено в PATH — потрібен для завантаження <toolId
|
|
94
|
-
- `tar не знайдено в PATH — потрібен для встановлення <toolId
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
110
|
+
- `curl не знайдено в PATH — потрібен для завантаження <toolId>` (`Error` — конфігурація середовища).
|
|
111
|
+
- `tar не знайдено в PATH — потрібен для встановлення <toolId>` (`Error`).
|
|
112
|
+
- `ensureTool: немає закріпленої версії для '<toolId>' у tool-pins.json — ...` (`Error` — конфігурація, з `resolvePinnedVersion`).
|
|
113
|
+
- `Завантаження <toolId> не вдалось: ...` / `curl exit <status> при завантаженні <toolId>: ...` (`ToolProvisionError` — транзієнтний мережевий збій).
|
|
114
|
+
- `tar failed for <toolId>: ...` / `tar exit <status> для <toolId>: ...` (`Error`).
|
|
115
|
+
- `Бінарник <toolId> не знайдено після розпакування: <extractedBin>` (`Error`).
|
|
116
|
+
- **Side effects:** мережа (лише download, без `latest`-lookup), файлова система (унікальний temp-каталог, атомарна публікація, chmod, очищення temp).
|
|
117
|
+
|
|
118
|
+
### `checkToolPinsFreshness(now?)` _(export)_
|
|
119
|
+
|
|
120
|
+
- **Сигнатура:** `checkToolPinsFreshness(now?: number): { pinnedAt: string, ageDays: number, stale: boolean }`
|
|
121
|
+
- **Параметри:** `now` — час порівняння в ms epoch; за замовчуванням `Date.now()`.
|
|
122
|
+
- **Повертає:** `pinnedAt` з `tool-pins.json`, обчислений `ageDays` (`Math.floor`) і `stale` — `true`, якщо `ageDays > TOOL_PINS_MAX_AGE_DAYS` (30).
|
|
123
|
+
- **Side effects:** делегує в `readToolPins()`.
|
|
99
124
|
|
|
100
125
|
### `installViaBrew(toolId, entry)`
|
|
101
126
|
|
|
@@ -174,10 +199,11 @@ docgen:
|
|
|
174
199
|
### Стандартна бібліотека Node.js
|
|
175
200
|
|
|
176
201
|
- `node:child_process` — `spawnSync` для синхронного запуску `curl`, `tar`, `brew`, `scoop`, `rm`, `hk`.
|
|
177
|
-
- `node:fs` — `chmodSync`, `existsSync`, `mkdirSync`, `renameSync`.
|
|
202
|
+
- `node:fs` — `chmodSync`, `existsSync`, `mkdirSync`, `mkdtempSync`, `readFileSync` (читання `tool-pins.json`), `renameSync`, `rmSync`.
|
|
178
203
|
- `node:os` — `homedir` для побудови шляху кешу.
|
|
179
|
-
- `node:path` — `join` для конструювання шляхів.
|
|
204
|
+
- `node:path` — `dirname`, `join` для конструювання шляхів.
|
|
180
205
|
- `node:process` — `arch`, `env`, `platform`.
|
|
206
|
+
- `node:url` — `fileURLToPath` для резолву абсолютного шляху `tool-pins.json` поряд із модулем.
|
|
181
207
|
|
|
182
208
|
### Внутрішні
|
|
183
209
|
|
|
@@ -208,14 +234,14 @@ docgen:
|
|
|
208
234
|
4. Перевірка `/home/<user>/.cache/@7n/rules/bin/shellcheck` — не існує.
|
|
209
235
|
5. `N_CURSOR_NO_AUTO_INSTALL` не виставлено → `autoInstall(...)`.
|
|
210
236
|
6. На Linux диспетчер викликає `installFromGithub('shellcheck', entry, cacheDir)`:
|
|
211
|
-
- `
|
|
212
|
-
- asset name = `shellcheck-v0.
|
|
213
|
-
- URL = `https://github.com/koalaman/shellcheck/releases/download/v0.
|
|
237
|
+
- `resolvePinnedVersion('shellcheck')` читає `tool-pins.json.versions.shellcheck` → наприклад `0.11.0` (мережевого запиту немає).
|
|
238
|
+
- asset name = `shellcheck-v0.11.0.linux.x86_64.tar.xz`.
|
|
239
|
+
- URL = `https://github.com/koalaman/shellcheck/releases/download/v0.11.0/<asset>`.
|
|
214
240
|
- `mkdirSync(cacheDir, { recursive: true })` і унікальний `tmpDir = mkdtempSync(join(cacheDir, '.tmp-shellcheck-'))`.
|
|
215
241
|
- `curl -sSL -o <tmpDir>/<asset> <url>`.
|
|
216
242
|
- `tar -xJf <asset> -C <tmpDir>` (бо `.tar.xz`).
|
|
217
|
-
- `binFinder('0.
|
|
218
|
-
- Атомарний `renameSync(<tmpDir>/shellcheck-v0.
|
|
243
|
+
- `binFinder('0.11.0')` → `<tmpDir>/shellcheck-v0.11.0/shellcheck`; перевірка `existsSync`.
|
|
244
|
+
- Атомарний `renameSync(<tmpDir>/shellcheck-v0.11.0/shellcheck, <cacheDir>/shellcheck)` — публікація під flat-іменем.
|
|
219
245
|
- `rmSync(tmpDir, { recursive: true, force: true })` у `finally`.
|
|
220
246
|
- Повертає `<cacheDir>/shellcheck`.
|
|
221
247
|
7. Викликач отримує абсолютний шлях і запускає `spawnSync(bin, [...args])`.
|
|
@@ -250,7 +276,24 @@ ensureHkInstall(hkBin) // git hook у .git/hooks/pre-commit
|
|
|
250
276
|
2. Знайти GitHub-репо релізів і визначити `archStyle` (`hk` / `conftest` / `actionlint`).
|
|
251
277
|
3. Описати `asset(ver)` та, якщо потрібно, `binFinder(ver)` (коли бінарник лежить не в корені архіву).
|
|
252
278
|
4. Виставити `archive: false` для прямого бінарника без архіву.
|
|
253
|
-
5. Додати запис у `TOOLS`.
|
|
279
|
+
5. Додати запис у `TOOLS`.
|
|
280
|
+
6. Додати закріплену версію в `tool-pins.json.versions.foo` (напр. запустивши `bun npm/scripts/tool-pins-refresh.mjs` — підхопить новий тул автоматично) — без цього кроку `installFromGithub('foo', …)` на Linux/Windows-fallback впаде з конфігураційною помилкою `немає закріпленої версії для 'foo'`.
|
|
281
|
+
|
|
282
|
+
Жодних змін у викликачах не потрібно — `ensureTool('foo')` запрацює одразу.
|
|
283
|
+
|
|
284
|
+
### Рефреш закріплених версій (`tool-pins-refresh.mjs`)
|
|
285
|
+
|
|
286
|
+
`scripts/lib/tool-pins.json` не оновлюється сам собою — це навмисно (інакше повернулася б проблема з постійними `latest`-lookup-ами). Рефреш — окрема ручна дія:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
bun npm/scripts/tool-pins-refresh.mjs
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Скрипт іде по `TOOLS`, для кожного тула резолвить `fetchLatestVersion(entry.github, curl)` (GitHub API з токеном за наявності → redirect-fallback), переписує `tool-pins.json` свіжими версіями і сьогоднішньою `pinnedAt`, друкує diff версій. Коли запускати:
|
|
293
|
+
|
|
294
|
+
- тест `tool-pins-freshness.test.mjs` червоніє (`checkToolPinsFreshness().stale === true`, пінам > `TOOL_PINS_MAX_AGE_DAYS` днів);
|
|
295
|
+
- відома вразливість/баг у закріпленому тулі — рефреш раніше терміну;
|
|
296
|
+
- просто хочеться підняти версії тулів на актуальні.
|
|
254
297
|
|
|
255
298
|
### Гарантії та інваріанти
|
|
256
299
|
|
|
@@ -258,3 +301,4 @@ ensureHkInstall(hkBin) // git hook у .git/hooks/pre-commit
|
|
|
258
301
|
- **Hard-fail:** на будь-яку нерозв’язну помилку install кидається `Error` із описовим повідомленням; немає silent fallback на «обірваний» бінарник.
|
|
259
302
|
- **Кросплатформність:** єдиний публічний API для трьох ОС; OS-specific деталі інкапсульовані всередині модуля.
|
|
260
303
|
- **Безпека для CI:** `ensureHkInstall` ніколи не змінює git-репозиторій під CI, навіть якщо `hk` доступний.
|
|
304
|
+
- **Без `latest`-lookup на звичайному install-шляху:** `installFromGithub` бере версію з закріпленого `tool-pins.json`; `fetchLatestVersion` викликається лише з `tool-pins-refresh.mjs` (ручний рефреш) і тестів — CI-job на порожньому кеші не робить запиту до GitHub API для резолву версії.
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* `ensureTool(toolId)` — єдиний seam резолву зовнішніх бінарників: PATH → кеш → авто-install → hard-fail.
|
|
5
5
|
* Новий тул = один запис у реєстрі `TOOLS`, без дублювання install-логіки в кожному `lint.mjs`/`fix.mjs`.
|
|
6
6
|
*
|
|
7
|
+
* Версії GitHub Release-тулів (Linux/Windows-fallback install-шлях) — **закріплені** у
|
|
8
|
+
* `tool-pins.json`, а не резолвляться як `latest` на кожен install: CI-runner-и ефемерні,
|
|
9
|
+
* кеш бінарників порожній щоразу, і `latest`-lookup на кожен job = постійний трафік у
|
|
10
|
+
* GitHub API з shared-IP (rate-limit). `fetchLatestVersion` (GitHub API з `GITHUB_TOKEN`/
|
|
11
|
+
* `GH_TOKEN` за наявності, з fallback-ом на redirect `releases/latest` повз API) лишається
|
|
12
|
+
* — це «мотор» для ручного рефрешу пінів (`tool-pins-refresh.mjs`), у звичайному install
|
|
13
|
+
* не викликається. `checkToolPinsFreshness()` — гейт «пінам більше 30 днів → час
|
|
14
|
+
* рефрешнути» (тест `tool-pins-freshness.test.mjs`). Транзієнтні збої download-у
|
|
15
|
+
* кидаються як `ToolProvisionError` (fail-open seam для lint-детекторів — див.
|
|
16
|
+
* `lint-surface/detect.mjs`).
|
|
17
|
+
*
|
|
7
18
|
* Per-platform matrix: macOS → brew, Windows → scoop (fallback: GitHub Release), Linux → GitHub Release binary.
|
|
8
19
|
* Бінарники кешуються у `~/.cache/@7n/rules/bin/` (Linux/Mac), `%LOCALAPPDATA%\@7n\cursor\bin\` (Win).
|
|
9
20
|
* Download завжди пишеться в унікальний per-call temp-каталог і публікується атомарним `renameSync` —
|
|
@@ -17,10 +28,11 @@
|
|
|
17
28
|
* `ensureHkInstall(hkBin)` — реєструє git pre-commit hook через `hk install`; пропускається в CI.
|
|
18
29
|
*/
|
|
19
30
|
import { spawnSync } from 'node:child_process'
|
|
20
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from 'node:fs'
|
|
31
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs'
|
|
21
32
|
import { homedir } from 'node:os'
|
|
22
|
-
import { join } from 'node:path'
|
|
33
|
+
import { dirname, join } from 'node:path'
|
|
23
34
|
import { arch, env, platform } from 'node:process'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
24
36
|
|
|
25
37
|
import { resolveCmd } from '../utils/resolve-cmd.mjs'
|
|
26
38
|
import { withLock } from '../utils/with-lock.mjs'
|
|
@@ -28,6 +40,68 @@ import { withLock } from '../utils/with-lock.mjs'
|
|
|
28
40
|
/** Префікс `v` у git-тегу релізу (`v1.2.3` → `1.2.3`). */
|
|
29
41
|
const TAG_V_PREFIX_RE = /^v/
|
|
30
42
|
|
|
43
|
+
/** Тег релізу з фінального URL redirect-у `releases/latest` (`…/releases/tag/v1.2.3`). */
|
|
44
|
+
const RELEASE_TAG_URL_RE = /\/tag\/([^/\s]+)\s*$/
|
|
45
|
+
|
|
46
|
+
/** Кількість мілісекунд у добі — для перетворення віку піна в дні. */
|
|
47
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
48
|
+
|
|
49
|
+
/** Поріг «застарілості» пінів версій (`tool-pins.json.pinnedAt`) у днях. */
|
|
50
|
+
export const TOOL_PINS_MAX_AGE_DAYS = 30
|
|
51
|
+
|
|
52
|
+
/** Абсолютний шлях до `tool-pins.json`, поряд із цим модулем. */
|
|
53
|
+
const TOOL_PINS_PATH = join(dirname(fileURLToPath(import.meta.url)), 'tool-pins.json')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Читає `tool-pins.json` — синхронно, свіжим на кожен виклик (без module-level кешу),
|
|
57
|
+
* щоб `ensureTool*`-виклики в довгоживучому процесі бачили щойно застосований рефреш.
|
|
58
|
+
* @returns {{ pinnedAt: string, versions: Record<string, string> }} закріплені версії й дата піна
|
|
59
|
+
*/
|
|
60
|
+
function readToolPins() {
|
|
61
|
+
return JSON.parse(readFileSync(TOOL_PINS_PATH, 'utf8'))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Вік поточних пінів версій у днях від `pinnedAt` до `now`.
|
|
66
|
+
* @param {number} [now] час порівняння (ms epoch); за замовчуванням `Date.now()`
|
|
67
|
+
* @returns {{ pinnedAt: string, ageDays: number, stale: boolean }} вік і чи перевищено `TOOL_PINS_MAX_AGE_DAYS`
|
|
68
|
+
*/
|
|
69
|
+
export function checkToolPinsFreshness(now = Date.now()) {
|
|
70
|
+
const { pinnedAt } = readToolPins()
|
|
71
|
+
const ageDays = Math.floor((now - Date.parse(pinnedAt)) / DAY_MS)
|
|
72
|
+
return { pinnedAt, ageDays, stale: ageDays > TOOL_PINS_MAX_AGE_DAYS }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Резолвить закріплену версію тула з `tool-pins.json`. Конфігураційна помилка (не
|
|
77
|
+
* `ToolProvisionError`) — відсутність піна для наявного в `TOOLS` тула означає, що
|
|
78
|
+
* його забули додати в `tool-pins.json` при реєстрації, а не транзієнтний збій.
|
|
79
|
+
* @param {string} toolId ключ у `TOOLS`
|
|
80
|
+
* @returns {string} закріплена версія без префікса `v`
|
|
81
|
+
*/
|
|
82
|
+
function resolvePinnedVersion(toolId) {
|
|
83
|
+
const { versions } = readToolPins()
|
|
84
|
+
const version = versions[toolId]
|
|
85
|
+
if (!version) {
|
|
86
|
+
throw new Error(`ensureTool: немає закріпленої версії для '${toolId}' у tool-pins.json — додай перед install-ом`)
|
|
87
|
+
}
|
|
88
|
+
return version
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Транзієнтний збій авто-встановлення зовнішнього тула (GitHub API rate-limit, мережа,
|
|
93
|
+
* обірваний download). Відрізняється від конфігураційних помилок (невідомий тул,
|
|
94
|
+
* `N_CURSOR_NO_AUTO_INSTALL`, відсутній curl) — споживачі розпізнають за `name`
|
|
95
|
+
* і можуть спрацювати fail-open замість валити весь прогін.
|
|
96
|
+
*/
|
|
97
|
+
export class ToolProvisionError extends Error {
|
|
98
|
+
/** @param {string} message причина збою */
|
|
99
|
+
constructor(message) {
|
|
100
|
+
super(message)
|
|
101
|
+
this.name = 'ToolProvisionError'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
31
105
|
/**
|
|
32
106
|
* Повертає каталог керованого кешу бінарників для поточного OS.
|
|
33
107
|
* @returns {string} абсолютний шлях
|
|
@@ -68,8 +142,12 @@ function mapArch(nodeArch, style) {
|
|
|
68
142
|
* @property {((ver: string) => string)|null} [binFinder] для архівів де бінарник не у корені; повертає відносний шлях
|
|
69
143
|
*/
|
|
70
144
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Реєстр install-стратегій. Експортовано read-only для `tool-pins-refresh.mjs`
|
|
147
|
+
* (ітерує `entry.github`, щоб рефрешнути `tool-pins.json`) — не мутуй.
|
|
148
|
+
* @type {Record<string, ToolEntry>}
|
|
149
|
+
*/
|
|
150
|
+
export const TOOLS = {
|
|
73
151
|
hk: {
|
|
74
152
|
brew: 'hk',
|
|
75
153
|
scoop: 'hk',
|
|
@@ -156,15 +234,27 @@ const TOOLS = {
|
|
|
156
234
|
}
|
|
157
235
|
}
|
|
158
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Заголовки авторизації GitHub для curl: `GITHUB_TOKEN`/`GH_TOKEN` з env, якщо є.
|
|
239
|
+
* Токен піднімає rate-limit API з 60/год (per-IP, вичерпується на shared CI-runner-ах)
|
|
240
|
+
* до 5000/год (per-token).
|
|
241
|
+
* @returns {string[]} додаткові аргументи curl (порожньо без токена)
|
|
242
|
+
*/
|
|
243
|
+
function githubAuthArgs() {
|
|
244
|
+
const token = env['GITHUB_TOKEN'] ?? env['GH_TOKEN']
|
|
245
|
+
return token ? ['-H', `Authorization: Bearer ${token}`] : []
|
|
246
|
+
}
|
|
247
|
+
|
|
159
248
|
/**
|
|
160
249
|
* Отримує останній тег з GitHub Releases API через curl (sync).
|
|
161
250
|
* @param {string} repo репо у форматі `owner/repo`
|
|
162
251
|
* @param {string} curlBin абсолютний шлях до curl
|
|
163
252
|
* @returns {string} рядок версії без префікса `v`, наприклад `0.4.1`
|
|
164
253
|
*/
|
|
165
|
-
function
|
|
254
|
+
function fetchLatestVersionViaApi(repo, curlBin) {
|
|
166
255
|
const url = `https://api.github.com/repos/${repo}/releases/latest`
|
|
167
|
-
const
|
|
256
|
+
const args = ['-sSL', '-H', 'Accept: application/vnd.github+json', ...githubAuthArgs(), url]
|
|
257
|
+
const r = spawnSync(curlBin, args, { encoding: 'utf8' })
|
|
168
258
|
if (r.error) throw new Error(`curl failed: ${r.error.message}`)
|
|
169
259
|
if (r.status !== 0) throw new Error(`curl exit ${r.status}: ${(r.stderr ?? '').slice(0, 300)}`)
|
|
170
260
|
let parsed
|
|
@@ -174,13 +264,60 @@ function fetchLatestVersion(repo, curlBin) {
|
|
|
174
264
|
throw new Error(`GitHub API response is not JSON: ${r.stdout.slice(0, 200)}`)
|
|
175
265
|
}
|
|
176
266
|
const tag = parsed['tag_name']
|
|
177
|
-
if (!tag)
|
|
267
|
+
if (!tag) {
|
|
268
|
+
// Без tag_name API типово повертає message («API rate limit exceeded …») — показуємо його
|
|
269
|
+
const apiMessage = typeof parsed['message'] === 'string' ? ` (${parsed['message'].slice(0, 200)})` : ''
|
|
270
|
+
throw new Error(`GitHub API: tag_name missing for ${repo}${apiMessage}`)
|
|
271
|
+
}
|
|
178
272
|
return tag.replace(TAG_V_PREFIX_RE, '')
|
|
179
273
|
}
|
|
180
274
|
|
|
181
275
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
276
|
+
* Fallback-резолюція останнього тега без API: `https://github.com/<repo>/releases/latest`
|
|
277
|
+
* переадресовує на `…/releases/tag/<tag>` — читаємо тег з фінального URL (`%{url_effective}`).
|
|
278
|
+
* Веб-endpoint не підпадає під API rate-limit, тож працює і на shared-runner-ах без токена.
|
|
279
|
+
* @param {string} repo репо у форматі `owner/repo`
|
|
280
|
+
* @param {string} curlBin абсолютний шлях до curl
|
|
281
|
+
* @returns {string} рядок версії без префікса `v`
|
|
282
|
+
*/
|
|
283
|
+
function fetchLatestVersionViaRedirect(repo, curlBin) {
|
|
284
|
+
const url = `https://github.com/${repo}/releases/latest`
|
|
285
|
+
const r = spawnSync(curlBin, ['-sIL', '-w', '%{url_effective}', url], { encoding: 'utf8' })
|
|
286
|
+
if (r.error) throw new Error(`curl failed: ${r.error.message}`)
|
|
287
|
+
if (r.status !== 0) throw new Error(`curl exit ${r.status}: ${(r.stderr ?? '').slice(0, 300)}`)
|
|
288
|
+
const m = RELEASE_TAG_URL_RE.exec(r.stdout)
|
|
289
|
+
if (!m) throw new Error(`releases/latest redirect без /tag/ у фінальному URL для ${repo}`)
|
|
290
|
+
return m[1].replace(TAG_V_PREFIX_RE, '')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Отримує останній тег релізу: спершу GitHub API (з токеном за наявності), при збої —
|
|
295
|
+
* redirect-fallback повз API. Кидає `ToolProvisionError`, лише якщо не вдались обидва шляхи.
|
|
296
|
+
* Експортовано для юніт-тестів; основний споживач — `installFromGithub`.
|
|
297
|
+
* @param {string} repo репо у форматі `owner/repo`
|
|
298
|
+
* @param {string} curlBin абсолютний шлях до curl
|
|
299
|
+
* @returns {string} рядок версії без префікса `v`, наприклад `0.4.1`
|
|
300
|
+
*/
|
|
301
|
+
export function fetchLatestVersion(repo, curlBin) {
|
|
302
|
+
let apiError
|
|
303
|
+
try {
|
|
304
|
+
return fetchLatestVersionViaApi(repo, curlBin)
|
|
305
|
+
} catch (error) {
|
|
306
|
+
apiError = error
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
return fetchLatestVersionViaRedirect(repo, curlBin)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw new ToolProvisionError(
|
|
312
|
+
`latest-release lookup не вдався для ${repo} — API: ${apiError.message}; redirect: ${error.message}`
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Завантажує та розпаковує GitHub Release бінарник у кеш-директорію. Версія береться
|
|
319
|
+
* закріпленою з `tool-pins.json` — жодного `latest`-lookup у GitHub API на звичайному
|
|
320
|
+
* install-шляху. Повертає абсолютний шлях до бінарника.
|
|
184
321
|
* @param {string} toolId ключ у TOOLS
|
|
185
322
|
* @param {ToolEntry} entry опис тула
|
|
186
323
|
* @param {string} cacheDir абсолютний шлях до кешу
|
|
@@ -192,7 +329,7 @@ function installFromGithub(toolId, entry, cacheDir) {
|
|
|
192
329
|
const tarBin = resolveCmd('tar')
|
|
193
330
|
if (!tarBin) throw new Error(`tar не знайдено в PATH — потрібен для встановлення ${toolId}`)
|
|
194
331
|
|
|
195
|
-
const ver =
|
|
332
|
+
const ver = resolvePinnedVersion(toolId)
|
|
196
333
|
const assetName = entry.asset(ver)
|
|
197
334
|
const downloadUrl = `https://github.com/${entry.github}/releases/download/v${ver}/${assetName}`
|
|
198
335
|
|
|
@@ -204,10 +341,11 @@ function installFromGithub(toolId, entry, cacheDir) {
|
|
|
204
341
|
try {
|
|
205
342
|
const archivePath = join(tmpDir, assetName)
|
|
206
343
|
|
|
344
|
+
// Збої download-у — транзієнтні (мережа/GitHub), тому ToolProvisionError, як і lookup вище.
|
|
207
345
|
const dlResult = spawnSync(curlBin, ['-sSL', '-o', archivePath, downloadUrl], { encoding: 'utf8' })
|
|
208
|
-
if (dlResult.error) throw new
|
|
346
|
+
if (dlResult.error) throw new ToolProvisionError(`Завантаження ${toolId} не вдалось: ${dlResult.error.message}`)
|
|
209
347
|
if (dlResult.status !== 0) {
|
|
210
|
-
throw new
|
|
348
|
+
throw new ToolProvisionError(
|
|
211
349
|
`curl exit ${dlResult.status} при завантаженні ${toolId}: ${(dlResult.stderr ?? '').slice(0, 300)}`
|
|
212
350
|
)
|
|
213
351
|
}
|
|
@@ -105,6 +105,26 @@ function normalizeResult(raw, ctx) {
|
|
|
105
105
|
return diagnostics.length > 0 ? { violations, diagnostics } : { violations }
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Fail-open результат для транзієнтного збою авто-встановлення зовнішнього тула
|
|
110
|
+
* (`ToolProvisionError` з `ensure-tool.mjs`, розпізнається за `name` — errors можуть
|
|
111
|
+
* приходити з іншого інстансу модуля). GitHub API rate-limit чи мережевий збій на
|
|
112
|
+
* CI-runner-і не має валити весь lint-прогін exit 2: concern пропускається з
|
|
113
|
+
* видимою warn-діагностикою, а не тихо.
|
|
114
|
+
* @param {LintContext} ctx контекст лінту (джерело ruleId/concernId)
|
|
115
|
+
* @param {Error} error транзієнтна помилка встановлення тула
|
|
116
|
+
* @returns {LintResult} порожні violations + warn-діагностика про пропуск
|
|
117
|
+
*/
|
|
118
|
+
function toolProvisionSkipResult(ctx, error) {
|
|
119
|
+
const message = `⚠️ ${ctx.ruleId}/${ctx.concernId} пропущено (транзієнтний збій встановлення тула): ${error.message}`
|
|
120
|
+
// Diagnostics рендеряться лише у verbose — дублюємо у stderr, щоб пропуск було видно в CI-логах завжди.
|
|
121
|
+
console.warn(message)
|
|
122
|
+
return {
|
|
123
|
+
violations: [],
|
|
124
|
+
diagnostics: [{ level: 'warn', message }]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
/**
|
|
109
129
|
* Чи має concern ручний (не-`@generated`) `main.mjs`, що перекриває policy-adapter.
|
|
110
130
|
* @param {string} mainPath абсолютний шлях до `main.mjs` concern-а.
|
|
@@ -140,6 +160,7 @@ export async function runConcernDetector(concern, ctx) {
|
|
|
140
160
|
missingMessage: concern.policy.missingMessage
|
|
141
161
|
})
|
|
142
162
|
} catch (error) {
|
|
163
|
+
if (error?.name === 'ToolProvisionError') return toolProvisionSkipResult(ctx, error)
|
|
143
164
|
throw new DetectorError(ctx.ruleId, ctx.concernId, `policy-adapter кинув: ${error.message}`)
|
|
144
165
|
}
|
|
145
166
|
return normalizeResult(raw, ctx)
|
|
@@ -163,6 +184,7 @@ export async function runConcernDetector(concern, ctx) {
|
|
|
163
184
|
try {
|
|
164
185
|
raw = await mod.lint(ctx)
|
|
165
186
|
} catch (error) {
|
|
187
|
+
if (error?.name === 'ToolProvisionError') return toolProvisionSkipResult(ctx, error)
|
|
166
188
|
throw new DetectorError(ctx.ruleId, ctx.concernId, `lint() кинув: ${error.message}`)
|
|
167
189
|
}
|
|
168
190
|
return normalizeResult(raw, ctx)
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: detect.mjs
|
|
4
4
|
resource: npm/scripts/lib/lint-surface/detect.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: cf3febf6
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 90
|
|
10
9
|
issues: internal-name:evaluatePolicyConcern,judge:inaccurate:0.92
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -19,11 +18,12 @@ docgen:
|
|
|
19
18
|
|
|
20
19
|
- `DetectorError` позначає збій detector-а або невалідний результат як помилку інфраструктури лінту, щоб unified lint surface міг завершитись із технічним failure.
|
|
21
20
|
- `runConcernDetector` запускає один concern-detector у read-only режимі, обирає policy-оцінювання за `concern.json` або ручний detector, нормалізує результат до єдиного формату й перетворює аномалії на `DetectorError`.
|
|
21
|
+
- Виняток із fail-fast: помилка з `name === 'ToolProvisionError'` (транзієнтний збій авто-встановлення зовнішнього тула з `ensure-tool.mjs` — GitHub API rate-limit, мережа, обірваний download) не кидається як `DetectorError`. Concern пропускається fail-open: повертаються порожні `violations` + warn-діагностика, а попередження дублюється у stderr (`console.warn`), щоб пропуск було видно в CI-логах і без verbose. Розпізнавання за `name` — помилка може приходити з іншого інстансу модуля `ensure-tool.mjs`.
|
|
22
22
|
|
|
23
23
|
## Публічний API
|
|
24
24
|
|
|
25
25
|
- DetectorError — позначає збій detector-а або непридатний результат; такий стан завершує виконання з exit 2.
|
|
26
|
-
- runConcernDetector — запускає detector для одного concern-а, приводить відповідь до очікуваного формату й перетворює будь-яку аномалію на `DetectorError
|
|
26
|
+
- runConcernDetector — запускає detector для одного concern-а, приводить відповідь до очікуваного формату й перетворює будь-яку аномалію на `DetectorError`; транзієнтні `ToolProvisionError` натомість дають fail-open пропуск concern-а з warn-діагностикою.
|
|
27
27
|
|
|
28
28
|
Код спирається на конфіг `concern.json`.
|
|
29
29
|
|
|
@@ -35,4 +35,4 @@ Concern-и без policy і без `main.mjs` вважаються помилк
|
|
|
35
35
|
|
|
36
36
|
## Гарантії поведінки
|
|
37
37
|
|
|
38
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
38
|
+
- Read-only: не виконує операцій запису (ФС/БД); єдиний side effect — `console.warn` при fail-open пропуску concern-а через `ToolProvisionError`.
|