kimchilang 1.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 (90) hide show
  1. package/.github/workflows/ci.yml +66 -0
  2. package/README.md +1547 -0
  3. package/create-kimchi-app/README.md +44 -0
  4. package/create-kimchi-app/index.js +214 -0
  5. package/create-kimchi-app/package.json +22 -0
  6. package/editors/README.md +121 -0
  7. package/editors/sublime/KimchiLang.sublime-syntax +138 -0
  8. package/editors/vscode/README.md +90 -0
  9. package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
  10. package/editors/vscode/language-configuration.json +37 -0
  11. package/editors/vscode/package.json +55 -0
  12. package/editors/vscode/src/extension.js +354 -0
  13. package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
  14. package/examples/api/client.km +36 -0
  15. package/examples/async_pipe.km +58 -0
  16. package/examples/basic.kimchi +109 -0
  17. package/examples/cli_framework/README.md +92 -0
  18. package/examples/cli_framework/calculator.km +61 -0
  19. package/examples/cli_framework/deploy.km +126 -0
  20. package/examples/cli_framework/greeter.km +26 -0
  21. package/examples/config.static +27 -0
  22. package/examples/config.static.js +10 -0
  23. package/examples/env_test.km +37 -0
  24. package/examples/fibonacci.kimchi +17 -0
  25. package/examples/greeter.km +15 -0
  26. package/examples/hello.js +1 -0
  27. package/examples/hello.kimchi +3 -0
  28. package/examples/js_interop.km +42 -0
  29. package/examples/logger_example.km +34 -0
  30. package/examples/memo_fibonacci.km +17 -0
  31. package/examples/myapp/lib/http.js +14 -0
  32. package/examples/myapp/lib/http.km +16 -0
  33. package/examples/myapp/main.km +16 -0
  34. package/examples/myapp/main_with_mock.km +42 -0
  35. package/examples/myapp/services/api.js +18 -0
  36. package/examples/myapp/services/api.km +18 -0
  37. package/examples/new_features.kimchi +52 -0
  38. package/examples/project_example.static +20 -0
  39. package/examples/readme_examples.km +240 -0
  40. package/examples/reduce_pattern_match.km +85 -0
  41. package/examples/regex_match.km +46 -0
  42. package/examples/sample.js +45 -0
  43. package/examples/sample.km +39 -0
  44. package/examples/secrets.static +35 -0
  45. package/examples/secrets.static.js +30 -0
  46. package/examples/shell-example.mjs +144 -0
  47. package/examples/shell_example.km +19 -0
  48. package/examples/stdlib_test.km +22 -0
  49. package/examples/test_example.km +69 -0
  50. package/examples/testing/README.md +88 -0
  51. package/examples/testing/http_client.km +18 -0
  52. package/examples/testing/math.km +48 -0
  53. package/examples/testing/math.test.km +93 -0
  54. package/examples/testing/user_service.km +29 -0
  55. package/examples/testing/user_service.test.km +72 -0
  56. package/examples/use-config.mjs +141 -0
  57. package/examples/use_config.km +13 -0
  58. package/install.sh +59 -0
  59. package/package.json +29 -0
  60. package/pantry/acorn/index.km +1 -0
  61. package/pantry/is_number/index.km +1 -0
  62. package/pantry/is_odd/index.km +2 -0
  63. package/project.static +6 -0
  64. package/src/cli.js +1245 -0
  65. package/src/generator.js +1241 -0
  66. package/src/index.js +141 -0
  67. package/src/js2km.js +568 -0
  68. package/src/lexer.js +822 -0
  69. package/src/linter.js +810 -0
  70. package/src/package-manager.js +307 -0
  71. package/src/parser.js +1876 -0
  72. package/src/static-parser.js +500 -0
  73. package/src/typechecker.js +950 -0
  74. package/stdlib/array.km +0 -0
  75. package/stdlib/bitwise.km +38 -0
  76. package/stdlib/console.km +49 -0
  77. package/stdlib/date.km +97 -0
  78. package/stdlib/function.km +44 -0
  79. package/stdlib/http.km +197 -0
  80. package/stdlib/http.md +333 -0
  81. package/stdlib/index.km +26 -0
  82. package/stdlib/json.km +17 -0
  83. package/stdlib/logger.js +114 -0
  84. package/stdlib/logger.km +104 -0
  85. package/stdlib/math.km +120 -0
  86. package/stdlib/object.km +41 -0
  87. package/stdlib/promise.km +33 -0
  88. package/stdlib/string.km +93 -0
  89. package/stdlib/testing.md +265 -0
  90. package/test/test.js +599 -0
