Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
98 changes: 81 additions & 17 deletions src/core/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,66 @@ 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<void> {
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes', options: ListOptions = {}): Promise<void> {
if (mode === 'changes') {
const changesDir = path.join(targetPath, 'openspec', 'changes');

// Check if changes directory exists
try {
await fs.access(changesDir);
} catch {
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({
Expand All @@ -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;
}
Expand Down
60 changes: 46 additions & 14 deletions src/core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MarkdownParser } from './parsers/markdown-parser.js';
export class ViewCommand {
async execute(targetPath: string = '.'): Promise<void> {
const openspecDir = path.join(targetPath, 'openspec');

if (!fs.existsSync(openspecDir)) {
console.error(chalk.red('No openspec directory found'));
process.exit(1);
Expand All @@ -18,21 +18,22 @@ 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) {
console.log(chalk.bold.cyan('\nActive Changes'));
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}%`)}`
);
Expand All @@ -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(
Expand Down Expand Up @@ -111,18 +121,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');
Expand All @@ -141,23 +151,44 @@ export class ViewCommand {
return specs;
}

private async getArchivedChangesData(openspecDir: string): Promise<Array<{ name: string }>> {
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
Expand All @@ -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)`);
Expand Down