@bovinphang/frontend-craft 2.5.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 (156) hide show
  1. package/.claude-plugin/marketplace.json +7 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +77 -1
  4. package/CHANGELOG.zh-CN.md +77 -1
  5. package/CONTRIBUTING.md +15 -10
  6. package/CONTRIBUTING.zh-CN.md +15 -10
  7. package/README.md +69 -43
  8. package/README.openclaw.md +11 -9
  9. package/README.openclaw.zh-CN.md +11 -9
  10. package/README.zh-CN.md +69 -43
  11. package/agents/fec-typescript-reviewer.md +1 -1
  12. package/commands/fec-doc-sync.md +7 -6
  13. package/dist/bin/frontend-craft.js +30 -27
  14. package/docs/example-prompts.md +82 -69
  15. package/docs/ja-JP/README.md +110 -84
  16. package/docs/ko-KR/README.md +100 -74
  17. package/docs/project-structure.md +8 -4
  18. package/docs/runtimes/antigravity.md +1 -1
  19. package/docs/runtimes/augment.md +1 -1
  20. package/docs/runtimes/claude.md +16 -10
  21. package/docs/runtimes/claude.zh-CN.md +16 -10
  22. package/docs/runtimes/cline.md +1 -1
  23. package/docs/runtimes/codebuddy.md +1 -1
  24. package/docs/runtimes/codex.md +1 -1
  25. package/docs/runtimes/copilot.md +1 -1
  26. package/docs/runtimes/cursor.md +1 -1
  27. package/docs/runtimes/gemini.md +1 -1
  28. package/docs/runtimes/kilo.md +1 -1
  29. package/docs/runtimes/openclaw.md +1 -1
  30. package/docs/runtimes/opencode.md +1 -1
  31. package/docs/runtimes/qoder.md +1 -1
  32. package/docs/runtimes/trae.md +1 -1
  33. package/docs/runtimes/windsurf.md +1 -1
  34. package/docs/zh-CN/example-prompts.md +82 -69
  35. package/docs/zh-CN/project-structure.md +8 -4
  36. package/docs/zh-TW/README.md +88 -62
  37. package/openclaw.plugin.json +1 -1
  38. package/package.json +3 -3
  39. package/skills/eval_queries.json +179 -56
  40. package/skills/fec-accessibility-check/SKILL.md +1 -1
  41. package/skills/fec-alchemy/SKILL.md +74 -0
  42. package/skills/fec-alchemy/assets/absorption-plan-template.md +47 -0
  43. package/skills/fec-alchemy/assets/intake-template.md +55 -0
  44. package/skills/fec-alchemy/assets/review-checklist.md +31 -0
  45. package/skills/fec-alchemy/references/methodology.md +94 -0
  46. package/skills/fec-alchemy/references/originality-and-licensing.md +57 -0
  47. package/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
  48. package/skills/fec-browser-storage/SKILL.md +1 -1
  49. package/skills/fec-canvas-threejs/SKILL.md +1 -1
  50. package/skills/fec-code-review/SKILL.md +11 -3
  51. package/skills/fec-data-fetching/SKILL.md +15 -15
  52. package/skills/fec-data-fetching/references/query-patterns.md +27 -1
  53. package/skills/fec-debug-framework/SKILL.md +14 -19
  54. package/skills/fec-debug-framework/references/report-template.md +58 -0
  55. package/skills/fec-doc-sync/SKILL.md +19 -17
  56. package/skills/fec-drawio-studio/SKILL.md +69 -0
  57. package/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  58. package/skills/fec-drawio-studio/data/brand-icons.json +878 -0
  59. package/skills/fec-drawio-studio/data/shape-index.json.gz +0 -0
  60. package/skills/fec-drawio-studio/references/data-residency.md +27 -0
  61. package/skills/fec-drawio-studio/references/diagram-patterns.md +41 -0
  62. package/skills/fec-drawio-studio/references/layout-and-export.md +57 -0
  63. package/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
  64. package/skills/fec-drawio-studio/references/xml-and-mermaid.md +54 -0
  65. package/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
  66. package/skills/fec-drawio-studio/scripts/diagram-lint.mjs +143 -0
  67. package/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
  68. package/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
  69. package/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  70. package/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  71. package/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  72. package/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  73. package/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  74. package/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  75. package/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  76. package/skills/fec-drawio-studio/scripts/shape-query.mjs +81 -0
  77. package/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
  78. package/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
  79. package/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
  80. package/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
  81. package/skills/fec-drawio-studio/styles/schema.json +112 -0
  82. package/skills/fec-e2e-testing/SKILL.md +5 -3
  83. package/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
  84. package/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
  85. package/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
  86. package/skills/fec-form-handling/SKILL.md +11 -11
  87. package/skills/fec-form-handling/references/advanced-form-patterns.md +34 -5
  88. package/skills/fec-image-generation/SKILL.md +56 -0
  89. package/skills/fec-image-generation/references/artifact-routing.md +80 -0
  90. package/skills/fec-image-generation/references/diagram-workflows.md +43 -0
  91. package/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
  92. package/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
  93. package/skills/fec-implement-from-design/SKILL.md +9 -2
  94. package/skills/fec-legacy-to-modern-migration/SKILL.md +29 -163
  95. package/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
  96. package/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
  97. package/skills/fec-legacy-web-standard/SKILL.md +1 -1
  98. package/skills/fec-list-virtualization/SKILL.md +3 -3
  99. package/skills/fec-list-virtualization/references/virtualization-patterns.md +32 -4
  100. package/skills/fec-motion-interaction/SKILL.md +7 -0
  101. package/skills/fec-nextjs-project-standard/SKILL.md +7 -1
  102. package/skills/fec-performance-optimization/SKILL.md +9 -1
  103. package/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
  104. package/skills/fec-performance-optimization/references/report-template.md +59 -0
  105. package/skills/fec-pwa-implementation/SKILL.md +3 -4
  106. package/skills/fec-react-project-standard/SKILL.md +12 -4
  107. package/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
  108. package/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
  109. package/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
  110. package/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
  111. package/skills/fec-refactor-clean/SKILL.md +4 -1
  112. package/skills/fec-refactor-clean/references/report-template.md +42 -0
  113. package/skills/fec-responsive-layout/references/responsive-layout-patterns.md +11 -0
  114. package/skills/fec-security-review/SKILL.md +3 -3
  115. package/skills/fec-source-driven-development/SKILL.md +26 -0
  116. package/skills/fec-state-management/SKILL.md +1 -1
  117. package/skills/fec-storybook-component-doc/SKILL.md +2 -2
  118. package/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +5 -1
  119. package/skills/fec-typescript-project-standard/SKILL.md +54 -0
  120. package/skills/{fec-typescript-type-safety → fec-typescript-project-standard}/references/type-safety-patterns.md +1 -1
  121. package/skills/{fec-typescript-type-safety/SKILL.md → fec-typescript-project-standard/references/type-safety.md} +1 -10
  122. package/skills/fec-ui-design/SKILL.md +26 -8
  123. package/skills/fec-ui-design/data/style-archetypes.json +40 -0
  124. package/skills/fec-ui-design/data/ux-quality-rules.json +35 -0
  125. package/skills/fec-ui-design/references/design-intelligence.md +66 -6
  126. package/skills/fec-ui-design/references/pre-delivery-checklist.md +6 -0
  127. package/skills/fec-ui-design/scripts/design-system.mjs +119 -1
  128. package/skills/fec-validation-fix/SKILL.md +5 -2
  129. package/skills/fec-validation-fix/references/report-template.md +43 -0
  130. package/skills/fec-vue3-project-standard/SKILL.md +12 -3
  131. package/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +163 -0
  132. package/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
  133. package/skills/fec-vue3-project-standard/references/vue3-project-structure.md +177 -0
  134. package/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
  135. package/skills/fec-web-video-presentation/SKILL.md +61 -0
  136. package/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
  137. package/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
  138. package/skills/fec-web-video-presentation/references/content-plan.md +38 -0
  139. package/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
  140. package/skills/fec-web-video-presentation/references/theme-system.md +36 -0
  141. package/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
  142. package/skills/fec-web-workers/SKILL.md +3 -4
  143. package/skills/metadata.json +381 -168
  144. package/skills/relations.json +136 -25
  145. package/templates/shared/rules/fec-ci-cd.md +20 -108
  146. package/templates/shared/rules/fec-design-system.md +47 -223
  147. package/templates/shared/rules/fec-error-handling.md +35 -179
  148. package/templates/shared/rules/fec-i18n.md +43 -91
  149. package/templates/shared/rules/fec-refactoring.md +4 -0
  150. package/templates/shared/rules/fec-rendering-patterns.md +28 -171
  151. package/templates/shared/rules/fec-responsive-design.md +34 -211
  152. package/templates/shared/rules/fec-state-management.md +41 -144
  153. package/templates/shared/rules/fec-typescript.md +47 -290
  154. package/skills/fec-e2e-testing/references/e2e-advanced.md +0 -176
  155. package/skills/fec-react-project-standard/references/react-project-details.md +0 -642
  156. package/skills/fec-vue3-project-standard/references/vue3-project-details.md +0 -547
