@lovelybunch/cli 1.0.75-alpha.9 → 1.0.76-alpha.0

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.
@@ -5,11 +5,11 @@ import inquirer from 'inquirer';
5
5
  import { promises as fs } from 'fs';
6
6
  import { table } from 'table';
7
7
  import os from 'os';
8
- import { listProposals, getProposal, createProposal, updateProposal, deleteProposal, addPlanStep, addComment, } from '@lovelybunch/core';
8
+ import { listTasks, getTask, createTask, updateTask, deleteTask, addPlanStep, updatePlanStep, addComment, } from '@lovelybunch/core';
9
9
  // ============================================================================
10
10
  // Constants & Helpers
11
11
  // ============================================================================
12
- const VALID_STATUSES = ['draft', 'backlog', 'ready', 'queued', 'active', 'blocked', 'review', 'done', 'canceled', 'duplicate'];
12
+ const VALID_STATUSES = ['draft', 'backlog', 'ready', 'queued', 'active', 'blocked', 'review', 'revision', 'done', 'canceled', 'duplicate'];
13
13
  const VALID_PRIORITIES = ['low', 'medium', 'high', 'critical'];
14
14
  function parseTags(tagsString) {
15
15
  if (!tagsString)
@@ -38,6 +38,15 @@ function validateStatus(status) {
38
38
  }
39
39
  return normalized;
40
40
  }
