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