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/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
- process.stderr.write('donsetch: binary not found. Run `npm rebuild donsetch` to reinstall.\n');
26
- process.exit(1);
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
- const known = {
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
- // ── musl detection (Alpine etc.) ────────────────────────────────
60
- // The Linux binaries are glibc-linked. On musl systems they install
61
- // fine but can never exec (missing ld-linux loader) : fail HERE with
62
- // the actual cause instead of a cryptic spawn error on first run.
63
- //
64
- // Checking for /lib/ld-musl-*.so.1 existence is a false positive on
65
- // glibc systems that also have musl installed as a secondary libc
66
- // (e.g. Arch/Manjaro with the `musl` package). Instead, check what
67
- // the RUNNING Node process actually uses: read /proc/self/exe's ELF
68
- // interpreter (.interp section). On glibc it's ld-linux-*.so.2, on
69
- // musl it's ld-musl-*.so.1. An env var escape hatch is also provided.
70
- if (process.platform === 'linux') {
71
- // Escape hatch: force glibc path even on a musl system (user knows
72
- // the binary will run, e.g. via gcompat or a glibc chroot).
73
- if (process.env.DONSETCH_FORCE_GLIBC === '1') {
74
- // skip musl check
75
- } else {
76
- let isMusl = false;
77
- let detected = false;
78
- try {
79
- // Read the ELF interpreter of /proc/self/exe (this Node process).
80
- // The .interp section starts at offset 0x18 in the ELF header
81
- // for the program header table, but the reliable way is to parse
82
- // the program headers for PT_INTERP.
83
- const exe = '/proc/self/exe';
84
- const fd = fs.openSync(exe, 'r');
85
- // ELF header: first 64 bytes for 64-bit.
86
- const hdr = Buffer.alloc(64);
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
- // Skip if already installed (npm cache reuse, reinstall, etc.).
137
- // A plausibility check first: a stale 0-byte or truncated leftover
138
- // (killed install, crashed download) must not shadow a fresh one.
139
- if (fs.existsSync(binaryPath)) {
140
- let size = 0;
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: leftover ${plat.binary} is ${size} bytes : re-downloading`);
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 baseUrl = `https://github.com/${REPO}/releases/download/${TAG}`;
153
- const assetUrl = `${baseUrl}/${plat.asset}`;
154
- const checksumUrl = `${baseUrl}/${plat.asset}.sha256`;
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
- // ── download with redirect following (max 5 hops) ───────────────
157
- function download(url, dest) {
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 MAX_REDIRECTS = 5;
160
- function get(u, hops) {
161
- if (hops > MAX_REDIRECTS) {
162
- reject(new Error(`too many redirects for ${url}`));
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 opts = { headers: { 'Accept': 'application/octet-stream', 'User-Agent': 'donsetch-npm-installer' } };
166
- https.get(u, opts, (res) => {
167
- if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
168
- res.resume();
169
- const loc = res.headers.location;
170
- if (!loc) { reject(new Error(`redirect without Location from ${u}`)); return; }
171
- // Stay on https: never follow a downgrade to cleartext.
172
- if (loc.startsWith('http://')) {
173
- reject(new Error(`refusing http:// downgrade redirect to ${loc}`));
174
- return;
175
- }
176
- get(loc, hops + 1);
177
- return;
178
- }
179
- if (res.statusCode !== 200) {
180
- res.resume();
181
- reject(new Error(`HTTP ${res.statusCode} for ${u}`));
182
- return;
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
- res.pipe(file);
186
- file.on('finish', () => { file.close(); resolve(); });
187
- file.on('error', (err) => {
188
- try { fs.unlinkSync(dest); } catch (_) {}
189
- reject(err);
258
+ response.pipe(file);
259
+ file.on('finish', () => {
260
+ file.close(resolve);
190
261
  });
191
- }).on('error', reject);
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
- get(url, 0);
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
- let hasTar = false;
206
- try { execFileSync('tar', ['--version'], { stdio: 'ignore' }); hasTar = true; } catch (_) {}
207
- if (!hasTar) {
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('after downloading ' + assetUrl);
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
- console.error(`donsetch: SHA256 mismatch!`);
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
- console.error(`donsetch: expected ${plat.binary} not found after extraction`);
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
- // 7. Make executable (Unix only)
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((err) => {
266
- console.error(`donsetch: install failed: ${err.message}`);
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(' git clone https://github.com/' + REPO);
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.1.1",
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": ">=14"
46
+ "node": ">=18"
50
47
  }
51
48
  }