ultra-dex 1.8.0 → 2.2.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 +165 -140
- package/assets/agents/0-orchestration/orchestrator.md +2 -2
- package/assets/docs/QUICK-REFERENCE.md +3 -3
- package/assets/docs/ROADMAP.md +5 -5
- package/assets/docs/WORKFLOW-DIAGRAMS.md +1 -1
- package/assets/templates/README.md +1 -1
- package/bin/ultra-dex.js +27 -1893
- package/lib/commands/agents.js +151 -0
- package/lib/commands/audit.js +135 -0
- package/lib/commands/banner.js +21 -0
- package/lib/commands/build.js +214 -0
- package/lib/commands/examples.js +34 -0
- package/lib/commands/fetch.js +186 -0
- package/lib/commands/generate.js +217 -0
- package/lib/commands/hooks.js +105 -0
- package/lib/commands/init.js +335 -0
- package/lib/commands/placeholders.js +11 -0
- package/lib/commands/review.js +287 -0
- package/lib/commands/serve.js +173 -0
- package/lib/commands/suggest.js +126 -0
- package/lib/commands/sync.js +35 -0
- package/lib/commands/validate.js +140 -0
- package/lib/commands/workflows.js +185 -0
- package/lib/config/paths.js +9 -0
- package/lib/config/urls.js +16 -0
- package/lib/providers/base.js +82 -0
- package/lib/providers/claude.js +177 -0
- package/lib/providers/gemini.js +170 -0
- package/lib/providers/index.js +93 -0
- package/lib/providers/openai.js +163 -0
- package/lib/templates/context.js +26 -0
- package/lib/templates/embedded.js +141 -0
- package/lib/templates/prompts/generate-plan.js +147 -0
- package/lib/templates/prompts/review-code.js +57 -0
- package/lib/templates/prompts/section-prompts.js +275 -0
- package/lib/templates/prompts/system-prompt.md +58 -0
- package/lib/templates/quick-start.js +43 -0
- package/lib/utils/build-helpers.js +257 -0
- package/lib/utils/fallback.js +38 -0
- package/lib/utils/files.js +26 -0
- package/lib/utils/network.js +18 -0
- package/lib/utils/output.js +20 -0
- package/lib/utils/parser.js +155 -0
- package/lib/utils/prompt-builder.js +93 -0
- package/lib/utils/review-helpers.js +334 -0
- package/lib/utils/sync.js +216 -0
- package/lib/utils/validation.js +34 -0
- package/package.json +17 -3
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const QUICK_START_TEMPLATE = `# {{PROJECT_NAME}} - Quick Start
|
|
2
|
+
|
|
3
|
+
## 1. Your Idea (2 sentences max)
|
|
4
|
+
|
|
5
|
+
**What:** {{IDEA_WHAT}}
|
|
6
|
+
**For whom:** {{IDEA_FOR}}
|
|
7
|
+
|
|
8
|
+
## 2. The Problem (3 bullets)
|
|
9
|
+
|
|
10
|
+
- {{PROBLEM_1}}
|
|
11
|
+
- {{PROBLEM_2}}
|
|
12
|
+
- {{PROBLEM_3}}
|
|
13
|
+
|
|
14
|
+
## 3. Core Production Features (5 max)
|
|
15
|
+
|
|
16
|
+
| Feature | Priority | Justification |
|
|
17
|
+
|---------|----------|---------------|
|
|
18
|
+
| {{FEATURE_1}} | P0 | |
|
|
19
|
+
| | P0 | |
|
|
20
|
+
| | P1 | |
|
|
21
|
+
| | P1 | |
|
|
22
|
+
| | P2 | |
|
|
23
|
+
|
|
24
|
+
## 4. Tech Stack
|
|
25
|
+
|
|
26
|
+
| Layer | Your Choice |
|
|
27
|
+
|-------|-------------|
|
|
28
|
+
| Frontend | {{FRONTEND}} |
|
|
29
|
+
| Database | {{DATABASE}} |
|
|
30
|
+
| Auth | {{AUTH}} |
|
|
31
|
+
| Payments | {{PAYMENTS}} |
|
|
32
|
+
| Hosting | {{HOSTING}} |
|
|
33
|
+
|
|
34
|
+
## 5. First 3 Tasks
|
|
35
|
+
|
|
36
|
+
1. [ ] Set up project with chosen stack
|
|
37
|
+
2. [ ] Implement core feature #1
|
|
38
|
+
3. [ ] Deploy to staging
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
**Next:** Fill out the full implementation plan using the Ultra-Dex template.
|
|
43
|
+
`;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Command Utilities
|
|
3
|
+
* Helpers for the AI-assisted build workflow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load implementation plan from current directory
|
|
11
|
+
* @param {string} dir - Directory to search
|
|
12
|
+
* @returns {Promise<{content: string, path: string}|null>}
|
|
13
|
+
*/
|
|
14
|
+
export async function loadImplementationPlan(dir = '.') {
|
|
15
|
+
const possibleNames = [
|
|
16
|
+
'IMPLEMENTATION-PLAN.md',
|
|
17
|
+
'implementation-plan.md',
|
|
18
|
+
'PLAN.md',
|
|
19
|
+
'plan.md',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const name of possibleNames) {
|
|
23
|
+
const filePath = path.join(dir, name);
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
return { content, path: filePath };
|
|
27
|
+
} catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load context file
|
|
36
|
+
* @param {string} dir - Directory to search
|
|
37
|
+
* @returns {Promise<string|null>}
|
|
38
|
+
*/
|
|
39
|
+
export async function loadContext(dir = '.') {
|
|
40
|
+
const possibleNames = ['CONTEXT.md', 'context.md'];
|
|
41
|
+
|
|
42
|
+
for (const name of possibleNames) {
|
|
43
|
+
try {
|
|
44
|
+
return await fs.readFile(path.join(dir, name), 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract tasks from implementation plan
|
|
54
|
+
* @param {string} planContent - Implementation plan content
|
|
55
|
+
* @returns {Array<{id: string, title: string, section: number, status: string}>}
|
|
56
|
+
*/
|
|
57
|
+
export function extractTasks(planContent) {
|
|
58
|
+
const tasks = [];
|
|
59
|
+
|
|
60
|
+
// Match task patterns: - [ ] Task name or - [x] Task name
|
|
61
|
+
const taskRegex = /- \[([ x])\]\s*(.+?)(?=\n|$)/gi;
|
|
62
|
+
let match;
|
|
63
|
+
let currentSection = 0;
|
|
64
|
+
|
|
65
|
+
// Track current section
|
|
66
|
+
const lines = planContent.split('\n');
|
|
67
|
+
let lineIndex = 0;
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const sectionMatch = line.match(/## SECTION (\d+):/i);
|
|
71
|
+
if (sectionMatch) {
|
|
72
|
+
currentSection = parseInt(sectionMatch[1], 10);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const taskMatch = line.match(/- \[([ x])\]\s*(.+)/i);
|
|
76
|
+
if (taskMatch) {
|
|
77
|
+
tasks.push({
|
|
78
|
+
id: `task-${tasks.length + 1}`,
|
|
79
|
+
title: taskMatch[2].trim(),
|
|
80
|
+
section: currentSection,
|
|
81
|
+
status: taskMatch[1] === 'x' ? 'complete' : 'pending',
|
|
82
|
+
line: lineIndex,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
lineIndex++;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tasks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract atomic tasks (4-9 hour tasks) from Section 16
|
|
93
|
+
* @param {string} planContent - Implementation plan content
|
|
94
|
+
* @returns {Array<{id: string, title: string, estimate: string, dependencies: string[]}>}
|
|
95
|
+
*/
|
|
96
|
+
export function extractAtomicTasks(planContent) {
|
|
97
|
+
const tasks = [];
|
|
98
|
+
|
|
99
|
+
// Find Section 16 content
|
|
100
|
+
const section16Match = planContent.match(/## SECTION 16:.*?(?=## SECTION 17:|$)/is);
|
|
101
|
+
if (!section16Match) return tasks;
|
|
102
|
+
|
|
103
|
+
const section16 = section16Match[0];
|
|
104
|
+
|
|
105
|
+
// Match task patterns with estimates
|
|
106
|
+
// e.g., "1. Setup project structure (4h)" or "- [ ] Configure database (6-8h)"
|
|
107
|
+
const taskPatterns = [
|
|
108
|
+
/(?:^|\n)\d+\.\s*(.+?)\s*\((\d+(?:-\d+)?h?)\)/gi,
|
|
109
|
+
/- \[[ x]\]\s*(.+?)\s*\((\d+(?:-\d+)?h?)\)/gi,
|
|
110
|
+
/\|\s*(.+?)\s*\|\s*(\d+(?:-\d+)?\s*h(?:ours?)?)\s*\|/gi,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const pattern of taskPatterns) {
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = pattern.exec(section16)) !== null) {
|
|
116
|
+
tasks.push({
|
|
117
|
+
id: `atomic-${tasks.length + 1}`,
|
|
118
|
+
title: match[1].trim(),
|
|
119
|
+
estimate: match[2].trim(),
|
|
120
|
+
dependencies: [],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return tasks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get pending tasks only
|
|
130
|
+
* @param {Array} tasks - All tasks
|
|
131
|
+
* @returns {Array} Pending tasks
|
|
132
|
+
*/
|
|
133
|
+
export function getPendingTasks(tasks) {
|
|
134
|
+
return tasks.filter(t => t.status === 'pending');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Group tasks by section
|
|
139
|
+
* @param {Array} tasks - All tasks
|
|
140
|
+
* @returns {Map<number, Array>} Tasks grouped by section number
|
|
141
|
+
*/
|
|
142
|
+
export function groupTasksBySection(tasks) {
|
|
143
|
+
const grouped = new Map();
|
|
144
|
+
|
|
145
|
+
for (const task of tasks) {
|
|
146
|
+
if (!grouped.has(task.section)) {
|
|
147
|
+
grouped.set(task.section, []);
|
|
148
|
+
}
|
|
149
|
+
grouped.get(task.section).push(task);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return grouped;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format context for AI agent
|
|
157
|
+
* @param {Object} options
|
|
158
|
+
* @returns {string} Formatted context
|
|
159
|
+
*/
|
|
160
|
+
export function formatAgentContext({ plan, context, task, section }) {
|
|
161
|
+
let formatted = `# Project Context\n\n`;
|
|
162
|
+
|
|
163
|
+
if (context) {
|
|
164
|
+
formatted += `## Overview\n\n${context}\n\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (task) {
|
|
168
|
+
formatted += `## Current Task\n\n**${task.title}**\n`;
|
|
169
|
+
if (task.estimate) {
|
|
170
|
+
formatted += `- Estimated time: ${task.estimate}\n`;
|
|
171
|
+
}
|
|
172
|
+
if (task.section) {
|
|
173
|
+
formatted += `- From Section ${task.section}\n`;
|
|
174
|
+
}
|
|
175
|
+
formatted += '\n';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (section && plan) {
|
|
179
|
+
const sectionContent = extractSection(plan, section);
|
|
180
|
+
if (sectionContent) {
|
|
181
|
+
formatted += `## Relevant Section\n\n${sectionContent}\n\n`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
formatted += `## Instructions\n\n`;
|
|
186
|
+
formatted += `1. Focus on completing the current task\n`;
|
|
187
|
+
formatted += `2. Follow the implementation plan specifications\n`;
|
|
188
|
+
formatted += `3. Write production-ready code with tests\n`;
|
|
189
|
+
formatted += `4. Document any deviations from the plan\n`;
|
|
190
|
+
|
|
191
|
+
return formatted;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract a specific section from the plan
|
|
196
|
+
* @param {string} planContent - Full plan content
|
|
197
|
+
* @param {number} sectionNum - Section number (1-34)
|
|
198
|
+
* @returns {string|null}
|
|
199
|
+
*/
|
|
200
|
+
export function extractSection(planContent, sectionNum) {
|
|
201
|
+
const nextSection = sectionNum < 34 ? `## SECTION ${sectionNum + 1}:` : '═══════════════';
|
|
202
|
+
const regex = new RegExp(`(## SECTION ${sectionNum}:.*?)(?=${nextSection}|$)`, 'is');
|
|
203
|
+
const match = planContent.match(regex);
|
|
204
|
+
return match ? match[1].trim() : null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get section titles
|
|
209
|
+
*/
|
|
210
|
+
export const SECTION_TITLES = {
|
|
211
|
+
1: 'High-Level Summary',
|
|
212
|
+
2: 'Core Features',
|
|
213
|
+
3: 'User Stories',
|
|
214
|
+
4: 'User Personas',
|
|
215
|
+
5: 'Competitive Analysis',
|
|
216
|
+
6: 'Screen Map',
|
|
217
|
+
7: 'Wireframes',
|
|
218
|
+
8: 'Design System',
|
|
219
|
+
9: 'UI/UX Specifications',
|
|
220
|
+
10: 'Data Model',
|
|
221
|
+
11: 'API Blueprint',
|
|
222
|
+
12: 'System Architecture',
|
|
223
|
+
13: 'Authentication & Authorization',
|
|
224
|
+
14: 'Payment Integration',
|
|
225
|
+
15: 'Tech Stack',
|
|
226
|
+
16: 'Implementation Plan',
|
|
227
|
+
17: 'Timeline',
|
|
228
|
+
18: 'Risk Assessment',
|
|
229
|
+
19: 'Deployment Plan',
|
|
230
|
+
20: 'Testing Strategy',
|
|
231
|
+
21: 'Security Guidelines',
|
|
232
|
+
22: 'Performance Requirements',
|
|
233
|
+
23: 'Monitoring & Logging',
|
|
234
|
+
24: 'Maintenance Plan',
|
|
235
|
+
25: 'Documentation',
|
|
236
|
+
26: 'Analytics',
|
|
237
|
+
27: 'Error Handling',
|
|
238
|
+
28: 'Legal & Compliance',
|
|
239
|
+
29: 'SEO',
|
|
240
|
+
30: 'Internationalization',
|
|
241
|
+
31: 'Accessibility',
|
|
242
|
+
32: 'Feature Flags',
|
|
243
|
+
33: 'AI/ML Integration',
|
|
244
|
+
34: 'Future Roadmap',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export default {
|
|
248
|
+
loadImplementationPlan,
|
|
249
|
+
loadContext,
|
|
250
|
+
extractTasks,
|
|
251
|
+
extractAtomicTasks,
|
|
252
|
+
getPendingTasks,
|
|
253
|
+
groupTasksBySection,
|
|
254
|
+
formatAgentContext,
|
|
255
|
+
extractSection,
|
|
256
|
+
SECTION_TITLES,
|
|
257
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function readWithFallback(primaryPath, fallbackPath, encoding = 'utf-8') {
|
|
4
|
+
try {
|
|
5
|
+
return await fs.readFile(primaryPath, encoding);
|
|
6
|
+
} catch (primaryError) {
|
|
7
|
+
if (!fallbackPath) {
|
|
8
|
+
throw primaryError;
|
|
9
|
+
}
|
|
10
|
+
return await fs.readFile(fallbackPath, encoding);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function copyWithFallback(primaryPath, fallbackPath, destinationPath) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.copyFile(primaryPath, destinationPath);
|
|
17
|
+
return 'primary';
|
|
18
|
+
} catch (primaryError) {
|
|
19
|
+
if (!fallbackPath) {
|
|
20
|
+
throw primaryError;
|
|
21
|
+
}
|
|
22
|
+
await fs.copyFile(fallbackPath, destinationPath);
|
|
23
|
+
return 'fallback';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listWithFallback(primaryPath, fallbackPath) {
|
|
28
|
+
try {
|
|
29
|
+
const files = await fs.readdir(primaryPath);
|
|
30
|
+
return { files, sourcePath: primaryPath };
|
|
31
|
+
} catch (primaryError) {
|
|
32
|
+
if (!fallbackPath) {
|
|
33
|
+
throw primaryError;
|
|
34
|
+
}
|
|
35
|
+
const files = await fs.readdir(fallbackPath);
|
|
36
|
+
return { files, sourcePath: fallbackPath };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function readFileSafe(filePath, label = '') {
|
|
5
|
+
try {
|
|
6
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
7
|
+
return { label, content };
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return { label, content: '' };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function pathExists(targetPath, type = 'file') {
|
|
14
|
+
try {
|
|
15
|
+
const stats = await fs.stat(targetPath);
|
|
16
|
+
if (type === 'file') return stats.isFile();
|
|
17
|
+
if (type === 'dir') return stats.isDirectory();
|
|
18
|
+
return false;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveAssetPath(basePath, relativePath) {
|
|
25
|
+
return path.join(basePath, relativePath);
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function fetchWithRetry(url, options = {}, retries = 2, delayMs = 400) {
|
|
2
|
+
let lastError;
|
|
3
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await fetch(url, options);
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw new Error(`HTTP ${response.status}`);
|
|
8
|
+
}
|
|
9
|
+
return response;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
lastError = err;
|
|
12
|
+
if (attempt < retries) {
|
|
13
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
throw lastError;
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function printError(message, err) {
|
|
4
|
+
console.log(chalk.red(message));
|
|
5
|
+
if (err?.message) {
|
|
6
|
+
console.log(chalk.gray(` → ${err.message}`));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function printWarning(message) {
|
|
11
|
+
console.log(chalk.yellow(message));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function printInfo(message) {
|
|
15
|
+
console.log(chalk.cyan(message));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function printSuccess(message) {
|
|
19
|
+
console.log(chalk.green(message));
|
|
20
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser Utility
|
|
3
|
+
* Parses AI responses into structured output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract project name from implementation plan
|
|
8
|
+
* @param {string} content - Generated implementation plan
|
|
9
|
+
* @returns {string} Project name
|
|
10
|
+
*/
|
|
11
|
+
export function extractProjectName(content) {
|
|
12
|
+
// Try to find PROJECT: line
|
|
13
|
+
const projectMatch = content.match(/PROJECT:\s*(.+)/i);
|
|
14
|
+
if (projectMatch) {
|
|
15
|
+
return projectMatch[1].trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Try to find product name in Section 1
|
|
19
|
+
const productMatch = content.match(/Product Vision.*?:\s*(.+?)(?:\.|$)/im);
|
|
20
|
+
if (productMatch) {
|
|
21
|
+
return productMatch[1].trim().slice(0, 50);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 'My SaaS Project';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract summary from implementation plan
|
|
29
|
+
* @param {string} content - Generated implementation plan
|
|
30
|
+
* @returns {string} Brief summary
|
|
31
|
+
*/
|
|
32
|
+
export function extractSummary(content) {
|
|
33
|
+
// Find Section 1.1 or Problem Statement
|
|
34
|
+
const visionMatch = content.match(/### 1\.1 Product Vision.*?\n(.+?)(?=\n###|\n##|$)/is);
|
|
35
|
+
if (visionMatch) {
|
|
36
|
+
return visionMatch[1].trim().slice(0, 200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const problemMatch = content.match(/### 1\.2 Problem Statement.*?\n(.+?)(?=\n###|\n##|$)/is);
|
|
40
|
+
if (problemMatch) {
|
|
41
|
+
return problemMatch[1].trim().slice(0, 200);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'A SaaS application';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract tech stack from implementation plan
|
|
49
|
+
* @param {string} content - Generated implementation plan
|
|
50
|
+
* @returns {Object} Tech stack details
|
|
51
|
+
*/
|
|
52
|
+
export function extractTechStack(content) {
|
|
53
|
+
const defaults = {
|
|
54
|
+
frontend: 'Next.js + TypeScript',
|
|
55
|
+
backend: 'Next.js API Routes',
|
|
56
|
+
database: 'PostgreSQL + Prisma',
|
|
57
|
+
auth: 'NextAuth.js',
|
|
58
|
+
payments: 'Stripe',
|
|
59
|
+
hosting: 'Vercel',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Try to find tech stack section
|
|
63
|
+
const stackSection = content.match(/## SECTION 15.*?(?=## SECTION 16|$)/is);
|
|
64
|
+
if (!stackSection) return defaults;
|
|
65
|
+
|
|
66
|
+
const text = stackSection[0];
|
|
67
|
+
|
|
68
|
+
// Extract each layer
|
|
69
|
+
const frontendMatch = text.match(/Frontend.*?:\s*(.+?)(?=\n|$)/i);
|
|
70
|
+
const backendMatch = text.match(/Backend.*?:\s*(.+?)(?=\n|$)/i);
|
|
71
|
+
const databaseMatch = text.match(/Database.*?:\s*(.+?)(?=\n|$)/i);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
frontend: frontendMatch?.[1]?.trim() || defaults.frontend,
|
|
75
|
+
backend: backendMatch?.[1]?.trim() || defaults.backend,
|
|
76
|
+
database: databaseMatch?.[1]?.trim() || defaults.database,
|
|
77
|
+
auth: defaults.auth,
|
|
78
|
+
payments: defaults.payments,
|
|
79
|
+
hosting: defaults.hosting,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate implementation plan completeness
|
|
85
|
+
* @param {string} content - Generated implementation plan
|
|
86
|
+
* @returns {{complete: boolean, missingSections: number[], percentage: number}}
|
|
87
|
+
*/
|
|
88
|
+
export function validateCompleteness(content) {
|
|
89
|
+
const missingSections = [];
|
|
90
|
+
|
|
91
|
+
for (let i = 1; i <= 34; i++) {
|
|
92
|
+
const regex = new RegExp(`## SECTION ${i}:`, 'i');
|
|
93
|
+
if (!regex.test(content)) {
|
|
94
|
+
missingSections.push(i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const percentage = Math.round(((34 - missingSections.length) / 34) * 100);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
complete: missingSections.length === 0,
|
|
102
|
+
missingSections,
|
|
103
|
+
percentage,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Split content into sections
|
|
109
|
+
* @param {string} content - Generated implementation plan
|
|
110
|
+
* @returns {Map<number, string>} Map of section number to content
|
|
111
|
+
*/
|
|
112
|
+
export function splitIntoSections(content) {
|
|
113
|
+
const sections = new Map();
|
|
114
|
+
|
|
115
|
+
for (let i = 1; i <= 34; i++) {
|
|
116
|
+
const nextSection = i < 34 ? `## SECTION ${i + 1}:` : '═══════════════';
|
|
117
|
+
const regex = new RegExp(`(## SECTION ${i}:.*?)(?=${nextSection}|$)`, 'is');
|
|
118
|
+
const match = content.match(regex);
|
|
119
|
+
|
|
120
|
+
if (match) {
|
|
121
|
+
sections.set(i, match[1].trim());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return sections;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format token usage for display
|
|
130
|
+
* @param {{inputTokens: number, outputTokens: number}} usage
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function formatUsage(usage) {
|
|
134
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
135
|
+
return `${total.toLocaleString()} tokens (${usage.inputTokens.toLocaleString()} in / ${usage.outputTokens.toLocaleString()} out)`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format cost for display
|
|
140
|
+
* @param {{input: number, output: number, total: number}} cost
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
export function formatCost(cost) {
|
|
144
|
+
return `$${cost.total.toFixed(4)} (input: $${cost.input.toFixed(4)}, output: $${cost.output.toFixed(4)})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default {
|
|
148
|
+
extractProjectName,
|
|
149
|
+
extractSummary,
|
|
150
|
+
extractTechStack,
|
|
151
|
+
validateCompleteness,
|
|
152
|
+
splitIntoSections,
|
|
153
|
+
formatUsage,
|
|
154
|
+
formatCost,
|
|
155
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Builder Utility
|
|
3
|
+
* Assembles prompts for the AI providers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { USER_PROMPT_TEMPLATE, QUICK_START_PROMPT, CONTEXT_PROMPT } from '../templates/prompts/section-prompts.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load the system prompt from file
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
export async function loadSystemPrompt() {
|
|
19
|
+
const promptPath = path.join(__dirname, '../templates/prompts/system-prompt.md');
|
|
20
|
+
try {
|
|
21
|
+
return await fs.readFile(promptPath, 'utf-8');
|
|
22
|
+
} catch {
|
|
23
|
+
// Fallback embedded prompt if file not found
|
|
24
|
+
return `You are an expert SaaS architect. Generate a comprehensive 34-section implementation plan.
|
|
25
|
+
Be specific, production-ready, and thorough. Include code examples, time estimates, and realistic constraints.`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the user prompt for implementation plan generation
|
|
31
|
+
* @param {string} idea - The user's SaaS idea
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function buildImplementationPrompt(idea) {
|
|
35
|
+
return USER_PROMPT_TEMPLATE.replace(/\{\{IDEA\}\}/g, idea);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the prompt for QUICK-START.md generation
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function buildQuickStartPrompt() {
|
|
43
|
+
return QUICK_START_PROMPT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the prompt for CONTEXT.md generation
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function buildContextPrompt() {
|
|
51
|
+
return CONTEXT_PROMPT;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Estimate token count for a string (rough approximation)
|
|
56
|
+
* @param {string} text - Text to estimate
|
|
57
|
+
* @returns {number} Estimated token count
|
|
58
|
+
*/
|
|
59
|
+
export function estimateTokens(text) {
|
|
60
|
+
// Rough approximation: ~4 characters per token
|
|
61
|
+
return Math.ceil(text.length / 4);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculate estimated cost for generation
|
|
66
|
+
* @param {Object} provider - AI provider instance
|
|
67
|
+
* @param {string} idea - User's idea
|
|
68
|
+
* @returns {{inputTokens: number, outputTokens: number, cost: Object}}
|
|
69
|
+
*/
|
|
70
|
+
export function estimateGenerationCost(provider, idea) {
|
|
71
|
+
// Estimate input: system prompt (~600 tokens) + user prompt (~2000 tokens) + idea
|
|
72
|
+
const inputTokens = 2600 + estimateTokens(idea);
|
|
73
|
+
|
|
74
|
+
// Estimate output: ~40,000 tokens for full 34-section plan
|
|
75
|
+
const outputTokens = 40000;
|
|
76
|
+
|
|
77
|
+
const cost = provider.estimateCost(inputTokens, outputTokens);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
inputTokens,
|
|
81
|
+
outputTokens,
|
|
82
|
+
cost,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
loadSystemPrompt,
|
|
88
|
+
buildImplementationPrompt,
|
|
89
|
+
buildQuickStartPrompt,
|
|
90
|
+
buildContextPrompt,
|
|
91
|
+
estimateTokens,
|
|
92
|
+
estimateGenerationCost,
|
|
93
|
+
};
|