popeye-cli 1.2.0 → 1.3.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 (134) hide show
  1. package/.env.example +4 -1
  2. package/CONTRIBUTING.md +10 -0
  3. package/README.md +111 -2
  4. package/dist/adapters/claude.d.ts +26 -2
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +257 -10
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/grok.d.ts +2 -1
  9. package/dist/adapters/grok.d.ts.map +1 -1
  10. package/dist/adapters/grok.js.map +1 -1
  11. package/dist/adapters/index.d.ts +8 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +12 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/openai.d.ts +2 -2
  16. package/dist/adapters/openai.d.ts.map +1 -1
  17. package/dist/adapters/openai.js.map +1 -1
  18. package/dist/cli/commands/create.d.ts.map +1 -1
  19. package/dist/cli/commands/create.js +25 -5
  20. package/dist/cli/commands/create.js.map +1 -1
  21. package/dist/cli/interactive.d.ts.map +1 -1
  22. package/dist/cli/interactive.js +79 -6
  23. package/dist/cli/interactive.js.map +1 -1
  24. package/dist/generators/all.d.ts +40 -0
  25. package/dist/generators/all.d.ts.map +1 -0
  26. package/dist/generators/all.js +826 -0
  27. package/dist/generators/all.js.map +1 -0
  28. package/dist/generators/fullstack.d.ts +9 -0
  29. package/dist/generators/fullstack.d.ts.map +1 -1
  30. package/dist/generators/fullstack.js.map +1 -1
  31. package/dist/generators/index.d.ts +3 -1
  32. package/dist/generators/index.d.ts.map +1 -1
  33. package/dist/generators/index.js +33 -0
  34. package/dist/generators/index.js.map +1 -1
  35. package/dist/generators/templates/index.d.ts +2 -0
  36. package/dist/generators/templates/index.d.ts.map +1 -1
  37. package/dist/generators/templates/index.js +2 -0
  38. package/dist/generators/templates/index.js.map +1 -1
  39. package/dist/generators/templates/website.d.ts +85 -0
  40. package/dist/generators/templates/website.d.ts.map +1 -0
  41. package/dist/generators/templates/website.js +877 -0
  42. package/dist/generators/templates/website.js.map +1 -0
  43. package/dist/generators/website.d.ts +56 -0
  44. package/dist/generators/website.d.ts.map +1 -0
  45. package/dist/generators/website.js +269 -0
  46. package/dist/generators/website.js.map +1 -0
  47. package/dist/types/consensus.d.ts +8 -3
  48. package/dist/types/consensus.d.ts.map +1 -1
  49. package/dist/types/index.d.ts +2 -2
  50. package/dist/types/index.d.ts.map +1 -1
  51. package/dist/types/index.js +2 -2
  52. package/dist/types/index.js.map +1 -1
  53. package/dist/types/project.d.ts +115 -1
  54. package/dist/types/project.d.ts.map +1 -1
  55. package/dist/types/project.js +41 -1
  56. package/dist/types/project.js.map +1 -1
  57. package/dist/types/workflow.d.ts +8 -0
  58. package/dist/types/workflow.d.ts.map +1 -1
  59. package/dist/types/workflow.js +2 -2
  60. package/dist/types/workflow.js.map +1 -1
  61. package/dist/workflow/consensus.d.ts +2 -1
  62. package/dist/workflow/consensus.d.ts.map +1 -1
  63. package/dist/workflow/consensus.js.map +1 -1
  64. package/dist/workflow/execution-mode.d.ts +2 -0
  65. package/dist/workflow/execution-mode.d.ts.map +1 -1
  66. package/dist/workflow/execution-mode.js +20 -0
  67. package/dist/workflow/execution-mode.js.map +1 -1
  68. package/dist/workflow/index.d.ts +8 -0
  69. package/dist/workflow/index.d.ts.map +1 -1
  70. package/dist/workflow/index.js +19 -0
  71. package/dist/workflow/index.js.map +1 -1
  72. package/dist/workflow/milestone-workflow.d.ts +2 -0
  73. package/dist/workflow/milestone-workflow.d.ts.map +1 -1
  74. package/dist/workflow/milestone-workflow.js +17 -0
  75. package/dist/workflow/milestone-workflow.js.map +1 -1
  76. package/dist/workflow/plan-mode.d.ts +3 -3
  77. package/dist/workflow/plan-mode.d.ts.map +1 -1
  78. package/dist/workflow/plan-mode.js.map +1 -1
  79. package/dist/workflow/plan-parser.d.ts +97 -0
  80. package/dist/workflow/plan-parser.d.ts.map +1 -0
  81. package/dist/workflow/plan-parser.js +235 -0
  82. package/dist/workflow/plan-parser.js.map +1 -0
  83. package/dist/workflow/plan-storage.d.ts +40 -12
  84. package/dist/workflow/plan-storage.d.ts.map +1 -1
  85. package/dist/workflow/plan-storage.js +47 -20
  86. package/dist/workflow/plan-storage.js.map +1 -1
  87. package/dist/workflow/seo-tests.d.ts +43 -0
  88. package/dist/workflow/seo-tests.d.ts.map +1 -0
  89. package/dist/workflow/seo-tests.js +192 -0
  90. package/dist/workflow/seo-tests.js.map +1 -0
  91. package/dist/workflow/separation-guard.d.ts +35 -0
  92. package/dist/workflow/separation-guard.d.ts.map +1 -0
  93. package/dist/workflow/separation-guard.js +154 -0
  94. package/dist/workflow/separation-guard.js.map +1 -0
  95. package/dist/workflow/task-workflow.d.ts +2 -0
  96. package/dist/workflow/task-workflow.d.ts.map +1 -1
  97. package/dist/workflow/task-workflow.js +19 -0
  98. package/dist/workflow/task-workflow.js.map +1 -1
  99. package/dist/workflow/test-runner.d.ts.map +1 -1
  100. package/dist/workflow/test-runner.js +128 -0
  101. package/dist/workflow/test-runner.js.map +1 -1
  102. package/dist/workflow/workspace-manager.d.ts +31 -20
  103. package/dist/workflow/workspace-manager.d.ts.map +1 -1
  104. package/dist/workflow/workspace-manager.js +38 -9
  105. package/dist/workflow/workspace-manager.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/adapters/claude.ts +289 -14
  108. package/src/adapters/grok.ts +2 -1
  109. package/src/adapters/index.ts +15 -0
  110. package/src/adapters/openai.ts +2 -2
  111. package/src/cli/commands/create.ts +25 -5
  112. package/src/cli/interactive.ts +76 -6
  113. package/src/generators/all.ts +897 -0
  114. package/src/generators/fullstack.ts +10 -0
  115. package/src/generators/index.ts +54 -0
  116. package/src/generators/templates/index.ts +2 -0
  117. package/src/generators/templates/website.ts +906 -0
  118. package/src/generators/website.ts +350 -0
  119. package/src/types/consensus.ts +9 -3
  120. package/src/types/index.ts +33 -0
  121. package/src/types/project.ts +139 -2
  122. package/src/types/workflow.ts +2 -2
  123. package/src/workflow/consensus.ts +3 -2
  124. package/src/workflow/execution-mode.ts +32 -0
  125. package/src/workflow/index.ts +20 -0
  126. package/src/workflow/milestone-workflow.ts +22 -0
  127. package/src/workflow/plan-mode.ts +3 -3
  128. package/src/workflow/plan-parser.ts +317 -0
  129. package/src/workflow/plan-storage.ts +69 -30
  130. package/src/workflow/seo-tests.ts +246 -0
  131. package/src/workflow/separation-guard.ts +200 -0
  132. package/src/workflow/task-workflow.ts +25 -0
  133. package/src/workflow/test-runner.ts +149 -0
  134. package/src/workflow/workspace-manager.ts +68 -31
