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.
Files changed (216) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +832 -123
  5. package/dist/adapters/claude.d.ts +19 -4
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +908 -42
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/gemini.d.ts +55 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +318 -0
  12. package/dist/adapters/gemini.js.map +1 -0
  13. package/dist/adapters/grok.d.ts +73 -0
  14. package/dist/adapters/grok.d.ts.map +1 -0
  15. package/dist/adapters/grok.js +430 -0
  16. package/dist/adapters/grok.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +1 -1
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js +47 -8
  20. package/dist/adapters/openai.js.map +1 -1
  21. package/dist/auth/claude.d.ts +11 -9
  22. package/dist/auth/claude.d.ts.map +1 -1
  23. package/dist/auth/claude.js +107 -71
  24. package/dist/auth/claude.js.map +1 -1
  25. package/dist/auth/gemini.d.ts +58 -0
  26. package/dist/auth/gemini.d.ts.map +1 -0
  27. package/dist/auth/gemini.js +172 -0
  28. package/dist/auth/gemini.js.map +1 -0
  29. package/dist/auth/grok.d.ts +73 -0
  30. package/dist/auth/grok.d.ts.map +1 -0
  31. package/dist/auth/grok.js +211 -0
  32. package/dist/auth/grok.js.map +1 -0
  33. package/dist/auth/index.d.ts +14 -7
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +41 -6
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/keychain.d.ts +20 -7
  38. package/dist/auth/keychain.d.ts.map +1 -1
  39. package/dist/auth/keychain.js +85 -29
  40. package/dist/auth/keychain.js.map +1 -1
  41. package/dist/auth/openai.d.ts +2 -2
  42. package/dist/auth/openai.d.ts.map +1 -1
  43. package/dist/auth/openai.js +30 -32
  44. package/dist/auth/openai.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +79 -8
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/create.d.ts.map +1 -1
  50. package/dist/cli/commands/create.js +15 -4
  51. package/dist/cli/commands/create.js.map +1 -1
  52. package/dist/cli/interactive.d.ts.map +1 -1
  53. package/dist/cli/interactive.js +1494 -114
  54. package/dist/cli/interactive.js.map +1 -1
  55. package/dist/config/defaults.d.ts +9 -1
  56. package/dist/config/defaults.d.ts.map +1 -1
  57. package/dist/config/defaults.js +19 -2
  58. package/dist/config/defaults.js.map +1 -1
  59. package/dist/config/index.d.ts +19 -0
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +33 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/schema.d.ts +47 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +29 -1
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/generators/fullstack.d.ts +32 -0
  68. package/dist/generators/fullstack.d.ts.map +1 -0
  69. package/dist/generators/fullstack.js +497 -0
  70. package/dist/generators/fullstack.js.map +1 -0
  71. package/dist/generators/index.d.ts +4 -3
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +15 -1
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/python.d.ts +17 -1
  76. package/dist/generators/python.d.ts.map +1 -1
  77. package/dist/generators/python.js +34 -20
  78. package/dist/generators/python.js.map +1 -1
  79. package/dist/generators/templates/fullstack.d.ts +113 -0
  80. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  81. package/dist/generators/templates/fullstack.js +1004 -0
  82. package/dist/generators/templates/fullstack.js.map +1 -0
  83. package/dist/generators/typescript.d.ts +19 -1
  84. package/dist/generators/typescript.d.ts.map +1 -1
  85. package/dist/generators/typescript.js +37 -20
  86. package/dist/generators/typescript.js.map +1 -1
  87. package/dist/state/index.d.ts +108 -0
  88. package/dist/state/index.d.ts.map +1 -1
  89. package/dist/state/index.js +551 -4
  90. package/dist/state/index.js.map +1 -1
  91. package/dist/state/registry.d.ts +52 -0
  92. package/dist/state/registry.d.ts.map +1 -0
  93. package/dist/state/registry.js +215 -0
  94. package/dist/state/registry.js.map +1 -0
  95. package/dist/types/cli.d.ts +8 -0
  96. package/dist/types/cli.d.ts.map +1 -1
  97. package/dist/types/cli.js.map +1 -1
  98. package/dist/types/consensus.d.ts +186 -4
  99. package/dist/types/consensus.d.ts.map +1 -1
  100. package/dist/types/consensus.js +35 -3
  101. package/dist/types/consensus.js.map +1 -1
  102. package/dist/types/project.d.ts +76 -0
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +1 -1
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +217 -16
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +40 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/workflow/auto-fix.d.ts +45 -0
  111. package/dist/workflow/auto-fix.d.ts.map +1 -0
  112. package/dist/workflow/auto-fix.js +274 -0
  113. package/dist/workflow/auto-fix.js.map +1 -0
  114. package/dist/workflow/consensus.d.ts +70 -2
  115. package/dist/workflow/consensus.d.ts.map +1 -1
  116. package/dist/workflow/consensus.js +872 -17
  117. package/dist/workflow/consensus.js.map +1 -1
  118. package/dist/workflow/execution-mode.d.ts +10 -4
  119. package/dist/workflow/execution-mode.d.ts.map +1 -1
  120. package/dist/workflow/execution-mode.js +547 -58
  121. package/dist/workflow/execution-mode.js.map +1 -1
  122. package/dist/workflow/index.d.ts +14 -2
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +69 -6
  125. package/dist/workflow/index.js.map +1 -1
  126. package/dist/workflow/milestone-workflow.d.ts +34 -0
  127. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  128. package/dist/workflow/milestone-workflow.js +414 -0
  129. package/dist/workflow/milestone-workflow.js.map +1 -0
  130. package/dist/workflow/plan-mode.d.ts +80 -3
  131. package/dist/workflow/plan-mode.d.ts.map +1 -1
  132. package/dist/workflow/plan-mode.js +767 -49
  133. package/dist/workflow/plan-mode.js.map +1 -1
  134. package/dist/workflow/plan-storage.d.ts +386 -0
  135. package/dist/workflow/plan-storage.d.ts.map +1 -0
  136. package/dist/workflow/plan-storage.js +878 -0
  137. package/dist/workflow/plan-storage.js.map +1 -0
  138. package/dist/workflow/project-verification.d.ts +37 -0
  139. package/dist/workflow/project-verification.d.ts.map +1 -0
  140. package/dist/workflow/project-verification.js +381 -0
  141. package/dist/workflow/project-verification.js.map +1 -0
  142. package/dist/workflow/task-workflow.d.ts +37 -0
  143. package/dist/workflow/task-workflow.d.ts.map +1 -0
  144. package/dist/workflow/task-workflow.js +386 -0
  145. package/dist/workflow/task-workflow.js.map +1 -0
  146. package/dist/workflow/test-runner.d.ts +9 -0
  147. package/dist/workflow/test-runner.d.ts.map +1 -1
  148. package/dist/workflow/test-runner.js +101 -5
  149. package/dist/workflow/test-runner.js.map +1 -1
  150. package/dist/workflow/ui-designer.d.ts +82 -0
  151. package/dist/workflow/ui-designer.d.ts.map +1 -0
  152. package/dist/workflow/ui-designer.js +234 -0
  153. package/dist/workflow/ui-designer.js.map +1 -0
  154. package/dist/workflow/ui-setup.d.ts +58 -0
  155. package/dist/workflow/ui-setup.d.ts.map +1 -0
  156. package/dist/workflow/ui-setup.js +685 -0
  157. package/dist/workflow/ui-setup.js.map +1 -0
  158. package/dist/workflow/ui-verification.d.ts +114 -0
  159. package/dist/workflow/ui-verification.d.ts.map +1 -0
  160. package/dist/workflow/ui-verification.js +258 -0
  161. package/dist/workflow/ui-verification.js.map +1 -0
  162. package/dist/workflow/workflow-logger.d.ts +110 -0
  163. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  164. package/dist/workflow/workflow-logger.js +267 -0
  165. package/dist/workflow/workflow-logger.js.map +1 -0
  166. package/dist/workflow/workspace-manager.d.ts +342 -0
  167. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  168. package/dist/workflow/workspace-manager.js +733 -0
  169. package/dist/workflow/workspace-manager.js.map +1 -0
  170. package/package.json +2 -2
  171. package/src/adapters/claude.ts +1067 -47
  172. package/src/adapters/gemini.ts +373 -0
  173. package/src/adapters/grok.ts +492 -0
  174. package/src/adapters/openai.ts +48 -9
  175. package/src/auth/claude.ts +120 -78
  176. package/src/auth/gemini.ts +207 -0
  177. package/src/auth/grok.ts +255 -0
  178. package/src/auth/index.ts +47 -9
  179. package/src/auth/keychain.ts +95 -28
  180. package/src/auth/openai.ts +29 -36
  181. package/src/cli/commands/auth.ts +89 -10
  182. package/src/cli/commands/create.ts +13 -4
  183. package/src/cli/interactive.ts +1774 -142
  184. package/src/config/defaults.ts +19 -2
  185. package/src/config/index.ts +36 -1
  186. package/src/config/schema.ts +30 -1
  187. package/src/generators/fullstack.ts +551 -0
  188. package/src/generators/index.ts +25 -1
  189. package/src/generators/python.ts +65 -20
  190. package/src/generators/templates/fullstack.ts +1047 -0
  191. package/src/generators/typescript.ts +69 -20
  192. package/src/state/index.ts +713 -4
  193. package/src/state/registry.ts +278 -0
  194. package/src/types/cli.ts +8 -0
  195. package/src/types/consensus.ts +197 -6
  196. package/src/types/project.ts +82 -1
  197. package/src/types/workflow.ts +90 -1
  198. package/src/workflow/auto-fix.ts +340 -0
  199. package/src/workflow/consensus.ts +1180 -16
  200. package/src/workflow/execution-mode.ts +673 -74
  201. package/src/workflow/index.ts +95 -6
  202. package/src/workflow/milestone-workflow.ts +576 -0
  203. package/src/workflow/plan-mode.ts +924 -50
  204. package/src/workflow/plan-storage.ts +1282 -0
  205. package/src/workflow/project-verification.ts +471 -0
  206. package/src/workflow/task-workflow.ts +528 -0
  207. package/src/workflow/test-runner.ts +120 -5
  208. package/src/workflow/ui-designer.ts +337 -0
  209. package/src/workflow/ui-setup.ts +797 -0
  210. package/src/workflow/ui-verification.ts +357 -0
  211. package/src/workflow/workflow-logger.ts +353 -0
  212. package/src/workflow/workspace-manager.ts +912 -0
  213. package/tests/config/config.test.ts +1 -1
  214. package/tests/types/consensus.test.ts +3 -3
  215. package/tests/workflow/plan-mode.test.ts +213 -0
  216. package/tests/workflow/test-runner.test.ts +5 -3
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Fullstack project generator
3
+ * Orchestrates Python and TypeScript generators for monorepo structure
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import type { ProjectSpec } from '../types/project.js';
9
+ import type { GenerationResult } from './python.js';
10
+ import {
11
+ generateWorkspaceJson,
12
+ generateRootDockerCompose,
13
+ generateRootReadme,
14
+ generateRootGitignore,
15
+ generateFrontendReadme,
16
+ generateBackendReadme,
17
+ generateUiSpec,
18
+ generateViteConfigReact,
19
+ generateTailwindConfig,
20
+ generatePostcssConfig,
21
+ generateMainCss,
22
+ generateAppTsx,
23
+ generateMainTsx,
24
+ generateIndexHtml,
25
+ generateFrontendPackageJson,
26
+ generateFrontendTsconfig,
27
+ generateFrontendTsconfigNode,
28
+ generateFrontendDockerfile,
29
+ generateNginxConfig,
30
+ generateFrontendTest,
31
+ generateVitestSetup,
32
+ generateFrontendVitestConfig,
33
+ generateFastAPIMain,
34
+ generateBackendDockerfile,
35
+ generateFastAPIRequirements,
36
+ } from './templates/fullstack.js';
37
+
38
+ /**
39
+ * Create a directory if it doesn't exist
40
+ */
41
+ async function ensureDir(dirPath: string): Promise<void> {
42
+ await fs.mkdir(dirPath, { recursive: true });
43
+ }
44
+
45
+ /**
46
+ * Write a file with content
47
+ */
48
+ async function writeFile(filePath: string, content: string): Promise<void> {
49
+ await fs.writeFile(filePath, content, 'utf-8');
50
+ }
51
+
52
+ /**
53
+ * Convert project name to Python package name
54
+ */
55
+ function toPythonPackageName(name: string): string {
56
+ return name.toLowerCase().replace(/-/g, '_').replace(/[^a-z0-9_]/g, '');
57
+ }
58
+
59
+ /**
60
+ * Generate a complete fullstack project (React frontend + FastAPI backend)
61
+ *
62
+ * @param spec - Project specification
63
+ * @param outputDir - Output directory
64
+ * @returns Generation result
65
+ */
66
+ export async function generateFullstackProject(
67
+ spec: ProjectSpec,
68
+ outputDir: string
69
+ ): Promise<GenerationResult> {
70
+ const projectName = spec.name || 'my-project';
71
+ const projectDir = path.join(outputDir, projectName);
72
+ const packageName = toPythonPackageName(projectName);
73
+ const filesCreated: string[] = [];
74
+
75
+ try {
76
+ // Create root directory structure
77
+ await ensureDir(projectDir);
78
+ await ensureDir(path.join(projectDir, 'apps'));
79
+ await ensureDir(path.join(projectDir, 'apps', 'frontend'));
80
+ await ensureDir(path.join(projectDir, 'apps', 'frontend', 'src'));
81
+ await ensureDir(path.join(projectDir, 'apps', 'frontend', 'tests'));
82
+ await ensureDir(path.join(projectDir, 'apps', 'frontend', 'public'));
83
+ await ensureDir(path.join(projectDir, 'apps', 'backend'));
84
+ await ensureDir(path.join(projectDir, 'apps', 'backend', 'src', packageName));
85
+ await ensureDir(path.join(projectDir, 'apps', 'backend', 'tests'));
86
+ await ensureDir(path.join(projectDir, 'packages', 'contracts'));
87
+ await ensureDir(path.join(projectDir, 'infra', 'docker'));
88
+ await ensureDir(path.join(projectDir, 'docs'));
89
+ await ensureDir(path.join(projectDir, '.popeye'));
90
+
91
+ // Generate root-level files
92
+ const rootFiles: Array<{ path: string; content: string }> = [
93
+ // Root config
94
+ {
95
+ path: path.join(projectDir, '.popeye', 'workspace.json'),
96
+ content: generateWorkspaceJson(projectName),
97
+ },
98
+ {
99
+ path: path.join(projectDir, '.popeye', 'ui-spec.json'),
100
+ content: generateUiSpec(projectName),
101
+ },
102
+ // Docker
103
+ {
104
+ path: path.join(projectDir, 'infra', 'docker', 'docker-compose.yml'),
105
+ content: generateRootDockerCompose(projectName),
106
+ },
107
+ {
108
+ path: path.join(projectDir, 'docker-compose.yml'),
109
+ content: generateRootDockerCompose(projectName),
110
+ },
111
+ // Documentation
112
+ {
113
+ path: path.join(projectDir, 'README.md'),
114
+ content: generateRootReadme(projectName, spec.idea),
115
+ },
116
+ {
117
+ path: path.join(projectDir, '.gitignore'),
118
+ content: generateRootGitignore(),
119
+ },
120
+ // Docs placeholders
121
+ {
122
+ path: path.join(projectDir, 'docs', 'PLAN.md'),
123
+ content: `# ${projectName} - Development Plan\n\nGenerated by Popeye CLI.\n`,
124
+ },
125
+ {
126
+ path: path.join(projectDir, 'docs', 'WORKFLOW_LOG.md'),
127
+ content: `# ${projectName} - Workflow Log\n\nGenerated by Popeye CLI.\n`,
128
+ },
129
+ // Packages placeholder
130
+ {
131
+ path: path.join(projectDir, 'packages', 'contracts', '.gitkeep'),
132
+ content: '',
133
+ },
134
+ ];
135
+
136
+ // Write root files
137
+ for (const file of rootFiles) {
138
+ await writeFile(file.path, file.content);
139
+ filesCreated.push(file.path);
140
+ }
141
+
142
+ // Generate frontend (React + Vite + Tailwind)
143
+ const frontendDir = path.join(projectDir, 'apps', 'frontend');
144
+ const frontendFiles: Array<{ path: string; content: string }> = [
145
+ // Config files
146
+ {
147
+ path: path.join(frontendDir, 'package.json'),
148
+ content: generateFrontendPackageJson(projectName),
149
+ },
150
+ {
151
+ path: path.join(frontendDir, 'tsconfig.json'),
152
+ content: generateFrontendTsconfig(),
153
+ },
154
+ {
155
+ path: path.join(frontendDir, 'tsconfig.node.json'),
156
+ content: generateFrontendTsconfigNode(),
157
+ },
158
+ {
159
+ path: path.join(frontendDir, 'vite.config.ts'),
160
+ content: generateViteConfigReact(),
161
+ },
162
+ {
163
+ path: path.join(frontendDir, 'vitest.config.ts'),
164
+ content: generateFrontendVitestConfig(),
165
+ },
166
+ {
167
+ path: path.join(frontendDir, 'tailwind.config.ts'),
168
+ content: generateTailwindConfig(),
169
+ },
170
+ {
171
+ path: path.join(frontendDir, 'postcss.config.js'),
172
+ content: generatePostcssConfig(),
173
+ },
174
+ // Entry files
175
+ {
176
+ path: path.join(frontendDir, 'index.html'),
177
+ content: generateIndexHtml(projectName),
178
+ },
179
+ {
180
+ path: path.join(frontendDir, 'src', 'main.tsx'),
181
+ content: generateMainTsx(),
182
+ },
183
+ {
184
+ path: path.join(frontendDir, 'src', 'App.tsx'),
185
+ content: generateAppTsx(projectName),
186
+ },
187
+ {
188
+ path: path.join(frontendDir, 'src', 'index.css'),
189
+ content: generateMainCss(),
190
+ },
191
+ {
192
+ path: path.join(frontendDir, 'src', 'vite-env.d.ts'),
193
+ content: '/// <reference types="vite/client" />\n',
194
+ },
195
+ // Test files
196
+ {
197
+ path: path.join(frontendDir, 'tests', 'setup.ts'),
198
+ content: generateVitestSetup(),
199
+ },
200
+ {
201
+ path: path.join(frontendDir, 'tests', 'App.test.tsx'),
202
+ content: generateFrontendTest(projectName),
203
+ },
204
+ // Docker
205
+ {
206
+ path: path.join(frontendDir, 'Dockerfile'),
207
+ content: generateFrontendDockerfile(),
208
+ },
209
+ {
210
+ path: path.join(frontendDir, 'nginx.conf'),
211
+ content: generateNginxConfig(),
212
+ },
213
+ // Documentation
214
+ {
215
+ path: path.join(frontendDir, 'README.md'),
216
+ content: generateFrontendReadme(projectName),
217
+ },
218
+ // Environment
219
+ {
220
+ path: path.join(frontendDir, '.env.example'),
221
+ content: 'VITE_API_URL=http://localhost:8000\n',
222
+ },
223
+ {
224
+ path: path.join(frontendDir, '.gitignore'),
225
+ content: 'node_modules/\ndist/\n.env\n.env.local\ncoverage/\n',
226
+ },
227
+ ];
228
+
229
+ // Write frontend files
230
+ for (const file of frontendFiles) {
231
+ await writeFile(file.path, file.content);
232
+ filesCreated.push(file.path);
233
+ }
234
+
235
+ // Generate backend (FastAPI)
236
+ const backendDir = path.join(projectDir, 'apps', 'backend');
237
+ const backendFiles: Array<{ path: string; content: string }> = [
238
+ // Config files
239
+ {
240
+ path: path.join(backendDir, 'pyproject.toml'),
241
+ content: generatePyprojectToml(projectName, packageName),
242
+ },
243
+ {
244
+ path: path.join(backendDir, 'requirements.txt'),
245
+ content: generateFastAPIRequirements(),
246
+ },
247
+ // Source files
248
+ {
249
+ path: path.join(backendDir, 'src', '__init__.py'),
250
+ content: '# Source root\n',
251
+ },
252
+ {
253
+ path: path.join(backendDir, 'src', packageName, '__init__.py'),
254
+ content: `"""${projectName} backend package."""\n\n__version__ = "1.0.0"\n`,
255
+ },
256
+ {
257
+ path: path.join(backendDir, 'src', packageName, 'main.py'),
258
+ content: generateFastAPIMain(projectName),
259
+ },
260
+ // Test files
261
+ {
262
+ path: path.join(backendDir, 'tests', '__init__.py'),
263
+ content: '# Tests package\n',
264
+ },
265
+ {
266
+ path: path.join(backendDir, 'tests', 'conftest.py'),
267
+ content: generateConftest(),
268
+ },
269
+ {
270
+ path: path.join(backendDir, 'tests', 'test_main.py'),
271
+ content: generateBackendTest(projectName, packageName),
272
+ },
273
+ // Docker
274
+ {
275
+ path: path.join(backendDir, 'Dockerfile'),
276
+ content: generateBackendDockerfile(projectName),
277
+ },
278
+ // Documentation
279
+ {
280
+ path: path.join(backendDir, 'README.md'),
281
+ content: generateBackendReadme(projectName),
282
+ },
283
+ // Environment
284
+ {
285
+ path: path.join(backendDir, '.env.example'),
286
+ content: 'DEBUG=true\nDATABASE_URL=sqlite:///./data/app.db\n',
287
+ },
288
+ {
289
+ path: path.join(backendDir, '.gitignore'),
290
+ content: '__pycache__/\n*.py[cod]\nvenv/\n.venv/\n.env\n.coverage\nhtmlcov/\n.pytest_cache/\ndata/\n',
291
+ },
292
+ // Makefile
293
+ {
294
+ path: path.join(backendDir, 'Makefile'),
295
+ content: generateBackendMakefile(packageName),
296
+ },
297
+ ];
298
+
299
+ // Write backend files
300
+ for (const file of backendFiles) {
301
+ await writeFile(file.path, file.content);
302
+ filesCreated.push(file.path);
303
+ }
304
+
305
+ return {
306
+ success: true,
307
+ projectDir,
308
+ filesCreated,
309
+ };
310
+ } catch (error) {
311
+ return {
312
+ success: false,
313
+ projectDir,
314
+ filesCreated,
315
+ error: error instanceof Error ? error.message : 'Unknown error',
316
+ };
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Generate pyproject.toml for FastAPI backend
322
+ */
323
+ function generatePyprojectToml(projectName: string, _packageName: string): string {
324
+ return `[build-system]
325
+ requires = ["setuptools>=61.0", "wheel"]
326
+ build-backend = "setuptools.build_meta"
327
+
328
+ [project]
329
+ name = "${projectName}-backend"
330
+ version = "1.0.0"
331
+ description = "Backend API for ${projectName}"
332
+ readme = "README.md"
333
+ requires-python = ">=3.10"
334
+ license = {text = "MIT"}
335
+ dependencies = [
336
+ "fastapi>=0.109.0",
337
+ "uvicorn[standard]>=0.27.0",
338
+ "pydantic>=2.5.0",
339
+ "pydantic-settings>=2.1.0",
340
+ ]
341
+
342
+ [project.optional-dependencies]
343
+ dev = [
344
+ "pytest>=7.4.0",
345
+ "pytest-asyncio>=0.23.0",
346
+ "httpx>=0.26.0",
347
+ "ruff>=0.1.0",
348
+ ]
349
+
350
+ [tool.setuptools.packages.find]
351
+ where = ["src"]
352
+
353
+ [tool.pytest.ini_options]
354
+ asyncio_mode = "auto"
355
+ testpaths = ["tests"]
356
+ python_files = ["test_*.py"]
357
+
358
+ [tool.ruff]
359
+ line-length = 100
360
+ target-version = "py310"
361
+
362
+ [tool.ruff.lint]
363
+ select = ["E", "F", "I", "N", "W"]
364
+ `;
365
+ }
366
+
367
+ /**
368
+ * Generate conftest.py for backend tests
369
+ */
370
+ function generateConftest(): string {
371
+ return `"""
372
+ Test configuration and fixtures.
373
+ """
374
+
375
+ import pytest
376
+ from httpx import AsyncClient
377
+ from httpx import ASGITransport
378
+
379
+
380
+ @pytest.fixture
381
+ def anyio_backend():
382
+ """Use asyncio backend for pytest-anyio."""
383
+ return "asyncio"
384
+
385
+
386
+ @pytest.fixture
387
+ async def client():
388
+ """Create async test client."""
389
+ from src.backend.main import app
390
+
391
+ transport = ASGITransport(app=app)
392
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
393
+ yield client
394
+ `;
395
+ }
396
+
397
+ /**
398
+ * Generate test file for backend
399
+ */
400
+ function generateBackendTest(projectName: string, _packageName: string): string {
401
+ return `"""
402
+ Tests for ${projectName} backend API.
403
+ """
404
+
405
+ import pytest
406
+ from httpx import AsyncClient
407
+
408
+
409
+ @pytest.mark.anyio
410
+ async def test_health_check(client: AsyncClient):
411
+ """Test health check endpoint."""
412
+ response = await client.get("/health")
413
+ assert response.status_code == 200
414
+ data = response.json()
415
+ assert data["status"] == "healthy"
416
+
417
+
418
+ @pytest.mark.anyio
419
+ async def test_root(client: AsyncClient):
420
+ """Test root endpoint."""
421
+ response = await client.get("/")
422
+ assert response.status_code == 200
423
+ data = response.json()
424
+ assert "message" in data
425
+ assert "docs" in data
426
+ `;
427
+ }
428
+
429
+ /**
430
+ * Generate Makefile for backend
431
+ */
432
+ function generateBackendMakefile(packageName: string): string {
433
+ return `.PHONY: dev test lint format clean install
434
+
435
+ install:
436
+ \tpip install -e ".[dev]"
437
+
438
+ dev:
439
+ \tuvicorn src.${packageName}.main:app --reload --port 8000
440
+
441
+ test:
442
+ \tpytest -v
443
+
444
+ test-cov:
445
+ \tpytest --cov=src/${packageName} --cov-report=html
446
+
447
+ lint:
448
+ \truff check src/ tests/
449
+
450
+ format:
451
+ \truff format src/ tests/
452
+
453
+ clean:
454
+ \trm -rf __pycache__ .pytest_cache .coverage htmlcov .ruff_cache
455
+ \tfind . -type d -name "__pycache__" -exec rm -rf {} +
456
+ `;
457
+ }
458
+
459
+ /**
460
+ * Get the list of files that would be generated for a fullstack project
461
+ *
462
+ * @param projectName - Project name
463
+ * @returns List of relative file paths
464
+ */
465
+ export function getFullstackProjectFiles(projectName: string): string[] {
466
+ const packageName = toPythonPackageName(projectName);
467
+
468
+ return [
469
+ // Root
470
+ '.popeye/workspace.json',
471
+ '.popeye/ui-spec.json',
472
+ 'infra/docker/docker-compose.yml',
473
+ 'docker-compose.yml',
474
+ 'README.md',
475
+ '.gitignore',
476
+ 'docs/PLAN.md',
477
+ 'docs/WORKFLOW_LOG.md',
478
+ 'packages/contracts/.gitkeep',
479
+ // Frontend
480
+ 'apps/frontend/package.json',
481
+ 'apps/frontend/tsconfig.json',
482
+ 'apps/frontend/tsconfig.node.json',
483
+ 'apps/frontend/vite.config.ts',
484
+ 'apps/frontend/vitest.config.ts',
485
+ 'apps/frontend/tailwind.config.ts',
486
+ 'apps/frontend/postcss.config.js',
487
+ 'apps/frontend/index.html',
488
+ 'apps/frontend/src/main.tsx',
489
+ 'apps/frontend/src/App.tsx',
490
+ 'apps/frontend/src/index.css',
491
+ 'apps/frontend/src/vite-env.d.ts',
492
+ 'apps/frontend/tests/setup.ts',
493
+ 'apps/frontend/tests/App.test.tsx',
494
+ 'apps/frontend/Dockerfile',
495
+ 'apps/frontend/nginx.conf',
496
+ 'apps/frontend/README.md',
497
+ 'apps/frontend/.env.example',
498
+ 'apps/frontend/.gitignore',
499
+ // Backend
500
+ 'apps/backend/pyproject.toml',
501
+ 'apps/backend/requirements.txt',
502
+ 'apps/backend/src/__init__.py',
503
+ `apps/backend/src/${packageName}/__init__.py`,
504
+ `apps/backend/src/${packageName}/main.py`,
505
+ 'apps/backend/tests/__init__.py',
506
+ 'apps/backend/tests/conftest.py',
507
+ 'apps/backend/tests/test_main.py',
508
+ 'apps/backend/Dockerfile',
509
+ 'apps/backend/README.md',
510
+ 'apps/backend/.env.example',
511
+ 'apps/backend/.gitignore',
512
+ 'apps/backend/Makefile',
513
+ ];
514
+ }
515
+
516
+ /**
517
+ * Validate a fullstack project structure
518
+ *
519
+ * @param projectDir - Project directory
520
+ * @returns Validation result
521
+ */
522
+ export async function validateFullstackProject(projectDir: string): Promise<{
523
+ valid: boolean;
524
+ missingFiles: string[];
525
+ }> {
526
+ const missingFiles: string[] = [];
527
+
528
+ const requiredPaths = [
529
+ 'apps/frontend/package.json',
530
+ 'apps/frontend/src',
531
+ 'apps/backend/pyproject.toml',
532
+ 'apps/backend/src',
533
+ '.popeye/workspace.json',
534
+ 'docker-compose.yml',
535
+ 'README.md',
536
+ ];
537
+
538
+ for (const file of requiredPaths) {
539
+ const filePath = path.join(projectDir, file);
540
+ try {
541
+ await fs.access(filePath);
542
+ } catch {
543
+ missingFiles.push(file);
544
+ }
545
+ }
546
+
547
+ return {
548
+ valid: missingFiles.length === 0,
549
+ missingFiles,
550
+ };
551
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Project generators module
3
- * Provides unified API for generating Python and TypeScript projects
3
+ * Provides unified API for generating Python, TypeScript, and Fullstack projects
4
4
  */
5
5
 
6
6
  import type { ProjectSpec, OutputLanguage } from '../types/project.js';
@@ -17,6 +17,11 @@ import {
17
17
  addTypeScriptModule,
18
18
  getTypeScriptProjectFiles,
19
19
  } from './typescript.js';
20
+ import {
21
+ generateFullstackProject,
22
+ validateFullstackProject,
23
+ getFullstackProjectFiles,
24
+ } from './fullstack.js';
20
25
 
21
26
  // Re-export (explicitly to avoid name conflicts)
22
27
  export {
@@ -25,6 +30,7 @@ export {
25
30
  addPythonModule,
26
31
  getPythonProjectFiles,
27
32
  type GenerationResult,
33
+ type PythonGeneratorOptions,
28
34
  } from './python.js';
29
35
  export {
30
36
  generateTypeScriptProject,
@@ -33,7 +39,13 @@ export {
33
39
  getTypeScriptProjectFiles,
34
40
  addDependency,
35
41
  updateScripts,
42
+ type TypeScriptGeneratorOptions,
36
43
  } from './typescript.js';
44
+ export {
45
+ generateFullstackProject,
46
+ validateFullstackProject,
47
+ getFullstackProjectFiles,
48
+ } from './fullstack.js';
37
49
  export * from './templates/index.js';
38
50
 
39
51
  /**
@@ -52,6 +64,8 @@ export async function generateProject(
52
64
  return generatePythonProject(spec, outputDir);
53
65
  case 'typescript':
54
66
  return generateTypeScriptProject(spec, outputDir);
67
+ case 'fullstack':
68
+ return generateFullstackProject(spec, outputDir);
55
69
  default:
56
70
  return {
57
71
  success: false,
@@ -81,6 +95,8 @@ export async function validateProject(
81
95
  return validatePythonProject(projectDir);
82
96
  case 'typescript':
83
97
  return validateTypeScriptProject(projectDir);
98
+ case 'fullstack':
99
+ return validateFullstackProject(projectDir);
84
100
  default:
85
101
  return {
86
102
  valid: false,
@@ -125,6 +141,8 @@ export function getProjectFiles(projectName: string, language: OutputLanguage):
125
141
  return getPythonProjectFiles(projectName);
126
142
  case 'typescript':
127
143
  return getTypeScriptProjectFiles(projectName);
144
+ case 'fullstack':
145
+ return getFullstackProjectFiles(projectName);
128
146
  default:
129
147
  return [];
130
148
  }
@@ -142,6 +160,8 @@ export function getTestCommand(language: OutputLanguage): string {
142
160
  return 'python -m pytest tests/ -v';
143
161
  case 'typescript':
144
162
  return 'npm test';
163
+ case 'fullstack':
164
+ return 'cd apps/backend && pytest && cd ../frontend && npm test';
145
165
  default:
146
166
  return 'echo "No test command configured"';
147
167
  }
@@ -159,6 +179,8 @@ export function getBuildCommand(language: OutputLanguage): string {
159
179
  return 'python -m pip install -e .';
160
180
  case 'typescript':
161
181
  return 'npm run build';
182
+ case 'fullstack':
183
+ return 'cd apps/backend && pip install -e . && cd ../frontend && npm run build';
162
184
  default:
163
185
  return 'echo "No build command configured"';
164
186
  }
@@ -176,6 +198,8 @@ export function getLintCommand(language: OutputLanguage): string {
176
198
  return 'ruff check src/ tests/';
177
199
  case 'typescript':
178
200
  return 'npm run lint';
201
+ case 'fullstack':
202
+ return 'cd apps/backend && ruff check . && cd ../frontend && npm run lint';
179
203
  default:
180
204
  return 'echo "No lint command configured"';
181
205
  }