mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45d4b6b207 |
+5
-2
@@ -96,11 +96,14 @@ 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('--sort <order>', 'Sort order: "recent" (default) or "name"', 'recent')
|
||||
.option('--json', 'Output as JSON (for programmatic use)')
|
||||
.action(async (options?: { specs?: boolean; changes?: boolean; sort?: string; json?: boolean }) => {
|
||||
try {
|
||||
const listCommand = new ListCommand();
|
||||
const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes';
|
||||
await listCommand.execute('.', mode);
|
||||
const sort = options?.sort === 'name' ? 'name' : 'recent';
|
||||
await listCommand.execute('.', mode, { sort, json: options?.json });
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
|
||||
+98
-8
@@ -9,13 +9,78 @@ interface ChangeInfo {
|
||||
name: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
lastModified: Date;
|
||||
}
|
||||
|
||||
interface ListOptions {
|
||||
sort?: 'recent' | 'name';
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent modification time of any file in a directory (recursive).
|
||||
* Falls back to the directory's own mtime if no files are found.
|
||||
*/
|
||||
async function getLastModified(dirPath: string): Promise<Date> {
|
||||
let latest: Date | null = null;
|
||||
|
||||
async function walk(dir: string): Promise<void> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else {
|
||||
const stat = await fs.stat(fullPath);
|
||||
if (latest === null || stat.mtime > latest) {
|
||||
latest = stat.mtime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dirPath);
|
||||
|
||||
// If no files found, use the directory's own modification time
|
||||
if (latest === null) {
|
||||
const dirStat = await fs.stat(dirPath);
|
||||
return dirStat.mtime;
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date as relative time (e.g., "2 hours ago", "3 days ago")
|
||||
*/
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
const diffMins = Math.floor(diffSecs / 60);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 30) {
|
||||
return date.toLocaleDateString();
|
||||
} else if (diffDays > 0) {
|
||||
return `${diffDays}d ago`;
|
||||
} else if (diffHours > 0) {
|
||||
return `${diffHours}h ago`;
|
||||
} else if (diffMins > 0) {
|
||||
return `${diffMins}m ago`;
|
||||
} else {
|
||||
return 'just now';
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
const { sort = 'recent', json = false } = options;
|
||||
|
||||
if (mode === 'changes') {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
@@ -30,24 +95,48 @@ export class ListCommand {
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
if (json) {
|
||||
console.log(JSON.stringify({ changes: [] }));
|
||||
} else {
|
||||
console.log('No active changes found.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
||||
const changePath = path.join(changesDir, changeDir);
|
||||
const lastModified = await getLastModified(changePath);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total
|
||||
totalTasks: progress.total,
|
||||
lastModified
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
// Sort by preference (default: recent first)
|
||||
if (sort === 'recent') {
|
||||
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
||||
} else {
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// JSON output for programmatic use
|
||||
if (json) {
|
||||
const jsonOutput = changes.map(c => ({
|
||||
name: c.name,
|
||||
completedTasks: c.completedTasks,
|
||||
totalTasks: c.totalTasks,
|
||||
lastModified: c.lastModified.toISOString(),
|
||||
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
|
||||
}));
|
||||
console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
@@ -56,7 +145,8 @@ export class ListCommand {
|
||||
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}`);
|
||||
const timeAgo = formatRelativeTime(change.lastModified);
|
||||
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user