@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
1,195 lines (1,100 loc) • 57.8 kB
JavaScript
#!/usr/bin/env node
/**
* @fileoverview CLI interface for SideQuest Code Quality Orchestrator
*
* Configuration-agnostic TypeScript and ESLint orchestrator that respects your project setup.
* Features include:
* - Interactive first-run setup with best practices guidance
* - TypeScript compilation checking using client's exact tsconfig.json
* - Optional ESLint integration with separation of concerns warnings
* - User preferences system with persistent configuration
* - Real-time watch mode with intelligent terminal color detection
* - SQLite persistence for historical tracking and performance optimization
*
* @example
* ```bash
* # First run triggers interactive setup
* sidequest --watch
*
* # Configuration management
* sidequest --config show
* sidequest --config edit
*
* # Analysis modes
* sidequest --watch # TypeScript only (default)
* sidequest --watch --include-eslint # Add ESLint (optional)
* ```
*
* @author SideQuest
* @version Dynamically loaded from package.json
*/
import { resetAllServices } from "../services/index.js";
// ViolationSummaryItem no longer needed - using live data only
import { getDeveloperWatchDisplay, resetDeveloperWatchDisplay, } from "./watch-display-v2.js";
import { SessionManager } from "../services/session-manager.js";
import { WatchController } from "./watch-controller.js";
import { detectTerminalBackground, detectTerminalModeHeuristic, debugTerminalEnvironment, } from "./terminal-detector.js";
import { isESLintCategory } from "../shared/constants.js";
// Import unified orchestrator and configuration bridge
import { UnifiedOrchestrator } from "../services/unified-orchestrator.js";
import { createUnifiedConfigFromFlags, createWatchModeConfig, createPRDConfig, } from "./unified-orchestrator-bridge.js";
// Parse command line arguments with Zod validation for security
import { safeCLIArgumentsParse, safeEnvironmentAccess, } from "../utils/validation-schemas.js";
// Static imports for better testability
import { writeFile, readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { checkEnvironmentCompatibility } from "../utils/node-compatibility.js";
/**
* Process violations into summary format for session state
*/
export function processViolationSummary(violations) {
const bySource = {};
const byCategory = {};
for (const violation of violations) {
bySource[violation.source] = (bySource[violation.source] || 0) + 1;
byCategory[violation.category] = (byCategory[violation.category] || 0) + 1;
}
return {
total: violations.length,
bySource,
byCategory,
};
}
/**
* Get the current package version from package.json
*/
async function getPackageVersion() {
try {
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
const packageJsonPath = path.join(currentDirectory, "..", "package.json");
const packageJsonContent = await readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonContent);
return packageJson.version;
}
catch {
// Fallback if package.json can't be read
return "0.1.0-alpha.x";
}
}
/**
* Detect if current project uses pnpm
*/
function detectPnpmProject() {
try {
const fs = require("node:fs");
const path = require("node:path");
// Check for pnpm-lock.yaml in current directory and parent directories
let currentDirectory = process.cwd();
while (currentDirectory !== path.dirname(currentDirectory)) {
if (fs.existsSync(path.join(currentDirectory, "pnpm-lock.yaml"))) {
return true;
}
currentDirectory = path.dirname(currentDirectory);
}
// Check user agent (if available)
if (process.env["npm_config_user_agent"]?.includes("pnpm")) {
return true;
}
return false;
}
catch {
return false;
}
}
const arguments_ = process.argv.slice(2);
// Validate environment variables for security
const validatedEnvironment = safeEnvironmentAccess();
// Use secure CLI argument parsing with Zod validation
let flags;
try {
flags = safeCLIArgumentsParse(arguments_);
if (validatedEnvironment.DEBUG) {
console.log("[Security] CLI arguments validated successfully");
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[Security Error] ${errorMessage}`);
console.error("Falling back to safe defaults...");
// Use safe default flags if validation fails
flags = {
help: false,
helpMarkdown: false,
helpQuick: false,
aiContext: false,
watch: false,
includeAny: false,
includeESLint: false,
eslintOnly: false,
archaeology: false,
includeArchaeology: false,
targetPath: ".",
verbose: false,
strict: false,
noCrossoverCheck: false,
failOnCrossover: false,
usePersistence: true,
showBurndown: false,
resetSession: false,
resumeSession: false,
debugTerminal: false,
debug: false,
dataDir: "./data",
generatePRD: false,
installShortcuts: false,
configAction: undefined,
};
}
// Enable debug logging if --debug flag is set
import { DebugLogger, debugLog } from "../utils/debug-logger.js";
if (flags.debug) {
DebugLogger.enable();
DebugLogger.debug("CLI", "Debug mode enabled via --debug flag");
DebugLogger.debug("CLI", "Command line arguments", {
args: arguments_,
flags,
});
}
// Color scheme is now handled via keyboard shortcuts in watch mode (Ctrl+D)
/**
* Show comprehensive AI/LLM context and guidance
*/
function showAIContext() {
console.log(`{
"tool": "SideQuest Code Quality Orchestrator",
"purpose": "Run configuration-agnostic TypeScript and ESLint analysis tailored to your project's existing setup. SideQuest avoids overwriting your configs while surfacing the highest-priority issues first.",
"llm_guidance": {
"preferred_commands": {
"analysis": [
"npm run sidequest:report", // TypeScript violations (non-interactive)
"npm run sidequest:precommit", // Clean CI/pre-commit validation
"npm run sidequest:report:strict" // Strict mode for deep inference and edge cases
],
"archaeology": [
"npx tsx lib/cli.ts --archaeology", // Dead code and duplication analysis
"npx tsx lib/cli.ts --include-archaeology --verbose" // Combined analysis with technical debt
],
"description": "These commands return clean, machine-readable JSON with no prompts. Use them to analyze codebases without side effects."
},
"avoid_commands": [
"npm run sidequest:start", // Enables watch mode – too noisy for LLMs
"npm run sidequest:config", // Triggers interactive config wizard
"npx tsx cli.ts" // Inconsistent behavior in some shells
],
"workflow": {
"step_1": "Run 'npm run sidequest:report' to catch TypeScript errors.",
"step_2": "Resolve all TS compilation issues first – they block builds.",
"step_3": "Optionally run 'npm run sidequest:precommit' for clean CI/pre-commit validation.",
"step_4": "For technical debt analysis, run 'npx tsx lib/cli.ts --archaeology --verbose'.",
"step_5": "Validate with 'npm run typecheck' and 'npm run test'."
}
},
"rationale": {
"separation_of_concerns": {
"why": "TypeScript enforces type safety; ESLint enforces code style. Mixing them causes conflicts and slows down analysis.",
"benefit": "TS-only checks run 3–5× faster and reduce noise in watch mode.",
"outcome": "Users get faster feedback and clearer error segmentation."
}
},
"archaeology_features": {
"purpose": "Technical debt analysis and cleanup guidance through dead code detection and duplication analysis.",
"capabilities": {
"dead_code_detection": "Uses ts-prune to find unused exports, unreachable code, and unused imports with confidence scoring.",
"code_duplication": "Uses jscpd to detect duplicate code blocks with similarity analysis and refactoring suggestions.",
"false_positive_filtering": "Smart pattern detection distinguishes CLI functions, public APIs, and utility functions.",
"jsdoc_annotations": "Developer-controlled exclusions with permanent and temporary options."
},
"annotation_syntax": {
"permanent_exclusion": "/** @archaeology-exclude permanent 'CLI entry point used by npm scripts' @since 0.2.0 */",
"temporary_exclusion": "/** @archaeology-exclude temporary 'Will be replaced in next version' @archaeology-recheck-after 0.3.0 @since 0.2.0 */",
"description": "Use JSDoc comments above exports to exclude them from archaeology analysis. Temporary exclusions trigger rechecks after specified versions."
},
"usage_patterns": {
"technical_debt_audit": "npx tsx lib/cli.ts --archaeology --verbose",
"combined_analysis": "npx tsx lib/cli.ts --include-archaeology --verbose",
"watch_with_archaeology": "npm run sidequest:watch --include-archaeology"
}
},
"type_guidance": {
"strategy": "Replace 'any' with meaningful types using docs, 'node_modules', or known interfaces.",
"avoid": "'unknown' is not a safe placeholder. Prefer explicitly defined interfaces.",
"validation": "Consider Zod for enhanced runtime type safety at external boundaries (APIs, user input). Optional but recommended - SideQuest will detect and analyze Zod usage patterns if present."
},
"recommended_libraries": {
"typescript": {
"type-fest": "Utility types (e.g., 'PackageJson', 'Merge') by Sindre Sorhus.",
"ts-reset": "Safe TS defaults by Matt Pocock – improves behavior of 'Object', 'includes', etc.",
"zod": "Schema-based validation with automatic TS inference.",
"utility-types": "Extra tools for structural typing and transformation."
},
"eslint": {
"eslint-config-xo-typescript": "Strict but clean ruleset by Sindre Sorhus.",
"@typescript-eslint/eslint-plugin": "Official TypeScript linting rules.",
"eslint-config-turbo": "Optimized for Vercel-style monorepos.",
"eslint-plugin-total-typescript": "Matt Pocock's plugin for inference-aware linting."
},
"tools": {
"tsup": "Fast TypeScript bundler (uses esbuild under the hood).",
"tsx": "Fast alternative to ts-node for running scripts.",
"type-challenges": "Excellent resource to learn deep TS pattern mastery.",
"ripgrep": "Essential tool: Use 'rg' instead of 'grep' for blazing-fast code searches."
}
},
"watch_mode": {
"command": "npm run sidequest:watch",
"usage": "Enables real-time feedback during dev.",
"note": "Use for iterative workflows. Avoid with LLMs unless batching changes with a human in the loop."
},
"type_discovery": {
"best_practices": [
"Check official docs for accurate types.",
"Browse '@types' packages and source code inside 'node_modules'.",
"Avoid masking issues with 'any' or overuse of utility types – prefer clarity over cleverness.",
"Use 'rg' (ripgrep) instead of 'grep' or 'find' for efficient codebase searches - much faster for type fixing workflows."
]
},
"if_confused": {
"recovery": "Run 'npm run sidequest:ai-context' to reload LLM context.",
"npx_warning": "If 'npx' fails, prefer 'npm run' for consistency and reliability."
},
"philosophy": "SideQuest exists to help – not enforce. LLMs should focus on reducing noise, highlighting meaningful violations, and guiding users toward clean, type-safe, maintainable code."
}`);
}
/**
* Show markdown-formatted help for documentation and human readability
*/
async function showMarkdownHelp() {
const version = await getPackageVersion();
console.log(`# SideQuest Code Quality Orchestrator v${version}
Configuration-agnostic TypeScript and ESLint analysis that respects your project setup.
## 🚀 Quick Start
### For Humans (Interactive)
\`\`\`bash
npm run sidequest:watch # Real-time watch mode
npm run sidequest:config # Manage preferences
\`\`\`
### For LLMs/Automation (JSON Output)
\`\`\`bash
npm run sidequest:report # TypeScript violations only
npm run sidequest:precommit # Clean CI/pre-commit validation
npm run sidequest:ai-context # Full LLM guidance
\`\`\`
## 🎯 Core Philosophy
**Separation of Concerns**: TypeScript handles type safety, ESLint handles code style.
- **3-5x faster** analysis when separated
- **Clearer error segmentation** for developers
- **No rule conflicts** between tools
## 📊 Analysis Modes
| Command | Purpose | Output | Speed |
|---------|---------|---------|--------|
| \`sidequest:report\` | TypeScript compilation errors | JSON | ⚡ Fast |
| \`sidequest:precommit\` | Clean CI/pre-commit check | JSON | 🐌 Thorough |
| \`sidequest:start\` | Real-time monitoring | Interactive | 🔄 Continuous |
## 🔧 Type Safety Best Practices
### Replace \`any\` with Proper Types
1. **Check documentation** for official type definitions
2. **Inspect \`node_modules/@types\`** for accurate interfaces
3. **Use Zod** for runtime validation of unknown data
4. **Avoid \`unknown\` as placeholder** - create explicit interfaces
### Recommended Libraries
- **type-fest**: Essential utility types by Sindre Sorhus
- **ts-reset**: Safer TypeScript defaults by Matt Pocock
- **eslint-config-xo-typescript**: Strict but clean ESLint rules
## 🐛 Troubleshooting
**Setup running every time?**
- Tool auto-detects first run based on preferences + database existence
- Reset: \`npm run sidequest:config:reset\`
**Colors wrong?**
- \`npm run sidequest:watch:dark\` or \`npm run sidequest:watch:light\`
**Command not found?**
- Use \`npm run sidequest:*\` (not \`npm sidequest:*\`)
## 💡 Help & Context
- \`npm run sidequest:help\` - This help
- \`npm run sidequest:help:quick\` - One-liner summary
- \`npm run sidequest:ai-context\` - Full LLM guidance
`);
}
/**
* Show quick one-liner help for tooltips and inline guidance
*/
function showQuickHelp() {
console.log("SideQuest: Use 'sidequest:report' for clean TS analysis, 'sidequest:start' for watch mode. Separates TS (types) from ESLint (style) for 3x speed. Run 'sidequest:ai-context' for full LLM guidance.");
}
/**
* Display help information
*/
async function showHelp() {
const version = await getPackageVersion();
console.log(`
📊 SideQuest Code Quality Orchestrator v${version}
Configuration-agnostic TypeScript and ESLint analysis that respects your project setup
COMMON COMMANDS:
sidequest --watch Watch mode with auto-detected colors
npm run sidequest:watch:light Watch mode for light terminals (Novel/Man Page)
npm run sidequest:watch:dark Watch mode for dark terminals
npm run sidequest:watch:eslint Watch mode with ESLint included (optional)
FOR LLMS/AI ASSISTANTS (Machine-Readable JSON):
npm run sidequest:report Clean JSON output, no interactive prompts
npm run sidequest:precommit Clean validation for CI/pre-commit hooks (JSON)
npm run sidequest:report:strict Strict mode analysis (JSON)
npm run sidequest:archaeology Technical debt analysis (dead code + duplication)
npm run sidequest:debt Combined analysis with technical debt
OTHER COMMANDS:
npm run sidequest:prd Generate PRD file for task management
npm run sidequest:burndown Show historical violation trends
npm run sidequest:session:reset Reset session baseline
ANALYSIS OPTIONS:
--watch Enable real-time watch mode
--include-eslint Include ESLint violations (optional)
--include-any Include TypeScript 'any' pattern violations (optional)
--archaeology Run technical debt analysis (dead code + duplication)
--include-archaeology Add archaeology analysis to standard checks
--path <dir> Target directory (default: app)
--color-scheme <mode> Color mode: auto, light, dark
--data-dir <dir> Database directory (default: ./data)
--resume Resume previous watch session with stats
--reset-session Clear session state and start fresh
OUTPUT OPTIONS:
--verbose Detailed JSON output format
--burndown Show burndown analysis
--prd Generate PRD file for task master ingestion
CONFIGURATION:
--config [action] Manage user preferences
show (default) - Display current preferences
edit - Open preferences in editor
reset - Reset to defaults
--install-shortcuts Manually install package.json shortcuts (pnpm fix)
EXAMPLES:
# Start watching with auto-detected colors
sidequest --watch
# Force light mode for Novel terminal
sidequest --watch --color-scheme light
# Force dark mode for black terminals
sidequest --watch --color-scheme dark
# Watch with ESLint analysis
sidequest --watch --include-eslint
# One-time analysis with ESLint
sidequest --include-eslint
# Generate verbose JSON output
sidequest --verbose
# Generate PRD file for task master
sidequest --prd
# Custom data directory (project-scoped)
sidequest --data-dir ./project-data
# Global data directory (user-scoped)
sidequest --data-dir ~/.cqo-data
# Show violation trends over time
sidequest --burndown
# Technical debt analysis (dead code + duplication)
sidequest --archaeology
# Combined analysis with archaeology
sidequest --include-archaeology --verbose
# Watch mode with technical debt tracking
sidequest --watch --include-archaeology
# Debug color detection
sidequest --debug-terminal
# Show current configuration
sidequest --config
# Edit preferences
sidequest --config edit
# Reset preferences to defaults
sidequest --config reset
# Install shortcuts manually (pnpm users)
sidequest --install-shortcuts
# Resume previous watch session
sidequest --watch --resume
# Reset session and start fresh
sidequest --watch --reset-session
TROUBLESHOOTING:
If colors look wrong, use explicit mode:
sidequest --watch --color-scheme dark # For black/dark terminals
sidequest --watch --color-scheme light # For white/light terminals
FEATURES:
🚀 Automatic terminal color detection (OSC + heuristics)
📊 SQLite persistence with historical tracking
📈 Burndown metrics and trend analysis
⚡ Real-time updates with smooth UX
🎨 APCA-compliant light/dark color schemes
📂 Configurable data directory for project or global storage
🏺 Code archaeology: dead code detection and duplication analysis
📝 JSDoc annotations for false positive control
JSDOC ARCHAEOLOGY ANNOTATIONS:
Control false positives in dead code detection with JSDoc comments:
Permanent exclusion (CLI functions, public APIs):
/**
* @archaeology-exclude permanent "CLI entry point used by npm scripts"
* @since 0.2.0
*/
export function myFunction() { ... }
Temporary exclusion (planned refactoring):
/**
* @archaeology-exclude temporary "Will be replaced by Zod validation"
* @archaeology-recheck-after 0.3.0
* @since 0.2.0
*/
export function legacyFunction() { ... }
DATA DIRECTORY:
By default, creates './data/' in current working directory.
Use --data-dir to specify custom location:
Project mode: --data-dir ./project-data
Global mode: --data-dir ~/.cqo-data
Temp mode: --data-dir /tmp/cqo-analysis
TROUBLESHOOTING:
Setup running every time?
First-run setup should only happen once. If it keeps running:
• Check ~/.sidequest-cqo/user-preferences.json exists
• Try: npm run sidequest:config:reset
Command not found: sidequest:watch?
Don't use: npm sidequest:watch
Use: npm run sidequest:watch
pnpm users: If shortcuts weren't added during install:
Run: npx sidequest-cqo --install-shortcuts
Want to skip interactive setup?
Use: npm run sidequest:report (for LLMs/automation)
Or: Delete ~/.sidequest-cqo/ and ./data/ if setup is corrupted
Colors look wrong?
In watch mode: Press Ctrl+D to toggle light/dark mode
For debugging: npm run sidequest:debug:terminal
`);
}
/**
* Get color scheme for terminal output with light/dark mode support
*/
function getColorScheme() {
const colorMode = validatedEnvironment.TERM_COLOR_MODE || detectTerminalMode();
return colorMode === "light"
? {
// Light mode: Replicate macOS Terminal "Man Page" theme colors
reset: "\u001B[0m",
bold: "\u001B[1m",
dim: "\u001B[2m",
primary: "\u001B[30m", // Black text (Man Page style)
secondary: "\u001B[90m", // Dark gray
info: "\u001B[34m", // Deep blue
success: "\u001B[32m", // Deep green
warning: "\u001B[33m", // Amber/brown
error: "\u001B[31m", // Deep red
muted: "\u001B[37m", // Medium gray
accent: "\u001B[36m", // Cyan
header: "\u001B[35m", // Purple (Man Page style)
}
: {
// Dark mode: Replicate macOS Terminal "Pro" theme colors
reset: "\u001B[0m",
bold: "\u001B[1m",
dim: "\u001B[2m",
primary: "\u001B[97m", // Bright white (Pro theme style)
secondary: "\u001B[37m", // Light gray
info: "\u001B[94m", // Bright blue (Pro theme blue)
success: "\u001B[92m", // Bright green (Pro theme green)
warning: "\u001B[93m", // Bright yellow (Pro theme yellow)
error: "\u001B[91m", // Bright red (Pro theme red)
muted: "\u001B[90m", // Dim gray
accent: "\u001B[95m", // Bright magenta
header: "\u001B[96m", // Bright cyan (Pro theme cyan)
};
}
/**
* Detect terminal color mode using various heuristics
*/
function detectTerminalMode() {
return detectTerminalModeHeuristic();
}
// Removed old complex display function - using clean developer display instead
/**
* Create WatchController with all required dependencies
*/
async function createWatchController(flags, orchestrator, sessionManager, colors) {
debugLog("CLI", "Starting createWatchController...");
// Create unified orchestrator for violation collection (SILENT mode for watch)
debugLog("CLI", "Creating unified orchestrator for watch mode...");
const unifiedConfig = createWatchModeConfig(flags);
const unifiedOrchestrator = new UnifiedOrchestrator(unifiedConfig);
await unifiedOrchestrator.initialize();
debugLog("CLI", "Unified orchestrator created and initialized successfully");
// Get the clean developer display
debugLog("CLI", "Getting developer watch display...");
const watchDisplay = getDeveloperWatchDisplay();
debugLog("CLI", "Watch display obtained");
debugLog("CLI", "Creating WatchController instance...");
return new WatchController({
flags,
orchestrator,
sessionManager,
display: watchDisplay,
legacyOrchestrator: unifiedOrchestrator, // Use unified orchestrator instead of legacy
colors,
});
}
/**
* Display burndown analysis
*/
async function displayBurndownAnalysis(orchestrator) {
const colors = getColorScheme();
console.log(`${colors.bold}${colors.header}📈 Burndown Analysis${colors.reset}\n`);
try {
const analysisService = orchestrator.getAnalysisService();
const timeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago
end: new Date(),
};
// Get violation trends
await analysisService.getViolationTrends(timeRange);
const stats = await analysisService.calculateViolationStats(timeRange);
console.log(`${colors.warning}24-Hour Summary:${colors.reset}`);
console.log(`${colors.secondary} Total violations: ${colors.primary}${stats.total}${colors.reset}`);
console.log(`${colors.secondary} Files affected: ${colors.primary}${stats.filesAffected}${colors.reset}`);
console.log(`${colors.secondary} Avg per file: ${colors.primary}${stats.avgPerFile.toFixed(1)}${colors.reset}\n`);
// Show category breakdown
console.log(`${colors.warning}By Category:${colors.reset}`);
Object.entries(stats.byCategory)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.forEach(([category, count]) => {
const percentage = ((count / stats.total) * 100).toFixed(1);
console.log(` ${colors.info}${category}:${colors.reset} ${colors.primary}${count}${colors.reset} ${colors.secondary}(${percentage}%)${colors.reset}`);
});
// Show rule recommendations
const recommendations = await analysisService.recommendRuleFrequencies();
if (recommendations.length > 0) {
console.log(`\n${colors.warning}Rule Frequency Recommendations:${colors.reset}`);
recommendations
.slice(0, 5)
.forEach((rec) => {
const currentFreq = Math.round(rec.currentFrequency / 1000);
const recommendedFreq = Math.round(rec.recommendedFrequency / 1000);
console.log(` ${colors.info}${rec.rule}:${colors.reset} ${currentFreq}s → ${recommendedFreq}s ${colors.secondary}(${rec.reasoning})${colors.reset}`);
});
}
}
catch (error) {
console.log(`${colors.error}Error generating burndown analysis: ${error}${colors.reset}`);
}
}
/**
* Process violations using the new persistence system
*/
// processViolationsWithPersistence function removed -
// persistence is now handled automatically by UnifiedOrchestrator
/**
* Legacy display function for compatibility
*/
function displayConsoleResults(result) {
const colors = getColorScheme();
const { violations, summary, totalExecutionTime } = result;
console.log(`\n${colors.bold}${colors.header}📊 Code Quality Analysis Results${colors.reset}`);
console.log(`${colors.secondary}Total violations: ${colors.primary}${summary.total}${colors.reset}`);
if (summary.bySource.typescript > 0 ||
summary.bySource.eslint > 0 ||
summary.bySource["unused-exports"] > 0) {
console.log(`\n${colors.warning}By Source:${colors.reset}`);
if (summary.bySource.typescript > 0) {
console.log(` 📝 ${colors.info}TypeScript:${colors.reset} ${colors.primary}${summary.bySource.typescript}${colors.reset}`);
}
if (summary.bySource.eslint > 0) {
console.log(` 🔍 ${colors.info}ESLint:${colors.reset} ${colors.primary}${summary.bySource.eslint}${colors.reset}`);
}
if (summary.bySource["unused-exports"] > 0) {
console.log(` 🗂️ ${colors.info}Unused Exports:${colors.reset} ${colors.primary}${summary.bySource["unused-exports"]}${colors.reset}`);
}
if (summary.bySource["zod-detection"] > 0) {
console.log(` 🛡️ ${colors.info}Zod Detection:${colors.reset} ${colors.primary}${summary.bySource["zod-detection"]}${colors.reset}`);
}
}
// Enhanced Zod Analysis Section
if (summary.bySource["zod-detection"] > 0) {
displayZodAnalysisSection(violations, colors);
}
console.log(`\n${colors.warning}By Category:${colors.reset}`);
Object.entries(summary.byCategory)
.sort(([, a], [, b]) => b - a)
.forEach(([category, count]) => {
const isESLint = isESLintCategory(category);
const prefix = isESLint ? "🔍" : "📝";
const severity = violations.find((v) => v.category === category)
?.severity || "info";
const severityIcon = severity === "error" ? "❌" : severity === "warn" ? "⚠️" : "ℹ️";
console.log(` ${severityIcon} ${prefix} ${colors.info}${category}:${colors.reset} ${colors.primary}${count}${colors.reset}`);
});
console.log(`\n${colors.muted}Analysis completed in ${totalExecutionTime}ms${colors.reset}`);
}
/**
* Display enhanced Zod analysis section with coverage metrics
*/
function displayZodAnalysisSection(violations, colors) {
console.log(`\n${colors.bold}${colors.header}🛡️ Zod Analysis${colors.reset}`);
// Extract Zod coverage data from violations
const zodViolations = violations.filter((v) => v.source === "zod-detection");
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
console.log(`${colors.secondary} Coverage: ${colors.primary}${coverage}%${colors.reset} ${colors.secondary}(${usedSchemas}/${totalSchemas} schemas used)${colors.reset}`);
console.log(`${colors.secondary} Risk Level: ${riskColor}${riskLevel}${colors.reset}`);
console.log(`${colors.secondary} Parse Safety: ${colors.primary}${parseCallsCount} unsafe${colors.reset}${colors.secondary}, ${colors.primary}${safeParseCallsCount} safe${colors.reset} ${colors.secondary}calls${colors.reset}`);
console.log(`${colors.secondary} Baseline: ${colors.info}${baseline}${colors.reset}`);
}
/**
* Generate PRD file for Claude Task Master ingestion
* @archaeology-exclude permanent "CLI entry point used by --prd flag"
* @since 0.2.0
*/
export async function generatePRD(violations, targetPath) {
const colors = getColorScheme();
const timestamp = new Date().toISOString().split("T")[0];
// Analyze violations for PRD content
const totalViolations = violations.length;
const filesAffected = new Set(violations.map((v) => v.file)).size;
const categoryBreakdown = {};
for (const violation of violations) {
categoryBreakdown[violation.category] =
(categoryBreakdown[violation.category] || 0) + 1;
}
const topCategories = Object.entries(categoryBreakdown)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([category, count]) => ({
category,
count,
percentage: ((count / totalViolations) * 100).toFixed(1),
}));
const errorCount = violations.filter((v) => v.severity === "error").length;
const warningCount = violations.filter((v) => v.severity === "warn").length;
// Generate PRD content
const prdContent = `# Code Quality Improvement PRD
Generated: ${timestamp}
Target: ${targetPath}
## Executive Summary
This codebase requires systematic code quality improvements to address ${totalViolations} violations across ${filesAffected} files. The analysis reveals patterns that can be addressed through focused development tasks.
## Problem Statement
**Current State:**
- ${errorCount} errors requiring immediate attention
- ${warningCount} warnings impacting code quality
- ${filesAffected} files affected across the codebase
- Primary issues in: ${topCategories
.slice(0, 3)
.map((c) => c.category)
.join(", ")}
**Impact:**
- Developer productivity hindered by type safety issues
- Code maintainability reduced by linting violations
- Technical debt accumulating in core files
## Solution Overview
Implement a systematic code quality improvement process targeting the highest-impact violations first.
## Detailed Requirements
### Priority 1: Critical Errors (${errorCount} items)
${errorCount > 0
? violations
.filter((v) => v.severity === "error")
.slice(0, 5)
.map((v) => `- **${v.category}**: ${v.message} (${v.file}:${v.line})`)
.join("\n")
: "- No critical errors found"}
### Priority 2: High-Impact Categories
${topCategories
.slice(0, 5)
.map((cat) => `- **${cat.category}**: ${cat.count} violations (${cat.percentage}% of total)`)
.join("\n")}
### Priority 3: File-Based Cleanup
Top affected files for focused remediation:
${[...new Set(violations.map((v) => v.file))]
.map((file) => ({
file,
count: violations.filter((v) => v.file === file).length,
}))
.sort((a, b) => b.count - a.count)
.slice(0, 10)
.map((f) => `- ${f.file}: ${f.count} violations`)
.join("\n")}
## Technical Approach
### Phase 1: Foundation (Week 1)
1. Set up automated linting and type checking
2. Fix critical errors preventing builds
3. Establish baseline metrics
### Phase 2: Systematic Cleanup (Weeks 2-3)
1. Address top 3 violation categories systematically
2. Implement proper TypeScript patterns
3. Refactor highest-impact files
### Phase 3: Prevention (Week 4)
1. Add pre-commit hooks
2. Configure CI/CD quality gates
3. Document coding standards
## Acceptance Criteria
- [ ] Zero critical errors (type checking passes)
- [ ] Reduce total violations by 80%
- [ ] All modified files pass linting
- [ ] Quality metrics tracked and improving
## Risk Assessment
**Low Risk:**
- Automated tooling reduces human error
- Incremental approach allows for testing
- Focus on highest-impact items first
**Mitigation:**
- Thorough testing after each phase
- Rollback plan for any breaking changes
- Continuous integration validation
## Success Metrics
- Violation count: ${totalViolations} → Target: <${Math.ceil(totalViolations * 0.2)}
- Files affected: ${filesAffected} → Target: <${Math.ceil(filesAffected * 0.5)}
- Build time: Measure and maintain/improve
- Developer satisfaction: Survey before/after
## Implementation Timeline
**Week 1:** Foundation setup and critical fixes
**Week 2:** Top category remediation (${topCategories
.slice(0, 2)
.map((c) => c.category)
.join(", ")})
**Week 3:** File-focused cleanup and remaining violations
**Week 4:** Prevention systems and documentation
## Resource Requirements
- 1 Senior Developer (40 hours)
- 1 Junior Developer for testing (20 hours)
- Code review bandwidth (10 hours)
---
*Generated by SideQuest Code Quality Orchestrator*
*For task ingestion by: https://github.com/eyaltoledano/claude-task-master*
`;
// Write PRD file
const prdPath = `${targetPath}/CODE_QUALITY_PRD.md`;
try {
await writeFile(prdPath, prdContent, "utf8");
console.log(`${colors.success}✅ PRD generated: ${prdPath}${colors.reset}`);
console.log(`${colors.info}📋 Ready for Claude Task Master ingestion${colors.reset}`);
}
catch (error) {
console.error(`${colors.error}❌ Failed to write PRD file: ${error}${colors.reset}`);
}
}
/**
* Check for first-time setup and run if needed
*/
async function checkAndRunFirstTimeSetup() {
try {
debugLog("CLI", "Importing InteractiveSetup module");
const { InteractiveSetup } = await import("../services/interactive-setup.js");
debugLog("CLI", "InteractiveSetup module imported successfully");
debugLog("CLI", "Checking if setup should run", { dataDir: flags.dataDir });
if (InteractiveSetup.shouldRunSetup(flags.dataDir)) {
debugLog("CLI", "Setup is required, starting interactive setup");
const colors = getColorScheme();
const setup = new InteractiveSetup(colors, flags.dataDir);
debugLog("CLI", "Running interactive setup");
await setup.runSetup();
debugLog("CLI", "Interactive setup completed");
return true; // Setup was run
}
debugLog("CLI", "Setup not required, continuing");
return false; // No setup needed
}
catch (error) {
debugLog("CLI", "Error during setup check", error);
console.warn("[Setup] Could not run first-time setup:", error);
return false;
}
}
/**
* Handle shortcuts installation for pnpm compatibility
*/
async function handleInstallShortcuts() {
const colors = getColorScheme();
try {
console.log(`${colors.info}📦 Installing SideQuest shortcuts...${colors.reset}`);
// Import and run the postinstall script directly
const { execSync } = await import("node:child_process");
// eslint-disable-next-line unicorn/import-style
const pathModule = await import("node:path");
const path = pathModule.default;
const { fileURLToPath } = await import("node:url");
// Get the package installation directory
const currentModuleUrl = import.meta.url;
const currentDirectory = path.dirname(fileURLToPath(currentModuleUrl));
// In published package: dist/lib/cli.js -> go up twice to package root
const packageRoot = path.join(currentDirectory, "..", "..");
const postinstallScript = path.join(packageRoot, "scripts", "postinstall.cjs");
// Run the postinstall script
execSync(`node "${postinstallScript}"`, {
stdio: "inherit",
cwd: process.cwd(),
});
console.log(`${colors.success}✅ Shortcuts installation completed!${colors.reset}`);
console.log(`${colors.info}💡 Try: pnpm run sidequest:help${colors.reset}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`${colors.error}❌ Failed to install shortcuts: ${errorMessage}${colors.reset}`);
console.error(`${colors.warning}💡 You can add scripts manually to package.json:${colors.reset}`);
console.log(`
{
"scripts": {
"sidequest:report": "sidequest-cqo --verbose",
"sidequest:watch": "sidequest-cqo --watch",
"sidequest:config": "sidequest-cqo --config",
"sidequest:help": "sidequest-cqo --help"
}
}`);
}
}
/**
* Handle configuration commands
*/
async function handleConfigCommand(action) {
const colors = getColorScheme();
try {
// Import preferences manager
const { PreferencesManager } = await import("../services/preferences-manager.js");
const prefs = PreferencesManager.getInstance(flags.dataDir);
switch (action) {
case "show": {
console.log(`${colors.bold}${colors.header}📋 Current User Preferences${colors.reset}\n`);
const allPrefs = prefs.getAllPreferences();
console.log(JSON.stringify(allPrefs, undefined, 2));
break;
}
case "edit": {
console.log(`${colors.info}Opening preferences file in editor...${colors.reset}`);
// TODO: Open in user's preferred editor
console.log(`${colors.secondary}Edit: ~/.sidequest-cqo/user-preferences.json${colors.reset}`);
break;
}
case "reset": {
console.log(`${colors.warning}⚠️ Resetting all preferences to defaults...${colors.reset}`);
prefs.resetToDefaults();
console.log(`${colors.success}✅ Preferences reset successfully${colors.reset}`);
break;
}
default: {
console.log(`${colors.error}❌ Unknown config action: ${action}${colors.reset}`);
console.log(`${colors.secondary}Available actions: show, edit, reset${colors.reset}`);
break;
}
}
}
catch (error) {
console.error(`${colors.error}❌ Configuration error: ${error}${colors.reset}`);
}
}
/**
* Intercept and provide helpful suggestions for common user errors
*/
function interceptCommonErrors() {
const arguments_ = new Set(process.argv.slice(2));
const colors = getColorScheme();
// Check for npm run sidequest --watch (common mistake)
if (process.env["npm_command"] === "run-script" &&
process.env["npm_config_argv"]) {
try {
const npmArguments = JSON.parse(process.env["npm_config_argv"]);
if (npmArguments.original?.includes("--watch")) {
console.log(`${colors.warning}💡 Command Suggestion${colors.reset}
It looks like you tried: ${colors.error}npm run sidequest --watch${colors.reset}
Try one of these instead:
${colors.success}npm run sidequest:watch${colors.reset} # Start watching (recommended)
${colors.success}npm run sidequest:watch:eslint${colors.reset} # Include ESLint
${colors.success}npm run sidequest:watch:strict${colors.reset} # Strict mode analysis
${colors.info}Note:${colors.reset} npm doesn't support flags after script names. Use the specific script instead.
`);
process.exit(0);
}
}
catch {
// Ignore parsing errors
}
}
// Check for unknown npm script attempts
if (process.env["npm_command"] === "run-script" &&
process.env["npm_lifecycle_event"]) {
const scriptName = process.env["npm_lifecycle_event"];
// Check for common sidequest: variations that don't exist
if (scriptName &&
scriptName.startsWith("sidequest") &&
!scriptName.includes(":")) {
const isPnpm = detectPnpmProject();
const packageManager = isPnpm ? "pnpm" : "npm";
const runCommand = isPnpm ? "" : "run ";
console.log(`${colors.error}❌ Script Not Found${colors.reset}
The command "${colors.error}${packageManager} ${runCommand}${scriptName}${colors.reset}" doesn't exist.
${isPnpm
? `${colors.warning}🔧 pnpm Setup Required${colors.reset}
SideQuest shortcuts need to be installed for pnpm projects:
${colors.success}npx sidequest-cqo --install-shortcuts${colors.reset}
After installation, you can use:
${colors.success}pnpm sidequest:help${colors.reset} # No "run" needed!
${colors.success}pnpm sidequest:watch${colors.reset} # Direct commands
${colors.success}pnpm sidequest:report${colors.reset} # Clean analysis
${colors.success}pnpm sidequest:ai-context${colors.reset} # LLM guidance
`
: ""}${colors.info}Are you a human?${colors.reset}
${colors.success}${packageManager} ${runCommand}sidequest:help${colors.reset} # Standard help
${colors.success}${packageManager} ${runCommand}sidequest:help:markdown${colors.reset} # Formatted documentation
${colors.info}Are you an LLM?${colors.reset}
${colors.success}${packageManager} ${runCommand}sidequest:ai-context${colors.reset} # Full machine-structured context
${colors.secondary}Common commands:${colors.reset}
${colors.success}${packageManager} ${runCommand}sidequest:watch${colors.reset} # Watch mode (humans)
${colors.success}${packageManager} ${runCommand}sidequest:report${colors.reset} # Analysis (LLMs)
${colors.success}${packageManager} ${runCommand}sidequest:config${colors.reset} # Configuration
`);
process.exit(0);
}
}
// Check for direct npx tsx cli.ts usage (suggest LLM context recovery)
// Only show for truly direct usage, not when npm scripts call the same command
if (process.argv[1]?.includes("cli.ts") &&
!process.env["npm_command"] &&
!process.env["npm_lifecycle_event"] &&
!process.env["npm_lifecycle_script"]) {
const isLikelyLLM = [...arguments_].some((argument) => argument.includes("--verbose"));
if (isLikelyLLM) {
console.log(`${colors.warning}💡 LLM Context Recovery${colors.reset}
It looks like you're running the CLI directly with npx.
${colors.info}Are you an LLM that's lost context?${colors.reset}
Run this to restore full context:
${colors.success}npm run sidequest:ai-context${colors.reset}
${colors.secondary}For consistent behavior, prefer npm scripts:${colors.reset}
${colors.success}npm run sidequest:report${colors.reset} # Clean JSON analysis
${colors.success}npm run sidequest:start${colors.reset} # Watch mode
${colors.secondary}Continuing with your command...${colors.reset}
`);
}
}
}
/**
* Main execution function
*/
async function main() {
// Check Node.js compatibility and warn about unsupported versions
const compatibility = checkEnvironmentCompatibility();
if (!compatibility.compatible) {
const colors = getColorScheme();
console.warn(`${colors.warning}⚠️ Node.js Compatibility Warning:${colors.reset}`);
compatibility.warnings.forEach((warning) => {
console.warn(` ${colors.error}${warning}${colors.reset}`);
});
if (compatibility.recommendations.length > 0) {
console.warn(`${colors.info}Recommendations:${colors.reset}`);
compatibility.recommendations.forEach((rec) => {
console.warn(` • ${rec}`);
});
}
console.warn(); // Empty line for spacing
}
// Intercept common user errors before processing
interceptCommonErrors();
if (flags.help) {
await showHelp();
process.exit(0);
}
if (flags.helpMarkdown) {
await showMarkdownHelp();
process.exit(0);
}
if (flags.helpQuick) {
showQuickHelp();
process.exit(0);
}
if (flags.aiContext) {
showAIContext();
process.exit(0);
}
// Handle configuration commands
if (flags.configAction) {
await handleConfigCommand(flags.configAction);
process.exit(0);
}
// Handle shortcuts installation for pnpm compatibility
if (flags.installShortcuts) {
await handleInstallShortcuts();
process.exit(0);
}
// Check for first-run setup (smart detection)
// Check for first-run setup (smart detection)
// Always run setup check unless in automation mode (verbose flag indicates LLM/automation)
if (flags.verbose) {
debugLog("CLI", "Skipping setup check due to verbose mode (automation detected)");
}
else {
debugLog("CLI", "Checking for first-time setup requirement");
const needsSetup = await checkAndRunFirstTimeSetup();
debugLog("CLI", "Setup check completed", { needsSetup });
if (needsSetup) {
debugLog("CLI", "Setup was required and completed, exiting for user to retry");
// Setup was run, exit to let user try again with their preferences
console.log(`\n${getColorScheme().info}Now try: ${getColorScheme().bold}npm run sidequest:start${getColorScheme().reset}`);
process.exit(0);
}
}
// Show terminal debug info if requested
if (flags.debugTerminal) {
debugTerminalEnvironment();
console.log("\n=== Color Detection Test ===");
const heuristicMode = detectTerminalModeHeuristic();
console.log("Heuristic detection:", heuristicMode);
console.log("\nTesting OSC background detection...");
const oscDetection = await detectTerminalBackground();
console.log("OSC detection result:", oscDetection || "Not supported/failed");
console.log("\nFinal color mode:", process.env["TERM_COLOR_MODE"] || oscDetection || heuristicMode);
process.exit(0);
}
debugLog("CLI", "Getting color scheme");
const colors = getColorScheme();
debugLog("CLI", "Color scheme obtained");
// Determine which system to use
debugLog("CLI", "Determining persistence system", {
usePersistence: flags.usePersistence,
});
const usePersistence = flags.usePersistence;
if (usePersistence) {
console.log(`${colors.info}🚀 Using enhanced SQLite persistence system...${colors.reset}`);
try {
// Initialize session manager
const sessionManager = new SessionManager(flags.dataDir);
// Handle session management
if (flags.resetSession) {
await sessionManager.clearSession();
resetAllServices();
resetDeveloperWatchDisplay();
console.log(`${colors.warning}♻️ Session reset - starting fresh...${colors.reset}`);
}
// Create unified orchestrator with custom data