From 5476768a59c7433e5ee31187f3eb5bcd67a9c5ae Mon Sep 17 00:00:00 2001 From: ZEDce Date: Tue, 23 Dec 2025 12:43:32 +0100 Subject: [PATCH] feat: show archived changes in list and view commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --archived and --all flags to the list command to display archived changes. The view dashboard now includes an "Archived Changes" section with a count in the summary. - list --archived: shows only archived changes - list --all: shows both active and archived changes - view: displays archived changes section in dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/cli/index.ts | 6 ++- src/core/list.ts | 98 +++++++++++++++++++++++++++++++++++++++--------- src/core/view.ts | 60 ++++++++++++++++++++++------- 3 files changed, 131 insertions(+), 33 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index e8cb2f53..327100d3 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -95,11 +95,13 @@ program .description('List items (changes by default). Use --specs to list specs.') .option('--specs', 'List specs instead of changes') .option('--changes', 'List changes explicitly (default)') - .action(async (options?: { specs?: boolean; changes?: boolean }) => { + .option('--archived', 'Show only archived changes') + .option('--all', 'Show both active and archived changes') + .action(async (options?: { specs?: boolean; changes?: boolean; archived?: boolean; all?: boolean }) => { try { const listCommand = new ListCommand(); const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes'; - await listCommand.execute('.', mode); + await listCommand.execute('.', mode, { archived: options?.archived, all: options?.all }); } catch (error) { console.log(); // Empty line for spacing ora().fail(`Error: ${(error as Error).message}`); diff --git a/src/core/list.ts b/src/core/list.ts index c815540a..daf1d35f 100644 --- a/src/core/list.ts +++ b/src/core/list.ts @@ -9,13 +9,19 @@ interface ChangeInfo { name: string; completedTasks: number; totalTasks: number; + archived?: boolean; +} + +export interface ListOptions { + archived?: boolean; + all?: boolean; } export class ListCommand { - async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes'): Promise { + async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes', options: ListOptions = {}): Promise { if (mode === 'changes') { const changesDir = path.join(targetPath, 'openspec', 'changes'); - + // Check if changes directory exists try { await fs.access(changesDir); @@ -23,20 +29,46 @@ export class ListCommand { throw new Error("No OpenSpec changes directory found. Run 'openspec init' first."); } + const showArchived = options.archived || options.all; + const showActive = !options.archived || options.all; + // Get all directories in changes (excluding archive) const entries = await fs.readdir(changesDir, { withFileTypes: true }); - const changeDirs = entries - .filter(entry => entry.isDirectory() && entry.name !== 'archive') - .map(entry => entry.name); + const changeDirs = showActive + ? entries + .filter(entry => entry.isDirectory() && entry.name !== 'archive') + .map(entry => entry.name) + : []; - if (changeDirs.length === 0) { - console.log('No active changes found.'); + // Get archived changes if requested + let archivedDirs: string[] = []; + if (showArchived) { + const archiveDir = path.join(changesDir, 'archive'); + try { + await fs.access(archiveDir); + const archiveEntries = await fs.readdir(archiveDir, { withFileTypes: true }); + archivedDirs = archiveEntries + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); + } catch { + // Archive directory doesn't exist, that's fine + } + } + + if (changeDirs.length === 0 && archivedDirs.length === 0) { + if (options.archived) { + console.log('No archived changes found.'); + } else if (options.all) { + console.log('No changes found.'); + } else { + console.log('No active changes found.'); + } return; } - // Collect information about each change + // Collect information about each active change const changes: ChangeInfo[] = []; - + for (const changeDir of changeDirs) { const progress = await getTaskProgressForChange(changesDir, changeDir); changes.push({ @@ -46,17 +78,49 @@ export class ListCommand { }); } + // Collect information about each archived change + const archivedChanges: ChangeInfo[] = []; + const archiveDir = path.join(changesDir, 'archive'); + + for (const changeDir of archivedDirs) { + const progress = await getTaskProgressForChange(archiveDir, changeDir); + archivedChanges.push({ + name: changeDir, + completedTasks: progress.completed, + totalTasks: progress.total, + archived: true + }); + } + // Sort alphabetically by name changes.sort((a, b) => a.name.localeCompare(b.name)); + archivedChanges.sort((a, b) => a.name.localeCompare(b.name)); + + // Display active changes + if (changes.length > 0) { + console.log('Changes:'); + const padding = ' '; + const nameWidth = Math.max(...changes.map(c => c.name.length)); + for (const change of changes) { + const paddedName = change.name.padEnd(nameWidth); + const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks }); + console.log(`${padding}${paddedName} ${status}`); + } + } - // Display results - console.log('Changes:'); - const padding = ' '; - const nameWidth = Math.max(...changes.map(c => c.name.length)); - for (const change of changes) { - const paddedName = change.name.padEnd(nameWidth); - const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks }); - console.log(`${padding}${paddedName} ${status}`); + // Display archived changes + if (archivedChanges.length > 0) { + if (changes.length > 0) { + console.log(''); // Add spacing between sections + } + console.log('Archived Changes:'); + const padding = ' '; + const nameWidth = Math.max(...archivedChanges.map(c => c.name.length)); + for (const change of archivedChanges) { + const paddedName = change.name.padEnd(nameWidth); + const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks }); + console.log(`${padding}${paddedName} ${status}`); + } } return; } diff --git a/src/core/view.ts b/src/core/view.ts index 0a80e619..df21d2d5 100644 --- a/src/core/view.ts +++ b/src/core/view.ts @@ -7,7 +7,7 @@ import { MarkdownParser } from './parsers/markdown-parser.js'; export class ViewCommand { async execute(targetPath: string = '.'): Promise { const openspecDir = path.join(targetPath, 'openspec'); - + if (!fs.existsSync(openspecDir)) { console.error(chalk.red('No openspec directory found')); process.exit(1); @@ -18,10 +18,11 @@ export class ViewCommand { // Get changes and specs data const changesData = await this.getChangesData(openspecDir); + const archivedData = await this.getArchivedChangesData(openspecDir); const specsData = await this.getSpecsData(openspecDir); // Display summary metrics - this.displaySummary(changesData, specsData); + this.displaySummary(changesData, specsData, archivedData); // Display active changes if (changesData.active.length > 0) { @@ -29,10 +30,10 @@ export class ViewCommand { console.log('─'.repeat(60)); changesData.active.forEach(change => { const progressBar = this.createProgressBar(change.progress.completed, change.progress.total); - const percentage = change.progress.total > 0 + const percentage = change.progress.total > 0 ? Math.round((change.progress.completed / change.progress.total) * 100) : 0; - + console.log( ` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}` ); @@ -48,14 +49,23 @@ export class ViewCommand { }); } + // Display archived changes + if (archivedData.length > 0) { + console.log(chalk.bold.gray('\nArchived Changes')); + console.log('─'.repeat(60)); + archivedData.forEach(change => { + console.log(` ${chalk.gray('◦')} ${chalk.gray(change.name)}`); + }); + } + // Display specifications if (specsData.length > 0) { console.log(chalk.bold.blue('\nSpecifications')); console.log('─'.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( @@ -111,18 +121,18 @@ export class ViewCommand { private async getSpecsData(openspecDir: string): Promise> { 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'); @@ -141,23 +151,44 @@ export class ViewCommand { return specs; } + private async getArchivedChangesData(openspecDir: string): Promise> { + const archiveDir = path.join(openspecDir, 'changes', 'archive'); + + if (!fs.existsSync(archiveDir)) { + return []; + } + + const archived: Array<{ name: string }> = []; + const entries = fs.readdirSync(archiveDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + archived.push({ name: entry.name }); + } + } + + archived.sort((a, b) => a.name.localeCompare(b.name)); + return archived; + } + private displaySummary( changesData: { active: any[]; completed: any[] }, - specsData: any[] + specsData: any[], + archivedData: Array<{ name: string }> = [] ): void { const totalChanges = changesData.active.length + changesData.completed.length; const totalSpecs = specsData.length; const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0); - + // Calculate total task progress let totalTasks = 0; let completedTasks = 0; - + changesData.active.forEach(change => { totalTasks += change.progress.total; completedTasks += change.progress.completed; }); - + changesData.completed.forEach(() => { // Completed changes count as 100% done (we don't know exact task count) // This is a simplification @@ -167,7 +198,8 @@ export class ViewCommand { console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`); console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`); console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`); - + console.log(` ${chalk.gray('●')} Archived Changes: ${chalk.bold(archivedData.length)}`); + if (totalTasks > 0) { const overallProgress = Math.round((completedTasks / totalTasks) * 100); console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);