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
package/src/interactive/menu.ts
DELETED
|
@@ -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
|
-
}
|