diff --git a/src/cli/commands/task.ts b/src/cli/commands/task.ts new file mode 100644 index 0000000..a5654a4 --- /dev/null +++ b/src/cli/commands/task.ts @@ -0,0 +1,102 @@ +import { input, select } from '@inquirer/prompts'; +import { join } from 'path'; +import { getEntityDir } from '../../core/paths.js'; +import { getNextId } from '../../core/counter.js'; +import { readEntity, writeEntity, listEntities } from '../../core/files.js'; +import { TaskSchema, type Task } from '../../core/schema.js'; +import { Index } from '../../core/index.js'; + +export async function taskCreate(cwd: string, options: Partial = {}): Promise { + const title = options.title ?? await input({ message: 'Task title:' }); + const role = options.role ?? await select({ + message: 'Role:', + choices: [ + { name: 'architect', value: 'architect' }, + { name: 'implementer', value: 'implementer' }, + { name: 'reviewer', value: 'reviewer' }, + { name: 'tester', value: 'tester' }, + ], + }); + const priority = options.priority ?? 'medium'; + + const now = new Date().toISOString(); + const task: Task = TaskSchema.parse({ + id: getNextId(cwd, 'task'), + title, + description: options.description ?? '', + status: 'open', + priority, + role, + createdAt: now, + updatedAt: now, + }); + + const filePath = join(getEntityDir(cwd, 'tasks'), `${task.id}.md`); + writeEntity(filePath, task, `# ${task.title}\n\n${task.description}`); + + const index = new Index(cwd); + index.upsert(toIndexEntry(task, filePath)); + index.close(); + + console.log(`Created ${task.id}: ${task.title}`); +} + +export function taskList(cwd: string, filters?: { status?: string; role?: string }): void { + const index = new Index(cwd); + const tasks = index.list('task', filters); + index.close(); + + if (tasks.length === 0) { + console.log('No tasks found.'); + return; + } + + for (const t of tasks) { + console.log(`${t.id} [${t.status}] (${t.role ?? 'unassigned'}) ${t.title}`); + } +} + +export function taskShow(cwd: string, id: string): void { + const filePath = join(getEntityDir(cwd, 'tasks'), `${id}.md`); + const { frontmatter, body } = readEntity(filePath); + console.log(`# ${frontmatter.title}`); + console.log(`Status: ${frontmatter.status} | Role: ${frontmatter.role ?? '-'} | Priority: ${frontmatter.priority}`); + console.log('\n' + body); +} + +export function taskClaim(cwd: string, id: string, agentName: string): void { + updateTask(cwd, id, { status: 'in_progress', assignedTo: agentName }); + console.log(`${id} claimed by ${agentName}.`); +} + +export function taskDone(cwd: string, id: string): void { + updateTask(cwd, id, { status: 'done' }); + console.log(`${id} marked as done.`); +} + +function updateTask(cwd: string, id: string, patch: Partial): void { + const filePath = join(getEntityDir(cwd, 'tasks'), `${id}.md`); + const { frontmatter, body } = readEntity(filePath); + const task = TaskSchema.parse({ ...frontmatter, ...patch, updatedAt: new Date().toISOString() }); + writeEntity(filePath, task, body); + + const index = new Index(cwd); + index.upsert(toIndexEntry(task, filePath)); + index.close(); +} + +function toIndexEntry(task: Task, filePath: string) { + return { + id: task.id, + type: 'task', + title: task.title, + content: `${task.description} ${task.tags.join(' ')}`, + filePath, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + status: task.status, + role: task.role, + assignedTo: task.assignedTo, + tags: JSON.stringify(task.tags), + }; +} diff --git a/tests/task-cmd.test.ts b/tests/task-cmd.test.ts new file mode 100644 index 0000000..dea0c5e --- /dev/null +++ b/tests/task-cmd.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { taskCreate, taskList, taskDone } from '../src/cli/commands/task.js'; +import { init } from '../src/cli/commands/init.js'; + +describe('task commands', () => { + let cwd: string; + + beforeEach(async () => { + cwd = mkdtempSync(join(tmpdir(), 'agenthub-')); + await init(cwd, { yes: true, projectName: 'test' }); + }); + + afterEach(() => { + rmSync(cwd, { recursive: true, force: true }); + }); + + it('creates and lists tasks', async () => { + await taskCreate(cwd, { title: 'Test task', role: 'implementer', priority: 'high' }); + const logs: string[] = []; + const original = console.log; + console.log = (msg: string) => logs.push(msg); + taskList(cwd); + console.log = original; + expect(logs.some((m) => m.includes('TSK-0001'))).toBe(true); + }); +});