@xnoxs/flux-lang 3.3.3 → 3.4.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.
@@ -0,0 +1,877 @@
1
+ // ============================================================
2
+ // Flux Self-Hosted CLI — flux command line interface
3
+ // src/self/cli.flux — written in Flux, compiled by stage-0
4
+ //
5
+ // Self-hosted: this CLI is compiled by the Flux compiler itself.
6
+ // Run via: node bin/flux.js run src/self/cli.flux
7
+ // Or after bootstrap: FLUX_SELF_HOSTED=1 flux <command>
8
+ // ============================================================
9
+
10
+ import Fs from "fs"
11
+ import Path from "path"
12
+ import Os from "os"
13
+ import { transpile, FLUX_VERSION, FLUX_STAGE } from "./transpiler"
14
+ import { bundle } from "./bundler"
15
+ import { format } from "./formatter"
16
+ import { lint } from "./linter"
17
+ import { runTests } from "./test-runner"
18
+ import { loadConfig } from "./config"
19
+ import {
20
+ cmdAdd, cmdRemove, cmdInstall, cmdList,
21
+ cmdSearch, cmdInfo, cmdPublish
22
+ } from "./pkg"
23
+
24
+ // ── Version ───────────────────────────────────────────────────
25
+ val VERSION = FLUX_VERSION ?? "3.0.0"
26
+ val STAGE = FLUX_STAGE ?? "self-hosted"
27
+
28
+ // ── ANSI Colors ───────────────────────────────────────────────
29
+ val C = {
30
+ reset: "\x1b[0m",
31
+ bold: "\x1b[1m",
32
+ dim: "\x1b[2m",
33
+ red: "\x1b[31m",
34
+ green: "\x1b[32m",
35
+ yellow: "\x1b[33m",
36
+ blue: "\x1b[34m",
37
+ cyan: "\x1b[36m",
38
+ white: "\x1b[37m",
39
+ gray: "\x1b[90m",
40
+ magenta: "\x1b[35m",
41
+ }
42
+
43
+ val noColor = process.env.NO_COLOR or not process.stdout.isTTY
44
+ fn clr(c, s): return noColor ? s : c + s + C.reset
45
+ fn bold(s): return clr(C.bold, s)
46
+ fn gray(s): return clr(C.gray, s)
47
+ fn green(s): return clr(C.green, s)
48
+ fn red(s): return clr(C.red, s)
49
+ fn cyan(s): return clr(C.cyan, s)
50
+ fn yellow(s): return clr(C.yellow, s)
51
+ fn blue(s): return clr(C.blue, s)
52
+
53
+ // ── Banner ────────────────────────────────────────────────────
54
+ fn showBanner():
55
+ if noColor:
56
+ console.log("Flux Lang " + VERSION + " [" + STAGE + "]")
57
+ return
58
+ console.log(cyan(bold(`
59
+ ███████╗██╗ ██╗ ██╗██╗ ██╗
60
+ ██╔════╝██║ ██║ ██║╚██╗██╔╝
61
+ █████╗ ██║ ██║ ██║ ╚███╔╝
62
+ ██╔══╝ ██║ ██║ ██║ ██╔██╗
63
+ ██║ ███████╗╚██████╔╝██╔╝ ██╗
64
+ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝`)))
65
+ console.log(gray(" Flux Lang v" + VERSION + " [" + STAGE + "] → JavaScript\n"))
66
+
67
+ // ── Help ──────────────────────────────────────────────────────
68
+ fn showHelp():
69
+ showBanner()
70
+ console.log(bold("USAGE:"))
71
+ console.log(" flux <command> [options]\n")
72
+
73
+ console.log(bold("COMPILER:"))
74
+ val compilerCmds = [
75
+ ["compile <file.flux>", "Compile .flux → .js"],
76
+ ["bundle <entry.flux>", "Bundle multiple files into one .js"],
77
+ ["run <file.flux>", "Compile and run immediately"],
78
+ ["watch <file.flux>", "Watch for changes, auto-compile"],
79
+ ["check <file.flux>", "Type-check and static analysis"],
80
+ ]
81
+ for [cmd, desc] in compilerCmds:
82
+ console.log(" " + green(("flux " + cmd).padEnd(36)) + " " + gray(desc))
83
+
84
+ console.log()
85
+ console.log(bold("TOOLING:"))
86
+ val toolCmds = [
87
+ ["lint <file.flux>", "Full lint: types + style + immutability"],
88
+ ["fmt <file.flux>", "Format source code in-place"],
89
+ ["test [dir]", "Run *.test.flux files"],
90
+ ["repl", "Interactive REPL mode"],
91
+ ["tokens <file.flux>", "Show lexer token list"],
92
+ ["ast <file.flux>", "Show Abstract Syntax Tree (JSON)"],
93
+ ]
94
+ for [cmd, desc] in toolCmds:
95
+ console.log(" " + green(("flux " + cmd).padEnd(36)) + " " + gray(desc))
96
+
97
+ console.log()
98
+ console.log(bold("PACKAGE MANAGER:"))
99
+ val pkgCmds = [
100
+ ["init [name]", "Scaffold a new Flux project"],
101
+ ["add <pkg[@version]>", "Add a dependency"],
102
+ ["remove <pkg>", "Remove a dependency"],
103
+ ["install", "Install all dependencies"],
104
+ ["list", "List installed packages"],
105
+ ["search <query>", "Search the package registry"],
106
+ ["info <pkg>", "Show package details"],
107
+ ["publish", "Publish package to registry"],
108
+ ]
109
+ for [cmd, desc] in pkgCmds:
110
+ console.log(" " + cyan(("flux " + cmd).padEnd(36)) + " " + gray(desc))
111
+
112
+ console.log()
113
+ console.log(bold("SELF-HOSTED:"))
114
+ val selfCmds = [
115
+ ["self-hosted", "Show self-hosted compiler status"],
116
+ ["self-hosted build", "Bootstrap: compile compiler with itself"],
117
+ ["self-hosted verify", "Verify self-hosted output matches stage-0"],
118
+ ]
119
+ for [cmd, desc] in selfCmds:
120
+ console.log(" " + yellow(("flux " + cmd).padEnd(36)) + " " + gray(desc))
121
+
122
+ console.log()
123
+ console.log(bold("OPTIONS:"))
124
+ console.log(" " + yellow("--out, -o <file> ") + " Output file")
125
+ console.log(" " + yellow("--sourcemap, -m ") + " Generate .js.map")
126
+ console.log(" " + yellow("--watch, -w ") + " Watch mode")
127
+ console.log(" " + yellow("--mangle ") + " Minify identifiers")
128
+ console.log(" " + yellow("--typecheck ") + " Enable type checking")
129
+ console.log(" " + yellow("--stdout ") + " Print to terminal")
130
+ console.log(" " + yellow("--no-color ") + " Disable colors")
131
+ console.log()
132
+
133
+ // ── Parse CLI args ────────────────────────────────────────────
134
+ fn parseArgs(argv):
135
+ val args = argv.slice(2)
136
+ val opts = {
137
+ out: null,
138
+ sourcemap: false,
139
+ mangle: false,
140
+ typecheck: false,
141
+ strict: false,
142
+ stdout: false,
143
+ watch: false,
144
+ dev: false,
145
+ verbose: false,
146
+ jsx: false,
147
+ jsxTarget: "browser",
148
+ }
149
+ val positional = []
150
+ var i = 0
151
+ while i < args.length:
152
+ val a = args[i]
153
+ if a == "--out" or a == "-o":
154
+ i = i + 1
155
+ opts.out = args[i]
156
+ else if a == "--sourcemap" or a == "-m":
157
+ opts.sourcemap = true
158
+ else if a == "--mangle":
159
+ opts.mangle = true
160
+ else if a == "--typecheck" or a == "-t":
161
+ opts.typecheck = true
162
+ else if a == "--strict":
163
+ opts.strict = true
164
+ else if a == "--stdout":
165
+ opts.stdout = true
166
+ else if a == "--watch" or a == "-w":
167
+ opts.watch = true
168
+ else if a == "--dev":
169
+ opts.dev = true
170
+ else if a == "--verbose" or a == "-v":
171
+ opts.verbose = true
172
+ else if a == "--jsx":
173
+ opts.jsx = true
174
+ else if a == "--jsx-target":
175
+ i = i + 1
176
+ opts.jsxTarget = args[i]
177
+ else if not a.startsWith("--"):
178
+ positional.push(a)
179
+ i = i + 1
180
+ return { positional, opts }
181
+
182
+ // ── Read file or exit ─────────────────────────────────────────
183
+ fn readFluxFile(filePath):
184
+ val abs = Path.resolve(filePath)
185
+ if not Fs.existsSync(abs):
186
+ console.error(red("✗ File not found: " + abs))
187
+ process.exit(1)
188
+ if not filePath.endsWith(".flux"):
189
+ console.warn(yellow("⚠ Not a .flux file: " + filePath))
190
+ return { source: Fs.readFileSync(abs, "utf8"), abs }
191
+
192
+ // ── Derive output path ────────────────────────────────────────
193
+ fn deriveOutPath(inputPath, outFlag):
194
+ if outFlag: return Path.resolve(outFlag)
195
+ val base = Path.basename(inputPath, ".flux")
196
+ return Path.join(Path.dirname(Path.resolve(inputPath)), base + ".js")
197
+
198
+ // ── Error renderer ────────────────────────────────────────────
199
+ val ERROR_KIND = {
200
+ ParseError: "Syntax error",
201
+ LexerError: "Syntax error",
202
+ CheckError: "Static error",
203
+ TypeCheckError: "Type error",
204
+ TypeError: "Type error",
205
+ }
206
+
207
+ fn printErrors(errors, source, filePath):
208
+ val lines = source.split("\n")
209
+ for err in errors:
210
+ val kind = ERROR_KIND[err.name] ?? "Error"
211
+ val stage = err.stage ? gray(" [" + err.stage + "]") : ""
212
+ console.error()
213
+ console.error(red(bold(kind)) + stage)
214
+ if err.line:
215
+ val fileLabel = filePath ? cyan(Path.relative(process.cwd(), filePath)) : ""
216
+ val locLabel = yellow(err.line + ":" + (err.col ?? 1))
217
+ if fileLabel: console.error(" " + fileLabel + ":" + locLabel)
218
+ else: console.error(" Line " + locLabel)
219
+ console.error(" " + err.message)
220
+ if err.line and err.line <= lines.length:
221
+ val errLineIdx = err.line - 1
222
+ val col = Math.max(0, (err.col ?? 1) - 1)
223
+ val tokLen = Math.max(1, err.len ?? 1)
224
+ if errLineIdx > 0 and lines[errLineIdx - 1].trim() != "":
225
+ val prev = String(err.line - 1).padStart(4)
226
+ console.error(gray(" " + prev + " │ " + lines[errLineIdx - 1]))
227
+ val lineNum = String(err.line).padStart(4)
228
+ console.error(gray(" " + lineNum + " │ ") + lines[errLineIdx])
229
+ val squiggle = "^" + "~".repeat(Math.max(0, tokLen - 1))
230
+ val pointer = " ".repeat(col) + red(squiggle)
231
+ console.error(gray(" │ ") + pointer)
232
+ if errLineIdx + 1 < lines.length and lines[errLineIdx + 1].trim() != "":
233
+ val next = String(err.line + 1).padStart(4)
234
+ console.error(gray(" " + next + " │ " + lines[errLineIdx + 1]))
235
+ if err.hint:
236
+ console.error(cyan(" Hint: ") + gray(err.hint))
237
+ console.error()
238
+
239
+ // ══════════════════════════════════════════════════════════════
240
+ // Commands
241
+ // ══════════════════════════════════════════════════════════════
242
+
243
+ // ── flux compile ──────────────────────────────────────────────
244
+ fn cmdCompile(filePath, opts):
245
+ val { source, abs } = readFluxFile(filePath)
246
+ val cfg = loadConfig(Path.dirname(abs))
247
+ val outPath = deriveOutPath(filePath, opts.out)
248
+ val mapPath = outPath + ".map"
249
+ val t0 = Date.now()
250
+
251
+ val result = transpile(source, {
252
+ sourcemap: opts.sourcemap ?? cfg.sourcemap,
253
+ mangle: opts.mangle ?? cfg.mangle,
254
+ typecheck: opts.typecheck ?? cfg.typecheck,
255
+ jsx: opts.jsx ?? cfg.jsx,
256
+ jsxTarget: opts.jsxTarget ?? cfg.jsxTarget,
257
+ sourceFile: Path.relative(Path.dirname(outPath), abs),
258
+ outputFile: Path.basename(outPath),
259
+ })
260
+
261
+ if not result.success:
262
+ console.error(red("\n✗ Compile failed — " + result.errors.length + " error(s)"))
263
+ printErrors(result.errors, source, abs)
264
+ process.exit(1)
265
+
266
+ val elapsed = Date.now() - t0
267
+
268
+ if opts.stdout:
269
+ console.log(result.output)
270
+ return
271
+
272
+ Fs.writeFileSync(outPath, result.output, "utf8")
273
+
274
+ var extra = ""
275
+ if opts.sourcemap and result.sourceMap:
276
+ Fs.writeFileSync(mapPath, result.sourceMap, "utf8")
277
+ extra = gray(" + " + Path.relative(process.cwd(), mapPath))
278
+
279
+ val rel = Path.relative(process.cwd(), abs)
280
+ val relO = Path.relative(process.cwd(), outPath)
281
+ console.log(
282
+ green("✓ ") + gray("(" + elapsed + "ms) ") +
283
+ blue(rel) + gray(" → ") + cyan(relO) + extra
284
+ )
285
+
286
+ if result.typeErrors and result.typeErrors.length > 0:
287
+ console.warn(yellow("\n⚠ " + result.typeErrors.length + " type warning(s)"))
288
+ printErrors(result.typeErrors, source, abs)
289
+
290
+ // ── flux run ──────────────────────────────────────────────────
291
+ fn cmdRun(filePath, opts):
292
+ val { source, abs } = readFluxFile(filePath)
293
+ val result = transpile(source, {
294
+ jsx: opts.jsx ?? false,
295
+ jsxTarget: opts.jsxTarget ?? "browser",
296
+ mangle: false,
297
+ })
298
+
299
+ if not result.success:
300
+ console.error(red("\n✗ Compile error"))
301
+ printErrors(result.errors, source, abs)
302
+ process.exit(1)
303
+
304
+ val tmpPath = Path.join(Os.tmpdir(), "_flux_run_" + Date.now() + ".js")
305
+ Fs.writeFileSync(tmpPath, result.output, "utf8")
306
+ console.log(gray("▶ Running " + Path.basename(abs) + " ...\n"))
307
+ try:
308
+ require(tmpPath)
309
+ catch(e):
310
+ console.error(red("\n[Runtime Error] " + e.message))
311
+ process.exitCode = 1
312
+ finally:
313
+ try: Fs.unlinkSync(tmpPath)
314
+ catch(e2): null
315
+
316
+ // ── flux check ───────────────────────────────────────────────
317
+ fn runCheck(abs):
318
+ val source = Fs.readFileSync(abs, "utf8")
319
+ val baseName = Path.basename(abs)
320
+ val result = transpile(source, { check: true, typecheck: true })
321
+
322
+ if not result.success:
323
+ val n = result.errors.length
324
+ val kind = result.errors.some(e -> e.name == "CheckError") ? "static" : "syntax"
325
+ console.error(red("\n✗ " + baseName + ": " + n + " " + kind + " error(s)"))
326
+ printErrors(result.errors, source, abs)
327
+ return { ok: false, typeErrors: 0, warnings: 0 }
328
+
329
+ var allOk = true
330
+ val typeErrors = result.typeErrors ?? []
331
+ if typeErrors.length > 0:
332
+ console.error(red("\n✗ " + baseName + ": " + typeErrors.length + " type error(s)"))
333
+ printErrors(typeErrors, source, abs)
334
+ allOk = false
335
+
336
+ val warnings = result.typeWarnings ?? []
337
+ for w in warnings:
338
+ val fileRef = cyan(baseName) + (w.line ? yellow(":" + w.line) : "")
339
+ console.warn(yellow(" ⚠ ") + fileRef + gray(" " + w.message))
340
+ if w.hint: console.warn(cyan(" Hint: ") + gray(w.hint))
341
+
342
+ val fnRe = /^(?:async )?function \w/gm
343
+ val clsRe = /^class \w/gm
344
+ val fns = (result.output.match(fnRe) ?? []).length
345
+ val cls = (result.output.match(clsRe) ?? []).length
346
+
347
+ if allOk:
348
+ console.log(
349
+ green("✓ ") + cyan(baseName) + gray(" — no errors") +
350
+ gray(" Functions: " + fns + " | Classes: " + cls + " | JS output: " + result.output.split("\n").length + " lines")
351
+ )
352
+
353
+ return { ok: allOk, typeErrors: typeErrors.length, warnings: warnings.length }
354
+
355
+ fn cmdCheck(filePaths, opts):
356
+ if filePaths.length == 0:
357
+ console.error(red("✗ No files specified"))
358
+ process.exit(1)
359
+
360
+ var totalErrors = 0
361
+ var totalWarnings = 0
362
+
363
+ for filePath in filePaths:
364
+ val abs = Path.resolve(filePath)
365
+ if not Fs.existsSync(abs):
366
+ console.error(red("✗ File not found: " + filePath))
367
+ totalErrors = totalErrors + 1
368
+ continue
369
+ val r = runCheck(abs)
370
+ if not r.ok: totalErrors = totalErrors + 1
371
+ totalWarnings = totalWarnings + r.warnings
372
+
373
+ console.log()
374
+ if totalErrors > 0:
375
+ console.error(red("✗ " + totalErrors + " file(s) with errors"))
376
+ process.exit(1)
377
+ else:
378
+ console.log(green("✓ All files OK") + (totalWarnings > 0 ? yellow(" (" + totalWarnings + " warning(s))") : ""))
379
+
380
+ // ── flux fmt ──────────────────────────────────────────────────
381
+ fn cmdFmt(filePaths, opts):
382
+ var changed = 0
383
+ for filePath in filePaths:
384
+ val abs = Path.resolve(filePath)
385
+ if not Fs.existsSync(abs):
386
+ console.error(red("✗ Not found: " + filePath))
387
+ continue
388
+ val source = Fs.readFileSync(abs, "utf8")
389
+ val formatted = format(source)
390
+ if formatted != source:
391
+ if not opts.stdout:
392
+ Fs.writeFileSync(abs, formatted, "utf8")
393
+ console.log(green("✓ ") + gray("Formatted ") + cyan(Path.relative(process.cwd(), abs)))
394
+ changed = changed + 1
395
+ else:
396
+ console.log(formatted)
397
+ else:
398
+ console.log(gray("○ ") + gray("No changes: ") + Path.relative(process.cwd(), abs))
399
+
400
+ if not opts.stdout and changed > 0:
401
+ console.log()
402
+ console.log(green("✓ " + changed + " file(s) formatted"))
403
+
404
+ // ── flux lint ─────────────────────────────────────────────────
405
+ fn cmdLint(filePaths, opts):
406
+ var hasErrors = false
407
+ for filePath in filePaths:
408
+ val abs = Path.resolve(filePath)
409
+ if not Fs.existsSync(abs):
410
+ console.error(red("✗ Not found: " + filePath))
411
+ continue
412
+ val source = Fs.readFileSync(abs, "utf8")
413
+ val result = lint(source)
414
+ val name = Path.relative(process.cwd(), abs)
415
+
416
+ if result.errors.length == 0 and result.warnings.length == 0:
417
+ console.log(green("✓ ") + cyan(name) + gray(" — clean"))
418
+ else:
419
+ for e in result.errors:
420
+ console.error(red(" error ") + cyan(name + ":" + (e.line ?? "?")) + " " + e.message)
421
+ hasErrors = true
422
+ for w in result.warnings:
423
+ console.warn(yellow(" warn ") + cyan(name + ":" + (w.line ?? "?")) + " " + w.message)
424
+
425
+ if hasErrors: process.exit(1)
426
+
427
+ // ── flux bundle ───────────────────────────────────────────────
428
+ fn cmdBundle(entryPath, opts):
429
+ val abs = Path.resolve(entryPath)
430
+ if not Fs.existsSync(abs):
431
+ console.error(red("✗ File not found: " + entryPath))
432
+ process.exit(1)
433
+
434
+ val outFile = opts.out ?? Path.join(Path.dirname(abs), Path.basename(abs, ".flux") + ".bundle.js")
435
+ val t0 = Date.now()
436
+ val result = bundle(abs)
437
+
438
+ if not result.success:
439
+ console.error(red("\n✗ Bundle failed:\n"))
440
+ for e in result.errors:
441
+ console.error(red(" " + e.message))
442
+ process.exit(1)
443
+
444
+ val elapsed = Date.now() - t0
445
+ if opts.stdout:
446
+ console.log(result.code)
447
+ return
448
+
449
+ Fs.writeFileSync(outFile, result.code, "utf8")
450
+ val kb = (result.code.length / 1024).toFixed(1)
451
+ console.log(
452
+ green("✓ Bundle done") + gray(" (" + elapsed + "ms) ") +
453
+ Path.basename(abs) + gray(" + " + (result.modules - 1) + " module(s) → ") +
454
+ cyan(Path.relative(process.cwd(), outFile)) + gray(" [" + kb + " KB]")
455
+ )
456
+
457
+ // ── flux watch ───────────────────────────────────────────────
458
+ fn cmdWatch(filePath, opts):
459
+ val abs = Path.resolve(filePath)
460
+ if not Fs.existsSync(abs):
461
+ console.error(red("✗ File not found: " + filePath))
462
+ process.exit(1)
463
+
464
+ console.log(cyan("◉ Watching ") + Path.relative(process.cwd(), abs) + gray(" (Ctrl+C to stop)\n"))
465
+ cmdCompile(filePath, opts)
466
+
467
+ var timer = null
468
+ fn onDebounce():
469
+ console.log(gray("\n" + new Date().toLocaleTimeString() + " — ") + blue("change detected"))
470
+ cmdCompile(filePath, opts)
471
+ fn onChange():
472
+ if timer: clearTimeout(timer)
473
+ timer = setTimeout(onDebounce, 80)
474
+ Fs.watch(abs, onChange)
475
+
476
+ // ── flux tokens ───────────────────────────────────────────────
477
+ fn cmdTokens(filePath, opts):
478
+ val { source } = readFluxFile(filePath)
479
+ val { Lexer } = require(Path.join(__dirname, "lexer.js"))
480
+ val lexer = new Lexer(source)
481
+ val tokens = lexer.tokenize()
482
+ console.log(gray("Tokens (" + tokens.length + "):\n"))
483
+ for tok in tokens:
484
+ val loc = tok.line ? gray(" " + tok.line + ":" + (tok.col ?? 1)) : ""
485
+ val val_ = tok.value != null ? cyan(" " + JSON.stringify(tok.value)) : ""
486
+ console.log(" " + yellow(tok.type.padEnd(16)) + val_ + loc)
487
+
488
+ // ── flux ast ──────────────────────────────────────────────────
489
+ fn cmdAst(filePath, opts):
490
+ val { source } = readFluxFile(filePath)
491
+ val result = transpile(source, {})
492
+ if not result.success:
493
+ printErrors(result.errors, source, filePath)
494
+ process.exit(1)
495
+ console.log(JSON.stringify(result.ast, null, 2))
496
+
497
+ // ── flux repl ─────────────────────────────────────────────────
498
+ fn cmdRepl(opts):
499
+ val readline = require("readline")
500
+ val rl = readline.createInterface({
501
+ input: process.stdin,
502
+ output: process.stdout,
503
+ terminal: true,
504
+ prompt: cyan("flux") + gray("> "),
505
+ })
506
+
507
+ console.log()
508
+ console.log(bold("Flux Lang Interactive REPL") + gray(" v" + VERSION + " [" + STAGE + "]"))
509
+ console.log(gray("Type Flux code to compile and run it. Ctrl+C or .exit to quit.\n"))
510
+ rl.prompt()
511
+
512
+ var multiLine = ""
513
+ var inBlock = false
514
+
515
+ fn onLine(rawLine):
516
+ val line = rawLine
517
+
518
+ if line.trim() == ".exit" or line.trim() == ".quit":
519
+ console.log(gray("\nBye!"))
520
+ process.exit(0)
521
+
522
+ if line.trim() == ".help":
523
+ console.log(gray(" .exit — quit"))
524
+ console.log(gray(" .clear — clear screen"))
525
+ console.log(gray(" .help — this message"))
526
+ rl.prompt()
527
+ return
528
+
529
+ if line.trim() == ".clear":
530
+ console.clear()
531
+ rl.prompt()
532
+ return
533
+
534
+ val needsContinue = line.trimEnd().endsWith(":") or inBlock
535
+
536
+ if needsContinue and line.trim() != "":
537
+ multiLine = multiLine + line + "\n"
538
+ inBlock = true
539
+ process.stdout.write(gray("... "))
540
+ return
541
+
542
+ val src = inBlock ? multiLine : line
543
+ multiLine = ""
544
+ inBlock = false
545
+
546
+ if not src.trim():
547
+ rl.prompt()
548
+ return
549
+
550
+ try:
551
+ val result = transpile(src, { check: false })
552
+ if result.success:
553
+ val tmpPath = Path.join(Os.tmpdir(), "_flux_repl_" + Date.now() + ".js")
554
+ Fs.writeFileSync(tmpPath, result.output, "utf8")
555
+ try:
556
+ val out = require(tmpPath)
557
+ if out != undefined: console.log(green("← ") + JSON.stringify(out))
558
+ catch(e):
559
+ console.error(red("Runtime: ") + e.message)
560
+ finally:
561
+ try: Fs.unlinkSync(tmpPath)
562
+ catch(e2): null
563
+ else:
564
+ for e in result.errors:
565
+ console.error(red("✗ ") + e.message)
566
+ catch(e3):
567
+ console.error(red("Error: ") + e3.message)
568
+
569
+ rl.prompt()
570
+
571
+ fn onClose():
572
+ console.log(gray("\nBye!"))
573
+ process.exit(0)
574
+
575
+ rl.on("line", onLine)
576
+ rl.on("close", onClose)
577
+
578
+ // ── flux init ────────────────────────────────────────────────
579
+ fn cmdInit(name, opts):
580
+ val projectName = name ?? "my-flux-app"
581
+ val dir = Path.resolve(projectName)
582
+
583
+ if Fs.existsSync(dir):
584
+ console.error(red("✗ Directory already exists: " + projectName))
585
+ process.exit(1)
586
+
587
+ Fs.mkdirSync(dir, { recursive: true })
588
+ Fs.mkdirSync(Path.join(dir, "src"), { recursive: true })
589
+ Fs.mkdirSync(Path.join(dir, "tests"), { recursive: true })
590
+
591
+ val mainFlux = `// {projectName} — written in Flux Lang v{VERSION}
592
+
593
+ type Shape =
594
+ | Circle(radius: Float)
595
+ | Rect(width: Float, height: Float)
596
+ | Triangle(base: Float, height: Float)
597
+
598
+ fn area(shape: Shape) -> Float:
599
+ match shape:
600
+ when Circle(r): return Math.PI * r * r
601
+ when Rect(w, h): return w * h
602
+ when Triangle(b, h): return 0.5 * b * h
603
+
604
+ fn greet(name: String) -> String:
605
+ return "Hello from Flux, {name}!"
606
+
607
+ val shapes = [
608
+ Circle(5.0),
609
+ Rect(10.0, 4.0),
610
+ Triangle(6.0, 8.0),
611
+ ]
612
+
613
+ for shape in shapes:
614
+ val a = area(shape)
615
+ print("Area: {a:.2f}")
616
+
617
+ print(greet("{projectName}"))
618
+ `
619
+
620
+ val testFlux = `// {projectName} tests
621
+
622
+ fn add(a: Int, b: Int) -> Int: return a + b
623
+ fn mul(a: Int, b: Int) -> Int: return a * b
624
+
625
+ test "add works":
626
+ assert add(1, 2) == 3
627
+ assert add(0, 0) == 0
628
+ assert add(-1, 1) == 0
629
+
630
+ test "mul works":
631
+ assert mul(3, 4) == 12
632
+ assert mul(0, 5) == 0
633
+ `
634
+
635
+ val fluxJson = {
636
+ name: projectName,
637
+ version: "1.0.0",
638
+ description: "A Flux Lang project",
639
+ author: "",
640
+ license: "MIT",
641
+ entry: "src/main.flux",
642
+ outDir: "dist",
643
+ sourcemap: false,
644
+ typecheck: true,
645
+ scripts: {
646
+ start: "flux run src/main.flux",
647
+ build: "flux bundle src/main.flux -o dist/bundle.js",
648
+ dev: "flux watch src/main.flux",
649
+ check: "flux check src/main.flux",
650
+ test: "flux test tests/",
651
+ fmt: "flux fmt src/",
652
+ lint: "flux lint src/",
653
+ },
654
+ dependencies: {},
655
+ devDependencies: {},
656
+ }
657
+
658
+ val gitignore = `node_modules/
659
+ dist/
660
+ flux_modules/
661
+ *.js.map
662
+ .DS_Store
663
+ `
664
+
665
+ val readme = `# {projectName}
666
+
667
+ A project built with [Flux Lang](https://flux-lang.dev) v{VERSION}.
668
+
669
+ ## Getting Started
670
+
671
+ \`\`\`bash
672
+ flux run src/main.flux
673
+ \`\`\`
674
+
675
+ ## Commands
676
+
677
+ | Command | Description |
678
+ |---|---|
679
+ | \`flux run src/main.flux\` | Run main file |
680
+ | \`flux build\` | Bundle to dist/ |
681
+ | \`flux watch src/main.flux\` | Watch mode |
682
+ | \`flux check src/main.flux\` | Type check |
683
+ | \`flux test tests/\` | Run tests |
684
+ | \`flux fmt src/\` | Format code |
685
+ `
686
+
687
+ Fs.writeFileSync(Path.join(dir, "src", "main.flux"), mainFlux, "utf8")
688
+ Fs.writeFileSync(Path.join(dir, "tests", "main.test.flux"), testFlux, "utf8")
689
+ Fs.writeFileSync(Path.join(dir, "flux.json"), JSON.stringify(fluxJson, null, 2) + "\n", "utf8")
690
+ Fs.writeFileSync(Path.join(dir, ".gitignore"), gitignore, "utf8")
691
+ Fs.writeFileSync(Path.join(dir, "README.md"), readme, "utf8")
692
+
693
+ console.log()
694
+ console.log(green("✓ Created: ") + bold(projectName + "/"))
695
+ console.log()
696
+ console.log(gray(" Files:"))
697
+ console.log(" " + cyan(projectName + "/src/main.flux"))
698
+ console.log(" " + cyan(projectName + "/tests/main.test.flux"))
699
+ console.log(" " + cyan(projectName + "/flux.json"))
700
+ console.log(" " + cyan(projectName + "/.gitignore"))
701
+ console.log(" " + cyan(projectName + "/README.md"))
702
+ console.log()
703
+ console.log(bold(" Next steps:"))
704
+ console.log(" " + yellow("cd " + projectName))
705
+ console.log(" " + yellow("flux run src/main.flux"))
706
+ console.log()
707
+
708
+ // ── flux self-hosted ──────────────────────────────────────────
709
+ fn cmdSelfHosted(sub, opts):
710
+ val SELF = Path.join(__dirname, ".")
711
+
712
+ val coreModules = [
713
+ "css-preprocessor", "checker", "type-checker",
714
+ "jsx", "lexer", "parser", "codegen", "transpiler",
715
+ ]
716
+ val extModules = [
717
+ "formatter", "sourcemap", "stdlib", "mangler",
718
+ "linter", "bundler", "test-runner",
719
+ ]
720
+ val newModules = ["config", "pkg", "cli"]
721
+ val allModules = [...coreModules, ...extModules, ...newModules]
722
+
723
+ if sub == "build":
724
+ val { execSync } = require("child_process")
725
+ val BIN = Path.join(__dirname, "../../bin/flux.js")
726
+ console.log()
727
+ console.log(bold("⚡ Flux Bootstrap — Stage 0"))
728
+ console.log(gray(" Compiling self-hosted sources with stage-0 compiler...\n"))
729
+
730
+ var ok = 0
731
+ var failed = 0
732
+ for name in allModules:
733
+ val src = Path.join(SELF, name + ".flux")
734
+ val out = Path.join(SELF, name + ".js")
735
+ if not Fs.existsSync(src):
736
+ console.log(gray(" ○ " + name + ".flux (skipped — not found)"))
737
+ continue
738
+ try:
739
+ val cmd = `node "${BIN}" compile "${src}" -o "${out}" --no-mangle`
740
+ execSync(cmd, { cwd: Path.join(__dirname, "../.."), stdio: "pipe" })
741
+ console.log(green(" ✓ ") + name + ".flux → " + gray(name + ".js"))
742
+ ok = ok + 1
743
+ catch(e):
744
+ console.error(red(" ✗ ") + name + ".flux — " + e.message.split("\n")[0])
745
+ failed = failed + 1
746
+
747
+ console.log()
748
+ if failed == 0:
749
+ console.log(green("✓ Bootstrap complete! ") + gray(ok + " modules compiled"))
750
+ console.log()
751
+ console.log(" Activate self-hosted mode:")
752
+ console.log(" " + yellow("FLUX_SELF_HOSTED=1 flux <command>"))
753
+ else:
754
+ console.log(red("✗ " + failed + " module(s) failed, " + ok + " succeeded"))
755
+ console.log()
756
+ return
757
+
758
+ if sub == "verify":
759
+ console.log(cyan("\n Verifying self-hosted compiler output...\n"))
760
+ try:
761
+ val selfMod = require(Path.join(SELF, "transpiler.js"))
762
+ val stage0Mod = require(Path.join(__dirname, "../transpiler.js"))
763
+ val testSrc = `fn greet(name): return "Hello, {name}!"\nval msg = greet("Flux")`
764
+ val r0 = stage0Mod.transpile(testSrc, {})
765
+ val r1 = selfMod.transpile(testSrc, {})
766
+ fn norm(s): return s.replace(/\/\/.*/g, "").replace(/\s+/g, " ").trim()
767
+ if norm(r0.output) == norm(r1.output):
768
+ console.log(green("✓ Self-hosted output matches stage-0!"))
769
+ console.log(green("✓ Flux is fully self-hosting."))
770
+ else:
771
+ console.log(yellow("⚠ Outputs differ (minor differences are OK)"))
772
+ console.log(gray(" Stage-0: ") + r0.output.split("\n")[0])
773
+ console.log(gray(" Self-hosted:") + r1.output.split("\n")[0])
774
+ catch(e):
775
+ console.error(red("✗ Verify failed: " + e.message))
776
+ console.log()
777
+ return
778
+
779
+ // Default: show status
780
+ console.log()
781
+ console.log(bold(" Flux Self-Hosted Compiler Status\n"))
782
+ val selfActive = process.env.FLUX_SELF_HOSTED == "1"
783
+ if selfActive:
784
+ console.log(" Mode: " + green("● ACTIVE") + gray(" (using self-hosted compiler)"))
785
+ else:
786
+ console.log(" Mode: " + gray("○ INACTIVE") + " " + gray("(using stage-0 compiler)"))
787
+ console.log(" Toggle: " + yellow("FLUX_SELF_HOSTED=1 flux <command>"))
788
+ console.log(" Build: " + yellow("flux self-hosted build"))
789
+
790
+ console.log()
791
+ console.log(bold(" Core Pipeline (" + coreModules.length + " modules)"))
792
+ for name in coreModules:
793
+ val jsPath = Path.join(SELF, name + ".js")
794
+ val flxPath = Path.join(SELF, name + ".flux")
795
+ val hasJs = Fs.existsSync(jsPath)
796
+ val hasFlx = Fs.existsSync(flxPath)
797
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
798
+ val sym = hasJs ? green("✓") : red("✗")
799
+ val note = not hasFlx ? gray(" (no .flux source)") : ""
800
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + gray(kb + " KB") + note)
801
+
802
+ console.log()
803
+ console.log(bold(" Extended Toolchain (" + extModules.length + " modules)"))
804
+ for name in extModules:
805
+ val jsPath = Path.join(SELF, name + ".js")
806
+ val flxPath = Path.join(SELF, name + ".flux")
807
+ val hasJs = Fs.existsSync(jsPath)
808
+ val hasFlx = Fs.existsSync(flxPath)
809
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
810
+ val sym = hasJs ? green("✓") : red("✗")
811
+ val note = not hasFlx ? gray(" (no .flux source)") : ""
812
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + gray(kb + " KB") + note)
813
+
814
+ console.log()
815
+ console.log(bold(" Ecosystem (" + newModules.length + " modules)"))
816
+ for name in newModules:
817
+ val jsPath = Path.join(SELF, name + ".js")
818
+ val flxPath = Path.join(SELF, name + ".flux")
819
+ val hasJs = Fs.existsSync(jsPath)
820
+ val hasFlx = Fs.existsSync(flxPath)
821
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
822
+ val sym = hasJs ? green("✓") : yellow("○")
823
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + (hasJs ? gray(kb + " KB") : yellow("not built")))
824
+
825
+ console.log()
826
+
827
+ // ── flux version ─────────────────────────────────────────────
828
+ fn cmdVersion(opts):
829
+ if noColor:
830
+ console.log("flux-lang v" + VERSION)
831
+ return
832
+ console.log(cyan(bold("⚡ Flux Lang")) + gray(" v" + VERSION + " [" + STAGE + "]"))
833
+
834
+ // ══════════════════════════════════════════════════════════════
835
+ // Main entry point
836
+ // ══════════════════════════════════════════════════════════════
837
+
838
+ fn main():
839
+ val { positional, opts } = parseArgs(process.argv)
840
+ val cmd = positional[0] ?? "help"
841
+
842
+ match cmd:
843
+ when "compile": cmdCompile(positional[1], opts)
844
+ when "run": cmdRun(positional[1], opts)
845
+ when "check": cmdCheck(positional.slice(1), opts)
846
+ when "fmt": cmdFmt(positional.slice(1), opts)
847
+ when "format": cmdFmt(positional.slice(1), opts)
848
+ when "lint": cmdLint(positional.slice(1), opts)
849
+ when "bundle": cmdBundle(positional[1], opts)
850
+ when "watch": cmdWatch(positional[1], opts)
851
+ when "tokens": cmdTokens(positional[1], opts)
852
+ when "ast": cmdAst(positional[1], opts)
853
+ when "repl": cmdRepl(opts)
854
+ when "init": cmdInit(positional[1], opts)
855
+ when "add": cmdAdd(positional.slice(1), opts)
856
+ when "remove": cmdRemove(positional.slice(1), opts)
857
+ when "rm": cmdRemove(positional.slice(1), opts)
858
+ when "install": cmdInstall(opts)
859
+ when "i": cmdInstall(opts)
860
+ when "list": cmdList(opts)
861
+ when "ls": cmdList(opts)
862
+ when "search": cmdSearch(positional[1], opts)
863
+ when "info": cmdInfo(positional[1], opts)
864
+ when "publish": cmdPublish(opts)
865
+ when "self-hosted": cmdSelfHosted(positional[1], opts)
866
+ when "version": cmdVersion(opts)
867
+ when "-v": cmdVersion(opts)
868
+ when "--version": cmdVersion(opts)
869
+ when "help": showHelp()
870
+ when "--help": showHelp()
871
+ when "-h": showHelp()
872
+ when _:
873
+ console.error(red("✗ Unknown command: " + cmd))
874
+ console.error(gray(" Run: flux help"))
875
+ process.exit(1)
876
+
877
+ main()