@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.
- package/README.MD +69 -615
- package/dist/cli/commands/check.js +43 -26
- package/dist/cli/commands/help.js +5 -2
- package/dist/cli/commands/jre.js +6 -6
- package/dist/cli/commands/list.js +4 -1
- package/dist/cli/commands/new.js +6 -6
- package/dist/cli/commands/site.js +3 -2
- package/dist/cli/dispatch.js +38 -18
- package/dist/cli/wizard/collect.js +16 -10
- package/dist/config/defaults.js +1 -0
- package/dist/config/options.js +1 -1
- package/dist/config/schema.js +5 -2
- package/dist/core/build.js +15 -6
- package/dist/core/compose/docsify.template.js +0 -1
- package/dist/core/compose/markdown.js +85 -19
- package/dist/core/plugins/hooks.js +1 -1
- package/dist/core/plugins/load.js +6 -6
- package/dist/core/plugins/source.js +6 -6
- package/dist/core/plugins/tree.js +5 -5
- package/dist/core/render/bpmnrenderer.js +522 -0
- package/dist/core/render/d2renderer.js +19 -19
- package/dist/core/render/diagrams.js +64 -45
- package/dist/core/render/jre.js +14 -14
- package/dist/core/render/pngraster.js +4 -4
- package/dist/{plugins/openspec → core/scan}/fences.js +11 -10
- package/dist/core/scan/tree.js +37 -14
- package/dist/plugins/openapi/index.js +17 -7
- package/dist/plugins/openspec/index.js +1 -1
- package/dist/plugins/openspec/render.js +5 -2
- package/dist/util/archive.js +1 -1
- package/dist/util/http.js +3 -3
- package/dist/util/lock.js +3 -3
- package/dist/util/paths.js +1 -1
- package/package.json +9 -4
- package/template/AGENTS.md +52 -0
- package/template/readme.md +3 -3
- package/template/src/5 BPMN Example/account-opening.bpmn +69 -0
- package/template/src/5 BPMN Example/account-opening.md +7 -0
- 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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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://
|
|
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
|
|
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')}
|
package/dist/cli/commands/jre.js
CHANGED
|
@@ -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(
|
|
17
|
-
console.log(chalk.gray('
|
|
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(
|
|
25
|
-
console.log(chalk.gray('
|
|
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},
|
|
31
|
-
console.log(chalk.green(`JRE
|
|
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('⚠
|
|
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('');
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -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
|
|
21
|
+
return `"${name}" already exists and is a file`;
|
|
22
22
|
if (fs.readdirSync(target).length > 0)
|
|
23
|
-
return
|
|
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(
|
|
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('
|
|
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;
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -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('
|
|
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⚠
|
|
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('
|
|
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 (
|
|
130
|
-
return cmdJre(
|
|
141
|
+
if (cmd === 'jre')
|
|
142
|
+
return cmdJre(cmdArgs, { force: opts.force });
|
|
131
143
|
// Проверка отдельных диаграмм — тоже без конфига проекта.
|
|
132
|
-
if (
|
|
133
|
-
return cmdCheck(
|
|
134
|
-
// `
|
|
135
|
-
//
|
|
136
|
-
if (
|
|
137
|
-
opts
|
|
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 '
|
|
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(
|
|
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(
|
|
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 = '
|
|
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, '
|
|
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
|
}
|
package/dist/config/defaults.js
CHANGED
package/dist/config/options.js
CHANGED
|
@@ -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);
|
package/dist/config/schema.js
CHANGED
|
@@ -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, '
|
|
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` — читаются лишь для однократного предупреждения.
|
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, 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(
|
|
55
|
-
'
|
|
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
|
|
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(`\
|
|
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>` : ''}
|