wiggum-cli 0.2.2 → 0.2.5
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/dist/ai/enhancer.d.ts +44 -35
- package/dist/ai/enhancer.d.ts.map +1 -1
- package/dist/ai/enhancer.js +138 -91
- package/dist/ai/enhancer.js.map +1 -1
- package/dist/ai/index.d.ts +3 -2
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/prompts.d.ts +6 -2
- package/dist/ai/prompts.d.ts.map +1 -1
- package/dist/ai/prompts.js +74 -43
- package/dist/ai/prompts.js.map +1 -1
- package/dist/ai/providers.d.ts +4 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +16 -0
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/tools.d.ts +47 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +253 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/generator/templates.d.ts +8 -0
- package/dist/generator/templates.d.ts.map +1 -1
- package/dist/generator/templates.js +90 -4
- package/dist/generator/templates.js.map +1 -1
- package/dist/templates/guides/AGENTS.md.tmpl +39 -22
- package/package.json +3 -2
- package/src/ai/enhancer.ts +191 -139
- package/src/ai/index.ts +10 -5
- package/src/ai/prompts.ts +75 -43
- package/src/ai/providers.ts +18 -0
- package/src/ai/tools.ts +278 -0
- package/src/commands/init.ts +21 -1
- package/src/generator/templates.ts +139 -6
- package/src/templates/guides/AGENTS.md.tmpl +39 -22
package/src/ai/tools.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tools Module
|
|
3
|
+
* Defines tools the AI agent can use to explore the codebase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tool, zodSchema } from 'ai';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join, relative } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create tools for codebase exploration
|
|
14
|
+
*/
|
|
15
|
+
export function createExplorationTools(projectRoot: string) {
|
|
16
|
+
return {
|
|
17
|
+
/**
|
|
18
|
+
* Search code using ripgrep patterns
|
|
19
|
+
*/
|
|
20
|
+
searchCode: tool({
|
|
21
|
+
description: `Search the codebase using ripgrep. Use this to find:
|
|
22
|
+
- Function/class definitions: pattern="^(def|function|class) NAME", fileType="py"
|
|
23
|
+
- Symbol usage: pattern="\\bSymbolName\\b" (word boundaries critical!)
|
|
24
|
+
- Imports: pattern="^import.*module", fileType="ts"
|
|
25
|
+
- File patterns: pattern="pattern", glob="*.tsx"
|
|
26
|
+
|
|
27
|
+
Tips:
|
|
28
|
+
- Use fileType for filtering (py, js, ts, rust, go)
|
|
29
|
+
- Use \\b for word boundaries to avoid partial matches
|
|
30
|
+
- Use literal=true for exact strings (faster)
|
|
31
|
+
- Always use word boundaries when searching symbols`,
|
|
32
|
+
inputSchema: zodSchema(z.object({
|
|
33
|
+
pattern: z.string().describe('The regex pattern to search for'),
|
|
34
|
+
fileType: z.string().optional().describe('File type filter (py, js, ts, etc.)'),
|
|
35
|
+
glob: z.string().optional().describe('Glob pattern like "*.tsx" or "src/**/*.ts"'),
|
|
36
|
+
literal: z.boolean().optional().describe('Use literal search (faster for exact strings)'),
|
|
37
|
+
context: z.number().optional().describe('Lines of context (default 0)'),
|
|
38
|
+
maxResults: z.number().optional().describe('Max results to return (default 50)'),
|
|
39
|
+
})),
|
|
40
|
+
execute: async ({ pattern, fileType, glob, literal, context, maxResults }) => {
|
|
41
|
+
try {
|
|
42
|
+
const args: string[] = [];
|
|
43
|
+
|
|
44
|
+
if (literal) args.push('-F');
|
|
45
|
+
if (fileType) args.push('-t', fileType);
|
|
46
|
+
if (glob) args.push('-g', glob);
|
|
47
|
+
if (context) args.push('-C', String(context));
|
|
48
|
+
|
|
49
|
+
args.push('--max-count', String(maxResults || 50));
|
|
50
|
+
args.push('--no-heading');
|
|
51
|
+
args.push('--line-number');
|
|
52
|
+
args.push('--');
|
|
53
|
+
args.push(pattern);
|
|
54
|
+
args.push(projectRoot);
|
|
55
|
+
|
|
56
|
+
// Use spawnSync instead of execSync for safety (no shell injection)
|
|
57
|
+
const result = spawnSync('rg', args, {
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
maxBuffer: 1024 * 1024,
|
|
60
|
+
timeout: 30000,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return `Search error: ${result.error.message}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (result.status === 1) {
|
|
68
|
+
return 'No matches found';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.status !== 0) {
|
|
72
|
+
return `Search error: ${result.stderr || 'Unknown error'}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Convert absolute paths to relative
|
|
76
|
+
const lines = result.stdout.split('\n').map(line => {
|
|
77
|
+
if (line.startsWith(projectRoot)) {
|
|
78
|
+
return line.replace(projectRoot + '/', '');
|
|
79
|
+
}
|
|
80
|
+
return line;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return lines.slice(0, 100).join('\n');
|
|
84
|
+
} catch (error: unknown) {
|
|
85
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return `Search error: ${errMsg}`;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read a file's contents
|
|
93
|
+
*/
|
|
94
|
+
readFile: tool({
|
|
95
|
+
description: 'Read the contents of a file. Use relative paths from project root.',
|
|
96
|
+
inputSchema: zodSchema(z.object({
|
|
97
|
+
path: z.string().describe('Relative path to the file from project root'),
|
|
98
|
+
startLine: z.number().optional().describe('Start line (1-indexed)'),
|
|
99
|
+
endLine: z.number().optional().describe('End line (inclusive)'),
|
|
100
|
+
})),
|
|
101
|
+
execute: async ({ path: filePath, startLine, endLine }) => {
|
|
102
|
+
try {
|
|
103
|
+
// Prevent path traversal
|
|
104
|
+
const normalizedPath = filePath.replace(/\.\./g, '');
|
|
105
|
+
const fullPath = join(projectRoot, normalizedPath);
|
|
106
|
+
|
|
107
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
108
|
+
return 'Invalid path: cannot access files outside project';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!existsSync(fullPath)) {
|
|
112
|
+
return `File not found: ${filePath}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stat = statSync(fullPath);
|
|
116
|
+
if (stat.isDirectory()) {
|
|
117
|
+
return `Path is a directory, not a file: ${filePath}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (stat.size > 100000) {
|
|
121
|
+
return `File too large (${stat.size} bytes). Use startLine/endLine to read a portion.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
125
|
+
const lines = content.split('\n');
|
|
126
|
+
|
|
127
|
+
if (startLine || endLine) {
|
|
128
|
+
const start = (startLine || 1) - 1;
|
|
129
|
+
const end = endLine || lines.length;
|
|
130
|
+
return lines.slice(start, end).join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return content;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
136
|
+
return `Error reading file: ${errMsg}`;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* List directory contents
|
|
143
|
+
*/
|
|
144
|
+
listDirectory: tool({
|
|
145
|
+
description: 'List contents of a directory. Shows files and subdirectories.',
|
|
146
|
+
inputSchema: zodSchema(z.object({
|
|
147
|
+
path: z.string().describe('Relative path to the directory (use "." for project root)'),
|
|
148
|
+
recursive: z.boolean().optional().describe('List recursively (default false)'),
|
|
149
|
+
maxDepth: z.number().optional().describe('Max depth for recursive listing (default 2)'),
|
|
150
|
+
})),
|
|
151
|
+
execute: async ({ path: dirPath, recursive, maxDepth }) => {
|
|
152
|
+
try {
|
|
153
|
+
// Prevent path traversal
|
|
154
|
+
const normalizedPath = dirPath.replace(/\.\./g, '');
|
|
155
|
+
const fullPath = join(projectRoot, normalizedPath);
|
|
156
|
+
|
|
157
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
158
|
+
return 'Invalid path: cannot access directories outside project';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!existsSync(fullPath)) {
|
|
162
|
+
return `Directory not found: ${dirPath}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const stat = statSync(fullPath);
|
|
166
|
+
if (!stat.isDirectory()) {
|
|
167
|
+
return `Path is not a directory: ${dirPath}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const results: string[] = [];
|
|
171
|
+
const depth = maxDepth || 2;
|
|
172
|
+
|
|
173
|
+
function scanDir(dir: string, currentDepth: number): void {
|
|
174
|
+
if (currentDepth > depth) return;
|
|
175
|
+
|
|
176
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
// Skip common ignored directories
|
|
180
|
+
if (['node_modules', '.git', 'dist', 'build', '.next', '__pycache__'].includes(entry.name)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const relativePath = relative(projectRoot, join(dir, entry.name));
|
|
185
|
+
const prefix = entry.isDirectory() ? '📁 ' : '📄 ';
|
|
186
|
+
results.push(prefix + relativePath);
|
|
187
|
+
|
|
188
|
+
if (recursive && entry.isDirectory() && currentDepth < depth) {
|
|
189
|
+
scanDir(join(dir, entry.name), currentDepth + 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
scanDir(fullPath, 1);
|
|
195
|
+
return results.slice(0, 200).join('\n');
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
198
|
+
return `Error listing directory: ${errMsg}`;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get package.json info including scripts
|
|
205
|
+
*/
|
|
206
|
+
getPackageInfo: tool({
|
|
207
|
+
description: 'Get package.json contents including scripts, dependencies, and metadata.',
|
|
208
|
+
inputSchema: zodSchema(z.object({
|
|
209
|
+
field: z.string().optional().describe('Specific field to get (scripts, dependencies, etc.)'),
|
|
210
|
+
})),
|
|
211
|
+
execute: async ({ field }) => {
|
|
212
|
+
try {
|
|
213
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
214
|
+
|
|
215
|
+
if (!existsSync(pkgPath)) {
|
|
216
|
+
return 'No package.json found';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
220
|
+
|
|
221
|
+
if (field) {
|
|
222
|
+
return JSON.stringify(pkg[field], null, 2) || `Field "${field}" not found`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Return relevant parts
|
|
226
|
+
return JSON.stringify({
|
|
227
|
+
name: pkg.name,
|
|
228
|
+
scripts: pkg.scripts,
|
|
229
|
+
dependencies: Object.keys(pkg.dependencies || {}),
|
|
230
|
+
devDependencies: Object.keys(pkg.devDependencies || {}),
|
|
231
|
+
}, null, 2);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
234
|
+
return `Error reading package.json: ${errMsg}`;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Ripgrep skill reference for the AI agent
|
|
243
|
+
*/
|
|
244
|
+
export const RIPGREP_SKILL = `
|
|
245
|
+
## ripgrep Code Search Skill
|
|
246
|
+
|
|
247
|
+
### Essential Patterns
|
|
248
|
+
|
|
249
|
+
**Find definitions:**
|
|
250
|
+
- Python functions: pattern="^def \\w+\\(", fileType="py"
|
|
251
|
+
- JS/TS functions: pattern="^(export )?(function|const) \\w+", fileType="ts"
|
|
252
|
+
- Classes: pattern="^class \\w+", fileType="py"
|
|
253
|
+
|
|
254
|
+
**Find symbol usage (CRITICAL: use word boundaries):**
|
|
255
|
+
- Exact word: pattern="\\bSymbolName\\b"
|
|
256
|
+
- Literal string: pattern="exact.string", literal=true
|
|
257
|
+
|
|
258
|
+
**Find imports:**
|
|
259
|
+
- ES imports: pattern="^import.*from", fileType="ts"
|
|
260
|
+
- CommonJS: pattern="require\\(", fileType="js"
|
|
261
|
+
|
|
262
|
+
**File type options:**
|
|
263
|
+
- py (Python)
|
|
264
|
+
- js (JavaScript)
|
|
265
|
+
- ts (TypeScript)
|
|
266
|
+
- rust (Rust)
|
|
267
|
+
- go (Go)
|
|
268
|
+
|
|
269
|
+
**Performance tips:**
|
|
270
|
+
1. Always use fileType when possible
|
|
271
|
+
2. Use literal=true for exact strings
|
|
272
|
+
3. Use \\b for word boundaries (prevents partial matches)
|
|
273
|
+
4. Keep maxResults reasonable
|
|
274
|
+
|
|
275
|
+
**Word boundaries are critical:**
|
|
276
|
+
- WITHOUT: pattern="log" matches "logger", "blogger", "catalog"
|
|
277
|
+
- WITH: pattern="\\blog\\b" matches only "log"
|
|
278
|
+
`;
|
package/src/commands/init.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface InitOptions {
|
|
|
23
23
|
ai?: boolean;
|
|
24
24
|
provider?: AIProvider;
|
|
25
25
|
yes?: boolean;
|
|
26
|
+
/** Use agentic mode for deep codebase exploration */
|
|
27
|
+
agentic?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -175,13 +177,31 @@ export async function initCommand(options: InitOptions): Promise<void> {
|
|
|
175
177
|
const selectedModel = modelChoice as string;
|
|
176
178
|
const modelLabel = modelOptions.find(m => m.value === selectedModel)?.label || selectedModel;
|
|
177
179
|
|
|
180
|
+
// Ask about agentic mode (deep exploration)
|
|
181
|
+
let useAgentic = options.agentic;
|
|
182
|
+
if (useAgentic === undefined && !options.yes) {
|
|
183
|
+
const wantAgentic = await prompts.confirm({
|
|
184
|
+
message: 'Enable deep codebase exploration? (AI will search files and directories)',
|
|
185
|
+
initialValue: true,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (prompts.isCancel(wantAgentic)) {
|
|
189
|
+
logger.info('Initialization cancelled');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
useAgentic = wantAgentic;
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
console.log('');
|
|
179
|
-
|
|
197
|
+
const modeLabel = useAgentic ? 'agentic' : 'simple';
|
|
198
|
+
console.log(simpson.yellow(`─── AI Enhancement (${provider} / ${modelLabel} / ${modeLabel}) ───`));
|
|
180
199
|
|
|
181
200
|
const aiEnhancer = new AIEnhancer({
|
|
182
201
|
provider,
|
|
183
202
|
model: selectedModel,
|
|
184
203
|
verbose: true,
|
|
204
|
+
agentic: useAgentic,
|
|
185
205
|
});
|
|
186
206
|
|
|
187
207
|
spinner.start('Running AI analysis...');
|
|
@@ -39,7 +39,7 @@ export interface TemplateVariables {
|
|
|
39
39
|
stylingVersion: string;
|
|
40
40
|
stylingVariant: string;
|
|
41
41
|
|
|
42
|
-
// Commands (derived from package manager)
|
|
42
|
+
// Commands (derived from package manager or AI detected)
|
|
43
43
|
devCommand: string;
|
|
44
44
|
buildCommand: string;
|
|
45
45
|
testCommand: string;
|
|
@@ -50,6 +50,16 @@ export interface TemplateVariables {
|
|
|
50
50
|
// Paths
|
|
51
51
|
appDir: string;
|
|
52
52
|
|
|
53
|
+
// AI Analysis (optional - populated when AI enhancement is used)
|
|
54
|
+
aiEntryPoints: string;
|
|
55
|
+
aiKeyDirectories: string;
|
|
56
|
+
aiNamingConventions: string;
|
|
57
|
+
aiImplementationGuidelines: string;
|
|
58
|
+
aiMcpEssential: string;
|
|
59
|
+
aiMcpRecommended: string;
|
|
60
|
+
aiMissedTechnologies: string;
|
|
61
|
+
hasAiAnalysis: string;
|
|
62
|
+
|
|
53
63
|
// Custom variables
|
|
54
64
|
[key: string]: string;
|
|
55
65
|
}
|
|
@@ -93,6 +103,115 @@ function deriveCommands(packageManager: string): Pick<
|
|
|
93
103
|
};
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
/**
|
|
107
|
+
* AI analysis template variables
|
|
108
|
+
*/
|
|
109
|
+
type AiTemplateVars = Pick<TemplateVariables,
|
|
110
|
+
'hasAiAnalysis' | 'aiEntryPoints' | 'aiKeyDirectories' | 'aiNamingConventions' |
|
|
111
|
+
'aiImplementationGuidelines' | 'aiMcpEssential' | 'aiMcpRecommended' | 'aiMissedTechnologies'
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format AI analysis data for templates
|
|
116
|
+
*/
|
|
117
|
+
function formatAiAnalysisForTemplates(scanResult: ScanResult): AiTemplateVars {
|
|
118
|
+
// Check if this is an EnhancedScanResult with aiAnalysis
|
|
119
|
+
const aiAnalysis = (scanResult as { aiAnalysis?: {
|
|
120
|
+
projectContext?: {
|
|
121
|
+
entryPoints?: string[];
|
|
122
|
+
keyDirectories?: Record<string, string>;
|
|
123
|
+
namingConventions?: string;
|
|
124
|
+
};
|
|
125
|
+
commands?: Record<string, string>;
|
|
126
|
+
implementationGuidelines?: string[];
|
|
127
|
+
mcpServers?: {
|
|
128
|
+
essential?: string[];
|
|
129
|
+
recommended?: string[];
|
|
130
|
+
};
|
|
131
|
+
possibleMissedTechnologies?: string[];
|
|
132
|
+
} }).aiAnalysis;
|
|
133
|
+
|
|
134
|
+
if (!aiAnalysis) {
|
|
135
|
+
return {
|
|
136
|
+
hasAiAnalysis: '',
|
|
137
|
+
aiEntryPoints: '',
|
|
138
|
+
aiKeyDirectories: '',
|
|
139
|
+
aiNamingConventions: '',
|
|
140
|
+
aiImplementationGuidelines: '',
|
|
141
|
+
aiMcpEssential: '',
|
|
142
|
+
aiMcpRecommended: '',
|
|
143
|
+
aiMissedTechnologies: '',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Format entry points as bullet list
|
|
148
|
+
const entryPoints = aiAnalysis.projectContext?.entryPoints || [];
|
|
149
|
+
const aiEntryPoints = entryPoints.length > 0
|
|
150
|
+
? entryPoints.map(e => `- \`${e}\``).join('\n')
|
|
151
|
+
: '';
|
|
152
|
+
|
|
153
|
+
// Format key directories as table rows
|
|
154
|
+
const keyDirs = aiAnalysis.projectContext?.keyDirectories || {};
|
|
155
|
+
const aiKeyDirectories = Object.keys(keyDirs).length > 0
|
|
156
|
+
? Object.entries(keyDirs).map(([dir, purpose]) => `| \`${dir}/\` | ${purpose} |`).join('\n')
|
|
157
|
+
: '';
|
|
158
|
+
|
|
159
|
+
// Naming conventions
|
|
160
|
+
const aiNamingConventions = aiAnalysis.projectContext?.namingConventions || '';
|
|
161
|
+
|
|
162
|
+
// Implementation guidelines as bullet list
|
|
163
|
+
const guidelines = aiAnalysis.implementationGuidelines || [];
|
|
164
|
+
const aiImplementationGuidelines = guidelines.length > 0
|
|
165
|
+
? guidelines.map(g => `- ${g}`).join('\n')
|
|
166
|
+
: '';
|
|
167
|
+
|
|
168
|
+
// MCP servers
|
|
169
|
+
const aiMcpEssential = (aiAnalysis.mcpServers?.essential || []).join(', ');
|
|
170
|
+
const aiMcpRecommended = (aiAnalysis.mcpServers?.recommended || []).join(', ');
|
|
171
|
+
|
|
172
|
+
// Missed technologies
|
|
173
|
+
const aiMissedTechnologies = (aiAnalysis.possibleMissedTechnologies || []).join(', ');
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
hasAiAnalysis: 'true',
|
|
177
|
+
aiEntryPoints,
|
|
178
|
+
aiKeyDirectories,
|
|
179
|
+
aiNamingConventions,
|
|
180
|
+
aiImplementationGuidelines,
|
|
181
|
+
aiMcpEssential,
|
|
182
|
+
aiMcpRecommended,
|
|
183
|
+
aiMissedTechnologies,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract AI-detected commands or fall back to derived commands
|
|
189
|
+
*/
|
|
190
|
+
function getCommands(
|
|
191
|
+
scanResult: ScanResult,
|
|
192
|
+
packageManager: string
|
|
193
|
+
): Pick<TemplateVariables, 'devCommand' | 'buildCommand' | 'testCommand' | 'lintCommand' | 'typecheckCommand' | 'formatCommand'> {
|
|
194
|
+
// Check for AI-detected commands
|
|
195
|
+
const aiCommands = (scanResult as { aiAnalysis?: { commands?: Record<string, string> } }).aiAnalysis?.commands;
|
|
196
|
+
|
|
197
|
+
// Derive default commands from package manager
|
|
198
|
+
const derived = deriveCommands(packageManager);
|
|
199
|
+
|
|
200
|
+
if (!aiCommands) {
|
|
201
|
+
return derived;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Use AI-detected commands where available, fall back to derived
|
|
205
|
+
return {
|
|
206
|
+
devCommand: aiCommands.dev || derived.devCommand,
|
|
207
|
+
buildCommand: aiCommands.build || derived.buildCommand,
|
|
208
|
+
testCommand: aiCommands.test || derived.testCommand,
|
|
209
|
+
lintCommand: aiCommands.lint || derived.lintCommand,
|
|
210
|
+
typecheckCommand: aiCommands.typecheck || derived.typecheckCommand,
|
|
211
|
+
formatCommand: aiCommands.format || derived.formatCommand,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
96
215
|
/**
|
|
97
216
|
* Extract template variables from scan result
|
|
98
217
|
*/
|
|
@@ -122,11 +241,24 @@ export function extractVariables(scanResult: ScanResult, customVars: Record<stri
|
|
|
122
241
|
const stylingVersion = stack.styling?.version || DEFAULT_VARIABLES.stylingVersion!;
|
|
123
242
|
const stylingVariant = stack.styling?.variant || DEFAULT_VARIABLES.stylingVariant!;
|
|
124
243
|
|
|
125
|
-
//
|
|
126
|
-
const commands =
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
const
|
|
244
|
+
// Get commands (AI-detected or derived)
|
|
245
|
+
const commands = getCommands(scanResult, packageManager);
|
|
246
|
+
|
|
247
|
+
// Extract AI analysis data
|
|
248
|
+
const aiData = formatAiAnalysisForTemplates(scanResult);
|
|
249
|
+
|
|
250
|
+
// Determine app directory - use AI entry points if available
|
|
251
|
+
let appDir = 'src';
|
|
252
|
+
if (frameworkVariant === 'app-router' || framework.toLowerCase().includes('next')) {
|
|
253
|
+
appDir = 'app';
|
|
254
|
+
} else if (aiData.aiEntryPoints) {
|
|
255
|
+
// Try to extract common directory from entry points
|
|
256
|
+
const entryPoints = (scanResult as { aiAnalysis?: { projectContext?: { entryPoints?: string[] } } })
|
|
257
|
+
.aiAnalysis?.projectContext?.entryPoints || [];
|
|
258
|
+
if (entryPoints.length > 0 && entryPoints[0].startsWith('src/')) {
|
|
259
|
+
appDir = 'src';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
130
262
|
|
|
131
263
|
return {
|
|
132
264
|
projectName,
|
|
@@ -145,6 +277,7 @@ export function extractVariables(scanResult: ScanResult, customVars: Record<stri
|
|
|
145
277
|
stylingVariant,
|
|
146
278
|
appDir,
|
|
147
279
|
...commands,
|
|
280
|
+
...aiData,
|
|
148
281
|
...customVars,
|
|
149
282
|
};
|
|
150
283
|
}
|
|
@@ -3,7 +3,20 @@
|
|
|
3
3
|
## Stack
|
|
4
4
|
{{framework}}{{#if frameworkVersion}} {{frameworkVersion}}{{/if}}{{#if frameworkVariant}} ({{frameworkVariant}}){{/if}}, TypeScript, {{styling || css}}{{#if unitTest}}, {{unitTest}}{{/if}}{{#if e2eTest}}, {{e2eTest}}{{/if}}
|
|
5
5
|
|
|
6
|
+
{{#if aiEntryPoints}}## Entry Points
|
|
7
|
+
{{aiEntryPoints}}
|
|
8
|
+
|
|
9
|
+
{{/if}}
|
|
6
10
|
## Commands
|
|
11
|
+
{{#if hasAiAnalysis}}
|
|
12
|
+
| Action | Command |
|
|
13
|
+
|--------|---------|
|
|
14
|
+
| Dev | `{{devCommand}}` |
|
|
15
|
+
| Build | `{{buildCommand}}` |
|
|
16
|
+
| Test | `{{testCommand}}` |
|
|
17
|
+
| Lint | `{{lintCommand}}` |
|
|
18
|
+
| Typecheck | `{{typecheckCommand}}` |
|
|
19
|
+
{{else}}
|
|
7
20
|
All commands run from `{{appDir}}/` directory:
|
|
8
21
|
|
|
9
22
|
| Action | Command |
|
|
@@ -16,19 +29,20 @@ All commands run from `{{appDir}}/` directory:
|
|
|
16
29
|
| Typecheck | `cd {{appDir}} && {{typecheckCommand}}` |
|
|
17
30
|
{{#if unitTest}}| Test | `cd {{appDir}} && {{testCommand}}` |
|
|
18
31
|
| Test watch | `cd {{appDir}} && {{testCommand}} -- --watch` |{{/if}}
|
|
32
|
+
{{/if}}
|
|
19
33
|
|
|
20
34
|
## Validation (run before commit)
|
|
21
35
|
```bash
|
|
22
|
-
|
|
36
|
+
{{lintCommand}} && {{typecheckCommand}}{{#if unitTest}} && {{testCommand}}{{/if}} && {{buildCommand}}
|
|
23
37
|
```
|
|
24
38
|
|
|
25
|
-
##
|
|
26
|
-
```bash
|
|
27
|
-
cd {{appDir}} && {{packageManager}} audit # Check vulnerable dependencies
|
|
28
|
-
```
|
|
29
|
-
Then review changes against @.ralph/guides/SECURITY.md checklist.
|
|
30
|
-
Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
|
|
39
|
+
{{#if aiKeyDirectories}}## Project Structure
|
|
31
40
|
|
|
41
|
+
| Directory | Purpose |
|
|
42
|
+
|-----------|---------|
|
|
43
|
+
{{aiKeyDirectories}}
|
|
44
|
+
|
|
45
|
+
{{else}}
|
|
32
46
|
## Patterns
|
|
33
47
|
- Pages: `{{appDir}}/app/` - Next.js App Router
|
|
34
48
|
- Components: `{{appDir}}/components/` - see ui/, ai-elements/, survey-editor/
|
|
@@ -36,27 +50,30 @@ Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
|
|
|
36
50
|
- Store: `{{appDir}}/store/` - State management
|
|
37
51
|
- Server Actions: `{{appDir}}/app/actions.ts` and `{{appDir}}/app/actions/`
|
|
38
52
|
- Libs: `{{appDir}}/lib/` - utilities and integrations
|
|
53
|
+
{{/if}}
|
|
54
|
+
|
|
55
|
+
{{#if aiImplementationGuidelines}}## Implementation Guidelines
|
|
56
|
+
{{aiImplementationGuidelines}}
|
|
39
57
|
|
|
58
|
+
{{/if}}
|
|
40
59
|
## Search Tools
|
|
41
60
|
- **Codebase search**: Use `mgrep "query"` (preferred over Grep/Glob)
|
|
42
61
|
- **Documentation lookup**: Use Context7 MCP for library/framework docs
|
|
43
62
|
- **Web search**: Use `mgrep --web "query"` for general web lookups
|
|
44
63
|
|
|
45
64
|
## Code Rules
|
|
46
|
-
-
|
|
65
|
+
{{#if aiNamingConventions}}- Naming: {{aiNamingConventions}}
|
|
66
|
+
{{/if}}- Explicit return types on functions
|
|
47
67
|
- No `any` - use `unknown` if truly unknown
|
|
48
68
|
- Early returns over nested conditionals
|
|
49
69
|
- Named exports, not default exports
|
|
50
70
|
- Path alias `@/` maps to `{{appDir}}/` root
|
|
51
|
-
- Follow patterns in existing
|
|
71
|
+
- Follow patterns in existing code
|
|
52
72
|
- 2-space indentation, single quotes
|
|
53
|
-
- PascalCase for components, `use` prefix for hooks
|
|
54
|
-
- kebab-case for folder names (e.g., `survey-editor`)
|
|
55
73
|
|
|
56
74
|
{{#if unitTest}}## Testing
|
|
57
75
|
- Framework: {{unitTest}}{{#if unitTestVersion}} {{unitTestVersion}}{{/if}}
|
|
58
|
-
-
|
|
59
|
-
- Run: `cd {{appDir}} && {{testCommand}}`
|
|
76
|
+
- Run: `{{testCommand}}`
|
|
60
77
|
- Each feature task should include tests
|
|
61
78
|
- Validation fails if tests fail
|
|
62
79
|
{{/if}}
|
|
@@ -71,30 +88,30 @@ Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
|
|
|
71
88
|
- After changes: validate, commit, push
|
|
72
89
|
|
|
73
90
|
## MCP Servers Available
|
|
74
|
-
- **
|
|
91
|
+
{{#if aiMcpEssential}}- **Essential**: {{aiMcpEssential}}
|
|
92
|
+
{{/if}}{{#if aiMcpRecommended}}- **Recommended**: {{aiMcpRecommended}}
|
|
93
|
+
{{/if}}- **Supabase**: Query DB, manage migrations, generate types
|
|
75
94
|
- **PostHog**: Analytics, feature flags, experiments
|
|
76
95
|
- **Playwright**: Browser automation for E2E testing
|
|
77
96
|
- Navigate: `browser_navigate`, `browser_snapshot`
|
|
78
97
|
- Interact: `browser_click`, `browser_type`, `browser_fill_form`
|
|
79
98
|
- Verify: `browser_wait_for`, `browser_console_messages`
|
|
80
99
|
- Debug: `browser_take_screenshot`
|
|
81
|
-
- **Codex CLI** (shell command, not MCP): PR review
|
|
82
|
-
- Command: `echo "prompt" | codex exec --full-auto -`
|
|
83
|
-
- For reviews: `codex review --base main` (interactive mode)
|
|
84
|
-
- Location: `/opt/homebrew/bin/codex`
|
|
85
|
-
- Use for: code review, PR feedback before merge
|
|
86
100
|
- **Context7**: Documentation lookup for libraries/frameworks
|
|
87
101
|
|
|
102
|
+
{{#if aiMissedTechnologies}}## May Also Use
|
|
103
|
+
{{aiMissedTechnologies}}
|
|
104
|
+
|
|
105
|
+
{{/if}}
|
|
88
106
|
## Quick Triggers
|
|
89
107
|
| Keywords | Action |
|
|
90
108
|
|----------|--------|
|
|
91
109
|
| type error, ts | Run typecheck |
|
|
92
|
-
| lint, format | Run lint --fix
|
|
110
|
+
| lint, format | Run lint --fix |
|
|
93
111
|
| deploy, build | Run full build |
|
|
94
|
-
| error, debug | Check
|
|
112
|
+
| error, debug | Check logs or console |
|
|
95
113
|
| database, table | Use Supabase MCP |
|
|
96
114
|
| e2e, browser test | Use Playwright MCP |
|
|
97
115
|
|
|
98
116
|
## Detailed Docs
|
|
99
|
-
- Architecture: `{{appDir}}/.claude/CLAUDE.md` - comprehensive codebase details
|
|
100
117
|
- Frontend: `.ralph/guides/FRONTEND.md` - design patterns, component usage, quality checklist
|