docdex 0.2.3 → 0.2.5
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 +1 -1
- package/README.md +1 -0
- package/lib/install.js +181 -10
- package/lib/postinstall_setup.js +210 -18
- package/lib/uninstall.js +420 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Docdex is a local-first docs + code indexer/search daemon. It runs per repo, kee
|
|
|
12
12
|
- If you publish from a fork, set `DOCDEX_DOWNLOAD_REPO=<owner/repo>` before installing so the downloader fetches your release assets.
|
|
13
13
|
- If you mirror release assets locally, set `DOCDEX_DOWNLOAD_BASE=http://host/path` to point the installer at the mirror.
|
|
14
14
|
- Distribution: binaries stay in GitHub Releases (small npm package); postinstall fetches `docdexd-<platform>.tar.gz` matching the npm version.
|
|
15
|
+
- Local dev install: if the release manifest/checksums are missing and a local `target/release/docdexd` (or `target/debug/docdexd`) exists, the installer falls back to that binary. Set `DOCDEX_LOCAL_FALLBACK=0` to disable or `DOCDEX_LOCAL_BINARY=/path/to/docdexd` to override.
|
|
15
16
|
- Platform diagnostics (no download): `docdex doctor` (or `docdex diagnostics`) prints detected OS/arch(/libc), whether supported, and the expected Rust target triple + release asset naming pattern.
|
|
16
17
|
- Publishing uses npm Trusted Publishing (OIDC) — no NPM token needed; see `.github/workflows/release.yml`.
|
|
17
18
|
- Postinstall prompts: if Ollama is missing, the installer asks to install Ollama and `nomic-embed-text`. If Ollama is available, it prompts to pick a default chat model and can install `phi3.5:3.8b` (~2.2 GB) while showing free disk space. Skip with `DOCDEX_OLLAMA_INSTALL=0` or `DOCDEX_OLLAMA_MODEL_PROMPT=0`; force with `DOCDEX_OLLAMA_INSTALL=1` or `DOCDEX_OLLAMA_MODEL=<model>`; preselect with `DOCDEX_OLLAMA_DEFAULT_MODEL`.
|
package/lib/install.js
CHANGED
|
@@ -37,6 +37,8 @@ const DEFAULT_INTEGRITY_CONFIG = Object.freeze({
|
|
|
37
37
|
metadataSources: ["manifest", "checksums", "sidecar"],
|
|
38
38
|
missingPolicy: "fallback"
|
|
39
39
|
});
|
|
40
|
+
const LOCAL_FALLBACK_ENV = "DOCDEX_LOCAL_FALLBACK";
|
|
41
|
+
const LOCAL_BINARY_ENV = "DOCDEX_LOCAL_BINARY";
|
|
40
42
|
|
|
41
43
|
const EXIT_CODE_BY_ERROR_CODE = Object.freeze({
|
|
42
44
|
DOCDEX_INSTALLER_CONFIG: 2,
|
|
@@ -361,6 +363,141 @@ function normalizeSha256Hex(value) {
|
|
|
361
363
|
return trimmed;
|
|
362
364
|
}
|
|
363
365
|
|
|
366
|
+
function parseEnvBool(value) {
|
|
367
|
+
if (value == null) return null;
|
|
368
|
+
const normalized = String(value).trim().toLowerCase();
|
|
369
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
370
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function detectLocalRepoRoot({ pathModule, fsModule } = {}) {
|
|
375
|
+
const pathImpl = pathModule || path;
|
|
376
|
+
const fsImpl = fsModule || fs;
|
|
377
|
+
const candidate = pathImpl.resolve(__dirname, "..", "..");
|
|
378
|
+
const hasCargo = fsImpl.existsSync(pathImpl.join(candidate, "Cargo.toml"));
|
|
379
|
+
const hasGit = fsImpl.existsSync(pathImpl.join(candidate, ".git"));
|
|
380
|
+
if (hasCargo || hasGit) {
|
|
381
|
+
return candidate;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function resolveLocalBinaryCandidate({
|
|
387
|
+
env = process.env,
|
|
388
|
+
platform = process.platform,
|
|
389
|
+
pathModule = path,
|
|
390
|
+
fsModule = fs,
|
|
391
|
+
repoRoot
|
|
392
|
+
} = {}) {
|
|
393
|
+
const explicit = env[LOCAL_BINARY_ENV];
|
|
394
|
+
if (explicit) {
|
|
395
|
+
const resolved = pathModule.resolve(explicit);
|
|
396
|
+
if (fsModule.existsSync(resolved)) return resolved;
|
|
397
|
+
}
|
|
398
|
+
const root = repoRoot || detectLocalRepoRoot({ pathModule, fsModule });
|
|
399
|
+
if (!root) return null;
|
|
400
|
+
const binaryName = platform === "win32" ? "docdexd.exe" : "docdexd";
|
|
401
|
+
const releasePath = pathModule.join(root, "target", "release", binaryName);
|
|
402
|
+
if (fsModule.existsSync(releasePath)) return releasePath;
|
|
403
|
+
const debugPath = pathModule.join(root, "target", "debug", binaryName);
|
|
404
|
+
if (fsModule.existsSync(debugPath)) return debugPath;
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function installFromLocalBinary({
|
|
409
|
+
fsModule,
|
|
410
|
+
pathModule,
|
|
411
|
+
distDir,
|
|
412
|
+
binaryPath,
|
|
413
|
+
isWin32,
|
|
414
|
+
version,
|
|
415
|
+
platformKey,
|
|
416
|
+
targetTriple,
|
|
417
|
+
repoSlug,
|
|
418
|
+
sha256FileFn,
|
|
419
|
+
writeJsonFileAtomicFn,
|
|
420
|
+
logger
|
|
421
|
+
}) {
|
|
422
|
+
await fsModule.promises.rm(distDir, { recursive: true, force: true });
|
|
423
|
+
await fsModule.promises.mkdir(distDir, { recursive: true });
|
|
424
|
+
const filename = isWin32 ? "docdexd.exe" : "docdexd";
|
|
425
|
+
const destPath = pathModule.join(distDir, filename);
|
|
426
|
+
await fsModule.promises.copyFile(binaryPath, destPath);
|
|
427
|
+
if (!isWin32) {
|
|
428
|
+
await fsModule.promises.chmod(destPath, 0o755).catch(() => {});
|
|
429
|
+
}
|
|
430
|
+
const binarySha256 = await sha256FileFn(destPath);
|
|
431
|
+
const metadata = {
|
|
432
|
+
schemaVersion: INSTALL_METADATA_SCHEMA_VERSION,
|
|
433
|
+
installedAt: nowIso(),
|
|
434
|
+
version,
|
|
435
|
+
repoSlug: repoSlug || "local",
|
|
436
|
+
platformKey,
|
|
437
|
+
targetTriple,
|
|
438
|
+
binary: {
|
|
439
|
+
filename,
|
|
440
|
+
sha256: binarySha256
|
|
441
|
+
},
|
|
442
|
+
archive: {
|
|
443
|
+
name: null,
|
|
444
|
+
sha256: null,
|
|
445
|
+
source: "local",
|
|
446
|
+
downloadUrl: null
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
await writeJsonFileAtomicFn({
|
|
450
|
+
fsModule,
|
|
451
|
+
pathModule,
|
|
452
|
+
filePath: installMetadataPath(distDir, pathModule),
|
|
453
|
+
value: metadata
|
|
454
|
+
});
|
|
455
|
+
logger?.warn?.(`[docdex] Installed local binary from ${binaryPath}`);
|
|
456
|
+
return { binaryPath: destPath, outcome: "local", outcomeCode: "local" };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function maybeInstallLocalFallback({
|
|
460
|
+
err,
|
|
461
|
+
env,
|
|
462
|
+
fsModule,
|
|
463
|
+
pathModule,
|
|
464
|
+
distDir,
|
|
465
|
+
isWin32,
|
|
466
|
+
version,
|
|
467
|
+
platformKey,
|
|
468
|
+
targetTriple,
|
|
469
|
+
repoSlug,
|
|
470
|
+
sha256FileFn,
|
|
471
|
+
writeJsonFileAtomicFn,
|
|
472
|
+
logger,
|
|
473
|
+
localRepoRoot,
|
|
474
|
+
localBinaryPath
|
|
475
|
+
}) {
|
|
476
|
+
if (!err || err.code !== "DOCDEX_CHECKSUM_UNUSABLE") return null;
|
|
477
|
+
const allowFallback = parseEnvBool(env[LOCAL_FALLBACK_ENV]);
|
|
478
|
+
if (allowFallback === false) return null;
|
|
479
|
+
|
|
480
|
+
const candidate =
|
|
481
|
+
localBinaryPath ||
|
|
482
|
+
resolveLocalBinaryCandidate({ env, platform: process.platform, pathModule, fsModule, repoRoot: localRepoRoot });
|
|
483
|
+
if (!candidate) return null;
|
|
484
|
+
|
|
485
|
+
return installFromLocalBinary({
|
|
486
|
+
fsModule,
|
|
487
|
+
pathModule,
|
|
488
|
+
distDir,
|
|
489
|
+
binaryPath: candidate,
|
|
490
|
+
isWin32,
|
|
491
|
+
version,
|
|
492
|
+
platformKey,
|
|
493
|
+
targetTriple,
|
|
494
|
+
repoSlug,
|
|
495
|
+
sha256FileFn,
|
|
496
|
+
writeJsonFileAtomicFn,
|
|
497
|
+
logger
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
364
501
|
function emitInstallerEvent(logger, payload) {
|
|
365
502
|
if (!logger || typeof logger.log !== "function") return;
|
|
366
503
|
try {
|
|
@@ -1348,6 +1485,8 @@ async function runInstaller(options) {
|
|
|
1348
1485
|
const sha256FileFn = opts.sha256FileFn || sha256File;
|
|
1349
1486
|
const writeJsonFileAtomicFn = opts.writeJsonFileAtomicFn || writeJsonFileAtomic;
|
|
1350
1487
|
const restartFn = opts.restartFn;
|
|
1488
|
+
const localRepoRoot = opts.localRepoRoot;
|
|
1489
|
+
const localBinaryPath = opts.localBinaryPath;
|
|
1351
1490
|
|
|
1352
1491
|
const detectedPlatform = opts.platform || process.platform;
|
|
1353
1492
|
const detectedArch = opts.arch || process.arch;
|
|
@@ -1431,16 +1570,48 @@ async function runInstaller(options) {
|
|
|
1431
1570
|
};
|
|
1432
1571
|
}
|
|
1433
1572
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1573
|
+
let repoSlug = null;
|
|
1574
|
+
let archive;
|
|
1575
|
+
let expectedSha256;
|
|
1576
|
+
let source;
|
|
1577
|
+
let manifestAttempt;
|
|
1578
|
+
try {
|
|
1579
|
+
repoSlug = parseRepoSlugFn();
|
|
1580
|
+
const resolved = await resolveInstallerDownloadPlanFn({
|
|
1581
|
+
repoSlug,
|
|
1582
|
+
version,
|
|
1583
|
+
platformKey,
|
|
1584
|
+
targetTriple,
|
|
1585
|
+
logger,
|
|
1586
|
+
integrityConfigFn: opts.integrityConfigFn
|
|
1587
|
+
});
|
|
1588
|
+
archive = resolved.archive;
|
|
1589
|
+
expectedSha256 = resolved.expectedSha256;
|
|
1590
|
+
source = resolved.source;
|
|
1591
|
+
manifestAttempt = resolved.manifestAttempt;
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
const fallback = await maybeInstallLocalFallback({
|
|
1594
|
+
err,
|
|
1595
|
+
env: process.env,
|
|
1596
|
+
fsModule,
|
|
1597
|
+
pathModule,
|
|
1598
|
+
distDir,
|
|
1599
|
+
isWin32,
|
|
1600
|
+
version,
|
|
1601
|
+
platformKey,
|
|
1602
|
+
targetTriple,
|
|
1603
|
+
repoSlug,
|
|
1604
|
+
sha256FileFn,
|
|
1605
|
+
writeJsonFileAtomicFn,
|
|
1606
|
+
logger,
|
|
1607
|
+
localRepoRoot,
|
|
1608
|
+
localBinaryPath
|
|
1609
|
+
});
|
|
1610
|
+
if (fallback) {
|
|
1611
|
+
return fallback;
|
|
1612
|
+
}
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1444
1615
|
|
|
1445
1616
|
const downloadUrl = `${getDownloadBaseFn(repoSlug)}/v${version}/${archive}`;
|
|
1446
1617
|
const nonce = buildInstallNonce();
|
package/lib/postinstall_setup.js
CHANGED
|
@@ -158,22 +158,166 @@ function upsertMcpServerJson(pathname, url) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function upsertCodexConfig(pathname, url) {
|
|
161
|
+
const hasSection = (contents, section) =>
|
|
162
|
+
new RegExp(`^\\s*\\[${section}\\]\\s*$`, "m").test(contents);
|
|
163
|
+
const hasNestedMcpServers = (contents) =>
|
|
164
|
+
/^\s*\[mcp_servers\.[^\]]+\]\s*$/m.test(contents);
|
|
165
|
+
const parseTomlString = (value) => {
|
|
166
|
+
const trimmed = value.trim();
|
|
167
|
+
const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
|
|
168
|
+
return quoted ? quoted[1] : trimmed;
|
|
169
|
+
};
|
|
170
|
+
const migrateLegacyMcpServers = (contents) => {
|
|
171
|
+
if (!/\[\[mcp_servers\]\]/m.test(contents)) {
|
|
172
|
+
return { contents, migrated: false };
|
|
173
|
+
}
|
|
174
|
+
const lines = contents.split(/\r?\n/);
|
|
175
|
+
const output = [];
|
|
176
|
+
const entries = [];
|
|
177
|
+
let inBlock = false;
|
|
178
|
+
let current = null;
|
|
179
|
+
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
if (/^\s*\[\[mcp_servers\]\]\s*$/.test(line)) {
|
|
182
|
+
if (current) entries.push(current);
|
|
183
|
+
current = {};
|
|
184
|
+
inBlock = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (inBlock) {
|
|
188
|
+
if (/^\s*\[.+\]\s*$/.test(line)) {
|
|
189
|
+
if (current) entries.push(current);
|
|
190
|
+
current = null;
|
|
191
|
+
inBlock = false;
|
|
192
|
+
output.push(line);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$/);
|
|
196
|
+
if (match) {
|
|
197
|
+
current[match[1]] = match[2].trim();
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
output.push(line);
|
|
202
|
+
}
|
|
203
|
+
if (current) entries.push(current);
|
|
204
|
+
|
|
205
|
+
const mapLines = [];
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (!entry.name) continue;
|
|
208
|
+
const name = parseTomlString(entry.name);
|
|
209
|
+
if (!name) continue;
|
|
210
|
+
mapLines.push(`[mcp_servers.${name}]`);
|
|
211
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
212
|
+
if (key === "name") continue;
|
|
213
|
+
mapLines.push(`${key} = ${value}`);
|
|
214
|
+
}
|
|
215
|
+
mapLines.push("");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (mapLines.length === 0) {
|
|
219
|
+
return { contents: output.join("\n"), migrated: true };
|
|
220
|
+
}
|
|
221
|
+
if (output.length && output[output.length - 1].trim()) output.push("");
|
|
222
|
+
while (mapLines.length && !mapLines[mapLines.length - 1].trim()) mapLines.pop();
|
|
223
|
+
output.push(...mapLines);
|
|
224
|
+
return { contents: output.join("\n"), migrated: true };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const upsertDocdexNested = (contents, urlValue) => {
|
|
228
|
+
const lines = contents.split(/\r?\n/);
|
|
229
|
+
const headerRe = /^\s*\[mcp_servers\.docdex\]\s*$/;
|
|
230
|
+
let start = lines.findIndex((line) => headerRe.test(line));
|
|
231
|
+
if (start === -1) {
|
|
232
|
+
if (lines.length && lines[lines.length - 1].trim()) lines.push("");
|
|
233
|
+
lines.push("[mcp_servers.docdex]");
|
|
234
|
+
lines.push(`url = "${urlValue}"`);
|
|
235
|
+
return { contents: lines.join("\n"), updated: true };
|
|
236
|
+
}
|
|
237
|
+
let end = start + 1;
|
|
238
|
+
while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
|
|
239
|
+
end += 1;
|
|
240
|
+
}
|
|
241
|
+
let updated = false;
|
|
242
|
+
let urlIndex = -1;
|
|
243
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
244
|
+
if (/^\s*url\s*=/.test(lines[i])) {
|
|
245
|
+
urlIndex = i;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (urlIndex === -1) {
|
|
250
|
+
lines.splice(start + 1, 0, `url = "${urlValue}"`);
|
|
251
|
+
updated = true;
|
|
252
|
+
} else if (!lines[urlIndex].includes(`"${urlValue}"`)) {
|
|
253
|
+
lines[urlIndex] = `url = "${urlValue}"`;
|
|
254
|
+
updated = true;
|
|
255
|
+
}
|
|
256
|
+
return { contents: lines.join("\n"), updated };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const upsertDocdexRoot = (contents, urlValue) => {
|
|
260
|
+
const lines = contents.split(/\r?\n/);
|
|
261
|
+
const headerRe = /^\s*\[mcp_servers\]\s*$/;
|
|
262
|
+
const start = lines.findIndex((line) => headerRe.test(line));
|
|
263
|
+
if (start === -1) {
|
|
264
|
+
if (lines.length && lines[lines.length - 1].trim()) lines.push("");
|
|
265
|
+
lines.push("[mcp_servers]");
|
|
266
|
+
lines.push(`docdex = { url = "${urlValue}" }`);
|
|
267
|
+
return { contents: lines.join("\n"), updated: true };
|
|
268
|
+
}
|
|
269
|
+
let end = start + 1;
|
|
270
|
+
while (end < lines.length && !/^\s*\[.+\]\s*$/.test(lines[end])) {
|
|
271
|
+
end += 1;
|
|
272
|
+
}
|
|
273
|
+
let updated = false;
|
|
274
|
+
let docdexLine = -1;
|
|
275
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
276
|
+
if (/^\s*docdex\s*=/.test(lines[i])) {
|
|
277
|
+
docdexLine = i;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (docdexLine === -1) {
|
|
282
|
+
lines.splice(end, 0, `docdex = { url = "${urlValue}" }`);
|
|
283
|
+
updated = true;
|
|
284
|
+
} else if (!lines[docdexLine].includes(`"${urlValue}"`)) {
|
|
285
|
+
lines[docdexLine] = `docdex = { url = "${urlValue}" }`;
|
|
286
|
+
updated = true;
|
|
287
|
+
}
|
|
288
|
+
return { contents: lines.join("\n"), updated };
|
|
289
|
+
};
|
|
290
|
+
|
|
161
291
|
let contents = "";
|
|
162
292
|
if (fs.existsSync(pathname)) {
|
|
163
293
|
contents = fs.readFileSync(pathname, "utf8");
|
|
164
294
|
}
|
|
165
|
-
|
|
295
|
+
let updated = false;
|
|
296
|
+
if (/\[\[mcp_servers\]\]/m.test(contents)) {
|
|
297
|
+
const migrated = migrateLegacyMcpServers(contents);
|
|
298
|
+
contents = migrated.contents;
|
|
299
|
+
updated = updated || migrated.migrated;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (hasNestedMcpServers(contents)) {
|
|
303
|
+
const nested = upsertDocdexNested(contents, url);
|
|
304
|
+
contents = nested.contents;
|
|
305
|
+
updated = updated || nested.updated;
|
|
306
|
+
} else if (hasSection(contents, "mcp_servers")) {
|
|
307
|
+
const root = upsertDocdexRoot(contents, url);
|
|
308
|
+
contents = root.contents;
|
|
309
|
+
updated = updated || root.updated;
|
|
310
|
+
} else {
|
|
311
|
+
const root = upsertDocdexRoot(contents, url);
|
|
312
|
+
contents = root.contents;
|
|
313
|
+
updated = updated || root.updated;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!updated) {
|
|
166
317
|
return false;
|
|
167
318
|
}
|
|
168
|
-
const block = [
|
|
169
|
-
"",
|
|
170
|
-
"[[mcp_servers]]",
|
|
171
|
-
'name = "docdex"',
|
|
172
|
-
`url = "${url}"`,
|
|
173
|
-
"",
|
|
174
|
-
].join("\n");
|
|
175
319
|
fs.mkdirSync(path.dirname(pathname), { recursive: true });
|
|
176
|
-
fs.writeFileSync(pathname, contents
|
|
320
|
+
fs.writeFileSync(pathname, contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
177
321
|
return true;
|
|
178
322
|
}
|
|
179
323
|
|
|
@@ -233,24 +377,52 @@ function parseEnvBool(value) {
|
|
|
233
377
|
return null;
|
|
234
378
|
}
|
|
235
379
|
|
|
236
|
-
function
|
|
380
|
+
function hasInteractiveTty(stdin, stdout) {
|
|
381
|
+
return Boolean((stdin && stdin.isTTY) || (stdout && stdout.isTTY));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function canPromptWithTty(stdin, stdout) {
|
|
385
|
+
if (hasInteractiveTty(stdin, stdout)) return true;
|
|
386
|
+
const ttyPath = process.platform === "win32" ? "CONIN$" : "/dev/tty";
|
|
387
|
+
try {
|
|
388
|
+
const fd = fs.openSync(ttyPath, "r");
|
|
389
|
+
fs.closeSync(fd);
|
|
390
|
+
return true;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolveOllamaInstallMode({
|
|
397
|
+
env = process.env,
|
|
398
|
+
stdin = process.stdin,
|
|
399
|
+
stdout = process.stdout,
|
|
400
|
+
canPrompt = canPromptWithTty
|
|
401
|
+
} = {}) {
|
|
237
402
|
const override = parseEnvBool(env.DOCDEX_OLLAMA_INSTALL);
|
|
238
403
|
if (override === true) return { mode: "install", reason: "env", interactive: false };
|
|
239
404
|
if (override === false) return { mode: "skip", reason: "env", interactive: false };
|
|
240
|
-
|
|
241
|
-
|
|
405
|
+
if (!canPrompt(stdin, stdout)) {
|
|
406
|
+
return { mode: "skip", reason: "non_interactive", interactive: false };
|
|
407
|
+
}
|
|
242
408
|
if (env.CI) return { mode: "skip", reason: "ci", interactive: false };
|
|
243
409
|
return { mode: "prompt", reason: "interactive", interactive: true };
|
|
244
410
|
}
|
|
245
411
|
|
|
246
|
-
function resolveOllamaModelPromptMode({
|
|
412
|
+
function resolveOllamaModelPromptMode({
|
|
413
|
+
env = process.env,
|
|
414
|
+
stdin = process.stdin,
|
|
415
|
+
stdout = process.stdout,
|
|
416
|
+
canPrompt = canPromptWithTty
|
|
417
|
+
} = {}) {
|
|
247
418
|
const override = parseEnvBool(env.DOCDEX_OLLAMA_MODEL_PROMPT);
|
|
248
419
|
if (override === true) return { mode: "prompt", reason: "env", interactive: true };
|
|
249
420
|
if (override === false) return { mode: "skip", reason: "env", interactive: false };
|
|
250
421
|
const assumeYes = parseEnvBool(env.DOCDEX_OLLAMA_MODEL_ASSUME_Y);
|
|
251
422
|
if (assumeYes === true) return { mode: "auto", reason: "env", interactive: false };
|
|
252
|
-
|
|
253
|
-
|
|
423
|
+
if (!canPrompt(stdin, stdout)) {
|
|
424
|
+
return { mode: "skip", reason: "non_interactive", interactive: false };
|
|
425
|
+
}
|
|
254
426
|
if (env.CI) return { mode: "skip", reason: "ci", interactive: false };
|
|
255
427
|
return { mode: "prompt", reason: "interactive", interactive: true };
|
|
256
428
|
}
|
|
@@ -408,11 +580,27 @@ function isOllamaAvailable() {
|
|
|
408
580
|
return isCommandAvailable("ollama", ["--version"]);
|
|
409
581
|
}
|
|
410
582
|
|
|
583
|
+
function resolvePromptStreams(stdin, stdout) {
|
|
584
|
+
if (hasInteractiveTty(stdin, stdout)) {
|
|
585
|
+
return { input: stdin, output: stdout, close: null };
|
|
586
|
+
}
|
|
587
|
+
const isWindows = process.platform === "win32";
|
|
588
|
+
const ttyPath = isWindows ? "CONIN$" : "/dev/tty";
|
|
589
|
+
try {
|
|
590
|
+
const input = fs.createReadStream(ttyPath, { autoClose: true });
|
|
591
|
+
return { input, output: stdout, close: () => input.close() };
|
|
592
|
+
} catch {
|
|
593
|
+
return { input: stdin, output: stdout, close: null };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
411
597
|
function promptYesNo(question, { defaultYes = true, stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
412
598
|
return new Promise((resolve) => {
|
|
413
|
-
const
|
|
599
|
+
const { input, output, close } = resolvePromptStreams(stdin, stdout);
|
|
600
|
+
const rl = readline.createInterface({ input, output });
|
|
414
601
|
rl.question(question, (answer) => {
|
|
415
602
|
rl.close();
|
|
603
|
+
if (typeof close === "function") close();
|
|
416
604
|
const normalized = String(answer || "").trim().toLowerCase();
|
|
417
605
|
if (!normalized) return resolve(defaultYes);
|
|
418
606
|
resolve(["y", "yes"].includes(normalized));
|
|
@@ -422,9 +610,11 @@ function promptYesNo(question, { defaultYes = true, stdin = process.stdin, stdou
|
|
|
422
610
|
|
|
423
611
|
function promptInput(question, { stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
424
612
|
return new Promise((resolve) => {
|
|
425
|
-
const
|
|
613
|
+
const { input, output, close } = resolvePromptStreams(stdin, stdout);
|
|
614
|
+
const rl = readline.createInterface({ input, output });
|
|
426
615
|
rl.question(question, (answer) => {
|
|
427
616
|
rl.close();
|
|
617
|
+
if (typeof close === "function") close();
|
|
428
618
|
resolve(String(answer || "").trim());
|
|
429
619
|
});
|
|
430
620
|
});
|
|
@@ -881,5 +1071,7 @@ module.exports = {
|
|
|
881
1071
|
readLlmDefaultModel,
|
|
882
1072
|
upsertLlmDefaultModel,
|
|
883
1073
|
pullOllamaModel,
|
|
884
|
-
listOllamaModels
|
|
1074
|
+
listOllamaModels,
|
|
1075
|
+
hasInteractiveTty,
|
|
1076
|
+
canPromptWithTty
|
|
885
1077
|
};
|
package/lib/uninstall.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const DAEMON_TASK_NAME = "Docdex Daemon";
|
|
10
|
+
const STARTUP_FAILURE_MARKER = "startup_registration_failed.json";
|
|
11
|
+
|
|
12
|
+
function daemonRootPath() {
|
|
13
|
+
return path.join(os.homedir(), ".docdex", "daemon_root");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stateDir() {
|
|
17
|
+
return path.join(os.homedir(), ".docdex", "state");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function daemonLockPath() {
|
|
21
|
+
const override = process.env.DOCDEX_DAEMON_LOCK_PATH;
|
|
22
|
+
if (override && override.trim()) return override.trim();
|
|
23
|
+
return path.join(os.homedir(), ".docdex", "daemon.lock");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clientConfigPaths() {
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
29
|
+
const userProfile = process.env.USERPROFILE || home;
|
|
30
|
+
switch (process.platform) {
|
|
31
|
+
case "win32":
|
|
32
|
+
return {
|
|
33
|
+
json: [
|
|
34
|
+
path.join(appData, "Claude", "claude_desktop_config.json"),
|
|
35
|
+
path.join(userProfile, ".cursor", "mcp.json"),
|
|
36
|
+
path.join(userProfile, ".cursor", "settings.json"),
|
|
37
|
+
path.join(userProfile, ".codeium", "windsurf", "mcp_config.json"),
|
|
38
|
+
path.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
39
|
+
path.join(appData, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
|
|
40
|
+
path.join(userProfile, ".continue", "config.json"),
|
|
41
|
+
path.join(userProfile, ".kiro", "settings", "mcp.json"),
|
|
42
|
+
path.join(appData, "Zed", "settings.json")
|
|
43
|
+
],
|
|
44
|
+
toml: [path.join(userProfile, ".codex", "config.toml")],
|
|
45
|
+
yaml: [path.join(appData, "Aider", "config.yml")]
|
|
46
|
+
};
|
|
47
|
+
case "darwin":
|
|
48
|
+
return {
|
|
49
|
+
json: [
|
|
50
|
+
path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
51
|
+
path.join(home, ".cursor", "mcp.json"),
|
|
52
|
+
path.join(home, ".cursor", "settings.json"),
|
|
53
|
+
path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
54
|
+
path.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
55
|
+
path.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
|
|
56
|
+
path.join(home, ".continue", "config.json"),
|
|
57
|
+
path.join(home, ".kiro", "settings", "mcp.json"),
|
|
58
|
+
path.join(home, ".config", "zed", "settings.json")
|
|
59
|
+
],
|
|
60
|
+
toml: [path.join(home, ".codex", "config.toml")],
|
|
61
|
+
yaml: [path.join(home, ".config", "aider", "config.yml"), path.join(home, ".aider.conf.yml")]
|
|
62
|
+
};
|
|
63
|
+
default:
|
|
64
|
+
return {
|
|
65
|
+
json: [
|
|
66
|
+
path.join(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
67
|
+
path.join(home, ".cursor", "mcp.json"),
|
|
68
|
+
path.join(home, ".cursor", "settings.json"),
|
|
69
|
+
path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
70
|
+
path.join(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
71
|
+
path.join(home, ".config", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
|
|
72
|
+
path.join(home, ".continue", "config.json"),
|
|
73
|
+
path.join(home, ".kiro", "settings", "mcp.json"),
|
|
74
|
+
path.join(home, ".config", "zed", "settings.json")
|
|
75
|
+
],
|
|
76
|
+
toml: [path.join(home, ".codex", "config.toml")],
|
|
77
|
+
yaml: [path.join(home, ".config", "aider", "config.yml"), path.join(home, ".aider.conf.yml")]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readJson(pathname) {
|
|
83
|
+
try {
|
|
84
|
+
if (!fs.existsSync(pathname)) return { value: {}, exists: false };
|
|
85
|
+
const raw = fs.readFileSync(pathname, "utf8");
|
|
86
|
+
if (!raw.trim()) return { value: {}, exists: true };
|
|
87
|
+
return { value: JSON.parse(raw), exists: true };
|
|
88
|
+
} catch {
|
|
89
|
+
return { value: {}, exists: true };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeJson(pathname, value) {
|
|
94
|
+
fs.mkdirSync(path.dirname(pathname), { recursive: true });
|
|
95
|
+
fs.writeFileSync(pathname, JSON.stringify(value, null, 2) + "\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeMcpServerJson(pathname, name = "docdex") {
|
|
99
|
+
const { value, exists } = readJson(pathname);
|
|
100
|
+
if (!exists || typeof value !== "object" || value == null || Array.isArray(value)) return false;
|
|
101
|
+
const root = value;
|
|
102
|
+
const keys = ["mcpServers", "mcp_servers"];
|
|
103
|
+
let changed = false;
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
const section = root[key];
|
|
106
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) continue;
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(section, name)) continue;
|
|
108
|
+
delete section[name];
|
|
109
|
+
changed = true;
|
|
110
|
+
if (Object.keys(section).length === 0) delete root[key];
|
|
111
|
+
}
|
|
112
|
+
if (!changed) return false;
|
|
113
|
+
writeJson(pathname, root);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function removeCodexConfig(pathname, name = "docdex") {
|
|
118
|
+
if (!fs.existsSync(pathname)) return false;
|
|
119
|
+
let contents = fs.readFileSync(pathname, "utf8");
|
|
120
|
+
const original = contents;
|
|
121
|
+
|
|
122
|
+
const parseTomlString = (value) => {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
const quoted = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
|
|
125
|
+
return quoted ? quoted[1] : trimmed;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const removeArrayBlocks = (text) => {
|
|
129
|
+
const lines = text.split(/\r?\n/);
|
|
130
|
+
const output = [];
|
|
131
|
+
let inBlock = false;
|
|
132
|
+
let block = [];
|
|
133
|
+
let blockHasName = false;
|
|
134
|
+
|
|
135
|
+
const flush = () => {
|
|
136
|
+
if (!inBlock) return;
|
|
137
|
+
if (!blockHasName) output.push(...block);
|
|
138
|
+
inBlock = false;
|
|
139
|
+
block = [];
|
|
140
|
+
blockHasName = false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (/^\s*\[\[mcp_servers\]\]\s*$/.test(line)) {
|
|
145
|
+
flush();
|
|
146
|
+
inBlock = true;
|
|
147
|
+
block = [line];
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (inBlock) {
|
|
151
|
+
if (/^\s*\[.+\]\s*$/.test(line)) {
|
|
152
|
+
flush();
|
|
153
|
+
output.push(line);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const match = line.match(/^\s*name\s*=\s*(.+?)\s*$/);
|
|
157
|
+
if (match && parseTomlString(match[1]) === name) {
|
|
158
|
+
blockHasName = true;
|
|
159
|
+
}
|
|
160
|
+
block.push(line);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
output.push(line);
|
|
164
|
+
}
|
|
165
|
+
flush();
|
|
166
|
+
return output.join("\n");
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const removeNestedSection = (text) => {
|
|
170
|
+
const lines = text.split(/\r?\n/);
|
|
171
|
+
const output = [];
|
|
172
|
+
let skip = false;
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (/^\s*\[mcp_servers\.docdex\]\s*$/.test(line)) {
|
|
175
|
+
skip = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (skip) {
|
|
179
|
+
if (/^\s*\[.+\]\s*$/.test(line)) {
|
|
180
|
+
skip = false;
|
|
181
|
+
output.push(line);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
output.push(line);
|
|
186
|
+
}
|
|
187
|
+
return output.join("\n");
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const removeTableEntry = (text) => {
|
|
191
|
+
const lines = text.split(/\r?\n/);
|
|
192
|
+
const output = [];
|
|
193
|
+
let inTable = false;
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
196
|
+
if (section) {
|
|
197
|
+
inTable = section[1].trim() === "mcp_servers";
|
|
198
|
+
output.push(line);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (inTable && new RegExp(`^\\s*${name}\\s*=`).test(line)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
output.push(line);
|
|
205
|
+
}
|
|
206
|
+
return output.join("\n");
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
contents = removeArrayBlocks(contents);
|
|
210
|
+
contents = removeNestedSection(contents);
|
|
211
|
+
contents = removeTableEntry(contents);
|
|
212
|
+
|
|
213
|
+
if (contents !== original) {
|
|
214
|
+
fs.writeFileSync(pathname, contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeMcpServerYaml(pathname, name = "docdex") {
|
|
221
|
+
if (!fs.existsSync(pathname)) return false;
|
|
222
|
+
const original = fs.readFileSync(pathname, "utf8");
|
|
223
|
+
const lines = original.split(/\r?\n/);
|
|
224
|
+
const output = [];
|
|
225
|
+
let inSection = false;
|
|
226
|
+
let sectionIndent = null;
|
|
227
|
+
let skipIndent = null;
|
|
228
|
+
let changed = false;
|
|
229
|
+
|
|
230
|
+
const indentSize = (line) => (line.match(/^\s*/)?.[0].length ?? 0);
|
|
231
|
+
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
if (skipIndent != null) {
|
|
234
|
+
if (line.trim() && indentSize(line) <= skipIndent) {
|
|
235
|
+
skipIndent = null;
|
|
236
|
+
} else {
|
|
237
|
+
changed = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!inSection) {
|
|
243
|
+
if (/^\s*mcp_servers\s*:\s*$/.test(line)) {
|
|
244
|
+
inSection = true;
|
|
245
|
+
sectionIndent = indentSize(line);
|
|
246
|
+
}
|
|
247
|
+
output.push(line);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (line.trim() && indentSize(line) <= sectionIndent) {
|
|
252
|
+
inSection = false;
|
|
253
|
+
output.push(line);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (new RegExp(`^\\s*${name}\\s*:`).test(line)) {
|
|
258
|
+
changed = true;
|
|
259
|
+
skipIndent = indentSize(line);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const listName = line.match(/^\s*-\s*name\s*:\s*(.+)\s*$/);
|
|
264
|
+
if (listName && listName[1].replace(/["']/g, "").trim() === name) {
|
|
265
|
+
changed = true;
|
|
266
|
+
skipIndent = indentSize(line);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const listValue = line.match(/^\s*-\s*([^\s#]+)\s*$/);
|
|
271
|
+
if (listValue && listValue[1].replace(/["']/g, "").trim() === name) {
|
|
272
|
+
changed = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
output.push(line);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (changed) {
|
|
280
|
+
fs.writeFileSync(pathname, output.join("\n"));
|
|
281
|
+
}
|
|
282
|
+
return changed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function killPid(pid) {
|
|
286
|
+
if (!pid) return false;
|
|
287
|
+
try {
|
|
288
|
+
if (process.platform === "win32") {
|
|
289
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
process.kill(pid, "SIGTERM");
|
|
293
|
+
return true;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function stopDaemonFromLock() {
|
|
300
|
+
const lockPath = daemonLockPath();
|
|
301
|
+
if (!fs.existsSync(lockPath)) return false;
|
|
302
|
+
try {
|
|
303
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
304
|
+
const payload = JSON.parse(raw);
|
|
305
|
+
const pid = payload && typeof payload.pid === "number" ? payload.pid : null;
|
|
306
|
+
const stopped = killPid(pid);
|
|
307
|
+
fs.unlinkSync(lockPath);
|
|
308
|
+
return stopped;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function stopDaemonByName() {
|
|
315
|
+
if (process.platform === "win32") {
|
|
316
|
+
spawnSync("taskkill", ["/IM", "docdexd.exe", "/T", "/F"]);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
spawnSync("pkill", ["-TERM", "-x", "docdexd"]);
|
|
320
|
+
spawnSync("pkill", ["-TERM", "-f", "docdexd daemon"]);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function unregisterStartup() {
|
|
325
|
+
if (process.platform === "darwin") {
|
|
326
|
+
const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.docdex.daemon.plist");
|
|
327
|
+
if (fs.existsSync(plistPath)) {
|
|
328
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
329
|
+
if (uid != null) {
|
|
330
|
+
spawnSync("launchctl", ["bootout", `gui/${uid}`, plistPath]);
|
|
331
|
+
}
|
|
332
|
+
spawnSync("launchctl", ["unload", "-w", plistPath]);
|
|
333
|
+
spawnSync("launchctl", ["remove", "com.docdex.daemon"]);
|
|
334
|
+
try {
|
|
335
|
+
fs.unlinkSync(plistPath);
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (process.platform === "linux") {
|
|
342
|
+
const systemdDir = path.join(os.homedir(), ".config", "systemd", "user");
|
|
343
|
+
const unitPath = path.join(systemdDir, "docdexd.service");
|
|
344
|
+
spawnSync("systemctl", ["--user", "disable", "--now", "docdexd.service"]);
|
|
345
|
+
if (fs.existsSync(unitPath)) {
|
|
346
|
+
try {
|
|
347
|
+
fs.unlinkSync(unitPath);
|
|
348
|
+
} catch {}
|
|
349
|
+
spawnSync("systemctl", ["--user", "daemon-reload"]);
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (process.platform === "win32") {
|
|
355
|
+
spawnSync("schtasks", ["/End", "/TN", DAEMON_TASK_NAME]);
|
|
356
|
+
spawnSync("schtasks", ["/Delete", "/TN", DAEMON_TASK_NAME, "/F"]);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function clearStartupFailure() {
|
|
364
|
+
const markerPath = path.join(stateDir(), STARTUP_FAILURE_MARKER);
|
|
365
|
+
if (fs.existsSync(markerPath)) {
|
|
366
|
+
try {
|
|
367
|
+
fs.unlinkSync(markerPath);
|
|
368
|
+
} catch {}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function removeDaemonRootNotice() {
|
|
373
|
+
const root = daemonRootPath();
|
|
374
|
+
const readmes = [path.join(root, "README.txt"), path.join(root, "README.md")];
|
|
375
|
+
for (const readme of readmes) {
|
|
376
|
+
if (fs.existsSync(readme)) {
|
|
377
|
+
try {
|
|
378
|
+
fs.unlinkSync(readme);
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function removeClientConfigs() {
|
|
385
|
+
const paths = clientConfigPaths();
|
|
386
|
+
for (const pathname of paths.json || []) {
|
|
387
|
+
removeMcpServerJson(pathname);
|
|
388
|
+
}
|
|
389
|
+
for (const pathname of paths.toml || []) {
|
|
390
|
+
removeCodexConfig(pathname);
|
|
391
|
+
}
|
|
392
|
+
for (const pathname of paths.yaml || []) {
|
|
393
|
+
removeMcpServerYaml(pathname);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function main() {
|
|
398
|
+
const stopped = stopDaemonFromLock();
|
|
399
|
+
if (!stopped) {
|
|
400
|
+
stopDaemonByName();
|
|
401
|
+
}
|
|
402
|
+
unregisterStartup();
|
|
403
|
+
removeClientConfigs();
|
|
404
|
+
clearStartupFailure();
|
|
405
|
+
removeDaemonRootNotice();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (require.main === module) {
|
|
409
|
+
main().catch(() => process.exit(0));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
removeMcpServerJson,
|
|
414
|
+
removeCodexConfig,
|
|
415
|
+
removeMcpServerYaml,
|
|
416
|
+
stopDaemonFromLock,
|
|
417
|
+
stopDaemonByName,
|
|
418
|
+
unregisterStartup,
|
|
419
|
+
removeClientConfigs
|
|
420
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docdex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Docdex CLI as an npm-installable binary wrapper.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"docdex": "bin/docdex.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"postinstall": "node ./lib/install.js",
|
|
18
|
+
"postuninstall": "node ./lib/uninstall.js",
|
|
18
19
|
"test": "node --test",
|
|
19
20
|
"pack:verify": "node --test test/packaging_guardrails.test.js"
|
|
20
21
|
},
|