diff --git a/src/cli/commands/promote.ts b/src/cli/commands/promote.ts new file mode 100644 index 0000000..f950f5e --- /dev/null +++ b/src/cli/commands/promote.ts @@ -0,0 +1,79 @@ +import { Command } from 'commander'; +import { getEventById, getRecentEvents, createEvent } from '../../core/eventService.js'; +import { findOrCreateTask } from '../../core/taskService.js'; +import { findOrCreateTopic } from '../../core/topicService.js'; +import { findOrCreateProject } from '../../core/projectService.js'; +import { formatNextActionsOutput } from '../../lib/formatting.js'; + +export function registerPromote(program: Command): void { + program + .command('promote') + .description('Promote a next action to a task (list next actions if --next is omitted)') + .option('--next ', 'Next action event ID to promote') + .option('--title ', 'Task title (defaults to next action summary)') + .option('--topic ', 'Topic name') + .option('--project ', 'Project name') + .action(async (options) => { + try { + // No --next: list recent next actions + if (!options.next) { + const events = getRecentEvents({ + eventTypes: ['next_action_defined'], + limit: 20, + }); + console.log(formatNextActionsOutput(events)); + return; + } + + // Find the target event + const event = getEventById(options.next); + if (!event) { + console.error(`Event not found: ${options.next}`); + process.exit(1); + } + if (event.event_type !== 'next_action_defined') { + console.error(`Event ${options.next} is not a next_action_defined event (got: ${event.event_type})`); + process.exit(1); + } + + // Resolve project / topic + let projectId: string | undefined; + if (options.project) { + const project = findOrCreateProject(options.project); + projectId = project.id; + } + + let topicId: string | undefined; + if (options.topic) { + topicId = findOrCreateTopic(options.topic, projectId).id; + } else if (event.topic_id) { + // Inherit topic from the original next_action event + topicId = event.topic_id; + } + + // Create the task + const taskTitle = options.title ?? event.summary; + const task = findOrCreateTask(taskTitle, projectId); + + // Record task_started event linked to the new task + const startEvent = createEvent({ + event_type: 'task_started', + task_id: task.id, + topic_id: topicId, + project_id: projectId, + actor: 'human', + origin: 'manual', + summary: `Promoted from next action: ${event.summary}`, + }); + + console.log(`Task created: ${task.id}`); + console.log(`Title : ${task.title}`); + console.log(`Event : ${startEvent.id}`); + console.log(`Source : next_action ${event.id} (${event.occurred_at})`); + if (topicId) console.log(`Topic : ${topicId}`); + } catch (err) { + console.error('Error:', err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index b3284d4..3a76de7 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,6 +20,7 @@ import { registerLog } from './commands/log.js'; import { registerTopics } from './commands/topics.js'; import { registerTasks } from './commands/tasks.js'; import { registerShow } from './commands/show.js'; +import { registerPromote } from './commands/promote.js'; // Ensure ~/.worklog directory and DB exist on startup const worklogDir = join(process.env.HOME ?? '.', '.worklog'); @@ -50,5 +51,6 @@ registerLog(program); registerTopics(program); registerTasks(program); registerShow(program); +registerPromote(program); program.parse(); diff --git a/src/core/eventService.ts b/src/core/eventService.ts index 7338d52..8034b01 100644 --- a/src/core/eventService.ts +++ b/src/core/eventService.ts @@ -47,7 +47,7 @@ export function createEvent(input: CreateEventInput): Event { return getEventById(id)!; } -function getEventById(id: string): Event | null { +export function getEventById(id: string): Event | null { const db = getDb(); const stmt = db.prepare('SELECT * FROM events WHERE id = ?'); return (stmt.get(id) as Event) ?? null; diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index aa4bf7c..5fc1216 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -255,6 +255,28 @@ export function formatTopicsOutput( return lines.join('\n'); } +export function formatNextActionsOutput(events: Event[]): string { + if (events.length === 0) { + return 'No next actions found.'; + } + + const lines: string[] = []; + lines.push('='.repeat(72)); + lines.push(`NEXT ACTIONS (${events.length} entries)`); + lines.push('='.repeat(72)); + lines.push(`${'ID'.padEnd(21)} ${'DATE'.padEnd(16)} ${'SUMMARY'}`); + lines.push('-'.repeat(72)); + + for (const e of events) { + const date = formatDate(e.occurred_at).padEnd(16); + lines.push(`${e.id.padEnd(21)} ${date} ${truncate(e.summary, 30)}`); + } + + lines.push('='.repeat(72)); + lines.push('To promote: ingest promote --next [--title ]'); + return lines.join('\n'); +} + const STATUS_LABELS: Record = { active: 'ACTIVE', paused: 'PAUSED',