Skip to content

Commit f7409ba

Browse files
Enhanced output formatting for CLI commands
1 parent dd992ef commit f7409ba

File tree

9 files changed

+339
-316
lines changed

9 files changed

+339
-316
lines changed

package-lock.json

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@modelcontextprotocol/sdk": "^1.7.0",
4646
"ai": "^4.2.6",
4747
"chalk": "^5.3.0",
48+
"cli-table3": "^0.6.5",
4849
"commander": "^11.0.0",
4950
"glob": "^10.3.10",
5051
"zod": "^3.22.4",

src/client/cli.ts

Lines changed: 45 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { TaskManager } from "../server/TaskManager.js";
1010
import { createError, normalizeError } from "../utils/errors.js";
1111
import { formatCliError } from "./errors.js";
12+
import { formatProjectsList, formatTaskProgressTable } from "./taskFormattingUtils.js";
1213

1314
const program = new Command();
1415

@@ -28,7 +29,6 @@ program.hook('preAction', (thisCommand, actionCommand) => {
2829
const envFilePath = process.env.TASK_MANAGER_FILE_PATH;
2930
const resolvedPath = cliFilePath || envFilePath || undefined;
3031

31-
console.log(chalk.blue(`Using task file path determined by CLI/Env: ${resolvedPath || 'TaskManager Default'}`));
3232
try {
3333
taskManager = new TaskManager(resolvedPath);
3434
} catch (error) {
@@ -306,8 +306,8 @@ program
306306

307307
program
308308
.command("list")
309-
.description("List all projects and their tasks")
310-
.option('-p, --project <projectId>', 'Show details for a specific project')
309+
.description("List project summaries, or list tasks for a specific project")
310+
.option('-p, --project <projectId>', 'Show details and tasks for a specific project')
311311
.option('-s, --state <state>', "Filter by task/project state (open, pending_approval, completed, all)")
312312
.action(async (options) => {
313313
try {
@@ -319,160 +319,79 @@ program
319319
console.log(chalk.yellow(`Valid states are: ${validStates.join(', ')}`));
320320
process.exit(1);
321321
}
322-
// Use 'undefined' if state is 'all' or not provided, as TaskManager methods expect TaskState or undefined
323322
const filterState = (stateOption === 'all' || !stateOption) ? undefined : stateOption as TaskState;
324323

325324
if (options.project) {
326325
// Show details for a specific project
327326
const projectId = options.project;
328-
329-
// Fetch project details for display first
330-
let projectDetailsResponse;
331327
try {
332-
projectDetailsResponse = await taskManager.readProject(projectId);
333-
if ('error' in projectDetailsResponse) {
334-
throw projectDetailsResponse.error;
335-
}
336-
if (projectDetailsResponse.status !== "success") {
337-
throw createError(ErrorCode.InvalidResponseFormat, "Unexpected response format from TaskManager");
338-
}
339-
const project = projectDetailsResponse.data;
340-
341-
// Fetch tasks for this project, applying state filter
342-
const tasksResponse = await taskManager.listTasks(projectId, filterState);
343-
// Check for success before accessing data
344-
const tasks = tasksResponse.status === 'success' ? tasksResponse.data.tasks : [];
345-
346-
console.log(chalk.cyan(`\n📋 Project ${chalk.bold(projectId)} details:`));
347-
console.log(` - ${chalk.bold('Initial Prompt:')} ${project.initialPrompt}`);
348-
if (project.projectPlan && project.projectPlan !== project.initialPrompt) {
349-
console.log(` - ${chalk.bold('Project Plan:')} ${project.projectPlan}`);
328+
const projectResponse = await taskManager.readProject(projectId);
329+
if ('error' in projectResponse) throw projectResponse.error;
330+
if (projectResponse.status !== "success") throw createError(ErrorCode.InvalidResponseFormat, "Unexpected response");
331+
332+
const project = projectResponse.data;
333+
334+
// Filter tasks based on state if provided
335+
const tasksToList = filterState
336+
? project.tasks.filter((task) => {
337+
if (filterState === 'open') return task.status !== 'done';
338+
if (filterState === 'pending_approval') return task.status === 'done' && !task.approved;
339+
if (filterState === 'completed') return task.status === 'done' && task.approved;
340+
return true; // Should not happen
341+
})
342+
: project.tasks;
343+
344+
// Use the formatter for the progress table - it now includes the header
345+
const projectForTableDisplay = { ...project, tasks: tasksToList };
346+
console.log(formatTaskProgressTable(projectForTableDisplay));
347+
348+
if (tasksToList.length === 0) {
349+
console.log(chalk.yellow(`\nNo tasks found${filterState ? ` matching state '${filterState}'` : ''} in project ${projectId}.`));
350+
} else if (filterState) {
351+
console.log(chalk.dim(`(Filtered by state: ${filterState})`));
350352
}
351-
console.log(` - ${chalk.bold('Status:')} ${project.completed ? chalk.green('Completed ✓') : chalk.yellow('In Progress')}`);
352353

353-
// Show progress info (using data from readProject)
354-
const totalTasks = project.tasks.length;
355-
const completedTasks = project.tasks.filter((t: { status: string }) => t.status === "done").length;
356-
const approvedTasks = project.tasks.filter((t: { approved: boolean }) => t.approved).length;
357-
358-
console.log(chalk.cyan(`\n📊 Progress: ${chalk.bold(`${approvedTasks}/${completedTasks}/${totalTasks}`)} (approved/completed/total)`));
359-
360-
// Create a progress bar
361-
if (totalTasks > 0) {
362-
const bar = '▓'.repeat(approvedTasks) + '▒'.repeat(completedTasks - approvedTasks) + '░'.repeat(totalTasks - completedTasks);
363-
console.log(` ${bar}`);
364-
} else {
365-
console.log(chalk.yellow(' No tasks in this project yet.'));
366-
}
367-
368-
if (tasks.length > 0) {
369-
console.log(chalk.cyan('\n📝 Tasks' + (filterState ? ` (filtered by state: ${filterState})` : '') + ':'));
370-
tasks.forEach((t: {
371-
id: string;
372-
title: string;
373-
status: string;
374-
approved: boolean;
375-
description: string;
376-
completedDetails?: string;
377-
toolRecommendations?: string;
378-
ruleRecommendations?: string;
379-
}) => {
380-
const status = t.status === 'done' ? chalk.green('Done ✓') : t.status === 'in progress' ? chalk.yellow('In Progress ⟳') : chalk.blue('Not Started ○');
381-
const approved = t.approved ? chalk.green('Yes ✓') : chalk.red('No ✗');
382-
console.log(` - ${chalk.bold(t.id)}: ${t.title}`);
383-
console.log(` Status: ${status}, Approved: ${approved}`);
384-
console.log(` Description: ${t.description}`);
385-
if (t.completedDetails) {
386-
console.log(` Completed Details: ${t.completedDetails}`);
387-
}
388-
if (t.toolRecommendations) {
389-
console.log(` Tool Recommendations: ${t.toolRecommendations}`);
390-
}
391-
if (t.ruleRecommendations) {
392-
console.log(` Rule Recommendations: ${t.ruleRecommendations}`);
393-
}
394-
});
395-
} else {
396-
console.log(chalk.yellow(`\nNo tasks found${filterState ? ` matching state '${filterState}'` : ''} in project ${projectId}.`));
397-
}
398354
} catch (error: unknown) {
399-
if (error instanceof Error) {
400-
console.error(chalk.red(`Error fetching details for project ${projectId}: ${error.message}`));
401-
} else {
402-
console.error(chalk.red(`Error fetching details for project ${projectId}: Unknown error`));
403-
}
404-
// Handle ProjectNotFound specifically if desired, otherwise let generic handler catch
405-
const normalized = normalizeError(error);
355+
const normalized = normalizeError(error);
406356
if (normalized.code === ErrorCode.ProjectNotFound) {
407357
console.error(chalk.red(`Project ${chalk.bold(projectId)} not found.`));
408358
// Optionally list available projects
409-
const projectsResponse = await taskManager.listProjects();
410-
if ('error' in projectsResponse) {
411-
throw projectsResponse.error;
412-
}
359+
const projectsResponse = await taskManager.listProjects(); // Fetch summaries
413360
if (projectsResponse.status === "success" && projectsResponse.data.projects.length > 0) {
414361
console.log(chalk.yellow('Available projects:'));
415362
projectsResponse.data.projects.forEach((p: { projectId: string; initialPrompt: string }) => {
416363
console.log(` - ${p.projectId}: ${p.initialPrompt.substring(0, 50)}${p.initialPrompt.length > 50 ? '...' : ''}`);
417364
});
418-
} else {
365+
} else if (projectsResponse.status === "success"){
419366
console.log(chalk.yellow('No projects available.'));
420367
}
368+
// else: error fetching list, handled by outer catch
421369
process.exit(1);
370+
} else {
371+
console.error(chalk.red(formatCliError(normalized)));
372+
process.exit(1);
422373
}
423-
throw error; // Re-throw other errors
424374
}
425375
} else {
426-
// List all projects, applying state filter
427-
const projectsResponse = await taskManager.listProjects(filterState);
428-
// Check for success before accessing data
429-
const projectsToList = projectsResponse.status === 'success' ? projectsResponse.data.projects : [];
376+
// List all projects, potentially filtered
377+
const projectsSummaryResponse = await taskManager.listProjects(filterState);
378+
if ('error' in projectsSummaryResponse) throw projectsSummaryResponse.error;
379+
if (projectsSummaryResponse.status !== "success") throw createError(ErrorCode.InvalidResponseFormat, "Unexpected response");
380+
381+
const projectSummaries = projectsSummaryResponse.data.projects;
430382

431-
if (projectsToList.length === 0) {
383+
if (projectSummaries.length === 0) {
432384
console.log(chalk.yellow(`No projects found${filterState ? ` matching state '${filterState}'` : ''}.`));
433385
return;
434386
}
435387

436-
console.log(chalk.cyan('\n📋 Projects List' + (filterState ? ` (filtered by state: ${filterState})` : '')));
437-
// Fetch full details for progress bar calculation if needed, or use summary data
438-
for (const pSummary of projectsToList) {
439-
try {
440-
const projDetailsResp = await taskManager.readProject(pSummary.projectId);
441-
if ('error' in projDetailsResp) {
442-
throw projDetailsResp.error;
443-
}
444-
if (projDetailsResp.status !== "success") {
445-
throw createError(ErrorCode.InvalidResponseFormat, "Unexpected response format from TaskManager");
446-
}
447-
const p = projDetailsResp.data;
448-
449-
const totalTasks = p.tasks.length;
450-
const completedTasks = p.tasks.filter((t: { status: string }) => t.status === "done").length;
451-
const approvedTasks = p.tasks.filter((t: { approved: boolean }) => t.approved).length;
452-
const status = p.completed ? chalk.green('Completed ✓') : chalk.yellow('In Progress');
453-
454-
console.log(`\n${chalk.bold(p.projectId)}: ${status}`);
455-
console.log(` Initial Prompt: ${p.initialPrompt.substring(0, 100)}${p.initialPrompt.length > 100 ? '...' : ''}`);
456-
console.log(` Progress: ${chalk.bold(`${approvedTasks}/${completedTasks}/${totalTasks}`)} (approved/completed/total)`);
457-
458-
// Create a progress bar
459-
if (totalTasks > 0) {
460-
const bar = '▓'.repeat(approvedTasks) + '▒'.repeat(completedTasks - approvedTasks) + '░'.repeat(totalTasks - completedTasks);
461-
console.log(` ${bar}`);
462-
} else {
463-
console.log(chalk.yellow(' No tasks in this project.'));
464-
}
465-
} catch (error: unknown) {
466-
if (error instanceof Error) {
467-
console.error(chalk.red(`Error fetching details for project ${pSummary.projectId}: ${error.message}`));
468-
} else {
469-
console.error(chalk.red(`Error fetching details for project ${pSummary.projectId}: Unknown error`));
470-
}
471-
}
388+
// Use the formatter directly with the summary data
389+
console.log(chalk.cyan(formatProjectsList(projectSummaries)));
390+
if (filterState) {
391+
console.log(chalk.dim(`(Filtered by state: ${filterState})`));
472392
}
473393
}
474394
} catch (error) {
475-
// Handle errors generally - no need for TaskNotDone handling in list command
476395
console.error(chalk.red(formatCliError(normalizeError(error))));
477396
process.exit(1);
478397
}

0 commit comments

Comments
 (0)