Skip to content

Instantly share code, notes, and snippets.

@lukeramsden
Created February 2, 2026 23:44
Show Gist options
  • Select an option

  • Save lukeramsden/db8e76637b7f6e188e00c45b113e97d4 to your computer and use it in GitHub Desktop.

Select an option

Save lukeramsden/db8e76637b7f6e188e00c45b113e97d4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/* eslint-disable no-console -- this script intentionally prints CLI output */
/**
* nx-filtered: Runs nx commands and only outputs tasks with failures or warnings.
*
* Usage: nx-filtered [nx arguments...]
* Example: nx-filtered run-many -t build,lint,typecheck-all
*
* This script runs nx commands, captures all output, and only displays
* output from tasks that have errors or warnings. Successful tasks with
* clean output are hidden to reduce noise.
*/
import { spawn } from 'child_process';
import { clearLine, createInterface, cursorTo } from 'readline';
interface TaskOutput {
taskId: string;
lines: string[];
seenLines: Set<string>;
hasError: boolean;
hasWarning: boolean;
isCacheHit: boolean;
}
const ERROR_PATTERNS = [
/\berror\b/i,
/\bERR\b/,
/\bfailed\b/i,
/\bexception\b/i,
//,
//,
/\berror\[/i,
/TS\d{4,5}:/,
/ESLint:/,
];
const WARNING_PATTERNS = [
/\bwarning\b/i,
/\bWARN\b/,
//,
/\bdeprecated\b/i,
];
const IGNORE_LINE_PATTERNS = [
/^\s*$/,
/^\s*Licensed to/,
/^\s*>\s*$/,
];
const STATUS_LINE_PATTERNS = [
/^\s*NX\s+/,
/^>\s*nx run\s+/,
];
function hasError(line: string): boolean {
// Don't count lines that are just showing what command ran
if (line.startsWith('>')) return false;
return ERROR_PATTERNS.some((pattern) => pattern.test(line));
}
function hasWarning(line: string): boolean {
if (line.startsWith('>')) return false;
return WARNING_PATTERNS.some((pattern) => pattern.test(line));
}
function shouldIgnoreLine(line: string): boolean {
return IGNORE_LINE_PATTERNS.some((pattern) => pattern.test(line));
}
function isStatusLine(line: string): boolean {
return STATUS_LINE_PATTERNS.some((pattern) => pattern.test(line));
}
function isCacheHit(lines: string[]): boolean {
return lines.some(
(line) =>
/\[(?:local|remote) cache\]/i.test(line) ||
/Nx read the output from the cache/i.test(line)
);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: nx-filtered [nx arguments...]');
console.error('Example: nx-filtered run-many -t build,lint,typecheck-all');
process.exit(1);
}
// Use npx to ensure nx is found in node_modules/.bin
// Disable colors for clean parsing
const child = spawn('npx', ['nx', ...args], {
shell: true,
stdio: ['inherit', 'pipe', 'pipe'],
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
});
const tasks = new Map<string, TaskOutput>();
let currentTaskId: string | null = null;
const globalLines: string[] = [];
const failedTaskIds: string[] = [];
let inFailedTasksSummary = false;
let tasksStarted = 0;
const showProgress = process.stderr.isTTY;
const renderProgress = (): void => {
if (!showProgress) return;
cursorTo(process.stderr, 0);
clearLine(process.stderr, 0);
process.stderr.write(`Started ${String(tasksStarted)} task(s)...`);
};
const clearProgress = (): void => {
if (!showProgress) return;
cursorTo(process.stderr, 0);
clearLine(process.stderr, 0);
};
const processLine = (line: string, isStderr = false): void => {
// Check for task start: "> nx run project:target"
const taskStartMatch = line.match(/^>\s*nx run\s+([^\s]+)/);
if (taskStartMatch?.[1]) {
const taskId = taskStartMatch[1];
currentTaskId = taskId;
inFailedTasksSummary = false;
if (!tasks.has(taskId)) {
tasksStarted += 1;
tasks.set(taskId, {
taskId: taskId,
lines: [],
seenLines: new Set(),
hasError: false,
hasWarning: false,
isCacheHit: false,
});
renderProgress();
}
// Add the task start line
const task = tasks.get(taskId);
if (task) {
task.lines.push(line);
}
return;
}
// Check for NX status lines (reset current task context)
const nxStatusMatch = line.match(
/^\s*NX\s+(Successfully ran|Running target|Ran target)/
);
if (nxStatusMatch) {
currentTaskId = null;
inFailedTasksSummary = false;
globalLines.push(line);
return;
}
// Check for failed tasks list
if (line.trim() === 'Failed tasks:') {
inFailedTasksSummary = true;
currentTaskId = null;
globalLines.push(line);
return;
}
if (inFailedTasksSummary) {
// Check for task in failed list: " - project:target"
const failedMatch = line.match(/^\s*-\s+([^\s]+:[^\s]+)/);
if (failedMatch?.[1]) {
failedTaskIds.push(failedMatch[1]);
globalLines.push(line);
return;
}
// Keep blank lines in the summary section, but stop parsing on any other content
if (!line.trim()) {
globalLines.push(line);
return;
}
inFailedTasksSummary = false;
}
// Add line to current task or global (avoid duplicates)
if (currentTaskId) {
const task = tasks.get(currentTaskId);
if (task) {
// Skip duplicate lines (nx sometimes outputs the same content twice)
if (!task.seenLines.has(line)) {
task.seenLines.add(line);
task.lines.push(line);
}
if (hasError(line)) {
task.hasError = true;
}
if (hasWarning(line)) {
task.hasWarning = true;
}
if (isStderr && line.trim()) {
// Non-empty stderr usually indicates problems
task.hasError = true;
}
}
} else if (!shouldIgnoreLine(line) && !isStatusLine(line)) {
globalLines.push(line);
}
};
const stdoutRL = createInterface({ input: child.stdout });
stdoutRL.on('line', (line) => {
processLine(line, false);
});
const stderrRL = createInterface({ input: child.stderr });
stderrRL.on('line', (line) => {
processLine(line, true);
});
return new Promise((resolve) => {
child.on('close', (code) => {
clearProgress();
const overallSuccess = code === 0;
// Update cache hit status
for (const task of tasks.values()) {
task.isCacheHit = isCacheHit(task.lines);
}
// Mark tasks as failed based on exit code and failed tasks list
for (const taskId of failedTaskIds) {
const task = tasks.get(taskId);
if (task) {
task.hasError = true;
}
}
// Get tasks to show (errors or warnings)
const tasksToShow = Array.from(tasks.values()).filter(
(task) => task.hasError || task.hasWarning
);
// Output section for tasks with issues
if (tasksToShow.length > 0) {
console.log('');
console.log(
'────────────────────────────────────────────────────────────────────────'
);
console.log(
overallSuccess
? '⚠ Tasks with warnings:'
: '❌ Tasks with errors or warnings:'
);
console.log(
'────────────────────────────────────────────────────────────────────────'
);
for (const task of tasksToShow) {
const icon = task.hasError ? '❌' : '⚠';
console.log('');
console.log(`${icon} ${task.taskId}`);
console.log('─'.repeat(60));
// Show meaningful lines (skip empty and command echo lines)
const meaningfulLines = task.lines.filter((line) => {
if (shouldIgnoreLine(line)) return false;
// Keep task start line for context
if (line.match(/^>\s*nx run\s+/)) return true;
// Skip pure command echo lines (just "> command")
if (
line.startsWith('>') &&
!hasError(line) &&
!hasWarning(line)
) {
return false;
}
return true;
});
for (const line of meaningfulLines) {
console.log(line);
}
}
console.log('');
console.log(
'────────────────────────────────────────────────────────────────────────'
);
}
// Summary
const totalTasks = tasks.size;
const errorTasks = Array.from(tasks.values()).filter(
(t) => t.hasError
).length;
const warningTasks = Array.from(tasks.values()).filter(
(t) => t.hasWarning && !t.hasError
).length;
const cacheHits = Array.from(tasks.values()).filter(
(t) => t.isCacheHit
).length;
console.log('');
if (overallSuccess) {
if (totalTasks > 0) {
console.log(`✓ ${String(totalTasks)} tasks completed (${String(cacheHits)} from cache)`);
}
if (warningTasks > 0) {
console.log(` ${String(warningTasks)} task(s) with warnings`);
}
} else {
console.log(`✗ ${String(errorTasks)} of ${String(totalTasks)} tasks failed`);
if (warningTasks > 0) {
console.log(` ${String(warningTasks)} additional task(s) with warnings`);
}
// List failed tasks
if (errorTasks > 0) {
console.log('');
console.log('Failed tasks:');
for (const task of tasks.values()) {
if (task.hasError) {
console.log(` - ${task.taskId}`);
}
}
}
}
// Set exit code and let process exit naturally after stdout flushes
process.exitCode = code ?? 1;
resolve();
});
});
}
main().catch((err: unknown) => {
console.error('nx-filtered error:', err);
process.exitCode = 1;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment