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,256 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig, ensureContextForgeDirs } from './config.js';
|
|
4
|
+
import { getLogsDir } from './paths.js';
|
|
5
|
+
import { scanRepository } from '../scanner/repo-scanner.js';
|
|
6
|
+
import { loadFileIndex, saveFileIndex, updateFileIndex } from '../scanner/file-index.js';
|
|
7
|
+
import { findConfigFiles } from '../scanner/config-files.js';
|
|
8
|
+
import { mergePackageDependencies } from '../scanner/package-deps.js';
|
|
9
|
+
import { detectTechStack } from '../detectors/tech-stack.js';
|
|
10
|
+
import { analyzeArchitecture } from '../detectors/architecture.js';
|
|
11
|
+
import { extractDatabaseInfo } from '../detectors/database.js';
|
|
12
|
+
import { detectAuthAndDeploy } from '../detectors/auth-deploy.js';
|
|
13
|
+
import { buildFolderTree } from '../analyzers/folder-tree.js';
|
|
14
|
+
import { collectGitInsights } from '../analyzers/git-insights.js';
|
|
15
|
+
import { analyzeDependencies } from '../analyzers/dependencies.js';
|
|
16
|
+
import { analyzeCodeInsights } from '../analyzers/code-insights.js';
|
|
17
|
+
import { runBugsLite } from '../analyzers/bugs-lite.js';
|
|
18
|
+
import { parseSourceFilesAst } from '../analyzers/ast-parser.js';
|
|
19
|
+
import { runEslintIfAvailable } from '../analyzers/eslint-runner.js';
|
|
20
|
+
import { computeChangelog, computeFileDiff, loadChangesHistory } from '../analyzers/changelog.js';
|
|
21
|
+
import { writeChangesMarkdown } from '../generators/changes-markdown.js';
|
|
22
|
+
import { buildContextSnapshot } from '../generators/context-json.js';
|
|
23
|
+
import { writeArtifacts } from '../generators/artifacts.js';
|
|
24
|
+
import { renderContextMarkdown } from '../generators/context-markdown.js';
|
|
25
|
+
import { loadProjectEnv } from './env.js';
|
|
26
|
+
import {
|
|
27
|
+
enrichWithAI,
|
|
28
|
+
isAiAvailable,
|
|
29
|
+
scheduleBackgroundEnrichment,
|
|
30
|
+
loadCachedEnrichment,
|
|
31
|
+
} from '../services/ai/index.js';
|
|
32
|
+
import { computeConfidenceScores } from './confidence.js';
|
|
33
|
+
import { analyzeProjectMeta } from '../analyzers/project-meta.js';
|
|
34
|
+
import { assessDetectionQuality } from '../analyzers/detection-quality.js';
|
|
35
|
+
import {
|
|
36
|
+
computeScanHash,
|
|
37
|
+
computeFilesSignature,
|
|
38
|
+
saveScanHash,
|
|
39
|
+
shouldSkipAi,
|
|
40
|
+
} from './snapshot-hash.js';
|
|
41
|
+
import { installCursorRules } from './cursor-rules.js';
|
|
42
|
+
|
|
43
|
+
function log(projectRoot, message, verbose) {
|
|
44
|
+
if (verbose) {
|
|
45
|
+
console.log(`[contextforge] ${message}`);
|
|
46
|
+
}
|
|
47
|
+
const logFile = path.join(getLogsDir(projectRoot), 'contextforge.log');
|
|
48
|
+
const line = `${new Date().toISOString()} ${message}\n`;
|
|
49
|
+
fs.appendFile(logFile, line).catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} projectRoot
|
|
54
|
+
* @param {{ verbose?: boolean, skipAi?: boolean, forceAi?: boolean }} options
|
|
55
|
+
*/
|
|
56
|
+
export async function runPipeline(projectRoot, options = {}) {
|
|
57
|
+
const { verbose = false, skipAi = false, forceAi = false } = options;
|
|
58
|
+
const projectRootResolved = path.resolve(projectRoot);
|
|
59
|
+
|
|
60
|
+
await loadProjectEnv(projectRootResolved);
|
|
61
|
+
await ensureContextForgeDirs(projectRootResolved);
|
|
62
|
+
const config = await loadConfig(projectRootResolved);
|
|
63
|
+
|
|
64
|
+
if (forceAi) config.ai = { ...config.ai, enabled: true };
|
|
65
|
+
if (skipAi) config.ai = { ...config.ai, enabled: false };
|
|
66
|
+
|
|
67
|
+
log(projectRootResolved, 'Scanning repository...', verbose);
|
|
68
|
+
|
|
69
|
+
const scanResult = await scanRepository(projectRootResolved, config);
|
|
70
|
+
const configFiles = await findConfigFiles(projectRootResolved, scanResult.relativePaths);
|
|
71
|
+
configFiles.mergedDependencies = await mergePackageDependencies(
|
|
72
|
+
projectRootResolved,
|
|
73
|
+
scanResult.relativePaths,
|
|
74
|
+
configFiles.packageJson
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const previousIndex = await loadFileIndex(projectRootResolved);
|
|
78
|
+
const fileDiff = config.trackChanges !== false
|
|
79
|
+
? computeFileDiff(previousIndex, scanResult.files)
|
|
80
|
+
: null;
|
|
81
|
+
const fileIndex = updateFileIndex(previousIndex, scanResult.files);
|
|
82
|
+
await saveFileIndex(projectRootResolved, fileIndex);
|
|
83
|
+
|
|
84
|
+
const techStack = detectTechStack(configFiles, scanResult.relativePaths);
|
|
85
|
+
const architecture = analyzeArchitecture(scanResult.relativePaths);
|
|
86
|
+
const database = await extractDatabaseInfo(projectRootResolved, scanResult.relativePaths);
|
|
87
|
+
const authDeploy = detectAuthAndDeploy(techStack, configFiles, scanResult.relativePaths);
|
|
88
|
+
const folderTree = buildFolderTree(scanResult.relativePaths, config.maxTreeDepth);
|
|
89
|
+
|
|
90
|
+
const ast = await parseSourceFilesAst(projectRootResolved, scanResult.relativePaths);
|
|
91
|
+
|
|
92
|
+
let git = { available: false, commits: [], insights: [] };
|
|
93
|
+
if (config.includeGitHistory) {
|
|
94
|
+
git = await collectGitInsights(projectRootResolved, config.gitCommitLimit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const codeInsights = await analyzeCodeInsights(
|
|
98
|
+
projectRootResolved,
|
|
99
|
+
scanResult,
|
|
100
|
+
configFiles,
|
|
101
|
+
ast
|
|
102
|
+
);
|
|
103
|
+
const projectMeta = await analyzeProjectMeta(
|
|
104
|
+
projectRootResolved,
|
|
105
|
+
configFiles,
|
|
106
|
+
scanResult.relativePaths
|
|
107
|
+
);
|
|
108
|
+
const dependencies = analyzeDependencies(
|
|
109
|
+
projectRootResolved,
|
|
110
|
+
scanResult,
|
|
111
|
+
config,
|
|
112
|
+
codeInsights
|
|
113
|
+
);
|
|
114
|
+
if (!architecture.dependencyFlow && dependencies.chains?.length) {
|
|
115
|
+
architecture.dependencyFlow = dependencies.chains[0];
|
|
116
|
+
}
|
|
117
|
+
if (!architecture.apiFlow && codeInsights.pipelineFlow) {
|
|
118
|
+
architecture.apiFlow = codeInsights.pipelineFlow;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let bugs = { issues: [], summary: 'Bug detection disabled' };
|
|
122
|
+
if (config.detectBugs) {
|
|
123
|
+
bugs = await runBugsLite(projectRootResolved, scanResult, config);
|
|
124
|
+
const eslint = await runEslintIfAvailable(projectRootResolved, scanResult.relativePaths);
|
|
125
|
+
if (eslint.ran && eslint.issues.length) {
|
|
126
|
+
bugs.issues = [...eslint.issues, ...bugs.issues].slice(0, 35);
|
|
127
|
+
bugs.summary = `${bugs.summary}; ${eslint.summary}`;
|
|
128
|
+
} else if (eslint.ran) {
|
|
129
|
+
bugs.summary = eslint.summary;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let aiEnrichment = null;
|
|
134
|
+
|
|
135
|
+
const filesSignature = computeFilesSignature(scanResult.files);
|
|
136
|
+
|
|
137
|
+
const snapshot = buildContextSnapshot({
|
|
138
|
+
projectRoot: projectRootResolved,
|
|
139
|
+
config,
|
|
140
|
+
scanResult,
|
|
141
|
+
filesSignature,
|
|
142
|
+
configFiles,
|
|
143
|
+
techStack,
|
|
144
|
+
architecture,
|
|
145
|
+
database,
|
|
146
|
+
authDeploy,
|
|
147
|
+
folderTree,
|
|
148
|
+
git,
|
|
149
|
+
dependencies,
|
|
150
|
+
codeInsights,
|
|
151
|
+
bugs,
|
|
152
|
+
aiEnrichment,
|
|
153
|
+
ast,
|
|
154
|
+
changelog: null,
|
|
155
|
+
confidence: null,
|
|
156
|
+
detectionQuality: null,
|
|
157
|
+
projectMeta,
|
|
158
|
+
astFull: ast,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
snapshot.changelog = await computeChangelog(
|
|
162
|
+
projectRootResolved,
|
|
163
|
+
snapshot,
|
|
164
|
+
fileDiff
|
|
165
|
+
);
|
|
166
|
+
snapshot.confidence = computeConfidenceScores(snapshot);
|
|
167
|
+
snapshot.detectionQuality = assessDetectionQuality(snapshot);
|
|
168
|
+
|
|
169
|
+
snapshot.filesSignature = filesSignature;
|
|
170
|
+
|
|
171
|
+
const scanHash = computeScanHash(snapshot, fileDiff);
|
|
172
|
+
const skipAiRun = await shouldSkipAi(
|
|
173
|
+
projectRootResolved,
|
|
174
|
+
snapshot,
|
|
175
|
+
forceAi,
|
|
176
|
+
fileDiff
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const cachedAi = await loadCachedEnrichment(projectRootResolved);
|
|
180
|
+
if (cachedAi && !forceAi && skipAiRun) {
|
|
181
|
+
aiEnrichment = cachedAi;
|
|
182
|
+
snapshot.aiEnrichment = cachedAi;
|
|
183
|
+
log(projectRootResolved, 'Using cached AI enrichment (scan unchanged)', verbose);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const aiReady = config.ai?.enabled && isAiAvailable(config);
|
|
187
|
+
|
|
188
|
+
if (aiReady && config.ai.enrichContext && !config.ai.background && !skipAi && !skipAiRun) {
|
|
189
|
+
log(projectRootResolved, `AI enrichment (${config.ai.provider})...`, verbose);
|
|
190
|
+
try {
|
|
191
|
+
aiEnrichment = await enrichWithAI(snapshot, config);
|
|
192
|
+
snapshot.aiEnrichment = aiEnrichment;
|
|
193
|
+
await saveScanHash(projectRootResolved, scanHash);
|
|
194
|
+
console.log(` AI enriched via ${aiEnrichment.provider} (${aiEnrichment.model})`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn(` AI enrichment skipped: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
} else if (skipAiRun && aiReady) {
|
|
199
|
+
console.log(' AI skipped — repository scan unchanged since last run');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await writeArtifacts(projectRootResolved, snapshot);
|
|
203
|
+
await renderContextMarkdown(projectRootResolved, snapshot);
|
|
204
|
+
|
|
205
|
+
if (config.installCursorRules !== false) {
|
|
206
|
+
const { rulePaths } = await installCursorRules(projectRootResolved, snapshot);
|
|
207
|
+
log(
|
|
208
|
+
projectRootResolved,
|
|
209
|
+
`Synced ${rulePaths.length} Cursor rule(s) in .cursor/rules/`,
|
|
210
|
+
verbose
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (config.trackChanges !== false) {
|
|
215
|
+
const history = await loadChangesHistory(projectRootResolved, 15);
|
|
216
|
+
await writeChangesMarkdown(projectRootResolved, snapshot.changelog, history);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await saveScanHash(projectRootResolved, scanHash);
|
|
220
|
+
|
|
221
|
+
log(projectRootResolved, `Generated ${path.join('.contextforge', 'context.md')}`, true);
|
|
222
|
+
console.log('Context generated successfully.');
|
|
223
|
+
console.log(` → .contextforge/context.md`);
|
|
224
|
+
if (fileDiff?.hasFileChanges) {
|
|
225
|
+
const parts = [];
|
|
226
|
+
if (fileDiff.added.length) parts.push(`+${fileDiff.added.length} new`);
|
|
227
|
+
if (fileDiff.modified.length) parts.push(`~${fileDiff.modified.length} modified`);
|
|
228
|
+
if (fileDiff.removed.length) parts.push(`-${fileDiff.removed.length} removed`);
|
|
229
|
+
console.log(` File/code changes detected — context updated (${parts.join(', ')})`);
|
|
230
|
+
if (fileDiff.modified.length && verbose) {
|
|
231
|
+
console.log(` Modified: ${fileDiff.modified.slice(0, 5).join(', ')}${fileDiff.modified.length > 5 ? '...' : ''}`);
|
|
232
|
+
}
|
|
233
|
+
} else if (config.trackChanges !== false && snapshot.changelog?.hasPrevious) {
|
|
234
|
+
console.log(' No file/code changes — context refreshed (metadata only).');
|
|
235
|
+
}
|
|
236
|
+
if (config.trackChanges !== false) {
|
|
237
|
+
console.log(` → .contextforge/CHANGES.md`);
|
|
238
|
+
if (snapshot.changelog?.hasChanges) {
|
|
239
|
+
console.log(` Changes: ${snapshot.changelog.summary}`);
|
|
240
|
+
} else if (snapshot.changelog?.hasPrevious) {
|
|
241
|
+
console.log(' Summary: no structural changes since last run.');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log(` Overall confidence: ${snapshot.confidence?.overall || 'medium'}`);
|
|
245
|
+
|
|
246
|
+
if (aiReady && config.ai.enrichContext && config.ai.background && !skipAi && !skipAiRun) {
|
|
247
|
+
scheduleBackgroundEnrichment(projectRootResolved, { ...snapshot }, config, scanHash);
|
|
248
|
+
console.log(' AI enrichment queued in background (context.md will update when ready)');
|
|
249
|
+
} else if (aiReady && skipAiRun && config.ai.background && !fileDiff?.hasFileChanges) {
|
|
250
|
+
console.log(' Background AI skipped — no file/code changes since last run');
|
|
251
|
+
} else if (config.ai?.enabled && !isAiAvailable(config)) {
|
|
252
|
+
console.log(' Tip: Set OPENAI_API_KEY or GROQ_API_KEY in .env to enable AI enrichment');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return snapshot;
|
|
256
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { ensureInitialized } from './ensure-setup.js';
|
|
6
|
+
import { loadConfig, syncConfigFile } from './config.js';
|
|
7
|
+
import { runPipeline } from './pipeline.js';
|
|
8
|
+
import { getPidPath, getLogsDir } from './paths.js';
|
|
9
|
+
import {
|
|
10
|
+
loadProjectEnv,
|
|
11
|
+
isAutoStartEnabledViaEnv,
|
|
12
|
+
isPostinstallGenerateEnabledViaEnv,
|
|
13
|
+
isPostinstallWatchEnabledViaEnv,
|
|
14
|
+
isAiEnabledViaEnv,
|
|
15
|
+
hasAnyAiKey,
|
|
16
|
+
} from './env.js';
|
|
17
|
+
import { ensureEnvFromExample } from './setup-env.js';
|
|
18
|
+
import { isAiAvailable } from '../services/ai/index.js';
|
|
19
|
+
import { startWatcher } from '../watcher/watcher.js';
|
|
20
|
+
import { getOwnPackageName, hasContextForgeDependency } from './package-meta.js';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const WORKER_SCRIPT = path.join(__dirname, '../../bin/postinstall-worker.js');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Project root where the user ran `npm install` (not node_modules/contextforge).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveInstallProjectRoot() {
|
|
29
|
+
return path.resolve(process.env.INIT_CWD || process.cwd());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} projectRoot
|
|
34
|
+
*/
|
|
35
|
+
export async function shouldRunPostinstall(projectRoot) {
|
|
36
|
+
await loadProjectEnv(projectRoot);
|
|
37
|
+
|
|
38
|
+
if (!isAutoStartEnabledViaEnv()) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (process.env.CONTEXTFORGE_POSTINSTALL_WORKER === '1') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (process.env.npm_command === 'publish' || process.env.npm_command === 'pack') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (isCi() && process.env.CONTEXTFORGE_RUN_IN_CI !== '1') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
52
|
+
if (!(await fs.pathExists(pkgPath))) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pkg = await fs.readJson(pkgPath);
|
|
57
|
+
if (pkg.contextforge?.postinstall === false) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ownName = await getOwnPackageName();
|
|
62
|
+
|
|
63
|
+
// npm lifecycle: npm_package_name matches this package when postinstall runs
|
|
64
|
+
if (process.env.npm_package_name === ownName) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const deps = {
|
|
69
|
+
...pkg.dependencies,
|
|
70
|
+
...pkg.devDependencies,
|
|
71
|
+
...pkg.optionalDependencies,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return hasContextForgeDependency(deps, pkg.name, ownName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCi() {
|
|
78
|
+
return (
|
|
79
|
+
process.env.CI === 'true' ||
|
|
80
|
+
process.env.CI === '1' ||
|
|
81
|
+
process.env.GITHUB_ACTIONS === 'true'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fire-and-forget worker so `npm install` is not blocked.
|
|
87
|
+
* @param {string} projectRoot
|
|
88
|
+
*/
|
|
89
|
+
export function spawnPostinstallWorker(projectRoot) {
|
|
90
|
+
const child = spawn(process.execPath, [WORKER_SCRIPT, projectRoot], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
CONTEXTFORGE_POSTINSTALL_WORKER: '1',
|
|
97
|
+
},
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
});
|
|
100
|
+
child.unref();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function logPostinstall(projectRoot, message) {
|
|
104
|
+
const logDir = getLogsDir(projectRoot);
|
|
105
|
+
await fs.ensureDir(logDir);
|
|
106
|
+
const line = `${new Date().toISOString()} ${message}\n`;
|
|
107
|
+
await fs.appendFile(path.join(logDir, 'postinstall.log'), line).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Setup + generate + optional background watch (runs in detached worker).
|
|
112
|
+
* @param {string} projectRoot
|
|
113
|
+
*/
|
|
114
|
+
export async function runPostinstall(projectRoot) {
|
|
115
|
+
const root = path.resolve(projectRoot);
|
|
116
|
+
await loadProjectEnv(root);
|
|
117
|
+
await logPostinstall(root, 'postinstall worker started');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const createdEnv = await ensureEnvFromExample(root);
|
|
121
|
+
if (createdEnv) {
|
|
122
|
+
await logPostinstall(root, 'created .env from .env.example — add API keys for AI');
|
|
123
|
+
await loadProjectEnv(root);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await ensureInitialized(root, { quiet: true });
|
|
127
|
+
await syncConfigFile(root, { upgrade: true });
|
|
128
|
+
const config = await loadConfig(root);
|
|
129
|
+
|
|
130
|
+
const runGenerate =
|
|
131
|
+
config.postinstallGenerate !== false &&
|
|
132
|
+
config.autoGenerate !== false &&
|
|
133
|
+
isPostinstallGenerateEnabledViaEnv();
|
|
134
|
+
|
|
135
|
+
if (runGenerate) {
|
|
136
|
+
const envAi = isAiEnabledViaEnv();
|
|
137
|
+
const useAi =
|
|
138
|
+
envAi !== false &&
|
|
139
|
+
config.ai?.enabled !== false &&
|
|
140
|
+
config.ai?.enrichContext !== false &&
|
|
141
|
+
hasAnyAiKey() &&
|
|
142
|
+
isAiAvailable(config);
|
|
143
|
+
await logPostinstall(root, `running generate (ai=${useAi})`);
|
|
144
|
+
await runPipeline(root, { verbose: false, skipAi: !useAi });
|
|
145
|
+
await logPostinstall(root, 'generate complete');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const startWatch =
|
|
149
|
+
config.postinstallWatch !== false &&
|
|
150
|
+
config.watch !== false &&
|
|
151
|
+
config.autoGenerate !== false &&
|
|
152
|
+
isPostinstallWatchEnabledViaEnv();
|
|
153
|
+
|
|
154
|
+
if (startWatch) {
|
|
155
|
+
const pidPath = getPidPath(root);
|
|
156
|
+
if (await fs.pathExists(pidPath)) {
|
|
157
|
+
await logPostinstall(root, 'watch already running (pid file exists)');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await logPostinstall(root, 'starting watch --daemon');
|
|
161
|
+
await startWatcher(root, { daemon: true, verbose: false });
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
await logPostinstall(root, `failed: ${err.message}`);
|
|
165
|
+
console.error('[contextforge] postinstall:', err.message);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
7
|
+
|
|
8
|
+
const ENV_EXAMPLE_CONTENT = `# ContextForge — auto-created on npm install
|
|
9
|
+
|
|
10
|
+
OPENAI_API_KEY=
|
|
11
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
12
|
+
|
|
13
|
+
GROQ_API_KEY=
|
|
14
|
+
GROQ_MODEL=llama-3.3-70b-versatile
|
|
15
|
+
|
|
16
|
+
CONTEXTFORGE_AI_PROVIDER=openai
|
|
17
|
+
CONTEXTFORGE_AI_ENABLED=true
|
|
18
|
+
|
|
19
|
+
CONTEXTFORGE_AUTO_START=true
|
|
20
|
+
CONTEXTFORGE_POSTINSTALL_GENERATE=true
|
|
21
|
+
CONTEXTFORGE_POSTINSTALL_WATCH=true
|
|
22
|
+
|
|
23
|
+
CONTEXTFORGE_REFRESH_INTERVAL_MINUTES=15
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ensure .env.example exists in the target project.
|
|
28
|
+
* @param {string} projectRoot
|
|
29
|
+
*/
|
|
30
|
+
export async function ensureEnvExample(projectRoot) {
|
|
31
|
+
const target = path.join(projectRoot, '.env.example');
|
|
32
|
+
const packageExample = path.join(PACKAGE_ROOT, '.env.example');
|
|
33
|
+
|
|
34
|
+
if (await fs.pathExists(target)) return;
|
|
35
|
+
|
|
36
|
+
if (await fs.pathExists(packageExample)) {
|
|
37
|
+
await fs.copy(packageExample, target);
|
|
38
|
+
} else {
|
|
39
|
+
await fs.writeFile(target, ENV_EXAMPLE_CONTENT, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create .env from .env.example if .env does not exist.
|
|
45
|
+
* @param {string} projectRoot
|
|
46
|
+
* @returns {Promise<boolean>} true if .env was created
|
|
47
|
+
*/
|
|
48
|
+
export async function ensureEnvFromExample(projectRoot) {
|
|
49
|
+
const envPath = path.join(projectRoot, '.env');
|
|
50
|
+
const examplePath = path.join(projectRoot, '.env.example');
|
|
51
|
+
|
|
52
|
+
if (await fs.pathExists(envPath)) return false;
|
|
53
|
+
|
|
54
|
+
await ensureEnvExample(projectRoot);
|
|
55
|
+
|
|
56
|
+
if (await fs.pathExists(examplePath)) {
|
|
57
|
+
await fs.copy(examplePath, envPath);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(envPath, ENV_EXAMPLE_CONTENT, 'utf8');
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate .env has at least placeholder structure (warn if keys empty when AI on).
|
|
67
|
+
* @param {string} projectRoot
|
|
68
|
+
*/
|
|
69
|
+
export async function validateEnvSetup(projectRoot) {
|
|
70
|
+
const envPath = path.join(projectRoot, '.env');
|
|
71
|
+
const issues = [];
|
|
72
|
+
|
|
73
|
+
if (!(await fs.pathExists(envPath))) {
|
|
74
|
+
return { ok: false, issues: ['.env file missing — run: copy .env.example .env'] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
78
|
+
const hasOpenAI = /^OPENAI_API_KEY=.+/m.test(content) && !/your-|sk-your/i.test(content);
|
|
79
|
+
const hasGroq = /^GROQ_API_KEY=.+/m.test(content) && !/your-|gsk_your/i.test(content);
|
|
80
|
+
|
|
81
|
+
if (!hasOpenAI && !hasGroq) {
|
|
82
|
+
issues.push('Add OPENAI_API_KEY and/or GROQ_API_KEY to .env for AI enrichment');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ok: issues.length === 0, issues, hasOpenAI, hasGroq };
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getContextForgeRoot } from './paths.js';
|
|
4
|
+
import { PACKAGE_PROMPTS, PROMPT_FILES } from '../services/ai/prompt-loader.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Copy default prompts to .contextforge/prompts/ (skip existing files).
|
|
8
|
+
* @param {string} projectRoot
|
|
9
|
+
*/
|
|
10
|
+
export async function ensureProjectPrompts(projectRoot) {
|
|
11
|
+
const targetDir = path.join(getContextForgeRoot(projectRoot), 'prompts');
|
|
12
|
+
await fs.ensureDir(targetDir);
|
|
13
|
+
|
|
14
|
+
let copied = 0;
|
|
15
|
+
for (const file of Object.values(PROMPT_FILES)) {
|
|
16
|
+
const dest = path.join(targetDir, file);
|
|
17
|
+
const src = path.join(PACKAGE_PROMPTS, file);
|
|
18
|
+
if (!(await fs.pathExists(dest)) && (await fs.pathExists(src))) {
|
|
19
|
+
await fs.copy(src, dest);
|
|
20
|
+
copied++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const readmeSrc = path.join(PACKAGE_PROMPTS, 'README.md');
|
|
25
|
+
const readmeDest = path.join(targetDir, 'README.md');
|
|
26
|
+
if (!(await fs.pathExists(readmeDest)) && (await fs.pathExists(readmeSrc))) {
|
|
27
|
+
await fs.copy(readmeSrc, readmeDest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { targetDir, copied };
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getCacheDir } from './paths.js';
|
|
5
|
+
|
|
6
|
+
function hashPayload(obj) {
|
|
7
|
+
return crypto.createHash('sha256').update(JSON.stringify(obj)).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fingerprint of all scanned file contents — detects code edits.
|
|
12
|
+
*/
|
|
13
|
+
export function computeFilesSignature(files = []) {
|
|
14
|
+
const parts = files
|
|
15
|
+
.filter((f) => f.hash && f.hash !== 'skipped-large' && f.hash !== 'unreadable')
|
|
16
|
+
.map((f) => `${f.relativePath}:${f.hash}`)
|
|
17
|
+
.sort();
|
|
18
|
+
return hashPayload(parts);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stable hash of scan data — skip AI if unchanged.
|
|
23
|
+
* @param {object} snapshot
|
|
24
|
+
* @param {{ added?: string[], modified?: string[], removed?: string[] } | null} fileDiff
|
|
25
|
+
*/
|
|
26
|
+
export function computeScanHash(snapshot, fileDiff = null) {
|
|
27
|
+
const payload = {
|
|
28
|
+
projectName: snapshot.projectName,
|
|
29
|
+
techStack: snapshot.techStack,
|
|
30
|
+
architecture: {
|
|
31
|
+
primary: snapshot.architecture?.primary?.pattern,
|
|
32
|
+
apiFlow: snapshot.architecture?.apiFlow,
|
|
33
|
+
},
|
|
34
|
+
database: snapshot.database?.models?.map((m) => m.name),
|
|
35
|
+
filesScanned: snapshot.filesScanned,
|
|
36
|
+
filesSignature: snapshot.filesSignature,
|
|
37
|
+
codeSummary: snapshot.codeInsights?.summary,
|
|
38
|
+
astRouteCount: snapshot.ast?.routes?.length,
|
|
39
|
+
bugCount: snapshot.bugs?.issues?.length,
|
|
40
|
+
gitHead: snapshot.git?.commits?.[0]?.hash,
|
|
41
|
+
fileDiff: fileDiff
|
|
42
|
+
? {
|
|
43
|
+
added: fileDiff.added?.length || 0,
|
|
44
|
+
modified: fileDiff.modified?.length || 0,
|
|
45
|
+
removed: fileDiff.removed?.length || 0,
|
|
46
|
+
}
|
|
47
|
+
: null,
|
|
48
|
+
};
|
|
49
|
+
return hashPayload(payload);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function loadScanHash(projectRoot) {
|
|
53
|
+
const p = path.join(getCacheDir(projectRoot), 'scan-hash.txt');
|
|
54
|
+
if (await fs.pathExists(p)) {
|
|
55
|
+
return (await fs.readFile(p, 'utf8')).trim();
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function saveScanHash(projectRoot, hash) {
|
|
61
|
+
await fs.writeFile(path.join(getCacheDir(projectRoot), 'scan-hash.txt'), hash, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function shouldSkipAi(projectRoot, snapshot, forceAi, fileDiff = null) {
|
|
65
|
+
if (forceAi) return false;
|
|
66
|
+
if (fileDiff?.hasFileChanges) return false;
|
|
67
|
+
const current = computeScanHash(snapshot, fileDiff);
|
|
68
|
+
const previous = await loadScanHash(projectRoot);
|
|
69
|
+
return previous === current;
|
|
70
|
+
}
|