@probelabs/probe 0.6.0-rc100
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/README.md +583 -0
- package/bin/.gitkeep +0 -0
- package/bin/probe +158 -0
- package/bin/probe-binary +0 -0
- package/build/agent/ProbeAgent.d.ts +199 -0
- package/build/agent/ProbeAgent.js +1486 -0
- package/build/agent/acp/README.md +347 -0
- package/build/agent/acp/connection.js +237 -0
- package/build/agent/acp/connection.test.js +311 -0
- package/build/agent/acp/examples/simple-client.js +212 -0
- package/build/agent/acp/examples/tool-lifecycle.js +230 -0
- package/build/agent/acp/final-test.js +173 -0
- package/build/agent/acp/index.js +5 -0
- package/build/agent/acp/integration.test.js +385 -0
- package/build/agent/acp/manual-test.js +410 -0
- package/build/agent/acp/protocol-test.js +190 -0
- package/build/agent/acp/server.js +448 -0
- package/build/agent/acp/server.test.js +371 -0
- package/build/agent/acp/test-runner.js +216 -0
- package/build/agent/acp/test-utils/README.md +315 -0
- package/build/agent/acp/test-utils/acp-tester.js +484 -0
- package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/build/agent/acp/tools.js +368 -0
- package/build/agent/acp/tools.test.js +334 -0
- package/build/agent/acp/types.js +218 -0
- package/build/agent/acp/types.test.js +327 -0
- package/build/agent/appTracer.js +360 -0
- package/build/agent/fileSpanExporter.js +169 -0
- package/build/agent/index.js +7426 -0
- package/build/agent/mcp/client.js +338 -0
- package/build/agent/mcp/config.js +313 -0
- package/build/agent/mcp/index.js +64 -0
- package/build/agent/mcp/xmlBridge.js +371 -0
- package/build/agent/mockProvider.js +53 -0
- package/build/agent/probeTool.js +257 -0
- package/build/agent/schemaUtils.js +1726 -0
- package/build/agent/simpleTelemetry.js +267 -0
- package/build/agent/telemetry.js +225 -0
- package/build/agent/tokenCounter.js +395 -0
- package/build/agent/tools.js +163 -0
- package/build/cli.js +49 -0
- package/build/delegate.js +267 -0
- package/build/directory-resolver.js +237 -0
- package/build/downloader.js +750 -0
- package/build/extract.js +149 -0
- package/build/index.js +70 -0
- package/build/mcp/index.js +514 -0
- package/build/mcp/index.ts +608 -0
- package/build/query.js +116 -0
- package/build/search.js +247 -0
- package/build/tools/common.js +410 -0
- package/build/tools/index.js +40 -0
- package/build/tools/langchain.js +88 -0
- package/build/tools/system-message.js +121 -0
- package/build/tools/vercel.js +271 -0
- package/build/utils/file-lister.js +193 -0
- package/build/utils.js +128 -0
- package/cjs/agent/ProbeAgent.cjs +5829 -0
- package/cjs/index.cjs +6217 -0
- package/cjs/package.json +3 -0
- package/index.d.ts +401 -0
- package/package.json +114 -0
- package/scripts/postinstall.js +172 -0
- package/src/agent/ProbeAgent.d.ts +199 -0
- package/src/agent/ProbeAgent.js +1486 -0
- package/src/agent/acp/README.md +347 -0
- package/src/agent/acp/connection.js +237 -0
- package/src/agent/acp/connection.test.js +311 -0
- package/src/agent/acp/examples/simple-client.js +212 -0
- package/src/agent/acp/examples/tool-lifecycle.js +230 -0
- package/src/agent/acp/final-test.js +173 -0
- package/src/agent/acp/index.js +5 -0
- package/src/agent/acp/integration.test.js +385 -0
- package/src/agent/acp/manual-test.js +410 -0
- package/src/agent/acp/protocol-test.js +190 -0
- package/src/agent/acp/server.js +448 -0
- package/src/agent/acp/server.test.js +371 -0
- package/src/agent/acp/test-runner.js +216 -0
- package/src/agent/acp/test-utils/README.md +315 -0
- package/src/agent/acp/test-utils/acp-tester.js +484 -0
- package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/src/agent/acp/tools.js +368 -0
- package/src/agent/acp/tools.test.js +334 -0
- package/src/agent/acp/types.js +218 -0
- package/src/agent/acp/types.test.js +327 -0
- package/src/agent/appTracer.js +360 -0
- package/src/agent/fileSpanExporter.js +169 -0
- package/src/agent/index.js +813 -0
- package/src/agent/mcp/client.js +338 -0
- package/src/agent/mcp/config.js +313 -0
- package/src/agent/mcp/index.js +64 -0
- package/src/agent/mcp/xmlBridge.js +371 -0
- package/src/agent/mockProvider.js +53 -0
- package/src/agent/probeTool.js +257 -0
- package/src/agent/schemaUtils.js +1726 -0
- package/src/agent/simpleTelemetry.js +267 -0
- package/src/agent/telemetry.js +225 -0
- package/src/agent/tokenCounter.js +395 -0
- package/src/agent/tools.js +163 -0
- package/src/cli.js +49 -0
- package/src/delegate.js +267 -0
- package/src/directory-resolver.js +237 -0
- package/src/downloader.js +750 -0
- package/src/extract.js +149 -0
- package/src/index.js +70 -0
- package/src/mcp/index.ts +608 -0
- package/src/query.js +116 -0
- package/src/search.js +247 -0
- package/src/tools/common.js +410 -0
- package/src/tools/index.js +40 -0
- package/src/tools/langchain.js +88 -0
- package/src/tools/system-message.js +121 -0
- package/src/tools/vercel.js +271 -0
- package/src/utils/file-lister.js +193 -0
- package/src/utils.js +128 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegate functionality for the probe package - used automatically by AI agents
|
|
3
|
+
* @module delegate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { getBinaryPath, buildCliArgs } from './utils.js';
|
|
9
|
+
import { createMessagePreview } from './tools/common.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Delegate a big distinct task to a probe subagent (used automatically by AI agents)
|
|
13
|
+
*
|
|
14
|
+
* This function is designed for automatic use within the agentic loop. AI agents
|
|
15
|
+
* should automatically identify complex multi-part requests and break them down
|
|
16
|
+
* into focused, parallel tasks using this delegation mechanism.
|
|
17
|
+
*
|
|
18
|
+
* Spawns a new probe agent with a clean environment that automatically:
|
|
19
|
+
* - Uses the default 'code-researcher' prompt (not inherited)
|
|
20
|
+
* - Disables schema validation for simpler responses
|
|
21
|
+
* - Disables mermaid validation for faster processing
|
|
22
|
+
* - Limits iterations to remaining parent iterations
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options - Delegate options
|
|
25
|
+
* @param {string} options.task - A complete, self-contained task for the subagent. Should be specific and focused on one area of expertise.
|
|
26
|
+
* @param {number} [options.timeout=300] - Timeout in seconds (default: 5 minutes)
|
|
27
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
28
|
+
* @param {number} [options.currentIteration=0] - Current tool iteration count from parent agent
|
|
29
|
+
* @param {number} [options.maxIterations=30] - Maximum tool iterations allowed
|
|
30
|
+
* @returns {Promise<string>} The response from the delegate agent
|
|
31
|
+
*/
|
|
32
|
+
export async function delegate({ task, timeout = 300, debug = false, currentIteration = 0, maxIterations = 30, tracer = null }) {
|
|
33
|
+
if (!task || typeof task !== 'string') {
|
|
34
|
+
throw new Error('Task parameter is required and must be a string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sessionId = randomUUID();
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
|
|
40
|
+
// Calculate remaining iterations for subagent
|
|
41
|
+
const remainingIterations = Math.max(1, maxIterations - currentIteration);
|
|
42
|
+
|
|
43
|
+
if (debug) {
|
|
44
|
+
console.error(`[DELEGATE] Starting delegation session ${sessionId}`);
|
|
45
|
+
console.error(`[DELEGATE] Task: ${task}`);
|
|
46
|
+
console.error(`[DELEGATE] Current iteration: ${currentIteration}/${maxIterations}`);
|
|
47
|
+
console.error(`[DELEGATE] Remaining iterations for subagent: ${remainingIterations}`);
|
|
48
|
+
console.error(`[DELEGATE] Timeout configured: ${timeout} seconds`);
|
|
49
|
+
console.error(`[DELEGATE] Using clean agent environment with code-researcher prompt`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Get the probe binary path
|
|
54
|
+
const binaryPath = await getBinaryPath();
|
|
55
|
+
|
|
56
|
+
// Create the agent command with automatic subagent configuration
|
|
57
|
+
const args = [
|
|
58
|
+
'agent',
|
|
59
|
+
'--task', task,
|
|
60
|
+
'--session-id', sessionId,
|
|
61
|
+
'--prompt-type', 'code-researcher', // Automatically use default code researcher prompt
|
|
62
|
+
'--no-schema-validation', // Automatically disable schema validation
|
|
63
|
+
'--no-mermaid-validation', // Automatically disable mermaid validation
|
|
64
|
+
'--max-iterations', remainingIterations.toString() // Automatically limit to remaining iterations
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
if (debug) {
|
|
68
|
+
args.push('--debug');
|
|
69
|
+
console.error(`[DELEGATE] Using binary at: ${binaryPath}`);
|
|
70
|
+
console.error(`[DELEGATE] Command args: ${args.join(' ')}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Spawn the delegate process
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
// Create delegation span for telemetry if tracer is available
|
|
76
|
+
const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
|
|
77
|
+
|
|
78
|
+
const process = spawn(binaryPath, args, {
|
|
79
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
|
+
timeout: timeout * 1000
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let stdout = '';
|
|
84
|
+
let stderr = '';
|
|
85
|
+
let isResolved = false;
|
|
86
|
+
|
|
87
|
+
// Collect stdout
|
|
88
|
+
process.stdout.on('data', (data) => {
|
|
89
|
+
const chunk = data.toString();
|
|
90
|
+
stdout += chunk;
|
|
91
|
+
|
|
92
|
+
if (debug) {
|
|
93
|
+
const preview = createMessagePreview(chunk);
|
|
94
|
+
console.error(`[DELEGATE] stdout chunk received (${chunk.length} chars): ${preview}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Collect stderr
|
|
99
|
+
process.stderr.on('data', (data) => {
|
|
100
|
+
const chunk = data.toString();
|
|
101
|
+
stderr += chunk;
|
|
102
|
+
|
|
103
|
+
if (debug) {
|
|
104
|
+
const preview = createMessagePreview(chunk);
|
|
105
|
+
console.error(`[DELEGATE] stderr chunk received (${chunk.length} chars): ${preview}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle process completion
|
|
110
|
+
process.on('close', (code) => {
|
|
111
|
+
if (isResolved) return;
|
|
112
|
+
isResolved = true;
|
|
113
|
+
|
|
114
|
+
const duration = Date.now() - startTime;
|
|
115
|
+
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.error(`[DELEGATE] Process completed with code ${code} in ${duration}ms`);
|
|
118
|
+
console.error(`[DELEGATE] Duration: ${(duration / 1000).toFixed(2)}s`);
|
|
119
|
+
console.error(`[DELEGATE] Total stdout: ${stdout.length} chars`);
|
|
120
|
+
console.error(`[DELEGATE] Total stderr: ${stderr.length} chars`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
// Successful delegation - return the response
|
|
125
|
+
const response = stdout.trim();
|
|
126
|
+
|
|
127
|
+
if (!response) {
|
|
128
|
+
if (debug) {
|
|
129
|
+
console.error(`[DELEGATE] Task completed but returned empty response for session ${sessionId}`);
|
|
130
|
+
}
|
|
131
|
+
reject(new Error('Delegate agent returned empty response'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (debug) {
|
|
136
|
+
console.error(`[DELEGATE] Task completed successfully for session ${sessionId}`);
|
|
137
|
+
console.error(`[DELEGATE] Response length: ${response.length} chars`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Record successful completion in telemetry
|
|
141
|
+
if (tracer) {
|
|
142
|
+
tracer.recordDelegationEvent('completed', {
|
|
143
|
+
'delegation.session_id': sessionId,
|
|
144
|
+
'delegation.duration_ms': duration,
|
|
145
|
+
'delegation.response_length': response.length,
|
|
146
|
+
'delegation.success': true
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (delegationSpan) {
|
|
150
|
+
delegationSpan.setAttributes({
|
|
151
|
+
'delegation.result.success': true,
|
|
152
|
+
'delegation.result.response_length': response.length,
|
|
153
|
+
'delegation.result.duration_ms': duration
|
|
154
|
+
});
|
|
155
|
+
delegationSpan.setStatus({ code: 1 }); // OK
|
|
156
|
+
delegationSpan.end();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolve(response);
|
|
161
|
+
} else {
|
|
162
|
+
// Failed delegation
|
|
163
|
+
const errorMessage = stderr.trim() || `Delegate process failed with exit code ${code}`;
|
|
164
|
+
if (debug) {
|
|
165
|
+
console.error(`[DELEGATE] Task failed for session ${sessionId} with code ${code}`);
|
|
166
|
+
console.error(`[DELEGATE] Error message: ${errorMessage}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Record failure in telemetry
|
|
170
|
+
if (tracer) {
|
|
171
|
+
tracer.recordDelegationEvent('failed', {
|
|
172
|
+
'delegation.session_id': sessionId,
|
|
173
|
+
'delegation.duration_ms': duration,
|
|
174
|
+
'delegation.exit_code': code,
|
|
175
|
+
'delegation.error_message': errorMessage,
|
|
176
|
+
'delegation.success': false
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (delegationSpan) {
|
|
180
|
+
delegationSpan.setAttributes({
|
|
181
|
+
'delegation.result.success': false,
|
|
182
|
+
'delegation.result.exit_code': code,
|
|
183
|
+
'delegation.result.error': errorMessage,
|
|
184
|
+
'delegation.result.duration_ms': duration
|
|
185
|
+
});
|
|
186
|
+
delegationSpan.setStatus({ code: 2, message: errorMessage }); // ERROR
|
|
187
|
+
delegationSpan.end();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
reject(new Error(`Delegation failed: ${errorMessage}`));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Handle process errors
|
|
196
|
+
process.on('error', (error) => {
|
|
197
|
+
if (isResolved) return;
|
|
198
|
+
isResolved = true;
|
|
199
|
+
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
201
|
+
|
|
202
|
+
if (debug) {
|
|
203
|
+
console.error(`[DELEGATE] Process spawn error after ${duration}ms:`, error);
|
|
204
|
+
console.error(`[DELEGATE] Session ${sessionId} failed during process creation`);
|
|
205
|
+
console.error(`[DELEGATE] Error type: ${error.code || 'unknown'}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
reject(new Error(`Failed to start delegate process: ${error.message}`));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Handle timeout
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (isResolved) return;
|
|
214
|
+
isResolved = true;
|
|
215
|
+
|
|
216
|
+
const duration = Date.now() - startTime;
|
|
217
|
+
|
|
218
|
+
if (debug) {
|
|
219
|
+
console.error(`[DELEGATE] Process timeout after ${(duration / 1000).toFixed(2)}s (limit: ${timeout}s)`);
|
|
220
|
+
console.error(`[DELEGATE] Terminating session ${sessionId} due to timeout`);
|
|
221
|
+
console.error(`[DELEGATE] Partial stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? '...' : ''}`);
|
|
222
|
+
console.error(`[DELEGATE] Partial stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? '...' : ''}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Kill the process
|
|
226
|
+
process.kill('SIGTERM');
|
|
227
|
+
|
|
228
|
+
// Give it a moment to terminate gracefully
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (!process.killed) {
|
|
231
|
+
if (debug) {
|
|
232
|
+
console.error(`[DELEGATE] Force killing process ${sessionId} after graceful timeout`);
|
|
233
|
+
}
|
|
234
|
+
process.kill('SIGKILL');
|
|
235
|
+
}
|
|
236
|
+
}, 5000);
|
|
237
|
+
|
|
238
|
+
reject(new Error(`Delegation timed out after ${timeout} seconds`));
|
|
239
|
+
}, timeout * 1000);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const duration = Date.now() - startTime;
|
|
244
|
+
|
|
245
|
+
if (debug) {
|
|
246
|
+
console.error(`[DELEGATE] Error in delegate function after ${duration}ms:`, error);
|
|
247
|
+
console.error(`[DELEGATE] Session ${sessionId} failed during setup`);
|
|
248
|
+
console.error(`[DELEGATE] Error stack: ${error.stack}`);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Delegation setup failed: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if delegate functionality is available
|
|
257
|
+
*
|
|
258
|
+
* @returns {Promise<boolean>} True if delegate is available
|
|
259
|
+
*/
|
|
260
|
+
export async function isDelegateAvailable() {
|
|
261
|
+
try {
|
|
262
|
+
const binaryPath = await getBinaryPath();
|
|
263
|
+
return !!binaryPath;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory resolver for probe binary storage
|
|
3
|
+
* Handles reliable directory resolution across different environments (CI, npx, Docker, etc.)
|
|
4
|
+
* @module directory-resolver
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get a writable directory for storing the probe binary
|
|
17
|
+
* Tries multiple strategies in order of preference
|
|
18
|
+
* @returns {Promise<string>} Path to writable bin directory
|
|
19
|
+
*/
|
|
20
|
+
export async function getPackageBinDir() {
|
|
21
|
+
const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
|
|
22
|
+
|
|
23
|
+
if (debug) {
|
|
24
|
+
console.log('DEBUG: Starting probe binary directory resolution');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Strategy 1: Check for explicit binary path override
|
|
28
|
+
if (process.env.PROBE_BINARY_PATH) {
|
|
29
|
+
if (debug) {
|
|
30
|
+
console.log(`DEBUG: Checking PROBE_BINARY_PATH: ${process.env.PROBE_BINARY_PATH}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const binaryPath = process.env.PROBE_BINARY_PATH;
|
|
34
|
+
if (await fs.pathExists(binaryPath)) {
|
|
35
|
+
const binDir = path.dirname(binaryPath);
|
|
36
|
+
if (await canWriteToDirectory(binDir)) {
|
|
37
|
+
if (debug) {
|
|
38
|
+
console.log(`DEBUG: Using PROBE_BINARY_PATH directory: ${binDir}`);
|
|
39
|
+
}
|
|
40
|
+
return binDir;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.warn(`Warning: PROBE_BINARY_PATH ${binaryPath} does not exist`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Strategy 2: Check for cache directory override
|
|
48
|
+
if (process.env.PROBE_CACHE_DIR) {
|
|
49
|
+
if (debug) {
|
|
50
|
+
console.log(`DEBUG: Checking PROBE_CACHE_DIR: ${process.env.PROBE_CACHE_DIR}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cacheDir = path.join(process.env.PROBE_CACHE_DIR, 'bin');
|
|
54
|
+
if (await ensureDirectory(cacheDir)) {
|
|
55
|
+
if (debug) {
|
|
56
|
+
console.log(`DEBUG: Using PROBE_CACHE_DIR: ${cacheDir}`);
|
|
57
|
+
}
|
|
58
|
+
return cacheDir;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strategy 3: Try to find package root and use its bin directory
|
|
63
|
+
const packageRoot = await findPackageRoot();
|
|
64
|
+
if (packageRoot) {
|
|
65
|
+
if (debug) {
|
|
66
|
+
console.log(`DEBUG: Found package root: ${packageRoot}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const packageBinDir = path.join(packageRoot, 'bin');
|
|
70
|
+
if (await ensureDirectory(packageBinDir) && await canWriteToDirectory(packageBinDir)) {
|
|
71
|
+
if (debug) {
|
|
72
|
+
console.log(`DEBUG: Using package bin directory: ${packageBinDir}`);
|
|
73
|
+
}
|
|
74
|
+
return packageBinDir;
|
|
75
|
+
} else if (debug) {
|
|
76
|
+
console.log(`DEBUG: Package bin directory ${packageBinDir} not writable, trying fallbacks`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Strategy 4: Use user home directory cache (like Puppeteer)
|
|
81
|
+
const homeCache = path.join(os.homedir(), '.probe', 'bin');
|
|
82
|
+
if (debug) {
|
|
83
|
+
console.log(`DEBUG: Trying home cache directory: ${homeCache}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (await ensureDirectory(homeCache)) {
|
|
87
|
+
if (debug) {
|
|
88
|
+
console.log(`DEBUG: Using home cache directory: ${homeCache}`);
|
|
89
|
+
}
|
|
90
|
+
return homeCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strategy 5: Use temp directory as last resort
|
|
94
|
+
const tempCache = path.join(os.tmpdir(), 'probe-cache', 'bin');
|
|
95
|
+
if (debug) {
|
|
96
|
+
console.log(`DEBUG: Trying temp cache directory: ${tempCache}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (await ensureDirectory(tempCache)) {
|
|
100
|
+
if (debug) {
|
|
101
|
+
console.log(`DEBUG: Using temp cache directory: ${tempCache}`);
|
|
102
|
+
}
|
|
103
|
+
return tempCache;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If all strategies fail, throw a helpful error
|
|
107
|
+
const errorMessage = [
|
|
108
|
+
'Could not find a writable directory for probe binary.',
|
|
109
|
+
'Tried the following locations:',
|
|
110
|
+
packageRoot ? `- Package bin directory: ${path.join(packageRoot, 'bin')}` : '- Package root not found',
|
|
111
|
+
`- Home cache directory: ${homeCache}`,
|
|
112
|
+
`- Temp cache directory: ${tempCache}`,
|
|
113
|
+
'',
|
|
114
|
+
'You can override the location using environment variables:',
|
|
115
|
+
'- PROBE_BINARY_PATH=/path/to/probe (direct path to binary)',
|
|
116
|
+
'- PROBE_CACHE_DIR=/path/to/cache (cache directory, binary will be in /bin subdirectory)'
|
|
117
|
+
].join('\n');
|
|
118
|
+
|
|
119
|
+
throw new Error(errorMessage);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Find the package root directory by looking for package.json with the correct name
|
|
124
|
+
* @returns {Promise<string|null>} Path to package root or null if not found
|
|
125
|
+
*/
|
|
126
|
+
async function findPackageRoot() {
|
|
127
|
+
const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
|
|
128
|
+
|
|
129
|
+
// Start from current module directory
|
|
130
|
+
let currentDir = __dirname;
|
|
131
|
+
const rootDir = path.parse(currentDir).root;
|
|
132
|
+
|
|
133
|
+
if (debug) {
|
|
134
|
+
console.log(`DEBUG: Starting package root search from: ${currentDir}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Walk up until we find package.json with our package name
|
|
138
|
+
while (currentDir !== rootDir) {
|
|
139
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
143
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
144
|
+
|
|
145
|
+
if (debug) {
|
|
146
|
+
console.log(`DEBUG: Found package.json at ${packageJsonPath}, name: ${packageJson.name}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if this is our package
|
|
150
|
+
if (packageJson.name === '@probelabs/probe') {
|
|
151
|
+
if (debug) {
|
|
152
|
+
console.log(`DEBUG: Found probe package root: ${currentDir}`);
|
|
153
|
+
}
|
|
154
|
+
return currentDir;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (debug) {
|
|
159
|
+
console.log(`DEBUG: Error reading package.json at ${packageJsonPath}: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
// Continue searching
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
currentDir = path.dirname(currentDir);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (debug) {
|
|
168
|
+
console.log('DEBUG: Package root not found, reached filesystem root');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Ensure a directory exists and is writable
|
|
176
|
+
* @param {string} dirPath - Path to directory
|
|
177
|
+
* @returns {Promise<boolean>} True if directory exists and is writable
|
|
178
|
+
*/
|
|
179
|
+
async function ensureDirectory(dirPath) {
|
|
180
|
+
const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await fs.ensureDir(dirPath);
|
|
184
|
+
|
|
185
|
+
// Test write permissions by creating a temporary file
|
|
186
|
+
const testFile = path.join(dirPath, '.probe-write-test');
|
|
187
|
+
await fs.writeFile(testFile, 'test');
|
|
188
|
+
await fs.remove(testFile);
|
|
189
|
+
|
|
190
|
+
if (debug) {
|
|
191
|
+
console.log(`DEBUG: Directory ${dirPath} is writable`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (debug) {
|
|
197
|
+
console.log(`DEBUG: Directory ${dirPath} not writable: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a directory is writable
|
|
205
|
+
* @param {string} dirPath - Path to directory
|
|
206
|
+
* @returns {Promise<boolean>} True if directory is writable
|
|
207
|
+
*/
|
|
208
|
+
async function canWriteToDirectory(dirPath) {
|
|
209
|
+
const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
// Check if directory exists
|
|
213
|
+
const exists = await fs.pathExists(dirPath);
|
|
214
|
+
if (!exists) {
|
|
215
|
+
if (debug) {
|
|
216
|
+
console.log(`DEBUG: Directory ${dirPath} does not exist`);
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Test write permissions
|
|
222
|
+
const testFile = path.join(dirPath, '.probe-write-test');
|
|
223
|
+
await fs.writeFile(testFile, 'test');
|
|
224
|
+
await fs.remove(testFile);
|
|
225
|
+
|
|
226
|
+
if (debug) {
|
|
227
|
+
console.log(`DEBUG: Directory ${dirPath} is writable`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (debug) {
|
|
233
|
+
console.log(`DEBUG: Directory ${dirPath} not writable: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|