docdex 0.2.18 → 0.2.20
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 +10 -0
- package/bin/docdex.js +22 -2
- package/lib/postinstall_setup.js +204 -38
- package/lib/update_check.js +218 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.20
|
|
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
|
+
|
|
8
|
+
## 0.2.19
|
|
9
|
+
- Playwright issue fix
|
|
10
|
+
- Agents md adding command manually
|
|
11
|
+
- Agents md append repeat fix
|
|
12
|
+
|
|
3
13
|
## 0.2.16
|
|
4
14
|
- Repo memory now tags items with `repoId` and filters recalls to prevent cross-repo leakage in multi-repo daemons.
|
|
5
15
|
- MCP HTTP requires explicit repo selection when multiple repos are active.
|
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
|
+
});
|
package/lib/postinstall_setup.js
CHANGED
|
@@ -20,6 +20,8 @@ const DEFAULT_OLLAMA_CHAT_MODEL = "phi3.5:3.8b";
|
|
|
20
20
|
const DEFAULT_OLLAMA_CHAT_MODEL_SIZE_GIB = 2.2;
|
|
21
21
|
const SETUP_PENDING_MARKER = "setup_pending.json";
|
|
22
22
|
const AGENTS_DOC_FILENAME = "agents.md";
|
|
23
|
+
const DOCDEX_INFO_START_PREFIX = "---- START OF DOCDEX INFO V";
|
|
24
|
+
const DOCDEX_INFO_END = "---- END OF DOCDEX INFO -----";
|
|
23
25
|
|
|
24
26
|
function defaultConfigPath() {
|
|
25
27
|
return path.join(os.homedir(), ".docdex", "config.toml");
|
|
@@ -158,6 +160,18 @@ function agentsDocSourcePath() {
|
|
|
158
160
|
return path.join(__dirname, "..", "assets", AGENTS_DOC_FILENAME);
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
function resolvePackageVersion() {
|
|
164
|
+
const packagePath = path.join(__dirname, "..", "package.json");
|
|
165
|
+
if (!fs.existsSync(packagePath)) return "unknown";
|
|
166
|
+
try {
|
|
167
|
+
const raw = fs.readFileSync(packagePath, "utf8");
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : "unknown";
|
|
170
|
+
} catch {
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
function loadAgentInstructions() {
|
|
162
176
|
const sourcePath = agentsDocSourcePath();
|
|
163
177
|
if (!fs.existsSync(sourcePath)) return "";
|
|
@@ -172,13 +186,123 @@ function normalizeInstructionText(value) {
|
|
|
172
186
|
return String(value || "").trim();
|
|
173
187
|
}
|
|
174
188
|
|
|
175
|
-
function
|
|
189
|
+
function docdexBlockStart(version) {
|
|
190
|
+
return `${DOCDEX_INFO_START_PREFIX}${version} ----`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildDocdexInstructionBlock(instructions) {
|
|
194
|
+
const next = normalizeInstructionText(instructions);
|
|
195
|
+
if (!next) return "";
|
|
196
|
+
const version = resolvePackageVersion();
|
|
197
|
+
return `${docdexBlockStart(version)}\n${next}\n${DOCDEX_INFO_END}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractDocdexBlockBody(text) {
|
|
201
|
+
const match = String(text || "").match(
|
|
202
|
+
new RegExp(
|
|
203
|
+
`${escapeRegExp(DOCDEX_INFO_START_PREFIX)}[^\\r\\n]* ----\\r?\\n([\\s\\S]*?)\\r?\\n${escapeRegExp(
|
|
204
|
+
DOCDEX_INFO_END
|
|
205
|
+
)}`
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
return match ? normalizeInstructionText(match[1]) : "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function extractDocdexBlockVersion(text) {
|
|
212
|
+
const match = String(text || "").match(
|
|
213
|
+
new RegExp(`${escapeRegExp(DOCDEX_INFO_START_PREFIX)}([^\\s]+) ----`)
|
|
214
|
+
);
|
|
215
|
+
return match ? match[1] : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hasDocdexBlockVersion(text, version) {
|
|
219
|
+
if (!version) return false;
|
|
220
|
+
return String(text || "").includes(docdexBlockStart(version));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function stripDocdexBlocks(text) {
|
|
224
|
+
const re = new RegExp(
|
|
225
|
+
`${escapeRegExp(DOCDEX_INFO_START_PREFIX)}[^\\r\\n]* ----\\r?\\n[\\s\\S]*?\\r?\\n${escapeRegExp(
|
|
226
|
+
DOCDEX_INFO_END
|
|
227
|
+
)}\\r?\\n?`,
|
|
228
|
+
"g"
|
|
229
|
+
);
|
|
230
|
+
return String(text || "").replace(re, "").trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function stripDocdexBlocksExcept(text, version) {
|
|
234
|
+
if (!version) return stripDocdexBlocks(text);
|
|
235
|
+
const source = String(text || "");
|
|
236
|
+
const re = new RegExp(
|
|
237
|
+
`${escapeRegExp(DOCDEX_INFO_START_PREFIX)}[^\\r\\n]* ----\\r?\\n[\\s\\S]*?\\r?\\n${escapeRegExp(
|
|
238
|
+
DOCDEX_INFO_END
|
|
239
|
+
)}\\r?\\n?`,
|
|
240
|
+
"g"
|
|
241
|
+
);
|
|
242
|
+
let result = "";
|
|
243
|
+
let lastIndex = 0;
|
|
244
|
+
let match;
|
|
245
|
+
while ((match = re.exec(source))) {
|
|
246
|
+
const before = source.slice(lastIndex, match.index);
|
|
247
|
+
result += before;
|
|
248
|
+
const block = match[0];
|
|
249
|
+
const blockVersion = extractDocdexBlockVersion(block);
|
|
250
|
+
if (blockVersion === version) {
|
|
251
|
+
result += block;
|
|
252
|
+
}
|
|
253
|
+
lastIndex = match.index + block.length;
|
|
254
|
+
}
|
|
255
|
+
result += source.slice(lastIndex);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stripLegacyDocdexBodySegment(segment, body) {
|
|
260
|
+
if (!body) return String(segment || "");
|
|
261
|
+
const normalizedSegment = String(segment || "").replace(/\r\n/g, "\n");
|
|
262
|
+
const normalizedBody = String(body || "").replace(/\r\n/g, "\n");
|
|
263
|
+
if (!normalizedBody.trim()) return normalizedSegment;
|
|
264
|
+
const re = new RegExp(`\\n?${escapeRegExp(normalizedBody)}\\n?`, "g");
|
|
265
|
+
return normalizedSegment.replace(re, "\n").replace(/\n{3,}/g, "\n\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function stripLegacyDocdexBody(text, body) {
|
|
269
|
+
if (!body) return String(text || "");
|
|
270
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
271
|
+
const re = new RegExp(
|
|
272
|
+
`${escapeRegExp(DOCDEX_INFO_START_PREFIX)}[^\\n]* ----\\n[\\s\\S]*?\\n${escapeRegExp(DOCDEX_INFO_END)}\\n?`,
|
|
273
|
+
"g"
|
|
274
|
+
);
|
|
275
|
+
let result = "";
|
|
276
|
+
let lastIndex = 0;
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = re.exec(source))) {
|
|
279
|
+
const before = source.slice(lastIndex, match.index);
|
|
280
|
+
result += stripLegacyDocdexBodySegment(before, body);
|
|
281
|
+
result += match[0];
|
|
282
|
+
lastIndex = match.index + match[0].length;
|
|
283
|
+
}
|
|
284
|
+
result += stripLegacyDocdexBodySegment(source.slice(lastIndex), body);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function mergeInstructionText(existing, instructions, { prepend = false } = {}) {
|
|
176
289
|
const next = normalizeInstructionText(instructions);
|
|
177
290
|
if (!next) return normalizeInstructionText(existing);
|
|
178
|
-
const
|
|
291
|
+
const existingText = String(existing || "");
|
|
292
|
+
const current = normalizeInstructionText(existingText);
|
|
179
293
|
if (!current) return next;
|
|
180
|
-
|
|
181
|
-
|
|
294
|
+
const version = extractDocdexBlockVersion(next);
|
|
295
|
+
if (version) {
|
|
296
|
+
const body = extractDocdexBlockBody(next);
|
|
297
|
+
const cleaned = stripLegacyDocdexBody(existingText, body);
|
|
298
|
+
const withoutOldBlocks = stripDocdexBlocksExcept(cleaned, version);
|
|
299
|
+
if (hasDocdexBlockVersion(withoutOldBlocks, version)) return withoutOldBlocks;
|
|
300
|
+
const remainder = normalizeInstructionText(stripDocdexBlocks(withoutOldBlocks));
|
|
301
|
+
if (!remainder) return next;
|
|
302
|
+
return prepend ? `${next}\n\n${remainder}` : `${remainder}\n\n${next}`;
|
|
303
|
+
}
|
|
304
|
+
if (existingText.includes(next)) return existingText;
|
|
305
|
+
return prepend ? `${next}\n\n${current}` : `${current}\n\n${next}`;
|
|
182
306
|
}
|
|
183
307
|
|
|
184
308
|
function writeTextFile(pathname, contents) {
|
|
@@ -199,13 +323,10 @@ function upsertPromptFile(pathname, instructions, { prepend = false } = {}) {
|
|
|
199
323
|
let current = "";
|
|
200
324
|
if (fs.existsSync(pathname)) {
|
|
201
325
|
current = fs.readFileSync(pathname, "utf8");
|
|
202
|
-
if (current.includes(next)) return false;
|
|
203
|
-
}
|
|
204
|
-
const currentTrimmed = normalizeInstructionText(current);
|
|
205
|
-
let merged = next;
|
|
206
|
-
if (currentTrimmed) {
|
|
207
|
-
merged = prepend ? `${next}\n\n${currentTrimmed}` : `${currentTrimmed}\n\n${next}`;
|
|
208
326
|
}
|
|
327
|
+
const merged = mergeInstructionText(current, instructions, { prepend });
|
|
328
|
+
if (!merged) return false;
|
|
329
|
+
if (merged === current) return false;
|
|
209
330
|
return writeTextFile(pathname, merged);
|
|
210
331
|
}
|
|
211
332
|
|
|
@@ -220,13 +341,51 @@ function upsertYamlInstruction(pathname, key, instructions) {
|
|
|
220
341
|
if (fs.existsSync(pathname)) {
|
|
221
342
|
current = fs.readFileSync(pathname, "utf8");
|
|
222
343
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
344
|
+
const lines = current.split(/\r?\n/);
|
|
345
|
+
const blockRe = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*:\\s*(\\|[+-]?)?\\s*$`);
|
|
346
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
347
|
+
const match = lines[idx].match(blockRe);
|
|
348
|
+
if (!match) continue;
|
|
349
|
+
const indent = match[1] || "";
|
|
350
|
+
const blockIndent = `${indent} `;
|
|
351
|
+
let existingBlock = "";
|
|
352
|
+
let blockEnd = idx + 1;
|
|
353
|
+
if (match[2]) {
|
|
354
|
+
for (let j = idx + 1; j < lines.length; j += 1) {
|
|
355
|
+
const line = lines[j];
|
|
356
|
+
if (!line.trim()) {
|
|
357
|
+
blockEnd = j + 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const leading = line.match(/^\s*/)[0].length;
|
|
361
|
+
if (leading <= indent.length) break;
|
|
362
|
+
blockEnd = j + 1;
|
|
363
|
+
}
|
|
364
|
+
const blockLines = lines.slice(idx + 1, blockEnd);
|
|
365
|
+
existingBlock = blockLines
|
|
366
|
+
.map((line) => {
|
|
367
|
+
if (!line.trim()) return "";
|
|
368
|
+
return line.startsWith(blockIndent) ? line.slice(blockIndent.length) : line.trimStart();
|
|
369
|
+
})
|
|
370
|
+
.join("\n");
|
|
371
|
+
} else {
|
|
372
|
+
const inlineMatch = lines[idx].match(new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*:\\s*(.*)$`));
|
|
373
|
+
existingBlock = inlineMatch ? inlineMatch[2].trim() : "";
|
|
374
|
+
}
|
|
375
|
+
const merged = mergeInstructionText(existingBlock, instructions);
|
|
376
|
+
if (!merged) return false;
|
|
377
|
+
if (normalizeInstructionText(merged) === normalizeInstructionText(existingBlock) && match[2]) return false;
|
|
378
|
+
const mergedLines = merged.split(/\r?\n/).map((line) => `${blockIndent}${line}`);
|
|
379
|
+
const updatedLines = [
|
|
380
|
+
...lines.slice(0, idx),
|
|
381
|
+
`${indent}${key}: |`,
|
|
382
|
+
...mergedLines,
|
|
383
|
+
...lines.slice(match[2] ? blockEnd : idx + 1)
|
|
384
|
+
];
|
|
385
|
+
return writeTextFile(pathname, updatedLines.join("\n").trimEnd());
|
|
227
386
|
}
|
|
228
|
-
const
|
|
229
|
-
const block = `${key}: |\n${
|
|
387
|
+
const contentLines = next.split(/\r?\n/).map((line) => ` ${line}`);
|
|
388
|
+
const block = `${key}: |\n${contentLines.join("\n")}`;
|
|
230
389
|
const merged = current.trim() ? `${current.trim()}\n\n${block}` : block;
|
|
231
390
|
return writeTextFile(pathname, merged);
|
|
232
391
|
}
|
|
@@ -668,7 +827,7 @@ function resolveBinaryPath({ binaryPath } = {}) {
|
|
|
668
827
|
}
|
|
669
828
|
|
|
670
829
|
function applyAgentInstructions({ logger } = {}) {
|
|
671
|
-
const instructions = loadAgentInstructions();
|
|
830
|
+
const instructions = buildDocdexInstructionBlock(loadAgentInstructions());
|
|
672
831
|
if (!normalizeInstructionText(instructions)) return { ok: false, reason: "missing_instructions" };
|
|
673
832
|
const paths = clientInstructionPaths();
|
|
674
833
|
let updated = false;
|
|
@@ -1305,8 +1464,26 @@ async function maybePromptOllamaModel({
|
|
|
1305
1464
|
return { status: "skipped", reason: "invalid_selection" };
|
|
1306
1465
|
}
|
|
1307
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
|
+
|
|
1308
1484
|
function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger }) {
|
|
1309
1485
|
if (!binaryPath) return { ok: false, reason: "missing_binary" };
|
|
1486
|
+
const envPairs = buildDaemonEnvPairs({ mcpBinaryPath });
|
|
1310
1487
|
const args = [
|
|
1311
1488
|
"daemon",
|
|
1312
1489
|
"--repo",
|
|
@@ -1319,21 +1496,16 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
|
|
|
1319
1496
|
"warn",
|
|
1320
1497
|
"--secure-mode=false"
|
|
1321
1498
|
];
|
|
1322
|
-
const envMcpBin = mcpBinaryPath ? `DOCDEX_MCP_SERVER_BIN=${mcpBinaryPath}` : null;
|
|
1323
1499
|
|
|
1324
1500
|
if (process.platform === "darwin") {
|
|
1325
1501
|
const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.docdex.daemon.plist");
|
|
1326
1502
|
const logDir = path.join(os.homedir(), ".docdex", "logs");
|
|
1327
1503
|
fs.mkdirSync(logDir, { recursive: true });
|
|
1328
1504
|
const programArgs = [binaryPath, ...args];
|
|
1329
|
-
const envVars = [
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
];
|
|
1333
|
-
if (mcpBinaryPath) {
|
|
1334
|
-
envVars.push(" <key>DOCDEX_MCP_SERVER_BIN</key>\n");
|
|
1335
|
-
envVars.push(` <string>${mcpBinaryPath}</string>\n`);
|
|
1336
|
-
}
|
|
1505
|
+
const envVars = envPairs.flatMap(([key, value]) => [
|
|
1506
|
+
` <key>${key}</key>\n`,
|
|
1507
|
+
` <string>${value}</string>\n`
|
|
1508
|
+
]);
|
|
1337
1509
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
1338
1510
|
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n` +
|
|
1339
1511
|
`<plist version="1.0">\n` +
|
|
@@ -1375,6 +1547,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
|
|
|
1375
1547
|
const systemdDir = path.join(os.homedir(), ".config", "systemd", "user");
|
|
1376
1548
|
const unitPath = path.join(systemdDir, "docdexd.service");
|
|
1377
1549
|
fs.mkdirSync(systemdDir, { recursive: true });
|
|
1550
|
+
const envLines = envPairs.map(([key, value]) => `Environment=${key}=${value}`);
|
|
1378
1551
|
const unit = [
|
|
1379
1552
|
"[Unit]",
|
|
1380
1553
|
"Description=Docdex daemon",
|
|
@@ -1382,8 +1555,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
|
|
|
1382
1555
|
"",
|
|
1383
1556
|
"[Service]",
|
|
1384
1557
|
`ExecStart=${binaryPath} ${args.join(" ")}`,
|
|
1385
|
-
|
|
1386
|
-
envMcpBin ? `Environment=${envMcpBin}` : null,
|
|
1558
|
+
...envLines,
|
|
1387
1559
|
"Restart=always",
|
|
1388
1560
|
"RestartSec=2",
|
|
1389
1561
|
"",
|
|
@@ -1402,10 +1574,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
|
|
|
1402
1574
|
if (process.platform === "win32") {
|
|
1403
1575
|
const taskName = "Docdex Daemon";
|
|
1404
1576
|
const joinedArgs = args.map((arg) => `"${arg}"`).join(" ");
|
|
1405
|
-
const envParts = [
|
|
1406
|
-
if (mcpBinaryPath) {
|
|
1407
|
-
envParts.push(`set "DOCDEX_MCP_SERVER_BIN=${mcpBinaryPath}"`);
|
|
1408
|
-
}
|
|
1577
|
+
const envParts = envPairs.map(([key, value]) => `set "${key}=${value}"`);
|
|
1409
1578
|
const taskArgs =
|
|
1410
1579
|
`"cmd.exe" /c "${envParts.join(" && ")} && \"${binaryPath}\" ${joinedArgs}"`;
|
|
1411
1580
|
const create = spawnSync("schtasks", [
|
|
@@ -1433,10 +1602,7 @@ function registerStartup({ binaryPath, mcpBinaryPath, port, repoRoot, logger })
|
|
|
1433
1602
|
|
|
1434
1603
|
function startDaemonNow({ binaryPath, mcpBinaryPath, port, repoRoot }) {
|
|
1435
1604
|
if (!binaryPath) return false;
|
|
1436
|
-
const extraEnv = {};
|
|
1437
|
-
if (mcpBinaryPath) {
|
|
1438
|
-
extraEnv.DOCDEX_MCP_SERVER_BIN = mcpBinaryPath;
|
|
1439
|
-
}
|
|
1605
|
+
const extraEnv = buildDaemonEnv({ mcpBinaryPath });
|
|
1440
1606
|
const child = spawn(
|
|
1441
1607
|
binaryPath,
|
|
1442
1608
|
[
|
|
@@ -1456,7 +1622,6 @@ function startDaemonNow({ binaryPath, mcpBinaryPath, port, repoRoot }) {
|
|
|
1456
1622
|
detached: true,
|
|
1457
1623
|
env: {
|
|
1458
1624
|
...process.env,
|
|
1459
|
-
DOCDEX_BROWSER_AUTO_INSTALL: "0",
|
|
1460
1625
|
...extraEnv
|
|
1461
1626
|
}
|
|
1462
1627
|
}
|
|
@@ -1688,5 +1853,6 @@ module.exports = {
|
|
|
1688
1853
|
canPromptWithTty,
|
|
1689
1854
|
shouldSkipSetup,
|
|
1690
1855
|
launchSetupWizard,
|
|
1691
|
-
applyAgentInstructions
|
|
1856
|
+
applyAgentInstructions,
|
|
1857
|
+
buildDaemonEnv
|
|
1692
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
|
+
};
|