popeye-cli 1.3.0 → 1.4.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +115 -17
  3. package/dist/adapters/gemini.d.ts +2 -2
  4. package/dist/adapters/gemini.d.ts.map +1 -1
  5. package/dist/cli/index.d.ts +1 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +5 -2
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/interactive.d.ts.map +1 -1
  10. package/dist/cli/interactive.js +307 -22
  11. package/dist/cli/interactive.js.map +1 -1
  12. package/dist/config/index.d.ts +2 -0
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/schema.d.ts +4 -0
  15. package/dist/config/schema.d.ts.map +1 -1
  16. package/dist/config/schema.js +2 -1
  17. package/dist/config/schema.js.map +1 -1
  18. package/dist/generators/all.d.ts +30 -0
  19. package/dist/generators/all.d.ts.map +1 -1
  20. package/dist/generators/all.js +5 -5
  21. package/dist/generators/all.js.map +1 -1
  22. package/dist/types/consensus.d.ts +10 -20
  23. package/dist/types/consensus.d.ts.map +1 -1
  24. package/dist/types/consensus.js +8 -3
  25. package/dist/types/consensus.js.map +1 -1
  26. package/dist/types/index.d.ts +2 -2
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/index.js +2 -2
  29. package/dist/types/index.js.map +1 -1
  30. package/dist/types/project.d.ts +15 -16
  31. package/dist/types/project.d.ts.map +1 -1
  32. package/dist/types/project.js +15 -8
  33. package/dist/types/project.js.map +1 -1
  34. package/dist/types/workflow.d.ts +2 -0
  35. package/dist/types/workflow.d.ts.map +1 -1
  36. package/dist/types/workflow.js +2 -1
  37. package/dist/types/workflow.js.map +1 -1
  38. package/dist/upgrade/context.d.ts +37 -0
  39. package/dist/upgrade/context.d.ts.map +1 -0
  40. package/dist/upgrade/context.js +284 -0
  41. package/dist/upgrade/context.js.map +1 -0
  42. package/dist/upgrade/handlers.d.ts +103 -0
  43. package/dist/upgrade/handlers.d.ts.map +1 -0
  44. package/dist/upgrade/handlers.js +384 -0
  45. package/dist/upgrade/handlers.js.map +1 -0
  46. package/dist/upgrade/index.d.ts +26 -0
  47. package/dist/upgrade/index.d.ts.map +1 -0
  48. package/dist/upgrade/index.js +194 -0
  49. package/dist/upgrade/index.js.map +1 -0
  50. package/dist/upgrade/transitions.d.ts +34 -0
  51. package/dist/upgrade/transitions.d.ts.map +1 -0
  52. package/dist/upgrade/transitions.js +56 -0
  53. package/dist/upgrade/transitions.js.map +1 -0
  54. package/dist/workflow/plan-mode.d.ts.map +1 -1
  55. package/dist/workflow/plan-mode.js +41 -5
  56. package/dist/workflow/plan-mode.js.map +1 -1
  57. package/dist/workflow/task-workflow.d.ts.map +1 -1
  58. package/dist/workflow/task-workflow.js +3 -2
  59. package/dist/workflow/task-workflow.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/adapters/gemini.ts +2 -2
  62. package/src/cli/index.ts +5 -2
  63. package/src/cli/interactive.ts +353 -23
  64. package/src/config/schema.ts +2 -1
  65. package/src/generators/all.ts +5 -5
  66. package/src/types/consensus.ts +11 -5
  67. package/src/types/index.ts +2 -0
  68. package/src/types/project.ts +18 -9
  69. package/src/types/workflow.ts +2 -1
  70. package/src/upgrade/context.ts +332 -0
  71. package/src/upgrade/handlers.ts +477 -0
  72. package/src/upgrade/index.ts +244 -0
  73. package/src/upgrade/transitions.ts +80 -0
  74. package/src/workflow/plan-mode.ts +41 -7
  75. package/src/workflow/task-workflow.ts +3 -2
  76. package/tests/cli/model-command.test.ts +93 -0
  77. package/tests/types/project.test.ts +90 -15
  78. package/tests/types/workflow-schema.test.ts +59 -0
  79. package/tests/upgrade/context.test.ts +211 -0
  80. package/tests/upgrade/transitions.test.ts +85 -0
@@ -6,6 +6,12 @@
6
6
  import * as readline from 'node:readline';
7
7
  import { promises as fs } from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { createRequire } from 'node:module';
