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,359 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { confirm } from '@inquirer/prompts';
|
|
4
|
-
import { colors, info } from '@src/theme/index.ts';
|
|
5
|
-
import {
|
|
6
|
-
createSpinner,
|
|
7
|
-
emoji,
|
|
8
|
-
field,
|
|
9
|
-
fieldMultiline,
|
|
10
|
-
icons,
|
|
11
|
-
log,
|
|
12
|
-
printHeader,
|
|
13
|
-
printSeparator,
|
|
14
|
-
progressBar,
|
|
15
|
-
renderCard,
|
|
16
|
-
showError,
|
|
17
|
-
showSuccess,
|
|
18
|
-
showTip,
|
|
19
|
-
showWarning,
|
|
20
|
-
} from '@src/theme/ui.ts';
|
|
21
|
-
import { assertSprintStatus, getSprint, resolveSprintId, saveSprint } from '@src/store/sprint.ts';
|
|
22
|
-
import { getPendingRequirements } from '@src/store/ticket.ts';
|
|
23
|
-
import { getProject } from '@src/store/project.ts';
|
|
24
|
-
import { buildTicketRefinePrompt } from '@src/ai/prompts/index.ts';
|
|
25
|
-
import { fileExists } from '@src/utils/storage.ts';
|
|
26
|
-
import { getRefinementDir, getSchemaPath, getSprintDir } from '@src/utils/paths.ts';
|
|
27
|
-
import { IssueFetchError, fetchIssueFromUrl, formatIssueContext } from '@src/utils/issue-fetch.ts';
|
|
28
|
-
import { type RefinedRequirement } from '@src/schemas/index.ts';
|
|
29
|
-
import { exportRequirementsToMarkdown } from '@src/utils/requirements-export.ts';
|
|
30
|
-
import { resolveProvider, providerDisplayName } from '@src/utils/provider.ts';
|
|
31
|
-
import { formatTicketForPrompt, parseRequirementsFile, runAiSession } from '@src/commands/ticket/refine-utils.ts';
|
|
32
|
-
|
|
33
|
-
interface RefineOptions {
|
|
34
|
-
project?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function parseArgs(args: string[]): { sprintId?: string; options: RefineOptions } {
|
|
38
|
-
const options: RefineOptions = {};
|
|
39
|
-
let sprintId: string | undefined;
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < args.length; i++) {
|
|
42
|
-
const arg = args[i];
|
|
43
|
-
const nextArg = args[i + 1];
|
|
44
|
-
|
|
45
|
-
if (arg === '--project') {
|
|
46
|
-
options.project = nextArg;
|
|
47
|
-
i++;
|
|
48
|
-
} else if (!arg?.startsWith('-')) {
|
|
49
|
-
sprintId = arg;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { sprintId, options };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function sprintRefineCommand(args: string[]): Promise<void> {
|
|
57
|
-
const { sprintId, options } = parseArgs(args);
|
|
58
|
-
|
|
59
|
-
let id: string;
|
|
60
|
-
try {
|
|
61
|
-
id = await resolveSprintId(sprintId);
|
|
62
|
-
} catch {
|
|
63
|
-
showWarning('No sprint specified and no current sprint set.');
|
|
64
|
-
showTip('Specify a sprint ID or create one first.');
|
|
65
|
-
log.newline();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const sprint = await getSprint(id);
|
|
70
|
-
|
|
71
|
-
// Check sprint status - must be draft to refine
|
|
72
|
-
try {
|
|
73
|
-
assertSprintStatus(sprint, ['draft'], 'refine');
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (err instanceof Error) {
|
|
76
|
-
showError(err.message);
|
|
77
|
-
log.newline();
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (sprint.tickets.length === 0) {
|
|
83
|
-
showWarning('No tickets in sprint.');
|
|
84
|
-
showTip('Add tickets first: ralphctl ticket add --project <project-name>');
|
|
85
|
-
log.newline();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Get pending tickets (filter by project if specified)
|
|
90
|
-
let pendingTickets = getPendingRequirements(sprint.tickets);
|
|
91
|
-
if (options.project) {
|
|
92
|
-
pendingTickets = pendingTickets.filter((t) => t.projectName === options.project);
|
|
93
|
-
if (pendingTickets.length === 0) {
|
|
94
|
-
showWarning(`No pending tickets for project: ${options.project}`);
|
|
95
|
-
log.newline();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (pendingTickets.length === 0) {
|
|
101
|
-
showSuccess('All tickets already have approved requirements!');
|
|
102
|
-
showTip('Run "ralphctl sprint plan" to generate tasks.');
|
|
103
|
-
log.newline();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Show initial summary
|
|
108
|
-
printHeader('Requirements Refinement', icons.ticket);
|
|
109
|
-
console.log(field('Sprint', sprint.name));
|
|
110
|
-
console.log(field('ID', sprint.id));
|
|
111
|
-
console.log(field('Pending', `${String(pendingTickets.length)} ticket(s)`));
|
|
112
|
-
log.newline();
|
|
113
|
-
|
|
114
|
-
// Load schema once before processing tickets
|
|
115
|
-
const schemaPath = getSchemaPath('requirements-output.schema.json');
|
|
116
|
-
const schema = await readFile(schemaPath, 'utf-8');
|
|
117
|
-
|
|
118
|
-
// Resolve AI provider for display names
|
|
119
|
-
const providerName = providerDisplayName(await resolveProvider());
|
|
120
|
-
|
|
121
|
-
// Process tickets one by one
|
|
122
|
-
let approved = 0;
|
|
123
|
-
let skipped = 0;
|
|
124
|
-
|
|
125
|
-
for (let i = 0; i < pendingTickets.length; i++) {
|
|
126
|
-
const ticket = pendingTickets[i];
|
|
127
|
-
if (!ticket) continue;
|
|
128
|
-
|
|
129
|
-
const ticketNum = i + 1;
|
|
130
|
-
const totalTickets = pendingTickets.length;
|
|
131
|
-
|
|
132
|
-
// Show ticket card
|
|
133
|
-
printSeparator(60);
|
|
134
|
-
console.log('');
|
|
135
|
-
console.log(` ${icons.ticket} ${info(`Ticket ${String(ticketNum)} of ${String(totalTickets)}`)}`);
|
|
136
|
-
console.log(
|
|
137
|
-
` ${progressBar(i, totalTickets, {
|
|
138
|
-
width: 15,
|
|
139
|
-
showPercent: false,
|
|
140
|
-
})} ${colors.muted(`${String(ticketNum)}/${String(totalTickets)}`)}`
|
|
141
|
-
);
|
|
142
|
-
console.log('');
|
|
143
|
-
console.log(field('Title', ticket.title, 14));
|
|
144
|
-
console.log(field('Project', ticket.projectName, 14));
|
|
145
|
-
if (ticket.link) {
|
|
146
|
-
console.log(field('Link', ticket.link, 14));
|
|
147
|
-
}
|
|
148
|
-
if (ticket.description) {
|
|
149
|
-
console.log(fieldMultiline('Description', ticket.description, 14));
|
|
150
|
-
}
|
|
151
|
-
log.newline();
|
|
152
|
-
|
|
153
|
-
// Validate project exists
|
|
154
|
-
try {
|
|
155
|
-
await getProject(ticket.projectName);
|
|
156
|
-
} catch {
|
|
157
|
-
showWarning(`Project '${ticket.projectName}' not found.`);
|
|
158
|
-
log.dim('Skipping this ticket.');
|
|
159
|
-
log.newline();
|
|
160
|
-
skipped++;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Confirm before starting AI session
|
|
165
|
-
const proceed = await confirm({
|
|
166
|
-
message: `${emoji.donut} Start ${providerName} refinement session for this ticket?`,
|
|
167
|
-
default: true,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (!proceed) {
|
|
171
|
-
log.dim('Skipped. You can refine this ticket later.');
|
|
172
|
-
log.newline();
|
|
173
|
-
skipped++;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Fetch live issue data if ticket has an issue link
|
|
178
|
-
let issueContext = '';
|
|
179
|
-
if (ticket.link) {
|
|
180
|
-
const fetchSpinner = createSpinner('Fetching issue data...');
|
|
181
|
-
fetchSpinner.start();
|
|
182
|
-
try {
|
|
183
|
-
const issueData = fetchIssueFromUrl(ticket.link);
|
|
184
|
-
if (issueData) {
|
|
185
|
-
issueContext = formatIssueContext(issueData);
|
|
186
|
-
fetchSpinner.succeed(`Issue data fetched (${String(issueData.comments.length)} comment(s))`);
|
|
187
|
-
} else {
|
|
188
|
-
fetchSpinner.stop();
|
|
189
|
-
}
|
|
190
|
-
} catch (err) {
|
|
191
|
-
fetchSpinner.fail('Could not fetch issue data');
|
|
192
|
-
if (err instanceof IssueFetchError) {
|
|
193
|
-
showWarning(`${err.message} — continuing without issue context`);
|
|
194
|
-
} else if (err instanceof Error) {
|
|
195
|
-
showWarning(`${err.message} — continuing without issue context`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Prepare AI session - use sprint's refinement directory
|
|
201
|
-
const refineDir = getRefinementDir(id, ticket.id);
|
|
202
|
-
await mkdir(refineDir, { recursive: true });
|
|
203
|
-
const outputFile = join(refineDir, 'requirements.json');
|
|
204
|
-
const ticketContent = formatTicketForPrompt(ticket);
|
|
205
|
-
const prompt = buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext);
|
|
206
|
-
|
|
207
|
-
log.dim(`Working directory: ${refineDir}`);
|
|
208
|
-
log.dim(`Requirements output: ${outputFile}`);
|
|
209
|
-
log.newline();
|
|
210
|
-
|
|
211
|
-
const spinner = createSpinner(`Starting ${providerName} session...`);
|
|
212
|
-
spinner.start();
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await runAiSession(refineDir, prompt, ticket.title);
|
|
216
|
-
spinner.succeed(`${providerName} session completed`);
|
|
217
|
-
} catch (err) {
|
|
218
|
-
spinner.fail(`${providerName} session failed`);
|
|
219
|
-
if (err instanceof Error) {
|
|
220
|
-
showError(err.message);
|
|
221
|
-
}
|
|
222
|
-
log.newline();
|
|
223
|
-
skipped++;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
log.newline();
|
|
228
|
-
|
|
229
|
-
// Process the requirements file
|
|
230
|
-
if (await fileExists(outputFile)) {
|
|
231
|
-
let content: string;
|
|
232
|
-
try {
|
|
233
|
-
content = await readFile(outputFile, 'utf-8');
|
|
234
|
-
} catch {
|
|
235
|
-
showError(`Failed to read requirements file: ${outputFile}`);
|
|
236
|
-
log.newline();
|
|
237
|
-
skipped++;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
let refinedRequirements: RefinedRequirement[];
|
|
242
|
-
try {
|
|
243
|
-
refinedRequirements = parseRequirementsFile(content);
|
|
244
|
-
} catch (err) {
|
|
245
|
-
if (err instanceof Error) {
|
|
246
|
-
showError(`Failed to parse requirements file: ${err.message}`);
|
|
247
|
-
}
|
|
248
|
-
log.newline();
|
|
249
|
-
skipped++;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (refinedRequirements.length === 0) {
|
|
254
|
-
showWarning('No requirements found in output file.');
|
|
255
|
-
log.newline();
|
|
256
|
-
skipped++;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Find all matching requirements (Claude may output multiple for one ticket)
|
|
261
|
-
const matchingRequirements = refinedRequirements.filter((r) => r.ref === ticket.id || r.ref === ticket.title);
|
|
262
|
-
|
|
263
|
-
if (matchingRequirements.length === 0) {
|
|
264
|
-
showWarning('Requirement reference does not match this ticket.');
|
|
265
|
-
log.newline();
|
|
266
|
-
skipped++;
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Combine multiple requirements into one (safety net for split outputs)
|
|
271
|
-
const requirement: RefinedRequirement =
|
|
272
|
-
matchingRequirements.length === 1
|
|
273
|
-
? {
|
|
274
|
-
ref: matchingRequirements[0]?.ref ?? '',
|
|
275
|
-
requirements: matchingRequirements[0]?.requirements ?? '',
|
|
276
|
-
}
|
|
277
|
-
: {
|
|
278
|
-
ref: matchingRequirements[0]?.ref ?? '',
|
|
279
|
-
requirements: matchingRequirements
|
|
280
|
-
.map((r, idx) => {
|
|
281
|
-
const text = r.requirements.trim();
|
|
282
|
-
if (/^#\s/.test(text)) return text;
|
|
283
|
-
return `# ${String(idx + 1)}. Section ${String(idx + 1)}\n\n${text}`;
|
|
284
|
-
})
|
|
285
|
-
.join('\n\n---\n\n'),
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// Show requirement for review
|
|
289
|
-
const reqLines = requirement.requirements.split('\n');
|
|
290
|
-
console.log(renderCard(`${icons.ticket} Refined Requirements`, reqLines));
|
|
291
|
-
log.newline();
|
|
292
|
-
|
|
293
|
-
const approveRequirement = await confirm({
|
|
294
|
-
message: `${emoji.donut} Approve these requirements?`,
|
|
295
|
-
default: true,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
if (approveRequirement) {
|
|
299
|
-
// Save requirements to ticket
|
|
300
|
-
const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
|
|
301
|
-
const ticketToSave = sprint.tickets[ticketIdx];
|
|
302
|
-
if (ticketIdx !== -1 && ticketToSave) {
|
|
303
|
-
ticketToSave.requirements = requirement.requirements;
|
|
304
|
-
ticketToSave.requirementStatus = 'approved';
|
|
305
|
-
}
|
|
306
|
-
await saveSprint(sprint);
|
|
307
|
-
showSuccess('Requirements approved and saved!');
|
|
308
|
-
approved++;
|
|
309
|
-
} else {
|
|
310
|
-
log.dim('Requirements not approved. You can refine this ticket later.');
|
|
311
|
-
skipped++;
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
showWarning('No requirements file found from AI session.');
|
|
315
|
-
log.dim('You can refine this ticket later.');
|
|
316
|
-
skipped++;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
log.newline();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Final summary
|
|
323
|
-
printSeparator(60);
|
|
324
|
-
log.newline();
|
|
325
|
-
printHeader('Summary', icons.success);
|
|
326
|
-
console.log(field('Approved', String(approved)));
|
|
327
|
-
console.log(field('Skipped', String(skipped)));
|
|
328
|
-
console.log(field('Total', String(pendingTickets.length)));
|
|
329
|
-
log.newline();
|
|
330
|
-
|
|
331
|
-
// Re-read sprint to get the latest state after all saves
|
|
332
|
-
const updatedSprint = await getSprint(id);
|
|
333
|
-
const remainingPending = getPendingRequirements(updatedSprint.tickets);
|
|
334
|
-
|
|
335
|
-
if (remainingPending.length === 0) {
|
|
336
|
-
showSuccess('All requirements approved!');
|
|
337
|
-
|
|
338
|
-
// Auto-export requirements to sprint directory
|
|
339
|
-
const sprintDir = getSprintDir(id);
|
|
340
|
-
const outputPath = join(sprintDir, 'requirements.md');
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
await exportRequirementsToMarkdown(updatedSprint, outputPath);
|
|
344
|
-
log.dim(`Requirements saved to: ${outputPath}`);
|
|
345
|
-
} catch (err) {
|
|
346
|
-
if (err instanceof Error) {
|
|
347
|
-
showError(`Failed to write requirements: ${err.message}`);
|
|
348
|
-
} else {
|
|
349
|
-
showError('Failed to write requirements: Unknown error');
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
showTip('Run "ralphctl sprint plan" to generate tasks.');
|
|
354
|
-
} else {
|
|
355
|
-
log.info(`${String(remainingPending.length)} ticket(s) still pending.`);
|
|
356
|
-
showTip('Continue refinement with: ralphctl sprint refine');
|
|
357
|
-
}
|
|
358
|
-
log.newline();
|
|
359
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { getSprint, resolveSprintId } from '@src/store/sprint.ts';
|
|
3
|
-
import { getSprintDir } from '@src/utils/paths.ts';
|
|
4
|
-
import { exportRequirementsToMarkdown } from '@src/utils/requirements-export.ts';
|
|
5
|
-
import { field, icons, log, printHeader, showEmpty, showError, showSuccess, showWarning } from '@src/theme/ui.ts';
|
|
6
|
-
import { selectSprint } from '@src/interactive/selectors.ts';
|
|
7
|
-
|
|
8
|
-
export async function sprintRequirementsCommand(args: string[] = []): Promise<void> {
|
|
9
|
-
const sprintId = args.find((a) => !a.startsWith('-'));
|
|
10
|
-
|
|
11
|
-
let id: string;
|
|
12
|
-
try {
|
|
13
|
-
id = await resolveSprintId(sprintId);
|
|
14
|
-
} catch {
|
|
15
|
-
const selected = await selectSprint('Select sprint to export requirements from:');
|
|
16
|
-
if (!selected) return;
|
|
17
|
-
id = selected;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const sprint = await getSprint(id);
|
|
21
|
-
|
|
22
|
-
if (sprint.tickets.length === 0) {
|
|
23
|
-
showEmpty('tickets in this sprint', 'Add tickets first: ralphctl ticket add --project <name>');
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === 'approved');
|
|
28
|
-
if (approvedTickets.length === 0) {
|
|
29
|
-
showWarning('No approved requirements to export.');
|
|
30
|
-
log.dim('Refine requirements first: ralphctl sprint refine');
|
|
31
|
-
log.newline();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
printHeader('Export Requirements', icons.sprint);
|
|
36
|
-
console.log(field('Sprint', sprint.name));
|
|
37
|
-
console.log(field('Tickets', `${String(sprint.tickets.length)} total, ${String(approvedTickets.length)} approved`));
|
|
38
|
-
log.newline();
|
|
39
|
-
|
|
40
|
-
// Export to sprint directory
|
|
41
|
-
const sprintDir = getSprintDir(id);
|
|
42
|
-
const outputPath = join(sprintDir, 'requirements.md');
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
await exportRequirementsToMarkdown(sprint, outputPath);
|
|
46
|
-
showSuccess('Requirements written to:');
|
|
47
|
-
log.item(outputPath);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
if (err instanceof Error) {
|
|
50
|
-
showError(`Failed to write requirements: ${err.message}`);
|
|
51
|
-
} else {
|
|
52
|
-
showError('Failed to write requirements: Unknown error');
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
log.newline();
|
|
58
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { colors, muted } from '@src/theme/index.ts';
|
|
2
|
-
import { getSprint, resolveSprintId } from '@src/store/sprint.ts';
|
|
3
|
-
import { listTasks } from '@src/store/task.ts';
|
|
4
|
-
import { getCurrentSprint } from '@src/store/config.ts';
|
|
5
|
-
import { formatTicketDisplay, getPendingRequirements, groupTicketsByProject } from '@src/store/ticket.ts';
|
|
6
|
-
import {
|
|
7
|
-
badge,
|
|
8
|
-
formatSprintStatus,
|
|
9
|
-
formatTaskStatus,
|
|
10
|
-
horizontalLine,
|
|
11
|
-
icons,
|
|
12
|
-
labelValue,
|
|
13
|
-
log,
|
|
14
|
-
printCountSummary,
|
|
15
|
-
renderCard,
|
|
16
|
-
showNextStep,
|
|
17
|
-
} from '@src/theme/ui.ts';
|
|
18
|
-
import { selectSprint } from '@src/interactive/selectors.ts';
|
|
19
|
-
|
|
20
|
-
export async function sprintShowCommand(args: string[]): Promise<void> {
|
|
21
|
-
const sprintId = args[0];
|
|
22
|
-
|
|
23
|
-
let id: string;
|
|
24
|
-
try {
|
|
25
|
-
id = await resolveSprintId(sprintId);
|
|
26
|
-
} catch {
|
|
27
|
-
const selected = await selectSprint('Select sprint to show:');
|
|
28
|
-
if (!selected) return;
|
|
29
|
-
id = selected;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const sprint = await getSprint(id);
|
|
33
|
-
const tasks = await listTasks(id);
|
|
34
|
-
const currentSprintId = await getCurrentSprint();
|
|
35
|
-
const isCurrent = sprint.id === currentSprintId;
|
|
36
|
-
|
|
37
|
-
// Sprint info card
|
|
38
|
-
const infoLines: string[] = [
|
|
39
|
-
labelValue('ID', sprint.id + (isCurrent ? ' ' + badge('current', 'success') : '')),
|
|
40
|
-
labelValue('Status', formatSprintStatus(sprint.status)),
|
|
41
|
-
labelValue('Created', new Date(sprint.createdAt).toLocaleString()),
|
|
42
|
-
];
|
|
43
|
-
if (sprint.activatedAt) {
|
|
44
|
-
infoLines.push(labelValue('Activated', new Date(sprint.activatedAt).toLocaleString()));
|
|
45
|
-
}
|
|
46
|
-
if (sprint.closedAt) {
|
|
47
|
-
infoLines.push(labelValue('Closed', new Date(sprint.closedAt).toLocaleString()));
|
|
48
|
-
}
|
|
49
|
-
if (sprint.branch) {
|
|
50
|
-
infoLines.push(labelValue('Branch', sprint.branch));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
log.newline();
|
|
54
|
-
console.log(renderCard(`${icons.sprint} ${sprint.name}`, infoLines));
|
|
55
|
-
|
|
56
|
-
// Tickets card
|
|
57
|
-
log.newline();
|
|
58
|
-
const ticketLines: string[] = [];
|
|
59
|
-
|
|
60
|
-
if (sprint.tickets.length === 0) {
|
|
61
|
-
ticketLines.push(muted('No tickets yet'));
|
|
62
|
-
ticketLines.push(muted(`${icons.tip} Add with: ralphctl ticket add`));
|
|
63
|
-
} else {
|
|
64
|
-
const ticketsByProject = groupTicketsByProject(sprint.tickets);
|
|
65
|
-
let first = true;
|
|
66
|
-
for (const [projectName, tickets] of ticketsByProject) {
|
|
67
|
-
if (!first) ticketLines.push('');
|
|
68
|
-
first = false;
|
|
69
|
-
ticketLines.push(`${colors.info(icons.project)} ${colors.info(projectName)}`);
|
|
70
|
-
for (const ticket of tickets) {
|
|
71
|
-
const reqBadge =
|
|
72
|
-
ticket.requirementStatus === 'approved' ? badge('approved', 'success') : badge('pending', 'warning');
|
|
73
|
-
ticketLines.push(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
console.log(renderCard(`${icons.ticket} Tickets (${String(sprint.tickets.length)})`, ticketLines));
|
|
79
|
-
|
|
80
|
-
// Tasks card
|
|
81
|
-
log.newline();
|
|
82
|
-
const taskLines: string[] = [];
|
|
83
|
-
|
|
84
|
-
const tasksByStatus = {
|
|
85
|
-
todo: tasks.filter((t) => t.status === 'todo').length,
|
|
86
|
-
in_progress: tasks.filter((t) => t.status === 'in_progress').length,
|
|
87
|
-
done: tasks.filter((t) => t.status === 'done').length,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (tasks.length === 0) {
|
|
91
|
-
taskLines.push(muted('No tasks yet'));
|
|
92
|
-
taskLines.push(muted(`${icons.tip} Plan with: ralphctl sprint plan`));
|
|
93
|
-
} else {
|
|
94
|
-
// Status summary row
|
|
95
|
-
taskLines.push(
|
|
96
|
-
`${formatTaskStatus('todo')} ${String(tasksByStatus.todo)} ` +
|
|
97
|
-
`${formatTaskStatus('in_progress')} ${String(tasksByStatus.in_progress)} ` +
|
|
98
|
-
`${formatTaskStatus('done')} ${String(tasksByStatus.done)}`
|
|
99
|
-
);
|
|
100
|
-
taskLines.push(colors.muted(horizontalLine(40, 'rounded')));
|
|
101
|
-
|
|
102
|
-
// Task list
|
|
103
|
-
for (const task of tasks) {
|
|
104
|
-
const statusIcon =
|
|
105
|
-
task.status === 'done' ? icons.success : task.status === 'in_progress' ? icons.active : icons.inactive;
|
|
106
|
-
const statusColor = task.status === 'done' ? 'success' : task.status === 'in_progress' ? 'warning' : 'muted';
|
|
107
|
-
taskLines.push(`${muted(String(task.order) + '.')} ${badge(statusIcon, statusColor)} ${task.name}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
console.log(renderCard(`${icons.task} Tasks (${String(tasks.length)})`, taskLines));
|
|
112
|
-
|
|
113
|
-
// Progress summary (outside card for consistent formatting)
|
|
114
|
-
if (tasks.length > 0) {
|
|
115
|
-
printCountSummary('Progress', tasksByStatus.done, tasks.length);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// State-aware next steps
|
|
119
|
-
log.newline();
|
|
120
|
-
if (sprint.status === 'draft') {
|
|
121
|
-
const pendingCount = getPendingRequirements(sprint.tickets).length;
|
|
122
|
-
if (sprint.tickets.length === 0) {
|
|
123
|
-
showNextStep('ralphctl ticket add --project <name>', 'add tickets to this sprint');
|
|
124
|
-
} else if (pendingCount > 0) {
|
|
125
|
-
showNextStep('ralphctl sprint refine', 'refine ticket requirements');
|
|
126
|
-
} else if (tasks.length === 0) {
|
|
127
|
-
showNextStep('ralphctl sprint plan', 'generate tasks from tickets');
|
|
128
|
-
} else {
|
|
129
|
-
showNextStep('ralphctl sprint start', 'begin implementation');
|
|
130
|
-
}
|
|
131
|
-
} else if (sprint.status === 'active') {
|
|
132
|
-
if (tasksByStatus.done === tasks.length && tasks.length > 0) {
|
|
133
|
-
showNextStep('ralphctl sprint close', 'all tasks done — close the sprint');
|
|
134
|
-
} else {
|
|
135
|
-
showNextStep('ralphctl sprint start', 'continue implementation');
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
log.newline();
|
|
140
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { type RunnerOptions, runSprint } from '@src/ai/runner.ts';
|
|
2
|
-
import { SprintNotFoundError, SprintStatusError } from '@src/store/sprint.ts';
|
|
3
|
-
import { EXIT_ERROR, EXIT_NO_TASKS, exitWithCode } from '@src/utils/exit-codes.ts';
|
|
4
|
-
import { log, showError, showNextStep, showWarning } from '@src/theme/ui.ts';
|
|
5
|
-
|
|
6
|
-
function parseArgs(args: string[]): { sprintId?: string; options: RunnerOptions } {
|
|
7
|
-
const options: RunnerOptions = {
|
|
8
|
-
step: false,
|
|
9
|
-
count: null,
|
|
10
|
-
session: false,
|
|
11
|
-
noCommit: false,
|
|
12
|
-
};
|
|
13
|
-
let sprintId: string | undefined;
|
|
14
|
-
|
|
15
|
-
for (let i = 0; i < args.length; i++) {
|
|
16
|
-
const arg = args[i];
|
|
17
|
-
|
|
18
|
-
if (arg === '-t' || arg === '--step') {
|
|
19
|
-
options.step = true;
|
|
20
|
-
} else if (arg === '-s' || arg === '--session') {
|
|
21
|
-
options.session = true;
|
|
22
|
-
} else if (arg === '--no-commit') {
|
|
23
|
-
options.noCommit = true;
|
|
24
|
-
} else if (arg === '-c' || arg === '--count') {
|
|
25
|
-
const countStr = args[++i];
|
|
26
|
-
if (!countStr) {
|
|
27
|
-
throw new Error('--count requires a number');
|
|
28
|
-
}
|
|
29
|
-
const count = parseInt(countStr, 10);
|
|
30
|
-
if (isNaN(count) || count < 1 || count > 10000) {
|
|
31
|
-
throw new Error('--count must be an integer between 1 and 10000');
|
|
32
|
-
}
|
|
33
|
-
options.count = count;
|
|
34
|
-
} else if (arg === '--concurrency') {
|
|
35
|
-
const concStr = args[++i];
|
|
36
|
-
if (!concStr) {
|
|
37
|
-
throw new Error('--concurrency requires a number');
|
|
38
|
-
}
|
|
39
|
-
const conc = parseInt(concStr, 10);
|
|
40
|
-
if (isNaN(conc) || conc < 1 || conc > 10) {
|
|
41
|
-
throw new Error('--concurrency must be an integer between 1 and 10');
|
|
42
|
-
}
|
|
43
|
-
options.concurrency = conc;
|
|
44
|
-
} else if (arg === '--max-retries') {
|
|
45
|
-
const retryStr = args[++i];
|
|
46
|
-
if (!retryStr) {
|
|
47
|
-
throw new Error('--max-retries requires a number');
|
|
48
|
-
}
|
|
49
|
-
const retries = parseInt(retryStr, 10);
|
|
50
|
-
if (isNaN(retries) || retries < 0 || retries > 20) {
|
|
51
|
-
throw new Error('--max-retries must be an integer between 0 and 20');
|
|
52
|
-
}
|
|
53
|
-
options.maxRetries = retries;
|
|
54
|
-
} else if (arg === '--fail-fast') {
|
|
55
|
-
options.failFast = true;
|
|
56
|
-
} else if (arg === '--no-fail-fast') {
|
|
57
|
-
options.failFast = false;
|
|
58
|
-
} else if (arg === '-f' || arg === '--force') {
|
|
59
|
-
options.force = true;
|
|
60
|
-
} else if (arg === '--refresh-check') {
|
|
61
|
-
options.refreshCheck = true;
|
|
62
|
-
} else if (arg === '-b' || arg === '--branch') {
|
|
63
|
-
options.branch = true;
|
|
64
|
-
} else if (arg === '--branch-name') {
|
|
65
|
-
const nameStr = args[++i];
|
|
66
|
-
if (!nameStr) {
|
|
67
|
-
throw new Error('--branch-name requires a value');
|
|
68
|
-
}
|
|
69
|
-
options.branchName = nameStr;
|
|
70
|
-
} else if (!arg?.startsWith('-')) {
|
|
71
|
-
sprintId = arg;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { sprintId, options };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function sprintStartCommand(args: string[]): Promise<void> {
|
|
79
|
-
let sprintId: string | undefined;
|
|
80
|
-
let options: RunnerOptions;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const parsed = parseArgs(args);
|
|
84
|
-
sprintId = parsed.sprintId;
|
|
85
|
-
options = parsed.options;
|
|
86
|
-
} catch (err) {
|
|
87
|
-
if (err instanceof Error) {
|
|
88
|
-
showError(err.message);
|
|
89
|
-
log.newline();
|
|
90
|
-
}
|
|
91
|
-
exitWithCode(EXIT_ERROR);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const summary = await runSprint(sprintId, options);
|
|
96
|
-
|
|
97
|
-
// Exit with appropriate code based on execution summary
|
|
98
|
-
if (summary) {
|
|
99
|
-
exitWithCode(summary.exitCode);
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
if (err instanceof SprintNotFoundError) {
|
|
103
|
-
showError(`Sprint not found: ${sprintId ?? 'unknown'}`);
|
|
104
|
-
log.newline();
|
|
105
|
-
exitWithCode(EXIT_ERROR);
|
|
106
|
-
} else if (err instanceof SprintStatusError) {
|
|
107
|
-
showError(err.message);
|
|
108
|
-
log.newline();
|
|
109
|
-
exitWithCode(EXIT_ERROR);
|
|
110
|
-
} else if (err instanceof Error && err.message.includes('No sprint specified')) {
|
|
111
|
-
showWarning('No sprint specified and no active sprint set.');
|
|
112
|
-
showNextStep('ralphctl sprint start <id>', 'specify a sprint ID');
|
|
113
|
-
log.newline();
|
|
114
|
-
exitWithCode(EXIT_NO_TASKS);
|
|
115
|
-
} else {
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { setCurrentSprint } from '@src/store/config.ts';
|
|
2
|
-
import { getSprint } from '@src/store/sprint.ts';
|
|
3
|
-
import { selectSprint } from '@src/interactive/selectors.ts';
|
|
4
|
-
import { log, showSuccess } from '@src/theme/ui.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Quick sprint switcher for interactive mode
|
|
8
|
-
*/
|
|
9
|
-
export async function sprintSwitchCommand(): Promise<void> {
|
|
10
|
-
const selectedId = await selectSprint('Select sprint to switch to:');
|
|
11
|
-
if (!selectedId) return;
|
|
12
|
-
|
|
13
|
-
await setCurrentSprint(selectedId);
|
|
14
|
-
const sprint = await getSprint(selectedId);
|
|
15
|
-
showSuccess('Switched to sprint!', [
|
|
16
|
-
['ID', sprint.id],
|
|
17
|
-
['Name', sprint.name],
|
|
18
|
-
]);
|
|
19
|
-
log.newline();
|
|
20
|
-
}
|