contextforge-cli-harshil 1.0.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/.env.example +37 -0
- package/README.md +170 -0
- package/bin/index.js +55 -0
- package/package.json +44 -0
- package/src/ai.js +399 -0
- package/src/analyze.js +432 -0
- package/src/commands/init.js +275 -0
- package/src/context.js +107 -0
- package/src/generate.js +211 -0
- package/src/scan.js +206 -0
- package/src/utils.js +93 -0
package/src/scan.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan.js
|
|
3
|
+
* Scans the repository using fast-glob and returns a list of relevant files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fg from 'fast-glob';
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve, relative } from 'path';
|
|
9
|
+
import ignore from 'ignore';
|
|
10
|
+
|
|
11
|
+
// Patterns to always include
|
|
12
|
+
const INCLUDE_PATTERNS = [
|
|
13
|
+
'src/**',
|
|
14
|
+
'lib/**',
|
|
15
|
+
'app/**',
|
|
16
|
+
'pages/**',
|
|
17
|
+
'components/**',
|
|
18
|
+
'routes/**',
|
|
19
|
+
'controllers/**',
|
|
20
|
+
'services/**',
|
|
21
|
+
'middleware/**',
|
|
22
|
+
'middlewares/**',
|
|
23
|
+
'models/**',
|
|
24
|
+
'schemas/**',
|
|
25
|
+
'prisma/**',
|
|
26
|
+
'config/**',
|
|
27
|
+
'configs/**',
|
|
28
|
+
'utils/**',
|
|
29
|
+
'helpers/**',
|
|
30
|
+
'hooks/**',
|
|
31
|
+
'store/**',
|
|
32
|
+
'api/**',
|
|
33
|
+
'docs/**',
|
|
34
|
+
'scripts/**',
|
|
35
|
+
'README.md',
|
|
36
|
+
'readme.md',
|
|
37
|
+
'package.json',
|
|
38
|
+
'.env.example',
|
|
39
|
+
'.env.sample',
|
|
40
|
+
'docker-compose.yml',
|
|
41
|
+
'docker-compose.yaml',
|
|
42
|
+
'Dockerfile',
|
|
43
|
+
'tsconfig.json',
|
|
44
|
+
'jsconfig.json',
|
|
45
|
+
'vite.config.*',
|
|
46
|
+
'next.config.*',
|
|
47
|
+
'webpack.config.*',
|
|
48
|
+
'.eslintrc*',
|
|
49
|
+
'babel.config.*',
|
|
50
|
+
'jest.config.*',
|
|
51
|
+
'tailwind.config.*',
|
|
52
|
+
'index.js',
|
|
53
|
+
'index.ts',
|
|
54
|
+
'main.js',
|
|
55
|
+
'main.ts',
|
|
56
|
+
'app.js',
|
|
57
|
+
'app.ts',
|
|
58
|
+
'server.js',
|
|
59
|
+
'server.ts',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Patterns to always ignore
|
|
63
|
+
const IGNORE_PATTERNS = [
|
|
64
|
+
'node_modules/**',
|
|
65
|
+
'dist/**',
|
|
66
|
+
'build/**',
|
|
67
|
+
'.git/**',
|
|
68
|
+
'coverage/**',
|
|
69
|
+
'.next/**',
|
|
70
|
+
'.nuxt/**',
|
|
71
|
+
'.cache/**',
|
|
72
|
+
'.turbo/**',
|
|
73
|
+
'out/**',
|
|
74
|
+
'**/*.log',
|
|
75
|
+
'**/*.lock',
|
|
76
|
+
'**/*.min.js',
|
|
77
|
+
'**/*.min.css',
|
|
78
|
+
'**/*.map',
|
|
79
|
+
'**/*.snap',
|
|
80
|
+
'**/*.png',
|
|
81
|
+
'**/*.jpg',
|
|
82
|
+
'**/*.jpeg',
|
|
83
|
+
'**/*.gif',
|
|
84
|
+
'**/*.svg',
|
|
85
|
+
'**/*.ico',
|
|
86
|
+
'**/*.woff',
|
|
87
|
+
'**/*.woff2',
|
|
88
|
+
'**/*.ttf',
|
|
89
|
+
'**/*.eot',
|
|
90
|
+
'**/*.mp4',
|
|
91
|
+
'**/*.mp3',
|
|
92
|
+
'**/*.pdf',
|
|
93
|
+
'**/*.zip',
|
|
94
|
+
'**/*.tar.gz',
|
|
95
|
+
'context.md',
|
|
96
|
+
'.env',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads .gitignore if present and returns an `ignore` instance.
|
|
101
|
+
* @param {string} cwd
|
|
102
|
+
*/
|
|
103
|
+
function loadGitignore(cwd) {
|
|
104
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
105
|
+
const ig = ignore();
|
|
106
|
+
if (existsSync(gitignorePath)) {
|
|
107
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
108
|
+
ig.add(content);
|
|
109
|
+
}
|
|
110
|
+
return ig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scan the repository and return an array of file entries.
|
|
115
|
+
* Each entry: { path: string (relative), absolutePath: string }
|
|
116
|
+
*
|
|
117
|
+
* @param {string} cwd - Root directory to scan
|
|
118
|
+
* @returns {Promise<Array<{ path: string, absolutePath: string }>>}
|
|
119
|
+
*/
|
|
120
|
+
export async function scanRepository(cwd) {
|
|
121
|
+
const ig = loadGitignore(cwd);
|
|
122
|
+
|
|
123
|
+
const files = await fg(INCLUDE_PATTERNS, {
|
|
124
|
+
cwd,
|
|
125
|
+
ignore: IGNORE_PATTERNS,
|
|
126
|
+
dot: true,
|
|
127
|
+
followSymbolicLinks: false,
|
|
128
|
+
onlyFiles: true,
|
|
129
|
+
absolute: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Filter through .gitignore rules
|
|
133
|
+
const filtered = files.filter((f) => {
|
|
134
|
+
try {
|
|
135
|
+
return !ig.ignores(f);
|
|
136
|
+
} catch {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Sort for deterministic ordering
|
|
142
|
+
filtered.sort();
|
|
143
|
+
|
|
144
|
+
return filtered.map((f) => ({
|
|
145
|
+
path: f,
|
|
146
|
+
absolutePath: resolve(cwd, f),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read the content of a file safely (returns null if unreadable / too large).
|
|
152
|
+
* @param {string} absolutePath
|
|
153
|
+
* @param {number} maxBytes - default 80KB
|
|
154
|
+
* @returns {string|null}
|
|
155
|
+
*/
|
|
156
|
+
export function readFileSafe(absolutePath, maxBytes = 80_000) {
|
|
157
|
+
try {
|
|
158
|
+
const content = readFileSync(absolutePath, 'utf-8');
|
|
159
|
+
if (content.length > maxBytes) {
|
|
160
|
+
return content.slice(0, maxBytes) + '\n\n[... truncated for context ...]';
|
|
161
|
+
}
|
|
162
|
+
return content;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build a simple tree-style folder structure string.
|
|
170
|
+
* @param {Array<{path:string}>} files
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
export function buildFolderTree(files) {
|
|
174
|
+
const dirs = new Set();
|
|
175
|
+
files.forEach(({ path }) => {
|
|
176
|
+
const parts = path.split('/');
|
|
177
|
+
for (let i = 1; i < parts.length; i++) {
|
|
178
|
+
dirs.add(parts.slice(0, i).join('/'));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Gather top-level entries
|
|
183
|
+
const topLevel = new Set();
|
|
184
|
+
files.forEach(({ path }) => {
|
|
185
|
+
topLevel.add(path.split('/')[0]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
let tree = './\n';
|
|
189
|
+
const allEntries = [
|
|
190
|
+
...Array.from(dirs).sort(),
|
|
191
|
+
...files.map((f) => f.path).filter((p) => !p.includes('/')),
|
|
192
|
+
].sort();
|
|
193
|
+
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
allEntries.forEach((entry) => {
|
|
196
|
+
if (seen.has(entry)) return;
|
|
197
|
+
seen.add(entry);
|
|
198
|
+
const depth = entry.split('/').length - 1;
|
|
199
|
+
const indent = ' '.repeat(depth);
|
|
200
|
+
const isDir = dirs.has(entry);
|
|
201
|
+
const name = entry.split('/').pop();
|
|
202
|
+
tree += `${indent}${isDir ? '📁 ' : '📄 '}${name}\n`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return tree;
|
|
206
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.js
|
|
3
|
+
* Shared utilities for logging, formatting, and path helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
// ── Logger ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const logger = {
|
|
11
|
+
info(msg) {
|
|
12
|
+
console.log(chalk.cyan(' ℹ ') + chalk.white(msg));
|
|
13
|
+
},
|
|
14
|
+
success(msg) {
|
|
15
|
+
console.log(chalk.green(' ✓ ') + chalk.white(msg));
|
|
16
|
+
},
|
|
17
|
+
warn(msg) {
|
|
18
|
+
console.log(chalk.yellow(' ⚠ ') + chalk.yellow(msg));
|
|
19
|
+
},
|
|
20
|
+
error(msg) {
|
|
21
|
+
console.error(chalk.red(' ✗ ') + chalk.red(msg));
|
|
22
|
+
},
|
|
23
|
+
dim(msg) {
|
|
24
|
+
console.log(chalk.dim(' ' + msg));
|
|
25
|
+
},
|
|
26
|
+
blank() {
|
|
27
|
+
console.log('');
|
|
28
|
+
},
|
|
29
|
+
divider() {
|
|
30
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
31
|
+
},
|
|
32
|
+
header(title) {
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.bold.cyan(` ${title}`));
|
|
35
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Formatting ───────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format bytes into a human-readable string.
|
|
43
|
+
* @param {number} bytes
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function formatBytes(bytes) {
|
|
47
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
48
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
49
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Truncate a string to a max length with ellipsis.
|
|
54
|
+
* @param {string} str
|
|
55
|
+
* @param {number} max
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function truncate(str, max = 60) {
|
|
59
|
+
if (str.length <= max) return str;
|
|
60
|
+
return str.slice(0, max - 3) + '...';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format elapsed time in ms to a readable string.
|
|
65
|
+
* @param {number} ms
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function formatDuration(ms) {
|
|
69
|
+
if (ms < 1000) return `${ms}ms`;
|
|
70
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Validation ───────────────────────────────────────────────────────────────
|
|
74
|
+
// API key validation is handled by src/ai.js → validateProviderKey()
|
|
75
|
+
|
|
76
|
+
// ── Security ─────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize an error message before printing to terminal.
|
|
80
|
+
* Redacts anything that looks like an API key or bearer token
|
|
81
|
+
* to prevent accidental credential leakage in logs.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} msg
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function sanitizeErrorMessage(msg) {
|
|
87
|
+
if (!msg) return 'Unknown error';
|
|
88
|
+
return String(msg)
|
|
89
|
+
.replace(/sk-[A-Za-z0-9\-_]{10,}/g, 'sk-[REDACTED]')
|
|
90
|
+
.replace(/gsk_[A-Za-z0-9\-_]{10,}/g, 'gsk_[REDACTED]')
|
|
91
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
|
|
92
|
+
.replace(/Authorization:\s*\S+/gi, 'Authorization: [REDACTED]');
|
|
93
|
+
}
|