feat(cli): add task create/list/show/claim/done commands
This commit is contained in:
parent
cd6add215d
commit
be9161d7d1
102
src/cli/commands/task.ts
Normal file
102
src/cli/commands/task.ts
Normal file
@ -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<Task> = {}): Promise<void> {
|
||||||
|
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<Task>): 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
29
tests/task-cmd.test.ts
Normal file
29
tests/task-cmd.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user