aeo-ready 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +9 -62
- package/package.json +2 -2
- package/src/dashboard/generate.js +22 -48
- package/src/dashboard/sections/benchmark-details.js +79 -0
- package/src/dashboard/sections/history-table.js +10 -7
- package/src/dashboard/sections/overall-score.js +55 -118
- package/src/dashboard/sections/trend-chart.js +31 -46
- package/src/history/index.js +8 -15
- package/src/scan.js +59 -293
- package/src/checks/agent-readiness/actionable.js +0 -165
- package/src/checks/agent-readiness/capability.js +0 -209
- package/src/checks/agent-readiness/content-structure.js +0 -242
- package/src/checks/agent-readiness/discovery.js +0 -231
- package/src/checks/ai-visibility/authority.js +0 -195
- package/src/checks/ai-visibility/citation-readiness.js +0 -228
- package/src/checks/ai-visibility/freshness.js +0 -182
- package/src/checks/ai-visibility/structured-data.js +0 -180
- package/src/dashboard/sections/agent-readiness.js +0 -71
- package/src/dashboard/sections/ai-visibility.js +0 -67
- package/src/dashboard/sections/recommendations.js +0 -196
- package/src/fix/generators/agents-json.js +0 -73
- package/src/fix/generators/agents-md.js +0 -85
- package/src/fix/generators/llms-txt.js +0 -166
- package/src/fix/generators/robots-txt.js +0 -64
- package/src/fix/index.js +0 -177
- package/src/track/index.js +0 -167
- package/src/utils/detect-type.js +0 -99
- package/src/utils/tokens.js +0 -18
|
@@ -6,54 +6,42 @@ export function renderTrendChart(history) {
|
|
|
6
6
|
const scans = history.scans.slice(-20);
|
|
7
7
|
const width = 700;
|
|
8
8
|
const height = 200;
|
|
9
|
-
const
|
|
10
|
-
const chartW = width -
|
|
11
|
-
const chartH = height -
|
|
12
|
-
|
|
13
|
-
const maxScore = 100;
|
|
9
|
+
const pad = { top: 20, right: 20, bottom: 30, left: 40 };
|
|
10
|
+
const chartW = width - pad.left - pad.right;
|
|
11
|
+
const chartH = height - pad.top - pad.bottom;
|
|
14
12
|
const xStep = scans.length > 1 ? chartW / (scans.length - 1) : chartW;
|
|
15
13
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
const y = padding.top + chartH - (value / maxScore) * chartH;
|
|
19
|
-
return { x, y };
|
|
14
|
+
function toY(value) {
|
|
15
|
+
return pad.top + chartH - (value / 100) * chartH;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function polyline(values, color) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.join(" ");
|
|
29
|
-
return `<path d="${d}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function dots(values, color) {
|
|
33
|
-
return values
|
|
34
|
-
.map((v, i) => {
|
|
35
|
-
const p = toPoint(i, v);
|
|
36
|
-
return `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}"/>`;
|
|
37
|
-
})
|
|
38
|
-
.join("\n");
|
|
19
|
+
const pts = values.map(
|
|
20
|
+
(v, i) =>
|
|
21
|
+
`${i === 0 ? "M" : "L"} ${(pad.left + i * xStep).toFixed(1)} ${toY(v ?? 0).toFixed(1)}`,
|
|
22
|
+
);
|
|
23
|
+
return `<path d="${pts.join(" ")}" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
39
24
|
}
|
|
40
25
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
26
|
+
const agenticSeo = scans.map((s) => s.agenticSeo ?? 0);
|
|
27
|
+
const cloudflare = scans.map((s) =>
|
|
28
|
+
s.cloudflare != null && s.cloudflareMax
|
|
29
|
+
? Math.round((s.cloudflare / s.cloudflareMax) * 100)
|
|
30
|
+
: 0,
|
|
31
|
+
);
|
|
32
|
+
const fern = scans.map((s) => s.fern ?? 0);
|
|
44
33
|
|
|
45
|
-
const
|
|
34
|
+
const grid = [0, 25, 50, 75, 100]
|
|
46
35
|
.map((v) => {
|
|
47
|
-
const y =
|
|
48
|
-
return `<line x1="${
|
|
49
|
-
<text x="${padding.left - 8}" y="${y + 4}" text-anchor="end" fill="#8b949e" font-size="10">${v}</text>`;
|
|
36
|
+
const y = toY(v);
|
|
37
|
+
return `<line x1="${pad.left}" y1="${y}" x2="${width - pad.right}" y2="${y}" stroke="#21262d"/><text x="${pad.left - 8}" y="${y + 4}" text-anchor="end" fill="#8b949e" font-size="10">${v}</text>`;
|
|
50
38
|
})
|
|
51
39
|
.join("\n");
|
|
52
40
|
|
|
53
|
-
const
|
|
41
|
+
const labels = scans
|
|
54
42
|
.map((s, i) => {
|
|
55
43
|
if (scans.length <= 10 || i % Math.ceil(scans.length / 8) === 0) {
|
|
56
|
-
const x =
|
|
44
|
+
const x = pad.left + i * xStep;
|
|
57
45
|
const label = s.timestamp ? s.timestamp.slice(5, 10) : "";
|
|
58
46
|
return `<text x="${x}" y="${height - 5}" text-anchor="middle" fill="#8b949e" font-size="10">${label}</text>`;
|
|
59
47
|
}
|
|
@@ -61,18 +49,15 @@ export function renderTrendChart(history) {
|
|
|
61
49
|
})
|
|
62
50
|
.join("\n");
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
return `<h2 id="trends">Score Trends</h2>
|
|
65
53
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
66
|
-
${
|
|
67
|
-
${
|
|
68
|
-
${polyline(
|
|
69
|
-
${polyline(
|
|
70
|
-
${polyline(
|
|
71
|
-
${
|
|
72
|
-
<text x="${width -
|
|
73
|
-
<text x="${width -
|
|
74
|
-
<text x="${width - padding.right - 180}" y="${padding.top - 5}" text-anchor="end" fill="#d29922" font-size="10">Visibility (x2)</text>
|
|
54
|
+
${grid}
|
|
55
|
+
${labels}
|
|
56
|
+
${polyline(agenticSeo, "#f85149")}
|
|
57
|
+
${polyline(cloudflare, "#3fb950")}
|
|
58
|
+
${polyline(fern, "#79c0ff")}
|
|
59
|
+
<text x="${width - pad.right}" y="${pad.top - 5}" text-anchor="end" fill="#f85149" font-size="10">agentic-seo</text>
|
|
60
|
+
<text x="${width - pad.right - 90}" y="${pad.top - 5}" text-anchor="end" fill="#3fb950" font-size="10">Cloudflare</text>
|
|
61
|
+
<text x="${width - pad.right - 170}" y="${pad.top - 5}" text-anchor="end" fill="#79c0ff" font-size="10">Fern</text>
|
|
75
62
|
</svg>`;
|
|
76
|
-
|
|
77
|
-
return svg;
|
|
78
63
|
}
|
package/src/history/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
const DIR_NAME = ".
|
|
4
|
+
const DIR_NAME = ".aeo-ready";
|
|
5
5
|
const HISTORY_FILE = "history.json";
|
|
6
6
|
|
|
7
7
|
export async function saveResult(result, baseDir) {
|
|
@@ -14,17 +14,12 @@ export async function saveResult(result, baseDir) {
|
|
|
14
14
|
history.scans.push({
|
|
15
15
|
id: generateId(),
|
|
16
16
|
timestamp: result.timestamp,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
benchmarks: {
|
|
24
|
-
agenticSeo: result.benchmarks?.agenticSeo?.score ?? null,
|
|
25
|
-
cloudflare: result.benchmarks?.cloudflare?.score ?? null,
|
|
26
|
-
fern: result.benchmarks?.fern?.score ?? null,
|
|
27
|
-
},
|
|
17
|
+
url: result.url,
|
|
18
|
+
averageScore: result.averageScore,
|
|
19
|
+
agenticSeo: result.benchmarks?.agenticSeo?.score ?? null,
|
|
20
|
+
cloudflare: result.benchmarks?.cloudflare?.score ?? null,
|
|
21
|
+
cloudflareMax: result.benchmarks?.cloudflare?.maxScore ?? null,
|
|
22
|
+
fern: result.benchmarks?.fern?.score ?? null,
|
|
28
23
|
});
|
|
29
24
|
|
|
30
25
|
writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
@@ -34,9 +29,7 @@ export function loadHistory(historyPath) {
|
|
|
34
29
|
if (existsSync(historyPath)) {
|
|
35
30
|
try {
|
|
36
31
|
return JSON.parse(readFileSync(historyPath, "utf8"));
|
|
37
|
-
} catch {
|
|
38
|
-
/* corrupted — start fresh */
|
|
39
|
-
}
|
|
32
|
+
} catch {}
|
|
40
33
|
}
|
|
41
34
|
return { scans: [] };
|
|
42
35
|
}
|
package/src/scan.js
CHANGED
|
@@ -1,65 +1,38 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { fetchUrl, resolveUrl } from "./utils/fetch.js";
|
|
3
|
-
import { detectSiteType } from "./utils/detect-type.js";
|
|
4
|
-
import { runDiscoveryChecks } from "./checks/agent-readiness/discovery.js";
|
|
5
|
-
import { runContentStructureChecks } from "./checks/agent-readiness/content-structure.js";
|
|
6
|
-
import { runCapabilityChecks } from "./checks/agent-readiness/capability.js";
|
|
7
|
-
import { runActionableChecks } from "./checks/agent-readiness/actionable.js";
|
|
8
|
-
import { runStructuredDataChecks } from "./checks/ai-visibility/structured-data.js";
|
|
9
|
-
import { runCitationReadinessChecks } from "./checks/ai-visibility/citation-readiness.js";
|
|
10
|
-
import { runAuthorityChecks } from "./checks/ai-visibility/authority.js";
|
|
11
|
-
import { runFreshnessChecks } from "./checks/ai-visibility/freshness.js";
|
|
12
2
|
import { runAllBenchmarks, printBenchmarks } from "./benchmark/index.js";
|
|
13
3
|
import { saveResult } from "./history/index.js";
|
|
14
4
|
import { generateDashboard } from "./dashboard/generate.js";
|
|
15
|
-
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
16
|
-
import { join } from "path";
|
|
17
5
|
import { exec } from "child_process";
|
|
18
6
|
|
|
19
7
|
export async function scan(opts) {
|
|
20
|
-
const { url,
|
|
21
|
-
const mode = url ? "url" : "repo";
|
|
8
|
+
const { url, json } = opts;
|
|
22
9
|
|
|
23
10
|
if (!json) {
|
|
24
11
|
console.log(
|
|
25
|
-
chalk.bold("\n aeo-ready") + chalk.dim(" —
|
|
12
|
+
chalk.bold("\n aeo-ready") + chalk.dim(" — AEO benchmark aggregator\n"),
|
|
26
13
|
);
|
|
14
|
+
console.log(chalk.dim(` Scanning ${url}...\n`));
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const agentReadiness = await runAgentReadinessChecks(context);
|
|
36
|
-
const aiVisibility = await runAiVisibilityChecks(context);
|
|
37
|
-
|
|
38
|
-
const score = agentReadiness.score + aiVisibility.score;
|
|
39
|
-
const grade = scoreToGrade(score);
|
|
40
|
-
|
|
41
|
-
let benchmarkResult = null;
|
|
42
|
-
if (benchmark) {
|
|
43
|
-
benchmarkResult = await runAllBenchmarks(mode === "url" ? url : dir);
|
|
44
|
-
}
|
|
17
|
+
const benchmarks = await runAllBenchmarks(url);
|
|
18
|
+
const scores = collectScores(benchmarks);
|
|
19
|
+
const averageScore =
|
|
20
|
+
scores.length > 0
|
|
21
|
+
? Math.round(scores.reduce((s, v) => s + v, 0) / scores.length)
|
|
22
|
+
: 0;
|
|
45
23
|
|
|
46
24
|
const result = {
|
|
47
|
-
|
|
48
|
-
grade,
|
|
49
|
-
siteType: context.siteType,
|
|
50
|
-
mode,
|
|
51
|
-
target: url || dir,
|
|
25
|
+
url,
|
|
52
26
|
timestamp: new Date().toISOString(),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
benchmarks: benchmarkResult,
|
|
27
|
+
averageScore,
|
|
28
|
+
benchmarks,
|
|
56
29
|
};
|
|
57
30
|
|
|
58
31
|
if (!json) {
|
|
59
32
|
printReport(result);
|
|
60
33
|
}
|
|
61
34
|
|
|
62
|
-
const baseDir =
|
|
35
|
+
const baseDir = process.cwd();
|
|
63
36
|
await saveResult(result, baseDir);
|
|
64
37
|
|
|
65
38
|
if (!json) {
|
|
@@ -71,278 +44,71 @@ export async function scan(opts) {
|
|
|
71
44
|
return result;
|
|
72
45
|
}
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const homepage = await fetchUrl(url);
|
|
79
|
-
context.html = homepage.text;
|
|
80
|
-
context.pages.home = homepage;
|
|
81
|
-
|
|
82
|
-
const fetches = [
|
|
83
|
-
fetchUrl(resolveUrl(url, "/llms.txt")),
|
|
84
|
-
fetchUrl(resolveUrl(url, "/robots.txt")),
|
|
85
|
-
fetchUrl(resolveUrl(url, "/sitemap.xml")),
|
|
86
|
-
fetchUrl(resolveUrl(url, "/.well-known/agent.json")),
|
|
87
|
-
fetchUrl(resolveUrl(url, "/openapi.json")),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const [llms, robots, sitemap, agentJson, openapi] =
|
|
91
|
-
await Promise.all(fetches);
|
|
92
|
-
context.pages.llmsTxt = llms;
|
|
93
|
-
context.pages.robotsTxt = robots;
|
|
94
|
-
context.pages.sitemap = sitemap;
|
|
95
|
-
context.pages.agentJson = agentJson;
|
|
96
|
-
context.pages.openapi = openapi;
|
|
97
|
-
} else {
|
|
98
|
-
const fileList = listFiles(dir);
|
|
99
|
-
context.files = fileList;
|
|
100
|
-
context.fileContents = {};
|
|
101
|
-
for (const f of fileList) {
|
|
102
|
-
try {
|
|
103
|
-
context.fileContents[f] = readFileSync(join(dir, f), "utf8");
|
|
104
|
-
} catch {
|
|
105
|
-
/* skip binary/unreadable */
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const indexHtml = [
|
|
109
|
-
"index.html",
|
|
110
|
-
"public/index.html",
|
|
111
|
-
"dist/index.html",
|
|
112
|
-
].find((p) => existsSync(join(dir, p)));
|
|
113
|
-
if (indexHtml) {
|
|
114
|
-
context.html = readFileSync(join(dir, indexHtml), "utf8");
|
|
115
|
-
}
|
|
47
|
+
function collectScores(benchmarks) {
|
|
48
|
+
const scores = [];
|
|
49
|
+
if (benchmarks.agenticSeo?.available) {
|
|
50
|
+
scores.push(benchmarks.agenticSeo.score);
|
|
116
51
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return context;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function listFiles(dir) {
|
|
128
|
-
const skip = new Set([
|
|
129
|
-
"node_modules",
|
|
130
|
-
".git",
|
|
131
|
-
"dist",
|
|
132
|
-
"build",
|
|
133
|
-
".next",
|
|
134
|
-
".venv",
|
|
135
|
-
"__pycache__",
|
|
136
|
-
"target",
|
|
137
|
-
".aeo-ready",
|
|
138
|
-
]);
|
|
139
|
-
const results = [];
|
|
140
|
-
|
|
141
|
-
function walk(current, prefix) {
|
|
142
|
-
let entries;
|
|
143
|
-
try {
|
|
144
|
-
entries = readdirSync(join(dir, current), { withFileTypes: true });
|
|
145
|
-
} catch {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
for (const entry of entries) {
|
|
149
|
-
if (skip.has(entry.name)) continue;
|
|
150
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
151
|
-
if (entry.isDirectory()) {
|
|
152
|
-
walk(join(current, entry.name), rel);
|
|
153
|
-
} else {
|
|
154
|
-
results.push(rel);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
52
|
+
if (benchmarks.cloudflare?.available) {
|
|
53
|
+
scores.push(
|
|
54
|
+
Math.round(
|
|
55
|
+
(benchmarks.cloudflare.score / benchmarks.cloudflare.maxScore) * 100,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
157
58
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
async function runAgentReadinessChecks(context) {
|
|
164
|
-
const discovery = await runDiscoveryChecks(context);
|
|
165
|
-
const contentStructure = await runContentStructureChecks(context);
|
|
166
|
-
const capability = await runCapabilityChecks(context);
|
|
167
|
-
const actionable = await runActionableChecks(context);
|
|
168
|
-
|
|
169
|
-
const score =
|
|
170
|
-
discovery.score +
|
|
171
|
-
contentStructure.score +
|
|
172
|
-
capability.score +
|
|
173
|
-
actionable.score;
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
score,
|
|
177
|
-
maxScore: 50,
|
|
178
|
-
categories: { discovery, contentStructure, capability, actionable },
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function runAiVisibilityChecks(context) {
|
|
183
|
-
const structuredData = await runStructuredDataChecks(context);
|
|
184
|
-
const citationReadiness = await runCitationReadinessChecks(context);
|
|
185
|
-
const authority = await runAuthorityChecks(context);
|
|
186
|
-
const freshness = await runFreshnessChecks(context);
|
|
187
|
-
|
|
188
|
-
const score =
|
|
189
|
-
structuredData.score +
|
|
190
|
-
citationReadiness.score +
|
|
191
|
-
authority.score +
|
|
192
|
-
freshness.score;
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
score,
|
|
196
|
-
maxScore: 50,
|
|
197
|
-
categories: { structuredData, citationReadiness, authority, freshness },
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function scoreToGrade(score) {
|
|
202
|
-
if (score >= 80) return "A";
|
|
203
|
-
if (score >= 65) return "B";
|
|
204
|
-
if (score >= 50) return "C";
|
|
205
|
-
if (score >= 35) return "D";
|
|
206
|
-
return "F";
|
|
59
|
+
if (benchmarks.fern?.available) {
|
|
60
|
+
scores.push(benchmarks.fern.score);
|
|
61
|
+
}
|
|
62
|
+
return scores;
|
|
207
63
|
}
|
|
208
64
|
|
|
209
65
|
function printReport(result) {
|
|
210
|
-
const {
|
|
66
|
+
const { benchmarks, averageScore } = result;
|
|
211
67
|
|
|
212
|
-
|
|
213
|
-
grade === "A"
|
|
214
|
-
? chalk.green
|
|
215
|
-
: grade === "B"
|
|
216
|
-
? chalk.cyan
|
|
217
|
-
: grade === "C"
|
|
218
|
-
? chalk.yellow
|
|
219
|
-
: chalk.red;
|
|
220
|
-
|
|
221
|
-
console.log(
|
|
222
|
-
` ${bar(score, 100, 40)} ${gradeColor.bold(grade)} ${score}/100\n`,
|
|
223
|
-
);
|
|
68
|
+
printBenchmarks(benchmarks);
|
|
224
69
|
|
|
70
|
+
const gc =
|
|
71
|
+
averageScore >= 80
|
|
72
|
+
? chalk.green
|
|
73
|
+
: averageScore >= 50
|
|
74
|
+
? chalk.yellow
|
|
75
|
+
: chalk.red;
|
|
225
76
|
console.log(
|
|
226
|
-
chalk.dim(
|
|
227
|
-
" Agent Readiness — can AI agents find, read, and use your site?",
|
|
228
|
-
),
|
|
77
|
+
chalk.dim(" ─────────────────────────────────────────────────\n"),
|
|
229
78
|
);
|
|
230
79
|
console.log(
|
|
231
|
-
|
|
232
|
-
" AI Visibility — how accurately do AI search engines describe you?",
|
|
233
|
-
),
|
|
80
|
+
` Average across all sources: ${gc.bold(`${averageScore}/100`)}\n`,
|
|
234
81
|
);
|
|
235
82
|
|
|
236
|
-
if (
|
|
83
|
+
if (averageScore < 80) {
|
|
84
|
+
console.log(chalk.bold(" Fix it:\n"));
|
|
237
85
|
console.log(
|
|
238
|
-
chalk.dim(
|
|
239
|
-
"
|
|
240
|
-
),
|
|
86
|
+
chalk.dim(" npx agentic-seo init") +
|
|
87
|
+
" scaffold llms.txt, AGENTS.md, skill.md",
|
|
241
88
|
);
|
|
242
|
-
console.log(chalk.dim(" Fix the agent side first. Visibility follows."));
|
|
243
|
-
}
|
|
244
|
-
console.log("");
|
|
245
89
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const passed = allChecks.filter((c) => c.passed).length;
|
|
254
|
-
const failed = allChecks.filter((c) => !c.passed && c.points === 0).length;
|
|
255
|
-
const partial = allChecks.length - passed - failed;
|
|
256
|
-
|
|
257
|
-
console.log(
|
|
258
|
-
chalk.dim(
|
|
259
|
-
` ${chalk.green(passed + " passed")} · ${partial ? chalk.yellow(partial + " partial") + " · " : ""}${chalk.red(failed + " failed")} · ${allChecks.length} checks total\n`,
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (benchmarks) {
|
|
264
|
-
printBenchmarks(benchmarks);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (failed + partial > 0) {
|
|
268
|
-
console.log(chalk.bold(" Next steps:\n"));
|
|
269
|
-
const topFixes = allChecks
|
|
270
|
-
.filter((c) => !c.passed && c.fix)
|
|
271
|
-
.sort(
|
|
272
|
-
(a, b) =>
|
|
273
|
-
b.maxPoints - (b.points || 0) - (a.maxPoints - (a.points || 0)),
|
|
274
|
-
)
|
|
275
|
-
.slice(0, 3);
|
|
276
|
-
for (const fix of topFixes) {
|
|
277
|
-
console.log(chalk.dim(` - ${fix.fix}`));
|
|
90
|
+
const cfFails =
|
|
91
|
+
benchmarks.cloudflare?.checks?.filter((c) => c.status === "fail") || [];
|
|
92
|
+
for (const fail of cfFails.slice(0, 2)) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.dim(` Cloudflare: ${fail.id}`) +
|
|
95
|
+
chalk.dim(` — see isitagentready.com for fix`),
|
|
96
|
+
);
|
|
278
97
|
}
|
|
279
|
-
console.log(
|
|
280
|
-
chalk.dim(
|
|
281
|
-
`\n Run ${chalk.bold("npx aeo-ready scan --fix")} to auto-fix what we can.`,
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
console.log(
|
|
285
|
-
chalk.dim(
|
|
286
|
-
" Open the dashboard for step-by-step guides on everything else.\n",
|
|
287
|
-
),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function printScorecard(name, scorecard) {
|
|
293
|
-
const pct = Math.round((scorecard.score / scorecard.maxScore) * 100);
|
|
294
|
-
console.log(
|
|
295
|
-
chalk.bold(` ${name}`) +
|
|
296
|
-
chalk.dim(` ${scorecard.score}/${scorecard.maxScore} (${pct}%)`),
|
|
297
|
-
);
|
|
298
98
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
console.log(
|
|
309
|
-
` ${icon} ${label.padEnd(28)} ${bar(cat.score, cat.maxScore, 20)} ${chalk.dim(`${cat.score}/${cat.maxScore} (${catPct}%)`)}`,
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
for (const check of cat.checks) {
|
|
313
|
-
if (check.passed) {
|
|
314
|
-
console.log(
|
|
315
|
-
chalk.green(` +`) +
|
|
316
|
-
chalk.dim(` ${check.name} [${check.points}]`),
|
|
317
|
-
);
|
|
318
|
-
} else {
|
|
319
|
-
console.log(
|
|
320
|
-
chalk.red(` -`) +
|
|
321
|
-
` ${check.name} ` +
|
|
322
|
-
chalk.dim(`[${check.points || 0}/${check.maxPoints}]`),
|
|
323
|
-
);
|
|
324
|
-
if (check.fix) {
|
|
325
|
-
console.log(chalk.dim(` ${check.fix}`));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
99
|
+
const fernFails =
|
|
100
|
+
benchmarks.fern?.checks?.filter(
|
|
101
|
+
(c) => c.status === "fail" || c.status === "warn",
|
|
102
|
+
) || [];
|
|
103
|
+
if (fernFails.length > 0) {
|
|
104
|
+
console.log(
|
|
105
|
+
chalk.dim(` Fern: ${fernFails.length} issues`) +
|
|
106
|
+
chalk.dim(` — run npx afdocs ${result.url}`),
|
|
107
|
+
);
|
|
328
108
|
}
|
|
329
|
-
}
|
|
330
|
-
console.log("");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function bar(value, max, width) {
|
|
334
|
-
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
335
|
-
const empty = width - filled;
|
|
336
|
-
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
337
|
-
const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
|
|
338
|
-
return color("█".repeat(filled)) + chalk.dim("░".repeat(empty));
|
|
339
|
-
}
|
|
340
109
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
.replace(/([A-Z])/g, " $1")
|
|
344
|
-
.replace(/^./, (c) => c.toUpperCase())
|
|
345
|
-
.trim();
|
|
110
|
+
console.log(chalk.dim("\n Fix, then re-scan to track improvement.\n"));
|
|
111
|
+
}
|
|
346
112
|
}
|
|
347
113
|
|
|
348
114
|
function openInBrowser(filePath) {
|