185 lines
8.0 KiB
JavaScript
185 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* drawio-tools CLI dispatcher
|
|
*
|
|
* Usage:
|
|
* drawio-tools --file <path-to-drawio> --action <action-name> [--page <index>]
|
|
*
|
|
* 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<string, unknown>;
|
|
};
|
|
|
|
const ACTIONS: Record<string, () => Promise<ActionModule>> = {
|
|
"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<void> {
|
|
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 <path> --action <action> [--page <index>] [--output <path>]
|
|
|
|
Lifecycle options:
|
|
--spec <path> Edit batch, policy list, or incoming sync model
|
|
--base <path> 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 <node-id> Failed node for what-if or story overlay
|
|
--views <csv> executive,system,deployment,dataflow,security
|
|
--kind <kind> Query node kind
|
|
--property k=v Repeatable exact property query
|
|
--from <node-id> Directed path start (use with --to)
|
|
--to <node-id> 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<string, unknown>).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 <path> --action <${Object.keys(ACTIONS).join("|")}> [--page <index>] [--output <path>]\n`
|
|
);
|
|
}
|
|
|
|
main();
|