@bovinphang/craft-common 0.0.0-stage → 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/content/catalog.d.ts +4 -0
- package/dist/content/resources.d.ts +2 -0
- package/dist/content/types.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +668 -0
- package/dist/install/identity.d.ts +5 -0
- package/dist/install/planner.d.ts +6 -0
- package/dist/install/transaction.d.ts +7 -0
- package/dist/install/types.d.ts +44 -0
- package/docs/consuming-common.md +33 -0
- package/localized/zh-CN/methods/debug.md +13 -0
- package/localized/zh-CN/methods/doc-sync.md +12 -0
- package/localized/zh-CN/methods/refactoring.md +16 -0
- package/localized/zh-CN/methods/source-driven-development.md +19 -0
- package/localized/zh-CN/methods/tdd.md +13 -0
- package/localized/zh-CN/skills/cc-debug-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-debug-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/references/method.md +12 -0
- package/localized/zh-CN/skills/cc-drawio-studio/SKILL.md +73 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/localized/zh-CN/skills/cc-refactoring-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-refactoring-core/references/method.md +16 -0
- package/localized/zh-CN/skills/cc-source-driven-development/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-source-driven-development/references/method.md +19 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/localized/zh-CN/skills/eval_queries.json +160 -0
- package/localized/zh-CN/skills/metadata.json +223 -0
- package/localized/zh-CN/skills/relations.json +53 -0
- package/methods/debug.md +13 -0
- package/methods/doc-sync.md +12 -0
- package/methods/refactoring.md +16 -0
- package/methods/source-driven-development.md +19 -0
- package/methods/tdd.md +13 -0
- package/package.json +51 -4
- package/skills/cc-debug-methodology/SKILL.md +8 -0
- package/skills/cc-debug-methodology/references/method.md +13 -0
- package/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/skills/cc-doc-sync-core/references/method.md +12 -0
- package/skills/cc-drawio-studio/SKILL.md +73 -0
- package/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/skills/cc-refactoring-core/SKILL.md +8 -0
- package/skills/cc-refactoring-core/references/method.md +16 -0
- package/skills/cc-source-driven-development/SKILL.md +8 -0
- package/skills/cc-source-driven-development/references/method.md +19 -0
- package/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/skills/cc-tdd-methodology/references/method.md +13 -0
- package/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/skills/eval_queries.json +160 -0
- package/skills/metadata.json +223 -0
- package/skills/relations.json +53 -0
|
@@ -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,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { formatResult, parseArgs } from "./studio-core.mjs";
|
|
7
|
+
|
|
8
|
+
const INSTALL_HINTS = [
|
|
9
|
+
"draw.io desktop CLI was not found.",
|
|
10
|
+
"Install draw.io desktop, then rerun the export.",
|
|
11
|
+
"Windows: winget install JGraph.Draw",
|
|
12
|
+
"macOS: brew install --cask drawio",
|
|
13
|
+
"Linux: install drawio from your package manager or snap install drawio",
|
|
14
|
+
"Manual download: https://github.com/jgraph/drawio-desktop/releases",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function buildExportPlan(input, args = {}, env = process.env, platform = process.platform) {
|
|
18
|
+
const drawio = findDrawio(args.drawio, env, platform);
|
|
19
|
+
if (!drawio) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
title: "Draw.io Export",
|
|
23
|
+
errors: [INSTALL_HINTS.join("\n")],
|
|
24
|
+
command: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const format = String(args.format ?? "png");
|
|
29
|
+
const output = String(args.output ?? defaultOutput(input, format));
|
|
30
|
+
const commandArgs = ["-x", "-f", format];
|
|
31
|
+
if (args.embed) commandArgs.push("-e");
|
|
32
|
+
if (args.scale) commandArgs.push("-s", String(args.scale));
|
|
33
|
+
if (args.width) commandArgs.push("--width", String(args.width));
|
|
34
|
+
if (args.border) commandArgs.push("--border", String(args.border));
|
|
35
|
+
if (args.crop) commandArgs.push("--crop");
|
|
36
|
+
if (args.page !== undefined) commandArgs.push("-p", String(args.page));
|
|
37
|
+
commandArgs.push("-o", output, input);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
title: "Draw.io Export",
|
|
42
|
+
drawio,
|
|
43
|
+
command: [drawio, ...commandArgs],
|
|
44
|
+
output,
|
|
45
|
+
format,
|
|
46
|
+
dryRun: Boolean(args.dryRun),
|
|
47
|
+
errors: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function findDrawio(explicitPath, env = process.env, platform = process.platform) {
|
|
52
|
+
if (explicitPath) return existingFile(explicitPath);
|
|
53
|
+
|
|
54
|
+
for (const command of ["drawio", "draw.io"]) {
|
|
55
|
+
const found = findOnPath(command, env, platform);
|
|
56
|
+
if (found) return found;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const candidate of defaultCandidates(env, platform)) {
|
|
60
|
+
const found = existingFile(candidate);
|
|
61
|
+
if (found) return found;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findOnPath(command, env, platform) {
|
|
68
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
69
|
+
const dirs = pathValue.split(path.delimiter).filter(Boolean);
|
|
70
|
+
const extensions = platform === "win32" ? pathExtensions(env) : [""];
|
|
71
|
+
for (const dir of dirs) {
|
|
72
|
+
for (const ext of extensions) {
|
|
73
|
+
const candidate = path.join(dir, command.endsWith(ext) ? command : `${command}${ext}`);
|
|
74
|
+
const found = existingFile(candidate);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pathExtensions(env) {
|
|
82
|
+
const configured = env.PATHEXT ? env.PATHEXT.split(";").filter(Boolean) : [".EXE", ".CMD", ".BAT", ""];
|
|
83
|
+
return [...new Set(["", ...configured, ...configured.map((item) => item.toLowerCase())])];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function defaultCandidates(env, platform) {
|
|
87
|
+
if (platform === "win32") {
|
|
88
|
+
return [
|
|
89
|
+
path.join(env.ProgramFiles ?? "C:\\Program Files", "draw.io", "draw.io.exe"),
|
|
90
|
+
path.join(env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "draw.io", "draw.io.exe"),
|
|
91
|
+
path.join(env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"), "Programs", "draw.io", "draw.io.exe"),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
if (platform === "darwin") {
|
|
95
|
+
return ["/Applications/draw.io.app/Contents/MacOS/draw.io"];
|
|
96
|
+
}
|
|
97
|
+
return ["/usr/bin/drawio", "/usr/local/bin/drawio", "/snap/bin/drawio"];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function existingFile(candidate) {
|
|
101
|
+
if (!candidate) return null;
|
|
102
|
+
const resolved = path.resolve(candidate);
|
|
103
|
+
return fs.existsSync(resolved) && fs.statSync(resolved).isFile() ? resolved : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultOutput(input, format) {
|
|
107
|
+
return `${input.replace(/\.(drawio|xml)$/i, "")}.${format}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printFailure(plan, args) {
|
|
111
|
+
const output = formatResult(plan, args.formatOutput ?? (args.json ? "json" : "markdown"));
|
|
112
|
+
process.stderr.write(output);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const args = parseArgs(process.argv.slice(2));
|
|
116
|
+
const input = args._[0];
|
|
117
|
+
if (!input) {
|
|
118
|
+
console.error("usage: node drawio-export.mjs <file.drawio> [--format png|svg|pdf] [--output file] [--scale 2] [--width 2000] [--border 20] [--dry-run] [--json]");
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const plan = buildExportPlan(input, args);
|
|
123
|
+
if (!plan.ok) {
|
|
124
|
+
printFailure(plan, args);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (args.dryRun) {
|
|
129
|
+
process.stdout.write(formatResult(plan, args.formatOutput ?? (args.json ? "json" : "markdown")));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = spawnSync(plan.command[0], plan.command.slice(1), { stdio: "inherit" });
|
|
134
|
+
process.exit(result.status ?? 1);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { parseArgs } from "./studio-core.mjs";
|
|
4
|
+
|
|
5
|
+
const IEND = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]);
|
|
6
|
+
const IEND_TAIL = IEND.subarray(4);
|
|
7
|
+
|
|
8
|
+
export function repairPngBuffer(buffer) {
|
|
9
|
+
if (buffer.subarray(-12).equals(IEND)) return { buffer, changed: false, reason: "already-valid" };
|
|
10
|
+
if (buffer.subarray(-4).equals(IEND.subarray(0, 4))) {
|
|
11
|
+
return { buffer: Buffer.concat([buffer, IEND_TAIL]), changed: true, reason: "appended-iend-tail" };
|
|
12
|
+
}
|
|
13
|
+
return { buffer, changed: false, reason: "not-truncated-iend" };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const args = parseArgs(process.argv.slice(2));
|
|
17
|
+
const input = args._[0];
|
|
18
|
+
if (!input) {
|
|
19
|
+
console.error("usage: node png-embed-fix.mjs <file.png> [--json]");
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repaired = repairPngBuffer(fs.readFileSync(input));
|
|
24
|
+
if (repaired.changed) fs.writeFileSync(input, repaired.buffer);
|
|
25
|
+
const result = { file: input, changed: repaired.changed, reason: repaired.reason };
|
|
26
|
+
process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : `${input}: ${repaired.reason}\n`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs, printOrWrite, reduceTransitive, toPosix, walkFiles } from "./studio-core.mjs";
|
|
5
|
+
|
|
6
|
+
export function scanGoPackages(root) {
|
|
7
|
+
const absRoot = path.resolve(root);
|
|
8
|
+
const mod = fs.existsSync(path.join(absRoot, "go.mod")) ? fs.readFileSync(path.join(absRoot, "go.mod"), "utf8").match(/^module\s+(.+)$/m)?.[1] : "";
|
|
9
|
+
const files = walkFiles(absRoot, (file) => file.endsWith(".go") && !file.endsWith("_test.go"));
|
|
10
|
+
const packageDirs = [...new Set(files.map((file) => path.dirname(file)))].sort();
|
|
11
|
+
const dirToId = new Map(packageDirs.map((dir) => [dir, mod ? `${mod}/${toPosix(path.relative(absRoot, dir))}`.replace(/\/$/, "") : toPosix(path.relative(absRoot, dir) || ".")]));
|
|
12
|
+
const nodes = [...dirToId.entries()].map(([dir, id]) => ({ id, label: path.basename(dir), group: toPosix(path.dirname(path.relative(absRoot, dir))).replace(".", "") || undefined }));
|
|
13
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
14
|
+
const edges = [];
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const source = dirToId.get(path.dirname(file));
|
|
17
|
+
const text = fs.readFileSync(file, "utf8");
|
|
18
|
+
for (const spec of extractGoImports(text)) if (ids.has(spec)) edges.push({ source, target: spec });
|
|
19
|
+
}
|
|
20
|
+
return { direction: "LR", nodes, edges: reduceTransitive(nodes, edges) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractGoImports(text) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const match of text.matchAll(/import\s+(?:[._A-Za-z]\w*\s+)?(?:"([^"]+)"|\(([\s\S]*?)\))/g)) {
|
|
26
|
+
if (match[1]) out.push(match[1]);
|
|
27
|
+
if (match[2]) for (const line of match[2].split(/\r?\n/)) {
|
|
28
|
+
const item = line.match(/"([^"]+)"/)?.[1];
|
|
29
|
+
if (item) out.push(item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const args = parseArgs(process.argv.slice(2));
|
|
36
|
+
printOrWrite(`${JSON.stringify(scanGoPackages(args._[0] ?? "."), null, 2)}\n`, args.output);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs, printOrWrite, reduceTransitive, toPosix, walkFiles } from "./studio-core.mjs";
|
|
6
|
+
|
|
7
|
+
const JS_EXTS = [".js", ".jsx", ".mjs", ".cjs"];
|
|
8
|
+
const IMPORT_RE = /\bimport\s+(?:[^'"]+?\s+from\s+)?['"]([^'"]+)['"]|\bexport\s+[^'"]*?\s+from\s+['"]([^'"]+)['"]|\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)|\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
9
|
+
|
|
10
|
+
export function scanJavaScriptModules(root, extensions = JS_EXTS) {
|
|
11
|
+
return scanModules(root, extensions);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function scanModules(root, extensions) {
|
|
15
|
+
const absRoot = path.resolve(root);
|
|
16
|
+
const files = walkFiles(absRoot, (file) => extensions.includes(path.extname(file)));
|
|
17
|
+
const byBase = new Map();
|
|
18
|
+
const nodes = files.map((file) => {
|
|
19
|
+
const id = toPosix(path.relative(absRoot, file)).replace(/\.[^.]+$/, "");
|
|
20
|
+
byBase.set(path.resolve(file), id);
|
|
21
|
+
return { id, label: id.split("/").pop(), group: id.includes("/") ? id.split("/").slice(0, -1).join("/") : undefined };
|
|
22
|
+
});
|
|
23
|
+
const edges = [];
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const source = byBase.get(path.resolve(file));
|
|
26
|
+
const text = fs.readFileSync(file, "utf8");
|
|
27
|
+
for (const match of text.matchAll(IMPORT_RE)) {
|
|
28
|
+
const spec = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
29
|
+
if (!spec?.startsWith(".")) continue;
|
|
30
|
+
const target = resolveRelative(file, spec, extensions, byBase);
|
|
31
|
+
if (target) edges.push({ source, target });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { direction: "LR", nodes, edges: reduceTransitive(nodes, edges) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveRelative(fromFile, spec, extensions, byBase) {
|
|
38
|
+
const base = path.resolve(path.dirname(fromFile), spec);
|
|
39
|
+
const candidates = [base, ...extensions.map((ext) => base + ext), ...extensions.map((ext) => path.join(base, `index${ext}`))];
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
const id = byBase.get(path.resolve(candidate));
|
|
42
|
+
if (id) return id;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
48
|
+
const args = parseArgs(process.argv.slice(2));
|
|
49
|
+
const root = args._[0] ?? ".";
|
|
50
|
+
const graph = scanJavaScriptModules(root);
|
|
51
|
+
printOrWrite(`${JSON.stringify(graph, null, 2)}\n`, args.output);
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs, printOrWrite, reduceTransitive, toPosix, walkFiles } from "./studio-core.mjs";
|
|
5
|
+
|
|
6
|
+
export function scanPythonClasses(root) {
|
|
7
|
+
const absRoot = path.resolve(root);
|
|
8
|
+
const files = walkFiles(absRoot, (file) => file.endsWith(".py"));
|
|
9
|
+
const classes = [];
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const moduleId = toPosix(path.relative(absRoot, file)).replace(/\/__init__\.py$/, "").replace(/\.py$/, "").replaceAll("/", ".");
|
|
12
|
+
const text = fs.readFileSync(file, "utf8");
|
|
13
|
+
for (const match of text.matchAll(/^\s*class\s+([A-Za-z_]\w*)\s*(?:\(([^)]*)\))?:/gm)) {
|
|
14
|
+
const id = `${moduleId}.${match[1]}`.replace(/^\./, "");
|
|
15
|
+
classes.push({ id, name: match[1], bases: (match[2] ?? "").split(",").map((item) => item.trim().split(".").pop()).filter(Boolean), moduleId });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const byName = new Map(classes.map((cls) => [cls.name, cls.id]));
|
|
19
|
+
const nodes = classes.map((cls) => ({ id: cls.id, label: cls.name, group: cls.moduleId }));
|
|
20
|
+
const edges = [];
|
|
21
|
+
for (const cls of classes) {
|
|
22
|
+
for (const base of cls.bases) {
|
|
23
|
+
if (byName.has(base)) edges.push({ source: cls.id, target: byName.get(base) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { direction: "TB", nodes, edges: reduceTransitive(nodes, edges) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const args = parseArgs(process.argv.slice(2));
|
|
30
|
+
printOrWrite(`${JSON.stringify(scanPythonClasses(args._[0] ?? "."), null, 2)}\n`, args.output);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs, printOrWrite, reduceTransitive, toPosix, walkFiles } from "./studio-core.mjs";
|
|
5
|
+
|
|
6
|
+
export function scanPythonModules(root) {
|
|
7
|
+
const absRoot = path.resolve(root);
|
|
8
|
+
const files = walkFiles(absRoot, (file) => file.endsWith(".py"));
|
|
9
|
+
const moduleByFile = new Map();
|
|
10
|
+
const nodes = files.map((file) => {
|
|
11
|
+
const rel = toPosix(path.relative(absRoot, file));
|
|
12
|
+
const id = rel.replace(/\/__init__\.py$/, "").replace(/\.py$/, "").replaceAll("/", ".");
|
|
13
|
+
moduleByFile.set(file, id || path.basename(absRoot));
|
|
14
|
+
return { id: id || path.basename(absRoot), label: path.basename(id || absRoot), group: id.includes(".") ? id.split(".").slice(0, -1).join(".") : undefined };
|
|
15
|
+
});
|
|
16
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
17
|
+
const edges = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const source = moduleByFile.get(file);
|
|
20
|
+
const text = fs.readFileSync(file, "utf8");
|
|
21
|
+
for (const line of text.split(/\r?\n/)) {
|
|
22
|
+
const importMatch = line.match(/^\s*import\s+(.+)/);
|
|
23
|
+
if (importMatch) {
|
|
24
|
+
for (const item of importMatch[1].split(",")) addEdge(source, item.trim().split(/\s+as\s+/)[0], ids, edges);
|
|
25
|
+
}
|
|
26
|
+
const fromMatch = line.match(/^\s*from\s+([.\w]+)\s+import\s+(.+)/);
|
|
27
|
+
if (fromMatch) addEdge(source, resolveFromImport(source, fromMatch[1]), ids, edges);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { direction: "LR", nodes, edges: reduceTransitive(nodes, edges) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function addEdge(source, target, ids, edges) {
|
|
34
|
+
if (!target) return;
|
|
35
|
+
const parts = target.split(".");
|
|
36
|
+
while (parts.length) {
|
|
37
|
+
const candidate = parts.join(".");
|
|
38
|
+
if (ids.has(candidate)) {
|
|
39
|
+
edges.push({ source, target: candidate });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
parts.pop();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveFromImport(source, spec) {
|
|
47
|
+
if (!spec.startsWith(".")) return spec;
|
|
48
|
+
const dots = spec.match(/^\.+/)?.[0].length ?? 0;
|
|
49
|
+
const suffix = spec.slice(dots);
|
|
50
|
+
const base = source.split(".").slice(0, Math.max(0, source.split(".").length - dots));
|
|
51
|
+
return [...base, suffix].filter(Boolean).join(".");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const args = parseArgs(process.argv.slice(2));
|
|
55
|
+
printOrWrite(`${JSON.stringify(scanPythonModules(args._[0] ?? "."), null, 2)}\n`, args.output);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs, printOrWrite, reduceTransitive, toPosix, walkFiles } from "./studio-core.mjs";
|
|
5
|
+
|
|
6
|
+
export function scanRustModules(root) {
|
|
7
|
+
const srcRoot = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
8
|
+
const absRoot = path.resolve(srcRoot);
|
|
9
|
+
const files = walkFiles(absRoot, (file) => file.endsWith(".rs"));
|
|
10
|
+
const fileToId = new Map();
|
|
11
|
+
const nodes = files.map((file) => {
|
|
12
|
+
let id = toPosix(path.relative(absRoot, file)).replace(/\.rs$/, "").replace(/\/mod$/, "").replace(/^(lib|main)$/, "crate").replaceAll("/", "::");
|
|
13
|
+
fileToId.set(file, id);
|
|
14
|
+
return { id, label: id.split("::").pop(), group: id.includes("::") ? id.split("::").slice(0, -1).join("::") : undefined };
|
|
15
|
+
});
|
|
16
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
17
|
+
const edges = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const source = fileToId.get(file);
|
|
20
|
+
const text = fs.readFileSync(file, "utf8");
|
|
21
|
+
for (const match of text.matchAll(/\buse\s+(crate|self|super)::([^;{]+)[;{]/g)) {
|
|
22
|
+
const target = match[2].split("::").filter(Boolean)[0];
|
|
23
|
+
const candidates = [...ids].filter((id) => id === target || id.endsWith(`::${target}`));
|
|
24
|
+
if (candidates[0]) edges.push({ source, target: candidates[0] });
|
|
25
|
+
}
|
|
26
|
+
for (const match of text.matchAll(/\bmod\s+([A-Za-z_]\w*)\s*;/g)) {
|
|
27
|
+
const candidates = [...ids].filter((id) => id === match[1] || id.endsWith(`::${match[1]}`));
|
|
28
|
+
if (candidates[0]) edges.push({ source, target: candidates[0] });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { direction: "LR", nodes, edges: reduceTransitive(nodes, edges) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = parseArgs(process.argv.slice(2));
|
|
35
|
+
printOrWrite(`${JSON.stringify(scanRustModules(args._[0] ?? "."), null, 2)}\n`, args.output);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs, printOrWrite } from "./studio-core.mjs";
|
|
3
|
+
import { scanModules } from "./scan-js-modules.mjs";
|
|
4
|
+
|
|
5
|
+
const args = parseArgs(process.argv.slice(2));
|
|
6
|
+
const graph = scanModules(args._[0] ?? ".", [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
7
|
+
printOrWrite(`${JSON.stringify(graph, null, 2)}\n`, args.output);
|