rtgl 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.js +359 -22
  2. package/package.json +3 -3
package/cli.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { build, check, scaffold, watch, examples } from "@rettangoli/fe/cli";
4
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
5
  import { generate, screenshot, report, accept } from "@rettangoli/vt/cli";
7
6
  import { buildSite, watchSite, initSite } from "@rettangoli/sites/cli";
8
7
  import { buildSvg } from "@rettangoli/ui/cli";
@@ -15,6 +14,42 @@ const packageJson = JSON.parse(
15
14
  readFileSync(new URL("./package.json", import.meta.url)),
16
15
  );
17
16
 
17
+ const localBeCliUrl = new URL("../rettangoli-be/src/cli/index.js", import.meta.url);
18
+ const beCli = existsSync(localBeCliUrl)
19
+ ? await import(localBeCliUrl)
20
+ : await import("@rettangoli/be/cli");
21
+
22
+ const {
23
+ app: appBe,
24
+ build: buildBe,
25
+ check: checkBe,
26
+ compat: compatBe,
27
+ db: dbBe,
28
+ init: initBe,
29
+ manifest: manifestBe,
30
+ resume: resumeBe,
31
+ scaffold: scaffoldBe,
32
+ start: startBe,
33
+ test: testBe,
34
+ verify: verifyBe,
35
+ watch: watchBe,
36
+ } = beCli;
37
+ const localBeRuntimeConfigUrl = new URL("../rettangoli-be/src/runtime/loadBeProjectConfig.js", import.meta.url);
38
+ const beRuntimeConfig = existsSync(localBeRuntimeConfigUrl)
39
+ ? await import(localBeRuntimeConfigUrl)
40
+ : await import("@rettangoli/be/runtime/loadBeProjectConfig");
41
+ const { loadBeProjectConfig: loadBeRuntimeConfig } = beRuntimeConfig;
42
+
43
+ function requireBeCommand(handler, name) {
44
+ if (typeof handler !== "function") {
45
+ throw new Error(
46
+ `@rettangoli/be/cli does not export '${name}'. Reinstall @rettangoli/be at the version required by rettangoli-cli.`,
47
+ );
48
+ }
49
+
50
+ return handler;
51
+ }
52
+
18
53
  // Function to read config file
