@scenar/cli 0.1.19 → 0.1.20

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 (73) hide show
  1. package/package.json +17 -5
  2. package/src/__tests__/deploy-command.test.ts +45 -0
  3. package/src/__tests__/deploy-flow.test.ts +125 -0
  4. package/src/__tests__/deploy-upload.test.ts +39 -0
  5. package/src/__tests__/embed-snippet.test.ts +56 -0
  6. package/src/__tests__/pack-bundle-contract.test.ts +135 -0
  7. package/src/__tests__/pack-command.test.ts +51 -0
  8. package/src/__tests__/pack-generate-embed-entry.test.ts +90 -0
  9. package/src/__tests__/pack-manifest.test.ts +138 -0
  10. package/src/__tests__/viewport.test.ts +33 -0
  11. package/src/commands/deploy.d.ts +3 -0
  12. package/src/commands/deploy.d.ts.map +1 -0
  13. package/src/commands/deploy.js +166 -0
  14. package/src/commands/deploy.js.map +1 -0
  15. package/src/commands/deploy.ts +204 -0
  16. package/src/commands/pack.d.ts +3 -0
  17. package/src/commands/pack.d.ts.map +1 -0
  18. package/src/commands/pack.js +156 -0
  19. package/src/commands/pack.js.map +1 -0
  20. package/src/commands/pack.ts +189 -0
  21. package/src/deploy/client.d.ts +20 -0
  22. package/src/deploy/client.d.ts.map +1 -0
  23. package/src/deploy/client.js +22 -0
  24. package/src/deploy/client.js.map +1 -0
  25. package/src/deploy/client.ts +28 -0
  26. package/src/deploy/deploy-flow.d.ts +78 -0
  27. package/src/deploy/deploy-flow.d.ts.map +1 -0
  28. package/src/deploy/deploy-flow.js +53 -0
  29. package/src/deploy/deploy-flow.js.map +1 -0
  30. package/src/deploy/deploy-flow.ts +115 -0
  31. package/src/deploy/embed-snippet.d.ts +27 -0
  32. package/src/deploy/embed-snippet.d.ts.map +1 -0
  33. package/src/deploy/embed-snippet.js +40 -0
  34. package/src/deploy/embed-snippet.js.map +1 -0
  35. package/src/deploy/embed-snippet.ts +52 -0
  36. package/src/deploy/upload.d.ts +10 -0
  37. package/src/deploy/upload.d.ts.map +1 -0
  38. package/src/deploy/upload.js +23 -0
  39. package/src/deploy/upload.js.map +1 -0
  40. package/src/deploy/upload.ts +29 -0
  41. package/src/index.d.ts.map +1 -1
  42. package/src/index.js +5 -1
  43. package/src/index.js.map +1 -1
  44. package/src/index.ts +5 -1
  45. package/src/pack/build.d.ts +17 -0
  46. package/src/pack/build.d.ts.map +1 -0
  47. package/src/pack/build.js +42 -0
  48. package/src/pack/build.js.map +1 -0
  49. package/src/pack/build.ts +59 -0
  50. package/src/pack/bundle-contract.d.ts +61 -0
  51. package/src/pack/bundle-contract.d.ts.map +1 -0
  52. package/src/pack/bundle-contract.js +116 -0
  53. package/src/pack/bundle-contract.js.map +1 -0
  54. package/src/pack/bundle-contract.ts +128 -0
  55. package/src/pack/generate-embed-entry.d.ts +42 -0
  56. package/src/pack/generate-embed-entry.d.ts.map +1 -0
  57. package/src/pack/generate-embed-entry.js +125 -0
  58. package/src/pack/generate-embed-entry.js.map +1 -0
  59. package/src/pack/generate-embed-entry.ts +162 -0
  60. package/src/pack/pack-manifest.d.ts +45 -0
  61. package/src/pack/pack-manifest.d.ts.map +1 -0
  62. package/src/pack/pack-manifest.js +95 -0
  63. package/src/pack/pack-manifest.js.map +1 -0
  64. package/src/pack/pack-manifest.ts +146 -0
  65. package/src/pack/viewport.d.ts +30 -0
  66. package/src/pack/viewport.d.ts.map +1 -0
  67. package/src/pack/viewport.js +19 -0
  68. package/src/pack/viewport.js.map +1 -0
  69. package/src/pack/viewport.ts +40 -0
  70. package/src/util/load-yaml.d.ts +1 -1
  71. package/src/util/load-yaml.js +1 -1
  72. package/src/util/load-yaml.ts +1 -1
  73. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, writeFile, stat } from "node:fs/promises";
