@kernlang/cli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,981 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
3
+ import { resolve, basename, dirname, relative } from 'path';
4
+ import { createJiti } from 'jiti';
5
+ import { parse, decompile, resolveConfig, VALID_TARGETS, VALID_STRUCTURES, generateCoreNode, isCoreNode, detectVersionsFromPackageJson, scanProject, generateConfigSource, formatScanSummary, registerTemplate, isTemplateNode, expandTemplateNode, clearTemplates, detectTemplates, COMMON_TEMPLATES } from '@kernlang/core';
6
+ import { generateReactNode, isReactNode } from '@kernlang/react';
7
+ import { transpile } from '@kernlang/native';
8
+ import { transpileWeb, transpileTailwind, transpileNextjs } from '@kernlang/react';
9
+ import { transpileExpress } from '@kernlang/express';
10
+ import { transpileCliApp } from './transpiler-cli.js';
11
+ import { transpileTerminal } from '@kernlang/terminal';
12
+ import { transpileVue, transpileNuxt } from '@kernlang/vue';
13
+ import { collectLanguageMetrics } from '@kernlang/metrics';
14
+ import { reviewFile, reviewDirectory, formatReport, formatSummary, checkEnforcement, formatEnforcement, exportKernIR, buildLLMPrompt, dedup, runESLint, runTSCDiagnosticsFromPaths, linkToNodes } from '@kernlang/review';
15
+ const args = process.argv.slice(2);
16
+ const GENERATED_HEADER = '// Generated by KERN — do not edit. Source: ';
17
+ // ── kern dev <dir|file> [--target=...] [--outdir=...] ─────────────────
18
+ if (args[0] === 'dev') {
19
+ const devInput = args[1];
20
+ if (!devInput) {
21
+ console.error('Usage: kern dev <file.kern|dir> [--target=nextjs] [--outdir=<dir>]');
22
+ process.exit(1);
23
+ }
24
+ const inputPath = resolve(devInput);
25
+ const stat = existsSync(inputPath) ? statSync(inputPath) : null;
26
+ if (!stat) {
27
+ console.error(`Not found: ${devInput}`);
28
+ process.exit(1);
29
+ }
30
+ const watchDir = stat.isDirectory() ? inputPath : dirname(inputPath);
31
+ const watchPattern = stat.isDirectory() ? undefined : basename(inputPath);
32
+ // Load config
33
+ const devConfig = loadConfig();
34
+ // CLI overrides
35
+ const devCliTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
36
+ if (devCliTarget) {
37
+ if (!VALID_TARGETS.includes(devCliTarget)) {
38
+ console.error(`Unknown target: '${devCliTarget}'.`);
39
+ process.exit(1);
40
+ }
41
+ devConfig.target = devCliTarget;
42
+ }
43
+ const devOutDir = args.find(a => a.startsWith('--outdir='))?.split('=')[1];
44
+ // Auto-detect framework versions from nearest package.json (walk up from watchDir)
45
+ const pkgPath = findNearestPackageJson(watchDir);
46
+ if (pkgPath && Object.keys(devConfig.frameworkVersions).length === 0) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
49
+ const detected = detectVersionsFromPackageJson(pkg);
50
+ if (detected.tailwind || detected.nextjs) {
51
+ devConfig.frameworkVersions = { ...devConfig.frameworkVersions, ...detected };
52
+ const parts = [];
53
+ if (detected.tailwind)
54
+ parts.push(`Tailwind ${detected.tailwind}`);
55
+ if (detected.nextjs)
56
+ parts.push(`Next.js ${detected.nextjs}`);
57
+ console.log(` Auto-detected: ${parts.join(', ')}`);
58
+ }
59
+ }
60
+ catch { }
61
+ }
62
+ // Load templates before compilation
63
+ loadTemplates(devConfig);
64
+ console.log(`\n KERN dev — watching for changes`);
65
+ console.log(` Target: ${devConfig.target}`);
66
+ console.log(` Watch: ${relative(process.cwd(), watchDir) || '.'}`);
67
+ console.log('');
68
+ // Initial build of all .kern files
69
+ const initialFiles = findKernFiles(watchDir, watchPattern);
70
+ for (const file of initialFiles) {
71
+ transpileAndWrite(file, devConfig, devOutDir);
72
+ }
73
+ if (initialFiles.length > 0) {
74
+ console.log(` ${initialFiles.length} file(s) compiled.\n`);
75
+ }
76
+ // Watch for changes
77
+ import('chokidar').then(({ watch }) => {
78
+ const globPattern = watchPattern
79
+ ? resolve(watchDir, watchPattern)
80
+ : resolve(watchDir, '**/*.kern');
81
+ const watcher = watch(globPattern, {
82
+ ignoreInitial: true,
83
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
84
+ });
85
+ watcher.on('change', (filePath) => {
86
+ const rel = relative(process.cwd(), filePath);
87
+ const start = performance.now();
88
+ try {
89
+ transpileAndWrite(filePath, devConfig, devOutDir);
90
+ const ms = Math.round(performance.now() - start);
91
+ console.log(` ${rel} → compiled (${ms}ms)`);
92
+ }
93
+ catch (err) {
94
+ console.error(` ${rel} → ERROR: ${err.message}`);
95
+ }
96
+ });
97
+ watcher.on('add', (filePath) => {
98
+ const rel = relative(process.cwd(), filePath);
99
+ const start = performance.now();
100
+ try {
101
+ transpileAndWrite(filePath, devConfig, devOutDir);
102
+ const ms = Math.round(performance.now() - start);
103
+ console.log(` ${rel} → compiled (${ms}ms)`);
104
+ }
105
+ catch (err) {
106
+ console.error(` ${rel} → ERROR: ${err.message}`);
107
+ }
108
+ });
109
+ watcher.on('unlink', (filePath) => {
110
+ const rel = relative(process.cwd(), filePath);
111
+ const ext = filePath.endsWith('.kern') ? '.kern' : '.ir';
112
+ const fileBaseName = basename(filePath, ext);
113
+ const outDir = resolve(devOutDir ? resolve(devOutDir) : dirname(filePath), devConfig.output.outDir);
114
+ const outExt = (devConfig.target === 'vue' || devConfig.target === 'nuxt') ? '.vue'
115
+ : (devConfig.target === 'express' || devConfig.target === 'cli' || devConfig.target === 'terminal') ? '.ts' : '.tsx';
116
+ const outFile = resolve(outDir, `${fileBaseName}${outExt}`);
117
+ try {
118
+ if (existsSync(outFile)) {
119
+ unlinkSync(outFile);
120
+ console.log(` ${rel} → deleted generated file`);
121
+ }
122
+ }
123
+ catch (err) {
124
+ console.error(` ${rel} → ERROR deleting: ${err.message}`);
125
+ }
126
+ });
127
+ console.log(' Watching for changes... (Ctrl+C to stop)\n');
128
+ }).catch((err) => {
129
+ console.error(`kern dev requires chokidar: npm install chokidar`);
130
+ process.exit(1);
131
+ });
132
+ // Keep process alive
133
+ process.on('SIGINT', () => {
134
+ console.log('\n KERN dev stopped.');
135
+ process.exit(0);
136
+ });
137
+ // Don't fall through to standard transpile
138
+ // Keep event loop running by not calling process.exit()
139
+ await new Promise(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function
140
+ }
141
+ function findNearestPackageJson(startDir) {
142
+ let dir = startDir;
143
+ while (true) {
144
+ const candidate = resolve(dir, 'package.json');
145
+ if (existsSync(candidate))
146
+ return candidate;
147
+ const parent = dirname(dir);
148
+ if (parent === dir)
149
+ return null; // hit root
150
+ dir = parent;
151
+ }
152
+ }
153
+ function findKernFiles(dir, singleFile) {
154
+ if (singleFile)
155
+ return [resolve(dir, singleFile)];
156
+ const files = [];
157
+ function walk(d) {
158
+ for (const entry of readdirSync(d)) {
159
+ const full = resolve(d, entry);
160
+ const s = statSync(full);
161
+ if (s.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
162
+ walk(full);
163
+ }
164
+ else if (entry.endsWith('.kern')) {
165
+ files.push(full);
166
+ }
167
+ }
168
+ }
169
+ walk(dir);
170
+ return files;
171
+ }
172
+ function transpileAndWrite(file, cfg, outDirOverride) {
173
+ const source = readFileSync(file, 'utf-8');
174
+ const ast = parse(source);
175
+ const ext = file.endsWith('.kern') ? '.kern' : '.ir';
176
+ const name = basename(file, ext);
177
+ const target = cfg.target;
178
+ const relSource = relative(process.cwd(), file);
179
+ const header = GENERATED_HEADER + relSource + '\n\n';
180
+ const result = target === 'native'
181
+ ? transpile(ast, cfg)
182
+ : target === 'web'
183
+ ? transpileWeb(ast, cfg)
184
+ : target === 'tailwind'
185
+ ? transpileTailwind(ast, cfg)
186
+ : target === 'express'
187
+ ? transpileExpress(ast, cfg)
188
+ : target === 'cli'
189
+ ? transpileCliApp(ast, cfg)
190
+ : target === 'terminal'
191
+ ? transpileTerminal(ast, cfg)
192
+ : target === 'vue'
193
+ ? transpileVue(ast, cfg)
194
+ : target === 'nuxt'
195
+ ? transpileNuxt(ast, cfg)
196
+ : transpileNextjs(ast, cfg);
197
+ const outDir = resolve(outDirOverride ? resolve(outDirOverride) : dirname(file), cfg.output.outDir);
198
+ mkdirSync(outDir, { recursive: true });
199
+ if (result.artifacts && result.artifacts.length > 0 && cfg.structure !== 'flat') {
200
+ for (const artifact of result.artifacts) {
201
+ const artifactPath = resolve(outDir, artifact.path);
202
+ mkdirSync(dirname(artifactPath), { recursive: true });
203
+ writeFileSync(artifactPath, header + artifact.content);
204
+ }
205
+ }
206
+ else {
207
+ const outExt = (target === 'vue' || target === 'nuxt') ? '.vue'
208
+ : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts' : '.tsx';
209
+ // For Next.js target, use the file convention name from the transpiler result (page.tsx, layout.tsx, etc.)
210
+ const resultWithFiles = result;
211
+ const outFileName = (target === 'nextjs' && resultWithFiles.files && resultWithFiles.files.length > 0)
212
+ ? resultWithFiles.files[0].path
213
+ : `${name}${outExt}`;
214
+ writeFileSync(resolve(outDir, outFileName), header + result.code);
215
+ if (result.artifacts) {
216
+ for (const artifact of result.artifacts) {
217
+ const artifactPath = resolve(outDir, artifact.path);
218
+ mkdirSync(dirname(artifactPath), { recursive: true });
219
+ writeFileSync(artifactPath, header + artifact.content);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ function loadConfig() {
225
+ const configPath = resolve(process.cwd(), 'kern.config.ts');
226
+ if (existsSync(configPath)) {
227
+ try {
228
+ const jiti = createJiti(import.meta.url);
229
+ const mod = jiti(configPath);
230
+ const userConfig = mod.default ?? mod;
231
+ return resolveConfig(userConfig);
232
+ }
233
+ catch (err) {
234
+ console.error(`Warning: Failed to load kern.config.ts: ${err.message}`);
235
+ return resolveConfig({});
236
+ }
237
+ }
238
+ return resolveConfig({});
239
+ }
240
+ function loadTemplates(cfg) {
241
+ clearTemplates();
242
+ if (!cfg.templates || cfg.templates.length === 0)
243
+ return;
244
+ for (const templatePath of cfg.templates) {
245
+ const resolved = resolve(process.cwd(), templatePath);
246
+ if (!existsSync(resolved))
247
+ continue;
248
+ const stat = statSync(resolved);
249
+ const files = [];
250
+ if (stat.isDirectory()) {
251
+ for (const entry of readdirSync(resolved)) {
252
+ if (entry.endsWith('.kern'))
253
+ files.push(resolve(resolved, entry));
254
+ }
255
+ }
256
+ else if (resolved.endsWith('.kern')) {
257
+ files.push(resolved);
258
+ }
259
+ for (const file of files) {
260
+ try {
261
+ const source = readFileSync(file, 'utf-8');
262
+ const ast = parse(source);
263
+ // Register top-level template nodes
264
+ const nodes = ast.type === 'template' ? [ast] : (ast.children || []).filter(n => n.type === 'template');
265
+ for (const node of nodes) {
266
+ registerTemplate(node, file);
267
+ }
268
+ }
269
+ catch (err) {
270
+ console.error(` Warning: Failed to load template ${basename(file)}: ${err.message}`);
271
+ }
272
+ }
273
+ }
274
+ }
275
+ // ── kern compile <dir|file> --outdir=<dir> ────────────────────────────
276
+ if (args[0] === 'compile') {
277
+ const compileInput = args[1];
278
+ const outDirArg = args.find(a => a.startsWith('--outdir='))?.split('=')[1];
279
+ if (!compileInput) {
280
+ console.error('Usage: kern compile <file.kern|dir> --outdir=<dir>');
281
+ process.exit(1);
282
+ }
283
+ const outDir = resolve(outDirArg || 'generated');
284
+ mkdirSync(outDir, { recursive: true });
285
+ const inputPath = resolve(compileInput);
286
+ const stat = existsSync(inputPath) ? statSync(inputPath) : null;
287
+ const kernFiles = [];
288
+ if (stat && stat.isDirectory()) {
289
+ for (const f of readdirSync(inputPath)) {
290
+ if (f.endsWith('.kern'))
291
+ kernFiles.push(resolve(inputPath, f));
292
+ }
293
+ }
294
+ else if (stat && stat.isFile()) {
295
+ kernFiles.push(inputPath);
296
+ }
297
+ else {
298
+ console.error(`Not found: ${compileInput}`);
299
+ process.exit(1);
300
+ }
301
+ if (kernFiles.length === 0) {
302
+ console.error(`No .kern files found in: ${compileInput}`);
303
+ process.exit(1);
304
+ }
305
+ // Load templates from config before compile
306
+ const compileConfig = loadConfig();
307
+ loadTemplates(compileConfig);
308
+ let compiled = 0;
309
+ for (const file of kernFiles) {
310
+ const source = readFileSync(file, 'utf-8');
311
+ const ast = parse(source);
312
+ const lines = [];
313
+ let hasReactNodes = false;
314
+ // Generate TypeScript for all core + React + template nodes (root + children)
315
+ function processNode(node) {
316
+ if (isCoreNode(node.type)) {
317
+ lines.push(...generateCoreNode(node));
318
+ lines.push('');
319
+ // hook generates React imports, so flag it
320
+ if (node.type === 'hook')
321
+ hasReactNodes = true;
322
+ }
323
+ else if (isTemplateNode(node.type)) {
324
+ lines.push(...expandTemplateNode(node));
325
+ lines.push('');
326
+ }
327
+ else if (isReactNode(node.type)) {
328
+ lines.push(...generateReactNode(node));
329
+ lines.push('');
330
+ hasReactNodes = true;
331
+ }
332
+ }
333
+ processNode(ast);
334
+ if (ast.children) {
335
+ for (const child of ast.children) {
336
+ processNode(child);
337
+ }
338
+ }
339
+ if (lines.length > 0) {
340
+ // Use .tsx for files with React nodes (JSX output), .ts otherwise
341
+ const ext = hasReactNodes ? '.tsx' : '.ts';
342
+ const outName = basename(file, '.kern') + ext;
343
+ const outFile = resolve(outDir, outName);
344
+ writeFileSync(outFile, lines.join('\n') + '\n');
345
+ console.log(` ${basename(file)} → ${outName}`);
346
+ compiled++;
347
+ }
348
+ else {
349
+ console.log(` ${basename(file)} → (no core nodes, skipped)`);
350
+ }
351
+ }
352
+ console.log(`\nCompiled ${compiled}/${kernFiles.length} files → ${outDir}`);
353
+ process.exit(0);
354
+ }
355
+ // ── kern scan [--force] [--dry-run] ─────────────────────────────────────
356
+ if (args[0] === 'scan') {
357
+ const scanCwd = process.cwd();
358
+ const force = args.includes('--force');
359
+ const dryRun = args.includes('--dry-run');
360
+ const result = scanProject(scanCwd);
361
+ console.log(formatScanSummary(result));
362
+ if (dryRun) {
363
+ console.log(' --dry-run: no files written.\n');
364
+ console.log(generateConfigSource(result));
365
+ process.exit(0);
366
+ }
367
+ const configOutPath = resolve(scanCwd, 'kern.config.ts');
368
+ if (existsSync(configOutPath) && !force) {
369
+ console.log(' kern.config.ts already exists. Use --force to overwrite.\n');
370
+ process.exit(0);
371
+ }
372
+ writeFileSync(configOutPath, generateConfigSource(result));
373
+ console.log(' Written: kern.config.ts\n');
374
+ process.exit(0);
375
+ }
376
+ // ── kern init-templates [--force] [--dry-run] ───────────────────────────
377
+ if (args[0] === 'init-templates') {
378
+ const force = args.includes('--force');
379
+ const dryRun = args.includes('--dry-run');
380
+ const initCwd = process.cwd();
381
+ const templatesDir = resolve(initCwd, 'templates');
382
+ // Find package.json
383
+ const pkgPath = findNearestPackageJson(initCwd);
384
+ if (!pkgPath) {
385
+ console.error('No package.json found. Run this in a project directory.');
386
+ process.exit(1);
387
+ }
388
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
389
+ const detected = detectTemplates(pkg);
390
+ console.log('\n KERN init-templates — scanning dependencies\n');
391
+ if (detected.length === 0 && !force) {
392
+ console.log(' No recognized libraries detected.');
393
+ console.log(' Common templates (arrow-fn, window-event) will still be created.\n');
394
+ }
395
+ // Collect all template files to write
396
+ const filesToWrite = { ...COMMON_TEMPLATES };
397
+ for (const entry of detected) {
398
+ console.log(` Detected: ${entry.libraryName} (${entry.packageName})`);
399
+ Object.assign(filesToWrite, entry.templates);
400
+ }
401
+ if (dryRun) {
402
+ console.log(`\n --dry-run: would create ${Object.keys(filesToWrite).length} template files in templates/\n`);
403
+ for (const name of Object.keys(filesToWrite).sort()) {
404
+ console.log(` templates/${name}`);
405
+ }
406
+ process.exit(0);
407
+ }
408
+ mkdirSync(templatesDir, { recursive: true });
409
+ let written = 0;
410
+ let skipped = 0;
411
+ for (const [name, content] of Object.entries(filesToWrite)) {
412
+ const outPath = resolve(templatesDir, name);
413
+ if (existsSync(outPath) && !force) {
414
+ console.log(` skip: templates/${name} (exists, use --force)`);
415
+ skipped++;
416
+ continue;
417
+ }
418
+ writeFileSync(outPath, content);
419
+ console.log(` wrote: templates/${name}`);
420
+ written++;
421
+ }
422
+ // Update kern.config.ts to include templates path
423
+ const configPath = resolve(initCwd, 'kern.config.ts');
424
+ if (existsSync(configPath)) {
425
+ const configContent = readFileSync(configPath, 'utf-8');
426
+ if (!configContent.includes('templates')) {
427
+ console.log('\n Note: Add templates to your kern.config.ts:');
428
+ console.log(" templates: ['./templates/'],\n");
429
+ }
430
+ }
431
+ else {
432
+ // Create a minimal kern.config.ts
433
+ const configSource = [
434
+ 'export default {',
435
+ " target: 'web',",
436
+ " templates: ['./templates/'],",
437
+ '};',
438
+ '',
439
+ ].join('\n');
440
+ writeFileSync(configPath, configSource);
441
+ console.log(' wrote: kern.config.ts');
442
+ written++;
443
+ }
444
+ console.log(`\n Done: ${written} written, ${skipped} skipped.`);
445
+ if (detected.length > 0) {
446
+ console.log(` Templates ready for: ${detected.map(d => d.libraryName).join(', ')}`);
447
+ }
448
+ console.log('');
449
+ process.exit(0);
450
+ }
451
+ // ── kern review <file|dir|--diff base> [--json] [--recursive] [--enforce] [--min-coverage=N] [--export-kern] [--llm] [--fix] [--lint] ──
452
+ if (args[0] === 'review') {
453
+ const jsonOutput = args.includes('--json');
454
+ const recursive = args.includes('--recursive') || args.includes('-r');
455
+ const enforce = args.includes('--enforce');
456
+ const exportKern = args.includes('--export-kern');
457
+ const llmMode = args.includes('--llm');
458
+ const fixMode = args.includes('--fix');
459
+ const lintMode = args.includes('--lint');
460
+ const minCoverageArg = args.find(a => a.startsWith('--min-coverage='))?.split('=')[1];
461
+ const minCoverage = minCoverageArg ? Number(minCoverageArg) : undefined;
462
+ const diffBase = args.find(a => a.startsWith('--diff'))
463
+ ? (args.find(a => a.startsWith('--diff='))?.split('=')[1] || args[args.indexOf('--diff') + 1] || 'origin/main')
464
+ : undefined;
465
+ // --diff mode: get changed files from git
466
+ const reviewInputs = args.filter(a => !a.startsWith('--') && a !== 'review');
467
+ let reviewInput = reviewInputs[0];
468
+ if (diffBase && !reviewInput) {
469
+ try {
470
+ const { execSync } = await import('child_process');
471
+ const diffFiles = execSync(`git diff --name-only --diff-filter=ACMR ${diffBase}`, { encoding: 'utf-8' })
472
+ .trim()
473
+ .split('\n')
474
+ .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
475
+ .filter(f => !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
476
+ if (diffFiles.length === 0) {
477
+ console.log(' No changed .ts/.tsx files since ' + diffBase);
478
+ process.exit(0);
479
+ }
480
+ console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
481
+ // Process each file individually below
482
+ reviewInput = '__diff__';
483
+ globalThis.__diffFiles = diffFiles;
484
+ }
485
+ catch (err) {
486
+ console.error(` git diff failed: ${err.message}`);
487
+ process.exit(1);
488
+ }
489
+ }
490
+ if (!reviewInput) {
491
+ console.error('Usage: kern review <file.ts|dir> [--diff base] [--json] [--recursive] [--enforce] [--min-coverage=N] [--export-kern] [--llm] [--fix]');
492
+ process.exit(1);
493
+ }
494
+ // Skip stat check for --diff mode (files are resolved individually below)
495
+ if (reviewInput !== '__diff__') {
496
+ const reviewPath = resolve(reviewInput);
497
+ const stat = existsSync(reviewPath) ? statSync(reviewPath) : null;
498
+ if (!stat) {
499
+ console.error(`Not found: ${reviewInput}`);
500
+ process.exit(1);
501
+ }
502
+ }
503
+ // Load kern.config.ts to get registered templates and target
504
+ const reviewCfg = loadConfig();
505
+ const reviewConfig = {
506
+ registeredTemplates: [],
507
+ minCoverage: minCoverage ?? 0,
508
+ enforceTemplates: enforce,
509
+ target: reviewCfg.target,
510
+ };
511
+ // Load templates and collect their names
512
+ if (reviewCfg.templates && reviewCfg.templates.length > 0) {
513
+ clearTemplates();
514
+ for (const templatePath of reviewCfg.templates) {
515
+ const resolvedTpl = resolve(process.cwd(), templatePath);
516
+ if (!existsSync(resolvedTpl))
517
+ continue;
518
+ const tplStat = statSync(resolvedTpl);
519
+ const tplFiles = [];
520
+ if (tplStat.isDirectory()) {
521
+ for (const entry of readdirSync(resolvedTpl)) {
522
+ if (entry.endsWith('.kern'))
523
+ tplFiles.push(resolve(resolvedTpl, entry));
524
+ }
525
+ }
526
+ else if (resolvedTpl.endsWith('.kern')) {
527
+ tplFiles.push(resolvedTpl);
528
+ }
529
+ for (const file of tplFiles) {
530
+ try {
531
+ const source = readFileSync(file, 'utf-8');
532
+ const ast = parse(source);
533
+ const nodes = ast.type === 'template' ? [ast] : (ast.children || []).filter(n => n.type === 'template');
534
+ for (const node of nodes) {
535
+ const tplName = node.props?.name;
536
+ if (tplName)
537
+ reviewConfig.registeredTemplates.push(tplName);
538
+ registerTemplate(node, file);
539
+ }
540
+ }
541
+ catch { }
542
+ }
543
+ }
544
+ if (reviewConfig.registeredTemplates.length > 0) {
545
+ console.log(` Templates loaded: ${reviewConfig.registeredTemplates.join(', ')}`);
546
+ }
547
+ }
548
+ // Collect reports from diff, directory, or single file
549
+ let reports = [];
550
+ if (reviewInput === '__diff__') {
551
+ const diffFiles = globalThis.__diffFiles;
552
+ for (const f of diffFiles) {
553
+ const fullPath = resolve(f);
554
+ if (existsSync(fullPath)) {
555
+ try {
556
+ reports.push(reviewFile(fullPath, reviewConfig));
557
+ }
558
+ catch { }
559
+ }
560
+ }
561
+ }
562
+ else {
563
+ // Support multiple positional paths: kern review file1.ts file2.ts dir/
564
+ const paths = reviewInputs.length > 0 ? reviewInputs : [reviewInput];
565
+ for (const p of paths) {
566
+ const rPath = resolve(p);
567
+ if (!existsSync(rPath))
568
+ continue;
569
+ const rStat = statSync(rPath);
570
+ if (rStat.isDirectory()) {
571
+ reports.push(...reviewDirectory(rPath, recursive, reviewConfig));
572
+ }
573
+ else {
574
+ try {
575
+ reports.push(reviewFile(rPath, reviewConfig));
576
+ }
577
+ catch { }
578
+ }
579
+ }
580
+ }
581
+ if (reports.length === 0) {
582
+ console.log(' No .ts/.tsx files found to review.');
583
+ process.exit(0);
584
+ }
585
+ // --export-kern: output KERN IR for AI review (v1 compat)
586
+ if (exportKern) {
587
+ for (const report of reports) {
588
+ console.log(`\n// ── ${report.filePath} ──`);
589
+ console.log(exportKernIR(report.inferred, report.templateMatches));
590
+ }
591
+ process.exit(0);
592
+ }
593
+ // --llm: output structured LLM prompt with nodeId aliases
594
+ if (llmMode) {
595
+ for (const report of reports) {
596
+ console.log(`\n// ── ${report.filePath} ──`);
597
+ console.log(buildLLMPrompt(report.inferred, report.templateMatches));
598
+ }
599
+ console.log('\n// Paste the JSON response from your AI to validate and map findings back to TS.');
600
+ process.exit(0);
601
+ }
602
+ // --fix: auto-migration — write .kern files from template suggestions, verify roundtrip
603
+ if (fixMode) {
604
+ let fixed = 0;
605
+ let verified = 0;
606
+ for (const report of reports) {
607
+ for (const t of report.templateMatches) {
608
+ if (!t.suggestedKern)
609
+ continue;
610
+ const kernFileName = report.filePath.replace(/\.tsx?$/, '.kern');
611
+ try {
612
+ writeFileSync(kernFileName, t.suggestedKern + '\n');
613
+ // Verify roundtrip: parse the written .kern file
614
+ try {
615
+ parse(readFileSync(kernFileName, 'utf-8'));
616
+ console.log(` ${report.filePath} → ${kernFileName} (verified)`);
617
+ verified++;
618
+ }
619
+ catch (parseErr) {
620
+ console.error(` ${kernFileName} written but parse failed: ${parseErr.message}`);
621
+ }
622
+ fixed++;
623
+ }
624
+ catch (err) {
625
+ console.error(` Failed to write ${kernFileName}: ${err.message}`);
626
+ }
627
+ }
628
+ }
629
+ if (fixed === 0) {
630
+ console.log(' No template suggestions to fix — nothing to migrate.');
631
+ }
632
+ else {
633
+ console.log(`\n ${fixed} .kern file(s) written, ${verified} verified.`);
634
+ }
635
+ process.exit(0);
636
+ }
637
+ // --lint: run ESLint + tsc diagnostics and merge into findings
638
+ if (lintMode) {
639
+ const filePaths = reports.map(r => r.filePath).filter(f => existsSync(f));
640
+ // ESLint pass
641
+ const eslintFindings = await runESLint(filePaths, process.cwd());
642
+ if (eslintFindings.length > 0) {
643
+ console.log(` ESLint: ${eslintFindings.length} findings`);
644
+ for (const report of reports) {
645
+ const fileFindings = eslintFindings.filter(f => f.primarySpan.file === report.filePath);
646
+ const linked = linkToNodes(fileFindings, report.inferred);
647
+ report.findings = dedup([...report.findings, ...linked]);
648
+ }
649
+ }
650
+ else {
651
+ console.log(' ESLint: no findings (or not installed)');
652
+ }
653
+ // tsc pass
654
+ const tscFindings = runTSCDiagnosticsFromPaths(filePaths);
655
+ if (tscFindings.length > 0) {
656
+ console.log(` tsc: ${tscFindings.length} findings`);
657
+ for (const report of reports) {
658
+ const fileFindings = tscFindings.filter(f => f.primarySpan.file === report.filePath);
659
+ const linked = linkToNodes(fileFindings, report.inferred);
660
+ report.findings = dedup([...report.findings, ...linked]);
661
+ }
662
+ }
663
+ else {
664
+ console.log(' tsc: no findings');
665
+ }
666
+ }
667
+ if (jsonOutput) {
668
+ console.log(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
669
+ }
670
+ else {
671
+ for (const report of reports) {
672
+ console.log('');
673
+ console.log(formatReport(report));
674
+ }
675
+ if (reports.length > 1) {
676
+ console.log('');
677
+ console.log(formatSummary(reports));
678
+ }
679
+ // Enforcement
680
+ if (enforce || minCoverage !== undefined) {
681
+ console.log('');
682
+ let allPassed = true;
683
+ for (const report of reports) {
684
+ const result = checkEnforcement(report, reviewConfig);
685
+ if (!result.passed) {
686
+ allPassed = false;
687
+ console.log(formatEnforcement(result));
688
+ }
689
+ }
690
+ if (allPassed) {
691
+ console.log(` Enforcement: PASS (all files)`);
692
+ }
693
+ else {
694
+ process.exit(1);
695
+ }
696
+ }
697
+ }
698
+ process.exit(0);
699
+ }
700
+ // ── Standard transpile mode ────────────────────────────────────────────
701
+ const inputFile = args.find(a => !a.startsWith('--'));
702
+ if (!inputFile) {
703
+ console.log('Usage: kern <file.kern> [--target=nextjs|tailwind|web|native|express|cli] [options]');
704
+ console.log('');
705
+ console.log('Commands:');
706
+ console.log(' dev <dir|file> [--target=...] [--outdir=...] Watch & hot-transpile .kern files');
707
+ console.log(' compile <dir|file> --outdir=<dir> Compile .kern → .ts (core nodes)');
708
+ console.log(' scan [--force] [--dry-run] Detect project → generate kern.config.ts');
709
+ console.log(' init-templates [--force] [--dry-run] Scan deps → scaffold template .kern files');
710
+ console.log(' review <file.ts|dir> [options] Analyze TS → infer .kern coverage + review');
711
+ console.log('');
712
+ console.log('Targets:');
713
+ console.log(' nextjs Next.js App Router (default)');
714
+ console.log(' tailwind React + Tailwind CSS');
715
+ console.log(' web React with inline styles');
716
+ console.log(' vue Vue 3 Single File Component');
717
+ console.log(' nuxt Nuxt 3 (pages, layouts, server routes)');
718
+ console.log(' native React Native component');
719
+ console.log(' express Express TypeScript backend');
720
+ console.log(' cli Commander.js CLI app');
721
+ console.log(' terminal ANSI terminal rendering');
722
+ console.log('');
723
+ console.log('Options:');
724
+ console.log(' --structure=flat|bulletproof|atomic|kern Output structure pattern (React targets)');
725
+ console.log(' --decompile Output human-readable pseudocode');
726
+ console.log(' --minify Output minified single-line Kern (LLM wire format)');
727
+ console.log(' --pretty Expand minified Kern back to indented format');
728
+ console.log(' --metrics Show language metrics (escape ratio, coverage, etc.)');
729
+ console.log('');
730
+ console.log('Structures (React targets only):');
731
+ console.log(' flat Single .tsx file (default)');
732
+ console.log(' bulletproof Feature-based folder structure');
733
+ console.log(' atomic Atomic Design hierarchy (pages/templates/organisms/molecules/atoms)');
734
+ console.log(' kern KERN-native (surfaces/blocks/signals/tokens/models)');
735
+ process.exit(1);
736
+ }
737
+ // ── Load config via jiti (supports .ts config at runtime) ────────────────
738
+ let config;
739
+ const configPath = resolve(process.cwd(), 'kern.config.ts');
740
+ if (existsSync(configPath)) {
741
+ try {
742
+ const jiti = createJiti(import.meta.url);
743
+ const mod = jiti(configPath);
744
+ const userConfig = mod.default ?? mod;
745
+ config = resolveConfig(userConfig);
746
+ }
747
+ catch (err) {
748
+ console.error(`Warning: Failed to load kern.config.ts: ${err.message}`);
749
+ config = resolveConfig({});
750
+ }
751
+ }
752
+ else {
753
+ config = resolveConfig({});
754
+ }
755
+ // Load templates before transpile
756
+ loadTemplates(config);
757
+ // CLI flags override config — target
758
+ const cliTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
759
+ if (cliTarget) {
760
+ if (!VALID_TARGETS.includes(cliTarget)) {
761
+ console.error(`Unknown target: '${cliTarget}'. Valid targets: ${VALID_TARGETS.join(', ')}`);
762
+ process.exit(1);
763
+ }
764
+ config = { ...config, target: cliTarget };
765
+ }
766
+ const target = config.target;
767
+ // CLI flags override config — structure
768
+ const cliStructure = args.find(a => a.startsWith('--structure='))?.split('=')[1];
769
+ if (cliStructure) {
770
+ if (!VALID_STRUCTURES.includes(cliStructure)) {
771
+ console.error(`Unknown structure: '${cliStructure}'. Valid structures: ${VALID_STRUCTURES.join(', ')}`);
772
+ process.exit(1);
773
+ }
774
+ config = { ...config, structure: cliStructure };
775
+ }
776
+ const irSource = readFileSync(resolve(inputFile), 'utf-8');
777
+ const ast = parse(irSource);
778
+ const ext = inputFile.endsWith('.kern') ? '.kern' : '.ir';
779
+ const name = basename(inputFile, ext);
780
+ // ── Minify: indented Kern → single-line wire format ─────────────────────
781
+ if (args.includes('--minify')) {
782
+ const minified = minifyKern(ast);
783
+ const outFile = resolve(dirname(inputFile), `${name}.min.kern`);
784
+ writeFileSync(outFile, minified);
785
+ const savings = Math.round((1 - minified.length / irSource.length) * 100);
786
+ console.log(`Minified: ${inputFile} → ${outFile}`);
787
+ console.log(`Chars: ${irSource.length} → ${minified.length} (${savings}% smaller)`);
788
+ process.exit(0);
789
+ }
790
+ // ── Pretty: re-indent (useful after minify or messy edits) ──────────────
791
+ if (args.includes('--pretty')) {
792
+ const pretty = prettyKern(ast);
793
+ const outFile = resolve(dirname(inputFile), `${name}.kern`);
794
+ writeFileSync(outFile, pretty);
795
+ console.log(`Formatted: ${inputFile} → ${outFile}`);
796
+ process.exit(0);
797
+ }
798
+ // ── Decompile: Kern → human-readable pseudocode ─────────────────────────
799
+ if (args.includes('--decompile')) {
800
+ const result = decompile(ast);
801
+ console.log(result.code);
802
+ process.exit(0);
803
+ }
804
+ // ── Metrics: analyze language coverage ────────────────────────────────────
805
+ if (args.includes('--metrics')) {
806
+ const metrics = collectLanguageMetrics(ast);
807
+ console.log(`Metrics: ${inputFile}`);
808
+ console.log(` Nodes: ${metrics.nodeCount} (${metrics.nodeTypes.length} types)`);
809
+ console.log(` Styles: ${metrics.styleMetrics.totalStyleDecls} declarations`);
810
+ console.log(` Mapped: ${metrics.styleMetrics.mappedStyleDecls} (${Math.round((1 - metrics.styleMetrics.escapeRatio) * 100)}%)`);
811
+ console.log(` Escaped: ${metrics.styleMetrics.escapedStyleDecls} (${Math.round(metrics.styleMetrics.escapeRatio * 100)}%)`);
812
+ if (metrics.styleMetrics.escapedKeys.length > 0) {
813
+ console.log(` Escape keys: ${metrics.styleMetrics.escapedKeys.join(', ')}`);
814
+ }
815
+ console.log(` Shorthand: ${Math.round(metrics.shorthandCoverage * 100)}% coverage`);
816
+ console.log(` Theme refs: ${metrics.themeRefCount}`);
817
+ console.log(` Pseudo: ${metrics.pseudoStyleCount}`);
818
+ if (metrics.unknownNodeCount > 0) {
819
+ console.log(` Unknown nodes: ${metrics.unknownNodeCount}`);
820
+ }
821
+ console.log('');
822
+ console.log(' Node types:');
823
+ for (const nt of metrics.nodeTypes.slice(0, 10)) {
824
+ console.log(` ${nt.type}: ${nt.count} (${nt.styleDecls} styles)`);
825
+ }
826
+ process.exit(0);
827
+ }
828
+ // ── Transpile: Kern → target code ───────────────────────────────────────
829
+ const result = target === 'native'
830
+ ? transpile(ast, config)
831
+ : target === 'web'
832
+ ? transpileWeb(ast, config)
833
+ : target === 'tailwind'
834
+ ? transpileTailwind(ast, config)
835
+ : target === 'express'
836
+ ? transpileExpress(ast, config)
837
+ : target === 'cli'
838
+ ? transpileCliApp(ast, config)
839
+ : target === 'terminal'
840
+ ? transpileTerminal(ast, config)
841
+ : target === 'vue'
842
+ ? transpileVue(ast, config)
843
+ : target === 'nuxt'
844
+ ? transpileNuxt(ast, config)
845
+ : transpileNextjs(ast, config);
846
+ const outDir = resolve(dirname(inputFile), config.output.outDir);
847
+ const isStructured = config.structure !== 'flat' && result.artifacts && result.artifacts.length > 0;
848
+ if (isStructured) {
849
+ // Structured output: write all artifacts, entry code comes from artifacts
850
+ for (const artifact of result.artifacts) {
851
+ const artifactPath = resolve(outDir, artifact.path);
852
+ mkdirSync(dirname(artifactPath), { recursive: true });
853
+ writeFileSync(artifactPath, artifact.content);
854
+ }
855
+ // Find entry file path for display
856
+ const entryArtifact = result.artifacts.find(a => a.type === 'entry' || a.type === 'page');
857
+ const displayPath = entryArtifact ? resolve(outDir, entryArtifact.path) : resolve(outDir, `${name}.tsx`);
858
+ console.log(`Transpiled: ${inputFile} → ${displayPath}`);
859
+ }
860
+ else {
861
+ // Flat output: single file
862
+ const outExt = (target === 'vue' || target === 'nuxt') ? '.vue'
863
+ : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts' : '.tsx';
864
+ const outFile = resolve(outDir, `${name}${outExt}`);
865
+ mkdirSync(dirname(outFile), { recursive: true });
866
+ writeFileSync(outFile, result.code);
867
+ if (result.artifacts) {
868
+ for (const artifact of result.artifacts) {
869
+ const artifactPath = resolve(outDir, artifact.path);
870
+ mkdirSync(dirname(artifactPath), { recursive: true });
871
+ writeFileSync(artifactPath, artifact.content);
872
+ }
873
+ }
874
+ console.log(`Transpiled: ${inputFile} → ${outFile}`);
875
+ }
876
+ const targetNames = { native: 'React Native', web: 'React (inline)', tailwind: 'React + Tailwind', nextjs: 'Next.js App Router', express: 'Express TypeScript', cli: 'Commander.js CLI', terminal: 'ANSI Terminal', vue: 'Vue 3 SFC', nuxt: 'Nuxt 3' };
877
+ console.log(`Target: ${targetNames[target] || target}`);
878
+ if (config.structure !== 'flat') {
879
+ const structureNames = { bulletproof: 'Bulletproof React', atomic: 'Atomic Design', kern: 'KERN Native' };
880
+ console.log(`Structure: ${structureNames[config.structure] || config.structure}`);
881
+ }
882
+ console.log(`IR tokens: ${result.irTokenCount}`);
883
+ console.log(`TS tokens: ${result.tsTokenCount}`);
884
+ console.log(`Reduction: ${result.tokenReduction}%`);
885
+ console.log(`Source map: ${result.sourceMap.length} entries`);
886
+ if (result.artifacts) {
887
+ console.log(`Artifacts: ${result.artifacts.length}`);
888
+ }
889
+ // ── Minify/Pretty implementations ───────────────────────────────────────
890
+ function minifyKern(node) {
891
+ const type = node.type;
892
+ const props = node.props || {};
893
+ let head = type;
894
+ // Serialize props (theme name is bare word, not key=value)
895
+ for (const [k, v] of Object.entries(props)) {
896
+ if (['styles', 'pseudoStyles', 'themeRefs'].includes(k))
897
+ continue;
898
+ if (type === 'theme' && k === 'name') {
899
+ head += ` ${v}`;
900
+ continue;
901
+ }
902
+ if (typeof v === 'object' && v !== null && '__expr' in v) {
903
+ head += ` ${k}={{ ${v.code} }}`;
904
+ continue;
905
+ }
906
+ const val = typeof v === 'string' && v.includes(' ') ? `"${v}"` : String(v);
907
+ head += ` ${k}=${val}`;
908
+ }
909
+ // Serialize styles
910
+ if (props.styles) {
911
+ const pairs = Object.entries(props.styles)
912
+ .map(([k, v]) => v.includes(' ') || v.includes(',') ? `"${k}":"${v}"` : `${k}:${v}`);
913
+ head += ` {${pairs.join(',')}}`;
914
+ }
915
+ // Serialize pseudo styles
916
+ if (props.pseudoStyles) {
917
+ const pseudo = props.pseudoStyles;
918
+ for (const [state, styles] of Object.entries(pseudo)) {
919
+ for (const [k, v] of Object.entries(styles)) {
920
+ head += ` {:${state}:${k}:${v}}`;
921
+ }
922
+ }
923
+ }
924
+ // Theme refs
925
+ if (props.themeRefs) {
926
+ for (const ref of props.themeRefs) {
927
+ head += ` $${ref}`;
928
+ }
929
+ }
930
+ // Children → S-expression style
931
+ if (node.children && node.children.length > 0) {
932
+ const kids = node.children.map(c => minifyKern(c)).join(',');
933
+ return `${head}(${kids})`;
934
+ }
935
+ return head;
936
+ }
937
+ function prettyKern(node, indent = '') {
938
+ const type = node.type;
939
+ const props = node.props || {};
940
+ let line = `${indent}${type}`;
941
+ for (const [k, v] of Object.entries(props)) {
942
+ if (['styles', 'pseudoStyles', 'themeRefs'].includes(k))
943
+ continue;
944
+ if (type === 'theme' && k === 'name') {
945
+ line += ` ${v}`;
946
+ continue;
947
+ }
948
+ if (typeof v === 'object' && v !== null && '__expr' in v) {
949
+ line += ` ${k}={{ ${v.code} }}`;
950
+ continue;
951
+ }
952
+ const val = typeof v === 'string' && v.includes(' ') ? `"${v}"` : String(v);
953
+ line += ` ${k}=${val}`;
954
+ }
955
+ if (props.styles) {
956
+ const pairs = Object.entries(props.styles)
957
+ .map(([k, v]) => v.includes(' ') || v.includes(',') ? `"${k}":"${v}"` : `${k}:${v}`);
958
+ line += ` {${pairs.join(',')}}`;
959
+ }
960
+ if (props.pseudoStyles) {
961
+ const pseudo = props.pseudoStyles;
962
+ for (const [state, styles] of Object.entries(pseudo)) {
963
+ for (const [k, v] of Object.entries(styles)) {
964
+ line += `,${`:${state}:${k}:${v}`}`;
965
+ }
966
+ }
967
+ }
968
+ if (props.themeRefs) {
969
+ for (const ref of props.themeRefs) {
970
+ line += ` $${ref}`;
971
+ }
972
+ }
973
+ let result = line + '\n';
974
+ if (node.children) {
975
+ for (const child of node.children) {
976
+ result += prettyKern(child, indent + ' ');
977
+ }
978
+ }
979
+ return result;
980
+ }
981
+ //# sourceMappingURL=cli.js.map