@aiready/cli 0.9.15 → 0.9.17

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,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/cli@0.9.15 build /Users/pengcao/projects/aiready/packages/cli
3
+ > @aiready/cli@0.9.17 build /Users/pengcao/projects/aiready/packages/cli
4
4
  > tsup src/index.ts src/cli.ts --format cjs,esm
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,10 +9,10 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- CJS dist/cli.js 58.90 KB
13
12
  CJS dist/index.js 4.93 KB
14
- CJS ⚡️ Build success in 18ms
13
+ CJS dist/cli.js 62.23 KB
14
+ CJS ⚡️ Build success in 17ms
15
+ ESM dist/cli.mjs 54.76 KB
15
16
  ESM dist/index.mjs 138.00 B
16
17
  ESM dist/chunk-5GZDRZ3T.mjs 4.17 KB
17
- ESM dist/cli.mjs 51.71 KB
18
- ESM ⚡️ Build success in 18ms
18
+ ESM ⚡️ Build success in 17ms
@@ -1,17 +1,17 @@
1
1
 
2
2
  
3
- > @aiready/cli@0.9.15 test /Users/pengcao/projects/aiready/packages/cli
3
+ > @aiready/cli@0.9.17 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
- ✓ dist/__tests__/cli.test.js (3 tests) 3ms
10
- ✓ src/__tests__/cli.test.ts (3 tests) 7ms
9
+ ✓ dist/__tests__/cli.test.js (3 tests) 4ms
10
+ ✓ src/__tests__/cli.test.ts (3 tests) 6ms
11
11
 
12
12
   Test Files  2 passed (2)
13
13
   Tests  6 passed (6)
14
-  Start at  23:54:01
15
-  Duration  1.52s (transform 330ms, setup 0ms, import 1.30s, tests 9ms, environment 55ms)
14
+  Start at  12:09:08
15
+  Duration  2.39s (transform 599ms, setup 0ms, import 3.19s, tests 10ms, environment 0ms)
16
16
 
17
17
  [?25h
package/dist/cli.js CHANGED
@@ -166,6 +166,7 @@ EXAMPLES:
166
166
  `).action(async (directory, options) => {
167
167
  console.log(import_chalk.default.blue("\u{1F680} Starting AIReady unified analysis...\n"));
168
168
  const startTime = Date.now();
169
+ const resolvedDir = (0, import_path2.resolve)(process.cwd(), directory || ".");
169
170
  try {
170
171
  const defaults = {
171
172
  tools: ["patterns", "context", "consistency"],
@@ -176,7 +177,7 @@ EXAMPLES:
176
177
  file: void 0
177
178
  }
178
179
  };
179
- const baseOptions = await (0, import_core.loadMergedConfig)(directory, defaults, {
180
+ const baseOptions = await (0, import_core.loadMergedConfig)(resolvedDir, defaults, {
180
181
  tools: options.tools ? options.tools.split(",").map((t) => t.trim()) : void 0,
181
182
  include: options.include?.split(","),
182
183
  exclude: options.exclude?.split(",")
@@ -184,7 +185,7 @@ EXAMPLES:
184
185
  let finalOptions = { ...baseOptions };
185
186
  if (baseOptions.tools.includes("patterns")) {
186
187
  const { getSmartDefaults } = await import("@aiready/pattern-detect");
187
- const patternSmartDefaults = await getSmartDefaults(directory, baseOptions);
188
+ const patternSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
188
189
  finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
189
190
  }
190
191
  console.log(import_chalk.default.cyan("\n=== AIReady Run Preview ==="));
@@ -381,17 +382,17 @@ EXAMPLES:
381
382
  console.log();
382
383
  }
383
384
  }
384
- const outputFormat = options.output || finalOptions.output?.format || "console";
385
- const userOutputFile = options.outputFile || finalOptions.output?.file;
386
- if (outputFormat === "json") {
387
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
388
- const defaultFilename = `aiready-scan-${dateStr}.json`;
389
- const outputPath = (0, import_core.resolveOutputPath)(userOutputFile, defaultFilename, directory);
390
- const outputData = { ...results, scoring: scoringResult };
391
- (0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Summary saved to ${outputPath}`);
392
- }
393
385
  }
394
386
  }
