ralphctl 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
EXIT_ERROR,
|
|
4
|
+
exitWithCode
|
|
5
|
+
} from "./chunk-7TG3EAQ2.mjs";
|
|
6
|
+
import {
|
|
7
|
+
ProjectNotFoundError,
|
|
8
|
+
getProject,
|
|
9
|
+
listProjects,
|
|
10
|
+
projectExists
|
|
11
|
+
} from "./chunk-WGHJI3OI.mjs";
|
|
12
|
+
import {
|
|
13
|
+
SprintStatusError,
|
|
14
|
+
assertSprintStatus,
|
|
15
|
+
generateUuid8,
|
|
16
|
+
getEditor,
|
|
17
|
+
resolveSprintId,
|
|
18
|
+
setEditor
|
|
19
|
+
} from "./chunk-EKMZZRWI.mjs";
|
|
20
|
+
import {
|
|
21
|
+
SprintSchema,
|
|
22
|
+
getSprintFilePath,
|
|
23
|
+
readValidatedJson,
|
|
24
|
+
writeValidatedJson
|
|
25
|
+
} from "./chunk-6PYTKGB5.mjs";
|
|
26
|
+
import {
|
|
27
|
+
createSpinner,
|
|
28
|
+
emoji,
|
|
29
|
+
error,
|
|
30
|
+
field,
|
|
31
|
+
fieldMultiline,
|
|
32
|
+
icons,
|
|
33
|
+
log,
|
|
34
|
+
muted,
|
|
35
|
+
renderCard,
|
|
36
|
+
showEmpty,
|
|
37
|
+
showError,
|
|
38
|
+
showSuccess,
|
|
39
|
+
showWarning
|
|
40
|
+
} from "./chunk-QBXHAXHI.mjs";
|
|
41
|
+
|
|
42
|
+
// src/commands/ticket/add.ts
|
|
43
|
+
import { confirm, input, select as select2 } from "@inquirer/prompts";
|
|
44
|
+
|
|
45
|
+
// src/utils/editor-input.ts
|
|
46
|
+
import { editor } from "@inquirer/prompts";
|
|
47
|
+
|
|
48
|
+
// src/utils/editor.ts
|
|
49
|
+
import { select } from "@inquirer/prompts";
|
|
50
|
+
async function resolveEditor() {
|
|
51
|
+
const stored = await getEditor();
|
|
52
|
+
if (stored) return stored;
|
|
53
|
+
const choice = await select({
|
|
54
|
+
message: `${emoji.donut} Which editor should open for multiline input?`,
|
|
55
|
+
choices: [
|
|
56
|
+
{ name: "Sublime Text", value: "subl -w" },
|
|
57
|
+
{ name: "VS Code", value: "code --wait" },
|
|
58
|
+
{ name: "Vim", value: "vim" },
|
|
59
|
+
{ name: "Nano", value: "nano" },
|
|
60
|
+
{ name: "Use $EDITOR env var", value: "__env__" }
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
if (choice === "__env__") {
|
|
64
|
+
const envEditor = process.env["VISUAL"] ?? process.env["EDITOR"];
|
|
65
|
+
if (!envEditor) {
|
|
66
|
+
await setEditor("vim");
|
|
67
|
+
return "vim";
|
|
68
|
+
}
|
|
69
|
+
await setEditor(envEditor);
|
|
70
|
+
return envEditor;
|
|
71
|
+
}
|
|
72
|
+
await setEditor(choice);
|
|
73
|
+
return choice;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/utils/editor-input.ts
|
|
77
|
+
async function editorInput(options) {
|
|
78
|
+
if (!process.stdin.isTTY) {
|
|
79
|
+
const { multilineInput } = await import("./multiline-OHSNFCRG.mjs");
|
|
80
|
+
return multilineInput({ message: options.message, default: options.default });
|
|
81
|
+
}
|
|
82
|
+
const editorCmd = await resolveEditor();
|
|
83
|
+
const prevVisual = process.env["VISUAL"];
|
|
84
|
+
process.env["VISUAL"] = editorCmd;
|
|
85
|
+
try {
|
|
86
|
+
const result = await editor({
|
|
87
|
+
message: options.message,
|
|
88
|
+
default: options.default,
|
|
89
|
+
postfix: ".md"
|
|
90
|
+
});
|
|
91
|
+
return result.trim();
|
|
92
|
+
} finally {
|
|
93
|
+
if (prevVisual === void 0) delete process.env["VISUAL"];
|
|
94
|
+
else process.env["VISUAL"] = prevVisual;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/store/ticket.ts
|
|
99
|
+
var TicketNotFoundError = class extends Error {
|
|
100
|
+
ticketId;
|
|
101
|
+
constructor(ticketId) {
|
|
102
|
+
super(`Ticket not found: ${ticketId}`);
|
|
103
|
+
this.name = "TicketNotFoundError";
|
|
104
|
+
this.ticketId = ticketId;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
async function getSprintData(sprintId) {
|
|
108
|
+
const id = await resolveSprintId(sprintId);
|
|
109
|
+
return readValidatedJson(getSprintFilePath(id), SprintSchema);
|
|
110
|
+
}
|
|
111
|
+
async function saveSprintData(sprint) {
|
|
112
|
+
await writeValidatedJson(getSprintFilePath(sprint.id), sprint, SprintSchema);
|
|
113
|
+
}
|
|
114
|
+
async function addTicket(input2, sprintId) {
|
|
115
|
+
const sprint = await getSprintData(sprintId);
|
|
116
|
+
assertSprintStatus(sprint, ["draft"], "add tickets");
|
|
117
|
+
try {
|
|
118
|
+
await getProject(input2.projectName);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err instanceof ProjectNotFoundError) {
|
|
121
|
+
throw new Error(`Project '${input2.projectName}' does not exist. Add it first with 'ralphctl project add'.`, {
|
|
122
|
+
cause: err
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
const ticket = {
|
|
128
|
+
id: generateUuid8(),
|
|
129
|
+
title: input2.title,
|
|
130
|
+
description: input2.description,
|
|
131
|
+
link: input2.link,
|
|
132
|
+
projectName: input2.projectName,
|
|
133
|
+
requirementStatus: "pending"
|
|
134
|
+
};
|
|
135
|
+
sprint.tickets.push(ticket);
|
|
136
|
+
await saveSprintData(sprint);
|
|
137
|
+
return ticket;
|
|
138
|
+
}
|
|
139
|
+
async function updateTicket(ticketId, updates, sprintId) {
|
|
140
|
+
const sprint = await getSprintData(sprintId);
|
|
141
|
+
assertSprintStatus(sprint, ["draft"], "update tickets");
|
|
142
|
+
const ticketIdx = sprint.tickets.findIndex((t) => t.id === ticketId);
|
|
143
|
+
if (ticketIdx === -1) {
|
|
144
|
+
throw new TicketNotFoundError(ticketId);
|
|
145
|
+
}
|
|
146
|
+
const ticket = sprint.tickets[ticketIdx];
|
|
147
|
+
if (!ticket) {
|
|
148
|
+
throw new TicketNotFoundError(ticketId);
|
|
149
|
+
}
|
|
150
|
+
if (updates.title !== void 0) {
|
|
151
|
+
ticket.title = updates.title;
|
|
152
|
+
}
|
|
153
|
+
if (updates.description !== void 0) {
|
|
154
|
+
ticket.description = updates.description || void 0;
|
|
155
|
+
}
|
|
156
|
+
if (updates.link !== void 0) {
|
|
157
|
+
ticket.link = updates.link || void 0;
|
|
158
|
+
}
|
|
159
|
+
await saveSprintData(sprint);
|
|
160
|
+
return ticket;
|
|
161
|
+
}
|
|
162
|
+
async function removeTicket(ticketId, sprintId) {
|
|
163
|
+
const sprint = await getSprintData(sprintId);
|
|
164
|
+
assertSprintStatus(sprint, ["draft"], "remove tickets");
|
|
165
|
+
const index = sprint.tickets.findIndex((t) => t.id === ticketId);
|
|
166
|
+
if (index === -1) {
|
|
167
|
+
throw new TicketNotFoundError(ticketId);
|
|
168
|
+
}
|
|
169
|
+
sprint.tickets.splice(index, 1);
|
|
170
|
+
await saveSprintData(sprint);
|
|
171
|
+
}
|
|
172
|
+
async function listTickets(sprintId) {
|
|
173
|
+
const sprint = await getSprintData(sprintId);
|
|
174
|
+
return sprint.tickets;
|
|
175
|
+
}
|
|
176
|
+
async function getTicket(ticketId, sprintId) {
|
|
177
|
+
const sprint = await getSprintData(sprintId);
|
|
178
|
+
const ticket = sprint.tickets.find((t) => t.id === ticketId);
|
|
179
|
+
if (!ticket) {
|
|
180
|
+
throw new TicketNotFoundError(ticketId);
|
|
181
|
+
}
|
|
182
|
+
return ticket;
|
|
183
|
+
}
|
|
184
|
+
function groupTicketsByProject(tickets) {
|
|
185
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
186
|
+
for (const ticket of tickets) {
|
|
187
|
+
const existing = grouped.get(ticket.projectName) ?? [];
|
|
188
|
+
existing.push(ticket);
|
|
189
|
+
grouped.set(ticket.projectName, existing);
|
|
190
|
+
}
|
|
191
|
+
return grouped;
|
|
192
|
+
}
|
|
193
|
+
function allRequirementsApproved(tickets) {
|
|
194
|
+
return tickets.length > 0 && tickets.every((t) => t.requirementStatus === "approved");
|
|
195
|
+
}
|
|
196
|
+
function getPendingRequirements(tickets) {
|
|
197
|
+
return tickets.filter((t) => t.requirementStatus === "pending");
|
|
198
|
+
}
|
|
199
|
+
function formatTicketDisplay(ticket) {
|
|
200
|
+
return `[${ticket.id}] ${ticket.title}`;
|
|
201
|
+
}
|
|
202
|
+
function formatTicketId(ticket) {
|
|
203
|
+
return ticket.id;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/utils/issue-fetch.ts
|
|
207
|
+
import { spawnSync } from "child_process";
|
|
208
|
+
var MAX_COMMENTS = 20;
|
|
209
|
+
function parseIssueUrl(url) {
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = new URL(url);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
220
|
+
if (parsed.hostname === "github.com") {
|
|
221
|
+
const owner = segments[0];
|
|
222
|
+
const repo = segments[1];
|
|
223
|
+
if (segments.length >= 4 && segments[2] === "issues" && owner && repo) {
|
|
224
|
+
const num = Number(segments[3]);
|
|
225
|
+
if (Number.isInteger(num) && num > 0) {
|
|
226
|
+
return { host: "github", hostname: parsed.hostname, owner, repo, number: num };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const dashIdx = segments.indexOf("-");
|
|
232
|
+
if (dashIdx >= 2 && segments[dashIdx + 1] === "issues") {
|
|
233
|
+
const num = Number(segments[dashIdx + 2]);
|
|
234
|
+
if (Number.isInteger(num) && num > 0) {
|
|
235
|
+
const repo = segments[dashIdx - 1];
|
|
236
|
+
if (repo) {
|
|
237
|
+
const owner = segments.slice(0, dashIdx - 1).join("/");
|
|
238
|
+
return { host: "gitlab", hostname: parsed.hostname, owner, repo, number: num };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function fetchGitHubIssue(parsed) {
|
|
245
|
+
const result = spawnSync(
|
|
246
|
+
"gh",
|
|
247
|
+
[
|
|
248
|
+
"issue",
|
|
249
|
+
"view",
|
|
250
|
+
String(parsed.number),
|
|
251
|
+
"--repo",
|
|
252
|
+
`${parsed.owner}/${parsed.repo}`,
|
|
253
|
+
"--json",
|
|
254
|
+
"title,body,comments"
|
|
255
|
+
],
|
|
256
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 3e4 }
|
|
257
|
+
);
|
|
258
|
+
if (result.status !== 0) {
|
|
259
|
+
const stderr = result.stderr.trim();
|
|
260
|
+
throw new IssueFetchError(`gh issue view failed: ${stderr || "unknown error"}`);
|
|
261
|
+
}
|
|
262
|
+
const data = JSON.parse(result.stdout);
|
|
263
|
+
const comments = (data.comments ?? []).slice(-MAX_COMMENTS).map((c) => ({
|
|
264
|
+
author: c.author?.login ?? "unknown",
|
|
265
|
+
createdAt: c.createdAt ?? "",
|
|
266
|
+
body: c.body ?? ""
|
|
267
|
+
}));
|
|
268
|
+
return {
|
|
269
|
+
title: data.title ?? "",
|
|
270
|
+
body: data.body ?? "",
|
|
271
|
+
comments,
|
|
272
|
+
url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/issues/${String(parsed.number)}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function fetchGitLabIssue(parsed) {
|
|
276
|
+
const result = spawnSync(
|
|
277
|
+
"glab",
|
|
278
|
+
["issue", "view", String(parsed.number), "--repo", `${parsed.owner}/${parsed.repo}`, "--output", "json"],
|
|
279
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 3e4 }
|
|
280
|
+
);
|
|
281
|
+
if (result.status !== 0) {
|
|
282
|
+
const stderr = result.stderr.trim();
|
|
283
|
+
throw new IssueFetchError(`glab issue view failed: ${stderr || "unknown error"}`);
|
|
284
|
+
}
|
|
285
|
+
const data = JSON.parse(result.stdout);
|
|
286
|
+
const notesResult = spawnSync(
|
|
287
|
+
"glab",
|
|
288
|
+
["issue", "note", "list", String(parsed.number), "--repo", `${parsed.owner}/${parsed.repo}`, "--output", "json"],
|
|
289
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 3e4 }
|
|
290
|
+
);
|
|
291
|
+
let comments = [];
|
|
292
|
+
if (notesResult.status === 0 && notesResult.stdout.trim()) {
|
|
293
|
+
try {
|
|
294
|
+
const notes = JSON.parse(notesResult.stdout);
|
|
295
|
+
comments = notes.slice(-MAX_COMMENTS).map((n) => ({
|
|
296
|
+
author: n.author?.username ?? "unknown",
|
|
297
|
+
createdAt: n.created_at ?? "",
|
|
298
|
+
body: n.body ?? ""
|
|
299
|
+
}));
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
title: data.title ?? "",
|
|
305
|
+
body: data.description ?? "",
|
|
306
|
+
comments,
|
|
307
|
+
url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/-/issues/${String(parsed.number)}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function fetchIssue(parsed) {
|
|
311
|
+
if (parsed.host === "github") {
|
|
312
|
+
return fetchGitHubIssue(parsed);
|
|
313
|
+
}
|
|
314
|
+
return fetchGitLabIssue(parsed);
|
|
315
|
+
}
|
|
316
|
+
function fetchIssueFromUrl(url) {
|
|
317
|
+
const parsed = parseIssueUrl(url);
|
|
318
|
+
if (!parsed) return null;
|
|
319
|
+
return fetchIssue(parsed);
|
|
320
|
+
}
|
|
321
|
+
function formatIssueContext(data) {
|
|
322
|
+
const lines = [];
|
|
323
|
+
lines.push("## Source Issue Data");
|
|
324
|
+
lines.push("");
|
|
325
|
+
lines.push(`> Fetched live from ${data.url}`);
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push(`**Title:** ${data.title}`);
|
|
328
|
+
lines.push("");
|
|
329
|
+
if (data.body) {
|
|
330
|
+
lines.push("**Body:**");
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push(data.body);
|
|
333
|
+
lines.push("");
|
|
334
|
+
}
|
|
335
|
+
if (data.comments.length > 0) {
|
|
336
|
+
lines.push(`**Comments (${String(data.comments.length)}):**`);
|
|
337
|
+
lines.push("");
|
|
338
|
+
for (const comment of data.comments) {
|
|
339
|
+
const timestamp = comment.createdAt ? ` (${comment.createdAt})` : "";
|
|
340
|
+
lines.push(`---`);
|
|
341
|
+
lines.push(`**@${comment.author}**${timestamp}:`);
|
|
342
|
+
lines.push("");
|
|
343
|
+
lines.push(comment.body);
|
|
344
|
+
lines.push("");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
var IssueFetchError = class extends Error {
|
|
350
|
+
constructor(message) {
|
|
351
|
+
super(message);
|
|
352
|
+
this.name = "IssueFetchError";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/commands/ticket/add.ts
|
|
357
|
+
function tryFetchIssue(url) {
|
|
358
|
+
const spinner = createSpinner("Fetching issue data...");
|
|
359
|
+
spinner.start();
|
|
360
|
+
let data;
|
|
361
|
+
try {
|
|
362
|
+
data = fetchIssueFromUrl(url);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
spinner.fail("Could not fetch issue data");
|
|
365
|
+
if (err instanceof IssueFetchError) {
|
|
366
|
+
showWarning(err.message);
|
|
367
|
+
} else if (err instanceof Error) {
|
|
368
|
+
showWarning(err.message);
|
|
369
|
+
}
|
|
370
|
+
log.newline();
|
|
371
|
+
return void 0;
|
|
372
|
+
}
|
|
373
|
+
if (!data) {
|
|
374
|
+
spinner.stop();
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
spinner.succeed("Issue data fetched");
|
|
378
|
+
log.newline();
|
|
379
|
+
const bodyPreview = data.body.length > 200 ? data.body.slice(0, 200) + "..." : data.body;
|
|
380
|
+
const cardLines = [`Title: ${data.title}`, "", bodyPreview];
|
|
381
|
+
if (data.comments.length > 0) {
|
|
382
|
+
cardLines.push("", `${String(data.comments.length)} comment(s)`);
|
|
383
|
+
}
|
|
384
|
+
console.log(renderCard(`${icons.info} Fetched Issue`, cardLines));
|
|
385
|
+
log.newline();
|
|
386
|
+
return data;
|
|
387
|
+
}
|
|
388
|
+
function validateUrl(url) {
|
|
389
|
+
try {
|
|
390
|
+
new URL(url);
|
|
391
|
+
return true;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function addSingleTicketNonInteractive(options) {
|
|
397
|
+
const errors = [];
|
|
398
|
+
const trimmedTitle = options.title?.trim();
|
|
399
|
+
const trimmedProject = options.project?.trim();
|
|
400
|
+
if (!trimmedTitle) {
|
|
401
|
+
errors.push("--title is required");
|
|
402
|
+
}
|
|
403
|
+
if (!trimmedProject) {
|
|
404
|
+
errors.push("--project is required");
|
|
405
|
+
} else if (!await projectExists(trimmedProject)) {
|
|
406
|
+
errors.push(`Project '${trimmedProject}' does not exist. Add it first with 'ralphctl project add'.`);
|
|
407
|
+
}
|
|
408
|
+
if (options.link && !validateUrl(options.link)) {
|
|
409
|
+
errors.push("--link must be a valid URL");
|
|
410
|
+
}
|
|
411
|
+
if (errors.length > 0 || !trimmedTitle || !trimmedProject) {
|
|
412
|
+
showError("Validation failed");
|
|
413
|
+
for (const e of errors) {
|
|
414
|
+
log.item(error(e));
|
|
415
|
+
}
|
|
416
|
+
log.newline();
|
|
417
|
+
exitWithCode(EXIT_ERROR);
|
|
418
|
+
}
|
|
419
|
+
const title = trimmedTitle;
|
|
420
|
+
const trimmedDesc = options.description?.trim();
|
|
421
|
+
const description = trimmedDesc === "" ? void 0 : trimmedDesc;
|
|
422
|
+
const trimmedLink = options.link?.trim();
|
|
423
|
+
const link = trimmedLink === "" ? void 0 : trimmedLink;
|
|
424
|
+
const projectName = trimmedProject;
|
|
425
|
+
try {
|
|
426
|
+
const ticket = await addTicket({
|
|
427
|
+
title,
|
|
428
|
+
description,
|
|
429
|
+
link,
|
|
430
|
+
projectName
|
|
431
|
+
});
|
|
432
|
+
showTicketResult(ticket);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
handleTicketError(err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function addSingleTicketInteractive(options) {
|
|
438
|
+
const projects = await listProjects();
|
|
439
|
+
if (projects.length === 0) {
|
|
440
|
+
showEmpty("projects", "Add one first with: ralphctl project add");
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
const projectName = await select2({
|
|
444
|
+
message: `${icons.project} Project:`,
|
|
445
|
+
default: options.project ?? projects[0]?.name,
|
|
446
|
+
choices: projects.map((p) => ({
|
|
447
|
+
name: `${icons.project} ${p.name} ${muted(`- ${p.displayName}`)}`,
|
|
448
|
+
value: p.name
|
|
449
|
+
}))
|
|
450
|
+
});
|
|
451
|
+
const link = await input({
|
|
452
|
+
message: `${icons.info} Issue link (optional):`,
|
|
453
|
+
default: options.link?.trim(),
|
|
454
|
+
validate: (v) => {
|
|
455
|
+
if (!v) return true;
|
|
456
|
+
return validateUrl(v) ? true : "Invalid URL format";
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const trimmedLink = link.trim();
|
|
460
|
+
const normalizedLink = trimmedLink === "" ? void 0 : trimmedLink;
|
|
461
|
+
let prefill;
|
|
462
|
+
if (normalizedLink) {
|
|
463
|
+
prefill = tryFetchIssue(normalizedLink);
|
|
464
|
+
}
|
|
465
|
+
let title = await input({
|
|
466
|
+
message: `${icons.ticket} Title:`,
|
|
467
|
+
default: prefill?.title ?? options.title?.trim(),
|
|
468
|
+
validate: (v) => v.trim().length > 0 ? true : "Title is required"
|
|
469
|
+
});
|
|
470
|
+
const description = await editorInput({
|
|
471
|
+
message: "Description (recommended):",
|
|
472
|
+
default: prefill?.body ?? options.description?.trim()
|
|
473
|
+
});
|
|
474
|
+
title = title.trim();
|
|
475
|
+
const trimmedDescription = description.trim();
|
|
476
|
+
const normalizedDescription = trimmedDescription === "" ? void 0 : trimmedDescription;
|
|
477
|
+
try {
|
|
478
|
+
const ticket = await addTicket({
|
|
479
|
+
title,
|
|
480
|
+
description: normalizedDescription,
|
|
481
|
+
link: normalizedLink,
|
|
482
|
+
projectName
|
|
483
|
+
});
|
|
484
|
+
showTicketResult(ticket);
|
|
485
|
+
return ticket;
|
|
486
|
+
} catch (err) {
|
|
487
|
+
handleTicketError(err);
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function showTicketResult(ticket) {
|
|
492
|
+
showSuccess("Ticket added!", [
|
|
493
|
+
["ID", ticket.id],
|
|
494
|
+
["Title", ticket.title],
|
|
495
|
+
["Project", ticket.projectName]
|
|
496
|
+
]);
|
|
497
|
+
if (ticket.description) {
|
|
498
|
+
console.log(fieldMultiline("Description", ticket.description));
|
|
499
|
+
}
|
|
500
|
+
if (ticket.link) {
|
|
501
|
+
console.log(field("Link", ticket.link));
|
|
502
|
+
}
|
|
503
|
+
console.log("");
|
|
504
|
+
}
|
|
505
|
+
function handleTicketError(err) {
|
|
506
|
+
if (err instanceof SprintStatusError) {
|
|
507
|
+
showError(err.message);
|
|
508
|
+
} else if (err instanceof Error && err.message.includes("does not exist")) {
|
|
509
|
+
showError(err.message);
|
|
510
|
+
} else {
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function ticketAddCommand(options = {}) {
|
|
515
|
+
if (options.interactive === false) {
|
|
516
|
+
await addSingleTicketNonInteractive(options);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
let count = 0;
|
|
520
|
+
let lastProjectName = options.project;
|
|
521
|
+
while (true) {
|
|
522
|
+
const ticket = await addSingleTicketInteractive({ ...options, project: lastProjectName });
|
|
523
|
+
if (ticket) {
|
|
524
|
+
count++;
|
|
525
|
+
lastProjectName = ticket.projectName;
|
|
526
|
+
log.dim(`${String(count)} ticket(s) added in this session`);
|
|
527
|
+
} else {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
const another = await confirm({
|
|
531
|
+
message: `${emoji.donut} Add another ticket?`,
|
|
532
|
+
default: true
|
|
533
|
+
});
|
|
534
|
+
if (!another) break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export {
|
|
539
|
+
TicketNotFoundError,
|
|
540
|
+
addTicket,
|
|
541
|
+
updateTicket,
|
|
542
|
+
removeTicket,
|
|
543
|
+
listTickets,
|
|
544
|
+
getTicket,
|
|
545
|
+
groupTicketsByProject,
|
|
546
|
+
allRequirementsApproved,
|
|
547
|
+
getPendingRequirements,
|
|
548
|
+
formatTicketDisplay,
|
|
549
|
+
formatTicketId,
|
|
550
|
+
editorInput,
|
|
551
|
+
fetchIssueFromUrl,
|
|
552
|
+
formatIssueContext,
|
|
553
|
+
IssueFetchError,
|
|
554
|
+
addSingleTicketInteractive,
|
|
555
|
+
ticketAddCommand
|
|
556
|
+
};
|