@diagrams-js/cli
Advanced tools
+1046
| #!/usr/bin/env node | ||
| import { createRequire } from "node:module"; | ||
| import { Command } from "commander"; | ||
| import { Diagram } from "diagrams-js"; | ||
| import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs"; | ||
| import { basename, dirname, extname, join, resolve } from "path"; | ||
| import { execSync } from "child_process"; | ||
| import { watch } from "chokidar"; | ||
| //#region \0rolldown/runtime.js | ||
| var __require = /* @__PURE__ */ createRequire(import.meta.url); | ||
| //#endregion | ||
| //#region src/utils/file-loader.ts | ||
| /** | ||
| * File loading utilities for diagram sources | ||
| * Handles .ts, .js, .json, .svg input files, stdin, and inline data | ||
| */ | ||
| /** | ||
| * Load a diagram from a file path | ||
| * Supports .ts, .js, .mjs, .json, .svg files | ||
| */ | ||
| async function loadDiagram(filePath, cwd) { | ||
| const fullPath = resolve(cwd || process.cwd(), filePath); | ||
| if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`); | ||
| const ext = extname(fullPath).toLowerCase(); | ||
| const content = readFileSync(fullPath, "utf-8"); | ||
| switch (ext) { | ||
| case ".ts": | ||
| case ".js": | ||
| case ".mjs": return { | ||
| diagram: await executeDiagramFile(fullPath, cwd), | ||
| format: "code" | ||
| }; | ||
| case ".json": { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| const json = JSON.parse(content); | ||
| return { | ||
| diagram: await Diagram.fromJSON(json), | ||
| format: "json" | ||
| }; | ||
| } | ||
| case ".svg": { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| return { | ||
| diagram: await Diagram.fromSVG(content), | ||
| format: "svg" | ||
| }; | ||
| } | ||
| default: throw new Error(`Unsupported file format: ${ext}. Supported: .ts, .js, .mjs, .json, .svg`); | ||
| } | ||
| } | ||
| /** | ||
| * Load a diagram from inline string data | ||
| * Supports JSON and SVG content | ||
| */ | ||
| async function loadDiagramFromData(data, format) { | ||
| const trimmed = data.trim(); | ||
| switch (format || detectDataFormat(trimmed)) { | ||
| case "json": { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| const json = JSON.parse(trimmed); | ||
| return { | ||
| diagram: await Diagram.fromJSON(json), | ||
| format: "json" | ||
| }; | ||
| } | ||
| case "svg": { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| return { | ||
| diagram: await Diagram.fromSVG(trimmed), | ||
| format: "svg" | ||
| }; | ||
| } | ||
| default: throw new Error(`Could not detect diagram format from data. Use --format to specify (json|svg).`); | ||
| } | ||
| } | ||
| function detectDataFormat(data) { | ||
| if (data.startsWith("{") || data.startsWith("[")) return "json"; | ||
| if (data.startsWith("<svg") || data.includes("<svg")) return "svg"; | ||
| } | ||
| /** | ||
| * Read all data from stdin | ||
| */ | ||
| function readStdin() { | ||
| return new Promise((resolve, reject) => { | ||
| let data = ""; | ||
| process.stdin.setEncoding("utf-8"); | ||
| process.stdin.on("data", (chunk) => { | ||
| data += chunk; | ||
| }); | ||
| process.stdin.on("end", () => { | ||
| resolve(data); | ||
| }); | ||
| process.stdin.on("error", (err) => { | ||
| reject(err); | ||
| }); | ||
| if (process.stdin.isTTY) resolve(data); | ||
| }); | ||
| } | ||
| /** | ||
| * Execute a TypeScript or JavaScript diagram file and extract the Diagram instance | ||
| */ | ||
| async function executeDiagramFile(filePath, cwd) { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| const isTypeScript = filePath.endsWith(".ts"); | ||
| const ext = isTypeScript ? ".ts" : ".js"; | ||
| const importPath = "file://" + resolve(filePath).replace(/\\/g, "/"); | ||
| const projectRoot = cwd || process.cwd(); | ||
| const wrapperDir = join(projectRoot, ".diagrams-cli"); | ||
| if (!existsSync(wrapperDir)) mkdirSync(wrapperDir, { recursive: true }); | ||
| const wrapperContent = ` | ||
| import { Diagram } from "diagrams-js"; | ||
| import * as diagramModule from "${importPath}"; | ||
| async function main() { | ||
| const exported = diagramModule.default || diagramModule; | ||
| // If it's a Diagram instance, serialize to JSON | ||
| if (exported && typeof exported.toJSON === "function") { | ||
| console.log(JSON.stringify(exported.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| // If it's a function that returns a Diagram or Promise<Diagram> | ||
| if (typeof exported === "function") { | ||
| const result = await exported(); | ||
| if (result && typeof result.toJSON === "function") { | ||
| console.log(JSON.stringify(result.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| } | ||
| // Try named exports | ||
| for (const key of Object.keys(diagramModule)) { | ||
| const val = diagramModule[key]; | ||
| if (val && typeof val.toJSON === "function") { | ||
| console.log(JSON.stringify(val.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| } | ||
| console.error("No diagram found in module. Export a Diagram instance as default or named export."); | ||
| process.exit(1); | ||
| } | ||
| main().catch(err => { | ||
| console.error("Error:", err); | ||
| process.exit(1); | ||
| }); | ||
| `; | ||
| const wrapperPath = join(wrapperDir, `wrapper-${Date.now()}${ext}`); | ||
| writeFileSync(wrapperPath, wrapperContent, "utf-8"); | ||
| try { | ||
| const { execSync } = await import("child_process"); | ||
| const output = execSync(`node ${(isTypeScript ? ["--experimental-strip-types"] : []).join(" ")} "${wrapperPath}"`, { | ||
| encoding: "utf-8", | ||
| cwd: projectRoot, | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "pipe" | ||
| ], | ||
| timeout: 3e4, | ||
| windowsHide: true | ||
| }); | ||
| const json = JSON.parse(output.trim()); | ||
| return Diagram.fromJSON(json); | ||
| } catch (error) { | ||
| const jsonCommentMatch = readFileSync(filePath, "utf-8").match(/\/\/\s*diagram-json:\s*(\{[\s\S]*?\})/); | ||
| if (jsonCommentMatch) try { | ||
| const json = JSON.parse(jsonCommentMatch[1]); | ||
| return Diagram.fromJSON(json); | ||
| } catch {} | ||
| throw new Error(`Failed to execute diagram file ${filePath}:\n` + (error instanceof Error ? error.message : String(error)) + `\n\nMake sure the file exports a Diagram instance.`); | ||
| } finally { | ||
| try { | ||
| rmSync(wrapperPath, { force: true }); | ||
| } catch {} | ||
| } | ||
| } | ||
| /** | ||
| * Read raw file content | ||
| */ | ||
| function readFileContent(filePath, cwd) { | ||
| const fullPath = resolve(cwd || process.cwd(), filePath); | ||
| if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`); | ||
| return readFileSync(fullPath, "utf-8"); | ||
| } | ||
| /** | ||
| * Detect if a file is an importable config file (yaml for plugins) | ||
| */ | ||
| function isImportableFile(filePath) { | ||
| const ext = extname(filePath).toLowerCase(); | ||
| return [".yaml", ".yml"].includes(ext); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/plugin-loader.ts | ||
| /** | ||
| * Plugin auto-discovery and loading utilities | ||
| */ | ||
| /** | ||
| * Auto-discover diagrams-js plugins in node_modules | ||
| */ | ||
| function discoverPlugins(cwd) { | ||
| const plugins = []; | ||
| const nodeModulesPath = resolve(cwd || process.cwd(), "node_modules"); | ||
| if (!existsSync(nodeModulesPath)) return plugins; | ||
| const scopedPath = join(nodeModulesPath, "@diagrams-js"); | ||
| if (existsSync(scopedPath)) { | ||
| for (const dir of readdirSync(scopedPath)) if (dir.startsWith("plugin-")) { | ||
| const pkgPath = join(scopedPath, dir, "package.json"); | ||
| if (existsSync(pkgPath)) try { | ||
| const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); | ||
| plugins.push({ | ||
| name: dir.replace("plugin-", ""), | ||
| packageName: pkg.name, | ||
| version: pkg.version, | ||
| description: pkg.description, | ||
| path: join(scopedPath, dir) | ||
| }); | ||
| } catch {} | ||
| } | ||
| } | ||
| for (const dir of readdirSync(nodeModulesPath)) if (dir.startsWith("diagrams-js-plugin-")) { | ||
| const pkgPath = join(nodeModulesPath, dir, "package.json"); | ||
| if (existsSync(pkgPath)) try { | ||
| const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); | ||
| plugins.push({ | ||
| name: dir.replace("diagrams-js-plugin-", ""), | ||
| packageName: pkg.name, | ||
| version: pkg.version, | ||
| description: pkg.description, | ||
| path: join(nodeModulesPath, dir) | ||
| }); | ||
| } catch {} | ||
| } | ||
| return plugins.sort((a, b) => a.name.localeCompare(b.name)); | ||
| } | ||
| /** | ||
| * Load a plugin by name | ||
| */ | ||
| async function loadPlugin(name, cwd) { | ||
| const nodeModulesPath = resolve(cwd || process.cwd(), "node_modules"); | ||
| const scopedPath = `@diagrams-js/plugin-${name}`; | ||
| try { | ||
| const mod = await import(resolve(nodeModulesPath, scopedPath)); | ||
| return mod[name + "Plugin"] || mod.default || mod.plugin || mod[Object.keys(mod).find((k) => k.endsWith("Plugin")) || ""]; | ||
| } catch {} | ||
| const unscopedPath = `diagrams-js-plugin-${name}`; | ||
| try { | ||
| const mod = await import(resolve(nodeModulesPath, unscopedPath)); | ||
| return mod[name + "Plugin"] || mod.default || mod.plugin || mod[Object.keys(mod).find((k) => k.endsWith("Plugin")) || ""]; | ||
| } catch {} | ||
| try { | ||
| const mod = await import(resolve(nodeModulesPath, name)); | ||
| return mod[name + "Plugin"] || mod.default || mod.plugin || mod[Object.keys(mod).find((k) => k.endsWith("Plugin")) || ""]; | ||
| } catch { | ||
| throw new Error(`Could not load plugin "${name}". Make sure it is installed.`); | ||
| } | ||
| } | ||
| /** | ||
| * Load multiple plugins | ||
| */ | ||
| async function loadPlugins(names, cwd) { | ||
| const plugins = []; | ||
| for (const name of names) try { | ||
| const plugin = await loadPlugin(name, cwd); | ||
| if (plugin) plugins.push(plugin); | ||
| } catch (error) { | ||
| console.error(`Warning: Failed to load plugin "${name}": ${error}`); | ||
| } | ||
| return plugins; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/helpers.ts | ||
| /** | ||
| * Cross-platform utilities and helpers for the CLI | ||
| */ | ||
| /** | ||
| * Write output to file or stdout | ||
| */ | ||
| function outputResult(data, outputPath) { | ||
| if (outputPath) { | ||
| const dir = dirname(outputPath); | ||
| if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); | ||
| if (typeof data === "string") writeFileSync(outputPath, data, "utf-8"); | ||
| else writeFileSync(outputPath, Buffer.from(data)); | ||
| console.error(`Output saved to: ${outputPath}`); | ||
| } else if (typeof data === "string") console.log(data); | ||
| else process.stdout.write(Buffer.from(data)); | ||
| } | ||
| /** | ||
| * Detect output format from file extension | ||
| */ | ||
| function formatFromPath(filePath) { | ||
| const ext = filePath.split(".").pop()?.toLowerCase(); | ||
| return ext ? { | ||
| svg: "svg", | ||
| png: "png", | ||
| jpg: "jpg", | ||
| jpeg: "jpg", | ||
| dot: "dot", | ||
| json: "json", | ||
| html: "html" | ||
| }[ext] : void 0; | ||
| } | ||
| /** | ||
| * Simple spinner for CLI feedback | ||
| */ | ||
| function createSpinner(text) { | ||
| let interval = null; | ||
| const frames = [ | ||
| "⠋", | ||
| "⠙", | ||
| "⠹", | ||
| "⠸", | ||
| "⠼", | ||
| "⠴", | ||
| "⠦", | ||
| "⠧", | ||
| "⠇", | ||
| "⠏" | ||
| ]; | ||
| let i = 0; | ||
| return { | ||
| start() { | ||
| interval = setInterval(() => { | ||
| process.stderr.write(`\r${frames[i]} ${text}`); | ||
| i = (i + 1) % frames.length; | ||
| }, 80); | ||
| }, | ||
| stop(message) { | ||
| if (interval) { | ||
| clearInterval(interval); | ||
| interval = null; | ||
| } | ||
| process.stderr.write(`\r${message || "Done!"}\n`); | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/config.ts | ||
| /** | ||
| * Configuration file support (.diagramsrc.json) | ||
| */ | ||
| const configFileNames = [ | ||
| ".diagramsrc.json", | ||
| ".diagramsrc.js", | ||
| ".diagramsrc.mjs" | ||
| ]; | ||
| /** | ||
| * Find and load configuration file from cwd or parent directories | ||
| */ | ||
| function loadConfig(cwd) { | ||
| let currentDir = resolve(cwd || process.cwd()); | ||
| while (currentDir !== dirname(currentDir)) { | ||
| for (const fileName of configFileNames) { | ||
| const configPath = resolve(currentDir, fileName); | ||
| if (existsSync(configPath)) return parseConfig(configPath); | ||
| } | ||
| currentDir = dirname(currentDir); | ||
| } | ||
| return {}; | ||
| } | ||
| function parseConfig(configPath) { | ||
| if (configPath.endsWith(".json")) try { | ||
| const content = readFileSync(configPath, "utf-8"); | ||
| return JSON.parse(content); | ||
| } catch (error) { | ||
| throw new Error(`Failed to parse config file ${configPath}: ${error}`); | ||
| } | ||
| return {}; | ||
| } | ||
| /** | ||
| * Merge CLI options with config file values | ||
| */ | ||
| function mergeWithConfig(options, config, keys) { | ||
| const merged = { ...options }; | ||
| for (const key of keys) if (config[key] !== void 0 && merged[key] === void 0) merged[key] = config[key]; | ||
| return merged; | ||
| } | ||
| //#endregion | ||
| //#region src/commands/render.ts | ||
| /** | ||
| * diagrams render <file> command | ||
| * Render a diagram file to svg/png/jpg/dot/json | ||
| * Supports plugin-based import for .yaml/.yml files | ||
| * Supports stdin (use - as file) | ||
| */ | ||
| async function renderCommand(file, options) { | ||
| const merged = mergeWithConfig(options, loadConfig(options.config), [ | ||
| "format", | ||
| "theme", | ||
| "direction", | ||
| "curveStyle", | ||
| "width", | ||
| "height", | ||
| "scale" | ||
| ]); | ||
| const outputFormat = merged.format || (merged.output ? formatFromPath(merged.output) : void 0) || "svg"; | ||
| if (!file) throw new Error("No input provided. Specify a file or pipe diagram data to stdin (use -)."); | ||
| const outputPath = resolveRenderOutputPath(file, outputFormat, merged.output, merged.stdout); | ||
| const spinnerMessage = file === "-" ? "Rendering from stdin..." : `Rendering ${file}...`; | ||
| const spinner = !merged.quiet ? createSpinner(spinnerMessage) : null; | ||
| spinner?.start(); | ||
| try { | ||
| let diagram; | ||
| if (file === "-") { | ||
| const stdinData = await readStdin(); | ||
| if (!stdinData.trim()) throw new Error("No data received from stdin. Pipe diagram content to stdin."); | ||
| diagram = (await loadDiagramFromData(stdinData, merged.format)).diagram; | ||
| } else if (isImportableFile(file)) diagram = await importWithPlugin(file, merged); | ||
| else diagram = (await loadDiagram(file, process.cwd())).diagram; | ||
| applyDiagramOptions(diagram, merged); | ||
| const renderOptions = { format: outputFormat }; | ||
| if (merged.width !== void 0) renderOptions.width = merged.width; | ||
| if (merged.height !== void 0) renderOptions.height = merged.height; | ||
| if (merged.scale !== void 0) renderOptions.scale = merged.scale; | ||
| if (merged.dataUrl) renderOptions.dataUrl = true; | ||
| if (merged.embedData !== void 0) renderOptions.embedData = merged.embedData; | ||
| const result = await diagram.render(renderOptions); | ||
| spinner?.stop(); | ||
| if (outputPath) outputResult(result, outputPath); | ||
| if (merged.stdout) outputResult(result, void 0); | ||
| } catch (error) { | ||
| spinner?.stop("Failed"); | ||
| throw error; | ||
| } | ||
| } | ||
| async function importWithPlugin(file, options) { | ||
| const content = readFileContent(file, process.cwd()); | ||
| const pluginName = options.plugin || autoDetectPlugin(file, content); | ||
| if (!pluginName) throw new Error(`Could not auto-detect plugin for ${file}. Use --plugin to specify one.\nAvailable plugins: ${discoverPlugins(process.cwd()).map((p) => p.name).join(", ")}`); | ||
| const plugins = await loadPlugins([pluginName], process.cwd()); | ||
| if (plugins.length === 0) throw new Error(`Failed to load plugin "${pluginName}"`); | ||
| const diagram = Diagram(file.replace(/\.[^.]+$/, "")); | ||
| await diagram.registerPlugins(plugins); | ||
| await diagram.import(content, pluginName); | ||
| return diagram; | ||
| } | ||
| function autoDetectPlugin(filePath, content) { | ||
| const lowerPath = filePath.toLowerCase(); | ||
| const lowerContent = content.toLowerCase().trim(); | ||
| if (lowerPath.includes("docker-compose") || lowerPath.includes("compose") || lowerContent.includes("services:")) { | ||
| if (lowerContent.includes("apiversion:") || lowerContent.includes("kind:")) return "kubernetes"; | ||
| return "docker-compose"; | ||
| } | ||
| if (lowerPath.includes("k8s") || lowerPath.includes("kubernetes") || lowerContent.startsWith("apiversion:") || lowerContent.includes("\napiversion:") || lowerContent.includes("kind:")) return "kubernetes"; | ||
| } | ||
| function resolveRenderOutputPath(file, outputFormat, output, stdout) { | ||
| if (output) return output; | ||
| if (stdout) return void 0; | ||
| return `${!file || file === "-" ? "diagram" : resolve(process.cwd(), file).replace(/\.[^.]+$/, "")}.${outputFormat}`; | ||
| } | ||
| function applyDiagramOptions(diagram, options) { | ||
| if (options.theme) diagram.theme = options.theme; | ||
| if (options.direction) diagram.direction = options.direction; | ||
| if (options.curveStyle) diagram.curveStyle = options.curveStyle; | ||
| } | ||
| //#endregion | ||
| //#region src/commands/export.ts | ||
| /** | ||
| * diagrams export <file> command | ||
| * Export a diagram to an external format (docker-compose, kubernetes, etc.) | ||
| */ | ||
| async function exportCommand(file, options) { | ||
| if (!options.format) throw new Error("Export format is required. Use --format or -f."); | ||
| const outputPath = resolveExportOutputPath(file, options.format, options.output, options.stdout); | ||
| const spinner = !options.quiet ? createSpinner(`Exporting ${file} to ${options.format}...`) : null; | ||
| spinner?.start(); | ||
| try { | ||
| let diagram; | ||
| if (file === "-") { | ||
| const stdinData = await readStdin(); | ||
| if (!stdinData.trim()) throw new Error("No data received from stdin. Pipe diagram content to stdin."); | ||
| diagram = (await loadDiagramFromData(stdinData)).diagram; | ||
| } else diagram = (await loadDiagram(file, process.cwd())).diagram; | ||
| const plugins = await loadPlugins([options.plugin || options.format], process.cwd()); | ||
| if (plugins.length > 0) await diagram.registerPlugins(plugins); | ||
| const result = await diagram.export(options.format); | ||
| spinner?.stop(); | ||
| if (outputPath) outputResult(result, outputPath); | ||
| if (options.stdout) outputResult(result, void 0); | ||
| } catch (error) { | ||
| spinner?.stop("Failed"); | ||
| throw error; | ||
| } | ||
| } | ||
| function resolveExportOutputPath(file, exportFormat, output, stdout) { | ||
| if (output) return output; | ||
| if (stdout) return void 0; | ||
| const ext = { | ||
| "docker-compose": "yml", | ||
| kubernetes: "yaml", | ||
| k8s: "yaml" | ||
| }[exportFormat.toLowerCase()] || exportFormat; | ||
| const baseName = file === "-" ? "diagram" : basename(file, extname(file)); | ||
| return resolve(process.cwd(), `${baseName}.${ext}`); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/diff.ts | ||
| /** | ||
| * diagrams diff <ref> <file> command | ||
| * Generate visual diffs of diagrams in git workflows | ||
| */ | ||
| function resolveDiffOutputPath(file, cwd, output, stdout) { | ||
| if (output) return output; | ||
| if (stdout) return void 0; | ||
| return resolve(cwd, `${basename(file, extname(file))}-diff.html`); | ||
| } | ||
| async function diffCommand(ref, file, options) { | ||
| const merged = mergeWithConfig(options, loadConfig(options.directory), [ | ||
| "format", | ||
| "theme", | ||
| "layout", | ||
| "showUnchanged", | ||
| "ignorePosition", | ||
| "ignoreMetadata" | ||
| ]); | ||
| const gitRef = parseGitRef(ref); | ||
| const cwd = merged.directory || process.cwd(); | ||
| const outputPath = resolveDiffOutputPath(file, cwd, merged.output, merged.stdout); | ||
| const spinner = !merged.quiet ? createSpinner(`Generating diff for ${file}...`) : null; | ||
| spinner?.start(); | ||
| try { | ||
| try { | ||
| git("rev-parse --show-toplevel", cwd); | ||
| } catch { | ||
| throw new Error("Not a git repository"); | ||
| } | ||
| const beforeContent = getFileContent(file, gitRef.base, cwd); | ||
| const afterContent = gitRef.target ? getFileContent(file, gitRef.target, cwd) : getWorkingContent(file, cwd); | ||
| if (!beforeContent) throw new Error(`File not found at ref: ${file}@${gitRef.base}`); | ||
| if (!afterContent) throw new Error(`File not found: ${file}${gitRef.target ? `@${gitRef.target}` : " (working directory)"}`); | ||
| const beforeJSON = await extractDiagramJSON(beforeContent, file, cwd); | ||
| const afterJSON = await extractDiagramJSON(afterContent, file, cwd); | ||
| const { computeDiff, renderDiff } = await import("diagrams-js"); | ||
| const diffOutput = await renderDiff(computeDiff(beforeJSON, afterJSON, { ignore: { | ||
| position: merged.ignorePosition ?? true, | ||
| metadata: merged.ignoreMetadata ?? false | ||
| } }), beforeJSON, afterJSON, { | ||
| format: merged.format ?? "html", | ||
| theme: merged.theme ?? "light", | ||
| layout: merged.layout ?? "side-by-side", | ||
| showUnchanged: merged.showUnchanged ?? "show", | ||
| showLegend: true, | ||
| showSummary: false, | ||
| hoverDetails: true | ||
| }); | ||
| spinner?.stop(); | ||
| if (outputPath) outputResult(diffOutput, outputPath); | ||
| if (merged.stdout) outputResult(diffOutput, void 0); | ||
| } catch (error) { | ||
| spinner?.stop("Failed"); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * diagrams diff batch <ref> command | ||
| * Generate diffs for all changed diagram files | ||
| */ | ||
| async function diffBatchCommand(ref, options) { | ||
| const merged = mergeWithConfig(options, loadConfig(options.directory), [ | ||
| "format", | ||
| "theme", | ||
| "layout", | ||
| "showUnchanged", | ||
| "ignorePosition", | ||
| "ignoreMetadata" | ||
| ]); | ||
| const gitRef = parseGitRef(ref); | ||
| const cwd = merged.directory || process.cwd(); | ||
| const outputDir = options.outputDir || "./diffs"; | ||
| try { | ||
| git("rev-parse --show-toplevel", cwd); | ||
| } catch { | ||
| throw new Error("Not a git repository"); | ||
| } | ||
| const changedFiles = getChangedDiagramFiles(gitRef.base, gitRef.target, cwd); | ||
| if (changedFiles.length === 0) { | ||
| if (!merged.quiet) console.error("No changed diagram files found."); | ||
| return; | ||
| } | ||
| if (!merged.quiet) console.error(`Found ${changedFiles.length} changed diagram file(s). Generating diffs...`); | ||
| const { mkdirSync } = await import("fs"); | ||
| mkdirSync(resolve(cwd, outputDir), { recursive: true }); | ||
| for (const file of changedFiles) try { | ||
| const diffOutput = await diffCommandInternal(gitRef, file, { | ||
| format: merged.format ?? "html", | ||
| theme: merged.theme ?? "light", | ||
| layout: merged.layout ?? "side-by-side", | ||
| showUnchanged: merged.showUnchanged ?? "show", | ||
| ignorePosition: merged.ignorePosition ?? true, | ||
| directory: cwd, | ||
| quiet: true | ||
| }); | ||
| const outputPath = resolve(cwd, outputDir, file.replace(/[^a-zA-Z0-9]/g, "_") + ".diff." + (merged.format ?? "html")); | ||
| writeFileSync(outputPath, diffOutput, "utf-8"); | ||
| if (!merged.quiet) console.error(` \u2713 ${file} \u2192 ${outputPath}`); | ||
| } catch (error) { | ||
| console.error(` \u2717 ${file}:`, error instanceof Error ? error.message : String(error)); | ||
| } | ||
| if (!merged.quiet) console.error("Done!"); | ||
| } | ||
| /** | ||
| * diagrams diff list <ref> command | ||
| * List changed diagram files between git refs | ||
| */ | ||
| async function diffListCommand(ref, options) { | ||
| const gitRef = parseGitRef(ref); | ||
| const cwd = options.directory || process.cwd(); | ||
| try { | ||
| git("rev-parse --show-toplevel", cwd); | ||
| } catch { | ||
| throw new Error("Not a git repository"); | ||
| } | ||
| const changedFiles = getChangedDiagramFiles(gitRef.base, gitRef.target, cwd); | ||
| if (changedFiles.length === 0) { | ||
| if (!options.quiet) console.error("No changed diagram files found."); | ||
| return; | ||
| } | ||
| for (const file of changedFiles) console.log(file); | ||
| } | ||
| async function diffCommandInternal(gitRef, filePath, options) { | ||
| const cwd = options.directory || process.cwd(); | ||
| const beforeContent = getFileContent(filePath, gitRef.base, cwd); | ||
| const afterContent = gitRef.target ? getFileContent(filePath, gitRef.target, cwd) : getWorkingContent(filePath, cwd); | ||
| if (!beforeContent || !afterContent) throw new Error(`Could not read file at both refs: ${filePath}`); | ||
| const beforeJSON = await extractDiagramJSON(beforeContent, filePath, cwd); | ||
| const afterJSON = await extractDiagramJSON(afterContent, filePath, cwd); | ||
| const { computeDiff, renderDiff } = await import("diagrams-js"); | ||
| return renderDiff(computeDiff(beforeJSON, afterJSON, { ignore: { | ||
| position: options.ignorePosition ?? true, | ||
| metadata: options.ignoreMetadata ?? false | ||
| } }), beforeJSON, afterJSON, { | ||
| format: options.format ?? "html", | ||
| theme: options.theme ?? "light", | ||
| layout: options.layout ?? "side-by-side", | ||
| showUnchanged: options.showUnchanged ?? "show", | ||
| showLegend: true, | ||
| showSummary: false, | ||
| hoverDetails: true | ||
| }); | ||
| } | ||
| function parseGitRef(ref) { | ||
| if (ref.includes("...")) { | ||
| const [base, target] = ref.split("..."); | ||
| return { | ||
| base: base.trim(), | ||
| target: target.trim() | ||
| }; | ||
| } | ||
| if (ref.includes("..")) { | ||
| const [base, target] = ref.split(".."); | ||
| return { | ||
| base: base.trim(), | ||
| target: target.trim() | ||
| }; | ||
| } | ||
| return { base: ref.trim() }; | ||
| } | ||
| function git(command, cwd) { | ||
| try { | ||
| return execSync(`git ${command}`, { | ||
| encoding: "utf-8", | ||
| cwd: cwd || process.cwd(), | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "pipe" | ||
| ], | ||
| windowsHide: true | ||
| }).trim(); | ||
| } catch (error) { | ||
| throw new Error(`Git command failed: git ${command}\n${error}`); | ||
| } | ||
| } | ||
| function getFileContent(filePath, ref, cwd) { | ||
| try { | ||
| return git(`show "${ref}:${filePath}"`, cwd); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function getWorkingContent(filePath, cwd) { | ||
| const fullPath = resolve(cwd || process.cwd(), filePath); | ||
| if (!existsSync(fullPath)) return null; | ||
| return readFileSync(fullPath, "utf-8"); | ||
| } | ||
| function getChangedDiagramFiles(baseRef, targetRef, cwd) { | ||
| const output = git(targetRef ? `diff --name-only "${baseRef}"..."${targetRef}"` : `diff --name-only "${baseRef}"`, cwd); | ||
| if (!output) return []; | ||
| return output.split("\n").filter((file) => file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".json") || file.endsWith(".svg") || file.endsWith(".yaml") || file.endsWith(".yml")).filter((file) => !file.includes("node_modules/")); | ||
| } | ||
| async function extractDiagramJSON(fileContent, filePath, cwd) { | ||
| const { Diagram } = await import("diagrams-js"); | ||
| if (filePath.endsWith(".svg")) try { | ||
| return (await Diagram.fromSVG(fileContent)).toJSON(); | ||
| } catch { | ||
| throw new Error(`Invalid or unsupported SVG file: ${filePath}`); | ||
| } | ||
| if (filePath.endsWith(".json")) try { | ||
| return JSON.parse(fileContent); | ||
| } catch { | ||
| throw new Error(`Invalid JSON in file: ${filePath}`); | ||
| } | ||
| const { writeFileSync, rmSync, mkdirSync } = await import("fs"); | ||
| const { join } = await import("path"); | ||
| const isTypeScript = filePath.endsWith(".ts"); | ||
| const ext = isTypeScript ? ".ts" : ".js"; | ||
| const importPath = "file://" + (cwd ? resolve(cwd, filePath) : resolve(filePath)).replace(/\\/g, "/"); | ||
| const projectRoot = cwd || process.cwd(); | ||
| const wrapperDir = join(projectRoot, ".diagrams-cli"); | ||
| if (!existsSync(wrapperDir)) mkdirSync(wrapperDir, { recursive: true }); | ||
| const wrapperContent = ` | ||
| import { Diagram } from "diagrams-js"; | ||
| import * as diagramModule from "${importPath}"; | ||
| async function main() { | ||
| const exported = diagramModule.default || diagramModule; | ||
| if (exported && typeof exported.toJSON === "function") { | ||
| console.log(JSON.stringify(exported.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| if (typeof exported === "function") { | ||
| const result = await exported(); | ||
| if (result && typeof result.toJSON === "function") { | ||
| console.log(JSON.stringify(result.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| } | ||
| for (const key of Object.keys(diagramModule)) { | ||
| const val = diagramModule[key]; | ||
| if (val && typeof val.toJSON === "function") { | ||
| console.log(JSON.stringify(val.toJSON(), null, 2)); | ||
| return; | ||
| } | ||
| } | ||
| console.error("No diagram found in module"); | ||
| process.exit(1); | ||
| } | ||
| main().catch(err => { | ||
| console.error("Error:", err); | ||
| process.exit(1); | ||
| }); | ||
| `; | ||
| const wrapperPath = join(wrapperDir, `diff-wrapper-${Date.now()}${ext}`); | ||
| writeFileSync(wrapperPath, wrapperContent, "utf-8"); | ||
| try { | ||
| const output = execSync(`node ${(isTypeScript ? ["--experimental-strip-types"] : []).join(" ")} "${wrapperPath}"`, { | ||
| encoding: "utf-8", | ||
| cwd: projectRoot, | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "pipe" | ||
| ], | ||
| timeout: 3e4, | ||
| windowsHide: true | ||
| }); | ||
| rmSync(wrapperPath, { force: true }); | ||
| return JSON.parse(output.trim()); | ||
| } catch (error) { | ||
| try { | ||
| rmSync(wrapperPath, { force: true }); | ||
| } catch {} | ||
| const jsonCommentMatch = fileContent.match(/\/\/\s*diagram-json:\s*(\{[\s\S]*?\})/); | ||
| if (jsonCommentMatch) try { | ||
| return JSON.parse(jsonCommentMatch[1]); | ||
| } catch {} | ||
| throw new Error(`Failed to extract diagram JSON from ${filePath}:\n` + (error instanceof Error ? error.message : String(error))); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/commands/init.ts | ||
| /** | ||
| * diagrams init [name] command | ||
| * Scaffold a new diagram file | ||
| */ | ||
| const templates = { | ||
| basic: (name) => `import { Diagram, Node } from "diagrams-js"; | ||
| const diagram = Diagram("${name}"); | ||
| const web = diagram.add(Node("Web Server")); | ||
| const db = diagram.add(Node("Database")); | ||
| web.to(db); | ||
| export default diagram; | ||
| `, | ||
| aws: (name) => `import { Diagram } from "diagrams-js"; | ||
| import { EC2 } from "diagrams-js/aws/compute"; | ||
| import { RDS } from "diagrams-js/aws/database"; | ||
| import { S3 } from "diagrams-js/aws/storage"; | ||
| const diagram = Diagram("${name}"); | ||
| const web = diagram.add(EC2("Web Server")); | ||
| const db = diagram.add(RDS("Database")); | ||
| const storage = diagram.add(S3("Storage")); | ||
| web.to(db); | ||
| web.to(storage); | ||
| export default diagram; | ||
| `, | ||
| k8s: (name) => `import { Diagram } from "diagrams-js"; | ||
| import { Deploy } from "diagrams-js/k8s/compute"; | ||
| import { SVC } from "diagrams-js/k8s/network"; | ||
| const diagram = Diagram("${name}"); | ||
| const deploy = diagram.add(Deploy("App Deployment")); | ||
| const svc = diagram.add(SVC("App Service")); | ||
| svc.to(deploy); | ||
| export default diagram; | ||
| ` | ||
| }; | ||
| async function initCommand(name, options = {}) { | ||
| const diagramName = name || "My Architecture"; | ||
| const templateName = options.template || "basic"; | ||
| const outputPath = options.output || "diagram.ts"; | ||
| if (!templates[templateName]) throw new Error(`Unknown template: ${templateName}. Available: ${Object.keys(templates).join(", ")}`); | ||
| const fullPath = resolve(process.cwd(), outputPath); | ||
| if (existsSync(fullPath)) throw new Error(`File already exists: ${fullPath}`); | ||
| writeFileSync(fullPath, templates[templateName](diagramName), "utf-8"); | ||
| if (!options.quiet) console.error(`Created ${outputPath} using template "${templateName}"`); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/watch.ts | ||
| /** | ||
| * diagrams watch <file> command | ||
| * Watch a diagram file and re-render on changes | ||
| */ | ||
| async function watchCommand(file, options) { | ||
| const merged = mergeWithConfig(options, loadConfig(options.config), [ | ||
| "format", | ||
| "theme", | ||
| "direction", | ||
| "curveStyle", | ||
| "width", | ||
| "height", | ||
| "scale" | ||
| ]); | ||
| const outputFormat = merged.format || (merged.output ? formatFromPath(merged.output) : void 0) || "svg"; | ||
| let outputPath; | ||
| if (merged.output) outputPath = merged.output; | ||
| else outputPath = `${resolve(process.cwd(), file).replace(/\.[^.]+$/, "")}.${outputFormat}`; | ||
| if (!merged.quiet) { | ||
| console.error(`Watching ${file} for changes...`); | ||
| console.error(`Output: ${outputPath} (${outputFormat})`); | ||
| } | ||
| await renderOnce(file, merged, outputFormat, outputPath); | ||
| const watcher = watch(file, { | ||
| persistent: true, | ||
| ignoreInitial: true | ||
| }); | ||
| watcher.on("change", async () => { | ||
| if (!merged.quiet) console.error(`\n[change] ${file}`); | ||
| try { | ||
| await renderOnce(file, merged, outputFormat, outputPath); | ||
| } catch (error) { | ||
| console.error("Render failed:", error instanceof Error ? error.message : String(error)); | ||
| } | ||
| }); | ||
| process.on("SIGINT", () => { | ||
| watcher.close().then(() => process.exit(0)); | ||
| }); | ||
| process.on("SIGTERM", () => { | ||
| watcher.close().then(() => process.exit(0)); | ||
| }); | ||
| } | ||
| async function renderOnce(file, options, outputFormat, outputPath) { | ||
| const spinner = !options.quiet ? createSpinner("Rendering...") : null; | ||
| spinner?.start(); | ||
| try { | ||
| const { diagram } = await loadDiagram(file, process.cwd()); | ||
| if (options.theme) diagram.theme = options.theme; | ||
| if (options.direction) diagram.direction = options.direction; | ||
| if (options.curveStyle) diagram.curveStyle = options.curveStyle; | ||
| const renderOptions = { format: outputFormat }; | ||
| if (options.width !== void 0) renderOptions.width = options.width; | ||
| if (options.height !== void 0) renderOptions.height = options.height; | ||
| if (options.scale !== void 0) renderOptions.scale = options.scale; | ||
| const result = await diagram.render(renderOptions); | ||
| const dir = dirname(outputPath); | ||
| if (!existsSync$1(dir)) mkdirSync(dir, { recursive: true }); | ||
| if (typeof result === "string") writeFileSync(outputPath, result, "utf-8"); | ||
| else writeFileSync(outputPath, Buffer.from(result)); | ||
| spinner?.stop(`Rendered to ${outputPath}`); | ||
| } catch (error) { | ||
| spinner?.stop("Failed"); | ||
| throw error; | ||
| } | ||
| } | ||
| function existsSync$1(path) { | ||
| try { | ||
| const { statSync } = __require("fs"); | ||
| statSync(path); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/commands/plugins.ts | ||
| /** | ||
| * diagrams plugins <subcommand> command | ||
| * Discover and manage plugins | ||
| */ | ||
| async function pluginsListCommand(options = {}) { | ||
| const plugins = discoverPlugins(process.cwd()); | ||
| if (plugins.length === 0) { | ||
| if (!options.quiet) { | ||
| console.error("No diagrams-js plugins found."); | ||
| console.error("Install plugins with: npm install @diagrams-js/plugin-<name>"); | ||
| } | ||
| return; | ||
| } | ||
| if (!options.quiet) console.error(`Found ${plugins.length} plugin(s):\n`); | ||
| for (const plugin of plugins) { | ||
| console.log(`${plugin.name}`); | ||
| console.log(` Package: ${plugin.packageName}`); | ||
| console.log(` Version: ${plugin.version}`); | ||
| if (plugin.description) console.log(` Description: ${plugin.description}`); | ||
| console.log(""); | ||
| } | ||
| } | ||
| async function pluginsInfoCommand(name, options = {}) { | ||
| try { | ||
| const plugin = await loadPlugin(name, process.cwd()); | ||
| if (!options.quiet) { | ||
| console.log(`Plugin: ${plugin.name}`); | ||
| console.log(`Version: ${plugin.version}`); | ||
| console.log(`API Version: ${plugin.apiVersion}`); | ||
| console.log(`Runtime Support:`); | ||
| console.log(` Node.js: ${plugin.runtimeSupport.node ? "Yes" : "No"}`); | ||
| console.log(` Browser: ${plugin.runtimeSupport.browser ? "Yes" : "No"}`); | ||
| console.log(` Deno: ${plugin.runtimeSupport.deno ? "Yes" : "No"}`); | ||
| console.log(` Bun: ${plugin.runtimeSupport.bun ? "Yes" : "No"}`); | ||
| console.log(`Capabilities:`); | ||
| for (const cap of plugin.capabilities) { | ||
| console.log(` - ${cap.type}: ${cap.name || ""}`); | ||
| if (cap.type === "importer") console.log(` Extensions: ${cap.extensions?.join(", ") || "N/A"}`); | ||
| if (cap.type === "exporter") console.log(` Extension: ${cap.extension || "N/A"}`); | ||
| } | ||
| if (plugin.dependencies && plugin.dependencies.length > 0) console.log(`Dependencies: ${plugin.dependencies.join(", ")}`); | ||
| } | ||
| } catch (error) { | ||
| console.error(`Failed to load plugin "${name}": ${error instanceof Error ? error.message : String(error)}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/cli.ts | ||
| const program = new Command(); | ||
| program.name("diagrams").description("CLI for diagrams-js - render, export, diff, and manage architecture diagrams").version("0.1.0").configureOutput({ | ||
| writeErr: (str) => process.stderr.write(str), | ||
| outputError: (str, write) => write(`Error: ${str}`) | ||
| }); | ||
| program.option("-q, --quiet", "suppress non-error output"); | ||
| program.option("-C, --config <path>", "path to config file"); | ||
| program.command("render").description("Render a diagram file to SVG, PNG, JPG, DOT, or JSON (supports plugin import for .yaml/.yml)").argument("[file]", "diagram file (.ts, .js, .json, .svg, .yaml, .yml) or - for stdin").option("-o, --output <path>", "output file path (default: same name as input with .svg extension)").option("--stdout", "output to stdout (in addition to --output if both are set)").option("-f, --format <format>", "output format (svg|png|jpg|dot|json)").option("-p, --plugin <name>", "plugin to use for importing .yaml/.yml files (auto-detected if omitted)").option("-t, --theme <theme>", "color theme").option("-d, --direction <dir>", "layout direction (TB|BT|LR|RL)").option("--curve-style <style>", "edge curve style (ortho|curved|spline|polyline)").option("--width <px>", "output width for PNG/JPG", parseInt).option("--height <px>", "output height for PNG/JPG", parseInt).option("--scale <n>", "scale factor for PNG/JPG", parseFloat).option("--data-url", "output as data URL").option("--no-embed-data", "disable embedding diagram data in SVG").action(async (file, options) => { | ||
| try { | ||
| await renderCommand(file, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.command("export").description("Export a diagram to an external format").argument("<file>", "diagram file (.ts, .js, .json, .svg) or - for stdin").requiredOption("-f, --format <format>", "export format (e.g., docker-compose, kubernetes)").option("-o, --output <path>", "output file path (default: same name with format extension)").option("--stdout", "output to stdout (in addition to --output if both are set)").option("-p, --plugin <name>", "plugin to use (defaults to format name)").action(async (file, options) => { | ||
| try { | ||
| await exportCommand(file, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| const diffCmd = program.command("diff").description("Generate visual diffs of diagrams in git workflows"); | ||
| diffCmd.command("show").description("Show diff for a specific file between git refs").argument("<ref>", "git ref (e.g., HEAD, main...feature, abc123..def456)").argument("<file>", "diagram file path (used for git lookup and output naming)").option("-o, --output <path>", "output file path (default: <filename>-diff.html)").option("--stdout", "output to stdout (in addition to --output if both are set)").option("-F, --format <format>", "diff output format (html|svg)", "html").option("-t, --theme <theme>", "theme (light|dark)", "light").option("-l, --layout <layout>", "layout (side-by-side|stacked)", "side-by-side").option("-u, --show-unchanged <mode>", "show unchanged (show|dim|hide)", "show").option("--ignore-position", "ignore position/layout changes", true).option("--ignore-metadata", "ignore metadata changes", false).option("-C, --directory <path>", "working directory for git commands").option("-q, --quiet", "suppress non-error output").action(async (ref, file, options) => { | ||
| try { | ||
| await diffCommand(ref, file, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| diffCmd.command("list").description("List changed diagram files between git refs").argument("<ref>", "git ref (e.g., HEAD, main...feature)").option("-C, --directory <path>", "working directory for git commands").option("-q, --quiet", "output only file paths").action(async (ref, options) => { | ||
| try { | ||
| await diffListCommand(ref, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| diffCmd.command("batch").description("Generate diffs for all changed diagram files").argument("<ref>", "git ref (e.g., HEAD, main...feature)").option("-o, --output-dir <dir>", "output directory for diff files", "./diffs").option("-F, --format <format>", "output format (html|svg)", "html").option("-t, --theme <theme>", "theme (light|dark)", "light").option("-u, --show-unchanged <mode>", "show unchanged (show|dim|hide)", "show").option("--ignore-position", "ignore position/layout changes", true).option("-C, --directory <path>", "working directory for git commands").option("-q, --quiet", "suppress non-error output").action(async (ref, options) => { | ||
| try { | ||
| await diffBatchCommand(ref, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.command("init").description("Scaffold a new diagram file").argument("[name]", "diagram name", "My Architecture").option("-o, --output <path>", "output file path", "diagram.ts").option("-t, --template <template>", "template to use (basic|aws|k8s)", "basic").option("-q, --quiet", "suppress output").action(async (name, options) => { | ||
| try { | ||
| await initCommand(name, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.command("watch").description("Watch a diagram file and re-render on changes").argument("<file>", "diagram file to watch").option("-o, --output <path>", "output file path (default: same name as input with .svg extension)").option("-f, --format <format>", "output format (svg|png|jpg|dot|json)", "svg").option("-t, --theme <theme>", "color theme").option("-d, --direction <dir>", "layout direction (TB|BT|LR|RL)").option("--curve-style <style>", "edge curve style").option("--width <px>", "output width", parseInt).option("--height <px>", "output height", parseInt).option("--scale <n>", "scale factor", parseFloat).option("-q, --quiet", "suppress non-error output").action(async (file, options) => { | ||
| try { | ||
| await watchCommand(file, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| const pluginsCmd = program.command("plugins").description("Discover and manage plugins"); | ||
| pluginsCmd.command("list").description("List available plugins").option("-q, --quiet", "suppress headers").action(async (options) => { | ||
| try { | ||
| await pluginsListCommand(options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| pluginsCmd.command("info").description("Show detailed information about a plugin").argument("<name>", "plugin name").option("-q, --quiet", "suppress output").action(async (name, options) => { | ||
| try { | ||
| await pluginsInfoCommand(name, options); | ||
| } catch (error) { | ||
| console.error("Error:", error instanceof Error ? error.message : String(error)); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| program.parse(); | ||
| //#endregion | ||
| export {}; |
+167
| # @diagrams-js/cli | ||
| CLI for [diagrams-js](https://diagrams-js.hatemhosny.dev) - render, import, export, diff, and manage architecture diagrams from the terminal. | ||
| ## Installation | ||
| Install globally with `npm`: | ||
| ```bash | ||
| npm install -g @diagrams-js/cli | ||
| # then run `diagrams` | ||
| diagrams render diagram.ts | ||
| ``` | ||
| Alternatively, install locally in your project: | ||
| ```bash | ||
| npm install @diagrams-js/cli | ||
| # then run `npx diagrams` | ||
| npx diagrams render diagram.ts | ||
| ``` | ||
| Or use directly with `npx`: | ||
| ```bash | ||
| npx @diagrams-js/cli render diagram.ts | ||
| ``` | ||
| ## Usage | ||
| ### Render a diagram | ||
| ```bash | ||
| # Render to SVG (default) | ||
| diagrams render diagram.ts | ||
| # Render to PNG | ||
| diagrams render diagram.ts -o diagram.png | ||
| # Render with theme and direction | ||
| diagrams render diagram.ts -f svg -t dark -d LR -o out.svg | ||
| # Render to JSON | ||
| diagrams render diagram.json -f json | ||
| ``` | ||
| ### Render from external formats (plugin import) | ||
| The `render` command automatically imports from `.yaml`/`.yml` files via plugins: | ||
| ```bash | ||
| # Import Docker Compose and render to SVG | ||
| diagrams render docker-compose.yml -o architecture.svg | ||
| # Import Kubernetes manifest | ||
| diagrams render k8s-deployment.yaml -p kubernetes -o architecture.svg | ||
| # Import with custom options | ||
| diagrams render compose.yml -f png -t dark --width 1200 -o out.png | ||
| ``` | ||
| ### Export to external formats | ||
| ```bash | ||
| # Export diagram to Docker Compose | ||
| diagrams export diagram.json -f docker-compose -o docker-compose.yml | ||
| # Export to Kubernetes | ||
| diagrams export diagram.ts -f kubernetes -o manifest.yaml | ||
| ``` | ||
| ### Diff diagrams in git | ||
| ```bash | ||
| # Diff a single file against HEAD | ||
| diagrams diff show HEAD diagram.ts -o diff.html | ||
| # Diff between branches | ||
| diagrams diff show main...feature diagram.json -F html -o diff.html | ||
| # List changed diagram files | ||
| diagrams diff list HEAD | ||
| # Batch diff all changed files | ||
| diagrams diff batch main...feature -o ./diffs | ||
| ``` | ||
| ### Scaffold a new diagram | ||
| ```bash | ||
| # Create a basic diagram | ||
| diagrams init "My Architecture" | ||
| # Create with AWS template | ||
| diagrams init "AWS Stack" -t aws -o aws.ts | ||
| # Create with Kubernetes template | ||
| diagrams init "K8s Cluster" -t k8s -o k8s.ts | ||
| ``` | ||
| ### Watch and auto-render | ||
| ```bash | ||
| # Watch a diagram file and re-render on changes | ||
| diagrams watch diagram.ts -o out.svg | ||
| # Watch with custom options | ||
| diagrams watch diagram.ts -f png -t dark --scale 2 -o out.png | ||
| ``` | ||
| ### Manage plugins | ||
| ```bash | ||
| # List installed plugins | ||
| diagrams plugins list | ||
| # Show plugin info | ||
| diagrams plugins info docker-compose | ||
| ``` | ||
| ## Configuration | ||
| Create a `.diagramsrc.json` file in your project root: | ||
| ```json | ||
| { | ||
| "format": "svg", | ||
| "theme": "light", | ||
| "direction": "TB", | ||
| "curveStyle": "ortho", | ||
| "scale": 2, | ||
| "diff": { | ||
| "layout": "side-by-side", | ||
| "showUnchanged": "show", | ||
| "ignorePosition": true | ||
| } | ||
| } | ||
| ``` | ||
| ## Supported File Formats | ||
| ### Input | ||
| | Extension | Description | | ||
| | ---------------- | --------------------------------------------- | | ||
| | `.ts` | TypeScript diagram file | | ||
| | `.js` / `.mjs` | JavaScript diagram file | | ||
| | `.json` | Diagram JSON export | | ||
| | `.svg` | Diagram SVG with embedded metadata | | ||
| | `.yaml` / `.yml` | Importable config (docker-compose, k8s, etc.) | | ||
| ### Output | ||
| | Format | Description | | ||
| | ------ | -------------------------------------- | | ||
| | `svg` | Scalable Vector Graphics (default) | | ||
| | `png` | PNG image (requires sharp in Node.js) | | ||
| | `jpg` | JPEG image (requires sharp in Node.js) | | ||
| | `dot` | Graphviz DOT source | | ||
| | `json` | Diagram JSON serialization | | ||
| | `html` | Self-contained HTML diff | | ||
| ## License | ||
| MIT |
+54
-4
| { | ||
| "name": "@diagrams-js/cli", | ||
| "version": "0.0.0", | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "MIT" | ||
| "version": "0.1.0", | ||
| "description": "CLI for diagrams-js - render, import, export, diff, and manage architecture diagrams", | ||
| "keywords": [ | ||
| "architecture", | ||
| "cli", | ||
| "diagram-as-code", | ||
| "diagrams", | ||
| "diagrams-js", | ||
| "infrastructure", | ||
| "visualization" | ||
| ], | ||
| "homepage": "https://diagrams-js.hatemhosny.dev", | ||
| "license": "MIT", | ||
| "author": "Hatem Hosny", | ||
| "repository": { | ||
| "url": "https://github.com/diagrams-js/cli" | ||
| }, | ||
| "bin": { | ||
| "diagrams": "./dist/cli.mjs" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "README.md", | ||
| "LICENSE" | ||
| ], | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vp pack", | ||
| "dev": "vp pack --watch", | ||
| "test": "vp test", | ||
| "check": "vp check", | ||
| "fix": "vp check --fix", | ||
| "prepublishOnly": "vp run build" | ||
| }, | ||
| "dependencies": { | ||
| "chokidar": "^4.0.0", | ||
| "commander": "^12.0.0", | ||
| "diagrams-js": "^0.7.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
| "typescript": "^6.0.2", | ||
| "vite-plus": "^0.1.16" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "packageManager": "pnpm@10.33.0", | ||
| "pnpm": { | ||
| "overrides": { | ||
| "vite": "npm:@voidzero-dev/vite-plus-core@latest", | ||
| "vitest": "npm:@voidzero-dev/vite-plus-test@latest" | ||
| } | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
44594
40811.93%3
200%1035
Infinity%1
-50%1
-50%0
-100%168
Infinity%Yes
NaN3
Infinity%3
Infinity%2
100%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added