@nitra/cursor 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.18.0] - 2026-06-03
4
+
5
+ ### Added
6
+
7
+ - docgen: трирівнева генерація md-доки (CLI `docgen scan|modules` + скіл); скіл-специфічні скрипти тепер живуть у npm/skills/<id>/js/, синк копіює лише top-level SKILL.md (підкаталоги js/ пропускаються)
8
+
9
+ ## [3.17.0] - 2026-06-02
10
+
11
+ ### Added
12
+
13
+ - skill changes (auto: завжди, worktree: true) — покласти change-файл у кожен зачеплений workspace через n-cursor change перед фінішем
14
+
3
15
  ## [3.16.0] - 2026-06-02
4
16
 
5
17
  ### Added
package/bin/n-cursor.js CHANGED
@@ -24,6 +24,8 @@
24
24
  * `npx \@nitra/cursor lint-docker` — канонічний lint-docker (docker.mdc): `hadolint` по `Dockerfile`/`*.Dockerfile`
25
25
  * `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
26
26
  * `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
27
+ * `npx \@nitra/cursor docgen scan` — детермінований JSON-лістинг кодових файлів для скілу docgen (тека `docs/` поряд із джерелом)
28
+ * `npx \@nitra/cursor docgen modules` — детермінований JSON-лістинг логічних модулів (межі за `package.json`) для Tier 2 скілу docgen
27
29
  * `npx \@nitra/cursor skill list` — скіли пакета без синку в проєкт
28
30
  * `npx \@nitra/cursor skill taze` — промпт на stdout
29
31
  * `npx \@nitra/cursor skill cursor taze ["task"]` — Cursor CLI (`cursor-agent -p`)
@@ -783,14 +785,17 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
783
785
  await mkdir(destDir, { recursive: true })
784
786
  const meta = readSkillMetaRaw(srcDir)
785
787
  const worktree = meta?.worktree === true
