auditkit 0.1.0 → 0.1.3
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/Cargo.lock +501 -6
- package/Cargo.toml +6 -1
- package/LICENSE +21 -0
- package/README.md +4 -23
- package/audits/.gitkeep +1 -0
- package/package.json +15 -17
- package/scripts/auditkit/ak +10 -9
- package/scripts/auditkit/lighthouse-runner.mjs +148 -20
- package/scripts/auditkit/lighthouse.test.mjs +17 -0
- package/scripts/auditkit/postinstall.mjs +71 -31
- package/scripts/lighthouse.mjs +38 -0
- package/src/html_check.rs +113 -5
- package/src/lighthouse.rs +8 -5
- package/src/main.rs +78 -54
- package/src/report.rs +58 -5
- package/src/security.rs +94 -10
- package/src/templates.rs +24 -75
- package/src/ui.rs +533 -23
- package/src/workspace.rs +75 -0
package/Cargo.toml
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "auditkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
edition = "2021"
|
|
5
|
+
description = "Local hybrid CLI for agency website audits."
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/Danilaa1/auditkit"
|
|
5
8
|
|
|
6
9
|
[dependencies]
|
|
7
10
|
anyhow = "1.0"
|
|
8
11
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
9
12
|
clap = { version = "4.5", features = ["derive"] }
|
|
10
13
|
colored = "3.0"
|
|
14
|
+
crossterm = "0.29.0"
|
|
11
15
|
indicatif = "0.18"
|
|
16
|
+
ratatui = { version = "0.30.0", default-features = false, features = ["crossterm"] }
|
|
12
17
|
regex = "1.12"
|
|
13
18
|
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
|
14
19
|
serde_json = "1.0"
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,32 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Local hybrid CLI for agency website audits.
|
|
4
4
|
|
|
5
|
-
Rust runs the core workflow: audit folders, quick HTML checks, security checks, and report generation. Node is used only for Lighthouse
|
|
5
|
+
Rust runs the core workflow: audit folders, quick HTML checks, security checks, and report generation. Node is used only for Lighthouse.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
bun install -g auditkit
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
NPM also works:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install -g auditkit
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Run without installing globally:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
bunx auditkit --help
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Local development install from this project folder:
|
|
9
|
+
From this project folder:
|
|
28
10
|
|
|
29
11
|
```bash
|
|
30
12
|
npm install
|
|
13
|
+
cargo build
|
|
31
14
|
npm link
|
|
32
15
|
```
|
|
33
16
|
|
|
@@ -48,9 +31,7 @@ ak report latest
|
|
|
48
31
|
`ak new` creates a workspace in `audits/`. Fill in:
|
|
49
32
|
|
|
50
33
|
- `findings.md`
|
|
51
|
-
- `
|
|
52
|
-
- `pages/*.md`
|
|
53
|
-
- `raw-notes.md`
|
|
34
|
+
- `workspace.md`
|
|
54
35
|
|
|
55
36
|
Then `ak report latest` creates:
|
|
56
37
|
|
package/audits/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/package.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auditkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local hybrid CLI for agency website audits.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/Danilaa1/auditkit.git"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"Cargo.toml",
|
|
13
12
|
"Cargo.lock",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"Cargo.toml",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
16
16
|
"auditkit.config.json",
|
|
17
|
-
"
|
|
17
|
+
"audits/.gitkeep",
|
|
18
|
+
"docs",
|
|
19
|
+
"scripts",
|
|
20
|
+
"src"
|
|
18
21
|
],
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=24"
|
|
22
|
+
"bin": {
|
|
23
|
+
"auditkit": "scripts/auditkit/ak",
|
|
24
|
+
"ak": "scripts/auditkit/ak"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"audit": "cargo run --quiet --",
|
|
@@ -32,9 +33,6 @@
|
|
|
32
33
|
"audit:lighthouse": "cargo run --quiet -- lighthouse",
|
|
33
34
|
"audit:inspect": "cargo run --quiet -- inspect",
|
|
34
35
|
"postinstall": "node scripts/auditkit/postinstall.mjs",
|
|
35
|
-
"site:dev": "npm --prefix site run dev",
|
|
36
|
-
"site:build": "npm --prefix site run build",
|
|
37
|
-
"site:preview": "npm --prefix site run preview",
|
|
38
36
|
"test": "npm run test:node && npm run test:rust",
|
|
39
37
|
"test:node": "node --test",
|
|
40
38
|
"test:rust": "cargo test"
|
package/scripts/auditkit/ak
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
set -
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
SOURCE="$0"
|
|
5
5
|
while [ -L "$SOURCE" ]; do
|
|
6
|
-
DIR="$(
|
|
6
|
+
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
7
7
|
TARGET="$(readlink "$SOURCE")"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
if [[ "$TARGET" == /* ]]; then
|
|
9
|
+
SOURCE="$TARGET"
|
|
10
|
+
else
|
|
11
|
+
SOURCE="$DIR/$TARGET"
|
|
12
|
+
fi
|
|
12
13
|
done
|
|
13
14
|
|
|
14
|
-
SCRIPT_DIR="$(
|
|
15
|
-
ROOT_DIR="$(
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
16
|
+
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
16
17
|
BINARY="$ROOT_DIR/target/debug/auditkit"
|
|
17
18
|
|
|
18
19
|
if [ ! -x "$BINARY" ]; then
|
|
@@ -25,16 +25,135 @@ const browserCandidates = [
|
|
|
25
25
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const colorEnabled = !process.env.NO_COLOR;
|
|
29
|
+
|
|
30
|
+
function color(code, value) {
|
|
31
|
+
return colorEnabled ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function positive(value) {
|
|
35
|
+
return color("1;32", value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function warning(value) {
|
|
39
|
+
return color("1;33", value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function critical(value) {
|
|
43
|
+
return color("1;31", value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function frame(value) {
|
|
47
|
+
return color("1;36", value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dim(value) {
|
|
51
|
+
return color("2", value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toneIcon(tone) {
|
|
55
|
+
if (tone === "positive") {
|
|
56
|
+
return positive("✓");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tone === "warning") {
|
|
60
|
+
return warning("◆");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return critical("●");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scoreTone(score) {
|
|
67
|
+
if (typeof score !== "number") {
|
|
68
|
+
return "warning";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (score >= 85) {
|
|
72
|
+
return "positive";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (score >= 65) {
|
|
76
|
+
return "warning";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return "critical";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function numericMetric(value) {
|
|
83
|
+
const match = String(value).match(/[0-9]+(?:\.[0-9]+)?/);
|
|
84
|
+
return match ? Number(match[0]) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lcpTone(value) {
|
|
88
|
+
const metric = numericMetric(value);
|
|
89
|
+
if (metric === null) {
|
|
90
|
+
return "warning";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (metric <= 2.5) {
|
|
94
|
+
return "positive";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (metric <= 4) {
|
|
98
|
+
return "warning";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return "critical";
|
|
30
102
|
}
|
|
31
103
|
|
|
32
|
-
function
|
|
33
|
-
|
|
104
|
+
function clsTone(value) {
|
|
105
|
+
const metric = numericMetric(value);
|
|
106
|
+
if (metric === null) {
|
|
107
|
+
return "warning";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (metric <= 0.1) {
|
|
111
|
+
return "positive";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (metric <= 0.25) {
|
|
115
|
+
return "warning";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return "critical";
|
|
34
119
|
}
|
|
35
120
|
|
|
36
|
-
function
|
|
37
|
-
|
|
121
|
+
function tbtTone(value) {
|
|
122
|
+
const metric = numericMetric(value);
|
|
123
|
+
if (metric === null) {
|
|
124
|
+
return "warning";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (metric <= 200) {
|
|
128
|
+
return "positive";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (metric <= 600) {
|
|
132
|
+
return "warning";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return "critical";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toneValue(tone, value) {
|
|
139
|
+
if (tone === "positive") {
|
|
140
|
+
return positive(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (tone === "warning") {
|
|
144
|
+
return warning(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return critical(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function signalLine(label, value, tone) {
|
|
151
|
+
return ` ${toneIcon(tone)} ${dim(label.padEnd(21))} ${toneValue(tone, value)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function feedbackLine(tone, value) {
|
|
155
|
+
const label = tone === "critical" ? critical("FIX") : warning("WATCH");
|
|
156
|
+
return ` ${toneIcon(tone)} ${label} ${toneValue(tone, value)}`;
|
|
38
157
|
}
|
|
39
158
|
|
|
40
159
|
export function findBrowserPath({ env = process.env, config = {}, exists = existsSync } = {}) {
|
|
@@ -92,21 +211,30 @@ export function summarizeLighthouse(lhr) {
|
|
|
92
211
|
}
|
|
93
212
|
|
|
94
213
|
export function formatLighthouseCli(summary) {
|
|
214
|
+
const opportunityLines = summary.opportunities.length
|
|
215
|
+
? summary.opportunities.map((item) =>
|
|
216
|
+
feedbackLine(
|
|
217
|
+
item.score < 50 ? "critical" : "warning",
|
|
218
|
+
`${item.title}${item.displayValue ? ` — ${item.displayValue}` : ""}`,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
: [signalLine("Opportunities", "No major Lighthouse opportunities found.", "positive")];
|
|
222
|
+
|
|
95
223
|
return [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
224
|
+
"",
|
|
225
|
+
frame("╭─ Lighthouse Check"),
|
|
226
|
+
`${frame("│ URL ")} ${summary.url}`,
|
|
227
|
+
frame("╰─ Signals"),
|
|
228
|
+
signalLine("Performance", `${summary.scores.performance ?? "n/a"}/100`, scoreTone(summary.scores.performance)),
|
|
229
|
+
signalLine("Accessibility", `${summary.scores.accessibility ?? "n/a"}/100`, scoreTone(summary.scores.accessibility)),
|
|
230
|
+
signalLine("Best practices", `${summary.scores.bestPractices ?? "n/a"}/100`, scoreTone(summary.scores.bestPractices)),
|
|
231
|
+
signalLine("SEO", `${summary.scores.seo ?? "n/a"}/100`, scoreTone(summary.scores.seo)),
|
|
232
|
+
signalLine("LCP", summary.vitals.lcp, lcpTone(summary.vitals.lcp)),
|
|
233
|
+
signalLine("CLS", summary.vitals.cls, clsTone(summary.vitals.cls)),
|
|
234
|
+
signalLine("TBT", summary.vitals.tbt, tbtTone(summary.vitals.tbt)),
|
|
235
|
+
"",
|
|
236
|
+
frame("Feedback"),
|
|
237
|
+
...opportunityLines,
|
|
110
238
|
].join("\n");
|
|
111
239
|
}
|
|
112
240
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatLighthouseReport,
|
|
6
6
|
summarizeLighthouse,
|
|
7
7
|
} from "./lighthouse-runner.mjs";
|
|
8
|
+
import { shouldShowWelcome, welcomeMessage } from "./postinstall.mjs";
|
|
8
9
|
|
|
9
10
|
const lhr = {
|
|
10
11
|
finalDisplayedUrl: "https://example.com/",
|
|
@@ -76,3 +77,19 @@ test("findBrowserPath falls back to Helium app path", () => {
|
|
|
76
77
|
"/Applications/Helium.app/Contents/MacOS/Helium",
|
|
77
78
|
);
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
test("postinstall welcome explains first commands", () => {
|
|
82
|
+
const message = welcomeMessage();
|
|
83
|
+
const plainMessage = welcomeMessage({ NO_COLOR: "1" });
|
|
84
|
+
|
|
85
|
+
assert.match(message, /Audit Kit/);
|
|
86
|
+
assert.match(message, /ak new/);
|
|
87
|
+
assert.match(message, /ak inspect latest/);
|
|
88
|
+
assert.match(message, /\x1b\[/);
|
|
89
|
+
assert.doesNotMatch(plainMessage, /\x1b\[/);
|
|
90
|
+
assert.match(plainMessage, /Audit Kit/);
|
|
91
|
+
assert.equal(shouldShowWelcome({}), true);
|
|
92
|
+
assert.equal(shouldShowWelcome({ CI: "true" }), false);
|
|
93
|
+
assert.equal(shouldShowWelcome({ AUDITKIT_SKIP_WELCOME: "1" }), false);
|
|
94
|
+
assert.equal(shouldShowWelcome({ npm_config_loglevel: "silent" }), false);
|
|
95
|
+
});
|
|
@@ -1,31 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
export function shouldShowWelcome(env = process.env) {
|
|
4
|
+
return !env.CI && !env.AUDITKIT_SKIP_WELCOME && env.npm_config_loglevel !== "silent";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function welcomeMessage(env = process.env) {
|
|
8
|
+
const colors = !env.NO_COLOR;
|
|
9
|
+
const rows = [
|
|
10
|
+
line(" ", colors),
|
|
11
|
+
line(` ${paint("Audit Kit", 96, colors)} `, colors),
|
|
12
|
+
line(` ${paint("Local website audits from your terminal.", 90, colors)} `, colors),
|
|
13
|
+
line(" ", colors),
|
|
14
|
+
divider(colors),
|
|
15
|
+
line(` ${paint("Try first", 97, colors)} `, colors),
|
|
16
|
+
line(" ", colors),
|
|
17
|
+
commandLine("ak new", "create an audit workspace", colors),
|
|
18
|
+
commandLine("ak inspect latest", "run check, security, Lighthouse", colors),
|
|
19
|
+
commandLine("ak report latest", "generate report + client email", colors),
|
|
20
|
+
commandLine("ak list", "show saved audits", colors),
|
|
21
|
+
line(" ", colors),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
"",
|
|
26
|
+
glow(" · ✦ · · · ✧ · · · ✦ ·", colors),
|
|
27
|
+
`${corner("╭", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${corner("╮", colors)}`,
|
|
28
|
+
...rows,
|
|
29
|
+
`${corner("╰", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${corner("╯", colors)}`,
|
|
30
|
+
glow(" · · ✧ · · · ✦ · ·", colors),
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href && shouldShowWelcome()) {
|
|
35
|
+
console.log(welcomeMessage());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commandLine(command, description, colors) {
|
|
39
|
+
return line(` ${paint(command.padEnd(18), 36, colors)} ${paint(description.padEnd(34), 90, colors)} `, colors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function line(content, colors) {
|
|
43
|
+
return `${border("│", colors)}${content}${border("│", colors)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function divider(colors) {
|
|
47
|
+
return `${border("├", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${border("┤", colors)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function gradient(value, colors) {
|
|
51
|
+
const palette = [36, 96, 35, 93, 95, 94];
|
|
52
|
+
return [...value]
|
|
53
|
+
.map((character, index) => paint(character, palette[index % palette.length], colors))
|
|
54
|
+
.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function border(value, colors) {
|
|
58
|
+
return paint(value, 96, colors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function corner(value, colors) {
|
|
62
|
+
return paint(value, 95, colors);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function glow(value, colors) {
|
|
66
|
+
return paint(value, 90, colors);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function paint(value, code, colors) {
|
|
70
|
+
return colors ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
71
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
formatLighthouseCli,
|
|
7
|
+
formatLighthouseReport,
|
|
8
|
+
runLighthouse,
|
|
9
|
+
} from "./auditkit/lighthouse-runner.mjs";
|
|
10
|
+
|
|
11
|
+
const url = process.argv[2];
|
|
12
|
+
const outIndex = process.argv.indexOf("--out");
|
|
13
|
+
const outDir = outIndex >= 0 ? process.argv[outIndex + 1] : null;
|
|
14
|
+
|
|
15
|
+
if (!url) {
|
|
16
|
+
console.error("Usage: node scripts/lighthouse.mjs <url> [--out <folder>]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await runLighthouse(url);
|
|
22
|
+
console.log(formatLighthouseCli(result.summary));
|
|
23
|
+
|
|
24
|
+
const folder = outDir ?? process.cwd();
|
|
25
|
+
await mkdir(folder, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const markdownPath = path.join(folder, "lighthouse.md");
|
|
28
|
+
const jsonPath = path.join(folder, "lighthouse.json");
|
|
29
|
+
|
|
30
|
+
await writeFile(markdownPath, formatLighthouseReport(result.summary), "utf8");
|
|
31
|
+
await writeFile(jsonPath, result.json, "utf8");
|
|
32
|
+
|
|
33
|
+
console.log(`LIGHTHOUSE_MD: ${markdownPath}`);
|
|
34
|
+
console.log(`LIGHTHOUSE_JSON: ${jsonPath}`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
package/src/html_check.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
use regex::Regex;
|
|
3
3
|
|
|
4
|
-
use crate::ui::score_status;
|
|
4
|
+
use crate::ui::{self, score_status, FeedbackTone};
|
|
5
5
|
|
|
6
6
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
7
7
|
pub struct HtmlCheck {
|
|
@@ -201,13 +201,65 @@ pub fn check_url(input_url: &str) -> Result<HtmlCheck> {
|
|
|
201
201
|
|
|
202
202
|
pub fn format_cli(result: &HtmlCheck) -> String {
|
|
203
203
|
let mut output = format!(
|
|
204
|
-
"\n
|
|
204
|
+
"\n{}\n{} {}\n{} {}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n\n{}\n",
|
|
205
|
+
ui::frame_line("╭─ Audit Kit Check"),
|
|
206
|
+
ui::frame_line("│ URL "),
|
|
207
|
+
result.url,
|
|
208
|
+
ui::frame_line("│ Score"),
|
|
209
|
+
ui::score_badge(result.score),
|
|
210
|
+
ui::frame_line("╰─ Signals"),
|
|
211
|
+
ui::signal_line("Status", result.status, status_tone(result.status)),
|
|
212
|
+
ui::signal_line(
|
|
213
|
+
"Response",
|
|
214
|
+
format!("{} bytes", result.bytes),
|
|
215
|
+
response_tone(result.bytes)
|
|
216
|
+
),
|
|
217
|
+
ui::signal_line("Title", title_value(result), title_tone(result)),
|
|
218
|
+
ui::signal_line(
|
|
219
|
+
"Meta description",
|
|
220
|
+
if result.meta_description_present {
|
|
221
|
+
"present"
|
|
222
|
+
} else {
|
|
223
|
+
"missing"
|
|
224
|
+
},
|
|
225
|
+
if result.meta_description_present {
|
|
226
|
+
FeedbackTone::Positive
|
|
227
|
+
} else {
|
|
228
|
+
FeedbackTone::Critical
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
ui::signal_line("H1 count", result.h1_count, h1_tone(result.h1_count)),
|
|
232
|
+
ui::signal_line(
|
|
233
|
+
"Images missing alt",
|
|
234
|
+
format!("{}/{}", result.image_missing_alt, result.image_total),
|
|
235
|
+
if result.image_missing_alt == 0 {
|
|
236
|
+
FeedbackTone::Positive
|
|
237
|
+
} else {
|
|
238
|
+
FeedbackTone::Critical
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
ui::frame_line("Feedback")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
for item in &result.feedback {
|
|
245
|
+
output.push_str(&format!(
|
|
246
|
+
"{}\n",
|
|
247
|
+
ui::feedback_line(feedback_tone(item), item)
|
|
248
|
+
));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
output
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
pub fn format_markdown(result: &HtmlCheck) -> String {
|
|
255
|
+
let mut output = format!(
|
|
256
|
+
"# Automated Check\n\nURL: {}\nScore: {}/100 ({})\n\n## Signals\n\n- Status: {}\n- Response: {} bytes\n- Title: {}\n- Meta description: {}\n- H1 count: {}\n- Images missing alt: {}/{}\n\n## Feedback\n\n",
|
|
205
257
|
result.url,
|
|
206
258
|
result.score,
|
|
207
259
|
score_status(result.score),
|
|
208
260
|
result.status,
|
|
209
261
|
result.bytes,
|
|
210
|
-
|
|
262
|
+
title_value(result),
|
|
211
263
|
if result.meta_description_present { "present" } else { "missing" },
|
|
212
264
|
result.h1_count,
|
|
213
265
|
result.image_missing_alt,
|
|
@@ -221,8 +273,64 @@ pub fn format_cli(result: &HtmlCheck) -> String {
|
|
|
221
273
|
output
|
|
222
274
|
}
|
|
223
275
|
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
fn title_value(result: &HtmlCheck) -> &str {
|
|
277
|
+
if result.title.is_empty() {
|
|
278
|
+
"missing"
|
|
279
|
+
} else {
|
|
280
|
+
&result.title
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn status_tone(status: u16) -> FeedbackTone {
|
|
285
|
+
if (200..400).contains(&status) {
|
|
286
|
+
FeedbackTone::Positive
|
|
287
|
+
} else {
|
|
288
|
+
FeedbackTone::Critical
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn response_tone(bytes: usize) -> FeedbackTone {
|
|
293
|
+
if bytes > 200_000 {
|
|
294
|
+
FeedbackTone::Warning
|
|
295
|
+
} else {
|
|
296
|
+
FeedbackTone::Positive
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn title_tone(result: &HtmlCheck) -> FeedbackTone {
|
|
301
|
+
if result.title.is_empty() {
|
|
302
|
+
FeedbackTone::Critical
|
|
303
|
+
} else if result.title.len() < 10 || result.title.len() > 65 {
|
|
304
|
+
FeedbackTone::Warning
|
|
305
|
+
} else {
|
|
306
|
+
FeedbackTone::Positive
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn h1_tone(count: usize) -> FeedbackTone {
|
|
311
|
+
match count {
|
|
312
|
+
1 => FeedbackTone::Positive,
|
|
313
|
+
0 => FeedbackTone::Critical,
|
|
314
|
+
_ => FeedbackTone::Warning,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn feedback_tone(value: &str) -> FeedbackTone {
|
|
319
|
+
let value = value.to_lowercase();
|
|
320
|
+
if value.starts_with("missing")
|
|
321
|
+
|| value.starts_with("no obvious")
|
|
322
|
+
|| value.contains("missing alt")
|
|
323
|
+
{
|
|
324
|
+
FeedbackTone::Critical
|
|
325
|
+
} else if value.starts_with("no canonical")
|
|
326
|
+
|| value.starts_with("title length")
|
|
327
|
+
|| value.starts_with("multiple")
|
|
328
|
+
|| value.contains("large")
|
|
329
|
+
{
|
|
330
|
+
FeedbackTone::Warning
|
|
331
|
+
} else {
|
|
332
|
+
FeedbackTone::Positive
|
|
333
|
+
}
|
|
226
334
|
}
|
|
227
335
|
|
|
228
336
|
#[cfg(test)]
|