Files
diagrams-drawio/references/maintenance.md
oleg-lukasonokandClaude Code 6584df0666 Fix review findings: importer DoS, sync data loss, views/profile rendering, docs
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>
2026-09-06 17:01:50 +03:00

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 after cli — 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, returns ParsedPage[]
  • parseDiagram(filePath) — backward-compat wrapper, returns first page only
  • Multi-page support: regex extracts all <diagram> blocks; each decoded separately (base64 + pako inflateRaw)
  • Uses @maxgraph/core ModelXmlSerializer + GraphDataModel with a jsdom DOM polyfill

src/services/drawio-parser/page-summary.ts

  • buildPageSummary(page: ParsedPage): PageSummaryResult — shared helper used by both summary and page-summary actions

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) and page-hierarchy-full (geometry per nesting level)

src/cli/commands.ts

  • parseArgs dispatcher → 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
  • doctor is the only action that does not require --file
  • All output serialised to YAML on stdout; exit 0 success, 1 error

Adding a new action

  1. Create src/actions/<name>/action.ts exporting a synchronous run (the dispatcher in commands.ts reads result.summary / result.failed without await, so an async function or a returned Promise would break exit-code handling). Copy the signature from an existing action, e.g. src/actions/page-hierarchy/action.ts:
    export 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>", /* ... */ };
    }
    
    The full ActionModule contract is run(filePath: string, pageIndex?: number, outputPath?: string, options?: LifecycleActionOptions): Record<string, unknown>; declare only the parameters you use. Set failed: true in the result to make the CLI exit with code 1.
  2. Register it in src/cli/commands.ts under ACTIONS:
    "my-action": () => import("../actions/my-action/action.js"),
    
  3. Use parseAllPages(filePath)[pageIndex] to honour --page, or parseDiagram(filePath) when the action is deliberately page-0-only; parseAllPages for all-page actions. Document the choice in the Scope column of capabilities.md
  4. Optionally import buildPageSummary(page) from page-summary.ts for standard shape/edge serialisation
  5. Return a plain object — the CLI serialises it to YAML automatically
  6. Run task build (or pnpm run build) to compile and verify no TypeScript errors, then add the action to capabilities.md, the Actions: list in commands.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-summary honour --page; the remaining page-* 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"),
};