@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 +9 -1
- package/dist/main.js +49 -4
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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").
|
|
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(
|
|
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