popeye-cli 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.env.example +4 -1
  2. package/CONTRIBUTING.md +10 -0
  3. package/README.md +224 -17
  4. package/dist/adapters/claude.d.ts +3 -2
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +214 -0
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/gemini.d.ts +2 -2
  9. package/dist/adapters/gemini.d.ts.map +1 -1
  10. package/dist/adapters/grok.d.ts +2 -1
  11. package/dist/adapters/grok.d.ts.map +1 -1
  12. package/dist/adapters/grok.js.map +1 -1
  13. package/dist/adapters/index.d.ts +8 -0
  14. package/dist/adapters/index.d.ts.map +1 -0
  15. package/dist/adapters/index.js +12 -0
  16. package/dist/adapters/index.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +2 -2
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js.map +1 -1
  20. package/dist/cli/commands/create.d.ts.map +1 -1
  21. package/dist/cli/commands/create.js +25 -5
  22. package/dist/cli/commands/create.js.map +1 -1
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +5 -2
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/interactive.d.ts.map +1 -1
  28. package/dist/cli/interactive.js +354 -28
  29. package/dist/cli/interactive.js.map +1 -1
  30. package/dist/config/index.d.ts +2 -0
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +4 -0
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/config/schema.js +2 -1
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/generators/all.d.ts +70 -0
  37. package/dist/generators/all.d.ts.map +1 -0
  38. package/dist/generators/all.js +826 -0
  39. package/dist/generators/all.js.map +1 -0
  40. package/dist/generators/fullstack.d.ts +9 -0
  41. package/dist/generators/fullstack.d.ts.map +1 -1
  42. package/dist/generators/fullstack.js.map +1 -1
  43. package/dist/generators/index.d.ts +3 -1
  44. package/dist/generators/index.d.ts.map +1 -1
  45. package/dist/generators/index.js +33 -0
  46. package/dist/generators/index.js.map +1 -1
  47. package/dist/generators/templates/index.d.ts +2 -0
  48. package/dist/generators/templates/index.d.ts.map +1 -1
  49. package/dist/generators/templates/index.js +2 -0
  50. package/dist/generators/templates/index.js.map +1 -1
  51. package/dist/generators/templates/website.d.ts +85 -0
  52. package/dist/generators/templates/website.d.ts.map +1 -0
  53. package/dist/generators/templates/website.js +877 -0
  54. package/dist/generators/templates/website.js.map +1 -0
  55. package/dist/generators/website.d.ts +56 -0
  56. package/dist/generators/website.d.ts.map +1 -0
  57. package/dist/generators/website.js +269 -0
  58. package/dist/generators/website.js.map +1 -0
  59. package/dist/types/consensus.d.ts +18 -23
  60. package/dist/types/consensus.d.ts.map +1 -1
  61. package/dist/types/consensus.js +8 -3
  62. package/dist/types/consensus.js.map +1 -1
  63. package/dist/types/index.d.ts +2 -2
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/index.js +2 -2
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/project.d.ts +130 -17
  68. package/dist/types/project.d.ts.map +1 -1
  69. package/dist/types/project.js +55 -8
  70. package/dist/types/project.js.map +1 -1
  71. package/dist/types/workflow.d.ts +2 -0
  72. package/dist/types/workflow.d.ts.map +1 -1
  73. package/dist/types/workflow.js +2 -1
  74. package/dist/types/workflow.js.map +1 -1
  75. package/dist/upgrade/context.d.ts +37 -0
  76. package/dist/upgrade/context.d.ts.map +1 -0
  77. package/dist/upgrade/context.js +284 -0
  78. package/dist/upgrade/context.js.map +1 -0
  79. package/dist/upgrade/handlers.d.ts +103 -0
  80. package/dist/upgrade/handlers.d.ts.map +1 -0
  81. package/dist/upgrade/handlers.js +384 -0
  82. package/dist/upgrade/handlers.js.map +1 -0
  83. package/dist/upgrade/index.d.ts +26 -0
  84. package/dist/upgrade/index.d.ts.map +1 -0
  85. package/dist/upgrade/index.js +194 -0
  86. package/dist/upgrade/index.js.map +1 -0
  87. package/dist/upgrade/transitions.d.ts +34 -0
  88. package/dist/upgrade/transitions.d.ts.map +1 -0
  89. package/dist/upgrade/transitions.js +56 -0
  90. package/dist/upgrade/transitions.js.map +1 -0
  91. package/dist/workflow/consensus.d.ts +2 -1
  92. package/dist/workflow/consensus.d.ts.map +1 -1
  93. package/dist/workflow/consensus.js.map +1 -1
  94. package/dist/workflow/index.d.ts +6 -0
  95. package/dist/workflow/index.d.ts.map +1 -1
  96. package/dist/workflow/index.js +8 -0
  97. package/dist/workflow/index.js.map +1 -1
  98. package/dist/workflow/plan-mode.d.ts +3 -3
  99. package/dist/workflow/plan-mode.d.ts.map +1 -1
  100. package/dist/workflow/plan-mode.js +41 -5
  101. package/dist/workflow/plan-mode.js.map +1 -1
  102. package/dist/workflow/plan-parser.d.ts +97 -0
  103. package/dist/workflow/plan-parser.d.ts.map +1 -0
  104. package/dist/workflow/plan-parser.js +235 -0
  105. package/dist/workflow/plan-parser.js.map +1 -0
  106. package/dist/workflow/plan-storage.d.ts +40 -12
  107. package/dist/workflow/plan-storage.d.ts.map +1 -1
  108. package/dist/workflow/plan-storage.js +47 -20
  109. package/dist/workflow/plan-storage.js.map +1 -1
  110. package/dist/workflow/seo-tests.d.ts +43 -0
  111. package/dist/workflow/seo-tests.d.ts.map +1 -0
  112. package/dist/workflow/seo-tests.js +192 -0
  113. package/dist/workflow/seo-tests.js.map +1 -0
  114. package/dist/workflow/separation-guard.d.ts +35 -0
  115. package/dist/workflow/separation-guard.d.ts.map +1 -0
  116. package/dist/workflow/separation-guard.js +154 -0
  117. package/dist/workflow/separation-guard.js.map +1 -0
  118. package/dist/workflow/task-workflow.d.ts.map +1 -1
  119. package/dist/workflow/task-workflow.js +3 -2
  120. package/dist/workflow/task-workflow.js.map +1 -1
  121. package/dist/workflow/test-runner.d.ts.map +1 -1
  122. package/dist/workflow/test-runner.js +128 -0
  123. package/dist/workflow/test-runner.js.map +1 -1
  124. package/dist/workflow/workspace-manager.d.ts +31 -20
  125. package/dist/workflow/workspace-manager.d.ts.map +1 -1
  126. package/dist/workflow/workspace-manager.js +38 -9
  127. package/dist/workflow/workspace-manager.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/adapters/claude.ts +221 -4
  130. package/src/adapters/gemini.ts +2 -2
  131. package/src/adapters/grok.ts +2 -1
  132. package/src/adapters/index.ts +15 -0
  133. package/src/adapters/openai.ts +2 -2
  134. package/src/cli/commands/create.ts +25 -5
  135. package/src/cli/index.ts +5 -2
  136. package/src/cli/interactive.ts +400 -29
  137. package/src/config/schema.ts +2 -1
  138. package/src/generators/all.ts +897 -0
  139. package/src/generators/fullstack.ts +10 -0
  140. package/src/generators/index.ts +54 -0
  141. package/src/generators/templates/index.ts +2 -0
  142. package/src/generators/templates/website.ts +906 -0
  143. package/src/generators/website.ts +350 -0
  144. package/src/types/consensus.ts +20 -8
  145. package/src/types/index.ts +35 -0
  146. package/src/types/project.ts +157 -11
  147. package/src/types/workflow.ts +2 -1
  148. package/src/upgrade/context.ts +332 -0
  149. package/src/upgrade/handlers.ts +477 -0
  150. package/src/upgrade/index.ts +244 -0
  151. package/src/upgrade/transitions.ts +80 -0
  152. package/src/workflow/consensus.ts +3 -2
  153. package/src/workflow/index.ts +8 -0
  154. package/src/workflow/plan-mode.ts +44 -10
  155. package/src/workflow/plan-parser.ts +317 -0
  156. package/src/workflow/plan-storage.ts +69 -30
  157. package/src/workflow/seo-tests.ts +246 -0
  158. package/src/workflow/separation-guard.ts +200 -0
  159. package/src/workflow/task-workflow.ts +3 -2
  160. package/src/workflow/test-runner.ts +149 -0
  161. package/src/workflow/workspace-manager.ts +68 -31
  162. package/tests/cli/model-command.test.ts +93 -0
  163. package/tests/types/project.test.ts +90 -15
  164. package/tests/types/workflow-schema.test.ts +59 -0
  165. package/tests/upgrade/context.test.ts +211 -0
  166. package/tests/upgrade/transitions.test.ts +85 -0
