@nitra/cursor 1.8.179 → 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 CHANGED
@@ -4,6 +4,13 @@
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
+
7
14
  ## [1.8.179] - 2026-05-05
8
15
 
9
16
  ### Changed
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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.179",
3
+ "version": "1.8.180",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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) {
@@ -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
@@ -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
- return new Promise(resolve => {
21
- if (process.stdin.isTTY) {
22
- resolve('')
23
- return
24
- }
25
- let data = ''
26
- process.stdin.setEncoding('utf8')
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
- return new Promise(resolve => {
64
- const child = spawn('npx', ['--no', '@nitra/cursor', 'check'], { stdio: 'inherit' })
65
- child.on('exit', code => resolve(code ?? 1))
66
- child.on('error', err => {
67
- process.stderr.write(`stop-hook: не вдалося запустити npx @nitra/cursor check — ${err.message}\n`)
68
- resolve(1)
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=[]] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
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