@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.
Files changed (115) hide show
  1. package/README.md +583 -0
  2. package/bin/.gitkeep +0 -0
  3. package/bin/probe +158 -0
  4. package/bin/probe-binary +0 -0
  5. package/build/agent/ProbeAgent.d.ts +199 -0
  6. package/build/agent/ProbeAgent.js +1486 -0
  7. package/build/agent/acp/README.md +347 -0
  8. package/build/agent/acp/connection.js +237 -0
  9. package/build/agent/acp/connection.test.js +311 -0
  10. package/build/agent/acp/examples/simple-client.js +212 -0
  11. package/build/agent/acp/examples/tool-lifecycle.js +230 -0
  12. package/build/agent/acp/final-test.js +173 -0
  13. package/build/agent/acp/index.js +5 -0
  14. package/build/agent/acp/integration.test.js +385 -0
  15. package/build/agent/acp/manual-test.js +410 -0
  16. package/build/agent/acp/protocol-test.js +190 -0
  17. package/build/agent/acp/server.js +448 -0
  18. package/build/agent/acp/server.test.js +371 -0
  19. package/build/agent/acp/test-runner.js +216 -0
  20. package/build/agent/acp/test-utils/README.md +315 -0
  21. package/build/agent/acp/test-utils/acp-tester.js +484 -0
  22. package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
  23. package/build/agent/acp/tools.js +368 -0
  24. package/build/agent/acp/tools.test.js +334 -0
  25. package/build/agent/acp/types.js +218 -0
  26. package/build/agent/acp/types.test.js +327 -0
  27. package/build/agent/appTracer.js +360 -0
  28. package/build/agent/fileSpanExporter.js +169 -0
  29. package/build/agent/index.js +7426 -0
  30. package/build/agent/mcp/client.js +338 -0
  31. package/build/agent/mcp/config.js +313 -0
  32. package/build/agent/mcp/index.js +64 -0
  33. package/build/agent/mcp/xmlBridge.js +371 -0
  34. package/build/agent/mockProvider.js +53 -0
  35. package/build/agent/probeTool.js +257 -0
  36. package/build/agent/schemaUtils.js +1726 -0
  37. package/build/agent/simpleTelemetry.js +267 -0
  38. package/build/agent/telemetry.js +225 -0
  39. package/build/agent/tokenCounter.js +395 -0
  40. package/build/agent/tools.js +163 -0
  41. package/build/cli.js +49 -0
  42. package/build/delegate.js +267 -0
  43. package/build/directory-resolver.js +237 -0
  44. package/build/downloader.js +750 -0
  45. package/build/extract.js +149 -0
  46. package/build/index.js +70 -0
  47. package/build/mcp/index.js +514 -0
  48. package/build/mcp/index.ts +608 -0
  49. package/build/query.js +116 -0
  50. package/build/search.js +247 -0
  51. package/build/tools/common.js +410 -0
  52. package/build/tools/index.js +40 -0
  53. package/build/tools/langchain.js +88 -0
  54. package/build/tools/system-message.js +121 -0
  55. package/build/tools/vercel.js +271 -0
  56. package/build/utils/file-lister.js +193 -0
  57. package/build/utils.js +128 -0
  58. package/cjs/agent/ProbeAgent.cjs +5829 -0
  59. package/cjs/index.cjs +6217 -0
  60. package/cjs/package.json +3 -0
  61. package/index.d.ts +401 -0
  62. package/package.json +114 -0
  63. package/scripts/postinstall.js +172 -0
  64. package/src/agent/ProbeAgent.d.ts +199 -0
  65. package/src/agent/ProbeAgent.js +1486 -0
  66. package/src/agent/acp/README.md +347 -0
  67. package/src/agent/acp/connection.js +237 -0
  68. package/src/agent/acp/connection.test.js +311 -0
  69. package/src/agent/acp/examples/simple-client.js +212 -0
  70. package/src/agent/acp/examples/tool-lifecycle.js +230 -0
  71. package/src/agent/acp/final-test.js +173 -0
  72. package/src/agent/acp/index.js +5 -0
  73. package/src/agent/acp/integration.test.js +385 -0
  74. package/src/agent/acp/manual-test.js +410 -0
  75. package/src/agent/acp/protocol-test.js +190 -0
  76. package/src/agent/acp/server.js +448 -0
  77. package/src/agent/acp/server.test.js +371 -0
  78. package/src/agent/acp/test-runner.js +216 -0
  79. package/src/agent/acp/test-utils/README.md +315 -0
  80. package/src/agent/acp/test-utils/acp-tester.js +484 -0
  81. package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
  82. package/src/agent/acp/tools.js +368 -0
  83. package/src/agent/acp/tools.test.js +334 -0
  84. package/src/agent/acp/types.js +218 -0
  85. package/src/agent/acp/types.test.js +327 -0
  86. package/src/agent/appTracer.js +360 -0
  87. package/src/agent/fileSpanExporter.js +169 -0
  88. package/src/agent/index.js +813 -0
  89. package/src/agent/mcp/client.js +338 -0
  90. package/src/agent/mcp/config.js +313 -0
  91. package/src/agent/mcp/index.js +64 -0
  92. package/src/agent/mcp/xmlBridge.js +371 -0
  93. package/src/agent/mockProvider.js +53 -0
  94. package/src/agent/probeTool.js +257 -0
  95. package/src/agent/schemaUtils.js +1726 -0
  96. package/src/agent/simpleTelemetry.js +267 -0
  97. package/src/agent/telemetry.js +225 -0
  98. package/src/agent/tokenCounter.js +395 -0
  99. package/src/agent/tools.js +163 -0
  100. package/src/cli.js +49 -0
  101. package/src/delegate.js +267 -0
  102. package/src/directory-resolver.js +237 -0
  103. package/src/downloader.js +750 -0
  104. package/src/extract.js +149 -0
  105. package/src/index.js +70 -0
  106. package/src/mcp/index.ts +608 -0
  107. package/src/query.js +116 -0
  108. package/src/search.js +247 -0
  109. package/src/tools/common.js +410 -0
  110. package/src/tools/index.js +40 -0
  111. package/src/tools/langchain.js +88 -0
  112. package/src/tools/system-message.js +121 -0
  113. package/src/tools/vercel.js +271 -0
  114. package/src/utils/file-lister.js +193 -0
  115. package/src/utils.js +128 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Tools for Vercel AI SDK
