#!/usr/bin/env node /** * drawio-tools CLI dispatcher * * Usage: * drawio-tools --file --action [--page ] * * Actions: * summary — inventory of all shapes and edges for every page in the file * page-summary — inventory of shapes and edges for a single page (use --page, default 0) * page-hierarchy — containment tree of shapes (layers → subsystems → components) * page-connectors-summary — summary of all connectors with type, labels, waypoints * page-connectors-validation — detect connector-shape overlaps and connector crossings * page-labels-validation — detect empty, duplicate, or overly long shape labels * page-shape-bbox-validation — detect non-containment bounding box overlaps between shapes * page-orphans — find isolated shapes and dangling connectors * page-recommendations — bounding box analysis and page size recommendation * page-negative-space-summary — free horizontal corridors (negative space) per nesting level * * Output is always YAML to stdout. Exit 0 on success, 1 on error. */ import { parseArgs } from "node:util"; import * as yaml from "js-yaml"; import type { LifecycleActionOptions } from "../services/semantic-lifecycle/lifecycle-io.js"; // --------------------------------------------------------------------------- // Action registry // --------------------------------------------------------------------------- type ActionModule = { run: (filePath: string, pageIndex?: number, outputPath?: string, options?: LifecycleActionOptions) => Record; }; const ACTIONS: Record Promise> = { "build": () => import("../actions/build/action.js"), "summary": () => import("../actions/summary/action.js"), "page-summary": () => import("../actions/page-summary/action.js"), "page-hierarchy": () => import("../actions/page-hierarchy/action.js"), "page-connectors-summary": () => import("../actions/page-connectors-summary/action.js"), "page-connectors-validation": () => import("../actions/page-connectors-validation/action.js"), "page-labels-validation": () => import("../actions/page-labels-validation/action.js"), "page-shape-bbox-validation": () => import("../actions/page-shape-bbox-validation/action.js"), "page-orphans": () => import("../actions/page-orphans/action.js"), "page-recommendations": () => import("../actions/page-recommendations/action.js"), "page-hierarchy-full": () => import("../actions/page-hierarchy-full/action.js"), "page-negative-space-summary":() => import("../actions/page-negative-space-summary/action.js"), "validate": () => import("../actions/validate/action.js"), "quality": () => import("../actions/quality/action.js"), "import": () => import("../actions/import/action.js"), "edit": () => import("../actions/edit/action.js"), "views": () => import("../actions/views/action.js"), "query": () => import("../actions/query/action.js"), "test": () => import("../actions/test/action.js"), "what-if": () => import("../actions/what-if/action.js"), "sync": () => import("../actions/sync/action.js"), "story": () => import("../actions/story/action.js"), "doctor": () => import("../actions/doctor/action.js"), }; // --------------------------------------------------------------------------- // CLI // --------------------------------------------------------------------------- async function main(): Promise { let values: { file?: string; action?: string; page?: string; output?: string; help?: boolean; spec?: string; base?: string; strict?: boolean; prune?: boolean; "dry-run"?: boolean; fail?: string; views?: string; kind?: string; property?: string[]; from?: string; to?: string }; try { ({ values } = parseArgs({ args: process.argv.slice(2), options: { file: { type: "string", short: "f" }, action: { type: "string", short: "a" }, page: { type: "string", short: "p" }, output: { type: "string", short: "o" }, spec: { type: "string" }, base: { type: "string" }, strict: { type: "boolean" }, prune: { type: "boolean" }, "dry-run": { type: "boolean" }, fail: { type: "string" }, views: { type: "string" }, kind: { type: "string" }, property: { type: "string", multiple: true }, from: { type: "string" }, to: { type: "string" }, help: { type: "boolean", short: "h" }, }, strict: true, })); } catch (err) { printError(String(err)); process.exit(1); } if (values.help) { console.log(`drawio-tools --file --action [--page ] [--output ] Lifecycle options: --spec Edit batch, policy list, or incoming sync model --base Previous generated model for three-way sync --strict Treat policy warnings as failures --prune Remove identities absent from incoming sync model --dry-run Return edit/sync preview without writing --fail Failed node for what-if or story overlay --views executive,system,deployment,dataflow,security --kind Query node kind --property k=v Repeatable exact property query --from Directed path start (use with --to) --to Directed path destination (use with --from) Doctor does not require --file. Actions: ${Object.keys(ACTIONS).join(", ")}`); process.exit(0); } if (!values.file && values.action !== "doctor") { printError("Missing required argument: --file"); process.exit(1); } if (!values.action) { printError("Missing required argument: --action"); process.exit(1); } const actionName = values.action; const filePath = values.file ?? ""; // Parse optional --page argument (0-based index, default 0) let pageIndex = 0; if (values.page !== undefined) { if (!/^\d+$/.test(values.page)) { printError(`--page must be a non-negative integer, got: ${values.page}`); process.exit(1); } pageIndex = Number(values.page); if (!Number.isSafeInteger(pageIndex)) { printError(`--page must be a non-negative integer, got: ${values.page}`); process.exit(1); } } if (!ACTIONS[actionName]) { printError( `Unknown action '${actionName}'. Available: ${Object.keys(ACTIONS).join(", ")}` ); process.exit(1); } try { const mod = await ACTIONS[actionName](); const result = mod.run(filePath, pageIndex, values.output, { spec: values.spec, base: values.base, strict: values.strict, prune: values.prune, dryRun: values["dry-run"], fail: values.fail, views: values.views, kind: values.kind, property: values.property, from: values.from, to: values.to, }); process.stdout.write( yaml.dump(result, { noRefs: true, sortKeys: false, lineWidth: 120 }) ); const summary = result.summary; if (actionName === "validate" && summary && typeof summary === "object" && (summary as Record).valid === false) { process.exitCode = 1; } if (result.failed === true) process.exitCode = 1; } catch (err) { const errorOutput = { error: true, action: actionName, file: filePath, message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, }; process.stdout.write(yaml.dump(errorOutput, { noRefs: true, sortKeys: false })); process.exit(1); } } function printError(msg: string): void { process.stderr.write(`Error: ${msg}\n`); process.stderr.write( `Usage: drawio-tools --file --action <${Object.keys(ACTIONS).join("|")}> [--page ] [--output ]\n` ); } main();