popeye-cli 1.0.0 → 1.0.1

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.
@@ -1,188 +1,301 @@
1
1
  /**
2
2
  * Interactive mode
3
- * REPL-style interface for Popeye CLI
3
+ * Claude Code-style interface for Popeye CLI
4
4
  */
5
5
  import * as readline from 'node:readline';
6
- import { getAuthStatusForDisplay } from '../auth/index.js';
6
+ import { getAuthStatusForDisplay, authenticateClaude, authenticateOpenAI } from '../auth/index.js';
7
7
  import { runWorkflow, resumeWorkflow, getWorkflowStatus, getWorkflowSummary, } from '../workflow/index.js';
8
8
  import { generateProject } from '../generators/index.js';
9
9
  import { loadConfig } from '../config/index.js';
10
- import { printHeader, printSection, printSuccess, printError, printWarning, printInfo, printKeyValue, printAuthStatus, startSpinner, succeedSpinner, failSpinner, stopSpinner, clearConsole, theme, } from './output.js';
10
+ import { printSuccess, printError, printWarning, printInfo, printKeyValue, startSpinner, succeedSpinner, failSpinner, stopSpinner, theme, } from './output.js';
11
11
  /**
12
- * Available slash commands
12
+ * Box drawing characters for Claude Code-style UI
13
13
  */
