ralphctl 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -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
+ };