mkctx 4.0.0 → 5.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/README.md +161 -140
- package/bin/mkctx.js +473 -506
- package/package.json +1 -1
package/bin/mkctx.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
5
7
|
|
|
6
8
|
// ============================================
|
|
7
|
-
// LAZY
|
|
9
|
+
// LAZY-LOADED DEPENDENCIES
|
|
8
10
|
// ============================================
|
|
9
11
|
|
|
10
12
|
let inquirer, chalk, ora;
|
|
11
13
|
|
|
12
14
|
function loadDependencies() {
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
15
|
+
if (inquirer) return;
|
|
16
|
+
inquirer = require('inquirer');
|
|
17
|
+
chalk = require('chalk');
|
|
18
|
+
ora = require('ora');
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// ============================================
|
|
@@ -23,29 +24,29 @@ function loadDependencies() {
|
|
|
23
24
|
|
|
24
25
|
const CONFIG_FILE = 'mkctx.config.json';
|
|
25
26
|
|
|
26
|
-
const
|
|
27
|
-
src:
|
|
28
|
-
ignore:
|
|
29
|
-
output:
|
|
30
|
-
first_comment:
|
|
31
|
-
last_comment:
|
|
27
|
+
const DEFAULT_CONFIG = {
|
|
28
|
+
src: '.',
|
|
29
|
+
ignore: 'mkctx.config.json, pnpm-lock.yaml, **/.titan/, mkctx/, node_modules/, .git/, dist/, build/, target/, .next/, out/, .cache, package-lock.json, *.log, temp/, tmp/, coverage/, .nyc_output, .env, .env.local, .env.development.local, .env.test.local, .env.production.local, npm-debug.log*, yarn-debug.log*, yarn-error.log*, .npm, .yarn-integrity, .parcel-cache, .vuepress/dist, .svelte-kit, **/*.rs.bk, .idea/, .vscode/, .DS_Store, Thumbs.db, *.swp, *.swo, .~lock.*, Cargo.lock, .cargo/registry/, .cargo/git/, .rustup/, *.pdb, *.dSYM/, *.so, *.dll, *.dylib, *.exe, *.lib, *.a, *.o, *.rlib, *.d, *.tmp, *.bak, *.orig, *.rej, *.pyc, *.pyo, *.class, *.jar, *.war, *.ear, *.zip, *.tar.gz, *.rar, *.7z, *.iso, *.img, *.dmg, *.pdf, *.doc, *.docx, *.xls, *.xlsx, *.ppt, *.pptx',
|
|
30
|
+
output: './mkctx',
|
|
31
|
+
first_comment: '/* Project Context */',
|
|
32
|
+
last_comment: '/* End of Context */',
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
const VALID_FORMATS = ['json', 'md', 'toon', 'xml'];
|
|
36
|
+
|
|
35
37
|
const LANG_MAP = {
|
|
36
|
-
js: 'javascript', ts: 'typescript', jsx: 'jsx',
|
|
37
|
-
py: 'python',
|
|
38
|
-
java: 'java',
|
|
39
|
-
c: 'c',
|
|
40
|
-
sh: 'bash',
|
|
41
|
-
sql: 'sql',
|
|
42
|
-
sass: 'sass',
|
|
43
|
-
yaml: 'yaml',
|
|
38
|
+
js: 'javascript', ts: 'typescript', jsx: 'jsx', tsx: 'tsx',
|
|
39
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust',
|
|
40
|
+
java: 'java', kt: 'kotlin', cs: 'csharp', cpp: 'cpp',
|
|
41
|
+
c: 'c', h: 'c', hpp: 'cpp', php: 'php',
|
|
42
|
+
sh: 'bash', bash: 'bash', zsh: 'bash', ps1: 'powershell',
|
|
43
|
+
sql: 'sql', html: 'html', css: 'css', scss: 'scss',
|
|
44
|
+
sass: 'sass', less: 'less', json: 'json', xml: 'xml',
|
|
45
|
+
yaml: 'yaml', yml: 'yaml', md: 'markdown', vue: 'vue',
|
|
44
46
|
svelte: 'svelte', dockerfile: 'dockerfile', makefile: 'makefile',
|
|
45
|
-
toml: 'toml',
|
|
47
|
+
toml: 'toml', ini: 'ini', cfg: 'ini', env: 'bash',
|
|
46
48
|
};
|
|
47
49
|
|
|
48
|
-
// Text file extensions
|
|
49
50
|
const TEXT_EXTENSIONS = new Set([
|
|
50
51
|
'.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
|
|
51
52
|
'.py', '.pyw', '.rb', '.rake', '.go', '.rs',
|
|
@@ -65,7 +66,7 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
65
66
|
'.ex', '.exs', '.erl', '.hrl', '.clj', '.cljs', '.cljc',
|
|
66
67
|
'.hs', '.lhs', '.elm', '.pug', '.jade',
|
|
67
68
|
'.ejs', '.hbs', '.handlebars', '.twig', '.blade.php',
|
|
68
|
-
'.astro', '.prisma', '.sol'
|
|
69
|
+
'.astro', '.prisma', '.sol',
|
|
69
70
|
]);
|
|
70
71
|
|
|
71
72
|
const KNOWN_FILES = new Set([
|
|
@@ -75,359 +76,393 @@ const KNOWN_FILES = new Set([
|
|
|
75
76
|
'.eslintrc', '.prettierrc', '.babelrc',
|
|
76
77
|
'.env', '.env.example', '.env.local',
|
|
77
78
|
'readme.md', 'readme.txt', 'readme',
|
|
78
|
-
'license', 'license.md', 'license.txt'
|
|
79
|
+
'license', 'license.md', 'license.txt',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const SYSTEM_IGNORES = [
|
|
83
|
+
'.git', '.DS_Store', 'Thumbs.db', 'node_modules',
|
|
84
|
+
'.svn', '.hg', '__pycache__', '.pytest_cache',
|
|
85
|
+
'.mypy_cache', '.vscode', '.idea',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Flags that take a value (not boolean)
|
|
89
|
+
const VALUE_FLAGS = new Set([
|
|
90
|
+
'src', 'output', 'format', 'ignore', 'name', 'first-comment', 'last-comment',
|
|
91
|
+
's', 'o', 'f', 'n',
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// Flags that trigger non-interactive mode
|
|
95
|
+
const NON_INTERACTIVE_FLAGS = new Set([
|
|
96
|
+
'src', 's', 'output', 'o', 'format', 'f',
|
|
97
|
+
'ignore', 'name', 'n', 'first-comment', 'last-comment',
|
|
79
98
|
]);
|
|
80
99
|
|
|
81
100
|
// ============================================
|
|
82
|
-
//
|
|
101
|
+
// CLI ARGUMENT PARSING
|
|
83
102
|
// ============================================
|
|
84
103
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
function parseArgs(argv) {
|
|
105
|
+
const args = argv.slice(2);
|
|
106
|
+
const flags = {};
|
|
107
|
+
let command = null;
|
|
108
|
+
|
|
109
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
110
|
+
command = args[0];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < args.length; i++) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
|
|
116
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
117
|
+
const eq = arg.indexOf('=');
|
|
118
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (arg.startsWith('-') && !arg.startsWith('--') && arg.includes('=')) {
|
|
123
|
+
const eq = arg.indexOf('=');
|
|
124
|
+
flags[arg.slice(1, eq)] = arg.slice(eq + 1);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (arg.startsWith('--')) {
|
|
129
|
+
const key = arg.slice(2);
|
|
130
|
+
const next = args[i + 1];
|
|
131
|
+
if (VALUE_FLAGS.has(key) && next !== undefined && !isFlagToken(next)) {
|
|
132
|
+
flags[key] = next;
|
|
133
|
+
i++;
|
|
134
|
+
} else {
|
|
135
|
+
flags[key] = true;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
141
|
+
const key = arg.slice(1);
|
|
142
|
+
const next = args[i + 1];
|
|
143
|
+
if (VALUE_FLAGS.has(key) && next !== undefined && !isFlagToken(next)) {
|
|
144
|
+
flags[key] = next;
|
|
145
|
+
i++;
|
|
146
|
+
} else {
|
|
147
|
+
flags[key] = true;
|
|
148
|
+
}
|
|
93
149
|
}
|
|
94
150
|
}
|
|
95
|
-
|
|
151
|
+
|
|
152
|
+
return { command, flags };
|
|
96
153
|
}
|
|
97
154
|
|
|
98
|
-
function
|
|
99
|
-
return
|
|
155
|
+
function isFlagToken(str) {
|
|
156
|
+
return /^--[a-zA-Z]/.test(str) || /^-[a-zA-Z]/.test(str);
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
function
|
|
103
|
-
|
|
159
|
+
function isNonInteractiveMode(flags) {
|
|
160
|
+
return Object.keys(flags).some(f => NON_INTERACTIVE_FLAGS.has(f));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Resolve short aliases to canonical names
|
|
164
|
+
function resolveFlags(flags) {
|
|
165
|
+
return {
|
|
166
|
+
src: flags.src || flags.s,
|
|
167
|
+
output: flags.output || flags.o,
|
|
168
|
+
format: flags.format || flags.f,
|
|
169
|
+
name: flags.name || flags.n,
|
|
170
|
+
ignore: flags.ignore,
|
|
171
|
+
firstComment: flags['first-comment'],
|
|
172
|
+
lastComment: flags['last-comment'],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
104
175
|
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
// ============================================
|
|
177
|
+
// CONFIG MANAGEMENT
|
|
178
|
+
// ============================================
|
|
179
|
+
|
|
180
|
+
function configFileExists() {
|
|
181
|
+
return fs.existsSync(CONFIG_FILE);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function loadConfig() {
|
|
185
|
+
if (!configFileExists()) return null;
|
|
186
|
+
try {
|
|
187
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
188
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
189
|
+
} catch {
|
|
190
|
+
console.log(chalk.yellow('⚠️ Error parsing config file, using defaults'));
|
|
191
|
+
return { ...DEFAULT_CONFIG };
|
|
107
192
|
}
|
|
193
|
+
}
|
|
108
194
|
|
|
109
|
-
|
|
110
|
-
|
|
195
|
+
function buildConfig(cliFlags = {}) {
|
|
196
|
+
const base = configFileExists() ? loadConfig() : { ...DEFAULT_CONFIG };
|
|
197
|
+
if (cliFlags.src) base.src = cliFlags.src;
|
|
198
|
+
if (cliFlags.output) base.output = cliFlags.output;
|
|
199
|
+
if (cliFlags.ignore) base.ignore = cliFlags.ignore;
|
|
200
|
+
if (cliFlags.firstComment) base.first_comment = cliFlags.firstComment;
|
|
201
|
+
if (cliFlags.lastComment) base.last_comment = cliFlags.lastComment;
|
|
202
|
+
return base;
|
|
203
|
+
}
|
|
111
204
|
|
|
205
|
+
function createConfigFile() {
|
|
206
|
+
loadDependencies();
|
|
207
|
+
fs.mkdirSync('mkctx', { recursive: true });
|
|
208
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
209
|
+
appendToGitignore();
|
|
112
210
|
console.log(chalk.green('\n✅ Configuration created:'));
|
|
113
211
|
console.log(chalk.white(' - mkctx.config.json'));
|
|
114
212
|
console.log(chalk.white(' - mkctx/ folder'));
|
|
115
213
|
console.log(chalk.white(' - Entry in .gitignore\n'));
|
|
116
214
|
}
|
|
117
215
|
|
|
118
|
-
function
|
|
216
|
+
function appendToGitignore() {
|
|
119
217
|
const gitignorePath = '.gitignore';
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
218
|
+
const current = fs.existsSync(gitignorePath)
|
|
219
|
+
? fs.readFileSync(gitignorePath, 'utf-8')
|
|
220
|
+
: '';
|
|
221
|
+
if (!current.includes('mkctx/')) {
|
|
222
|
+
fs.writeFileSync(gitignorePath, current + '\n# mkctx - generated context\nmkctx/\n');
|
|
124
223
|
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================
|
|
227
|
+
// FORMAT RESOLUTION
|
|
228
|
+
// ============================================
|
|
229
|
+
|
|
230
|
+
function resolveFormats(formatArg) {
|
|
231
|
+
if (!formatArg) return ['md'];
|
|
232
|
+
if (formatArg === 'all') return [...VALID_FORMATS];
|
|
233
|
+
|
|
234
|
+
const requested = formatArg.split(',').map(f => f.trim().toLowerCase());
|
|
235
|
+
const invalid = requested.filter(f => !VALID_FORMATS.includes(f));
|
|
125
236
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fs.writeFileSync(gitignorePath, content);
|
|
237
|
+
if (invalid.length > 0) {
|
|
238
|
+
console.error(`❌ Invalid format(s): ${invalid.join(', ')}. Valid: ${VALID_FORMATS.join(', ')}, all`);
|
|
239
|
+
process.exit(1);
|
|
130
240
|
}
|
|
241
|
+
|
|
242
|
+
return requested;
|
|
131
243
|
}
|
|
132
244
|
|
|
133
245
|
// ============================================
|
|
134
|
-
// UTILITY
|
|
246
|
+
// UTILITY HELPERS
|
|
135
247
|
// ============================================
|
|
136
248
|
|
|
137
249
|
function formatSize(bytes) {
|
|
138
|
-
const
|
|
250
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
139
251
|
if (bytes === 0) return '0 B';
|
|
140
252
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
141
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${
|
|
253
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
142
254
|
}
|
|
143
255
|
|
|
144
256
|
function estimateTokens(text) {
|
|
145
257
|
return Math.ceil(text.length / 4);
|
|
146
258
|
}
|
|
147
259
|
|
|
148
|
-
|
|
149
|
-
function normalizePath(filePath) {
|
|
260
|
+
function toUnixPath(filePath) {
|
|
150
261
|
return filePath.replace(/\\/g, '/');
|
|
151
262
|
}
|
|
152
263
|
|
|
153
264
|
// ============================================
|
|
154
|
-
// FILE
|
|
265
|
+
// FILE FILTERING
|
|
155
266
|
// ============================================
|
|
156
267
|
|
|
157
268
|
function isTextFile(filename) {
|
|
158
|
-
const ext
|
|
269
|
+
const ext = path.extname(filename).toLowerCase();
|
|
159
270
|
const basename = path.basename(filename).toLowerCase();
|
|
160
|
-
|
|
161
|
-
if (KNOWN_FILES.has(basename)) return true;
|
|
162
|
-
if (ext && TEXT_EXTENSIONS.has(ext)) return true;
|
|
163
|
-
|
|
164
|
-
return false;
|
|
271
|
+
return KNOWN_FILES.has(basename) || (!!ext && TEXT_EXTENSIONS.has(ext));
|
|
165
272
|
}
|
|
166
273
|
|
|
167
274
|
function getLanguage(filename) {
|
|
168
|
-
const ext
|
|
275
|
+
const ext = path.extname(filename).slice(1).toLowerCase();
|
|
169
276
|
const basename = path.basename(filename).toLowerCase();
|
|
170
|
-
|
|
171
|
-
if (basename === '
|
|
172
|
-
if (basename
|
|
173
|
-
if (basename.startsWith('.env')) return 'bash';
|
|
174
|
-
|
|
277
|
+
if (basename === 'dockerfile') return 'dockerfile';
|
|
278
|
+
if (basename === 'makefile') return 'makefile';
|
|
279
|
+
if (basename.startsWith('.env')) return 'bash';
|
|
175
280
|
return LANG_MAP[ext] || ext || 'text';
|
|
176
281
|
}
|
|
177
282
|
|
|
178
283
|
function parseIgnorePatterns(ignoreString) {
|
|
179
284
|
if (!ignoreString) return [];
|
|
180
|
-
return ignoreString
|
|
181
|
-
.split(',')
|
|
182
|
-
.map(p => p.trim())
|
|
183
|
-
.filter(Boolean);
|
|
285
|
+
return ignoreString.split(',').map(p => p.trim()).filter(Boolean);
|
|
184
286
|
}
|
|
185
287
|
|
|
186
|
-
function matchWildcard(pattern,
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
288
|
+
function matchWildcard(pattern, subject) {
|
|
289
|
+
const regex = new RegExp(
|
|
290
|
+
'^' + pattern
|
|
291
|
+
.replace(/\./g, '\\.')
|
|
292
|
+
.replace(/\*\*/g, '.*')
|
|
293
|
+
.replace(/\*/g, '[^/]*') + '$',
|
|
294
|
+
'i'
|
|
295
|
+
);
|
|
296
|
+
return regex.test(subject);
|
|
193
297
|
}
|
|
194
298
|
|
|
195
299
|
function shouldIgnore(fullPath, name, relativePath, patterns) {
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const ignore of systemIgnores) {
|
|
207
|
-
if (normalizedFull.includes('/' + ignore + '/') ||
|
|
208
|
-
normalizedFull.includes('/' + ignore) ||
|
|
209
|
-
normalizedFull.endsWith('/' + ignore) ||
|
|
210
|
-
name === ignore) {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
300
|
+
const normFull = toUnixPath(fullPath);
|
|
301
|
+
const normRelative = toUnixPath(relativePath);
|
|
302
|
+
|
|
303
|
+
const inSystemIgnore = SYSTEM_IGNORES.some(ig =>
|
|
304
|
+
normFull.includes(`/${ig}/`) ||
|
|
305
|
+
normFull.includes(`/${ig}`) ||
|
|
306
|
+
normFull.endsWith(`/${ig}`) ||
|
|
307
|
+
name === ig
|
|
308
|
+
);
|
|
309
|
+
if (inSystemIgnore) return true;
|
|
214
310
|
|
|
215
|
-
|
|
311
|
+
return patterns.some(pattern => {
|
|
216
312
|
if (pattern.includes('*')) {
|
|
217
|
-
|
|
218
|
-
if (matchWildcard(pattern, normalizedRelative)) return true;
|
|
313
|
+
return matchWildcard(pattern, name) || matchWildcard(pattern, normRelative);
|
|
219
314
|
}
|
|
220
|
-
|
|
221
315
|
if (pattern.endsWith('/')) {
|
|
222
316
|
const dir = pattern.slice(0, -1);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
317
|
+
return normFull.includes(`/${dir}/`) ||
|
|
318
|
+
normFull.endsWith(`/${dir}`) ||
|
|
319
|
+
name === dir;
|
|
228
320
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return false;
|
|
321
|
+
return normRelative === pattern || name === pattern;
|
|
322
|
+
});
|
|
236
323
|
}
|
|
237
324
|
|
|
238
325
|
// ============================================
|
|
239
|
-
//
|
|
326
|
+
// FILE SCANNING
|
|
240
327
|
// ============================================
|
|
241
328
|
|
|
242
|
-
function
|
|
243
|
-
const jsonArray = [];
|
|
329
|
+
function scanFiles(srcPath, config) {
|
|
244
330
|
const ignorePatterns = parseIgnorePatterns(config.ignore);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const filesByExt = {};
|
|
331
|
+
const files = [];
|
|
332
|
+
const stats = { files: 0, totalSize: 0, totalLines: 0, filesByExt: {} };
|
|
248
333
|
|
|
249
334
|
function walk(dir) {
|
|
250
335
|
if (!fs.existsSync(dir)) return;
|
|
251
336
|
|
|
252
337
|
let entries;
|
|
253
|
-
try {
|
|
254
|
-
|
|
255
|
-
} catch (err) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
338
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
339
|
+
catch { return; }
|
|
258
340
|
|
|
259
341
|
for (const entry of entries) {
|
|
260
|
-
const fullPath
|
|
342
|
+
const fullPath = path.join(dir, entry.name);
|
|
261
343
|
const relativePath = path.relative(srcPath, fullPath);
|
|
262
344
|
|
|
263
|
-
if (shouldIgnore(fullPath, entry.name, relativePath, ignorePatterns))
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
345
|
+
if (shouldIgnore(fullPath, entry.name, relativePath, ignorePatterns)) continue;
|
|
266
346
|
|
|
267
|
-
if (entry.isDirectory()) {
|
|
268
|
-
|
|
269
|
-
} else if (entry.isFile() && isTextFile(entry.name)) {
|
|
270
|
-
// Read file immediately when found
|
|
271
|
-
let content;
|
|
272
|
-
try {
|
|
273
|
-
content = fs.readFileSync(fullPath, 'utf-8');
|
|
274
|
-
} catch (err) {
|
|
275
|
-
// Skip files that can't be read
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
347
|
+
if (entry.isDirectory()) { walk(fullPath); continue; }
|
|
348
|
+
if (!entry.isFile() || !isTextFile(entry.name)) continue;
|
|
278
349
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
extension: ext,
|
|
294
|
-
language: language,
|
|
295
|
-
lines: lines,
|
|
296
|
-
size: size,
|
|
297
|
-
content: content
|
|
298
|
-
});
|
|
299
|
-
}
|
|
350
|
+
let content;
|
|
351
|
+
try { content = fs.readFileSync(fullPath, 'utf-8'); }
|
|
352
|
+
catch { continue; }
|
|
353
|
+
|
|
354
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase() || null;
|
|
355
|
+
const lines = content.split('\n').length;
|
|
356
|
+
const size = Buffer.byteLength(content, 'utf-8');
|
|
357
|
+
const language = getLanguage(entry.name);
|
|
358
|
+
|
|
359
|
+
stats.totalSize += size;
|
|
360
|
+
stats.totalLines += lines;
|
|
361
|
+
stats.filesByExt[ext || 'other'] = (stats.filesByExt[ext || 'other'] || 0) + 1;
|
|
362
|
+
|
|
363
|
+
files.push({ path: toUnixPath(relativePath), name: entry.name, extension: ext, language, lines, size, content });
|
|
300
364
|
}
|
|
301
365
|
}
|
|
302
366
|
|
|
303
367
|
walk(srcPath);
|
|
368
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
369
|
+
stats.files = files.length;
|
|
304
370
|
|
|
305
|
-
|
|
306
|
-
jsonArray.sort((a, b) => a.path.localeCompare(b.path));
|
|
307
|
-
|
|
308
|
-
const stats = {
|
|
309
|
-
files: jsonArray.length,
|
|
310
|
-
totalSize,
|
|
311
|
-
totalLines,
|
|
312
|
-
filesByExt
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
return { jsonArray, stats };
|
|
371
|
+
return { files, stats };
|
|
316
372
|
}
|
|
317
373
|
|
|
318
374
|
// ============================================
|
|
319
|
-
//
|
|
375
|
+
// OUTPUT FORMATTERS
|
|
320
376
|
// ============================================
|
|
321
377
|
|
|
322
|
-
function toJson(
|
|
323
|
-
return JSON.stringify(
|
|
378
|
+
function toJson(files) {
|
|
379
|
+
return JSON.stringify(files, null, 2);
|
|
324
380
|
}
|
|
325
381
|
|
|
326
|
-
function toMarkdown(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (config.last_comment) {
|
|
357
|
-
content += config.last_comment;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return content;
|
|
382
|
+
function toMarkdown(files, config) {
|
|
383
|
+
const dirs = [...new Set(
|
|
384
|
+
files.map(f => path.dirname(f.path)).filter(d => d !== '.')
|
|
385
|
+
)].sort();
|
|
386
|
+
|
|
387
|
+
const structure = dirs.map(d => `📁 ${d}/`).join('\n');
|
|
388
|
+
|
|
389
|
+
const sources = files.map(file => {
|
|
390
|
+
const body = file.content.endsWith('\n') ? file.content : file.content + '\n';
|
|
391
|
+
return `### ${file.path}\n\n\`\`\`${file.language}\n${body}\`\`\`\n`;
|
|
392
|
+
}).join('\n');
|
|
393
|
+
|
|
394
|
+
return [
|
|
395
|
+
config.first_comment,
|
|
396
|
+
'',
|
|
397
|
+
'## Project Structure',
|
|
398
|
+
'',
|
|
399
|
+
'```',
|
|
400
|
+
structure,
|
|
401
|
+
'',
|
|
402
|
+
`${files.length} files total`,
|
|
403
|
+
'```',
|
|
404
|
+
'',
|
|
405
|
+
'## Source Files',
|
|
406
|
+
'',
|
|
407
|
+
sources,
|
|
408
|
+
config.last_comment,
|
|
409
|
+
].join('\n');
|
|
361
410
|
}
|
|
362
411
|
|
|
363
|
-
function toToon(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
content += `language: ${file.language}\n`;
|
|
388
|
-
content += `content:\n`;
|
|
389
|
-
// Indent each line with 2 spaces
|
|
390
|
-
const lines = file.content.split('\n');
|
|
391
|
-
for (const line of lines) {
|
|
392
|
-
content += ` ${line}\n`;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return content;
|
|
412
|
+
function toToon(files, stats) {
|
|
413
|
+
const header = [
|
|
414
|
+
'# Project Context',
|
|
415
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
416
|
+
`# Files: ${files.length}`,
|
|
417
|
+
`# Lines: ${stats.totalLines}`,
|
|
418
|
+
`# Size: ${stats.totalSize} bytes`,
|
|
419
|
+
'',
|
|
420
|
+
`files[${files.length}]{path,name,extension,language,lines,size}:`,
|
|
421
|
+
...files.map(f =>
|
|
422
|
+
` ${escapeToon(f.path)},${escapeToon(f.name)},${f.extension || ''},${f.language},${f.lines},${f.size}`
|
|
423
|
+
),
|
|
424
|
+
'',
|
|
425
|
+
].join('\n');
|
|
426
|
+
|
|
427
|
+
const bodies = files.map((f, i) => [
|
|
428
|
+
'---',
|
|
429
|
+
`[${i}] ${f.path}`,
|
|
430
|
+
`language: ${f.language}`,
|
|
431
|
+
'content:',
|
|
432
|
+
...f.content.split('\n').map(l => ` ${l}`),
|
|
433
|
+
].join('\n')).join('\n');
|
|
434
|
+
|
|
435
|
+
return header + bodies;
|
|
397
436
|
}
|
|
398
437
|
|
|
399
|
-
function
|
|
400
|
-
if (value
|
|
401
|
-
const str
|
|
402
|
-
|
|
403
|
-
str.startsWith(' ') || str.endsWith(' ')
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function toXml(baseJson) {
|
|
410
|
-
let content = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
411
|
-
content += '<context>\n';
|
|
412
|
-
|
|
413
|
-
for (const file of baseJson) {
|
|
414
|
-
content += ` <file>\n`;
|
|
415
|
-
content += ` <path>${escapeXml(file.path)}</path>\n`;
|
|
416
|
-
content += ` <name>${escapeXml(file.name)}</name>\n`;
|
|
417
|
-
content += ` <extension>${escapeXml(file.extension || '')}</extension>\n`;
|
|
418
|
-
content += ` <language>${escapeXml(file.language)}</language>\n`;
|
|
419
|
-
content += ` <lines>${file.lines}</lines>\n`;
|
|
420
|
-
content += ` <size>${file.size}</size>\n`;
|
|
421
|
-
content += ` <content><![CDATA[\n${file.content}${file.content.endsWith('\n') ? '' : '\n'}]]></content>\n`;
|
|
422
|
-
content += ` </file>\n`;
|
|
423
|
-
}
|
|
438
|
+
function escapeToon(value) {
|
|
439
|
+
if (value == null) return '';
|
|
440
|
+
const str = String(value);
|
|
441
|
+
const needsQuotes = str.includes(',') || str.includes('\n') || str.includes('"')
|
|
442
|
+
|| str.startsWith(' ') || str.endsWith(' ');
|
|
443
|
+
return needsQuotes
|
|
444
|
+
? `"${str.replace(/"/g, '""').replace(/\n/g, '\\n')}"`
|
|
445
|
+
: str;
|
|
446
|
+
}
|
|
424
447
|
|
|
425
|
-
|
|
426
|
-
|
|
448
|
+
function toXml(files) {
|
|
449
|
+
const fileEntries = files.map(f => [
|
|
450
|
+
' <file>',
|
|
451
|
+
` <path>${escapeXml(f.path)}</path>`,
|
|
452
|
+
` <name>${escapeXml(f.name)}</name>`,
|
|
453
|
+
` <extension>${escapeXml(f.extension || '')}</extension>`,
|
|
454
|
+
` <language>${escapeXml(f.language)}</language>`,
|
|
455
|
+
` <lines>${f.lines}</lines>`,
|
|
456
|
+
` <size>${f.size}</size>`,
|
|
457
|
+
` <content><![CDATA[\n${f.content}${f.content.endsWith('\n') ? '' : '\n'}]]></content>`,
|
|
458
|
+
' </file>',
|
|
459
|
+
].join('\n')).join('\n');
|
|
460
|
+
|
|
461
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<context>\n${fileEntries}\n</context>\n`;
|
|
427
462
|
}
|
|
428
463
|
|
|
429
464
|
function escapeXml(str) {
|
|
430
|
-
if (str
|
|
465
|
+
if (str == null) return '';
|
|
431
466
|
return String(str)
|
|
432
467
|
.replace(/&/g, '&')
|
|
433
468
|
.replace(/</g, '<')
|
|
@@ -436,125 +471,76 @@ function escapeXml(str) {
|
|
|
436
471
|
.replace(/'/g, ''');
|
|
437
472
|
}
|
|
438
473
|
|
|
474
|
+
function renderFormat(format, files, stats, config) {
|
|
475
|
+
switch (format) {
|
|
476
|
+
case 'json': return { content: toJson(files), ext: 'json' };
|
|
477
|
+
case 'md': return { content: toMarkdown(files, config), ext: 'md' };
|
|
478
|
+
case 'toon': return { content: toToon(files, stats), ext: 'toon' };
|
|
479
|
+
case 'xml': return { content: toXml(files), ext: 'xml' };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
439
483
|
// ============================================
|
|
440
484
|
// CONTEXT GENERATION
|
|
441
485
|
// ============================================
|
|
442
486
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const { srcPath } = await inquirer.prompt([
|
|
447
|
-
{
|
|
448
|
-
type: 'input',
|
|
449
|
-
name: 'srcPath',
|
|
450
|
-
message: 'Enter the source path to analyze:',
|
|
451
|
-
default: '.',
|
|
452
|
-
validate: (input) => {
|
|
453
|
-
if (!fs.existsSync(input)) {
|
|
454
|
-
return `Path does not exist: ${input}`;
|
|
455
|
-
}
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
]);
|
|
460
|
-
|
|
461
|
-
const config = { ...DEFAULT_PROJECT_CONFIG, src: srcPath };
|
|
462
|
-
return generateContext(config, srcPath);
|
|
463
|
-
}
|
|
487
|
+
function generateContext(config) {
|
|
488
|
+
const spinner = ora(`Scanning ${config.src}...`).start();
|
|
464
489
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const config = loadProjectConfig();
|
|
469
|
-
if (!config) {
|
|
470
|
-
console.log(chalk.yellow('\n⚠️ No config file found.'));
|
|
490
|
+
if (!fs.existsSync(config.src)) {
|
|
491
|
+
spinner.fail(`Source path does not exist: ${config.src}`);
|
|
471
492
|
return null;
|
|
472
493
|
}
|
|
473
494
|
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function generateContext(config, srcPath) {
|
|
478
|
-
const spinner = ora(`Scanning and reading files from ${srcPath}...`).start();
|
|
495
|
+
const { files, stats } = scanFiles(config.src, config);
|
|
479
496
|
|
|
480
|
-
if (
|
|
481
|
-
spinner.fail(`
|
|
497
|
+
if (files.length === 0) {
|
|
498
|
+
spinner.fail(`No files found in: ${config.src}`);
|
|
482
499
|
return null;
|
|
483
500
|
}
|
|
484
501
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (jsonArray.length === 0) {
|
|
489
|
-
spinner.fail(`No files found in: ${srcPath}`);
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
spinner.succeed(`Context built: ${chalk.yellow(jsonArray.length)} files, ${chalk.yellow(formatSize(stats.totalSize))}`);
|
|
494
|
-
|
|
495
|
-
return {
|
|
496
|
-
baseJson: jsonArray,
|
|
497
|
-
stats: stats,
|
|
498
|
-
config
|
|
499
|
-
};
|
|
502
|
+
spinner.succeed(`Context built: ${chalk.yellow(files.length)} files, ${chalk.yellow(formatSize(stats.totalSize))}`);
|
|
503
|
+
return { files, stats, config };
|
|
500
504
|
}
|
|
501
505
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
function printSummary(stats) {
|
|
507
|
+
console.log(chalk.cyan('\n📊 Context Summary:'));
|
|
508
|
+
console.log(chalk.white(` Files: ${stats.files}`));
|
|
509
|
+
console.log(chalk.white(` Lines: ${stats.totalLines.toLocaleString()}`));
|
|
510
|
+
console.log(chalk.white(` Size: ${formatSize(stats.totalSize)}`));
|
|
511
|
+
}
|
|
505
512
|
|
|
506
513
|
// ============================================
|
|
507
514
|
// SAVE CONTEXT
|
|
508
515
|
// ============================================
|
|
509
516
|
|
|
510
|
-
async function saveContext(result, formats) {
|
|
517
|
+
async function saveContext(result, formats, fileName) {
|
|
511
518
|
loadDependencies();
|
|
512
519
|
|
|
513
|
-
|
|
514
|
-
{
|
|
515
|
-
type: 'input',
|
|
516
|
-
name: 'fileName',
|
|
520
|
+
if (!fileName) {
|
|
521
|
+
const answer = await inquirer.prompt([{
|
|
522
|
+
type: 'input', name: 'fileName',
|
|
517
523
|
message: 'Enter a name for the output files:',
|
|
518
|
-
default: 'context'
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
let outputPath = result.config.output || './mkctx';
|
|
523
|
-
|
|
524
|
-
if (!fs.existsSync(outputPath)) {
|
|
525
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
524
|
+
default: 'context',
|
|
525
|
+
}]);
|
|
526
|
+
fileName = answer.fileName;
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
const outputDir = result.config.output || './mkctx';
|
|
530
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
531
|
+
|
|
528
532
|
const savedFiles = [];
|
|
529
533
|
|
|
530
534
|
for (const format of formats) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
content = toMarkdown(result.baseJson, result.config);
|
|
541
|
-
filename = `${fileName}.md`;
|
|
542
|
-
break;
|
|
543
|
-
case 'toon':
|
|
544
|
-
content = toToon(result.baseJson, result.stats);
|
|
545
|
-
filename = `${fileName}.toon`;
|
|
546
|
-
break;
|
|
547
|
-
case 'xml':
|
|
548
|
-
content = toXml(result.baseJson);
|
|
549
|
-
filename = `${fileName}.xml`;
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const outputFile = path.join(outputPath, filename);
|
|
554
|
-
fs.writeFileSync(outputFile, content);
|
|
555
|
-
const size = Buffer.byteLength(content, 'utf-8');
|
|
556
|
-
const tokens = estimateTokens(content);
|
|
557
|
-
savedFiles.push({ format, file: outputFile, size, tokens });
|
|
535
|
+
const { content, ext } = renderFormat(format, result.files, result.stats, result.config);
|
|
536
|
+
const outputPath = path.join(outputDir, `${fileName}.${ext}`);
|
|
537
|
+
fs.writeFileSync(outputPath, content);
|
|
538
|
+
savedFiles.push({
|
|
539
|
+
format,
|
|
540
|
+
file: outputPath,
|
|
541
|
+
size: Buffer.byteLength(content, 'utf-8'),
|
|
542
|
+
tokens: estimateTokens(content),
|
|
543
|
+
});
|
|
558
544
|
}
|
|
559
545
|
|
|
560
546
|
console.log(chalk.green('\n✅ Context saved:\n'));
|
|
@@ -567,59 +553,38 @@ async function saveContext(result, formats) {
|
|
|
567
553
|
}
|
|
568
554
|
|
|
569
555
|
// ============================================
|
|
570
|
-
//
|
|
556
|
+
// INTERACTIVE PROMPTS
|
|
571
557
|
// ============================================
|
|
572
558
|
|
|
573
|
-
async function
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
default: 'all',
|
|
582
|
-
choices: [
|
|
583
|
-
{
|
|
584
|
-
name: chalk.magenta('📦 All formats (MD, JSON, TOON, XML)'),
|
|
585
|
-
value: 'all'
|
|
586
|
-
},
|
|
587
|
-
new inquirer.Separator(),
|
|
588
|
-
{
|
|
589
|
-
name: chalk.blue('📝 Markdown (.md)'),
|
|
590
|
-
value: 'md'
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
name: chalk.green('🔧 JSON (.json) - Simple array'),
|
|
594
|
-
value: 'json'
|
|
595
|
-
},
|
|
596
|
-
{
|
|
597
|
-
name: chalk.yellow('🎒 TOON (.toon) - Token-optimized'),
|
|
598
|
-
value: 'toon'
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
name: chalk.red('📄 XML (.xml)'),
|
|
602
|
-
value: 'xml'
|
|
603
|
-
}
|
|
604
|
-
]
|
|
605
|
-
}
|
|
606
|
-
]);
|
|
607
|
-
|
|
608
|
-
if (format === 'all') {
|
|
609
|
-
return ['json', 'md', 'toon', 'xml'];
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return [format];
|
|
559
|
+
async function promptSrcPath() {
|
|
560
|
+
const { srcPath } = await inquirer.prompt([{
|
|
561
|
+
type: 'input', name: 'srcPath',
|
|
562
|
+
message: 'Enter the source path to analyze:',
|
|
563
|
+
default: '.',
|
|
564
|
+
validate: input => fs.existsSync(input) || `Path does not exist: ${input}`,
|
|
565
|
+
}]);
|
|
566
|
+
return srcPath;
|
|
613
567
|
}
|
|
614
568
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
569
|
+
async function promptFormat() {
|
|
570
|
+
const { format } = await inquirer.prompt([{
|
|
571
|
+
type: 'list', name: 'format',
|
|
572
|
+
message: 'Select output format:',
|
|
573
|
+
default: 'all',
|
|
574
|
+
choices: [
|
|
575
|
+
{ name: chalk.magenta('📦 All formats (MD, JSON, TOON, XML)'), value: 'all' },
|
|
576
|
+
new inquirer.Separator(),
|
|
577
|
+
{ name: chalk.blue('📝 Markdown (.md)'), value: 'md' },
|
|
578
|
+
{ name: chalk.green('🔧 JSON (.json) - Simple array'), value: 'json' },
|
|
579
|
+
{ name: chalk.yellow('🎒 TOON (.toon) - Token-optimized'), value: 'toon' },
|
|
580
|
+
{ name: chalk.red('📄 XML (.xml)'), value: 'xml' },
|
|
581
|
+
],
|
|
582
|
+
}]);
|
|
583
|
+
return resolveFormats(format);
|
|
584
|
+
}
|
|
621
585
|
|
|
622
|
-
|
|
586
|
+
async function promptMainMenu() {
|
|
587
|
+
const hasConfig = configFileExists();
|
|
623
588
|
|
|
624
589
|
console.log(chalk.cyan('\n╔════════════════════════════════════════╗'));
|
|
625
590
|
console.log(chalk.cyan('║') + chalk.cyan.bold(' 📄 mkctx - Make Context ') + chalk.cyan('║'));
|
|
@@ -628,116 +593,96 @@ async function showMainMenu() {
|
|
|
628
593
|
const choices = [];
|
|
629
594
|
|
|
630
595
|
if (hasConfig) {
|
|
631
|
-
choices.push({
|
|
632
|
-
name: chalk.green('📁 Generate from config file'),
|
|
633
|
-
value: 'from-config'
|
|
634
|
-
});
|
|
596
|
+
choices.push({ name: chalk.green('📁 Generate from config file'), value: 'from-config' });
|
|
635
597
|
}
|
|
636
598
|
|
|
637
599
|
choices.push(
|
|
638
|
-
{
|
|
639
|
-
name: chalk.blue('🔍 Generate dynamically (choose path)'),
|
|
640
|
-
value: 'dynamic'
|
|
641
|
-
},
|
|
600
|
+
{ name: chalk.blue('🔍 Generate dynamically (choose path)'), value: 'dynamic' },
|
|
642
601
|
new inquirer.Separator(),
|
|
643
602
|
{
|
|
644
|
-
name:
|
|
645
|
-
|
|
646
|
-
: chalk.yellow('⚙️ Create configuration file'),
|
|
647
|
-
value: 'config'
|
|
603
|
+
name: hasConfig ? chalk.gray('⚙️ View configuration') : chalk.yellow('⚙️ Create configuration file'),
|
|
604
|
+
value: 'config',
|
|
648
605
|
},
|
|
649
606
|
new inquirer.Separator(),
|
|
650
|
-
{
|
|
651
|
-
name: chalk.red('❌ Exit'),
|
|
652
|
-
value: 'exit'
|
|
653
|
-
}
|
|
607
|
+
{ name: chalk.red('❌ Exit'), value: 'exit' },
|
|
654
608
|
);
|
|
655
609
|
|
|
656
|
-
const { action } = await inquirer.prompt([
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
choices
|
|
662
|
-
}
|
|
663
|
-
]);
|
|
610
|
+
const { action } = await inquirer.prompt([{
|
|
611
|
+
type: 'list', name: 'action',
|
|
612
|
+
message: 'What would you like to do?',
|
|
613
|
+
choices,
|
|
614
|
+
}]);
|
|
664
615
|
|
|
665
616
|
return action;
|
|
666
617
|
}
|
|
667
618
|
|
|
668
619
|
// ============================================
|
|
669
|
-
//
|
|
620
|
+
// EXECUTION FLOWS
|
|
670
621
|
// ============================================
|
|
671
622
|
|
|
672
|
-
async function
|
|
673
|
-
|
|
623
|
+
async function runNonInteractive(rawFlags) {
|
|
624
|
+
loadDependencies();
|
|
674
625
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
626
|
+
const cli = resolveFlags(rawFlags);
|
|
627
|
+
const config = buildConfig(cli);
|
|
628
|
+
const formats = resolveFormats(cli.format);
|
|
629
|
+
const fileName = cli.name || 'context';
|
|
679
630
|
|
|
680
|
-
if (
|
|
681
|
-
|
|
682
|
-
|
|
631
|
+
if (!fs.existsSync(config.src)) {
|
|
632
|
+
console.error(`❌ Source path does not exist: ${config.src}`);
|
|
633
|
+
process.exit(1);
|
|
683
634
|
}
|
|
684
635
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
createProjectConfig();
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
636
|
+
const result = generateContext(config);
|
|
637
|
+
if (!result) process.exit(1);
|
|
690
638
|
|
|
639
|
+
printSummary(result.stats);
|
|
640
|
+
await saveContext(result, formats, fileName);
|
|
641
|
+
console.log(chalk.yellow('👋 Done!\n'));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function runInteractive() {
|
|
691
645
|
loadDependencies();
|
|
692
646
|
|
|
693
647
|
let running = true;
|
|
694
648
|
|
|
695
649
|
while (running) {
|
|
696
|
-
const action = await
|
|
650
|
+
const action = await promptMainMenu();
|
|
697
651
|
|
|
698
652
|
switch (action) {
|
|
699
653
|
case 'from-config': {
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
console.log(chalk.yellow('👋 Done!\n'));
|
|
711
|
-
running = false;
|
|
712
|
-
}
|
|
654
|
+
const config = loadConfig();
|
|
655
|
+
if (!config) { console.log(chalk.yellow('\n⚠️ No config file found.')); break; }
|
|
656
|
+
const result = generateContext(config);
|
|
657
|
+
if (!result) break;
|
|
658
|
+
printSummary(result.stats);
|
|
659
|
+
const formats = await promptFormat();
|
|
660
|
+
await saveContext(result, formats);
|
|
661
|
+
console.log(chalk.yellow('👋 Done!\n'));
|
|
662
|
+
running = false;
|
|
713
663
|
break;
|
|
714
664
|
}
|
|
715
665
|
|
|
716
666
|
case 'dynamic': {
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
console.log(chalk.yellow('👋 Done!\n'));
|
|
728
|
-
running = false;
|
|
729
|
-
}
|
|
667
|
+
const srcPath = await promptSrcPath();
|
|
668
|
+
const config = { ...DEFAULT_CONFIG, src: srcPath };
|
|
669
|
+
const result = generateContext(config);
|
|
670
|
+
if (!result) break;
|
|
671
|
+
printSummary(result.stats);
|
|
672
|
+
const formats = await promptFormat();
|
|
673
|
+
await saveContext(result, formats);
|
|
674
|
+
console.log(chalk.yellow('👋 Done!\n'));
|
|
675
|
+
running = false;
|
|
730
676
|
break;
|
|
731
677
|
}
|
|
732
678
|
|
|
733
679
|
case 'config':
|
|
734
|
-
if (
|
|
680
|
+
if (configFileExists()) {
|
|
735
681
|
console.log(chalk.cyan('\n📄 Current configuration:\n'));
|
|
736
|
-
|
|
737
|
-
console.log(JSON.stringify(config, null, 2));
|
|
682
|
+
console.log(JSON.stringify(loadConfig(), null, 2));
|
|
738
683
|
console.log(chalk.gray(`\n Edit ${CONFIG_FILE} to modify settings.\n`));
|
|
739
684
|
} else {
|
|
740
|
-
|
|
685
|
+
createConfigFile();
|
|
741
686
|
}
|
|
742
687
|
break;
|
|
743
688
|
|
|
@@ -749,9 +694,12 @@ async function main() {
|
|
|
749
694
|
}
|
|
750
695
|
}
|
|
751
696
|
|
|
697
|
+
// ============================================
|
|
698
|
+
// HELP & VERSION
|
|
699
|
+
// ============================================
|
|
700
|
+
|
|
752
701
|
function showHelp() {
|
|
753
702
|
loadDependencies();
|
|
754
|
-
|
|
755
703
|
console.log(chalk.cyan(`
|
|
756
704
|
╔════════════════════════════════════════════════════════════╗
|
|
757
705
|
║ 📄 mkctx - Make Context for AI Code Analysis ║
|
|
@@ -760,28 +708,35 @@ function showHelp() {
|
|
|
760
708
|
${chalk.white('Generate context files from your codebase for AI analysis.')}
|
|
761
709
|
|
|
762
710
|
${chalk.yellow('Usage:')}
|
|
763
|
-
mkctx
|
|
764
|
-
mkctx config
|
|
765
|
-
mkctx help
|
|
766
|
-
mkctx version
|
|
711
|
+
mkctx Interactive mode
|
|
712
|
+
mkctx config Create configuration file
|
|
713
|
+
mkctx help / --help Show this help message
|
|
714
|
+
mkctx version / --version Show version
|
|
715
|
+
|
|
716
|
+
${chalk.yellow('Non-interactive flags (skip all prompts):')}
|
|
717
|
+
--src <path> Source directory (default: .)
|
|
718
|
+
--output <path> Output directory (default: ./mkctx)
|
|
719
|
+
--format <fmt> md, json, toon, xml, all, or comma-separated
|
|
720
|
+
--name <filename> Output file base name (default: context)
|
|
721
|
+
--ignore <patterns> Comma-separated ignore patterns
|
|
722
|
+
--first-comment <text> Override first comment header
|
|
723
|
+
--last-comment <text> Override last comment footer
|
|
724
|
+
|
|
725
|
+
${chalk.yellow('Short aliases:')}
|
|
726
|
+
-s --src -o --output -f --format -n --name
|
|
727
|
+
|
|
728
|
+
${chalk.yellow('Examples:')}
|
|
729
|
+
mkctx --src ./src
|
|
730
|
+
mkctx --src . --format all --name my-project --output ./docs
|
|
731
|
+
mkctx --src ./app --format md,json --ignore "*.test.ts,__tests__/"
|
|
732
|
+
mkctx -s ./src -f toon -n snapshot
|
|
767
733
|
|
|
768
734
|
${chalk.yellow('Output Formats:')}
|
|
769
|
-
${chalk.green('JSON')} Simple array of file objects
|
|
735
|
+
${chalk.green('JSON')} Simple array of file objects
|
|
770
736
|
${chalk.blue('MD')} Markdown with code blocks
|
|
771
737
|
${chalk.yellow('TOON')} Token-Oriented Object Notation (LLM optimized)
|
|
772
738
|
${chalk.red('XML')} XML with CDATA sections
|
|
773
739
|
|
|
774
|
-
${chalk.yellow('JSON Structure:')}
|
|
775
|
-
[{
|
|
776
|
-
"path": "src/index.ts",
|
|
777
|
-
"name": "index.ts",
|
|
778
|
-
"extension": "ts",
|
|
779
|
-
"language": "typescript",
|
|
780
|
-
"lines": 150,
|
|
781
|
-
"size": 4096,
|
|
782
|
-
"content": "..."
|
|
783
|
-
}]
|
|
784
|
-
|
|
785
740
|
${chalk.gray('More info: https://github.com/pnkkzero/mkctx')}
|
|
786
741
|
`));
|
|
787
742
|
}
|
|
@@ -796,13 +751,25 @@ function showVersion() {
|
|
|
796
751
|
}
|
|
797
752
|
|
|
798
753
|
// ============================================
|
|
799
|
-
//
|
|
754
|
+
// ENTRY POINT
|
|
800
755
|
// ============================================
|
|
801
756
|
|
|
802
|
-
main()
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
757
|
+
async function main() {
|
|
758
|
+
const { command, flags } = parseArgs(process.argv);
|
|
759
|
+
|
|
760
|
+
if (flags.help || flags.h || command === 'help') return showHelp();
|
|
761
|
+
if (flags.version || flags.v || command === 'version') return showVersion();
|
|
762
|
+
if (command === 'config') { loadDependencies(); return createConfigFile(); }
|
|
763
|
+
|
|
764
|
+
if (isNonInteractiveMode(flags)) {
|
|
765
|
+
await runNonInteractive(flags);
|
|
766
|
+
} else {
|
|
767
|
+
await runInteractive();
|
|
806
768
|
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
main().catch(err => {
|
|
772
|
+
console.error(`\n❌ Error: ${err.message}`);
|
|
773
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
807
774
|
process.exit(1);
|
|
808
775
|
});
|