docdex 0.2.29 → 0.2.30

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 CHANGED
@@ -1,4 +1,4 @@
1
- ---- START OF DOCDEX INFO V0.2.29 ----
1
+ ---- START OF DOCDEX INFO V0.2.30 ----
2
2
  Docdex URL: http://127.0.0.1:28491
3
3
  Use this base URL for Docdex HTTP endpoints.
4
4
  Health check endpoint: `GET /healthz` (not `/v1/health`).
package/bin/docdex.js CHANGED
@@ -6,6 +6,7 @@ const path = require("node:path");
6
6
  const { spawn } = require("node:child_process");
7
7
 
8
8
  const pkg = require("../package.json");
9
+ const { resolveDistBaseDir, resolveDistBaseCandidates } = require("../lib/paths");
9
10
  const {
10
11
  artifactName,
11
12
  detectLibcFromRuntime,
@@ -53,6 +54,30 @@ function formatInstallSource(meta) {
53
54
  return `release (${source})`;
54
55
  }
55
56
 
57
+ function resolveInstallPaths(platformKey) {
58
+ const binaryName = process.platform === "win32" ? "docdexd.exe" : "docdexd";
59
+ const candidates = [];
60
+ for (const distBase of resolveDistBaseCandidates({ env: process.env })) {
61
+ candidates.push(path.join(distBase, platformKey));
62
+ }
63
+ candidates.push(path.join(__dirname, "..", "dist", platformKey));
64
+ const seen = new Set();
65
+ const unique = candidates.filter((candidate) => {
66
+ if (!candidate || seen.has(candidate)) return false;
67
+ seen.add(candidate);
68
+ return true;
69
+ });
70
+ for (const basePath of unique) {
71
+ const binaryPath = path.join(basePath, binaryName);
72
+ if (fs.existsSync(binaryPath)) {
73
+ return { basePath, binaryPath };
74
+ }
75
+ }
76
+ const fallbackBase =
77
+ unique[0] || path.join(resolveDistBaseDir({ env: process.env, fsModule: fs }), platformKey);
78
+ return { basePath: fallbackBase, binaryPath: path.join(fallbackBase, binaryName) };
79
+ }
80
+
56
81
  function runDoctor() {
57
82
  const platform = process.platform;
58
83
  const arch = process.arch;
@@ -81,7 +106,10 @@ function runDoctor() {
81
106
  const targetTriple = targetTripleForPlatformKey(platformKey);
82
107
  const expectedAssetName = artifactName(platformKey);
83
108
  const expectedAssetPattern = assetPatternForPlatformKey(platformKey, { exampleAssetName: expectedAssetName });
84
- const basePath = path.join(__dirname, "..", "dist", platformKey);
109
+ const distCandidates = resolveDistBaseCandidates({ env: process.env });
110
+ const distBase =
111
+ distCandidates[0] || resolveDistBaseDir({ env: process.env, fsModule: null });
112
+ const basePath = path.join(distBase, platformKey);
85
113
  const installMeta = readInstallMetadata({ fsModule: fs, pathModule: path, basePath });
86
114
  const installSource = formatInstallSource(installMeta);
87
115
 
@@ -179,11 +207,7 @@ async function run() {
179
207
  return;
180
208
  }
181
209
 
182
- const basePath = path.join(__dirname, "..", "dist", platformKey);
183
- const binaryPath = path.join(
184
- basePath,
185
- process.platform === "win32" ? "docdexd.exe" : "docdexd"
186
- );
210
+ const { binaryPath } = resolveInstallPaths(platformKey);
187
211
 
188
212
  if (!fs.existsSync(binaryPath)) {
189
213
  console.error(`[docdex] Missing binary for ${platformKey}. Try reinstalling or set DOCDEX_DOWNLOAD_REPO to a repo with release assets.`);
package/lib/install.js CHANGED
@@ -8,7 +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 } = require("node:child_process");
11
+ const { spawn, spawnSync } = require("node:child_process");
12
12
 
13
13
  const pkg = require("../package.json");
14
14
  const {
@@ -21,6 +21,7 @@ const {
21
21
  } = require("./platform");
22
22
  const { ManifestResolutionError, resolveCanonicalAssetForTargetTriple } = require("./release_manifest");
23
23
  const { runPostInstallSetup } = require("./postinstall_setup");
24
+ const { resolveDistBaseDir, resolveDistBaseCandidates } = require("./paths");
24
25
 
25
26
  const MAX_REDIRECTS = 5;
26
27
  const USER_AGENT = "docdex-installer";
@@ -368,6 +369,40 @@ async function extractTarballWithSystemTar(archivePath, targetDir) {
368
369
  });
369
370
  }
370
371
 
372
+ function escapePowerShellLiteral(value) {
373
+ return `'${String(value).replace(/'/g, "''")}'`;
374
+ }
375
+
376
+ function tryUnblockWindowsBinary(filePath, { logger, fsModule = fs, spawnSyncFn = spawnSync } = {}) {
377
+ if (process.platform !== "win32") return { ok: false, reason: "not_win32" };
378
+ if (!filePath) return { ok: false, reason: "missing_path" };
379
+ const existsSync = typeof fsModule.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
380
+ if (existsSync && !existsSync(filePath)) return { ok: false, reason: "missing_file" };
381
+ let unblocked = false;
382
+ try {
383
+ const zonePath = `${filePath}:Zone.Identifier`;
384
+ if (typeof fsModule.rmSync === "function") {
385
+ fsModule.rmSync(zonePath, { force: true });
386
+ unblocked = true;
387
+ } else if (typeof fsModule.unlinkSync === "function") {
388
+ try {
389
+ fsModule.unlinkSync(zonePath);
390
+ unblocked = true;
391
+ } catch {}
392
+ }
393
+ } catch {}
394
+ try {
395
+ const result = spawnSyncFn("powershell.exe", [
396
+ "-NoProfile",
397
+ "-Command",
398
+ `Unblock-File -LiteralPath ${escapePowerShellLiteral(filePath)}`
399
+ ], { stdio: "ignore" });
400
+ if (result?.status === 0) unblocked = true;
401
+ } catch {}
402
+ if (unblocked) return { ok: true, reason: "unblocked" };
403
+ return { ok: false, reason: "noop" };
404
+ }
405
+
371
406
  async function sha256File(filePath) {
372
407
  return new Promise((resolve, reject) => {
373
408
  const hash = crypto.createHash("sha256");
@@ -1954,11 +1989,16 @@ async function runInstaller(options) {
1954
1989
  manifestName: manifestAttempt?.manifestName ?? null,
1955
1990
  manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
1956
1991
  fallbackAttempted: source === "fallback",
1957
- binaryPath: stagedBinaryPath
1992
+ binaryPath: stagedBinaryPath,
1993
+ hint: isWin32 ? "possible_av_quarantine" : null
1958
1994
  });
1959
1995
  }
1960
1996
 
1961
- await fsModule.promises.chmod(stagedBinaryPath, 0o755);
1997
+ if (isWin32) {
1998
+ tryUnblockWindowsBinary(stagedBinaryPath, { logger, fsModule });
1999
+ } else {
2000
+ await fsModule.promises.chmod(stagedBinaryPath, 0o755);
2001
+ }
1962
2002
 
1963
2003
  if (existsSync && existsSync(distDir)) {
1964
2004
  await fsModule.promises.rm(backupDir, { recursive: true, force: true }).catch(() => {});
@@ -1974,6 +2014,9 @@ async function runInstaller(options) {
1974
2014
  }
1975
2015
 
1976
2016
  const binaryPath = pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd");
2017
+ if (isWin32) {
2018
+ tryUnblockWindowsBinary(binaryPath, { logger, fsModule });
2019
+ }
1977
2020
  const binarySha256 = await sha256FileFn(binaryPath);
1978
2021
  const metadata = {
1979
2022
  schemaVersion: INSTALL_METADATA_SCHEMA_VERSION,
@@ -2091,12 +2134,25 @@ async function runInstaller(options) {
2091
2134
  }
2092
2135
 
2093
2136
  async function main() {
2094
- const result = await runInstaller();
2137
+ const env = process.env;
2138
+ const distBaseCandidates = resolveDistBaseCandidates({ env });
2139
+ const distBaseDir = resolveDistBaseDir({ env, fsModule: fs });
2140
+ if (
2141
+ process.platform === "win32" &&
2142
+ !env?.DOCDEX_DIST_DIR &&
2143
+ distBaseCandidates[0] &&
2144
+ distBaseDir !== distBaseCandidates[0]
2145
+ ) {
2146
+ console.warn(
2147
+ `[docdex] LOCALAPPDATA not writable; using fallback dist dir: ${distBaseDir}. Set DOCDEX_DIST_DIR to override.`
2148
+ );
2149
+ }
2150
+ const result = await runInstaller({ env, distBaseDir });
2095
2151
  try {
2096
- await runPostInstallSetup({ binaryPath: result?.binaryPath });
2152
+ const skipDaemon = Boolean(env?.npm_lifecycle_event);
2153
+ await runPostInstallSetup({ binaryPath: result?.binaryPath, env, skipDaemon, distBaseDir });
2097
2154
  } catch (err) {
2098
2155
  console.warn(`[docdex] postinstall setup failed: ${err?.message || err}`);
2099
- throw err;
2100
2156
  }
2101
2157
  try {
2102
2158
  writeAgentInstructions();
@@ -2134,6 +2190,11 @@ function printPostInstallBanner() {
2134
2190
  "\x1b[33mSetup:\x1b[0m configures Ollama/models + browser.",
2135
2191
  "\x1b[34mTip:\x1b[0m after setup, the daemon should auto-start; if not, run \x1b[36m`docdexd daemon`\x1b[0m"
2136
2192
  ];
2193
+ if (process.platform === "win32") {
2194
+ content.push(
2195
+ "\x1b[33mNote:\x1b[0m If PowerShell blocks `docdex`, run `docdex.cmd` or set ExecutionPolicy RemoteSigned."
2196
+ );
2197
+ }
2137
2198
  width = Math.max(72, content.reduce((max, line) => Math.max(max, stripAnsi(line).length), 0));
2138
2199
  const padLine = (text) => {
2139
2200
  const visible = stripAnsi(text).length;
@@ -2361,18 +2422,23 @@ function describeFatalError(err) {
2361
2422
  }
2362
2423
 
2363
2424
  if (err instanceof ArchiveInvalidError) {
2425
+ const lines = [
2426
+ `[docdex] install failed: ${err.message}`,
2427
+ `[docdex] error code: ${err.code}`,
2428
+ err.details?.binaryPath ? `[docdex] Expected binary path: ${err.details.binaryPath}` : null
2429
+ ].filter(Boolean);
2430
+ if (process.platform === "win32" && err.details?.hint === "possible_av_quarantine") {
2431
+ lines.push(
2432
+ "[docdex] Windows Defender/AV may have quarantined the downloaded binary.",
2433
+ "[docdex] Re-run the install or add an exclusion for the Docdex dist directory.",
2434
+ "[docdex] Tip: set DOCDEX_DIST_DIR to a writable directory outside protected paths."
2435
+ );
2436
+ }
2364
2437
  return {
2365
2438
  code: err.code,
2366
2439
  exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
2367
2440
  details: withBaseDetails(err.details),
2368
- lines: appendInstallSafetyLines(
2369
- [
2370
- `[docdex] install failed: ${err.message}`,
2371
- `[docdex] error code: ${err.code}`,
2372
- err.details?.binaryPath ? `[docdex] Expected binary path: ${err.details.binaryPath}` : null
2373
- ].filter(Boolean),
2374
- err
2375
- )
2441
+ lines: appendInstallSafetyLines(lines, err)
2376
2442
  };
2377
2443
  }
2378
2444
 
@@ -2414,6 +2480,25 @@ function describeFatalError(err) {
2414
2480
  }
2415
2481
 
2416
2482
  const code = (err && typeof err.code === "string" && err.code) || "DOCDEX_INSTALL_FAILED";
2483
+ if (code === "EACCES" || code === "EPERM") {
2484
+ const location = err?.path ? ` (${err.path})` : "";
2485
+ return {
2486
+ code,
2487
+ exitCode: EXIT_CODE_BY_ERROR_CODE[code] || 1,
2488
+ details: withBaseDetails(err && err.details),
2489
+ lines: appendInstallSafetyLines(
2490
+ [
2491
+ `[docdex] install failed: ${err?.message || "permission denied"}`,
2492
+ `[docdex] error code: ${code}`,
2493
+ `[docdex] Ensure write access${location} or set DOCDEX_DIST_DIR to a writable location.`,
2494
+ process.platform === "win32"
2495
+ ? "[docdex] On Windows, run in an elevated shell if needed."
2496
+ : null
2497
+ ].filter(Boolean),
2498
+ err
2499
+ )
2500
+ };
2501
+ }
2417
2502
  return {
2418
2503
  code,
2419
2504
  exitCode: (err && typeof err.exitCode === "number" && err.exitCode) || EXIT_CODE_BY_ERROR_CODE[code] || 1,
package/lib/paths.js ADDED
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ function resolveUserDataDir({
8
+ env = process.env,
9
+ platform = process.platform,
10
+ homedir = os.homedir,
11
+ pathModule = path
12
+ } = {}) {
13
+ const home = typeof homedir === "function" ? homedir() : os.homedir();
14
+ if (platform === "win32") {
15
+ const base = env?.LOCALAPPDATA || pathModule.join(home, "AppData", "Local");
16
+ return pathModule.resolve(base);
17
+ }
18
+ if (platform === "darwin") {
19
+ return pathModule.join(home, "Library", "Application Support");
20
+ }
21
+ const xdg = env?.XDG_DATA_HOME;
22
+ if (xdg && String(xdg).trim()) return pathModule.resolve(String(xdg).trim());
23
+ return pathModule.join(home, ".local", "share");
24
+ }
25
+
26
+ function resolveDocdexDataDir(options = {}) {
27
+ const pathModule = options.pathModule || path;
28
+ return pathModule.join(resolveUserDataDir({ ...options, pathModule }), "docdex");
29
+ }
30
+
31
+ function resolveDistBaseCandidates(options = {}) {
32
+ const env = options.env || process.env;
33
+ const pathModule = options.pathModule || path;
34
+ const override = env?.DOCDEX_DIST_DIR;
35
+ if (override && String(override).trim()) {
36
+ return [pathModule.resolve(String(override).trim())];
37
+ }
38
+ const candidates = [];
39
+ const primary = pathModule.join(resolveDocdexDataDir({ ...options, pathModule }), "dist");
40
+ if (primary) candidates.push(primary);
41
+ const homedir = typeof options.homedir === "function" ? options.homedir : os.homedir;
42
+ const home = homedir ? homedir() : os.homedir();
43
+ if (home) {
44
+ const fallback = pathModule.join(home, ".docdex", "dist");
45
+ if (!candidates.includes(fallback)) candidates.push(fallback);
46
+ }
47
+ return candidates;
48
+ }
49
+
50
+ function findExistingParent(candidate, fsModule, pathModule) {
51
+ if (!candidate) return null;
52
+ let current = pathModule.resolve(candidate);
53
+ while (current && !fsModule.existsSync(current)) {
54
+ const parent = pathModule.dirname(current);
55
+ if (parent === current) return null;
56
+ current = parent;
57
+ }
58
+ return current;
59
+ }
60
+
61
+ function canWritePath(candidate, fsModule) {
62
+ if (!fsModule?.accessSync) return true;
63
+ const mode = fsModule.constants?.W_OK ?? 0o2;
64
+ try {
65
+ fsModule.accessSync(candidate, mode);
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function resolveDistBaseDir(options = {}) {
73
+ const fsModule = options.fsModule || fs;
74
+ const pathModule = options.pathModule || path;
75
+ const candidates = resolveDistBaseCandidates({ ...options, pathModule });
76
+ if (!candidates.length) {
77
+ return pathModule.join(resolveDocdexDataDir({ ...options, pathModule }), "dist");
78
+ }
79
+ for (const candidate of candidates) {
80
+ if (!fsModule?.existsSync) return candidate;
81
+ const parent = findExistingParent(candidate, fsModule, pathModule);
82
+ if (parent && canWritePath(parent, fsModule)) {
83
+ return candidate;
84
+ }
85
+ }
86
+ return candidates[0];
87
+ }
88
+
89
+ function resolveBinDir(options = {}) {
90
+ const pathModule = options.pathModule || path;
91
+ if (options.distBaseDir) {
92
+ return pathModule.join(pathModule.dirname(options.distBaseDir), "bin");
93
+ }
94
+ return pathModule.join(resolveDocdexDataDir({ ...options, pathModule }), "bin");
95
+ }
96
+
97
+ function resolveWindowsRunnerPath(options = {}) {
98
+ const pathModule = options.pathModule || path;
99
+ if (options.distBaseDir) {
100
+ return pathModule.join(pathModule.dirname(options.distBaseDir), "run-daemon.cmd");
101
+ }
102
+ return pathModule.join(resolveDocdexDataDir({ ...options, pathModule }), "run-daemon.cmd");
103
+ }
104
+
105
+ module.exports = {
106
+ resolveUserDataDir,
107
+ resolveDocdexDataDir,
108
+ resolveDistBaseCandidates,
109
+ resolveDistBaseDir,
110
+ resolveBinDir,
111
+ resolveWindowsRunnerPath
112
+ };
@@ -11,6 +11,12 @@ const tty = require("node:tty");
11
11
  const { spawn, spawnSync } = require("node:child_process");
12
12
 
13
13
  const { detectPlatformKey, UnsupportedPlatformError } = require("./platform");
14
+ const {
15
+ resolveDistBaseDir,
16
+ resolveDistBaseCandidates,
17
+ resolveBinDir,
18
+ resolveWindowsRunnerPath
19
+ } = require("./paths");
14
20
 
15
21
  const DEFAULT_HOST = "127.0.0.1";
16
22
  const DEFAULT_DAEMON_PORT = 28491;
@@ -1391,12 +1397,38 @@ function clientInstructionPaths() {
1391
1397
  }
1392
1398
  }
1393
1399
 
1394
- function resolveBinaryPath({ binaryPath } = {}) {
1400
+ function sanitizeVersionForFilename(version) {
1401
+ if (!version) return null;
1402
+ return String(version).replace(/[^0-9A-Za-z._-]/g, "_");
1403
+ }
1404
+
1405
+ function resolveBinaryPath({ binaryPath, env, distBaseDir, distBaseCandidates } = {}) {
1395
1406
  if (binaryPath && fs.existsSync(binaryPath)) return binaryPath;
1396
1407
  try {
1397
1408
  const platformKey = detectPlatformKey();
1398
- const candidate = path.join(__dirname, "..", "dist", platformKey, process.platform === "win32" ? "docdexd.exe" : "docdexd");
1399
- if (fs.existsSync(candidate)) return candidate;
1409
+ const isWin32 = process.platform === "win32";
1410
+ const resolvedVersion = sanitizeVersionForFilename(
1411
+ normalizeVersion(resolvePackageVersion())
1412
+ );
1413
+ const resolvedDistBaseDir = distBaseDir || resolveDistBaseDir({ env, fsModule: fs });
1414
+ const binDir = resolveBinDir({ env, distBaseDir: resolvedDistBaseDir });
1415
+ const baseCandidates = distBaseCandidates || resolveDistBaseCandidates({ env });
1416
+ const candidates = [];
1417
+ if (binDir) {
1418
+ if (isWin32 && resolvedVersion) {
1419
+ candidates.push(path.join(binDir, `docdexd-${resolvedVersion}.exe`));
1420
+ }
1421
+ candidates.push(path.join(binDir, isWin32 ? "docdexd.exe" : "docdexd"));
1422
+ }
1423
+ for (const base of baseCandidates) {
1424
+ candidates.push(path.join(base, platformKey, isWin32 ? "docdexd.exe" : "docdexd"));
1425
+ }
1426
+ candidates.push(
1427
+ path.join(__dirname, "..", "dist", platformKey, isWin32 ? "docdexd.exe" : "docdexd")
1428
+ );
1429
+ for (const candidate of candidates) {
1430
+ if (candidate && fs.existsSync(candidate)) return candidate;
1431
+ }
1400
1432
  } catch (err) {
1401
1433
  if (!(err instanceof UnsupportedPlatformError)) throw err;
1402
1434
  }
@@ -1416,16 +1448,26 @@ function isMacProtectedPath(candidate) {
1416
1448
  return ["Desktop", "Documents", "Downloads"].some((dir) => isPathWithin(path.join(home, dir), candidate));
1417
1449
  }
1418
1450
 
1419
- function ensureStartupBinary(binaryPath, { logger } = {}) {
1451
+ function ensureStartupBinary(binaryPath, { logger, env, distBaseDir } = {}) {
1420
1452
  if (!binaryPath) return null;
1421
- if (!isMacProtectedPath(binaryPath) && !isTempPath(binaryPath)) return binaryPath;
1422
- const binDir = path.join(os.homedir(), ".docdex", "bin");
1423
- const target = path.join(binDir, path.basename(binaryPath));
1453
+ const isWin32 = process.platform === "win32";
1454
+ const mustCopy = isWin32 || isMacProtectedPath(binaryPath) || isTempPath(binaryPath);
1455
+ if (!mustCopy) return binaryPath;
1456
+ const binDir = resolveBinDir({ env, distBaseDir });
1457
+ const resolvedVersion = sanitizeVersionForFilename(
1458
+ normalizeVersion(resolvePackageVersion())
1459
+ );
1460
+ const targetName = isWin32 && resolvedVersion
1461
+ ? `docdexd-${resolvedVersion}.exe`
1462
+ : path.basename(binaryPath);
1463
+ const target = path.join(binDir, targetName);
1424
1464
  if (fs.existsSync(target)) return target;
1425
1465
  try {
1426
1466
  fs.mkdirSync(binDir, { recursive: true });
1427
1467
  fs.copyFileSync(binaryPath, target);
1428
- fs.chmodSync(target, 0o755);
1468
+ if (!isWin32) {
1469
+ fs.chmodSync(target, 0o755);
1470
+ }
1429
1471
  return target;
1430
1472
  } catch (err) {
1431
1473
  logger?.warn?.(`[docdex] failed to stage daemon binary for startup: ${err?.message || err}`);
@@ -1433,8 +1475,8 @@ function ensureStartupBinary(binaryPath, { logger } = {}) {
1433
1475
  }
1434
1476
  }
1435
1477
 
1436
- function resolveStartupBinaryPaths({ binaryPath, logger } = {}) {
1437
- const resolvedBinary = ensureStartupBinary(binaryPath, { logger });
1478
+ function resolveStartupBinaryPaths({ binaryPath, logger, env, distBaseDir } = {}) {
1479
+ const resolvedBinary = ensureStartupBinary(binaryPath, { logger, env, distBaseDir });
1438
1480
  return { binaryPath: resolvedBinary };
1439
1481
  }
1440
1482
 
@@ -2112,7 +2154,32 @@ function buildDaemonEnv() {
2112
2154
  return Object.fromEntries(buildDaemonEnvPairs());
2113
2155
  }
2114
2156
 
2115
- function registerStartup({ binaryPath, port, repoRoot, logger }) {
2157
+ function escapeCmdArg(value) {
2158
+ return `"${String(value).replace(/"/g, "\"\"")}"`;
2159
+ }
2160
+
2161
+ function writeWindowsRunner({ binaryPath, args, envPairs, workingDir, logger, distBaseDir } = {}) {
2162
+ const runnerPath = resolveWindowsRunnerPath({ distBaseDir });
2163
+ const lines = ["@echo off", "setlocal"];
2164
+ for (const [key, value] of envPairs || []) {
2165
+ lines.push(`set "${key}=${value}"`);
2166
+ }
2167
+ if (workingDir) {
2168
+ lines.push(`cd /d ${escapeCmdArg(workingDir)}`);
2169
+ }
2170
+ const argString = (args || []).map((arg) => escapeCmdArg(arg)).join(" ");
2171
+ lines.push(`${escapeCmdArg(binaryPath)} ${argString}`.trim());
2172
+ try {
2173
+ fs.mkdirSync(path.dirname(runnerPath), { recursive: true });
2174
+ fs.writeFileSync(runnerPath, `${lines.join("\r\n")}\r\n`);
2175
+ return runnerPath;
2176
+ } catch (err) {
2177
+ logger?.warn?.(`[docdex] failed to write Windows runner: ${err?.message || err}`);
2178
+ return null;
2179
+ }
2180
+ }
2181
+
2182
+ function registerStartup({ binaryPath, port, repoRoot, logger, distBaseDir }) {
2116
2183
  if (!binaryPath) return { ok: false, reason: "missing_binary" };
2117
2184
  stopDaemonService({ logger });
2118
2185
  const envPairs = buildDaemonEnvPairs();
@@ -2208,11 +2275,16 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
2208
2275
 
2209
2276
  if (process.platform === "win32") {
2210
2277
  const taskName = "Docdex Daemon";
2211
- const joinedArgs = args.map((arg) => `"${arg}"`).join(" ");
2212
- const envParts = envPairs.map(([key, value]) => `set "${key}=${value}"`);
2213
- const cdCommand = workingDir ? `cd /d "${workingDir}"` : null;
2214
- const taskArgs =
2215
- `"cmd.exe" /c "${envParts.join(" && ")}${cdCommand ? ` && ${cdCommand}` : ""} && \"${binaryPath}\" ${joinedArgs}"`;
2278
+ const runnerPath = writeWindowsRunner({
2279
+ binaryPath,
2280
+ args,
2281
+ envPairs,
2282
+ workingDir,
2283
+ logger,
2284
+ distBaseDir
2285
+ });
2286
+ if (!runnerPath) return { ok: false, reason: "runner_failed" };
2287
+ const taskArgs = `cmd.exe /c ${escapeCmdArg(runnerPath)}`;
2216
2288
  const create = spawnSync("schtasks", [
2217
2289
  "/Create",
2218
2290
  "/F",
@@ -2236,12 +2308,13 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
2236
2308
  return { ok: false, reason: "unsupported_platform" };
2237
2309
  }
2238
2310
 
2239
- async function startDaemonWithHealthCheck({ binaryPath, port, host, logger }) {
2311
+ async function startDaemonWithHealthCheck({ binaryPath, port, host, logger, distBaseDir }) {
2240
2312
  const startup = registerStartup({
2241
2313
  binaryPath,
2242
2314
  port,
2243
2315
  repoRoot: daemonRootPath(),
2244
- logger
2316
+ logger,
2317
+ distBaseDir
2245
2318
  });
2246
2319
  if (!startup.ok) {
2247
2320
  logger?.warn?.(`[docdex] daemon service registration failed (${startup.reason || "unknown"}).`);
@@ -2281,6 +2354,17 @@ function startupFailureReported() {
2281
2354
  return fs.existsSync(path.join(stateDir(), STARTUP_FAILURE_MARKER));
2282
2355
  }
2283
2356
 
2357
+ function isNpmLifecycle(env = process.env) {
2358
+ return Boolean(env?.npm_lifecycle_event);
2359
+ }
2360
+
2361
+ function shouldSkipDaemonSideEffects({ env = process.env, skipDaemon } = {}) {
2362
+ if (skipDaemon) return true;
2363
+ if (isNpmLifecycle(env)) return true;
2364
+ if (parseEnvBool(env?.DOCDEX_DAEMON_SKIP_SETUP)) return true;
2365
+ return false;
2366
+ }
2367
+
2284
2368
  function shouldSkipSetup(env = process.env) {
2285
2369
  return parseEnvBool(env.DOCDEX_SETUP_SKIP) === true;
2286
2370
  }
@@ -2381,34 +2465,49 @@ function launchSetupWizard({
2381
2465
  return { ok: false, reason: "unsupported_platform" };
2382
2466
  }
2383
2467
 
2384
- async function runPostInstallSetup({ binaryPath, logger } = {}) {
2468
+ async function runPostInstallSetup({ binaryPath, logger, env, skipDaemon, distBaseDir } = {}) {
2385
2469
  const log = logger || console;
2470
+ const effectiveEnv = env || process.env;
2471
+ const distCandidates = resolveDistBaseCandidates({ env: effectiveEnv });
2472
+ const resolvedDistBaseDir = distBaseDir || resolveDistBaseDir({ env: effectiveEnv, fsModule: fs });
2473
+ let allowDaemon = !shouldSkipDaemonSideEffects({ env: effectiveEnv, skipDaemon });
2386
2474
  const configPath = defaultConfigPath();
2387
2475
  let existingConfig = "";
2388
2476
  if (fs.existsSync(configPath)) {
2389
2477
  existingConfig = fs.readFileSync(configPath, "utf8");
2390
2478
  }
2391
2479
  const port = DEFAULT_DAEMON_PORT;
2392
- const portState = await resolveDaemonPortState({
2393
- host: DEFAULT_HOST,
2394
- port,
2395
- logger: log
2396
- });
2397
- if (!portState.available && !portState.reuseExisting) {
2398
- log.error?.(
2399
- `[docdex] ${DEFAULT_HOST}:${port} is already in use; docdex requires a fixed port. Stop the process using this port and re-run the install.`
2400
- );
2401
- throw new Error(`docdex requires ${DEFAULT_HOST}:${port}, but the port is already in use`);
2480
+ let portState = { available: true, reuseExisting: false };
2481
+ if (allowDaemon) {
2482
+ portState = await resolveDaemonPortState({
2483
+ host: DEFAULT_HOST,
2484
+ port,
2485
+ logger: log
2486
+ });
2487
+ if (!portState.available && !portState.reuseExisting) {
2488
+ log.warn?.(
2489
+ `[docdex] ${DEFAULT_HOST}:${port} is already in use; skipping daemon startup. Run \`docdexd daemon\` after freeing the port.`
2490
+ );
2491
+ recordStartupFailure({ reason: "port_in_use", host: DEFAULT_HOST, port });
2492
+ allowDaemon = false;
2493
+ }
2402
2494
  }
2403
2495
 
2404
2496
  const daemonRoot = ensureDaemonRoot();
2405
- const resolvedBinary = resolveBinaryPath({ binaryPath });
2497
+ const resolvedBinary = resolveBinaryPath({
2498
+ binaryPath,
2499
+ env: effectiveEnv,
2500
+ distBaseDir: resolvedDistBaseDir,
2501
+ distBaseCandidates: distCandidates
2502
+ });
2406
2503
  const startupBinaries = resolveStartupBinaryPaths({
2407
2504
  binaryPath: resolvedBinary,
2408
- logger: log
2505
+ logger: log,
2506
+ env: effectiveEnv,
2507
+ distBaseDir: resolvedDistBaseDir
2409
2508
  });
2410
- let reuseExisting = portState.reuseExisting;
2411
- if (reuseExisting) {
2509
+ let reuseExisting = allowDaemon ? portState.reuseExisting : false;
2510
+ if (reuseExisting && allowDaemon) {
2412
2511
  const daemonInfo = await fetchDaemonInfo({ host: DEFAULT_HOST, port });
2413
2512
  const daemonVersion = normalizeVersion(daemonInfo?.version);
2414
2513
  const packageVersion = normalizeVersion(resolvePackageVersion());
@@ -2426,22 +2525,29 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
2426
2525
  port
2427
2526
  });
2428
2527
  if (!released) {
2429
- throw new Error("docdex daemon restart failed; port still in use");
2528
+ log.warn?.("[docdex] daemon restart failed; port still in use.");
2529
+ recordStartupFailure({ reason: "restart_failed", host: DEFAULT_HOST, port });
2530
+ allowDaemon = false;
2430
2531
  }
2431
2532
  reuseExisting = false;
2432
2533
  }
2433
2534
  }
2434
2535
  }
2435
- if (!reuseExisting) {
2536
+ let startupOk = reuseExisting;
2537
+ if (allowDaemon && !reuseExisting) {
2436
2538
  const result = await startDaemonWithHealthCheck({
2437
2539
  binaryPath: startupBinaries.binaryPath,
2438
2540
  port,
2439
2541
  host: DEFAULT_HOST,
2440
- logger: log
2542
+ logger: log,
2543
+ distBaseDir: resolvedDistBaseDir
2441
2544
  });
2442
2545
  if (!result.ok) {
2443
2546
  log.warn?.(`[docdex] daemon failed to start on ${DEFAULT_HOST}:${port}.`);
2444
- throw new Error("docdex daemon failed to start");
2547
+ recordStartupFailure({ reason: result.reason || "startup_failed", host: DEFAULT_HOST, port });
2548
+ allowDaemon = false;
2549
+ } else {
2550
+ startupOk = true;
2445
2551
  }
2446
2552
  }
2447
2553
 
@@ -2475,8 +2581,13 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
2475
2581
  }
2476
2582
  upsertCodexConfig(paths.codex, codexUrl);
2477
2583
  applyAgentInstructions({ logger: log });
2478
- clearStartupFailure();
2479
- const setupLaunch = launchSetupWizard({ binaryPath: startupBinaries.binaryPath, logger: log });
2584
+ if (startupOk) {
2585
+ clearStartupFailure();
2586
+ }
2587
+ const skipWizard = isNpmLifecycle(effectiveEnv) || shouldSkipSetup(effectiveEnv);
2588
+ const setupLaunch = skipWizard
2589
+ ? { ok: false, reason: "skipped" }
2590
+ : launchSetupWizard({ binaryPath: startupBinaries.binaryPath, logger: log });
2480
2591
  if (!setupLaunch.ok && setupLaunch.reason !== "skipped") {
2481
2592
  log.warn?.("[docdex] setup wizard did not launch. Run `docdex setup`.");
2482
2593
  recordSetupPending({ reason: setupLaunch.reason, port, repoRoot: daemonRoot });
package/lib/uninstall.js CHANGED
@@ -6,6 +6,7 @@ const net = require("node:net");
6
6
  const os = require("node:os");
7
7
  const path = require("node:path");
8
8
  const { spawnSync } = require("node:child_process");
9
+ const { resolveDocdexDataDir } = require("./paths");
9
10
 
10
11
  const DAEMON_TASK_NAME = "Docdex Daemon";
11
12
  const STARTUP_FAILURE_MARKER = "startup_registration_failed.json";
@@ -24,6 +25,10 @@ function daemonRootPath() {
24
25
  return path.join(docdexRootPath(), "daemon_root");
25
26
  }
26
27
 
28
+ function docdexDataDir() {
29
+ return resolveDocdexDataDir({ env: process.env });
30
+ }
31
+
27
32
  function stateDir() {
28
33
  return path.join(docdexRootPath(), "state");
29
34
  }
@@ -108,12 +113,15 @@ function manualStopInstructions() {
108
113
  ];
109
114
  }
110
115
  if (process.platform === "win32") {
116
+ const dataDir = docdexDataDir();
111
117
  return [
112
118
  "Manual cleanup required:",
113
119
  `- schtasks /End /TN "${DAEMON_TASK_NAME}"`,
114
120
  "- schtasks /Delete /TN \"Docdex Daemon\" /F",
115
121
  "- taskkill /IM docdexd.exe /T /F",
116
122
  "- del %USERPROFILE%\\.docdex\\locks\\daemon.lock",
123
+ `- del "${path.join(dataDir, "run-daemon.cmd")}"`,
124
+ `- rmdir /S /Q "${dataDir}"`,
117
125
  "- netstat -ano | findstr 28491",
118
126
  ];
119
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "mcpName": "io.github.bekirdag/docdex",
5
5
  "description": "Local-first documentation and code indexer with HTTP/MCP search, AST, and agent memory.",
6
6
  "bin": {