10
+
11
+ // Get package version
12
+ const require = createRequire(import.meta.url);
13
+ const packageJson = require('../../package.json');
14
+ const VERSION: string = packageJson.version;
9
15
  import {
10
16
  getAuthStatusForDisplay,
11
17
  authenticateClaude,
@@ -22,10 +28,12 @@ import {
22
28
  resumeWorkflow,
23
29
  getWorkflowStatus,
24
30
  getWorkflowSummary,
31
+ resetWorkflow,
25
32
  } from '../workflow/index.js';
26
33
  import {
27
34
  analyzeProjectProgress,
28
35
  verifyProjectCompletion,
36
+ storeSpecification,
29
37
  } from '../state/index.js';
30
38
  import { generateProject } from '../generators/index.js';
31
39
  import {
@@ -33,8 +41,14 @@ import {
33
41
  formatProjectForDisplay,
34
42
  } from '../state/registry.js';
35
43
  import { loadConfig, saveConfig } from '../config/index.js';
44
+ import { getValidUpgradeTargets, getTransitionDetails } from '../upgrade/transitions.js';
45
+ import { upgradeProject } from '../upgrade/index.js';
46
+ import { buildUpgradeContext } from '../upgrade/context.js';
47
+ import { OutputLanguageSchema, KNOWN_OPENAI_MODELS } from '../types/project.js';
36
48
  import type { ProjectSpec, OutputLanguage, OpenAIModel } from '../types/project.js';
37
- import type { AIProvider, GeminiModel } from '../types/consensus.js';
49
+ import { GeminiModelSchema, KNOWN_GEMINI_MODELS } from '../types/consensus.js';
50
+ import { OpenAIModelSchema } from '../types/project.js';
51
+ import type { AIProvider, GeminiModel, GrokModel } from '../types/consensus.js';
38
52
  import {
39
53
  printSuccess,
40
54
  printError,
@@ -61,6 +75,9 @@ interface PopeyeProjectConfig {
61
75
  projectName?: string;
62
76
  description?: string;
63
77
  notes?: string;
78
+ openaiModel?: OpenAIModel;
79
+ geminiModel?: GeminiModel;
80
+ grokModel?: GrokModel;
64
81
  }
65
82
 
66
83
  /**
@@ -91,7 +108,7 @@ async function readPopeyeConfig(projectDir: string): Promise<PopeyeProjectConfig
91
108
 
92
109
  switch (key) {
93
110
  case 'language':
94
- if (['python', 'typescript', 'fullstack'].includes(cleanValue)) {
111
+ if (OutputLanguageSchema.safeParse(cleanValue).success) {
95
112
  config.language = cleanValue as OutputLanguage;
96
113
  }
97
114
  break;
@@ -119,6 +136,21 @@ async function readPopeyeConfig(projectDir: string): Promise<PopeyeProjectConfig
119
136
  case 'projectName':
120
137
  config.projectName = cleanValue;
121
138
  break;
139
+ case 'openaiModel':
140
+ if (OpenAIModelSchema.safeParse(cleanValue).success) {
141
+ config.openaiModel = cleanValue as OpenAIModel;
142
+ }
143
+ break;
144
+ case 'geminiModel':
145
+ if (GeminiModelSchema.safeParse(cleanValue).success) {
146
+ config.geminiModel = cleanValue as GeminiModel;
147
+ }
148
+ break;
149
+ case 'grokModel':
150
+ if (cleanValue.length > 0) {
151
+ config.grokModel = cleanValue;
152
+ }
153
+ break;
122
154
  }
123
155
  }
124
156
  }
@@ -159,6 +191,12 @@ async function writePopeyeConfig(
159
191
  ): Promise<void> {
160
192
  const configPath = path.join(projectDir, 'popeye.md');
161
193
 
194
+ const modelLines = [
195
+ config.openaiModel ? `openaiModel: ${config.openaiModel}` : '',
196
+ config.geminiModel ? `geminiModel: ${config.geminiModel}` : '',
197
+ config.grokModel ? `grokModel: ${config.grokModel}` : '',
198
+ ].filter(Boolean).join('\n');
199
+
162
200
  const content = `---
163
201
  # Popeye Project Configuration
164
202
  language: ${config.language}
@@ -167,6 +205,7 @@ arbitrator: ${config.enableArbitration ? config.arbitrator : 'off'}
167
205
  created: ${config.created}
168
206
  lastRun: ${new Date().toISOString()}
169
207
  ${config.projectName ? `projectName: ${config.projectName}` : ''}
208
+ ${modelLines}
170
209
  ---
171
210
 
172
211
  # ${config.projectName || 'Popeye Project'}
@@ -217,6 +256,9 @@ function applyPopeyeConfig(state: SessionState, config: PopeyeProjectConfig): vo
217
256
  state.reviewer = config.reviewer;
218
257
  state.arbitrator = config.arbitrator;
219
258
  state.enableArbitration = config.enableArbitration;
259
+ if (config.openaiModel) state.openaiModel = config.openaiModel;
260
+ if (config.geminiModel) state.geminiModel = config.geminiModel;
261
+ if (config.grokModel) state.grokModel = config.grokModel;
220
262
  }
221
263
 
222
264
  // Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
@@ -241,8 +283,9 @@ const box = {
241
283
  interface SessionState {
242
284
  projectDir: string | null;
243
285
  language: OutputLanguage;
244
- model: OpenAIModel;
286
+ openaiModel: OpenAIModel;
245
287
  geminiModel: GeminiModel;
288
+ grokModel: GrokModel;
246
289
  claudeAuth: boolean;
247
290
  openaiAuth: boolean;
248
291
  geminiAuth: boolean;
@@ -264,7 +307,7 @@ function getTerminalWidth(): number {
264
307
  */
265
308
  function drawHeader(): void {
266
309
  const width = getTerminalWidth();
267
- const title = ' Popeye CLI ';
310
+ const title = ` Popeye CLI v${VERSION} `;
268
311
  const subtitle = ' Autonomous Code Generation with AI Consensus ';
269
312
 
270
313
  // Top border
@@ -780,7 +823,9 @@ function showHelp(): void {
780
823
  ['/config', 'Show/change configuration'],
781
824
  ['/config reviewer', 'Set reviewer (openai/gemini/grok)'],
782
825
  ['/config arbitrator', 'Set arbitrator (openai/gemini/grok/off)'],
783
- ['/lang <lang>', 'Set language (python/typescript/fullstack)'],
826
+ ['/lang <lang>', 'Set language (be/fe/fs/web/all)'],
827
+ ['/model [provider] [model]', 'Show/set AI model (openai/gemini/grok)'],
828
+ ['/upgrade [target]', 'Upgrade project type (e.g., fullstack -> all)'],
784
829
  ['/new <idea>', 'Force start a new project (skips existing check)'],
785
830
  ['/resume', 'Resume interrupted project'],
786
831
  ['/clear', 'Clear screen'],
@@ -933,6 +978,10 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
933
978
  await handleResume(state, args);
934
979
  break;
935
980
 
981
+ case '/upgrade':
982
+ await handleUpgrade(state, args);
983
+ break;
984
+
936
985
  case '/new':
937
986
  // Force start a new project even if existing projects found
938
987
  if (args.length === 0) {
@@ -1090,9 +1139,13 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
1090
1139
  }
1091
1140
  return;
1092
1141
 
1142
+ case 'model':
1143
+ handleModel(args.slice(1), state);
1144
+ return;
1145
+
1093
1146
  default:
1094
1147
  printError(`Unknown config option: ${subcommand}`);
1095
- printInfo('Options: reviewer, arbitrator, language');
1148
+ printInfo('Options: reviewer, arbitrator, language, model');
1096
1149
  return;
1097
1150
  }
1098
1151
  }
@@ -1110,11 +1163,16 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
1110
1163
  console.log(` ${theme.dim('Grok:')} ${state.grokAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
1111
1164
  console.log();
1112
1165
  console.log(theme.primary.bold(' AI Configuration:'));
1113
- const configReviewerName = state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : state.reviewer === 'grok' ? 'Grok' : 'Gemini';
1166
+ const configReviewerName = state.reviewer === 'openai' ? `OpenAI (${state.openaiModel})` : state.reviewer === 'grok' ? `Grok (${state.grokModel})` : `Gemini (${state.geminiModel})`;
1114
1167
  const configArbitratorName = state.arbitrator === 'openai' ? 'OpenAI' : state.arbitrator === 'grok' ? 'Grok' : 'Gemini';
1115
1168
  console.log(` ${theme.dim('Reviewer:')} ${theme.primary(configReviewerName)}`);
1116
1169
  console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(configArbitratorName) : theme.dim('Disabled')}`);
1117
1170
  console.log();
1171
+ console.log(theme.primary.bold(' Models:'));
1172
+ console.log(` ${theme.dim('OpenAI:')} ${theme.primary(state.openaiModel)}`);
1173
+ console.log(` ${theme.dim('Gemini:')} ${theme.primary(state.geminiModel)}`);
1174
+ console.log(` ${theme.dim('Grok:')} ${theme.primary(state.grokModel)}`);
1175
+ console.log();
1118
1176
  console.log(theme.primary.bold(' Consensus:'));
1119
1177
  console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
1120
1178
  console.log(` ${theme.dim('Max Iters:')} ${config.consensus.max_disagreements}`);
@@ -1123,6 +1181,7 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
1123
1181
  console.log(theme.dim(' /config reviewer <openai|gemini|grok>'));
1124
1182
  console.log(theme.dim(' /config arbitrator <openai|gemini|grok|off>'));
1125
1183
  console.log(theme.dim(' /config language <be|fe|fs|web|all>'));
1184
+ console.log(theme.dim(' /config model <provider> <model>'));
1126
1185
  console.log();
1127
1186
  }
1128
1187
 
@@ -1178,27 +1237,281 @@ function handleLanguage(args: string[], state: SessionState): void {
1178
1237
  }
1179
1238
 
1180
1239
  /**
1181
- * Handle /model command
1240
+ * Available models per provider for display
1182
1241
  */
1183
- function handleModel(args: string[], state: SessionState): void {
1184
- const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
1242
+ const KNOWN_MODELS: Record<string, readonly string[]> = {
1243
+ openai: KNOWN_OPENAI_MODELS,
1244
+ gemini: KNOWN_GEMINI_MODELS,
1245
+ grok: ['grok-3', 'grok-3-mini', 'grok-2'],
1246
+ };
1185
1247
 
1248
+ /**
1249
+ * Handle /model command - multi-provider model switching
1250
+ */
1251
+ function handleModel(args: string[], state: SessionState): void {
1252
+ // /model (no args) -> show all provider models
1186
1253
  if (args.length === 0) {
1187
1254
  console.log();
1188
- printKeyValue('Current model', state.model);
1189
- printInfo(`Available: ${validModels.join(', ')}`);
1255
+ console.log(theme.primary.bold(' Models:'));
1256
+ console.log(` ${theme.dim('OpenAI:')} ${theme.primary(state.openaiModel)}`);
1257
+ console.log(` ${theme.dim('Gemini:')} ${theme.primary(state.geminiModel)}`);
1258
+ console.log(` ${theme.dim('Grok:')} ${theme.primary(state.grokModel)}`);
1259
+ console.log();
1260
+ console.log(theme.secondary(' Usage:'));
1261
+ console.log(theme.dim(' /model <provider> <model> Set model for provider'));
1262
+ console.log(theme.dim(' /model <provider> list Show available models'));
1263
+ console.log(theme.dim(' /model <openai-model> Set OpenAI model (shortcut)'));
1190
1264
  return;
1191
1265
  }
1192
1266
 
1193
- const model = args[0] as OpenAIModel;
1194
- if (!validModels.includes(model)) {
1195
- printError(`Invalid model. Use one of: ${validModels.join(', ')}`);
1267
+ const firstArg = args[0].toLowerCase();
1268
+
1269
+ // Check if first arg is a provider
1270
+ if (firstArg === 'openai' || firstArg === 'gemini' || firstArg === 'grok') {
1271
+ const provider = firstArg;
1272
+
1273
+ // /model <provider> or /model <provider> list -> show known models
1274
+ if (args.length === 1 || args[1]?.toLowerCase() === 'list') {
1275
+ console.log();
1276
+ const currentModel = provider === 'openai' ? state.openaiModel
1277
+ : provider === 'gemini' ? state.geminiModel : state.grokModel;
1278
+ console.log(theme.primary.bold(` ${provider} models:`));
1279
+ console.log(` ${theme.dim('Current:')} ${theme.primary(currentModel)}`);
1280
+ console.log(` ${theme.dim('Known models:')}`);
1281
+ for (const m of KNOWN_MODELS[provider]) {
1282
+ const marker = m === currentModel ? theme.success(' (active)') : '';
1283
+ console.log(` ${theme.secondary(m)}${marker}`);
1284
+ }
1285
+ console.log();
1286
+ console.log(theme.dim(' Custom models are also accepted (e.g., gpt-5, gemini-2.5-pro)'));
1287
+ return;
1288
+ }
1289
+
1290
+ // /model <provider> <model> -> set model (warn if unknown but accept)
1291
+ const newModel = args[1];
1292
+
1293
+ if (!newModel || newModel.length === 0) {
1294
+ printError('Model name must not be empty.');
1295
+ return;
1296
+ }
1297
+
1298
+ const isKnown = KNOWN_MODELS[provider].includes(newModel);
1299
+
1300
+ if (provider === 'openai') {
1301
+ state.openaiModel = newModel;
1302
+ } else if (provider === 'gemini') {
1303
+ state.geminiModel = newModel;
1304
+ } else if (provider === 'grok') {
1305
+ state.grokModel = newModel;
1306
+ }
1307
+
1308
+ console.log();
1309
+ if (isKnown) {
1310
+ printSuccess(`${provider} model set to ${newModel}`);
1311
+ } else {
1312
+ printSuccess(`${provider} model set to ${newModel}`);
1313
+ printInfo(`Note: '${newModel}' is not in the known models list. Make sure it's a valid ${provider} model.`);
1314
+ }
1315
+ return;
1316
+ }
1317
+
1318
+ // Backward compat: /model <known-openai-model> (auto-detect known OpenAI model name)
1319
+ if ((KNOWN_OPENAI_MODELS as readonly string[]).includes(firstArg)) {
1320
+ state.openaiModel = firstArg;
1321
+ console.log();
1322
+ printSuccess(`OpenAI model set to ${firstArg}`);
1196
1323
  return;
1197
1324
  }
1198
1325
 
1199
- state.model = model;
1326
+ printError(`Unknown provider: ${firstArg}`);
1327
+ printInfo('Use: /model <openai|gemini|grok> <model>');
1328
+ }
1329
+
1330
+ /**
1331
+ * Handle /upgrade command - upgrade project type
1332
+ */
1333
+ async function handleUpgrade(state: SessionState, args: string[]): Promise<void> {
1334
+ if (!state.projectDir) {
1335
+ printError('No active project. Start or resume a project first.');
1336
+ return;
1337
+ }
1338
+
1339
+ // Load current state to get language
1340
+ const status = await getWorkflowStatus(state.projectDir);
1341
+ if (!status.exists || !status.state) {
1342
+ printError('No project state found in current directory.');
1343
+ return;
1344
+ }
1345
+
1346
+ const currentLanguage = status.state.language;
1347
+ const validTargets = getValidUpgradeTargets(currentLanguage);
1348
+
1349
+ if (validTargets.length === 0) {
1350
+ printInfo(`Project type '${currentLanguage}' is already at maximum scope. No upgrades available.`);
1351
+ return;
1352
+ }
1353
+
1354
+ // Determine target
1355
+ let targetLanguage: OutputLanguage | null = null;
1356
+
1357
+ if (args.length > 0) {
1358
+ const langAliases: Record<string, OutputLanguage> = {
1359
+ 'py': 'python', 'python': 'python', 'be': 'python', 'backend': 'python',
1360
+ 'ts': 'typescript', 'typescript': 'typescript', 'fe': 'typescript', 'frontend': 'typescript',
1361
+ 'fs': 'fullstack', 'fullstack': 'fullstack',
1362
+ 'web': 'website', 'website': 'website',
1363
+ 'all': 'all',
1364
+ };
1365
+ const resolved = langAliases[args[0].toLowerCase()];
1366
+ if (resolved && validTargets.includes(resolved)) {
1367
+ targetLanguage = resolved;
1368
+ } else if (resolved) {
1369
+ printError(`Cannot upgrade from '${currentLanguage}' to '${resolved}'.`);
1370
+ printInfo(`Valid targets: ${validTargets.join(', ')}`);
1371
+ return;
1372
+ } else {
1373
+ printError(`Unknown target: ${args[0]}`);
1374
+ printInfo(`Valid targets: ${validTargets.join(', ')}`);
1375
+ return;
1376
+ }
1377
+ } else {
1378
+ // Prompt selection
1379
+ const target = await promptSelection(
1380
+ `Upgrade '${currentLanguage}' project to:`,
1381
+ validTargets.map((t) => {
1382
+ const details = getTransitionDetails(currentLanguage, t);
1383
+ return {
1384
+ value: t,
1385
+ label: `${t} - ${details?.description || ''}`,
1386
+ };
1387
+ }),
1388
+ validTargets[0],
1389
+ );
1390
+ targetLanguage = target as OutputLanguage;
1391
+ }
1392
+
1393
+ if (!targetLanguage) return;
1394
+
1395
+ const transition = getTransitionDetails(currentLanguage, targetLanguage);
1396
+ if (!transition) return;
1397
+
1398
+ // Show dry-run summary
1399
+ console.log();
1400
+ console.log(theme.primary.bold(' Upgrade Summary:'));
1401
+ console.log(` ${theme.dim('From:')} ${theme.primary(currentLanguage)}`);
1402
+ console.log(` ${theme.dim('To:')} ${theme.primary(targetLanguage)}`);
1403
+ console.log(` ${theme.dim('New apps:')} ${transition.newApps.join(', ') || 'none'}`);
1404
+ console.log(` ${theme.dim('Restructure:')} ${transition.requiresRestructure ? 'Yes - code will be moved to apps/' : 'No'}`);
1405
+ console.log(` ${theme.dim('Description:')} ${transition.description}`);
1200
1406
  console.log();
1201
- printSuccess(`Model set to ${model}`);
1407
+
1408
+ // Confirm
1409
+ const confirmed = await promptYesNo(
1410
+ theme.primary('Proceed with upgrade?'),
1411
+ true,
1412
+ );
1413
+
1414
+ if (!confirmed) {
1415
+ printInfo('Upgrade cancelled.');
1416
+ return;
1417
+ }
1418
+
1419
+ // Execute upgrade
1420
+ console.log();
1421
+ startSpinner(`Upgrading ${currentLanguage} -> ${targetLanguage}...`);
1422
+
1423
+ const result = await upgradeProject(state.projectDir, targetLanguage);
1424
+
1425
+ if (result.success) {
1426
+ succeedSpinner(`Upgraded to ${targetLanguage}`);
1427
+ state.language = targetLanguage;
1428
+
1429
+ console.log();
1430
+ if (result.filesCreated.length > 0) {
1431
+ printInfo(`Created ${result.filesCreated.length} new files`);
1432
+ }
1433
+ if (result.filesMoved.length > 0) {
1434
+ printInfo(`Moved ${result.filesMoved.length} items`);
1435
+ }
1436
+ printSuccess(`Project upgraded from '${currentLanguage}' to '${targetLanguage}'`);
1437
+
1438
+ // Build upgrade context for planning
1439
+ console.log();
1440
+ startSpinner('Building expansion context...');
1441
+
1442
+ const upgradeContext = await buildUpgradeContext(
1443
+ state.projectDir,
1444
+ transition,
1445
+ status.state!.idea || 'Project expansion',
1446
+ currentLanguage,
1447
+ );
1448
+
1449
+ succeedSpinner('Expansion context ready');
1450
+
1451
+ // Show what will be planned
1452
+ console.log();
1453
+ console.log(theme.primary.bold(' Expansion Planning:'));
1454
+ console.log(` ${theme.dim('Existing apps:')} ${upgradeContext.existingApps.join(', ')} (already built)`);
1455
+ console.log(` ${theme.dim('New apps:')} ${upgradeContext.newApps.join(', ')} (will be planned)`);
1456
+ console.log();
1457
+
1458
+ // Ask user if they want to start planning now
1459
+ const startPlanning = await promptYesNo(
1460
+ theme.primary('Start planning the new apps now?'),
1461
+ true,
1462
+ );
1463
+
1464
+ if (!startPlanning) {
1465
+ printInfo('You can start planning later with /resume');
1466
+ return;
1467
+ }
1468
+
1469
+ // Reset state to plan phase so workflow re-plans for the expanded project
1470
+ // This clears old plan/milestones but keeps the idea and project metadata
1471
+ await resetWorkflow(state.projectDir, 'plan');
1472
+
1473
+ // Clear old specification so the idea gets re-expanded for the new project scope
1474
+ // The upgrade context will guide the planner to focus on new apps
1475
+ await storeSpecification(state.projectDir, '');
1476
+
1477
+ console.log();
1478
+ printInfo('Starting expansion planning...');
1479
+ console.log();
1480
+
1481
+ const workflowResult = await resumeWorkflow(state.projectDir, {
1482
+ consensusConfig: {
1483
+ reviewer: state.reviewer,
1484
+ arbitrator: state.arbitrator,
1485
+ enableArbitration: state.enableArbitration,
1486
+ openaiModel: state.openaiModel,
1487
+ geminiModel: state.geminiModel,
1488
+ grokModel: state.grokModel,
1489
+ },
1490
+ additionalContext: upgradeContext.summary,
1491
+ onProgress: (phase, message) => {
1492
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1493
+ },
1494
+ });
1495
+
1496
+ console.log();
1497
+ if (workflowResult.success) {
1498
+ printSuccess('Expansion planning and implementation complete!');
1499
+ console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
1500
+ } else if (workflowResult.rateLimitPaused) {
1501
+ console.log(` ${theme.warning('Rate Limit Reached')}`);
1502
+ console.log(` ${theme.dim(workflowResult.error || 'API rate limit exceeded')}`);
1503
+ console.log();
1504
+ console.log(` ${theme.info('Your progress has been saved.')}`);
1505
+ console.log(` ${theme.dim('Run')} ${theme.highlight('/resume')} ${theme.dim('after the rate limit resets to continue.')}`);
1506
+ } else {
1507
+ printError(workflowResult.error || 'Expansion workflow failed');
1508
+ printInfo('Use /resume to retry.');
1509
+ }
1510
+ } else {
1511
+ failSpinner('Upgrade failed');
1512
+ printError(result.error || 'Unknown error during upgrade');
1513
+ printInfo('All changes have been rolled back.');
1514
+ }
1202
1515
  }
1203
1516
 
1204
1517
  /**
@@ -1618,6 +1931,9 @@ async function handleResume(state: SessionState, args: string[]): Promise<void>
1618
1931
  reviewer: state.reviewer,
1619
1932
  arbitrator: state.arbitrator,
1620
1933
  enableArbitration: state.enableArbitration,
1934
+ openaiModel: state.openaiModel,
1935
+ geminiModel: state.geminiModel,
1936
+ grokModel: state.grokModel,
1621
1937
  },
1622
1938
  additionalContext,
1623
1939
  onProgress: (phase, message) => {
@@ -1771,7 +2087,7 @@ async function handleResume(state: SessionState, args: string[]): Promise<void>
1771
2087
  idea: discovered.idea || discovered.plan?.slice(0, 500) || `Continue developing ${projectName}`,
1772
2088
  name: projectName,
1773
2089
  language: discovered.language || state.language,
1774
- openaiModel: state.model,
2090
+ openaiModel: state.openaiModel,
1775
2091
  outputDir: state.projectDir,
1776
2092
  };
1777
2093
 
@@ -1789,6 +2105,9 @@ async function handleResume(state: SessionState, args: string[]): Promise<void>
1789
2105
  reviewer: state.reviewer,
1790
2106
  arbitrator: state.arbitrator,
1791
2107
  enableArbitration: state.enableArbitration,
2108
+ openaiModel: state.openaiModel,
2109
+ geminiModel: state.geminiModel,
2110
+ grokModel: state.grokModel,
1792
2111
  },
1793
2112
  onProgress: (phase, message) => {
1794
2113
  console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
@@ -2028,14 +2347,14 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
2028
2347
  console.log(` ${theme.dim('Idea:')} ${idea}`);
2029
2348
  console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
2030
2349
  console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
2031
- console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
2350
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.openaiModel)}`);
2032
2351
  console.log();
2033
2352
 
2034
2353
  const spec: ProjectSpec = {
2035
2354
  idea,
2036
2355
  name: projectName,
2037
2356
  language: state.language,
2038
- openaiModel: state.model,
2357
+ openaiModel: state.openaiModel,
2039
2358
  outputDir: cwd,
2040
2359
  };
2041
2360
 
@@ -2061,6 +2380,9 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
2061
2380
  lastRun: new Date().toISOString(),
2062
2381
  projectName,
2063
2382
  description: idea,
2383
+ openaiModel: state.openaiModel,
2384
+ geminiModel: state.geminiModel,
2385
+ grokModel: state.grokModel,
2064
2386
  });
2065
2387
  printInfo('Created popeye.md with project configuration');
2066
2388
 
@@ -2079,7 +2401,9 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
2079
2401
  reviewer: state.reviewer,
2080
2402
  arbitrator: state.arbitrator,
2081
2403
  enableArbitration: state.enableArbitration,
2404
+ openaiModel: state.openaiModel,
2082
2405
  geminiModel: state.geminiModel,
2406
+ grokModel: state.grokModel,
2083
2407
  },
2084
2408
  onProgress: (phase, message) => {
2085
2409
  console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
@@ -2139,14 +2463,14 @@ async function handleNewProject(idea: string, state: SessionState): Promise<void
2139
2463
  console.log(` ${theme.dim('Idea:')} ${idea}`);
2140
2464
  console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
2141
2465
  console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
2142
- console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
2466
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.openaiModel)}`);
2143
2467
  console.log();
2144
2468
 
2145
2469
  const spec: ProjectSpec = {
2146
2470
  idea,
2147
2471
  name: projectName,
2148
2472
  language: state.language,
2149
- openaiModel: state.model,
2473
+ openaiModel: state.openaiModel,
2150
2474
  outputDir: cwd,
2151
2475
  };
2152
2476
 
@@ -2172,6 +2496,9 @@ async function handleNewProject(idea: string, state: SessionState): Promise<void
2172
2496
  lastRun: new Date().toISOString(),
2173
2497
  projectName,
2174
2498
  description: idea,
2499
+ openaiModel: state.openaiModel,
2500
+ geminiModel: state.geminiModel,
2501
+ grokModel: state.grokModel,
2175
2502
  });
2176
2503
  printInfo('Created popeye.md with project configuration');
2177
2504
 
@@ -2190,7 +2517,9 @@ async function handleNewProject(idea: string, state: SessionState): Promise<void
2190
2517
  reviewer: state.reviewer,
2191
2518
  arbitrator: state.arbitrator,
2192
2519
  enableArbitration: state.enableArbitration,
2520
+ openaiModel: state.openaiModel,
2193
2521
  geminiModel: state.geminiModel,
2522
+ grokModel: state.grokModel,
2194
2523
  },
2195
2524
  onProgress: (phase, message) => {
2196
2525
  console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
@@ -2233,8 +2562,9 @@ export async function startInteractiveMode(): Promise<void> {
2233
2562
  const state: SessionState = {
2234
2563
  projectDir: process.cwd(),
2235
2564
  language: config.project.default_language,
2236
- model: config.apis.openai.model,
2565
+ openaiModel: config.apis.openai.model,
2237
2566
  geminiModel: 'gemini-2.0-flash',
2567
+ grokModel: config.apis.grok.model,
2238
2568
  claudeAuth: false,
2239
2569
  openaiAuth: false,
2240
2570
  geminiAuth: false,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { z } from 'zod';
7
+ import { OutputLanguageSchema } from '../types/project.js';
7
8
 
8
9
  /**
9
10
  * Consensus settings schema
@@ -92,7 +93,7 @@ export const TypeScriptSettingsSchema = z.object({
92
93
  * Project defaults schema
93
94
  */
94
95
  export const ProjectSettingsSchema = z.object({
95
- default_language: z.enum(['python', 'typescript', 'fullstack']).default('python'),
96
+ default_language: OutputLanguageSchema.default('python'),
96
97
  python: PythonSettingsSchema.default({
97
98
  package_manager: 'pip',
98
99
  test_framework: 'pytest',
@@ -43,7 +43,7 @@ function toPythonPackageName(name: string): string {
43
43
  /**
44
44
  * Generate workspace.json for "all" projects
45
45
  */
46
- function generateAllWorkspaceJson(projectName: string): string {
46
+ export function generateAllWorkspaceJson(projectName: string): string {
47
47
  const packageName = toPythonPackageName(projectName);
48
48
 
49
49
  const config: WorkspaceConfig = {
@@ -131,7 +131,7 @@ function generateAllWorkspaceJson(projectName: string): string {
131
131
  /**
132
132
  * Generate root package.json for npm workspaces
133
133
  */
134
- function generateRootPackageJson(projectName: string): string {
134
+ export function generateRootPackageJson(projectName: string): string {
135
135
  return JSON.stringify(
136
136
  {
137
137
  name: `@${projectName}/root`,
@@ -164,7 +164,7 @@ function generateRootPackageJson(projectName: string): string {
164
164
  /**
165
165
  * Generate docker-compose.yml for "all" projects
166
166
  */
167
- function generateAllDockerCompose(projectName: string): string {
167
+ export function generateAllDockerCompose(projectName: string): string {
168
168
  return `version: '3.8'
169
169
 
170
170
  services:
@@ -342,7 +342,7 @@ Generated by [Popeye CLI](https://github.com/popeye-cli/popeye)
342
342
  /**
343
343
  * Generate design tokens package
344
344
  */
345
- function generateDesignTokensPackage(projectName: string): {
345
+ export function generateDesignTokensPackage(projectName: string): {
346
346
  files: Array<{ path: string; content: string }>;
347
347
  } {
348
348
  return {
@@ -496,7 +496,7 @@ export default preset;
496
496
  /**
497
497
  * Generate UI components package
498
498
  */
499
- function generateUiPackage(projectName: string): {
499
+ export function generateUiPackage(projectName: string): {
500
500
  files: Array<{ path: string; content: string }>;
501
501
  } {
502
502
  return {