squirrelscan 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/bin/squirrel.js +50 -0
- package/lib/platform.js +101 -0
- package/package.json +50 -0
- package/scripts/postinstall.js +164 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# squirrelscan
|
|
4
|
+
|
|
5
|
+
**CLI Website Audits for Humans, Agents & LLMs**
|
|
6
|
+
|
|
7
|
+
A comprehensive website audit tool for SEO, performance, accessibility, content, and more. Built from the ground up for AI coding agents and developer workflows.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **149+ Audit Rules** across 20 categories (SEO, accessibility, performance, security, E-E-A-T)
|
|
12
|
+
- **AI-Native Design** with LLM-optimized output formats for Claude Code, Cursor, and other agents
|
|
13
|
+
- **Smart Incremental Crawling** with ETag, Last-Modified, and content hashing
|
|
14
|
+
- **Multiple Output Formats** including console, JSON, HTML reports, and text for piping to LLMs
|
|
15
|
+
- **Single Binary** with zero dependencies
|
|
16
|
+
- **Shell Completions** for bash, zsh, fish
|
|
17
|
+
- **Self-Updating** with built-in version management
|
|
18
|
+
|
|
19
|
+
## Three Ways to Use
|
|
20
|
+
|
|
21
|
+
### 1. CLI for Humans
|
|
22
|
+
|
|
23
|
+
Run audits directly in your terminal:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
squirrel audit example.com
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Pipe to AI
|
|
30
|
+
|
|
31
|
+
Pipe clean output to any AI assistant:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
squirrel audit example.com --format text | claude
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. AI Agent Skill
|
|
38
|
+
|
|
39
|
+
Install the skill for autonomous workflows:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx skills install squirrelscan/skills
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then prompt your AI agent:
|
|
46
|
+
```
|
|
47
|
+
Use the audit-website skill to audit this site and fix all issues
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
**npm (all platforms):**
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g squirrelscan
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or run without installing:
|
|
58
|
+
```bash
|
|
59
|
+
npx squirrelscan audit example.com
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**macOS / Linux:**
|
|
63
|
+
```bash
|
|
64
|
+
curl -fsSL https://squirrelscan.com/install | bash
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Windows:**
|
|
68
|
+
```powershell
|
|
69
|
+
iwr -useb https://squirrelscan.com/install.ps1 | iex
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Audit a website
|
|
76
|
+
squirrel audit https://example.com
|
|
77
|
+
|
|
78
|
+
# Generate HTML report
|
|
79
|
+
squirrel audit https://example.com -f html -o report.html
|
|
80
|
+
|
|
81
|
+
# Pipe to Claude for AI analysis
|
|
82
|
+
squirrel audit https://example.com --format text | claude
|
|
83
|
+
|
|
84
|
+
# Limit pages for faster results
|
|
85
|
+
squirrel audit https://example.com -m 10
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Resources
|
|
89
|
+
|
|
90
|
+
- **Website:** [squirrelscan.com](https://squirrelscan.com)
|
|
91
|
+
- **Documentation:** [docs.squirrelscan.com](https://docs.squirrelscan.com)
|
|
92
|
+
- **AI Agent Skills:** [github.com/squirrelscan/skills](https://github.com/squirrelscan/skills)
|
|
93
|
+
|
|
94
|
+
## Rule Categories
|
|
95
|
+
|
|
96
|
+
| Category | Examples |
|
|
97
|
+
|----------|----------|
|
|
98
|
+
| **Core SEO** | Meta tags, canonical URLs, h1, robots meta |
|
|
99
|
+
| **Accessibility** | ARIA labels, focus indicators, landmarks |
|
|
100
|
+
| **Performance** | Core Web Vitals (LCP, CLS, INP) |
|
|
101
|
+
| **Security** | HTTPS, CSP, leaked secrets (96 patterns) |
|
|
102
|
+
| **E-E-A-T** | Author bylines, expertise signals, trust indicators |
|
|
103
|
+
| **Content** | Word count, readability, freshness |
|
|
104
|
+
| **Images** | Alt text, modern formats, lazy loading |
|
|
105
|
+
| **Links** | Broken links, redirect chains, anchor text |
|
|
106
|
+
| **Schema** | JSON-LD structured data validation |
|
|
107
|
+
| **Mobile** | Viewport, tap targets, responsive design |
|
|
108
|
+
|
|
109
|
+
And 10 more categories covering video, analytics, i18n, local SEO, and more.
|
|
110
|
+
|
|
111
|
+
## AI Agent Integration
|
|
112
|
+
|
|
113
|
+
squirrelscan is designed for autonomous AI workflows:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Install the skill for Claude Code, Cursor, etc.
|
|
117
|
+
npx skills install squirrelscan/skills
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example AI prompts:
|
|
121
|
+
- "Use the audit-website skill to audit example.com and fix all high-severity issues"
|
|
122
|
+
- "Audit this site, enter plan mode, and create a comprehensive fix strategy"
|
|
123
|
+
- "Use audit-website skill to check for regressions after my recent changes"
|
|
124
|
+
|
|
125
|
+
See [AI Agent Integration docs](https://docs.squirrelscan.com/agents) for advanced workflows.
|
|
126
|
+
|
|
127
|
+
## Output Formats
|
|
128
|
+
|
|
129
|
+
| Format | Flag | Use Case |
|
|
130
|
+
|--------|------|----------|
|
|
131
|
+
| Console | (default) | Human-readable terminal output |
|
|
132
|
+
| JSON | `-f json` | CI/CD, programmatic processing |
|
|
133
|
+
| HTML | `-f html` | Visual reports for sharing |
|
|
134
|
+
| Markdown | `-f markdown` | Documentation, GitHub |
|
|
135
|
+
| Text | `-f text` | Clean output for piping to LLMs |
|
|
136
|
+
|
|
137
|
+
## Development Status
|
|
138
|
+
|
|
139
|
+
squirrelscan is in **active beta**. Expect rapid iteration and breaking changes. Feedback and issue reports welcome!
|
|
140
|
+
|
|
141
|
+
## Links
|
|
142
|
+
|
|
143
|
+
- [Website](https://squirrelscan.com)
|
|
144
|
+
- [Documentation](https://docs.squirrelscan.com)
|
|
145
|
+
- [AI Agent Skills](https://github.com/squirrelscan/skills)
|
|
146
|
+
- [Share Feedback](https://squirrelscan.com/feedback)
|
|
147
|
+
- [Bugs, Issues & Feature Requests](https://github.com/squirrelscan/squirrelscan/issues)
|
|
148
|
+
- [Twitter/X](https://x.com/squirrelscan_)
|
package/bin/squirrel.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* squirrel CLI wrapper
|
|
5
|
+
* Executes the platform-specific binary downloaded during npm install
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execFileSync, spawnSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const { getBinaryExtension } = require("../lib/platform");
|
|
12
|
+
|
|
13
|
+
const binaryName = `squirrel${getBinaryExtension()}`;
|
|
14
|
+
const binaryPath = path.join(__dirname, binaryName);
|
|
15
|
+
|
|
16
|
+
// Check if binary exists
|
|
17
|
+
if (!fs.existsSync(binaryPath)) {
|
|
18
|
+
console.error("Error: squirrelscan binary not found.");
|
|
19
|
+
console.error("");
|
|
20
|
+
console.error("The binary should have been downloaded during npm install.");
|
|
21
|
+
console.error("Try reinstalling: npm install -g squirrelscan");
|
|
22
|
+
console.error("");
|
|
23
|
+
console.error("Or install directly:");
|
|
24
|
+
console.error(" curl -fsSL https://squirrelscan.com/install | bash");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Execute binary with all arguments
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Use spawnSync for proper signal handling and stdio inheritance
|
|
33
|
+
const result = spawnSync(binaryPath, args, {
|
|
34
|
+
stdio: "inherit",
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Forward exit code
|
|
39
|
+
if (result.status !== null) {
|
|
40
|
+
process.exit(result.status);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Handle signal termination
|
|
44
|
+
if (result.signal) {
|
|
45
|
+
process.kill(process.pid, result.signal);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`Error executing squirrel: ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection for squirrelscan binary downloads
|
|
3
|
+
* Mirrors logic from install.sh
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect if running on musl libc (Alpine, etc.)
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
function isMusl() {
|
|
14
|
+
// Check for musl loader
|
|
15
|
+
try {
|
|
16
|
+
const files = fs.readdirSync("/lib");
|
|
17
|
+
if (files.some((f) => f.startsWith("ld-musl-"))) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// /lib doesn't exist or not readable
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check for Alpine
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync("/etc/alpine-release")) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Not Alpine
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get platform identifier for binary download
|
|
38
|
+
* @returns {string} Platform identifier (e.g., "darwin-arm64", "linux-x64-musl")
|
|
39
|
+
*/
|
|
40
|
+
function getPlatform() {
|
|
41
|
+
const platform = os.platform();
|
|
42
|
+
const arch = os.arch();
|
|
43
|
+
|
|
44
|
+
let osName;
|
|
45
|
+
switch (platform) {
|
|
46
|
+
case "darwin":
|
|
47
|
+
osName = "darwin";
|
|
48
|
+
break;
|
|
49
|
+
case "linux":
|
|
50
|
+
osName = "linux";
|
|
51
|
+
break;
|
|
52
|
+
case "win32":
|
|
53
|
+
osName = "windows";
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let archName;
|
|
60
|
+
switch (arch) {
|
|
61
|
+
case "x64":
|
|
62
|
+
archName = "x64";
|
|
63
|
+
break;
|
|
64
|
+
case "arm64":
|
|
65
|
+
archName = "arm64";
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add musl suffix for Linux if needed
|
|
72
|
+
const libc = osName === "linux" && isMusl() ? "-musl" : "";
|
|
73
|
+
|
|
74
|
+
return `${osName}-${archName}${libc}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get binary filename for current platform
|
|
79
|
+
* @param {string} version - Version string (e.g., "0.0.17")
|
|
80
|
+
* @returns {string} Binary filename
|
|
81
|
+
*/
|
|
82
|
+
function getBinaryFilename(version) {
|
|
83
|
+
const platform = getPlatform();
|
|
84
|
+
const ext = os.platform() === "win32" ? ".exe" : "";
|
|
85
|
+
return `squirrel-${version}-${platform}${ext}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get extension for current platform
|
|
90
|
+
* @returns {string} ".exe" on Windows, "" otherwise
|
|
91
|
+
*/
|
|
92
|
+
function getBinaryExtension() {
|
|
93
|
+
return os.platform() === "win32" ? ".exe" : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
getPlatform,
|
|
98
|
+
getBinaryFilename,
|
|
99
|
+
getBinaryExtension,
|
|
100
|
+
isMusl,
|
|
101
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "squirrelscan",
|
|
3
|
+
"version": "0.0.17",
|
|
4
|
+
"description": "CLI website audits for humans, agents and LLMs",
|
|
5
|
+
"bin": {
|
|
6
|
+
"squirrel": "bin/squirrel.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node scripts/postinstall.js"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/squirrelscan/squirrelscan.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://squirrelscan.com",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/squirrelscan/squirrelscan/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"seo",
|
|
21
|
+
"audit",
|
|
22
|
+
"cli",
|
|
23
|
+
"website",
|
|
24
|
+
"llm",
|
|
25
|
+
"accessibility",
|
|
26
|
+
"performance",
|
|
27
|
+
"security",
|
|
28
|
+
"crawler"
|
|
29
|
+
],
|
|
30
|
+
"author": "squirrelscan",
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"os": [
|
|
36
|
+
"darwin",
|
|
37
|
+
"linux",
|
|
38
|
+
"win32"
|
|
39
|
+
],
|
|
40
|
+
"cpu": [
|
|
41
|
+
"x64",
|
|
42
|
+
"arm64"
|
|
43
|
+
],
|
|
44
|
+
"files": [
|
|
45
|
+
"bin/",
|
|
46
|
+
"lib/",
|
|
47
|
+
"scripts/",
|
|
48
|
+
"README.md"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* postinstall script for squirrelscan npm package
|
|
5
|
+
* Downloads the platform-specific binary from GitHub releases
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const https = require("https");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
const { getPlatform, getBinaryExtension } = require("../lib/platform");
|
|
13
|
+
|
|
14
|
+
const REPO = "squirrelscan/squirrelscan";
|
|
15
|
+
const pkg = require("../package.json");
|
|
16
|
+
const VERSION = `v${pkg.version}`;
|
|
17
|
+
|
|
18
|
+
// Colors for terminal output
|
|
19
|
+
const supportsColor = process.stdout.isTTY;
|
|
20
|
+
const green = (s) => (supportsColor ? `\x1b[32m${s}\x1b[0m` : s);
|
|
21
|
+
const yellow = (s) => (supportsColor ? `\x1b[33m${s}\x1b[0m` : s);
|
|
22
|
+
const red = (s) => (supportsColor ? `\x1b[31m${s}\x1b[0m` : s);
|
|
23
|
+
const blue = (s) => (supportsColor ? `\x1b[34m${s}\x1b[0m` : s);
|
|
24
|
+
|
|
25
|
+
const log = (msg) => console.log(`${green("==>")} ${msg}`);
|
|
26
|
+
const info = (msg) => console.log(`${blue("::")} ${msg}`);
|
|
27
|
+
const warn = (msg) => console.log(`${yellow("Warning:")} ${msg}`);
|
|
28
|
+
const error = (msg) => {
|
|
29
|
+
console.error(`${red("Error:")} ${msg}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* HTTPS GET with redirect following
|
|
35
|
+
* @param {string} url
|
|
36
|
+
* @returns {Promise<{data: Buffer, statusCode: number}>}
|
|
37
|
+
*/
|
|
38
|
+
function httpsGet(url) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const request = https.get(url, { headers: { "User-Agent": "squirrelscan-npm" } }, (res) => {
|
|
41
|
+
// Follow redirects
|
|
42
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
43
|
+
return httpsGet(res.headers.location).then(resolve).catch(reject);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (res.statusCode !== 200) {
|
|
47
|
+
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const chunks = [];
|
|
52
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
53
|
+
res.on("end", () => resolve({ data: Buffer.concat(chunks), statusCode: res.statusCode }));
|
|
54
|
+
res.on("error", reject);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
request.on("error", reject);
|
|
58
|
+
request.setTimeout(120000, () => {
|
|
59
|
+
request.destroy();
|
|
60
|
+
reject(new Error("Request timeout"));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch with retry
|
|
67
|
+
* @param {string} url
|
|
68
|
+
* @param {number} attempts
|
|
69
|
+
* @returns {Promise<Buffer>}
|
|
70
|
+
*/
|
|
71
|
+
async function fetchWithRetry(url, attempts = 3) {
|
|
72
|
+
for (let i = 1; i <= attempts; i++) {
|
|
73
|
+
try {
|
|
74
|
+
const { data } = await httpsGet(url);
|
|
75
|
+
return data;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (i < attempts) {
|
|
78
|
+
warn(`Download failed, retrying (${i}/${attempts})...`);
|
|
79
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
80
|
+
} else {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compute SHA256 hash of buffer
|
|
89
|
+
* @param {Buffer} buffer
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function sha256(buffer) {
|
|
93
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
const platform = getPlatform();
|
|
98
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
99
|
+
const binaryName = `squirrel${getBinaryExtension()}`;
|
|
100
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
101
|
+
|
|
102
|
+
log(`Installing squirrelscan ${VERSION} for ${platform}...`);
|
|
103
|
+
|
|
104
|
+
// Ensure bin directory exists
|
|
105
|
+
if (!fs.existsSync(binDir)) {
|
|
106
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fetch manifest
|
|
110
|
+
const releaseUrl = `https://github.com/${REPO}/releases/download/${VERSION}`;
|
|
111
|
+
const manifestUrl = `${releaseUrl}/manifest.json`;
|
|
112
|
+
|
|
113
|
+
info("Fetching manifest...");
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
const manifestData = await fetchWithRetry(manifestUrl);
|
|
117
|
+
manifest = JSON.parse(manifestData.toString());
|
|
118
|
+
} catch (err) {
|
|
119
|
+
error(`Failed to fetch manifest: ${err.message}\n URL: ${manifestUrl}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get binary info for platform
|
|
123
|
+
const binaryInfo = manifest.binaries?.[platform];
|
|
124
|
+
if (!binaryInfo) {
|
|
125
|
+
error(`No binary available for platform: ${platform}\n See: https://github.com/${REPO}/releases/tag/${VERSION}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { filename, sha256: expectedSha256 } = binaryInfo;
|
|
129
|
+
|
|
130
|
+
// Download binary
|
|
131
|
+
const binaryUrl = `${releaseUrl}/${filename}`;
|
|
132
|
+
info(`Downloading ${filename}...`);
|
|
133
|
+
|
|
134
|
+
let binaryData;
|
|
135
|
+
try {
|
|
136
|
+
binaryData = await fetchWithRetry(binaryUrl);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
error(`Failed to download binary: ${err.message}\n URL: ${binaryUrl}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Verify checksum
|
|
142
|
+
info("Verifying checksum...");
|
|
143
|
+
const actualSha256 = sha256(binaryData);
|
|
144
|
+
if (actualSha256 !== expectedSha256) {
|
|
145
|
+
error(`Checksum mismatch!\n Expected: ${expectedSha256}\n Actual: ${actualSha256}`);
|
|
146
|
+
}
|
|
147
|
+
info(`Checksum verified: ${expectedSha256.slice(0, 16)}...`);
|
|
148
|
+
|
|
149
|
+
// Write binary
|
|
150
|
+
fs.writeFileSync(binaryPath, binaryData);
|
|
151
|
+
|
|
152
|
+
// Make executable (Unix only)
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
log("Installation complete!");
|
|
158
|
+
info(`Binary: ${binaryPath}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main().catch((err) => {
|
|
162
|
+
console.error(err);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|