@proletariat/cli 0.3.26 → 0.3.28

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 (76) hide show
  1. package/bin/dev.js +7 -0
  2. package/bin/run.js +7 -0
  3. package/dist/commands/action/show.js +7 -1
  4. package/dist/commands/agent/shell.js +24 -10
  5. package/dist/commands/branch/list.js +14 -11
  6. package/dist/commands/branch/validate.js +10 -1
  7. package/dist/commands/claude.js +12 -40
  8. package/dist/commands/docker/clean.js +7 -9
  9. package/dist/commands/docker/index.js +5 -4
  10. package/dist/commands/docker/list.d.ts +1 -0
  11. package/dist/commands/docker/list.js +31 -17
  12. package/dist/commands/docker/status.d.ts +3 -1
  13. package/dist/commands/docker/status.js +28 -2
  14. package/dist/commands/docker/sync.js +7 -6
  15. package/dist/commands/epic/list.js +17 -2
  16. package/dist/commands/execution/list.js +25 -17
  17. package/dist/commands/pmo/init.js +22 -3
  18. package/dist/commands/repo/list.js +14 -8
  19. package/dist/commands/repo/view.js +2 -1
  20. package/dist/commands/roadmap/list.js +16 -1
  21. package/dist/commands/session/health.js +11 -10
  22. package/dist/commands/session/list.js +15 -8
  23. package/dist/commands/staff/list.d.ts +3 -1
  24. package/dist/commands/staff/list.js +15 -1
  25. package/dist/commands/theme/list.d.ts +3 -0
  26. package/dist/commands/theme/list.js +25 -0
  27. package/dist/commands/ticket/complete.js +4 -1
  28. package/dist/commands/ticket/create.d.ts +1 -0
  29. package/dist/commands/ticket/create.js +30 -0
  30. package/dist/commands/ticket/delete.js +3 -3
  31. package/dist/commands/ticket/edit.js +2 -2
  32. package/dist/commands/ticket/list.js +24 -5
  33. package/dist/commands/ticket/move.js +4 -1
  34. package/dist/commands/ticket/view.js +4 -2
  35. package/dist/commands/whoami.d.ts +3 -0
  36. package/dist/commands/whoami.js +22 -5
  37. package/dist/commands/work/complete.js +2 -2
  38. package/dist/commands/work/ready.js +2 -2
  39. package/dist/commands/work/revise.js +2 -2
  40. package/dist/commands/work/spawn.js +6 -21
  41. package/dist/commands/work/start.js +10 -25
  42. package/dist/commands/work/watch.js +57 -33
  43. package/dist/commands/workspace/prune.d.ts +3 -2
  44. package/dist/commands/workspace/prune.js +70 -10
  45. package/dist/lib/agents/commands.js +4 -0
  46. package/dist/lib/agents/index.js +12 -0
  47. package/dist/lib/execution/devcontainer.d.ts +4 -0
  48. package/dist/lib/execution/devcontainer.js +88 -3
  49. package/dist/lib/mcp/helpers.d.ts +15 -0
  50. package/dist/lib/mcp/helpers.js +15 -0
  51. package/dist/lib/mcp/tools/action.js +5 -5
  52. package/dist/lib/mcp/tools/board.js +7 -7
  53. package/dist/lib/mcp/tools/category.js +5 -5
  54. package/dist/lib/mcp/tools/cli-passthrough.js +30 -30
  55. package/dist/lib/mcp/tools/epic.js +8 -8
  56. package/dist/lib/mcp/tools/phase.js +7 -7
  57. package/dist/lib/mcp/tools/project.js +10 -10
  58. package/dist/lib/mcp/tools/roadmap.js +7 -7
  59. package/dist/lib/mcp/tools/spec.js +9 -9
  60. package/dist/lib/mcp/tools/status.js +6 -6
  61. package/dist/lib/mcp/tools/template.js +6 -6
  62. package/dist/lib/mcp/tools/ticket.js +19 -19
  63. package/dist/lib/mcp/tools/view.js +4 -4
  64. package/dist/lib/mcp/tools/work.js +6 -6
  65. package/dist/lib/mcp/tools/workflow.js +5 -5
  66. package/dist/lib/pmo/index.js +4 -0
  67. package/dist/lib/pmo/storage/base.js +49 -0
  68. package/dist/lib/pmo/types.d.ts +1 -1
  69. package/dist/lib/pmo/types.js +1 -0
  70. package/dist/lib/pr/index.d.ts +5 -0
  71. package/dist/lib/pr/index.js +69 -0
  72. package/dist/lib/repos/index.js +4 -0
  73. package/dist/lib/string-utils.d.ts +10 -0
  74. package/dist/lib/string-utils.js +16 -0
  75. package/oclif.manifest.json +3331 -3253
  76. package/package.json +3 -2
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
5
  // Progress bar helper
