@proletariat/cli 0.3.96 → 0.3.98

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 (206) hide show
  1. package/dist/commands/gc.d.ts +1 -0
  2. package/dist/commands/gc.js +31 -1
  3. package/dist/commands/gc.js.map +1 -1
  4. package/dist/commands/linear/connect.d.ts +5 -0
  5. package/dist/commands/linear/connect.js +84 -0
  6. package/dist/commands/linear/connect.js.map +1 -1
  7. package/dist/commands/orchestrate/index.js +18 -10
  8. package/dist/commands/orchestrate/index.js.map +1 -1
  9. package/dist/commands/qa/index.js +1 -1
  10. package/dist/commands/session/watch.d.ts +1 -0
  11. package/dist/commands/session/watch.js +46 -2
  12. package/dist/commands/session/watch.js.map +1 -1
  13. package/dist/commands/watch/index.d.ts +28 -0
  14. package/dist/commands/watch/index.js +172 -0
  15. package/dist/commands/watch/index.js.map +1 -0
  16. package/dist/commands/work/complete.d.ts +1 -0
  17. package/dist/commands/work/complete.js +38 -31
  18. package/dist/commands/work/complete.js.map +1 -1
  19. package/dist/commands/{ticket/index.d.ts → work/drop.d.ts} +5 -4
  20. package/dist/commands/work/drop.js +215 -0
  21. package/dist/commands/work/drop.js.map +1 -0
  22. package/dist/commands/work/linear.js +1 -1
  23. package/dist/commands/work/linear.js.map +1 -1
  24. package/dist/commands/work/ready.d.ts +1 -0
  25. package/dist/commands/work/ready.js +46 -30
  26. package/dist/commands/work/ready.js.map +1 -1
  27. package/dist/commands/work/ship.d.ts +1 -0
  28. package/dist/commands/work/ship.js +56 -40
  29. package/dist/commands/work/ship.js.map +1 -1
  30. package/dist/commands/work/start.d.ts +2 -0
  31. package/dist/commands/work/start.js +238 -83
  32. package/dist/commands/work/start.js.map +1 -1
  33. package/dist/commands/work/stop.d.ts +1 -0
  34. package/dist/commands/work/stop.js +40 -0
  35. package/dist/commands/work/stop.js.map +1 -1
  36. package/dist/lib/agents/commands.js +7 -5
  37. package/dist/lib/agents/commands.js.map +1 -1
  38. package/dist/lib/asana/client.d.ts +4 -1
  39. package/dist/lib/asana/client.js +15 -0
  40. package/dist/lib/asana/client.js.map +1 -1
  41. package/dist/lib/asana/index.d.ts +1 -1
  42. package/dist/lib/asana/types.d.ts +4 -0
  43. package/dist/lib/database/credential-store.js +1 -0
  44. package/dist/lib/database/credential-store.js.map +1 -1
  45. package/dist/lib/database/drizzle-schema.d.ts +17 -0
  46. package/dist/lib/database/drizzle-schema.js +1 -0
  47. package/dist/lib/database/drizzle-schema.js.map +1 -1
  48. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.d.ts +9 -0
  49. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.js +23 -0
  50. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.js.map +1 -0
  51. package/dist/lib/database/migrations/0020_transition_map.d.ts +2 -0
  52. package/dist/lib/database/migrations/0020_transition_map.js +27 -0
  53. package/dist/lib/database/migrations/0020_transition_map.js.map +1 -0
  54. package/dist/lib/database/migrations/index.js +4 -0
  55. package/dist/lib/database/migrations/index.js.map +1 -1
  56. package/dist/lib/execution/config.d.ts +10 -0
  57. package/dist/lib/execution/config.js +24 -0
  58. package/dist/lib/execution/config.js.map +1 -1
  59. package/dist/lib/execution/preflight.d.ts +51 -0
  60. package/dist/lib/execution/preflight.js +278 -0
  61. package/dist/lib/execution/preflight.js.map +1 -0
  62. package/dist/lib/execution/runners/prompt-builder.d.ts +6 -0
  63. package/dist/lib/execution/runners/prompt-builder.js +38 -7
  64. package/dist/lib/execution/runners/prompt-builder.js.map +1 -1
  65. package/dist/lib/execution/session-utils.d.ts +23 -0
  66. package/dist/lib/execution/session-utils.js +69 -0
  67. package/dist/lib/execution/session-utils.js.map +1 -1
  68. package/dist/lib/execution/spawner.d.ts +11 -1
  69. package/dist/lib/execution/spawner.js +44 -16
  70. package/dist/lib/execution/spawner.js.map +1 -1
  71. package/dist/lib/execution/storage.d.ts +6 -0
  72. package/dist/lib/execution/storage.js +18 -0
  73. package/dist/lib/execution/storage.js.map +1 -1
  74. package/dist/lib/execution/ticket-refs.d.ts +71 -0
  75. package/dist/lib/execution/ticket-refs.js +125 -0
  76. package/dist/lib/execution/ticket-refs.js.map +1 -0
  77. package/dist/lib/execution/types.d.ts +7 -2
  78. package/dist/lib/execution/types.js +5 -3
  79. package/dist/lib/execution/types.js.map +1 -1
  80. package/dist/lib/external-issues/index.d.ts +1 -0
  81. package/dist/lib/external-issues/index.js +2 -0
  82. package/dist/lib/external-issues/index.js.map +1 -1
  83. package/dist/lib/external-issues/linear.js +1 -1
  84. package/dist/lib/external-issues/linear.js.map +1 -1
  85. package/dist/lib/external-issues/ticket-builder.d.ts +21 -0
  86. package/dist/lib/external-issues/ticket-builder.js +43 -0
  87. package/dist/lib/external-issues/ticket-builder.js.map +1 -0
  88. package/dist/lib/external-issues/work-start.js +1 -1
  89. package/dist/lib/external-issues/work-start.js.map +1 -1
  90. package/dist/lib/gc/index.d.ts +67 -6
  91. package/dist/lib/gc/index.js +293 -15
  92. package/dist/lib/gc/index.js.map +1 -1
  93. package/dist/lib/github/client.d.ts +84 -0
  94. package/dist/lib/github/client.js +123 -0
  95. package/dist/lib/github/client.js.map +1 -0
  96. package/dist/lib/github/config.d.ts +42 -0
  97. package/dist/lib/github/config.js +115 -0
  98. package/dist/lib/github/config.js.map +1 -0
  99. package/dist/lib/github/types.d.ts +71 -0
  100. package/dist/lib/github/types.js +50 -0
  101. package/dist/lib/github/types.js.map +1 -0
  102. package/dist/lib/jira/client.d.ts +113 -0
  103. package/dist/lib/jira/client.js +208 -0
  104. package/dist/lib/jira/client.js.map +1 -0
  105. package/dist/lib/jira/config.d.ts +9 -0
  106. package/dist/lib/jira/config.js +30 -0
  107. package/dist/lib/jira/config.js.map +1 -1
  108. package/dist/lib/jira/index.d.ts +6 -2
  109. package/dist/lib/jira/index.js +4 -2
  110. package/dist/lib/jira/index.js.map +1 -1
  111. package/dist/lib/jira/types.d.ts +118 -0
  112. package/dist/lib/jira/types.js +45 -0
  113. package/dist/lib/jira/types.js.map +1 -0
  114. package/dist/lib/linear/config.d.ts +10 -0
  115. package/dist/lib/linear/config.js +33 -0
  116. package/dist/lib/linear/config.js.map +1 -1
  117. package/dist/lib/mcp/tools/index.d.ts +0 -2
  118. package/dist/lib/mcp/tools/index.js +0 -2
  119. package/dist/lib/mcp/tools/index.js.map +1 -1
  120. package/dist/lib/orchestrate/index.d.ts +2 -0
  121. package/dist/lib/orchestrate/index.js +1 -0
  122. package/dist/lib/orchestrate/index.js.map +1 -1
  123. package/dist/lib/orchestrate/simple-poller.d.ts +57 -0
  124. package/dist/lib/orchestrate/simple-poller.js +324 -0
  125. package/dist/lib/orchestrate/simple-poller.js.map +1 -0
  126. package/dist/lib/pmo/storage/index.js +16 -5
  127. package/dist/lib/pmo/storage/index.js.map +1 -1
  128. package/dist/lib/prompt-json.d.ts +31 -0
  129. package/dist/lib/prompt-json.js.map +1 -1
  130. package/dist/lib/providers/asana-provider.d.ts +27 -0
  131. package/dist/lib/providers/asana-provider.js +426 -0
  132. package/dist/lib/providers/asana-provider.js.map +1 -0
  133. package/dist/lib/providers/auto-mapper.d.ts +45 -0
  134. package/dist/lib/providers/auto-mapper.js +138 -0
  135. package/dist/lib/providers/auto-mapper.js.map +1 -0
  136. package/dist/lib/providers/github-provider.d.ts +34 -0
  137. package/dist/lib/providers/github-provider.js +418 -0
  138. package/dist/lib/providers/github-provider.js.map +1 -0
  139. package/dist/lib/providers/index.d.ts +3 -0
  140. package/dist/lib/providers/index.js +3 -0
  141. package/dist/lib/providers/index.js.map +1 -1
  142. package/dist/lib/providers/jira-provider.d.ts +31 -0
  143. package/dist/lib/providers/jira-provider.js +383 -0
  144. package/dist/lib/providers/jira-provider.js.map +1 -0
  145. package/dist/lib/providers/linear-provider.js +6 -7
  146. package/dist/lib/providers/linear-provider.js.map +1 -1
  147. package/dist/lib/providers/resolver.js +54 -0
  148. package/dist/lib/providers/resolver.js.map +1 -1
  149. package/dist/lib/providers/state-intents.d.ts +20 -0
  150. package/dist/lib/providers/state-intents.js +61 -7
  151. package/dist/lib/providers/state-intents.js.map +1 -1
  152. package/dist/lib/providers/state-resolution.d.ts +15 -11
  153. package/dist/lib/providers/state-resolution.js +54 -48
  154. package/dist/lib/providers/state-resolution.js.map +1 -1
  155. package/dist/lib/providers/transition-map.d.ts +59 -0
  156. package/dist/lib/providers/transition-map.js +113 -0
  157. package/dist/lib/providers/transition-map.js.map +1 -0
  158. package/dist/lib/providers/types.d.ts +1 -1
  159. package/dist/lib/session/index.d.ts +3 -1
  160. package/dist/lib/session/index.js +3 -1
  161. package/dist/lib/session/index.js.map +1 -1
  162. package/dist/lib/session/tmux-watchdog.d.ts +157 -0
  163. package/dist/lib/session/tmux-watchdog.js +424 -0
  164. package/dist/lib/session/tmux-watchdog.js.map +1 -0
  165. package/dist/lib/session/watcher.d.ts +22 -4
  166. package/dist/lib/session/watcher.js +66 -8
  167. package/dist/lib/session/watcher.js.map +1 -1
  168. package/dist/lib/work-lifecycle/post-execution.js +26 -1
  169. package/dist/lib/work-lifecycle/post-execution.js.map +1 -1
  170. package/dist/lib/work-lifecycle/transition.d.ts +73 -0
  171. package/dist/lib/work-lifecycle/transition.js +138 -0
  172. package/dist/lib/work-lifecycle/transition.js.map +1 -0
  173. package/dist/lib/work-source/config.d.ts +1 -1
  174. package/dist/lib/work-source/config.js +11 -1
  175. package/dist/lib/work-source/config.js.map +1 -1
  176. package/oclif.manifest.json +936 -1628
  177. package/package.json +1 -1
  178. package/dist/commands/ticket/create.d.ts +0 -44
  179. package/dist/commands/ticket/create.js +0 -760
  180. package/dist/commands/ticket/create.js.map +0 -1
  181. package/dist/commands/ticket/delete.d.ts +0 -17
  182. package/dist/commands/ticket/delete.js +0 -204
  183. package/dist/commands/ticket/delete.js.map +0 -1
  184. package/dist/commands/ticket/edit.d.ts +0 -28
  185. package/dist/commands/ticket/edit.js +0 -402
  186. package/dist/commands/ticket/edit.js.map +0 -1
  187. package/dist/commands/ticket/index.js +0 -74
  188. package/dist/commands/ticket/index.js.map +0 -1
  189. package/dist/commands/ticket/list.d.ts +0 -33
  190. package/dist/commands/ticket/list.js +0 -519
  191. package/dist/commands/ticket/list.js.map +0 -1
  192. package/dist/commands/ticket/move.d.ts +0 -27
  193. package/dist/commands/ticket/move.js +0 -413
  194. package/dist/commands/ticket/move.js.map +0 -1
  195. package/dist/commands/ticket/show.d.ts +0 -14
  196. package/dist/commands/ticket/show.js +0 -110
  197. package/dist/commands/ticket/show.js.map +0 -1
  198. package/dist/commands/ticket/update.d.ts +0 -28
  199. package/dist/commands/ticket/update.js +0 -458
  200. package/dist/commands/ticket/update.js.map +0 -1
  201. package/dist/lib/mcp/tools/action.d.ts +0 -6
  202. package/dist/lib/mcp/tools/action.js +0 -123
  203. package/dist/lib/mcp/tools/action.js.map +0 -1
  204. package/dist/lib/mcp/tools/ticket.d.ts +0 -6
  205. package/dist/lib/mcp/tools/ticket.js +0 -464
  206. package/dist/lib/mcp/tools/ticket.js.map +0 -1
