donsetch 4.1.1 → 4.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/LICENSE +662 -0
- package/README.md +20 -0
- package/bin/donsetch.js +21 -3
- package/install.js +229 -178
- package/package.json +4 -7
- package/pi-extension.ts +52 -7
package/README.md
CHANGED
|
@@ -23,6 +23,26 @@ Downloads the prebuilt binary for your platform from [GitHub Releases](https://g
|
|
|
23
23
|
| macOS arm64 | `donsetch-darwin-arm64.tar.gz` |
|
|
24
24
|
| Windows x86_64 | `donsetch-win32-x64.tar.gz` |
|
|
25
25
|
|
|
26
|
+
## Troubleshooting install
|
|
27
|
+
|
|
28
|
+
- **pnpm or bun:** approve the `donsetch` build script (`pnpm approve-builds`
|
|
29
|
+
or the equivalent bun approval), then reinstall. If scripts were blocked,
|
|
30
|
+
running `npx donsetch` invokes the self-healing shim.
|
|
31
|
+
- **`--ignore-scripts`:** postinstall is intentionally skipped. Run
|
|
32
|
+
`node node_modules/donsetch/install.js`, or invoke `npx donsetch` to
|
|
33
|
+
download the binary when network access is available.
|
|
34
|
+
- **Proxy:** set `HTTPS_PROXY` (or `https_proxy`, `HTTP_PROXY`, or
|
|
35
|
+
`http_proxy`) to an HTTP CONNECT proxy.
|
|
36
|
+
- **Release mirror:** set `DONSETCH_RELEASES_BASE` to a mirror containing
|
|
37
|
+
`<tag>/<asset>` paths, for example
|
|
38
|
+
`https://mirror.example/donsetch/releases`.
|
|
39
|
+
- **Windows:** the installer requires `tar`; Windows 10 version 1803 and
|
|
40
|
+
newer include it.
|
|
41
|
+
- **musl/Alpine:** the published Linux binaries use glibc. Build from source
|
|
42
|
+
with `cargo build --release` on musl systems.
|
|
43
|
+
- **Windows ARM64:** the x64 build runs under Windows emulation; no native
|
|
44
|
+
ARM64 asset is required.
|
|
45
|
+
|
|
26
46
|
## Two ways to use it
|
|
27
47
|
|
|
28
48
|
### MCP Server (for AI agents)
|
package/bin/donsetch.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// wrapper, we forward it to the native binary so it can clean up
|
|
13
13
|
// (close connections, save ghost state, etc.) before exiting.
|
|
14
14
|
|
|
15
|
-
const { spawn } = require('child_process');
|
|
15
|
+
const { spawn, execFileSync } = require('child_process');
|
|
16
16
|
const { existsSync } = require('fs');
|
|
17
17
|
const { join } = require('path');
|
|
18
18
|
|
|
@@ -22,8 +22,26 @@ const binDir = join(__dirname, '..', 'binaries');
|
|
|
22
22
|
const binaryPath = join(binDir, binaryName);
|
|
23
23
|
|
|
24
24
|
if (!existsSync(binaryPath)) {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const installScript = join(__dirname, '..', 'install.js');
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(process.execPath, [installScript], {
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
cwd: join(__dirname, '..'),
|
|
30
|
+
});
|
|
31
|
+
} catch (_) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`donsetch: native binary missing and download failed. Retry with network access, ` +
|
|
34
|
+
`or run node ${installScript}; pnpm users: pnpm approve-builds then reinstall.\n`
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(binaryPath)) {
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`donsetch: native binary missing and download failed. Retry with network access, ` +
|
|
41
|
+
`or run node ${installScript}; pnpm users: pnpm approve-builds then reinstall.\n`
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
// ── spawn native binary ─────────────────────────────────────────
|
package/install.js
CHANGED
|
@@ -3,20 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
// donsetch postinstall: download the prebuilt binary for this platform
|
|
5
5
|
// from GitHub Releases, verify SHA256, and extract to ./binaries/.
|
|
6
|
-
//
|
|
7
|
-
// The binary is NOT bundled in the npm package : it's fetched at
|
|
8
|
-
// install time from the GitHub release matching this package version.
|
|
9
|
-
// This keeps the npm registry clean (no 35 MB binary tarballs) and
|
|
10
|
-
// uses the same release artifacts that manual users download.
|
|
11
|
-
//
|
|
12
|
-
// Supported platforms:
|
|
13
|
-
// linux-x64 Linux x86_64 (glibc)
|
|
14
|
-
// linux-arm64 Linux ARM64 (glibc)
|
|
15
|
-
// darwin-arm64 macOS Apple Silicon
|
|
16
|
-
// darwin-x64 macOS Intel
|
|
17
|
-
// win32-x64 Windows x86_64
|
|
18
6
|
|
|
7
|
+
const http = require('http');
|
|
19
8
|
const https = require('https');
|
|
9
|
+
const tls = require('tls');
|
|
20
10
|
const crypto = require('crypto');
|
|
21
11
|
const fs = require('fs');
|
|
22
12
|
const path = require('path');
|
|
@@ -24,26 +14,26 @@ const { execFileSync } = require('child_process');
|
|
|
24
14
|
|
|
25
15
|
const REPO = 'dondai44423/donsetch';
|
|
26
16
|
const VERSION = require('./package.json').version;
|
|
27
|
-
const TAG = `v${VERSION}`;
|
|
17
|
+
const TAG = process.env.DONSETCH_INSTALL_TAG || `v${VERSION}`;
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
19
|
+
const MAX_REDIRECTS = 5;
|
|
20
|
+
const RETRIES = 3;
|
|
21
|
+
const RETRY_BACKOFF_MS = [1_000, 3_000];
|
|
28
22
|
|
|
29
|
-
// ── platform mapping ────────────────────────────────────────────
|
|
30
23
|
const PLATFORMS = {
|
|
31
|
-
'linux-x64': { asset: 'donsetch-linux-x64.tar.gz', binary: 'donsetch'
|
|
32
|
-
'linux-arm64': { asset: 'donsetch-linux-arm64.tar.gz', binary: 'donsetch'
|
|
33
|
-
'darwin-arm64': { asset: 'donsetch-darwin-arm64.tar.gz', binary: 'donsetch'
|
|
34
|
-
'darwin-x64': { asset: 'donsetch-darwin-x64.tar.gz', binary: 'donsetch'
|
|
24
|
+
'linux-x64': { asset: 'donsetch-linux-x64.tar.gz', binary: 'donsetch' },
|
|
25
|
+
'linux-arm64': { asset: 'donsetch-linux-arm64.tar.gz', binary: 'donsetch' },
|
|
26
|
+
'darwin-arm64': { asset: 'donsetch-darwin-arm64.tar.gz', binary: 'donsetch' },
|
|
27
|
+
'darwin-x64': { asset: 'donsetch-darwin-x64.tar.gz', binary: 'donsetch' },
|
|
35
28
|
'win32-x64': { asset: 'donsetch-win32-x64.tar.gz', binary: 'donsetch.exe' },
|
|
29
|
+
'win32-arm64': { asset: 'donsetch-win32-x64.tar.gz', binary: 'donsetch.exe', emulated: true },
|
|
36
30
|
};
|
|
37
31
|
|
|
38
32
|
const platKey = `${process.platform}-${process.arch}`;
|
|
39
33
|
const plat = PLATFORMS[platKey];
|
|
40
34
|
|
|
41
35
|
if (!plat) {
|
|
42
|
-
|
|
43
|
-
'darwin-x64': 'prebuilt binaries exist : update donsetch to a version that ships one',
|
|
44
|
-
'win32-arm64': 'no prebuilt binary yet : build from source (see below)',
|
|
45
|
-
}[platKey];
|
|
46
|
-
console.error(`donsetch: unsupported platform ${platKey}${known ? ` (${known})` : ''}`);
|
|
36
|
+
console.error(`donsetch: unsupported platform ${platKey}`);
|
|
47
37
|
console.error('');
|
|
48
38
|
console.error('Supported platforms:');
|
|
49
39
|
console.error(' linux-x64 Linux x86_64 (glibc)');
|
|
@@ -51,222 +41,283 @@ if (!plat) {
|
|
|
51
41
|
console.error(' darwin-arm64 macOS Apple Silicon');
|
|
52
42
|
console.error(' darwin-x64 macOS Intel');
|
|
53
43
|
console.error(' win32-x64 Windows x86_64');
|
|
44
|
+
console.error(' win32-arm64 Windows ARM64 (x64 emulation)');
|
|
54
45
|
console.error('');
|
|
55
46
|
console.error('Build from source: https://github.com/' + REPO);
|
|
56
47
|
process.exit(1);
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fs.readSync(fd, hdr, 0, 64, 0);
|
|
88
|
-
// e_phoff at offset 0x20 (64-bit), e_phentsize at 0x36, e_phnum at 0x38.
|
|
89
|
-
const e_phoff = hdr.readBigUInt64LE(0x20);
|
|
90
|
-
const e_phentsize = hdr.readUInt16LE(0x36);
|
|
91
|
-
const e_phnum = hdr.readUInt16LE(0x38);
|
|
92
|
-
for (let i = 0; i < e_phnum; i++) {
|
|
93
|
-
const ph = Buffer.alloc(e_phentsize);
|
|
94
|
-
fs.readSync(fd, ph, 0, e_phentsize, Number(e_phoff) + i * e_phentsize);
|
|
95
|
-
// PT_INTERP = 3
|
|
96
|
-
if (ph.readUInt32LE(0) === 3) {
|
|
97
|
-
// p_offset at 0x08 (64-bit), p_filesz at 0x20.
|
|
98
|
-
const p_offset = Number(ph.readBigUInt64LE(0x08));
|
|
99
|
-
const p_filesz = ph.readBigUInt64LE(0x20);
|
|
100
|
-
const interp = Buffer.alloc(Number(p_filesz));
|
|
101
|
-
fs.readSync(fd, interp, 0, Number(p_filesz), p_offset);
|
|
102
|
-
const loaderPath = interp.toString('ascii').replace(/\0/g, '').trim();
|
|
103
|
-
isMusl = loaderPath.includes('ld-musl');
|
|
104
|
-
detected = true;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
50
|
+
if (process.env.DONSETCH_SKIP_DOWNLOAD === '1') {
|
|
51
|
+
console.log('donsetch: download skipped (DONSETCH_SKIP_DOWNLOAD=1)');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (plat.emulated) {
|
|
56
|
+
console.log('donsetch: Windows arm64 uses the win32-x64 build under emulation.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function detectMusl() {
|
|
60
|
+
let interpreter;
|
|
61
|
+
try {
|
|
62
|
+
const fd = fs.openSync('/proc/self/exe', 'r');
|
|
63
|
+
const hdr = Buffer.alloc(64);
|
|
64
|
+
fs.readSync(fd, hdr, 0, 64, 0);
|
|
65
|
+
const ePhOff = hdr.readBigUInt64LE(0x20);
|
|
66
|
+
const ePhEntSize = hdr.readUInt16LE(0x36);
|
|
67
|
+
const ePhNum = hdr.readUInt16LE(0x38);
|
|
68
|
+
for (let i = 0; i < ePhNum; i++) {
|
|
69
|
+
const ph = Buffer.alloc(ePhEntSize);
|
|
70
|
+
fs.readSync(fd, ph, 0, ePhEntSize, Number(ePhOff) + i * ePhEntSize);
|
|
71
|
+
if (ph.readUInt32LE(0) === 3) {
|
|
72
|
+
const offset = Number(ph.readBigUInt64LE(0x08));
|
|
73
|
+
const size = Number(ph.readBigUInt64LE(0x20));
|
|
74
|
+
const value = Buffer.alloc(size);
|
|
75
|
+
fs.readSync(fd, value, 0, size, offset);
|
|
76
|
+
interpreter = value.toString('ascii').replace(/\0/g, '').trim();
|
|
77
|
+
break;
|
|
107
78
|
}
|
|
108
|
-
fs.closeSync(fd);
|
|
109
|
-
} catch (_) {
|
|
110
|
-
// /proc/self/exe not readable (some containers). Fall back to
|
|
111
|
-
// the old existence check, but only as a last resort.
|
|
112
|
-
}
|
|
113
|
-
// Only fall back to the existence check if we could not read
|
|
114
|
-
// the ELF interpreter at all. A successful read that found glibc
|
|
115
|
-
// (isMusl=false, detected=true) must NOT fall back.
|
|
116
|
-
if (!detected) {
|
|
117
|
-
isMusl = fs.existsSync('/lib/ld-musl-x86_64.so.1')
|
|
118
|
-
|| fs.existsSync('/lib/ld-musl-aarch64.so.1');
|
|
119
|
-
}
|
|
120
|
-
if (isMusl) {
|
|
121
|
-
console.error('donsetch: musl libc detected (Alpine?).');
|
|
122
|
-
console.error('The prebuilt Linux binaries are glibc-linked and will not run.');
|
|
123
|
-
console.error('');
|
|
124
|
-
console.error('Options:');
|
|
125
|
-
console.error(' - build from source: git clone https://github.com/' + REPO + ' && cargo build --release');
|
|
126
|
-
console.error(' - use a glibc-based image/dist (debian, ubuntu, fedora)');
|
|
127
|
-
console.error(' - set DONSETCH_FORCE_GLIBC=1 if you have a glibc compat layer (gcompat)');
|
|
128
|
-
process.exit(1);
|
|
129
79
|
}
|
|
80
|
+
fs.closeSync(fd);
|
|
81
|
+
} catch (_) {
|
|
82
|
+
interpreter = undefined;
|
|
130
83
|
}
|
|
84
|
+
if (interpreter) return interpreter.includes('musl');
|
|
85
|
+
|
|
86
|
+
const muslLoader = fs.existsSync('/lib/ld-musl-x86_64.so.1')
|
|
87
|
+
|| fs.existsSync('/lib/ld-musl-aarch64.so.1');
|
|
88
|
+
const glibcLoader = fs.existsSync('/lib64/ld-linux-x86-64.so.2')
|
|
89
|
+
|| fs.existsSync('/lib/ld-linux-aarch64.so.1');
|
|
90
|
+
return muslLoader && !glibcLoader;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.platform === 'linux'
|
|
94
|
+
&& process.env.DONSETCH_FORCE_GLIBC !== '1'
|
|
95
|
+
&& detectMusl()) {
|
|
96
|
+
console.error('donsetch: musl libc detected (Alpine?).');
|
|
97
|
+
console.error('The prebuilt Linux binaries are glibc-linked and will not run.');
|
|
98
|
+
console.error('');
|
|
99
|
+
console.error('Build from source:');
|
|
100
|
+
console.error(` git clone https://github.com/${REPO} && cd donsetch && cargo build --release`);
|
|
101
|
+
console.error('Or use a glibc-based image (Debian, Ubuntu, Fedora).');
|
|
102
|
+
console.error('Set DONSETCH_FORCE_GLIBC=1 only if a glibc compatibility layer is installed.');
|
|
103
|
+
process.exit(1);
|
|
131
104
|
}
|
|
132
105
|
|
|
133
106
|
const binDir = path.join(__dirname, 'binaries');
|
|
134
107
|
const binaryPath = path.join(binDir, plat.binary);
|
|
108
|
+
const stampPath = `${binaryPath}.version`;
|
|
135
109
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
try { size = fs.statSync(binaryPath).size; } catch (_) {}
|
|
142
|
-
if (size > 1024 * 1024) {
|
|
143
|
-
console.log(`donsetch: binary already present (${plat.binary})`);
|
|
110
|
+
if (fs.existsSync(binaryPath) && fs.existsSync(stampPath)) {
|
|
111
|
+
let stamp;
|
|
112
|
+
try { stamp = fs.readFileSync(stampPath, 'utf8').trim(); } catch (_) {}
|
|
113
|
+
if (stamp === VERSION) {
|
|
114
|
+
console.log(`donsetch: binary already present (${plat.binary} ${VERSION})`);
|
|
144
115
|
process.exit(0);
|
|
145
116
|
}
|
|
146
|
-
console.log(`donsetch:
|
|
147
|
-
try { fs.unlinkSync(binaryPath); } catch (_) {}
|
|
117
|
+
console.log(`donsetch: binary version stamp mismatch; re-downloading ${VERSION}`);
|
|
148
118
|
}
|
|
149
119
|
|
|
150
120
|
fs.mkdirSync(binDir, { recursive: true });
|
|
151
121
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
122
|
+
const releasesBase = (process.env.DONSETCH_RELEASES_BASE
|
|
123
|
+
|| `https://github.com/${REPO}/releases/download`).replace(/\/+$/, '');
|
|
124
|
+
const assetUrl = `${releasesBase}/${TAG}/${plat.asset}`;
|
|
125
|
+
const checksumUrl = `${assetUrl}.sha256`;
|
|
155
126
|
|
|
156
|
-
|
|
157
|
-
|
|
127
|
+
function sleep(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function noProxy(host) {
|
|
132
|
+
const value = process.env.NO_PROXY || process.env.no_proxy || '';
|
|
133
|
+
return value.split(',').map((part) => part.trim().toLowerCase()).filter(Boolean).some((entry) => {
|
|
134
|
+
if (entry === '*') return true;
|
|
135
|
+
const normalized = entry.replace(/^\*\./, '').replace(/^\./, '');
|
|
136
|
+
return host === normalized || host.endsWith(`.${normalized}`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function proxyFor(target) {
|
|
141
|
+
if (noProxy(target.hostname)) return null;
|
|
142
|
+
const value = target.protocol === 'https:'
|
|
143
|
+
? (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy)
|
|
144
|
+
: (process.env.HTTP_PROXY || process.env.http_proxy);
|
|
145
|
+
return value ? new URL(value) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function requestDirect(target) {
|
|
158
149
|
return new Promise((resolve, reject) => {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
151
|
+
const req = transport.get(target, {
|
|
152
|
+
headers: {
|
|
153
|
+
Accept: 'application/octet-stream',
|
|
154
|
+
'User-Agent': 'donsetch-npm-installer',
|
|
155
|
+
},
|
|
156
|
+
}, resolve);
|
|
157
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error('request timeout')));
|
|
158
|
+
req.on('error', reject);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function requestThroughProxy(target, proxy) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const connectTransport = proxy.protocol === 'https:' ? https : http;
|
|
165
|
+
const proxyReq = connectTransport.request({
|
|
166
|
+
hostname: proxy.hostname,
|
|
167
|
+
port: proxy.port || (proxy.protocol === 'https:' ? 443 : 80),
|
|
168
|
+
method: 'CONNECT',
|
|
169
|
+
path: `${target.hostname}:${target.port || 443}`,
|
|
170
|
+
headers: { Host: `${target.hostname}:${target.port || 443}` },
|
|
171
|
+
});
|
|
172
|
+
proxyReq.once('connect', (response, socket) => {
|
|
173
|
+
if (response.statusCode !== 200) {
|
|
174
|
+
socket.destroy();
|
|
175
|
+
reject(new Error(`proxy CONNECT returned HTTP ${response.statusCode}`));
|
|
163
176
|
return;
|
|
164
177
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
const req = https.request({
|
|
179
|
+
hostname: target.hostname,
|
|
180
|
+
port: target.port || 443,
|
|
181
|
+
path: `${target.pathname}${target.search}`,
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers: {
|
|
184
|
+
Accept: 'application/octet-stream',
|
|
185
|
+
'User-Agent': 'donsetch-npm-installer',
|
|
186
|
+
},
|
|
187
|
+
agent: false,
|
|
188
|
+
createConnection: () => tls.connect({
|
|
189
|
+
socket,
|
|
190
|
+
servername: target.hostname,
|
|
191
|
+
}),
|
|
192
|
+
}, resolve);
|
|
193
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error('request timeout')));
|
|
194
|
+
req.on('error', reject);
|
|
195
|
+
req.end();
|
|
196
|
+
});
|
|
197
|
+
proxyReq.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
198
|
+
proxyReq.destroy(new Error('proxy CONNECT timeout'));
|
|
199
|
+
});
|
|
200
|
+
proxyReq.on('error', reject);
|
|
201
|
+
proxyReq.end();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function requestUrl(url) {
|
|
206
|
+
const target = new URL(url);
|
|
207
|
+
if (target.protocol !== 'https:') {
|
|
208
|
+
throw new Error(`refusing non-HTTPS download URL ${url}`);
|
|
209
|
+
}
|
|
210
|
+
const proxy = proxyFor(target);
|
|
211
|
+
return proxy ? requestThroughProxy(target, proxy) : requestDirect(target);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function getWithRedirects(url) {
|
|
215
|
+
let current = url;
|
|
216
|
+
for (let hops = 0; hops <= MAX_REDIRECTS; hops++) {
|
|
217
|
+
const response = await requestUrl(current);
|
|
218
|
+
if (![301, 302, 303, 307, 308].includes(response.statusCode)) {
|
|
219
|
+
return response;
|
|
220
|
+
}
|
|
221
|
+
response.resume();
|
|
222
|
+
const location = response.headers.location;
|
|
223
|
+
if (!location) throw new Error(`redirect without Location from ${current}`);
|
|
224
|
+
const next = new URL(location, current).toString();
|
|
225
|
+
if (!next.startsWith('https://')) {
|
|
226
|
+
throw new Error(`refusing HTTP downgrade redirect to ${next}`);
|
|
227
|
+
}
|
|
228
|
+
current = next;
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`too many redirects for ${url}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function drain(response) {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
response.resume();
|
|
236
|
+
response.once('end', resolve);
|
|
237
|
+
response.once('close', resolve);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function statusError(url, status) {
|
|
242
|
+
const error = new Error(`HTTP ${status} for ${url}`);
|
|
243
|
+
error.retryable = status === 429 || status >= 500;
|
|
244
|
+
return error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function download(url, dest) {
|
|
248
|
+
for (let attempt = 0; attempt < RETRIES; attempt++) {
|
|
249
|
+
try {
|
|
250
|
+
const response = await getWithRedirects(url);
|
|
251
|
+
if (response.statusCode !== 200) {
|
|
252
|
+
const error = statusError(url, response.statusCode);
|
|
253
|
+
await drain(response);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
await new Promise((resolve, reject) => {
|
|
184
257
|
const file = fs.createWriteStream(dest);
|
|
185
|
-
|
|
186
|
-
file.on('finish', () => {
|
|
187
|
-
|
|
188
|
-
try { fs.unlinkSync(dest); } catch (_) {}
|
|
189
|
-
reject(err);
|
|
258
|
+
response.pipe(file);
|
|
259
|
+
file.on('finish', () => {
|
|
260
|
+
file.close(resolve);
|
|
190
261
|
});
|
|
191
|
-
|
|
262
|
+
file.on('error', reject);
|
|
263
|
+
response.on('error', reject);
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
try { fs.unlinkSync(dest); } catch (_) {}
|
|
268
|
+
if (attempt + 1 >= RETRIES || error.retryable === false) throw error;
|
|
269
|
+
const delay = RETRY_BACKOFF_MS[attempt] || RETRY_BACKOFF_MS[RETRY_BACKOFF_MS.length - 1];
|
|
270
|
+
console.error(`donsetch: download attempt ${attempt + 1} failed (${error.message}); retrying in ${delay / 1000}s`);
|
|
271
|
+
await sleep(delay);
|
|
192
272
|
}
|
|
193
|
-
|
|
194
|
-
});
|
|
273
|
+
}
|
|
195
274
|
}
|
|
196
275
|
|
|
197
|
-
// ── main ────────────────────────────────────────────────────────
|
|
198
276
|
async function main() {
|
|
199
277
|
const tarball = path.join(binDir, plat.asset);
|
|
200
278
|
const checksumFile = path.join(binDir, 'checksum.sha256');
|
|
201
279
|
|
|
202
|
-
// 0. Windows: tar ships with Windows 10 1803+; older boxes lack it.
|
|
203
|
-
// Detect BEFORE downloading so the error names the real problem.
|
|
204
280
|
if (process.platform === 'win32') {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
281
|
+
try {
|
|
282
|
+
execFileSync('tar', ['--version'], { stdio: 'ignore' });
|
|
283
|
+
} catch (_) {
|
|
208
284
|
console.error('donsetch: `tar` not found on this Windows system.');
|
|
209
285
|
console.error('tar ships with Windows 10 1803+. Update Windows, or extract manually');
|
|
210
|
-
console.error(
|
|
286
|
+
console.error(`after downloading ${assetUrl}`);
|
|
211
287
|
process.exit(1);
|
|
212
288
|
}
|
|
213
289
|
}
|
|
214
290
|
|
|
215
|
-
// 1. Download the binary tarball
|
|
216
291
|
console.log(`donsetch: downloading ${plat.asset} from ${TAG}...`);
|
|
217
292
|
await download(assetUrl, tarball);
|
|
218
|
-
|
|
219
|
-
// 2. Download the SHA256 checksum
|
|
220
293
|
console.log('donsetch: verifying checksum...');
|
|
221
294
|
await download(checksumUrl, checksumFile);
|
|
222
295
|
|
|
223
|
-
// 3. Verify SHA256
|
|
224
296
|
const expectedHash = fs.readFileSync(checksumFile, 'utf8').trim().split(/\s+/)[0];
|
|
225
297
|
const actualHash = crypto.createHash('sha256').update(fs.readFileSync(tarball)).digest('hex');
|
|
226
|
-
|
|
227
|
-
if (actualHash !== expectedHash) {
|
|
298
|
+
if (!/^[a-f0-9]{64}$/.test(expectedHash) || actualHash !== expectedHash) {
|
|
228
299
|
try { fs.unlinkSync(tarball); } catch (_) {}
|
|
229
300
|
try { fs.unlinkSync(checksumFile); } catch (_) {}
|
|
230
|
-
|
|
231
|
-
console.error(` expected: ${expectedHash}`);
|
|
232
|
-
console.error(` actual: ${actualHash}`);
|
|
233
|
-
console.error('The download may have been corrupted or tampered with.');
|
|
234
|
-
process.exit(1);
|
|
301
|
+
throw new Error(`SHA256 mismatch (expected ${expectedHash}, actual ${actualHash})`);
|
|
235
302
|
}
|
|
236
303
|
|
|
237
|
-
// 4. Extract (tar is built into Linux, macOS, and Windows 10+)
|
|
238
304
|
console.log('donsetch: extracting...');
|
|
239
|
-
// execFileSync: no shell, no string interpolation : the install
|
|
240
|
-
// path (which can contain quotes/spaces) is passed as argv.
|
|
241
305
|
execFileSync('tar', ['xzf', tarball, '-C', binDir], { stdio: 'inherit' });
|
|
242
|
-
|
|
243
|
-
// 5. Cleanup
|
|
244
306
|
try { fs.unlinkSync(tarball); } catch (_) {}
|
|
245
307
|
try { fs.unlinkSync(checksumFile); } catch (_) {}
|
|
246
308
|
|
|
247
|
-
// 6. Verify the binary exists after extraction (BEFORE chmod :
|
|
248
|
-
// chmod on a missing file throws an opaque error).
|
|
249
309
|
if (!fs.existsSync(binaryPath)) {
|
|
250
|
-
|
|
251
|
-
console.error(` looked at: ${binaryPath}`);
|
|
252
|
-
console.error(' contents of binaries/:', fs.readdirSync(binDir).join(', '));
|
|
253
|
-
process.exit(1);
|
|
310
|
+
throw new Error(`expected ${plat.binary} not found after extraction in ${binDir}`);
|
|
254
311
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (process.platform !== 'win32') {
|
|
258
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
259
|
-
}
|
|
260
|
-
|
|
312
|
+
if (process.platform !== 'win32') fs.chmodSync(binaryPath, 0o755);
|
|
313
|
+
fs.writeFileSync(stampPath, `${VERSION}\n`);
|
|
261
314
|
console.log(`donsetch: installed ${plat.binary} to ${binaryPath}`);
|
|
262
|
-
console.log(`donsetch: run \`donsetch\` to see available commands.`);
|
|
263
315
|
}
|
|
264
316
|
|
|
265
|
-
main().catch((
|
|
266
|
-
console.error(`donsetch: install failed: ${
|
|
317
|
+
main().catch((error) => {
|
|
318
|
+
console.error(`donsetch: install failed: ${error.message}`);
|
|
267
319
|
console.error('');
|
|
268
320
|
console.error('You can build from source:');
|
|
269
|
-
console.error(
|
|
270
|
-
console.error(' cd donsetch && cargo build --release');
|
|
321
|
+
console.error(` git clone https://github.com/${REPO} && cd donsetch && cargo build --release`);
|
|
271
322
|
process.exit(1);
|
|
272
323
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "donsetch",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Web fetch, search and crawl for AI agents. Zero API keys. Chrome-true TLS.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Bishesh Bhandari",
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"pi": {
|
|
30
30
|
"extensions": ["./pi-extension.ts"]
|
|
31
31
|
},
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
34
|
-
"typebox": "*"
|
|
35
|
-
},
|
|
36
32
|
"bin": {
|
|
37
33
|
"donsetch": "bin/donsetch.js"
|
|
38
34
|
},
|
|
@@ -43,9 +39,10 @@
|
|
|
43
39
|
"install.js",
|
|
44
40
|
"bin/donsetch.js",
|
|
45
41
|
"pi-extension.ts",
|
|
46
|
-
"README.md"
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
47
44
|
],
|
|
48
45
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
46
|
+
"node": ">=18"
|
|
50
47
|
}
|
|
51
48
|
}
|