@xnoxs/flux-lang 3.2.1 → 3.2.2

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/bin/flux.js CHANGED
@@ -1,1397 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- // Self-hosted compiler: enable with --self-hosted flag or FLUX_SELF_HOSTED=1
7
- const USE_SELF_HOSTED = process.argv.includes('--self-hosted') || process.env.FLUX_SELF_HOSTED === '1';
8
- process.argv = process.argv.filter(a => a !== '--self-hosted');
9
-
10
- function loadTranspiler() {
11
- if (USE_SELF_HOSTED) {
12
- const selfPath = require('path').join(__dirname, '../src/self/transpiler.js');
13
- try {
14
- const mod = require(selfPath);
15
- if (process.env.FLUX_VERBOSE) process.stderr.write('[flux] using self-hosted transpiler\n');
16
- return mod;
17
- } catch (e) {
18
- process.stderr.write('[flux] warning: self-hosted transpiler not available, falling back to stage-0\n');
19
- process.stderr.write('[flux] run: node scripts/bootstrap.js to build the self-hosted compiler\n');
20
- }
21
- }
22
- return require('../src/transpiler');
23
- }
24
-
25
- function loadBundler() {
26
- if (USE_SELF_HOSTED) {
27
- const selfPath = require('path').join(__dirname, '../src/self/bundler.js');
28
- try { return require(selfPath); } catch (_) {}
29
- }
30
- return require('../src/bundler');
31
- }
32
-
33
- const { transpile } = loadTranspiler();
34
- const { bundle } = loadBundler();
35
-
36
- const VERSION = require('../package.json').version;
37
-
38
- // ── ANSI Colors ──────────────────────────────────────────────────────────────
39
- const C = {
40
- reset: '\x1b[0m',
41
- bold: '\x1b[1m',
42
- dim: '\x1b[2m',
43
- red: '\x1b[31m',
44
- green: '\x1b[32m',
45
- yellow: '\x1b[33m',
46
- blue: '\x1b[34m',
47
- cyan: '\x1b[36m',
48
- white: '\x1b[37m',
49
- gray: '\x1b[90m',
50
- };
51
- const colored = (c, s) => `${c}${s}${C.reset}`;
52
- const noColor = process.env.NO_COLOR || !process.stdout.isTTY;
53
- const clr = (c, s) => noColor ? s : colored(c, s);
54
-
55
- // ── Banner ───────────────────────────────────────────────────────────────────
56
- function banner() {
57
- console.log(clr(C.cyan, C.bold + `
58
- ███████╗██╗ ██╗ ██╗██╗ ██╗
59
- ██╔════╝██║ ██║ ██║╚██╗██╔╝
60
- █████╗ ██║ ██║ ██║ ╚███╔╝
61
- ██╔══╝ ██║ ██║ ██║ ██╔██╗
62
- ██║ ███████╗╚██████╔╝██╔╝ ██╗
63
- ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝` + C.reset));
64
- console.log(clr(C.gray, ` Flux Lang Transpiler → JavaScript v${VERSION}\n`));
65
- }
66
-
67
- // ── Help ─────────────────────────────────────────────────────────────────────
68
- function showHelp() {
69
- banner();
70
- console.log(clr(C.bold, 'USAGE:'));
71
- console.log(' flux <command> [options]\n');
72
- console.log(clr(C.bold, 'COMMANDS:'));
73
- const cmds = [
74
- ['init [name]', 'Scaffold a new Flux project'],
75
- ['compile <file.flux>', 'Compile to a .js file'],
76
- ['bundle <entry.flux>', 'Bundle multiple .flux files into one .js'],
77
- ['run <file.flux>', 'Compile and run immediately'],
78
- ['watch <file.flux>', 'Watch for changes and auto-compile'],
79
- ['check <file.flux>', 'Type-check and static analysis'],
80
- ['lint <file.flux>', 'Full lint: types + style + immutability'],
81
- ['fmt <file.flux>', 'Format source code in-place'],
82
- ['test [dir]', 'Discover and run *.test.flux files'],
83
- ['publish [--patch|minor|major]', 'Bump version, update CHANGELOG, tag & release'],
84
- ['tokens <file.flux>', 'Show lexer token list'],
85
- ['ast <file.flux>', 'Show Abstract Syntax Tree (JSON)'],
86
- ['repl', 'Interactive REPL mode'],
87
- ['self-hosted [build|verify|on|off]','Self-hosted compiler: status, build & toggle'],
88
- ['version', 'Show version'],
89
- ['help', 'Show this help'],
90
- ];
91
- for (const [cmd, desc] of cmds) {
92
- console.log(` ${clr(C.green, ('flux ' + cmd).padEnd(38))} ${clr(C.gray, desc)}`);
93
- }
94
- console.log();
95
- console.log(clr(C.bold, 'OPTIONS:'));
96
- console.log(` ${clr(C.yellow, '--out, -o <file>')} Output file name`);
97
- console.log(` ${clr(C.yellow, '--sourcemap, -m')} Generate source map (.js.map)`);
98
- console.log(` ${clr(C.yellow, '--stdout')} Print output to terminal`);
99
- console.log(` ${clr(C.yellow, '--no-color')} Disable colors`);
100
- console.log();
101
- console.log(clr(C.bold, 'EXAMPLES:'));
102
- console.log(clr(C.gray, ' flux init my-app'));
103
- console.log(clr(C.gray, ' flux compile app.flux'));
104
- console.log(clr(C.gray, ' flux compile app.flux -o dist/app.js'));
105
- console.log(clr(C.gray, ' flux bundle src/main.flux -o bundle.js'));
106
- console.log(clr(C.gray, ' flux run app.flux'));
107
- console.log(clr(C.gray, ' flux watch app.flux'));
108
- console.log(clr(C.gray, ' flux repl'));
109
- console.log();
110
- console.log(clr(C.bold, "WHAT'S NEW in v3.0.0:"));
111
- const news = [
112
- 'Return type annotations — fn greet(name: String) -> String:',
113
- 'Union types — val x: Int | String | Null',
114
- 'Nullable shorthand — String? = String | Null',
115
- 'Full type checker — flux check catches type mismatches at compile time',
116
- 'Interface enforcement — implements checks all required members',
117
- 'Generic interfaces — interface Container<T>: fn get() -> T',
118
- 'type / ADT — type Result = Ok(value) | Err(msg)',
119
- 'when Pattern(x) — ADT pattern matching with field bindings',
120
- 'when Pattern if guard — pattern match guards',
121
- 'String format specs — "Pi is {pi:.2f}" "{n:,}" "{rate:.1%}"',
122
- 'async / await — async functions and promise handling',
123
- 'try / catch / finally — structured error handling',
124
- 'spread / rest (...) — spread arrays/objects, rest params',
125
- 'optional chaining (?.) — safe property access',
126
- 'nullish coalescing (??) — default value for null/undefined',
127
- 'destructuring — val { a, b } = obj / val [x, y] = arr',
128
- ];
129
- for (const n of news)
130
- console.log(' ' + clr(C.cyan, '⊕') + ' ' + clr(C.gray, n));
131
- console.log();
132
- }
133
-
134
- // ── Helpers ──────────────────────────────────────────────────────────────────
135
- function readFluxFile(filePath) {
136
- const abs = path.resolve(filePath);
137
- if (!fs.existsSync(abs)) {
138
- console.error(clr(C.red, `[Error] File not found: ${abs}`));
139
- process.exit(1);
140
- }
141
- if (!filePath.endsWith('.flux')) {
142
- console.warn(clr(C.yellow, `[Warning] File is not .flux: ${filePath}`));
143
- }
144
- return { source: fs.readFileSync(abs, 'utf8'), abs };
145
- }
146
-
147
- // ── Error label → friendly prefix ────────────────────────────────────────────
148
- const ERROR_KIND = {
149
- ParseError: 'Syntax error',
150
- LexerError: 'Syntax error',
151
- CheckError: 'Static error',
152
- TypeCheckError: 'Type error',
153
- TypeError: 'Type error',
154
- };
155
-
156
- // ── Central error renderer ────────────────────────────────────────────────────
157
- // Accepts an array of error objects with optional:
158
- // { message, name, line, col, len, hint, stage }
159
- // Also accepts a file path for the header label.
160
- function printErrors(errors, source, filePath) {
161
- const lines = source.split('\n');
162
-
163
- for (const err of errors) {
164
- const kind = ERROR_KIND[err.name] || 'Error';
165
- const stage = err.stage ? clr(C.gray, ` [${err.stage}]`) : '';
166
-
167
- // ── header ──────────────────────────────────────────────────────────────
168
- console.error();
169
- console.error(clr(C.red, C.bold + `${kind}` + C.reset) + stage);
170
-
171
- // ── location ────────────────────────────────────────────────────────────
172
- if (err.line) {
173
- const fileLabel = filePath ? clr(C.cyan, path.relative(process.cwd(), filePath)) : '';
174
- const locLabel = clr(C.yellow, `${err.line}:${err.col || 1}`);
175
- if (fileLabel) console.error(` ${fileLabel}:${locLabel}`);
176
- else console.error(` Line ${locLabel}`);
177
- }
178
-
179
- // ── message ─────────────────────────────────────────────────────────────
180
- console.error(` ${err.message}`);
181
-
182
- // ── source context (1 line before + error line + 1 line after) ──────────
183
- if (err.line && err.line <= lines.length) {
184
- const errLineIdx = err.line - 1;
185
- const col = Math.max(0, (err.col || 1) - 1);
186
- const tokLen = Math.max(1, err.len || 1);
187
-
188
- // line before (context)
189
- if (errLineIdx > 0 && lines[errLineIdx - 1].trim() !== '') {
190
- const prev = String(err.line - 1).padStart(4);
191
- console.error(clr(C.gray, ` ${prev} │ ${lines[errLineIdx - 1]}`));
192
- }
193
-
194
- // error line (highlighted)
195
- const lineNum = String(err.line).padStart(4);
196
- console.error(clr(C.gray, ` ${lineNum} │ `) + lines[errLineIdx]);
197
-
198
- // squiggly pointer (^~~~)
199
- const squiggle = '^' + '~'.repeat(Math.max(0, tokLen - 1));
200
- const pointer = ' '.repeat(col) + clr(C.red, squiggle);
201
- console.error(clr(C.gray, ` │ `) + pointer);
202
-
203
- // line after (context)
204
- if (errLineIdx + 1 < lines.length && lines[errLineIdx + 1].trim() !== '') {
205
- const next = String(err.line + 1).padStart(4);
206
- console.error(clr(C.gray, ` ${next} │ ${lines[errLineIdx + 1]}`));
207
- }
208
- }
209
-
210
- // ── hint ────────────────────────────────────────────────────────────────
211
- if (err.hint) {
212
- console.error(clr(C.cyan, ` Hint: `) + clr(C.gray, err.hint));
213
- }
214
- }
215
-
216
- console.error();
217
- }
218
-
219
- function deriveOutPath(inputPath, outFlag) {
220
- if (outFlag) return path.resolve(outFlag);
221
- const base = path.basename(inputPath, '.flux');
222
- return path.join(path.dirname(path.resolve(inputPath)), base + '.js');
223
- }
224
-
225
- // ── flux init ────────────────────────────────────────────────────────────────
226
- function cmdInit(name) {
227
- const projectName = name || 'my-flux-app';
228
- const dir = path.resolve(projectName);
229
-
230
- if (fs.existsSync(dir)) {
231
- console.error(clr(C.red, `[Error] Directory already exists: ${projectName}`));
232
- process.exit(1);
233
- }
234
-
235
- fs.mkdirSync(dir, { recursive: true });
236
- fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
237
-
238
- // main.flux
239
- fs.writeFileSync(path.join(dir, 'src', 'main.flux'), [
240
- '// Welcome to Flux Lang v2.0!',
241
- '',
242
- '// --- Basic ---',
243
- 'val greeting = "Hello, Flux!"',
244
- 'print(greeting)',
245
- '',
246
- '// --- Async / Await ---',
247
- 'async fn fetchData(url):',
248
- ' try:',
249
- ' val res = await fetch(url)',
250
- ' val data = await res.json()',
251
- ' return data',
252
- ' catch(e):',
253
- ' print("Fetch error:", e.message)',
254
- ' return null',
255
- '',
256
- '// --- Destructuring ---',
257
- 'val person = { name: "Budi", age: 25, city: "Jakarta" }',
258
- 'val { name, age } = person',
259
- 'print("Name: {name}, Age: {age}")',
260
- '',
261
- '// --- Optional chaining & nullish coalescing ---',
262
- 'val user = null',
263
- 'val displayName = user?.name ?? "Anonymous"',
264
- 'print("User:", displayName)',
265
- '',
266
- '// --- Spread ---',
267
- 'val nums = [1, 2, 3]',
268
- 'val more = [...nums, 4, 5, 6]',
269
- 'print("Numbers:", more)',
270
- '',
271
- '// --- Rest params ---',
272
- 'fn sum(...args):',
273
- ' return args.reduce((acc, x) -> acc + x, 0)',
274
- '',
275
- 'print("Sum:", sum(1, 2, 3, 4, 5))',
276
- '',
277
- '// --- Default params ---',
278
- 'fn greet(name = "World") -> "Hello, {name}!"',
279
- 'print(greet())',
280
- 'print(greet("Flux"))',
281
- '',
282
- '// --- Error handling ---',
283
- 'fn divide(a, b):',
284
- ' if b == 0:',
285
- ' throw new Error("Division by zero")',
286
- ' return a / b',
287
- '',
288
- 'try:',
289
- ' print(divide(10, 2))',
290
- ' print(divide(10, 0))',
291
- 'catch(e):',
292
- ' print("Error:", e.message)',
293
- 'finally:',
294
- ' print("Done.")',
295
- '',
296
- ].join('\n'), 'utf8');
297
-
298
- // package.json
299
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
300
- name: projectName,
301
- version: '1.0.0',
302
- description: `A Flux Lang project`,
303
- scripts: {
304
- start: `flux run src/main.flux`,
305
- build: `flux bundle src/main.flux -o dist/bundle.js`,
306
- dev: `flux watch src/main.flux`,
307
- check: `flux check src/main.flux`,
308
- },
309
- dependencies: {},
310
- devDependencies: { 'flux-lang': `^${VERSION}` },
311
- }, null, 2), 'utf8');
312
-
313
- // .gitignore
314
- fs.writeFileSync(path.join(dir, '.gitignore'), [
315
- 'node_modules/',
316
- 'dist/',
317
- '*.js.map',
318
- '.DS_Store',
319
- ].join('\n'), 'utf8');
320
-
321
- // README.md
322
- fs.writeFileSync(path.join(dir, 'README.md'), [
323
- `# ${projectName}`,
324
- '',
325
- 'A project built with [Flux Lang](https://github.com/flux-lang/flux-lang) v2.',
326
- '',
327
- '## Getting Started',
328
- '',
329
- '```bash',
330
- 'npm install',
331
- 'flux run src/main.flux',
332
- '```',
333
- '',
334
- '## Commands',
335
- '',
336
- '| Command | Description |',
337
- '|---|---|',
338
- '| `npm start` | Run main.flux |',
339
- '| `npm run build` | Bundle to dist/ |',
340
- '| `npm run dev` | Watch mode |',
341
- '| `npm run check` | Check syntax |',
342
- '',
343
- ].join('\n'), 'utf8');
344
-
345
- console.log();
346
- console.log(clr(C.green, `✓ Project created: ${projectName}/`));
347
- console.log();
348
- console.log(clr(C.gray, ' Files created:'));
349
- console.log(' ' + clr(C.cyan, `${projectName}/src/main.flux`));
350
- console.log(' ' + clr(C.cyan, `${projectName}/package.json`));
351
- console.log(' ' + clr(C.cyan, `${projectName}/.gitignore`));
352
- console.log(' ' + clr(C.cyan, `${projectName}/README.md`));
353
- console.log();
354
- console.log(clr(C.bold, ' Next steps:'));
355
- console.log(` ${clr(C.yellow, `cd ${projectName}`)}`);
356
- console.log(` ${clr(C.yellow, 'npm install')}`);
357
- console.log(` ${clr(C.yellow, 'flux run src/main.flux')}`);
358
- console.log();
359
- }
360
-
361
- // ── Commands ─────────────────────────────────────────────────────────────────
362
-
363
- // flux compile <file.flux> [-o out.js] [--stdout] [--sourcemap]
364
- function cmdCompile(filePath, opts) {
365
- const { source, abs } = readFluxFile(filePath);
366
- const outPath = deriveOutPath(filePath, opts.out);
367
- const mapPath = outPath + '.map';
368
- const t0 = Date.now();
369
-
370
- const result = transpile(source, {
371
- sourcemap: opts.sourcemap,
372
- mangle: opts.mangle,
373
- sourceFile: path.relative(path.dirname(outPath), abs),
374
- outputFile: path.basename(outPath),
375
- });
376
-
377
- if (!result.success) {
378
- console.error(clr(C.red, `\n✗ Compile failed — ${result.errors.length} error(s)`));
379
- printErrors(result.errors, source, abs);
380
- process.exit(1);
381
- }
382
-
383
- const elapsed = Date.now() - t0;
384
-
385
- if (opts.stdout) {
386
- console.log(result.output);
387
- return;
388
- }
389
-
390
- fs.writeFileSync(outPath, result.output, 'utf8');
391
-
392
- let extra = '';
393
- if (opts.sourcemap && result.sourceMap) {
394
- fs.writeFileSync(mapPath, result.sourceMap, 'utf8');
395
- extra = clr(C.gray, ` + ${path.relative(process.cwd(), mapPath)}`);
396
- }
397
-
398
- console.log(
399
- clr(C.green, `✓ Done`) + clr(C.gray, ` (${elapsed}ms)`) +
400
- ` ${clr(C.blue, path.basename(abs))} → ${clr(C.cyan, path.relative(process.cwd(), outPath))}${extra}`
401
- );
402
- }
403
-
404
- // flux bundle <entry.flux> [-o output.js] [--stdout]
405
- function cmdBundle(entryPath, opts) {
406
- const abs = path.resolve(entryPath);
407
- if (!fs.existsSync(abs)) {
408
- console.error(clr(C.red, `✗ File not found: ${entryPath}`));
409
- process.exit(1);
410
- }
411
-
412
- const outFile = opts.out
413
- || path.join(path.dirname(abs), path.basename(abs, '.flux') + '.bundle.js');
414
-
415
- const t0 = Date.now();
416
- const result = bundle(abs);
417
-
418
- if (!result.success) {
419
- console.error(clr(C.red, '\n✗ Bundle failed:\n'));
420
- for (const e of result.errors) {
421
- console.error(clr(C.red, ` ${e.message}`));
422
- }
423
- process.exit(1);
424
- }
425
-
426
- const elapsed = Date.now() - t0;
427
-
428
- if (opts.stdout) {
429
- console.log(result.code);
430
- return;
431
- }
432
-
433
- fs.writeFileSync(outFile, result.code, 'utf8');
434
- const kb = (result.code.length / 1024).toFixed(1);
435
- console.log(
436
- clr(C.green, `✓ Bundle done`) +
437
- clr(C.gray, ` (${elapsed}ms)`) +
438
- ` ${path.basename(abs)} + ${result.modules - 1} module(s) → ` +
439
- clr(C.cyan, path.relative(process.cwd(), outFile)) +
440
- clr(C.gray, ` [${kb} KB]`)
441
- );
442
- }
443
-
444
- // flux run <file.flux>
445
- function cmdRun(filePath) {
446
- const { source, abs } = readFluxFile(filePath);
447
- const result = transpile(source);
448
-
449
- if (!result.success) {
450
- console.error(clr(C.red, `\n✗ Compile error`));
451
- printErrors(result.errors, source, abs);
452
- process.exit(1);
453
- }
454
-
455
- const tmpPath = path.join(require('os').tmpdir(), '_flux_run_' + Date.now() + '.js');
456
- fs.writeFileSync(tmpPath, result.output, 'utf8');
457
-
458
- console.log(clr(C.gray, `▶ Running ${path.basename(abs)} ...\n`));
459
- try {
460
- require(tmpPath);
461
- } catch (e) {
462
- console.error(clr(C.red, `\n[Runtime Error] ${e.message}`));
463
- process.exitCode = 1;
464
- } finally {
465
- try { fs.unlinkSync(tmpPath); } catch (_) {}
466
- }
467
- }
468
-
469
- // flux check <file.flux> — full type check + val-immutability analysis
470
- function cmdCheck(filePath) {
471
- const { source, abs } = readFluxFile(filePath);
472
- const baseName = path.basename(abs);
473
-
474
- // Run transpile with both immutability + type checker
475
- const result = transpile(source, { check: true, typecheck: true });
476
-
477
- // Parse / compile errors (includes immutability CheckErrors when check:true)
478
- if (!result.success) {
479
- const n = result.errors.length;
480
- const kind = result.errors.some(e => e.name === 'CheckError') ? 'static' : 'syntax';
481
- console.error(clr(C.red, `\n✗ ${baseName}: ${n} ${kind} error${n > 1 ? 's' : ''}`));
482
- printErrors(result.errors, source, abs);
483
- process.exit(1);
484
- }
485
-
486
- let exitCode = 0;
487
-
488
- // Type errors — use unified printErrors renderer
489
- const typeErrors = result.typeErrors || [];
490
- if (typeErrors.length > 0) {
491
- console.error(clr(C.red, `\n✗ ${baseName}: ${typeErrors.length} type error(s)`));
492
- printErrors(typeErrors, source, abs);
493
- exitCode = 1;
494
- }
495
-
496
- // All warnings
497
- const warnings = [...(result.typeWarnings || [])];
498
- for (const w of warnings) {
499
- const fileRef = clr(C.cyan, baseName) + (w.line ? clr(C.yellow, `:${w.line}`) : '');
500
- console.warn(clr(C.yellow, ` ⚠ `) + fileRef + clr(C.gray, ` ${w.message}`));
501
- if (w.hint) console.warn(clr(C.cyan, ` Hint: `) + clr(C.gray, w.hint));
502
- }
503
-
504
- // Summary
505
- const fnRe = /^(?:async )?function \w/gm;
506
- const clsRe = /^class \w/gm;
507
- const fns = (result.output.match(fnRe) || []).length;
508
- const cls = (result.output.match(clsRe) || []).length;
509
- const lines = result.output.split('\n').length;
510
- const tw = typeErrors.length;
511
- const ww = warnings.length;
512
-
513
- console.log();
514
- if (exitCode === 0) {
515
- console.log(
516
- clr(C.green, `✓ ${baseName} — no errors`) +
517
- (ww > 0 ? clr(C.yellow, ` (${ww} warning${ww>1?'s':''})`) : '')
518
- );
519
- } else {
520
- console.log(clr(C.red, `✗ ${baseName} — ${tw} type error${tw>1?'s':''}`));
521
- }
522
- console.log(clr(C.gray, ` Functions: ${fns} | Classes: ${cls} | JS output: ${lines} lines`));
523
- console.log();
524
-
525
- if (exitCode !== 0) process.exit(exitCode);
526
- }
527
-
528
- // flux lint <file.flux> — comprehensive lint: types + style + AST rules
529
- function cmdLint(filePath) {
530
- const { source, abs } = readFluxFile(filePath);
531
- const baseName = path.basename(abs);
532
- const { format } = require('../src/formatter');
533
- const { lint } = require('../src/linter');
534
- const { detectUsedSymbols } = require('../src/stdlib');
535
-
536
- console.log(clr(C.cyan, `\n⊛ Linting: ${baseName}\n`));
537
-
538
- let exitCode = 0;
539
- let totalIssues = 0;
540
-
541
- // ── 1. Syntax check ───────────────────────────────────────────────────────
542
- const result = transpile(source, { check: true, typecheck: true });
543
-
544
- if (!result.success) {
545
- console.error(clr(C.red, ` Syntax error${result.errors.length > 1 ? 's' : ''} (${result.errors.length})`));
546
- printErrors(result.errors, source, abs);
547
- process.exit(1);
548
- }
549
-
550
- const sourceLines = source.split('\n');
551
-
552
- function printIssue(tag, colorFn, msg, hint, line, col) {
553
- const loc = line ? `:${line}${col ? ':' + col : ''}` : '';
554
- console.log(colorFn(` ${tag}${loc} ${msg}`));
555
- if (hint) console.log(clr(C.gray, ` hint: ${hint}`));
556
- if (line && line <= sourceLines.length) {
557
- const lineStr = sourceLines[line - 1] || '';
558
- const pointer = ' '.repeat(Math.max(0, (col || 1) - 1)) + '^';
559
- console.log(clr(C.gray, ` ${String(line).padStart(4)} │ `) + lineStr);
560
- console.log(clr(C.gray, ` │ `) + clr(colorFn === clr.bind(null, C.red) ? C.red : C.yellow, pointer));
561
- }
562
- console.log();
563
- }
564
-
565
- // ── 2. Immutability errors ────────────────────────────────────────────────
566
- const staticErrors = result.errors.filter(e => e.name === 'CheckError');
567
- for (const e of staticErrors) {
568
- totalIssues++; exitCode = 1;
569
- printIssue('[E] immutability', clr.bind(null, C.red), e.message, e.hint, e.line, e.col);
570
- }
571
-
572
- // ── 3. Type errors ────────────────────────────────────────────────────────
573
- for (const e of (result.typeErrors || [])) {
574
- totalIssues++; exitCode = 1;
575
- printIssue('[E] type', clr.bind(null, C.red), e.message, e.hint, e.line, e.col);
576
- }
577
-
578
- // ── 4. Type warnings ─────────────────────────────────────────────────────
579
- for (const w of (result.typeWarnings || [])) {
580
- totalIssues++;
581
- printIssue('[W] type', clr.bind(null, C.yellow), w.message, w.hint, w.line, null);
582
- }
583
-
584
- // ── 5. AST lint rules (unused-var / unreachable / shadow-val) ─────────────
585
- if (result.ast) {
586
- const lintIssues = lint(result.ast);
587
- for (const issue of lintIssues) {
588
- totalIssues++;
589
- const tag = `[${issue.severity === 'error' ? 'E' : issue.severity === 'warn' ? 'W' : 'I'}] ${issue.rule}`;
590
- const colorFn = issue.severity === 'error'
591
- ? clr.bind(null, C.red)
592
- : issue.severity === 'warn'
593
- ? clr.bind(null, C.yellow)
594
- : clr.bind(null, C.gray);
595
- if (issue.severity === 'error') exitCode = 1;
596
- printIssue(tag, colorFn, issue.message, issue.hint, issue.line, issue.col);
597
- }
598
- }
599
-
600
- // ── 6. Format check ───────────────────────────────────────────────────────
601
- try {
602
- const formatted = format(source);
603
- if (formatted !== source) {
604
- totalIssues++;
605
- console.log(clr(C.yellow,
606
- ` [W] format File is not formatted — run: flux fmt ${baseName}\n`));
607
- }
608
- } catch (_) { /* formatter errors are not lint errors */ }
609
-
610
- // ── 7. Style: long lines & TODO markers ──────────────────────────────────
611
- const MAX_LINE = 120;
612
- let longLines = 0;
613
- let todoCount = 0;
614
- sourceLines.forEach((line, i) => {
615
- if (line.length > MAX_LINE) {
616
- longLines++;
617
- if (longLines <= 3)
618
- console.log(clr(C.yellow,
619
- ` [W] style:${i + 1} Line exceeds ${MAX_LINE} characters (${line.length} chars)\n`));
620
- }
621
- if (/\/\/\s*(TODO|FIXME|HACK)/i.test(line)) {
622
- todoCount++;
623
- console.log(clr(C.gray, ` [I] note:${i + 1} ${line.trim()}\n`));
624
- }
625
- });
626
- if (longLines > 3)
627
- console.log(clr(C.yellow, ` [W] style ... and ${longLines - 3} more long line(s)\n`));
628
- totalIssues += longLines + todoCount;
629
-
630
- // ── 8. Stdlib usage info ──────────────────────────────────────────────────
631
- const stdlibUsed = detectUsedSymbols(result.output);
632
- if (stdlibUsed.length > 0) {
633
- console.log(clr(C.gray, ` [I] stdlib Using: ${stdlibUsed.join(', ')}\n`));
634
- }
635
-
636
- // ── Summary ───────────────────────────────────────────────────────────────
637
- const lineCount = sourceLines.length;
638
- console.log('─'.repeat(50));
639
- if (exitCode === 0 && totalIssues === 0) {
640
- console.log(
641
- clr(C.green, `✓ ${baseName} — no issues`) +
642
- clr(C.gray, ` (${lineCount} lines)`)
643
- );
644
- } else if (exitCode === 0) {
645
- console.log(
646
- clr(C.yellow, `⚠ ${baseName} — ${totalIssues} warning(s)`) +
647
- clr(C.gray, ` (${lineCount} lines)`)
648
- );
649
- } else {
650
- console.log(
651
- clr(C.red, `✗ ${baseName} — ${totalIssues} issue(s)`) +
652
- clr(C.gray, ` (${lineCount} lines)`)
653
- );
654
- }
655
- console.log();
656
- if (exitCode !== 0) process.exit(exitCode);
657
- }
658
-
659
- // flux fmt <file.flux> — format source in-place
660
- function cmdFmt(filePath, opts) {
661
- const { source, abs } = readFluxFile(filePath);
662
- const { format } = require('../src/formatter');
663
- const output = format(source);
664
-
665
- if (output === source) {
666
- console.log(clr(C.gray, `~ ${path.basename(abs)} — already formatted`));
667
- return;
668
- }
669
-
670
- if (opts.stdout) {
671
- console.log(output);
672
- return;
673
- }
674
-
675
- fs.writeFileSync(abs, output, 'utf8');
676
- const diff = Math.abs(output.split('\n').length - source.split('\n').length);
677
- console.log(clr(C.green, `✓ ${path.basename(abs)} — formatted`) +
678
- (diff > 0 ? clr(C.gray, ` (${diff} line${diff===1?'':'s'} changed)`) : ''));
679
- }
680
-
681
- // flux test [dir] — discover and run *.test.flux test files
682
- function cmdTest(dirOrFile) {
683
- const { runTests } = require('../src/test-runner');
684
- const target = dirOrFile || 'tests';
685
- runTests(target, transpile);
686
- }
687
-
688
- // flux tokens <file.flux>
689
- function cmdTokens(filePath) {
690
- const { source } = readFluxFile(filePath);
691
- const { Lexer } = require('../src/lexer');
692
- const tokens = new Lexer(source).tokenize();
693
-
694
- console.log(clr(C.bold, '\n=== TOKEN LIST ===\n'));
695
- const typeW = 20;
696
- console.log(clr(C.gray, `${'TYPE'.padEnd(typeW)} VALUE LINE:COL`));
697
- console.log(clr(C.gray, '─'.repeat(60)));
698
- for (const t of tokens) {
699
- const val = JSON.stringify(t.value) || '';
700
- console.log(
701
- clr(C.cyan, t.type.padEnd(typeW)) +
702
- val.substring(0, 24).padEnd(25) +
703
- clr(C.gray, `${t.line}:${t.col}`)
704
- );
705
- }
706
- console.log();
707
- }
708
-
709
- // flux ast <file.flux>
710
- function cmdAst(filePath) {
711
- const { source, abs } = readFluxFile(filePath);
712
- const result = transpile(source);
713
-
714
- if (!result.success) {
715
- printErrors(result.errors, source, abs);
716
- process.exit(1);
717
- }
718
-
719
- console.log(clr(C.bold, '\n=== ABSTRACT SYNTAX TREE ===\n'));
720
- console.log(JSON.stringify(result.ast, null, 2));
721
- console.log();
722
- }
723
-
724
- // flux watch <file.flux>
725
- function cmdWatch(filePath, opts) {
726
- const abs = path.resolve(filePath);
727
- console.log(clr(C.cyan, `\n👁 Watching: ${path.basename(abs)}\n`));
728
-
729
- function doCompile() {
730
- try {
731
- const source = fs.readFileSync(abs, 'utf8');
732
- const result = transpile(source);
733
- const ts = new Date().toLocaleTimeString();
734
- if (!result.success) {
735
- console.error(clr(C.red, `[${ts}] ✗ Error`));
736
- printErrors(result.errors, source, abs);
737
- } else {
738
- const outPath = deriveOutPath(filePath, opts.out);
739
- fs.writeFileSync(outPath, result.output, 'utf8');
740
- console.log(clr(C.gray, `[${ts}]`) + clr(C.green, ` ✓ Compiled → ${path.relative(process.cwd(), outPath)}`));
741
- }
742
- } catch (e) {
743
- console.error(clr(C.red, `[Error] ${e.message}`));
744
- }
745
- }
746
-
747
- doCompile();
748
- fs.watch(abs, { persistent: true }, (event) => {
749
- if (event === 'change') doCompile();
750
- });
751
- }
752
-
753
- // flux repl
754
- function cmdRepl() {
755
- const readline = require('readline');
756
- const rl = readline.createInterface({
757
- input: process.stdin,
758
- output: process.stdout,
759
- prompt: clr(C.cyan, 'flux') + clr(C.gray, '> '),
760
- });
761
-
762
- banner();
763
- console.log(clr(C.gray, 'Flux REPL — type Flux code and press Enter.'));
764
- console.log(clr(C.gray, 'Commands: .exit .clear .js .help\n'));
765
-
766
- let buffer = '';
767
- let showJs = false;
768
-
769
- rl.prompt();
770
-
771
- rl.on('line', (line) => {
772
- const trimmed = line.trim();
773
-
774
- if (trimmed === '.exit' || trimmed === '.quit') {
775
- console.log(clr(C.gray, 'Goodbye!'));
776
- process.exit(0);
777
- }
778
- if (trimmed === '.clear') { buffer = ''; console.clear(); rl.prompt(); return; }
779
- if (trimmed === '.js') { showJs = !showJs; console.log(clr(C.gray, `Show JS: ${showJs ? 'ON' : 'OFF'}`)); rl.prompt(); return; }
780
- if (trimmed === '.help') {
781
- console.log(clr(C.gray, '.exit / .quit — exit REPL'));
782
- console.log(clr(C.gray, '.clear — clear screen & buffer'));
783
- console.log(clr(C.gray, '.js — toggle JavaScript output'));
784
- rl.prompt(); return;
785
- }
786
-
787
- buffer += line + '\n';
788
-
789
- const opens = (buffer.match(/\{/g) || []).length;
790
- const closes = (buffer.match(/\}/g) || []).length;
791
-
792
- if (opens === closes && buffer.trim().length > 0) {
793
- const result = transpile(buffer);
794
- if (!result.success) {
795
- printErrors(result.errors, buffer, null);
796
- } else {
797
- if (showJs) {
798
- console.log(clr(C.gray, '--- JavaScript ---'));
799
- console.log(clr(C.dim, result.output));
800
- console.log(clr(C.gray, '------------------'));
801
- }
802
- const tmpPath = require('path').join(require('os').tmpdir(), '_flux_repl_' + Date.now() + '.js');
803
- fs.writeFileSync(tmpPath, result.output);
804
- try { require(tmpPath); }
805
- catch (e) { console.error(clr(C.red, `[Runtime] ${e.message}`)); }
806
- finally { try { fs.unlinkSync(tmpPath); } catch (_) {} }
807
- }
808
- buffer = '';
809
- }
810
-
811
- rl.prompt();
812
- });
813
-
814
- rl.on('close', () => { console.log(); process.exit(0); });
815
- }
816
-
817
- // ── flux publish ──────────────────────────────────────────────────────────────
818
- // Automated release workflow:
819
- // 1. Validate git working tree is clean
820
- // 2. Run full test suite
821
- // 3. Bump version (patch / minor / major / explicit)
822
- // 4. Update CHANGELOG.md (Unreleased → new version)
823
- // 5. git commit + tag
824
- // 6. Optionally: git push --follow-tags
825
- // 7. Optionally: npm publish
826
- //
827
- // Flags:
828
- // --patch bump patch (default)
829
- // --minor bump minor
830
- // --major bump major
831
- // --ver <x.y.z> explicit version
832
- // --push git push origin + tags after tagging
833
- // --npm run npm publish after tagging
834
- // --dry-run print what would happen; no writes or git operations
835
- // --skip-tests skip test suite (faster, risky)
836
- // --yes / -y skip all confirmation prompts
837
-
838
- function cmdPublish(publishOpts) {
839
- const { execSync, spawnSync } = require('child_process');
840
- const readline = require('readline');
841
-
842
- const isDry = publishOpts.dryRun;
843
- const doPush = publishOpts.push;
844
- const doNpm = publishOpts.npm;
845
- const skipTests = publishOpts.skipTests;
846
- const autoYes = publishOpts.yes;
847
- const bumpType = publishOpts.bump || 'patch'; // 'patch' | 'minor' | 'major' | explicit
848
- const explicitVer = publishOpts.ver || null;
849
-
850
- // ── helpers ─────────────────────────────────────────────────────────────────
851
- function step(msg) { console.log(clr(C.cyan, `\n ▸ ${msg}`)); }
852
- function ok(msg) { console.log(clr(C.green, ` ✓ ${msg}`)); }
853
- function warn(msg) { console.log(clr(C.yellow,` ⚠ ${msg}`)); }
854
- function fail(msg) { console.error(clr(C.red, ` ✗ ${msg}`)); process.exit(1); }
855
- function info(msg) { console.log(clr(C.gray, ` ${msg}`)); }
856
- function dry(msg) { console.log(clr(C.dim, ` [dry] ${msg}`)); }
857
-
858
- function run(cmd, opts = {}) {
859
- if (isDry) { dry(cmd); return ''; }
860
- try {
861
- return execSync(cmd, { encoding: 'utf8', stdio: opts.silent ? 'pipe' : 'inherit' });
862
- } catch (e) {
863
- if (opts.allowFail) return null;
864
- fail(`Command failed: ${cmd}\n${e.message}`);
865
- }
866
- }
867
-
868
- function runSilent(cmd) {
869
- try { return execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }); }
870
- catch (e) { return null; }
871
- }
872
-
873
- // ── bump version logic ───────────────────────────────────────────────────────
874
- function bumpVersion(current, type) {
875
- if (explicitVer) return explicitVer;
876
- const parts = current.split('.').map(Number);
877
- if (type === 'major') { parts[0]++; parts[1] = 0; parts[2] = 0; }
878
- else if (type === 'minor') { parts[1]++; parts[2] = 0; }
879
- else { parts[2]++; }
880
- return parts.join('.');
881
- }
882
-
883
- // ── update CHANGELOG ────────────────────────────────────────────────────────
884
- function updateChangelog(newVersion, changelogPath) {
885
- if (!fs.existsSync(changelogPath)) {
886
- warn('CHANGELOG.md not found — skipping changelog update');
887
- return false;
888
- }
889
- const content = fs.readFileSync(changelogPath, 'utf8');
890
- const date = new Date().toISOString().slice(0, 10);
891
- const marker = '## [Unreleased]';
892
-
893
- if (!content.includes(marker)) {
894
- warn('No [Unreleased] section found in CHANGELOG.md');
895
- return false;
896
- }
897
-
898
- const updated = content.replace(
899
- marker,
900
- `${marker}\n\n## [${newVersion}] — ${date}`
901
- );
902
-
903
- if (isDry) {
904
- dry(`CHANGELOG.md: replace "${marker}" → add "## [${newVersion}] — ${date}"`);
905
- } else {
906
- fs.writeFileSync(changelogPath, updated, 'utf8');
907
- }
908
- return true;
909
- }
910
-
911
- // ── confirm prompt ───────────────────────────────────────────────────────────
912
- function confirm(question) {
913
- return new Promise(resolve => {
914
- if (autoYes || isDry) { console.log(clr(C.gray, ` ? ${question} → y (auto)`)); return resolve(true); }
915
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
916
- rl.question(clr(C.yellow, ` ? ${question} [y/N] `), ans => {
917
- rl.close();
918
- resolve(ans.trim().toLowerCase() === 'y');
919
- });
920
- });
921
- }
922
-
923
- // ── MAIN FLOW (async) ────────────────────────────────────────────────────────
924
- async function publish() {
925
- banner();
926
- console.log(clr(C.bold, ` Release Workflow${isDry ? clr(C.yellow, ' [DRY RUN]') : ''}\n`));
927
-
928
- const pkgPath = path.resolve('package.json');
929
- const changelogPath = path.resolve('CHANGELOG.md');
930
-
931
- // ── 0. Read package.json ─────────────────────────────────────────────────
932
- if (!fs.existsSync(pkgPath)) fail('package.json not found');
933
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
934
- const oldVersion = pkg.version;
935
- const newVersion = bumpVersion(oldVersion, bumpType);
936
-
937
- step(`Preparing release: ${clr(C.gray, oldVersion)} → ${clr(C.green, newVersion)}`);
938
- info(`Package: ${pkg.name}`);
939
- info(`Bump: ${explicitVer ? `explicit (${newVersion})` : bumpType}`);
940
- info(`Dry run: ${isDry ? 'YES' : 'no'}`);
941
- info(`Push: ${doPush ? 'yes' : 'no'}`);
942
- info(`npm: ${doNpm ? 'yes' : 'no'}`);
943
- console.log();
944
-
945
- // ── 1. Git availability ──────────────────────────────────────────────────
946
- step('Checking git');
947
- const gitVersion = runSilent('git --version');
948
- if (!gitVersion) fail('git is not installed or not in PATH');
949
- ok(`git found: ${gitVersion.trim()}`);
950
-
951
- // ── 2. Working tree status ───────────────────────────────────────────────
952
- step('Checking working tree');
953
- const status = runSilent('git status --porcelain');
954
- if (status && status.trim().length > 0) {
955
- warn('Working tree has uncommitted changes:');
956
- status.trim().split('\n').forEach(l => info(l));
957
- const cont = await confirm('Continue anyway? (uncommitted changes will be committed with the release)');
958
- if (!cont) fail('Aborted — please commit or stash changes first');
959
- } else {
960
- ok('Working tree is clean');
961
- }
962
-
963
- // ── 3. Check tag doesn't already exist ───────────────────────────────────
964
- const tagName = `v${newVersion}`;
965
- const existsTag = runSilent(`git tag -l ${tagName}`);
966
- if (existsTag && existsTag.trim() === tagName) {
967
- fail(`Tag ${tagName} already exists — bump the version again or delete the tag first`);
968
- }
969
-
970
- // ── 4. Run tests ─────────────────────────────────────────────────────────
971
- if (skipTests) {
972
- warn('Skipping tests (--skip-tests)');
973
- } else {
974
- step('Running test suite');
975
- const testDir = fs.existsSync(path.resolve('tests')) ? 'tests' : null;
976
- if (!testDir) {
977
- warn('No tests/ directory found — skipping');
978
- } else {
979
- if (isDry) {
980
- dry('node bin/flux.js test tests/');
981
- ok('Tests passed (dry run)');
982
- } else {
983
- const { runTests } = require('../src/test-runner');
984
- const savedExit = process.exitCode;
985
- let testsPassed = true;
986
- try {
987
- // Run tests synchronously via child process so we get exit code
988
- execSync('node bin/flux.js test tests/', {
989
- encoding: 'utf8',
990
- stdio: 'inherit',
991
- env: { ...process.env, NO_COLOR: process.env.NO_COLOR },
992
- });
993
- } catch {
994
- testsPassed = false;
995
- }
996
- if (!testsPassed) fail('Tests failed — fix them before releasing');
997
- ok('All tests passed');
998
- }
999
- }
1000
- }
1001
-
1002
- // ── 5. Confirm version ───────────────────────────────────────────────────
1003
- step('Confirm release');
1004
- const go = await confirm(`Release ${clr(C.bold, pkg.name + '@' + newVersion)} (${tagName})?`);
1005
- if (!go) fail('Release cancelled');
1006
-
1007
- // ── 6. Bump package.json ─────────────────────────────────────────────────
1008
- step(`Bumping package.json: ${oldVersion} → ${newVersion}`);
1009
- if (!isDry) {
1010
- pkg.version = newVersion;
1011
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
1012
- } else {
1013
- dry(`package.json: "version": "${oldVersion}" → "${newVersion}"`);
1014
- }
1015
- ok(`package.json updated`);
1016
-
1017
- // ── 7. Update CHANGELOG ──────────────────────────────────────────────────
1018
- step('Updating CHANGELOG.md');
1019
- const clUpdated = updateChangelog(newVersion, changelogPath);
1020
- if (clUpdated) ok(`CHANGELOG.md updated — [${newVersion}] section created`);
1021
-
1022
- // ── 8. Git commit ────────────────────────────────────────────────────────
1023
- step('Creating git commit');
1024
- run('git add package.json CHANGELOG.md');
1025
- run(`git commit -m "chore: release ${tagName}"`);
1026
- ok(`Committed: chore: release ${tagName}`);
1027
-
1028
- // ── 9. Git tag ───────────────────────────────────────────────────────────
1029
- step(`Creating git tag: ${tagName}`);
1030
- run(`git tag -a ${tagName} -m "Release ${tagName}"`);
1031
- ok(`Tag created: ${tagName}`);
1032
-
1033
- // ── 10. Git push ─────────────────────────────────────────────────────────
1034
- if (doPush) {
1035
- step('Pushing to origin');
1036
- run('git push origin HEAD --follow-tags');
1037
- ok('Pushed branch + tags to origin');
1038
- if (doNpm) info('→ GitHub Actions publish.yml will now auto-publish to npm');
1039
- } else {
1040
- info(`To push: ${clr(C.yellow, 'git push origin HEAD --follow-tags')}`);
1041
- }
1042
-
1043
- // ── 11. npm publish ──────────────────────────────────────────────────────
1044
- if (doNpm) {
1045
- step('Publishing to npm');
1046
- const npmWho = runSilent('npm whoami');
1047
- if (!npmWho) {
1048
- warn('Not logged in to npm — run: npm login');
1049
- warn('Skipping npm publish');
1050
- } else {
1051
- info(`npm user: ${npmWho.trim()}`);
1052
- run('npm publish --access public');
1053
- ok(`Published to npm: ${pkg.name}@${newVersion}`);
1054
- }
1055
- } else {
1056
- info(`To publish: ${clr(C.yellow, 'npm publish --access public')}`);
1057
- }
1058
-
1059
- // ── Done ─────────────────────────────────────────────────────────────────
1060
- console.log();
1061
- console.log(clr(C.green, C.bold + ` ✓ Released: ${pkg.name}@${newVersion}` + C.reset));
1062
- console.log();
1063
- console.log(clr(C.gray, ' Summary:'));
1064
- console.log(` ${clr(C.cyan, `package.json`)} version → ${newVersion}`);
1065
- if (clUpdated)
1066
- console.log(` ${clr(C.cyan, `CHANGELOG.md`)} [${newVersion}] — ${new Date().toISOString().slice(0,10)}`);
1067
- console.log(` ${clr(C.cyan, `git tag`)} ${tagName}`);
1068
- if (doPush)
1069
- console.log(` ${clr(C.cyan, `git push`)} origin + tags`);
1070
- if (doNpm && !isDry)
1071
- console.log(` ${clr(C.cyan, `npm`)} ${pkg.name}@${newVersion} published`);
1072
- console.log();
1073
-
1074
- if (isDry) {
1075
- console.log(clr(C.yellow, ' [Dry run] — no files changed, no git operations performed\n'));
1076
- }
1077
- }
1078
-
1079
- publish().catch(e => {
1080
- console.error(clr(C.red, `\n✗ Publish error: ${e.message}\n`));
1081
- process.exit(1);
1082
- });
1083
- }
1084
-
1085
- // ── flux self-hosted ─────────────────────────────────────────────────────────
1086
- // Shows bootstrap status, toggles the active compiler, and lets the user
1087
- // rebuild/verify self-hosting from within the CLI.
1088
- function cmdSelfHosted(subArg) {
1089
- const SELF_DIR = path.join(__dirname, '../src/self');
1090
- const BOOTSTRAP = path.join(__dirname, '../scripts/bootstrap.js');
1091
- const { execSync, spawnSync } = require('child_process');
1092
-
1093
- const SOURCES = [
1094
- 'css-preprocessor', 'checker', 'type-checker', 'jsx',
1095
- 'lexer', 'parser', 'codegen', 'transpiler',
1096
- 'formatter', 'sourcemap', 'stdlib', 'mangler',
1097
- 'linter', 'bundler', 'test-runner',
1098
- ];
1099
-
1100
- // ── sub-commands ──────────────────────────────────────────────────────────
1101
-
1102
- // flux self-hosted build — compile all .flux sources to .js
1103
- if (subArg === 'build') {
1104
- console.log(clr(C.cyan, '\n⊛ Building self-hosted compiler (all 15 modules)...\n'));
1105
- const t0 = Date.now();
1106
- let ok = 0, fail = 0;
1107
- for (const name of SOURCES) {
1108
- const src = path.join(SELF_DIR, name + '.flux');
1109
- const out = path.join(SELF_DIR, name + '.js');
1110
- if (!fs.existsSync(src)) {
1111
- console.log(clr(C.yellow, ` ⚠ ${name}.flux — missing`));
1112
- fail++;
1113
- continue;
1114
- }
1115
- try {
1116
- execSync(`node "${path.join(__dirname, 'flux.js')}" compile "${src}" -o "${out}" --no-mangle`, {
1117
- cwd: path.join(__dirname, '..'),
1118
- stdio: 'pipe',
1119
- });
1120
- console.log(clr(C.green, ` ✓`) + clr(C.gray, ` ${name}.flux → ${name}.js`));
1121
- ok++;
1122
- } catch (e) {
1123
- const msg = e.stderr ? e.stderr.toString().split('\n')[0] : e.message;
1124
- console.log(clr(C.red, ` ✗ ${name}.flux — ${msg}`));
1125
- fail++;
1126
- }
1127
- }
1128
- const elapsed = Date.now() - t0;
1129
- console.log();
1130
- if (fail === 0) {
1131
- console.log(clr(C.green, `✓ All ${ok} modules built`) + clr(C.gray, ` in ${elapsed}ms`));
1132
- console.log(clr(C.gray, ` Run: flux self-hosted on to activate`));
1133
- } else {
1134
- console.log(clr(C.red, `✗ ${fail} module(s) failed`) + clr(C.gray, `, ${ok} succeeded`));
1135
- process.exitCode = 1;
1136
- }
1137
- console.log();
1138
- return;
1139
- }
1140
-
1141
- // flux self-hosted verify — run bootstrap.js (all 3 stages)
1142
- if (subArg === 'verify') {
1143
- console.log(clr(C.cyan, '\n⊛ Running full bootstrap verification...\n'));
1144
- const result = spawnSync('node', [BOOTSTRAP, '--verbose'], {
1145
- cwd: path.join(__dirname, '..'),
1146
- stdio: 'inherit',
1147
- });
1148
- process.exitCode = result.status || 0;
1149
- return;
1150
- }
1151
-
1152
- // flux self-hosted on — activate self-hosted compiler
1153
- if (subArg === 'on') {
1154
- const envFile = path.join(__dirname, '../.env.flux');
1155
- // Check all .js files exist
1156
- const missing = SOURCES.filter(n => !fs.existsSync(path.join(SELF_DIR, n + '.js')));
1157
- if (missing.length > 0) {
1158
- console.log(clr(C.yellow, '\n⚠ Self-hosted compiler not built yet. Run:\n'));
1159
- console.log(clr(C.cyan, ' flux self-hosted build\n'));
1160
- return;
1161
- }
1162
- fs.writeFileSync(envFile, 'FLUX_SELF_HOSTED=1\n', 'utf8');
1163
- console.log();
1164
- console.log(clr(C.green, '✓ Self-hosted compiler activated'));
1165
- console.log(clr(C.gray, ' All flux commands now use src/self/*.js (compiled-from-Flux)'));
1166
- console.log(clr(C.gray, ' Deactivate anytime: flux self-hosted off'));
1167
- console.log(clr(C.gray, ' Or use per-command: flux --self-hosted compile <file>'));
1168
- console.log();
1169
- return;
1170
- }
1171
-
1172
- // flux self-hosted off — deactivate self-hosted compiler
1173
- if (subArg === 'off') {
1174
- const envFile = path.join(__dirname, '../.env.flux');
1175
- if (fs.existsSync(envFile)) fs.unlinkSync(envFile);
1176
- console.log();
1177
- console.log(clr(C.green, '✓ Self-hosted compiler deactivated'));
1178
- console.log(clr(C.gray, ' flux now uses the stage-0 JavaScript compiler (src/*.js)'));
1179
- console.log();
1180
- return;
1181
- }
1182
-
1183
- // flux self-hosted (no subArg) — show status dashboard
1184
- console.log(clr(C.cyan, C.bold + '\n Flux Self-Hosted Compiler Status\n' + C.reset));
1185
-
1186
- // Detect active mode
1187
- const isActive = USE_SELF_HOSTED;
1188
- const activeLabel = isActive
1189
- ? clr(C.green, '● ACTIVE') + clr(C.gray, ' (using self-hosted compiler)')
1190
- : clr(C.gray, '○ INACTIVE') + clr(C.gray, ' (using stage-0 compiler)');
1191
- console.log(` Mode: ${activeLabel}`);
1192
- console.log(` Activate: ${clr(C.yellow, 'flux self-hosted on')} ${clr(C.gray, ' or --self-hosted flag')}`);
1193
- console.log();
1194
-
1195
- // Module status table
1196
- const CORE_MODULES = ['css-preprocessor','checker','type-checker','jsx','lexer','parser','codegen','transpiler'];
1197
- const TOOL_MODULES = ['formatter','sourcemap','stdlib','mangler','linter','bundler','test-runner'];
1198
-
1199
- function printModuleGroup(label, names) {
1200
- console.log(clr(C.bold, ` ${label}`));
1201
- let allBuilt = true;
1202
- for (const name of names) {
1203
- const fluxPath = path.join(SELF_DIR, name + '.flux');
1204
- const jsPath = path.join(SELF_DIR, name + '.js');
1205
- const hasFlux = fs.existsSync(fluxPath);
1206
- const hasJs = fs.existsSync(jsPath);
1207
-
1208
- let status, note = '';
1209
- if (hasJs && hasFlux) {
1210
- const fluxMtime = fs.statSync(fluxPath).mtimeMs;
1211
- const jsMtime = fs.statSync(jsPath).mtimeMs;
1212
- if (fluxMtime > jsMtime) {
1213
- status = clr(C.yellow, '⚡ stale');
1214
- note = clr(C.gray, ' (.flux newer than .js — run: flux self-hosted build)');
1215
- allBuilt = false;
1216
- } else {
1217
- const jsKB = (fs.statSync(jsPath).size / 1024).toFixed(1);
1218
- status = clr(C.green, '✓ built');
1219
- note = clr(C.gray, ` ${jsKB} KB`);
1220
- }
1221
- } else if (hasFlux && !hasJs) {
1222
- status = clr(C.red, '✗ not built');
1223
- note = clr(C.gray, ' (.js missing)');
1224
- allBuilt = false;
1225
- } else if (!hasFlux) {
1226
- status = clr(C.red, '✗ missing');
1227
- note = clr(C.gray, ' (.flux source not found)');
1228
- allBuilt = false;
1229
- }
1230
- const padded = (name + '.flux').padEnd(24);
1231
- console.log(` ${clr(C.cyan, padded)} ${status}${note}`);
1232
- }
1233
- return allBuilt;
1234
- }
1235
-
1236
- const coreOk = printModuleGroup('Core Pipeline (8 modules)', CORE_MODULES);
1237
- console.log();
1238
- const toolOk = printModuleGroup('Extended Toolchain (7 modules)', TOOL_MODULES);
1239
- console.log();
1240
-
1241
- const allOk = coreOk && toolOk;
1242
- if (allOk) {
1243
- console.log(clr(C.green, ' ✓ All 15 modules compiled and up-to-date'));
1244
- } else {
1245
- console.log(clr(C.yellow, ' ⚠ Some modules need rebuilding.') + ' Run: ' + clr(C.cyan, 'flux self-hosted build'));
1246
- }
1247
-
1248
- // Show stage-2 proof if it exists
1249
- const stage2 = path.join(SELF_DIR, 'lexer.stage2.js');
1250
- if (fs.existsSync(stage2)) {
1251
- const mtime = new Date(fs.statSync(stage2).mtimeMs);
1252
- const when = mtime.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
1253
- console.log(clr(C.gray, `\n Stage-2 self-compile proof: lexer.stage2.js (verified ${when})`));
1254
- }
1255
-
1256
- console.log();
1257
- console.log(clr(C.bold, ' Commands:'));
1258
- const cmds2 = [
1259
- ['flux self-hosted build', 'Compile all 15 .flux → .js modules'],
1260
- ['flux self-hosted verify', 'Full 3-stage bootstrap verification'],
1261
- ['flux self-hosted on', 'Activate self-hosted compiler globally'],
1262
- ['flux self-hosted off', 'Revert to stage-0 compiler'],
1263
- ];
1264
- for (const [c, d] of cmds2) {
1265
- console.log(` ${clr(C.cyan, c.padEnd(32))} ${clr(C.gray, d)}`);
1266
- }
1267
- console.log();
1268
- }
1269
-
1270
- // ── Argument Parser ───────────────────────────────────────────────────────────
1271
- function parseArgs(argv) {
1272
- const args = argv.slice(2);
1273
- const cmd = args[0];
1274
- const file = args[1] && !args[1].startsWith('-') ? args[1] : null;
1275
-
1276
- // General opts
1277
- const opts = { stdout: false, out: null, sourcemap: false, mangle: true };
1278
-
1279
- // Publish-specific opts
1280
- const publishOpts = {
1281
- bump: 'patch',
1282
- ver: null,
1283
- push: false,
1284
- npm: false,
1285
- dryRun: false,
1286
- skipTests: false,
1287
- yes: false,
1288
- };
1289
-
1290
- for (let i = 0; i < args.length; i++) {
1291
- // General
1292
- if (args[i] === '--stdout') opts.stdout = true;
1293
- if (args[i] === '--sourcemap' || args[i] === '-m') opts.sourcemap = true;
1294
- if (args[i] === '--mangle' || args[i] === '-M') opts.mangle = true;
1295
- if (args[i] === '--no-mangle' || args[i] === '-nm') opts.mangle = false;
1296
- if ((args[i] === '-o' || args[i] === '--out') && args[i + 1]) opts.out = args[++i];
1297
- if (args[i] === '--no-color') process.env.NO_COLOR = '1';
1298
-
1299
- // Publish
1300
- if (args[i] === '--patch') publishOpts.bump = 'patch';
1301
- if (args[i] === '--minor') publishOpts.bump = 'minor';
1302
- if (args[i] === '--major') publishOpts.bump = 'major';
1303
- if ((args[i] === '--ver' || args[i] === '--version-explicit') && args[i + 1])
1304
- publishOpts.ver = args[++i];
1305
- if (args[i] === '--push') publishOpts.push = true;
1306
- if (args[i] === '--npm') publishOpts.npm = true;
1307
- if (args[i] === '--dry-run' || args[i] === '--dry') publishOpts.dryRun = true;
1308
- if (args[i] === '--skip-tests') publishOpts.skipTests = true;
1309
- if (args[i] === '--yes' || args[i] === '-y') publishOpts.yes = true;
1310
- }
1311
-
1312
- return { cmd, file, opts, publishOpts };
1313
- }
1314
-
1315
- // ── Entry Point ───────────────────────────────────────────────────────────────
1316
- function main() {
1317
- const { cmd, file, opts, publishOpts } = parseArgs(process.argv);
1318
-
1319
- switch (cmd) {
1320
- case 'init': {
1321
- const name = process.argv[3] && !process.argv[3].startsWith('-') ? process.argv[3] : null;
1322
- cmdInit(name);
1323
- break;
1324
- }
1325
- case 'compile': {
1326
- if (!file) { console.error(clr(C.red, 'Specify a file: flux compile <file.flux>')); process.exit(1); }
1327
- cmdCompile(file, opts);
1328
- break;
1329
- }
1330
- case 'bundle': {
1331
- if (!file) { console.error(clr(C.red, 'Specify a file: flux bundle <entry.flux>')); process.exit(1); }
1332
- cmdBundle(file, opts);
1333
- break;
1334
- }
1335
- case 'run': {
1336
- if (!file) { console.error(clr(C.red, 'Specify a file: flux run <file.flux>')); process.exit(1); }
1337
- cmdRun(file);
1338
- break;
1339
- }
1340
- case 'check': {
1341
- if (!file) { console.error(clr(C.red, 'Specify a file: flux check <file.flux>')); process.exit(1); }
1342
- cmdCheck(file);
1343
- break;
1344
- }
1345
- case 'lint': {
1346
- if (!file) { console.error(clr(C.red, 'Specify a file: flux lint <file.flux>')); process.exit(1); }
1347
- cmdLint(file);
1348
- break;
1349
- }
1350
- case 'tokens': {
1351
- if (!file) { console.error(clr(C.red, 'Specify a file: flux tokens <file.flux>')); process.exit(1); }
1352
- cmdTokens(file);
1353
- break;
1354
- }
1355
- case 'ast': {
1356
- if (!file) { console.error(clr(C.red, 'Specify a file: flux ast <file.flux>')); process.exit(1); }
1357
- cmdAst(file);
1358
- break;
1359
- }
1360
- case 'watch': {
1361
- if (!file) { console.error(clr(C.red, 'Specify a file: flux watch <file.flux>')); process.exit(1); }
1362
- cmdWatch(file, opts);
1363
- break;
1364
- }
1365
- case 'fmt': {
1366
- if (!file) { console.error(clr(C.red, 'Specify a file: flux fmt <file.flux>')); process.exit(1); }
1367
- cmdFmt(file, opts);
1368
- break;
1369
- }
1370
- case 'test': {
1371
- cmdTest(file || process.argv[3]);
1372
- break;
1373
- }
1374
- case 'publish': {
1375
- cmdPublish(publishOpts);
1376
- break;
1377
- }
1378
- case 'self-hosted': {
1379
- const subArg = process.argv[3] && !process.argv[3].startsWith('-') ? process.argv[3] : undefined;
1380
- cmdSelfHosted(subArg);
1381
- break;
1382
- }
1383
- case 'repl':
1384
- case undefined:
1385
- if (!cmd) { showHelp(); }
1386
- else { cmdRepl(); }
1387
- break;
1388
- case 'version':
1389
- console.log(`flux-lang v${VERSION}`);
1390
- break;
1391
- case 'help':
1392
- default:
1393
- showHelp();
1394
- }
1395
- }
1396
-
1397
- main();
3
+ require('../dist/flux-cli.js');