@@ -2,7 +2,7 @@
2
2
  * Plan Storage System
3
3
  * Manages plans in markdown files to reduce API calls and maintain tracking
4
4
  *
5
- * Directory Structure for Fullstack Projects:
5
+ * Directory Structure for Fullstack/All Projects:
6
6
  * docs/plans/
7
7
  * ├── master/
8
8
  * │ ├── plan.md
@@ -13,7 +13,10 @@
13
13
  * │ ├── frontend/
14
14
  * │ │ ├── feedback.json
15
15
  * │ │ └── feedback.md
16
- * │ └── backend/
16
+ * │ ├── backend/
17
+ * │ │ ├── feedback.json
18
+ * │ │ └── feedback.md
19
+ * │ └── website/
17
20
  * │ ├── feedback.json
18
21
  * │ └── feedback.md
19
22
  * ├── milestone-1/
@@ -22,13 +25,15 @@
22
25
  * │ ├── unified/
23
26
  * │ ├── frontend/
24
27
  * │ ├── backend/
28
+ * │ ├── website/
25
29
  * │ └── tasks/
26
30
  * │ └── task-1/
27
31
  * │ ├── plan.md
28
32
  * │ ├── metadata.json
29
33
  * │ ├── unified/
30
34
  * │ ├── frontend/
31
- * │ └── backend/
35
+ * │ ├── backend/
36
+ * │ └── website/
32
37
  */
