ralphctl 0.2.5 → 0.3.1
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/dist/add-CIM72NE3.mjs +18 -0
- package/dist/add-GX7P7XTT.mjs +16 -0
- package/dist/bootstrap-FMHG6DRY.mjs +11 -0
- package/dist/chunk-3QBEBKMZ.mjs +103 -0
- package/dist/{chunk-EDJX7TT6.mjs → chunk-57UWLHRH.mjs} +22 -2
- package/dist/chunk-747KW2RW.mjs +24 -0
- package/dist/chunk-7JLZQICD.mjs +228 -0
- package/dist/{chunk-7TG3EAQ2.mjs → chunk-CFUVE2BP.mjs} +1 -5
- package/dist/chunk-CSC4TBJB.mjs +5546 -0
- package/dist/{chunk-IB6OCKZW.mjs → chunk-CTP2A436.mjs} +60 -55
- package/dist/{chunk-UBPZHHCD.mjs → chunk-D2YGPLIV.mjs} +84 -41
- package/dist/chunk-EPDR6VO5.mjs +5109 -0
- package/dist/{chunk-QBXHAXHI.mjs → chunk-FKMKOWLA.mjs} +154 -208
- package/dist/{chunk-OEUJDSHY.mjs → chunk-IWXBJD2D.mjs} +1 -1
- package/dist/chunk-JOQO4HMM.mjs +269 -0
- package/dist/{chunk-EUNAUHC3.mjs → chunk-NUYQK5MN.mjs} +80 -29
- package/dist/{chunk-JRFOUFD3.mjs → chunk-YCDUVPRT.mjs} +32 -52
- package/dist/cli.mjs +171 -3996
- package/dist/create-7WFSCMP4.mjs +15 -0
- package/dist/{handle-TA4MYNQJ.mjs → handle-BBAZJ44Y.mjs} +2 -2
- package/dist/mount-U7QXVB5Q.mjs +6804 -0
- package/dist/{project-YONEJICR.mjs → project-2IE7VWDB.mjs} +9 -5
- package/dist/prompts/harness-context.md +3 -3
- package/dist/prompts/ideate-auto.md +8 -10
- package/dist/prompts/ideate.md +3 -2
- package/dist/prompts/plan-auto.md +12 -12
- package/dist/prompts/plan-common.md +47 -19
- package/dist/prompts/plan-interactive.md +8 -8
- package/dist/prompts/signals-evaluation.md +1 -1
- package/dist/prompts/sprint-feedback.md +48 -0
- package/dist/prompts/task-evaluation-resume.md +12 -5
- package/dist/prompts/task-evaluation.md +37 -33
- package/dist/prompts/task-execution.md +33 -24
- package/dist/prompts/ticket-refine.md +6 -5
- package/dist/prompts/validation-checklist.md +10 -10
- package/dist/{resolver-RXEY6EJE.mjs → resolver-EOE5WUMV.mjs} +5 -5
- package/dist/{sprint-FGLWYWKX.mjs → sprint-OGOFEJJH.mjs} +7 -9
- package/dist/start-WG7VMEB2.mjs +17 -0
- package/package.json +15 -13
- package/dist/add-3T225IX5.mjs +0 -16
- package/dist/add-6A5432U2.mjs +0 -16
- package/dist/chunk-742XQ7FL.mjs +0 -551
- package/dist/chunk-7LZ6GOGN.mjs +0 -53
- package/dist/chunk-CSICORGV.mjs +0 -4333
- package/dist/chunk-DUU5346E.mjs +0 -59
- package/dist/create-MYGOWO2F.mjs +0 -12
- package/dist/multiline-OHSNFCRG.mjs +0 -40
- package/dist/wizard-XZ7OGBCJ.mjs +0 -193
- package/schemas/config.schema.json +0 -30
- package/schemas/ideate-output.schema.json +0 -22
- package/schemas/projects.schema.json +0 -58
- package/schemas/requirements-output.schema.json +0 -24
- package/schemas/sprint.schema.json +0 -109
- package/schemas/task-import.schema.json +0 -56
- package/schemas/tasks.schema.json +0 -98
package/dist/cli.mjs
CHANGED
|
@@ -1,3849 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
assertSprintStatus,
|
|
92
|
-
closeSprint,
|
|
93
|
-
deleteSprint,
|
|
94
|
-
getAiProvider,
|
|
95
|
-
getConfig,
|
|
96
|
-
getCurrentSprint,
|
|
97
|
-
getCurrentSprintOrThrow,
|
|
98
|
-
getEditor,
|
|
99
|
-
getEvaluationIterations,
|
|
100
|
-
getProgress,
|
|
101
|
-
getSprint,
|
|
102
|
-
listSprints,
|
|
103
|
-
logProgress,
|
|
104
|
-
resolveSprintId,
|
|
105
|
-
saveSprint,
|
|
106
|
-
setAiProvider,
|
|
107
|
-
setCurrentSprint,
|
|
108
|
-
setEditor,
|
|
109
|
-
setEvaluationIterations,
|
|
110
|
-
withFileLock
|
|
111
|
-
} from "./chunk-JRFOUFD3.mjs";
|
|
112
|
-
import {
|
|
113
|
-
ensureError,
|
|
114
|
-
wrapAsync
|
|
115
|
-
} from "./chunk-OEUJDSHY.mjs";
|
|
116
|
-
import {
|
|
117
|
-
AiProviderSchema,
|
|
118
|
-
IdeateOutputSchema,
|
|
119
|
-
ImportTasksSchema,
|
|
120
|
-
RequirementStatusSchema,
|
|
121
|
-
SprintSchema,
|
|
122
|
-
SprintStatusSchema,
|
|
123
|
-
TaskStatusSchema,
|
|
124
|
-
TasksSchema,
|
|
125
|
-
assertSafeCwd,
|
|
126
|
-
ensureDir,
|
|
127
|
-
expandTilde,
|
|
128
|
-
fileExists,
|
|
129
|
-
getDataDir,
|
|
130
|
-
getIdeateDir,
|
|
131
|
-
getRefinementDir,
|
|
132
|
-
getSchemaPath,
|
|
133
|
-
getSprintDir,
|
|
134
|
-
getSprintFilePath,
|
|
135
|
-
getTasksFilePath,
|
|
136
|
-
readValidatedJson,
|
|
137
|
-
validateProjectPath
|
|
138
|
-
} from "./chunk-IB6OCKZW.mjs";
|
|
139
|
-
import {
|
|
140
|
-
DomainError,
|
|
141
|
-
NoCurrentSprintError,
|
|
142
|
-
ProjectNotFoundError,
|
|
143
|
-
SprintNotFoundError,
|
|
144
|
-
SprintStatusError,
|
|
145
|
-
TaskNotFoundError,
|
|
146
|
-
TicketNotFoundError
|
|
147
|
-
} from "./chunk-EDJX7TT6.mjs";
|
|
148
|
-
import {
|
|
149
|
-
DETAIL_LABEL_WIDTH,
|
|
150
|
-
badge,
|
|
151
|
-
boxChars,
|
|
152
|
-
clearScreen,
|
|
153
|
-
colors,
|
|
154
|
-
createSpinner,
|
|
155
|
-
emoji,
|
|
156
|
-
error,
|
|
157
|
-
field,
|
|
158
|
-
fieldMultiline,
|
|
159
|
-
formatSprintStatus,
|
|
160
|
-
formatTaskStatus,
|
|
161
|
-
getQuoteForContext,
|
|
162
|
-
horizontalLine,
|
|
163
|
-
icons,
|
|
164
|
-
labelValue,
|
|
165
|
-
log,
|
|
166
|
-
muted,
|
|
167
|
-
printCountSummary,
|
|
168
|
-
printHeader,
|
|
169
|
-
printSeparator,
|
|
170
|
-
progressBar,
|
|
171
|
-
renderCard,
|
|
172
|
-
renderTable,
|
|
173
|
-
showBanner,
|
|
174
|
-
showEmpty,
|
|
175
|
-
showError,
|
|
176
|
-
showInfo,
|
|
177
|
-
showNextStep,
|
|
178
|
-
showNextSteps,
|
|
179
|
-
showRandomQuote,
|
|
180
|
-
showSuccess,
|
|
181
|
-
showTip,
|
|
182
|
-
showWarning,
|
|
183
|
-
success,
|
|
184
|
-
terminalBell
|
|
185
|
-
} from "./chunk-QBXHAXHI.mjs";
|
|
186
|
-
|
|
187
|
-
// src/cli.ts
|
|
188
|
-
import { Command } from "commander";
|
|
189
|
-
|
|
190
|
-
// src/interactive/menu.ts
|
|
191
|
-
import { Separator } from "@inquirer/prompts";
|
|
192
|
-
var SEPARATOR_WIDTH = 48;
|
|
193
|
-
function titled(label) {
|
|
194
|
-
const lineLen = Math.max(2, SEPARATOR_WIDTH - label.length - 4);
|
|
195
|
-
return new Separator(colors.muted(`
|
|
196
|
-
\u2500\u2500 ${label} ${"\u2500".repeat(lineLen)}`));
|
|
197
|
-
}
|
|
198
|
-
function line() {
|
|
199
|
-
return new Separator(colors.muted("\u2500".repeat(SEPARATOR_WIDTH)));
|
|
200
|
-
}
|
|
201
|
-
var WORKFLOW_ACTIONS = {
|
|
202
|
-
sprint: /* @__PURE__ */ new Set(["create", "refine", "ideate", "plan", "start", "close"]),
|
|
203
|
-
ticket: /* @__PURE__ */ new Set(["add", "refine"]),
|
|
204
|
-
task: /* @__PURE__ */ new Set(["add", "import"]),
|
|
205
|
-
progress: /* @__PURE__ */ new Set(["log"])
|
|
206
|
-
};
|
|
207
|
-
function isWorkflowAction(group, subCommand) {
|
|
208
|
-
return WORKFLOW_ACTIONS[group]?.has(subCommand) ?? false;
|
|
209
|
-
}
|
|
210
|
-
function buildPlanActions(ctx) {
|
|
211
|
-
const items = [];
|
|
212
|
-
const isDraft = ctx.currentSprintStatus === "draft";
|
|
213
|
-
const hasSprint = ctx.currentSprintId !== null;
|
|
214
|
-
items.push({ name: "Create Sprint", value: "action:sprint:create", description: "Start a new sprint" });
|
|
215
|
-
const addTicketDisabled = !hasSprint ? "create a sprint first" : !isDraft ? "need draft sprint" : !ctx.hasProjects ? "add a project first" : false;
|
|
216
|
-
items.push({
|
|
217
|
-
name: "Add Ticket",
|
|
218
|
-
value: "action:ticket:add",
|
|
219
|
-
description: "Add work to current sprint",
|
|
220
|
-
disabled: addTicketDisabled
|
|
221
|
-
});
|
|
222
|
-
let refineDisabled = false;
|
|
223
|
-
let refineDesc = "Clarify ticket requirements";
|
|
224
|
-
if (!hasSprint) {
|
|
225
|
-
refineDisabled = "create a sprint first";
|
|
226
|
-
} else if (!isDraft) {
|
|
227
|
-
refineDisabled = "need draft sprint";
|
|
228
|
-
} else if (ctx.ticketCount === 0) {
|
|
229
|
-
refineDisabled = "add tickets first";
|
|
230
|
-
} else if (ctx.pendingRequirements === 0) {
|
|
231
|
-
refineDisabled = "all tickets refined";
|
|
232
|
-
} else {
|
|
233
|
-
refineDesc = `${String(ctx.pendingRequirements)} ticket${ctx.pendingRequirements !== 1 ? "s" : ""} pending`;
|
|
234
|
-
}
|
|
235
|
-
items.push({
|
|
236
|
-
name: "Refine Requirements",
|
|
237
|
-
value: "action:sprint:refine",
|
|
238
|
-
description: refineDesc,
|
|
239
|
-
disabled: refineDisabled
|
|
240
|
-
});
|
|
241
|
-
let planDisabled = false;
|
|
242
|
-
const planDesc = "Generate tasks from requirements";
|
|
243
|
-
if (!hasSprint) {
|
|
244
|
-
planDisabled = "create a sprint first";
|
|
245
|
-
} else if (!isDraft) {
|
|
246
|
-
planDisabled = "need draft sprint";
|
|
247
|
-
} else if (ctx.ticketCount === 0) {
|
|
248
|
-
planDisabled = "add tickets first";
|
|
249
|
-
} else if (!ctx.allRequirementsApproved) {
|
|
250
|
-
planDisabled = "refine all tickets first";
|
|
251
|
-
}
|
|
252
|
-
items.push({
|
|
253
|
-
name: ctx.taskCount > 0 ? "Re-Plan Tasks" : "Plan Tasks",
|
|
254
|
-
value: "action:sprint:plan",
|
|
255
|
-
description: planDesc,
|
|
256
|
-
disabled: planDisabled
|
|
257
|
-
});
|
|
258
|
-
const ideateDisabled = !hasSprint ? "create a sprint first" : !isDraft ? "need draft sprint" : !ctx.hasProjects ? "add a project first" : false;
|
|
259
|
-
items.push({
|
|
260
|
-
name: "Ideate",
|
|
261
|
-
value: "action:sprint:ideate",
|
|
262
|
-
description: "Quick idea to tasks",
|
|
263
|
-
disabled: ideateDisabled
|
|
264
|
-
});
|
|
265
|
-
return items;
|
|
266
|
-
}
|
|
267
|
-
function buildExecuteActions(ctx) {
|
|
268
|
-
const items = [];
|
|
269
|
-
const isDraft = ctx.currentSprintStatus === "draft";
|
|
270
|
-
const isActive = ctx.currentSprintStatus === "active";
|
|
271
|
-
const hasSprint = ctx.currentSprintId !== null;
|
|
272
|
-
let startDisabled = false;
|
|
273
|
-
if (!hasSprint) {
|
|
274
|
-
startDisabled = "create a sprint first";
|
|
275
|
-
} else if (!isDraft && !isActive) {
|
|
276
|
-
startDisabled = "need draft or active sprint";
|
|
277
|
-
} else if (ctx.taskCount === 0) {
|
|
278
|
-
startDisabled = "plan tasks first";
|
|
279
|
-
}
|
|
280
|
-
items.push({
|
|
281
|
-
name: "Start Sprint",
|
|
282
|
-
value: "action:sprint:start",
|
|
283
|
-
description: "Begin implementation",
|
|
284
|
-
disabled: startDisabled
|
|
285
|
-
});
|
|
286
|
-
items.push({
|
|
287
|
-
name: "Health Check",
|
|
288
|
-
value: "action:sprint:health",
|
|
289
|
-
description: "Diagnose blockers and stale tasks",
|
|
290
|
-
disabled: !hasSprint ? "no sprint" : false
|
|
291
|
-
});
|
|
292
|
-
items.push({
|
|
293
|
-
name: "Close Sprint",
|
|
294
|
-
value: "action:sprint:close",
|
|
295
|
-
description: "Close the current sprint",
|
|
296
|
-
disabled: !isActive ? "need active sprint" : false
|
|
297
|
-
});
|
|
298
|
-
return items;
|
|
299
|
-
}
|
|
300
|
-
function buildMainMenu(ctx) {
|
|
301
|
-
const items = [];
|
|
302
|
-
let defaultValue;
|
|
303
|
-
if (ctx.nextAction) {
|
|
304
|
-
const actionValue = `action:${ctx.nextAction.group}:${ctx.nextAction.subCommand}`;
|
|
305
|
-
items.push({
|
|
306
|
-
name: `\u2192 ${ctx.nextAction.label}`,
|
|
307
|
-
value: actionValue,
|
|
308
|
-
description: ctx.nextAction.description
|
|
309
|
-
});
|
|
310
|
-
defaultValue = actionValue;
|
|
311
|
-
}
|
|
312
|
-
items.push(titled("PLAN"));
|
|
313
|
-
for (const action of buildPlanActions(ctx)) {
|
|
314
|
-
items.push(action);
|
|
315
|
-
}
|
|
316
|
-
items.push(titled("EXECUTE"));
|
|
317
|
-
for (const action of buildExecuteActions(ctx)) {
|
|
318
|
-
items.push(action);
|
|
319
|
-
}
|
|
320
|
-
items.push(titled("BROWSE"));
|
|
321
|
-
items.push({ name: "Sprints", value: "sprint", description: "List, show, switch" });
|
|
322
|
-
items.push({ name: "Tickets", value: "ticket", description: "List, show, edit" });
|
|
323
|
-
items.push({ name: "Tasks", value: "task", description: "List, show, manage" });
|
|
324
|
-
items.push(titled("SETUP"));
|
|
325
|
-
items.push({ name: "Projects", value: "project", description: "Manage projects & repositories" });
|
|
326
|
-
items.push({ name: "Configuration", value: "config", description: "AI provider, settings" });
|
|
327
|
-
items.push({ name: "Doctor", value: "action:doctor:run", description: "Check environment health" });
|
|
328
|
-
items.push(titled("SESSION"));
|
|
329
|
-
if (!ctx.currentSprintId) {
|
|
330
|
-
items.push({ name: "Quick Start Wizard", value: "wizard", description: "Guided sprint setup" });
|
|
331
|
-
}
|
|
332
|
-
items.push({ name: "Exit", value: "exit" });
|
|
333
|
-
return { items, defaultValue };
|
|
334
|
-
}
|
|
335
|
-
function buildSprintSubMenu(ctx) {
|
|
336
|
-
const items = [];
|
|
337
|
-
items.push(titled("BROWSE"));
|
|
338
|
-
items.push({ name: "List", value: "list", description: "List all sprints" });
|
|
339
|
-
items.push({ name: "Show", value: "show", description: "Show sprint details" });
|
|
340
|
-
items.push({ name: "Set Current", value: "current", description: "Set current sprint" });
|
|
341
|
-
items.push(titled("EXPORT"));
|
|
342
|
-
items.push({
|
|
343
|
-
name: "Requirements",
|
|
344
|
-
value: "requirements",
|
|
345
|
-
description: "Export refined requirements"
|
|
346
|
-
});
|
|
347
|
-
items.push({ name: "Context", value: "context", description: "Output full sprint context" });
|
|
348
|
-
items.push({ name: "Progress", value: "progress show", description: "View progress log" });
|
|
349
|
-
items.push(titled("MANAGE"));
|
|
350
|
-
items.push({ name: "Log Progress", value: "progress log", description: "Add progress entry" });
|
|
351
|
-
items.push({ name: "Delete", value: "delete", description: "Delete a sprint permanently" });
|
|
352
|
-
items.push(line());
|
|
353
|
-
items.push({ name: "Back", value: "back", description: "Return to main menu" });
|
|
354
|
-
const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName} (${ctx.currentSprintStatus ?? "unknown"})` : "";
|
|
355
|
-
return { title: `Sprint${titleSuffix}`, items };
|
|
356
|
-
}
|
|
357
|
-
function buildTicketSubMenu(ctx) {
|
|
358
|
-
const items = [];
|
|
359
|
-
items.push({
|
|
360
|
-
name: "Add",
|
|
361
|
-
value: "add",
|
|
362
|
-
description: ctx.hasProjects ? "Add a ticket" : "Add a ticket (add a project first)",
|
|
363
|
-
disabled: !ctx.hasProjects ? "add a project first" : false
|
|
364
|
-
});
|
|
365
|
-
items.push({ name: "Edit", value: "edit", description: "Edit a ticket" });
|
|
366
|
-
items.push({ name: "List", value: "list", description: "List all tickets" });
|
|
367
|
-
items.push({ name: "Show", value: "show", description: "Show ticket details" });
|
|
368
|
-
const approvedCount = ctx.ticketCount - ctx.pendingRequirements;
|
|
369
|
-
let refineDisabled = false;
|
|
370
|
-
if (ctx.currentSprintStatus !== "draft") {
|
|
371
|
-
refineDisabled = "need draft sprint";
|
|
372
|
-
} else if (approvedCount === 0) {
|
|
373
|
-
refineDisabled = "no approved tickets";
|
|
374
|
-
}
|
|
375
|
-
items.push({
|
|
376
|
-
name: "Refine",
|
|
377
|
-
value: "refine",
|
|
378
|
-
description: "Re-refine approved requirements",
|
|
379
|
-
disabled: refineDisabled
|
|
380
|
-
});
|
|
381
|
-
items.push(line());
|
|
382
|
-
items.push({ name: "Remove", value: "remove", description: "Remove a ticket" });
|
|
383
|
-
items.push({ name: "Back", value: "back", description: "Return to main menu" });
|
|
384
|
-
const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : "";
|
|
385
|
-
return { title: `Ticket${titleSuffix}`, items };
|
|
386
|
-
}
|
|
387
|
-
function buildTaskSubMenu(ctx) {
|
|
388
|
-
const items = [];
|
|
389
|
-
items.push(titled("VIEW"));
|
|
390
|
-
items.push({ name: "List", value: "list", description: "List all tasks" });
|
|
391
|
-
items.push({ name: "Show", value: "show", description: "Show task details" });
|
|
392
|
-
items.push({ name: "Next", value: "next", description: "Get next task" });
|
|
393
|
-
items.push(titled("MANAGE"));
|
|
394
|
-
items.push({ name: "Add", value: "add", description: "Add a new task" });
|
|
395
|
-
items.push({ name: "Import", value: "import", description: "Import from JSON" });
|
|
396
|
-
items.push({ name: "Status", value: "status", description: "Update status" });
|
|
397
|
-
items.push({ name: "Reorder", value: "reorder", description: "Change priority" });
|
|
398
|
-
items.push(line());
|
|
399
|
-
items.push({ name: "Remove", value: "remove", description: "Remove a task" });
|
|
400
|
-
items.push({ name: "Back", value: "back", description: "Return to main menu" });
|
|
401
|
-
const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : "";
|
|
402
|
-
return { title: `Task${titleSuffix}`, items };
|
|
403
|
-
}
|
|
404
|
-
function buildProjectSubMenu() {
|
|
405
|
-
const items = [];
|
|
406
|
-
items.push({ name: "Add", value: "add", description: "Add a new project" });
|
|
407
|
-
items.push({ name: "List", value: "list", description: "List all projects" });
|
|
408
|
-
items.push({ name: "Show", value: "show", description: "Show project details" });
|
|
409
|
-
items.push(titled("REPOSITORIES"));
|
|
410
|
-
items.push({
|
|
411
|
-
name: "Add Repository",
|
|
412
|
-
value: "repo add",
|
|
413
|
-
description: "Add repository to project"
|
|
414
|
-
});
|
|
415
|
-
items.push({ name: "Remove Repository", value: "repo remove", description: "Remove repository" });
|
|
416
|
-
items.push(line());
|
|
417
|
-
items.push({ name: "Remove", value: "remove", description: "Remove a project" });
|
|
418
|
-
items.push({ name: "Back", value: "back", description: "Return to main menu" });
|
|
419
|
-
return { title: "Project", items };
|
|
420
|
-
}
|
|
421
|
-
function buildConfigSubMenu() {
|
|
422
|
-
const items = [];
|
|
423
|
-
items.push({ name: "Show Settings", value: "show", description: "View current configuration" });
|
|
424
|
-
items.push({ name: "Set AI Provider", value: "set provider", description: "Choose Claude Code or GitHub Copilot" });
|
|
425
|
-
items.push({ name: "Set Editor", value: "set editor", description: "Editor for refinement sessions" });
|
|
426
|
-
items.push({
|
|
427
|
-
name: "Set Evaluation Iterations",
|
|
428
|
-
value: "set evaluationIterations",
|
|
429
|
-
description: "Generator-evaluator loop count (0 = disabled)"
|
|
430
|
-
});
|
|
431
|
-
items.push(line());
|
|
432
|
-
items.push({ name: "Back", value: "back", description: "Return to main menu" });
|
|
433
|
-
return { title: "Configuration", items };
|
|
434
|
-
}
|
|
435
|
-
function buildSubMenu(group, ctx) {
|
|
436
|
-
switch (group) {
|
|
437
|
-
case "sprint":
|
|
438
|
-
return buildSprintSubMenu(ctx);
|
|
439
|
-
case "ticket":
|
|
440
|
-
return buildTicketSubMenu(ctx);
|
|
441
|
-
case "task":
|
|
442
|
-
return buildTaskSubMenu(ctx);
|
|
443
|
-
case "project":
|
|
444
|
-
return buildProjectSubMenu();
|
|
445
|
-
case "config":
|
|
446
|
-
return buildConfigSubMenu();
|
|
447
|
-
default:
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// src/interactive/dashboard.ts
|
|
453
|
-
async function loadDashboardData() {
|
|
454
|
-
const sprintId = await getCurrentSprint();
|
|
455
|
-
if (!sprintId) return null;
|
|
456
|
-
const r = await wrapAsync(async () => {
|
|
457
|
-
const sprint = await getSprint(sprintId);
|
|
458
|
-
const tasks = await getTasks(sprintId);
|
|
459
|
-
const pendingTickets = getPendingRequirements(sprint.tickets);
|
|
460
|
-
const pendingCount = pendingTickets.length;
|
|
461
|
-
const approvedCount = sprint.tickets.length - pendingCount;
|
|
462
|
-
const doneIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
|
|
463
|
-
const blockedCount = tasks.filter(
|
|
464
|
-
(t) => t.status !== "done" && t.blockedBy.length > 0 && !t.blockedBy.every((id) => doneIds.has(id))
|
|
465
|
-
).length;
|
|
466
|
-
const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
|
|
467
|
-
const plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
|
|
468
|
-
const aiProvider = await getAiProvider();
|
|
469
|
-
return { sprint, tasks, approvedCount, pendingCount, blockedCount, plannedTicketCount, aiProvider };
|
|
470
|
-
}, ensureError);
|
|
471
|
-
return r.ok ? r.value : null;
|
|
472
|
-
}
|
|
473
|
-
function getNextAction(data) {
|
|
474
|
-
const { sprint, tasks, pendingCount, approvedCount } = data;
|
|
475
|
-
const ticketCount = sprint.tickets.length;
|
|
476
|
-
const totalTasks = tasks.length;
|
|
477
|
-
const allDone = totalTasks > 0 && tasks.every((t) => t.status === "done");
|
|
478
|
-
if (sprint.status === "draft") {
|
|
479
|
-
if (ticketCount === 0) {
|
|
480
|
-
return { label: "Add Ticket", description: "No tickets yet", group: "ticket", subCommand: "add" };
|
|
481
|
-
}
|
|
482
|
-
if (pendingCount > 0) {
|
|
483
|
-
return {
|
|
484
|
-
label: "Refine Requirements",
|
|
485
|
-
description: `${String(pendingCount)} ticket${pendingCount !== 1 ? "s" : ""} pending`,
|
|
486
|
-
group: "sprint",
|
|
487
|
-
subCommand: "refine"
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
if (approvedCount > 0 && totalTasks === 0) {
|
|
491
|
-
return { label: "Plan Tasks", description: "Requirements approved", group: "sprint", subCommand: "plan" };
|
|
492
|
-
}
|
|
493
|
-
if (totalTasks > 0 && data.plannedTicketCount < ticketCount) {
|
|
494
|
-
const unplanned = ticketCount - data.plannedTicketCount;
|
|
495
|
-
return {
|
|
496
|
-
label: "Re-Plan Tasks",
|
|
497
|
-
description: `${String(unplanned)} unplanned ticket${unplanned !== 1 ? "s" : ""}`,
|
|
498
|
-
group: "sprint",
|
|
499
|
-
subCommand: "plan"
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
if (totalTasks > 0) {
|
|
503
|
-
return {
|
|
504
|
-
label: "Start Sprint",
|
|
505
|
-
description: `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""} ready`,
|
|
506
|
-
group: "sprint",
|
|
507
|
-
subCommand: "start"
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (sprint.status === "active") {
|
|
512
|
-
if (allDone) {
|
|
513
|
-
return { label: "Close Sprint", description: "All tasks done", group: "sprint", subCommand: "close" };
|
|
514
|
-
}
|
|
515
|
-
return {
|
|
516
|
-
label: "Continue Work",
|
|
517
|
-
description: `${String(totalTasks - tasks.filter((t) => t.status === "done").length)} task${totalTasks - tasks.filter((t) => t.status === "done").length !== 1 ? "s" : ""} remaining`,
|
|
518
|
-
group: "sprint",
|
|
519
|
-
subCommand: "start"
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
function renderStatusHeader(data) {
|
|
525
|
-
if (!data) return [];
|
|
526
|
-
const { sprint, tasks, approvedCount, aiProvider } = data;
|
|
527
|
-
const totalTasks = tasks.length;
|
|
528
|
-
const ticketCount = sprint.tickets.length;
|
|
529
|
-
const lines = [];
|
|
530
|
-
const sprintLabel = colors.highlight(sprint.name);
|
|
531
|
-
const statusBadge = formatSprintStatus(sprint.status);
|
|
532
|
-
const ticketPart = `${String(ticketCount)} ticket${ticketCount !== 1 ? "s" : ""}`;
|
|
533
|
-
const taskPart = `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""}`;
|
|
534
|
-
const providerPart = aiProvider === "claude" ? "Claude" : aiProvider === "copilot" ? "Copilot" : null;
|
|
535
|
-
const providerSuffix = providerPart ? ` | ${providerPart}` : "";
|
|
536
|
-
lines.push(
|
|
537
|
-
` ${icons.sprint} ${sprintLabel} ${statusBadge} ${colors.muted(`| ${ticketPart} | ${taskPart}${providerSuffix}`)}`
|
|
538
|
-
);
|
|
539
|
-
if ((sprint.status === "active" || sprint.status === "closed") && totalTasks > 0) {
|
|
540
|
-
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
541
|
-
const bar = progressBar(doneCount, totalTasks, { width: 15 });
|
|
542
|
-
const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
|
|
543
|
-
const todoCount = tasks.filter((t) => t.status === "todo").length;
|
|
544
|
-
lines.push(
|
|
545
|
-
` ${bar} ${colors.muted(`${String(doneCount)} done, ${String(inProgressCount)} active, ${String(todoCount)} todo`)}`
|
|
546
|
-
);
|
|
547
|
-
} else if (sprint.status === "draft" && ticketCount > 0) {
|
|
548
|
-
const refinedColor = approvedCount === ticketCount ? colors.success : colors.warning;
|
|
549
|
-
const refinedPart = refinedColor(`Refined: ${String(approvedCount)}/${String(ticketCount)}`);
|
|
550
|
-
const plannedColor = data.plannedTicketCount === ticketCount ? colors.success : colors.muted;
|
|
551
|
-
const plannedPart = plannedColor(`Planned: ${String(data.plannedTicketCount)}/${String(ticketCount)}`);
|
|
552
|
-
lines.push(` ${refinedPart} ${colors.muted("|")} ${plannedPart}`);
|
|
553
|
-
}
|
|
554
|
-
return lines;
|
|
555
|
-
}
|
|
556
|
-
function renderDashboard(data) {
|
|
557
|
-
const { sprint, tasks, approvedCount, blockedCount } = data;
|
|
558
|
-
const chars = boxChars.rounded;
|
|
559
|
-
const todoCount = tasks.filter((t) => t.status === "todo").length;
|
|
560
|
-
const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
|
|
561
|
-
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
562
|
-
const totalTasks = tasks.length;
|
|
563
|
-
const ticketCount = sprint.tickets.length;
|
|
564
|
-
const lines = [];
|
|
565
|
-
const sprintLabel = colors.highlight(sprint.name);
|
|
566
|
-
const statusBadge = formatSprintStatus(sprint.status);
|
|
567
|
-
lines.push(` ${icons.sprint} ${sprintLabel} ${statusBadge}`);
|
|
568
|
-
const ticketSummary = `${String(ticketCount)} ticket${ticketCount !== 1 ? "s" : ""}`;
|
|
569
|
-
const taskSummary = `${String(totalTasks)} task${totalTasks !== 1 ? "s" : ""}`;
|
|
570
|
-
lines.push(` ${colors.muted(`${ticketSummary} ${chars.vertical} ${taskSummary}`)}`);
|
|
571
|
-
if (totalTasks > 0) {
|
|
572
|
-
const bar = progressBar(doneCount, totalTasks);
|
|
573
|
-
const detail = colors.muted(
|
|
574
|
-
`${String(doneCount)} done, ${String(inProgressCount)} active, ${String(todoCount)} todo`
|
|
575
|
-
);
|
|
576
|
-
lines.push(` ${bar} ${detail}`);
|
|
577
|
-
}
|
|
578
|
-
if (sprint.status === "draft" && ticketCount > 0) {
|
|
579
|
-
const refinedColor = approvedCount === ticketCount ? colors.success : colors.warning;
|
|
580
|
-
const refinedPart = refinedColor(`Refined: ${String(approvedCount)}/${String(ticketCount)}`);
|
|
581
|
-
const plannedColor = data.plannedTicketCount === ticketCount ? colors.success : colors.muted;
|
|
582
|
-
const plannedPart = plannedColor(`Planned: ${String(data.plannedTicketCount)}/${String(ticketCount)}`);
|
|
583
|
-
lines.push(` ${refinedPart} ${colors.muted("|")} ${plannedPart}`);
|
|
584
|
-
}
|
|
585
|
-
if (blockedCount > 0) {
|
|
586
|
-
lines.push(
|
|
587
|
-
` ${colors.warning(icons.warning)} ${colors.warning(`${String(blockedCount)} blocked task${blockedCount !== 1 ? "s" : ""}`)}`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
const nextAction = getNextAction(data);
|
|
591
|
-
if (nextAction) {
|
|
592
|
-
lines.push(
|
|
593
|
-
` ${colors.muted(icons.tip)} ${colors.muted(nextAction.label + ":")} ${colors.highlight(nextAction.description)}`
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
return lines;
|
|
597
|
-
}
|
|
598
|
-
function renderEmptyDashboard() {
|
|
599
|
-
const quote = getQuoteForContext("idle");
|
|
600
|
-
return [
|
|
601
|
-
` ${emoji.donut} ${colors.muted("No current sprint")}`,
|
|
602
|
-
` ${colors.muted(`"${quote}"`)}`,
|
|
603
|
-
"",
|
|
604
|
-
` ${colors.muted(icons.tip)} ${colors.muted("Get started:")}`,
|
|
605
|
-
` ${colors.muted("1.")} ${colors.muted("Add a project:")} ${colors.highlight("ralphctl project add")}`,
|
|
606
|
-
` ${colors.muted("2.")} ${colors.muted("Create a sprint:")} ${colors.highlight("ralphctl sprint create")}`
|
|
607
|
-
];
|
|
608
|
-
}
|
|
609
|
-
async function showDashboard() {
|
|
610
|
-
const data = await loadDashboardData();
|
|
611
|
-
console.log("");
|
|
612
|
-
if (data) {
|
|
613
|
-
const lines = renderDashboard(data);
|
|
614
|
-
for (const line2 of lines) {
|
|
615
|
-
console.log(line2);
|
|
616
|
-
}
|
|
617
|
-
} else {
|
|
618
|
-
const lines = renderEmptyDashboard();
|
|
619
|
-
for (const line2 of lines) {
|
|
620
|
-
console.log(line2);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
console.log("");
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// src/interactive/index.ts
|
|
627
|
-
import { input as input5, select as select3 } from "@inquirer/prompts";
|
|
628
|
-
|
|
629
|
-
// src/commands/project/list.ts
|
|
630
|
-
async function projectListCommand() {
|
|
631
|
-
const projects = await listProjects();
|
|
632
|
-
if (projects.length === 0) {
|
|
633
|
-
showEmpty("projects", "Add one with: ralphctl project add");
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
printHeader("Projects", icons.project);
|
|
637
|
-
for (const project of projects) {
|
|
638
|
-
const repoCount = muted(
|
|
639
|
-
`(${String(project.repositories.length)} repo${project.repositories.length !== 1 ? "s" : ""})`
|
|
640
|
-
);
|
|
641
|
-
log.raw(`${colors.highlight(project.name)} ${project.displayName} ${repoCount}`);
|
|
642
|
-
for (const repo of project.repositories) {
|
|
643
|
-
log.item(`${repo.name} ${muted("\u2192")} ${muted(repo.path)}`);
|
|
644
|
-
}
|
|
645
|
-
if (project.description) {
|
|
646
|
-
log.dim(` ${project.description}`);
|
|
647
|
-
}
|
|
648
|
-
log.newline();
|
|
649
|
-
}
|
|
650
|
-
log.dim(`Showing ${String(projects.length)} project(s)`);
|
|
651
|
-
log.newline();
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// src/commands/project/show.ts
|
|
655
|
-
async function projectShowCommand(args) {
|
|
656
|
-
let projectName = args[0];
|
|
657
|
-
if (!projectName) {
|
|
658
|
-
const selected = await selectProject("Select project to show:");
|
|
659
|
-
if (!selected) return;
|
|
660
|
-
projectName = selected;
|
|
661
|
-
}
|
|
662
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
663
|
-
if (!projectR.ok) {
|
|
664
|
-
if (projectR.error instanceof ProjectNotFoundError) {
|
|
665
|
-
showError(`Project not found: ${projectName}`);
|
|
666
|
-
log.newline();
|
|
667
|
-
} else {
|
|
668
|
-
throw projectR.error;
|
|
669
|
-
}
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const project = projectR.value;
|
|
673
|
-
const infoLines = [labelValue("Name", project.name), labelValue("Display Name", project.displayName)];
|
|
674
|
-
if (project.description) {
|
|
675
|
-
infoLines.push(labelValue("Description", project.description));
|
|
676
|
-
}
|
|
677
|
-
infoLines.push(labelValue("Repositories", String(project.repositories.length)));
|
|
678
|
-
log.newline();
|
|
679
|
-
console.log(renderCard(`${icons.project} ${project.displayName}`, infoLines));
|
|
680
|
-
for (const repo of project.repositories) {
|
|
681
|
-
log.newline();
|
|
682
|
-
const repoLines = [labelValue("Path", repo.path)];
|
|
683
|
-
if (repo.checkScript) {
|
|
684
|
-
repoLines.push(labelValue("Check", colors.info(repo.checkScript)));
|
|
685
|
-
} else {
|
|
686
|
-
repoLines.push(muted("No check script configured"));
|
|
687
|
-
}
|
|
688
|
-
console.log(renderCard(` ${repo.name}`, repoLines));
|
|
689
|
-
}
|
|
690
|
-
log.newline();
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// src/commands/project/remove.ts
|
|
694
|
-
import { confirm } from "@inquirer/prompts";
|
|
695
|
-
async function projectRemoveCommand(args) {
|
|
696
|
-
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
697
|
-
let projectName = args.find((a) => !a.startsWith("-"));
|
|
698
|
-
if (!projectName) {
|
|
699
|
-
const selected = await selectProject("Select project to remove:");
|
|
700
|
-
if (!selected) return;
|
|
701
|
-
projectName = selected;
|
|
702
|
-
}
|
|
703
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
704
|
-
if (!projectR.ok) {
|
|
705
|
-
if (projectR.error instanceof ProjectNotFoundError) {
|
|
706
|
-
showError(`Project not found: ${projectName}`);
|
|
707
|
-
console.log("");
|
|
708
|
-
} else {
|
|
709
|
-
throw projectR.error;
|
|
710
|
-
}
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
const project = projectR.value;
|
|
714
|
-
if (!skipConfirm) {
|
|
715
|
-
const confirmed = await confirm({
|
|
716
|
-
message: `Remove project "${project.displayName}" (${project.name})?`,
|
|
717
|
-
default: false
|
|
718
|
-
});
|
|
719
|
-
if (!confirmed) {
|
|
720
|
-
console.log(muted("\nProject removal cancelled.\n"));
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
await removeProject(projectName);
|
|
725
|
-
showSuccess("Project removed", [["Name", projectName]]);
|
|
726
|
-
console.log("");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// src/commands/project/repo.ts
|
|
730
|
-
import { basename, resolve } from "path";
|
|
731
|
-
import { confirm as confirm2, input, select } from "@inquirer/prompts";
|
|
732
|
-
async function projectRepoAddCommand(args) {
|
|
733
|
-
let projectName = args[0];
|
|
734
|
-
let path = args[1];
|
|
735
|
-
if (!projectName) {
|
|
736
|
-
const selected = await selectProject("Select project to add repository to:");
|
|
737
|
-
if (!selected) return;
|
|
738
|
-
projectName = selected;
|
|
739
|
-
}
|
|
740
|
-
path ??= await input({
|
|
741
|
-
message: `${emoji.donut} Repository path to add:`,
|
|
742
|
-
validate: (v) => v.trim().length > 0 ? true : "Path is required"
|
|
743
|
-
});
|
|
744
|
-
const resolvedPath = resolve(expandTilde(path));
|
|
745
|
-
const bareRepo = { name: basename(resolvedPath), path: resolvedPath };
|
|
746
|
-
log.info(`
|
|
747
|
-
Configuring: ${bareRepo.name}`);
|
|
748
|
-
const repoWithScripts = await addCheckScriptToRepository(bareRepo);
|
|
749
|
-
const addR = await wrapAsync(() => addProjectRepo(projectName, repoWithScripts), ensureError);
|
|
750
|
-
if (!addR.ok) {
|
|
751
|
-
if (addR.error instanceof ProjectNotFoundError) {
|
|
752
|
-
showError(`Project not found: ${projectName}`);
|
|
753
|
-
} else {
|
|
754
|
-
showError(addR.error.message);
|
|
755
|
-
}
|
|
756
|
-
log.newline();
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
showSuccess("Repository added", [["Project", projectName]]);
|
|
760
|
-
log.newline();
|
|
761
|
-
log.info("Current repositories:");
|
|
762
|
-
for (const repo of addR.value.repositories) {
|
|
763
|
-
log.item(`${repo.name} \u2192 ${repo.path}`);
|
|
764
|
-
}
|
|
765
|
-
log.newline();
|
|
766
|
-
}
|
|
767
|
-
async function projectRepoRemoveCommand(args) {
|
|
768
|
-
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
769
|
-
const filteredArgs = args.filter((a) => !a.startsWith("-"));
|
|
770
|
-
let projectName = filteredArgs[0];
|
|
771
|
-
let path = filteredArgs[1];
|
|
772
|
-
if (!projectName) {
|
|
773
|
-
const selected = await selectProject("Select project to remove repository from:");
|
|
774
|
-
if (!selected) return;
|
|
775
|
-
projectName = selected;
|
|
776
|
-
}
|
|
777
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
778
|
-
if (!projectR.ok) {
|
|
779
|
-
if (projectR.error instanceof ProjectNotFoundError) {
|
|
780
|
-
showError(`Project not found: ${projectName}`);
|
|
781
|
-
} else {
|
|
782
|
-
showError(projectR.error.message);
|
|
783
|
-
}
|
|
784
|
-
log.newline();
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const project = projectR.value;
|
|
788
|
-
if (!path) {
|
|
789
|
-
if (project.repositories.length === 0) {
|
|
790
|
-
console.log(muted("\nNo repositories to remove.\n"));
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
path = await select({
|
|
794
|
-
message: `${emoji.donut} Select repository to remove:`,
|
|
795
|
-
choices: project.repositories.map((r) => ({
|
|
796
|
-
name: `${r.name} (${r.path})`,
|
|
797
|
-
value: r.path
|
|
798
|
-
}))
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
if (!skipConfirm) {
|
|
802
|
-
const confirmed = await confirm2({
|
|
803
|
-
message: `Remove repository "${path}" from project "${project.displayName}"?`,
|
|
804
|
-
default: false
|
|
805
|
-
});
|
|
806
|
-
if (!confirmed) {
|
|
807
|
-
console.log(muted("\nRepository removal cancelled.\n"));
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
const removeR = await wrapAsync(() => removeProjectRepo(projectName, path), ensureError);
|
|
812
|
-
if (!removeR.ok) {
|
|
813
|
-
if (removeR.error instanceof ProjectNotFoundError) {
|
|
814
|
-
showError(`Project not found: ${projectName}`);
|
|
815
|
-
} else {
|
|
816
|
-
showError(removeR.error.message);
|
|
817
|
-
}
|
|
818
|
-
log.newline();
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
showSuccess("Repository removed", [["Project", projectName]]);
|
|
822
|
-
log.newline();
|
|
823
|
-
log.info("Remaining repositories:");
|
|
824
|
-
for (const repo of removeR.value.repositories) {
|
|
825
|
-
log.item(`${repo.name} \u2192 ${repo.path}`);
|
|
826
|
-
}
|
|
827
|
-
log.newline();
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// src/commands/sprint/list.ts
|
|
831
|
-
async function sprintListCommand(args = []) {
|
|
832
|
-
let statusFilter;
|
|
833
|
-
for (let i = 0; i < args.length; i++) {
|
|
834
|
-
if (args[i] === "--status" && args[i + 1]) {
|
|
835
|
-
statusFilter = args[i + 1];
|
|
836
|
-
i++;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
if (statusFilter) {
|
|
840
|
-
const result = SprintStatusSchema.safeParse(statusFilter);
|
|
841
|
-
if (!result.success) {
|
|
842
|
-
showError(`Invalid status: "${statusFilter}". Valid values: draft, active, closed`);
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
const sprints = await listSprints();
|
|
847
|
-
if (sprints.length === 0) {
|
|
848
|
-
showEmpty("sprints", "Create one with: ralphctl sprint create");
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
const filtered = statusFilter ? sprints.filter((s) => s.status === statusFilter) : sprints;
|
|
852
|
-
const isFiltered = filtered.length !== sprints.length;
|
|
853
|
-
const filterStr = statusFilter ? ` (filtered: status=${statusFilter})` : "";
|
|
854
|
-
if (filtered.length === 0) {
|
|
855
|
-
showEmpty("matching sprints", "Try adjusting your filters");
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
printHeader("Sprints", icons.sprint);
|
|
859
|
-
const currentSprintId = await getCurrentSprint();
|
|
860
|
-
const rows = filtered.map((sprint) => {
|
|
861
|
-
const isCurrent = sprint.id === currentSprintId;
|
|
862
|
-
const marker = isCurrent ? badge("current", "success") : "";
|
|
863
|
-
return [marker, sprint.id, formatSprintStatus(sprint.status), sprint.name, String(sprint.tickets.length)];
|
|
864
|
-
});
|
|
865
|
-
console.log(
|
|
866
|
-
renderTable(
|
|
867
|
-
[
|
|
868
|
-
{ header: "", minWidth: 0 },
|
|
869
|
-
{ header: "ID" },
|
|
870
|
-
{ header: "Status" },
|
|
871
|
-
{ header: "Name" },
|
|
872
|
-
{ header: "Tickets", align: "right" }
|
|
873
|
-
],
|
|
874
|
-
rows
|
|
875
|
-
)
|
|
876
|
-
);
|
|
877
|
-
log.newline();
|
|
878
|
-
const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(sprints.length)} sprint(s)${filterStr}` : `Showing ${String(sprints.length)} sprint(s)`;
|
|
879
|
-
log.dim(showingLabel);
|
|
880
|
-
const hasActive = sprints.some((s) => s.status === "active");
|
|
881
|
-
if (!hasActive) {
|
|
882
|
-
log.newline();
|
|
883
|
-
showNextStep("ralphctl sprint start", "start a sprint");
|
|
884
|
-
}
|
|
885
|
-
log.newline();
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// src/commands/sprint/show.ts
|
|
889
|
-
async function sprintShowCommand(args) {
|
|
890
|
-
const sprintId = args[0];
|
|
891
|
-
let id;
|
|
892
|
-
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
893
|
-
if (!idR.ok) {
|
|
894
|
-
const selected = await selectSprint("Select sprint to show:");
|
|
895
|
-
if (!selected) return;
|
|
896
|
-
id = selected;
|
|
897
|
-
} else {
|
|
898
|
-
id = idR.value;
|
|
899
|
-
}
|
|
900
|
-
const sprint = await getSprint(id);
|
|
901
|
-
const tasks = await listTasks(id);
|
|
902
|
-
const currentSprintId = await getCurrentSprint();
|
|
903
|
-
const isCurrent = sprint.id === currentSprintId;
|
|
904
|
-
const infoLines = [
|
|
905
|
-
labelValue("ID", sprint.id + (isCurrent ? " " + badge("current", "success") : "")),
|
|
906
|
-
labelValue("Status", formatSprintStatus(sprint.status)),
|
|
907
|
-
labelValue("Created", new Date(sprint.createdAt).toLocaleString())
|
|
908
|
-
];
|
|
909
|
-
if (sprint.activatedAt) {
|
|
910
|
-
infoLines.push(labelValue("Activated", new Date(sprint.activatedAt).toLocaleString()));
|
|
911
|
-
}
|
|
912
|
-
if (sprint.closedAt) {
|
|
913
|
-
infoLines.push(labelValue("Closed", new Date(sprint.closedAt).toLocaleString()));
|
|
914
|
-
}
|
|
915
|
-
if (sprint.branch) {
|
|
916
|
-
infoLines.push(labelValue("Branch", sprint.branch));
|
|
917
|
-
}
|
|
918
|
-
log.newline();
|
|
919
|
-
console.log(renderCard(`${icons.sprint} ${sprint.name}`, infoLines));
|
|
920
|
-
log.newline();
|
|
921
|
-
const ticketLines = [];
|
|
922
|
-
if (sprint.tickets.length === 0) {
|
|
923
|
-
ticketLines.push(muted("No tickets yet"));
|
|
924
|
-
ticketLines.push(muted(`${icons.tip} Add with: ralphctl ticket add`));
|
|
925
|
-
} else {
|
|
926
|
-
const ticketsByProject = groupTicketsByProject(sprint.tickets);
|
|
927
|
-
let first = true;
|
|
928
|
-
for (const [projectName, tickets] of ticketsByProject) {
|
|
929
|
-
if (!first) ticketLines.push("");
|
|
930
|
-
first = false;
|
|
931
|
-
ticketLines.push(`${colors.info(icons.project)} ${colors.info(projectName)}`);
|
|
932
|
-
for (const ticket of tickets) {
|
|
933
|
-
const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "warning");
|
|
934
|
-
ticketLines.push(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
console.log(renderCard(`${icons.ticket} Tickets (${String(sprint.tickets.length)})`, ticketLines));
|
|
939
|
-
log.newline();
|
|
940
|
-
const taskLines = [];
|
|
941
|
-
const tasksByStatus = {
|
|
942
|
-
todo: tasks.filter((t) => t.status === "todo").length,
|
|
943
|
-
in_progress: tasks.filter((t) => t.status === "in_progress").length,
|
|
944
|
-
done: tasks.filter((t) => t.status === "done").length
|
|
945
|
-
};
|
|
946
|
-
if (tasks.length === 0) {
|
|
947
|
-
taskLines.push(muted("No tasks yet"));
|
|
948
|
-
taskLines.push(muted(`${icons.tip} Plan with: ralphctl sprint plan`));
|
|
949
|
-
} else {
|
|
950
|
-
taskLines.push(
|
|
951
|
-
`${formatTaskStatus("todo")} ${String(tasksByStatus.todo)} ${formatTaskStatus("in_progress")} ${String(tasksByStatus.in_progress)} ${formatTaskStatus("done")} ${String(tasksByStatus.done)}`
|
|
952
|
-
);
|
|
953
|
-
taskLines.push(colors.muted(horizontalLine(40, "rounded")));
|
|
954
|
-
for (const task of tasks) {
|
|
955
|
-
const statusIcon = task.status === "done" ? icons.success : task.status === "in_progress" ? icons.active : icons.inactive;
|
|
956
|
-
const statusColor = task.status === "done" ? "success" : task.status === "in_progress" ? "warning" : "muted";
|
|
957
|
-
taskLines.push(`${muted(String(task.order) + ".")} ${badge(statusIcon, statusColor)} ${task.name}`);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
console.log(renderCard(`${icons.task} Tasks (${String(tasks.length)})`, taskLines));
|
|
961
|
-
if (tasks.length > 0) {
|
|
962
|
-
printCountSummary("Progress", tasksByStatus.done, tasks.length);
|
|
963
|
-
}
|
|
964
|
-
log.newline();
|
|
965
|
-
if (sprint.status === "draft") {
|
|
966
|
-
const pendingCount = getPendingRequirements(sprint.tickets).length;
|
|
967
|
-
if (sprint.tickets.length === 0) {
|
|
968
|
-
showNextStep("ralphctl ticket add --project <name>", "add tickets to this sprint");
|
|
969
|
-
} else if (pendingCount > 0) {
|
|
970
|
-
showNextStep("ralphctl sprint refine", "refine ticket requirements");
|
|
971
|
-
} else if (tasks.length === 0) {
|
|
972
|
-
showNextStep("ralphctl sprint plan", "generate tasks from tickets");
|
|
973
|
-
} else {
|
|
974
|
-
showNextStep("ralphctl sprint start", "begin implementation");
|
|
975
|
-
}
|
|
976
|
-
} else if (sprint.status === "active") {
|
|
977
|
-
if (tasksByStatus.done === tasks.length && tasks.length > 0) {
|
|
978
|
-
showNextStep("ralphctl sprint close", "all tasks done \u2014 close the sprint");
|
|
979
|
-
} else {
|
|
980
|
-
showNextStep("ralphctl sprint start", "continue implementation");
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
log.newline();
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// src/commands/sprint/context.ts
|
|
987
|
-
async function sprintContextCommand(args) {
|
|
988
|
-
const sprintId = args[0];
|
|
989
|
-
let id;
|
|
990
|
-
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
991
|
-
if (!idR.ok) {
|
|
992
|
-
const selected = await selectSprint("Select sprint to show context for:");
|
|
993
|
-
if (!selected) {
|
|
994
|
-
showWarning("No sprints available.");
|
|
995
|
-
showNextStep("ralphctl sprint create", "create a sprint first");
|
|
996
|
-
log.newline();
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
id = selected;
|
|
1000
|
-
} else {
|
|
1001
|
-
id = idR.value;
|
|
1002
|
-
}
|
|
1003
|
-
const sprint = await getSprint(id);
|
|
1004
|
-
const tasks = await listTasks(id);
|
|
1005
|
-
console.log(`# Sprint: ${sprint.name}`);
|
|
1006
|
-
console.log(`ID: ${sprint.id}`);
|
|
1007
|
-
console.log(`Status: ${sprint.status}`);
|
|
1008
|
-
console.log("");
|
|
1009
|
-
console.log("## Tickets");
|
|
1010
|
-
console.log("");
|
|
1011
|
-
if (sprint.tickets.length === 0) {
|
|
1012
|
-
console.log("_No tickets defined_");
|
|
1013
|
-
} else {
|
|
1014
|
-
const ticketsByProject = groupTicketsByProject(sprint.tickets);
|
|
1015
|
-
for (const [projectName, tickets] of ticketsByProject) {
|
|
1016
|
-
console.log(`### Project: ${projectName}`);
|
|
1017
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
1018
|
-
if (projectR.ok) {
|
|
1019
|
-
const repoPaths = projectR.value.repositories.map((r) => `${r.name} (${r.path})`);
|
|
1020
|
-
console.log(`Repositories: ${repoPaths.join(", ")}`);
|
|
1021
|
-
} else {
|
|
1022
|
-
console.log("Repositories: (project not found)");
|
|
1023
|
-
}
|
|
1024
|
-
console.log("");
|
|
1025
|
-
for (const ticket of tickets) {
|
|
1026
|
-
const reqBadge = ticket.requirementStatus === "approved" ? " [approved]" : " [pending]";
|
|
1027
|
-
console.log(`#### ${formatTicketDisplay(ticket)}${reqBadge}`);
|
|
1028
|
-
if (ticket.description) {
|
|
1029
|
-
console.log("");
|
|
1030
|
-
console.log(ticket.description);
|
|
1031
|
-
}
|
|
1032
|
-
if (ticket.link) {
|
|
1033
|
-
console.log("");
|
|
1034
|
-
console.log(`Link: ${ticket.link}`);
|
|
1035
|
-
}
|
|
1036
|
-
if (ticket.requirements) {
|
|
1037
|
-
console.log("");
|
|
1038
|
-
console.log("**Refined Requirements:**");
|
|
1039
|
-
console.log("");
|
|
1040
|
-
console.log(ticket.requirements);
|
|
1041
|
-
}
|
|
1042
|
-
console.log("");
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
console.log("## Tasks");
|
|
1047
|
-
console.log("");
|
|
1048
|
-
if (tasks.length === 0) {
|
|
1049
|
-
console.log("_No tasks defined yet_");
|
|
1050
|
-
} else {
|
|
1051
|
-
for (const task of tasks) {
|
|
1052
|
-
const ticketRef = task.ticketId ? ` [${task.ticketId}]` : "";
|
|
1053
|
-
console.log(`### ${task.id}: ${task.name}${ticketRef}`);
|
|
1054
|
-
console.log(`Status: ${task.status} | Order: ${String(task.order)} | Project: ${task.projectPath}`);
|
|
1055
|
-
if (task.blockedBy.length > 0) {
|
|
1056
|
-
console.log(`Blocked By: ${task.blockedBy.join(", ")}`);
|
|
1057
|
-
}
|
|
1058
|
-
if (task.description) {
|
|
1059
|
-
console.log("");
|
|
1060
|
-
console.log(task.description);
|
|
1061
|
-
}
|
|
1062
|
-
if (task.steps.length > 0) {
|
|
1063
|
-
console.log("");
|
|
1064
|
-
console.log("Steps:");
|
|
1065
|
-
task.steps.forEach((step, i) => {
|
|
1066
|
-
console.log(`${String(i + 1)}. ${step}`);
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
console.log("");
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// src/commands/sprint/current.ts
|
|
1075
|
-
async function sprintCurrentCommand(args) {
|
|
1076
|
-
const sprintId = args[0];
|
|
1077
|
-
if (!sprintId) {
|
|
1078
|
-
const currentSprintId = await getCurrentSprint();
|
|
1079
|
-
if (!currentSprintId) {
|
|
1080
|
-
showWarning("No current sprint set.");
|
|
1081
|
-
showNextStep("ralphctl sprint create", "create a new sprint");
|
|
1082
|
-
log.newline();
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const sprintR = await wrapAsync(() => getSprint(currentSprintId), ensureError);
|
|
1086
|
-
if (sprintR.ok) {
|
|
1087
|
-
printHeader("Current Sprint");
|
|
1088
|
-
console.log(field("ID", sprintR.value.id));
|
|
1089
|
-
console.log(field("Name", sprintR.value.name));
|
|
1090
|
-
console.log(field("Status", formatSprintStatus(sprintR.value.status)));
|
|
1091
|
-
log.newline();
|
|
1092
|
-
} else {
|
|
1093
|
-
showWarning(`Current sprint "${currentSprintId}" no longer exists.`);
|
|
1094
|
-
showNextStep("ralphctl sprint current -", "select a different sprint");
|
|
1095
|
-
log.newline();
|
|
1096
|
-
}
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
if (sprintId === "-" || sprintId === "--select") {
|
|
1100
|
-
const selectedId = await selectSprint("Select current sprint:", ["draft", "active"]);
|
|
1101
|
-
if (!selectedId) return;
|
|
1102
|
-
await setCurrentSprint(selectedId);
|
|
1103
|
-
const sprint = await getSprint(selectedId);
|
|
1104
|
-
showSuccess("Current sprint set!", [
|
|
1105
|
-
["ID", sprint.id],
|
|
1106
|
-
["Name", sprint.name]
|
|
1107
|
-
]);
|
|
1108
|
-
log.newline();
|
|
1109
|
-
} else {
|
|
1110
|
-
const setR = await wrapAsync(async () => {
|
|
1111
|
-
const sprint = await getSprint(sprintId);
|
|
1112
|
-
await setCurrentSprint(sprintId);
|
|
1113
|
-
return sprint;
|
|
1114
|
-
}, ensureError);
|
|
1115
|
-
if (setR.ok) {
|
|
1116
|
-
showSuccess("Current sprint set!", [
|
|
1117
|
-
["ID", setR.value.id],
|
|
1118
|
-
["Name", setR.value.name]
|
|
1119
|
-
]);
|
|
1120
|
-
log.newline();
|
|
1121
|
-
} else if (setR.error instanceof SprintNotFoundError) {
|
|
1122
|
-
showError(`Sprint not found: ${sprintId}`);
|
|
1123
|
-
showNextStep("ralphctl sprint list", "see available sprints");
|
|
1124
|
-
log.newline();
|
|
1125
|
-
} else {
|
|
1126
|
-
throw setR.error;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// src/commands/sprint/ideate.ts
|
|
1132
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1133
|
-
import { join } from "path";
|
|
1134
|
-
import { input as input2, select as select2 } from "@inquirer/prompts";
|
|
1135
|
-
import { Result } from "typescript-result";
|
|
1136
|
-
function parseArgs(args) {
|
|
1137
|
-
const options = {
|
|
1138
|
-
auto: false,
|
|
1139
|
-
allPaths: false
|
|
1140
|
-
};
|
|
1141
|
-
let sprintId;
|
|
1142
|
-
for (let i = 0; i < args.length; i++) {
|
|
1143
|
-
const arg = args[i];
|
|
1144
|
-
const nextArg = args[i + 1];
|
|
1145
|
-
if (arg === "--auto") {
|
|
1146
|
-
options.auto = true;
|
|
1147
|
-
} else if (arg === "--all-paths") {
|
|
1148
|
-
options.allPaths = true;
|
|
1149
|
-
} else if (arg === "--project") {
|
|
1150
|
-
options.project = nextArg;
|
|
1151
|
-
i++;
|
|
1152
|
-
} else if (!arg?.startsWith("-")) {
|
|
1153
|
-
sprintId = arg;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
return { sprintId, options };
|
|
1157
|
-
}
|
|
1158
|
-
async function invokeAiInteractive(prompt, repoPaths, ideateDir) {
|
|
1159
|
-
const contextFile = join(ideateDir, "ideate-context.md");
|
|
1160
|
-
await writeFile(contextFile, prompt, "utf-8");
|
|
1161
|
-
const provider = await getActiveProvider();
|
|
1162
|
-
const startPrompt = `I have a quick idea I want to implement. The full context is in ideate-context.md. Please read that file and help me refine the idea into requirements and then plan implementation tasks.`;
|
|
1163
|
-
const args = repoPaths.flatMap((path) => ["--add-dir", path]);
|
|
1164
|
-
const result = spawnInteractive(
|
|
1165
|
-
startPrompt,
|
|
1166
|
-
{
|
|
1167
|
-
cwd: ideateDir,
|
|
1168
|
-
args,
|
|
1169
|
-
env: provider.getSpawnEnv()
|
|
1170
|
-
},
|
|
1171
|
-
provider
|
|
1172
|
-
);
|
|
1173
|
-
if (result.error) {
|
|
1174
|
-
throw new Error(result.error);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
async function invokeAiAuto(prompt, repoPaths, ideateDir) {
|
|
1178
|
-
const provider = await getActiveProvider();
|
|
1179
|
-
const request = buildHeadlessAiRequest(repoPaths, prompt);
|
|
1180
|
-
return spawnHeadless(
|
|
1181
|
-
{
|
|
1182
|
-
cwd: ideateDir,
|
|
1183
|
-
args: request.args,
|
|
1184
|
-
prompt: request.prompt,
|
|
1185
|
-
env: provider.getSpawnEnv()
|
|
1186
|
-
},
|
|
1187
|
-
provider
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
function parseIdeateOutput(output) {
|
|
1191
|
-
const firstBrace = output.indexOf("{");
|
|
1192
|
-
const firstBracket = output.indexOf("[");
|
|
1193
|
-
const objectFirst = firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket);
|
|
1194
|
-
if (objectFirst) {
|
|
1195
|
-
return parseIdeateObject(output);
|
|
1196
|
-
}
|
|
1197
|
-
if (firstBracket !== -1) {
|
|
1198
|
-
const arrayR = Result.try(() => extractJsonArray(output));
|
|
1199
|
-
if (arrayR.ok) {
|
|
1200
|
-
const parseR = Result.try(() => JSON.parse(arrayR.value));
|
|
1201
|
-
if (parseR.ok && Array.isArray(parseR.value)) {
|
|
1202
|
-
return { requirements: "", tasks: parseR.value };
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
throw new Error("No valid ideate output found \u2014 expected { requirements, tasks } object or a tasks array");
|
|
1207
|
-
}
|
|
1208
|
-
function parseIdeateObject(output) {
|
|
1209
|
-
const jsonStr = extractJsonObject(output);
|
|
1210
|
-
const parsed = JSON.parse(jsonStr);
|
|
1211
|
-
const result = IdeateOutputSchema.safeParse(parsed);
|
|
1212
|
-
if (result.success) {
|
|
1213
|
-
return result.data;
|
|
1214
|
-
}
|
|
1215
|
-
if (typeof parsed === "object" && parsed !== null && "tasks" in parsed) {
|
|
1216
|
-
const obj = parsed;
|
|
1217
|
-
if (Array.isArray(obj["tasks"])) {
|
|
1218
|
-
const requirements = typeof obj["requirements"] === "string" ? obj["requirements"] : "";
|
|
1219
|
-
return { requirements, tasks: obj["tasks"] };
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
const issues = result.error.issues.map((issue) => {
|
|
1223
|
-
const path = issue.path.length > 0 ? `[${issue.path.join(".")}]` : "";
|
|
1224
|
-
return ` ${path}: ${issue.message}`;
|
|
1225
|
-
}).join("\n");
|
|
1226
|
-
throw new Error(`Invalid ideate output format:
|
|
1227
|
-
${issues}`);
|
|
1228
|
-
}
|
|
1229
|
-
async function sprintIdeateCommand(args) {
|
|
1230
|
-
const { sprintId, options } = parseArgs(args);
|
|
1231
|
-
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
1232
|
-
if (!idR.ok) {
|
|
1233
|
-
showWarning("No sprint specified and no current sprint set.");
|
|
1234
|
-
showNextStep("ralphctl sprint create", "create a new sprint");
|
|
1235
|
-
log.newline();
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
const id = idR.value;
|
|
1239
|
-
const sprint = await getSprint(id);
|
|
1240
|
-
try {
|
|
1241
|
-
assertSprintStatus(sprint, ["draft"], "ideate");
|
|
1242
|
-
} catch (err) {
|
|
1243
|
-
showError(err instanceof Error ? err.message : String(err));
|
|
1244
|
-
log.newline();
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
const projects = await listProjects();
|
|
1248
|
-
if (projects.length === 0) {
|
|
1249
|
-
showWarning("No projects configured.");
|
|
1250
|
-
showNextStep("ralphctl project add", "add a project first");
|
|
1251
|
-
log.newline();
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
printHeader("Quick Ideation", icons.ticket);
|
|
1255
|
-
console.log(field("Sprint", sprint.name));
|
|
1256
|
-
console.log(field("ID", sprint.id));
|
|
1257
|
-
console.log(field("Mode", options.auto ? "Auto (headless)" : "Interactive"));
|
|
1258
|
-
log.newline();
|
|
1259
|
-
let projectName = options.project;
|
|
1260
|
-
if (!projectName) {
|
|
1261
|
-
if (projects.length === 1) {
|
|
1262
|
-
projectName = projects[0]?.name;
|
|
1263
|
-
console.log(field("Project", projectName ?? "(unknown)"));
|
|
1264
|
-
} else {
|
|
1265
|
-
projectName = await select2({
|
|
1266
|
-
message: "Select project:",
|
|
1267
|
-
choices: projects.map((p) => ({ name: p.displayName, value: p.name }))
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
if (!projectName) {
|
|
1272
|
-
showError("No project selected.");
|
|
1273
|
-
log.newline();
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
1277
|
-
if (!projectR.ok) {
|
|
1278
|
-
showError(`Project '${projectName}' not found.`);
|
|
1279
|
-
log.newline();
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
const project = projectR.value;
|
|
1283
|
-
const ideaTitle = await input2({
|
|
1284
|
-
message: "Idea title (short summary):",
|
|
1285
|
-
validate: (value) => value.trim().length > 0 ? true : "Title is required"
|
|
1286
|
-
});
|
|
1287
|
-
const editorR = await editorInput({
|
|
1288
|
-
message: "Idea description (what you want to build):"
|
|
1289
|
-
});
|
|
1290
|
-
if (!editorR.ok) {
|
|
1291
|
-
showError(`Editor input failed: ${editorR.error.message}`);
|
|
1292
|
-
log.newline();
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
const ideaDescription = editorR.value;
|
|
1296
|
-
if (!ideaDescription.trim()) {
|
|
1297
|
-
showError("Description is required.");
|
|
1298
|
-
log.newline();
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
log.newline();
|
|
1302
|
-
showInfo("Creating ticket...");
|
|
1303
|
-
const ticket = await addTicket(
|
|
1304
|
-
{
|
|
1305
|
-
title: ideaTitle,
|
|
1306
|
-
description: ideaDescription,
|
|
1307
|
-
projectName
|
|
1308
|
-
},
|
|
1309
|
-
id
|
|
1310
|
-
);
|
|
1311
|
-
console.log(field("Ticket ID", ticket.id));
|
|
1312
|
-
log.newline();
|
|
1313
|
-
const providerName = providerDisplayName(await resolveProvider());
|
|
1314
|
-
let selectedPaths;
|
|
1315
|
-
const totalRepos = project.repositories.length;
|
|
1316
|
-
if (options.allPaths) {
|
|
1317
|
-
selectedPaths = project.repositories.map((r) => r.path);
|
|
1318
|
-
} else if (options.auto) {
|
|
1319
|
-
selectedPaths = project.repositories.slice(0, 1).map((r) => r.path);
|
|
1320
|
-
} else if (totalRepos === 1) {
|
|
1321
|
-
selectedPaths = [project.repositories[0]?.path ?? ""];
|
|
1322
|
-
} else {
|
|
1323
|
-
const reposByProject = /* @__PURE__ */ new Map();
|
|
1324
|
-
reposByProject.set(projectName, project.repositories);
|
|
1325
|
-
selectedPaths = await selectProjectPaths(reposByProject, "Select paths to explore:");
|
|
1326
|
-
}
|
|
1327
|
-
const updatedSprint = await getSprint(id);
|
|
1328
|
-
const savedTicket = updatedSprint.tickets.find((t) => t.id === ticket.id);
|
|
1329
|
-
if (savedTicket) {
|
|
1330
|
-
savedTicket.affectedRepositories = selectedPaths;
|
|
1331
|
-
}
|
|
1332
|
-
await saveSprint(updatedSprint);
|
|
1333
|
-
if (selectedPaths.length > 1) {
|
|
1334
|
-
console.log(muted(`Paths: ${selectedPaths.join(", ")}`));
|
|
1335
|
-
} else {
|
|
1336
|
-
console.log(muted(`Path: ${selectedPaths[0] ?? process.cwd()}`));
|
|
1337
|
-
}
|
|
1338
|
-
const repositoriesText = selectedPaths.map((path) => `- ${path}`).join("\n");
|
|
1339
|
-
const schema = await getTaskImportSchema();
|
|
1340
|
-
const ideateDir = getIdeateDir(id, ticket.id);
|
|
1341
|
-
await mkdir(ideateDir, { recursive: true });
|
|
1342
|
-
const projectToolingSection = buildProjectToolingSection(selectedPaths);
|
|
1343
|
-
if (options.auto) {
|
|
1344
|
-
const prompt = buildIdeateAutoPrompt(
|
|
1345
|
-
ideaTitle,
|
|
1346
|
-
ideaDescription,
|
|
1347
|
-
projectName,
|
|
1348
|
-
repositoriesText,
|
|
1349
|
-
schema,
|
|
1350
|
-
projectToolingSection
|
|
1351
|
-
);
|
|
1352
|
-
const spinner = createSpinner(`${providerName} is refining idea and planning tasks...`);
|
|
1353
|
-
spinner.start();
|
|
1354
|
-
const outputR = await wrapAsync(() => invokeAiAuto(prompt, selectedPaths, ideateDir), ensureError);
|
|
1355
|
-
if (!outputR.ok) {
|
|
1356
|
-
spinner.fail(`${providerName} session failed`);
|
|
1357
|
-
showError(`Failed to invoke ${providerName}: ${outputR.error.message}`);
|
|
1358
|
-
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
1359
|
-
log.newline();
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
spinner.succeed(`${providerName} finished`);
|
|
1363
|
-
const output = outputR.value;
|
|
1364
|
-
const blockedReason = parsePlanningBlocked(output);
|
|
1365
|
-
if (blockedReason) {
|
|
1366
|
-
showWarning(`Planning blocked: ${blockedReason}`);
|
|
1367
|
-
log.newline();
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
log.dim("Parsing response...");
|
|
1371
|
-
const ideateR = Result.try(() => parseIdeateOutput(output));
|
|
1372
|
-
if (!ideateR.ok) {
|
|
1373
|
-
showError(`Failed to parse ${providerName} output: ${ideateR.error.message}`);
|
|
1374
|
-
log.dim("Raw output:");
|
|
1375
|
-
console.log(output);
|
|
1376
|
-
log.newline();
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
const ideateOutput = ideateR.value;
|
|
1380
|
-
const autoSprint = await getSprint(id);
|
|
1381
|
-
const autoTicket = autoSprint.tickets.find((t) => t.id === ticket.id);
|
|
1382
|
-
if (autoTicket) {
|
|
1383
|
-
autoTicket.requirements = ideateOutput.requirements;
|
|
1384
|
-
autoTicket.requirementStatus = "approved";
|
|
1385
|
-
}
|
|
1386
|
-
await saveSprint(autoSprint);
|
|
1387
|
-
showSuccess("Requirements approved and saved!");
|
|
1388
|
-
log.newline();
|
|
1389
|
-
const parsedTasksR = Result.try(() => parseTasksJson(JSON.stringify(ideateOutput.tasks)));
|
|
1390
|
-
if (!parsedTasksR.ok) {
|
|
1391
|
-
showError(`Failed to parse tasks: ${parsedTasksR.error.message}`);
|
|
1392
|
-
log.newline();
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
const parsedTasks = parsedTasksR.value;
|
|
1396
|
-
for (const task of parsedTasks) {
|
|
1397
|
-
task.ticketId ??= ticket.id;
|
|
1398
|
-
}
|
|
1399
|
-
if (parsedTasks.length === 0) {
|
|
1400
|
-
showWarning("No tasks generated.");
|
|
1401
|
-
log.newline();
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
showSuccess(`Generated ${String(parsedTasks.length)} task(s):`);
|
|
1405
|
-
log.newline();
|
|
1406
|
-
console.log(renderParsedTasksTable(parsedTasks));
|
|
1407
|
-
console.log("");
|
|
1408
|
-
const existingTasks = await getTasks(id);
|
|
1409
|
-
const ticketIds = new Set(autoSprint.tickets.map((t) => t.id));
|
|
1410
|
-
const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
|
|
1411
|
-
if (validationErrors.length > 0) {
|
|
1412
|
-
showError("Validation failed");
|
|
1413
|
-
for (const err of validationErrors) {
|
|
1414
|
-
log.item(error(err));
|
|
1415
|
-
}
|
|
1416
|
-
log.newline();
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
if (ideateOutput.requirements === "") {
|
|
1420
|
-
showWarning("AI output was a bare tasks array \u2014 requirements not captured.");
|
|
1421
|
-
}
|
|
1422
|
-
showInfo("Importing tasks...");
|
|
1423
|
-
const imported = await importTasks(parsedTasks, id);
|
|
1424
|
-
await reorderByDependencies(id);
|
|
1425
|
-
log.dim("Tasks reordered by dependencies.");
|
|
1426
|
-
terminalBell();
|
|
1427
|
-
showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
|
|
1428
|
-
log.newline();
|
|
1429
|
-
} else {
|
|
1430
|
-
const outputFile = join(ideateDir, "output.json");
|
|
1431
|
-
const prompt = buildIdeatePrompt(
|
|
1432
|
-
ideaTitle,
|
|
1433
|
-
ideaDescription,
|
|
1434
|
-
projectName,
|
|
1435
|
-
repositoriesText,
|
|
1436
|
-
outputFile,
|
|
1437
|
-
schema,
|
|
1438
|
-
projectToolingSection
|
|
1439
|
-
);
|
|
1440
|
-
showInfo(`Starting interactive ${providerName} session...`);
|
|
1441
|
-
console.log(muted(` Exploring: ${selectedPaths.join(", ")}`));
|
|
1442
|
-
console.log(muted(`
|
|
1443
|
-
${providerName} will guide you through requirements refinement and task planning.`));
|
|
1444
|
-
console.log(muted(` When done, ask ${providerName} to write the output to: ${outputFile}
|
|
1445
|
-
`));
|
|
1446
|
-
const interactiveR = await wrapAsync(() => invokeAiInteractive(prompt, selectedPaths, ideateDir), ensureError);
|
|
1447
|
-
if (!interactiveR.ok) {
|
|
1448
|
-
showError(`Failed to invoke ${providerName}: ${interactiveR.error.message}`);
|
|
1449
|
-
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
1450
|
-
log.newline();
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
console.log("");
|
|
1454
|
-
if (await fileExists(outputFile)) {
|
|
1455
|
-
showInfo("Output file found. Processing...");
|
|
1456
|
-
const contentR = await wrapAsync(() => readFile(outputFile, "utf-8"), ensureError);
|
|
1457
|
-
if (!contentR.ok) {
|
|
1458
|
-
showError(`Failed to read output file: ${outputFile}`);
|
|
1459
|
-
log.newline();
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
const ideateR = Result.try(() => parseIdeateOutput(contentR.value));
|
|
1463
|
-
if (!ideateR.ok) {
|
|
1464
|
-
showError(`Failed to parse output file: ${ideateR.error.message}`);
|
|
1465
|
-
log.newline();
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
const ideateOutput = ideateR.value;
|
|
1469
|
-
const interactiveSprint = await getSprint(id);
|
|
1470
|
-
const interactiveTicket = interactiveSprint.tickets.find((t) => t.id === ticket.id);
|
|
1471
|
-
if (interactiveTicket) {
|
|
1472
|
-
interactiveTicket.requirements = ideateOutput.requirements;
|
|
1473
|
-
interactiveTicket.requirementStatus = "approved";
|
|
1474
|
-
}
|
|
1475
|
-
await saveSprint(interactiveSprint);
|
|
1476
|
-
showSuccess("Requirements approved and saved!");
|
|
1477
|
-
log.newline();
|
|
1478
|
-
const parsedTasksR = Result.try(() => parseTasksJson(JSON.stringify(ideateOutput.tasks)));
|
|
1479
|
-
if (!parsedTasksR.ok) {
|
|
1480
|
-
showError(`Failed to parse tasks: ${parsedTasksR.error.message}`);
|
|
1481
|
-
log.newline();
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
const parsedTasks = parsedTasksR.value;
|
|
1485
|
-
for (const task of parsedTasks) {
|
|
1486
|
-
task.ticketId ??= ticket.id;
|
|
1487
|
-
}
|
|
1488
|
-
if (parsedTasks.length === 0) {
|
|
1489
|
-
showWarning("No tasks in file.");
|
|
1490
|
-
log.newline();
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
showSuccess(`Found ${String(parsedTasks.length)} task(s):`);
|
|
1494
|
-
log.newline();
|
|
1495
|
-
console.log(renderParsedTasksTable(parsedTasks));
|
|
1496
|
-
console.log("");
|
|
1497
|
-
const existingTasks = await getTasks(id);
|
|
1498
|
-
const ticketIds = new Set(interactiveSprint.tickets.map((t) => t.id));
|
|
1499
|
-
const validationErrors = validateImportTasks(parsedTasks, existingTasks, ticketIds);
|
|
1500
|
-
if (validationErrors.length > 0) {
|
|
1501
|
-
showError("Validation failed");
|
|
1502
|
-
for (const err of validationErrors) {
|
|
1503
|
-
log.item(error(err));
|
|
1504
|
-
}
|
|
1505
|
-
log.newline();
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
if (ideateOutput.requirements === "") {
|
|
1509
|
-
showWarning("AI output was a bare tasks array \u2014 requirements not captured.");
|
|
1510
|
-
}
|
|
1511
|
-
showInfo("Importing tasks...");
|
|
1512
|
-
const imported = await importTasks(parsedTasks, id);
|
|
1513
|
-
await reorderByDependencies(id);
|
|
1514
|
-
log.dim("Tasks reordered by dependencies.");
|
|
1515
|
-
terminalBell();
|
|
1516
|
-
showSuccess(`Imported ${String(imported)}/${String(parsedTasks.length)} tasks.`);
|
|
1517
|
-
log.newline();
|
|
1518
|
-
} else {
|
|
1519
|
-
showWarning("No output file found.");
|
|
1520
|
-
showTip(`Expected: ${outputFile}`);
|
|
1521
|
-
showNextStep("ralphctl sprint ideate", "run ideation again");
|
|
1522
|
-
log.newline();
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// src/commands/sprint/close.ts
|
|
1528
|
-
import { spawnSync } from "child_process";
|
|
1529
|
-
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1530
|
-
import { Result as Result2 } from "typescript-result";
|
|
1531
|
-
async function sprintCloseCommand(args) {
|
|
1532
|
-
let sprintId;
|
|
1533
|
-
let createPr = false;
|
|
1534
|
-
const positionalArgs = [];
|
|
1535
|
-
for (const arg of args) {
|
|
1536
|
-
if (arg === "--create-pr") {
|
|
1537
|
-
createPr = true;
|
|
1538
|
-
} else {
|
|
1539
|
-
positionalArgs.push(arg);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (positionalArgs[0]) {
|
|
1543
|
-
sprintId = positionalArgs[0];
|
|
1544
|
-
} else {
|
|
1545
|
-
const sprints = await listSprints();
|
|
1546
|
-
const activeSprints = sprints.filter((s) => s.status === "active");
|
|
1547
|
-
if (activeSprints.length === 0) {
|
|
1548
|
-
showError("No active sprints to close.");
|
|
1549
|
-
log.newline();
|
|
1550
|
-
return;
|
|
1551
|
-
} else if (activeSprints.length === 1 && activeSprints[0]) {
|
|
1552
|
-
sprintId = activeSprints[0].id;
|
|
1553
|
-
} else {
|
|
1554
|
-
const selected = await selectSprint("Select sprint to close:", ["active"]);
|
|
1555
|
-
if (!selected) return;
|
|
1556
|
-
sprintId = selected;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const allDone = await areAllTasksDone(sprintId);
|
|
1560
|
-
if (!allDone) {
|
|
1561
|
-
const tasks = await listTasks(sprintId);
|
|
1562
|
-
const remaining = tasks.filter((t) => t.status !== "done");
|
|
1563
|
-
log.newline();
|
|
1564
|
-
showWarning(`${String(remaining.length)} task(s) are not done:`);
|
|
1565
|
-
for (const task of remaining) {
|
|
1566
|
-
log.item(`${task.id}: ${task.name} (${task.status})`);
|
|
1567
|
-
}
|
|
1568
|
-
log.newline();
|
|
1569
|
-
const proceed = await confirm3({
|
|
1570
|
-
message: "Close sprint anyway?",
|
|
1571
|
-
default: false
|
|
1572
|
-
});
|
|
1573
|
-
if (!proceed) {
|
|
1574
|
-
console.log(muted("\nSprint close cancelled.\n"));
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
const closeR = await wrapAsync(async () => {
|
|
1579
|
-
const sprintBeforeClose2 = await getSprint(sprintId);
|
|
1580
|
-
const sprint2 = await closeSprint(sprintId);
|
|
1581
|
-
return { sprintBeforeClose: sprintBeforeClose2, sprint: sprint2 };
|
|
1582
|
-
}, ensureError);
|
|
1583
|
-
if (!closeR.ok) {
|
|
1584
|
-
const err = closeR.error;
|
|
1585
|
-
if (err instanceof SprintNotFoundError) {
|
|
1586
|
-
showError(`Sprint not found: ${sprintId}`);
|
|
1587
|
-
log.newline();
|
|
1588
|
-
} else if (err instanceof SprintStatusError) {
|
|
1589
|
-
showError(err.message);
|
|
1590
|
-
log.newline();
|
|
1591
|
-
} else {
|
|
1592
|
-
throw err;
|
|
1593
|
-
}
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
const { sprintBeforeClose, sprint } = closeR.value;
|
|
1597
|
-
showSuccess("Sprint closed!", [
|
|
1598
|
-
["ID", sprint.id],
|
|
1599
|
-
["Name", sprint.name],
|
|
1600
|
-
["Status", formatSprintStatus(sprint.status)]
|
|
1601
|
-
]);
|
|
1602
|
-
showRandomQuote();
|
|
1603
|
-
log.newline();
|
|
1604
|
-
if (createPr && sprintBeforeClose.branch) {
|
|
1605
|
-
await createPullRequests(sprintId, sprintBeforeClose.branch, sprint.name);
|
|
1606
|
-
} else if (createPr && !sprintBeforeClose.branch) {
|
|
1607
|
-
log.dim("No sprint branch set \u2014 skipping PR creation.");
|
|
1608
|
-
log.newline();
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
async function createPullRequests(sprintId, branchName, sprintName) {
|
|
1612
|
-
if (!isGhAvailable()) {
|
|
1613
|
-
showWarning("GitHub CLI (gh) not found. Install it to create PRs automatically.");
|
|
1614
|
-
log.dim(` Manual: gh pr create --head ${branchName} --title "Sprint: ${sprintName}"`);
|
|
1615
|
-
log.newline();
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
const tasks = await listTasks(sprintId);
|
|
1619
|
-
const uniquePaths = [...new Set(tasks.map((t) => t.projectPath))];
|
|
1620
|
-
for (const projectPath of uniquePaths) {
|
|
1621
|
-
const prR = Result2.try(() => {
|
|
1622
|
-
assertSafeCwd(projectPath);
|
|
1623
|
-
return { branchExists: branchExists(projectPath, branchName) };
|
|
1624
|
-
});
|
|
1625
|
-
if (!prR.ok) {
|
|
1626
|
-
showWarning(`Error creating PR for ${projectPath}: ${prR.error.message}`);
|
|
1627
|
-
continue;
|
|
1628
|
-
}
|
|
1629
|
-
if (!prR.value.branchExists) {
|
|
1630
|
-
log.dim(`Branch '${branchName}' not found in ${projectPath} \u2014 skipping`);
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
const baseBranch = getDefaultBranch(projectPath);
|
|
1634
|
-
const title = `Sprint: ${sprintName}`;
|
|
1635
|
-
log.info(`Creating PR in ${projectPath}...`);
|
|
1636
|
-
const pushResult = spawnSync("git", ["push", "-u", "origin", branchName], {
|
|
1637
|
-
cwd: projectPath,
|
|
1638
|
-
encoding: "utf-8",
|
|
1639
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1640
|
-
});
|
|
1641
|
-
if (pushResult.status !== 0) {
|
|
1642
|
-
showWarning(`Failed to push branch in ${projectPath}: ${pushResult.stderr.trim()}`);
|
|
1643
|
-
log.dim(
|
|
1644
|
-
` Manual: cd ${projectPath} && git push -u origin ${branchName} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
|
|
1645
|
-
);
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
const result = spawnSync(
|
|
1649
|
-
"gh",
|
|
1650
|
-
[
|
|
1651
|
-
"pr",
|
|
1652
|
-
"create",
|
|
1653
|
-
"--base",
|
|
1654
|
-
baseBranch,
|
|
1655
|
-
"--head",
|
|
1656
|
-
branchName,
|
|
1657
|
-
"--title",
|
|
1658
|
-
title,
|
|
1659
|
-
"--body",
|
|
1660
|
-
`Sprint: ${sprintName}
|
|
1661
|
-
ID: ${sprintId}`
|
|
1662
|
-
],
|
|
1663
|
-
{
|
|
1664
|
-
cwd: projectPath,
|
|
1665
|
-
encoding: "utf-8",
|
|
1666
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1667
|
-
}
|
|
1668
|
-
);
|
|
1669
|
-
if (result.status === 0) {
|
|
1670
|
-
const prUrl = result.stdout.trim();
|
|
1671
|
-
showSuccess(`PR created: ${prUrl}`);
|
|
1672
|
-
} else {
|
|
1673
|
-
showWarning(`Failed to create PR in ${projectPath}: ${result.stderr.trim()}`);
|
|
1674
|
-
log.dim(
|
|
1675
|
-
` Manual: cd ${projectPath} && gh pr create --base ${baseBranch} --head ${branchName} --title "${title}"`
|
|
1676
|
-
);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
log.newline();
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// src/commands/sprint/delete.ts
|
|
1683
|
-
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1684
|
-
async function sprintDeleteCommand(args) {
|
|
1685
|
-
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
1686
|
-
let sprintId = args.find((a) => !a.startsWith("-"));
|
|
1687
|
-
if (!sprintId) {
|
|
1688
|
-
const selected = await selectSprint("Select sprint to delete:");
|
|
1689
|
-
if (!selected) return;
|
|
1690
|
-
sprintId = selected;
|
|
1691
|
-
}
|
|
1692
|
-
const sprintR = await wrapAsync(() => getSprint(sprintId), ensureError);
|
|
1693
|
-
if (!sprintR.ok) {
|
|
1694
|
-
if (sprintR.error instanceof SprintNotFoundError) {
|
|
1695
|
-
showError(`Sprint not found: ${sprintId}`);
|
|
1696
|
-
log.newline();
|
|
1697
|
-
} else {
|
|
1698
|
-
throw sprintR.error;
|
|
1699
|
-
}
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
const sprint = sprintR.value;
|
|
1703
|
-
let taskCount = 0;
|
|
1704
|
-
const tasksR = await wrapAsync(() => listTasks(sprintId), ensureError);
|
|
1705
|
-
if (tasksR.ok) {
|
|
1706
|
-
taskCount = tasksR.value.length;
|
|
1707
|
-
}
|
|
1708
|
-
if (!skipConfirm) {
|
|
1709
|
-
log.newline();
|
|
1710
|
-
log.warn("This will permanently delete the sprint and all its data.");
|
|
1711
|
-
log.item(`Name: ${sprint.name}`);
|
|
1712
|
-
log.item(`Status: ${formatSprintStatus(sprint.status)}`);
|
|
1713
|
-
log.item(`Tickets: ${String(sprint.tickets.length)}`);
|
|
1714
|
-
log.item(`Tasks: ${String(taskCount)}`);
|
|
1715
|
-
log.newline();
|
|
1716
|
-
const confirmed = await confirm4({
|
|
1717
|
-
message: `Delete sprint "${sprint.name}"?`,
|
|
1718
|
-
default: false
|
|
1719
|
-
});
|
|
1720
|
-
if (!confirmed) {
|
|
1721
|
-
console.log(muted("\nSprint deletion cancelled.\n"));
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
const currentSprintId = await getCurrentSprint();
|
|
1726
|
-
await deleteSprint(sprintId);
|
|
1727
|
-
if (currentSprintId === sprintId) {
|
|
1728
|
-
await setCurrentSprint(null);
|
|
1729
|
-
showTip('Current sprint was cleared. Use "ralphctl sprint current" to set a new one.');
|
|
1730
|
-
}
|
|
1731
|
-
showSuccess("Sprint deleted", [
|
|
1732
|
-
["Name", sprint.name],
|
|
1733
|
-
["ID", sprint.id]
|
|
1734
|
-
]);
|
|
1735
|
-
showRandomQuote();
|
|
1736
|
-
log.newline();
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
// src/commands/sprint/requirements.ts
|
|
1740
|
-
import { join as join2 } from "path";
|
|
1741
|
-
async function sprintRequirementsCommand(args = []) {
|
|
1742
|
-
const sprintId = args.find((a) => !a.startsWith("-"));
|
|
1743
|
-
let id;
|
|
1744
|
-
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
1745
|
-
if (!idR.ok) {
|
|
1746
|
-
const selected = await selectSprint("Select sprint to export requirements from:");
|
|
1747
|
-
if (!selected) return;
|
|
1748
|
-
id = selected;
|
|
1749
|
-
} else {
|
|
1750
|
-
id = idR.value;
|
|
1751
|
-
}
|
|
1752
|
-
const sprint = await getSprint(id);
|
|
1753
|
-
if (sprint.tickets.length === 0) {
|
|
1754
|
-
showEmpty("tickets in this sprint", "Add tickets first: ralphctl ticket add --project <name>");
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === "approved");
|
|
1758
|
-
if (approvedTickets.length === 0) {
|
|
1759
|
-
showWarning("No approved requirements to export.");
|
|
1760
|
-
log.dim("Refine requirements first: ralphctl sprint refine");
|
|
1761
|
-
log.newline();
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
printHeader("Export Requirements", icons.sprint);
|
|
1765
|
-
console.log(field("Sprint", sprint.name));
|
|
1766
|
-
console.log(field("Tickets", `${String(sprint.tickets.length)} total, ${String(approvedTickets.length)} approved`));
|
|
1767
|
-
log.newline();
|
|
1768
|
-
const sprintDir = getSprintDir(id);
|
|
1769
|
-
const outputPath = join2(sprintDir, "requirements.md");
|
|
1770
|
-
const exportR = await wrapAsync(() => exportRequirementsToMarkdown(sprint, outputPath), ensureError);
|
|
1771
|
-
if (!exportR.ok) {
|
|
1772
|
-
showError(`Failed to write requirements: ${exportR.error.message}`);
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
showSuccess("Requirements written to:");
|
|
1776
|
-
log.item(outputPath);
|
|
1777
|
-
log.newline();
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// src/commands/sprint/health.ts
|
|
1781
|
-
import { Result as Result3 } from "typescript-result";
|
|
1782
|
-
function checkBlockers(tasks) {
|
|
1783
|
-
const doneTasks = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
|
|
1784
|
-
const allTaskIds = new Set(tasks.map((t) => t.id));
|
|
1785
|
-
const blocked = [];
|
|
1786
|
-
for (const task of tasks) {
|
|
1787
|
-
if (task.status === "done") continue;
|
|
1788
|
-
const unresolvedDeps = task.blockedBy.filter((depId) => allTaskIds.has(depId) && !doneTasks.has(depId));
|
|
1789
|
-
if (unresolvedDeps.length > 0) {
|
|
1790
|
-
blocked.push(`${task.name} ${colors.muted(`(${task.id})`)} blocked by ${unresolvedDeps.join(", ")}`);
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
return {
|
|
1794
|
-
name: "Blockers",
|
|
1795
|
-
status: blocked.length > 0 ? "fail" : "pass",
|
|
1796
|
-
items: blocked
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
function checkStaleTasks(tasks) {
|
|
1800
|
-
const stale = tasks.filter((t) => t.status === "in_progress");
|
|
1801
|
-
const items = stale.map((t) => `${t.name} ${colors.muted(`(${t.id})`)}`);
|
|
1802
|
-
return {
|
|
1803
|
-
name: "Stale Tasks",
|
|
1804
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1805
|
-
items
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
function checkOrphanedDeps(tasks) {
|
|
1809
|
-
const allTaskIds = new Set(tasks.map((t) => t.id));
|
|
1810
|
-
const orphaned = [];
|
|
1811
|
-
for (const task of tasks) {
|
|
1812
|
-
const missingDeps = task.blockedBy.filter((depId) => !allTaskIds.has(depId));
|
|
1813
|
-
if (missingDeps.length > 0) {
|
|
1814
|
-
orphaned.push(`${task.name} ${colors.muted(`(${task.id})`)} references missing: ${missingDeps.join(", ")}`);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
return {
|
|
1818
|
-
name: "Orphaned Dependencies",
|
|
1819
|
-
status: orphaned.length > 0 ? "fail" : "pass",
|
|
1820
|
-
items: orphaned
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
function checkTicketsWithoutTasks(sprint, tasks) {
|
|
1824
|
-
const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
|
|
1825
|
-
const orphanedTickets = sprint.tickets.filter((t) => !ticketIdsWithTasks.has(t.id));
|
|
1826
|
-
const items = orphanedTickets.map((t) => `${t.title} ${colors.muted(`(${t.id})`)}`);
|
|
1827
|
-
return {
|
|
1828
|
-
name: "Tickets Without Tasks",
|
|
1829
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1830
|
-
items
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
function checkDuplicateOrders(tasks) {
|
|
1834
|
-
const orderCounts = /* @__PURE__ */ new Map();
|
|
1835
|
-
for (const task of tasks) {
|
|
1836
|
-
const existing = orderCounts.get(task.order) ?? [];
|
|
1837
|
-
existing.push(`${task.name} ${colors.muted(`(${task.id})`)}`);
|
|
1838
|
-
orderCounts.set(task.order, existing);
|
|
1839
|
-
}
|
|
1840
|
-
const items = [];
|
|
1841
|
-
for (const [order, taskNames] of orderCounts) {
|
|
1842
|
-
if (taskNames.length > 1) {
|
|
1843
|
-
items.push(`Order ${String(order)}: ${taskNames.join(", ")}`);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
return {
|
|
1847
|
-
name: "Duplicate Task Orders",
|
|
1848
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1849
|
-
items
|
|
1850
|
-
};
|
|
1851
|
-
}
|
|
1852
|
-
function checkPendingRequirementsOnActive(sprint) {
|
|
1853
|
-
if (sprint.status !== "active") {
|
|
1854
|
-
return { name: "Pending Requirements", status: "pass", items: [] };
|
|
1855
|
-
}
|
|
1856
|
-
const pending = sprint.tickets.filter((t) => t.requirementStatus === "pending");
|
|
1857
|
-
const items = pending.map((t) => `${t.title} ${colors.muted(`(${t.id})`)} \u2014 refine before planning`);
|
|
1858
|
-
return {
|
|
1859
|
-
name: "Pending Requirements",
|
|
1860
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1861
|
-
items
|
|
1862
|
-
};
|
|
1863
|
-
}
|
|
1864
|
-
function checkBranchConsistency(sprint, tasks) {
|
|
1865
|
-
if (!sprint.branch) {
|
|
1866
|
-
return { name: "Branch Consistency", status: "pass", items: [] };
|
|
1867
|
-
}
|
|
1868
|
-
const remainingTasks = tasks.filter((t) => t.status !== "done");
|
|
1869
|
-
const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
|
|
1870
|
-
const items = [];
|
|
1871
|
-
for (const projectPath of uniquePaths) {
|
|
1872
|
-
const branchR = Result3.try(() => getCurrentBranch(projectPath));
|
|
1873
|
-
if (!branchR.ok) {
|
|
1874
|
-
items.push(`${projectPath} \u2014 unable to determine branch`);
|
|
1875
|
-
} else if (branchR.value !== sprint.branch) {
|
|
1876
|
-
items.push(`${projectPath} \u2014 on '${branchR.value}', expected '${sprint.branch}'`);
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
return {
|
|
1880
|
-
name: "Branch Consistency",
|
|
1881
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1882
|
-
items
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
function checkTasksWithoutSteps(tasks) {
|
|
1886
|
-
const empty = tasks.filter((t) => t.steps.length === 0);
|
|
1887
|
-
const items = empty.map((t) => `${t.name} ${colors.muted(`(${t.id})`)}`);
|
|
1888
|
-
return {
|
|
1889
|
-
name: "Tasks Without Steps",
|
|
1890
|
-
status: items.length > 0 ? "warn" : "pass",
|
|
1891
|
-
items
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
function renderCheckCard(check) {
|
|
1895
|
-
const colorFn = check.status === "pass" ? colors.success : check.status === "warn" ? colors.warning : colors.error;
|
|
1896
|
-
const statusIcon = check.status === "pass" ? icons.success : check.status === "warn" ? icons.warning : icons.error;
|
|
1897
|
-
const lines = [];
|
|
1898
|
-
if (check.items.length === 0) {
|
|
1899
|
-
lines.push(colors.success(`${icons.success} No issues found`));
|
|
1900
|
-
} else {
|
|
1901
|
-
for (const item of check.items) {
|
|
1902
|
-
lines.push(`${colorFn(statusIcon)} ${item}`);
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
return renderCard(check.name, lines, { colorFn });
|
|
1906
|
-
}
|
|
1907
|
-
async function sprintHealthCommand() {
|
|
1908
|
-
const sprintR = await wrapAsync(() => getCurrentSprintOrThrow(), ensureError);
|
|
1909
|
-
if (!sprintR.ok) {
|
|
1910
|
-
showError(sprintR.error.message);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
const sprint = sprintR.value;
|
|
1914
|
-
const tasks = await getTasks(sprint.id);
|
|
1915
|
-
printHeader(`Sprint Health: ${sprint.name}`, icons.sprint);
|
|
1916
|
-
const checks = [
|
|
1917
|
-
checkBlockers(tasks),
|
|
1918
|
-
checkStaleTasks(tasks),
|
|
1919
|
-
checkOrphanedDeps(tasks),
|
|
1920
|
-
checkTicketsWithoutTasks(sprint, tasks),
|
|
1921
|
-
checkTasksWithoutSteps(tasks),
|
|
1922
|
-
checkDuplicateOrders(tasks),
|
|
1923
|
-
checkPendingRequirementsOnActive(sprint),
|
|
1924
|
-
checkBranchConsistency(sprint, tasks)
|
|
1925
|
-
];
|
|
1926
|
-
for (const check of checks) {
|
|
1927
|
-
console.log(renderCheckCard(check));
|
|
1928
|
-
log.newline();
|
|
1929
|
-
}
|
|
1930
|
-
const passing = checks.filter((c) => c.status === "pass").length;
|
|
1931
|
-
const total = checks.length;
|
|
1932
|
-
const bar = progressBar(passing, total);
|
|
1933
|
-
log.info(`Health Score: ${bar} ${colors.muted(`${String(passing)}/${String(total)} checks passing`)}`);
|
|
1934
|
-
log.newline();
|
|
1935
|
-
const category = passing === total ? "success" : "error";
|
|
1936
|
-
const quote = getQuoteForContext(category);
|
|
1937
|
-
console.log(colors.muted(` "${quote}"`));
|
|
1938
|
-
log.newline();
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// src/commands/ticket/edit.ts
|
|
1942
|
-
import { input as input3 } from "@inquirer/prompts";
|
|
1943
|
-
function validateUrl(url) {
|
|
1944
|
-
try {
|
|
1945
|
-
new URL(url);
|
|
1946
|
-
return true;
|
|
1947
|
-
} catch {
|
|
1948
|
-
return false;
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
async function ticketEditCommand(ticketId, options = {}) {
|
|
1952
|
-
const isInteractive = options.interactive !== false;
|
|
1953
|
-
let resolvedId = ticketId;
|
|
1954
|
-
if (!resolvedId) {
|
|
1955
|
-
if (!isInteractive) {
|
|
1956
|
-
showError("Ticket ID is required in non-interactive mode");
|
|
1957
|
-
exitWithCode(EXIT_ERROR);
|
|
1958
|
-
}
|
|
1959
|
-
const selected = await selectTicket("Select ticket to edit:");
|
|
1960
|
-
if (!selected) {
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
resolvedId = selected;
|
|
1964
|
-
}
|
|
1965
|
-
const ticketR = await wrapAsync(() => getTicket(resolvedId), ensureError);
|
|
1966
|
-
if (!ticketR.ok) {
|
|
1967
|
-
if (ticketR.error instanceof TicketNotFoundError) {
|
|
1968
|
-
showError(`Ticket not found: ${resolvedId}`);
|
|
1969
|
-
showNextStep("ralphctl ticket list", "see available tickets");
|
|
1970
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
throw ticketR.error;
|
|
1974
|
-
}
|
|
1975
|
-
const ticket = ticketR.value;
|
|
1976
|
-
let newTitle;
|
|
1977
|
-
let newDescription;
|
|
1978
|
-
let newLink;
|
|
1979
|
-
if (isInteractive) {
|
|
1980
|
-
console.log(`
|
|
1981
|
-
Editing: ${formatTicketDisplay(ticket)}`);
|
|
1982
|
-
console.log(muted(` Project: ${ticket.projectName} (read-only)
|
|
1983
|
-
`));
|
|
1984
|
-
newTitle = await input3({
|
|
1985
|
-
message: `${icons.ticket} Title:`,
|
|
1986
|
-
default: ticket.title,
|
|
1987
|
-
validate: (v) => v.trim().length > 0 ? true : "Title is required"
|
|
1988
|
-
});
|
|
1989
|
-
const descR = await editorInput({
|
|
1990
|
-
message: "Description:",
|
|
1991
|
-
default: ticket.description
|
|
1992
|
-
});
|
|
1993
|
-
if (!descR.ok) {
|
|
1994
|
-
showError(`Editor input failed: ${descR.error.message}`);
|
|
1995
|
-
return;
|
|
1996
|
-
}
|
|
1997
|
-
newDescription = descR.value;
|
|
1998
|
-
newLink = await input3({
|
|
1999
|
-
message: `${icons.info} Link:`,
|
|
2000
|
-
default: ticket.link ?? "",
|
|
2001
|
-
validate: (v) => {
|
|
2002
|
-
if (!v) return true;
|
|
2003
|
-
return validateUrl(v) ? true : "Invalid URL format";
|
|
2004
|
-
}
|
|
2005
|
-
});
|
|
2006
|
-
newTitle = newTitle.trim();
|
|
2007
|
-
newDescription = newDescription.trim() || void 0;
|
|
2008
|
-
newLink = newLink.trim() || void 0;
|
|
2009
|
-
} else {
|
|
2010
|
-
if (options.title !== void 0) {
|
|
2011
|
-
const trimmed = options.title.trim();
|
|
2012
|
-
if (trimmed.length === 0) {
|
|
2013
|
-
showError("--title cannot be empty");
|
|
2014
|
-
exitWithCode(EXIT_ERROR);
|
|
2015
|
-
}
|
|
2016
|
-
newTitle = trimmed;
|
|
2017
|
-
}
|
|
2018
|
-
if (options.description !== void 0) {
|
|
2019
|
-
newDescription = options.description.trim() || void 0;
|
|
2020
|
-
}
|
|
2021
|
-
if (options.link !== void 0) {
|
|
2022
|
-
const trimmed = options.link.trim();
|
|
2023
|
-
if (trimmed && !validateUrl(trimmed)) {
|
|
2024
|
-
showError("--link must be a valid URL");
|
|
2025
|
-
exitWithCode(EXIT_ERROR);
|
|
2026
|
-
}
|
|
2027
|
-
newLink = trimmed || void 0;
|
|
2028
|
-
}
|
|
2029
|
-
if (newTitle === void 0 && newDescription === void 0 && newLink === void 0) {
|
|
2030
|
-
showError("No updates provided. Use --title, --description, or --link.");
|
|
2031
|
-
exitWithCode(EXIT_ERROR);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
const updates = {};
|
|
2035
|
-
if (newTitle !== void 0 && newTitle !== ticket.title) {
|
|
2036
|
-
updates.title = newTitle;
|
|
2037
|
-
}
|
|
2038
|
-
if (newDescription !== void 0 && newDescription !== ticket.description) {
|
|
2039
|
-
updates.description = newDescription;
|
|
2040
|
-
}
|
|
2041
|
-
if (newLink !== void 0 && newLink !== ticket.link) {
|
|
2042
|
-
updates.link = newLink;
|
|
2043
|
-
}
|
|
2044
|
-
if (Object.keys(updates).length === 0) {
|
|
2045
|
-
console.log(muted("\n No changes made.\n"));
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
const updateR = await wrapAsync(() => updateTicket(ticket.id, updates), ensureError);
|
|
2049
|
-
if (!updateR.ok) {
|
|
2050
|
-
if (updateR.error instanceof SprintStatusError) {
|
|
2051
|
-
showError(updateR.error.message);
|
|
2052
|
-
} else {
|
|
2053
|
-
throw updateR.error;
|
|
2054
|
-
}
|
|
2055
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
const updated = updateR.value;
|
|
2059
|
-
showSuccess("Ticket updated!", [
|
|
2060
|
-
["ID", updated.id],
|
|
2061
|
-
["Title", updated.title],
|
|
2062
|
-
["Project", updated.projectName]
|
|
2063
|
-
]);
|
|
2064
|
-
if (updated.description) {
|
|
2065
|
-
console.log(fieldMultiline("Description", updated.description));
|
|
2066
|
-
}
|
|
2067
|
-
if (updated.link) {
|
|
2068
|
-
console.log(field("Link", updated.link));
|
|
2069
|
-
}
|
|
2070
|
-
console.log("");
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
// src/commands/ticket/list.ts
|
|
2074
|
-
function parseListArgs(args) {
|
|
2075
|
-
const result = {
|
|
2076
|
-
brief: false
|
|
2077
|
-
};
|
|
2078
|
-
for (let i = 0; i < args.length; i++) {
|
|
2079
|
-
const arg = args[i];
|
|
2080
|
-
const next = args[i + 1];
|
|
2081
|
-
if (arg === "-b" || arg === "--brief") result.brief = true;
|
|
2082
|
-
else if (arg === "--project" && next) {
|
|
2083
|
-
result.projectFilter = next;
|
|
2084
|
-
i++;
|
|
2085
|
-
} else if (arg === "--status" && next) {
|
|
2086
|
-
result.statusFilter = next;
|
|
2087
|
-
i++;
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
return result;
|
|
2091
|
-
}
|
|
2092
|
-
function buildFilterSummary(filters) {
|
|
2093
|
-
const parts = [];
|
|
2094
|
-
if (filters.projectFilter) parts.push(`project=${filters.projectFilter}`);
|
|
2095
|
-
if (filters.statusFilter) parts.push(`status=${filters.statusFilter}`);
|
|
2096
|
-
return parts.length > 0 ? ` (filtered: ${parts.join(", ")})` : "";
|
|
2097
|
-
}
|
|
2098
|
-
async function ticketListCommand(args) {
|
|
2099
|
-
const { brief, projectFilter, statusFilter } = parseListArgs(args);
|
|
2100
|
-
if (statusFilter) {
|
|
2101
|
-
const result = RequirementStatusSchema.safeParse(statusFilter);
|
|
2102
|
-
if (!result.success) {
|
|
2103
|
-
showError(`Invalid status: "${statusFilter}". Valid values: pending, approved`);
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
const tickets = await listTickets();
|
|
2108
|
-
if (tickets.length === 0) {
|
|
2109
|
-
showEmpty("tickets", "Add one with: ralphctl ticket add --project <project-name>");
|
|
2110
|
-
return;
|
|
2111
|
-
}
|
|
2112
|
-
let filtered = tickets;
|
|
2113
|
-
if (projectFilter) filtered = filtered.filter((t) => t.projectName === projectFilter);
|
|
2114
|
-
if (statusFilter) filtered = filtered.filter((t) => t.requirementStatus === statusFilter);
|
|
2115
|
-
const filterStr = buildFilterSummary({ brief, projectFilter, statusFilter });
|
|
2116
|
-
const isFiltered = filtered.length !== tickets.length;
|
|
2117
|
-
if (filtered.length === 0) {
|
|
2118
|
-
showEmpty("matching tickets", "Try adjusting your filters");
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
|
-
if (brief) {
|
|
2122
|
-
const countLabel = isFiltered ? `${String(filtered.length)} of ${String(tickets.length)}` : String(tickets.length);
|
|
2123
|
-
console.log(`
|
|
2124
|
-
# Tickets (${countLabel})${filterStr}
|
|
2125
|
-
`);
|
|
2126
|
-
for (const ticket of filtered) {
|
|
2127
|
-
const display = `[${ticket.id}] ${ticket.title}`;
|
|
2128
|
-
const reqBadge = ticket.requirementStatus === "approved" ? " [approved]" : " [pending]";
|
|
2129
|
-
console.log(`- ${display}${reqBadge} (${ticket.projectName})`);
|
|
2130
|
-
}
|
|
2131
|
-
console.log("");
|
|
2132
|
-
return;
|
|
2133
|
-
}
|
|
2134
|
-
const ticketsByProject = groupTicketsByProject(filtered);
|
|
2135
|
-
printHeader(`Tickets (${String(filtered.length)})`, icons.ticket);
|
|
2136
|
-
for (const [projectName, projectTickets] of ticketsByProject) {
|
|
2137
|
-
log.raw(`${colors.info(icons.project)} ${colors.info(projectName)}`);
|
|
2138
|
-
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
2139
|
-
if (projectR.ok) {
|
|
2140
|
-
for (const repo of projectR.value.repositories) {
|
|
2141
|
-
log.raw(` ${muted(repo.name)} ${muted("\u2192")} ${muted(repo.path)}`, 1);
|
|
2142
|
-
}
|
|
2143
|
-
} else {
|
|
2144
|
-
log.raw(` ${muted("(project not found)")}`, 1);
|
|
2145
|
-
}
|
|
2146
|
-
log.newline();
|
|
2147
|
-
for (const ticket of projectTickets) {
|
|
2148
|
-
const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "muted");
|
|
2149
|
-
log.raw(` ${icons.bullet} ${formatTicketDisplay(ticket)} ${reqBadge}`);
|
|
2150
|
-
if (ticket.description) {
|
|
2151
|
-
const preview = ticket.description.split("\n")[0] ?? "";
|
|
2152
|
-
const truncated = preview.length > 60 ? preview.slice(0, 57) + "..." : preview;
|
|
2153
|
-
log.raw(` ${muted(truncated)}`, 1);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
log.newline();
|
|
2157
|
-
}
|
|
2158
|
-
const approved = filtered.filter((t) => t.requirementStatus === "approved").length;
|
|
2159
|
-
log.dim(
|
|
2160
|
-
`Requirements: ${success(`${String(approved)} approved`)} / ${muted(`${String(filtered.length - approved)} pending`)}`
|
|
2161
|
-
);
|
|
2162
|
-
const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(tickets.length)} ticket(s)${filterStr}` : `Showing ${String(tickets.length)} ticket(s)`;
|
|
2163
|
-
log.dim(showingLabel);
|
|
2164
|
-
log.newline();
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
// src/commands/ticket/show.ts
|
|
2168
|
-
async function ticketShowCommand(args) {
|
|
2169
|
-
let ticketId = args[0];
|
|
2170
|
-
if (!ticketId) {
|
|
2171
|
-
const selected = await selectTicket("Select ticket to show:");
|
|
2172
|
-
if (!selected) return;
|
|
2173
|
-
ticketId = selected;
|
|
2174
|
-
}
|
|
2175
|
-
const ticketR = await wrapAsync(() => getTicket(ticketId), ensureError);
|
|
2176
|
-
if (!ticketR.ok) {
|
|
2177
|
-
if (ticketR.error instanceof TicketNotFoundError) {
|
|
2178
|
-
showError(`Ticket not found: ${ticketId}`);
|
|
2179
|
-
showNextStep("ralphctl ticket list", "see available tickets");
|
|
2180
|
-
log.newline();
|
|
2181
|
-
} else {
|
|
2182
|
-
throw ticketR.error;
|
|
2183
|
-
}
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
|
-
const ticket = ticketR.value;
|
|
2187
|
-
const reqBadge = ticket.requirementStatus === "approved" ? badge("approved", "success") : badge("pending", "muted");
|
|
2188
|
-
const infoLines = [labelValue("ID", ticket.id)];
|
|
2189
|
-
infoLines.push(labelValue("Project", ticket.projectName));
|
|
2190
|
-
infoLines.push(labelValue("Requirements", reqBadge));
|
|
2191
|
-
if (ticket.link) {
|
|
2192
|
-
infoLines.push(labelValue("Link", ticket.link));
|
|
2193
|
-
}
|
|
2194
|
-
const projectR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
|
|
2195
|
-
if (projectR.ok) {
|
|
2196
|
-
infoLines.push("");
|
|
2197
|
-
for (const repo of projectR.value.repositories) {
|
|
2198
|
-
infoLines.push(` ${icons.bullet} ${repo.name} ${muted("\u2192")} ${muted(repo.path)}`);
|
|
2199
|
-
}
|
|
2200
|
-
} else {
|
|
2201
|
-
infoLines.push(labelValue("Repositories", muted("(project not found)")));
|
|
2202
|
-
}
|
|
2203
|
-
log.newline();
|
|
2204
|
-
console.log(renderCard(`${icons.ticket} ${ticket.title}`, infoLines));
|
|
2205
|
-
if (ticket.description) {
|
|
2206
|
-
log.newline();
|
|
2207
|
-
const descLines = [];
|
|
2208
|
-
for (const line2 of ticket.description.split("\n")) {
|
|
2209
|
-
descLines.push(line2);
|
|
2210
|
-
}
|
|
2211
|
-
console.log(renderCard(`${icons.edit} Description`, descLines));
|
|
2212
|
-
}
|
|
2213
|
-
if (ticket.affectedRepositories && ticket.affectedRepositories.length > 0) {
|
|
2214
|
-
log.newline();
|
|
2215
|
-
const affectedLines = [];
|
|
2216
|
-
for (const repoPath of ticket.affectedRepositories) {
|
|
2217
|
-
affectedLines.push(`${icons.bullet} ${repoPath}`);
|
|
2218
|
-
}
|
|
2219
|
-
console.log(renderCard(`${icons.project} Affected Repositories`, affectedLines));
|
|
2220
|
-
}
|
|
2221
|
-
log.newline();
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
// src/commands/ticket/remove.ts
|
|
2225
|
-
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
2226
|
-
async function ticketRemoveCommand(args) {
|
|
2227
|
-
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
2228
|
-
let ticketId = args.find((a) => !a.startsWith("-"));
|
|
2229
|
-
if (!ticketId) {
|
|
2230
|
-
const selected = await selectTicket("Select ticket to remove:");
|
|
2231
|
-
if (!selected) return;
|
|
2232
|
-
ticketId = selected;
|
|
2233
|
-
}
|
|
2234
|
-
const opR = await wrapAsync(async () => {
|
|
2235
|
-
const ticket = await getTicket(ticketId);
|
|
2236
|
-
if (!skipConfirm) {
|
|
2237
|
-
const confirmed = await confirm5({
|
|
2238
|
-
message: `Remove ticket ${formatTicketDisplay(ticket)}?`,
|
|
2239
|
-
default: false
|
|
2240
|
-
});
|
|
2241
|
-
if (!confirmed) {
|
|
2242
|
-
console.log(muted("\nTicket removal cancelled.\n"));
|
|
2243
|
-
return null;
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
await removeTicket(ticketId);
|
|
2247
|
-
return ticket;
|
|
2248
|
-
}, ensureError);
|
|
2249
|
-
if (!opR.ok) {
|
|
2250
|
-
if (opR.error instanceof TicketNotFoundError) {
|
|
2251
|
-
showError(`Ticket not found: ${ticketId}`);
|
|
2252
|
-
showNextStep("ralphctl ticket list", "see available tickets");
|
|
2253
|
-
log.newline();
|
|
2254
|
-
} else if (opR.error instanceof SprintStatusError) {
|
|
2255
|
-
showError(opR.error.message);
|
|
2256
|
-
log.newline();
|
|
2257
|
-
} else {
|
|
2258
|
-
throw opR.error;
|
|
2259
|
-
}
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
if (opR.value !== null) {
|
|
2263
|
-
showSuccess("Ticket removed", [["ID", ticketId]]);
|
|
2264
|
-
log.newline();
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
// src/commands/ticket/refine.ts
|
|
2269
|
-
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2270
|
-
import { join as join3 } from "path";
|
|
2271
|
-
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
2272
|
-
import { Result as Result4 } from "typescript-result";
|
|
2273
|
-
async function ticketRefineCommand(ticketId, options = {}) {
|
|
2274
|
-
const isInteractive = options.interactive !== false;
|
|
2275
|
-
const sprintIdR = await wrapAsync(() => resolveSprintId(), ensureError);
|
|
2276
|
-
if (!sprintIdR.ok) {
|
|
2277
|
-
showWarning("No current sprint set.");
|
|
2278
|
-
showTip("Create a sprint first or set one with: ralphctl sprint current");
|
|
2279
|
-
log.newline();
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
const sprintId = sprintIdR.value;
|
|
2283
|
-
const sprint = await getSprint(sprintId);
|
|
2284
|
-
try {
|
|
2285
|
-
assertSprintStatus(sprint, ["draft"], "refine ticket");
|
|
2286
|
-
} catch (err) {
|
|
2287
|
-
showError(err instanceof Error ? err.message : String(err));
|
|
2288
|
-
log.newline();
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
const approvedTickets = sprint.tickets.filter((t) => t.requirementStatus === "approved");
|
|
2292
|
-
if (approvedTickets.length === 0) {
|
|
2293
|
-
showWarning("No approved tickets to re-refine.");
|
|
2294
|
-
showTip('Run "ralphctl sprint refine" to refine pending tickets first.');
|
|
2295
|
-
log.newline();
|
|
2296
|
-
return;
|
|
2297
|
-
}
|
|
2298
|
-
let resolvedId = ticketId;
|
|
2299
|
-
if (!resolvedId) {
|
|
2300
|
-
if (!isInteractive) {
|
|
2301
|
-
showError("Ticket ID is required in non-interactive mode");
|
|
2302
|
-
exitWithCode(EXIT_ERROR);
|
|
2303
|
-
}
|
|
2304
|
-
const selected = await selectTicket("Select ticket to re-refine:", (t) => t.requirementStatus === "approved");
|
|
2305
|
-
if (!selected) return;
|
|
2306
|
-
resolvedId = selected;
|
|
2307
|
-
}
|
|
2308
|
-
const ticket = sprint.tickets.find((t) => t.id === resolvedId);
|
|
2309
|
-
if (!ticket) {
|
|
2310
|
-
showError(`Ticket not found: ${resolvedId}`);
|
|
2311
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2312
|
-
return;
|
|
2313
|
-
}
|
|
2314
|
-
if (ticket.requirementStatus !== "approved") {
|
|
2315
|
-
showError('Only approved tickets can be re-refined. Run "ralphctl sprint refine" for pending tickets.');
|
|
2316
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2317
|
-
return;
|
|
2318
|
-
}
|
|
2319
|
-
printHeader("Re-Refine Ticket", icons.ticket);
|
|
2320
|
-
console.log(field("Sprint", sprint.name));
|
|
2321
|
-
console.log(field("Ticket", formatTicketDisplay(ticket)));
|
|
2322
|
-
console.log(field("Project", ticket.projectName));
|
|
2323
|
-
if (ticket.link) {
|
|
2324
|
-
console.log(field("Link", ticket.link));
|
|
2325
|
-
}
|
|
2326
|
-
if (ticket.description) {
|
|
2327
|
-
console.log(fieldMultiline("Description", ticket.description));
|
|
2328
|
-
}
|
|
2329
|
-
log.newline();
|
|
2330
|
-
const schemaPath = getSchemaPath("requirements-output.schema.json");
|
|
2331
|
-
const schema = await readFile2(schemaPath, "utf-8");
|
|
2332
|
-
const providerName = providerDisplayName(await resolveProvider());
|
|
2333
|
-
const proceed = await confirm6({
|
|
2334
|
-
message: `${emoji.donut} Start ${providerName} re-refinement session?`,
|
|
2335
|
-
default: true
|
|
2336
|
-
});
|
|
2337
|
-
if (!proceed) {
|
|
2338
|
-
log.dim("Cancelled.");
|
|
2339
|
-
log.newline();
|
|
2340
|
-
return;
|
|
2341
|
-
}
|
|
2342
|
-
let issueContext = "";
|
|
2343
|
-
if (ticket.link) {
|
|
2344
|
-
const ticketLink = ticket.link;
|
|
2345
|
-
const fetchSpinner = createSpinner("Fetching issue data...");
|
|
2346
|
-
fetchSpinner.start();
|
|
2347
|
-
const fetchR = Result4.try(() => fetchIssueFromUrl(ticketLink));
|
|
2348
|
-
if (!fetchR.ok) {
|
|
2349
|
-
fetchSpinner.fail("Could not fetch issue data");
|
|
2350
|
-
showWarning(`${fetchR.error.message} \u2014 continuing without issue context`);
|
|
2351
|
-
} else if (fetchR.value) {
|
|
2352
|
-
issueContext = formatIssueContext(fetchR.value);
|
|
2353
|
-
fetchSpinner.succeed(`Issue data fetched (${String(fetchR.value.comments.length)} comment(s))`);
|
|
2354
|
-
} else {
|
|
2355
|
-
fetchSpinner.stop();
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
const refineDir = getRefinementDir(sprintId, ticket.id);
|
|
2359
|
-
await mkdir2(refineDir, { recursive: true });
|
|
2360
|
-
const outputFile = join3(refineDir, "requirements.json");
|
|
2361
|
-
let ticketContent = formatTicketForPrompt(ticket);
|
|
2362
|
-
if (ticket.requirements) {
|
|
2363
|
-
ticketContent += "\n### Previously Approved Requirements\n\n";
|
|
2364
|
-
ticketContent += ticket.requirements;
|
|
2365
|
-
ticketContent += "\n";
|
|
2366
|
-
}
|
|
2367
|
-
const prompt = buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext);
|
|
2368
|
-
log.dim(`Working directory: ${refineDir}`);
|
|
2369
|
-
log.dim(`Requirements output: ${outputFile}`);
|
|
2370
|
-
log.newline();
|
|
2371
|
-
const spinner = createSpinner(`Starting ${providerName} session...`);
|
|
2372
|
-
spinner.start();
|
|
2373
|
-
const sessionR = await wrapAsync(() => runAiSession(refineDir, prompt, ticket.title), ensureError);
|
|
2374
|
-
if (!sessionR.ok) {
|
|
2375
|
-
spinner.fail(`${providerName} session failed`);
|
|
2376
|
-
showError(sessionR.error.message);
|
|
2377
|
-
log.newline();
|
|
2378
|
-
return;
|
|
2379
|
-
}
|
|
2380
|
-
spinner.succeed(`${providerName} session completed`);
|
|
2381
|
-
log.newline();
|
|
2382
|
-
if (!await fileExists(outputFile)) {
|
|
2383
|
-
showWarning("No requirements file found from AI session.");
|
|
2384
|
-
log.newline();
|
|
2385
|
-
return;
|
|
2386
|
-
}
|
|
2387
|
-
const contentR = await wrapAsync(() => readFile2(outputFile, "utf-8"), ensureError);
|
|
2388
|
-
if (!contentR.ok) {
|
|
2389
|
-
showError(`Failed to read requirements file: ${outputFile}`);
|
|
2390
|
-
log.newline();
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
const content = contentR.value;
|
|
2394
|
-
const parseR = Result4.try(() => parseRequirementsFile(content));
|
|
2395
|
-
if (!parseR.ok) {
|
|
2396
|
-
showError(`Failed to parse requirements file: ${parseR.error.message}`);
|
|
2397
|
-
log.newline();
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
const refinedRequirements = parseR.value;
|
|
2401
|
-
if (refinedRequirements.length === 0) {
|
|
2402
|
-
showWarning("No requirements found in output file.");
|
|
2403
|
-
log.newline();
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
const matchingRequirements = refinedRequirements.filter((r) => r.ref === ticket.id || r.ref === ticket.title);
|
|
2407
|
-
if (matchingRequirements.length === 0) {
|
|
2408
|
-
showWarning("Requirement reference does not match this ticket.");
|
|
2409
|
-
log.newline();
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
const requirement = matchingRequirements.length === 1 ? {
|
|
2413
|
-
ref: matchingRequirements[0]?.ref ?? "",
|
|
2414
|
-
requirements: matchingRequirements[0]?.requirements ?? ""
|
|
2415
|
-
} : {
|
|
2416
|
-
ref: matchingRequirements[0]?.ref ?? "",
|
|
2417
|
-
requirements: matchingRequirements.map((r, idx) => {
|
|
2418
|
-
const text = r.requirements.trim();
|
|
2419
|
-
if (/^#\s/.test(text)) return text;
|
|
2420
|
-
return `# ${String(idx + 1)}. Section ${String(idx + 1)}
|
|
2421
|
-
|
|
2422
|
-
${text}`;
|
|
2423
|
-
}).join("\n\n---\n\n")
|
|
2424
|
-
};
|
|
2425
|
-
const reqLines = requirement.requirements.split("\n");
|
|
2426
|
-
console.log(renderCard(`${icons.ticket} Re-Refined Requirements`, reqLines));
|
|
2427
|
-
log.newline();
|
|
2428
|
-
const approveRequirement = await confirm6({
|
|
2429
|
-
message: `${emoji.donut} Approve these requirements?`,
|
|
2430
|
-
default: true
|
|
2431
|
-
});
|
|
2432
|
-
if (approveRequirement) {
|
|
2433
|
-
const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticket.id);
|
|
2434
|
-
const ticketToSave = sprint.tickets[ticketIdx];
|
|
2435
|
-
if (ticketIdx !== -1 && ticketToSave) {
|
|
2436
|
-
ticketToSave.requirements = requirement.requirements;
|
|
2437
|
-
}
|
|
2438
|
-
await saveSprint(sprint);
|
|
2439
|
-
showSuccess("Requirements updated and saved!");
|
|
2440
|
-
} else {
|
|
2441
|
-
log.dim("Requirements not approved. Previous requirements unchanged.");
|
|
2442
|
-
}
|
|
2443
|
-
log.newline();
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
// src/commands/task/add.ts
|
|
2447
|
-
import { resolve as resolve2 } from "path";
|
|
2448
|
-
import { confirm as confirm7, input as input4 } from "@inquirer/prompts";
|
|
2449
|
-
async function taskAddCommand(options = {}) {
|
|
2450
|
-
const isInteractive = options.interactive !== false;
|
|
2451
|
-
const statusCheckR = await wrapAsync(async () => {
|
|
2452
|
-
const sprintId = await resolveSprintId();
|
|
2453
|
-
const sprint = await getSprint(sprintId);
|
|
2454
|
-
assertSprintStatus(sprint, ["draft"], "add tasks");
|
|
2455
|
-
}, ensureError);
|
|
2456
|
-
if (!statusCheckR.ok) {
|
|
2457
|
-
const err = statusCheckR.error;
|
|
2458
|
-
if (err instanceof SprintStatusError) {
|
|
2459
|
-
const mainError = err.message.split("\n")[0] ?? err.message;
|
|
2460
|
-
showError(mainError);
|
|
2461
|
-
showNextSteps([
|
|
2462
|
-
["ralphctl sprint close", "close current sprint"],
|
|
2463
|
-
["ralphctl sprint create", "start a new draft sprint"]
|
|
2464
|
-
]);
|
|
2465
|
-
log.newline();
|
|
2466
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
if (err instanceof NoCurrentSprintError) {
|
|
2470
|
-
showError("No current sprint set.");
|
|
2471
|
-
showNextSteps([["ralphctl sprint create", "create a new sprint"]]);
|
|
2472
|
-
log.newline();
|
|
2473
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2474
|
-
return;
|
|
2475
|
-
}
|
|
2476
|
-
throw err;
|
|
2477
|
-
}
|
|
2478
|
-
let name;
|
|
2479
|
-
let description;
|
|
2480
|
-
let steps;
|
|
2481
|
-
let ticketId;
|
|
2482
|
-
let projectPath;
|
|
2483
|
-
if (options.interactive === false) {
|
|
2484
|
-
const errors = [];
|
|
2485
|
-
const trimmedName = options.name?.trim();
|
|
2486
|
-
const trimmedProject = options.project?.trim();
|
|
2487
|
-
if (!trimmedName) {
|
|
2488
|
-
errors.push("--name is required");
|
|
2489
|
-
}
|
|
2490
|
-
if (!trimmedProject && !options.ticket) {
|
|
2491
|
-
errors.push("--project is required (or --ticket to inherit from ticket)");
|
|
2492
|
-
}
|
|
2493
|
-
if (errors.length > 0 || !trimmedName) {
|
|
2494
|
-
showError("Validation failed");
|
|
2495
|
-
for (const e of errors) {
|
|
2496
|
-
log.item(error(e));
|
|
2497
|
-
}
|
|
2498
|
-
log.newline();
|
|
2499
|
-
exitWithCode(EXIT_ERROR);
|
|
2500
|
-
}
|
|
2501
|
-
name = trimmedName;
|
|
2502
|
-
const trimmedDesc = options.description?.trim();
|
|
2503
|
-
description = trimmedDesc === "" ? void 0 : trimmedDesc;
|
|
2504
|
-
steps = options.steps ?? [];
|
|
2505
|
-
const trimmedTicket = options.ticket?.trim();
|
|
2506
|
-
ticketId = trimmedTicket === "" ? void 0 : trimmedTicket;
|
|
2507
|
-
if (ticketId) {
|
|
2508
|
-
const resolvedTicketId = ticketId;
|
|
2509
|
-
const ticketProjectR = await wrapAsync(async () => {
|
|
2510
|
-
const ticket = await getTicket(resolvedTicketId);
|
|
2511
|
-
const project = await getProject(ticket.projectName);
|
|
2512
|
-
return project.repositories[0]?.path;
|
|
2513
|
-
}, ensureError);
|
|
2514
|
-
if (ticketProjectR.ok) {
|
|
2515
|
-
projectPath = ticketProjectR.value;
|
|
2516
|
-
} else {
|
|
2517
|
-
if (!trimmedProject) {
|
|
2518
|
-
showError(`Ticket not found: ${ticketId}`);
|
|
2519
|
-
console.log(muted(" Provide --project or a valid --ticket\n"));
|
|
2520
|
-
exitWithCode(EXIT_ERROR);
|
|
2521
|
-
}
|
|
2522
|
-
const validation = await validateProjectPath(trimmedProject);
|
|
2523
|
-
if (!validation.ok) {
|
|
2524
|
-
showError(`Invalid project path: ${validation.error.message}`);
|
|
2525
|
-
exitWithCode(EXIT_ERROR);
|
|
2526
|
-
}
|
|
2527
|
-
projectPath = resolve2(trimmedProject);
|
|
2528
|
-
}
|
|
2529
|
-
} else if (trimmedProject) {
|
|
2530
|
-
const validation = await validateProjectPath(trimmedProject);
|
|
2531
|
-
if (!validation.ok) {
|
|
2532
|
-
showError(`Invalid project path: ${validation.error.message}`);
|
|
2533
|
-
exitWithCode(EXIT_ERROR);
|
|
2534
|
-
}
|
|
2535
|
-
projectPath = resolve2(trimmedProject);
|
|
2536
|
-
} else {
|
|
2537
|
-
showError("--project is required");
|
|
2538
|
-
exitWithCode(EXIT_ERROR);
|
|
2539
|
-
}
|
|
2540
|
-
} else {
|
|
2541
|
-
name = await input4({
|
|
2542
|
-
message: `${icons.task} Task name:`,
|
|
2543
|
-
default: options.name?.trim(),
|
|
2544
|
-
validate: (v) => v.trim().length > 0 ? true : "Name is required"
|
|
2545
|
-
});
|
|
2546
|
-
const descR = await editorInput({
|
|
2547
|
-
message: "Description (optional):",
|
|
2548
|
-
default: options.description?.trim()
|
|
2549
|
-
});
|
|
2550
|
-
if (!descR.ok) {
|
|
2551
|
-
showError(`Editor input failed: ${descR.error.message}`);
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
description = descR.value;
|
|
2555
|
-
steps = options.steps ? [...options.steps] : [];
|
|
2556
|
-
const addSteps = await confirm7({
|
|
2557
|
-
message: `${emoji.donut} ${steps.length > 0 ? `Add more steps? (${String(steps.length)} pre-filled)` : "Add implementation steps?"}`,
|
|
2558
|
-
default: steps.length === 0
|
|
2559
|
-
});
|
|
2560
|
-
if (addSteps) {
|
|
2561
|
-
let stepNum = steps.length + 1;
|
|
2562
|
-
let adding = true;
|
|
2563
|
-
while (adding) {
|
|
2564
|
-
const step = await input4({
|
|
2565
|
-
message: ` Step ${String(stepNum)} (empty to finish):`
|
|
2566
|
-
});
|
|
2567
|
-
if (step.trim()) {
|
|
2568
|
-
steps.push(step.trim());
|
|
2569
|
-
stepNum++;
|
|
2570
|
-
} else {
|
|
2571
|
-
adding = false;
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
const tickets = await listTickets();
|
|
2576
|
-
if (tickets.length > 0) {
|
|
2577
|
-
const { select: select4 } = await import("@inquirer/prompts");
|
|
2578
|
-
const defaultTicketValue = options.ticket ? tickets.find((t) => t.id === options.ticket)?.id ?? "" : "";
|
|
2579
|
-
const ticketChoice = await select4({
|
|
2580
|
-
message: `${icons.ticket} Link to ticket:`,
|
|
2581
|
-
default: defaultTicketValue,
|
|
2582
|
-
choices: [
|
|
2583
|
-
{ name: `${emoji.donut} None (select project/repo manually)`, value: "" },
|
|
2584
|
-
...tickets.map((t) => ({
|
|
2585
|
-
name: `${icons.ticket} ${formatTicketDisplay(t)} ${muted(`(${t.projectName})`)}`,
|
|
2586
|
-
value: t.id
|
|
2587
|
-
}))
|
|
2588
|
-
]
|
|
2589
|
-
});
|
|
2590
|
-
if (ticketChoice) {
|
|
2591
|
-
ticketId = ticketChoice;
|
|
2592
|
-
const ticket = tickets.find((t) => t.id === ticketChoice);
|
|
2593
|
-
if (ticket) {
|
|
2594
|
-
const projR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
|
|
2595
|
-
if (projR.ok) {
|
|
2596
|
-
const project = projR.value;
|
|
2597
|
-
if (project.repositories.length === 1) {
|
|
2598
|
-
projectPath = project.repositories[0]?.path;
|
|
2599
|
-
} else {
|
|
2600
|
-
const { select: selectRepo } = await import("@inquirer/prompts");
|
|
2601
|
-
projectPath = await selectRepo({
|
|
2602
|
-
message: `${emoji.donut} Select repository for this task:`,
|
|
2603
|
-
choices: project.repositories.map((r) => ({
|
|
2604
|
-
name: `${r.name} (${r.path})`,
|
|
2605
|
-
value: r.path
|
|
2606
|
-
}))
|
|
2607
|
-
});
|
|
2608
|
-
}
|
|
2609
|
-
} else {
|
|
2610
|
-
log.warn(`Project '${ticket.projectName}' not found, will prompt for path.`);
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
} else if (options.ticket) {
|
|
2615
|
-
ticketId = options.ticket;
|
|
2616
|
-
const resolvedTicketId = ticketId;
|
|
2617
|
-
const tpR = await wrapAsync(async () => {
|
|
2618
|
-
const ticket = await getTicket(resolvedTicketId);
|
|
2619
|
-
const project = await getProject(ticket.projectName);
|
|
2620
|
-
return project.repositories[0]?.path;
|
|
2621
|
-
}, ensureError);
|
|
2622
|
-
if (tpR.ok) {
|
|
2623
|
-
projectPath = tpR.value;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
if (projectPath === void 0) {
|
|
2627
|
-
const projects = await listProjects();
|
|
2628
|
-
if (projects.length > 0) {
|
|
2629
|
-
const { select: select4 } = await import("@inquirer/prompts");
|
|
2630
|
-
const choice = await select4({
|
|
2631
|
-
message: `${icons.project} Select project:`,
|
|
2632
|
-
choices: [
|
|
2633
|
-
{ name: `${icons.edit} Enter path manually`, value: "__manual__" },
|
|
2634
|
-
{ name: `${emoji.donut} Select project/repository`, value: "__select__" }
|
|
2635
|
-
]
|
|
2636
|
-
});
|
|
2637
|
-
if (choice === "__manual__") {
|
|
2638
|
-
projectPath = await input4({
|
|
2639
|
-
message: `${icons.project} Project path:`,
|
|
2640
|
-
default: options.project?.trim() ?? process.cwd(),
|
|
2641
|
-
validate: async (v) => {
|
|
2642
|
-
const result = await validateProjectPath(v.trim());
|
|
2643
|
-
return result.ok ? true : result.error.message;
|
|
2644
|
-
}
|
|
2645
|
-
});
|
|
2646
|
-
projectPath = resolve2(expandTilde(projectPath.trim()));
|
|
2647
|
-
} else {
|
|
2648
|
-
const selectedPath = await selectProjectRepository("Select repository:");
|
|
2649
|
-
if (!selectedPath) {
|
|
2650
|
-
showError("No repository selected");
|
|
2651
|
-
exitWithCode(EXIT_ERROR);
|
|
2652
|
-
}
|
|
2653
|
-
projectPath = selectedPath;
|
|
2654
|
-
}
|
|
2655
|
-
} else {
|
|
2656
|
-
projectPath = await input4({
|
|
2657
|
-
message: `${icons.project} Project path:`,
|
|
2658
|
-
default: options.project?.trim() ?? process.cwd(),
|
|
2659
|
-
validate: async (v) => {
|
|
2660
|
-
const result = await validateProjectPath(v.trim());
|
|
2661
|
-
return result.ok ? true : result.error.message;
|
|
2662
|
-
}
|
|
2663
|
-
});
|
|
2664
|
-
projectPath = resolve2(expandTilde(projectPath.trim()));
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
name = name.trim();
|
|
2668
|
-
const trimmedDescription = description.trim();
|
|
2669
|
-
description = trimmedDescription === "" ? void 0 : trimmedDescription;
|
|
2670
|
-
}
|
|
2671
|
-
if (!projectPath) {
|
|
2672
|
-
showError("Project path is required");
|
|
2673
|
-
exitWithCode(EXIT_ERROR);
|
|
2674
|
-
}
|
|
2675
|
-
const addR = await wrapAsync(() => addTask({ name, description, steps, ticketId, projectPath }), ensureError);
|
|
2676
|
-
if (!addR.ok) {
|
|
2677
|
-
if (addR.error instanceof SprintStatusError) {
|
|
2678
|
-
const mainError = addR.error.message.split("\n")[0] ?? addR.error.message;
|
|
2679
|
-
showError(mainError);
|
|
2680
|
-
showNextSteps([
|
|
2681
|
-
["ralphctl sprint close", "close current sprint"],
|
|
2682
|
-
["ralphctl sprint create", "start a new draft sprint"]
|
|
2683
|
-
]);
|
|
2684
|
-
log.newline();
|
|
2685
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
2686
|
-
return;
|
|
2687
|
-
}
|
|
2688
|
-
throw addR.error;
|
|
2689
|
-
}
|
|
2690
|
-
const task = addR.value;
|
|
2691
|
-
showSuccess("Task added!", [
|
|
2692
|
-
["ID", task.id],
|
|
2693
|
-
["Name", task.name],
|
|
2694
|
-
["Project", task.projectPath],
|
|
2695
|
-
["Order", String(task.order)]
|
|
2696
|
-
]);
|
|
2697
|
-
if (task.ticketId) {
|
|
2698
|
-
console.log(field("Ticket", task.ticketId));
|
|
2699
|
-
}
|
|
2700
|
-
if (task.steps.length > 0) {
|
|
2701
|
-
console.log(field("Steps", ""));
|
|
2702
|
-
task.steps.forEach((step, i) => {
|
|
2703
|
-
console.log(muted(` ${String(i + 1)}. ${step}`));
|
|
2704
|
-
});
|
|
2705
|
-
}
|
|
2706
|
-
console.log("");
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
// src/commands/task/import.ts
|
|
2710
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
2711
|
-
import { Result as Result5 } from "typescript-result";
|
|
2712
|
-
async function taskImportCommand(args) {
|
|
2713
|
-
const filePath = args[0];
|
|
2714
|
-
if (!filePath) {
|
|
2715
|
-
showError("File path required.");
|
|
2716
|
-
showNextStep("ralphctl task import <file.json>", "provide a task file");
|
|
2717
|
-
log.dim("Expected JSON format:");
|
|
2718
|
-
console.log(
|
|
2719
|
-
muted(`[
|
|
2720
|
-
{
|
|
2721
|
-
"id": "1",
|
|
2722
|
-
"name": "Task name",
|
|
2723
|
-
"projectPath": "/path/to/repo",
|
|
2724
|
-
"description": "Optional description",
|
|
2725
|
-
"steps": ["Step 1", "Step 2"],
|
|
2726
|
-
"ticketId": "abc12345",
|
|
2727
|
-
"blockedBy": ["task-001"]
|
|
2728
|
-
}
|
|
2729
|
-
]`)
|
|
2730
|
-
);
|
|
2731
|
-
log.dim("Note: projectPath is required for each task.");
|
|
2732
|
-
log.newline();
|
|
2733
|
-
return;
|
|
2734
|
-
}
|
|
2735
|
-
const contentR = await wrapAsync(() => readFile3(filePath, "utf-8"), ensureError);
|
|
2736
|
-
if (!contentR.ok) {
|
|
2737
|
-
showError(`Failed to read file: ${filePath}`);
|
|
2738
|
-
log.newline();
|
|
2739
|
-
return;
|
|
2740
|
-
}
|
|
2741
|
-
const dataR = Result5.try(() => JSON.parse(contentR.value));
|
|
2742
|
-
if (!dataR.ok) {
|
|
2743
|
-
showError("Invalid JSON format.");
|
|
2744
|
-
log.newline();
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
const data = dataR.value;
|
|
2748
|
-
const result = ImportTasksSchema.safeParse(data);
|
|
2749
|
-
if (!result.success) {
|
|
2750
|
-
showError("Invalid task format");
|
|
2751
|
-
for (const issue of result.error.issues) {
|
|
2752
|
-
log.item(error(`${issue.path.join(".")}: ${issue.message}`));
|
|
2753
|
-
}
|
|
2754
|
-
log.newline();
|
|
2755
|
-
return;
|
|
2756
|
-
}
|
|
2757
|
-
const tasks = result.data;
|
|
2758
|
-
if (tasks.length === 0) {
|
|
2759
|
-
showError("No tasks to import.");
|
|
2760
|
-
log.newline();
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
const existingTasks = await getTasks();
|
|
2764
|
-
const sprintId = await resolveSprintId();
|
|
2765
|
-
const sprint = await getSprint(sprintId);
|
|
2766
|
-
const ticketIds = new Set(sprint.tickets.map((t) => t.id));
|
|
2767
|
-
const validationErrors = validateImportTasks(tasks, existingTasks, ticketIds);
|
|
2768
|
-
if (validationErrors.length > 0) {
|
|
2769
|
-
showError("Dependency validation failed");
|
|
2770
|
-
for (const err of validationErrors) {
|
|
2771
|
-
log.item(error(err));
|
|
2772
|
-
}
|
|
2773
|
-
log.newline();
|
|
2774
|
-
return;
|
|
2775
|
-
}
|
|
2776
|
-
const localToRealId = /* @__PURE__ */ new Map();
|
|
2777
|
-
const createdTasks = [];
|
|
2778
|
-
const spinner = createSpinner(`Importing ${String(tasks.length)} task(s)...`).start();
|
|
2779
|
-
let imported = 0;
|
|
2780
|
-
for (const taskInput of tasks) {
|
|
2781
|
-
const addR = await wrapAsync(
|
|
2782
|
-
() => addTask({
|
|
2783
|
-
name: taskInput.name,
|
|
2784
|
-
description: taskInput.description,
|
|
2785
|
-
steps: taskInput.steps ?? [],
|
|
2786
|
-
ticketId: taskInput.ticketId,
|
|
2787
|
-
blockedBy: [],
|
|
2788
|
-
// Set later
|
|
2789
|
-
projectPath: taskInput.projectPath
|
|
2790
|
-
}),
|
|
2791
|
-
ensureError
|
|
2792
|
-
);
|
|
2793
|
-
if (!addR.ok) {
|
|
2794
|
-
if (addR.error instanceof SprintStatusError) {
|
|
2795
|
-
spinner.fail("Import failed");
|
|
2796
|
-
showError(addR.error.message);
|
|
2797
|
-
log.newline();
|
|
2798
|
-
return;
|
|
2799
|
-
}
|
|
2800
|
-
log.itemError(`Failed to add: ${taskInput.name}`);
|
|
2801
|
-
console.log(muted(` ${addR.error.message}`));
|
|
2802
|
-
continue;
|
|
2803
|
-
}
|
|
2804
|
-
const task = addR.value;
|
|
2805
|
-
if (taskInput.id) {
|
|
2806
|
-
localToRealId.set(taskInput.id, task.id);
|
|
2807
|
-
}
|
|
2808
|
-
createdTasks.push({ task: taskInput, realId: task.id });
|
|
2809
|
-
imported++;
|
|
2810
|
-
spinner.text = `Importing tasks... (${String(imported)}/${String(tasks.length)})`;
|
|
2811
|
-
}
|
|
2812
|
-
spinner.text = "Resolving task dependencies...";
|
|
2813
|
-
const tasksFilePath = getTasksFilePath(sprintId);
|
|
2814
|
-
const lockR = await withFileLock(tasksFilePath, async () => {
|
|
2815
|
-
const allTasks = await getTasks();
|
|
2816
|
-
for (const { task: taskInput, realId } of createdTasks) {
|
|
2817
|
-
const blockedBy = (taskInput.blockedBy ?? []).map((localId) => localToRealId.get(localId) ?? "").filter((id) => id !== "");
|
|
2818
|
-
if (blockedBy.length > 0) {
|
|
2819
|
-
const taskToUpdate = allTasks.find((t) => t.id === realId);
|
|
2820
|
-
if (taskToUpdate) {
|
|
2821
|
-
taskToUpdate.blockedBy = blockedBy;
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
await saveTasks(allTasks);
|
|
2826
|
-
});
|
|
2827
|
-
if (!lockR.ok) {
|
|
2828
|
-
showError(`Failed to update dependencies: ${lockR.error.message}`);
|
|
2829
|
-
log.newline();
|
|
2830
|
-
return;
|
|
2831
|
-
}
|
|
2832
|
-
spinner.succeed(`Imported ${String(imported)}/${String(tasks.length)} tasks`);
|
|
2833
|
-
for (const { task: taskInput, realId } of createdTasks) {
|
|
2834
|
-
log.itemSuccess(`${realId}: ${taskInput.name}`);
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
// src/commands/task/list.ts
|
|
2839
|
-
function parseListArgs2(args) {
|
|
2840
|
-
const result = {
|
|
2841
|
-
brief: false,
|
|
2842
|
-
blockedOnly: false
|
|
2843
|
-
};
|
|
2844
|
-
for (let i = 0; i < args.length; i++) {
|
|
2845
|
-
const arg = args[i];
|
|
2846
|
-
const next = args[i + 1];
|
|
2847
|
-
if (arg === "-b" || arg === "--brief") result.brief = true;
|
|
2848
|
-
else if (arg === "--status" && next) {
|
|
2849
|
-
result.statusFilter = next;
|
|
2850
|
-
i++;
|
|
2851
|
-
} else if (arg === "--project" && next) {
|
|
2852
|
-
result.projectFilter = next;
|
|
2853
|
-
i++;
|
|
2854
|
-
} else if (arg === "--ticket" && next) {
|
|
2855
|
-
result.ticketFilter = next;
|
|
2856
|
-
i++;
|
|
2857
|
-
} else if (arg === "--blocked") result.blockedOnly = true;
|
|
2858
|
-
}
|
|
2859
|
-
return result;
|
|
2860
|
-
}
|
|
2861
|
-
function buildFilterSummary2(filters) {
|
|
2862
|
-
const parts = [];
|
|
2863
|
-
if (filters.statusFilter) parts.push(`status=${filters.statusFilter}`);
|
|
2864
|
-
if (filters.projectFilter) parts.push(`project=${filters.projectFilter}`);
|
|
2865
|
-
if (filters.ticketFilter) parts.push(`ticket=${filters.ticketFilter}`);
|
|
2866
|
-
if (filters.blockedOnly) parts.push("blocked");
|
|
2867
|
-
return parts.length > 0 ? ` (filtered: ${parts.join(", ")})` : "";
|
|
2868
|
-
}
|
|
2869
|
-
async function taskListCommand(args = []) {
|
|
2870
|
-
const { brief, statusFilter, projectFilter, ticketFilter, blockedOnly } = parseListArgs2(args);
|
|
2871
|
-
if (statusFilter) {
|
|
2872
|
-
const result = TaskStatusSchema.safeParse(statusFilter);
|
|
2873
|
-
if (!result.success) {
|
|
2874
|
-
showError(`Invalid status: "${statusFilter}". Valid values: todo, in_progress, done`);
|
|
2875
|
-
return;
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
const tasks = await listTasks();
|
|
2879
|
-
if (tasks.length === 0) {
|
|
2880
|
-
showEmpty("tasks", "Add one with: ralphctl task add");
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
let filtered = tasks;
|
|
2884
|
-
if (statusFilter) filtered = filtered.filter((t) => t.status === statusFilter);
|
|
2885
|
-
if (projectFilter) filtered = filtered.filter((t) => t.projectPath.includes(projectFilter));
|
|
2886
|
-
if (ticketFilter) filtered = filtered.filter((t) => t.ticketId === ticketFilter);
|
|
2887
|
-
if (blockedOnly) filtered = filtered.filter((t) => t.blockedBy.length > 0);
|
|
2888
|
-
const filterStr = buildFilterSummary2({ brief, statusFilter, projectFilter, ticketFilter, blockedOnly });
|
|
2889
|
-
const isFiltered = filtered.length !== tasks.length;
|
|
2890
|
-
if (filtered.length === 0) {
|
|
2891
|
-
showEmpty("matching tasks", "Try adjusting your filters");
|
|
2892
|
-
return;
|
|
2893
|
-
}
|
|
2894
|
-
if (brief) {
|
|
2895
|
-
const countLabel = isFiltered ? `${String(filtered.length)} of ${String(tasks.length)}` : String(tasks.length);
|
|
2896
|
-
console.log(`
|
|
2897
|
-
# Tasks (${countLabel})${filterStr}
|
|
2898
|
-
`);
|
|
2899
|
-
for (const task of filtered) {
|
|
2900
|
-
const ticketRef = task.ticketId ? ` [${task.ticketId}]` : "";
|
|
2901
|
-
const blockedRef = task.blockedBy.length > 0 ? ` (blocked by: ${task.blockedBy.join(", ")})` : "";
|
|
2902
|
-
console.log(
|
|
2903
|
-
`- ${String(task.order)}. **[${task.status}]** ${task.id}: ${task.name} (${task.projectPath})${ticketRef}${blockedRef}`
|
|
2904
|
-
);
|
|
2905
|
-
}
|
|
2906
|
-
console.log("");
|
|
2907
|
-
return;
|
|
2908
|
-
}
|
|
2909
|
-
const tasksByStatus = {
|
|
2910
|
-
todo: filtered.filter((t) => t.status === "todo").length,
|
|
2911
|
-
in_progress: filtered.filter((t) => t.status === "in_progress").length,
|
|
2912
|
-
done: filtered.filter((t) => t.status === "done").length
|
|
2913
|
-
};
|
|
2914
|
-
printHeader(`Tasks (${String(filtered.length)})`, icons.task);
|
|
2915
|
-
log.raw(
|
|
2916
|
-
`${formatTaskStatus("todo")} ${String(tasksByStatus.todo)} ${formatTaskStatus("in_progress")} ${String(tasksByStatus.in_progress)} ${formatTaskStatus("done")} ${String(tasksByStatus.done)}`
|
|
2917
|
-
);
|
|
2918
|
-
log.newline();
|
|
2919
|
-
const rows = filtered.map((task) => {
|
|
2920
|
-
const statusIcon = task.status === "done" ? icons.success : task.status === "in_progress" ? icons.active : icons.inactive;
|
|
2921
|
-
const statusColor = task.status === "done" ? "success" : task.status === "in_progress" ? "warning" : "muted";
|
|
2922
|
-
const blocked = task.blockedBy.length > 0 ? colors.warning("(blocked)") : "";
|
|
2923
|
-
return [badge(statusIcon, statusColor), String(task.order), task.name, task.id, blocked];
|
|
2924
|
-
});
|
|
2925
|
-
console.log(
|
|
2926
|
-
renderTable(
|
|
2927
|
-
[
|
|
2928
|
-
{ header: "", minWidth: 0 },
|
|
2929
|
-
{ header: "#", align: "right" },
|
|
2930
|
-
{ header: "Name" },
|
|
2931
|
-
{ header: "ID" },
|
|
2932
|
-
{ header: "" }
|
|
2933
|
-
],
|
|
2934
|
-
rows
|
|
2935
|
-
)
|
|
2936
|
-
);
|
|
2937
|
-
const percent = filtered.length > 0 ? Math.round(tasksByStatus.done / filtered.length * 100) : 0;
|
|
2938
|
-
const progressColor = percent === 100 ? colors.success : percent > 50 ? colors.warning : colors.muted;
|
|
2939
|
-
const showingLabel = isFiltered ? `Showing ${String(filtered.length)} of ${String(tasks.length)} task(s)${filterStr}` : `Showing ${String(tasks.length)} task(s)`;
|
|
2940
|
-
log.newline();
|
|
2941
|
-
log.dim(
|
|
2942
|
-
`Progress: ${progressColor(`${String(tasksByStatus.done)}/${String(filtered.length)} (${String(percent)}%)`)} | ${showingLabel}`
|
|
2943
|
-
);
|
|
2944
|
-
log.newline();
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
// src/commands/task/show.ts
|
|
2948
|
-
async function taskShowCommand(args) {
|
|
2949
|
-
let taskId = args[0];
|
|
2950
|
-
if (!taskId) {
|
|
2951
|
-
const selected = await selectTask("Select task to show:");
|
|
2952
|
-
if (!selected) return;
|
|
2953
|
-
taskId = selected;
|
|
2954
|
-
}
|
|
2955
|
-
const taskR = await wrapAsync(() => getTask(taskId), ensureError);
|
|
2956
|
-
if (!taskR.ok) {
|
|
2957
|
-
if (taskR.error instanceof TaskNotFoundError) {
|
|
2958
|
-
showError(`Task not found: ${taskId}`);
|
|
2959
|
-
showNextStep("ralphctl task list", "see available tasks");
|
|
2960
|
-
log.newline();
|
|
2961
|
-
} else {
|
|
2962
|
-
throw taskR.error;
|
|
2963
|
-
}
|
|
2964
|
-
return;
|
|
2965
|
-
}
|
|
2966
|
-
const task = taskR.value;
|
|
2967
|
-
const infoLines = [
|
|
2968
|
-
labelValue("ID", task.id),
|
|
2969
|
-
labelValue("Status", formatTaskStatus(task.status)),
|
|
2970
|
-
labelValue("Order", String(task.order)),
|
|
2971
|
-
labelValue("Project", task.projectPath)
|
|
2972
|
-
];
|
|
2973
|
-
if (task.ticketId) {
|
|
2974
|
-
infoLines.push(labelValue("Ticket", task.ticketId));
|
|
2975
|
-
}
|
|
2976
|
-
if (task.description) {
|
|
2977
|
-
infoLines.push("");
|
|
2978
|
-
infoLines.push(labelValue("Description", ""));
|
|
2979
|
-
for (const line2 of task.description.split("\n")) {
|
|
2980
|
-
infoLines.push(`${" ".repeat(DETAIL_LABEL_WIDTH + 1)}${line2}`);
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
log.newline();
|
|
2984
|
-
console.log(renderCard(`${icons.task} ${task.name}`, infoLines));
|
|
2985
|
-
if (task.steps.length > 0) {
|
|
2986
|
-
log.newline();
|
|
2987
|
-
const stepLines = [];
|
|
2988
|
-
for (let i = 0; i < task.steps.length; i++) {
|
|
2989
|
-
const step = task.steps[i] ?? "";
|
|
2990
|
-
const checkbox = task.status === "done" ? colors.success("[x]") : muted("[ ]");
|
|
2991
|
-
stepLines.push(`${checkbox} ${muted(String(i + 1) + ".")} ${step}`);
|
|
2992
|
-
}
|
|
2993
|
-
console.log(renderCard(`${icons.bullet} Steps (${String(task.steps.length)})`, stepLines));
|
|
2994
|
-
}
|
|
2995
|
-
if (task.blockedBy.length > 0) {
|
|
2996
|
-
log.newline();
|
|
2997
|
-
const depLines = [];
|
|
2998
|
-
for (const dep of task.blockedBy) {
|
|
2999
|
-
depLines.push(`${icons.bullet} ${dep}`);
|
|
3000
|
-
}
|
|
3001
|
-
console.log(renderCard(`${icons.warning} Blocked By`, depLines));
|
|
3002
|
-
}
|
|
3003
|
-
if (task.ticketId) {
|
|
3004
|
-
const taskTicketId = task.ticketId;
|
|
3005
|
-
const ticketR = await wrapAsync(() => getTicket(taskTicketId), ensureError);
|
|
3006
|
-
if (ticketR.ok && ticketR.value.requirements) {
|
|
3007
|
-
log.newline();
|
|
3008
|
-
const reqLines = ticketR.value.requirements.split("\n");
|
|
3009
|
-
console.log(renderCard(`${icons.ticket} Requirements`, reqLines));
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
if (task.verified) {
|
|
3013
|
-
log.newline();
|
|
3014
|
-
const verifyLines = [`${colors.success(icons.success)} Verified`];
|
|
3015
|
-
if (task.verificationOutput) {
|
|
3016
|
-
verifyLines.push(colors.muted(horizontalLine(30, "rounded")));
|
|
3017
|
-
for (const line2 of task.verificationOutput.split("\n").slice(0, 10)) {
|
|
3018
|
-
verifyLines.push(muted(line2));
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
console.log(renderCard(`${icons.success} Verification`, verifyLines));
|
|
3022
|
-
}
|
|
3023
|
-
log.newline();
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
// src/commands/task/status.ts
|
|
3027
|
-
var VALID_STATUSES = ["todo", "in_progress", "done"];
|
|
3028
|
-
async function taskStatusCommand(args, options = {}) {
|
|
3029
|
-
let taskId = args[0] ?? options.taskId;
|
|
3030
|
-
let newStatus = args[1] ?? options.status;
|
|
3031
|
-
if (options.noInteractive) {
|
|
3032
|
-
const errors = [];
|
|
3033
|
-
if (!taskId?.trim()) {
|
|
3034
|
-
errors.push("Task ID is required");
|
|
3035
|
-
}
|
|
3036
|
-
if (!newStatus?.trim()) {
|
|
3037
|
-
errors.push("Status is required");
|
|
3038
|
-
} else {
|
|
3039
|
-
const result2 = TaskStatusSchema.safeParse(newStatus);
|
|
3040
|
-
if (!result2.success) {
|
|
3041
|
-
errors.push(`Invalid status: ${newStatus} (valid: ${VALID_STATUSES.join(", ")})`);
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
if (errors.length > 0) {
|
|
3045
|
-
showError("Validation failed");
|
|
3046
|
-
for (const e of errors) {
|
|
3047
|
-
log.error(e);
|
|
3048
|
-
}
|
|
3049
|
-
log.newline();
|
|
3050
|
-
exitWithCode(EXIT_ERROR);
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
if (!taskId) {
|
|
3054
|
-
const selected = await selectTask("Select task to update:");
|
|
3055
|
-
if (!selected) return;
|
|
3056
|
-
taskId = selected;
|
|
3057
|
-
}
|
|
3058
|
-
if (!newStatus) {
|
|
3059
|
-
const selected = await selectTaskStatus("Select new status:");
|
|
3060
|
-
if (!selected) return;
|
|
3061
|
-
newStatus = selected;
|
|
3062
|
-
}
|
|
3063
|
-
const result = TaskStatusSchema.safeParse(newStatus);
|
|
3064
|
-
if (!result.success) {
|
|
3065
|
-
showError(`Invalid status: ${newStatus}`);
|
|
3066
|
-
log.dim(`Valid statuses: ${VALID_STATUSES.join(", ")}`);
|
|
3067
|
-
log.newline();
|
|
3068
|
-
if (options.noInteractive) {
|
|
3069
|
-
exitWithCode(EXIT_ERROR);
|
|
3070
|
-
}
|
|
3071
|
-
return;
|
|
3072
|
-
}
|
|
3073
|
-
const updateR = await wrapAsync(() => updateTaskStatus(taskId, result.data), ensureError);
|
|
3074
|
-
if (!updateR.ok) {
|
|
3075
|
-
if (updateR.error instanceof TaskNotFoundError) {
|
|
3076
|
-
showError(`Task not found: ${taskId}`);
|
|
3077
|
-
showNextStep("ralphctl task list", "see available tasks");
|
|
3078
|
-
log.newline();
|
|
3079
|
-
if (options.noInteractive) {
|
|
3080
|
-
exitWithCode(EXIT_ERROR);
|
|
3081
|
-
}
|
|
3082
|
-
} else if (updateR.error instanceof SprintStatusError) {
|
|
3083
|
-
showError(updateR.error.message);
|
|
3084
|
-
log.newline();
|
|
3085
|
-
if (options.noInteractive) {
|
|
3086
|
-
exitWithCode(EXIT_ERROR);
|
|
3087
|
-
}
|
|
3088
|
-
} else {
|
|
3089
|
-
throw updateR.error;
|
|
3090
|
-
}
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
showSuccess("Task status updated!", [
|
|
3094
|
-
["ID", updateR.value.id],
|
|
3095
|
-
["Name", updateR.value.name],
|
|
3096
|
-
["Status", formatTaskStatus(updateR.value.status)]
|
|
3097
|
-
]);
|
|
3098
|
-
log.newline();
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// src/commands/task/next.ts
|
|
3102
|
-
async function taskNextCommand() {
|
|
3103
|
-
const task = await getNextTask();
|
|
3104
|
-
if (!task) {
|
|
3105
|
-
showEmpty("pending tasks", "All tasks are done, or add more with: ralphctl task add");
|
|
3106
|
-
return;
|
|
3107
|
-
}
|
|
3108
|
-
printHeader("Next Task");
|
|
3109
|
-
console.log(field("ID", task.id));
|
|
3110
|
-
console.log(field("Name", task.name));
|
|
3111
|
-
console.log(field("Status", formatTaskStatus(task.status)));
|
|
3112
|
-
console.log(field("Order", String(task.order)));
|
|
3113
|
-
if (task.ticketId) {
|
|
3114
|
-
console.log(field("Ticket", task.ticketId));
|
|
3115
|
-
}
|
|
3116
|
-
if (task.description) {
|
|
3117
|
-
log.newline();
|
|
3118
|
-
console.log(field("Description", ""));
|
|
3119
|
-
log.raw(task.description, 2);
|
|
3120
|
-
}
|
|
3121
|
-
if (task.steps.length > 0) {
|
|
3122
|
-
log.newline();
|
|
3123
|
-
console.log(field("Steps", ""));
|
|
3124
|
-
task.steps.forEach((step, i) => {
|
|
3125
|
-
log.raw(`${String(i + 1)}. ${step}`, 2);
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
|
-
if (task.blockedBy.length > 0) {
|
|
3129
|
-
log.newline();
|
|
3130
|
-
console.log(field("Blocked By", ""));
|
|
3131
|
-
task.blockedBy.forEach((dep) => {
|
|
3132
|
-
log.item(dep);
|
|
3133
|
-
});
|
|
3134
|
-
}
|
|
3135
|
-
showNextStep(`ralphctl task status ${task.id} in_progress`, "Start working on this task");
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
// src/commands/task/reorder.ts
|
|
3139
|
-
async function taskReorderCommand(args) {
|
|
3140
|
-
let taskId = args[0];
|
|
3141
|
-
let newOrder;
|
|
3142
|
-
if (args[1]) {
|
|
3143
|
-
newOrder = parseInt(args[1], 10);
|
|
3144
|
-
}
|
|
3145
|
-
if (!taskId) {
|
|
3146
|
-
const selected = await selectTask("Select task to reorder:");
|
|
3147
|
-
if (!selected) return;
|
|
3148
|
-
taskId = selected;
|
|
3149
|
-
}
|
|
3150
|
-
if (newOrder === void 0 || isNaN(newOrder) || newOrder < 1) {
|
|
3151
|
-
newOrder = await inputPositiveInt("New position (1 = highest priority):");
|
|
3152
|
-
}
|
|
3153
|
-
const reorderR = await wrapAsync(() => reorderTask(taskId, newOrder), ensureError);
|
|
3154
|
-
if (!reorderR.ok) {
|
|
3155
|
-
if (reorderR.error instanceof TaskNotFoundError) {
|
|
3156
|
-
showError(`Task not found: ${taskId}`);
|
|
3157
|
-
log.newline();
|
|
3158
|
-
} else if (reorderR.error instanceof SprintStatusError) {
|
|
3159
|
-
showError(reorderR.error.message);
|
|
3160
|
-
log.newline();
|
|
3161
|
-
} else {
|
|
3162
|
-
throw reorderR.error;
|
|
3163
|
-
}
|
|
3164
|
-
return;
|
|
3165
|
-
}
|
|
3166
|
-
showSuccess("Task reordered!", [
|
|
3167
|
-
["ID", reorderR.value.id],
|
|
3168
|
-
["Name", reorderR.value.name],
|
|
3169
|
-
["New Order", String(reorderR.value.order)]
|
|
3170
|
-
]);
|
|
3171
|
-
log.newline();
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
// src/commands/task/remove.ts
|
|
3175
|
-
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
3176
|
-
async function taskRemoveCommand(args) {
|
|
3177
|
-
const skipConfirm = args.includes("-y") || args.includes("--yes");
|
|
3178
|
-
let taskId = args.find((a) => !a.startsWith("-"));
|
|
3179
|
-
if (!taskId) {
|
|
3180
|
-
const selected = await selectTask("Select task to remove:");
|
|
3181
|
-
if (!selected) return;
|
|
3182
|
-
taskId = selected;
|
|
3183
|
-
}
|
|
3184
|
-
const opR = await wrapAsync(async () => {
|
|
3185
|
-
const task = await getTask(taskId);
|
|
3186
|
-
if (!skipConfirm) {
|
|
3187
|
-
const confirmed = await confirm8({
|
|
3188
|
-
message: `Remove task "${task.name}" (${task.id})?`,
|
|
3189
|
-
default: false
|
|
3190
|
-
});
|
|
3191
|
-
if (!confirmed) {
|
|
3192
|
-
console.log(muted("\nTask removal cancelled.\n"));
|
|
3193
|
-
return null;
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
await removeTask(taskId);
|
|
3197
|
-
return task;
|
|
3198
|
-
}, ensureError);
|
|
3199
|
-
if (!opR.ok) {
|
|
3200
|
-
if (opR.error instanceof TaskNotFoundError) {
|
|
3201
|
-
showError(`Task not found: ${taskId}`);
|
|
3202
|
-
log.newline();
|
|
3203
|
-
} else if (opR.error instanceof SprintStatusError) {
|
|
3204
|
-
showError(opR.error.message);
|
|
3205
|
-
log.newline();
|
|
3206
|
-
} else {
|
|
3207
|
-
throw opR.error;
|
|
3208
|
-
}
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
if (opR.value !== null) {
|
|
3212
|
-
showSuccess("Task removed", [["ID", taskId]]);
|
|
3213
|
-
log.newline();
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
// src/commands/progress/log.ts
|
|
3218
|
-
async function progressLogCommand(args) {
|
|
3219
|
-
const statusCheckR = await wrapAsync(async () => {
|
|
3220
|
-
const sprintId = await resolveSprintId();
|
|
3221
|
-
const sprint = await getSprint(sprintId);
|
|
3222
|
-
assertSprintStatus(sprint, ["active"], "log progress");
|
|
3223
|
-
}, ensureError);
|
|
3224
|
-
if (!statusCheckR.ok) {
|
|
3225
|
-
const err = statusCheckR.error;
|
|
3226
|
-
if (err instanceof SprintStatusError) {
|
|
3227
|
-
const mainError = err.message.split("\n")[0] ?? err.message;
|
|
3228
|
-
showError(mainError);
|
|
3229
|
-
showNextStep("ralphctl sprint start", "activate the sprint");
|
|
3230
|
-
log.newline();
|
|
3231
|
-
return;
|
|
3232
|
-
}
|
|
3233
|
-
if (err instanceof NoCurrentSprintError) {
|
|
3234
|
-
showError("No current sprint set.");
|
|
3235
|
-
showNextStep("ralphctl sprint create", "create a new sprint");
|
|
3236
|
-
log.newline();
|
|
3237
|
-
return;
|
|
3238
|
-
}
|
|
3239
|
-
throw err;
|
|
3240
|
-
}
|
|
3241
|
-
let message = args.join(" ").trim();
|
|
3242
|
-
if (!message) {
|
|
3243
|
-
const editorR = await editorInput({
|
|
3244
|
-
message: "Progress message:"
|
|
3245
|
-
});
|
|
3246
|
-
if (!editorR.ok) {
|
|
3247
|
-
showError(`Editor input failed: ${editorR.error.message}`);
|
|
3248
|
-
log.newline();
|
|
3249
|
-
return;
|
|
3250
|
-
}
|
|
3251
|
-
message = editorR.value;
|
|
3252
|
-
message = message.trim();
|
|
3253
|
-
}
|
|
3254
|
-
if (!message) {
|
|
3255
|
-
showError("No message provided.");
|
|
3256
|
-
log.newline();
|
|
3257
|
-
return;
|
|
3258
|
-
}
|
|
3259
|
-
const logR = await wrapAsync(() => logProgress(message), ensureError);
|
|
3260
|
-
if (!logR.ok) {
|
|
3261
|
-
if (logR.error instanceof SprintStatusError) {
|
|
3262
|
-
showError(logR.error.message);
|
|
3263
|
-
log.newline();
|
|
3264
|
-
} else {
|
|
3265
|
-
throw logR.error;
|
|
3266
|
-
}
|
|
3267
|
-
return;
|
|
3268
|
-
}
|
|
3269
|
-
showSuccess("Progress logged.");
|
|
3270
|
-
log.newline();
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
// src/commands/progress/show.ts
|
|
3274
|
-
async function progressShowCommand() {
|
|
3275
|
-
const content = await getProgress();
|
|
3276
|
-
if (!content.trim()) {
|
|
3277
|
-
showEmpty("progress entries", "Log with: ralphctl progress log");
|
|
3278
|
-
return;
|
|
3279
|
-
}
|
|
3280
|
-
printHeader("Progress Log");
|
|
3281
|
-
console.log(content);
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
// src/commands/config/config.ts
|
|
3285
|
-
async function configSetCommand(args) {
|
|
3286
|
-
if (args.length < 2) {
|
|
3287
|
-
showError("Usage: ralphctl config set <key> <value>");
|
|
3288
|
-
log.dim("Available keys: provider, editor, evaluationIterations");
|
|
3289
|
-
log.newline();
|
|
3290
|
-
return;
|
|
3291
|
-
}
|
|
3292
|
-
const [key, value] = args;
|
|
3293
|
-
if (key === "provider") {
|
|
3294
|
-
const parsed = AiProviderSchema.safeParse(value);
|
|
3295
|
-
if (!parsed.success) {
|
|
3296
|
-
showError(`Invalid provider: ${value ?? "(empty)"}`);
|
|
3297
|
-
log.dim("Valid providers: claude, copilot");
|
|
3298
|
-
log.newline();
|
|
3299
|
-
return;
|
|
3300
|
-
}
|
|
3301
|
-
await setAiProvider(parsed.data);
|
|
3302
|
-
showSuccess(`AI provider set to: ${parsed.data}`);
|
|
3303
|
-
log.newline();
|
|
3304
|
-
return;
|
|
3305
|
-
}
|
|
3306
|
-
if (key === "editor") {
|
|
3307
|
-
const trimmed = value?.trim();
|
|
3308
|
-
if (!trimmed) {
|
|
3309
|
-
showError("Editor command cannot be empty");
|
|
3310
|
-
log.dim('Examples: "subl -w", "code --wait", "vim", "nano"');
|
|
3311
|
-
log.newline();
|
|
3312
|
-
return;
|
|
3313
|
-
}
|
|
3314
|
-
await setEditor(trimmed);
|
|
3315
|
-
showSuccess(`Editor set to: ${trimmed}`);
|
|
3316
|
-
log.newline();
|
|
3317
|
-
return;
|
|
3318
|
-
}
|
|
3319
|
-
if (key === "evaluationIterations") {
|
|
3320
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
3321
|
-
if (Number.isNaN(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
|
|
3322
|
-
showError(`Invalid evaluation iterations: ${value ?? "(empty)"}`);
|
|
3323
|
-
log.dim("Must be an integer >= 0 (0 = disabled)");
|
|
3324
|
-
log.newline();
|
|
3325
|
-
return;
|
|
3326
|
-
}
|
|
3327
|
-
await setEvaluationIterations(parsed);
|
|
3328
|
-
showSuccess(`Evaluation iterations set to: ${String(parsed)}`);
|
|
3329
|
-
log.newline();
|
|
3330
|
-
return;
|
|
3331
|
-
}
|
|
3332
|
-
showError(`Unknown config key: ${key ?? "(empty)"}`);
|
|
3333
|
-
log.dim("Available keys: provider, editor, evaluationIterations");
|
|
3334
|
-
log.newline();
|
|
3335
|
-
}
|
|
3336
|
-
async function configShowCommand() {
|
|
3337
|
-
const provider = await getAiProvider();
|
|
3338
|
-
const editorCmd = await getEditor();
|
|
3339
|
-
const evalIterations = await getEvaluationIterations();
|
|
3340
|
-
printHeader("Configuration", icons.info);
|
|
3341
|
-
console.log(field("AI Provider", provider ?? "(not set \u2014 will prompt on first use)"));
|
|
3342
|
-
console.log(field("Editor", editorCmd ?? "(not set \u2014 will prompt on first use)"));
|
|
3343
|
-
const evalDisplay = evalIterations === DEFAULT_EVALUATION_ITERATIONS ? `${String(evalIterations)} (default)` : evalIterations === 0 ? "0 (disabled)" : String(evalIterations);
|
|
3344
|
-
console.log(field("Evaluation Iterations", evalDisplay));
|
|
3345
|
-
log.newline();
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
// src/commands/doctor/doctor.ts
|
|
3349
|
-
import { access, constants } from "fs/promises";
|
|
3350
|
-
import { join as join4 } from "path";
|
|
3351
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
3352
|
-
var REQUIRED_NODE_MAJOR = 24;
|
|
3353
|
-
function checkNodeVersion() {
|
|
3354
|
-
const version = process.version;
|
|
3355
|
-
const match = /^v(\d+)/.exec(version);
|
|
3356
|
-
const major = match ? Number(match[1]) : 0;
|
|
3357
|
-
if (major >= REQUIRED_NODE_MAJOR) {
|
|
3358
|
-
return { name: "Node.js version", status: "pass", detail: version };
|
|
3359
|
-
}
|
|
3360
|
-
return {
|
|
3361
|
-
name: "Node.js version",
|
|
3362
|
-
status: "fail",
|
|
3363
|
-
detail: `${version} (requires >= ${String(REQUIRED_NODE_MAJOR)}.0.0)`
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
function checkGitInstalled() {
|
|
3367
|
-
const result = spawnSync2("git", ["--version"], {
|
|
3368
|
-
encoding: "utf-8",
|
|
3369
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3370
|
-
});
|
|
3371
|
-
if (result.status === 0) {
|
|
3372
|
-
const version = result.stdout.trim();
|
|
3373
|
-
return { name: "Git installed", status: "pass", detail: version };
|
|
3374
|
-
}
|
|
3375
|
-
return { name: "Git installed", status: "fail", detail: "git not found in PATH" };
|
|
3376
|
-
}
|
|
3377
|
-
function checkGitIdentity() {
|
|
3378
|
-
const nameResult = spawnSync2("git", ["config", "user.name"], {
|
|
3379
|
-
encoding: "utf-8",
|
|
3380
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3381
|
-
});
|
|
3382
|
-
const emailResult = spawnSync2("git", ["config", "user.email"], {
|
|
3383
|
-
encoding: "utf-8",
|
|
3384
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3385
|
-
});
|
|
3386
|
-
const name = nameResult.status === 0 ? nameResult.stdout.trim() : "";
|
|
3387
|
-
const email = emailResult.status === 0 ? emailResult.stdout.trim() : "";
|
|
3388
|
-
if (name && email) {
|
|
3389
|
-
return { name: "Git identity", status: "pass", detail: `${name} <${email}>` };
|
|
3390
|
-
}
|
|
3391
|
-
const missing = [];
|
|
3392
|
-
if (!name) missing.push("user.name");
|
|
3393
|
-
if (!email) missing.push("user.email");
|
|
3394
|
-
return { name: "Git identity", status: "warn", detail: `missing: ${missing.join(", ")}` };
|
|
3395
|
-
}
|
|
3396
|
-
async function checkAiProvider() {
|
|
3397
|
-
const config = await getConfig();
|
|
3398
|
-
const provider = config.aiProvider;
|
|
3399
|
-
if (!provider) {
|
|
3400
|
-
return { name: "AI provider binary", status: "skip", detail: "not configured" };
|
|
3401
|
-
}
|
|
3402
|
-
const binary = provider === "claude" ? "claude" : "copilot";
|
|
3403
|
-
const result = spawnSync2("which", [binary], {
|
|
3404
|
-
encoding: "utf-8",
|
|
3405
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3406
|
-
});
|
|
3407
|
-
if (result.status === 0) {
|
|
3408
|
-
const detail = provider === "copilot" ? `${binary} found (public preview)` : `${binary} found`;
|
|
3409
|
-
return { name: "AI provider binary", status: "pass", detail };
|
|
3410
|
-
}
|
|
3411
|
-
return {
|
|
3412
|
-
name: "AI provider binary",
|
|
3413
|
-
status: "fail",
|
|
3414
|
-
detail: `${binary} not found in PATH (provider: ${provider})`
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
function checkGlabInstalled() {
|
|
3418
|
-
if (isGlabAvailable()) {
|
|
3419
|
-
return { name: "GitLab CLI (glab)", status: "pass", detail: "installed" };
|
|
3420
|
-
}
|
|
3421
|
-
return {
|
|
3422
|
-
name: "GitLab CLI (glab)",
|
|
3423
|
-
status: "skip",
|
|
3424
|
-
detail: "not installed (optional \u2014 needed for GitLab issue enrichment)"
|
|
3425
|
-
};
|
|
3426
|
-
}
|
|
3427
|
-
async function checkDataDirectory() {
|
|
3428
|
-
const dataDir = getDataDir();
|
|
3429
|
-
const accessR = await wrapAsync(() => access(dataDir, constants.R_OK | constants.W_OK), ensureError);
|
|
3430
|
-
if (accessR.ok) {
|
|
3431
|
-
return { name: "Data directory", status: "pass", detail: dataDir };
|
|
3432
|
-
}
|
|
3433
|
-
return { name: "Data directory", status: "fail", detail: `${dataDir} not accessible or writable` };
|
|
3434
|
-
}
|
|
3435
|
-
async function checkProjectPaths() {
|
|
3436
|
-
const projects = await listProjects();
|
|
3437
|
-
if (projects.length === 0) {
|
|
3438
|
-
return { name: "Project paths", status: "skip", detail: "no projects registered" };
|
|
3439
|
-
}
|
|
3440
|
-
const issues = [];
|
|
3441
|
-
for (const project of projects) {
|
|
3442
|
-
for (const repo of project.repositories) {
|
|
3443
|
-
const validation = await validateProjectPath(repo.path);
|
|
3444
|
-
if (!validation.ok) {
|
|
3445
|
-
issues.push(`${project.name}/${repo.name}: ${validation.error.message}`);
|
|
3446
|
-
continue;
|
|
3447
|
-
}
|
|
3448
|
-
const gitDir = join4(repo.path, ".git");
|
|
3449
|
-
if (!await fileExists(gitDir)) {
|
|
3450
|
-
issues.push(`${project.name}/${repo.name}: not a git repository`);
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
|
-
}
|
|
3454
|
-
if (issues.length === 0) {
|
|
3455
|
-
const repoCount = projects.reduce((sum, p) => sum + p.repositories.length, 0);
|
|
3456
|
-
return {
|
|
3457
|
-
name: "Project paths",
|
|
3458
|
-
status: "pass",
|
|
3459
|
-
detail: `${String(repoCount)} repo${repoCount !== 1 ? "s" : ""} verified`
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
return { name: "Project paths", status: "fail", detail: issues.join("; ") };
|
|
3463
|
-
}
|
|
3464
|
-
async function checkEvaluationConfig() {
|
|
3465
|
-
const config = await getConfig();
|
|
3466
|
-
if (config.evaluationIterations == null) {
|
|
3467
|
-
return {
|
|
3468
|
-
name: "Evaluation config",
|
|
3469
|
-
status: "warn",
|
|
3470
|
-
detail: "evaluationIterations not set \u2014 defaulting to 1 (set via: ralphctl config set evaluationIterations <n>)"
|
|
3471
|
-
};
|
|
3472
|
-
}
|
|
3473
|
-
return {
|
|
3474
|
-
name: "Evaluation config",
|
|
3475
|
-
status: "pass",
|
|
3476
|
-
detail: `evaluationIterations: ${String(config.evaluationIterations)}`
|
|
3477
|
-
};
|
|
3478
|
-
}
|
|
3479
|
-
async function checkCurrentSprint() {
|
|
3480
|
-
const config = await getConfig();
|
|
3481
|
-
const sprintId = config.currentSprint;
|
|
3482
|
-
if (!sprintId) {
|
|
3483
|
-
return { name: "Current sprint", status: "skip", detail: "no current sprint set" };
|
|
3484
|
-
}
|
|
3485
|
-
const sprintPath = getSprintFilePath(sprintId);
|
|
3486
|
-
if (!await fileExists(sprintPath)) {
|
|
3487
|
-
return { name: "Current sprint", status: "fail", detail: `sprint file missing: ${sprintId}` };
|
|
3488
|
-
}
|
|
3489
|
-
const result = await readValidatedJson(sprintPath, SprintSchema);
|
|
3490
|
-
if (!result.ok) {
|
|
3491
|
-
return { name: "Current sprint", status: "fail", detail: `invalid sprint data: ${result.error.message}` };
|
|
3492
|
-
}
|
|
3493
|
-
return { name: "Current sprint", status: "pass", detail: `${result.value.name} (${result.value.status})` };
|
|
3494
|
-
}
|
|
3495
|
-
async function doctorCommand() {
|
|
3496
|
-
printHeader("System Health Check", icons.info);
|
|
3497
|
-
const results = [];
|
|
3498
|
-
results.push(checkNodeVersion());
|
|
3499
|
-
results.push(checkGitInstalled());
|
|
3500
|
-
results.push(checkGitIdentity());
|
|
3501
|
-
results.push(checkGlabInstalled());
|
|
3502
|
-
const asyncResults = await Promise.all([
|
|
3503
|
-
checkAiProvider(),
|
|
3504
|
-
checkDataDirectory(),
|
|
3505
|
-
checkProjectPaths(),
|
|
3506
|
-
checkCurrentSprint(),
|
|
3507
|
-
checkEvaluationConfig()
|
|
3508
|
-
]);
|
|
3509
|
-
results.push(...asyncResults);
|
|
3510
|
-
for (const result of results) {
|
|
3511
|
-
if (result.status === "pass") {
|
|
3512
|
-
log.success(`${result.name}${result.detail ? colors.muted(` \u2014 ${result.detail}`) : ""}`);
|
|
3513
|
-
} else if (result.status === "warn") {
|
|
3514
|
-
log.warn(`${result.name}${result.detail ? colors.muted(` \u2014 ${result.detail}`) : ""}`);
|
|
3515
|
-
} else if (result.status === "fail") {
|
|
3516
|
-
log.error(result.name);
|
|
3517
|
-
if (result.detail) {
|
|
3518
|
-
log.dim(` ${result.detail}`);
|
|
3519
|
-
}
|
|
3520
|
-
} else {
|
|
3521
|
-
log.raw(
|
|
3522
|
-
`${icons.bullet} ${colors.muted(result.name)} ${colors.muted("\u2014")} ${colors.muted(result.detail ?? "skipped")}`
|
|
3523
|
-
);
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
log.newline();
|
|
3527
|
-
const passed = results.filter((r) => r.status === "pass").length;
|
|
3528
|
-
const warned = results.filter((r) => r.status === "warn").length;
|
|
3529
|
-
const failed = results.filter((r) => r.status === "fail").length;
|
|
3530
|
-
const total = results.filter((r) => r.status !== "skip").length;
|
|
3531
|
-
if (failed === 0 && warned === 0) {
|
|
3532
|
-
log.success(`All checks passed (${String(passed)}/${String(total)})`);
|
|
3533
|
-
log.newline();
|
|
3534
|
-
const quote = getQuoteForContext("success");
|
|
3535
|
-
log.dim(`"${quote}"`);
|
|
3536
|
-
} else if (failed === 0) {
|
|
3537
|
-
log.success(
|
|
3538
|
-
`${String(passed)}/${String(total)} checks passed, ${String(warned)} warning${warned !== 1 ? "s" : ""}`
|
|
3539
|
-
);
|
|
3540
|
-
log.newline();
|
|
3541
|
-
const quote = getQuoteForContext("success");
|
|
3542
|
-
log.dim(`"${quote}"`);
|
|
3543
|
-
} else {
|
|
3544
|
-
log.error(`${String(passed)}/${String(total)} checks passed, ${String(failed)} failed`);
|
|
3545
|
-
log.newline();
|
|
3546
|
-
const quote = getQuoteForContext("error");
|
|
3547
|
-
log.dim(`"${quote}"`);
|
|
3548
|
-
process.exitCode = EXIT_ERROR;
|
|
3549
|
-
}
|
|
3550
|
-
log.newline();
|
|
3551
|
-
}
|
|
3
|
+
cliMetadata,
|
|
4
|
+
configSetCommand,
|
|
5
|
+
configShowCommand,
|
|
6
|
+
createSharedDeps,
|
|
7
|
+
doctorCommand,
|
|
8
|
+
progressLogCommand,
|
|
9
|
+
progressShowCommand,
|
|
10
|
+
projectListCommand,
|
|
11
|
+
projectRemoveCommand,
|
|
12
|
+
projectRepoAddCommand,
|
|
13
|
+
projectRepoRemoveCommand,
|
|
14
|
+
projectShowCommand,
|
|
15
|
+
selectSprint,
|
|
16
|
+
showDashboard,
|
|
17
|
+
sprintCloseCommand,
|
|
18
|
+
sprintContextCommand,
|
|
19
|
+
sprintCurrentCommand,
|
|
20
|
+
sprintDeleteCommand,
|
|
21
|
+
sprintHealthCommand,
|
|
22
|
+
sprintIdeateCommand,
|
|
23
|
+
sprintListCommand,
|
|
24
|
+
sprintPlanCommand,
|
|
25
|
+
sprintRefineCommand,
|
|
26
|
+
sprintRequirementsCommand,
|
|
27
|
+
sprintShowCommand,
|
|
28
|
+
taskAddCommand,
|
|
29
|
+
taskImportCommand,
|
|
30
|
+
taskListCommand,
|
|
31
|
+
taskNextCommand,
|
|
32
|
+
taskRemoveCommand,
|
|
33
|
+
taskReorderCommand,
|
|
34
|
+
taskShowCommand,
|
|
35
|
+
taskStatusCommand,
|
|
36
|
+
ticketEditCommand,
|
|
37
|
+
ticketListCommand,
|
|
38
|
+
ticketRefineCommand,
|
|
39
|
+
ticketRemoveCommand,
|
|
40
|
+
ticketShowCommand
|
|
41
|
+
} from "./chunk-EPDR6VO5.mjs";
|
|
42
|
+
import {
|
|
43
|
+
projectAddCommand
|
|
44
|
+
} from "./chunk-D2YGPLIV.mjs";
|
|
45
|
+
import {
|
|
46
|
+
sprintCreateCommand
|
|
47
|
+
} from "./chunk-3QBEBKMZ.mjs";
|
|
48
|
+
import {
|
|
49
|
+
ticketAddCommand
|
|
50
|
+
} from "./chunk-7JLZQICD.mjs";
|
|
51
|
+
import "./chunk-NUYQK5MN.mjs";
|
|
52
|
+
import {
|
|
53
|
+
getTasks,
|
|
54
|
+
sprintStartCommand
|
|
55
|
+
} from "./chunk-CSC4TBJB.mjs";
|
|
56
|
+
import {
|
|
57
|
+
truncate
|
|
58
|
+
} from "./chunk-JOQO4HMM.mjs";
|
|
59
|
+
import {
|
|
60
|
+
EXIT_ERROR
|
|
61
|
+
} from "./chunk-CFUVE2BP.mjs";
|
|
62
|
+
import {
|
|
63
|
+
setSharedDeps
|
|
64
|
+
} from "./chunk-747KW2RW.mjs";
|
|
65
|
+
import {
|
|
66
|
+
getCurrentSprintOrThrow,
|
|
67
|
+
getSprint,
|
|
68
|
+
setCurrentSprint
|
|
69
|
+
} from "./chunk-YCDUVPRT.mjs";
|
|
70
|
+
import {
|
|
71
|
+
colors,
|
|
72
|
+
error,
|
|
73
|
+
icons,
|
|
74
|
+
log,
|
|
75
|
+
printBanner,
|
|
76
|
+
printHeader,
|
|
77
|
+
showError,
|
|
78
|
+
showSuccess
|
|
79
|
+
} from "./chunk-FKMKOWLA.mjs";
|
|
80
|
+
import {
|
|
81
|
+
ensureError,
|
|
82
|
+
wrapAsync
|
|
83
|
+
} from "./chunk-IWXBJD2D.mjs";
|
|
84
|
+
import {
|
|
85
|
+
ensureDir,
|
|
86
|
+
getDataDir
|
|
87
|
+
} from "./chunk-CTP2A436.mjs";
|
|
88
|
+
import {
|
|
89
|
+
DomainError
|
|
90
|
+
} from "./chunk-57UWLHRH.mjs";
|
|
3552
91
|
|
|
3553
|
-
// src/
|
|
3554
|
-
|
|
3555
|
-
icon: { cursor: emoji.donut },
|
|
3556
|
-
style: {
|
|
3557
|
-
highlight: (text) => colors.highlight(text),
|
|
3558
|
-
description: (text) => colors.muted(text)
|
|
3559
|
-
}
|
|
3560
|
-
};
|
|
3561
|
-
var commandMap = {
|
|
3562
|
-
project: {
|
|
3563
|
-
add: () => projectAddCommand({ interactive: true }),
|
|
3564
|
-
list: () => projectListCommand(),
|
|
3565
|
-
show: () => projectShowCommand([]),
|
|
3566
|
-
remove: () => projectRemoveCommand([]),
|
|
3567
|
-
"repo add": () => projectRepoAddCommand([]),
|
|
3568
|
-
"repo remove": () => projectRepoRemoveCommand([])
|
|
3569
|
-
},
|
|
3570
|
-
sprint: {
|
|
3571
|
-
create: () => sprintCreateCommand({ interactive: true }),
|
|
3572
|
-
list: () => sprintListCommand(),
|
|
3573
|
-
show: () => sprintShowCommand([]),
|
|
3574
|
-
context: () => sprintContextCommand([]),
|
|
3575
|
-
current: () => sprintCurrentCommand(["-"]),
|
|
3576
|
-
refine: () => sprintRefineCommand([]),
|
|
3577
|
-
ideate: () => sprintIdeateCommand([]),
|
|
3578
|
-
plan: () => sprintPlanCommand([]),
|
|
3579
|
-
start: () => sprintStartCommand([]),
|
|
3580
|
-
requirements: () => sprintRequirementsCommand([]),
|
|
3581
|
-
health: () => sprintHealthCommand(),
|
|
3582
|
-
close: () => sprintCloseCommand([]),
|
|
3583
|
-
delete: () => sprintDeleteCommand([]),
|
|
3584
|
-
"progress show": () => progressShowCommand(),
|
|
3585
|
-
"progress log": () => progressLogCommand([])
|
|
3586
|
-
},
|
|
3587
|
-
ticket: {
|
|
3588
|
-
add: () => ticketAddCommand({ interactive: true }),
|
|
3589
|
-
edit: () => ticketEditCommand(void 0, { interactive: true }),
|
|
3590
|
-
list: () => ticketListCommand([]),
|
|
3591
|
-
show: () => ticketShowCommand([]),
|
|
3592
|
-
refine: () => ticketRefineCommand(void 0, { interactive: true }),
|
|
3593
|
-
remove: () => ticketRemoveCommand([])
|
|
3594
|
-
},
|
|
3595
|
-
task: {
|
|
3596
|
-
add: () => taskAddCommand({ interactive: true }),
|
|
3597
|
-
import: () => taskImportCommand([]),
|
|
3598
|
-
list: () => taskListCommand([]),
|
|
3599
|
-
show: () => taskShowCommand([]),
|
|
3600
|
-
status: () => taskStatusCommand([]),
|
|
3601
|
-
next: () => taskNextCommand(),
|
|
3602
|
-
reorder: () => taskReorderCommand([]),
|
|
3603
|
-
remove: () => taskRemoveCommand([])
|
|
3604
|
-
},
|
|
3605
|
-
progress: {
|
|
3606
|
-
log: () => progressLogCommand([]),
|
|
3607
|
-
show: () => progressShowCommand()
|
|
3608
|
-
},
|
|
3609
|
-
doctor: {
|
|
3610
|
-
run: () => doctorCommand()
|
|
3611
|
-
},
|
|
3612
|
-
config: {
|
|
3613
|
-
show: () => configShowCommand(),
|
|
3614
|
-
"set provider": async () => {
|
|
3615
|
-
const choice = await select3({
|
|
3616
|
-
message: `${emoji.donut} Which AI buddy should help with my homework?`,
|
|
3617
|
-
choices: [
|
|
3618
|
-
{ name: "Claude Code", value: "claude" },
|
|
3619
|
-
{ name: "GitHub Copilot", value: "copilot" }
|
|
3620
|
-
],
|
|
3621
|
-
default: await getAiProvider() ?? void 0,
|
|
3622
|
-
theme: selectTheme
|
|
3623
|
-
});
|
|
3624
|
-
await configSetCommand(["provider", choice]);
|
|
3625
|
-
},
|
|
3626
|
-
"set editor": async () => {
|
|
3627
|
-
const current = await getEditor();
|
|
3628
|
-
const value = await input5({
|
|
3629
|
-
message: `${emoji.donut} Which editor should open for refinement?`,
|
|
3630
|
-
default: current ?? void 0,
|
|
3631
|
-
theme: selectTheme
|
|
3632
|
-
});
|
|
3633
|
-
if (value.trim()) {
|
|
3634
|
-
await configSetCommand(["editor", value.trim()]);
|
|
3635
|
-
}
|
|
3636
|
-
},
|
|
3637
|
-
"set evaluationIterations": async () => {
|
|
3638
|
-
const current = await getEvaluationIterations();
|
|
3639
|
-
const value = await input5({
|
|
3640
|
-
message: `${emoji.donut} How many evaluation loops? (0 = disabled)`,
|
|
3641
|
-
default: String(current),
|
|
3642
|
-
theme: selectTheme
|
|
3643
|
-
});
|
|
3644
|
-
await configSetCommand(["evaluationIterations", value.trim()]);
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
};
|
|
3648
|
-
function showFarewell() {
|
|
3649
|
-
const quote = getQuoteForContext("farewell");
|
|
3650
|
-
console.log("");
|
|
3651
|
-
printSeparator();
|
|
3652
|
-
console.log(` ${emoji.donut} ${colors.muted(quote)}`);
|
|
3653
|
-
console.log("");
|
|
3654
|
-
}
|
|
3655
|
-
async function pressEnterToContinue() {
|
|
3656
|
-
const { createInterface } = await import("readline");
|
|
3657
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3658
|
-
await new Promise((resolve3) => {
|
|
3659
|
-
rl.question(colors.muted(" Press Enter to continue..."), () => {
|
|
3660
|
-
rl.close();
|
|
3661
|
-
resolve3();
|
|
3662
|
-
});
|
|
3663
|
-
});
|
|
3664
|
-
}
|
|
3665
|
-
function showWelcomeBanner() {
|
|
3666
|
-
showBanner();
|
|
3667
|
-
}
|
|
3668
|
-
async function readTasksSafe(sprintId) {
|
|
3669
|
-
const result = await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
|
|
3670
|
-
if (!result.ok) return [];
|
|
3671
|
-
return result.value;
|
|
3672
|
-
}
|
|
3673
|
-
async function getMenuContext() {
|
|
3674
|
-
let dashboardData = null;
|
|
3675
|
-
const ctx = {
|
|
3676
|
-
hasProjects: false,
|
|
3677
|
-
projectCount: 0,
|
|
3678
|
-
currentSprintId: null,
|
|
3679
|
-
currentSprintName: null,
|
|
3680
|
-
currentSprintStatus: null,
|
|
3681
|
-
ticketCount: 0,
|
|
3682
|
-
taskCount: 0,
|
|
3683
|
-
tasksDone: 0,
|
|
3684
|
-
tasksInProgress: 0,
|
|
3685
|
-
pendingRequirements: 0,
|
|
3686
|
-
allRequirementsApproved: false,
|
|
3687
|
-
plannedTicketCount: 0,
|
|
3688
|
-
nextAction: null,
|
|
3689
|
-
aiProvider: null
|
|
3690
|
-
};
|
|
3691
|
-
const [config, projects] = await Promise.all([getConfig().catch(() => null), listProjects().catch(() => [])]);
|
|
3692
|
-
ctx.hasProjects = projects.length > 0;
|
|
3693
|
-
ctx.projectCount = projects.length;
|
|
3694
|
-
ctx.aiProvider = config?.aiProvider ?? null;
|
|
3695
|
-
const sprintId = config?.currentSprint ?? null;
|
|
3696
|
-
if (!sprintId) return { ctx, dashboardData };
|
|
3697
|
-
ctx.currentSprintId = sprintId;
|
|
3698
|
-
const [sprint, tasks] = await Promise.all([getSprint(sprintId).catch(() => null), readTasksSafe(sprintId)]);
|
|
3699
|
-
if (!sprint) return { ctx, dashboardData };
|
|
3700
|
-
ctx.currentSprintName = sprint.name;
|
|
3701
|
-
ctx.currentSprintStatus = sprint.status;
|
|
3702
|
-
ctx.ticketCount = sprint.tickets.length;
|
|
3703
|
-
const pendingTickets = getPendingRequirements(sprint.tickets);
|
|
3704
|
-
ctx.pendingRequirements = pendingTickets.length;
|
|
3705
|
-
ctx.allRequirementsApproved = allRequirementsApproved(sprint.tickets);
|
|
3706
|
-
ctx.taskCount = tasks.length;
|
|
3707
|
-
ctx.tasksDone = tasks.filter((t) => t.status === "done").length;
|
|
3708
|
-
ctx.tasksInProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
3709
|
-
const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
|
|
3710
|
-
ctx.plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
|
|
3711
|
-
const doneIds = new Set(tasks.filter((t) => t.status === "done").map((t) => t.id));
|
|
3712
|
-
const blockedCount = tasks.filter(
|
|
3713
|
-
(t) => t.status !== "done" && t.blockedBy.length > 0 && !t.blockedBy.every((id) => doneIds.has(id))
|
|
3714
|
-
).length;
|
|
3715
|
-
dashboardData = {
|
|
3716
|
-
sprint,
|
|
3717
|
-
tasks,
|
|
3718
|
-
approvedCount: sprint.tickets.length - pendingTickets.length,
|
|
3719
|
-
pendingCount: pendingTickets.length,
|
|
3720
|
-
blockedCount,
|
|
3721
|
-
plannedTicketCount: ctx.plannedTicketCount,
|
|
3722
|
-
aiProvider: ctx.aiProvider
|
|
3723
|
-
};
|
|
3724
|
-
ctx.nextAction = getNextAction(dashboardData);
|
|
3725
|
-
return { ctx, dashboardData };
|
|
3726
|
-
}
|
|
3727
|
-
async function interactiveMode() {
|
|
3728
|
-
let escPressed = false;
|
|
3729
|
-
while (true) {
|
|
3730
|
-
const { ctx, dashboardData } = await getMenuContext();
|
|
3731
|
-
clearScreen();
|
|
3732
|
-
showWelcomeBanner();
|
|
3733
|
-
const statusLines = renderStatusHeader(dashboardData);
|
|
3734
|
-
if (statusLines.length > 0) {
|
|
3735
|
-
for (const line2 of statusLines) {
|
|
3736
|
-
console.log(line2);
|
|
3737
|
-
}
|
|
3738
|
-
log.newline();
|
|
3739
|
-
}
|
|
3740
|
-
const { items: mainMenu, defaultValue } = buildMainMenu(ctx);
|
|
3741
|
-
const effectiveDefault = escPressed ? "exit" : defaultValue;
|
|
3742
|
-
escPressed = false;
|
|
3743
|
-
const commandResult = await wrapAsync(
|
|
3744
|
-
() => escapableSelect(
|
|
3745
|
-
{
|
|
3746
|
-
message: `${emoji.donut} What would you like to do?`,
|
|
3747
|
-
choices: mainMenu,
|
|
3748
|
-
default: effectiveDefault,
|
|
3749
|
-
pageSize: 30,
|
|
3750
|
-
loop: true,
|
|
3751
|
-
theme: selectTheme
|
|
3752
|
-
},
|
|
3753
|
-
{ escLabel: "exit" }
|
|
3754
|
-
),
|
|
3755
|
-
ensureError
|
|
3756
|
-
);
|
|
3757
|
-
if (!commandResult.ok) {
|
|
3758
|
-
if (commandResult.error.name === "ExitPromptError") {
|
|
3759
|
-
showFarewell();
|
|
3760
|
-
break;
|
|
3761
|
-
}
|
|
3762
|
-
throw commandResult.error;
|
|
3763
|
-
}
|
|
3764
|
-
const command = commandResult.value;
|
|
3765
|
-
if (command === null) {
|
|
3766
|
-
escPressed = true;
|
|
3767
|
-
continue;
|
|
3768
|
-
}
|
|
3769
|
-
if (command === "exit") {
|
|
3770
|
-
showFarewell();
|
|
3771
|
-
break;
|
|
3772
|
-
}
|
|
3773
|
-
if (command.startsWith("action:")) {
|
|
3774
|
-
const parts = command.split(":");
|
|
3775
|
-
const group = parts[1] ?? "";
|
|
3776
|
-
const subCommand = parts[2] ?? "";
|
|
3777
|
-
log.newline();
|
|
3778
|
-
await executeCommand(group, subCommand);
|
|
3779
|
-
log.newline();
|
|
3780
|
-
await pressEnterToContinue();
|
|
3781
|
-
continue;
|
|
3782
|
-
}
|
|
3783
|
-
if (command === "wizard") {
|
|
3784
|
-
const { runWizard } = await import("./wizard-XZ7OGBCJ.mjs");
|
|
3785
|
-
await runWizard();
|
|
3786
|
-
continue;
|
|
3787
|
-
}
|
|
3788
|
-
const subMenu = buildSubMenu(command, ctx);
|
|
3789
|
-
if (subMenu) {
|
|
3790
|
-
await handleSubMenu(command, subMenu);
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
async function handleSubMenu(commandGroup, initialSubMenu) {
|
|
3795
|
-
let currentTitle = initialSubMenu.title;
|
|
3796
|
-
let currentItems = initialSubMenu.items;
|
|
3797
|
-
while (true) {
|
|
3798
|
-
log.newline();
|
|
3799
|
-
const subCommandResult = await wrapAsync(
|
|
3800
|
-
() => escapableSelect({
|
|
3801
|
-
message: `${emoji.donut} ${currentTitle}`,
|
|
3802
|
-
choices: currentItems,
|
|
3803
|
-
pageSize: 30,
|
|
3804
|
-
loop: true,
|
|
3805
|
-
theme: selectTheme
|
|
3806
|
-
}),
|
|
3807
|
-
ensureError
|
|
3808
|
-
);
|
|
3809
|
-
if (!subCommandResult.ok) {
|
|
3810
|
-
if (subCommandResult.error.name === "ExitPromptError") {
|
|
3811
|
-
break;
|
|
3812
|
-
}
|
|
3813
|
-
throw subCommandResult.error;
|
|
3814
|
-
}
|
|
3815
|
-
const subCommand = subCommandResult.value;
|
|
3816
|
-
if (subCommand === null || subCommand === "back") {
|
|
3817
|
-
break;
|
|
3818
|
-
}
|
|
3819
|
-
log.newline();
|
|
3820
|
-
await executeCommand(commandGroup, subCommand);
|
|
3821
|
-
log.newline();
|
|
3822
|
-
if (isWorkflowAction(commandGroup, subCommand)) {
|
|
3823
|
-
break;
|
|
3824
|
-
}
|
|
3825
|
-
const { ctx: refreshedCtx } = await getMenuContext();
|
|
3826
|
-
const refreshedMenu = buildSubMenu(commandGroup, refreshedCtx);
|
|
3827
|
-
if (refreshedMenu) {
|
|
3828
|
-
currentTitle = refreshedMenu.title;
|
|
3829
|
-
currentItems = refreshedMenu.items;
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
async function executeCommand(group, subCommand) {
|
|
3834
|
-
const groupHandlers = commandMap[group];
|
|
3835
|
-
const handler = groupHandlers?.[subCommand];
|
|
3836
|
-
if (!handler) {
|
|
3837
|
-
log.error(`Unknown command: ${group} ${subCommand}`);
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
const r = await wrapAsync(() => handler(), ensureError);
|
|
3841
|
-
if (!r.ok) {
|
|
3842
|
-
log.error(r.error.message);
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
92
|
+
// src/application/entrypoint.ts
|
|
93
|
+
import { Command } from "commander";
|
|
3845
94
|
|
|
3846
|
-
// src/commands/project/
|
|
95
|
+
// src/integration/cli/commands/project/register.ts
|
|
3847
96
|
function registerProjectCommands(program2) {
|
|
3848
97
|
const project = program2.command("project").description("Manage projects");
|
|
3849
98
|
project.addHelpText(
|
|
@@ -3903,7 +152,7 @@ Examples:
|
|
|
3903
152
|
});
|
|
3904
153
|
}
|
|
3905
154
|
|
|
3906
|
-
// src/commands/sprint/switch.ts
|
|
155
|
+
// src/integration/cli/commands/sprint/switch.ts
|
|
3907
156
|
async function sprintSwitchCommand() {
|
|
3908
157
|
const selectedId = await selectSprint("Select sprint to switch to:");
|
|
3909
158
|
if (!selectedId) return;
|
|
@@ -3916,9 +165,9 @@ async function sprintSwitchCommand() {
|
|
|
3916
165
|
log.newline();
|
|
3917
166
|
}
|
|
3918
167
|
|
|
3919
|
-
// src/commands/sprint/insights.ts
|
|
3920
|
-
import { writeFile
|
|
3921
|
-
import { join
|
|
168
|
+
// src/integration/cli/commands/sprint/insights.ts
|
|
169
|
+
import { writeFile } from "fs/promises";
|
|
170
|
+
import { join } from "path";
|
|
3922
171
|
async function sprintInsightsCommand(args) {
|
|
3923
172
|
const exportFlag = args.includes("--export");
|
|
3924
173
|
const positionalArgs = args.filter((a) => !a.startsWith("--"));
|
|
@@ -3948,7 +197,7 @@ async function sprintInsightsCommand(args) {
|
|
|
3948
197
|
console.log(` ${colors.accent("Evaluation output:")}`);
|
|
3949
198
|
for (const task of withOutput) {
|
|
3950
199
|
const output = task.evaluationOutput ?? "";
|
|
3951
|
-
const truncated = output
|
|
200
|
+
const truncated = truncate(output, 200);
|
|
3952
201
|
console.log(` ${icons.bullet} ${colors.accent(task.name)}: ${colors.muted(truncated)}`);
|
|
3953
202
|
}
|
|
3954
203
|
log.newline();
|
|
@@ -3970,9 +219,9 @@ async function sprintInsightsCommand(args) {
|
|
|
3970
219
|
}
|
|
3971
220
|
}
|
|
3972
221
|
async function exportInsights(sprint, tasks) {
|
|
3973
|
-
const dir =
|
|
222
|
+
const dir = join(getDataDir(), "insights");
|
|
3974
223
|
await ensureDir(dir);
|
|
3975
|
-
const filePath =
|
|
224
|
+
const filePath = join(dir, `${sprint.id}.md`);
|
|
3976
225
|
const evaluatedCount = tasks.filter((t) => t.evaluated).length;
|
|
3977
226
|
const lines = [
|
|
3978
227
|
`# Sprint Insights: ${sprint.name}`,
|
|
@@ -3993,11 +242,11 @@ async function exportInsights(sprint, tasks) {
|
|
|
3993
242
|
lines.push("");
|
|
3994
243
|
lines.push("---");
|
|
3995
244
|
}
|
|
3996
|
-
await
|
|
245
|
+
await writeFile(filePath, lines.join("\n"), "utf-8");
|
|
3997
246
|
log.success(`Insights exported to ${colors.accent(filePath)}`);
|
|
3998
247
|
}
|
|
3999
248
|
|
|
4000
|
-
// src/commands/sprint/
|
|
249
|
+
// src/integration/cli/commands/sprint/register.ts
|
|
4001
250
|
function registerSprintCommands(program2) {
|
|
4002
251
|
const sprint = program2.command("sprint").description("Manage sprints");
|
|
4003
252
|
sprint.addHelpText(
|
|
@@ -4010,10 +259,10 @@ Examples:
|
|
|
4010
259
|
$ ralphctl sprint start -s # Start with interactive session
|
|
4011
260
|
`
|
|
4012
261
|
);
|
|
4013
|
-
sprint.command("create").description("Create a new sprint").option("--name <name>", "Sprint name").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(async (opts) => {
|
|
262
|
+
sprint.command("create").description("Create a new sprint").option("--name <name>", "Sprint name").option("--project <name>", "Project to scope this sprint to (name or id)").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(async (opts) => {
|
|
4014
263
|
await sprintCreateCommand({
|
|
4015
264
|
name: opts.name,
|
|
4016
|
-
|
|
265
|
+
project: opts.project,
|
|
4017
266
|
interactive: opts.interactive !== false
|
|
4018
267
|
});
|
|
4019
268
|
});
|
|
@@ -4123,7 +372,7 @@ Branch Management:
|
|
|
4123
372
|
);
|
|
4124
373
|
}
|
|
4125
374
|
|
|
4126
|
-
// src/commands/task/
|
|
375
|
+
// src/integration/cli/commands/task/register.ts
|
|
4127
376
|
function registerTaskCommands(program2) {
|
|
4128
377
|
const task = program2.command("task").description("Manage tasks");
|
|
4129
378
|
task.addHelpText(
|
|
@@ -4136,15 +385,14 @@ Examples:
|
|
|
4136
385
|
$ ralphctl task next
|
|
4137
386
|
`
|
|
4138
387
|
);
|
|
4139
|
-
task.command("add").description("Add task to current sprint").option("--name <name>", "Task name").option("-d, --description <desc>", "Description").option("--step <step...>", "Implementation step (repeatable)").option("--ticket <id>", "Link to ticket ID").option("-
|
|
388
|
+
task.command("add").description("Add task to current sprint").option("--name <name>", "Task name").option("-d, --description <desc>", "Description").option("--step <step...>", "Implementation step (repeatable)").option("--ticket <id>", "Link to ticket ID").option("-r, --repo <name-or-id>", "Repository (within the sprint's project)").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(
|
|
4140
389
|
async (opts) => {
|
|
4141
390
|
await taskAddCommand({
|
|
4142
391
|
name: opts.name,
|
|
4143
392
|
description: opts.description,
|
|
4144
393
|
steps: opts.step,
|
|
4145
394
|
ticket: opts.ticket,
|
|
4146
|
-
|
|
4147
|
-
// --no-interactive sets interactive=false, otherwise true (prompt for missing)
|
|
395
|
+
repo: opts.repo,
|
|
4148
396
|
interactive: opts.interactive !== false
|
|
4149
397
|
});
|
|
4150
398
|
}
|
|
@@ -4152,17 +400,15 @@ Examples:
|
|
|
4152
400
|
task.command("import <file>").description("Import tasks from JSON file").action(async (file) => {
|
|
4153
401
|
await taskImportCommand([file]);
|
|
4154
402
|
});
|
|
4155
|
-
task.command("list").description("List tasks").option("-b, --brief", "Brief format").option("--status <status>", "Filter by status (todo, in_progress, done)").option("--
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
}
|
|
4165
|
-
);
|
|
403
|
+
task.command("list").description("List tasks").option("-b, --brief", "Brief format").option("--status <status>", "Filter by status (todo, in_progress, done)").option("--repo <name-or-id>", "Filter by repo id").option("--ticket <id>", "Filter by ticket ID").option("--blocked", "Show only blocked tasks").action(async (opts) => {
|
|
404
|
+
const args = [];
|
|
405
|
+
if (opts.brief) args.push("-b");
|
|
406
|
+
if (opts.status) args.push("--status", opts.status);
|
|
407
|
+
if (opts.repo) args.push("--repo", opts.repo);
|
|
408
|
+
if (opts.ticket) args.push("--ticket", opts.ticket);
|
|
409
|
+
if (opts.blocked) args.push("--blocked");
|
|
410
|
+
await taskListCommand(args);
|
|
411
|
+
});
|
|
4166
412
|
task.command("show [id]").description("Show task details").action(async (id) => {
|
|
4167
413
|
await taskShowCommand(id ? [id] : []);
|
|
4168
414
|
});
|
|
@@ -4188,31 +434,27 @@ Examples:
|
|
|
4188
434
|
});
|
|
4189
435
|
}
|
|
4190
436
|
|
|
4191
|
-
// src/commands/ticket/
|
|
437
|
+
// src/integration/cli/commands/ticket/register.ts
|
|
4192
438
|
function registerTicketCommands(program2) {
|
|
4193
439
|
const ticket = program2.command("ticket").description("Manage tickets");
|
|
4194
440
|
ticket.addHelpText(
|
|
4195
441
|
"after",
|
|
4196
442
|
`
|
|
4197
443
|
Examples:
|
|
4198
|
-
$ ralphctl ticket add --
|
|
444
|
+
$ ralphctl ticket add --title "Fix auth bug"
|
|
4199
445
|
$ ralphctl ticket edit abc123 --title "New title"
|
|
4200
446
|
$ ralphctl ticket list -b
|
|
4201
447
|
$ ralphctl ticket show abc123
|
|
4202
448
|
`
|
|
4203
449
|
);
|
|
4204
|
-
ticket.command("add").description("Add ticket to current sprint
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
interactive: opts.interactive !== false
|
|
4213
|
-
});
|
|
4214
|
-
}
|
|
4215
|
-
);
|
|
450
|
+
ticket.command("add").description("Add ticket to current sprint (project inherited from sprint)").option("-t, --title <title>", "Ticket title").option("-d, --description <desc>", "Description").option("--link <url>", "Link to external issue").option("-n, --no-interactive", "Non-interactive mode (error on missing params)").action(async (opts) => {
|
|
451
|
+
await ticketAddCommand({
|
|
452
|
+
title: opts.title,
|
|
453
|
+
description: opts.description,
|
|
454
|
+
link: opts.link,
|
|
455
|
+
interactive: opts.interactive !== false
|
|
456
|
+
});
|
|
457
|
+
});
|
|
4216
458
|
ticket.command("edit [id]").description("Edit an existing ticket").option("--title <title>", "New title").option("--description <desc>", "New description").option("--link <url>", "New link").option("-n, --no-interactive", "Non-interactive mode").action(
|
|
4217
459
|
async (id, opts) => {
|
|
4218
460
|
await ticketEditCommand(id, {
|
|
@@ -4223,10 +465,9 @@ Examples:
|
|
|
4223
465
|
});
|
|
4224
466
|
}
|
|
4225
467
|
);
|
|
4226
|
-
ticket.command("list").description("List tickets").option("-b, --brief", "Brief one-liner format").option("--
|
|
468
|
+
ticket.command("list").description("List tickets").option("-b, --brief", "Brief one-liner format").option("--status <status>", "Filter by requirement status (pending, approved)").action(async (opts) => {
|
|
4227
469
|
const args = [];
|
|
4228
470
|
if (opts.brief) args.push("-b");
|
|
4229
|
-
if (opts.project) args.push("--project", opts.project);
|
|
4230
471
|
if (opts.status) args.push("--status", opts.status);
|
|
4231
472
|
await ticketListCommand(args);
|
|
4232
473
|
});
|
|
@@ -4244,7 +485,7 @@ Examples:
|
|
|
4244
485
|
});
|
|
4245
486
|
}
|
|
4246
487
|
|
|
4247
|
-
// src/commands/progress/
|
|
488
|
+
// src/integration/cli/commands/progress/register.ts
|
|
4248
489
|
function registerProgressCommands(program2) {
|
|
4249
490
|
const progress = program2.command("progress").description("Log and view progress");
|
|
4250
491
|
progress.addHelpText(
|
|
@@ -4261,17 +502,17 @@ Examples:
|
|
|
4261
502
|
progress.command("show").description("Display progress log").action(progressShowCommand);
|
|
4262
503
|
}
|
|
4263
504
|
|
|
4264
|
-
// src/commands/dashboard/dashboard.ts
|
|
505
|
+
// src/integration/cli/commands/dashboard/dashboard.ts
|
|
4265
506
|
async function dashboardCommand() {
|
|
4266
507
|
await showDashboard();
|
|
4267
508
|
}
|
|
4268
509
|
|
|
4269
|
-
// src/commands/dashboard/
|
|
510
|
+
// src/integration/cli/commands/dashboard/register.ts
|
|
4270
511
|
function registerDashboardCommands(program2) {
|
|
4271
512
|
program2.command("status").description("Show current sprint overview").action(dashboardCommand);
|
|
4272
513
|
}
|
|
4273
514
|
|
|
4274
|
-
// src/commands/config/
|
|
515
|
+
// src/integration/cli/commands/config/register.ts
|
|
4275
516
|
function registerConfigCommands(program2) {
|
|
4276
517
|
const config = program2.command("config").description("Manage configuration");
|
|
4277
518
|
config.addHelpText(
|
|
@@ -4294,7 +535,7 @@ Examples:
|
|
|
4294
535
|
});
|
|
4295
536
|
}
|
|
4296
537
|
|
|
4297
|
-
// src/commands/completion/
|
|
538
|
+
// src/integration/cli/commands/completion/register.ts
|
|
4298
539
|
function registerCompletionCommands(program2) {
|
|
4299
540
|
const completion = program2.command("completion").description("Manage shell tab-completion");
|
|
4300
541
|
completion.addHelpText(
|
|
@@ -4317,7 +558,7 @@ Examples:
|
|
|
4317
558
|
});
|
|
4318
559
|
}
|
|
4319
560
|
|
|
4320
|
-
// src/commands/doctor/
|
|
561
|
+
// src/integration/cli/commands/doctor/register.ts
|
|
4321
562
|
function registerDoctorCommands(program2) {
|
|
4322
563
|
program2.command("doctor").description("Check environment health and diagnose setup issues").addHelpText(
|
|
4323
564
|
"after",
|
|
@@ -4337,106 +578,8 @@ Checks performed:
|
|
|
4337
578
|
});
|
|
4338
579
|
}
|
|
4339
580
|
|
|
4340
|
-
//
|
|
4341
|
-
|
|
4342
|
-
name: "ralphctl",
|
|
4343
|
-
version: "0.2.5",
|
|
4344
|
-
description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
|
|
4345
|
-
homepage: "https://github.com/lukas-grigis/ralphctl",
|
|
4346
|
-
type: "module",
|
|
4347
|
-
license: "MIT",
|
|
4348
|
-
author: "Lukas Grigis",
|
|
4349
|
-
repository: {
|
|
4350
|
-
type: "git",
|
|
4351
|
-
url: "https://github.com/lukas-grigis/ralphctl.git"
|
|
4352
|
-
},
|
|
4353
|
-
bugs: {
|
|
4354
|
-
url: "https://github.com/lukas-grigis/ralphctl/issues"
|
|
4355
|
-
},
|
|
4356
|
-
keywords: [
|
|
4357
|
-
"cli",
|
|
4358
|
-
"agent-harness",
|
|
4359
|
-
"claude-code",
|
|
4360
|
-
"github-copilot",
|
|
4361
|
-
"ai-coding",
|
|
4362
|
-
"task-orchestration",
|
|
4363
|
-
"anthropic",
|
|
4364
|
-
"developer-tools",
|
|
4365
|
-
"long-running-agents",
|
|
4366
|
-
"generator-evaluator"
|
|
4367
|
-
],
|
|
4368
|
-
bin: {
|
|
4369
|
-
ralphctl: "./dist/cli.mjs"
|
|
4370
|
-
},
|
|
4371
|
-
files: [
|
|
4372
|
-
"dist/",
|
|
4373
|
-
"schemas/"
|
|
4374
|
-
],
|
|
4375
|
-
publishConfig: {
|
|
4376
|
-
access: "public"
|
|
4377
|
-
},
|
|
4378
|
-
scripts: {
|
|
4379
|
-
build: "tsup && mkdir -p dist/prompts && cp src/ai/prompts/*.md dist/prompts/",
|
|
4380
|
-
prepublishOnly: "pnpm build",
|
|
4381
|
-
dev: "tsx src/cli.ts",
|
|
4382
|
-
lint: "eslint .",
|
|
4383
|
-
"lint:fix": "eslint . --fix",
|
|
4384
|
-
format: "prettier --write .",
|
|
4385
|
-
"format:check": "prettier --check .",
|
|
4386
|
-
typecheck: "tsc --noEmit",
|
|
4387
|
-
test: "vitest run",
|
|
4388
|
-
"test:watch": "vitest",
|
|
4389
|
-
"test:coverage": "vitest run --coverage",
|
|
4390
|
-
prepare: "husky"
|
|
4391
|
-
},
|
|
4392
|
-
packageManager: "pnpm@10.29.3",
|
|
4393
|
-
engines: {
|
|
4394
|
-
node: ">=24.0.0"
|
|
4395
|
-
},
|
|
4396
|
-
dependencies: {
|
|
4397
|
-
"@inquirer/prompts": "^8.3.2",
|
|
4398
|
-
colorette: "^2.0.20",
|
|
4399
|
-
commander: "^14.0.3",
|
|
4400
|
-
"gradient-string": "^3.0.0",
|
|
4401
|
-
ora: "^9.3.0",
|
|
4402
|
-
tabtab: "^3.0.2",
|
|
4403
|
-
"typescript-result": "^3.5.2",
|
|
4404
|
-
zod: "^4.3.6"
|
|
4405
|
-
},
|
|
4406
|
-
devDependencies: {
|
|
4407
|
-
"@eslint/js": "^10.0.1",
|
|
4408
|
-
"@types/node": "^25.5.2",
|
|
4409
|
-
"@types/tabtab": "^3.0.4",
|
|
4410
|
-
"@vitest/coverage-v8": "^4.1.2",
|
|
4411
|
-
eslint: "^10.2.0",
|
|
4412
|
-
"eslint-config-prettier": "^10.1.8",
|
|
4413
|
-
globals: "^17.4.0",
|
|
4414
|
-
husky: "^9.1.7",
|
|
4415
|
-
"lint-staged": "^16.4.0",
|
|
4416
|
-
prettier: "^3.8.1",
|
|
4417
|
-
tsup: "^8.5.1",
|
|
4418
|
-
tsx: "^4.21.0",
|
|
4419
|
-
typescript: "^5.9.3",
|
|
4420
|
-
"typescript-eslint": "^8.58.0",
|
|
4421
|
-
vitest: "^4.1.2"
|
|
4422
|
-
},
|
|
4423
|
-
"lint-staged": {
|
|
4424
|
-
"*.ts": [
|
|
4425
|
-
"eslint --cache --fix",
|
|
4426
|
-
"prettier --write"
|
|
4427
|
-
],
|
|
4428
|
-
"*.{md,json,yml,yaml}": "prettier --write"
|
|
4429
|
-
}
|
|
4430
|
-
};
|
|
4431
|
-
|
|
4432
|
-
// src/cli-metadata.ts
|
|
4433
|
-
var cliMetadata = {
|
|
4434
|
-
name: "ralphctl",
|
|
4435
|
-
version: package_default.version,
|
|
4436
|
-
description: "I'm helping! Plan sprints and execute tasks with AI"
|
|
4437
|
-
};
|
|
4438
|
-
|
|
4439
|
-
// src/cli.ts
|
|
581
|
+
// src/application/entrypoint.ts
|
|
582
|
+
setSharedDeps(createSharedDeps());
|
|
4440
583
|
var program = new Command();
|
|
4441
584
|
program.name(cliMetadata.name).description(cliMetadata.description).version(cliMetadata.version).addHelpText(
|
|
4442
585
|
"after",
|
|
@@ -4462,15 +605,47 @@ registerCompletionCommands(program);
|
|
|
4462
605
|
registerDoctorCommands(program);
|
|
4463
606
|
async function main() {
|
|
4464
607
|
if (process.env["COMP_CWORD"] && process.env["COMP_POINT"] && process.env["COMP_LINE"]) {
|
|
4465
|
-
const { handleCompletionRequest } = await import("./handle-
|
|
608
|
+
const { handleCompletionRequest } = await import("./handle-BBAZJ44Y.mjs");
|
|
4466
609
|
if (await handleCompletionRequest(program)) return;
|
|
4467
610
|
}
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
await
|
|
611
|
+
const argv = process.argv;
|
|
612
|
+
const isBare = argv.length <= 2;
|
|
613
|
+
const isInteractive = argv[2] === "interactive";
|
|
614
|
+
if (isBare || isInteractive) {
|
|
615
|
+
const { mountInkApp } = await import("./mount-U7QXVB5Q.mjs");
|
|
616
|
+
const { fallback } = await mountInkApp({ initialView: "repl" });
|
|
617
|
+
if (!fallback) return;
|
|
618
|
+
printBanner();
|
|
619
|
+
console.log("");
|
|
620
|
+
console.log("Interactive mode requires a TTY. Available commands:");
|
|
621
|
+
console.log("");
|
|
622
|
+
program.outputHelp();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (argv[2] === "sprint" && argv[3] === "start") {
|
|
626
|
+
const { parseSprintStartArgs } = await import("./start-WG7VMEB2.mjs");
|
|
627
|
+
const parsed = parseSprintStartArgs(argv.slice(4));
|
|
628
|
+
if (parsed.ok) {
|
|
629
|
+
const { mountInkApp } = await import("./mount-U7QXVB5Q.mjs");
|
|
630
|
+
const { getSharedDeps } = await import("./bootstrap-FMHG6DRY.mjs");
|
|
631
|
+
let sprintId;
|
|
632
|
+
try {
|
|
633
|
+
sprintId = await getSharedDeps().persistence.resolveSprintId(parsed.value.sprintId);
|
|
634
|
+
} catch {
|
|
635
|
+
sprintId = void 0;
|
|
636
|
+
}
|
|
637
|
+
if (sprintId) {
|
|
638
|
+
const { fallback } = await mountInkApp({
|
|
639
|
+
initialView: "execute",
|
|
640
|
+
sprintId,
|
|
641
|
+
executionOptions: parsed.value.options
|
|
642
|
+
});
|
|
643
|
+
if (!fallback) return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
4473
646
|
}
|
|
647
|
+
printBanner();
|
|
648
|
+
await program.parseAsync(argv);
|
|
4474
649
|
}
|
|
4475
650
|
main().catch((err) => {
|
|
4476
651
|
if (err instanceof DomainError) {
|