@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/LICENSE +661 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +981 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/transpiler-cli.d.ts +2 -0
- package/dist/transpiler-cli.js +279 -0
- package/dist/transpiler-cli.js.map +1 -0
- package/package.json +35 -0
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
|