-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat(cli): add --watch flag to view command #685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,77 +5,132 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre | |
| import { MarkdownParser } from './parsers/markdown-parser.js'; | ||
|
|
||
| export class ViewCommand { | ||
| async execute(targetPath: string = '.'): Promise<void> { | ||
| async execute(targetPath: string = '.', options: { watch?: boolean; signal?: AbortSignal } = {}): Promise<void> { | ||
| const openspecDir = path.join(targetPath, 'openspec'); | ||
|
|
||
| if (!fs.existsSync(openspecDir)) { | ||
| console.error(chalk.red('No openspec directory found')); | ||
| process.exit(1); | ||
| throw new Error('No openspec directory found'); | ||
| } | ||
|
|
||
| if (options.watch) { | ||
| let lastOutput = ''; | ||
|
|
||
| const update = async () => { | ||
| try { | ||
| const output = await this.getDashboardOutput(openspecDir); | ||
|
|
||
| // Only update if content changed | ||
| if (output !== lastOutput) { | ||
| lastOutput = output; | ||
| // Clear screen, scrollback and move cursor to home | ||
| process.stdout.write('\x1B[2J\x1B[3J\x1B[H' + output); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR description states the goal is to "preserve terminal scrolling" and avoid forcing scroll, but 🤖 Prompt for AI Agents |
||
| } | ||
| } catch (error) { | ||
| // Clear screen and show error prominently | ||
| const errorOutput = chalk.red(`\nError updating dashboard: ${(error as Error).message}\n`); | ||
| process.stdout.write('\x1B[2J\x1B[3J\x1B[H' + errorOutput); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| // Initial render | ||
| await update(); | ||
|
|
||
| const interval = setInterval(update, 2000); | ||
|
|
||
| // Keep the process running until aborted | ||
| if (options.signal) { | ||
| if (options.signal.aborted) { | ||
| clearInterval(interval); | ||
| return; | ||
| } | ||
|
|
||
| await new Promise<void>((resolve) => { | ||
| options.signal!.addEventListener('abort', () => { | ||
| clearInterval(interval); | ||
| console.log('\nExiting watch mode...'); | ||
| resolve(); | ||
| }); | ||
| }); | ||
| } else { | ||
| await new Promise(() => {}); | ||
| } | ||
| } else { | ||
| const output = await this.getDashboardOutput(openspecDir); | ||
| console.log(output); | ||
| } | ||
| } | ||
|
|
||
| private async getDashboardOutput(openspecDir: string): Promise<string> { | ||
| let output = ''; | ||
| const append = (str: string) => { output += str + '\n'; }; | ||
|
|
||
| console.log(chalk.bold('\nOpenSpec Dashboard\n')); | ||
| console.log('═'.repeat(60)); | ||
| append(chalk.bold('\nOpenSpec Dashboard\n')); | ||
| append('═'.repeat(60)); | ||
|
|
||
| // Get changes and specs data | ||
| const changesData = await this.getChangesData(openspecDir); | ||
| const specsData = await this.getSpecsData(openspecDir); | ||
|
|
||
| // Display summary metrics | ||
| this.displaySummary(changesData, specsData); | ||
| output += this.getSummaryOutput(changesData, specsData); | ||
|
|
||
| // Display draft changes | ||
| if (changesData.draft.length > 0) { | ||
| console.log(chalk.bold.gray('\nDraft Changes')); | ||
| console.log('─'.repeat(60)); | ||
| append(chalk.bold.gray('\nDraft Changes')); | ||
| append('─'.repeat(60)); | ||
| changesData.draft.forEach((change) => { | ||
| console.log(` ${chalk.gray('○')} ${change.name}`); | ||
| append(` ${chalk.gray('○')} ${change.name}`); | ||
| }); | ||
| } | ||
|
|
||
| // Display active changes | ||
| if (changesData.active.length > 0) { | ||
| console.log(chalk.bold.cyan('\nActive Changes')); | ||
| console.log('─'.repeat(60)); | ||
| append(chalk.bold.cyan('\nActive Changes')); | ||
| append('─'.repeat(60)); | ||
| changesData.active.forEach((change) => { | ||
| const progressBar = this.createProgressBar(change.progress.completed, change.progress.total); | ||
| const percentage = | ||
| change.progress.total > 0 | ||
| ? Math.round((change.progress.completed / change.progress.total) * 100) | ||
| : 0; | ||
|
|
||
| console.log( | ||
| append( | ||
| ` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}` | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| // Display completed changes | ||
| if (changesData.completed.length > 0) { | ||
| console.log(chalk.bold.green('\nCompleted Changes')); | ||
| console.log('─'.repeat(60)); | ||
| append(chalk.bold.green('\nCompleted Changes')); | ||
| append('─'.repeat(60)); | ||
| changesData.completed.forEach((change) => { | ||
| console.log(` ${chalk.green('✓')} ${change.name}`); | ||
| append(` ${chalk.green('✓')} ${change.name}`); | ||
| }); | ||
| } | ||
|
|
||
| // Display specifications | ||
| if (specsData.length > 0) { | ||
| console.log(chalk.bold.blue('\nSpecifications')); | ||
| console.log('─'.repeat(60)); | ||
| append(chalk.bold.blue('\nSpecifications')); | ||
| append('─'.repeat(60)); | ||
|
|
||
| // Sort specs by requirement count (descending) | ||
| specsData.sort((a, b) => b.requirementCount - a.requirementCount); | ||
|
|
||
| specsData.forEach(spec => { | ||
| const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements'; | ||
| console.log( | ||
| append( | ||
| ` ${chalk.blue('▪')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}` | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| console.log('\n' + '═'.repeat(60)); | ||
| console.log(chalk.dim(`\nUse ${chalk.white('openspec list --changes')} or ${chalk.white('openspec list --specs')} for detailed views`)); | ||
| append(''); | ||
| append('═'.repeat(60)); | ||
| append(''); | ||
| append(chalk.dim(`Use ${chalk.white('openspec list --changes')} or ${chalk.white('openspec list --specs')} for detailed views`)); | ||
|
|
||
| return output; | ||
| } | ||
|
|
||
| private async getChangesData(openspecDir: string): Promise<{ | ||
|
|
@@ -131,18 +186,18 @@ export class ViewCommand { | |
|
|
||
| private async getSpecsData(openspecDir: string): Promise<Array<{ name: string; requirementCount: number }>> { | ||
| const specsDir = path.join(openspecDir, 'specs'); | ||
|
|
||
| if (!fs.existsSync(specsDir)) { | ||
| return []; | ||
| } | ||
|
|
||
| const specs: Array<{ name: string; requirementCount: number }> = []; | ||
| const entries = fs.readdirSync(specsDir, { withFileTypes: true }); | ||
|
|
||
| for (const entry of entries) { | ||
| if (entry.isDirectory()) { | ||
| const specFile = path.join(specsDir, entry.name, 'spec.md'); | ||
|
|
||
| if (fs.existsSync(specFile)) { | ||
| try { | ||
| const content = fs.readFileSync(specFile, 'utf-8'); | ||
|
|
@@ -161,12 +216,13 @@ export class ViewCommand { | |
| return specs; | ||
| } | ||
|
|
||
| private displaySummary( | ||
| private getSummaryOutput( | ||
| changesData: { draft: any[]; active: any[]; completed: any[] }, | ||
| specsData: any[] | ||
| ): void { | ||
| const totalChanges = | ||
| changesData.draft.length + changesData.active.length + changesData.completed.length; | ||
| ): string { | ||
| let output = ''; | ||
| const append = (str: string) => { output += str + '\n'; }; | ||
|
|
||
| const totalSpecs = specsData.length; | ||
| const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0); | ||
|
|
||
|
|
@@ -179,29 +235,26 @@ export class ViewCommand { | |
| completedTasks += change.progress.completed; | ||
| }); | ||
|
|
||
| changesData.completed.forEach(() => { | ||
| // Completed changes count as 100% done (we don't know exact task count) | ||
| // This is a simplification | ||
| }); | ||
|
|
||
| console.log(chalk.bold('Summary:')); | ||
| console.log( | ||
| append(chalk.bold('Summary:')); | ||
| append( | ||
| ` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements` | ||
| ); | ||
| if (changesData.draft.length > 0) { | ||
| console.log(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`); | ||
| append(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`); | ||
| } | ||
| console.log( | ||
| append( | ||
| ` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress` | ||
| ); | ||
| console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`); | ||
| append(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`); | ||
|
|
||
| if (totalTasks > 0) { | ||
| const overallProgress = Math.round((completedTasks / totalTasks) * 100); | ||
| console.log( | ||
| append( | ||
| ` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)` | ||
| ); | ||
| } | ||
|
|
||
| return output; | ||
| } | ||
|
|
||
| private createProgressBar(completed: number, total: number, width: number = 20): string { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
--watchhere is good, but it currently creates a docs/completions drift:viewstill has no flags insrc/core/completions/command-registry.ts, anddocs/cli.mddoes not mention watch mode. Suggested outcome: update command-registry + docs in the same PR so runtime behavior, generated completions, and docs stay in sync.