@alfacapital-tech/c4builder 0.4.0-rc.4 → 0.5.0-rc.1

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.
Files changed (39) hide show
  1. package/README.MD +69 -615
  2. package/dist/cli/commands/check.js +43 -26
  3. package/dist/cli/commands/help.js +5 -2
  4. package/dist/cli/commands/jre.js +6 -6
  5. package/dist/cli/commands/list.js +4 -1
  6. package/dist/cli/commands/new.js +6 -6
  7. package/dist/cli/commands/site.js +3 -2
  8. package/dist/cli/dispatch.js +38 -18
  9. package/dist/cli/wizard/collect.js +16 -10
  10. package/dist/config/defaults.js +1 -0
  11. package/dist/config/options.js +1 -1
  12. package/dist/config/schema.js +5 -2
  13. package/dist/core/build.js +15 -6
  14. package/dist/core/compose/docsify.template.js +0 -1
  15. package/dist/core/compose/markdown.js +85 -19
  16. package/dist/core/plugins/hooks.js +1 -1
  17. package/dist/core/plugins/load.js +6 -6
  18. package/dist/core/plugins/source.js +6 -6
  19. package/dist/core/plugins/tree.js +5 -5
  20. package/dist/core/render/bpmnrenderer.js +522 -0
  21. package/dist/core/render/d2renderer.js +19 -19
  22. package/dist/core/render/diagrams.js +64 -45
  23. package/dist/core/render/jre.js +14 -14
  24. package/dist/core/render/pngraster.js +4 -4
  25. package/dist/{plugins/openspec → core/scan}/fences.js +11 -10
  26. package/dist/core/scan/tree.js +37 -14
  27. package/dist/plugins/openapi/index.js +17 -7
  28. package/dist/plugins/openspec/index.js +1 -1
  29. package/dist/plugins/openspec/render.js +5 -2
  30. package/dist/util/archive.js +1 -1
  31. package/dist/util/http.js +3 -3
  32. package/dist/util/lock.js +3 -3
  33. package/dist/util/paths.js +1 -1
  34. package/package.json +9 -4
  35. package/template/AGENTS.md +52 -0
  36. package/template/readme.md +3 -3
  37. package/template/src/5 BPMN Example/account-opening.bpmn +69 -0
  38. package/template/src/5 BPMN Example/account-opening.md +7 -0
  39. package/dist/util/glob.js +0 -69
@@ -6,8 +6,9 @@ import { VENDORED_JAR } from "../../util/utils.js";
6
6
  import { resolveJava } from "../../core/render/jre.js";
7
7
  import { renderDiagram } from "../../core/render/diagrams.js";
8
8
  import { renderD2, teardownD2 } from "../../core/render/d2renderer.js";
9
+ import { BpmnError, renderBpmn, teardownBpmn } from "../../core/render/bpmnrenderer.js";
9
10
  // `c4builder check <file...>` — валидация отдельных диаграмм ТЕМ ЖЕ движком, что и
10
- // сборка (вендорный PlantUML jar + системная/managed java, бандл D2), без проекта и
11
+ // сборка (вендорный PlantUML jar + системная/managed java, бандл D2, конвейер BPMN), без проекта и
11
12
  // `.c4builder`. Рендер идёт в память и выбрасывается: проверяем ровно то, что упало бы
12
13
  // в сборке (системный `plantuml` может отличаться версией). Код выхода 0 — все файлы ок,
13
14
  // 1 — есть ошибка (движка, неизвестного расширения или чтения файла).
@@ -23,39 +24,51 @@ const getJava = () => {
23
24
  javaPromise ??= resolveJava({ log: (m) => console.log(chalk.gray(m)) }).then((r) => r.path);
24
25
  return javaPromise;
25
26
  };