@@ -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);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import zlib from "node:zlib";
5
+ import { formatResult, parseArgs, skillDir } from "./studio-core.mjs";
6
+
7
+ const INDEX = path.join(skillDir, "data", "shape-index.json.gz");
8
+ const ALIASES = new Map([
9
+ ["亚马逊", "aws"], ["阿里云", "alibaba cloud"], ["微软云", "azure"], ["谷歌云", "gcp"],
10
+ ["云", "cloud"], ["数据库", "database"], ["队列", "queue"], ["网关", "gateway"],
11
+ ["服务", "service"], ["用户", "user"], ["角色", "actor"], ["流程", "flowchart"],
12
+ ["容器", "container"], ["网络", "network"], ["交换机", "switch"], ["路由器", "router"],
13
+ ["函数", "lambda"], ["对象存储", "storage"], ["消息", "message"],
14
+ ]);
15
+
16
+ export function loadShapes() {
17
+ return JSON.parse(zlib.gunzipSync(fs.readFileSync(INDEX)).toString("utf8"));
18
+ }
19
+
20
+ export function queryShapes(shapes, query, options = {}) {
21
+ const terms = expandTerms(query);
22
+ const category = options.category ? String(options.category).toLowerCase() : "";
23
+ const scored = [];
24
+ for (const [index, shape] of shapes.entries()) {
25
+ const haystack = `${shape.title ?? ""} ${shape.tags ?? ""} ${shape.style ?? ""}`.toLowerCase();
26
+ if (category && !haystack.includes(category)) continue;
27
+ let score = 0;
28
+ const title = String(shape.title ?? "").toLowerCase();
29
+ const tags = String(shape.tags ?? "").toLowerCase();
30
+ for (const term of terms) {
31
+ if (title === term) score += 80;
32
+ else if (title.includes(term)) score += 45;
33
+ if (tags.split(/\s+/).includes(term)) score += 30;
34
+ else if (tags.includes(term)) score += 12;
35
+ if (fuzzyContains(haystack, term)) score += 4;
36
+ }
37
+ if (score > 0) scored.push({ index, score, titleHits: terms.filter((term) => title.includes(term)).length, shape });
38
+ }
39
+ return scored
40
+ .sort((a, b) => b.score - a.score || b.titleHits - a.titleHits || String(a.shape.title).localeCompare(String(b.shape.title)) || a.index - b.index)
41
+ .slice(0, Number(options.limit ?? 10))
42
+ .map(({ shape, score }) => ({ title: shape.title, w: shape.w, h: shape.h, style: shape.style, score }));
43
+ }
44
+
45
+ function expandTerms(query) {
46
+ const normalized = String(query ?? "").toLowerCase();
47
+ const raw = normalized.match(/[\p{L}\p{N}._-]+/gu) ?? [];
48
+ const expanded = [];
49
+ for (const term of raw) {
50
+ expanded.push(ALIASES.get(term) ?? term);
51
+ if (term.includes("-")) expanded.push(...term.split("-"));
52
+ }
53
+ return [...new Set(expanded.join(" ").match(/[a-z0-9._-]+|[\u4e00-\u9fff]+/g) ?? [])].filter((term) => term.length >= 2);
54
+ }
55
+
56
+ function fuzzyContains(text, term) {
57
+ if (term.length < 4) return false;
58
+ let cursor = 0;
59
+ for (const ch of text) {
60
+ if (ch === term[cursor]) cursor += 1;
61
+ if (cursor === term.length) return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ const args = parseArgs(process.argv.slice(2));
67
+ const query = args._.join(" ");
68
+ if (!query) {
69
+ console.error("usage: node shape-query.mjs <keywords> [--limit 10] [--category aws] [--format markdown|json]");
70
+ process.exit(2);
71
+ }
72
+ const results = queryShapes(loadShapes(), query, args);
73
+ if (results.length === 0) {
74
+ console.error(`no shapes matched ${JSON.stringify(query)}`);
75
+ process.exit(1);
76
+ }
77
+ if ((args.format ?? (args.json ? "json" : "markdown")) === "json") {
78
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
79
+ } else {
80
+ process.stdout.write(formatResult({ title: "Shape Query", query, results }));
81
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const scriptDir = path.dirname(fileURLToPath(import.meta.url));
7
+ export const skillDir = path.resolve(scriptDir, "..");
8
+
9
+ export function readText(filePath) {
10
+ return fs.readFileSync(filePath, "utf8");
11
+ }
12
+
13
+ export function writeText(filePath, body) {
14
+ fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true });
15
+ fs.writeFileSync(filePath, body, "utf8");
16
+ }
17
+
18
+ export function parseArgs(argv) {
19
+ const args = { _: [] };
20
+ for (let i = 0; i < argv.length; i += 1) {
21
+ const token = argv[i];
22
+ if (!token.startsWith("--")) {
23
+ args._.push(token);
24
+ continue;
25
+ }
26
+ const [rawKey, inline] = token.slice(2).split("=", 2);
27
+ const key = rawKey.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
28
+ if (inline !== undefined) {
29
+ args[key] = inline;
30
+ } else if (argv[i + 1] && !argv[i + 1].startsWith("--")) {
31
+ args[key] = argv[i + 1];
32
+ i += 1;
33
+ } else {
34
+ args[key] = true;
35
+ }
36
+ }
37
+ return args;
38
+ }
39
+
40
+ export function formatResult(result, format = "markdown") {
41
+ if (format === "json") return `${JSON.stringify(result, null, 2)}\n`;
42
+ const lines = [`# ${result.title ?? "Drawio Studio Result"}`, ""];
43
+ for (const [key, value] of Object.entries(result)) {
44
+ if (key === "title") continue;
45
+ if (Array.isArray(value)) {
46
+ lines.push(`## ${label(key)}`, "");
47
+ if (value.length === 0) lines.push("- None");
48
+ else for (const item of value) lines.push(`- ${typeof item === "string" ? item : JSON.stringify(item)}`);
49
+ lines.push("");
50
+ } else if (value && typeof value === "object") {
51
+ lines.push(`## ${label(key)}`, "", "```json", JSON.stringify(value, null, 2), "```", "");
52
+ } else {
53
+ lines.push(`- ${label(key)}: ${String(value)}`);
54
+ }
55
+ }
56
+ return `${lines.join("\n").trimEnd()}\n`;
57
+ }
58
+
59
+ export function label(value) {
60
+ return value.replace(/([A-Z])/g, " $1").replace(/^./, (ch) => ch.toUpperCase());
61
+ }
62
+
63
+ export function xmlEscape(value) {
64
+ return String(value)
65
+ .replaceAll("&", "&amp;")
66
+ .replaceAll("<", "&lt;")
67
+ .replaceAll(">", "&gt;")
68
+ .replaceAll('"', "&quot;");
69
+ }
70
+
71
+ export function snap(value, grid = 10) {
72
+ return Math.round(value / grid) * grid;
73
+ }
74
+
75
+ export function walkFiles(root, predicate) {
76
+ const out = [];
77
+ const skip = new Set([".git", "node_modules", "dist", "target", "vendor", ".next", ".nuxt", "coverage"]);
78
+ function visit(dir) {
79
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
80
+ if (skip.has(entry.name)) continue;
81
+ const full = path.join(dir, entry.name);
82
+ if (entry.isDirectory()) visit(full);
83
+ else if (!predicate || predicate(full)) out.push(full);
84
+ }
85
+ }
86
+ visit(path.resolve(root));
87
+ return out.sort();
88
+ }
89
+
90
+ export function toPosix(value) {
91
+ return value.split(path.sep).join("/");
92
+ }
93
+
94
+ export function commonPrefix(values) {
95
+ if (values.length === 0) return "";
96
+ let prefix = values[0];
97
+ for (const value of values.slice(1)) {
98
+ while (prefix && !value.startsWith(prefix)) prefix = prefix.slice(0, -1);
99
+ }
100
+ return prefix.replace(/[^/]*$/, "");
101
+ }
102
+
103
+ export function reduceTransitive(nodes, edges) {
104
+ const ids = new Set(nodes.map((node) => node.id));
105
+ const unique = [];
106
+ const seen = new Set();
107
+ for (const edge of edges) {
108
+ if (!ids.has(edge.source) || !ids.has(edge.target) || edge.source === edge.target) continue;
109
+ const key = `${edge.source}\0${edge.target}`;
110
+ if (!seen.has(key)) {
111
+ seen.add(key);
112
+ unique.push({ source: edge.source, target: edge.target });
113
+ }
114
+ }
115
+ const adjacency = new Map();
116
+ for (const edge of unique) {
117
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, new Set());
118
+ adjacency.get(edge.source).add(edge.target);
119
+ }
120
+ return unique.filter((edge) => !hasAlternatePath(edge.source, edge.target, adjacency, edge));
121
+ }
122
+
123
+ function hasAlternatePath(source, target, adjacency, skipped) {
124
+ const stack = [...(adjacency.get(source) ?? [])].filter((next) => !(source === skipped.source && next === skipped.target));
125
+ const seen = new Set();
126
+ while (stack.length) {
127
+ const next = stack.pop();
128
+ if (next === target) return true;
129
+ if (!next || seen.has(next)) continue;
130
+ seen.add(next);
131
+ for (const child of adjacency.get(next) ?? []) stack.push(child);
132
+ }
133
+ return false;
134
+ }
135
+
136
+ export function printOrWrite(body, output) {
137
+ if (output) writeText(output, body);
138
+ else process.stdout.write(body);
139
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "../schema.json",
3
+ "name": "corporate",
4
+ "version": 1,
5
+ "default": false,
6
+ "source": { "type": "built-in" },
7
+ "confidence": "high",
8
+ "palette": {
9
+ "primary": { "fillColor": "#e3f2fd", "strokeColor": "#1565c0" },
10
+ "success": { "fillColor": "#e8f5e9", "strokeColor": "#2e7d32" },
11
+ "warning": { "fillColor": "#fff9c4", "strokeColor": "#f57c00" },
12
+ "accent": { "fillColor": "#fff3e0", "strokeColor": "#e65100" },
13
+ "danger": { "fillColor": "#ffebee", "strokeColor": "#c62828" },
14
+ "neutral": { "fillColor": "#eceff1", "strokeColor": "#455a64" },
15
+ "secondary": { "fillColor": "#f3e5f5", "strokeColor": "#6a1b9a" }
16
+ },
17
+ "roles": {
18
+ "service": "primary",
19
+ "database": "success",
20
+ "queue": "warning",
21
+ "gateway": "accent",
22
+ "error": "danger",
23
+ "external": "neutral",
24
+ "security": "secondary"
25
+ },
26
+ "shapes": {
27
+ "service": "rounded=0",
28
+ "database": "shape=cylinder3",
29
+ "queue": "rounded=0",
30
+ "decision": "rhombus",
31
+ "external": "rounded=0;dashed=1",
32
+ "container": "swimlane;startSize=30"
33
+ },
34
+ "font": {
35
+ "fontFamily": "Arial",
36
+ "fontSize": 11,
37
+ "titleFontSize": 13,
38
+ "titleBold": true
39
+ },
40
+ "edges": {
41
+ "style": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1",
42
+ "arrow": "endArrow=classic;endFill=1",
43
+ "dashedFor": ["optional", "async"]
44
+ },
45
+ "extras": {
46
+ "sketch": false,
47
+ "globalStrokeWidth": 1
48
+ }
49
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "../schema.json",
3
+ "name": "default",
4
+ "version": 1,
5
+ "default": false,
6
+ "source": { "type": "built-in" },
7
+ "confidence": "high",
8
+ "palette": {
9
+ "primary": { "fillColor": "#dae8fc", "strokeColor": "#6c8ebf" },
10
+ "success": { "fillColor": "#d5e8d4", "strokeColor": "#82b366" },
11
+ "warning": { "fillColor": "#fff2cc", "strokeColor": "#d6b656" },
12
+ "accent": { "fillColor": "#ffe6cc", "strokeColor": "#d79b00" },
13
+ "danger": { "fillColor": "#f8cecc", "strokeColor": "#b85450" },
14
+ "neutral": { "fillColor": "#f5f5f5", "strokeColor": "#666666" },
15
+ "secondary": { "fillColor": "#e1d5e7", "strokeColor": "#9673a6" }
16
+ },
17
+ "roles": {
18
+ "service": "primary",
19
+ "database": "success",
20
+ "queue": "warning",
21
+ "gateway": "accent",
22
+ "error": "danger",
23
+ "external": "neutral",
24
+ "security": "secondary"
25
+ },
26
+ "shapes": {
27
+ "service": "rounded=1",
28
+ "database": "shape=cylinder3",
29
+ "queue": "rounded=1",
30
+ "decision": "rhombus",
31
+ "external": "rounded=1;dashed=1",
32
+ "container": "swimlane;startSize=30"
33
+ },
34
+ "font": {
35
+ "fontFamily": "Helvetica",
36
+ "fontSize": 12,
37
+ "titleFontSize": 14,
38
+ "titleBold": true
39
+ },
40
+ "edges": {
41
+ "style": "edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1",
42
+ "arrow": "endArrow=classic;endFill=1",
43
+ "dashedFor": []
44
+ },
45
+ "extras": {
46
+ "sketch": false,
47
+ "globalStrokeWidth": 1
48
+ }
49
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "../schema.json",
3
+ "name": "handdrawn",
4
+ "version": 1,
5
+ "default": false,
6
+ "source": { "type": "built-in" },
7
+ "confidence": "high",
8
+ "palette": {
9
+ "primary": { "fillColor": "#ffe4b5", "strokeColor": "#b8651e" },
10
+ "success": { "fillColor": "#def0dc", "strokeColor": "#5c8a49" },
11
+ "warning": { "fillColor": "#fff4cc", "strokeColor": "#b8901a" },
12
+ "accent": { "fillColor": "#ffd9b3", "strokeColor": "#c25100" },
13
+ "danger": { "fillColor": "#ffcdbf", "strokeColor": "#a53d3d" },
14
+ "neutral": { "fillColor": "#f5e6d3", "strokeColor": "#8b7355" },
15
+ "secondary": { "fillColor": "#e6d7e8", "strokeColor": "#7b4397" }
16
+ },
17
+ "roles": {
18
+ "service": "primary",
19
+ "database": "success",
20
+ "queue": "warning",
21
+ "gateway": "accent",
22
+ "error": "danger",
23
+ "external": "neutral",
24
+ "security": "secondary"
25
+ },
26
+ "shapes": {
27
+ "service": "rounded=1",
28
+ "database": "shape=cylinder3",
29
+ "queue": "rounded=1",
30
+ "decision": "rhombus",
31
+ "external": "rounded=1;dashed=1",
32
+ "container": "swimlane;startSize=30"
33
+ },
34
+ "font": {
35
+ "fontFamily": "Helvetica",
36
+ "fontSize": 12,
37
+ "titleFontSize": 14,
38
+ "titleBold": true
39
+ },
40
+ "edges": {
41
+ "style": "edgeStyle=orthogonalEdgeStyle;curved=1;rounded=1;orthogonalLoop=1;jettySize=auto;html=1",
42
+ "arrow": "endArrow=classic;endFill=1",
43
+ "dashedFor": ["optional"]
44
+ },
45
+ "extras": {
46
+ "sketch": true,
47
+ "globalStrokeWidth": 2
48
+ }
49
+ }