popeye-cli 1.1.0 → 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 (137) 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 +340 -27
  5. package/dist/adapters/claude.d.ts +5 -2
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +239 -19
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/grok.d.ts +73 -0
  10. package/dist/adapters/grok.d.ts.map +1 -0
  11. package/dist/adapters/grok.js +430 -0
  12. package/dist/adapters/grok.js.map +1 -0
  13. package/dist/adapters/openai.d.ts +1 -1
  14. package/dist/adapters/openai.d.ts.map +1 -1
  15. package/dist/adapters/openai.js +6 -1
  16. package/dist/adapters/openai.js.map +1 -1
  17. package/dist/auth/grok.d.ts +73 -0
  18. package/dist/auth/grok.d.ts.map +1 -0
  19. package/dist/auth/grok.js +211 -0
  20. package/dist/auth/grok.js.map +1 -0
  21. package/dist/auth/index.d.ts +9 -6
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -6
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/cli/commands/auth.d.ts +1 -1
  26. package/dist/cli/commands/auth.d.ts.map +1 -1
  27. package/dist/cli/commands/auth.js +79 -8
  28. package/dist/cli/commands/auth.js.map +1 -1
  29. package/dist/cli/commands/create.d.ts.map +1 -1
  30. package/dist/cli/commands/create.js +15 -4
  31. package/dist/cli/commands/create.js.map +1 -1
  32. package/dist/cli/interactive.d.ts.map +1 -1
  33. package/dist/cli/interactive.js +374 -35
  34. package/dist/cli/interactive.js.map +1 -1
  35. package/dist/config/defaults.d.ts +3 -0
  36. package/dist/config/defaults.d.ts.map +1 -1
  37. package/dist/config/defaults.js +9 -0
  38. package/dist/config/defaults.js.map +1 -1
  39. package/dist/config/index.d.ts +9 -0
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js +16 -3
  42. package/dist/config/index.js.map +1 -1
  43. package/dist/config/schema.d.ts +27 -0
  44. package/dist/config/schema.d.ts.map +1 -1
  45. package/dist/config/schema.js +24 -3
  46. package/dist/config/schema.js.map +1 -1
  47. package/dist/generators/fullstack.d.ts +32 -0
  48. package/dist/generators/fullstack.d.ts.map +1 -0
  49. package/dist/generators/fullstack.js +497 -0
  50. package/dist/generators/fullstack.js.map +1 -0
  51. package/dist/generators/index.d.ts +4 -3
  52. package/dist/generators/index.d.ts.map +1 -1
  53. package/dist/generators/index.js +15 -1
  54. package/dist/generators/index.js.map +1 -1
  55. package/dist/generators/python.d.ts +17 -1
  56. package/dist/generators/python.d.ts.map +1 -1
  57. package/dist/generators/python.js +34 -21
  58. package/dist/generators/python.js.map +1 -1
  59. package/dist/generators/templates/fullstack.d.ts +113 -0
  60. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  61. package/dist/generators/templates/fullstack.js +1004 -0
  62. package/dist/generators/templates/fullstack.js.map +1 -0
  63. package/dist/generators/typescript.d.ts +19 -1
  64. package/dist/generators/typescript.d.ts.map +1 -1
  65. package/dist/generators/typescript.js +37 -21
  66. package/dist/generators/typescript.js.map +1 -1
  67. package/dist/types/cli.d.ts +4 -0
  68. package/dist/types/cli.d.ts.map +1 -1
  69. package/dist/types/cli.js.map +1 -1
  70. package/dist/types/consensus.d.ts +119 -2
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +12 -1
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/project.d.ts +76 -0
  75. package/dist/types/project.d.ts.map +1 -1
  76. package/dist/types/project.js +1 -1
  77. package/dist/types/project.js.map +1 -1
  78. package/dist/types/workflow.d.ts +162 -16
  79. package/dist/types/workflow.d.ts.map +1 -1
  80. package/dist/types/workflow.js +24 -1
  81. package/dist/types/workflow.js.map +1 -1
  82. package/dist/workflow/consensus.d.ts +29 -3
  83. package/dist/workflow/consensus.d.ts.map +1 -1
  84. package/dist/workflow/consensus.js +334 -27
  85. package/dist/workflow/consensus.js.map +1 -1
  86. package/dist/workflow/milestone-workflow.js +2 -2
  87. package/dist/workflow/milestone-workflow.js.map +1 -1
  88. package/dist/workflow/plan-mode.d.ts +66 -2
  89. package/dist/workflow/plan-mode.d.ts.map +1 -1
  90. package/dist/workflow/plan-mode.js +187 -11
  91. package/dist/workflow/plan-mode.js.map +1 -1
  92. package/dist/workflow/plan-storage.d.ts +252 -8
  93. package/dist/workflow/plan-storage.d.ts.map +1 -1
  94. package/dist/workflow/plan-storage.js +580 -33
  95. package/dist/workflow/plan-storage.js.map +1 -1
  96. package/dist/workflow/project-verification.js +1 -1
  97. package/dist/workflow/project-verification.js.map +1 -1
  98. package/dist/workflow/task-workflow.d.ts.map +1 -1
  99. package/dist/workflow/task-workflow.js +4 -1
  100. package/dist/workflow/task-workflow.js.map +1 -1
  101. package/dist/workflow/test-runner.d.ts +8 -0
  102. package/dist/workflow/test-runner.d.ts.map +1 -1
  103. package/dist/workflow/test-runner.js +92 -0
  104. package/dist/workflow/test-runner.js.map +1 -1
  105. package/dist/workflow/workspace-manager.d.ts +342 -0
  106. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  107. package/dist/workflow/workspace-manager.js +733 -0
  108. package/dist/workflow/workspace-manager.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/adapters/claude.ts +263 -24
  111. package/src/adapters/grok.ts +492 -0
  112. package/src/adapters/openai.ts +8 -2
  113. package/src/auth/grok.ts +255 -0
  114. package/src/auth/index.ts +27 -9
  115. package/src/cli/commands/auth.ts +89 -10
  116. package/src/cli/commands/create.ts +13 -4
  117. package/src/cli/interactive.ts +424 -34
  118. package/src/config/defaults.ts +9 -0
  119. package/src/config/index.ts +17 -3
  120. package/src/config/schema.ts +25 -3
  121. package/src/generators/fullstack.ts +551 -0
  122. package/src/generators/index.ts +25 -1
  123. package/src/generators/python.ts +65 -21
  124. package/src/generators/templates/fullstack.ts +1047 -0
  125. package/src/generators/typescript.ts +69 -21
  126. package/src/types/cli.ts +4 -0
  127. package/src/types/consensus.ts +135 -3
  128. package/src/types/project.ts +82 -1
  129. package/src/types/workflow.ts +56 -2
  130. package/src/workflow/consensus.ts +461 -31
  131. package/src/workflow/milestone-workflow.ts +2 -2
  132. package/src/workflow/plan-mode.ts +238 -10
  133. package/src/workflow/plan-storage.ts +835 -35
  134. package/src/workflow/project-verification.ts +1 -1
  135. package/src/workflow/task-workflow.ts +4 -1
  136. package/src/workflow/test-runner.ts +110 -0
  137. package/src/workflow/workspace-manager.ts +912 -0
