From e02213555de3e148c4b306777035cc663347a576 Mon Sep 17 00:00:00 2001 From: Marcelo Melogno Date: Sat, 4 Jul 2026 15:12:07 -0300 Subject: [PATCH 1/3] =?UTF-8?q?debug(grep-tool):=20refactorizaci=C3=B3n=20?= =?UTF-8?q?de=20grep-tool=20para=20solucionar=20error=20en=20entorno=20win?= =?UTF-8?q?dows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La tool grep es una herramienta de línea de comandos que permite buscar patrones en archivos y directorios. Se ha refactorizado para mejorar su compatibilidad con entornos Windows, corrigiendo el uso de funciones específicas de Unix que causaban errores en sistemas Windows. Ahora, la herramienta utiliza métodos más universales para manejar rutas y archivos, asegurando un funcionamiento correcto en ambos entornos. --- package.json | 1 + packages/server/src/tools/grep.ts | 156 +++++++++++++++++++++--------- 2 files changed, 112 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 69cbf61..867cb76 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "$schema": "https://json.schemastore.org/package.json", "name": "whalincode", "private": true, "version": "1.0.0", diff --git a/packages/server/src/tools/grep.ts b/packages/server/src/tools/grep.ts index dfb71ed..1fbfb0e 100644 --- a/packages/server/src/tools/grep.ts +++ b/packages/server/src/tools/grep.ts @@ -1,9 +1,65 @@ -import { resolve, relative } from 'path'; +import { resolve, relative, join } from 'path'; import { tool } from 'ai'; import { z } from 'zod'; const MAX_MATCHES = 50; +const SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', '.DS_Store']); + +const BINARY_EXTENSIONS = new Set([ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.bmp', + '.ico', + '.webp', + '.svg', + '.woff', + '.woff2', + '.ttf', + '.eot', + '.otf', + '.zip', + '.tar', + '.gz', + '.bz2', + '.7z', + '.rar', + '.exe', + '.dll', + '.so', + '.dylib', + '.bin', + '.pdf', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.mp3', + '.mp4', + '.avi', + '.mov', + '.wav', + '.lock', +]); + +function isBinaryPath(filePath: string): boolean { + const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase(); + return BINARY_EXTENSIONS.has(ext); +} + +function isHiddenPath(filePath: string): boolean { + return filePath + .split(/[\\/]/) + .some((segment) => segment.startsWith('.') && segment !== '.' && segment !== '..'); +} + +function shouldSkip(filePath: string): boolean { + const segments = filePath.split(/[\\/]/); + return segments.some((s) => SKIP_DIRS.has(s)); +} + export function createGrepTool(cwd: string) { return tool({ description: @@ -29,69 +85,79 @@ export function createGrepTool(cwd: string) { } try { - const args = [ - '-rn', - '--color=never', - '--exclude-dir=node_modules', - '--exclude-dir=git', - '-E', - ]; - - if (include) { - args.push(`--include=${include}`); - } - - args.push(pattern, resolved); + const regex = new RegExp(pattern); - const proc = Bun.spawn(['grep', ...args], { - stdout: 'pipe', - stderr: 'pipe', - cwd, - }); + // Scan all files in the target directory + const scanGlob = new Bun.Glob(include ?? '**/*'); + const filePaths: string[] = []; - const stdout = await new Response(proc.stdout).text(); - const stderr = await new Response(proc.stderr).text(); - - await proc.exited; - - // grep exits with 1 when no matches found - not an error - if (proc.exitCode !== 0 && proc.exitCode !== 1) { - return { error: `grep failed: ${stderr.trim()}` }; + for await (const entry of scanGlob.scan({ + cwd: resolved, + dot: false, + onlyFiles: true, + })) { + if (!shouldSkip(entry) && !isHiddenPath(entry) && !isBinaryPath(entry)) { + filePaths.push(entry); + } } - if (!stdout.trim()) { - return { matches: [], message: 'No matches found' }; - } + filePaths.sort(); - const lines = stdout.trim().split('\n'); const matches: { file: string; line: number; content: string }[] = []; let truncated = false; + let totalMatches = 0; + + for (const filePath of filePaths) { + if (truncated) break; - for (const line of lines) { - if (matches.length >= MAX_MATCHES) { - truncated = true; - break; + const absolutePath = join(resolved, filePath); + let text: string; + + try { + const file = Bun.file(absolutePath); + text = await file.text(); + } catch { + // Skip files that can't be read + continue; } - // grep output format; /absolute/path:linenum:content - const match = line.match(/^(.+?):(\d+):(.*)$/); - if (match) { - matches.push({ - file: relative(cwd, match[1]!), - line: parseInt(match[2]!, 10), - content: match[3]!, - }); + const lines = text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i]!)) { + totalMatches++; + + if (matches.length < MAX_MATCHES) { + matches.push({ + file: relative(cwd, absolutePath), + line: i + 1, + content: lines[i]!.trimEnd(), + }); + } else { + truncated = true; + } + } } } + if (matches.length === 0) { + return { matches: [], message: 'No matches found' }; + } + return { matches, - ...(truncated ? { truncated: true, totalMatches: lines.length } : {}), + ...(truncated ? { truncated: true, totalMatches } : {}), }; } catch (error) { const message = error instanceof Error ? error.message : String(error); + + // Check if it's an invalid regex + if (error instanceof SyntaxError) { + return { error: `Invalid regex pattern: ${message}` }; + } + return { - error: `Failed to execute command: ${message}`, + error: `Failed to search files: ${message}`, }; } }, From 1ebaf834f958361c91bd02e674c05d8ca024b72a Mon Sep 17 00:00:00 2001 From: Marcelo Melogno Date: Sat, 4 Jul 2026 18:17:55 -0300 Subject: [PATCH 2/3] debug(grep-tool): implementa algunas correciones extras sugeridas por coderabbit --- packages/server/src/tools/grep.ts | 76 +++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/server/src/tools/grep.ts b/packages/server/src/tools/grep.ts index 1fbfb0e..4c17281 100644 --- a/packages/server/src/tools/grep.ts +++ b/packages/server/src/tools/grep.ts @@ -1,4 +1,5 @@ import { resolve, relative, join } from 'path'; +import { readdir } from 'fs/promises'; import { tool } from 'ai'; import { z } from 'zod'; @@ -55,9 +56,54 @@ function isHiddenPath(filePath: string): boolean { .some((segment) => segment.startsWith('.') && segment !== '.' && segment !== '..'); } -function shouldSkip(filePath: string): boolean { - const segments = filePath.split(/[\\/]/); - return segments.some((s) => SKIP_DIRS.has(s)); +/** + * Recursively walk a directory, pruning skip/hidden directories during + * traversal so we never descend into node_modules (etc.). Returns file paths + * relative to `root`. An optional glob matcher filters files by pattern. + */ +async function walkFiles( + root: string, + matcher: Bun.Glob | null, + dir = root, + results: string[] = [], +): Promise { + let entries; + + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return results; + } + + for (const entry of entries) { + const name = entry.name; + + if (entry.isDirectory()) { + // Prune excluded and hidden directories up front so we never + // traverse into them (e.g. node_modules, .git). + if (SKIP_DIRS.has(name) || name.startsWith('.')) { + continue; + } + await walkFiles(root, matcher, join(dir, name), results); + continue; + } + + if (!entry.isFile()) { + continue; + } + + const relPath = relative(root, join(dir, name)); + + if (matcher && !matcher.match(relPath)) { + continue; + } + + if (!isHiddenPath(relPath) && !isBinaryPath(relPath)) { + results.push(relPath); + } + } + + return results; } export function createGrepTool(cwd: string) { @@ -87,25 +133,16 @@ export function createGrepTool(cwd: string) { try { const regex = new RegExp(pattern); - // Scan all files in the target directory - const scanGlob = new Bun.Glob(include ?? '**/*'); - const filePaths: string[] = []; - - for await (const entry of scanGlob.scan({ - cwd: resolved, - dot: false, - onlyFiles: true, - })) { - if (!shouldSkip(entry) && !isHiddenPath(entry) && !isBinaryPath(entry)) { - filePaths.push(entry); - } - } + // Scan all files in the target directory, pruning + // node_modules and hidden directories during traversal. + const matcher = include ? new Bun.Glob(include) : null; + const filePaths = await walkFiles(resolved, matcher); filePaths.sort(); const matches: { file: string; line: number; content: string }[] = []; let truncated = false; - let totalMatches = 0; + let scanedMatches = 0; for (const filePath of filePaths) { if (truncated) break; @@ -125,7 +162,7 @@ export function createGrepTool(cwd: string) { for (let i = 0; i < lines.length; i++) { if (regex.test(lines[i]!)) { - totalMatches++; + scanedMatches++; if (matches.length < MAX_MATCHES) { matches.push({ @@ -135,6 +172,7 @@ export function createGrepTool(cwd: string) { }); } else { truncated = true; + break; } } } @@ -146,7 +184,7 @@ export function createGrepTool(cwd: string) { return { matches, - ...(truncated ? { truncated: true, totalMatches } : {}), + ...(truncated ? { truncated: true, scanedMatches } : {}), }; } catch (error) { const message = error instanceof Error ? error.message : String(error); From 78849f532dcedade720237d883a8d90f671c6512 Mon Sep 17 00:00:00 2001 From: Marcelo Melogno Date: Sun, 5 Jul 2026 15:08:53 -0300 Subject: [PATCH 3/3] debug/(bash & deploy): Refactorizo bash.ts para convertirlo en multiplataforma. Agrego comando start en server y root package.json para deploy --- package.json | 8 ++++++- packages/server/package.json | 1 + packages/server/src/tools/bash.ts | 39 +++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 867cb76..190e235 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,16 @@ "packages/*" ], "scripts": { + "start": "bun run --cwd packages/server start", "dev:cli": "bun run --watch packages/cli/src/index.tsx", "dev:server": "bun run --hot packages/server/src/index.ts", "format": "prettier . --write", - "format:check": "prettier . --check" + "format:check": "prettier . --check", + "typecheck": "bun run typecheck:cli && bun run typecheck:server && bun run typecheck:database && bun run typecheck:shared", + "typecheck:cli": "tsc --noEmit -p packages/cli/tsconfig.json", + "typecheck:server": "tsc --noEmit -p packages/server/tsconfig.json", + "typecheck:database": "tsc --noEmit -p packages/database/tsconfig.json", + "typecheck:shared": "tsc --noEmit -p packages/shared/tsconfig.json" }, "devDependencies": { "@types/node": "^25.9.1", diff --git a/packages/server/package.json b/packages/server/package.json index eb530b3..2a3b1b8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -7,6 +7,7 @@ ".": "./src/index.ts" }, "scripts": { + "start": "bun run src/index.ts", "dev": "bun run --hot src/index.ts", "build": "bun build src/index.ts --outdir dist --target bun", "postinstall": "bun run --cwd ../database db:generate" diff --git a/packages/server/src/tools/bash.ts b/packages/server/src/tools/bash.ts index 73cbb0e..d404dfc 100644 --- a/packages/server/src/tools/bash.ts +++ b/packages/server/src/tools/bash.ts @@ -1,13 +1,34 @@ +import { platform } from 'os'; import { tool } from 'ai'; import { z } from 'zod'; const MAX_OUTPUT = 20_000; const DEFAULT_TIMEOUT = 30_000; +/** + * Resolve the shell invocation for the current platform so commands run + * consistently across Windows, macOS and Linux. + * + * - On Windows we prefer PowerShell (available by default on modern Windows) + * and fall back to cmd.exe when it isn't present. + * - On Unix-like systems we use the user's $SHELL, falling back to /bin/sh + * which is guaranteed to exist. + */ +function resolveShell(command: string): string[] { + if (platform() === 'win32') { + const comspec = process.env.ComSpec ?? 'cmd.exe'; + // /d skips AutoRun, /s + /c preserves quoting, /c runs and exits. + return [comspec, '/d', '/s', '/c', command]; + } + + const shell = process.env.SHELL ?? '/bin/sh'; + return [shell, '-c', command]; +} + export function createBashTool(cwd: string) { return tool({ description: - 'Execute a shell command in the project. Use this for running tests, builds, git operations, package installs, and any other shell commands.', + 'Execute a shell command in the project. Use this for running tests, builds, git operations, package installs, and any other shell commands. Runs on the platform-native shell (PowerShell/cmd on Windows, sh on Unix).', inputSchema: z.object({ command: z.string().describe('The shell command to execute'), timeout: z @@ -18,8 +39,10 @@ export function createBashTool(cwd: string) { ), }), execute: async ({ command, timeout }) => { + let timedOut = false; + try { - const proc = Bun.spawn(['bash', '-c', command], { + const proc = Bun.spawn(resolveShell(command), { cwd, stdout: 'pipe', stderr: 'pipe', @@ -27,9 +50,12 @@ export function createBashTool(cwd: string) { }); const timer = setTimeout(() => { + timedOut = true; proc.kill(); }, timeout); + const response = new Response(proc.stdout); + const [stdout, stderr] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), @@ -43,6 +69,15 @@ export function createBashTool(cwd: string) { ? s.slice(0, MAX_OUTPUT) + `\n... (truncated, ${s.length} total chars)` : s; + if (timedOut) { + return { + stdout: truncate(stdout), + stderr: truncate(stderr), + exitCode, + error: `Command timed out after ${timeout}ms and was terminated.`, + }; + } + return { stdout: truncate(stdout), stderr: truncate(stderr),