@cuongph.dev/dbdocgen 0.1.0 → 0.1.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.
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_commander = require("commander");
28
28
  var import_promises7 = require("fs/promises");
29
29
  var import_node_fs = require("fs");
30
- var import_node_path6 = require("path");
30
+ var import_node_path7 = require("path");
31
31
 
32
32
  // src/core/config/loader.ts
33
33
  var import_cosmiconfig = require("cosmiconfig");
@@ -53,7 +53,7 @@ var outputLanguageSchema = import_zod.z.enum(["en", "jp"]);
53
53
  var dbdocgenConfigSchema = import_zod.z.object({
54
54
  schema: import_zod.z.string().default("./schema.sql"),
55
55
  dialect: dialectSchema.optional(),
56
- outDir: import_zod.z.string().default("./docs/db"),
56
+ outDir: import_zod.z.string().default("./output"),
57
57
  output: import_zod.z.object({
58
58
  formats: import_zod.z.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
59
59
  language: outputLanguageSchema.default("en")
@@ -160,6 +160,10 @@ var LABELS = {
160
160
  type: "Type",
161
161
  required: "Required",
162
162
  defaultValue: "Default Value",
163
+ size: "Size",
164
+ minValue: "Min",
165
+ maxValue: "Max",
166
+ unique: "Unique",
163
167
  notes: "Notes",
164
168
  yes: "Yes",
165
169
  no: "No",
@@ -188,7 +192,15 @@ var LABELS = {
188
192
  rowNo: "#",
189
193
  backToOverview: "\u2190 Overview",
190
194
  pkMarker: "PK",
191
- fkMarker: "FK"
195
+ fkMarker: "FK",
196
+ erDiagramHeading: "ER Diagram",
197
+ erDiagramSheet: "ER Diagram",
198
+ viewErDiagram: "View interactive ER diagram (html/er-diagram.html)",
199
+ zoomIn: "Zoom in",
200
+ zoomOut: "Zoom out",
201
+ zoomReset: "Reset",
202
+ zoomFit: "Fit",
203
+ panZoomHint: "Drag to pan \xB7 Scroll to zoom"
192
204
  },
193
205
  jp: {
194
206
  docTitle: "Database Documentation",
@@ -208,6 +220,10 @@ var LABELS = {
208
220
  type: "\u578B",
209
221
  required: "\u5FC5\u9808",
210
222
  defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
223
+ size: "\u6841\u6570",
224
+ minValue: "\u6700\u5C0F\u5024",
225
+ maxValue: "\u6700\u5927\u5024",
226
+ unique: "\u4E00\u610F",
211
227
  notes: "\u5099\u8003",
212
228
  yes: "Yes",
213
229
  no: "No",
@@ -236,13 +252,533 @@ var LABELS = {
236
252
  rowNo: "No.",
237
253
  backToOverview: "\u2190 \u4E00\u89A7",
238
254
  pkMarker: "PK",
239
- fkMarker: "FK"
255
+ fkMarker: "FK",
256
+ erDiagramHeading: "ER Diagram",
257
+ erDiagramSheet: "ER Diagram",
258
+ viewErDiagram: "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6ER\u56F3 (html/er-diagram.html)",
259
+ zoomIn: "\u62E1\u5927",
260
+ zoomOut: "\u7E2E\u5C0F",
261
+ zoomReset: "\u30EA\u30BB\u30C3\u30C8",
262
+ zoomFit: "\u5168\u4F53\u8868\u793A",
263
+ panZoomHint: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5 \xB7 \u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u62E1\u5927\u7E2E\u5C0F"
240
264
  }
241
265
  };
242
266
  function getOutputLabels(language = "en") {
243
267
  return LABELS[language];
244
268
  }
245
269
 
270
+ // src/exporters/shared/column-definition.ts
271
+ var A5_COLUMN_COUNT = 10;
272
+ function columnDefinitionHeaders(labels) {
273
+ return [
274
+ labels.physicalName,
275
+ labels.logicalName,
276
+ labels.type,
277
+ labels.size,
278
+ labels.required,
279
+ labels.defaultValue,
280
+ labels.minValue,
281
+ labels.maxValue,
282
+ labels.unique,
283
+ labels.notes
284
+ ];
285
+ }
286
+ function formatColumnNotes(column, labels) {
287
+ const parts = [];
288
+ if (column.isPrimaryKey) parts.push(labels.pkMarker);
289
+ if (column.isForeignKey) parts.push(labels.fkMarker);
290
+ if (column.constraintNotes?.length) parts.push(...column.constraintNotes);
291
+ if (column.description?.value) parts.push(column.description.value);
292
+ return parts.join(", ") || labels.none;
293
+ }
294
+ function columnDefinitionRow(column, labels) {
295
+ return [
296
+ column.name,
297
+ column.comment ?? "",
298
+ column.type,
299
+ column.size ?? labels.none,
300
+ column.nullable ? labels.no : labels.yes,
301
+ column.defaultValue ?? labels.none,
302
+ column.minValue ?? labels.none,
303
+ column.maxValue ?? labels.none,
304
+ column.isUnique ? labels.yes : labels.no,
305
+ formatColumnNotes(column, labels)
306
+ ];
307
+ }
308
+
309
+ // src/exporters/diagram/er-diagram-embed.ts
310
+ function getErDiagramMermaid(doc) {
311
+ return renderMermaid(doc);
312
+ }
313
+ function renderErDiagramHtmlPage(mermaidSource, labels) {
314
+ const escaped = mermaidSource.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
315
+ return `<!DOCTYPE html>
316
+ <html lang="en">
317
+ <head>
318
+ <meta charset="UTF-8">
319
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
320
+ <title>${esc(labels.erDiagramHeading)}</title>
321
+ <style>
322
+ body { margin: 0; font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif; background: #f3f4f6; }
323
+ .toolbar {
324
+ background: #4472c4; color: #fff; padding: 10px 16px;
325
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
326
+ }
327
+ .toolbar a { color: #fff; text-decoration: underline; }
328
+ .toolbar .spacer { flex: 1; }
329
+ .toolbar .hint { opacity: 0.9; font-size: 13px; }
330
+ .toolbar button {
331
+ background: #fff; color: #2f5597; border: none; border-radius: 4px;
332
+ padding: 6px 12px; font-size: 13px; cursor: pointer; font-weight: 600;
333
+ }
334
+ .toolbar button:hover { background: #e8eef8; }
335
+ .viewport {
336
+ position: relative; height: calc(100vh - 52px); margin: 12px;
337
+ background: #fff; border: 1px solid #bfc7d4; border-radius: 4px;
338
+ overflow: hidden; cursor: grab; touch-action: none;
339
+ }
340
+ .viewport.dragging { cursor: grabbing; }
341
+ .canvas {
342
+ position: absolute; left: 0; top: 0; transform-origin: 0 0;
343
+ padding: 24px;
344
+ }
345
+ .mermaid { min-width: 320px; }
346
+ .mermaid svg { max-width: none !important; height: auto !important; }
347
+ </style>
348
+ </head>
349
+ <body>
350
+ <div class="toolbar">
351
+ <strong>${esc(labels.erDiagramHeading)}</strong>
352
+ <a href="index.html">\u2190 ${esc(labels.tableListHeading)}</a>
353
+ <span class="spacer"></span>
354
+ <span class="hint">${esc(labels.panZoomHint)}</span>
355
+ <button type="button" id="zoom-out" title="${esc(labels.zoomOut)}">\u2212</button>
356
+ <button type="button" id="zoom-reset" title="${esc(labels.zoomReset)}">${esc(labels.zoomReset)}</button>
357
+ <button type="button" id="zoom-in" title="${esc(labels.zoomIn)}">+</button>
358
+ <button type="button" id="zoom-fit" title="${esc(labels.zoomFit)}">${esc(labels.zoomFit)}</button>
359
+ </div>
360
+ <div class="viewport" id="viewport">
361
+ <div class="canvas" id="canvas">
362
+ <pre class="mermaid">${escaped}</pre>
363
+ </div>
364
+ </div>
365
+ <script type="module">
366
+ import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
367
+
368
+ mermaid.initialize({
369
+ startOnLoad: false,
370
+ theme: "default",
371
+ er: { useMaxWidth: false }
372
+ });
373
+
374
+ await mermaid.run({ querySelector: ".mermaid" });
375
+ setupPanZoom(
376
+ document.getElementById("viewport"),
377
+ document.getElementById("canvas")
378
+ );
379
+
380
+ function setupPanZoom(viewport, canvas) {
381
+ let scale = 1;
382
+ let tx = 40;
383
+ let ty = 40;
384
+ let dragging = false;
385
+ let lastX = 0;
386
+ let lastY = 0;
387
+
388
+ function apply() {
389
+ canvas.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
390
+ }
391
+
392
+ function zoomAt(factor, cx, cy) {
393
+ const next = Math.min(4, Math.max(0.15, scale * factor));
394
+ const ratio = next / scale;
395
+ tx = cx - (cx - tx) * ratio;
396
+ ty = cy - (cy - ty) * ratio;
397
+ scale = next;
398
+ apply();
399
+ }
400
+
401
+ function fitToView() {
402
+ const svg = canvas.querySelector("svg");
403
+ if (!svg) return;
404
+ const box = svg.getBBox();
405
+ const pad = 32;
406
+ const vw = viewport.clientWidth;
407
+ const vh = viewport.clientHeight;
408
+ scale = Math.min(
409
+ (vw - pad * 2) / Math.max(box.width, 1),
410
+ (vh - pad * 2) / Math.max(box.height, 1),
411
+ 1.5
412
+ );
413
+ tx = (vw - box.width * scale) / 2 - box.x * scale;
414
+ ty = (vh - box.height * scale) / 2 - box.y * scale;
415
+ apply();
416
+ }
417
+
418
+ viewport.addEventListener("wheel", (e) => {
419
+ e.preventDefault();
420
+ const rect = viewport.getBoundingClientRect();
421
+ const cx = e.clientX - rect.left;
422
+ const cy = e.clientY - rect.top;
423
+ zoomAt(e.deltaY < 0 ? 1.12 : 0.89, cx, cy);
424
+ }, { passive: false });
425
+
426
+ viewport.addEventListener("mousedown", (e) => {
427
+ if (e.button !== 0) return;
428
+ dragging = true;
429
+ lastX = e.clientX;
430
+ lastY = e.clientY;
431
+ viewport.classList.add("dragging");
432
+ });
433
+
434
+ window.addEventListener("mousemove", (e) => {
435
+ if (!dragging) return;
436
+ tx += e.clientX - lastX;
437
+ ty += e.clientY - lastY;
438
+ lastX = e.clientX;
439
+ lastY = e.clientY;
440
+ apply();
441
+ });
442
+
443
+ window.addEventListener("mouseup", () => {
444
+ dragging = false;
445
+ viewport.classList.remove("dragging");
446
+ });
447
+
448
+ document.getElementById("zoom-in").addEventListener("click", () => {
449
+ zoomAt(1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
450
+ });
451
+ document.getElementById("zoom-out").addEventListener("click", () => {
452
+ zoomAt(1 / 1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
453
+ });
454
+ document.getElementById("zoom-reset").addEventListener("click", () => {
455
+ scale = 1;
456
+ tx = 40;
457
+ ty = 40;
458
+ apply();
459
+ });
460
+ document.getElementById("zoom-fit").addEventListener("click", fitToView);
461
+
462
+ fitToView();
463
+ }
464
+ </script>
465
+ </body>
466
+ </html>`;
467
+ }
468
+ function renderErDiagramMarkdown(mermaidSource, labels) {
469
+ return [
470
+ `# ${labels.erDiagramHeading}`,
471
+ "",
472
+ "```mermaid",
473
+ mermaidSource.trimEnd(),
474
+ "```",
475
+ ""
476
+ ].join("\n");
477
+ }
478
+ function esc(text) {
479
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
480
+ }
481
+
482
+ // src/exporters/diagram/er-diagram-layout.ts
483
+ var import_elk_bundled = __toESM(require("elkjs/lib/elk.bundled.js"), 1);
484
+ var BOX_W = 200;
485
+ var HEADER_H = 28;
486
+ var LINE_H = 14;
487
+ var MAX_COLS_SHOWN = 6;
488
+ var COMPACT_THRESHOLD = 18;
489
+ var PAD = 24;
490
+ var CLUSTER_GAP = 56;
491
+ function isCompactLayout(tableCount) {
492
+ return tableCount >= COMPACT_THRESHOLD;
493
+ }
494
+ function getVisibleErColumns(table) {
495
+ const prioritized = [
496
+ ...table.columns.filter((column) => column.isPrimaryKey),
497
+ ...table.columns.filter(
498
+ (column) => column.isForeignKey && !column.isPrimaryKey
499
+ ),
500
+ ...table.columns.filter(
501
+ (column) => !column.isPrimaryKey && !column.isForeignKey
502
+ )
503
+ ];
504
+ const unique = prioritized.filter(
505
+ (column, index, columns) => columns.findIndex((item) => item.name === column.name) === index
506
+ );
507
+ return unique.slice(0, MAX_COLS_SHOWN);
508
+ }
509
+ function measureTableBox(table, _compact = false) {
510
+ const visible = getVisibleErColumns(table);
511
+ const extra = table.columns.length > visible.length ? 1 : 0;
512
+ return {
513
+ w: BOX_W,
514
+ h: HEADER_H + (visible.length + extra) * LINE_H + 8
515
+ };
516
+ }
517
+ function buildAdjacency(doc) {
518
+ const names = new Set(doc.tables.map((t) => t.name));
519
+ const adj = /* @__PURE__ */ new Map();
520
+ for (const name of names) adj.set(name, /* @__PURE__ */ new Set());
521
+ for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
522
+ if (!names.has(rel.fromTable) || !names.has(rel.toTable)) continue;
523
+ adj.get(rel.fromTable).add(rel.toTable);
524
+ adj.get(rel.toTable).add(rel.fromTable);
525
+ }
526
+ return adj;
527
+ }
528
+ function connectedComponents(tableNames, adj) {
529
+ const visited = /* @__PURE__ */ new Set();
530
+ const components = [];
531
+ for (const name of tableNames) {
532
+ if (visited.has(name)) continue;
533
+ const stack = [name];
534
+ const component = [];
535
+ visited.add(name);
536
+ while (stack.length > 0) {
537
+ const current = stack.pop();
538
+ component.push(current);
539
+ for (const next of adj.get(current) ?? []) {
540
+ if (!visited.has(next)) {
541
+ visited.add(next);
542
+ stack.push(next);
543
+ }
544
+ }
545
+ }
546
+ component.sort();
547
+ components.push(component);
548
+ }
549
+ return components.sort((a, b) => b.length - a.length);
550
+ }
551
+ function sectionToPoints(section) {
552
+ return [section.startPoint, ...section.bendPoints ?? [], section.endPoint];
553
+ }
554
+ function extractEdges(layouted) {
555
+ const edges = [];
556
+ for (const edge of layouted.edges ?? []) {
557
+ for (const section of edge.sections ?? []) {
558
+ edges.push({
559
+ id: edge.id,
560
+ points: sectionToPoints(section)
561
+ });
562
+ }
563
+ }
564
+ return edges;
565
+ }
566
+ async function layoutComponent(doc, tableNames, compact) {
567
+ const elk = new import_elk_bundled.default();
568
+ const nameSet = new Set(tableNames);
569
+ const tables = doc.tables.filter((t) => nameSet.has(t.name));
570
+ const direction = tables.length >= 8 ? "DOWN" : "RIGHT";
571
+ const children = tables.map((table) => {
572
+ const { w, h } = measureTableBox(table, compact);
573
+ return { id: table.name, width: w, height: h };
574
+ });
575
+ const edges = [];
576
+ const seen = /* @__PURE__ */ new Set();
577
+ for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
578
+ if (!nameSet.has(rel.fromTable) || !nameSet.has(rel.toTable)) continue;
579
+ const key = `${rel.fromTable}->${rel.toTable}`;
580
+ if (seen.has(key)) continue;
581
+ seen.add(key);
582
+ edges.push({
583
+ id: key,
584
+ sources: [rel.fromTable],
585
+ targets: [rel.toTable]
586
+ });
587
+ }
588
+ const graph = {
589
+ id: "root",
590
+ layoutOptions: {
591
+ "elk.algorithm": "layered",
592
+ "elk.direction": direction,
593
+ "elk.edgeRouting": "ORTHOGONAL",
594
+ "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
595
+ "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
596
+ "elk.layered.spacing.nodeNodeBetweenLayers": compact ? "56" : "80",
597
+ "elk.spacing.nodeNode": compact ? "32" : "48",
598
+ "elk.spacing.edgeNode": "24",
599
+ "elk.padding": `[top=${PAD},left=${PAD},bottom=${PAD},right=${PAD}]`
600
+ },
601
+ children,
602
+ edges
603
+ };
604
+ const layouted = await elk.layout(graph);
605
+ const boxes = /* @__PURE__ */ new Map();
606
+ let minX = Infinity;
607
+ let minY = Infinity;
608
+ let maxX = -Infinity;
609
+ let maxY = -Infinity;
610
+ for (const child of layouted.children ?? []) {
611
+ const table = tables.find((t) => t.name === child.id);
612
+ if (!table) continue;
613
+ const { w, h } = measureTableBox(table, compact);
614
+ const x = child.x ?? 0;
615
+ const y = child.y ?? 0;
616
+ const box = { x, y, w, h };
617
+ boxes.set(child.id, box);
618
+ minX = Math.min(minX, x);
619
+ minY = Math.min(minY, y);
620
+ maxX = Math.max(maxX, x + w);
621
+ maxY = Math.max(maxY, y + h);
622
+ }
623
+ const width = Math.ceil(maxX - minX + PAD * 2);
624
+ const height = Math.ceil(maxY - minY + PAD * 2);
625
+ const dx = PAD - minX;
626
+ const dy = PAD - minY;
627
+ for (const [name, box] of boxes) {
628
+ boxes.set(name, { x: box.x + dx, y: box.y + dy, w: box.w, h: box.h });
629
+ }
630
+ const shiftedEdges = extractEdges(layouted).map((edge) => ({
631
+ ...edge,
632
+ points: edge.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
633
+ }));
634
+ return { boxes, edges: shiftedEdges, width, height };
635
+ }
636
+ function shiftLayout(boxes, edges, offsetX, offsetY) {
637
+ for (const [name, box] of boxes) {
638
+ boxes.set(name, { ...box, x: box.x + offsetX, y: box.y + offsetY });
639
+ }
640
+ for (const edge of edges) {
641
+ for (const p of edge.points) {
642
+ p.x += offsetX;
643
+ p.y += offsetY;
644
+ }
645
+ }
646
+ }
647
+ async function layoutErDiagram(doc) {
648
+ const tables = doc.tables;
649
+ if (tables.length === 0) {
650
+ return {
651
+ boxes: /* @__PURE__ */ new Map(),
652
+ edges: [],
653
+ compact: false,
654
+ width: 400,
655
+ height: 80
656
+ };
657
+ }
658
+ const compact = isCompactLayout(tables.length);
659
+ const adj = buildAdjacency(doc);
660
+ const components = connectedComponents(
661
+ tables.map((t) => t.name),
662
+ adj
663
+ );
664
+ const mergedBoxes = /* @__PURE__ */ new Map();
665
+ const mergedEdges = [];
666
+ const clusterCols = components.length <= 1 ? 1 : components.length <= 4 ? 2 : 3;
667
+ let tileX = 0;
668
+ let tileY = 0;
669
+ let rowHeight = 0;
670
+ let maxWidth = PAD;
671
+ let maxHeight = PAD;
672
+ for (const [i, component] of components.entries()) {
673
+ const laid = await layoutComponent(doc, component, compact);
674
+ shiftLayout(laid.boxes, laid.edges, tileX, tileY);
675
+ for (const [name, box] of laid.boxes) mergedBoxes.set(name, box);
676
+ mergedEdges.push(...laid.edges);
677
+ rowHeight = Math.max(rowHeight, laid.height);
678
+ tileX += laid.width + CLUSTER_GAP;
679
+ maxWidth = Math.max(maxWidth, tileX);
680
+ maxHeight = Math.max(maxHeight, tileY + laid.height);
681
+ if ((i + 1) % clusterCols === 0) {
682
+ tileX = 0;
683
+ tileY += rowHeight + CLUSTER_GAP;
684
+ rowHeight = 0;
685
+ }
686
+ }
687
+ return {
688
+ boxes: mergedBoxes,
689
+ edges: mergedEdges,
690
+ compact,
691
+ width: Math.ceil(maxWidth),
692
+ height: Math.ceil(maxHeight + PAD)
693
+ };
694
+ }
695
+
696
+ // src/exporters/diagram/er-diagram-svg.ts
697
+ async function renderErDiagramSvg(doc) {
698
+ const tables = doc.tables;
699
+ if (tables.length === 0) {
700
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="80"><text x="10" y="40" font-family="Arial,sans-serif" font-size="14">No tables</text></svg>`;
701
+ }
702
+ const layout = await layoutErDiagram(doc);
703
+ return buildErDiagramSvg(doc, layout);
704
+ }
705
+ async function renderErDiagramPng(doc) {
706
+ const tables = doc.tables;
707
+ if (tables.length === 0) {
708
+ const svg2 = await renderErDiagramSvg(doc);
709
+ const sharp2 = (await import("sharp")).default;
710
+ const buffer2 = await sharp2(Buffer.from(svg2)).png().toBuffer();
711
+ return { buffer: buffer2, width: 400, height: 80 };
712
+ }
713
+ const layout = await layoutErDiagram(doc);
714
+ const svg = buildErDiagramSvg(doc, layout);
715
+ const sharp = (await import("sharp")).default;
716
+ const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
717
+ return { buffer, width: layout.width, height: layout.height };
718
+ }
719
+ function buildErDiagramSvg(doc, layout) {
720
+ const { boxes, edges, width, height } = layout;
721
+ const parts = [
722
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Arial,sans-serif" font-size="11">`,
723
+ `<rect width="100%" height="100%" fill="#ffffff"/>`,
724
+ `<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b7aa6"/></marker></defs>`,
725
+ `<g class="edges">`
726
+ ];
727
+ for (const edge of edges) {
728
+ if (edge.points.length < 2) continue;
729
+ const d = pointsToPath(edge.points);
730
+ parts.push(
731
+ `<path d="${d}" fill="none" stroke="#7d96b8" stroke-width="1.25" marker-end="url(#arrow)"/>`
732
+ );
733
+ }
734
+ parts.push(`</g><g class="nodes">`);
735
+ for (const table of doc.tables) {
736
+ const box = boxes.get(table.name);
737
+ parts.push(...renderTableBox(table, box));
738
+ }
739
+ parts.push("</g></svg>");
740
+ return parts.join("");
741
+ }
742
+ function pointsToPath(points) {
743
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
744
+ }
745
+ function renderTableBox(table, box) {
746
+ const parts = [
747
+ `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" fill="#f8fafc" stroke="#4472c4" stroke-width="1.5" rx="4"/>`,
748
+ `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${HEADER_H}" fill="#4472c4" rx="4"/>`,
749
+ `<rect x="${box.x}" y="${box.y + HEADER_H - 4}" width="${box.w}" height="4" fill="#4472c4"/>`,
750
+ `<text x="${box.x + 8}" y="${box.y + 18}" fill="#ffffff" font-weight="bold">${escapeXml(table.name)}</text>`
751
+ ];
752
+ let cy = box.y + HEADER_H + 14;
753
+ const visible = getVisibleErColumns(table);
754
+ for (const col of visible) {
755
+ const marker = col.isPrimaryKey ? " PK" : col.isForeignKey ? " FK" : "";
756
+ parts.push(
757
+ `<text x="${box.x + 8}" y="${cy}" fill="#333333">${escapeXml(col.name)} : ${escapeXml(shortType(col.type))}${marker}</text>`
758
+ );
759
+ cy += LINE_H;
760
+ }
761
+ if (table.columns.length > visible.length) {
762
+ parts.push(
763
+ `<text x="${box.x + 8}" y="${cy}" fill="#666666">... +${table.columns.length - visible.length} more</text>`
764
+ );
765
+ }
766
+ return parts;
767
+ }
768
+ function shortType(type) {
769
+ return type.length > 18 ? `${type.slice(0, 15)}...` : type;
770
+ }
771
+ function escapeXml(text) {
772
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
773
+ }
774
+ function fitErDiagramToBox(width, height, maxWidth, maxHeight) {
775
+ const scale = Math.min(maxWidth / width, maxHeight / height, 1);
776
+ return {
777
+ width: Math.max(1, Math.round(width * scale)),
778
+ height: Math.max(1, Math.round(height * scale))
779
+ };
780
+ }
781
+
246
782
  // src/exporters/excel/excel-exporter.ts
247
783
  var COLOR = {
248
784
  headerBg: "FF4472C4",
@@ -268,6 +804,9 @@ async function exportExcelDictionary(doc, options) {
268
804
  sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
269
805
  }
270
806
  addOverviewSheet(workbook, doc, labels, sheetNames);
807
+ if (doc.tables.length > 0) {
808
+ await addErDiagramSheet(workbook, doc, labels, options.outDir);
809
+ }
271
810
  for (const table of doc.tables) {
272
811
  const sheetName = sheetNames.get(table.name);
273
812
  const sheet = workbook.addWorksheet(sheetName);
@@ -355,6 +894,51 @@ function addOverviewSheet(workbook, doc, labels, sheetNames) {
355
894
  };
356
895
  sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
357
896
  }
897
+ async function addErDiagramSheet(workbook, doc, labels, outDir) {
898
+ const sheet = workbook.addWorksheet(labels.erDiagramSheet);
899
+ sheet.mergeCells(1, 1, 1, 6);
900
+ const titleCell = sheet.getCell(1, 1);
901
+ titleCell.value = labels.erDiagramHeading;
902
+ titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
903
+ titleCell.fill = solidFill(COLOR.overviewBg);
904
+ titleCell.alignment = { horizontal: "center", vertical: "middle" };
905
+ sheet.getRow(1).height = 28;
906
+ let nextRow = 3;
907
+ try {
908
+ const { buffer: png, width, height } = await renderErDiagramPng(doc);
909
+ await (0, import_promises2.writeFile)((0, import_node_path2.join)(outDir, "er_diagram.png"), png);
910
+ const imageId = workbook.addImage({
911
+ base64: png.toString("base64"),
912
+ extension: "png"
913
+ });
914
+ const fitted = fitErDiagramToBox(width, height, 1100, 1200);
915
+ sheet.addImage(imageId, {
916
+ tl: { col: 0, row: 2 },
917
+ ext: fitted
918
+ });
919
+ nextRow = Math.max(28, Math.ceil(fitted.height / 18) + 4);
920
+ } catch {
921
+ sheet.getCell(3, 1).value = labels.viewErDiagram;
922
+ nextRow = 5;
923
+ }
924
+ const mermaid = getErDiagramMermaid(doc);
925
+ sheet.getCell(nextRow, 1).value = "Mermaid source";
926
+ sheet.getCell(nextRow, 1).font = { bold: true, color: { argb: COLOR.metaFg } };
927
+ nextRow += 1;
928
+ sheet.mergeCells(nextRow, 1, nextRow + 20, 6);
929
+ const sourceCell = sheet.getCell(nextRow, 1);
930
+ sourceCell.value = mermaid;
931
+ sourceCell.alignment = { wrapText: true, vertical: "top" };
932
+ sourceCell.font = { name: "Courier New", size: 9 };
933
+ sheet.columns = [
934
+ { width: 24 },
935
+ { width: 24 },
936
+ { width: 24 },
937
+ { width: 24 },
938
+ { width: 24 },
939
+ { width: 24 }
940
+ ];
941
+ }
358
942
  function populateTableSheet(sheet, table, doc, labels) {
359
943
  const indexes = collectTableIndexes(table, doc);
360
944
  sheet.mergeCells(1, 1, 1, 6);
@@ -395,48 +979,39 @@ function populateTableSheet(sheet, table, doc, labels) {
395
979
  row.getCell(2).alignment = { wrapText: true, vertical: "top" };
396
980
  }
397
981
  sheet.addRow([]);
398
- const headerRow = sheet.addRow([
399
- labels.physicalName,
400
- labels.logicalName,
401
- labels.type,
402
- labels.required,
403
- labels.defaultValue,
404
- labels.notes
405
- ]);
982
+ const headerRow = sheet.addRow(columnDefinitionHeaders(labels));
406
983
  styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
407
- applyBorderToRow(headerRow, 6);
984
+ applyBorderToRow(headerRow, A5_COLUMN_COUNT);
408
985
  const headerRowNum = headerRow.number;
409
986
  for (const [i, column] of table.columns.entries()) {
410
- const markers = [];
411
- if (column.isPrimaryKey) markers.push(labels.pkMarker);
412
- if (column.isForeignKey) markers.push(labels.fkMarker);
413
- const notes = [markers.join(", "), column.description?.value ?? ""].filter(Boolean).join(" | ");
414
- const row = sheet.addRow([
415
- column.name,
416
- displayValue(column.comment, labels),
417
- column.type,
418
- column.nullable ? labels.no : labels.yes,
419
- column.defaultValue ?? "-",
420
- notes || "-"
421
- ]);
987
+ const row = sheet.addRow(
988
+ columnDefinitionRow(column, labels).map(
989
+ (value, index) => index === 1 ? displayValue(value, labels) : value
990
+ )
991
+ );
422
992
  if (column.isPrimaryKey) {
423
- shadeRow(row, 6, COLOR.pkBg);
993
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.pkBg);
424
994
  row.getCell(1).font = { bold: true };
425
995
  } else if (column.isForeignKey) {
426
- shadeRow(row, 6, COLOR.fkBg);
996
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.fkBg);
427
997
  } else if (i % 2 === 1) {
428
- shadeRow(row, 6, COLOR.altRow);
998
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.altRow);
429
999
  }
430
- row.getCell(4).alignment = { horizontal: "center" };
431
- applyBorderToRow(row, 6);
1000
+ row.getCell(5).alignment = { horizontal: "center" };
1001
+ row.getCell(9).alignment = { horizontal: "center" };
1002
+ applyBorderToRow(row, A5_COLUMN_COUNT);
432
1003
  }
433
1004
  sheet.columns = [
1005
+ { width: 22 },
434
1006
  { width: 24 },
435
- { width: 28 },
436
- { width: 18 },
1007
+ { width: 16 },
437
1008
  { width: 10 },
438
- { width: 18 },
439
- { width: 36 }
1009
+ { width: 8 },
1010
+ { width: 14 },
1011
+ { width: 8 },
1012
+ { width: 8 },
1013
+ { width: 8 },
1014
+ { width: 28 }
440
1015
  ];
441
1016
  sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
442
1017
  }
@@ -521,6 +1096,14 @@ async function exportMarkdownDocs(doc, options) {
521
1096
  const tablesDir = (0, import_node_path3.join)(options.outDir, "tables");
522
1097
  await (0, import_promises3.mkdir)(tablesDir, { recursive: true });
523
1098
  const labels = getOutputLabels(options.language);
1099
+ if (doc.tables.length > 0) {
1100
+ const mermaid = getErDiagramMermaid(doc);
1101
+ await (0, import_promises3.writeFile)(
1102
+ (0, import_node_path3.join)(options.outDir, "ER_DIAGRAM.md"),
1103
+ renderErDiagramMarkdown(mermaid, labels),
1104
+ "utf8"
1105
+ );
1106
+ }
524
1107
  for (const table of doc.tables) {
525
1108
  await (0, import_promises3.writeFile)(
526
1109
  (0, import_node_path3.join)(tablesDir, `${sanitizeFilename(table.name)}.md`),
@@ -569,12 +1152,12 @@ function renderTableDoc(table, doc, labels) {
569
1152
  lines.push(`## ${labels.columnsHeading}`);
570
1153
  lines.push("");
571
1154
  lines.push(
572
- `| ${labels.physicalName} | ${labels.logicalName} | ${labels.type} | ${labels.required} | ${labels.defaultValue} | ${labels.notes} |`
1155
+ `| ${columnDefinitionHeaders(labels).join(" | ")} |`
573
1156
  );
574
- lines.push("|--------|--------|----|------|--------------|------|");
1157
+ lines.push(`|${columnDefinitionHeaders(labels).map(() => "--------").join("|")}|`);
575
1158
  for (const col of table.columns) {
576
1159
  lines.push(
577
- `| ${escapeMd(col.name)} | ${escapeMd(col.comment ?? "")} | ${escapeMd(col.type)} | ${col.nullable ? labels.no : labels.yes} | ${escapeMd(col.defaultValue ?? "-")} | ${escapeMd(col.description?.value ?? "")} |`
1160
+ `| ${columnDefinitionRow(col, labels).map((value) => escapeMd(value)).join(" | ")} |`
578
1161
  );
579
1162
  }
580
1163
  lines.push("");
@@ -606,6 +1189,14 @@ async function exportHtmlDocs(doc, options) {
606
1189
  renderIndexPage(doc, labels),
607
1190
  "utf8"
608
1191
  );
1192
+ if (doc.tables.length > 0) {
1193
+ const mermaid = getErDiagramMermaid(doc);
1194
+ await (0, import_promises4.writeFile)(
1195
+ (0, import_node_path4.join)(htmlDir, "er-diagram.html"),
1196
+ renderErDiagramHtmlPage(mermaid, labels),
1197
+ "utf8"
1198
+ );
1199
+ }
609
1200
  for (const table of doc.tables) {
610
1201
  await (0, import_promises4.writeFile)(
611
1202
  (0, import_node_path4.join)(tablesDir, `${sanitizeFilename(table.name)}.html`),
@@ -689,7 +1280,7 @@ function pageShell(title, body, fromSubdir = false) {
689
1280
  <head>
690
1281
  <meta charset="UTF-8">
691
1282
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
692
- <title>${esc(title)}</title>
1283
+ <title>${esc2(title)}</title>
693
1284
  <style>${CSS} </style>
694
1285
  </head>
695
1286
  <body>
@@ -706,34 +1297,35 @@ function renderIndexPage(doc, labels) {
706
1297
  const fkCount = table.foreignKeys.length;
707
1298
  const fileName = sanitizeFilename(table.name);
708
1299
  tableRows += ` <tr>
709
- <td><a href="tables/${fileName}.html">${esc(table.name)}</a></td>
710
- <td>${esc(table.comment ?? "")}</td>
1300
+ <td><a href="tables/${fileName}.html">${esc2(table.name)}</a></td>
1301
+ <td>${esc2(table.comment ?? "")}</td>
711
1302
  <td style="text-align:center">${table.columns.length}</td>
712
- <td>${esc(pkCols)}</td>
1303
+ <td>${esc2(pkCols)}</td>
713
1304
  <td style="text-align:center">${fkCount}</td>
714
1305
  </tr>
715
1306
  `;
716
1307
  }
717
1308
  const body = `
718
- <h1>${esc(labels.docTitle)}</h1>
1309
+ <h1>${esc2(labels.docTitle)}</h1>
719
1310
  <div class="summary">
720
- <div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc(labels.tablesLabel)}</div></div>
721
- <div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc(labels.relationshipsLabel)}</div></div>
722
- <div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc(labels.dialectLabel)}</div></div>
1311
+ <div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc2(labels.tablesLabel)}</div></div>
1312
+ <div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc2(labels.relationshipsLabel)}</div></div>
1313
+ <div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc2(labels.dialectLabel)}</div></div>
723
1314
  </div>
724
- <h2>${esc(labels.tableListHeading)}</h2>
1315
+ <p class="back"><a href="er-diagram.html">${esc2(labels.erDiagramHeading)} \u2192</a></p>
1316
+ <h2>${esc2(labels.tableListHeading)}</h2>
725
1317
  <table class="table-list">
726
1318
  <thead><tr>
727
- <th>${esc(labels.tableLabel)}</th>
728
- <th>${esc(labels.tableLogicalName)}</th>
1319
+ <th>${esc2(labels.tableLabel)}</th>
1320
+ <th>${esc2(labels.tableLogicalName)}</th>
729
1321
  <th style="width:70px;text-align:center">Cols</th>
730
- <th>${esc(labels.primaryKey)}</th>
1322
+ <th>${esc2(labels.primaryKey)}</th>
731
1323
  <th style="width:50px;text-align:center">FK</th>
732
1324
  </tr></thead>
733
1325
  <tbody>
734
1326
  ${tableRows} </tbody>
735
1327
  </table>
736
- <p class="note">${esc(labels.generatedNote)}</p>
1328
+ <p class="note">${esc2(labels.generatedNote)}</p>
737
1329
  `;
738
1330
  return pageShell(labels.docTitle, body);
739
1331
  }
@@ -751,39 +1343,34 @@ function renderTablePage(table, doc, labels) {
751
1343
  const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
752
1344
  const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
753
1345
  const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
754
- const required = col.nullable ? labels.no : labels.yes;
755
- colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc(col.name)}${pkBadge}${fkBadge}</td><td>${esc(col.comment ?? "")}</td><td>${esc(col.type)}</td><td>${required}</td><td>${esc(col.defaultValue ?? "-")}</td><td>${esc(col.description?.value ?? "")}</td></tr>
1346
+ const cells = columnDefinitionRow(col, labels);
1347
+ colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc2(cells[0] ?? "")}${pkBadge}${fkBadge}</td>` + cells.slice(1).map((cell) => `<td>${esc2(cell)}</td>`).join("") + `</tr>
756
1348
  `;
757
1349
  }
758
1350
  const body = `
759
- <p class="back"><a href="../index.html">\u2190 ${esc(labels.tableListHeading)}</a></p>
760
- <h1>${esc(table.name)}</h1>
761
- <h2>${esc(labels.tableInfoHeading)}</h2>
1351
+ <p class="back"><a href="../index.html">\u2190 ${esc2(labels.tableListHeading)}</a></p>
1352
+ <h1>${esc2(table.name)}</h1>
1353
+ <h2>${esc2(labels.tableInfoHeading)}</h2>
762
1354
  <table class="meta">
763
1355
  <tbody>
764
- <tr><th>${esc(labels.tablePhysicalName)}</th><td>${esc(table.name)}</td></tr>
765
- <tr><th>${esc(labels.tableLogicalName)}</th><td>${esc(table.comment ?? "")}</td></tr>
766
- <tr><th>${esc(labels.schema)}</th><td>${esc(table.schema ?? "")}</td></tr>
767
- <tr><th>${esc(labels.primaryKey)}</th><td>${esc(table.primaryKeys.join(", ") || labels.none)}</td></tr>
768
- <tr><th>${esc(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
769
- <tr><th>${esc(labels.indexes)}</th><td>${indexText}</td></tr>
1356
+ <tr><th>${esc2(labels.tablePhysicalName)}</th><td>${esc2(table.name)}</td></tr>
1357
+ <tr><th>${esc2(labels.tableLogicalName)}</th><td>${esc2(table.comment ?? "")}</td></tr>
1358
+ <tr><th>${esc2(labels.schema)}</th><td>${esc2(table.schema ?? "")}</td></tr>
1359
+ <tr><th>${esc2(labels.primaryKey)}</th><td>${esc2(table.primaryKeys.join(", ") || labels.none)}</td></tr>
1360
+ <tr><th>${esc2(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
1361
+ <tr><th>${esc2(labels.indexes)}</th><td>${indexText}</td></tr>
770
1362
  </tbody>
771
1363
  </table>
772
1364
 
773
- <h2>${esc(labels.columnsHeading)}</h2>
1365
+ <h2>${esc2(labels.columnsHeading)}</h2>
774
1366
  <table class="columns">
775
1367
  <thead><tr>
776
- <th>${esc(labels.physicalName)}</th>
777
- <th>${esc(labels.logicalName)}</th>
778
- <th>${esc(labels.type)}</th>
779
- <th>${esc(labels.required)}</th>
780
- <th>${esc(labels.defaultValue)}</th>
781
- <th>${esc(labels.notes)}</th>
1368
+ ${columnDefinitionHeaders(labels).map((header) => `<th>${esc2(header)}</th>`).join("\n ")}
782
1369
  </tr></thead>
783
1370
  <tbody>
784
1371
  ${colRows} </tbody>
785
1372
  </table>
786
- <p class="note">${esc(labels.generatedNote)}</p>
1373
+ <p class="note">${esc2(labels.generatedNote)}</p>
787
1374
  `;
788
1375
  return pageShell(table.name, body);
789
1376
  }
@@ -795,7 +1382,7 @@ function collectTableIndexes3(table, doc) {
795
1382
  )
796
1383
  ];
797
1384
  }
798
- function esc(text) {
1385
+ function esc2(text) {
799
1386
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
800
1387
  }
801
1388
 
@@ -835,6 +1422,53 @@ async function exportWordDocument(doc, options) {
835
1422
  children: [new import_docx.TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
836
1423
  })
837
1424
  );
1425
+ if (doc.tables.length > 0) {
1426
+ children.push(
1427
+ new import_docx.Paragraph({
1428
+ heading: import_docx.HeadingLevel.HEADING_2,
1429
+ children: [new import_docx.TextRun(labels.erDiagramHeading)]
1430
+ })
1431
+ );
1432
+ try {
1433
+ const { buffer: png, width, height } = await renderErDiagramPng(doc);
1434
+ await (0, import_promises5.writeFile)((0, import_node_path5.join)(options.outDir, "er_diagram.png"), png);
1435
+ const fitted = fitErDiagramToBox(width, height, 620, 900);
1436
+ children.push(
1437
+ new import_docx.Paragraph({
1438
+ children: [
1439
+ new import_docx.ImageRun({
1440
+ data: png,
1441
+ transformation: fitted,
1442
+ type: "png"
1443
+ })
1444
+ ]
1445
+ })
1446
+ );
1447
+ } catch {
1448
+ children.push(
1449
+ new import_docx.Paragraph({
1450
+ children: [new import_docx.TextRun(labels.viewErDiagram)]
1451
+ })
1452
+ );
1453
+ }
1454
+ const mermaid = getErDiagramMermaid(doc);
1455
+ children.push(
1456
+ new import_docx.Paragraph({
1457
+ children: [new import_docx.TextRun({ text: "Mermaid source", bold: true })]
1458
+ })
1459
+ );
1460
+ children.push(
1461
+ new import_docx.Paragraph({
1462
+ children: [
1463
+ new import_docx.TextRun({
1464
+ text: mermaid,
1465
+ font: "Courier New",
1466
+ size: 18
1467
+ })
1468
+ ]
1469
+ })
1470
+ );
1471
+ }
838
1472
  children.push(
839
1473
  new import_docx.Paragraph({
840
1474
  heading: import_docx.HeadingLevel.HEADING_2,
@@ -1094,14 +1728,7 @@ function renderTableDetail(table, doc, labels) {
1094
1728
  return items;
1095
1729
  }
1096
1730
  function renderColumnsTable(table, labels) {
1097
- const headerCells = [
1098
- labels.physicalName,
1099
- labels.logicalName,
1100
- labels.type,
1101
- labels.required,
1102
- labels.defaultValue,
1103
- labels.notes
1104
- ].map(
1731
+ const headerCells = columnDefinitionHeaders(labels).map(
1105
1732
  (h) => new import_docx.TableCell({
1106
1733
  children: [
1107
1734
  new import_docx.Paragraph({ children: [new import_docx.TextRun({ text: h, bold: true })] })
@@ -1112,40 +1739,11 @@ function renderColumnsTable(table, labels) {
1112
1739
  for (const col of table.columns) {
1113
1740
  colRows.push(
1114
1741
  new import_docx.TableRow({
1115
- children: [
1116
- new import_docx.TableCell({
1117
- children: [new import_docx.Paragraph({ children: [new import_docx.TextRun(col.name)] })]
1118
- }),
1119
- new import_docx.TableCell({
1120
- children: [
1121
- new import_docx.Paragraph({ children: [new import_docx.TextRun(col.comment ?? "")] })
1122
- ]
1123
- }),
1124
- new import_docx.TableCell({
1125
- children: [new import_docx.Paragraph({ children: [new import_docx.TextRun(col.type)] })]
1126
- }),
1127
- new import_docx.TableCell({
1128
- children: [
1129
- new import_docx.Paragraph({
1130
- children: [new import_docx.TextRun(col.nullable ? labels.no : labels.yes)]
1131
- })
1132
- ]
1133
- }),
1134
- new import_docx.TableCell({
1135
- children: [
1136
- new import_docx.Paragraph({
1137
- children: [new import_docx.TextRun(col.defaultValue ?? "-")]
1138
- })
1139
- ]
1140
- }),
1141
- new import_docx.TableCell({
1142
- children: [
1143
- new import_docx.Paragraph({
1144
- children: [new import_docx.TextRun(col.description?.value ?? "")]
1145
- })
1146
- ]
1742
+ children: columnDefinitionRow(col, labels).map(
1743
+ (value) => new import_docx.TableCell({
1744
+ children: [new import_docx.Paragraph({ children: [new import_docx.TextRun(value)] })]
1147
1745
  })
1148
- ]
1746
+ )
1149
1747
  })
1150
1748
  );
1151
1749
  }
@@ -1193,6 +1791,221 @@ function createWarning(code, message, target) {
1193
1791
  };
1194
1792
  }
1195
1793
 
1794
+ // src/parsers/sql/column-meta.ts
1795
+ function normalizeColumnType(definition) {
1796
+ if (typeof definition !== "object" || definition === null) {
1797
+ return { type: String(definition ?? "unknown").toLowerCase() };
1798
+ }
1799
+ const def = definition;
1800
+ const base = String(def.dataType ?? def.type ?? def.name ?? "unknown").toLowerCase();
1801
+ if (String(def.dataType ?? "").toUpperCase() === "ENUM") {
1802
+ const values = extractEnumValues(def.expr);
1803
+ if (values.length > 0) {
1804
+ const joined = values.join(", ");
1805
+ return {
1806
+ type: `${base}(${values.map((v) => `'${v}'`).join(",")})`,
1807
+ size: String(values.length)
1808
+ };
1809
+ }
1810
+ }
1811
+ if (def.length !== void 0 && def.length !== null) {
1812
+ const length = formatAstValue(def.length);
1813
+ if (def.scale !== void 0 && def.scale !== null) {
1814
+ const scale = formatAstValue(def.scale);
1815
+ return {
1816
+ type: `${base}(${length},${scale})`,
1817
+ size: `${length},${scale}`
1818
+ };
1819
+ }
1820
+ return {
1821
+ type: `${base}(${length})`,
1822
+ size: length
1823
+ };
1824
+ }
1825
+ const suffix = Array.isArray(def.suffix) ? def.suffix.map((item) => formatAstValue(item)).filter(Boolean).join(" ") : def.suffix ? formatAstValue(def.suffix) : "";
1826
+ return {
1827
+ type: suffix ? `${base} ${suffix}`.trim() : base
1828
+ };
1829
+ }
1830
+ function extractColumnComment(definition) {
1831
+ const comment = definition.comment;
1832
+ if (!comment) return void 0;
1833
+ const value = comment.value;
1834
+ if (value?.value !== void 0) return String(value.value);
1835
+ if (comment.value !== void 0 && typeof comment.value === "string") {
1836
+ return comment.value;
1837
+ }
1838
+ return void 0;
1839
+ }
1840
+ function hasColumnUnique(definition) {
1841
+ return definition.unique === "unique" || definition.unique === true;
1842
+ }
1843
+ function extractColumnConstraintNotes(definition) {
1844
+ const notes = [];
1845
+ if (definition.auto_increment === "auto_increment" || definition.auto_increment === true) {
1846
+ notes.push("AUTO_INCREMENT");
1847
+ }
1848
+ if (definition.on_update) {
1849
+ notes.push(`ON UPDATE ${formatOnUpdate(definition.on_update)}`);
1850
+ }
1851
+ const generated = definition.generated;
1852
+ if (generated) {
1853
+ const storage = String(generated.storage_type ?? "virtual").toUpperCase();
1854
+ const expression = stringifyGeneratedExpression(generated.expr);
1855
+ notes.push(
1856
+ expression ? `GENERATED ALWAYS ${storage}: ${expression}` : `GENERATED ALWAYS ${storage}`
1857
+ );
1858
+ }
1859
+ const enumValues = extractEnumValues(definition.definition?.expr);
1860
+ if (enumValues.length > 0) {
1861
+ notes.push(`ENUM: ${enumValues.join(", ")}`);
1862
+ }
1863
+ return notes;
1864
+ }
1865
+ function extractEnumValues(expr) {
1866
+ if (!expr || typeof expr !== "object") return [];
1867
+ const object = expr;
1868
+ if (object.type !== "expr_list" || !Array.isArray(object.value)) return [];
1869
+ return object.value.map((item) => {
1870
+ if (!item || typeof item !== "object") return "";
1871
+ const entry = item;
1872
+ return entry.value !== void 0 ? String(entry.value) : "";
1873
+ }).filter(Boolean);
1874
+ }
1875
+ function formatOnUpdate(value) {
1876
+ if (!value || typeof value !== "object") return String(value ?? "");
1877
+ const object = value;
1878
+ if (object.type === "function" && object.name) {
1879
+ const name = object.name;
1880
+ const parts = Array.isArray(name.name) ? name.name : [];
1881
+ return parts.map((part) => String(part.value ?? "")).join("") || "CURRENT_TIMESTAMP";
1882
+ }
1883
+ return formatAstValue(value);
1884
+ }
1885
+ function stringifyGeneratedExpression(expr) {
1886
+ if (!expr || typeof expr !== "object") return void 0;
1887
+ const text = stringifyExpression(expr);
1888
+ return text === "check" ? void 0 : text;
1889
+ }
1890
+ function extractCheckBounds(expression, columnName) {
1891
+ const result = {};
1892
+ walkCheckExpression(expression, columnName, result);
1893
+ return result;
1894
+ }
1895
+ function walkCheckExpression(expression, columnName, result) {
1896
+ if (!expression || typeof expression !== "object") return;
1897
+ const expr = expression;
1898
+ if (expr.type === "binary_expr") {
1899
+ const operator = String(expr.operator ?? "").toUpperCase();
1900
+ if (operator === "AND" || operator === "OR") {
1901
+ walkCheckExpression(expr.left, columnName, result);
1902
+ walkCheckExpression(expr.right, columnName, result);
1903
+ return;
1904
+ }
1905
+ const columnRef = findColumnRef(expr.left) ?? findColumnRef(expr.right);
1906
+ if (columnRef !== columnName) return;
1907
+ const bound = readBound(expr, columnName);
1908
+ if (!bound) return;
1909
+ if (bound.kind === "min") {
1910
+ result.minValue = mergeBound(result.minValue, bound.value, "max");
1911
+ } else {
1912
+ result.maxValue = mergeBound(result.maxValue, bound.value, "min");
1913
+ }
1914
+ return;
1915
+ }
1916
+ result.expression ??= stringifyExpression(expr);
1917
+ }
1918
+ function readBound(expr, columnName) {
1919
+ const operator = String(expr.operator ?? "");
1920
+ const left = expr.left;
1921
+ const right = expr.right;
1922
+ if (findColumnRef(left) === columnName) {
1923
+ if (operator === ">=" || operator === ">") {
1924
+ return { kind: "min", value: formatAstValue(right) };
1925
+ }
1926
+ if (operator === "<=" || operator === "<") {
1927
+ return { kind: "max", value: formatAstValue(right) };
1928
+ }
1929
+ }
1930
+ if (findColumnRef(right) === columnName) {
1931
+ if (operator === ">=" || operator === ">") {
1932
+ return { kind: "max", value: formatAstValue(left) };
1933
+ }
1934
+ if (operator === "<=" || operator === "<") {
1935
+ return { kind: "min", value: formatAstValue(left) };
1936
+ }
1937
+ }
1938
+ return void 0;
1939
+ }
1940
+ function mergeBound(current, next, pick) {
1941
+ if (!current) return next;
1942
+ const currentNum = Number(current);
1943
+ const nextNum = Number(next);
1944
+ if (!Number.isNaN(currentNum) && !Number.isNaN(nextNum)) {
1945
+ return pick === "min" ? String(Math.max(currentNum, nextNum)) : String(Math.min(currentNum, nextNum));
1946
+ }
1947
+ return next;
1948
+ }
1949
+ function findColumnRef(value) {
1950
+ if (!value || typeof value !== "object") return void 0;
1951
+ const expr = value;
1952
+ if (expr.type === "column_ref" && expr.column) {
1953
+ return String(expr.column);
1954
+ }
1955
+ return void 0;
1956
+ }
1957
+ function formatAstValue(value) {
1958
+ if (value === null || value === void 0) return "";
1959
+ if (typeof value === "object") {
1960
+ const object = value;
1961
+ if (object.value !== void 0) return String(object.value);
1962
+ if (object.dataType) return normalizeColumnType(object).type;
1963
+ }
1964
+ return String(value);
1965
+ }
1966
+ function stringifyExpression(expr) {
1967
+ if (expr.type === "binary_expr") {
1968
+ const left = stringifyExpression(expr.left ?? {});
1969
+ const right = stringifyExpression(expr.right ?? {});
1970
+ return `${left} ${expr.operator} ${right}`.trim();
1971
+ }
1972
+ if (expr.type === "column_ref") return String(expr.column ?? "");
1973
+ if (expr.value !== void 0) return formatAstValue(expr);
1974
+ return "check";
1975
+ }
1976
+ function extractConstraintColumnNames(definition) {
1977
+ return extractDeepColumnNames(definition);
1978
+ }
1979
+ function extractDeepColumnNames(value) {
1980
+ if (!Array.isArray(value)) return [];
1981
+ return value.map((item) => {
1982
+ if (typeof item !== "object" || item === null) return String(item ?? "unknown");
1983
+ const object = item;
1984
+ if (object.column !== void 0) return String(object.column);
1985
+ if (object.expr) return extractDeepColumnName(object.expr);
1986
+ return String(object.name ?? "unknown");
1987
+ });
1988
+ }
1989
+ function extractDeepColumnName(value) {
1990
+ if (typeof value !== "object" || value === null) {
1991
+ return String(value ?? "unknown");
1992
+ }
1993
+ const object = value;
1994
+ if (object.expr) return extractDeepColumnName(object.expr);
1995
+ if (object.column && typeof object.column === "object") {
1996
+ return extractDeepColumnName(object.column);
1997
+ }
1998
+ if (object.column !== void 0) return String(object.column);
1999
+ return String(object.name ?? "unknown");
2000
+ }
2001
+ function stringifyCheckDefinition(definition) {
2002
+ if (!Array.isArray(definition) || definition.length === 0) return void 0;
2003
+ if (definition.length === 1) {
2004
+ return stringifyExpression(definition[0]);
2005
+ }
2006
+ return definition.map((item) => stringifyExpression(item)).filter(Boolean).join("; ");
2007
+ }
2008
+
1196
2009
  // src/parsers/sql/sql-normalizer.ts
1197
2010
  function normalizeSqlAst(ast, dialect) {
1198
2011
  const statements = Array.isArray(ast) ? ast : [ast];
@@ -1214,7 +2027,11 @@ function normalizeSqlAst(ast, dialect) {
1214
2027
  }
1215
2028
  for (const index of indexes) {
1216
2029
  const table = tables.find((candidate) => candidate.name === index.table);
1217
- table?.indexes.push(index);
2030
+ if (!table) continue;
2031
+ table.indexes.push(index);
2032
+ if (index.unique) {
2033
+ markColumnsUnique(table, index.columns, `INDEX ${index.name}`);
2034
+ }
1218
2035
  }
1219
2036
  return {
1220
2037
  dialect,
@@ -1236,31 +2053,49 @@ function normalizeCreateTable(statement) {
1236
2053
  reviewTodos: []
1237
2054
  };
1238
2055
  for (const definition of createDefinitions) {
1239
- if (definition.resource === "column") {
1240
- const columnName = extractDeepColumnName(definition.column);
1241
- const isPrimaryKey = hasPrimaryKey(definition);
1242
- const isNotNull = hasNotNull(definition);
1243
- table.columns.push({
1244
- name: columnName,
1245
- type: normalizeType(definition.definition),
1246
- nullable: !isNotNull && !isPrimaryKey,
1247
- defaultValue: extractDefaultFromDef(definition),
1248
- isPrimaryKey,
1249
- isForeignKey: false
1250
- });
1251
- if (isPrimaryKey) table.primaryKeys.push(columnName);
2056
+ if (definition.resource !== "column") continue;
2057
+ const columnName = extractDeepColumnName2(definition.column);
2058
+ const isPrimaryKey = hasPrimaryKey(definition);
2059
+ const isNotNull = hasNotNull(definition);
2060
+ const { type, size } = normalizeColumnType(definition.definition);
2061
+ const check = definition.check;
2062
+ const bounds = check?.definition ? extractCheckBounds(check.definition, columnName) : {};
2063
+ const constraintNotes = extractColumnConstraintNotes(definition);
2064
+ if (check?.definition) {
2065
+ const expression = stringifyCheckDefinition(check.definition);
2066
+ if (expression && (!bounds.minValue || !bounds.maxValue)) {
2067
+ constraintNotes.push(`CHECK: ${expression}`);
2068
+ }
1252
2069
  }
1253
- if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
1254
- table.primaryKeys = extractDeepColumnNames(definition.definition);
2070
+ table.columns.push({
2071
+ name: columnName,
2072
+ type,
2073
+ size,
2074
+ nullable: !isNotNull && !isPrimaryKey,
2075
+ defaultValue: extractDefaultFromDef(definition),
2076
+ minValue: bounds.minValue,
2077
+ maxValue: bounds.maxValue,
2078
+ isUnique: hasColumnUnique(definition),
2079
+ isPrimaryKey,
2080
+ isForeignKey: false,
2081
+ comment: extractColumnComment(definition),
2082
+ constraintNotes: constraintNotes.length > 0 ? constraintNotes : void 0
2083
+ });
2084
+ if (isPrimaryKey) table.primaryKeys.push(columnName);
2085
+ }
2086
+ for (const definition of createDefinitions) {
2087
+ if (definition.resource !== "constraint") continue;
2088
+ if (isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
2089
+ table.primaryKeys = extractDeepColumnNames2(definition.definition);
1255
2090
  for (const column of table.columns) {
1256
2091
  if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
1257
2092
  }
1258
2093
  }
1259
- if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
1260
- const columns = extractDeepColumnNames(definition.definition);
2094
+ if (isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
2095
+ const columns = extractDeepColumnNames2(definition.definition);
1261
2096
  const refDef = definition.reference_definition;
1262
2097
  const referencedTable = extractTableName(refDef?.table);
1263
- const referencedColumns = extractDeepColumnNames(refDef?.definition);
2098
+ const referencedColumns = extractDeepColumnNames2(refDef?.definition);
1264
2099
  table.foreignKeys.push({
1265
2100
  name: typeof definition.constraint === "string" ? definition.constraint : void 0,
1266
2101
  columns,
@@ -1271,14 +2106,71 @@ function normalizeCreateTable(statement) {
1271
2106
  if (columns.includes(column.name)) column.isForeignKey = true;
1272
2107
  }
1273
2108
  }
2109
+ if (isConstraintType(definition.constraint_type, "UNIQUE")) {
2110
+ const columns = extractConstraintColumnNames(definition.definition);
2111
+ const label = typeof definition.constraint === "string" ? definition.constraint : "UNIQUE";
2112
+ markColumnsUnique(table, columns, label);
2113
+ }
2114
+ if (isConstraintType(definition.constraint_type, "CHECK")) {
2115
+ applyTableCheckConstraint(table, definition);
2116
+ }
1274
2117
  }
1275
2118
  return table;
1276
2119
  }
2120
+ function markColumnsUnique(table, columns, label) {
2121
+ const composite = columns.length > 1;
2122
+ for (const columnName of columns) {
2123
+ const column = table.columns.find((item) => item.name === columnName);
2124
+ if (!column) continue;
2125
+ column.isUnique = true;
2126
+ if (composite) {
2127
+ addConstraintNote(
2128
+ column,
2129
+ `UNIQUE (${label}: ${columns.join(", ")})`
2130
+ );
2131
+ }
2132
+ }
2133
+ }
2134
+ function applyTableCheckConstraint(table, definition) {
2135
+ const expression = stringifyCheckDefinition(definition.definition);
2136
+ if (!expression) return;
2137
+ const referencedColumns = /* @__PURE__ */ new Set();
2138
+ for (const column of table.columns) {
2139
+ const bounds = extractCheckBounds(definition.definition, column.name);
2140
+ if (bounds.minValue) column.minValue = bounds.minValue;
2141
+ if (bounds.maxValue) column.maxValue = bounds.maxValue;
2142
+ if (bounds.minValue || bounds.maxValue) {
2143
+ referencedColumns.add(column.name);
2144
+ continue;
2145
+ }
2146
+ if (expression.includes(column.name)) {
2147
+ referencedColumns.add(column.name);
2148
+ }
2149
+ }
2150
+ if (referencedColumns.size === 0) {
2151
+ for (const column of table.columns) {
2152
+ addConstraintNote(column, `CHECK: ${expression}`);
2153
+ }
2154
+ return;
2155
+ }
2156
+ for (const columnName of referencedColumns) {
2157
+ const column = table.columns.find((item) => item.name === columnName);
2158
+ if (!column) continue;
2159
+ if (!column.minValue && !column.maxValue) {
2160
+ addConstraintNote(column, `CHECK: ${expression}`);
2161
+ }
2162
+ }
2163
+ }
2164
+ function addConstraintNote(column, note) {
2165
+ const notes = column.constraintNotes ?? [];
2166
+ if (!notes.includes(note)) notes.push(note);
2167
+ column.constraintNotes = notes;
2168
+ }
1277
2169
  function normalizeCreateIndex(statement) {
1278
2170
  return {
1279
2171
  name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
1280
2172
  table: extractTableName(statement.table),
1281
- columns: extractDeepColumnNames(
2173
+ columns: extractDeepColumnNames2(
1282
2174
  statement.index_columns ?? statement.columns
1283
2175
  ),
1284
2176
  unique: Boolean(statement.unique)
@@ -1332,15 +2224,15 @@ function extractTableName(value) {
1332
2224
  }
1333
2225
  return String(value ?? "unknown");
1334
2226
  }
1335
- function extractDeepColumnName(value) {
2227
+ function extractDeepColumnName2(value) {
1336
2228
  if (typeof value !== "object" || value === null)
1337
2229
  return String(value ?? "unknown");
1338
2230
  const object = value;
1339
2231
  if (object.expr && typeof object.expr === "object") {
1340
- return extractDeepColumnName(object.expr);
2232
+ return extractDeepColumnName2(object.expr);
1341
2233
  }
1342
2234
  if (object.column && typeof object.column === "object") {
1343
- return extractDeepColumnName(object.column);
2235
+ return extractDeepColumnName2(object.column);
1344
2236
  }
1345
2237
  if (object.value !== void 0) {
1346
2238
  return String(object.value);
@@ -1350,18 +2242,9 @@ function extractDeepColumnName(value) {
1350
2242
  }
1351
2243
  return String(object.name ?? object.tableName ?? "unknown");
1352
2244
  }
1353
- function extractDeepColumnNames(value) {
2245
+ function extractDeepColumnNames2(value) {
1354
2246
  if (!Array.isArray(value)) return [];
1355
- return value.map((item) => extractDeepColumnName(item));
1356
- }
1357
- function normalizeType(value) {
1358
- if (typeof value === "object" && value !== null) {
1359
- const object = value;
1360
- return String(
1361
- object.dataType ?? object.type ?? object.name ?? "unknown"
1362
- ).toLowerCase();
1363
- }
1364
- return String(value ?? "unknown").toLowerCase();
2247
+ return value.map((item) => extractDeepColumnName2(item));
1365
2248
  }
1366
2249
  function hasPrimaryKey(def) {
1367
2250
  if (def.primary_key) return true;
@@ -1641,18 +2524,40 @@ async function generateDbDocs(options) {
1641
2524
  return doc;
1642
2525
  }
1643
2526
 
2527
+ // src/core/output-path.ts
2528
+ var import_node_path6 = require("path");
2529
+ var OUTPUT_RUN_DIR_PREFIX = "db_doc_gen_";
2530
+ function createTimestampedRunName(date = /* @__PURE__ */ new Date()) {
2531
+ const year = String(date.getFullYear()).slice(-2);
2532
+ const month = String(date.getMonth() + 1).padStart(2, "0");
2533
+ const day = String(date.getDate()).padStart(2, "0");
2534
+ const hours = String(date.getHours()).padStart(2, "0");
2535
+ const minutes = String(date.getMinutes()).padStart(2, "0");
2536
+ return `${OUTPUT_RUN_DIR_PREFIX}${year}${month}${day}${hours}${minutes}`;
2537
+ }
2538
+ function resolveGenerateOutDir(parentDir, date = /* @__PURE__ */ new Date()) {
2539
+ return (0, import_node_path6.join)(parentDir, createTimestampedRunName(date));
2540
+ }
2541
+ function isOutputRunDir(path) {
2542
+ return (0, import_node_path6.basename)(path).startsWith(OUTPUT_RUN_DIR_PREFIX);
2543
+ }
2544
+ function isOutputRunDirName(name) {
2545
+ return name.startsWith(OUTPUT_RUN_DIR_PREFIX);
2546
+ }
2547
+
1644
2548
  // src/cli/index.ts
1645
2549
  var DEFAULT_CONFIG_PATH = "dbdocgen.config.json";
1646
2550
  var program = new import_commander.Command();
1647
2551
  program.name("dbdocgen").description("Generate database documentation from SQL schema files.").version("0.1.0");
1648
2552
  program.command("init").description("Create a default config file").option("-f, --force", "Overwrite existing config file").action(async (rawOptions) => {
1649
- const configPath = (0, import_node_path6.resolve)(process.cwd(), DEFAULT_CONFIG_PATH);
2553
+ const configPath = (0, import_node_path7.resolve)(process.cwd(), DEFAULT_CONFIG_PATH);
1650
2554
  if ((0, import_node_fs.existsSync)(configPath) && !rawOptions.force) {
1651
2555
  console.log(`Config already exists at ${configPath}. Use --force to overwrite.`);
1652
2556
  return;
1653
2557
  }
1654
2558
  const defaultConfig = {
1655
2559
  schema: "./database/schema.sql",
2560
+ outDir: "./output",
1656
2561
  output: {
1657
2562
  formats: ["excel", "markdown", "html", "diagram", "word"],
1658
2563
  language: "en"
@@ -1660,7 +2565,9 @@ program.command("init").description("Create a default config file").option("-f,
1660
2565
  };
1661
2566
  await (0, import_promises7.writeFile)(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
1662
2567
  console.log(`Created config at ${configPath}`);
1663
- console.log("Default generate output directory is ./output/db_doc_gen_{yymmddhhmm} unless you pass --out.");
2568
+ console.log(
2569
+ "Each generate run writes to {outDir}/db_doc_gen_{yymmddhhmm} (outDir defaults to ./output)."
2570
+ );
1664
2571
  console.log("Edit the file to configure your database schema path and output formats.");
1665
2572
  });
1666
2573
  var configCommand = program.command("config").description("Manage configuration");
@@ -1690,7 +2597,7 @@ configCommand.command("validate").description("Validate config file").option("--
1690
2597
  process.exitCode = 1;
1691
2598
  }
1692
2599
  });
1693
- program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Output directory").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
2600
+ program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Parent output directory (run folder: db_doc_gen_{yymmddhhmm})").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
1694
2601
  console.log("[dbdocgen] Loading configuration...");
1695
2602
  const config = await loadConfig({
1696
2603
  cwd: process.cwd(),
@@ -1701,9 +2608,11 @@ program.command("generate").description("Generate database documentation").optio
1701
2608
  configPath: rawOptions.config
1702
2609
  }
1703
2610
  });
1704
- const outDir = rawOptions.out ?? createTimestampedOutputDir();
2611
+ const parentOutDir = config.outDir;
2612
+ const outDir = resolveGenerateOutDir(parentOutDir);
1705
2613
  console.log("[dbdocgen] Configuration loaded");
1706
2614
  console.log(` schema: ${config.schema}`);
2615
+ console.log(` outputParent: ${parentOutDir}`);
1707
2616
  console.log(` outDir: ${outDir}`);
1708
2617
  console.log(` formats: ${config.output.formats.join(", ")}`);
1709
2618
  console.log(` language: ${config.output.language}`);
@@ -1773,7 +2682,7 @@ Found ${doc.tables.length} table(s):
1773
2682
  }
1774
2683
  console.log("Schema validation passed.");
1775
2684
  });
1776
- program.command("clean").description("Clean output directory").option("--out <path>", "Output directory to clean").option("--config <path>", "Config file path").action(async (rawOptions) => {
2685
+ program.command("clean").description("Clean output directory").option("--out <path>", "Parent output directory or a specific db_doc_gen_* run folder").option("--config <path>", "Config file path").action(async (rawOptions) => {
1777
2686
  const config = await loadConfig({
1778
2687
  cwd: process.cwd(),
1779
2688
  cliOptions: {
@@ -1781,13 +2690,29 @@ program.command("clean").description("Clean output directory").option("--out <pa
1781
2690
  configPath: rawOptions.config
1782
2691
  }
1783
2692
  });
1784
- const outDir = (0, import_node_path6.resolve)(config.outDir);
1785
- if (!(0, import_node_fs.existsSync)(outDir)) {
1786
- console.log(`Output directory ${outDir} does not exist. Nothing to clean.`);
2693
+ const target = (0, import_node_path7.resolve)(config.outDir);
2694
+ if (!(0, import_node_fs.existsSync)(target)) {
2695
+ console.log(`Output path ${target} does not exist. Nothing to clean.`);
2696
+ return;
2697
+ }
2698
+ if (isOutputRunDir(target)) {
2699
+ console.log(`Cleaning ${target}...`);
2700
+ await (0, import_promises7.rm)(target, { recursive: true, force: true });
2701
+ console.log("Done.");
1787
2702
  return;
1788
2703
  }
1789
- console.log(`Cleaning ${outDir}...`);
1790
- await (0, import_promises7.rm)(outDir, { recursive: true, force: true });
2704
+ const entries = await (0, import_promises7.readdir)(target, { withFileTypes: true });
2705
+ const runDirs = entries.filter(
2706
+ (entry) => entry.isDirectory() && isOutputRunDirName(entry.name)
2707
+ );
2708
+ if (runDirs.length === 0) {
2709
+ console.log(`No db_doc_gen_* folders found under ${target}.`);
2710
+ return;
2711
+ }
2712
+ console.log(`Cleaning ${runDirs.length} run folder(s) under ${target}...`);
2713
+ for (const entry of runDirs) {
2714
+ await (0, import_promises7.rm)((0, import_node_path7.resolve)(target, entry.name), { recursive: true, force: true });
2715
+ }
1791
2716
  console.log("Done.");
1792
2717
  });
1793
2718
  program.command("info").description("Show project info and supported features").action(() => {
@@ -1833,12 +2758,4 @@ function parseFormats(value) {
1833
2758
  }
1834
2759
  return valid.length > 0 ? valid : void 0;
1835
2760
  }
1836
- function createTimestampedOutputDir(date = /* @__PURE__ */ new Date()) {
1837
- const year = String(date.getFullYear()).slice(-2);
1838
- const month = String(date.getMonth() + 1).padStart(2, "0");
1839
- const day = String(date.getDate()).padStart(2, "0");
1840
- const hours = String(date.getHours()).padStart(2, "0");
1841
- const minutes = String(date.getMinutes()).padStart(2, "0");
1842
- return `./output/db_doc_gen_${year}${month}${day}${hours}${minutes}`;
1843
- }
1844
2761
  //# sourceMappingURL=index.cjs.map