@nitra/cursor 3.22.0 → 3.23.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/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
- package/CHANGELOG.md +31 -3
- package/bin/docs/n-cursor.md +636 -0
- package/bin/docs/rename-yaml-extensions.md +207 -0
- package/bin/n-cursor.js +30 -3
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +18 -0
- package/rules/abie/js/docs/applies.md +26 -0
- package/rules/abie/js/docs/env_dns.md +32 -0
- package/rules/abie/js/docs/firebase_hosting.md +23 -0
- package/rules/abie/js/docs/hc_pairing.md +35 -0
- package/rules/abie/js/docs/ua_http_route.md +28 -0
- package/rules/abie/js/docs/ua_node_selector.md +28 -0
- package/rules/abie/lib/docs/enabled.md +29 -0
- package/rules/abie/lib/docs/env-dns.md +35 -0
- package/rules/abie/lib/docs/hc-yaml.md +33 -0
- package/rules/abie/lib/docs/http-route.md +44 -0
- package/rules/abie/lib/docs/k8s-tree.md +40 -0
- package/rules/abie/lib/docs/kustomization-patches.md +47 -0
- package/rules/abie/lib/docs/overlay-paths.md +38 -0
- package/rules/abie/lib/docs/yaml.md +29 -0
- package/rules/adr/docs/fix.md +148 -0
- package/rules/adr/js/docs/hooks.md +259 -0
- package/rules/bun/docs/fix.md +156 -0
- package/rules/bun/js/docs/layout.md +393 -0
- package/rules/capacitor/docs/fix.md +121 -0
- package/rules/capacitor/js/docs/platforms.md +295 -0
- package/rules/changelog/changelog.mdc +2 -2
- package/rules/changelog/docs/fix.md +174 -0
- package/rules/changelog/js/consistency.mjs +114 -13
- package/rules/changelog/js/docs/consistency.md +387 -0
- package/rules/changelog/lib/docs/package-manifest.md +210 -0
- package/rules/ci4/docs/fix.md +179 -0
- package/rules/ci4/js/docs/marksman_config.md +128 -0
- package/rules/docker/docker.mdc +8 -3
- package/rules/docker/docs/fix.md +171 -0
- package/rules/docker/js/docs/lint.md +258 -0
- package/rules/docker/lib/docs/docker-hadolint.md +184 -0
- package/rules/docker/lib/docs/docker-mirror.md +247 -0
- package/rules/docker/lib/docs/docker-native-addon.md +170 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
- package/rules/docker/lint/docs/lint.md +193 -0
- package/rules/efes/docs/fix.md +203 -0
- package/rules/feedback/docs/fix.md +140 -0
- package/rules/flow/docs/fix.md +152 -0
- package/rules/ga/docs/fix.md +158 -0
- package/rules/ga/js/docs/lint.md +100 -0
- package/rules/ga/js/docs/workflows.md +217 -0
- package/rules/ga/lint/docs/lint.md +209 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
- package/rules/graphql/docs/fix.md +126 -0
- package/rules/graphql/js/docs/tooling.md +264 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
- package/rules/hasura/docs/fix.md +120 -0
- package/rules/hasura/hasura.mdc +14 -0
- package/rules/hasura/js/docs/internal_urls.md +326 -0
- package/rules/image-avif/docs/fix.md +132 -0
- package/rules/image-avif/js/docs/avif_generation.md +241 -0
- package/rules/image-compress/docs/fix.md +150 -0
- package/rules/image-compress/js/docs/package_setup.md +191 -0
- package/rules/js-bun-db/docs/fix.md +148 -0
- package/rules/js-bun-db/js/docs/safety.md +231 -0
- package/rules/js-bun-db/js-bun-db.mdc +42 -13
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
- package/rules/js-bun-redis/docs/fix.md +123 -0
- package/rules/js-bun-redis/js/docs/imports.md +176 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
- package/rules/js-lint/docs/fix.md +117 -0
- package/rules/js-lint/js/docs/lint.md +250 -0
- package/rules/js-lint/js/docs/tooling.md +348 -0
- package/rules/js-lint/js/docs/utils_imports.md +207 -0
- package/rules/js-lint-ci/docs/fix.md +154 -0
- package/rules/js-lint-ci/js/docs/lint.md +144 -0
- package/rules/js-mssql/docs/fix.md +128 -0
- package/rules/js-mssql/js/docs/deps.md +263 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
- package/rules/js-run/docs/fix.md +144 -0
- package/rules/js-run/js/docs/runtime.md +388 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
- package/rules/js-run/lib/docs/check-env-scan.md +433 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
- package/rules/k8s/docs/fix.md +129 -0
- package/rules/k8s/js/docs/manifests.md +344 -0
- package/rules/k8s/js/manifests.mjs +6 -2
- package/rules/k8s/k8s.mdc +4 -2
- package/rules/k8s/lint/docs/lint.md +411 -0
- package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
- package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
- package/rules/nginx-default-tpl/docs/fix.md +124 -0
- package/rules/nginx-default-tpl/js/docs/template.md +378 -0
- package/rules/npm-module/docs/fix.md +98 -0
- package/rules/npm-module/js/docs/package_structure.md +274 -0
- package/rules/npm-module/js/docs/rule_meta.md +137 -0
- package/rules/npm-module/js/docs/skill_meta.md +190 -0
- package/rules/php/docs/fix.md +107 -0
- package/rules/php/js/docs/tooling.md +152 -0
- package/rules/php/lint/docs/lint.md +215 -0
- package/rules/python/docs/fix.md +163 -0
- package/rules/python/js/docs/applies.md +108 -0
- package/rules/python/js/docs/tooling.md +153 -0
- package/rules/python/lint/docs/lint.md +322 -0
- package/rules/rego/docs/fix.md +121 -0
- package/rules/rego/js/docs/applies.md +174 -0
- package/rules/rego/js/docs/lint.md +118 -0
- package/rules/rego/lint/docs/lint.md +204 -0
- package/rules/release/docs/change.md +185 -0
- package/rules/release/docs/fix.md +119 -0
- package/rules/release/docs/release.md +222 -0
- package/rules/release/lib/docs/aggregate.md +246 -0
- package/rules/release/lib/docs/change-file.md +200 -0
- package/rules/release/lib/docs/fallback.md +203 -0
- package/rules/rust/docs/fix.md +129 -0
- package/rules/rust/js/docs/applies.md +140 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
- package/rules/security/docs/fix.md +86 -0
- package/rules/security/js/docs/lint.md +171 -0
- package/rules/security/js/docs/sample_secret.md +190 -0
- package/rules/security/js/docs/trufflehog.md +137 -0
- package/rules/security/js/lint.mjs +9 -1
- package/rules/style-lint/docs/fix.md +155 -0
- package/rules/style-lint/js/docs/lint.md +184 -0
- package/rules/style-lint/js/docs/tooling.md +194 -0
- package/rules/tauri/docs/fix.md +158 -0
- package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
- package/rules/tauri/js/docs/tooling.md +228 -0
- package/rules/test/coverage/coverage.mjs +15 -3
- package/rules/test/docs/fix.md +132 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
- package/rules/test/js/docs/cargo_mutants_config.md +173 -0
- package/rules/test/js/docs/location.md +136 -0
- package/rules/test/js/docs/no-process-chdir.md +160 -0
- package/rules/test/js/docs/no-relative-fs-path.md +271 -0
- package/rules/test/js/docs/stryker_config.md +152 -0
- package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
- package/rules/text/docs/fix.md +118 -0
- package/rules/text/js/docs/forbidden-prettier.md +143 -0
- package/rules/text/js/docs/formatting.md +256 -0
- package/rules/text/js/docs/lint.md +122 -0
- package/rules/text/lint/docs/lint.md +220 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
- package/rules/text/lint/docs/run-shellcheck.md +212 -0
- package/rules/text/lint/docs/run-v8r.md +197 -0
- package/rules/vue/docs/fix.md +127 -0
- package/rules/vue/js/docs/packages.md +335 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
- package/rules/worktree/docs/fix.md +161 -0
- package/schemas/rule-meta.json +5 -1
- package/scripts/auto-rules.mjs +7 -4
- package/scripts/coverage-classify/docs/apply.md +202 -0
- package/scripts/coverage-classify/docs/cache.md +203 -0
- package/scripts/coverage-classify/docs/index.md +218 -0
- package/scripts/coverage-classify/docs/prompt.md +132 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
- package/scripts/coverage-fix-extract.mjs +122 -0
- package/scripts/coverage-fix.mjs +1 -1
- package/scripts/dispatcher/docs/graph.md +346 -0
- package/scripts/dispatcher/docs/index.md +236 -0
- package/scripts/dispatcher/docs/trace.md +296 -0
- package/scripts/dispatcher/index.mjs +1 -1
- package/scripts/dispatcher/lib/active.mjs +4 -8
- package/scripts/dispatcher/lib/commands.mjs +7 -11
- package/scripts/dispatcher/lib/docs/active.md +348 -0
- package/scripts/dispatcher/lib/docs/artifact.md +232 -0
- package/scripts/dispatcher/lib/docs/budget.md +167 -0
- package/scripts/dispatcher/lib/docs/capability.md +196 -0
- package/scripts/dispatcher/lib/docs/commands.md +210 -0
- package/scripts/dispatcher/lib/docs/events.md +182 -0
- package/scripts/dispatcher/lib/docs/executor.md +190 -0
- package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
- package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
- package/scripts/dispatcher/lib/docs/gate.md +231 -0
- package/scripts/dispatcher/lib/docs/level.md +335 -0
- package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
- package/scripts/dispatcher/lib/docs/plan.md +200 -0
- package/scripts/dispatcher/lib/docs/planner.md +269 -0
- package/scripts/dispatcher/lib/docs/review.md +255 -0
- package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
- package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
- package/scripts/dispatcher/lib/docs/spec.md +203 -0
- package/scripts/dispatcher/lib/docs/state-store.md +303 -0
- package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
- package/scripts/dispatcher/lib/executor.mjs +6 -1
- package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
- package/scripts/dispatcher/lib/level.mjs +29 -3
- package/scripts/dispatcher/lib/review.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
- package/scripts/docs/auto-rules.md +376 -0
- package/scripts/docs/auto-skills.md +173 -0
- package/scripts/docs/build-agents-commands.md +183 -0
- package/scripts/docs/cli-entry.md +153 -0
- package/scripts/docs/coverage-fix.md +177 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
- package/scripts/lib/changed-files.mjs +4 -1
- package/scripts/lib/docs/changed-files.md +149 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
- package/scripts/lib/docs/check-reporter.md +175 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
- package/scripts/lib/docs/discover-checkable-rules.md +165 -0
- package/scripts/lib/docs/ensure-tool.md +254 -0
- package/scripts/lib/docs/generated-markdown.md +275 -0
- package/scripts/lib/docs/gha-workflow.md +326 -0
- package/scripts/lib/docs/inline-template-links.md +303 -0
- package/scripts/lib/docs/list-rule-ids.md +156 -0
- package/scripts/lib/docs/load-cursor-config.md +147 -0
- package/scripts/lib/docs/mirror-parity.md +167 -0
- package/scripts/lib/worktree.mjs +26 -0
- package/scripts/worktree-cli.mjs +12 -2
- package/skills/coverage-fix/SKILL.md +34 -45
- package/skills/docgen/SKILL.md +44 -23
- package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
- package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
- package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
- package/skills/docgen/js/docgen-ignore.mjs +54 -0
- package/skills/docgen/js/docgen-scan.mjs +37 -21
- package/skills/llm-patch/SKILL.md +23 -2
- package/skills/start-check/SKILL.md +26 -53
- package/skills/start-check/js/check.mjs +211 -0
- package/skills/taze/SKILL.md +9 -3
- package/skills/taze/js/diff.mjs +154 -0
- package/types/bin/n-cursor.d.ts +1 -1
- package/skills/fix-tests/SKILL.md +0 -119
- package/skills/fix-tests/meta.json +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# ensure-tool.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `ensure-tool.mjs` — єдина точка резолву зовнішніх CLI-залежностей пакета `@nitra/cursor`. Він гарантує, що потрібний бінарник (`hk`, `conftest`, `shellcheck`, `actionlint`, `dotenv-linter`, `opa`, `regal`, `hadolint`, `kubeconform`, `kubescape`) доступний у системі, виконуючи послідовний пошук:
|
|
6
|
+
|
|
7
|
+
1. У системному `PATH` (через `resolveCmd`).
|
|
8
|
+
2. У керованому кеші бінарників (`~/.cache/@nitra/cursor/bin/` на Linux/macOS або `%LOCALAPPDATA%\@nitra\cursor\bin\` на Windows).
|
|
9
|
+
3. Авто-встановлення відповідно до OS (`brew` для macOS, `scoop` для Windows із fallback на GitHub Release, прямий завантажувач GitHub Release для Linux).
|
|
10
|
+
4. Hard-fail з персоналізованою підказкою, якщо авто-встановлення вимкнено змінною середовища `N_CURSOR_NO_AUTO_INSTALL`.
|
|
11
|
+
|
|
12
|
+
Така архітектура усуває дублювання install-логіки в кожному `lint.mjs` / `fix.mjs`: щоб додати нову зовнішню утиліту, достатньо одного запису в реєстрі `TOOLS`. Додатково модуль експортує `ensureHkInstall`, який реєструє git pre-commit hook через `hk install` (пропускається в CI).
|
|
13
|
+
|
|
14
|
+
Файл написаний для Node.js (ESM), використовує лише стандартну бібліотеку та один локальний хелпер `resolveCmd`.
|
|
15
|
+
|
|
16
|
+
## Експорти / API
|
|
17
|
+
|
|
18
|
+
| Експорт | Тип | Призначення |
|
|
19
|
+
| ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `ensureTool(toolId)` | `function` | Резолвить і за потреби встановлює зовнішній CLI. Повертає абсолютний шлях до бінарника або кидає `Error`. |
|
|
21
|
+
| `ensureHkInstall(hkBin)` | `function` | Виконує `hk install` для реєстрації git pre-commit hook. Жодного return value; на помилку лише `console.warn`. |
|
|
22
|
+
|
|
23
|
+
Внутрішні (не експортуються, але формують контракт модуля):
|
|
24
|
+
|
|
25
|
+
- `TOOLS` — реєстр `Record<string, ToolEntry>` із описом install-стратегії для кожного тула.
|
|
26
|
+
- `ToolEntry` — JSDoc-тип, що описує поля одного запису реєстру.
|
|
27
|
+
- Допоміжні функції: `getCacheDir`, `mapArch`, `fetchLatestVersion`, `installFromGithub`, `installViaBrew`, `installViaScoop`, `autoInstall`, `buildHint`.
|
|
28
|
+
|
|
29
|
+
## Функції
|
|
30
|
+
|
|
31
|
+
### `getCacheDir()`
|
|
32
|
+
|
|
33
|
+
- **Сигнатура:** `getCacheDir(): string`
|
|
34
|
+
- **Параметри:** немає.
|
|
35
|
+
- **Повертає:** абсолютний шлях до каталогу кешу бінарників.
|
|
36
|
+
- **Логіка:**
|
|
37
|
+
- На `win32` бере `process.env.LOCALAPPDATA` (fallback `homedir()/AppData/Local`) і додає `@nitra/cursor/bin`.
|
|
38
|
+
- На інших платформах повертає `homedir()/.cache/@nitra/cursor/bin`.
|
|
39
|
+
- **Side effects:** немає (тільки читає env / `os.homedir`).
|
|
40
|
+
|
|
41
|
+
### `mapArch(nodeArch, style)`
|
|
42
|
+
|
|
43
|
+
- **Сигнатура:** `mapArch(nodeArch: 'x64'|'arm64'|string, style: 'hk'|'conftest'|'actionlint'): string`
|
|
44
|
+
- **Параметри:**
|
|
45
|
+
- `nodeArch` — значення `process.arch`.
|
|
46
|
+
- `style` — стиль іменування платформи для release-asset:
|
|
47
|
+
- `'actionlint'` → `amd64` / `arm64`.
|
|
48
|
+
- `'conftest'` → `x86_64` / `arm64`.
|
|
49
|
+
- `'hk'` (та інші release-asset стилі: shellcheck, dotenv-linter) → `x86_64` / `aarch64`.
|
|
50
|
+
- **Повертає:** рядок архітектури, очікуваний у назві release-asset.
|
|
51
|
+
- **Side effects:** немає.
|
|
52
|
+
|
|
53
|
+
### `fetchLatestVersion(repo, curlBin)`
|
|
54
|
+
|
|
55
|
+
- **Сигнатура:** `fetchLatestVersion(repo: string, curlBin: string): string`
|
|
56
|
+
- **Параметри:**
|
|
57
|
+
- `repo` — репозиторій у форматі `owner/repo`.
|
|
58
|
+
- `curlBin` — абсолютний шлях до бінарника `curl`.
|
|
59
|
+
- **Повертає:** версію останнього релізу без префікса `v` (наприклад `0.4.1`).
|
|
60
|
+
- **Поведінка:** через `spawnSync` викликає `curl -sSL -H "Accept: application/vnd.github+json" https://api.github.com/repos/<repo>/releases/latest`, парсить JSON, бере поле `tag_name`, прибирає префікс `v` за допомогою регексу `TAG_V_PREFIX_RE`.
|
|
61
|
+
- **Помилки:**
|
|
62
|
+
- `curl failed: ...` — `r.error` ненульове.
|
|
63
|
+
- `curl exit <status>: ...` — ненульовий exit-код.
|
|
64
|
+
- `GitHub API response is not JSON: ...` — некоректний JSON.
|
|
65
|
+
- `GitHub API: tag_name missing for <repo>` — у відповіді немає `tag_name`.
|
|
66
|
+
- **Side effects:** мережевий HTTP-запит до GitHub API.
|
|
67
|
+
|
|
68
|
+
### `installFromGithub(toolId, entry, cacheDir)`
|
|
69
|
+
|
|
70
|
+
- **Сигнатура:** `installFromGithub(toolId: string, entry: ToolEntry, cacheDir: string): string`
|
|
71
|
+
- **Параметри:**
|
|
72
|
+
- `toolId` — ключ у `TOOLS`.
|
|
73
|
+
- `entry` — `ToolEntry` для цього тула.
|
|
74
|
+
- `cacheDir` — абсолютний шлях до каталогу кешу.
|
|
75
|
+
- **Повертає:** абсолютний шлях до встановленого бінарника.
|
|
76
|
+
- **Послідовність дій:**
|
|
77
|
+
1. Резолвить `curl` та `tar` у PATH; за відсутності — кидає `Error`.
|
|
78
|
+
2. Через `fetchLatestVersion` отримує актуальну версію.
|
|
79
|
+
3. Формує назву asset через `entry.asset(ver)` і URL `https://github.com/<github>/releases/download/v<ver>/<asset>`.
|
|
80
|
+
4. Створює `cacheDir` (`mkdirSync` з `recursive: true`).
|
|
81
|
+
5. Завантажує asset через `curl -sSL -o <archivePath> <downloadUrl>`.
|
|
82
|
+
6. Якщо `entry.archive === false` — перейменовує завантажений файл у `<cacheDir>/<toolId>`, виставляє права `0o755` і повертає шлях.
|
|
83
|
+
7. Інакше викликає `tar` із прапорцем `-xJf` (для `.tar.xz`) або `-xzf` (для `.tar.gz`) для розпакування в `cacheDir`.
|
|
84
|
+
8. Визначає реальний шлях до бінарника через `entry.binFinder(ver)` або просто `toolId`. Перевіряє існування файлу.
|
|
85
|
+
9. Опціонально видаляє завантажений архів через `rm` (м’яко: якщо `rm` не знайдено, продовжує).
|
|
86
|
+
- **Помилки:**
|
|
87
|
+
- `curl не знайдено в PATH — потрібен для завантаження <toolId>`.
|
|
88
|
+
- `tar не знайдено в PATH — потрібен для встановлення <toolId>`.
|
|
89
|
+
- `Завантаження <toolId> не вдалось: ...` / `curl exit <status> при завантаженні <toolId>: ...`.
|
|
90
|
+
- `tar failed for <toolId>: ...` / `tar exit <status> для <toolId>: ...`.
|
|
91
|
+
- `Бінарник <toolId> не знайдено після розпакування: <binPath>`.
|
|
92
|
+
- **Side effects:** мережа, файлова система (створення/перейменування файлів, chmod, видалення архіву).
|
|
93
|
+
|
|
94
|
+
### `installViaBrew(toolId, entry)`
|
|
95
|
+
|
|
96
|
+
- **Сигнатура:** `installViaBrew(toolId: string, entry: ToolEntry): string`
|
|
97
|
+
- **Параметри:** `toolId` і `entry` як у попередній функції.
|
|
98
|
+
- **Повертає:** абсолютний шлях до бінарника після встановлення (через повторний `resolveCmd(toolId)`).
|
|
99
|
+
- **Послідовність дій:**
|
|
100
|
+
1. Резолвить `brew` у PATH; на відсутність кидає `brew не знайдено в PATH. Встанови Homebrew: https://brew.sh`.
|
|
101
|
+
2. Виконує `brew install <entry.brew>` із `stdio: 'inherit'` (інтерактивний прогрес).
|
|
102
|
+
3. Перевіряє exit-код і `error` об’єкт.
|
|
103
|
+
4. Після успіху повторно резолвить `toolId` у PATH; якщо й тоді нема — кидає `... не знайдено в PATH після brew install`.
|
|
104
|
+
- **Side effects:** виклик зовнішнього `brew install` (мережа, мутація системного стану на macOS).
|
|
105
|
+
|
|
106
|
+
### `installViaScoop(toolId, entry)`
|
|
107
|
+
|
|
108
|
+
- **Сигнатура:** `installViaScoop(toolId: string, entry: ToolEntry): string`
|
|
109
|
+
- **Параметри:** як вище.
|
|
110
|
+
- **Повертає:** абсолютний шлях до бінарника після встановлення.
|
|
111
|
+
- **Поведінка:** дзеркало `installViaBrew`, але для `scoop install`.
|
|
112
|
+
- Якщо `entry.scoop === null` — кидає `... недоступний у Scoop. Встанови вручну: https://github.com/<repo>/releases`.
|
|
113
|
+
- Якщо `scoop` не у PATH — кидає `scoop не знайдено в PATH. Встанови Scoop: https://scoop.sh`.
|
|
114
|
+
- **Side effects:** виклик зовнішнього `scoop install`.
|
|
115
|
+
|
|
116
|
+
### `autoInstall(toolId, entry, cacheDir)`
|
|
117
|
+
|
|
118
|
+
- **Сигнатура:** `autoInstall(toolId: string, entry: ToolEntry, cacheDir: string): string`
|
|
119
|
+
- **Поведінка диспетчера за платформою:**
|
|
120
|
+
- `darwin` → `installViaBrew`.
|
|
121
|
+
- `win32` → пробує `installViaScoop`, на будь-який throw — fallback на `installFromGithub` (наприклад, для `dotenv-linter` чи `regal`, де `scoop: null`).
|
|
122
|
+
- Інше (Linux) → `installFromGithub`.
|
|
123
|
+
- **Повертає:** абсолютний шлях до бінарника.
|
|
124
|
+
- **Side effects:** делегує своїм внутрішнім installer-ам.
|
|
125
|
+
|
|
126
|
+
### `buildHint(toolId, entry)`
|
|
127
|
+
|
|
128
|
+
- **Сигнатура:** `buildHint(toolId: string, entry: ToolEntry): string`
|
|
129
|
+
- **Параметри:** як вище.
|
|
130
|
+
- **Повертає:** багаторядкове повідомлення для error message при заблокованому авто-installі.
|
|
131
|
+
- **Формат:**
|
|
132
|
+
- Перший рядок — `❌ <toolId> не знайдено в PATH і авто-встановлення відключено (N_CURSOR_NO_AUTO_INSTALL).`.
|
|
133
|
+
- Другий рядок — ` Встанови:`.
|
|
134
|
+
- Далі — OS-specific підказка:
|
|
135
|
+
- macOS: ` macOS: brew install <entry.brew>`.
|
|
136
|
+
- Windows: ` Windows: scoop install <entry.scoop>` (якщо доступний) та ` або: https://github.com/<repo>/releases`.
|
|
137
|
+
- Linux: ` Linux: https://github.com/<repo>/releases`.
|
|
138
|
+
- **Side effects:** немає.
|
|
139
|
+
|
|
140
|
+
### `ensureTool(toolId)` _(export)_
|
|
141
|
+
|
|
142
|
+
- **Сигнатура:** `ensureTool(toolId: string): string`
|
|
143
|
+
- **Параметри:** `toolId` — ключ у реєстрі `TOOLS` (`'hk'`, `'conftest'`, `'shellcheck'`, `'actionlint'`, `'dotenv-linter'`, `'opa'`, `'regal'`, `'hadolint'`, `'kubeconform'`, `'kubescape'`).
|
|
144
|
+
- **Повертає:** абсолютний шлях до бінарника.
|
|
145
|
+
- **Послідовність резолву:**
|
|
146
|
+
1. **Валідація** — якщо `TOOLS[toolId]` відсутній, кидає `ensureTool: невідомий тул '<toolId>'`.
|
|
147
|
+
2. **PATH** — `resolveCmd(toolId)`; якщо знайдено — повертає одразу.
|
|
148
|
+
3. **Кеш** — `join(getCacheDir(), toolId)`; якщо файл існує — повертає його шлях. Зауваження: перевірка спрощена і не враховує `entry.binFinder` (для cached binaries `installFromGithub` уже клав фінальний бінарник у відоме місце або кеш просто не містить такого файлу — у такому разі переходимо до install).
|
|
149
|
+
4. **Авто-install** — якщо змінна середовища `N_CURSOR_NO_AUTO_INSTALL` не виставлена, викликає `autoInstall(toolId, entry, cacheDir)`.
|
|
150
|
+
5. **Hard-fail** — кидає `Error(buildHint(toolId, entry))`.
|
|
151
|
+
- **Помилки:** будь-яка з помилок `autoInstall` / `installFrom*` піднімається вгору; додатково — `невідомий тул` та `❌ ... не знайдено в PATH`.
|
|
152
|
+
- **Side effects:** мережа, файлова система, виклик зовнішніх install-команд (`brew`, `scoop`, `curl`, `tar`, `rm`).
|
|
153
|
+
|
|
154
|
+
### `ensureHkInstall(hkBin)` _(export)_
|
|
155
|
+
|
|
156
|
+
- **Сигнатура:** `ensureHkInstall(hkBin: string): void`
|
|
157
|
+
- **Параметри:**
|
|
158
|
+
- `hkBin` — абсолютний шлях до бінарника `hk` (зазвичай отриманий через `ensureTool('hk')`).
|
|
159
|
+
- **Повертає:** `void`.
|
|
160
|
+
- **Логіка:**
|
|
161
|
+
- Якщо `process.env.CI` truthy — функція виходить без дії (не реєструємо hook у CI).
|
|
162
|
+
- Інакше виконує `spawnSync(hkBin, ['install'], { stdio: 'inherit' })`.
|
|
163
|
+
- На `r.error` або ненульовий `r.status` виводить попередження через `console.warn` (не кидає!).
|
|
164
|
+
- **Side effects:** запис git pre-commit hook у `.git/hooks/pre-commit` (через `hk`), вивід у stdout/stderr.
|
|
165
|
+
|
|
166
|
+
## Залежності
|
|
167
|
+
|
|
168
|
+
### Стандартна бібліотека Node.js
|
|
169
|
+
|
|
170
|
+
- `node:child_process` — `spawnSync` для синхронного запуску `curl`, `tar`, `brew`, `scoop`, `rm`, `hk`.
|
|
171
|
+
- `node:fs` — `chmodSync`, `existsSync`, `mkdirSync`, `renameSync`.
|
|
172
|
+
- `node:os` — `homedir` для побудови шляху кешу.
|
|
173
|
+
- `node:path` — `join` для конструювання шляхів.
|
|
174
|
+
- `node:process` — `arch`, `env`, `platform`.
|
|
175
|
+
|
|
176
|
+
### Внутрішні
|
|
177
|
+
|
|
178
|
+
- `../utils/resolve-cmd.mjs` → `resolveCmd(cmd)` — кросплатформне резолвлення абсолютного шляху до бінарника в PATH (Linux/macOS — like `which`, Windows — like `where`).
|
|
179
|
+
|
|
180
|
+
### Зовнішні CLI (виконуються через `spawnSync`)
|
|
181
|
+
|
|
182
|
+
- `curl` — завантаження GitHub API і release-asset.
|
|
183
|
+
- `tar` — розпакування `.tar.gz` / `.tar.xz`.
|
|
184
|
+
- `rm` — м’яке видалення завантаженого архіву (необов’язково).
|
|
185
|
+
- `brew` — install на macOS.
|
|
186
|
+
- `scoop` — install на Windows.
|
|
187
|
+
- `hk` — реєстрація git pre-commit hook у `ensureHkInstall`.
|
|
188
|
+
|
|
189
|
+
### Змінні середовища
|
|
190
|
+
|
|
191
|
+
- `N_CURSOR_NO_AUTO_INSTALL` — якщо встановлена, блокує авто-install і змушує `ensureTool` кидати помилку з install-hint.
|
|
192
|
+
- `CI` — якщо truthy, `ensureHkInstall` нічого не робить.
|
|
193
|
+
- `LOCALAPPDATA` (Windows) — використовується для побудови шляху кешу.
|
|
194
|
+
|
|
195
|
+
## Потік виконання / Використання
|
|
196
|
+
|
|
197
|
+
### Типовий сценарій `ensureTool('shellcheck')` на Linux x64
|
|
198
|
+
|
|
199
|
+
1. Викликається `ensureTool('shellcheck')`.
|
|
200
|
+
2. `resolveCmd('shellcheck')` — не знайдено в `PATH`.
|
|
201
|
+
3. `getCacheDir()` повертає `/home/<user>/.cache/@nitra/cursor/bin`.
|
|
202
|
+
4. Перевірка `/home/<user>/.cache/@nitra/cursor/bin/shellcheck` — не існує.
|
|
203
|
+
5. `N_CURSOR_NO_AUTO_INSTALL` не виставлено → `autoInstall(...)`.
|
|
204
|
+
6. На Linux диспетчер викликає `installFromGithub('shellcheck', entry, cacheDir)`:
|
|
205
|
+
- `fetchLatestVersion('koalaman/shellcheck', curl)` → наприклад `0.10.0`.
|
|
206
|
+
- asset name = `shellcheck-v0.10.0.linux.x86_64.tar.xz`.
|
|
207
|
+
- URL = `https://github.com/koalaman/shellcheck/releases/download/v0.10.0/<asset>`.
|
|
208
|
+
- `mkdirSync(cacheDir, { recursive: true })`.
|
|
209
|
+
- `curl -sSL -o <cacheDir>/<asset> <url>`.
|
|
210
|
+
- `tar -xJf <asset> -C <cacheDir>` (бо `.tar.xz`).
|
|
211
|
+
- `binFinder('0.10.0')` → `shellcheck-v0.10.0/shellcheck`.
|
|
212
|
+
- Перевірка `existsSync(<cacheDir>/shellcheck-v0.10.0/shellcheck)`.
|
|
213
|
+
- `rm <archivePath>`.
|
|
214
|
+
- Повертає `<cacheDir>/shellcheck-v0.10.0/shellcheck`.
|
|
215
|
+
7. Викликач отримує абсолютний шлях і запускає `spawnSync(bin, [...args])`.
|
|
216
|
+
|
|
217
|
+
### Сценарій блокування авто-installу
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
process.env.N_CURSOR_NO_AUTO_INSTALL = '1'
|
|
221
|
+
ensureTool('hk')
|
|
222
|
+
// throws Error із багаторядковим hint, наприклад на macOS:
|
|
223
|
+
// ❌ hk не знайдено в PATH і авто-встановлення відключено (N_CURSOR_NO_AUTO_INSTALL).
|
|
224
|
+
// Встанови:
|
|
225
|
+
// macOS: brew install hk
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Сценарій реєстрації git hook
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { ensureTool, ensureHkInstall } from './lib/ensure-tool.mjs'
|
|
232
|
+
|
|
233
|
+
const hkBin = ensureTool('hk') // PATH → кеш → brew/scoop/github
|
|
234
|
+
ensureHkInstall(hkBin) // git hook у .git/hooks/pre-commit
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
У CI (`process.env.CI=true`) другий виклик стає no-op, що зручно для pipeline-ів, де hooks не потрібні.
|
|
238
|
+
|
|
239
|
+
### Розширення реєстру
|
|
240
|
+
|
|
241
|
+
Щоб додати новий тул `foo`:
|
|
242
|
+
|
|
243
|
+
1. Підбрати `brew`-формулу та `scoop`-пакет (або `null`, якщо відсутній).
|
|
244
|
+
2. Знайти GitHub-репо релізів і визначити `archStyle` (`hk` / `conftest` / `actionlint`).
|
|
245
|
+
3. Описати `asset(ver)` та, якщо потрібно, `binFinder(ver)` (коли бінарник лежить не в корені архіву).
|
|
246
|
+
4. Виставити `archive: false` для прямого бінарника без архіву.
|
|
247
|
+
5. Додати запис у `TOOLS`. Жодних змін у викликачах не потрібно — `ensureTool('foo')` запрацює одразу.
|
|
248
|
+
|
|
249
|
+
### Гарантії та інваріанти
|
|
250
|
+
|
|
251
|
+
- **Ідемпотентність:** повторний виклик `ensureTool` повертає шлях за O(1) після першого install — спочатку PATH, потім кеш.
|
|
252
|
+
- **Hard-fail:** на будь-яку нерозв’язну помилку install кидається `Error` із описовим повідомленням; немає silent fallback на «обірваний» бінарник.
|
|
253
|
+
- **Кросплатформність:** єдиний публічний API для трьох ОС; OS-specific деталі інкапсульовані всередині модуля.
|
|
254
|
+
- **Безпека для CI:** `ensureHkInstall` ніколи не змінює git-репозиторій під CI, навіть якщо `hk` доступний.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# generated-markdown.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `generated-markdown.mjs` містить набір чистих утиліт для генерації згенерованих маркдаун-файлів (зокрема `AGENTS.md` та `CLAUDE.md`) у межах CLI `n-cursor`. Він виконує дві основні задачі:
|
|
6
|
+
|
|
7
|
+
1. Розгортає Mustache-подібні блоки `{{#section}}…{{/section}}` із простою підстановкою одного поля `{{prop}}` для кожного елемента переданого масиву.
|
|
8
|
+
2. Нормалізує підсумковий markdown, не лишаючи послідовностей із трьох і більше `\n` (тобто двох і більше порожніх рядків поспіль), щоб результат відповідав правилу markdownlint **MD012** (no multiple blank lines).
|
|
9
|
+
|
|
10
|
+
Усі функції модуля — суто функціональні (без побічних ефектів, без I/O, без залежностей від глобального стану). Вхід — рядки та звичайні JS-обʼєкти, вихід — рядок.
|
|
11
|
+
|
|
12
|
+
Файл реалізовано як ES-модуль (ESM), розширення `.mjs`, експорти `export function`.
|
|
13
|
+
|
|
14
|
+
## Експорти / API
|
|
15
|
+
|
|
16
|
+
Модуль експортує чотири іменовані функції:
|
|
17
|
+
|
|
18
|
+
| Експорт | Тип | Призначення |
|
|
19
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `collapseMultipleBlankLines` | `(text: string) => string` | Згортає 3+ послідовних `\n` до рівно `\n\n`. |
|
|
21
|
+
| `expandMustacheSection` | `(template: string, section: string, items: Record<string,string>[], prop: string) => string` | Розгортає блок `{{#section}}…{{/section}}` для кожного елемента масиву `items`, підставляючи `item[prop]` замість `{{prop}}`. |
|
|
22
|
+
| `renderAgentsTemplate` | `(templateText: string, mdcBasenames: string[], skillItems: { name: string }[], commandItems: { name: string }[]) => string` | Високорівневий рендерер шаблону `AGENTS.template.md`: підставляє списки правил, скілів і команд та нормалізує порожні рядки. |
|
|
23
|
+
| `formatGeneratedMarkdownLines` | `(lines: string[]) => string` | Збирає масив рядків у єдиний markdown-документ із гарантованим завершальним `\n` і без подвійних порожніх рядків. |
|
|
24
|
+
|
|
25
|
+
Усі функції детерміновані: для однакових входів повертають однаковий вихід.
|
|
26
|
+
|
|
27
|
+
## Функції
|
|
28
|
+
|
|
29
|
+
### `collapseMultipleBlankLines(text)`
|
|
30
|
+
|
|
31
|
+
**Сигнатура**
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
export function collapseMultipleBlankLines(text)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Параметри**
|
|
38
|
+
|
|
39
|
+
- `text` (`string`) — вихідний markdown або будь-який текст. Якщо передано не-рядок, він буде явно скастовано через `String(text)`.
|
|
40
|
+
|
|
41
|
+
**Повертає**
|
|
42
|
+
|
|
43
|
+
- `string` — той самий текст, у якому всі послідовності з трьох і більше `\n` замінено на рівно два `\n` (тобто між блоками лишається не більше ніж один порожній рядок).
|
|
44
|
+
|
|
45
|
+
**Алгоритм**
|
|
46
|
+
|
|
47
|
+
1. Виконує `String(text)` — захист від нерядкового входу (наприклад, `null`, `undefined`, число, обʼєкт).
|
|
48
|
+
2. Викликає `replaceAll(/\n{3,}/g, '\n\n')` — глобальна заміна всіх послідовностей `\n{3,}` на `\n\n`.
|
|
49
|
+
|
|
50
|
+
**Side effects**
|
|
51
|
+
|
|
52
|
+
- Немає. Функція чиста.
|
|
53
|
+
|
|
54
|
+
**Особливості**
|
|
55
|
+
|
|
56
|
+
- Послідовності `\n\n` (один порожній рядок) залишаються без змін.
|
|
57
|
+
- Працює лише з символом `\n`; `\r\n` (CRLF) не нормалізується явно — якщо вхід містить CRLF, регулярка не спрацює на парах `\r\n\r\n\r\n` як на трьох `\n`.
|
|
58
|
+
|
|
59
|
+
### `expandMustacheSection(template, section, items, prop)`
|
|
60
|
+
|
|
61
|
+
**Сигнатура**
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
export function expandMustacheSection(template, section, items, prop)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Параметри**
|
|
68
|
+
|
|
69
|
+
- `template` (`string`) — вихідний текст шаблону, що може містити нуль чи більше блоків `{{#section}}…{{/section}}`.
|
|
70
|
+
- `section` (`string`) — імʼя секції (наприклад, `services`, `skills`, `commands`). Підставляється у відкриваючий тег `{{#section}}` і закриваючий `{{/section}}`.
|
|
71
|
+
- `items` (`Record<string, string>[]`) — масив елементів-обʼєктів. Для кожного елемента тіло секції повторюється один раз.
|
|
72
|
+
- `prop` (`string`) — імʼя поля, значення якого підставляється замість `{{prop}}` у тілі секції. Значення приводиться до рядка через `String(item[prop])`.
|
|
73
|
+
|
|
74
|
+
**Повертає**
|
|
75
|
+
|
|
76
|
+
- `string` — текст шаблону, у якому всі знайдені блоки `{{#section}}…{{/section}}` замінено на згенерований вміст. Якщо блоків немає — повертає вхідний рядок без змін.
|
|
77
|
+
|
|
78
|
+
**Алгоритм**
|
|
79
|
+
|
|
80
|
+
1. Будує константи `open = '{{#${section}}}'`, `close = '{{/${section}}}'`, `placeholder = '{{${prop}}}'`.
|
|
81
|
+
2. У циклі шукає першу позицію `start = indexOf(open)` та `end = indexOf(close)` у поточному значенні `result`.
|
|
82
|
+
3. Поки знайдено валідну пару (`start !== -1 && end !== -1 && end > start`):
|
|
83
|
+
- Витягує `inner = result.slice(start + open.length, end).trim()` — тіло секції без обрамних пробілів/переносів.
|
|
84
|
+
- Для кожного `item` з `items` будує рядок `inner.split(placeholder).join(String(item[prop]))` — повна заміна всіх входжень `placeholder` на значення поля.
|
|
85
|
+
- Зʼєднує всі рендери одним `\n` (без зайвих порожніх рядків між елементами).
|
|
86
|
+
- Підставляє згенерований текст замість усього блоку: `result = result.slice(0, start) + rendered + result.slice(end + close.length)`.
|
|
87
|
+
- Перераховує `start` і `end` для наступної ітерації.
|
|
88
|
+
4. Повертає `result`.
|
|
89
|
+
|
|
90
|
+
**Side effects**
|
|
91
|
+
|
|
92
|
+
- Немає. Функція чиста.
|
|
93
|
+
|
|
94
|
+
**Особливості й обмеження**
|
|
95
|
+
|
|
96
|
+
- Блок підставляється лише за умови `end > start`, тобто закриття має йти після відкриття; вкладені секції з тим самим імʼям не підтримуються.
|
|
97
|
+
- `trim()` тіла секції видаляє пробіли/переноси на початку й у кінці — це навмисно, щоб не лишати пустого рядка на стику з оточуючим markdown.
|
|
98
|
+
- Plаceholder `{{prop}}` замінюється через `split(...).join(...)` — це еквівалентно глобальній заміні без побудови RegExp.
|
|
99
|
+
- Якщо `items` порожній — блок замінюється на порожній рядок (`[].join('\n') === ''`).
|
|
100
|
+
- Підстановка `prop` єдина на блок: модуль не підтримує кілька різних змінних усередині одного блоку.
|
|
101
|
+
|
|
102
|
+
### `renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems)`
|
|
103
|
+
|
|
104
|
+
**Сигнатура**
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
export function renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Параметри**
|
|
111
|
+
|
|
112
|
+
- `templateText` (`string`) — повний вміст файлу `AGENTS.template.md` із Mustache-блоками `{{#services}}`, `{{#skills}}`, `{{#commands}}` (кожен зі своїм `{{name}}` усередині).
|
|
113
|
+
- `mdcBasenames` (`string[]`) — масив імен файлів правил `*.mdc` з каталогу `.cursor/rules` (саме базові імена, без шляху).
|
|
114
|
+
- `skillItems` (`{ name: string }[]`) — готові рядки для секції Skills у форматі `{ name: '…' }` (зазвичай вже включають bullet/опис).
|
|
115
|
+
- `commandItems` (`{ name: string }[]`) — готові рядки для секції commands у форматі `{ name: '…' }`.
|
|
116
|
+
|
|
117
|
+
**Повертає**
|
|
118
|
+
|
|
119
|
+
- `string` — готовий вміст `AGENTS.md` із розгорнутими секціями та згорнутими подвійними порожніми рядками.
|
|
120
|
+
|
|
121
|
+
**Алгоритм**
|
|
122
|
+
|
|
123
|
+
1. Перетворює `mdcBasenames` на `serviceItems` виду `{ name: '- .cursor/rules/${mdcName}' }` — додає префікс маркера списку й каталог `.cursor/rules/`.
|
|
124
|
+
2. Послідовно викликає `expandMustacheSection` для трьох секцій:
|
|
125
|
+
- `services` із `serviceItems` і ключем `name`;
|
|
126
|
+
- `skills` із `skillItems` і ключем `name`;
|
|
127
|
+
- `commands` із `commandItems` і ключем `name`.
|
|
128
|
+
3. Передає результат у `collapseMultipleBlankLines` і повертає його.
|
|
129
|
+
|
|
130
|
+
**Side effects**
|
|
131
|
+
|
|
132
|
+
- Немає. Файлову систему не читає й не пише — лише трансформує переданий рядок.
|
|
133
|
+
|
|
134
|
+
**Особливості**
|
|
135
|
+
|
|
136
|
+
- Ключ `name` зашитий у функцію — структура `skillItems` і `commandItems` має містити саме поле `name`.
|
|
137
|
+
- Кожен елемент `mdcBasenames` форматується як bullet списку (`- ...`) у єдиному стилі.
|
|
138
|
+
|
|
139
|
+
### `formatGeneratedMarkdownLines(lines)`
|
|
140
|
+
|
|
141
|
+
**Сигнатура**
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
export function formatGeneratedMarkdownLines(lines)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Параметри**
|
|
148
|
+
|
|
149
|
+
- `lines` (`string[]`) — масив рядків markdown-документа. Кожен елемент — окремий «рядок» (може бути порожнім або містити кілька логічних рядків).
|
|
150
|
+
|
|
151
|
+
**Повертає**
|
|
152
|
+
|
|
153
|
+
- `string` — підсумковий текст, у якому:
|
|
154
|
+
- елементи `lines` зʼєднано через `\n`;
|
|
155
|
+
- послідовності з 3+ `\n` згорнуто до `\n\n` (через `collapseMultipleBlankLines`);
|
|
156
|
+
- гарантовано додано завершальний `\n`, якщо його не було.
|
|
157
|
+
|
|
158
|
+
**Алгоритм**
|
|
159
|
+
|
|
160
|
+
1. `text = lines.join('\n')` — конкатенація через символ переносу рядка.
|
|
161
|
+
2. `collapsed = collapseMultipleBlankLines(text)` — нормалізація порожніх рядків.
|
|
162
|
+
3. Якщо `collapsed.endsWith('\n')` — повернути як є; інакше повернути `collapsed + '\n'`.
|
|
163
|
+
|
|
164
|
+
**Side effects**
|
|
165
|
+
|
|
166
|
+
- Немає.
|
|
167
|
+
|
|
168
|
+
**Особливості**
|
|
169
|
+
|
|
170
|
+
- Завершальний `\n` гарантує, що згенерований файл закінчується новим рядком (вимога багатьох лінтерів і POSIX-конвенції).
|
|
171
|
+
- Жодних відступів чи табуляцій функція не корегує — лише вертикальну щільність.
|
|
172
|
+
|
|
173
|
+
## Залежності
|
|
174
|
+
|
|
175
|
+
### Зовнішні
|
|
176
|
+
|
|
177
|
+
- Жодних. Модуль не імпортує нічого ні з `node:*`, ні з npm-пакетів, ні з інших файлів проєкту.
|
|
178
|
+
|
|
179
|
+
### Внутрішні (між функціями модуля)
|
|
180
|
+
|
|
181
|
+
- `renderAgentsTemplate` використовує `expandMustacheSection` (тричі) і `collapseMultipleBlankLines` (один раз).
|
|
182
|
+
- `formatGeneratedMarkdownLines` використовує `collapseMultipleBlankLines`.
|
|
183
|
+
- `collapseMultipleBlankLines` і `expandMustacheSection` — незалежні низькорівневі будівельні блоки.
|
|
184
|
+
|
|
185
|
+
### Runtime
|
|
186
|
+
|
|
187
|
+
- ECMAScript із підтримкою:
|
|
188
|
+
- `String.prototype.replaceAll` (Node.js ≥ 15);
|
|
189
|
+
- класичних `String.prototype.indexOf`, `slice`, `split`, `join`, `trim`, `endsWith`;
|
|
190
|
+
- синтаксису ESM (`export function`).
|
|
191
|
+
|
|
192
|
+
## Потік виконання / Використання
|
|
193
|
+
|
|
194
|
+
Типовий потік генерації `AGENTS.md`:
|
|
195
|
+
|
|
196
|
+
1. Викликач (CLI `n-cursor`) читає шаблон `AGENTS.template.md` з файлової системи.
|
|
197
|
+
2. Збирає три масиви даних:
|
|
198
|
+
- список `*.mdc`-файлів із `.cursor/rules` (через `fs.readdir` або еквівалент);
|
|
199
|
+
- метадані скілів (`name` уже з bullet/описом);
|
|
200
|
+
- метадані команд (`name` уже з bullet/описом).
|
|
201
|
+
3. Викликає `renderAgentsTemplate(templateText, mdcBasenames, skillItems, commandItems)`.
|
|
202
|
+
4. Записує отриманий рядок у файл `AGENTS.md` (наприклад, через `fs.writeFile`).
|
|
203
|
+
|
|
204
|
+
Типовий потік генерації багаторядкового документа (наприклад, `CLAUDE.md`):
|
|
205
|
+
|
|
206
|
+
1. Викликач формує масив рядків `lines` — заголовки, абзаци, bullet-и тощо. Між логічними секціями може лишатися кілька порожніх рядків.
|
|
207
|
+
2. Викликає `formatGeneratedMarkdownLines(lines)` — отримує цілісний документ із чистими стиками секцій та фінальним `\n`.
|
|
208
|
+
3. Записує результат у файл.
|
|
209
|
+
|
|
210
|
+
Приклад використання `expandMustacheSection` ізольовано:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
import { expandMustacheSection } from './generated-markdown.mjs'
|
|
214
|
+
|
|
215
|
+
const tpl = '# Rules\n{{#services}}- {{name}}\n{{/services}}\nEnd'
|
|
216
|
+
const out = expandMustacheSection(tpl, 'services', [{ name: 'a.mdc' }, { name: 'b.mdc' }], 'name')
|
|
217
|
+
// out === '# Rules\n- a.mdc\n- b.mdc\nEnd'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Приклад використання `collapseMultipleBlankLines`:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { collapseMultipleBlankLines } from './generated-markdown.mjs'
|
|
224
|
+
|
|
225
|
+
const cleaned = collapseMultipleBlankLines('A\n\n\n\nB')
|
|
226
|
+
// cleaned === 'A\n\nB'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Приклад використання `formatGeneratedMarkdownLines`:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import { formatGeneratedMarkdownLines } from './generated-markdown.mjs'
|
|
233
|
+
|
|
234
|
+
const md = formatGeneratedMarkdownLines(['# Title', '', '', '', 'Body'])
|
|
235
|
+
// md === '# Title\n\nBody\n'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Rebuild Test
|
|
239
|
+
|
|
240
|
+
Контрольний приклад для перевірки коректності модуля після рефакторингу:
|
|
241
|
+
|
|
242
|
+
Дано шаблон:
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
# Agents
|
|
246
|
+
|
|
247
|
+
## Services
|
|
248
|
+
{{#services}}
|
|
249
|
+
{{name}}
|
|
250
|
+
{{/services}}
|
|
251
|
+
|
|
252
|
+
## Skills
|
|
253
|
+
{{#skills}}
|
|
254
|
+
{{name}}
|
|
255
|
+
{{/skills}}
|
|
256
|
+
|
|
257
|
+
## Commands
|
|
258
|
+
{{#commands}}
|
|
259
|
+
{{name}}
|
|
260
|
+
{{/commands}}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
і виклик:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
renderAgentsTemplate(template, ['n-bun.mdc', 'n-vue.mdc'], [{ name: '- /n-fix — fix' }], [{ name: '- /n-lint — lint' }])
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Очікувані властивості результату:
|
|
270
|
+
|
|
271
|
+
- секція `services` містить рядки `- .cursor/rules/n-bun.mdc` і `- .cursor/rules/n-vue.mdc`, розділені одним `\n`;
|
|
272
|
+
- секція `skills` містить рядок `- /n-fix — fix`;
|
|
273
|
+
- секція `commands` містить рядок `- /n-lint — lint`;
|
|
274
|
+
- ніде немає трьох поспіль `\n` (тобто не зʼявляються два порожніх рядки підряд);
|
|
275
|
+
- виклик `formatGeneratedMarkdownLines(result.split('\n'))` повертає той самий зміст із гарантованим завершальним `\n`.
|