# drawio-main — Maintenance Guide This document is for **developers** maintaining or extending the `drawio-tools` CLI. For agent usage instructions, see [SKILL.md](../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: ```yaml allowBuilds: esbuild: true ``` --- ## Install and deploy The git repository is the source of truth. Install dependencies and build inside `scripts/`: ```bash cd /scripts pnpm install 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): ```bash cd /scripts task deploy -- --skill-dir="/absolute/path/to/drawio-main" ``` Verify the CLI works after installation: ```bash cd /scripts node dist/cli/commands.js -f -a summary ``` --- ## Run the CLI ```bash cd /scripts task run -- --file="" --action= # or directly: node dist/cli/commands.js -f -a # or build-and-run during development: pnpm run cli -f -a ``` > Note: with `pnpm run cli`, pass arguments directly after `cli` — do **not** use `--` separator. --- ## Build (development only) Build only (no run): ```bash cd /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 └── 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) └── validate/ # MANDATORY final gate — XML well-formedness + maxGraph compile + sanity check ``` Each action exports `run(filePath, pageIndex?, outputPath?, options?): Record`. 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 `` tabs in an mxfile, returns `ParsedPage[]` - `parseDiagram(filePath)` — backward-compat wrapper, returns first page only - Multi-page support: regex extracts all `` 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//action.ts` exporting: ```ts export async function run(filePath: string, pageIndex?: number): Promise> ``` 2. Register it in `src/cli/commands.ts` under `ACTIONS`: ```ts "my-action": () => import("../actions/my-action/action.js"), ``` 3. Use `parseDiagram(filePath)` (first page) or `parseAllPages(filePath)` (all pages) from the parser 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 `pnpm run build` to compile and verify no TypeScript errors ### Naming convention Actions follow `{object}-{action}` naming: - `page-*` — operates on a single diagram page (uses `--page`, default 0) - `summary` — operates on all pages --- ## CLI action registration map ```typescript 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"), }; ```