Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@diagrams-js/cli

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@diagrams-js/cli - npm Package Compare versions

Comparing version
0.0.0
to
0.1.0
+1046
dist/cli.mjs
#!/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 {};
# @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"
}
}
}