@aiready/cli 0.9.26 → 0.9.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +17 -18
- package/dist/cli.js +100 -167
- package/dist/cli.mjs +101 -167
- package/package.json +3 -3
- package/src/cli.ts +119 -165
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[32mESM[39m ⚡️ Build success in 186ms
|
|
1
|
+
|
|
2
|
+
> @aiready/cli@0.9.26 build /Users/pengcao/projects/aiready/packages/cli
|
|
3
|
+
> tsup src/index.ts src/cli.ts --format cjs,esm
|
|
4
|
+
|
|
5
|
+
CLI Building entry: src/cli.ts, src/index.ts
|
|
6
|
+
CLI Using tsconfig: tsconfig.json
|
|
7
|
+
CLI tsup v8.5.1
|
|
8
|
+
CLI Target: es2020
|
|
9
|
+
CJS Build start
|
|
10
|
+
ESM Build start
|
|
11
|
+
CJS dist/cli.js 57.70 KB
|
|
12
|
+
CJS dist/index.js 4.93 KB
|
|
13
|
+
CJS ⚡️ Build success in 32ms
|
|
14
|
+
ESM dist/cli.mjs 50.05 KB
|
|
15
|
+
ESM dist/index.mjs 138.00 B
|
|
16
|
+
ESM dist/chunk-5GZDRZ3T.mjs 4.17 KB
|
|
17
|
+
ESM ⚡️ Build success in 32ms
|
package/dist/cli.js
CHANGED
|
@@ -157,12 +157,24 @@ VERSION: ${packageJson.version}
|
|
|
157
157
|
DOCUMENTATION: https://aiready.dev/docs/cli
|
|
158
158
|
GITHUB: https://github.com/caopengau/aiready-cli
|
|
159
159
|
LANDING: https://github.com/caopengau/aiready-landing`);
|
|
160
|
-
program.command("scan").description("Run comprehensive AI-readiness analysis (patterns + context + consistency)").argument("[directory]", "Directory to analyze", ".").option("-t, --tools <tools>", "Tools to run (comma-separated: patterns,context,consistency)", "patterns,context,consistency").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", "json").option("--output-file <path>", "Output file path (for json)").option("--no-score", "Disable calculating AI Readiness Score (enabled by default)").option("--weights <weights>", "Custom scoring weights (patterns:40,context:35,consistency:25)").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").addHelpText("after", `
|
|
160
|
+
program.command("scan").description("Run comprehensive AI-readiness analysis (patterns + context + consistency)").argument("[directory]", "Directory to analyze", ".").option("-t, --tools <tools>", "Tools to run (comma-separated: patterns,context,consistency)", "patterns,context,consistency").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", "json").option("--output-file <path>", "Output file path (for json)").option("--no-score", "Disable calculating AI Readiness Score (enabled by default)").option("--weights <weights>", "Custom scoring weights (patterns:40,context:35,consistency:25)").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").option("--ci", "CI mode: GitHub Actions annotations, no colors, fail on threshold").option("--fail-on <level>", "Fail on issues: critical, major, any", "critical").addHelpText("after", `
|
|
161
161
|
EXAMPLES:
|
|
162
162
|
$ aiready scan # Analyze all tools
|
|
163
163
|
$ aiready scan --tools patterns,context # Skip consistency
|
|
164
164
|
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
165
|
+
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
166
|
+
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
165
167
|
$ aiready scan --output json --output-file report.json
|
|
168
|
+
|
|
169
|
+
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
170
|
+
Use --ci for GitHub Actions integration:
|
|
171
|
+
- Outputs GitHub Actions annotations for PR checks
|
|
172
|
+
- Fails with exit code 1 if threshold not met
|
|
173
|
+
- Shows clear "blocked" message with remediation steps
|
|
174
|
+
|
|
175
|
+
Example GitHub Actions workflow:
|
|
176
|
+
- name: AI Readiness Check
|
|
177
|
+
run: aiready scan --ci --threshold 70
|
|
166
178
|
`).action(async (directory, options) => {
|
|
167
179
|
console.log(import_chalk.default.blue("\u{1F680} Starting AIReady unified analysis...\n"));
|
|
168
180
|
const startTime = Date.now();
|
|
@@ -394,6 +406,91 @@ EXAMPLES:
|
|
|
394
406
|
(0, import_core.handleJSONOutput)(outputData, outputPath, `\u2705 Report saved to ${outputPath}`);
|
|
395
407
|
warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
396
408
|
}
|
|
409
|
+
const isCI = options.ci || process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
410
|
+
if (isCI && scoringResult) {
|
|
411
|
+
const threshold = options.threshold ? parseInt(options.threshold) : void 0;
|
|
412
|
+
const failOnLevel = options.failOn || "critical";
|
|
413
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
414
|
+
console.log(`
|
|
415
|
+
::group::AI Readiness Score`);
|
|
416
|
+
console.log(`score=${scoringResult.overallScore}`);
|
|
417
|
+
if (scoringResult.breakdown) {
|
|
418
|
+
scoringResult.breakdown.forEach((tool) => {
|
|
419
|
+
console.log(`${tool.toolName}=${tool.score}`);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
console.log("::endgroup::");
|
|
423
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
424
|
+
console.log(`::error::AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`);
|
|
425
|
+
} else if (threshold) {
|
|
426
|
+
console.log(`::notice::AI Readiness Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`);
|
|
427
|
+
}
|
|
428
|
+
if (results.patterns) {
|
|
429
|
+
const criticalPatterns = results.patterns.flatMap(
|
|
430
|
+
(p) => p.issues.filter((i) => i.severity === "critical")
|
|
431
|
+
);
|
|
432
|
+
criticalPatterns.slice(0, 10).forEach((issue) => {
|
|
433
|
+
console.log(`::warning file=${issue.location?.file || "unknown"},line=${issue.location?.line || 1}::${issue.message}`);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
let shouldFail = false;
|
|
438
|
+
let failReason = "";
|
|
439
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
440
|
+
shouldFail = true;
|
|
441
|
+
failReason = `AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`;
|
|
442
|
+
}
|
|
443
|
+
if (failOnLevel !== "none") {
|
|
444
|
+
const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
|
|
445
|
+
const minSeverity = severityLevels[failOnLevel] || 4;
|
|
446
|
+
let criticalCount = 0;
|
|
447
|
+
let majorCount = 0;
|
|
448
|
+
if (results.patterns) {
|
|
449
|
+
results.patterns.forEach((p) => {
|
|
450
|
+
p.issues.forEach((i) => {
|
|
451
|
+
if (i.severity === "critical") criticalCount++;
|
|
452
|
+
if (i.severity === "major") majorCount++;
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (results.context) {
|
|
457
|
+
results.context.forEach((c) => {
|
|
458
|
+
if (c.severity === "critical") criticalCount++;
|
|
459
|
+
if (c.severity === "major") majorCount++;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (results.consistency?.results) {
|
|
463
|
+
results.consistency.results.forEach((r) => {
|
|
464
|
+
r.issues?.forEach((i) => {
|
|
465
|
+
if (i.severity === "critical") criticalCount++;
|
|
466
|
+
if (i.severity === "major") majorCount++;
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (minSeverity >= 4 && criticalCount > 0) {
|
|
471
|
+
shouldFail = true;
|
|
472
|
+
failReason = `Found ${criticalCount} critical issues`;
|
|
473
|
+
} else if (minSeverity >= 3 && criticalCount + majorCount > 0) {
|
|
474
|
+
shouldFail = true;
|
|
475
|
+
failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (shouldFail) {
|
|
479
|
+
console.log(import_chalk.default.red("\n\u{1F6AB} PR BLOCKED: AI Readiness Check Failed"));
|
|
480
|
+
console.log(import_chalk.default.red(` Reason: ${failReason}`));
|
|
481
|
+
console.log(import_chalk.default.dim("\n Remediation steps:"));
|
|
482
|
+
console.log(import_chalk.default.dim(" 1. Run `aiready scan` locally to see detailed issues"));
|
|
483
|
+
console.log(import_chalk.default.dim(" 2. Fix the critical issues before merging"));
|
|
484
|
+
console.log(import_chalk.default.dim(" 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing"));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
} else {
|
|
487
|
+
console.log(import_chalk.default.green("\n\u2705 PR PASSED: AI Readiness Check"));
|
|
488
|
+
if (threshold) {
|
|
489
|
+
console.log(import_chalk.default.green(` Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`));
|
|
490
|
+
}
|
|
491
|
+
console.log(import_chalk.default.dim("\n \u{1F4A1} Track historical trends: https://getaiready.dev \u2014 Team plan $99/mo"));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
397
494
|
} catch (error) {
|
|
398
495
|
(0, import_core.handleCLIError)(error, "Analysis");
|
|
399
496
|
}
|
|
@@ -817,170 +914,6 @@ function generateMarkdownReport(report, elapsedTime) {
|
|
|
817
914
|
}
|
|
818
915
|
return markdown;
|
|
819
916
|
}
|
|
820
|
-
function generateHTML(graph) {
|
|
821
|
-
const payload = JSON.stringify(graph, null, 2);
|
|
822
|
-
return `<!doctype html>
|
|
823
|
-
<html>
|
|
824
|
-
<head>
|
|
825
|
-
<meta charset="utf-8" />
|
|
826
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
827
|
-
<title>AIReady Visualization</title>
|
|
828
|
-
<style>
|
|
829
|
-
html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
|
|
830
|
-
#container { display:flex; height:100vh }
|
|
831
|
-
#panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
|
|
832
|
-
#canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
|
|
833
|
-
canvas { background: #0b1220; border-radius:8px }
|
|
834
|
-
.stat { margin-bottom:12px }
|
|
835
|
-
</style>
|
|
836
|
-
</head>
|
|
837
|
-
<body>
|
|
838
|
-
<div id="container">
|
|
839
|
-
<div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
|
|
840
|
-
<div id="panel">
|
|
841
|
-
<h2>AIReady Visualization</h2>
|
|
842
|
-
<div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
|
|
843
|
-
<div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
|
|
844
|
-
<div class="stat"><strong>Legend</strong></div>
|
|
845
|
-
<div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
|
|
846
|
-
<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>
|
|
847
|
-
<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>
|
|
848
|
-
<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>
|
|
849
|
-
<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>
|
|
850
|
-
<div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
|
|
851
|
-
<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>
|
|
852
|
-
<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>
|
|
853
|
-
</div>
|
|
854
|
-
</div>
|
|
855
|
-
</div>
|
|
856
|
-
|
|
857
|
-
<script>
|
|
858
|
-
const graphData = ${payload};
|
|
859
|
-
document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
|
|
860
|
-
document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
|
|
861
|
-
|
|
862
|
-
const canvas = document.getElementById('canvas');
|
|
863
|
-
const ctx = canvas.getContext('2d');
|
|
864
|
-
|
|
865
|
-
const nodes = graphData.nodes.map((n, i) => ({
|
|
866
|
-
...n,
|
|
867
|
-
x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
868
|
-
y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
869
|
-
}));
|
|
870
|
-
|
|
871
|
-
function draw() {
|
|
872
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
873
|
-
|
|
874
|
-
graphData.edges.forEach(edge => {
|
|
875
|
-
const s = nodes.find(n => n.id === edge.source);
|
|
876
|
-
const t = nodes.find(n => n.id === edge.target);
|
|
877
|
-
if (!s || !t) return;
|
|
878
|
-
if (edge.type === 'related') return;
|
|
879
|
-
if (edge.type === 'similarity') {
|
|
880
|
-
ctx.strokeStyle = '#fb7e81';
|
|
881
|
-
ctx.lineWidth = 1.2;
|
|
882
|
-
} else if (edge.type === 'dependency') {
|
|
883
|
-
ctx.strokeStyle = '#84c1ff';
|
|
884
|
-
ctx.lineWidth = 1.0;
|
|
885
|
-
} else if (edge.type === 'reference') {
|
|
886
|
-
ctx.strokeStyle = '#ffa500';
|
|
887
|
-
ctx.lineWidth = 0.9;
|
|
888
|
-
} else {
|
|
889
|
-
ctx.strokeStyle = '#334155';
|
|
890
|
-
ctx.lineWidth = 0.8;
|
|
891
|
-
}
|
|
892
|
-
ctx.beginPath();
|
|
893
|
-
ctx.moveTo(s.x, s.y);
|
|
894
|
-
ctx.lineTo(t.x, t.y);
|
|
895
|
-
ctx.stroke();
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
const groups = {};
|
|
899
|
-
nodes.forEach(n => {
|
|
900
|
-
const g = n.group || '__default';
|
|
901
|
-
if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
|
|
902
|
-
groups[g].minX = Math.min(groups[g].minX, n.x);
|
|
903
|
-
groups[g].minY = Math.min(groups[g].minY, n.y);
|
|
904
|
-
groups[g].maxX = Math.max(groups[g].maxX, n.x);
|
|
905
|
-
groups[g].maxY = Math.max(groups[g].maxY, n.y);
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
const groupRelations = {};
|
|
909
|
-
graphData.edges.forEach(edge => {
|
|
910
|
-
const sNode = nodes.find(n => n.id === edge.source);
|
|
911
|
-
const tNode = nodes.find(n => n.id === edge.target);
|
|
912
|
-
if (!sNode || !tNode) return;
|
|
913
|
-
const g1 = sNode.group || '__default';
|
|
914
|
-
const g2 = tNode.group || '__default';
|
|
915
|
-
if (g1 === g2) return;
|
|
916
|
-
const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
|
|
917
|
-
groupRelations[key] = (groupRelations[key] || 0) + 1;
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
Object.keys(groupRelations).forEach(k => {
|
|
921
|
-
const count = groupRelations[k];
|
|
922
|
-
const [ga, gb] = k.split('::');
|
|
923
|
-
if (!groups[ga] || !groups[gb]) return;
|
|
924
|
-
const ax = (groups[ga].minX + groups[ga].maxX) / 2;
|
|
925
|
-
const ay = (groups[ga].minY + groups[ga].maxY) / 2;
|
|
926
|
-
const bx = (groups[gb].minX + groups[gb].maxX) / 2;
|
|
927
|
-
const by = (groups[gb].minY + groups[gb].maxY) / 2;
|
|
928
|
-
ctx.beginPath();
|
|
929
|
-
ctx.strokeStyle = 'rgba(148,163,184,0.25)';
|
|
930
|
-
ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
|
|
931
|
-
ctx.moveTo(ax, ay);
|
|
932
|
-
ctx.lineTo(bx, by);
|
|
933
|
-
ctx.stroke();
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
Object.keys(groups).forEach(g => {
|
|
937
|
-
if (g === '__default') return;
|
|
938
|
-
const box = groups[g];
|
|
939
|
-
const pad = 16;
|
|
940
|
-
const x = box.minX - pad;
|
|
941
|
-
const y = box.minY - pad;
|
|
942
|
-
const w = (box.maxX - box.minX) + pad * 2;
|
|
943
|
-
const h = (box.maxY - box.minY) + pad * 2;
|
|
944
|
-
ctx.save();
|
|
945
|
-
ctx.fillStyle = 'rgba(30,64,175,0.04)';
|
|
946
|
-
ctx.strokeStyle = 'rgba(30,64,175,0.12)';
|
|
947
|
-
ctx.lineWidth = 1.2;
|
|
948
|
-
const r = 8;
|
|
949
|
-
ctx.beginPath();
|
|
950
|
-
ctx.moveTo(x + r, y);
|
|
951
|
-
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
952
|
-
ctx.arcTo(x + w, y + h, x, y + h, r);
|
|
953
|
-
ctx.arcTo(x, y + h, x, y, r);
|
|
954
|
-
ctx.arcTo(x, y, x + w, y, r);
|
|
955
|
-
ctx.closePath();
|
|
956
|
-
ctx.fill();
|
|
957
|
-
ctx.stroke();
|
|
958
|
-
ctx.restore();
|
|
959
|
-
ctx.fillStyle = '#94a3b8';
|
|
960
|
-
ctx.font = '11px sans-serif';
|
|
961
|
-
ctx.fillText(g, x + 8, y + 14);
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
nodes.forEach(n => {
|
|
965
|
-
const sizeVal = (n.size || n.value || 1);
|
|
966
|
-
const r = 6 + (sizeVal / 2);
|
|
967
|
-
ctx.beginPath();
|
|
968
|
-
ctx.fillStyle = n.color || '#60a5fa';
|
|
969
|
-
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
|
970
|
-
ctx.fill();
|
|
971
|
-
|
|
972
|
-
ctx.fillStyle = '#e2e8f0';
|
|
973
|
-
ctx.font = '11px sans-serif';
|
|
974
|
-
ctx.textAlign = 'center';
|
|
975
|
-
ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
draw();
|
|
980
|
-
</script>
|
|
981
|
-
</body>
|
|
982
|
-
</html>`;
|
|
983
|
-
}
|
|
984
917
|
function getReportTimestamp() {
|
|
985
918
|
const now = /* @__PURE__ */ new Date();
|
|
986
919
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -1173,7 +1106,7 @@ Or specify a custom report:
|
|
|
1173
1106
|
}
|
|
1174
1107
|
}
|
|
1175
1108
|
console.log("Generating HTML...");
|
|
1176
|
-
const html = generateHTML(graph);
|
|
1109
|
+
const html = (0, import_core.generateHTML)(graph);
|
|
1177
1110
|
const outPath = (0, import_path2.resolve)(dirPath, options.output || "packages/visualizer/visualization.html");
|
|
1178
1111
|
(0, import_fs.writeFileSync)(outPath, html, "utf8");
|
|
1179
1112
|
console.log("Visualization written to:", outPath);
|
|
@@ -1184,7 +1117,7 @@ Or specify a custom report:
|
|
|
1184
1117
|
}
|
|
1185
1118
|
if (options.serve) {
|
|
1186
1119
|
try {
|
|
1187
|
-
const port =
|
|
1120
|
+
const port = typeof options.serve === "number" ? options.serve : 5173;
|
|
1188
1121
|
const http = await import("http");
|
|
1189
1122
|
const fsp = await import("fs/promises");
|
|
1190
1123
|
const { exec } = await import("child_process");
|
package/dist/cli.mjs
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
formatToolScore,
|
|
21
21
|
getRating,
|
|
22
22
|
getRatingDisplay,
|
|
23
|
-
parseWeightString
|
|
23
|
+
parseWeightString,
|
|
24
|
+
generateHTML
|
|
24
25
|
} from "@aiready/core";
|
|
25
26
|
import { readFileSync, existsSync, copyFileSync } from "fs";
|
|
26
27
|
import { resolve as resolvePath } from "path";
|
|
@@ -62,12 +63,24 @@ VERSION: ${packageJson.version}
|
|
|
62
63
|
DOCUMENTATION: https://aiready.dev/docs/cli
|
|
63
64
|
GITHUB: https://github.com/caopengau/aiready-cli
|
|
64
65
|
LANDING: https://github.com/caopengau/aiready-landing`);
|
|
65
|
-
program.command("scan").description("Run comprehensive AI-readiness analysis (patterns + context + consistency)").argument("[directory]", "Directory to analyze", ".").option("-t, --tools <tools>", "Tools to run (comma-separated: patterns,context,consistency)", "patterns,context,consistency").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", "json").option("--output-file <path>", "Output file path (for json)").option("--no-score", "Disable calculating AI Readiness Score (enabled by default)").option("--weights <weights>", "Custom scoring weights (patterns:40,context:35,consistency:25)").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").addHelpText("after", `
|
|
66
|
+
program.command("scan").description("Run comprehensive AI-readiness analysis (patterns + context + consistency)").argument("[directory]", "Directory to analyze", ".").option("-t, --tools <tools>", "Tools to run (comma-separated: patterns,context,consistency)", "patterns,context,consistency").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", "json").option("--output-file <path>", "Output file path (for json)").option("--no-score", "Disable calculating AI Readiness Score (enabled by default)").option("--weights <weights>", "Custom scoring weights (patterns:40,context:35,consistency:25)").option("--threshold <score>", "Fail CI/CD if score below threshold (0-100)").option("--ci", "CI mode: GitHub Actions annotations, no colors, fail on threshold").option("--fail-on <level>", "Fail on issues: critical, major, any", "critical").addHelpText("after", `
|
|
66
67
|
EXAMPLES:
|
|
67
68
|
$ aiready scan # Analyze all tools
|
|
68
69
|
$ aiready scan --tools patterns,context # Skip consistency
|
|
69
70
|
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
71
|
+
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
72
|
+
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
70
73
|
$ aiready scan --output json --output-file report.json
|
|
74
|
+
|
|
75
|
+
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
76
|
+
Use --ci for GitHub Actions integration:
|
|
77
|
+
- Outputs GitHub Actions annotations for PR checks
|
|
78
|
+
- Fails with exit code 1 if threshold not met
|
|
79
|
+
- Shows clear "blocked" message with remediation steps
|
|
80
|
+
|
|
81
|
+
Example GitHub Actions workflow:
|
|
82
|
+
- name: AI Readiness Check
|
|
83
|
+
run: aiready scan --ci --threshold 70
|
|
71
84
|
`).action(async (directory, options) => {
|
|
72
85
|
console.log(chalk.blue("\u{1F680} Starting AIReady unified analysis...\n"));
|
|
73
86
|
const startTime = Date.now();
|
|
@@ -299,6 +312,91 @@ EXAMPLES:
|
|
|
299
312
|
handleJSONOutput(outputData, outputPath, `\u2705 Report saved to ${outputPath}`);
|
|
300
313
|
warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
301
314
|
}
|
|
315
|
+
const isCI = options.ci || process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
316
|
+
if (isCI && scoringResult) {
|
|
317
|
+
const threshold = options.threshold ? parseInt(options.threshold) : void 0;
|
|
318
|
+
const failOnLevel = options.failOn || "critical";
|
|
319
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
320
|
+
console.log(`
|
|
321
|
+
::group::AI Readiness Score`);
|
|
322
|
+
console.log(`score=${scoringResult.overallScore}`);
|
|
323
|
+
if (scoringResult.breakdown) {
|
|
324
|
+
scoringResult.breakdown.forEach((tool) => {
|
|
325
|
+
console.log(`${tool.toolName}=${tool.score}`);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
console.log("::endgroup::");
|
|
329
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
330
|
+
console.log(`::error::AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`);
|
|
331
|
+
} else if (threshold) {
|
|
332
|
+
console.log(`::notice::AI Readiness Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`);
|
|
333
|
+
}
|
|
334
|
+
if (results.patterns) {
|
|
335
|
+
const criticalPatterns = results.patterns.flatMap(
|
|
336
|
+
(p) => p.issues.filter((i) => i.severity === "critical")
|
|
337
|
+
);
|
|
338
|
+
criticalPatterns.slice(0, 10).forEach((issue) => {
|
|
339
|
+
console.log(`::warning file=${issue.location?.file || "unknown"},line=${issue.location?.line || 1}::${issue.message}`);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
let shouldFail = false;
|
|
344
|
+
let failReason = "";
|
|
345
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
346
|
+
shouldFail = true;
|
|
347
|
+
failReason = `AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`;
|
|
348
|
+
}
|
|
349
|
+
if (failOnLevel !== "none") {
|
|
350
|
+
const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
|
|
351
|
+
const minSeverity = severityLevels[failOnLevel] || 4;
|
|
352
|
+
let criticalCount = 0;
|
|
353
|
+
let majorCount = 0;
|
|
354
|
+
if (results.patterns) {
|
|
355
|
+
results.patterns.forEach((p) => {
|
|
356
|
+
p.issues.forEach((i) => {
|
|
357
|
+
if (i.severity === "critical") criticalCount++;
|
|
358
|
+
if (i.severity === "major") majorCount++;
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (results.context) {
|
|
363
|
+
results.context.forEach((c) => {
|
|
364
|
+
if (c.severity === "critical") criticalCount++;
|
|
365
|
+
if (c.severity === "major") majorCount++;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (results.consistency?.results) {
|
|
369
|
+
results.consistency.results.forEach((r) => {
|
|
370
|
+
r.issues?.forEach((i) => {
|
|
371
|
+
if (i.severity === "critical") criticalCount++;
|
|
372
|
+
if (i.severity === "major") majorCount++;
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (minSeverity >= 4 && criticalCount > 0) {
|
|
377
|
+
shouldFail = true;
|
|
378
|
+
failReason = `Found ${criticalCount} critical issues`;
|
|
379
|
+
} else if (minSeverity >= 3 && criticalCount + majorCount > 0) {
|
|
380
|
+
shouldFail = true;
|
|
381
|
+
failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (shouldFail) {
|
|
385
|
+
console.log(chalk.red("\n\u{1F6AB} PR BLOCKED: AI Readiness Check Failed"));
|
|
386
|
+
console.log(chalk.red(` Reason: ${failReason}`));
|
|
387
|
+
console.log(chalk.dim("\n Remediation steps:"));
|
|
388
|
+
console.log(chalk.dim(" 1. Run `aiready scan` locally to see detailed issues"));
|
|
389
|
+
console.log(chalk.dim(" 2. Fix the critical issues before merging"));
|
|
390
|
+
console.log(chalk.dim(" 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing"));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(chalk.green("\n\u2705 PR PASSED: AI Readiness Check"));
|
|
394
|
+
if (threshold) {
|
|
395
|
+
console.log(chalk.green(` Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`));
|
|
396
|
+
}
|
|
397
|
+
console.log(chalk.dim("\n \u{1F4A1} Track historical trends: https://getaiready.dev \u2014 Team plan $99/mo"));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
302
400
|
} catch (error) {
|
|
303
401
|
handleCLIError(error, "Analysis");
|
|
304
402
|
}
|
|
@@ -722,170 +820,6 @@ function generateMarkdownReport(report, elapsedTime) {
|
|
|
722
820
|
}
|
|
723
821
|
return markdown;
|
|
724
822
|
}
|
|
725
|
-
function generateHTML(graph) {
|
|
726
|
-
const payload = JSON.stringify(graph, null, 2);
|
|
727
|
-
return `<!doctype html>
|
|
728
|
-
<html>
|
|
729
|
-
<head>
|
|
730
|
-
<meta charset="utf-8" />
|
|
731
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
732
|
-
<title>AIReady Visualization</title>
|
|
733
|
-
<style>
|
|
734
|
-
html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
|
|
735
|
-
#container { display:flex; height:100vh }
|
|
736
|
-
#panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
|
|
737
|
-
#canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
|
|
738
|
-
canvas { background: #0b1220; border-radius:8px }
|
|
739
|
-
.stat { margin-bottom:12px }
|
|
740
|
-
</style>
|
|
741
|
-
</head>
|
|
742
|
-
<body>
|
|
743
|
-
<div id="container">
|
|
744
|
-
<div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
|
|
745
|
-
<div id="panel">
|
|
746
|
-
<h2>AIReady Visualization</h2>
|
|
747
|
-
<div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
|
|
748
|
-
<div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
|
|
749
|
-
<div class="stat"><strong>Legend</strong></div>
|
|
750
|
-
<div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
|
|
751
|
-
<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>
|
|
752
|
-
<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>
|
|
753
|
-
<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>
|
|
754
|
-
<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>
|
|
755
|
-
<div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
|
|
756
|
-
<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>
|
|
757
|
-
<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>
|
|
758
|
-
</div>
|
|
759
|
-
</div>
|
|
760
|
-
</div>
|
|
761
|
-
|
|
762
|
-
<script>
|
|
763
|
-
const graphData = ${payload};
|
|
764
|
-
document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
|
|
765
|
-
document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
|
|
766
|
-
|
|
767
|
-
const canvas = document.getElementById('canvas');
|
|
768
|
-
const ctx = canvas.getContext('2d');
|
|
769
|
-
|
|
770
|
-
const nodes = graphData.nodes.map((n, i) => ({
|
|
771
|
-
...n,
|
|
772
|
-
x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
773
|
-
y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
774
|
-
}));
|
|
775
|
-
|
|
776
|
-
function draw() {
|
|
777
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
778
|
-
|
|
779
|
-
graphData.edges.forEach(edge => {
|
|
780
|
-
const s = nodes.find(n => n.id === edge.source);
|
|
781
|
-
const t = nodes.find(n => n.id === edge.target);
|
|
782
|
-
if (!s || !t) return;
|
|
783
|
-
if (edge.type === 'related') return;
|
|
784
|
-
if (edge.type === 'similarity') {
|
|
785
|
-
ctx.strokeStyle = '#fb7e81';
|
|
786
|
-
ctx.lineWidth = 1.2;
|
|
787
|
-
} else if (edge.type === 'dependency') {
|
|
788
|
-
ctx.strokeStyle = '#84c1ff';
|
|
789
|
-
ctx.lineWidth = 1.0;
|
|
790
|
-
} else if (edge.type === 'reference') {
|
|
791
|
-
ctx.strokeStyle = '#ffa500';
|
|
792
|
-
ctx.lineWidth = 0.9;
|
|
793
|
-
} else {
|
|
794
|
-
ctx.strokeStyle = '#334155';
|
|
795
|
-
ctx.lineWidth = 0.8;
|
|
796
|
-
}
|
|
797
|
-
ctx.beginPath();
|
|
798
|
-
ctx.moveTo(s.x, s.y);
|
|
799
|
-
ctx.lineTo(t.x, t.y);
|
|
800
|
-
ctx.stroke();
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
const groups = {};
|
|
804
|
-
nodes.forEach(n => {
|
|
805
|
-
const g = n.group || '__default';
|
|
806
|
-
if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
|
|
807
|
-
groups[g].minX = Math.min(groups[g].minX, n.x);
|
|
808
|
-
groups[g].minY = Math.min(groups[g].minY, n.y);
|
|
809
|
-
groups[g].maxX = Math.max(groups[g].maxX, n.x);
|
|
810
|
-
groups[g].maxY = Math.max(groups[g].maxY, n.y);
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const groupRelations = {};
|
|
814
|
-
graphData.edges.forEach(edge => {
|
|
815
|
-
const sNode = nodes.find(n => n.id === edge.source);
|
|
816
|
-
const tNode = nodes.find(n => n.id === edge.target);
|
|
817
|
-
if (!sNode || !tNode) return;
|
|
818
|
-
const g1 = sNode.group || '__default';
|
|
819
|
-
const g2 = tNode.group || '__default';
|
|
820
|
-
if (g1 === g2) return;
|
|
821
|
-
const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
|
|
822
|
-
groupRelations[key] = (groupRelations[key] || 0) + 1;
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
Object.keys(groupRelations).forEach(k => {
|
|
826
|
-
const count = groupRelations[k];
|
|
827
|
-
const [ga, gb] = k.split('::');
|
|
828
|
-
if (!groups[ga] || !groups[gb]) return;
|
|
829
|
-
const ax = (groups[ga].minX + groups[ga].maxX) / 2;
|
|
830
|
-
const ay = (groups[ga].minY + groups[ga].maxY) / 2;
|
|
831
|
-
const bx = (groups[gb].minX + groups[gb].maxX) / 2;
|
|
832
|
-
const by = (groups[gb].minY + groups[gb].maxY) / 2;
|
|
833
|
-
ctx.beginPath();
|
|
834
|
-
ctx.strokeStyle = 'rgba(148,163,184,0.25)';
|
|
835
|
-
ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
|
|
836
|
-
ctx.moveTo(ax, ay);
|
|
837
|
-
ctx.lineTo(bx, by);
|
|
838
|
-
ctx.stroke();
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
Object.keys(groups).forEach(g => {
|
|
842
|
-
if (g === '__default') return;
|
|
843
|
-
const box = groups[g];
|
|
844
|
-
const pad = 16;
|
|
845
|
-
const x = box.minX - pad;
|
|
846
|
-
const y = box.minY - pad;
|
|
847
|
-
const w = (box.maxX - box.minX) + pad * 2;
|
|
848
|
-
const h = (box.maxY - box.minY) + pad * 2;
|
|
849
|
-
ctx.save();
|
|
850
|
-
ctx.fillStyle = 'rgba(30,64,175,0.04)';
|
|
851
|
-
ctx.strokeStyle = 'rgba(30,64,175,0.12)';
|
|
852
|
-
ctx.lineWidth = 1.2;
|
|
853
|
-
const r = 8;
|
|
854
|
-
ctx.beginPath();
|
|
855
|
-
ctx.moveTo(x + r, y);
|
|
856
|
-
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
857
|
-
ctx.arcTo(x + w, y + h, x, y + h, r);
|
|
858
|
-
ctx.arcTo(x, y + h, x, y, r);
|
|
859
|
-
ctx.arcTo(x, y, x + w, y, r);
|
|
860
|
-
ctx.closePath();
|
|
861
|
-
ctx.fill();
|
|
862
|
-
ctx.stroke();
|
|
863
|
-
ctx.restore();
|
|
864
|
-
ctx.fillStyle = '#94a3b8';
|
|
865
|
-
ctx.font = '11px sans-serif';
|
|
866
|
-
ctx.fillText(g, x + 8, y + 14);
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
nodes.forEach(n => {
|
|
870
|
-
const sizeVal = (n.size || n.value || 1);
|
|
871
|
-
const r = 6 + (sizeVal / 2);
|
|
872
|
-
ctx.beginPath();
|
|
873
|
-
ctx.fillStyle = n.color || '#60a5fa';
|
|
874
|
-
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
|
875
|
-
ctx.fill();
|
|
876
|
-
|
|
877
|
-
ctx.fillStyle = '#e2e8f0';
|
|
878
|
-
ctx.font = '11px sans-serif';
|
|
879
|
-
ctx.textAlign = 'center';
|
|
880
|
-
ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
draw();
|
|
885
|
-
</script>
|
|
886
|
-
</body>
|
|
887
|
-
</html>`;
|
|
888
|
-
}
|
|
889
823
|
function getReportTimestamp() {
|
|
890
824
|
const now = /* @__PURE__ */ new Date();
|
|
891
825
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -1089,7 +1023,7 @@ Or specify a custom report:
|
|
|
1089
1023
|
}
|
|
1090
1024
|
if (options.serve) {
|
|
1091
1025
|
try {
|
|
1092
|
-
const port =
|
|
1026
|
+
const port = typeof options.serve === "number" ? options.serve : 5173;
|
|
1093
1027
|
const http = await import("http");
|
|
1094
1028
|
const fsp = await import("fs/promises");
|
|
1095
1029
|
const { exec } = await import("child_process");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27",
|
|
4
4
|
"description": "Unified CLI for AIReady analysis tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"chalk": "^5.3.0",
|
|
14
14
|
"@aiready/core": "0.9.22",
|
|
15
15
|
"@aiready/pattern-detect": "0.11.22",
|
|
16
|
+
"@aiready/visualizer": "0.1.28",
|
|
16
17
|
"@aiready/context-analyzer": "0.9.26",
|
|
17
|
-
"@aiready/consistency": "0.8.22"
|
|
18
|
-
"@aiready/visualizer": "0.1.28"
|
|
18
|
+
"@aiready/consistency": "0.8.22"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"tsup": "^8.3.5",
|
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getRating,
|
|
18
18
|
getRatingDisplay,
|
|
19
19
|
parseWeightString,
|
|
20
|
+
generateHTML,
|
|
20
21
|
type AIReadyConfig,
|
|
21
22
|
type ToolScoringOutput,
|
|
22
23
|
} from '@aiready/core';
|
|
@@ -82,12 +83,26 @@ program
|
|
|
82
83
|
.option('--no-score', 'Disable calculating AI Readiness Score (enabled by default)')
|
|
83
84
|
.option('--weights <weights>', 'Custom scoring weights (patterns:40,context:35,consistency:25)')
|
|
84
85
|
.option('--threshold <score>', 'Fail CI/CD if score below threshold (0-100)')
|
|
86
|
+
.option('--ci', 'CI mode: GitHub Actions annotations, no colors, fail on threshold')
|
|
87
|
+
.option('--fail-on <level>', 'Fail on issues: critical, major, any', 'critical')
|
|
85
88
|
.addHelpText('after', `
|
|
86
89
|
EXAMPLES:
|
|
87
90
|
$ aiready scan # Analyze all tools
|
|
88
91
|
$ aiready scan --tools patterns,context # Skip consistency
|
|
89
92
|
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
93
|
+
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
94
|
+
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
90
95
|
$ aiready scan --output json --output-file report.json
|
|
96
|
+
|
|
97
|
+
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
98
|
+
Use --ci for GitHub Actions integration:
|
|
99
|
+
- Outputs GitHub Actions annotations for PR checks
|
|
100
|
+
- Fails with exit code 1 if threshold not met
|
|
101
|
+
- Shows clear "blocked" message with remediation steps
|
|
102
|
+
|
|
103
|
+
Example GitHub Actions workflow:
|
|
104
|
+
- name: AI Readiness Check
|
|
105
|
+
run: aiready scan --ci --threshold 70
|
|
91
106
|
`)
|
|
92
107
|
.action(async (directory, options) => {
|
|
93
108
|
console.log(chalk.blue('🚀 Starting AIReady unified analysis...\n'));
|
|
@@ -385,6 +400,109 @@ EXAMPLES:
|
|
|
385
400
|
// Warn if graph caps may be exceeded
|
|
386
401
|
warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
387
402
|
}
|
|
403
|
+
|
|
404
|
+
// CI/CD Gatekeeper Mode
|
|
405
|
+
const isCI = options.ci || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
406
|
+
if (isCI && scoringResult) {
|
|
407
|
+
const threshold = options.threshold ? parseInt(options.threshold) : undefined;
|
|
408
|
+
const failOnLevel = options.failOn || 'critical';
|
|
409
|
+
|
|
410
|
+
// Output GitHub Actions annotations
|
|
411
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
412
|
+
console.log(`\n::group::AI Readiness Score`);
|
|
413
|
+
console.log(`score=${scoringResult.overallScore}`);
|
|
414
|
+
if (scoringResult.breakdown) {
|
|
415
|
+
scoringResult.breakdown.forEach(tool => {
|
|
416
|
+
console.log(`${tool.toolName}=${tool.score}`);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
console.log('::endgroup::');
|
|
420
|
+
|
|
421
|
+
// Output annotation for score
|
|
422
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
423
|
+
console.log(`::error::AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`);
|
|
424
|
+
} else if (threshold) {
|
|
425
|
+
console.log(`::notice::AI Readiness Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Output annotations for critical issues
|
|
429
|
+
if (results.patterns) {
|
|
430
|
+
const criticalPatterns = results.patterns.flatMap((p: any) =>
|
|
431
|
+
p.issues.filter((i: any) => i.severity === 'critical')
|
|
432
|
+
);
|
|
433
|
+
criticalPatterns.slice(0, 10).forEach((issue: any) => {
|
|
434
|
+
console.log(`::warning file=${issue.location?.file || 'unknown'},line=${issue.location?.line || 1}::${issue.message}`);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Determine if we should fail
|
|
440
|
+
let shouldFail = false;
|
|
441
|
+
let failReason = '';
|
|
442
|
+
|
|
443
|
+
// Check threshold
|
|
444
|
+
if (threshold && scoringResult.overallScore < threshold) {
|
|
445
|
+
shouldFail = true;
|
|
446
|
+
failReason = `AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check fail-on severity
|
|
450
|
+
if (failOnLevel !== 'none') {
|
|
451
|
+
const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
|
|
452
|
+
const minSeverity = severityLevels[failOnLevel as keyof typeof severityLevels] || 4;
|
|
453
|
+
|
|
454
|
+
let criticalCount = 0;
|
|
455
|
+
let majorCount = 0;
|
|
456
|
+
|
|
457
|
+
if (results.patterns) {
|
|
458
|
+
results.patterns.forEach((p: any) => {
|
|
459
|
+
p.issues.forEach((i: any) => {
|
|
460
|
+
if (i.severity === 'critical') criticalCount++;
|
|
461
|
+
if (i.severity === 'major') majorCount++;
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (results.context) {
|
|
466
|
+
results.context.forEach((c: any) => {
|
|
467
|
+
if (c.severity === 'critical') criticalCount++;
|
|
468
|
+
if (c.severity === 'major') majorCount++;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (results.consistency?.results) {
|
|
472
|
+
results.consistency.results.forEach((r: any) => {
|
|
473
|
+
r.issues?.forEach((i: any) => {
|
|
474
|
+
if (i.severity === 'critical') criticalCount++;
|
|
475
|
+
if (i.severity === 'major') majorCount++;
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (minSeverity >= 4 && criticalCount > 0) {
|
|
481
|
+
shouldFail = true;
|
|
482
|
+
failReason = `Found ${criticalCount} critical issues`;
|
|
483
|
+
} else if (minSeverity >= 3 && (criticalCount + majorCount) > 0) {
|
|
484
|
+
shouldFail = true;
|
|
485
|
+
failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Output result
|
|
490
|
+
if (shouldFail) {
|
|
491
|
+
console.log(chalk.red('\n🚫 PR BLOCKED: AI Readiness Check Failed'));
|
|
492
|
+
console.log(chalk.red(` Reason: ${failReason}`));
|
|
493
|
+
console.log(chalk.dim('\n Remediation steps:'));
|
|
494
|
+
console.log(chalk.dim(' 1. Run `aiready scan` locally to see detailed issues'));
|
|
495
|
+
console.log(chalk.dim(' 2. Fix the critical issues before merging'));
|
|
496
|
+
console.log(chalk.dim(' 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing'));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
} else {
|
|
499
|
+
console.log(chalk.green('\n✅ PR PASSED: AI Readiness Check'));
|
|
500
|
+
if (threshold) {
|
|
501
|
+
console.log(chalk.green(` Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`));
|
|
502
|
+
}
|
|
503
|
+
console.log(chalk.dim('\n 💡 Track historical trends: https://getaiready.dev — Team plan $99/mo'));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
388
506
|
} catch (error) {
|
|
389
507
|
handleCLIError(error, 'Analysis');
|
|
390
508
|
}
|
|
@@ -930,170 +1048,6 @@ program
|
|
|
930
1048
|
return markdown;
|
|
931
1049
|
}
|
|
932
1050
|
|
|
933
|
-
function generateHTML(graph: GraphData): string {
|
|
934
|
-
const payload = JSON.stringify(graph, null, 2);
|
|
935
|
-
return `<!doctype html>
|
|
936
|
-
<html>
|
|
937
|
-
<head>
|
|
938
|
-
<meta charset="utf-8" />
|
|
939
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
940
|
-
<title>AIReady Visualization</title>
|
|
941
|
-
<style>
|
|
942
|
-
html,body { height: 100%; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0 }
|
|
943
|
-
#container { display:flex; height:100vh }
|
|
944
|
-
#panel { width: 320px; padding: 16px; background: #071130; box-shadow: -2px 0 8px rgba(0,0,0,0.3); overflow:auto }
|
|
945
|
-
#canvasWrap { flex:1; display:flex; align-items:center; justify-content:center }
|
|
946
|
-
canvas { background: #0b1220; border-radius:8px }
|
|
947
|
-
.stat { margin-bottom:12px }
|
|
948
|
-
</style>
|
|
949
|
-
</head>
|
|
950
|
-
<body>
|
|
951
|
-
<div id="container">
|
|
952
|
-
<div id="canvasWrap"><canvas id="canvas" width="1200" height="800"></canvas></div>
|
|
953
|
-
<div id="panel">
|
|
954
|
-
<h2>AIReady Visualization</h2>
|
|
955
|
-
<div class="stat"><strong>Files:</strong> <span id="stat-files"></span></div>
|
|
956
|
-
<div class="stat"><strong>Dependencies:</strong> <span id="stat-deps"></span></div>
|
|
957
|
-
<div class="stat"><strong>Legend</strong></div>
|
|
958
|
-
<div style="font-size:13px;line-height:1.3;color:#cbd5e1;margin-top:8px">
|
|
959
|
-
<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>
|
|
960
|
-
<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>
|
|
961
|
-
<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>
|
|
962
|
-
<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>
|
|
963
|
-
<div style="margin-top:10px;color:#94a3b8"><strong>Node size</strong>: larger = higher token cost, more issues or dependency weight.</div>
|
|
964
|
-
<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>
|
|
965
|
-
<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>
|
|
966
|
-
</div>
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
|
|
970
|
-
<script>
|
|
971
|
-
const graphData = ${payload};
|
|
972
|
-
document.getElementById('stat-files').textContent = graphData.metadata.totalFiles;
|
|
973
|
-
document.getElementById('stat-deps').textContent = graphData.metadata.totalDependencies;
|
|
974
|
-
|
|
975
|
-
const canvas = document.getElementById('canvas');
|
|
976
|
-
const ctx = canvas.getContext('2d');
|
|
977
|
-
|
|
978
|
-
const nodes = graphData.nodes.map((n, i) => ({
|
|
979
|
-
...n,
|
|
980
|
-
x: canvas.width / 2 + Math.cos(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
981
|
-
y: canvas.height / 2 + Math.sin(i / graphData.nodes.length * Math.PI * 2) * (Math.min(canvas.width, canvas.height) / 3),
|
|
982
|
-
}));
|
|
983
|
-
|
|
984
|
-
function draw() {
|
|
985
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
986
|
-
|
|
987
|
-
graphData.edges.forEach(edge => {
|
|
988
|
-
const s = nodes.find(n => n.id === edge.source);
|
|
989
|
-
const t = nodes.find(n => n.id === edge.target);
|
|
990
|
-
if (!s || !t) return;
|
|
991
|
-
if (edge.type === 'related') return;
|
|
992
|
-
if (edge.type === 'similarity') {
|
|
993
|
-
ctx.strokeStyle = '#fb7e81';
|
|
994
|
-
ctx.lineWidth = 1.2;
|
|
995
|
-
} else if (edge.type === 'dependency') {
|
|
996
|
-
ctx.strokeStyle = '#84c1ff';
|
|
997
|
-
ctx.lineWidth = 1.0;
|
|
998
|
-
} else if (edge.type === 'reference') {
|
|
999
|
-
ctx.strokeStyle = '#ffa500';
|
|
1000
|
-
ctx.lineWidth = 0.9;
|
|
1001
|
-
} else {
|
|
1002
|
-
ctx.strokeStyle = '#334155';
|
|
1003
|
-
ctx.lineWidth = 0.8;
|
|
1004
|
-
}
|
|
1005
|
-
ctx.beginPath();
|
|
1006
|
-
ctx.moveTo(s.x, s.y);
|
|
1007
|
-
ctx.lineTo(t.x, t.y);
|
|
1008
|
-
ctx.stroke();
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
const groups = {};
|
|
1012
|
-
nodes.forEach(n => {
|
|
1013
|
-
const g = n.group || '__default';
|
|
1014
|
-
if (!groups[g]) groups[g] = { minX: n.x, minY: n.y, maxX: n.x, maxY: n.y };
|
|
1015
|
-
groups[g].minX = Math.min(groups[g].minX, n.x);
|
|
1016
|
-
groups[g].minY = Math.min(groups[g].minY, n.y);
|
|
1017
|
-
groups[g].maxX = Math.max(groups[g].maxX, n.x);
|
|
1018
|
-
groups[g].maxY = Math.max(groups[g].maxY, n.y);
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
const groupRelations = {};
|
|
1022
|
-
graphData.edges.forEach(edge => {
|
|
1023
|
-
const sNode = nodes.find(n => n.id === edge.source);
|
|
1024
|
-
const tNode = nodes.find(n => n.id === edge.target);
|
|
1025
|
-
if (!sNode || !tNode) return;
|
|
1026
|
-
const g1 = sNode.group || '__default';
|
|
1027
|
-
const g2 = tNode.group || '__default';
|
|
1028
|
-
if (g1 === g2) return;
|
|
1029
|
-
const key = g1 < g2 ? g1 + '::' + g2 : g2 + '::' + g1;
|
|
1030
|
-
groupRelations[key] = (groupRelations[key] || 0) + 1;
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
Object.keys(groupRelations).forEach(k => {
|
|
1034
|
-
const count = groupRelations[k];
|
|
1035
|
-
const [ga, gb] = k.split('::');
|
|
1036
|
-
if (!groups[ga] || !groups[gb]) return;
|
|
1037
|
-
const ax = (groups[ga].minX + groups[ga].maxX) / 2;
|
|
1038
|
-
const ay = (groups[ga].minY + groups[ga].maxY) / 2;
|
|
1039
|
-
const bx = (groups[gb].minX + groups[gb].maxX) / 2;
|
|
1040
|
-
const by = (groups[gb].minY + groups[gb].maxY) / 2;
|
|
1041
|
-
ctx.beginPath();
|
|
1042
|
-
ctx.strokeStyle = 'rgba(148,163,184,0.25)';
|
|
1043
|
-
ctx.lineWidth = Math.min(6, 0.6 + Math.sqrt(count));
|
|
1044
|
-
ctx.moveTo(ax, ay);
|
|
1045
|
-
ctx.lineTo(bx, by);
|
|
1046
|
-
ctx.stroke();
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
Object.keys(groups).forEach(g => {
|
|
1050
|
-
if (g === '__default') return;
|
|
1051
|
-
const box = groups[g];
|
|
1052
|
-
const pad = 16;
|
|
1053
|
-
const x = box.minX - pad;
|
|
1054
|
-
const y = box.minY - pad;
|
|
1055
|
-
const w = (box.maxX - box.minX) + pad * 2;
|
|
1056
|
-
const h = (box.maxY - box.minY) + pad * 2;
|
|
1057
|
-
ctx.save();
|
|
1058
|
-
ctx.fillStyle = 'rgba(30,64,175,0.04)';
|
|
1059
|
-
ctx.strokeStyle = 'rgba(30,64,175,0.12)';
|
|
1060
|
-
ctx.lineWidth = 1.2;
|
|
1061
|
-
const r = 8;
|
|
1062
|
-
ctx.beginPath();
|
|
1063
|
-
ctx.moveTo(x + r, y);
|
|
1064
|
-
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
1065
|
-
ctx.arcTo(x + w, y + h, x, y + h, r);
|
|
1066
|
-
ctx.arcTo(x, y + h, x, y, r);
|
|
1067
|
-
ctx.arcTo(x, y, x + w, y, r);
|
|
1068
|
-
ctx.closePath();
|
|
1069
|
-
ctx.fill();
|
|
1070
|
-
ctx.stroke();
|
|
1071
|
-
ctx.restore();
|
|
1072
|
-
ctx.fillStyle = '#94a3b8';
|
|
1073
|
-
ctx.font = '11px sans-serif';
|
|
1074
|
-
ctx.fillText(g, x + 8, y + 14);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
nodes.forEach(n => {
|
|
1078
|
-
const sizeVal = (n.size || n.value || 1);
|
|
1079
|
-
const r = 6 + (sizeVal / 2);
|
|
1080
|
-
ctx.beginPath();
|
|
1081
|
-
ctx.fillStyle = n.color || '#60a5fa';
|
|
1082
|
-
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
|
1083
|
-
ctx.fill();
|
|
1084
|
-
|
|
1085
|
-
ctx.fillStyle = '#e2e8f0';
|
|
1086
|
-
ctx.font = '11px sans-serif';
|
|
1087
|
-
ctx.textAlign = 'center';
|
|
1088
|
-
ctx.fillText(n.label || n.id.split('/').slice(-1)[0], n.x, n.y + r + 12);
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
draw();
|
|
1093
|
-
</script>
|
|
1094
|
-
</body>
|
|
1095
|
-
</html>`;
|
|
1096
|
-
}
|
|
1097
1051
|
|
|
1098
1052
|
/**
|
|
1099
1053
|
* Generate timestamp for report filenames (YYYYMMDD-HHMMSS)
|
|
@@ -1351,7 +1305,7 @@ program
|
|
|
1351
1305
|
|
|
1352
1306
|
if (options.serve) {
|
|
1353
1307
|
try {
|
|
1354
|
-
const port =
|
|
1308
|
+
const port = typeof options.serve === 'number' ? options.serve : 5173;
|
|
1355
1309
|
const http = await import('http');
|
|
1356
1310
|
const fsp = await import('fs/promises');
|
|
1357
1311
|
const { exec } = await import('child_process');
|