docdex 0.2.29 → 0.2.31
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 +3 -0
- package/assets/agents.md +1 -1
- package/bin/docdex.js +30 -6
- package/lib/cli_entry.js +241 -0
- package/lib/install.js +215 -21
- package/lib/paths.js +112 -0
- package/lib/postinstall_setup.js +150 -39
- package/lib/uninstall.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
- Remove legacy stdio MCP (`docdexd mcp` / `docdex-mcp-server`); MCP is served only over HTTP/SSE.
|
|
5
|
+
- Ensure npm tarballs include CLI wrapper entrypoints and restore missing wrappers during postinstall.
|
|
6
|
+
- Retry Windows file operations during install to reduce EPERM/EACCES failures.
|
|
7
|
+
- Nightly HTTP soak waits for index readiness before load testing.
|
|
5
8
|
|
|
6
9
|
## 0.2.23
|
|
7
10
|
- Add Smithery session config schema metadata (titles/descriptions, defaults, example config) for local MCP sessions.
|
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/cli_entry.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const pkg = require("../package.json");
|
|
8
|
+
const { resolveDistBaseDir, resolveDistBaseCandidates } = require("./paths");
|
|
9
|
+
const {
|
|
10
|
+
artifactName,
|
|
11
|
+
detectLibcFromRuntime,
|
|
12
|
+
detectPlatformKey,
|
|
13
|
+
targetTripleForPlatformKey,
|
|
14
|
+
assetPatternForPlatformKey,
|
|
15
|
+
UnsupportedPlatformError
|
|
16
|
+
} = require("./platform");
|
|
17
|
+
const { checkForUpdateOnce } = require("./update_check");
|
|
18
|
+
|
|
19
|
+
function isDoctorCommand(argv) {
|
|
20
|
+
const sub = argv[0];
|
|
21
|
+
return sub === "doctor" || sub === "diagnostics";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printLines(lines, { stderr } = {}) {
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (!line) continue;
|
|
27
|
+
if (stderr) console.error(line);
|
|
28
|
+
else console.log(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envBool(value) {
|
|
33
|
+
if (!value) return false;
|
|
34
|
+
const normalized = String(value).trim().toLowerCase();
|
|
35
|
+
return ["1", "true", "t", "yes", "y", "on"].includes(normalized);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readInstallMetadata({ fsModule, pathModule, basePath }) {
|
|
39
|
+
if (!fsModule || typeof fsModule.readFileSync !== "function") return null;
|
|
40
|
+
const metadataPath = pathModule.join(basePath, "docdexd-install.json");
|
|
41
|
+
try {
|
|
42
|
+
const raw = fsModule.readFileSync(metadataPath, "utf8");
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatInstallSource(meta) {
|
|
50
|
+
const source = meta?.archive?.source;
|
|
51
|
+
if (!source || typeof source !== "string") return "unknown";
|
|
52
|
+
if (source === "local") return "local binary";
|
|
53
|
+
return `release (${source})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveInstallPaths(platformKey) {
|
|
57
|
+
const binaryName = process.platform === "win32" ? "docdexd.exe" : "docdexd";
|
|
58
|
+
const candidates = [];
|
|
59
|
+
for (const distBase of resolveDistBaseCandidates({ env: process.env })) {
|
|
60
|
+
candidates.push(path.join(distBase, platformKey));
|
|
61
|
+
}
|
|
62
|
+
candidates.push(path.join(__dirname, "..", "dist", platformKey));
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const unique = candidates.filter((candidate) => {
|
|
65
|
+
if (!candidate || seen.has(candidate)) return false;
|
|
66
|
+
seen.add(candidate);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
for (const basePath of unique) {
|
|
70
|
+
const binaryPath = path.join(basePath, binaryName);
|
|
71
|
+
if (fs.existsSync(binaryPath)) {
|
|
72
|
+
return { basePath, binaryPath };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const fallbackBase =
|
|
76
|
+
unique[0] || path.join(resolveDistBaseDir({ env: process.env, fsModule: fs }), platformKey);
|
|
77
|
+
return { basePath: fallbackBase, binaryPath: path.join(fallbackBase, binaryName) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runDoctor() {
|
|
81
|
+
const platform = process.platform;
|
|
82
|
+
const arch = process.arch;
|
|
83
|
+
|
|
84
|
+
let libc = null;
|
|
85
|
+
if (platform === "linux") {
|
|
86
|
+
try {
|
|
87
|
+
libc = detectLibcFromRuntime();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
printLines(
|
|
90
|
+
[
|
|
91
|
+
"[docdex] doctor failed: could not detect libc",
|
|
92
|
+
`[docdex] Detected platform: ${platform}/${arch}`,
|
|
93
|
+
`[docdex] Error: ${err?.message || String(err)}`
|
|
94
|
+
],
|
|
95
|
+
{ stderr: true }
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let report;
|
|
103
|
+
try {
|
|
104
|
+
const platformKey = detectPlatformKey();
|
|
105
|
+
const targetTriple = targetTripleForPlatformKey(platformKey);
|
|
106
|
+
const expectedAssetName = artifactName(platformKey);
|
|
107
|
+
const expectedAssetPattern = assetPatternForPlatformKey(platformKey, { exampleAssetName: expectedAssetName });
|
|
108
|
+
const distCandidates = resolveDistBaseCandidates({ env: process.env });
|
|
109
|
+
const distBase =
|
|
110
|
+
distCandidates[0] || resolveDistBaseDir({ env: process.env, fsModule: null });
|
|
111
|
+
const basePath = path.join(distBase, platformKey);
|
|
112
|
+
const installMeta = readInstallMetadata({ fsModule: fs, pathModule: path, basePath });
|
|
113
|
+
const installSource = formatInstallSource(installMeta);
|
|
114
|
+
|
|
115
|
+
report = {
|
|
116
|
+
exitCode: 0,
|
|
117
|
+
stderr: false,
|
|
118
|
+
lines: [
|
|
119
|
+
"[docdex] doctor",
|
|
120
|
+
`[docdex] Detected platform: ${platform}/${arch}${libc ? `/${libc}` : ""}`,
|
|
121
|
+
"[docdex] Supported: yes",
|
|
122
|
+
`[docdex] Platform key: ${platformKey}`,
|
|
123
|
+
`[docdex] Expected target triple: ${targetTriple}`,
|
|
124
|
+
`[docdex] Expected release asset: ${expectedAssetName}`,
|
|
125
|
+
`[docdex] Asset naming pattern: ${expectedAssetPattern}`,
|
|
126
|
+
`[docdex] Install source: ${installSource}`
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err instanceof UnsupportedPlatformError) {
|
|
131
|
+
const detected = `${err.details?.platform ?? platform}/${err.details?.arch ?? arch}`;
|
|
132
|
+
const libcSuffix = err.details?.libc ? `/${err.details.libc}` : "";
|
|
133
|
+
const candidatePlatformKey =
|
|
134
|
+
typeof err.details?.candidatePlatformKey === "string" ? err.details.candidatePlatformKey : null;
|
|
135
|
+
const candidateTargetTriple =
|
|
136
|
+
typeof err.details?.candidateTargetTriple === "string" ? err.details.candidateTargetTriple : null;
|
|
137
|
+
const supportedKeys = Array.isArray(err.details?.supportedPlatformKeys) ? err.details.supportedPlatformKeys : [];
|
|
138
|
+
|
|
139
|
+
const candidateAssetName = candidatePlatformKey ? artifactName(candidatePlatformKey) : null;
|
|
140
|
+
const candidateAssetPattern = candidatePlatformKey
|
|
141
|
+
? assetPatternForPlatformKey(candidatePlatformKey, { exampleAssetName: candidateAssetName })
|
|
142
|
+
: null;
|
|
143
|
+
|
|
144
|
+
report = {
|
|
145
|
+
exitCode: err.exitCode || 3,
|
|
146
|
+
stderr: true,
|
|
147
|
+
lines: [
|
|
148
|
+
"[docdex] doctor",
|
|
149
|
+
`[docdex] Detected platform: ${detected}${libcSuffix}`,
|
|
150
|
+
"[docdex] Supported: no",
|
|
151
|
+
`[docdex] error code: ${err.code}`,
|
|
152
|
+
"[docdex] No download/install is attempted for this platform.",
|
|
153
|
+
candidatePlatformKey ? `[docdex] Platform key: ${candidatePlatformKey}` : null,
|
|
154
|
+
candidateTargetTriple ? `[docdex] Target triple: ${candidateTargetTriple}` : null,
|
|
155
|
+
candidateAssetPattern ? `[docdex] Asset naming pattern: ${candidateAssetPattern}` : null,
|
|
156
|
+
supportedKeys.length ? `[docdex] Supported platforms: ${supportedKeys.join(", ")}` : null,
|
|
157
|
+
"[docdex] Next steps: use a supported platform or build from source (Rust)."
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
} else {
|
|
161
|
+
report = {
|
|
162
|
+
exitCode: 1,
|
|
163
|
+
stderr: true,
|
|
164
|
+
lines: [
|
|
165
|
+
"[docdex] doctor failed: unexpected error",
|
|
166
|
+
`[docdex] Detected platform: ${platform}/${arch}${libc ? `/${libc}` : ""}`,
|
|
167
|
+
`[docdex] Error: ${err?.message || String(err)}`
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
printLines(report.lines, { stderr: report.stderr });
|
|
174
|
+
process.exit(report.exitCode);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function run() {
|
|
178
|
+
const argv = process.argv.slice(2);
|
|
179
|
+
if (isDoctorCommand(argv)) {
|
|
180
|
+
runDoctor();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let platformKey;
|
|
185
|
+
try {
|
|
186
|
+
platformKey = detectPlatformKey();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof UnsupportedPlatformError) {
|
|
189
|
+
const detected = `${err.details?.platform ?? process.platform}/${err.details?.arch ?? process.arch}`;
|
|
190
|
+
const libc = err.details?.libc ? `/${err.details.libc}` : "";
|
|
191
|
+
console.error(`[docdex] unsupported platform (${detected}${libc})`);
|
|
192
|
+
console.error(`[docdex] error code: ${err.code}`);
|
|
193
|
+
console.error("[docdex] No download/run was attempted for this platform.");
|
|
194
|
+
if (Array.isArray(err.details?.supportedPlatformKeys) && err.details.supportedPlatformKeys.length) {
|
|
195
|
+
console.error(`[docdex] Supported platforms: ${err.details.supportedPlatformKeys.join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
if (typeof err.details?.candidatePlatformKey === "string") {
|
|
198
|
+
console.error(`[docdex] Asset naming pattern: ${assetPatternForPlatformKey(err.details.candidatePlatformKey)}`);
|
|
199
|
+
}
|
|
200
|
+
console.error("[docdex] Next steps: use a supported platform or build from source (Rust).");
|
|
201
|
+
process.exit(err.exitCode || 3);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.error(`[docdex] failed to detect platform: ${err?.message || String(err)}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { binaryPath } = resolveInstallPaths(platformKey);
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(binaryPath)) {
|
|
212
|
+
console.error(`[docdex] Missing binary for ${platformKey}. Try reinstalling or set DOCDEX_DOWNLOAD_REPO to a repo with release assets.`);
|
|
213
|
+
try {
|
|
214
|
+
console.error(`[docdex] Expected target triple: ${targetTripleForPlatformKey(platformKey)}`);
|
|
215
|
+
console.error(`[docdex] Asset naming pattern: ${assetPatternForPlatformKey(platformKey)}`);
|
|
216
|
+
} catch {}
|
|
217
|
+
process.exit(1);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await checkForUpdateOnce({
|
|
222
|
+
currentVersion: pkg.version,
|
|
223
|
+
env: process.env,
|
|
224
|
+
stdout: process.stdout,
|
|
225
|
+
stderr: process.stderr,
|
|
226
|
+
logger: console
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const env = { ...process.env };
|
|
230
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", env });
|
|
231
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
232
|
+
child.on("error", (err) => {
|
|
233
|
+
console.error(`[docdex] failed to launch binary: ${err.message}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
run().catch((err) => {
|
|
239
|
+
console.error(`[docdex] unexpected error: ${err?.message || String(err)}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
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";
|
|
@@ -41,6 +42,31 @@ const DEFAULT_INTEGRITY_CONFIG = Object.freeze({
|
|
|
41
42
|
const LOCAL_FALLBACK_ENV = "DOCDEX_LOCAL_FALLBACK";
|
|
42
43
|
const LOCAL_BINARY_ENV = "DOCDEX_LOCAL_BINARY";
|
|
43
44
|
const AGENTS_DOC_FILENAME = "agents.md";
|
|
45
|
+
const CLI_WRAPPER_SCRIPT = [
|
|
46
|
+
"#!/usr/bin/env node",
|
|
47
|
+
"\"use strict\";",
|
|
48
|
+
"",
|
|
49
|
+
"require(\"../lib/cli_entry\");",
|
|
50
|
+
""
|
|
51
|
+
].join("\n");
|
|
52
|
+
const MCP_STDIO_WRAPPER_SCRIPT = [
|
|
53
|
+
"#!/usr/bin/env node",
|
|
54
|
+
"\"use strict\";",
|
|
55
|
+
"",
|
|
56
|
+
"const { runBridge } = require(\"../lib/mcp_stdio_bridge\");",
|
|
57
|
+
"",
|
|
58
|
+
"async function main() {",
|
|
59
|
+
" try {",
|
|
60
|
+
" await runBridge({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr });",
|
|
61
|
+
" } catch (err) {",
|
|
62
|
+
" process.stderr.write(`[docdex-mcp-stdio] fatal: ${err}\\n`);",
|
|
63
|
+
" process.exit(1);",
|
|
64
|
+
" }",
|
|
65
|
+
"}",
|
|
66
|
+
"",
|
|
67
|
+
"main();",
|
|
68
|
+
""
|
|
69
|
+
].join("\n");
|
|
44
70
|
|
|
45
71
|
const EXIT_CODE_BY_ERROR_CODE = Object.freeze({
|
|
46
72
|
DOCDEX_INSTALLER_CONFIG: 2,
|
|
@@ -230,6 +256,53 @@ function writeAgentInstructions() {
|
|
|
230
256
|
}
|
|
231
257
|
}
|
|
232
258
|
|
|
259
|
+
function shouldWriteWrapper(fsModule, filePath) {
|
|
260
|
+
if (!fsModule?.existsSync) return true;
|
|
261
|
+
if (!fsModule.existsSync(filePath)) return true;
|
|
262
|
+
try {
|
|
263
|
+
const stat = fsModule.statSync(filePath);
|
|
264
|
+
if (!stat.isFile()) return true;
|
|
265
|
+
return stat.size < 8;
|
|
266
|
+
} catch {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function ensureCliWrappers({ fsModule = fs, pathModule = path, logger } = {}) {
|
|
272
|
+
const log = logger || console;
|
|
273
|
+
const binDir = pathModule.join(__dirname, "..", "bin");
|
|
274
|
+
const wrappers = [
|
|
275
|
+
{ path: pathModule.join(binDir, "docdex.js"), contents: CLI_WRAPPER_SCRIPT },
|
|
276
|
+
{ path: pathModule.join(binDir, "docdex-mcp-stdio.js"), contents: MCP_STDIO_WRAPPER_SCRIPT }
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
fsModule.mkdirSync(binDir, { recursive: true });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
log?.warn?.(`[docdex] unable to ensure CLI wrappers (mkdir failed): ${err?.message || err}`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let repaired = 0;
|
|
287
|
+
for (const wrapper of wrappers) {
|
|
288
|
+
if (!shouldWriteWrapper(fsModule, wrapper.path)) continue;
|
|
289
|
+
try {
|
|
290
|
+
fsModule.writeFileSync(wrapper.path, wrapper.contents, "utf8");
|
|
291
|
+
if (process.platform !== "win32" && fsModule.chmodSync) {
|
|
292
|
+
fsModule.chmodSync(wrapper.path, 0o755);
|
|
293
|
+
}
|
|
294
|
+
repaired += 1;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log?.warn?.(`[docdex] unable to write CLI wrapper at ${wrapper.path}: ${err?.message || err}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (repaired > 0) {
|
|
301
|
+
log?.warn?.(`[docdex] restored ${repaired} CLI wrapper(s) under ${binDir}`);
|
|
302
|
+
}
|
|
303
|
+
return repaired > 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
233
306
|
function selectHttpClient(url) {
|
|
234
307
|
try {
|
|
235
308
|
const protocol = new URL(url).protocol;
|
|
@@ -368,6 +441,40 @@ async function extractTarballWithSystemTar(archivePath, targetDir) {
|
|
|
368
441
|
});
|
|
369
442
|
}
|
|
370
443
|
|
|
444
|
+
function escapePowerShellLiteral(value) {
|
|
445
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function tryUnblockWindowsBinary(filePath, { logger, fsModule = fs, spawnSyncFn = spawnSync } = {}) {
|
|
449
|
+
if (process.platform !== "win32") return { ok: false, reason: "not_win32" };
|
|
450
|
+
if (!filePath) return { ok: false, reason: "missing_path" };
|
|
451
|
+
const existsSync = typeof fsModule.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
452
|
+
if (existsSync && !existsSync(filePath)) return { ok: false, reason: "missing_file" };
|
|
453
|
+
let unblocked = false;
|
|
454
|
+
try {
|
|
455
|
+
const zonePath = `${filePath}:Zone.Identifier`;
|
|
456
|
+
if (typeof fsModule.rmSync === "function") {
|
|
457
|
+
fsModule.rmSync(zonePath, { force: true });
|
|
458
|
+
unblocked = true;
|
|
459
|
+
} else if (typeof fsModule.unlinkSync === "function") {
|
|
460
|
+
try {
|
|
461
|
+
fsModule.unlinkSync(zonePath);
|
|
462
|
+
unblocked = true;
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
} catch {}
|
|
466
|
+
try {
|
|
467
|
+
const result = spawnSyncFn("powershell.exe", [
|
|
468
|
+
"-NoProfile",
|
|
469
|
+
"-Command",
|
|
470
|
+
`Unblock-File -LiteralPath ${escapePowerShellLiteral(filePath)}`
|
|
471
|
+
], { stdio: "ignore" });
|
|
472
|
+
if (result?.status === 0) unblocked = true;
|
|
473
|
+
} catch {}
|
|
474
|
+
if (unblocked) return { ok: true, reason: "unblocked" };
|
|
475
|
+
return { ok: false, reason: "noop" };
|
|
476
|
+
}
|
|
477
|
+
|
|
371
478
|
async function sha256File(filePath) {
|
|
372
479
|
return new Promise((resolve, reject) => {
|
|
373
480
|
const hash = crypto.createHash("sha256");
|
|
@@ -386,6 +493,38 @@ function nowIso() {
|
|
|
386
493
|
return new Date().toISOString();
|
|
387
494
|
}
|
|
388
495
|
|
|
496
|
+
function sleep(ms) {
|
|
497
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isRetryableFsError(err) {
|
|
501
|
+
if (!err || typeof err !== "object") return false;
|
|
502
|
+
const code = err.code;
|
|
503
|
+
return code === "EPERM" || code === "EACCES" || code === "EBUSY" || code === "ENOTEMPTY";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function withWindowsRetry(fn, { attempts = 6, delayMs = 50 } = {}) {
|
|
507
|
+
if (process.platform !== "win32") {
|
|
508
|
+
return fn();
|
|
509
|
+
}
|
|
510
|
+
let lastErr = null;
|
|
511
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
512
|
+
try {
|
|
513
|
+
return await fn();
|
|
514
|
+
} catch (err) {
|
|
515
|
+
lastErr = err;
|
|
516
|
+
if (!isRetryableFsError(err)) throw err;
|
|
517
|
+
const waitMs = delayMs * Math.pow(2, attempt);
|
|
518
|
+
await sleep(waitMs);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
throw lastErr;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function renameWithRetry(fsModule, fromPath, toPath) {
|
|
525
|
+
return withWindowsRetry(() => fsModule.promises.rename(fromPath, toPath));
|
|
526
|
+
}
|
|
527
|
+
|
|
389
528
|
async function readJsonFileIfPossible({ fsModule, filePath }) {
|
|
390
529
|
if (!fsModule?.promises?.readFile) {
|
|
391
530
|
return { value: null, error: "readFile_unavailable", errorCode: "READFILE_UNAVAILABLE" };
|
|
@@ -412,7 +551,7 @@ async function writeJsonFileAtomic({ fsModule, pathModule, filePath, value }) {
|
|
|
412
551
|
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
413
552
|
const payload = `${JSON.stringify(value, null, 2)}\n`;
|
|
414
553
|
await fsModule.promises.writeFile(tmp, payload, "utf8");
|
|
415
|
-
await fsModule
|
|
554
|
+
await renameWithRetry(fsModule, tmp, filePath);
|
|
416
555
|
}
|
|
417
556
|
|
|
418
557
|
function isValidInstallMetadata(meta) {
|
|
@@ -570,7 +709,7 @@ async function installFromLocalBinary({
|
|
|
570
709
|
writeJsonFileAtomicFn,
|
|
571
710
|
logger
|
|
572
711
|
}) {
|
|
573
|
-
await fsModule.promises.rm(distDir, { recursive: true, force: true });
|
|
712
|
+
await withWindowsRetry(() => fsModule.promises.rm(distDir, { recursive: true, force: true }));
|
|
574
713
|
await fsModule.promises.mkdir(distDir, { recursive: true });
|
|
575
714
|
const filename = isWin32 ? "docdexd.exe" : "docdexd";
|
|
576
715
|
const destPath = pathModule.join(distDir, filename);
|
|
@@ -688,7 +827,7 @@ function failedDirName({ distDir, nonce }) {
|
|
|
688
827
|
async function removeDirSafe(fsModule, dirPath) {
|
|
689
828
|
if (!dirPath) return false;
|
|
690
829
|
try {
|
|
691
|
-
await fsModule.promises.rm(dirPath, { recursive: true, force: true });
|
|
830
|
+
await withWindowsRetry(() => fsModule.promises.rm(dirPath, { recursive: true, force: true }));
|
|
692
831
|
return true;
|
|
693
832
|
} catch {
|
|
694
833
|
return false;
|
|
@@ -830,7 +969,7 @@ async function recoverInterruptedInstall({ fsModule, pathModule, distDir, isWin3
|
|
|
830
969
|
const candidate = await selectLatestCandidate(fsModule, backups);
|
|
831
970
|
if (candidate) {
|
|
832
971
|
try {
|
|
833
|
-
await fsModule
|
|
972
|
+
await renameWithRetry(fsModule, candidate.path, distDir);
|
|
834
973
|
recoveredFrom = candidate.path;
|
|
835
974
|
action = "recovered";
|
|
836
975
|
} catch (err) {
|
|
@@ -1954,19 +2093,24 @@ async function runInstaller(options) {
|
|
|
1954
2093
|
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1955
2094
|
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1956
2095
|
fallbackAttempted: source === "fallback",
|
|
1957
|
-
binaryPath: stagedBinaryPath
|
|
2096
|
+
binaryPath: stagedBinaryPath,
|
|
2097
|
+
hint: isWin32 ? "possible_av_quarantine" : null
|
|
1958
2098
|
});
|
|
1959
2099
|
}
|
|
1960
2100
|
|
|
1961
|
-
|
|
2101
|
+
if (isWin32) {
|
|
2102
|
+
tryUnblockWindowsBinary(stagedBinaryPath, { logger, fsModule });
|
|
2103
|
+
} else {
|
|
2104
|
+
await fsModule.promises.chmod(stagedBinaryPath, 0o755);
|
|
2105
|
+
}
|
|
1962
2106
|
|
|
1963
2107
|
if (existsSync && existsSync(distDir)) {
|
|
1964
2108
|
await fsModule.promises.rm(backupDir, { recursive: true, force: true }).catch(() => {});
|
|
1965
|
-
await fsModule
|
|
2109
|
+
await renameWithRetry(fsModule, distDir, backupDir);
|
|
1966
2110
|
backupMoved = true;
|
|
1967
2111
|
}
|
|
1968
2112
|
|
|
1969
|
-
await fsModule
|
|
2113
|
+
await renameWithRetry(fsModule, stagingDir, distDir);
|
|
1970
2114
|
promoted = true;
|
|
1971
2115
|
|
|
1972
2116
|
if (typeof restartFn === "function") {
|
|
@@ -1974,6 +2118,9 @@ async function runInstaller(options) {
|
|
|
1974
2118
|
}
|
|
1975
2119
|
|
|
1976
2120
|
const binaryPath = pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd");
|
|
2121
|
+
if (isWin32) {
|
|
2122
|
+
tryUnblockWindowsBinary(binaryPath, { logger, fsModule });
|
|
2123
|
+
}
|
|
1977
2124
|
const binarySha256 = await sha256FileFn(binaryPath);
|
|
1978
2125
|
const metadata = {
|
|
1979
2126
|
schemaVersion: INSTALL_METADATA_SCHEMA_VERSION,
|
|
@@ -2033,7 +2180,7 @@ async function runInstaller(options) {
|
|
|
2033
2180
|
if (existsSync(distDir)) {
|
|
2034
2181
|
await fsModule.promises.rm(distDir, { recursive: true, force: true });
|
|
2035
2182
|
}
|
|
2036
|
-
await fsModule
|
|
2183
|
+
await renameWithRetry(fsModule, backupDir, distDir);
|
|
2037
2184
|
rollbackStatus = "restored previous installation";
|
|
2038
2185
|
rollbackSucceeded = true;
|
|
2039
2186
|
} else if (promoted && !backupMoved) {
|
|
@@ -2091,12 +2238,30 @@ async function runInstaller(options) {
|
|
|
2091
2238
|
}
|
|
2092
2239
|
|
|
2093
2240
|
async function main() {
|
|
2094
|
-
const
|
|
2241
|
+
const env = process.env;
|
|
2242
|
+
const distBaseCandidates = resolveDistBaseCandidates({ env });
|
|
2243
|
+
const distBaseDir = resolveDistBaseDir({ env, fsModule: fs });
|
|
2095
2244
|
try {
|
|
2096
|
-
|
|
2245
|
+
ensureCliWrappers({ logger: console });
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
console.warn(`[docdex] CLI wrapper check failed: ${err?.message || err}`);
|
|
2248
|
+
}
|
|
2249
|
+
if (
|
|
2250
|
+
process.platform === "win32" &&
|
|
2251
|
+
!env?.DOCDEX_DIST_DIR &&
|
|
2252
|
+
distBaseCandidates[0] &&
|
|
2253
|
+
distBaseDir !== distBaseCandidates[0]
|
|
2254
|
+
) {
|
|
2255
|
+
console.warn(
|
|
2256
|
+
`[docdex] LOCALAPPDATA not writable; using fallback dist dir: ${distBaseDir}. Set DOCDEX_DIST_DIR to override.`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
const result = await runInstaller({ env, distBaseDir });
|
|
2260
|
+
try {
|
|
2261
|
+
const skipDaemon = Boolean(env?.npm_lifecycle_event);
|
|
2262
|
+
await runPostInstallSetup({ binaryPath: result?.binaryPath, env, skipDaemon, distBaseDir });
|
|
2097
2263
|
} catch (err) {
|
|
2098
2264
|
console.warn(`[docdex] postinstall setup failed: ${err?.message || err}`);
|
|
2099
|
-
throw err;
|
|
2100
2265
|
}
|
|
2101
2266
|
try {
|
|
2102
2267
|
writeAgentInstructions();
|
|
@@ -2134,6 +2299,11 @@ function printPostInstallBanner() {
|
|
|
2134
2299
|
"\x1b[33mSetup:\x1b[0m configures Ollama/models + browser.",
|
|
2135
2300
|
"\x1b[34mTip:\x1b[0m after setup, the daemon should auto-start; if not, run \x1b[36m`docdexd daemon`\x1b[0m"
|
|
2136
2301
|
];
|
|
2302
|
+
if (process.platform === "win32") {
|
|
2303
|
+
content.push(
|
|
2304
|
+
"\x1b[33mNote:\x1b[0m If PowerShell blocks `docdex`, run `docdex.cmd` or set ExecutionPolicy RemoteSigned."
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2137
2307
|
width = Math.max(72, content.reduce((max, line) => Math.max(max, stripAnsi(line).length), 0));
|
|
2138
2308
|
const padLine = (text) => {
|
|
2139
2309
|
const visible = stripAnsi(text).length;
|
|
@@ -2361,18 +2531,23 @@ function describeFatalError(err) {
|
|
|
2361
2531
|
}
|
|
2362
2532
|
|
|
2363
2533
|
if (err instanceof ArchiveInvalidError) {
|
|
2534
|
+
const lines = [
|
|
2535
|
+
`[docdex] install failed: ${err.message}`,
|
|
2536
|
+
`[docdex] error code: ${err.code}`,
|
|
2537
|
+
err.details?.binaryPath ? `[docdex] Expected binary path: ${err.details.binaryPath}` : null
|
|
2538
|
+
].filter(Boolean);
|
|
2539
|
+
if (process.platform === "win32" && err.details?.hint === "possible_av_quarantine") {
|
|
2540
|
+
lines.push(
|
|
2541
|
+
"[docdex] Windows Defender/AV may have quarantined the downloaded binary.",
|
|
2542
|
+
"[docdex] Re-run the install or add an exclusion for the Docdex dist directory.",
|
|
2543
|
+
"[docdex] Tip: set DOCDEX_DIST_DIR to a writable directory outside protected paths."
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2364
2546
|
return {
|
|
2365
2547
|
code: err.code,
|
|
2366
2548
|
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
2367
2549
|
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
|
-
)
|
|
2550
|
+
lines: appendInstallSafetyLines(lines, err)
|
|
2376
2551
|
};
|
|
2377
2552
|
}
|
|
2378
2553
|
|
|
@@ -2414,6 +2589,25 @@ function describeFatalError(err) {
|
|
|
2414
2589
|
}
|
|
2415
2590
|
|
|
2416
2591
|
const code = (err && typeof err.code === "string" && err.code) || "DOCDEX_INSTALL_FAILED";
|
|
2592
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
2593
|
+
const location = err?.path ? ` (${err.path})` : "";
|
|
2594
|
+
return {
|
|
2595
|
+
code,
|
|
2596
|
+
exitCode: EXIT_CODE_BY_ERROR_CODE[code] || 1,
|
|
2597
|
+
details: withBaseDetails(err && err.details),
|
|
2598
|
+
lines: appendInstallSafetyLines(
|
|
2599
|
+
[
|
|
2600
|
+
`[docdex] install failed: ${err?.message || "permission denied"}`,
|
|
2601
|
+
`[docdex] error code: ${code}`,
|
|
2602
|
+
`[docdex] Ensure write access${location} or set DOCDEX_DIST_DIR to a writable location.`,
|
|
2603
|
+
process.platform === "win32"
|
|
2604
|
+
? "[docdex] On Windows, run in an elevated shell if needed."
|
|
2605
|
+
: null
|
|
2606
|
+
].filter(Boolean),
|
|
2607
|
+
err
|
|
2608
|
+
)
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2417
2611
|
return {
|
|
2418
2612
|
code,
|
|
2419
2613
|
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;
|
|
@@ -1391,12 +1397,38 @@ function clientInstructionPaths() {
|
|
|
1391
1397
|
}
|
|
1392
1398
|
}
|
|
1393
1399
|
|
|
1394
|
-
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 } = {}) {
|
|
1395
1406
|
if (binaryPath && fs.existsSync(binaryPath)) return binaryPath;
|
|
1396
1407
|
try {
|
|
1397
1408
|
const platformKey = detectPlatformKey();
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1409
|
+
const isWin32 = process.platform === "win32";
|
|
1410
|
+
const resolvedVersion = sanitizeVersionForFilename(
|
|
1411
|
+
normalizeVersion(resolvePackageVersion())
|
|
1412
|
+
);
|
|
1413
|
+
const resolvedDistBaseDir = distBaseDir || resolveDistBaseDir({ env, fsModule: fs });
|
|
1414
|
+
const binDir = resolveBinDir({ env, distBaseDir: resolvedDistBaseDir });
|
|
1415
|
+
const baseCandidates = distBaseCandidates || resolveDistBaseCandidates({ env });
|
|
1416
|
+
const candidates = [];
|
|
1417
|
+
if (binDir) {
|
|
1418
|
+
if (isWin32 && resolvedVersion) {
|
|
1419
|
+
candidates.push(path.join(binDir, `docdexd-${resolvedVersion}.exe`));
|
|
1420
|
+
}
|
|
1421
|
+
candidates.push(path.join(binDir, isWin32 ? "docdexd.exe" : "docdexd"));
|
|
1422
|
+
}
|
|
1423
|
+
for (const base of baseCandidates) {
|
|
1424
|
+
candidates.push(path.join(base, platformKey, isWin32 ? "docdexd.exe" : "docdexd"));
|
|
1425
|
+
}
|
|
1426
|
+
candidates.push(
|
|
1427
|
+
path.join(__dirname, "..", "dist", platformKey, isWin32 ? "docdexd.exe" : "docdexd")
|
|
1428
|
+
);
|
|
1429
|
+
for (const candidate of candidates) {
|
|
1430
|
+
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
1431
|
+
}
|
|
1400
1432
|
} catch (err) {
|
|
1401
1433
|
if (!(err instanceof UnsupportedPlatformError)) throw err;
|
|
1402
1434
|
}
|
|
@@ -1416,16 +1448,26 @@ function isMacProtectedPath(candidate) {
|
|
|
1416
1448
|
return ["Desktop", "Documents", "Downloads"].some((dir) => isPathWithin(path.join(home, dir), candidate));
|
|
1417
1449
|
}
|
|
1418
1450
|
|
|
1419
|
-
function ensureStartupBinary(binaryPath, { logger } = {}) {
|
|
1451
|
+
function ensureStartupBinary(binaryPath, { logger, env, distBaseDir } = {}) {
|
|
1420
1452
|
if (!binaryPath) return null;
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1453
|
+
const isWin32 = process.platform === "win32";
|
|
1454
|
+
const mustCopy = isWin32 || isMacProtectedPath(binaryPath) || isTempPath(binaryPath);
|
|
1455
|
+
if (!mustCopy) return binaryPath;
|
|
1456
|
+
const binDir = resolveBinDir({ env, distBaseDir });
|
|
1457
|
+
const resolvedVersion = sanitizeVersionForFilename(
|
|
1458
|
+
normalizeVersion(resolvePackageVersion())
|
|
1459
|
+
);
|
|
1460
|
+
const targetName = isWin32 && resolvedVersion
|
|
1461
|
+
? `docdexd-${resolvedVersion}.exe`
|
|
1462
|
+
: path.basename(binaryPath);
|
|
1463
|
+
const target = path.join(binDir, targetName);
|
|
1424
1464
|
if (fs.existsSync(target)) return target;
|
|
1425
1465
|
try {
|
|
1426
1466
|
fs.mkdirSync(binDir, { recursive: true });
|
|
1427
1467
|
fs.copyFileSync(binaryPath, target);
|
|
1428
|
-
|
|
1468
|
+
if (!isWin32) {
|
|
1469
|
+
fs.chmodSync(target, 0o755);
|
|
1470
|
+
}
|
|
1429
1471
|
return target;
|
|
1430
1472
|
} catch (err) {
|
|
1431
1473
|
logger?.warn?.(`[docdex] failed to stage daemon binary for startup: ${err?.message || err}`);
|
|
@@ -1433,8 +1475,8 @@ function ensureStartupBinary(binaryPath, { logger } = {}) {
|
|
|
1433
1475
|
}
|
|
1434
1476
|
}
|
|
1435
1477
|
|
|
1436
|
-
function resolveStartupBinaryPaths({ binaryPath, logger } = {}) {
|
|
1437
|
-
const resolvedBinary = ensureStartupBinary(binaryPath, { logger });
|
|
1478
|
+
function resolveStartupBinaryPaths({ binaryPath, logger, env, distBaseDir } = {}) {
|
|
1479
|
+
const resolvedBinary = ensureStartupBinary(binaryPath, { logger, env, distBaseDir });
|
|
1438
1480
|
return { binaryPath: resolvedBinary };
|
|
1439
1481
|
}
|
|
1440
1482
|
|
|
@@ -2112,7 +2154,32 @@ function buildDaemonEnv() {
|
|
|
2112
2154
|
return Object.fromEntries(buildDaemonEnvPairs());
|
|
2113
2155
|
}
|
|
2114
2156
|
|
|
2115
|
-
function
|
|
2157
|
+
function escapeCmdArg(value) {
|
|
2158
|
+
return `"${String(value).replace(/"/g, "\"\"")}"`;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
function writeWindowsRunner({ binaryPath, args, envPairs, workingDir, logger, distBaseDir } = {}) {
|
|
2162
|
+
const runnerPath = resolveWindowsRunnerPath({ distBaseDir });
|
|
2163
|
+
const lines = ["@echo off", "setlocal"];
|
|
2164
|
+
for (const [key, value] of envPairs || []) {
|
|
2165
|
+
lines.push(`set "${key}=${value}"`);
|
|
2166
|
+
}
|
|
2167
|
+
if (workingDir) {
|
|
2168
|
+
lines.push(`cd /d ${escapeCmdArg(workingDir)}`);
|
|
2169
|
+
}
|
|
2170
|
+
const argString = (args || []).map((arg) => escapeCmdArg(arg)).join(" ");
|
|
2171
|
+
lines.push(`${escapeCmdArg(binaryPath)} ${argString}`.trim());
|
|
2172
|
+
try {
|
|
2173
|
+
fs.mkdirSync(path.dirname(runnerPath), { recursive: true });
|
|
2174
|
+
fs.writeFileSync(runnerPath, `${lines.join("\r\n")}\r\n`);
|
|
2175
|
+
return runnerPath;
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
logger?.warn?.(`[docdex] failed to write Windows runner: ${err?.message || err}`);
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function registerStartup({ binaryPath, port, repoRoot, logger, distBaseDir }) {
|
|
2116
2183
|
if (!binaryPath) return { ok: false, reason: "missing_binary" };
|
|
2117
2184
|
stopDaemonService({ logger });
|
|
2118
2185
|
const envPairs = buildDaemonEnvPairs();
|
|
@@ -2208,11 +2275,16 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
|
|
|
2208
2275
|
|
|
2209
2276
|
if (process.platform === "win32") {
|
|
2210
2277
|
const taskName = "Docdex Daemon";
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2278
|
+
const runnerPath = writeWindowsRunner({
|
|
2279
|
+
binaryPath,
|
|
2280
|
+
args,
|
|
2281
|
+
envPairs,
|
|
2282
|
+
workingDir,
|
|
2283
|
+
logger,
|
|
2284
|
+
distBaseDir
|
|
2285
|
+
});
|
|
2286
|
+
if (!runnerPath) return { ok: false, reason: "runner_failed" };
|
|
2287
|
+
const taskArgs = `cmd.exe /c ${escapeCmdArg(runnerPath)}`;
|
|
2216
2288
|
const create = spawnSync("schtasks", [
|
|
2217
2289
|
"/Create",
|
|
2218
2290
|
"/F",
|
|
@@ -2236,12 +2308,13 @@ function registerStartup({ binaryPath, port, repoRoot, logger }) {
|
|
|
2236
2308
|
return { ok: false, reason: "unsupported_platform" };
|
|
2237
2309
|
}
|
|
2238
2310
|
|
|
2239
|
-
async function startDaemonWithHealthCheck({ binaryPath, port, host, logger }) {
|
|
2311
|
+
async function startDaemonWithHealthCheck({ binaryPath, port, host, logger, distBaseDir }) {
|
|
2240
2312
|
const startup = registerStartup({
|
|
2241
2313
|
binaryPath,
|
|
2242
2314
|
port,
|
|
2243
2315
|
repoRoot: daemonRootPath(),
|
|
2244
|
-
logger
|
|
2316
|
+
logger,
|
|
2317
|
+
distBaseDir
|
|
2245
2318
|
});
|
|
2246
2319
|
if (!startup.ok) {
|
|
2247
2320
|
logger?.warn?.(`[docdex] daemon service registration failed (${startup.reason || "unknown"}).`);
|
|
@@ -2281,6 +2354,17 @@ function startupFailureReported() {
|
|
|
2281
2354
|
return fs.existsSync(path.join(stateDir(), STARTUP_FAILURE_MARKER));
|
|
2282
2355
|
}
|
|
2283
2356
|
|
|
2357
|
+
function isNpmLifecycle(env = process.env) {
|
|
2358
|
+
return Boolean(env?.npm_lifecycle_event);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function shouldSkipDaemonSideEffects({ env = process.env, skipDaemon } = {}) {
|
|
2362
|
+
if (skipDaemon) return true;
|
|
2363
|
+
if (isNpmLifecycle(env)) return true;
|
|
2364
|
+
if (parseEnvBool(env?.DOCDEX_DAEMON_SKIP_SETUP)) return true;
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2284
2368
|
function shouldSkipSetup(env = process.env) {
|
|
2285
2369
|
return parseEnvBool(env.DOCDEX_SETUP_SKIP) === true;
|
|
2286
2370
|
}
|
|
@@ -2381,34 +2465,49 @@ function launchSetupWizard({
|
|
|
2381
2465
|
return { ok: false, reason: "unsupported_platform" };
|
|
2382
2466
|
}
|
|
2383
2467
|
|
|
2384
|
-
async function runPostInstallSetup({ binaryPath, logger } = {}) {
|
|
2468
|
+
async function runPostInstallSetup({ binaryPath, logger, env, skipDaemon, distBaseDir } = {}) {
|
|
2385
2469
|
const log = logger || console;
|
|
2470
|
+
const effectiveEnv = env || process.env;
|
|
2471
|
+
const distCandidates = resolveDistBaseCandidates({ env: effectiveEnv });
|
|
2472
|
+
const resolvedDistBaseDir = distBaseDir || resolveDistBaseDir({ env: effectiveEnv, fsModule: fs });
|
|
2473
|
+
let allowDaemon = !shouldSkipDaemonSideEffects({ env: effectiveEnv, skipDaemon });
|
|
2386
2474
|
const configPath = defaultConfigPath();
|
|
2387
2475
|
let existingConfig = "";
|
|
2388
2476
|
if (fs.existsSync(configPath)) {
|
|
2389
2477
|
existingConfig = fs.readFileSync(configPath, "utf8");
|
|
2390
2478
|
}
|
|
2391
2479
|
const port = DEFAULT_DAEMON_PORT;
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2480
|
+
let portState = { available: true, reuseExisting: false };
|
|
2481
|
+
if (allowDaemon) {
|
|
2482
|
+
portState = await resolveDaemonPortState({
|
|
2483
|
+
host: DEFAULT_HOST,
|
|
2484
|
+
port,
|
|
2485
|
+
logger: log
|
|
2486
|
+
});
|
|
2487
|
+
if (!portState.available && !portState.reuseExisting) {
|
|
2488
|
+
log.warn?.(
|
|
2489
|
+
`[docdex] ${DEFAULT_HOST}:${port} is already in use; skipping daemon startup. Run \`docdexd daemon\` after freeing the port.`
|
|
2490
|
+
);
|
|
2491
|
+
recordStartupFailure({ reason: "port_in_use", host: DEFAULT_HOST, port });
|
|
2492
|
+
allowDaemon = false;
|
|
2493
|
+
}
|
|
2402
2494
|
}
|
|
2403
2495
|
|
|
2404
2496
|
const daemonRoot = ensureDaemonRoot();
|
|
2405
|
-
const resolvedBinary = resolveBinaryPath({
|
|
2497
|
+
const resolvedBinary = resolveBinaryPath({
|
|
2498
|
+
binaryPath,
|
|
2499
|
+
env: effectiveEnv,
|
|
2500
|
+
distBaseDir: resolvedDistBaseDir,
|
|
2501
|
+
distBaseCandidates: distCandidates
|
|
2502
|
+
});
|
|
2406
2503
|
const startupBinaries = resolveStartupBinaryPaths({
|
|
2407
2504
|
binaryPath: resolvedBinary,
|
|
2408
|
-
logger: log
|
|
2505
|
+
logger: log,
|
|
2506
|
+
env: effectiveEnv,
|
|
2507
|
+
distBaseDir: resolvedDistBaseDir
|
|
2409
2508
|
});
|
|
2410
|
-
let reuseExisting = portState.reuseExisting;
|
|
2411
|
-
if (reuseExisting) {
|
|
2509
|
+
let reuseExisting = allowDaemon ? portState.reuseExisting : false;
|
|
2510
|
+
if (reuseExisting && allowDaemon) {
|
|
2412
2511
|
const daemonInfo = await fetchDaemonInfo({ host: DEFAULT_HOST, port });
|
|
2413
2512
|
const daemonVersion = normalizeVersion(daemonInfo?.version);
|
|
2414
2513
|
const packageVersion = normalizeVersion(resolvePackageVersion());
|
|
@@ -2426,22 +2525,29 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
|
|
|
2426
2525
|
port
|
|
2427
2526
|
});
|
|
2428
2527
|
if (!released) {
|
|
2429
|
-
|
|
2528
|
+
log.warn?.("[docdex] daemon restart failed; port still in use.");
|
|
2529
|
+
recordStartupFailure({ reason: "restart_failed", host: DEFAULT_HOST, port });
|
|
2530
|
+
allowDaemon = false;
|
|
2430
2531
|
}
|
|
2431
2532
|
reuseExisting = false;
|
|
2432
2533
|
}
|
|
2433
2534
|
}
|
|
2434
2535
|
}
|
|
2435
|
-
|
|
2536
|
+
let startupOk = reuseExisting;
|
|
2537
|
+
if (allowDaemon && !reuseExisting) {
|
|
2436
2538
|
const result = await startDaemonWithHealthCheck({
|
|
2437
2539
|
binaryPath: startupBinaries.binaryPath,
|
|
2438
2540
|
port,
|
|
2439
2541
|
host: DEFAULT_HOST,
|
|
2440
|
-
logger: log
|
|
2542
|
+
logger: log,
|
|
2543
|
+
distBaseDir: resolvedDistBaseDir
|
|
2441
2544
|
});
|
|
2442
2545
|
if (!result.ok) {
|
|
2443
2546
|
log.warn?.(`[docdex] daemon failed to start on ${DEFAULT_HOST}:${port}.`);
|
|
2444
|
-
|
|
2547
|
+
recordStartupFailure({ reason: result.reason || "startup_failed", host: DEFAULT_HOST, port });
|
|
2548
|
+
allowDaemon = false;
|
|
2549
|
+
} else {
|
|
2550
|
+
startupOk = true;
|
|
2445
2551
|
}
|
|
2446
2552
|
}
|
|
2447
2553
|
|
|
@@ -2475,8 +2581,13 @@ async function runPostInstallSetup({ binaryPath, logger } = {}) {
|
|
|
2475
2581
|
}
|
|
2476
2582
|
upsertCodexConfig(paths.codex, codexUrl);
|
|
2477
2583
|
applyAgentInstructions({ logger: log });
|
|
2478
|
-
|
|
2479
|
-
|
|
2584
|
+
if (startupOk) {
|
|
2585
|
+
clearStartupFailure();
|
|
2586
|
+
}
|
|
2587
|
+
const skipWizard = isNpmLifecycle(effectiveEnv) || shouldSkipSetup(effectiveEnv);
|
|
2588
|
+
const setupLaunch = skipWizard
|
|
2589
|
+
? { ok: false, reason: "skipped" }
|
|
2590
|
+
: launchSetupWizard({ binaryPath: startupBinaries.binaryPath, logger: log });
|
|
2480
2591
|
if (!setupLaunch.ok && setupLaunch.reason !== "skipped") {
|
|
2481
2592
|
log.warn?.("[docdex] setup wizard did not launch. Run `docdex setup`.");
|
|
2482
2593
|
recordSetupPending({ reason: setupLaunch.reason, port, repoRoot: daemonRoot });
|
package/lib/uninstall.js
CHANGED
|
@@ -6,6 +6,7 @@ const net = require("node:net");
|
|
|
6
6
|
const os = require("node:os");
|
|
7
7
|
const path = require("node:path");
|
|
8
8
|
const { spawnSync } = require("node:child_process");
|
|
9
|
+
const { resolveDocdexDataDir } = require("./paths");
|
|
9
10
|
|
|
10
11
|
const DAEMON_TASK_NAME = "Docdex Daemon";
|
|
11
12
|
const STARTUP_FAILURE_MARKER = "startup_registration_failed.json";
|
|
@@ -24,6 +25,10 @@ function daemonRootPath() {
|
|
|
24
25
|
return path.join(docdexRootPath(), "daemon_root");
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function docdexDataDir() {
|
|
29
|
+
return resolveDocdexDataDir({ env: process.env });
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
function stateDir() {
|
|
28
33
|
return path.join(docdexRootPath(), "state");
|
|
29
34
|
}
|
|
@@ -108,12 +113,15 @@ function manualStopInstructions() {
|
|
|
108
113
|
];
|
|
109
114
|
}
|
|
110
115
|
if (process.platform === "win32") {
|
|
116
|
+
const dataDir = docdexDataDir();
|
|
111
117
|
return [
|
|
112
118
|
"Manual cleanup required:",
|
|
113
119
|
`- schtasks /End /TN "${DAEMON_TASK_NAME}"`,
|
|
114
120
|
"- schtasks /Delete /TN \"Docdex Daemon\" /F",
|
|
115
121
|
"- taskkill /IM docdexd.exe /T /F",
|
|
116
122
|
"- del %USERPROFILE%\\.docdex\\locks\\daemon.lock",
|
|
123
|
+
`- del "${path.join(dataDir, "run-daemon.cmd")}"`,
|
|
124
|
+
`- rmdir /S /Q "${dataDir}"`,
|
|
117
125
|
"- netstat -ano | findstr 28491",
|
|
118
126
|
];
|
|
119
127
|
}
|