14
- const commands = {
15
- '/help': {
16
- description: 'Show available commands',
17
- handler: handleHelp,
18
- },
19
- '/status': {
20
- description: 'Show current project status',
21
- handler: handleStatus,
22
- },
23
- '/auth': {
24
- description: 'Check authentication status',
25
- handler: handleAuth,
26
- },
27
- '/config': {
28
- description: 'Show current configuration',
29
- handler: handleConfig,
30
- },
31
- '/language': {
32
- description: 'Set output language (python/typescript)',
33
- handler: handleLanguage,
34
- },
35
- '/model': {
36
- description: 'Set OpenAI model',
37
- handler: handleModel,
38
- },
39
- '/project': {
40
- description: 'Set project directory',
41
- handler: handleProject,
42
- },
43
- '/resume': {
44
- description: 'Resume current project workflow',
45
- handler: handleResume,
46
- },
47
- '/clear': {
48
- description: 'Clear the screen',
49
- handler: handleClear,
50
- },
51
- '/exit': {
52
- description: 'Exit interactive mode',
53
- handler: handleExit,
54
- },
55
- '/quit': {
56
- description: 'Exit interactive mode',
57
- handler: handleExit,
58
- },
14
+ const box = {
15
+ topLeft: '',
16
+ topRight: '',
17
+ bottomLeft: '╰',
18
+ bottomRight: '╯',
19
+ horizontal: '',
20
+ vertical: '',
21
+ leftT: '├',
22
+ rightT: '┤',
59
23
  };
60
24
  /**
61
- * Start interactive mode
25
+ * Get terminal width
62
26
  */
63
- export async function startInteractiveMode() {
64
- clearConsole();
65
- printHeader('Popeye CLI - Interactive Mode');
66
- printInfo('Type your project idea or use /help for commands');
27
+ function getTerminalWidth() {
28
+ return process.stdout.columns || 80;
29
+ }
30
+ /**
31
+ * Draw the header box
32
+ */
33
+ function drawHeader() {
34
+ const width = getTerminalWidth();
35
+ const title = ' Popeye CLI ';
36
+ const subtitle = ' AI-Powered Code Generation ';
37
+ // Top border
38
+ console.log(theme.dim(box.topLeft + box.horizontal.repeat(width - 2) + box.topRight));
39
+ // Title line
40
+ const titlePadding = Math.floor((width - title.length - 2) / 2);
41
+ console.log(theme.dim(box.vertical) +
42
+ ' '.repeat(titlePadding) +
43
+ theme.primary.bold(title) +
44
+ ' '.repeat(width - titlePadding - title.length - 2) +
45
+ theme.dim(box.vertical));
46
+ // Subtitle line
47
+ const subPadding = Math.floor((width - subtitle.length - 2) / 2);
48
+ console.log(theme.dim(box.vertical) +
49
+ ' '.repeat(subPadding) +
50
+ theme.secondary(subtitle) +
51
+ ' '.repeat(width - subPadding - subtitle.length - 2) +
52
+ theme.dim(box.vertical));
53
+ // Bottom border
54
+ console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
55
+ }
56
+ /**
57
+ * Draw the input box frame
58
+ */
59
+ function drawInputFrame(state) {
60
+ const width = getTerminalWidth();
61
+ // Status items
62
+ const langStatus = `${state.language}`;
63
+ const modelStatus = `${state.model}`;
64
+ const authStatus = state.claudeAuth && state.openaiAuth ? '●' : '○';
65
+ const authColor = state.claudeAuth && state.openaiAuth ? theme.success : theme.warning;
66
+ // Build status line
67
+ const statusItems = [
68
+ theme.dim('lang:') + theme.primary(langStatus),
69
+ theme.dim('model:') + theme.secondary(modelStatus),
70
+ authColor(authStatus) + theme.dim(' auth'),
71
+ ];
72
+ const statusText = statusItems.join(theme.dim(' │ '));
73
+ // Calculate visible length (without ANSI codes)
74
+ // eslint-disable-next-line no-control-regex
75
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
76
+ const statusLen = stripAnsi(statusText).length;
77
+ // Top line with status
78
+ const topLine = box.topLeft +
79
+ box.horizontal.repeat(2) +
80
+ ' ' + stripAnsi(statusText) + ' ' +
81
+ box.horizontal.repeat(Math.max(0, width - statusLen - 6)) +
82
+ box.topRight;
83
+ console.log(theme.dim(topLine.slice(0, 1)) + statusText + theme.dim(topLine.slice(statusLen + 4)));
84
+ }
85
+ /**
86
+ * Clear screen and redraw UI
87
+ */
88
+ function redrawUI(_state) {
89
+ console.clear();
90
+ drawHeader();
67
91
  console.log();
68
- // Initialize state
69
- const config = await loadConfig();
70
- const state = {
71
- projectDir: process.cwd(),
72
- language: config.project.default_language,
73
- model: config.apis.openai.model,
74
- authenticated: false,
75
- };
76
- // Check authentication
77
- const authStatus = await getAuthStatusForDisplay();
78
- state.authenticated = authStatus.claude.authenticated && authStatus.openai.authenticated;
79
- if (!state.authenticated) {
80
- printWarning('Not fully authenticated. Run /auth to check status.');
81
- console.log();
92
+ }
93
+ /**
94
+ * Prompt for input with styled prompt
95
+ */
96
+ function getPrompt() {
97
+ return theme.dim(box.vertical) + ' ' + theme.primary('❯') + ' ';
98
+ }
99
+ /**
100
+ * Check and perform authentication
101
+ */
102
+ async function ensureAuthentication(state) {
103
+ const status = await getAuthStatusForDisplay();
104
+ state.claudeAuth = status.claude.authenticated;
105
+ state.openaiAuth = status.openai.authenticated;
106
+ if (state.claudeAuth && state.openaiAuth) {
107
+ return true;
82
108
  }
83
- // Create readline interface
84
- const rl = readline.createInterface({
85
- input: process.stdin,
86
- output: process.stdout,
87
- prompt: theme.primary('popeye> '),
88
- });
89
- rl.prompt();
90
- rl.on('line', async (line) => {
91
- const input = line.trim();
92
- if (!input) {
93
- rl.prompt();
94
- return;
95
- }
96
- try {
97
- if (input.startsWith('/')) {
98
- // Handle command
99
- const [command, ...args] = input.split(/\s+/);
100
- const cmd = commands[command.toLowerCase()];
101
- if (cmd) {
102
- await cmd.handler(args, state);
109
+ console.log();
110
+ printWarning('Authentication required to continue');
111
+ console.log();
112
+ // Authenticate Claude if needed
113
+ if (!state.claudeAuth) {
114
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude CLI') + theme.dim(' - Browser authentication'));
115
+ console.log(theme.dim(box.vertical));
116
+ const rl = readline.createInterface({
117
+ input: process.stdin,
118
+ output: process.stdout,
119
+ });
120
+ const proceed = await new Promise((resolve) => {
121
+ rl.question(theme.dim(box.vertical) + ' Press Enter to open browser (or "skip" to skip): ', (answer) => {
122
+ rl.close();
123
+ resolve(answer.toLowerCase() !== 'skip');
124
+ });
125
+ });
126
+ if (proceed) {
127
+ startSpinner('Opening browser for Claude authentication...');
128
+ try {
129
+ const success = await authenticateClaude();
130
+ if (success) {
131
+ succeedSpinner('Claude authenticated');
132
+ state.claudeAuth = true;
103
133
  }
104
134
  else {
105
- printError(`Unknown command: ${command}. Use /help for available commands.`);
135
+ failSpinner('Claude authentication failed');
106
136
  }
107
137
  }
108
- else {
109
- // Treat as project idea
110
- await handleIdea(input, state);
138
+ catch (err) {
139
+ failSpinner('Claude authentication failed');
140
+ printError(err instanceof Error ? err.message : 'Authentication failed');
111
141
  }
112
142
  }
113
- catch (error) {
114
- printError(error instanceof Error ? error.message : 'Unknown error');
115
- }
116
143
  console.log();
117
- rl.prompt();
118
- });
119
- rl.on('close', () => {
144
+ }
145
+ // Authenticate OpenAI if needed
146
+ if (!state.openaiAuth) {
147
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - API key required'));
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...');
161
+ try {
162
+ const success = await authenticateOpenAI();
163
+ if (success) {
164
+ succeedSpinner('OpenAI authenticated');
165
+ state.openaiAuth = true;
166
+ }
167
+ else {
168
+ failSpinner('OpenAI authentication failed');
169
+ }
170
+ }
171
+ catch (err) {
172
+ failSpinner('OpenAI authentication failed');
173
+ printError(err instanceof Error ? err.message : 'Authentication failed');
174
+ }
175
+ }
120
176
  console.log();
121
- printInfo('Goodbye!');
122
- process.exit(0);
123
- });
177
+ }
178
+ return state.claudeAuth && state.openaiAuth;
124
179
  }
125
180
  /**
126
- * Handle /help command
181
+ * Display help
127
182
  */
128
- async function handleHelp(_args, _state) {
129
- printSection('Available Commands');
130
- for (const [cmd, info] of Object.entries(commands)) {
131
- console.log(` ${theme.primary(cmd.padEnd(15))} ${info.description}`);
183
+ function showHelp() {
184
+ console.log();
185
+ console.log(theme.primary.bold(' Commands:'));
186
+ console.log();
187
+ const commands = [
188
+ ['/help', 'Show this help message'],
189
+ ['/status', 'Show current project status'],
190
+ ['/auth', 'Re-authenticate services'],
191
+ ['/config', 'Show configuration'],
192
+ ['/language <lang>', 'Set language (python/typescript)'],
193
+ ['/model <model>', 'Set OpenAI model'],
194
+ ['/resume', 'Resume interrupted project'],
195
+ ['/clear', 'Clear screen'],
196
+ ['/exit', 'Exit Popeye'],
197
+ ];
198
+ for (const [cmd, desc] of commands) {
199
+ console.log(` ${theme.primary(cmd.padEnd(20))} ${theme.dim(desc)}`);
132
200
  }
133
201
  console.log();
134
- printInfo('Or type a project idea to start creating');
202
+ console.log(theme.secondary(' Or just type your project idea to get started!'));
203
+ console.log();
204
+ }
205
+ /**
206
+ * Handle a command or idea
207
+ */
208
+ async function handleInput(input, state) {
209
+ const trimmed = input.trim();
210
+ if (!trimmed)
211
+ return true;
212
+ // Handle commands
213
+ if (trimmed.startsWith('/')) {
214
+ const [cmd, ...args] = trimmed.split(/\s+/);
215
+ const command = cmd.toLowerCase();
216
+ switch (command) {
217
+ case '/help':
218
+ showHelp();
219
+ break;
220
+ case '/exit':
221
+ case '/quit':
222
+ case '/q':
223
+ console.log();
224
+ printInfo('Goodbye!');
225
+ return false;
226
+ case '/clear':
227
+ redrawUI(state);
228
+ break;
229
+ case '/status':
230
+ await handleStatus(state);
231
+ break;
232
+ case '/auth':
233
+ await ensureAuthentication(state);
234
+ break;
235
+ case '/config':
236
+ await handleConfig(state);
237
+ break;
238
+ case '/language':
239
+ handleLanguage(args, state);
240
+ break;
241
+ case '/model':
242
+ handleModel(args, state);
243
+ break;
244
+ case '/resume':
245
+ await handleResume(state);
246
+ break;
247
+ default:
248
+ printError(`Unknown command: ${cmd}`);
249
+ printInfo('Type /help for available commands');
250
+ }
251
+ return true;
252
+ }
253
+ // Handle as project idea
254
+ await handleIdea(trimmed, state);
255
+ return true;
135
256
  }
136
257
  /**
137
258
  * Handle /status command
138
259
  */
139
- async function handleStatus(_args, state) {
260
+ async function handleStatus(state) {
261
+ console.log();
140
262
  if (!state.projectDir) {
141
- printInfo('No project directory set. Use /project <dir> to set one.');
263
+ printInfo('No active project');
142
264
  return;
143
265
  }
144
266
  const status = await getWorkflowStatus(state.projectDir);
145
267
  if (!status.exists) {
146
268
  printInfo('No project found in current directory');
147
269
  printKeyValue('Directory', state.projectDir);
148
- printKeyValue('Language', state.language);
149
- printKeyValue('Model', state.model);
150
270
  return;
151
271
  }
152
272
  const summary = await getWorkflowSummary(state.projectDir);
153
273
  console.log(summary);
154
274
  }
155
- /**
156
- * Handle /auth command
157
- */
158
- async function handleAuth(_args, state) {
159
- const status = await getAuthStatusForDisplay();
160
- printAuthStatus(status);
161
- state.authenticated = status.claude.authenticated && status.openai.authenticated;
162
- if (!state.authenticated) {
163
- console.log();
164
- printInfo('Run "popeye-cli auth login" to authenticate');
165
- }
166
- }
167
275
  /**
168
276
  * Handle /config command
169
277
  */
170
- async function handleConfig(_args, state) {
278
+ async function handleConfig(state) {
171
279
  const config = await loadConfig();
172
- printSection('Current Session');
173
- printKeyValue('Project Dir', state.projectDir || 'Not set');
174
- printKeyValue('Language', state.language);
175
- printKeyValue('Model', state.model);
176
- printKeyValue('Authenticated', state.authenticated ? 'Yes' : 'No');
177
- printSection('Configuration');
178
- printKeyValue('Threshold', `${config.consensus.threshold}%`);
179
- printKeyValue('Max Iterations', config.consensus.max_disagreements.toString());
280
+ console.log();
281
+ console.log(theme.primary.bold(' Session:'));
282
+ console.log(` ${theme.dim('Directory:')} ${state.projectDir || 'Not set'}`);
283
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
284
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
285
+ console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('●') : theme.error('○')}`);
286
+ console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('●') : theme.error('○')}`);
287
+ console.log();
288
+ console.log(theme.primary.bold(' Consensus:'));
289
+ console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
290
+ console.log(` ${theme.dim('Max Iterations:')} ${config.consensus.max_disagreements}`);
291
+ console.log();
180
292
  }
181
293
  /**
182
294
  * Handle /language command
183
295
  */
184
- async function handleLanguage(args, state) {
296
+ function handleLanguage(args, state) {
185
297
  if (args.length === 0) {
298
+ console.log();
186
299
  printKeyValue('Current language', state.language);
187
300
  printInfo('Use /language <python|typescript> to change');
188
301
  return;
@@ -193,16 +306,18 @@ async function handleLanguage(args, state) {
193
306
  return;
194
307
  }
195
308
  state.language = lang;
196
- printSuccess(`Language set to: ${lang}`);
309
+ console.log();
310
+ printSuccess(`Language set to ${lang}`);
197
311
  }
198
312
  /**
199
313
  * Handle /model command
200
314
  */
201
- async function handleModel(args, state) {
315
+ function handleModel(args, state) {
202
316
  const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
203
317
  if (args.length === 0) {
318
+ console.log();
204
319
  printKeyValue('Current model', state.model);
205
- printInfo(`Use /model <${validModels.join('|')}> to change`);
320
+ printInfo(`Available: ${validModels.join(', ')}`);
206
321
  return;
207
322
  }
208
323
  const model = args[0];
@@ -211,32 +326,19 @@ async function handleModel(args, state) {
211
326
  return;
212
327
  }
213
328
  state.model = model;
214
- printSuccess(`Model set to: ${model}`);
215
- }
216
- /**
217
- * Handle /project command
218
- */
219
- async function handleProject(args, state) {
220
- const path = await import('node:path');
221
- if (args.length === 0) {
222
- printKeyValue('Current directory', state.projectDir || 'Not set');
223
- printInfo('Use /project <directory> to change');
224
- return;
225
- }
226
- const dir = path.resolve(args[0]);
227
- state.projectDir = dir;
228
- printSuccess(`Project directory set to: ${dir}`);
329
+ console.log();
330
+ printSuccess(`Model set to ${model}`);
229
331
  }
230
332
  /**
231
333
  * Handle /resume command
232
334
  */
233
- async function handleResume(_args, state) {
335
+ async function handleResume(state) {
234
336
  if (!state.projectDir) {
235
- printError('No project directory set. Use /project <dir> first.');
337
+ printError('No project directory set');
236
338
  return;
237
339
  }
238
- if (!state.authenticated) {
239
- printError('Not authenticated. Run "popeye-cli auth login" first.');
340
+ if (!state.claudeAuth || !state.openaiAuth) {
341
+ printError('Authentication required. Run /auth first.');
240
342
  return;
241
343
  }
242
344
  const status = await getWorkflowStatus(state.projectDir);
@@ -244,13 +346,15 @@ async function handleResume(_args, state) {
244
346
  printError('No project found to resume');
245
347
  return;
246
348
  }
349
+ console.log();
247
350
  printInfo('Resuming workflow...');
248
351
  console.log();
249
352
  const result = await resumeWorkflow(state.projectDir, {
250
353
  onProgress: (phase, message) => {
251
- console.log(` [${phase}] ${message}`);
354
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
252
355
  },
253
356
  });
357
+ console.log();
254
358
  if (result.success) {
255
359
  printSuccess('Workflow completed!');
256
360
  }
@@ -258,33 +362,28 @@ async function handleResume(_args, state) {
258
362
  printError(result.error || 'Workflow failed');
259
363
  }
260
364
  }
261
- /**
262
- * Handle /clear command
263
- */
264
- async function handleClear(_args, _state) {
265
- clearConsole();
266
- }
267
- /**
268
- * Handle /exit command
269
- */
270
- async function handleExit(_args, _state) {
271
- printInfo('Goodbye!');
272
- process.exit(0);
273
- }
274
365
  /**
275
366
  * Handle project idea input
276
367
  */
277
368
  async function handleIdea(idea, state) {
278
- if (!state.authenticated) {
279
- printError('Not authenticated. Run /auth to check status.');
280
- return;
369
+ if (!state.claudeAuth || !state.openaiAuth) {
370
+ console.log();
371
+ printError('Authentication required');
372
+ printInfo('Running authentication flow...');
373
+ console.log();
374
+ const authenticated = await ensureAuthentication(state);
375
+ if (!authenticated) {
376
+ printWarning('Skipping project creation - authentication incomplete');
377
+ return;
378
+ }
281
379
  }
282
- printSection('Creating Project');
283
- printKeyValue('Idea', idea);
284
- printKeyValue('Language', state.language);
285
- printKeyValue('Model', state.model);
286
380
  console.log();
287
- // Generate project name
381
+ console.log(theme.primary.bold(' Creating Project'));
382
+ console.log(` ${theme.dim('Idea:')} ${idea}`);
383
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
384
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
385
+ console.log();
386
+ // Generate project name from idea
288
387
  const projectName = idea
289
388
  .toLowerCase()
290
389
  .replace(/[^a-z0-9\s]/g, '')
@@ -311,20 +410,77 @@ async function handleIdea(idea, state) {
311
410
  }
312
411
  succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
313
412
  // Run workflow
413
+ console.log();
414
+ printInfo('Starting AI workflow...');
415
+ console.log();
314
416
  const workflowResult = await runWorkflow(spec, {
315
417
  projectDir,
316
418
  onProgress: (phase, message) => {
317
- console.log(` [${phase}] ${message}`);
419
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
318
420
  },
319
421
  });
320
422
  stopSpinner();
423
+ console.log();
321
424
  if (workflowResult.success) {
322
425
  printSuccess('Project created successfully!');
323
- printKeyValue('Location', projectDir);
426
+ console.log(` ${theme.dim('Location:')} ${projectDir}`);
324
427
  state.projectDir = projectDir;
325
428
  }
326
429
  else {
327
430
  printError(workflowResult.error || 'Workflow failed');
328
431
  }
329
432
  }
433
+ /**
434
+ * Start interactive mode with auto-authentication
435
+ */
436
+ export async function startInteractiveMode() {
437
+ console.clear();
438
+ // Initialize state
439
+ const config = await loadConfig();
440
+ const state = {
441
+ projectDir: process.cwd(),
442
+ language: config.project.default_language,
443
+ model: config.apis.openai.model,
444
+ claudeAuth: false,
445
+ openaiAuth: false,
446
+ };
447
+ // Draw header
448
+ drawHeader();
449
+ console.log();
450
+ // Check and perform authentication
451
+ const isAuthenticated = await ensureAuthentication(state);
452
+ if (!isAuthenticated) {
453
+ console.log();
454
+ printWarning('Some services are not authenticated. Some features may not work.');
455
+ printInfo('You can authenticate later with /auth');
456
+ }
457
+ else {
458
+ console.log();
459
+ printSuccess('Ready! Type your project idea or /help for commands');
460
+ }
461
+ console.log();
462
+ // Create readline interface
463
+ const rl = readline.createInterface({
464
+ input: process.stdin,
465
+ output: process.stdout,
466
+ });
467
+ // Input loop
468
+ const promptUser = () => {
469
+ drawInputFrame(state);
470
+ rl.question(getPrompt(), async (input) => {
471
+ // Clear the input frame line
472
+ process.stdout.write('\x1b[1A\x1b[2K'); // Move up and clear
473
+ const shouldContinue = await handleInput(input, state);
474
+ if (shouldContinue) {
475
+ console.log();
476
+ promptUser();
477
+ }
478
+ else {
479
+ rl.close();
480
+ process.exit(0);
481
+ }
482
+ });
483
+ };
484
+ promptUser();
485
+ }
330
486
  //# sourceMappingURL=interactive.js.map