@proletariat/cli 0.3.24 → 0.3.25

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 (75) hide show
  1. package/dist/commands/action/create.js +3 -3
  2. package/dist/commands/action/update.js +3 -3
  3. package/dist/commands/epic/activate.js +9 -17
  4. package/dist/commands/epic/archive.js +13 -24
  5. package/dist/commands/epic/create.js +7 -6
  6. package/dist/commands/epic/move.js +28 -47
  7. package/dist/commands/epic/progress.js +10 -14
  8. package/dist/commands/epic/project.js +42 -59
  9. package/dist/commands/epic/reorder.js +25 -30
  10. package/dist/commands/epic/spec.d.ts +1 -0
  11. package/dist/commands/epic/spec.js +39 -40
  12. package/dist/commands/epic/ticket.d.ts +2 -0
  13. package/dist/commands/epic/ticket.js +63 -37
  14. package/dist/commands/feedback/index.d.ts +10 -0
  15. package/dist/commands/feedback/index.js +60 -0
  16. package/dist/commands/feedback/list.d.ts +12 -0
  17. package/dist/commands/feedback/list.js +126 -0
  18. package/dist/commands/feedback/submit.d.ts +16 -0
  19. package/dist/commands/feedback/submit.js +220 -0
  20. package/dist/commands/feedback/view.d.ts +15 -0
  21. package/dist/commands/feedback/view.js +109 -0
  22. package/dist/commands/gh/index.js +4 -0
  23. package/dist/commands/repo/create.d.ts +38 -0
  24. package/dist/commands/repo/create.js +283 -0
  25. package/dist/commands/repo/index.js +7 -0
  26. package/dist/commands/roadmap/add-project.js +9 -22
  27. package/dist/commands/roadmap/create.d.ts +0 -1
  28. package/dist/commands/roadmap/create.js +46 -40
  29. package/dist/commands/roadmap/delete.js +10 -24
  30. package/dist/commands/roadmap/generate.d.ts +1 -0
  31. package/dist/commands/roadmap/generate.js +21 -22
  32. package/dist/commands/roadmap/remove-project.js +14 -34
  33. package/dist/commands/roadmap/reorder.js +19 -26
  34. package/dist/commands/roadmap/update.js +27 -26
  35. package/dist/commands/roadmap/view.js +5 -12
  36. package/dist/commands/session/attach.d.ts +1 -8
  37. package/dist/commands/session/attach.js +93 -59
  38. package/dist/commands/session/list.d.ts +0 -8
  39. package/dist/commands/session/list.js +130 -81
  40. package/dist/commands/spec/create.js +1 -1
  41. package/dist/commands/spec/edit.js +63 -33
  42. package/dist/commands/support/book.d.ts +10 -0
  43. package/dist/commands/support/book.js +54 -0
  44. package/dist/commands/support/discord.d.ts +10 -0
  45. package/dist/commands/support/discord.js +54 -0
  46. package/dist/commands/support/docs.d.ts +10 -0
  47. package/dist/commands/support/docs.js +54 -0
  48. package/dist/commands/support/index.d.ts +19 -0
  49. package/dist/commands/support/index.js +81 -0
  50. package/dist/commands/support/issues.d.ts +11 -0
  51. package/dist/commands/support/issues.js +77 -0
  52. package/dist/commands/support/logs.d.ts +18 -0
  53. package/dist/commands/support/logs.js +247 -0
  54. package/dist/commands/ticket/create.js +21 -13
  55. package/dist/commands/ticket/edit.js +44 -13
  56. package/dist/commands/ticket/move.d.ts +7 -0
  57. package/dist/commands/ticket/move.js +132 -0
  58. package/dist/commands/work/spawn.d.ts +1 -0
  59. package/dist/commands/work/spawn.js +71 -7
  60. package/dist/commands/work/start.js +6 -0
  61. package/dist/lib/execution/runners.js +21 -17
  62. package/dist/lib/execution/session-utils.d.ts +60 -0
  63. package/dist/lib/execution/session-utils.js +162 -0
  64. package/dist/lib/execution/spawner.d.ts +2 -0
  65. package/dist/lib/execution/spawner.js +42 -0
  66. package/dist/lib/flags/resolver.d.ts +2 -2
  67. package/dist/lib/flags/resolver.js +15 -0
  68. package/dist/lib/init/index.js +18 -0
  69. package/dist/lib/multiline-input.d.ts +63 -0
  70. package/dist/lib/multiline-input.js +360 -0
  71. package/dist/lib/prompt-json.d.ts +5 -5
  72. package/dist/lib/repos/git.d.ts +7 -0
  73. package/dist/lib/repos/git.js +20 -0
  74. package/oclif.manifest.json +2206 -1607
  75. package/package.json +1 -1
