codex-overleaf-link 1.1.1
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/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/codex-overleaf-link.mjs +223 -0
- package/extension/src/shared/agentTranscript.js +1175 -0
- package/extension/src/shared/auditRecords.js +568 -0
- package/extension/src/shared/compatibility.js +372 -0
- package/extension/src/shared/compileAdapter.js +176 -0
- package/extension/src/shared/governanceRules.js +252 -0
- package/extension/src/shared/i18n.js +565 -0
- package/extension/src/shared/models.js +106 -0
- package/extension/src/shared/otText.js +505 -0
- package/extension/src/shared/projectFiles.js +180 -0
- package/extension/src/shared/reviewing.js +99 -0
- package/extension/src/shared/sensitiveScan.js +116 -0
- package/extension/src/shared/sessionState.js +1084 -0
- package/extension/src/shared/staleGuard.js +150 -0
- package/extension/src/shared/storageDb.js +986 -0
- package/extension/src/shared/storageKeys.js +29 -0
- package/extension/src/shared/storageMigration.js +168 -0
- package/extension/src/shared/summary.js +248 -0
- package/extension/src/shared/undoOperations.js +369 -0
- package/native-host/src/codexArgs.js +43 -0
- package/native-host/src/codexHome.js +538 -0
- package/native-host/src/codexModels.js +247 -0
- package/native-host/src/codexPrompt.js +192 -0
- package/native-host/src/codexPromptAssembly.js +411 -0
- package/native-host/src/codexSessionRunner.js +1247 -0
- package/native-host/src/commandApproval.js +914 -0
- package/native-host/src/debugLog.js +78 -0
- package/native-host/src/diffEngine.js +247 -0
- package/native-host/src/index.js +132 -0
- package/native-host/src/launcher.js +81 -0
- package/native-host/src/localSkills.js +476 -0
- package/native-host/src/manifest.js +226 -0
- package/native-host/src/mirrorSensitiveScan.js +119 -0
- package/native-host/src/mirrorWorkspace.js +1019 -0
- package/native-host/src/nativeDoctor.js +826 -0
- package/native-host/src/nativeEnvironment.js +315 -0
- package/native-host/src/nativeHostPlatform.js +112 -0
- package/native-host/src/nativeMessaging.js +60 -0
- package/native-host/src/nativeQuotas.js +294 -0
- package/native-host/src/nativeResponseBudget.js +194 -0
- package/native-host/src/runtimeInstaller.js +357 -0
- package/native-host/src/taskRunner.js +3 -0
- package/native-host/src/taskRunnerRuntime.js +1083 -0
- package/native-host/src/textPatch.js +287 -0
- package/package.json +40 -0
- package/scripts/codex-json-agent.mjs +269 -0
- package/scripts/install-native-host.mjs +255 -0
- package/scripts/npm-package-files-v1.1.1.txt +52 -0
- package/scripts/uninstall-native-host.mjs +298 -0
- package/scripts/verify-npm-package.mjs +296 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawn } = require('node:child_process');
|
|
7
|
+
const { HOST_NAME, validateChromeExtensionId } = require('./manifest');
|
|
8
|
+
const {
|
|
9
|
+
getHomeDir,
|
|
10
|
+
getNativeHostPlatform,
|
|
11
|
+
getNativeHostRegistrationTarget
|
|
12
|
+
} = require('./nativeHostPlatform');
|
|
13
|
+
const { decodeFrames, encodeMessage } = require('./nativeMessaging');
|
|
14
|
+
const {
|
|
15
|
+
buildBridgePingParams,
|
|
16
|
+
evaluateNativeCompatibility
|
|
17
|
+
} = require('../../extension/src/shared/compatibility');
|
|
18
|
+
const { version: PACKAGE_VERSION } = require('../../package.json');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_DOCTOR_TIMEOUT_MS = 3000;
|
|
21
|
+
const DOCTOR_PING_ID = 'doctor-ping';
|
|
22
|
+
const MAX_DOCTOR_STDERR_BYTES = 64 * 1024;
|
|
23
|
+
const MAX_DOCTOR_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
async function runDoctor(options = {}) {
|
|
26
|
+
const platform = getNativeHostPlatform(options);
|
|
27
|
+
const browser = normalizeBrowser(options.browser || 'auto');
|
|
28
|
+
const env = options.env || process.env;
|
|
29
|
+
const homeDir = options.homeDir || getHomeDir({ ...options, env, platform });
|
|
30
|
+
const pathOptions = {
|
|
31
|
+
revealPaths: Boolean(options.revealPaths),
|
|
32
|
+
homeDir,
|
|
33
|
+
platform
|
|
34
|
+
};
|
|
35
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
36
|
+
const registrationTarget = getNativeHostRegistrationTarget({
|
|
37
|
+
...options,
|
|
38
|
+
browser,
|
|
39
|
+
env,
|
|
40
|
+
homeDir,
|
|
41
|
+
platform
|
|
42
|
+
});
|
|
43
|
+
const runtimeRoot = options.runtimeRoot
|
|
44
|
+
? normalizeDiagnosticPath(resolveRuntimeRoot(options.runtimeRoot), pathOptions)
|
|
45
|
+
: undefined;
|
|
46
|
+
const body = {
|
|
47
|
+
ok: false,
|
|
48
|
+
status: 'unknown',
|
|
49
|
+
browser: registrationTarget.browser,
|
|
50
|
+
platform,
|
|
51
|
+
registration: sanitizeRegistrationTarget(registrationTarget, pathOptions),
|
|
52
|
+
manifest: {
|
|
53
|
+
path: normalizeDiagnosticPath(registrationTarget.manifestPath, pathOptions),
|
|
54
|
+
exists: false,
|
|
55
|
+
valid: false,
|
|
56
|
+
errors: []
|
|
57
|
+
},
|
|
58
|
+
bridge: null,
|
|
59
|
+
ping: {
|
|
60
|
+
attempted: false
|
|
61
|
+
},
|
|
62
|
+
compatibility: null,
|
|
63
|
+
diagnostics: {
|
|
64
|
+
browser: registrationTarget.browser,
|
|
65
|
+
registrationKind: registrationTarget.kind,
|
|
66
|
+
manifestPath: normalizeDiagnosticPath(registrationTarget.manifestPath, pathOptions)
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (runtimeRoot) {
|
|
71
|
+
body.runtimeRoot = runtimeRoot;
|
|
72
|
+
body.diagnostics.runtimeRoot = runtimeRoot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const manifestResult = readAndValidateManifest(registrationTarget.manifestPath, {
|
|
76
|
+
platform,
|
|
77
|
+
pathOptions
|
|
78
|
+
});
|
|
79
|
+
Object.assign(body.manifest, manifestResult.manifest);
|
|
80
|
+
|
|
81
|
+
if (!manifestResult.manifest.exists || !manifestResult.manifest.valid) {
|
|
82
|
+
return finalizeDoctorResult(body);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bridgePath = manifestResult.bridgePath;
|
|
86
|
+
body.bridge = validateBridgePath(bridgePath, { platform, pathOptions });
|
|
87
|
+
body.diagnostics.bridgePath = body.bridge.path;
|
|
88
|
+
body.diagnostics.allowedOrigins = body.manifest.allowedOrigins || [];
|
|
89
|
+
|
|
90
|
+
if (!body.bridge.valid || !body.bridge.exists) {
|
|
91
|
+
return finalizeDoctorResult(body);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pingResult = await pingNativeBridge(bridgePath, {
|
|
95
|
+
id: DOCTOR_PING_ID,
|
|
96
|
+
method: 'bridge.ping',
|
|
97
|
+
params: buildBridgePingParams({ version: PACKAGE_VERSION })
|
|
98
|
+
}, {
|
|
99
|
+
env,
|
|
100
|
+
pathOptions,
|
|
101
|
+
platform,
|
|
102
|
+
timeoutMs
|
|
103
|
+
});
|
|
104
|
+
const rawPingResponse = pingResult.rawResponse;
|
|
105
|
+
delete pingResult.rawResponse;
|
|
106
|
+
body.ping = pingResult;
|
|
107
|
+
|
|
108
|
+
if (body.ping.ok) {
|
|
109
|
+
body.compatibility = sanitizeCompatibility(
|
|
110
|
+
evaluateNativeCompatibility(rawPingResponse, { version: PACKAGE_VERSION })
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return finalizeDoctorResult(body);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function classifyDoctorResult(body = {}) {
|
|
118
|
+
if (!body.manifest || body.manifest.exists === false) {
|
|
119
|
+
return {
|
|
120
|
+
exitCode: 2,
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 'missing_install'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (body.manifest.valid === false) {
|
|
127
|
+
return {
|
|
128
|
+
exitCode: 2,
|
|
129
|
+
ok: false,
|
|
130
|
+
status: 'invalid_manifest'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!body.bridge || body.bridge.valid === false) {
|
|
135
|
+
return {
|
|
136
|
+
exitCode: 2,
|
|
137
|
+
ok: false,
|
|
138
|
+
status: 'invalid_bridge'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (body.bridge.exists === false) {
|
|
143
|
+
return {
|
|
144
|
+
exitCode: 2,
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 'missing_bridge'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!body.ping || body.ping.ok !== true) {
|
|
151
|
+
return {
|
|
152
|
+
exitCode: 3,
|
|
153
|
+
ok: false,
|
|
154
|
+
status: 'execution_failure'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (body.compatibility?.classification === 'compatible') {
|
|
159
|
+
return {
|
|
160
|
+
exitCode: 0,
|
|
161
|
+
ok: true,
|
|
162
|
+
status: 'ok'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (body.compatibility?.classification === 'update-available') {
|
|
167
|
+
return {
|
|
168
|
+
exitCode: 3,
|
|
169
|
+
ok: false,
|
|
170
|
+
status: 'native_stale'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
exitCode: 3,
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 'native_incompatible'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatDoctorHuman(body = {}) {
|
|
182
|
+
const lines = [
|
|
183
|
+
'Native host doctor',
|
|
184
|
+
`Status: ${body.status || 'unknown'}`,
|
|
185
|
+
`OK: ${body.ok === true ? 'yes' : 'no'}`,
|
|
186
|
+
`Browser: ${body.browser || 'unknown'}`,
|
|
187
|
+
`Manifest: ${body.manifest?.path || 'unknown'}`
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (body.registration?.kind) {
|
|
191
|
+
lines.push(`Registration: ${body.registration.kind}`);
|
|
192
|
+
}
|
|
193
|
+
if (body.registration?.registryKey) {
|
|
194
|
+
lines.push(`Registry key: ${body.registration.registryKey}`);
|
|
195
|
+
}
|
|
196
|
+
if (body.bridge?.path) {
|
|
197
|
+
lines.push(`Bridge: ${body.bridge.path}`);
|
|
198
|
+
}
|
|
199
|
+
if (body.runtimeRoot) {
|
|
200
|
+
lines.push(`Runtime root: ${body.runtimeRoot}`);
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(body.manifest?.allowedOrigins) && body.manifest.allowedOrigins.length) {
|
|
203
|
+
lines.push(`Allowed origins: ${body.manifest.allowedOrigins.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
if (body.compatibility) {
|
|
206
|
+
lines.push(`Native compatibility: ${body.compatibility.classification || 'unknown'}`);
|
|
207
|
+
lines.push(`Native version: ${body.compatibility.nativeVersion || 'unknown'}`);
|
|
208
|
+
}
|
|
209
|
+
if (body.ping?.ok === false) {
|
|
210
|
+
lines.push(`Ping: failed (${body.ping.error || 'unknown error'})`);
|
|
211
|
+
} else if (body.ping?.ok === true) {
|
|
212
|
+
lines.push('Ping: ok');
|
|
213
|
+
} else {
|
|
214
|
+
lines.push('Ping: not run');
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(body.manifest?.errors) && body.manifest.errors.length) {
|
|
217
|
+
lines.push(`Manifest errors: ${body.manifest.errors.join('; ')}`);
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(body.bridge?.errors) && body.bridge.errors.length) {
|
|
220
|
+
lines.push(`Bridge errors: ${body.bridge.errors.join('; ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `${lines.join('\n')}\n`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeDiagnosticPath(targetPath, options = {}) {
|
|
227
|
+
if (!targetPath) {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
const value = String(targetPath);
|
|
231
|
+
if (options.revealPaths) {
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const platform = options.platform || process.platform;
|
|
236
|
+
const platformPath = getPlatformPath(platform);
|
|
237
|
+
const homeDir = options.homeDir || getHomeFromEnv(options.env || process.env, platform);
|
|
238
|
+
const normalizedValue = platformPath.normalize(value);
|
|
239
|
+
const normalizedHome = homeDir ? platformPath.normalize(String(homeDir)) : '';
|
|
240
|
+
const comparisonValue = platform === 'win32' ? normalizedValue.toLowerCase() : normalizedValue;
|
|
241
|
+
const comparisonHome = platform === 'win32' ? normalizedHome.toLowerCase() : normalizedHome;
|
|
242
|
+
|
|
243
|
+
if (comparisonHome && comparisonValue === comparisonHome) {
|
|
244
|
+
return '~';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (comparisonHome && comparisonValue.startsWith(`${comparisonHome}${platformPath.sep}`)) {
|
|
248
|
+
return `~${platformPath.sep}${normalizedValue.slice(normalizedHome.length + 1)}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isAbsolutePathForPlatform(value, platform)) {
|
|
252
|
+
return `${platformPath.sep}<absolute-path>${platformPath.sep}${platformPath.basename(value)}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function finalizeDoctorResult(body) {
|
|
259
|
+
const classification = classifyDoctorResult(body);
|
|
260
|
+
body.ok = classification.ok;
|
|
261
|
+
body.status = classification.status;
|
|
262
|
+
return {
|
|
263
|
+
exitCode: classification.exitCode,
|
|
264
|
+
body
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function readAndValidateManifest(manifestPath, options) {
|
|
269
|
+
const manifest = {
|
|
270
|
+
path: normalizeDiagnosticPath(manifestPath, options.pathOptions),
|
|
271
|
+
exists: false,
|
|
272
|
+
valid: false,
|
|
273
|
+
errors: []
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let raw;
|
|
277
|
+
try {
|
|
278
|
+
raw = fs.readFileSync(manifestPath, 'utf8');
|
|
279
|
+
manifest.exists = true;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error?.code !== 'ENOENT') {
|
|
282
|
+
manifest.errors.push(`Unable to read manifest: ${sanitizePathInMessage(error.message, manifestPath, options.pathOptions)}`);
|
|
283
|
+
}
|
|
284
|
+
return { manifest, bridgePath: '' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(raw);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
manifest.errors.push(`Invalid manifest JSON: ${error.message}`);
|
|
292
|
+
return { manifest, bridgePath: '' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
296
|
+
manifest.errors.push('Manifest must be a JSON object');
|
|
297
|
+
return { manifest, bridgePath: '' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (parsed.name !== HOST_NAME) {
|
|
301
|
+
manifest.errors.push(`Manifest name must be ${HOST_NAME}`);
|
|
302
|
+
}
|
|
303
|
+
if (parsed.type !== 'stdio') {
|
|
304
|
+
manifest.errors.push('Manifest type must be stdio');
|
|
305
|
+
}
|
|
306
|
+
if (typeof parsed.path !== 'string' || !parsed.path) {
|
|
307
|
+
manifest.errors.push('Manifest path must be a non-empty string');
|
|
308
|
+
} else if (!isAbsolutePathForPlatform(parsed.path, options.platform)) {
|
|
309
|
+
manifest.errors.push('Manifest path must be absolute');
|
|
310
|
+
} else {
|
|
311
|
+
manifest.bridgePath = normalizeDiagnosticPath(parsed.path, options.pathOptions);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
manifest.allowedOrigins = normalizeAllowedOrigins(parsed.allowed_origins);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
manifest.errors.push(error.message);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
manifest.valid = manifest.errors.length === 0;
|
|
321
|
+
return {
|
|
322
|
+
manifest,
|
|
323
|
+
bridgePath: typeof parsed.path === 'string' ? parsed.path : ''
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeAllowedOrigins(origins) {
|
|
328
|
+
if (!Array.isArray(origins) || origins.length === 0) {
|
|
329
|
+
throw new Error('Manifest allowed_origins must be a non-empty array');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
const normalized = [];
|
|
334
|
+
for (const origin of origins) {
|
|
335
|
+
const value = typeof origin === 'string' ? origin.trim() : '';
|
|
336
|
+
const match = /^chrome-extension:\/\/([a-p]{32})\/?$/.exec(value);
|
|
337
|
+
if (!match || !validateChromeExtensionId(match[1])) {
|
|
338
|
+
throw new Error(`Invalid allowed origin: ${String(origin)}`);
|
|
339
|
+
}
|
|
340
|
+
const clean = `chrome-extension://${match[1]}/`;
|
|
341
|
+
if (!seen.has(clean)) {
|
|
342
|
+
seen.add(clean);
|
|
343
|
+
normalized.push(clean);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!normalized.length) {
|
|
348
|
+
throw new Error('Manifest allowed_origins must contain at least one valid Chrome extension origin');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return normalized;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function validateBridgePath(bridgePath, options) {
|
|
355
|
+
const bridge = {
|
|
356
|
+
path: normalizeDiagnosticPath(bridgePath, options.pathOptions),
|
|
357
|
+
exists: false,
|
|
358
|
+
valid: true,
|
|
359
|
+
errors: []
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (!bridgePath || typeof bridgePath !== 'string') {
|
|
363
|
+
bridge.valid = false;
|
|
364
|
+
bridge.errors.push('Bridge path is missing');
|
|
365
|
+
return bridge;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!isAbsolutePathForPlatform(bridgePath, options.platform)) {
|
|
369
|
+
bridge.valid = false;
|
|
370
|
+
bridge.errors.push('Bridge path must be absolute');
|
|
371
|
+
return bridge;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let stat;
|
|
375
|
+
try {
|
|
376
|
+
stat = fs.statSync(bridgePath);
|
|
377
|
+
bridge.exists = true;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error?.code !== 'ENOENT') {
|
|
380
|
+
bridge.valid = false;
|
|
381
|
+
bridge.errors.push(`Unable to inspect bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`);
|
|
382
|
+
}
|
|
383
|
+
return bridge;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!stat.isFile()) {
|
|
387
|
+
bridge.valid = false;
|
|
388
|
+
bridge.errors.push('Bridge path must point to a file');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return bridge;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function pingNativeBridge(bridgePath, message, options = {}) {
|
|
395
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
396
|
+
const env = options.env || process.env;
|
|
397
|
+
const spawnInvocation = buildDoctorBridgeSpawnInvocation(bridgePath, {
|
|
398
|
+
env,
|
|
399
|
+
platform: options.platform
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return new Promise(resolve => {
|
|
403
|
+
let child;
|
|
404
|
+
let settled = false;
|
|
405
|
+
let timedOut = false;
|
|
406
|
+
let stdout = Buffer.alloc(0);
|
|
407
|
+
let stderr = Buffer.alloc(0);
|
|
408
|
+
let forceKillTimer = null;
|
|
409
|
+
|
|
410
|
+
function settle(result) {
|
|
411
|
+
if (settled) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
settled = true;
|
|
415
|
+
clearTimeout(timeoutTimer);
|
|
416
|
+
clearTimeout(forceKillTimer);
|
|
417
|
+
resolve({
|
|
418
|
+
attempted: true,
|
|
419
|
+
...result
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function killChild(signal) {
|
|
424
|
+
try {
|
|
425
|
+
if (child && !child.killed) {
|
|
426
|
+
child.kill(signal);
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
// Best effort; the close/error handlers will settle the ping.
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const timeoutTimer = setTimeout(() => {
|
|
434
|
+
timedOut = true;
|
|
435
|
+
killChild('SIGTERM');
|
|
436
|
+
forceKillTimer = setTimeout(() => {
|
|
437
|
+
killChild('SIGKILL');
|
|
438
|
+
settle({
|
|
439
|
+
ok: false,
|
|
440
|
+
timedOut: true,
|
|
441
|
+
error: `Bridge ping timed out after ${timeoutMs}ms`
|
|
442
|
+
});
|
|
443
|
+
}, 500);
|
|
444
|
+
if (typeof forceKillTimer.unref === 'function') {
|
|
445
|
+
forceKillTimer.unref();
|
|
446
|
+
}
|
|
447
|
+
}, timeoutMs);
|
|
448
|
+
if (typeof timeoutTimer.unref === 'function') {
|
|
449
|
+
timeoutTimer.unref();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
child = spawn(spawnInvocation.command, spawnInvocation.args, {
|
|
454
|
+
env,
|
|
455
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
456
|
+
windowsVerbatimArguments: spawnInvocation.windowsVerbatimArguments === true,
|
|
457
|
+
windowsHide: true
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
settle({
|
|
461
|
+
ok: false,
|
|
462
|
+
error: `Unable to start bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`,
|
|
463
|
+
errorCode: error.code || undefined
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
child.on('error', error => {
|
|
469
|
+
settle({
|
|
470
|
+
ok: false,
|
|
471
|
+
error: `Unable to start bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`,
|
|
472
|
+
errorCode: error.code || undefined
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
child.stdout.on('data', chunk => {
|
|
477
|
+
stdout = appendBoundedBuffer(stdout, chunk, MAX_DOCTOR_STDOUT_BYTES);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
child.stderr.on('data', chunk => {
|
|
481
|
+
stderr = appendBoundedBuffer(stderr, chunk, MAX_DOCTOR_STDERR_BYTES);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
child.stdin.on('error', () => {
|
|
485
|
+
// The close/error handlers produce the deterministic doctor result.
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
child.on('close', (exitCode, signal) => {
|
|
489
|
+
if (settled) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (timedOut) {
|
|
493
|
+
settle({
|
|
494
|
+
ok: false,
|
|
495
|
+
timedOut: true,
|
|
496
|
+
exitCode,
|
|
497
|
+
signal: signal || undefined,
|
|
498
|
+
error: `Bridge ping timed out after ${timeoutMs}ms`
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if ((exitCode !== 0 && exitCode !== null) || signal) {
|
|
504
|
+
settle({
|
|
505
|
+
ok: false,
|
|
506
|
+
exitCode,
|
|
507
|
+
signal: signal || undefined,
|
|
508
|
+
error: summarizeBridgeFailure(exitCode, signal, stderr)
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let decoded;
|
|
514
|
+
try {
|
|
515
|
+
decoded = decodeFrames(stdout);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
settle({
|
|
518
|
+
ok: false,
|
|
519
|
+
exitCode,
|
|
520
|
+
signal: signal || undefined,
|
|
521
|
+
error: `Bridge returned an invalid native message: ${error.message}`
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (decoded.remainder.length) {
|
|
527
|
+
settle({
|
|
528
|
+
ok: false,
|
|
529
|
+
exitCode,
|
|
530
|
+
signal: signal || undefined,
|
|
531
|
+
error: 'Bridge returned trailing partial native output'
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const response = decoded.messages.find(item => item?.id === message.id);
|
|
537
|
+
if (response) {
|
|
538
|
+
settle({
|
|
539
|
+
ok: true,
|
|
540
|
+
exitCode,
|
|
541
|
+
signal: signal || undefined,
|
|
542
|
+
rawResponse: response,
|
|
543
|
+
response: sanitizePingResponse(response, options.pathOptions)
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
settle({
|
|
549
|
+
ok: false,
|
|
550
|
+
exitCode,
|
|
551
|
+
signal: signal || undefined,
|
|
552
|
+
error: `Bridge did not return a response for ${message.id}`
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
child.stdin.end(encodeMessage(message));
|
|
558
|
+
} catch (error) {
|
|
559
|
+
killChild('SIGTERM');
|
|
560
|
+
settle({
|
|
561
|
+
ok: false,
|
|
562
|
+
error: `Unable to write bridge ping: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function buildDoctorBridgeSpawnInvocation(bridgePath, options = {}) {
|
|
569
|
+
const platform = options.platform || process.platform;
|
|
570
|
+
if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(String(bridgePath))) {
|
|
571
|
+
return {
|
|
572
|
+
command: bridgePath,
|
|
573
|
+
args: []
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
command: options.env?.ComSpec || options.env?.COMSPEC || 'cmd.exe',
|
|
579
|
+
args: ['/d', '/c', quoteWindowsCommandPath(bridgePath)],
|
|
580
|
+
windowsVerbatimArguments: true
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function quoteWindowsCommandPath(targetPath) {
|
|
585
|
+
return `"${String(targetPath).replace(/"/g, '""')}"`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sanitizePingResponse(response, pathOptions) {
|
|
589
|
+
if (!response || typeof response !== 'object') {
|
|
590
|
+
return response;
|
|
591
|
+
}
|
|
592
|
+
if (response.ok !== true) {
|
|
593
|
+
return {
|
|
594
|
+
id: response.id,
|
|
595
|
+
ok: response.ok,
|
|
596
|
+
error: sanitizeNativeError(response.error, pathOptions)
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const native = response.result && typeof response.result === 'object' ? response.result : {};
|
|
600
|
+
return {
|
|
601
|
+
id: response.id,
|
|
602
|
+
ok: true,
|
|
603
|
+
result: sanitizeNativePingResult(native)
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function sanitizeNativePingResult(native) {
|
|
608
|
+
return {
|
|
609
|
+
host: native.host,
|
|
610
|
+
platform: native.platform,
|
|
611
|
+
protocolVersion: native.protocolVersion,
|
|
612
|
+
supportedProtocol: native.supportedProtocol,
|
|
613
|
+
capabilities: native.capabilities && typeof native.capabilities === 'object'
|
|
614
|
+
? Object.keys(native.capabilities).filter(capability => native.capabilities[capability] === true).sort()
|
|
615
|
+
: [],
|
|
616
|
+
minExtensionVersion: native.minExtensionVersion,
|
|
617
|
+
version: native.version,
|
|
618
|
+
environment: {
|
|
619
|
+
codexOk: native.environment?.codex?.ok === true,
|
|
620
|
+
latexOk: native.environment?.latex?.ok === true
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function sanitizeNativeError(error, pathOptions) {
|
|
626
|
+
if (!error || typeof error !== 'object') {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
code: typeof error.code === 'string' ? error.code : undefined,
|
|
631
|
+
message: typeof error.message === 'string'
|
|
632
|
+
? sanitizeDiagnosticMessage(error.message, pathOptions)
|
|
633
|
+
: undefined
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function sanitizeCompatibility(compatibility) {
|
|
638
|
+
if (!compatibility || typeof compatibility !== 'object') {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
status: compatibility.status,
|
|
643
|
+
classification: compatibility.classification,
|
|
644
|
+
nativeVersion: compatibility.nativeVersion,
|
|
645
|
+
currentNativeVersion: compatibility.currentNativeVersion,
|
|
646
|
+
extensionVersion: compatibility.extensionVersion,
|
|
647
|
+
requiredVersion: compatibility.requiredVersion,
|
|
648
|
+
platform: compatibility.platform,
|
|
649
|
+
missingCapabilities: Array.isArray(compatibility.missingCapabilities)
|
|
650
|
+
? compatibility.missingCapabilities.slice()
|
|
651
|
+
: [],
|
|
652
|
+
missingUpdateCapabilities: Array.isArray(compatibility.missingUpdateCapabilities)
|
|
653
|
+
? compatibility.missingUpdateCapabilities.slice()
|
|
654
|
+
: [],
|
|
655
|
+
releaseUrl: compatibility.releaseUrl,
|
|
656
|
+
updateCommand: compatibility.updateCommand
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function sanitizeRegistrationTarget(target, pathOptions) {
|
|
661
|
+
const sanitized = {
|
|
662
|
+
kind: target.kind,
|
|
663
|
+
browser: target.browser,
|
|
664
|
+
manifestPath: normalizeDiagnosticPath(target.manifestPath, pathOptions)
|
|
665
|
+
};
|
|
666
|
+
if (target.registryKey) {
|
|
667
|
+
sanitized.registryKey = target.registryKey;
|
|
668
|
+
}
|
|
669
|
+
return sanitized;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function appendBoundedBuffer(current, chunk, maxBytes) {
|
|
673
|
+
const next = Buffer.concat([current, chunk]);
|
|
674
|
+
if (next.length <= maxBytes) {
|
|
675
|
+
return next;
|
|
676
|
+
}
|
|
677
|
+
return next.subarray(next.length - maxBytes);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function summarizeBridgeFailure(exitCode, signal, stderr) {
|
|
681
|
+
const stderrSummary = stderr && stderr.length
|
|
682
|
+
? firstDiagnosticLine(stderr.toString('utf8'))
|
|
683
|
+
: '';
|
|
684
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
685
|
+
return stderrSummary
|
|
686
|
+
? `Bridge exited with status ${exitCode}: ${stderrSummary}`
|
|
687
|
+
: `Bridge exited with status ${exitCode}`;
|
|
688
|
+
}
|
|
689
|
+
if (signal) {
|
|
690
|
+
return stderrSummary
|
|
691
|
+
? `Bridge exited with signal ${signal}: ${stderrSummary}`
|
|
692
|
+
: `Bridge exited with signal ${signal}`;
|
|
693
|
+
}
|
|
694
|
+
if (stderrSummary) {
|
|
695
|
+
return `Bridge wrote stderr but no native response: ${stderrSummary}`;
|
|
696
|
+
}
|
|
697
|
+
return 'Bridge produced no native response';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function sanitizePathInMessage(message, targetPath, pathOptions) {
|
|
701
|
+
const firstLine = firstDiagnosticLine(message);
|
|
702
|
+
if (!targetPath || !firstLine.includes(targetPath)) {
|
|
703
|
+
return firstLine;
|
|
704
|
+
}
|
|
705
|
+
return firstLine.split(targetPath).join(normalizeDiagnosticPath(targetPath, pathOptions));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function sanitizeDiagnosticMessage(message, pathOptions = {}) {
|
|
709
|
+
const firstLine = firstDiagnosticLine(message);
|
|
710
|
+
if (pathOptions.revealPaths) {
|
|
711
|
+
return firstLine;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const platform = pathOptions.platform || process.platform;
|
|
715
|
+
const platformPath = getPlatformPath(platform);
|
|
716
|
+
const homeDir = pathOptions.homeDir || getHomeFromEnv(pathOptions.env || process.env, platform);
|
|
717
|
+
let redacted = firstLine;
|
|
718
|
+
|
|
719
|
+
if (homeDir) {
|
|
720
|
+
const normalizedHome = platformPath.normalize(String(homeDir));
|
|
721
|
+
redacted = platform === 'win32'
|
|
722
|
+
? replaceCaseInsensitive(redacted, normalizedHome, '~')
|
|
723
|
+
: redacted.split(normalizedHome).join('~');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (hasUnredactedAbsolutePath(redacted)) {
|
|
727
|
+
return 'Native error message redacted (absolute path omitted)';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return redacted;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function firstDiagnosticLine(message) {
|
|
734
|
+
return String(message || '')
|
|
735
|
+
.split(/\r?\n/)
|
|
736
|
+
.find(line => line.trim() && !/^\s*at\s+/.test(line)) ||
|
|
737
|
+
'unknown error';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function replaceCaseInsensitive(value, needle, replacement) {
|
|
741
|
+
const lowerValue = String(value).toLowerCase();
|
|
742
|
+
const lowerNeedle = String(needle).toLowerCase();
|
|
743
|
+
if (!lowerNeedle) {
|
|
744
|
+
return value;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let result = '';
|
|
748
|
+
let offset = 0;
|
|
749
|
+
let index = lowerValue.indexOf(lowerNeedle, offset);
|
|
750
|
+
while (index !== -1) {
|
|
751
|
+
result += value.slice(offset, index) + replacement;
|
|
752
|
+
offset = index + needle.length;
|
|
753
|
+
index = lowerValue.indexOf(lowerNeedle, offset);
|
|
754
|
+
}
|
|
755
|
+
return result + value.slice(offset);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function hasUnredactedAbsolutePath(message) {
|
|
759
|
+
const candidate = String(message)
|
|
760
|
+
.replace(/~\/[^\s"'`<>|]+/g, '~')
|
|
761
|
+
.replace(/~\\[^\s"'`<>|]+/g, '~');
|
|
762
|
+
if (/[A-Za-z]:[\\/]/.test(candidate)) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
if (candidate.includes('\\')) {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
return candidate.includes('/');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function normalizeTimeoutMs(timeoutMs) {
|
|
772
|
+
const value = Number(timeoutMs);
|
|
773
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
774
|
+
return DEFAULT_DOCTOR_TIMEOUT_MS;
|
|
775
|
+
}
|
|
776
|
+
return Math.max(1, Math.floor(value));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function normalizeBrowser(browser) {
|
|
780
|
+
if (!browser || browser === 'auto') {
|
|
781
|
+
return 'auto';
|
|
782
|
+
}
|
|
783
|
+
if (browser === 'chrome' || browser === 'chromium') {
|
|
784
|
+
return browser;
|
|
785
|
+
}
|
|
786
|
+
throw new Error(`Unsupported doctor browser: ${browser}`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function resolveRuntimeRoot(runtimeRoot) {
|
|
790
|
+
if (!runtimeRoot) {
|
|
791
|
+
return '';
|
|
792
|
+
}
|
|
793
|
+
return path.isAbsolute(String(runtimeRoot))
|
|
794
|
+
? String(runtimeRoot)
|
|
795
|
+
: path.resolve(String(runtimeRoot));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function getHomeFromEnv(env, platform) {
|
|
799
|
+
if (platform === 'win32') {
|
|
800
|
+
return env.USERPROFILE || env.HOME || os.homedir();
|
|
801
|
+
}
|
|
802
|
+
return env.HOME || env.USERPROFILE || os.homedir();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function isAbsolutePathForPlatform(targetPath, platform) {
|
|
806
|
+
if (platform === 'win32') {
|
|
807
|
+
return path.win32.isAbsolute(targetPath);
|
|
808
|
+
}
|
|
809
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
810
|
+
return path.posix.isAbsolute(targetPath);
|
|
811
|
+
}
|
|
812
|
+
return path.isAbsolute(targetPath);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function getPlatformPath(platform) {
|
|
816
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
module.exports = {
|
|
820
|
+
buildDoctorBridgeSpawnInvocation,
|
|
821
|
+
classifyDoctorResult,
|
|
822
|
+
formatDoctorHuman,
|
|
823
|
+
normalizeDiagnosticPath,
|
|
824
|
+
normalizeBrowser,
|
|
825
|
+
runDoctor
|
|
826
|
+
};
|