@novedu/cli 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # @novedu/cli
2
2
 
3
3
  Command-line companion for the Novedu chat app (installed command: `novedu-cli`).
4
- Today it validates **tutor YAML** definitions; more commands will follow.
4
+ Today it validates **tutor YAML** definitions and **fragment libraries**; more
5
+ commands will follow. Validating a tutor also fully validates every fragment
6
+ library it references; pass `--kind fragment` to validate a fragment library on
7
+ its own.
5
8
 
6
9
  It reuses the app's exact validation pipeline (`lib/tutors`), so a tutor that
7
10
  passes here is the same tutor the app would accept — no separate, drifting rules.
@@ -15,10 +18,15 @@ npx @novedu/cli validate ./tutors/simple-tutor.yaml
15
18
  # Validate a published tutor by URL
16
19
  npx @novedu/cli validate https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/tutors/simple-tutor.yaml
17
20
 
21
+ # Validate a fragment library on its own
22
+ npx @novedu/cli validate ./tutors/simple-fragments.yaml --kind fragment
23
+
18
24
  # Machine-readable output (the raw validation result)
19
25
  npx @novedu/cli validate ./tutors/simple-tutor.yaml --json
20
26
  ```
21
27
 
28
+ `--kind` defaults to `tutor`; it is caller-declared, not auto-detected.
29
+
22
30
  Exit code is `0` when the tutor is valid and `1` when it has errors, so it works
23
31
  as a pre-commit / CI gate.
24
32
 
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import { Command } from "commander";
3
4
  import { resolve } from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -40,6 +41,26 @@ function warning(code, message, extra = {}) {
40
41
  ...extra
41
42
  };
42
43
  }
44
+ /**
45
+ * Flattens a treeified Zod error into `path: message` lines so a generic
46
+ * "Document does not match the expected structure" becomes actionable — e.g.
47
+ * `Unrecognized key: "nae"` and `name: Invalid input: expected string`.
48
+ * Framework-agnostic: shared by the web UI (`ErrorList`), the share-tutor
49
+ * action, and the CLI formatter, so a schema error reads the same everywhere.
50
+ */
51
+ function formatZodIssues(zodIssues) {
52
+ const out = [];
53
+ const walk = (node, path) => {
54
+ if (!node || typeof node !== "object") return;
55
+ if (Array.isArray(node.errors)) for (const message of node.errors) out.push(path.length ? `${path.join(".")}: ${message}` : message);
56
+ if (node.properties) for (const [key, child] of Object.entries(node.properties)) walk(child, [...path, key]);
57
+ if (Array.isArray(node.items)) node.items.forEach((child, index) => {
58
+ walk(child, [...path, String(index)]);
59
+ });
60
+ };
61
+ walk(zodIssues, []);
62
+ return out;
63
+ }
43
64
  //#endregion
44
65
  //#region ../lib/tutors/consistency.ts
45
66
  /** Compare a supplied value against its declared property type. Returns null when it matches. */
@@ -635,6 +656,20 @@ function context(item) {
635
656
  function renderWarnings(warnings) {
636
657
  return warnings.map((w) => ` ${yellow("⚠")} ${yellow(w.code)} ${w.message}${context(w)}`);
637
658
  }
659
+ /**
660
+ * Render each error as a line, with any flattened Zod schema-issue detail
661
+ * indented beneath it — so a generic "Document does not match the expected
662
+ * structure" is followed by the actual field paths (e.g. `Unrecognized key:
663
+ * "nae"`), matching what the web UI shows.
664
+ */
665
+ function renderErrors(errors) {
666
+ const lines = [];
667
+ for (const e of errors) {
668
+ lines.push(` ${red("✗")} ${red(e.code)} ${e.message}${context(e)}`);
669
+ if (e.zodIssues) for (const issue of formatZodIssues(e.zodIssues)) lines.push(` ${dim(issue)}`);
670
+ }
671
+ return lines;
672
+ }
638
673
  function formatResult(result, source) {
639
674
  const lines = [];
640
675
  if (result.ok) {
@@ -652,7 +687,7 @@ function formatResult(result, source) {
652
687
  lines.push(red(`✘ Invalid tutor`) + dim(` — ${source}`));
653
688
  lines.push("");
654
689
  lines.push(red(`${result.errors.length} error(s):`));
655
- for (const e of result.errors) lines.push(` ${red("✗")} ${red(e.code)} ${e.message}${context(e)}`);
690
+ lines.push(...renderErrors(result.errors));
656
691
  if (result.warnings.length) {
657
692
  lines.push("");
658
693
  lines.push(yellow(`${result.warnings.length} warning(s):`));
@@ -677,7 +712,7 @@ function formatFragmentResult(result, source) {
677
712
  lines.push(red(`✘ Invalid fragment file`) + dim(` — ${source}`));
678
713
  lines.push("");
679
714
  lines.push(red(`${result.errors.length} error(s):`));
680
- for (const e of result.errors) lines.push(` ${red("✗")} ${red(e.code)} ${e.message}${context(e)}`);
715
+ lines.push(...renderErrors(result.errors));
681
716
  if (result.warnings.length) {
682
717
  lines.push("");
683
718
  lines.push(yellow(`${result.warnings.length} warning(s):`));
@@ -723,7 +758,16 @@ function runValidate(pathOrUrl, kind) {
723
758
  }));
724
759
  }
725
760
  function registerValidate(program) {
726
- program.command("validate").description("Validate a tutor YAML (default) or a fragment library by local path or public http(s) URL").argument("<pathOrUrl>", "path to a tutor or fragment YAML file, or a public http(s) URL").option("--kind <kind>", "what the file is: 'tutor' (default) or 'fragment'", "tutor").option("--json", "print the raw validation result as JSON").action(async (pathOrUrl, options) => {
761
+ program.command("validate").description("Validate a tutor YAML (default) or a fragment library by local path or public http(s) URL").argument("<pathOrUrl>", "path to a tutor or fragment YAML file, or a public http(s) URL").option("--kind <kind>", "what the file is: 'tutor' (default) or 'fragment'", "tutor").option("--json", "print the raw validation result as JSON").addHelpText("after", `
762
+ Examples:
763
+ # Validate a tutor (also strict-renders every fragment in every referenced library)
764
+ $ novedu-cli validate ./tutors/my-tutor.yaml
765
+
766
+ # Validate a fragment library on its own
767
+ $ novedu-cli validate ./tutors/my-fragments.yaml --kind fragment
768
+
769
+ # Machine-readable output for CI
770
+ $ novedu-cli validate https://example.com/tutor.yaml --json`).action(async (pathOrUrl, options) => {
727
771
  if (options.kind !== void 0 && options.kind !== "tutor" && options.kind !== "fragment") {
728
772
  console.error(`Invalid --kind "${options.kind}": expected "tutor" or "fragment".`);
729
773
  process.exitCode = 1;
@@ -737,8 +781,9 @@ function registerValidate(program) {
737
781
  }
738
782
  //#endregion
739
783
  //#region src/main.ts
784
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
740
785
  const program = new Command();
741
- program.name("novedu-cli").description("Command-line companion for the Novedu chat app").version("0.2.0");
786
+ program.name("novedu-cli").description("Command-line companion for the Novedu chat app").version(version);
742
787
  registerValidate(program);
743
788
  program.parseAsync().catch((err) => {
744
789
  console.error(err instanceof Error ? err.message : err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novedu/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Command-line companion for the Novedu chat app. Validates tutor YAML definitions and fragment libraries (more commands to follow).",
5
5
  "type": "module",
6
6
  "repository": {