UNPKG

simple-task-master

Version:
207 lines 8.3 kB
"use strict"; /** * Search tasks command (grep functionality) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.grepCommand = void 0; exports.grepTasks = grepTasks; const commander_1 = require("commander"); const task_manager_1 = require("../lib/task-manager"); const output_1 = require("../lib/output"); const errors_1 = require("../lib/errors"); /** * Highlight matches in text using ANSI escape codes */ function highlightMatches(text, regex) { if (!process.stdout.isTTY || process.env.NODE_ENV === 'test') { return text; // No highlighting for non-TTY output or in test mode } const highlightStart = '\x1b[43m\x1b[30m'; // Yellow background, black text const highlightEnd = '\x1b[0m'; // Reset return text.replace(regex, (match) => `${highlightStart}${match}${highlightEnd}`); } /** * Extract context lines around matching lines in content */ function extractContextLines(content, regex, contextLines) { if (contextLines === 0) { return content; } const lines = content.split('\n'); const matchingLineNumbers = new Set(); // Find all lines that match the pattern lines.forEach((line, index) => { if (regex.test(line)) { matchingLineNumbers.add(index); } }); if (matchingLineNumbers.size === 0) { return content; } // Collect all lines that should be included (matches + context) const linesToInclude = new Set(); matchingLineNumbers.forEach((lineNum) => { // Add the matching line linesToInclude.add(lineNum); // Add context lines before for (let i = Math.max(0, lineNum - contextLines); i < lineNum; i++) { linesToInclude.add(i); } // Add context lines after for (let i = lineNum + 1; i <= Math.min(lines.length - 1, lineNum + contextLines); i++) { linesToInclude.add(i); } }); // Sort the line numbers and extract the lines const sortedLineNumbers = Array.from(linesToInclude).sort((a, b) => a - b); // Build the result with line numbers and separators const result = []; let lastLineNum = -2; sortedLineNumbers.forEach((lineNum) => { // Add separator if there's a gap if (lineNum > lastLineNum + 1 && result.length > 0) { result.push('--'); } const line = lines[lineNum]; if (line !== undefined) { result.push(line); } lastLineNum = lineNum; }); return result.join('\n'); } /** * Create a task object with highlighted matches for ND-JSON output */ function createHighlightedTask(task, regex, titleMatch, contentMatch, contextLines = 0) { const highlighted = { ...task }; if (titleMatch) { highlighted.title = highlightMatches(task.title, regex); } if (contentMatch && task.content) { // Reset regex for context extraction const contextRegex = new RegExp(regex.source, regex.flags); const contextContent = extractContextLines(task.content, contextRegex, contextLines); highlighted.content = highlightMatches(contextContent, regex); } return highlighted; } /** * Search for tasks matching a pattern */ async function grepTasks(pattern, options) { try { const taskManager = await task_manager_1.TaskManager.create(); // Parse context option const contextLines = options.context ? parseInt(options.context, 10) : 0; if (options.context && (isNaN(contextLines) || contextLines < 0)) { throw new errors_1.ValidationError('Context must be a non-negative number'); } // Get all tasks (include content for searching) const allTasks = await taskManager.list(); // Read content for each task const tasksWithContent = await Promise.all(allTasks.map(async (task) => { try { const fullTask = await taskManager.get(task.id); return fullTask; } catch { return task; // Fallback to task without content } })); // Handle empty pattern if (!pattern || pattern.trim() === '') { (0, output_1.printError)(`No tasks found matching pattern: ${pattern}`); process.exit(1); } // Create regex pattern with global flag for highlighting const flags = options.ignoreCase ? 'gi' : 'g'; let regex; try { regex = new RegExp(pattern, flags); } catch { throw new errors_1.ValidationError(`Invalid regular expression: ${pattern}`); } // Filter tasks and track match locations const matchingTasks = []; for (const task of tasksWithContent) { let titleMatch = false; let contentMatch = false; // Search in title if (!options.contentOnly && regex.test(task.title)) { titleMatch = true; } // Search in content if (!options.titleOnly && task.content && regex.test(task.content)) { contentMatch = true; } if (titleMatch || contentMatch) { // Reset regex for highlighting regex.lastIndex = 0; const highlightedTask = createHighlightedTask(task, regex, titleMatch, contentMatch, contextLines); matchingTasks.push(highlightedTask); } } // Handle no matches if (matchingTasks.length === 0) { (0, output_1.printError)(`No tasks found matching pattern: ${pattern}`); process.exit(1); } // Determine output format let format = 'ndjson'; // default if (options.pretty) { format = 'table'; } if (options.format) { const validFormats = ['ndjson', 'json', 'table', 'csv', 'yaml']; if (!validFormats.includes(options.format)) { throw new errors_1.ValidationError(`Invalid format: ${options.format}. Valid formats: ${validFormats.join(', ')}`); } format = options.format; } // Format and output const output = (0, output_1.formatTasks)(matchingTasks, format); (0, output_1.printOutput)(output); // Print summary to stderr only in interactive mode if (matchingTasks.length > 0 && process.stdout.isTTY) { const summary = `Found ${matchingTasks.length} matching task${matchingTasks.length === 1 ? '' : 's'}`; process.stderr.write(`\n${summary}\n`); } } catch (error) { if (error instanceof errors_1.ValidationError || error instanceof errors_1.FileSystemError || error instanceof errors_1.ConfigurationError || error instanceof Error) { (0, output_1.printError)(error.message); process.exit(1); } throw error; } } /** * Create the grep command */ exports.grepCommand = new commander_1.Command('grep') .description('Search tasks by pattern (supports regular expressions)') .argument('<pattern>', 'Search pattern (regular expression)') .option('-i, --ignore-case', 'Case-insensitive search') .option('--title-only', 'Search only in task titles') .option('--content-only', 'Search only in task content') .option('--context <lines>', 'Show lines of context around matches') .option('-p, --pretty', 'Pretty table output format') .option('-f, --format <format>', 'Output format (ndjson, json, table, csv, yaml)') .addHelpText('after', ` Examples: stm grep "urgent" # Search for "urgent" in titles and content stm grep -i "TODO" # Case-insensitive search for "TODO" stm grep --title-only "^Fix" # Search only titles starting with "Fix" stm grep --content-only "bug.*fix" # Search only content for bug fix patterns stm grep --context 2 "bug" # Show 2 lines of context around matches stm grep -p "feature" # Pretty table output stm grep -f json "error" # JSON output format`) .action(async (pattern, options) => { await grepTasks(pattern, options); }); //# sourceMappingURL=grep.js.map