contextforge-cli-ai-prompt-pirates 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.contextforge/config.ai.example.json +28 -0
- package/.contextforge/config.example.json +29 -0
- package/.contextforge/prompts/README.md +50 -0
- package/.env.example +73 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/contextforge.js +14 -0
- package/bin/postinstall-worker.js +14 -0
- package/bin/postinstall.js +26 -0
- package/package.json +65 -0
- package/prompts/README.md +50 -0
- package/prompts/response-schema.md +22 -0
- package/prompts/retry-addon.md +1 -0
- package/prompts/system.md +10 -0
- package/prompts/user-template.md +22 -0
- package/src/analyzers/ast-parser.js +139 -0
- package/src/analyzers/bugs-lite.js +118 -0
- package/src/analyzers/changelog.js +190 -0
- package/src/analyzers/code-insights.js +225 -0
- package/src/analyzers/dependencies.js +110 -0
- package/src/analyzers/detection-quality.js +106 -0
- package/src/analyzers/eslint-runner.js +56 -0
- package/src/analyzers/folder-tree.js +60 -0
- package/src/analyzers/git-insights.js +94 -0
- package/src/analyzers/project-meta.js +98 -0
- package/src/cli/commands/changes.js +36 -0
- package/src/cli/commands/doctor.js +152 -0
- package/src/cli/commands/generate.js +7 -0
- package/src/cli/commands/init.js +98 -0
- package/src/cli/commands/prompt.js +53 -0
- package/src/cli/commands/stop.js +10 -0
- package/src/cli/commands/summary.js +22 -0
- package/src/cli/commands/watch.js +9 -0
- package/src/cli/index.js +120 -0
- package/src/core/confidence.js +51 -0
- package/src/core/config.js +183 -0
- package/src/core/cursor-rules.js +293 -0
- package/src/core/ensure-setup.js +45 -0
- package/src/core/env.js +113 -0
- package/src/core/package-meta.js +35 -0
- package/src/core/paths.js +39 -0
- package/src/core/pipeline.js +256 -0
- package/src/core/postinstall.js +168 -0
- package/src/core/setup-env.js +86 -0
- package/src/core/setup-prompts.js +31 -0
- package/src/core/snapshot-hash.js +70 -0
- package/src/detectors/architecture.js +117 -0
- package/src/detectors/auth-deploy.js +51 -0
- package/src/detectors/database.js +127 -0
- package/src/detectors/tech-stack.js +180 -0
- package/src/generators/artifacts.js +44 -0
- package/src/generators/changes-markdown.js +72 -0
- package/src/generators/context-json.js +91 -0
- package/src/generators/context-markdown.js +571 -0
- package/src/generators/mermaid.js +59 -0
- package/src/index.js +13 -0
- package/src/scanner/change-detector.js +24 -0
- package/src/scanner/config-files.js +52 -0
- package/src/scanner/file-index.js +55 -0
- package/src/scanner/package-deps.js +32 -0
- package/src/scanner/repo-scanner.js +143 -0
- package/src/services/ai/background.js +87 -0
- package/src/services/ai/config.js +48 -0
- package/src/services/ai/enrich.js +87 -0
- package/src/services/ai/index.js +3 -0
- package/src/services/ai/prompt-loader.js +104 -0
- package/src/services/ai/prompt.js +42 -0
- package/src/services/ai/providers/groq.js +36 -0
- package/src/services/ai/providers/openai.js +32 -0
- package/src/services/ai/redact.js +33 -0
- package/src/services/ai/validate.js +70 -0
- package/src/watcher/watcher.js +128 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const IMPORT_RE = /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\))/g;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build import graph and dependency flow chains (§7.4).
|
|
8
|
+
*/
|
|
9
|
+
export function analyzeDependencies(projectRoot, scanResult, config, codeInsights = null) {
|
|
10
|
+
const graph = [];
|
|
11
|
+
const roleMap = new Map();
|
|
12
|
+
|
|
13
|
+
const sourceFiles = scanResult.relativePaths
|
|
14
|
+
.filter((p) => /\.(js|jsx|ts|tsx|mjs|cjs)$/.test(p))
|
|
15
|
+
.filter((p) => !p.includes('node_modules'))
|
|
16
|
+
.slice(0, 80);
|
|
17
|
+
|
|
18
|
+
for (const rel of sourceFiles) {
|
|
19
|
+
const full = path.join(projectRoot, rel);
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
22
|
+
const imports = [];
|
|
23
|
+
let m;
|
|
24
|
+
IMPORT_RE.lastIndex = 0;
|
|
25
|
+
while ((m = IMPORT_RE.exec(content)) !== null) {
|
|
26
|
+
const target = m[1] || m[2];
|
|
27
|
+
if (target.startsWith('.') || target.startsWith('@/')) {
|
|
28
|
+
imports.push(target);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const role = inferRole(rel);
|
|
32
|
+
if (role) roleMap.set(rel, role);
|
|
33
|
+
|
|
34
|
+
if (imports.length) {
|
|
35
|
+
graph.push({
|
|
36
|
+
file: rel,
|
|
37
|
+
role,
|
|
38
|
+
imports: [...new Set(imports)].slice(0, 12),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
/* skip */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chains = buildDependencyChains(graph, roleMap, codeInsights);
|
|
47
|
+
const flowHints = [];
|
|
48
|
+
|
|
49
|
+
if (chains.length) {
|
|
50
|
+
flowHints.push(...chains.slice(0, 8));
|
|
51
|
+
}
|
|
52
|
+
if (graph.some((g) => g.file.includes('middleware'))) {
|
|
53
|
+
flowHints.push('Middleware → routes/controllers (cross-cutting layer)');
|
|
54
|
+
}
|
|
55
|
+
if (graph.some((g) => g.role === 'controller' && g.imports.some((i) => i.includes('service')))) {
|
|
56
|
+
flowHints.push('Controllers → Services (thin controller pattern)');
|
|
57
|
+
}
|
|
58
|
+
if (graph.some((g) => g.role === 'service' && g.imports.some((i) => i.includes('model')))) {
|
|
59
|
+
flowHints.push('Services → Models / data layer');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
sampleSize: sourceFiles.length,
|
|
64
|
+
edges: graph.slice(0, 50),
|
|
65
|
+
chains,
|
|
66
|
+
flowHints: [...new Set(flowHints)],
|
|
67
|
+
summary:
|
|
68
|
+
graph.length > 0
|
|
69
|
+
? `Mapped ${graph.length} files with local imports; ${chains.length} dependency chain(s) inferred.`
|
|
70
|
+
: 'No local import graph extracted.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferRole(filePath) {
|
|
75
|
+
if (/middleware/i.test(filePath)) return 'middleware';
|
|
76
|
+
if (/controllers?/i.test(filePath)) return 'controller';
|
|
77
|
+
if (/routes?/i.test(filePath)) return 'route';
|
|
78
|
+
if (/services?/i.test(filePath)) return 'service';
|
|
79
|
+
if (/models?/i.test(filePath)) return 'model';
|
|
80
|
+
if (/components?/i.test(filePath)) return 'component';
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildDependencyChains(graph, roleMap, codeInsights) {
|
|
85
|
+
const chains = [];
|
|
86
|
+
|
|
87
|
+
const hasAuth = codeInsights?.modules?.middleware?.length ||
|
|
88
|
+
graph.some((g) => /auth|jwt|passport/i.test(g.file));
|
|
89
|
+
const hasJwt = graph.some((g) => /jwt|jsonwebtoken/i.test(JSON.stringify(g.imports)));
|
|
90
|
+
|
|
91
|
+
if (hasAuth && roleMap.has('middleware') || graph.some((g) => g.role === 'middleware')) {
|
|
92
|
+
chains.push('Request → Auth Middleware → Route → Controller → Service → Model/DB');
|
|
93
|
+
} else if (graph.some((g) => g.role === 'route' && graph.some((x) => x.role === 'controller'))) {
|
|
94
|
+
chains.push('Client → Route → Controller → Service → Database');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (hasJwt) {
|
|
98
|
+
chains.push('AuthService → JWT → Middleware → UserModel');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const services = graph.filter((g) => g.role === 'service');
|
|
102
|
+
const controllers = graph.filter((g) => g.role === 'controller');
|
|
103
|
+
if (controllers.length && services.length) {
|
|
104
|
+
const ctrl = path.basename(controllers[0].file);
|
|
105
|
+
const svc = path.basename(services[0].file);
|
|
106
|
+
chains.push(`${ctrl} → ${svc} → data layer`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [...new Set(chains)];
|
|
110
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-check scan results so context.md accuracy reflects real evidence.
|
|
3
|
+
* @param {object} snapshot
|
|
4
|
+
*/
|
|
5
|
+
export function assessDetectionQuality(snapshot) {
|
|
6
|
+
const ts = snapshot.techStack || {};
|
|
7
|
+
const code = snapshot.codeInsights || {};
|
|
8
|
+
const arch = snapshot.architecture;
|
|
9
|
+
const db = snapshot.database || {};
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const strengths = [];
|
|
12
|
+
let score = 70;
|
|
13
|
+
|
|
14
|
+
const signalCount = ts.signals?.length || 0;
|
|
15
|
+
if (signalCount >= 5) {
|
|
16
|
+
strengths.push(`${signalCount} detection signals from package.json and config files`);
|
|
17
|
+
score += 10;
|
|
18
|
+
} else if (signalCount >= 1) {
|
|
19
|
+
strengths.push('Some stack signals detected from dependencies');
|
|
20
|
+
} else {
|
|
21
|
+
warnings.push('No dependency signals — add a package.json with dependencies');
|
|
22
|
+
score -= 25;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (ts.confidence === 'high') {
|
|
26
|
+
strengths.push('Tech stack confidence: high (package.json + multiple categories)');
|
|
27
|
+
score += 5;
|
|
28
|
+
} else if (ts.confidence === 'low') {
|
|
29
|
+
warnings.push('Tech stack confidence is low — verify Frontend/Backend sections manually');
|
|
30
|
+
score -= 15;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasExpress = (ts.backend || []).some((b) => /express/i.test(b));
|
|
34
|
+
const hasRoutes = (code.apiEndpoints?.length || 0) > 0 || (snapshot.ast?.routesCount || 0) > 0;
|
|
35
|
+
if (hasExpress && !hasRoutes) {
|
|
36
|
+
warnings.push('Express detected but few routes found — check server/ or routes/ folders');
|
|
37
|
+
score -= 5;
|
|
38
|
+
} else if (hasExpress && hasRoutes) {
|
|
39
|
+
strengths.push('Backend framework matches detected API routes');
|
|
40
|
+
score += 5;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hasOrmDep = (ts.orm || []).length || (ts.database || []).some((d) => /mongo|sql|prisma/i.test(d));
|
|
44
|
+
const hasModels = (db.models || []).length > 0;
|
|
45
|
+
if (hasOrmDep && !hasModels) {
|
|
46
|
+
warnings.push('Database library detected but no models/schemas parsed — add model files or schema.prisma');
|
|
47
|
+
score -= 8;
|
|
48
|
+
} else if (hasModels) {
|
|
49
|
+
strengths.push(`Database schemas parsed (${db.models.length} model(s))`);
|
|
50
|
+
score += 8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hasReact = (ts.frontend || []).some((f) => /react/i.test(f));
|
|
54
|
+
const hasComponents = (code.modules?.components?.length || 0) > 0;
|
|
55
|
+
if (hasReact && !hasComponents) {
|
|
56
|
+
warnings.push('React detected but no components/ folder — may be minimal UI or different structure');
|
|
57
|
+
} else if (hasReact && hasComponents) {
|
|
58
|
+
strengths.push('Frontend framework matches component files in tree');
|
|
59
|
+
score += 5;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if ((ts.auth || []).length && !code.modules?.middleware?.length) {
|
|
63
|
+
strengths.push('Auth libraries detected from dependencies');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arch?.primary?.confidence === 'high') {
|
|
67
|
+
strengths.push(`Architecture "${arch.primary.label}" matched with high confidence`);
|
|
68
|
+
score += 5;
|
|
69
|
+
} else if (arch?.primary?.confidence === 'low') {
|
|
70
|
+
warnings.push('Architecture pattern is uncertain — confirm MVC/layered/monolith manually');
|
|
71
|
+
score -= 5;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!snapshot.projectMeta?.scripts?.length) {
|
|
75
|
+
warnings.push('No npm scripts found — run init from project root with package.json');
|
|
76
|
+
score -= 5;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (snapshot.filesScanned < 10) {
|
|
80
|
+
warnings.push('Very few files scanned — check .contextforge/config.json ignore list');
|
|
81
|
+
score -= 10;
|
|
82
|
+
} else if (snapshot.filesScanned >= 30) {
|
|
83
|
+
strengths.push(`${snapshot.filesScanned} files scanned for analysis`);
|
|
84
|
+
score += 5;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
score = Math.max(0, Math.min(100, score));
|
|
88
|
+
|
|
89
|
+
let grade = 'medium';
|
|
90
|
+
if (score >= 80) grade = 'high';
|
|
91
|
+
else if (score < 55) grade = 'low';
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
score,
|
|
95
|
+
grade,
|
|
96
|
+
strengths,
|
|
97
|
+
warnings,
|
|
98
|
+
reliableForAi: score >= 65 && signalCount >= 2,
|
|
99
|
+
summary:
|
|
100
|
+
grade === 'high'
|
|
101
|
+
? 'Detection is strong — context.md should closely match this project.'
|
|
102
|
+
: grade === 'medium'
|
|
103
|
+
? 'Detection is usable — verify sections marked with lower confidence.'
|
|
104
|
+
: 'Detection is weak — add package.json, standard folders, and run generate again.',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run ESLint if available and configured in project.
|
|
6
|
+
* @param {string} projectRoot
|
|
7
|
+
* @param {string[]} relativePaths
|
|
8
|
+
*/
|
|
9
|
+
export async function runEslintIfAvailable(projectRoot, relativePaths) {
|
|
10
|
+
const eslintConfigs = relativePaths.filter(
|
|
11
|
+
(p) =>
|
|
12
|
+
/^\.eslintrc/i.test(path.basename(p)) ||
|
|
13
|
+
p.endsWith('eslint.config.js') ||
|
|
14
|
+
p.endsWith('eslint.config.mjs')
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (!eslintConfigs.length) {
|
|
18
|
+
return { ran: false, issues: [], summary: 'No ESLint config found' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const { ESLint } = await import('eslint');
|
|
23
|
+
const eslint = new ESLint({ cwd: projectRoot });
|
|
24
|
+
const results = await eslint.lintFiles(['**/*.{js,jsx,ts,tsx,mjs,cjs}']);
|
|
25
|
+
|
|
26
|
+
const issues = [];
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
for (const msg of result.messages.slice(0, 5)) {
|
|
29
|
+
if (msg.severity >= 1) {
|
|
30
|
+
issues.push({
|
|
31
|
+
severity: msg.severity === 2 ? 'high' : 'medium',
|
|
32
|
+
type: 'eslint',
|
|
33
|
+
file: path.relative(projectRoot, result.filePath).replace(/\\/g, '/'),
|
|
34
|
+
message: `${msg.ruleId || 'eslint'}: ${msg.message} (line ${msg.line})`,
|
|
35
|
+
ruleId: msg.ruleId,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
ran: true,
|
|
43
|
+
issues: issues.slice(0, 30),
|
|
44
|
+
summary:
|
|
45
|
+
issues.length > 0
|
|
46
|
+
? `ESLint found ${issues.length} issue(s)`
|
|
47
|
+
: 'ESLint passed with no issues in scanned files',
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
ran: false,
|
|
52
|
+
issues: [],
|
|
53
|
+
summary: `ESLint not run: ${err.message?.split('\n')[0]}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a nested tree object from relative paths (depth-limited).
|
|
3
|
+
* @param {string[]} relativePaths
|
|
4
|
+
* @param {number} maxDepth
|
|
5
|
+
*/
|
|
6
|
+
export function buildFolderTree(relativePaths, maxDepth = 4) {
|
|
7
|
+
const root = {};
|
|
8
|
+
|
|
9
|
+
const dirSet = new Set();
|
|
10
|
+
for (const p of relativePaths) {
|
|
11
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
12
|
+
for (let i = 1; i < parts.length; i++) {
|
|
13
|
+
dirSet.add(parts.slice(0, i).join('/'));
|
|
14
|
+
}
|
|
15
|
+
if (parts.length === 1) {
|
|
16
|
+
root[p] = root[p] || null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const dir of [...dirSet].sort()) {
|
|
21
|
+
const parts = dir.split('/');
|
|
22
|
+
if (parts.length > maxDepth) continue;
|
|
23
|
+
|
|
24
|
+
let node = root;
|
|
25
|
+
for (let i = 0; i < parts.length; i++) {
|
|
26
|
+
const part = parts[i];
|
|
27
|
+
if (!node[part]) {
|
|
28
|
+
node[part] = i === parts.length - 1 ? {} : {};
|
|
29
|
+
}
|
|
30
|
+
if (typeof node[part] === 'object' && node[part] !== null) {
|
|
31
|
+
node = node[part];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderTreeAscii(node, prefix = '', isLast = true, depth = 0, maxDepth = 4) {
|
|
40
|
+
if (depth > maxDepth) return '';
|
|
41
|
+
|
|
42
|
+
const lines = [];
|
|
43
|
+
const entries = Object.keys(node).sort();
|
|
44
|
+
|
|
45
|
+
entries.forEach((key, index) => {
|
|
46
|
+
const last = index === entries.length - 1;
|
|
47
|
+
const connector = last ? '└── ' : '├── ';
|
|
48
|
+
lines.push(`${prefix}${connector}${key}`);
|
|
49
|
+
|
|
50
|
+
const child = node[key];
|
|
51
|
+
if (child && typeof child === 'object' && Object.keys(child).length > 0) {
|
|
52
|
+
const extension = last ? ' ' : '│ ';
|
|
53
|
+
lines.push(
|
|
54
|
+
renderTreeAscii(child, prefix + extension, last, depth + 1, maxDepth)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return lines.filter(Boolean).join('\n');
|
|
60
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import simpleGit from 'simple-git';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const NARRATIVE_PATTERNS = [
|
|
6
|
+
{ re: /redux\s*→\s*zustand|migrat.*zustand|zustand.*instead/i, text: 'Migrated Redux → Zustand (mentioned in commit history).' },
|
|
7
|
+
{ re: /refactor.*(api|service|modular)/i, text: 'Refactored API into modular services architecture.' },
|
|
8
|
+
{ re: /migrat/i, text: 'Architecture or schema migration activity in recent commits.' },
|
|
9
|
+
{ re: /add.*(auth|jwt|oauth)/i, text: 'Authentication-related changes in project timeline.' },
|
|
10
|
+
{ re: /docker|deploy|vercel|nginx/i, text: 'Deployment or infrastructure changes recorded.' },
|
|
11
|
+
{ re: /prisma|schema|database/i, text: 'Database schema evolution in recent commits.' },
|
|
12
|
+
{ re: /dependenc|bump|upgrade/i, text: 'Dependency upgrades in recent development.' },
|
|
13
|
+
{ re: /fix.*(bug|memory|leak)/i, text: 'Bug or stability fixes in recent history.' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} projectRoot
|
|
18
|
+
* @param {number} limit
|
|
19
|
+
*/
|
|
20
|
+
export async function collectGitInsights(projectRoot, limit = 20) {
|
|
21
|
+
const result = {
|
|
22
|
+
available: false,
|
|
23
|
+
commits: [],
|
|
24
|
+
insights: [],
|
|
25
|
+
narratives: [],
|
|
26
|
+
branch: null,
|
|
27
|
+
timeline: [],
|
|
28
|
+
packageChanges: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const git = simpleGit(projectRoot);
|
|
33
|
+
const isRepo = await git.checkIsRepo();
|
|
34
|
+
if (!isRepo) {
|
|
35
|
+
result.insights.push('Not a git repository — git history skipped.');
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result.available = true;
|
|
40
|
+
result.branch = (await git.branch()).current;
|
|
41
|
+
|
|
42
|
+
const log = await git.log({ maxCount: limit });
|
|
43
|
+
result.commits = log.all.map((c) => ({
|
|
44
|
+
hash: c.hash.slice(0, 7),
|
|
45
|
+
date: c.date,
|
|
46
|
+
author: c.author_name,
|
|
47
|
+
message: c.message,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
result.timeline = log.all.slice(0, 8).map((c) => ({
|
|
51
|
+
date: c.date?.slice(0, 10),
|
|
52
|
+
event: c.message.split('\n')[0].slice(0, 80),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const messages = log.all.map((c) => c.message);
|
|
56
|
+
const narratives = [];
|
|
57
|
+
const insights = [];
|
|
58
|
+
|
|
59
|
+
for (const { re, text } of NARRATIVE_PATTERNS) {
|
|
60
|
+
if (messages.some((m) => re.test(m))) {
|
|
61
|
+
narratives.push(text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
67
|
+
if (await fs.pathExists(pkgPath)) {
|
|
68
|
+
const diff = await git.diff(['HEAD~5', 'HEAD', '--', 'package.json']);
|
|
69
|
+
if (diff && diff.length > 50) {
|
|
70
|
+
result.packageChanges = 'package.json modified in last 5 commits';
|
|
71
|
+
insights.push('Recent dependency or script changes in package.json');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* optional */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (narratives.length === 0 && log.all.length > 0) {
|
|
79
|
+
insights.push(`Analyzed ${log.all.length} recent commits on branch "${result.branch}".`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result.narratives = narratives;
|
|
83
|
+
result.insights = [...narratives, ...insights];
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = err.message || String(err);
|
|
86
|
+
if (msg.includes('ENOENT') || msg.includes('git')) {
|
|
87
|
+
result.insights.push('Git not available in PATH — install Git to enable commit history and timeline.');
|
|
88
|
+
} else {
|
|
89
|
+
result.insights.push(`Git analysis unavailable: ${msg.split('\n')[0]}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ENTRY_PATTERNS = [
|
|
5
|
+
/^server\/index\.(js|ts|mjs|cjs)$/i,
|
|
6
|
+
/^src\/index\.(js|ts|tsx|jsx)$/i,
|
|
7
|
+
/^src\/main\.(js|ts|tsx)$/i,
|
|
8
|
+
/^index\.(js|ts|mjs|cjs)$/i,
|
|
9
|
+
/^app\.(js|ts|tsx)$/i,
|
|
10
|
+
/^bin\/[^/]+\.(js|mjs)$/i,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Package scripts, entry points, env var names, workspaces — for detailed context.md.
|
|
15
|
+
* @param {string} projectRoot
|
|
16
|
+
* @param {object} configFiles
|
|
17
|
+
* @param {string[]} relativePaths
|
|
18
|
+
*/
|
|
19
|
+
export async function analyzeProjectMeta(projectRoot, configFiles, relativePaths) {
|
|
20
|
+
const pkg = configFiles.packageJson;
|
|
21
|
+
const meta = {
|
|
22
|
+
scripts: [],
|
|
23
|
+
engines: null,
|
|
24
|
+
workspaces: null,
|
|
25
|
+
packageVersion: null,
|
|
26
|
+
entryPoints: [],
|
|
27
|
+
envVarNames: [],
|
|
28
|
+
dependenciesSummary: { production: 0, development: 0, topProduction: [] },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (pkg) {
|
|
32
|
+
meta.packageVersion = pkg.version || null;
|
|
33
|
+
if (pkg.engines) {
|
|
34
|
+
meta.engines = Object.entries(pkg.engines)
|
|
35
|
+
.map(([k, v]) => `${k} ${v}`)
|
|
36
|
+
.join(', ');
|
|
37
|
+
}
|
|
38
|
+
if (pkg.workspaces) {
|
|
39
|
+
meta.workspaces = Array.isArray(pkg.workspaces)
|
|
40
|
+
? pkg.workspaces.join(', ')
|
|
41
|
+
: typeof pkg.workspaces === 'object'
|
|
42
|
+
? JSON.stringify(pkg.workspaces)
|
|
43
|
+
: String(pkg.workspaces);
|
|
44
|
+
}
|
|
45
|
+
if (pkg.scripts) {
|
|
46
|
+
meta.scripts = Object.entries(pkg.scripts).map(([name, cmd]) => ({
|
|
47
|
+
name,
|
|
48
|
+
command: String(cmd),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
const prod = Object.keys(pkg.dependencies || {});
|
|
52
|
+
const dev = Object.keys(pkg.devDependencies || {});
|
|
53
|
+
meta.dependenciesSummary = {
|
|
54
|
+
production: prod.length,
|
|
55
|
+
development: dev.length,
|
|
56
|
+
topProduction: prod.slice(0, 20),
|
|
57
|
+
};
|
|
58
|
+
if (pkg.main) meta.entryPoints.push({ type: 'main', path: pkg.main });
|
|
59
|
+
if (pkg.bin) {
|
|
60
|
+
const bins =
|
|
61
|
+
typeof pkg.bin === 'string'
|
|
62
|
+
? [{ name: pkg.name || 'cli', path: pkg.bin }]
|
|
63
|
+
: Object.entries(pkg.bin).map(([name, p]) => ({ name, path: p }));
|
|
64
|
+
meta.entryPoints.push(...bins.map((b) => ({ type: 'bin', ...b })));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const rel of relativePaths) {
|
|
69
|
+
if (ENTRY_PATTERNS.some((re) => re.test(rel))) {
|
|
70
|
+
if (!meta.entryPoints.some((e) => e.path === rel)) {
|
|
71
|
+
meta.entryPoints.push({ type: 'detected', path: rel });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const envExamplePath = path.join(projectRoot, '.env.example');
|
|
77
|
+
if (await fs.pathExists(envExamplePath)) {
|
|
78
|
+
try {
|
|
79
|
+
const content = await fs.readFile(envExamplePath, 'utf8');
|
|
80
|
+
meta.envVarNames = parseEnvKeys(content);
|
|
81
|
+
} catch {
|
|
82
|
+
/* skip */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return meta;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseEnvKeys(content) {
|
|
90
|
+
const keys = [];
|
|
91
|
+
for (const line of content.split('\n')) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
94
|
+
const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
95
|
+
if (m) keys.push(m[1]);
|
|
96
|
+
}
|
|
97
|
+
return [...new Set(keys)].slice(0, 40);
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadChangesHistory } from '../../analyzers/changelog.js';
|
|
2
|
+
import { isInitialized } from '../../core/config.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getContextForgeRoot } from '../../core/paths.js';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
|
|
7
|
+
export async function changesCommand(projectRoot, options = {}) {
|
|
8
|
+
if (!(await isInitialized(projectRoot))) {
|
|
9
|
+
console.error('ContextForge not initialized. Run: npx contextforge init');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const limit = options.limit || 10;
|
|
14
|
+
const history = await loadChangesHistory(projectRoot, limit);
|
|
15
|
+
const changesMd = path.join(getContextForgeRoot(projectRoot), 'CHANGES.md');
|
|
16
|
+
|
|
17
|
+
console.log('\nContextForge — Change history\n');
|
|
18
|
+
|
|
19
|
+
if (history.runs.length === 0) {
|
|
20
|
+
console.log(' No runs recorded yet. Run: npx contextforge generate');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const run of history.runs) {
|
|
25
|
+
const when = run.runAt?.slice(0, 19).replace('T', ' ') || '?';
|
|
26
|
+
const icon = run.hasChanges ? '~' : '=';
|
|
27
|
+
console.log(` ${icon} ${when} ${run.summary}`);
|
|
28
|
+
if (run.added || run.modified || run.removed) {
|
|
29
|
+
console.log(` files: +${run.added} ~${run.modified} -${run.removed}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (await fs.pathExists(changesMd)) {
|
|
34
|
+
console.log(`\n Full log: .contextforge/CHANGES.md\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|