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.
- package/README.md +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /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
|
-
}
|