@flux-lang/cli 0.1.5 → 0.1.6-canary.18d439adc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/flux.js CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { stdin as nodeStdin } from "node:process";
5
- import { createRuntime, parseDocument, initRuntimeState, checkDocument } from "@flux-lang/core";
3
+ import fs from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { parseCommand, checkCommand, renderCommand, tickCommand, stepCommand, viewCommand, pdfCommand, configCommand, newCommand, addCommand, updateRecents, resolveConfig, formatCommand, } from "@flux-lang/cli-core";
6
6
  import { runViewer } from "../view/runViewer.js";
7
- const VERSION = "0.1.0";
8
- // Entry
7
+ import { createRuntime, parseDocument } from "@flux-lang/core";
8
+ import { shouldLaunchUi } from "../ui-routing.js";
9
+ import { computeBuildId, defaultEmbeddedDir, VIEWER_VERSION as VIEWER_PACKAGE_VERSION, } from "@flux-lang/viewer";
10
+ import { FLUX_CLI_VERSION } from "../version.js";
11
+ const CLI_VERSION = FLUX_CLI_VERSION;
9
12
  void (async () => {
10
13
  try {
11
14
  const code = await main(process.argv.slice(2));
12
- if (code !== 0) {
15
+ if (code !== 0)
13
16
  process.exit(code);
14
- }
15
17
  }
16
18
  catch (error) {
17
19
  const msg = error?.message ?? String(error);
@@ -20,69 +22,227 @@ void (async () => {
20
22
  }
21
23
  })();
22
24
  async function main(argv) {
23
- if (argv.length === 0) {
24
- printGlobalHelp();
25
+ const parsed = parseGlobalArgs(argv);
26
+ const args = parsed.args;
27
+ const uiEnabled = shouldLaunchUi({
28
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
29
+ stdinIsTTY: process.stdin.isTTY,
30
+ json: parsed.json,
31
+ noUi: parsed.noUi,
32
+ env: process.env,
33
+ });
34
+ if (uiEnabled) {
35
+ return launchUi({
36
+ cwd: process.cwd(),
37
+ initialArgs: args,
38
+ detach: parsed.detach,
39
+ helpCommand: parsed.help ? args[0] : undefined,
40
+ version: parsed.version ? `flux v${CLI_VERSION}` : undefined,
41
+ });
42
+ }
43
+ if (parsed.version) {
44
+ await printVersion(parsed.json);
25
45
  return 0;
26
46
  }
27
- // Global --help / -h
28
- if (argv.includes("-h") || argv.includes("--help")) {
29
- const idx = argv.findIndex((a) => a === "-h" || a === "--help");
30
- // No subcommand yet → global
31
- if (idx === 0) {
47
+ if (parsed.help) {
48
+ if (args.length === 0) {
32
49
  printGlobalHelp();
33
50
  return 0;
34
51
  }
35
- const cmd = argv[0];
36
- if (cmd === "parse") {
37
- printParseHelp();
38
- }
39
- else if (cmd === "check") {
40
- printCheckHelp();
41
- }
42
- else {
43
- printGlobalHelp();
44
- }
52
+ printCommandHelp(args[0]);
45
53
  return 0;
46
54
  }
47
- // Global --version / -v
48
- if (argv.includes("-v") || argv.includes("--version")) {
49
- console.log(`flux v${VERSION}`);
55
+ if (args.length === 0) {
56
+ printGlobalHelp();
50
57
  return 0;
51
58
  }
52
- const [cmd, ...rest] = argv;
53
- if (cmd === "parse") {
54
- return runParse(rest);
59
+ const [cmd, ...rest] = args;
60
+ switch (cmd) {
61
+ case "parse":
62
+ return runParse(rest);
63
+ case "check":
64
+ return runCheck(rest, parsed);
65
+ case "render":
66
+ return runRender(rest);
67
+ case "fmt":
68
+ return runFormat(rest, parsed);
69
+ case "tick":
70
+ return runTick(rest);
71
+ case "step":
72
+ return runStep(rest);
73
+ case "view":
74
+ return runView(rest, parsed);
75
+ case "edit":
76
+ return runEdit(rest, parsed);
77
+ case "pdf":
78
+ return runPdf(rest, parsed);
79
+ case "config":
80
+ return runConfig(rest, parsed);
81
+ case "new":
82
+ return runNew(rest, parsed);
83
+ case "add":
84
+ return runAdd(rest, parsed);
85
+ default:
86
+ console.error(`Unknown command '${cmd}'.`);
87
+ printGlobalHelp();
88
+ return 1;
89
+ }
90
+ }
91
+ function isInteractive() {
92
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
93
+ }
94
+ function parseGlobalArgs(argv) {
95
+ const flags = {
96
+ help: false,
97
+ version: false,
98
+ noUi: false,
99
+ ui: false,
100
+ detach: false,
101
+ json: false,
102
+ quiet: false,
103
+ verbose: false,
104
+ };
105
+ const args = [];
106
+ let passthrough = false;
107
+ for (let i = 0; i < argv.length; i += 1) {
108
+ const arg = argv[i];
109
+ if (arg === "--") {
110
+ passthrough = true;
111
+ continue;
112
+ }
113
+ if (!passthrough) {
114
+ if (arg === "-h" || arg === "--help") {
115
+ flags.help = true;
116
+ continue;
117
+ }
118
+ if (arg === "-v" || arg === "--version") {
119
+ flags.version = true;
120
+ continue;
121
+ }
122
+ if (arg === "--no-ui") {
123
+ flags.noUi = true;
124
+ continue;
125
+ }
126
+ if (arg === "--ui") {
127
+ flags.ui = true;
128
+ continue;
129
+ }
130
+ if (arg === "--detach") {
131
+ flags.detach = true;
132
+ continue;
133
+ }
134
+ if (arg === "--json") {
135
+ flags.json = true;
136
+ continue;
137
+ }
138
+ if (arg === "--quiet" || arg === "-q") {
139
+ flags.quiet = true;
140
+ continue;
141
+ }
142
+ if (arg === "--verbose" || arg === "-V") {
143
+ flags.verbose = true;
144
+ continue;
145
+ }
146
+ }
147
+ args.push(arg);
148
+ }
149
+ return { ...flags, args };
150
+ }
151
+ async function collectComponentVersions() {
152
+ let editorBuildId = null;
153
+ try {
154
+ const embedded = defaultEmbeddedDir();
155
+ const indexPath = path.join(embedded, "index.html");
156
+ editorBuildId = await computeBuildId(embedded, indexPath);
55
157
  }
56
- if (cmd === "check") {
57
- return runCheck(rest);
158
+ catch {
159
+ editorBuildId = null;
58
160
  }
59
- if (cmd === "view") {
60
- return runView(rest);
161
+ return {
162
+ cli: CLI_VERSION,
163
+ viewer: VIEWER_PACKAGE_VERSION,
164
+ editorBuildId,
165
+ };
166
+ }
167
+ async function printVersion(asJson) {
168
+ const info = await collectComponentVersions();
169
+ if (asJson) {
170
+ console.log(JSON.stringify(info));
171
+ return;
61
172
  }
62
- console.error(`Unknown command '${cmd}'.`);
63
- printGlobalHelp();
64
- return 1;
173
+ console.log(`cli ${info.cli}`);
174
+ console.log(`viewer ${info.viewer}`);
175
+ console.log(`editor ${info.editorBuildId ?? "unknown"}`);
176
+ }
177
+ function printCommandHelp(cmd) {
178
+ if (cmd === "parse")
179
+ return printParseHelp();
180
+ if (cmd === "check")
181
+ return printCheckHelp();
182
+ if (cmd === "render")
183
+ return printRenderHelp();
184
+ if (cmd === "fmt")
185
+ return printFormatHelp();
186
+ if (cmd === "tick")
187
+ return printTickHelp();
188
+ if (cmd === "step")
189
+ return printStepHelp();
190
+ if (cmd === "view")
191
+ return printViewHelp();
192
+ if (cmd === "edit")
193
+ return printEditHelp();
194
+ if (cmd === "pdf")
195
+ return printPdfHelp();
196
+ if (cmd === "config")
197
+ return printConfigHelp();
198
+ if (cmd === "new")
199
+ return printNewHelp();
200
+ if (cmd === "add")
201
+ return printAddHelp();
202
+ return printGlobalHelp();
65
203
  }
66
- /* -------------------------------------------------------------------------- */
67
- /* Help text */
68
- /* -------------------------------------------------------------------------- */
69
204
  function printGlobalHelp() {
70
205
  console.log([
71
- `Flux CLI v${VERSION}`,
206
+ `Flux CLI v${CLI_VERSION}`,
72
207
  "",
73
208
  "Usage:",
209
+ " flux (launch UI in TTY)",
74
210
  " flux parse [options] <files...>",
75
211
  " flux check [options] <files...>",
212
+ " flux render [options] <file>",
213
+ " flux fmt <file>",
214
+ " flux tick [options] <file>",
215
+ " flux step [options] <file>",
76
216
  " flux view <file>",
217
+ " flux edit <file>",
218
+ " flux pdf <file> --out <file.pdf>",
219
+ " flux config [set <key> <value>]",
220
+ " flux new <template> [options]",
221
+ " flux add <kind> [options]",
77
222
  "",
78
223
  "Commands:",
79
224
  " parse Parse Flux source files and print their IR as JSON.",
80
225
  " check Parse and run basic static checks.",
81
- " view View a Flux document in a simple docstep viewer.",
226
+ " render Render a Flux document to canonical Render IR JSON.",
227
+ " fmt Format a Flux document in-place.",
228
+ " tick Advance time and render the updated document.",
229
+ " step Advance docsteps and render the updated document.",
230
+ " view View a Flux document in a local web preview.",
231
+ " edit Edit a Flux document in the local web editor.",
232
+ " pdf Export a Flux document snapshot to PDF.",
233
+ " config View or edit configuration.",
234
+ " new Create a new Flux document.",
235
+ " add Apply structured edits to a Flux document.",
82
236
  "",
83
237
  "Global options:",
84
238
  " -h, --help Show this help message.",
85
239
  " -v, --version Show CLI version.",
240
+ " --no-ui Disable Ink UI launch.",
241
+ " --ui Force Ink UI launch (TTY only).",
242
+ " --detach Keep viewer running on UI exit.",
243
+ " --json Emit machine-readable JSON where applicable.",
244
+ " -q, --quiet Reduce non-essential output.",
245
+ " -V, --verbose Show verbose logs.",
86
246
  "",
87
247
  ].join("\n"));
88
248
  }
@@ -117,239 +277,1061 @@ function printCheckHelp() {
117
277
  "",
118
278
  ].join("\n"));
119
279
  }
120
- /* -------------------------------------------------------------------------- */
121
- /* flux parse */
122
- /* -------------------------------------------------------------------------- */
280
+ function printRenderHelp() {
281
+ console.log([
282
+ "Usage:",
283
+ " flux render [options] <file>",
284
+ "",
285
+ "Description:",
286
+ " Render a Flux document to canonical Render IR JSON.",
287
+ "",
288
+ "Options:",
289
+ " --format ir Output format. (required; currently only 'ir')",
290
+ " --seed N Deterministic RNG seed (default: 0).",
291
+ " --time T Render time in seconds (default: 0).",
292
+ " --docstep D Render at docstep D (default: 0).",
293
+ " -h, --help Show this message.",
294
+ "",
295
+ ].join("\n"));
296
+ }
297
+ function printFormatHelp() {
298
+ console.log([
299
+ "Usage:",
300
+ " flux fmt <file>",
301
+ "",
302
+ "Description:",
303
+ " Apply a minimal formatter to a Flux document.",
304
+ "",
305
+ "Options:",
306
+ " -h, --help Show this message.",
307
+ "",
308
+ ].join("\n"));
309
+ }
310
+ function printTickHelp() {
311
+ console.log([
312
+ "Usage:",
313
+ " flux tick [options] <file>",
314
+ "",
315
+ "Description:",
316
+ " Advance time by a number of seconds and render the updated IR.",
317
+ "",
318
+ "Options:",
319
+ " --seconds S Seconds to advance time by.",
320
+ " --seed N Deterministic RNG seed (default: 0).",
321
+ " -h, --help Show this message.",
322
+ "",
323
+ ].join("\n"));
324
+ }
325
+ function printStepHelp() {
326
+ console.log([
327
+ "Usage:",
328
+ " flux step [options] <file>",
329
+ "",
330
+ "Description:",
331
+ " Advance docsteps and render the updated IR.",
332
+ "",
333
+ "Options:",
334
+ " --n N Docsteps to advance by (default: 1).",
335
+ " --seed N Deterministic RNG seed (default: 0).",
336
+ " -h, --help Show this message.",
337
+ "",
338
+ ].join("\n"));
339
+ }
340
+ function printViewHelp() {
341
+ console.log([
342
+ "Usage:",
343
+ " flux view [options] <file>",
344
+ "",
345
+ "Description:",
346
+ " Open a local web viewer for a Flux document.",
347
+ "",
348
+ "Options:",
349
+ " --port <n> Port for the local server (default: auto).",
350
+ " --docstep-ms <n> Docstep interval in milliseconds.",
351
+ " --time-rate <n> Time multiplier for viewer ticks (default: 1).",
352
+ " --seed <n> Seed for deterministic rendering.",
353
+ " --allow-net <orig> Allow remote assets for origin (repeatable or comma-separated).",
354
+ " --editor-dist <p> Serve editor assets from this dist folder.",
355
+ " --no-time Disable automatic time advancement.",
356
+ " --tty Use the legacy TTY grid viewer.",
357
+ " -h, --help Show this message.",
358
+ "",
359
+ ].join("\n"));
360
+ }
361
+ function printEditHelp() {
362
+ console.log([
363
+ "Usage:",
364
+ " flux edit [options] <file>",
365
+ "",
366
+ "Description:",
367
+ " Open the local web editor for a Flux document.",
368
+ "",
369
+ "Options:",
370
+ " --port <n> Port for the local server (default: auto).",
371
+ " --docstep-ms <n> Docstep interval in milliseconds.",
372
+ " --time-rate <n> Time multiplier for viewer ticks (default: 1).",
373
+ " --seed <n> Seed for deterministic rendering.",
374
+ " --allow-net <orig> Allow remote assets for origin (repeatable or comma-separated).",
375
+ " --editor-dist <p> Serve editor assets from this dist folder.",
376
+ " --no-time Disable automatic time advancement.",
377
+ " -h, --help Show this message.",
378
+ "",
379
+ ].join("\n"));
380
+ }
381
+ function printPdfHelp() {
382
+ console.log([
383
+ "Usage:",
384
+ " flux pdf [options] <file> --out <file.pdf>",
385
+ "",
386
+ "Description:",
387
+ " Render a Flux document snapshot to PDF.",
388
+ "",
389
+ "Options:",
390
+ " --out <file> Output PDF path. (required)",
391
+ " --seed <n> Seed for deterministic rendering.",
392
+ " --docstep <n> Docstep to render.",
393
+ " -h, --help Show this message.",
394
+ "",
395
+ ].join("\n"));
396
+ }
397
+ function printConfigHelp() {
398
+ console.log([
399
+ "Usage:",
400
+ " flux config",
401
+ " flux config set <key> <value> [--init]",
402
+ "",
403
+ "Options:",
404
+ " --json Emit JSON.",
405
+ " --init Create flux.config.json if missing.",
406
+ "",
407
+ ].join("\n"));
408
+ }
409
+ function printNewHelp() {
410
+ console.log([
411
+ "Usage:",
412
+ " flux new (launch wizard)",
413
+ " flux new <template> --out <dir> --page Letter|A4 --theme print|screen|both --fonts tech|bookish",
414
+ " --fallback system|none --assets yes|no --chapters N --live yes|no",
415
+ "",
416
+ "Templates:",
417
+ " demo, article, spec, zine, paper, blank",
418
+ "",
419
+ ].join("\n"));
420
+ }
421
+ function printAddHelp() {
422
+ console.log([
423
+ "Usage:",
424
+ " flux add <kind> [options] <file>",
425
+ "",
426
+ "Kinds:",
427
+ " title, page, section, figure, callout, table, slot, inline-slot, bibliography-stub",
428
+ "",
429
+ "Options:",
430
+ " --text <value> Text value for title/callout.",
431
+ " --heading <value> Heading text for sections.",
432
+ " --label <value> Optional label for figure/callout.",
433
+ " --no-heading Omit section heading.",
434
+ " --no-check Skip check after editing.",
435
+ "",
436
+ ].join("\n"));
437
+ }
123
438
  async function runParse(args) {
124
- const opts = {
125
- ndjson: false,
126
- pretty: false,
127
- compact: false,
128
- };
129
- const files = [];
439
+ const opts = { ndjson: false, pretty: false, compact: false, files: [] };
130
440
  for (const arg of args) {
131
- if (arg === "--ndjson") {
441
+ if (arg === "--ndjson")
132
442
  opts.ndjson = true;
133
- }
134
- else if (arg === "--pretty") {
443
+ else if (arg === "--pretty")
135
444
  opts.pretty = true;
136
- }
137
- else if (arg === "--compact") {
445
+ else if (arg === "--compact")
138
446
  opts.compact = true;
447
+ else
448
+ opts.files.push(arg);
449
+ }
450
+ const result = await parseCommand(opts);
451
+ if (!result.ok || !result.data) {
452
+ console.error(result.error?.message ?? "flux parse failed");
453
+ if (result.error?.code === "NO_INPUT")
454
+ printParseHelp();
455
+ return 1;
456
+ }
457
+ if (result.data.ndjson) {
458
+ for (const item of result.data.docs) {
459
+ process.stdout.write(JSON.stringify({ file: item.file, doc: item.doc }) + "\n");
460
+ }
461
+ return 0;
462
+ }
463
+ const doc = result.data.docs[0].doc;
464
+ const space = result.data.compact ? 0 : 2;
465
+ process.stdout.write(JSON.stringify(doc, null, space) + "\n");
466
+ return 0;
467
+ }
468
+ async function runCheck(args, globals) {
469
+ const files = args.filter((arg) => !arg.startsWith("-"));
470
+ const result = await checkCommand({ files, json: globals.json });
471
+ if (!result.ok || !result.data) {
472
+ console.error(result.error?.message ?? "flux check failed");
473
+ printCheckHelp();
474
+ return 1;
475
+ }
476
+ const results = result.data.results;
477
+ const failed = results.filter((r) => !r.ok);
478
+ if (globals.json) {
479
+ for (const r of results) {
480
+ const payload = {
481
+ file: r.file,
482
+ ok: r.ok,
483
+ };
484
+ if (r.errors)
485
+ payload.errors = r.errors.map((message) => ({ message }));
486
+ process.stdout.write(JSON.stringify(payload) + "\n");
487
+ }
488
+ return failed.length ? 1 : 0;
489
+ }
490
+ for (const r of results) {
491
+ if (!r.errors)
492
+ continue;
493
+ for (const msg of r.errors)
494
+ console.error(msg);
495
+ }
496
+ if (failed.length) {
497
+ if (!globals.quiet)
498
+ console.log(`✗ ${failed.length} of ${results.length} files failed checks`);
499
+ return 1;
500
+ }
501
+ if (!globals.quiet)
502
+ console.log(`✓ ${results.length} files OK`);
503
+ return 0;
504
+ }
505
+ async function runRender(args) {
506
+ let format = "ir";
507
+ let seed;
508
+ let time;
509
+ let docstep;
510
+ let file;
511
+ try {
512
+ for (let i = 0; i < args.length; i += 1) {
513
+ const arg = args[i];
514
+ if (arg === "--format") {
515
+ format = args[i + 1] ?? "";
516
+ i += 1;
517
+ }
518
+ else if (arg.startsWith("--format=")) {
519
+ format = arg.slice("--format=".length);
520
+ }
521
+ else if (arg === "--seed") {
522
+ seed = parseNumberFlag("--seed", args[i + 1]);
523
+ i += 1;
524
+ }
525
+ else if (arg.startsWith("--seed=")) {
526
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
527
+ }
528
+ else if (arg === "--time") {
529
+ time = parseNumberFlag("--time", args[i + 1]);
530
+ i += 1;
531
+ }
532
+ else if (arg.startsWith("--time=")) {
533
+ time = parseNumberFlag("--time", arg.slice("--time=".length));
534
+ }
535
+ else if (arg === "--docstep") {
536
+ docstep = parseNumberFlag("--docstep", args[i + 1]);
537
+ i += 1;
538
+ }
539
+ else if (arg.startsWith("--docstep=")) {
540
+ docstep = parseNumberFlag("--docstep", arg.slice("--docstep=".length));
541
+ }
542
+ else if (!arg.startsWith("-")) {
543
+ file = arg;
544
+ }
139
545
  }
140
- else {
141
- files.push(arg);
546
+ }
547
+ catch (error) {
548
+ console.error(`flux render: ${error?.message ?? error}`);
549
+ return 1;
550
+ }
551
+ if (!file) {
552
+ console.error("flux render: No input file specified.");
553
+ printRenderHelp();
554
+ return 1;
555
+ }
556
+ const result = await renderCommand({ file, format: format, seed, time, docstep });
557
+ if (!result.ok || !result.data) {
558
+ console.error(result.error?.message ?? "flux render failed");
559
+ return 1;
560
+ }
561
+ process.stdout.write(JSON.stringify(result.data.rendered, null, 2) + "\n");
562
+ return 0;
563
+ }
564
+ async function runFormat(args, globals) {
565
+ const file = args.find((arg) => !arg.startsWith("-"));
566
+ if (!file) {
567
+ console.error("flux fmt: No input file specified.");
568
+ printFormatHelp();
569
+ return 1;
570
+ }
571
+ const result = await formatCommand({ file });
572
+ if (!result.ok || !result.data) {
573
+ console.error(result.error?.message ?? "flux fmt failed");
574
+ return 1;
575
+ }
576
+ if (globals.json) {
577
+ process.stdout.write(JSON.stringify(result.data) + "\n");
578
+ }
579
+ else if (!globals.quiet) {
580
+ console.log(`Formatted ${file}`);
581
+ }
582
+ return 0;
583
+ }
584
+ async function runTick(args) {
585
+ let seconds;
586
+ let seed;
587
+ let file;
588
+ try {
589
+ for (let i = 0; i < args.length; i += 1) {
590
+ const arg = args[i];
591
+ if (arg === "--seconds") {
592
+ seconds = parseNumberFlag("--seconds", args[i + 1]);
593
+ i += 1;
594
+ }
595
+ else if (arg.startsWith("--seconds=")) {
596
+ seconds = parseNumberFlag("--seconds", arg.slice("--seconds=".length));
597
+ }
598
+ else if (arg === "--seed") {
599
+ seed = parseNumberFlag("--seed", args[i + 1]);
600
+ i += 1;
601
+ }
602
+ else if (arg.startsWith("--seed=")) {
603
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
604
+ }
605
+ else if (!arg.startsWith("-")) {
606
+ file = arg;
607
+ }
142
608
  }
143
609
  }
144
- if (files.length === 0) {
145
- console.error("flux parse: No input files specified.");
146
- printParseHelp();
610
+ catch (error) {
611
+ console.error(`flux tick: ${error?.message ?? error}`);
147
612
  return 1;
148
613
  }
149
- const usesStdin = files.includes("-");
150
- if (usesStdin && files.length > 1) {
151
- console.error("flux parse: '-' (stdin) can only be used with a single input.");
614
+ if (!file) {
615
+ console.error("flux tick: No input file specified.");
616
+ printTickHelp();
152
617
  return 1;
153
618
  }
154
- if (opts.pretty && opts.compact && !opts.ndjson) {
155
- console.error("flux parse: --pretty and --compact are mutually exclusive.");
619
+ if (seconds === undefined) {
620
+ console.error("flux tick: --seconds is required.");
621
+ printTickHelp();
156
622
  return 1;
157
623
  }
158
- const docs = [];
159
- for (const file of files) {
160
- let source;
161
- try {
162
- source = await readSource(file);
624
+ const result = await tickCommand({ file, seconds, seed });
625
+ if (!result.ok || !result.data) {
626
+ console.error(result.error?.message ?? "flux tick failed");
627
+ return 1;
628
+ }
629
+ process.stdout.write(JSON.stringify(result.data.rendered, null, 2) + "\n");
630
+ return 0;
631
+ }
632
+ async function runStep(args) {
633
+ let count;
634
+ let seed;
635
+ let file;
636
+ try {
637
+ for (let i = 0; i < args.length; i += 1) {
638
+ const arg = args[i];
639
+ if (arg === "--n") {
640
+ count = parseNumberFlag("--n", args[i + 1]);
641
+ i += 1;
642
+ }
643
+ else if (arg.startsWith("--n=")) {
644
+ count = parseNumberFlag("--n", arg.slice("--n=".length));
645
+ }
646
+ else if (arg === "--seed") {
647
+ seed = parseNumberFlag("--seed", args[i + 1]);
648
+ i += 1;
649
+ }
650
+ else if (arg.startsWith("--seed=")) {
651
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
652
+ }
653
+ else if (!arg.startsWith("-")) {
654
+ file = arg;
655
+ }
163
656
  }
164
- catch (error) {
165
- const msg = formatIoError(file, error);
166
- console.error(msg);
167
- return 1;
657
+ }
658
+ catch (error) {
659
+ console.error(`flux step: ${error?.message ?? error}`);
660
+ return 1;
661
+ }
662
+ if (!file) {
663
+ console.error("flux step: No input file specified.");
664
+ printStepHelp();
665
+ return 1;
666
+ }
667
+ const result = await stepCommand({ file, steps: count, seed });
668
+ if (!result.ok || !result.data) {
669
+ console.error(result.error?.message ?? "flux step failed");
670
+ return 1;
671
+ }
672
+ process.stdout.write(JSON.stringify(result.data.rendered, null, 2) + "\n");
673
+ return 0;
674
+ }
675
+ async function runView(args, globals) {
676
+ let port;
677
+ let docstepMs;
678
+ let timeRate;
679
+ let seed;
680
+ let useTty = false;
681
+ let editorDist;
682
+ const allowNet = [];
683
+ let file;
684
+ let advanceTime = true;
685
+ let advanceTimeExplicit = false;
686
+ try {
687
+ for (let i = 0; i < args.length; i += 1) {
688
+ const arg = args[i];
689
+ if (arg === "--tty") {
690
+ useTty = true;
691
+ }
692
+ else if (arg === "--no-time") {
693
+ advanceTime = false;
694
+ advanceTimeExplicit = true;
695
+ }
696
+ else if (arg === "--port") {
697
+ port = parseNumberFlag("--port", args[i + 1]);
698
+ i += 1;
699
+ }
700
+ else if (arg.startsWith("--port=")) {
701
+ port = parseNumberFlag("--port", arg.slice("--port=".length));
702
+ }
703
+ else if (arg === "--docstep-ms") {
704
+ docstepMs = parseNumberFlag("--docstep-ms", args[i + 1]);
705
+ i += 1;
706
+ }
707
+ else if (arg.startsWith("--docstep-ms=")) {
708
+ docstepMs = parseNumberFlag("--docstep-ms", arg.slice("--docstep-ms=".length));
709
+ }
710
+ else if (arg === "--time-rate") {
711
+ timeRate = parseNumberFlag("--time-rate", args[i + 1]);
712
+ i += 1;
713
+ }
714
+ else if (arg.startsWith("--time-rate=")) {
715
+ timeRate = parseNumberFlag("--time-rate", arg.slice("--time-rate=".length));
716
+ }
717
+ else if (arg === "--seed") {
718
+ seed = parseNumberFlag("--seed", args[i + 1]);
719
+ i += 1;
720
+ }
721
+ else if (arg.startsWith("--seed=")) {
722
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
723
+ }
724
+ else if (arg === "--allow-net") {
725
+ const raw = args[i + 1] ?? "";
726
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
727
+ i += 1;
728
+ }
729
+ else if (arg.startsWith("--allow-net=")) {
730
+ const raw = arg.slice("--allow-net=".length);
731
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
732
+ }
733
+ else if (arg === "--editor-dist") {
734
+ editorDist = args[i + 1];
735
+ i += 1;
736
+ }
737
+ else if (arg.startsWith("--editor-dist=")) {
738
+ editorDist = arg.slice("--editor-dist=".length);
739
+ }
740
+ else if (!arg.startsWith("-")) {
741
+ file = arg;
742
+ }
168
743
  }
744
+ }
745
+ catch (error) {
746
+ console.error(`flux view: ${error?.message ?? error}`);
747
+ return 1;
748
+ }
749
+ const resolved = await resolveConfig({ cwd: process.cwd(), env: process.env });
750
+ if (docstepMs === undefined) {
751
+ docstepMs = resolved.config.docstepMs;
752
+ }
753
+ if (!advanceTimeExplicit) {
754
+ advanceTime = resolved.config.advanceTime;
755
+ }
756
+ if (!file) {
757
+ console.error("flux view: No input file specified.");
758
+ printViewHelp();
759
+ return 1;
760
+ }
761
+ if (file === "-") {
762
+ console.error("flux view: stdin input is not supported for the web viewer.");
763
+ return 1;
764
+ }
765
+ if (useTty) {
169
766
  try {
170
- const doc = parseDocument(source);
171
- docs.push({ file: file === "-" ? "<stdin>" : file, doc });
767
+ const source = await fs.readFile(file, "utf8");
768
+ const doc = parseFlux(source, file);
769
+ const runtime = createRuntime(doc, { clock: "manual" });
770
+ await runViewer(runtime, { docPath: file, title: doc.meta.title, materialLabels: new Map() });
771
+ return 0;
172
772
  }
173
773
  catch (error) {
174
- const msg = formatParseOrLexerError(file, error);
175
- console.error(msg);
774
+ console.error(`flux view: ${String(error?.message ?? error)}`);
176
775
  return 1;
177
776
  }
178
777
  }
179
- const useNdjson = opts.ndjson || docs.length > 1;
180
- if (useNdjson) {
181
- for (const item of docs) {
182
- const payload = { file: item.file, doc: item.doc };
183
- process.stdout.write(JSON.stringify(payload) + "\n");
778
+ const result = await viewCommand({
779
+ cwd: process.cwd(),
780
+ docPath: file,
781
+ port,
782
+ docstepMs,
783
+ seed,
784
+ allowNet,
785
+ advanceTime,
786
+ timeRate,
787
+ editorDist,
788
+ });
789
+ if (!result.ok || !result.data) {
790
+ console.error(result.error?.message ?? "flux view failed");
791
+ return 1;
792
+ }
793
+ await updateRecents(process.cwd(), path.resolve(file));
794
+ const session = result.data.session;
795
+ if (globals.json) {
796
+ process.stdout.write(JSON.stringify(session) + "\n");
797
+ }
798
+ else if (!globals.quiet) {
799
+ console.log(`Flux viewer running at ${session.url}`);
800
+ if (session.attached) {
801
+ console.log("Attached to existing viewer.");
184
802
  }
185
- return 0;
803
+ console.log("Press Ctrl+C to stop.");
804
+ }
805
+ if (!globals.quiet) {
806
+ openBrowser(session.url);
807
+ }
808
+ if (session.close) {
809
+ await new Promise((resolve) => {
810
+ const shutdown = async () => {
811
+ await session.close?.();
812
+ resolve();
813
+ };
814
+ process.on("SIGINT", shutdown);
815
+ process.on("SIGTERM", shutdown);
816
+ });
186
817
  }
187
- // Single file → pretty by default unless compact explicitly requested
188
- const doc = docs[0].doc;
189
- const space = opts.compact ? 0 : 2;
190
- const json = JSON.stringify(doc, null, space);
191
- process.stdout.write(json + "\n");
192
818
  return 0;
193
819
  }
194
- /* -------------------------------------------------------------------------- */
195
- /* flux check */
196
- /* -------------------------------------------------------------------------- */
197
- async function runCheck(args) {
198
- const opts = {
199
- json: false,
200
- };
201
- const files = [];
202
- for (const arg of args) {
203
- if (arg === "--json") {
204
- opts.json = true;
205
- }
206
- else {
207
- files.push(arg);
820
+ async function runEdit(args, globals) {
821
+ let port;
822
+ let docstepMs;
823
+ let timeRate;
824
+ let seed;
825
+ let editorDist;
826
+ const allowNet = [];
827
+ let file;
828
+ let advanceTime = true;
829
+ let advanceTimeExplicit = false;
830
+ try {
831
+ for (let i = 0; i < args.length; i += 1) {
832
+ const arg = args[i];
833
+ if (arg === "--no-time") {
834
+ advanceTime = false;
835
+ advanceTimeExplicit = true;
836
+ }
837
+ else if (arg === "--port") {
838
+ port = parseNumberFlag("--port", args[i + 1]);
839
+ i += 1;
840
+ }
841
+ else if (arg.startsWith("--port=")) {
842
+ port = parseNumberFlag("--port", arg.slice("--port=".length));
843
+ }
844
+ else if (arg === "--docstep-ms") {
845
+ docstepMs = parseNumberFlag("--docstep-ms", args[i + 1]);
846
+ i += 1;
847
+ }
848
+ else if (arg.startsWith("--docstep-ms=")) {
849
+ docstepMs = parseNumberFlag("--docstep-ms", arg.slice("--docstep-ms=".length));
850
+ }
851
+ else if (arg === "--time-rate") {
852
+ timeRate = parseNumberFlag("--time-rate", args[i + 1]);
853
+ i += 1;
854
+ }
855
+ else if (arg.startsWith("--time-rate=")) {
856
+ timeRate = parseNumberFlag("--time-rate", arg.slice("--time-rate=".length));
857
+ }
858
+ else if (arg === "--seed") {
859
+ seed = parseNumberFlag("--seed", args[i + 1]);
860
+ i += 1;
861
+ }
862
+ else if (arg.startsWith("--seed=")) {
863
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
864
+ }
865
+ else if (arg === "--allow-net") {
866
+ const raw = args[i + 1] ?? "";
867
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
868
+ i += 1;
869
+ }
870
+ else if (arg.startsWith("--allow-net=")) {
871
+ const raw = arg.slice("--allow-net=".length);
872
+ allowNet.push(...raw.split(",").map((item) => item.trim()).filter(Boolean));
873
+ }
874
+ else if (arg === "--editor-dist") {
875
+ editorDist = args[i + 1];
876
+ i += 1;
877
+ }
878
+ else if (arg.startsWith("--editor-dist=")) {
879
+ editorDist = arg.slice("--editor-dist=".length);
880
+ }
881
+ else if (!arg.startsWith("-")) {
882
+ file = arg;
883
+ }
208
884
  }
209
885
  }
210
- if (files.length === 0) {
211
- console.error("flux check: No input files specified.");
212
- printCheckHelp();
886
+ catch (error) {
887
+ console.error(`flux edit: ${error?.message ?? error}`);
213
888
  return 1;
214
889
  }
215
- const results = [];
216
- for (const file of files) {
217
- let source;
218
- try {
219
- source = await readSource(file);
220
- }
221
- catch (error) {
222
- const diagnostic = formatIoError(file, error);
223
- results.push({
224
- file,
225
- ok: false,
226
- errors: [diagnostic],
227
- });
228
- continue;
229
- }
230
- let doc;
231
- try {
232
- doc = parseDocument(source);
890
+ const resolved = await resolveConfig({ cwd: process.cwd(), env: process.env });
891
+ if (docstepMs === undefined) {
892
+ docstepMs = resolved.config.docstepMs;
893
+ }
894
+ if (!advanceTimeExplicit) {
895
+ advanceTime = resolved.config.advanceTime;
896
+ }
897
+ if (!file) {
898
+ console.error("flux edit: No input file specified.");
899
+ printEditHelp();
900
+ return 1;
901
+ }
902
+ if (file === "-") {
903
+ console.error("flux edit: stdin input is not supported for the web editor.");
904
+ return 1;
905
+ }
906
+ const result = await viewCommand({
907
+ cwd: process.cwd(),
908
+ docPath: file,
909
+ port,
910
+ docstepMs,
911
+ seed,
912
+ allowNet,
913
+ advanceTime,
914
+ timeRate,
915
+ editorDist,
916
+ });
917
+ if (!result.ok || !result.data) {
918
+ console.error(result.error?.message ?? "flux edit failed");
919
+ return 1;
920
+ }
921
+ await updateRecents(process.cwd(), path.resolve(file));
922
+ const session = result.data.session;
923
+ const absolutePath = path.resolve(file);
924
+ const editorUrl = `${session.url}/edit?file=${encodeURIComponent(absolutePath)}`;
925
+ let buildId = session.buildId;
926
+ let editorDistPath = session.editorDist;
927
+ if (!buildId) {
928
+ const fetchImpl = globalThis?.fetch;
929
+ if (fetchImpl) {
930
+ try {
931
+ const res = await fetchImpl(`${session.url}/edit/build-id.json`, { cache: "no-store" });
932
+ if (res.ok) {
933
+ const json = await res.json();
934
+ buildId = json?.buildId ?? null;
935
+ editorDistPath = json?.editorDist ?? editorDistPath;
936
+ }
937
+ }
938
+ catch {
939
+ // ignore
940
+ }
233
941
  }
234
- catch (error) {
235
- const diagnostic = formatParseOrLexerError(file, error);
236
- results.push({
237
- file,
238
- ok: false,
239
- errors: [diagnostic],
240
- });
241
- continue;
942
+ }
943
+ if (globals.json) {
944
+ process.stdout.write(JSON.stringify({ ...session, editorUrl }) + "\n");
945
+ }
946
+ else if (!globals.quiet) {
947
+ if (buildId || editorDistPath) {
948
+ console.log(`[flux] editor build ${buildId ?? "unknown"} (${editorDistPath ?? "unknown"})`);
242
949
  }
243
- const errors = [];
244
- // initRuntimeState smoke check — should not throw for valid IR.
245
- try {
246
- initRuntimeState(doc);
950
+ console.log(`Flux editor running at ${editorUrl}`);
951
+ if (session.attached) {
952
+ console.log("Attached to existing editor.");
247
953
  }
248
- catch (error) {
249
- const detail = error?.message ?? String(error);
250
- errors.push(`${file}:0:0: Check error: initRuntimeState failed: ${detail}`);
251
- }
252
- // Static checks (grids, neighbors, timers, etc.)
253
- errors.push(...checkDocument(file, doc));
254
- results.push({
255
- file,
256
- ok: errors.length === 0,
257
- errors: errors.length ? errors : undefined,
258
- });
954
+ console.log("Press Ctrl+C to stop.");
259
955
  }
260
- const failed = results.filter((r) => !r.ok);
261
- const hasFailure = failed.length > 0;
262
- // JSON (NDJSON) diagnostics
263
- if (opts.json) {
264
- for (const r of results) {
265
- const payload = {
266
- file: r.file,
267
- ok: r.ok,
956
+ if (!globals.quiet) {
957
+ openBrowser(editorUrl);
958
+ }
959
+ if (session.close) {
960
+ await new Promise((resolve) => {
961
+ const shutdown = async () => {
962
+ await session.close?.();
963
+ resolve();
268
964
  };
269
- if (r.errors) {
270
- payload.errors = r.errors.map((message) => ({ message }));
965
+ process.on("SIGINT", shutdown);
966
+ process.on("SIGTERM", shutdown);
967
+ });
968
+ }
969
+ return 0;
970
+ }
971
+ async function runPdf(args, globals) {
972
+ let seed;
973
+ let docstep;
974
+ let outPath;
975
+ let file;
976
+ try {
977
+ for (let i = 0; i < args.length; i += 1) {
978
+ const arg = args[i];
979
+ if (arg === "--out") {
980
+ outPath = args[i + 1];
981
+ i += 1;
982
+ }
983
+ else if (arg.startsWith("--out=")) {
984
+ outPath = arg.slice("--out=".length);
985
+ }
986
+ else if (arg === "--seed") {
987
+ seed = parseNumberFlag("--seed", args[i + 1]);
988
+ i += 1;
989
+ }
990
+ else if (arg.startsWith("--seed=")) {
991
+ seed = parseNumberFlag("--seed", arg.slice("--seed=".length));
992
+ }
993
+ else if (arg === "--docstep") {
994
+ docstep = parseNumberFlag("--docstep", args[i + 1]);
995
+ i += 1;
996
+ }
997
+ else if (arg.startsWith("--docstep=")) {
998
+ docstep = parseNumberFlag("--docstep", arg.slice("--docstep=".length));
999
+ }
1000
+ else if (!arg.startsWith("-")) {
1001
+ file = arg;
271
1002
  }
272
- process.stdout.write(JSON.stringify(payload) + "\n");
273
1003
  }
274
- return hasFailure ? 1 : 0;
275
1004
  }
276
- // Human-readable: diagnostics per failing file + summary
277
- for (const r of results) {
278
- if (!r.errors)
279
- continue;
280
- for (const msg of r.errors) {
281
- console.error(msg);
282
- }
1005
+ catch (error) {
1006
+ console.error(`flux pdf: ${error?.message ?? error}`);
1007
+ return 1;
1008
+ }
1009
+ if (!file || !outPath) {
1010
+ console.error("flux pdf: --out <file.pdf> is required.");
1011
+ printPdfHelp();
1012
+ return 1;
283
1013
  }
284
- if (hasFailure) {
285
- console.log(`✗ ${failed.length} of ${results.length} files failed checks`);
1014
+ const result = await pdfCommand({ file, outPath, seed, docstep });
1015
+ if (!result.ok || !result.data) {
1016
+ console.error(result.error?.message ?? "flux pdf failed");
286
1017
  return 1;
287
1018
  }
288
- console.log(`✓ ${results.length} files OK`);
1019
+ if (globals.json) {
1020
+ process.stdout.write(JSON.stringify(result.data) + "\n");
1021
+ }
1022
+ else if (!globals.quiet) {
1023
+ console.log(`Wrote PDF to ${outPath}`);
1024
+ }
289
1025
  return 0;
290
1026
  }
291
- /* -------------------------------------------------------------------------- */
292
- /* flux view */
293
- /* -------------------------------------------------------------------------- */
294
- async function runView(args) {
1027
+ async function runConfig(args, globals) {
295
1028
  if (args.length === 0) {
296
- console.error("flux view: No input file specified.");
1029
+ const result = await configCommand({
1030
+ cwd: process.cwd(),
1031
+ action: "view",
1032
+ env: process.env,
1033
+ });
1034
+ if (!result.ok || !result.data) {
1035
+ console.error(result.error?.message ?? "flux config failed");
1036
+ return 1;
1037
+ }
1038
+ return printConfig(result.data.config, globals);
1039
+ }
1040
+ if (args[0] === "set") {
1041
+ const key = args[1];
1042
+ const value = args[2];
1043
+ const init = args.includes("--init");
1044
+ if (!key || value === undefined) {
1045
+ printConfigHelp();
1046
+ return 1;
1047
+ }
1048
+ const parsedValue = parseConfigValue(key, value);
1049
+ const result = await configCommand({
1050
+ cwd: process.cwd(),
1051
+ action: "set",
1052
+ key: parsedValue.key,
1053
+ value: parsedValue.value,
1054
+ init,
1055
+ env: process.env,
1056
+ });
1057
+ if (!result.ok || !result.data) {
1058
+ console.error(result.error?.message ?? "flux config set failed");
1059
+ return 1;
1060
+ }
1061
+ return printConfig(result.data.config, globals);
1062
+ }
1063
+ printConfigHelp();
1064
+ return 1;
1065
+ }
1066
+ async function runNew(args, globals) {
1067
+ const [template, ...rest] = args;
1068
+ if (!template) {
1069
+ printNewHelp();
297
1070
  return 1;
298
1071
  }
299
- const docPath = path.resolve(args[0]);
300
- try {
301
- const source = await fs.readFile(docPath, "utf8");
302
- const doc = parseDocument(source);
303
- const runtime = createRuntime(doc, { clock: "manual" });
304
- const labels = new Map();
305
- for (const mat of doc.materials?.materials ?? []) {
306
- labels.set(mat.name, mat.label ?? mat.name);
307
- }
308
- await runViewer(runtime, { docPath, title: doc.meta.title, materialLabels: labels });
309
- return 0;
1072
+ const opts = parseNewArgs(rest);
1073
+ if (!opts.out) {
1074
+ const resolved = await resolveConfig({ cwd: process.cwd(), env: process.env });
1075
+ if (resolved.config.defaultOutputDir && resolved.config.defaultOutputDir !== ".") {
1076
+ opts.out = resolved.config.defaultOutputDir;
1077
+ }
310
1078
  }
311
- catch (error) {
312
- console.error(`flux view: ${String(error?.message ?? error)}`);
1079
+ const result = await newCommand({
1080
+ cwd: process.cwd(),
1081
+ template: template,
1082
+ out: opts.out,
1083
+ page: opts.page,
1084
+ theme: opts.theme,
1085
+ fonts: opts.fonts,
1086
+ fontFallback: opts.fontFallback,
1087
+ assets: opts.assets,
1088
+ chapters: opts.chapters,
1089
+ live: opts.live,
1090
+ });
1091
+ if (!result.ok || !result.data) {
1092
+ console.error(result.error?.message ?? "flux new failed");
313
1093
  return 1;
314
1094
  }
1095
+ await updateRecents(process.cwd(), result.data.docPath);
1096
+ if (globals.json) {
1097
+ process.stdout.write(JSON.stringify(result.data) + "\n");
1098
+ }
1099
+ else if (!globals.quiet) {
1100
+ console.log(`Created ${result.data.docPath}`);
1101
+ printNewNextSteps(result.data.docPath);
1102
+ }
1103
+ return 0;
315
1104
  }
316
- /* -------------------------------------------------------------------------- */
317
- /* I/O + errors */
318
- /* -------------------------------------------------------------------------- */
319
- async function readSource(file) {
320
- if (file === "-") {
321
- return readAllFromStdin();
1105
+ async function runAdd(args, globals) {
1106
+ const [kind, ...rest] = args;
1107
+ if (!kind) {
1108
+ printAddHelp();
1109
+ return 1;
1110
+ }
1111
+ const parsed = parseAddArgs(rest);
1112
+ if (!parsed.file) {
1113
+ console.error("flux add: missing <file>");
1114
+ printAddHelp();
1115
+ return 1;
1116
+ }
1117
+ const result = await addCommand({
1118
+ cwd: process.cwd(),
1119
+ file: parsed.file,
1120
+ kind: kind,
1121
+ text: parsed.text,
1122
+ heading: parsed.heading,
1123
+ label: parsed.label,
1124
+ noHeading: parsed.noHeading,
1125
+ noCheck: parsed.noCheck,
1126
+ });
1127
+ if (!result.ok || !result.data) {
1128
+ console.error(result.error?.message ?? "flux add failed");
1129
+ return 1;
1130
+ }
1131
+ if (globals.json) {
1132
+ process.stdout.write(JSON.stringify(result.data) + "\n");
1133
+ }
1134
+ else if (!globals.quiet) {
1135
+ console.log(`Updated ${result.data.file}`);
322
1136
  }
323
- return fs.readFile(file, "utf8");
1137
+ return 0;
324
1138
  }
325
- function readAllFromStdin() {
326
- return new Promise((resolve, reject) => {
327
- let data = "";
328
- nodeStdin.setEncoding("utf8");
329
- nodeStdin.on("data", (chunk) => {
330
- data += chunk;
331
- });
332
- nodeStdin.on("error", reject);
333
- nodeStdin.on("end", () => resolve(data));
1139
+ function parseNewArgs(args) {
1140
+ const opts = {};
1141
+ for (let i = 0; i < args.length; i += 1) {
1142
+ const arg = args[i];
1143
+ if (arg === "--out") {
1144
+ opts.out = args[i + 1];
1145
+ i += 1;
1146
+ }
1147
+ else if (arg.startsWith("--out=")) {
1148
+ opts.out = arg.slice("--out=".length);
1149
+ }
1150
+ else if (arg === "--page") {
1151
+ opts.page = args[i + 1];
1152
+ i += 1;
1153
+ }
1154
+ else if (arg.startsWith("--page=")) {
1155
+ opts.page = arg.slice("--page=".length);
1156
+ }
1157
+ else if (arg === "--theme") {
1158
+ opts.theme = args[i + 1];
1159
+ i += 1;
1160
+ }
1161
+ else if (arg.startsWith("--theme=")) {
1162
+ opts.theme = arg.slice("--theme=".length);
1163
+ }
1164
+ else if (arg === "--fonts") {
1165
+ opts.fonts = args[i + 1];
1166
+ i += 1;
1167
+ }
1168
+ else if (arg.startsWith("--fonts=")) {
1169
+ opts.fonts = arg.slice("--fonts=".length);
1170
+ }
1171
+ else if (arg === "--fallback" || arg === "--font-fallback") {
1172
+ opts.fontFallback = parseFontFallback(args[i + 1]);
1173
+ i += 1;
1174
+ }
1175
+ else if (arg.startsWith("--fallback=")) {
1176
+ opts.fontFallback = parseFontFallback(arg.slice("--fallback=".length));
1177
+ }
1178
+ else if (arg.startsWith("--font-fallback=")) {
1179
+ opts.fontFallback = parseFontFallback(arg.slice("--font-fallback=".length));
1180
+ }
1181
+ else if (arg === "--assets") {
1182
+ opts.assets = parseYesNo(args[i + 1]);
1183
+ i += 1;
1184
+ }
1185
+ else if (arg.startsWith("--assets=")) {
1186
+ opts.assets = parseYesNo(arg.slice("--assets=".length));
1187
+ }
1188
+ else if (arg === "--chapters") {
1189
+ opts.chapters = parseNumberFlag("--chapters", args[i + 1]);
1190
+ i += 1;
1191
+ }
1192
+ else if (arg.startsWith("--chapters=")) {
1193
+ opts.chapters = parseNumberFlag("--chapters", arg.slice("--chapters=".length));
1194
+ }
1195
+ else if (arg === "--live") {
1196
+ opts.live = parseYesNo(args[i + 1]);
1197
+ i += 1;
1198
+ }
1199
+ else if (arg.startsWith("--live=")) {
1200
+ opts.live = parseYesNo(arg.slice("--live=".length));
1201
+ }
1202
+ }
1203
+ return opts;
1204
+ }
1205
+ function parseAddArgs(args) {
1206
+ const opts = {};
1207
+ for (let i = 0; i < args.length; i += 1) {
1208
+ const arg = args[i];
1209
+ if (arg === "--text") {
1210
+ opts.text = args[i + 1];
1211
+ i += 1;
1212
+ }
1213
+ else if (arg.startsWith("--text=")) {
1214
+ opts.text = arg.slice("--text=".length);
1215
+ }
1216
+ else if (arg === "--heading") {
1217
+ opts.heading = args[i + 1];
1218
+ i += 1;
1219
+ }
1220
+ else if (arg.startsWith("--heading=")) {
1221
+ opts.heading = arg.slice("--heading=".length);
1222
+ }
1223
+ else if (arg === "--label") {
1224
+ opts.label = args[i + 1];
1225
+ i += 1;
1226
+ }
1227
+ else if (arg.startsWith("--label=")) {
1228
+ opts.label = arg.slice("--label=".length);
1229
+ }
1230
+ else if (arg === "--no-heading") {
1231
+ opts.noHeading = true;
1232
+ }
1233
+ else if (arg === "--no-check") {
1234
+ opts.noCheck = true;
1235
+ }
1236
+ else if (!arg.startsWith("-")) {
1237
+ opts.file = arg;
1238
+ }
1239
+ }
1240
+ return opts;
1241
+ }
1242
+ function parseYesNo(raw) {
1243
+ if (!raw)
1244
+ return true;
1245
+ return !(raw === "no" || raw === "false" || raw === "0");
1246
+ }
1247
+ function parseFontFallback(raw) {
1248
+ if (!raw)
1249
+ return "system";
1250
+ if (raw === "none" || raw === "off" || raw === "false" || raw === "0")
1251
+ return "none";
1252
+ return "system";
1253
+ }
1254
+ function parseConfigValue(key, raw) {
1255
+ switch (key) {
1256
+ case "docstepMs":
1257
+ case "docstep-ms":
1258
+ return { key: "docstepMs", value: Number(raw) };
1259
+ case "advanceTime":
1260
+ case "advance-time":
1261
+ return { key: "advanceTime", value: raw !== "0" && raw !== "false" };
1262
+ case "defaultPageSize":
1263
+ case "page":
1264
+ return { key: "defaultPageSize", value: (raw === "A4" ? "A4" : "Letter") };
1265
+ case "defaultTheme":
1266
+ case "theme":
1267
+ return { key: "defaultTheme", value: raw };
1268
+ case "defaultFonts":
1269
+ case "fonts":
1270
+ return { key: "defaultFonts", value: raw };
1271
+ case "defaultOutputDir":
1272
+ case "output":
1273
+ return { key: "defaultOutputDir", value: raw };
1274
+ default:
1275
+ return { key: key, value: raw };
1276
+ }
1277
+ }
1278
+ async function printConfig(config, globals) {
1279
+ if (globals.json) {
1280
+ process.stdout.write(JSON.stringify(config, null, 2) + "\n");
1281
+ return 0;
1282
+ }
1283
+ if (!globals.quiet) {
1284
+ console.log(JSON.stringify(config, null, 2));
1285
+ }
1286
+ return 0;
1287
+ }
1288
+ function printNewNextSteps(docPath) {
1289
+ const pdfPath = docPath.replace(/\.flux$/i, ".pdf");
1290
+ console.log([
1291
+ "",
1292
+ "Next steps:",
1293
+ ` flux view ${docPath}`,
1294
+ ` flux check ${docPath}`,
1295
+ ` flux pdf ${docPath} --out ${pdfPath}`,
1296
+ "",
1297
+ ].join("\n"));
1298
+ }
1299
+ function parseNumberFlag(flag, raw) {
1300
+ const value = Number(raw);
1301
+ if (!Number.isFinite(value)) {
1302
+ throw new Error(`${flag} expects a finite number`);
1303
+ }
1304
+ return value;
1305
+ }
1306
+ function openBrowser(url) {
1307
+ const command = process.platform === "darwin"
1308
+ ? "open"
1309
+ : process.platform === "win32"
1310
+ ? "cmd"
1311
+ : "xdg-open";
1312
+ const args = process.platform === "win32" ? ["/c", "start", url.replace(/&/g, "^&")] : [url];
1313
+ spawn(command, args, { stdio: "ignore", detached: true });
1314
+ }
1315
+ async function launchUi(options) {
1316
+ const { runCliUi } = await import("@flux-lang/cli-ui");
1317
+ await runCliUi({
1318
+ cwd: options.cwd,
1319
+ mode: options.mode,
1320
+ initialArgs: options.initialArgs,
1321
+ detach: options.detach,
1322
+ helpCommand: options.helpCommand,
1323
+ version: options.version,
334
1324
  });
1325
+ return 0;
335
1326
  }
336
- function formatIoError(file, error) {
337
- const err = error;
338
- const code = err.code ?? "UNKNOWN";
339
- return `${file}:0:0: Error: Cannot read file (${code})`;
340
- }
341
- function formatParseOrLexerError(file, error) {
342
- const err = error;
343
- const message = err?.message ?? String(error);
344
- const parseMatch = /Parse error at (\d+):(\d+) near '([^']*)': (.*)/.exec(message);
345
- if (parseMatch) {
346
- const [, line, column, near, detail] = parseMatch;
347
- return `${file}:${line}:${column}: Parse error near '${near}': ${detail}`;
348
- }
349
- const lexMatch = /Lexer error at (\d+):(\d+)\s*-\s*(.*)/.exec(message);
350
- if (lexMatch) {
351
- const [, line, column, detail] = lexMatch;
352
- return `${file}:${line}:${column}: Lexer error: ${detail}`;
353
- }
354
- return `${file}:0:0: ${message}`;
1327
+ function parseFlux(source, filePath) {
1328
+ if (!filePath || filePath === "-") {
1329
+ return parseDocument(source);
1330
+ }
1331
+ const resolved = path.resolve(filePath);
1332
+ return parseDocument(source, {
1333
+ sourcePath: resolved,
1334
+ docRoot: path.dirname(resolved),
1335
+ resolveIncludes: true,
1336
+ });
355
1337
  }