@@ -27,6 +27,12 @@ export const DEFAULT_CONFIG: Config = {
27
27
  claude: {
28
28
  model: 'claude-sonnet-4-20250514',
29
29
  },
30
+ grok: {
31
+ model: 'grok-3',
32
+ temperature: 0.3,
33
+ max_tokens: 4096,
34
+ api_url: 'https://api.x.ai/v1',
35
+ },
30
36
  },
31
37
  project: {
32
38
  default_language: 'python',
@@ -92,6 +98,7 @@ export const KEYCHAIN_ACCOUNTS = {
92
98
  CLAUDE: 'claude-cli',
93
99
  OPENAI: 'openai-api',
94
100
  GEMINI: 'gemini-api',
101
+ GROK: 'grok-api',
95
102
  } as const;
96
103
 
97
104
  /**
@@ -101,9 +108,11 @@ export const ENV_VARS = {
101
108
  OPENAI_KEY: 'POPEYE_OPENAI_KEY',
102
109
  ANTHROPIC_KEY: 'POPEYE_ANTHROPIC_KEY',
103
110
  GEMINI_KEY: 'POPEYE_GEMINI_KEY',
111
+ GROK_KEY: 'POPEYE_GROK_KEY',
104
112
  DEFAULT_LANGUAGE: 'POPEYE_DEFAULT_LANGUAGE',
105
113
  OPENAI_MODEL: 'POPEYE_OPENAI_MODEL',
106
114
  GEMINI_MODEL: 'POPEYE_GEMINI_MODEL',
115
+ GROK_MODEL: 'POPEYE_GROK_MODEL',
107
116
  CONSENSUS_REVIEWER: 'POPEYE_CONSENSUS_REVIEWER',
108
117
  CONSENSUS_ARBITRATOR: 'POPEYE_CONSENSUS_ARBITRATOR',
109
118
  CONSENSUS_THRESHOLD: 'POPEYE_CONSENSUS_THRESHOLD',
@@ -86,12 +86,26 @@ function loadEnvConfig(): Partial<Config> {
86
86
  model: openaiModel as Config['apis']['openai']['model'],
87
87
  },
88
88
  claude: DEFAULT_CONFIG.apis.claude,
89
+ grok: DEFAULT_CONFIG.apis.grok,
90
+ };
91
+ }
92
+
93
+ // Grok model
94
+ const grokModel = process.env[ENV_VARS.GROK_MODEL];
95
+ if (grokModel) {
96
+ config.apis = {
97
+ ...DEFAULT_CONFIG.apis,
98
+ ...(config.apis || {}),
99
+ grok: {
100
+ ...DEFAULT_CONFIG.apis.grok,
101
+ model: grokModel,
102
+ },
89
103
  };
90
104
  }
91
105
 
92
106
  // Default language
93
107
  const defaultLanguage = process.env[ENV_VARS.DEFAULT_LANGUAGE];
94
- if (defaultLanguage && (defaultLanguage === 'python' || defaultLanguage === 'typescript')) {
108
+ if (defaultLanguage && (defaultLanguage === 'python' || defaultLanguage === 'typescript' || defaultLanguage === 'fullstack')) {
95
109
  config.project = {
96
110
  ...DEFAULT_CONFIG.project,
97
111
  default_language: defaultLanguage,
@@ -126,7 +140,7 @@ function loadEnvConfig(): Partial<Config> {
126
140
 
127
141
  // Reviewer
128
142
  const reviewer = process.env[ENV_VARS.CONSENSUS_REVIEWER];
129
- if (reviewer && (reviewer === 'openai' || reviewer === 'gemini')) {
143
+ if (reviewer && (reviewer === 'openai' || reviewer === 'gemini' || reviewer === 'grok')) {
130
144
  config.consensus = {
131
145
  ...DEFAULT_CONFIG.consensus,
132
146
  ...(config.consensus || {}),
@@ -136,7 +150,7 @@ function loadEnvConfig(): Partial<Config> {
136
150
 
137
151
  // Arbitrator
138
152
  const arbitrator = process.env[ENV_VARS.CONSENSUS_ARBITRATOR];
139
- if (arbitrator && (arbitrator === 'openai' || arbitrator === 'gemini' || arbitrator === 'off')) {
153
+ if (arbitrator && (arbitrator === 'openai' || arbitrator === 'gemini' || arbitrator === 'grok' || arbitrator === 'off')) {
140
154
  config.consensus = {
141
155
  ...DEFAULT_CONFIG.consensus,
142
156
  ...(config.consensus || {}),
@@ -13,8 +13,8 @@ export const ConsensusSettingsSchema = z.object({
13
13
  max_disagreements: z.number().min(1).max(10).default(5),
14
14
  escalation_action: z.enum(['pause', 'continue', 'abort']).default('pause'),
15
15
  // Reviewer and arbitrator settings (persisted across sessions)
16
- reviewer: z.enum(['openai', 'gemini']).default('openai'),
17
- arbitrator: z.enum(['openai', 'gemini', 'off']).default('off'),
16
+ reviewer: z.enum(['openai', 'gemini', 'grok']).default('openai'),
17
+ arbitrator: z.enum(['openai', 'gemini', 'grok', 'off']).default('off'),
18
18
  enable_arbitration: z.boolean().default(false),
19
19
  });
20
20
 
@@ -39,6 +39,16 @@ export const ClaudeSettingsSchema = z.object({
39
39
  model: z.string().default('claude-sonnet-4-20250514'),
40
40
  });
41
41
 
42
+ /**
43
+ * Grok API settings schema
44
+ */
45
+ export const GrokSettingsSchema = z.object({
46
+ model: z.string().default('grok-3'),
47
+ temperature: z.number().min(0).max(2).default(0.3),
48
+ max_tokens: z.number().min(100).max(32000).default(4096),
49
+ api_url: z.string().default('https://api.x.ai/v1'),
50
+ });
51
+
42
52
  /**
43
53
  * API configuration schema
44
54
  */
@@ -52,6 +62,12 @@ export const APISettingsSchema = z.object({
52
62
  claude: ClaudeSettingsSchema.default({
53
63
  model: 'claude-sonnet-4-20250514',
54
64
  }),
65
+ grok: GrokSettingsSchema.default({
66
+ model: 'grok-3',
67
+ temperature: 0.3,
68
+ max_tokens: 4096,
69
+ api_url: 'https://api.x.ai/v1',
70
+ }),
55
71
  });
56
72
 
57
73
  /**
@@ -76,7 +92,7 @@ export const TypeScriptSettingsSchema = z.object({
76
92
  * Project defaults schema
77
93
  */
78
94
  export const ProjectSettingsSchema = z.object({
79
- default_language: z.enum(['python', 'typescript']).default('python'),
95
+ default_language: z.enum(['python', 'typescript', 'fullstack']).default('python'),
80
96
  python: PythonSettingsSchema.default({
81
97
  package_manager: 'pip',
82
98
  test_framework: 'pytest',
@@ -130,6 +146,12 @@ export const ConfigSchema = z.object({
130
146
  claude: {
131
147
  model: 'claude-sonnet-4-20250514',
132
148
  },
149
+ grok: {
150
+ model: 'grok-3',
151
+ temperature: 0.3,
152
+ max_tokens: 4096,
153
+ api_url: 'https://api.x.ai/v1',
154
+ },
133
155
  }),
134
156
  project: ProjectSettingsSchema.default({
135
157
  default_language: 'python',
@@ -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
+ }