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,153 +0,0 @@
1
- import { readdirSync, statSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { emoji } from '@src/theme/ui.ts';
5
- import { muted } from '@src/theme/index.ts';
6
- import { escapableSelect } from './escapable.ts';
7
-
8
- interface BrowseChoice {
9
- name: string;
10
- value: string;
11
- description?: string;
12
- }
13
-
14
- /**
15
- * List directories in a path, sorted alphabetically.
16
- * Excludes hidden directories (starting with .).
17
- */
18
- function listDirectories(dirPath: string): string[] {
19
- try {
20
- const entries = readdirSync(dirPath, { withFileTypes: true });
21
- return entries
22
- .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
23
- .map((e) => e.name)
24
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
25
- } catch {
26
- return [];
27
- }
28
- }
29
-
30
- /**
31
- * Check if a directory contains subdirectories.
32
- */
33
- function hasSubdirectories(dirPath: string): boolean {
34
- try {
35
- const entries = readdirSync(dirPath, { withFileTypes: true });
36
- return entries.some((e) => e.isDirectory() && !e.name.startsWith('.'));
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- /**
43
- * Check if a path is likely a git repository.
44
- */
45
- function isGitRepo(dirPath: string): boolean {
46
- try {
47
- const gitDir = join(dirPath, '.git');
48
- return statSync(gitDir).isDirectory();
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- /**
55
- * Interactive filesystem browser starting from home directory.
56
- * Returns the selected directory path or null if cancelled.
57
- */
58
- export async function browseDirectory(message = 'Browse to directory:', startPath?: string): Promise<string | null> {
59
- let currentPath = startPath ? resolve(startPath) : homedir();
60
-
61
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- loop control
62
- while (true) {
63
- const dirs = listDirectories(currentPath);
64
- const choices: BrowseChoice[] = [];
65
-
66
- // Navigation options
67
- choices.push({
68
- name: `${emoji.donut} Select this directory`,
69
- value: '__SELECT__',
70
- description: currentPath,
71
- });
72
-
73
- // Parent directory (if not at root)
74
- const parentDir = dirname(currentPath);
75
- if (parentDir !== currentPath) {
76
- choices.push({
77
- name: '↑ Parent directory',
78
- value: '__PARENT__',
79
- description: parentDir,
80
- });
81
- }
82
-
83
- // Home directory shortcut
84
- if (currentPath !== homedir()) {
85
- choices.push({
86
- name: '⌂ Home directory',
87
- value: '__HOME__',
88
- description: homedir(),
89
- });
90
- }
91
-
92
- // Subdirectories
93
- for (const dir of dirs) {
94
- const fullPath = join(currentPath, dir);
95
- const hasChildren = hasSubdirectories(fullPath);
96
- const isRepo = isGitRepo(fullPath);
97
-
98
- let icon = ' ';
99
- if (isRepo) {
100
- icon = '⚙ '; // Git repo indicator
101
- } else if (hasChildren) {
102
- icon = '▸ '; // Has subdirectories
103
- }
104
-
105
- choices.push({
106
- name: `${icon}${dir}`,
107
- value: fullPath,
108
- description: isRepo ? 'git repo' : undefined,
109
- });
110
- }
111
-
112
- // Cancel option
113
- choices.push({
114
- name: muted('Cancel'),
115
- value: '__CANCEL__',
116
- });
117
-
118
- try {
119
- const selected = await escapableSelect({
120
- message: `${emoji.donut} ${message}\n ${muted(currentPath)}`,
121
- choices,
122
- pageSize: 15,
123
- loop: false,
124
- });
125
-
126
- if (selected === null) {
127
- return null;
128
- }
129
-
130
- switch (selected) {
131
- case '__SELECT__':
132
- return currentPath;
133
- case '__PARENT__':
134
- currentPath = parentDir;
135
- break;
136
- case '__HOME__':
137
- currentPath = homedir();
138
- break;
139
- case '__CANCEL__':
140
- return null;
141
- default:
142
- // Navigate into selected directory
143
- currentPath = selected;
144
- }
145
- } catch (err) {
146
- // Handle Ctrl+C
147
- if ((err as Error).name === 'ExitPromptError') {
148
- return null;
149
- }
150
- throw err;
151
- }
152
- }
153
- }
@@ -1,429 +0,0 @@
1
- import { clearScreen, emoji, log, printSeparator, showBanner } from '@src/theme/ui.ts';
2
- import { colors, getQuoteForContext } from '@src/theme/index.ts';
3
- import { buildMainMenu, buildSubMenu, isWorkflowAction, type MenuContext, type MenuItem } from './menu.ts';
4
- import { renderStatusHeader } from './dashboard.ts';
5
- import { getAiProvider, getConfig } from '@src/store/config.ts';
6
- import { getSprint } from '@src/store/sprint.ts';
7
- import { listProjects } from '@src/store/project.ts';
8
- import { getNextAction, type DashboardData } from './dashboard.ts';
9
- import { allRequirementsApproved, getPendingRequirements } from '@src/store/ticket.ts';
10
- import { type Tasks, TasksSchema } from '@src/schemas/index.ts';
11
- import { getTasksFilePath } from '@src/utils/paths.ts';
12
- import { readValidatedJson } from '@src/utils/storage.ts';
13
- import { select } from '@inquirer/prompts';
14
- import { escapableSelect } from './escapable.ts';
15
-
16
- // Command imports - project
17
- import { projectAddCommand } from '@src/commands/project/add.ts';
18
- import { projectListCommand } from '@src/commands/project/list.ts';
19
- import { projectShowCommand } from '@src/commands/project/show.ts';
20
- import { projectRemoveCommand } from '@src/commands/project/remove.ts';
21
- import { projectRepoAddCommand, projectRepoRemoveCommand } from '@src/commands/project/repo.ts';
22
-
23
- // Command imports - sprint
24
- import { sprintCreateCommand } from '@src/commands/sprint/create.ts';
25
- import { sprintListCommand } from '@src/commands/sprint/list.ts';
26
- import { sprintShowCommand } from '@src/commands/sprint/show.ts';
27
- import { sprintContextCommand } from '@src/commands/sprint/context.ts';
28
- import { sprintCurrentCommand } from '@src/commands/sprint/current.ts';
29
- import { sprintRefineCommand } from '@src/commands/sprint/refine.ts';
30
- import { sprintIdeateCommand } from '@src/commands/sprint/ideate.ts';
31
- import { sprintPlanCommand } from '@src/commands/sprint/plan.ts';
32
- import { sprintStartCommand } from '@src/commands/sprint/start.ts';
33
- import { sprintCloseCommand } from '@src/commands/sprint/close.ts';
34
- import { sprintDeleteCommand } from '@src/commands/sprint/delete.ts';
35
- import { sprintRequirementsCommand } from '@src/commands/sprint/requirements.ts';
36
- import { sprintHealthCommand } from '@src/commands/sprint/health.ts';
37
-
38
- // Command imports - ticket
39
- import { ticketAddCommand } from '@src/commands/ticket/add.ts';
40
- import { ticketEditCommand } from '@src/commands/ticket/edit.ts';
41
- import { ticketListCommand } from '@src/commands/ticket/list.ts';
42
- import { ticketShowCommand } from '@src/commands/ticket/show.ts';
43
- import { ticketRemoveCommand } from '@src/commands/ticket/remove.ts';
44
- import { ticketRefineCommand } from '@src/commands/ticket/refine.ts';
45
-
46
- // Command imports - task
47
- import { taskAddCommand } from '@src/commands/task/add.ts';
48
- import { taskImportCommand } from '@src/commands/task/import.ts';
49
- import { taskListCommand } from '@src/commands/task/list.ts';
50
- import { taskShowCommand } from '@src/commands/task/show.ts';
51
- import { taskStatusCommand } from '@src/commands/task/status.ts';
52
- import { taskNextCommand } from '@src/commands/task/next.ts';
53
- import { taskReorderCommand } from '@src/commands/task/reorder.ts';
54
- import { taskRemoveCommand } from '@src/commands/task/remove.ts';
55
-
56
- // Command imports - progress
57
- import { progressLogCommand } from '@src/commands/progress/log.ts';
58
- import { progressShowCommand } from '@src/commands/progress/show.ts';
59
-
60
- // Command imports - config
61
- import { configShowCommand, configSetCommand } from '@src/commands/config/config.ts';
62
-
63
- // Command imports - doctor
64
- import { doctorCommand } from '@src/commands/doctor/doctor.ts';
65
-
66
- // Custom theme with donut selector
67
- const selectTheme = {
68
- icon: { cursor: emoji.donut },
69
- style: {
70
- highlight: (text: string) => colors.highlight(text),
71
- description: (text: string) => colors.muted(text),
72
- },
73
- };
74
-
75
- /**
76
- * Command dispatch map: (group, subCommand) → handler
77
- */
78
- type CommandHandler = () => Promise<void>;
79
-
80
- const commandMap: Record<string, Record<string, CommandHandler>> = {
81
- project: {
82
- add: () => projectAddCommand({ interactive: true }),
83
- list: () => projectListCommand(),
84
- show: () => projectShowCommand([]),
85
- remove: () => projectRemoveCommand([]),
86
- 'repo add': () => projectRepoAddCommand([]),
87
- 'repo remove': () => projectRepoRemoveCommand([]),
88
- },
89
- sprint: {
90
- create: () => sprintCreateCommand({ interactive: true }),
91
- list: () => sprintListCommand(),
92
- show: () => sprintShowCommand([]),
93
- context: () => sprintContextCommand([]),
94
- current: () => sprintCurrentCommand(['-']),
95
- refine: () => sprintRefineCommand([]),
96
- ideate: () => sprintIdeateCommand([]),
97
- plan: () => sprintPlanCommand([]),
98
- start: () => sprintStartCommand([]),
99
- requirements: () => sprintRequirementsCommand([]),
100
- health: () => sprintHealthCommand(),
101
- close: () => sprintCloseCommand([]),
102
- delete: () => sprintDeleteCommand([]),
103
- 'progress show': () => progressShowCommand(),
104
- 'progress log': () => progressLogCommand([]),
105
- },
106
- ticket: {
107
- add: () => ticketAddCommand({ interactive: true }),
108
- edit: () => ticketEditCommand(undefined, { interactive: true }),
109
- list: () => ticketListCommand([]),
110
- show: () => ticketShowCommand([]),
111
- refine: () => ticketRefineCommand(undefined, { interactive: true }),
112
- remove: () => ticketRemoveCommand([]),
113
- },
114
- task: {
115
- add: () => taskAddCommand({ interactive: true }),
116
- import: () => taskImportCommand([]),
117
- list: () => taskListCommand([]),
118
- show: () => taskShowCommand([]),
119
- status: () => taskStatusCommand([]),
120
- next: () => taskNextCommand(),
121
- reorder: () => taskReorderCommand([]),
122
- remove: () => taskRemoveCommand([]),
123
- },
124
- progress: {
125
- log: () => progressLogCommand([]),
126
- show: () => progressShowCommand(),
127
- },
128
- doctor: {
129
- run: () => doctorCommand(),
130
- },
131
- config: {
132
- show: () => configShowCommand(),
133
- 'set provider': async () => {
134
- const choice = await select({
135
- message: `${emoji.donut} Which AI buddy should help with my homework?`,
136
- choices: [
137
- { name: 'Claude Code', value: 'claude' as const },
138
- { name: 'GitHub Copilot', value: 'copilot' as const },
139
- ],
140
- default: (await getAiProvider()) ?? undefined,
141
- theme: selectTheme,
142
- });
143
- await configSetCommand(['provider', choice]);
144
- },
145
- },
146
- };
147
-
148
- /**
149
- * Show themed farewell message on exit.
150
- */
151
- function showFarewell(): void {
152
- const quote = getQuoteForContext('farewell');
153
- console.log('');
154
- printSeparator();
155
- console.log(` ${emoji.donut} ${colors.muted(quote)}`);
156
- console.log('');
157
- }
158
-
159
- /**
160
- * Pause until the user presses Enter so they can read command output
161
- * before the screen is cleared for the next menu render.
162
- */
163
- async function pressEnterToContinue(): Promise<void> {
164
- const { createInterface } = await import('node:readline');
165
- const rl = createInterface({ input: process.stdin, output: process.stdout });
166
- await new Promise<void>((resolve) => {
167
- rl.question(colors.muted(' Press Enter to continue...'), () => {
168
- rl.close();
169
- resolve();
170
- });
171
- });
172
- }
173
-
174
- /**
175
- * Show the welcome banner with gradient styling.
176
- * Note: showBanner() already prints a Ralph quote.
177
- */
178
- function showWelcomeBanner(): void {
179
- showBanner();
180
- }
181
-
182
- /**
183
- * Read tasks for a sprint, returning empty array if the file doesn't exist yet.
184
- */
185
- async function readTasksSafe(sprintId: string): Promise<Tasks> {
186
- try {
187
- return await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
188
- } catch {
189
- return [];
190
- }
191
- }
192
-
193
- /**
194
- * Gather current application state for context-aware menus.
195
- * Reads each data file at most once and parallelizes independent reads.
196
- * Returns both MenuContext and optional DashboardData for status header.
197
- */
198
- async function getMenuContext(): Promise<{ ctx: MenuContext; dashboardData: DashboardData | null }> {
199
- let dashboardData: DashboardData | null = null;
200
-
201
- const ctx: MenuContext = {
202
- hasProjects: false,
203
- projectCount: 0,
204
- currentSprintId: null,
205
- currentSprintName: null,
206
- currentSprintStatus: null,
207
- ticketCount: 0,
208
- taskCount: 0,
209
- tasksDone: 0,
210
- tasksInProgress: 0,
211
- pendingRequirements: 0,
212
- allRequirementsApproved: false,
213
- plannedTicketCount: 0,
214
- nextAction: null,
215
- aiProvider: null,
216
- };
217
-
218
- // Read config and projects in parallel (independent files)
219
- const [config, projects] = await Promise.all([getConfig().catch(() => null), listProjects().catch(() => [])]);
220
-
221
- ctx.hasProjects = projects.length > 0;
222
- ctx.projectCount = projects.length;
223
- ctx.aiProvider = config?.aiProvider ?? null;
224
-
225
- const sprintId = config?.currentSprint ?? null;
226
- if (!sprintId) return { ctx, dashboardData };
227
-
228
- ctx.currentSprintId = sprintId;
229
-
230
- // Read sprint and tasks in parallel (both depend on sprintId, but not each other)
231
- const [sprint, tasks] = await Promise.all([getSprint(sprintId).catch(() => null), readTasksSafe(sprintId)]);
232
-
233
- if (!sprint) return { ctx, dashboardData };
234
-
235
- ctx.currentSprintName = sprint.name;
236
- ctx.currentSprintStatus = sprint.status;
237
- ctx.ticketCount = sprint.tickets.length;
238
-
239
- const pendingTickets = getPendingRequirements(sprint.tickets);
240
- ctx.pendingRequirements = pendingTickets.length;
241
- ctx.allRequirementsApproved = allRequirementsApproved(sprint.tickets);
242
-
243
- ctx.taskCount = tasks.length;
244
- ctx.tasksDone = tasks.filter((t) => t.status === 'done').length;
245
- ctx.tasksInProgress = tasks.filter((t) => t.status === 'in_progress').length;
246
-
247
- // Count tickets that have at least one associated task
248
- const ticketIdsWithTasks = new Set(tasks.map((t) => t.ticketId).filter(Boolean));
249
- ctx.plannedTicketCount = sprint.tickets.filter((t) => ticketIdsWithTasks.has(t.id)).length;
250
-
251
- // Build DashboardData from already-loaded data (no extra I/O)
252
- const doneIds = new Set(tasks.filter((t) => t.status === 'done').map((t) => t.id));
253
- const blockedCount = tasks.filter(
254
- (t) => t.status !== 'done' && t.blockedBy.length > 0 && !t.blockedBy.every((id) => doneIds.has(id))
255
- ).length;
256
-
257
- dashboardData = {
258
- sprint,
259
- tasks,
260
- approvedCount: sprint.tickets.length - pendingTickets.length,
261
- pendingCount: pendingTickets.length,
262
- blockedCount,
263
- plannedTicketCount: ctx.plannedTicketCount,
264
- aiProvider: ctx.aiProvider,
265
- };
266
-
267
- ctx.nextAction = getNextAction(dashboardData);
268
-
269
- return { ctx, dashboardData };
270
- }
271
-
272
- /**
273
- * Run the interactive REPL mode
274
- */
275
- export async function interactiveMode(): Promise<void> {
276
- let escPressed = false;
277
-
278
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- loop control variable
279
- while (true) {
280
- try {
281
- const { ctx, dashboardData } = await getMenuContext();
282
-
283
- // Clear and re-render banner + content each iteration
284
- clearScreen();
285
- showWelcomeBanner();
286
-
287
- // Persistent status header before main menu
288
- const statusLines = renderStatusHeader(dashboardData);
289
- if (statusLines.length > 0) {
290
- for (const line of statusLines) {
291
- console.log(line);
292
- }
293
- log.newline();
294
- }
295
-
296
- const { items: mainMenu, defaultValue } = buildMainMenu(ctx);
297
-
298
- // ESC re-renders with Exit pre-selected; Enter on Exit actually exits
299
- const effectiveDefault = escPressed ? 'exit' : defaultValue;
300
- escPressed = false;
301
-
302
- const command = await escapableSelect(
303
- {
304
- message: `${emoji.donut} What would you like to do?`,
305
- choices: mainMenu,
306
- default: effectiveDefault,
307
- pageSize: 30,
308
- loop: true,
309
- theme: selectTheme,
310
- },
311
- { escLabel: 'exit' }
312
- );
313
-
314
- if (command === null) {
315
- escPressed = true;
316
- continue;
317
- }
318
-
319
- if (command === 'exit') {
320
- showFarewell();
321
- break;
322
- }
323
-
324
- // Direct action dispatch (next action + workflow actions)
325
- if (command.startsWith('action:')) {
326
- const parts = command.split(':');
327
- const group = parts[1] ?? '';
328
- const subCommand = parts[2] ?? '';
329
- log.newline();
330
- await executeCommand(group, subCommand);
331
- log.newline();
332
- await pressEnterToContinue();
333
- continue;
334
- }
335
-
336
- if (command === 'wizard') {
337
- const { runWizard } = await import('./wizard.ts');
338
- await runWizard();
339
- continue;
340
- }
341
-
342
- const subMenu = buildSubMenu(command, ctx);
343
- if (subMenu) {
344
- await handleSubMenu(command, subMenu);
345
- }
346
- } catch (err) {
347
- if ((err as Error).name === 'ExitPromptError') {
348
- showFarewell();
349
- break;
350
- }
351
- throw err;
352
- }
353
- }
354
- }
355
-
356
- /**
357
- * Handle a submenu with smooth transitions.
358
- * Rebuilds the submenu on each iteration so disabled states refresh after actions.
359
- * Workflow actions (create, refine, plan, start, etc.) return to main menu.
360
- */
361
- async function handleSubMenu(
362
- commandGroup: string,
363
- initialSubMenu: { title: string; items: MenuItem[] }
364
- ): Promise<void> {
365
- let currentTitle = initialSubMenu.title;
366
- let currentItems = initialSubMenu.items;
367
-
368
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- loop control variable
369
- while (true) {
370
- try {
371
- log.newline();
372
- const subCommand = await escapableSelect({
373
- message: `${emoji.donut} ${currentTitle}`,
374
- choices: currentItems,
375
- pageSize: 30,
376
- loop: true,
377
- theme: selectTheme,
378
- });
379
-
380
- if (subCommand === null || subCommand === 'back') {
381
- break;
382
- }
383
-
384
- log.newline();
385
- await executeCommand(commandGroup, subCommand);
386
- log.newline();
387
-
388
- // Workflow actions return to main menu so next action updates
389
- if (isWorkflowAction(commandGroup, subCommand)) {
390
- break;
391
- }
392
-
393
- // Management actions stay in submenu — refresh context
394
- const { ctx: refreshedCtx } = await getMenuContext();
395
- const refreshedMenu = buildSubMenu(commandGroup, refreshedCtx);
396
- if (refreshedMenu) {
397
- currentTitle = refreshedMenu.title;
398
- currentItems = refreshedMenu.items;
399
- }
400
- } catch (err) {
401
- if ((err as Error).name === 'ExitPromptError') {
402
- // Ctrl+C in submenu returns to main menu
403
- break;
404
- }
405
- throw err;
406
- }
407
- }
408
- }
409
-
410
- /**
411
- * Execute a command by dispatching directly to the handler
412
- */
413
- async function executeCommand(group: string, subCommand: string): Promise<void> {
414
- const groupHandlers = commandMap[group];
415
- const handler = groupHandlers?.[subCommand];
416
-
417
- if (!handler) {
418
- log.error(`Unknown command: ${group} ${subCommand}`);
419
- return;
420
- }
421
-
422
- try {
423
- await handler();
424
- } catch (err) {
425
- if (err instanceof Error) {
426
- log.error(err.message);
427
- }
428
- }
429
- }