donsetch 4.7.3 → 4.7.4
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 +1 -0
- package/bin/donsetch.js +24 -28
- package/install.js +102 -9
- package/package.json +4 -2
- package/pi-extension.ts +26 -19
- package/resolve-binary.js +247 -0
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ Downloads the prebuilt binary for your platform from [GitHub Releases](https://g
|
|
|
59
59
|
|
|
60
60
|
- **pnpm or bun:** the postinstall needs approval. `pnpm approve-builds` (or the bun equivalent), then reinstall. If scripts were blocked, `npx donsetch` runs the self-healing shim.
|
|
61
61
|
- **`--ignore-scripts`:** postinstall is intentionally skipped. Run `node node_modules/donsetch/install.js`, or use `npx donsetch` to download the binary when network access is available.
|
|
62
|
+
- **System-wide installs:** if the npm prefix is root-owned (for example `/usr/local/lib/node_modules`) and you run donsetch as a normal user, the package copy cannot be written there. donsetch then uses an existing binary from `PATH` or `~/.cargo/bin` automatically; `DONSETCH_BINARY=/path/to/donsetch` points at one explicitly. To keep the package copy instead: `npm config set prefix ~/.npm-global && npm install -g donsetch`.
|
|
62
63
|
- **Proxy:** set `HTTPS_PROXY` (or `https_proxy`, `HTTP_PROXY`, `http_proxy`) to an HTTP CONNECT proxy.
|
|
63
64
|
- **Release mirror:** set `DONSETCH_RELEASES_BASE` to a mirror serving `<tag>/<asset>` paths, for example `https://mirror.example/donsetch/releases`.
|
|
64
65
|
- **Windows:** the installer needs `tar`, included since Windows 10 1803.
|
package/bin/donsetch.js
CHANGED
|
@@ -1,48 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// donsetch CLI wrapper:
|
|
4
|
+
// donsetch CLI wrapper: resolve the binary and spawn it with
|
|
5
|
+
// forwarded stdio.
|
|
5
6
|
//
|
|
6
7
|
// For MCP server usage (`donsetch mcp`), the MCP client spawns this
|
|
7
8
|
// wrapper as a subprocess and communicates via stdin/stdout (JSON-RPC).
|
|
8
9
|
// stdio: 'inherit' pipes the native binary's stdio directly to the
|
|
9
10
|
// parent process, so the MCP protocol passes through unmodified.
|
|
10
11
|
//
|
|
12
|
+
// Resolution goes through resolve-binary.js: package-local binary
|
|
13
|
+
// first, install.js when it can run, then an existing donsetch on
|
|
14
|
+
// PATH / $CARGO_HOME / ~/.cargo/bin (#361: a system-wide package
|
|
15
|
+
// under a root-owned prefix must not kill a working install). All
|
|
16
|
+
// installer chatter and notices go to stderr: stdout belongs to the
|
|
17
|
+
// protocol.
|
|
18
|
+
//
|
|
11
19
|
// Signal forwarding: when the MCP client sends SIGTERM/SIGINT to this
|
|
12
20
|
// wrapper, we forward it to the native binary so it can clean up
|
|
13
21
|
// (close connections, save ghost state, etc.) before exiting.
|
|
14
22
|
|
|
15
|
-
const { spawn
|
|
16
|
-
const { existsSync } = require('fs');
|
|
23
|
+
const { spawn } = require('child_process');
|
|
17
24
|
const { join } = require('path');
|
|
25
|
+
const { resolveBinary } = require('../resolve-binary.js');
|
|
18
26
|
|
|
19
27
|
// ── resolve binary path ─────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
}
|
|
28
|
+
let resolved;
|
|
29
|
+
try {
|
|
30
|
+
resolved = resolveBinary({
|
|
31
|
+
pkgDir: join(__dirname, '..'),
|
|
32
|
+
self: __filename,
|
|
33
|
+
onOutput: (chunk) => process.stderr.write(chunk),
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
process.stderr.write(`donsetch: ${err.message || err}\n`);
|
|
37
|
+
process.exit(1);
|
|
45
38
|
}
|
|
39
|
+
if (resolved.notice) process.stderr.write(resolved.notice + '\n');
|
|
40
|
+
|
|
41
|
+
const binaryPath = resolved.path;
|
|
46
42
|
|
|
47
43
|
// ── spawn native binary ─────────────────────────────────────────
|
|
48
44
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
package/install.js
CHANGED
|
@@ -11,6 +11,7 @@ const crypto = require('crypto');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { execFileSync } = require('child_process');
|
|
14
|
+
const { findExistingBinary, targetWritable } = require('./resolve-binary.js');
|
|
14
15
|
const { pipeline } = require('stream/promises');
|
|
15
16
|
|
|
16
17
|
const REPO = 'dondai44423/donsetch';
|
|
@@ -269,12 +270,26 @@ async function download(url, dest) {
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
async function main() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
// Per-pid temp names: two first runs racing (two MCP clients
|
|
274
|
+
// starting together) must never share a write target. The renames
|
|
275
|
+
// that land the files are atomic; the temps are invisible until then.
|
|
276
|
+
const tarball = path.join(binDir, `${plat.asset}.tmp-${process.pid}`);
|
|
277
|
+
const checksumFile = path.join(binDir, `checksum.sha256.tmp-${process.pid}`);
|
|
278
|
+
const extractDir = path.join(binDir, `extract.tmp-${process.pid}`);
|
|
279
|
+
|
|
280
|
+
// The unwritable install dir (#361: a system-wide npm package under
|
|
281
|
+
// a root-owned prefix, run by an unprivileged user) is decided
|
|
282
|
+
// BEFORE any download, so the failure names the directory and the
|
|
283
|
+
// ways out instead of dying after 19MB of network.
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
286
|
+
} catch (err) {
|
|
287
|
+
settleUnwritable(err);
|
|
288
|
+
}
|
|
289
|
+
if (!targetWritable(__dirname)) {
|
|
290
|
+
settleUnwritable(new Error(`${binDir} is not writable`));
|
|
291
|
+
}
|
|
292
|
+
cleanupStaleTemps();
|
|
278
293
|
|
|
279
294
|
if (process.platform === 'win32') {
|
|
280
295
|
try {
|
|
@@ -300,11 +315,22 @@ async function main() {
|
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
console.log('donsetch: extracting...');
|
|
303
|
-
|
|
318
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
319
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
320
|
+
execFileSync('tar', ['xzf', tarball, '-C', extractDir], { stdio: 'inherit' });
|
|
304
321
|
|
|
305
|
-
|
|
322
|
+
const entries = fs.readdirSync(extractDir);
|
|
323
|
+
if (!entries.includes(plat.binary)) {
|
|
306
324
|
throw new Error(`expected ${plat.binary} not found after extraction in ${binDir}`);
|
|
307
325
|
}
|
|
326
|
+
// Land each entry with a rename inside binDir: a rename is atomic,
|
|
327
|
+
// so a concurrent first run can only ever replace a complete file,
|
|
328
|
+
// never expose a half-written one. The stamp goes last: a crash
|
|
329
|
+
// before it leaves no stamp, and the next run re-downloads.
|
|
330
|
+
for (const name of entries) {
|
|
331
|
+
fs.renameSync(path.join(extractDir, name), path.join(binDir, name));
|
|
332
|
+
}
|
|
333
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
308
334
|
if (process.platform !== 'win32') fs.chmodSync(binaryPath, 0o755);
|
|
309
335
|
fs.writeFileSync(stampPath, `${VERSION}\n`);
|
|
310
336
|
console.log(`donsetch: installed ${plat.binary} to ${binaryPath}`);
|
|
@@ -314,13 +340,80 @@ async function main() {
|
|
|
314
340
|
// and checksum sitting in ./binaries/ forever.
|
|
315
341
|
try { fs.unlinkSync(tarball); } catch (_) {}
|
|
316
342
|
try { fs.unlinkSync(checksumFile); } catch (_) {}
|
|
343
|
+
try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch (_) {}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// One existing-binary lookup for the failure paths: an install that
|
|
348
|
+
// cannot succeed must not take a working donsetch down with it (#361).
|
|
349
|
+
// Returns false when there is nothing to fall back on.
|
|
350
|
+
function ackExisting(detail) {
|
|
351
|
+
let existing = null;
|
|
352
|
+
try {
|
|
353
|
+
existing = findExistingBinary({ pkgDir: __dirname });
|
|
354
|
+
} catch (_) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
if (!existing) return false;
|
|
358
|
+
console.log(`donsetch: install failed: ${detail}`);
|
|
359
|
+
console.log(`donsetch: an existing binary was found and will be used: ${existing.path}`);
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The unwritable install dir: acknowledge an existing binary when one
|
|
364
|
+
// is findable (npm install / postinstall stays green), otherwise fail
|
|
365
|
+
// with every way out named.
|
|
366
|
+
function settleUnwritable(cause) {
|
|
367
|
+
const detail = cause && cause.message ? cause.message : 'directory is not writable';
|
|
368
|
+
if (ackExisting(detail)) return;
|
|
369
|
+
console.error(
|
|
370
|
+
`donsetch: cannot install into ${binDir} (${detail}) and no existing donsetch binary was found.`
|
|
371
|
+
);
|
|
372
|
+
console.error('');
|
|
373
|
+
console.error('Options:');
|
|
374
|
+
console.error(' - install under a user-writable prefix:');
|
|
375
|
+
console.error(' npm config set prefix ~/.npm-global && npm install -g donsetch');
|
|
376
|
+
console.error(' - point at a donsetch binary you already have:');
|
|
377
|
+
console.error(' DONSETCH_BINARY=/path/to/donsetch');
|
|
378
|
+
console.error(' - build from source:');
|
|
379
|
+
console.error(` git clone https://github.com/${REPO} && cd donsetch && cargo build --release`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Temp leftovers from crashed runs (this installer is the only writer
|
|
384
|
+
// of `.tmp-<pid>` / `extract.tmp-<pid>` names in binDir). A pid that
|
|
385
|
+
// is still alive keeps its workspace.
|
|
386
|
+
function cleanupStaleTemps() {
|
|
387
|
+
let names;
|
|
388
|
+
try {
|
|
389
|
+
names = fs.readdirSync(binDir);
|
|
390
|
+
} catch (_) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const name of names) {
|
|
394
|
+
const m = /\.tmp-(\d+)$/.exec(name);
|
|
395
|
+
if (!m || Number(m[1]) === process.pid) continue;
|
|
396
|
+
let alive = false;
|
|
397
|
+
try {
|
|
398
|
+
process.kill(Number(m[1]), 0);
|
|
399
|
+
alive = true;
|
|
400
|
+
} catch (_) {/* not running (or not ours): its temps are stale */}
|
|
401
|
+
if (!alive) {
|
|
402
|
+
try { fs.rmSync(path.join(binDir, name), { recursive: true, force: true }); } catch (_) {}
|
|
403
|
+
}
|
|
317
404
|
}
|
|
318
405
|
}
|
|
319
406
|
|
|
320
407
|
main().catch((error) => {
|
|
408
|
+
if (ackExisting(error.message)) return;
|
|
321
409
|
console.error(`donsetch: install failed: ${error.message}`);
|
|
322
410
|
console.error('');
|
|
323
|
-
console.error('
|
|
411
|
+
console.error('Options:');
|
|
412
|
+
console.error(' - install under a user-writable prefix:');
|
|
413
|
+
console.error(' npm config set prefix ~/.npm-global && npm install -g donsetch');
|
|
414
|
+
console.error(' - point at a donsetch binary you already have:');
|
|
415
|
+
console.error(' DONSETCH_BINARY=/path/to/donsetch');
|
|
416
|
+
console.error(' - build from source:');
|
|
324
417
|
console.error(` git clone https://github.com/${REPO} && cd donsetch && cargo build --release`);
|
|
325
418
|
process.exit(1);
|
|
326
419
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "donsetch",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.4",
|
|
4
4
|
"description": "MCP server that gives AI agents the web: fetch, search, crawl and screenshot from one local Rust binary. Zero API keys, zero accounts, Chrome-true TLS.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Bishesh Bhandari",
|
|
@@ -49,10 +49,12 @@
|
|
|
49
49
|
"donsetch": "bin/donsetch.js"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"postinstall": "node install.js"
|
|
52
|
+
"postinstall": "node install.js",
|
|
53
|
+
"test": "node --test --test-timeout=90000 tests/*.test.js"
|
|
53
54
|
},
|
|
54
55
|
"files": [
|
|
55
56
|
"install.js",
|
|
57
|
+
"resolve-binary.js",
|
|
56
58
|
"bin/donsetch.js",
|
|
57
59
|
"pi-extension.ts",
|
|
58
60
|
"README.md",
|
package/pi-extension.ts
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
* them up automatically : no code changes needed here.
|
|
12
12
|
*
|
|
13
13
|
* Auto-download: if the binary is missing (e.g. postinstall was
|
|
14
|
-
* blocked by npm 10+), the extension
|
|
15
|
-
*
|
|
14
|
+
* blocked by npm 10+), the extension resolves through
|
|
15
|
+
* resolve-binary.js at session_start: the package binary gets
|
|
16
|
+
* installed when possible, otherwise an existing donsetch
|
|
17
|
+
* (DONSETCH_BINARY / PATH / cargo bin) is used (#361: system-wide
|
|
18
|
+
* installs under a root-owned prefix).
|
|
16
19
|
*
|
|
17
20
|
* Custom TUI: each tool has clean renderCall/renderResult showing
|
|
18
21
|
* a compact summary card : not the full raw output. The LLM still
|
|
@@ -22,7 +25,8 @@
|
|
|
22
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
26
|
import { Type } from "typebox";
|
|
24
27
|
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
25
|
-
import { existsSync } from "node:fs";
|
|
28
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
26
30
|
import { join } from "node:path";
|
|
27
31
|
import { Text } from "@earendil-works/pi-tui";
|
|
28
32
|
|
|
@@ -108,32 +112,35 @@ function ensureBinary(): string {
|
|
|
108
112
|
const binaryPath = getBinaryPath();
|
|
109
113
|
if (existsSync(binaryPath)) return binaryPath;
|
|
110
114
|
|
|
111
|
-
const
|
|
112
|
-
if (!existsSync(
|
|
115
|
+
const resolverScript = join(__dirname, "resolve-binary.js");
|
|
116
|
+
if (!existsSync(resolverScript)) {
|
|
113
117
|
throw new Error(
|
|
114
|
-
`donsetch binary not found at ${binaryPath} and
|
|
118
|
+
`donsetch binary not found at ${binaryPath} and resolve-binary.js is missing. ` +
|
|
115
119
|
`Run \`npm rebuild donsetch\` or \`npm install -g --allow-scripts=donsetch donsetch@latest\`.`
|
|
116
120
|
);
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
// The shared resolver installs the package binary when it can and
|
|
124
|
+
// falls back to an existing donsetch when it cannot. stdio is
|
|
125
|
+
// inherited so first-run download progress stays visible; the path
|
|
126
|
+
// arrives via a temp file because stdout belongs to the terminal.
|
|
127
|
+
const outFile = join(tmpdir(), `donsetch-resolve-${process.pid}.txt`);
|
|
119
128
|
try {
|
|
120
|
-
execFileSync(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
timeout: 300_000
|
|
124
|
-
|
|
129
|
+
execFileSync(
|
|
130
|
+
process.execPath,
|
|
131
|
+
[resolverScript, "--print-file", outFile, "--pkg-dir", __dirname],
|
|
132
|
+
{ stdio: "inherit", cwd: __dirname, timeout: 300_000 }
|
|
133
|
+
);
|
|
134
|
+
const resolved = readFileSync(outFile, "utf8").trim();
|
|
135
|
+
if (!resolved) throw new Error("resolver returned no binary path");
|
|
136
|
+
return resolved;
|
|
125
137
|
} catch (err: any) {
|
|
126
|
-
throw new Error(`Failed to download donsetch binary: ${err.message}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!existsSync(binaryPath)) {
|
|
130
138
|
throw new Error(
|
|
131
|
-
`donsetch binary
|
|
132
|
-
`Run \`npm install -g --allow-scripts=donsetch donsetch@latest\` manually.`
|
|
139
|
+
`Failed to resolve the donsetch binary. The resolver output above names the cause and the ways out (${err.message}).`
|
|
133
140
|
);
|
|
141
|
+
} finally {
|
|
142
|
+
try { rmSync(outFile, { force: true }); } catch { /* best effort */ }
|
|
134
143
|
}
|
|
135
|
-
|
|
136
|
-
return binaryPath;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// ── MCP JSON-RPC 2.0 over stdio ──
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Binary resolution for the npm package, shared by every consumer:
|
|
5
|
+
// bin/donsetch.js (the CLI / MCP wrapper, in-process), install.js
|
|
6
|
+
// (install-time acknowledgement), and pi-extension.ts (spawned as
|
|
7
|
+
// `node resolve-binary.js --print-file <path>`).
|
|
8
|
+
//
|
|
9
|
+
// Order:
|
|
10
|
+
// 1. DONSETCH_BINARY explicit override, wins over everything
|
|
11
|
+
// 2. <pkg>/binaries/<name> the pinned copy this package installs
|
|
12
|
+
// 3. install.js fetch the pinned copy when it can run
|
|
13
|
+
// 4. an existing binary PATH, $CARGO_HOME/bin, ~/.cargo/bin
|
|
14
|
+
// 5. hard failure curated, names every way out
|
|
15
|
+
//
|
|
16
|
+
// The fallback exists because the pinned copy is not always obtainable
|
|
17
|
+
// (issue #361): a system-wide npm package under a root-owned prefix
|
|
18
|
+
// cannot be written by the user running it, and an install that cannot
|
|
19
|
+
// succeed must not take a working donsetch down with it. The wrapper
|
|
20
|
+
// itself is never a candidate: on a global npm install the PATH entry
|
|
21
|
+
// named `donsetch` is a symlink back into this package, so candidates
|
|
22
|
+
// are compared by their real path and anything resolving into the
|
|
23
|
+
// package is skipped.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { execFileSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const BINARY_NAME = process.platform === 'win32' ? 'donsetch.exe' : 'donsetch';
|
|
30
|
+
|
|
31
|
+
function isExecutableFile(p) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.statSync(p).isFile()) return false;
|
|
34
|
+
if (process.platform !== 'win32') fs.accessSync(p, fs.constants.X_OK);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function real(p) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.realpathSync(p);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function packageBinary(pkgDir) {
|
|
50
|
+
return path.join(pkgDir, 'binaries', BINARY_NAME);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The package-local copy, with a chmod rescue: an executable bit lost
|
|
54
|
+
// in transit (copied tree, artifact download) is repaired in place
|
|
55
|
+
// rather than triggering a re-download.
|
|
56
|
+
function usableLocal(p) {
|
|
57
|
+
if (!fs.existsSync(p)) return false;
|
|
58
|
+
if (isExecutableFile(p)) return true;
|
|
59
|
+
if (process.platform !== 'win32') {
|
|
60
|
+
try {
|
|
61
|
+
fs.chmodSync(p, 0o755);
|
|
62
|
+
} catch (_) {/* fall through to the recheck */}
|
|
63
|
+
}
|
|
64
|
+
return isExecutableFile(p);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// True when install.js could write into <pkgDir>/binaries. A missing
|
|
68
|
+
// directory is judged by its parent. Advisory only: the install itself
|
|
69
|
+
// still handles a late failure, but a known-unwritable target skips a
|
|
70
|
+
// doomed 19MB download first (#361).
|
|
71
|
+
function targetWritable(pkgDir) {
|
|
72
|
+
const binDir = path.join(pkgDir, 'binaries');
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(binDir)) {
|
|
75
|
+
fs.accessSync(binDir, fs.constants.W_OK);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
fs.accessSync(pkgDir, fs.constants.W_OK);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (_) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The one explicit override. Invalid values fail loudly: a DONSETCH_BINARY
|
|
86
|
+
// that names nothing is a config bug, not a fall-through case.
|
|
87
|
+
function overrideBinary() {
|
|
88
|
+
const over = process.env.DONSETCH_BINARY;
|
|
89
|
+
if (!over) return null;
|
|
90
|
+
const p = path.resolve(over);
|
|
91
|
+
if (!isExecutableFile(p)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`DONSETCH_BINARY is set to ${p}, which is not an executable file. ` +
|
|
94
|
+
`Unset it or point it at a donsetch binary.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return { path: p, source: 'env' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An already-installed donsetch outside this package: PATH entries
|
|
101
|
+
// (skipping empty ones, which mean the current directory), then the
|
|
102
|
+
// cargo bin directories even when they are not on PATH.
|
|
103
|
+
function findExistingBinary(opts) {
|
|
104
|
+
const pkgDir = opts.pkgDir;
|
|
105
|
+
|
|
106
|
+
const over = overrideBinary();
|
|
107
|
+
if (over) return over;
|
|
108
|
+
|
|
109
|
+
const banned = new Set([real(path.join(pkgDir, 'bin', 'donsetch.js'))]);
|
|
110
|
+
if (opts.self) banned.add(real(opts.self));
|
|
111
|
+
const pkgRoot = real(pkgDir);
|
|
112
|
+
const pkgPrefix = pkgRoot.endsWith(path.sep) ? pkgRoot : pkgRoot + path.sep;
|
|
113
|
+
|
|
114
|
+
const candidates = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const add = (p) => {
|
|
117
|
+
const r = real(p);
|
|
118
|
+
if (seen.has(r)) return;
|
|
119
|
+
seen.add(r);
|
|
120
|
+
candidates.push({ p, r });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
124
|
+
if (!dir) continue;
|
|
125
|
+
add(path.join(dir, BINARY_NAME));
|
|
126
|
+
}
|
|
127
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
128
|
+
if (process.env.CARGO_HOME) add(path.join(process.env.CARGO_HOME, 'bin', BINARY_NAME));
|
|
129
|
+
if (home) add(path.join(home, '.cargo', 'bin', BINARY_NAME));
|
|
130
|
+
|
|
131
|
+
for (const c of candidates) {
|
|
132
|
+
if (banned.has(c.r)) continue;
|
|
133
|
+
if (c.r === pkgRoot || c.r.startsWith(pkgPrefix)) continue;
|
|
134
|
+
if (!isExecutableFile(c.p)) continue;
|
|
135
|
+
return { path: c.p, source: 'path' };
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function noBinaryMessage(why) {
|
|
141
|
+
return [
|
|
142
|
+
`could not install the donsetch binary and none was found (${why}).`,
|
|
143
|
+
'Options:',
|
|
144
|
+
' - install under a user-writable prefix:',
|
|
145
|
+
' npm config set prefix ~/.npm-global && npm install -g donsetch',
|
|
146
|
+
' - point at a donsetch binary you already have:',
|
|
147
|
+
' DONSETCH_BINARY=/path/to/donsetch',
|
|
148
|
+
' - build from source:',
|
|
149
|
+
' git clone https://github.com/dondai44423/donsetch && cd donsetch && cargo build --release',
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runInstaller(pkgDir, onOutput) {
|
|
154
|
+
const installScript = path.join(pkgDir, 'install.js');
|
|
155
|
+
if (!fs.existsSync(installScript)) {
|
|
156
|
+
return { ok: false, error: `installer missing: ${installScript}` };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const out = execFileSync(process.execPath, [installScript], {
|
|
160
|
+
cwd: pkgDir,
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
timeout: 300_000,
|
|
163
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
164
|
+
});
|
|
165
|
+
if (onOutput && out) onOutput(out);
|
|
166
|
+
return { ok: true };
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (onOutput) {
|
|
169
|
+
if (err.stdout) onOutput(String(err.stdout));
|
|
170
|
+
if (err.stderr) onOutput(String(err.stderr));
|
|
171
|
+
}
|
|
172
|
+
const lines = String(err.stderr || err.message || err).trim().split('\n');
|
|
173
|
+
return { ok: false, error: (lines[0] || 'installer failed').trim() };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Resolve the binary for a run. `onOutput` receives installer output
|
|
178
|
+
// chunks; callers point it at stderr (never stdout: an MCP stdio
|
|
179
|
+
// session owns that channel).
|
|
180
|
+
function resolveBinary(opts) {
|
|
181
|
+
const pkgDir = opts.pkgDir;
|
|
182
|
+
const onOutput = opts.onOutput;
|
|
183
|
+
|
|
184
|
+
const over = overrideBinary();
|
|
185
|
+
if (over) return over;
|
|
186
|
+
|
|
187
|
+
const local = packageBinary(pkgDir);
|
|
188
|
+
if (usableLocal(local)) return { path: local, source: 'package' };
|
|
189
|
+
|
|
190
|
+
let why;
|
|
191
|
+
if (process.env.DONSETCH_SKIP_DOWNLOAD === '1') {
|
|
192
|
+
why = 'download skipped (DONSETCH_SKIP_DOWNLOAD=1)';
|
|
193
|
+
} else if (!targetWritable(pkgDir)) {
|
|
194
|
+
why = `${path.join(pkgDir, 'binaries')} is not writable`;
|
|
195
|
+
} else {
|
|
196
|
+
const result = runInstaller(pkgDir, onOutput);
|
|
197
|
+
if (usableLocal(local)) return { path: local, source: 'package' };
|
|
198
|
+
// The installer can also exit 0 without a local binary (its own
|
|
199
|
+
// existing-binary acknowledgement): name that, never "undefined".
|
|
200
|
+
why = result.ok ? 'the installer did not produce the binary' : result.error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const existing = findExistingBinary({ pkgDir, self: opts.self });
|
|
204
|
+
if (existing) {
|
|
205
|
+
return {
|
|
206
|
+
path: existing.path,
|
|
207
|
+
source: existing.source,
|
|
208
|
+
notice: `donsetch: package binary unavailable (${why}); using existing binary: ${existing.path}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
throw new Error(noBinaryMessage(why));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { resolveBinary, findExistingBinary, targetWritable, packageBinary };
|
|
215
|
+
|
|
216
|
+
// ── CLI mode ─────────────────────────────────────────────────────
|
|
217
|
+
// Used by pi-extension.ts. `--print` writes the resolved path to
|
|
218
|
+
// stdout; `--print-file <path>` writes it to a file and keeps stdout
|
|
219
|
+
// free for an inheriting caller. Progress and failures go to stderr.
|
|
220
|
+
if (require.main === module) {
|
|
221
|
+
const args = process.argv.slice(2);
|
|
222
|
+
const pkgIdx = args.indexOf('--pkg-dir');
|
|
223
|
+
const fileIdx = args.indexOf('--print-file');
|
|
224
|
+
if (pkgIdx < 0) {
|
|
225
|
+
process.stderr.write('usage: resolve-binary.js --pkg-dir <dir> [--print-file <path>]\n');
|
|
226
|
+
process.exit(2);
|
|
227
|
+
}
|
|
228
|
+
const pkgDir = path.resolve(args[pkgIdx + 1] || '');
|
|
229
|
+
const outFile = fileIdx >= 0 ? args[fileIdx + 1] : null;
|
|
230
|
+
try {
|
|
231
|
+
const resolved = resolveBinary({
|
|
232
|
+
pkgDir,
|
|
233
|
+
self: process.argv[1],
|
|
234
|
+
onOutput: (chunk) => process.stderr.write(chunk),
|
|
235
|
+
});
|
|
236
|
+
if (resolved.notice) process.stderr.write(resolved.notice + '\n');
|
|
237
|
+
if (outFile) {
|
|
238
|
+
fs.writeFileSync(outFile, resolved.path + '\n');
|
|
239
|
+
} else {
|
|
240
|
+
process.stdout.write(resolved.path + '\n');
|
|
241
|
+
}
|
|
242
|
+
process.exit(0);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
process.stderr.write(String(err.message || err) + '\n');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|