@alfacapital-tech/c4builder 0.9.0 → 0.10.0-rc.2
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/README.MD +3 -2
- package/dist/cli/commands/new.js +3 -0
- package/dist/cli/commands/site.js +11 -4
- package/dist/cli/commands/skills.js +80 -0
- package/dist/cli/dispatch.js +55 -6
- package/dist/cli/update.js +1 -1
- package/dist/core/build.js +3 -7
- package/dist/index.js +9 -1
- package/dist/util/utils.js +9 -3
- package/package.json +2 -1
- package/skills/c4builder/SKILL.md +199 -0
- package/skills/c4builder/references/bpmn.md +138 -0
- package/skills/c4builder/references/config.md +111 -0
- package/skills/c4builder/references/diagrams.md +77 -0
- package/skills/c4builder/references/mermaid.md +78 -0
- package/skills/c4builder/references/plugins.md +68 -0
- package/skills/c4builder-review/SKILL.md +97 -0
- package/skills/c4builder-setup/SKILL.md +130 -0
- package/template/AGENTS.md +13 -2
package/README.MD
CHANGED
|
@@ -60,8 +60,9 @@ c4builder check src/context.puml # exit code 0 or 1, "✗ file: line N: messag
|
|
|
60
60
|
c4builder --site -w # rebuilds and reloads the browser on every save
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
For agents: `
|
|
64
|
-
`"generateLLMS": true` adds `llms.txt` /
|
|
63
|
+
For agents: `c4builder skills` installs the agent skills of the installed version into the project
|
|
64
|
+
(`c4builder --new` does it for a new one), and `"generateLLMS": true` adds `llms.txt` /
|
|
65
|
+
`llms-full.txt` (diagram sources included) to the site.
|
|
65
66
|
|
|
66
67
|
Or with Docker (Node, JRE, PlantUML, fonts and D2 included):
|
|
67
68
|
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -7,6 +7,7 @@ import Configstore from 'configstore';
|
|
|
7
7
|
import { readFile, writeFile, makeDirectory } from "../../util/utils.js";
|
|
8
8
|
import { defaultConfig } from "../../config/defaults.js";
|
|
9
9
|
import { TEMPLATE_DIR } from "../../util/paths.js";
|
|
10
|
+
import cmdSkills from "./skills.js";
|
|
10
11
|
// Общая проверка имени проекта: возвращает текст ошибки или null (валидно).
|
|
11
12
|
// Интерактив показывает её и переспрашивает; --yes падает с ней (exit≠0), без ре-промпта.
|
|
12
13
|
const validateProjectName = (name) => {
|
|
@@ -55,6 +56,8 @@ export default async (opts = {}) => {
|
|
|
55
56
|
}
|
|
56
57
|
await makeDirectory(projectName);
|
|
57
58
|
await generateTemplate(TEMPLATE_DIR, projectName);
|
|
59
|
+
// Скиллы рядом с src/, как AGENTS.md: сборка их не видит (change skills-install, D6).
|
|
60
|
+
cmdSkills(path.join(process.cwd(), projectName));
|
|
58
61
|
const conf = new Configstore(path.join(process.cwd(), projectName).split(path.sep).splice(1).join('_'), {}, { configPath: path.join(process.cwd(), projectName, '.c4builder') });
|
|
59
62
|
// --yes: полный конфиг из единого defaultConfig → последующий c4builder собирает
|
|
60
63
|
// без wizard'а. Интерактив пишет только projectName (первая сборка ведёт через wizard).
|
|
@@ -100,7 +100,7 @@ pages = () => new Map()) => {
|
|
|
100
100
|
const distFolder = path.resolve(currentConfiguration.DIST_FOLDER);
|
|
101
101
|
const liveReloadEnabled = !!(program.watch && reloadEmitter);
|
|
102
102
|
const agent = { ...(program.agent ?? { on: false, name: '', explicit: false }) };
|
|
103
|
-
// Кеш index.html — отдаём его из памяти, если на диске его временно нет
|
|
103
|
+
// Кеш корневого index.html — отдаём его из памяти, если на диске его временно нет
|
|
104
104
|
// (в окне build.js → emptyDir(DIST_FOLDER) перед записью).
|
|
105
105
|
let lastIndexHtml = null;
|
|
106
106
|
let sweeper;
|
|
@@ -421,6 +421,9 @@ pages = () => new Map()) => {
|
|
|
421
421
|
}
|
|
422
422
|
let filePath;
|
|
423
423
|
const isIndex = urlPath.endsWith('/');
|
|
424
|
+
// docsify — один корневой index.html с hash-роутингом. Фолбэк окна ребилда только
|
|
425
|
+
// для корня: иначе любой несуществующий `…/` отвечал бы 200 главной, а не 404.
|
|
426
|
+
const isRoot = urlPath === '/';
|
|
424
427
|
if (isIndex) {
|
|
425
428
|
filePath = path.join(distFolder, urlPath, 'index.html');
|
|
426
429
|
}
|
|
@@ -436,14 +439,14 @@ pages = () => new Map()) => {
|
|
|
436
439
|
fs.readFile(resolved, 'utf8', (err, data) => {
|
|
437
440
|
if (err) {
|
|
438
441
|
// ENOENT в окне ребилда (emptyDir → запись) — отдаём последний известный index.html.
|
|
439
|
-
if (
|
|
442
|
+
if (isRoot && lastIndexHtml) {
|
|
440
443
|
res.set('Cache-Control', 'no-store').type('html').send(lastIndexHtml);
|
|
441
444
|
return;
|
|
442
445
|
}
|
|
443
446
|
return next();
|
|
444
447
|
}
|
|
445
448
|
const page = inject(data);
|
|
446
|
-
if (
|
|
449
|
+
if (isRoot)
|
|
447
450
|
lastIndexHtml = page;
|
|
448
451
|
res.set('Cache-Control', 'no-store').type('html').send(page);
|
|
449
452
|
});
|
|
@@ -453,7 +456,11 @@ pages = () => new Map()) => {
|
|
|
453
456
|
// статику через app.use (тот же эффект: fallthrough-обработчик всех GET).
|
|
454
457
|
app.use(express.static(distFolder));
|
|
455
458
|
return new Promise((resolve, reject) => {
|
|
456
|
-
|
|
459
|
+
// Express 5 зовёт колбэк listen и с ошибкой (занятый порт) — её разбирает
|
|
460
|
+
// server.on('error') ниже, а «serving» печатаем только после настоящего listen.
|
|
461
|
+
const server = app.listen(port, '127.0.0.1', (err) => {
|
|
462
|
+
if (err)
|
|
463
|
+
return;
|
|
457
464
|
const url = `http://localhost:${port}`;
|
|
458
465
|
console.log('serving your docsify site');
|
|
459
466
|
console.log(`go to ${chalk.green(url)}`);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { detectAgent } from "../agent.js";
|
|
5
|
+
import { compareVersions, disabled } from "../update.js";
|
|
6
|
+
import { PACKAGE_ROOT, packageJson as pkg } from "../../util/paths.js";
|
|
7
|
+
// Скиллы агентов (change skills-install): входят в пакет, поэтому ставятся офлайн и той же версии,
|
|
8
|
+
// что и CLI. По основному скиллу судим обо всём наборе — ставятся все сразу (D3).
|
|
9
|
+
const SKILLS_DIR = path.join(PACKAGE_ROOT, 'skills');
|
|
10
|
+
const MAIN = 'c4builder';
|
|
11
|
+
const RUN = 'c4builder skills';
|
|
12
|
+
// Отметка версии из frontmatter `<root>/c4builder/SKILL.md` регуляркой, без YAML-парсера (D4):
|
|
13
|
+
// undefined — скилла нет, '' — скилл без отметки (поставлен `npx skills add` или руками).
|
|
14
|
+
const stampIn = (root) => {
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
text = fs.readFileSync(path.join(root, MAIN, 'SKILL.md'), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const front = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
|
|
23
|
+
return front.match(/^\s+generatedBy:\s*["']?([^"'\s]+)/m)?.[1] ?? '';
|
|
24
|
+
};
|
|
25
|
+
// Копия новее CLI не устаревшая: о старом CLI скажет уведомление о новой версии (D4).
|
|
26
|
+
const outdated = (stamp) => !stamp || compareVersions(stamp, pkg.version) < 0;
|
|
27
|
+
const personalRoot = (dir) => path.join(os.homedir(), dir, 'skills');
|
|
28
|
+
const overrides = (dir, stamp) => `~/${dir}/skills/${MAIN} (${stamp || 'no version'}) overrides project skills. Ask the user to remove it`;
|
|
29
|
+
// `c4builder skills` и `--new` (D2, D6): каталог каждого скилла пакета заменяется целиком — так уходят
|
|
30
|
+
// файлы прошлых версий, а чужие скиллы рядом не трогаются. `.claude` — только под Claude Code или если
|
|
31
|
+
// он уже есть: Claude Code не читает `.agents`, а создавать `.claude/` в чужом проекте незачем.
|
|
32
|
+
export default (root = process.cwd()) => {
|
|
33
|
+
const names = fs
|
|
34
|
+
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
35
|
+
.filter((d) => d.isDirectory())
|
|
36
|
+
.map((d) => d.name);
|
|
37
|
+
const dirs = ['.agents'];
|
|
38
|
+
if (process.env.CLAUDECODE || fs.existsSync(path.join(root, '.claude')))
|
|
39
|
+
dirs.push('.claude');
|
|
40
|
+
for (const dir of dirs) {
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const target = path.join(root, dir, 'skills', name);
|
|
43
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
44
|
+
fs.cpSync(path.join(SKILLS_DIR, name), target, { recursive: true });
|
|
45
|
+
const file = path.join(target, 'SKILL.md');
|
|
46
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
47
|
+
fs.writeFileSync(file, text.replace(/^description:.*$/m, `$&\nmetadata:\n generatedBy: "${pkg.version}"`));
|
|
48
|
+
console.log(`installed ${path.relative(process.cwd(), target)}`);
|
|
49
|
+
}
|
|
50
|
+
// Личную копию не трогаем: её поставил пользователь, удалять — ему (D3).
|
|
51
|
+
const personal = stampIn(personalRoot(dir));
|
|
52
|
+
if (personal !== undefined && outdated(personal))
|
|
53
|
+
console.error(overrides(dir, personal));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// Строка о скиллах при старте сборки и `site` под агентом (D3–D5), undefined — молчим. Агент — только
|
|
57
|
+
// по окружению: флаги --agent/--no-agent управляют замечаниями со страницы, а не этим.
|
|
58
|
+
export const skillsNotice = (cwd = process.cwd()) => {
|
|
59
|
+
const agent = detectAgent(process.env, {});
|
|
60
|
+
if (!agent.on || disabled())
|
|
61
|
+
return undefined;
|
|
62
|
+
const dir = agent.name === 'claude-code' ? '.claude' : '.agents';
|
|
63
|
+
// Личная копия побеждает проектную при одинаковом имени (так выбирает Claude Code).
|
|
64
|
+
const personal = stampIn(personalRoot(dir));
|
|
65
|
+
if (personal !== undefined)
|
|
66
|
+
return outdated(personal) ? `${overrides(dir, personal)}, then run: ${RUN}` : undefined;
|
|
67
|
+
// Проектная — ближайшая от cwd вверх до корня git-репозитория: агента обычно запускают в корне
|
|
68
|
+
// репозитория, а сборку — в подкаталоге документации, и он видит скиллы обоих уровней.
|
|
69
|
+
let stamp;
|
|
70
|
+
for (let d = path.resolve(cwd); stamp === undefined; d = path.dirname(d)) {
|
|
71
|
+
stamp = stampIn(path.join(d, dir, 'skills'));
|
|
72
|
+
if (fs.existsSync(path.join(d, '.git')) || path.dirname(d) === d)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (stamp === undefined)
|
|
76
|
+
return `c4builder agent skills are not installed here. Ask the user, then run: ${RUN}`;
|
|
77
|
+
if (!outdated(stamp))
|
|
78
|
+
return undefined;
|
|
79
|
+
return `c4builder agent skills here are from ${stamp || 'an unknown version'} (current ${pkg.version}). Ask the user, then run: ${RUN}`;
|
|
80
|
+
};
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -10,6 +10,7 @@ import cmdCheck from "./commands/check.js";
|
|
|
10
10
|
import cmdList from "./commands/list.js";
|
|
11
11
|
import cmdSite from "./commands/site.js";
|
|
12
12
|
import cmdFeedback from "./commands/feedback.js";
|
|
13
|
+
import cmdSkills, { skillsNotice } from "./commands/skills.js";
|
|
13
14
|
import { detectAgent } from "./agent.js";
|
|
14
15
|
import { cmdUpdate, pendingUpdate, startUpdateCheck } from "./update.js";
|
|
15
16
|
import cmdCollect from "./wizard/collect.js";
|
|
@@ -30,7 +31,25 @@ import { packageJson as pkg } from "../util/paths.js";
|
|
|
30
31
|
import { outputDirs, parseConfig } from "../config/options.js";
|
|
31
32
|
import { isValidPort } from "../config/schema.js";
|
|
32
33
|
// Позиционные подкоманды: allowExcessArguments пропускает любые, список — единственный фильтр.
|
|
33
|
-
|
|
34
|
+
// Commander о них не знает — аргументы и описания здесь же, для блока команд в --help.
|
|
35
|
+
const COMMANDS = {
|
|
36
|
+
check: ['<files...>', 'check diagrams without a build, exit code 0 or 1'],
|
|
37
|
+
jre: ['info|install', 'which Java will be used | download a JRE in advance'],
|
|
38
|
+
site: ['', 'build and serve the site locally (same as --site)'],
|
|
39
|
+
new: ['', 'create a project from the template (same as --new)'],
|
|
40
|
+
config: ['', 'setup wizard with the current values (same as --config)'],
|
|
41
|
+
update: ['', 'update a global npm install to the newest version'],
|
|
42
|
+
feedback: ['', 'wait for page feedback from site -w (--wait --as <name>)'],
|
|
43
|
+
skills: ['', 'install or update the agent skills in the current folder']
|
|
44
|
+
};
|
|
45
|
+
const commandsHelp = () => {
|
|
46
|
+
const rows = Object.entries(COMMANDS).map(([name, [args, about]]) => [
|
|
47
|
+
`${name} ${args}`.trim(),
|
|
48
|
+
about
|
|
49
|
+
]);
|
|
50
|
+
const width = Math.max(...rows.map(([usage]) => usage.length));
|
|
51
|
+
return `\nCommands:\n${rows.map(([usage, about]) => ` ${usage.padEnd(width)} ${about}`).join('\n')}`;
|
|
52
|
+
};
|
|
34
53
|
const intro = () => {
|
|
35
54
|
console.log(chalk.blue(figlet.textSync('c4builder')));
|
|
36
55
|
console.log(chalk.gray('Blow up your software documentation writing skills'));
|
|
@@ -112,6 +131,8 @@ function getOptions(c) {
|
|
|
112
131
|
}
|
|
113
132
|
export default async () => {
|
|
114
133
|
program
|
|
134
|
+
.name('c4builder')
|
|
135
|
+
.usage('[command] [options]')
|
|
115
136
|
.version(pkg.version)
|
|
116
137
|
.option('--new', 'create a new project from template')
|
|
117
138
|
.option('--name <name>', 'project name for --new (skips the name prompt)')
|
|
@@ -137,14 +158,15 @@ export default async () => {
|
|
|
137
158
|
return Number(v);
|
|
138
159
|
})
|
|
139
160
|
.allowExcessArguments() // позиционные аргументы подкоманд `jre <action>` и `check <file...>`
|
|
161
|
+
.addHelpText('after', commandsHelp())
|
|
140
162
|
.parse(process.argv);
|
|
141
163
|
const opts = program.opts();
|
|
142
164
|
const [cmd, ...cmdArgs] = program.args;
|
|
143
165
|
// allowExcessArguments глотает любой позиционный аргумент — без этой проверки
|
|
144
166
|
// опечатка (`c4builder nwe`) молча превращалась бы в сборку.
|
|
145
|
-
if (cmd !== undefined && !
|
|
167
|
+
if (cmd !== undefined && !Object.hasOwn(COMMANDS, cmd)) {
|
|
146
168
|
console.error(chalk.red(`unknown command: ${cmd}`));
|
|
147
|
-
console.error(`available commands: ${COMMANDS.join(', ')} (see c4builder --help for options)`);
|
|
169
|
+
console.error(`available commands: ${Object.keys(COMMANDS).join(', ')} (see c4builder --help for options)`);
|
|
148
170
|
process.exit(1);
|
|
149
171
|
}
|
|
150
172
|
// --version/--help уже вышли в parse. Уведомление — по кэшу прошлой проверки, до её
|
|
@@ -167,6 +189,9 @@ export default async () => {
|
|
|
167
189
|
// Замечания со страницы `site -w` — тоже до Configstore: команду запускают в любом каталоге.
|
|
168
190
|
if (cmd === 'feedback')
|
|
169
191
|
return cmdFeedback(path.join(process.cwd(), opts.configFile ?? '.c4builder'), opts);
|
|
192
|
+
// Установка скиллов — до Configstore: ставит их и в каталог без проекта.
|
|
193
|
+
if (cmd === 'skills')
|
|
194
|
+
return cmdSkills();
|
|
170
195
|
// `site`/`new`/`config` — позиционные синонимы одноимённых флагов: эту форму печатают
|
|
171
196
|
// подсказки CLI и учат README, шаблон и скиллы.
|
|
172
197
|
if (cmd)
|
|
@@ -180,6 +205,11 @@ export default async () => {
|
|
|
180
205
|
console.error(chalk.red(`no .c4builder in ${process.cwd()} — run inside a project or create one with c4builder new`));
|
|
181
206
|
process.exit(1);
|
|
182
207
|
}
|
|
208
|
+
// Без терминала мастеру неоткуда взять ответы: inquirer падал со стектрейсом и кодом 0.
|
|
209
|
+
if (!opts.new && !process.stdin.isTTY && !fs.existsSync(configPath)) {
|
|
210
|
+
console.error(chalk.red(`no ${opts.configFile ?? '.c4builder'} in ${process.cwd()} — run c4builder --new, or c4builder --config in a terminal`));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
183
213
|
let conf = { get: () => { } };
|
|
184
214
|
let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
|
|
185
215
|
// Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
|
|
@@ -187,7 +217,15 @@ export default async () => {
|
|
|
187
217
|
if (!opts.new) {
|
|
188
218
|
const projectKey = process.cwd().split(path.sep).splice(1).join('_');
|
|
189
219
|
lockPath = `${configPath}.lock`;
|
|
190
|
-
|
|
220
|
+
try {
|
|
221
|
+
// По умолчанию Configstore молча затирает битый JSON пустым конфигом — настройки
|
|
222
|
+
// терялись, а сборка уходила в мастер. --reset стирает конфиг и так.
|
|
223
|
+
conf = new Configstore(projectKey, {}, { configPath, clearInvalidConfig: !!opts.reset });
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
console.error(chalk.red(`.c4builder error — cannot read ${configPath}: ${e.message}`));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
191
229
|
cacheConf = new Configstore(`${projectKey}_cache`, {}, { configPath: `${configPath}.cache` });
|
|
192
230
|
// Миграция: чексуммы раньше жили в .c4builder, переносим их в .c4builder.cache,
|
|
193
231
|
// чтобы рабочий конфиг перестал «дёргаться» в git при каждой сборке.
|
|
@@ -247,6 +285,10 @@ export default async () => {
|
|
|
247
285
|
}
|
|
248
286
|
await cmdCollect(currentConfig, conf, opts);
|
|
249
287
|
if (!opts.config) {
|
|
288
|
+
// При старте, а не при выходе, как уведомление о версии: `site -w` живёт до остановки.
|
|
289
|
+
const skills = skillsNotice();
|
|
290
|
+
if (skills)
|
|
291
|
+
process.stderr.write(`${skills}\n`);
|
|
250
292
|
let isBuilding = false;
|
|
251
293
|
let attemptedWatchBuild = false;
|
|
252
294
|
// Опции сборки после визарда: строгий разбор итогового конфига. Штатно валиден
|
|
@@ -359,8 +401,15 @@ export default async () => {
|
|
|
359
401
|
}
|
|
360
402
|
}
|
|
361
403
|
isBuilding = false;
|
|
362
|
-
if (opts.site)
|
|
363
|
-
|
|
404
|
+
if (opts.site) {
|
|
405
|
+
try {
|
|
406
|
+
return await cmdSite(options, { ...opts, agent: detectAgent(process.env, opts) }, reloadEmitter, () => pages);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Ошибку запуска сервера (занятый порт) cmdSite уже напечатал — остаётся код выхода.
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
364
413
|
if (options.GENERATE_WEBSITE && !opts.watch) {
|
|
365
414
|
console.log(chalk.gray('\nto view the generated website run'));
|
|
366
415
|
console.log(`> c4builder site`);
|
package/dist/cli/update.js
CHANGED
|
@@ -19,7 +19,7 @@ const npmSync = (args, options) => {
|
|
|
19
19
|
};
|
|
20
20
|
// CI ставят GitLab CI, GitHub Actions и большинство CI; C4BUILDER_DOCKER — наш Dockerfile;
|
|
21
21
|
// NO_UPDATE_NOTIFIER — общая конвенция с пакетом update-notifier.
|
|
22
|
-
const disabled = () => !!(process.env.CI || process.env.NO_UPDATE_NOTIFIER || process.env.C4BUILDER_DOCKER);
|
|
22
|
+
export const disabled = () => !!(process.env.CI || process.env.NO_UPDATE_NOTIFIER || process.env.C4BUILDER_DOCKER);
|
|
23
23
|
const cacheFile = () => path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'c4builder', 'dist-tags.json');
|
|
24
24
|
// Порядок semver без зависимости semver: numeric-коллатор сравнивает цифровые серии численно
|
|
25
25
|
// (0.10.0 > 0.9.0, rc.10 > rc.2); стабильная версия старше пререлиза того же ядра.
|
package/dist/core/build.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fsextra from 'fs-extra';
|
|
4
|
-
import { makeDirectory, writeFile,
|
|
4
|
+
import { makeDirectory, writeFile, writeProgress } from "../util/utils.js";
|
|
5
5
|
// clearD2FileCache: сброс кеша графа D2-импортов на границах сборки (сам webworker
|
|
6
6
|
// D2 гасит CLI после одиночной сборки — в watch-режиме он переживает ребилды).
|
|
7
7
|
import { clearD2FileCache } from "./render/d2renderer.js";
|
|
@@ -57,16 +57,12 @@ const build = async (options, cacheConf, plugins = []) => {
|
|
|
57
57
|
}
|
|
58
58
|
if (options.GENERATE_LOCAL_IMAGES) {
|
|
59
59
|
console.log(chalk.blue('generating images'));
|
|
60
|
-
await generateImages(tree, options, (count, total) =>
|
|
61
|
-
writeOnSameLine(`processed ${count}/${total} images`);
|
|
62
|
-
}, cacheConf, bkFolderName);
|
|
60
|
+
await generateImages(tree, options, (count, total) => writeProgress(count, total, 'images'), cacheConf, bkFolderName);
|
|
63
61
|
console.log('');
|
|
64
62
|
}
|
|
65
63
|
if (options.GENERATE_MD) {
|
|
66
64
|
console.log(chalk.blue('generating markdown files'));
|
|
67
|
-
await generateMD(tree, options, (count, total) =>
|
|
68
|
-
writeOnSameLine(`processed ${count}/${total} files`);
|
|
69
|
-
});
|
|
65
|
+
await generateMD(tree, options, (count, total) => writeProgress(count, total, 'files'));
|
|
70
66
|
console.log('');
|
|
71
67
|
}
|
|
72
68
|
if (options.GENERATE_WEBSITE) {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
//entry point
|
|
3
3
|
import cli from "./cli/dispatch.js";
|
|
4
|
-
|
|
4
|
+
// Закрытый промпт (Ctrl+C или EOF без терминала) — отмена, а не сбой: одна строка вместо
|
|
5
|
+
// стектрейса inquirer. Код выставляем сами: на EOF процесс уже выходил с кодом 0, и CI
|
|
6
|
+
// принимал несобранный сайт за успех.
|
|
7
|
+
cli().catch((e) => {
|
|
8
|
+
if (!(e instanceof Error && e.name === 'ExitPromptError'))
|
|
9
|
+
throw e;
|
|
10
|
+
console.error(`aborted: the prompt was closed${process.stdin.isTTY ? '' : ' (no terminal for the wizard)'}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/util/utils.js
CHANGED
|
@@ -9,8 +9,14 @@ const makeDirectory = async (path) => {
|
|
|
9
9
|
// Кодировка не передаётся ни одним вызовом → всегда Buffer (потребители зовут .toString()).
|
|
10
10
|
const readFile = (path) => fs.promises.readFile(path);
|
|
11
11
|
const writeFile = (path, data) => fs.promises.writeFile(path, data);
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Вне терминала (лог фонового `--site -w`, CI) `\r` не перетирает строку, а склеивает
|
|
13
|
+
// все обновления в одну гигантскую — там печатаем только итог фазы. Перевод строки
|
|
14
|
+
// после прогресса ставит вызывающий.
|
|
15
|
+
const writeProgress = (count, total, unit) => {
|
|
16
|
+
if (process.stdout.isTTY)
|
|
17
|
+
process.stdout.write(`processed ${count}/${total} ${unit}\r`);
|
|
18
|
+
else if (count === total)
|
|
19
|
+
process.stdout.write(`processed ${total} ${unit}`);
|
|
14
20
|
};
|
|
15
21
|
const encodeURIPath = (path) => {
|
|
16
22
|
path = path.split('\\').join('/');
|
|
@@ -110,4 +116,4 @@ const VENDORED_JAR = {
|
|
|
110
116
|
version: '1.2026.8',
|
|
111
117
|
jar: 'plantuml-1.2026.8.jar'
|
|
112
118
|
};
|
|
113
|
-
export { makeDirectory, readFile, writeFile, encodeURIPath,
|
|
119
|
+
export { makeDirectory, readFile, writeFile, encodeURIPath, writeProgress, clearConsole, plantUmlServerUrl, VENDORED_JAR };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfacapital-tech/c4builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-rc.2",
|
|
4
4
|
"description": "A CLI tool designed to compile a folder structure of markdowns and plant uml files into a site, single file markdown or a collection of markdowns with links",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"!template/docs",
|
|
14
14
|
"!template/.c4builder",
|
|
15
15
|
"vendor/",
|
|
16
|
+
"skills/",
|
|
16
17
|
"README.MD",
|
|
17
18
|
"license"
|
|
18
19
|
],
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c4builder
|
|
3
|
+
description: Work on a c4builder architecture-docs project - C4 model diagrams in PlantUML (C4-PlantUML stdlib) or D2, BPMN process diagrams, Mermaid diagrams, plus markdown, built into a docsify site or markdown files. Use when creating or editing .puml, .iuml, .d2, .bpmn, .mmd or .md files in a project that has a .c4builder config, when changing .c4builder settings or plugins (openspec, openapi), or when building, validating or previewing the docs. Teaches the edit, c4builder check, build loop and the C4-PlantUML conventions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c4builder
|
|
7
|
+
|
|
8
|
+
c4builder compiles a folder tree of markdown and diagrams into documentation: a docsify
|
|
9
|
+
site, a collection of markdown files with navigation and/or one complete markdown file.
|
|
10
|
+
Installing or repairing the CLI itself is the `c4builder-setup` skill.
|
|
11
|
+
|
|
12
|
+
A c4builder project is a folder with a `.c4builder` file (JSON config) next to the source
|
|
13
|
+
folder (`rootFolder`, `src` by default).
|
|
14
|
+
|
|
15
|
+
## Project layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
.c4builder config (JSON) -> references/config.md
|
|
19
|
+
.c4builder.cache render checksums (git-ignored)
|
|
20
|
+
src/ rootFolder: one folder = one page
|
|
21
|
+
context.md page text
|
|
22
|
+
context.puml diagram attached to the same page
|
|
23
|
+
styles.iuml shared PlantUML styles, included by diagrams
|
|
24
|
+
_c4lib.d2 "_" prefix: never rendered or published, still includable
|
|
25
|
+
1 Internet Banking System/
|
|
26
|
+
system.md
|
|
27
|
+
system.puml
|
|
28
|
+
API Application/
|
|
29
|
+
container.md
|
|
30
|
+
container.puml
|
|
31
|
+
docs/ distFolder: generated, wiped on every build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
How the source tree becomes pages:
|
|
35
|
+
|
|
36
|
+
- Every folder is one page and one node in the navigation; the page title is the folder
|
|
37
|
+
name (the root page is `homepageName`). Siblings are sorted by name — prefix folders with
|
|
38
|
+
numbers (`1 …`, `2 …`) to order them.
|
|
39
|
+
- All `.md` files of a folder are concatenated into its page.
|
|
40
|
+
- Every `.puml` / `.d2` / `.bpmn` / `.mmd` in a folder is rendered and attached to the page (on top by
|
|
41
|
+
default, `diagramsOnTop`), unless the markdown places it explicitly with
|
|
42
|
+
`` — then it appears exactly there.
|
|
43
|
+
- Files and folders starting with `_` are skipped (use it for shared libraries and drafts);
|
|
44
|
+
`CLAUDE.md` is skipped too. Other files (images, `.iuml`) are copied next to the page —
|
|
45
|
+
keep their names unique across the project.
|
|
46
|
+
- `foo.puml`, `foo.d2`, `foo.bpmn` and `foo.mmd` in one folder render to the same image name → build error.
|
|
47
|
+
- Never edit `docs/` (or whatever `distFolder` is): every build regenerates it.
|
|
48
|
+
|
|
49
|
+
## The loop: edit → check → build
|
|
50
|
+
|
|
51
|
+
1. **Edit** markdown and diagrams under `src/`.
|
|
52
|
+
2. **Check every diagram you changed** — fast, no project build, writes nothing:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
c4builder check src/context.puml "src/1 Internet Banking System/system.puml"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Exit code 0 means every file compiles. Otherwise each broken file is reported as
|
|
59
|
+
`✗ <file>: line <N>: <message>` (PlantUML lines count from the top of the file). Errors
|
|
60
|
+
in a `.iuml` carry no line number; D2 errors end with `<file>:<line>:<col>: <message>`;
|
|
61
|
+
BPMN problems are `✗ <file>: <element id> [<rule>] <description>`, one line each;
|
|
62
|
+
mermaid errors are `✗ <file>: line <N>: <message>`.
|
|
63
|
+
Fix and re-run until the exit code is 0. A changed `.iuml` is checked directly too: a
|
|
64
|
+
styles file calling C4 macros (`UpdateElementStyle`) is checked with the C4 stdlib
|
|
65
|
+
included. Same engines as the build (bundled PlantUML jar, bundled D2, BPMN pipeline, mermaid), no
|
|
66
|
+
`.c4builder` needed.
|
|
67
|
+
3. **Build** the whole project:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
c4builder
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
With a complete `.c4builder` (written by `c4builder --new --name <name> -y` or by the
|
|
74
|
+
first interactive run) this is a one-off, non-interactive build. A missing or partial
|
|
75
|
+
config needs the interactive wizard; without a terminal the run stops with exit code 1.
|
|
76
|
+
Do not drive the wizard: ask the user or write the full config (references/config.md). A diagram that fails here aborts the build with a long
|
|
77
|
+
Java stack trace and keeps the previous output in `docs_bk/`; run `c4builder check` on
|
|
78
|
+
the changed files to get the short message.
|
|
79
|
+
4. **Look at the result** in `docs/`: `docs/<folder>/README.md` (markdown collection),
|
|
80
|
+
`docs/index.html` + `docs/<folder>/<folder>.md` (site), `docs/<folder>/<diagram>.svg`.
|
|
81
|
+
5. **Live preview for a human:** `c4builder --site -w` builds, serves on
|
|
82
|
+
`http://localhost:3000` (`webPort`, or `-p <n>`), rebuilds on save and reloads the
|
|
83
|
+
browser; `-o` opens it. It never exits — start it in the background or leave it to the
|
|
84
|
+
user. `c4builder site` builds and serves without watching (also never exits). In `-w` a
|
|
85
|
+
human can select text on a page and add a question: it lands in the source `.md` under the
|
|
86
|
+
`questionsHeading` section (`Open questions` by default) — read it there, not in `docs/`.
|
|
87
|
+
|
|
88
|
+
## Feedback from the page
|
|
89
|
+
|
|
90
|
+
When the user wants to look at the pages and tell you what to fix, use the `c4builder-review`
|
|
91
|
+
skill (`/c4builder-review [change-id|page]`): it checks the server, gives the link to the page and
|
|
92
|
+
runs the batch loop.
|
|
93
|
+
|
|
94
|
+
In short: under an agent (`CLAUDECODE`, `CODEX_THREAD_ID`, `GEMINI_CLI`, `OPENCODE`, `COPILOT_CLI`,
|
|
95
|
+
`AI_AGENT`, or `--agent`) `c4builder --site -w` adds a **Feedback** button to text selected on a
|
|
96
|
+
page. The comments do not go into the files: `c4builder feedback --wait --as <name>`, run in the
|
|
97
|
+
background with your change or task as `<name>`, exits with one batch on stdout. Apply it to the
|
|
98
|
+
sources, run the command again with the same name and stop on `Decision: approved`.
|
|
99
|
+
|
|
100
|
+
## Writing C4 diagrams (PlantUML)
|
|
101
|
+
|
|
102
|
+
```plantuml
|
|
103
|
+
@startuml
|
|
104
|
+
!include <C4/C4_Container>
|
|
105
|
+
!include ../styles.iuml
|
|
106
|
+
|
|
107
|
+
Person(user, "Customer", "Uses the web app")
|
|
108
|
+
System_Boundary(shop, "Shop") {
|
|
109
|
+
Container(web, "Web App", "TypeScript", "Catalog and checkout")
|
|
110
|
+
ContainerDb(db, "Database", "PostgreSQL", "Orders")
|
|
111
|
+
}
|
|
112
|
+
Rel(user, web, "Uses", "HTTPS")
|
|
113
|
+
Rel(web, db, "Reads/writes", "SQL")
|
|
114
|
+
@enduml
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- Include C4 from the PlantUML stdlib: `!include <C4/C4_Context>`, `<C4/C4_Container>`,
|
|
118
|
+
`<C4/C4_Component>`, `<C4/C4_Dynamic>`, `<C4/C4_Deployment>`. It is bundled in the jar —
|
|
119
|
+
builds work offline. Never `!include https://…` (network on every build, breaks CI).
|
|
120
|
+
- Shared look lives in one `.iuml` (the template's `src/styles.iuml`), included **after**
|
|
121
|
+
the C4 include by a path relative to the diagram: `!include styles.iuml` from `src/`,
|
|
122
|
+
`!include ../styles.iuml` one level down.
|
|
123
|
+
- One diagram per file, `@startuml` … `@enduml`, file name = image name.
|
|
124
|
+
- Non-Latin text (Cyrillic etc.) works: sources are UTF-8, the bundled font covers it.
|
|
125
|
+
|
|
126
|
+
Details — sequence/class diagrams, D2, fonts, PNG output: read
|
|
127
|
+
[references/diagrams.md](references/diagrams.md) when writing anything beyond a plain
|
|
128
|
+
C4-PlantUML diagram.
|
|
129
|
+
|
|
130
|
+
## Business processes (BPMN)
|
|
131
|
+
|
|
132
|
+
A `.bpmn` file is BPMN 2.0 XML with **only the semantic model** — no `bpmndi` coordinates:
|
|
133
|
+
the build validates the model and lays it out with horizontal pools and lanes. Read
|
|
134
|
+
[references/bpmn.md](references/bpmn.md) before creating or editing any `.bpmn` file: source
|
|
135
|
+
format, pool/lane conventions and how to fix every `c4builder check` message.
|
|
136
|
+
|
|
137
|
+
## Mermaid
|
|
138
|
+
|
|
139
|
+
`.mmd` files and ```` ```mermaid ```` blocks in pages and OpenSpec artifacts are rendered at
|
|
140
|
+
build time into images (real mermaid, no browser). Use them for flows, sequences, states and
|
|
141
|
+
ER inside the docs; keep the C4 model in PlantUML. Read
|
|
142
|
+
[references/mermaid.md](references/mermaid.md) before writing mermaid: supported types,
|
|
143
|
+
fixed config keys and the `c4builder check` loop.
|
|
144
|
+
|
|
145
|
+
## Config and plugins
|
|
146
|
+
|
|
147
|
+
- Read [references/config.md](references/config.md) before editing `.c4builder`: unknown
|
|
148
|
+
keys are **silently dropped**, so a typo does nothing. `c4builder --list` prints the
|
|
149
|
+
current config.
|
|
150
|
+
- Read [references/plugins.md](references/plugins.md) when the project has or needs
|
|
151
|
+
`plugins` — `openspec` (OpenSpec store as a site section) or `openapi` (swagger-ui pages).
|
|
152
|
+
|
|
153
|
+
## Reading an existing architecture
|
|
154
|
+
|
|
155
|
+
With `generateLLMS: true` the built site has `llms.txt` (every page in sidebar order, links
|
|
156
|
+
to its `.md`) and `llms-full.txt` (all pages in one file, diagrams inlined as PlantUML/D2/BPMN/Mermaid
|
|
157
|
+
source, shared local includes once in an appendix). To answer questions about a documented
|
|
158
|
+
system, read `llms-full.txt` from the site or `distFolder` instead of crawling pages or
|
|
159
|
+
images: the C4 source (`Person`, `System`, `Rel`) is the model itself.
|
|
160
|
+
|
|
161
|
+
## New version notice
|
|
162
|
+
|
|
163
|
+
When c4builder output ends with a stderr line
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
c4builder <new> is available (current <current>). Run: c4builder update
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
a newer c4builder is published. Tell the user. Run `c4builder update` only when the user asks
|
|
170
|
+
for it: it reinstalls the global CLI. The notice changes neither stdout nor the exit code.
|
|
171
|
+
|
|
172
|
+
## Skills notice
|
|
173
|
+
|
|
174
|
+
When a build or `c4builder site` run by an agent starts with one of these stderr lines
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
c4builder agent skills are not installed here. Ask the user, then run: c4builder skills
|
|
178
|
+
c4builder agent skills here are from <stamp> (current <version>). Ask the user, then run: c4builder skills
|
|
179
|
+
~/.claude/skills/c4builder (<stamp|no version>) overrides project skills. Ask the user to remove it, then run: c4builder skills
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
the c4builder skills you work with are missing, older than the CLI, or shadowed by a stale personal
|
|
183
|
+
copy (`~/.agents/skills/c4builder` for agents other than Claude Code). Tell the user. Run
|
|
184
|
+
`c4builder skills` only when the user agrees: it writes the skills of the installed CLI into
|
|
185
|
+
`.agents/skills/` of the current folder (and `.claude/skills/` under Claude Code), replacing the
|
|
186
|
+
c4builder skills there. Remove a personal copy only when the user agrees. The notice changes
|
|
187
|
+
neither stdout nor the exit code.
|
|
188
|
+
|
|
189
|
+
## Common mistakes
|
|
190
|
+
|
|
191
|
+
- Editing generated files in `docs/` instead of `src/`.
|
|
192
|
+
- Remote `!include` URLs instead of `<C4/...>` stdlib includes.
|
|
193
|
+
- Running `c4builder --site -w` or `c4builder feedback --wait --as <name>` in the foreground — the
|
|
194
|
+
agent hangs.
|
|
195
|
+
- Forgetting `c4builder check` and reading a Java stack trace from the full build instead.
|
|
196
|
+
- A page "missing" from the output because its file or folder starts with `_`.
|
|
197
|
+
- `generateLocalImages: false` in a project with `.d2`, `.bpmn` or `.mmd` files — they have no online
|
|
198
|
+
renderer, the build stops with an error.
|
|
199
|
+
- Writing `bpmndi` coordinates into a `.bpmn` — they are ignored; the layout is always automatic.
|