docdex 0.2.19 → 0.2.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.21
4
+ - Prompt for npm updates at CLI start (TTY-only, opt-out via `DOCDEX_UPDATE_CHECK=0`).
5
+ - Export bundled Playwright fetcher for daemon startup (launchd/systemd/schtasks + immediate spawn).
6
+ - Pass `DOCDEX_PLAYWRIGHT_FETCHER` in the npm wrapper when launching the daemon.
7
+
3
8
  ## 0.2.19
4
9
  - Playwright issue fix
5
10
  - Agents md adding command manually
package/bin/docdex.js CHANGED
@@ -5,6 +5,7 @@ const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const { spawn } = require("node:child_process");
7
7
 
8
+ const pkg = require("../package.json");
8
9
  const {
9
10
  artifactName,
10
11
  detectLibcFromRuntime,
@@ -13,6 +14,7 @@ const {
13
14
  assetPatternForPlatformKey,
14
15
  UnsupportedPlatformError
15
16
  } = require("../lib/platform");
17
+ const { checkForUpdateOnce } = require("../lib/update_check");
16
18
 
17
19
  function isDoctorCommand(argv) {
18
20
  const sub = argv[0];
@@ -139,7 +141,7 @@ function runDoctor() {
139
141
  process.exit(report.exitCode);
140
142
  }
141
143
 
142
- function run() {
144
+ async function run() {
143
145
  const argv = process.argv.slice(2);
144
146
  if (isDoctorCommand(argv)) {
145
147
  runDoctor();
@@ -164,9 +166,11 @@ function run() {
164
166
  }
165
167
  console.error("[docdex] Next steps: use a supported platform or build from source (Rust).");
166
168
  process.exit(err.exitCode || 3);
169
+ return;
167
170
  }
168
171
  console.error(`[docdex] failed to detect platform: ${err?.message || String(err)}`);
169
172
  process.exit(1);
173
+ return;
170
174
  }
171
175
 
172
176
  const basePath = path.join(__dirname, "..", "dist", platformKey);
@@ -186,12 +190,25 @@ function run() {
186
190
  console.error(`[docdex] Asset naming pattern: ${assetPatternForPlatformKey(platformKey)}`);
187
191
  } catch {}
188
192
  process.exit(1);
193
+ return;
189
194
  }
190
195
 
196
+ await checkForUpdateOnce({
197
+ currentVersion: pkg.version,
198
+ env: process.env,
199
+ stdout: process.stdout,
200
+ stderr: process.stderr,
201
+ logger: console
202
+ });
203
+
191
204
  const env = { ...process.env };
192
205
  if (!env.DOCDEX_MCP_SERVER_BIN && fs.existsSync(mcpBinaryPath)) {
193
206
  env.DOCDEX_MCP_SERVER_BIN = mcpBinaryPath;
194
207
  }
208
+ const fetcherPath = path.join(__dirname, "..", "lib", "playwright_fetch.js");
209
+ if (!env.DOCDEX_PLAYWRIGHT_FETCHER && fs.existsSync(fetcherPath)) {
210
+ env.DOCDEX_PLAYWRIGHT_FETCHER = fetcherPath;
211
+ }
195
212
  const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", env });
196
213
  child.on("exit", (code) => process.exit(code ?? 1));
197
214
  child.on("error", (err) => {
@@ -200,4 +217,7 @@ function run() {
200
217
  });
201
218
  }
202
219
 
203
- run();
220
+ run().catch((err) => {
221
+ console.error(`[docdex] unexpected error: ${err?.message || String(err)}`);
222
+ process.exit(1);
223
+ });
@@ -1464,8 +1464,26 @@ async function maybePromptOllamaModel({
1464
1464
  return { status: "skipped", reason: "invalid_selection" };
1465
1465
  }
1466
1466
 
1467
+ function resolvePlaywrightFetcherPath() {
1468
+ const candidate = path.join(__dirname, "playwright_fetch.js");
1469
+ return fs.existsSync(candidate) ? candidate : null;
1470
+ }
1471
+
1472
+ function buildDaemonEnvPairs({ mcpBinaryPath } = {}) {
1473
+ const pairs = [["DOCDEX_BROWSER_AUTO_INSTALL", "0"]];
1474
+ if (mcpBinaryPath) pairs.push(["DOCDEX_MCP_SERVER_BIN", mcpBinaryPath]);
1475
+ const fetcher = resolvePlaywrightFetcherPath();
1476
+ if (fetcher) pairs.push(["DOCDEX_PLAYWRIGHT_FETCHER", fetcher]);
1477
+ return pairs;
1478
+ }
1479
+
1480
+ function buildDaemonEnv({ mcpBinaryPath } = {}) {
1481
+ return Object.fromEntries(buildDaemonEnvPairs({ mcpBinaryPath }));
1482
+ }
1483
+
1467
1484
  function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger }) {
1468
1485
  if (!binaryPath) return { ok: false, reason: "missing_binary" };
1486
+ const envPairs = buildDaemonEnvPairs({ mcpBinaryPath });
1469
1487
  const args = [
1470
1488
  "daemon",
1471
1489
  "--repo",
@@ -1478,21 +1496,16 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
1478
1496
  "warn",
1479
1497
  "--secure-mode=false"
1480
1498
  ];
1481
- const envMcpBin = mcpBinaryPath ? `DOCDEX_MCP_SERVER_BIN=${mcpBinaryPath}` : null;
1482
1499
 
1483
1500
  if (process.platform === "darwin") {
1484
1501
  const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.docdex.daemon.plist");
1485
1502
  const logDir = path.join(os.homedir(), ".docdex", "logs");
1486
1503
  fs.mkdirSync(logDir, { recursive: true });
1487
1504
  const programArgs = [binaryPath, ...args];
1488
- const envVars = [
1489
- " <key>DOCDEX_BROWSER_AUTO_INSTALL</key>\n",
1490
- " <string>0</string>\n"
1491
- ];
1492
- if (mcpBinaryPath) {
1493
- envVars.push(" <key>DOCDEX_MCP_SERVER_BIN</key>\n");
1494
- envVars.push(` <string>${mcpBinaryPath}</string>\n`);
1495
- }
1505
+ const envVars = envPairs.flatMap(([key, value]) => [
1506
+ ` <key>${key}</key>\n`,
1507
+ ` <string>${value}</string>\n`
1508
+ ]);
1496
1509
  const plist = `<?xml version="1.0" encoding="UTF-8"?>\n` +
1497
1510
  `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n` +
1498
1511
  `<plist version="1.0">\n` +
@@ -1534,6 +1547,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
1534
1547
  const systemdDir = path.join(os.homedir(), ".config", "systemd", "user");
1535
1548
  const unitPath = path.join(systemdDir, "docdexd.service");
1536
1549
  fs.mkdirSync(systemdDir, { recursive: true });
1550
+ const envLines = envPairs.map(([key, value]) => `Environment=${key}=${value}`);
1537
1551
  const unit = [
1538
1552
  "[Unit]",
1539
1553
  "Description=Docdex daemon",
@@ -1541,8 +1555,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
1541
1555
  "",
1542
1556
  "[Service]",
1543
1557
  `ExecStart=${binaryPath} ${args.join(" ")}`,
1544
- "Environment=DOCDEX_BROWSER_AUTO_INSTALL=0",
1545
- envMcpBin ? `Environment=${envMcpBin}` : null,
1558
+ ...envLines,
1546
1559
  "Restart=always",
1547
1560
  "RestartSec=2",
1548
1561
  "",
@@ -1561,10 +1574,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
1561
1574
  if (process.platform === "win32") {
1562
1575
  const taskName = "Docdex Daemon";
1563
1576
  const joinedArgs = args.map((arg) => `"${arg}"`).join(" ");
1564
- const envParts = ['set "DOCDEX_BROWSER_AUTO_INSTALL=0"'];
1565
- if (mcpBinaryPath) {
1566
- envParts.push(`set "DOCDEX_MCP_SERVER_BIN=${mcpBinaryPath}"`);
1567
- }
1577
+ const envParts = envPairs.map(([key, value]) => `set "${key}=${value}"`);
1568
1578
  const taskArgs =
1569
1579
  `"cmd.exe" /c "${envParts.join(" && ")} && \"${binaryPath}\" ${joinedArgs}"`;
1570
1580
  const create = spawnSync("schtasks", [
@@ -1592,10 +1602,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
1592
1602
 
1593
1603
  function startDaemonNow({ binaryPath, mcpBinaryPath, port, repoRoot }) {
1594
1604
  if (!binaryPath) return false;
1595
- const extraEnv = {};
1596
- if (mcpBinaryPath) {
1597
- extraEnv.DOCDEX_MCP_SERVER_BIN = mcpBinaryPath;
1598
- }
1605
+ const extraEnv = buildDaemonEnv({ mcpBinaryPath });
1599
1606
  const child = spawn(
1600
1607
  binaryPath,
1601
1608
  [
@@ -1615,7 +1622,6 @@ function startDaemonNow({ binaryPath, mcpBinaryPath, port, repoRoot }) {
1615
1622
  detached: true,
1616
1623
  env: {
1617
1624
  ...process.env,
1618
- DOCDEX_BROWSER_AUTO_INSTALL: "0",
1619
1625
  ...extraEnv
1620
1626
  }
1621
1627
  }
@@ -1847,5 +1853,6 @@ module.exports = {
1847
1853
  canPromptWithTty,
1848
1854
  shouldSkipSetup,
1849
1855
  launchSetupWizard,
1850
- applyAgentInstructions
1856
+ applyAgentInstructions,
1857
+ buildDaemonEnv
1851
1858
  };
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ const https = require("node:https");
4
+
5
+ const UPDATE_CHECK_ENV = "DOCDEX_UPDATE_CHECK";
6
+ const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/docdex/latest";
7
+ const DEFAULT_TIMEOUT_MS = 1500;
8
+ const MAX_RESPONSE_BYTES = 128 * 1024;
9
+
10
+ let hasChecked = false;
11
+
12
+ function normalizeVersion(value) {
13
+ if (typeof value !== "string") return "";
14
+ return value.trim().replace(/^v/i, "");
15
+ }
16
+
17
+ function parseSemver(value) {
18
+ const normalized = normalizeVersion(value);
19
+ const match = normalized.match(
20
+ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u
21
+ );
22
+ if (!match) return null;
23
+ return {
24
+ major: Number(match[1]),
25
+ minor: Number(match[2]),
26
+ patch: Number(match[3]),
27
+ prerelease: match[4] ? match[4].split(".") : null
28
+ };
29
+ }
30
+
31
+ function compareIdentifiers(left, right) {
32
+ const leftNum = /^[0-9]+$/.test(left) ? Number(left) : null;
33
+ const rightNum = /^[0-9]+$/.test(right) ? Number(right) : null;
34
+
35
+ if (leftNum != null && rightNum != null) {
36
+ if (leftNum === rightNum) return 0;
37
+ return leftNum > rightNum ? 1 : -1;
38
+ }
39
+
40
+ if (leftNum != null) return -1;
41
+ if (rightNum != null) return 1;
42
+ if (left === right) return 0;
43
+ return left > right ? 1 : -1;
44
+ }
45
+
46
+ function comparePrerelease(left, right) {
47
+ if (!left && !right) return 0;
48
+ if (!left) return 1;
49
+ if (!right) return -1;
50
+
51
+ const length = Math.max(left.length, right.length);
52
+ for (let i = 0; i < length; i += 1) {
53
+ const leftId = left[i];
54
+ const rightId = right[i];
55
+ if (leftId == null) return -1;
56
+ if (rightId == null) return 1;
57
+ const result = compareIdentifiers(leftId, rightId);
58
+ if (result !== 0) return result;
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ function compareSemver(left, right) {
64
+ if (!left || !right) return null;
65
+ if (left.major !== right.major) return left.major > right.major ? 1 : -1;
66
+ if (left.minor !== right.minor) return left.minor > right.minor ? 1 : -1;
67
+ if (left.patch !== right.patch) return left.patch > right.patch ? 1 : -1;
68
+ return comparePrerelease(left.prerelease, right.prerelease);
69
+ }
70
+
71
+ function isDisabledEnv(value) {
72
+ if (value == null) return false;
73
+ const normalized = String(value).trim().toLowerCase();
74
+ return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
75
+ }
76
+
77
+ function isEnabledEnv(value) {
78
+ if (value == null) return false;
79
+ const normalized = String(value).trim().toLowerCase();
80
+ return normalized === "1" || normalized === "true" || normalized === "on" || normalized === "yes";
81
+ }
82
+
83
+ function isInteractive({ stdout, stderr } = {}) {
84
+ return Boolean(stdout?.isTTY || stderr?.isTTY);
85
+ }
86
+
87
+ function shouldCheckForUpdate({ env, stdout, stderr } = {}) {
88
+ const envValue = env?.[UPDATE_CHECK_ENV];
89
+ if (isDisabledEnv(envValue)) return false;
90
+ if (env?.CI && !isEnabledEnv(envValue)) return false;
91
+ if (!isInteractive({ stdout, stderr }) && !isEnabledEnv(envValue)) return false;
92
+ return true;
93
+ }
94
+
95
+ function fetchLatestVersion({
96
+ httpsModule = https,
97
+ registryUrl = DEFAULT_REGISTRY_URL,
98
+ timeoutMs = DEFAULT_TIMEOUT_MS,
99
+ maxBytes = MAX_RESPONSE_BYTES
100
+ } = {}) {
101
+ if (!httpsModule || typeof httpsModule.request !== "function") {
102
+ return Promise.resolve(null);
103
+ }
104
+
105
+ return new Promise((resolve) => {
106
+ let resolved = false;
107
+ const finish = (value) => {
108
+ if (resolved) return;
109
+ resolved = true;
110
+ resolve(value);
111
+ };
112
+
113
+ const req = httpsModule.request(
114
+ registryUrl,
115
+ {
116
+ method: "GET",
117
+ headers: {
118
+ "User-Agent": "docdex-update-check",
119
+ Accept: "application/json"
120
+ }
121
+ },
122
+ (res) => {
123
+ if (!res || res.statusCode !== 200) {
124
+ res?.resume?.();
125
+ finish(null);
126
+ return;
127
+ }
128
+ res.setEncoding?.("utf8");
129
+ let body = "";
130
+ res.on("data", (chunk) => {
131
+ body += chunk;
132
+ if (body.length > maxBytes) {
133
+ req.destroy?.();
134
+ finish(null);
135
+ }
136
+ });
137
+ res.on("end", () => {
138
+ if (!body) {
139
+ finish(null);
140
+ return;
141
+ }
142
+ try {
143
+ const parsed = JSON.parse(body);
144
+ const version = typeof parsed?.version === "string" ? parsed.version : null;
145
+ finish(version);
146
+ } catch {
147
+ finish(null);
148
+ }
149
+ });
150
+ }
151
+ );
152
+
153
+ req.on("error", () => finish(null));
154
+ if (typeof req.setTimeout === "function") {
155
+ req.setTimeout(timeoutMs, () => {
156
+ req.destroy?.();
157
+ finish(null);
158
+ });
159
+ }
160
+ if (typeof req.end === "function") req.end();
161
+ });
162
+ }
163
+
164
+ async function checkForUpdate({
165
+ currentVersion,
166
+ env = process.env,
167
+ stdout = process.stdout,
168
+ stderr = process.stderr,
169
+ logger = console,
170
+ httpsModule = https,
171
+ registryUrl = DEFAULT_REGISTRY_URL,
172
+ timeoutMs = DEFAULT_TIMEOUT_MS,
173
+ maxBytes = MAX_RESPONSE_BYTES
174
+ } = {}) {
175
+ if (!shouldCheckForUpdate({ env, stdout, stderr })) {
176
+ return { checked: false, updateAvailable: false };
177
+ }
178
+
179
+ const current = normalizeVersion(currentVersion);
180
+ if (!current) {
181
+ return { checked: true, updateAvailable: false };
182
+ }
183
+
184
+ const latest = normalizeVersion(
185
+ await fetchLatestVersion({ httpsModule, registryUrl, timeoutMs, maxBytes })
186
+ );
187
+ if (!latest) {
188
+ return { checked: true, updateAvailable: false };
189
+ }
190
+
191
+ const comparison = compareSemver(parseSemver(current), parseSemver(latest));
192
+ if (comparison == null || comparison >= 0) {
193
+ return { checked: true, updateAvailable: false, latestVersion: latest };
194
+ }
195
+
196
+ logger?.log?.(`[docdex] Update available: v${current} -> v${latest}`);
197
+ logger?.log?.("[docdex] Run: npm i -g docdex@latest");
198
+ logger?.log?.(`[docdex] Disable update checks with ${UPDATE_CHECK_ENV}=0`);
199
+
200
+ return { checked: true, updateAvailable: true, latestVersion: latest };
201
+ }
202
+
203
+ async function checkForUpdateOnce(options) {
204
+ if (hasChecked) return { checked: false, updateAvailable: false };
205
+ hasChecked = true;
206
+ return checkForUpdate(options);
207
+ }
208
+
209
+ module.exports = {
210
+ DEFAULT_REGISTRY_URL,
211
+ DEFAULT_TIMEOUT_MS,
212
+ MAX_RESPONSE_BYTES,
213
+ checkForUpdate,
214
+ checkForUpdateOnce,
215
+ compareSemver,
216
+ parseSemver,
217
+ shouldCheckForUpdate
218
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docdex",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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": {