package/src/cli.js ADDED
@@ -0,0 +1,1245 @@
1
+ #!/usr/bin/env node
2
+
3
+ // KimchiLang CLI - Command line interface for the KimchiLang compiler
4
+
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
6
+ import { resolve, dirname, basename, extname, join, relative } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { execSync, spawn } from 'child_process';
9
+ import { compile } from './index.js';
10
+ import { convertJS } from './js2km.js';
11
+ import { Linter, Severity } from './linter.js';
12
+ import { tokenize, parse } from './index.js';
13
+ import { parseStaticFile, generateStaticCode } from './static-parser.js';
14
+ import { installDependencies, cleanDependencies, parseProjectFile } from './package-manager.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ const VERSION = '1.0.1';
20
+
21
+ const HELP = `
22
+ KimchiLang v${VERSION}
23
+
24
+ Usage:
25
+ kimchi <module.path> [--arg value] [--dep module=path]
26
+ kimchi <command> [options] <file>
27
+
28
+ Module Execution:
29
+ kimchi <module.path> Run a module (e.g., salesforce.client)
30
+ kimchi <module> --arg value Pass args to module (converts to camelCase)
31
+ kimchi <module> --dep name=path Inject dependency
32
+ kimchi help <module.path> Show module help (_describe, args, deps, _help)
33
+
34
+ Commands:
35
+ ls [path] List modules (--verbose for descriptions, --recursive for tree)
36
+ compile <file> Compile a .kimchi file to JavaScript
37
+ run <file> Compile and run a .kimchi file
38
+ test <file> Run tests in a .kimchi file
39
+ lint <file> Run linter on a .kimchi file
40
+ check <file> Check a file for errors (for editor integration)
41
+ convert <file> Convert a JavaScript file to KimchiLang
42
+ npm <args> Run npm and convert installed packages to pantry/
43
+ build <dir> Compile all .kimchi files in a directory
44
+ install Install dependencies from project.static
45
+ clean Remove installed dependencies
46
+ repl Start an interactive REPL session
47
+ help Show this help message
48
+ version Show version information
49
+
50
+ Options:
51
+ -o, --output <file> Output file path (for compile command)
52
+ -d, --debug Enable debug output
53
+ --no-lint Skip linting during compilation
54
+ --verbose, -v Show module descriptions (for ls command)
55
+ --recursive, -r Recurse into subdirectories (for ls command)
56
+ --dep <name=path> Inject dependency (can be used multiple times)
57
+
58
+ Examples:
59
+ kimchi ls
60
+ kimchi ls ./lib --verbose --recursive
61
+ kimchi salesforce.client --client-id ABC123
62
+ kimchi help salesforce.client
63
+ kimchi compile app.kimchi -o dist/app.js
64
+
65
+ File Extension: .kimchi, .km, or .kc
66
+ `;
67
+
68
+ function parseArgs(args) {
69
+ const result = {
70
+ command: null,
71
+ file: null,
72
+ output: null,
73
+ debug: false,
74
+ watch: false,
75
+ help: false,
76
+ verbose: false,
77
+ recursive: false,
78
+ skipLint: false,
79
+ moduleArgs: {}, // Args to pass to module (--arg-name value)
80
+ deps: {}, // Dependency injections (--dep name=path)
81
+ };
82
+
83
+ let i = 0;
84
+ while (i < args.length) {
85
+ const arg = args[i];
86
+
87
+ if (arg === '-h' || arg === '--help') {
88
+ result.help = true;
89
+ } else if (arg === '-d' || arg === '--debug') {
90
+ result.debug = true;
91
+ } else if (arg === '--no-lint') {
92
+ result.skipLint = true;
93
+ } else if (arg === '-v' || arg === '--verbose') {
94
+ result.verbose = true;
95
+ } else if (arg === '-r' || arg === '--recursive') {
96
+ result.recursive = true;
97
+ } else if (arg === '-w' || arg === '--watch') {
98
+ result.watch = true;
99
+ } else if (arg === '-o' || arg === '--output') {
100
+ result.output = args[++i];
101
+ } else if (arg === '--dep') {
102
+ // --dep name=path
103
+ const depArg = args[++i];
104
+ if (depArg && depArg.includes('=')) {
105
+ const [name, path] = depArg.split('=');
106
+ result.deps[name] = path;
107
+ }
108
+ } else if (arg.startsWith('--')) {
109
+ // Module arg: --arg-name value -> argName: value
110
+ const argName = arg.slice(2);
111
+ const camelName = argName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
112
+ const value = args[++i];
113
+ result.moduleArgs[camelName] = value;
114
+ } else if (!result.command) {
115
+ result.command = arg;
116
+ } else if (!result.file) {
117
+ result.file = arg;
118
+ }
119
+
120
+ i++;
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ // Convert module path to file path: salesforce.client -> ./salesforce/client.km
127
+ function modulePathToFilePath(modulePath) {
128
+ const parts = modulePath.split('.');
129
+ const filePath = './' + parts.join('/');
130
+
131
+ // Try different extensions (including .static for static data files)
132
+ const extensions = ['.km', '.kimchi', '.kc', '.static'];
133
+ for (const ext of extensions) {
134
+ const fullPath = resolve(filePath + ext);
135
+ if (existsSync(fullPath)) {
136
+ return fullPath;
137
+ }
138
+ }
139
+
140
+ // Try as directory with index
141
+ for (const ext of extensions) {
142
+ const indexPath = resolve(filePath, 'index' + ext);
143
+ if (existsSync(indexPath)) {
144
+ return indexPath;
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ // Extract module metadata (args, deps) from source
152
+ function extractModuleMetadata(source) {
153
+ const metadata = {
154
+ args: [],
155
+ deps: [],
156
+ hasDescribe: false,
157
+ hasHelp: false,
158
+ };
159
+
160
+ // Find arg declarations
161
+ const argRegex = /(!?)arg\s+(\w+)(?:\s*=\s*(.+))?/g;
162
+ let match;
163
+ while ((match = argRegex.exec(source)) !== null) {
164
+ metadata.args.push({
165
+ name: match[2],
166
+ required: match[1] === '!',
167
+ default: match[3] || null,
168
+ });
169
+ }
170
+
171
+ // Find dep statements
172
+ const depRegex = /as\s+(\w+)\s+dep\s+([\w.]+)/g;
173
+ while ((match = depRegex.exec(source)) !== null) {
174
+ metadata.deps.push({
175
+ alias: match[1],
176
+ path: match[2],
177
+ });
178
+ }
179
+
180
+ // Check for _describe and _help functions
181
+ metadata.hasDescribe = /fn\s+_describe\s*\(/.test(source) || /expose\s+fn\s+_describe\s*\(/.test(source);
182
+ metadata.hasHelp = /fn\s+_help\s*\(/.test(source) || /expose\s+fn\s+_help\s*\(/.test(source);
183
+
184
+ return metadata;
185
+ }
186
+
187
+ // Run a module with args and dep injections
188
+ async function runModule(modulePath, moduleArgs, deps, options) {
189
+ const filePath = modulePathToFilePath(modulePath);
190
+
191
+ if (!filePath) {
192
+ console.error(`Error: Module not found: ${modulePath}`);
193
+ console.error(`Looked for: ./${modulePath.split('.').join('/')}.[km|kimchi|kc]`);
194
+ process.exit(1);
195
+ }
196
+
197
+ const source = readFileSync(filePath, 'utf-8');
198
+ const metadata = extractModuleMetadata(source);
199
+
200
+ // Validate required args
201
+ for (const arg of metadata.args) {
202
+ if (arg.required && !moduleArgs[arg.name]) {
203
+ console.error(`Error: Required argument '${arg.name}' not provided`);
204
+ console.error(`Usage: kimchi ${modulePath} --${arg.name.replace(/([A-Z])/g, '-$1').toLowerCase()} <value>`);
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ try {
210
+ // Compile the module
211
+ let javascript = compile(source, { debug: options.debug });
212
+
213
+ // Build the args object to pass
214
+ const argsObj = { ...moduleArgs };
215
+
216
+ // Add defaults for missing optional args
217
+ for (const arg of metadata.args) {
218
+ if (!argsObj[arg.name] && arg.default) {
219
+ argsObj[arg.name] = arg.default;
220
+ }
221
+ }
222
+
223
+ if (options.debug) {
224
+ console.log('\n--- Module Args ---');
225
+ console.log(argsObj);
226
+ console.log('\n--- Dep Injections ---');
227
+ console.log(deps);
228
+ console.log('\n--- Generated JavaScript ---\n');
229
+ console.log(javascript);
230
+ console.log('\n--- Output ---\n');
231
+ }
232
+
233
+ // Write to temp file and execute as ES module
234
+ const os = await import('os');
235
+ const crypto = await import('crypto');
236
+ const tempDir = os.default.tmpdir();
237
+ const tempFile = join(tempDir, `kimchi_${crypto.default.randomBytes(8).toString('hex')}.mjs`);
238
+
239
+ // Replace "export default function" with just "const _module = function" so we can call it
240
+ const modifiedCode = javascript.replace(
241
+ /^export default function/m,
242
+ 'const _module = function'
243
+ );
244
+
245
+ // Wrap the code to call the factory with args
246
+ const wrappedCode = `
247
+ ${modifiedCode}
248
+
249
+ // Call the module factory
250
+ _module(${JSON.stringify(argsObj)});
251
+ `;
252
+
253
+ writeFileSync(tempFile, wrappedCode);
254
+
255
+ try {
256
+ // Execute the temp file
257
+ const { execSync: execSyncImport } = await import('child_process');
258
+ execSyncImport(`node "${tempFile}"`, {
259
+ stdio: 'inherit',
260
+ cwd: dirname(filePath)
261
+ });
262
+ } finally {
263
+ // Clean up temp file
264
+ try {
265
+ const fs = await import('fs');
266
+ fs.default.unlinkSync(tempFile);
267
+ } catch (e) {
268
+ // Ignore cleanup errors
269
+ }
270
+ }
271
+
272
+ } catch (error) {
273
+ console.error('Error:', error.message);
274
+ if (options.debug) {
275
+ console.error(error.stack);
276
+ }
277
+ process.exit(1);
278
+ }
279
+ }
280
+
281
+ // Show help for a module
282
+ async function showModuleHelp(modulePath, options) {
283
+ const filePath = modulePathToFilePath(modulePath);
284
+
285
+ if (!filePath) {
286
+ console.error(`Error: Module not found: ${modulePath}`);
287
+ process.exit(1);
288
+ }
289
+
290
+ const source = readFileSync(filePath, 'utf-8');
291
+ const metadata = extractModuleMetadata(source);
292
+
293
+ console.log(`\nModule: ${modulePath}`);
294
+ console.log(`File: ${filePath}`);
295
+ console.log('');
296
+
297
+ // Try to call _describe if it exists
298
+ if (metadata.hasDescribe) {
299
+ try {
300
+ const javascript = compile(source, { debug: false });
301
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
302
+ const wrappedCode = `
303
+ ${javascript}
304
+ if (typeof _describe === 'function') {
305
+ return _describe();
306
+ }
307
+ return null;
308
+ `;
309
+ const fn = new AsyncFunction(wrappedCode);
310
+ const description = await fn();
311
+ if (description) {
312
+ console.log('Description:');
313
+ console.log(` ${description}`);
314
+ console.log('');
315
+ }
316
+ } catch (e) {
317
+ // Ignore errors in _describe
318
+ }
319
+ }
320
+
321
+ // Show args
322
+ if (metadata.args.length > 0) {
323
+ console.log('Arguments:');
324
+ for (const arg of metadata.args) {
325
+ const flag = '--' + arg.name.replace(/([A-Z])/g, '-$1').toLowerCase();
326
+ const required = arg.required ? ' (required)' : '';
327
+ const defaultVal = arg.default ? ` [default: ${arg.default}]` : '';
328
+ console.log(` ${flag}${required}${defaultVal}`);
329
+ }
330
+ console.log('');
331
+ }
332
+
333
+ // Show dependencies
334
+ if (metadata.deps.length > 0) {
335
+ console.log('Dependencies:');
336
+ for (const dep of metadata.deps) {
337
+ console.log(` ${dep.alias} <- ${dep.path}`);
338
+ }
339
+ console.log('');
340
+ console.log('Inject dependencies with: --dep <alias>=<path>');
341
+ console.log('');
342
+ }
343
+
344
+ // Try to call _help if it exists
345
+ if (metadata.hasHelp) {
346
+ try {
347
+ const javascript = compile(source, { debug: false });
348
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
349
+ const wrappedCode = `
350
+ ${javascript}
351
+ if (typeof _help === 'function') {
352
+ return _help();
353
+ }
354
+ return null;
355
+ `;
356
+ const fn = new AsyncFunction(wrappedCode);
357
+ const helpText = await fn();
358
+ if (helpText) {
359
+ console.log('Additional Help:');
360
+ console.log(helpText);
361
+ }
362
+ } catch (e) {
363
+ // Ignore errors in _help
364
+ }
365
+ }
366
+
367
+ // Usage example
368
+ console.log('Usage:');
369
+ let usage = ` kimchi ${modulePath}`;
370
+ for (const arg of metadata.args) {
371
+ const flag = '--' + arg.name.replace(/([A-Z])/g, '-$1').toLowerCase();
372
+ if (arg.required) {
373
+ usage += ` ${flag} <value>`;
374
+ }
375
+ }
376
+ console.log(usage);
377
+ }
378
+
379
+ // Get description from a module by calling _describe if it exists
380
+ async function getModuleDescription(filePath) {
381
+ try {
382
+ const source = readFileSync(filePath, 'utf-8');
383
+ const hasDescribe = /fn\s+_describe\s*\(/.test(source) || /expose\s+fn\s+_describe\s*\(/.test(source);
384
+
385
+ if (!hasDescribe) {
386
+ return null;
387
+ }
388
+
389
+ const javascript = compile(source, { debug: false });
390
+
391
+ // Replace export default with const so we can call it
392
+ const modifiedCode = javascript.replace(
393
+ /^export default function/m,
394
+ 'const _module = function'
395
+ );
396
+
397
+ // Remove the required arg check so we can call _describe without args
398
+ // Replace: if (_opts["name"] === undefined) throw new Error(...);
399
+ const noArgCheckCode = modifiedCode.replace(
400
+ /if \(_opts\["\w+"\] === undefined\) throw new Error\([^)]+\);\s*/g,
401
+ ''
402
+ );
403
+
404
+ // Suppress console.log to avoid side effects when getting description
405
+ const wrappedCode = `
406
+ const _originalLog = console.log;
407
+ console.log = () => {};
408
+ try {
409
+ ${noArgCheckCode}
410
+
411
+ const result = _module({});
412
+ console.log = _originalLog;
413
+ if (result && typeof result._describe === 'function') {
414
+ return result._describe();
415
+ }
416
+ return null;
417
+ } finally {
418
+ console.log = _originalLog;
419
+ }
420
+ `;
421
+
422
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
423
+ const fn = new AsyncFunction(wrappedCode);
424
+ return await fn();
425
+ } catch (e) {
426
+ return null;
427
+ }
428
+ }
429
+
430
+ // Convert file path to module path: /base/foo/bar.km -> foo.bar
431
+ function filePathToModulePath(filePath, baseDir) {
432
+ const relativePath = relative(baseDir, filePath);
433
+ const ext = extname(relativePath);
434
+ const withoutExt = relativePath.slice(0, -ext.length);
435
+ return withoutExt.replace(/[\/]/g, '.');
436
+ }
437
+
438
+ // List modules in a directory
439
+ async function listModules(searchPath, options) {
440
+ const targetDir = searchPath ? resolve(searchPath) : process.cwd();
441
+
442
+ if (!existsSync(targetDir)) {
443
+ console.error(`Error: Directory not found: ${targetDir}`);
444
+ process.exit(1);
445
+ }
446
+
447
+ const stat = statSync(targetDir);
448
+ if (!stat.isDirectory()) {
449
+ console.error(`Error: Not a directory: ${targetDir}`);
450
+ process.exit(1);
451
+ }
452
+
453
+ console.log(`\nModules in ${targetDir}:\n`);
454
+
455
+ if (options.recursive) {
456
+ await listModulesRecursive(targetDir, '', options, targetDir);
457
+ } else {
458
+ await listModulesFlat(targetDir, options, targetDir);
459
+ }
460
+ }
461
+
462
+ // List modules in a single directory (non-recursive)
463
+ async function listModulesFlat(dir, options, baseDir) {
464
+ const entries = readdirSync(dir);
465
+ const extensions = ['.km', '.kimchi', '.kc'];
466
+
467
+ for (const entry of entries) {
468
+ const fullPath = join(dir, entry);
469
+ const stat = statSync(fullPath);
470
+
471
+ if (stat.isFile()) {
472
+ const ext = extname(entry);
473
+ if (extensions.includes(ext)) {
474
+ const moduleName = basename(entry, ext);
475
+ const modulePath = filePathToModulePath(fullPath, baseDir);
476
+
477
+ if (options.verbose) {
478
+ const description = await getModuleDescription(fullPath);
479
+ if (description) {
480
+ console.log(` ${moduleName}`);
481
+ console.log(` ${modulePath}`);
482
+ console.log(` ${description}`);
483
+ console.log('');
484
+ } else {
485
+ console.log(` ${moduleName}`);
486
+ console.log(` ${modulePath}`);
487
+ console.log('');
488
+ }
489
+ } else {
490
+ console.log(` ${moduleName}`);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ // List modules recursively with tree format
498
+ async function listModulesRecursive(dir, prefix, options, baseDir) {
499
+ const entries = readdirSync(dir).sort();
500
+ const extensions = ['.km', '.kimchi', '.kc'];
501
+
502
+ // Separate files and directories
503
+ const files = [];
504
+ const dirs = [];
505
+
506
+ for (const entry of entries) {
507
+ const fullPath = join(dir, entry);
508
+ const stat = statSync(fullPath);
509
+
510
+ if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
511
+ dirs.push(entry);
512
+ } else if (stat.isFile()) {
513
+ const ext = extname(entry);
514
+ if (extensions.includes(ext)) {
515
+ files.push(entry);
516
+ }
517
+ }
518
+ }
519
+
520
+ const totalItems = files.length + dirs.length;
521
+ let itemIndex = 0;
522
+
523
+ // List files first
524
+ for (const file of files) {
525
+ itemIndex++;
526
+ const isLast = itemIndex === totalItems;
527
+ const connector = isLast ? '└── ' : '├── ';
528
+ const ext = extname(file);
529
+ const moduleName = basename(file, ext);
530
+ const fullPath = join(dir, file);
531
+ const modulePath = filePathToModulePath(fullPath, baseDir);
532
+
533
+ if (options.verbose) {
534
+ const description = await getModuleDescription(fullPath);
535
+ console.log(`${prefix}${connector}${moduleName}`);
536
+ console.log(`${prefix}${isLast ? ' ' : '│ '} ${modulePath}`);
537
+ if (description) {
538
+ console.log(`${prefix}${isLast ? ' ' : '│ '} ${description}`);
539
+ }
540
+ } else {
541
+ console.log(`${prefix}${connector}${moduleName}`);
542
+ }
543
+ }
544
+
545
+ // Then recurse into directories
546
+ for (const subdir of dirs) {
547
+ itemIndex++;
548
+ const isLast = itemIndex === totalItems;
549
+ const connector = isLast ? '└── ' : '├── ';
550
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
551
+ const fullPath = join(dir, subdir);
552
+
553
+ console.log(`${prefix}${connector}${subdir}/`);
554
+ await listModulesRecursive(fullPath, newPrefix, options, baseDir);
555
+ }
556
+ }
557
+
558
+ function compileFile(filePath, options = {}) {
559
+ if (!existsSync(filePath)) {
560
+ console.error(`Error: File not found: ${filePath}`);
561
+ process.exit(1);
562
+ }
563
+
564
+ // Reject kebab-case file names
565
+ const fileName = basename(filePath);
566
+ if (/-/.test(fileName) && /\.(km|kimchi|kc)$/.test(fileName)) {
567
+ console.error(`Error: Kebab-case file names are not allowed: ${fileName}`);
568
+ console.error(`Please rename to use underscores: ${fileName.replace(/-/g, '_')}`);
569
+ process.exit(1);
570
+ }
571
+
572
+ const source = readFileSync(filePath, 'utf-8');
573
+
574
+ // Static file resolver - checks if a module path resolves to a .static file
575
+ const staticFileResolver = (modulePath) => {
576
+ const parts = modulePath.split('.');
577
+ const baseFilePath = './' + parts.join('/');
578
+ const staticPath = resolve(baseFilePath + '.static');
579
+ return existsSync(staticPath);
580
+ };
581
+
582
+ try {
583
+ const javascript = compile(source, {
584
+ debug: options.debug,
585
+ skipLint: options.skipLint,
586
+ showLintWarnings: true,
587
+ staticFileResolver,
588
+ basePath: options.basePath,
589
+ });
590
+ return javascript;
591
+ } catch (error) {
592
+ console.error(`Compilation Error in ${filePath}:`);
593
+ console.error(error.message);
594
+ if (options.debug) {
595
+ console.error(error.stack);
596
+ }
597
+ process.exit(1);
598
+ }
599
+ }
600
+
601
+ function compileStaticFile(filePath, options = {}) {
602
+ if (!existsSync(filePath)) {
603
+ console.error(`Error: File not found: ${filePath}`);
604
+ process.exit(1);
605
+ }
606
+
607
+ const source = readFileSync(filePath, 'utf-8');
608
+
609
+ try {
610
+ const declarations = parseStaticFile(source, filePath);
611
+ const javascript = generateStaticCode(declarations, filePath);
612
+ return javascript;
613
+ } catch (error) {
614
+ console.error(`Static File Compilation Error in ${filePath}:`);
615
+ console.error(error.message);
616
+ if (options.debug) {
617
+ console.error(error.stack);
618
+ }
619
+ process.exit(1);
620
+ }
621
+ }
622
+
623
+ function checkFile(source, filePath = null) {
624
+ // Check file for errors and return JSON-compatible error array
625
+ const errors = [];
626
+
627
+ try {
628
+ // Step 1: Tokenize
629
+ const tokens = tokenize(source);
630
+
631
+ // Step 2: Parse
632
+ const ast = parse(tokens);
633
+
634
+ // Step 3: Lint
635
+ const linter = new Linter();
636
+ const lintMessages = linter.lint(ast, source);
637
+
638
+ for (const msg of lintMessages) {
639
+ if (msg.severity === Severity.Error) {
640
+ errors.push({
641
+ line: msg.line || 1,
642
+ column: msg.column || 1,
643
+ message: `[${msg.rule}] ${msg.message}`,
644
+ severity: 'error',
645
+ });
646
+ }
647
+ }
648
+
649
+ // Step 4: Type check (compile without generating code)
650
+ compile(source, { skipLint: true });
651
+
652
+ } catch (error) {
653
+ // Parse error format: "Parse Error at line:column: message"
654
+ const match = error.message.match(/(?:Parse |Lexer |Type |Compile )?Error at (\d+):(\d+):\s*(.+)/i);
655
+ if (match) {
656
+ errors.push({
657
+ line: parseInt(match[1], 10),
658
+ column: parseInt(match[2], 10),
659
+ message: match[3],
660
+ severity: 'error',
661
+ });
662
+ } else {
663
+ // Generic error
664
+ errors.push({
665
+ line: 1,
666
+ column: 1,
667
+ message: error.message,
668
+ severity: 'error',
669
+ });
670
+ }
671
+ }
672
+
673
+ return errors;
674
+ }
675
+
676
+ function lintFile(filePath, options = {}) {
677
+ if (!existsSync(filePath)) {
678
+ console.error(`Error: File not found: ${filePath}`);
679
+ process.exit(1);
680
+ }
681
+
682
+ const source = readFileSync(filePath, 'utf-8');
683
+
684
+ try {
685
+ const tokens = tokenize(source);
686
+ const ast = parse(tokens);
687
+
688
+ const linter = new Linter();
689
+ const messages = linter.lint(ast, source);
690
+
691
+ if (messages.length === 0) {
692
+ console.log(`✅ ${basename(filePath)}: No lint issues found`);
693
+ } else {
694
+ console.log(`\n📋 Lint results for ${basename(filePath)}:`);
695
+ console.log(linter.formatMessages());
696
+
697
+ if (linter.hasErrors()) {
698
+ process.exit(1);
699
+ }
700
+ }
701
+ } catch (error) {
702
+ console.error(`Error linting ${filePath}:`);
703
+ console.error(error.message);
704
+ if (options.debug) {
705
+ console.error(error.stack);
706
+ }
707
+ process.exit(1);
708
+ }
709
+ }
710
+
711
+ async function runTests(filePath, options = {}) {
712
+ if (!existsSync(filePath)) {
713
+ console.error(`Error: File not found: ${filePath}`);
714
+ process.exit(1);
715
+ }
716
+
717
+ const javascript = compileFile(filePath, options);
718
+
719
+ if (options.debug) {
720
+ console.log('\n--- Generated JavaScript ---\n');
721
+ console.log(javascript);
722
+ console.log('\n--- Running Tests ---\n');
723
+ }
724
+
725
+ try {
726
+ const os = await import('os');
727
+ const crypto = await import('crypto');
728
+ const tempDir = os.default.tmpdir();
729
+ const tempFile = join(tempDir, `kimchi_test_${crypto.default.randomBytes(8).toString('hex')}.mjs`);
730
+
731
+ const modifiedCode = javascript.replace(
732
+ /^export default function/m,
733
+ 'const _module = function'
734
+ );
735
+
736
+ const wrappedCode = `
737
+ ${modifiedCode}
738
+
739
+ // Call the module factory to register tests
740
+ _module({});
741
+
742
+ // Run all registered tests
743
+ await _runTests();
744
+ `;
745
+
746
+ writeFileSync(tempFile, wrappedCode);
747
+
748
+ try {
749
+ execSync(`node "${tempFile}"`, {
750
+ stdio: 'inherit',
751
+ cwd: dirname(filePath)
752
+ });
753
+ } finally {
754
+ try {
755
+ const fs = await import('fs');
756
+ fs.default.unlinkSync(tempFile);
757
+ } catch (e) {
758
+ // Ignore cleanup errors
759
+ }
760
+ }
761
+ } catch (error) {
762
+ console.error('Test Error:');
763
+ console.error(error.message);
764
+ if (options.debug) {
765
+ console.error(error.stack);
766
+ }
767
+ process.exit(1);
768
+ }
769
+ }
770
+
771
+ async function runFile(filePath, options = {}) {
772
+ // Pass basePath for absolute path resolution of static files
773
+ const javascript = compileFile(filePath, { ...options, basePath: dirname(resolve(filePath)) });
774
+
775
+ if (options.debug) {
776
+ console.log('\n--- Generated JavaScript ---\n');
777
+ console.log(javascript);
778
+ console.log('\n--- Output ---\n');
779
+ }
780
+
781
+ try {
782
+ // Write to temp file and execute as ES module
783
+ const os = await import('os');
784
+ const crypto = await import('crypto');
785
+ const tempDir = os.default.tmpdir();
786
+ const tempFile = join(tempDir, `kimchi_${crypto.default.randomBytes(8).toString('hex')}.mjs`);
787
+
788
+ // Replace export default with const so we can call it
789
+ const modifiedCode = javascript.replace(
790
+ /^export default function/m,
791
+ 'const _module = function'
792
+ );
793
+
794
+ const wrappedCode = `
795
+ ${modifiedCode}
796
+
797
+ // Call the module factory
798
+ _module({});
799
+ `;
800
+
801
+ writeFileSync(tempFile, wrappedCode);
802
+
803
+ try {
804
+ execSync(`node "${tempFile}"`, {
805
+ stdio: 'inherit',
806
+ cwd: dirname(filePath)
807
+ });
808
+ } finally {
809
+ try {
810
+ const fs = await import('fs');
811
+ fs.default.unlinkSync(tempFile);
812
+ } catch (e) {
813
+ // Ignore cleanup errors
814
+ }
815
+ }
816
+ } catch (error) {
817
+ console.error('Runtime Error:');
818
+ console.error(error.message);
819
+ if (options.debug) {
820
+ console.error(error.stack);
821
+ }
822
+ process.exit(1);
823
+ }
824
+ }
825
+
826
+ async function startRepl() {
827
+ const readline = await import('readline');
828
+
829
+ const rl = readline.createInterface({
830
+ input: process.stdin,
831
+ output: process.stdout,
832
+ prompt: 'kimchi> ',
833
+ });
834
+
835
+ console.log(`KimchiLang REPL v${VERSION}`);
836
+ console.log('Type "exit" or press Ctrl+C to quit.\n');
837
+
838
+ rl.prompt();
839
+
840
+ let multilineBuffer = '';
841
+ let braceCount = 0;
842
+
843
+ rl.on('line', (line) => {
844
+ if (line.trim() === 'exit') {
845
+ rl.close();
846
+ return;
847
+ }
848
+
849
+ multilineBuffer += line + '\n';
850
+ braceCount += (line.match(/{/g) || []).length;
851
+ braceCount -= (line.match(/}/g) || []).length;
852
+
853
+ if (braceCount > 0) {
854
+ process.stdout.write('... ');
855
+ return;
856
+ }
857
+
858
+ if (multilineBuffer.trim()) {
859
+ try {
860
+ const javascript = compile(multilineBuffer.trim());
861
+ const result = eval(javascript);
862
+ if (result !== undefined) {
863
+ console.log(result);
864
+ }
865
+ } catch (error) {
866
+ console.error('Error:', error.message);
867
+ }
868
+ }
869
+
870
+ multilineBuffer = '';
871
+ braceCount = 0;
872
+ rl.prompt();
873
+ });
874
+
875
+ rl.on('close', () => {
876
+ console.log('\nGoodbye!');
877
+ process.exit(0);
878
+ });
879
+ }
880
+
881
+ function getAllJsFiles(dir, files = []) {
882
+ const entries = readdirSync(dir);
883
+
884
+ for (const entry of entries) {
885
+ const fullPath = join(dir, entry);
886
+ const stat = statSync(fullPath);
887
+
888
+ if (stat.isDirectory()) {
889
+ // Skip certain directories
890
+ if (entry === '.bin' || entry === '.cache') continue;
891
+ getAllJsFiles(fullPath, files);
892
+ } else if (entry.endsWith('.js') && !entry.endsWith('.min.js')) {
893
+ files.push(fullPath);
894
+ }
895
+ }
896
+
897
+ return files;
898
+ }
899
+
900
+ function getPackageMainFile(packageDir) {
901
+ const packageJsonPath = join(packageDir, 'package.json');
902
+ if (existsSync(packageJsonPath)) {
903
+ try {
904
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
905
+ // Try main, then module, then index.js
906
+ const mainFile = pkg.main || pkg.module || 'index.js';
907
+ const mainPath = join(packageDir, mainFile);
908
+ if (existsSync(mainPath)) {
909
+ return mainPath;
910
+ }
911
+ // Try adding .js extension
912
+ if (existsSync(mainPath + '.js')) {
913
+ return mainPath + '.js';
914
+ }
915
+ } catch (e) {
916
+ // Ignore JSON parse errors
917
+ }
918
+ }
919
+
920
+ // Fallback to index.js
921
+ const indexPath = join(packageDir, 'index.js');
922
+ if (existsSync(indexPath)) {
923
+ return indexPath;
924
+ }
925
+
926
+ return null;
927
+ }
928
+
929
+ async function runNpmAndConvert(npmArgs, options) {
930
+ const cwd = process.cwd();
931
+ const nodeModulesDir = join(cwd, 'node_modules');
932
+ const pantryDir = join(cwd, 'pantry');
933
+
934
+ // Run npm with the provided arguments
935
+ console.log(`Running: npm ${npmArgs.join(' ')}`);
936
+
937
+ try {
938
+ const npmProcess = spawn('npm', npmArgs, {
939
+ cwd,
940
+ stdio: 'inherit',
941
+ });
942
+
943
+ await new Promise((resolve, reject) => {
944
+ npmProcess.on('close', (code) => {
945
+ if (code === 0) {
946
+ resolve();
947
+ } else {
948
+ reject(new Error(`npm exited with code ${code}`));
949
+ }
950
+ });
951
+ npmProcess.on('error', reject);
952
+ });
953
+ } catch (error) {
954
+ console.error('npm command failed:', error.message);
955
+ process.exit(1);
956
+ }
957
+
958
+ // Only convert if this was an install command
959
+ if (!npmArgs.includes('install') && !npmArgs.includes('i') && !npmArgs.includes('add')) {
960
+ return;
961
+ }
962
+
963
+ // Create pantry directory
964
+ if (!existsSync(pantryDir)) {
965
+ mkdirSync(pantryDir, { recursive: true });
966
+ }
967
+
968
+ // Find installed packages
969
+ if (!existsSync(nodeModulesDir)) {
970
+ console.log('No node_modules directory found');
971
+ return;
972
+ }
973
+
974
+ const packages = readdirSync(nodeModulesDir).filter(name => {
975
+ // Skip hidden files and scoped packages for now
976
+ if (name.startsWith('.') || name.startsWith('@')) return false;
977
+ const pkgPath = join(nodeModulesDir, name);
978
+ return statSync(pkgPath).isDirectory();
979
+ });
980
+
981
+ console.log(`\nConverting ${packages.length} packages to pantry/...`);
982
+
983
+ let converted = 0;
984
+ let failed = 0;
985
+
986
+ for (const pkgName of packages) {
987
+ const pkgDir = join(nodeModulesDir, pkgName);
988
+ const mainFile = getPackageMainFile(pkgDir);
989
+
990
+ if (!mainFile) {
991
+ if (options.debug) {
992
+ console.log(` Skipping ${pkgName}: no main file found`);
993
+ }
994
+ continue;
995
+ }
996
+
997
+ try {
998
+ const jsSource = readFileSync(mainFile, 'utf-8');
999
+ const kimchiCode = convertJS(jsSource);
1000
+
1001
+ // Create package directory in pantry
1002
+ const pantryPkgDir = join(pantryDir, pkgName);
1003
+ if (!existsSync(pantryPkgDir)) {
1004
+ mkdirSync(pantryPkgDir, { recursive: true });
1005
+ }
1006
+
1007
+ // Write the converted file
1008
+ const outputFile = join(pantryPkgDir, 'index.km');
1009
+ writeFileSync(outputFile, kimchiCode);
1010
+
1011
+ converted++;
1012
+ if (options.debug) {
1013
+ console.log(` ✓ ${pkgName}`);
1014
+ }
1015
+ } catch (error) {
1016
+ failed++;
1017
+ if (options.debug) {
1018
+ console.log(` ✗ ${pkgName}: ${error.message}`);
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ console.log(`\nPantry: ${converted} packages converted, ${failed} failed`);
1024
+ console.log(`Use: as <alias> dep pantry.<package>`);
1025
+ }
1026
+
1027
+ async function main() {
1028
+ const args = process.argv.slice(2);
1029
+ const options = parseArgs(args);
1030
+
1031
+ if (options.help || args.length === 0) {
1032
+ console.log(HELP);
1033
+ process.exit(0);
1034
+ }
1035
+
1036
+ switch (options.command) {
1037
+ case 'version':
1038
+ case '-v':
1039
+ case '--version':
1040
+ console.log(`KimchiLang v${VERSION}`);
1041
+ break;
1042
+
1043
+ case 'help':
1044
+ // Check if help is for a specific module
1045
+ if (options.file) {
1046
+ // Try to find the module
1047
+ const helpFilePath = modulePathToFilePath(options.file);
1048
+ if (helpFilePath) {
1049
+ await showModuleHelp(options.file, options);
1050
+ } else {
1051
+ console.log(HELP);
1052
+ }
1053
+ } else {
1054
+ console.log(HELP);
1055
+ }
1056
+ break;
1057
+
1058
+ case 'ls': {
1059
+ // List modules in directory
1060
+ await listModules(options.file, options);
1061
+ break;
1062
+ }
1063
+
1064
+ case 'compile': {
1065
+ if (!options.file) {
1066
+ console.error('Error: No input file specified');
1067
+ process.exit(1);
1068
+ }
1069
+
1070
+ const filePath = resolve(options.file);
1071
+
1072
+ // Check if it's a static file
1073
+ if (filePath.endsWith('.static')) {
1074
+ const javascript = compileStaticFile(filePath, options);
1075
+ if (options.output) {
1076
+ writeFileSync(options.output, javascript);
1077
+ console.log(`Compiled: ${options.file} -> ${options.output}`);
1078
+ } else {
1079
+ const outputPath = filePath.replace(/\.static$/, '.static.js');
1080
+ writeFileSync(outputPath, javascript);
1081
+ console.log(`Compiled: ${options.file} -> ${basename(outputPath)}`);
1082
+ }
1083
+ } else {
1084
+ const javascript = compileFile(filePath, options);
1085
+ if (options.output) {
1086
+ writeFileSync(options.output, javascript);
1087
+ console.log(`Compiled: ${options.file} -> ${options.output}`);
1088
+ } else {
1089
+ const outputPath = filePath.replace(/\.(kimchi|kc)$/, '.js');
1090
+ writeFileSync(outputPath, javascript);
1091
+ console.log(`Compiled: ${options.file} -> ${basename(outputPath)}`);
1092
+ }
1093
+ }
1094
+ break;
1095
+ }
1096
+
1097
+ case 'run': {
1098
+ if (!options.file) {
1099
+ console.error('Error: No input file specified');
1100
+ process.exit(1);
1101
+ }
1102
+
1103
+ const filePath = resolve(options.file);
1104
+ runFile(filePath, options);
1105
+ break;
1106
+ }
1107
+
1108
+ case 'lint': {
1109
+ if (!options.file) {
1110
+ console.error('Error: No input file specified');
1111
+ process.exit(1);
1112
+ }
1113
+
1114
+ const filePath = resolve(options.file);
1115
+ lintFile(filePath, options);
1116
+ break;
1117
+ }
1118
+
1119
+ case 'check': {
1120
+ // Check command for editor integration - outputs JSON errors
1121
+ let source;
1122
+ let filePath = options.file ? resolve(options.file) : null;
1123
+
1124
+ // Read from stdin if --json flag or no file
1125
+ if (process.stdin.isTTY === false) {
1126
+ // Read from stdin
1127
+ const chunks = [];
1128
+ for await (const chunk of process.stdin) {
1129
+ chunks.push(chunk);
1130
+ }
1131
+ source = Buffer.concat(chunks).toString('utf-8');
1132
+ } else if (filePath && existsSync(filePath)) {
1133
+ source = readFileSync(filePath, 'utf-8');
1134
+ } else {
1135
+ console.log(JSON.stringify({ errors: [] }));
1136
+ break;
1137
+ }
1138
+
1139
+ const errors = checkFile(source, filePath);
1140
+ console.log(JSON.stringify({ errors }));
1141
+ break;
1142
+ }
1143
+
1144
+ case 'test': {
1145
+ if (!options.file) {
1146
+ console.error('Error: No input file specified');
1147
+ process.exit(1);
1148
+ }
1149
+
1150
+ const filePath = resolve(options.file);
1151
+ await runTests(filePath, options);
1152
+ break;
1153
+ }
1154
+
1155
+ case 'repl':
1156
+ await startRepl();
1157
+ break;
1158
+
1159
+ case 'convert': {
1160
+ if (!options.file) {
1161
+ console.error('Error: No input file specified');
1162
+ process.exit(1);
1163
+ }
1164
+
1165
+ const filePath = resolve(options.file);
1166
+ if (!existsSync(filePath)) {
1167
+ console.error(`Error: File not found: ${filePath}`);
1168
+ process.exit(1);
1169
+ }
1170
+
1171
+ const jsSource = readFileSync(filePath, 'utf-8');
1172
+
1173
+ try {
1174
+ const kimchiCode = convertJS(jsSource);
1175
+
1176
+ if (options.output) {
1177
+ writeFileSync(options.output, kimchiCode);
1178
+ console.log(`Converted: ${options.file} -> ${options.output}`);
1179
+ } else {
1180
+ const outputPath = filePath.replace(/\.js$/, '.km');
1181
+ writeFileSync(outputPath, kimchiCode);
1182
+ console.log(`Converted: ${options.file} -> ${basename(outputPath)}`);
1183
+ }
1184
+ } catch (error) {
1185
+ console.error(`Conversion Error in ${filePath}:`);
1186
+ console.error(error.message);
1187
+ if (options.debug) {
1188
+ console.error(error.stack);
1189
+ }
1190
+ process.exit(1);
1191
+ }
1192
+ break;
1193
+ }
1194
+
1195
+ case 'npm': {
1196
+ // Pass all remaining args to npm
1197
+ const npmArgs = process.argv.slice(3);
1198
+ await runNpmAndConvert(npmArgs, options);
1199
+ break;
1200
+ }
1201
+
1202
+ case 'install': {
1203
+ // Install dependencies from project.static
1204
+ console.log('KimchiLang Package Manager\n');
1205
+ installDependencies('.');
1206
+ break;
1207
+ }
1208
+
1209
+ case 'clean': {
1210
+ // Remove installed dependencies
1211
+ console.log('Cleaning dependencies...\n');
1212
+ cleanDependencies('.');
1213
+ break;
1214
+ }
1215
+
1216
+ default:
1217
+ // Check if it's a file to run directly
1218
+ if (options.command && (options.command.endsWith('.kimchi') || options.command.endsWith('.kc') || options.command.endsWith('.km'))) {
1219
+ const filePath = resolve(options.command);
1220
+ runFile(filePath, options);
1221
+ }
1222
+ // Check if it's a module path (contains dots but not a file extension)
1223
+ else if (options.command && options.command.includes('.') && !options.command.match(/\.(kimchi|kc|km|js)$/)) {
1224
+ await runModule(options.command, options.moduleArgs, options.deps, options);
1225
+ }
1226
+ else if (options.command) {
1227
+ // Try as a simple module name (single word)
1228
+ const filePath = modulePathToFilePath(options.command);
1229
+ if (filePath) {
1230
+ await runModule(options.command, options.moduleArgs, options.deps, options);
1231
+ } else {
1232
+ console.error(`Unknown command or module: ${options.command}`);
1233
+ console.log(HELP);
1234
+ process.exit(1);
1235
+ }
1236
+ } else {
1237
+ console.log(HELP);
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ main().catch(error => {
1243
+ console.error('Fatal Error:', error.message);
1244
+ process.exit(1);
1245
+ });