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.
- package/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +340 -27
- package/dist/adapters/claude.d.ts +5 -2
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +239 -19
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +6 -1
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +9 -6
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +23 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +374 -35
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +9 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +16 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +27 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +24 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -21
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -21
- package/dist/generators/typescript.js.map +1 -1
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +119 -2
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +12 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +162 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +24 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/consensus.d.ts +29 -3
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +334 -27
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/milestone-workflow.js +2 -2
- package/dist/workflow/milestone-workflow.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +66 -2
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +187 -11
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +252 -8
- package/dist/workflow/plan-storage.d.ts.map +1 -1
- package/dist/workflow/plan-storage.js +580 -33
- package/dist/workflow/plan-storage.js.map +1 -1
- package/dist/workflow/project-verification.js +1 -1
- package/dist/workflow/project-verification.js.map +1 -1
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +4 -1
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/test-runner.d.ts +8 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +92 -0
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/claude.ts +263 -24
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +8 -2
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +27 -9
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +424 -34
- package/src/config/defaults.ts +9 -0
- package/src/config/index.ts +17 -3
- package/src/config/schema.ts +25 -3
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -21
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -21
- package/src/types/cli.ts +4 -0
- package/src/types/consensus.ts +135 -3
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +56 -2
- package/src/workflow/consensus.ts +461 -31
- package/src/workflow/milestone-workflow.ts +2 -2
- package/src/workflow/plan-mode.ts +238 -10
- package/src/workflow/plan-storage.ts +835 -35
- package/src/workflow/project-verification.ts +1 -1
- package/src/workflow/task-workflow.ts +4 -1
- package/src/workflow/test-runner.ts +110 -0
- package/src/workflow/workspace-manager.ts +912 -0
package/src/config/defaults.ts
CHANGED
|
@@ -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',
|
package/src/config/index.ts
CHANGED
|
@@ -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 || {}),
|
package/src/config/schema.ts
CHANGED
|
@@ -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
|
+
}
|