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,152 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig, isInitialized } from '../../core/config.js';
|
|
4
|
+
import {
|
|
5
|
+
loadProjectEnv,
|
|
6
|
+
hasAnyAiKey,
|
|
7
|
+
isAutoStartEnabledViaEnv,
|
|
8
|
+
isPostinstallWatchEnabledViaEnv,
|
|
9
|
+
} from '../../core/env.js';
|
|
10
|
+
import { getPidPath } from '../../core/paths.js';
|
|
11
|
+
import { validateEnvSetup } from '../../core/setup-env.js';
|
|
12
|
+
import { isAiAvailable } from '../../services/ai/index.js';
|
|
13
|
+
import { getContextForgeRoot, getOutputPath, OUTPUT_FILES } from '../../core/paths.js';
|
|
14
|
+
|
|
15
|
+
export async function doctorCommand(projectRoot) {
|
|
16
|
+
const root = path.resolve(projectRoot);
|
|
17
|
+
await loadProjectEnv(root);
|
|
18
|
+
|
|
19
|
+
const checks = [];
|
|
20
|
+
let ok = true;
|
|
21
|
+
|
|
22
|
+
const nodeVersion = process.version;
|
|
23
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
24
|
+
if (nodeMajor >= 18) {
|
|
25
|
+
checks.push({ name: 'Node.js', status: 'ok', detail: nodeVersion });
|
|
26
|
+
} else {
|
|
27
|
+
checks.push({ name: 'Node.js', status: 'fail', detail: `${nodeVersion} — need >= 18` });
|
|
28
|
+
ok = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (await isInitialized(root)) {
|
|
32
|
+
checks.push({ name: 'ContextForge init', status: 'ok', detail: getContextForgeRoot(root) });
|
|
33
|
+
} else {
|
|
34
|
+
checks.push({ name: 'ContextForge init', status: 'fail', detail: 'Run: npx contextforge init' });
|
|
35
|
+
ok = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const contextMd = getOutputPath(root, OUTPUT_FILES.contextMd);
|
|
39
|
+
if (await fs.pathExists(contextMd)) {
|
|
40
|
+
const stat = await fs.stat(contextMd);
|
|
41
|
+
checks.push({
|
|
42
|
+
name: 'context.md',
|
|
43
|
+
status: 'ok',
|
|
44
|
+
detail: `Exists (${Math.round(stat.size / 1024)} KB, ${stat.mtime.toISOString().slice(0, 10)})`,
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
checks.push({ name: 'context.md', status: 'warn', detail: 'Run: npx contextforge generate' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await fs.access(root, fs.constants.W_OK);
|
|
52
|
+
checks.push({ name: 'Write permission', status: 'ok', detail: root });
|
|
53
|
+
} catch {
|
|
54
|
+
checks.push({ name: 'Write permission', status: 'fail', detail: 'Cannot write to project root' });
|
|
55
|
+
ok = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const envCheck = await validateEnvSetup(root);
|
|
59
|
+
if (await fs.pathExists(path.join(root, '.env'))) {
|
|
60
|
+
checks.push({
|
|
61
|
+
name: '.env file',
|
|
62
|
+
status: 'ok',
|
|
63
|
+
detail: hasAnyAiKey() ? 'API key(s) present' : 'No API keys set (AI disabled)',
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
checks.push({
|
|
67
|
+
name: '.env file',
|
|
68
|
+
status: 'warn',
|
|
69
|
+
detail: 'Copy .env.example to .env for AI enrichment',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (envCheck.hasOpenAI) checks.push({ name: 'OpenAI key', status: 'ok', detail: 'Configured' });
|
|
74
|
+
if (envCheck.hasGroq) checks.push({ name: 'Groq key', status: 'ok', detail: 'Configured' });
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { default: simpleGit } = await import('simple-git');
|
|
78
|
+
const git = simpleGit(root);
|
|
79
|
+
if (await git.checkIsRepo()) {
|
|
80
|
+
checks.push({ name: 'Git', status: 'ok', detail: `Branch: ${(await git.branch()).current}` });
|
|
81
|
+
} else {
|
|
82
|
+
checks.push({ name: 'Git', status: 'warn', detail: 'Not a git repo — history skipped' });
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
checks.push({
|
|
86
|
+
name: 'Git',
|
|
87
|
+
status: 'warn',
|
|
88
|
+
detail: 'Git not in PATH — install Git for commit insights',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config = await loadConfig(root);
|
|
93
|
+
if (config.ai?.enabled) {
|
|
94
|
+
if (isAiAvailable(config)) {
|
|
95
|
+
checks.push({
|
|
96
|
+
name: 'AI enrichment',
|
|
97
|
+
status: 'ok',
|
|
98
|
+
detail: `Provider: ${config.ai.provider}, model: ${config.ai.openai?.model || config.ai.groq?.model}`,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
checks.push({
|
|
102
|
+
name: 'AI enrichment',
|
|
103
|
+
status: 'fail',
|
|
104
|
+
detail: 'Enabled in config but no valid API keys',
|
|
105
|
+
});
|
|
106
|
+
ok = false;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
checks.push({ name: 'AI enrichment', status: 'ok', detail: 'Disabled (enable in config + .env)' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isAutoStartEnabledViaEnv()) {
|
|
113
|
+
const pidPath = getPidPath(root);
|
|
114
|
+
const watcherRunning = await fs.pathExists(pidPath);
|
|
115
|
+
checks.push({
|
|
116
|
+
name: 'npm install auto-start',
|
|
117
|
+
status: 'ok',
|
|
118
|
+
detail: watcherRunning
|
|
119
|
+
? 'Enabled — background watch is running'
|
|
120
|
+
: 'Enabled — runs on next npm install (or npm run watch)',
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: 'npm install auto-start',
|
|
125
|
+
status: 'ok',
|
|
126
|
+
detail: 'Disabled (CONTEXTFORGE_AUTO_START=false or SKIP_POSTINSTALL=1)',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const intervalMin = config.refreshIntervalMinutes ?? 0;
|
|
131
|
+
if (intervalMin > 0 && isPostinstallWatchEnabledViaEnv()) {
|
|
132
|
+
checks.push({
|
|
133
|
+
name: 'Watch auto-refresh',
|
|
134
|
+
status: 'ok',
|
|
135
|
+
detail: `Every ${intervalMin} minute(s) (from .env or config)`,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
checks.push({
|
|
139
|
+
name: 'Watch auto-refresh',
|
|
140
|
+
status: 'ok',
|
|
141
|
+
detail: 'Off — set CONTEXTFORGE_REFRESH_INTERVAL_MINUTES in .env or watch --interval',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('\nContextForge Doctor\n');
|
|
146
|
+
for (const c of checks) {
|
|
147
|
+
const icon = c.status === 'ok' ? '✓' : c.status === 'warn' ? '!' : '✗';
|
|
148
|
+
console.log(` ${icon} ${c.name}: ${c.detail}`);
|
|
149
|
+
}
|
|
150
|
+
console.log(ok ? '\nAll critical checks passed.\n' : '\nSome checks failed — fix above issues.\n');
|
|
151
|
+
if (!ok) process.exit(1);
|
|
152
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ensureInitialized } from '../../core/ensure-setup.js';
|
|
2
|
+
import { runPipeline } from '../../core/pipeline.js';
|
|
3
|
+
|
|
4
|
+
export async function generateCommand(projectRoot, options = {}) {
|
|
5
|
+
await ensureInitialized(projectRoot, { enableAi: options.forceAi });
|
|
6
|
+
await runPipeline(projectRoot, options);
|
|
7
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
writeDefaultConfig,
|
|
5
|
+
isInitialized,
|
|
6
|
+
loadConfig,
|
|
7
|
+
syncConfigFile,
|
|
8
|
+
} from '../../core/config.js';
|
|
9
|
+
import { runPipeline } from '../../core/pipeline.js';
|
|
10
|
+
import { getContextForgeRoot } from '../../core/paths.js';
|
|
11
|
+
import { ensureEnvExample, ensureEnvFromExample, validateEnvSetup } from '../../core/setup-env.js';
|
|
12
|
+
import { isAiAvailable } from '../../services/ai/index.js';
|
|
13
|
+
import { loadProjectEnv } from '../../core/env.js';
|
|
14
|
+
import { ensureProjectPrompts } from '../../core/setup-prompts.js';
|
|
15
|
+
|
|
16
|
+
export async function initCommand(projectRoot, options = {}) {
|
|
17
|
+
const initialized = await isInitialized(projectRoot);
|
|
18
|
+
|
|
19
|
+
if (initialized && !options.force) {
|
|
20
|
+
console.log('ContextForge already initialized.');
|
|
21
|
+
console.log(` → ${getContextForgeRoot(projectRoot)}`);
|
|
22
|
+
console.log('Run: npx contextforge generate');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initOptions = {};
|
|
27
|
+
if (options.enableAi) {
|
|
28
|
+
initOptions.ai = { enabled: true, enrichContext: true, background: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await writeDefaultConfig(projectRoot, initOptions);
|
|
32
|
+
const cfgWritten = await syncConfigFile(projectRoot, {
|
|
33
|
+
enableAi: options.enableAi,
|
|
34
|
+
upgrade: options.force || !initialized,
|
|
35
|
+
});
|
|
36
|
+
const { targetDir: promptsDir, copied: promptsCopied } =
|
|
37
|
+
await ensureProjectPrompts(projectRoot);
|
|
38
|
+
await ensureEnvExample(projectRoot);
|
|
39
|
+
const createdEnv = await ensureEnvFromExample(projectRoot);
|
|
40
|
+
|
|
41
|
+
console.log('Initialized ContextForge.');
|
|
42
|
+
console.log(` → ${getContextForgeRoot(projectRoot)}/config.json`);
|
|
43
|
+
console.log(` → ${promptsDir}/ (${promptsCopied} prompt file(s) copied — edit to customize AI)`);
|
|
44
|
+
if (createdEnv) {
|
|
45
|
+
console.log(' → .env created from .env.example — add your API keys');
|
|
46
|
+
} else {
|
|
47
|
+
console.log(' → Copy .env.example to .env and set OPENAI_API_KEY / GROQ_API_KEY');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await loadProjectEnv(projectRoot);
|
|
51
|
+
await runPipeline(projectRoot, {
|
|
52
|
+
verbose: options.verbose,
|
|
53
|
+
forceAi: options.enableAi,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const cfg = await loadConfig(projectRoot);
|
|
57
|
+
|
|
58
|
+
if (cfg.installCursorRules !== false) {
|
|
59
|
+
console.log(' → Cursor rule: .cursor/rules/contextforge.mdc');
|
|
60
|
+
}
|
|
61
|
+
if (cfg.watch && cfg.autoGenerate) {
|
|
62
|
+
console.log('\nWatch mode: `npx contextforge watch`');
|
|
63
|
+
console.log(' • Updates when you save files (content hash detection)');
|
|
64
|
+
if (cfg.refreshIntervalMinutes > 0) {
|
|
65
|
+
console.log(
|
|
66
|
+
` • Auto-refresh every ${cfg.refreshIntervalMinutes} min (from .env or config)`
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(' • Timer: set CONTEXTFORGE_REFRESH_INTERVAL_MINUTES in .env (e.g. 15)');
|
|
70
|
+
}
|
|
71
|
+
console.log(' • Change log: `npx contextforge changes` → .contextforge/CHANGES.md');
|
|
72
|
+
console.log(' • Daemon: `watch --daemon` then `npx contextforge stop`');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const envCheck = await validateEnvSetup(projectRoot);
|
|
76
|
+
if (cfg.ai?.enabled) {
|
|
77
|
+
if (isAiAvailable(cfg)) {
|
|
78
|
+
console.log('\nAI: enabled — OpenAI/Groq will enrich context.md (background if configured)');
|
|
79
|
+
if (envCheck.hasOpenAI) console.log(' → OpenAI key loaded');
|
|
80
|
+
if (envCheck.hasGroq) console.log(' → Groq key loaded');
|
|
81
|
+
} else {
|
|
82
|
+
console.log('\nAI: enabled in config but API keys missing in .env');
|
|
83
|
+
envCheck.issues.forEach((i) => console.log(` → ${i}`));
|
|
84
|
+
}
|
|
85
|
+
} else if (options.enableAi) {
|
|
86
|
+
envCheck.issues.forEach((i) => console.log(` → ${i}`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const gitignore = projectRoot + '/.gitignore';
|
|
90
|
+
if (await fs.pathExists(gitignore)) {
|
|
91
|
+
const content = await fs.readFile(gitignore, 'utf8');
|
|
92
|
+
if (!content.includes('.contextforge/cache')) {
|
|
93
|
+
console.log('\nTip: Add to .gitignore:');
|
|
94
|
+
console.log(' .contextforge/cache/');
|
|
95
|
+
console.log(' .contextforge/logs/');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { getOutputPath, OUTPUT_FILES } from '../../core/paths.js';
|
|
3
|
+
|
|
4
|
+
export async function promptCommand(projectRoot, options = {}) {
|
|
5
|
+
const mdPath = getOutputPath(projectRoot, OUTPUT_FILES.contextMd);
|
|
6
|
+
const jsonPath = getOutputPath(projectRoot, OUTPUT_FILES.contextJson);
|
|
7
|
+
|
|
8
|
+
if (!(await fs.pathExists(mdPath))) {
|
|
9
|
+
console.error('No context found. Run: npx contextforge init');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sections = (options.sections || 'all').split(',').map((s) => s.trim().toLowerCase());
|
|
14
|
+
const useAll = sections.includes('all');
|
|
15
|
+
|
|
16
|
+
if (useAll) {
|
|
17
|
+
const md = await fs.readFile(mdPath, 'utf8');
|
|
18
|
+
console.log('--- ContextForge AI Context ---\n');
|
|
19
|
+
console.log(md);
|
|
20
|
+
console.log('\n--- End Context ---');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const snapshot = await fs.readJson(jsonPath);
|
|
25
|
+
const parts = [];
|
|
26
|
+
|
|
27
|
+
if (sections.includes('stack')) {
|
|
28
|
+
const ts = snapshot.techStack;
|
|
29
|
+
parts.push(
|
|
30
|
+
`Tech: Frontend[${ts.frontend.join(', ')}] Backend[${ts.backend.join(', ')}] DB[${ts.database.join(', ')}]`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (sections.includes('arch')) {
|
|
34
|
+
parts.push(`Architecture: ${snapshot.architecture?.primary?.displayLabel}`);
|
|
35
|
+
if (snapshot.architecture?.apiFlow) parts.push(`API Flow: ${snapshot.architecture.apiFlow}`);
|
|
36
|
+
}
|
|
37
|
+
if (sections.includes('git') && snapshot.git?.commits?.length) {
|
|
38
|
+
parts.push('Recent commits:');
|
|
39
|
+
snapshot.git.commits.slice(0, 5).forEach((c) => {
|
|
40
|
+
parts.push(` ${c.hash} ${c.message.slice(0, 50)}`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (sections.includes('bugs') && snapshot.bugs?.issues?.length) {
|
|
44
|
+
parts.push('Issues:');
|
|
45
|
+
snapshot.bugs.issues.slice(0, 5).forEach((i) => {
|
|
46
|
+
parts.push(` [${i.severity}] ${i.message}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('--- ContextForge AI Context ---\n');
|
|
51
|
+
console.log(parts.join('\n'));
|
|
52
|
+
console.log('\n--- End Context ---');
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { getOutputPath, OUTPUT_FILES } from '../../core/paths.js';
|
|
3
|
+
|
|
4
|
+
export async function summaryCommand(projectRoot) {
|
|
5
|
+
const jsonPath = getOutputPath(projectRoot, OUTPUT_FILES.contextJson);
|
|
6
|
+
|
|
7
|
+
if (!(await fs.pathExists(jsonPath))) {
|
|
8
|
+
console.error('No context found. Run: npx contextforge init');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const snapshot = await fs.readJson(jsonPath);
|
|
13
|
+
const ts = snapshot.techStack;
|
|
14
|
+
|
|
15
|
+
console.log(`\n${snapshot.projectName || 'Project'} — ContextForge Summary\n`);
|
|
16
|
+
console.log(`Generated: ${snapshot.generatedAt}`);
|
|
17
|
+
console.log(`Files scanned: ${snapshot.filesScanned}`);
|
|
18
|
+
console.log(`\nStack: ${[...ts.frontend, ...ts.backend].join(', ') || 'unknown'}`);
|
|
19
|
+
console.log(`Architecture: ${snapshot.architecture?.primary?.displayLabel || 'unknown'}`);
|
|
20
|
+
console.log(`Database: ${[...ts.database, ...ts.orm].join(', ') || 'none detected'}`);
|
|
21
|
+
console.log(`\nFull context: .contextforge/context.md\n`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ensureInitialized } from '../../core/ensure-setup.js';
|
|
2
|
+
import { runPipeline } from '../../core/pipeline.js';
|
|
3
|
+
import { startWatcher } from '../../watcher/watcher.js';
|
|
4
|
+
|
|
5
|
+
export async function watchCommand(projectRoot, options = {}) {
|
|
6
|
+
await ensureInitialized(projectRoot);
|
|
7
|
+
await runPipeline(projectRoot, { verbose: options.verbose });
|
|
8
|
+
await startWatcher(projectRoot, options);
|
|
9
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { generateCommand } from './commands/generate.js';
|
|
6
|
+
import { watchCommand } from './commands/watch.js';
|
|
7
|
+
import { summaryCommand } from './commands/summary.js';
|
|
8
|
+
import { promptCommand } from './commands/prompt.js';
|
|
9
|
+
import { stopCommand } from './commands/stop.js';
|
|
10
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
11
|
+
import { changesCommand } from './commands/changes.js';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
function resolveCwd(cwd) {
|
|
16
|
+
return path.resolve(cwd || process.cwd());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runCli(argv) {
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('contextforge-cli-ai-prompt-pirates')
|
|
24
|
+
.description('AI-ready repository context generator')
|
|
25
|
+
.version('0.4.0')
|
|
26
|
+
.option('-c, --cwd <path>', 'Project root directory', process.cwd())
|
|
27
|
+
.option('-v, --verbose', 'Verbose logging');
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('init')
|
|
31
|
+
.description('Initialize .contextforge and generate initial context')
|
|
32
|
+
.option('-f, --force', 'Re-run setup even if already initialized')
|
|
33
|
+
.option('--ai', 'Enable AI enrichment in config')
|
|
34
|
+
.action(async (opts, cmd) => {
|
|
35
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
36
|
+
await initCommand(resolveCwd(globalOpts.cwd), {
|
|
37
|
+
force: opts.force,
|
|
38
|
+
verbose: globalOpts.verbose,
|
|
39
|
+
enableAi: opts.ai,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('generate')
|
|
45
|
+
.description('Scan repository and regenerate context')
|
|
46
|
+
.option('--ai', 'Force enable AI enrichment (OpenAI/Groq)')
|
|
47
|
+
.option('--no-ai', 'Skip AI enrichment for this run')
|
|
48
|
+
.action(async (opts, cmd) => {
|
|
49
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
50
|
+
await generateCommand(resolveCwd(globalOpts.cwd), {
|
|
51
|
+
verbose: globalOpts.verbose,
|
|
52
|
+
forceAi: opts.ai,
|
|
53
|
+
skipAi: opts.noAi,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('watch')
|
|
59
|
+
.description('Watch files + optional timed refresh of context.md')
|
|
60
|
+
.option('--daemon', 'Write PID file for contextforge stop')
|
|
61
|
+
.option(
|
|
62
|
+
'-i, --interval <minutes>',
|
|
63
|
+
'Also regenerate every N minutes (e.g. 5, 15, 30)'
|
|
64
|
+
)
|
|
65
|
+
.action(async (opts, cmd) => {
|
|
66
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
67
|
+
const interval = opts.interval ? parseFloat(opts.interval) : undefined;
|
|
68
|
+
await watchCommand(resolveCwd(globalOpts.cwd), {
|
|
69
|
+
verbose: globalOpts.verbose,
|
|
70
|
+
daemon: opts.daemon,
|
|
71
|
+
intervalMinutes: interval,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
program
|
|
76
|
+
.command('changes')
|
|
77
|
+
.description('Show history of what changed between runs')
|
|
78
|
+
.option('-n, --limit <number>', 'Number of runs to show', '10')
|
|
79
|
+
.action(async (opts, cmd) => {
|
|
80
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
81
|
+
await changesCommand(resolveCwd(globalOpts.cwd), {
|
|
82
|
+
limit: parseInt(opts.limit, 10),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('stop')
|
|
88
|
+
.description('Stop background watcher started with watch --daemon')
|
|
89
|
+
.action(async (_opts, cmd) => {
|
|
90
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
91
|
+
await stopCommand(resolveCwd(globalOpts.cwd));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('doctor')
|
|
96
|
+
.description('Health check: Node, Git, .env, AI keys, context.md')
|
|
97
|
+
.action(async (_opts, cmd) => {
|
|
98
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
99
|
+
await doctorCommand(resolveCwd(globalOpts.cwd));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('summary')
|
|
104
|
+
.description('Print short project summary to stdout')
|
|
105
|
+
.action(async (_opts, cmd) => {
|
|
106
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
107
|
+
await summaryCommand(resolveCwd(globalOpts.cwd));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command('prompt')
|
|
112
|
+
.description('Export AI-ready context block to stdout')
|
|
113
|
+
.option('-s, --sections <list>', 'Comma-separated sections: stack,arch,git,bugs,all', 'all')
|
|
114
|
+
.action(async (opts, cmd) => {
|
|
115
|
+
const globalOpts = cmd.parent?.opts?.() || {};
|
|
116
|
+
await promptCommand(resolveCwd(globalOpts.cwd), { sections: opts.sections });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await program.parseAsync(argv);
|
|
120
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-section confidence for context.md accuracy labels.
|
|
3
|
+
* @param {object} snapshot
|
|
4
|
+
*/
|
|
5
|
+
export function computeConfidenceScores(snapshot) {
|
|
6
|
+
const ts = snapshot.techStack || {};
|
|
7
|
+
const scores = {};
|
|
8
|
+
|
|
9
|
+
scores.techStack =
|
|
10
|
+
ts.signals?.length >= 3 ? 'high' : ts.signals?.length >= 1 ? 'medium' : 'low';
|
|
11
|
+
|
|
12
|
+
scores.architecture = snapshot.architecture?.primary?.confidence || 'low';
|
|
13
|
+
|
|
14
|
+
scores.database =
|
|
15
|
+
snapshot.database?.models?.length > 0
|
|
16
|
+
? 'high'
|
|
17
|
+
: snapshot.database?.orm
|
|
18
|
+
? 'medium'
|
|
19
|
+
: 'low';
|
|
20
|
+
|
|
21
|
+
scores.git = snapshot.git?.available && snapshot.git?.commits?.length ? 'high' : 'low';
|
|
22
|
+
|
|
23
|
+
scores.apiRoutes =
|
|
24
|
+
snapshot.codeInsights?.apiEndpoints?.length > 0 ||
|
|
25
|
+
snapshot.ast?.routes?.length > 0
|
|
26
|
+
? 'high'
|
|
27
|
+
: 'low';
|
|
28
|
+
|
|
29
|
+
scores.bugs =
|
|
30
|
+
snapshot.bugs?.issues?.some((i) => i.type === 'eslint')
|
|
31
|
+
? 'high'
|
|
32
|
+
: snapshot.bugs?.issues?.length
|
|
33
|
+
? 'medium'
|
|
34
|
+
: 'low';
|
|
35
|
+
|
|
36
|
+
scores.overall = aggregateOverall(scores);
|
|
37
|
+
|
|
38
|
+
return scores;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function aggregateOverall(scores) {
|
|
42
|
+
const vals = Object.values(scores).filter((v) => v !== scores.overall);
|
|
43
|
+
if (vals.every((v) => v === 'high')) return 'high';
|
|
44
|
+
if (vals.some((v) => v === 'low') && !vals.some((v) => v === 'high')) return 'low';
|
|
45
|
+
return 'medium';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function confidenceBadge(level) {
|
|
49
|
+
const map = { high: '🟢 High', medium: '🟡 Medium', low: '🔴 Low' };
|
|
50
|
+
return map[level] || level;
|
|
51
|
+
}
|