pi-harness-runtime 0.2.0 → 0.3.1
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/README.md +133 -158
- package/harness/agent-handoff.ts +189 -0
- package/harness/blackboard.ts +291 -0
- package/harness/context-window-manager.ts +210 -0
- package/harness/e2e/playwright-runner.ts +252 -0
- package/harness/e2e/test-engine.ts +402 -0
- package/harness/job-state-machine.ts +363 -0
- package/harness/loop-runtime.ts +337 -0
- package/harness/master-planner.ts +327 -0
- package/harness/project-detector/detector.ts +328 -0
- package/harness/repair-engine.ts +340 -0
- package/harness/task-graph.ts +336 -0
- package/index.ts +294 -2
- package/package.json +6 -3
- package/packages/checkpoint/README.md +3 -0
- package/packages/checkpoint/src/checkpoint-manager.ts +38 -0
- package/packages/provider-router/src/provider-router.ts +42 -0
- package/packages/providers/README.md +3 -0
- package/packages/providers/adapters.ts +261 -0
- package/packages/quota-manager/README.md +3 -0
- package/packages/quota-manager/quota-manager.ts +328 -0
- package/packages/runtime/README.md +3 -0
- package/packages/scheduler/README.md +3 -0
- package/packages/scheduler/src/scheduler.ts +50 -0
- package/packages/shared-context/README.md +3 -0
- package/packages/shared-context/src/shared-context.ts +42 -0
- package/packages/tui/README.md +3 -0
- package/packages/types/src/runtime-types.ts +439 -0
- package/packages/worktree/README.md +3 -0
- package/packages/worktree/worktree.ts +293 -0
- package/skills/harness-runtime/SKILL.md +186 -72
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Master Planner — RFC-0017
|
|
3
|
+
*
|
|
4
|
+
* Converts a human requirement into an executable task graph.
|
|
5
|
+
* Uses a planning LLM to decompose the requirement into tasks with dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TaskGraph } from "../packages/types/src/runtime-types.ts";
|
|
9
|
+
import { TaskGraphManager } from "./task-graph.ts";
|
|
10
|
+
|
|
11
|
+
export interface PlanResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
graph?: TaskGraph;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PlannerOptions {
|
|
18
|
+
planningProvider?: {
|
|
19
|
+
call: (prompt: string, systemPrompt: string) => Promise<string>;
|
|
20
|
+
};
|
|
21
|
+
maxTasks?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a software project planner. Given a human requirement, decompose it into a clear task list.
|
|
25
|
+
|
|
26
|
+
Rules:
|
|
27
|
+
1. Each task should be atomic and independently testable
|
|
28
|
+
2. Tasks must be ordered with proper dependencies
|
|
29
|
+
3. Include acceptance criteria for each task
|
|
30
|
+
4. Consider: analysis, implementation, testing, review phases
|
|
31
|
+
5. Output ONLY valid JSON in the specified format
|
|
32
|
+
|
|
33
|
+
Output format:
|
|
34
|
+
{
|
|
35
|
+
"tasks": [
|
|
36
|
+
{
|
|
37
|
+
"id": "task-001",
|
|
38
|
+
"title": "Descriptive title",
|
|
39
|
+
"description": "What this task does",
|
|
40
|
+
"dependencies": [], // array of task IDs this depends on
|
|
41
|
+
"acceptanceCriteria": ["criterion 1", "criterion 2"]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}`;
|
|
45
|
+
|
|
46
|
+
export class MasterPlanner {
|
|
47
|
+
constructor(private readonly options: PlannerOptions = {}) {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a plan from a requirement
|
|
51
|
+
*/
|
|
52
|
+
async createPlan(
|
|
53
|
+
requirement: string,
|
|
54
|
+
jobId: string,
|
|
55
|
+
rootDir: string,
|
|
56
|
+
): Promise<PlanResult> {
|
|
57
|
+
try {
|
|
58
|
+
// Build the planning prompt
|
|
59
|
+
const userPrompt = `Human requirement:\n${requirement}\n\nMax tasks: ${this.options.maxTasks ?? 20}`;
|
|
60
|
+
|
|
61
|
+
let taskList: {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
description: string;
|
|
65
|
+
dependencies: string[];
|
|
66
|
+
acceptanceCriteria?: string[];
|
|
67
|
+
}[];
|
|
68
|
+
|
|
69
|
+
if (this.options.planningProvider) {
|
|
70
|
+
// Use LLM to generate task list
|
|
71
|
+
const response = await this.options.planningProvider.call(
|
|
72
|
+
userPrompt,
|
|
73
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
74
|
+
);
|
|
75
|
+
const parsed = this.parsePlanningResponse(response);
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
return { success: false, error: "Failed to parse planning response" };
|
|
78
|
+
}
|
|
79
|
+
taskList = parsed;
|
|
80
|
+
} else {
|
|
81
|
+
// Use heuristic planner for simple requirements
|
|
82
|
+
taskList = this.heuristicPlan(requirement);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate the task list
|
|
86
|
+
if (taskList.length === 0) {
|
|
87
|
+
return { success: false, error: "No tasks generated from requirement" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate dependencies (no cycles, all deps exist)
|
|
91
|
+
const taskIds = new Set(taskList.map((t) => t.id));
|
|
92
|
+
for (const task of taskList) {
|
|
93
|
+
for (const dep of task.dependencies) {
|
|
94
|
+
if (!taskIds.has(dep)) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: `Task ${task.id} has invalid dependency: ${dep}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create task graph
|
|
104
|
+
const graphManager = new TaskGraphManager({ jobId });
|
|
105
|
+
|
|
106
|
+
for (const task of taskList) {
|
|
107
|
+
graphManager.addTask(
|
|
108
|
+
task.id,
|
|
109
|
+
task.title,
|
|
110
|
+
task.description,
|
|
111
|
+
task.dependencies,
|
|
112
|
+
task.acceptanceCriteria,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const graph = graphManager.getGraph();
|
|
117
|
+
|
|
118
|
+
// Save the graph
|
|
119
|
+
await graphManager.save(rootDir);
|
|
120
|
+
|
|
121
|
+
return { success: true, graph };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return { success: false, error: String(error) };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse planning response from LLM
|
|
129
|
+
*/
|
|
130
|
+
private parsePlanningResponse(
|
|
131
|
+
response: string,
|
|
132
|
+
):
|
|
133
|
+
| {
|
|
134
|
+
id: string;
|
|
135
|
+
title: string;
|
|
136
|
+
description: string;
|
|
137
|
+
dependencies: string[];
|
|
138
|
+
acceptanceCriteria?: string[];
|
|
139
|
+
}[]
|
|
140
|
+
| null {
|
|
141
|
+
// Try to extract JSON from response
|
|
142
|
+
const jsonMatch =
|
|
143
|
+
response.match(/```json\n([\s\S]*?)\n```/) ??
|
|
144
|
+
response.match(/\{[\s\S]*"tasks"[\s\S]*\}/);
|
|
145
|
+
|
|
146
|
+
if (!jsonMatch) {
|
|
147
|
+
console.error("Failed to extract JSON from planning response");
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const jsonStr = jsonMatch[1] ?? jsonMatch[0];
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(jsonStr);
|
|
155
|
+
return parsed.tasks ?? [];
|
|
156
|
+
} catch {
|
|
157
|
+
console.error("Failed to parse JSON from planning response");
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Heuristic planner for simple requirements
|
|
164
|
+
* Used when no LLM provider is available
|
|
165
|
+
*/
|
|
166
|
+
heuristicPlan(
|
|
167
|
+
requirement: string,
|
|
168
|
+
): {
|
|
169
|
+
id: string;
|
|
170
|
+
title: string;
|
|
171
|
+
description: string;
|
|
172
|
+
dependencies: string[];
|
|
173
|
+
acceptanceCriteria?: string[];
|
|
174
|
+
}[] {
|
|
175
|
+
const tasks: {
|
|
176
|
+
id: string;
|
|
177
|
+
title: string;
|
|
178
|
+
description: string;
|
|
179
|
+
dependencies: string[];
|
|
180
|
+
acceptanceCriteria?: string[];
|
|
181
|
+
}[] = [];
|
|
182
|
+
const req = requirement.toLowerCase();
|
|
183
|
+
|
|
184
|
+
// Task 1: Analysis
|
|
185
|
+
tasks.push({
|
|
186
|
+
id: "task-001",
|
|
187
|
+
title: "Analyze requirements",
|
|
188
|
+
description: `Analyze and document the requirements: ${requirement}`,
|
|
189
|
+
dependencies: [],
|
|
190
|
+
acceptanceCriteria: [
|
|
191
|
+
"Requirements are clearly documented",
|
|
192
|
+
"All edge cases identified",
|
|
193
|
+
"Technical approach defined",
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Task 2: Implementation
|
|
198
|
+
if (
|
|
199
|
+
req.includes("api") ||
|
|
200
|
+
req.includes("endpoint") ||
|
|
201
|
+
req.includes("backend")
|
|
202
|
+
) {
|
|
203
|
+
tasks.push({
|
|
204
|
+
id: "task-002",
|
|
205
|
+
title: "Implement API endpoints",
|
|
206
|
+
description: "Create API endpoints based on requirements",
|
|
207
|
+
dependencies: ["task-001"],
|
|
208
|
+
acceptanceCriteria: [
|
|
209
|
+
"Endpoints return correct responses",
|
|
210
|
+
"Error handling implemented",
|
|
211
|
+
"Input validation in place",
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
req.includes("database") ||
|
|
218
|
+
req.includes("model") ||
|
|
219
|
+
req.includes("schema")
|
|
220
|
+
) {
|
|
221
|
+
tasks.push({
|
|
222
|
+
id: "task-003",
|
|
223
|
+
title: "Implement database schema",
|
|
224
|
+
description: "Create database models and migrations",
|
|
225
|
+
dependencies: ["task-001"],
|
|
226
|
+
acceptanceCriteria: [
|
|
227
|
+
"Schema matches requirements",
|
|
228
|
+
"Migrations run successfully",
|
|
229
|
+
"Relationships defined correctly",
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
req.includes("ui") ||
|
|
236
|
+
req.includes("frontend") ||
|
|
237
|
+
req.includes("page") ||
|
|
238
|
+
req.includes("component")
|
|
239
|
+
) {
|
|
240
|
+
tasks.push({
|
|
241
|
+
id: "task-004",
|
|
242
|
+
title: "Implement UI components",
|
|
243
|
+
description: "Create frontend UI components",
|
|
244
|
+
dependencies: ["task-001"],
|
|
245
|
+
acceptanceCriteria: [
|
|
246
|
+
"Components match design",
|
|
247
|
+
"Responsive on all devices",
|
|
248
|
+
"Accessible",
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Task 5: Tests
|
|
254
|
+
const implDeps = tasks
|
|
255
|
+
.filter((t) => t.id.startsWith("task-00"))
|
|
256
|
+
.map((t) => t.id);
|
|
257
|
+
tasks.push({
|
|
258
|
+
id: "task-010",
|
|
259
|
+
title: "Write unit tests",
|
|
260
|
+
description: "Write unit tests for all implemented code",
|
|
261
|
+
dependencies: implDeps.length > 0 ? implDeps : ["task-001"],
|
|
262
|
+
acceptanceCriteria: [
|
|
263
|
+
"All new code has >80% test coverage",
|
|
264
|
+
"All tests pass",
|
|
265
|
+
"Edge cases covered",
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Task 6: Integration
|
|
270
|
+
tasks.push({
|
|
271
|
+
id: "task-011",
|
|
272
|
+
title: "Integration testing",
|
|
273
|
+
description: "Run integration tests and verify end-to-end flow",
|
|
274
|
+
dependencies: ["task-010"],
|
|
275
|
+
acceptanceCriteria: [
|
|
276
|
+
"Integration tests pass",
|
|
277
|
+
"No regression in existing functionality",
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Task 7: Review
|
|
282
|
+
tasks.push({
|
|
283
|
+
id: "task-012",
|
|
284
|
+
title: "Code review",
|
|
285
|
+
description: "Review code for quality, security, and best practices",
|
|
286
|
+
dependencies: ["task-011"],
|
|
287
|
+
acceptanceCriteria: [
|
|
288
|
+
"Code follows project style guide",
|
|
289
|
+
"No security vulnerabilities",
|
|
290
|
+
"Documentation updated",
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return tasks;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Generate a simple task ID
|
|
299
|
+
*/
|
|
300
|
+
static generateTaskId(index: number): string {
|
|
301
|
+
return `task-${String(index).padStart(3, "0")}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parse a requirement into a basic task list (synchronous, no LLM)
|
|
307
|
+
*/
|
|
308
|
+
export function parseRequirementIntoTasks(
|
|
309
|
+
requirement: string,
|
|
310
|
+
jobId: string,
|
|
311
|
+
): TaskGraphManager {
|
|
312
|
+
const planner = new MasterPlanner();
|
|
313
|
+
const taskList = planner.heuristicPlan(requirement);
|
|
314
|
+
|
|
315
|
+
const graphManager = new TaskGraphManager({ jobId });
|
|
316
|
+
for (const task of taskList) {
|
|
317
|
+
graphManager.addTask(
|
|
318
|
+
task.id,
|
|
319
|
+
task.title,
|
|
320
|
+
task.description,
|
|
321
|
+
task.dependencies,
|
|
322
|
+
task.acceptanceCriteria,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return graphManager;
|
|
327
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Detector — RFC-0014
|
|
3
|
+
*
|
|
4
|
+
* Auto-detect project framework and choose suitable dummy data,
|
|
5
|
+
* seed strategy, and E2E approach.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ProjectType,
|
|
10
|
+
ProjectDetection,
|
|
11
|
+
SeedStrategy,
|
|
12
|
+
E2EStrategy,
|
|
13
|
+
} from "../../packages/types/src/runtime-types.ts";
|
|
14
|
+
import { readJson } from "../../cli.ts";
|
|
15
|
+
// @ts-expect-error - Bun has built-in Node.js types
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface DetectionSignal {
|
|
19
|
+
pattern: string;
|
|
20
|
+
weight: number;
|
|
21
|
+
matched: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProjectDetectorOptions {
|
|
25
|
+
rootDir?: string;
|
|
26
|
+
maxDepth?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ProjectDetector {
|
|
30
|
+
private readonly signals: Map<ProjectType, DetectionSignal[]> = new Map([
|
|
31
|
+
[
|
|
32
|
+
"frappe_erpnext",
|
|
33
|
+
[
|
|
34
|
+
{ pattern: "frappe-bench/sites", weight: 0.3, matched: false },
|
|
35
|
+
{ pattern: "frappe-bench/apps", weight: 0.2, matched: false },
|
|
36
|
+
{ pattern: "hooks.py", weight: 0.2, matched: false },
|
|
37
|
+
{ pattern: "doctype/", weight: 0.2, matched: false },
|
|
38
|
+
{ pattern: "bench", weight: 0.1, matched: false },
|
|
39
|
+
],
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"frappe_spa",
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
pattern: "frappe-bench/apps/*/frontend",
|
|
46
|
+
weight: 0.3,
|
|
47
|
+
matched: false,
|
|
48
|
+
},
|
|
49
|
+
{ pattern: "vite.config.", weight: 0.2, matched: false },
|
|
50
|
+
{ pattern: ".reactrc", weight: 0.15, matched: false },
|
|
51
|
+
{ pattern: "package.json", weight: 0.15, matched: false },
|
|
52
|
+
{ pattern: "tsx", weight: 0.2, matched: false },
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"nextjs",
|
|
57
|
+
[
|
|
58
|
+
{ pattern: "next.config.", weight: 0.3, matched: false },
|
|
59
|
+
{ pattern: "package.json", weight: 0.15, matched: false },
|
|
60
|
+
{ pattern: "pages/", weight: 0.2, matched: false },
|
|
61
|
+
{ pattern: "app/", weight: 0.25, matched: false },
|
|
62
|
+
{ pattern: '"next"', weight: 0.1, matched: false },
|
|
63
|
+
],
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"react_vite",
|
|
67
|
+
[
|
|
68
|
+
{ pattern: "vite.config.", weight: 0.3, matched: false },
|
|
69
|
+
{ pattern: "package.json", weight: 0.15, matched: false },
|
|
70
|
+
{ pattern: "src/main.tsx", weight: 0.25, matched: false },
|
|
71
|
+
{ pattern: "index.html", weight: 0.15, matched: false },
|
|
72
|
+
{ pattern: '"vite"', weight: 0.15, matched: false },
|
|
73
|
+
],
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"django",
|
|
77
|
+
[
|
|
78
|
+
{ pattern: "manage.py", weight: 0.4, matched: false },
|
|
79
|
+
{ pattern: "settings.py", weight: 0.25, matched: false },
|
|
80
|
+
{ pattern: "wsgi.py", weight: 0.15, matched: false },
|
|
81
|
+
{ pattern: "migrations/", weight: 0.2, matched: false },
|
|
82
|
+
],
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"laravel",
|
|
86
|
+
[
|
|
87
|
+
{ pattern: "artisan", weight: 0.4, matched: false },
|
|
88
|
+
{ pattern: "database/seeders", weight: 0.25, matched: false },
|
|
89
|
+
{ pattern: "composer.json", weight: 0.15, matched: false },
|
|
90
|
+
{ pattern: "routes/", weight: 0.2, matched: false },
|
|
91
|
+
],
|
|
92
|
+
],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detect project type from a directory
|
|
97
|
+
*/
|
|
98
|
+
async detect(rootDir: string): Promise<ProjectDetection> {
|
|
99
|
+
const projectFiles = await this.scanDirectory(rootDir);
|
|
100
|
+
|
|
101
|
+
// Score each project type
|
|
102
|
+
const scores: Record<ProjectType, number> = {
|
|
103
|
+
frappe_erpnext: 0,
|
|
104
|
+
frappe_spa: 0,
|
|
105
|
+
nextjs: 0,
|
|
106
|
+
react_vite: 0,
|
|
107
|
+
django: 0,
|
|
108
|
+
laravel: 0,
|
|
109
|
+
generic_web: 0,
|
|
110
|
+
unknown: 0,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const signals: string[] = [];
|
|
114
|
+
|
|
115
|
+
for (const [projectType, signalList] of this.signals) {
|
|
116
|
+
for (const signal of signalList) {
|
|
117
|
+
signal.matched = false;
|
|
118
|
+
for (const file of projectFiles) {
|
|
119
|
+
if (file.includes(signal.pattern)) {
|
|
120
|
+
signal.matched = true;
|
|
121
|
+
scores[projectType] += signal.weight;
|
|
122
|
+
if (!signals.includes(signal.pattern)) {
|
|
123
|
+
signals.push(signal.pattern);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Find the project type with highest score
|
|
132
|
+
let bestType: ProjectType = "unknown";
|
|
133
|
+
let bestScore = 0;
|
|
134
|
+
|
|
135
|
+
for (const [projectType, score] of Object.entries(scores)) {
|
|
136
|
+
if (score > bestScore) {
|
|
137
|
+
bestScore = score;
|
|
138
|
+
bestType = projectType as ProjectType;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for generic web
|
|
143
|
+
if (projectFiles.some((f) => f === "package.json")) {
|
|
144
|
+
if (bestType === "unknown") {
|
|
145
|
+
bestType = "generic_web";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Determine confidence based on score
|
|
150
|
+
const confidence = Math.min(1, bestScore);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
projectType: bestType,
|
|
154
|
+
confidence,
|
|
155
|
+
signals,
|
|
156
|
+
recommendedSeedStrategy: this.getSeedStrategy(bestType),
|
|
157
|
+
recommendedE2EStrategy: this.getE2EStrategy(bestType),
|
|
158
|
+
...this.getAdditionalInfo(bestType, projectFiles),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Scan directory for relevant files
|
|
164
|
+
*/
|
|
165
|
+
private async scanDirectory(rootDir: string): Promise<string[]> {
|
|
166
|
+
// This is a simplified implementation.
|
|
167
|
+
// In practice, you'd use recursive directory scanning.
|
|
168
|
+
const files: string[] = [];
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Check for common files
|
|
172
|
+
const checks = [
|
|
173
|
+
"frappe-bench",
|
|
174
|
+
"frappe-bench/sites",
|
|
175
|
+
"frappe-bench/apps",
|
|
176
|
+
"hooks.py",
|
|
177
|
+
"doctype",
|
|
178
|
+
"bench",
|
|
179
|
+
"vite.config.ts",
|
|
180
|
+
"vite.config.js",
|
|
181
|
+
"next.config.js",
|
|
182
|
+
"next.config.ts",
|
|
183
|
+
"package.json",
|
|
184
|
+
"pages",
|
|
185
|
+
"app",
|
|
186
|
+
"src/main.tsx",
|
|
187
|
+
"index.html",
|
|
188
|
+
"manage.py",
|
|
189
|
+
"settings.py",
|
|
190
|
+
"artisan",
|
|
191
|
+
"database/seeders",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
// @ts-expect-error - Bun has built-in file system access
|
|
195
|
+
const { existsSync } = await import("node:fs");
|
|
196
|
+
// @ts-expect-error - Bun has built-in file system access
|
|
197
|
+
const { readdirSync } = await import("node:fs");
|
|
198
|
+
|
|
199
|
+
for (const check of checks) {
|
|
200
|
+
const fullPath = join(rootDir, check);
|
|
201
|
+
if (existsSync(fullPath)) {
|
|
202
|
+
files.push(check);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Scan root directory for common files
|
|
207
|
+
try {
|
|
208
|
+
const rootFiles = readdirSync(rootDir);
|
|
209
|
+
for (const file of rootFiles) {
|
|
210
|
+
if (file.includes("package.json")) files.push(file);
|
|
211
|
+
if (file.includes("vite.config")) files.push(file);
|
|
212
|
+
if (file.includes("next.config")) files.push(file);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Ignore errors
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Return empty on error
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return files;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get recommended seed strategy
|
|
226
|
+
*/
|
|
227
|
+
private getSeedStrategy(projectType: ProjectType): SeedStrategy {
|
|
228
|
+
const strategies: Record<ProjectType, SeedStrategy> = {
|
|
229
|
+
frappe_erpnext: "frappe_doc_insert",
|
|
230
|
+
frappe_spa: "frappe_site_seed",
|
|
231
|
+
nextjs: "nextjs_factory",
|
|
232
|
+
react_vite: "react_factory",
|
|
233
|
+
django: "django_fixture",
|
|
234
|
+
laravel: "laravel_factory",
|
|
235
|
+
generic_web: "generic_sql",
|
|
236
|
+
unknown: "generic_sql",
|
|
237
|
+
};
|
|
238
|
+
return strategies[projectType];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get recommended E2E strategy
|
|
243
|
+
*/
|
|
244
|
+
private getE2EStrategy(projectType: ProjectType): E2EStrategy {
|
|
245
|
+
const strategies: Record<ProjectType, E2EStrategy> = {
|
|
246
|
+
frappe_erpnext: "bench_site_browser_flow",
|
|
247
|
+
frappe_spa: "bench_site_browser_flow",
|
|
248
|
+
nextjs: "next_dev_server_flow",
|
|
249
|
+
react_vite: "vite_dev_server_flow",
|
|
250
|
+
django: "django_test_client_flow",
|
|
251
|
+
laravel: "laravel_dusk_flow",
|
|
252
|
+
generic_web: "generic_playwright_flow",
|
|
253
|
+
unknown: "generic_playwright_flow",
|
|
254
|
+
};
|
|
255
|
+
return strategies[projectType];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get additional project info
|
|
260
|
+
*/
|
|
261
|
+
private getAdditionalInfo(
|
|
262
|
+
projectType: ProjectType,
|
|
263
|
+
files: string[],
|
|
264
|
+
): { framework?: string; version?: string } {
|
|
265
|
+
const info: { framework?: string; version?: string } = {};
|
|
266
|
+
|
|
267
|
+
// Try to extract version from package.json (if rootDir is available)
|
|
268
|
+
if (files.includes("package.json")) {
|
|
269
|
+
try {
|
|
270
|
+
const pkgPath = join(".", "package.json");
|
|
271
|
+
const pkg = readJson(pkgPath) as {
|
|
272
|
+
version?: string;
|
|
273
|
+
dependencies?: Record<string, string>;
|
|
274
|
+
} | null;
|
|
275
|
+
if (pkg?.version) {
|
|
276
|
+
info.version = pkg.version;
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Set framework name
|
|
284
|
+
const frameworks: Partial<Record<ProjectType, string>> = {
|
|
285
|
+
frappe_erpnext: "Frappe/ERPNext",
|
|
286
|
+
frappe_spa: "Frappe SPA",
|
|
287
|
+
nextjs: "Next.js",
|
|
288
|
+
react_vite: "React + Vite",
|
|
289
|
+
django: "Django",
|
|
290
|
+
laravel: "Laravel",
|
|
291
|
+
generic_web: "Generic Web",
|
|
292
|
+
};
|
|
293
|
+
info.framework = frameworks[projectType];
|
|
294
|
+
|
|
295
|
+
return info;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate detection report
|
|
300
|
+
*/
|
|
301
|
+
generateReport(detection: ProjectDetection): string {
|
|
302
|
+
const lines = [
|
|
303
|
+
"Project Detection Report",
|
|
304
|
+
"=".repeat(40),
|
|
305
|
+
"",
|
|
306
|
+
`Type: ${detection.projectType}`,
|
|
307
|
+
`Framework: ${detection.framework ?? "Unknown"}`,
|
|
308
|
+
`Confidence: ${(detection.confidence * 100).toFixed(1)}%`,
|
|
309
|
+
"",
|
|
310
|
+
"Signals:",
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
for (const signal of detection.signals) {
|
|
314
|
+
lines.push(` - ${signal}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push("Recommendations:");
|
|
319
|
+
lines.push(` Seed Strategy: ${detection.recommendedSeedStrategy}`);
|
|
320
|
+
lines.push(` E2E Strategy: ${detection.recommendedE2EStrategy}`);
|
|
321
|
+
|
|
322
|
+
if (detection.version) {
|
|
323
|
+
lines.push(` Version: ${detection.version}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
}
|