33
38
 
34
39
  import { promises as fs } from 'node:fs';
@@ -43,7 +48,7 @@ import type {
43
48
  /**
44
49
  * App target for feedback storage
45
50
  */
46
- export type FeedbackAppTarget = 'frontend' | 'backend' | 'unified';
51
+ export type FeedbackAppTarget = 'frontend' | 'backend' | 'website' | 'unified';
47
52
 
48
53
  /**
49
54
  * Feedback entry from a reviewer
@@ -89,13 +94,15 @@ export interface PlanMetadata {
89
94
  consensusScore?: number;
90
95
  status: 'draft' | 'reviewing' | 'approved' | 'implemented';
91
96
 
92
- /** Fullstack-specific tracking */
97
+ /** Fullstack/All project-specific tracking */
93
98
  isFullstack?: boolean;
94
99
  frontendScore?: number;
95
100
  backendScore?: number;
101
+ websiteScore?: number;
96
102
  unifiedScore?: number;
97
103
  frontendApproved?: boolean;
98
104
  backendApproved?: boolean;
105
+ websiteApproved?: boolean;
99
106
  unifiedApproved?: boolean;
100
107
 
101
108
  /** Total iterations for this plan */
@@ -121,18 +128,20 @@ export interface StoredPlan {
121
128
  }
122
129
 
123
130
  /**
124
- * Fullstack stored plan with per-app feedback
131
+ * Fullstack/All stored plan with per-app feedback
125
132
  */
126
133
  export interface FullstackStoredPlan extends StoredPlan {
127
134
  /** Per-app feedback */
128
135
  frontendFeedback: ReviewerFeedback[];
129
136
  backendFeedback: ReviewerFeedback[];
137
+ websiteFeedback: ReviewerFeedback[];
130
138
  unifiedFeedback: ReviewerFeedback[];
131
139
 
132
140
  /** Per-app revision history */
133
141
  appRevisionHistory: {
134
142
  frontend: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
135
143
  backend: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
144
+ website: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
136
145
  unified: Array<{ version: number; timestamp: string; changes: string; score?: number }>;
137
146
  };
138
147
  }
@@ -171,13 +180,14 @@ export class PlanStorage {
171
180
  }
172
181
 
173
182
  /**
174
- * Initialize app subdirectories (frontend/backend/unified)
183
+ * Initialize app subdirectories (frontend/backend/website/unified)
175
184
  */
176
185
  private async initializeAppDirectories(baseDir: string): Promise<void> {
177
186
  await fs.mkdir(baseDir, { recursive: true });
178
187
  await fs.mkdir(path.join(baseDir, 'unified'), { recursive: true });
179
188
  await fs.mkdir(path.join(baseDir, 'frontend'), { recursive: true });
180
189
  await fs.mkdir(path.join(baseDir, 'backend'), { recursive: true });
190
+ await fs.mkdir(path.join(baseDir, 'website'), { recursive: true });
181
191
  }
182
192
 
183
193
  /**
@@ -512,7 +522,7 @@ export class PlanStorage {
512
522
  /**
513
523
  * Save fullstack feedback with per-app breakdown
514
524
  *
515
- * Saves feedback to all three directories (unified, frontend, backend)
525
+ * Saves feedback to all four directories (unified, frontend, backend, website)
516
526
  */
517
527
  async saveFullstackFeedback(
518
528
  feedback: FullstackReviewerFeedback,
@@ -526,7 +536,7 @@ export class PlanStorage {
526
536
  return;
527
537
  }
528
538
 
529
- const apps: FeedbackAppTarget[] = ['unified', 'frontend', 'backend'];
539
+ const apps: FeedbackAppTarget[] = ['unified', 'frontend', 'backend', 'website'];
530
540
 
531
541
  for (const app of apps) {
532
542
  // Extract app-specific concerns and recommendations
@@ -542,6 +552,8 @@ export class PlanStorage {
542
552
  ? feedback.appScores.frontend
543
553
  : app === 'backend'
544
554
  ? feedback.appScores.backend
555
+ : app === 'website'
556
+ ? feedback.appScores.website
545
557
  : feedback.appScores.unified;
546
558
 
547
559
  const appFeedback: ReviewerFeedback = {
@@ -623,7 +635,7 @@ export class PlanStorage {
623
635
  }
624
636
 
625
637
  /**
626
- * Load all feedback for all apps (fullstack)
638
+ * Load all feedback for all apps (fullstack/all)
627
639
  */
628
640
  async loadAllAppFeedback(
629
641
  milestoneId: string,
@@ -632,19 +644,21 @@ export class PlanStorage {
632
644
  unified: ReviewerFeedback[];
633
645
  frontend: ReviewerFeedback[];
634
646
  backend: ReviewerFeedback[];
647
+ website: ReviewerFeedback[];
635
648
  }> {
636
649
  if (!this.isFullstack) {
637
650
  const unified = await this.loadFeedback(milestoneId, taskId);
638
- return { unified, frontend: [], backend: [] };
651
+ return { unified, frontend: [], backend: [], website: [] };
639
652
  }
640
653
 
641
- const [unified, frontend, backend] = await Promise.all([
654
+ const [unified, frontend, backend, website] = await Promise.all([
642
655
  this.loadFeedback(milestoneId, taskId, 'unified'),
643
656
  this.loadFeedback(milestoneId, taskId, 'frontend'),
644
657
  this.loadFeedback(milestoneId, taskId, 'backend'),
658
+ this.loadFeedback(milestoneId, taskId, 'website'),
645
659
  ]);
646
660
 
647
- return { unified, frontend, backend };
661
+ return { unified, frontend, backend, website };
648
662
  }
649
663
 
650
664
  /**
@@ -662,25 +676,27 @@ export class PlanStorage {
662
676
  }
663
677
 
664
678
  /**
665
- * Load all master plan feedback (fullstack)
679
+ * Load all master plan feedback (fullstack/all)
666
680
  */
667
681
  async loadAllMasterFeedback(): Promise<{
668
682
  unified: ReviewerFeedback[];
669
683
  frontend: ReviewerFeedback[];
670
684
  backend: ReviewerFeedback[];
685
+ website: ReviewerFeedback[];
671
686
  }> {
672
687
  if (!this.isFullstack) {
673
688
  const unified = await this.loadMasterFeedback();
674
- return { unified, frontend: [], backend: [] };
689
+ return { unified, frontend: [], backend: [], website: [] };
675
690
  }
676
691
 
677
- const [unified, frontend, backend] = await Promise.all([
692
+ const [unified, frontend, backend, website] = await Promise.all([
678
693
  this.loadMasterFeedback('unified'),
679
694
  this.loadMasterFeedback('frontend'),
680
695
  this.loadMasterFeedback('backend'),
696
+ this.loadMasterFeedback('website'),
681
697
  ]);
682
698
 
683
- return { unified, frontend, backend };
699
+ return { unified, frontend, backend, website };
684
700
  }
685
701
 
686
702
  /**
@@ -693,6 +709,7 @@ export class PlanStorage {
693
709
  this.clearFeedback(milestoneId, taskId, 'unified'),
694
710
  this.clearFeedback(milestoneId, taskId, 'frontend'),
695
711
  this.clearFeedback(milestoneId, taskId, 'backend'),
712
+ this.clearFeedback(milestoneId, taskId, 'website'),
696
713
  ]);
697
714
  return;
698
715
  }
@@ -716,6 +733,7 @@ export class PlanStorage {
716
733
  this.clearMasterFeedback('unified'),
717
734
  this.clearMasterFeedback('frontend'),
718
735
  this.clearMasterFeedback('backend'),
736
+ this.clearMasterFeedback('website'),
719
737
  ]);
720
738
  return;
721
739
  }
@@ -827,7 +845,7 @@ export class PlanStorage {
827
845
  }
828
846
 
829
847
  /**
830
- * Get combined feedback for all apps (fullstack)
848
+ * Get combined feedback for all apps (fullstack/all)
831
849
  */
832
850
  async getFullstackCombinedFeedback(
833
851
  milestoneId: string,
@@ -836,18 +854,20 @@ export class PlanStorage {
836
854
  unified: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
837
855
  frontend: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
838
856
  backend: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
857
+ website: { averageScore: number; allConcerns: string[]; allRecommendations: string[]; combinedAnalysis: string };
839
858
  overallScore: number;
840
859
  allTaggedConcerns: TaggedItem[];
841
860
  allTaggedRecommendations: TaggedItem[];
842
861
  }> {
843
- const [unified, frontend, backend] = await Promise.all([
862
+ const [unified, frontend, backend, website] = await Promise.all([
844
863
  this.getCombinedFeedbackForRevision(milestoneId, taskId, 'unified'),
845
864
  this.getCombinedFeedbackForRevision(milestoneId, taskId, 'frontend'),
846
865
  this.getCombinedFeedbackForRevision(milestoneId, taskId, 'backend'),
866
+ this.getCombinedFeedbackForRevision(milestoneId, taskId, 'website'),
847
867
  ]);
848
868
 
849
- // Calculate overall score (weighted average - unified counts more)
850
- const scores = [unified.averageScore, frontend.averageScore, backend.averageScore].filter(s => s > 0);
869
+ // Calculate overall score (weighted average)
870
+ const scores = [unified.averageScore, frontend.averageScore, backend.averageScore, website.averageScore].filter(s => s > 0);
851
871
  const overallScore = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
852
872
 
853
873
  // Create tagged concerns and recommendations
@@ -855,18 +875,21 @@ export class PlanStorage {
855
875
  ...unified.allConcerns.map(c => ({ app: 'unified' as ReviewAppTarget, content: c })),
856
876
  ...frontend.allConcerns.map(c => ({ app: 'frontend' as ReviewAppTarget, content: c })),
857
877
  ...backend.allConcerns.map(c => ({ app: 'backend' as ReviewAppTarget, content: c })),
878
+ ...website.allConcerns.map(c => ({ app: 'website' as ReviewAppTarget, content: c })),
858
879
  ];
859
880
 
860
881
  const allTaggedRecommendations: TaggedItem[] = [
861
882
  ...unified.allRecommendations.map(r => ({ app: 'unified' as ReviewAppTarget, content: r })),
862
883
  ...frontend.allRecommendations.map(r => ({ app: 'frontend' as ReviewAppTarget, content: r })),
863
884
  ...backend.allRecommendations.map(r => ({ app: 'backend' as ReviewAppTarget, content: r })),
885
+ ...website.allRecommendations.map(r => ({ app: 'website' as ReviewAppTarget, content: r })),
864
886
  ];
865
887
 
866
888
  return {
867
889
  unified,
868
890
  frontend,
869
891
  backend,
892
+ website,
870
893
  overallScore,
871
894
  allTaggedConcerns,
872
895
  allTaggedRecommendations,
@@ -896,7 +919,7 @@ export class PlanStorage {
896
919
  }
897
920
 
898
921
  /**
899
- * Update per-app approval status (fullstack)
922
+ * Update per-app approval status (fullstack/all)
900
923
  */
901
924
  async updateAppApproval(
902
925
  type: 'master' | 'milestone' | 'task',
@@ -918,6 +941,9 @@ export class PlanStorage {
918
941
  } else if (appTarget === 'backend') {
919
942
  metadata.backendApproved = approved;
920
943
  metadata.backendScore = score;
944
+ } else if (appTarget === 'website') {
945
+ metadata.websiteApproved = approved;
946
+ metadata.websiteScore = score;
921
947
  } else {
922
948
  metadata.unifiedApproved = approved;
923
949
  metadata.unifiedScore = score;
@@ -985,9 +1011,11 @@ export class PlanStorage {
985
1011
  status?: string;
986
1012
  frontendScore?: number;
987
1013
  backendScore?: number;
1014
+ websiteScore?: number;
988
1015
  unifiedScore?: number;
989
1016
  frontendApproved?: boolean;
990
1017
  backendApproved?: boolean;
1018
+ websiteApproved?: boolean;
991
1019
  unifiedApproved?: boolean;
992
1020
  };
993
1021
  taskPlans: Array<{
@@ -998,6 +1026,7 @@ export class PlanStorage {
998
1026
  status?: string;
999
1027
  frontendScore?: number;
1000
1028
  backendScore?: number;
1029
+ websiteScore?: number;
1001
1030
  unifiedScore?: number;
1002
1031
  }>;
1003
1032
  }> {
@@ -1010,9 +1039,11 @@ export class PlanStorage {
1010
1039
  status?: string;
1011
1040
  frontendScore?: number;
1012
1041
  backendScore?: number;
1042
+ websiteScore?: number;
1013
1043
  unifiedScore?: number;
1014
1044
  frontendApproved?: boolean;
1015
1045
  backendApproved?: boolean;
1046
+ websiteApproved?: boolean;
1016
1047
  unifiedApproved?: boolean;
1017
1048
  } = { exists: false };
1018
1049
 
@@ -1026,9 +1057,11 @@ export class PlanStorage {
1026
1057
  status: metadata.status,
1027
1058
  frontendScore: metadata.frontendScore,
1028
1059
  backendScore: metadata.backendScore,
1060
+ websiteScore: metadata.websiteScore,
1029
1061
  unifiedScore: metadata.unifiedScore,
1030
1062
  frontendApproved: metadata.frontendApproved,
1031
1063
  backendApproved: metadata.backendApproved,
1064
+ websiteApproved: metadata.websiteApproved,
1032
1065
  unifiedApproved: metadata.unifiedApproved,
1033
1066
  };
1034
1067
  } catch {
@@ -1044,6 +1077,7 @@ export class PlanStorage {
1044
1077
  status?: string;
1045
1078
  frontendScore?: number;
1046
1079
  backendScore?: number;
1080
+ websiteScore?: number;
1047
1081
  unifiedScore?: number;
1048
1082
  }> = [];
1049
1083
 
@@ -1067,6 +1101,7 @@ export class PlanStorage {
1067
1101
  status: metadata.status,
1068
1102
  frontendScore: metadata.frontendScore,
1069
1103
  backendScore: metadata.backendScore,
1104
+ websiteScore: metadata.websiteScore,
1070
1105
  unifiedScore: metadata.unifiedScore,
1071
1106
  });
1072
1107
  } catch {
@@ -1097,6 +1132,7 @@ export class PlanStorage {
1097
1132
  status: metadata.status,
1098
1133
  frontendScore: metadata.frontendScore,
1099
1134
  backendScore: metadata.backendScore,
1135
+ websiteScore: metadata.websiteScore,
1100
1136
  unifiedScore: metadata.unifiedScore,
1101
1137
  });
1102
1138
  }
@@ -1190,24 +1226,24 @@ export class PlanStorage {
1190
1226
  * Get all feedback file paths for the project
1191
1227
  */
1192
1228
  async getAllFeedbackPaths(): Promise<{
1193
- master: { unified?: string; frontend?: string; backend?: string };
1229
+ master: { unified?: string; frontend?: string; backend?: string; website?: string };
1194
1230
  milestones: Array<{
1195
1231
  milestoneId: string;
1196
- paths: { unified?: string; frontend?: string; backend?: string };
1232
+ paths: { unified?: string; frontend?: string; backend?: string; website?: string };
1197
1233
  tasks: Array<{
1198
1234
  taskId: string;
1199
- paths: { unified?: string; frontend?: string; backend?: string };
1235
+ paths: { unified?: string; frontend?: string; backend?: string; website?: string };
1200
1236
  }>;
1201
1237
  }>;
1202
1238
  }> {
1203
1239
  const result: {
1204
- master: { unified?: string; frontend?: string; backend?: string };
1240
+ master: { unified?: string; frontend?: string; backend?: string; website?: string };
1205
1241
  milestones: Array<{
1206
1242
  milestoneId: string;
1207
- paths: { unified?: string; frontend?: string; backend?: string };
1243
+ paths: { unified?: string; frontend?: string; backend?: string; website?: string };
1208
1244
  tasks: Array<{
1209
1245
  taskId: string;
1210
- paths: { unified?: string; frontend?: string; backend?: string };
1246
+ paths: { unified?: string; frontend?: string; backend?: string; website?: string };
1211
1247
  }>;
1212
1248
  }>;
1213
1249
  } = {
@@ -1221,6 +1257,7 @@ export class PlanStorage {
1221
1257
  unified: this.getMasterFeedbackPath('unified'),
1222
1258
  frontend: this.getMasterFeedbackPath('frontend'),
1223
1259
  backend: this.getMasterFeedbackPath('backend'),
1260
+ website: this.getMasterFeedbackPath('website'),
1224
1261
  };
1225
1262
  } else {
1226
1263
  result.master = {
@@ -1237,12 +1274,13 @@ export class PlanStorage {
1237
1274
 
1238
1275
  for (const dir of milestoneDirs) {
1239
1276
  const milestoneId = dir.replace('milestone-', '');
1240
- const milestonePaths: { unified?: string; frontend?: string; backend?: string } = {};
1277
+ const milestonePaths: { unified?: string; frontend?: string; backend?: string; website?: string } = {};
1241
1278
 
1242
1279
  if (this.isFullstack) {
1243
1280
  milestonePaths.unified = this.getFeedbackPath(milestoneId, undefined, 'unified');
1244
1281
  milestonePaths.frontend = this.getFeedbackPath(milestoneId, undefined, 'frontend');
1245
1282
  milestonePaths.backend = this.getFeedbackPath(milestoneId, undefined, 'backend');
1283
+ milestonePaths.website = this.getFeedbackPath(milestoneId, undefined, 'website');
1246
1284
  } else {
1247
1285
  milestonePaths.unified = this.getFeedbackPath(milestoneId);
1248
1286
  }
@@ -1250,12 +1288,13 @@ export class PlanStorage {
1250
1288
  // Get task paths
1251
1289
  const { taskPlans } = await this.getMilestoneTrackingSummary(milestoneId);
1252
1290
  const tasks = taskPlans.map(tp => {
1253
- const taskPaths: { unified?: string; frontend?: string; backend?: string } = {};
1291
+ const taskPaths: { unified?: string; frontend?: string; backend?: string; website?: string } = {};
1254
1292
 
1255
1293
  if (this.isFullstack) {
1256
1294
  taskPaths.unified = this.getFeedbackPath(milestoneId, tp.taskId, 'unified');
1257
1295
  taskPaths.frontend = this.getFeedbackPath(milestoneId, tp.taskId, 'frontend');
1258
1296
  taskPaths.backend = this.getFeedbackPath(milestoneId, tp.taskId, 'backend');
1297
+ taskPaths.website = this.getFeedbackPath(milestoneId, tp.taskId, 'website');
1259
1298
  } else {
1260
1299
  taskPaths.unified = this.getFeedbackPath(milestoneId, tp.taskId);
1261
1300
  }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * SEO acceptance tests for website projects
3
+ * Validates sitemap, robots.txt, metadata, and OG images
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ /**
10
+ * SEO check result
11
+ */
12
+ export interface SeoCheckResult {
13
+ check: string;
14
+ passed: boolean;
15
+ error?: string;
16
+ details?: string;
17
+ }
18
+
19
+ /**
20
+ * Full SEO test result
21
+ */
22
+ export interface SeoTestResult {
23
+ passed: boolean;
24
+ results: SeoCheckResult[];
25
+ summary: string;
26
+ }
27
+
28
+ /**
29
+ * Check if a file exists
30
+ */
31
+ async function fileExists(filePath: string): Promise<boolean> {
32
+ try {
33
+ await fs.access(filePath);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Read file content safely
42
+ */
43
+ async function readFile(filePath: string): Promise<string | null> {
44
+ try {
45
+ return await fs.readFile(filePath, 'utf-8');
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Find files matching a pattern recursively
53
+ */
54
+ async function findFiles(dir: string, pattern: RegExp): Promise<string[]> {
55
+ const matches: string[] = [];
56
+
57
+ try {
58
+ const entries = await fs.readdir(dir, { withFileTypes: true });
59
+
60
+ for (const entry of entries) {
61
+ const fullPath = path.join(dir, entry.name);
62
+
63
+ if (entry.isDirectory()) {
64
+ // Skip node_modules and .next
65
+ if (entry.name === 'node_modules' || entry.name === '.next') {
66
+ continue;
67
+ }
68
+ const subMatches = await findFiles(fullPath, pattern);
69
+ matches.push(...subMatches);
70
+ } else if (entry.isFile() && pattern.test(entry.name)) {
71
+ matches.push(fullPath);
72
+ }
73
+ }
74
+ } catch {
75
+ // Directory might not exist
76
+ }
77
+
78
+ return matches;
79
+ }
80
+
81
+ /**
82
+ * Check if content exports metadata
83
+ */
84
+ function hasMetadataExport(content: string): boolean {
85
+ // Check for metadata export (const or function)
86
+ return (
87
+ /export\s+(const|async\s+function)\s+metadata/i.test(content) ||
88
+ /export\s+(const|async\s+function)\s+generateMetadata/i.test(content)
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Run SEO acceptance tests on a website project
94
+ *
95
+ * @param websiteDir - Path to the website directory (apps/website or standalone)
96
+ * @returns Test results
97
+ */
98
+ export async function runSeoAcceptanceTests(websiteDir: string): Promise<SeoTestResult> {
99
+ const results: SeoCheckResult[] = [];
100
+
101
+ // 1. Check sitemap.ts exists
102
+ const sitemapPath = path.join(websiteDir, 'src', 'app', 'sitemap.ts');
103
+ const sitemapExists = await fileExists(sitemapPath);
104
+ results.push({
105
+ check: 'sitemap.ts exists',
106
+ passed: sitemapExists,
107
+ error: sitemapExists ? undefined : 'Missing src/app/sitemap.ts',
108
+ details: sitemapExists ? sitemapPath : undefined,
109
+ });
110
+
111
+ // 2. Check robots.ts exists
112
+ const robotsPath = path.join(websiteDir, 'src', 'app', 'robots.ts');
113
+ const robotsExists = await fileExists(robotsPath);
114
+ results.push({
115
+ check: 'robots.ts exists',
116
+ passed: robotsExists,
117
+ error: robotsExists ? undefined : 'Missing src/app/robots.ts',
118
+ details: robotsExists ? robotsPath : undefined,
119
+ });
120
+
121
+ // 3. Check root layout has metadata export
122
+ const layoutPath = path.join(websiteDir, 'src', 'app', 'layout.tsx');
123
+ const layoutContent = await readFile(layoutPath);
124
+ const layoutHasMetadata = layoutContent ? hasMetadataExport(layoutContent) : false;
125
+ results.push({
126
+ check: 'Root layout exports metadata',
127
+ passed: layoutHasMetadata,
128
+ error: layoutHasMetadata ? undefined : 'layout.tsx missing metadata export',
129
+ details: layoutHasMetadata ? 'Found metadata export in layout.tsx' : undefined,
130
+ });
131
+
132
+ // 4. Check OG image exists
133
+ const ogImagePaths = [
134
+ path.join(websiteDir, 'public', 'og-image.png'),
135
+ path.join(websiteDir, 'public', 'og-image.jpg'),
136
+ path.join(websiteDir, 'src', 'app', 'opengraph-image.png'),
137
+ path.join(websiteDir, 'src', 'app', 'opengraph-image.jpg'),
138
+ ];
139
+
140
+ let ogImageFound = false;
141
+ let ogImagePath = '';
142
+ for (const p of ogImagePaths) {
143
+ if (await fileExists(p)) {
144
+ ogImageFound = true;
145
+ ogImagePath = p;
146
+ break;
147
+ }
148
+ }
149
+
150
+ results.push({
151
+ check: 'OG image exists',
152
+ passed: ogImageFound,
153
+ error: ogImageFound
154
+ ? undefined
155
+ : 'Missing OG image (public/og-image.png or opengraph-image.png)',
156
+ details: ogImageFound ? ogImagePath : undefined,
157
+ });
158
+
159
+ // 5. Check page files have individual metadata
160
+ const pageFiles = await findFiles(
161
+ path.join(websiteDir, 'src', 'app'),
162
+ /page\.tsx$/
163
+ );
164
+
165
+ // Check first 5 pages (skip root layout)
166
+ const pagesToCheck = pageFiles.slice(0, 5);
167
+ for (const pageFile of pagesToCheck) {
168
+ const content = await readFile(pageFile);
169
+ const relativePath = path.relative(websiteDir, pageFile);
170
+ const hasPageMetadata = content ? hasMetadataExport(content) : false;
171
+
172
+ results.push({
173
+ check: `${relativePath} has metadata`,
174
+ passed: hasPageMetadata,
175
+ error: hasPageMetadata ? undefined : `${relativePath} missing metadata export`,
176
+ details: hasPageMetadata ? 'Found metadata export' : undefined,
177
+ });
178
+ }
179
+
180
+ // 6. Check for next.config with proper settings
181
+ const nextConfigPath = path.join(websiteDir, 'next.config.mjs');
182
+ const nextConfigAltPath = path.join(websiteDir, 'next.config.js');
183
+ const hasNextConfig =
184
+ (await fileExists(nextConfigPath)) || (await fileExists(nextConfigAltPath));
185
+
186
+ results.push({
187
+ check: 'next.config exists',
188
+ passed: hasNextConfig,
189
+ error: hasNextConfig ? undefined : 'Missing next.config.mjs or next.config.js',
190
+ });
191
+
192
+ // Calculate overall result
193
+ const passed = results.every((r) => r.passed);
194
+ const passedCount = results.filter((r) => r.passed).length;
195
+ const totalCount = results.length;
196
+
197
+ const summary = passed
198
+ ? `All ${totalCount} SEO checks passed!`
199
+ : `${passedCount}/${totalCount} SEO checks passed. ${totalCount - passedCount} issue(s) found.`;
200
+
201
+ return {
202
+ passed,
203
+ results,
204
+ summary,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Format SEO test results for display
210
+ *
211
+ * @param result - The test result
212
+ * @returns Formatted string
213
+ */
214
+ export function formatSeoResults(result: SeoTestResult): string {
215
+ const lines = ['SEO Acceptance Tests:', ''];
216
+
217
+ for (const check of result.results) {
218
+ const icon = check.passed ? '[PASS]' : '[FAIL]';
219
+ lines.push(` ${icon} ${check.check}`);
220
+ if (!check.passed && check.error) {
221
+ lines.push(` ${check.error}`);
222
+ }
223
+ }
224
+
225
+ lines.push('');
226
+ lines.push(result.summary);
227
+
228
+ return lines.join('\n');
229
+ }
230
+
231
+ /**
232
+ * Quick check if a website has basic SEO setup
233
+ *
234
+ * @param websiteDir - Path to the website directory
235
+ * @returns True if basic SEO files exist
236
+ */
237
+ export async function hasBasicSeoSetup(websiteDir: string): Promise<boolean> {
238
+ const sitemapExists = await fileExists(
239
+ path.join(websiteDir, 'src', 'app', 'sitemap.ts')
240
+ );
241
+ const robotsExists = await fileExists(
242
+ path.join(websiteDir, 'src', 'app', 'robots.ts')
243
+ );
244
+
245
+ return sitemapExists && robotsExists;
246
+ }