@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 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: `npx skills add AlfaCapital-Tech/C4-Builder/skills` installs the skills, and
64
- `"generateLLMS": true` adds `llms.txt` / `llms-full.txt` (diagram sources included) to the site.
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
 
@@ -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 (isIndex && lastIndexHtml) {
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 (isIndex)
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
- const server = app.listen(port, '127.0.0.1', () => {
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
+ };
@@ -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
- const COMMANDS = ['check', 'jre', 'site', 'new', 'config', 'update', 'feedback'];
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 && !COMMANDS.includes(cmd)) {
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
- conf = new Configstore(projectKey, {}, { configPath });
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
- return await cmdSite(options, { ...opts, agent: detectAgent(process.env, opts) }, reloadEmitter, () => pages);
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`);
@@ -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); стабильная версия старше пререлиза того же ядра.
@@ -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, writeOnSameLine } from "../util/utils.js";
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
- cli();
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
+ });
@@ -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
- const writeOnSameLine = async (message) => {
13
- process.stdout.write(`${message}\r`);
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, writeOnSameLine, clearConsole, plantUmlServerUrl, VENDORED_JAR };
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.9.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
+ `![name](container.puml)` — 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.