Source importers: lstat before open so FIFOs no longer hang the directory walk; replace the quadratic Rust macro regex with a linear scan. Three-way sync: report page add/add and delete-vs-modify as conflicts instead of silently overwriting or resurrecting; canonical comparison so key order is not a change; the sync action withholds output and fails on conflicts unless --force. Story publishing escapes <, >, & and U+2028/9 inside the embedded JSON. Views: drop parentId of unselected containers, re-layout instead of manual geometry, validate before serialising; flatten() merges identical nodes repeated across pages so C4 output works with every analysis action. Dark theme edge labels get a background; tube-map corridors sit above the stations; C4 containers keep the swimlane style and orphan relationships land on the matching page; router keeps container header bands as obstacles; sequence self-messages loop on one side. Docs: SKILL.md lists all 23 CLI actions and how each capability family is invoked, task wrappers for query/test/what-if/doctor, capability tables and --page scope corrected, maintenance snippet uses the real synchronous action signature, duplicated rule bullets moved to the rule references, coverage matrix wording made verifiable. Co-Authored-By: Claude Code <noreply@anthropic.com>
10 KiB
diagrams-drawio — Maintenance Guide
This document is for developers maintaining or extending the drawio-tools CLI.
For agent usage instructions, see SKILL.md.
The CLI lives under the skill's scripts/ directory (scripts/src/ → scripts/dist/),
per the skill-manager structure conventions. All commands below run from scripts/.
Tech stack
| Layer | Library / Tool | Version |
|---|---|---|
| Language | TypeScript ESM (NodeNext) |
— |
| Package manager | pnpm | v11.9+ |
| XML model | @maxgraph/core |
v0.23 |
| DOM polyfill | jsdom |
— |
| Deflate decode | pako |
— |
| YAML output | js-yaml |
— |
| Build | tsc |
— |
| Dev runner | tsx |
— |
pnpm-workspace.yaml must include:
allowBuilds:
esbuild: true
Install and deploy
The git repository is the source of truth. Install dependencies and build inside scripts/:
cd <skill>/scripts
pnpm install --frozen-lockfile
task build # or: pnpm run build
Deploy with skill-manager (creates absolute-path symlinks in $HOME/.agents/skills
and $HOME/.claude/skills, so the repo working copy stays the single source of truth):
cd <skill-manager>/scripts
task deploy -- --skill-dir="/absolute/path/to/diagrams-drawio"
Verify the CLI works after installation:
cd <skill>/scripts
node dist/cli/commands.js -f <diagram.drawio> -a summary
Run the CLI
cd <skill>/scripts
task run -- --file="<diagram.drawio>" --action=<action>
# or directly:
node dist/cli/commands.js -f <diagram.drawio> -a <action>
# or build-and-run during development:
pnpm run cli -f <diagram.drawio> -a <action>
Note: with
pnpm run cli, pass arguments directly aftercli— do not use--separator.
Build (development only)
Build only (no run):
cd <skill>/scripts
task build # tsc → dist/
Source structure
src/
├── cli/
│ └── commands.ts # parseArgs dispatcher → dynamic action imports
├── authoring/
│ └── ir-to-drawio.ts # deterministic IR v1/v2 serializer + preservation envelopes
├── model/
│ └── diagram-ir.ts # canonical types, normalization, runtime validation
├── services/
│ ├── drawio-parser/
│ │ ├── parser.ts # parseAllPages() / parseDiagram() — Shape, Edge, ParsedPage
│ │ └── page-summary.ts # buildPageSummary() — shared per-page serialisation helper
│ ├── hierarchy-builder/
│ │ └── hierarchy-builder.ts # buildHierarchy() — shared BFS depth map + containment tree
│ ├── semantic-lifecycle/ # import, edit, views/query/policy/what-if, sync, story, atomic I/O
│ ├── layout/, authoring-router/ # deterministic layout + obstacle-aware routing used by build (ir-to-drawio)
│ ├── connector-router/ # edge path reconstruction used by page-connectors-summary/-validation
│ ├── maxgraph-loader/ # jsdom polyfill + maxGraph state loader (used by validate and negative-space)
│ └── source-importers/, transforms/, themes/, shape-catalog/, profiles/ # library-only services (no CLI action)
└── actions/
├── build|import|edit|views|query|test|what-if|sync|story|doctor/
│ # authoring and semantic lifecycle actions
├── summary/ # all pages inventory
├── page-summary/ # single page inventory (uses --page)
├── page-hierarchy/ # containment tree from parentId
├── page-connectors-summary/ # connector stats
├── page-connectors-validation/ # overlap + crossing detection
├── page-labels-validation/ # label quality checks
├── page-shape-bbox-validation/ # bounding box overlap detection
├── page-orphans/ # isolated shapes + dangling connectors
├── page-recommendations/ # page size recommendation
├── page-hierarchy-full/ # nesting levels with full shape geometry (x, y, width, height)
├── page-negative-space-summary/ # free horizontal corridors per nesting level (bbox + text-aware)
├── quality/ # all-pages clipping/overflow/placeholder/palette/density checks with severity
└── validate/ # MANDATORY final gate — XML well-formedness + maxGraph compile + sanity check
Each action exports run(filePath, pageIndex?, outputPath?, options?): Record<string, unknown>. Analysis actions may ignore lifecycle-only parameters. File-producing actions must require --output and use the shared atomic writer.
Key modules
src/services/drawio-parser/parser.ts
parseAllPages(filePath)— parses all<diagram>tabs in an mxfile, returnsParsedPage[]parseDiagram(filePath)— backward-compat wrapper, returns first page only- Multi-page support: regex extracts all
<diagram>blocks; each decoded separately (base64 + pakoinflateRaw) - Uses
@maxgraph/coreModelXmlSerializer+GraphDataModelwith ajsdomDOM polyfill
src/services/drawio-parser/page-summary.ts
buildPageSummary(page: ParsedPage): PageSummaryResult— shared helper used by bothsummaryandpage-summaryactions
src/services/hierarchy-builder/hierarchy-builder.ts
buildHierarchy(page: ParsedPage): HierarchyResult— shared BFS depth map + containment tree- Returns:
tree,allNodes,depthMap,childrenOf,maxDepth,totalLevels(= maxDepth + 1),depthCounts - Used by
page-hierarchy(tree output) andpage-hierarchy-full(geometry per nesting level)
src/cli/commands.ts
parseArgsdispatcher → dynamic imports of action modules- Common options:
--file/-f,--action/-a,--page/-p,--output/-o,--help/-h - Lifecycle options:
--spec,--base,--strict,--prune,--dry-run,--fail,--views,--kind, repeatable--property,--from, and--to doctoris the only action that does not require--file- All output serialised to YAML on stdout; exit
0success,1error
Adding a new action
- Create
src/actions/<name>/action.tsexporting a synchronousrun(the dispatcher incommands.tsreadsresult.summary/result.failedwithoutawait, so anasyncfunction or a returnedPromisewould break exit-code handling). Copy the signature from an existing action, e.g.src/actions/page-hierarchy/action.ts:The fullexport function run(filePath: string, pageIndex: number = 0): Record<string, unknown> { const pages = parseAllPages(filePath); const page = pages[pageIndex]; if (!page) { return { error: true, message: `Page index ${pageIndex} not found` }; } // ... return { action: "<name>", /* ... */ }; }ActionModulecontract isrun(filePath: string, pageIndex?: number, outputPath?: string, options?: LifecycleActionOptions): Record<string, unknown>; declare only the parameters you use. Setfailed: truein the result to make the CLI exit with code1. - Register it in
src/cli/commands.tsunderACTIONS:"my-action": () => import("../actions/my-action/action.js"), - Use
parseAllPages(filePath)[pageIndex]to honour--page, orparseDiagram(filePath)when the action is deliberately page-0-only;parseAllPagesfor all-page actions. Document the choice in the Scope column ofcapabilities.md - Optionally import
buildPageSummary(page)frompage-summary.tsfor standard shape/edge serialisation - Return a plain object — the CLI serialises it to YAML automatically
- Run
task build(orpnpm run build) to compile and verify no TypeScript errors, then add the action tocapabilities.md, theActions:list incommands.ts --help, and the Taskfile descriptions
Naming convention
Actions follow {object}-{action} naming:
page-*— operates on a single diagram page (page-summary,page-hierarchy,page-hierarchy-full,page-negative-space-summaryhonour--page; the remainingpage-*actions currently analyse page 0 only)summary,validate,quality— operate on all pages
CLI action registration map
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"),
};