@randomcode-seolint/cli 0.1.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/dist/index.js +117 -0
- package/package.json +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const API_BASE = process.env.SEOLINT_API_URL ?? "https://seolint.dev";
|
|
3
|
+
const API_KEY = process.env.SEOLINT_API_KEY ?? "";
|
|
4
|
+
function headers() {
|
|
5
|
+
const h = { "Content-Type": "application/json" };
|
|
6
|
+
if (API_KEY)
|
|
7
|
+
h["Authorization"] = `Bearer ${API_KEY}`;
|
|
8
|
+
return h;
|
|
9
|
+
}
|
|
10
|
+
function printUsage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
seolint — scan any website for SEO issues
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
seolint scan <url> Scan a URL and print the report
|
|
16
|
+
seolint scan <url> --json Output raw JSON instead of markdown
|
|
17
|
+
seolint help Show this message
|
|
18
|
+
|
|
19
|
+
Environment:
|
|
20
|
+
SEOLINT_API_KEY Your API key (get one at seolint.dev/dashboard)
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
seolint scan https://example.com
|
|
24
|
+
SEOLINT_API_KEY=sl_xxx seolint scan https://mysite.com --json
|
|
25
|
+
npx seolint scan https://example.com
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
async function scan(url, json) {
|
|
29
|
+
// Normalise URL
|
|
30
|
+
let fullUrl = url;
|
|
31
|
+
if (!/^https?:\/\//i.test(fullUrl))
|
|
32
|
+
fullUrl = `https://${fullUrl}`;
|
|
33
|
+
process.stderr.write(`Scanning ${fullUrl}...\n`);
|
|
34
|
+
// Start scan
|
|
35
|
+
const startRes = await fetch(`${API_BASE}/api/v1/scan`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: headers(),
|
|
38
|
+
body: JSON.stringify({ url: fullUrl }),
|
|
39
|
+
});
|
|
40
|
+
if (!startRes.ok) {
|
|
41
|
+
const err = await startRes.json().catch(() => ({}));
|
|
42
|
+
console.error(`Error: ${err.error ?? startRes.statusText}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const { scanId } = await startRes.json();
|
|
46
|
+
// Poll
|
|
47
|
+
const maxWait = 90_000;
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
while (Date.now() - start < maxWait) {
|
|
50
|
+
const res = await fetch(`${API_BASE}/api/v1/scan/${scanId}`, { headers: headers() });
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
console.error(`Poll error: ${res.status}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
if (data.status === "complete") {
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(JSON.stringify(data.issues ?? [], null, 2));
|
|
59
|
+
}
|
|
60
|
+
else if (data.markdown) {
|
|
61
|
+
console.log(data.markdown);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Fallback: format issues as text
|
|
65
|
+
const issues = (data.issues ?? []);
|
|
66
|
+
if (issues.length === 0) {
|
|
67
|
+
console.log("No issues found. The site looks good!");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(`\n${issues.length} issues found:\n`);
|
|
71
|
+
for (const issue of issues) {
|
|
72
|
+
console.log(` [${issue.severity.toUpperCase()}] ${issue.title}`);
|
|
73
|
+
console.log(` ${issue.description}`);
|
|
74
|
+
console.log(` Fix: ${issue.fix}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
process.stderr.write(`Done. Report: ${API_BASE}/scan/${scanId}\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (data.status === "error") {
|
|
82
|
+
console.error(`Scan failed: ${data.error_message ?? "Unknown error"}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
// Still pending
|
|
86
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
87
|
+
process.stderr.write(`\r ${elapsed}s elapsed...`);
|
|
88
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
89
|
+
}
|
|
90
|
+
console.error("Scan timed out after 90 seconds");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
// Parse args
|
|
94
|
+
const args = process.argv.slice(2);
|
|
95
|
+
const command = args[0];
|
|
96
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
97
|
+
printUsage();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
if (command === "scan") {
|
|
101
|
+
const url = args[1];
|
|
102
|
+
if (!url) {
|
|
103
|
+
console.error("Error: URL required. Usage: seolint scan <url>");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const json = args.includes("--json");
|
|
107
|
+
scan(url, json).catch((err) => {
|
|
108
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.error(`Unknown command: ${command}`);
|
|
114
|
+
printUsage();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@randomcode-seolint/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scan any website for SEO issues from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"seolint": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["seo", "audit", "cli", "seolint", "ai", "claude"],
|
|
15
|
+
"author": "Random Code",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.5.0",
|
|
19
|
+
"@types/node": "^22.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|