simple-task-master
Version:
A simple command-line task management tool
207 lines • 8.3 kB
JavaScript
;
/**
* 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