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