387
+ const outputFormat = options.output || finalOptions.output?.format || "console";
388
+ const userOutputFile = options.outputFile || finalOptions.output?.file;
389
+ if (outputFormat === "json") {
390
+ const timestamp = getReportTimestamp();
391
+ const defaultFilename = `aiready-report-${timestamp}.json`;
392
+ const outputPath = (0, import_core.resolveOutputPath)(userOutputFile, defaultFilename, resolvedDir);
393
+ const outputData = { ...results, scoring: scoringResult };
394
+ (0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Report saved to ${outputPath}`);
395
+ }
395
396
  } catch (error) {
396
397
  (0, import_core.handleCLIError)(error, "Analysis");
397
398
  }
@@ -404,6 +405,7 @@ EXAMPLES:
404
405
  `).action(async (directory, options) => {
405
406
  console.log(import_chalk.default.blue("\u{1F50D} Analyzing patterns...\n"));
406
407
  const startTime = Date.now();
408
+ const resolvedDir = (0, import_path2.resolve)(process.cwd(), directory || ".");
407
409
  try {
408
410
  const useSmartDefaults = !options.fullScan;
409
411
  const defaults = {
@@ -432,7 +434,7 @@ EXAMPLES:
432
434
  if (options.minSharedTokens) {
433
435
  cliOptions.minSharedTokens = parseInt(options.minSharedTokens);
434
436
  }
435
- const finalOptions = await (0, import_core.loadMergedConfig)(directory, defaults, cliOptions);
437
+ const finalOptions = await (0, import_core.loadMergedConfig)(resolvedDir, defaults, cliOptions);
436
438
  const { analyzePatterns: analyzePatterns2, generateSummary, calculatePatternScore } = await import("@aiready/pattern-detect");
437
439
  const { results, duplicates } = await analyzePatterns2(finalOptions);
438
440
  const elapsedTime = (0, import_core.getElapsedTime)(startTime);
@@ -451,8 +453,8 @@ EXAMPLES:
451
453
  };
452
454
  const outputPath = (0, import_core.resolveOutputPath)(
453
455
  userOutputFile,
454
- `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
455
- directory
456
+ `aiready-report-${getReportTimestamp()}.json`,
457
+ resolvedDir
456
458
  );
457
459
  (0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
458
460
  } else {
@@ -508,6 +510,7 @@ EXAMPLES:
508
510
  program.command("context").description("Analyze context window costs and dependency fragmentation").argument("[directory]", "Directory to analyze", ".").option("--max-depth <number>", "Maximum acceptable import depth", "5").option("--max-context <number>", "Maximum acceptable context budget (tokens)", "10000").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option("--score", "Calculate and display AI Readiness Score for context (0-100)").action(async (directory, options) => {
509
511
  console.log(import_chalk.default.blue("\u{1F9E0} Analyzing context costs...\n"));
510
512
  const startTime = Date.now();
513
+ const resolvedDir = (0, import_path2.resolve)(process.cwd(), directory || ".");
511
514
  try {
512
515
  const defaults = {
513
516
  maxDepth: 5,
@@ -519,7 +522,7 @@ program.command("context").description("Analyze context window costs and depende
519
522
  file: void 0
520
523
  }
521
524
  };
522
- let baseOptions = await (0, import_core.loadMergedConfig)(directory, defaults, {
525
+ let baseOptions = await (0, import_core.loadMergedConfig)(resolvedDir, defaults, {
523
526
  maxDepth: options.maxDepth ? parseInt(options.maxDepth) : void 0,
524
527
  maxContextBudget: options.maxContext ? parseInt(options.maxContext) : void 0,
525
528
  include: options.include?.split(","),
@@ -527,7 +530,7 @@ program.command("context").description("Analyze context window costs and depende
527
530
  });
528
531
  let finalOptions = { ...baseOptions };
529
532
  const { getSmartDefaults } = await import("@aiready/context-analyzer");
530
- const contextSmartDefaults = await getSmartDefaults(directory, baseOptions);
533
+ const contextSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
531
534
  finalOptions = { ...contextSmartDefaults, ...finalOptions };
532
535
  console.log("\u{1F4CB} Configuration:");
533
536
  console.log(` Max depth: ${finalOptions.maxDepth}`);
@@ -554,8 +557,8 @@ program.command("context").description("Analyze context window costs and depende
554
557
  };
555
558
  const outputPath = (0, import_core.resolveOutputPath)(
556
559
  userOutputFile,
557
- `context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
558
- directory
560
+ `aiready-report-${getReportTimestamp()}.json`,
561
+ resolvedDir
559
562
  );
560
563
  (0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
561
564
  } else {
@@ -645,6 +648,7 @@ program.command("context").description("Analyze context window costs and depende
645
648
  program.command("consistency").description("Check naming conventions and architectural consistency").argument("[directory]", "Directory to analyze", ".").option("--naming", "Check naming conventions (default: true)").option("--no-naming", "Skip naming analysis").option("--patterns", "Check code patterns (default: true)").option("--no-patterns", "Skip pattern analysis").option("--min-severity <level>", "Minimum severity: info|minor|major|critical", "info").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json, markdown", "console").option("--output-file <path>", "Output file path (for json/markdown)").option("--score", "Calculate and display AI Readiness Score for consistency (0-100)").action(async (directory, options) => {
646
649
  console.log(import_chalk.default.blue("\u{1F50D} Analyzing consistency...\n"));
647
650
  const startTime = Date.now();
651
+ const resolvedDir = (0, import_path2.resolve)(process.cwd(), directory || ".");
648
652
  try {
649
653
  const defaults = {
650
654
  checkNaming: true,
@@ -657,7 +661,7 @@ program.command("consistency").description("Check naming conventions and archite
657
661
  file: void 0
658
662
  }
659
663
  };
660
- const finalOptions = await (0, import_core.loadMergedConfig)(directory, defaults, {
664
+ const finalOptions = await (0, import_core.loadMergedConfig)(resolvedDir, defaults, {
661
665
  checkNaming: options.naming !== false,
662
666
  checkPatterns: options.patterns !== false,
663
667
  minSeverity: options.minSeverity,
@@ -685,16 +689,16 @@ program.command("consistency").description("Check naming conventions and archite
685
689
  };
686
690
  const outputPath = (0, import_core.resolveOutputPath)(
687
691
  userOutputFile,
688
- `consistency-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
689
- directory
692
+ `aiready-report-${getReportTimestamp()}.json`,
693
+ resolvedDir
690
694
  );
691
695
  (0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
692
696
  } else if (outputFormat === "markdown") {
693
697
  const markdown = generateMarkdownReport(report, elapsedTime);
694
698
  const outputPath = (0, import_core.resolveOutputPath)(
695
699
  userOutputFile,
696
- `consistency-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`,
697
- directory
700
+ `aiready-report-${getReportTimestamp()}.md`,
701
+ resolvedDir
698
702
  );
699
703
  (0, import_fs.writeFileSync)(outputPath, markdown);
700
704
  console.log(import_chalk.default.green(`\u2705 Report saved to ${outputPath}`));
@@ -976,13 +980,21 @@ function generateHTML(graph) {
976
980
  </body>
977
981
  </html>`;
978
982
  }
983
+ function getReportTimestamp() {
984
+ const now = /* @__PURE__ */ new Date();
985
+ const pad = (n) => String(n).padStart(2, "0");
986
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
987
+ }
979
988
  function findLatestScanReport(dirPath) {
980
989
  const aireadyDir = (0, import_path2.resolve)(dirPath, ".aiready");
981
990
  if (!(0, import_fs2.existsSync)(aireadyDir)) {
982
991
  return null;
983
992
  }
984
993
  const { readdirSync, statSync } = require("fs");
985
- const files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-scan-") && f.endsWith(".json"));
994
+ let files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-report-") && f.endsWith(".json"));
995
+ if (files.length === 0) {
996
+ files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-scan-") && f.endsWith(".json"));
997
+ }
986
998
  if (files.length === 0) {
987
999
  return null;
988
1000
  }
@@ -992,15 +1004,20 @@ function findLatestScanReport(dirPath) {
992
1004
  async function handleVisualize(directory, options) {
993
1005
  try {
994
1006
  const dirPath = (0, import_path2.resolve)(process.cwd(), directory || ".");
995
- let reportPath = (0, import_path2.resolve)(dirPath, options.report || "aiready-improvement-report.json");
996
- if (!(0, import_fs2.existsSync)(reportPath)) {
1007
+ let reportPath = options.report ? (0, import_path2.resolve)(dirPath, options.report) : null;
1008
+ if (!reportPath || !(0, import_fs2.existsSync)(reportPath)) {
997
1009
  const latestScan = findLatestScanReport(dirPath);
998
1010
  if (latestScan) {
999
- console.log(`Using latest scan report: ${latestScan}`);
1000
1011
  reportPath = latestScan;
1012
+ console.log(import_chalk.default.dim(`Found latest report: ${latestScan.split("/").pop()}`));
1001
1013
  } else {
1002
- console.error("Report not found at", reportPath);
1003
- console.log("Run `aiready scan --output json` to generate the report, or pass --report");
1014
+ console.error(import_chalk.default.red("\u274C No AI readiness report found"));
1015
+ console.log(import_chalk.default.dim(`
1016
+ Generate a report with:
1017
+ aiready scan --output json
1018
+
1019
+ Or specify a custom report:
1020
+ aiready visualise --report <path-to-report.json>`));
1004
1021
  return;
1005
1022
  }
1006
1023
  }
@@ -1012,12 +1029,68 @@ async function handleVisualize(directory, options) {
1012
1029
  if (options.dev) {
1013
1030
  try {
1014
1031
  const { spawn } = await import("child_process");
1015
- const webDir = (0, import_path2.resolve)(dirPath, "packages/visualizer");
1016
- const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
1017
- const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
1032
+ const monorepoWebDir = (0, import_path2.resolve)(dirPath, "packages/visualizer");
1033
+ let webDir = "";
1034
+ let visualizerAvailable = false;
1035
+ if ((0, import_fs2.existsSync)(monorepoWebDir)) {
1036
+ webDir = monorepoWebDir;
1037
+ visualizerAvailable = true;
1038
+ } else {
1039
+ const nodemodulesLocations = [
1040
+ (0, import_path2.resolve)(dirPath, "node_modules", "@aiready", "visualizer"),
1041
+ (0, import_path2.resolve)(process.cwd(), "node_modules", "@aiready", "visualizer")
1042
+ ];
1043
+ let currentDir = dirPath;
1044
+ while (currentDir !== "/" && currentDir !== ".") {
1045
+ nodemodulesLocations.push((0, import_path2.resolve)(currentDir, "node_modules", "@aiready", "visualizer"));
1046
+ const parent = (0, import_path2.resolve)(currentDir, "..");
1047
+ if (parent === currentDir) break;
1048
+ currentDir = parent;
1049
+ }
1050
+ for (const location of nodemodulesLocations) {
1051
+ if ((0, import_fs2.existsSync)(location) && (0, import_fs2.existsSync)((0, import_path2.resolve)(location, "package.json"))) {
1052
+ webDir = location;
1053
+ visualizerAvailable = true;
1054
+ break;
1055
+ }
1056
+ }
1057
+ if (!visualizerAvailable) {
1058
+ try {
1059
+ const vizPkgPath = require.resolve("@aiready/visualizer/package.json");
1060
+ webDir = (0, import_path2.resolve)(vizPkgPath, "..");
1061
+ visualizerAvailable = true;
1062
+ } catch (e) {
1063
+ }
1064
+ }
1065
+ }
1066
+ const spawnCwd = webDir || process.cwd();
1067
+ const nodeBinCandidate = process.execPath;
1068
+ const nodeBin = (0, import_fs2.existsSync)(nodeBinCandidate) ? nodeBinCandidate : "node";
1069
+ if (!visualizerAvailable) {
1070
+ console.error(import_chalk.default.red("\u274C Cannot start dev server: @aiready/visualizer not available."));
1071
+ console.log(import_chalk.default.dim("Install @aiready/visualizer in your project with:\n npm install @aiready/visualizer"));
1072
+ return;
1073
+ }
1074
+ const { watch } = await import("fs");
1075
+ const copyReportToViz = () => {
1076
+ try {
1077
+ const destPath = (0, import_path2.resolve)(spawnCwd, "web", "report-data.json");
1078
+ (0, import_fs2.copyFileSync)(reportPath, destPath);
1079
+ console.log(`\u{1F4CB} Report synced to ${destPath}`);
1080
+ } catch (e) {
1081
+ console.error("Failed to sync report:", e);
1082
+ }
1083
+ };
1084
+ copyReportToViz();
1085
+ let watchTimeout = null;
1086
+ const reportWatcher = watch(reportPath, () => {
1087
+ if (watchTimeout) clearTimeout(watchTimeout);
1088
+ watchTimeout = setTimeout(copyReportToViz, 100);
1089
+ });
1090
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true });
1018
1091
  const onExit = () => {
1019
1092
  try {
1020
- watcher.kill();
1093
+ reportWatcher.close();
1021
1094
  } catch (e) {
1022
1095
  }
1023
1096
  try {
@@ -1083,18 +1156,20 @@ async function handleVisualize(directory, options) {
1083
1156
  (0, import_core.handleCLIError)(err, "Visualization");
1084
1157
  }
1085
1158
  }
1086
- 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", true).addHelpText("after", `
1159
+ program.command("visualise").description("Alias for visualize (British spelling)").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)").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", true).addHelpText("after", `
1087
1160
  EXAMPLES:
1088
- $ aiready visualise . --report aiready-improvement-report.json
1161
+ $ aiready visualise . # Auto-detects latest report
1162
+ $ aiready visualise . --report .aiready/aiready-report-20260217-143022.json
1089
1163
  $ aiready visualise . --report report.json --dev
1090
1164
  $ aiready visualise . --report report.json --serve 8080
1091
1165
 
1092
1166
  NOTES:
1093
1167
  - Same options as 'visualize'. Use --dev for live reload and --serve to host a static HTML.
1094
1168
  `).action(async (directory, options) => await handleVisualize(directory, options));
1095
- 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", `
1169
+ program.command("visualize").description("Generate interactive visualization from an AIReady report").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)").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", `
1096
1170
  EXAMPLES:
1097
- $ aiready visualize . --report aiready-improvement-report.json
1171
+ $ aiready visualize . # Auto-detects latest report
1172
+ $ aiready visualize . --report .aiready/aiready-report-20260217-143022.json
1098
1173
  $ aiready visualize . --report report.json -o out/visualization.html --open
1099
1174
  $ aiready visualize . --report report.json --serve
1100
1175
  $ aiready visualize . --report report.json --serve 8080
package/dist/cli.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  getRatingDisplay,
23
23
  parseWeightString
24
24
  } from "@aiready/core";
25
- import { readFileSync, existsSync } from "fs";
25
+ import { readFileSync, existsSync, copyFileSync } from "fs";
26
26
  import { resolve as resolvePath } from "path";
27
27
  var packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
28
28
  var program = new Command();
@@ -71,6 +71,7 @@ EXAMPLES:
71
71
  `).action(async (directory, options) => {
72
72
  console.log(chalk.blue("\u{1F680} Starting AIReady unified analysis...\n"));
73
73
  const startTime = Date.now();
74
+ const resolvedDir = resolvePath(process.cwd(), directory || ".");
74
75
  try {
75
76
  const defaults = {
76
77
  tools: ["patterns", "context", "consistency"],
@@ -81,7 +82,7 @@ EXAMPLES:
81
82
  file: void 0
82
83
  }
83
84
  };
84
- const baseOptions = await loadMergedConfig(directory, defaults, {
85
+ const baseOptions = await loadMergedConfig(resolvedDir, defaults, {
85
86
  tools: options.tools ? options.tools.split(",").map((t) => t.trim()) : void 0,
86
87
  include: options.include?.split(","),
87
88
  exclude: options.exclude?.split(",")
@@ -89,7 +90,7 @@ EXAMPLES:
89
90
  let finalOptions = { ...baseOptions };
90
91
  if (baseOptions.tools.includes("patterns")) {
91
92
  const { getSmartDefaults } = await import("@aiready/pattern-detect");
92
- const patternSmartDefaults = await getSmartDefaults(directory, baseOptions);
93
+ const patternSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
93
94
  finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
94
95
  }
95
96
  console.log(chalk.cyan("\n=== AIReady Run Preview ==="));
@@ -286,17 +287,17 @@ EXAMPLES:
286
287
  console.log();
287
288
  }
288
289
  }
289
- const outputFormat = options.output || finalOptions.output?.format || "console";
290
- const userOutputFile = options.outputFile || finalOptions.output?.file;
291
- if (outputFormat === "json") {
292
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
293
- const defaultFilename = `aiready-scan-${dateStr}.json`;
294
- const outputPath = resolveOutputPath(userOutputFile, defaultFilename, directory);
295
- const outputData = { ...results, scoring: scoringResult };
296
- handleJSONOutput(outputData, outputPath, `\u2705 Summary saved to ${outputPath}`);
297
- }
298
290
  }
299
291
  }
292
+ const outputFormat = options.output || finalOptions.output?.format || "console";
293
+ const userOutputFile = options.outputFile || finalOptions.output?.file;
294
+ if (outputFormat === "json") {
295
+ const timestamp = getReportTimestamp();
296
+ const defaultFilename = `aiready-report-${timestamp}.json`;
297
+ const outputPath = resolveOutputPath(userOutputFile, defaultFilename, resolvedDir);
298
+ const outputData = { ...results, scoring: scoringResult };
299
+ handleJSONOutput(outputData, outputPath, `\u2705 Report saved to ${outputPath}`);
300
+ }
300
301
  } catch (error) {
301
302
  handleCLIError(error, "Analysis");
302
303
  }
@@ -309,6 +310,7 @@ EXAMPLES:
309
310
  `).action(async (directory, options) => {
310
311
  console.log(chalk.blue("\u{1F50D} Analyzing patterns...\n"));
311
312
  const startTime = Date.now();
313
+ const resolvedDir = resolvePath(process.cwd(), directory || ".");
312
314
  try {
313
315
  const useSmartDefaults = !options.fullScan;
314
316
  const defaults = {
@@ -337,7 +339,7 @@ EXAMPLES:
337
339
  if (options.minSharedTokens) {
338
340
  cliOptions.minSharedTokens = parseInt(options.minSharedTokens);
339
341
  }
340
- const finalOptions = await loadMergedConfig(directory, defaults, cliOptions);
342
+ const finalOptions = await loadMergedConfig(resolvedDir, defaults, cliOptions);
341
343
  const { analyzePatterns, generateSummary, calculatePatternScore } = await import("@aiready/pattern-detect");
342
344
  const { results, duplicates } = await analyzePatterns(finalOptions);
343
345
  const elapsedTime = getElapsedTime(startTime);
@@ -356,8 +358,8 @@ EXAMPLES:
356
358
  };
357
359
  const outputPath = resolveOutputPath(
358
360
  userOutputFile,
359
- `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
360
- directory
361
+ `aiready-report-${getReportTimestamp()}.json`,
362
+ resolvedDir
361
363
  );
362
364
  handleJSONOutput(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
363
365
  } else {
@@ -413,6 +415,7 @@ EXAMPLES:
413
415
  program.command("context").description("Analyze context window costs and dependency fragmentation").argument("[directory]", "Directory to analyze", ".").option("--max-depth <number>", "Maximum acceptable import depth", "5").option("--max-context <number>", "Maximum acceptable context budget (tokens)", "10000").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").option("--score", "Calculate and display AI Readiness Score for context (0-100)").action(async (directory, options) => {
414
416
  console.log(chalk.blue("\u{1F9E0} Analyzing context costs...\n"));
415
417
  const startTime = Date.now();
418
+ const resolvedDir = resolvePath(process.cwd(), directory || ".");
416
419
  try {
417
420
  const defaults = {
418
421
  maxDepth: 5,
@@ -424,7 +427,7 @@ program.command("context").description("Analyze context window costs and depende
424
427
  file: void 0
425
428
  }
426
429
  };
427
- let baseOptions = await loadMergedConfig(directory, defaults, {
430
+ let baseOptions = await loadMergedConfig(resolvedDir, defaults, {
428
431
  maxDepth: options.maxDepth ? parseInt(options.maxDepth) : void 0,
429
432
  maxContextBudget: options.maxContext ? parseInt(options.maxContext) : void 0,
430
433
  include: options.include?.split(","),
@@ -432,7 +435,7 @@ program.command("context").description("Analyze context window costs and depende
432
435
  });
433
436
  let finalOptions = { ...baseOptions };
434
437
  const { getSmartDefaults } = await import("@aiready/context-analyzer");
435
- const contextSmartDefaults = await getSmartDefaults(directory, baseOptions);
438
+ const contextSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
436
439
  finalOptions = { ...contextSmartDefaults, ...finalOptions };
437
440
  console.log("\u{1F4CB} Configuration:");
438
441
  console.log(` Max depth: ${finalOptions.maxDepth}`);
@@ -459,8 +462,8 @@ program.command("context").description("Analyze context window costs and depende
459
462
  };
460
463
  const outputPath = resolveOutputPath(
461
464
  userOutputFile,
462
- `context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
463
- directory
465
+ `aiready-report-${getReportTimestamp()}.json`,
466
+ resolvedDir
464
467
  );
465
468
  handleJSONOutput(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
466
469
  } else {
@@ -550,6 +553,7 @@ program.command("context").description("Analyze context window costs and depende
550
553
  program.command("consistency").description("Check naming conventions and architectural consistency").argument("[directory]", "Directory to analyze", ".").option("--naming", "Check naming conventions (default: true)").option("--no-naming", "Skip naming analysis").option("--patterns", "Check code patterns (default: true)").option("--no-patterns", "Skip pattern analysis").option("--min-severity <level>", "Minimum severity: info|minor|major|critical", "info").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json, markdown", "console").option("--output-file <path>", "Output file path (for json/markdown)").option("--score", "Calculate and display AI Readiness Score for consistency (0-100)").action(async (directory, options) => {
551
554
  console.log(chalk.blue("\u{1F50D} Analyzing consistency...\n"));
552
555
  const startTime = Date.now();
556
+ const resolvedDir = resolvePath(process.cwd(), directory || ".");
553
557
  try {
554
558
  const defaults = {
555
559
  checkNaming: true,
@@ -562,7 +566,7 @@ program.command("consistency").description("Check naming conventions and archite
562
566
  file: void 0
563
567
  }
564
568
  };
565
- const finalOptions = await loadMergedConfig(directory, defaults, {
569
+ const finalOptions = await loadMergedConfig(resolvedDir, defaults, {
566
570
  checkNaming: options.naming !== false,
567
571
  checkPatterns: options.patterns !== false,
568
572
  minSeverity: options.minSeverity,
@@ -590,16 +594,16 @@ program.command("consistency").description("Check naming conventions and archite
590
594
  };
591
595
  const outputPath = resolveOutputPath(
592
596
  userOutputFile,
593
- `consistency-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
594
- directory
597
+ `aiready-report-${getReportTimestamp()}.json`,
598
+ resolvedDir
595
599
  );
596
600
  handleJSONOutput(outputData, outputPath, `\u2705 Results saved to ${outputPath}`);
597
601
  } else if (outputFormat === "markdown") {
598
602
  const markdown = generateMarkdownReport(report, elapsedTime);
599
603
  const outputPath = resolveOutputPath(
600
604
  userOutputFile,
601
- `consistency-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`,
602
- directory
605
+ `aiready-report-${getReportTimestamp()}.md`,
606
+ resolvedDir
603
607
  );
604
608
  writeFileSync(outputPath, markdown);
605
609
  console.log(chalk.green(`\u2705 Report saved to ${outputPath}`));
@@ -881,13 +885,21 @@ function generateHTML(graph) {
881
885
  </body>
882
886
  </html>`;
883
887
  }
888
+ function getReportTimestamp() {
889
+ const now = /* @__PURE__ */ new Date();
890
+ const pad = (n) => String(n).padStart(2, "0");
891
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
892
+ }
884
893
  function findLatestScanReport(dirPath) {
885
894
  const aireadyDir = resolvePath(dirPath, ".aiready");
886
895
  if (!existsSync(aireadyDir)) {
887
896
  return null;
888
897
  }
889
898
  const { readdirSync, statSync } = __require("fs");
890
- const files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-scan-") && f.endsWith(".json"));
899
+ let files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-report-") && f.endsWith(".json"));
900
+ if (files.length === 0) {
901
+ files = readdirSync(aireadyDir).filter((f) => f.startsWith("aiready-scan-") && f.endsWith(".json"));
902
+ }
891
903
  if (files.length === 0) {
892
904
  return null;
893
905
  }
@@ -897,15 +909,20 @@ function findLatestScanReport(dirPath) {
897
909
  async function handleVisualize(directory, options) {
898
910
  try {
899
911
  const dirPath = resolvePath(process.cwd(), directory || ".");
900
- let reportPath = resolvePath(dirPath, options.report || "aiready-improvement-report.json");
901
- if (!existsSync(reportPath)) {
912
+ let reportPath = options.report ? resolvePath(dirPath, options.report) : null;
913
+ if (!reportPath || !existsSync(reportPath)) {
902
914
  const latestScan = findLatestScanReport(dirPath);
903
915
  if (latestScan) {
904
- console.log(`Using latest scan report: ${latestScan}`);
905
916
  reportPath = latestScan;
917
+ console.log(chalk.dim(`Found latest report: ${latestScan.split("/").pop()}`));
906
918
  } else {
907
- console.error("Report not found at", reportPath);
908
- console.log("Run `aiready scan --output json` to generate the report, or pass --report");
919
+ console.error(chalk.red("\u274C No AI readiness report found"));
920
+ console.log(chalk.dim(`
921
+ Generate a report with:
922
+ aiready scan --output json
923
+
924
+ Or specify a custom report:
925
+ aiready visualise --report <path-to-report.json>`));
909
926
  return;
910
927
  }
911
928
  }
@@ -917,12 +934,68 @@ async function handleVisualize(directory, options) {
917
934
  if (options.dev) {
918
935
  try {
919
936
  const { spawn } = await import("child_process");
920
- const webDir = resolvePath(dirPath, "packages/visualizer");
921
- const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
922
- const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
937
+ const monorepoWebDir = resolvePath(dirPath, "packages/visualizer");
938
+ let webDir = "";
939
+ let visualizerAvailable = false;
940
+ if (existsSync(monorepoWebDir)) {
941
+ webDir = monorepoWebDir;
942
+ visualizerAvailable = true;
943
+ } else {
944
+ const nodemodulesLocations = [
945
+ resolvePath(dirPath, "node_modules", "@aiready", "visualizer"),
946
+ resolvePath(process.cwd(), "node_modules", "@aiready", "visualizer")
947
+ ];
948
+ let currentDir = dirPath;
949
+ while (currentDir !== "/" && currentDir !== ".") {
950
+ nodemodulesLocations.push(resolvePath(currentDir, "node_modules", "@aiready", "visualizer"));
951
+ const parent = resolvePath(currentDir, "..");
952
+ if (parent === currentDir) break;
953
+ currentDir = parent;
954
+ }
955
+ for (const location of nodemodulesLocations) {
956
+ if (existsSync(location) && existsSync(resolvePath(location, "package.json"))) {
957
+ webDir = location;
958
+ visualizerAvailable = true;
959
+ break;
960
+ }
961
+ }
962
+ if (!visualizerAvailable) {
963
+ try {
964
+ const vizPkgPath = __require.resolve("@aiready/visualizer/package.json");
965
+ webDir = resolvePath(vizPkgPath, "..");
966
+ visualizerAvailable = true;
967
+ } catch (e) {
968
+ }
969
+ }
970
+ }
971
+ const spawnCwd = webDir || process.cwd();
972
+ const nodeBinCandidate = process.execPath;
973
+ const nodeBin = existsSync(nodeBinCandidate) ? nodeBinCandidate : "node";
974
+ if (!visualizerAvailable) {
975
+ console.error(chalk.red("\u274C Cannot start dev server: @aiready/visualizer not available."));
976
+ console.log(chalk.dim("Install @aiready/visualizer in your project with:\n npm install @aiready/visualizer"));
977
+ return;
978
+ }
979
+ const { watch } = await import("fs");
980
+ const copyReportToViz = () => {
981
+ try {
982
+ const destPath = resolvePath(spawnCwd, "web", "report-data.json");
983
+ copyFileSync(reportPath, destPath);
984
+ console.log(`\u{1F4CB} Report synced to ${destPath}`);
985
+ } catch (e) {
986
+ console.error("Failed to sync report:", e);
987
+ }
988
+ };
989
+ copyReportToViz();
990
+ let watchTimeout = null;
991
+ const reportWatcher = watch(reportPath, () => {
992
+ if (watchTimeout) clearTimeout(watchTimeout);
993
+ watchTimeout = setTimeout(copyReportToViz, 100);
994
+ });
995
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true });
923
996
  const onExit = () => {
924
997
  try {
925
- watcher.kill();
998
+ reportWatcher.close();
926
999
  } catch (e) {
927
1000
  }
928
1001
  try {
@@ -988,18 +1061,20 @@ async function handleVisualize(directory, options) {
988
1061
  handleCLIError(err, "Visualization");
989
1062
  }
990
1063
  }
991
- 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", true).addHelpText("after", `
1064
+ program.command("visualise").description("Alias for visualize (British spelling)").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)").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", true).addHelpText("after", `
992
1065
  EXAMPLES:
993
- $ aiready visualise . --report aiready-improvement-report.json
1066
+ $ aiready visualise . # Auto-detects latest report
1067
+ $ aiready visualise . --report .aiready/aiready-report-20260217-143022.json
994
1068
  $ aiready visualise . --report report.json --dev
995
1069
  $ aiready visualise . --report report.json --serve 8080
996
1070
 
997
1071
  NOTES:
998
1072
  - Same options as 'visualize'. Use --dev for live reload and --serve to host a static HTML.
999
1073
  `).action(async (directory, options) => await handleVisualize(directory, options));
1000
- 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", `
1074
+ program.command("visualize").description("Generate interactive visualization from an AIReady report").argument("[directory]", "Directory to analyze", ".").option("--report <path>", "Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)").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", `
1001
1075
  EXAMPLES:
1002
- $ aiready visualize . --report aiready-improvement-report.json
1076
+ $ aiready visualize . # Auto-detects latest report
1077
+ $ aiready visualize . --report .aiready/aiready-report-20260217-143022.json
1003
1078
  $ aiready visualize . --report report.json -o out/visualization.html --open
1004
1079
  $ aiready visualize . --report report.json --serve
1005
1080
  $ aiready visualize . --report report.json --serve 8080
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/cli",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "description": "Unified CLI for AIReady analysis tools",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,11 +11,11 @@
11
11
  "dependencies": {
12
12
  "commander": "^14.0.0",
13
13
  "chalk": "^5.3.0",
14
- "@aiready/visualizer": "0.1.13",
15
- "@aiready/core": "0.9.14",
16
- "@aiready/consistency": "0.8.14",
17
- "@aiready/pattern-detect": "0.11.14",
18
- "@aiready/context-analyzer": "0.9.14"
14
+ "@aiready/visualizer": "0.1.18",
15
+ "@aiready/core": "0.9.16",
16
+ "@aiready/consistency": "0.8.16",
17
+ "@aiready/context-analyzer": "0.9.16",
18
+ "@aiready/pattern-detect": "0.11.16"
19
19
  },
20
20
  "devDependencies": {
21
21
  "tsup": "^8.3.5",
package/src/cli.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  type AIReadyConfig,
21
21
  type ToolScoringOutput,
22
22
  } from '@aiready/core';
23
- import { readFileSync, existsSync } from 'fs';
23
+ import { readFileSync, existsSync, copyFileSync } from 'fs';
24
24
  import { resolve as resolvePath } from 'path';
25
25
  import { GraphBuilder } from '@aiready/visualizer/graph';
26
26
  import type { GraphData } from '@aiready/visualizer';
@@ -93,6 +93,8 @@ EXAMPLES:
93
93
  console.log(chalk.blue('🚀 Starting AIReady unified analysis...\n'));
94
94
 
95
95
  const startTime = Date.now();
96
+ // Resolve directory to absolute path to ensure .aiready/ is created in the right location
97
+ const resolvedDir = resolvePath(process.cwd(), directory || '.');
96
98
 
97
99
  try {
98
100
  // Define defaults
@@ -107,7 +109,7 @@ EXAMPLES:
107
109
  };
108
110
 
109
111
  // Load and merge config with CLI options
110
- const baseOptions = await loadMergedConfig(directory, defaults, {
112
+ const baseOptions = await loadMergedConfig(resolvedDir, defaults, {
111
113
  tools: options.tools ? options.tools.split(',').map((t: string) => t.trim()) as ('patterns' | 'context' | 'consistency')[] : undefined,
112
114
  include: options.include?.split(','),
113
115
  exclude: options.exclude?.split(','),
@@ -118,7 +120,7 @@ EXAMPLES:
118
120
  let finalOptions = { ...baseOptions };
119
121
  if (baseOptions.tools.includes('patterns')) {
120
122
  const { getSmartDefaults } = await import('@aiready/pattern-detect');
121
- const patternSmartDefaults = await getSmartDefaults(directory, baseOptions);
123
+ const patternSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
122
124
  // Merge deeply to preserve nested config
123
125
  finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
124
126
  }
@@ -367,19 +369,19 @@ EXAMPLES:
367
369
  console.log();
368
370
  }
369
371
  }
370
-
371
- // Persist JSON summary by default when output is json or when config directory present
372
- const outputFormat = options.output || finalOptions.output?.format || 'console';
373
- const userOutputFile = options.outputFile || finalOptions.output?.file;
374
- if (outputFormat === 'json') {
375
- const dateStr = new Date().toISOString().split('T')[0];
376
- const defaultFilename = `aiready-scan-${dateStr}.json`;
377
- const outputPath = resolveOutputPath(userOutputFile, defaultFilename, directory);
378
- const outputData = { ...results, scoring: scoringResult };
379
- handleJSONOutput(outputData, outputPath, `✅ Summary saved to ${outputPath}`);
380
- }
381
372
  }
382
373
  }
374
+
375
+ // Persist JSON summary when output format is json
376
+ const outputFormat = options.output || finalOptions.output?.format || 'console';
377
+ const userOutputFile = options.outputFile || finalOptions.output?.file;
378
+ if (outputFormat === 'json') {
379
+ const timestamp = getReportTimestamp();
380
+ const defaultFilename = `aiready-report-${timestamp}.json`;
381
+ const outputPath = resolveOutputPath(userOutputFile, defaultFilename, resolvedDir);
382
+ const outputData = { ...results, scoring: scoringResult };
383
+ handleJSONOutput(outputData, outputPath, `✅ Report saved to ${outputPath}`);
384
+ }
383
385
  } catch (error) {
384
386
  handleCLIError(error, 'Analysis');
385
387
  }
@@ -410,6 +412,7 @@ EXAMPLES:
410
412
  console.log(chalk.blue('🔍 Analyzing patterns...\n'));
411
413
 
412
414
  const startTime = Date.now();
415
+ const resolvedDir = resolvePath(process.cwd(), directory || '.');
413
416
 
414
417
  try {
415
418
  // Determine if smart defaults should be used
@@ -449,7 +452,7 @@ EXAMPLES:
449
452
  cliOptions.minSharedTokens = parseInt(options.minSharedTokens);
450
453
  }
451
454
 
452
- const finalOptions = await loadMergedConfig(directory, defaults, cliOptions);
455
+ const finalOptions = await loadMergedConfig(resolvedDir, defaults, cliOptions);
453
456
 
454
457
  const { analyzePatterns, generateSummary, calculatePatternScore } = await import('@aiready/pattern-detect');
455
458
 
@@ -476,8 +479,8 @@ EXAMPLES:
476
479
 
477
480
  const outputPath = resolveOutputPath(
478
481
  userOutputFile,
479
- `pattern-report-${new Date().toISOString().split('T')[0]}.json`,
480
- directory
482
+ `aiready-report-${getReportTimestamp()}.json`,
483
+ resolvedDir
481
484
  );
482
485
 
483
486
  handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
@@ -563,6 +566,7 @@ program
563
566
  console.log(chalk.blue('🧠 Analyzing context costs...\n'));
564
567
 
565
568
  const startTime = Date.now();
569
+ const resolvedDir = resolvePath(process.cwd(), directory || '.');
566
570
 
567
571
  try {
568
572
  // Define defaults
@@ -578,7 +582,7 @@ program
578
582
  };
579
583
 
580
584
  // Load and merge config with CLI options
581
- let baseOptions = await loadMergedConfig(directory, defaults, {
585
+ let baseOptions = await loadMergedConfig(resolvedDir, defaults, {
582
586
  maxDepth: options.maxDepth ? parseInt(options.maxDepth) : undefined,
583
587
  maxContextBudget: options.maxContext ? parseInt(options.maxContext) : undefined,
584
588
  include: options.include?.split(','),
@@ -588,7 +592,7 @@ program
588
592
  // Apply smart defaults for context analysis (always for individual context command)
589
593
  let finalOptions: any = { ...baseOptions };
590
594
  const { getSmartDefaults } = await import('@aiready/context-analyzer');
591
- const contextSmartDefaults = await getSmartDefaults(directory, baseOptions);
595
+ const contextSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
592
596
  finalOptions = { ...contextSmartDefaults, ...finalOptions };
593
597
 
594
598
  // Display configuration
@@ -625,8 +629,8 @@ program
625
629
 
626
630
  const outputPath = resolveOutputPath(
627
631
  userOutputFile,
628
- `context-report-${new Date().toISOString().split('T')[0]}.json`,
629
- directory
632
+ `aiready-report-${getReportTimestamp()}.json`,
633
+ resolvedDir
630
634
  );
631
635
 
632
636
  handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
@@ -742,6 +746,7 @@ program
742
746
  console.log(chalk.blue('🔍 Analyzing consistency...\n'));
743
747
 
744
748
  const startTime = Date.now();
749
+ const resolvedDir = resolvePath(process.cwd(), directory || '.');
745
750
 
746
751
  try {
747
752
  // Define defaults
@@ -758,7 +763,7 @@ program
758
763
  };
759
764
 
760
765
  // Load and merge config with CLI options
761
- const finalOptions = await loadMergedConfig(directory, defaults, {
766
+ const finalOptions = await loadMergedConfig(resolvedDir, defaults, {
762
767
  checkNaming: options.naming !== false,
763
768
  checkPatterns: options.patterns !== false,
764
769
  minSeverity: options.minSeverity,
@@ -794,8 +799,8 @@ program
794
799
 
795
800
  const outputPath = resolveOutputPath(
796
801
  userOutputFile,
797
- `consistency-report-${new Date().toISOString().split('T')[0]}.json`,
798
- directory
802
+ `aiready-report-${getReportTimestamp()}.json`,
803
+ resolvedDir
799
804
  );
800
805
 
801
806
  handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
@@ -804,8 +809,8 @@ program
804
809
  const markdown = generateMarkdownReport(report, elapsedTime);
805
810
  const outputPath = resolveOutputPath(
806
811
  userOutputFile,
807
- `consistency-report-${new Date().toISOString().split('T')[0]}.md`,
808
- directory
812
+ `aiready-report-${getReportTimestamp()}.md`,
813
+ resolvedDir
809
814
  );
810
815
  writeFileSync(outputPath, markdown);
811
816
  console.log(chalk.green(`✅ Report saved to ${outputPath}`));
@@ -1088,7 +1093,18 @@ program
1088
1093
  }
1089
1094
 
1090
1095
  /**
1091
- * Find the latest aiready scan report in the .aiready directory
1096
+ * Generate timestamp for report filenames (YYYYMMDD-HHMMSS)
1097
+ * Provides better granularity than date-only filenames
1098
+ */
1099
+ function getReportTimestamp(): string {
1100
+ const now = new Date();
1101
+ const pad = (n: number) => String(n).padStart(2, '0');
1102
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
1103
+ }
1104
+
1105
+ /**
1106
+ * Find the latest aiready report in the .aiready directory
1107
+ * Searches for both new format (aiready-report-*) and legacy format (aiready-scan-*)
1092
1108
  */
1093
1109
  function findLatestScanReport(dirPath: string): string | null {
1094
1110
  const aireadyDir = resolvePath(dirPath, '.aiready');
@@ -1097,7 +1113,11 @@ program
1097
1113
  }
1098
1114
 
1099
1115
  const { readdirSync, statSync } = require('fs');
1100
- const files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-scan-') && f.endsWith('.json'));
1116
+ // Search for new format first, then legacy format
1117
+ let files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-report-') && f.endsWith('.json'));
1118
+ if (files.length === 0) {
1119
+ files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-scan-') && f.endsWith('.json'));
1120
+ }
1101
1121
 
1102
1122
  if (files.length === 0) {
1103
1123
  return null;
@@ -1114,17 +1134,17 @@ program
1114
1134
  async function handleVisualize(directory: string | undefined, options: any) {
1115
1135
  try {
1116
1136
  const dirPath = resolvePath(process.cwd(), directory || '.');
1117
- let reportPath = resolvePath(dirPath, options.report || 'aiready-improvement-report.json');
1137
+ let reportPath = options.report ? resolvePath(dirPath, options.report) : null;
1118
1138
 
1119
- // If report not found, try to find latest scan report
1120
- if (!existsSync(reportPath)) {
1139
+ // If report not provided or not found, try to find latest scan report
1140
+ if (!reportPath || !existsSync(reportPath)) {
1121
1141
  const latestScan = findLatestScanReport(dirPath);
1122
1142
  if (latestScan) {
1123
- console.log(`Using latest scan report: ${latestScan}`);
1124
1143
  reportPath = latestScan;
1144
+ console.log(chalk.dim(`Found latest report: ${latestScan.split('/').pop()}`));
1125
1145
  } else {
1126
- console.error('Report not found at', reportPath);
1127
- console.log('Run `aiready scan --output json` to generate the report, or pass --report');
1146
+ console.error(chalk.red(' No AI readiness report found'));
1147
+ console.log(chalk.dim(`\nGenerate a report with:\n aiready scan --output json\n\nOr specify a custom report:\n aiready visualise --report <path-to-report.json>`));
1128
1148
  return;
1129
1149
  }
1130
1150
  }
@@ -1139,12 +1159,84 @@ program
1139
1159
  if (options.dev) {
1140
1160
  try {
1141
1161
  const { spawn } = await import("child_process");
1142
- const webDir = resolvePath(dirPath, 'packages/visualizer');
1143
- const watcher = spawn("node", ["scripts/watch-report.js", reportPath], { cwd: webDir, stdio: "inherit" });
1144
- const vite = spawn("pnpm", ["run", "dev:web"], { cwd: webDir, stdio: "inherit", shell: true });
1162
+ const monorepoWebDir = resolvePath(dirPath, 'packages/visualizer');
1163
+ let webDir = '';
1164
+ let visualizerAvailable = false;
1165
+ if (existsSync(monorepoWebDir)) {
1166
+ webDir = monorepoWebDir;
1167
+ visualizerAvailable = true;
1168
+ } else {
1169
+ // Try to resolve installed @aiready/visualizer package from node_modules
1170
+ // Check multiple locations to support pnpm, npm, yarn, etc.
1171
+ const nodemodulesLocations: string[] = [
1172
+ resolvePath(dirPath, 'node_modules', '@aiready', 'visualizer'),
1173
+ resolvePath(process.cwd(), 'node_modules', '@aiready', 'visualizer'),
1174
+ ];
1175
+
1176
+ // Walk up directory tree to find node_modules in parent directories
1177
+ let currentDir = dirPath;
1178
+ while (currentDir !== '/' && currentDir !== '.') {
1179
+ nodemodulesLocations.push(resolvePath(currentDir, 'node_modules', '@aiready', 'visualizer'));
1180
+ const parent = resolvePath(currentDir, '..');
1181
+ if (parent === currentDir) break; // Reached filesystem root
1182
+ currentDir = parent;
1183
+ }
1184
+
1185
+ for (const location of nodemodulesLocations) {
1186
+ if (existsSync(location) && existsSync(resolvePath(location, 'package.json'))) {
1187
+ webDir = location;
1188
+ visualizerAvailable = true;
1189
+ break;
1190
+ }
1191
+ }
1192
+
1193
+ // Fallback: try require.resolve
1194
+ if (!visualizerAvailable) {
1195
+ try {
1196
+ const vizPkgPath = require.resolve('@aiready/visualizer/package.json');
1197
+ webDir = resolvePath(vizPkgPath, '..');
1198
+ visualizerAvailable = true;
1199
+ } catch (e) {
1200
+ // Visualizer not found
1201
+ }
1202
+ }
1203
+ }
1204
+ const spawnCwd = webDir || process.cwd();
1205
+ const nodeBinCandidate = process.execPath;
1206
+ const nodeBin = existsSync(nodeBinCandidate) ? nodeBinCandidate : 'node';
1207
+ if (!visualizerAvailable) {
1208
+ console.error(chalk.red('❌ Cannot start dev server: @aiready/visualizer not available.'));
1209
+ console.log(chalk.dim('Install @aiready/visualizer in your project with:\n npm install @aiready/visualizer'));
1210
+ return;
1211
+ }
1212
+
1213
+ // Inline report watcher: copy report to web/report-data.json and watch for changes
1214
+ const { watch } = await import('fs');
1215
+ const copyReportToViz = () => {
1216
+ try {
1217
+ const destPath = resolvePath(spawnCwd, 'web', 'report-data.json');
1218
+ copyFileSync(reportPath, destPath);
1219
+ console.log(`📋 Report synced to ${destPath}`);
1220
+ } catch (e) {
1221
+ console.error('Failed to sync report:', e);
1222
+ }
1223
+ };
1224
+
1225
+ // Initial copy
1226
+ copyReportToViz();
1227
+
1228
+ // Watch source report for changes
1229
+ let watchTimeout: NodeJS.Timeout | null = null;
1230
+ const reportWatcher = watch(reportPath, () => {
1231
+ // Debounce to avoid multiple copies during file write
1232
+ if (watchTimeout) clearTimeout(watchTimeout);
1233
+ watchTimeout = setTimeout(copyReportToViz, 100);
1234
+ });
1235
+
1236
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true });
1145
1237
  const onExit = () => {
1146
1238
  try {
1147
- watcher.kill();
1239
+ reportWatcher.close();
1148
1240
  } catch (e) {}
1149
1241
  try {
1150
1242
  vite.kill();
@@ -1222,14 +1314,15 @@ program
1222
1314
  .command('visualise')
1223
1315
  .description('Alias for visualize (British spelling)')
1224
1316
  .argument('[directory]', 'Directory to analyze', '.')
1225
- .option('--report <path>', 'Report path (relative to directory)', 'aiready-improvement-report.json')
1317
+ .option('--report <path>', 'Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)')
1226
1318
  .option('-o, --output <path>', 'Output HTML path (relative to directory)', 'packages/visualizer/visualization.html')
1227
1319
  .option('--open', 'Open generated HTML in default browser')
1228
1320
  .option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
1229
1321
  .option('--dev', 'Start Vite dev server (live reload) for interactive development', true)
1230
1322
  .addHelpText('after', `
1231
1323
  EXAMPLES:
1232
- $ aiready visualise . --report aiready-improvement-report.json
1324
+ $ aiready visualise . # Auto-detects latest report
1325
+ $ aiready visualise . --report .aiready/aiready-report-20260217-143022.json
1233
1326
  $ aiready visualise . --report report.json --dev
1234
1327
  $ aiready visualise . --report report.json --serve 8080
1235
1328
 
@@ -1242,14 +1335,15 @@ program
1242
1335
  .command('visualize')
1243
1336
  .description('Generate interactive visualization from an AIReady report')
1244
1337
  .argument('[directory]', 'Directory to analyze', '.')
1245
- .option('--report <path>', 'Report path (relative to directory)', 'aiready-improvement-report.json')
1338
+ .option('--report <path>', 'Report path (auto-detects latest .aiready/aiready-report-*.json if not provided)')
1246
1339
  .option('-o, --output <path>', 'Output HTML path (relative to directory)', 'packages/visualizer/visualization.html')
1247
1340
  .option('--open', 'Open generated HTML in default browser')
1248
1341
  .option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
1249
1342
  .option('--dev', 'Start Vite dev server (live reload) for interactive development', false)
1250
1343
  .addHelpText('after', `
1251
1344
  EXAMPLES:
1252
- $ aiready visualize . --report aiready-improvement-report.json
1345
+ $ aiready visualize . # Auto-detects latest report
1346
+ $ aiready visualize . --report .aiready/aiready-report-20260217-143022.json
1253
1347
  $ aiready visualize . --report report.json -o out/visualization.html --open
1254
1348
  $ aiready visualize . --report report.json --serve
1255
1349
  $ aiready visualize . --report report.json --serve 8080