41
+ function validateReadiness(value) {
42
+ if (value == null)
43
+ return undefined;
44
+ const n = parseInt(value, 10);
45
+ if (Number.isNaN(n) || n < 0 || n > 100) {
46
+ throw new Error(`Invalid readiness "${value}". Must be an integer between 0 and 100.`);
47
+ }
48
+ return n;
49
+ }
41
50
  function interpretEscapeSequences(str) {
42
51
  return str
43
52
  .replace(/\\n/g, '\n')
@@ -54,6 +63,7 @@ function getStatusColor(status) {
54
63
  active: chalk.yellow,
55
64
  blocked: chalk.red,
56
65
  review: chalk.cyan,
66
+ revision: chalk.magenta,
57
67
  done: chalk.green,
58
68
  canceled: chalk.gray,
59
69
  duplicate: chalk.gray,
@@ -72,9 +82,10 @@ function getPriorityColor(priority) {
72
82
  function getStepStatusIcon(status) {
73
83
  const icons = {
74
84
  pending: chalk.gray('○'),
75
- 'in-progress': chalk.yellow('◐'),
76
- completed: chalk.green('●'),
85
+ active: chalk.yellow('◐'),
86
+ done: chalk.green('●'),
77
87
  failed: chalk.red('✗'),
88
+ skipped: chalk.dim('⊘'),
78
89
  };
79
90
  return icons[status] || chalk.gray('○');
80
91
  }
@@ -82,8 +93,7 @@ function getStepStatusIcon(status) {
82
93
  // Parent Command
83
94
  // ============================================================================
84
95
  export const taskCommand = new Command('task')
85
- .description('Manage tasks (change proposals)')
86
- .alias('proposal')
96
+ .description('Manage tasks')
87
97
  .addHelpText('after', `
88
98
  Examples:
89
99
  ${chalk.gray('# List all tasks')}
@@ -119,7 +129,7 @@ Examples:
119
129
  taskCommand
120
130
  .command('list')
121
131
  .description('List all tasks')
122
- .option('-s, --status <status>', 'Filter by status (draft|backlog|ready|queued|active|blocked|review|done|canceled|duplicate)')
132
+ .option('-s, --status <status>', 'Filter by status (draft|backlog|ready|queued|active|blocked|review|revision|done|canceled|duplicate)')
123
133
  .option('-a, --author <author>', 'Filter by author name or email')
124
134
  .option('-p, --priority <priority>', 'Filter by priority (low|medium|high|critical)')
125
135
  .option('-q, --search <query>', 'Search tasks by content (fuzzy search)')
@@ -145,9 +155,10 @@ taskCommand
145
155
  if (options.search) {
146
156
  filters.search = options.search;
147
157
  }
148
- const proposals = await listProposals(filters);
149
158
  const limit = parseInt(options.limit, 10) || 20;
150
- const limited = proposals.slice(0, limit);
159
+ filters.limit = limit;
160
+ const tasks = await listTasks(filters);
161
+ const limited = tasks.slice(0, limit);
151
162
  spinner.stop();
152
163
  if (options.json) {
153
164
  console.log(JSON.stringify(limited, null, 2));
@@ -167,26 +178,32 @@ taskCommand
167
178
  const tableData = [
168
179
  [
169
180
  chalk.bold('ID'),
170
- chalk.bold('Intent'),
181
+ chalk.bold('Title'),
171
182
  chalk.bold('Author'),
172
183
  chalk.bold('Status'),
173
184
  chalk.bold('Priority'),
185
+ chalk.bold('Steps'),
174
186
  chalk.bold('Created'),
175
187
  ],
176
188
  ];
177
- for (const proposal of limited) {
178
- const titleText = proposal.title || proposal.intent || '';
189
+ for (const task of limited) {
190
+ const titleText = task.title || task.intent || '';
179
191
  const displayTitle = titleText.length > 35
180
192
  ? titleText.substring(0, 32) + '...'
181
193
  : titleText;
182
- const priority = proposal.metadata?.priority || 'medium';
194
+ const priority = task.metadata?.priority || 'medium';
195
+ const steps = task.planSteps || [];
196
+ const stepsDisplay = steps.length > 0
197
+ ? chalk.dim(`${steps.filter(s => s.status === 'done').length}/${steps.length}`)
198
+ : chalk.dim('-');
183
199
  tableData.push([
184
- chalk.cyan(proposal.id),
200
+ chalk.cyan(task.id),
185
201
  displayTitle,
186
- proposal.author.name,
187
- getStatusColor(proposal.status)(proposal.status),
202
+ task.author.name,
203
+ getStatusColor(task.status)(task.status),
188
204
  getPriorityColor(priority)(priority),
189
- chalk.gray(new Date(proposal.metadata?.createdAt || '').toLocaleDateString()),
205
+ stepsDisplay,
206
+ chalk.gray(new Date(task.metadata?.createdAt || '').toLocaleDateString()),
190
207
  ]);
191
208
  }
192
209
  // Display table
@@ -211,7 +228,7 @@ taskCommand
211
228
  };
212
229
  console.log('\n' + chalk.bold.underline('Tasks'));
213
230
  console.log(table(tableData, tableConfig));
214
- console.log(chalk.gray(`Showing ${limited.length} of ${proposals.length} tasks`));
231
+ console.log(chalk.gray(`Showing ${limited.length} of ${tasks.length} tasks`));
215
232
  }
216
233
  catch (error) {
217
234
  spinner.fail('Failed to list tasks');
@@ -231,19 +248,19 @@ taskCommand
231
248
  .action(async (id, options) => {
232
249
  const spinner = ora('Loading task...').start();
233
250
  try {
234
- const proposal = await getProposal(id);
251
+ const task = await getTask(id);
235
252
  spinner.stop();
236
- if (!proposal) {
253
+ if (!task) {
237
254
  console.error(chalk.red(`Task '${id}' not found.`));
238
255
  console.log(chalk.gray('Use "nut task list" to see all tasks.'));
239
256
  process.exit(1);
240
257
  }
241
258
  if (options.json) {
242
- console.log(JSON.stringify(proposal, null, 2));
259
+ console.log(JSON.stringify(task, null, 2));
243
260
  return;
244
261
  }
245
- if (options.raw && proposal.content) {
246
- console.log(proposal.content);
262
+ if (options.raw && task.content) {
263
+ console.log(task.content);
247
264
  return;
248
265
  }
249
266
  // Display formatted output
@@ -251,55 +268,75 @@ taskCommand
251
268
  console.log();
252
269
  // Basic Information
253
270
  console.log(chalk.cyan('Basic Information:'));
254
- console.log(' ID: ' + chalk.white(proposal.id));
255
- console.log(' Title: ' + chalk.white(proposal.title || proposal.intent));
256
- console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status.toUpperCase()));
257
- if (proposal.metadata?.priority) {
258
- console.log(' Priority: ' + getPriorityColor(proposal.metadata.priority)(proposal.metadata.priority.toUpperCase()));
259
- }
260
- console.log(' Created: ' + chalk.white(new Date(proposal.metadata?.createdAt || '').toLocaleString()));
261
- console.log(' Updated: ' + chalk.white(new Date(proposal.metadata?.updatedAt || '').toLocaleString()));
271
+ console.log(' ID: ' + chalk.white(task.id));
272
+ console.log(' Title: ' + chalk.white(task.title || task.intent));
273
+ console.log(' Status: ' + getStatusColor(task.status)(task.status.toUpperCase()));
274
+ if (task.metadata?.priority) {
275
+ console.log(' Priority: ' + getPriorityColor(task.metadata.priority)(task.metadata.priority.toUpperCase()));
276
+ }
277
+ if (task.metadata?.readiness != null) {
278
+ console.log(' Readiness: ' + chalk.white(`${task.metadata.readiness}%`));
279
+ }
280
+ console.log(' Created: ' + chalk.white(new Date(task.metadata?.createdAt || '').toLocaleString()));
281
+ console.log(' Updated: ' + chalk.white(new Date(task.metadata?.updatedAt || '').toLocaleString()));
262
282
  // Author Information
263
283
  console.log('\n' + chalk.cyan('Author:'));
264
- console.log(' Name: ' + chalk.white(proposal.author.name));
265
- console.log(' Type: ' + chalk.white(proposal.author.type));
266
- if (proposal.author.email) {
267
- console.log(' Email: ' + chalk.white(proposal.author.email));
284
+ console.log(' Name: ' + chalk.white(task.author.name));
285
+ console.log(' Type: ' + chalk.white(task.author.type));
286
+ if (task.author.email) {
287
+ console.log(' Email: ' + chalk.white(task.author.email));
268
288
  }
269
289
  // Product Spec Reference
270
- if (proposal.productSpecRef) {
290
+ if (task.productSpecRef) {
271
291
  console.log('\n' + chalk.cyan('Product Specification:'));
272
- console.log(' Reference: ' + chalk.white(proposal.productSpecRef));
292
+ console.log(' Reference: ' + chalk.white(task.productSpecRef));
273
293
  }
274
294
  // Content
275
- if (proposal.content) {
295
+ if (task.content) {
276
296
  console.log('\n' + chalk.cyan('Content:'));
277
- const contentLines = proposal.content.split('\n').slice(0, 10);
278
- contentLines.forEach(line => console.log(' ' + chalk.gray(line)));
279
- if (proposal.content.split('\n').length > 10) {
280
- console.log(' ' + chalk.gray('... (truncated)'));
281
- }
297
+ task.content.split('\n').forEach(line => console.log(' ' + chalk.gray(line)));
298
+ }
299
+ // Comments
300
+ if (task.comments && task.comments.length > 0) {
301
+ console.log('\n' + chalk.cyan('Comments:'));
302
+ task.comments.forEach((comment) => {
303
+ const date = new Date(comment.createdAt).toLocaleString();
304
+ console.log(' ' + chalk.white(comment.author) + ' ' + chalk.dim(`(${date})`));
305
+ console.log(' ' + chalk.gray(comment.content));
306
+ console.log();
307
+ });
282
308
  }
283
309
  // Plan Steps
284
- if (proposal.planSteps && proposal.planSteps.length > 0) {
285
- console.log('\n' + chalk.cyan('Plan Steps:'));
286
- proposal.planSteps.forEach((step, index) => {
310
+ if (task.planSteps && task.planSteps.length > 0) {
311
+ const doneCount = task.planSteps.filter(s => s.status === 'done').length;
312
+ const total = task.planSteps.length;
313
+ console.log('\n' + chalk.cyan('Plan Steps:') + ' ' + chalk.dim(`(${doneCount}/${total} done)`));
314
+ task.planSteps.forEach((step, index) => {
287
315
  const statusIcon = getStepStatusIcon(step.status);
288
- console.log(` ${index + 1}. ${statusIcon} ${step.description}`);
316
+ console.log(` ${index + 1}. ${statusIcon} ${chalk.dim(`[${step.id}]`)} ${step.description}`);
289
317
  if (step.command) {
290
318
  console.log(' Command: ' + chalk.gray(step.command));
291
319
  }
292
320
  });
293
321
  }
322
+ // Reviewers
323
+ if (task.metadata?.reviewers && task.metadata.reviewers.length > 0) {
324
+ console.log('\n' + chalk.cyan('Reviewers:'));
325
+ console.log(' ' + task.metadata.reviewers.map(r => chalk.white(r)).join(', '));
326
+ }
294
327
  // Tags
295
- if (proposal.metadata?.tags && proposal.metadata.tags.length > 0) {
328
+ if (task.metadata?.tags && task.metadata.tags.length > 0) {
296
329
  console.log('\n' + chalk.cyan('Tags:'));
297
- console.log(' ' + proposal.metadata.tags.map(tag => chalk.magenta(`#${tag}`)).join(' '));
330
+ console.log(' ' + task.metadata.tags.map(tag => chalk.magenta(`#${tag}`)).join(' '));
298
331
  }
299
332
  // Actions
300
333
  console.log('\n' + chalk.cyan('Actions:'));
301
- console.log(' • Update task: ' + chalk.yellow(`nut task update ${proposal.id} --status <status>`));
302
- console.log(' • Delete task: ' + chalk.yellow(`nut task delete ${proposal.id}`));
334
+ console.log(' • Update status: ' + chalk.yellow(`nut task update ${task.id} --status <status>`));
335
+ console.log(' ' + chalk.gray(`statuses: ${VALID_STATUSES.join(', ')}`));
336
+ console.log(' • Add step: ' + chalk.yellow(`nut task step ${task.id} "Description"`));
337
+ console.log(' • Update step: ' + chalk.yellow(`nut task step-update ${task.id} <stepId> --status <status>`));
338
+ console.log(' ' + chalk.gray('statuses: pending, active, done, failed, skipped'));
339
+ console.log(' • Add comment: ' + chalk.yellow(`nut task comment ${task.id} "Comment"`));
303
340
  }
304
341
  catch (error) {
305
342
  spinner.fail('Failed to load task');
@@ -321,7 +358,8 @@ taskCommand
321
358
  .option('-f, --file <path>', 'Read content from a file')
322
359
  .option('-t, --tags <tags>', 'Comma-separated tags')
323
360
  .option('-p, --priority <level>', 'Priority (low|medium|high|critical)')
324
- .option('-s, --status <status>', 'Initial status (draft|backlog|ready|queued|active|blocked|review|done|canceled|duplicate, default: draft)')
361
+ .option('-r, --readiness <score>', 'Readiness score (0-100)')
362
+ .option('-s, --status <status>', 'Initial status (draft|backlog|ready|queued|active|blocked|review|revision|done|canceled|duplicate, default: draft)')
325
363
  .option('--spec <ref>', 'Product specification reference')
326
364
  .option('--json', 'Output as JSON')
327
365
  .option('-y, --yes', 'Skip confirmation prompts')
@@ -369,7 +407,7 @@ taskCommand
369
407
  content = interpretEscapeSequences(options.content);
370
408
  }
371
409
  const spinner = ora('Creating task...').start();
372
- const proposal = await createProposal({
410
+ const task = await createTask({
373
411
  intent: title,
374
412
  content: content || title, // Use title as content if no content provided
375
413
  author: {
@@ -382,28 +420,32 @@ taskCommand
382
420
  metadata: {
383
421
  tags: parseTags(options.tags),
384
422
  priority: validatePriority(options.priority),
423
+ readiness: validateReadiness(options.readiness),
385
424
  },
386
425
  productSpecRef: options.spec,
387
426
  });
388
- spinner.succeed(`Task created: ${chalk.cyan(proposal.id)}`);
427
+ spinner.succeed(`Task created: ${chalk.cyan(task.id)}`);
389
428
  if (options.json) {
390
- console.log(JSON.stringify(proposal, null, 2));
429
+ console.log(JSON.stringify(task, null, 2));
391
430
  return;
392
431
  }
393
432
  console.log('\n' + chalk.bold('Task Details:'));
394
- console.log(' ID: ' + chalk.cyan(proposal.id));
395
- console.log(' Title: ' + chalk.white(proposal.title || proposal.intent));
396
- console.log(' Author: ' + chalk.white(`${proposal.author.name} (${proposal.author.type})`));
397
- console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status));
398
- if (proposal.metadata?.priority) {
399
- console.log(' Priority: ' + getPriorityColor(proposal.metadata.priority)(proposal.metadata.priority));
433
+ console.log(' ID: ' + chalk.cyan(task.id));
434
+ console.log(' Title: ' + chalk.white(task.title || task.intent));
435
+ console.log(' Author: ' + chalk.white(`${task.author.name} (${task.author.type})`));
436
+ console.log(' Status: ' + getStatusColor(task.status)(task.status));
437
+ if (task.metadata?.priority) {
438
+ console.log(' Priority: ' + getPriorityColor(task.metadata.priority)(task.metadata.priority));
439
+ }
440
+ if (task.metadata?.readiness != null) {
441
+ console.log(' Readiness: ' + chalk.white(`${task.metadata.readiness}%`));
400
442
  }
401
- if (proposal.metadata?.tags && proposal.metadata.tags.length > 0) {
402
- console.log(' Tags: ' + chalk.gray(proposal.metadata.tags.join(', ')));
443
+ if (task.metadata?.tags && task.metadata.tags.length > 0) {
444
+ console.log(' Tags: ' + chalk.gray(task.metadata.tags.join(', ')));
403
445
  }
404
446
  console.log('\n' + chalk.cyan('Next steps:'));
405
- console.log(' • View task: ' + chalk.yellow(`nut task get ${proposal.id}`));
406
- console.log(' • Update task: ' + chalk.yellow(`nut task update ${proposal.id} --status proposed`));
447
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${task.id}`));
448
+ console.log(' • Update task: ' + chalk.yellow(`nut task update ${task.id} --status ready`));
407
449
  console.log(' • List tasks: ' + chalk.yellow('nut task list'));
408
450
  }
409
451
  catch (error) {
@@ -421,9 +463,11 @@ taskCommand
421
463
  .option('-t, -i, --title, --intent <text>', 'Update title (aliases: -i, --intent)')
422
464
  .option('-c, --content <text>', 'Update content')
423
465
  .option('-f, --file <path>', 'Read new content from a file')
424
- .option('-s, --status <status>', 'Update status (draft|backlog|ready|queued|active|blocked|review|done|canceled|duplicate)')
466
+ .option('-s, --status <status>', 'Update status (draft|backlog|ready|queued|active|blocked|review|revision|done|canceled|duplicate)')
425
467
  .option('-p, --priority <level>', 'Update priority (low|medium|high|critical)')
468
+ .option('-r, --readiness <score>', 'Update readiness score (0-100)')
426
469
  .option('--tags <tags>', 'Update tags (comma-separated)')
470
+ .option('--reviewers <reviewers>', 'Update reviewers (comma-separated names or emails)')
427
471
  .option('--spec <ref>', 'Update product specification reference')
428
472
  .option('--json', 'Output as JSON')
429
473
  .action(async (id, options) => {
@@ -431,10 +475,10 @@ taskCommand
431
475
  // Check if any update option is provided
432
476
  const titleValue = options.title || options.intent;
433
477
  const hasUpdates = titleValue || options.content || options.file ||
434
- options.status || options.priority || options.tags || options.spec;
478
+ options.status || options.priority || options.readiness || options.tags || options.reviewers || options.spec;
435
479
  if (!hasUpdates) {
436
480
  console.error(chalk.red('No updates specified.'));
437
- console.log(chalk.gray('Use --title, --content, --status, --priority, --tags, or --spec to update.'));
481
+ console.log(chalk.gray('Use --title, --content, --status, --priority, --readiness, --tags, --reviewers, or --spec to update.'));
438
482
  process.exit(1);
439
483
  }
440
484
  // Read content from file if specified
@@ -465,14 +509,18 @@ taskCommand
465
509
  if (options.spec !== undefined)
466
510
  updates.productSpecRef = options.spec;
467
511
  // Handle metadata updates
468
- if (options.priority || options.tags) {
512
+ if (options.priority || options.readiness || options.tags || options.reviewers) {
469
513
  updates.metadata = {};
470
514
  if (options.priority)
471
515
  updates.metadata.priority = validatePriority(options.priority);
516
+ if (options.readiness)
517
+ updates.metadata.readiness = validateReadiness(options.readiness);
472
518
  if (options.tags)
473
519
  updates.metadata.tags = parseTags(options.tags);
520
+ if (options.reviewers)
521
+ updates.metadata.reviewers = options.reviewers.split(',').map((r) => r.trim()).filter((r) => r.length > 0);
474
522
  }
475
- const updated = await updateProposal(id, updates);
523
+ const updated = await updateTask(id, updates);
476
524
  spinner.succeed(`Task updated: ${chalk.cyan(id)}`);
477
525
  if (options.json) {
478
526
  console.log(JSON.stringify(updated, null, 2));
@@ -485,6 +533,9 @@ taskCommand
485
533
  if (updated.metadata?.priority) {
486
534
  console.log(' Priority: ' + getPriorityColor(updated.metadata.priority)(updated.metadata.priority));
487
535
  }
536
+ if (updated.metadata?.readiness != null) {
537
+ console.log(' Readiness: ' + chalk.white(`${updated.metadata.readiness}%`));
538
+ }
488
539
  }
489
540
  catch (error) {
490
541
  console.error(chalk.red('Failed to update task:'), error.message);
@@ -503,18 +554,18 @@ taskCommand
503
554
  try {
504
555
  // Verify task exists first
505
556
  const spinner = ora('Loading task...').start();
506
- const proposal = await getProposal(id);
557
+ const task = await getTask(id);
507
558
  spinner.stop();
508
- if (!proposal) {
559
+ if (!task) {
509
560
  console.error(chalk.red(`Task '${id}' not found.`));
510
561
  process.exit(1);
511
562
  }
512
563
  // Confirm deletion
513
564
  if (!options.yes) {
514
565
  console.log('\n' + chalk.bold('Task to delete:'));
515
- console.log(' ID: ' + chalk.cyan(proposal.id));
516
- console.log(' Title: ' + chalk.white(proposal.title || proposal.intent));
517
- console.log(' Status: ' + getStatusColor(proposal.status)(proposal.status));
566
+ console.log(' ID: ' + chalk.cyan(task.id));
567
+ console.log(' Title: ' + chalk.white(task.title || task.intent));
568
+ console.log(' Status: ' + getStatusColor(task.status)(task.status));
518
569
  console.log();
519
570
  const { confirm } = await inquirer.prompt([
520
571
  {
@@ -530,7 +581,7 @@ taskCommand
530
581
  }
531
582
  }
532
583
  const deleteSpinner = ora('Deleting task...').start();
533
- const deleted = await deleteProposal(id);
584
+ const deleted = await deleteTask(id);
534
585
  if (deleted) {
535
586
  deleteSpinner.succeed(`Task deleted: ${chalk.cyan(id)}`);
536
587
  }
@@ -552,14 +603,14 @@ taskCommand
552
603
  .description('Add a plan step to a task')
553
604
  .argument('<id>', 'Task ID to add step to')
554
605
  .argument('<description>', 'Description of the step')
555
- .option('-s, --status <status>', 'Initial status (pending/in-progress/completed/failed)', 'pending')
606
+ .option('-s, --status <status>', 'Initial status (pending/active/done/failed/skipped)', 'pending')
556
607
  .option('-c, --command <cmd>', 'Command to execute for this step')
557
608
  .option('-e, --expected <outcome>', 'Expected outcome of this step')
558
609
  .option('--json', 'Output as JSON')
559
610
  .action(async (id, description, options) => {
560
611
  try {
561
612
  const spinner = ora('Adding plan step...').start();
562
- const validStepStatuses = ['pending', 'in-progress', 'completed', 'failed'];
613
+ const validStepStatuses = ['pending', 'active', 'done', 'failed', 'skipped'];
563
614
  const status = options.status?.toLowerCase();
564
615
  if (status && !validStepStatuses.includes(status)) {
565
616
  spinner.fail(`Invalid step status: ${options.status}`);
@@ -588,8 +639,9 @@ taskCommand
588
639
  console.log(' Expected: ' + chalk.gray(step.expectedOutcome));
589
640
  }
590
641
  console.log('\n' + chalk.cyan('Next steps:'));
591
- console.log(' • View task: ' + chalk.yellow(`nut task get ${id}`));
592
- console.log(' • Add more: ' + chalk.yellow(`nut task step ${id} "Next step"`));
642
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${id}`));
643
+ console.log(' • Add more: ' + chalk.yellow(`nut task step ${id} "Next step"`));
644
+ console.log(' • Update status: ' + chalk.yellow(`nut task step-update ${id} ${step.id} --status <status>`));
593
645
  }
594
646
  catch (error) {
595
647
  console.error(chalk.red('Failed to add plan step:'), error.message);
@@ -597,6 +649,81 @@ taskCommand
597
649
  }
598
650
  });
599
651
  // ============================================================================
652
+ // task step-update (update existing plan step)
653
+ // ============================================================================
654
+ taskCommand
655
+ .command('step-update')
656
+ .description('Update an existing plan step on a task')
657
+ .argument('<id>', 'Task ID containing the step')
658
+ .argument('<stepId>', 'Step ID to update (e.g. step-01)')
659
+ .option('-s, --status <status>', 'New status (pending/active/done/failed/skipped)')
660
+ .option('-d, --description <desc>', 'Update description')
661
+ .option('-c, --command <cmd>', 'Update command')
662
+ .option('-e, --expected <outcome>', 'Update expected outcome')
663
+ .option('--output <output>', 'Set output')
664
+ .option('--error <error>', 'Set error message')
665
+ .option('--json', 'Output as JSON')
666
+ .action(async (id, stepId, options) => {
667
+ try {
668
+ const spinner = ora('Updating plan step...').start();
669
+ const validStepStatuses = ['pending', 'active', 'done', 'failed', 'skipped'];
670
+ if (options.status) {
671
+ const status = options.status.toLowerCase();
672
+ if (!validStepStatuses.includes(status)) {
673
+ spinner.fail(`Invalid step status: ${options.status}`);
674
+ console.log(chalk.gray(`Valid statuses: ${validStepStatuses.join(', ')}`));
675
+ process.exit(1);
676
+ }
677
+ }
678
+ const updates = {};
679
+ if (options.status)
680
+ updates.status = options.status.toLowerCase();
681
+ if (options.description)
682
+ updates.description = options.description;
683
+ if (options.command)
684
+ updates.command = options.command;
685
+ if (options.expected)
686
+ updates.expectedOutcome = options.expected;
687
+ if (options.output)
688
+ updates.output = options.output;
689
+ if (options.error)
690
+ updates.error = options.error;
691
+ if (Object.keys(updates).length === 0) {
692
+ spinner.fail('No updates provided');
693
+ console.log(chalk.gray('Use --status, --description, --command, --expected, --output, or --error'));
694
+ process.exit(1);
695
+ }
696
+ const step = await updatePlanStep(id, stepId, updates);
697
+ spinner.succeed(`Plan step updated: ${chalk.cyan(step.id)}`);
698
+ if (options.json) {
699
+ console.log(JSON.stringify(step, null, 2));
700
+ return;
701
+ }
702
+ console.log('\n' + chalk.bold('Step Details:'));
703
+ console.log(' ID: ' + chalk.cyan(step.id));
704
+ console.log(' Description: ' + chalk.white(step.description));
705
+ console.log(' Status: ' + getStepStatusIcon(step.status) + ' ' + step.status);
706
+ if (step.command) {
707
+ console.log(' Command: ' + chalk.gray(step.command));
708
+ }
709
+ if (step.expectedOutcome) {
710
+ console.log(' Expected: ' + chalk.gray(step.expectedOutcome));
711
+ }
712
+ if (step.output) {
713
+ console.log(' Output: ' + chalk.gray(step.output));
714
+ }
715
+ if (step.error) {
716
+ console.log(' Error: ' + chalk.red(step.error));
717
+ }
718
+ console.log('\n' + chalk.cyan('Next steps:'));
719
+ console.log(' • View task: ' + chalk.yellow(`nut task get ${id}`));
720
+ }
721
+ catch (error) {
722
+ console.error(chalk.red('Failed to update plan step:'), error.message);
723
+ process.exit(1);
724
+ }
725
+ });
726
+ // ============================================================================
600
727
  // task comment (add comment)
601
728
  // ============================================================================
602
729
  taskCommand