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
@@ -1,403 +0,0 @@
1
- import { Separator } from '@inquirer/prompts';
2
- import { colors } from '@src/theme/index.ts';
3
- import type { NextAction } from './dashboard.ts';
4
-
5
- /**
6
- * Dynamic context-aware menu system for interactive mode
7
- */
8
-
9
- const SEPARATOR_WIDTH = 48;
10
-
11
- /** Create a titled separator: ── LABEL ──────────── */
12
- function titled(label: string): SeparatorInstance {
13
- const lineLen = Math.max(2, SEPARATOR_WIDTH - label.length - 4); // 4 = "── " + " "
14
- return new Separator(colors.muted(`\n── ${label} ${'─'.repeat(lineLen)}`));
15
- }
16
-
17
- /** Plain line separator: ────────────────────────── */
18
- function line(): SeparatorInstance {
19
- return new Separator(colors.muted('─'.repeat(SEPARATOR_WIDTH)));
20
- }
21
-
22
- interface Choice {
23
- name: string;
24
- value: string;
25
- description?: string;
26
- disabled?: string | boolean;
27
- }
28
-
29
- type SeparatorInstance = InstanceType<typeof Separator>;
30
-
31
- export type MenuItem = Choice | SeparatorInstance;
32
-
33
- export interface SubMenu {
34
- title: string;
35
- items: MenuItem[];
36
- }
37
-
38
- /** Sprint/ticket/task counts for menu context */
39
- export interface MenuContext {
40
- hasProjects: boolean;
41
- projectCount: number;
42
- currentSprintId: string | null;
43
- currentSprintName: string | null;
44
- currentSprintStatus: 'draft' | 'active' | 'closed' | null;
45
- ticketCount: number;
46
- taskCount: number;
47
- tasksDone: number;
48
- tasksInProgress: number;
49
- pendingRequirements: number;
50
- allRequirementsApproved: boolean;
51
- /** Number of tickets that have at least one associated task */
52
- plannedTicketCount: number;
53
- nextAction: NextAction | null;
54
- /** Current AI provider setting */
55
- aiProvider: string | null;
56
- }
57
-
58
- // ============================================================================
59
- // WORKFLOW ACTIONS — actions that advance sprint state
60
- // ============================================================================
61
-
62
- const WORKFLOW_ACTIONS: Record<string, Set<string>> = {
63
- sprint: new Set(['create', 'refine', 'ideate', 'plan', 'start', 'close']),
64
- ticket: new Set(['add', 'refine']),
65
- task: new Set(['add', 'import']),
66
- progress: new Set(['log']),
67
- };
68
-
69
- /**
70
- * Check if a command is a workflow action that should return to main menu.
71
- */
72
- export function isWorkflowAction(group: string, subCommand: string): boolean {
73
- return WORKFLOW_ACTIONS[group]?.has(subCommand) ?? false;
74
- }
75
-
76
- /**
77
- * Build PLAN section actions — sprint planning lifecycle.
78
- */
79
- function buildPlanActions(ctx: MenuContext): MenuItem[] {
80
- const items: MenuItem[] = [];
81
- const isDraft = ctx.currentSprintStatus === 'draft';
82
- const hasSprint = ctx.currentSprintId !== null;
83
-
84
- // Create Sprint — always available
85
- items.push({ name: 'Create Sprint', value: 'action:sprint:create', description: 'Start a new sprint' });
86
-
87
- // Add Ticket — requires draft sprint + projects
88
- const addTicketDisabled = !hasSprint
89
- ? 'create a sprint first'
90
- : !isDraft
91
- ? 'need draft sprint'
92
- : !ctx.hasProjects
93
- ? 'add a project first'
94
- : false;
95
- items.push({
96
- name: 'Add Ticket',
97
- value: 'action:ticket:add',
98
- description: 'Add work to current sprint',
99
- disabled: addTicketDisabled,
100
- });
101
-
102
- // Refine Requirements — requires draft sprint + pending tickets
103
- let refineDisabled: string | false = false;
104
- let refineDesc = 'Clarify ticket requirements';
105
- if (!hasSprint) {
106
- refineDisabled = 'create a sprint first';
107
- } else if (!isDraft) {
108
- refineDisabled = 'need draft sprint';
109
- } else if (ctx.ticketCount === 0) {
110
- refineDisabled = 'add tickets first';
111
- } else if (ctx.pendingRequirements === 0) {
112
- refineDisabled = 'all tickets refined';
113
- } else {
114
- refineDesc = `${String(ctx.pendingRequirements)} ticket${ctx.pendingRequirements !== 1 ? 's' : ''} pending`;
115
- }
116
- items.push({
117
- name: 'Refine Requirements',
118
- value: 'action:sprint:refine',
119
- description: refineDesc,
120
- disabled: refineDisabled,
121
- });
122
-
123
- // Plan Tasks — requires draft sprint
124
- let planDisabled: string | false = false;
125
- const planDesc = 'Generate tasks from requirements';
126
- if (!hasSprint) {
127
- planDisabled = 'create a sprint first';
128
- } else if (!isDraft) {
129
- planDisabled = 'need draft sprint';
130
- } else if (ctx.ticketCount === 0) {
131
- planDisabled = 'add tickets first';
132
- } else if (!ctx.allRequirementsApproved) {
133
- planDisabled = 'refine all tickets first';
134
- }
135
- items.push({
136
- name: ctx.taskCount > 0 ? 'Re-Plan Tasks' : 'Plan Tasks',
137
- value: 'action:sprint:plan',
138
- description: planDesc,
139
- disabled: planDisabled,
140
- });
141
-
142
- // Ideate — requires draft + projects
143
- const ideateDisabled = !hasSprint
144
- ? 'create a sprint first'
145
- : !isDraft
146
- ? 'need draft sprint'
147
- : !ctx.hasProjects
148
- ? 'add a project first'
149
- : false;
150
- items.push({
151
- name: 'Ideate',
152
- value: 'action:sprint:ideate',
153
- description: 'Quick idea to tasks',
154
- disabled: ideateDisabled,
155
- });
156
-
157
- return items;
158
- }
159
-
160
- /**
161
- * Build EXECUTE section actions — sprint execution lifecycle.
162
- */
163
- function buildExecuteActions(ctx: MenuContext): MenuItem[] {
164
- const items: MenuItem[] = [];
165
- const isDraft = ctx.currentSprintStatus === 'draft';
166
- const isActive = ctx.currentSprintStatus === 'active';
167
- const hasSprint = ctx.currentSprintId !== null;
168
-
169
- // Start Sprint — requires draft/active + tasks
170
- let startDisabled: string | false = false;
171
- if (!hasSprint) {
172
- startDisabled = 'create a sprint first';
173
- } else if (!isDraft && !isActive) {
174
- startDisabled = 'need draft or active sprint';
175
- } else if (ctx.taskCount === 0) {
176
- startDisabled = 'plan tasks first';
177
- }
178
- items.push({
179
- name: 'Start Sprint',
180
- value: 'action:sprint:start',
181
- description: 'Begin implementation',
182
- disabled: startDisabled,
183
- });
184
-
185
- // Health Check — requires a sprint
186
- items.push({
187
- name: 'Health Check',
188
- value: 'action:sprint:health',
189
- description: 'Diagnose blockers and stale tasks',
190
- disabled: !hasSprint ? 'no sprint' : false,
191
- });
192
-
193
- // Close Sprint — requires active sprint
194
- items.push({
195
- name: 'Close Sprint',
196
- value: 'action:sprint:close',
197
- description: 'Close the current sprint',
198
- disabled: !isActive ? 'need active sprint' : false,
199
- });
200
-
201
- return items;
202
- }
203
-
204
- /**
205
- * Build main menu items based on current application state.
206
- */
207
- export function buildMainMenu(ctx: MenuContext): { items: MenuItem[]; defaultValue?: string } {
208
- const items: MenuItem[] = [];
209
-
210
- // Next action — first item, default selection
211
- let defaultValue: string | undefined;
212
- if (ctx.nextAction) {
213
- const actionValue = `action:${ctx.nextAction.group}:${ctx.nextAction.subCommand}`;
214
- items.push({
215
- name: `\u2192 ${ctx.nextAction.label}`,
216
- value: actionValue,
217
- description: ctx.nextAction.description,
218
- });
219
- defaultValue = actionValue;
220
- }
221
-
222
- // Plan section — sprint planning lifecycle
223
- items.push(titled('PLAN'));
224
- for (const action of buildPlanActions(ctx)) {
225
- items.push(action);
226
- }
227
-
228
- // Execute section — sprint execution lifecycle
229
- items.push(titled('EXECUTE'));
230
- for (const action of buildExecuteActions(ctx)) {
231
- items.push(action);
232
- }
233
-
234
- // Browse section — entity submenus
235
- items.push(titled('BROWSE'));
236
- items.push({ name: 'Sprints', value: 'sprint', description: 'List, show, switch' });
237
- items.push({ name: 'Tickets', value: 'ticket', description: 'List, show, edit' });
238
- items.push({ name: 'Tasks', value: 'task', description: 'List, show, manage' });
239
-
240
- // Setup section — one-time configuration
241
- items.push(titled('SETUP'));
242
- items.push({ name: 'Projects', value: 'project', description: 'Manage projects & repositories' });
243
- items.push({ name: 'Configuration', value: 'config', description: 'AI provider, settings' });
244
- items.push({ name: 'Doctor', value: 'action:doctor:run', description: 'Check environment health' });
245
-
246
- // Session
247
- items.push(titled('SESSION'));
248
- if (!ctx.currentSprintId) {
249
- items.push({ name: 'Quick Start Wizard', value: 'wizard', description: 'Guided sprint setup' });
250
- }
251
- items.push({ name: 'Exit', value: 'exit' });
252
-
253
- return { items, defaultValue };
254
- }
255
-
256
- /**
257
- * Build sprint submenu — browse/manage only (workflow actions are in main menu).
258
- */
259
- function buildSprintSubMenu(ctx: MenuContext): SubMenu {
260
- const items: MenuItem[] = [];
261
-
262
- items.push(titled('BROWSE'));
263
- items.push({ name: 'List', value: 'list', description: 'List all sprints' });
264
- items.push({ name: 'Show', value: 'show', description: 'Show sprint details' });
265
- items.push({ name: 'Set Current', value: 'current', description: 'Set current sprint' });
266
- items.push(titled('EXPORT'));
267
- items.push({
268
- name: 'Requirements',
269
- value: 'requirements',
270
- description: 'Export refined requirements',
271
- });
272
- items.push({ name: 'Context', value: 'context', description: 'Output full sprint context' });
273
- items.push({ name: 'Progress', value: 'progress show', description: 'View progress log' });
274
- items.push(titled('MANAGE'));
275
- items.push({ name: 'Log Progress', value: 'progress log', description: 'Add progress entry' });
276
- items.push({ name: 'Delete', value: 'delete', description: 'Delete a sprint permanently' });
277
- items.push(line());
278
- items.push({ name: 'Back', value: 'back', description: 'Return to main menu' });
279
-
280
- const titleSuffix = ctx.currentSprintName
281
- ? ` \u2014 ${ctx.currentSprintName} (${ctx.currentSprintStatus ?? 'unknown'})`
282
- : '';
283
- return { title: `Sprint${titleSuffix}`, items };
284
- }
285
-
286
- /**
287
- * Build ticket submenu with state-aware descriptions.
288
- */
289
- function buildTicketSubMenu(ctx: MenuContext): SubMenu {
290
- const items: MenuItem[] = [];
291
-
292
- items.push({
293
- name: 'Add',
294
- value: 'add',
295
- description: ctx.hasProjects ? 'Add a ticket' : 'Add a ticket (add a project first)',
296
- disabled: !ctx.hasProjects ? 'add a project first' : false,
297
- });
298
- items.push({ name: 'Edit', value: 'edit', description: 'Edit a ticket' });
299
- items.push({ name: 'List', value: 'list', description: 'List all tickets' });
300
- items.push({ name: 'Show', value: 'show', description: 'Show ticket details' });
301
-
302
- // Re-refine — requires draft sprint with approved tickets
303
- const approvedCount = ctx.ticketCount - ctx.pendingRequirements;
304
- let refineDisabled: string | false = false;
305
- if (ctx.currentSprintStatus !== 'draft') {
306
- refineDisabled = 'need draft sprint';
307
- } else if (approvedCount === 0) {
308
- refineDisabled = 'no approved tickets';
309
- }
310
- items.push({
311
- name: 'Refine',
312
- value: 'refine',
313
- description: 'Re-refine approved requirements',
314
- disabled: refineDisabled,
315
- });
316
-
317
- items.push(line());
318
- items.push({ name: 'Remove', value: 'remove', description: 'Remove a ticket' });
319
- items.push({ name: 'Back', value: 'back', description: 'Return to main menu' });
320
-
321
- const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : '';
322
- return { title: `Ticket${titleSuffix}`, items };
323
- }
324
-
325
- /**
326
- * Build task submenu.
327
- */
328
- function buildTaskSubMenu(ctx: MenuContext): SubMenu {
329
- const items: MenuItem[] = [];
330
-
331
- items.push(titled('VIEW'));
332
- items.push({ name: 'List', value: 'list', description: 'List all tasks' });
333
- items.push({ name: 'Show', value: 'show', description: 'Show task details' });
334
- items.push({ name: 'Next', value: 'next', description: 'Get next task' });
335
- items.push(titled('MANAGE'));
336
- items.push({ name: 'Add', value: 'add', description: 'Add a new task' });
337
- items.push({ name: 'Import', value: 'import', description: 'Import from JSON' });
338
- items.push({ name: 'Status', value: 'status', description: 'Update status' });
339
- items.push({ name: 'Reorder', value: 'reorder', description: 'Change priority' });
340
- items.push(line());
341
- items.push({ name: 'Remove', value: 'remove', description: 'Remove a task' });
342
- items.push({ name: 'Back', value: 'back', description: 'Return to main menu' });
343
-
344
- const titleSuffix = ctx.currentSprintName ? ` \u2014 ${ctx.currentSprintName}` : '';
345
- return { title: `Task${titleSuffix}`, items };
346
- }
347
-
348
- /**
349
- * Build project submenu.
350
- */
351
- function buildProjectSubMenu(): SubMenu {
352
- const items: MenuItem[] = [];
353
-
354
- items.push({ name: 'Add', value: 'add', description: 'Add a new project' });
355
- items.push({ name: 'List', value: 'list', description: 'List all projects' });
356
- items.push({ name: 'Show', value: 'show', description: 'Show project details' });
357
- items.push(titled('REPOSITORIES'));
358
- items.push({
359
- name: 'Add Repository',
360
- value: 'repo add',
361
- description: 'Add repository to project',
362
- });
363
- items.push({ name: 'Remove Repository', value: 'repo remove', description: 'Remove repository' });
364
- items.push(line());
365
- items.push({ name: 'Remove', value: 'remove', description: 'Remove a project' });
366
- items.push({ name: 'Back', value: 'back', description: 'Return to main menu' });
367
-
368
- return { title: 'Project', items };
369
- }
370
-
371
- /**
372
- * Build config submenu.
373
- */
374
- function buildConfigSubMenu(): SubMenu {
375
- const items: MenuItem[] = [];
376
-
377
- items.push({ name: 'Show Settings', value: 'show', description: 'View current configuration' });
378
- items.push({ name: 'Set AI Provider', value: 'set provider', description: 'Choose Claude Code or GitHub Copilot' });
379
- items.push(line());
380
- items.push({ name: 'Back', value: 'back', description: 'Return to main menu' });
381
-
382
- return { title: 'Configuration', items };
383
- }
384
-
385
- /**
386
- * Build a submenu by group name with full context.
387
- */
388
- export function buildSubMenu(group: string, ctx: MenuContext): SubMenu | null {
389
- switch (group) {
390
- case 'sprint':
391
- return buildSprintSubMenu(ctx);
392
- case 'ticket':
393
- return buildTicketSubMenu(ctx);
394
- case 'task':
395
- return buildTaskSubMenu(ctx);
396
- case 'project':
397
- return buildProjectSubMenu();
398
- case 'config':
399
- return buildConfigSubMenu();
400
- default:
401
- return null;
402
- }
403
- }
@@ -1,273 +0,0 @@
1
- import { checkbox, confirm, input } from '@inquirer/prompts';
2
- import { listProjects } from '@src/store/project.ts';
3
- import { listSprints } from '@src/store/sprint.ts';
4
- import { formatTicketDisplay, listTickets } from '@src/store/ticket.ts';
5
- import { listTasks } from '@src/store/task.ts';
6
- import { emoji, formatSprintStatus, formatTaskStatus } from '@src/theme/ui.ts';
7
- import { muted } from '@src/theme/index.ts';
8
- import type { Repository, SprintStatus, TaskStatus, Ticket } from '@src/schemas/index.ts';
9
- import { escapableSelect } from './escapable.ts';
10
-
11
- /**
12
- * Select a project from the list.
13
- * @returns project name or null if no projects exist
14
- */
15
- export async function selectProject(message = 'Select project:'): Promise<string | null> {
16
- const projects = await listProjects();
17
- if (projects.length === 0) {
18
- console.log(muted('\nNo projects found.'));
19
- const create = await confirm({
20
- message: 'Create one now?',
21
- default: true,
22
- });
23
- if (create) {
24
- const { projectAddCommand } = await import('@src/commands/project/add.ts');
25
- await projectAddCommand({ interactive: true });
26
- // Re-check after creation
27
- const updated = await listProjects();
28
- if (updated.length === 0) return null;
29
- if (updated.length === 1 && updated[0]) return updated[0].name;
30
- // Fall through to selection below
31
- return escapableSelect({
32
- message: `${emoji.donut} ${message}`,
33
- choices: updated.map((p) => ({
34
- name: p.displayName,
35
- value: p.name,
36
- description: p.description,
37
- })),
38
- });
39
- }
40
- return null;
41
- }
42
-
43
- return escapableSelect({
44
- message: `${emoji.donut} ${message}`,
45
- choices: projects.map((p) => ({
46
- name: p.displayName,
47
- value: p.name,
48
- description: p.description,
49
- })),
50
- });
51
- }
52
-
53
- /**
54
- * Select a project and then a repository within it.
55
- * Auto-selects if only one option available at each step.
56
- * @returns repository path or null if no projects exist
57
- */
58
- export async function selectProjectRepository(message = 'Select repository:'): Promise<string | null> {
59
- const projects = await listProjects();
60
- if (projects.length === 0) {
61
- console.log(muted('\nNo projects found.\n'));
62
- return null;
63
- }
64
-
65
- // Step 1: Select project (auto-select if only one)
66
- let projectName: string | null;
67
- const firstProject = projects[0];
68
- if (projects.length === 1 && firstProject) {
69
- projectName = firstProject.name;
70
- } else {
71
- projectName = await escapableSelect({
72
- message: `${emoji.donut} Select project:`,
73
- choices: projects.map((p) => ({
74
- name: p.displayName,
75
- value: p.name,
76
- description: `${String(p.repositories.length)} repo(s)`,
77
- })),
78
- });
79
- }
80
-
81
- if (!projectName) return null;
82
-
83
- const project = projects.find((p) => p.name === projectName);
84
- if (!project) {
85
- return null;
86
- }
87
-
88
- // Step 2: Select repository (auto-select if only one)
89
- const firstRepo = project.repositories[0];
90
- if (project.repositories.length === 1 && firstRepo) {
91
- return firstRepo.path;
92
- }
93
-
94
- return escapableSelect({
95
- message: `${emoji.donut} ${message}`,
96
- choices: project.repositories.map((r) => ({
97
- name: r.name,
98
- value: r.path,
99
- description: r.path,
100
- })),
101
- });
102
- }
103
-
104
- /**
105
- * Select a sprint from the list, optionally filtered by status.
106
- * @returns sprint ID or null if no matching sprints
107
- */
108
- export async function selectSprint(message = 'Select sprint:', filter?: SprintStatus[]): Promise<string | null> {
109
- const sprints = await listSprints();
110
- const filtered = filter ? sprints.filter((s) => filter.includes(s.status)) : sprints;
111
-
112
- if (filtered.length === 0) {
113
- console.log(muted('\nNo sprints found.'));
114
- const create = await confirm({
115
- message: 'Create one now?',
116
- default: true,
117
- });
118
- if (create) {
119
- const { sprintCreateCommand } = await import('@src/commands/sprint/create.ts');
120
- await sprintCreateCommand({ interactive: true });
121
- // Re-check
122
- const updated = await listSprints();
123
- const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
124
- if (refiltered.length === 0) return null;
125
- if (refiltered.length === 1 && refiltered[0]) return refiltered[0].id;
126
- return escapableSelect({
127
- message: `${emoji.donut} ${message}`,
128
- choices: refiltered.map((s) => ({
129
- name: `${s.id} - ${s.name} (${formatSprintStatus(s.status)})`,
130
- value: s.id,
131
- })),
132
- });
133
- }
134
- return null;
135
- }
136
-
137
- return escapableSelect({
138
- message: `${emoji.donut} ${message}`,
139
- choices: filtered.map((s) => ({
140
- name: `${s.id} - ${s.name} (${formatSprintStatus(s.status)})`,
141
- value: s.id,
142
- })),
143
- });
144
- }
145
-
146
- /**
147
- * Select a ticket from the current sprint, optionally filtered.
148
- * @returns ticket ID or null if no tickets exist/match
149
- */
150
- export async function selectTicket(
151
- message = 'Select ticket:',
152
- filter?: (t: Ticket) => boolean
153
- ): Promise<string | null> {
154
- const tickets = await listTickets();
155
- const filtered = filter ? tickets.filter(filter) : tickets;
156
-
157
- if (filtered.length === 0) {
158
- if (tickets.length === 0) {
159
- console.log(muted('\nNo tickets found.'));
160
- const create = await confirm({
161
- message: 'Add one now?',
162
- default: true,
163
- });
164
- if (create) {
165
- const { ticketAddCommand } = await import('@src/commands/ticket/add.ts');
166
- await ticketAddCommand({ interactive: true });
167
- // Re-check
168
- const updated = await listTickets();
169
- const refiltered = filter ? updated.filter(filter) : updated;
170
- if (refiltered.length === 0) return null;
171
- if (refiltered.length === 1 && refiltered[0]) return refiltered[0].id;
172
- return escapableSelect({
173
- message: `${emoji.donut} ${message}`,
174
- choices: refiltered.map((t) => ({
175
- name: formatTicketDisplay(t),
176
- value: t.id,
177
- })),
178
- });
179
- }
180
- return null;
181
- }
182
- console.log(muted('\nNo matching tickets found.\n'));
183
- return null;
184
- }
185
-
186
- return escapableSelect({
187
- message: `${emoji.donut} ${message}`,
188
- choices: filtered.map((t) => ({
189
- name: formatTicketDisplay(t),
190
- value: t.id,
191
- })),
192
- });
193
- }
194
-
195
- /**
196
- * Select a task from the current sprint, optionally filtered by status.
197
- * @returns task ID or null if no matching tasks
198
- */
199
- export async function selectTask(message = 'Select task:', filter?: TaskStatus[]): Promise<string | null> {
200
- const tasks = await listTasks();
201
- const filtered = filter ? tasks.filter((t) => filter.includes(t.status)) : tasks;
202
-
203
- if (filtered.length === 0) {
204
- console.log(muted('\nNo tasks found. Use "sprint plan" to generate tasks.\n'));
205
- return null;
206
- }
207
-
208
- return escapableSelect({
209
- message: `${emoji.donut} ${message}`,
210
- choices: filtered.map((t) => ({
211
- name: `${formatTaskStatus(t.status)} ${t.name}`,
212
- value: t.id,
213
- })),
214
- });
215
- }
216
-
217
- /**
218
- * Select a task status.
219
- * @returns task status
220
- */
221
- export async function selectTaskStatus(message = 'Select status:'): Promise<TaskStatus | null> {
222
- const statuses: TaskStatus[] = ['todo', 'in_progress', 'done'];
223
-
224
- return escapableSelect({
225
- message: `${emoji.donut} ${message}`,
226
- choices: statuses.map((s) => ({
227
- name: formatTaskStatus(s),
228
- value: s,
229
- })),
230
- });
231
- }
232
-
233
- /**
234
- * Prompt for a positive integer.
235
- * @returns the parsed number
236
- */
237
- export async function inputPositiveInt(message: string): Promise<number> {
238
- const value = await input({
239
- message: `${emoji.donut} ${message}`,
240
- validate: (v) => {
241
- const n = parseInt(v, 10);
242
- if (isNaN(n) || n < 1) return 'Must be a positive integer';
243
- return true;
244
- },
245
- });
246
- return parseInt(value, 10);
247
- }
248
-
249
- /**
250
- * Select project repositories for AI to explore.
251
- * If preSelected is provided, those paths are checked by default.
252
- * Otherwise, the first repository per project is pre-selected.
253
- */
254
- export async function selectProjectPaths(
255
- reposByProject: Map<string, Repository[]>,
256
- message = 'Select paths to explore:',
257
- preSelected?: string[]
258
- ): Promise<string[]> {
259
- const choices: { name: string; value: string; checked: boolean }[] = [];
260
- const preSelectedSet = preSelected ? new Set(preSelected) : null;
261
-
262
- for (const [projectName, repos] of reposByProject) {
263
- repos.forEach((repo, i) => {
264
- choices.push({
265
- name: `[${projectName}] ${repo.name} (${repo.path})`,
266
- value: repo.path,
267
- checked: preSelectedSet ? preSelectedSet.has(repo.path) : i === 0,
268
- });
269
- });
270
- }
271
-
272
- return checkbox({ message: `${emoji.donut} ${message}`, choices });
273
- }