rtgl 1.4.0 → 2.0.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.
Files changed (2) hide show
  1. package/cli.js +412 -31
  2. package/package.json +4 -4
package/cli.js CHANGED
@@ -1,11 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { build, check, scaffold, watch, examples } from "@rettangoli/fe/cli";
4
- import { check as checkContracts } from "@rettangoli/check/cli";
5
- import { build as buildBe, check as checkBe, start as startBe, watch as watchBe } from "@rettangoli/be/cli";
6
- import { generate, screenshot, report, accept } from "@rettangoli/vt/cli";
7
- import { buildSite, watchSite, initSite } from "@rettangoli/sites/cli";
8
- import { buildSvg } from "@rettangoli/ui/cli";
9
3
  import { Command, InvalidArgumentError } from "commander";
10
4
  import { readFileSync, existsSync } from "fs";
11
5
  import { resolve } from "path";
@@ -15,6 +9,91 @@ const packageJson = JSON.parse(
15
9
  readFileSync(new URL("./package.json", import.meta.url)),
16
10
  );
17
11
 
12
+ const requestedCommand = process.argv[2];
13
+
14
+ let build;
15
+ let check;
16
+ let scaffold;
17
+ let watch;
18
+ let examples;
19
+ let checkContracts;
20
+ let generate;
21
+ let screenshot;
22
+ let report;
23
+ let accept;
24
+ let buildSite;
25
+ let watchSite;
26
+ let initSite;
27
+ let buildSvg;
28
+ let appBe;
29
+ let buildBe;
30
+ let checkBe;
31
+ let compatBe;
32
+ let dbBe;
33
+ let initBe;
34
+ let manifestBe;
35
+ let resumeBe;
36
+ let scaffoldBe;
37
+ let startBe;
38
+ let testBe;
39
+ let verifyBe;
40
+ let watchBe;
41
+ let loadBeRuntimeConfig;
42
+
43
+ if (requestedCommand === "fe") {
44
+ ({ build, check, scaffold, watch, examples } = await import(
45
+ "@rettangoli/fe/cli",
46
+ ));
47
+ } else if (requestedCommand === "check") {
48
+ ({ check: checkContracts } = await import("@rettangoli/check/cli"));
49
+ } else if (requestedCommand === "vt") {
50
+ ({ generate, screenshot, report, accept } = await import("@rettangoli/vt/cli"));
51
+ } else if (requestedCommand === "sites") {
52
+ ({ buildSite, watchSite, initSite } = await import("@rettangoli/sites/cli"));
53
+ } else if (requestedCommand === "ui") {
54
+ ({ buildSvg } = await import("@rettangoli/ui/cli"));
55
+ } else if (requestedCommand === "be") {
56
+ const localBeCliUrl = new URL("../rettangoli-be/src/cli/index.js", import.meta.url);
57
+ const beCli = existsSync(localBeCliUrl)
58
+ ? await import(localBeCliUrl)
59
+ : await import("@rettangoli/be/cli");
60
+
61
+ ({
62
+ app: appBe,
63
+ build: buildBe,
64
+ check: checkBe,
65
+ compat: compatBe,
66
+ db: dbBe,
67
+ init: initBe,
68
+ manifest: manifestBe,
69
+ resume: resumeBe,
70
+ scaffold: scaffoldBe,
71
+ start: startBe,
72
+ test: testBe,
73
+ verify: verifyBe,
74
+ watch: watchBe,
75
+ } = beCli);
76
+
77
+ const localBeRuntimeConfigUrl = new URL(
78
+ "../rettangoli-be/src/runtime/loadBeProjectConfig.js",
79
+ import.meta.url,
80
+ );
81
+ const beRuntimeConfig = existsSync(localBeRuntimeConfigUrl)
82
+ ? await import(localBeRuntimeConfigUrl)
83
+ : await import("@rettangoli/be/runtime/loadBeProjectConfig");
84
+ ({ loadBeProjectConfig: loadBeRuntimeConfig } = beRuntimeConfig);
85
+ }
86
+
87
+ function requireBeCommand(handler, name) {
88
+ if (typeof handler !== "function") {
89
+ throw new Error(
90
+ `@rettangoli/be/cli does not export '${name}'. Reinstall @rettangoli/be at the version required by rettangoli-cli.`,
91
+ );
92
+ }
93
+
94
+ return handler;
95
+ }
96
+
18
97
  // Function to read config file
