@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
909 lines âĸ 46.2 kB
JavaScript
/**
* Clean Watch Mode Display - Developer-Focused Metrics
* Shows current state, trends, and actionable insights
*/
import { getCategoryLabel, } from "../utils/violation-types.js";
import { detectTerminalModeHeuristic } from "./terminal-detector.js";
import { ANSI_CODES, isESLintCategory } from "../shared/constants.js";
import { replaceAll } from "../utils/node-compatibility.js";
import { debugLog } from "../utils/debug-logger.js";
export class DeveloperWatchDisplay {
state;
colors;
consoleBackup = undefined;
constructor() {
debugLog("WatchDisplay", "Constructor started");
this.state = {
isInitialized: false,
sessionStart: Date.now(),
lastUpdate: 0,
baseline: undefined,
current: { total: 0, bySource: {}, byCategory: {} },
viewMode: "dashboard", // 'dashboard' | 'tidy' | 'burndown'
currentViolations: [],
};
debugLog("WatchDisplay", "State initialized");
this.colors = this.createColorScheme();
debugLog("WatchDisplay", "Color scheme created");
this.setupKeyboardHandling();
debugLog("WatchDisplay", "Keyboard handling setup completed");
// Note: captureOutput will be called later when watch mode actually starts
// to avoid hanging during constructor
debugLog("WatchDisplay", "Constructor completed successfully");
}
colorModeOverride = undefined;
createColorScheme() {
const mode = this.colorModeOverride || detectTerminalModeHeuristic();
const colorSet = mode === "dark" ? ANSI_CODES.DARK : ANSI_CODES.LIGHT;
return {
reset: ANSI_CODES.RESET,
bold: ANSI_CODES.BOLD,
dim: ANSI_CODES.DIM,
primary: colorSet.PRIMARY,
secondary: colorSet.SECONDARY,
success: colorSet.SUCCESS,
warning: colorSet.WARNING,
error: colorSet.ERROR,
info: colorSet.INFO,
muted: colorSet.MUTED,
accent: colorSet.ACCENT,
header: colorSet.PRIMARY, // Use primary color for headers
};
}
/**
* Toggle between light and dark color schemes
*/
toggleColorScheme() {
if (this.colorModeOverride === undefined) {
// First toggle - determine current mode and switch to opposite
const currentMode = detectTerminalModeHeuristic();
this.colorModeOverride = currentMode === "dark" ? "light" : "dark";
}
else {
// Toggle between the two modes
this.colorModeOverride =
this.colorModeOverride === "dark" ? "light" : "dark";
}
// Recreate color scheme with new mode
this.colors = this.createColorScheme();
}
captureOutput() {
debugLog("WatchDisplay", "captureOutput method started");
// Store original console methods for restoration
if (!this.consoleBackup) {
debugLog("WatchDisplay", "Creating console backup...");
this.consoleBackup = {
log: console.log,
error: console.error,
warn: console.warn,
stderrWrite: process.stderr.write,
};
debugLog("WatchDisplay", "Console backup created");
}
debugLog("WatchDisplay", "Overriding console methods...");
// Override console methods to silence output during watch mode
console.log = () => { };
debugLog("WatchDisplay", "console.log overridden");
console.error = () => { };
debugLog("WatchDisplay", "console.error overridden");
console.warn = () => { };
debugLog("WatchDisplay", "console.warn overridden");
try {
process.stderr.write = () => true;
debugLog("WatchDisplay", "process.stderr.write overridden");
}
catch (error) {
debugLog("WatchDisplay", "Failed to override process.stderr.write", error);
}
debugLog("WatchDisplay", "Console methods override completed");
}
restoreOutput() {
if (this.consoleBackup) {
console.log = this.consoleBackup.log;
console.error = this.consoleBackup.error;
console.warn = this.consoleBackup.warn;
process.stderr.write = this.consoleBackup.stderrWrite;
this.consoleBackup = undefined;
}
}
/**
* Set up keyboard input handling for view mode toggle
*/
setupKeyboardHandling() {
debugLog("WatchDisplay", "Setting up keyboard handling...");
// Enable raw mode to capture individual keystrokes
if (process.stdin.isTTY) {
debugLog("WatchDisplay", "TTY detected, setting up raw mode...");
try {
process.stdin.setRawMode(true);
debugLog("WatchDisplay", "Raw mode enabled successfully");
process.stdin.resume();
debugLog("WatchDisplay", "stdin resumed");
process.stdin.setEncoding("utf8");
debugLog("WatchDisplay", "stdin encoding set to utf8");
// Setup keyboard event listeners only when in TTY mode
process.stdin.on("data", (key) => {
// Handle keyboard shortcuts
switch (key) {
case "\u0014": {
// Ctrl+T - Tidy view
this.state.viewMode = "tidy";
this.renderCurrentView().catch(console.error);
break;
}
case "\u0002": {
// Ctrl+B - Burndown mode
this.state.viewMode = "burndown";
this.renderCurrentView().catch(console.error);
break;
}
case " ": {
// Spacebar - Manual refresh in burndown mode
if (this.state.viewMode === "burndown") {
this.renderCurrentView().catch(console.error);
}
break;
}
case "\u000D": {
// Ctrl+M - Monitor mode (back to dashboard)
this.state.viewMode = "dashboard";
this.renderCurrentView().catch(console.error);
break;
}
case "\u0004": {
// Ctrl+D - Toggle dark/light mode
this.toggleColorScheme();
this.renderCurrentView().catch(console.error);
break;
}
case "\u001B": {
// Escape - back to dashboard
this.state.viewMode = "dashboard";
this.renderCurrentView().catch(console.error);
break;
}
case "\u0003": {
// Ctrl+C - Exit
process.stdin.setRawMode(false);
process.exit(0);
}
// No default
}
});
debugLog("WatchDisplay", "Keyboard event listeners setup complete");
}
catch (error) {
debugLog("WatchDisplay", "Failed to setup raw mode", error);
console.warn("â ī¸ Could not enable keyboard shortcuts:", error);
return;
}
}
else {
debugLog("WatchDisplay", "No TTY detected, skipping keyboard setup");
}
debugLog("WatchDisplay", "Keyboard handling setup completed");
}
/**
* Render the current view based on state.viewMode
*/
renderCurrentView() {
if (this.state.viewMode === "tidy") {
this.renderTidyView();
}
else if (this.state.viewMode === "burndown") {
this.renderBurndownView();
}
else {
this.renderDashboardView();
}
return Promise.resolve();
}
/**
* Render a clean diagnostic view showing only actual issues
*/
renderTidyView() {
// Clear screen completely and reset position
process.stdout.write("\u001B[?25l"); // Hide cursor
process.stdout.write("\u001B[2J"); // Clear entire screen
process.stdout.write("\u001B[3J"); // Clear scrollback buffer
process.stdout.write("\u001B[H"); // Move cursor to home
process.stdout.write(`${this.colors.bold}${this.colors.info}đ Comprehensive Analysis View${this.colors.reset}\n`);
process.stdout.write(`${this.colors.secondary}Shows all findings (errors, warnings, and info)${this.colors.reset}\n`);
process.stdout.write(`${this.colors.secondary}${"â".repeat(80)}${this.colors.reset}\n\n`);
if (this.state.currentViolations.length === 0) {
process.stdout.write(`${this.colors.success}â
No violations found - all clear!${this.colors.reset}\n\n`);
}
else {
// Group violations by file for cleaner display
const violationsByFile = new Map();
for (const violation of this.state.currentViolations) {
if (!violationsByFile.has(violation.file)) {
violationsByFile.set(violation.file, []);
}
violationsByFile.get(violation.file).push(violation);
}
// Display each file's violations
for (const [file, violations] of violationsByFile) {
const severityIcon = violations.some((v) => v.severity === "error")
? "â"
: violations.some((v) => v.severity === "warn")
? "â ī¸"
: "âšī¸";
process.stdout.write(`${severityIcon} ${this.colors.info}${file}${this.colors.reset} ${this.colors.secondary}(${violations.length} issues)${this.colors.reset}\n`);
// Show each violation in compact format with severity indicators
for (const violation of violations.slice(0, 5)) {
// Limit to 5 per file for tidiness
const sourceIcon = violation.source === "typescript"
? "đ"
: violation.source === "eslint"
? "đ"
: "đī¸";
const severityColor = violation.severity === "error"
? this.colors.error
: violation.severity === "warn"
? this.colors.warning
: this.colors.muted;
const severityLabel = violation.severity === "info"
? `${this.colors.muted}[info]${this.colors.reset} `
: "";
process.stdout.write(` ${sourceIcon} ${severityColor}Line ${violation.line}:${this.colors.reset} ${severityLabel}${violation.message}\n`);
}
if (violations.length > 5) {
process.stdout.write(` ${this.colors.secondary}... and ${violations.length - 5} more issues${this.colors.reset}\n`);
}
process.stdout.write("\n");
}
}
process.stdout.write(`${this.colors.muted}Press Ctrl+T or Esc to return to dashboard | Ctrl+C to stop watching...${this.colors.reset}\n`);
process.stdout.write("\u001B[?25h"); // Show cursor
}
/**
* Render the full dashboard view (original view)
*/
renderDashboardView() {
// Clear screen completely and reset position
process.stdout.write("\u001B[?25l"); // Hide cursor
process.stdout.write("\u001B[2J"); // Clear entire screen
process.stdout.write("\u001B[3J"); // Clear scrollback buffer
process.stdout.write("\u001B[H"); // Move cursor to home
this.state.isInitialized = false;
// The next updateDisplay call will recreate the dashboard
}
/**
* Render the burndown progress view for active fixing sessions
*/
renderBurndownView(checksCount, actionableViolations) {
const { colors } = this;
const { sessionStart, currentViolations, baseline, current } = this.state;
// Use filtered actionable violations (errors + warnings only) instead of all violations
const displayViolations = actionableViolations ||
this.filterActionableViolations(currentViolations);
const sessionDuration = Math.floor((Date.now() - sessionStart) / 1000);
const timestamp = new Date().toLocaleTimeString();
// Clear screen completely and reset position
process.stdout.write("\u001B[?25l"); // Hide cursor
process.stdout.write("\u001B[2J"); // Clear entire screen
process.stdout.write("\u001B[3J"); // Clear scrollback buffer
process.stdout.write("\u001B[H"); // Move cursor to home
process.stdout.write(`${colors.bold}${colors.error}đĨ SideQuest Burndown Dashboard${colors.reset}\n`);
process.stdout.write(`${colors.secondary}Showing actionable issues only (errors + warnings)${colors.reset}\n`);
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n\n`);
process.stdout.write(`Session Goal: Fix Critical Issues âĸ Started: ${timestamp} âĸ ${Math.floor(sessionDuration / 60)}m ${sessionDuration % 60}s âĸ Checks: ${checksCount || 0}\n\n`);
// Progress This Session
process.stdout.write("Progress This Session:\n");
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n`);
// Find the largest category to show as "working on"
const currentData = this.processViolations(displayViolations);
const topCategory = Object.entries(currentData.byCategory).sort(([, a], [, b]) => b - a)[0];
if (topCategory) {
const [categoryKey, count] = topCategory;
const categoryName = this.getCategoryDisplayName(categoryKey);
process.stdout.write(`âļī¸ Working on: ${categoryName} (${count} remaining)\n`);
}
else {
process.stdout.write("âļī¸ Working on: No issues found\n");
}
// Calculate session progress if baseline exists
let fixedIssues = 0;
let addedIssues = 0;
if (baseline) {
const baselineTotal = baseline.total;
const currentTotal = current.total;
const netChange = currentTotal - baselineTotal;
if (netChange < 0) {
fixedIssues = Math.abs(netChange);
}
else if (netChange > 0) {
addedIssues = netChange;
}
}
process.stdout.write(`â
Fixed: ${fixedIssues} issues\n`);
process.stdout.write(`đ Added: ${addedIssues} new issues\n`);
process.stdout.write(`đ Net Progress: ${fixedIssues - addedIssues >= 0 ? "+" : ""}${fixedIssues - addedIssues}\n\n`);
// Burndown Progress by Category
process.stdout.write("Burndown Progress: Start â Current (Change)\n");
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n`);
// Get real-time data from actionable violations only
const violationData = this.processViolations(displayViolations);
// Show meaningful progress bars based on reduction from baseline
process.stdout.write("đ ESLint Categories:\n");
const eslintCategoryData = [
{ key: "unused-vars", name: "Unused Variables", severity: "â ī¸" },
{ key: "modernization", name: "Modernization", severity: "âšī¸" },
{ key: "style", name: "Code Style", severity: "â ī¸" },
{ key: "code-quality", name: "Code Quality", severity: "â ī¸" },
{ key: "other-eslint", name: "Other ESLint", severity: "âšī¸" },
];
for (const category of eslintCategoryData) {
const currentCount = violationData.byCategory[category.key] || 0;
const baselineCount = baseline?.byCategory[category.key] || currentCount;
if (currentCount > 0 || baselineCount > 0) {
const change = currentCount - baselineCount;
const changeText = change === 0 ? "Âą0" : change > 0 ? `+${change}` : `${change}`;
const changeColor = change > 0
? colors.error
: change < 0
? colors.success
: colors.muted;
// Create burndown progress bar: shows current vs baseline
const burndownBar = this.createBurndownBar(baselineCount, currentCount);
process.stdout.write(` ${category.severity} ${category.name.padEnd(18)} ${baselineCount.toString().padStart(2)} â ${currentCount.toString().padStart(2)} ${changeColor}(${changeText})${colors.reset} ${burndownBar}\n`);
}
}
process.stdout.write("\nđ TypeScript Categories:\n");
const tsCategoryData = [
{ key: "best-practices", name: "Best Practices", severity: "âšī¸" },
{ key: "type-alias", name: "Type Issues", severity: "â" },
{ key: "inheritance", name: "Class/Override", severity: "âšī¸" },
];
for (const category of tsCategoryData) {
const currentCount = violationData.byCategory[category.key] || 0;
const baselineCount = baseline?.byCategory[category.key] || currentCount;
if (currentCount > 0 || baselineCount > 0) {
const change = currentCount - baselineCount;
const changeText = change === 0 ? "Âą0" : change > 0 ? `+${change}` : `${change}`;
const changeColor = change > 0
? colors.error
: change < 0
? colors.success
: colors.muted;
const burndownBar = this.createBurndownBar(baselineCount, currentCount);
process.stdout.write(` ${category.severity} ${category.name.padEnd(18)} ${baselineCount.toString().padStart(2)} â ${currentCount.toString().padStart(2)} ${changeColor}(${changeText})${colors.reset} ${burndownBar}\n`);
}
}
// Unused Exports
const unusedExportsCount = violationData.bySource["unused-exports"] || 0;
const baselineUnusedExports = baseline?.bySource["unused-exports"] || unusedExportsCount;
if (unusedExportsCount > 0 || baselineUnusedExports > 0) {
const change = unusedExportsCount - baselineUnusedExports;
const changeText = change === 0 ? "Âą0" : change > 0 ? `+${change}` : `${change}`;
const changeColor = change > 0 ? colors.error : change < 0 ? colors.success : colors.muted;
const burndownBar = this.createBurndownBar(baselineUnusedExports, unusedExportsCount);
process.stdout.write(`\nđī¸ Unused Exports ${baselineUnusedExports.toString().padStart(2)} â ${unusedExportsCount.toString().padStart(2)} ${changeColor}(${changeText})${colors.reset} ${burndownBar}\n\n`);
}
// Zod Validation Health (using real-time data)
const zodViolations = displayViolations.filter((v) => v.source === "zod-detection");
if (zodViolations.length > 0) {
process.stdout.write("đĄī¸ Zod Validation Health\n");
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n`);
// Extract real coverage data from violations
const coverageViolation = zodViolations.find((v) => v.message && v.message.includes("coverage is"));
const parseRatioViolation = zodViolations.find((v) => v.message && v.message.includes("parse() vs"));
let coverage = "0";
let unsafeCalls = "0";
if (coverageViolation?.message) {
const coverageMatch = coverageViolation.message.match(/coverage is ([.\\d]+)%/);
if (coverageMatch) {
coverage = coverageMatch[1] || "0";
}
}
if (parseRatioViolation?.message) {
const parseMatch = parseRatioViolation.message.match(/(\\d+) \\.parse\\(\\)/);
if (parseMatch) {
unsafeCalls = parseMatch[1] || "0";
}
}
const coverageNumber = Number.parseFloat(coverage);
const progressBars = Math.floor((coverageNumber / 100) * 30);
const emptyBars = 30 - progressBars;
process.stdout.write(`Coverage: ${coverage}% ${"â".repeat(progressBars)}${"â".repeat(emptyBars)} Target: 70%\n`);
process.stdout.write(`Parse Safety: ${unsafeCalls} unsafe calls need fixing\n\n`);
}
// Quick Wins (using real-time data)
process.stdout.write("Quick Wins Available:\n");
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n`);
const styleCount = violationData.byCategory["style"] || 0;
const modernizationCount = violationData.byCategory["modernization"] || 0;
const codeQualityCount = violationData.byCategory["code-quality"] || 0;
const bestPracticesCount = violationData.byCategory["best-practices"] || 0;
if (styleCount > 0) {
process.stdout.write(`âĸ ${styleCount} Code Style issues (ESLint --fix can resolve most)\n`);
}
if (modernizationCount > 0) {
process.stdout.write(`âĸ ${modernizationCount} Modernization opportunities (prefer-const, unicorn rules)\n`);
}
if (codeQualityCount > 0) {
process.stdout.write(`âĸ ${codeQualityCount} Code Quality improvements (undef, console, await)\n`);
}
if (bestPracticesCount > 0) {
process.stdout.write(`âĸ ${bestPracticesCount} Best Practice improvements\n`);
}
if (styleCount === 0 &&
modernizationCount === 0 &&
codeQualityCount === 0 &&
bestPracticesCount === 0) {
process.stdout.write("âĸ No quick wins available - focus on manual fixes\n");
}
process.stdout.write("\n");
process.stdout.write(`Session Stats: ${fixedIssues} fixed âĸ ${current.total} remaining âĸ ETA: --:-- (${fixedIssues > 0 ? "progress detected!" : "start fixing to estimate"})\n`);
process.stdout.write(`${colors.muted}Ctrl+M: Monitor âĸ Ctrl+T: Tidy âĸ Ctrl+D: Toggle Colors âĸ Ctrl+C: Exit${colors.reset}\n`);
process.stdout.write("\u001B[?25h"); // Show cursor
}
/**
* Create a burndown progress bar showing reduction from baseline
*/
createBurndownBar(baseline, current, width = 20) {
if (baseline === 0 && current === 0) {
return "â".repeat(width);
}
const maxCount = Math.max(baseline, current, 1);
const baselineBar = Math.floor((baseline / maxCount) * width);
const currentBar = Math.floor((current / maxCount) * width);
if (current <= baseline) {
// Progress made (reduction) - show green completed portion
const completed = baselineBar - currentBar;
const remaining = currentBar;
const empty = width - baselineBar;
return ("đŠ".repeat(completed) + "đ¨".repeat(remaining) + "â".repeat(empty));
}
else {
// Regression (increase) - show red
const baseline_portion = baselineBar;
const increase = currentBar - baselineBar;
const empty = width - currentBar;
return ("đ¨".repeat(baseline_portion) +
"đĨ".repeat(increase) +
"â".repeat(empty));
}
}
/**
* Filter violations to show only actionable issues (errors + warnings)
* Info-level items are just noise in watch mode
*/
filterActionableViolations(violations) {
return violations.filter((violation) => violation.severity === "error" || violation.severity === "warn");
}
/**
* Get display name for category keys
*/
getCategoryDisplayName(categoryKey) {
const displayNames = {
"unused-vars": "Unused Variables",
"other-eslint": "Other ESLint",
modernization: "Modernization",
style: "Code Style",
"best-practices": "Best Practices",
"type-alias": "Type Issues",
inheritance: "Class/Override",
"unused-code": "Unused Code",
"code-quality": "Code Quality",
};
return (displayNames[categoryKey] ||
replaceAll(categoryKey, "-", " ").replaceAll(/\b\w/g, (l) => l.toUpperCase()));
}
async updateDisplay(violations, checksCount, orchestrator) {
debugLog("WatchDisplay", "updateDisplay called", {
violationCount: violations.length,
checksCount,
isInitialized: this.state.isInitialized,
hasConsoleBackup: !!this.consoleBackup,
});
// Ensure output capture is enabled (safe to call multiple times)
if (!this.consoleBackup) {
debugLog("WatchDisplay", "Enabling output capture during updateDisplay");
this.captureOutput();
}
// Store current violations for all view modes
this.state.currentViolations = violations;
// Check for setup/configuration issues first (critical)
const setupIssues = violations.filter((v) => v.category === "setup-issue");
// Filter violations for actionable display (errors + warnings only)
const actionableViolations = this.filterActionableViolations(violations);
// If in tidy mode, show ALL violations (comprehensive view)
if (this.state.viewMode === "tidy") {
this.renderTidyView();
return;
}
// If in burndown mode, show actionable violations only
if (this.state.viewMode === "burndown") {
this.renderBurndownView(checksCount, actionableViolations);
return;
}
// Process actionable violations for dashboard mode (watch focus)
const current = this.processViolations(actionableViolations);
// Set baseline on first run (race condition fixed by state management)
if (!this.state.baseline) {
this.state.baseline = { ...current };
}
this.state.current = current;
this.state.lastUpdate = Date.now();
// Get today's data if orchestrator is provided
let todayData = undefined;
if (orchestrator) {
try {
const analysisService = orchestrator.getAnalysisService();
// Get stats for all violations to show meaningful progress data
// TODO: Fix this to properly filter by today when database schema is updated
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const stats = await analysisService.calculateViolationStats({
start: yesterday,
end: new Date(),
});
todayData = {
total: stats.total,
filesAffected: stats.filesAffected,
avgPerFile: stats.avgPerFile,
};
}
catch (error) {
// Log error in debug mode, continue without today's data
if (process.env["DEBUG"]) {
console.error("[WatchDisplay] Failed to get today's data:", error);
}
}
}
// Clear screen and render
debugLog("WatchDisplay", "About to call render method", {
checksCount,
hasTodayData: !!todayData,
setupIssuesCount: setupIssues.length,
});
this.render(checksCount, todayData, setupIssues);
debugLog("WatchDisplay", "Render method completed");
// Mark as initialized after first successful render
if (!this.state.isInitialized) {
this.state.isInitialized = true;
debugLog("WatchDisplay", "Display marked as initialized");
}
}
processViolations(violations) {
const bySource = {};
const byCategory = {};
const bySeverity = {};
const byCategoryBySource = {};
for (const violation of violations) {
bySource[violation.source] = (bySource[violation.source] || 0) + 1;
byCategory[violation.category] =
(byCategory[violation.category] || 0) + 1;
// Track severity by source
if (!bySeverity[violation.source]) {
bySeverity[violation.source] = {};
}
bySeverity[violation.source][violation.severity] =
(bySeverity[violation.source][violation.severity] || 0) + 1;
// Track categories by source
if (!byCategoryBySource[violation.source]) {
byCategoryBySource[violation.source] = {};
}
byCategoryBySource[violation.source][violation.category] =
(byCategoryBySource[violation.source][violation.category] || 0) + 1;
}
return {
total: violations.length,
bySource,
byCategory,
bySeverity,
byCategoryBySource,
};
}
render(checksCount, todayData, setupIssues) {
debugLog("WatchDisplay", "render method called", {
checksCount,
hasTodayData: !!todayData,
setupIssuesCount: setupIssues?.length || 0,
currentTotal: this.state.current.total,
baselineTotal: this.state.baseline?.total,
});
const { colors } = this;
const { lastUpdate, sessionStart, current, baseline } = this.state;
const sessionDuration = Math.floor((lastUpdate - sessionStart) / 1000);
const timestamp = new Date().toLocaleTimeString();
// Clear screen completely and reset position
debugLog("WatchDisplay", "About to clear screen and start rendering UI");
process.stdout.write("\u001B[?25l"); // Hide cursor
process.stdout.write("\u001B[2J"); // Clear entire screen
process.stdout.write("\u001B[3J"); // Clear scrollback buffer
process.stdout.write("\u001B[H"); // Move cursor to home
// Header
process.stdout.write(`${colors.bold}${colors.accent}đ Code Quality Monitor${colors.reset}\n`);
process.stdout.write(`${colors.secondary}Showing actionable issues only (errors + warnings)${colors.reset}\n`);
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n\n`);
// â ī¸ CRITICAL: Setup/Configuration Issues (shown prominently)
if (setupIssues && setupIssues.length > 0) {
process.stdout.write(`${colors.bold}${colors.error}â ī¸ SETUP ISSUES DETECTED${colors.reset}\n`);
process.stdout.write(`${colors.error}${"â".repeat(60)}${colors.reset}\n`);
for (const issue of setupIssues) {
const toolName = issue.source === "typescript"
? "TypeScript"
: issue.source === "eslint"
? "ESLint"
: issue.source.toUpperCase();
process.stdout.write(`${colors.error}đ¨ ${toolName} Configuration Problem:${colors.reset}\n`);
// Extract the main error message (first line of the detailed message)
const mainMessage = issue.message?.split("\n")[0] || issue.code;
process.stdout.write(` ${colors.warning}${mainMessage}${colors.reset}\n`);
// Show fix suggestion if available
if (issue.fixSuggestion) {
process.stdout.write(` ${colors.info}đĄ Fix: ${issue.fixSuggestion}${colors.reset}\n`);
}
process.stdout.write("\n");
}
process.stdout.write(`${colors.error}${"â".repeat(60)}${colors.reset}\n`);
process.stdout.write(`${colors.warning}⥠Fix these setup issues first - analysis may be incomplete!${colors.reset}\n\n`);
}
// Current Status
const baseline_ = baseline;
const totalDelta = current.total - baseline_.total;
const deltaColor = totalDelta > 0
? colors.error
: totalDelta < 0
? colors.success
: colors.muted;
const deltaText = totalDelta === 0 ? "" : ` (${totalDelta > 0 ? "+" : ""}${totalDelta})`;
process.stdout.write(`${colors.bold}Current Issues: ${colors.primary}${current.total}${deltaColor}${deltaText}${colors.reset}\n`);
process.stdout.write(`${colors.muted}Last check: ${timestamp} | Session: ${sessionDuration}s | Checks: ${checksCount}${colors.reset}\n\n`);
// By Source with severity breakdown (excluding zod-detection which has its own section)
if (Object.keys(current.bySource).length > 0) {
process.stdout.write(`${colors.warning}By Source:${colors.reset}\n`);
for (const [source, count] of Object.entries(current.bySource).sort(([, a], [, b]) => b - a)) {
// Skip zod-detection as it has its own dedicated section
if (source === "zod-detection") {
continue;
}
const baselineCount = baseline_.bySource[source] || 0;
const delta = count - baselineCount;
const deltaString = delta === 0 ? "" : ` (${delta > 0 ? "+" : ""}${delta})`;
const deltaColor = delta > 0 ? colors.error : delta < 0 ? colors.success : colors.reset;
const icon = source === "typescript"
? "đ"
: source === "unused-exports"
? "đī¸"
: "đ";
process.stdout.write(` ${icon} ${colors.info}${source}:${colors.reset} ${colors.primary}${count}${deltaColor}${deltaString}${colors.reset}\n`);
// Show severity breakdown for ESLint only (TypeScript errors are mostly all "error" severity)
if (current.bySeverity &&
current.bySeverity[source] &&
source === "eslint") {
const severities = current.bySeverity[source];
const severityOrder = ["error", "warn", "info"];
for (const severity of severityOrder) {
if (severities[severity]) {
const sevIcon = severity === "error" ? "â" : severity === "warn" ? "â ī¸" : "âšī¸";
process.stdout.write(` ${sevIcon} ${colors.secondary}${severity}:${colors.reset} ${colors.primary}${severities[severity]}${colors.reset}\n`);
}
}
}
// Show top categories for ESLint and TypeScript (limit to top 5 to avoid clutter)
if (current.byCategoryBySource &&
current.byCategoryBySource[source] &&
(source === "eslint" || source === "typescript")) {
const categories = Object.entries(current.byCategoryBySource[source])
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
for (const [category, categoryCount] of categories) {
const displayLabel = getCategoryLabel(category);
process.stdout.write(` âĸ ${colors.secondary}${displayLabel}:${colors.reset} ${colors.primary}${categoryCount}${colors.reset}\n`);
}
}
}
process.stdout.write("\n");
}
// Enhanced Zod Analysis Section (if Zod violations exist) - show even in actionable mode since it's contextual
const allViolations = this.state.currentViolations; // Use all violations for Zod context
const zodViolations = allViolations.filter((v) => v.source === "zod-detection");
if (zodViolations.length > 0) {
this.renderZodAnalysisSection(zodViolations);
}
// Top Issues (by category)
const topCategories = Object.entries(current.byCategory)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
if (topCategories.length > 0) {
process.stdout.write(`${colors.warning}Top Issues:${colors.reset}\n`);
for (const [category, count] of topCategories) {
const baselineCount = baseline_.byCategory[category] || 0;
const delta = count - baselineCount;
const deltaString = delta === 0 ? "" : ` (${delta > 0 ? "+" : ""}${delta})`;
const deltaColor = delta > 0 ? colors.error : delta < 0 ? colors.success : colors.reset;
// Determine severity and icon
const isESLintViolation = isESLintCategory(category);
const severity = this.getSeverity(category);
const severityIcon = severity === "error" ? "â" : severity === "warn" ? "â ī¸" : "âšī¸";
const sourceIcon = isESLintViolation ? "đ" : "đ";
const displayLabel = getCategoryLabel(category);
process.stdout.write(` ${severityIcon} ${sourceIcon} ${colors.info}${displayLabel}:${colors.reset} ${colors.primary}${count}${deltaColor}${deltaString}${colors.reset}\n`);
}
}
// Session Summary - show detailed breakdown
process.stdout.write(`\n${colors.muted}Session Summary:${colors.reset}\n`);
// Calculate positive and negative changes separately
let newIssues = 0;
let resolvedIssues = 0;
for (const [category, count] of Object.entries(current.byCategory)) {
const baselineCount = baseline_.byCategory[category] || 0;
const delta = count - baselineCount;
if (delta > 0) {
newIssues += delta;
}
else if (delta < 0) {
resolvedIssues += Math.abs(delta);
}
}
// Show detailed breakdown
if (newIssues > 0) {
process.stdout.write(`${colors.error} đ +${newIssues} new issues found${colors.reset}\n`);
}
if (resolvedIssues > 0) {
process.stdout.write(`${colors.success} đ ${resolvedIssues} issues resolved${colors.reset}\n`);
}
// Show net change in blue
const netChange = newIssues - resolvedIssues;
if (netChange !== 0) {
const netColor = colors.info; // Blue for net change
const netIcon = netChange > 0 ? "đē" : "đģ";
const netSign = netChange > 0 ? "+" : "";
process.stdout.write(`${netColor} ${netIcon} Net: ${netSign}${netChange}${colors.reset}\n`);
}
else if (newIssues === 0 && resolvedIssues === 0) {
process.stdout.write(`${colors.muted} âĄī¸ No changes this session${colors.reset}\n`);
}
else {
process.stdout.write(`${colors.info} âī¸ Net: No change (${newIssues} new, ${resolvedIssues} resolved)${colors.reset}\n`);
}
// Today's Progress
if (todayData) {
process.stdout.write(`\n${colors.muted}Today's Progress:${colors.reset}\n`);
process.stdout.write(`${colors.accent} đ
Total issues processed: ${colors.primary}${todayData.total}${colors.reset}\n`);
process.stdout.write(`${colors.accent} đ Files affected: ${colors.primary}${todayData.filesAffected}${colors.reset}\n`);
if (todayData.avgPerFile > 0) {
process.stdout.write(`${colors.accent} đ Avg per file: ${colors.primary}${todayData.avgPerFile.toFixed(1)}${colors.reset}\n`);
}
}
process.stdout.write(`\n${colors.muted}Ctrl+B: Burndown âĸ Ctrl+T: Comprehensive âĸ Ctrl+D: Toggle Colors âĸ Ctrl+C: Exit${colors.reset}\n`);
process.stdout.write("\u001B[?25h"); // Show cursor
debugLog("WatchDisplay", "Main render method completed - UI should now be visible");
}
/**
* Render enhanced Zod analysis section with coverage metrics
*/
renderZodAnalysisSection(zodViolations) {
const { colors } = this;
process.stdout.write(`${colors.bold}${colors.accent}đĄī¸ Zod Analysis${colors.reset}\n`);
process.stdout.write(`${colors.muted}${"â".repeat(60)}${colors.reset}\n`);
// Extract Zod coverage data from violations
const coverageViolation = zodViolations.find((v) => v.message && v.message.includes("coverage is"));
const parseRatioViolation = zodViolations.find((v) => v.message && v.message.includes("parse() vs"));
const baselineViolation = zodViolations.find((v) => v.message && v.message.includes("Target "));
// Extract coverage percentage
let coverage = "0";
let usedSchemas = "0";
let totalSchemas = "0";
if (coverageViolation && coverageViolation.message) {
const coverageMatch = coverageViolation.message.match(/coverage is ([\d.]+)% \((\d+)\/(\d+) schemas used\)/);
if (coverageMatch) {
coverage = coverageMatch[1] || "0";
usedSchemas = coverageMatch[2] || "0";
totalSchemas = coverageMatch[3] || "0";
}
}
// Extract parse safety data
let parseCallsCount = "0";
let safeParseCallsCount = "0";
if (parseRatioViolation && parseRatioViolation.message) {
const parseMatch = parseRatioViolation.message.match(/(\d+) \.parse\(\) vs (\d+) \.safeParse\(\)/);
if (parseMatch) {
parseCallsCount = parseMatch[1] || "0";
safeParseCallsCount = parseMatch[2] || "0";
}
}
// Extract risk level from coverage percentage
const coverageNumber = Number.parseFloat(coverage);
let riskLevel = "High";
let riskColor = colors.error;
if (coverageNumber >= 80) {
riskLevel = "Low";
riskColor = colors.success;
}
else if (coverageNumber >= 50) {
riskLevel = "Medium";
riskColor = colors.warning;
}
// Extract baseline recommendation
let baseline = "General TypeScript project: Target 70%+ coverage";
if (baselineViolation && baselineViolation.message) {
const baselineMatch = baselineViolation.message.match(/Target ([^.]+)\./);
if (baselineMatch) {
baseline = `Target ${baselineMatch[1]}`;
}
}
// Display coverage metrics prominently
process.stdout.write(`${colors.secondary} Coverage: ${colors.primary}${coverage}%${colors.reset} ${colors.secondary}(${usedSchemas}/${totalSchemas} schemas used)${colors.reset}\n`);
process.stdout.write(`${colors.secondary} Risk Level: ${riskColor}${riskLevel}${colors.reset}\n`);
process.stdout.write(`${colors.secondary} Parse Safety: ${colors.primary}${parseCallsCount} unsafe${colors.reset}${colors.secondary}, ${colors.primary}${safeParseCallsCount} safe${colors.reset} ${colors.secondary}calls${colors.reset}\n`);
process.stdout.write(`${colors.secondary} Baseline: ${colors.info}${baseline}${colors.reset}\n\n`);
}
getSeverity(category) {
// Error categories
if (["type-alias", "no-explicit-any"].includes(category)) {
return "error";
}
// Warning categories
if ([
"annotation",
"cast",
"unused-vars",
"code-quality",
"return-type",
"style",
].includes(category)) {
return "warn";
}
// Default to info
return "info";
}
/**
* Restore display state from a previous session
*/
restoreFromSession(sessionData) {
this.state.sessionStart = sessionData.sessionStart;
this.state.baseline = sessionData.baseline;
this.state.current = sessionData.current;
this.state.viewMode = sessionData.viewMode;
this.state.isInitialized = true;
}
/**
* Wait for initial analysis to complete before allowing display updates
* This prevents race conditions where display shows before analysis finishes
*/
async waitForInitialAnalysis() {
// This is called by the controller to ensure proper sequencing
// The needsBaselineRefresh flag handles the actual synchronization
}
/**
* Check if display is ready for updates
*/
isReady() {
return this.state.isInitialized;
}
/**
* Reset baseline to force refresh on next update
* Used when resuming sessions to prevent stale delta calculations
*/
resetBaseline() {
this.state.baseline = undefined;
}
shutdown() {
this.restoreOutput();
// Restore stdin if it was modified
if (process.stdin.isTTY && process.stdin.isRaw) {
process.stdin.setRawMode(false);
}
process.stdout.write("\u001B[?25h"); // Show cursor
}
}
// Singleton
let displayInstance;
export function getDeveloperWatchDisplay() {
if (!displayInstance) {
displayInstance = new DeveloperWatchDisplay();
}
return displayInstance;
}
export function resetDeveloperWatchDisplay() {
if (displayInstance) {
displayInstance.shutdown();
}
displayInstance = undefined;
}
//# sourceMappingURL=watch-display-v2.js.map