786
- const files = await readdir(srcDir)
787
- for (const file of files) {
788
- if (file === 'meta.json') continue
789
- let content = await readFile(join(srcDir, file), 'utf8')
790
- if (file === 'SKILL.md') {
788
+ const entries = await readdir(srcDir, { withFileTypes: true })
789
+ for (const entry of entries) {
790
+ // Лише top-level файли скіла. `meta.json` — метадані (не для споживача);
791
+ // підкаталоги (`js/` скіл-специфічний код) виконуються з пакета через
792
+ // `npx`, у проєкт не копіюються (як `npm/rules/<id>/js/`). Див. scripts.mdc.
793
+ if (!entry.isFile() || entry.name === 'meta.json') continue
794
+ let content = await readFile(join(srcDir, entry.name), 'utf8')
795
+ if (entry.name === 'SKILL.md') {
791
796
  content = injectWorktreeNotice(content, worktree)
792
797
  }
793
- await writeFile(join(destDir, file), content, 'utf8')
798
+ await writeFile(join(destDir, entry.name), content, 'utf8')
794
799
  }
795
800
  console.log(`✅`)
796
801
  success++
@@ -1580,6 +1585,22 @@ try {
1580
1585
 
1581
1586
  break
1582
1587
  }
1588
+ case 'docgen': {
1589
+ // n-cursor docgen scan|modules — детермінований лістинг для скілу docgen.
1590
+ // scan — кодові файли; modules — логічні модулі (межі за package.json).
1591
+ // Друкує JSON; генерацію доки робить скіл, диспатчачи субагентів.
1592
+ const { runDocgenScanCli, runDocgenModulesCli } = await import('../skills/docgen/js/docgen-scan.mjs')
1593
+ if (args[0] === 'scan') {
1594
+ process.exitCode = await runDocgenScanCli(args.slice(1))
1595
+ } else if (args[0] === 'modules') {
1596
+ process.exitCode = await runDocgenModulesCli(args.slice(1))
1597
+ } else {
1598
+ console.error('Usage: npx @nitra/cursor docgen <scan|modules> [--root <dir>]')
1599
+ process.exitCode = 1
1600
+ }
1601
+
1602
+ break
1603
+ }
1583
1604
  case undefined:
1584
1605
  case '': {
1585
1606
  await runSync()
@@ -1589,7 +1610,7 @@ try {
1589
1610
  default: {
1590
1611
  console.error(`❌ Невідома команда: ${command}`)
1591
1612
  console.error(
1592
- ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci, flow, trace, graph`
1613
+ ` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci, flow, trace, graph, docgen`
1593
1614
  )
1594
1615
  process.exitCode = 1
1595
1616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.16.0",
3
+ "version": "3.18.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: docgen
3
+ description: >-
4
+ Обходить проєкт і для кожного кодового файлу (js/mjs/ts/vue/py) пише вичерпну українську md-документацію у теку docs/ поряд із кодом — диспатчить окремого субагента на кожен файл, за правилами adr/ci4
5
+ ---
6
+
7
+ # docgen — генерація документації по файлах
8
+
9
+ ## Мета
10
+
11
+ Для кожного кодового файлу проєкту створити вичерпну `.md`-документацію у теці `docs/`
12
+ **поряд із самим файлом** (`<dir>/docs/<stem>.md`). Документацію пише **окремий субагент**
13
+ на кожен файл — не один прохід, а батч-диспатч. Джерело правди стилю — правила `adr` і
14
+ `ci4` (`docs/explanation`/`docs/adr`-каталоги з тих правил **не застосовуємо** — доку
15
+ кладемо локально поряд із кодом).
16
+
17
+ Документація — **трирівнева**, рівні виконуються строго послідовно:
18
+
19
+ 1. **Tier 1 — файли**: `<dir>/docs/<stem>.md`, субагент на файл (нижче).
20
+ 2. **Tier 2 — module-summary**: `<module_root>/docs/ARCHITECTURE.md`, субагент на модуль.
21
+ 3. **Tier 3 — доменні доки**: `docs/<домен>.md` у кореневій `docs/`, субагент-синтезатор
22
+ виділяє бізнес-домени й пише файл на кожен домен.
23
+
24
+ Агрегат ніколи не випереджає джерело: Tier 2 — лише після завершення всього Tier 1,
25
+ Tier 3 — лише після завершення всього Tier 2.
26
+
27
+ ## ⚠️ Паралелізм
28
+
29
+ Диспатч субагентів — **батчами по 5 одночасно**. Не запускати весь список одразу
30
+ (перевантаження). Кожен субагент пише свій окремий файл — спільного стану немає, гонок
31
+ за файли немає.
32
+
33
+ Рівні строго послідовні: Tier 2 стартує лише після завершення всього Tier 1, Tier 3 —
34
+ лише після всього Tier 2. Усередині Tier 1 і Tier 2 — батчі по 5. Tier 3 — один субагент.
35
+
36
+ ## Передумова
37
+
38
+ - Поточна директорія — корінь проєкту, який документуємо.
39
+ - Доступний `npx @nitra/cursor` (пакет `@nitra/cursor` встановлено або через npx).
40
+
41
+ ## Workflow
42
+
43
+ ### Крок 1: Зібрати список файлів
44
+
45
+ ```bash
46
+ npx @nitra/cursor docgen scan
47
+ ```
48
+
49
+ Команда друкує JSON-масив об'єктів:
50
+
51
+ ```json
52
+ [
53
+ { "sourcePath": "/abs/src/lib/foo.js", "relSource": "src/lib/foo.js",
54
+ "docPath": "/abs/src/lib/docs/foo.md", "exists": false }
55
+ ]
56
+ ```
57
+
58
+ Розпарси JSON.
59
+
60
+ ### Крок 2: Відфільтрувати вже описані
61
+
62
+ За замовчуванням **пропусти** елементи з `"exists": true`. Перегенеровуй їх лише якщо
63
+ користувач явно попросив `--overwrite` (тоді обробляй усі). `--overwrite` — **не** прапор
64
+ `docgen scan`: scanner лише лістить файли, а рішення «пропустити чи перегенерувати» приймаєш
65
+ ти тут, фільтруючи за полем `exists`.
66
+
67
+ Якщо після фільтра список порожній — зупинись:
68
+
69
+ ```
70
+ ✓ Усі кодові файли вже мають документацію. Нічого робити.
71
+ ```
72
+
73
+ Запам'ятай `total = довжина відфільтрованого списку`.
74
+
75
+ ### Крок 3: Диспатч субагентів батчами по 5
76
+
77
+ Розбий список на батчі по 5 елементів. Для кожного батчу запусти **до 5 субагентів
78
+ одночасно (в одному повідомленні)**, дочекайся завершення батчу, переходь до наступного.
79
+
80
+ Промпт кожного субагента (підстав `sourcePath` і `docPath`):
81
+
82
+ ```
83
+ Напиши вичерпну технічну документацію для одного файлу коду.
84
+
85
+ ФАЙЛ-ДЖЕРЕЛО: <sourcePath>
86
+ ЗАПИСАТИ В: <docPath>
87
+
88
+ Кроки:
89
+ 1. Прочитай файл <sourcePath> повністю.
90
+ 2. Створи теку для <docPath>, якщо її немає.
91
+ 3. Запиши markdown-документ у <docPath> за правилами нижче.
92
+
93
+ Правила документа (за adr/ci4):
94
+ - Мова — УКРАЇНСЬКА для всього тексту (заголовки, абзаци, таблиці). Code identifiers,
95
+ шляхи, імена API, команди — лишай як у коді (зазвичай ASCII).
96
+ - ЧИСТИЙ Markdown. Жодних HTML-обгорток (<div>/<span>/класів) — токен-ефективність.
97
+ - Контекстна незалежність: кожна секція самодостатня. Явно повторюй імена сутностей
98
+ («функція `calculateTotal()`», «модуль `src/lib/foo.js`»). Заборонено «як вище»,
99
+ «ця функція», «той сервіс».
100
+ - ВИЧЕРПНІСТЬ — опиши всю логіку файлу. Секції:
101
+ ## Огляд — призначення файлу, його роль, що дає.
102
+ ## Експорти / API — кожен експорт (функція/клас/компонент/константа).
103
+ ## Функції — для кожної: сигнатура, параметри (типи), що повертає, side effects.
104
+ ## Залежності — імпорти й зовнішні залежності та навіщо вони.
105
+ ## Потік виконання / Використання — як цей файл використовують; ключові гілки логіки.
106
+ - Для .vue додай опис props, emits, slots, реактивного стану.
107
+ - НЕ вигадуй деталей, яких немає в коді.
108
+ - Мета — Rebuild Test: за цим документом можна відтворити логіку файлу.
109
+
110
+ Поверни лише підтвердження, що файл <docPath> записано.
111
+ ```
112
+
113
+ ### Крок 4: Tier 2 — module-summary
114
+
115
+ Після завершення **всіх** батчів Tier 1 зібрати список модулів:
116
+
117
+ ```bash
118
+ npx @nitra/cursor docgen modules
119
+ ```
120
+
121
+ Команда друкує JSON-масив:
122
+
123
+ ```json
124
+ [
125
+ { "moduleRoot": "/abs/npm/rules/adr", "relRoot": "npm/rules/adr",
126
+ "slug": "npm-rules-adr", "docPath": "/abs/npm/rules/adr/docs/ARCHITECTURE.md",
127
+ "members": ["npm/rules/adr/index.mjs"], "exists": false }
128
+ ]
129
+ ```
130
+
131
+ module-summary **завжди регенерується** (це агрегат — поле `exists` ігноруй). Розбий модулі на батчі по 5 і диспатч субагентів. Промпт кожного (підстав `relRoot`, `docPath`, `members`):
132
+
133
+ ```
134
+ Напиши module-summary для одного логічного модуля.
135
+
136
+ МОДУЛЬ: <relRoot>
137
+ ЗАПИСАТИ В: <docPath>
138
+ ФАЙЛИ МОДУЛЯ (members): <members>
139
+
140
+ Кроки:
141
+ 1. Прочитай файлові доки членів модуля. <member> — relSource відносно кореня проєкту
142
+ (= поточний CWD); його файлова дока — <CWD>/<dir>/docs/<stem>.md. За потреби зазирни
143
+ в самі файли.
144
+ 2. Створи теку для <docPath>, якщо її немає.
145
+ 3. Запиши markdown у <docPath> за тими ж правилами стилю, що й файлова дока
146
+ (українська, чистий Markdown, контекстна незалежність, без HTML).
147
+
148
+ Секції module-summary:
149
+ ## Огляд модуля — призначення модуля <relRoot>, його роль у проєкті.
150
+ ## Ключові файли — список із кліковими посиланнями (відносними до розташування цього
151
+ ARCHITECTURE.md) на члени модуля та їхні файлові доки.
152
+ ## Публічний API — що модуль експортує назовні.
153
+ ## Внутрішній потік — як компоненти модуля взаємодіють.
154
+ ## Підмодулі — вкладені модулі, якщо є.
155
+
156
+ Поверни лише підтвердження, що файл <docPath> записано.
157
+ ```
158
+
159
+ ### Крок 5: Tier 3 — доменні доки
160
+
161
+ Після завершення **всіх** module-summary диспатч **одного** субагента-синтезатора.
162
+ У промпт підстав конкретний перелік шляхів module-summary (<module_root>/docs/ARCHITECTURE.md
163
+ кожного модуля з виводу `docgen modules`), а не інструкцію їх шукати. Промпт:
164
+
165
+ ```
166
+ Синтезуй доменну документацію бізнес-процесів проєкту.
167
+
168
+ ДЖЕРЕЛА (module-summary, читай усі): <перелік шляхів ARCHITECTURE.md, підставлений вище>
169
+
170
+ Кроки:
171
+ 1. Прочитай усі module-summary.
172
+ 2. Виділи бізнес-домени та процеси (можуть перетинати межі модулів). Доменів може бути багато.
173
+ 3. Для КОЖНОГО домену запиши окремий файл docs/<домен>.md у кореневій docs/:
174
+ - назва файлу — короткий kebab-slug домену;
175
+ - не перезаписуй файлові доки кореневих файлів у docs/ (напр. app.md, eslint.config.md):
176
+ якщо слаґ домену збігається з іменем такого файлу — додай суфікс -domain
177
+ (напр. app-domain.md). Інакше пиши docs/<домен>.md як є;
178
+ - опиши бізнес-процес домену з кліковими відносними посиланнями на module-summary, конкретні файли й директорії.
179
+
180
+ Правила стилю — ті ж (українська, чистий Markdown, контекстна незалежність, без HTML).
181
+
182
+ Поверни перелік створених файлів docs/<домен>.md.
183
+ ```
184
+
185
+ ### Крок 6: Підсумок
186
+
187
+ Після всіх батчів виведи:
188
+
189
+ ```
190
+ ✓ docgen завершено.
191
+ Tier 1 (файли): описано <N>, пропущено <S>, помилок <E>.
192
+ Tier 2 (модулі): <M> module-summary.
193
+ Tier 3 (домени): <D> доменних доків у docs/.
194
+ ```
195
+
196
+ Перелічи файли з помилками (субагент впав або не записав `docPath`), якщо такі є.
197
+ Помилка одного файлу не зупиняє решту — обробляй усі батчі до кінця.
198
+
199
+ ## Нотатки
200
+
201
+ - Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
202
+ - Scanner ігнорує `node_modules`, `dist`, `.git`, `__pycache__`, `coverage`, `.cursor`,
203
+ `.claude`, усі теки `docs/`, а також `*.test.*` / `*.spec.*` / `*.d.ts`.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * docgen scanner — детермінований обхід проєкту для скілу `docgen`.
3
+ *
4
+ * Друкує JSON-список кодових файлів із обчисленим `docPath` (тека `docs/` поряд із
5
+ * джерелом). Рішення про overwrite/skip приймає скіл — scanner лише лістить і ставить
6
+ * прапор `exists`. LLM/мережі тут немає: уся генерація доки — у субагентах скілу.
7
+ */
8
+ // eslint-disable-next-line unicorn/import-style
9
+ import path from 'node:path'
10
+ import { existsSync, readdirSync, statSync } from 'node:fs'
11
+
12
+ import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
13
+
14
+ /** Кодові розширення, для яких генеруємо документацію. */
15
+ const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.vue', '.py'])
16
+
17
+ /** Теки, які scanner ніколи не заходить (включно з самими `docs/`). */
18
+ const IGNORED_DIRS = new Set([
19
+ 'node_modules', 'dist', '.git', '__pycache__', 'coverage', '.cursor', '.claude', 'docs'
20
+ ])
21
+
22
+ /** `*.test.*`, `*.spec.*` — тести, документувати не треба. */
23
+ const TEST_FILE_RE = /\.(?:test|spec)\.[^.]+$/u
24
+
25
+ /**
26
+ * Чи є файл кодовим джерелом для документування.
27
+ * @param {string} fileName базове ім'я файлу
28
+ * @returns {boolean} true — документуємо; false — пропускаємо
29
+ */
30
+ export function isSourceFile(fileName) {
31
+ if (fileName.endsWith('.d.ts')) return false
32
+ if (TEST_FILE_RE.test(fileName)) return false
33
+ return SOURCE_EXTENSIONS.has(path.extname(fileName))
34
+ }
35
+
36
+ /**
37
+ * Обчислює шлях md-документа для кодового файлу: тека `docs/` поряд із джерелом.
38
+ * @param {string} sourcePath шлях до джерела (відносний або абсолютний)
39
+ * @returns {string} шлях до `<dir>/docs/<stem>.md`
40
+ */
41
+ export function docPathForSource(sourcePath) {
42
+ const dir = path.dirname(sourcePath)
43
+ const stem = path.basename(sourcePath, path.extname(sourcePath))
44
+ return path.join(dir, 'docs', `${stem}.md`)
45
+ }
46
+
47
+ /**
48
+ * Рекурсивно обходить дерево від `root`, повертає кодові файли для документування.
49
+ * Синхронний `readdirSync` — детермінований порядок і простий рекурсивний обхід без
50
+ * гонок; обсяг дерева проєкту це дозволяє.
51
+ * @param {string} root абсолютний корінь обходу
52
+ * @returns {Array<{sourcePath:string, relSource:string, docPath:string, exists:boolean}>} список кандидатів
53
+ */
54
+ export function scanForDocgen(root) {
55
+ const results = []
56
+
57
+ /**
58
+ * @param {string} dir поточний каталог обходу
59
+ */
60
+ function walk(dir) {
61
+ let entries
62
+ try {
63
+ entries = readdirSync(dir, { withFileTypes: true })
64
+ } catch {
65
+ return
66
+ }
67
+ for (const entry of entries) {
68
+ const fullPath = path.join(dir, entry.name)
69
+ if (entry.isDirectory()) {
70
+ if (IGNORED_DIRS.has(entry.name)) continue
71
+ walk(fullPath)
72
+ } else if (entry.isFile() && isSourceFile(entry.name)) {
73
+ const docPath = docPathForSource(fullPath)
74
+ results.push({
75
+ sourcePath: fullPath,
76
+ relSource: path.relative(root, fullPath),
77
+ docPath,
78
+ exists: existsSync(docPath)
79
+ })
80
+ }
81
+ }
82
+ }
83
+
84
+ walk(root)
85
+ return results
86
+ }
87
+
88
+ /**
89
+ * Стабільний slug модуля з його відносного шляху (для лейблів/логів).
90
+ * @param {string} root абсолютний корінь обходу
91
+ * @param {string} moduleRoot абсолютний корінь модуля
92
+ * @returns {string} slug: `npm/rules/adr` → `npm-rules-adr`, корінь → `root`
93
+ */
94
+ export function slugForModule(root, moduleRoot) {
95
+ const rel = path.relative(root, moduleRoot)
96
+ // корінь репо: фіксований sentinel 'root'
97
+ if (rel === '') return 'root'
98
+ return rel.split(path.sep).join('-').replaceAll(/[^\w-]+/gu, '-')
99
+ }
100
+
101
+ /**
102
+ * Знаходить корені модулів — теки з `package.json` (корінь завжди модуль).
103
+ * Ті ж IGNORED_DIRS, тож `package.json` у node_modules тощо не враховується.
104
+ * @param {string} root абсолютний корінь обходу
105
+ * @returns {string[]} абсолютні шляхи коренів модулів
106
+ */
107
+ export function findModuleRoots(root) {
108
+ const roots = [root]
109
+
110
+ /** @param {string} dir поточний каталог обходу */
111
+ function walk(dir) {
112
+ let entries
113
+ try {
114
+ entries = readdirSync(dir, { withFileTypes: true })
115
+ } catch {
116
+ return
117
+ }
118
+ for (const entry of entries) {
119
+ const fullPath = path.join(dir, entry.name)
120
+ if (entry.isDirectory()) {
121
+ if (IGNORED_DIRS.has(entry.name)) continue
122
+ walk(fullPath)
123
+ } else if (entry.isFile() && entry.name === 'package.json' && dir !== root) {
124
+ roots.push(dir)
125
+ }
126
+ }
127
+ }
128
+
129
+ walk(root)
130
+ return roots
131
+ }
132
+
133
+ /**
134
+ * Найближчий модуль-предок для файлу (найдовший збіг шляху).
135
+ * @param {string} filePath абсолютний шлях до файлу
136
+ * @param {string[]} moduleRoots абсолютні корені модулів
137
+ * @returns {string|null} абсолютний корінь модуля або null
138
+ */
139
+ export function nearestModuleRoot(filePath, moduleRoots) {
140
+ let best = null
141
+ for (const moduleRoot of moduleRoots) {
142
+ const rel = path.relative(moduleRoot, filePath)
143
+ if (rel.startsWith('..') || path.isAbsolute(rel)) continue
144
+ if (best === null || moduleRoot.length > best.length) best = moduleRoot
145
+ }
146
+ return best
147
+ }
148
+
149
+ /**
150
+ * Лістить логічні модулі проєкту з членами-файлами і docPath module-summary.
151
+ * Модулі без кодових файлів пропускаються.
152
+ * @param {string} root абсолютний корінь обходу
153
+ * @returns {Array<{moduleRoot:string, relRoot:string, slug:string, docPath:string, members:string[], exists:boolean}>} модулі (members — relSource-и, відносні від root)
154
+ */
155
+ export function scanForModules(root) {
156
+ const files = scanForDocgen(root)
157
+ const moduleRoots = findModuleRoots(root)
158
+ const byRoot = new Map()
159
+ for (const file of files) {
160
+ const moduleRoot = nearestModuleRoot(file.sourcePath, moduleRoots)
161
+ if (moduleRoot === null) continue
162
+ if (!byRoot.has(moduleRoot)) byRoot.set(moduleRoot, [])
163
+ byRoot.get(moduleRoot).push(file.relSource)
164
+ }
165
+
166
+ const results = []
167
+ for (const moduleRoot of moduleRoots) {
168
+ const members = byRoot.get(moduleRoot)
169
+ if (!members || members.length === 0) continue
170
+ const docPath = path.join(moduleRoot, 'docs', 'ARCHITECTURE.md')
171
+ results.push({
172
+ moduleRoot,
173
+ relRoot: path.relative(root, moduleRoot) || '.',
174
+ slug: slugForModule(root, moduleRoot),
175
+ docPath,
176
+ members: members.toSorted(),
177
+ exists: existsSync(docPath)
178
+ })
179
+ }
180
+ return results
181
+ }
182
+
183
+ /**
184
+ * Парсить `--root <dir>` з argv; default — cwd.
185
+ * @param {string[]} argv аргументи після підкоманди
186
+ * @returns {string} абсолютний корінь
187
+ */
188
+ export function resolveRoot(argv) {
189
+ const i = argv.indexOf('--root')
190
+ return i !== -1 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd()
191
+ }
192
+
193
+ /**
194
+ * Парсить `--root <dir>` (default — cwd), сканує і друкує JSON-масив у stdout.
195
+ * @param {string[]} argv аргументи після назви субкоманди (наприклад ['--root', '<dir>'])
196
+ * @returns {Promise<number>} exit-код: 0 — успіх, 1 — корінь не існує
197
+ */
198
+ export async function runDocgenScanCli(argv) {
199
+ const root = resolveRoot(argv)
200
+
201
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
202
+ console.error(`docgen scan: корінь не існує або не є директорією: ${root}`)
203
+ return 1
204
+ }
205
+
206
+ const items = await scanForDocgen(root)
207
+ console.log(JSON.stringify(items, null, 2))
208
+ return 0
209
+ }
210
+
211
+ /**
212
+ * Парсить `--root`, сканує модулі і друкує JSON-масив у stdout.
213
+ * @param {string[]} argv аргументи після назви субкоманди (наприклад ['--root', '<dir>'])
214
+ * @returns {Promise<number>} exit-код: 0 — успіх, 1 — корінь не існує
215
+ */
216
+ export async function runDocgenModulesCli(argv) {
217
+ const root = resolveRoot(argv)
218
+
219
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
220
+ console.error(`docgen modules: корінь не існує або не є директорією: ${root}`)
221
+ return 1
222
+ }
223
+
224
+ const items = await scanForModules(root)
225
+ console.log(JSON.stringify(items, null, 2))
226
+ return 0
227
+ }
228
+
229
+ if (isRunAsCli(import.meta.url)) {
230
+ // Прямий запуск: `node skills/docgen/js/docgen-scan.mjs --root <dir>`
231
+ process.exitCode = await runDocgenScanCli(process.argv.slice(2))
232
+ }
@@ -0,0 +1 @@
1
+ { "worktree": true }
@@ -1 +1 @@
1
- { "auto": "завжди", "worktree": false }
1
+ { "auto": "завжди", "worktree": true }