@nitra/cursor 3.17.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 +6 -0
- package/bin/n-cursor.js +28 -7
- package/package.json +1 -1
- package/skills/docgen/SKILL.md +203 -0
- package/skills/docgen/js/docgen-scan.mjs +232 -0
- package/skills/docgen/meta.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## [3.17.0] - 2026-06-02
|
|
4
10
|
|
|
5
11
|
### 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
|
|
787
|
-
for (const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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,
|
|
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
|
@@ -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 }
|