@proletariat/cli 0.3.19 → 0.3.21
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/agent/login.js +2 -2
- package/dist/commands/agent/remove.d.ts +1 -0
- package/dist/commands/agent/remove.js +36 -28
- package/dist/commands/agent/shell.js +2 -2
- package/dist/commands/agent/staff/remove.d.ts +1 -0
- package/dist/commands/agent/staff/remove.js +36 -28
- package/dist/commands/agent/status.js +2 -2
- package/dist/commands/agent/temp/cleanup.js +10 -17
- package/dist/commands/agent/themes/add-names.d.ts +1 -0
- package/dist/commands/agent/themes/add-names.js +5 -1
- package/dist/commands/agent/visit.js +2 -2
- package/dist/commands/board/view.d.ts +15 -0
- package/dist/commands/board/view.js +136 -0
- package/dist/commands/config/index.js +6 -3
- package/dist/commands/epic/link/index.js +17 -0
- package/dist/commands/execution/config.d.ts +34 -0
- package/dist/commands/execution/config.js +433 -0
- package/dist/commands/execution/index.js +6 -1
- package/dist/commands/execution/kill.d.ts +12 -0
- package/dist/commands/execution/kill.js +17 -0
- package/dist/commands/execution/list.js +5 -4
- package/dist/commands/execution/logs.js +1 -0
- package/dist/commands/execution/view.d.ts +17 -0
- package/dist/commands/execution/view.js +288 -0
- package/dist/commands/phase/move.js +8 -0
- package/dist/commands/phase/template/apply.js +2 -2
- package/dist/commands/phase/template/create.js +67 -20
- package/dist/commands/phase/template/list.js +1 -1
- package/dist/commands/pr/index.js +6 -2
- package/dist/commands/pr/list.d.ts +17 -0
- package/dist/commands/pr/list.js +163 -0
- package/dist/commands/project/update.d.ts +19 -0
- package/dist/commands/project/update.js +163 -0
- package/dist/commands/roadmap/create.js +5 -0
- package/dist/commands/spec/delete.d.ts +18 -0
- package/dist/commands/spec/delete.js +111 -0
- package/dist/commands/spec/edit.d.ts +23 -0
- package/dist/commands/spec/edit.js +232 -0
- package/dist/commands/spec/index.js +5 -0
- package/dist/commands/status/create.js +38 -34
- package/dist/commands/status/list.js +5 -3
- package/dist/commands/template/phase/create.d.ts +1 -0
- package/dist/commands/template/phase/create.js +10 -1
- package/dist/commands/template/phase/index.js +4 -4
- package/dist/commands/template/ticket/create.d.ts +20 -0
- package/dist/commands/template/ticket/create.js +87 -0
- package/dist/commands/template/ticket/delete.d.ts +1 -1
- package/dist/commands/template/ticket/delete.js +4 -2
- package/dist/commands/template/ticket/save.d.ts +2 -0
- package/dist/commands/template/ticket/save.js +11 -0
- package/dist/commands/ticket/create.js +8 -1
- package/dist/commands/ticket/edit.js +1 -1
- package/dist/commands/ticket/list.d.ts +2 -0
- package/dist/commands/ticket/list.js +39 -2
- package/dist/commands/ticket/template/create.d.ts +9 -1
- package/dist/commands/ticket/template/create.js +224 -52
- package/dist/commands/ticket/template/save.d.ts +2 -1
- package/dist/commands/ticket/template/save.js +58 -7
- package/dist/commands/ticket/update.js +2 -2
- package/dist/commands/work/ready.js +8 -8
- package/dist/commands/work/spawn.js +32 -8
- package/dist/commands/work/watch.js +2 -0
- package/dist/lib/agents/commands.d.ts +7 -0
- package/dist/lib/agents/commands.js +11 -0
- package/dist/lib/agents/index.js +14 -4
- package/dist/lib/branch/index.js +24 -0
- package/dist/lib/execution/config.d.ts +2 -0
- package/dist/lib/execution/config.js +12 -0
- package/dist/lib/execution/runners.js +1 -2
- package/dist/lib/pmo/storage/epics.js +20 -10
- package/dist/lib/pmo/storage/helpers.d.ts +10 -0
- package/dist/lib/pmo/storage/helpers.js +59 -1
- package/dist/lib/pmo/storage/projects.js +20 -8
- package/dist/lib/pmo/storage/specs.js +23 -13
- package/dist/lib/pmo/storage/statuses.js +39 -18
- package/dist/lib/pmo/storage/subtasks.js +19 -8
- package/dist/lib/pmo/storage/tickets.js +27 -15
- package/dist/lib/pmo/utils.d.ts +4 -2
- package/dist/lib/pmo/utils.js +4 -2
- package/oclif.manifest.json +4037 -3234
- package/package.json +2 -4
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { styles } from '../../lib/styles.js';
|
|
6
|
+
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
7
|
+
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
8
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
9
|
+
import { outputErrorAsJson, createMetadata, shouldOutputJson, } from '../../lib/prompt-json.js';
|
|
10
|
+
export default class ExecutionView extends PMOCommand {
|
|
11
|
+
static description = 'View details of a specific execution';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %> WORK-001',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> # Interactive mode',
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
id: Args.string({
|
|
18
|
+
description: 'Execution ID - prompts if not provided',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
...pmoBaseFlags,
|
|
24
|
+
json: Flags.boolean({
|
|
25
|
+
description: 'Output execution details as JSON',
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
getPMOOptions() {
|
|
30
|
+
return { promptIfMultiple: false };
|
|
31
|
+
}
|
|
32
|
+
async execute() {
|
|
33
|
+
const { args, flags } = await this.parse(ExecutionView);
|
|
34
|
+
// Check if JSON output mode is active
|
|
35
|
+
const jsonMode = shouldOutputJson(flags);
|
|
36
|
+
// Helper to handle errors in JSON mode
|
|
37
|
+
const handleError = (code, message) => {
|
|
38
|
+
if (jsonMode) {
|
|
39
|
+
outputErrorAsJson(code, message, createMetadata('execution view', flags));
|
|
40
|
+
this.exit(1);
|
|
41
|
+
}
|
|
42
|
+
this.error(message);
|
|
43
|
+
};
|
|
44
|
+
// Get workspace info
|
|
45
|
+
let workspaceInfo;
|
|
46
|
+
try {
|
|
47
|
+
workspaceInfo = getWorkspaceInfo();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return handleError('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt init" first.');
|
|
51
|
+
}
|
|
52
|
+
// Open database
|
|
53
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
54
|
+
const db = new Database(dbPath);
|
|
55
|
+
const executionStorage = new ExecutionStorage(db);
|
|
56
|
+
try {
|
|
57
|
+
// Get execution ID - prompt if not provided
|
|
58
|
+
let execId = args.id;
|
|
59
|
+
if (!execId) {
|
|
60
|
+
const executions = executionStorage.listExecutions({ limit: 20 });
|
|
61
|
+
if (executions.length === 0) {
|
|
62
|
+
if (jsonMode) {
|
|
63
|
+
outputErrorAsJson('NO_EXECUTIONS', 'No executions found.', createMetadata('execution view', flags));
|
|
64
|
+
db.close();
|
|
65
|
+
this.exit(1);
|
|
66
|
+
}
|
|
67
|
+
this.log(styles.muted('\nNo executions found.\n'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const jsonModeConfig = (flags.json || flags.machine) ? { flags, commandName: 'execution view' } : null;
|
|
71
|
+
const { selectedId } = await this.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'selectedId',
|
|
75
|
+
message: 'Select execution to view:',
|
|
76
|
+
choices: executions.map((e) => ({
|
|
77
|
+
name: `${e.id} - ${e.ticketId} (${e.agentName}, ${e.status})`,
|
|
78
|
+
value: e.id,
|
|
79
|
+
command: `prlt execution view ${e.id} --json`,
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
], jsonModeConfig);
|
|
83
|
+
execId = selectedId;
|
|
84
|
+
}
|
|
85
|
+
// Get execution
|
|
86
|
+
const execution = executionStorage.getExecution(execId);
|
|
87
|
+
if (!execution) {
|
|
88
|
+
return handleError('NOT_FOUND', `Execution "${execId}" not found.`);
|
|
89
|
+
}
|
|
90
|
+
// If JSON mode with ID provided, output the execution data as JSON
|
|
91
|
+
if (jsonMode && args.id) {
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
success: true,
|
|
94
|
+
data: {
|
|
95
|
+
id: execution.id,
|
|
96
|
+
ticketId: execution.ticketId,
|
|
97
|
+
agentName: execution.agentName,
|
|
98
|
+
executor: execution.executor,
|
|
99
|
+
environment: execution.environment,
|
|
100
|
+
displayMode: execution.displayMode,
|
|
101
|
+
sandboxed: execution.sandboxed,
|
|
102
|
+
status: execution.status,
|
|
103
|
+
branch: execution.branch || null,
|
|
104
|
+
pid: execution.pid || null,
|
|
105
|
+
containerId: execution.containerId || null,
|
|
106
|
+
sessionId: execution.sessionId || null,
|
|
107
|
+
host: execution.host || null,
|
|
108
|
+
logPath: execution.logPath || null,
|
|
109
|
+
startedAt: execution.startedAt.toISOString(),
|
|
110
|
+
completedAt: execution.completedAt?.toISOString() || null,
|
|
111
|
+
exitCode: execution.exitCode ?? null,
|
|
112
|
+
},
|
|
113
|
+
metadata: createMetadata('execution view', flags),
|
|
114
|
+
}, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Display execution details
|
|
118
|
+
this.log('');
|
|
119
|
+
this.log(`${styles.header('🚀 Execution')} ${styles.emphasis(execution.id)}`);
|
|
120
|
+
this.log('═'.repeat(60));
|
|
121
|
+
this.log('');
|
|
122
|
+
// Basic info
|
|
123
|
+
this.log(`${styles.header('Ticket:')} ${execution.ticketId}`);
|
|
124
|
+
this.log(`${styles.header('Agent:')} ${execution.agentName}`);
|
|
125
|
+
this.log(`${styles.header('Executor:')} ${execution.executor}`);
|
|
126
|
+
this.log(`${styles.header('Status:')} ${getStatusDisplay(execution.status)}`);
|
|
127
|
+
this.log('');
|
|
128
|
+
// Environment info
|
|
129
|
+
this.log(styles.header('Environment'));
|
|
130
|
+
this.log('─'.repeat(40));
|
|
131
|
+
const envIcon = getEnvironmentIcon(execution.environment);
|
|
132
|
+
this.log(`${styles.muted('Type:')} ${envIcon} ${execution.environment}`);
|
|
133
|
+
this.log(`${styles.muted('Display:')} ${execution.displayMode}`);
|
|
134
|
+
this.log(`${styles.muted('Permissions:')} ${execution.sandboxed ? styles.success('sandboxed (safe)') : styles.warning('unrestricted (danger)')}`);
|
|
135
|
+
if (execution.branch) {
|
|
136
|
+
this.log(`${styles.muted('Branch:')} ${execution.branch}`);
|
|
137
|
+
}
|
|
138
|
+
this.log('');
|
|
139
|
+
// Process info
|
|
140
|
+
if (execution.pid || execution.containerId || execution.sessionId || execution.host) {
|
|
141
|
+
this.log(styles.header('Process Info'));
|
|
142
|
+
this.log('─'.repeat(40));
|
|
143
|
+
if (execution.pid) {
|
|
144
|
+
this.log(`${styles.muted('PID:')} ${execution.pid}`);
|
|
145
|
+
}
|
|
146
|
+
if (execution.containerId) {
|
|
147
|
+
this.log(`${styles.muted('Container:')} ${execution.containerId}`);
|
|
148
|
+
}
|
|
149
|
+
if (execution.sessionId) {
|
|
150
|
+
this.log(`${styles.muted('Session:')} ${execution.sessionId}`);
|
|
151
|
+
}
|
|
152
|
+
if (execution.host) {
|
|
153
|
+
this.log(`${styles.muted('Host:')} ${execution.host}`);
|
|
154
|
+
}
|
|
155
|
+
this.log('');
|
|
156
|
+
}
|
|
157
|
+
// Timing info
|
|
158
|
+
this.log(styles.header('Timing'));
|
|
159
|
+
this.log('─'.repeat(40));
|
|
160
|
+
this.log(`${styles.muted('Started:')} ${execution.startedAt.toLocaleString()} (${formatTimeAgo(execution.startedAt)})`);
|
|
161
|
+
if (execution.completedAt) {
|
|
162
|
+
this.log(`${styles.muted('Completed:')} ${execution.completedAt.toLocaleString()} (${formatTimeAgo(execution.completedAt)})`);
|
|
163
|
+
const duration = execution.completedAt.getTime() - execution.startedAt.getTime();
|
|
164
|
+
this.log(`${styles.muted('Duration:')} ${formatDuration(duration)}`);
|
|
165
|
+
}
|
|
166
|
+
if (execution.exitCode !== undefined) {
|
|
167
|
+
const exitStyle = execution.exitCode === 0 ? styles.success : styles.error;
|
|
168
|
+
this.log(`${styles.muted('Exit Code:')} ${exitStyle(execution.exitCode.toString())}`);
|
|
169
|
+
}
|
|
170
|
+
this.log('');
|
|
171
|
+
// Logs summary
|
|
172
|
+
if (execution.logPath) {
|
|
173
|
+
this.log(styles.header('Logs'));
|
|
174
|
+
this.log('─'.repeat(40));
|
|
175
|
+
this.log(`${styles.muted('Path:')} ${execution.logPath}`);
|
|
176
|
+
if (fs.existsSync(execution.logPath)) {
|
|
177
|
+
const stats = fs.statSync(execution.logPath);
|
|
178
|
+
this.log(`${styles.muted('Size:')} ${formatFileSize(stats.size)}`);
|
|
179
|
+
// Show last few lines of the log
|
|
180
|
+
const content = fs.readFileSync(execution.logPath, 'utf-8');
|
|
181
|
+
const lines = content.trim().split('\n');
|
|
182
|
+
if (lines.length > 0) {
|
|
183
|
+
this.log(`${styles.muted('Lines:')} ${lines.length}`);
|
|
184
|
+
this.log('');
|
|
185
|
+
this.log(styles.muted('Last 5 lines:'));
|
|
186
|
+
const lastLines = lines.slice(-5);
|
|
187
|
+
for (const line of lastLines) {
|
|
188
|
+
// Truncate long lines
|
|
189
|
+
const truncated = line.length > 80 ? line.substring(0, 77) + '...' : line;
|
|
190
|
+
this.log(styles.muted(` ${truncated}`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.log(styles.warning(' Log file not found'));
|
|
196
|
+
}
|
|
197
|
+
this.log('');
|
|
198
|
+
}
|
|
199
|
+
// Commands
|
|
200
|
+
this.log('═'.repeat(60));
|
|
201
|
+
this.log(styles.muted('Commands:'));
|
|
202
|
+
if (execution.logPath) {
|
|
203
|
+
this.log(styles.muted(` prlt execution logs ${execution.id} View full logs`));
|
|
204
|
+
}
|
|
205
|
+
if (['starting', 'running'].includes(execution.status)) {
|
|
206
|
+
this.log(styles.muted(` prlt execution stop ${execution.id} Stop execution`));
|
|
207
|
+
if (execution.sessionId) {
|
|
208
|
+
if (execution.environment === 'devcontainer' && execution.containerId) {
|
|
209
|
+
this.log(styles.muted(` docker exec -it ${execution.containerId} tmux attach -t ${execution.sessionId}`));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
this.log(styles.muted(` tmux attach -t ${execution.sessionId} Attach to session`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.log('');
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
db.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// Helper Functions
|
|
225
|
+
// =============================================================================
|
|
226
|
+
function getStatusDisplay(status) {
|
|
227
|
+
switch (status) {
|
|
228
|
+
case 'running':
|
|
229
|
+
return styles.success('● running');
|
|
230
|
+
case 'starting':
|
|
231
|
+
return styles.warning('◐ starting');
|
|
232
|
+
case 'completed':
|
|
233
|
+
return styles.muted('✓ completed');
|
|
234
|
+
case 'failed':
|
|
235
|
+
return styles.error('✗ failed');
|
|
236
|
+
case 'stopped':
|
|
237
|
+
return styles.muted('■ stopped');
|
|
238
|
+
default:
|
|
239
|
+
return status;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getEnvironmentIcon(environment) {
|
|
243
|
+
switch (environment) {
|
|
244
|
+
case 'devcontainer':
|
|
245
|
+
return '🐳';
|
|
246
|
+
case 'host':
|
|
247
|
+
return '💻';
|
|
248
|
+
case 'docker':
|
|
249
|
+
return '📦';
|
|
250
|
+
case 'vm':
|
|
251
|
+
return '☁️';
|
|
252
|
+
default:
|
|
253
|
+
return '❓';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function formatTimeAgo(date) {
|
|
257
|
+
const now = new Date();
|
|
258
|
+
const diffMs = now.getTime() - date.getTime();
|
|
259
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
260
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
261
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
262
|
+
if (diffMins < 1)
|
|
263
|
+
return 'just now';
|
|
264
|
+
if (diffMins < 60)
|
|
265
|
+
return `${diffMins} min ago`;
|
|
266
|
+
if (diffHours < 24)
|
|
267
|
+
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
268
|
+
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
269
|
+
}
|
|
270
|
+
function formatDuration(ms) {
|
|
271
|
+
const seconds = Math.floor(ms / 1000);
|
|
272
|
+
const minutes = Math.floor(seconds / 60);
|
|
273
|
+
const hours = Math.floor(minutes / 60);
|
|
274
|
+
if (hours > 0) {
|
|
275
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
276
|
+
}
|
|
277
|
+
if (minutes > 0) {
|
|
278
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
279
|
+
}
|
|
280
|
+
return `${seconds}s`;
|
|
281
|
+
}
|
|
282
|
+
function formatFileSize(bytes) {
|
|
283
|
+
if (bytes < 1024)
|
|
284
|
+
return `${bytes} B`;
|
|
285
|
+
if (bytes < 1024 * 1024)
|
|
286
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
287
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
288
|
+
}
|
|
@@ -101,9 +101,17 @@ export default class PhaseMove extends PMOCommand {
|
|
|
101
101
|
}
|
|
102
102
|
newPosition = parseInt(resolved.position, 10);
|
|
103
103
|
}
|
|
104
|
+
// Get phases in same category for validation
|
|
105
|
+
const allPhases = await this.storage.listPhases();
|
|
106
|
+
const categoryPhases = allPhases.filter(p => p.category === phase.category);
|
|
107
|
+
const maxPosition = categoryPhases.length - 1;
|
|
104
108
|
if (newPosition < 0) {
|
|
105
109
|
this.error('Position must be >= 0');
|
|
106
110
|
}
|
|
111
|
+
if (newPosition > maxPosition) {
|
|
112
|
+
this.warn(`Position ${newPosition} exceeds max (${maxPosition}). Clamping to ${maxPosition}.`);
|
|
113
|
+
newPosition = maxPosition;
|
|
114
|
+
}
|
|
107
115
|
const updated = await this.storage.reorderPhase(phaseId, newPosition);
|
|
108
116
|
if (phase.position === updated.position) {
|
|
109
117
|
this.log(styles.muted(`Phase "${updated.name}" is already at position ${updated.position}`));
|
|
@@ -48,7 +48,7 @@ export default class PhaseTemplateApply extends PMOCommand {
|
|
|
48
48
|
if (!templateId) {
|
|
49
49
|
const templates = await this.storage.listPhaseTemplates();
|
|
50
50
|
if (templates.length === 0) {
|
|
51
|
-
return handleError('NO_TEMPLATES', `No phase templates found.\nCreate one with: prlt phase
|
|
51
|
+
return handleError('NO_TEMPLATES', `No phase templates found.\nCreate one with: prlt template phase create "Template Name"`);
|
|
52
52
|
}
|
|
53
53
|
const { selectedTemplate } = await inquirer.prompt([{
|
|
54
54
|
type: 'list',
|
|
@@ -64,7 +64,7 @@ export default class PhaseTemplateApply extends PMOCommand {
|
|
|
64
64
|
// Verify template exists
|
|
65
65
|
const template = await this.storage.getPhaseTemplate(templateId);
|
|
66
66
|
if (!template) {
|
|
67
|
-
return handleError('TEMPLATE_NOT_FOUND', `Phase template not found: ${templateId}. Run 'prlt phase
|
|
67
|
+
return handleError('TEMPLATE_NOT_FOUND', `Phase template not found: ${templateId}. Run 'prlt template phase list' to see available templates.`);
|
|
68
68
|
}
|
|
69
69
|
// Check if workspace has existing phases
|
|
70
70
|
const existingPhases = await this.storage.listPhases();
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Flags, Args } from '@oclif/core';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
2
|
import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
|
|
4
3
|
import { styles } from '../../../lib/styles.js';
|
|
4
|
+
import { shouldOutputJson, outputSuccessAsJson, createMetadata } from '../../../lib/prompt-json.js';
|
|
5
|
+
import { FlagResolver } from '../../../lib/flags/index.js';
|
|
5
6
|
export default class PhaseTemplateCreate extends PMOCommand {
|
|
6
7
|
static description = 'Create a new phase template from current workspace phases';
|
|
7
8
|
static examples = [
|
|
8
9
|
'<%= config.bin %> <%= command.id %> "My Custom Phases"',
|
|
9
10
|
'<%= config.bin %> <%= command.id %> "Enterprise" --description "Enterprise project lifecycle"',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> "My Phases" --description "Custom phases" --json',
|
|
10
12
|
];
|
|
11
13
|
static args = {
|
|
12
14
|
name: Args.string({
|
|
@@ -26,28 +28,73 @@ export default class PhaseTemplateCreate extends PMOCommand {
|
|
|
26
28
|
}
|
|
27
29
|
async execute() {
|
|
28
30
|
const { args, flags } = await this.parse(PhaseTemplateCreate);
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
if
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
// Check if JSON output mode is active
|
|
32
|
+
const jsonMode = shouldOutputJson(flags);
|
|
33
|
+
// Build base command with positional arg if name provided
|
|
34
|
+
const baseCmd = args.name
|
|
35
|
+
? `prlt template phase create "${args.name}"`
|
|
36
|
+
: 'prlt template phase create';
|
|
37
|
+
// Use FlagResolver for unified JSON mode and interactive handling
|
|
38
|
+
const resolver = new FlagResolver({
|
|
39
|
+
commandName: 'phase template create',
|
|
40
|
+
baseCommand: baseCmd,
|
|
41
|
+
jsonMode,
|
|
42
|
+
flags: {
|
|
43
|
+
description: flags.description,
|
|
44
|
+
machine: flags.machine,
|
|
45
|
+
json: flags.json,
|
|
46
|
+
},
|
|
47
|
+
args: { name: args.name },
|
|
48
|
+
});
|
|
49
|
+
// Name prompt - required (only if not provided as positional arg)
|
|
50
|
+
if (!args.name) {
|
|
51
|
+
resolver.addPrompt({
|
|
52
|
+
flagName: 'name',
|
|
53
|
+
type: 'input',
|
|
54
|
+
message: 'Template name:',
|
|
55
|
+
validate: (value) => value.length > 0 || 'Name is required',
|
|
56
|
+
context: {
|
|
57
|
+
hint: 'Provide name with: prlt template phase create "Template Name"',
|
|
58
|
+
example: 'prlt template phase create "My Phases" --description "Custom phases"',
|
|
59
|
+
},
|
|
60
|
+
// For input prompts, the agent will re-run with the positional arg
|
|
61
|
+
getCommand: (value) => `prlt template phase create "${value}" --json`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Description prompt - optional (only in interactive mode without --json)
|
|
65
|
+
if (!jsonMode && args.name && flags.description === undefined) {
|
|
66
|
+
resolver.addPrompt({
|
|
67
|
+
flagName: 'description',
|
|
68
|
+
type: 'input',
|
|
69
|
+
message: 'Description (optional):',
|
|
70
|
+
});
|
|
39
71
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}]);
|
|
48
|
-
description = desc || undefined;
|
|
72
|
+
// Resolve missing flags
|
|
73
|
+
const resolved = await resolver.resolve();
|
|
74
|
+
// Get name from args or resolved (for interactive mode)
|
|
75
|
+
const templateName = args.name || resolved.name;
|
|
76
|
+
// Validate required fields
|
|
77
|
+
if (!templateName) {
|
|
78
|
+
this.error('Name is required. Provide as positional argument: prlt template phase create "Template Name"');
|
|
49
79
|
}
|
|
80
|
+
// Get description from flags or resolved
|
|
81
|
+
const description = flags.description ?? resolved.description ?? undefined;
|
|
50
82
|
const template = await this.storage.savePhaseTemplate(templateName, description);
|
|
83
|
+
// Output as JSON in machine mode
|
|
84
|
+
if (jsonMode) {
|
|
85
|
+
outputSuccessAsJson({
|
|
86
|
+
id: template.id,
|
|
87
|
+
name: template.name,
|
|
88
|
+
description: template.description,
|
|
89
|
+
phasesCount: template.phases.length,
|
|
90
|
+
phases: template.phases.map(p => ({
|
|
91
|
+
name: p.name,
|
|
92
|
+
category: p.category,
|
|
93
|
+
isDefault: p.isDefault,
|
|
94
|
+
})),
|
|
95
|
+
}, createMetadata('phase template create', flags));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
51
98
|
this.log(styles.success(`\nCreated phase template "${styles.emphasis(template.name)}" (${template.id})`));
|
|
52
99
|
this.log(styles.muted(`Saved ${template.phases.length} phases:`));
|
|
53
100
|
for (const phase of template.phases) {
|
|
@@ -62,7 +62,7 @@ export default class PhaseTemplateList extends PMOCommand {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
this.log('');
|
|
65
|
-
this.log(styles.muted('Apply a template: prlt phase
|
|
65
|
+
this.log(styles.muted('Apply a template: prlt template phase apply <template-id>'));
|
|
66
66
|
this.log('');
|
|
67
67
|
}
|
|
68
68
|
printTemplate(template) {
|
|
@@ -11,8 +11,8 @@ export default class PR extends PMOCommand {
|
|
|
11
11
|
...pmoBaseFlags,
|
|
12
12
|
action: Flags.string({
|
|
13
13
|
char: 'a',
|
|
14
|
-
description: 'Action to perform (create, link, status)',
|
|
15
|
-
options: ['create', 'link', 'status'],
|
|
14
|
+
description: 'Action to perform (list, create, link, status)',
|
|
15
|
+
options: ['list', 'create', 'link', 'status'],
|
|
16
16
|
}),
|
|
17
17
|
};
|
|
18
18
|
async execute() {
|
|
@@ -43,6 +43,7 @@ export default class PR extends PMOCommand {
|
|
|
43
43
|
type: 'list',
|
|
44
44
|
message: 'Pull Request Operations - What would you like to do?',
|
|
45
45
|
choices: () => [
|
|
46
|
+
{ name: 'List all open PRs', value: 'list' },
|
|
46
47
|
{ name: 'Create PR from current branch', value: 'create' },
|
|
47
48
|
{ name: 'Link existing PR to ticket', value: 'link' },
|
|
48
49
|
{ name: 'View PR status for ticket', value: 'status' },
|
|
@@ -56,6 +57,9 @@ export default class PR extends PMOCommand {
|
|
|
56
57
|
}
|
|
57
58
|
// Run the selected subcommand
|
|
58
59
|
switch (resolved.action) {
|
|
60
|
+
case 'list':
|
|
61
|
+
await this.config.runCommand('pr:list', []);
|
|
62
|
+
break;
|
|
59
63
|
case 'create':
|
|
60
64
|
await this.config.runCommand('pr:create', []);
|
|
61
65
|
break;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class PRList extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
state: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
execute(): Promise<void>;
|
|
14
|
+
private outputTable;
|
|
15
|
+
private outputCompact;
|
|
16
|
+
private getStateEmoji;
|
|
17
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { PMOCommand, pmoBaseFlags, } from '../../lib/pmo/index.js';
|
|
3
|
+
import { styles, divider } from '../../lib/styles.js';
|
|
4
|
+
import { isGHInstalled, isGHAuthenticated, listOpenPRs, } from '../../lib/pr/index.js';
|
|
5
|
+
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
|
+
export default class PRList extends PMOCommand {
|
|
7
|
+
static description = 'List pull requests linked to tickets in the workspace';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --state open',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --state draft',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --format json',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --machine',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...pmoBaseFlags,
|
|
17
|
+
state: Flags.string({
|
|
18
|
+
char: 's',
|
|
19
|
+
description: 'Filter by PR state',
|
|
20
|
+
options: ['open', 'draft', 'all'],
|
|
21
|
+
default: 'open',
|
|
22
|
+
}),
|
|
23
|
+
format: Flags.string({
|
|
24
|
+
char: 'f',
|
|
25
|
+
description: 'Output format',
|
|
26
|
+
options: ['table', 'compact', 'json'],
|
|
27
|
+
default: 'table',
|
|
28
|
+
}),
|
|
29
|
+
limit: Flags.integer({
|
|
30
|
+
char: 'l',
|
|
31
|
+
description: 'Maximum number of PRs to show',
|
|
32
|
+
default: 50,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
async execute() {
|
|
36
|
+
const { flags } = await this.parse(PRList);
|
|
37
|
+
// Check if JSON output mode is active
|
|
38
|
+
const jsonMode = shouldOutputJson(flags);
|
|
39
|
+
// Helper to handle errors in JSON mode
|
|
40
|
+
const handleError = (code, message) => {
|
|
41
|
+
if (jsonMode) {
|
|
42
|
+
outputErrorAsJson(code, message, createMetadata('pr list', flags));
|
|
43
|
+
this.exit(1);
|
|
44
|
+
}
|
|
45
|
+
this.error(message);
|
|
46
|
+
};
|
|
47
|
+
// PMOCommand base class ensures PMO context is available
|
|
48
|
+
if (!this.storage) {
|
|
49
|
+
return handleError('PMO_NOT_FOUND', 'PMO not found. Run "prlt pmo init" first.');
|
|
50
|
+
}
|
|
51
|
+
// Check gh CLI
|
|
52
|
+
if (!isGHInstalled()) {
|
|
53
|
+
return handleError('GH_NOT_INSTALLED', 'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/');
|
|
54
|
+
}
|
|
55
|
+
if (!isGHAuthenticated()) {
|
|
56
|
+
return handleError('GH_NOT_AUTHENTICATED', 'GitHub CLI is not authenticated. Run "gh auth login" first.');
|
|
57
|
+
}
|
|
58
|
+
// Get all tickets with linked PRs from database
|
|
59
|
+
const allTickets = await this.storage.listTickets(flags.project);
|
|
60
|
+
const ticketsWithPR = allTickets.filter(t => t.metadata?.pr_url || t.metadata?.pr_number);
|
|
61
|
+
// Build a map of PR number -> ticket info for quick lookup
|
|
62
|
+
const prToTicketMap = new Map();
|
|
63
|
+
for (const ticket of ticketsWithPR) {
|
|
64
|
+
const prNumber = parseInt(ticket.metadata?.pr_number || '0', 10);
|
|
65
|
+
if (prNumber > 0) {
|
|
66
|
+
prToTicketMap.set(prNumber, {
|
|
67
|
+
id: ticket.id,
|
|
68
|
+
title: ticket.title,
|
|
69
|
+
status: ticket.statusName || 'Unknown',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Fetch open PRs from GitHub
|
|
74
|
+
let prs = listOpenPRs();
|
|
75
|
+
// Apply state filter
|
|
76
|
+
if (flags.state === 'draft') {
|
|
77
|
+
prs = prs.filter(pr => pr.isDraft);
|
|
78
|
+
}
|
|
79
|
+
else if (flags.state !== 'all') {
|
|
80
|
+
// 'open' is default - listOpenPRs already returns open PRs
|
|
81
|
+
// but filter out drafts if we only want non-draft open PRs
|
|
82
|
+
// Actually, keep drafts in 'open' view as they are still open
|
|
83
|
+
}
|
|
84
|
+
// Apply limit
|
|
85
|
+
if (flags.limit && prs.length > flags.limit) {
|
|
86
|
+
prs = prs.slice(0, flags.limit);
|
|
87
|
+
}
|
|
88
|
+
// Enrich PRs with ticket info
|
|
89
|
+
const enrichedPRs = prs.map(pr => {
|
|
90
|
+
const ticketInfo = prToTicketMap.get(pr.number);
|
|
91
|
+
return {
|
|
92
|
+
...pr,
|
|
93
|
+
ticketId: ticketInfo?.id,
|
|
94
|
+
ticketTitle: ticketInfo?.title,
|
|
95
|
+
ticketStatus: ticketInfo?.status,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
// Output based on format
|
|
99
|
+
if (jsonMode || flags.format === 'json') {
|
|
100
|
+
this.log(JSON.stringify(enrichedPRs, null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (enrichedPRs.length === 0) {
|
|
104
|
+
this.log(styles.info('No open pull requests found.'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (flags.format === 'compact') {
|
|
108
|
+
this.outputCompact(enrichedPRs);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.outputTable(enrichedPRs);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
outputTable(prs) {
|
|
115
|
+
this.log('');
|
|
116
|
+
this.log(styles.header(`Pull Requests (${prs.length})`));
|
|
117
|
+
this.log(divider(80));
|
|
118
|
+
for (const pr of prs) {
|
|
119
|
+
const stateEmoji = this.getStateEmoji(pr);
|
|
120
|
+
const draftBadge = pr.isDraft ? styles.muted(' [Draft]') : '';
|
|
121
|
+
this.log(`${stateEmoji} ${styles.emphasis(`#${pr.number}`)} ${pr.title}${draftBadge}`);
|
|
122
|
+
this.log(styles.muted(` Branch: ${pr.headBranch} → ${pr.baseBranch}`));
|
|
123
|
+
this.log(styles.muted(` URL: ${pr.url}`));
|
|
124
|
+
if (pr.ticketId) {
|
|
125
|
+
this.log(styles.info(` Ticket: ${pr.ticketId} - ${pr.ticketTitle} [${pr.ticketStatus}]`));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
this.log(styles.muted(` Ticket: (not linked)`));
|
|
129
|
+
}
|
|
130
|
+
const created = new Date(pr.createdAt).toLocaleDateString();
|
|
131
|
+
const updated = new Date(pr.updatedAt).toLocaleDateString();
|
|
132
|
+
this.log(styles.muted(` Created: ${created} | Updated: ${updated}`));
|
|
133
|
+
this.log('');
|
|
134
|
+
}
|
|
135
|
+
// Summary
|
|
136
|
+
this.log(divider(80));
|
|
137
|
+
const linkedCount = prs.filter(pr => pr.ticketId).length;
|
|
138
|
+
const draftCount = prs.filter(pr => pr.isDraft).length;
|
|
139
|
+
this.log(styles.muted(`Total: ${prs.length} PR${prs.length === 1 ? '' : 's'} | Linked: ${linkedCount} | Drafts: ${draftCount}`));
|
|
140
|
+
}
|
|
141
|
+
outputCompact(prs) {
|
|
142
|
+
this.log('');
|
|
143
|
+
this.log(styles.header(`Pull Requests (${prs.length})`));
|
|
144
|
+
this.log(divider(60));
|
|
145
|
+
for (const pr of prs) {
|
|
146
|
+
const stateEmoji = this.getStateEmoji(pr);
|
|
147
|
+
const ticketBadge = pr.ticketId ? styles.code(`[${pr.ticketId}]`) : '';
|
|
148
|
+
const draftBadge = pr.isDraft ? styles.muted('[Draft]') : '';
|
|
149
|
+
this.log(`${stateEmoji} #${pr.number}: ${pr.title} ${ticketBadge} ${draftBadge}`);
|
|
150
|
+
}
|
|
151
|
+
this.log('');
|
|
152
|
+
}
|
|
153
|
+
getStateEmoji(pr) {
|
|
154
|
+
if (pr.isDraft)
|
|
155
|
+
return '📝';
|
|
156
|
+
switch (pr.state) {
|
|
157
|
+
case 'OPEN': return '🟢';
|
|
158
|
+
case 'CLOSED': return '🔴';
|
|
159
|
+
case 'MERGED': return '🟣';
|
|
160
|
+
default: return '⚪';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|