@alfacapital-tech/c4builder 0.10.0-rc.1 → 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.
@@ -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)}`);
@@ -31,7 +31,25 @@ import { packageJson as pkg } from "../util/paths.js";
31
31
  import { outputDirs, parseConfig } from "../config/options.js";
32
32
  import { isValidPort } from "../config/schema.js";
33
33
  // Позиционные подкоманды: allowExcessArguments пропускает любые, список — единственный фильтр.
34
- const COMMANDS = ['check', 'jre', 'site', 'new', 'config', 'update', 'feedback', 'skills'];
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
+ };
35
53
  const intro = () => {
36
54
  console.log(chalk.blue(figlet.textSync('c4builder')));
37
55
  console.log(chalk.gray('Blow up your software documentation writing skills'));
@@ -113,6 +131,8 @@ function getOptions(c) {
113
131
  }
114
132
  export default async () => {
115
133
  program
134
+ .name('c4builder')
135
+ .usage('[command] [options]')
116
136
  .version(pkg.version)
117
137
  .option('--new', 'create a new project from template')
118
138
  .option('--name <name>', 'project name for --new (skips the name prompt)')
@@ -138,14 +158,15 @@ export default async () => {
138
158
  return Number(v);
139
159
  })
140
160
  .allowExcessArguments() // позиционные аргументы подкоманд `jre <action>` и `check <file...>`
161
+ .addHelpText('after', commandsHelp())
141
162
  .parse(process.argv);
142
163
  const opts = program.opts();
143
164
  const [cmd, ...cmdArgs] = program.args;
144
165
  // allowExcessArguments глотает любой позиционный аргумент — без этой проверки
145
166
  // опечатка (`c4builder nwe`) молча превращалась бы в сборку.
146
- if (cmd !== undefined && !COMMANDS.includes(cmd)) {
167
+ if (cmd !== undefined && !Object.hasOwn(COMMANDS, cmd)) {
147
168
  console.error(chalk.red(`unknown command: ${cmd}`));
148
- 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)`);
149
170
  process.exit(1);
150
171
  }
151
172
  // --version/--help уже вышли в parse. Уведомление — по кэшу прошлой проверки, до её
@@ -184,6 +205,11 @@ export default async () => {
184
205
  console.error(chalk.red(`no .c4builder in ${process.cwd()} — run inside a project or create one with c4builder new`));
185
206
  process.exit(1);
186
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
+ }
187
213
  let conf = { get: () => { } };
188
214
  let cacheConf = { get: () => { }, set: () => { }, clear: () => { } };
189
215
  // Лок сборки — рядом с конфигом проекта (см. util/lock.ts). Заполняется вместе с conf.
@@ -191,7 +217,15 @@ export default async () => {
191
217
  if (!opts.new) {
192
218
  const projectKey = process.cwd().split(path.sep).splice(1).join('_');
193
219
  lockPath = `${configPath}.lock`;
194
- 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
+ }
195
229
  cacheConf = new Configstore(`${projectKey}_cache`, {}, { configPath: `${configPath}.cache` });
196
230
  // Миграция: чексуммы раньше жили в .c4builder, переносим их в .c4builder.cache,
197
231
  // чтобы рабочий конфиг перестал «дёргаться» в git при каждой сборке.
@@ -367,8 +401,15 @@ export default async () => {
367
401
  }
368
402
  }
369
403
  isBuilding = false;
370
- if (opts.site)
371
- 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
+ }
372
413
  if (options.GENERATE_WEBSITE && !opts.watch) {
373
414
  console.log(chalk.gray('\nto view the generated website run'));
374
415
  console.log(`> c4builder site`);
@@ -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.10.0-rc.1",
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",
@@ -72,8 +72,8 @@ How the source tree becomes pages:
72
72
 
73
73
  With a complete `.c4builder` (written by `c4builder --new --name <name> -y` or by the
74
74
  first interactive run) this is a one-off, non-interactive build. A missing or partial
75
- config starts an interactive wizard — do not drive it, ask the user or write the full
76
- config (references/config.md). A diagram that fails here aborts the build with a long
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
77
  Java stack trace and keeps the previous output in `docs_bk/`; run `c4builder check` on
78
78
  the changed files to get the short message.
79
79
  4. **Look at the result** in `docs/`: `docs/<folder>/README.md` (markdown collection),