@@ -1,760 +0,0 @@
1
- import { Flags } from '@oclif/core';
2
- import * as fs from 'node:fs';
3
- import inquirer from 'inquirer';
4
- import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
5
- // Note: inquirer import kept for inquirer.Separator usage in interactive mode
6
- import { styles } from '../../lib/styles.js';
7
- import { updateEpicTicketsSection } from '../../lib/pmo/epic-files.js';
8
- import { getWorkspacePriorities } from '../../lib/work-lifecycle/settings.js';
9
- import { shouldOutputJson, outputErrorAsJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
10
- import { FlagResolver } from '../../lib/flags/index.js';
11
- import { multiLineInput } from '../../lib/multiline-input.js';
12
- import { getRegisteredWorkSources, loadDefaultWorkSource } from '../../lib/work-source/config.js';
13
- export default class TicketCreate extends PMOCommand {
14
- static description = 'Create a new ticket (routes to Linear when configured, or local PMO)';
15
- static examples = [
16
- '<%= config.bin %> <%= command.id %>',
17
- '<%= config.bin %> <%= command.id %> --title "Fix login bug" --column Backlog',
18
- '<%= config.bin %> <%= command.id %> -t "Add feature" -c "In Progress" -p P1',
19
- '<%= config.bin %> <%= command.id %> --project mobile-app -t "New feature"',
20
- '<%= config.bin %> <%= command.id %> --epic EPIC-001 -t "Implement auth flow"',
21
- '<%= config.bin %> <%= command.id %> --title "My ticket" --description-file ./ticket-desc.md',
22
- '<%= config.bin %> <%= command.id %> --title "My ticket" --description-file - # Read from stdin',
23
- '<%= config.bin %> <%= command.id %> --json # Output column choices as JSON',
24
- '<%= config.bin %> <%= command.id %> --title "Test" -P PROJ-001 --dry-run --json # Validate without creating',
25
- '<%= config.bin %> <%= command.id %> --source linear -t "Fix bug" --team ENG',
26
- '<%= config.bin %> <%= command.id %> --source pmo -t "Local task" -c Backlog',
27
- ];
28
- static flags = {
29
- ...pmoBaseFlags,
30
- title: Flags.string({
31
- char: 't',
32
- description: 'Ticket title [required for non-interactive]',
33
- }),
34
- column: Flags.string({
35
- char: 'c',
36
- description: 'Column to place the ticket in',
37
- }),
38
- priority: Flags.string({
39
- char: 'p',
40
- description: 'Ticket priority (uses workspace priority scale)',
41
- }),
42
- category: Flags.string({
43
- description: 'Ticket category (e.g., bug, feature, refactor)',
44
- }),
45
- description: Flags.string({
46
- char: 'd',
47
- description: 'Ticket description',
48
- }),
49
- 'description-file': Flags.string({
50
- char: 'D',
51
- description: 'Path to a markdown file for the ticket description (use - for stdin)',
52
- exclusive: ['description'],
53
- }),
54
- id: Flags.string({
55
- description: 'Custom ticket ID (auto-generated if not provided)',
56
- }),
57
- interactive: Flags.boolean({
58
- char: 'i',
59
- description: 'Interactive mode',
60
- default: false,
61
- }),
62
- epic: Flags.string({
63
- char: 'e',
64
- description: 'Link ticket to an epic (e.g., EPIC-001)',
65
- }),
66
- template: Flags.string({
67
- char: 'T',
68
- description: 'Create from a template (e.g., bug-report, feature-request)',
69
- }),
70
- labels: Flags.string({
71
- char: 'l',
72
- aliases: ['label'],
73
- description: 'Labels (comma-separated)',
74
- }),
75
- 'dry-run': Flags.boolean({
76
- description: 'Validate inputs without creating ticket (use with --json for structured output)',
77
- default: false,
78
- }),
79
- source: Flags.string({
80
- description: 'Ticket source: "pmo" for local DB, "linear" for Linear API, or "auto" to detect (default: auto)',
81
- options: ['auto', 'pmo', 'linear'],
82
- default: 'auto',
83
- }),
84
- team: Flags.string({
85
- description: 'Linear team key (fallback: PRLT_LINEAR_TEAM)',
86
- }),
87
- };
88
- async execute() {
89
- const { flags } = await this.parse(TicketCreate);
90
- // Check if JSON output mode is active
91
- const jsonMode = shouldOutputJson(flags);
92
- // Determine ticket source (pmo, linear, or prompt user)
93
- const resolvedSource = await this.resolveSource(flags, jsonMode);
94
- // If Linear source is selected, delegate to the Linear creation path
95
- if (resolvedSource === 'linear') {
96
- return this.createLinearIssue(flags, jsonMode);
97
- }
98
- // PMO path — existing flow
99
- // Get project and board info (pass JSON mode config for AI agents)
100
- const projectId = await this.requireProject({
101
- jsonMode: {
102
- flags,
103
- commandName: 'ticket create',
104
- baseCommand: 'prlt ticket create',
105
- },
106
- });
107
- const board = await this.storage.getBoard(projectId);
108
- const columns = board.columns.map(c => c.name);
109
- const projectName = await this.getProjectName(projectId);
110
- // Helper to handle errors in JSON mode
111
- const handleError = (code, message) => {
112
- if (jsonMode) {
113
- outputErrorAsJson(code, message, createMetadata('ticket create', flags));
114
- return;
115
- }
116
- this.error(message);
117
- };
118
- // Read description from file if --description-file is provided
119
- if (flags['description-file']) {
120
- const filePath = flags['description-file'];
121
- try {
122
- if (filePath === '-') {
123
- // Guard: prevent hanging when no input is piped
124
- if (process.stdin.isTTY) {
125
- 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 -');
126
- }
127
- // Read from stdin
128
- flags.description = fs.readFileSync(0, 'utf-8');
129
- }
130
- else {
131
- flags.description = fs.readFileSync(filePath, 'utf-8');
132
- }
133
- }
134
- catch (error) {
135
- const errMsg = error instanceof Error ? error.message : String(error);
136
- return handleError('DESCRIPTION_FILE_ERROR', `Failed to read description file "${filePath}": ${errMsg}`);
137
- }
138
- }
139
- // Validate epic if provided
140
- if (flags.epic) {
141
- const epic = await this.storage.getEpic(flags.epic);
142
- if (!epic) {
143
- return handleError('EPIC_NOT_FOUND', `Epic not found: ${flags.epic}. Use 'prlt epic list' to see available epics.`);
144
- }
145
- }
146
- // Load template if specified
147
- let template = null;
148
- if (flags.template) {
149
- template = await this.storage.getTicketTemplate(flags.template);
150
- if (!template) {
151
- return handleError('TEMPLATE_NOT_FOUND', `Template not found: ${flags.template}. Run 'prlt ticket template list' to see available templates.`);
152
- }
153
- }
154
- // Parse labels from flag
155
- const labelsFromFlag = flags.labels
156
- ? flags.labels.split(',').map(l => l.trim()).filter(Boolean)
157
- : undefined;
158
- // Get ticket data (interactive or from flags)
159
- let ticketData;
160
- // Use FlagResolver to handle both JSON mode and interactive prompts
161
- // This unifies the two code paths into one pattern
162
- if (!flags.interactive) {
163
- // In JSON mode, default column to first backlog status if not provided
164
- // This prevents prompting for column in non-interactive mode
165
- if (jsonMode && !flags.column) {
166
- // Prefer "Backlog" column, fall back to first column
167
- const backlogColumn = columns.find(c => c.toLowerCase() === 'backlog') || columns[0];
168
- flags.column = backlogColumn;
169
- }
170
- const resolver = new FlagResolver({
171
- commandName: 'ticket create',
172
- baseCommand: 'prlt ticket create',
173
- jsonMode,
174
- flags,
175
- context: { projectId },
176
- });
177
- // Column selection - prompted first if missing
178
- resolver.addPrompt({
179
- flagName: 'column',
180
- type: 'list',
181
- message: 'Select column to place the ticket in:',
182
- choices: () => columns.map(c => ({ name: c, value: c })),
183
- when: (ctx) => !ctx.flags.column,
184
- });
185
- // Title input - prompted after column is set
186
- resolver.addPrompt({
187
- flagName: 'title',
188
- type: 'input',
189
- message: 'Enter ticket title:',
190
- when: (ctx) => !ctx.flags.title && ctx.flags.column !== undefined,
191
- validate: (value) => value.trim() ? true : 'Title cannot be empty',
192
- context: (ctx) => ({
193
- hint: `Provide title with: ${ctx.baseCommand}${ctx.projectId ? ` -P ${ctx.projectId}` : ''} --column "${ctx.flags.column}" --title "Your title here"`,
194
- requiredFields: ['--title'],
195
- optionalFields: ['--priority', '--category', '--description', '--epic', '--labels'],
196
- example: `${ctx.baseCommand}${ctx.projectId ? ` -P ${ctx.projectId}` : ''} --column "${ctx.flags.column}" --title "Fix login bug" --priority P1 --category bug`,
197
- }),
198
- });
199
- // Resolve missing flags (in JSON mode, outputs prompt and exits; in interactive mode, prompts user)
200
- const resolvedFlags = await resolver.resolve();
201
- // If we get here, we have both column and title
202
- if (!resolvedFlags.title && !template?.titlePattern) {
203
- return handleError('TITLE_REQUIRED', 'Title is required. Use --title or -t flag, or use --interactive mode.');
204
- }
205
- ticketData = {
206
- title: resolvedFlags.title || template?.titlePattern || '',
207
- statusName: resolvedFlags.column || columns[0],
208
- priority: resolvedFlags.priority || template?.defaultPriority,
209
- category: resolvedFlags.category || template?.defaultCategory,
210
- description: resolvedFlags.description || template?.descriptionTemplate,
211
- id: resolvedFlags.id,
212
- epicId: resolvedFlags.epic,
213
- labels: labelsFromFlag || template?.defaultLabels,
214
- };
215
- }
216
- else {
217
- // Full interactive mode - use the detailed prompts
218
- ticketData = await this.promptTicketData(flags, this.storage, template, columns);
219
- }
220
- // Validate status/column
221
- if (!columns.includes(ticketData.statusName)) {
222
- if (flags['dry-run']) {
223
- if (jsonMode) {
224
- outputDryRunErrorsAsJson([{ field: 'column', error: `Invalid column "${ticketData.statusName}". Available: ${columns.join(', ')}` }], createMetadata('ticket create', flags));
225
- return;
226
- }
227
- this.error(`Invalid column "${ticketData.statusName}". Available columns: ${columns.join(', ')}`);
228
- }
229
- return handleError('INVALID_COLUMN', `Invalid column "${ticketData.statusName}". Available columns: ${columns.join(', ')}`);
230
- }
231
- // Handle dry-run: show what would be created without actually creating
232
- if (flags['dry-run']) {
233
- const wouldCreate = {
234
- title: ticketData.title,
235
- project: projectId,
236
- column: ticketData.statusName,
237
- ...(ticketData.priority && { priority: ticketData.priority }),
238
- ...(ticketData.category && { category: ticketData.category }),
239
- ...(ticketData.description && { description: ticketData.description }),
240
- ...(ticketData.epicId && { epic: ticketData.epicId }),
241
- ...(ticketData.labels && ticketData.labels.length > 0 && { labels: ticketData.labels }),
242
- };
243
- if (jsonMode) {
244
- outputDryRunSuccessAsJson('ticket', wouldCreate, createMetadata('ticket create', flags));
245
- return;
246
- }
247
- // Human-readable dry-run output
248
- this.log(styles.warning('\n[DRY RUN] Would create ticket:'));
249
- this.log(styles.muted(` Title: ${ticketData.title}`));
250
- this.log(styles.muted(` Project: ${projectName}`));
251
- this.log(styles.muted(` Column: ${ticketData.statusName}`));
252
- if (ticketData.priority) {
253
- this.log(styles.muted(` Priority: ${ticketData.priority}`));
254
- }
255
- if (ticketData.category) {
256
- this.log(styles.muted(` Category: ${ticketData.category}`));
257
- }
258
- if (ticketData.epicId) {
259
- this.log(styles.muted(` Epic: ${ticketData.epicId}`));
260
- }
261
- if (ticketData.labels && ticketData.labels.length > 0) {
262
- this.log(styles.muted(` Labels: ${ticketData.labels.join(', ')}`));
263
- }
264
- if (template) {
265
- this.log(styles.muted(` Template: ${template.name}`));
266
- if (template.suggestedSubtasks.length > 0) {
267
- this.log(styles.muted(` Subtasks: ${template.suggestedSubtasks.length} would be created`));
268
- }
269
- }
270
- this.log(styles.muted('\n(No ticket was created)'));
271
- return;
272
- }
273
- // Create ticket through the provider (routes to configured backend)
274
- const provider = this.resolveProjectProvider(projectId, 'pmo');
275
- const createResult = await provider.createTicket(projectId, {
276
- id: ticketData.id,
277
- title: ticketData.title,
278
- statusName: ticketData.statusName,
279
- priority: ticketData.priority,
280
- category: ticketData.category,
281
- description: ticketData.description,
282
- epicId: ticketData.epicId,
283
- labels: ticketData.labels,
284
- });
285
- if (!createResult.success || !createResult.ticket) {
286
- return handleError('CREATE_FAILED', `Failed to create ticket: ${createResult.error}`);
287
- }
288
- const ticket = createResult.ticket;
289
- // Add subtasks from template if applicable
290
- if (template && template.suggestedSubtasks.length > 0) {
291
- // Sequential subtask creation for consistent ordering
292
- for (const subtask of template.suggestedSubtasks) {
293
- // eslint-disable-next-line no-await-in-loop
294
- await this.storage.addSubtask(ticket.id, subtask.title);
295
- }
296
- }
297
- // Auto-export to board.md after write
298
- await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
299
- // If linked to an epic, update the epic's markdown file with ticket list
300
- if (ticketData.epicId) {
301
- const epic = await this.storage.getEpic(ticketData.epicId);
302
- if (epic) {
303
- const epicTickets = await this.storage.getTicketsForEpic(projectId, ticketData.epicId);
304
- const ticketInfos = epicTickets.map(t => ({
305
- id: t.id,
306
- title: t.title,
307
- status: t.statusName || 'Unknown',
308
- priority: t.priority,
309
- }));
310
- updateEpicTicketsSection(this.pmoPath, ticketData.epicId, epic.status, ticketInfos, projectId);
311
- }
312
- }
313
- // JSON output mode - match MCP tool response shape
314
- if (jsonMode) {
315
- this.log(JSON.stringify({
316
- success: true,
317
- ticket: {
318
- id: ticket.id,
319
- title: ticket.title,
320
- priority: ticket.priority,
321
- category: ticket.category,
322
- statusName: ticket.statusName,
323
- statusCategory: ticket.statusCategory,
324
- projectId: ticket.projectId,
325
- assignee: ticket.assignee,
326
- owner: ticket.owner,
327
- branch: ticket.branch,
328
- epicId: ticket.epicId,
329
- position: ticket.position,
330
- },
331
- }, null, 2));
332
- return;
333
- }
334
- this.log(styles.success(`\n✅ Created ticket ${styles.emphasis(ticket.id)} in project ${styles.emphasis(projectName)}`));
335
- if (template) {
336
- this.log(styles.muted(` Template: ${template.name}`));
337
- }
338
- this.log(styles.muted(` Title: ${ticket.title}`));
339
- this.log(styles.muted(` Status: ${ticket.statusName}`));
340
- if (ticket.priority) {
341
- this.log(styles.muted(` Priority: ${ticket.priority}`));
342
- }
343
- if (ticket.category) {
344
- this.log(styles.muted(` Category: ${ticket.category}`));
345
- }
346
- if (ticketData.epicId) {
347
- this.log(styles.muted(` Epic: ${ticketData.epicId}`));
348
- }
349
- if (ticketData.labels && ticketData.labels.length > 0) {
350
- this.log(styles.muted(` Labels: ${ticketData.labels.join(', ')}`));
351
- }
352
- if (template && template.suggestedSubtasks.length > 0) {
353
- this.log(styles.muted(` Subtasks: ${template.suggestedSubtasks.length} created`));
354
- }
355
- this.log(styles.muted(`\n View board: prlt board`));
356
- this.log(styles.muted(` List tickets: prlt ticket list`));
357
- }
358
- /**
359
- * Determine whether to route through Linear or PMO based on --source flag
360
- * and workspace config.
361
- *
362
- * Resolution order for auto mode:
363
- * 1. Explicit default work source (loadDefaultWorkSource) — user preference
364
- * 2. Single external provider configured — auto-select it
365
- * 3. Multiple external providers — prompt user to choose
366
- * 4. No external providers — fall back to PMO
367
- */
368
- async resolveSource(flags, jsonMode) {
369
- const source = flags.source || 'auto';
370
- if (source === 'pmo')
371
- return 'pmo';
372
- if (source === 'linear')
373
- return 'linear';
374
- // auto: resolve from workspace config
375
- const db = this.storage.getDatabase();
376
- try {
377
- // 1. Respect explicit default work source if configured
378
- const defaultSource = loadDefaultWorkSource(db);
379
- if (defaultSource?.provider === 'linear')
380
- return 'linear';
381
- if (defaultSource?.provider === 'pmo')
382
- return 'pmo';
383
- // 2. Check registered providers (PMO is always implicitly registered)
384
- const registeredSources = getRegisteredWorkSources(db);
385
- const externalProviders = [...new Set(registeredSources.map(s => s.provider).filter(p => p !== 'pmo'))];
386
- // No external providers — use PMO
387
- if (externalProviders.length === 0)
388
- return 'pmo';
389
- // Single external provider — auto-select it
390
- if (externalProviders.length === 1) {
391
- return externalProviders[0] === 'linear' ? 'linear' : 'pmo';
392
- }
393
- // Multiple external providers — prompt user to select
394
- const allProviders = ['pmo', ...externalProviders];
395
- const choices = allProviders.map(p => ({
396
- name: p === 'pmo' ? 'PMO (local)' : `${p.charAt(0).toUpperCase() + p.slice(1)}`,
397
- value: p,
398
- }));
399
- const message = 'Multiple ticket providers configured. Where should this ticket be created?';
400
- const selectedSource = await this.selectFromList({
401
- message,
402
- items: allProviders.map(p => ({
403
- name: p === 'pmo' ? 'PMO (local)' : `${p.charAt(0).toUpperCase() + p.slice(1)}`,
404
- value: p,
405
- })),
406
- getName: (item) => item.name,
407
- getValue: (item) => item.value,
408
- getCommand: (item) => `prlt ticket create --source ${item.value} --json`,
409
- jsonMode: jsonMode
410
- ? { flags: flags, commandName: 'ticket create' }
411
- : null,
412
- });
413
- // In JSON mode selectFromList returns null (prompt already emitted)
414
- if (selectedSource === null)
415
- return 'pmo';
416
- // Only 'linear' gets the special path; everything else falls through to PMO
417
- return selectedSource === 'linear' ? 'linear' : 'pmo';
418
- }
419
- catch {
420
- // workspace_settings table may not exist in older/test databases
421
- return 'pmo';
422
- }
423
- }
424
- /**
425
- * Create a ticket through the Linear provider adapter.
426
- * Collects inputs (title, description, priority, labels) and routes
427
- * through the provider, which handles the Linear API and mirror creation.
428
- */
429
- async createLinearIssue(flags, jsonMode) {
430
- const handleError = (code, message) => {
431
- if (jsonMode) {
432
- outputErrorAsJson(code, message, createMetadata('ticket create', flags));
433
- return;
434
- }
435
- this.error(message);
436
- };
437
- // Collect title (required)
438
- let title = flags.title;
439
- if (!title) {
440
- const inputTitle = await this.promptForInput({
441
- message: 'Enter ticket title:',
442
- fieldName: 'title',
443
- validate: (input) => input.trim() ? true : 'Title cannot be empty',
444
- jsonMode: jsonMode
445
- ? { flags: flags, commandName: 'ticket create', commandHint: 'Provide --title flag', example: 'prlt ticket create --source linear --title "My ticket"' }
446
- : null,
447
- });
448
- // In JSON mode, promptForInput returns '' (prompt already emitted)
449
- if (jsonMode && !inputTitle)
450
- return;
451
- title = inputTitle;
452
- }
453
- // Read description from file if --description-file is provided
454
- let description = flags.description;
455
- if (flags['description-file']) {
456
- const filePath = flags['description-file'];
457
- try {
458
- if (filePath === '-') {
459
- if (process.stdin.isTTY) {
460
- return handleError('DESCRIPTION_FILE_ERROR', 'Cannot read from stdin: no input piped.');
461
- }
462
- description = fs.readFileSync(0, 'utf-8');
463
- }
464
- else {
465
- description = fs.readFileSync(filePath, 'utf-8');
466
- }
467
- }
468
- catch (error) {
469
- const errMsg = error instanceof Error ? error.message : String(error);
470
- return handleError('DESCRIPTION_FILE_ERROR', `Failed to read description file "${filePath}": ${errMsg}`);
471
- }
472
- }
473
- const pmoPriority = flags.priority;
474
- const category = flags.category;
475
- // Parse labels from flag
476
- const labelsInput = flags.labels;
477
- const labelNames = labelsInput
478
- ? labelsInput.split(',').map(l => l.trim()).filter(Boolean)
479
- : [];
480
- // Handle dry-run
481
- if (flags['dry-run']) {
482
- const wouldCreate = {
483
- source: 'linear',
484
- title: title,
485
- ...(description && { description }),
486
- ...(pmoPriority && { priority: pmoPriority }),
487
- ...(labelNames.length > 0 && { labels: labelNames }),
488
- ...(category && { category }),
489
- };
490
- if (jsonMode) {
491
- outputDryRunSuccessAsJson('ticket', wouldCreate, createMetadata('ticket create', flags));
492
- return;
493
- }
494
- this.log(styles.warning('\n[DRY RUN] Would create Linear issue:'));
495
- this.log(styles.muted(` Title: ${title}`));
496
- if (pmoPriority) {
497
- this.log(styles.muted(` Priority: ${pmoPriority}`));
498
- }
499
- if (labelNames.length > 0) {
500
- this.log(styles.muted(` Labels: ${labelNames.join(', ')}`));
501
- }
502
- if (category) {
503
- this.log(styles.muted(` Category: ${category}`));
504
- }
505
- if (description) {
506
- const shortDesc = description.split('\n')[0].substring(0, 60);
507
- this.log(styles.muted(` Description: ${shortDesc}${description.length > 60 ? '...' : ''}`));
508
- }
509
- this.log(styles.muted('\n(No issue was created)'));
510
- return;
511
- }
512
- // Get project for the provider (needed for PMO mirror)
513
- const projectId = await this.requireProject({
514
- jsonMode: {
515
- flags: flags,
516
- commandName: 'ticket create',
517
- baseCommand: 'prlt ticket create --source linear',
518
- },
519
- });
520
- // Route through provider adapter — Linear provider handles API call, mirror, and mapping
521
- const provider = this.resolveProjectProvider(projectId, 'linear');
522
- const createResult = await provider.createTicket(projectId, {
523
- title: title,
524
- description,
525
- priority: pmoPriority,
526
- category,
527
- labels: labelNames.length > 0 ? labelNames : undefined,
528
- });
529
- if (!createResult.success || !createResult.ticket) {
530
- return handleError('CREATE_FAILED', `Failed to create ticket: ${createResult.error}`);
531
- }
532
- const ticket = createResult.ticket;
533
- const externalKey = ticket.metadata?.external_key;
534
- const externalUrl = ticket.metadata?.external_url;
535
- // JSON output
536
- if (jsonMode) {
537
- this.log(JSON.stringify({
538
- success: true,
539
- source: 'linear',
540
- ticket: {
541
- id: ticket.id,
542
- title: ticket.title,
543
- priority: ticket.priority,
544
- category: ticket.category,
545
- statusName: ticket.statusName,
546
- projectId: ticket.projectId,
547
- ...(externalKey && { externalKey }),
548
- ...(externalUrl && { externalUrl }),
549
- },
550
- }, null, 2));
551
- return;
552
- }
553
- this.log(styles.success(`\n✅ Created ticket ${styles.emphasis(externalKey || ticket.id)} via Linear`));
554
- this.log(styles.muted(` Title: ${ticket.title}`));
555
- if (ticket.priority) {
556
- this.log(styles.muted(` Priority: ${ticket.priority}`));
557
- }
558
- if (externalUrl) {
559
- this.log(styles.muted(` URL: ${externalUrl}`));
560
- }
561
- }
562
- async promptTicketData(flags, storage, existingTemplate, columns) {
563
- // If no template was specified via flag, offer to select one
564
- let template = existingTemplate;
565
- if (!template && !flags.template) {
566
- const templates = await storage.listTicketTemplates();
567
- if (templates.length > 0) {
568
- const { selectedTemplate } = await this.prompt([
569
- {
570
- type: 'list',
571
- name: 'selectedTemplate',
572
- message: 'Start from a template?',
573
- choices: [
574
- { name: 'No template (blank ticket)', value: '' },
575
- new inquirer.Separator('── Templates ──'),
576
- ...templates.map(t => ({
577
- name: `${t.name}${t.isBuiltin ? '' : ' [custom]'} - ${t.description || ''}`,
578
- value: t.id,
579
- })),
580
- ],
581
- },
582
- ], null);
583
- if (selectedTemplate) {
584
- template = templates.find(t => t.id === selectedTemplate) || null;
585
- }
586
- }
587
- }
588
- // Prompt for title
589
- const { title: answerTitle } = await this.prompt([
590
- {
591
- type: 'input',
592
- name: 'title',
593
- message: 'Ticket title:',
594
- default: flags.title || template?.titlePattern,
595
- validate: (input) => input.trim() ? true : 'Title cannot be empty',
596
- },
597
- ], null);
598
- // Prompt for column
599
- const { column: answerColumn } = await this.prompt([
600
- {
601
- type: 'list',
602
- name: 'column',
603
- message: 'Column:',
604
- choices: columns.map(c => ({ name: c, value: c })),
605
- default: flags.column || columns[0],
606
- },
607
- ], null);
608
- // Prompt for priority (using workspace priority scale)
609
- const db = this.storage.getDatabase();
610
- const workspacePriorities = getWorkspacePriorities(db);
611
- const { priority: answerPriority } = await this.prompt([
612
- {
613
- type: 'list',
614
- name: 'priority',
615
- message: 'Priority:',
616
- choices: [
617
- { name: 'None', value: undefined },
618
- ...workspacePriorities.map(p => ({ name: p, value: p })),
619
- ],
620
- default: flags.priority || template?.defaultPriority,
621
- },
622
- ], null);
623
- // Prompt for category
624
- const { categoryChoice } = await this.prompt([
625
- {
626
- type: 'list',
627
- name: 'categoryChoice',
628
- message: 'Category:',
629
- choices: [
630
- { name: 'Skip (none)', value: '' },
631
- new inquirer.Separator('── Conventional Commits ──'),
632
- { name: 'feature - New feature or capability', value: 'feature' },
633
- { name: 'bug - Bug fix', value: 'bug' },
634
- { name: 'refactor - Code refactoring', value: 'refactor' },
635
- { name: 'docs - Documentation', value: 'docs' },
636
- { name: 'test - Test additions/fixes', value: 'test' },
637
- { name: 'chore - Maintenance tasks', value: 'chore' },
638
- { name: 'performance - Performance improvements', value: 'performance' },
639
- { name: 'ci - CI/CD changes', value: 'ci' },
640
- { name: 'build - Build system changes', value: 'build' },
641
- new inquirer.Separator('── Extended Types ──'),
642
- { name: 'security - Security fixes', value: 'security' },
643
- { name: 'database - Database migrations', value: 'database' },
644
- { name: 'release - Release preparation', value: 'release' },
645
- new inquirer.Separator('── 5Tool Founder ──'),
646
- { name: 'ship - Shipping and deployment', value: 'ship' },
647
- { name: 'growth - Growth and marketing', value: 'growth' },
648
- { name: 'support - Customer experience', value: 'support' },
649
- { name: 'strategy - Strategy and planning', value: 'strategy' },
650
- { name: 'ops - Business operations', value: 'ops' },
651
- new inquirer.Separator('───────────────────'),
652
- { name: 'Custom...', value: '__custom__' },
653
- ],
654
- default: flags.category || template?.defaultCategory || '',
655
- },
656
- ], null);
657
- // Custom category prompt if needed
658
- let customCategory;
659
- if (categoryChoice === '__custom__') {
660
- const result = await this.prompt([{
661
- type: 'input',
662
- name: 'customCategory',
663
- message: 'Enter custom category:',
664
- validate: (input) => input.trim() ? true : 'Category cannot be empty',
665
- }], null);
666
- customCategory = result.customCategory;
667
- }
668
- const answers = { title: answerTitle, column: answerColumn, priority: answerPriority, categoryChoice, customCategory };
669
- // Resolve category from choice or custom input
670
- const category = answers.categoryChoice === '__custom__'
671
- ? answers.customCategory
672
- : answers.categoryChoice || undefined;
673
- // Prompt for structured description (use template description if available)
674
- const description = await this.promptStructuredDescription(flags.description || template?.descriptionTemplate);
675
- // Parse labels from flag or use template defaults
676
- const labels = flags.labels
677
- ? flags.labels.split(',').map(l => l.trim()).filter(Boolean)
678
- : template?.defaultLabels;
679
- return {
680
- title: answers.title,
681
- statusName: answers.column,
682
- priority: answers.priority || undefined,
683
- category,
684
- description: description || undefined,
685
- id: flags.id,
686
- epicId: flags.epic,
687
- labels,
688
- };
689
- }
690
- async promptStructuredDescription(existingDescription) {
691
- // If description already provided via flag, use it
692
- if (existingDescription) {
693
- return existingDescription;
694
- }
695
- this.log(styles.muted('\n─── Ticket Description (for agent execution) ───'));
696
- // Prompt for "What" - the main outcome
697
- const { what } = await this.prompt([
698
- {
699
- type: 'input',
700
- name: 'what',
701
- message: 'What is the concrete outcome? (one sentence):',
702
- validate: (input) => input.trim() ? true : 'Outcome cannot be empty - what does success look like?',
703
- },
704
- ], null);
705
- // Prompt for acceptance criteria using multiline input
706
- const doneWhenResult = await multiLineInput({
707
- message: 'Done when (acceptance criteria):',
708
- hint: 'Enter each criterion on a new line. Ctrl+D to finish, Ctrl+C to cancel',
709
- });
710
- if (doneWhenResult.cancelled) {
711
- throw new Error('Ticket creation cancelled');
712
- }
713
- // Continue with remaining prompts
714
- const { context } = await this.prompt([
715
- {
716
- type: 'input',
717
- name: 'context',
718
- message: 'Context (files, patterns, hints - optional):',
719
- default: '',
720
- },
721
- ], null);
722
- const { notInScope } = await this.prompt([
723
- {
724
- type: 'input',
725
- name: 'notInScope',
726
- message: 'Not in scope (explicit exclusions - optional):',
727
- default: '',
728
- },
729
- ], null);
730
- // Build structured description
731
- const parts = [];
732
- parts.push(`## What\n${what}`);
733
- if (doneWhenResult.value.trim()) {
734
- // Ensure each line in doneWhen starts with - [ ] if it doesn't already
735
- const criteria = doneWhenResult.value
736
- .split('\n')
737
- .map(line => line.trim())
738
- .filter(line => line.length > 0)
739
- .map(line => {
740
- if (line.startsWith('- [ ]') || line.startsWith('- [x]')) {
741
- return line;
742
- }
743
- if (line.startsWith('-')) {
744
- return `- [ ]${line.slice(1)}`;
745
- }
746
- return `- [ ] ${line}`;
747
- })
748
- .join('\n');
749
- parts.push(`## Done when\n${criteria}`);
750
- }
751
- if (context.trim()) {
752
- parts.push(`## Context\n${context}`);
753
- }
754
- if (notInScope.trim()) {
755
- parts.push(`## Not in scope\n${notInScope}`);
756
- }
757
- return parts.join('\n\n');
758
- }
759
- }
760
- //# sourceMappingURL=create.js.map