@probelabs/probe 0.6.0-rc56
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 +528 -0
- package/bin/.gitkeep +0 -0
- package/bin/probe +0 -0
- package/build/mcp/index.js +499 -0
- package/build/mcp/index.ts +593 -0
- package/package.json +62 -0
- package/scripts/postinstall.js +127 -0
- package/src/cli.js +49 -0
- package/src/downloader.js +571 -0
- package/src/extract.js +147 -0
- package/src/index.js +55 -0
- package/src/mcp/index.ts +593 -0
- package/src/query.js +112 -0
- package/src/search.js +235 -0
- package/src/tools/common.js +224 -0
- package/src/tools/index.js +36 -0
- package/src/tools/langchain.js +88 -0
- package/src/tools/system-message.js +69 -0
- package/src/tools/vercel.js +215 -0
- package/src/utils/file-lister.js +193 -0
- package/src/utils.js +120 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default system message for code intelligence assistants
|
|
3
|
+
* @module tools/system-message
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_SYSTEM_MESSAGE = `[Persona & Objective]
|
|
6
|
+
|
|
7
|
+
You are Probe, a specialized code intelligence assistant. Your objective is to accurately answer questions about multi-language codebases by effectively using your available tools: \`search\`, \`query\`, and \`extract\`.
|
|
8
|
+
|
|
9
|
+
[Core Workflow & Principles]
|
|
10
|
+
|
|
11
|
+
1. **Tool-First Always:** Immediately use tools for any code-related query. Do not guess or use general knowledge.
|
|
12
|
+
2. **Mandatory Path:** ALL tool calls (\`search\`, \`query\`, \`extract\`) MUST include the \`path\` argument. Use \`"."\` for the whole project, specific directories/files (e.g., \`"src/api"\`, \`"pkg/utils/helpers.py"\`), or dependency syntax (e.g., \`"go:github.com/gin-gonic/gin"\`, \`"js:@ai-sdk/anthropic"\`, \`"rust:serde"\`).
|
|
13
|
+
3. **Start with \`search\`:**
|
|
14
|
+
* **Keywords are Key:** Formulate queries like you would in Elasticsearch. Use specific keywords, boolean operators (\`AND\`, \`OR\`, \`NOT\`), and exact phrases (\`""\`). This is NOT a simple text search.
|
|
15
|
+
* **Iterate if Needed:** If initial results are too broad or insufficient, **repeat the exact same \`search\` query** to get the next page of results (pagination). Reuse the \`sessionID\` if provided by the previous identical search. If results are irrelevant, refine the keywords (add terms, use \`NOT\`, try synonyms).
|
|
16
|
+
4. **Analyze & Refine:** Review \`search\` results (snippets, file paths).
|
|
17
|
+
* Use \`query\` if you need code based on *structure* (AST patterns) within specific files/directories identified by \`search\`.
|
|
18
|
+
* Use \`extract\` if \`search\` or \`query\` identified the exact location (file, symbol, line range) and you need the full definition or more context.
|
|
19
|
+
5. **Synthesize & Cite:** Construct the answer *only* from tool outputs. ALWAYS cite the specific file paths and relevant locations (symbols, line numbers) found. Adapt detail to the likely user role (developer vs. PM).
|
|
20
|
+
6. **Clarify Sparingly:** If an initial \`search\` attempt completely fails due to ambiguity, ask a *specific* question to guide the next search. Don't ask before trying a search first.
|
|
21
|
+
|
|
22
|
+
[Tool Reference]
|
|
23
|
+
|
|
24
|
+
* \`search\`
|
|
25
|
+
* **Purpose:** Find relevant code snippets/files using keyword-based search (like Elasticsearch). Locate named symbols. Search project code or dependencies.
|
|
26
|
+
* **Syntax:** \`query\` (Elasticsearch-like string: keywords, \`AND\`, \`OR\`, \`NOT\`, \`""\` exact phrases), \`path\` (Mandatory: \`"."\`, \`"path/to/dir"\`, \`"path/to/file.ext"\`, \`"go:pkg"\`, \`"js:npm_module"\`, \`"rust:crate"\`), \`exact\` (Optional: Set to \`true\` for case-insensitive exact matching without tokenization).
|
|
27
|
+
* **Features:** Returns snippets/paths. Supports pagination (repeat query). Caching via \`sessionID\` (reuse if returned). Use \`exact\` flag when you need precise matching of terms.
|
|
28
|
+
* \`query\`
|
|
29
|
+
* **Purpose:** Find code by its *structure* (AST patterns) within specific files/directories, typically after \`search\`.
|
|
30
|
+
* **Syntax:** \`pattern\` (ast-grep pattern), \`language\` (e.g., "go", "python").
|
|
31
|
+
* **Mandatory Argument:** \`path\` (file or directory path, e.g., \`"src/services"\`, \`"app/main.py"\`).
|
|
32
|
+
* \`extract\`
|
|
33
|
+
* **Purpose:** Retrieve specific code blocks or entire files *after* \`search\` or \`query\` identifies the target.
|
|
34
|
+
* **Syntax:** Optional \`#symbol\` (e.g., \`#MyClass\`), \`#Lstart-Lend\` (e.g., \`#L50-L75\`).
|
|
35
|
+
* **Mandatory Argument:** \`path\` (specific file path, e.g., \`"src/utils/helpers.go"\`, or dependency file like \`"go:github.com/gin-gonic/gin/context.go"\`).
|
|
36
|
+
|
|
37
|
+
[Examples]
|
|
38
|
+
|
|
39
|
+
* **Example 1: Finding a Specific Function Definition**
|
|
40
|
+
* User: "Show me the code for the \`calculate_total\` function in our payments module."
|
|
41
|
+
* Probe Action 1: \`search\` query: \`"calculate_total"\`, path: \`"src/payments"\` (Targeted search in the likely directory)
|
|
42
|
+
* (Analysis: Search returns a clear hit in \`src/payments/logic.py\`.)
|
|
43
|
+
* Probe Action 2: \`extract\` path: \`"src/payments/logic.py#calculate_total"\`
|
|
44
|
+
* (Response: Provide the extracted function code, citing \`src/payments/logic.py#calculate_total\`.)
|
|
45
|
+
|
|
46
|
+
* **Example 2: Investigating Initialization**
|
|
47
|
+
* User: "Where is the primary configuration for the Redis cache loaded?"
|
|
48
|
+
* Probe Action 1: \`search\` query: \`redis AND (config OR load OR init OR setup) NOT test\`, path: \`"."\`
|
|
49
|
+
* (Analysis: Results point towards \`pkg/cache/redis.go\` and a function \`LoadRedisConfig\`.)
|
|
50
|
+
* Probe Action 2: \`extract\` path: \`"pkg/cache/redis.go#LoadRedisConfig"\`
|
|
51
|
+
* (Response: Explain config loading based on the extracted \`LoadRedisConfig\` function, citing \`pkg/cache/redis.go#LoadRedisConfig\`.)
|
|
52
|
+
|
|
53
|
+
* **Example 3: Understanding Usage of a Dependency Feature**
|
|
54
|
+
* User: "How are we using the \`createAnthropic\` function from the \`@ai-sdk/anthropic\` library?"
|
|
55
|
+
* Probe Action 1: \`search\` query: \`"createAnthropic"\`, path: \`"."\` (Search project code for usage)
|
|
56
|
+
* (Analysis: Find usage in \`src/ai/providers.ts\`. Want to understand the library function itself better.)
|
|
57
|
+
* Probe Action 2: \`search\` query: \`"createAnthropic"\`, path: \`"js:@ai-sdk/anthropic"\` (Search within the specific dependency)
|
|
58
|
+
* (Analysis: Search locates the definition within the dependency code, e.g., \`node_modules/@ai-sdk/anthropic/dist/index.js\` or similar mapped path.)
|
|
59
|
+
* Probe Action 3: \`extract\` path: \`"js:@ai-sdk/anthropic/dist/index.js#createAnthropic"\` (Extract the specific function *from the dependency*. Note: Actual file path within dependency might vary, use the one found by search).
|
|
60
|
+
* (Response: Show how \`createAnthropic\` is used in \`src/ai/providers.ts\`, and explain its purpose based on the extracted definition from the \`@ai-sdk/anthropic\` library, citing both files.)
|
|
61
|
+
|
|
62
|
+
* **Example 4: Exploring Error Handling Patterns**
|
|
63
|
+
* User: "What's the standard way errors are wrapped or handled in our Go backend services?"
|
|
64
|
+
* Probe Action 1: \`search\` query: \`error AND (wrap OR handle OR new) AND lang:go NOT test\`, path: \`"service/"\` (Focus on service directories)
|
|
65
|
+
* (Analysis: Many results. See frequent use of \`fmt.Errorf\` and a custom \`errors.Wrap\` in several files like \`service/user/handler.go\`.)
|
|
66
|
+
* Probe Action 2: \`search\` query: \`import AND "pkg/errors"\`, path: \`"service/"\` (Check where a potential custom error package is used)
|
|
67
|
+
* (Analysis: Confirms \`pkg/errors\` is widely used.)
|
|
68
|
+
* Probe Action 3: \`query\` language: \`go\`, pattern: \`errors.Wrap($$$)\`, path: \`"service/"\` (Find structural usage of the custom wrapper)
|
|
69
|
+
* (Response: Summarize error handling: Mention standard \`fmt.Errorf\` and the prevalent use of a custom \`errors.Wrap\` function from \`pkg/errors\`, providing examples from locations found by search/query like \`service/user/handler.go\`.)`
|
|
@@ -0,0 +1,215 @@
|
|
|
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 { searchSchema, querySchema, extractSchema, searchDescription, queryDescription, extractDescription } from './common.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Search tool generator
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} [options] - Configuration options
|
|
16
|
+
* @param {string} [options.sessionId] - Session ID for caching search results
|
|
17
|
+
* @param {number} [options.maxTokens=10000] - Default max tokens
|
|
18
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
19
|
+
* @returns {Object} Configured search tool
|
|
20
|
+
*/
|
|
21
|
+
export const searchTool = (options = {}) => {
|
|
22
|
+
const { sessionId, maxTokens = 10000, debug = false } = options;
|
|
23
|
+
|
|
24
|
+
return tool({
|
|
25
|
+
name: 'search',
|
|
26
|
+
description: searchDescription,
|
|
27
|
+
parameters: searchSchema,
|
|
28
|
+
execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language }) => {
|
|
29
|
+
try {
|
|
30
|
+
// Use parameter maxTokens if provided, otherwise use the default
|
|
31
|
+
const effectiveMaxTokens = paramMaxTokens || maxTokens;
|
|
32
|
+
|
|
33
|
+
// Use the path from parameters if provided, otherwise use defaultPath from config
|
|
34
|
+
let searchPath = path || options.defaultPath || '.';
|
|
35
|
+
|
|
36
|
+
// If path is "." or "./", use the defaultPath if available
|
|
37
|
+
if ((searchPath === "." || searchPath === "./") && options.defaultPath) {
|
|
38
|
+
if (debug) {
|
|
39
|
+
console.error(`Using default path "${options.defaultPath}" instead of "${searchPath}"`);
|
|
40
|
+
}
|
|
41
|
+
searchPath = options.defaultPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (debug) {
|
|
45
|
+
console.error(`Executing search with query: "${searchQuery}", path: "${searchPath}", exact: ${exact ? 'true' : 'false'}, language: ${language || 'all'}, session: ${sessionId || 'none'}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const results = await search({
|
|
49
|
+
query: searchQuery,
|
|
50
|
+
path: searchPath,
|
|
51
|
+
allow_tests,
|
|
52
|
+
exact,
|
|
53
|
+
json: false,
|
|
54
|
+
maxTokens: effectiveMaxTokens,
|
|
55
|
+
session: sessionId, // Pass session ID if provided
|
|
56
|
+
language // Pass language parameter if provided
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return results;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error executing search command:', error);
|
|
62
|
+
return `Error executing search command: ${error.message}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Query tool generator
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} [options] - Configuration options
|
|
72
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
73
|
+
* @returns {Object} Configured query tool
|
|
74
|
+
*/
|
|
75
|
+
export const queryTool = (options = {}) => {
|
|
76
|
+
const { debug = false } = options;
|
|
77
|
+
|
|
78
|
+
return tool({
|
|
79
|
+
name: 'query',
|
|
80
|
+
description: queryDescription,
|
|
81
|
+
parameters: querySchema,
|
|
82
|
+
execute: async ({ pattern, path, language, allow_tests }) => {
|
|
83
|
+
try {
|
|
84
|
+
// Use the path from parameters if provided, otherwise use defaultPath from config
|
|
85
|
+
let queryPath = path || options.defaultPath || '.';
|
|
86
|
+
|
|
87
|
+
// If path is "." or "./", use the defaultPath if available
|
|
88
|
+
if ((queryPath === "." || queryPath === "./") && options.defaultPath) {
|
|
89
|
+
if (debug) {
|
|
90
|
+
console.error(`Using default path "${options.defaultPath}" instead of "${queryPath}"`);
|
|
91
|
+
}
|
|
92
|
+
queryPath = options.defaultPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (debug) {
|
|
96
|
+
console.error(`Executing query with pattern: "${pattern}", path: "${queryPath}", language: ${language || 'auto'}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const results = await query({
|
|
100
|
+
pattern,
|
|
101
|
+
path: queryPath,
|
|
102
|
+
language,
|
|
103
|
+
allow_tests,
|
|
104
|
+
json: false
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return results;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Error executing query command:', error);
|
|
110
|
+
return `Error executing query command: ${error.message}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract tool generator
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} [options] - Configuration options
|
|
120
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
121
|
+
* @returns {Object} Configured extract tool
|
|
122
|
+
*/
|
|
123
|
+
export const extractTool = (options = {}) => {
|
|
124
|
+
const { debug = false } = options;
|
|
125
|
+
|
|
126
|
+
return tool({
|
|
127
|
+
name: 'extract',
|
|
128
|
+
description: extractDescription,
|
|
129
|
+
parameters: extractSchema,
|
|
130
|
+
execute: async ({ file_path, input_content, line, end_line, allow_tests, context_lines, format }) => {
|
|
131
|
+
try {
|
|
132
|
+
// Use the defaultPath from config for context
|
|
133
|
+
let extractPath = options.defaultPath || '.';
|
|
134
|
+
|
|
135
|
+
// If path is "." or "./", use the defaultPath if available
|
|
136
|
+
if ((extractPath === "." || extractPath === "./") && options.defaultPath) {
|
|
137
|
+
if (debug) {
|
|
138
|
+
console.error(`Using default path "${options.defaultPath}" instead of "${extractPath}"`);
|
|
139
|
+
}
|
|
140
|
+
extractPath = options.defaultPath;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (debug) {
|
|
144
|
+
if (file_path) {
|
|
145
|
+
console.error(`Executing extract with file: "${file_path}", path: "${extractPath}", context lines: ${context_lines || 10}`);
|
|
146
|
+
} else if (input_content) {
|
|
147
|
+
console.error(`Executing extract with input content, path: "${extractPath}", context lines: ${context_lines || 10}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create a temporary file for input content if provided
|
|
152
|
+
let tempFilePath = null;
|
|
153
|
+
let extractOptions = { path: extractPath };
|
|
154
|
+
|
|
155
|
+
if (input_content) {
|
|
156
|
+
// Import required modules
|
|
157
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
158
|
+
const { join } = await import('path');
|
|
159
|
+
const { tmpdir } = await import('os');
|
|
160
|
+
const { randomUUID } = await import('crypto');
|
|
161
|
+
|
|
162
|
+
// Create a temporary file with the input content
|
|
163
|
+
tempFilePath = join(tmpdir(), `probe-extract-${randomUUID()}.txt`);
|
|
164
|
+
writeFileSync(tempFilePath, input_content);
|
|
165
|
+
|
|
166
|
+
if (debug) {
|
|
167
|
+
console.error(`Created temporary file for input content: ${tempFilePath}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Set up extract options with input file
|
|
171
|
+
extractOptions = {
|
|
172
|
+
inputFile: tempFilePath,
|
|
173
|
+
allowTests: allow_tests,
|
|
174
|
+
contextLines: context_lines,
|
|
175
|
+
format
|
|
176
|
+
};
|
|
177
|
+
} else if (file_path) {
|
|
178
|
+
// Parse file_path to handle line numbers and symbol names
|
|
179
|
+
const files = [file_path];
|
|
180
|
+
|
|
181
|
+
// Set up extract options with files
|
|
182
|
+
extractOptions = {
|
|
183
|
+
files,
|
|
184
|
+
allowTests: allow_tests,
|
|
185
|
+
contextLines: context_lines,
|
|
186
|
+
format
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error('Either file_path or input_content must be provided');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Execute the extract command
|
|
193
|
+
const results = await extract(extractOptions);
|
|
194
|
+
|
|
195
|
+
// Clean up temporary file if created
|
|
196
|
+
if (tempFilePath) {
|
|
197
|
+
const { unlinkSync } = await import('fs');
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync(tempFilePath);
|
|
200
|
+
if (debug) {
|
|
201
|
+
console.error(`Removed temporary file: ${tempFilePath}`);
|
|
202
|
+
}
|
|
203
|
+
} catch (cleanupError) {
|
|
204
|
+
console.error(`Warning: Failed to remove temporary file: ${cleanupError.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Error executing extract command:', error);
|
|
211
|
+
return `Error executing extract command: ${error.message}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
};
|
|
@@ -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/src/utils.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const binDir = path.resolve(__dirname, '..', 'bin');
|
|
14
|
+
|
|
15
|
+
// Store the binary path
|
|
16
|
+
let probeBinaryPath = '';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the path to the probe binary, downloading it if necessary
|
|
20
|
+
* @param {Object} options - Options for getting the binary
|
|
21
|
+
* @param {boolean} [options.forceDownload=false] - Force download even if binary exists
|
|
22
|
+
* @param {string} [options.version] - Specific version to download
|
|
23
|
+
* @returns {Promise<string>} - Path to the binary
|
|
24
|
+
*/
|
|
25
|
+
export async function getBinaryPath(options = {}) {
|
|
26
|
+
const { forceDownload = false, version } = options;
|
|
27
|
+
|
|
28
|
+
// Return cached path if available and not forcing download
|
|
29
|
+
if (probeBinaryPath && !forceDownload && fs.existsSync(probeBinaryPath)) {
|
|
30
|
+
return probeBinaryPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check environment variable
|
|
34
|
+
if (process.env.PROBE_PATH && fs.existsSync(process.env.PROBE_PATH) && !forceDownload) {
|
|
35
|
+
probeBinaryPath = process.env.PROBE_PATH;
|
|
36
|
+
return probeBinaryPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check bin directory
|
|
40
|
+
const isWindows = process.platform === 'win32';
|
|
41
|
+
const binaryName = isWindows ? 'probe.exe' : 'probe';
|
|
42
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(binaryPath) && !forceDownload) {
|
|
45
|
+
probeBinaryPath = binaryPath;
|
|
46
|
+
return probeBinaryPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Download if not found or force download
|
|
50
|
+
console.log(`${forceDownload ? 'Force downloading' : 'Binary not found. Downloading'} probe binary...`);
|
|
51
|
+
probeBinaryPath = await downloadProbeBinary(version);
|
|
52
|
+
return probeBinaryPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Manually set the path to the probe binary
|
|
57
|
+
* @param {string} binaryPath - Path to the probe binary
|
|
58
|
+
* @throws {Error} If the binary doesn't exist at the specified path
|
|
59
|
+
*/
|
|
60
|
+
export function setBinaryPath(binaryPath) {
|
|
61
|
+
if (!fs.existsSync(binaryPath)) {
|
|
62
|
+
throw new Error(`No binary found at path: ${binaryPath}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
probeBinaryPath = binaryPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ensure the bin directory exists
|
|
70
|
+
* @returns {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
export async function ensureBinDirectory() {
|
|
73
|
+
await fs.ensureDir(binDir);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build command-line arguments from an options object
|
|
78
|
+
* @param {Object} options - Options object
|
|
79
|
+
* @param {Array<string>} flagMap - Map of option keys to command-line flags
|
|
80
|
+
* @returns {Array<string>} - Array of command-line arguments
|
|
81
|
+
*/
|
|
82
|
+
export function buildCliArgs(options, flagMap) {
|
|
83
|
+
const cliArgs = [];
|
|
84
|
+
|
|
85
|
+
for (const [key, flag] of Object.entries(flagMap)) {
|
|
86
|
+
if (key in options) {
|
|
87
|
+
const value = options[key];
|
|
88
|
+
|
|
89
|
+
if (typeof value === 'boolean') {
|
|
90
|
+
if (value) {
|
|
91
|
+
cliArgs.push(flag);
|
|
92
|
+
}
|
|
93
|
+
} else if (Array.isArray(value)) {
|
|
94
|
+
for (const item of value) {
|
|
95
|
+
cliArgs.push(flag, item);
|
|
96
|
+
}
|
|
97
|
+
} else if (value !== undefined && value !== null) {
|
|
98
|
+
cliArgs.push(flag, value.toString());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return cliArgs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Escape a string for use in a command line
|
|
108
|
+
* @param {string} str - String to escape
|
|
109
|
+
* @returns {string} - Escaped string
|
|
110
|
+
*/
|
|
111
|
+
export function escapeString(str) {
|
|
112
|
+
if (process.platform === 'win32') {
|
|
113
|
+
// For Windows PowerShell, escape double quotes and wrap with double quotes
|
|
114
|
+
return `"${str.replace(/"/g, '\\"')}"`;
|
|
115
|
+
} else {
|
|
116
|
+
// Use single quotes for POSIX shells
|
|
117
|
+
// Escape single quotes in the string by replacing ' with '\''
|
|
118
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
119
|
+
}
|
|
120
|
+
}
|