3
+ * @module tools/vercel
4
+ */
5
+
6
+ import { tool } from 'ai';
7
+ import { search } from '../search.js';
8
+ import { query } from '../query.js';
9
+ import { extract } from '../extract.js';
10
+ import { delegate } from '../delegate.js';
11
+ import { searchSchema, querySchema, extractSchema, delegateSchema, searchDescription, queryDescription, extractDescription, delegateDescription } from './common.js';
12
+
13
+ /**
14
+ * Search tool generator
15
+ *
16
+ * @param {Object} [options] - Configuration options
17
+ * @param {string} [options.sessionId] - Session ID for caching search results
18
+ * @param {number} [options.maxTokens=10000] - Default max tokens
19
+ * @param {boolean} [options.debug=false] - Enable debug logging
20
+ * @returns {Object} Configured search tool
21
+ */
22
+ export const searchTool = (options = {}) => {
23
+ const { sessionId, maxTokens = 10000, debug = false, outline = false } = options;
24
+
25
+ return tool({
26
+ name: 'search',
27
+ description: searchDescription,
28
+ inputSchema: searchSchema,
29
+ execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language }) => {
30
+ try {
31
+ // Use parameter maxTokens if provided, otherwise use the default
32
+ const effectiveMaxTokens = paramMaxTokens || maxTokens;
33
+
34
+ // Use the path from parameters if provided, otherwise use defaultPath from config
35
+ let searchPath = path || options.defaultPath || '.';
36
+
37
+ // If path is "." or "./", use the defaultPath if available
38
+ if ((searchPath === "." || searchPath === "./") && options.defaultPath) {
39
+ if (debug) {
40
+ console.error(`Using default path "${options.defaultPath}" instead of "${searchPath}"`);
41
+ }
42
+ searchPath = options.defaultPath;
43
+ }
44
+
45
+ if (debug) {
46
+ console.error(`Executing search with query: "${searchQuery}", path: "${searchPath}", exact: ${exact ? 'true' : 'false'}, language: ${language || 'all'}, session: ${sessionId || 'none'}`);
47
+ }
48
+
49
+ const searchOptions = {
50
+ query: searchQuery,
51
+ path: searchPath,
52
+ allowTests: allow_tests,
53
+ exact,
54
+ json: false,
55
+ maxTokens: effectiveMaxTokens,
56
+ session: sessionId, // Pass session ID if provided
57
+ language // Pass language parameter if provided
58
+ };
59
+
60
+ // Add outline format if enabled
61
+ if (outline) {
62
+ searchOptions.format = 'outline-xml';
63
+ }
64
+
65
+ const results = await search(searchOptions);
66
+
67
+ return results;
68
+ } catch (error) {
69
+ console.error('Error executing search command:', error);
70
+ return `Error executing search command: ${error.message}`;
71
+ }
72
+ }
73
+ });
74
+ };
75
+
76
+ /**
77
+ * Query tool generator
78
+ *
79
+ * @param {Object} [options] - Configuration options
80
+ * @param {boolean} [options.debug=false] - Enable debug logging
81
+ * @returns {Object} Configured query tool
82
+ */
83
+ export const queryTool = (options = {}) => {
84
+ const { debug = false } = options;
85
+
86
+ return tool({
87
+ name: 'query',
88
+ description: queryDescription,
89
+ inputSchema: querySchema,
90
+ execute: async ({ pattern, path, language, allow_tests }) => {
91
+ try {
92
+ // Use the path from parameters if provided, otherwise use defaultPath from config
93
+ let queryPath = path || options.defaultPath || '.';
94
+
95
+ // If path is "." or "./", use the defaultPath if available
96
+ if ((queryPath === "." || queryPath === "./") && options.defaultPath) {
97
+ if (debug) {
98
+ console.error(`Using default path "${options.defaultPath}" instead of "${queryPath}"`);
99
+ }
100
+ queryPath = options.defaultPath;
101
+ }
102
+
103
+ if (debug) {
104
+ console.error(`Executing query with pattern: "${pattern}", path: "${queryPath}", language: ${language || 'auto'}`);
105
+ }
106
+
107
+ const results = await query({
108
+ pattern,
109
+ path: queryPath,
110
+ language,
111
+ allow_tests,
112
+ json: false
113
+ });
114
+
115
+ return results;
116
+ } catch (error) {
117
+ console.error('Error executing query command:', error);
118
+ return `Error executing query command: ${error.message}`;
119
+ }
120
+ }
121
+ });
122
+ };
123
+
124
+ /**
125
+ * Extract tool generator
126
+ *
127
+ * @param {Object} [options] - Configuration options
128
+ * @param {boolean} [options.debug=false] - Enable debug logging
129
+ * @returns {Object} Configured extract tool
130
+ */
131
+ export const extractTool = (options = {}) => {
132
+ const { debug = false, outline = false } = options;
133
+
134
+ return tool({
135
+ name: 'extract',
136
+ description: extractDescription,
137
+ inputSchema: extractSchema,
138
+ execute: async ({ targets, input_content, line, end_line, allow_tests, context_lines, format }) => {
139
+ try {
140
+ // Use the defaultPath from config for context
141
+ let extractPath = options.defaultPath || '.';
142
+
143
+ // If path is "." or "./", use the defaultPath if available
144
+ if ((extractPath === "." || extractPath === "./") && options.defaultPath) {
145
+ if (debug) {
146
+ console.error(`Using default path "${options.defaultPath}" instead of "${extractPath}"`);
147
+ }
148
+ extractPath = options.defaultPath;
149
+ }
150
+
151
+ if (debug) {
152
+ if (targets) {
153
+ console.error(`Executing extract with targets: "${targets}", path: "${extractPath}", context lines: ${context_lines || 10}`);
154
+ } else if (input_content) {
155
+ console.error(`Executing extract with input content, path: "${extractPath}", context lines: ${context_lines || 10}`);
156
+ }
157
+ }
158
+
159
+ // Create a temporary file for input content if provided
160
+ let tempFilePath = null;
161
+ let extractOptions = { path: extractPath };
162
+
163
+ if (input_content) {
164
+ // Import required modules
165
+ const { writeFileSync, unlinkSync } = await import('fs');
166
+ const { join } = await import('path');
167
+ const { tmpdir } = await import('os');
168
+ const { randomUUID } = await import('crypto');
169
+
170
+ // Create a temporary file with the input content
171
+ tempFilePath = join(tmpdir(), `probe-extract-${randomUUID()}.txt`);
172
+ writeFileSync(tempFilePath, input_content);
173
+
174
+ if (debug) {
175
+ console.error(`Created temporary file for input content: ${tempFilePath}`);
176
+ }
177
+
178
+ // Apply format mapping for outline-xml to xml
179
+ let effectiveFormat = format;
180
+ if (outline && format === 'outline-xml') {
181
+ effectiveFormat = 'xml';
182
+ }
183
+
184
+ // Set up extract options with input file
185
+ extractOptions = {
186
+ inputFile: tempFilePath,
187
+ allowTests: allow_tests,
188
+ contextLines: context_lines,
189
+ format: effectiveFormat
190
+ };
191
+ } else if (targets) {
192
+ // Parse targets to handle line numbers and symbol names
193
+ const files = [targets];
194
+
195
+ // Apply format mapping for outline-xml to xml
196
+ let effectiveFormat = format;
197
+ if (outline && format === 'outline-xml') {
198
+ effectiveFormat = 'xml';
199
+ }
200
+
201
+ // Set up extract options with files
202
+ extractOptions = {
203
+ files,
204
+ allowTests: allow_tests,
205
+ contextLines: context_lines,
206
+ format: effectiveFormat
207
+ };
208
+ } else {
209
+ throw new Error('Either targets or input_content must be provided');
210
+ }
211
+
212
+ // Execute the extract command
213
+ const results = await extract(extractOptions);
214
+
215
+ // Clean up temporary file if created
216
+ if (tempFilePath) {
217
+ const { unlinkSync } = await import('fs');
218
+ try {
219
+ unlinkSync(tempFilePath);
220
+ if (debug) {
221
+ console.error(`Removed temporary file: ${tempFilePath}`);
222
+ }
223
+ } catch (cleanupError) {
224
+ console.error(`Warning: Failed to remove temporary file: ${cleanupError.message}`);
225
+ }
226
+ }
227
+
228
+ return results;
229
+ } catch (error) {
230
+ console.error('Error executing extract command:', error);
231
+ return `Error executing extract command: ${error.message}`;
232
+ }
233
+ }
234
+ });
235
+ };
236
+
237
+ /**
238
+ * Delegate tool generator
239
+ *
240
+ * @param {Object} [options] - Configuration options
241
+ * @param {boolean} [options.debug=false] - Enable debug logging
242
+ * @param {number} [options.timeout=300] - Default timeout in seconds
243
+ * @returns {Object} Configured delegate tool
244
+ */
245
+ export const delegateTool = (options = {}) => {
246
+ const { debug = false, timeout = 300 } = options;
247
+
248
+ return tool({
249
+ name: 'delegate',
250
+ description: delegateDescription,
251
+ inputSchema: delegateSchema,
252
+ execute: async ({ task }) => {
253
+ try {
254
+ if (debug) {
255
+ console.error(`Executing delegate with task: "${task}"`);
256
+ }
257
+
258
+ const result = await delegate({
259
+ task,
260
+ timeout,
261
+ debug
262
+ });
263
+
264
+ return result;
265
+ } catch (error) {
266
+ console.error('Error executing delegate command:', error);
267
+ return `Error executing delegate command: ${error.message}`;
268
+ }
269
+ }
270
+ });
271
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * File listing utility for the probe package
3
+ * @module utils/file-lister
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { promisify } from 'util';
9
+ import { exec } from 'child_process';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * List files in a directory by nesting level, respecting .gitignore
15
+ *
16
+ * @param {Object} options - Options for listing files
17
+ * @param {string} options.directory - Directory to list files from
18
+ * @param {number} [options.maxFiles=100] - Maximum number of files to return
19
+ * @param {boolean} [options.respectGitignore=true] - Whether to respect .gitignore
20
+ * @returns {Promise<string[]>} - Array of file paths
21
+ */
22
+ export async function listFilesByLevel(options) {
23
+ const {
24
+ directory,
25
+ maxFiles = 100,
26
+ respectGitignore = true
27
+ } = options;
28
+
29
+ // Check if directory exists
30
+ if (!fs.existsSync(directory)) {
31
+ throw new Error(`Directory does not exist: ${directory}`);
32
+ }
33
+
34
+ // Use git ls-files if .git directory exists and respectGitignore is true
35
+ const gitDirExists = fs.existsSync(path.join(directory, '.git'));
36
+ if (gitDirExists && respectGitignore) {
37
+ try {
38
+ return await listFilesUsingGit(directory, maxFiles);
39
+ } catch (error) {
40
+ console.error(`Warning: Failed to use git ls-files: ${error.message}`);
41
+ console.error('Falling back to manual file listing');
42
+ }
43
+ }
44
+
45
+ // Fall back to manual file listing
46
+ return await listFilesByLevelManually(directory, maxFiles, respectGitignore);
47
+ }
48
+
49
+ /**
50
+ * List files using git ls-files (respects .gitignore by default)
51
+ *
52
+ * @param {string} directory - Directory to list files from
53
+ * @param {number} maxFiles - Maximum number of files to return
54
+ * @returns {Promise<string[]>} - Array of file paths
55
+ */
56
+ async function listFilesUsingGit(directory, maxFiles) {
57
+ // Use git ls-files to get tracked files (respects .gitignore)
58
+ const { stdout } = await execAsync('git ls-files', { cwd: directory });
59
+
60
+ // Split output into lines and filter out empty lines
61
+ const files = stdout.split('\n').filter(Boolean);
62
+
63
+ // Sort files by directory depth (breadth-first)
64
+ const sortedFiles = files.sort((a, b) => {
65
+ const depthA = a.split(path.sep).length;
66
+ const depthB = b.split(path.sep).length;
67
+ return depthA - depthB;
68
+ });
69
+
70
+ // Limit to maxFiles
71
+ return sortedFiles.slice(0, maxFiles);
72
+ }
73
+
74
+ /**
75
+ * List files manually by nesting level
76
+ *
77
+ * @param {string} directory - Directory to list files from
78
+ * @param {number} maxFiles - Maximum number of files to return
79
+ * @param {boolean} respectGitignore - Whether to respect .gitignore
80
+ * @returns {Promise<string[]>} - Array of file paths
81
+ */
82
+ async function listFilesByLevelManually(directory, maxFiles, respectGitignore) {
83
+ // Load .gitignore patterns if needed
84
+ let ignorePatterns = [];
85
+ if (respectGitignore) {
86
+ ignorePatterns = loadGitignorePatterns(directory);
87
+ }
88
+
89
+ // Initialize result array
90
+ const result = [];
91
+
92
+ // Initialize queue with root directory
93
+ const queue = [{ dir: directory, level: 0 }];
94
+
95
+ // Process queue (breadth-first)
96
+ while (queue.length > 0 && result.length < maxFiles) {
97
+ const { dir, level } = queue.shift();
98
+
99
+ try {
100
+ // Read directory contents
101
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
102
+
103
+ // Process files first (at current level)
104
+ const files = entries.filter(entry => entry.isFile());
105
+ for (const file of files) {
106
+ if (result.length >= maxFiles) break;
107
+
108
+ const filePath = path.join(dir, file.name);
109
+ const relativePath = path.relative(directory, filePath);
110
+
111
+ // Skip if file matches any ignore pattern
112
+ if (shouldIgnore(relativePath, ignorePatterns)) continue;
113
+
114
+ result.push(relativePath);
115
+ }
116
+
117
+ // Then add directories to queue for next level
118
+ const dirs = entries.filter(entry => entry.isDirectory());
119
+ for (const subdir of dirs) {
120
+ const subdirPath = path.join(dir, subdir.name);
121
+ const relativeSubdirPath = path.relative(directory, subdirPath);
122
+
123
+ // Skip if directory matches any ignore pattern
124
+ if (shouldIgnore(relativeSubdirPath, ignorePatterns)) continue;
125
+
126
+ // Skip node_modules and .git directories
127
+ if (subdir.name === 'node_modules' || subdir.name === '.git') continue;
128
+
129
+ queue.push({ dir: subdirPath, level: level + 1 });
130
+ }
131
+ } catch (error) {
132
+ console.error(`Warning: Could not read directory ${dir}: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Load .gitignore patterns from a directory
141
+ *
142
+ * @param {string} directory - Directory to load .gitignore from
143
+ * @returns {string[]} - Array of ignore patterns
144
+ */
145
+ function loadGitignorePatterns(directory) {
146
+ const gitignorePath = path.join(directory, '.gitignore');
147
+
148
+ if (!fs.existsSync(gitignorePath)) {
149
+ return [];
150
+ }
151
+
152
+ try {
153
+ const content = fs.readFileSync(gitignorePath, 'utf8');
154
+ return content
155
+ .split('\n')
156
+ .map(line => line.trim())
157
+ .filter(line => line && !line.startsWith('#'));
158
+ } catch (error) {
159
+ console.error(`Warning: Could not read .gitignore: ${error.message}`);
160
+ return [];
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Check if a file path should be ignored based on ignore patterns
166
+ *
167
+ * @param {string} filePath - File path to check
168
+ * @param {string[]} ignorePatterns - Array of ignore patterns
169
+ * @returns {boolean} - Whether the file should be ignored
170
+ */
171
+ function shouldIgnore(filePath, ignorePatterns) {
172
+ if (!ignorePatterns.length) return false;
173
+
174
+ // Simple pattern matching (could be improved with minimatch or similar)
175
+ for (const pattern of ignorePatterns) {
176
+ // Exact match
177
+ if (pattern === filePath) return true;
178
+
179
+ // Directory match (pattern ends with /)
180
+ if (pattern.endsWith('/') && filePath.startsWith(pattern)) return true;
181
+
182
+ // File extension match (pattern starts with *.)
183
+ if (pattern.startsWith('*.') && filePath.endsWith(pattern.substring(1))) return true;
184
+
185
+ // Wildcard at start (pattern starts with *)
186
+ if (pattern.startsWith('*') && filePath.endsWith(pattern.substring(1))) return true;
187
+
188
+ // Wildcard at end (pattern ends with *)
189
+ if (pattern.endsWith('*') && filePath.startsWith(pattern.substring(0, pattern.length - 1))) return true;
190
+ }
191
+
192
+ return false;
193
+ }
package/build/utils.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Utility functions for the probe package
3
+ * @module utils
4
+ */
5
+
6
+ import path from 'path';
7
+ import fs from 'fs-extra';
8
+ import { fileURLToPath } from 'url';
9
+ import { downloadProbeBinary } from './downloader.js';
10
+ import { getPackageBinDir } from './directory-resolver.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ // Note: binDir is now resolved dynamically using getPackageBinDir()
16
+
17
+ // Store the binary path
18
+ let probeBinaryPath = '';
19
+
20
+ /**
21
+ * Get the path to the probe binary, downloading it if necessary
22
+ * @param {Object} options - Options for getting the binary
23
+ * @param {boolean} [options.forceDownload=false] - Force download even if binary exists
24
+ * @param {string} [options.version] - Specific version to download
25
+ * @returns {Promise<string>} - Path to the binary
26
+ */
27
+ export async function getBinaryPath(options = {}) {
28
+ const { forceDownload = false, version } = options;
29
+
30
+ // Return cached path if available and not forcing download
31
+ if (probeBinaryPath && !forceDownload && fs.existsSync(probeBinaryPath)) {
32
+ return probeBinaryPath;
33
+ }
34
+
35
+ // Check environment variable
36
+ if (process.env.PROBE_PATH && fs.existsSync(process.env.PROBE_PATH) && !forceDownload) {
37
+ probeBinaryPath = process.env.PROBE_PATH;
38
+ return probeBinaryPath;
39
+ }
40
+
41
+ // Get dynamic bin directory (handles CI, npx, Docker scenarios)
42
+ const binDir = await getPackageBinDir();
43
+
44
+ // Check bin directory
45
+ const isWindows = process.platform === 'win32';
46
+ const binaryName = isWindows ? 'probe.exe' : 'probe';
47
+ const binaryPath = path.join(binDir, binaryName);
48
+
49
+ if (fs.existsSync(binaryPath) && !forceDownload) {
50
+ probeBinaryPath = binaryPath;
51
+ return probeBinaryPath;
52
+ }
53
+
54
+ // Download if not found or force download
55
+ console.log(`${forceDownload ? 'Force downloading' : 'Binary not found. Downloading'} probe binary...`);
56
+ probeBinaryPath = await downloadProbeBinary(version);
57
+ return probeBinaryPath;
58
+ }
59
+
60
+ /**
61
+ * Manually set the path to the probe binary
62
+ * @param {string} binaryPath - Path to the probe binary
63
+ * @throws {Error} If the binary doesn't exist at the specified path
64
+ */
65
+ export function setBinaryPath(binaryPath) {
66
+ if (!fs.existsSync(binaryPath)) {
67
+ throw new Error(`No binary found at path: ${binaryPath}`);
68
+ }
69
+
70
+ probeBinaryPath = binaryPath;
71
+ }
72
+
73
+ /**
74
+ * Ensure the bin directory exists
75
+ * @returns {Promise<void>}
76
+ */
77
+ export async function ensureBinDirectory() {
78
+ // This function is now handled by getPackageBinDir() which ensures directory exists
79
+ // Keeping for backward compatibility but it's no longer needed
80
+ const binDir = await getPackageBinDir();
81
+ await fs.ensureDir(binDir);
82
+ }
83
+
84
+ /**
85
+ * Build command-line arguments from an options object
86
+ * @param {Object} options - Options object
87
+ * @param {Array<string>} flagMap - Map of option keys to command-line flags
88
+ * @returns {Array<string>} - Array of command-line arguments
89
+ */
90
+ export function buildCliArgs(options, flagMap) {
91
+ const cliArgs = [];
92
+
93
+ for (const [key, flag] of Object.entries(flagMap)) {
94
+ if (key in options) {
95
+ const value = options[key];
96
+
97
+ if (typeof value === 'boolean') {
98
+ if (value) {
99
+ cliArgs.push(flag);
100
+ }
101
+ } else if (Array.isArray(value)) {
102
+ for (const item of value) {
103
+ cliArgs.push(flag, item);
104
+ }
105
+ } else if (value !== undefined && value !== null) {
106
+ cliArgs.push(flag, value.toString());
107
+ }
108
+ }
109
+ }
110
+
111
+ return cliArgs;
112
+ }
113
+
114
+ /**
115
+ * Escape a string for use in a command line
116
+ * @param {string} str - String to escape
117
+ * @returns {string} - Escaped string
118
+ */
119
+ export function escapeString(str) {
120
+ if (process.platform === 'win32') {
121
+ // For Windows PowerShell, escape double quotes and wrap with double quotes
122
+ return `"${str.replace(/"/g, '\\"')}"`;
123
+ } else {
124
+ // Use single quotes for POSIX shells
125
+ // Escape single quotes in the string by replacing ' with '\''
126
+ return `'${str.replace(/'/g, "'\\''")}'`;
127
+ }
128
+ }