@@ -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: 'frontend' | 'backend';
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: 'frontend' | 'backend'): WorkspaceApp | undefined {
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: 'frontend' | 'backend'): string | null {
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(): ('frontend' | 'backend')[] {
134
+ getAppNames(): WorkspaceAppName[] {
129
135
  if (!this.config) return [];
130
- const names: ('frontend' | 'backend')[] = [];
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: 'frontend' | 'backend'): string | null {
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: 'frontend' | 'backend'): string | null {
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: 'frontend' | 'backend'): string | null {
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: 'frontend' | 'backend'): string | null {
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: 'frontend' | 'backend'): string[] {
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: 'frontend' | 'backend'): 'python' | 'typescript' | null {
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): 'frontend' | 'backend' | null {
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: 'frontend' | 'backend',
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
- const testDir = path.join(appPath, appName === 'frontend' ? 'src' : 'tests');
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', 'page', 'layout', 'style', 'vite', 'frontend',
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: 'frontend' | 'backend'
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 backend)
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: 'frontend' | 'backend'
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 reject invalid OpenAI model', () => {
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: 'invalid-model',
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 python', () => {
105
- const result = OutputLanguageSchema.safeParse('python');
106
- expect(result.success).toBe(true);
107
- });
108
-
109
- it('should accept typescript', () => {
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 all valid models', () => {
122
- const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
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 validModels) {
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 reject invalid model', () => {
131
- const result = OpenAIModelSchema.safeParse('gpt-5');
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
+ });