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.
- package/.env.example +4 -1
- package/CONTRIBUTING.md +10 -0
- package/README.md +224 -17
- package/dist/adapters/claude.d.ts +3 -2
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +214 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +2 -2
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/grok.d.ts +2 -1
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.d.ts +2 -2
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +25 -5
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +354 -28
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +4 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/all.d.ts +70 -0
- package/dist/generators/all.d.ts.map +1 -0
- package/dist/generators/all.js +826 -0
- package/dist/generators/all.js.map +1 -0
- package/dist/generators/fullstack.d.ts +9 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +3 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +33 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/index.d.ts +2 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +2 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website.d.ts +85 -0
- package/dist/generators/templates/website.d.ts.map +1 -0
- package/dist/generators/templates/website.js +877 -0
- package/dist/generators/templates/website.js.map +1 -0
- package/dist/generators/website.d.ts +56 -0
- package/dist/generators/website.d.ts.map +1 -0
- package/dist/generators/website.js +269 -0
- package/dist/generators/website.js.map +1 -0
- package/dist/types/consensus.d.ts +18 -23
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +8 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/project.d.ts +130 -17
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +55 -8
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +2 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/context.d.ts +37 -0
- package/dist/upgrade/context.d.ts.map +1 -0
- package/dist/upgrade/context.js +284 -0
- package/dist/upgrade/context.js.map +1 -0
- package/dist/upgrade/handlers.d.ts +103 -0
- package/dist/upgrade/handlers.d.ts.map +1 -0
- package/dist/upgrade/handlers.js +384 -0
- package/dist/upgrade/handlers.js.map +1 -0
- package/dist/upgrade/index.d.ts +26 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +194 -0
- package/dist/upgrade/index.js.map +1 -0
- package/dist/upgrade/transitions.d.ts +34 -0
- package/dist/upgrade/transitions.d.ts.map +1 -0
- package/dist/upgrade/transitions.js +56 -0
- package/dist/upgrade/transitions.js.map +1 -0
- package/dist/workflow/consensus.d.ts +2 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/index.d.ts +6 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +3 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +41 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-parser.d.ts +97 -0
- package/dist/workflow/plan-parser.d.ts.map +1 -0
- package/dist/workflow/plan-parser.js +235 -0
- package/dist/workflow/plan-parser.js.map +1 -0
- package/dist/workflow/plan-storage.d.ts +40 -12
- package/dist/workflow/plan-storage.d.ts.map +1 -1
- package/dist/workflow/plan-storage.js +47 -20
- package/dist/workflow/plan-storage.js.map +1 -1
- package/dist/workflow/seo-tests.d.ts +43 -0
- package/dist/workflow/seo-tests.d.ts.map +1 -0
- package/dist/workflow/seo-tests.js +192 -0
- package/dist/workflow/seo-tests.js.map +1 -0
- package/dist/workflow/separation-guard.d.ts +35 -0
- package/dist/workflow/separation-guard.d.ts.map +1 -0
- package/dist/workflow/separation-guard.js +154 -0
- package/dist/workflow/separation-guard.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +3 -2
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +128 -0
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/workspace-manager.d.ts +31 -20
- package/dist/workflow/workspace-manager.d.ts.map +1 -1
- package/dist/workflow/workspace-manager.js +38 -9
- package/dist/workflow/workspace-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude.ts +221 -4
- package/src/adapters/gemini.ts +2 -2
- package/src/adapters/grok.ts +2 -1
- package/src/adapters/index.ts +15 -0
- package/src/adapters/openai.ts +2 -2
- package/src/cli/commands/create.ts +25 -5
- package/src/cli/index.ts +5 -2
- package/src/cli/interactive.ts +400 -29
- package/src/config/schema.ts +2 -1
- package/src/generators/all.ts +897 -0
- package/src/generators/fullstack.ts +10 -0
- package/src/generators/index.ts +54 -0
- package/src/generators/templates/index.ts +2 -0
- package/src/generators/templates/website.ts +906 -0
- package/src/generators/website.ts +350 -0
- package/src/types/consensus.ts +20 -8
- package/src/types/index.ts +35 -0
- package/src/types/project.ts +157 -11
- package/src/types/workflow.ts +2 -1
- package/src/upgrade/context.ts +332 -0
- package/src/upgrade/handlers.ts +477 -0
- package/src/upgrade/index.ts +244 -0
- package/src/upgrade/transitions.ts +80 -0
- package/src/workflow/consensus.ts +3 -2
- package/src/workflow/index.ts +8 -0
- package/src/workflow/plan-mode.ts +44 -10
- package/src/workflow/plan-parser.ts +317 -0
- package/src/workflow/plan-storage.ts +69 -30
- package/src/workflow/seo-tests.ts +246 -0
- package/src/workflow/separation-guard.ts +200 -0
- package/src/workflow/task-workflow.ts +3 -2
- package/src/workflow/test-runner.ts +149 -0
- package/src/workflow/workspace-manager.ts +68 -31
- package/tests/cli/model-command.test.ts +93 -0
- package/tests/types/project.test.ts +90 -15
- package/tests/types/workflow-schema.test.ts +59 -0
- package/tests/upgrade/context.test.ts +211 -0
- package/tests/upgrade/transitions.test.ts +85 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace manager module
|
|
3
|
-
* Handles loading, saving, and querying workspace configuration for fullstack projects
|
|
3
|
+
* Handles loading, saving, and querying workspace configuration for fullstack/all projects
|
|
4
4
|
* Also provides app-specific context for AI reviews
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,11 +9,16 @@ import path from 'node:path';
|
|
|
9
9
|
import type { WorkspaceConfig, WorkspaceApp } from '../types/project.js';
|
|
10
10
|
import type { ReviewAppTarget } from '../types/consensus.js';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* App name type for workspace apps
|
|
14
|
+
*/
|
|
15
|
+
export type WorkspaceAppName = 'frontend' | 'backend' | 'website';
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Context for AI review of a specific app
|
|
14
19
|
*/
|
|
15
20
|
export interface AppReviewContext {
|
|
16
|
-
appName:
|
|
21
|
+
appName: WorkspaceAppName;
|
|
17
22
|
language: 'python' | 'typescript';
|
|
18
23
|
path: string;
|
|
19
24
|
/** Key source files content for review */
|
|
@@ -29,11 +34,12 @@ export interface AppReviewContext {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* Combined review context for fullstack projects
|
|
37
|
+
* Combined review context for fullstack/all projects
|
|
33
38
|
*/
|
|
34
39
|
export interface FullstackReviewContext {
|
|
35
40
|
frontend?: AppReviewContext;
|
|
36
41
|
backend?: AppReviewContext;
|
|
42
|
+
website?: AppReviewContext;
|
|
37
43
|
/** Shared contracts (OpenAPI spec) */
|
|
38
44
|
contracts?: string;
|
|
39
45
|
/** Project-level context */
|
|
@@ -109,14 +115,14 @@ export class WorkspaceManager {
|
|
|
109
115
|
/**
|
|
110
116
|
* Get a specific app configuration
|
|
111
117
|
*/
|
|
112
|
-
getApp(appName:
|
|
118
|
+
getApp(appName: WorkspaceAppName): WorkspaceApp | undefined {
|
|
113
119
|
return this.config?.apps[appName];
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/**
|
|
117
123
|
* Get the absolute path to an app
|
|
118
124
|
*/
|
|
119
|
-
getAppPath(appName:
|
|
125
|
+
getAppPath(appName: WorkspaceAppName): string | null {
|
|
120
126
|
const app = this.getApp(appName);
|
|
121
127
|
if (!app) return null;
|
|
122
128
|
return path.join(this.projectDir, app.path);
|
|
@@ -125,18 +131,19 @@ export class WorkspaceManager {
|
|
|
125
131
|
/**
|
|
126
132
|
* Get all app names
|
|
127
133
|
*/
|
|
128
|
-
getAppNames():
|
|
134
|
+
getAppNames(): WorkspaceAppName[] {
|
|
129
135
|
if (!this.config) return [];
|
|
130
|
-
const names:
|
|
136
|
+
const names: WorkspaceAppName[] = [];
|
|
131
137
|
if (this.config.apps.frontend) names.push('frontend');
|
|
132
138
|
if (this.config.apps.backend) names.push('backend');
|
|
139
|
+
if (this.config.apps.website) names.push('website');
|
|
133
140
|
return names;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|
|
137
144
|
* Get test command for a specific app
|
|
138
145
|
*/
|
|
139
|
-
getTestCommand(appName:
|
|
146
|
+
getTestCommand(appName: WorkspaceAppName): string | null {
|
|
140
147
|
const app = this.getApp(appName);
|
|
141
148
|
return app?.commands.test ?? null;
|
|
142
149
|
}
|
|
@@ -172,7 +179,7 @@ export class WorkspaceManager {
|
|
|
172
179
|
/**
|
|
173
180
|
* Get lint command for a specific app
|
|
174
181
|
*/
|
|
175
|
-
getLintCommand(appName:
|
|
182
|
+
getLintCommand(appName: WorkspaceAppName): string | null {
|
|
176
183
|
const app = this.getApp(appName);
|
|
177
184
|
return app?.commands.lint ?? null;
|
|
178
185
|
}
|
|
@@ -208,7 +215,7 @@ export class WorkspaceManager {
|
|
|
208
215
|
/**
|
|
209
216
|
* Get build command for a specific app
|
|
210
217
|
*/
|
|
211
|
-
getBuildCommand(appName:
|
|
218
|
+
getBuildCommand(appName: WorkspaceAppName): string | null {
|
|
212
219
|
const app = this.getApp(appName);
|
|
213
220
|
return app?.commands.build ?? null;
|
|
214
221
|
}
|
|
@@ -244,7 +251,7 @@ export class WorkspaceManager {
|
|
|
244
251
|
/**
|
|
245
252
|
* Get dev command for a specific app
|
|
246
253
|
*/
|
|
247
|
-
getDevCommand(appName:
|
|
254
|
+
getDevCommand(appName: WorkspaceAppName): string | null {
|
|
248
255
|
const app = this.getApp(appName);
|
|
249
256
|
return app?.commands.dev ?? null;
|
|
250
257
|
}
|
|
@@ -288,7 +295,7 @@ export class WorkspaceManager {
|
|
|
288
295
|
/**
|
|
289
296
|
* Get context roots for an app (files to include in AI context)
|
|
290
297
|
*/
|
|
291
|
-
getContextRoots(appName:
|
|
298
|
+
getContextRoots(appName: WorkspaceAppName): string[] {
|
|
292
299
|
const app = this.getApp(appName);
|
|
293
300
|
if (!app || !app.contextRoots) return [];
|
|
294
301
|
|
|
@@ -315,7 +322,7 @@ export class WorkspaceManager {
|
|
|
315
322
|
/**
|
|
316
323
|
* Get app language
|
|
317
324
|
*/
|
|
318
|
-
getAppLanguage(appName:
|
|
325
|
+
getAppLanguage(appName: WorkspaceAppName): 'python' | 'typescript' | null {
|
|
319
326
|
const app = this.getApp(appName);
|
|
320
327
|
return app?.language ?? null;
|
|
321
328
|
}
|
|
@@ -323,7 +330,7 @@ export class WorkspaceManager {
|
|
|
323
330
|
/**
|
|
324
331
|
* Determine which app should handle a file based on path
|
|
325
332
|
*/
|
|
326
|
-
getAppForFile(filePath: string):
|
|
333
|
+
getAppForFile(filePath: string): WorkspaceAppName | null {
|
|
327
334
|
const relativePath = path.relative(this.projectDir, filePath);
|
|
328
335
|
|
|
329
336
|
for (const appName of this.getAppNames()) {
|
|
@@ -341,7 +348,7 @@ export class WorkspaceManager {
|
|
|
341
348
|
* Reads key files from contextRoots to provide to AI reviewers
|
|
342
349
|
*/
|
|
343
350
|
async getAppReviewContext(
|
|
344
|
-
appName:
|
|
351
|
+
appName: WorkspaceAppName,
|
|
345
352
|
options: {
|
|
346
353
|
maxFiles?: number;
|
|
347
354
|
maxFileSize?: number;
|
|
@@ -406,7 +413,8 @@ export class WorkspaceManager {
|
|
|
406
413
|
|
|
407
414
|
// Read test files
|
|
408
415
|
if (includeTests) {
|
|
409
|
-
|
|
416
|
+
// Frontend and website use 'src' for tests, backend uses 'tests'
|
|
417
|
+
const testDir = path.join(appPath, (appName === 'frontend' || appName === 'website') ? 'src' : 'tests');
|
|
410
418
|
try {
|
|
411
419
|
const testFiles = await this.findTestFiles(testDir, app.language);
|
|
412
420
|
context.testFiles = testFiles.slice(0, 5); // Limit test files
|
|
@@ -464,6 +472,15 @@ export class WorkspaceManager {
|
|
|
464
472
|
context.backend = backend;
|
|
465
473
|
}
|
|
466
474
|
|
|
475
|
+
// Get website context (for 'all' projects)
|
|
476
|
+
const website = await this.getAppReviewContext('website', {
|
|
477
|
+
maxFiles: maxFilesPerApp,
|
|
478
|
+
includeTests,
|
|
479
|
+
});
|
|
480
|
+
if (website) {
|
|
481
|
+
context.website = website;
|
|
482
|
+
}
|
|
483
|
+
|
|
467
484
|
// Get shared contracts
|
|
468
485
|
const contractsPath = this.getContractsPath();
|
|
469
486
|
if (contractsPath) {
|
|
@@ -527,7 +544,7 @@ export class WorkspaceManager {
|
|
|
527
544
|
}
|
|
528
545
|
|
|
529
546
|
/**
|
|
530
|
-
* Format fullstack context for review prompt
|
|
547
|
+
* Format fullstack/all context for review prompt
|
|
531
548
|
*/
|
|
532
549
|
formatFullstackContextForReview(context: FullstackReviewContext): string {
|
|
533
550
|
const lines: string[] = [];
|
|
@@ -554,21 +571,26 @@ export class WorkspaceManager {
|
|
|
554
571
|
lines.push(this.formatContextForReview(context.backend));
|
|
555
572
|
}
|
|
556
573
|
|
|
574
|
+
if (context.website) {
|
|
575
|
+
lines.push(this.formatContextForReview(context.website));
|
|
576
|
+
}
|
|
577
|
+
|
|
557
578
|
return lines.join('\n');
|
|
558
579
|
}
|
|
559
580
|
|
|
560
581
|
/**
|
|
561
582
|
* Determine review app target based on plan content
|
|
562
|
-
* Analyzes plan text to determine if it's frontend, backend, or unified
|
|
583
|
+
* Analyzes plan text to determine if it's frontend, backend, website, or unified
|
|
563
584
|
*/
|
|
564
585
|
categorizeByPlanContent(planContent: string): ReviewAppTarget {
|
|
565
586
|
const lowerContent = planContent.toLowerCase();
|
|
566
587
|
|
|
567
|
-
// Frontend indicators
|
|
588
|
+
// Frontend indicators (app frontend - React/Vue components)
|
|
568
589
|
const frontendKeywords = [
|
|
569
590
|
'react', 'component', 'jsx', 'tsx', 'css', 'tailwind', 'ui',
|
|
570
|
-
'button', 'form', '
|
|
591
|
+
'button', 'form', 'layout', 'style', 'vite', 'frontend',
|
|
571
592
|
'client', 'browser', 'dom', 'render', 'hook', 'state',
|
|
593
|
+
'apps/frontend',
|
|
572
594
|
];
|
|
573
595
|
|
|
574
596
|
// Backend indicators
|
|
@@ -576,12 +598,24 @@ export class WorkspaceManager {
|
|
|
576
598
|
'api', 'endpoint', 'route', 'database', 'model', 'schema',
|
|
577
599
|
'fastapi', 'flask', 'django', 'express', 'server', 'backend',
|
|
578
600
|
'authentication', 'middleware', 'orm', 'sql', 'query', 'crud',
|
|
601
|
+
'apps/backend',
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
// Website indicators (marketing/landing pages, SEO)
|
|
605
|
+
const websiteKeywords = [
|
|
606
|
+
'website', 'landing', 'marketing', 'seo', 'meta', 'sitemap',
|
|
607
|
+
'static', 'astro', 'next', 'gatsby', 'blog', 'content',
|
|
608
|
+
'apps/website', '[web]', 'web page', 'homepage',
|
|
579
609
|
];
|
|
580
610
|
|
|
581
611
|
const frontendScore = frontendKeywords.filter(kw => lowerContent.includes(kw)).length;
|
|
582
612
|
const backendScore = backendKeywords.filter(kw => lowerContent.includes(kw)).length;
|
|
613
|
+
const websiteScore = websiteKeywords.filter(kw => lowerContent.includes(kw)).length;
|
|
583
614
|
|
|
584
|
-
// Threshold for classification
|
|
615
|
+
// Threshold for classification - check website first as it's most specific
|
|
616
|
+
if (websiteScore > Math.max(frontendScore, backendScore) && websiteScore >= 2) {
|
|
617
|
+
return 'website';
|
|
618
|
+
}
|
|
585
619
|
if (frontendScore > backendScore * 2 && frontendScore >= 3) {
|
|
586
620
|
return 'frontend';
|
|
587
621
|
}
|
|
@@ -682,9 +716,9 @@ export class WorkspaceManager {
|
|
|
682
716
|
* Get feedback document paths for workspace
|
|
683
717
|
*/
|
|
684
718
|
getFeedbackPaths(): {
|
|
685
|
-
master: { unified: string; frontend: string; backend: string };
|
|
686
|
-
getMilestonePaths: (milestoneId: string) => { unified: string; frontend: string; backend: string };
|
|
687
|
-
getTaskPaths: (milestoneId: string, taskId: string) => { unified: string; frontend: string; backend: string };
|
|
719
|
+
master: { unified: string; frontend: string; backend: string; website: string };
|
|
720
|
+
getMilestonePaths: (milestoneId: string) => { unified: string; frontend: string; backend: string; website: string };
|
|
721
|
+
getTaskPaths: (milestoneId: string, taskId: string) => { unified: string; frontend: string; backend: string; website: string };
|
|
688
722
|
} {
|
|
689
723
|
const plansDir = path.join(this.projectDir, 'docs', 'plans');
|
|
690
724
|
|
|
@@ -693,16 +727,19 @@ export class WorkspaceManager {
|
|
|
693
727
|
unified: path.join(plansDir, 'master', 'unified', 'feedback.md'),
|
|
694
728
|
frontend: path.join(plansDir, 'master', 'frontend', 'feedback.md'),
|
|
695
729
|
backend: path.join(plansDir, 'master', 'backend', 'feedback.md'),
|
|
730
|
+
website: path.join(plansDir, 'master', 'website', 'feedback.md'),
|
|
696
731
|
},
|
|
697
732
|
getMilestonePaths: (milestoneId: string) => ({
|
|
698
733
|
unified: path.join(plansDir, `milestone-${milestoneId}`, 'unified', 'feedback.md'),
|
|
699
734
|
frontend: path.join(plansDir, `milestone-${milestoneId}`, 'frontend', 'feedback.md'),
|
|
700
735
|
backend: path.join(plansDir, `milestone-${milestoneId}`, 'backend', 'feedback.md'),
|
|
736
|
+
website: path.join(plansDir, `milestone-${milestoneId}`, 'website', 'feedback.md'),
|
|
701
737
|
}),
|
|
702
738
|
getTaskPaths: (milestoneId: string, taskId: string) => ({
|
|
703
739
|
unified: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'unified', 'feedback.md'),
|
|
704
740
|
frontend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'frontend', 'feedback.md'),
|
|
705
741
|
backend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'backend', 'feedback.md'),
|
|
742
|
+
website: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'website', 'feedback.md'),
|
|
706
743
|
}),
|
|
707
744
|
};
|
|
708
745
|
}
|
|
@@ -745,12 +782,12 @@ export async function isWorkspaceProject(projectDir: string): Promise<boolean> {
|
|
|
745
782
|
* Get app context for AI code generation
|
|
746
783
|
*
|
|
747
784
|
* @param projectDir - Project directory
|
|
748
|
-
* @param appName - App name
|
|
785
|
+
* @param appName - App name (frontend, backend, or website)
|
|
749
786
|
* @returns Object with app info and context files
|
|
750
787
|
*/
|
|
751
788
|
export async function getAppContext(
|
|
752
789
|
projectDir: string,
|
|
753
|
-
appName:
|
|
790
|
+
appName: WorkspaceAppName
|
|
754
791
|
): Promise<{
|
|
755
792
|
app: WorkspaceApp | undefined;
|
|
756
793
|
language: 'python' | 'typescript' | null;
|
|
@@ -818,12 +855,12 @@ export async function getBuildCommands(projectDir: string): Promise<{
|
|
|
818
855
|
* Get app-specific review context
|
|
819
856
|
*
|
|
820
857
|
* @param projectDir - Project directory
|
|
821
|
-
* @param appName - App name (frontend or
|
|
858
|
+
* @param appName - App name (frontend, backend, or website)
|
|
822
859
|
* @returns Review context with source files and metadata
|
|
823
860
|
*/
|
|
824
861
|
export async function getAppReviewContext(
|
|
825
862
|
projectDir: string,
|
|
826
|
-
appName:
|
|
863
|
+
appName: WorkspaceAppName
|
|
827
864
|
): Promise<AppReviewContext | null> {
|
|
828
865
|
const manager = new WorkspaceManager(projectDir);
|
|
829
866
|
const config = await manager.load();
|
|
@@ -899,9 +936,9 @@ export async function categorizePlanContent(
|
|
|
899
936
|
* @returns Object with feedback path getters
|
|
900
937
|
*/
|
|
901
938
|
export async function getWorkspaceFeedbackPaths(projectDir: string): Promise<{
|
|
902
|
-
master: { unified: string; frontend: string; backend: string };
|
|
903
|
-
getMilestonePaths: (milestoneId: string) => { unified: string; frontend: string; backend: string };
|
|
904
|
-
getTaskPaths: (milestoneId: string, taskId: string) => { unified: string; frontend: string; backend: string };
|
|
939
|
+
master: { unified: string; frontend: string; backend: string; website: string };
|
|
940
|
+
getMilestonePaths: (milestoneId: string) => { unified: string; frontend: string; backend: string; website: string };
|
|
941
|
+
getTaskPaths: (milestoneId: string, taskId: string) => { unified: string; frontend: string; backend: string; website: string };
|
|
905
942
|
} | null> {
|
|
906
943
|
const manager = new WorkspaceManager(projectDir);
|
|
907
944
|
const config = await manager.load();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for model command validation logic
|
|
3
|
+
* Tests the schemas and validation used by handleModel
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { OpenAIModelSchema, KNOWN_OPENAI_MODELS } from '../../src/types/project.js';
|
|
8
|
+
import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS } from '../../src/types/consensus.js';
|
|
9
|
+
|
|
10
|
+
describe('OpenAI model validation', () => {
|
|
11
|
+
it('should accept known OpenAI models', () => {
|
|
12
|
+
for (const model of KNOWN_OPENAI_MODELS) {
|
|
13
|
+
expect(OpenAIModelSchema.safeParse(model).success).toBe(true);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept unknown/new OpenAI models (flexible)', () => {
|
|
18
|
+
expect(OpenAIModelSchema.safeParse('gpt-5').success).toBe(true);
|
|
19
|
+
expect(OpenAIModelSchema.safeParse('gpt-5.2-turbo').success).toBe(true);
|
|
20
|
+
expect(OpenAIModelSchema.safeParse('o3-mini').success).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should reject empty string', () => {
|
|
24
|
+
expect(OpenAIModelSchema.safeParse('').success).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Gemini model validation', () => {
|
|
29
|
+
it('should accept known Gemini models', () => {
|
|
30
|
+
for (const model of KNOWN_GEMINI_MODELS) {
|
|
31
|
+
expect(GeminiModelSchema.safeParse(model).success).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should accept unknown/new Gemini models (flexible)', () => {
|
|
36
|
+
expect(GeminiModelSchema.safeParse('gemini-2.5-pro').success).toBe(true);
|
|
37
|
+
expect(GeminiModelSchema.safeParse('gemini-3.0-ultra').success).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject empty string', () => {
|
|
41
|
+
expect(GeminiModelSchema.safeParse('').success).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('Grok model validation', () => {
|
|
46
|
+
it('should accept any non-empty string as Grok model', () => {
|
|
47
|
+
expect(GrokModelSchema.safeParse('grok-3').success).toBe(true);
|
|
48
|
+
expect(GrokModelSchema.safeParse('grok-3-mini').success).toBe(true);
|
|
49
|
+
expect(GrokModelSchema.safeParse('grok-2').success).toBe(true);
|
|
50
|
+
expect(GrokModelSchema.safeParse('some-future-model').success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should accept empty string with default', () => {
|
|
54
|
+
// GrokModelSchema has a default, so empty parse gets default
|
|
55
|
+
const result = GrokModelSchema.safeParse(undefined);
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
if (result.success) {
|
|
58
|
+
expect(result.data).toBe('grok-3');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('known models lists', () => {
|
|
64
|
+
it('should have known OpenAI models', () => {
|
|
65
|
+
expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o');
|
|
66
|
+
expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o-mini');
|
|
67
|
+
expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(5);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should have known Gemini models', () => {
|
|
71
|
+
expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.0-flash');
|
|
72
|
+
expect(KNOWN_GEMINI_MODELS).toContain('gemini-1.5-pro');
|
|
73
|
+
expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(3);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('backward compatibility', () => {
|
|
78
|
+
it('should auto-detect known OpenAI models from bare name', () => {
|
|
79
|
+
// Simulating the backward-compat logic in handleModel
|
|
80
|
+
for (const model of KNOWN_OPENAI_MODELS) {
|
|
81
|
+
const isKnown = (KNOWN_OPENAI_MODELS as readonly string[]).includes(model);
|
|
82
|
+
expect(isKnown).toBe(true);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not auto-detect non-OpenAI models as known OpenAI', () => {
|
|
87
|
+
const nonOpenAI = ['gemini-2.0-flash', 'grok-3'];
|
|
88
|
+
for (const model of nonOpenAI) {
|
|
89
|
+
const isKnown = (KNOWN_OPENAI_MODELS as readonly string[]).includes(model);
|
|
90
|
+
expect(isKnown).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
ProjectSpecSchema,
|
|
8
8
|
OutputLanguageSchema,
|
|
9
9
|
OpenAIModelSchema,
|
|
10
|
+
isWorkspace,
|
|
11
|
+
languageToApps,
|
|
12
|
+
hasApp,
|
|
10
13
|
} from '../../src/types/project.js';
|
|
14
|
+
import type { OutputLanguage } from '../../src/types/project.js';
|
|
11
15
|
|
|
12
16
|
describe('ProjectSpecSchema', () => {
|
|
13
17
|
describe('valid inputs', () => {
|
|
@@ -78,11 +82,22 @@ describe('ProjectSpecSchema', () => {
|
|
|
78
82
|
expect(result.success).toBe(false);
|
|
79
83
|
});
|
|
80
84
|
|
|
81
|
-
it('should
|
|
85
|
+
it('should accept new/custom OpenAI model names', () => {
|
|
82
86
|
const spec = {
|
|
83
87
|
idea: 'Build something great',
|
|
84
88
|
language: 'python',
|
|
85
|
-
openaiModel: '
|
|
89
|
+
openaiModel: 'gpt-5.2-turbo',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = ProjectSpecSchema.safeParse(spec);
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should reject empty OpenAI model', () => {
|
|
97
|
+
const spec = {
|
|
98
|
+
idea: 'Build something great',
|
|
99
|
+
language: 'python',
|
|
100
|
+
openaiModel: '',
|
|
86
101
|
};
|
|
87
102
|
|
|
88
103
|
const result = ProjectSpecSchema.safeParse(spec);
|
|
@@ -101,14 +116,12 @@ describe('ProjectSpecSchema', () => {
|
|
|
101
116
|
});
|
|
102
117
|
|
|
103
118
|
describe('OutputLanguageSchema', () => {
|
|
104
|
-
it('should accept
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const result = OutputLanguageSchema.safeParse('typescript');
|
|
111
|
-
expect(result.success).toBe(true);
|
|
119
|
+
it('should accept all 5 valid languages', () => {
|
|
120
|
+
const validLanguages = ['python', 'typescript', 'fullstack', 'website', 'all'];
|
|
121
|
+
for (const lang of validLanguages) {
|
|
122
|
+
const result = OutputLanguageSchema.safeParse(lang);
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
}
|
|
112
125
|
});
|
|
113
126
|
|
|
114
127
|
it('should reject invalid language', () => {
|
|
@@ -117,18 +130,80 @@ describe('OutputLanguageSchema', () => {
|
|
|
117
130
|
});
|
|
118
131
|
});
|
|
119
132
|
|
|
133
|
+
describe('isWorkspace', () => {
|
|
134
|
+
it('should return true for fullstack', () => {
|
|
135
|
+
expect(isWorkspace('fullstack')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return true for all', () => {
|
|
139
|
+
expect(isWorkspace('all')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return false for python', () => {
|
|
143
|
+
expect(isWorkspace('python')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return false for typescript', () => {
|
|
147
|
+
expect(isWorkspace('typescript')).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should return false for website', () => {
|
|
151
|
+
expect(isWorkspace('website')).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('languageToApps', () => {
|
|
156
|
+
it('should return backend for python', () => {
|
|
157
|
+
expect(languageToApps('python')).toEqual(['backend']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return frontend for typescript', () => {
|
|
161
|
+
expect(languageToApps('typescript')).toEqual(['frontend']);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return frontend and backend for fullstack', () => {
|
|
165
|
+
expect(languageToApps('fullstack')).toEqual(['frontend', 'backend']);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return website for website', () => {
|
|
169
|
+
expect(languageToApps('website')).toEqual(['website']);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return all three for all', () => {
|
|
173
|
+
expect(languageToApps('all')).toEqual(['frontend', 'backend', 'website']);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('hasApp', () => {
|
|
178
|
+
it('should detect backend in python', () => {
|
|
179
|
+
expect(hasApp('python', 'backend')).toBe(true);
|
|
180
|
+
expect(hasApp('python', 'frontend')).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should detect all apps in all', () => {
|
|
184
|
+
expect(hasApp('all', 'frontend')).toBe(true);
|
|
185
|
+
expect(hasApp('all', 'backend')).toBe(true);
|
|
186
|
+
expect(hasApp('all', 'website')).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
120
190
|
describe('OpenAIModelSchema', () => {
|
|
121
|
-
it('should accept
|
|
122
|
-
const
|
|
191
|
+
it('should accept known models', () => {
|
|
192
|
+
const knownModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
|
|
123
193
|
|
|
124
|
-
for (const model of
|
|
194
|
+
for (const model of knownModels) {
|
|
125
195
|
const result = OpenAIModelSchema.safeParse(model);
|
|
126
196
|
expect(result.success).toBe(true);
|
|
127
197
|
}
|
|
128
198
|
});
|
|
129
199
|
|
|
130
|
-
it('should
|
|
131
|
-
|
|
200
|
+
it('should accept new/unknown models (flexible)', () => {
|
|
201
|
+
expect(OpenAIModelSchema.safeParse('gpt-5').success).toBe(true);
|
|
202
|
+
expect(OpenAIModelSchema.safeParse('gpt-5.2-turbo').success).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should reject empty string', () => {
|
|
206
|
+
const result = OpenAIModelSchema.safeParse('');
|
|
132
207
|
expect(result.success).toBe(false);
|
|
133
208
|
});
|
|
134
209
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ProjectStateSchema accepting all 5 language types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { ProjectStateSchema } from '../../src/types/workflow.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal valid project state for testing schema validation
|
|
10
|
+
*/
|
|
11
|
+
function makeMinimalState(language: string) {
|
|
12
|
+
return {
|
|
13
|
+
id: 'test-id',
|
|
14
|
+
name: 'test-project',
|
|
15
|
+
idea: 'A test project idea that is long enough',
|
|
16
|
+
language,
|
|
17
|
+
openaiModel: 'gpt-4o',
|
|
18
|
+
phase: 'plan',
|
|
19
|
+
status: 'pending',
|
|
20
|
+
milestones: [],
|
|
21
|
+
currentMilestone: null,
|
|
22
|
+
currentTask: null,
|
|
23
|
+
consensusHistory: [],
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
updatedAt: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('ProjectStateSchema language validation', () => {
|
|
30
|
+
it('should accept python', () => {
|
|
31
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('python'));
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should accept typescript', () => {
|
|
36
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('typescript'));
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should accept fullstack', () => {
|
|
41
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('fullstack'));
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should accept website', () => {
|
|
46
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('website'));
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should accept all', () => {
|
|
51
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('all'));
|
|
52
|
+
expect(result.success).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should reject invalid language', () => {
|
|
56
|
+
const result = ProjectStateSchema.safeParse(makeMinimalState('java'));
|
|
57
|
+
expect(result.success).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|