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.
- package/.env.example +4 -1
- package/CONTRIBUTING.md +10 -0
- package/README.md +111 -2
- package/dist/adapters/claude.d.ts +26 -2
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +257 -10
- package/dist/adapters/claude.js.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/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +79 -6
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +40 -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 +8 -3
- package/dist/types/consensus.d.ts.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 +115 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +41 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +8 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -2
- package/dist/types/workflow.js.map +1 -1
- 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/execution-mode.d.ts +2 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +20 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +19 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +2 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -1
- package/dist/workflow/milestone-workflow.js +17 -0
- package/dist/workflow/milestone-workflow.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.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 +2 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +19 -0
- 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 +289 -14
- 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/interactive.ts +76 -6
- 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 +9 -3
- package/src/types/index.ts +33 -0
- package/src/types/project.ts +139 -2
- package/src/types/workflow.ts +2 -2
- package/src/workflow/consensus.ts +3 -2
- package/src/workflow/execution-mode.ts +32 -0
- package/src/workflow/index.ts +20 -0
- package/src/workflow/milestone-workflow.ts +22 -0
- package/src/workflow/plan-mode.ts +3 -3
- 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 +25 -0
- package/src/workflow/test-runner.ts +149 -0
- 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
|
-
* │
|
|
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
|
-
* │
|
|
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
|
|
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
|
|
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
|
+
}
|