@bovinphang/frontend-craft 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +38 -1
  4. package/CHANGELOG.zh-CN.md +37 -1
  5. package/README.md +10 -6
  6. package/README.openclaw.md +3 -1
  7. package/README.openclaw.zh-CN.md +3 -1
  8. package/README.zh-CN.md +10 -6
  9. package/agents/fec-typescript-reviewer.md +1 -1
  10. package/docs/example-prompts.md +81 -69
  11. package/docs/ja-JP/README.md +56 -52
  12. package/docs/ko-KR/README.md +45 -41
  13. package/docs/project-structure.md +8 -4
  14. package/docs/superpowers/plans/2026-05-29-fec-debug.md +491 -0
  15. package/docs/superpowers/specs/2026-05-29-fec-debug-design.md +340 -0
  16. package/docs/zh-CN/example-prompts.md +81 -69
  17. package/docs/zh-CN/project-structure.md +8 -4
  18. package/docs/zh-TW/README.md +33 -29
  19. package/openclaw.plugin.json +1 -1
  20. package/package.json +3 -3
  21. package/skills/eval_queries.json +154 -56
  22. package/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
  23. package/skills/fec-code-review/SKILL.md +1 -1
  24. package/skills/fec-drawio-studio/SKILL.md +69 -0
  25. package/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  26. package/skills/fec-drawio-studio/data/brand-icons.json +878 -0
  27. package/skills/fec-drawio-studio/data/shape-index.json.gz +0 -0
  28. package/skills/fec-drawio-studio/references/data-residency.md +27 -0
  29. package/skills/fec-drawio-studio/references/diagram-patterns.md +41 -0
  30. package/skills/fec-drawio-studio/references/layout-and-export.md +57 -0
  31. package/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
  32. package/skills/fec-drawio-studio/references/xml-and-mermaid.md +54 -0
  33. package/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
  34. package/skills/fec-drawio-studio/scripts/diagram-lint.mjs +143 -0
  35. package/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
  36. package/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
  37. package/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  38. package/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  39. package/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  40. package/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  41. package/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  42. package/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  43. package/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  44. package/skills/fec-drawio-studio/scripts/shape-query.mjs +81 -0
  45. package/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
  46. package/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
  47. package/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
  48. package/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
  49. package/skills/fec-drawio-studio/styles/schema.json +112 -0
  50. package/skills/fec-e2e-testing/SKILL.md +3 -1
  51. package/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
  52. package/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
  53. package/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
  54. package/skills/fec-image-generation/SKILL.md +56 -0
  55. package/skills/fec-image-generation/references/artifact-routing.md +80 -0
  56. package/skills/fec-image-generation/references/diagram-workflows.md +43 -0
  57. package/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
  58. package/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
  59. package/skills/fec-implement-from-design/SKILL.md +7 -0
  60. package/skills/fec-legacy-to-modern-migration/SKILL.md +24 -168
  61. package/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
  62. package/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
  63. package/skills/fec-motion-interaction/SKILL.md +7 -0
  64. package/skills/fec-nextjs-project-standard/SKILL.md +7 -1
  65. package/skills/fec-performance-optimization/SKILL.md +5 -1
  66. package/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
  67. package/skills/fec-performance-optimization/references/report-template.md +6 -0
  68. package/skills/fec-react-project-standard/SKILL.md +11 -3
  69. package/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
  70. package/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
  71. package/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
  72. package/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
  73. package/skills/fec-responsive-layout/references/responsive-layout-patterns.md +11 -0
  74. package/skills/fec-source-driven-development/SKILL.md +26 -0
  75. package/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +5 -1
  76. package/skills/fec-typescript-project-standard/SKILL.md +54 -0
  77. package/skills/{fec-typescript-type-safety → fec-typescript-project-standard}/references/type-safety-patterns.md +1 -1
  78. package/skills/{fec-typescript-type-safety/SKILL.md → fec-typescript-project-standard/references/type-safety.md} +1 -10
  79. package/skills/fec-ui-design/SKILL.md +26 -8
  80. package/skills/fec-ui-design/data/style-archetypes.json +40 -0
  81. package/skills/fec-ui-design/data/ux-quality-rules.json +35 -0
  82. package/skills/fec-ui-design/references/design-intelligence.md +66 -6
  83. package/skills/fec-ui-design/references/pre-delivery-checklist.md +6 -0
  84. package/skills/fec-ui-design/scripts/design-system.mjs +119 -1
  85. package/skills/fec-vue3-project-standard/SKILL.md +11 -2
  86. package/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +163 -0
  87. package/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
  88. package/skills/fec-vue3-project-standard/references/vue3-project-structure.md +177 -0
  89. package/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
  90. package/skills/fec-web-video-presentation/SKILL.md +61 -0
  91. package/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
  92. package/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
  93. package/skills/fec-web-video-presentation/references/content-plan.md +38 -0
  94. package/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
  95. package/skills/fec-web-video-presentation/references/theme-system.md +36 -0
  96. package/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
  97. package/skills/metadata.json +340 -164
  98. package/skills/relations.json +116 -25
  99. package/templates/shared/rules/fec-ci-cd.md +20 -108
  100. package/templates/shared/rules/fec-design-system.md +47 -223
  101. package/templates/shared/rules/fec-error-handling.md +35 -179
  102. package/templates/shared/rules/fec-i18n.md +43 -91
  103. package/templates/shared/rules/fec-refactoring.md +4 -0
  104. package/templates/shared/rules/fec-rendering-patterns.md +28 -171
  105. package/templates/shared/rules/fec-responsive-design.md +34 -211
  106. package/templates/shared/rules/fec-state-management.md +41 -144
  107. package/templates/shared/rules/fec-typescript.md +47 -290
  108. package/skills/fec-e2e-testing/references/e2e-advanced.md +0 -176
  109. package/skills/fec-react-project-standard/references/react-project-details.md +0 -642
  110. package/skills/fec-vue3-project-standard/references/vue3-project-details.md +0 -547
