mkctx 2.0.1 → 4.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.
Files changed (3) hide show
  1. package/README.md +146 -135
  2. package/bin/mkctx.js +720 -374
  3. package/package.json +15 -6
package/bin/mkctx.js CHANGED
@@ -2,461 +2,807 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const readline = require('readline');
6
5
 
7
- const CONFIG_FILE = 'mkctx.config.json';
6
+ // ============================================
7
+ // LAZY LOAD DEPENDENCIES (for faster startup)
8
+ // ============================================
9
+
10
+ let inquirer, chalk, ora;
8
11
 
9
- const defaultConfig = {
10
- "src": ".",
11
- "ignore": "mkctx.config.json, pnpm-lock.yaml, **/.titan/, mkctx/, node_modules/, .git/, dist/, build/, target/, .next/, out/, .cache, package-lock.json, README.md, *.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",
12
- "output": "./mkctx",
13
- "first_comment": "/* Project Context */",
14
- "last_comment": "/* End of Context */",
15
- "dynamic": false
12
+ function loadDependencies() {
13
+ if (!inquirer) {
14
+ inquirer = require('inquirer');
15
+ chalk = require('chalk');
16
+ ora = require('ora');
17
+ }
16
18
  }
17
19
 
18
- // Mapeo de extensiones a lenguajes para mejor resaltado de sintaxis
19
- const langMap = {
20
- js: 'javascript',
21
- ts: 'typescript',
22
- jsx: 'jsx',
23
- tsx: 'tsx',
24
- py: 'python',
25
- rb: 'ruby',
26
- go: 'go',
27
- rs: 'rust',
28
- java: 'java',
29
- kt: 'kotlin',
30
- cs: 'csharp',
31
- cpp: 'cpp',
32
- c: 'c',
33
- h: 'c',
34
- hpp: 'cpp',
35
- php: 'php',
36
- sh: 'bash',
37
- bash: 'bash',
38
- zsh: 'bash',
39
- ps1: 'powershell',
40
- sql: 'sql',
41
- html: 'html',
42
- css: 'css',
43
- scss: 'scss',
44
- sass: 'sass',
45
- less: 'less',
46
- json: 'json',
47
- xml: 'xml',
48
- yaml: 'yaml',
49
- yml: 'yaml',
50
- md: 'markdown',
51
- vue: 'vue',
52
- svelte: 'svelte',
53
- dockerfile: 'dockerfile',
54
- makefile: 'makefile',
55
- toml: 'toml',
56
- ini: 'ini',
57
- cfg: 'ini',
58
- env: 'bash'
20
+ // ============================================
21
+ // CONSTANTS
22
+ // ============================================
23
+
24
+ const CONFIG_FILE = 'mkctx.config.json';
25
+
26
+ const DEFAULT_PROJECT_CONFIG = {
27
+ src: ".",
28
+ 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",
29
+ output: "./mkctx",
30
+ first_comment: "/* Project Context */",
31
+ last_comment: "/* End of Context */"
59
32
  };
60
33
 
61
- async function main() {
62
- const args = process.argv.slice(2);
63
- const command = args[0];
64
-
65
- switch (command) {
66
- case 'config':
67
- createConfig();
68
- break;
69
- case 'help':
70
- case '--help':
71
- case '-h':
72
- showHelp();
73
- break;
74
- case 'version':
75
- case '--version':
76
- case '-v':
77
- showVersion();
78
- break;
79
- default:
80
- await generateContext();
81
- }
82
- }
34
+ // Language mapping for syntax highlighting
35
+ const LANG_MAP = {
36
+ js: 'javascript', ts: 'typescript', jsx: 'jsx', tsx: 'tsx',
37
+ py: 'python', rb: 'ruby', go: 'go', rs: 'rust',
38
+ java: 'java', kt: 'kotlin', cs: 'csharp', cpp: 'cpp',
39
+ c: 'c', h: 'c', hpp: 'cpp', php: 'php',
40
+ sh: 'bash', bash: 'bash', zsh: 'bash', ps1: 'powershell',
41
+ sql: 'sql', html: 'html', css: 'css', scss: 'scss',
42
+ sass: 'sass', less: 'less', json: 'json', xml: 'xml',
43
+ yaml: 'yaml', yml: 'yaml', md: 'markdown', vue: 'vue',
44
+ svelte: 'svelte', dockerfile: 'dockerfile', makefile: 'makefile',
45
+ toml: 'toml', ini: 'ini', cfg: 'ini', env: 'bash'
46
+ };
83
47
 
84
- function showHelp() {
85
- console.log(`
86
- šŸ“„ mkctx - Make Context
87
-
88
- Generate markdown context files from your project code for AI assistants.
89
-
90
- Usage:
91
- mkctx Generate context (interactive if dynamic mode enabled)
92
- mkctx config Create configuration file
93
- mkctx help Show this help message
94
- mkctx version Show version
95
-
96
- Configuration (mkctx.config.json):
97
- src Source directory to scan (default: "./src")
98
- ignore Comma-separated patterns to ignore
99
- output Output directory (default: "./mkctx")
100
- first_comment Comment at the beginning of context
101
- last_comment Comment at the end of context
102
- dynamic If true, prompts for path on each run
103
-
104
- Examples:
105
- mkctx # Generate context
106
- mkctx config # Create config file
107
-
108
- More info: https://github.com/yourusername/mkctx
109
- `);
48
+ // Text file extensions
49
+ const TEXT_EXTENSIONS = new Set([
50
+ '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
51
+ '.py', '.pyw', '.rb', '.rake', '.go', '.rs',
52
+ '.java', '.kt', '.kts', '.scala', '.cs', '.fs', '.vb',
53
+ '.cpp', '.c', '.h', '.hpp', '.cc', '.cxx',
54
+ '.php', '.phtml', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd',
55
+ '.sql', '.html', '.htm', '.xhtml',
56
+ '.css', '.scss', '.sass', '.less', '.styl',
57
+ '.json', '.json5', '.xml', '.xsl', '.xslt',
58
+ '.yaml', '.yml', '.md', '.markdown', '.mdx',
59
+ '.txt', '.text', '.vue', '.svelte',
60
+ '.dockerfile', '.makefile', '.toml', '.ini', '.cfg', '.conf',
61
+ '.env', '.env.example', '.gitignore', '.gitattributes', '.editorconfig',
62
+ '.eslintrc', '.prettierrc', '.babelrc',
63
+ '.graphql', '.gql', '.proto', '.tf', '.tfvars',
64
+ '.lua', '.r', '.R', '.swift', '.m', '.mm',
65
+ '.ex', '.exs', '.erl', '.hrl', '.clj', '.cljs', '.cljc',
66
+ '.hs', '.lhs', '.elm', '.pug', '.jade',
67
+ '.ejs', '.hbs', '.handlebars', '.twig', '.blade.php',
68
+ '.astro', '.prisma', '.sol'
69
+ ]);
70
+
71
+ const KNOWN_FILES = new Set([
72
+ 'dockerfile', 'makefile', 'gemfile', 'rakefile',
73
+ 'procfile', 'vagrantfile', 'jenkinsfile',
74
+ '.gitignore', '.gitattributes', '.editorconfig',
75
+ '.eslintrc', '.prettierrc', '.babelrc',
76
+ '.env', '.env.example', '.env.local',
77
+ 'readme.md', 'readme.txt', 'readme',
78
+ 'license', 'license.md', 'license.txt'
79
+ ]);
80
+
81
+ // ============================================
82
+ // CONFIGURATION MANAGEMENT
83
+ // ============================================
84
+
85
+ function loadProjectConfig() {
86
+ if (fs.existsSync(CONFIG_FILE)) {
87
+ try {
88
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
89
+ return { ...DEFAULT_PROJECT_CONFIG, ...config };
90
+ } catch (err) {
91
+ console.log(chalk.yellow('āš ļø Error parsing config file, using defaults'));
92
+ return { ...DEFAULT_PROJECT_CONFIG };
93
+ }
94
+ }
95
+ return null;
110
96
  }
111
97
 
112
- function showVersion() {
113
- const pkg = require('../package.json');
114
- console.log(`mkctx v${pkg.version}`);
98
+ function hasProjectConfig() {
99
+ return fs.existsSync(CONFIG_FILE);
115
100
  }
116
101
 
117
- function createConfig() {
118
- // Crear directorio mkctx
119
- if (!fs.existsSync('mkctx')) {
120
- fs.mkdirSync('mkctx', { recursive: true });
121
- }
102
+ function createProjectConfig() {
103
+ loadDependencies();
122
104
 
123
- // Escribir configuración
124
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2));
105
+ if (!fs.existsSync('mkctx')) {
106
+ fs.mkdirSync('mkctx', { recursive: true });
107
+ }
125
108
 
126
- // Actualizar .gitignore
127
- updateGitignore();
109
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_PROJECT_CONFIG, null, 2));
110
+ updateGitignore();
128
111
 
129
- console.log('āœ… Configuration created:');
130
- console.log(' - mkctx.config.json');
131
- console.log(' - mkctx/ folder');
132
- console.log(' - Entry in .gitignore');
112
+ console.log(chalk.green('\nāœ… Configuration created:'));
113
+ console.log(chalk.white(' - mkctx.config.json'));
114
+ console.log(chalk.white(' - mkctx/ folder'));
115
+ console.log(chalk.white(' - Entry in .gitignore\n'));
133
116
  }
134
117
 
135
118
  function updateGitignore() {
136
- const gitignorePath = '.gitignore';
137
- let content = '';
138
-
139
- if (fs.existsSync(gitignorePath)) {
140
- content = fs.readFileSync(gitignorePath, 'utf-8');
141
- }
142
-
143
- if (!content.includes('mkctx/')) {
144
- const entry = '\n# mkctx - generated context\nmkctx/\n';
145
- content += entry;
146
- fs.writeFileSync(gitignorePath, content);
147
- }
148
- }
119
+ const gitignorePath = '.gitignore';
120
+ let content = '';
149
121
 
150
- function loadConfig() {
151
- if (fs.existsSync(CONFIG_FILE)) {
152
- try {
153
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
154
- return { ...defaultConfig, ...config };
155
- } catch (err) {
156
- console.log('āš ļø Error parsing config file, using defaults');
157
- return { ...defaultConfig, dynamic: true };
122
+ if (fs.existsSync(gitignorePath)) {
123
+ content = fs.readFileSync(gitignorePath, 'utf-8');
124
+ }
125
+
126
+ if (!content.includes('mkctx/')) {
127
+ const entry = '\n# mkctx - generated context\nmkctx/\n';
128
+ content += entry;
129
+ fs.writeFileSync(gitignorePath, content);
158
130
  }
159
- }
160
- // Sin config, activar dynamic por defecto
161
- return { ...defaultConfig, dynamic: true };
162
131
  }
163
132
 
164
- async function askForPath(defaultPath) {
165
- const rl = readline.createInterface({
166
- input: process.stdin,
167
- output: process.stdout
168
- });
133
+ // ============================================
134
+ // UTILITY FUNCTIONS
135
+ // ============================================
136
+
137
+ function formatSize(bytes) {
138
+ const sizes = ['B', 'KB', 'MB', 'GB'];
139
+ if (bytes === 0) return '0 B';
140
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
141
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
142
+ }
143
+
144
+ function estimateTokens(text) {
145
+ return Math.ceil(text.length / 4);
146
+ }
169
147
 
170
- return new Promise((resolve) => {
171
- console.log('\nšŸ” Dynamic mode enabled');
172
- console.log(` Current directory: ${process.cwd()}`);
148
+ // Normalize path to always use forward slashes
149
+ function normalizePath(filePath) {
150
+ return filePath.replace(/\\/g, '/');
151
+ }
173
152
 
174
- rl.question(` Enter path (or press Enter for '${defaultPath}'): `, (answer) => {
175
- rl.close();
176
- const input = answer.trim();
153
+ // ============================================
154
+ // FILE OPERATIONS
155
+ // ============================================
177
156
 
178
- if (!input) {
179
- resolve(defaultPath);
180
- return;
181
- }
157
+ function isTextFile(filename) {
158
+ const ext = path.extname(filename).toLowerCase();
159
+ const basename = path.basename(filename).toLowerCase();
182
160
 
183
- if (!fs.existsSync(input)) {
184
- console.log(`āš ļø Path '${input}' does not exist. Using default: ${defaultPath}`);
185
- resolve(defaultPath);
186
- return;
187
- }
161
+ if (KNOWN_FILES.has(basename)) return true;
162
+ if (ext && TEXT_EXTENSIONS.has(ext)) return true;
188
163
 
189
- resolve(input);
190
- });
191
- });
164
+ return false;
192
165
  }
193
166
 
194
- async function generateContext() {
195
- const config = loadConfig();
167
+ function getLanguage(filename) {
168
+ const ext = path.extname(filename).slice(1).toLowerCase();
169
+ const basename = path.basename(filename).toLowerCase();
196
170
 
197
- let srcPath = config.src || '.';
171
+ if (basename === 'dockerfile') return 'dockerfile';
172
+ if (basename === 'makefile') return 'makefile';
173
+ if (basename.startsWith('.env')) return 'bash';
198
174
 
199
- // Determinar si usar modo dinƔmico
200
- if (config.dynamic) {
201
- srcPath = await askForPath(srcPath);
202
- } else if (config.src === '.' || config.src === '') {
203
- srcPath = await askForPath('.');
204
- }
175
+ return LANG_MAP[ext] || ext || 'text';
176
+ }
205
177
 
206
- // Verificar que la ruta existe
207
- if (!fs.existsSync(srcPath)) {
208
- console.log(`āŒ Source path does not exist: ${srcPath}`);
209
- process.exit(1);
210
- }
178
+ function parseIgnorePatterns(ignoreString) {
179
+ if (!ignoreString) return [];
180
+ return ignoreString
181
+ .split(',')
182
+ .map(p => p.trim())
183
+ .filter(Boolean);
184
+ }
211
185
 
212
- const files = getFiles(srcPath, config);
186
+ function matchWildcard(pattern, filename) {
187
+ const regexPattern = pattern
188
+ .replace(/\./g, '\\.')
189
+ .replace(/\*\*/g, '.*')
190
+ .replace(/\*/g, '[^/]*');
191
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
192
+ return regex.test(filename);
193
+ }
213
194
 
214
- if (files.length === 0) {
215
- console.log(`āš ļø No files found in: ${srcPath}`);
216
- return;
217
- }
195
+ function shouldIgnore(fullPath, name, relativePath, patterns) {
196
+ // Normalize paths for comparison
197
+ const normalizedFull = normalizePath(fullPath);
198
+ const normalizedRelative = normalizePath(relativePath);
199
+
200
+ const systemIgnores = [
201
+ '.git', '.DS_Store', 'Thumbs.db', 'node_modules',
202
+ '.svn', '.hg', '__pycache__', '.pytest_cache',
203
+ '.mypy_cache', '.vscode', '.idea'
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
+ }
218
214
 
219
- const content = buildContent(files, config);
215
+ for (const pattern of patterns) {
216
+ if (pattern.includes('*')) {
217
+ if (matchWildcard(pattern, name)) return true;
218
+ if (matchWildcard(pattern, normalizedRelative)) return true;
219
+ }
220
220
 
221
- const outputPath = config.output || '.';
222
- if (!fs.existsSync(outputPath)) {
223
- fs.mkdirSync(outputPath, { recursive: true });
224
- }
221
+ if (pattern.endsWith('/')) {
222
+ const dir = pattern.slice(0, -1);
223
+ if (normalizedFull.includes('/' + dir + '/') ||
224
+ normalizedFull.endsWith('/' + dir) ||
225
+ name === dir) {
226
+ return true;
227
+ }
228
+ }
225
229
 
226
- const outputFile = path.join(outputPath, 'context.md');
227
- fs.writeFileSync(outputFile, content);
230
+ if (normalizedRelative === pattern || name === pattern) {
231
+ return true;
232
+ }
233
+ }
228
234
 
229
- console.log(`āœ… Context generated at: ${outputFile}`);
230
- console.log(` šŸ“ Source: ${srcPath}`);
231
- console.log(` šŸ“„ Files included: ${files.length}`);
235
+ return false;
232
236
  }
233
237
 
234
- function getFiles(srcPath, config) {
235
- const files = [];
236
- const ignorePatterns = parseIgnorePatterns(config.ignore);
238
+ // ============================================
239
+ // SCAN AND BUILD JSON IN ONE PASS
240
+ // ============================================
241
+
242
+ function scanAndBuildJson(srcPath, config) {
243
+ const jsonArray = [];
244
+ const ignorePatterns = parseIgnorePatterns(config.ignore);
245
+ let totalSize = 0;
246
+ let totalLines = 0;
247
+ const filesByExt = {};
248
+
249
+ function walk(dir) {
250
+ if (!fs.existsSync(dir)) return;
251
+
252
+ let entries;
253
+ try {
254
+ entries = fs.readdirSync(dir, { withFileTypes: true });
255
+ } catch (err) {
256
+ return;
257
+ }
237
258
 
238
- function walk(dir) {
239
- if (!fs.existsSync(dir)) return;
259
+ for (const entry of entries) {
260
+ const fullPath = path.join(dir, entry.name);
261
+ const relativePath = path.relative(srcPath, fullPath);
262
+
263
+ if (shouldIgnore(fullPath, entry.name, relativePath, ignorePatterns)) {
264
+ continue;
265
+ }
266
+
267
+ if (entry.isDirectory()) {
268
+ walk(fullPath);
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
+ }
278
+
279
+ const ext = path.extname(entry.name).slice(1).toLowerCase() || null;
280
+ const lines = content.split('\n').length;
281
+ const size = Buffer.byteLength(content, 'utf-8');
282
+ const language = getLanguage(entry.name);
283
+
284
+ // Update stats
285
+ totalSize += size;
286
+ totalLines += lines;
287
+ filesByExt[ext || 'other'] = (filesByExt[ext || 'other'] || 0) + 1;
288
+
289
+ // Add to JSON array immediately
290
+ jsonArray.push({
291
+ path: normalizePath(relativePath),
292
+ name: entry.name,
293
+ extension: ext,
294
+ language: language,
295
+ lines: lines,
296
+ size: size,
297
+ content: content
298
+ });
299
+ }
300
+ }
301
+ }
240
302
 
241
- let entries;
242
- try {
243
- entries = fs.readdirSync(dir, { withFileTypes: true });
244
- } catch (err) {
245
- // Sin permisos para leer el directorio
246
- return;
303
+ walk(srcPath);
304
+
305
+ // Sort by path for consistency
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 };
316
+ }
317
+
318
+ // ============================================
319
+ // FORMAT CONVERTERS (from base JSON)
320
+ // ============================================
321
+
322
+ function toJson(baseJson) {
323
+ return JSON.stringify(baseJson, null, 2);
324
+ }
325
+
326
+ function toMarkdown(baseJson, config) {
327
+ let content = '';
328
+
329
+ if (config.first_comment) {
330
+ content += config.first_comment + '\n\n';
247
331
  }
248
332
 
249
- for (const entry of entries) {
250
- const fullPath = path.join(dir, entry.name);
251
- const relativePath = path.relative(srcPath, fullPath);
333
+ // Project structure
334
+ content += '## Project Structure\n\n```\n';
335
+ const dirs = new Set();
336
+ baseJson.forEach(f => {
337
+ const dir = path.dirname(f.path);
338
+ if (dir !== '.') dirs.add(dir);
339
+ });
340
+ Array.from(dirs).sort().forEach(d => content += `šŸ“ ${d}/\n`);
341
+ content += `\n${baseJson.length} files total\n\`\`\`\n\n`;
342
+
343
+ // Source files
344
+ content += '## Source Files\n\n';
345
+
346
+ for (const file of baseJson) {
347
+ content += `### ${file.path}\n\n`;
348
+ content += '```' + file.language + '\n';
349
+ content += file.content;
350
+ if (!file.content.endsWith('\n')) {
351
+ content += '\n';
352
+ }
353
+ content += '```\n\n';
354
+ }
355
+
356
+ if (config.last_comment) {
357
+ content += config.last_comment;
358
+ }
252
359
 
253
- if (shouldIgnore(fullPath, entry.name, relativePath, ignorePatterns)) {
254
- continue;
255
- }
360
+ return content;
361
+ }
362
+
363
+ function toToon(baseJson, stats) {
364
+ let content = '';
365
+
366
+ // Meta header
367
+ content += `# Project Context\n`;
368
+ content += `# Generated: ${new Date().toISOString()}\n`;
369
+ content += `# Files: ${baseJson.length}\n`;
370
+ content += `# Lines: ${stats.totalLines}\n`;
371
+ content += `# Size: ${stats.totalSize} bytes\n\n`;
372
+
373
+ // Files table (compact tabular format - TOON's strength)
374
+ content += `files[${baseJson.length}]{path,name,extension,language,lines,size}:\n`;
375
+ for (const file of baseJson) {
376
+ const ext = file.extension || '';
377
+ content += ` ${escapeToonValue(file.path)},${escapeToonValue(file.name)},${ext},${file.language},${file.lines},${file.size}\n`;
378
+ }
256
379
 
257
- if (entry.isDirectory()) {
258
- walk(fullPath);
259
- } else if (entry.isFile()) {
260
- // Verificar que es un archivo de texto (no binario)
261
- if (isTextFile(entry.name)) {
262
- files.push(fullPath);
380
+ content += '\n';
381
+
382
+ // File contents
383
+ for (let i = 0; i < baseJson.length; i++) {
384
+ const file = baseJson[i];
385
+ content += `---\n`;
386
+ content += `[${i}] ${file.path}\n`;
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`;
263
393
  }
264
- }
265
394
  }
266
- }
267
395
 
268
- walk(srcPath);
269
- return files.sort(); // Ordenar alfabƩticamente
396
+ return content;
270
397
  }
271
398
 
272
- function parseIgnorePatterns(ignoreString) {
273
- if (!ignoreString) return [];
274
- return ignoreString
275
- .split(',')
276
- .map(p => p.trim())
277
- .filter(Boolean);
399
+ function escapeToonValue(value) {
400
+ if (value === null || value === undefined) return '';
401
+ const str = String(value);
402
+ if (str.includes(',') || str.includes('\n') || str.includes('"') ||
403
+ str.startsWith(' ') || str.endsWith(' ')) {
404
+ return '"' + str.replace(/"/g, '""').replace(/\n/g, '\\n') + '"';
405
+ }
406
+ return str;
278
407
  }
279
408
 
280
- function shouldIgnore(fullPath, name, relativePath, patterns) {
281
- // Ignorar archivos y carpetas del sistema
282
- const systemIgnores = [
283
- '.git',
284
- '.DS_Store',
285
- 'Thumbs.db',
286
- 'node_modules',
287
- '.svn',
288
- '.hg',
289
- '__pycache__',
290
- '.pytest_cache',
291
- '.mypy_cache',
292
- '.vscode',
293
- '.idea',
294
- '*.pyc',
295
- '*.pyo',
296
- '.env.local',
297
- '.env.*.local'
298
- ];
299
-
300
- for (const ignore of systemIgnores) {
301
- if (ignore.includes('*')) {
302
- if (matchWildcard(ignore, name)) return true;
303
- } else {
304
- if (fullPath.includes(ignore) || name === ignore) return true;
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`;
305
423
  }
306
- }
307
424
 
308
- // Aplicar patrones de configuración
309
- for (const pattern of patterns) {
310
- // Wildcard (*.log, *.test.js)
311
- if (pattern.includes('*')) {
312
- if (matchWildcard(pattern, name)) return true;
425
+ content += '</context>\n';
426
+ return content;
427
+ }
428
+
429
+ function escapeXml(str) {
430
+ if (str === null || str === undefined) return '';
431
+ return String(str)
432
+ .replace(/&/g, '&amp;')
433
+ .replace(/</g, '&lt;')
434
+ .replace(/>/g, '&gt;')
435
+ .replace(/"/g, '&quot;')
436
+ .replace(/'/g, '&apos;');
437
+ }
438
+
439
+ // ============================================
440
+ // CONTEXT GENERATION
441
+ // ============================================
442
+
443
+ async function generateContextDynamic() {
444
+ loadDependencies();
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
+ }
464
+
465
+ async function generateContextFromConfigFile() {
466
+ loadDependencies();
467
+
468
+ const config = loadProjectConfig();
469
+ if (!config) {
470
+ console.log(chalk.yellow('\nāš ļø No config file found.'));
471
+ return null;
313
472
  }
314
473
 
315
- // Directorio (temp/, dist/)
316
- if (pattern.endsWith('/')) {
317
- const dir = pattern.slice(0, -1);
318
- if (fullPath.includes(path.sep + dir + path.sep) ||
319
- fullPath.includes(dir + path.sep) ||
320
- name === dir) {
321
- return true;
322
- }
474
+ return generateContext(config, config.src);
475
+ }
476
+
477
+ function generateContext(config, srcPath) {
478
+ const spinner = ora(`Scanning and reading files from ${srcPath}...`).start();
479
+
480
+ if (!fs.existsSync(srcPath)) {
481
+ spinner.fail(`Source path does not exist: ${srcPath}`);
482
+ return null;
323
483
  }
324
484
 
325
- // Coincidencia exacta o parcial
326
- if (relativePath.includes(pattern) || name === pattern) {
327
- return true;
485
+ // Single pass: scan AND build JSON at the same time
486
+ const { jsonArray, stats } = scanAndBuildJson(srcPath, config);
487
+
488
+ if (jsonArray.length === 0) {
489
+ spinner.fail(`No files found in: ${srcPath}`);
490
+ return null;
328
491
  }
329
- }
330
492
 
331
- return false;
332
- }
493
+ spinner.succeed(`Context built: ${chalk.yellow(jsonArray.length)} files, ${chalk.yellow(formatSize(stats.totalSize))}`);
333
494
 
334
- function matchWildcard(pattern, filename) {
335
- // Convertir patrón glob simple a regex
336
- const regexPattern = pattern
337
- .replace(/\./g, '\\.')
338
- .replace(/\*/g, '.*');
339
- const regex = new RegExp(`^${regexPattern}$`, 'i');
340
- return regex.test(filename);
495
+ return {
496
+ baseJson: jsonArray,
497
+ stats: stats,
498
+ config
499
+ };
341
500
  }
342
501
 
343
- function isTextFile(filename) {
344
- // Extensiones de archivos de texto conocidos
345
- const textExtensions = [
346
- '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
347
- '.py', '.pyw',
348
- '.rb', '.rake',
349
- '.go',
350
- '.rs',
351
- '.java', '.kt', '.kts', '.scala',
352
- '.cs', '.fs', '.vb',
353
- '.cpp', '.c', '.h', '.hpp', '.cc', '.cxx',
354
- '.php', '.phtml',
355
- '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd',
356
- '.sql',
357
- '.html', '.htm', '.xhtml',
358
- '.css', '.scss', '.sass', '.less', '.styl',
359
- '.json', '.json5',
360
- '.xml', '.xsl', '.xslt',
361
- '.yaml', '.yml',
362
- '.md', '.markdown', '.mdx',
363
- '.txt', '.text',
364
- '.vue', '.svelte',
365
- '.dockerfile', '.makefile',
366
- '.toml', '.ini', '.cfg', '.conf',
367
- '.env', '.env.example',
368
- '.gitignore', '.gitattributes', '.editorconfig',
369
- '.eslintrc', '.prettierrc', '.babelrc',
370
- '.graphql', '.gql',
371
- '.proto',
372
- '.tf', '.tfvars',
373
- '.lua',
374
- '.r', '.R',
375
- '.swift',
376
- '.m', '.mm',
377
- '.ex', '.exs',
378
- '.erl', '.hrl',
379
- '.clj', '.cljs', '.cljc',
380
- '.hs', '.lhs',
381
- '.elm',
382
- '.pug', '.jade',
383
- '.ejs', '.hbs', '.handlebars',
384
- '.twig', '.blade.php',
385
- '.astro',
386
- '.prisma',
387
- '.sol'
388
- ];
389
-
390
- const ext = path.extname(filename).toLowerCase();
391
- const basename = path.basename(filename).toLowerCase();
392
-
393
- // Archivos sin extensión pero conocidos
394
- const knownFiles = [
395
- 'dockerfile', 'makefile', 'gemfile', 'rakefile',
396
- 'procfile', 'vagrantfile', 'jenkinsfile',
397
- '.gitignore', '.gitattributes', '.editorconfig',
398
- '.eslintrc', '.prettierrc', '.babelrc',
399
- '.env', '.env.example', '.env.local'
400
- ];
502
+ // ============================================
503
+ // SAVE CONTEXT
504
+ // ============================================
505
+
506
+ // ============================================
507
+ // SAVE CONTEXT
508
+ // ============================================
509
+
510
+ async function saveContext(result, formats) {
511
+ loadDependencies();
512
+
513
+ const { fileName } = await inquirer.prompt([
514
+ {
515
+ type: 'input',
516
+ name: 'fileName',
517
+ 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 });
526
+ }
527
+
528
+ const savedFiles = [];
529
+
530
+ for (const format of formats) {
531
+ let content;
532
+ let filename;
533
+
534
+ switch (format) {
535
+ case 'json':
536
+ content = toJson(result.baseJson);
537
+ filename = `${fileName}.json`;
538
+ break;
539
+ case 'md':
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
+ }
401
552
 
402
- if (knownFiles.includes(basename)) return true;
403
- if (ext && textExtensions.includes(ext)) return true;
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 });
558
+ }
404
559
 
405
- return false;
560
+ console.log(chalk.green('\nāœ… Context saved:\n'));
561
+ for (const { format, file, size, tokens } of savedFiles) {
562
+ console.log(chalk.white(` ${chalk.cyan(format.toUpperCase().padEnd(4))} → ${chalk.yellow(file)}`));
563
+ console.log(chalk.gray(` ${formatSize(size)} | ~${tokens.toLocaleString()} tokens\n`));
564
+ }
565
+
566
+ return savedFiles;
406
567
  }
407
568
 
408
- function getLanguage(filename) {
409
- const ext = path.extname(filename).slice(1).toLowerCase();
410
- const basename = path.basename(filename).toLowerCase();
569
+ // ============================================
570
+ // FORMAT SELECTION
571
+ // ============================================
572
+
573
+ async function selectFormat() {
574
+ loadDependencies();
575
+
576
+ const { format } = await inquirer.prompt([
577
+ {
578
+ type: 'list',
579
+ name: 'format',
580
+ message: 'Select output format:',
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
+ ]);
411
607
 
412
- // Archivos especiales
413
- if (basename === 'dockerfile') return 'dockerfile';
414
- if (basename === 'makefile') return 'makefile';
415
- if (basename.startsWith('.env')) return 'bash';
608
+ if (format === 'all') {
609
+ return ['json', 'md', 'toon', 'xml'];
610
+ }
416
611
 
417
- return langMap[ext] || ext || 'text';
612
+ return [format];
418
613
  }
419
614
 
420
- function buildContent(files, config) {
421
- let content = '';
615
+ // ============================================
616
+ // MAIN MENU
617
+ // ============================================
422
618
 
423
- if (config.first_comment) {
424
- content += config.first_comment + '\n\n';
425
- }
619
+ async function showMainMenu() {
620
+ loadDependencies();
426
621
 
427
- for (const file of files) {
428
- let fileContent;
429
- try {
430
- fileContent = fs.readFileSync(file, 'utf-8');
431
- } catch (err) {
432
- console.log(`āš ļø Could not read: ${file}`);
433
- continue;
622
+ const hasConfig = hasProjectConfig();
623
+
624
+ console.log(chalk.cyan('\n╔════════════════════════════════════════╗'));
625
+ console.log(chalk.cyan('ā•‘') + chalk.cyan.bold(' šŸ“„ mkctx - Make Context ') + chalk.cyan('ā•‘'));
626
+ console.log(chalk.cyan('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n'));
627
+
628
+ const choices = [];
629
+
630
+ if (hasConfig) {
631
+ choices.push({
632
+ name: chalk.green('šŸ“ Generate from config file'),
633
+ value: 'from-config'
634
+ });
434
635
  }
435
636
 
436
- const lang = getLanguage(file);
437
- const relativePath = file; // Mantener ruta relativa
637
+ choices.push(
638
+ {
639
+ name: chalk.blue('šŸ” Generate dynamically (choose path)'),
640
+ value: 'dynamic'
641
+ },
642
+ new inquirer.Separator(),
643
+ {
644
+ name: hasConfig
645
+ ? chalk.gray('āš™ļø View configuration')
646
+ : chalk.yellow('āš™ļø Create configuration file'),
647
+ value: 'config'
648
+ },
649
+ new inquirer.Separator(),
650
+ {
651
+ name: chalk.red('āŒ Exit'),
652
+ value: 'exit'
653
+ }
654
+ );
655
+
656
+ const { action } = await inquirer.prompt([
657
+ {
658
+ type: 'list',
659
+ name: 'action',
660
+ message: 'What would you like to do?',
661
+ choices
662
+ }
663
+ ]);
438
664
 
439
- content += '```' + lang + '\n';
440
- content += '// ' + relativePath + '\n';
441
- content += fileContent;
665
+ return action;
666
+ }
442
667
 
443
- // Asegurar que termina con newline
444
- if (!fileContent.endsWith('\n')) {
445
- content += '\n';
668
+ // ============================================
669
+ // MAIN APPLICATION
670
+ // ============================================
671
+
672
+ async function main() {
673
+ const args = process.argv.slice(2);
674
+
675
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
676
+ showHelp();
677
+ return;
446
678
  }
447
679
 
448
- content += '```\n\n';
449
- }
680
+ if (args.includes('--version') || args.includes('-v') || args[0] === 'version') {
681
+ showVersion();
682
+ return;
683
+ }
450
684
 
451
- if (config.last_comment) {
452
- content += config.last_comment;
453
- }
685
+ if (args[0] === 'config') {
686
+ loadDependencies();
687
+ createProjectConfig();
688
+ return;
689
+ }
454
690
 
455
- return content;
691
+ loadDependencies();
692
+
693
+ let running = true;
694
+
695
+ while (running) {
696
+ const action = await showMainMenu();
697
+
698
+ switch (action) {
699
+ case 'from-config': {
700
+ const result = await generateContextFromConfigFile();
701
+ if (result) {
702
+ console.log(chalk.cyan('\nšŸ“Š Context Summary:'));
703
+ console.log(chalk.white(` Files: ${result.stats.files}`));
704
+ console.log(chalk.white(` Lines: ${result.stats.totalLines.toLocaleString()}`));
705
+ console.log(chalk.white(` Size: ${formatSize(result.stats.totalSize)}`));
706
+
707
+ const formats = await selectFormat();
708
+ await saveContext(result, formats);
709
+
710
+ console.log(chalk.yellow('šŸ‘‹ Done!\n'));
711
+ running = false;
712
+ }
713
+ break;
714
+ }
715
+
716
+ case 'dynamic': {
717
+ const result = await generateContextDynamic();
718
+ if (result) {
719
+ console.log(chalk.cyan('\nšŸ“Š Context Summary:'));
720
+ console.log(chalk.white(` Files: ${result.stats.files}`));
721
+ console.log(chalk.white(` Lines: ${result.stats.totalLines.toLocaleString()}`));
722
+ console.log(chalk.white(` Size: ${formatSize(result.stats.totalSize)}`));
723
+
724
+ const formats = await selectFormat();
725
+ await saveContext(result, formats);
726
+
727
+ console.log(chalk.yellow('šŸ‘‹ Done!\n'));
728
+ running = false;
729
+ }
730
+ break;
731
+ }
732
+
733
+ case 'config':
734
+ if (hasProjectConfig()) {
735
+ console.log(chalk.cyan('\nšŸ“„ Current configuration:\n'));
736
+ const config = loadProjectConfig();
737
+ console.log(JSON.stringify(config, null, 2));
738
+ console.log(chalk.gray(`\n Edit ${CONFIG_FILE} to modify settings.\n`));
739
+ } else {
740
+ createProjectConfig();
741
+ }
742
+ break;
743
+
744
+ case 'exit':
745
+ running = false;
746
+ console.log(chalk.yellow('\nšŸ‘‹ Goodbye!\n'));
747
+ break;
748
+ }
749
+ }
750
+ }
751
+
752
+ function showHelp() {
753
+ loadDependencies();
754
+
755
+ console.log(chalk.cyan(`
756
+ ╔════════════════════════════════════════════════════════════╗
757
+ ā•‘ šŸ“„ mkctx - Make Context for AI Code Analysis ā•‘
758
+ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
759
+
760
+ ${chalk.white('Generate context files from your codebase for AI analysis.')}
761
+
762
+ ${chalk.yellow('Usage:')}
763
+ mkctx Interactive mode (recommended)
764
+ mkctx config Create configuration file
765
+ mkctx help Show this help message
766
+ mkctx version Show version
767
+
768
+ ${chalk.yellow('Output Formats:')}
769
+ ${chalk.green('JSON')} Simple array of file objects (base format)
770
+ ${chalk.blue('MD')} Markdown with code blocks
771
+ ${chalk.yellow('TOON')} Token-Oriented Object Notation (LLM optimized)
772
+ ${chalk.red('XML')} XML with CDATA sections
773
+
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
+ ${chalk.gray('More info: https://github.com/pnkkzero/mkctx')}
786
+ `));
456
787
  }
457
788
 
458
- // Ejecutar
789
+ function showVersion() {
790
+ try {
791
+ const pkg = require('./package.json');
792
+ console.log(`mkctx v${pkg.version}`);
793
+ } catch {
794
+ console.log('mkctx v4.0.0');
795
+ }
796
+ }
797
+
798
+ // ============================================
799
+ // RUN
800
+ // ============================================
801
+
459
802
  main().catch((err) => {
460
- console.error('āŒ Error:', err.message);
461
- process.exit(1);
462
- });
803
+ console.error(`\nāŒ Error: ${err.message}`);
804
+ if (process.env.DEBUG) {
805
+ console.error(err.stack);
806
+ }
807
+ process.exit(1);
808
+ });