docdex 0.2.28 → 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 +1 -1
- package/bin/docdex.js +30 -6
- package/lib/install.js +99 -14
- package/lib/paths.js +112 -0
- package/lib/postinstall_setup.js +318 -43
- package/lib/uninstall.js +8 -0
- package/package.json +1 -1
package/assets/agents.md
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/postinstall_setup.js
CHANGED
|
@@ -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;
|
|
@@ -18,6 +24,8 @@ const DAEMON_HEALTH_TIMEOUT_MS = 8000;
|
|
|
18
24
|
const DAEMON_HEALTH_REQUEST_TIMEOUT_MS = 1000;
|
|
19
25
|
const DAEMON_HEALTH_POLL_INTERVAL_MS = 200;
|
|
20
26
|
const DAEMON_HEALTH_PATH = "/healthz";
|
|
27
|
+
const DAEMON_INFO_PATH = "/ai-help";
|
|
28
|
+
const DAEMON_PORT_RELEASE_TIMEOUT_MS = 5000;
|
|
21
29
|
const STARTUP_FAILURE_MARKER = "startup_registration_failed.json";
|
|
22
30
|
const DEFAULT_OLLAMA_MODEL = "nomic-embed-text";
|
|
23
31
|
const DEFAULT_OLLAMA_CHAT_MODEL = "phi3.5:3.8b";
|
|
@@ -120,6 +128,139 @@ async function waitForDaemonHealthy({ host, port, timeoutMs = DAEMON_HEALTH_TIME
|
|
|
120
128
|
return false;
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
async function waitForPortAvailable({
|
|
132
|
+
host,
|
|
133
|
+
port,
|
|
134
|
+
timeoutMs = DAEMON_PORT_RELEASE_TIMEOUT_MS
|
|
135
|
+
}) {
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
while (Date.now() < deadline) {
|
|
138
|
+
if (await isPortAvailable(port, host)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
await sleep(DAEMON_HEALTH_POLL_INTERVAL_MS);
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isPidRunning(pid) {
|
|
147
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 0);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return err?.code === "EPERM";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readDaemonLockMetadataForPort(port) {
|
|
157
|
+
for (const lockPath of daemonLockPaths()) {
|
|
158
|
+
if (!lockPath || !fs.existsSync(lockPath)) continue;
|
|
159
|
+
try {
|
|
160
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
161
|
+
if (!raw.trim()) continue;
|
|
162
|
+
const payload = JSON.parse(raw);
|
|
163
|
+
const lockPort = Number(payload?.port);
|
|
164
|
+
const pid = Number(payload?.pid);
|
|
165
|
+
if (!Number.isFinite(lockPort) || lockPort !== port) continue;
|
|
166
|
+
return { pid, port: lockPort };
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeVersion(value) {
|
|
175
|
+
return String(value || "")
|
|
176
|
+
.trim()
|
|
177
|
+
.replace(/^v/i, "");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fetchDaemonInfo({ host, port, timeoutMs = DAEMON_HEALTH_REQUEST_TIMEOUT_MS }) {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
const req = http.request(
|
|
183
|
+
{
|
|
184
|
+
host,
|
|
185
|
+
port,
|
|
186
|
+
path: DAEMON_INFO_PATH,
|
|
187
|
+
method: "GET",
|
|
188
|
+
timeout: timeoutMs
|
|
189
|
+
},
|
|
190
|
+
(res) => {
|
|
191
|
+
let body = "";
|
|
192
|
+
res.setEncoding("utf8");
|
|
193
|
+
res.on("data", (chunk) => {
|
|
194
|
+
body += chunk;
|
|
195
|
+
});
|
|
196
|
+
res.on("end", () => {
|
|
197
|
+
if (res.statusCode !== 200) return resolve(null);
|
|
198
|
+
try {
|
|
199
|
+
const payload = JSON.parse(body);
|
|
200
|
+
resolve(payload);
|
|
201
|
+
} catch {
|
|
202
|
+
resolve(null);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
req.on("timeout", () => {
|
|
208
|
+
req.destroy();
|
|
209
|
+
resolve(null);
|
|
210
|
+
});
|
|
211
|
+
req.on("error", () => resolve(null));
|
|
212
|
+
req.end();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkDocdexIdentity({ host, port, timeoutMs = DAEMON_HEALTH_REQUEST_TIMEOUT_MS }) {
|
|
217
|
+
return fetchDaemonInfo({ host, port, timeoutMs }).then((payload) => payload?.product === "Docdex");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function resolveDaemonPortState({ host, port, logger, deps } = {}) {
|
|
221
|
+
const log = logger || console;
|
|
222
|
+
const helpers = {
|
|
223
|
+
isPortAvailable,
|
|
224
|
+
checkDaemonHealth,
|
|
225
|
+
checkDocdexIdentity,
|
|
226
|
+
stopDaemonService,
|
|
227
|
+
stopDaemonFromLock,
|
|
228
|
+
stopDaemonByName,
|
|
229
|
+
clearDaemonLocks,
|
|
230
|
+
sleep,
|
|
231
|
+
readDaemonLockMetadataForPort,
|
|
232
|
+
isPidRunning,
|
|
233
|
+
normalizeVersion
|
|
234
|
+
};
|
|
235
|
+
if (deps && typeof deps === "object") {
|
|
236
|
+
Object.assign(helpers, deps);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let available = await helpers.isPortAvailable(port, host);
|
|
240
|
+
if (available) return { available: true, reuseExisting: false, stopped: false };
|
|
241
|
+
|
|
242
|
+
helpers.stopDaemonService({ logger: log });
|
|
243
|
+
helpers.stopDaemonFromLock({ logger: log });
|
|
244
|
+
helpers.stopDaemonByName({ logger: log });
|
|
245
|
+
await helpers.sleep(DAEMON_HEALTH_POLL_INTERVAL_MS);
|
|
246
|
+
|
|
247
|
+
available = await helpers.isPortAvailable(port, host);
|
|
248
|
+
if (available) {
|
|
249
|
+
helpers.clearDaemonLocks();
|
|
250
|
+
return { available: true, reuseExisting: false, stopped: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const lockMeta = helpers.readDaemonLockMetadataForPort(port);
|
|
254
|
+
const lockRunning = lockMeta ? helpers.isPidRunning(lockMeta.pid) : false;
|
|
255
|
+
const healthy = await helpers.checkDaemonHealth({ host, port });
|
|
256
|
+
const identity = lockRunning ? true : await helpers.checkDocdexIdentity({ host, port });
|
|
257
|
+
const reuseExisting = Boolean(lockRunning || healthy || identity);
|
|
258
|
+
if (reuseExisting) {
|
|
259
|
+
log.warn?.(`[docdex] ${host}:${port} already in use by a running docdex daemon; reusing it.`);
|
|
260
|
+
}
|
|
261
|
+
return { available: false, reuseExisting, stopped: false };
|
|
262
|
+
}
|
|
263
|
+
|
|
123
264
|
function parseServerBind(contents) {
|
|
124
265
|
let inServer = false;
|
|
125
266
|
const lines = contents.split(/\r?\n/);
|
|
@@ -1256,12 +1397,38 @@ function clientInstructionPaths() {
|
|
|
1256
1397
|
}
|
|
1257
1398
|
}
|
|
1258
1399
|
|
|
1259
|
-
function
|
|
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 } = {}) {
|
|
1260
1406
|
if (binaryPath && fs.existsSync(binaryPath)) return binaryPath;
|
|
1261
1407
|
try {
|
|
1262
1408
|
const platformKey = detectPlatformKey();
|
|
1263
|
-
const
|
|
1264
|
-
|
|
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
|
+
}
|
|
1265
1432
|
} catch (err) {
|
|
1266
1433
|
if (!(err instanceof UnsupportedPlatformError)) throw err;
|
|
1267
1434
|
}
|
|
@@ -1281,16 +1448,26 @@ function isMacProtectedPath(candidate) {
|
|
|
1281
1448
|
return ["Desktop", "Documents", "Downloads"].some((dir) => isPathWithin(path.join(home, dir), candidate));
|
|
1282
1449
|
}
|
|
1283
1450
|
|
|
1284
|
-
function ensureStartupBinary(binaryPath, { logger } = {}) {
|
|
1451
|
+
function ensureStartupBinary(binaryPath, { logger, env, distBaseDir } = {}) {
|
|
1285
1452
|
if (!binaryPath) return null;
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
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);
|
|
1289
1464
|
if (fs.existsSync(target)) return target;
|
|
1290
1465
|
try {
|
|
1291
1466
|
fs.mkdirSync(binDir, { recursive: true });
|
|
1292
1467
|
fs.copyFileSync(binaryPath, target);
|
|
1293
|
-
|
|
1468
|
+
if (!isWin32) {
|
|
1469
|
+
fs.chmodSync(target, 0o755);
|
|
1470
|
+
}
|
|
1294
1471
|
return target;
|
|
1295
1472
|
} catch (err) {
|
|
1296
1473
|
logger?.warn?.(`[docdex] failed to stage daemon binary for startup: ${err?.message || err}`);
|
|
@@ -1298,8 +1475,8 @@ function ensureStartupBinary(binaryPath, { logger } = {}) {
|
|
|
1298
1475
|
}
|
|
1299
1476
|
}
|
|
1300
1477
|
|
|
1301
|
-
function resolveStartupBinaryPaths({ binaryPath, logger } = {}) {
|
|
1302
|
-
const resolvedBinary = ensureStartupBinary(binaryPath, { logger });
|
|
1478
|
+
function resolveStartupBinaryPaths({ binaryPath, logger, env, distBaseDir } = {}) {
|
|
1479
|
+
const resolvedBinary = ensureStartupBinary(binaryPath, { logger, env, distBaseDir });
|
|
1303
1480
|
return { binaryPath: resolvedBinary };
|
|
1304
1481
|
}
|
|
1305
1482
|
|
|
@@ -1977,7 +2154,32 @@ function buildDaemonEnv() {
|
|
|
1977
2154
|
return Object.fromEntries(buildDaemonEnvPairs());
|
|
1978
2155
|
}
|
|
1979
2156
|
|
|
1980
|
-
function
|
|
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 }) {
|
|
1981
2183
|
if (!binaryPath) return { ok: false, reason: "missing_binary" };
|
|
1982
2184
|
stopDaemonService({ logger });
|
|
1983
2185
|
const envPairs = buildDaemonEnvPairs();
|
|
@@ -2073,11 +2275,16 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
|
|
|
2073
2275
|
|
|
2074
2276
|
if (process.platform === "win32") {
|
|
2075
2277
|
const taskName = "Docdex Daemon";
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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)}`;
|
|
2081
2288
|
const create = spawnSync("schtasks", [
|
|
2082
2289
|
"/Create",
|
|
2083
2290
|
"/F",
|
|
@@ -2101,12 +2308,13 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
|
|
|
2101
2308
|
return { ok: false, reason: "unsupported_platform" };
|
|
2102
2309
|
}
|
|
2103
2310
|
|
|
2104
|
-
async function startDaemonWithHealthCheck({ binaryPath, port, host, logger }) {
|
|
2311
|
+
async function startDaemonWithHealthCheck({ binaryPath, port, host, logger, distBaseDir }) {
|
|
2105
2312
|
const startup = registerStartup({
|
|
2106
2313
|
binaryPath,
|
|
2107
2314
|
port,
|
|
2108
2315
|
repoRoot: daemonRootPath(),
|
|
2109
|
-
logger
|
|
2316
|
+
logger,
|
|
2317
|
+
distBaseDir
|
|
2110
2318
|
});
|
|
2111
2319
|
if (!startup.ok) {
|
|
2112
2320
|
logger?.warn?.(`[docdex] daemon service registration failed (${startup.reason || "unknown"}).`);
|
|
@@ -2146,6 +2354,17 @@ function startupFailureReported() {
|
|
|
2146
2354
|
return fs.existsSync(path.join(stateDir(), STARTUP_FAILURE_MARKER));
|
|
2147
2355
|
}
|
|
2148
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
|
+
|
|
2149
2368
|
function shouldSkipSetup(env = process.env) {
|
|
2150
2369
|
return parseEnvBool(env.DOCDEX_SETUP_SKIP) === true;
|
|
2151
2370
|
}
|
|
@@ -2246,41 +2465,90 @@ function launchSetupWizard({
|
|
|
2246
2465
|
return { ok: false, reason: "unsupported_platform" };
|
|
2247
2466
|
}
|
|
2248
2467
|
|
|
2249
|
-
async function runPostInstallSetup({ binaryPath, logger } = {}) {
|
|
2468
|
+
async function runPostInstallSetup({ binaryPath, logger, env, skipDaemon, distBaseDir } = {}) {
|
|
2250
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 });
|
|
2251
2474
|
const configPath = defaultConfigPath();
|
|
2252
2475
|
let existingConfig = "";
|
|
2253
2476
|
if (fs.existsSync(configPath)) {
|
|
2254
2477
|
existingConfig = fs.readFileSync(configPath, "utf8");
|
|
2255
2478
|
}
|
|
2256
2479
|
const port = DEFAULT_DAEMON_PORT;
|
|
2257
|
-
|
|
2258
|
-
if (
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
+
}
|
|
2263
2494
|
}
|
|
2264
2495
|
|
|
2265
2496
|
const daemonRoot = ensureDaemonRoot();
|
|
2266
|
-
const resolvedBinary = resolveBinaryPath({
|
|
2497
|
+
const resolvedBinary = resolveBinaryPath({
|
|
2498
|
+
binaryPath,
|
|
2499
|
+
env: effectiveEnv,
|
|
2500
|
+
distBaseDir: resolvedDistBaseDir,
|
|
2501
|
+
distBaseCandidates: distCandidates
|
|
2502
|
+
});
|
|
2267
2503
|
const startupBinaries = resolveStartupBinaryPaths({
|
|
2268
2504
|
binaryPath: resolvedBinary,
|
|
2269
|
-
logger: log
|
|
2505
|
+
logger: log,
|
|
2506
|
+
env: effectiveEnv,
|
|
2507
|
+
distBaseDir: resolvedDistBaseDir
|
|
2270
2508
|
});
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2509
|
+
let reuseExisting = allowDaemon ? portState.reuseExisting : false;
|
|
2510
|
+
if (reuseExisting && allowDaemon) {
|
|
2511
|
+
const daemonInfo = await fetchDaemonInfo({ host: DEFAULT_HOST, port });
|
|
2512
|
+
const daemonVersion = normalizeVersion(daemonInfo?.version);
|
|
2513
|
+
const packageVersion = normalizeVersion(resolvePackageVersion());
|
|
2514
|
+
if (daemonInfo?.product === "Docdex" && daemonVersion && packageVersion) {
|
|
2515
|
+
if (daemonVersion !== packageVersion) {
|
|
2516
|
+
log.warn?.(
|
|
2517
|
+
`[docdex] daemon version ${daemonVersion} differs from package ${packageVersion}; restarting daemon.`
|
|
2518
|
+
);
|
|
2519
|
+
stopDaemonService({ logger: log });
|
|
2520
|
+
stopDaemonFromLock({ logger: log });
|
|
2521
|
+
stopDaemonByName({ logger: log });
|
|
2522
|
+
clearDaemonLocks();
|
|
2523
|
+
const released = await waitForPortAvailable({
|
|
2524
|
+
host: DEFAULT_HOST,
|
|
2525
|
+
port
|
|
2526
|
+
});
|
|
2527
|
+
if (!released) {
|
|
2528
|
+
log.warn?.("[docdex] daemon restart failed; port still in use.");
|
|
2529
|
+
recordStartupFailure({ reason: "restart_failed", host: DEFAULT_HOST, port });
|
|
2530
|
+
allowDaemon = false;
|
|
2531
|
+
}
|
|
2532
|
+
reuseExisting = false;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
let startupOk = reuseExisting;
|
|
2537
|
+
if (allowDaemon && !reuseExisting) {
|
|
2538
|
+
const result = await startDaemonWithHealthCheck({
|
|
2539
|
+
binaryPath: startupBinaries.binaryPath,
|
|
2540
|
+
port,
|
|
2541
|
+
host: DEFAULT_HOST,
|
|
2542
|
+
logger: log,
|
|
2543
|
+
distBaseDir: resolvedDistBaseDir
|
|
2544
|
+
});
|
|
2545
|
+
if (!result.ok) {
|
|
2546
|
+
log.warn?.(`[docdex] daemon failed to start on ${DEFAULT_HOST}:${port}.`);
|
|
2547
|
+
recordStartupFailure({ reason: result.reason || "startup_failed", host: DEFAULT_HOST, port });
|
|
2548
|
+
allowDaemon = false;
|
|
2549
|
+
} else {
|
|
2550
|
+
startupOk = true;
|
|
2551
|
+
}
|
|
2284
2552
|
}
|
|
2285
2553
|
|
|
2286
2554
|
const httpBindAddr = `${DEFAULT_HOST}:${port}`;
|
|
@@ -2313,8 +2581,13 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
|
|
|
2313
2581
|
}
|
|
2314
2582
|
upsertCodexConfig(paths.codex, codexUrl);
|
|
2315
2583
|
applyAgentInstructions({ logger: log });
|
|
2316
|
-
|
|
2317
|
-
|
|
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 });
|
|
2318
2591
|
if (!setupLaunch.ok && setupLaunch.reason !== "skipped") {
|
|
2319
2592
|
log.warn?.("[docdex] setup wizard did not launch. Run `docdex setup`.");
|
|
2320
2593
|
recordSetupPending({ reason: setupLaunch.reason, port, repoRoot: daemonRoot });
|
|
@@ -2345,5 +2618,7 @@ module.exports = {
|
|
|
2345
2618
|
shouldSkipSetup,
|
|
2346
2619
|
launchSetupWizard,
|
|
2347
2620
|
applyAgentInstructions,
|
|
2348
|
-
buildDaemonEnv
|
|
2621
|
+
buildDaemonEnv,
|
|
2622
|
+
resolveDaemonPortState,
|
|
2623
|
+
normalizeVersion
|
|
2349
2624
|
};
|
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
|
}
|