@scenar/cli 0.1.18 → 0.1.20-rc.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/package.json +17 -5
- package/src/__tests__/deploy-command.test.ts +45 -0
- package/src/__tests__/deploy-flow.test.ts +125 -0
- package/src/__tests__/deploy-upload.test.ts +39 -0
- package/src/__tests__/edge-tts-provider.test.ts +6 -6
- package/src/__tests__/embed-snippet.test.ts +56 -0
- package/src/__tests__/pack-bundle-contract.test.ts +135 -0
- package/src/__tests__/pack-command.test.ts +51 -0
- package/src/__tests__/pack-generate-embed-entry.test.ts +90 -0
- package/src/__tests__/pack-manifest.test.ts +138 -0
- package/src/__tests__/viewport.test.ts +33 -0
- package/src/commands/deploy.d.ts +3 -0
- package/src/commands/deploy.d.ts.map +1 -0
- package/src/commands/deploy.js +166 -0
- package/src/commands/deploy.js.map +1 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/pack.d.ts +3 -0
- package/src/commands/pack.d.ts.map +1 -0
- package/src/commands/pack.js +156 -0
- package/src/commands/pack.js.map +1 -0
- package/src/commands/pack.ts +189 -0
- package/src/deploy/client.d.ts +20 -0
- package/src/deploy/client.d.ts.map +1 -0
- package/src/deploy/client.js +22 -0
- package/src/deploy/client.js.map +1 -0
- package/src/deploy/client.ts +28 -0
- package/src/deploy/deploy-flow.d.ts +78 -0
- package/src/deploy/deploy-flow.d.ts.map +1 -0
- package/src/deploy/deploy-flow.js +53 -0
- package/src/deploy/deploy-flow.js.map +1 -0
- package/src/deploy/deploy-flow.ts +115 -0
- package/src/deploy/embed-snippet.d.ts +27 -0
- package/src/deploy/embed-snippet.d.ts.map +1 -0
- package/src/deploy/embed-snippet.js +40 -0
- package/src/deploy/embed-snippet.js.map +1 -0
- package/src/deploy/embed-snippet.ts +52 -0
- package/src/deploy/upload.d.ts +10 -0
- package/src/deploy/upload.d.ts.map +1 -0
- package/src/deploy/upload.js +23 -0
- package/src/deploy/upload.js.map +1 -0
- package/src/deploy/upload.ts +29 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/index.ts +5 -1
- package/src/pack/build.d.ts +17 -0
- package/src/pack/build.d.ts.map +1 -0
- package/src/pack/build.js +42 -0
- package/src/pack/build.js.map +1 -0
- package/src/pack/build.ts +59 -0
- package/src/pack/bundle-contract.d.ts +61 -0
- package/src/pack/bundle-contract.d.ts.map +1 -0
- package/src/pack/bundle-contract.js +116 -0
- package/src/pack/bundle-contract.js.map +1 -0
- package/src/pack/bundle-contract.ts +128 -0
- package/src/pack/generate-embed-entry.d.ts +42 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -0
- package/src/pack/generate-embed-entry.js +125 -0
- package/src/pack/generate-embed-entry.js.map +1 -0
- package/src/pack/generate-embed-entry.ts +162 -0
- package/src/pack/pack-manifest.d.ts +45 -0
- package/src/pack/pack-manifest.d.ts.map +1 -0
- package/src/pack/pack-manifest.js +95 -0
- package/src/pack/pack-manifest.js.map +1 -0
- package/src/pack/pack-manifest.ts +146 -0
- package/src/pack/viewport.d.ts +30 -0
- package/src/pack/viewport.d.ts.map +1 -0
- package/src/pack/viewport.js +19 -0
- package/src/pack/viewport.js.map +1 -0
- package/src/pack/viewport.ts +40 -0
- package/src/tts/edge-tts.d.ts +8 -0
- package/src/tts/edge-tts.d.ts.map +1 -1
- package/src/tts/edge-tts.js +11 -3
- package/src/tts/edge-tts.js.map +1 -1
- package/src/tts/edge-tts.ts +11 -3
- package/src/util/load-yaml.d.ts +1 -1
- package/src/util/load-yaml.js +1 -1
- package/src/util/load-yaml.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { resolve, join, basename } from "node:path";
|
|
2
|
+
import { stat, mkdir, writeFile, rm, readdir, copyFile, access } from "node:fs/promises";
|
|
3
|
+
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
4
|
+
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
5
|
+
import { generateEmbedEntry, generateEmbedHtml } from "../pack/generate-embed-entry.js";
|
|
6
|
+
import { runViteBuild } from "../pack/build.js";
|
|
7
|
+
import { buildPackManifest, writePackManifest, writeScenarioJson, verifyManifestFilesExist, } from "../pack/pack-manifest.js";
|
|
8
|
+
import { DEFAULT_VIEWPORT } from "../pack/viewport.js";
|
|
9
|
+
/** Generator version stamped into scenario.json (kept in sync with the CLI). */
|
|
10
|
+
const PACK_GENERATOR_VERSION = "0.0.1";
|
|
11
|
+
/** Default canonical viewport width (matches DemoViewport's default). */
|
|
12
|
+
const DEFAULT_WIDTH = DEFAULT_VIEWPORT.width;
|
|
13
|
+
/** Default shell height for the embed. */
|
|
14
|
+
const DEFAULT_SHELL_HEIGHT = DEFAULT_VIEWPORT.height;
|
|
15
|
+
export function registerPackCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command("pack")
|
|
18
|
+
.description("Bundle a scenario into a self-contained, hosted embed.\n\n" +
|
|
19
|
+
"Accepts the same scenario directory as `render`/`preview`\n" +
|
|
20
|
+
"(steps.ts + an index.tsx that exports renderStep, plus an\n" +
|
|
21
|
+
"optional .scenar/providers.tsx and narration/). Produces a static\n" +
|
|
22
|
+
"bundle that boots ScenarioPlayer in the browser, ready for\n" +
|
|
23
|
+
"`scenar deploy`.\n\n" +
|
|
24
|
+
"The bundle contains index.html, hashed JS/CSS, a scenario.json\n" +
|
|
25
|
+
"descriptor, and a pack-manifest.json listing every file with its\n" +
|
|
26
|
+
"lowercase-hex sha256 and content type. Output stays within the\n" +
|
|
27
|
+
"deploy allowlist: HTML/JS/CSS/JSON, MP3 narration, raster images\n" +
|
|
28
|
+
"(png/jpg/jpeg/gif/webp/avif), and woff2/woff fonts. SVG is not a\n" +
|
|
29
|
+
"served type — inline it as a component or data URI.\n\n" +
|
|
30
|
+
"Output defaults to ./<scenario-id>-bundle. Use --out to override.")
|
|
31
|
+
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
32
|
+
.option("--out <path>", "output directory for the bundle")
|
|
33
|
+
.option("--width <number>", `canonical viewport width in px (default: ${DEFAULT_WIDTH})`, String(DEFAULT_WIDTH))
|
|
34
|
+
.option("--shell-height <number>", `shell height in px (default: ${DEFAULT_SHELL_HEIGHT})`, String(DEFAULT_SHELL_HEIGHT))
|
|
35
|
+
.option("--keep-temp", "keep the generated entry directory for debugging")
|
|
36
|
+
.action(async (dir, options) => {
|
|
37
|
+
const scenarioDir = resolve(dir);
|
|
38
|
+
let info;
|
|
39
|
+
try {
|
|
40
|
+
info = await stat(scenarioDir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} does not exist.\n`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!info.isDirectory()) {
|
|
48
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} is not a directory.\n` +
|
|
49
|
+
"The pack command requires a scenario directory (with steps.ts).\n");
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const scenarioId = basename(scenarioDir);
|
|
54
|
+
const outDir = options.out ? resolve(options.out) : resolve(`./${scenarioId}-bundle`);
|
|
55
|
+
const width = Number(options.width) || DEFAULT_WIDTH;
|
|
56
|
+
const shellHeight = Number(options.shellHeight) || DEFAULT_SHELL_HEIGHT;
|
|
57
|
+
// The entry must live inside the scenario directory so the bundler's
|
|
58
|
+
// node_modules resolution walks up into the consumer project.
|
|
59
|
+
const tempDir = join(scenarioDir, ".scenar-pack");
|
|
60
|
+
try {
|
|
61
|
+
const renderFilePath = await detectRenderExport(scenarioDir);
|
|
62
|
+
const providersPath = await resolveProvidersPath(scenarioDir);
|
|
63
|
+
const hasNarration = await fileExists(join(scenarioDir, "narration", "manifest.json"));
|
|
64
|
+
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
65
|
+
process.stderr.write(`Render: ${renderFilePath}\n`);
|
|
66
|
+
process.stderr.write(`Providers: ${providersPath ?? "none"}\n`);
|
|
67
|
+
process.stderr.write(`Narration: ${hasNarration ? "yes (manifest found)" : "none"}\n`);
|
|
68
|
+
process.stderr.write(`Output: ${outDir}\n\n`);
|
|
69
|
+
// 1. Generate the browser entry + index.html into the temp dir.
|
|
70
|
+
const entrySource = generateEmbedEntry({
|
|
71
|
+
scenarioDir,
|
|
72
|
+
renderFilePath,
|
|
73
|
+
scenarioId,
|
|
74
|
+
hasNarration,
|
|
75
|
+
providersPath,
|
|
76
|
+
canonicalWidth: width,
|
|
77
|
+
shellHeight,
|
|
78
|
+
});
|
|
79
|
+
await mkdir(tempDir, { recursive: true });
|
|
80
|
+
const entryFileName = "entry.tsx";
|
|
81
|
+
const entryHtmlPath = join(tempDir, "index.html");
|
|
82
|
+
await writeFile(join(tempDir, entryFileName), entrySource, "utf-8");
|
|
83
|
+
await writeFile(entryHtmlPath, generateEmbedHtml(scenarioId, entryFileName), "utf-8");
|
|
84
|
+
// 2. Build the static bundle with Vite.
|
|
85
|
+
process.stderr.write("Bundling embed with Vite...\n");
|
|
86
|
+
await runViteBuild({ root: tempDir, outDir, entryHtmlPath });
|
|
87
|
+
// 3. Copy the narration manifest + audio. The embed fetches
|
|
88
|
+
// ./narration/manifest.json at runtime and resolves each clip's
|
|
89
|
+
// relative src against it, so both must ship in the bundle.
|
|
90
|
+
if (hasNarration) {
|
|
91
|
+
await copyNarration(scenarioDir, outDir);
|
|
92
|
+
}
|
|
93
|
+
// 4. Write the required scenario.json descriptor at the bundle root,
|
|
94
|
+
// recording the canonical viewport baked into the bundle so `deploy`
|
|
95
|
+
// can derive a correctly-proportioned embed snippet (DD-004).
|
|
96
|
+
await writeScenarioJson(outDir, scenarioId, PACK_GENERATOR_VERSION, {
|
|
97
|
+
width,
|
|
98
|
+
height: shellHeight,
|
|
99
|
+
});
|
|
100
|
+
// 5. Compute + write the deploy-facing pack manifest (validates the
|
|
101
|
+
// bundle against the backend allowlist; throws on any violation).
|
|
102
|
+
const manifest = await buildPackManifest(outDir, scenarioId);
|
|
103
|
+
await verifyManifestFilesExist(outDir, manifest);
|
|
104
|
+
await writePackManifest(outDir, manifest);
|
|
105
|
+
const totalBytes = manifest.files.reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
106
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Packed ${manifest.files.length} files (${formatBytes(totalBytes)}) to ${outDir}\n`);
|
|
107
|
+
process.stderr.write(` Next: scenar deploy ${outDir}\n`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
111
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (!options.keepTemp) {
|
|
116
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Copy the narration manifest and every *.mp3 from <scenarioDir>/narration
|
|
123
|
+
* into <outDir>/narration. The manifest is the runtime index the embed fetches;
|
|
124
|
+
* the mp3s are the clips it references by relative src.
|
|
125
|
+
*/
|
|
126
|
+
async function copyNarration(scenarioDir, outDir) {
|
|
127
|
+
const srcDir = join(scenarioDir, "narration");
|
|
128
|
+
const destDir = join(outDir, "narration");
|
|
129
|
+
await mkdir(destDir, { recursive: true });
|
|
130
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (!entry.isFile())
|
|
133
|
+
continue;
|
|
134
|
+
const name = entry.name.toLowerCase();
|
|
135
|
+
if (name === "manifest.json" || name.endsWith(".mp3")) {
|
|
136
|
+
await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function fileExists(path) {
|
|
141
|
+
try {
|
|
142
|
+
await access(path);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function formatBytes(bytes) {
|
|
150
|
+
if (bytes < 1024)
|
|
151
|
+
return `${bytes} B`;
|
|
152
|
+
if (bytes < 1024 * 1024)
|
|
153
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
154
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=pack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { resolve, join, basename } from "node:path";
|
|
2
|
+
import { stat, mkdir, writeFile, rm, readdir, copyFile, access } from "node:fs/promises";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
5
|
+
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
6
|
+
import { generateEmbedEntry, generateEmbedHtml } from "../pack/generate-embed-entry.js";
|
|
7
|
+
import { runViteBuild } from "../pack/build.js";
|
|
8
|
+
import {
|
|
9
|
+
buildPackManifest,
|
|
10
|
+
writePackManifest,
|
|
11
|
+
writeScenarioJson,
|
|
12
|
+
verifyManifestFilesExist,
|
|
13
|
+
} from "../pack/pack-manifest.js";
|
|
14
|
+
import { DEFAULT_VIEWPORT } from "../pack/viewport.js";
|
|
15
|
+
|
|
16
|
+
/** Generator version stamped into scenario.json (kept in sync with the CLI). */
|
|
17
|
+
const PACK_GENERATOR_VERSION = "0.0.1";
|
|
18
|
+
|
|
19
|
+
/** Default canonical viewport width (matches DemoViewport's default). */
|
|
20
|
+
const DEFAULT_WIDTH = DEFAULT_VIEWPORT.width;
|
|
21
|
+
|
|
22
|
+
/** Default shell height for the embed. */
|
|
23
|
+
const DEFAULT_SHELL_HEIGHT = DEFAULT_VIEWPORT.height;
|
|
24
|
+
|
|
25
|
+
interface PackOptions {
|
|
26
|
+
out?: string;
|
|
27
|
+
width?: string;
|
|
28
|
+
shellHeight?: string;
|
|
29
|
+
keepTemp?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerPackCommand(program: Command): void {
|
|
33
|
+
program
|
|
34
|
+
.command("pack")
|
|
35
|
+
.description(
|
|
36
|
+
"Bundle a scenario into a self-contained, hosted embed.\n\n" +
|
|
37
|
+
"Accepts the same scenario directory as `render`/`preview`\n" +
|
|
38
|
+
"(steps.ts + an index.tsx that exports renderStep, plus an\n" +
|
|
39
|
+
"optional .scenar/providers.tsx and narration/). Produces a static\n" +
|
|
40
|
+
"bundle that boots ScenarioPlayer in the browser, ready for\n" +
|
|
41
|
+
"`scenar deploy`.\n\n" +
|
|
42
|
+
"The bundle contains index.html, hashed JS/CSS, a scenario.json\n" +
|
|
43
|
+
"descriptor, and a pack-manifest.json listing every file with its\n" +
|
|
44
|
+
"lowercase-hex sha256 and content type. Output stays within the\n" +
|
|
45
|
+
"deploy allowlist: HTML/JS/CSS/JSON, MP3 narration, raster images\n" +
|
|
46
|
+
"(png/jpg/jpeg/gif/webp/avif), and woff2/woff fonts. SVG is not a\n" +
|
|
47
|
+
"served type — inline it as a component or data URI.\n\n" +
|
|
48
|
+
"Output defaults to ./<scenario-id>-bundle. Use --out to override.",
|
|
49
|
+
)
|
|
50
|
+
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
51
|
+
.option("--out <path>", "output directory for the bundle")
|
|
52
|
+
.option("--width <number>", `canonical viewport width in px (default: ${DEFAULT_WIDTH})`, String(DEFAULT_WIDTH))
|
|
53
|
+
.option(
|
|
54
|
+
"--shell-height <number>",
|
|
55
|
+
`shell height in px (default: ${DEFAULT_SHELL_HEIGHT})`,
|
|
56
|
+
String(DEFAULT_SHELL_HEIGHT),
|
|
57
|
+
)
|
|
58
|
+
.option("--keep-temp", "keep the generated entry directory for debugging")
|
|
59
|
+
.action(async (dir: string, options: PackOptions) => {
|
|
60
|
+
const scenarioDir = resolve(dir);
|
|
61
|
+
|
|
62
|
+
let info;
|
|
63
|
+
try {
|
|
64
|
+
info = await stat(scenarioDir);
|
|
65
|
+
} catch {
|
|
66
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} does not exist.\n`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!info.isDirectory()) {
|
|
71
|
+
process.stderr.write(
|
|
72
|
+
`\x1b[31mError:\x1b[0m ${dir} is not a directory.\n` +
|
|
73
|
+
"The pack command requires a scenario directory (with steps.ts).\n",
|
|
74
|
+
);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const scenarioId = basename(scenarioDir);
|
|
80
|
+
const outDir = options.out ? resolve(options.out) : resolve(`./${scenarioId}-bundle`);
|
|
81
|
+
const width = Number(options.width) || DEFAULT_WIDTH;
|
|
82
|
+
const shellHeight = Number(options.shellHeight) || DEFAULT_SHELL_HEIGHT;
|
|
83
|
+
|
|
84
|
+
// The entry must live inside the scenario directory so the bundler's
|
|
85
|
+
// node_modules resolution walks up into the consumer project.
|
|
86
|
+
const tempDir = join(scenarioDir, ".scenar-pack");
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const renderFilePath = await detectRenderExport(scenarioDir);
|
|
90
|
+
const providersPath = await resolveProvidersPath(scenarioDir);
|
|
91
|
+
const hasNarration = await fileExists(join(scenarioDir, "narration", "manifest.json"));
|
|
92
|
+
|
|
93
|
+
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
94
|
+
process.stderr.write(`Render: ${renderFilePath}\n`);
|
|
95
|
+
process.stderr.write(`Providers: ${providersPath ?? "none"}\n`);
|
|
96
|
+
process.stderr.write(`Narration: ${hasNarration ? "yes (manifest found)" : "none"}\n`);
|
|
97
|
+
process.stderr.write(`Output: ${outDir}\n\n`);
|
|
98
|
+
|
|
99
|
+
// 1. Generate the browser entry + index.html into the temp dir.
|
|
100
|
+
const entrySource = generateEmbedEntry({
|
|
101
|
+
scenarioDir,
|
|
102
|
+
renderFilePath,
|
|
103
|
+
scenarioId,
|
|
104
|
+
hasNarration,
|
|
105
|
+
providersPath,
|
|
106
|
+
canonicalWidth: width,
|
|
107
|
+
shellHeight,
|
|
108
|
+
});
|
|
109
|
+
await mkdir(tempDir, { recursive: true });
|
|
110
|
+
const entryFileName = "entry.tsx";
|
|
111
|
+
const entryHtmlPath = join(tempDir, "index.html");
|
|
112
|
+
await writeFile(join(tempDir, entryFileName), entrySource, "utf-8");
|
|
113
|
+
await writeFile(entryHtmlPath, generateEmbedHtml(scenarioId, entryFileName), "utf-8");
|
|
114
|
+
|
|
115
|
+
// 2. Build the static bundle with Vite.
|
|
116
|
+
process.stderr.write("Bundling embed with Vite...\n");
|
|
117
|
+
await runViteBuild({ root: tempDir, outDir, entryHtmlPath });
|
|
118
|
+
|
|
119
|
+
// 3. Copy the narration manifest + audio. The embed fetches
|
|
120
|
+
// ./narration/manifest.json at runtime and resolves each clip's
|
|
121
|
+
// relative src against it, so both must ship in the bundle.
|
|
122
|
+
if (hasNarration) {
|
|
123
|
+
await copyNarration(scenarioDir, outDir);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Write the required scenario.json descriptor at the bundle root,
|
|
127
|
+
// recording the canonical viewport baked into the bundle so `deploy`
|
|
128
|
+
// can derive a correctly-proportioned embed snippet (DD-004).
|
|
129
|
+
await writeScenarioJson(outDir, scenarioId, PACK_GENERATOR_VERSION, {
|
|
130
|
+
width,
|
|
131
|
+
height: shellHeight,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 5. Compute + write the deploy-facing pack manifest (validates the
|
|
135
|
+
// bundle against the backend allowlist; throws on any violation).
|
|
136
|
+
const manifest = await buildPackManifest(outDir, scenarioId);
|
|
137
|
+
await verifyManifestFilesExist(outDir, manifest);
|
|
138
|
+
await writePackManifest(outDir, manifest);
|
|
139
|
+
|
|
140
|
+
const totalBytes = manifest.files.reduce((sum, f) => sum + f.sizeBytes, 0);
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`\n\x1b[32m✓\x1b[0m Packed ${manifest.files.length} files (${formatBytes(totalBytes)}) to ${outDir}\n`,
|
|
143
|
+
);
|
|
144
|
+
process.stderr.write(` Next: scenar deploy ${outDir}\n`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
147
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
} finally {
|
|
150
|
+
if (!options.keepTemp) {
|
|
151
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Copy the narration manifest and every *.mp3 from <scenarioDir>/narration
|
|
159
|
+
* into <outDir>/narration. The manifest is the runtime index the embed fetches;
|
|
160
|
+
* the mp3s are the clips it references by relative src.
|
|
161
|
+
*/
|
|
162
|
+
async function copyNarration(scenarioDir: string, outDir: string): Promise<void> {
|
|
163
|
+
const srcDir = join(scenarioDir, "narration");
|
|
164
|
+
const destDir = join(outDir, "narration");
|
|
165
|
+
await mkdir(destDir, { recursive: true });
|
|
166
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (!entry.isFile()) continue;
|
|
169
|
+
const name = entry.name.toLowerCase();
|
|
170
|
+
if (name === "manifest.json" || name.endsWith(".mp3")) {
|
|
171
|
+
await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
177
|
+
try {
|
|
178
|
+
await access(path);
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatBytes(bytes: number): string {
|
|
186
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
187
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
188
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Client } from "@connectrpc/connect";
|
|
2
|
+
import { DeployCommandController } from "@scenar/stubs/ai/scenar/deploy/v1/command_pb.js";
|
|
3
|
+
import { ScenarioCommandController } from "@scenar/stubs/ai/scenar/scenario/v1/command_pb.js";
|
|
4
|
+
/** The backend controllers `scenar deploy` drives. */
|
|
5
|
+
export interface BackendClients {
|
|
6
|
+
readonly deploy: Client<typeof DeployCommandController>;
|
|
7
|
+
readonly scenario: Client<typeof ScenarioCommandController>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build typed Connect clients over a single gRPC transport, mirroring the
|
|
11
|
+
* canonical CLI pattern (createClient(service, transport)). The transport uses
|
|
12
|
+
* Node's http2 module; an http:// baseUrl speaks gRPC over cleartext HTTP/2
|
|
13
|
+
* (h2c), which is what the local scenar-service expects.
|
|
14
|
+
*
|
|
15
|
+
* Auth: none is sent for local `test` security mode (the backend injects a
|
|
16
|
+
* synthetic authorized caller). A production scoped-API-key path would attach a
|
|
17
|
+
* bearer interceptor here — the clean seam is the transport's interceptors.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createBackendClients(baseUrl: string): BackendClients;
|
|
20
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/deploy/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAEhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAE9F,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,uBAAuB,CAAC,CAAC;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC;CAC7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAMpE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createClient } from "@connectrpc/connect";
|
|
2
|
+
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
3
|
+
import { DeployCommandController } from "@scenar/stubs/ai/scenar/deploy/v1/command_pb.js";
|
|
4
|
+
import { ScenarioCommandController } from "@scenar/stubs/ai/scenar/scenario/v1/command_pb.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build typed Connect clients over a single gRPC transport, mirroring the
|
|
7
|
+
* canonical CLI pattern (createClient(service, transport)). The transport uses
|
|
8
|
+
* Node's http2 module; an http:// baseUrl speaks gRPC over cleartext HTTP/2
|
|
9
|
+
* (h2c), which is what the local scenar-service expects.
|
|
10
|
+
*
|
|
11
|
+
* Auth: none is sent for local `test` security mode (the backend injects a
|
|
12
|
+
* synthetic authorized caller). A production scoped-API-key path would attach a
|
|
13
|
+
* bearer interceptor here — the clean seam is the transport's interceptors.
|
|
14
|
+
*/
|
|
15
|
+
export function createBackendClients(baseUrl) {
|
|
16
|
+
const transport = createGrpcTransport({ baseUrl });
|
|
17
|
+
return {
|
|
18
|
+
deploy: createClient(DeployCommandController, transport),
|
|
19
|
+
scenario: createClient(ScenarioCommandController, transport),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/deploy/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAQ9F;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,MAAM,SAAS,GAAG,mBAAmB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IACnD,OAAO;QACL,MAAM,EAAE,YAAY,CAAC,uBAAuB,EAAE,SAAS,CAAC;QACxD,QAAQ,EAAE,YAAY,CAAC,yBAAyB,EAAE,SAAS,CAAC;KAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createClient, type Client } from "@connectrpc/connect";
|
|
2
|
+
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
3
|
+
import { DeployCommandController } from "@scenar/stubs/ai/scenar/deploy/v1/command_pb.js";
|
|
4
|
+
import { ScenarioCommandController } from "@scenar/stubs/ai/scenar/scenario/v1/command_pb.js";
|
|
5
|
+
|
|
6
|
+
/** The backend controllers `scenar deploy` drives. */
|
|
7
|
+
export interface BackendClients {
|
|
8
|
+
readonly deploy: Client<typeof DeployCommandController>;
|
|
9
|
+
readonly scenario: Client<typeof ScenarioCommandController>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build typed Connect clients over a single gRPC transport, mirroring the
|
|
14
|
+
* canonical CLI pattern (createClient(service, transport)). The transport uses
|
|
15
|
+
* Node's http2 module; an http:// baseUrl speaks gRPC over cleartext HTTP/2
|
|
16
|
+
* (h2c), which is what the local scenar-service expects.
|
|
17
|
+
*
|
|
18
|
+
* Auth: none is sent for local `test` security mode (the backend injects a
|
|
19
|
+
* synthetic authorized caller). A production scoped-API-key path would attach a
|
|
20
|
+
* bearer interceptor here — the clean seam is the transport's interceptors.
|
|
21
|
+
*/
|
|
22
|
+
export function createBackendClients(baseUrl: string): BackendClients {
|
|
23
|
+
const transport = createGrpcTransport({ baseUrl });
|
|
24
|
+
return {
|
|
25
|
+
deploy: createClient(DeployCommandController, transport),
|
|
26
|
+
scenario: createClient(ScenarioCommandController, transport),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { PackManifest } from "../pack/pack-manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* One presigned upload target as the backend returns it. Mirrors the relevant
|
|
4
|
+
* fields of ai.scenar.deploy.v1.FileUploadTarget.
|
|
5
|
+
*/
|
|
6
|
+
export interface UploadTarget {
|
|
7
|
+
readonly relativePath: string;
|
|
8
|
+
readonly presignedPutUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* Headers that MUST be replayed verbatim on the PUT for the signature to
|
|
11
|
+
* validate (Content-Type + x-amz-checksum-sha256). The backend deliberately
|
|
12
|
+
* omits host/content-length, which the HTTP layer owns.
|
|
13
|
+
*/
|
|
14
|
+
readonly requiredHeaders: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** A file declared to the upload session — the init shape for DeclaredFile. */
|
|
17
|
+
export interface DeclaredFileInit {
|
|
18
|
+
readonly relativePath: string;
|
|
19
|
+
/** Lowercase-hex SHA-256 (the proto + presign both bind this exact form). */
|
|
20
|
+
readonly sha256: string;
|
|
21
|
+
/** int64 on the wire — carried as bigint to avoid precision loss. */
|
|
22
|
+
readonly sizeBytes: bigint;
|
|
23
|
+
readonly contentType: string;
|
|
24
|
+
}
|
|
25
|
+
/** Identity for the ensure-scenario (apply) step. */
|
|
26
|
+
export interface ScenarioApplyInput {
|
|
27
|
+
readonly org: string;
|
|
28
|
+
readonly slug: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The side-effecting operations the flow depends on, injected so the ordering
|
|
33
|
+
* and payloads can be unit-tested against fakes without a live backend.
|
|
34
|
+
*/
|
|
35
|
+
export interface DeployFlowDeps {
|
|
36
|
+
/** Idempotently ensure the parent scenario exists; returns its metadata.id. */
|
|
37
|
+
applyScenario(input: ScenarioApplyInput): Promise<string>;
|
|
38
|
+
/** Open the upload session; returns the deploy id + per-file targets. */
|
|
39
|
+
createSession(scenarioId: string, files: DeclaredFileInit[]): Promise<{
|
|
40
|
+
deployId: string;
|
|
41
|
+
uploadTargets: UploadTarget[];
|
|
42
|
+
}>;
|
|
43
|
+
/** Read a bundle file's bytes by its relative path. */
|
|
44
|
+
readBundleFile(relativePath: string): Promise<Uint8Array>;
|
|
45
|
+
/** PUT one file's bytes to its presigned target, replaying required headers. */
|
|
46
|
+
uploadFile(target: UploadTarget, bytes: Uint8Array): Promise<void>;
|
|
47
|
+
/** Finalize the session; returns the published deploy's embed_url. */
|
|
48
|
+
completeSession(deployId: string): Promise<string>;
|
|
49
|
+
/** Progress sink (stderr in the command; captured in tests). */
|
|
50
|
+
log(message: string): void;
|
|
51
|
+
}
|
|
52
|
+
/** Map a pack manifest to the deploy session's declared-file inventory. */
|
|
53
|
+
export declare function toDeclaredFiles(manifest: PackManifest): DeclaredFileInit[];
|
|
54
|
+
/**
|
|
55
|
+
* Drive the deploy: ensure the parent scenario, open the upload session, PUT
|
|
56
|
+
* every file to its presigned target (in the order the backend returned them,
|
|
57
|
+
* replaying required headers verbatim), then complete and return the embed URL.
|
|
58
|
+
*
|
|
59
|
+
* The ensure-then-deploy ordering is load-bearing: createDeployUploadSession
|
|
60
|
+
* authorizes against and inherits its org from an existing scenario, so the
|
|
61
|
+
* scenario must exist first.
|
|
62
|
+
*/
|
|
63
|
+
export declare function runDeployFlow(deps: DeployFlowDeps, opts: {
|
|
64
|
+
manifest: PackManifest;
|
|
65
|
+
org: string;
|
|
66
|
+
slug: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
deployId: string;
|
|
70
|
+
embedUrl: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Derive a locally-viewable URL from a deploy's embed_url. The backend always
|
|
74
|
+
* emits https://, but the local edge (wrangler dev) serves over http on
|
|
75
|
+
* *.localhost; for those hosts we downgrade the scheme so the link is clickable.
|
|
76
|
+
*/
|
|
77
|
+
export declare function localViewUrl(embedUrl: string): string;
|
|
78
|
+
//# sourceMappingURL=deploy-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy-flow.d.ts","sourceRoot":"","sources":["../../../src/deploy/deploy-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,qDAAqD;AACrD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,yEAAyE;IACzE,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,gBAAgB,EAAE,GACxB,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC;IAChE,uDAAuD;IACvD,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,gFAAgF;IAChF,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,sEAAsE;IACtE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,gEAAgE;IAChE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAO1E;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxE,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYrD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Map a pack manifest to the deploy session's declared-file inventory. */
|
|
2
|
+
export function toDeclaredFiles(manifest) {
|
|
3
|
+
return manifest.files.map((file) => ({
|
|
4
|
+
relativePath: file.path,
|
|
5
|
+
sha256: file.sha256,
|
|
6
|
+
sizeBytes: BigInt(file.sizeBytes),
|
|
7
|
+
contentType: file.contentType,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Drive the deploy: ensure the parent scenario, open the upload session, PUT
|
|
12
|
+
* every file to its presigned target (in the order the backend returned them,
|
|
13
|
+
* replaying required headers verbatim), then complete and return the embed URL.
|
|
14
|
+
*
|
|
15
|
+
* The ensure-then-deploy ordering is load-bearing: createDeployUploadSession
|
|
16
|
+
* authorizes against and inherits its org from an existing scenario, so the
|
|
17
|
+
* scenario must exist first.
|
|
18
|
+
*/
|
|
19
|
+
export async function runDeployFlow(deps, opts) {
|
|
20
|
+
deps.log(`Ensuring scenario ${opts.org}/${opts.slug}...`);
|
|
21
|
+
const scenarioId = await deps.applyScenario({ org: opts.org, slug: opts.slug, name: opts.name });
|
|
22
|
+
const files = toDeclaredFiles(opts.manifest);
|
|
23
|
+
deps.log(`Creating deploy session (${files.length} files)...`);
|
|
24
|
+
const { deployId, uploadTargets } = await deps.createSession(scenarioId, files);
|
|
25
|
+
for (const target of uploadTargets) {
|
|
26
|
+
const bytes = await deps.readBundleFile(target.relativePath);
|
|
27
|
+
deps.log(`Uploading ${target.relativePath} (${bytes.byteLength} bytes)...`);
|
|
28
|
+
await deps.uploadFile(target, bytes);
|
|
29
|
+
}
|
|
30
|
+
deps.log("Completing deploy...");
|
|
31
|
+
const embedUrl = await deps.completeSession(deployId);
|
|
32
|
+
return { deployId, embedUrl };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Derive a locally-viewable URL from a deploy's embed_url. The backend always
|
|
36
|
+
* emits https://, but the local edge (wrangler dev) serves over http on
|
|
37
|
+
* *.localhost; for those hosts we downgrade the scheme so the link is clickable.
|
|
38
|
+
*/
|
|
39
|
+
export function localViewUrl(embedUrl) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(embedUrl);
|
|
42
|
+
const isLocal = url.hostname === "localhost" || url.hostname.endsWith(".localhost");
|
|
43
|
+
if (isLocal && url.protocol === "https:") {
|
|
44
|
+
url.protocol = "http:";
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Not a parseable URL — return as-is.
|
|
50
|
+
}
|
|
51
|
+
return embedUrl;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=deploy-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy-flow.js","sourceRoot":"","sources":["../../../src/deploy/deploy-flow.ts"],"names":[],"mappings":"AAwDA,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAAC,QAAsB;IACpD,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACnC,YAAY,EAAE,IAAI,CAAC,IAAI;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QACjC,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAoB,EACpB,IAAyE;IAEzE,IAAI,CAAC,GAAG,CAAC,qBAAqB,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAEjG,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG,CAAC,4BAA4B,KAAK,CAAC,MAAM,YAAY,CAAC,CAAC;IAC/D,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAEhF,KAAK,MAAM,MAAM,IAAI,aAAa,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC7D,IAAI,CAAC,GAAG,CAAC,aAAa,MAAM,CAAC,YAAY,KAAK,KAAK,CAAC,UAAU,YAAY,CAAC,CAAC;QAC5E,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;QACpF,IAAI,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACzC,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC;YACvB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|