@aiready/cli 0.9.2 → 0.9.5

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.
@@ -1,7 +1,7 @@
1
1
 
2
2
  
3
- > @aiready/cli@0.9.1 build /Users/pengcao/projects/aiready/packages/cli
4
- > tsup src/index.ts src/cli.ts --format cjs,esm --dts
3
+ > @aiready/cli@0.9.5 build /Users/pengcao/projects/aiready/packages/cli
4
+ > tsup src/index.ts src/cli.ts --format cjs,esm
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
7
7
  CLI Using tsconfig: tsconfig.json
@@ -9,16 +9,10 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
+ CJS dist/cli.js 58.06 KB
12
13
  CJS dist/index.js 4.93 KB
13
- CJS dist/cli.js 44.24 KB
14
- CJS ⚡️ Build success in 22ms
14
+ CJS ⚡️ Build success in 20ms
15
+ ESM dist/cli.mjs 50.92 KB
15
16
  ESM dist/chunk-3SG2GLFJ.mjs 3.80 KB
16
17
  ESM dist/index.mjs 138.00 B
17
- ESM dist/cli.mjs 37.19 KB
18
- ESM ⚡️ Build success in 22ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 616ms
21
- DTS dist/cli.d.ts 20.00 B
22
- DTS dist/index.d.ts 1.22 KB
23
- DTS dist/cli.d.mts 20.00 B
24
- DTS dist/index.d.mts 1.22 KB
18
+ ESM ⚡️ Build success in 19ms
@@ -1,34 +1,16 @@
1
1
 
2
2
  
3
- > @aiready/cli@0.9.1 test /Users/pengcao/projects/aiready/packages/cli
3
+ > @aiready/cli@0.9.5 test /Users/pengcao/projects/aiready/packages/cli
4
4
  > vitest run
5
5
 
