Skip to content
Merged
137 changes: 105 additions & 32 deletions extensions/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion extensions/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@sentry/profiling-node": "^9.43.0",
"fdir": "^6.4.2",
"find-up": "^8.0.0",
"fzf": "^0.5.2",
"js-yaml": "^4.1.1"
},
Expand Down Expand Up @@ -87,6 +88,7 @@
"@vitest/ui": "^3.2.4",
"@workos-inc/node": "^7.45.0",
"chalk": "^5.4.1",
"clipboardy": "^4.0.0",
"commander": "^14.0.0",
"conventional-changelog-conventionalcommits": "^9.1.0",
"core": "file:../../core",
Expand All @@ -100,7 +102,6 @@
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-unused-imports": "^4.1.4",
"execa": "^9.6.0",
"clipboardy": "^4.0.0",
"express": "^5.1.0",
"glob": "^11.0.3",
"gpt-tokenizer": "^3.0.1",
Expand Down
103 changes: 86 additions & 17 deletions extensions/cli/src/tools/searchCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ import * as fs from "fs";
import * as util from "util";

import { ContinueError, ContinueErrorReason } from "core/util/errors.js";
import { findUp } from "find-up";

import { Tool } from "./types.js";

const execPromise = util.promisify(child_process.exec);

// Default maximum number of results to display
const DEFAULT_MAX_RESULTS = 100;
const MAX_LINE_LENGTH = 1000;
async function getGitignorePatterns() {
const gitIgnorePath = await findUp(".gitignore");
if (!gitIgnorePath) return [];
const content = fs.readFileSync(gitIgnorePath, "utf-8");
const ignorePatterns = [];
for (let line of content.trim().split("\n")) {
line = line.trim();
if (line.startsWith("#") || line === "") continue; // ignore comments and empty line
if (line.startsWith("!")) continue; // ignore negated ignores
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negated entries in .gitignore are now ignored entirely, so files explicitly re-included with !pattern are still excluded from both ripgrep and grep/findstr searches, breaking expected .gitignore override behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/tools/searchCode.ts, line 20:

<comment>Negated entries in .gitignore are now ignored entirely, so files explicitly re-included with `!pattern` are still excluded from both ripgrep and grep/findstr searches, breaking expected .gitignore override behavior.</comment>

<file context>
@@ -9,25 +9,18 @@ import { Tool } from &quot;./types.js&quot;;
-      ignorePatterns.push(line);
-    }
+    if (line.startsWith(&quot;#&quot;) || line === &quot;&quot;) continue; // ignore comments and empty line
+    if (line.startsWith(&quot;!&quot;)) continue; // ignore negated ignores
+    ignorePatterns.push(line);
   }
</file context>
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is done, for ex. --include={myNegatedFolder}, both grep and ripgrep will search in that folder only.

ignorePatterns.push(line);
}
return ignorePatterns;
}

// procedure 1: search with ripgrep
export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
try {
await execPromise("rg --version");
Expand All @@ -21,6 +33,57 @@ export async function checkIfRipgrepIsInstalled(): Promise<boolean> {
}
}

async function searchWithRipgrep(
pattern: string,
searchPath: string,
filePattern?: string,
) {
let command = `rg --line-number --with-filename --color never "${pattern}"`;

if (filePattern) {
command += ` -g "${filePattern}"`;
}

const ignorePatterns = await getGitignorePatterns();
for (const ignorePattern of ignorePatterns) {
command += ` -g "!${ignorePattern}"`;
}

command += ` "${searchPath}"`;
const { stdout, stderr } = await execPromise(command);
return { stdout, stderr };
}

// procedure 2: search with grep on unix or findstr on windows
async function searchWithGrepOrFindstr(
pattern: string,
searchPath: string,
filePattern?: string,
) {
const isWindows = process.platform === "win32";
const ignorePatterns = await getGitignorePatterns();
let command: string;
if (isWindows) {
const fileSpec = filePattern ? filePattern : "*";
command = `findstr /S /N /P /R "${pattern}" "${fileSpec}"`; // findstr does not support ignoring patterns
} else {
let excludeArgs = "";
for (const ignorePattern of ignorePatterns) {
excludeArgs += ` --exclude="${ignorePattern}" --exclude-dir="${ignorePattern}"`; // use both exclude and exclude-dir because ignorePattern can be a file or directory
}
if (filePattern) {
command = `find . -type f -path "${filePattern}" -print0 | xargs -0 grep -nH -I${excludeArgs} "${pattern}"`;
} else {
command = `grep -R -n -H -I${excludeArgs} "${pattern}" .`;
}
}
return await execPromise(command, { cwd: searchPath });
}

// Default maximum number of results to display
const DEFAULT_MAX_RESULTS = 100;
const MAX_LINE_LENGTH = 1000;

export const searchCodeTool: Tool = {
name: "Search",
displayName: "Search",
Expand Down Expand Up @@ -73,15 +136,26 @@ export const searchCodeTool: Tool = {
);
}

let command = `rg --line-number --with-filename --color never "${args.pattern}"`;

if (args.file_pattern) {
command += ` -g "${args.file_pattern}"`;
}

command += ` "${searchPath}"`;
let stdout = "",
stderr = "";
try {
const { stdout, stderr } = await execPromise(command);
if (await checkIfRipgrepIsInstalled()) {
const results = await searchWithRipgrep(
args.pattern,
searchPath,
args.file_pattern,
);
stdout = results.stdout;
stderr = results.stderr;
} else {
const results = await searchWithGrepOrFindstr(
args.pattern,
searchPath,
args.file_pattern,
);
stdout = results.stdout;
stderr = results.stderr;
}

if (stderr) {
return `Warning during search: ${stderr}\n\n${stdout}`;
Expand Down Expand Up @@ -121,13 +195,8 @@ export const searchCodeTool: Tool = {
args.file_pattern ? ` in files matching "${args.file_pattern}"` : ""
}.`;
}
if (error instanceof Error) {
if (error.message.includes("command not found")) {
throw new Error(`ripgrep is not installed.`);
}
}
throw new Error(
`Error executing ripgrep: ${
`Error executing search: ${
error instanceof Error ? error.message : String(error)
}`,
);
Expand Down
Loading