@@ -0,0 +1,247 @@
1
+ import { Flags } from '@oclif/core';
2
+ import * as os from 'node:os';
3
+ import { execSync } from 'node:child_process';
4
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
5
+ import { styles } from '../../lib/styles.js';
6
+ import { isMachineOutput, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
7
+ export default class SupportLogs extends PMOCommand {
8
+ static description = 'Collect diagnostic info for troubleshooting';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %>',
11
+ '<%= config.bin %> <%= command.id %> --clipboard',
12
+ '<%= config.bin %> <%= command.id %> --json',
13
+ ];
14
+ static flags = {
15
+ ...pmoBaseFlags,
16
+ clipboard: Flags.boolean({
17
+ description: 'Copy diagnostics to clipboard',
18
+ default: false,
19
+ }),
20
+ };
21
+ async execute() {
22
+ const { flags } = await this.parse(SupportLogs);
23
+ const diagnostics = await this.collectDiagnostics();
24
+ // In JSON mode, return structured object
25
+ if (isMachineOutput(flags)) {
26
+ outputSuccessAsJson(diagnostics, createMetadata('support logs', flags));
27
+ return;
28
+ }
29
+ // Format as readable text
30
+ const text = this.formatDiagnostics(diagnostics);
31
+ // Copy to clipboard if requested
32
+ if (flags.clipboard) {
33
+ const copied = this.copyToClipboard(text);
34
+ if (copied) {
35
+ this.log(styles.success('Diagnostics copied to clipboard!'));
36
+ this.log('');
37
+ }
38
+ }
39
+ // Display diagnostics
40
+ this.log(text);
41
+ }
42
+ async collectDiagnostics() {
43
+ const diagnostics = {
44
+ prlt: {
45
+ version: this.config.version,
46
+ },
47
+ node: {
48
+ version: process.version,
49
+ },
50
+ os: {
51
+ platform: process.platform,
52
+ release: os.release(),
53
+ arch: os.arch(),
54
+ },
55
+ shell: process.env.SHELL,
56
+ tools: {
57
+ gh: this.checkGh(),
58
+ docker: this.checkDocker(),
59
+ tmux: this.checkTmux(),
60
+ },
61
+ };
62
+ // Add workspace info if available
63
+ try {
64
+ const workspaceInfo = await this.getWorkspaceInfo();
65
+ if (workspaceInfo) {
66
+ diagnostics.workspace = workspaceInfo;
67
+ }
68
+ }
69
+ catch {
70
+ // PMO may not be initialized
71
+ }
72
+ return diagnostics;
73
+ }
74
+ checkGh() {
75
+ try {
76
+ const versionOutput = execSync('gh --version', {
77
+ encoding: 'utf-8',
78
+ stdio: ['ignore', 'pipe', 'ignore'],
79
+ });
80
+ const version = versionOutput.split('\n')[0]?.replace('gh version ', '').trim();
81
+ let authenticated = false;
82
+ try {
83
+ execSync('gh auth status', { stdio: 'ignore' });
84
+ authenticated = true;
85
+ }
86
+ catch {
87
+ authenticated = false;
88
+ }
89
+ return { installed: true, version, authenticated };
90
+ }
91
+ catch {
92
+ return { installed: false };
93
+ }
94
+ }
95
+ checkDocker() {
96
+ try {
97
+ execSync('docker --version', { stdio: 'ignore' });
98
+ let running = false;
99
+ try {
100
+ execSync('docker info', { stdio: 'ignore' });
101
+ running = true;
102
+ }
103
+ catch {
104
+ running = false;
105
+ }
106
+ return { installed: true, running };
107
+ }
108
+ catch {
109
+ return { installed: false };
110
+ }
111
+ }
112
+ checkTmux() {
113
+ try {
114
+ const versionOutput = execSync('tmux -V', {
115
+ encoding: 'utf-8',
116
+ stdio: ['ignore', 'pipe', 'ignore'],
117
+ });
118
+ const version = versionOutput.trim();
119
+ return { installed: true, version };
120
+ }
121
+ catch {
122
+ return { installed: false };
123
+ }
124
+ }
125
+ async getWorkspaceInfo() {
126
+ try {
127
+ const projects = await this.storage.listProjects();
128
+ let totalTickets = 0;
129
+ for (const project of projects) {
130
+ // eslint-disable-next-line no-await-in-loop
131
+ const tickets = await this.storage.listTickets(project.id);
132
+ totalTickets += tickets.length;
133
+ }
134
+ // Get agents count by checking for agents directory
135
+ let agentCount = 0;
136
+ try {
137
+ const { getWorkspaceInfo } = await import('../../lib/agents/commands.js');
138
+ const wsInfo = getWorkspaceInfo();
139
+ agentCount = wsInfo.agents?.length || 0;
140
+ }
141
+ catch {
142
+ // Agents module may not be available
143
+ }
144
+ return {
145
+ path: this.pmoPath,
146
+ name: projects[0]?.name,
147
+ repoCount: projects.length,
148
+ agentCount,
149
+ ticketCount: totalTickets,
150
+ };
151
+ }
152
+ catch {
153
+ return undefined;
154
+ }
155
+ }
156
+ formatDiagnostics(d) {
157
+ const lines = [];
158
+ lines.push(styles.title('Diagnostic Information'));
159
+ lines.push('');
160
+ // System info
161
+ lines.push(styles.header('System'));
162
+ lines.push(` prlt version: ${d.prlt.version}`);
163
+ lines.push(` Node version: ${d.node.version}`);
164
+ lines.push(` OS: ${d.os.platform} ${d.os.release} (${d.os.arch})`);
165
+ lines.push(` Shell: ${d.shell || 'unknown'}`);
166
+ lines.push('');
167
+ // Tools
168
+ lines.push(styles.header('Tools'));
169
+ // gh
170
+ if (d.tools.gh.installed) {
171
+ const authStatus = d.tools.gh.authenticated ? styles.success('authenticated') : styles.warning('not authenticated');
172
+ lines.push(` gh CLI: ${styles.success('installed')} (${d.tools.gh.version}) - ${authStatus}`);
173
+ }
174
+ else {
175
+ lines.push(` gh CLI: ${styles.warning('not installed')}`);
176
+ }
177
+ // docker
178
+ if (d.tools.docker.installed) {
179
+ const runStatus = d.tools.docker.running ? styles.success('running') : styles.warning('not running');
180
+ lines.push(` Docker: ${styles.success('installed')} - ${runStatus}`);
181
+ }
182
+ else {
183
+ lines.push(` Docker: ${styles.warning('not installed')}`);
184
+ }
185
+ // tmux
186
+ if (d.tools.tmux.installed) {
187
+ lines.push(` tmux: ${styles.success('installed')} (${d.tools.tmux.version})`);
188
+ }
189
+ else {
190
+ lines.push(` tmux: ${styles.muted('not installed')}`);
191
+ }
192
+ lines.push('');
193
+ // Workspace
194
+ if (d.workspace) {
195
+ lines.push(styles.header('Workspace'));
196
+ lines.push(` Path: ${d.workspace.path}`);
197
+ if (d.workspace.name) {
198
+ lines.push(` Name: ${d.workspace.name}`);
199
+ }
200
+ lines.push(` Projects: ${d.workspace.repoCount ?? 0}`);
201
+ lines.push(` Agents: ${d.workspace.agentCount ?? 0}`);
202
+ lines.push(` Tickets: ${d.workspace.ticketCount ?? 0}`);
203
+ }
204
+ else {
205
+ lines.push(styles.header('Workspace'));
206
+ lines.push(styles.muted(' No workspace initialized'));
207
+ }
208
+ return lines.join('\n');
209
+ }
210
+ copyToClipboard(text) {
211
+ const platform = process.platform;
212
+ // Strip ANSI codes for clipboard
213
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape code stripping
214
+ const plainText = text.replace(/\x1b\[[0-9;]*m/g, '');
215
+ try {
216
+ if (platform === 'darwin') {
217
+ execSync('pbcopy', { input: plainText, encoding: 'utf-8' });
218
+ return true;
219
+ }
220
+ else if (platform === 'linux') {
221
+ // Try xclip first, then xsel
222
+ try {
223
+ execSync('xclip -selection clipboard', { input: plainText, encoding: 'utf-8' });
224
+ return true;
225
+ }
226
+ catch {
227
+ try {
228
+ execSync('xsel --clipboard --input', { input: plainText, encoding: 'utf-8' });
229
+ return true;
230
+ }
231
+ catch {
232
+ this.log(styles.warning('Install xclip or xsel to enable clipboard support.'));
233
+ return false;
234
+ }
235
+ }
236
+ }
237
+ else if (platform === 'win32') {
238
+ execSync('clip', { input: plainText, encoding: 'utf-8' });
239
+ return true;
240
+ }
241
+ }
242
+ catch {
243
+ this.log(styles.warning('Could not copy to clipboard.'));
244
+ }
245
+ return false;
246
+ }
247
+ }
@@ -6,6 +6,7 @@ import { updateEpicTicketsSection } from '../../lib/pmo/epic-files.js';
6
6
  import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
7
7
  import { shouldOutputJson, outputErrorAsJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
8
8
  import { FlagResolver } from '../../lib/flags/index.js';
9
+ import { multiLineInput } from '../../lib/multiline-input.js';
9
10
  export default class TicketCreate extends PMOCommand {
10
11
  static description = 'Create a new ticket on the PMO board';
11
12
  static examples = [
@@ -400,18 +401,25 @@ export default class TicketCreate extends PMOCommand {
400
401
  return existingDescription;
401
402
  }
402
403
  this.log(styles.muted('\n─── Ticket Description (for agent execution) ───'));
403
- const descAnswers = await inquirer.prompt([
404
+ // Prompt for "What" - the main outcome
405
+ const { what } = await inquirer.prompt([
404
406
  {
405
407
  type: 'input',
406
408
  name: 'what',
407
409
  message: 'What is the concrete outcome? (one sentence):',
408
410
  validate: (input) => input.trim() ? true : 'Outcome cannot be empty - what does success look like?',
409
411
  },
410
- {
411
- type: 'input',
412
- name: 'doneWhen',
413
- message: 'Done when (acceptance criteria):',
414
- },
412
+ ]);
413
+ // Prompt for acceptance criteria using multiline input
414
+ const doneWhenResult = await multiLineInput({
415
+ message: 'Done when (acceptance criteria):',
416
+ hint: 'Enter each criterion on a new line. Ctrl+D to finish, Ctrl+C to cancel',
417
+ });
418
+ if (doneWhenResult.cancelled) {
419
+ throw new Error('Ticket creation cancelled');
420
+ }
421
+ // Continue with remaining prompts
422
+ const { context, notInScope } = await inquirer.prompt([
415
423
  {
416
424
  type: 'input',
417
425
  name: 'context',
@@ -427,10 +435,10 @@ export default class TicketCreate extends PMOCommand {
427
435
  ]);
428
436
  // Build structured description
429
437
  const parts = [];
430
- parts.push(`## What\n${descAnswers.what}`);
431
- if (descAnswers.doneWhen.trim()) {
438
+ parts.push(`## What\n${what}`);
439
+ if (doneWhenResult.value.trim()) {
432
440
  // Ensure each line in doneWhen starts with - [ ] if it doesn't already
433
- const criteria = descAnswers.doneWhen
441
+ const criteria = doneWhenResult.value
434
442
  .split('\n')
435
443
  .map(line => line.trim())
436
444
  .filter(line => line.length > 0)
@@ -446,11 +454,11 @@ export default class TicketCreate extends PMOCommand {
446
454
  .join('\n');
447
455
  parts.push(`## Done when\n${criteria}`);
448
456
  }
449
- if (descAnswers.context.trim()) {
450
- parts.push(`## Context\n${descAnswers.context}`);
457
+ if (context.trim()) {
458
+ parts.push(`## Context\n${context}`);
451
459
  }
452
- if (descAnswers.notInScope.trim()) {
453
- parts.push(`## Not in scope\n${descAnswers.notInScope}`);
460
+ if (notInScope.trim()) {
461
+ parts.push(`## Not in scope\n${notInScope}`);
454
462
  }
455
463
  return parts.join('\n\n');
456
464
  }
@@ -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 inquirer.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
+ ]);
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
291
+ const answers = await inquirer.prompt([
262
292
  {
263
293
  type: 'list',
264
294
  name: 'priority',
@@ -304,17 +334,18 @@ export default class TicketEdit extends PMOCommand {
304
334
  type: 'input',
305
335
  name: 'customCategory',
306
336
  message: 'Enter custom category:',
307
- when: (answers) => answers.categoryChoice === '__custom__',
337
+ when: (promptAnswers) => promptAnswers.categoryChoice === '__custom__',
308
338
  validate: (input) => input.trim() ? true : 'Category cannot be empty',
309
339
  },
310
340
  ]);
311
341
  // Build updates object with only changed fields
312
342
  const updates = {};
313
- if (answers.title !== ticket.title) {
314
- updates.title = answers.title;
343
+ if (title !== ticket.title) {
344
+ updates.title = title;
315
345
  }
316
- if (answers.description !== (ticket.description || '')) {
317
- updates.description = answers.description || undefined;
346
+ if (descResult.value !== (ticket.description || '')) {
347
+ // Preserve empty string to allow clearing the description
348
+ updates.description = descResult.value;
318
349
  }
319
350
  if (answers.priority !== (ticket.priority || '')) {
320
351
  updates.priority = answers.priority || undefined;
@@ -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
  }
@@ -22,6 +22,7 @@ export default class WorkSpawn extends PMOCommand {
22
22
  'create-pr': import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
23
  'no-pr': import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
24
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
26
  session: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
26
27
  focus: import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
28
  clone: import("@oclif/core/interfaces").BooleanFlag<boolean>;