@xnoxs/flux-lang 3.3.4 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,996 @@
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} — built with Flux Lang v{VERSION}
592
+ // Run: flux run src/main.flux
593
+
594
+ // ── Algebraic Data Types + Pattern Matching ───────────────────
595
+ type Shape = Circle(radius) | Rect(width, height) | Triangle(base, height)
596
+
597
+ fn area(shape):
598
+ match shape:
599
+ when Circle(r): return Math.PI * r * r
600
+ when Rect(w, h): return w * h
601
+ when Triangle(b, h): return 0.5 * b * h
602
+
603
+ fn describe(shape):
604
+ match shape:
605
+ when Circle(r): return "Circle(r={r:.2f})"
606
+ when Rect(w, h): return "Rect({w:.1f}x{h:.1f})"
607
+ when Triangle(b, h): return "Triangle(b={b:.1f}, h={h:.1f})"
608
+
609
+ // ── Result type ───────────────────────────────────────────────
610
+ type Result = Ok(value) | Err(message)
611
+
612
+ fn safeDivide(a, b):
613
+ if b == 0: return Err("division by zero")
614
+ return Ok(a / b)
615
+
616
+ // ── Utility functions ─────────────────────────────────────────
617
+ fn greet(name): return "Hello from Flux, {name}!"
618
+
619
+ fn formatList(items):
620
+ if items.length == 0: return "(empty)"
621
+ return "[" + items.join(", ") + "]"
622
+
623
+ fn clamp(value, lo, hi):
624
+ if value < lo: return lo
625
+ if value > hi: return hi
626
+ return value
627
+
628
+ // ── Pipe operator + stdlib ────────────────────────────────────
629
+ val numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
630
+ val processed = numbers
631
+ |> filter(n -> n > 3)
632
+ |> map(n -> n * n)
633
+ |> sort
634
+
635
+ // ── Main ──────────────────────────────────────────────────────
636
+ print(greet("{projectName}"))
637
+ print("Squares > 3: {formatList(processed)}")
638
+ print("clamp(12, 0, 10) = {clamp(12, 0, 10)}")
639
+
640
+ val shapes = [Circle(5.0), Rect(8.0, 3.0), Triangle(6.0, 4.0)]
641
+ for shape in shapes:
642
+ val a = area(shape)
643
+ print("{describe(shape)} area={a:.2f}")
644
+
645
+ match safeDivide(10.0, 3.0):
646
+ when Ok(v): print("10 / 3 = {v:.4f}")
647
+ when Err(e): print("Error: {e}")
648
+ `
649
+
650
+ val utilsFlux = `// Utility functions for {projectName}
651
+ // Use with: flux bundle src/main.flux (bundles imports automatically)
652
+
653
+ export fn greet(name):
654
+ return "Hello from Flux, {name}!"
655
+
656
+ export fn formatList(items):
657
+ if items.length == 0: return "(empty)"
658
+ return "[" + items.join(", ") + "]"
659
+
660
+ export fn clamp(value, lo, hi):
661
+ if value < lo: return lo
662
+ if value > hi: return hi
663
+ return value
664
+
665
+ export fn sum(nums):
666
+ return nums.reduce((acc, x) -> acc + x, 0)
667
+
668
+ export fn average(nums):
669
+ if nums.length == 0: return 0
670
+ return sum(nums) / nums.length
671
+ `
672
+
673
+ val testFlux = `// Tests for {projectName}
674
+ // Run: flux test tests/
675
+
676
+ // ── Functions under test ──────────────────────────────────────
677
+ fn add(a, b):
678
+ return a + b
679
+
680
+ fn mul(a, b):
681
+ return a * b
682
+
683
+ fn clamp(value, lo, hi):
684
+ if value < lo: return lo
685
+ if value > hi: return hi
686
+ return value
687
+
688
+ fn average(nums):
689
+ if nums.length == 0: return 0
690
+ return nums.reduce((s, x) -> s + x, 0) / nums.length
691
+
692
+ type Result = Ok(value) | Err(message)
693
+
694
+ fn safeDivide(a, b):
695
+ if b == 0: return Err("div by zero")
696
+ return Ok(a / b)
697
+
698
+ // ── Test functions ────────────────────────────────────────────
699
+ fn test_add():
700
+ assert(add(1, 2) == 3, "1+2=3")
701
+ assert(add(0, 0) == 0, "0+0=0")
702
+ assert(add(-1, 1) == 0, "-1+1=0")
703
+
704
+ fn test_mul():
705
+ assert(mul(3, 4) == 12, "3*4=12")
706
+ assert(mul(0, 5) == 0, "0*5=0")
707
+
708
+ fn test_clamp():
709
+ assert(clamp(5, 0, 10) == 5, "within range")
710
+ assert(clamp(-1, 0, 10) == 0, "below min")
711
+ assert(clamp(15, 0, 10) == 10, "above max")
712
+
713
+ fn test_average():
714
+ assert(average([2, 4, 6]) == 4, "average of 2,4,6")
715
+ assert(average([]) == 0, "empty list")
716
+
717
+ fn test_safe_divide():
718
+ val ok = safeDivide(10, 2)
719
+ match ok:
720
+ when Ok(v): assert(v == 5, "10/2=5")
721
+ when Err(e): assert(false, "unexpected error")
722
+ val err = safeDivide(10, 0)
723
+ match err:
724
+ when Ok(v): assert(false, "expected error")
725
+ when Err(e): assert(e == "div by zero", "error message")
726
+
727
+ fn test_pipe_operator():
728
+ val result = [1, 2, 3, 4, 5] |> filter(n -> n > 2) |> map(n -> n * 2)
729
+ assert(result.length == 3, "filter length")
730
+ assert(result[0] == 6, "first element")
731
+ assert(result[2] == 10, "last element")
732
+ `
733
+
734
+ val fluxJson = {
735
+ name: projectName,
736
+ version: "1.0.0",
737
+ description: "A Flux Lang v" + VERSION + " project",
738
+ author: "",
739
+ license: "MIT",
740
+ entry: "src/main.flux",
741
+ outDir: "dist",
742
+ sourcemap: false,
743
+ typecheck: true,
744
+ scripts: {
745
+ start: "flux run src/main.flux",
746
+ build: "flux bundle src/main.flux -o dist/bundle.js",
747
+ dev: "flux watch src/main.flux",
748
+ check: "flux check src/main.flux",
749
+ test: "flux test tests/",
750
+ fmt: "flux fmt src/",
751
+ lint: "flux lint src/",
752
+ },
753
+ dependencies: {},
754
+ devDependencies: { "@xnoxs/flux-lang": "^" + VERSION },
755
+ }
756
+
757
+ val gitignore = "node_modules/\ndist/\nflux_modules/\n*.js.map\n.DS_Store\n"
758
+
759
+ val readme = `# {projectName}
760
+
761
+ A project built with [Flux Lang](https://flux-lang.dev) v{VERSION} — the self-hosted compiler.
762
+
763
+ ## Quick Start
764
+
765
+ \`\`\`bash
766
+ flux run src/main.flux
767
+ \`\`\`
768
+
769
+ ## Project Structure
770
+
771
+ \`\`\`
772
+ {projectName}/
773
+ ├── src/
774
+ │ ├── main.flux # Entry point
775
+ │ └── utils.flux # Utility functions
776
+ ├── tests/
777
+ │ └── main.test.flux # Test suite
778
+ ├── flux.json # Project config
779
+ └── README.md
780
+ \`\`\`
781
+
782
+ ## Commands
783
+
784
+ | Command | Description |
785
+ |---|---|
786
+ | \`flux run src/main.flux\` | Run the project |
787
+ | \`flux bundle src/main.flux -o dist/bundle.js\` | Bundle to single file |
788
+ | \`flux watch src/main.flux\` | Watch mode |
789
+ | \`flux check src/main.flux\` | Type check + static analysis |
790
+ | \`flux test tests/\` | Run all tests |
791
+ | \`flux fmt src/\` | Format source code |
792
+ | \`flux lint src/\` | Lint for issues |
793
+ | \`flux add <package>\` | Add a dependency |
794
+ `
795
+
796
+ val allFiles = [
797
+ projectName + "/src/main.flux",
798
+ projectName + "/src/utils.flux",
799
+ projectName + "/tests/main.test.flux",
800
+ projectName + "/flux.json",
801
+ projectName + "/.gitignore",
802
+ projectName + "/README.md",
803
+ ]
804
+
805
+ Fs.writeFileSync(Path.join(dir, "src", "main.flux"), mainFlux, "utf8")
806
+ Fs.writeFileSync(Path.join(dir, "src", "utils.flux"), utilsFlux, "utf8")
807
+ Fs.writeFileSync(Path.join(dir, "tests", "main.test.flux"), testFlux, "utf8")
808
+ Fs.writeFileSync(Path.join(dir, "flux.json"), JSON.stringify(fluxJson, null, 2) + "\n", "utf8")
809
+ Fs.writeFileSync(Path.join(dir, ".gitignore"), gitignore, "utf8")
810
+ Fs.writeFileSync(Path.join(dir, "README.md"), readme, "utf8")
811
+
812
+ console.log()
813
+ console.log(green("✓ Created: ") + bold(projectName + "/"))
814
+ console.log()
815
+ console.log(gray(" Files:"))
816
+ for f in allFiles:
817
+ console.log(" " + cyan(f))
818
+ console.log()
819
+ console.log(bold(" Next steps:"))
820
+ console.log(" " + yellow("cd " + projectName))
821
+ console.log(" " + yellow("flux run src/main.flux"))
822
+ console.log()
823
+ console.log(gray(" Or run the test suite:"))
824
+ console.log(" " + yellow("flux test tests/"))
825
+ console.log()
826
+
827
+ // ── flux self-hosted ──────────────────────────────────────────
828
+ fn cmdSelfHosted(sub, opts):
829
+ val SELF = Path.join(__dirname, ".")
830
+
831
+ val coreModules = [
832
+ "css-preprocessor", "checker", "type-checker",
833
+ "jsx", "lexer", "parser", "codegen", "transpiler",
834
+ ]
835
+ val extModules = [
836
+ "formatter", "sourcemap", "stdlib", "mangler",
837
+ "linter", "bundler", "test-runner",
838
+ ]
839
+ val newModules = ["config", "pkg", "cli"]
840
+ val allModules = [...coreModules, ...extModules, ...newModules]
841
+
842
+ if sub == "build":
843
+ val { execSync } = require("child_process")
844
+ val BIN = Path.join(__dirname, "../../bin/flux.js")
845
+ console.log()
846
+ console.log(bold("⚡ Flux Bootstrap — Stage 0"))
847
+ console.log(gray(" Compiling self-hosted sources with stage-0 compiler...\n"))
848
+
849
+ var ok = 0
850
+ var failed = 0
851
+ for name in allModules:
852
+ val src = Path.join(SELF, name + ".flux")
853
+ val out = Path.join(SELF, name + ".js")
854
+ if not Fs.existsSync(src):
855
+ console.log(gray(" ○ " + name + ".flux (skipped — not found)"))
856
+ continue
857
+ try:
858
+ val cmd = `node "${BIN}" compile "${src}" -o "${out}" --no-mangle`
859
+ execSync(cmd, { cwd: Path.join(__dirname, "../.."), stdio: "pipe" })
860
+ console.log(green(" ✓ ") + name + ".flux → " + gray(name + ".js"))
861
+ ok = ok + 1
862
+ catch(e):
863
+ console.error(red(" ✗ ") + name + ".flux — " + e.message.split("\n")[0])
864
+ failed = failed + 1
865
+
866
+ console.log()
867
+ if failed == 0:
868
+ console.log(green("✓ Bootstrap complete! ") + gray(ok + " modules compiled"))
869
+ console.log()
870
+ console.log(" Activate self-hosted mode:")
871
+ console.log(" " + yellow("FLUX_SELF_HOSTED=1 flux <command>"))
872
+ else:
873
+ console.log(red("✗ " + failed + " module(s) failed, " + ok + " succeeded"))
874
+ console.log()
875
+ return
876
+
877
+ if sub == "verify":
878
+ console.log(cyan("\n Verifying self-hosted compiler output...\n"))
879
+ try:
880
+ val selfMod = require(Path.join(SELF, "transpiler.js"))
881
+ val stage0Mod = require(Path.join(__dirname, "../transpiler.js"))
882
+ val testSrc = `fn greet(name): return "Hello, {name}!"\nval msg = greet("Flux")`
883
+ val r0 = stage0Mod.transpile(testSrc, {})
884
+ val r1 = selfMod.transpile(testSrc, {})
885
+ fn norm(s): return s.replace(/\/\/.*/g, "").replace(/\s+/g, " ").trim()
886
+ if norm(r0.output) == norm(r1.output):
887
+ console.log(green("✓ Self-hosted output matches stage-0!"))
888
+ console.log(green("✓ Flux is fully self-hosting."))
889
+ else:
890
+ console.log(yellow("⚠ Outputs differ (minor differences are OK)"))
891
+ console.log(gray(" Stage-0: ") + r0.output.split("\n")[0])
892
+ console.log(gray(" Self-hosted:") + r1.output.split("\n")[0])
893
+ catch(e):
894
+ console.error(red("✗ Verify failed: " + e.message))
895
+ console.log()
896
+ return
897
+
898
+ // Default: show status
899
+ console.log()
900
+ console.log(bold(" Flux Self-Hosted Compiler Status\n"))
901
+ val selfActive = process.env.FLUX_SELF_HOSTED == "1"
902
+ if selfActive:
903
+ console.log(" Mode: " + green("● ACTIVE") + gray(" (using self-hosted compiler)"))
904
+ else:
905
+ console.log(" Mode: " + gray("○ INACTIVE") + " " + gray("(using stage-0 compiler)"))
906
+ console.log(" Toggle: " + yellow("FLUX_SELF_HOSTED=1 flux <command>"))
907
+ console.log(" Build: " + yellow("flux self-hosted build"))
908
+
909
+ console.log()
910
+ console.log(bold(" Core Pipeline (" + coreModules.length + " modules)"))
911
+ for name in coreModules:
912
+ val jsPath = Path.join(SELF, name + ".js")
913
+ val flxPath = Path.join(SELF, name + ".flux")
914
+ val hasJs = Fs.existsSync(jsPath)
915
+ val hasFlx = Fs.existsSync(flxPath)
916
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
917
+ val sym = hasJs ? green("✓") : red("✗")
918
+ val note = not hasFlx ? gray(" (no .flux source)") : ""
919
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + gray(kb + " KB") + note)
920
+
921
+ console.log()
922
+ console.log(bold(" Extended Toolchain (" + extModules.length + " modules)"))
923
+ for name in extModules:
924
+ val jsPath = Path.join(SELF, name + ".js")
925
+ val flxPath = Path.join(SELF, name + ".flux")
926
+ val hasJs = Fs.existsSync(jsPath)
927
+ val hasFlx = Fs.existsSync(flxPath)
928
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
929
+ val sym = hasJs ? green("✓") : red("✗")
930
+ val note = not hasFlx ? gray(" (no .flux source)") : ""
931
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + gray(kb + " KB") + note)
932
+
933
+ console.log()
934
+ console.log(bold(" Ecosystem (" + newModules.length + " modules)"))
935
+ for name in newModules:
936
+ val jsPath = Path.join(SELF, name + ".js")
937
+ val flxPath = Path.join(SELF, name + ".flux")
938
+ val hasJs = Fs.existsSync(jsPath)
939
+ val hasFlx = Fs.existsSync(flxPath)
940
+ val kb = hasJs ? (Fs.statSync(jsPath).size / 1024).toFixed(1) : "?"
941
+ val sym = hasJs ? green("✓") : yellow("○")
942
+ console.log(" " + sym + " " + (name + ".flux").padEnd(26) + (hasJs ? gray(kb + " KB") : yellow("not built")))
943
+
944
+ console.log()
945
+
946
+ // ── flux version ─────────────────────────────────────────────
947
+ fn cmdVersion(opts):
948
+ if noColor:
949
+ console.log("flux-lang v" + VERSION)
950
+ return
951
+ console.log(cyan(bold("⚡ Flux Lang")) + gray(" v" + VERSION + " [" + STAGE + "]"))
952
+
953
+ // ══════════════════════════════════════════════════════════════
954
+ // Main entry point
955
+ // ══════════════════════════════════════════════════════════════
956
+
957
+ fn main():
958
+ val { positional, opts } = parseArgs(process.argv)
959
+ val cmd = positional[0] ?? "help"
960
+
961
+ match cmd:
962
+ when "compile": cmdCompile(positional[1], opts)
963
+ when "run": cmdRun(positional[1], opts)
964
+ when "check": cmdCheck(positional.slice(1), opts)
965
+ when "fmt": cmdFmt(positional.slice(1), opts)
966
+ when "format": cmdFmt(positional.slice(1), opts)
967
+ when "lint": cmdLint(positional.slice(1), opts)
968
+ when "bundle": cmdBundle(positional[1], opts)
969
+ when "watch": cmdWatch(positional[1], opts)
970
+ when "tokens": cmdTokens(positional[1], opts)
971
+ when "ast": cmdAst(positional[1], opts)
972
+ when "repl": cmdRepl(opts)
973
+ when "init": cmdInit(positional[1], opts)
974
+ when "add": cmdAdd(positional.slice(1), opts)
975
+ when "remove": cmdRemove(positional.slice(1), opts)
976
+ when "rm": cmdRemove(positional.slice(1), opts)
977
+ when "install": cmdInstall(opts)
978
+ when "i": cmdInstall(opts)
979
+ when "list": cmdList(opts)
980
+ when "ls": cmdList(opts)
981
+ when "search": cmdSearch(positional[1], opts)
982
+ when "info": cmdInfo(positional[1], opts)
983
+ when "publish": cmdPublish(opts)
984
+ when "self-hosted": cmdSelfHosted(positional[1], opts)
985
+ when "version": cmdVersion(opts)
986
+ when "-v": cmdVersion(opts)
987
+ when "--version": cmdVersion(opts)
988
+ when "help": showHelp()
989
+ when "--help": showHelp()
990
+ when "-h": showHelp()
991
+ when _:
992
+ console.error(red("✗ Unknown command: " + cmd))
993
+ console.error(gray(" Run: flux help"))
994
+ process.exit(1)
995
+
996
+ main()