popeye-cli 1.2.1 → 1.4.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.
Files changed (166) hide show
  1. package/.env.example +4 -1
  2. package/CONTRIBUTING.md +10 -0
  3. package/README.md +224 -17
  4. package/dist/adapters/claude.d.ts +3 -2
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +214 -0
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/gemini.d.ts +2 -2
  9. package/dist/adapters/gemini.d.ts.map +1 -1
  10. package/dist/adapters/grok.d.ts +2 -1
  11. package/dist/adapters/grok.d.ts.map +1 -1
  12. package/dist/adapters/grok.js.map +1 -1
  13. package/dist/adapters/index.d.ts +8 -0
  14. package/dist/adapters/index.d.ts.map +1 -0
  15. package/dist/adapters/index.js +12 -0
  16. package/dist/adapters/index.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +2 -2
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js.map +1 -1
  20. package/dist/cli/commands/create.d.ts.map +1 -1
  21. package/dist/cli/commands/create.js +25 -5
  22. package/dist/cli/commands/create.js.map +1 -1
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +5 -2
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/interactive.d.ts.map +1 -1
  28. package/dist/cli/interactive.js +354 -28
  29. package/dist/cli/interactive.js.map +1 -1
  30. package/dist/config/index.d.ts +2 -0
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +4 -0
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/config/schema.js +2 -1
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/generators/all.d.ts +70 -0
  37. package/dist/generators/all.d.ts.map +1 -0
  38. package/dist/generators/all.js +826 -0
  39. package/dist/generators/all.js.map +1 -0
  40. package/dist/generators/fullstack.d.ts +9 -0
  41. package/dist/generators/fullstack.d.ts.map +1 -1
  42. package/dist/generators/fullstack.js.map +1 -1
  43. package/dist/generators/index.d.ts +3 -1
  44. package/dist/generators/index.d.ts.map +1 -1
  45. package/dist/generators/index.js +33 -0
  46. package/dist/generators/index.js.map +1 -1
  47. package/dist/generators/templates/index.d.ts +2 -0
  48. package/dist/generators/templates/index.d.ts.map +1 -1
  49. package/dist/generators/templates/index.js +2 -0
  50. package/dist/generators/templates/index.js.map +1 -1
  51. package/dist/generators/templates/website.d.ts +85 -0
  52. package/dist/generators/templates/website.d.ts.map +1 -0
  53. package/dist/generators/templates/website.js +877 -0
  54. package/dist/generators/templates/website.js.map +1 -0
  55. package/dist/generators/website.d.ts +56 -0
  56. package/dist/generators/website.d.ts.map +1 -0
  57. package/dist/generators/website.js +269 -0
  58. package/dist/generators/website.js.map +1 -0
  59. package/dist/types/consensus.d.ts +18 -23
  60. package/dist/types/consensus.d.ts.map +1 -1
  61. package/dist/types/consensus.js +8 -3
  62. package/dist/types/consensus.js.map +1 -1
  63. package/dist/types/index.d.ts +2 -2
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/index.js +2 -2
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/project.d.ts +130 -17
  68. package/dist/types/project.d.ts.map +1 -1
  69. package/dist/types/project.js +55 -8
  70. package/dist/types/project.js.map +1 -1
  71. package/dist/types/workflow.d.ts +2 -0
  72. package/dist/types/workflow.d.ts.map +1 -1
  73. package/dist/types/workflow.js +2 -1
  74. package/dist/types/workflow.js.map +1 -1
  75. package/dist/upgrade/context.d.ts +37 -0
  76. package/dist/upgrade/context.d.ts.map +1 -0
  77. package/dist/upgrade/context.js +284 -0
  78. package/dist/upgrade/context.js.map +1 -0
  79. package/dist/upgrade/handlers.d.ts +103 -0
  80. package/dist/upgrade/handlers.d.ts.map +1 -0
  81. package/dist/upgrade/handlers.js +384 -0
  82. package/dist/upgrade/handlers.js.map +1 -0
  83. package/dist/upgrade/index.d.ts +26 -0
  84. package/dist/upgrade/index.d.ts.map +1 -0
  85. package/dist/upgrade/index.js +194 -0
  86. package/dist/upgrade/index.js.map +1 -0
  87. package/dist/upgrade/transitions.d.ts +34 -0
  88. package/dist/upgrade/transitions.d.ts.map +1 -0
  89. package/dist/upgrade/transitions.js +56 -0
  90. package/dist/upgrade/transitions.js.map +1 -0
  91. package/dist/workflow/consensus.d.ts +2 -1
  92. package/dist/workflow/consensus.d.ts.map +1 -1
  93. package/dist/workflow/consensus.js.map +1 -1
  94. package/dist/workflow/index.d.ts +6 -0
  95. package/dist/workflow/index.d.ts.map +1 -1
  96. package/dist/workflow/index.js +8 -0
  97. package/dist/workflow/index.js.map +1 -1
  98. package/dist/workflow/plan-mode.d.ts +3 -3
  99. package/dist/workflow/plan-mode.d.ts.map +1 -1
  100. package/dist/workflow/plan-mode.js +41 -5
  101. package/dist/workflow/plan-mode.js.map +1 -1
  102. package/dist/workflow/plan-parser.d.ts +97 -0
  103. package/dist/workflow/plan-parser.d.ts.map +1 -0
  104. package/dist/workflow/plan-parser.js +235 -0
  105. package/dist/workflow/plan-parser.js.map +1 -0
  106. package/dist/workflow/plan-storage.d.ts +40 -12
  107. package/dist/workflow/plan-storage.d.ts.map +1 -1
  108. package/dist/workflow/plan-storage.js +47 -20
  109. package/dist/workflow/plan-storage.js.map +1 -1
  110. package/dist/workflow/seo-tests.d.ts +43 -0
  111. package/dist/workflow/seo-tests.d.ts.map +1 -0
  112. package/dist/workflow/seo-tests.js +192 -0
  113. package/dist/workflow/seo-tests.js.map +1 -0
  114. package/dist/workflow/separation-guard.d.ts +35 -0
  115. package/dist/workflow/separation-guard.d.ts.map +1 -0
  116. package/dist/workflow/separation-guard.js +154 -0
  117. package/dist/workflow/separation-guard.js.map +1 -0
  118. package/dist/workflow/task-workflow.d.ts.map +1 -1
  119. package/dist/workflow/task-workflow.js +3 -2
  120. package/dist/workflow/task-workflow.js.map +1 -1
  121. package/dist/workflow/test-runner.d.ts.map +1 -1
  122. package/dist/workflow/test-runner.js +128 -0
  123. package/dist/workflow/test-runner.js.map +1 -1
  124. package/dist/workflow/workspace-manager.d.ts +31 -20
  125. package/dist/workflow/workspace-manager.d.ts.map +1 -1
  126. package/dist/workflow/workspace-manager.js +38 -9
  127. package/dist/workflow/workspace-manager.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/adapters/claude.ts +221 -4
  130. package/src/adapters/gemini.ts +2 -2
  131. package/src/adapters/grok.ts +2 -1
  132. package/src/adapters/index.ts +15 -0
  133. package/src/adapters/openai.ts +2 -2
  134. package/src/cli/commands/create.ts +25 -5
  135. package/src/cli/index.ts +5 -2
  136. package/src/cli/interactive.ts +400 -29
  137. package/src/config/schema.ts +2 -1
  138. package/src/generators/all.ts +897 -0
  139. package/src/generators/fullstack.ts +10 -0
  140. package/src/generators/index.ts +54 -0
  141. package/src/generators/templates/index.ts +2 -0
  142. package/src/generators/templates/website.ts +906 -0
  143. package/src/generators/website.ts +350 -0
  144. package/src/types/consensus.ts +20 -8
  145. package/src/types/index.ts +35 -0
  146. package/src/types/project.ts +157 -11
  147. package/src/types/workflow.ts +2 -1
  148. package/src/upgrade/context.ts +332 -0
  149. package/src/upgrade/handlers.ts +477 -0
  150. package/src/upgrade/index.ts +244 -0
  151. package/src/upgrade/transitions.ts +80 -0
  152. package/src/workflow/consensus.ts +3 -2
  153. package/src/workflow/index.ts +8 -0
  154. package/src/workflow/plan-mode.ts +44 -10
  155. package/src/workflow/plan-parser.ts +317 -0
  156. package/src/workflow/plan-storage.ts +69 -30
  157. package/src/workflow/seo-tests.ts +246 -0
  158. package/src/workflow/separation-guard.ts +200 -0
  159. package/src/workflow/task-workflow.ts +3 -2
  160. package/src/workflow/test-runner.ts +149 -0
  161. package/src/workflow/workspace-manager.ts +68 -31
  162. package/tests/cli/model-command.test.ts +93 -0
  163. package/tests/types/project.test.ts +90 -15
  164. package/tests/types/workflow-schema.test.ts +59 -0
  165. package/tests/upgrade/context.test.ts +211 -0
  166. package/tests/upgrade/transitions.test.ts +85 -0
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { z } from 'zod';
7
+ import { OutputLanguageSchema } from './project.js';
7
8
  import type { OutputLanguage, OpenAIModel } from './project.js';
