@proletariat/cli 0.3.19 → 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.
Files changed (46) hide show
  1. package/dist/commands/agent/staff/remove.d.ts +1 -0
  2. package/dist/commands/agent/staff/remove.js +34 -26
  3. package/dist/commands/agent/temp/cleanup.js +10 -17
  4. package/dist/commands/board/view.d.ts +15 -0
  5. package/dist/commands/board/view.js +136 -0
  6. package/dist/commands/config/index.js +6 -3
  7. package/dist/commands/execution/config.d.ts +34 -0
  8. package/dist/commands/execution/config.js +411 -0
  9. package/dist/commands/execution/index.js +6 -1
  10. package/dist/commands/execution/kill.d.ts +9 -0
  11. package/dist/commands/execution/kill.js +16 -0
  12. package/dist/commands/execution/view.d.ts +17 -0
  13. package/dist/commands/execution/view.js +288 -0
  14. package/dist/commands/phase/template/create.js +67 -20
  15. package/dist/commands/pr/index.js +6 -2
  16. package/dist/commands/pr/list.d.ts +17 -0
  17. package/dist/commands/pr/list.js +163 -0
  18. package/dist/commands/project/update.d.ts +19 -0
  19. package/dist/commands/project/update.js +163 -0
  20. package/dist/commands/roadmap/create.js +5 -0
  21. package/dist/commands/spec/delete.d.ts +18 -0
  22. package/dist/commands/spec/delete.js +111 -0
  23. package/dist/commands/spec/edit.d.ts +23 -0
  24. package/dist/commands/spec/edit.js +232 -0
  25. package/dist/commands/spec/index.js +5 -0
  26. package/dist/commands/status/create.js +38 -34
  27. package/dist/commands/template/phase/create.d.ts +1 -0
  28. package/dist/commands/template/phase/create.js +10 -1
  29. package/dist/commands/template/ticket/create.d.ts +20 -0
  30. package/dist/commands/template/ticket/create.js +87 -0
  31. package/dist/commands/template/ticket/save.d.ts +2 -0
  32. package/dist/commands/template/ticket/save.js +11 -0
  33. package/dist/commands/ticket/create.js +7 -0
  34. package/dist/commands/ticket/template/create.d.ts +9 -1
  35. package/dist/commands/ticket/template/create.js +224 -52
  36. package/dist/commands/ticket/template/save.d.ts +2 -1
  37. package/dist/commands/ticket/template/save.js +58 -7
  38. package/dist/commands/work/ready.js +8 -8
  39. package/dist/lib/agents/index.js +14 -4
  40. package/dist/lib/branch/index.js +24 -0
  41. package/dist/lib/execution/config.d.ts +2 -0
  42. package/dist/lib/execution/config.js +12 -0
  43. package/dist/lib/pmo/utils.d.ts +4 -2
  44. package/dist/lib/pmo/utils.js +4 -2
  45. package/oclif.manifest.json +3017 -2243
  46. 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
+ }
@@ -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
- // Get template name - prompt if not provided
30
- let templateName = args.name;
31
- if (!templateName) {
32
- const { name } = await inquirer.prompt([{
33
- type: 'input',
34
- name: 'name',
35
- message: 'Template name:',
36
- validate: (input) => input.length > 0 || 'Name is required',
37
- }]);
38
- templateName = name;
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
- // Get description if not provided
41
- let description = flags.description;
42
- if (description === undefined) {
43
- const { desc } = await inquirer.prompt([{
44
- type: 'input',
45
- name: 'desc',
46
- message: 'Description (optional):',
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
+ }