@proletariat/cli 0.3.18 → 0.3.20
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/LICENSE +21 -0
- package/bin/dev.js +0 -0
- package/dist/commands/agent/staff/remove.d.ts +1 -0
- package/dist/commands/agent/staff/remove.js +34 -26
- package/dist/commands/agent/temp/cleanup.js +10 -17
- 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/execution/config.d.ts +34 -0
- package/dist/commands/execution/config.js +411 -0
- package/dist/commands/execution/index.js +6 -1
- package/dist/commands/execution/kill.d.ts +9 -0
- package/dist/commands/execution/kill.js +16 -0
- package/dist/commands/execution/view.d.ts +17 -0
- package/dist/commands/execution/view.js +288 -0
- package/dist/commands/phase/template/create.js +67 -20
- 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/template/phase/create.d.ts +1 -0
- package/dist/commands/template/phase/create.js +10 -1
- package/dist/commands/template/ticket/create.d.ts +20 -0
- package/dist/commands/template/ticket/create.js +87 -0
- 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 +7 -0
- 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/work/ready.js +8 -8
- 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/pmo/utils.d.ts +4 -2
- package/dist/lib/pmo/utils.js +4 -2
- package/oclif.manifest.json +2555 -1781
- package/package.json +5 -6
|
@@ -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
|
+
}
|
|
@@ -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 phase template create "${args.name}"`
|
|
36
|
+
: 'prlt phase template 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 phase template create "Template Name"',
|
|
58
|
+
example: 'prlt phase template create "My Phases" --description "Custom phases"',
|
|
59
|
+
},
|
|
60
|
+
// For input prompts, the agent will re-run with the positional arg
|
|
61
|
+
getCommand: (value) => `prlt phase template 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 phase template 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) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class ProjectUpdate extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
protected getPMOOptions(): {
|
|
16
|
+
promptIfMultiple: boolean;
|
|
17
|
+
};
|
|
18
|
+
execute(): Promise<void>;
|
|
19
|
+
}
|