19
54
  function readConfig() {
20
55
  const configPath = resolve(process.cwd(), "rettangoli.config.yaml");
@@ -66,16 +101,105 @@ function parseIsolationOption(value) {
66
101
  function resolveBeRuntimePaths(config) {
67
102
  const be = config?.be || {};
68
103
  const dirs = Array.isArray(be.dirs) && be.dirs.length > 0 ? be.dirs : ["./src/modules"];
104
+ const runtimeConfig = loadBeRuntimeConfig({ cwd: process.cwd() });
69
105
 
70
106
  return {
71
107
  dirs,
72
108
  middlewareDir: be.middlewareDir || "./src/middleware",
73
109
  setup: be.setup || "./src/setup.js",
74
110
  outdir: be.outdir || "./.rtgl-be/generated",
111
+ migrationsDir: be.migrationsDir || "./migrations",
75
112
  domainErrors: be.domainErrors || {},
113
+ globalMiddleware: runtimeConfig.globalMiddleware,
76
114
  };
77
115
  }
78
116
 
117
+ function wantsJsonOutput(options) {
118
+ return options?.json === true || options?.format === "json";
119
+ }
120
+
121
+ function writeBeCliError({ command, options, ruleId = "RTGL-BE-CLI-001", message }) {
122
+ if (!wantsJsonOutput(options)) {
123
+ throw new Error(message);
124
+ }
125
+
126
+ console.log(JSON.stringify({
127
+ schemaVersion: "rettangoli.cliResult/v1",
128
+ command,
129
+ artifactSchemaVersion: "rettangoli.cliError/v1",
130
+ ok: false,
131
+ diagnostics: [
132
+ {
133
+ schemaVersion: "rettangoli.diagnostic/v1",
134
+ ruleId,
135
+ code: ruleId,
136
+ severity: "error",
137
+ phase: "cli",
138
+ message,
139
+ },
140
+ ],
141
+ }, null, 2));
142
+ process.exitCode = 1;
143
+ return null;
144
+ }
145
+
146
+ function requireBeConfig(command, options) {
147
+ let config;
148
+ try {
149
+ config = readConfig();
150
+ } catch (error) {
151
+ return writeBeCliError({
152
+ command,
153
+ options,
154
+ ruleId: "RTGL-BE-CLI-003",
155
+ message: error.message,
156
+ });
157
+ }
158
+
159
+ if (config) {
160
+ return config;
161
+ }
162
+
163
+ return writeBeCliError({
164
+ command,
165
+ options,
166
+ message: "rettangoli.config.yaml not found",
167
+ });
168
+ }
169
+
170
+ function readOptionalBeConfig(command, options) {
171
+ try {
172
+ return {
173
+ ok: true,
174
+ config: readConfig(),
175
+ };
176
+ } catch (error) {
177
+ writeBeCliError({
178
+ command,
179
+ options,
180
+ ruleId: "RTGL-BE-CLI-003",
181
+ message: error.message,
182
+ });
183
+ return {
184
+ ok: false,
185
+ config: null,
186
+ };
187
+ }
188
+ }
189
+
190
+ function resolveBeRuntimePathsOrReport(command, options, config) {
191
+ try {
192
+ return resolveBeRuntimePaths(config);
193
+ } catch (error) {
194
+ return writeBeCliError({
195
+ command,
196
+ options,
197
+ ruleId: "RTGL-BE-CLI-003",
198
+ message: error.message,
199
+ });
200
+ }
201
+ }
202
+
79
203
  const program = new Command();
80
204
 
81
205
  program
@@ -306,33 +430,57 @@ feCommand
306
430
 
307
431
  const beCommand = program.command("be").description("Backend framework");
308
432
 
433
+ beCommand
434
+ .command("init")
435
+ .description("Initialize a minimal backend app")
436
+ .option("--method <method>", "Initial method id", "health.ping")
437
+ .option("--dir <path>", "Backend method directory to create under")
438
+ .option("--dry-run", "Print the init plan without writing files")
439
+ .option("--check", "Alias for --dry-run")
440
+ .option("--json", "Output JSON")
441
+ .action((options) => {
442
+ options.dirs = options.dir || "./src/modules";
443
+ if (options.json) options.format = "json";
444
+ requireBeCommand(initBe, "init")(options);
445
+ });
446
+
309
447
  beCommand
310
448
  .command("build")
311
449
  .description("Build backend method registry and app entry")
450
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
312
451
  .option("-s, --setup-path <path>", "Custom setup file path")
313
452
  .option("-m, --middleware-dir <path>", "Custom middleware directory path")
314
453
  .option("-o, --outdir <path>", "Generated output directory")
454
+ .option("--dry-run", "Print the deterministic build plan without writing files")
455
+ .option("--check", "Check whether generated files are fresh")
456
+ .option("--json", "Output JSON")
315
457
  .action((options) => {
316
- const config = readConfig();
458
+ const config = requireBeConfig("be build", options);
459
+ if (!config) return;
317
460
 
318
- if (!config) {
319
- throw new Error("rettangoli.config.yaml not found");
320
- }
461
+ const bePaths = resolveBeRuntimePathsOrReport("be build", options, config);
462
+ if (!bePaths) return;
321
463
 
322
- const bePaths = resolveBeRuntimePaths(config);
323
-
324
- const missingDirs = bePaths.dirs.filter(
464
+ const selectedDirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
465
+ const missingDirs = selectedDirs.filter(
325
466
  (dir) => !existsSync(resolve(process.cwd(), dir)),
326
467
  );
327
468
  if (missingDirs.length > 0) {
328
- throw new Error(`Directories do not exist: ${missingDirs.join(", ")}`);
469
+ writeBeCliError({
470
+ command: "be build",
471
+ options,
472
+ ruleId: "RTGL-BE-CLI-004",
473
+ message: `Directories do not exist: ${missingDirs.join(", ")}`,
474
+ });
475
+ return;
329
476
  }
330
477
 
331
- options.dirs = bePaths.dirs;
478
+ options.dirs = selectedDirs;
332
479
  options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
333
480
  options.setup = options.setupPath || bePaths.setup;
334
481
  options.outdir = options.outdir || bePaths.outdir;
335
482
  options.domainErrors = bePaths.domainErrors;
483
+ if (options.json) options.format = "json";
336
484
 
337
485
  buildBe(options);
338
486
  });
@@ -341,27 +489,216 @@ beCommand
341
489
  .command("check")
342
490
  .description("Validate backend RPC contracts")
343
491
  .option("--format <format>", "Output format: text or json", "text")
492
+ .option("--json", "Output JSON")
493
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
494
+ .option("--method <method>", "Validate one backend method id")
344
495
  .option("-m, --middleware-dir <path>", "Custom middleware directory path")
345
496
  .action((options) => {
346
- const config = readConfig();
497
+ const config = requireBeConfig("be check", options);
498
+ if (!config) return;
347
499
 
348
- if (!config) {
349
- throw new Error("rettangoli.config.yaml not found");
350
- }
500
+ const bePaths = resolveBeRuntimePathsOrReport("be check", options, config);
501
+ if (!bePaths) return;
351
502
 
352
- const bePaths = resolveBeRuntimePaths(config);
503
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
504
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
505
+ if (options.json) options.format = "json";
353
506
 
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(", ")}`);
507
+ checkBe(options);
508
+ });
509
+
510
+ beCommand
511
+ .command("manifest")
512
+ .description("Print deterministic backend API manifest")
513
+ .option("--json", "Output JSON")
514
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
515
+ .option("--method <method>", "Print manifest for one backend method id")
516
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
517
+ .option("--outdir <path>", "Generated output directory used for target facts")
518
+ .option("--migrations-dir <path>", "SQLite migrations directory")
519
+ .option("-o, --output <path>", "Write manifest JSON to a file")
520
+ .action((options) => {
521
+ const config = requireBeConfig("be manifest", options);
522
+ if (!config) return;
523
+
524
+ const bePaths = resolveBeRuntimePathsOrReport("be manifest", options, config);
525
+ if (!bePaths) return;
526
+
527
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
528
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
529
+ options.outdir = options.outdir || bePaths.outdir;
530
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
531
+
532
+ requireBeCommand(manifestBe, "manifest")(options);
533
+ });
534
+
535
+ beCommand
536
+ .command("compat")
537
+ .description("Compare two backend manifests for API compatibility")
538
+ .option("--from <path>", "Previous manifest JSON")
539
+ .option("--to <path>", "Current manifest JSON")
540
+ .option("--json", "Output JSON")
541
+ .action((options) => {
542
+ if (options.json) options.format = "json";
543
+ if (!options.from || !options.to) {
544
+ writeBeCliError({
545
+ command: "be compat",
546
+ options,
547
+ ruleId: "RTGL-BE-CLI-002",
548
+ message: "be compat requires --from and --to manifest JSON files.",
549
+ });
550
+ return;
359
551
  }
552
+ requireBeCommand(compatBe, "compat")(options);
553
+ });
360
554
 
361
- options.dirs = bePaths.dirs;
555
+ beCommand
556
+ .command("test")
557
+ .description("Run backend executable examples")
558
+ .option("--format <format>", "Output format: text or json", "text")
559
+ .option("--json", "Output JSON")
560
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
561
+ .option("--method <method>", "Run examples for one backend method id")
562
+ .option("-s, --setup-path <path>", "Custom setup file path")
563
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
564
+ .option("--config <path>", "Vitest config path", "./vitest.config.js")
565
+ .option("--test-config <path>", "Alias for --config")
566
+ .option("--package-manager <name>", "Package manager for running Vitest: npm, pnpm, yarn, bun")
567
+ .option("--runner <command>", "Executable used to run Vitest")
568
+ .action((options) => {
569
+ const config = requireBeConfig("be test", options);
570
+ if (!config) return;
571
+
572
+ const bePaths = resolveBeRuntimePathsOrReport("be test", options, config);
573
+ if (!bePaths) return;
574
+
575
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
362
576
  options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
577
+ options.setup = options.setupPath || bePaths.setup;
578
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
579
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
580
+ options.config = options.testConfig || options.config;
581
+ options.executable = options.runner;
582
+ if (options.json) options.format = "json";
363
583
 
364
- checkBe(options);
584
+ requireBeCommand(testBe, "test")(options);
585
+ });
586
+
587
+ beCommand
588
+ .command("verify")
589
+ .description("Run backend check, build, manifest, and examples")
590
+ .option("--format <format>", "Output format: text or json", "text")
591
+ .option("--json", "Output JSON")
592
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
593
+ .option("--method <method>", "Verify one backend method id")
594
+ .option("-s, --setup-path <path>", "Custom setup file path")
595
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
596
+ .option("-o, --outdir <path>", "Generated output directory")
597
+ .option("--migrations-dir <path>", "SQLite migrations directory")
598
+ .option("--fail-on-warnings", "Return non-zero when warnings are present")
599
+ .option("--evidence <taskId>", "Write verification evidence under .rtgl-be/evidence")
600
+ .option("--task-id <taskId>", "Task id for verification evidence")
601
+ .option("--config <path>", "Alias for --test-config")
602
+ .option("--test-config <path>", "Vitest config path", "./vitest.config.js")
603
+ .option("--package-manager <name>", "Package manager for running Vitest: npm, pnpm, yarn, bun")
604
+ .option("--runner <command>", "Executable used to run Vitest")
605
+ .action(async (options) => {
606
+ const config = requireBeConfig("be verify", options);
607
+ if (!config) return;
608
+
609
+ const bePaths = resolveBeRuntimePathsOrReport("be verify", options, config);
610
+ if (!bePaths) return;
611
+
612
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
613
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
614
+ options.setup = options.setupPath || bePaths.setup;
615
+ options.outdir = options.outdir || bePaths.outdir;
616
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
617
+ options.failOnWarnings = !!options.failOnWarnings;
618
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
619
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
620
+ options.testConfig = options.config || options.testConfig;
621
+ options.executable = options.runner;
622
+ if (options.json) options.format = "json";
623
+
624
+ await requireBeCommand(verifyBe, "verify")(options);
625
+ });
626
+
627
+ const beAppCommand = beCommand.command("app").description("Backend app runtime checks");
628
+
629
+ beAppCommand
630
+ .command("check")
631
+ .description("Import setup, handlers, middleware, and instantiate the backend app")
632
+ .option("--json", "Output JSON")
633
+ .option("--dir <path>", "Backend method directory to scan (repeatable)", collectValues, [])
634
+ .option("--method <method>", "Check one backend method id")
635
+ .option("-s, --setup-path <path>", "Custom setup file path")
636
+ .option("-m, --middleware-dir <path>", "Custom middleware directory path")
637
+ .action(async (options) => {
638
+ const config = requireBeConfig("be app check", options);
639
+ if (!config) return;
640
+ const bePaths = resolveBeRuntimePathsOrReport("be app check", options, config);
641
+ if (!bePaths) return;
642
+
643
+ options.dirs = options.dir.length > 0 ? options.dir : bePaths.dirs;
644
+ options.middlewareDir = options.middlewareDir || bePaths.middlewareDir;
645
+ options.setup = options.setupPath || bePaths.setup;
646
+ options.globalMiddlewareBefore = bePaths.globalMiddleware.before;
647
+ options.globalMiddlewareAfter = bePaths.globalMiddleware.after;
648
+ if (options.json) options.format = "json";
649
+
650
+ await requireBeCommand(appBe, "app")(options);
651
+ });
652
+
653
+ beCommand
654
+ .command("scaffold [methodId]")
655
+ .description("Scaffold a backend method package")
656
+ .option("--method <method>", "Backend method id, e.g. user.getProfile")
657
+ .option("--dir <path>", "Backend method directory to create under")
658
+ .option("--dry-run", "Print the scaffold plan without writing files")
659
+ .option("--check", "Alias for --dry-run")
660
+ .option("--json", "Output JSON")
661
+ .action((methodId, options) => {
662
+ const configResult = readOptionalBeConfig("be scaffold", options);
663
+ if (!configResult.ok) return;
664
+ const bePaths = resolveBeRuntimePathsOrReport("be scaffold", options, configResult.config);
665
+ if (!bePaths) return;
666
+
667
+ options.methodId = options.method || methodId;
668
+ options.dirs = options.dir || bePaths.dirs[0] || "./src/modules";
669
+ if (options.json) options.format = "json";
670
+ requireBeCommand(scaffoldBe, "scaffold")(options);
671
+ });
672
+
673
+ const beDbCommand = beCommand.command("db").description("SQLite backend database checks");
674
+
675
+ beDbCommand
676
+ .command("check")
677
+ .description("Validate and replay SQLite migrations")
678
+ .option("--json", "Output JSON")
679
+ .option("--migrations-dir <path>", "SQLite migrations directory")
680
+ .option("--fail-on-warnings", "Return non-zero when warnings are present")
681
+ .action((options) => {
682
+ const configResult = readOptionalBeConfig("be db check", options);
683
+ if (!configResult.ok) return;
684
+ const bePaths = resolveBeRuntimePathsOrReport("be db check", options, configResult.config);
685
+ if (!bePaths) return;
686
+
687
+ options.migrationsDir = options.migrationsDir || bePaths.migrationsDir;
688
+ options.failOnWarnings = !!options.failOnWarnings;
689
+ if (options.json) options.format = "json";
690
+
691
+ requireBeCommand(dbBe, "db")(options);
692
+ });
693
+
694
+ beCommand
695
+ .command("resume <taskId>")
696
+ .description("Resume a backend verification task anchor")
697
+ .option("--json", "Output JSON")
698
+ .action((taskId, options) => {
699
+ options.taskId = taskId;
700
+ if (options.json) options.format = "json";
701
+ requireBeCommand(resumeBe, "resume")(options);
365
702
  });
366
703
 
367
704
  beCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtgl",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI tool for Rettangoli - A frontend framework and development toolkit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,9 +48,9 @@
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",
51
+ "@rettangoli/be": "2.0.0",
52
52
  "@rettangoli/fe": "1.2.0",
53
- "@rettangoli/sites": "1.1.0",
53
+ "@rettangoli/sites": "1.2.0",
54
54
  "@rettangoli/vt": "1.0.5",
55
55
  "@rettangoli/ui": "1.4.2"
56
56
  }