popeye-cli 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +832 -123
- package/dist/adapters/claude.d.ts +19 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +908 -42
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +47 -8
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +14 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +41 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1494 -114
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +9 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +19 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +33 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +47 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +29 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -20
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -20
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +186 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +35 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +217 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +40 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +70 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +872 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +80 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +767 -49
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +386 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +878 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +386 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +9 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +101 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +1067 -47
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +48 -9
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +47 -9
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +1774 -142
- package/src/config/defaults.ts +19 -2
- package/src/config/index.ts +36 -1
- package/src/config/schema.ts +30 -1
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -20
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -20
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +8 -0
- package/src/types/consensus.ts +197 -6
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +90 -1
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +1180 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +924 -50
- package/src/workflow/plan-storage.ts +1282 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +528 -0
- package/src/workflow/test-runner.ts +120 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/src/workflow/workspace-manager.ts +912 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- package/tests/workflow/test-runner.test.ts +5 -3
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace manager module
|
|
3
|
+
* Handles loading, saving, and querying workspace configuration for fullstack projects
|
|
4
|
+
* Also provides app-specific context for AI reviews
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import type { WorkspaceConfig, WorkspaceApp } from '../types/project.js';
|
|
10
|
+
import type { ReviewAppTarget } from '../types/consensus.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context for AI review of a specific app
|
|
14
|
+
*/
|
|
15
|
+
export interface AppReviewContext {
|
|
16
|
+
appName: 'frontend' | 'backend';
|
|
17
|
+
language: 'python' | 'typescript';
|
|
18
|
+
path: string;
|
|
19
|
+
/** Key source files content for review */
|
|
20
|
+
sourceFiles: Array<{ path: string; content: string }>;
|
|
21
|
+
/** UI spec for frontend */
|
|
22
|
+
uiSpec?: string;
|
|
23
|
+
/** API contracts (OpenAPI) */
|
|
24
|
+
apiContracts?: string;
|
|
25
|
+
/** Test file content */
|
|
26
|
+
testFiles?: Array<{ path: string; content: string }>;
|
|
27
|
+
/** Dependencies (package.json or pyproject.toml) */
|
|
28
|
+
dependencies?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Combined review context for fullstack projects
|
|
33
|
+
*/
|
|
34
|
+
export interface FullstackReviewContext {
|
|
35
|
+
frontend?: AppReviewContext;
|
|
36
|
+
backend?: AppReviewContext;
|
|
37
|
+
/** Shared contracts (OpenAPI spec) */
|
|
38
|
+
contracts?: string;
|
|
39
|
+
/** Project-level context */
|
|
40
|
+
projectName: string;
|
|
41
|
+
projectIdea?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Workspace manager class
|
|
46
|
+
*/
|
|
47
|
+
export class WorkspaceManager {
|
|
48
|
+
private projectDir: string;
|
|
49
|
+
private config: WorkspaceConfig | null = null;
|
|
50
|
+
|
|
51
|
+
constructor(projectDir: string) {
|
|
52
|
+
this.projectDir = projectDir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the path to workspace.json
|
|
57
|
+
*/
|
|
58
|
+
private getWorkspacePath(): string {
|
|
59
|
+
return path.join(this.projectDir, '.popeye', 'workspace.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if this is a workspace project
|
|
64
|
+
*/
|
|
65
|
+
async isWorkspaceProject(): Promise<boolean> {
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(this.getWorkspacePath());
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load workspace configuration
|
|
76
|
+
*/
|
|
77
|
+
async load(): Promise<WorkspaceConfig | null> {
|
|
78
|
+
try {
|
|
79
|
+
const content = await fs.readFile(this.getWorkspacePath(), 'utf-8');
|
|
80
|
+
this.config = JSON.parse(content) as WorkspaceConfig;
|
|
81
|
+
return this.config;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Save workspace configuration
|
|
89
|
+
*/
|
|
90
|
+
async save(config: WorkspaceConfig): Promise<void> {
|
|
91
|
+
const workspacePath = this.getWorkspacePath();
|
|
92
|
+
const dir = path.dirname(workspacePath);
|
|
93
|
+
|
|
94
|
+
// Ensure directory exists
|
|
95
|
+
await fs.mkdir(dir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
// Write config
|
|
98
|
+
await fs.writeFile(workspacePath, JSON.stringify(config, null, 2), 'utf-8');
|
|
99
|
+
this.config = config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the loaded configuration
|
|
104
|
+
*/
|
|
105
|
+
getConfig(): WorkspaceConfig | null {
|
|
106
|
+
return this.config;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific app configuration
|
|
111
|
+
*/
|
|
112
|
+
getApp(appName: 'frontend' | 'backend'): WorkspaceApp | undefined {
|
|
113
|
+
return this.config?.apps[appName];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the absolute path to an app
|
|
118
|
+
*/
|
|
119
|
+
getAppPath(appName: 'frontend' | 'backend'): string | null {
|
|
120
|
+
const app = this.getApp(appName);
|
|
121
|
+
if (!app) return null;
|
|
122
|
+
return path.join(this.projectDir, app.path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get all app names
|
|
127
|
+
*/
|
|
128
|
+
getAppNames(): ('frontend' | 'backend')[] {
|
|
129
|
+
if (!this.config) return [];
|
|
130
|
+
const names: ('frontend' | 'backend')[] = [];
|
|
131
|
+
if (this.config.apps.frontend) names.push('frontend');
|
|
132
|
+
if (this.config.apps.backend) names.push('backend');
|
|
133
|
+
return names;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get test command for a specific app
|
|
138
|
+
*/
|
|
139
|
+
getTestCommand(appName: 'frontend' | 'backend'): string | null {
|
|
140
|
+
const app = this.getApp(appName);
|
|
141
|
+
return app?.commands.test ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all test commands (returns object with app names as keys)
|
|
146
|
+
*/
|
|
147
|
+
getAllTestCommands(): Record<string, { path: string; command: string }> {
|
|
148
|
+
if (!this.config) return {};
|
|
149
|
+
|
|
150
|
+
const commands: Record<string, { path: string; command: string }> = {};
|
|
151
|
+
|
|
152
|
+
for (const appName of this.getAppNames()) {
|
|
153
|
+
const app = this.getApp(appName);
|
|
154
|
+
if (app) {
|
|
155
|
+
commands[appName] = {
|
|
156
|
+
path: path.join(this.projectDir, app.path),
|
|
157
|
+
command: app.commands.test,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return commands;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the combined test-all command
|
|
167
|
+
*/
|
|
168
|
+
getTestAllCommand(): string | null {
|
|
169
|
+
return this.config?.commands.testAll ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get lint command for a specific app
|
|
174
|
+
*/
|
|
175
|
+
getLintCommand(appName: 'frontend' | 'backend'): string | null {
|
|
176
|
+
const app = this.getApp(appName);
|
|
177
|
+
return app?.commands.lint ?? null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all lint commands
|
|
182
|
+
*/
|
|
183
|
+
getAllLintCommands(): Record<string, { path: string; command: string }> {
|
|
184
|
+
if (!this.config) return {};
|
|
185
|
+
|
|
186
|
+
const commands: Record<string, { path: string; command: string }> = {};
|
|
187
|
+
|
|
188
|
+
for (const appName of this.getAppNames()) {
|
|
189
|
+
const app = this.getApp(appName);
|
|
190
|
+
if (app) {
|
|
191
|
+
commands[appName] = {
|
|
192
|
+
path: path.join(this.projectDir, app.path),
|
|
193
|
+
command: app.commands.lint,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return commands;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the combined lint-all command
|
|
203
|
+
*/
|
|
204
|
+
getLintAllCommand(): string | null {
|
|
205
|
+
return this.config?.commands.lintAll ?? null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get build command for a specific app
|
|
210
|
+
*/
|
|
211
|
+
getBuildCommand(appName: 'frontend' | 'backend'): string | null {
|
|
212
|
+
const app = this.getApp(appName);
|
|
213
|
+
return app?.commands.build ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get all build commands
|
|
218
|
+
*/
|
|
219
|
+
getAllBuildCommands(): Record<string, { path: string; command: string }> {
|
|
220
|
+
if (!this.config) return {};
|
|
221
|
+
|
|
222
|
+
const commands: Record<string, { path: string; command: string }> = {};
|
|
223
|
+
|
|
224
|
+
for (const appName of this.getAppNames()) {
|
|
225
|
+
const app = this.getApp(appName);
|
|
226
|
+
if (app) {
|
|
227
|
+
commands[appName] = {
|
|
228
|
+
path: path.join(this.projectDir, app.path),
|
|
229
|
+
command: app.commands.build,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return commands;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the combined build-all command
|
|
239
|
+
*/
|
|
240
|
+
getBuildAllCommand(): string | null {
|
|
241
|
+
return this.config?.commands.buildAll ?? null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get dev command for a specific app
|
|
246
|
+
*/
|
|
247
|
+
getDevCommand(appName: 'frontend' | 'backend'): string | null {
|
|
248
|
+
const app = this.getApp(appName);
|
|
249
|
+
return app?.commands.dev ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get all dev commands
|
|
254
|
+
*/
|
|
255
|
+
getAllDevCommands(): Record<string, { path: string; command: string }> {
|
|
256
|
+
if (!this.config) return {};
|
|
257
|
+
|
|
258
|
+
const commands: Record<string, { path: string; command: string }> = {};
|
|
259
|
+
|
|
260
|
+
for (const appName of this.getAppNames()) {
|
|
261
|
+
const app = this.getApp(appName);
|
|
262
|
+
if (app) {
|
|
263
|
+
commands[appName] = {
|
|
264
|
+
path: path.join(this.projectDir, app.path),
|
|
265
|
+
command: app.commands.dev,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return commands;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get the combined dev-all command (usually docker-compose up)
|
|
275
|
+
*/
|
|
276
|
+
getDevAllCommand(): string | null {
|
|
277
|
+
return this.config?.commands.devAll ?? null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get docker-compose path
|
|
282
|
+
*/
|
|
283
|
+
getDockerComposePath(): string | null {
|
|
284
|
+
if (!this.config) return null;
|
|
285
|
+
return path.join(this.projectDir, this.config.docker.composePath);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get context roots for an app (files to include in AI context)
|
|
290
|
+
*/
|
|
291
|
+
getContextRoots(appName: 'frontend' | 'backend'): string[] {
|
|
292
|
+
const app = this.getApp(appName);
|
|
293
|
+
if (!app || !app.contextRoots) return [];
|
|
294
|
+
|
|
295
|
+
return app.contextRoots.map((root) => path.join(this.projectDir, app.path, root));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get UI spec path (for frontend)
|
|
300
|
+
*/
|
|
301
|
+
getUiSpecPath(): string | null {
|
|
302
|
+
const frontend = this.getApp('frontend');
|
|
303
|
+
if (!frontend || !frontend.uiSpec) return null;
|
|
304
|
+
return path.join(this.projectDir, frontend.uiSpec);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get contracts path (OpenAPI spec)
|
|
309
|
+
*/
|
|
310
|
+
getContractsPath(): string | null {
|
|
311
|
+
if (!this.config?.shared?.contracts) return null;
|
|
312
|
+
return path.join(this.projectDir, this.config.shared.contracts);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get app language
|
|
317
|
+
*/
|
|
318
|
+
getAppLanguage(appName: 'frontend' | 'backend'): 'python' | 'typescript' | null {
|
|
319
|
+
const app = this.getApp(appName);
|
|
320
|
+
return app?.language ?? null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Determine which app should handle a file based on path
|
|
325
|
+
*/
|
|
326
|
+
getAppForFile(filePath: string): 'frontend' | 'backend' | null {
|
|
327
|
+
const relativePath = path.relative(this.projectDir, filePath);
|
|
328
|
+
|
|
329
|
+
for (const appName of this.getAppNames()) {
|
|
330
|
+
const app = this.getApp(appName);
|
|
331
|
+
if (app && relativePath.startsWith(app.path)) {
|
|
332
|
+
return appName;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get review context for a specific app
|
|
341
|
+
* Reads key files from contextRoots to provide to AI reviewers
|
|
342
|
+
*/
|
|
343
|
+
async getAppReviewContext(
|
|
344
|
+
appName: 'frontend' | 'backend',
|
|
345
|
+
options: {
|
|
346
|
+
maxFiles?: number;
|
|
347
|
+
maxFileSize?: number;
|
|
348
|
+
includeTests?: boolean;
|
|
349
|
+
} = {}
|
|
350
|
+
): Promise<AppReviewContext | null> {
|
|
351
|
+
const { maxFiles = 20, maxFileSize = 50000, includeTests = true } = options;
|
|
352
|
+
|
|
353
|
+
const app = this.getApp(appName);
|
|
354
|
+
if (!app) return null;
|
|
355
|
+
|
|
356
|
+
const appPath = this.getAppPath(appName);
|
|
357
|
+
if (!appPath) return null;
|
|
358
|
+
|
|
359
|
+
const context: AppReviewContext = {
|
|
360
|
+
appName,
|
|
361
|
+
language: app.language,
|
|
362
|
+
path: app.path,
|
|
363
|
+
sourceFiles: [],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Read files from context roots
|
|
367
|
+
const contextRoots = this.getContextRoots(appName);
|
|
368
|
+
let filesRead = 0;
|
|
369
|
+
|
|
370
|
+
for (const root of contextRoots) {
|
|
371
|
+
if (filesRead >= maxFiles) break;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const files = await this.readDirectoryRecursive(root, maxFileSize);
|
|
375
|
+
for (const file of files) {
|
|
376
|
+
if (filesRead >= maxFiles) break;
|
|
377
|
+
context.sourceFiles.push(file);
|
|
378
|
+
filesRead++;
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
// Directory doesn't exist, skip
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Read UI spec for frontend
|
|
386
|
+
if (appName === 'frontend') {
|
|
387
|
+
const uiSpecPath = this.getUiSpecPath();
|
|
388
|
+
if (uiSpecPath) {
|
|
389
|
+
try {
|
|
390
|
+
context.uiSpec = await fs.readFile(uiSpecPath, 'utf-8');
|
|
391
|
+
} catch {
|
|
392
|
+
// UI spec doesn't exist
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Read API contracts
|
|
398
|
+
const contractsPath = this.getContractsPath();
|
|
399
|
+
if (contractsPath) {
|
|
400
|
+
try {
|
|
401
|
+
context.apiContracts = await fs.readFile(contractsPath, 'utf-8');
|
|
402
|
+
} catch {
|
|
403
|
+
// Contracts don't exist
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Read test files
|
|
408
|
+
if (includeTests) {
|
|
409
|
+
const testDir = path.join(appPath, appName === 'frontend' ? 'src' : 'tests');
|
|
410
|
+
try {
|
|
411
|
+
const testFiles = await this.findTestFiles(testDir, app.language);
|
|
412
|
+
context.testFiles = testFiles.slice(0, 5); // Limit test files
|
|
413
|
+
} catch {
|
|
414
|
+
// Test directory doesn't exist
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Read dependencies file
|
|
419
|
+
try {
|
|
420
|
+
const depsFile = app.language === 'typescript'
|
|
421
|
+
? path.join(appPath, 'package.json')
|
|
422
|
+
: path.join(appPath, 'pyproject.toml');
|
|
423
|
+
context.dependencies = await fs.readFile(depsFile, 'utf-8');
|
|
424
|
+
} catch {
|
|
425
|
+
// Dependencies file doesn't exist
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return context;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get combined review context for fullstack project
|
|
433
|
+
*/
|
|
434
|
+
async getFullstackReviewContext(
|
|
435
|
+
projectName: string,
|
|
436
|
+
projectIdea?: string,
|
|
437
|
+
options: {
|
|
438
|
+
maxFilesPerApp?: number;
|
|
439
|
+
includeTests?: boolean;
|
|
440
|
+
} = {}
|
|
441
|
+
): Promise<FullstackReviewContext> {
|
|
442
|
+
const { maxFilesPerApp = 15, includeTests = true } = options;
|
|
443
|
+
|
|
444
|
+
const context: FullstackReviewContext = {
|
|
445
|
+
projectName,
|
|
446
|
+
projectIdea,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Get frontend context
|
|
450
|
+
const frontend = await this.getAppReviewContext('frontend', {
|
|
451
|
+
maxFiles: maxFilesPerApp,
|
|
452
|
+
includeTests,
|
|
453
|
+
});
|
|
454
|
+
if (frontend) {
|
|
455
|
+
context.frontend = frontend;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Get backend context
|
|
459
|
+
const backend = await this.getAppReviewContext('backend', {
|
|
460
|
+
maxFiles: maxFilesPerApp,
|
|
461
|
+
includeTests,
|
|
462
|
+
});
|
|
463
|
+
if (backend) {
|
|
464
|
+
context.backend = backend;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Get shared contracts
|
|
468
|
+
const contractsPath = this.getContractsPath();
|
|
469
|
+
if (contractsPath) {
|
|
470
|
+
try {
|
|
471
|
+
context.contracts = await fs.readFile(contractsPath, 'utf-8');
|
|
472
|
+
} catch {
|
|
473
|
+
// Contracts don't exist
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return context;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Format app context for AI review prompt
|
|
482
|
+
*/
|
|
483
|
+
formatContextForReview(context: AppReviewContext): string {
|
|
484
|
+
const lines: string[] = [];
|
|
485
|
+
|
|
486
|
+
lines.push(`## ${context.appName.toUpperCase()} (${context.language})`);
|
|
487
|
+
lines.push(`Path: ${context.path}`);
|
|
488
|
+
lines.push('');
|
|
489
|
+
|
|
490
|
+
if (context.dependencies) {
|
|
491
|
+
lines.push('### Dependencies');
|
|
492
|
+
lines.push('```');
|
|
493
|
+
lines.push(context.dependencies.slice(0, 2000)); // Limit size
|
|
494
|
+
lines.push('```');
|
|
495
|
+
lines.push('');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (context.uiSpec) {
|
|
499
|
+
lines.push('### UI Specification');
|
|
500
|
+
lines.push('```json');
|
|
501
|
+
lines.push(context.uiSpec.slice(0, 3000));
|
|
502
|
+
lines.push('```');
|
|
503
|
+
lines.push('');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (context.apiContracts) {
|
|
507
|
+
lines.push('### API Contracts (OpenAPI)');
|
|
508
|
+
lines.push('```yaml');
|
|
509
|
+
lines.push(context.apiContracts.slice(0, 3000));
|
|
510
|
+
lines.push('```');
|
|
511
|
+
lines.push('');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (context.sourceFiles.length > 0) {
|
|
515
|
+
lines.push('### Key Source Files');
|
|
516
|
+
for (const file of context.sourceFiles) {
|
|
517
|
+
const relativePath = path.relative(this.projectDir, file.path);
|
|
518
|
+
lines.push(`#### ${relativePath}`);
|
|
519
|
+
lines.push('```');
|
|
520
|
+
lines.push(file.content.slice(0, 5000)); // Limit per file
|
|
521
|
+
lines.push('```');
|
|
522
|
+
lines.push('');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return lines.join('\n');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Format fullstack context for review prompt
|
|
531
|
+
*/
|
|
532
|
+
formatFullstackContextForReview(context: FullstackReviewContext): string {
|
|
533
|
+
const lines: string[] = [];
|
|
534
|
+
|
|
535
|
+
lines.push(`# Project: ${context.projectName}`);
|
|
536
|
+
if (context.projectIdea) {
|
|
537
|
+
lines.push(`**Idea:** ${context.projectIdea}`);
|
|
538
|
+
}
|
|
539
|
+
lines.push('');
|
|
540
|
+
|
|
541
|
+
if (context.contracts) {
|
|
542
|
+
lines.push('## Shared API Contracts');
|
|
543
|
+
lines.push('```yaml');
|
|
544
|
+
lines.push(context.contracts.slice(0, 3000));
|
|
545
|
+
lines.push('```');
|
|
546
|
+
lines.push('');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (context.frontend) {
|
|
550
|
+
lines.push(this.formatContextForReview(context.frontend));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (context.backend) {
|
|
554
|
+
lines.push(this.formatContextForReview(context.backend));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return lines.join('\n');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Determine review app target based on plan content
|
|
562
|
+
* Analyzes plan text to determine if it's frontend, backend, or unified
|
|
563
|
+
*/
|
|
564
|
+
categorizeByPlanContent(planContent: string): ReviewAppTarget {
|
|
565
|
+
const lowerContent = planContent.toLowerCase();
|
|
566
|
+
|
|
567
|
+
// Frontend indicators
|
|
568
|
+
const frontendKeywords = [
|
|
569
|
+
'react', 'component', 'jsx', 'tsx', 'css', 'tailwind', 'ui',
|
|
570
|
+
'button', 'form', 'page', 'layout', 'style', 'vite', 'frontend',
|
|
571
|
+
'client', 'browser', 'dom', 'render', 'hook', 'state',
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
// Backend indicators
|
|
575
|
+
const backendKeywords = [
|
|
576
|
+
'api', 'endpoint', 'route', 'database', 'model', 'schema',
|
|
577
|
+
'fastapi', 'flask', 'django', 'express', 'server', 'backend',
|
|
578
|
+
'authentication', 'middleware', 'orm', 'sql', 'query', 'crud',
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
const frontendScore = frontendKeywords.filter(kw => lowerContent.includes(kw)).length;
|
|
582
|
+
const backendScore = backendKeywords.filter(kw => lowerContent.includes(kw)).length;
|
|
583
|
+
|
|
584
|
+
// Threshold for classification
|
|
585
|
+
if (frontendScore > backendScore * 2 && frontendScore >= 3) {
|
|
586
|
+
return 'frontend';
|
|
587
|
+
}
|
|
588
|
+
if (backendScore > frontendScore * 2 && backendScore >= 3) {
|
|
589
|
+
return 'backend';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Mixed or unclear - unified
|
|
593
|
+
return 'unified';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Read directory recursively and return file contents
|
|
598
|
+
*/
|
|
599
|
+
private async readDirectoryRecursive(
|
|
600
|
+
dir: string,
|
|
601
|
+
maxFileSize: number
|
|
602
|
+
): Promise<Array<{ path: string; content: string }>> {
|
|
603
|
+
const files: Array<{ path: string; content: string }> = [];
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
607
|
+
|
|
608
|
+
for (const entry of entries) {
|
|
609
|
+
const fullPath = path.join(dir, entry.name);
|
|
610
|
+
|
|
611
|
+
// Skip node_modules, __pycache__, etc.
|
|
612
|
+
if (entry.name.startsWith('.') ||
|
|
613
|
+
entry.name === 'node_modules' ||
|
|
614
|
+
entry.name === '__pycache__' ||
|
|
615
|
+
entry.name === 'dist' ||
|
|
616
|
+
entry.name === 'build') {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (entry.isDirectory()) {
|
|
621
|
+
const subFiles = await this.readDirectoryRecursive(fullPath, maxFileSize);
|
|
622
|
+
files.push(...subFiles);
|
|
623
|
+
} else if (entry.isFile()) {
|
|
624
|
+
// Only read code files
|
|
625
|
+
const ext = path.extname(entry.name);
|
|
626
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.py', '.json', '.yaml', '.yml'].includes(ext)) {
|
|
627
|
+
try {
|
|
628
|
+
const stat = await fs.stat(fullPath);
|
|
629
|
+
if (stat.size <= maxFileSize) {
|
|
630
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
631
|
+
files.push({ path: fullPath, content });
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
// Skip unreadable files
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
// Directory doesn't exist or isn't readable
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return files;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Find test files in a directory
|
|
648
|
+
*/
|
|
649
|
+
private async findTestFiles(
|
|
650
|
+
dir: string,
|
|
651
|
+
language: 'python' | 'typescript'
|
|
652
|
+
): Promise<Array<{ path: string; content: string }>> {
|
|
653
|
+
const files: Array<{ path: string; content: string }> = [];
|
|
654
|
+
|
|
655
|
+
const testPatterns = language === 'typescript'
|
|
656
|
+
? ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx']
|
|
657
|
+
: ['test_', '_test.py'];
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const allFiles = await this.readDirectoryRecursive(dir, 30000);
|
|
661
|
+
|
|
662
|
+
for (const file of allFiles) {
|
|
663
|
+
const fileName = path.basename(file.path);
|
|
664
|
+
const isTestFile = testPatterns.some(pattern =>
|
|
665
|
+
language === 'typescript'
|
|
666
|
+
? fileName.endsWith(pattern)
|
|
667
|
+
: fileName.startsWith(pattern) || fileName.endsWith(pattern)
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
if (isTestFile) {
|
|
671
|
+
files.push(file);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
// Directory doesn't exist
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return files;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get feedback document paths for workspace
|
|
683
|
+
*/
|
|
684
|
+
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 };
|
|
688
|
+
} {
|
|
689
|
+
const plansDir = path.join(this.projectDir, 'docs', 'plans');
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
master: {
|
|
693
|
+
unified: path.join(plansDir, 'master', 'unified', 'feedback.md'),
|
|
694
|
+
frontend: path.join(plansDir, 'master', 'frontend', 'feedback.md'),
|
|
695
|
+
backend: path.join(plansDir, 'master', 'backend', 'feedback.md'),
|
|
696
|
+
},
|
|
697
|
+
getMilestonePaths: (milestoneId: string) => ({
|
|
698
|
+
unified: path.join(plansDir, `milestone-${milestoneId}`, 'unified', 'feedback.md'),
|
|
699
|
+
frontend: path.join(plansDir, `milestone-${milestoneId}`, 'frontend', 'feedback.md'),
|
|
700
|
+
backend: path.join(plansDir, `milestone-${milestoneId}`, 'backend', 'feedback.md'),
|
|
701
|
+
}),
|
|
702
|
+
getTaskPaths: (milestoneId: string, taskId: string) => ({
|
|
703
|
+
unified: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'unified', 'feedback.md'),
|
|
704
|
+
frontend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'frontend', 'feedback.md'),
|
|
705
|
+
backend: path.join(plansDir, `milestone-${milestoneId}`, 'tasks', `task-${taskId}`, 'backend', 'feedback.md'),
|
|
706
|
+
}),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Load workspace configuration from a project directory
|
|
713
|
+
*
|
|
714
|
+
* @param projectDir - Project directory
|
|
715
|
+
* @returns WorkspaceConfig or null if not a workspace project
|
|
716
|
+
*/
|
|
717
|
+
export async function loadWorkspace(projectDir: string): Promise<WorkspaceConfig | null> {
|
|
718
|
+
const manager = new WorkspaceManager(projectDir);
|
|
719
|
+
return manager.load();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Save workspace configuration to a project directory
|
|
724
|
+
*
|
|
725
|
+
* @param projectDir - Project directory
|
|
726
|
+
* @param config - Workspace configuration
|
|
727
|
+
*/
|
|
728
|
+
export async function saveWorkspace(projectDir: string, config: WorkspaceConfig): Promise<void> {
|
|
729
|
+
const manager = new WorkspaceManager(projectDir);
|
|
730
|
+
return manager.save(config);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Check if a directory is a workspace project
|
|
735
|
+
*
|
|
736
|
+
* @param projectDir - Project directory
|
|
737
|
+
* @returns True if workspace project
|
|
738
|
+
*/
|
|
739
|
+
export async function isWorkspaceProject(projectDir: string): Promise<boolean> {
|
|
740
|
+
const manager = new WorkspaceManager(projectDir);
|
|
741
|
+
return manager.isWorkspaceProject();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get app context for AI code generation
|
|
746
|
+
*
|
|
747
|
+
* @param projectDir - Project directory
|
|
748
|
+
* @param appName - App name
|
|
749
|
+
* @returns Object with app info and context files
|
|
750
|
+
*/
|
|
751
|
+
export async function getAppContext(
|
|
752
|
+
projectDir: string,
|
|
753
|
+
appName: 'frontend' | 'backend'
|
|
754
|
+
): Promise<{
|
|
755
|
+
app: WorkspaceApp | undefined;
|
|
756
|
+
language: 'python' | 'typescript' | null;
|
|
757
|
+
contextRoots: string[];
|
|
758
|
+
path: string | null;
|
|
759
|
+
} | null> {
|
|
760
|
+
const manager = new WorkspaceManager(projectDir);
|
|
761
|
+
const config = await manager.load();
|
|
762
|
+
|
|
763
|
+
if (!config) return null;
|
|
764
|
+
|
|
765
|
+
const app = manager.getApp(appName);
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
app,
|
|
769
|
+
language: manager.getAppLanguage(appName),
|
|
770
|
+
contextRoots: manager.getContextRoots(appName),
|
|
771
|
+
path: manager.getAppPath(appName),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Get test commands for workspace
|
|
777
|
+
*
|
|
778
|
+
* @param projectDir - Project directory
|
|
779
|
+
* @returns Test commands per app and combined command
|
|
780
|
+
*/
|
|
781
|
+
export async function getTestCommands(projectDir: string): Promise<{
|
|
782
|
+
perApp: Record<string, { path: string; command: string }>;
|
|
783
|
+
combined: string | null;
|
|
784
|
+
} | null> {
|
|
785
|
+
const manager = new WorkspaceManager(projectDir);
|
|
786
|
+
const config = await manager.load();
|
|
787
|
+
|
|
788
|
+
if (!config) return null;
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
perApp: manager.getAllTestCommands(),
|
|
792
|
+
combined: manager.getTestAllCommand(),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Get build commands for workspace
|
|
798
|
+
*
|
|
799
|
+
* @param projectDir - Project directory
|
|
800
|
+
* @returns Build commands per app and combined command
|
|
801
|
+
*/
|
|
802
|
+
export async function getBuildCommands(projectDir: string): Promise<{
|
|
803
|
+
perApp: Record<string, { path: string; command: string }>;
|
|
804
|
+
combined: string | null;
|
|
805
|
+
} | null> {
|
|
806
|
+
const manager = new WorkspaceManager(projectDir);
|
|
807
|
+
const config = await manager.load();
|
|
808
|
+
|
|
809
|
+
if (!config) return null;
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
perApp: manager.getAllBuildCommands(),
|
|
813
|
+
combined: manager.getBuildAllCommand(),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Get app-specific review context
|
|
819
|
+
*
|
|
820
|
+
* @param projectDir - Project directory
|
|
821
|
+
* @param appName - App name (frontend or backend)
|
|
822
|
+
* @returns Review context with source files and metadata
|
|
823
|
+
*/
|
|
824
|
+
export async function getAppReviewContext(
|
|
825
|
+
projectDir: string,
|
|
826
|
+
appName: 'frontend' | 'backend'
|
|
827
|
+
): Promise<AppReviewContext | null> {
|
|
828
|
+
const manager = new WorkspaceManager(projectDir);
|
|
829
|
+
const config = await manager.load();
|
|
830
|
+
|
|
831
|
+
if (!config) return null;
|
|
832
|
+
|
|
833
|
+
return manager.getAppReviewContext(appName);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Get fullstack review context for AI reviews
|
|
838
|
+
*
|
|
839
|
+
* @param projectDir - Project directory
|
|
840
|
+
* @param projectName - Project name
|
|
841
|
+
* @param projectIdea - Original project idea
|
|
842
|
+
* @returns Combined review context for both apps
|
|
843
|
+
*/
|
|
844
|
+
export async function getFullstackReviewContext(
|
|
845
|
+
projectDir: string,
|
|
846
|
+
projectName: string,
|
|
847
|
+
projectIdea?: string
|
|
848
|
+
): Promise<FullstackReviewContext | null> {
|
|
849
|
+
const manager = new WorkspaceManager(projectDir);
|
|
850
|
+
const config = await manager.load();
|
|
851
|
+
|
|
852
|
+
if (!config) return null;
|
|
853
|
+
|
|
854
|
+
return manager.getFullstackReviewContext(projectName, projectIdea);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Format context for AI review prompt
|
|
859
|
+
*
|
|
860
|
+
* @param projectDir - Project directory
|
|
861
|
+
* @param projectName - Project name
|
|
862
|
+
* @param projectIdea - Original project idea
|
|
863
|
+
* @returns Formatted string for AI review
|
|
864
|
+
*/
|
|
865
|
+
export async function formatContextForAIReview(
|
|
866
|
+
projectDir: string,
|
|
867
|
+
projectName: string,
|
|
868
|
+
projectIdea?: string
|
|
869
|
+
): Promise<string | null> {
|
|
870
|
+
const manager = new WorkspaceManager(projectDir);
|
|
871
|
+
const config = await manager.load();
|
|
872
|
+
|
|
873
|
+
if (!config) return null;
|
|
874
|
+
|
|
875
|
+
const context = await manager.getFullstackReviewContext(projectName, projectIdea);
|
|
876
|
+
return manager.formatFullstackContextForReview(context);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Categorize a task/plan as frontend, backend, or unified
|
|
881
|
+
*
|
|
882
|
+
* @param projectDir - Project directory
|
|
883
|
+
* @param planContent - Plan or task content to analyze
|
|
884
|
+
* @returns App target category
|
|
885
|
+
*/
|
|
886
|
+
export async function categorizePlanContent(
|
|
887
|
+
projectDir: string,
|
|
888
|
+
planContent: string
|
|
889
|
+
): Promise<ReviewAppTarget> {
|
|
890
|
+
const manager = new WorkspaceManager(projectDir);
|
|
891
|
+
await manager.load();
|
|
892
|
+
return manager.categorizeByPlanContent(planContent);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Get feedback paths for a workspace project
|
|
897
|
+
*
|
|
898
|
+
* @param projectDir - Project directory
|
|
899
|
+
* @returns Object with feedback path getters
|
|
900
|
+
*/
|
|
901
|
+
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 };
|
|
905
|
+
} | null> {
|
|
906
|
+
const manager = new WorkspaceManager(projectDir);
|
|
907
|
+
const config = await manager.load();
|
|
908
|
+
|
|
909
|
+
if (!config) return null;
|
|
910
|
+
|
|
911
|
+
return manager.getFeedbackPaths();
|
|
912
|
+
}
|