popeye-cli 1.0.1 → 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/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1151 -110
- 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/interactive.ts +1357 -115
- 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
|
@@ -4,16 +4,35 @@
|
|
|
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
37
|
printSuccess,
|
|
19
38
|
printError,
|
|
@@ -27,6 +46,8 @@ import {
|
|
|
27
46
|
theme,
|
|
28
47
|
} from './output.js';
|
|
29
48
|
|
|
49
|
+
// Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
52
|
* Box drawing characters for Claude Code-style UI
|
|
32
53
|
*/
|
|
@@ -48,8 +69,13 @@ interface SessionState {
|
|
|
48
69
|
projectDir: string | null;
|
|
49
70
|
language: OutputLanguage;
|
|
50
71
|
model: OpenAIModel;
|
|
72
|
+
geminiModel: GeminiModel;
|
|
51
73
|
claudeAuth: boolean;
|
|
52
74
|
openaiAuth: boolean;
|
|
75
|
+
geminiAuth: boolean;
|
|
76
|
+
reviewer: AIProvider;
|
|
77
|
+
arbitrator: AIProvider;
|
|
78
|
+
enableArbitration: boolean;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
/**
|
|
@@ -65,7 +91,7 @@ function getTerminalWidth(): number {
|
|
|
65
91
|
function drawHeader(): void {
|
|
66
92
|
const width = getTerminalWidth();
|
|
67
93
|
const title = ' Popeye CLI ';
|
|
68
|
-
const subtitle = '
|
|
94
|
+
const subtitle = ' Autonomous Code Generation with AI Consensus ';
|
|
69
95
|
|
|
70
96
|
// Top border
|
|
71
97
|
console.log(theme.dim(box.topLeft + box.horizontal.repeat(width - 2) + box.topRight));
|
|
@@ -95,38 +121,57 @@ function drawHeader(): void {
|
|
|
95
121
|
}
|
|
96
122
|
|
|
97
123
|
/**
|
|
98
|
-
* Draw
|
|
124
|
+
* Draw hints line and top of input box
|
|
99
125
|
*/
|
|
100
|
-
function
|
|
101
|
-
const width = getTerminalWidth();
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
function drawInputBoxTop(state: SessionState): void {
|
|
127
|
+
const width = Math.min(getTerminalWidth(), 100);
|
|
128
|
+
|
|
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),
|
|
114
152
|
];
|
|
115
|
-
const statusText =
|
|
153
|
+
const statusText = statusParts.join(theme.dim(' │ '));
|
|
116
154
|
|
|
117
155
|
// Calculate visible length (without ANSI codes)
|
|
118
156
|
// eslint-disable-next-line no-control-regex
|
|
119
157
|
const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
120
158
|
const statusLen = stripAnsi(statusText).length;
|
|
121
159
|
|
|
122
|
-
// Top line
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
box.topRight
|
|
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
|
+
}
|
|
128
168
|
|
|
129
|
-
|
|
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));
|
|
130
175
|
}
|
|
131
176
|
|
|
132
177
|
/**
|
|
@@ -139,10 +184,112 @@ function redrawUI(_state: SessionState): void {
|
|
|
139
184
|
}
|
|
140
185
|
|
|
141
186
|
/**
|
|
142
|
-
* Prompt for input with styled prompt
|
|
187
|
+
* Prompt for input with styled prompt (inside box)
|
|
143
188
|
*/
|
|
144
189
|
function getPrompt(): string {
|
|
145
|
-
return theme.dim(box.vertical
|
|
190
|
+
return theme.dim(box.vertical + ' ') + theme.primary('popeye') + theme.dim(' > ');
|
|
191
|
+
}
|
|
192
|
+
|
|
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
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log();
|
|
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();
|
|
216
|
+
|
|
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
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
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
|
+
});
|
|
248
|
+
|
|
249
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
250
|
+
process.stdout.write(` ${question} ${theme.dim(hint)} `);
|
|
251
|
+
|
|
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
|
+
}
|
|
146
293
|
}
|
|
147
294
|
|
|
148
295
|
/**
|
|
@@ -152,83 +299,181 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
|
|
|
152
299
|
const status = await getAuthStatusForDisplay();
|
|
153
300
|
state.claudeAuth = status.claude.authenticated;
|
|
154
301
|
state.openaiAuth = status.openai.authenticated;
|
|
155
|
-
|
|
156
|
-
if (state.claudeAuth && state.openaiAuth) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
302
|
+
state.geminiAuth = status.gemini?.authenticated || false;
|
|
159
303
|
|
|
160
304
|
console.log();
|
|
161
|
-
|
|
305
|
+
printInfo('Checking authentication...');
|
|
162
306
|
console.log();
|
|
163
307
|
|
|
164
308
|
// Authenticate Claude if needed
|
|
165
309
|
if (!state.claudeAuth) {
|
|
166
|
-
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude CLI') + theme.dim(' -
|
|
310
|
+
console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude Code CLI') + theme.dim(' - Required for code generation'));
|
|
167
311
|
console.log(theme.dim(box.vertical));
|
|
168
312
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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');
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
} else {
|
|
326
|
+
printSuccess('Claude Code CLI ready');
|
|
327
|
+
}
|
|
173
328
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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));
|
|
333
|
+
|
|
334
|
+
try {
|
|
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
|
+
}
|
|
411
|
+
|
|
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));
|
|
180
417
|
|
|
181
|
-
if (proceed) {
|
|
182
|
-
startSpinner('Opening browser for Claude authentication...');
|
|
183
418
|
try {
|
|
184
|
-
const success = await
|
|
419
|
+
const success = await authenticateGemini();
|
|
185
420
|
if (success) {
|
|
186
|
-
|
|
187
|
-
state.
|
|
421
|
+
printSuccess('Gemini API ready');
|
|
422
|
+
state.geminiAuth = true;
|
|
188
423
|
} else {
|
|
189
|
-
|
|
424
|
+
printWarning('Gemini API not authenticated - falling back to OpenAI');
|
|
425
|
+
state.reviewer = 'openai';
|
|
190
426
|
}
|
|
191
427
|
} catch (err) {
|
|
192
|
-
failSpinner('Claude authentication failed');
|
|
193
428
|
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
429
|
+
state.reviewer = 'openai';
|
|
194
430
|
}
|
|
195
431
|
}
|
|
196
|
-
console.log();
|
|
197
|
-
}
|
|
198
432
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - API key required'));
|
|
202
|
-
console.log(theme.dim(box.vertical));
|
|
433
|
+
// Save the configuration to persist between sessions
|
|
434
|
+
await saveConsensusConfig(state);
|
|
203
435
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
436
|
+
// Show summary
|
|
437
|
+
console.log();
|
|
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();
|
|
208
449
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
});
|
|
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));
|
|
215
455
|
|
|
216
|
-
if (proceed2) {
|
|
217
|
-
startSpinner('Opening browser for OpenAI API key entry...');
|
|
218
456
|
try {
|
|
219
|
-
const success = await
|
|
457
|
+
const success = await authenticateGemini();
|
|
220
458
|
if (success) {
|
|
221
|
-
|
|
222
|
-
state.
|
|
459
|
+
printSuccess('Gemini API ready');
|
|
460
|
+
state.geminiAuth = true;
|
|
223
461
|
} else {
|
|
224
|
-
|
|
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
|
+
}
|
|
225
471
|
}
|
|
226
472
|
} catch (err) {
|
|
227
|
-
|
|
228
|
-
printError(err instanceof Error ? err.message : 'Authentication failed');
|
|
473
|
+
printError(err instanceof Error ? err.message : 'Gemini authentication failed');
|
|
229
474
|
}
|
|
475
|
+
console.log();
|
|
230
476
|
}
|
|
231
|
-
console.log();
|
|
232
477
|
}
|
|
233
478
|
|
|
234
479
|
return state.claudeAuth && state.openaiAuth;
|
|
@@ -244,11 +489,14 @@ function showHelp(): void {
|
|
|
244
489
|
|
|
245
490
|
const commands = [
|
|
246
491
|
['/help', 'Show this help message'],
|
|
492
|
+
['/info', 'Show system info (Claude CLI status, etc.)'],
|
|
247
493
|
['/status', 'Show current project status'],
|
|
248
494
|
['/auth', 'Re-authenticate services'],
|
|
249
|
-
['/config', 'Show configuration'],
|
|
250
|
-
['/
|
|
251
|
-
['/
|
|
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)'],
|
|
252
500
|
['/resume', 'Resume interrupted project'],
|
|
253
501
|
['/clear', 'Clear screen'],
|
|
254
502
|
['/exit', 'Exit Popeye'],
|
|
@@ -259,7 +507,65 @@ function showHelp(): void {
|
|
|
259
507
|
}
|
|
260
508
|
|
|
261
509
|
console.log();
|
|
262
|
-
console.log(theme.secondary('
|
|
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();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Handle /info command - show system info
|
|
517
|
+
*/
|
|
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
|
+
}
|
|
551
|
+
|
|
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)}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log();
|
|
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.'));
|
|
263
569
|
console.log();
|
|
264
570
|
}
|
|
265
571
|
|
|
@@ -271,6 +577,13 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
|
|
|
271
577
|
|
|
272
578
|
if (!trimmed) return true;
|
|
273
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
|
+
|
|
274
587
|
// Handle commands
|
|
275
588
|
if (trimmed.startsWith('/')) {
|
|
276
589
|
const [cmd, ...args] = trimmed.split(/\s+/);
|
|
@@ -278,9 +591,16 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
|
|
|
278
591
|
|
|
279
592
|
switch (command) {
|
|
280
593
|
case '/help':
|
|
594
|
+
case '/h':
|
|
595
|
+
case '/?':
|
|
281
596
|
showHelp();
|
|
282
597
|
break;
|
|
283
598
|
|
|
599
|
+
case '/info':
|
|
600
|
+
case '/check':
|
|
601
|
+
await handleInfo();
|
|
602
|
+
break;
|
|
603
|
+
|
|
284
604
|
case '/exit':
|
|
285
605
|
case '/quit':
|
|
286
606
|
case '/q':
|
|
@@ -289,6 +609,7 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
|
|
|
289
609
|
return false;
|
|
290
610
|
|
|
291
611
|
case '/clear':
|
|
612
|
+
case '/cls':
|
|
292
613
|
redrawUI(state);
|
|
293
614
|
break;
|
|
294
615
|
|
|
@@ -301,19 +622,32 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
|
|
|
301
622
|
break;
|
|
302
623
|
|
|
303
624
|
case '/config':
|
|
304
|
-
await handleConfig(state);
|
|
625
|
+
await handleConfig(state, args);
|
|
305
626
|
break;
|
|
306
627
|
|
|
307
628
|
case '/language':
|
|
629
|
+
case '/lang':
|
|
630
|
+
case '/l':
|
|
308
631
|
handleLanguage(args, state);
|
|
309
632
|
break;
|
|
310
633
|
|
|
311
634
|
case '/model':
|
|
635
|
+
case '/m':
|
|
312
636
|
handleModel(args, state);
|
|
313
637
|
break;
|
|
314
638
|
|
|
315
639
|
case '/resume':
|
|
316
|
-
await handleResume(state);
|
|
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
|
+
}
|
|
317
651
|
break;
|
|
318
652
|
|
|
319
653
|
default:
|
|
@@ -324,6 +658,13 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
|
|
|
324
658
|
return true;
|
|
325
659
|
}
|
|
326
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
|
+
|
|
327
668
|
// Handle as project idea
|
|
328
669
|
await handleIdea(trimmed, state);
|
|
329
670
|
return true;
|
|
@@ -355,20 +696,107 @@ async function handleStatus(state: SessionState): Promise<void> {
|
|
|
355
696
|
/**
|
|
356
697
|
* Handle /config command
|
|
357
698
|
*/
|
|
358
|
-
async function handleConfig(state: SessionState): Promise<void> {
|
|
699
|
+
async function handleConfig(state: SessionState, args: string[] = []): Promise<void> {
|
|
359
700
|
const config = await loadConfig();
|
|
360
701
|
|
|
702
|
+
// Handle config subcommands
|
|
703
|
+
if (args.length > 0) {
|
|
704
|
+
const subcommand = args[0].toLowerCase();
|
|
705
|
+
|
|
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;
|
|
727
|
+
|
|
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;
|
|
769
|
+
|
|
770
|
+
default:
|
|
771
|
+
printError(`Unknown config option: ${subcommand}`);
|
|
772
|
+
printInfo('Options: reviewer, arbitrator, language');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Show full config
|
|
361
778
|
console.log();
|
|
362
779
|
console.log(theme.primary.bold(' Session:'));
|
|
363
|
-
console.log(` ${theme.dim('Directory:')}
|
|
364
|
-
console.log(` ${theme.dim('Language:')}
|
|
365
|
-
console.log(
|
|
366
|
-
console.log(
|
|
367
|
-
console.log(` ${theme.dim('
|
|
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')}`);
|
|
368
791
|
console.log();
|
|
369
792
|
console.log(theme.primary.bold(' Consensus:'));
|
|
370
|
-
console.log(` ${theme.dim('Threshold:')}
|
|
371
|
-
console.log(` ${theme.dim('Max
|
|
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>'));
|
|
372
800
|
console.log();
|
|
373
801
|
}
|
|
374
802
|
|
|
@@ -418,32 +846,578 @@ function handleModel(args: string[], state: SessionState): void {
|
|
|
418
846
|
printSuccess(`Model set to ${model}`);
|
|
419
847
|
}
|
|
420
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Prompt for additional context
|
|
851
|
+
* Uses terminal: false to prevent echo issues when nested with main readline
|
|
852
|
+
*/
|
|
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
|
+
});
|
|
860
|
+
|
|
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();
|
|
865
|
+
|
|
866
|
+
process.stdout.write(' > ');
|
|
867
|
+
|
|
868
|
+
rl.once('line', (answer) => {
|
|
869
|
+
rl.close();
|
|
870
|
+
resolve(answer.trim());
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Discovered project context from docs/ folder
|
|
877
|
+
*/
|
|
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
|
+
}
|
|
889
|
+
|
|
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
|
+
};
|
|
899
|
+
|
|
900
|
+
const docsDir = path.join(projectDir, 'docs');
|
|
901
|
+
|
|
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
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
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;
|
|
936
|
+
|
|
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
|
+
}
|
|
944
|
+
|
|
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
|
|
954
|
+
}
|
|
955
|
+
|
|
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
|
+
};
|
|
963
|
+
|
|
964
|
+
let pyCount = 0;
|
|
965
|
+
let tsCount = 0;
|
|
966
|
+
|
|
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
|
+
|
|
421
998
|
/**
|
|
422
999
|
* Handle /resume command
|
|
423
1000
|
*/
|
|
424
|
-
async function handleResume(state: SessionState): Promise<void> {
|
|
1001
|
+
async function handleResume(state: SessionState, args: string[]): Promise<void> {
|
|
1002
|
+
if (!state.claudeAuth || !state.openaiAuth) {
|
|
1003
|
+
printError('Authentication required. Run /auth first.');
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Discover all projects (registered + scanned in current directory)
|
|
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
|
|
425
1090
|
if (!state.projectDir) {
|
|
426
1091
|
printError('No project directory set');
|
|
427
1092
|
return;
|
|
428
1093
|
}
|
|
429
1094
|
|
|
430
|
-
|
|
431
|
-
|
|
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
|
+
}
|
|
432
1281
|
return;
|
|
433
1282
|
}
|
|
434
1283
|
|
|
435
|
-
|
|
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();
|
|
436
1287
|
|
|
437
|
-
|
|
438
|
-
|
|
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'));
|
|
439
1299
|
return;
|
|
440
1300
|
}
|
|
441
1301
|
|
|
1302
|
+
// Show what we discovered
|
|
1303
|
+
console.log(theme.primary.bold(' Discovered Project Context:'));
|
|
442
1304
|
console.log();
|
|
443
|
-
|
|
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
|
+
|
|
444
1344
|
console.log();
|
|
445
1345
|
|
|
446
|
-
|
|
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
|
+
},
|
|
447
1421
|
onProgress: (phase, message) => {
|
|
448
1422
|
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
449
1423
|
},
|
|
@@ -451,9 +1425,144 @@ async function handleResume(state: SessionState): Promise<void> {
|
|
|
451
1425
|
|
|
452
1426
|
console.log();
|
|
453
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
|
+
}
|
|
1437
|
+
|
|
454
1438
|
printSuccess('Workflow completed!');
|
|
1439
|
+
console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
|
|
455
1440
|
} else {
|
|
456
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
|
|
1465
|
+
.toLowerCase()
|
|
1466
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
1467
|
+
.split(/\s+/)
|
|
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
|
|
457
1566
|
}
|
|
458
1567
|
}
|
|
459
1568
|
|
|
@@ -461,6 +1570,29 @@ async function handleResume(state: SessionState): Promise<void> {
|
|
|
461
1570
|
* Handle project idea input
|
|
462
1571
|
*/
|
|
463
1572
|
async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
1573
|
+
const cwd = state.projectDir || process.cwd();
|
|
1574
|
+
|
|
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
|
+
|
|
464
1596
|
if (!state.claudeAuth || !state.openaiAuth) {
|
|
465
1597
|
console.log();
|
|
466
1598
|
printError('Authentication required');
|
|
@@ -474,36 +1606,117 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
|
474
1606
|
}
|
|
475
1607
|
}
|
|
476
1608
|
|
|
1609
|
+
// Generate a meaningful project name
|
|
1610
|
+
const projectName = generateProjectName(idea);
|
|
1611
|
+
const projectDir = path.join(cwd, projectName);
|
|
1612
|
+
|
|
477
1613
|
console.log();
|
|
478
1614
|
console.log(theme.primary.bold(' Creating Project'));
|
|
479
1615
|
console.log(` ${theme.dim('Idea:')} ${idea}`);
|
|
1616
|
+
console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
|
|
480
1617
|
console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
|
|
481
1618
|
console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
|
|
482
1619
|
console.log();
|
|
483
1620
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
.
|
|
488
|
-
.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1621
|
+
const spec: ProjectSpec = {
|
|
1622
|
+
idea,
|
|
1623
|
+
name: projectName,
|
|
1624
|
+
language: state.language,
|
|
1625
|
+
openaiModel: state.model,
|
|
1626
|
+
outputDir: cwd,
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
// Generate scaffold
|
|
1630
|
+
startSpinner('Creating project structure...');
|
|
1631
|
+
const scaffoldResult = await generateProject(spec, cwd);
|
|
1632
|
+
|
|
1633
|
+
if (!scaffoldResult.success) {
|
|
1634
|
+
failSpinner('Scaffolding failed');
|
|
1635
|
+
printError(scaffoldResult.error || 'Failed to create project');
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
|
|
1640
|
+
|
|
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
|
+
|
|
1650
|
+
const workflowResult = await runWorkflow(spec, {
|
|
1651
|
+
projectDir,
|
|
1652
|
+
consensusConfig: {
|
|
1653
|
+
reviewer: state.reviewer,
|
|
1654
|
+
arbitrator: state.arbitrator,
|
|
1655
|
+
enableArbitration: state.enableArbitration,
|
|
1656
|
+
geminiModel: state.geminiModel,
|
|
1657
|
+
},
|
|
1658
|
+
onProgress: (phase, message) => {
|
|
1659
|
+
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
492
1662
|
|
|
493
|
-
|
|
494
|
-
|
|
1663
|
+
stopSpinner();
|
|
1664
|
+
|
|
1665
|
+
console.log();
|
|
1666
|
+
if (workflowResult.success) {
|
|
1667
|
+
// Update README with project description
|
|
1668
|
+
await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
|
|
1669
|
+
|
|
1670
|
+
printSuccess('Project created successfully!');
|
|
1671
|
+
console.log(` ${theme.dim('Location:')} ${projectDir}`);
|
|
1672
|
+
state.projectDir = projectDir;
|
|
1673
|
+
} else {
|
|
1674
|
+
printError(workflowResult.error || 'Workflow failed');
|
|
1675
|
+
}
|
|
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();
|
|
495
1708
|
|
|
496
1709
|
const spec: ProjectSpec = {
|
|
497
1710
|
idea,
|
|
498
1711
|
name: projectName,
|
|
499
1712
|
language: state.language,
|
|
500
1713
|
openaiModel: state.model,
|
|
501
|
-
outputDir:
|
|
1714
|
+
outputDir: cwd,
|
|
502
1715
|
};
|
|
503
1716
|
|
|
504
1717
|
// Generate scaffold
|
|
505
1718
|
startSpinner('Creating project structure...');
|
|
506
|
-
const scaffoldResult = await generateProject(spec,
|
|
1719
|
+
const scaffoldResult = await generateProject(spec, cwd);
|
|
507
1720
|
|
|
508
1721
|
if (!scaffoldResult.success) {
|
|
509
1722
|
failSpinner('Scaffolding failed');
|
|
@@ -513,13 +1726,23 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
|
513
1726
|
|
|
514
1727
|
succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
|
|
515
1728
|
|
|
516
|
-
// Run workflow
|
|
1729
|
+
// Run workflow with reviewer/arbitrator settings
|
|
517
1730
|
console.log();
|
|
518
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
|
+
}
|
|
519
1736
|
console.log();
|
|
520
1737
|
|
|
521
1738
|
const workflowResult = await runWorkflow(spec, {
|
|
522
1739
|
projectDir,
|
|
1740
|
+
consensusConfig: {
|
|
1741
|
+
reviewer: state.reviewer,
|
|
1742
|
+
arbitrator: state.arbitrator,
|
|
1743
|
+
enableArbitration: state.enableArbitration,
|
|
1744
|
+
geminiModel: state.geminiModel,
|
|
1745
|
+
},
|
|
523
1746
|
onProgress: (phase, message) => {
|
|
524
1747
|
console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
|
|
525
1748
|
},
|
|
@@ -529,6 +1752,9 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
|
529
1752
|
|
|
530
1753
|
console.log();
|
|
531
1754
|
if (workflowResult.success) {
|
|
1755
|
+
// Update README with project description
|
|
1756
|
+
await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
|
|
1757
|
+
|
|
532
1758
|
printSuccess('Project created successfully!');
|
|
533
1759
|
console.log(` ${theme.dim('Location:')} ${projectDir}`);
|
|
534
1760
|
state.projectDir = projectDir;
|
|
@@ -543,20 +1769,36 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
|
|
|
543
1769
|
export async function startInteractiveMode(): Promise<void> {
|
|
544
1770
|
console.clear();
|
|
545
1771
|
|
|
546
|
-
// Initialize state
|
|
1772
|
+
// Initialize state from saved config
|
|
547
1773
|
const config = await loadConfig();
|
|
548
1774
|
const state: SessionState = {
|
|
549
1775
|
projectDir: process.cwd(),
|
|
550
1776
|
language: config.project.default_language,
|
|
551
1777
|
model: config.apis.openai.model,
|
|
1778
|
+
geminiModel: 'gemini-2.0-flash',
|
|
552
1779
|
claudeAuth: false,
|
|
553
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,
|
|
554
1786
|
};
|
|
555
1787
|
|
|
556
1788
|
// Draw header
|
|
557
1789
|
drawHeader();
|
|
558
1790
|
console.log();
|
|
559
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
|
+
|
|
560
1802
|
// Check and perform authentication
|
|
561
1803
|
const isAuthenticated = await ensureAuthentication(state);
|
|
562
1804
|
|
|
@@ -579,11 +1821,11 @@ export async function startInteractiveMode(): Promise<void> {
|
|
|
579
1821
|
|
|
580
1822
|
// Input loop
|
|
581
1823
|
const promptUser = (): void => {
|
|
582
|
-
|
|
1824
|
+
drawInputBoxTop(state);
|
|
583
1825
|
|
|
584
1826
|
rl.question(getPrompt(), async (input) => {
|
|
585
|
-
//
|
|
586
|
-
|
|
1827
|
+
// Draw bottom of input box after user presses enter
|
|
1828
|
+
drawInputBoxBottom();
|
|
587
1829
|
|
|
588
1830
|
const shouldContinue = await handleInput(input, state);
|
|
589
1831
|
|