5
6
  function progressBar(percent, width = 20) {
6
7
  const filled = Math.round((percent / 100) * width);
@@ -23,15 +24,18 @@ export default class EpicList extends PMOCommand {
23
24
  };
24
25
  async execute() {
25
26
  const { flags } = await this.parse(EpicList);
27
+ const jsonMode = shouldOutputJson(flags);
26
28
  const projectId = await this.requireProject();
27
29
  const epics = await this.storage.listEpics(projectId, flags.status ? { status: flags.status } : undefined);
28
30
  if (epics.length === 0) {
31
+ if (jsonMode) {
32
+ this.log(JSON.stringify([], null, 2));
33
+ return;
34
+ }
29
35
  this.log(styles.muted('\nNo epics found.'));
30
36
  this.log(styles.muted('Create one with: prlt epic create'));
31
37
  return;
32
38
  }
33
- // Group epics by status
34
- const grouped = this.groupByStatus(epics);
35
39
  // Get ticket counts for each epic in parallel
36
40
  const ticketCounts = await Promise.all(epics.map(async (epic) => {
37
41
  const tickets = await this.storage.getTicketsForEpic(projectId, epic.id);
@@ -39,6 +43,17 @@ export default class EpicList extends PMOCommand {
39
43
  return { epicId: epic.id, done, total: tickets.length };
40
44
  }));
41
45
  const epicProgress = new Map(ticketCounts.map(({ epicId, done, total }) => [epicId, { done, total }]));
46
+ if (jsonMode) {
47
+ const epicsWithProgress = epics.map(epic => {
48
+ const progress = epicProgress.get(epic.id) || { done: 0, total: 0 };
49
+ const percent = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
50
+ return { ...epic, progress: { ...progress, percent } };
51
+ });
52
+ this.log(JSON.stringify(epicsWithProgress, null, 2));
53
+ return;
54
+ }
55
+ // Group epics by status
56
+ const grouped = this.groupByStatus(epics);
42
57
  const projectName = await this.getProjectName(projectId);
43
58
  this.log(`\n🎯 ${styles.emphasis('Epics')} - ${projectName}`);
44
59
  this.log('═'.repeat(55));
@@ -5,6 +5,8 @@ import { styles } from '../../lib/styles.js';
5
5
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
6
6
  import { ExecutionStorage } from '../../lib/execution/storage.js';
7
7
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
8
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
9
+ import { visualPadEnd } from '../../lib/string-utils.js';
8
10
  export default class ExecutionList extends PMOCommand {
9
11
  static description = 'List running and recent executions';
10
12
  static examples = [
@@ -36,6 +38,7 @@ export default class ExecutionList extends PMOCommand {
36
38
  }
37
39
  async execute() {
38
40
  const { flags } = await this.parse(ExecutionList);
41
+ const jsonMode = shouldOutputJson(flags);
39
42
  // Get workspace info
40
43
  let workspaceInfo;
41
44
  try {
@@ -55,20 +58,28 @@ export default class ExecutionList extends PMOCommand {
55
58
  limit: flags.limit,
56
59
  });
57
60
  if (executions.length === 0) {
61
+ if (jsonMode) {
62
+ this.log(JSON.stringify([], null, 2));
63
+ return;
64
+ }
58
65
  this.log(styles.muted('\nNo executions found.\n'));
59
66
  return;
60
67
  }
68
+ if (jsonMode) {
69
+ this.log(JSON.stringify(executions, null, 2));
70
+ return;
71
+ }
61
72
  // Display header
62
73
  this.log('');
63
74
  this.log(styles.header('🚀 Agent Work'));
64
75
  this.log('═'.repeat(100));
65
- this.log(styles.muted(padEnd('ID', 14) +
66
- padEnd('Ticket', 9) +
67
- padEnd('Agent', 10) +
68
- padEnd('Env', 13) +
69
- padEnd('Display', 11) +
70
- padEnd('Perms', 8) +
71
- padEnd('Status', 10) +
76
+ this.log(styles.muted(visualPadEnd('ID', 14) +
77
+ visualPadEnd('Ticket', 9) +
78
+ visualPadEnd('Agent', 10) +
79
+ visualPadEnd('Env', 13) +
80
+ visualPadEnd('Display', 11) +
81
+ visualPadEnd('Perms', 8) +
82
+ visualPadEnd('Status', 10) +
72
83
  'Started'));
73
84
  this.log('─'.repeat(100));
74
85
  // Display executions
@@ -79,13 +90,13 @@ export default class ExecutionList extends PMOCommand {
79
90
  const envStr = `${envIcon} ${exec.environment}`;
80
91
  const permsStr = exec.sandboxed ? 'safe' : 'danger';
81
92
  const permsColor = exec.sandboxed ? styles.success : styles.warning;
82
- this.log(padEnd(exec.id, 14) +
83
- padEnd(exec.ticketId, 9) +
84
- padEnd(exec.agentName, 10) +
85
- padEnd(envStr, 13) +
86
- padEnd(exec.displayMode, 11) +
87
- permsColor(padEnd(permsStr, 8)) +
88
- statusColor(padEnd(exec.status, 10)) +
93
+ this.log(visualPadEnd(exec.id, 14) +
94
+ visualPadEnd(exec.ticketId, 9) +
95
+ visualPadEnd(exec.agentName, 10) +
96
+ visualPadEnd(envStr, 13) +
97
+ visualPadEnd(exec.displayMode, 11) +
98
+ permsColor(visualPadEnd(permsStr, 8)) +
99
+ statusColor(visualPadEnd(exec.status, 10)) +
89
100
  styles.muted(timeAgo));
90
101
  }
91
102
  this.log('═'.repeat(100));
@@ -110,9 +121,6 @@ export default class ExecutionList extends PMOCommand {
110
121
  // =============================================================================
111
122
  // Helper Functions
112
123
  // =============================================================================
113
- function padEnd(str, length) {
114
- return str.padEnd(length);
115
- }
116
124
  function getStatusColor(status) {
117
125
  switch (status) {
118
126
  case 'running':
@@ -102,6 +102,9 @@ export default class PMOInit extends PromptCommand {
102
102
  if (flags.location) {
103
103
  location = flags.location;
104
104
  }
105
+ else if (jsonMode) {
106
+ location = 'separate';
107
+ }
105
108
  else {
106
109
  location = await promptForPMOLocation(hqRoot);
107
110
  }
@@ -111,6 +114,9 @@ export default class PMOInit extends PromptCommand {
111
114
  if (flags.template) {
112
115
  template = flags.template;
113
116
  }
117
+ else if (jsonMode) {
118
+ template = 'kanban';
119
+ }
114
120
  else {
115
121
  let storage;
116
122
  if (hqRoot) {
@@ -131,14 +137,27 @@ export default class PMOInit extends PromptCommand {
131
137
  }
132
138
  // Get columns for template
133
139
  let columns = getColumnsForTemplate(template);
134
- if (template === 'custom') {
140
+ if (template === 'custom' && !jsonMode) {
135
141
  columns = await promptForCustomColumns();
136
142
  }
143
+ else if (template === 'custom' && jsonMode) {
144
+ // Custom column prompts not supported in JSON mode — use default columns
145
+ this.log(JSON.stringify({ warning: 'Custom columns not supported in JSON mode, using default columns. Use a named template (e.g. --template kanban) for predictable results.' }));
146
+ }
137
147
  // Get board name using shared prompt (or from flag)
138
148
  // Default to {hqname}-kanban pattern
139
149
  const hqName = hqRoot ? path.basename(hqRoot).replace(/-hq$/, '') : undefined;
140
- const defaultBoardName = hqName ? `${hqName}-kanban` : undefined;
141
- const boardName = flags.name || await promptForBoardName(defaultBoardName);
150
+ const defaultBoardName = hqName ? `${hqName}-kanban` : 'Project Board';
151
+ let boardName;
152
+ if (flags.name) {
153
+ boardName = flags.name;
154
+ }
155
+ else if (jsonMode) {
156
+ boardName = defaultBoardName;
157
+ }
158
+ else {
159
+ boardName = await promptForBoardName(defaultBoardName);
160
+ }
142
161
  // For standalone PMO (no HQ), we need to create mini-HQ structure first
143
162
  const isStandalone = !hqRoot;
144
163
  let effectiveHqPath;
@@ -2,6 +2,8 @@ import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { colors, format } from '../../lib/colors.js';
4
4
  import { findHQRoot, getWorkspaceRepoInfo } from '../../lib/repos/index.js';
5
+ import { isNonTTY } from '../../lib/prompt-json.js';
6
+ import { visualPadEnd } from '../../lib/string-utils.js';
5
7
  export default class List extends PMOCommand {
6
8
  static description = 'List all repositories in the HQ';
7
9
  static examples = [
@@ -23,6 +25,10 @@ export default class List extends PMOCommand {
23
25
  }
24
26
  async execute() {
25
27
  const { flags } = await this.parse(List);
28
+ // Default format to 'json' in non-TTY environments (piped output, CI, agents)
29
+ if (flags.format === 'table' && isNonTTY()) {
30
+ flags.format = 'json';
31
+ }
26
32
  // Find HQ root
27
33
  const hqPath = findHQRoot();
28
34
  if (!hqPath) {
@@ -53,10 +59,10 @@ export default class List extends PMOCommand {
53
59
  this.log(format.title(`📦 Repositories (${repositories.length})`));
54
60
  this.log('');
55
61
  // Header
56
- this.log(colors.textMuted('Name'.padEnd(20) +
57
- 'Status'.padEnd(10) +
58
- 'Branch'.padEnd(15) +
59
- 'Commits'.padEnd(12) +
62
+ this.log(colors.textMuted(visualPadEnd('Name', 20) +
63
+ visualPadEnd('Status', 10) +
64
+ visualPadEnd('Branch', 15) +
65
+ visualPadEnd('Commits', 12) +
60
66
  'Added'));
61
67
  this.log(colors.textMuted('─'.repeat(70)));
62
68
  for (const repo of repositories) {
@@ -74,10 +80,10 @@ export default class List extends PMOCommand {
74
80
  commits = `${repo.commitsBehind} behind`;
75
81
  }
76
82
  const added = repo.addedAt ? new Date(repo.addedAt).toLocaleDateString() : '-';
77
- this.log(colors.text(repo.name.padEnd(20)) +
78
- statusColor(repo.status.padEnd(10)) +
79
- colors.warning((repo.branch || '-').padEnd(15)) +
80
- colors.text(commits.padEnd(12)) +
83
+ this.log(colors.text(visualPadEnd(repo.name, 20)) +
84
+ statusColor(visualPadEnd(repo.status, 10)) +
85
+ colors.warning(visualPadEnd(repo.branch || '-', 15)) +
86
+ colors.text(visualPadEnd(commits, 12)) +
81
87
  colors.textMuted(added));
82
88
  }
83
89
  // Summary
@@ -4,6 +4,7 @@ import { colors, format } from '../../lib/colors.js';
4
4
  import { findHQRoot, getWorkspaceRepoInfo, } from '../../lib/repos/index.js';
5
5
  import { openWorkspaceDatabase } from '../../lib/database/index.js';
6
6
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
7
+ import { visualPadEnd } from '../../lib/string-utils.js';
7
8
  export default class View extends PMOCommand {
8
9
  static description = 'View detailed information about a repository';
9
10
  static examples = [
@@ -113,7 +114,7 @@ export default class View extends PMOCommand {
113
114
  for (const wt of worktrees) {
114
115
  const wtStatus = wt.is_clean ? 'clean' : 'dirty';
115
116
  const wtColor = wt.is_clean ? colors.repoClean : colors.repoDirty;
116
- let line = ` • ${colors.agentName(wt.agent_name.padEnd(10))} - ${wtColor(wtStatus)}`;
117
+ let line = ` • ${colors.agentName(visualPadEnd(wt.agent_name, 10))} - ${wtColor(wtStatus)}`;
117
118
  if (wt.commits_ahead > 0) {
118
119
  line += colors.commitsAhead(` (${wt.commits_ahead} ahead)`);
119
120
  }
@@ -1,5 +1,6 @@
1
1
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
2
2
  import { styles } from '../../lib/styles.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
3
4
  export default class RoadmapList extends PMOCommand {
4
5
  static description = 'List all roadmaps';
5
6
  static examples = [
@@ -12,17 +13,31 @@ export default class RoadmapList extends PMOCommand {
12
13
  return { promptIfMultiple: false };
13
14
  }
14
15
  async execute() {
16
+ const { flags } = await this.parse(RoadmapList);
17
+ const jsonMode = shouldOutputJson(flags);
15
18
  const roadmaps = await this.storage.listRoadmaps();
16
19
  if (roadmaps.length === 0) {
20
+ if (jsonMode) {
21
+ this.log(JSON.stringify([], null, 2));
22
+ return;
23
+ }
17
24
  this.log(styles.muted('No roadmaps found. Create one with: prlt roadmap create'));
18
25
  return;
19
26
  }
20
- this.log(styles.title('\nRoadmaps\n'));
21
27
  // Fetch all project counts in parallel
22
28
  const projectCounts = await Promise.all(roadmaps.map(async (roadmap) => {
23
29
  const projects = await this.storage.listRoadmapProjects(roadmap.id);
24
30
  return projects.length;
25
31
  }));
32
+ if (jsonMode) {
33
+ const roadmapsWithCounts = roadmaps.map((roadmap, i) => ({
34
+ ...roadmap,
35
+ projectCount: projectCounts[i],
36
+ }));
37
+ this.log(JSON.stringify(roadmapsWithCounts, null, 2));
38
+ return;
39
+ }
40
+ this.log(styles.title('\nRoadmaps\n'));
26
41
  for (let i = 0; i < roadmaps.length; i++) {
27
42
  const roadmap = roadmaps[i];
28
43
  const markers = [];
@@ -7,6 +7,7 @@ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
7
  import { ExecutionStorage } from '../../lib/execution/index.js';
8
8
  import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
9
9
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
10
+ import { visualPadEnd } from '../../lib/string-utils.js';
10
11
  import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
11
12
  // =============================================================================
12
13
  // Detection Logic
@@ -361,11 +362,11 @@ export default class SessionHealth extends PMOCommand {
361
362
  this.log(styles.header('Agent Health Status'));
362
363
  this.log('═'.repeat(95));
363
364
  this.log(styles.muted(' ' +
364
- 'Ticket'.padEnd(12) +
365
- 'Agent'.padEnd(20) +
366
- 'State'.padEnd(12) +
367
- 'Type'.padEnd(15) +
368
- 'Elapsed'.padEnd(10) +
365
+ visualPadEnd('Ticket', 12) +
366
+ visualPadEnd('Agent', 20) +
367
+ visualPadEnd('State', 12) +
368
+ visualPadEnd('Type', 15) +
369
+ visualPadEnd('Elapsed', 10) +
369
370
  'Session'));
370
371
  this.log(' ' + '─'.repeat(93));
371
372
  for (const agent of agents) {
@@ -384,11 +385,11 @@ export default class SessionHealth extends PMOCommand {
384
385
  ? agent.sessionId.substring(0, 21) + '...'
385
386
  : agent.sessionId;
386
387
  this.log(' ' +
387
- agent.ticketId.padEnd(12) +
388
- agent.agentName.padEnd(20) +
389
- stateColor(`${stateIcon} ${agent.state}`).padEnd(22) +
390
- typeIcon.padEnd(15) +
391
- agent.elapsed.padEnd(10) +
388
+ visualPadEnd(agent.ticketId, 12) +
389
+ visualPadEnd(agent.agentName, 20) +
390
+ stateColor(visualPadEnd(`${stateIcon} ${agent.state}`, 12)) +
391
+ visualPadEnd(typeIcon, 15) +
392
+ visualPadEnd(agent.elapsed, 10) +
392
393
  styles.muted(displaySession));
393
394
  }
394
395
  this.log('');
@@ -6,6 +6,8 @@ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
6
6
  import { ExecutionStorage } from '../../lib/execution/index.js';
7
7
  import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
8
8
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
9
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
10
+ import { visualPadEnd } from '../../lib/string-utils.js';
9
11
  export default class SessionList extends PMOCommand {
10
12
  static description = 'List active tmux sessions (host and container)';
11
13
  static examples = [
@@ -25,6 +27,7 @@ export default class SessionList extends PMOCommand {
25
27
  }
26
28
  async execute() {
27
29
  const { flags } = await this.parse(SessionList);
30
+ const jsonMode = shouldOutputJson(flags);
28
31
  // Get workspace info for execution records
29
32
  let executionStorage = null;
30
33
  let db = null;
@@ -153,15 +156,19 @@ export default class SessionList extends PMOCommand {
153
156
  });
154
157
  }
155
158
  }
159
+ if (jsonMode) {
160
+ this.log(JSON.stringify(sessions, null, 2));
161
+ return;
162
+ }
156
163
  if (sessions.length > 0) {
157
164
  this.log('');
158
165
  this.log(styles.header('🖥️ Active Sessions'));
159
166
  this.log('═'.repeat(90));
160
167
  this.log(styles.muted(' ' +
161
- 'Session'.padEnd(34) +
162
- 'Ticket'.padEnd(12) +
163
- 'Agent'.padEnd(18) +
164
- 'Type'.padEnd(15) +
168
+ visualPadEnd('Session', 34) +
169
+ visualPadEnd('Ticket', 12) +
170
+ visualPadEnd('Agent', 18) +
171
+ visualPadEnd('Type', 15) +
165
172
  'Status'));
166
173
  this.log(' ' + '─'.repeat(88));
167
174
  for (const session of sessions) {
@@ -177,10 +184,10 @@ export default class SessionList extends PMOCommand {
177
184
  ? session.sessionId.substring(0, 29) + '...'
178
185
  : session.sessionId;
179
186
  this.log(' ' +
180
- displaySession.padEnd(34) +
181
- session.ticketId.padEnd(12) +
182
- session.agentName.padEnd(18) +
183
- typeIcon.padEnd(15) +
187
+ visualPadEnd(displaySession, 34) +
188
+ visualPadEnd(session.ticketId, 12) +
189
+ visualPadEnd(session.agentName, 18) +
190
+ visualPadEnd(typeIcon, 15) +
184
191
  statusColor(statusText));
185
192
  }
186
193
  this.log('');
@@ -2,6 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class List extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
- static flags: {};
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
6
8
  run(): Promise<void>;
7
9
  }
@@ -3,25 +3,39 @@ import chalk from 'chalk';
3
3
  import * as path from 'node:path';
4
4
  import * as fs from 'node:fs';
5
5
  import { getWorkspaceInfo, getAllAgentsStatus } from '../../lib/agents/commands.js';
6
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
7
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
6
8
  export default class List extends Command {
7
9
  static description = 'List all staff (persistent) agents and their status';
8
10
  static examples = [
9
11
  '<%= config.bin %> <%= command.id %>',
10
12
  ];
11
- static flags = {};
13
+ static flags = {
14
+ ...machineOutputFlags,
15
+ };
12
16
  async run() {
17
+ const { flags } = await this.parse(List);
18
+ const jsonMode = shouldOutputJson(flags);
13
19
  try {
14
20
  // Get workspace information
15
21
  const workspaceInfo = getWorkspaceInfo();
16
22
  // Filter to staff agents only
17
23
  const staffAgents = workspaceInfo.agents.filter(a => a.type === 'persistent');
18
24
  if (staffAgents.length === 0) {
25
+ if (jsonMode) {
26
+ this.log(JSON.stringify([], null, 2));
27
+ return;
28
+ }
19
29
  this.log(chalk.yellow('No staff agents found. Add staff agents with "prlt staff add"'));
20
30
  return;
21
31
  }
22
32
  // Get status for all agents and filter to staff
23
33
  const agentsStatus = getAllAgentsStatus(workspaceInfo);
24
34
  const staffStatus = agentsStatus.filter(a => staffAgents.some(s => s.name === a.name));
35
+ if (jsonMode) {
36
+ this.log(JSON.stringify(staffStatus, null, 2));
37
+ return;
38
+ }
25
39
  this.log(chalk.bold.cyan('\n Staff Agents:\n'));
26
40
  for (const agentStatus of staffStatus) {
27
41
  // Agent info line
@@ -2,5 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class ThemeList extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
5
8
  run(): Promise<void>;
6
9
  }
@@ -3,21 +3,46 @@ import chalk from 'chalk';
3
3
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
4
4
  import { ensureBuiltinThemes } from '../../lib/themes.js';
5
5
  import { getThemes, getThemeNames, getAvailableThemeNames } from '../../lib/database/index.js';
6
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
7
+ import { machineOutputFlags } from '../../lib/pmo/index.js';
6
8
  export default class ThemeList extends Command {
7
9
  static description = 'List available agent themes';
8
10
  static examples = [
9
11
  '<%= config.bin %> <%= command.id %>',
10
12
  ];
13
+ static flags = {
14
+ ...machineOutputFlags,
15
+ };
11
16
  async run() {
17
+ const { flags } = await this.parse(ThemeList);
18
+ const jsonMode = shouldOutputJson(flags);
12
19
  try {
13
20
  const workspaceInfo = getWorkspaceInfo();
14
21
  // Ensure built-in themes are seeded
15
22
  ensureBuiltinThemes(workspaceInfo.path);
16
23
  const themes = getThemes(workspaceInfo.path);
17
24
  if (themes.length === 0) {
25
+ if (jsonMode) {
26
+ this.log(JSON.stringify([], null, 2));
27
+ return;
28
+ }
18
29
  this.log(chalk.yellow('No themes found.'));
19
30
  return;
20
31
  }
32
+ if (jsonMode) {
33
+ const themesWithNames = themes.map(theme => {
34
+ const allNames = getThemeNames(workspaceInfo.path, theme.id);
35
+ const availableNames = getAvailableThemeNames(workspaceInfo.path, theme.id);
36
+ return {
37
+ ...theme,
38
+ availableNames: availableNames.length,
39
+ inUse: allNames.length - availableNames.length,
40
+ totalNames: allNames.length,
41
+ };
42
+ });
43
+ this.log(JSON.stringify(themesWithNames, null, 2));
44
+ return;
45
+ }
21
46
  this.log(chalk.bold('\nAgent Themes\n'));
22
47
  for (const theme of themes) {
23
48
  const allNames = getThemeNames(workspaceInfo.path, theme.id);
@@ -59,7 +59,10 @@ export default class TicketComplete extends PMOCommand {
59
59
  return;
60
60
  }
61
61
  // Get board for columns (use the first incomplete ticket's project)
62
- const board = await this.storage.getBoard(incompleteTickets[0].projectId);
62
+ const board = await this.storage.getProjectBoard(incompleteTickets[0].projectId);
63
+ if (!board) {
64
+ return handleError('PROJECT_NOT_FOUND', `Project "${incompleteTickets[0].projectId}" not found. The ticket may belong to an orphaned project.`);
65
+ }
63
66
  // Find the "Done" column (case-insensitive)
64
67
  const doneColumn = board.columns.find(col => col.name.toLowerCase().includes('done'));
65
68
  if (!doneColumn) {
@@ -9,6 +9,7 @@ export default class TicketCreate extends PMOCommand {
9
9
  priority: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  category: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'description-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  id: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  interactive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  epic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,4 +1,5 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
2
3
  import inquirer from 'inquirer';
3
4
  import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
5
  // Note: inquirer import kept for inquirer.Separator usage in interactive mode
@@ -16,6 +17,8 @@ export default class TicketCreate extends PMOCommand {
16
17
  '<%= config.bin %> <%= command.id %> -t "Add feature" -c "In Progress" -p P1',
17
18
  '<%= config.bin %> <%= command.id %> --project mobile-app -t "New feature"',
18
19
  '<%= config.bin %> <%= command.id %> --epic EPIC-001 -t "Implement auth flow"',
20
+ '<%= config.bin %> <%= command.id %> --title "My ticket" --description-file ./ticket-desc.md',
21
+ '<%= config.bin %> <%= command.id %> --title "My ticket" --description-file - # Read from stdin',
19
22
  '<%= config.bin %> <%= command.id %> --json # Output column choices as JSON',
20
23
  '<%= config.bin %> <%= command.id %> --title "Test" -P PROJ-001 --dry-run --json # Validate without creating',
21
24
  ];
@@ -47,6 +50,11 @@ export default class TicketCreate extends PMOCommand {
47
50
  char: 'd',
48
51
  description: 'Ticket description',
49
52
  }),
53
+ 'description-file': Flags.string({
54
+ char: 'D',
55
+ description: 'Path to a markdown file for the ticket description (use - for stdin)',
56
+ exclusive: ['description'],
57
+ }),
50
58
  id: Flags.string({
51
59
  description: 'Custom ticket ID (auto-generated if not provided)',
52
60
  }),
@@ -65,6 +73,7 @@ export default class TicketCreate extends PMOCommand {
65
73
  }),
66
74
  labels: Flags.string({
67
75
  char: 'l',
76
+ aliases: ['label'],
68
77
  description: 'Labels (comma-separated)',
69
78
  }),
70
79
  'dry-run': Flags.boolean({
@@ -95,6 +104,27 @@ export default class TicketCreate extends PMOCommand {
95
104
  }
96
105
  this.error(message);
97
106
  };
107
+ // Read description from file if --description-file is provided
108
+ if (flags['description-file']) {
109
+ const filePath = flags['description-file'];
110
+ try {
111
+ if (filePath === '-') {
112
+ // Guard: prevent hanging when no input is piped
113
+ if (process.stdin.isTTY) {
114
+ return handleError('DESCRIPTION_FILE_ERROR', 'Cannot read from stdin: no input piped. Use --description-file <path> with a file path instead, or pipe content via: echo "desc" | prlt ticket create --description-file -');
115
+ }
116
+ // Read from stdin
117
+ flags.description = fs.readFileSync(0, 'utf-8');
118
+ }
119
+ else {
120
+ flags.description = fs.readFileSync(filePath, 'utf-8');
121
+ }
122
+ }
123
+ catch (error) {
124
+ const errMsg = error instanceof Error ? error.message : String(error);
125
+ return handleError('DESCRIPTION_FILE_ERROR', `Failed to read description file "${filePath}": ${errMsg}`);
126
+ }
127
+ }
98
128
  // Validate epic if provided
99
129
  if (flags.epic) {
100
130
  const epic = await this.storage.getEpic(flags.epic);
@@ -81,13 +81,13 @@ export default class TicketDelete extends PMOCommand {
81
81
  if (!ticket) {
82
82
  this.error(`Ticket "${ticketId}" not found.`);
83
83
  }
84
- // Get board for project name
85
- const board = await this.storage.getBoard(ticket.projectId);
84
+ // Get board for project name (may be null if project was deleted/orphaned)
85
+ const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
86
86
  // Confirmation prompt (unless --force)
87
87
  if (!flags.force) {
88
88
  this.log(`\nDelete ticket ${styles.emphasis(ticketId)}?`);
89
89
  this.log(` Title: ${ticket.title}`);
90
- this.log(` Project: ${board.name}`);
90
+ this.log(` Project: ${board?.name || ticket.projectId || 'Unknown'}`);
91
91
  this.log(` Status: ${ticket.statusName}`);
92
92
  const jsonModeConfig = jsonMode ? { flags, commandName: 'ticket delete' } : null;
93
93
  const { confirmed } = await this.prompt([{
@@ -153,8 +153,8 @@ export default class TicketEdit extends PMOCommand {
153
153
  return; // outputPromptAsJson exits, but TypeScript doesn't know
154
154
  }
155
155
  // Interactive mode - prompt for all editable fields
156
- const board = await this.storage.getBoard(ticket.projectId);
157
- const columns = board.columns.map(col => col.name);
156
+ const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
157
+ const columns = board ? board.columns.map(col => col.name) : [];
158
158
  updates = await this.promptForEdits(ticket, columns);
159
159
  }
160
160
  else {