ralphctl 0.1.0 → 0.1.2

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 (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -1,265 +0,0 @@
1
- import { confirm, input, select } from '@inquirer/prompts';
2
- import { error, muted } from '@src/theme/index.ts';
3
- import {
4
- createSpinner,
5
- emoji,
6
- field,
7
- fieldMultiline,
8
- icons,
9
- log,
10
- renderCard,
11
- showEmpty,
12
- showError,
13
- showSuccess,
14
- showWarning,
15
- } from '@src/theme/ui.ts';
16
- import { editorInput } from '@src/utils/editor-input.ts';
17
- import { addTicket } from '@src/store/ticket.ts';
18
- import { listProjects, projectExists } from '@src/store/project.ts';
19
- import { SprintStatusError } from '@src/store/sprint.ts';
20
- import { EXIT_ERROR, exitWithCode } from '@src/utils/exit-codes.ts';
21
- import { IssueFetchError, fetchIssueFromUrl, type IssueData } from '@src/utils/issue-fetch.ts';
22
- import type { Ticket } from '@src/schemas/index.ts';
23
-
24
- export interface TicketAddOptions {
25
- title?: string;
26
- description?: string;
27
- link?: string;
28
- project?: string;
29
- interactive?: boolean; // Set by REPL or CLI (default true unless --no-interactive)
30
- }
31
-
32
- /**
33
- * Attempt to fetch issue data from a URL. Returns the data if the user confirms,
34
- * undefined otherwise. Non-fatal — warns on failure and returns undefined.
35
- */
36
- function tryFetchIssue(url: string): IssueData | undefined {
37
- const spinner = createSpinner('Fetching issue data...');
38
- spinner.start();
39
-
40
- let data: IssueData | null;
41
- try {
42
- data = fetchIssueFromUrl(url);
43
- } catch (err) {
44
- spinner.fail('Could not fetch issue data');
45
- if (err instanceof IssueFetchError) {
46
- showWarning(err.message);
47
- } else if (err instanceof Error) {
48
- showWarning(err.message);
49
- }
50
- log.newline();
51
- return undefined;
52
- }
53
-
54
- if (!data) {
55
- spinner.stop();
56
- return undefined;
57
- }
58
-
59
- spinner.succeed('Issue data fetched');
60
- log.newline();
61
-
62
- // Show summary card
63
- const bodyPreview = data.body.length > 200 ? data.body.slice(0, 200) + '...' : data.body;
64
- const cardLines = [`Title: ${data.title}`, '', bodyPreview];
65
- if (data.comments.length > 0) {
66
- cardLines.push('', `${String(data.comments.length)} comment(s)`);
67
- }
68
- console.log(renderCard(`${icons.info} Fetched Issue`, cardLines));
69
- log.newline();
70
-
71
- return data;
72
- }
73
-
74
- function validateUrl(url: string): boolean {
75
- try {
76
- new URL(url);
77
- return true;
78
- } catch {
79
- return false;
80
- }
81
- }
82
-
83
- /**
84
- * Non-interactive ticket creation: validates params and creates ticket.
85
- */
86
- async function addSingleTicketNonInteractive(options: TicketAddOptions): Promise<void> {
87
- const errors: string[] = [];
88
- const trimmedTitle = options.title?.trim();
89
- const trimmedProject = options.project?.trim();
90
-
91
- if (!trimmedTitle) {
92
- errors.push('--title is required');
93
- }
94
- if (!trimmedProject) {
95
- errors.push('--project is required');
96
- } else if (!(await projectExists(trimmedProject))) {
97
- errors.push(`Project '${trimmedProject}' does not exist. Add it first with 'ralphctl project add'.`);
98
- }
99
- if (options.link && !validateUrl(options.link)) {
100
- errors.push('--link must be a valid URL');
101
- }
102
-
103
- if (errors.length > 0 || !trimmedTitle || !trimmedProject) {
104
- showError('Validation failed');
105
- for (const e of errors) {
106
- log.item(error(e));
107
- }
108
- log.newline();
109
- exitWithCode(EXIT_ERROR);
110
- }
111
-
112
- const title = trimmedTitle;
113
- const trimmedDesc = options.description?.trim();
114
- const description = trimmedDesc === '' ? undefined : trimmedDesc;
115
- const trimmedLink = options.link?.trim();
116
- const link = trimmedLink === '' ? undefined : trimmedLink;
117
- const projectName = trimmedProject;
118
-
119
- try {
120
- const ticket = await addTicket({
121
- title,
122
- description,
123
- link,
124
- projectName,
125
- });
126
-
127
- showTicketResult(ticket);
128
- } catch (err) {
129
- handleTicketError(err);
130
- }
131
- }
132
-
133
- /**
134
- * Interactive ticket creation: prompts for fields and creates ticket.
135
- * Returns the created ticket on success, or null on failure.
136
- */
137
- export async function addSingleTicketInteractive(options: TicketAddOptions): Promise<Ticket | null> {
138
- const projects = await listProjects();
139
-
140
- if (projects.length === 0) {
141
- showEmpty('projects', 'Add one first with: ralphctl project add');
142
- return null;
143
- }
144
-
145
- const projectName = await select({
146
- message: `${icons.project} Project:`,
147
- default: options.project ?? projects[0]?.name,
148
- choices: projects.map((p) => ({
149
- name: `${icons.project} ${p.name} ${muted(`- ${p.displayName}`)}`,
150
- value: p.name,
151
- })),
152
- });
153
-
154
- // Link prompt first — enables issue fetching for pre-fill
155
- const link: string | undefined = await input({
156
- message: `${icons.info} Issue link (optional):`,
157
- default: options.link?.trim(),
158
- validate: (v) => {
159
- if (!v) return true;
160
- return validateUrl(v) ? true : 'Invalid URL format';
161
- },
162
- });
163
-
164
- const trimmedLink = link.trim();
165
- const normalizedLink = trimmedLink === '' ? undefined : trimmedLink;
166
-
167
- // Try to fetch issue data if a valid issue URL was provided
168
- let prefill: IssueData | undefined;
169
- if (normalizedLink) {
170
- prefill = tryFetchIssue(normalizedLink);
171
- }
172
-
173
- let title = await input({
174
- message: `${icons.ticket} Title:`,
175
- default: prefill?.title ?? options.title?.trim(),
176
- validate: (v) => (v.trim().length > 0 ? true : 'Title is required'),
177
- });
178
-
179
- const description = await editorInput({
180
- message: 'Description (recommended):',
181
- default: prefill?.body ?? options.description?.trim(),
182
- });
183
-
184
- // Trim and normalize empty strings to undefined
185
- title = title.trim();
186
- const trimmedDescription = description.trim();
187
- const normalizedDescription = trimmedDescription === '' ? undefined : trimmedDescription;
188
-
189
- try {
190
- const ticket = await addTicket({
191
- title,
192
- description: normalizedDescription,
193
- link: normalizedLink,
194
- projectName,
195
- });
196
-
197
- showTicketResult(ticket);
198
- return ticket;
199
- } catch (err) {
200
- handleTicketError(err);
201
- return null;
202
- }
203
- }
204
-
205
- /**
206
- * Display the result of a successfully added ticket.
207
- */
208
- function showTicketResult(ticket: Ticket): void {
209
- showSuccess('Ticket added!', [
210
- ['ID', ticket.id],
211
- ['Title', ticket.title],
212
- ['Project', ticket.projectName],
213
- ]);
214
-
215
- if (ticket.description) {
216
- console.log(fieldMultiline('Description', ticket.description));
217
- }
218
- if (ticket.link) {
219
- console.log(field('Link', ticket.link));
220
- }
221
- console.log('');
222
- }
223
-
224
- /**
225
- * Handle known ticket creation errors with user-friendly messages.
226
- */
227
- function handleTicketError(err: unknown): void {
228
- if (err instanceof SprintStatusError) {
229
- showError(err.message);
230
- } else if (err instanceof Error && err.message.includes('does not exist')) {
231
- showError(err.message);
232
- } else {
233
- throw err;
234
- }
235
- }
236
-
237
- export async function ticketAddCommand(options: TicketAddOptions = {}): Promise<void> {
238
- if (options.interactive === false) {
239
- await addSingleTicketNonInteractive(options);
240
- return;
241
- }
242
-
243
- // Interactive mode with batch loop
244
- let count = 0;
245
- let lastProjectName: string | undefined = options.project;
246
-
247
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- loop control via break
248
- while (true) {
249
- const ticket = await addSingleTicketInteractive({ ...options, project: lastProjectName });
250
- if (ticket) {
251
- count++;
252
- lastProjectName = ticket.projectName;
253
- log.dim(`${String(count)} ticket(s) added in this session`);
254
- } else {
255
- // No ticket created (no projects, or unrecoverable error) — exit loop
256
- break;
257
- }
258
-
259
- const another = await confirm({
260
- message: `${emoji.donut} Add another ticket?`,
261
- default: true,
262
- });
263
- if (!another) break;
264
- }
265
- }
@@ -1,166 +0,0 @@
1
- import { input } from '@inquirer/prompts';
2
- import { muted } from '@src/theme/index.ts';
3
- import { field, fieldMultiline, icons, showError, showNextStep, showSuccess } from '@src/theme/ui.ts';
4
- import { formatTicketDisplay, getTicket, TicketNotFoundError, updateTicket } from '@src/store/ticket.ts';
5
- import { SprintStatusError } from '@src/store/sprint.ts';
6
- import { EXIT_ERROR, exitWithCode } from '@src/utils/exit-codes.ts';
7
- import { selectTicket } from '@src/interactive/selectors.ts';
8
- import { editorInput } from '@src/utils/editor-input.ts';
9
-
10
- export interface TicketEditOptions {
11
- title?: string;
12
- description?: string;
13
- link?: string;
14
- interactive?: boolean;
15
- }
16
-
17
- function validateUrl(url: string): boolean {
18
- try {
19
- new URL(url);
20
- return true;
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- export async function ticketEditCommand(ticketId?: string, options: TicketEditOptions = {}): Promise<void> {
27
- const isInteractive = options.interactive !== false;
28
-
29
- // Get ticket ID
30
- let resolvedId = ticketId;
31
- if (!resolvedId) {
32
- if (!isInteractive) {
33
- showError('Ticket ID is required in non-interactive mode');
34
- exitWithCode(EXIT_ERROR);
35
- }
36
-
37
- const selected = await selectTicket('Select ticket to edit:');
38
- if (!selected) {
39
- return;
40
- }
41
- resolvedId = selected;
42
- }
43
-
44
- // Fetch existing ticket
45
- let ticket;
46
- try {
47
- ticket = await getTicket(resolvedId);
48
- } catch (err) {
49
- if (err instanceof TicketNotFoundError) {
50
- showError(`Ticket not found: ${resolvedId}`);
51
- showNextStep('ralphctl ticket list', 'see available tickets');
52
- if (!isInteractive) exitWithCode(EXIT_ERROR);
53
- return;
54
- }
55
- throw err;
56
- }
57
-
58
- let newTitle: string | undefined;
59
- let newDescription: string | undefined;
60
- let newLink: string | undefined;
61
-
62
- if (isInteractive) {
63
- // Show current ticket info
64
- console.log(`\n Editing: ${formatTicketDisplay(ticket)}`);
65
- console.log(muted(` Project: ${ticket.projectName} (read-only)\n`));
66
-
67
- // Prompt for each field with current value as default
68
- newTitle = await input({
69
- message: `${icons.ticket} Title:`,
70
- default: ticket.title,
71
- validate: (v) => (v.trim().length > 0 ? true : 'Title is required'),
72
- });
73
-
74
- newDescription = await editorInput({
75
- message: 'Description:',
76
- default: ticket.description,
77
- });
78
-
79
- newLink = await input({
80
- message: `${icons.info} Link:`,
81
- default: ticket.link ?? '',
82
- validate: (v) => {
83
- if (!v) return true;
84
- return validateUrl(v) ? true : 'Invalid URL format';
85
- },
86
- });
87
-
88
- // Trim and normalize empty values
89
- newTitle = newTitle.trim();
90
- newDescription = newDescription.trim() || undefined;
91
- newLink = newLink.trim() || undefined;
92
- } else {
93
- // Non-interactive mode: use provided options
94
- if (options.title !== undefined) {
95
- const trimmed = options.title.trim();
96
- if (trimmed.length === 0) {
97
- showError('--title cannot be empty');
98
- exitWithCode(EXIT_ERROR);
99
- }
100
- newTitle = trimmed;
101
- }
102
-
103
- if (options.description !== undefined) {
104
- newDescription = options.description.trim() || undefined;
105
- }
106
-
107
- if (options.link !== undefined) {
108
- const trimmed = options.link.trim();
109
- if (trimmed && !validateUrl(trimmed)) {
110
- showError('--link must be a valid URL');
111
- exitWithCode(EXIT_ERROR);
112
- }
113
- newLink = trimmed || undefined;
114
- }
115
-
116
- // Check if any updates were provided
117
- if (newTitle === undefined && newDescription === undefined && newLink === undefined) {
118
- showError('No updates provided. Use --title, --description, or --link.');
119
- exitWithCode(EXIT_ERROR);
120
- }
121
- }
122
-
123
- // Build updates object (only include changed fields)
124
- const updates: { title?: string; description?: string; link?: string } = {};
125
-
126
- if (newTitle !== undefined && newTitle !== ticket.title) {
127
- updates.title = newTitle;
128
- }
129
- if (newDescription !== undefined && newDescription !== ticket.description) {
130
- updates.description = newDescription;
131
- }
132
- if (newLink !== undefined && newLink !== ticket.link) {
133
- updates.link = newLink;
134
- }
135
-
136
- // Check if anything changed
137
- if (Object.keys(updates).length === 0) {
138
- console.log(muted('\n No changes made.\n'));
139
- return;
140
- }
141
-
142
- try {
143
- const updated = await updateTicket(ticket.id, updates);
144
-
145
- showSuccess('Ticket updated!', [
146
- ['ID', updated.id],
147
- ['Title', updated.title],
148
- ['Project', updated.projectName],
149
- ]);
150
-
151
- if (updated.description) {
152
- console.log(fieldMultiline('Description', updated.description));
153
- }
154
- if (updated.link) {
155
- console.log(field('Link', updated.link));
156
- }
157
- console.log('');
158
- } catch (err) {
159
- if (err instanceof SprintStatusError) {
160
- showError(err.message);
161
- } else {
162
- throw err;
163
- }
164
- if (!isInteractive) exitWithCode(EXIT_ERROR);
165
- }
166
- }
@@ -1,114 +0,0 @@
1
- import type { Command } from 'commander';
2
- import { ticketAddCommand } from '@src/commands/ticket/add.ts';
3
- import { ticketEditCommand } from '@src/commands/ticket/edit.ts';
4
- import { ticketListCommand } from '@src/commands/ticket/list.ts';
5
- import { ticketShowCommand } from '@src/commands/ticket/show.ts';
6
- import { ticketRemoveCommand } from '@src/commands/ticket/remove.ts';
7
- import { ticketRefineCommand } from '@src/commands/ticket/refine.ts';
8
-
9
- export function registerTicketCommands(program: Command): void {
10
- const ticket = program.command('ticket').description('Manage tickets');
11
-
12
- ticket.addHelpText(
13
- 'after',
14
- `
15
- Examples:
16
- $ ralphctl ticket add --project api --title "Fix auth bug"
17
- $ ralphctl ticket edit abc123 --title "New title"
18
- $ ralphctl ticket list -b
19
- $ ralphctl ticket show abc123
20
- `
21
- );
22
-
23
- ticket
24
- .command('add')
25
- .description('Add ticket to current sprint')
26
- .option('-p, --project <name>', 'Project name')
27
- .option('-t, --title <title>', 'Ticket title')
28
- .option('-d, --description <desc>', 'Description')
29
- .option('--link <url>', 'Link to external issue')
30
- .option('-n, --no-interactive', 'Non-interactive mode (error on missing params)')
31
- .action(
32
- async (opts: {
33
- project?: string;
34
- title?: string;
35
- description?: string;
36
- link?: string;
37
- interactive?: boolean;
38
- }) => {
39
- await ticketAddCommand({
40
- project: opts.project,
41
- title: opts.title,
42
- description: opts.description,
43
- link: opts.link,
44
- // --no-interactive sets interactive=false, otherwise true (prompt for missing)
45
- interactive: opts.interactive !== false,
46
- });
47
- }
48
- );
49
-
50
- ticket
51
- .command('edit [id]')
52
- .description('Edit an existing ticket')
53
- .option('--title <title>', 'New title')
54
- .option('--description <desc>', 'New description')
55
- .option('--link <url>', 'New link')
56
- .option('-n, --no-interactive', 'Non-interactive mode')
57
- .action(
58
- async (
59
- id?: string,
60
- opts?: {
61
- title?: string;
62
- description?: string;
63
- link?: string;
64
- interactive?: boolean;
65
- }
66
- ) => {
67
- await ticketEditCommand(id, {
68
- title: opts?.title,
69
- description: opts?.description,
70
- link: opts?.link,
71
- interactive: opts?.interactive !== false,
72
- });
73
- }
74
- );
75
-
76
- ticket
77
- .command('list')
78
- .description('List tickets')
79
- .option('-b, --brief', 'Brief one-liner format')
80
- .option('--project <name>', 'Filter by project')
81
- .option('--status <status>', 'Filter by requirement status (pending, approved)')
82
- .action(async (opts: { brief?: boolean; project?: string; status?: string }) => {
83
- const args: string[] = [];
84
- if (opts.brief) args.push('-b');
85
- if (opts.project) args.push('--project', opts.project);
86
- if (opts.status) args.push('--status', opts.status);
87
- await ticketListCommand(args);
88
- });
89
-
90
- ticket
91
- .command('show [id]')
92
- .description('Show ticket details')
93
- .action(async (id?: string) => {
94
- await ticketShowCommand(id ? [id] : []);
95
- });
96
-
97
- ticket
98
- .command('refine [id]')
99
- .description('Re-refine an approved ticket')
100
- .action(async (id?: string) => {
101
- await ticketRefineCommand(id);
102
- });
103
-
104
- ticket
105
- .command('remove [id]')
106
- .description('Remove a ticket')
107
- .option('-y, --yes', 'Skip confirmation')
108
- .action(async (id?: string, opts?: { yes?: boolean }) => {
109
- const args: string[] = [];
110
- if (id) args.push(id);
111
- if (opts?.yes) args.push('-y');
112
- await ticketRemoveCommand(args);
113
- });
114
- }
@@ -1,128 +0,0 @@
1
- import { colors, muted, success } from '@src/theme/index.ts';
2
- import { formatTicketDisplay, groupTicketsByProject, listTickets } from '@src/store/ticket.ts';
3
- import { getProject } from '@src/store/project.ts';
4
- import { RequirementStatusSchema } from '@src/schemas/index.ts';
5
- import { badge, icons, log, printHeader, showEmpty, showError } from '@src/theme/ui.ts';
6
-
7
- interface TicketListFilters {
8
- brief: boolean;
9
- projectFilter?: string;
10
- statusFilter?: string;
11
- }
12
-
13
- function parseListArgs(args: string[]): TicketListFilters {
14
- const result: TicketListFilters = {
15
- brief: false,
16
- };
17
-
18
- for (let i = 0; i < args.length; i++) {
19
- const arg = args[i];
20
- const next = args[i + 1];
21
- if (arg === '-b' || arg === '--brief') result.brief = true;
22
- else if (arg === '--project' && next) {
23
- result.projectFilter = next;
24
- i++;
25
- } else if (arg === '--status' && next) {
26
- result.statusFilter = next;
27
- i++;
28
- }
29
- }
30
- return result;
31
- }
32
-
33
- function buildFilterSummary(filters: TicketListFilters): string {
34
- const parts: string[] = [];
35
- if (filters.projectFilter) parts.push(`project=${filters.projectFilter}`);
36
- if (filters.statusFilter) parts.push(`status=${filters.statusFilter}`);
37
- return parts.length > 0 ? ` (filtered: ${parts.join(', ')})` : '';
38
- }
39
-
40
- export async function ticketListCommand(args: string[]): Promise<void> {
41
- const { brief, projectFilter, statusFilter } = parseListArgs(args);
42
-
43
- // Validate status filter
44
- if (statusFilter) {
45
- const result = RequirementStatusSchema.safeParse(statusFilter);
46
- if (!result.success) {
47
- showError(`Invalid status: "${statusFilter}". Valid values: pending, approved`);
48
- return;
49
- }
50
- }
51
-
52
- const tickets = await listTickets();
53
-
54
- if (tickets.length === 0) {
55
- showEmpty('tickets', 'Add one with: ralphctl ticket add --project <project-name>');
56
- return;
57
- }
58
-
59
- // Apply filters
60
- let filtered = tickets;
61
- if (projectFilter) filtered = filtered.filter((t) => t.projectName === projectFilter);
62
- if (statusFilter) filtered = filtered.filter((t) => t.requirementStatus === statusFilter);
63
-
64
- const filterStr = buildFilterSummary({ brief, projectFilter, statusFilter });
65
- const isFiltered = filtered.length !== tickets.length;
66
-
67
- if (filtered.length === 0) {
68
- showEmpty('matching tickets', 'Try adjusting your filters');
69
- return;
70
- }
71
-
72
- if (brief) {
73
- // Brief mode: one line per ticket (markdown for LLM readability)
74
- const countLabel = isFiltered ? `${String(filtered.length)} of ${String(tickets.length)}` : String(tickets.length);
75
- console.log(`\n# Tickets (${countLabel})${filterStr}\n`);
76
- for (const ticket of filtered) {
77
- const display = `[${ticket.id}] ${ticket.title}`;
78
- const reqBadge = ticket.requirementStatus === 'approved' ? ' [approved]' : ' [pending]';
79
- console.log(`- ${display}${reqBadge} (${ticket.projectName})`);
80
- }
81
- console.log('');
82
- return;
83
- }
84
-
85
- // Interactive list grouped by project
86
- const ticketsByProject = groupTicketsByProject(filtered);
87
-
88
- printHeader(`Tickets (${String(filtered.length)})`, icons.ticket);
89
-
90
- for (const [projectName, projectTickets] of ticketsByProject) {
91
- // Project group header
92
- log.raw(`${colors.info(icons.project)} ${colors.info(projectName)}`);
93
-
94
- // Show project repos
95
- try {
96
- const project = await getProject(projectName);
97
- for (const repo of project.repositories) {
98
- log.raw(` ${muted(repo.name)} ${muted('→')} ${muted(repo.path)}`, 1);
99
- }
100
- } catch {
101
- log.raw(` ${muted('(project not found)')}`, 1);
102
- }
103
- log.newline();
104
-
105
- for (const ticket of projectTickets) {
106
- const reqBadge =
107
- ticket.requirementStatus === 'approved' ? badge('approved', 'success') : badge('pending', 'muted');
108
- log.raw(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
109
- if (ticket.description) {
110
- const preview = ticket.description.split('\n')[0] ?? '';
111
- const truncated = preview.length > 60 ? preview.slice(0, 57) + '...' : preview;
112
- log.raw(` ${muted(truncated)}`, 1);
113
- }
114
- }
115
- log.newline();
116
- }
117
-
118
- // Summary
119
- const approved = filtered.filter((t) => t.requirementStatus === 'approved').length;
120
- log.dim(
121
- `Requirements: ${success(`${String(approved)} approved`)} / ${muted(`${String(filtered.length - approved)} pending`)}`
122
- );
123
- const showingLabel = isFiltered
124
- ? `Showing ${String(filtered.length)} of ${String(tickets.length)} ticket(s)${filterStr}`
125
- : `Showing ${String(tickets.length)} ticket(s)`;
126
- log.dim(showingLabel);
127
- log.newline();
128
- }