27
+ // .iuml — библиотека без @startuml: сам по себе PlantUML «рендерит» его без ошибок,
28
+ // поэтому проверяем через включение в пустую диаграмму. Контекст C4 библиотеке стилей
29
+ // задаёт подключающая диаграмма — при ошибке движка повтор с C4-stdlib (Dynamic тянет
30
+ // Component → Container → Context, Deployment — Container). Не сразу с C4: это +~1.2 с
31
+ // java на файл, а самодостаточные библиотеки подключают C4 сами.
32
+ const LIB_CONTEXTS = ['', '!include <C4/C4_Dynamic>\n!include <C4/C4_Deployment>\n'];
26
33
  const checkPlantUml = async (abs) => {
27
- // .iuml — библиотека без @startuml: сам по себе PlantUML «рендерит» его без ошибок,
28
- // поэтому проверяем через включение в пустую диаграмму.
29
34
  const isLib = path.extname(abs) === '.iuml';
30
- const content = isLib ? `@startuml\n!include ${abs}\n@enduml\n` : await readFile(abs);
31
- const isDitaa = /@startditaa/i.test(content.toString());
32
- try {
33
- await renderDiagram(content, {
34
- javaBin: await getJava(),
35
- jarPath: JAR_PATH,
36
- includePath: path.dirname(abs),
37
- format: isDitaa ? 'png' : 'svg',
38
- charset: 'UTF-8',
39
- isDitaa,
40
- useSystemFonts: false
41
- });
42
- }
43
- catch (e) {
44
- const m = e.message.match(PUML_ERROR);
45
- if (!m)
46
- throw e;
47
- // Для .iuml позиция относится к обёртке, а не к файлу — не показываем.
48
- throw new Error(isLib ? m[2] : `строка ${Number(m[1]) + 1}: ${m[2]}`);
35
+ const attempts = isLib
36
+ ? LIB_CONTEXTS.map((ctx) => `@startuml\n${ctx}!include ${abs}\n@enduml\n`)
37
+ : [await readFile(abs)];
38
+ for (const [i, content] of attempts.entries()) {
39
+ const isDitaa = /@startditaa/i.test(content.toString());
40
+ try {
41
+ await renderDiagram(content, {
42
+ javaBin: await getJava(),
43
+ jarPath: JAR_PATH,
44
+ includePath: path.dirname(abs),
45
+ format: isDitaa ? 'png' : 'svg',
46
+ charset: 'UTF-8',
47
+ isDitaa,
48
+ useSystemFonts: false
49
+ });
50
+ return;
51
+ }
52
+ catch (e) {
53
+ const m = e.message.match(PUML_ERROR);
54
+ if (!m)
55
+ throw e;
56
+ if (i < attempts.length - 1)
57
+ continue;
58
+ // Для .iuml позиция относится к обёртке, а не к файлу — не показываем.
59
+ throw new Error(isLib ? m[2] : `line ${Number(m[1]) + 1}: ${m[2]}`);
60
+ }
49
61
  }
50
62
  };
51
63
  const CHECKERS = {
52
64
  '.puml': checkPlantUml,
53
65
  '.iuml': checkPlantUml,
54
- '.d2': (abs) => renderD2(abs)
66
+ '.d2': (abs) => renderD2(abs),
67
+ '.bpmn': async (abs) => renderBpmn(await readFile(abs, 'utf8'), path.relative(process.cwd(), abs))
55
68
  };
56
69
  export default async (files) => {
57
70
  if (files.length === 0) {
58
- console.log(chalk.red('использование: c4builder check <file...> (.puml | .iuml | .d2)'));
71
+ console.log(chalk.red('usage: c4builder check <file...> (.puml | .iuml | .d2 | .bpmn)'));
59
72
  process.exit(1);
60
73
  }
61
74
  let failed = 0;
@@ -65,16 +78,20 @@ export default async (files) => {
65
78
  const check = CHECKERS[path.extname(abs).toLowerCase()];
66
79
  try {
67
80
  if (!check)
68
- throw new Error('неизвестное расширение (ожидается .puml, .iuml или .d2)');
81
+ throw new Error('unsupported extension (expected .puml, .iuml, .d2 or .bpmn)');
69
82
  await check(abs);
70
83
  console.log(chalk.green(`✓ ${file}`));
71
84
  }
72
85
  catch (e) {
73
86
  failed++;
74
- console.error(chalk.red(`✗ ${file}: ${e.message}`));
87
+ // BPMN: по строке на нарушение — агент правит их по id элемента.
88
+ const problems = e instanceof BpmnError ? e.problems : [e.message];
89
+ for (const p of problems)
90
+ console.error(chalk.red(`✗ ${file}: ${p}`));
75
91
  }
76
92
  }
77
93
  await teardownD2();
94
+ await teardownBpmn();
78
95
  if (failed)
79
96
  process.exit(1);
80
97
  };
@@ -4,7 +4,8 @@ export default () => {
4
4
  console.log(chalk.blue(figlet.textSync('c4builder')));
5
5
  console.log(`
6
6
  Full documentation
7
- ${chalk.blue('https://adrianvlupu.github.io/C4-Builder/')}
7
+ ${chalk.blue('https://alfacapital-tech.github.io/C4-Builder/ru/')} (in Russian)
8
+ ${chalk.blue('https://alfacapital-tech.github.io/C4-Builder/en/')} (English)
8
9
 
9
10
  CONFIGURATION OPTIONS
10
11
  ${chalk.cyan('Project Name')}
@@ -30,6 +31,8 @@ ${chalk.cyan('Generate website')}
30
31
  Uses docsify to generate a website with a sidebar for navigation. The site can be easily deployed to github pages.
31
32
  ${chalk.cyan('Website docsify theme')}
32
33
  Changes the default theme of the generated docsify website
34
+ ${chalk.cyan('Generate llms.txt for AI agents')} (${chalk.gray('.c4builder → "generateLLMS"')})
35
+ Writes llms.txt (page index) and llms-full.txt (all pages with diagram sources) to the site root.
33
36
  ${chalk.cyan('Generate diagram images locally')}
34
37
  Uses the localy installed plantuml package to build svg images for each diagram.
35
38
  ${chalk.cyan('Replace diagrams with a link')}
@@ -39,7 +42,7 @@ Shows the original folder hierarchy after each title.
39
42
  ${chalk.cyan('Plugins')} (${chalk.gray('.c4builder → "plugins", no wizard prompt')})
40
43
  List of build plugins: "name" or ["name", { options }]. Built-in: openspec (local OpenSpec store →
41
44
  site section), openapi (swagger-ui pages for OpenAPI specs from a folder or an HTTP archive).
42
- See README «Plugins».
45
+ See the "Plugins" page of the documentation.
43
46
  ${chalk.cyan('Place diagrams before text')}
44
47
  Choose to place diagrams before or after the text.
45
48
  ${chalk.cyan('Exclude other files')}
@@ -13,22 +13,22 @@ export default async (args = [], { force = false } = {}) => {
13
13
  return;
14
14
  }
15
15
  if (action !== 'install') {
16
- console.log(chalk.red(`неизвестная подкоманда: c4builder jre ${action || ''}`.trim()));
17
- console.log(chalk.gray('доступно: c4builder jre install [--force] | jre info'));
16
+ console.log(chalk.red(`unknown subcommand: c4builder jre ${action || ''}`.trim()));
17
+ console.log(chalk.gray('available: c4builder jre install [--force] | jre info'));
18
18
  process.exit(1);
19
19
  }
20
20
  try {
21
21
  if (!force) {
22
22
  const sys = detectSystemJava();
23
23
  if (sys) {
24
- console.log(chalk.green(`системная java годна (v${sys.major}): ${sys.path}`));
25
- console.log(chalk.gray('скачивание не требуется — для форс-загрузки: c4builder jre install --force'));
24
+ console.log(chalk.green(`system java is suitable (v${sys.major}): ${sys.path}`));
25
+ console.log(chalk.gray('no download needed — to force it: c4builder jre install --force'));
26
26
  return;
27
27
  }
28
28
  }
29
29
  const resolved = await resolveJava({ force, log: (m) => console.log(chalk.gray(m)) });
30
- const from = resolved.source === 'download' ? `Temurin ${TEMURIN_FEATURE}, скачан` : resolved.source;
31
- console.log(chalk.green(`JRE готов (${from}): ${resolved.path}`));
30
+ const from = resolved.source === 'download' ? `Temurin ${TEMURIN_FEATURE}, downloaded` : resolved.source;
31
+ console.log(chalk.green(`JRE ready (${from}): ${resolved.path}`));
32
32
  }
33
33
  catch (e) {
34
34
  console.log(chalk.red(e.message));
@@ -38,6 +38,9 @@ Generate website: ${currentConfiguration.GENERATE_WEBSITE !== undefined
38
38
  ? chalk.green(currentConfiguration.DOCSIFY_TEMPLATE)
39
39
  : chalk.red('not set')}`
40
40
  : ''}
41
+ Generate llms.txt for AI agents: ${currentConfiguration.GENERATE_LLMS !== undefined
42
+ ? chalk.green(currentConfiguration.GENERATE_LLMS)
43
+ : chalk.red('not set')}
41
44
  Repository Url: ${currentConfiguration.REPO_NAME ? chalk.green(currentConfiguration.REPO_NAME) : chalk.red('not set')}
42
45
  Include breadcrumbs: ${currentConfiguration.INCLUDE_BREADCRUMBS !== undefined
43
46
  ? chalk.green(currentConfiguration.INCLUDE_BREADCRUMBS)
@@ -80,7 +83,7 @@ Plugins: ${currentConfiguration.PLUGINS?.length
80
83
  // Битые ключи показаны выше как «not set» (щадящий разбор их выбросил); помечаем их
81
84
  // сырым значением и причиной, чтобы было видно, что и почему требует правки/визарда.
82
85
  if (issues.length) {
83
- console.log(chalk.yellow('⚠ Невалидные ключи в .c4builder (показаны как «not set», чините через `c4builder --config`):'));
86
+ console.log(chalk.yellow('⚠ Invalid keys in .c4builder (shown as "not set", fix them with `c4builder --config`):'));
84
87
  for (const { key, value, message } of issues)
85
88
  console.log(chalk.yellow(` ⚠ ${key}: ${JSON.stringify(value)} — ${message}`));
86
89
  console.log('');
@@ -11,16 +11,16 @@ import { TEMPLATE_DIR } from "../../util/paths.js";
11
11
  // Интерактив показывает её и переспрашивает; --yes падает с ней (exit≠0), без ре-промпта.
12
12
  const validateProjectName = (name) => {
13
13
  if (!name?.trim())
14
- return 'имя проекта не задано';
14
+ return 'project name is not set';
15
15
  if (name.indexOf('/') !== -1 || name.indexOf('\\') !== -1)
16
- return 'имя проекта не должно содержать «/» или «\\»';
16
+ return 'project name must not contain "/" or "\\"';
17
17
  const target = path.join(process.cwd(), name);
18
18
  if (fs.existsSync(target)) {
19
19
  // readdirSync по файлу бросает ENOTDIR — сперва различаем файл и каталог.
20
20
  if (!fs.statSync(target).isDirectory())
21
- return `«${name}» уже существует и является файлом`;
21
+ return `"${name}" already exists and is a file`;
22
22
  if (fs.readdirSync(target).length > 0)
23
- return `папка «${name}» уже существует и не пуста`;
23
+ return `folder "${name}" already exists and is not empty`;
24
24
  }
25
25
  return null;
26
26
  };
@@ -34,13 +34,13 @@ export default async (opts = {}) => {
34
34
  if (opts.name) {
35
35
  const err = validateProjectName(opts.name);
36
36
  if (err) {
37
- console.log(chalk.red(`ОШИБКА: ${err}`));
37
+ console.log(chalk.red(`ERROR: ${err}`));
38
38
  process.exit(1);
39
39
  }
40
40
  projectName = opts.name;
41
41
  }
42
42
  else if (nonInteractive) {
43
- console.log(chalk.red('ОШИБКА: режим --yes требует --name <name>'));
43
+ console.log(chalk.red('ERROR: --yes requires --name <name>'));
44
44
  process.exit(1);
45
45
  }
46
46
  else {
@@ -4,6 +4,9 @@ import fs from 'node:fs';
4
4
  import express from 'express';
5
5
  import open from 'open';
6
6
  const LIVERELOAD_PATH = '/__livereload';
7
+ // Восстановление скролла после reload: docsify рендерит контент асинхронно после fetch md,
8
+ // высота страницы доступна не сразу — поллим до достижения нужной y или таймаута.
9
+ // Комментарии держим здесь, а не в сниппете: он уходит в браузер пользователя.
7
10
  const LIVERELOAD_SNIPPET = `
8
11
  <script>
9
12
  (function () {
@@ -16,8 +19,6 @@ const LIVERELOAD_SNIPPET = `
16
19
  sessionStorage.removeItem(KEY);
17
20
  var data = JSON.parse(saved);
18
21
  if (data && data.hash === location.hash && typeof data.y === 'number') {
19
- // docsify рендерит контент асинхронно после fetch md,
20
- // высота страницы доступна не сразу — поллим до достижения нужной y или таймаута.
21
22
  var deadline = Date.now() + 2000;
22
23
  var tryRestore = function () {
23
24
  var maxScroll = document.documentElement.scrollHeight - window.innerHeight;
@@ -21,9 +21,12 @@ import { acquireBuildLock, BuildLockHeldError } from "../util/lock.js";
21
21
  // переживать ребилды (его пересоздание — секунды), гасим его только когда сборок
22
22
  // больше не будет (одиночный запуск).
23
23
  import { teardownD2 } from "../core/render/d2renderer.js";
24
+ import { teardownBpmn } from "../core/render/bpmnrenderer.js";
24
25
  import { packageJson as pkg } from "../util/paths.js";
25
26
  import { outputDirs, parseConfig } from "../config/options.js";
26
27
  import { isValidPort } from "../config/schema.js";
28
+ // Позиционные подкоманды: allowExcessArguments пропускает любые, список — единственный фильтр.
29
+ const COMMANDS = ['check', 'jre', 'site', 'new', 'config'];
27
30
  const intro = () => {
28
31
  console.log(chalk.blue(figlet.textSync('c4builder')));
29
32
  console.log(chalk.gray('Blow up your software documentation writing skills'));
@@ -47,7 +50,7 @@ function readLenientConfig(conf) {
47
50
  // Строгий путь (сборка) не терпит битого конфига: печатает внятную ошибку и завершает
48
51
  // процесс с ненулевым кодом. Держим решение о выходе здесь, в CLI, а не в config/*.
49
52
  function failOnConfigIssues(issues) {
50
- console.error(chalk.red('Ошибка в .c4builder — невалидные значения не дают собрать документацию:'));
53
+ console.error(chalk.red('.c4builder error — invalid values prevent building the documentation:'));
51
54
  for (const { key, message } of issues)
52
55
  console.error(chalk.red(` ${key} — ${message}`));
53
56
  process.exit(1);
@@ -55,7 +58,7 @@ function failOnConfigIssues(issues) {
55
58
  // Щадящий путь (--config/первый запуск визарда) предупреждает о битых ключах: они
56
59
  // трактуются как «не задано» и будут перезаписаны валидными значениями после визарда.
57
60
  function warnConfigIssues(issues) {
58
- console.log(chalk.yellow('\n⚠ Невалидные ключи в .c4builder (трактуются как «не задано», визард их перезапишет):'));
61
+ console.log(chalk.yellow('\n⚠ Invalid keys in .c4builder (treated as "not set", the wizard will overwrite them):'));
59
62
  for (const { key, value, message } of issues)
60
63
  console.log(chalk.yellow(` ⚠ ${key}: ${JSON.stringify(value)} — ${message}`));
61
64
  }
@@ -67,6 +70,7 @@ function getOptions(c) {
67
70
  GENERATE_MD: c.generateMD,
68
71
  GENERATE_WEBSITE: c.generateWEB,
69
72
  GENERATE_COMPLETE_MD_FILE: c.generateCompleteMD,
73
+ GENERATE_LLMS: c.generateLLMS,
70
74
  // Легаси-детект: PDF-вывод удалён, но truthy-ключи в старых .c4builder
71
75
  // ловим здесь (где доступен conf) и отдаём build.js для предупреждения.
72
76
  LEGACY_PDF_KEYS: ['generatePDF', 'generateCompletePDF'].filter((k) => c[k]),
@@ -119,27 +123,43 @@ export default async () => {
119
123
  .option('-p, --port <n>', 'port used for serving the generated site', (v) => {
120
124
  // parseInt молча резал бы опечатку ('30OO' → 30) — валидируем строго.
121
125
  if (!isValidPort(v))
122
- throw new InvalidArgumentError('ожидается TCP-порт: целое число 1..65535');
126
+ throw new InvalidArgumentError('expected a TCP port: an integer 1..65535');
123
127
  return Number(v);
124
128
  })
125
129
  .allowExcessArguments() // позиционные аргументы подкоманд `jre <action>` и `check <file...>`
126
130
  .parse(process.argv);
127
131
  const opts = program.opts();
132
+ const [cmd, ...cmdArgs] = program.args;
133
+ // allowExcessArguments глотает любой позиционный аргумент — без этой проверки
134
+ // опечатка (`c4builder nwe`) молча превращалась бы в сборку.
135
+ if (cmd !== undefined && !COMMANDS.includes(cmd)) {
136
+ console.error(chalk.red(`unknown command: ${cmd}`));
137
+ console.error(`available commands: ${COMMANDS.join(', ')} (see c4builder --help for options)`);
138
+ process.exit(1);
139
+ }
128
140
  // Прогрев JRE — до загрузки конфига проекта и intro: команда самостоятельна.
129
- if (program.args[0] === 'jre')
130
- return cmdJre(program.args.slice(1), { force: opts.force });
141
+ if (cmd === 'jre')
142
+ return cmdJre(cmdArgs, { force: opts.force });
131
143
  // Проверка отдельных диаграмм — тоже без конфига проекта.
132
- if (program.args[0] === 'check')
133
- return cmdCheck(program.args.slice(1));
134
- // `c4builder site` — подсказка после сборки печатает именно эту форму; без
135
- // этой строки allowExcessArguments молча глотал бы её и просто пересобирал.
136
- if (program.args[0] === 'site')
137
- opts.site = true;
144
+ if (cmd === 'check')
145
+ return cmdCheck(cmdArgs);
146
+ // `site`/`new`/`config` — позиционные синонимы одноимённых флагов: эту форму печатают
147
+ // подсказки CLI и учат README, шаблон и скиллы.
148
+ if (cmd)
149
+ opts[cmd] = true;
150
+ // Справочные команды не должны оставлять файлов: конструктор Configstore сразу пишет
151
+ // .c4builder и .c4builder.cache, и следующий запуск считал бы каталог проектом.
152
+ if (opts.docs)
153
+ return cmdHelp();
154
+ const configPath = path.join(process.cwd(), opts.configFile ?? '.c4builder');
155
+ if ((opts.list || opts.reset) && !opts.new && !fs.existsSync(configPath)) {
156
+ console.error(chalk.red(`no .c4builder in ${process.cwd()} — run inside a project or create one with c4builder new`));
157
+ process.exit(1);
158
+ }
138
159
  let conf = { get: () => { } };
139
160
  let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
140
161
  // Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
141
162
  let lockPath = '';
142
- const configPath = path.join(process.cwd(), opts.configFile ?? '.c4builder');
143
163
  if (!opts.new) {
144
164
  const projectKey = process.cwd().split(path.sep).splice(1).join('_');
145
165
  lockPath = `${configPath}.lock`;
@@ -155,8 +175,6 @@ export default async () => {
155
175
  conf.delete('checksums');
156
176
  }
157
177
  }
158
- if (opts.docs)
159
- return cmdHelp();
160
178
  // --reset чистит .c4builder при ЛЮБОМ его содержимом — до строгой проверки конфига,
161
179
  // чтобы сломанное значение не мешало собственному сбросу.
162
180
  if (opts.reset && !opts.new) {
@@ -175,7 +193,7 @@ export default async () => {
175
193
  clearConsole();
176
194
  intro();
177
195
  if (!currentConfig.HAS_RUN && !opts.new) {
178
- console.log(`\nif you created the project using the 'c4model new' command you can just press enter and go with the default options to get a basic idea of how it works.\n`);
196
+ console.log(`\nif you created the project using the 'c4builder new' command you can just press enter and go with the default options to get a basic idea of how it works.\n`);
179
197
  console.log(`you can always change the configuration by running > c4builder config\n`);
180
198
  }
181
199
  if (opts.new)
@@ -221,7 +239,7 @@ export default async () => {
221
239
  options.USE_SYSTEM_FONTS = true; // флаг сильнее ключа конфига
222
240
  // Плагины грузятся и валидируются до первой сборки: битые опции — exit 1 сразу.
223
241
  const plugins = await loadPlugins(options.PLUGINS, process.cwd(), options).catch((e) => {
224
- console.error(chalk.red(`Ошибка загрузки плагинов: ${e.message}`));
242
+ console.error(chalk.red(`failed to load plugins: ${e.message}`));
225
243
  return process.exit(1);
226
244
  });
227
245
  const reloadEmitter = new EventEmitter();
@@ -274,7 +292,7 @@ export default async () => {
274
292
  attemptedWatchBuild = false;
275
293
  if (err instanceof BuildLockHeldError) {
276
294
  // Соседняя сборка ещё идёт — ребилд пропущен, watch жив.
277
- console.log(chalk.yellow(`ребилд пропущен: ${err.message}`));
295
+ console.log(chalk.yellow(`rebuild skipped: ${err.message}`));
278
296
  }
279
297
  else {
280
298
  const e = err;
@@ -305,8 +323,10 @@ export default async () => {
305
323
  release();
306
324
  // Одиночная сборка: D2-воркер больше не нужен, без teardown он держал бы
307
325
  // процесс. В watch-режиме воркер живёт до конца процесса (Ctrl+C).
308
- if (!opts.watch)
326
+ if (!opts.watch) {
309
327
  await teardownD2();
328
+ await teardownBpmn();
329
+ }
310
330
  }
311
331
  isBuilding = false;
312
332
  if (opts.site)
@@ -9,7 +9,7 @@ const nonEmptyString = z.string().trim().min(1);
9
9
  // Валидатор ответа визарда: true при успешном safeParse, иначе текст ошибки для inquirer.
10
10
  // Пустой Enter уже подставил дефолт (answer = value || default) — сообщение получают
11
11
  // только явно введённые пробелы/пустая строка при отсутствии дефолта.
12
- const validate = (schema, message = 'значение обязательно') => (answer) => schema.safeParse(answer).success || message;
12
+ const validate = (schema, message = 'a value is required') => (answer) => schema.safeParse(answer).success || message;
13
13
  // Пункт default-списка чекбокса inquirer: ключ отмечен, если он включён в текущем
14
14
  // конфиге, а при незаданном значении — если включён по умолчанию.
15
15
  const checkedKey = (current, dflt, key) => (current === undefined ? dflt : current) ? key : null;
@@ -23,7 +23,7 @@ export default async (currentConfiguration, conf, program) => {
23
23
  name: 'projectName',
24
24
  message: 'Project Name',
25
25
  default: currentConfiguration.PROJECT_NAME || path.parse(process.cwd()).name,
26
- validate: validate(nonEmptyString, 'укажите имя проекта')
26
+ validate: validate(nonEmptyString, 'enter a project name')
27
27
  });
28
28
  conf.set('projectName', responses.projectName);
29
29
  }
@@ -33,7 +33,7 @@ export default async (currentConfiguration, conf, program) => {
33
33
  name: 'homepageName',
34
34
  message: 'HomePage Name',
35
35
  default: currentConfiguration.HOMEPAGE_NAME || defaultConfig.homepageName,
36
- validate: validate(nonEmptyString, 'укажите имя главной страницы')
36
+ validate: validate(nonEmptyString, 'enter a home page name')
37
37
  });
38
38
  conf.set('homepageName', responses.homepageName);
39
39
  }
@@ -45,17 +45,17 @@ export default async (currentConfiguration, conf, program) => {
45
45
  default: currentConfiguration.ROOT_FOLDER || defaultConfig.rootFolder,
46
46
  validate: (answer) => {
47
47
  if (!nonEmptyString.safeParse(answer).success)
48
- return 'укажите папку с документацией';
48
+ return 'enter the documentation folder';
49
49
  if (answer.indexOf('/') !== -1 || answer.indexOf('\\') !== -1)
50
- return 'имя папки не должно содержать «/» или «\\»';
50
+ return 'folder name must not contain "/" or "\\"';
51
51
  // statSync бросает ENOENT для несуществующего пути — ловим и превращаем в
52
52
  // сообщение, иначе async-хендлер inquirer уронит процесс сырым стектрейсом.
53
53
  try {
54
54
  if (!fs.statSync(path.join(process.cwd(), answer)).isDirectory())
55
- return 'указанный путь не является каталогом';
55
+ return 'the path is not a directory';
56
56
  }
57
57
  catch {
58
- return 'папка не найдена';
58
+ return 'folder not found';
59
59
  }
60
60
  return true;
61
61
  }
@@ -88,7 +88,8 @@ export default async (currentConfiguration, conf, program) => {
88
88
  const defaults = [
89
89
  checkedKey(currentConfiguration.GENERATE_MD, defaultConfig.generateMD, 'generateMD'),
90
90
  checkedKey(currentConfiguration.GENERATE_COMPLETE_MD_FILE, defaultConfig.generateCompleteMD, 'generateCompleteMD'),
91
- checkedKey(currentConfiguration.GENERATE_WEBSITE, defaultConfig.generateWEB, 'generateWEB')
91
+ checkedKey(currentConfiguration.GENERATE_WEBSITE, defaultConfig.generateWEB, 'generateWEB'),
92
+ checkedKey(currentConfiguration.GENERATE_LLMS, defaultConfig.generateLLMS, 'generateLLMS')
92
93
  ];
93
94
  responses = await inquirer.prompt({
94
95
  type: 'checkbox',
@@ -107,12 +108,17 @@ export default async (currentConfiguration, conf, program) => {
107
108
  {
108
109
  name: 'Generate website',
109
110
  value: 'generateWEB'
111
+ },
112
+ {
113
+ name: 'llms.txt for AI agents (needs website)',
114
+ value: 'generateLLMS'
110
115
  }
111
116
  ]
112
117
  });
113
118
  conf.set('generateMD', responses.generate.includes('generateMD'));
114
119
  conf.set('generateCompleteMD', responses.generate.includes('generateCompleteMD'));
115
120
  conf.set('generateWEB', responses.generate.includes('generateWEB'));
121
+ conf.set('generateLLMS', responses.generate.includes('generateLLMS'));
116
122
  if (responses.generate.includes('generateMD')) {
117
123
  let mdOptions = await inquirer.prompt({
118
124
  type: 'confirm',
@@ -244,7 +250,7 @@ export default async (currentConfiguration, conf, program) => {
244
250
  name: 'plantumlServerUrl',
245
251
  message: 'PlantUML Server URL',
246
252
  default: currentConfiguration.PLANTUML_SERVER_URL || defaultConfig.plantumlServerUrl,
247
- validate: validate(nonEmptyString, 'укажите URL сервера PlantUML')
253
+ validate: validate(nonEmptyString, 'enter the PlantUML server URL')
248
254
  });
249
255
  conf.set('plantumlServerUrl', responses.plantumlServerUrl);
250
256
  }
@@ -254,7 +260,7 @@ export default async (currentConfiguration, conf, program) => {
254
260
  name: 'diagramFormat',
255
261
  message: 'Diagram Image Format',
256
262
  default: currentConfiguration.DIAGRAM_FORMAT || defaultConfig.diagramFormat,
257
- validate: validate(nonEmptyString, 'укажите формат изображения диаграмм')
263
+ validate: validate(nonEmptyString, 'enter the diagram image format')
258
264
  });
259
265
  conf.set('diagramFormat', responses.diagramFormat);
260
266
  }
@@ -12,6 +12,7 @@ const defaultConfig = {
12
12
  generateMD: true,
13
13
  generateCompleteMD: false,
14
14
  generateWEB: true,
15
+ generateLLMS: true,
15
16
  includeNavigation: false,
16
17
  includeTableOfContents: true,
17
18
  webTheme: 'vendor/vue.css',
@@ -24,7 +24,7 @@ export function parseConfig(raw) {
24
24
  const issues = [];
25
25
  const brokenKeys = new Set();
26
26
  for (const issue of parsed.error.issues) {
27
- const key = issue.path.length ? String(issue.path[0]) : '(корень)';
27
+ const key = issue.path.length ? String(issue.path[0]) : '(root)';
28
28
  if (brokenKeys.has(key))
29
29
  continue; // один ключ — одно сообщение (напр. массив с N битых элементов)
30
30
  brokenKeys.add(key);
@@ -32,6 +32,9 @@ export const configSchema = z.object({
32
32
  generateMD: bool(defaultConfig.generateMD),
33
33
  generateCompleteMD: bool(defaultConfig.generateCompleteMD),
34
34
  generateWEB: bool(defaultConfig.generateWEB),
35
+ // Намеренно не defaultConfig.generateLLMS: отсутствующий ключ = выкл — легаси-конфиги
36
+ // не меняют вывод. Новые проекты получают true из defaultConfig (new --yes, wizard).
37
+ generateLLMS: bool(false),
35
38
  includeNavigation: bool(defaultConfig.includeNavigation),
36
39
  includeTableOfContents: bool(defaultConfig.includeTableOfContents),
37
40
  webTheme: str(defaultConfig.webTheme),
@@ -41,7 +44,7 @@ export const configSchema = z.object({
41
44
  docsifyTemplate: str(defaultConfig.docsifyTemplate),
42
45
  webPort: z.preprocess(strPre, z
43
46
  .string()
44
- .refine(isValidPort, 'ожидается TCP-порт: целое число 1..65535')
47
+ .refine(isValidPort, 'expected a TCP port: an integer 1..65535')
45
48
  .default(defaultConfig.webPort)),
46
49
  includeBreadcrumbs: bool(defaultConfig.includeBreadcrumbs),
47
50
  includeLinkToDiagram: bool(defaultConfig.includeLinkToDiagram),
@@ -66,7 +69,7 @@ export const configSchema = z.object({
66
69
  // проверяет лишь форму записи, чтобы ошибка называла `plugins` и позицию.
67
70
  plugins: nullish(z
68
71
  .array(z.union([z.string(), z.tuple([z.string(), z.record(z.string(), z.unknown())])], {
69
- error: 'ожидается "имя" либо ["имя", {опции}]'
72
+ error: 'expected "name" or ["name", {options}]'
70
73
  }))
71
74
  .default([])),
72
75
  // Легаси-ключи старых `.c4builder` — читаются лишь для однократного предупреждения.
@@ -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, writeOnSameLine } from "../util/utils.js";
4
+ import { makeDirectory, writeFile, writeOnSameLine } from "../util/utils.js";
5
5
  // clearD2FileCache: сброс кеша графа D2-импортов на границах сборки (сам webworker
6
6
  // D2 гасит CLI после одиночной сборки — в watch-режиме он переживает ребилды).
7
7
  import { clearD2FileCache } from "./render/d2renderer.js";
@@ -9,7 +9,7 @@ import { DIST_BACKUP_FOLDER_SUFFIX } from "../config/options.js";
9
9
  // Фазы сборки: scan (дерево исходников) → render (диаграммы) → compose (markdown/сайт).
10
10
  import { generateTree, engineSupportsRemote, clearIncludeCache } from "./scan/tree.js";
11
11
  import { generateImages } from "./render/diagrams.js";
12
- import { generateMD, generateWebMD, generateCompleteMD, clearDiagramCache } from "./compose/markdown.js";
12
+ import { generateMD, generateWebMD, generateCompleteMD, generateLlmsTxt, generateLlmsFull, clearDiagramCache } from "./compose/markdown.js";
13
13
  // Плагины: хуки afterScan (виртуальные страницы) / afterBuild и ассеты сайта.
14
14
  // Загружены заранее в cli/dispatch и приходят третьим аргументом (как cacheConf).
15
15
  import { runAfterScan, runAfterBuild } from "./plugins/hooks.js";
@@ -51,8 +51,8 @@ const build = async (options, cacheConf, plugins = []) => {
51
51
  if (!options.GENERATE_LOCAL_IMAGES) {
52
52
  const offline = new Set(tree.flatMap((item) => item.diagrams.filter((d) => !engineSupportsRemote(d.engine)).map((d) => d.engine)));
53
53
  if (offline.size) {
54
- throw new Error(`В проекте есть диаграммы без онлайн-сервера рендера (${[...offline].join(', ')}), ` +
55
- 'но generateLocalImages выключен — включите локальную генерацию изображений ' +
54
+ throw new Error(`The project has diagrams with no online render server (${[...offline].join(', ')}), ` +
55
+ 'but generateLocalImages is off — enable local image generation ' +
56
56
  '(generateLocalImages).');
57
57
  }
58
58
  }
@@ -75,6 +75,15 @@ const build = async (options, cacheConf, plugins = []) => {
75
75
  await generateWebMD(tree, options);
76
76
  await injectPluginAssets(plugins, options);
77
77
  }
78
+ // llms.txt ссылается на .md страниц сайта — без website ссылаться не на что.
79
+ if (options.GENERATE_LLMS && options.GENERATE_WEBSITE) {
80
+ console.log(chalk.blue('generating llms.txt'));
81
+ await writeFile(path.join(options.DIST_FOLDER, 'llms.txt'), generateLlmsTxt(tree, options));
82
+ await writeFile(path.join(options.DIST_FOLDER, 'llms-full.txt'), await generateLlmsFull(tree, options));
83
+ }
84
+ else if (options.GENERATE_LLMS) {
85
+ console.log(chalk.yellow('generateLLMS requires generateWEB — llms.txt not created'));
86
+ }
78
87
  if (options.GENERATE_COMPLETE_MD_FILE) {
79
88
  console.log(chalk.blue('generating complete markdown file'));
80
89
  await generateCompleteMD(tree, options);
@@ -84,7 +93,7 @@ const build = async (options, cacheConf, plugins = []) => {
84
93
  // остальные выходы (exit 0). Конфиг НЕ мутируем — только подсказка пользователю.
85
94
  if (options.LEGACY_PDF_KEYS?.length) {
86
95
  console.log(chalk.bold(chalk.yellow('WARNING:')));
87
- console.log(chalk.yellow(`PDF-вывод больше не поддерживается. Удалите вручную из .c4builder: ${options.LEGACY_PDF_KEYS.join(', ')}.`));
96
+ console.log(chalk.yellow(`PDF output is no longer supported. Remove manually from .c4builder: ${options.LEGACY_PDF_KEYS.join(', ')}.`));
88
97
  }
89
98
  await runAfterBuild(options, plugins);
90
99
  console.log(chalk.green(`built in ${(Date.now() - start_date.getTime()) / 1000} seconds`));
@@ -98,7 +107,7 @@ const build = async (options, cacheConf, plugins = []) => {
98
107
  await fsextra.removeSync(bkFolderName);
99
108
  }
100
109
  else if (options.GENERATE_LOCAL_IMAGES && fsextra.existsSync(bkFolderName)) {
101
- console.log(chalk.yellow(`\nсборка прервана — бэкап предыдущего билда сохранён в ./${bkFolderName}`));
110
+ console.log(chalk.yellow(`\nbuild aborted — the previous build is backed up in ./${bkFolderName}`));
102
111
  }
103
112
  // Пофайловые кеши нужны только внутри сборки — чистим и на выходе, чтобы
104
113
  // watch-процесс не держал base64 всех диаграмм в памяти между ребилдами.
@@ -18,7 +18,6 @@ export default (options) => {
18
18
  window.$docsify = ${JSON.stringify(options, null, 2)};
19
19
  </script>
20
20
  <script src="vendor/docsify.min.js"></script>
21
- <script src="vendor/docsify-plantuml.min.js"></script>
22
21
  <script src="vendor/zoom-image.min.js"></script>
23
22
  ${options.supportSearch ? `<script src="vendor/search.min.js"></script>` : ''}
24
23
  ${options.executeScript ? `<script src="vendor/swagger-ui-bundle.js"></script>` : ''}