8
9
  import type { ConsensusIteration } from './consensus.js';
9
10
 
@@ -191,7 +192,7 @@ export const ProjectStateSchema = z.object({
191
192
  id: z.string(),
192
193
  name: z.string(),
193
194
  idea: z.string(),
194
- language: z.enum(['python', 'typescript', 'fullstack']),
195
+ language: OutputLanguageSchema,
195
196
  openaiModel: z.enum(['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini']),
196
197
  phase: WorkflowPhaseSchema,
197
198
  status: ProjectStatusSchema,
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Upgrade context builder
3
+ * Builds rich context about existing project structure so the planner
4
+ * knows what it's building on top of after an upgrade
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import type { OutputLanguage } from '../types/project.js';
10
+ import { languageToApps } from '../types/project.js';
11
+ import type { UpgradeTransition } from './transitions.js';
12
+
13
+ /**
14
+ * Upgrade context passed to planning mode
15
+ */
16
+ export interface UpgradeContext {
17
+ /** Summary text for the planner */
18
+ summary: string;
19
+ /** Apps that already exist (from the old project type) */
20
+ existingApps: string[];
21
+ /** Apps that were just added by the upgrade */
22
+ newApps: string[];
23
+ /** The original project idea */
24
+ originalIdea: string;
25
+ /** The original language before upgrade */
26
+ fromLanguage: OutputLanguage;
27
+ /** The new language after upgrade */
28
+ toLanguage: OutputLanguage;
29
+ }
30
+
31
+ /**
32
+ * Scan an app directory for key structural information
33
+ *
34
+ * @param appDir - Path to the app directory
35
+ * @param appName - Name of the app (frontend, backend, website)
36
+ * @returns Summary of the app structure
37
+ */
38
+ async function scanAppStructure(appDir: string, appName: string): Promise<string> {
39
+ const lines: string[] = [];
40
+
41
+ try {
42
+ await fs.access(appDir);
43
+ } catch {
44
+ return ` ${appName}: (not found)`;
45
+ }
46
+
47
+ lines.push(` ${appName}/`);
48
+
49
+ // Check for package.json to understand dependencies and scripts
50
+ const pkgPath = path.join(appDir, 'package.json');
51
+ try {
52
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8');
53
+ const pkg = JSON.parse(pkgContent);
54
+ if (pkg.dependencies) {
55
+ const deps = Object.keys(pkg.dependencies).slice(0, 10);
56
+ lines.push(` Dependencies: ${deps.join(', ')}${Object.keys(pkg.dependencies).length > 10 ? '...' : ''}`);
57
+ }
58
+ if (pkg.scripts) {
59
+ const scripts = Object.keys(pkg.scripts).slice(0, 8);
60
+ lines.push(` Scripts: ${scripts.join(', ')}`);
61
+ }
62
+ } catch {
63
+ // No package.json
64
+ }
65
+
66
+ // Check for requirements.txt (Python)
67
+ const reqPath = path.join(appDir, 'requirements.txt');
68
+ try {
69
+ const reqContent = await fs.readFile(reqPath, 'utf-8');
70
+ const deps = reqContent.split('\n').filter(l => l.trim() && !l.startsWith('#')).slice(0, 10);
71
+ lines.push(` Python dependencies: ${deps.join(', ')}${deps.length >= 10 ? '...' : ''}`);
72
+ } catch {
73
+ // No requirements.txt
74
+ }
75
+
76
+ // Check for pyproject.toml (Python)
77
+ const pyprojectPath = path.join(appDir, 'pyproject.toml');
78
+ try {
79
+ await fs.access(pyprojectPath);
80
+ lines.push(` Has pyproject.toml`);
81
+ } catch {
82
+ // No pyproject.toml
83
+ }
84
+
85
+ // Scan for key directories and files
86
+ try {
87
+ const entries = await fs.readdir(appDir, { withFileTypes: true });
88
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== '__pycache__');
89
+ const files = entries.filter(e => e.isFile() && !e.name.startsWith('.'));
90
+
91
+ if (dirs.length > 0) {
92
+ lines.push(` Directories: ${dirs.map(d => d.name).join(', ')}`);
93
+ }
94
+ if (files.length > 0) {
95
+ const keyFiles = files.map(f => f.name).slice(0, 12);
96
+ lines.push(` Key files: ${keyFiles.join(', ')}${files.length > 12 ? '...' : ''}`);
97
+ }
98
+ } catch {
99
+ // Can't read directory
100
+ }
101
+
102
+ // Check for API routes or endpoints
103
+ const apiDirs = ['src/routes', 'src/api', 'routes', 'api', 'src/app/api'];
104
+ for (const apiDir of apiDirs) {
105
+ const fullPath = path.join(appDir, apiDir);
106
+ try {
107
+ const entries = await fs.readdir(fullPath);
108
+ const routeFiles = entries.filter(f => !f.startsWith('.'));
109
+ if (routeFiles.length > 0) {
110
+ lines.push(` API routes (${apiDir}/): ${routeFiles.slice(0, 8).join(', ')}${routeFiles.length > 8 ? '...' : ''}`);
111
+ }
112
+ } catch {
113
+ // No API directory
114
+ }
115
+ }
116
+
117
+ return lines.join('\n');
118
+ }
119
+
120
+ /**
121
+ * Scan for shared packages (contracts, design-tokens, ui)
122
+ *
123
+ * @param projectDir - Project root directory
124
+ * @returns Summary of shared packages
125
+ */
126
+ async function scanSharedPackages(projectDir: string): Promise<string> {
127
+ const packagesDir = path.join(projectDir, 'packages');
128
+ const lines: string[] = [];
129
+
130
+ try {
131
+ const entries = await fs.readdir(packagesDir, { withFileTypes: true });
132
+ const packages = entries.filter(e => e.isDirectory());
133
+
134
+ if (packages.length === 0) return '';
135
+
136
+ lines.push('Shared packages:');
137
+ for (const pkg of packages) {
138
+ const pkgJsonPath = path.join(packagesDir, pkg.name, 'package.json');
139
+ try {
140
+ const content = await fs.readFile(pkgJsonPath, 'utf-8');
141
+ const pkgJson = JSON.parse(content);
142
+ lines.push(` ${pkg.name}: ${pkgJson.description || '(no description)'}`);
143
+ } catch {
144
+ lines.push(` ${pkg.name}/`);
145
+ }
146
+ }
147
+ } catch {
148
+ // No packages directory
149
+ }
150
+
151
+ return lines.join('\n');
152
+ }
153
+
154
+ /**
155
+ * Check for contracts between apps (API schemas, shared types)
156
+ *
157
+ * @param projectDir - Project root directory
158
+ * @returns Summary of contracts/shared types
159
+ */
160
+ async function scanContracts(projectDir: string): Promise<string> {
161
+ const contractPaths = [
162
+ path.join(projectDir, 'packages', 'contracts'),
163
+ path.join(projectDir, 'packages', 'shared'),
164
+ path.join(projectDir, 'packages', 'types'),
165
+ ];
166
+
167
+ for (const contractDir of contractPaths) {
168
+ try {
169
+ const entries = await fs.readdir(contractDir);
170
+ const files = entries.filter(f => !f.startsWith('.') && f !== 'node_modules');
171
+ if (files.length > 0) {
172
+ return `Shared contracts: ${files.join(', ')}`;
173
+ }
174
+ } catch {
175
+ // Directory doesn't exist
176
+ }
177
+ }
178
+
179
+ return '';
180
+ }
181
+
182
+ /**
183
+ * Read the existing plan if available
184
+ *
185
+ * @param projectDir - Project root directory
186
+ * @returns Plan summary or empty string
187
+ */
188
+ async function getExistingPlanSummary(projectDir: string): Promise<string> {
189
+ const planPaths = [
190
+ path.join(projectDir, 'docs', 'PLAN.md'),
191
+ path.join(projectDir, 'docs', 'PLAN-DRAFT.md'),
192
+ ];
193
+
194
+ for (const planPath of planPaths) {
195
+ try {
196
+ const content = await fs.readFile(planPath, 'utf-8');
197
+ // Return first 2000 chars as summary
198
+ const summary = content.slice(0, 2000);
199
+ return `Previous plan summary:\n${summary}${content.length > 2000 ? '\n... (truncated)' : ''}`;
200
+ } catch {
201
+ // File doesn't exist
202
+ }
203
+ }
204
+
205
+ return '';
206
+ }
207
+
208
+ /**
209
+ * Build rich context about the project after an upgrade
210
+ * This context is passed to the planner so it knows what already exists
211
+ * and can focus the plan on the new apps + integration
212
+ *
213
+ * @param projectDir - Project root directory
214
+ * @param transition - The upgrade transition details
215
+ * @param originalIdea - The original project idea
216
+ * @param fromLanguage - The language before upgrade
217
+ * @returns Upgrade context with summary for the planner
218
+ */
219
+ export async function buildUpgradeContext(
220
+ projectDir: string,
221
+ transition: UpgradeTransition,
222
+ originalIdea: string,
223
+ fromLanguage: OutputLanguage,
224
+ ): Promise<UpgradeContext> {
225
+ const existingApps = languageToApps(fromLanguage);
226
+ const newApps = transition.newApps;
227
+
228
+ const sections: string[] = [];
229
+
230
+ // Header
231
+ sections.push(`=== PROJECT EXPANSION: ${fromLanguage} -> ${transition.to} ===`);
232
+ sections.push('');
233
+ sections.push(`This is an EXPANSION of an existing project, NOT a new project.`);
234
+ sections.push(`Original idea: ${originalIdea}`);
235
+ sections.push('');
236
+
237
+ // What already exists
238
+ sections.push('== EXISTING APPS (already built - DO NOT rebuild) ==');
239
+ const appsDir = path.join(projectDir, 'apps');
240
+ for (const app of existingApps) {
241
+ const appDir = path.join(appsDir, app);
242
+ const structure = await scanAppStructure(appDir, app);
243
+ sections.push(structure);
244
+ }
245
+ // Also scan root if code was not yet in apps/ (single-app pre-restructure state is gone after upgrade,
246
+ // but the code is now in apps/)
247
+ sections.push('');
248
+
249
+ // What's new
250
+ sections.push('== NEW APPS TO PLAN (focus your plan here) ==');
251
+ for (const app of newApps) {
252
+ sections.push(` ${app}: Scaffolded but needs full implementation planning`);
253
+ const appDir = path.join(appsDir, app);
254
+ const structure = await scanAppStructure(appDir, app);
255
+ sections.push(structure);
256
+ }
257
+ sections.push('');
258
+
259
+ // Shared packages
260
+ const sharedPkgs = await scanSharedPackages(projectDir);
261
+ if (sharedPkgs) {
262
+ sections.push('== SHARED PACKAGES ==');
263
+ sections.push(sharedPkgs);
264
+ sections.push('');
265
+ }
266
+
267
+ // Contracts
268
+ const contracts = await scanContracts(projectDir);
269
+ if (contracts) {
270
+ sections.push('== CONTRACTS ==');
271
+ sections.push(contracts);
272
+ sections.push('');
273
+ }
274
+
275
+ // Previous plan context
276
+ const planSummary = await getExistingPlanSummary(projectDir);
277
+ if (planSummary) {
278
+ sections.push('== PREVIOUS PLAN (for reference only - existing apps are already implemented) ==');
279
+ sections.push(planSummary);
280
+ sections.push('');
281
+ }
282
+
283
+ // Integration instructions
284
+ sections.push('== PLANNING INSTRUCTIONS ==');
285
+ sections.push('');
286
+ sections.push('Your plan MUST:');
287
+ sections.push(`1. Focus ONLY on the new ${newApps.join(', ')} app(s) - do NOT replan existing ${existingApps.join(', ')} app(s)`);
288
+ sections.push('2. Include integration tasks between new and existing apps:');
289
+
290
+ // Generate specific integration guidance based on what's being added
291
+ if (newApps.includes('website') && existingApps.includes('frontend')) {
292
+ sections.push(' - Shared design tokens and UI components between frontend and website');
293
+ sections.push(' - Consistent navigation and branding across frontend app and marketing website');
294
+ }
295
+ if (newApps.includes('website') && existingApps.includes('backend')) {
296
+ sections.push(' - Website API calls to backend (e.g., contact forms, newsletter signup)');
297
+ sections.push(' - Shared authentication if needed (SSO between website and app)');
298
+ }
299
+ if (newApps.includes('frontend') && existingApps.includes('backend')) {
300
+ sections.push(' - API contracts: frontend must consume existing backend API endpoints');
301
+ sections.push(' - Shared types/interfaces between frontend and backend');
302
+ sections.push(' - Authentication flow: frontend login using backend auth endpoints');
303
+ }
304
+ if (newApps.includes('backend') && existingApps.includes('frontend')) {
305
+ sections.push(' - API endpoints that the existing frontend needs');
306
+ sections.push(' - Data models and database schema');
307
+ sections.push(' - Authentication and authorization backend');
308
+ }
309
+ if (newApps.includes('frontend') && newApps.includes('backend')) {
310
+ sections.push(' - Full API contract design between new frontend and backend');
311
+ sections.push(' - Shared types/interfaces');
312
+ sections.push(' - Authentication flow end-to-end');
313
+ }
314
+
315
+ const appTagMap: Record<string, string> = { frontend: '[FE]', backend: '[BE]', website: '[WEB]' };
316
+ const appTags = newApps.map(a => appTagMap[a] || `[${String(a).toUpperCase()}]`);
317
+ sections.push(`3. Tag tasks with app targets: ${appTags.join(', ')} and [INT] for integration tasks`);
318
+ sections.push('4. Include Docker Compose updates for new services');
319
+ sections.push('5. Include tests for new apps and integration tests');
320
+ sections.push('');
321
+
322
+ const summary = sections.join('\n');
323
+
324
+ return {
325
+ summary,
326
+ existingApps,
327
+ newApps,
328
+ originalIdea,
329
+ fromLanguage,
330
+ toLanguage: transition.to,
331
+ };
332
+ }