@lovelybunch/cli 1.0.72 → 1.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,643 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import inquirer from 'inquirer';
5
+ import { promises as fs } from 'fs';
6
+ import { table } from 'table';
7
+ import os from 'os';
8
+ import { listProposals, getProposal, createProposal, updateProposal, deleteProposal, addPlanStep, addComment, } from '@lovelybunch/core';
9
+ // ============================================================================
10
+ // Constants & Helpers
11
+ // ============================================================================
12
+ const VALID_STATUSES = ['draft', 'proposed', 'in-review', 'code-complete', 'approved', 'merged', 'rejected'];
13
+ const VALID_PRIORITIES = ['low', 'medium', 'high', 'critical'];
14
+ function parseTags(tagsString) {
15
+ if (!tagsString)
16
+ return undefined;
17
+ const tags = tagsString
18
+ .split(',')
19
+ .map(tag => tag.trim())
20
+ .filter(tag => tag.length > 0);
21
+ return tags.length > 0 ? tags : undefined;
22
+ }
23
+ function validatePriority(priority) {
24
+ if (!priority)
25
+ return undefined;
26
+ const normalized = priority.toLowerCase();
27
+ if (!VALID_PRIORITIES.includes(normalized)) {
28
+ throw new Error(`Invalid priority "${priority}". Must be one of: ${VALID_PRIORITIES.join(', ')}`);
29
+ }
30
+ return normalized;
31
+ }
32
+ function validateStatus(status) {
33
+ if (!status)
34
+ return undefined;
35
+ const normalized = status.toLowerCase();
36
+ if (!VALID_STATUSES.includes(normalized)) {
37
+ throw new Error(`Invalid status "${status}". Must be one of: ${VALID_STATUSES.join(', ')}`);
38
+ }
39
+ return normalized;
40
+ }
41
+ function interpretEscapeSequences(str) {
42
+ return str
43
+ .replace(/\\n/g, '\n')
44
+ .replace(/\\t/g, '\t')
45
+ .replace(/\\r/g, '\r')
46
+ .replace(/\\\\/g, '\\');
47
+ }
48
+ function getStatusColor(status) {
49
+ const colors = {
50
+ draft: chalk.gray,
51
+ proposed: chalk.blue,
52
+ 'in-review': chalk.yellow,
53
+ 'code-complete': chalk.green,
54
+ approved: chalk.green,
55
+ merged: chalk.cyan,
56
+ rejected: chalk.red,
57
+ };
58
+ return colors[status] || chalk.white;
59
+ }
60
+ function getPriorityColor(priority) {
61
+ const colors = {
62
+ low: chalk.gray,
63
+ medium: chalk.blue,
64
+ high: chalk.yellow,
65
+ critical: chalk.red,
66
+ };
67
+ return colors[priority] || chalk.white;
68
+ }
69
+ function getStepStatusIcon(status) {
70
+ const icons = {
71
+ pending: chalk.gray('○'),
72
+ 'in-progress': chalk.yellow('◐'),
73
+ completed: chalk.green('●'),
74
+ failed: chalk.red('✗'),
75
+ };
76
+ return icons[status] || chalk.gray('○');
77
+ }
78
+ // ============================================================================
79
+ // Parent Command
80
+ // ============================================================================
81
+ export const taskCommand = new Command('task')
82
+ .description('Manage tasks (change proposals)')
83
+ .alias('proposal')
84
+ .addHelpText('after', `
85
+ Examples:
86
+ ${chalk.gray('# List all tasks')}
87
+ nut task list
88
+ nut task list --status draft
89
+ nut task list --search "authentication"
90
+
91
+ ${chalk.gray('# Get task details')}
92
+ nut task get cp-1234567890
93
+
94
+ ${chalk.gray('# Create a new task')}
95
+ nut task create "Add user authentication"
96
+ nut task create "Fix bug" --priority high --tags "bug,urgent"
97
+
98
+ ${chalk.gray('# Update a task')}
99
+ nut task update cp-1234567890 --status in-review
100
+ nut task update cp-1234567890 --priority critical
101
+
102
+ ${chalk.gray('# Add plan steps')}
103
+ nut task step cp-1234567890 "Implement feature"
104
+ nut task step cp-1234567890 "Run tests" --command "npm test"
105
+
106
+ ${chalk.gray('# Add comments')}
107
+ nut task comment cp-1234567890 "Looks good, approved!"
108
+ nut task comment cp-1234567890 "Please review" --author "reviewer@example.com"
109
+
110
+ ${chalk.gray('# Delete a task')}
111
+ nut task delete cp-1234567890
112
+ `);
113
+ // ============================================================================
114
+ // task list
115
+ // ============================================================================
116
+ taskCommand
117
+ .command('list')
118
+ .description('List all tasks')
119
+ .option('-s, --status <status>', 'Filter by status (draft/proposed/in-review/code-complete/approved/merged/rejected)')
120
+ .option('-a, --author <author>', 'Filter by author name or email')
121
+ .option('-p, --priority <priority>', 'Filter by priority (low/medium/high/critical)')
122
+ .option('-q, --search <query>', 'Search tasks by content (fuzzy search)')
123
+ .option('-t, --tags <tags>', 'Filter by tags (comma-separated)')
124
+ .option('-l, --limit <number>', 'Limit number of results', '20')
125
+ .option('--json', 'Output as JSON')
126
+ .action(async (options) => {
127
+ const spinner = ora('Loading tasks...').start();
128
+ try {
129
+ const filters = {};
130
+ if (options.status) {
131
+ filters.status = validateStatus(options.status);
132
+ }
133
+ if (options.author) {
134
+ filters.author = options.author;
135
+ }
136
+ if (options.priority) {
137
+ filters.priority = validatePriority(options.priority);
138
+ }
139
+ if (options.tags) {
140
+ filters.tags = parseTags(options.tags);
141
+ }
142
+ if (options.search) {
143
+ filters.search = options.search;
144
+ }
145
+ const proposals = await listProposals(filters);
146
+ const limit = parseInt(options.limit, 10) || 20;
147
+ const limited = proposals.slice(0, limit);
148
+ spinner.stop();
149
+ if (options.json) {
150
+ console.log(JSON.stringify(limited, null, 2));
151
+ return;
152
+ }
153
+ if (limited.length === 0) {
154
+ console.log(chalk.yellow('No tasks found.'));
155
+ if (Object.keys(filters).length > 0) {
156
+ console.log(chalk.gray('Try adjusting your filters or search query.'));
157
+ }
158
+ else {
159
+ console.log(chalk.gray('Create one with: nut task create "Your intent"'));
160
+ }
161
+ return;
162
+ }
163
+ // Prepare table data
164
+ const tableData = [
165
+ [
166
+ chalk.bold('ID'),
167
+ chalk.bold('Intent'),
168
+ chalk.bold('Author'),
169
+ chalk.bold('Status'),
170
+ chalk.bold('Priority'),
171
+ chalk.bold('Created'),
172
+ ],
173
+ ];
174
+ for (const proposal of limited) {
175
+ const intent = proposal.intent.length > 35
176
+ ? proposal.intent.substring(0, 32) + '...'
177
+ : proposal.intent;
178
+ const priority = proposal.metadata?.priority || 'medium';
179
+ tableData.push([
180
+ chalk.cyan(proposal.id),
181
+ intent,
182
+ proposal.author.name,
183
+ getStatusColor(proposal.status)(proposal.status),
184
+ getPriorityColor(priority)(priority),
185
+ chalk.gray(new Date(proposal.metadata?.createdAt || '').toLocaleDateString()),
186
+ ]);
187
+ }
188
+ // Display table
189
+ const tableConfig = {
190
+ border: {
191
+ topBody: chalk.gray('─'),
192
+ topJoin: chalk.gray('┬'),
193
+ topLeft: chalk.gray('┌'),
194
+ topRight: chalk.gray('┐'),
195
+ bottomBody: chalk.gray('─'),
196
+ bottomJoin: chalk.gray('┴'),
197
+ bottomLeft: chalk.gray('└'),
198
+ bottomRight: chalk.gray('┘'),
199
+ bodyLeft: chalk.gray('│'),
200
+ bodyRight: chalk.gray('│'),
201
+ bodyJoin: chalk.gray('│'),
202
+ joinBody: chalk.gray('─'),
203
+ joinLeft: chalk.gray('├'),
204
+ joinRight: chalk.gray('┤'),
205
+ joinJoin: chalk.gray('┼'),
206
+ },
207
+ };
208
+ console.log('\n' + chalk.bold.underline('Tasks'));
209
+ console.log(table(tableData, tableConfig));
210
+ console.log(chalk.gray(`Showing ${limited.length} of ${proposals.length} tasks`));
211
+ }
212
+ catch (error) {
213
+ spinner.fail('Failed to list tasks');
214
+ console.error(chalk.red('Error:'), error.message);
215
+ process.exit(1);
216
+ }
217
+ });
218
+ // ============================================================================
219
+ // task get
220
+ // ============================================================================
221
+ taskCommand
222
+ .command('get')
223
+ .description('Get details of a task')
224
+ .argument('<id>', 'Task ID (e.g., cp-1234567890)')
225
+ .option('--json', 'Output as JSON')
226
+ .option('--raw', 'Show raw content without formatting')
227
+ .action(async (id, options) => {
228
+ const spinner = ora('Loading task...').start();
229
+ try {
230
+ const proposal = await getProposal(id);
231
+ spinner.stop();
232
+ if (!proposal) {
233
+ console.error(chalk.red(`Task '${id}' not found.`));
234
+ console.log(chalk.gray('Use "nut task list" to see all tasks.'));
235
+ process.exit(1);
236
+ }
237
+ if (options.json) {
238
+ console.log(JSON.stringify(proposal, null, 2));
239
+ return;
240
+ }
241
+ if (options.raw && proposal.content) {
242
+ console.log(proposal.content);
243
+ return;
244
+ }
245
+ // Display formatted output
246
+ console.log('\n' + chalk.bold.underline('Task Details'));
247
+ console.log();
248
+ // Basic Information
249
+ console.log(chalk.cyan('Basic Information:'));
250
+ console.log(' ID: ' + chalk.white(proposal.id));
251
+ console.log(' Intent: ' + chalk.white(proposal.intent));
252
+ console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status.toUpperCase()));
253
+ if (proposal.metadata?.priority) {
254
+ console.log(' Priority: ' + getPriorityColor(proposal.metadata.priority)(proposal.metadata.priority.toUpperCase()));
255
+ }
256
+ console.log(' Created: ' + chalk.white(new Date(proposal.metadata?.createdAt || '').toLocaleString()));
257
+ console.log(' Updated: ' + chalk.white(new Date(proposal.metadata?.updatedAt || '').toLocaleString()));
258
+ // Author Information
259
+ console.log('\n' + chalk.cyan('Author:'));
260
+ console.log(' Name: ' + chalk.white(proposal.author.name));
261
+ console.log(' Type: ' + chalk.white(proposal.author.type));
262
+ if (proposal.author.email) {
263
+ console.log(' Email: ' + chalk.white(proposal.author.email));
264
+ }
265
+ // Product Spec Reference
266
+ if (proposal.productSpecRef) {
267
+ console.log('\n' + chalk.cyan('Product Specification:'));
268
+ console.log(' Reference: ' + chalk.white(proposal.productSpecRef));
269
+ }
270
+ // Content
271
+ if (proposal.content) {
272
+ console.log('\n' + chalk.cyan('Content:'));
273
+ const contentLines = proposal.content.split('\n').slice(0, 10);
274
+ contentLines.forEach(line => console.log(' ' + chalk.gray(line)));
275
+ if (proposal.content.split('\n').length > 10) {
276
+ console.log(' ' + chalk.gray('... (truncated)'));
277
+ }
278
+ }
279
+ // Plan Steps
280
+ if (proposal.planSteps && proposal.planSteps.length > 0) {
281
+ console.log('\n' + chalk.cyan('Plan Steps:'));
282
+ proposal.planSteps.forEach((step, index) => {
283
+ const statusIcon = getStepStatusIcon(step.status);
284
+ console.log(` ${index + 1}. ${statusIcon} ${step.description}`);
285
+ if (step.command) {
286
+ console.log(' Command: ' + chalk.gray(step.command));
287
+ }
288
+ });
289
+ }
290
+ // Tags
291
+ if (proposal.metadata?.tags && proposal.metadata.tags.length > 0) {
292
+ console.log('\n' + chalk.cyan('Tags:'));
293
+ console.log(' ' + proposal.metadata.tags.map(tag => chalk.magenta(`#${tag}`)).join(' '));
294
+ }
295
+ // Actions
296
+ console.log('\n' + chalk.cyan('Actions:'));
297
+ console.log(' • Update task: ' + chalk.yellow(`nut task update ${proposal.id} --status <status>`));
298
+ console.log(' • Delete task: ' + chalk.yellow(`nut task delete ${proposal.id}`));
299
+ }
300
+ catch (error) {
301
+ spinner.fail('Failed to load task');
302
+ console.error(chalk.red('Error:'), error.message);
303
+ process.exit(1);
304
+ }
305
+ });
306
+ // ============================================================================
307
+ // task create
308
+ // ============================================================================
309
+ taskCommand
310
+ .command('create')
311
+ .description('Create a new task')
312
+ .argument('<intent>', 'The intent/title of the task')
313
+ .option('--author <name>', 'Author name')
314
+ .option('--email <email>', 'Author email')
315
+ .option('--type <type>', 'Author type (human/agent)', 'human')
316
+ .option('-c, --content <text>', 'Detailed description or content')
317
+ .option('-f, --file <path>', 'Read content from a file')
318
+ .option('-t, --tags <tags>', 'Comma-separated tags')
319
+ .option('-p, --priority <level>', 'Priority: low, medium, high, critical')
320
+ .option('-s, --status <status>', 'Initial status (default: draft)')
321
+ .option('--spec <ref>', 'Product specification reference')
322
+ .option('--json', 'Output as JSON')
323
+ .option('-y, --yes', 'Skip confirmation prompts')
324
+ .action(async (intent, options) => {
325
+ try {
326
+ // Get author information
327
+ let authorName = options.author;
328
+ let authorEmail = options.email;
329
+ if (!options.yes && (!authorName || !authorEmail)) {
330
+ const authorInfo = await inquirer.prompt([
331
+ {
332
+ type: 'input',
333
+ name: 'name',
334
+ message: 'Author name:',
335
+ default: options.author || os.userInfo().username,
336
+ when: !options.author,
337
+ },
338
+ {
339
+ type: 'input',
340
+ name: 'email',
341
+ message: 'Author email (optional):',
342
+ default: options.email,
343
+ when: !options.email,
344
+ },
345
+ ]);
346
+ authorName = authorName || authorInfo.name;
347
+ authorEmail = authorEmail || authorInfo.email;
348
+ }
349
+ authorName = authorName || os.userInfo().username;
350
+ // Process content: --file takes precedence over --content
351
+ let content;
352
+ if (options.file) {
353
+ const spinner = ora(`Reading content from ${options.file}...`).start();
354
+ try {
355
+ content = await fs.readFile(options.file, 'utf-8');
356
+ spinner.stop();
357
+ }
358
+ catch (error) {
359
+ spinner.fail(`Failed to read file: ${options.file}`);
360
+ console.error(chalk.red('Error:'), error.message);
361
+ process.exit(1);
362
+ }
363
+ }
364
+ else if (options.content) {
365
+ content = interpretEscapeSequences(options.content);
366
+ }
367
+ const spinner = ora('Creating task...').start();
368
+ const proposal = await createProposal({
369
+ intent,
370
+ content: content || intent, // Use intent as content if no content provided
371
+ author: {
372
+ id: authorEmail || `${authorName}@local`,
373
+ name: authorName,
374
+ email: authorEmail,
375
+ type: options.type === 'agent' ? 'agent' : 'human',
376
+ },
377
+ status: validateStatus(options.status) || 'draft',
378
+ metadata: {
379
+ tags: parseTags(options.tags),
380
+ priority: validatePriority(options.priority),
381
+ },
382
+ productSpecRef: options.spec,
383
+ });
384
+ spinner.succeed(`Task created: ${chalk.cyan(proposal.id)}`);
385
+ if (options.json) {
386
+ console.log(JSON.stringify(proposal, null, 2));
387
+ return;
388
+ }
389
+ console.log('\n' + chalk.bold('Task Details:'));
390
+ console.log(' ID: ' + chalk.cyan(proposal.id));
391
+ console.log(' Intent: ' + chalk.white(proposal.intent));
392
+ console.log(' Author: ' + chalk.white(`${proposal.author.name} (${proposal.author.type})`));
393
+ console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status));
394
+ if (proposal.metadata?.priority) {
395
+ console.log(' Priority: ' + getPriorityColor(proposal.metadata.priority)(proposal.metadata.priority));
396
+ }
397
+ if (proposal.metadata?.tags && proposal.metadata.tags.length > 0) {
398
+ console.log(' Tags: ' + chalk.gray(proposal.metadata.tags.join(', ')));
399
+ }
400
+ console.log('\n' + chalk.cyan('Next steps:'));
401
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${proposal.id}`));
402
+ console.log(' • Update task: ' + chalk.yellow(`nut task update ${proposal.id} --status proposed`));
403
+ console.log(' • List tasks: ' + chalk.yellow('nut task list'));
404
+ }
405
+ catch (error) {
406
+ console.error(chalk.red('Failed to create task:'), error.message);
407
+ process.exit(1);
408
+ }
409
+ });
410
+ // ============================================================================
411
+ // task update
412
+ // ============================================================================
413
+ taskCommand
414
+ .command('update')
415
+ .description('Update an existing task')
416
+ .argument('<id>', 'Task ID to update')
417
+ .option('-i, --intent <text>', 'Update intent/title')
418
+ .option('-c, --content <text>', 'Update content')
419
+ .option('-f, --file <path>', 'Read new content from a file')
420
+ .option('-s, --status <status>', 'Update status')
421
+ .option('-p, --priority <level>', 'Update priority')
422
+ .option('-t, --tags <tags>', 'Update tags (comma-separated)')
423
+ .option('--spec <ref>', 'Update product specification reference')
424
+ .option('--json', 'Output as JSON')
425
+ .action(async (id, options) => {
426
+ try {
427
+ // Check if any update option is provided
428
+ const hasUpdates = options.intent || options.content || options.file ||
429
+ options.status || options.priority || options.tags || options.spec;
430
+ if (!hasUpdates) {
431
+ console.error(chalk.red('No updates specified.'));
432
+ console.log(chalk.gray('Use --intent, --content, --status, --priority, --tags, or --spec to update.'));
433
+ process.exit(1);
434
+ }
435
+ // Read content from file if specified
436
+ let content;
437
+ if (options.file) {
438
+ const spinner = ora(`Reading content from ${options.file}...`).start();
439
+ try {
440
+ content = await fs.readFile(options.file, 'utf-8');
441
+ spinner.stop();
442
+ }
443
+ catch (error) {
444
+ spinner.fail(`Failed to read file: ${options.file}`);
445
+ console.error(chalk.red('Error:'), error.message);
446
+ process.exit(1);
447
+ }
448
+ }
449
+ else if (options.content) {
450
+ content = interpretEscapeSequences(options.content);
451
+ }
452
+ const spinner = ora('Updating task...').start();
453
+ const updates = {};
454
+ if (options.intent)
455
+ updates.intent = options.intent;
456
+ if (content)
457
+ updates.content = content;
458
+ if (options.status)
459
+ updates.status = validateStatus(options.status);
460
+ if (options.spec !== undefined)
461
+ updates.productSpecRef = options.spec;
462
+ // Handle metadata updates
463
+ if (options.priority || options.tags) {
464
+ updates.metadata = {};
465
+ if (options.priority)
466
+ updates.metadata.priority = validatePriority(options.priority);
467
+ if (options.tags)
468
+ updates.metadata.tags = parseTags(options.tags);
469
+ }
470
+ const updated = await updateProposal(id, updates);
471
+ spinner.succeed(`Task updated: ${chalk.cyan(id)}`);
472
+ if (options.json) {
473
+ console.log(JSON.stringify(updated, null, 2));
474
+ return;
475
+ }
476
+ console.log('\n' + chalk.bold('Updated Task:'));
477
+ console.log(' ID: ' + chalk.cyan(updated.id));
478
+ console.log(' Intent: ' + chalk.white(updated.intent));
479
+ console.log(' Status: ' + getStatusColor(updated.status)(updated.status));
480
+ if (updated.metadata?.priority) {
481
+ console.log(' Priority: ' + getPriorityColor(updated.metadata.priority)(updated.metadata.priority));
482
+ }
483
+ }
484
+ catch (error) {
485
+ console.error(chalk.red('Failed to update task:'), error.message);
486
+ process.exit(1);
487
+ }
488
+ });
489
+ // ============================================================================
490
+ // task delete
491
+ // ============================================================================
492
+ taskCommand
493
+ .command('delete')
494
+ .description('Delete a task')
495
+ .argument('<id>', 'Task ID to delete')
496
+ .option('-y, --yes', 'Skip confirmation prompt')
497
+ .action(async (id, options) => {
498
+ try {
499
+ // Verify task exists first
500
+ const spinner = ora('Loading task...').start();
501
+ const proposal = await getProposal(id);
502
+ spinner.stop();
503
+ if (!proposal) {
504
+ console.error(chalk.red(`Task '${id}' not found.`));
505
+ process.exit(1);
506
+ }
507
+ // Confirm deletion
508
+ if (!options.yes) {
509
+ console.log('\n' + chalk.bold('Task to delete:'));
510
+ console.log(' ID: ' + chalk.cyan(proposal.id));
511
+ console.log(' Intent: ' + chalk.white(proposal.intent));
512
+ console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status));
513
+ console.log();
514
+ const { confirm } = await inquirer.prompt([
515
+ {
516
+ type: 'confirm',
517
+ name: 'confirm',
518
+ message: chalk.yellow('Are you sure you want to delete this task?'),
519
+ default: false,
520
+ },
521
+ ]);
522
+ if (!confirm) {
523
+ console.log(chalk.gray('Deletion cancelled.'));
524
+ return;
525
+ }
526
+ }
527
+ const deleteSpinner = ora('Deleting task...').start();
528
+ const deleted = await deleteProposal(id);
529
+ if (deleted) {
530
+ deleteSpinner.succeed(`Task deleted: ${chalk.cyan(id)}`);
531
+ }
532
+ else {
533
+ deleteSpinner.fail('Failed to delete task');
534
+ process.exit(1);
535
+ }
536
+ }
537
+ catch (error) {
538
+ console.error(chalk.red('Failed to delete task:'), error.message);
539
+ process.exit(1);
540
+ }
541
+ });
542
+ // ============================================================================
543
+ // task step (add plan step)
544
+ // ============================================================================
545
+ taskCommand
546
+ .command('step')
547
+ .description('Add a plan step to a task')
548
+ .argument('<id>', 'Task ID to add step to')
549
+ .argument('<description>', 'Description of the step')
550
+ .option('-s, --status <status>', 'Initial status (pending/in-progress/completed/failed)', 'pending')
551
+ .option('-c, --command <cmd>', 'Command to execute for this step')
552
+ .option('-e, --expected <outcome>', 'Expected outcome of this step')
553
+ .option('--json', 'Output as JSON')
554
+ .action(async (id, description, options) => {
555
+ try {
556
+ const spinner = ora('Adding plan step...').start();
557
+ const validStepStatuses = ['pending', 'in-progress', 'completed', 'failed'];
558
+ const status = options.status?.toLowerCase();
559
+ if (status && !validStepStatuses.includes(status)) {
560
+ spinner.fail(`Invalid step status: ${options.status}`);
561
+ console.log(chalk.gray(`Valid statuses: ${validStepStatuses.join(', ')}`));
562
+ process.exit(1);
563
+ }
564
+ const step = await addPlanStep(id, {
565
+ description,
566
+ status: status,
567
+ command: options.command,
568
+ expectedOutcome: options.expected,
569
+ });
570
+ spinner.succeed(`Plan step added: ${chalk.cyan(step.id)}`);
571
+ if (options.json) {
572
+ console.log(JSON.stringify(step, null, 2));
573
+ return;
574
+ }
575
+ console.log('\n' + chalk.bold('Step Details:'));
576
+ console.log(' ID: ' + chalk.cyan(step.id));
577
+ console.log(' Description: ' + chalk.white(step.description));
578
+ console.log(' Status: ' + getStepStatusIcon(step.status) + ' ' + step.status);
579
+ if (step.command) {
580
+ console.log(' Command: ' + chalk.gray(step.command));
581
+ }
582
+ if (step.expectedOutcome) {
583
+ console.log(' Expected: ' + chalk.gray(step.expectedOutcome));
584
+ }
585
+ console.log('\n' + chalk.cyan('Next steps:'));
586
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${id}`));
587
+ console.log(' • Add more: ' + chalk.yellow(`nut task step ${id} "Next step"`));
588
+ }
589
+ catch (error) {
590
+ console.error(chalk.red('Failed to add plan step:'), error.message);
591
+ process.exit(1);
592
+ }
593
+ });
594
+ // ============================================================================
595
+ // task comment (add comment)
596
+ // ============================================================================
597
+ taskCommand
598
+ .command('comment')
599
+ .description('Add a comment to a task')
600
+ .argument('<id>', 'Task ID to add comment to')
601
+ .argument('<content>', 'Comment content')
602
+ .option('-a, --author <author>', 'Comment author (email or name)')
603
+ .option('--json', 'Output as JSON')
604
+ .action(async (id, content, options) => {
605
+ try {
606
+ // Get author - use provided or prompt
607
+ let author = options.author;
608
+ if (!author) {
609
+ const inquirerPrompt = await inquirer.prompt([
610
+ {
611
+ type: 'input',
612
+ name: 'author',
613
+ message: 'Comment author (email or name):',
614
+ default: os.userInfo().username + '@local',
615
+ },
616
+ ]);
617
+ author = inquirerPrompt.author;
618
+ }
619
+ const spinner = ora('Adding comment...').start();
620
+ const comment = await addComment(id, {
621
+ author,
622
+ content,
623
+ });
624
+ spinner.succeed(`Comment added: ${chalk.cyan(comment.id)}`);
625
+ if (options.json) {
626
+ console.log(JSON.stringify(comment, null, 2));
627
+ return;
628
+ }
629
+ console.log('\n' + chalk.bold('Comment Details:'));
630
+ console.log(' ID: ' + chalk.cyan(comment.id));
631
+ console.log(' Author: ' + chalk.white(comment.author));
632
+ console.log(' Content: ' + chalk.gray(comment.content.substring(0, 80) + (comment.content.length > 80 ? '...' : '')));
633
+ console.log(' Created: ' + chalk.gray(new Date(comment.createdAt).toLocaleString()));
634
+ console.log('\n' + chalk.cyan('Next steps:'));
635
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${id}`));
636
+ console.log(' • Add more: ' + chalk.yellow(`nut task comment ${id} "Another comment"`));
637
+ }
638
+ catch (error) {
639
+ console.error(chalk.red('Failed to add comment:'), error.message);
640
+ process.exit(1);
641
+ }
642
+ });
643
+ //# sourceMappingURL=task.js.map