3
+ import { join, relative, sep } from "node:path";
4
+ import { CONTENT_TYPE_BY_EXTENSION, finalExtension, validateRelativePath, validateScenarioJson, } from "./bundle-contract.js";
5
+ /** Name of the manifest file written at the bundle root (not itself uploaded). */
6
+ export const PACK_MANIFEST_FILE = "pack-manifest.json";
7
+ /** Name of the required scenario descriptor at the bundle root. */
8
+ export const SCENARIO_JSON_FILE = "scenario.json";
9
+ /**
10
+ * Write the bundle's scenario.json — the required-at-root descriptor. Kept
11
+ * intentionally minimal (a validated metadata header) plus the canonical
12
+ * `viewport` baked into the bundle: the player reads its steps from the bundled
13
+ * JS, so the full serialized spec is still a follow-up, but `deploy` needs the
14
+ * viewport to derive a correctly-proportioned embed snippet (DD-004). The
15
+ * viewport mirrors `ViewportConfig { width, height }`. Validated against the
16
+ * backend rules before writing (which accept the extra `viewport` key).
17
+ */
18
+ export async function writeScenarioJson(outDir, scenarioId, generatorVersion, viewport) {
19
+ const content = JSON.stringify({
20
+ schemaVersion: "1",
21
+ id: scenarioId,
22
+ generator: `@scenar/cli pack ${generatorVersion}`,
23
+ viewport: { width: viewport.width, height: viewport.height },
24
+ }, null, 2);
25
+ const error = validateScenarioJson(content);
26
+ if (error) {
27
+ throw new Error(`Generated scenario.json is invalid: ${error}`);
28
+ }
29
+ await writeFile(join(outDir, SCENARIO_JSON_FILE), content, "utf-8");
30
+ }
31
+ /**
32
+ * Walk the build output and produce the pack manifest: one validated entry per
33
+ * file (excluding the manifest itself), with a lowercase-hex SHA-256, byte size,
34
+ * and content type. Throws with a clear message if any file violates the bundle
35
+ * contract (e.g. a disallowed extension), so the failure is local and obvious.
36
+ */
37
+ export async function buildPackManifest(outDir, scenarioId) {
38
+ const absPaths = await walkFiles(outDir);
39
+ const files = [];
40
+ for (const abs of absPaths) {
41
+ const rel = toPosix(relative(outDir, abs));
42
+ if (rel === PACK_MANIFEST_FILE) {
43
+ continue; // the manifest never lists itself.
44
+ }
45
+ const pathError = validateRelativePath(rel);
46
+ if (pathError) {
47
+ throw new Error(`Bundle contains a file that the deploy allowlist would reject:\n ${pathError}\n\n` +
48
+ "The deploy allowlist covers HTML/JS/CSS/JSON, MP3 narration, raster images\n" +
49
+ "(png/jpg/jpeg/gif/webp/avif), and woff2/woff fonts. SVG is not a served type:\n" +
50
+ "inline it as a React component or a data URI rather than emitting an .svg file.");
51
+ }
52
+ const bytes = await readFile(abs);
53
+ files.push({
54
+ path: rel,
55
+ sha256: createHash("sha256").update(bytes).digest("hex"),
56
+ sizeBytes: bytes.length,
57
+ contentType: CONTENT_TYPE_BY_EXTENSION[finalExtension(rel)],
58
+ });
59
+ }
60
+ files.sort((a, b) => a.path.localeCompare(b.path));
61
+ return { schemaVersion: 1, scenarioId, files };
62
+ }
63
+ /** Write pack-manifest.json at the bundle root. */
64
+ export async function writePackManifest(outDir, manifest) {
65
+ await writeFile(join(outDir, PACK_MANIFEST_FILE), JSON.stringify(manifest, null, 2), "utf-8");
66
+ }
67
+ /** Recursively collect absolute file paths under a directory. */
68
+ async function walkFiles(dir) {
69
+ const out = [];
70
+ const entries = await readdir(dir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const abs = join(dir, entry.name);
73
+ if (entry.isDirectory()) {
74
+ out.push(...(await walkFiles(abs)));
75
+ }
76
+ else if (entry.isFile()) {
77
+ out.push(abs);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /** True if every listed file exists on disk under outDir with the recorded size. */
83
+ export async function verifyManifestFilesExist(outDir, manifest) {
84
+ for (const file of manifest.files) {
85
+ const abs = join(outDir, ...file.path.split("/"));
86
+ const info = await stat(abs).catch(() => null);
87
+ if (!info || !info.isFile()) {
88
+ throw new Error(`pack-manifest lists a missing file: ${file.path}`);
89
+ }
90
+ }
91
+ }
92
+ function toPosix(p) {
93
+ return sep === "/" ? p : p.split(sep).join("/");
94
+ }
95
+ //# sourceMappingURL=pack-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack-manifest.js","sourceRoot":"","sources":["../../../src/pack/pack-manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, writeFile, stat } from "node:fs/promises";
3
+ import { join, relative, sep } from "node:path";
4
+ import {
5
+ CONTENT_TYPE_BY_EXTENSION,
6
+ finalExtension,
7
+ validateRelativePath,
8
+ validateScenarioJson,
9
+ } from "./bundle-contract.js";
10
+ import type { Viewport } from "./viewport.js";
11
+
12
+ /** One uploadable file, described exactly as the deploy session needs it. */
13
+ export interface PackFile {
14
+ /** Bundle-relative POSIX path (e.g. "assets/index-abc.js"). */
15
+ readonly path: string;
16
+ /** Lowercase-hex SHA-256 of the bytes (64 chars). */
17
+ readonly sha256: string;
18
+ /** Byte length (> 0). */
19
+ readonly sizeBytes: number;
20
+ /** Content type the server binds into the presign and the edge serves. */
21
+ readonly contentType: string;
22
+ }
23
+
24
+ /** The deploy instruction file `scenar deploy` consumes. */
25
+ export interface PackManifest {
26
+ readonly schemaVersion: number;
27
+ readonly scenarioId: string;
28
+ /** Files to upload — excludes the manifest itself. */
29
+ readonly files: PackFile[];
30
+ }
31
+
32
+ /** Name of the manifest file written at the bundle root (not itself uploaded). */
33
+ export const PACK_MANIFEST_FILE = "pack-manifest.json";
34
+
35
+ /** Name of the required scenario descriptor at the bundle root. */
36
+ export const SCENARIO_JSON_FILE = "scenario.json";
37
+
38
+ /**
39
+ * Write the bundle's scenario.json — the required-at-root descriptor. Kept
40
+ * intentionally minimal (a validated metadata header) plus the canonical
41
+ * `viewport` baked into the bundle: the player reads its steps from the bundled
42
+ * JS, so the full serialized spec is still a follow-up, but `deploy` needs the
43
+ * viewport to derive a correctly-proportioned embed snippet (DD-004). The
44
+ * viewport mirrors `ViewportConfig { width, height }`. Validated against the
45
+ * backend rules before writing (which accept the extra `viewport` key).
46
+ */
47
+ export async function writeScenarioJson(
48
+ outDir: string,
49
+ scenarioId: string,
50
+ generatorVersion: string,
51
+ viewport: Viewport,
52
+ ): Promise<void> {
53
+ const content = JSON.stringify(
54
+ {
55
+ schemaVersion: "1",
56
+ id: scenarioId,
57
+ generator: `@scenar/cli pack ${generatorVersion}`,
58
+ viewport: { width: viewport.width, height: viewport.height },
59
+ },
60
+ null,
61
+ 2,
62
+ );
63
+ const error = validateScenarioJson(content);
64
+ if (error) {
65
+ throw new Error(`Generated scenario.json is invalid: ${error}`);
66
+ }
67
+ await writeFile(join(outDir, SCENARIO_JSON_FILE), content, "utf-8");
68
+ }
69
+
70
+ /**
71
+ * Walk the build output and produce the pack manifest: one validated entry per
72
+ * file (excluding the manifest itself), with a lowercase-hex SHA-256, byte size,
73
+ * and content type. Throws with a clear message if any file violates the bundle
74
+ * contract (e.g. a disallowed extension), so the failure is local and obvious.
75
+ */
76
+ export async function buildPackManifest(outDir: string, scenarioId: string): Promise<PackManifest> {
77
+ const absPaths = await walkFiles(outDir);
78
+ const files: PackFile[] = [];
79
+
80
+ for (const abs of absPaths) {
81
+ const rel = toPosix(relative(outDir, abs));
82
+ if (rel === PACK_MANIFEST_FILE) {
83
+ continue; // the manifest never lists itself.
84
+ }
85
+
86
+ const pathError = validateRelativePath(rel);
87
+ if (pathError) {
88
+ throw new Error(
89
+ `Bundle contains a file that the deploy allowlist would reject:\n ${pathError}\n\n` +
90
+ "The deploy allowlist covers HTML/JS/CSS/JSON, MP3 narration, raster images\n" +
91
+ "(png/jpg/jpeg/gif/webp/avif), and woff2/woff fonts. SVG is not a served type:\n" +
92
+ "inline it as a React component or a data URI rather than emitting an .svg file.",
93
+ );
94
+ }
95
+
96
+ const bytes = await readFile(abs);
97
+ files.push({
98
+ path: rel,
99
+ sha256: createHash("sha256").update(bytes).digest("hex"),
100
+ sizeBytes: bytes.length,
101
+ contentType: CONTENT_TYPE_BY_EXTENSION[finalExtension(rel) as keyof typeof CONTENT_TYPE_BY_EXTENSION],
102
+ });
103
+ }
104
+
105
+ files.sort((a, b) => a.path.localeCompare(b.path));
106
+ return { schemaVersion: 1, scenarioId, files };
107
+ }
108
+
109
+ /** Write pack-manifest.json at the bundle root. */
110
+ export async function writePackManifest(outDir: string, manifest: PackManifest): Promise<void> {
111
+ await writeFile(
112
+ join(outDir, PACK_MANIFEST_FILE),
113
+ JSON.stringify(manifest, null, 2),
114
+ "utf-8",
115
+ );
116
+ }
117
+
118
+ /** Recursively collect absolute file paths under a directory. */
119
+ async function walkFiles(dir: string): Promise<string[]> {
120
+ const out: string[] = [];
121
+ const entries = await readdir(dir, { withFileTypes: true });
122
+ for (const entry of entries) {
123
+ const abs = join(dir, entry.name);
124
+ if (entry.isDirectory()) {
125
+ out.push(...(await walkFiles(abs)));
126
+ } else if (entry.isFile()) {
127
+ out.push(abs);
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /** True if every listed file exists on disk under outDir with the recorded size. */
134
+ export async function verifyManifestFilesExist(outDir: string, manifest: PackManifest): Promise<void> {
135
+ for (const file of manifest.files) {
136
+ const abs = join(outDir, ...file.path.split("/"));
137
+ const info = await stat(abs).catch(() => null);
138
+ if (!info || !info.isFile()) {
139
+ throw new Error(`pack-manifest lists a missing file: ${file.path}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ function toPosix(p: string): string {
145
+ return sep === "/" ? p : p.split(sep).join("/");
146
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Canonical embed dimensions, mirroring `ai.scenar.scenario.v1.ViewportConfig`:
3
+ * `width` is the canonical render width and `height` the shell/container height
4
+ * (the CLI's `--shell-height`). The engine renders at these fixed dimensions and
5
+ * CSS-zooms to fit the container, so they also define the embed's intrinsic
6
+ * aspect ratio.
7
+ *
8
+ * The proto owns the *concept and vocabulary*; in the code-authored CLI path
9
+ * there is no `ViewportConfig` instance to read, so `pack` resolves the
10
+ * dimensions (flags or these defaults), bakes them into the bundle via
11
+ * `DemoViewport`, and records them in `scenario.json` so `deploy` can derive a
12
+ * correctly-proportioned embed snippet without re-deriving them (DD-004). This
13
+ * one module is the single home for the defaults, consumed by both the bake
14
+ * (`pack`) and the fallback (`deploy`) so they cannot drift.
15
+ */
16
+ export interface Viewport {
17
+ /** Canonical render width in px (ViewportConfig.width). */
18
+ readonly width: number;
19
+ /** Shell/container height in px (ViewportConfig.height; CLI `--shell-height`). */
20
+ readonly height: number;
21
+ }
22
+ /** Defaults matching `DemoViewport`: 896 canonical width, 480 shell height. */
23
+ export declare const DEFAULT_VIEWPORT: Viewport;
24
+ /**
25
+ * Narrow unknown JSON into a {@link Viewport}, or null if it is missing or
26
+ * malformed (non-object, non-positive, or non-finite dimensions). Lets `deploy`
27
+ * accept a recorded viewport while falling back cleanly for older bundles.
28
+ */
29
+ export declare function parseViewport(value: unknown): Viewport | null;
30
+ //# sourceMappingURL=viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,QAAsC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAK7D"}
@@ -0,0 +1,19 @@
1
+ /** Defaults matching `DemoViewport`: 896 canonical width, 480 shell height. */
2
+ export const DEFAULT_VIEWPORT = { width: 896, height: 480 };
3
+ /**
4
+ * Narrow unknown JSON into a {@link Viewport}, or null if it is missing or
5
+ * malformed (non-object, non-positive, or non-finite dimensions). Lets `deploy`
6
+ * accept a recorded viewport while falling back cleanly for older bundles.
7
+ */
8
+ export function parseViewport(value) {
9
+ if (typeof value !== "object" || value === null)
10
+ return null;
11
+ const { width, height } = value;
12
+ if (!isPositiveInt(width) || !isPositiveInt(height))
13
+ return null;
14
+ return { width, height };
15
+ }
16
+ function isPositiveInt(value) {
17
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
18
+ }
19
+ //# sourceMappingURL=viewport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.js","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAsBA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAa,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAgC,CAAC;IAC3D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Canonical embed dimensions, mirroring `ai.scenar.scenario.v1.ViewportConfig`:
3
+ * `width` is the canonical render width and `height` the shell/container height
4
+ * (the CLI's `--shell-height`). The engine renders at these fixed dimensions and
5
+ * CSS-zooms to fit the container, so they also define the embed's intrinsic
6
+ * aspect ratio.
7
+ *
8
+ * The proto owns the *concept and vocabulary*; in the code-authored CLI path
9
+ * there is no `ViewportConfig` instance to read, so `pack` resolves the
10
+ * dimensions (flags or these defaults), bakes them into the bundle via
11
+ * `DemoViewport`, and records them in `scenario.json` so `deploy` can derive a
12
+ * correctly-proportioned embed snippet without re-deriving them (DD-004). This
13
+ * one module is the single home for the defaults, consumed by both the bake
14
+ * (`pack`) and the fallback (`deploy`) so they cannot drift.
15
+ */
16
+ export interface Viewport {
17
+ /** Canonical render width in px (ViewportConfig.width). */
18
+ readonly width: number;
19
+ /** Shell/container height in px (ViewportConfig.height; CLI `--shell-height`). */
20
+ readonly height: number;
21
+ }
22
+
23
+ /** Defaults matching `DemoViewport`: 896 canonical width, 480 shell height. */
24
+ export const DEFAULT_VIEWPORT: Viewport = { width: 896, height: 480 };
25
+
26
+ /**
27
+ * Narrow unknown JSON into a {@link Viewport}, or null if it is missing or
28
+ * malformed (non-object, non-positive, or non-finite dimensions). Lets `deploy`
29
+ * accept a recorded viewport while falling back cleanly for older bundles.
30
+ */
31
+ export function parseViewport(value: unknown): Viewport | null {
32
+ if (typeof value !== "object" || value === null) return null;
33
+ const { width, height } = value as Record<string, unknown>;
34
+ if (!isPositiveInt(width) || !isPositiveInt(height)) return null;
35
+ return { width, height };
36
+ }
37
+
38
+ function isPositiveInt(value: unknown): value is number {
39
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
40
+ }
@@ -6,7 +6,7 @@ export declare function snakeToCamelKeys(value: unknown): unknown;
6
6
  /**
7
7
  * Reads a scenario YAML file from disk, parses it, and converts
8
8
  * snake_case field names to the camelCase convention used by the
9
- * SDK's `ProtoScenario` type.
9
+ * SDK's `ProtoScenarioSpec` type.
10
10
  *
11
11
  * @throws {Error} on file-not-found or YAML parse failure.
12
12
  */
@@ -23,7 +23,7 @@ function snakeToCamel(s) {
23
23
  /**
24
24
  * Reads a scenario YAML file from disk, parses it, and converts
25
25
  * snake_case field names to the camelCase convention used by the
26
- * SDK's `ProtoScenario` type.
26
+ * SDK's `ProtoScenarioSpec` type.
27
27
  *
28
28
  * @throws {Error} on file-not-found or YAML parse failure.
29
29
  */
@@ -26,7 +26,7 @@ function snakeToCamel(s: string): string {
26
26
  /**
27
27
  * Reads a scenario YAML file from disk, parses it, and converts
28
28
  * snake_case field names to the camelCase convention used by the
29
- * SDK's `ProtoScenario` type.
29
+ * SDK's `ProtoScenarioSpec` type.
30
30
  *
31
31
  * @throws {Error} on file-not-found or YAML parse failure.
32
32
  */