@proletariat/cli 0.3.24 → 0.3.26

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.
Files changed (134) hide show
  1. package/dist/commands/action/create.js +3 -3
  2. package/dist/commands/action/index.js +2 -2
  3. package/dist/commands/action/update.js +3 -3
  4. package/dist/commands/agent/auth.js +1 -1
  5. package/dist/commands/agent/cleanup.js +6 -6
  6. package/dist/commands/agent/discover.js +1 -1
  7. package/dist/commands/agent/remove.js +4 -4
  8. package/dist/commands/autocomplete/setup.d.ts +2 -2
  9. package/dist/commands/autocomplete/setup.js +5 -5
  10. package/dist/commands/branch/create.js +31 -30
  11. package/dist/commands/category/create.js +4 -5
  12. package/dist/commands/category/delete.js +2 -3
  13. package/dist/commands/category/rename.js +2 -3
  14. package/dist/commands/claude.d.ts +2 -8
  15. package/dist/commands/claude.js +26 -26
  16. package/dist/commands/commit.d.ts +2 -8
  17. package/dist/commands/commit.js +4 -26
  18. package/dist/commands/config/index.d.ts +2 -10
  19. package/dist/commands/config/index.js +8 -34
  20. package/dist/commands/docker/index.d.ts +2 -2
  21. package/dist/commands/docker/index.js +8 -8
  22. package/dist/commands/epic/activate.js +9 -17
  23. package/dist/commands/epic/archive.js +13 -24
  24. package/dist/commands/epic/create.js +7 -6
  25. package/dist/commands/epic/delete.js +4 -5
  26. package/dist/commands/epic/move.js +28 -47
  27. package/dist/commands/epic/progress.js +10 -14
  28. package/dist/commands/epic/project.js +42 -59
  29. package/dist/commands/epic/reorder.js +25 -30
  30. package/dist/commands/epic/spec.d.ts +1 -0
  31. package/dist/commands/epic/spec.js +39 -40
  32. package/dist/commands/epic/ticket.d.ts +2 -0
  33. package/dist/commands/epic/ticket.js +63 -37
  34. package/dist/commands/feedback/index.d.ts +10 -0
  35. package/dist/commands/feedback/index.js +60 -0
  36. package/dist/commands/feedback/list.d.ts +12 -0
  37. package/dist/commands/feedback/list.js +126 -0
  38. package/dist/commands/feedback/submit.d.ts +16 -0
  39. package/dist/commands/feedback/submit.js +220 -0
  40. package/dist/commands/feedback/view.d.ts +15 -0
  41. package/dist/commands/feedback/view.js +109 -0
  42. package/dist/commands/gh/index.js +4 -0
  43. package/dist/commands/link/index.js +2 -2
  44. package/dist/commands/pmo/init.d.ts +2 -2
  45. package/dist/commands/pmo/init.js +7 -7
  46. package/dist/commands/project/spec.js +6 -6
  47. package/dist/commands/repo/create.d.ts +38 -0
  48. package/dist/commands/repo/create.js +283 -0
  49. package/dist/commands/repo/index.js +7 -0
  50. package/dist/commands/roadmap/add-project.js +9 -22
  51. package/dist/commands/roadmap/create.d.ts +0 -1
  52. package/dist/commands/roadmap/create.js +46 -40
  53. package/dist/commands/roadmap/delete.js +10 -24
  54. package/dist/commands/roadmap/generate.d.ts +1 -0
  55. package/dist/commands/roadmap/generate.js +21 -22
  56. package/dist/commands/roadmap/remove-project.js +14 -34
  57. package/dist/commands/roadmap/reorder.js +19 -26
  58. package/dist/commands/roadmap/update.js +27 -26
  59. package/dist/commands/roadmap/view.js +5 -12
  60. package/dist/commands/session/attach.d.ts +1 -8
  61. package/dist/commands/session/attach.js +93 -59
  62. package/dist/commands/session/health.d.ts +29 -0
  63. package/dist/commands/session/health.js +495 -0
  64. package/dist/commands/session/index.js +4 -0
  65. package/dist/commands/session/list.d.ts +0 -8
  66. package/dist/commands/session/list.js +130 -81
  67. package/dist/commands/spec/create.js +1 -1
  68. package/dist/commands/spec/edit.js +64 -35
  69. package/dist/commands/staff/add.d.ts +2 -2
  70. package/dist/commands/staff/add.js +15 -14
  71. package/dist/commands/staff/index.js +2 -2
  72. package/dist/commands/staff/remove.js +4 -4
  73. package/dist/commands/status/index.js +6 -7
  74. package/dist/commands/support/book.d.ts +10 -0
  75. package/dist/commands/support/book.js +54 -0
  76. package/dist/commands/support/discord.d.ts +10 -0
  77. package/dist/commands/support/discord.js +54 -0
  78. package/dist/commands/support/docs.d.ts +10 -0
  79. package/dist/commands/support/docs.js +54 -0
  80. package/dist/commands/support/index.d.ts +19 -0
  81. package/dist/commands/support/index.js +81 -0
  82. package/dist/commands/support/issues.d.ts +11 -0
  83. package/dist/commands/support/issues.js +77 -0
  84. package/dist/commands/support/logs.d.ts +18 -0
  85. package/dist/commands/support/logs.js +247 -0
  86. package/dist/commands/template/apply.js +10 -11
  87. package/dist/commands/template/create.js +18 -17
  88. package/dist/commands/template/index.d.ts +2 -2
  89. package/dist/commands/template/index.js +6 -6
  90. package/dist/commands/template/save.js +8 -7
  91. package/dist/commands/template/update.js +6 -7
  92. package/dist/commands/terminal/title.d.ts +2 -26
  93. package/dist/commands/terminal/title.js +4 -33
  94. package/dist/commands/theme/index.d.ts +2 -2
  95. package/dist/commands/theme/index.js +19 -18
  96. package/dist/commands/theme/set.d.ts +2 -2
  97. package/dist/commands/theme/set.js +5 -5
  98. package/dist/commands/ticket/create.js +52 -26
  99. package/dist/commands/ticket/delete.js +15 -13
  100. package/dist/commands/ticket/edit.js +59 -20
  101. package/dist/commands/ticket/epic.js +12 -10
  102. package/dist/commands/ticket/move.d.ts +7 -0
  103. package/dist/commands/ticket/move.js +132 -0
  104. package/dist/commands/ticket/project.js +11 -9
  105. package/dist/commands/ticket/reassign.js +23 -19
  106. package/dist/commands/ticket/spec.js +7 -5
  107. package/dist/commands/ticket/update.js +55 -53
  108. package/dist/commands/whoami.js +1 -0
  109. package/dist/commands/work/ready.js +7 -7
  110. package/dist/commands/work/revise.js +13 -11
  111. package/dist/commands/work/spawn.d.ts +1 -0
  112. package/dist/commands/work/spawn.js +225 -64
  113. package/dist/commands/work/start.d.ts +1 -0
  114. package/dist/commands/work/start.js +301 -173
  115. package/dist/hooks/init.js +4 -0
  116. package/dist/lib/execution/runners.js +21 -17
  117. package/dist/lib/execution/session-utils.d.ts +60 -0
  118. package/dist/lib/execution/session-utils.js +162 -0
  119. package/dist/lib/execution/spawner.d.ts +2 -0
  120. package/dist/lib/execution/spawner.js +42 -0
  121. package/dist/lib/flags/resolver.d.ts +2 -2
  122. package/dist/lib/flags/resolver.js +15 -0
  123. package/dist/lib/init/index.js +18 -0
  124. package/dist/lib/multiline-input.d.ts +63 -0
  125. package/dist/lib/multiline-input.js +360 -0
  126. package/dist/lib/pr/index.d.ts +4 -0
  127. package/dist/lib/pr/index.js +32 -14
  128. package/dist/lib/prompt-command.d.ts +3 -0
  129. package/dist/lib/prompt-json.d.ts +77 -6
  130. package/dist/lib/prompt-json.js +46 -0
  131. package/dist/lib/repos/git.d.ts +7 -0
  132. package/dist/lib/repos/git.js +20 -0
  133. package/oclif.manifest.json +2913 -2246
  134. package/package.json +1 -1