19
98
  function readConfig() {
20
99
  const configPath = resolve(process.cwd(), "rettangoli.config.yaml");
@@ -66,16 +145,105 @@ function parseIsolationOption(value) {
66
145
  function resolveBeRuntimePaths(config) {
67
146
  const be = config?.be || {};
68
147
  const dirs = Array.isArray(be.dirs) && be.dirs.length > 0 ? be.dirs : ["./src/modules"];
148
+ const runtimeConfig = loadBeRuntimeConfig({ cwd: process.cwd() });
69
149
 
70
150
  return {
71
151
  dirs,
72
152
  middlewareDir: be.middlewareDir || "./src/middleware",
73
153
  setup: be.setup || "./src/setup.js",
74
154
  outdir: be.outdir || "./.rtgl-be/generated",
155
+ migrationsDir: be.migrationsDir || "./migrations",
75
156
  domainErrors: be.domainErrors || {},
157
+ globalMiddleware: runtimeConfig.globalMiddleware,
76
158
  };
77
159
  }
78
160
 
161
+ function wantsJsonOutput(options) {
162
+ return options?.json === true || options?.format === "json";
163
+ }
164
+
165
+ function writeBeCliError({ command, options, ruleId = "RTGL-BE-CLI-001", message }) {
166
+ if (!wantsJsonOutput(options)) {
167
+ throw new Error(message);
168
+ }
169
+
170
+ console.log(JSON.stringify({
171
+ schemaVersion: "rettangoli.cliResult/v1",
172
+ command,
173
+ artifactSchemaVersion: "rettangoli.cliError/v1",
174
+ ok: false,
175
+ diagnostics: [
176
+ {
177
+ schemaVersion: "rettangoli.diagnostic/v1",
178
+ ruleId,
179
+ code: ruleId,
180
+ severity: "error",
181
+ phase: "cli",
182
+ message,
183
+ },
184
+ ],
185
+ }, null, 2));
186
+ process.exitCode = 1;
187
+ return null;
188
+ }
189
+
190
+ function requireBeConfig(command, options) {
191
+ let config;
192
+ try {
193
+ config = readConfig();
194
+ } catch (error) {
195
+ return writeBeCliError({
196
+ command,
197
+ options,
198
+ ruleId: "RTGL-BE-CLI-003",
199
+ message: error.message,
200
+ });
201
+ }
202
+
203
+ if (config) {
204
+ return config;
205
+ }
206
+
207
+ return writeBeCliError({
208
+ command,
209
+ options,
210
+ message: "rettangoli.config.yaml not found",
211
+ });
212
+ }
213
+
214
+ function readOptionalBeConfig(command, options) {
215
+ try {
216
+ return {
217
+ ok: true,
218
+ config: readConfig(),
219
+ };
220
+ } catch (error) {
221
+ writeBeCliError({
222
+ command,
223
+ options,
224
+ ruleId: "RTGL-BE-CLI-003",
225
+ message: error.message,
226
+ });
227
+ return {
228
+ ok: false,
229
+ config: null,
230
+ };
231
+ }
232
+ }
233
+
234
+ function resolveBeRuntimePathsOrReport(command, options, config) {
235
+ try {
236
+ return resolveBeRuntimePaths(config);
237
+ } catch (error) {
238
+ return writeBeCliError({
239
+ command,
240
+ options,
241
+ ruleId: "RTGL-BE-CLI-003",
242
+ message: error.message,
243
+ });
244
+ }
245
+ }
246
+
79
247
  const program = new Command();
80
248
 
81
249
  program
@@ -150,7 +318,7 @@ Examples:
150
318
  $ rettangoli fe build --setup-path src/setup.web.js
151
319
  `,
152
320
  )
153
- .action((options) => {
321
+ .action(async (options) => {
154
322
  const config = readConfig();
155
323
 
156
324
  if (!config) {
@@ -180,7 +348,7 @@ Examples:
180
348
  options.outfile = config.fe.outfile;
181
349
  }
182
350
 
183
- build(options);
351
+ await build(options);
184
352
  });
185
353
 
186
354
  feCommand
@@ -261,7 +429,7 @@ Examples:
261
429
  $ rettangoli fe watch --setup-path src/setup.web.js
262
430
  `,
263
431
  )
264
- .action((options) => {
432
+ .action(async (options) => {
265
433
  const config = readConfig();
266
434
 
267
435
  if (!config) {
@@ -291,7 +459,7 @@ Examples:
291
459
  options.outfile = config.fe.outfile;
292
460
  }
293
461
 
294
- watch(options);
462
+ await watch(options);
295
463
  });
296
464
 
297
465
  feCommand
@@ -306,33 +474,57 @@ feCommand
306
474
 
307
475
  const beCommand = program.command("be").description("Backend framework");
308
476
 
477
+ beCommand
478
+ .command("init")
479
+ .description("Initialize a minimal backend app")
480
+ .option("--method <method>", "Initial method id", "health.ping")
481
+ .option("--dir <path>", "Backend method directory to create under")
482
+ .option("--dry-run", "Print the init plan without writing files")
483
+ .option("--check", "Alias for --dry-run")
484
+ .option("--json", "Output JSON")
485
+ .action((options) => {
486
+ options.dirs = options.dir || "./src/modules";
487
+ if (options.json) options.format = "json";
488
+ requireBeCommand(initBe, "init")(options);
489
+ });
490
+
309
491
  beCommand
310
492
  .command("build")
311
493
  .description("Build backend method registry and app entry")
494
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
312
495
  .option("-s, --setup-path <path>", "Custom setup file path")
313
496
  .option("-m, --middleware-dir <path>", "Custom middleware directory path")
314
497
  .option("-o, --outdir <path>", "Generated output directory")
498
+ .option("--dry-run", "Print the deterministic build plan without writing files")
499
+ .option("--check", "Check whether generated files are fresh")
500
+ .option("--json", "Output JSON")
315
501
  .action((options) => {
316
- const config = readConfig();
317
-
318
- if (!config) {
319
- throw new Error("rettangoli.config.yaml not found");
320
- }
502
+ const config = requireBeConfig("be build", options);
503
+ if (!config) return;
321
504
 
322
- const bePaths = resolveBeRuntimePaths(config);
505
+ const bePaths = resolveBeRuntimePathsOrReport("be build", options, config);
506
+ if (!bePaths) return;
323
507
 
324
- const missingDirs = bePaths.dirs.filter(
508
+ const selectedDirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
509
+ const missingDirs = selectedDirs.filter(
325
510
  (dir) => !existsSync(resolve(process.cwd(), dir)),
326
511
  );
327
512
  if (missingDirs.length > 0) {
328
- throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
513
+ writeBeCliError({
514
+ command: "be build",
515
+ options,
516
+ ruleId: "RTGL-BE-CLI-004",
517
+ message: `Directories do not exist: ${missingDirs.join(", ")}`,
518
+ });
519
+ return;
329
520
  }
330
521
 
331
- options.dirs = bePaths.dirs;
522
+ options.dirs = selectedDirs;
332
523
  options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
333
524
  options.setup = options.setupPath || bePaths.setup;
334
525
  options.outdir = options.outdir || bePaths.outdir;
335
526
  options.domainErrors = bePaths.domainErrors;
527
+ if (options.json) options.format = "json";
336
528
 
337
529
  buildBe(options);
338
530
  });
@@ -341,27 +533,216 @@ beCommand
341
533
  .command("check")
342
534
  .description("Validate backend RPC contracts")
343
535
  .option("--format <format>", "Output format: text or json", "text")
536
+ .option("--json", "Output JSON")
537
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
538
+ .option("--method <method>", "Validate one backend method id")
344
539
  .option("-m, --middleware-dir <path>", "Custom middleware directory path")
345
540
  .action((options) => {
346
- const config = readConfig();
541
+ const config = requireBeConfig("be check", options);
542
+ if (!config) return;
347
543
 
348
- if (!config) {
349
- throw new Error("rettangoli.config.yaml not found");
350
- }
544
+ const bePaths = resolveBeRuntimePathsOrReport("be check", options, config);
545
+ if (!bePaths) return;
351
546
 
352
- const bePaths = resolveBeRuntimePaths(config);
547
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
548
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
549
+ if (options.json) options.format = "json";
353
550
 
354
- const missingDirs = bePaths.dirs.filter(
355
- (dir) => !existsSync(resolve(process.cwd(), dir)),
356
- );
357
- if (missingDirs.length > 0) {
358
- throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
551
+ checkBe(options);
552
+ });
553
+
554
+ beCommand
555
+ .command("manifest")
556
+ .description("Print deterministic backend API manifest")
557
+ .option("--json", "Output JSON")
558
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
559
+ .option("--method <method>", "Print manifest for one backend method id")
560
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
561
+ .option("--outdir <path>", "Generated output directory used for target facts")
562
+ .option("--migrations-dir <path>", "SQLite migrations directory")
563
+ .option("-o, --output <path>", "Write manifest JSON to a file")
564
+ .action((options) => {
565
+ const config = requireBeConfig("be manifest", options);
566
+ if (!config) return;
567
+
568
+ const bePaths = resolveBeRuntimePathsOrReport("be manifest", options, config);
569
+ if (!bePaths) return;
570
+
571
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
572
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
573
+ options.outdir = options.outdir || bePaths.outdir;
574
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
575
+
576
+ requireBeCommand(manifestBe, "manifest")(options);
577
+ });
578
+
579
+ beCommand
580
+ .command("compat")
581
+ .description("Compare two backend manifests for API compatibility")
582
+ .option("--from <path>", "Previous manifest JSON")
583
+ .option("--to <path>", "Current manifest JSON")
584
+ .option("--json", "Output JSON")
585
+ .action((options) => {
586
+ if (options.json) options.format = "json";
587
+ if (!options.from || !options.to) {
588
+ writeBeCliError({
589
+ command: "be compat",
590
+ options,
591
+ ruleId: "RTGL-BE-CLI-002",
592
+ message: "be compat requires --from and --to manifest JSON files.",
593
+ });
594
+ return;
359
595
  }
596
+ requireBeCommand(compatBe, "compat")(options);
597
+ });
360
598
 
361
- options.dirs = bePaths.dirs;
599
+ beCommand
600
+ .command("test")
601
+ .description("Run backend executable examples")
602
+ .option("--format <format>", "Output format: text or json", "text")
603
+ .option("--json", "Output JSON")
604
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
605
+ .option("--method <method>", "Run examples for one backend method id")
606
+ .option("-s, --setup-path <path>", "Custom setup file path")
607
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
608
+ .option("--config <path>", "Vitest config path", "./vitest.config.js")
609
+ .option("--test-config <path>", "Alias for --config")
610
+ .option("--package-manager <name>", "Package manager for running Vitest: npm, pnpm, yarn, bun")
611
+ .option("--runner <command>", "Executable used to run Vitest")
612
+ .action((options) => {
613
+ const config = requireBeConfig("be test", options);
614
+ if (!config) return;
615
+
616
+ const bePaths = resolveBeRuntimePathsOrReport("be test", options, config);
617
+ if (!bePaths) return;
618
+
619
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
362
620
  options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
621
+ options.setup = options.setupPath || bePaths.setup;
622
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
623
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
624
+ options.config = options.testConfig || options.config;
625
+ options.executable = options.runner;
626
+ if (options.json) options.format = "json";
363
627
 
364
- checkBe(options);
628
+ requireBeCommand(testBe, "test")(options);
629
+ });
630
+
631
+ beCommand
632
+ .command("verify")
633
+ .description("Run backend check, build, manifest, and examples")
634
+ .option("--format <format>", "Output format: text or json", "text")
635
+ .option("--json", "Output JSON")
636
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
637
+ .option("--method <method>", "Verify one backend method id")
638
+ .option("-s, --setup-path <path>", "Custom setup file path")
639
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
640
+ .option("-o, --outdir <path>", "Generated output directory")
641
+ .option("--migrations-dir <path>", "SQLite migrations directory")
642
+ .option("--fail-on-warnings", "Return non-zero when warnings are present")
643
+ .option("--evidence <taskId>", "Write verification evidence under .rtgl-be/evidence")
644
+ .option("--task-id <taskId>", "Task id for verification evidence")
645
+ .option("--config <path>", "Alias for --test-config")
646
+ .option("--test-config <path>", "Vitest config path", "./vitest.config.js")
647
+ .option("--package-manager <name>", "Package manager for running Vitest: npm, pnpm, yarn, bun")
648
+ .option("--runner <command>", "Executable used to run Vitest")
649
+ .action(async (options) => {
650
+ const config = requireBeConfig("be verify", options);
651
+ if (!config) return;
652
+
653
+ const bePaths = resolveBeRuntimePathsOrReport("be verify", options, config);
654
+ if (!bePaths) return;
655
+
656
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
657
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
658
+ options.setup = options.setupPath || bePaths.setup;
659
+ options.outdir = options.outdir || bePaths.outdir;
660
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
661
+ options.failOnWarnings = !!options.failOnWarnings;
662
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
663
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
664
+ options.testConfig = options.config || options.testConfig;
665
+ options.executable = options.runner;
666
+ if (options.json) options.format = "json";
667
+
668
+ await requireBeCommand(verifyBe, "verify")(options);
669
+ });
670
+
671
+ const beAppCommand = beCommand.command("app").description("Backend app runtime checks");
672
+
673
+ beAppCommand
674
+ .command("check")
675
+ .description("Import setup, handlers, middleware, and instantiate the backend app")
676
+ .option("--json", "Output JSON")
677
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
678
+ .option("--method <method>", "Check one backend method id")
679
+ .option("-s, --setup-path <path>", "Custom setup file path")
680
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
681
+ .action(async (options) => {
682
+ const config = requireBeConfig("be app check", options);
683
+ if (!config) return;
684
+ const bePaths = resolveBeRuntimePathsOrReport("be app check", options, config);
685
+ if (!bePaths) return;
686
+
687
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
688
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
689
+ options.setup = options.setupPath || bePaths.setup;
690
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
691
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
692
+ if (options.json) options.format = "json";
693
+
694
+ await requireBeCommand(appBe, "app")(options);
695
+ });
696
+
697
+ beCommand
698
+ .command("scaffold [methodId]")
699
+ .description("Scaffold a backend method package")
700
+ .option("--method <method>", "Backend method id, e.g. user.getProfile")
701
+ .option("--dir <path>", "Backend method directory to create under")
702
+ .option("--dry-run", "Print the scaffold plan without writing files")
703
+ .option("--check", "Alias for --dry-run")
704
+ .option("--json", "Output JSON")
705
+ .action((methodId, options) => {
706
+ const configResult = readOptionalBeConfig("be scaffold", options);
707
+ if (!configResult.ok) return;
708
+ const bePaths = resolveBeRuntimePathsOrReport("be scaffold", options, configResult.config);
709
+ if (!bePaths) return;
710
+
711
+ options.methodId = options.method || methodId;
712
+ options.dirs = options.dir || bePaths.dirs[0] || "./src/modules";
713
+ if (options.json) options.format = "json";
714
+ requireBeCommand(scaffoldBe, "scaffold")(options);
715
+ });
716
+
717
+ const beDbCommand = beCommand.command("db").description("SQLite backend database checks");
718
+
719
+ beDbCommand
720
+ .command("check")
721
+ .description("Validate and replay SQLite migrations")
722
+ .option("--json", "Output JSON")
723
+ .option("--migrations-dir <path>", "SQLite migrations directory")
724
+ .option("--fail-on-warnings", "Return non-zero when warnings are present")
725
+ .action((options) => {
726
+ const configResult = readOptionalBeConfig("be db check", options);
727
+ if (!configResult.ok) return;
728
+ const bePaths = resolveBeRuntimePathsOrReport("be db check", options, configResult.config);
729
+ if (!bePaths) return;
730
+
731
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
732
+ options.failOnWarnings = !!options.failOnWarnings;
733
+ if (options.json) options.format = "json";
734
+
735
+ requireBeCommand(dbBe, "db")(options);
736
+ });
737
+
738
+ beCommand
739
+ .command("resume <taskId>")
740
+ .description("Resume a backend verification task anchor")
741
+ .option("--json", "Output JSON")
742
+ .action((taskId, options) => {
743
+ options.taskId = taskId;
744
+ if (options.json) options.format = "json";
745
+ requireBeCommand(resumeBe, "resume")(options);
365
746
  });
366
747
 
367
748
  beCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtgl",
3
- "version": "1.4.0",
3
+ "version": "2.0.1",
4
4
  "description": "CLI tool for Rettangoli - A frontend framework and development toolkit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "homepage": "https://rettangoli.dev/",
40
40
  "engines": {
41
- "node": ">=18.0.0"
41
+ "node": "^20.19.0 || >=22.12.0"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public",
@@ -48,8 +48,8 @@
48
48
  "commander": "^14.0.0",
49
49
  "js-yaml": "^4.1.0",
50
50
  "@rettangoli/check": "0.1.2",
51
- "@rettangoli/be": "1.0.2",
52
- "@rettangoli/fe": "1.2.0",
51
+ "@rettangoli/be": "2.0.0",
52
+ "@rettangoli/fe": "1.2.1",
53
53
  "@rettangoli/sites": "1.2.0",
54
54
  "@rettangoli/vt": "1.0.5",
55
55
  "@rettangoli/ui": "1.4.2"