popeye-cli 1.0.0 → 1.1.0
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 +521 -125
- package/dist/adapters/claude.d.ts +16 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +679 -33
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +41 -7
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/index.d.ts +11 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +23 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts +2 -2
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1380 -183
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +6 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +20 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +1 -0
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +1 -0
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +69 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +24 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/workflow.d.ts +55 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +16 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +44 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +565 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +14 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +589 -47
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +142 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +331 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +383 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +1 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +9 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +815 -34
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/openai.ts +40 -7
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/index.ts +28 -8
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/index.ts +4 -7
- package/src/cli/interactive.ts +1641 -216
- package/src/config/defaults.ts +10 -2
- package/src/config/index.ts +21 -0
- package/src/config/schema.ts +7 -0
- package/src/generators/python.ts +1 -0
- package/src/generators/typescript.ts +1 -0
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +4 -0
- package/src/types/consensus.ts +65 -6
- package/src/types/workflow.ts +35 -0
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +750 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +696 -50
- package/src/workflow/plan-storage.ts +482 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +525 -0
- package/src/workflow/test-runner.ts +10 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- package/tests/workflow/test-runner.test.ts +5 -3
package/src/cli/interactive.ts
CHANGED
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive mode
|
|
3
|
-
*
|
|
3
|
+
* Claude Code-style interface for Popeye CLI
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as readline from 'node:readline';
|
|
7
|
-
import {
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
getAuthStatusForDisplay,
|
|
11
|
+
authenticateClaude,
|
|
12
|
+
authenticateOpenAI,
|
|
13
|
+
authenticateGemini,
|
|
14
|
+
isClaudeCLIInstalled,
|
|
15
|
+
checkClaudeCLIAuth,
|
|
16
|
+
checkGeminiAuth,
|
|
17
|
+
} from '../auth/index.js';
|
|
8
18
|
import {
|
|
9
19
|
runWorkflow,
|
|
10
20
|
resumeWorkflow,
|
|
11
21
|
getWorkflowStatus,
|
|
12
22
|
getWorkflowSummary,
|
|
13
23
|
} from '../workflow/index.js';
|
|
24
|
+
import {
|
|
25
|
+
analyzeProjectProgress,
|
|
26
|
+
verifyProjectCompletion,
|
|
27
|
+
} from '../state/index.js';
|
|
14
28
|
import { generateProject } from '../generators/index.js';
|
|
15
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
discoverProjects,
|
|
31
|
+
formatProjectForDisplay,
|
|
32
|
+
} from '../state/registry.js';
|
|
33
|
+
import { loadConfig, saveConfig } from '../config/index.js';
|
|
16
34
|
import type { ProjectSpec, OutputLanguage, OpenAIModel } from '../types/project.js';
|
|
35
|
+
import type { AIProvider, GeminiModel } from '../types/consensus.js';
|
|
17
36
|
import {
|
|
18
|
-
printHeader,
|
|
19
|
-
printSection,
|
|
20
37
|
printSuccess,
|
|
21
38
|
printError,
|
|
22
39
|
printWarning,
|
|
23
40
|
printInfo,
|
|
24
41
|
printKeyValue,
|
|
25
|
-
printAuthStatus,
|
|
26
42
|
startSpinner,
|
|
27
43
|
succeedSpinner,
|
|
28
44
|
failSpinner,
|
|
29
45
|
stopSpinner,
|
|
30
|
-
clearConsole,
|
|
31
46
|
theme,
|
|
32
47
|
} from './output.js';
|
|
33
48
|
|
|
49
|
+
// Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Box drawing characters for Claude Code-style UI
|
|
53
|
+
*/
|
|
54
|
+
const box = {
|
|
55
|
+
topLeft: '╭',
|
|
56
|
+
topRight: '╮',
|
|
57
|
+
bottomLeft: '╰',
|
|
58
|
+
bottomRight: '╯',
|
|
59
|
+
horizontal: '─',
|
|
60
|
+
vertical: '│',
|
|
61
|
+
leftT: '├',
|
|
62
|
+
rightT: '┤',
|
|
63
|
+
};
|
|
64
|
+
|
|
34
65
|
/**
|
|
35
66
|
* Interactive session state
|
|
36
67
|
*/
|
|
@@ -38,159 +69,615 @@ interface SessionState {
|
|
|
38
69
|
projectDir: string | null;
|
|
39
70
|
language: OutputLanguage;
|
|
40
71
|
model: OpenAIModel;
|
|
41
|
-
|
|
72
|
+
geminiModel: GeminiModel;
|
|
73
|
+
claudeAuth: boolean;
|
|
74
|
+
openaiAuth: boolean;
|
|
75
|
+
geminiAuth: boolean;
|
|
76
|
+
reviewer: AIProvider;
|
|
77
|
+
arbitrator: AIProvider;
|
|
78
|
+
enableArbitration: boolean;
|
|
42
79
|
}
|
|
43
80
|
|
|
44
81
|
/**
|
|
45
|
-
*
|
|
82
|
+
* Get terminal width
|
|
46
83
|
*/
|
|
47
|
-
|
|
84
|
+
function getTerminalWidth(): number {
|
|
85
|
+
return process.stdout.columns || 80;
|
|
86
|
+
}
|
|
48
87
|
|
|
49
88
|
/**
|
|
50
|
-
*
|
|
89
|
+
* Draw the header box
|
|
51
90
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
handler: handleResume,
|
|
84
|
-
},
|
|
85
|
-
'/clear': {
|
|
86
|
-
description: 'Clear the screen',
|
|
87
|
-
handler: handleClear,
|
|
88
|
-
},
|
|
89
|
-
'/exit': {
|
|
90
|
-
description: 'Exit interactive mode',
|
|
91
|
-
handler: handleExit,
|
|
92
|
-
},
|
|
93
|
-
'/quit': {
|
|
94
|
-
description: 'Exit interactive mode',
|
|
95
|
-
handler: handleExit,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
91
|
+
function drawHeader(): void {
|
|
92
|
+
const width = getTerminalWidth();
|
|
93
|
+
const title = ' Popeye CLI ';
|
|
94
|
+
const subtitle = ' Autonomous Code Generation with AI Consensus ';
|
|
95
|
+
|
|
96
|
+
// Top border
|
|
97
|
+
console.log(theme.dim(box.topLeft + box.horizontal.repeat(width - 2) + box.topRight));
|
|
98
|
+
|
|
99
|
+
// Title line
|
|
100
|
+
const titlePadding = Math.floor((width - title.length - 2) / 2);
|
|
101
|
+
console.log(
|
|
102
|
+
theme.dim(box.vertical) +
|
|
103
|
+
' '.repeat(titlePadding) +
|
|
104
|
+
theme.primary.bold(title) +
|
|
105
|
+
' '.repeat(width - titlePadding - title.length - 2) +
|
|
106
|
+
theme.dim(box.vertical)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Subtitle line
|
|
110
|
+
const subPadding = Math.floor((width - subtitle.length - 2) / 2);
|
|
111
|
+
console.log(
|
|
112
|
+
theme.dim(box.vertical) +
|
|
113
|
+
' '.repeat(subPadding) +
|
|
114
|
+
theme.secondary(subtitle) +
|
|
115
|
+
' '.repeat(width - subPadding - subtitle.length - 2) +
|
|
116
|
+
theme.dim(box.vertical)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Bottom border
|
|
120
|
+
console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
|
|
121
|
+
}
|
|
98
122
|
|
|
99
123
|
/**
|
|
100
|
-
*
|
|
124
|
+
* Draw hints line and top of input box
|
|
101
125
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
printHeader('Popeye CLI - Interactive Mode');
|
|
126
|
+
function drawInputBoxTop(state: SessionState): void {
|
|
127
|
+
const width = Math.min(getTerminalWidth(), 100);
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
// Hints line (above the box)
|
|
130
|
+
const hints = [
|
|
131
|
+
theme.dim('/lang ') + theme.primary('py') + theme.dim('|') + theme.primary('ts'),
|
|
132
|
+
theme.dim('/config'),
|
|
133
|
+
theme.dim('/help'),
|
|
134
|
+
theme.dim('/exit'),
|
|
135
|
+
];
|
|
136
|
+
console.log(' ' + hints.join(' '));
|
|
137
|
+
|
|
138
|
+
// Status items for the top line
|
|
139
|
+
const langStatus = state.language;
|
|
140
|
+
const reviewerStatus = state.reviewer === 'openai' ? 'O' : 'G';
|
|
141
|
+
const arbitratorStatus = state.enableArbitration ? (state.arbitrator === 'openai' ? 'O' : 'G') : '-';
|
|
142
|
+
const allAuth = state.claudeAuth && state.openaiAuth && (state.enableArbitration ? state.geminiAuth : true);
|
|
143
|
+
const authIcon = allAuth ? '●' : '○';
|
|
144
|
+
const authColor = allAuth ? theme.success : theme.warning;
|
|
145
|
+
|
|
146
|
+
// Build status text
|
|
147
|
+
const statusParts = [
|
|
148
|
+
theme.primary(langStatus),
|
|
149
|
+
theme.dim('R:') + theme.secondary(reviewerStatus),
|
|
150
|
+
theme.dim('A:') + theme.secondary(arbitratorStatus),
|
|
151
|
+
authColor(authIcon),
|
|
152
|
+
];
|
|
153
|
+
const statusText = statusParts.join(theme.dim(' │ '));
|
|
154
|
+
|
|
155
|
+
// Calculate visible length (without ANSI codes)
|
|
156
|
+
// eslint-disable-next-line no-control-regex
|
|
157
|
+
const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
158
|
+
const statusLen = stripAnsi(statusText).length;
|
|
159
|
+
|
|
160
|
+
// Top line: ╭─ status ─────────────────────────────────────────╮
|
|
161
|
+
const paddingLen = Math.max(0, width - statusLen - 6);
|
|
162
|
+
console.log(
|
|
163
|
+
theme.dim(box.topLeft + box.horizontal + ' ') +
|
|
164
|
+
statusText +
|
|
165
|
+
theme.dim(' ' + box.horizontal.repeat(paddingLen) + box.topRight)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Draw bottom of input box after user presses enter
|
|
171
|
+
*/
|
|
172
|
+
function drawInputBoxBottom(): void {
|
|
173
|
+
const width = Math.min(getTerminalWidth(), 100);
|
|
174
|
+
console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clear screen and redraw UI
|
|
179
|
+
*/
|
|
180
|
+
function redrawUI(_state: SessionState): void {
|
|
181
|
+
console.clear();
|
|
182
|
+
drawHeader();
|
|
107
183
|
console.log();
|
|
184
|
+
}
|
|
108
185
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
authenticated: false,
|
|
116
|
-
};
|
|
186
|
+
/**
|
|
187
|
+
* Prompt for input with styled prompt (inside box)
|
|
188
|
+
*/
|
|
189
|
+
function getPrompt(): string {
|
|
190
|
+
return theme.dim(box.vertical + ' ') + theme.primary('popeye') + theme.dim(' > ');
|
|
191
|
+
}
|
|
117
192
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Prompt user to select an option
|
|
195
|
+
* Uses terminal: false to prevent echo issues when nested with main readline
|
|
196
|
+
*/
|
|
197
|
+
async function promptSelection(
|
|
198
|
+
question: string,
|
|
199
|
+
options: { label: string; value: string }[],
|
|
200
|
+
defaultValue: string
|
|
201
|
+
): Promise<string> {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const rl = readline.createInterface({
|
|
204
|
+
input: process.stdin,
|
|
205
|
+
output: process.stdout,
|
|
206
|
+
terminal: false, // Prevent terminal mode to avoid echo issues
|
|
207
|
+
});
|
|
121
208
|
|
|
122
|
-
if (!state.authenticated) {
|
|
123
|
-
printWarning('Not fully authenticated. Run /auth to check status.');
|
|
124
209
|
console.log();
|
|
125
|
-
}
|
|
210
|
+
console.log(theme.primary(` ${question}`));
|
|
211
|
+
options.forEach((opt, i) => {
|
|
212
|
+
const isDefault = opt.value === defaultValue;
|
|
213
|
+
console.log(` ${theme.dim(`${i + 1}.`)} ${opt.label}${isDefault ? theme.dim(' (default)') : ''}`);
|
|
214
|
+
});
|
|
215
|
+
console.log();
|
|
126
216
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
217
|
+
// Print prompt manually since terminal: false disables it
|
|
218
|
+
process.stdout.write(` Enter choice [1-${options.length}] or press Enter for default: `);
|
|
219
|
+
|
|
220
|
+
rl.once('line', (answer) => {
|
|
221
|
+
rl.close();
|
|
222
|
+
const trimmed = answer.trim();
|
|
223
|
+
if (!trimmed) {
|
|
224
|
+
resolve(defaultValue);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const num = parseInt(trimmed, 10);
|
|
228
|
+
if (num >= 1 && num <= options.length) {
|
|
229
|
+
resolve(options[num - 1].value);
|
|
230
|
+
} else {
|
|
231
|
+
resolve(defaultValue);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
132
234
|
});
|
|
235
|
+
}
|
|
133
236
|
|
|
134
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Prompt yes/no question
|
|
239
|
+
* Uses terminal: false to prevent echo issues when nested with main readline
|
|
240
|
+
*/
|
|
241
|
+
async function promptYesNo(question: string, defaultYes: boolean = true): Promise<boolean> {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const rl = readline.createInterface({
|
|
244
|
+
input: process.stdin,
|
|
245
|
+
output: process.stdout,
|
|
246
|
+
terminal: false, // Prevent terminal mode to avoid echo issues
|
|
247
|
+
});
|
|
135
248
|
|
|
136
|
-
|
|
137
|
-
|
|
249
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
250
|
+
process.stdout.write(` ${question} ${theme.dim(hint)} `);
|
|
138
251
|
|
|
139
|
-
|
|
140
|
-
rl.
|
|
141
|
-
|
|
252
|
+
rl.once('line', (answer) => {
|
|
253
|
+
rl.close();
|
|
254
|
+
const trimmed = answer.trim().toLowerCase();
|
|
255
|
+
if (!trimmed) {
|
|
256
|
+
resolve(defaultYes);
|
|
257
|
+
} else {
|
|
258
|
+
resolve(trimmed === 'y' || trimmed === 'yes');
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if reviewer/arbitrator has been configured (saved to config)
|
|
266
|
+
*/
|
|
267
|
+
async function isConsensusConfigured(): Promise<boolean> {
|
|
268
|
+
const config = await loadConfig();
|
|
269
|
+
// Consider configured if enable_arbitration is true or if arbitrator is explicitly set to a provider
|
|
270
|
+
// (Default is arbitrator='off' and enable_arbitration=false, so any change indicates user configured it)
|
|
271
|
+
return config.consensus.enable_arbitration || config.consensus.arbitrator !== 'off';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Save reviewer/arbitrator settings to config file
|
|
276
|
+
*/
|
|
277
|
+
async function saveConsensusConfig(state: SessionState): Promise<void> {
|
|
278
|
+
try {
|
|
279
|
+
// Load existing config and merge with new consensus settings
|
|
280
|
+
const existingConfig = await loadConfig();
|
|
281
|
+
const updatedConsensus = {
|
|
282
|
+
...existingConfig.consensus,
|
|
283
|
+
reviewer: state.reviewer,
|
|
284
|
+
arbitrator: state.enableArbitration ? state.arbitrator : 'off' as const,
|
|
285
|
+
enable_arbitration: state.enableArbitration,
|
|
286
|
+
};
|
|
287
|
+
await saveConfig({
|
|
288
|
+
consensus: updatedConsensus,
|
|
289
|
+
}, true); // Save to global config
|
|
290
|
+
} catch (err) {
|
|
291
|
+
printWarning(`Could not save config: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check and perform authentication
|
|
297
|
+
*/
|
|
298
|
+
async function ensureAuthentication(state: SessionState): Promise<boolean> {
|
|
299
|
+
const status = await getAuthStatusForDisplay();
|
|
300
|
+
state.claudeAuth = status.claude.authenticated;
|
|
301
|
+
state.openaiAuth = status.openai.authenticated;
|
|
302
|
+
state.geminiAuth = status.gemini?.authenticated || false;
|
|
303
|
+
|
|
304
|
+
console.log();
|
|
305
|
+
printInfo('Checking authentication...');
|
|
306
|
+
console.log();
|
|
307
|
+
|
|
308
|
+
// Authenticate Claude if needed
|
|
309
|
+
if (!state.claudeAuth) {
|
|
310
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude Code CLI') + theme.dim(' - Required for code generation'));
|
|
311
|
+
console.log(theme.dim(box.vertical));
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const success = await authenticateClaude();
|
|
315
|
+
if (success) {
|
|
316
|
+
printSuccess('Claude Code CLI ready');
|
|
317
|
+
state.claudeAuth = true;
|
|
318
|
+
} else {
|
|
319
|
+
printWarning('Claude Code CLI not authenticated - run "claude login" to authenticate');
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
142
323
|
}
|
|
324
|
+
console.log();
|
|
325
|
+
} else {
|
|
326
|
+
printSuccess('Claude Code CLI ready');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Authenticate OpenAI if needed
|
|
330
|
+
if (!state.openaiAuth) {
|
|
331
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - Required for consensus review'));
|
|
332
|
+
console.log(theme.dim(box.vertical));
|
|
143
333
|
|
|
144
334
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
335
|
+
const success = await authenticateOpenAI();
|
|
336
|
+
if (success) {
|
|
337
|
+
printSuccess('OpenAI API ready');
|
|
338
|
+
state.openaiAuth = true;
|
|
339
|
+
} else {
|
|
340
|
+
printWarning('OpenAI API not authenticated');
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
} else {
|
|
347
|
+
printSuccess('OpenAI API ready');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if reviewer/arbitrator is already configured
|
|
351
|
+
const alreadyConfigured = await isConsensusConfigured();
|
|
352
|
+
|
|
353
|
+
// Only ask about reviewer/arbitrator if not already configured
|
|
354
|
+
if (state.claudeAuth && state.openaiAuth && !alreadyConfigured) {
|
|
355
|
+
console.log();
|
|
356
|
+
console.log(theme.primary.bold(' AI Configuration'));
|
|
357
|
+
console.log(theme.dim(' Claude generates code. Choose who reviews and arbitrates:'));
|
|
358
|
+
|
|
359
|
+
// Ask who should review plans
|
|
360
|
+
state.reviewer = await promptSelection(
|
|
361
|
+
'Who should review Claude\'s plans?',
|
|
362
|
+
[
|
|
363
|
+
{ label: theme.secondary('OpenAI') + theme.dim(' - GPT-4o reviews plans'), value: 'openai' },
|
|
364
|
+
{ label: theme.secondary('Gemini') + theme.dim(' - Gemini 2.0 reviews plans'), value: 'gemini' },
|
|
365
|
+
],
|
|
366
|
+
'openai'
|
|
367
|
+
) as AIProvider;
|
|
368
|
+
|
|
369
|
+
// Ask about arbitration
|
|
370
|
+
console.log();
|
|
371
|
+
state.enableArbitration = await promptYesNo(
|
|
372
|
+
theme.primary('Enable arbitration when consensus is stuck?'),
|
|
373
|
+
true
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (state.enableArbitration) {
|
|
377
|
+
// Auto-select the other provider as arbitrator
|
|
378
|
+
const defaultArbitrator = state.reviewer === 'openai' ? 'gemini' : 'openai';
|
|
379
|
+
|
|
380
|
+
state.arbitrator = await promptSelection(
|
|
381
|
+
'Who should arbitrate when stuck?',
|
|
382
|
+
[
|
|
383
|
+
{ label: theme.secondary('Gemini') + theme.dim(' - Google Gemini breaks deadlocks'), value: 'gemini' },
|
|
384
|
+
{ label: theme.secondary('OpenAI') + theme.dim(' - OpenAI breaks deadlocks'), value: 'openai' },
|
|
385
|
+
],
|
|
386
|
+
defaultArbitrator
|
|
387
|
+
) as AIProvider;
|
|
388
|
+
|
|
389
|
+
// Authenticate Gemini if needed for reviewer or arbitrator
|
|
390
|
+
const needsGemini = state.reviewer === 'gemini' || state.arbitrator === 'gemini';
|
|
391
|
+
if (needsGemini && !state.geminiAuth) {
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
|
|
394
|
+
console.log(theme.dim(box.vertical));
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const success = await authenticateGemini();
|
|
398
|
+
if (success) {
|
|
399
|
+
printSuccess('Gemini API ready');
|
|
400
|
+
state.geminiAuth = true;
|
|
401
|
+
} else {
|
|
402
|
+
printWarning('Gemini API not authenticated - arbitration disabled');
|
|
403
|
+
state.enableArbitration = false;
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
407
|
+
state.enableArbitration = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
149
411
|
|
|
150
|
-
|
|
151
|
-
|
|
412
|
+
// Also check if reviewer is gemini and we need to auth
|
|
413
|
+
if (state.reviewer === 'gemini' && !state.geminiAuth) {
|
|
414
|
+
console.log();
|
|
415
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for review'));
|
|
416
|
+
console.log(theme.dim(box.vertical));
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const success = await authenticateGemini();
|
|
420
|
+
if (success) {
|
|
421
|
+
printSuccess('Gemini API ready');
|
|
422
|
+
state.geminiAuth = true;
|
|
152
423
|
} else {
|
|
153
|
-
|
|
424
|
+
printWarning('Gemini API not authenticated - falling back to OpenAI');
|
|
425
|
+
state.reviewer = 'openai';
|
|
154
426
|
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
427
|
+
} catch (err) {
|
|
428
|
+
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
429
|
+
state.reviewer = 'openai';
|
|
158
430
|
}
|
|
159
|
-
} catch (error) {
|
|
160
|
-
printError(error instanceof Error ? error.message : 'Unknown error');
|
|
161
431
|
}
|
|
162
432
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
433
|
+
// Save the configuration to persist between sessions
|
|
434
|
+
await saveConsensusConfig(state);
|
|
166
435
|
|
|
167
|
-
|
|
436
|
+
// Show summary
|
|
168
437
|
console.log();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
438
|
+
console.log(theme.secondary(' Configuration saved. Use /config to change later.'));
|
|
439
|
+
console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
|
|
440
|
+
console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
|
|
441
|
+
console.log();
|
|
442
|
+
} else if (state.claudeAuth && state.openaiAuth && alreadyConfigured) {
|
|
443
|
+
// Show loaded configuration
|
|
444
|
+
console.log();
|
|
445
|
+
console.log(theme.secondary(' Using saved configuration (use /config to change):'));
|
|
446
|
+
console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
|
|
447
|
+
console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
|
|
448
|
+
console.log();
|
|
449
|
+
|
|
450
|
+
// Authenticate Gemini if needed based on saved config
|
|
451
|
+
const needsGemini = state.reviewer === 'gemini' || (state.enableArbitration && state.arbitrator === 'gemini');
|
|
452
|
+
if (needsGemini && !state.geminiAuth) {
|
|
453
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
|
|
454
|
+
console.log(theme.dim(box.vertical));
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const success = await authenticateGemini();
|
|
458
|
+
if (success) {
|
|
459
|
+
printSuccess('Gemini API ready');
|
|
460
|
+
state.geminiAuth = true;
|
|
461
|
+
} else {
|
|
462
|
+
printWarning('Gemini API not authenticated');
|
|
463
|
+
if (state.reviewer === 'gemini') {
|
|
464
|
+
printWarning('Falling back to OpenAI as reviewer');
|
|
465
|
+
state.reviewer = 'openai';
|
|
466
|
+
}
|
|
467
|
+
if (state.enableArbitration && state.arbitrator === 'gemini') {
|
|
468
|
+
printWarning('Disabling arbitration');
|
|
469
|
+
state.enableArbitration = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch (err) {
|
|
473
|
+
printError(err instanceof Error ? err.message : 'Gemini authentication failed');
|
|
474
|
+
}
|
|
475
|
+
console.log();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return state.claudeAuth && state.openaiAuth;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Display help
|
|
484
|
+
*/
|
|
485
|
+
function showHelp(): void {
|
|
486
|
+
console.log();
|
|
487
|
+
console.log(theme.primary.bold(' Commands:'));
|
|
488
|
+
console.log();
|
|
489
|
+
|
|
490
|
+
const commands = [
|
|
491
|
+
['/help', 'Show this help message'],
|
|
492
|
+
['/info', 'Show system info (Claude CLI status, etc.)'],
|
|
493
|
+
['/status', 'Show current project status'],
|
|
494
|
+
['/auth', 'Re-authenticate services'],
|
|
495
|
+
['/config', 'Show/change configuration'],
|
|
496
|
+
['/config reviewer', 'Set reviewer (openai/gemini)'],
|
|
497
|
+
['/config arbitrator', 'Set arbitrator (openai/gemini/off)'],
|
|
498
|
+
['/lang <lang>', 'Set language (python/typescript)'],
|
|
499
|
+
['/new <idea>', 'Force start a new project (skips existing check)'],
|
|
500
|
+
['/resume', 'Resume interrupted project'],
|
|
501
|
+
['/clear', 'Clear screen'],
|
|
502
|
+
['/exit', 'Exit Popeye'],
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
for (const [cmd, desc] of commands) {
|
|
506
|
+
console.log(` ${theme.primary(cmd.padEnd(20))} ${theme.dim(desc)}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
console.log();
|
|
510
|
+
console.log(theme.secondary(' Type your project idea to get started!'));
|
|
511
|
+
console.log(theme.secondary(' Example: "A REST API for managing todo items"'));
|
|
512
|
+
console.log();
|
|
172
513
|
}
|
|
173
514
|
|
|
174
515
|
/**
|
|
175
|
-
* Handle /
|
|
516
|
+
* Handle /info command - show system info
|
|
176
517
|
*/
|
|
177
|
-
async function
|
|
178
|
-
|
|
518
|
+
async function handleInfo(): Promise<void> {
|
|
519
|
+
console.log();
|
|
520
|
+
console.log(theme.primary.bold(' System Info:'));
|
|
521
|
+
console.log();
|
|
522
|
+
|
|
523
|
+
// Check Claude CLI
|
|
524
|
+
const claudeInstalled = await isClaudeCLIInstalled();
|
|
525
|
+
const claudeStatus = await checkClaudeCLIAuth();
|
|
526
|
+
|
|
527
|
+
console.log(theme.secondary(' Claude Code:'));
|
|
528
|
+
console.log(` ${theme.dim('Installed:')} ${claudeInstalled ? theme.success('Yes') : theme.error('No')}`);
|
|
529
|
+
|
|
530
|
+
if (claudeInstalled) {
|
|
531
|
+
console.log(` ${theme.dim('Authenticated:')} ${claudeStatus.authenticated ? theme.success('Yes') : theme.warning('No')}`);
|
|
532
|
+
console.log(` ${theme.dim('Model:')} ${theme.primary('Uses your Claude Code settings')}`);
|
|
533
|
+
console.log(` ${theme.dim('MCPs:')} ${theme.primary('Uses your configured MCP servers')}`);
|
|
534
|
+
|
|
535
|
+
if (!claudeStatus.authenticated) {
|
|
536
|
+
console.log();
|
|
537
|
+
console.log(` ${theme.warning('Run:')} ${theme.primary('claude login')} ${theme.warning('to authenticate')}`);
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
console.log();
|
|
541
|
+
console.log(` ${theme.warning('Install:')} ${theme.primary('npm install -g @anthropic-ai/claude-code')}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
console.log();
|
|
545
|
+
console.log(theme.secondary(' OpenAI:'));
|
|
546
|
+
const authStatus = await getAuthStatusForDisplay();
|
|
547
|
+
console.log(` ${theme.dim('Authenticated:')} ${authStatus.openai.authenticated ? theme.success('Yes') : theme.warning('No')}`);
|
|
548
|
+
if (authStatus.openai.authenticated && authStatus.openai.keyLastFour) {
|
|
549
|
+
console.log(` ${theme.dim('API Key:')} ${theme.dim(authStatus.openai.keyLastFour)}`);
|
|
550
|
+
}
|
|
179
551
|
|
|
180
|
-
|
|
181
|
-
|
|
552
|
+
console.log();
|
|
553
|
+
console.log(theme.secondary(' Gemini:'));
|
|
554
|
+
const geminiStatus = await checkGeminiAuth();
|
|
555
|
+
console.log(` ${theme.dim('Authenticated:')} ${geminiStatus.authenticated ? theme.success('Yes') : theme.dim('No')}`);
|
|
556
|
+
if (geminiStatus.authenticated && geminiStatus.keyLastFour) {
|
|
557
|
+
console.log(` ${theme.dim('API Key:')} ${theme.dim(geminiStatus.keyLastFour)}`);
|
|
182
558
|
}
|
|
183
559
|
|
|
184
560
|
console.log();
|
|
185
|
-
|
|
561
|
+
console.log(theme.secondary(' Environment:'));
|
|
562
|
+
console.log(` ${theme.dim('Node.js:')} ${process.version}`);
|
|
563
|
+
console.log(` ${theme.dim('Platform:')} ${process.platform}`);
|
|
564
|
+
console.log(` ${theme.dim('Working Dir:')} ${process.cwd()}`);
|
|
565
|
+
console.log();
|
|
566
|
+
|
|
567
|
+
console.log(theme.dim(' Tip: Claude Code model and MCP settings are configured in your'));
|
|
568
|
+
console.log(theme.dim(' Claude Code CLI. Run "claude config" to see/change them.'));
|
|
569
|
+
console.log();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Handle a command or idea
|
|
574
|
+
*/
|
|
575
|
+
async function handleInput(input: string, state: SessionState): Promise<boolean> {
|
|
576
|
+
const trimmed = input.trim();
|
|
577
|
+
|
|
578
|
+
if (!trimmed) return true;
|
|
579
|
+
|
|
580
|
+
// Check for common words that should be commands (without /)
|
|
581
|
+
const lowerTrimmed = trimmed.toLowerCase();
|
|
582
|
+
if (['help', 'exit', 'quit', 'info', 'status', 'config'].includes(lowerTrimmed)) {
|
|
583
|
+
printWarning(`Did you mean /${lowerTrimmed}? Use / prefix for commands.`);
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Handle commands
|
|
588
|
+
if (trimmed.startsWith('/')) {
|
|
589
|
+
const [cmd, ...args] = trimmed.split(/\s+/);
|
|
590
|
+
const command = cmd.toLowerCase();
|
|
591
|
+
|
|
592
|
+
switch (command) {
|
|
593
|
+
case '/help':
|
|
594
|
+
case '/h':
|
|
595
|
+
case '/?':
|
|
596
|
+
showHelp();
|
|
597
|
+
break;
|
|
598
|
+
|
|
599
|
+
case '/info':
|
|
600
|
+
case '/check':
|
|
601
|
+
await handleInfo();
|
|
602
|
+
break;
|
|
603
|
+
|
|
604
|
+
case '/exit':
|
|
605
|
+
case '/quit':
|
|
606
|
+
case '/q':
|
|
607
|
+
console.log();
|
|
608
|
+
printInfo('Goodbye!');
|
|
609
|
+
return false;
|
|
610
|
+
|
|
611
|
+
case '/clear':
|
|
612
|
+
case '/cls':
|
|
613
|
+
redrawUI(state);
|
|
614
|
+
break;
|
|
615
|
+
|
|
616
|
+
case '/status':
|
|
617
|
+
await handleStatus(state);
|
|
618
|
+
break;
|
|
619
|
+
|
|
620
|
+
case '/auth':
|
|
621
|
+
await ensureAuthentication(state);
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
case '/config':
|
|
625
|
+
await handleConfig(state, args);
|
|
626
|
+
break;
|
|
627
|
+
|
|
628
|
+
case '/language':
|
|
629
|
+
case '/lang':
|
|
630
|
+
case '/l':
|
|
631
|
+
handleLanguage(args, state);
|
|
632
|
+
break;
|
|
633
|
+
|
|
634
|
+
case '/model':
|
|
635
|
+
case '/m':
|
|
636
|
+
handleModel(args, state);
|
|
637
|
+
break;
|
|
638
|
+
|
|
639
|
+
case '/resume':
|
|
640
|
+
await handleResume(state, args);
|
|
641
|
+
break;
|
|
642
|
+
|
|
643
|
+
case '/new':
|
|
644
|
+
// Force start a new project even if existing projects found
|
|
645
|
+
if (args.length === 0) {
|
|
646
|
+
printError('Usage: /new <project idea>');
|
|
647
|
+
printInfo('Example: /new todo app with user authentication');
|
|
648
|
+
} else {
|
|
649
|
+
await handleNewProject(args.join(' '), state);
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
|
|
653
|
+
default:
|
|
654
|
+
printError(`Unknown command: ${cmd}`);
|
|
655
|
+
printInfo('Type /help for available commands');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Warn if input is too short (likely accidental)
|
|
662
|
+
if (trimmed.length < 10) {
|
|
663
|
+
printWarning(`Input "${trimmed}" is very short. Did you mean to type a command?`);
|
|
664
|
+
printInfo('Type /help for commands, or enter a longer project description.');
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Handle as project idea
|
|
669
|
+
await handleIdea(trimmed, state);
|
|
670
|
+
return true;
|
|
186
671
|
}
|
|
187
672
|
|
|
188
673
|
/**
|
|
189
674
|
* Handle /status command
|
|
190
675
|
*/
|
|
191
|
-
async function handleStatus(
|
|
676
|
+
async function handleStatus(state: SessionState): Promise<void> {
|
|
677
|
+
console.log();
|
|
678
|
+
|
|
192
679
|
if (!state.projectDir) {
|
|
193
|
-
printInfo('No
|
|
680
|
+
printInfo('No active project');
|
|
194
681
|
return;
|
|
195
682
|
}
|
|
196
683
|
|
|
@@ -199,8 +686,6 @@ async function handleStatus(_args: string[], state: SessionState): Promise<void>
|
|
|
199
686
|
if (!status.exists) {
|
|
200
687
|
printInfo('No project found in current directory');
|
|
201
688
|
printKeyValue('Directory', state.projectDir);
|
|
202
|
-
printKeyValue('Language', state.language);
|
|
203
|
-
printKeyValue('Model', state.model);
|
|
204
689
|
return;
|
|
205
690
|
}
|
|
206
691
|
|
|
@@ -209,42 +694,118 @@ async function handleStatus(_args: string[], state: SessionState): Promise<void>
|
|
|
209
694
|
}
|
|
210
695
|
|
|
211
696
|
/**
|
|
212
|
-
* Handle /
|
|
697
|
+
* Handle /config command
|
|
213
698
|
*/
|
|
214
|
-
async function
|
|
215
|
-
const
|
|
216
|
-
printAuthStatus(status);
|
|
699
|
+
async function handleConfig(state: SessionState, args: string[] = []): Promise<void> {
|
|
700
|
+
const config = await loadConfig();
|
|
217
701
|
|
|
218
|
-
|
|
702
|
+
// Handle config subcommands
|
|
703
|
+
if (args.length > 0) {
|
|
704
|
+
const subcommand = args[0].toLowerCase();
|
|
219
705
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
706
|
+
switch (subcommand) {
|
|
707
|
+
case 'reviewer':
|
|
708
|
+
if (args.length > 1) {
|
|
709
|
+
const newReviewer = args[1].toLowerCase();
|
|
710
|
+
if (newReviewer === 'openai' || newReviewer === 'gemini') {
|
|
711
|
+
if (newReviewer === 'gemini' && !state.geminiAuth) {
|
|
712
|
+
printWarning('Gemini API not authenticated. Run /auth first.');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
state.reviewer = newReviewer as AIProvider;
|
|
716
|
+
// Save to config
|
|
717
|
+
await saveConsensusConfig(state);
|
|
718
|
+
printSuccess(`Reviewer set to ${newReviewer}`);
|
|
719
|
+
} else {
|
|
720
|
+
printError('Invalid reviewer. Use: openai or gemini');
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
printKeyValue('Reviewer', state.reviewer);
|
|
724
|
+
printInfo('Use: /config reviewer <openai|gemini>');
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
225
727
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
728
|
+
case 'arbitrator':
|
|
729
|
+
if (args.length > 1) {
|
|
730
|
+
const newArbitrator = args[1].toLowerCase();
|
|
731
|
+
if (newArbitrator === 'openai' || newArbitrator === 'gemini') {
|
|
732
|
+
if (newArbitrator === 'gemini' && !state.geminiAuth) {
|
|
733
|
+
printWarning('Gemini API not authenticated. Run /auth first.');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
state.arbitrator = newArbitrator as AIProvider;
|
|
737
|
+
state.enableArbitration = true;
|
|
738
|
+
// Save to config
|
|
739
|
+
await saveConsensusConfig(state);
|
|
740
|
+
printSuccess(`Arbitrator set to ${newArbitrator}`);
|
|
741
|
+
} else if (newArbitrator === 'off' || newArbitrator === 'none') {
|
|
742
|
+
state.enableArbitration = false;
|
|
743
|
+
// Save to config
|
|
744
|
+
await saveConsensusConfig(state);
|
|
745
|
+
printSuccess('Arbitration disabled');
|
|
746
|
+
} else {
|
|
747
|
+
printError('Invalid arbitrator. Use: openai, gemini, or off');
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
printKeyValue('Arbitrator', state.enableArbitration ? state.arbitrator : 'disabled');
|
|
751
|
+
printInfo('Use: /config arbitrator <openai|gemini|off>');
|
|
752
|
+
}
|
|
753
|
+
return;
|
|
754
|
+
|
|
755
|
+
case 'language':
|
|
756
|
+
case 'lang':
|
|
757
|
+
if (args.length > 1) {
|
|
758
|
+
const lang = args[1].toLowerCase() as OutputLanguage;
|
|
759
|
+
if (['python', 'typescript'].includes(lang)) {
|
|
760
|
+
state.language = lang;
|
|
761
|
+
printSuccess(`Language set to ${lang}`);
|
|
762
|
+
} else {
|
|
763
|
+
printError('Invalid language. Use: python or typescript');
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
printKeyValue('Language', state.language);
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
231
769
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
770
|
+
default:
|
|
771
|
+
printError(`Unknown config option: ${subcommand}`);
|
|
772
|
+
printInfo('Options: reviewer, arbitrator, language');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
237
776
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
777
|
+
// Show full config
|
|
778
|
+
console.log();
|
|
779
|
+
console.log(theme.primary.bold(' Session:'));
|
|
780
|
+
console.log(` ${theme.dim('Directory:')} ${state.projectDir || 'Not set'}`);
|
|
781
|
+
console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
|
|
782
|
+
console.log();
|
|
783
|
+
console.log(theme.primary.bold(' Authentication:'));
|
|
784
|
+
console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
|
|
785
|
+
console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
|
|
786
|
+
console.log(` ${theme.dim('Gemini:')} ${state.geminiAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
|
|
787
|
+
console.log();
|
|
788
|
+
console.log(theme.primary.bold(' AI Configuration:'));
|
|
789
|
+
console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
|
|
790
|
+
console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
|
|
791
|
+
console.log();
|
|
792
|
+
console.log(theme.primary.bold(' Consensus:'));
|
|
793
|
+
console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
|
|
794
|
+
console.log(` ${theme.dim('Max Iters:')} ${config.consensus.max_disagreements}`);
|
|
795
|
+
console.log();
|
|
796
|
+
console.log(theme.secondary(' Change settings:'));
|
|
797
|
+
console.log(theme.dim(' /config reviewer <openai|gemini>'));
|
|
798
|
+
console.log(theme.dim(' /config arbitrator <openai|gemini|off>'));
|
|
799
|
+
console.log(theme.dim(' /config language <python|typescript>'));
|
|
800
|
+
console.log();
|
|
241
801
|
}
|
|
242
802
|
|
|
243
803
|
/**
|
|
244
804
|
* Handle /language command
|
|
245
805
|
*/
|
|
246
|
-
|
|
806
|
+
function handleLanguage(args: string[], state: SessionState): void {
|
|
247
807
|
if (args.length === 0) {
|
|
808
|
+
console.log();
|
|
248
809
|
printKeyValue('Current language', state.language);
|
|
249
810
|
printInfo('Use /language <python|typescript> to change');
|
|
250
811
|
return;
|
|
@@ -257,18 +818,20 @@ async function handleLanguage(args: string[], state: SessionState): Promise<void
|
|
|
257
818
|
}
|
|
258
819
|
|
|
259
820
|
state.language = lang;
|
|
260
|
-
|
|
821
|
+
console.log();
|
|
822
|
+
printSuccess(`Language set to ${lang}`);
|
|
261
823
|
}
|
|
262
824
|
|
|
263
825
|
/**
|
|
264
826
|
* Handle /model command
|
|
265
827
|
*/
|
|
266
|
-
|
|
828
|
+
function handleModel(args: string[], state: SessionState): void {
|
|
267
829
|
const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
|
|
268
830
|
|
|
269
831
|
if (args.length === 0) {
|
|
832
|
+
console.log();
|
|
270
833
|
printKeyValue('Current model', state.model);
|
|
271
|
-
printInfo(`
|
|
834
|
+
printInfo(`Available: ${validModels.join(', ')}`);
|
|
272
835
|
return;
|
|
273
836
|
}
|
|
274
837
|
|
|
@@ -279,116 +842,793 @@ async function handleModel(args: string[], state: SessionState): Promise<void> {
|
|
|
279
842
|
}
|
|
280
843
|
|
|
281
844
|
state.model = model;
|
|
282
|
-
|
|
845
|
+
console.log();
|
|
846
|
+
printSuccess(`Model set to ${model}`);
|
|
283
847
|
}
|
|
284
848
|
|
|
285
849
|
/**
|
|
286
|
-
*
|
|
850
|
+
* Prompt for additional context
|
|
851
|
+
* Uses terminal: false to prevent echo issues when nested with main readline
|
|
287
852
|
*/
|
|
288
|
-
async function
|
|
289
|
-
|
|
853
|
+
async function promptForContext(prompt: string): Promise<string> {
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
const rl = readline.createInterface({
|
|
856
|
+
input: process.stdin,
|
|
857
|
+
output: process.stdout,
|
|
858
|
+
terminal: false, // Prevent terminal mode to avoid echo issues
|
|
859
|
+
});
|
|
290
860
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
861
|
+
console.log();
|
|
862
|
+
console.log(theme.primary(` ${prompt}`));
|
|
863
|
+
console.log(theme.dim(' (Press Enter to skip, or type your guidance)'));
|
|
864
|
+
console.log();
|
|
296
865
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
866
|
+
process.stdout.write(' > ');
|
|
867
|
+
|
|
868
|
+
rl.once('line', (answer) => {
|
|
869
|
+
rl.close();
|
|
870
|
+
resolve(answer.trim());
|
|
871
|
+
});
|
|
872
|
+
});
|
|
300
873
|
}
|
|
301
874
|
|
|
302
875
|
/**
|
|
303
|
-
*
|
|
876
|
+
* Discovered project context from docs/ folder
|
|
304
877
|
*/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
878
|
+
interface DiscoveredProject {
|
|
879
|
+
found: boolean;
|
|
880
|
+
name?: string;
|
|
881
|
+
idea?: string;
|
|
882
|
+
plan?: string;
|
|
883
|
+
planFile?: string;
|
|
884
|
+
readme?: string;
|
|
885
|
+
language?: OutputLanguage;
|
|
886
|
+
hasCode: boolean;
|
|
887
|
+
codeFiles: string[];
|
|
888
|
+
}
|
|
310
889
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
890
|
+
/**
|
|
891
|
+
* Discover project context from docs/ and codebase
|
|
892
|
+
*/
|
|
893
|
+
async function discoverProjectContext(projectDir: string): Promise<DiscoveredProject> {
|
|
894
|
+
const result: DiscoveredProject = {
|
|
895
|
+
found: false,
|
|
896
|
+
hasCode: false,
|
|
897
|
+
codeFiles: [],
|
|
898
|
+
};
|
|
315
899
|
|
|
316
|
-
const
|
|
900
|
+
const docsDir = path.join(projectDir, 'docs');
|
|
317
901
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
902
|
+
// Try to read plan files
|
|
903
|
+
const planFiles = ['PLAN.md', 'PLAN-DRAFT.md'];
|
|
904
|
+
for (const planFile of planFiles) {
|
|
905
|
+
try {
|
|
906
|
+
const planPath = path.join(docsDir, planFile);
|
|
907
|
+
const content = await fs.readFile(planPath, 'utf-8');
|
|
908
|
+
result.plan = content;
|
|
909
|
+
result.planFile = planFile;
|
|
910
|
+
result.found = true;
|
|
911
|
+
|
|
912
|
+
// Try to extract project name from plan
|
|
913
|
+
const nameMatch = content.match(/^#\s*(?:Development Plan|Project):\s*(.+)$/mi) ||
|
|
914
|
+
content.match(/^#\s*(.+)$/m);
|
|
915
|
+
if (nameMatch) {
|
|
916
|
+
result.name = nameMatch[1].replace(/Development Plan/i, '').trim();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Try to extract idea/overview from plan
|
|
920
|
+
const overviewMatch = content.match(/(?:Overview|Summary|Description)[:\s]*\n+([\s\S]*?)(?=\n#|\n\*\*|$)/i);
|
|
921
|
+
if (overviewMatch) {
|
|
922
|
+
result.idea = overviewMatch[1].trim().slice(0, 500);
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
} catch {
|
|
926
|
+
// File doesn't exist, continue
|
|
927
|
+
}
|
|
321
928
|
}
|
|
322
929
|
|
|
323
|
-
|
|
324
|
-
|
|
930
|
+
// Try to read README
|
|
931
|
+
try {
|
|
932
|
+
const readmePath = path.join(projectDir, 'README.md');
|
|
933
|
+
const content = await fs.readFile(readmePath, 'utf-8');
|
|
934
|
+
result.readme = content;
|
|
935
|
+
result.found = true;
|
|
325
936
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
937
|
+
// Extract project name from README if not already found
|
|
938
|
+
if (!result.name) {
|
|
939
|
+
const nameMatch = content.match(/^#\s*(.+)$/m);
|
|
940
|
+
if (nameMatch) {
|
|
941
|
+
result.name = nameMatch[1].trim();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
331
944
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
945
|
+
// Extract idea from README if not found in plan
|
|
946
|
+
if (!result.idea) {
|
|
947
|
+
const lines = content.split('\n').slice(1, 10).join('\n').trim();
|
|
948
|
+
if (lines.length > 20) {
|
|
949
|
+
result.idea = lines.slice(0, 500);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
// README doesn't exist
|
|
336
954
|
}
|
|
337
|
-
}
|
|
338
955
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
956
|
+
// Scan for code files to detect language
|
|
957
|
+
try {
|
|
958
|
+
const files = await fs.readdir(projectDir, { recursive: true });
|
|
959
|
+
const codeExtensions = {
|
|
960
|
+
python: ['.py'],
|
|
961
|
+
typescript: ['.ts', '.tsx', '.js', '.jsx'],
|
|
962
|
+
};
|
|
345
963
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
*/
|
|
349
|
-
async function handleExit(_args: string[], _state: SessionState): Promise<void> {
|
|
350
|
-
printInfo('Goodbye!');
|
|
351
|
-
process.exit(0);
|
|
352
|
-
}
|
|
964
|
+
let pyCount = 0;
|
|
965
|
+
let tsCount = 0;
|
|
353
966
|
|
|
354
|
-
|
|
355
|
-
|
|
967
|
+
for (const file of files) {
|
|
968
|
+
const fileName = String(file);
|
|
969
|
+
if (fileName.includes('node_modules') || fileName.includes('.git') || fileName.includes('__pycache__')) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (codeExtensions.python.some(ext => fileName.endsWith(ext))) {
|
|
974
|
+
pyCount++;
|
|
975
|
+
result.codeFiles.push(fileName);
|
|
976
|
+
}
|
|
977
|
+
if (codeExtensions.typescript.some(ext => fileName.endsWith(ext))) {
|
|
978
|
+
tsCount++;
|
|
979
|
+
result.codeFiles.push(fileName);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
result.hasCode = result.codeFiles.length > 0;
|
|
984
|
+
|
|
985
|
+
// Determine language from code files
|
|
986
|
+
if (pyCount > tsCount) {
|
|
987
|
+
result.language = 'python';
|
|
988
|
+
} else if (tsCount > 0) {
|
|
989
|
+
result.language = 'typescript';
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
// Can't read directory
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return result;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Handle /resume command
|
|
356
1000
|
*/
|
|
357
|
-
async function
|
|
358
|
-
if (!state.
|
|
359
|
-
printError('
|
|
1001
|
+
async function handleResume(state: SessionState, args: string[]): Promise<void> {
|
|
1002
|
+
if (!state.claudeAuth || !state.openaiAuth) {
|
|
1003
|
+
printError('Authentication required. Run /auth first.');
|
|
360
1004
|
return;
|
|
361
1005
|
}
|
|
362
1006
|
|
|
363
|
-
|
|
364
|
-
printKeyValue('Idea', idea);
|
|
365
|
-
printKeyValue('Language', state.language);
|
|
366
|
-
printKeyValue('Model', state.model);
|
|
1007
|
+
// Discover all projects (registered + scanned in current directory)
|
|
367
1008
|
console.log();
|
|
1009
|
+
printInfo('Scanning for projects...');
|
|
1010
|
+
|
|
1011
|
+
const { all: allProjects } = await discoverProjects(state.projectDir || process.cwd());
|
|
1012
|
+
|
|
1013
|
+
// If projects found, let user select one
|
|
1014
|
+
if (allProjects.length > 0) {
|
|
1015
|
+
console.log();
|
|
1016
|
+
console.log(theme.primary.bold(' Found Projects:'));
|
|
1017
|
+
console.log();
|
|
1018
|
+
|
|
1019
|
+
// Show project list with numbers
|
|
1020
|
+
const displayProjects = allProjects.slice(0, 10); // Limit to 10
|
|
1021
|
+
for (let i = 0; i < displayProjects.length; i++) {
|
|
1022
|
+
const project = displayProjects[i];
|
|
1023
|
+
const info = formatProjectForDisplay(project);
|
|
1024
|
+
const statusColor = project.status === 'complete' ? theme.success :
|
|
1025
|
+
project.status === 'failed' ? theme.error :
|
|
1026
|
+
project.status === 'in-progress' ? theme.warning : theme.dim;
|
|
1027
|
+
|
|
1028
|
+
console.log(` ${theme.primary(`${i + 1}.`)} ${theme.secondary(info.name)}`);
|
|
1029
|
+
console.log(` ${statusColor(info.status)} ${theme.dim('|')} ${info.age}`);
|
|
1030
|
+
console.log(` ${theme.dim(info.path)}`);
|
|
1031
|
+
if (project.idea) {
|
|
1032
|
+
console.log(` ${theme.dim(project.idea.slice(0, 60))}${project.idea.length > 60 ? '...' : ''}`);
|
|
1033
|
+
}
|
|
1034
|
+
console.log();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (allProjects.length > 10) {
|
|
1038
|
+
console.log(theme.dim(` ... and ${allProjects.length - 10} more projects`));
|
|
1039
|
+
console.log();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Let user select
|
|
1043
|
+
const selection = await promptSelection(
|
|
1044
|
+
'Select a project to resume:',
|
|
1045
|
+
[
|
|
1046
|
+
...displayProjects.map((p, i) => ({
|
|
1047
|
+
value: String(i),
|
|
1048
|
+
label: `${p.name} (${formatProjectForDisplay(p).age})`,
|
|
1049
|
+
})),
|
|
1050
|
+
{ value: 'scan', label: 'Scan for more projects...' },
|
|
1051
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
1052
|
+
],
|
|
1053
|
+
'0'
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
if (selection === 'cancel') {
|
|
1057
|
+
printInfo('Cancelled');
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (selection === 'scan') {
|
|
1062
|
+
// Scan deeper in current directory
|
|
1063
|
+
printInfo('Scanning subdirectories...');
|
|
1064
|
+
const { all: deepScan } = await discoverProjects(state.projectDir || process.cwd());
|
|
1065
|
+
if (deepScan.length === allProjects.length) {
|
|
1066
|
+
printWarning('No additional projects found');
|
|
1067
|
+
} else {
|
|
1068
|
+
printSuccess(`Found ${deepScan.length - allProjects.length} additional projects`);
|
|
1069
|
+
}
|
|
1070
|
+
// Recursively call handleResume to show updated list
|
|
1071
|
+
await handleResume(state, args);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const selectedIndex = parseInt(selection, 10);
|
|
1076
|
+
const selectedProject = displayProjects[selectedIndex];
|
|
1077
|
+
|
|
1078
|
+
if (!selectedProject) {
|
|
1079
|
+
printError('Invalid selection');
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Set the project directory and continue
|
|
1084
|
+
state.projectDir = selectedProject.path;
|
|
1085
|
+
console.log();
|
|
1086
|
+
printInfo(`Selected: ${selectedProject.name}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Now check for formal project state at the selected/current directory
|
|
1090
|
+
if (!state.projectDir) {
|
|
1091
|
+
printError('No project directory set');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const status = await getWorkflowStatus(state.projectDir);
|
|
1096
|
+
|
|
1097
|
+
if (status.exists && status.state) {
|
|
1098
|
+
// Formal project state exists - analyze actual progress before resuming
|
|
1099
|
+
// Update session state to reflect project's language (preserves language on resume)
|
|
1100
|
+
state.language = status.state.language;
|
|
1101
|
+
|
|
1102
|
+
// Get detailed progress analysis
|
|
1103
|
+
const progressAnalysis = await analyzeProjectProgress(state.projectDir);
|
|
1104
|
+
const verification = await verifyProjectCompletion(state.projectDir);
|
|
1105
|
+
|
|
1106
|
+
console.log();
|
|
1107
|
+
console.log(theme.primary.bold(' Project Status:'));
|
|
1108
|
+
console.log(` ${theme.dim('Name:')} ${status.state.name}`);
|
|
1109
|
+
console.log(` ${theme.dim('Language:')} ${theme.primary(status.state.language)}`);
|
|
1110
|
+
console.log(` ${theme.dim('Phase:')} ${theme.primary(status.state.phase)}`);
|
|
1111
|
+
console.log(` ${theme.dim('Status:')} ${status.state.status}`);
|
|
1112
|
+
|
|
1113
|
+
// Show detailed progress comparison
|
|
1114
|
+
console.log();
|
|
1115
|
+
console.log(theme.primary.bold(' Progress Analysis:'));
|
|
1116
|
+
console.log(` ${theme.dim('Milestones:')} ${progressAnalysis.completedMilestones}/${progressAnalysis.totalMilestones} complete`);
|
|
1117
|
+
console.log(` ${theme.dim('Tasks:')} ${progressAnalysis.completedTasks}/${progressAnalysis.totalTasks} complete (${progressAnalysis.percentComplete}%)`);
|
|
1118
|
+
|
|
1119
|
+
if (progressAnalysis.inProgressTasks > 0) {
|
|
1120
|
+
console.log(` ${theme.dim('In Progress:')} ${theme.warning(String(progressAnalysis.inProgressTasks))} task(s)`);
|
|
1121
|
+
}
|
|
1122
|
+
if (progressAnalysis.failedTasks > 0) {
|
|
1123
|
+
console.log(` ${theme.dim('Failed:')} ${theme.error(String(progressAnalysis.failedTasks))} task(s)`);
|
|
1124
|
+
}
|
|
1125
|
+
if (progressAnalysis.pendingTasks > 0) {
|
|
1126
|
+
console.log(` ${theme.dim('Pending:')} ${progressAnalysis.pendingTasks} task(s)`);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Show plan file comparison
|
|
1130
|
+
if (progressAnalysis.planTaskCount > 0) {
|
|
1131
|
+
console.log();
|
|
1132
|
+
console.log(theme.primary.bold(' Plan Comparison (from docs/PLAN.md):'));
|
|
1133
|
+
console.log(` ${theme.dim('Plan Tasks:')} ${progressAnalysis.planTaskCount} tasks found in plan`);
|
|
1134
|
+
console.log(` ${theme.dim('State Tasks:')} ${progressAnalysis.totalTasks} tasks in state`);
|
|
1135
|
+
|
|
1136
|
+
// Show plan mismatch warning (critical - plan has more tasks than state)
|
|
1137
|
+
if (progressAnalysis.planMismatch) {
|
|
1138
|
+
console.log();
|
|
1139
|
+
console.log(theme.error.bold(' CRITICAL: Plan Mismatch Detected!'));
|
|
1140
|
+
console.log(theme.error(` The plan file has ${progressAnalysis.planTaskCount} tasks but state only has ${progressAnalysis.totalTasks}.`));
|
|
1141
|
+
console.log(theme.error(` This means the plan was not fully parsed into tasks.`));
|
|
1142
|
+
console.log(theme.error(` True progress: ${progressAnalysis.completedTasks}/${progressAnalysis.planTaskCount} tasks (${progressAnalysis.percentComplete}%)`));
|
|
1143
|
+
|
|
1144
|
+
// Show some missing tasks
|
|
1145
|
+
if (progressAnalysis.missingFromState.length > 0) {
|
|
1146
|
+
console.log();
|
|
1147
|
+
console.log(theme.warning(' Tasks in plan but missing from state:'));
|
|
1148
|
+
for (const task of progressAnalysis.missingFromState.slice(0, 8)) {
|
|
1149
|
+
console.log(` ${theme.dim('-')} ${task.slice(0, 70)}${task.length > 70 ? '...' : ''}`);
|
|
1150
|
+
}
|
|
1151
|
+
if (progressAnalysis.missingFromState.length > 8) {
|
|
1152
|
+
console.log(` ${theme.dim(`... and ${progressAnalysis.missingFromState.length - 8} more tasks`)}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
console.log();
|
|
1157
|
+
console.log(theme.secondary(' The plan needs to be re-parsed to capture all tasks.'));
|
|
1158
|
+
console.log(theme.secondary(' Consider running the workflow again or manually adding tasks.'));
|
|
1159
|
+
}
|
|
1160
|
+
} else if (progressAnalysis.planParseError) {
|
|
1161
|
+
console.log();
|
|
1162
|
+
console.log(theme.dim(` Plan file: ${progressAnalysis.planParseError}`));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Check for status mismatch (status says complete but state tasks are incomplete)
|
|
1166
|
+
if (progressAnalysis.statusMismatch && !progressAnalysis.planMismatch) {
|
|
1167
|
+
console.log();
|
|
1168
|
+
console.log(theme.warning.bold(' WARNING: Status Mismatch Detected!'));
|
|
1169
|
+
console.log(theme.warning(` Project status says '${status.state.status}' but work is incomplete.`));
|
|
1170
|
+
console.log(theme.warning(` ${progressAnalysis.progressSummary}`));
|
|
1171
|
+
console.log(theme.secondary(' Will reset status and continue execution.'));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Show next items to work on
|
|
1175
|
+
if (progressAnalysis.nextMilestone || progressAnalysis.nextTask) {
|
|
1176
|
+
console.log();
|
|
1177
|
+
console.log(theme.secondary(' Next Up:'));
|
|
1178
|
+
if (progressAnalysis.nextMilestone) {
|
|
1179
|
+
console.log(` ${theme.dim('Milestone:')} ${progressAnalysis.nextMilestone.name}`);
|
|
1180
|
+
}
|
|
1181
|
+
if (progressAnalysis.nextTask) {
|
|
1182
|
+
console.log(` ${theme.dim('Task:')} ${progressAnalysis.nextTask.name}`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Show incomplete milestones
|
|
1187
|
+
if (progressAnalysis.incompleteMilestones.length > 0 && !verification.isComplete) {
|
|
1188
|
+
console.log();
|
|
1189
|
+
console.log(theme.secondary(' Remaining Milestones:'));
|
|
1190
|
+
for (const m of progressAnalysis.incompleteMilestones.slice(0, 5)) {
|
|
1191
|
+
console.log(` ${theme.dim('-')} ${m.name} (${m.tasksRemaining} tasks remaining)`);
|
|
1192
|
+
}
|
|
1193
|
+
if (progressAnalysis.incompleteMilestones.length > 5) {
|
|
1194
|
+
console.log(` ${theme.dim(`... and ${progressAnalysis.incompleteMilestones.length - 5} more`)}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (status.state.consensusHistory && status.state.consensusHistory.length > 0) {
|
|
1199
|
+
const lastConsensus = status.state.consensusHistory[status.state.consensusHistory.length - 1];
|
|
1200
|
+
console.log();
|
|
1201
|
+
console.log(theme.secondary(' Consensus History:'));
|
|
1202
|
+
console.log(` ${theme.dim('Last Score:')} ${lastConsensus.result.score}%`);
|
|
1203
|
+
console.log(` ${theme.dim('Iterations:')} ${status.state.consensusHistory.length}`);
|
|
1204
|
+
|
|
1205
|
+
// Show last concerns
|
|
1206
|
+
if (lastConsensus.result.concerns && lastConsensus.result.concerns.length > 0) {
|
|
1207
|
+
console.log();
|
|
1208
|
+
console.log(theme.secondary(' Last Concerns:'));
|
|
1209
|
+
for (const concern of lastConsensus.result.concerns.slice(0, 3)) {
|
|
1210
|
+
console.log(` ${theme.dim('-')} ${concern.slice(0, 80)}${concern.length > 80 ? '...' : ''}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (status.state.error) {
|
|
1216
|
+
console.log();
|
|
1217
|
+
console.log(theme.error(` Error: ${status.state.error}`));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// If project says complete but isn't, inform user we'll continue
|
|
1221
|
+
if (verification.isComplete) {
|
|
1222
|
+
console.log();
|
|
1223
|
+
printSuccess('Project is fully complete!');
|
|
1224
|
+
printInfo(`All ${progressAnalysis.totalTasks} tasks across ${progressAnalysis.totalMilestones} milestones are done.`);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Check if user provided context as argument
|
|
1229
|
+
let additionalContext = args.join(' ').trim();
|
|
1230
|
+
|
|
1231
|
+
// If no context provided, ask if they want to add guidance
|
|
1232
|
+
if (!additionalContext) {
|
|
1233
|
+
console.log();
|
|
1234
|
+
const wantsContext = await promptYesNo(
|
|
1235
|
+
theme.primary('Would you like to add guidance before resuming?'),
|
|
1236
|
+
false
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
if (wantsContext) {
|
|
1240
|
+
additionalContext = await promptForContext(
|
|
1241
|
+
'What guidance would you like to give? (e.g., "Focus on simplicity", "Use SQLite instead of PostgreSQL")'
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
console.log();
|
|
1247
|
+
printInfo('Resuming workflow...');
|
|
1248
|
+
if (additionalContext) {
|
|
1249
|
+
console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
|
|
1250
|
+
}
|
|
1251
|
+
console.log();
|
|
1252
|
+
|
|
1253
|
+
const result = await resumeWorkflow(state.projectDir, {
|
|
1254
|
+
consensusConfig: {
|
|
1255
|
+
reviewer: state.reviewer,
|
|
1256
|
+
arbitrator: state.arbitrator,
|
|
1257
|
+
enableArbitration: state.enableArbitration,
|
|
1258
|
+
},
|
|
1259
|
+
additionalContext,
|
|
1260
|
+
onProgress: (phase, message) => {
|
|
1261
|
+
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
console.log();
|
|
1266
|
+
if (result.success) {
|
|
1267
|
+
// Update README with project description on completion
|
|
1268
|
+
await updateReadmeOnCompletion(
|
|
1269
|
+
state.projectDir,
|
|
1270
|
+
status.state.name,
|
|
1271
|
+
status.state.idea,
|
|
1272
|
+
status.state.language
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
printSuccess('Workflow completed!');
|
|
1276
|
+
console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
|
|
1277
|
+
} else {
|
|
1278
|
+
printError(result.error || 'Workflow failed');
|
|
1279
|
+
printInfo('You can run /resume again with additional guidance');
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// No formal project state - try to discover context from docs/
|
|
1285
|
+
printInfo('No project state found in selected directory. Scanning for project context...');
|
|
1286
|
+
console.log();
|
|
1287
|
+
|
|
1288
|
+
const discovered = await discoverProjectContext(state.projectDir);
|
|
1289
|
+
|
|
1290
|
+
if (!discovered.found) {
|
|
1291
|
+
console.log(theme.secondary(' No project context found in this directory.'));
|
|
1292
|
+
console.log();
|
|
1293
|
+
console.log(theme.dim(' To start a new project, simply type your idea:'));
|
|
1294
|
+
console.log(theme.dim(' Example: "Build a REST API for task management"'));
|
|
1295
|
+
console.log();
|
|
1296
|
+
console.log(theme.dim(' Or navigate to a directory with existing plans:'));
|
|
1297
|
+
console.log(theme.dim(' - docs/PLAN.md or docs/PLAN-DRAFT.md'));
|
|
1298
|
+
console.log(theme.dim(' - README.md'));
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Show what we discovered
|
|
1303
|
+
console.log(theme.primary.bold(' Discovered Project Context:'));
|
|
1304
|
+
console.log();
|
|
1305
|
+
|
|
1306
|
+
if (discovered.name) {
|
|
1307
|
+
console.log(` ${theme.dim('Name:')} ${discovered.name}`);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (discovered.language) {
|
|
1311
|
+
console.log(` ${theme.dim('Language:')} ${theme.primary(discovered.language)}`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (discovered.planFile) {
|
|
1315
|
+
console.log(` ${theme.dim('Plan:')} docs/${discovered.planFile}`);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (discovered.hasCode) {
|
|
1319
|
+
console.log(` ${theme.dim('Code:')} ${discovered.codeFiles.length} source files found`);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (discovered.idea) {
|
|
1323
|
+
console.log();
|
|
1324
|
+
console.log(theme.secondary(' Project Overview:'));
|
|
1325
|
+
const ideaLines = discovered.idea.split('\n').slice(0, 4);
|
|
1326
|
+
for (const line of ideaLines) {
|
|
1327
|
+
console.log(` ${theme.dim(line.slice(0, 80))}`);
|
|
1328
|
+
}
|
|
1329
|
+
if (discovered.idea.split('\n').length > 4) {
|
|
1330
|
+
console.log(theme.dim(' ...'));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (discovered.plan) {
|
|
1335
|
+
// Show plan summary
|
|
1336
|
+
console.log();
|
|
1337
|
+
console.log(theme.secondary(' Plan Summary:'));
|
|
1338
|
+
const planLines = discovered.plan.split('\n').filter(l => l.trim().startsWith('#') || l.trim().startsWith('-')).slice(0, 8);
|
|
1339
|
+
for (const line of planLines) {
|
|
1340
|
+
console.log(` ${theme.dim(line.slice(0, 80))}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
console.log();
|
|
1345
|
+
|
|
1346
|
+
// Ask user what they want to do
|
|
1347
|
+
const action = await promptSelection(
|
|
1348
|
+
'What would you like to do?',
|
|
1349
|
+
[
|
|
1350
|
+
{ value: 'continue', label: 'Continue with this plan - use existing and continue development' },
|
|
1351
|
+
{ value: 'refine', label: 'Refine the plan - review and improve with consensus' },
|
|
1352
|
+
{ value: 'new', label: 'Start fresh - provide a new idea' },
|
|
1353
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
1354
|
+
],
|
|
1355
|
+
'continue'
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
if (action === 'cancel') {
|
|
1359
|
+
printInfo('Cancelled');
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
if (action === 'new') {
|
|
1364
|
+
console.log();
|
|
1365
|
+
printInfo('Type your project idea to start a new workflow');
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Get additional context/guidance
|
|
1370
|
+
let additionalContext = args.join(' ').trim();
|
|
1371
|
+
|
|
1372
|
+
if (!additionalContext && action === 'refine') {
|
|
1373
|
+
additionalContext = await promptForContext(
|
|
1374
|
+
'What changes or improvements would you like? (e.g., "Add authentication", "Simplify the architecture")'
|
|
1375
|
+
);
|
|
1376
|
+
} else if (!additionalContext) {
|
|
1377
|
+
console.log();
|
|
1378
|
+
const wantsContext = await promptYesNo(
|
|
1379
|
+
theme.primary('Would you like to add any guidance?'),
|
|
1380
|
+
false
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
if (wantsContext) {
|
|
1384
|
+
additionalContext = await promptForContext(
|
|
1385
|
+
'What guidance would you like to give?'
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Create project spec from discovered context
|
|
1391
|
+
const projectName = discovered.name ||
|
|
1392
|
+
path.basename(state.projectDir)
|
|
1393
|
+
.toLowerCase()
|
|
1394
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
1395
|
+
.substring(0, 30) ||
|
|
1396
|
+
'my-project';
|
|
1397
|
+
|
|
1398
|
+
const spec: ProjectSpec = {
|
|
1399
|
+
idea: discovered.idea || discovered.plan?.slice(0, 500) || `Continue developing ${projectName}`,
|
|
1400
|
+
name: projectName,
|
|
1401
|
+
language: discovered.language || state.language,
|
|
1402
|
+
openaiModel: state.model,
|
|
1403
|
+
outputDir: state.projectDir,
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
console.log();
|
|
1407
|
+
printInfo(`Starting workflow for "${projectName}"...`);
|
|
1408
|
+
if (additionalContext) {
|
|
1409
|
+
console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
|
|
1410
|
+
}
|
|
1411
|
+
console.log();
|
|
1412
|
+
|
|
1413
|
+
// Run the workflow
|
|
1414
|
+
const result = await runWorkflow(spec, {
|
|
1415
|
+
projectDir: state.projectDir,
|
|
1416
|
+
consensusConfig: {
|
|
1417
|
+
reviewer: state.reviewer,
|
|
1418
|
+
arbitrator: state.arbitrator,
|
|
1419
|
+
enableArbitration: state.enableArbitration,
|
|
1420
|
+
},
|
|
1421
|
+
onProgress: (phase, message) => {
|
|
1422
|
+
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
console.log();
|
|
1427
|
+
if (result.success) {
|
|
1428
|
+
// Update README with project description on completion
|
|
1429
|
+
if (state.projectDir) {
|
|
1430
|
+
await updateReadmeOnCompletion(
|
|
1431
|
+
state.projectDir,
|
|
1432
|
+
spec.name || 'my-project',
|
|
1433
|
+
spec.idea,
|
|
1434
|
+
spec.language
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
368
1437
|
|
|
369
|
-
|
|
370
|
-
|
|
1438
|
+
printSuccess('Workflow completed!');
|
|
1439
|
+
console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
|
|
1440
|
+
} else {
|
|
1441
|
+
printError(result.error || 'Workflow failed');
|
|
1442
|
+
printInfo('You can run /resume again with additional guidance');
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Generate a meaningful project name from an idea
|
|
1448
|
+
* Extracts key nouns and creates a kebab-case name
|
|
1449
|
+
*/
|
|
1450
|
+
function generateProjectName(idea: string): string {
|
|
1451
|
+
// Common words to filter out
|
|
1452
|
+
const stopWords = new Set([
|
|
1453
|
+
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
1454
|
+
'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
|
|
1455
|
+
'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
1456
|
+
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought',
|
|
1457
|
+
'used', 'create', 'build', 'make', 'develop', 'write', 'implement',
|
|
1458
|
+
'want', 'like', 'please', 'help', 'me', 'i', 'my', 'we', 'our', 'you',
|
|
1459
|
+
'your', 'that', 'which', 'who', 'what', 'where', 'when', 'why', 'how',
|
|
1460
|
+
'this', 'these', 'those', 'it', 'its', 'simple', 'basic', 'new',
|
|
1461
|
+
]);
|
|
1462
|
+
|
|
1463
|
+
// Extract meaningful words
|
|
1464
|
+
const words = idea
|
|
371
1465
|
.toLowerCase()
|
|
372
|
-
.replace(/[^a-z0-9\s]/g, '')
|
|
1466
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
373
1467
|
.split(/\s+/)
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
1468
|
+
.filter(word => word.length > 2 && !stopWords.has(word));
|
|
1469
|
+
|
|
1470
|
+
// Take first 2-3 meaningful words
|
|
1471
|
+
const nameWords = words.slice(0, 3);
|
|
1472
|
+
|
|
1473
|
+
if (nameWords.length === 0) {
|
|
1474
|
+
// Fallback: use first words from original idea
|
|
1475
|
+
const fallback = idea
|
|
1476
|
+
.toLowerCase()
|
|
1477
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
1478
|
+
.split(/\s+/)
|
|
1479
|
+
.filter(w => w.length > 0)
|
|
1480
|
+
.slice(0, 2);
|
|
1481
|
+
return fallback.join('-') || 'my-project';
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return nameWords.join('-').substring(0, 40);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Update README.md with project description and usage instructions
|
|
1489
|
+
*/
|
|
1490
|
+
async function updateReadmeOnCompletion(
|
|
1491
|
+
projectDir: string,
|
|
1492
|
+
projectName: string,
|
|
1493
|
+
idea: string,
|
|
1494
|
+
language: OutputLanguage
|
|
1495
|
+
): Promise<void> {
|
|
1496
|
+
const readmePath = path.join(projectDir, 'README.md');
|
|
1497
|
+
|
|
1498
|
+
try {
|
|
1499
|
+
// Read existing README
|
|
1500
|
+
let content = await fs.readFile(readmePath, 'utf-8');
|
|
1501
|
+
|
|
1502
|
+
// Check if it still has the placeholder description
|
|
1503
|
+
if (content.includes('Generated by Popeye CLI')) {
|
|
1504
|
+
// Generate a better description based on the idea
|
|
1505
|
+
const description = `${idea}\n\nThis project was automatically generated and implemented using [Popeye CLI](https://github.com/popeye-cli).`;
|
|
1506
|
+
|
|
1507
|
+
// Replace the placeholder
|
|
1508
|
+
content = content.replace(
|
|
1509
|
+
/Generated by Popeye CLI/g,
|
|
1510
|
+
description
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
// Add a "Getting Started" section if it doesn't exist
|
|
1514
|
+
if (!content.includes('## Getting Started')) {
|
|
1515
|
+
const gettingStarted = language === 'python'
|
|
1516
|
+
? `
|
|
1517
|
+
## Getting Started
|
|
1518
|
+
|
|
1519
|
+
1. Create and activate a virtual environment:
|
|
1520
|
+
\`\`\`bash
|
|
1521
|
+
python -m venv venv
|
|
1522
|
+
source venv/bin/activate # On Windows: venv\\Scripts\\activate
|
|
1523
|
+
\`\`\`
|
|
1524
|
+
|
|
1525
|
+
2. Install dependencies:
|
|
1526
|
+
\`\`\`bash
|
|
1527
|
+
pip install -e ".[dev]"
|
|
1528
|
+
\`\`\`
|
|
1529
|
+
|
|
1530
|
+
3. Run the application:
|
|
1531
|
+
\`\`\`bash
|
|
1532
|
+
python -m src.${projectName.replace(/-/g, '_')}.main
|
|
1533
|
+
\`\`\`
|
|
1534
|
+
`
|
|
1535
|
+
: `
|
|
1536
|
+
## Getting Started
|
|
1537
|
+
|
|
1538
|
+
1. Install dependencies:
|
|
1539
|
+
\`\`\`bash
|
|
1540
|
+
npm install
|
|
1541
|
+
\`\`\`
|
|
1542
|
+
|
|
1543
|
+
2. Build the project:
|
|
1544
|
+
\`\`\`bash
|
|
1545
|
+
npm run build
|
|
1546
|
+
\`\`\`
|
|
1547
|
+
|
|
1548
|
+
3. Run the application:
|
|
1549
|
+
\`\`\`bash
|
|
1550
|
+
npm start
|
|
1551
|
+
\`\`\`
|
|
1552
|
+
`;
|
|
1553
|
+
|
|
1554
|
+
// Insert before "## Development" or at the end
|
|
1555
|
+
if (content.includes('## Development')) {
|
|
1556
|
+
content = content.replace('## Development', gettingStarted + '\n## Development');
|
|
1557
|
+
} else {
|
|
1558
|
+
content += gettingStarted;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
await fs.writeFile(readmePath, content, 'utf-8');
|
|
1563
|
+
}
|
|
1564
|
+
} catch {
|
|
1565
|
+
// Silently ignore if README doesn't exist or can't be updated
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Handle project idea input
|
|
1571
|
+
*/
|
|
1572
|
+
async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
1573
|
+
const cwd = state.projectDir || process.cwd();
|
|
377
1574
|
|
|
378
|
-
|
|
379
|
-
const
|
|
1575
|
+
// Check for existing Popeye projects in the current directory
|
|
1576
|
+
const { all: existingProjects } = await discoverProjects(cwd);
|
|
1577
|
+
const localProjects = existingProjects.filter(p => p.path.startsWith(cwd));
|
|
1578
|
+
|
|
1579
|
+
if (localProjects.length > 0) {
|
|
1580
|
+
console.log();
|
|
1581
|
+
printWarning('Existing Popeye projects found in this directory:');
|
|
1582
|
+
console.log();
|
|
1583
|
+
|
|
1584
|
+
for (const project of localProjects.slice(0, 5)) {
|
|
1585
|
+
const display = formatProjectForDisplay(project);
|
|
1586
|
+
console.log(` ${theme.primary(display.name)} ${theme.dim(`(${display.age})`)}`);
|
|
1587
|
+
console.log(` ${theme.dim(display.status)} - ${theme.dim(project.path)}`);
|
|
1588
|
+
console.log();
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
printInfo('Consider running /resume to continue an existing project.');
|
|
1592
|
+
printInfo('To start a new project anyway, run: /new ' + idea);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (!state.claudeAuth || !state.openaiAuth) {
|
|
1597
|
+
console.log();
|
|
1598
|
+
printError('Authentication required');
|
|
1599
|
+
printInfo('Running authentication flow...');
|
|
1600
|
+
console.log();
|
|
1601
|
+
|
|
1602
|
+
const authenticated = await ensureAuthentication(state);
|
|
1603
|
+
if (!authenticated) {
|
|
1604
|
+
printWarning('Skipping project creation - authentication incomplete');
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Generate a meaningful project name
|
|
1610
|
+
const projectName = generateProjectName(idea);
|
|
1611
|
+
const projectDir = path.join(cwd, projectName);
|
|
1612
|
+
|
|
1613
|
+
console.log();
|
|
1614
|
+
console.log(theme.primary.bold(' Creating Project'));
|
|
1615
|
+
console.log(` ${theme.dim('Idea:')} ${idea}`);
|
|
1616
|
+
console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
|
|
1617
|
+
console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
|
|
1618
|
+
console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
|
|
1619
|
+
console.log();
|
|
380
1620
|
|
|
381
1621
|
const spec: ProjectSpec = {
|
|
382
1622
|
idea,
|
|
383
1623
|
name: projectName,
|
|
384
1624
|
language: state.language,
|
|
385
1625
|
openaiModel: state.model,
|
|
386
|
-
outputDir:
|
|
1626
|
+
outputDir: cwd,
|
|
387
1627
|
};
|
|
388
1628
|
|
|
389
1629
|
// Generate scaffold
|
|
390
1630
|
startSpinner('Creating project structure...');
|
|
391
|
-
const scaffoldResult = await generateProject(spec,
|
|
1631
|
+
const scaffoldResult = await generateProject(spec, cwd);
|
|
392
1632
|
|
|
393
1633
|
if (!scaffoldResult.success) {
|
|
394
1634
|
failSpinner('Scaffolding failed');
|
|
@@ -398,21 +1638,206 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
|
398
1638
|
|
|
399
1639
|
succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
|
|
400
1640
|
|
|
401
|
-
// Run workflow
|
|
1641
|
+
// Run workflow with reviewer/arbitrator settings
|
|
1642
|
+
console.log();
|
|
1643
|
+
printInfo('Starting AI workflow...');
|
|
1644
|
+
console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
|
|
1645
|
+
if (state.enableArbitration) {
|
|
1646
|
+
console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
|
|
1647
|
+
}
|
|
1648
|
+
console.log();
|
|
1649
|
+
|
|
402
1650
|
const workflowResult = await runWorkflow(spec, {
|
|
403
1651
|
projectDir,
|
|
1652
|
+
consensusConfig: {
|
|
1653
|
+
reviewer: state.reviewer,
|
|
1654
|
+
arbitrator: state.arbitrator,
|
|
1655
|
+
enableArbitration: state.enableArbitration,
|
|
1656
|
+
geminiModel: state.geminiModel,
|
|
1657
|
+
},
|
|
404
1658
|
onProgress: (phase, message) => {
|
|
405
|
-
console.log(` [${phase}] ${message}`);
|
|
1659
|
+
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
406
1660
|
},
|
|
407
1661
|
});
|
|
408
1662
|
|
|
409
1663
|
stopSpinner();
|
|
410
1664
|
|
|
1665
|
+
console.log();
|
|
411
1666
|
if (workflowResult.success) {
|
|
1667
|
+
// Update README with project description
|
|
1668
|
+
await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
|
|
1669
|
+
|
|
412
1670
|
printSuccess('Project created successfully!');
|
|
413
|
-
|
|
1671
|
+
console.log(` ${theme.dim('Location:')} ${projectDir}`);
|
|
414
1672
|
state.projectDir = projectDir;
|
|
415
1673
|
} else {
|
|
416
1674
|
printError(workflowResult.error || 'Workflow failed');
|
|
417
1675
|
}
|
|
418
1676
|
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Handle /new command - force create a new project (skips existing project check)
|
|
1680
|
+
*/
|
|
1681
|
+
async function handleNewProject(idea: string, state: SessionState): Promise<void> {
|
|
1682
|
+
if (!state.claudeAuth || !state.openaiAuth) {
|
|
1683
|
+
console.log();
|
|
1684
|
+
printError('Authentication required');
|
|
1685
|
+
printInfo('Running authentication flow...');
|
|
1686
|
+
console.log();
|
|
1687
|
+
|
|
1688
|
+
const authenticated = await ensureAuthentication(state);
|
|
1689
|
+
if (!authenticated) {
|
|
1690
|
+
printWarning('Skipping project creation - authentication incomplete');
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const cwd = state.projectDir || process.cwd();
|
|
1696
|
+
|
|
1697
|
+
// Generate a meaningful project name
|
|
1698
|
+
const projectName = generateProjectName(idea);
|
|
1699
|
+
const projectDir = path.join(cwd, projectName);
|
|
1700
|
+
|
|
1701
|
+
console.log();
|
|
1702
|
+
console.log(theme.primary.bold(' Creating New Project'));
|
|
1703
|
+
console.log(` ${theme.dim('Idea:')} ${idea}`);
|
|
1704
|
+
console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
|
|
1705
|
+
console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
|
|
1706
|
+
console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
|
|
1707
|
+
console.log();
|
|
1708
|
+
|
|
1709
|
+
const spec: ProjectSpec = {
|
|
1710
|
+
idea,
|
|
1711
|
+
name: projectName,
|
|
1712
|
+
language: state.language,
|
|
1713
|
+
openaiModel: state.model,
|
|
1714
|
+
outputDir: cwd,
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
// Generate scaffold
|
|
1718
|
+
startSpinner('Creating project structure...');
|
|
1719
|
+
const scaffoldResult = await generateProject(spec, cwd);
|
|
1720
|
+
|
|
1721
|
+
if (!scaffoldResult.success) {
|
|
1722
|
+
failSpinner('Scaffolding failed');
|
|
1723
|
+
printError(scaffoldResult.error || 'Failed to create project');
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
|
|
1728
|
+
|
|
1729
|
+
// Run workflow with reviewer/arbitrator settings
|
|
1730
|
+
console.log();
|
|
1731
|
+
printInfo('Starting AI workflow...');
|
|
1732
|
+
console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
|
|
1733
|
+
if (state.enableArbitration) {
|
|
1734
|
+
console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
|
|
1735
|
+
}
|
|
1736
|
+
console.log();
|
|
1737
|
+
|
|
1738
|
+
const workflowResult = await runWorkflow(spec, {
|
|
1739
|
+
projectDir,
|
|
1740
|
+
consensusConfig: {
|
|
1741
|
+
reviewer: state.reviewer,
|
|
1742
|
+
arbitrator: state.arbitrator,
|
|
1743
|
+
enableArbitration: state.enableArbitration,
|
|
1744
|
+
geminiModel: state.geminiModel,
|
|
1745
|
+
},
|
|
1746
|
+
onProgress: (phase, message) => {
|
|
1747
|
+
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
stopSpinner();
|
|
1752
|
+
|
|
1753
|
+
console.log();
|
|
1754
|
+
if (workflowResult.success) {
|
|
1755
|
+
// Update README with project description
|
|
1756
|
+
await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
|
|
1757
|
+
|
|
1758
|
+
printSuccess('Project created successfully!');
|
|
1759
|
+
console.log(` ${theme.dim('Location:')} ${projectDir}`);
|
|
1760
|
+
state.projectDir = projectDir;
|
|
1761
|
+
} else {
|
|
1762
|
+
printError(workflowResult.error || 'Workflow failed');
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Start interactive mode with auto-authentication
|
|
1768
|
+
*/
|
|
1769
|
+
export async function startInteractiveMode(): Promise<void> {
|
|
1770
|
+
console.clear();
|
|
1771
|
+
|
|
1772
|
+
// Initialize state from saved config
|
|
1773
|
+
const config = await loadConfig();
|
|
1774
|
+
const state: SessionState = {
|
|
1775
|
+
projectDir: process.cwd(),
|
|
1776
|
+
language: config.project.default_language,
|
|
1777
|
+
model: config.apis.openai.model,
|
|
1778
|
+
geminiModel: 'gemini-2.0-flash',
|
|
1779
|
+
claudeAuth: false,
|
|
1780
|
+
openaiAuth: false,
|
|
1781
|
+
geminiAuth: false,
|
|
1782
|
+
// Load saved reviewer/arbitrator settings from config
|
|
1783
|
+
reviewer: config.consensus.reviewer,
|
|
1784
|
+
arbitrator: config.consensus.arbitrator === 'off' ? 'openai' : config.consensus.arbitrator,
|
|
1785
|
+
enableArbitration: config.consensus.enable_arbitration,
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// Draw header
|
|
1789
|
+
drawHeader();
|
|
1790
|
+
console.log();
|
|
1791
|
+
|
|
1792
|
+
// Show how Popeye works
|
|
1793
|
+
console.log(theme.secondary(' How Popeye works:'));
|
|
1794
|
+
console.log(theme.dim(' ├─ ') + theme.primary('Claude Code CLI') + theme.dim(' - Generates code (uses your model & MCP settings)'));
|
|
1795
|
+
console.log(theme.dim(' ├─ ') + theme.secondary('Reviewer (configurable)') + theme.dim(' - Reviews plans until consensus'));
|
|
1796
|
+
console.log(theme.dim(' └─ ') + theme.secondary('Arbitrator (optional)') + theme.dim(' - Breaks deadlocks when stuck'));
|
|
1797
|
+
console.log();
|
|
1798
|
+
console.log(theme.dim(' You can choose OpenAI or Gemini as reviewer/arbitrator during setup.'));
|
|
1799
|
+
console.log(theme.dim(' Plans are saved to docs/ folder in markdown format.'));
|
|
1800
|
+
console.log();
|
|
1801
|
+
|
|
1802
|
+
// Check and perform authentication
|
|
1803
|
+
const isAuthenticated = await ensureAuthentication(state);
|
|
1804
|
+
|
|
1805
|
+
if (!isAuthenticated) {
|
|
1806
|
+
console.log();
|
|
1807
|
+
printWarning('Some services are not authenticated. Some features may not work.');
|
|
1808
|
+
printInfo('You can authenticate later with /auth');
|
|
1809
|
+
} else {
|
|
1810
|
+
console.log();
|
|
1811
|
+
printSuccess('Ready! Type your project idea or /help for commands');
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
console.log();
|
|
1815
|
+
|
|
1816
|
+
// Create readline interface
|
|
1817
|
+
const rl = readline.createInterface({
|
|
1818
|
+
input: process.stdin,
|
|
1819
|
+
output: process.stdout,
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
// Input loop
|
|
1823
|
+
const promptUser = (): void => {
|
|
1824
|
+
drawInputBoxTop(state);
|
|
1825
|
+
|
|
1826
|
+
rl.question(getPrompt(), async (input) => {
|
|
1827
|
+
// Draw bottom of input box after user presses enter
|
|
1828
|
+
drawInputBoxBottom();
|
|
1829
|
+
|
|
1830
|
+
const shouldContinue = await handleInput(input, state);
|
|
1831
|
+
|
|
1832
|
+
if (shouldContinue) {
|
|
1833
|
+
console.log();
|
|
1834
|
+
promptUser();
|
|
1835
|
+
} else {
|
|
1836
|
+
rl.close();
|
|
1837
|
+
process.exit(0);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
promptUser();
|
|
1843
|
+
}
|