@@ -1,11 +1,13 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
3
  import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
+ // Note: inquirer import kept for inquirer.Separator usage in interactive mode
4
5
  import { styles } from '../../lib/styles.js';
5
6
  import { updateEpicTicketsSection } from '../../lib/pmo/epic-files.js';
6
7
  import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
7
8
  import { shouldOutputJson, outputErrorAsJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
8
9
  import { FlagResolver } from '../../lib/flags/index.js';
10
+ import { multiLineInput } from '../../lib/multiline-input.js';
9
11
  export default class TicketCreate extends PMOCommand {
10
12
  static description = 'Create a new ticket on the PMO board';
11
13
  static examples = [
@@ -289,7 +291,7 @@ export default class TicketCreate extends PMOCommand {
289
291
  if (!template && !flags.template) {
290
292
  const templates = await storage.listTicketTemplates();
291
293
  if (templates.length > 0) {
292
- const { selectedTemplate } = await inquirer.prompt([
294
+ const { selectedTemplate } = await this.prompt([
293
295
  {
294
296
  type: 'list',
295
297
  name: 'selectedTemplate',
@@ -303,13 +305,14 @@ export default class TicketCreate extends PMOCommand {
303
305
  })),
304
306
  ],
305
307
  },
306
- ]);
308
+ ], null);
307
309
  if (selectedTemplate) {
308
310
  template = templates.find(t => t.id === selectedTemplate) || null;
309
311
  }
310
312
  }
311
313
  }
312
- const answers = await inquirer.prompt([
314
+ // Prompt for title
315
+ const { title: answerTitle } = await this.prompt([
313
316
  {
314
317
  type: 'input',
315
318
  name: 'title',
@@ -317,13 +320,19 @@ export default class TicketCreate extends PMOCommand {
317
320
  default: flags.title || template?.titlePattern,
318
321
  validate: (input) => input.trim() ? true : 'Title cannot be empty',
319
322
  },
323
+ ], null);
324
+ // Prompt for column
325
+ const { column: answerColumn } = await this.prompt([
320
326
  {
321
327
  type: 'list',
322
328
  name: 'column',
323
329
  message: 'Column:',
324
- choices: columns,
330
+ choices: columns.map(c => ({ name: c, value: c })),
325
331
  default: flags.column || columns[0],
326
332
  },
333
+ ], null);
334
+ // Prompt for priority
335
+ const { priority: answerPriority } = await this.prompt([
327
336
  {
328
337
  type: 'list',
329
338
  name: 'priority',
@@ -334,6 +343,9 @@ export default class TicketCreate extends PMOCommand {
334
343
  ],
335
344
  default: flags.priority || template?.defaultPriority,
336
345
  },
346
+ ], null);
347
+ // Prompt for category
348
+ const { categoryChoice } = await this.prompt([
337
349
  {
338
350
  type: 'list',
339
351
  name: 'categoryChoice',
@@ -365,14 +377,19 @@ export default class TicketCreate extends PMOCommand {
365
377
  ],
366
378
  default: flags.category || template?.defaultCategory || '',
367
379
  },
368
- {
369
- type: 'input',
370
- name: 'customCategory',
371
- message: 'Enter custom category:',
372
- when: (answers) => answers.categoryChoice === '__custom__',
373
- validate: (input) => input.trim() ? true : 'Category cannot be empty',
374
- },
375
- ]);
380
+ ], null);
381
+ // Custom category prompt if needed
382
+ let customCategory;
383
+ if (categoryChoice === '__custom__') {
384
+ const result = await this.prompt([{
385
+ type: 'input',
386
+ name: 'customCategory',
387
+ message: 'Enter custom category:',
388
+ validate: (input) => input.trim() ? true : 'Category cannot be empty',
389
+ }], null);
390
+ customCategory = result.customCategory;
391
+ }
392
+ const answers = { title: answerTitle, column: answerColumn, priority: answerPriority, categoryChoice, customCategory };
376
393
  // Resolve category from choice or custom input
377
394
  const category = answers.categoryChoice === '__custom__'
378
395
  ? answers.customCategory
@@ -400,37 +417,46 @@ export default class TicketCreate extends PMOCommand {
400
417
  return existingDescription;
401
418
  }
402
419
  this.log(styles.muted('\n─── Ticket Description (for agent execution) ───'));
403
- const descAnswers = await inquirer.prompt([
420
+ // Prompt for "What" - the main outcome
421
+ const { what } = await this.prompt([
404
422
  {
405
423
  type: 'input',
406
424
  name: 'what',
407
425
  message: 'What is the concrete outcome? (one sentence):',
408
426
  validate: (input) => input.trim() ? true : 'Outcome cannot be empty - what does success look like?',
409
427
  },
410
- {
411
- type: 'input',
412
- name: 'doneWhen',
413
- message: 'Done when (acceptance criteria):',
414
- },
428
+ ], null);
429
+ // Prompt for acceptance criteria using multiline input
430
+ const doneWhenResult = await multiLineInput({
431
+ message: 'Done when (acceptance criteria):',
432
+ hint: 'Enter each criterion on a new line. Ctrl+D to finish, Ctrl+C to cancel',
433
+ });
434
+ if (doneWhenResult.cancelled) {
435
+ throw new Error('Ticket creation cancelled');
436
+ }
437
+ // Continue with remaining prompts
438
+ const { context } = await this.prompt([
415
439
  {
416
440
  type: 'input',
417
441
  name: 'context',
418
442
  message: 'Context (files, patterns, hints - optional):',
419
443
  default: '',
420
444
  },
445
+ ], null);
446
+ const { notInScope } = await this.prompt([
421
447
  {
422
448
  type: 'input',
423
449
  name: 'notInScope',
424
450
  message: 'Not in scope (explicit exclusions - optional):',
425
451
  default: '',
426
452
  },
427
- ]);
453
+ ], null);
428
454
  // Build structured description
429
455
  const parts = [];
430
- parts.push(`## What\n${descAnswers.what}`);
431
- if (descAnswers.doneWhen.trim()) {
456
+ parts.push(`## What\n${what}`);
457
+ if (doneWhenResult.value.trim()) {
432
458
  // Ensure each line in doneWhen starts with - [ ] if it doesn't already
433
- const criteria = descAnswers.doneWhen
459
+ const criteria = doneWhenResult.value
434
460
  .split('\n')
435
461
  .map(line => line.trim())
436
462
  .filter(line => line.length > 0)
@@ -446,11 +472,11 @@ export default class TicketCreate extends PMOCommand {
446
472
  .join('\n');
447
473
  parts.push(`## Done when\n${criteria}`);
448
474
  }
449
- if (descAnswers.context.trim()) {
450
- parts.push(`## Context\n${descAnswers.context}`);
475
+ if (context.trim()) {
476
+ parts.push(`## Context\n${context}`);
451
477
  }
452
- if (descAnswers.notInScope.trim()) {
453
- parts.push(`## Not in scope\n${descAnswers.notInScope}`);
478
+ if (notInScope.trim()) {
479
+ parts.push(`## Not in scope\n${notInScope}`);
454
480
  }
455
481
  return parts.join('\n\n');
456
482
  }
@@ -1,5 +1,4 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
- import inquirer from 'inquirer';
3
2
  import { autoExportToBoard, PMOCommand, pmoBaseFlags, } from '../../lib/pmo/index.js';
4
3
  import { styles } from '../../lib/styles.js';
5
4
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
@@ -57,7 +56,7 @@ export default class TicketDelete extends PMOCommand {
57
56
  }
58
57
  // Bulk mode
59
58
  if (flags.bulk) {
60
- await this.executeBulk(allTickets, flags.force);
59
+ await this.executeBulk(allTickets, flags.force, flags);
61
60
  return;
62
61
  }
63
62
  // Single ticket mode
@@ -90,16 +89,17 @@ export default class TicketDelete extends PMOCommand {
90
89
  this.log(` Title: ${ticket.title}`);
91
90
  this.log(` Project: ${board.name}`);
92
91
  this.log(` Status: ${ticket.statusName}`);
93
- const { confirmed } = await inquirer.prompt([{
92
+ const jsonModeConfig = jsonMode ? { flags, commandName: 'ticket delete' } : null;
93
+ const { confirmed } = await this.prompt([{
94
94
  type: 'list',
95
95
  name: 'confirmed',
96
96
  message: 'Are you sure?',
97
97
  choices: [
98
- { name: 'No, cancel', value: false },
99
- { name: 'Yes, delete', value: true },
98
+ { name: 'No, cancel', value: false, command: '' },
99
+ { name: 'Yes, delete', value: true, command: `prlt ticket delete ${ticketId} --force --json` },
100
100
  ],
101
101
  default: 0,
102
- }]);
102
+ }], jsonModeConfig);
103
103
  if (!confirmed) {
104
104
  this.log(styles.warning('Deletion cancelled.'));
105
105
  return;
@@ -112,10 +112,12 @@ export default class TicketDelete extends PMOCommand {
112
112
  this.log(styles.success(`\n✅ Ticket ${styles.emphasis(ticketId)} deleted`));
113
113
  this.log(styles.muted(' Removed from database and board'));
114
114
  }
115
- async executeBulk(allTickets, force) {
115
+ async executeBulk(allTickets, force, flags) {
116
+ const jsonMode = flags ? shouldOutputJson(flags) : false;
117
+ const jsonModeConfig = jsonMode ? { flags: flags, commandName: 'ticket delete' } : null;
116
118
  this.log(styles.emphasis('🗑️ Delete Multiple Tickets\n'));
117
119
  // Select tickets to delete
118
- const { selectedTickets } = await inquirer.prompt([{
120
+ const { selectedTickets } = await this.prompt([{
119
121
  type: 'checkbox',
120
122
  name: 'selectedTickets',
121
123
  message: 'Select tickets to DELETE:',
@@ -123,7 +125,7 @@ export default class TicketDelete extends PMOCommand {
123
125
  name: `${t.id} - ${t.title} (${t.statusName})`,
124
126
  value: t.id,
125
127
  })),
126
- }]);
128
+ }], jsonModeConfig);
127
129
  if (selectedTickets.length === 0) {
128
130
  this.log(styles.muted('No tickets selected.'));
129
131
  return;
@@ -136,16 +138,16 @@ export default class TicketDelete extends PMOCommand {
136
138
  this.log(styles.primary(` • ${ticketId}: ${ticket?.title}`));
137
139
  }
138
140
  this.log('');
139
- const { confirm } = await inquirer.prompt([{
141
+ const { confirm } = await this.prompt([{
140
142
  type: 'list',
141
143
  name: 'confirm',
142
144
  message: 'Are you sure? This cannot be undone.',
143
145
  choices: [
144
- { name: 'No, cancel', value: false },
145
- { name: 'Yes, DELETE tickets', value: true }
146
+ { name: 'No, cancel', value: false, command: '' },
147
+ { name: 'Yes, DELETE tickets', value: true, command: 'prlt ticket delete --bulk --force --json' }
146
148
  ],
147
149
  default: 0
148
- }]);
150
+ }], jsonModeConfig);
149
151
  if (!confirm) {
150
152
  this.log(styles.muted('Deletion cancelled.'));
151
153
  return;
@@ -4,6 +4,7 @@ import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index
4
4
  import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
7
+ import { multiLineInput } from '../../lib/multiline-input.js';
7
8
  export default class TicketEdit extends PMOCommand {
8
9
  static description = 'Edit an existing ticket';
9
10
  static examples = [
@@ -128,6 +129,29 @@ export default class TicketEdit extends PMOCommand {
128
129
  flags.owner || flags.assignee || flags['add-subtask'] || flags['clear-subtasks'] ||
129
130
  flags['add-label'] || flags['remove-label'] || flags['add-ac'] || flags['clear-ac'];
130
131
  if (flags.interactive || !hasFlags) {
132
+ // In JSON mode without flags, output a form prompt instead of interactive prompts
133
+ if (jsonMode) {
134
+ const { outputPromptAsJson, buildFormPromptConfig } = await import('../../lib/prompt-json.js');
135
+ const formConfig = buildFormPromptConfig([
136
+ { type: 'input', name: 'title', message: 'Title:', default: ticket.title },
137
+ { type: 'multiline', name: 'description', message: 'Description:', default: ticket.description || '' },
138
+ { type: 'list', name: 'priority', message: 'Priority:', choices: [
139
+ { name: 'None', value: '' },
140
+ { name: 'P0 - Critical', value: 'P0' },
141
+ { name: 'P1 - High', value: 'P1' },
142
+ { name: 'P2 - Medium', value: 'P2' },
143
+ { name: 'P3 - Low', value: 'P3' },
144
+ ], default: ticket.priority || '' },
145
+ { type: 'input', name: 'category', message: 'Category:', default: ticket.category || '' },
146
+ ]);
147
+ formConfig.context = {
148
+ hint: `Edit ticket with: prlt ticket edit ${ticketId} --title "..." --description "..." --priority P0 --json`,
149
+ ticketId,
150
+ currentValues: { title: ticket.title, description: ticket.description, priority: ticket.priority, category: ticket.category },
151
+ };
152
+ outputPromptAsJson(formConfig, createMetadata('ticket edit', flags));
153
+ return; // outputPromptAsJson exits, but TypeScript doesn't know
154
+ }
131
155
  // Interactive mode - prompt for all editable fields
132
156
  const board = await this.storage.getBoard(ticket.projectId);
133
157
  const columns = board.columns.map(col => col.name);
@@ -244,7 +268,8 @@ export default class TicketEdit extends PMOCommand {
244
268
  this.log('');
245
269
  }
246
270
  async promptForEdits(ticket, _columns) {
247
- const answers = await inquirer.prompt([
271
+ // First prompt for title
272
+ const { title } = await this.prompt([
248
273
  {
249
274
  type: 'input',
250
275
  name: 'title',
@@ -252,13 +277,18 @@ export default class TicketEdit extends PMOCommand {
252
277
  default: ticket.title,
253
278
  validate: (input) => input.trim() ? true : 'Title cannot be empty',
254
279
  },
255
- {
256
- type: 'editor',
257
- name: 'description',
258
- message: 'Description (opens $EDITOR for multiline input):',
259
- default: ticket.description || '',
260
- waitForUseInput: false,
261
- },
280
+ ], null);
281
+ // Prompt for description using inline multiline input
282
+ const descResult = await multiLineInput({
283
+ message: 'Description:',
284
+ default: ticket.description || '',
285
+ hint: 'Ctrl+D to finish, Ctrl+C to cancel',
286
+ });
287
+ if (descResult.cancelled) {
288
+ throw new Error('Edit cancelled');
289
+ }
290
+ // Continue with remaining prompts - priority first
291
+ const { priority } = await this.prompt([
262
292
  {
263
293
  type: 'list',
264
294
  name: 'priority',
@@ -269,6 +299,9 @@ export default class TicketEdit extends PMOCommand {
269
299
  ],
270
300
  default: ticket.priority || '',
271
301
  },
302
+ ], null);
303
+ // Then category
304
+ const { categoryChoice } = await this.prompt([
272
305
  {
273
306
  type: 'list',
274
307
  name: 'categoryChoice',
@@ -300,21 +333,27 @@ export default class TicketEdit extends PMOCommand {
300
333
  ],
301
334
  default: ticket.category || '',
302
335
  },
303
- {
304
- type: 'input',
305
- name: 'customCategory',
306
- message: 'Enter custom category:',
307
- when: (answers) => answers.categoryChoice === '__custom__',
308
- validate: (input) => input.trim() ? true : 'Category cannot be empty',
309
- },
310
- ]);
336
+ ], null);
337
+ // Custom category prompt if needed
338
+ let customCategory;
339
+ if (categoryChoice === '__custom__') {
340
+ const result = await this.prompt([{
341
+ type: 'input',
342
+ name: 'customCategory',
343
+ message: 'Enter custom category:',
344
+ validate: (input) => input.trim() ? true : 'Category cannot be empty',
345
+ }], null);
346
+ customCategory = result.customCategory;
347
+ }
348
+ const answers = { priority, categoryChoice, customCategory };
311
349
  // Build updates object with only changed fields
312
350
  const updates = {};
313
- if (answers.title !== ticket.title) {
314
- updates.title = answers.title;
351
+ if (title !== ticket.title) {
352
+ updates.title = title;
315
353
  }
316
- if (answers.description !== (ticket.description || '')) {
317
- updates.description = answers.description || undefined;
354
+ if (descResult.value !== (ticket.description || '')) {
355
+ // Preserve empty string to allow clearing the description
356
+ updates.description = descResult.value;
318
357
  }
319
358
  if (answers.priority !== (ticket.priority || '')) {
320
359
  updates.priority = answers.priority || undefined;
@@ -1,5 +1,4 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
- import inquirer from 'inquirer';
3
2
  import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
4
3
  import { styles } from '../../lib/styles.js';
5
4
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
@@ -198,6 +197,8 @@ export default class TicketEpic extends PMOCommand {
198
197
  this.log(styles.muted(` Epic: ${epic.title}`));
199
198
  }
200
199
  async executeBulk(flags) {
200
+ const jsonMode = shouldOutputJson(flags);
201
+ const jsonModeConfig = jsonMode ? { flags: flags, commandName: 'ticket epic' } : null;
201
202
  this.log(styles.emphasis('🔗 Link Tickets to Epic\n'));
202
203
  // Get project first
203
204
  const projectId = await this.requireProject();
@@ -238,7 +239,7 @@ export default class TicketEpic extends PMOCommand {
238
239
  ticketEpics.set(ticket.id, row?.epic_id || null);
239
240
  }
240
241
  // Select tickets to link
241
- const { selectedTickets } = await inquirer.prompt([{
242
+ const { selectedTickets } = await this.prompt([{
242
243
  type: 'checkbox',
243
244
  name: 'selectedTickets',
244
245
  message: 'Select tickets to link:',
@@ -250,7 +251,7 @@ export default class TicketEpic extends PMOCommand {
250
251
  value: t.id,
251
252
  };
252
253
  }),
253
- }]);
254
+ }], jsonModeConfig);
254
255
  if (selectedTickets.length === 0) {
255
256
  this.log(styles.muted('No tickets selected.'));
256
257
  return;
@@ -258,18 +259,19 @@ export default class TicketEpic extends PMOCommand {
258
259
  // Select target epic
259
260
  let targetEpic = flags['to-epic'];
260
261
  if (targetEpic === undefined) {
261
- const { epic } = await inquirer.prompt([{
262
+ const { epic } = await this.prompt([{
262
263
  type: 'list',
263
264
  name: 'epic',
264
265
  message: 'Link to which epic?',
265
266
  choices: [
266
- { name: 'None (remove epic link)', value: null },
267
+ { name: 'None (remove epic link)', value: null, command: 'prlt ticket epic --bulk --unlink --json' },
267
268
  ...epics.map(e => ({
268
269
  name: `${e.title} (${e.status})`,
269
270
  value: e.id,
271
+ command: `prlt ticket epic --bulk --to-epic ${e.id} --json`,
270
272
  })),
271
273
  ],
272
- }]);
274
+ }], jsonModeConfig);
273
275
  targetEpic = epic;
274
276
  }
275
277
  // Confirmation
@@ -285,16 +287,16 @@ export default class TicketEpic extends PMOCommand {
285
287
  this.log(styles.primary(` • ${ticketId}: ${ticket?.title}`));
286
288
  }
287
289
  this.log('');
288
- const { confirm } = await inquirer.prompt([{
290
+ const { confirm } = await this.prompt([{
289
291
  type: 'list',
290
292
  name: 'confirm',
291
293
  message: 'Continue?',
292
294
  choices: [
293
- { name: 'No, cancel', value: false },
294
- { name: 'Yes, link tickets', value: true }
295
+ { name: 'No, cancel', value: false, command: '' },
296
+ { name: 'Yes, link tickets', value: true, command: `prlt ticket epic --bulk --to-epic ${targetEpic || 'none'} --force --json` }
295
297
  ],
296
298
  default: 0
297
- }]);
299
+ }], jsonModeConfig);
298
300
  if (!confirm) {
299
301
  this.log(styles.muted('Operation cancelled.'));
300
302
  return;
@@ -9,10 +9,17 @@ export default class TicketMove extends PMOCommand {
9
9
  static flags: {
10
10
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  position: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'to-project': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  bulk: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  };
16
17
  execute(): Promise<void>;
17
18
  private executeBulk;
19
+ /**
20
+ * Move a ticket to a different project.
21
+ * If a target column is specified and exists in the target project, move to that column.
22
+ * Otherwise, use the default/backlog column.
23
+ */
24
+ private executeCrossProjectMove;
18
25
  }
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { autoExportToBoard, PMOCommand, pmoBaseFlags, } from '../../lib/pmo/index.js';
3
+ import { PMOError } from '../../lib/pmo/types.js';
3
4
  import { styles } from '../../lib/styles.js';
4
5
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
5
6
  export default class TicketMove extends PMOCommand {
@@ -8,6 +9,7 @@ export default class TicketMove extends PMOCommand {
8
9
  '<%= config.bin %> <%= command.id %> my-ticket "In Progress"',
9
10
  '<%= config.bin %> <%= command.id %> implement-auth Done',
10
11
  '<%= config.bin %> <%= command.id %> fix-bug "In Review" --position 0',
12
+ '<%= config.bin %> <%= command.id %> TKT-123 --to-project PROJ-002',
11
13
  '<%= config.bin %> <%= command.id %> --bulk',
12
14
  ];
13
15
  static args = {
@@ -31,6 +33,9 @@ export default class TicketMove extends PMOCommand {
31
33
  position: Flags.integer({
32
34
  description: 'Position within the column (0 = top)',
33
35
  }),
36
+ 'to-project': Flags.string({
37
+ description: 'Move ticket to a different project (uses Backlog/default column)',
38
+ }),
34
39
  bulk: Flags.boolean({
35
40
  char: 'b',
36
41
  description: 'Enable bulk mode to move multiple tickets',
@@ -54,6 +59,16 @@ export default class TicketMove extends PMOCommand {
54
59
  }
55
60
  this.error(message);
56
61
  };
62
+ // Cross-project move: if ticketId and --to-project are provided, skip project context
63
+ // The source project is determined from the ticket itself
64
+ if (args.ticketId && flags['to-project']) {
65
+ const ticket = await this.storage.getTicket(args.ticketId);
66
+ if (!ticket) {
67
+ return handleError('TICKET_NOT_FOUND', `Ticket "${args.ticketId}" not found.`);
68
+ }
69
+ await this.executeCrossProjectMove(ticket, flags['to-project'], args.column, jsonMode, flags);
70
+ return;
71
+ }
57
72
  // This command requires project context - get projectId (with JSON mode support)
58
73
  const projectId = await this.requireProject({
59
74
  jsonMode: {
@@ -94,9 +109,69 @@ export default class TicketMove extends PMOCommand {
94
109
  if (!ticket) {
95
110
  this.error(`Ticket "${ticketId}" not found.`);
96
111
  }
112
+ // Cross-project move (when --to-project flag is provided)
113
+ if (flags['to-project']) {
114
+ await this.executeCrossProjectMove(ticket, flags['to-project'], args.column, jsonMode, flags);
115
+ return;
116
+ }
97
117
  // Get target column - prompt if not provided
98
118
  let targetColumn = args.column;
99
119
  if (!targetColumn) {
120
+ // Check if there are other projects to move to
121
+ const allProjects = await this.storage.listProjects();
122
+ const otherProjects = allProjects.filter(p => p.id !== projectId);
123
+ // If there are other projects, ask user what type of move they want
124
+ if (otherProjects.length > 0) {
125
+ const moveTypeChoices = [
126
+ { id: 'column', name: 'Different column (same project)' },
127
+ { id: 'project', name: 'Different project' },
128
+ ];
129
+ const moveType = await this.selectFromList({
130
+ message: 'Move to:',
131
+ items: moveTypeChoices,
132
+ getName: (choice) => choice.name,
133
+ getValue: (choice) => choice.id,
134
+ getCommand: (choice) => choice.id === 'column'
135
+ ? `prlt ticket move ${ticketId} -P ${projectId} --json`
136
+ : `prlt ticket project ${ticketId} -P ${projectId} --json`,
137
+ jsonMode: jsonMode ? { flags, commandName: 'ticket move' } : null,
138
+ });
139
+ if (!moveType) {
140
+ return; // Cancelled or JSON mode
141
+ }
142
+ // If user chose different project, handle cross-project move
143
+ if (moveType === 'project') {
144
+ const targetProjectId = await this.selectFromList({
145
+ message: 'Select target project:',
146
+ items: otherProjects,
147
+ getName: (p) => `${p.name} (${p.id})`,
148
+ getValue: (p) => p.id,
149
+ getCommand: (p) => `prlt ticket move ${ticketId} --to-project ${p.id} --json`,
150
+ jsonMode: jsonMode ? { flags, commandName: 'ticket move' } : null,
151
+ });
152
+ if (!targetProjectId) {
153
+ return; // Cancelled or JSON mode
154
+ }
155
+ // Get columns from target project and ask which column to move to
156
+ const targetProjectBoard = await this.storage.getProjectBoard(targetProjectId);
157
+ if (!targetProjectBoard) {
158
+ this.error('Target project not found.');
159
+ }
160
+ const targetColumnName = await this.selectFromList({
161
+ message: 'Move to column:',
162
+ items: targetProjectBoard.columns,
163
+ getName: (col) => col.name,
164
+ getValue: (col) => col.name,
165
+ getCommand: (col) => `prlt ticket move ${ticketId} "${col.name}" --to-project ${targetProjectId} --json`,
166
+ jsonMode: jsonMode ? { flags, commandName: 'ticket move' } : null,
167
+ });
168
+ if (!targetColumnName) {
169
+ return; // Cancelled or JSON mode
170
+ }
171
+ await this.executeCrossProjectMove(ticket, targetProjectId, targetColumnName, jsonMode, flags);
172
+ return;
173
+ }
174
+ }
100
175
  // Get columns from the database (not config.json) to ensure accuracy
101
176
  const project = await this.storage.getProjectBoard(projectId);
102
177
  if (!project) {
@@ -221,4 +296,61 @@ export default class TicketMove extends PMOCommand {
221
296
  this.log(styles.error(`Failed to move ${failCount} ticket(s)`));
222
297
  }
223
298
  }
299
+ /**
300
+ * Move a ticket to a different project.
301
+ * If a target column is specified and exists in the target project, move to that column.
302
+ * Otherwise, use the default/backlog column.
303
+ */
304
+ async executeCrossProjectMove(ticket, targetProjectId, targetColumn, jsonMode, flags) {
305
+ const ticketId = ticket.id;
306
+ const sourceProjectId = ticket.projectId;
307
+ // Check if target project exists
308
+ const projects = await this.storage.listProjects();
309
+ const targetProject = projects.find(p => p.id === targetProjectId ||
310
+ p.id.toLowerCase() === targetProjectId.toLowerCase() ||
311
+ p.name.toLowerCase() === targetProjectId.toLowerCase());
312
+ if (!targetProject) {
313
+ if (jsonMode) {
314
+ outputErrorAsJson('PROJECT_NOT_FOUND', `Project not found: ${targetProjectId}`, createMetadata('ticket move', flags));
315
+ this.exit(1);
316
+ }
317
+ this.error(`Project not found: ${targetProjectId}`);
318
+ }
319
+ // Check if moving to the same project
320
+ if (targetProject.id === sourceProjectId) {
321
+ this.log(styles.warning(`Ticket "${ticketId}" is already in project "${targetProject.id}".`));
322
+ this.log(styles.muted(`To move to a different column, use: prlt ticket move ${ticketId} <column>`));
323
+ return;
324
+ }
325
+ // Move ticket to the new project
326
+ const movedTicket = await this.storage.moveTicketToProject(ticketId, targetProject.id);
327
+ // If a target column was specified, try to move to that column in the new project
328
+ if (targetColumn) {
329
+ try {
330
+ await this.storage.moveTicket(targetProject.id, ticketId, targetColumn);
331
+ // Refresh ticket to get updated status
332
+ const updatedTicket = await this.storage.getTicket(ticketId);
333
+ await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
334
+ this.log(styles.success(`\n✅ Moved ticket ${styles.emphasis(ticketId)} to project ${styles.emphasis(targetProject.id)}`));
335
+ this.log(styles.muted(` From project: ${sourceProjectId}`));
336
+ this.log(styles.muted(` To project: ${targetProject.id}`));
337
+ this.log(styles.muted(` Column: ${updatedTicket?.statusName || targetColumn}`));
338
+ return;
339
+ }
340
+ catch (error) {
341
+ // Only catch "status not found" errors - re-throw unexpected errors
342
+ if (error instanceof PMOError && error.code === 'NOT_FOUND') {
343
+ this.log(styles.muted(`Note: Column "${targetColumn}" not found in target project, using default column.`));
344
+ }
345
+ else {
346
+ throw error;
347
+ }
348
+ }
349
+ }
350
+ await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
351
+ this.log(styles.success(`\n✅ Moved ticket ${styles.emphasis(ticketId)} to project ${styles.emphasis(targetProject.id)}`));
352
+ this.log(styles.muted(` From project: ${sourceProjectId}`));
353
+ this.log(styles.muted(` To project: ${targetProject.id}`));
354
+ this.log(styles.muted(` Column: ${movedTicket.statusName || 'default'}`));
355
+ }
224
356
  }