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>
214 lines
10 KiB
Markdown
214 lines
10 KiB
Markdown
# diagrams-drawio — 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 <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):
|
|
|
|
```bash
|
|
cd <skill-manager>/scripts
|
|
task deploy -- --skill-dir="/absolute/path/to/diagrams-drawio"
|
|
```
|
|
|
|
Verify the CLI works after installation:
|
|
|
|
```bash
|
|
cd <skill>/scripts
|
|
node dist/cli/commands.js -f <diagram.drawio> -a summary
|
|
```
|
|
|
|
---
|
|
|
|
## Run the CLI
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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`:
|
|
```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`:
|
|
```ts
|
|
"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
|
|
|
|
```typescript
|
|
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"),
|
|
};
|
|
```
|