mkctx 4.0.0 → 4.0.1

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 +161 -140
  2. package/bin/mkctx.js +473 -506
  3. package/package.json +1 -1
package/bin/mkctx.js CHANGED
@@ -1,20 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
4
6
  const path = require('path');
5
7
 
6
8
  // ============================================
7
- // LAZY LOAD DEPENDENCIES (for faster startup)
9
+ // LAZY-LOADED DEPENDENCIES
8
10
  // ============================================
9
11
 
10
12
  let inquirer, chalk, ora;
11
13
 
12
14
  function loadDependencies() {
13
- if (!inquirer) {
14
- inquirer = require('inquirer');
15
- chalk = require('chalk');
16
- ora = require('ora');
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 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 */"
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
- // Language mapping for syntax highlighting
35
+ const VALID_FORMATS = ['json', 'md', 'toon', 'xml'];
36
+
35
37
  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',
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', ini: 'ini', cfg: 'ini', env: 'bash'
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
- // CONFIGURATION MANAGEMENT
101
+ // CLI ARGUMENT PARSING
83
102
  // ============================================
84
103
 
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 };
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
- return null;
151
+
152
+ return { command, flags };
96
153
  }
97
154
 
98
- function hasProjectConfig() {
99
- return fs.existsSync(CONFIG_FILE);
155
+ function isFlagToken(str) {
156
+ return /^--[a-zA-Z]/.test(str) || /^-[a-zA-Z]/.test(str);
100
157
  }
101
158
 
102
- function createProjectConfig() {
103
- loadDependencies();
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
- if (!fs.existsSync('mkctx')) {
106
- fs.mkdirSync('mkctx', { recursive: true });
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
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_PROJECT_CONFIG, null, 2));
110
- updateGitignore();
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 updateGitignore() {
216
+ function appendToGitignore() {
119
217
  const gitignorePath = '.gitignore';
120
- let content = '';
121
-
122
- if (fs.existsSync(gitignorePath)) {
123
- content = fs.readFileSync(gitignorePath, 'utf-8');
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 (!content.includes('mkctx/')) {
127
- const entry = '\n# mkctx - generated context\nmkctx/\n';
128
- content += entry;
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 FUNCTIONS
246
+ // UTILITY HELPERS
135
247
  // ============================================
136
248
 
137
249
  function formatSize(bytes) {
138
- const sizes = ['B', 'KB', 'MB', 'GB'];
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)} ${sizes[i]}`;
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
- // Normalize path to always use forward slashes
149
- function normalizePath(filePath) {
260
+ function toUnixPath(filePath) {
150
261
  return filePath.replace(/\\/g, '/');
151
262
  }
152
263
 
153
264
  // ============================================
154
- // FILE OPERATIONS
265
+ // FILE FILTERING
155
266
  // ============================================
156
267
 
157
268
  function isTextFile(filename) {
158
- const ext = path.extname(filename).toLowerCase();
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 = path.extname(filename).slice(1).toLowerCase();
275
+ const ext = path.extname(filename).slice(1).toLowerCase();
169
276
  const basename = path.basename(filename).toLowerCase();
170
-
171
- if (basename === 'dockerfile') return 'dockerfile';
172
- if (basename === 'makefile') return 'makefile';
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, 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);
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
- // 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
- }
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
- for (const pattern of patterns) {
311
+ return patterns.some(pattern => {
216
312
  if (pattern.includes('*')) {
217
- if (matchWildcard(pattern, name)) return true;
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
- if (normalizedFull.includes('/' + dir + '/') ||
224
- normalizedFull.endsWith('/' + dir) ||
225
- name === dir) {
226
- return true;
227
- }
317
+ return normFull.includes(`/${dir}/`) ||
318
+ normFull.endsWith(`/${dir}`) ||
319
+ name === dir;
228
320
  }
229
-
230
- if (normalizedRelative === pattern || name === pattern) {
231
- return true;
232
- }
233
- }
234
-
235
- return false;
321
+ return normRelative === pattern || name === pattern;
322
+ });
236
323
  }
237
324
 
238
325
  // ============================================
239
- // SCAN AND BUILD JSON IN ONE PASS
326
+ // FILE SCANNING
240
327
  // ============================================
241
328
 
242
- function scanAndBuildJson(srcPath, config) {
243
- const jsonArray = [];
329
+ function scanFiles(srcPath, config) {
244
330
  const ignorePatterns = parseIgnorePatterns(config.ignore);
245
- let totalSize = 0;
246
- let totalLines = 0;
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
- entries = fs.readdirSync(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
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
- 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
- }
347
+ if (entry.isDirectory()) { walk(fullPath); continue; }
348
+ if (!entry.isFile() || !isTextFile(entry.name)) continue;
278
349
 
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
- }
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
- // 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 };
371
+ return { files, stats };
316
372
  }
317
373
 
318
374
  // ============================================
319
- // FORMAT CONVERTERS (from base JSON)
375
+ // OUTPUT FORMATTERS
320
376
  // ============================================
321
377
 
322
- function toJson(baseJson) {
323
- return JSON.stringify(baseJson, null, 2);
378
+ function toJson(files) {
379
+ return JSON.stringify(files, null, 2);
324
380
  }
325
381
 
326
- function toMarkdown(baseJson, config) {
327
- let content = '';
328
-
329
- if (config.first_comment) {
330
- content += config.first_comment + '\n\n';
331
- }
332
-
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
- }
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(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
- }
379
-
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`;
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 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;
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
- content += '</context>\n';
426
- return content;
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 === null || str === undefined) return '';
465
+ if (str == null) return '';
431
466
  return String(str)
432
467
  .replace(/&/g, '&amp;')
433
468
  .replace(/</g, '&lt;')
@@ -436,125 +471,76 @@ function escapeXml(str) {
436
471
  .replace(/'/g, '&apos;');
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
- 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
- }
487
+ function generateContext(config) {
488
+ const spinner = ora(`Scanning ${config.src}...`).start();
464
489
 
465
- async function generateContextFromConfigFile() {
466
- loadDependencies();
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
- return generateContext(config, config.src);
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 (!fs.existsSync(srcPath)) {
481
- spinner.fail(`Source path does not exist: ${srcPath}`);
497
+ if (files.length === 0) {
498
+ spinner.fail(`No files found in: ${config.src}`);
482
499
  return null;
483
500
  }
484
501
 
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;
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
- // SAVE CONTEXT
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
- const { fileName } = await inquirer.prompt([
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
- 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
- }
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
- // FORMAT SELECTION
556
+ // INTERACTIVE PROMPTS
571
557
  // ============================================
572
558
 
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
- ]);
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
- // MAIN MENU
617
- // ============================================
618
-
619
- async function showMainMenu() {
620
- loadDependencies();
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
- const hasConfig = hasProjectConfig();
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: hasConfig
645
- ? chalk.gray('⚙️ View configuration')
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
- type: 'list',
659
- name: 'action',
660
- message: 'What would you like to do?',
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
- // MAIN APPLICATION
620
+ // EXECUTION FLOWS
670
621
  // ============================================
671
622
 
672
- async function main() {
673
- const args = process.argv.slice(2);
623
+ async function runNonInteractive(rawFlags) {
624
+ loadDependencies();
674
625
 
675
- if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
676
- showHelp();
677
- return;
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 (args.includes('--version') || args.includes('-v') || args[0] === 'version') {
681
- showVersion();
682
- return;
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
- if (args[0] === 'config') {
686
- loadDependencies();
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 showMainMenu();
650
+ const action = await promptMainMenu();
697
651
 
698
652
  switch (action) {
699
653
  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
- }
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 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
- }
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 (hasProjectConfig()) {
680
+ if (configFileExists()) {
735
681
  console.log(chalk.cyan('\n📄 Current configuration:\n'));
736
- const config = loadProjectConfig();
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
- createProjectConfig();
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 Interactive mode (recommended)
764
- mkctx config Create configuration file
765
- mkctx help Show this help message
766
- mkctx version Show 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 (base format)
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
- // RUN
754
+ // ENTRY POINT
800
755
  // ============================================
801
756
 
802
- main().catch((err) => {
803
- console.error(`\n❌ Error: ${err.message}`);
804
- if (process.env.DEBUG) {
805
- console.error(err.stack);
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
  });