6
6
  [?25l
7
7
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/cli
8
8
 
9
- [?2026h
10
-  ❯ src/__tests__/cli.test.ts [queued]
11
-
12
-  Test Files 0 passed (1)
13
-  Tests 0 passed (0)
14
-  Start at 20:59:54
15
-  Duration 101ms
16
- [?2026l[?2026h
17
-  ❯ src/__tests__/cli.test.ts 0/3
18
-
19
-  Test Files 0 passed (1)
20
-  Tests 0 passed (3)
21
-  Start at 20:59:54
22
-  Duration 521ms
23
- [?2026l ✓ src/__tests__/cli.test.ts (3 tests) 2ms
24
- ✓ CLI Unified Analysis (3)
25
- ✓ should run unified analysis with both tools 1ms
26
- ✓ should run analysis with only patterns tool 0ms
27
- ✓ should run analysis with only context tool 0ms
9
+ ✓ src/__tests__/cli.test.ts (3 tests) 2ms
28
10
 
29
11
   Test Files  1 passed (1)
30
12
   Tests  3 passed (3)
31
-  Start at  20:59:54
32
-  Duration  581ms (transform 114ms, setup 0ms, import 378ms, tests 2ms, environment 0ms)
13
+  Start at  22:45:25
14
+  Duration  1.55s (transform 428ms, setup 0ms, import 949ms, tests 2ms, environment 0ms)
33
15
 
34
16
  [?25h
@@ -0,0 +1,126 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/index.ts
9
+ import { analyzePatterns } from "@aiready/pattern-detect";
10
+ import { analyzeContext } from "@aiready/context-analyzer";
11
+ import { analyzeConsistency } from "@aiready/consistency";
12
+ var severityOrder = {
13
+ critical: 4,
14
+ major: 3,
15
+ minor: 2,
16
+ info: 1
17
+ };
18
+ function sortBySeverity(results) {
19
+ return results.map((file) => {
20
+ const sortedIssues = [...file.issues].sort((a, b) => {
21
+ const severityDiff = (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
22
+ if (severityDiff !== 0) return severityDiff;
23
+ return (a.location?.line || 0) - (b.location?.line || 0);
24
+ });
25
+ return { ...file, issues: sortedIssues };
26
+ }).sort((a, b) => {
27
+ const aMaxSeverity = Math.max(...a.issues.map((i) => severityOrder[i.severity] || 0), 0);
28
+ const bMaxSeverity = Math.max(...b.issues.map((i) => severityOrder[i.severity] || 0), 0);
29
+ if (aMaxSeverity !== bMaxSeverity) {
30
+ return bMaxSeverity - aMaxSeverity;
31
+ }
32
+ if (a.issues.length !== b.issues.length) {
33
+ return b.issues.length - a.issues.length;
34
+ }
35
+ return a.fileName.localeCompare(b.fileName);
36
+ });
37
+ }
38
+ async function analyzeUnified(options) {
39
+ const startTime = Date.now();
40
+ const tools = options.tools || ["patterns", "context", "consistency"];
41
+ const result = {
42
+ summary: {
43
+ totalIssues: 0,
44
+ toolsRun: tools,
45
+ executionTime: 0
46
+ }
47
+ };
48
+ if (tools.includes("patterns")) {
49
+ const patternResult = await analyzePatterns(options);
50
+ if (options.progressCallback) {
51
+ options.progressCallback({ tool: "patterns", data: patternResult });
52
+ }
53
+ result.patterns = sortBySeverity(patternResult.results);
54
+ result.duplicates = patternResult.duplicates;
55
+ result.summary.totalIssues += patternResult.results.reduce(
56
+ (sum, file) => sum + file.issues.length,
57
+ 0
58
+ );
59
+ }
60
+ if (tools.includes("context")) {
61
+ const contextResults = await analyzeContext(options);
62
+ if (options.progressCallback) {
63
+ options.progressCallback({ tool: "context", data: contextResults });
64
+ }
65
+ result.context = contextResults.sort((a, b) => {
66
+ const severityDiff = (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
67
+ if (severityDiff !== 0) return severityDiff;
68
+ if (a.tokenCost !== b.tokenCost) return b.tokenCost - a.tokenCost;
69
+ return b.fragmentationScore - a.fragmentationScore;
70
+ });
71
+ result.summary.totalIssues += result.context?.length || 0;
72
+ }
73
+ if (tools.includes("consistency")) {
74
+ const consistencyOptions = {
75
+ rootDir: options.rootDir,
76
+ include: options.include,
77
+ exclude: options.exclude,
78
+ ...options.consistency || {}
79
+ };
80
+ const report = await analyzeConsistency(consistencyOptions);
81
+ if (options.progressCallback) {
82
+ options.progressCallback({ tool: "consistency", data: report });
83
+ }
84
+ if (report.results) {
85
+ report.results = sortBySeverity(report.results);
86
+ }
87
+ result.consistency = report;
88
+ result.summary.totalIssues += report.summary.totalIssues;
89
+ }
90
+ result.summary.executionTime = Date.now() - startTime;
91
+ return result;
92
+ }
93
+ function generateUnifiedSummary(result) {
94
+ const { summary } = result;
95
+ let output = `\u{1F680} AIReady Analysis Complete
96
+
97
+ `;
98
+ output += `\u{1F4CA} Summary:
99
+ `;
100
+ output += ` Tools run: ${summary.toolsRun.join(", ")}
101
+ `;
102
+ output += ` Total issues found: ${summary.totalIssues}
103
+ `;
104
+ output += ` Execution time: ${(summary.executionTime / 1e3).toFixed(2)}s
105
+
106
+ `;
107
+ if (result.patterns) {
108
+ output += `\u{1F50D} Pattern Analysis: ${result.patterns.length} issues
109
+ `;
110
+ }
111
+ if (result.context) {
112
+ output += `\u{1F9E0} Context Analysis: ${result.context.length} issues
113
+ `;
114
+ }
115
+ if (result.consistency) {
116
+ output += `\u{1F3F7}\uFE0F Consistency Analysis: ${result.consistency.summary.totalIssues} issues
117
+ `;
118
+ }
119
+ return output;
120
+ }
121
+
122
+ export {
123
+ __require,
124
+ analyzeUnified,
125
+ generateUnifiedSummary
126
+ };
package/dist/cli.js CHANGED
@@ -118,6 +118,7 @@ var import_fs = require("fs");
118
118
  var import_path = require("path");
119
119
  var import_core = require("@aiready/core");
120
120
  var import_fs2 = require("fs");
121
+ var import_path2 = require("path");
121
122
  var packageJson = JSON.parse((0, import_fs2.readFileSync)((0, import_path.join)(__dirname, "../package.json"), "utf8"));
122
123
  var program = new import_commander.Command();
123
124
  program.name("aiready").description("AIReady - Assess and improve AI-readiness of codebases").version(packageJson.version).addHelpText("after", `
@@ -811,4 +812,284 @@ function generateMarkdownReport(report, elapsedTime) {
811
812
  }
812
813
  return markdown;
813
814
  }
815
+ function generateHTML(graph) {
816
+ const payload = JSON.stringify(graph, null, 2);
817
+ return `<!doctype html>
818
+ <html>
819
+ <head>
820
+ <meta charset="utf-8" />
821
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
822
+ <title>AIReady Visualization</title>
823
+ <style>
824
+ html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
825
+ #container { display:flex; height:100vh }
826
+ #panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
827
+ #canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
828
+ canvas { background: #0b1220; border-radius:8px }
829
+ .stat { margin-bottom:12px }
830
+ </style>
831
+ </head>
832
+ <body>
833
+ <div id="container">
834
+ <div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
835
+ <div id="panel">
836
+ <h2>AIReady Visualization</h2>
837
+ <div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
838
+ <div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
839
+ <div class="stat"><strong>Legend</strong></div>
840
+ <div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
841
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff4d4f;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Critical</strong>: highest severity issues.</div>
842
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff9900;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Major</strong>: important issues.</div>
843
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ffd666;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Minor</strong>: low priority issues.</div>
844
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#91d5ff;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Info</strong>: informational notes.</div>
845
+ <div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
846
+ <div style="margin-top:6px;color:#94a3b8"><strong>Proximity</strong>: nodes that are spatially close are more contextually related; relatedness is represented by distance and size rather than explicit edges.</div>
847
+ <div style="margin-top:6px;color:#94a3b8"><strong>Edge colors</strong>: <span style="color:#fb7e81">Similarity</span>, <span style="color:#84c1ff">Dependency</span>, <span style="color:#ffa500">Reference</span>, default <span style="color:#334155">Other</span>.</div>
848
+ </div>
849
+ </div>
850
+ </div>
851
+
852
+ <script>
853
+ const graphData = ${payload};
854
+ document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
855
+ document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
856
+
857
+ const canvas = document.getElementById('canvas');
858
+ const ctx = canvas.getContext('2d');
859
+
860
+ const nodes = graphData.nodes.map((n, i) => ({
861
+ ...n,
862
+ x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
863
+ y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
864
+ }));
865
+
866
+ function draw() {
867
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
868
+
869
+ graphData.edges.forEach(edge => {
870
+ const s = nodes.find(n => n.id === edge.source);
871
+ const t = nodes.find(n => n.id === edge.target);
872
+ if (!s || !t) return;
873
+ if (edge.type === 'related') return;
874
+ if (edge.type === 'similarity') {
875
+ ctx.strokeStyle = '#fb7e81';
876
+ ctx.lineWidth = 1.2;
877
+ } else if (edge.type === 'dependency') {
878
+ ctx.strokeStyle = '#84c1ff';
879
+ ctx.lineWidth = 1.0;
880
+ } else if (edge.type === 'reference') {
881
+ ctx.strokeStyle = '#ffa500';
882
+ ctx.lineWidth = 0.9;
883
+ } else {
884
+ ctx.strokeStyle = '#334155';
885
+ ctx.lineWidth = 0.8;
886
+ }
887
+ ctx.beginPath();
888
+ ctx.moveTo(s.x, s.y);
889
+ ctx.lineTo(t.x, t.y);
890
+ ctx.stroke();
891
+ });
892
+
893
+ const groups = {};
894
+ nodes.forEach(n => {
895
+ const g = n.group || '__default';
896
+ if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
897
+ groups[g].minX = Math.min(groups[g].minX, n.x);
898
+ groups[g].minY = Math.min(groups[g].minY, n.y);
899
+ groups[g].maxX = Math.max(groups[g].maxX, n.x);
900
+ groups[g].maxY = Math.max(groups[g].maxY, n.y);
901
+ });
902
+
903
+ const groupRelations = {};
904
+ graphData.edges.forEach(edge => {
905
+ const sNode = nodes.find(n => n.id === edge.source);
906
+ const tNode = nodes.find(n => n.id === edge.target);
907
+ if (!sNode || !tNode) return;
908
+ const g1 = sNode.group || '__default';
909
+ const g2 = tNode.group || '__default';
910
+ if (g1 === g2) return;
911
+ const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
912
+ groupRelations[key] = (groupRelations[key] || 0) + 1;
913
+ });
914
+
915
+ Object.keys(groupRelations).forEach(k => {
916
+ const count = groupRelations[k];
917
+ const [ga, gb] = k.split('::');
918
+ if (!groups[ga] || !groups[gb]) return;
919
+ const ax = (groups[ga].minX + groups[ga].maxX) / 2;
920
+ const ay = (groups[ga].minY + groups[ga].maxY) / 2;
921
+ const bx = (groups[gb].minX + groups[gb].maxX) / 2;
922
+ const by = (groups[gb].minY + groups[gb].maxY) / 2;
923
+ ctx.beginPath();
924
+ ctx.strokeStyle = 'rgba(148,163,184,0.25)';
925
+ ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
926
+ ctx.moveTo(ax, ay);
927
+ ctx.lineTo(bx, by);
928
+ ctx.stroke();
929
+ });
930
+
931
+ Object.keys(groups).forEach(g => {
932
+ if (g === '__default') return;
933
+ const box = groups[g];
934
+ const pad = 16;
935
+ const x = box.minX - pad;
936
+ const y = box.minY - pad;
937
+ const w = (box.maxX - box.minX) + pad * 2;
938
+ const h = (box.maxY - box.minY) + pad * 2;
939
+ ctx.save();
940
+ ctx.fillStyle = 'rgba(30,64,175,0.04)';
941
+ ctx.strokeStyle = 'rgba(30,64,175,0.12)';
942
+ ctx.lineWidth = 1.2;
943
+ const r = 8;
944
+ ctx.beginPath();
945
+ ctx.moveTo(x + r, y);
946
+ ctx.arcTo(x + w, y, x + w, y + h, r);
947
+ ctx.arcTo(x + w, y + h, x, y + h, r);
948
+ ctx.arcTo(x, y + h, x, y, r);
949
+ ctx.arcTo(x, y, x + w, y, r);
950
+ ctx.closePath();
951
+ ctx.fill();
952
+ ctx.stroke();
953
+ ctx.restore();
954
+ ctx.fillStyle = '#94a3b8';
955
+ ctx.font = '11px sans-serif';
956
+ ctx.fillText(g, x + 8, y + 14);
957
+ });
958
+
959
+ nodes.forEach(n => {
960
+ const sizeVal = (n.size || n.value || 1);
961
+ const r = 6 + (sizeVal / 2);
962
+ ctx.beginPath();
963
+ ctx.fillStyle = n.color || '#60a5fa';
964
+ ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
965
+ ctx.fill();
966
+
967
+ ctx.fillStyle = '#e2e8f0';
968
+ ctx.font = '11px sans-serif';
969
+ ctx.textAlign = 'center';
970
+ ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
971
+ });
972
+ }
973
+
974
+ draw();
975
+ </script>
976
+ </body>
977
+ </html>`;
978
+ }
979
+ async function handleVisualize(directory, options) {
980
+ try {
981
+ const dirPath = (0, import_path2.resolve)(process.cwd(), directory || ".");
982
+ const reportPath = (0, import_path2.resolve)(dirPath, options.report || "aiready-improvement-report.json");
983
+ if (!(0, import_fs2.existsSync)(reportPath)) {
984
+ console.error("Report not found at", reportPath);
985
+ console.log("Run `aiready scan` to generate the report, or pass --report");
986
+ return;
987
+ }
988
+ const raw = (0, import_fs2.readFileSync)(reportPath, "utf8");
989
+ const report = JSON.parse(raw);
990
+ console.log("Building graph from report...");
991
+ const { GraphBuilder } = await import("@aiready/visualizer/graph");
992
+ const graph = GraphBuilder.buildFromReport(report, dirPath);
993
+ if (options.dev) {
994
+ try {
995
+ const { spawn } = await import("child_process");
996
+ const webDir = (0, import_path2.resolve)(dirPath, "packages/visualizer");
997
+ const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
998
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
999
+ const onExit = () => {
1000
+ try {
1001
+ watcher.kill();
1002
+ } catch (e) {
1003
+ }
1004
+ try {
1005
+ vite.kill();
1006
+ } catch (e) {
1007
+ }
1008
+ process.exit(0);
1009
+ };
1010
+ process.on("SIGINT", onExit);
1011
+ process.on("SIGTERM", onExit);
1012
+ return;
1013
+ } catch (err) {
1014
+ console.error("Failed to start dev server:", err);
1015
+ }
1016
+ }
1017
+ console.log("Generating HTML...");
1018
+ const html = generateHTML(graph);
1019
+ const outPath = (0, import_path2.resolve)(dirPath, options.output || "packages/visualizer/visualization.html");
1020
+ (0, import_fs.writeFileSync)(outPath, html, "utf8");
1021
+ console.log("Visualization written to:", outPath);
1022
+ if (options.open) {
1023
+ const { exec } = await import("child_process");
1024
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1025
+ exec(`${opener} "${outPath}"`);
1026
+ }
1027
+ if (options.serve) {
1028
+ try {
1029
+ const port = Number(options.serve) || 5173;
1030
+ const http = await import("http");
1031
+ const fsp = await import("fs/promises");
1032
+ const { exec } = await import("child_process");
1033
+ const server = http.createServer(async (req, res) => {
1034
+ try {
1035
+ const urlPath = req.url || "/";
1036
+ if (urlPath === "/" || urlPath === "/index.html") {
1037
+ const content = await fsp.readFile(outPath, "utf8");
1038
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1039
+ res.end(content);
1040
+ return;
1041
+ }
1042
+ res.writeHead(404, { "Content-Type": "text/plain" });
1043
+ res.end("Not found");
1044
+ } catch (e) {
1045
+ res.writeHead(500, { "Content-Type": "text/plain" });
1046
+ res.end("Server error");
1047
+ }
1048
+ });
1049
+ server.listen(port, () => {
1050
+ const addr = `http://localhost:${port}/`;
1051
+ console.log(`Local visualization server running at ${addr}`);
1052
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1053
+ exec(`${opener} "${addr}"`);
1054
+ });
1055
+ process.on("SIGINT", () => {
1056
+ server.close();
1057
+ process.exit(0);
1058
+ });
1059
+ } catch (err) {
1060
+ console.error("Failed to start local server:", err);
1061
+ }
1062
+ }
1063
+ } catch (err) {
1064
+ (0, import_core.handleCLIError)(err, "Visualization");
1065
+ }
1066
+ }
1067
+ program.command("visualise").description("Alias for visualize (British spelling)").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (relative to directory)", "aiready-improvement-report.json").option("-o, --output <path>", "Output HTML path (relative to directory)", "packages/visualizer/visualization.html").option("--open", "Open generated HTML in default browser").option("--serve [port]", "Start a local static server to serve the visualization (optional port number)", false).option("--dev", "Start Vite dev server (live reload) for interactive development", false).addHelpText("after", `
1068
+ EXAMPLES:
1069
+ $ aiready visualise . --report aiready-improvement-report.json
1070
+ $ aiready visualise . --report report.json --dev
1071
+ $ aiready visualise . --report report.json --serve 8080
1072
+
1073
+ NOTES:
1074
+ - Same options as 'visualize'. Use --dev for live reload and --serve to host a static HTML.
1075
+ `).action(async (directory, options) => await handleVisualize(directory, options));
1076
+ program.command("visualize").description("Generate interactive visualization from an AIReady report").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (relative to directory)", "aiready-improvement-report.json").option("-o, --output <path>", "Output HTML path (relative to directory)", "packages/visualizer/visualization.html").option("--open", "Open generated HTML in default browser").option("--serve [port]", "Start a local static server to serve the visualization (optional port number)", false).option("--dev", "Start Vite dev server (live reload) for interactive development", false).addHelpText("after", `
1077
+ EXAMPLES:
1078
+ $ aiready visualize . --report aiready-improvement-report.json
1079
+ $ aiready visualize . --report report.json -o out/visualization.html --open
1080
+ $ aiready visualize . --report report.json --serve
1081
+ $ aiready visualize . --report report.json --serve 8080
1082
+ $ aiready visualize . --report report.json --dev
1083
+
1084
+ NOTES:
1085
+ - The value passed to --report is interpreted relative to the directory argument (first positional).
1086
+ If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
1087
+ - Default output path: packages/visualizer/visualization.html (relative to the directory argument).
1088
+ - --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
1089
+ It serves only the generated HTML (no additional asset folders).
1090
+ - Relatedness is represented by node proximity and size; explicit 'related' edges are not drawn to
1091
+ reduce clutter and improve interactivity on large graphs.
1092
+ - For very large graphs, consider narrowing the input with --include/--exclude or use --serve and
1093
+ allow the browser a moment to stabilize after load.
1094
+ `).action(async (directory, options) => await handleVisualize(directory, options));
814
1095
  program.parse();
package/dist/cli.mjs CHANGED
@@ -21,7 +21,8 @@ import {
21
21
  getRatingDisplay,
22
22
  parseWeightString
23
23
  } from "@aiready/core";
24
- import { readFileSync } from "fs";
24
+ import { readFileSync, existsSync } from "fs";
25
+ import { resolve as resolvePath } from "path";
25
26
  var packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
26
27
  var program = new Command();
27
28
  program.name("aiready").description("AIReady - Assess and improve AI-readiness of codebases").version(packageJson.version).addHelpText("after", `
@@ -715,4 +716,284 @@ function generateMarkdownReport(report, elapsedTime) {
715
716
  }
716
717
  return markdown;
717
718
  }
719
+ function generateHTML(graph) {
720
+ const payload = JSON.stringify(graph, null, 2);
721
+ return `<!doctype html>
722
+ <html>
723
+ <head>
724
+ <meta charset="utf-8" />
725
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
726
+ <title>AIReady Visualization</title>
727
+ <style>
728
+ html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
729
+ #container { display:flex; height:100vh }
730
+ #panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
731
+ #canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
732
+ canvas { background: #0b1220; border-radius:8px }
733
+ .stat { margin-bottom:12px }
734
+ </style>
735
+ </head>
736
+ <body>
737
+ <div id="container">
738
+ <div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
739
+ <div id="panel">
740
+ <h2>AIReady Visualization</h2>
741
+ <div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
742
+ <div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
743
+ <div class="stat"><strong>Legend</strong></div>
744
+ <div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
745
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff4d4f;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Critical</strong>: highest severity issues.</div>
746
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff9900;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Major</strong>: important issues.</div>
747
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ffd666;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Minor</strong>: low priority issues.</div>
748
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#91d5ff;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Info</strong>: informational notes.</div>
749
+ <div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
750
+ <div style="margin-top:6px;color:#94a3b8"><strong>Proximity</strong>: nodes that are spatially close are more contextually related; relatedness is represented by distance and size rather than explicit edges.</div>
751
+ <div style="margin-top:6px;color:#94a3b8"><strong>Edge colors</strong>: <span style="color:#fb7e81">Similarity</span>, <span style="color:#84c1ff">Dependency</span>, <span style="color:#ffa500">Reference</span>, default <span style="color:#334155">Other</span>.</div>
752
+ </div>
753
+ </div>
754
+ </div>
755
+
756
+ <script>
757
+ const graphData = ${payload};
758
+ document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
759
+ document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
760
+
761
+ const canvas = document.getElementById('canvas');
762
+ const ctx = canvas.getContext('2d');
763
+
764
+ const nodes = graphData.nodes.map((n, i) => ({
765
+ ...n,
766
+ x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
767
+ y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
768
+ }));
769
+
770
+ function draw() {
771
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
772
+
773
+ graphData.edges.forEach(edge => {
774
+ const s = nodes.find(n => n.id === edge.source);
775
+ const t = nodes.find(n => n.id === edge.target);
776
+ if (!s || !t) return;
777
+ if (edge.type === 'related') return;
778
+ if (edge.type === 'similarity') {
779
+ ctx.strokeStyle = '#fb7e81';
780
+ ctx.lineWidth = 1.2;
781
+ } else if (edge.type === 'dependency') {
782
+ ctx.strokeStyle = '#84c1ff';
783
+ ctx.lineWidth = 1.0;
784
+ } else if (edge.type === 'reference') {
785
+ ctx.strokeStyle = '#ffa500';
786
+ ctx.lineWidth = 0.9;
787
+ } else {
788
+ ctx.strokeStyle = '#334155';
789
+ ctx.lineWidth = 0.8;
790
+ }
791
+ ctx.beginPath();
792
+ ctx.moveTo(s.x, s.y);
793
+ ctx.lineTo(t.x, t.y);
794
+ ctx.stroke();
795
+ });
796
+
797
+ const groups = {};
798
+ nodes.forEach(n => {
799
+ const g = n.group || '__default';
800
+ if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
801
+ groups[g].minX = Math.min(groups[g].minX, n.x);
802
+ groups[g].minY = Math.min(groups[g].minY, n.y);
803
+ groups[g].maxX = Math.max(groups[g].maxX, n.x);
804
+ groups[g].maxY = Math.max(groups[g].maxY, n.y);
805
+ });
806
+
807
+ const groupRelations = {};
808
+ graphData.edges.forEach(edge => {
809
+ const sNode = nodes.find(n => n.id === edge.source);
810
+ const tNode = nodes.find(n => n.id === edge.target);
811
+ if (!sNode || !tNode) return;
812
+ const g1 = sNode.group || '__default';
813
+ const g2 = tNode.group || '__default';
814
+ if (g1 === g2) return;
815
+ const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
816
+ groupRelations[key] = (groupRelations[key] || 0) + 1;
817
+ });
818
+
819
+ Object.keys(groupRelations).forEach(k => {
820
+ const count = groupRelations[k];
821
+ const [ga, gb] = k.split('::');
822
+ if (!groups[ga] || !groups[gb]) return;
823
+ const ax = (groups[ga].minX + groups[ga].maxX) / 2;
824
+ const ay = (groups[ga].minY + groups[ga].maxY) / 2;
825
+ const bx = (groups[gb].minX + groups[gb].maxX) / 2;
826
+ const by = (groups[gb].minY + groups[gb].maxY) / 2;
827
+ ctx.beginPath();
828
+ ctx.strokeStyle = 'rgba(148,163,184,0.25)';
829
+ ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
830
+ ctx.moveTo(ax, ay);
831
+ ctx.lineTo(bx, by);
832
+ ctx.stroke();
833
+ });
834
+
835
+ Object.keys(groups).forEach(g => {
836
+ if (g === '__default') return;
837
+ const box = groups[g];
838
+ const pad = 16;
839
+ const x = box.minX - pad;
840
+ const y = box.minY - pad;
841
+ const w = (box.maxX - box.minX) + pad * 2;
842
+ const h = (box.maxY - box.minY) + pad * 2;
843
+ ctx.save();
844
+ ctx.fillStyle = 'rgba(30,64,175,0.04)';
845
+ ctx.strokeStyle = 'rgba(30,64,175,0.12)';
846
+ ctx.lineWidth = 1.2;
847
+ const r = 8;
848
+ ctx.beginPath();
849
+ ctx.moveTo(x + r, y);
850
+ ctx.arcTo(x + w, y, x + w, y + h, r);
851
+ ctx.arcTo(x + w, y + h, x, y + h, r);
852
+ ctx.arcTo(x, y + h, x, y, r);
853
+ ctx.arcTo(x, y, x + w, y, r);
854
+ ctx.closePath();
855
+ ctx.fill();
856
+ ctx.stroke();
857
+ ctx.restore();
858
+ ctx.fillStyle = '#94a3b8';
859
+ ctx.font = '11px sans-serif';
860
+ ctx.fillText(g, x + 8, y + 14);
861
+ });
862
+
863
+ nodes.forEach(n => {
864
+ const sizeVal = (n.size || n.value || 1);
865
+ const r = 6 + (sizeVal / 2);
866
+ ctx.beginPath();
867
+ ctx.fillStyle = n.color || '#60a5fa';
868
+ ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
869
+ ctx.fill();
870
+
871
+ ctx.fillStyle = '#e2e8f0';
872
+ ctx.font = '11px sans-serif';
873
+ ctx.textAlign = 'center';
874
+ ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
875
+ });
876
+ }
877
+
878
+ draw();
879
+ </script>
880
+ </body>
881
+ </html>`;
882
+ }
883
+ async function handleVisualize(directory, options) {
884
+ try {
885
+ const dirPath = resolvePath(process.cwd(), directory || ".");
886
+ const reportPath = resolvePath(dirPath, options.report || "aiready-improvement-report.json");
887
+ if (!existsSync(reportPath)) {
888
+ console.error("Report not found at", reportPath);
889
+ console.log("Run `aiready scan` to generate the report, or pass --report");
890
+ return;
891
+ }
892
+ const raw = readFileSync(reportPath, "utf8");
893
+ const report = JSON.parse(raw);
894
+ console.log("Building graph from report...");
895
+ const { GraphBuilder } = await import("@aiready/visualizer/graph");
896
+ const graph = GraphBuilder.buildFromReport(report, dirPath);
897
+ if (options.dev) {
898
+ try {
899
+ const { spawn } = await import("child_process");
900
+ const webDir = resolvePath(dirPath, "packages/visualizer");
901
+ const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
902
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
903
+ const onExit = () => {
904
+ try {
905
+ watcher.kill();
906
+ } catch (e) {
907
+ }
908
+ try {
909
+ vite.kill();
910
+ } catch (e) {
911
+ }
912
+ process.exit(0);
913
+ };
914
+ process.on("SIGINT", onExit);
915
+ process.on("SIGTERM", onExit);
916
+ return;
917
+ } catch (err) {
918
+ console.error("Failed to start dev server:", err);
919
+ }
920
+ }
921
+ console.log("Generating HTML...");
922
+ const html = generateHTML(graph);
923
+ const outPath = resolvePath(dirPath, options.output || "packages/visualizer/visualization.html");
924
+ writeFileSync(outPath, html, "utf8");
925
+ console.log("Visualization written to:", outPath);
926
+ if (options.open) {
927
+ const { exec } = await import("child_process");
928
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
929
+ exec(`${opener} "${outPath}"`);
930
+ }
931
+ if (options.serve) {
932
+ try {
933
+ const port = Number(options.serve) || 5173;
934
+ const http = await import("http");
935
+ const fsp = await import("fs/promises");
936
+ const { exec } = await import("child_process");
937
+ const server = http.createServer(async (req, res) => {
938
+ try {
939
+ const urlPath = req.url || "/";
940
+ if (urlPath === "/" || urlPath === "/index.html") {
941
+ const content = await fsp.readFile(outPath, "utf8");
942
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
943
+ res.end(content);
944
+ return;
945
+ }
946
+ res.writeHead(404, { "Content-Type": "text/plain" });
947
+ res.end("Not found");
948
+ } catch (e) {
949
+ res.writeHead(500, { "Content-Type": "text/plain" });
950
+ res.end("Server error");
951
+ }
952
+ });
953
+ server.listen(port, () => {
954
+ const addr = `http://localhost:${port}/`;
955
+ console.log(`Local visualization server running at ${addr}`);
956
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
957
+ exec(`${opener} "${addr}"`);
958
+ });
959
+ process.on("SIGINT", () => {
960
+ server.close();
961
+ process.exit(0);
962
+ });
963
+ } catch (err) {
964
+ console.error("Failed to start local server:", err);
965
+ }
966
+ }
967
+ } catch (err) {
968
+ handleCLIError(err, "Visualization");
969
+ }
970
+ }
971
+ program.command("visualise").description("Alias for visualize (British spelling)").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (relative to directory)", "aiready-improvement-report.json").option("-o, --output <path>", "Output HTML path (relative to directory)", "packages/visualizer/visualization.html").option("--open", "Open generated HTML in default browser").option("--serve [port]", "Start a local static server to serve the visualization (optional port number)", false).option("--dev", "Start Vite dev server (live reload) for interactive development", false).addHelpText("after", `
972
+ EXAMPLES:
973
+ $ aiready visualise . --report aiready-improvement-report.json
974
+ $ aiready visualise . --report report.json --dev
975
+ $ aiready visualise . --report report.json --serve 8080
976
+
977
+ NOTES:
978
+ - Same options as 'visualize'. Use --dev for live reload and --serve to host a static HTML.
979
+ `).action(async (directory, options) => await handleVisualize(directory, options));
980
+ program.command("visualize").description("Generate interactive visualization from an AIReady report").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (relative to directory)", "aiready-improvement-report.json").option("-o, --output <path>", "Output HTML path (relative to directory)", "packages/visualizer/visualization.html").option("--open", "Open generated HTML in default browser").option("--serve [port]", "Start a local static server to serve the visualization (optional port number)", false).option("--dev", "Start Vite dev server (live reload) for interactive development", false).addHelpText("after", `
981
+ EXAMPLES:
982
+ $ aiready visualize . --report aiready-improvement-report.json
983
+ $ aiready visualize . --report report.json -o out/visualization.html --open
984
+ $ aiready visualize . --report report.json --serve
985
+ $ aiready visualize . --report report.json --serve 8080
986
+ $ aiready visualize . --report report.json --dev
987
+
988
+ NOTES:
989
+ - The value passed to --report is interpreted relative to the directory argument (first positional).
990
+ If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
991
+ - Default output path: packages/visualizer/visualization.html (relative to the directory argument).
992
+ - --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
993
+ It serves only the generated HTML (no additional asset folders).
994
+ - Relatedness is represented by node proximity and size; explicit 'related' edges are not drawn to
995
+ reduce clutter and improve interactivity on large graphs.
996
+ - For very large graphs, consider narrowing the input with --include/--exclude or use --serve and
997
+ allow the browser a moment to stabilize after load.
998
+ `).action(async (directory, options) => await handleVisualize(directory, options));
718
999
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
4
4
  "description": "Unified CLI for AIReady analysis tools",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,10 +11,11 @@
11
11
  "dependencies": {
12
12
  "commander": "^14.0.0",
13
13
  "chalk": "^5.3.0",
14
- "@aiready/consistency": "0.8.1",
15
- "@aiready/context-analyzer": "0.9.1",
16
- "@aiready/pattern-detect": "0.11.1",
17
- "@aiready/core": "0.9.1"
14
+ "@aiready/core": "0.9.4",
15
+ "@aiready/context-analyzer": "0.9.4",
16
+ "@aiready/visualizer": "0.1.3",
17
+ "@aiready/consistency": "0.8.4",
18
+ "@aiready/pattern-detect": "0.11.4"
18
19
  },
19
20
  "devDependencies": {
20
21
  "tsup": "^8.3.5",
@@ -36,7 +37,7 @@
36
37
  "url": "https://github.com/caopengau/aiready-cli/issues"
37
38
  },
38
39
  "scripts": {
39
- "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
40
+ "build": "tsup src/index.ts src/cli.ts --format cjs,esm",
40
41
  "dev": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --watch",
41
42
  "test": "vitest run",
42
43
  "lint": "eslint src",
package/src/cli.ts CHANGED
@@ -20,7 +20,10 @@ import {
20
20
  type AIReadyConfig,
21
21
  type ToolScoringOutput,
22
22
  } from '@aiready/core';
23
- import { readFileSync } from 'fs';
23
+ import { readFileSync, existsSync } from 'fs';
24
+ import { resolve as resolvePath } from 'path';
25
+ import { GraphBuilder } from '@aiready/visualizer/graph';
26
+ import type { GraphData } from '@aiready/visualizer';
24
27
 
25
28
  const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
26
29
 
@@ -919,4 +922,318 @@ program
919
922
  return markdown;
920
923
  }
921
924
 
925
+ function generateHTML(graph: GraphData): string {
926
+ const payload = JSON.stringify(graph, null, 2);
927
+ return `<!doctype html>
928
+ <html>
929
+ <head>
930
+ <meta charset="utf-8" />
931
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
932
+ <title>AIReady Visualization</title>
933
+ <style>
934
+ html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
935
+ #container { display:flex; height:100vh }
936
+ #panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
937
+ #canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
938
+ canvas { background: #0b1220; border-radius:8px }
939
+ .stat { margin-bottom:12px }
940
+ </style>
941
+ </head>
942
+ <body>
943
+ <div id="container">
944
+ <div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
945
+ <div id="panel">
946
+ <h2>AIReady Visualization</h2>
947
+ <div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
948
+ <div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
949
+ <div class="stat"><strong>Legend</strong></div>
950
+ <div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
951
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff4d4f;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Critical</strong>: highest severity issues.</div>
952
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ff9900;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Major</strong>: important issues.</div>
953
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#ffd666;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Minor</strong>: low priority issues.</div>
954
+ <div style="margin-bottom:8px"><span style="display:inline-block;width:12px;height:12px;background:#91d5ff;margin-right:8px;border:1px solid rgba(255,255,255,0.06)"></span><strong>Info</strong>: informational notes.</div>
955
+ <div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
956
+ <div style="margin-top:6px;color:#94a3b8"><strong>Proximity</strong>: nodes that are spatially close are more contextually related; relatedness is represented by distance and size rather than explicit edges.</div>
957
+ <div style="margin-top:6px;color:#94a3b8"><strong>Edge colors</strong>: <span style="color:#fb7e81">Similarity</span>, <span style="color:#84c1ff">Dependency</span>, <span style="color:#ffa500">Reference</span>, default <span style="color:#334155">Other</span>.</div>
958
+ </div>
959
+ </div>
960
+ </div>
961
+
962
+ <script>
963
+ const graphData = ${payload};
964
+ document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
965
+ document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
966
+
967
+ const canvas = document.getElementById('canvas');
968
+ const ctx = canvas.getContext('2d');
969
+
970
+ const nodes = graphData.nodes.map((n, i) => ({
971
+ ...n,
972
+ x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
973
+ y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
974
+ }));
975
+
976
+ function draw() {
977
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
978
+
979
+ graphData.edges.forEach(edge => {
980
+ const s = nodes.find(n => n.id === edge.source);
981
+ const t = nodes.find(n => n.id === edge.target);
982
+ if (!s || !t) return;
983
+ if (edge.type === 'related') return;
984
+ if (edge.type === 'similarity') {
985
+ ctx.strokeStyle = '#fb7e81';
986
+ ctx.lineWidth = 1.2;
987
+ } else if (edge.type === 'dependency') {
988
+ ctx.strokeStyle = '#84c1ff';
989
+ ctx.lineWidth = 1.0;
990
+ } else if (edge.type === 'reference') {
991
+ ctx.strokeStyle = '#ffa500';
992
+ ctx.lineWidth = 0.9;
993
+ } else {
994
+ ctx.strokeStyle = '#334155';
995
+ ctx.lineWidth = 0.8;
996
+ }
997
+ ctx.beginPath();
998
+ ctx.moveTo(s.x, s.y);
999
+ ctx.lineTo(t.x, t.y);
1000
+ ctx.stroke();
1001
+ });
1002
+
1003
+ const groups = {};
1004
+ nodes.forEach(n => {
1005
+ const g = n.group || '__default';
1006
+ if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
1007
+ groups[g].minX = Math.min(groups[g].minX, n.x);
1008
+ groups[g].minY = Math.min(groups[g].minY, n.y);
1009
+ groups[g].maxX = Math.max(groups[g].maxX, n.x);
1010
+ groups[g].maxY = Math.max(groups[g].maxY, n.y);
1011
+ });
1012
+
1013
+ const groupRelations = {};
1014
+ graphData.edges.forEach(edge => {
1015
+ const sNode = nodes.find(n => n.id === edge.source);
1016
+ const tNode = nodes.find(n => n.id === edge.target);
1017
+ if (!sNode || !tNode) return;
1018
+ const g1 = sNode.group || '__default';
1019
+ const g2 = tNode.group || '__default';
1020
+ if (g1 === g2) return;
1021
+ const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
1022
+ groupRelations[key] = (groupRelations[key] || 0) + 1;
1023
+ });
1024
+
1025
+ Object.keys(groupRelations).forEach(k => {
1026
+ const count = groupRelations[k];
1027
+ const [ga, gb] = k.split('::');
1028
+ if (!groups[ga] || !groups[gb]) return;
1029
+ const ax = (groups[ga].minX + groups[ga].maxX) / 2;
1030
+ const ay = (groups[ga].minY + groups[ga].maxY) / 2;
1031
+ const bx = (groups[gb].minX + groups[gb].maxX) / 2;
1032
+ const by = (groups[gb].minY + groups[gb].maxY) / 2;
1033
+ ctx.beginPath();
1034
+ ctx.strokeStyle = 'rgba(148,163,184,0.25)';
1035
+ ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
1036
+ ctx.moveTo(ax, ay);
1037
+ ctx.lineTo(bx, by);
1038
+ ctx.stroke();
1039
+ });
1040
+
1041
+ Object.keys(groups).forEach(g => {
1042
+ if (g === '__default') return;
1043
+ const box = groups[g];
1044
+ const pad = 16;
1045
+ const x = box.minX - pad;
1046
+ const y = box.minY - pad;
1047
+ const w = (box.maxX - box.minX) + pad * 2;
1048
+ const h = (box.maxY - box.minY) + pad * 2;
1049
+ ctx.save();
1050
+ ctx.fillStyle = 'rgba(30,64,175,0.04)';
1051
+ ctx.strokeStyle = 'rgba(30,64,175,0.12)';
1052
+ ctx.lineWidth = 1.2;
1053
+ const r = 8;
1054
+ ctx.beginPath();
1055
+ ctx.moveTo(x + r, y);
1056
+ ctx.arcTo(x + w, y, x + w, y + h, r);
1057
+ ctx.arcTo(x + w, y + h, x, y + h, r);
1058
+ ctx.arcTo(x, y + h, x, y, r);
1059
+ ctx.arcTo(x, y, x + w, y, r);
1060
+ ctx.closePath();
1061
+ ctx.fill();
1062
+ ctx.stroke();
1063
+ ctx.restore();
1064
+ ctx.fillStyle = '#94a3b8';
1065
+ ctx.font = '11px sans-serif';
1066
+ ctx.fillText(g, x + 8, y + 14);
1067
+ });
1068
+
1069
+ nodes.forEach(n => {
1070
+ const sizeVal = (n.size || n.value || 1);
1071
+ const r = 6 + (sizeVal / 2);
1072
+ ctx.beginPath();
1073
+ ctx.fillStyle = n.color || '#60a5fa';
1074
+ ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
1075
+ ctx.fill();
1076
+
1077
+ ctx.fillStyle = '#e2e8f0';
1078
+ ctx.font = '11px sans-serif';
1079
+ ctx.textAlign = 'center';
1080
+ ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
1081
+ });
1082
+ }
1083
+
1084
+ draw();
1085
+ </script>
1086
+ </body>
1087
+ </html>`;
1088
+ }
1089
+
1090
+ async function handleVisualize(directory: string | undefined, options: any) {
1091
+ try {
1092
+ const dirPath = resolvePath(process.cwd(), directory || '.');
1093
+ const reportPath = resolvePath(dirPath, options.report || 'aiready-improvement-report.json');
1094
+ if (!existsSync(reportPath)) {
1095
+ console.error('Report not found at', reportPath);
1096
+ console.log('Run `aiready scan` to generate the report, or pass --report');
1097
+ return;
1098
+ }
1099
+
1100
+ const raw = readFileSync(reportPath, 'utf8');
1101
+ const report = JSON.parse(raw);
1102
+
1103
+ console.log("Building graph from report...");
1104
+ const { GraphBuilder } = await import('@aiready/visualizer/graph');
1105
+ const graph = GraphBuilder.buildFromReport(report, dirPath);
1106
+
1107
+ if (options.dev) {
1108
+ try {
1109
+ const { spawn } = await import("child_process");
1110
+ const webDir = resolvePath(dirPath, 'packages/visualizer');
1111
+ const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
1112
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
1113
+ const onExit = () => {
1114
+ try {
1115
+ watcher.kill();
1116
+ } catch (e) {}
1117
+ try {
1118
+ vite.kill();
1119
+ } catch (e) {}
1120
+ process.exit(0);
1121
+ };
1122
+ process.on("SIGINT", onExit);
1123
+ process.on("SIGTERM", onExit);
1124
+ return;
1125
+ } catch (err) {
1126
+ console.error("Failed to start dev server:", err);
1127
+ }
1128
+ }
1129
+
1130
+ console.log("Generating HTML...");
1131
+ const html = generateHTML(graph);
1132
+ const outPath = resolvePath(dirPath, options.output || 'packages/visualizer/visualization.html');
1133
+ writeFileSync(outPath, html, 'utf8');
1134
+ console.log("Visualization written to:", outPath);
1135
+
1136
+
1137
+ if (options.open) {
1138
+ const { exec } = await import('child_process');
1139
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1140
+ exec(`${opener} "${outPath}"`);
1141
+ }
1142
+
1143
+ if (options.serve) {
1144
+ try {
1145
+ const port = Number(options.serve) || 5173;
1146
+ const http = await import('http');
1147
+ const fsp = await import('fs/promises');
1148
+ const { exec } = await import('child_process');
1149
+
1150
+ const server = http.createServer(async (req, res) => {
1151
+ try {
1152
+ const urlPath = req.url || '/';
1153
+ if (urlPath === '/' || urlPath === '/index.html') {
1154
+ const content = await fsp.readFile(outPath, 'utf8');
1155
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1156
+ res.end(content);
1157
+ return;
1158
+ }
1159
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1160
+ res.end('Not found');
1161
+ } catch (e: any) {
1162
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
1163
+ res.end('Server error');
1164
+ }
1165
+ });
1166
+
1167
+ server.listen(port, () => {
1168
+ const addr = `http://localhost:${port}/`;
1169
+ console.log(`Local visualization server running at ${addr}`);
1170
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1171
+ exec(`${opener} "${addr}"`);
1172
+ });
1173
+
1174
+ process.on('SIGINT', () => {
1175
+ server.close();
1176
+ process.exit(0);
1177
+ });
1178
+ } catch (err) {
1179
+ console.error('Failed to start local server:', err);
1180
+ }
1181
+ }
1182
+
1183
+ } catch (err: any) {
1184
+ handleCLIError(err, 'Visualization');
1185
+ }
1186
+ }
1187
+
1188
+ // Visualize command: build interactive HTML from an AIReady report
1189
+ program
1190
+ .command('visualise')
1191
+ .description('Alias for visualize (British spelling)')
1192
+ .argument('[directory]', 'Directory to analyze', '.')
1193
+ .option('--report <path>', 'Report path (relative to directory)', 'aiready-improvement-report.json')
1194
+ .option('-o, --output <path>', 'Output HTML path (relative to directory)', 'packages/visualizer/visualization.html')
1195
+ .option('--open', 'Open generated HTML in default browser')
1196
+ .option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
1197
+ .option('--dev', 'Start Vite dev server (live reload) for interactive development', false)
1198
+ .addHelpText('after', `
1199
+ EXAMPLES:
1200
+ $ aiready visualise . --report aiready-improvement-report.json
1201
+ $ aiready visualise . --report report.json --dev
1202
+ $ aiready visualise . --report report.json --serve 8080
1203
+
1204
+ NOTES:
1205
+ - Same options as \'visualize\'. Use --dev for live reload and --serve to host a static HTML.
1206
+ `)
1207
+ .action(async (directory, options) => await handleVisualize(directory, options));
1208
+
1209
+ program
1210
+ .command('visualize')
1211
+ .description('Generate interactive visualization from an AIReady report')
1212
+ .argument('[directory]', 'Directory to analyze', '.')
1213
+ .option('--report <path>', 'Report path (relative to directory)', 'aiready-improvement-report.json')
1214
+ .option('-o, --output <path>', 'Output HTML path (relative to directory)', 'packages/visualizer/visualization.html')
1215
+ .option('--open', 'Open generated HTML in default browser')
1216
+ .option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
1217
+ .option('--dev', 'Start Vite dev server (live reload) for interactive development', false)
1218
+ .addHelpText('after', `
1219
+ EXAMPLES:
1220
+ $ aiready visualize . --report aiready-improvement-report.json
1221
+ $ aiready visualize . --report report.json -o out/visualization.html --open
1222
+ $ aiready visualize . --report report.json --serve
1223
+ $ aiready visualize . --report report.json --serve 8080
1224
+ $ aiready visualize . --report report.json --dev
1225
+
1226
+ NOTES:
1227
+ - The value passed to --report is interpreted relative to the directory argument (first positional).
1228
+ If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
1229
+ - Default output path: packages/visualizer/visualization.html (relative to the directory argument).
1230
+ - --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
1231
+ It serves only the generated HTML (no additional asset folders).
1232
+ - Relatedness is represented by node proximity and size; explicit 'related' edges are not drawn to
1233
+ reduce clutter and improve interactivity on large graphs.
1234
+ - For very large graphs, consider narrowing the input with --include/--exclude or use --serve and
1235
+ allow the browser a moment to stabilize after load.
1236
+ `)
1237
+ .action(async (directory, options) => await handleVisualize(directory, options));
1238
+
922
1239
  program.parse();