@nitra/cursor 1.8.178 → 1.8.180
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 +13 -0
- package/mdc/abie.mdc +1 -1
- package/mdc/js-run.mdc +15 -1
- package/package.json +1 -1
- package/scripts/check-abie.mjs +5 -2
- package/scripts/check-docker.mjs +1 -1
- package/scripts/check-graphql.mjs +1 -0
- package/scripts/check-js-run.mjs +26 -0
- package/scripts/check-k8s.mjs +1 -1
- package/scripts/check-nginx-default-tpl.mjs +3 -0
- package/scripts/check-npm-module.mjs +1 -0
- package/scripts/claude-stop-hook.mjs +23 -21
- package/scripts/rename-yaml-extensions.mjs +1 -0
- package/scripts/sync-claude-config.mjs +28 -28
- package/scripts/utils/ast-scan-utils.mjs +1 -1
- package/scripts/utils/walkDir.mjs +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.180] - 2026-05-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `js-run` (mdc v1.1 → v1.2): додано секцію **«Область застосування»** — правило явно не застосовується до frontend-пакетів (маркер `vite` у `devDependencies`). У браузерному бандлі немає `node:process`, тому заміна `process.env.X` на `import { env } from 'node:process'` ламає рантайм (`TypeError: Cannot read properties of undefined (reading 'X')`); для frontend замість `process.env.NODE_ENV` — `import.meta.env.MODE` / `import.meta.env.PROD`, інші ENV — лише `import.meta.env.VITE_*`. Передумова — інцидент у abie/b2b `site/`, де LLM-агент за правилом замінив `process.env.NODE_ENV` у `src/main.js` і вибив прод-бандл.
|
|
12
|
+
- `check-js-run.mjs`: workspace-пакети з `vite` у `devDependencies` пропускаються — нова `packageJsonHasViteDevDependency(pkgJson)`, виклик одразу після `loadPackageJsonAndCheckBunyanDeps`. bunyan-залежність у `package.json` все одно перевіряється (бо це робиться до раннього виходу), але скан `process.env`, `#conn/*` і OTEL configmap для frontend-пакета не запускається. Тести: 2 нові кейси у `check-js-run-fixture.test.mjs` (vite-пакет з прямим `process.env` — pass; non-vite пакет з тим же кодом — fail).
|
|
13
|
+
|
|
14
|
+
## [1.8.179] - 2026-05-05
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `abie` (`check-abie.mjs` / `mdc/abie.mdc`): `httpHealthCheck.requestPath` у `HealthCheckPolicy` (`hc.yaml`) тепер допускає будь-який непорожній шлях від кореня — рядок, що починається з `/` (`/healthz`, `/IsAlive`, `/api/live` тощо), замість жорсткої вимоги `/healthz`. Решта вимог незмінна: `type: HTTP`, `port: 8080`, `targetRef` на headless Service з суфіксом `-hl`. Канонічно рекомендується `/healthz`, але правило не блокує сервіси з власним liveness endpoint. JSDoc у `check-abie.mjs` і опис у `mdc/abie.mdc` приведено у відповідність до коду. Тести: 3 нові кейси у `check-abie.test.mjs` (нестандартний `/IsAlive`, відсутній лідируючий `/`, порожній рядок).
|
|
19
|
+
|
|
7
20
|
## [1.8.178] - 2026-05-05
|
|
8
21
|
|
|
9
22
|
### Added
|
package/mdc/abie.mdc
CHANGED
|
@@ -12,7 +12,7 @@ version: '1.17'
|
|
|
12
12
|
|
|
13
13
|
## k8s: `hc.yaml` поруч із Deployment
|
|
14
14
|
|
|
15
|
-
Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** — **headless** **Service** з суфіксом **`-hl`** (узгоджено з парою **`svc.yaml`** / **`svc-hl.yaml`** у **k8s.mdc**): або **`${metadata.name}-hl`**, або те саме ім’я, якщо **`metadata.name`** уже з **`-hl`**.
|
|
15
|
+
Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`httpHealthCheck.requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: канонічно **`/healthz`**, але також допустимі **`/IsAlive`**, **`/api/live`** тощо — узгоджується з реальним endpoint сервісу), порт **8080**, **`targetRef.name`** — **headless** **Service** з суфіксом **`-hl`** (узгоджено з парою **`svc.yaml`** / **`svc-hl.yaml`** у **k8s.mdc**): або **`${metadata.name}-hl`**, або те саме ім’я, якщо **`metadata.name`** уже з **`-hl`**.
|
|
16
16
|
|
|
17
17
|
```yaml title="hc.yaml"
|
|
18
18
|
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
package/mdc/js-run.mdc
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Область застосування
|
|
8
|
+
|
|
9
|
+
Правило стосується **виключно backend Node.js workspace-пакетів** (jobs, GraphQL/HTTP-сервери, CLI). **Не застосовується** до frontend-пакетів, які бандляться в браузер: маркер — наявність `vite` у `devDependencies` пакета (`site/`, мобільні Capacitor-пакети, будь-яка Vue/Quasar SPA).
|
|
10
|
+
|
|
11
|
+
У браузерному середовищі:
|
|
12
|
+
|
|
13
|
+
- немає `node:process` — імпорт `import { env } from 'node:process'` resolve'иться у `undefined`, і `env.X` падає з `TypeError: Cannot read properties of undefined`;
|
|
14
|
+
- `process.env.X` у джерелах пакета відсутнє в рантаймі — Vite або взагалі не підставляє його, або підставляє лише `process.env.NODE_ENV`;
|
|
15
|
+
- усі змінні оточення для frontend задаються через `VITE_*` і доступні як `import.meta.env.VITE_X` (типобезпечно через `vite-check-env`); режим — `import.meta.env.MODE` / `import.meta.env.PROD`.
|
|
16
|
+
|
|
17
|
+
Тому **у frontend-пакетах не торкайся `process.env.*`** і **не додавай** `import { env } from 'node:process'`. Якщо натрапив на `process.env.NODE_ENV` у frontend-коді — заміна, якщо взагалі потрібна, лише на `import.meta.env.MODE`.
|
|
18
|
+
|
|
7
19
|
## Структура проекту
|
|
8
20
|
|
|
9
21
|
Рекомендується використовувати таку структуру проекту:
|
|
@@ -108,6 +120,8 @@ export const db = new SQL({ url: env.PG_CONN })
|
|
|
108
120
|
|
|
109
121
|
Прямий доступ до `process.env.X` у коді заборонений — його треба замінити на `env`:
|
|
110
122
|
|
|
123
|
+
> Стосується лише backend-пакетів (див. **Область застосування**). У frontend-пакетах (`vite` у `devDependencies`) — **не змінюй** `process.env.*` і **не додавай** імпорт `node:process`.
|
|
124
|
+
|
|
111
125
|
- **обов'язкова змінна** — `import { checkEnv, env } from '@nitra/check-env'` плюс `checkEnv(['X'])`
|
|
112
126
|
у тому ж файлі (приклад див. вище в розділі **CheckEnv**);
|
|
113
127
|
- **опційна змінна** — `import { env } from 'node:process'`:
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
|
|
15
15
|
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
16
|
-
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
|
|
16
|
+
* як у abie.mdc, **`requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: **`/healthz`**, **`/IsAlive`**, **`/api/live`** тощо), порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
|
|
17
17
|
* якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
|
|
18
18
|
* Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc** (наприклад **HttpBackendGroup** `alb.yc.io/v1alpha1` — **без** modeline).
|
|
19
19
|
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
@@ -1324,7 +1324,10 @@ function validateAbieHcPolicy(policy, relPath) {
|
|
|
1324
1324
|
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
1325
1325
|
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
1326
1326
|
}
|
|
1327
|
-
|
|
1327
|
+
const requestPath = typeof httpHc.requestPath === 'string' ? httpHc.requestPath.trim() : ''
|
|
1328
|
+
if (requestPath === '' || !requestPath.startsWith('/')) {
|
|
1329
|
+
return `${relPath}: httpHealthCheck.requestPath має бути непорожнім шляхом від кореня (рядок, що починається з /) (abie.mdc)`
|
|
1330
|
+
}
|
|
1328
1331
|
if (httpHc.port !== 8080) return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
1329
1332
|
const targetRef = specRec.targetRef
|
|
1330
1333
|
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export function isDockerfileName(name) {
|
|
|
66
66
|
/**
|
|
67
67
|
* Збирає абсолютні шляхи до Dockerfile / Containerfile від кореня cwd.
|
|
68
68
|
* @param {string} root корінь репозиторію
|
|
69
|
-
* @param {string[]} [ignorePaths
|
|
69
|
+
* @param {string[]} [ignorePaths] шляхи каталогів, повністю виключених з обходу
|
|
70
70
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи
|
|
71
71
|
*/
|
|
72
72
|
export async function findDockerfilePaths(root, ignorePaths = []) {
|
|
@@ -32,6 +32,7 @@ export const REQUIRED_DUMP_SCHEMA_SCRIPT =
|
|
|
32
32
|
/**
|
|
33
33
|
* Збирає абсолютні шляхи source-файлів, які підлягають скануванню на gql templates.
|
|
34
34
|
* @param {string} root абсолютний шлях кореня
|
|
35
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
35
36
|
* @returns {Promise<string[]>} список кандидатів
|
|
36
37
|
*/
|
|
37
38
|
async function collectScanCandidates(root, ignorePaths) {
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -53,6 +53,7 @@ function relPosix(absPackageRoot, absPath) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
55
55
|
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
56
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
56
57
|
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
57
58
|
* @param {(msg: string) => void} fail callback при помилці
|
|
58
59
|
* @returns {Promise<number>} кількість знайдених порушень
|
|
@@ -86,6 +87,7 @@ async function checkBunyanImports(absPackageRoot, ignorePaths, label, fail) {
|
|
|
86
87
|
/**
|
|
87
88
|
* Збирає всі JS/TS-файли пакета (без node_modules, dist тощо).
|
|
88
89
|
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
90
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
89
91
|
* @returns {Promise<string[]>} абсолютні шляхи до файлів
|
|
90
92
|
*/
|
|
91
93
|
async function collectSourceFiles(absPackageRoot, ignorePaths) {
|
|
@@ -158,6 +160,7 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
|
158
160
|
/**
|
|
159
161
|
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
160
162
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
163
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
161
164
|
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
162
165
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
163
166
|
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
@@ -167,6 +170,15 @@ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
|
167
170
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
168
171
|
const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
|
|
169
172
|
|
|
173
|
+
// Frontend-пакети (vite у devDependencies) виходять за межі js-run:
|
|
174
|
+
// браузерний бандл не має `node:process`, а `process.env.*` бандлер
|
|
175
|
+
// обробляє самостійно. Перевірку process.env / conn-аліасів пропускаємо,
|
|
176
|
+
// bunyan-залежність уже звірено в `loadPackageJsonAndCheckBunyanDeps`.
|
|
177
|
+
if (packageJsonHasViteDevDependency(pkgJson)) {
|
|
178
|
+
passFn(`${label}vite-пакет (frontend) — js-run пропущено (process.env / conn-aliases / OTEL configmap)`)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
|
|
171
183
|
if (importViolations === 0) {
|
|
172
184
|
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
@@ -190,6 +202,20 @@ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
|
190
202
|
await checkOtelConfigmap(rootDir, label, fail, passFn)
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Чи має пакет `vite` у `devDependencies` (маркер frontend-пакета — vite/quasar/capacitor SPA).
|
|
207
|
+
* Семантично ідентично `packageJsonLacksViteDevDependency` з `auto-rules.mjs`, але
|
|
208
|
+
* приймає вже розпарсений pkgJson.
|
|
209
|
+
* @param {unknown} pkgJson розпарсений `package.json` пакета (або null)
|
|
210
|
+
* @returns {boolean} true, якщо `vite` присутній у `devDependencies`
|
|
211
|
+
*/
|
|
212
|
+
function packageJsonHasViteDevDependency(pkgJson) {
|
|
213
|
+
if (!pkgJson || typeof pkgJson !== 'object' || Array.isArray(pkgJson)) return false
|
|
214
|
+
const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
|
|
215
|
+
if (!devDeps || typeof devDeps !== 'object' || Array.isArray(devDeps)) return false
|
|
216
|
+
return Object.hasOwn(devDeps, 'vite')
|
|
217
|
+
}
|
|
218
|
+
|
|
193
219
|
/**
|
|
194
220
|
* Завантажує `package.json` пакета (якщо є) і реєструє порушення для bunyan-залежностей.
|
|
195
221
|
* @param {string} rootDir відносний шлях workspace
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -1636,7 +1636,7 @@ export function baseKustomizationNamespaceViolation(obj) {
|
|
|
1636
1636
|
/**
|
|
1637
1637
|
* Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
|
|
1638
1638
|
* @param {string} root корінь репозиторію (cwd)
|
|
1639
|
-
* @param {string[]} [ignorePaths
|
|
1639
|
+
* @param {string[]} [ignorePaths] шляхи каталогів, повністю виключених з обходу
|
|
1640
1640
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
|
|
1641
1641
|
*/
|
|
1642
1642
|
async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
@@ -35,6 +35,7 @@ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
|
|
|
35
35
|
/**
|
|
36
36
|
* Збирає абсолютні шляхи до **default.conf.template** у репозиторії; шлях `tests/fixtures` не обходиться як проєктний шаблон.
|
|
37
37
|
* @param {string} root корінь cwd
|
|
38
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
38
39
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
|
|
39
40
|
*/
|
|
40
41
|
export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
|
|
@@ -57,6 +58,7 @@ export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
|
|
|
57
58
|
* Знаходить у дереві від `root` усі **default.tpl.conf**. Якщо поруч немає **default.conf.template** —
|
|
58
59
|
* перейменовує файл; якщо є — перезаписує **default.conf.template** вмістом **default.tpl.conf** і видаляє **default.tpl.conf**.
|
|
59
60
|
* @param {string} root корінь обходу (зазвичай cwd репозиторію)
|
|
61
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
60
62
|
* @returns {Promise<{ renamed: string[], overwritten: string[] }>} відносні шляхи до обробленого **default.tpl.conf** (для звіту)
|
|
61
63
|
*/
|
|
62
64
|
export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
|
|
@@ -322,6 +324,7 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
|
|
|
322
324
|
/**
|
|
323
325
|
* Перевіряє Dockerfile на наявність gzip та envsubst.
|
|
324
326
|
* @param {string} root корінь репозиторію
|
|
327
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
325
328
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
326
329
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
327
330
|
*/
|
|
@@ -36,6 +36,7 @@ const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
39
|
+
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
39
40
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
40
41
|
*/
|
|
41
42
|
async function npmSrcTreeHasJsFile(ignorePaths = []) {
|
|
@@ -11,25 +11,27 @@
|
|
|
11
11
|
* `npx --no @nitra/cursor stop-hook`
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from 'node:child_process'
|
|
14
|
+
import { once } from 'node:events'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Зчитує stdin до EOF як utf8 рядок. Якщо stdin порожній (TTY) — повертає '' одразу.
|
|
17
18
|
* @returns {Promise<string>} вміст stdin
|
|
18
19
|
*/
|
|
19
|
-
function readStdin() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
process.stdin.on('data', chunk => {
|
|
28
|
-
data += chunk
|
|
29
|
-
})
|
|
30
|
-
process.stdin.on('end', () => resolve(data))
|
|
31
|
-
process.stdin.on('error', () => resolve(data))
|
|
20
|
+
async function readStdin() {
|
|
21
|
+
if (process.stdin.isTTY) {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
process.stdin.setEncoding('utf8')
|
|
25
|
+
const chunks = []
|
|
26
|
+
process.stdin.on('data', chunk => {
|
|
27
|
+
chunks.push(chunk)
|
|
32
28
|
})
|
|
29
|
+
try {
|
|
30
|
+
await once(process.stdin, 'end')
|
|
31
|
+
} catch {
|
|
32
|
+
// 'error' на stdin — повертаємо те, що встигли зібрати
|
|
33
|
+
}
|
|
34
|
+
return chunks.join('')
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -60,12 +62,12 @@ export async function runStopHookCli() {
|
|
|
60
62
|
if (isRecursiveStopHookCall(stdin)) {
|
|
61
63
|
return 0
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
65
|
+
const child = spawn('npx', ['--no', '@nitra/cursor', 'check'], { stdio: 'inherit' })
|
|
66
|
+
try {
|
|
67
|
+
const [code] = await once(child, 'exit')
|
|
68
|
+
return code ?? 1
|
|
69
|
+
} catch (error) {
|
|
70
|
+
process.stderr.write(`stop-hook: не вдалося запустити npx @nitra/cursor check — ${error.message}\n`)
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
71
73
|
}
|
|
@@ -68,6 +68,7 @@ export function replaceExtension(relPosix, newExt) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Збирає операції перейменування (без виконання).
|
|
70
70
|
* @param {string} rootAbs абсолютний корінь репозиторію
|
|
71
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
71
72
|
* @returns {Promise<Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>>} відсортовані операції перейменування без запису на диск
|
|
72
73
|
*/
|
|
73
74
|
async function collectRenameOps(rootAbs, ignorePaths) {
|
|
@@ -28,27 +28,27 @@ const TEMPLATE_DIR_NAME = '.claude-template'
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @typedef {object} HookEntry
|
|
31
|
-
* @property {string} type
|
|
32
|
-
* @property {string} command
|
|
33
|
-
* @property {number} [timeout]
|
|
31
|
+
* @property {string} type тип hook'а у форматі Claude Code (зазвичай `'command'`)
|
|
32
|
+
* @property {string} command команда, яку виконує Claude Code (наш маркер живе саме тут)
|
|
33
|
+
* @property {number} [timeout] опційний таймаут у секундах
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* @typedef {object} HookGroup
|
|
38
|
-
* @property {string} [matcher]
|
|
39
|
-
* @property {HookEntry[]} hooks
|
|
38
|
+
* @property {string} [matcher] патерн (наприклад, `'.*'`) для звуження hook'а
|
|
39
|
+
* @property {HookEntry[]} hooks впорядкований список команд hook-групи
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* @typedef {object} ClaudeSettings
|
|
44
|
-
* @property {{ allow?: string[] }} [permissions]
|
|
45
|
-
* @property {Record<string, HookGroup[]>} [hooks]
|
|
44
|
+
* @property {{ allow?: string[] }} [permissions] секція `permissions` із .claude/settings.json
|
|
45
|
+
* @property {Record<string, HookGroup[]>} [hooks] hooks за подіями (`Stop`, `PreToolUse`, ...)
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Чи hook-група містить лише наші managed-команди (за маркером).
|
|
50
|
-
* @param {HookGroup} group
|
|
51
|
-
* @returns {boolean}
|
|
50
|
+
* @param {HookGroup} group hook-група з .claude/settings.json
|
|
51
|
+
* @returns {boolean} `true`, якщо всі hooks мають маркер `MANAGED_HOOK_COMMAND_MARKER`
|
|
52
52
|
*/
|
|
53
53
|
function isManagedHookGroup(group) {
|
|
54
54
|
if (!group?.hooks?.length) {
|
|
@@ -60,9 +60,9 @@ function isManagedHookGroup(group) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Зливає список allow-permissions: union існуючого і темплейтного без дублікатів,
|
|
62
62
|
* порядок — спочатку існуючі (щоб не міняти користувацький порядок), потім нові.
|
|
63
|
-
* @param {string[] | undefined} existing
|
|
64
|
-
* @param {string[] | undefined} fromTemplate
|
|
65
|
-
* @returns {string[]}
|
|
63
|
+
* @param {string[] | undefined} existing існуючий список з `.claude/settings.json` користувача
|
|
64
|
+
* @param {string[] | undefined} fromTemplate список з темплейту пакета `@nitra/cursor`
|
|
65
|
+
* @returns {string[]} об'єднаний список без дублікатів (порядок: існуючі, потім нові)
|
|
66
66
|
*/
|
|
67
67
|
export function mergeAllowList(existing, fromTemplate) {
|
|
68
68
|
const out = []
|
|
@@ -83,9 +83,9 @@ export function mergeAllowList(existing, fromTemplate) {
|
|
|
83
83
|
* Зливає hooks-секцію: для кожної події в темплейті видаляємо managed-групи
|
|
84
84
|
* з існуючої конфігурації і додаємо актуальні з темплейту. Немені події в
|
|
85
85
|
* темплейті не чіпаються.
|
|
86
|
-
* @param {Record<string, HookGroup[]> | undefined} existing
|
|
87
|
-
* @param {Record<string, HookGroup[]> | undefined} fromTemplate
|
|
88
|
-
* @returns {Record<string, HookGroup[]>}
|
|
86
|
+
* @param {Record<string, HookGroup[]> | undefined} existing поточна `hooks`-секція з .claude/settings.json
|
|
87
|
+
* @param {Record<string, HookGroup[]> | undefined} fromTemplate цільова `hooks`-секція з темплейту
|
|
88
|
+
* @returns {Record<string, HookGroup[]>} результат злиття (порожні події видаляються)
|
|
89
89
|
*/
|
|
90
90
|
export function mergeHooks(existing, fromTemplate) {
|
|
91
91
|
/** @type {Record<string, HookGroup[]>} */
|
|
@@ -105,9 +105,9 @@ export function mergeHooks(existing, fromTemplate) {
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Повертає об'єднаний об'єкт settings.json.
|
|
108
|
-
* @param {ClaudeSettings | undefined} existing
|
|
109
|
-
* @param {ClaudeSettings} template
|
|
110
|
-
* @returns {ClaudeSettings}
|
|
108
|
+
* @param {ClaudeSettings | undefined} existing існуючий вміст `.claude/settings.json` користувача (або undefined, якщо файла нема)
|
|
109
|
+
* @param {ClaudeSettings} template settings із темплейту пакета `@nitra/cursor`
|
|
110
|
+
* @returns {ClaudeSettings} результат merge-у (користувацькі поля збережено, наші перевизначено)
|
|
111
111
|
*/
|
|
112
112
|
export function mergeSettings(existing, template) {
|
|
113
113
|
/** @type {ClaudeSettings} */
|
|
@@ -127,8 +127,8 @@ export function mergeSettings(existing, template) {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Читає JSON-файл; якщо файл відсутній або не валідний — повертає `undefined`.
|
|
130
|
-
* @param {string} path
|
|
131
|
-
* @returns {Promise<ClaudeSettings | undefined>}
|
|
130
|
+
* @param {string} path абсолютний шлях до JSON-файлу
|
|
131
|
+
* @returns {Promise<ClaudeSettings | undefined>} розпарсений об'єкт або `undefined` (файл відсутній / невалідний)
|
|
132
132
|
*/
|
|
133
133
|
async function readJsonOrUndefined(path) {
|
|
134
134
|
if (!existsSync(path)) {
|
|
@@ -146,7 +146,7 @@ async function readJsonOrUndefined(path) {
|
|
|
146
146
|
* користувацьких полів.
|
|
147
147
|
* @param {string} projectRoot корінь проєкту, куди писати
|
|
148
148
|
* @param {string} templateDir каталог `.claude-template/` усередині пакету
|
|
149
|
-
* @returns {Promise<{ written: boolean, path: string }>}
|
|
149
|
+
* @returns {Promise<{ written: boolean, path: string }>} результат: чи писали файл, та його відносний шлях
|
|
150
150
|
*/
|
|
151
151
|
export async function syncClaudeSettings(projectRoot, templateDir) {
|
|
152
152
|
const templatePath = join(templateDir, 'settings.template.json')
|
|
@@ -164,9 +164,9 @@ export async function syncClaudeSettings(projectRoot, templateDir) {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Копіює `npm/CLAUDE.md` з темплейту, якщо в проєкті є каталог `npm/`.
|
|
167
|
-
* @param {string} projectRoot
|
|
168
|
-
* @param {string} templateDir
|
|
169
|
-
* @returns {Promise<{ written: boolean, path: string }>}
|
|
167
|
+
* @param {string} projectRoot корінь проєкту, куди писати
|
|
168
|
+
* @param {string} templateDir каталог `.claude-template/` усередині пакету `@nitra/cursor`
|
|
169
|
+
* @returns {Promise<{ written: boolean, path: string }>} результат: чи писали файл, та його відносний шлях
|
|
170
170
|
*/
|
|
171
171
|
export async function syncNpmClaudeMd(projectRoot, templateDir) {
|
|
172
172
|
if (!existsSync(join(projectRoot, 'npm'))) {
|
|
@@ -185,8 +185,8 @@ export async function syncNpmClaudeMd(projectRoot, templateDir) {
|
|
|
185
185
|
* Копіює всі slash-команди з `templateDir/commands/` у `.claude/commands/`.
|
|
186
186
|
* Команди ідентифікуються тим, що вони лежать у темплейті — не перетинаються
|
|
187
187
|
* з командами скілів (n-fix, n-lint, ...).
|
|
188
|
-
* @param {string} projectRoot
|
|
189
|
-
* @param {string} templateDir
|
|
188
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
189
|
+
* @param {string} templateDir каталог `.claude-template/` усередині пакету `@nitra/cursor`
|
|
190
190
|
* @returns {Promise<string[]>} масив відносних шляхів записаних файлів
|
|
191
191
|
*/
|
|
192
192
|
export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
@@ -211,11 +211,11 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
211
211
|
/**
|
|
212
212
|
* Виконує повну синхронізацію Claude Code-конфігу з темплейту пакету в проєкт.
|
|
213
213
|
* Використовується з `bin/n-cursor.js` після інших синків.
|
|
214
|
-
* @param {object} options
|
|
214
|
+
* @param {object} options опції синку
|
|
215
215
|
* @param {string} options.projectRoot корінь проєкту-споживача
|
|
216
216
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
217
217
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
218
|
-
* @returns {Promise<{ settings: boolean, npmClaudeMd: boolean, commands: string[] }>}
|
|
218
|
+
* @returns {Promise<{ settings: boolean, npmClaudeMd: boolean, commands: string[] }>} прапорці записів settings/CLAUDE.md та список записаних slash-команд
|
|
219
219
|
*/
|
|
220
220
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled }) {
|
|
221
221
|
if (!enabled) {
|
|
@@ -118,7 +118,7 @@ export function parseProgramOrNull(content, virtualPath) {
|
|
|
118
118
|
* базовий `parseProgramOrNull` свідомо лишається без коментарів, щоб не змінювати API.
|
|
119
119
|
* @param {string} content вихідний код
|
|
120
120
|
* @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
|
|
121
|
-
* @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null}
|
|
121
|
+
* @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null} `program` + список коментарів, або `null` якщо парсер віддав помилки/exception
|
|
122
122
|
*/
|
|
123
123
|
export function parseProgramAndCommentsOrNull(content, virtualPath) {
|
|
124
124
|
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
@@ -25,7 +25,7 @@ function toAbsPosix(p) {
|
|
|
25
25
|
* Часткові збіги басенейму не враховуються (postgres-master-test ≠ postgres-master).
|
|
26
26
|
* @param {string} dirAbsPosix абсолютний posix-шлях каталогу
|
|
27
27
|
* @param {string[]} ignorePosix вже нормалізовані ignore-шляхи
|
|
28
|
-
* @returns {boolean}
|
|
28
|
+
* @returns {boolean} `true`, якщо шлях слід пропустити (точний збіг або префікс з `/`)
|
|
29
29
|
*/
|
|
30
30
|
function isIgnoredDir(dirAbsPosix, ignorePosix) {
|
|
31
31
|
for (const ig of ignorePosix) {
|
|
@@ -39,8 +39,8 @@ function isIgnoredDir(dirAbsPosix, ignorePosix) {
|
|
|
39
39
|
* Рекурсивно обходить каталог, пропускає типові артефакти збірки/залежностей та `ignorePaths`.
|
|
40
40
|
* @param {string} dir абсолютний шлях
|
|
41
41
|
* @param {(filePath: string) => void} onFile виклик для кожного файлу
|
|
42
|
-
* @param {string[]} [ignorePaths
|
|
43
|
-
* @returns {Promise<void>}
|
|
42
|
+
* @param {string[]} [ignorePaths] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
|
|
43
|
+
* @returns {Promise<void>} резолвиться по завершенню обходу
|
|
44
44
|
*/
|
|
45
45
|
export async function walkDir(dir, onFile, ignorePaths = []) {
|
|
46
46
|
const ignorePosix = ignorePaths.map(toAbsPosix)
|
|
@@ -49,10 +49,10 @@ export async function walkDir(dir, onFile, ignorePaths = []) {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Внутрішній рекурсор. ignorePosix вже нормалізовано — не нормалізуємо повторно на кожному рівні.
|
|
52
|
-
* @param {string} dir
|
|
53
|
-
* @param {(filePath: string) => void} onFile
|
|
54
|
-
* @param {string[]} ignorePosix
|
|
55
|
-
* @returns {Promise<void>}
|
|
52
|
+
* @param {string} dir абсолютний шлях каталогу для обходу
|
|
53
|
+
* @param {(filePath: string) => void} onFile колбек, що викликається для кожного звичайного файлу
|
|
54
|
+
* @param {string[]} ignorePosix вже нормалізовані абсолютні posix-шляхи ігнорованих каталогів
|
|
55
|
+
* @returns {Promise<void>} резолвиться по завершенню рекурсії
|
|
56
56
|
*/
|
|
57
57
|
async function walkDirInner(dir, onFile, ignorePosix) {
|
|
58
58
|
if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(dir), ignorePosix)) return
|