docdex 0.2.16 → 0.2.18
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/assets/agents.md +117 -0
- package/bin/docdex-mcp-server.js +66 -0
- package/bin/docdex.js +32 -2
- package/lib/install.js +310 -8
- package/lib/playwright_fetch.js +174 -0
- package/lib/playwright_install.js +302 -0
- package/lib/postinstall_setup.js +318 -5
- package/package.json +5 -2
package/lib/install.js
CHANGED
|
@@ -8,6 +8,7 @@ const os = require("node:os");
|
|
|
8
8
|
const path = require("node:path");
|
|
9
9
|
const { pipeline } = require("node:stream/promises");
|
|
10
10
|
const crypto = require("node:crypto");
|
|
11
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
11
12
|
|
|
12
13
|
const pkg = require("../package.json");
|
|
13
14
|
const {
|
|
@@ -39,6 +40,9 @@ const DEFAULT_INTEGRITY_CONFIG = Object.freeze({
|
|
|
39
40
|
});
|
|
40
41
|
const LOCAL_FALLBACK_ENV = "DOCDEX_LOCAL_FALLBACK";
|
|
41
42
|
const LOCAL_BINARY_ENV = "DOCDEX_LOCAL_BINARY";
|
|
43
|
+
const AGENTS_DOC_FILENAME = "agents.md";
|
|
44
|
+
const PLAYWRIGHT_INSTALL_GUARD = "DOCDEX_INTERNAL_PLAYWRIGHT_INSTALL";
|
|
45
|
+
const PLAYWRIGHT_SKIP_ENV = "DOCDEX_SKIP_PLAYWRIGHT_DEP_INSTALL";
|
|
42
46
|
|
|
43
47
|
const EXIT_CODE_BY_ERROR_CODE = Object.freeze({
|
|
44
48
|
DOCDEX_INSTALLER_CONFIG: 2,
|
|
@@ -198,6 +202,90 @@ function requestOptions() {
|
|
|
198
202
|
return { headers };
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
function agentsDocSourcePath() {
|
|
206
|
+
return path.join(__dirname, "..", "assets", AGENTS_DOC_FILENAME);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function agentsDocTargetPath() {
|
|
210
|
+
return path.join(os.homedir(), ".docdex", AGENTS_DOC_FILENAME);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeAgentInstructions() {
|
|
214
|
+
const sourcePath = agentsDocSourcePath();
|
|
215
|
+
if (!fs.existsSync(sourcePath)) return false;
|
|
216
|
+
let contents = "";
|
|
217
|
+
try {
|
|
218
|
+
contents = fs.readFileSync(sourcePath, "utf8");
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (!contents.trim()) return false;
|
|
223
|
+
const targetPath = agentsDocTargetPath();
|
|
224
|
+
try {
|
|
225
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
226
|
+
const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
227
|
+
if (existing === contents) return false;
|
|
228
|
+
fs.writeFileSync(targetPath, contents);
|
|
229
|
+
return true;
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolvePlaywrightPackage() {
|
|
236
|
+
const baseDir = path.join(__dirname, "..");
|
|
237
|
+
try {
|
|
238
|
+
return require.resolve("playwright/package.json", { paths: [baseDir] });
|
|
239
|
+
} catch {}
|
|
240
|
+
try {
|
|
241
|
+
return require.resolve("playwright/package.json");
|
|
242
|
+
} catch {}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveNpmCommand() {
|
|
247
|
+
const npmExec = process.env.npm_execpath;
|
|
248
|
+
if (npmExec) {
|
|
249
|
+
return { cmd: process.execPath, args: [npmExec] };
|
|
250
|
+
}
|
|
251
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
252
|
+
return { cmd: npmCmd, args: [] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ensurePlaywrightDependency({ logger = console } = {}) {
|
|
256
|
+
if (process.env[PLAYWRIGHT_SKIP_ENV]) return;
|
|
257
|
+
if (process.env[PLAYWRIGHT_INSTALL_GUARD]) return;
|
|
258
|
+
if (resolvePlaywrightPackage()) return;
|
|
259
|
+
|
|
260
|
+
const rootDir = path.join(__dirname, "..");
|
|
261
|
+
const { cmd, args } = resolveNpmCommand();
|
|
262
|
+
const installArgs = args.concat([
|
|
263
|
+
"install",
|
|
264
|
+
"--no-save",
|
|
265
|
+
"--ignore-scripts",
|
|
266
|
+
"--no-package-lock",
|
|
267
|
+
"--no-audit",
|
|
268
|
+
"--no-fund",
|
|
269
|
+
"playwright"
|
|
270
|
+
]);
|
|
271
|
+
const result = spawnSync(cmd, installArgs, {
|
|
272
|
+
cwd: rootDir,
|
|
273
|
+
stdio: "inherit",
|
|
274
|
+
env: {
|
|
275
|
+
...process.env,
|
|
276
|
+
[PLAYWRIGHT_INSTALL_GUARD]: "1"
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (result.error || (typeof result.status === "number" && result.status !== 0)) {
|
|
280
|
+
const message = result.error?.message || `npm exit status ${result.status}`;
|
|
281
|
+
logger.warn?.(`[docdex] Playwright dependency install failed: ${message}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!resolvePlaywrightPackage()) {
|
|
285
|
+
logger.warn?.("[docdex] Playwright dependency still missing after install attempt");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
201
289
|
function selectHttpClient(url) {
|
|
202
290
|
try {
|
|
203
291
|
const protocol = new URL(url).protocol;
|
|
@@ -294,11 +382,48 @@ function download(url, dest, redirects = 0) {
|
|
|
294
382
|
|
|
295
383
|
async function extractTarball(archivePath, targetDir) {
|
|
296
384
|
// Lazy import so unit tests can load this module without installing optional npm deps.
|
|
297
|
-
|
|
385
|
+
let tar;
|
|
386
|
+
try {
|
|
387
|
+
tar = require("tar");
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (err && err.code === "MODULE_NOT_FOUND") {
|
|
390
|
+
await extractTarballWithSystemTar(archivePath, targetDir);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
298
395
|
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
299
396
|
await tar.x({ file: archivePath, cwd: targetDir, gzip: true });
|
|
300
397
|
}
|
|
301
398
|
|
|
399
|
+
async function extractTarballWithSystemTar(archivePath, targetDir) {
|
|
400
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
401
|
+
const args = ["-xzf", archivePath, "-C", targetDir];
|
|
402
|
+
await new Promise((resolve, reject) => {
|
|
403
|
+
const proc = spawn("tar", args, { stdio: "ignore" });
|
|
404
|
+
proc.on("error", (err) => {
|
|
405
|
+
reject(
|
|
406
|
+
new ArchiveInvalidError(
|
|
407
|
+
`tar module missing and system tar failed: ${err.message}`,
|
|
408
|
+
{ archivePath }
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
proc.on("close", (code) => {
|
|
413
|
+
if (code === 0) {
|
|
414
|
+
resolve();
|
|
415
|
+
} else {
|
|
416
|
+
reject(
|
|
417
|
+
new ArchiveInvalidError(
|
|
418
|
+
`system tar exited with code ${code}`,
|
|
419
|
+
{ archivePath }
|
|
420
|
+
)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
302
427
|
async function sha256File(filePath) {
|
|
303
428
|
return new Promise((resolve, reject) => {
|
|
304
429
|
const hash = crypto.createHash("sha256");
|
|
@@ -371,6 +496,45 @@ function parseEnvBool(value) {
|
|
|
371
496
|
return null;
|
|
372
497
|
}
|
|
373
498
|
|
|
499
|
+
function readJsonSync({ fsModule, filePath }) {
|
|
500
|
+
try {
|
|
501
|
+
const raw = fsModule.readFileSync(filePath, "utf8");
|
|
502
|
+
return JSON.parse(raw);
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function readTextSync({ fsModule, filePath }) {
|
|
509
|
+
try {
|
|
510
|
+
return fsModule.readFileSync(filePath, "utf8");
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function isDocdexRepoRoot({ baseDir, fsModule, pathModule }) {
|
|
517
|
+
const cargoPath = pathModule.join(baseDir, "Cargo.toml");
|
|
518
|
+
const npmPackagePath = pathModule.join(baseDir, "npm", "package.json");
|
|
519
|
+
if (!fsModule.existsSync(cargoPath) || !fsModule.existsSync(npmPackagePath)) return false;
|
|
520
|
+
const pkgJson = readJsonSync({ fsModule, filePath: npmPackagePath });
|
|
521
|
+
if (pkgJson?.name !== "docdex") return false;
|
|
522
|
+
const cargoToml = readTextSync({ fsModule, filePath: cargoPath });
|
|
523
|
+
return Boolean(cargoToml && /name\s*=\s*"docdexd"/.test(cargoToml));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function detectLocalRepoRootFromInitCwd({ env = process.env, fsModule = fs, pathModule = path } = {}) {
|
|
527
|
+
const initCwd = env?.INIT_CWD;
|
|
528
|
+
if (!initCwd) return null;
|
|
529
|
+
const candidate = pathModule.resolve(initCwd);
|
|
530
|
+
if (isDocdexRepoRoot({ baseDir: candidate, fsModule, pathModule })) return candidate;
|
|
531
|
+
const parent = pathModule.dirname(candidate);
|
|
532
|
+
if (parent && parent !== candidate && isDocdexRepoRoot({ baseDir: parent, fsModule, pathModule })) {
|
|
533
|
+
return parent;
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
374
538
|
function detectLocalRepoRoot({ pathModule, fsModule } = {}) {
|
|
375
539
|
const pathImpl = pathModule || path;
|
|
376
540
|
const fsImpl = fsModule || fs;
|
|
@@ -383,6 +547,45 @@ function detectLocalRepoRoot({ pathModule, fsModule } = {}) {
|
|
|
383
547
|
return null;
|
|
384
548
|
}
|
|
385
549
|
|
|
550
|
+
function parseNpmConfigArgv(env) {
|
|
551
|
+
const raw = env?.npm_config_argv;
|
|
552
|
+
if (!raw || typeof raw !== "string") return null;
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(raw);
|
|
555
|
+
if (Array.isArray(parsed?.original)) return parsed.original;
|
|
556
|
+
if (Array.isArray(parsed?.cooked)) return parsed.cooked;
|
|
557
|
+
} catch {}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isLikelyLocalInstallArg(arg, pathModule) {
|
|
562
|
+
if (typeof arg !== "string" || !arg) return false;
|
|
563
|
+
if (arg === "." || arg === "..") return true;
|
|
564
|
+
if (arg.startsWith("file:")) return true;
|
|
565
|
+
if (arg.startsWith("./") || arg.startsWith("../")) return true;
|
|
566
|
+
if (pathModule.isAbsolute(arg)) return true;
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function isNpmInstallCommand(argv) {
|
|
571
|
+
return argv.some((arg) => arg === "install" || arg === "i" || arg === "add");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isLocalInstallRequest({ env, pathModule }) {
|
|
575
|
+
const argv = parseNpmConfigArgv(env);
|
|
576
|
+
if (!argv || !isNpmInstallCommand(argv)) return false;
|
|
577
|
+
return argv.some((arg) => isLikelyLocalInstallArg(arg, pathModule));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function shouldPreferLocalInstall({ env, localBinaryPath, pathModule }) {
|
|
581
|
+
if (!localBinaryPath) return false;
|
|
582
|
+
if (parseEnvBool(env?.[LOCAL_FALLBACK_ENV]) === false) return false;
|
|
583
|
+
if (env?.[LOCAL_BINARY_ENV]) return true;
|
|
584
|
+
if (!env?.INIT_CWD) return false;
|
|
585
|
+
if (env?.npm_lifecycle_event !== "postinstall") return false;
|
|
586
|
+
return isLocalInstallRequest({ env, pathModule });
|
|
587
|
+
}
|
|
588
|
+
|
|
386
589
|
function resolveLocalBinaryCandidate({
|
|
387
590
|
env = process.env,
|
|
388
591
|
platform = process.platform,
|
|
@@ -395,16 +598,36 @@ function resolveLocalBinaryCandidate({
|
|
|
395
598
|
const resolved = pathModule.resolve(explicit);
|
|
396
599
|
if (fsModule.existsSync(resolved)) return resolved;
|
|
397
600
|
}
|
|
601
|
+
const isWin32 = platform === "win32";
|
|
602
|
+
const mcpName = isWin32 ? "docdex-mcp-server.exe" : "docdex-mcp-server";
|
|
398
603
|
const root = repoRoot || detectLocalRepoRoot({ pathModule, fsModule });
|
|
399
604
|
if (!root) return null;
|
|
400
605
|
const binaryName = platform === "win32" ? "docdexd.exe" : "docdexd";
|
|
401
606
|
const releasePath = pathModule.join(root, "target", "release", binaryName);
|
|
402
|
-
if (fsModule.existsSync(releasePath))
|
|
607
|
+
if (fsModule.existsSync(releasePath)) {
|
|
608
|
+
if (localMcpPresent({ fsModule, pathModule, binaryPath: releasePath, mcpName })) {
|
|
609
|
+
return releasePath;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
403
612
|
const debugPath = pathModule.join(root, "target", "debug", binaryName);
|
|
404
|
-
if (fsModule.existsSync(debugPath))
|
|
613
|
+
if (fsModule.existsSync(debugPath)) {
|
|
614
|
+
if (localMcpPresent({ fsModule, pathModule, binaryPath: debugPath, mcpName })) {
|
|
615
|
+
return debugPath;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
405
618
|
return null;
|
|
406
619
|
}
|
|
407
620
|
|
|
621
|
+
function localMcpPresent({ fsModule, pathModule, binaryPath, mcpName }) {
|
|
622
|
+
const dir = pathModule.dirname(binaryPath);
|
|
623
|
+
const candidates = [
|
|
624
|
+
pathModule.join(dir, mcpName),
|
|
625
|
+
pathModule.join(pathModule.dirname(dir), "release", mcpName),
|
|
626
|
+
pathModule.join(pathModule.dirname(dir), "debug", mcpName)
|
|
627
|
+
];
|
|
628
|
+
return candidates.some((candidate) => fsModule.existsSync(candidate));
|
|
629
|
+
}
|
|
630
|
+
|
|
408
631
|
async function installFromLocalBinary({
|
|
409
632
|
fsModule,
|
|
410
633
|
pathModule,
|
|
@@ -428,13 +651,20 @@ async function installFromLocalBinary({
|
|
|
428
651
|
await fsModule.promises.chmod(destPath, 0o755).catch(() => {});
|
|
429
652
|
}
|
|
430
653
|
const mcpName = isWin32 ? "docdex-mcp-server.exe" : "docdex-mcp-server";
|
|
431
|
-
const
|
|
432
|
-
|
|
654
|
+
const mcpCandidates = [
|
|
655
|
+
pathModule.join(pathModule.dirname(binaryPath), mcpName),
|
|
656
|
+
pathModule.join(pathModule.dirname(pathModule.dirname(binaryPath)), "release", mcpName),
|
|
657
|
+
pathModule.join(pathModule.dirname(pathModule.dirname(binaryPath)), "debug", mcpName)
|
|
658
|
+
];
|
|
659
|
+
const mcpSource = mcpCandidates.find((candidate) => fsModule.existsSync(candidate));
|
|
660
|
+
if (mcpSource) {
|
|
433
661
|
const mcpDest = pathModule.join(distDir, mcpName);
|
|
434
662
|
await fsModule.promises.copyFile(mcpSource, mcpDest);
|
|
435
663
|
if (!isWin32) {
|
|
436
664
|
await fsModule.promises.chmod(mcpDest, 0o755).catch(() => {});
|
|
437
665
|
}
|
|
666
|
+
} else {
|
|
667
|
+
logger?.warn?.(`[docdex] local MCP binary not found; expected near ${binaryPath}`);
|
|
438
668
|
}
|
|
439
669
|
const binarySha256 = await sha256FileFn(destPath);
|
|
440
670
|
const metadata = {
|
|
@@ -844,6 +1074,9 @@ function decideInstallAction({
|
|
|
844
1074
|
integrityResult
|
|
845
1075
|
}) {
|
|
846
1076
|
if (!discoveredInstalledState?.binaryPresent) return { outcome: "update", reason: "binary_missing" };
|
|
1077
|
+
if (discoveredInstalledState.mcpBinaryPresent === false) {
|
|
1078
|
+
return { outcome: "update", reason: "mcp_binary_missing" };
|
|
1079
|
+
}
|
|
847
1080
|
|
|
848
1081
|
if (discoveredInstalledState.metadataStatus !== "valid") {
|
|
849
1082
|
return {
|
|
@@ -878,6 +1111,10 @@ function decideInstallAction({
|
|
|
878
1111
|
|
|
879
1112
|
async function discoverInstalledState({ fsModule, pathModule, distDir, platformKey, isWin32 }) {
|
|
880
1113
|
const binaryPath = pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd");
|
|
1114
|
+
const mcpBinaryPath = pathModule.join(
|
|
1115
|
+
distDir,
|
|
1116
|
+
isWin32 ? "docdex-mcp-server.exe" : "docdex-mcp-server"
|
|
1117
|
+
);
|
|
881
1118
|
const metadataPath = installMetadataPath(distDir, pathModule);
|
|
882
1119
|
|
|
883
1120
|
const existsSync = typeof fsModule?.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
@@ -886,6 +1123,7 @@ async function discoverInstalledState({ fsModule, pathModule, distDir, platformK
|
|
|
886
1123
|
binaryPath,
|
|
887
1124
|
metadataPath,
|
|
888
1125
|
binaryPresent: false,
|
|
1126
|
+
mcpBinaryPresent: false,
|
|
889
1127
|
installedVersion: null,
|
|
890
1128
|
metadata: null,
|
|
891
1129
|
metadataStatus: "unavailable",
|
|
@@ -899,6 +1137,7 @@ async function discoverInstalledState({ fsModule, pathModule, distDir, platformK
|
|
|
899
1137
|
binaryPath,
|
|
900
1138
|
metadataPath,
|
|
901
1139
|
binaryPresent: false,
|
|
1140
|
+
mcpBinaryPresent: false,
|
|
902
1141
|
installedVersion: null,
|
|
903
1142
|
metadata: null,
|
|
904
1143
|
metadataStatus: "missing",
|
|
@@ -907,11 +1146,13 @@ async function discoverInstalledState({ fsModule, pathModule, distDir, platformK
|
|
|
907
1146
|
};
|
|
908
1147
|
}
|
|
909
1148
|
|
|
1149
|
+
const mcpBinaryPresent = existsSync(mcpBinaryPath);
|
|
910
1150
|
const metaResult = await readJsonFileIfPossible({ fsModule, filePath: metadataPath });
|
|
911
1151
|
const meta = metaResult.value;
|
|
912
1152
|
if (!isValidInstallMetadata(meta)) {
|
|
913
1153
|
return {
|
|
914
1154
|
binaryPath,
|
|
1155
|
+
mcpBinaryPresent,
|
|
915
1156
|
metadataPath,
|
|
916
1157
|
binaryPresent: true,
|
|
917
1158
|
installedVersion: typeof meta?.version === "string" ? meta.version : null,
|
|
@@ -934,6 +1175,7 @@ async function discoverInstalledState({ fsModule, pathModule, distDir, platformK
|
|
|
934
1175
|
|
|
935
1176
|
return {
|
|
936
1177
|
binaryPath,
|
|
1178
|
+
mcpBinaryPresent,
|
|
937
1179
|
metadataPath,
|
|
938
1180
|
binaryPresent: true,
|
|
939
1181
|
installedVersion: meta.version,
|
|
@@ -1007,6 +1249,7 @@ async function determineLocalInstallerOutcome({
|
|
|
1007
1249
|
|
|
1008
1250
|
const shouldVerifyIntegrity =
|
|
1009
1251
|
discoveredInstalledState.binaryPresent &&
|
|
1252
|
+
discoveredInstalledState.mcpBinaryPresent !== false &&
|
|
1010
1253
|
!discoveredInstalledState.platformMismatch &&
|
|
1011
1254
|
discoveredInstalledState.installedVersion === expectedVersion &&
|
|
1012
1255
|
(normalizeSha256Hex(expectedBinarySha256) || discoveredInstalledState.metadataStatus === "valid");
|
|
@@ -1476,6 +1719,7 @@ async function verifyDownloadedFileIntegrity({
|
|
|
1476
1719
|
async function runInstaller(options) {
|
|
1477
1720
|
const opts = options || {};
|
|
1478
1721
|
const logger = opts.logger || console;
|
|
1722
|
+
const env = opts.env || process.env;
|
|
1479
1723
|
|
|
1480
1724
|
const detectPlatformKeyFn = opts.detectPlatformKeyFn || detectPlatformKey;
|
|
1481
1725
|
const targetTripleForPlatformKeyFn = opts.targetTripleForPlatformKeyFn || targetTripleForPlatformKey;
|
|
@@ -1494,8 +1738,19 @@ async function runInstaller(options) {
|
|
|
1494
1738
|
const sha256FileFn = opts.sha256FileFn || sha256File;
|
|
1495
1739
|
const writeJsonFileAtomicFn = opts.writeJsonFileAtomicFn || writeJsonFileAtomic;
|
|
1496
1740
|
const restartFn = opts.restartFn;
|
|
1497
|
-
const localRepoRoot =
|
|
1498
|
-
|
|
1741
|
+
const localRepoRoot =
|
|
1742
|
+
opts.localRepoRoot ||
|
|
1743
|
+
detectLocalRepoRootFromInitCwd({ env, fsModule, pathModule }) ||
|
|
1744
|
+
detectLocalRepoRoot({ pathModule, fsModule });
|
|
1745
|
+
const localBinaryPath =
|
|
1746
|
+
opts.localBinaryPath ||
|
|
1747
|
+
resolveLocalBinaryCandidate({
|
|
1748
|
+
env,
|
|
1749
|
+
platform: process.platform,
|
|
1750
|
+
pathModule,
|
|
1751
|
+
fsModule,
|
|
1752
|
+
repoRoot: localRepoRoot
|
|
1753
|
+
});
|
|
1499
1754
|
|
|
1500
1755
|
const detectedPlatform = opts.platform || process.platform;
|
|
1501
1756
|
const detectedArch = opts.arch || process.arch;
|
|
@@ -1551,6 +1806,25 @@ async function runInstaller(options) {
|
|
|
1551
1806
|
|
|
1552
1807
|
const priorRunnable = existsSync ? existsSync(local.binaryPath) : false;
|
|
1553
1808
|
|
|
1809
|
+
const forceLocalBinary = Boolean(env?.[LOCAL_BINARY_ENV]);
|
|
1810
|
+
if (forceLocalBinary && localBinaryPath) {
|
|
1811
|
+
const localInstall = await installFromLocalBinary({
|
|
1812
|
+
fsModule,
|
|
1813
|
+
pathModule,
|
|
1814
|
+
distDir,
|
|
1815
|
+
binaryPath: localBinaryPath,
|
|
1816
|
+
isWin32,
|
|
1817
|
+
version,
|
|
1818
|
+
platformKey,
|
|
1819
|
+
targetTriple,
|
|
1820
|
+
repoSlug: null,
|
|
1821
|
+
sha256FileFn,
|
|
1822
|
+
writeJsonFileAtomicFn,
|
|
1823
|
+
logger
|
|
1824
|
+
});
|
|
1825
|
+
return localInstall;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1554
1828
|
if (local.outcome === "no-op") {
|
|
1555
1829
|
logger.log("[docdex] Install outcome: no-op");
|
|
1556
1830
|
await cleanupInstallArtifacts({
|
|
@@ -1579,6 +1853,24 @@ async function runInstaller(options) {
|
|
|
1579
1853
|
};
|
|
1580
1854
|
}
|
|
1581
1855
|
|
|
1856
|
+
if (shouldPreferLocalInstall({ env, localBinaryPath, pathModule })) {
|
|
1857
|
+
const localInstall = await installFromLocalBinary({
|
|
1858
|
+
fsModule,
|
|
1859
|
+
pathModule,
|
|
1860
|
+
distDir,
|
|
1861
|
+
binaryPath: localBinaryPath,
|
|
1862
|
+
isWin32,
|
|
1863
|
+
version,
|
|
1864
|
+
platformKey,
|
|
1865
|
+
targetTriple,
|
|
1866
|
+
repoSlug: null,
|
|
1867
|
+
sha256FileFn,
|
|
1868
|
+
writeJsonFileAtomicFn,
|
|
1869
|
+
logger
|
|
1870
|
+
});
|
|
1871
|
+
return localInstall;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1582
1874
|
let repoSlug = null;
|
|
1583
1875
|
let archive;
|
|
1584
1876
|
let expectedSha256;
|
|
@@ -1601,7 +1893,7 @@ async function runInstaller(options) {
|
|
|
1601
1893
|
} catch (err) {
|
|
1602
1894
|
const fallback = await maybeInstallLocalFallback({
|
|
1603
1895
|
err,
|
|
1604
|
-
env
|
|
1896
|
+
env,
|
|
1605
1897
|
fsModule,
|
|
1606
1898
|
pathModule,
|
|
1607
1899
|
distDir,
|
|
@@ -1899,6 +2191,16 @@ async function main() {
|
|
|
1899
2191
|
} catch (err) {
|
|
1900
2192
|
console.warn(`[docdex] postinstall setup skipped: ${err?.message || err}`);
|
|
1901
2193
|
}
|
|
2194
|
+
try {
|
|
2195
|
+
writeAgentInstructions();
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
console.warn(`[docdex] agent instructions skipped: ${err?.message || err}`);
|
|
2198
|
+
}
|
|
2199
|
+
try {
|
|
2200
|
+
ensurePlaywrightDependency();
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
console.warn(`[docdex] playwright dependency check skipped: ${err?.message || err}`);
|
|
2203
|
+
}
|
|
1902
2204
|
printPostInstallBanner();
|
|
1903
2205
|
}
|
|
1904
2206
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { chromium, firefox, webkit } = require("playwright");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
|
+
const DEFAULT_BROWSER = "chromium";
|
|
8
|
+
const VIEWPORT = { width: 1920, height: 1080 };
|
|
9
|
+
const CHROMIUM_ARGS = [
|
|
10
|
+
"--disable-blink-features=AutomationControlled",
|
|
11
|
+
"--disable-dev-shm-usage",
|
|
12
|
+
"--disable-gpu",
|
|
13
|
+
"--no-sandbox",
|
|
14
|
+
"--no-first-run",
|
|
15
|
+
"--no-default-browser-check"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function normalizeBrowser(value) {
|
|
19
|
+
const trimmed = String(value || "").trim().toLowerCase();
|
|
20
|
+
if (trimmed === "chrome" || trimmed === "chromium" || trimmed === "chromium-browser") {
|
|
21
|
+
return "chromium";
|
|
22
|
+
}
|
|
23
|
+
if (trimmed === "firefox") return "firefox";
|
|
24
|
+
if (trimmed === "webkit") return "webkit";
|
|
25
|
+
return DEFAULT_BROWSER;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveBrowserType(name) {
|
|
29
|
+
switch (name) {
|
|
30
|
+
case "chromium":
|
|
31
|
+
return chromium;
|
|
32
|
+
case "firefox":
|
|
33
|
+
return firefox;
|
|
34
|
+
case "webkit":
|
|
35
|
+
return webkit;
|
|
36
|
+
default:
|
|
37
|
+
return chromium;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const parsed = {
|
|
43
|
+
url: "",
|
|
44
|
+
browser: DEFAULT_BROWSER,
|
|
45
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
46
|
+
userAgent: "",
|
|
47
|
+
headless: true,
|
|
48
|
+
userDataDir: ""
|
|
49
|
+
};
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const value = argv[i];
|
|
52
|
+
if (value === "--url" && argv[i + 1]) {
|
|
53
|
+
parsed.url = argv[i + 1];
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (value === "--browser" && argv[i + 1]) {
|
|
58
|
+
parsed.browser = argv[i + 1];
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (value === "--timeout-ms" && argv[i + 1]) {
|
|
63
|
+
parsed.timeoutMs = Number(argv[i + 1]);
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (value === "--user-agent" && argv[i + 1]) {
|
|
68
|
+
parsed.userAgent = argv[i + 1];
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (value === "--user-data-dir" && argv[i + 1]) {
|
|
73
|
+
parsed.userDataDir = argv[i + 1];
|
|
74
|
+
i += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (value === "--headless") {
|
|
78
|
+
parsed.headless = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (value === "--headed") {
|
|
82
|
+
parsed.headless = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
parsed.browser = normalizeBrowser(parsed.browser);
|
|
87
|
+
if (!parsed.url) {
|
|
88
|
+
throw new Error("missing --url");
|
|
89
|
+
}
|
|
90
|
+
if (!Number.isFinite(parsed.timeoutMs) || parsed.timeoutMs <= 0) {
|
|
91
|
+
parsed.timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function fetchWithPlaywright(options) {
|
|
97
|
+
const browserName = normalizeBrowser(options.browser);
|
|
98
|
+
const browserType = resolveBrowserType(browserName);
|
|
99
|
+
const launchOptions = {
|
|
100
|
+
headless: options.headless
|
|
101
|
+
};
|
|
102
|
+
if (browserName === "chromium") {
|
|
103
|
+
launchOptions.args = CHROMIUM_ARGS;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let browser;
|
|
107
|
+
let context;
|
|
108
|
+
try {
|
|
109
|
+
if (options.userDataDir) {
|
|
110
|
+
context = await browserType.launchPersistentContext(options.userDataDir, {
|
|
111
|
+
...launchOptions,
|
|
112
|
+
viewport: VIEWPORT,
|
|
113
|
+
userAgent: options.userAgent || undefined
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
browser = await browserType.launch(launchOptions);
|
|
117
|
+
context = await browser.newContext({
|
|
118
|
+
viewport: VIEWPORT,
|
|
119
|
+
userAgent: options.userAgent || undefined
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await context.addInitScript(() => {
|
|
124
|
+
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const page = await context.newPage();
|
|
128
|
+
page.setDefaultTimeout(options.timeoutMs);
|
|
129
|
+
const response = await page.goto(options.url, {
|
|
130
|
+
waitUntil: "domcontentloaded",
|
|
131
|
+
timeout: options.timeoutMs
|
|
132
|
+
});
|
|
133
|
+
const html = await page.content();
|
|
134
|
+
const status = response ? response.status() : null;
|
|
135
|
+
const finalUrl = page.url();
|
|
136
|
+
await page.close();
|
|
137
|
+
|
|
138
|
+
if (!html || !String(html).trim()) {
|
|
139
|
+
throw new Error("empty HTML response");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { html: String(html), status, final_url: finalUrl };
|
|
143
|
+
} finally {
|
|
144
|
+
if (context) {
|
|
145
|
+
await context.close();
|
|
146
|
+
}
|
|
147
|
+
if (browser) {
|
|
148
|
+
await browser.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
try {
|
|
155
|
+
const options = parseArgs(process.argv.slice(2));
|
|
156
|
+
const result = await fetchWithPlaywright(options);
|
|
157
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const message = err?.message || String(err);
|
|
160
|
+
console.error(`[docdex] playwright fetch failed: ${message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (require.main === module) {
|
|
166
|
+
main();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
fetchWithPlaywright,
|
|
171
|
+
normalizeBrowser,
|
|
172
|
+
parseArgs,
|
|
173
|
+
resolveBrowserType
|
|
174
|
+
};
|