@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.
- package/dist/commands/action/create.js +3 -3
- package/dist/commands/action/update.js +3 -3
- package/dist/commands/epic/activate.js +9 -17
- package/dist/commands/epic/archive.js +13 -24
- package/dist/commands/epic/create.js +7 -6
- package/dist/commands/epic/move.js +28 -47
- package/dist/commands/epic/progress.js +10 -14
- package/dist/commands/epic/project.js +42 -59
- package/dist/commands/epic/reorder.js +25 -30
- package/dist/commands/epic/spec.d.ts +1 -0
- package/dist/commands/epic/spec.js +39 -40
- package/dist/commands/epic/ticket.d.ts +2 -0
- package/dist/commands/epic/ticket.js +63 -37
- package/dist/commands/feedback/index.d.ts +10 -0
- package/dist/commands/feedback/index.js +60 -0
- package/dist/commands/feedback/list.d.ts +12 -0
- package/dist/commands/feedback/list.js +126 -0
- package/dist/commands/feedback/submit.d.ts +16 -0
- package/dist/commands/feedback/submit.js +220 -0
- package/dist/commands/feedback/view.d.ts +15 -0
- package/dist/commands/feedback/view.js +109 -0
- package/dist/commands/gh/index.js +4 -0
- package/dist/commands/repo/create.d.ts +38 -0
- package/dist/commands/repo/create.js +283 -0
- package/dist/commands/repo/index.js +7 -0
- package/dist/commands/roadmap/add-project.js +9 -22
- package/dist/commands/roadmap/create.d.ts +0 -1
- package/dist/commands/roadmap/create.js +46 -40
- package/dist/commands/roadmap/delete.js +10 -24
- package/dist/commands/roadmap/generate.d.ts +1 -0
- package/dist/commands/roadmap/generate.js +21 -22
- package/dist/commands/roadmap/remove-project.js +14 -34
- package/dist/commands/roadmap/reorder.js +19 -26
- package/dist/commands/roadmap/update.js +27 -26
- package/dist/commands/roadmap/view.js +5 -12
- package/dist/commands/session/attach.d.ts +1 -8
- package/dist/commands/session/attach.js +93 -59
- package/dist/commands/session/list.d.ts +0 -8
- package/dist/commands/session/list.js +130 -81
- package/dist/commands/spec/create.js +1 -1
- package/dist/commands/spec/edit.js +63 -33
- package/dist/commands/support/book.d.ts +10 -0
- package/dist/commands/support/book.js +54 -0
- package/dist/commands/support/discord.d.ts +10 -0
- package/dist/commands/support/discord.js +54 -0
- package/dist/commands/support/docs.d.ts +10 -0
- package/dist/commands/support/docs.js +54 -0
- package/dist/commands/support/index.d.ts +19 -0
- package/dist/commands/support/index.js +81 -0
- package/dist/commands/support/issues.d.ts +11 -0
- package/dist/commands/support/issues.js +77 -0
- package/dist/commands/support/logs.d.ts +18 -0
- package/dist/commands/support/logs.js +247 -0
- package/dist/commands/ticket/create.js +21 -13
- package/dist/commands/ticket/edit.js +44 -13
- package/dist/commands/ticket/move.d.ts +7 -0
- package/dist/commands/ticket/move.js +132 -0
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +71 -7
- package/dist/commands/work/start.js +6 -0
- package/dist/lib/execution/runners.js +21 -17
- package/dist/lib/execution/session-utils.d.ts +60 -0
- package/dist/lib/execution/session-utils.js +162 -0
- package/dist/lib/execution/spawner.d.ts +2 -0
- package/dist/lib/execution/spawner.js +42 -0
- package/dist/lib/flags/resolver.d.ts +2 -2
- package/dist/lib/flags/resolver.js +15 -0
- package/dist/lib/init/index.js +18 -0
- package/dist/lib/multiline-input.d.ts +63 -0
- package/dist/lib/multiline-input.js +360 -0
- package/dist/lib/prompt-json.d.ts +5 -5
- package/dist/lib/repos/git.d.ts +7 -0
- package/dist/lib/repos/git.js +20 -0
- package/oclif.manifest.json +2206 -1607
- 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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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${
|
|
431
|
-
if (
|
|
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 =
|
|
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 (
|
|
450
|
-
parts.push(`## Context\n${
|
|
457
|
+
if (context.trim()) {
|
|
458
|
+
parts.push(`## Context\n${context}`);
|
|
451
459
|
}
|
|
452
|
-
if (
|
|
453
|
-
parts.push(`## Not in scope\n${
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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: (
|
|
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 (
|
|
314
|
-
updates.title =
|
|
343
|
+
if (title !== ticket.title) {
|
|
344
|
+
updates.title = title;
|
|
315
345
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
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>;
|