@@ -0,0 +1,27 @@
1
+ # Data Residency
2
+
3
+ Use this checklist when a diagram contains private architecture, unreleased product details, customer data or regulated information.
4
+
5
+ ## Local First
6
+
7
+ - A `.drawio` file written in the workspace is the safest default artifact because it can be inspected, versioned and opened locally.
8
+ - draw.io Desktop export runs on the local machine when the CLI is installed. If the CLI is missing, keep the source file and offer a URL fallback instead of sending the diagram to a cloud converter.
9
+ - `diagram-lint.mjs`, `diagram-url.mjs`, `layout-graph.mjs`, `png-embed-fix.mjs` and code scanners run locally. Graphviz is optional and local when installed.
10
+
11
+ ## URL Modes
12
+
13
+ - Viewer/editor `#R` URLs and `#create=` URLs place compressed diagram content in the URL fragment.
14
+ - URL fragments are not sent to ordinary HTTP servers as request paths, but the browser still loads application code from the chosen diagrams.net host.
15
+ - Very large diagrams can exceed practical browser URL limits. Fall back to a local `.drawio` file when the URL becomes unwieldy.
16
+ - On Windows, open long diagram URLs through a `.url` shortcut file or use the JSON `windowsShortcut` output from `diagram-url.mjs`; passing the URL directly through `cmd start` can lose the fragment.
17
+
18
+ ## External Requests
19
+
20
+ - Brand CDN symbols require network access when rendered unless embedded as data URIs.
21
+ - diagrams.net web editor/viewer loads editor code and assets from the configured host.
22
+ - A self-hosted diagrams.net base URL can be passed with `--base-url` when the environment requires a controlled endpoint.
23
+ - Existing draw.io MCP App or Tool servers may be useful as preview surfaces, but they are optional and governed by the user's configured host and model provider.
24
+
25
+ ## Handoff Rule
26
+
27
+ For sensitive diagrams, deliver the local `.drawio` source plus lint results first. Add PNG/SVG/PDF or URL outputs only when the user explicitly needs them and the data boundary is acceptable.
@@ -0,0 +1,41 @@
1
+ # Diagram Patterns
2
+
3
+ Use these conventions when a user asks for a named technical diagram.
4
+
5
+ ## Architecture
6
+
7
+ - Group nodes by tier: client, edge, service, data, external.
8
+ - Use swimlanes or grouped containers when boundaries matter.
9
+ - Put bus or event-stream nodes near the center of publisher/subscriber rows.
10
+ - Use cylinders for databases, rounded rectangles for services, dashed outlines for external systems.
11
+
12
+ ## ERD
13
+
14
+ - Model tables as containers with rows for columns.
15
+ - Mark primary keys and foreign keys in labels.
16
+ - Use dashed relationship edges when cardinality is not fully known.
17
+ - Prefer top-to-bottom layout for schemas with multiple relation clusters.
18
+
19
+ ## UML Class
20
+
21
+ - Use a class box split into name, fields and methods.
22
+ - Use hollow triangle arrows for inheritance, dashed hollow triangle for interface implementation.
23
+ - Keep interfaces or abstract bases above concrete classes.
24
+
25
+ ## Sequence
26
+
27
+ - Lay participants left-to-right and time top-to-bottom.
28
+ - Keep request, response and async message styles visually distinct.
29
+ - Add activation boxes only when they clarify nested work.
30
+
31
+ ## ML / Deep Learning
32
+
33
+ - Show tensor shape on a second line when it matters.
34
+ - Group encoder, decoder, embedding, attention and output stages.
35
+ - Route skip connections around the primary flow.
36
+
37
+ ## Flowchart
38
+
39
+ - Use ovals for start/end, rectangles for process, diamonds for decisions.
40
+ - Label decision branches.
41
+ - Merge branches back to a central path when possible.
@@ -0,0 +1,57 @@
1
+ # Layout And Export
2
+
3
+ ## Auto Layout
4
+
5
+ Use `layout-graph.mjs` for dependency graphs, code structure maps and diagrams with more than about 15 nodes.
6
+
7
+ ```bash
8
+ node skills/fec-drawio-studio/scripts/layout-graph.mjs graph.json --output diagram.drawio --manifest layout.json
9
+ ```
10
+
11
+ Input graph:
12
+
13
+ ```json
14
+ {
15
+ "direction": "TB",
16
+ "nodes": [
17
+ { "id": "client", "label": "Web Client", "group": "Client" },
18
+ { "id": "api", "label": "API", "group": "Service" }
19
+ ],
20
+ "edges": [
21
+ { "source": "client", "target": "api", "label": "HTTPS" }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ Graphviz `dot` is optional. If it is missing, hand-place the diagram or deliver XML plus a diagrams.net URL.
27
+
28
+ ## Export
29
+
30
+ Preview:
31
+
32
+ ```bash
33
+ drawio -x -f png --width 2000 -o diagram.png diagram.drawio
34
+ ```
35
+
36
+ Final editable PNG:
37
+
38
+ ```bash
39
+ drawio -x -f png -e -s 2 -o diagram.drawio.png diagram.drawio
40
+ node skills/fec-drawio-studio/scripts/png-embed-fix.mjs diagram.drawio.png
41
+ ```
42
+
43
+ Fallback URL:
44
+
45
+ ```bash
46
+ node skills/fec-drawio-studio/scripts/diagram-url.mjs diagram.drawio --edit
47
+ ```
48
+
49
+ Create/import URL for XML, Mermaid or CSV:
50
+
51
+ ```bash
52
+ node skills/fec-drawio-studio/scripts/diagram-url.mjs diagram.drawio --create --type xml --json
53
+ node skills/fec-drawio-studio/scripts/diagram-url.mjs flow.mmd --create --type mermaid
54
+ node skills/fec-drawio-studio/scripts/diagram-url.mjs org.csv --create --type csv --lightbox
55
+ ```
56
+
57
+ Use `--base-url` for a self-hosted diagrams.net endpoint. Use `--shortcut` or the JSON `windowsShortcut` field when opening the URL through Windows shell tooling.
@@ -0,0 +1,29 @@
1
+ # Troubleshooting
2
+
3
+ ## draw.io CLI Missing
4
+
5
+ Generate the `.drawio` file and use `diagram-url.mjs --edit` to open diagrams.net in the browser.
6
+
7
+ ## Graphviz Missing
8
+
9
+ `layout-graph.mjs` exits with a clear Graphviz message. Keep the graph JSON and either install Graphviz or hand-place a smaller diagram.
10
+
11
+ ## Blank Vendor Shape
12
+
13
+ Run `shape-query.mjs` and use the returned style string. A guessed `shape=mxgraph.*` often renders as a generic box.
14
+
15
+ ## Brand Icon Does Not Render
16
+
17
+ CDN-referenced symbols require network access during render. Use `brand-symbols.mjs --embed` for a self-contained data URI when portability matters.
18
+
19
+ ## Embedded PNG Looks Corrupt
20
+
21
+ Run `png-embed-fix.mjs` after final `drawio -e` PNG export. The command is idempotent.
22
+
23
+ ## Browser URL Opens Blank On Windows
24
+
25
+ Use `diagram-url.mjs --json` and open the emitted `windowsShortcut` content as a `.url` file, or run `diagram-url.mjs --shortcut`. This preserves the `#R` or `#create=` fragment that carries the compressed diagram.
26
+
27
+ ## Mermaid Or CSV URL Fails
28
+
29
+ Mermaid and CSV must use `diagram-url.mjs --create --type mermaid|csv`. The older viewer/editor `#R` mode is XML-only.
@@ -0,0 +1,54 @@
1
+ # XML And Mermaid
2
+
3
+ Use this reference to choose the lightest editable diagram source that still meets the user's maintenance and fidelity needs.
4
+
5
+ ## Route Choice
6
+
7
+ - Use Mermaid for common flowcharts, sequence diagrams, class diagrams, state diagrams, ERDs, timelines, mind maps and other text-first diagrams that will live in Markdown.
8
+ - Use draw.io XML when the diagram needs exact shapes, vendor symbols, nested containers, layers, tags, metadata, custom colors or a durable `.drawio` source.
9
+ - Use CSV only as a short import path for tabular structures such as org charts or simple relationship maps; save the resulting editable source when possible.
10
+ - Use URL creation for preview and handoff, not as the only artifact for important diagrams.
11
+
12
+ ## XML Structure
13
+
14
+ - Include root cells `id="0"` and `id="1"` in every diagram.
15
+ - Give every visible vertex and edge a stable unique id.
16
+ - Put child nodes inside their visual container by setting `parent` to the container id and using coordinates relative to that container.
17
+ - Put edges that cross container boundaries at `parent="1"` so they are not clipped by a lane or group.
18
+ - Every edge must contain `<mxGeometry relative="1" as="geometry"/>`; do not use self-closing edge cells.
19
+
20
+ ## Containers And Process Layout
21
+
22
+ - Use titled swimlanes for actor, tier or bounded-context groupings.
23
+ - Use nested swimlanes for real hierarchy such as region, VPC, availability zone and workload.
24
+ - Use a table layout for cross-functional flows where both actor rows and phase columns matter.
25
+ - Use plain groups only when a visual boundary is unnecessary and the group should not capture connectors.
26
+
27
+ ## Layers, Tags And Metadata
28
+
29
+ - Use layers for major visibility sets, such as logical view, physical view, annotations or migration phases.
30
+ - Use tags for cross-cutting filters such as critical, deprecated, external, v2 or owner names.
31
+ - Use object metadata and placeholders when a shape represents a record with fields such as owner, status, IP, service tier or version.
32
+ - Keep visible labels concise; put secondary facts in metadata or notes when the diagram would otherwise become hard to scan.
33
+
34
+ ## Labels And Dark Mode
35
+
36
+ - Match diagram label language to the user's language.
37
+ - Add `html=1` whenever labels contain HTML markup or escaped `<br>` formatting.
38
+ - Escape XML attribute values: `&`, `<`, `>`, and `"` must be entities.
39
+ - Prefer adaptive colors and simple palettes; specify dark-mode overrides only when automatic inversion makes labels or shapes unclear.
40
+
41
+ ## Mermaid Import Notes
42
+
43
+ - Pick the Mermaid header deliberately: `flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`, `erDiagram`, `timeline`, `mindmap`, `gantt`, `C4Context` and similar headers select different parsers.
44
+ - Quote labels that include punctuation, spaces, parentheses or non-ASCII text.
45
+ - Keep node ids simple and put display text in labels.
46
+ - For large, highly styled or vendor-specific diagrams, move to XML before final delivery.
47
+
48
+ ## XML Well-Formedness
49
+
50
+ - Do not include XML comments.
51
+ - Do not leave raw ampersands or angle brackets inside attribute values.
52
+ - Do not use duplicate ids.
53
+ - Do not leave edges pointing to missing source or target cells.
54
+ - Run `diagram-lint.mjs` before final handoff.
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parseArgs, skillDir } from "./studio-core.mjs";
5
+
6
+ const MANIFEST = path.join(skillDir, "data", "brand-icons.json");
7
+ const STYLE_PREFIX = "shape=image;html=1;imageAspect=0;aspect=fixed;verticalLabelPosition=bottom;verticalAlign=top;image=";
8
+ const SIMPLE = new Map([
9
+ ["qdrant", "qdrant"], ["milvus", "milvus"], ["supabase", "supabase"], ["redis", "redis"],
10
+ ["postgresql", "postgresql"], ["postgres", "postgresql"], ["mongodb", "mongodb"],
11
+ ["elasticsearch", "elasticsearch"], ["neo4j", "neo4j"], ["kafka", "apachekafka"],
12
+ ["clickhouse", "clickhouse"], ["duckdb", "duckdb"], ["mysql", "mysql"], ["sqlite", "sqlite"],
13
+ ]);
14
+
15
+ export function loadFamilies() {
16
+ const manifest = JSON.parse(fs.readFileSync(MANIFEST, "utf8"));
17
+ const families = new Map();
18
+ for (const icon of manifest.icons) {
19
+ const base = icon.replace(/-(color|text)$/, "");
20
+ if (!families.has(base)) families.set(base, new Set());
21
+ families.get(base).add(icon);
22
+ }
23
+ return { cdn: manifest.cdn, families };
24
+ }
25
+
26
+ export async function queryBrand(query, options = {}) {
27
+ const { cdn, families } = loadFamilies();
28
+ const hits = rankBrands(families, query).slice(0, Number(options.limit ?? 8));
29
+ const size = Number(options.size ?? 48);
30
+ const variant = options.variant ?? "color";
31
+ const results = [];
32
+ for (const brand of hits) {
33
+ const file = pickVariant(brand, families.get(brand), variant);
34
+ const url = `${cdn}${file}.svg`;
35
+ results.push({ brand, file, w: size, h: size, style: STYLE_PREFIX + (options.embed ? await dataUri(url) : url) });
36
+ }
37
+ if (results.length === 0) {
38
+ const slug = SIMPLE.get(squish(query));
39
+ if (slug) {
40
+ const url = `https://cdn.simpleicons.org/${slug}`;
41
+ results.push({ brand: squish(query), file: `simpleicons:${slug}`, w: size, h: size, style: STYLE_PREFIX + (options.embed ? await dataUri(url) : url) });
42
+ }
43
+ }
44
+ return results;
45
+ }
46
+
47
+ function rankBrands(families, query) {
48
+ const needle = squish(query);
49
+ const tokens = String(query).toLowerCase().match(/[a-z0-9]+/g) ?? [];
50
+ const scored = [];
51
+ for (const brand of families.keys()) {
52
+ const base = squish(brand);
53
+ let score = 0;
54
+ if (base === needle) score = 100;
55
+ else if (base.startsWith(needle)) score = 70;
56
+ else if (base.includes(needle) || needle.includes(base)) score = 45;
57
+ for (const token of tokens) {
58
+ if (base === token) score = Math.max(score, 90);
59
+ else if (token.length >= 3 && base.includes(token)) score = Math.max(score, 35);
60
+ }
61
+ if (score > 0) scored.push([brand, score]);
62
+ }
63
+ return scored.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([brand]) => brand);
64
+ }
65
+
66
+ function pickVariant(base, variants, preference) {
67
+ const order = preference === "mono" ? ["", "-color", "-text"] : preference === "text" ? ["-text", "-color", ""] : ["-color", "", "-text"];
68
+ for (const suffix of order) if (variants.has(base + suffix)) return base + suffix;
69
+ return [...variants].sort()[0];
70
+ }
71
+
72
+ function squish(value) {
73
+ return String(value ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
74
+ }
75
+
76
+ async function dataUri(url) {
77
+ const response = await fetch(url);
78
+ if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`);
79
+ const svg = Buffer.from(await response.arrayBuffer()).toString("base64");
80
+ return `data:image/svg+xml;base64,${svg}`;
81
+ }
82
+
83
+ const args = parseArgs(process.argv.slice(2));
84
+ const query = args._.join(" ");
85
+ if (!query && !args.list) {
86
+ console.error("usage: node brand-symbols.mjs <brand> [--variant color|mono|text] [--embed] [--json]");
87
+ process.exit(2);
88
+ }
89
+ if (args.list) {
90
+ process.stdout.write([...loadFamilies().families.keys()].sort().join("\n") + "\n");
91
+ } else {
92
+ const results = await queryBrand(query, args);
93
+ if (results.length === 0) {
94
+ console.error(`no brand symbol matched ${JSON.stringify(query)}`);
95
+ process.exit(1);
96
+ }
97
+ if (args.json) process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
98
+ else for (const item of results) process.stdout.write(`${item.brand} (${item.file}, ${item.w}x${item.h})\n ${item.style.slice(0, 180)}${item.style.length > 180 ? "..." : ""}\n`);
99
+ }
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { formatResult, parseArgs, readText } from "./studio-core.mjs";
3
+
4
+ export function lintDrawio(xml) {
5
+ const errors = [];
6
+ const warnings = [];
7
+ if (/<!--/.test(xml) || /-->/.test(xml)) errors.push("XML comments are not allowed in draw.io diagram sources");
8
+ if (!/<mx(Cell|file|GraphModel)\b/.test(xml)) warnings.push("No draw.io graph markers found");
9
+ if (/<mxCell\b[^>]*\bedge="1"[^>]*\/>/.test(xml)) errors.push("edge mxCell must not be self-closing; add mxGeometry relative=\"1\"");
10
+
11
+ const cellRe = /<mxCell\b([^>]*?)(?:\/>|>([\s\S]*?)<\/mxCell>)/g;
12
+ const objectByCellIndex = objectsByCellIndex(xml);
13
+ const cells = [];
14
+ const ids = new Map();
15
+ for (const match of xml.matchAll(cellRe)) {
16
+ const objectAttrs = objectByCellIndex.get(match.index) ?? {};
17
+ const attrs = attrsOf(match[1]);
18
+ if (!attrs.id && objectAttrs.id) attrs.id = objectAttrs.id;
19
+ if (!attrs.value && objectAttrs.label) attrs.value = objectAttrs.label;
20
+ const body = match[2] ?? "";
21
+ const id = attrs.id;
22
+ if (!id) {
23
+ errors.push("mxCell without id");
24
+ continue;
25
+ }
26
+ if (ids.has(id)) errors.push(`duplicate id ${id}`);
27
+ ids.set(id, attrs);
28
+ for (const [name, value] of rawAttrsOf(match[1])) {
29
+ if (hasUnescapedAmpersand(value)) errors.push(`cell ${id} attribute ${name} contains an unescaped ampersand`);
30
+ if (/[<>]/.test(value)) errors.push(`cell ${id} attribute ${name} contains an unescaped angle bracket`);
31
+ }
32
+ const geom = body.match(/<mxGeometry\b([^>]*?)(?:\/>|>)/)?.[1];
33
+ cells.push({ id, attrs, body, geom: geom ? attrsOf(geom) : null });
34
+ }
35
+
36
+ if (cells.length > 0) {
37
+ if (!ids.has("0")) errors.push("missing root cell id 0");
38
+ if (!ids.has("1")) errors.push("missing default layer cell id 1");
39
+ }
40
+
41
+ for (const cell of cells) {
42
+ const { attrs, id, geom } = cell;
43
+ if ((attrs.vertex === "1" || attrs.edge === "1") && (id === "0" || id === "1")) {
44
+ errors.push(`reserved id used by graph cell ${id}`);
45
+ }
46
+ if (attrs.parent && !ids.has(attrs.parent)) errors.push(`cell ${id} parent ${attrs.parent} does not exist`);
47
+ for (const end of ["source", "target"]) {
48
+ if (attrs[end] && !ids.has(attrs[end])) errors.push(`edge ${id} ${end} ${attrs[end]} does not exist`);
49
+ }
50
+ if (attrs.edge === "1" && !geom) errors.push(`edge ${id} is missing mxGeometry`);
51
+ if (attrs.edge === "1" && (!attrs.source || !attrs.target)) warnings.push(`edge ${id} has no source or target`);
52
+ if (attrs.vertex === "1") {
53
+ if (looksLikeHtml(attrs.value) && !styleHas(attrs.style, "html", "1")) warnings.push(`cell ${id} label contains HTML but style does not include html=1`);
54
+ if (!geom) errors.push(`vertex ${id} is missing mxGeometry`);
55
+ else {
56
+ const rect = rectOf(geom);
57
+ if (!Number.isFinite(rect.w) || !Number.isFinite(rect.h)) errors.push(`vertex ${id} has invalid size`);
58
+ else if (rect.w <= 0 || rect.h <= 0) warnings.push(`vertex ${id} has non-positive size`);
59
+ if (rect.x < 0 || rect.y < 0) warnings.push(`vertex ${id} has negative position`);
60
+ }
61
+ }
62
+ }
63
+
64
+ const leafVertices = cells
65
+ .filter((cell) => cell.attrs.vertex === "1" && cell.geom)
66
+ .filter((cell) => !cells.some((candidate) => candidate.attrs.parent === cell.id))
67
+ .map((cell) => ({ id: cell.id, parent: cell.attrs.parent ?? "1", rect: rectOf(cell.geom) }))
68
+ .filter((item) => Number.isFinite(item.rect.w) && Number.isFinite(item.rect.h));
69
+ for (let i = 0; i < leafVertices.length; i += 1) {
70
+ for (let j = i + 1; j < leafVertices.length; j += 1) {
71
+ if (leafVertices[i].parent === leafVertices[j].parent && overlaps(leafVertices[i].rect, leafVertices[j].rect)) {
72
+ warnings.push(`vertices ${leafVertices[i].id} and ${leafVertices[j].id} overlap`);
73
+ }
74
+ }
75
+ }
76
+ if (cells.filter((cell) => cell.attrs.vertex === "1").length === 0) warnings.push("diagram has no vertex cells");
77
+ return { title: "Diagram Lint", errors, warnings, ok: errors.length === 0 };
78
+ }
79
+
80
+ function objectsByCellIndex(xml) {
81
+ const out = new Map();
82
+ const objectRe = /<object\b([^>]*)>([\s\S]*?)<\/object>/g;
83
+ for (const match of xml.matchAll(objectRe)) {
84
+ const inner = match[2] ?? "";
85
+ const cellOffset = inner.search(/<mxCell\b/);
86
+ if (cellOffset < 0) continue;
87
+ const index = match.index + match[0].indexOf("<mxCell");
88
+ out.set(index, attrsOf(match[1]));
89
+ }
90
+ return out;
91
+ }
92
+
93
+ function attrsOf(raw) {
94
+ const attrs = {};
95
+ for (const match of raw.matchAll(/([A-Za-z_:][-A-Za-z0-9_:.]*)="([^"]*)"/g)) attrs[match[1]] = decode(match[2]);
96
+ return attrs;
97
+ }
98
+
99
+ function rawAttrsOf(raw) {
100
+ return [...raw.matchAll(/([A-Za-z_:][-A-Za-z0-9_:.]*)="([^"]*)"/g)].map((match) => [match[1], match[2]]);
101
+ }
102
+
103
+ function decode(value) {
104
+ return value.replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
105
+ }
106
+
107
+ function hasUnescapedAmpersand(value) {
108
+ return /&(?!amp;|lt;|gt;|quot;|apos;|#[0-9]+;|#x[0-9a-fA-F]+;)/.test(value);
109
+ }
110
+
111
+ function looksLikeHtml(value = "") {
112
+ return /<\/?[A-Za-z][^>]*>/.test(value);
113
+ }
114
+
115
+ function styleHas(style = "", key, expected) {
116
+ return style.split(";").some((part) => {
117
+ const [partKey, value] = part.split("=", 2);
118
+ return partKey === key && value === expected;
119
+ });
120
+ }
121
+
122
+ function rectOf(geom) {
123
+ return {
124
+ x: Number(geom.x ?? 0),
125
+ y: Number(geom.y ?? 0),
126
+ w: Number(geom.width),
127
+ h: Number(geom.height),
128
+ };
129
+ }
130
+
131
+ function overlaps(a, b) {
132
+ return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
133
+ }
134
+
135
+ const args = parseArgs(process.argv.slice(2));
136
+ const input = args._[0];
137
+ if (!input) {
138
+ console.error("usage: node diagram-lint.mjs <file.drawio> [--format markdown|json] [--strict]");
139
+ process.exit(2);
140
+ }
141
+ const result = lintDrawio(readText(input));
142
+ process.stdout.write(formatResult(result, args.format ?? (args.json ? "json" : "markdown")));
143
+ if (!result.ok || (args.strict && result.warnings.length > 0)) process.exit(1);
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import zlib from "node:zlib";
3
+ import { parseArgs, readText } from "./studio-core.mjs";
4
+
5
+ const args = parseArgs(process.argv.slice(2));
6
+ const input = args._[0];
7
+ if (!input) {
8
+ console.error(
9
+ "usage: node diagram-url.mjs <diagram-file> [--edit|--create] [--type xml|mermaid|csv] [--lightbox] [--dark true|false|auto] [--base-url URL] [--json|--shortcut]",
10
+ );
11
+ process.exit(2);
12
+ }
13
+
14
+ export function encodeDiagramXml(xml) {
15
+ const encoded = encodeURIComponent(xml);
16
+ return zlib.deflateRawSync(Buffer.from(encoded, "utf8")).toString("base64");
17
+ }
18
+
19
+ export function viewerUrl(xml, options = {}) {
20
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://viewer.diagrams.net/");
21
+ return `${baseUrl}?highlight=0000ff&edit=_blank#R${encodeDiagramXml(xml)}`;
22
+ }
23
+
24
+ export function editorUrl(xml, options = {}) {
25
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://app.diagrams.net/");
26
+ return `${baseUrl}?offline=1#R${encodeDiagramXml(xml)}`;
27
+ }
28
+
29
+ export function createUrl(content, options = {}) {
30
+ const type = normalizeType(options.type);
31
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://app.diagrams.net/");
32
+ const params = new URLSearchParams();
33
+ if (options.lightbox) {
34
+ params.set("lightbox", "1");
35
+ params.set("edit", "_blank");
36
+ } else {
37
+ params.set("grid", "0");
38
+ params.set("pv", "0");
39
+ params.set("edit", "_blank");
40
+ }
41
+ params.set("border", String(options.border ?? 10));
42
+ const dark = normalizeDark(options.dark);
43
+ if (dark === true) params.set("dark", "1");
44
+
45
+ const payload = encodeURIComponent(
46
+ JSON.stringify({
47
+ type,
48
+ compressed: true,
49
+ data: encodeDiagramXml(content),
50
+ }),
51
+ );
52
+ const query = params.toString();
53
+ return `${baseUrl}${query ? `?${query}` : ""}#create=${payload}`;
54
+ }
55
+
56
+ export function internetShortcut(url) {
57
+ return `[InternetShortcut]\r\nURL=${url}\r\n`;
58
+ }
59
+
60
+ function normalizeType(value = "xml") {
61
+ if (["xml", "mermaid", "csv"].includes(value)) return value;
62
+ throw new Error(`Unsupported diagram type "${value}". Expected xml, mermaid, or csv.`);
63
+ }
64
+
65
+ function normalizeDark(value = "auto") {
66
+ if (value === true || value === "true") return true;
67
+ if (value === false || value === "false") return false;
68
+ if (value === "auto" || value === undefined) return "auto";
69
+ throw new Error(`Unsupported dark mode "${value}". Expected true, false, or auto.`);
70
+ }
71
+
72
+ function normalizeBaseUrl(value) {
73
+ return String(value).replace(/[?#].*$/, "").replace(/\/?$/, "/");
74
+ }
75
+
76
+ try {
77
+ const content = readText(input);
78
+ const type = normalizeType(args.type ?? "xml");
79
+ if (type !== "xml" && !args.create) {
80
+ throw new Error("Mermaid and CSV inputs require --create because viewer/editor #R URLs only load XML.");
81
+ }
82
+ const options = {
83
+ baseUrl: args.baseUrl,
84
+ border: args.border,
85
+ dark: args.dark,
86
+ lightbox: Boolean(args.lightbox),
87
+ type,
88
+ };
89
+ const mode = args.create ? "create" : args.edit ? "edit" : "view";
90
+ const url = args.create ? createUrl(content, options) : args.edit ? editorUrl(content, options) : viewerUrl(content, options);
91
+ const shortcut = internetShortcut(url);
92
+ if (args.shortcut) {
93
+ process.stdout.write(shortcut);
94
+ } else if (args.json) {
95
+ process.stdout.write(`${JSON.stringify({ url, mode, type, windowsShortcut: shortcut }, null, 2)}\n`);
96
+ } else {
97
+ process.stdout.write(`${url}\n`);
98
+ }
99
+ } catch (error) {
100
+ console.error(error instanceof Error ? error.message : String(error));
101
+ process.exit(1);
102
+ }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { parseArgs, printOrWrite, readText, snap, writeText, xmlEscape } from "./studio-core.mjs";
4
+
5
+ const DEFAULT_W = 140;
6
+ const DEFAULT_H = 64;
7
+ const NODE_STYLE = "rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;";
8
+ const EDGE_STYLE = "edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;";
9
+ const PALETTE = [
10
+ ["#dae8fc", "#6c8ebf"], ["#d5e8d4", "#82b366"], ["#ffe6cc", "#d79b00"],
11
+ ["#e1d5e7", "#9673a6"], ["#fff2cc", "#d6b656"], ["#f8cecc", "#b85450"],
12
+ ];
13
+
14
+ export function buildDot(graph) {
15
+ const direction = String(graph.direction ?? "TB").toUpperCase() === "LR" ? "LR" : "TB";
16
+ const lines = [`digraph G { rankdir=${direction}; splines=ortho; node [shape=box fixedsize=true];`];
17
+ for (const node of graph.nodes) {
18
+ lines.push(`"${escapeDot(node.id)}" [width=${Number(node.width ?? DEFAULT_W) / 72} height=${Number(node.height ?? DEFAULT_H) / 72}];`);
19
+ }
20
+ for (const edge of graph.edges ?? []) lines.push(`"${escapeDot(edge.source)}" -> "${escapeDot(edge.target)}";`);
21
+ lines.push("}");
22
+ return lines.join("\n");
23
+ }
24
+
25
+ export function runDot(dot) {
26
+ const proc = spawnSync("dot", ["-Tplain"], { input: dot, encoding: "utf8" });
27
+ if (proc.error?.code === "ENOENT") throw new Error("Graphviz `dot` not found on PATH");
28
+ if (proc.status !== 0) throw new Error(`Graphviz failed: ${proc.stderr.trim()}`);
29
+ const pos = new Map();
30
+ let height = 0;
31
+ for (const line of proc.stdout.split(/\r?\n/)) {
32
+ const parts = splitPlain(line);
33
+ if (parts[0] === "graph") height = Number(parts[3]);
34
+ if (parts[0] === "node") pos.set(parts[1], { x: Number(parts[2]), y: Number(parts[3]) });
35
+ }
36
+ return { height, pos };
37
+ }
38
+
39
+ export function graphToDrawio(graph, layout) {
40
+ const groups = [...new Set((graph.nodes ?? []).map((node) => node.group).filter(Boolean))];
41
+ const groupColor = (group) => PALETTE[Math.max(0, groups.indexOf(group)) % PALETTE.length];
42
+ const cells = [];
43
+ const manifest = { nodes: {}, edges: graph.edges ?? [] };
44
+ for (const node of graph.nodes ?? []) {
45
+ const p = layout.pos.get(node.id);
46
+ if (!p) continue;
47
+ const w = Number(node.width ?? DEFAULT_W);
48
+ const h = Number(node.height ?? DEFAULT_H);
49
+ const x = snap(p.x * 72 - w / 2 + 40);
50
+ const y = snap((layout.height - p.y) * 72 - h / 2 + 40);
51
+ const style = node.style ?? (node.group ? tintStyle(groupColor(node.group)) : NODE_STYLE);
52
+ manifest.nodes[node.id] = { x, y, width: w, height: h, group: node.group ?? null };
53
+ cells.push(` <mxCell id="${xmlEscape(node.id)}" value="${xmlEscape(node.label ?? node.id)}" style="${xmlEscape(style)}" vertex="1" parent="1">
54
+ <mxGeometry x="${x}" y="${y}" width="${w}" height="${h}" as="geometry"/>
55
+ </mxCell>`);
56
+ }
57
+ let edgeIndex = 0;
58
+ for (const edge of graph.edges ?? []) {
59
+ cells.push(` <mxCell id="edge_${edgeIndex++}" value="${xmlEscape(edge.label ?? "")}" style="${EDGE_STYLE}" edge="1" parent="1" source="${xmlEscape(edge.source)}" target="${xmlEscape(edge.target)}">
60
+ <mxGeometry relative="1" as="geometry"/>
61
+ </mxCell>`);
62
+ }
63
+ return {
64
+ manifest,
65
+ xml: `<mxfile host="drawio">
66
+ <diagram name="Page-1">
67
+ <mxGraphModel grid="1" gridSize="10" page="1" pageWidth="1100" pageHeight="850">
68
+ <root>
69
+ <mxCell id="0"/>
70
+ <mxCell id="1" parent="0"/>
71
+ ${cells.join("\n")}
72
+ </root>
73
+ </mxGraphModel>
74
+ </diagram>
75
+ </mxfile>
76
+ `,
77
+ };
78
+ }
79
+
80
+ function tintStyle([fill, stroke]) {
81
+ return `rounded=1;whiteSpace=wrap;html=1;fillColor=${fill};strokeColor=${stroke};`;
82
+ }
83
+
84
+ function escapeDot(value) {
85
+ return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
86
+ }
87
+
88
+ function splitPlain(line) {
89
+ return line.match(/"[^"]*"|\S+/g)?.map((part) => part.replace(/^"|"$/g, "")) ?? [];
90
+ }
91
+
92
+ const args = parseArgs(process.argv.slice(2));
93
+ const input = args._[0];
94
+ if (!input) {
95
+ console.error("usage: node layout-graph.mjs graph.json --output diagram.drawio [--manifest layout.json]");
96
+ process.exit(2);
97
+ }
98
+ try {
99
+ const graph = JSON.parse(readText(input));
100
+ const rendered = graphToDrawio(graph, runDot(buildDot(graph)));
101
+ printOrWrite(rendered.xml, args.output);
102
+ if (args.manifest) writeText(args.manifest, `${JSON.stringify(rendered.manifest, null, 2)}\n`);
103
+ if (!args.output) process.stderr.write(`rendered ${graph.nodes.length} nodes\n`);
104
+ } catch (error) {
105
+ console.error(`error: ${error.message}`);
106
+ process.exit(1);
107
+ }