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,117 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
pattern: 'mvc',
|
|
4
|
+
label: 'MVC',
|
|
5
|
+
confidence: 'high',
|
|
6
|
+
test: (dirs) =>
|
|
7
|
+
dirs.has('controllers') && dirs.has('models') && (dirs.has('routes') || dirs.has('views')),
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
pattern: 'feature-based',
|
|
11
|
+
label: 'Feature-based',
|
|
12
|
+
confidence: 'high',
|
|
13
|
+
test: (dirs) => dirs.has('features') || dirs.has('modules'),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
pattern: 'next-app-router',
|
|
17
|
+
label: 'Next.js App Router',
|
|
18
|
+
confidence: 'high',
|
|
19
|
+
test: (dirs, paths) =>
|
|
20
|
+
dirs.has('app') && paths.some((p) => p.startsWith('app/') && /layout\.(tsx?|jsx?)$/.test(p)),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: 'pages-router',
|
|
24
|
+
label: 'Next.js Pages Router',
|
|
25
|
+
confidence: 'medium',
|
|
26
|
+
test: (dirs) => dirs.has('pages') && !dirs.has('app'),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: 'layered',
|
|
30
|
+
label: 'Layered (services/controllers)',
|
|
31
|
+
confidence: 'medium',
|
|
32
|
+
test: (dirs) =>
|
|
33
|
+
(dirs.has('services') && dirs.has('controllers')) ||
|
|
34
|
+
(dirs.has('services') && dirs.has('routes')),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
pattern: 'monolith',
|
|
38
|
+
label: 'Monolithic single-package',
|
|
39
|
+
confidence: 'low',
|
|
40
|
+
test: (dirs) => dirs.has('src') && !dirs.has('packages'),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: 'microservices',
|
|
44
|
+
label: 'Multi-package / microservices',
|
|
45
|
+
confidence: 'medium',
|
|
46
|
+
test: (_dirs, paths) => {
|
|
47
|
+
const servicePackages = paths.filter(
|
|
48
|
+
(p) => /^services\/[^/]+\/package\.json$/i.test(p) || /^packages\/[^/]+\/package\.json$/i.test(p)
|
|
49
|
+
);
|
|
50
|
+
return servicePackages.length >= 2;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: 'clean-architecture',
|
|
55
|
+
label: 'Clean Architecture',
|
|
56
|
+
confidence: 'medium',
|
|
57
|
+
test: (dirs) =>
|
|
58
|
+
dirs.has('domain') || dirs.has('use-cases') || dirs.has('infrastructure'),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string[]} relativePaths
|
|
64
|
+
*/
|
|
65
|
+
export function analyzeArchitecture(relativePaths) {
|
|
66
|
+
const dirs = new Set();
|
|
67
|
+
const normalized = relativePaths.map((p) => p.replace(/\\/g, '/'));
|
|
68
|
+
|
|
69
|
+
for (const p of normalized) {
|
|
70
|
+
const top = p.split('/')[0];
|
|
71
|
+
if (top && !top.includes('.')) dirs.add(top);
|
|
72
|
+
const parts = p.split('/');
|
|
73
|
+
if (parts.length >= 2 && !parts[1].includes('.')) {
|
|
74
|
+
dirs.add(parts[1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const matches = [];
|
|
79
|
+
|
|
80
|
+
for (const { pattern, label, confidence, test } of PATTERNS) {
|
|
81
|
+
if (test(dirs, normalized)) {
|
|
82
|
+
matches.push({ pattern, label, confidence });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const primary = matches[0] || {
|
|
87
|
+
pattern: 'unknown',
|
|
88
|
+
label: 'Standard src-based layout',
|
|
89
|
+
confidence: 'low',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const folderHierarchy = [...dirs].sort().slice(0, 20);
|
|
93
|
+
|
|
94
|
+
let apiFlow = null;
|
|
95
|
+
if (matches.some((m) => m.pattern === 'mvc' || m.pattern === 'layered')) {
|
|
96
|
+
apiFlow = 'Client → Route → Controller → Service → Database';
|
|
97
|
+
} else if (matches.some((m) => m.pattern === 'next-app-router')) {
|
|
98
|
+
apiFlow = 'Browser → app/ routes → Server Components / API routes → Data layer';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
primary: {
|
|
103
|
+
pattern: primary.pattern,
|
|
104
|
+
label: primary.label,
|
|
105
|
+
confidence: primary.confidence,
|
|
106
|
+
displayLabel: `Likely ${primary.label}`,
|
|
107
|
+
},
|
|
108
|
+
allMatches: matches.map((m) => ({
|
|
109
|
+
...m,
|
|
110
|
+
displayLabel: `Likely ${m.label}`,
|
|
111
|
+
})),
|
|
112
|
+
folderHierarchy,
|
|
113
|
+
apiFlow,
|
|
114
|
+
dependencyFlow: null,
|
|
115
|
+
signals: matches.map((m) => m.pattern),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {ReturnType<import('./tech-stack.js').detectTechStack>} techStack
|
|
3
|
+
* @param {Awaited<ReturnType<import('../scanner/config-files.js').findConfigFiles>>} configFiles
|
|
4
|
+
* @param {string[]} relativePaths
|
|
5
|
+
*/
|
|
6
|
+
export function detectAuthAndDeploy(techStack, configFiles, relativePaths) {
|
|
7
|
+
const auth = [...(techStack.auth || [])];
|
|
8
|
+
const deploy = [...(techStack.deploy || [])];
|
|
9
|
+
const hints = [];
|
|
10
|
+
|
|
11
|
+
const paths = relativePaths.map((p) => p.replace(/\\/g, '/').toLowerCase());
|
|
12
|
+
|
|
13
|
+
if (paths.some((p) => p.includes('middleware') && p.includes('auth'))) {
|
|
14
|
+
hints.push('Auth middleware detected in project structure');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (paths.some((p) => p.includes('passport'))) {
|
|
18
|
+
if (!auth.includes('Passport')) auth.push('Passport');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (paths.some((p) => /vercel\.json/i.test(p))) {
|
|
22
|
+
deploy.push('Vercel');
|
|
23
|
+
hints.push('vercel.json configuration found');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (paths.some((p) => /netlify\.toml/i.test(p))) {
|
|
27
|
+
deploy.push('Netlify');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (configFiles.parsed.dockerfile || configFiles.parsed.dockerCompose) {
|
|
31
|
+
if (!deploy.includes('Docker')) deploy.push('Docker');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (configFiles.parsed.nginx) {
|
|
35
|
+
if (!deploy.includes('Nginx')) deploy.push('Nginx');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sessionPatterns = paths.filter((p) =>
|
|
39
|
+
p.includes('session') || p.includes('cookie')
|
|
40
|
+
);
|
|
41
|
+
if (sessionPatterns.length && !auth.length) {
|
|
42
|
+
auth.push('Session-based auth (possible)');
|
|
43
|
+
hints.push('Session/cookie related files detected');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
authentication: [...new Set(auth)],
|
|
48
|
+
deployment: [...new Set(deploy)],
|
|
49
|
+
hints,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function parsePrismaSchema(content) {
|
|
5
|
+
const models = [];
|
|
6
|
+
const enums = [];
|
|
7
|
+
let provider = null;
|
|
8
|
+
|
|
9
|
+
const datasourceMatch = content.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"/s);
|
|
10
|
+
if (datasourceMatch) provider = datasourceMatch[1];
|
|
11
|
+
|
|
12
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]*)\}/gs;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = modelRegex.exec(content)) !== null) {
|
|
15
|
+
const name = match[1];
|
|
16
|
+
const body = match[2];
|
|
17
|
+
const fields = [];
|
|
18
|
+
for (const line of body.split('\n')) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
21
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+/);
|
|
22
|
+
if (fieldMatch && !['model', 'enum'].includes(fieldMatch[1])) {
|
|
23
|
+
fields.push(fieldMatch[1]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const relationFields = [];
|
|
27
|
+
for (const line of body.split('\n')) {
|
|
28
|
+
const relMatch = line.match(/(\w+)\s+.*@relation/);
|
|
29
|
+
if (relMatch) relationFields.push(relMatch[1]);
|
|
30
|
+
}
|
|
31
|
+
const relations = body.match(/@relation/g) || [];
|
|
32
|
+
models.push({
|
|
33
|
+
name,
|
|
34
|
+
fields: fields.slice(0, 15),
|
|
35
|
+
relationCount: relations.length,
|
|
36
|
+
relations: relationFields,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const enumRegex = /enum\s+(\w+)/g;
|
|
41
|
+
let enumMatch;
|
|
42
|
+
while ((enumMatch = enumRegex.exec(content)) !== null) {
|
|
43
|
+
enums.push(enumMatch[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { provider, models, enums };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseMongooseStub(content, filename) {
|
|
50
|
+
const schemaMatch = content.match(/mongoose\.Schema\s*\(\s*\{([^}]+)\}/s);
|
|
51
|
+
if (!schemaMatch) return null;
|
|
52
|
+
const fields = [...schemaMatch[1].matchAll(/(\w+)\s*:/g)].map((m) => m[1]);
|
|
53
|
+
const name = path.basename(filename).replace(/\.(js|ts)$/, '');
|
|
54
|
+
return { name, fields: fields.slice(0, 15), source: filename };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} projectRoot
|
|
59
|
+
* @param {string[]} relativePaths
|
|
60
|
+
*/
|
|
61
|
+
export async function extractDatabaseInfo(projectRoot, relativePaths) {
|
|
62
|
+
const result = {
|
|
63
|
+
orm: null,
|
|
64
|
+
provider: null,
|
|
65
|
+
models: [],
|
|
66
|
+
enums: [],
|
|
67
|
+
migrations: [],
|
|
68
|
+
textDiagram: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const prismaPaths = relativePaths.filter((p) =>
|
|
72
|
+
p.replace(/\\/g, '/').endsWith('schema.prisma')
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
for (const rel of prismaPaths) {
|
|
76
|
+
const full = path.join(projectRoot, rel);
|
|
77
|
+
try {
|
|
78
|
+
const content = await fs.readFile(full, 'utf8');
|
|
79
|
+
const parsed = parsePrismaSchema(content);
|
|
80
|
+
result.orm = 'Prisma';
|
|
81
|
+
result.provider = parsed.provider;
|
|
82
|
+
result.models.push(...parsed.models);
|
|
83
|
+
result.enums.push(...parsed.enums);
|
|
84
|
+
} catch {
|
|
85
|
+
/* skip */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const migrationDirs = relativePaths.filter((p) =>
|
|
90
|
+
/migrations?\/|prisma\/migrations/i.test(p)
|
|
91
|
+
);
|
|
92
|
+
result.migrations = migrationDirs.slice(0, 10);
|
|
93
|
+
|
|
94
|
+
const modelFiles = relativePaths.filter((p) =>
|
|
95
|
+
/models?\/.*\.(js|ts)$/i.test(p) && !p.includes('node_modules')
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
for (const rel of modelFiles.slice(0, 15)) {
|
|
99
|
+
const full = path.join(projectRoot, rel);
|
|
100
|
+
try {
|
|
101
|
+
const content = await fs.readFile(full, 'utf8');
|
|
102
|
+
if (content.includes('mongoose')) {
|
|
103
|
+
const stub = parseMongooseStub(content, rel);
|
|
104
|
+
if (stub) {
|
|
105
|
+
result.orm = result.orm || 'Mongoose';
|
|
106
|
+
result.models.push(stub);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
/* skip */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (result.models.length > 0) {
|
|
115
|
+
result.textDiagram = result.models
|
|
116
|
+
.map((m) => {
|
|
117
|
+
const lines = (m.fields || []).map((f) => ` ├── ${f}`);
|
|
118
|
+
if (m.relations?.length) {
|
|
119
|
+
lines.push(` └── relations: ${m.relations.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
return `${m.name}\n${lines.join('\n') || ' ├── (fields)'}`;
|
|
122
|
+
})
|
|
123
|
+
.join('\n\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const DEP_MAP = {
|
|
2
|
+
react: { label: 'React', category: 'frontend' },
|
|
3
|
+
'react-dom': { label: 'React', category: 'frontend' },
|
|
4
|
+
next: { label: 'Next.js', category: 'frontend' },
|
|
5
|
+
vue: { label: 'Vue', category: 'frontend' },
|
|
6
|
+
nuxt: { label: 'Nuxt', category: 'frontend' },
|
|
7
|
+
'@angular/core': { label: 'Angular', category: 'frontend' },
|
|
8
|
+
vite: { label: 'Vite', category: 'frontend' },
|
|
9
|
+
express: { label: 'Express.js', category: 'backend' },
|
|
10
|
+
'@nestjs/core': { label: 'NestJS', category: 'backend' },
|
|
11
|
+
fastify: { label: 'Fastify', category: 'backend' },
|
|
12
|
+
koa: { label: 'Koa', category: 'backend' },
|
|
13
|
+
mongoose: { label: 'Mongoose', category: 'database' },
|
|
14
|
+
mongodb: { label: 'MongoDB', category: 'database' },
|
|
15
|
+
pg: { label: 'PostgreSQL', category: 'database' },
|
|
16
|
+
mysql2: { label: 'MySQL', category: 'database' },
|
|
17
|
+
'better-sqlite3': { label: 'SQLite', category: 'database' },
|
|
18
|
+
ioredis: { label: 'Redis', category: 'database' },
|
|
19
|
+
redis: { label: 'Redis', category: 'database' },
|
|
20
|
+
'@prisma/client': { label: 'Prisma', category: 'orm' },
|
|
21
|
+
prisma: { label: 'Prisma', category: 'orm' },
|
|
22
|
+
sequelize: { label: 'Sequelize', category: 'orm' },
|
|
23
|
+
typeorm: { label: 'TypeORM', category: 'orm' },
|
|
24
|
+
tailwindcss: { label: 'TailwindCSS', category: 'styling' },
|
|
25
|
+
bootstrap: { label: 'Bootstrap', category: 'styling' },
|
|
26
|
+
'@mui/material': { label: 'Material UI', category: 'styling' },
|
|
27
|
+
'@chakra-ui/react': { label: 'Chakra UI', category: 'styling' },
|
|
28
|
+
'@reduxjs/toolkit': { label: 'Redux Toolkit', category: 'state' },
|
|
29
|
+
redux: { label: 'Redux', category: 'state' },
|
|
30
|
+
zustand: { label: 'Zustand', category: 'state' },
|
|
31
|
+
mobx: { label: 'MobX', category: 'state' },
|
|
32
|
+
jsonwebtoken: { label: 'JWT', category: 'auth' },
|
|
33
|
+
passport: { label: 'Passport', category: 'auth' },
|
|
34
|
+
'@clerk/clerk-sdk-node': { label: 'Clerk', category: 'auth' },
|
|
35
|
+
'@auth0/auth0-react': { label: 'Auth0', category: 'auth' },
|
|
36
|
+
'firebase-admin': { label: 'Firebase Auth', category: 'auth' },
|
|
37
|
+
axios: { label: 'Axios', category: 'http' },
|
|
38
|
+
'simple-git': { label: 'Git (simple-git)', category: 'backend' },
|
|
39
|
+
'fs-extra': { label: 'fs-extra', category: 'backend' },
|
|
40
|
+
glob: { label: 'glob', category: 'backend' },
|
|
41
|
+
commander: { label: 'CLI (commander)', category: 'backend' },
|
|
42
|
+
chokidar: { label: 'File watcher (chokidar)', category: 'backend' },
|
|
43
|
+
vitest: { label: 'Vitest', category: 'testing' },
|
|
44
|
+
jest: { label: 'Jest', category: 'testing' },
|
|
45
|
+
mocha: { label: 'Mocha', category: 'testing' },
|
|
46
|
+
'@playwright/test': { label: 'Playwright', category: 'testing' },
|
|
47
|
+
cypress: { label: 'Cypress', category: 'testing' },
|
|
48
|
+
'react-router-dom': { label: 'React Router', category: 'frontend' },
|
|
49
|
+
'@tanstack/react-query': { label: 'TanStack Query', category: 'frontend' },
|
|
50
|
+
hono: { label: 'Hono', category: 'backend' },
|
|
51
|
+
'@hono/node-server': { label: 'Hono', category: 'backend' },
|
|
52
|
+
'socket.io': { label: 'Socket.io', category: 'backend' },
|
|
53
|
+
ws: { label: 'WebSocket (ws)', category: 'backend' },
|
|
54
|
+
cors: { label: 'CORS', category: 'backend' },
|
|
55
|
+
bcryptjs: { label: 'bcrypt', category: 'auth' },
|
|
56
|
+
bcrypt: { label: 'bcrypt', category: 'auth' },
|
|
57
|
+
'cookie-parser': { label: 'Cookie sessions', category: 'auth' },
|
|
58
|
+
'express-session': { label: 'Express sessions', category: 'auth' },
|
|
59
|
+
supabase: { label: 'Supabase', category: 'database' },
|
|
60
|
+
'@supabase/supabase-js': { label: 'Supabase', category: 'database' },
|
|
61
|
+
firebase: { label: 'Firebase', category: 'database' },
|
|
62
|
+
'drizzle-orm': { label: 'Drizzle ORM', category: 'orm' },
|
|
63
|
+
'@drizzle-team/kit': { label: 'Drizzle ORM', category: 'orm' },
|
|
64
|
+
knex: { label: 'Knex', category: 'orm' },
|
|
65
|
+
'react-native': { label: 'React Native', category: 'frontend' },
|
|
66
|
+
electron: { label: 'Electron', category: 'frontend' },
|
|
67
|
+
svelte: { label: 'Svelte', category: 'frontend' },
|
|
68
|
+
'@sveltejs/kit': { label: 'SvelteKit', category: 'frontend' },
|
|
69
|
+
remix: { label: 'Remix', category: 'frontend' },
|
|
70
|
+
'@remix-run/node': { label: 'Remix', category: 'frontend' },
|
|
71
|
+
turbo: { label: 'Turborepo', category: 'deploy' },
|
|
72
|
+
nx: { label: 'Nx monorepo', category: 'deploy' },
|
|
73
|
+
lerna: { label: 'Lerna monorepo', category: 'deploy' },
|
|
74
|
+
dotenv: { label: 'dotenv', category: 'backend' },
|
|
75
|
+
openai: { label: 'OpenAI SDK', category: 'backend' },
|
|
76
|
+
'@anthropic-ai/sdk': { label: 'Anthropic SDK', category: 'backend' },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const CONFIG_SIGNALS = [
|
|
80
|
+
{ key: 'vite', label: 'Vite', category: 'frontend' },
|
|
81
|
+
{ key: 'next', label: 'Next.js', category: 'frontend' },
|
|
82
|
+
{ key: 'tailwind', label: 'TailwindCSS', category: 'styling' },
|
|
83
|
+
{ key: 'prisma', label: 'Prisma', category: 'orm' },
|
|
84
|
+
{ key: 'dockerfile', label: 'Docker', category: 'deploy' },
|
|
85
|
+
{ key: 'dockerCompose', label: 'Docker Compose', category: 'deploy' },
|
|
86
|
+
{ key: 'nginx', label: 'Nginx', category: 'deploy' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
function addUnique(arr, item) {
|
|
90
|
+
if (!arr.includes(item)) arr.push(item);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {import('../scanner/config-files.js').findConfigFiles extends Function ? Awaited<ReturnType<typeof import('../scanner/config-files.js').findConfigFiles>> : never} configFiles
|
|
95
|
+
* @param {string[]} relativePaths
|
|
96
|
+
*/
|
|
97
|
+
export function detectTechStack(configFiles, relativePaths) {
|
|
98
|
+
const stack = {
|
|
99
|
+
frontend: [],
|
|
100
|
+
backend: [],
|
|
101
|
+
database: [],
|
|
102
|
+
orm: [],
|
|
103
|
+
styling: [],
|
|
104
|
+
state: [],
|
|
105
|
+
auth: [],
|
|
106
|
+
deploy: [],
|
|
107
|
+
http: [],
|
|
108
|
+
testing: [],
|
|
109
|
+
confidence: 'medium',
|
|
110
|
+
signals: [],
|
|
111
|
+
packageJsonCount: 0,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const pkg = configFiles.packageJson;
|
|
115
|
+
const allDeps = { ...(configFiles.mergedDependencies || {}) };
|
|
116
|
+
|
|
117
|
+
if (!Object.keys(allDeps).length && pkg) {
|
|
118
|
+
Object.assign(allDeps, pkg.dependencies || {}, pkg.devDependencies || {}, pkg.peerDependencies || {});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const pkgJsonPaths = relativePaths.filter((p) =>
|
|
122
|
+
p.replace(/\\/g, '/').endsWith('package.json')
|
|
123
|
+
);
|
|
124
|
+
stack.packageJsonCount = Math.max(1, pkgJsonPaths.length);
|
|
125
|
+
|
|
126
|
+
if (pkg) {
|
|
127
|
+
if (pkg.name) stack.projectName = pkg.name;
|
|
128
|
+
if (pkg.description) stack.description = pkg.description;
|
|
129
|
+
stack.confidence = 'high';
|
|
130
|
+
|
|
131
|
+
if (pkg.bin || pkg.type === 'module') {
|
|
132
|
+
addUnique(stack.backend, 'Node.js');
|
|
133
|
+
stack.signals.push('package:node-cli');
|
|
134
|
+
}
|
|
135
|
+
if (pkg.engines?.node) {
|
|
136
|
+
addUnique(stack.backend, 'Node.js');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [dep, meta] of Object.entries(DEP_MAP)) {
|
|
141
|
+
if (allDeps[dep]) {
|
|
142
|
+
const cat = meta.category in stack ? meta.category : 'backend';
|
|
143
|
+
addUnique(stack[cat], meta.label);
|
|
144
|
+
stack.signals.push(`dependency:${dep}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const depName of Object.keys(allDeps)) {
|
|
149
|
+
if (DEP_MAP[depName]) continue;
|
|
150
|
+
const lower = depName.toLowerCase();
|
|
151
|
+
if (/^@types\//.test(depName)) continue;
|
|
152
|
+
if (lower.includes('eslint') || lower.includes('prettier')) continue;
|
|
153
|
+
if (lower.includes('typescript') && lower !== 'typescript') continue;
|
|
154
|
+
stack.signals.push(`dependency:unmapped:${depName}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const { key, label, category } of CONFIG_SIGNALS) {
|
|
158
|
+
if (configFiles.parsed[key]) {
|
|
159
|
+
const cat = category in stack ? category : 'frontend';
|
|
160
|
+
addUnique(stack[cat], label);
|
|
161
|
+
stack.signals.push(`config:${key}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const normalizedPaths = relativePaths.map((p) => p.replace(/\\/g, '/'));
|
|
166
|
+
if (normalizedPaths.some((p) => p.startsWith('app/') && p.includes('layout.'))) {
|
|
167
|
+
addUnique(stack.frontend, 'Next.js App Router');
|
|
168
|
+
stack.signals.push('folder:app-router');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (stack.frontend.length && stack.backend.length) {
|
|
172
|
+
stack.confidence = 'high';
|
|
173
|
+
} else if (stack.frontend.length || stack.backend.length) {
|
|
174
|
+
stack.confidence = 'medium';
|
|
175
|
+
} else {
|
|
176
|
+
stack.confidence = 'low';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return stack;
|
|
180
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { getOutputPath, OUTPUT_FILES } from '../core/paths.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} projectRoot
|
|
6
|
+
* @param {ReturnType<import('./context-json.js').buildContextSnapshot>} snapshot
|
|
7
|
+
*/
|
|
8
|
+
export async function writeArtifacts(projectRoot, snapshot) {
|
|
9
|
+
await fs.writeJson(
|
|
10
|
+
getOutputPath(projectRoot, OUTPUT_FILES.contextJson),
|
|
11
|
+
snapshot,
|
|
12
|
+
{ spaces: 2 }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
await fs.writeJson(
|
|
16
|
+
getOutputPath(projectRoot, OUTPUT_FILES.architectureJson),
|
|
17
|
+
{
|
|
18
|
+
generatedAt: snapshot.generatedAt,
|
|
19
|
+
primary: snapshot.architecture.primary,
|
|
20
|
+
allMatches: snapshot.architecture.allMatches,
|
|
21
|
+
apiFlow: snapshot.architecture.apiFlow,
|
|
22
|
+
folderHierarchy: snapshot.architecture.folderHierarchy,
|
|
23
|
+
},
|
|
24
|
+
{ spaces: 2 }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
await fs.writeJson(
|
|
28
|
+
getOutputPath(projectRoot, OUTPUT_FILES.gitInsightsJson),
|
|
29
|
+
{
|
|
30
|
+
generatedAt: snapshot.generatedAt,
|
|
31
|
+
...snapshot.git,
|
|
32
|
+
},
|
|
33
|
+
{ spaces: 2 }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await fs.writeJson(
|
|
37
|
+
getOutputPath(projectRoot, OUTPUT_FILES.bugsReportJson),
|
|
38
|
+
{
|
|
39
|
+
generatedAt: snapshot.generatedAt,
|
|
40
|
+
...snapshot.bugs,
|
|
41
|
+
},
|
|
42
|
+
{ spaces: 2 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getContextForgeRoot } from '../core/paths.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Write standalone CHANGES.md for change tracking over time.
|
|
7
|
+
*/
|
|
8
|
+
export async function writeChangesMarkdown(projectRoot, changelog, history) {
|
|
9
|
+
const outPath = path.join(getContextForgeRoot(projectRoot), 'CHANGES.md');
|
|
10
|
+
|
|
11
|
+
let md = `# ContextForge — Change Log
|
|
12
|
+
|
|
13
|
+
> Auto-updated when you run \`generate\` or \`watch\`.
|
|
14
|
+
> Last run: **${changelog.runAt}**
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Latest run
|
|
19
|
+
|
|
20
|
+
**Status:** ${changelog.hasChanges ? 'Changes detected' : 'No changes'}
|
|
21
|
+
|
|
22
|
+
${changelog.summary}
|
|
23
|
+
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
if (changelog.previousRunAt) {
|
|
27
|
+
md += `**Previous run:** ${changelog.previousRunAt}\n\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (changelog.addedFiles?.length) {
|
|
31
|
+
md += `### New files (${changelog.addedFiles.length})\n\n`;
|
|
32
|
+
md += changelog.addedFiles.slice(0, 30).map((f) => `- \`${f}\``).join('\n');
|
|
33
|
+
if (changelog.addedFiles.length > 30) md += `\n- _...and ${changelog.addedFiles.length - 30} more_`;
|
|
34
|
+
md += '\n\n';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (changelog.modifiedFiles?.length) {
|
|
38
|
+
md += `### Modified files (${changelog.modifiedFiles.length})\n\n`;
|
|
39
|
+
md += changelog.modifiedFiles.slice(0, 30).map((f) => `- \`${f}\``).join('\n');
|
|
40
|
+
if (changelog.modifiedFiles.length > 30) md += `\n- _...and ${changelog.modifiedFiles.length - 30} more_`;
|
|
41
|
+
md += '\n\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (changelog.removedFiles?.length) {
|
|
45
|
+
md += `### Removed files (${changelog.removedFiles.length})\n\n`;
|
|
46
|
+
md += changelog.removedFiles.slice(0, 20).map((f) => `- \`${f}\``).join('\n');
|
|
47
|
+
md += '\n\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (changelog.techStackChanged?.length) {
|
|
51
|
+
md += `### Tech stack\n\n${changelog.techStackChanged.map((t) => `- ${t}`).join('\n')}\n\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (changelog.architectureChanged) {
|
|
55
|
+
md += `### Architecture\n\n- ${changelog.architectureChanged}\n\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (history?.runs?.length > 1) {
|
|
59
|
+
md += `## Run history\n\n| When | Changed? | Summary |\n|------|----------|----------|\n`;
|
|
60
|
+
for (const run of history.runs.slice(0, 15)) {
|
|
61
|
+
const when = run.runAt?.slice(0, 19).replace('T', ' ') || '';
|
|
62
|
+
const flag = run.hasChanges ? 'Yes' : 'No';
|
|
63
|
+
md += `| ${when} | ${flag} | ${(run.summary || '').replace(/\|/g, '/').slice(0, 60)} |\n`;
|
|
64
|
+
}
|
|
65
|
+
md += '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
md += `---\n\nRegenerate: \`npx contextforge generate\` · Watch: \`npx contextforge watch\`\n`;
|
|
69
|
+
|
|
70
|
+
await fs.writeFile(outPath, md, 'utf8');
|
|
71
|
+
return outPath;
|
|
72
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const VERSION = '1.0.0';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {object} data
|
|
5
|
+
*/
|
|
6
|
+
export function buildContextSnapshot(data) {
|
|
7
|
+
const {
|
|
8
|
+
projectRoot,
|
|
9
|
+
config,
|
|
10
|
+
scanResult,
|
|
11
|
+
configFiles,
|
|
12
|
+
techStack,
|
|
13
|
+
architecture,
|
|
14
|
+
database,
|
|
15
|
+
authDeploy,
|
|
16
|
+
folderTree,
|
|
17
|
+
git,
|
|
18
|
+
dependencies,
|
|
19
|
+
codeInsights,
|
|
20
|
+
bugs,
|
|
21
|
+
aiEnrichment,
|
|
22
|
+
ast,
|
|
23
|
+
changelog,
|
|
24
|
+
confidence,
|
|
25
|
+
projectMeta,
|
|
26
|
+
astFull,
|
|
27
|
+
} = data;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
version: VERSION,
|
|
31
|
+
generatedAt: new Date().toISOString(),
|
|
32
|
+
projectRoot,
|
|
33
|
+
projectName: techStack.projectName || pathBasename(projectRoot),
|
|
34
|
+
description: techStack.description || null,
|
|
35
|
+
techStack: {
|
|
36
|
+
frontend: techStack.frontend,
|
|
37
|
+
backend: techStack.backend,
|
|
38
|
+
database: techStack.database,
|
|
39
|
+
orm: techStack.orm,
|
|
40
|
+
styling: techStack.styling,
|
|
41
|
+
state: techStack.state,
|
|
42
|
+
auth: techStack.auth,
|
|
43
|
+
deploy: techStack.deploy,
|
|
44
|
+
http: techStack.http,
|
|
45
|
+
testing: techStack.testing || [],
|
|
46
|
+
confidence: techStack.confidence,
|
|
47
|
+
signals: techStack.signals,
|
|
48
|
+
packageJsonCount: techStack.packageJsonCount || 1,
|
|
49
|
+
},
|
|
50
|
+
architecture: {
|
|
51
|
+
primary: architecture.primary,
|
|
52
|
+
allMatches: architecture.allMatches,
|
|
53
|
+
folderHierarchy: architecture.folderHierarchy,
|
|
54
|
+
apiFlow: architecture.apiFlow,
|
|
55
|
+
dependencyFlow: architecture.dependencyFlow,
|
|
56
|
+
signals: architecture.signals,
|
|
57
|
+
},
|
|
58
|
+
folderTree,
|
|
59
|
+
database,
|
|
60
|
+
authDeploy,
|
|
61
|
+
git,
|
|
62
|
+
dependencies,
|
|
63
|
+
codeInsights,
|
|
64
|
+
bugs,
|
|
65
|
+
aiEnrichment: aiEnrichment || null,
|
|
66
|
+
ast: astFull || (ast ? { available: ast.available, routesCount: ast.routes?.length, filesParsed: ast.filesParsed, routes: ast.routes?.slice(0, 50) } : null),
|
|
67
|
+
projectMeta: projectMeta || null,
|
|
68
|
+
changelog: changelog || null,
|
|
69
|
+
confidence: confidence || null,
|
|
70
|
+
detectionQuality: data.detectionQuality || null,
|
|
71
|
+
configFiles: configFiles.paths,
|
|
72
|
+
filesScanned: scanResult.files.length,
|
|
73
|
+
filesSignature: data.filesSignature || null,
|
|
74
|
+
projectDirs: scanResult.projectDirs || [],
|
|
75
|
+
config: {
|
|
76
|
+
watch: config.watch,
|
|
77
|
+
detectBugs: config.detectBugs,
|
|
78
|
+
includeGitHistory: config.includeGitHistory,
|
|
79
|
+
ai: {
|
|
80
|
+
enabled: config.ai?.enabled,
|
|
81
|
+
provider: config.ai?.provider,
|
|
82
|
+
background: config.ai?.background,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pathBasename(p) {
|
|
89
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
90
|
+
return parts[parts.length - 1] || 'project';
|
|
91
|
+
}
|