@tonycasey/lisa 0.5.13
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 +42 -0
- package/dist/cli.js +390 -0
- package/dist/lib/interfaces/IDockerClient.js +2 -0
- package/dist/lib/interfaces/IMcpClient.js +2 -0
- package/dist/lib/interfaces/IServices.js +2 -0
- package/dist/lib/interfaces/ITemplateCopier.js +2 -0
- package/dist/lib/mcp.js +35 -0
- package/dist/lib/services.js +57 -0
- package/dist/package.json +36 -0
- package/dist/templates/agents/.sample.env +12 -0
- package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
- package/dist/templates/agents/skills/common/group-id.js +193 -0
- package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
- package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
- package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
- package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
- package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
- package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
- package/dist/templates/agents/skills/memory/SKILL.md +31 -0
- package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
- package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
- package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
- package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
- package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
- package/dist/templates/claude/config.js +40 -0
- package/dist/templates/claude/hooks/README.md +158 -0
- package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
- package/dist/templates/claude/hooks/common/context.js +263 -0
- package/dist/templates/claude/hooks/common/group-id.js +188 -0
- package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
- package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
- package/dist/templates/claude/hooks/common/zep-client.js +175 -0
- package/dist/templates/claude/hooks/session-start.js +401 -0
- package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
- package/dist/templates/claude/hooks/session-stop.js +122 -0
- package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
- package/dist/templates/claude/settings.json +46 -0
- package/dist/templates/docker/.env.lisa.example +17 -0
- package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
- package/dist/templates/rules/shared/clean-architecture.md +333 -0
- package/dist/templates/rules/shared/code-quality-rules.md +469 -0
- package/dist/templates/rules/shared/git-rules.md +64 -0
- package/dist/templates/rules/shared/testing-principles.md +469 -0
- package/dist/templates/rules/typescript/coding-standards.md +751 -0
- package/dist/templates/rules/typescript/testing.md +629 -0
- package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +710 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* init-review.ts - Codebase analysis for Lisa
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* node init-review.js run [--force] - Run static analysis + queue AI enrichment
|
|
8
|
+
* node init-review.js show - Show current init review from memory
|
|
9
|
+
* node init-review.js status - Check if init review is done
|
|
10
|
+
*
|
|
11
|
+
* This runs automatically during npm install via postinstall.js
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawn } = require('child_process');
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Group ID Utilities (inline to avoid import complexity)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const MAX_GROUP_ID_LENGTH = 128;
|
|
21
|
+
function normalizePathToGroupId(absolutePath) {
|
|
22
|
+
let normalized = absolutePath
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/^[a-z]:/i, (match) => match.charAt(0))
|
|
25
|
+
.replace(/^\//, '')
|
|
26
|
+
.replace(/\\/g, '-')
|
|
27
|
+
.replace(/\//g, '-')
|
|
28
|
+
.replace(/\./g, '_')
|
|
29
|
+
.replace(/^-+/, '')
|
|
30
|
+
.replace(/-+/g, '-');
|
|
31
|
+
if (normalized.length > MAX_GROUP_ID_LENGTH) {
|
|
32
|
+
normalized = normalized.slice(-MAX_GROUP_ID_LENGTH);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
function getCurrentGroupId(cwd = process.cwd()) {
|
|
37
|
+
return normalizePathToGroupId(cwd);
|
|
38
|
+
}
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Detection Utilities
|
|
41
|
+
// ============================================================================
|
|
42
|
+
const PROJECT_FILES = {
|
|
43
|
+
// High confidence - definite codebase
|
|
44
|
+
high: [
|
|
45
|
+
'package.json',
|
|
46
|
+
'pyproject.toml',
|
|
47
|
+
'setup.py',
|
|
48
|
+
'requirements.txt',
|
|
49
|
+
'Cargo.toml',
|
|
50
|
+
'go.mod',
|
|
51
|
+
'pom.xml',
|
|
52
|
+
'build.gradle',
|
|
53
|
+
'Gemfile',
|
|
54
|
+
'composer.json',
|
|
55
|
+
'Makefile',
|
|
56
|
+
'CMakeLists.txt',
|
|
57
|
+
],
|
|
58
|
+
// Medium confidence
|
|
59
|
+
medium: [
|
|
60
|
+
'.git',
|
|
61
|
+
'src',
|
|
62
|
+
'lib',
|
|
63
|
+
'app',
|
|
64
|
+
'README.md',
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
const LANGUAGE_EXTENSIONS = {
|
|
68
|
+
'.ts': 'TypeScript',
|
|
69
|
+
'.tsx': 'TypeScript',
|
|
70
|
+
'.js': 'JavaScript',
|
|
71
|
+
'.jsx': 'JavaScript',
|
|
72
|
+
'.py': 'Python',
|
|
73
|
+
'.go': 'Go',
|
|
74
|
+
'.rs': 'Rust',
|
|
75
|
+
'.java': 'Java',
|
|
76
|
+
'.kt': 'Kotlin',
|
|
77
|
+
'.rb': 'Ruby',
|
|
78
|
+
'.php': 'PHP',
|
|
79
|
+
'.cs': 'C#',
|
|
80
|
+
'.cpp': 'C++',
|
|
81
|
+
'.c': 'C',
|
|
82
|
+
'.swift': 'Swift',
|
|
83
|
+
};
|
|
84
|
+
const FRAMEWORK_INDICATORS = {
|
|
85
|
+
'React': { files: [], deps: ['react', 'react-dom'] },
|
|
86
|
+
'Next.js': { files: ['next.config.js', 'next.config.mjs'], deps: ['next'] },
|
|
87
|
+
'Vue': { files: ['vue.config.js'], deps: ['vue'] },
|
|
88
|
+
'Angular': { files: ['angular.json'], deps: ['@angular/core'] },
|
|
89
|
+
'Express': { files: [], deps: ['express'] },
|
|
90
|
+
'NestJS': { files: ['nest-cli.json'], deps: ['@nestjs/core'] },
|
|
91
|
+
'FastAPI': { files: [], deps: ['fastapi'] },
|
|
92
|
+
'Django': { files: ['manage.py'], deps: ['django'] },
|
|
93
|
+
'Flask': { files: [], deps: ['flask'] },
|
|
94
|
+
'Rails': { files: ['Gemfile'], deps: ['rails'] },
|
|
95
|
+
'Spring': { files: [], deps: ['spring-boot'] },
|
|
96
|
+
};
|
|
97
|
+
const ARCHITECTURE_INDICATORS = {
|
|
98
|
+
'clean-architecture': ['domain', 'application', 'infrastructure'],
|
|
99
|
+
'mvc': ['models', 'views', 'controllers'],
|
|
100
|
+
'hexagonal': ['adapters', 'ports', 'domain'],
|
|
101
|
+
'monorepo': ['packages', 'apps', 'libs'],
|
|
102
|
+
'microservices': ['services', 'gateway', 'shared'],
|
|
103
|
+
};
|
|
104
|
+
const TEST_DIRS = ['tests', 'test', '__tests__', 'spec', 'specs'];
|
|
105
|
+
const ENTRY_PATTERNS = ['index.ts', 'index.js', 'main.ts', 'main.js', 'app.ts', 'app.js', 'cli.ts', 'cli.js', 'main.py', 'app.py', '__main__.py'];
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Analysis Functions
|
|
108
|
+
// ============================================================================
|
|
109
|
+
function isCodebase(projectRoot) {
|
|
110
|
+
// Check high confidence files
|
|
111
|
+
for (const file of PROJECT_FILES.high) {
|
|
112
|
+
const filePath = path.join(projectRoot, file);
|
|
113
|
+
if (fs.existsSync(filePath)) {
|
|
114
|
+
return { isCodebase: true, confidence: 'high', reason: `Found ${file}` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Check medium confidence
|
|
118
|
+
let mediumCount = 0;
|
|
119
|
+
for (const file of PROJECT_FILES.medium) {
|
|
120
|
+
const filePath = path.join(projectRoot, file);
|
|
121
|
+
if (fs.existsSync(filePath)) {
|
|
122
|
+
mediumCount++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (mediumCount >= 2) {
|
|
126
|
+
return { isCodebase: true, confidence: 'medium', reason: `Found ${mediumCount} indicators` };
|
|
127
|
+
}
|
|
128
|
+
// Check for code files
|
|
129
|
+
try {
|
|
130
|
+
const files = fs.readdirSync(projectRoot);
|
|
131
|
+
const codeFiles = files.filter((f) => {
|
|
132
|
+
const ext = path.extname(f);
|
|
133
|
+
return LANGUAGE_EXTENSIONS[ext];
|
|
134
|
+
});
|
|
135
|
+
if (codeFiles.length >= 3) {
|
|
136
|
+
return { isCodebase: true, confidence: 'medium', reason: `Found ${codeFiles.length} code files` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (_) {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
return { isCodebase: false, confidence: 'low', reason: 'No codebase indicators found' };
|
|
143
|
+
}
|
|
144
|
+
function getProjectName(projectRoot) {
|
|
145
|
+
// Try package.json
|
|
146
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
147
|
+
if (fs.existsSync(pkgPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
150
|
+
if (pkg.name) {
|
|
151
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (_) {
|
|
155
|
+
// Ignore errors
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Try pyproject.toml
|
|
159
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
160
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
163
|
+
const match = content.match(/name\s*=\s*"([^"]+)"/);
|
|
164
|
+
if (match)
|
|
165
|
+
return match[1];
|
|
166
|
+
}
|
|
167
|
+
catch (_) {
|
|
168
|
+
// Ignore errors
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Fall back to directory name
|
|
172
|
+
return path.basename(projectRoot);
|
|
173
|
+
}
|
|
174
|
+
function scanDirectory(dir, maxDepth = 4, currentDepth = 0) {
|
|
175
|
+
const files = [];
|
|
176
|
+
const dirs = [];
|
|
177
|
+
if (currentDepth >= maxDepth)
|
|
178
|
+
return { files, dirs };
|
|
179
|
+
try {
|
|
180
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
// Skip common non-source directories and lisa scaffolding
|
|
183
|
+
if (['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv', 'target', '.next', '.agents', '.claude', '.dev'].includes(entry.name)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const fullPath = path.join(dir, entry.name);
|
|
187
|
+
const relativePath = path.relative(process.cwd(), fullPath);
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
dirs.push(relativePath);
|
|
190
|
+
const sub = scanDirectory(fullPath, maxDepth, currentDepth + 1);
|
|
191
|
+
files.push(...sub.files);
|
|
192
|
+
dirs.push(...sub.dirs);
|
|
193
|
+
}
|
|
194
|
+
else if (entry.isFile()) {
|
|
195
|
+
files.push(relativePath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (_) {
|
|
200
|
+
// ignore permission errors
|
|
201
|
+
}
|
|
202
|
+
return { files, dirs };
|
|
203
|
+
}
|
|
204
|
+
function detectLanguages(files) {
|
|
205
|
+
const langCounts = {};
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const ext = path.extname(file);
|
|
208
|
+
const lang = LANGUAGE_EXTENSIONS[ext];
|
|
209
|
+
if (lang) {
|
|
210
|
+
langCounts[lang] = (langCounts[lang] || 0) + 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Sort by count
|
|
214
|
+
return Object.entries(langCounts)
|
|
215
|
+
.sort((a, b) => b[1] - a[1])
|
|
216
|
+
.map(([lang]) => lang);
|
|
217
|
+
}
|
|
218
|
+
function detectFrameworks(projectRoot, deps) {
|
|
219
|
+
const frameworks = [];
|
|
220
|
+
for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
|
|
221
|
+
// Check files
|
|
222
|
+
for (const file of indicators.files) {
|
|
223
|
+
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
224
|
+
frameworks.push(framework);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Check dependencies
|
|
229
|
+
if (!frameworks.includes(framework)) {
|
|
230
|
+
for (const dep of indicators.deps) {
|
|
231
|
+
if (deps.includes(dep)) {
|
|
232
|
+
frameworks.push(framework);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return frameworks;
|
|
239
|
+
}
|
|
240
|
+
function detectArchitecture(dirs) {
|
|
241
|
+
const dirNames = dirs.map(d => path.basename(d).toLowerCase());
|
|
242
|
+
for (const [arch, indicators] of Object.entries(ARCHITECTURE_INDICATORS)) {
|
|
243
|
+
const matches = indicators.filter(ind => dirNames.includes(ind));
|
|
244
|
+
if (matches.length >= 2) {
|
|
245
|
+
return arch;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function detectBuildTools(projectRoot) {
|
|
251
|
+
const tools = [];
|
|
252
|
+
if (fs.existsSync(path.join(projectRoot, 'package.json')))
|
|
253
|
+
tools.push('npm');
|
|
254
|
+
if (fs.existsSync(path.join(projectRoot, 'yarn.lock')))
|
|
255
|
+
tools.push('yarn');
|
|
256
|
+
if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml')))
|
|
257
|
+
tools.push('pnpm');
|
|
258
|
+
if (fs.existsSync(path.join(projectRoot, 'tsconfig.json')))
|
|
259
|
+
tools.push('tsc');
|
|
260
|
+
if (fs.existsSync(path.join(projectRoot, 'webpack.config.js')))
|
|
261
|
+
tools.push('webpack');
|
|
262
|
+
if (fs.existsSync(path.join(projectRoot, 'vite.config.js')) || fs.existsSync(path.join(projectRoot, 'vite.config.ts')))
|
|
263
|
+
tools.push('vite');
|
|
264
|
+
if (fs.existsSync(path.join(projectRoot, 'rollup.config.js')))
|
|
265
|
+
tools.push('rollup');
|
|
266
|
+
if (fs.existsSync(path.join(projectRoot, 'Makefile')))
|
|
267
|
+
tools.push('make');
|
|
268
|
+
if (fs.existsSync(path.join(projectRoot, 'Cargo.toml')))
|
|
269
|
+
tools.push('cargo');
|
|
270
|
+
if (fs.existsSync(path.join(projectRoot, 'go.mod')))
|
|
271
|
+
tools.push('go');
|
|
272
|
+
if (fs.existsSync(path.join(projectRoot, 'pyproject.toml')))
|
|
273
|
+
tools.push('poetry');
|
|
274
|
+
if (fs.existsSync(path.join(projectRoot, 'requirements.txt')))
|
|
275
|
+
tools.push('pip');
|
|
276
|
+
return tools;
|
|
277
|
+
}
|
|
278
|
+
function getDependencies(projectRoot) {
|
|
279
|
+
const production = [];
|
|
280
|
+
const dev = [];
|
|
281
|
+
// Try package.json
|
|
282
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
283
|
+
if (fs.existsSync(pkgPath)) {
|
|
284
|
+
try {
|
|
285
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
286
|
+
if (pkg.dependencies) {
|
|
287
|
+
production.push(...Object.keys(pkg.dependencies));
|
|
288
|
+
}
|
|
289
|
+
if (pkg.devDependencies) {
|
|
290
|
+
dev.push(...Object.keys(pkg.devDependencies));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (_) {
|
|
294
|
+
// Ignore errors
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Try requirements.txt
|
|
298
|
+
const reqPath = path.join(projectRoot, 'requirements.txt');
|
|
299
|
+
if (fs.existsSync(reqPath)) {
|
|
300
|
+
try {
|
|
301
|
+
const content = fs.readFileSync(reqPath, 'utf8');
|
|
302
|
+
const deps = content.split('\n')
|
|
303
|
+
.map((l) => l.split('==')[0].split('>=')[0].trim())
|
|
304
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
305
|
+
production.push(...deps);
|
|
306
|
+
}
|
|
307
|
+
catch (_) {
|
|
308
|
+
// Ignore errors
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { production, dev, all: [...production, ...dev] };
|
|
312
|
+
}
|
|
313
|
+
function findEntryPoints(projectRoot, _files) {
|
|
314
|
+
const entryPoints = [];
|
|
315
|
+
// Check common entry point patterns
|
|
316
|
+
for (const pattern of ENTRY_PATTERNS) {
|
|
317
|
+
// Check root
|
|
318
|
+
if (fs.existsSync(path.join(projectRoot, pattern))) {
|
|
319
|
+
entryPoints.push(pattern);
|
|
320
|
+
}
|
|
321
|
+
// Check src/
|
|
322
|
+
if (fs.existsSync(path.join(projectRoot, 'src', pattern))) {
|
|
323
|
+
entryPoints.push(`src/${pattern}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Check package.json main/bin
|
|
327
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
328
|
+
if (fs.existsSync(pkgPath)) {
|
|
329
|
+
try {
|
|
330
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
331
|
+
if (pkg.main && !entryPoints.includes(pkg.main)) {
|
|
332
|
+
entryPoints.push(pkg.main);
|
|
333
|
+
}
|
|
334
|
+
if (pkg.bin) {
|
|
335
|
+
const bins = typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin);
|
|
336
|
+
for (const bin of bins) {
|
|
337
|
+
if (!entryPoints.includes(bin)) {
|
|
338
|
+
entryPoints.push(bin);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (_) {
|
|
344
|
+
// Ignore errors
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return entryPoints.slice(0, 5); // Limit to 5
|
|
348
|
+
}
|
|
349
|
+
function findMainModules(dirs) {
|
|
350
|
+
const mainDirs = ['src', 'lib', 'app', 'domain', 'application', 'infrastructure', 'services', 'components', 'utils', 'core'];
|
|
351
|
+
return dirs
|
|
352
|
+
.filter(d => mainDirs.includes(path.basename(d)))
|
|
353
|
+
.slice(0, 10);
|
|
354
|
+
}
|
|
355
|
+
function findTestDirs(dirs) {
|
|
356
|
+
return dirs
|
|
357
|
+
.filter(d => TEST_DIRS.includes(path.basename(d)))
|
|
358
|
+
.slice(0, 5);
|
|
359
|
+
}
|
|
360
|
+
function findConfigFiles(projectRoot) {
|
|
361
|
+
const configs = [
|
|
362
|
+
'tsconfig.json', '.eslintrc.js', '.eslintrc.json', '.prettierrc',
|
|
363
|
+
'jest.config.js', 'vitest.config.ts', '.env.example',
|
|
364
|
+
'docker-compose.yml', 'Dockerfile', '.github/workflows'
|
|
365
|
+
];
|
|
366
|
+
return configs.filter(c => fs.existsSync(path.join(projectRoot, c)));
|
|
367
|
+
}
|
|
368
|
+
function detectTesting(projectRoot, deps) {
|
|
369
|
+
if (deps.includes('jest'))
|
|
370
|
+
return 'jest';
|
|
371
|
+
if (deps.includes('vitest'))
|
|
372
|
+
return 'vitest';
|
|
373
|
+
if (deps.includes('mocha'))
|
|
374
|
+
return 'mocha';
|
|
375
|
+
if (deps.includes('pytest'))
|
|
376
|
+
return 'pytest';
|
|
377
|
+
if (fs.existsSync(path.join(projectRoot, 'jest.config.js')))
|
|
378
|
+
return 'jest';
|
|
379
|
+
if (fs.existsSync(path.join(projectRoot, 'vitest.config.ts')))
|
|
380
|
+
return 'vitest';
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
function detectFormatting(deps) {
|
|
384
|
+
if (deps.includes('prettier'))
|
|
385
|
+
return 'prettier';
|
|
386
|
+
if (deps.includes('eslint'))
|
|
387
|
+
return 'eslint';
|
|
388
|
+
if (deps.includes('black'))
|
|
389
|
+
return 'black';
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function detectCI(projectRoot) {
|
|
393
|
+
if (fs.existsSync(path.join(projectRoot, '.github', 'workflows')))
|
|
394
|
+
return 'github-actions';
|
|
395
|
+
if (fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')))
|
|
396
|
+
return 'gitlab-ci';
|
|
397
|
+
if (fs.existsSync(path.join(projectRoot, 'Jenkinsfile')))
|
|
398
|
+
return 'jenkins';
|
|
399
|
+
if (fs.existsSync(path.join(projectRoot, '.circleci')))
|
|
400
|
+
return 'circleci';
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
function getNoteworthy(deps) {
|
|
404
|
+
const noteworthy = [
|
|
405
|
+
// Databases
|
|
406
|
+
'pg', 'mysql', 'mongodb', 'redis', 'neo4j-driver', 'prisma', 'typeorm', 'sequelize', 'mongoose',
|
|
407
|
+
// Frameworks
|
|
408
|
+
'express', 'fastify', 'koa', 'nestjs', 'next', 'react', 'vue', 'angular', 'svelte',
|
|
409
|
+
// AI/ML
|
|
410
|
+
'openai', '@anthropic-ai', 'langchain', 'tensorflow', 'pytorch',
|
|
411
|
+
// Auth
|
|
412
|
+
'passport', 'jsonwebtoken', 'bcrypt',
|
|
413
|
+
// Testing
|
|
414
|
+
'jest', 'vitest', 'playwright', 'cypress',
|
|
415
|
+
// Build
|
|
416
|
+
'webpack', 'vite', 'rollup', 'esbuild',
|
|
417
|
+
// GraphQL
|
|
418
|
+
'graphql', 'apollo', '@apollo/client',
|
|
419
|
+
];
|
|
420
|
+
return deps.filter(d => noteworthy.some(n => d.includes(n))).slice(0, 10);
|
|
421
|
+
}
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Main Analysis
|
|
424
|
+
// ============================================================================
|
|
425
|
+
function runAnalysis(projectRoot) {
|
|
426
|
+
const { files, dirs } = scanDirectory(projectRoot);
|
|
427
|
+
const deps = getDependencies(projectRoot);
|
|
428
|
+
const languages = detectLanguages(files);
|
|
429
|
+
const frameworks = detectFrameworks(projectRoot, deps.all);
|
|
430
|
+
const testDirs = findTestDirs(dirs);
|
|
431
|
+
return {
|
|
432
|
+
version: '1.0',
|
|
433
|
+
timestamp: new Date().toISOString(),
|
|
434
|
+
project: {
|
|
435
|
+
name: getProjectName(projectRoot),
|
|
436
|
+
path: projectRoot,
|
|
437
|
+
groupId: getCurrentGroupId(projectRoot),
|
|
438
|
+
},
|
|
439
|
+
codebase: {
|
|
440
|
+
language: languages[0] || 'Unknown',
|
|
441
|
+
languages,
|
|
442
|
+
framework: frameworks[0] || null,
|
|
443
|
+
frameworks,
|
|
444
|
+
buildTools: detectBuildTools(projectRoot),
|
|
445
|
+
},
|
|
446
|
+
structure: {
|
|
447
|
+
entryPoints: findEntryPoints(projectRoot, files),
|
|
448
|
+
mainModules: findMainModules(dirs),
|
|
449
|
+
testDirs,
|
|
450
|
+
configFiles: findConfigFiles(projectRoot),
|
|
451
|
+
},
|
|
452
|
+
dependencies: {
|
|
453
|
+
count: deps.all.length,
|
|
454
|
+
production: deps.production.slice(0, 10),
|
|
455
|
+
dev: deps.dev.slice(0, 5),
|
|
456
|
+
noteworthy: getNoteworthy(deps.all),
|
|
457
|
+
},
|
|
458
|
+
patterns: {
|
|
459
|
+
architecture: detectArchitecture(dirs),
|
|
460
|
+
testing: detectTesting(projectRoot, deps.all),
|
|
461
|
+
formatting: detectFormatting(deps.all),
|
|
462
|
+
ci: detectCI(projectRoot),
|
|
463
|
+
},
|
|
464
|
+
metrics: {
|
|
465
|
+
fileCount: files.length,
|
|
466
|
+
dirCount: dirs.length,
|
|
467
|
+
hasTests: testDirs.length > 0,
|
|
468
|
+
hasDocumentation: fs.existsSync(path.join(projectRoot, 'README.md')),
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function generateSummary(result) {
|
|
473
|
+
const parts = [];
|
|
474
|
+
// Language and framework
|
|
475
|
+
const framework = result.codebase.framework ? ` with ${result.codebase.framework}` : '';
|
|
476
|
+
parts.push(`${result.codebase.language} project${framework}`);
|
|
477
|
+
// Architecture
|
|
478
|
+
if (result.patterns.architecture) {
|
|
479
|
+
parts.push(`using ${result.patterns.architecture} pattern`);
|
|
480
|
+
}
|
|
481
|
+
// Build tools
|
|
482
|
+
if (result.codebase.buildTools.length > 0) {
|
|
483
|
+
parts.push(`Build: ${result.codebase.buildTools.join(', ')}`);
|
|
484
|
+
}
|
|
485
|
+
// Entry points
|
|
486
|
+
if (result.structure.entryPoints.length > 0) {
|
|
487
|
+
parts.push(`Entry: ${result.structure.entryPoints.slice(0, 3).join(', ')}`);
|
|
488
|
+
}
|
|
489
|
+
// Main modules
|
|
490
|
+
if (result.structure.mainModules.length > 0) {
|
|
491
|
+
parts.push(`Modules: ${result.structure.mainModules.slice(0, 4).join(', ')}`);
|
|
492
|
+
}
|
|
493
|
+
// Dependencies
|
|
494
|
+
if (result.dependencies.noteworthy.length > 0) {
|
|
495
|
+
parts.push(`Key deps: ${result.dependencies.noteworthy.slice(0, 5).join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
// Testing
|
|
498
|
+
if (result.patterns.testing) {
|
|
499
|
+
parts.push(`Testing: ${result.patterns.testing}`);
|
|
500
|
+
}
|
|
501
|
+
// Metrics
|
|
502
|
+
parts.push(`${result.metrics.fileCount} files, ${result.metrics.dirCount} directories`);
|
|
503
|
+
return parts.join('. ') + '.';
|
|
504
|
+
}
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// Memory Storage
|
|
507
|
+
// ============================================================================
|
|
508
|
+
async function storeToMemory(summary, projectRoot) {
|
|
509
|
+
const memoryScript = path.join(__dirname, '..', '..', 'memory', 'scripts', 'memory.js');
|
|
510
|
+
if (!fs.existsSync(memoryScript)) {
|
|
511
|
+
throw new Error('Memory script not found');
|
|
512
|
+
}
|
|
513
|
+
return new Promise((resolve, reject) => {
|
|
514
|
+
const child = spawn('node', [
|
|
515
|
+
memoryScript,
|
|
516
|
+
'add',
|
|
517
|
+
summary,
|
|
518
|
+
'--type', 'init-review',
|
|
519
|
+
'--tag', 'scope:codebase',
|
|
520
|
+
'--cache',
|
|
521
|
+
], {
|
|
522
|
+
cwd: projectRoot,
|
|
523
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
524
|
+
});
|
|
525
|
+
let stdout = '';
|
|
526
|
+
let stderr = '';
|
|
527
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
528
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
529
|
+
child.on('close', (code) => {
|
|
530
|
+
if (code === 0) {
|
|
531
|
+
resolve();
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
reject(new Error(`Memory storage failed: ${stderr || stdout}`));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
async function loadFromMemory(projectRoot) {
|
|
540
|
+
const memoryScript = path.join(__dirname, '..', '..', 'memory', 'scripts', 'memory.js');
|
|
541
|
+
if (!fs.existsSync(memoryScript)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
const child = spawn('node', [
|
|
546
|
+
memoryScript,
|
|
547
|
+
'load',
|
|
548
|
+
'--query', 'init-review',
|
|
549
|
+
'--limit', '1',
|
|
550
|
+
'--cache',
|
|
551
|
+
], {
|
|
552
|
+
cwd: projectRoot,
|
|
553
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
554
|
+
});
|
|
555
|
+
let stdout = '';
|
|
556
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
557
|
+
child.on('close', (code) => {
|
|
558
|
+
if (code === 0) {
|
|
559
|
+
try {
|
|
560
|
+
const result = JSON.parse(stdout);
|
|
561
|
+
const facts = result.facts || [];
|
|
562
|
+
if (facts.length > 0) {
|
|
563
|
+
resolve(facts[0].fact || facts[0].name || null);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
resolve(null);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch (_) {
|
|
570
|
+
resolve(null);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
resolve(null);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// Marker File
|
|
581
|
+
// ============================================================================
|
|
582
|
+
function getMarkerPath(projectRoot) {
|
|
583
|
+
return path.join(projectRoot, '.agents', '.init-review-done');
|
|
584
|
+
}
|
|
585
|
+
function readMarker(projectRoot) {
|
|
586
|
+
const markerPath = getMarkerPath(projectRoot);
|
|
587
|
+
if (!fs.existsSync(markerPath)) {
|
|
588
|
+
return { done: false, enriched: false, timestamp: null };
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const content = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
592
|
+
return {
|
|
593
|
+
done: true,
|
|
594
|
+
enriched: content.enriched || false,
|
|
595
|
+
timestamp: content.timestamp || null,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
catch (_) {
|
|
599
|
+
return { done: true, enriched: false, timestamp: null };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function writeMarker(projectRoot, enriched = false) {
|
|
603
|
+
const markerPath = getMarkerPath(projectRoot);
|
|
604
|
+
const content = {
|
|
605
|
+
version: '1.0',
|
|
606
|
+
timestamp: new Date().toISOString(),
|
|
607
|
+
groupId: getCurrentGroupId(projectRoot),
|
|
608
|
+
enriched,
|
|
609
|
+
};
|
|
610
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
611
|
+
fs.writeFileSync(markerPath, JSON.stringify(content, null, 2));
|
|
612
|
+
}
|
|
613
|
+
function deleteMarker(projectRoot) {
|
|
614
|
+
const markerPath = getMarkerPath(projectRoot);
|
|
615
|
+
if (fs.existsSync(markerPath)) {
|
|
616
|
+
fs.unlinkSync(markerPath);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// ============================================================================
|
|
620
|
+
// CLI Commands
|
|
621
|
+
// ============================================================================
|
|
622
|
+
async function commandRun(projectRoot, force) {
|
|
623
|
+
// Check marker
|
|
624
|
+
const marker = readMarker(projectRoot);
|
|
625
|
+
if (marker.done && !force) {
|
|
626
|
+
console.log(JSON.stringify({
|
|
627
|
+
status: 'skipped',
|
|
628
|
+
action: 'run',
|
|
629
|
+
reason: 'Init review already done. Use --force to re-run.',
|
|
630
|
+
timestamp: marker.timestamp,
|
|
631
|
+
}, null, 2));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Delete marker if forcing
|
|
635
|
+
if (force) {
|
|
636
|
+
deleteMarker(projectRoot);
|
|
637
|
+
}
|
|
638
|
+
// Check if codebase
|
|
639
|
+
const codebaseInfo = isCodebase(projectRoot);
|
|
640
|
+
if (!codebaseInfo.isCodebase) {
|
|
641
|
+
console.log(JSON.stringify({
|
|
642
|
+
status: 'skipped',
|
|
643
|
+
action: 'run',
|
|
644
|
+
reason: codebaseInfo.reason,
|
|
645
|
+
}, null, 2));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Run analysis
|
|
649
|
+
const result = runAnalysis(projectRoot);
|
|
650
|
+
const summary = generateSummary(result);
|
|
651
|
+
// Store to memory
|
|
652
|
+
try {
|
|
653
|
+
await storeToMemory(summary, projectRoot);
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
// Log but don't fail
|
|
657
|
+
console.error(`Warning: Could not store to memory: ${err instanceof Error ? err.message : err}`);
|
|
658
|
+
}
|
|
659
|
+
// Write marker (not enriched yet)
|
|
660
|
+
writeMarker(projectRoot, false);
|
|
661
|
+
// Save static analysis for AI enrichment worker
|
|
662
|
+
const agentsDir = path.join(projectRoot, '.agents');
|
|
663
|
+
const staticFile = path.join(agentsDir, '.init-review-static.json');
|
|
664
|
+
try {
|
|
665
|
+
fs.writeFileSync(staticFile, JSON.stringify({ summary, result }, null, 2));
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
console.error(`Warning: Could not save static analysis: ${err instanceof Error ? err.message : err}`);
|
|
669
|
+
}
|
|
670
|
+
// Spawn AI enrichment worker in background
|
|
671
|
+
const enrichWorker = path.join(__dirname, 'ai-enrich.js');
|
|
672
|
+
if (fs.existsSync(enrichWorker)) {
|
|
673
|
+
try {
|
|
674
|
+
// Use stdio: 'ignore' for cross-platform compatibility
|
|
675
|
+
// The worker handles its own logging via fs.appendFileSync
|
|
676
|
+
const child = spawn('node', [enrichWorker, projectRoot, agentsDir], {
|
|
677
|
+
cwd: projectRoot,
|
|
678
|
+
stdio: 'ignore',
|
|
679
|
+
detached: true,
|
|
680
|
+
windowsHide: true,
|
|
681
|
+
});
|
|
682
|
+
child.unref();
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
// Log but don't fail
|
|
686
|
+
console.error(`Warning: Could not start AI enrichment: ${err instanceof Error ? err.message : err}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Output result
|
|
690
|
+
console.log(JSON.stringify({
|
|
691
|
+
status: 'ok',
|
|
692
|
+
action: 'run',
|
|
693
|
+
result,
|
|
694
|
+
summary,
|
|
695
|
+
enrichmentQueued: fs.existsSync(path.join(__dirname, 'ai-enrich.js')),
|
|
696
|
+
}, null, 2));
|
|
697
|
+
}
|
|
698
|
+
async function commandShow(projectRoot) {
|
|
699
|
+
const marker = readMarker(projectRoot);
|
|
700
|
+
if (!marker.done) {
|
|
701
|
+
console.log(JSON.stringify({
|
|
702
|
+
status: 'not_found',
|
|
703
|
+
action: 'show',
|
|
704
|
+
message: 'No init review found. Run with: node init-review.js run',
|
|
705
|
+
}, null, 2));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// Try to load from memory
|
|
709
|
+
const review = await loadFromMemory(projectRoot);
|
|
710
|
+
console.log(JSON.stringify({
|
|
711
|
+
status: 'ok',
|
|
712
|
+
action: 'show',
|
|
713
|
+
review: review || 'Init review stored but not found in memory',
|
|
714
|
+
enriched: marker.enriched,
|
|
715
|
+
timestamp: marker.timestamp,
|
|
716
|
+
}, null, 2));
|
|
717
|
+
}
|
|
718
|
+
async function commandStatus(projectRoot) {
|
|
719
|
+
const marker = readMarker(projectRoot);
|
|
720
|
+
const codebaseInfo = isCodebase(projectRoot);
|
|
721
|
+
console.log(JSON.stringify({
|
|
722
|
+
status: 'ok',
|
|
723
|
+
action: 'status',
|
|
724
|
+
isCodebase: codebaseInfo.isCodebase,
|
|
725
|
+
confidence: codebaseInfo.confidence,
|
|
726
|
+
done: marker.done,
|
|
727
|
+
enriched: marker.enriched,
|
|
728
|
+
timestamp: marker.timestamp,
|
|
729
|
+
groupId: getCurrentGroupId(projectRoot),
|
|
730
|
+
}, null, 2));
|
|
731
|
+
}
|
|
732
|
+
// ============================================================================
|
|
733
|
+
// Main
|
|
734
|
+
// ============================================================================
|
|
735
|
+
async function main() {
|
|
736
|
+
const args = process.argv.slice(2);
|
|
737
|
+
const command = args[0] || 'status';
|
|
738
|
+
const force = args.includes('--force');
|
|
739
|
+
const projectRoot = process.cwd();
|
|
740
|
+
try {
|
|
741
|
+
switch (command) {
|
|
742
|
+
case 'run':
|
|
743
|
+
await commandRun(projectRoot, force);
|
|
744
|
+
break;
|
|
745
|
+
case 'show':
|
|
746
|
+
await commandShow(projectRoot);
|
|
747
|
+
break;
|
|
748
|
+
case 'status':
|
|
749
|
+
await commandStatus(projectRoot);
|
|
750
|
+
break;
|
|
751
|
+
default:
|
|
752
|
+
console.log(JSON.stringify({
|
|
753
|
+
status: 'error',
|
|
754
|
+
error: `Unknown command: ${command}. Use run|show|status`,
|
|
755
|
+
}, null, 2));
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
console.log(JSON.stringify({
|
|
761
|
+
status: 'error',
|
|
762
|
+
error: err instanceof Error ? err.message : String(err),
|
|
763
|
+
}, null, 2));
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Export for use by postinstall
|
|
768
|
+
module.exports = { isCodebase, runAnalysis, generateSummary, writeMarker };
|
|
769
|
+
main();
|