@scenar/cli 0.1.19 → 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__/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/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,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
buildPackManifest,
|
|
8
|
+
writeScenarioJson,
|
|
9
|
+
writePackManifest,
|
|
10
|
+
PACK_MANIFEST_FILE,
|
|
11
|
+
} from "../pack/pack-manifest.js";
|
|
12
|
+
import {
|
|
13
|
+
RELATIVE_PATH_PATTERN,
|
|
14
|
+
SHA256_HEX_PATTERN,
|
|
15
|
+
CONTENT_TYPE_PATTERN,
|
|
16
|
+
ALLOWED_EXTENSIONS,
|
|
17
|
+
MAX_PATH_DEPTH,
|
|
18
|
+
finalExtension,
|
|
19
|
+
} from "../pack/bundle-contract.js";
|
|
20
|
+
import { DEFAULT_VIEWPORT } from "../pack/viewport.js";
|
|
21
|
+
|
|
22
|
+
let dir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
dir = await mkdtemp(join(tmpdir(), "scenar-pack-"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await rm(dir, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Write a minimal, allowlist-clean bundle into `dir`. */
|
|
33
|
+
async function seedCleanBundle(): Promise<void> {
|
|
34
|
+
await writeFile(join(dir, "index.html"), "<!doctype html><div id=root></div>", "utf-8");
|
|
35
|
+
await mkdir(join(dir, "assets"), { recursive: true });
|
|
36
|
+
await writeFile(join(dir, "assets", "index-abc123.js"), "console.log(1)", "utf-8");
|
|
37
|
+
await writeFile(join(dir, "assets", "index-abc123.css"), ".x{}", "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("writeScenarioJson", () => {
|
|
41
|
+
it("writes a valid scenario.json at the bundle root with the recorded viewport", async () => {
|
|
42
|
+
await writeScenarioJson(dir, "welcome-tour", "0.0.1", { width: 1024, height: 576 });
|
|
43
|
+
const raw = await readFile(join(dir, "scenario.json"), "utf-8");
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
expect(parsed.schemaVersion).toBe("1");
|
|
46
|
+
expect(parsed.id).toBe("welcome-tour");
|
|
47
|
+
expect(parsed.generator).toContain("@scenar/cli pack");
|
|
48
|
+
// The baked canonical viewport flows downstream to the embed snippet (DD-004).
|
|
49
|
+
expect(parsed.viewport).toEqual({ width: 1024, height: 576 });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("buildPackManifest", () => {
|
|
54
|
+
it("computes one validated entry per file with lowercase-hex sha256", async () => {
|
|
55
|
+
await seedCleanBundle();
|
|
56
|
+
await writeScenarioJson(dir, "welcome-tour", "0.0.1", DEFAULT_VIEWPORT);
|
|
57
|
+
|
|
58
|
+
const manifest = await buildPackManifest(dir, "welcome-tour");
|
|
59
|
+
|
|
60
|
+
expect(manifest.scenarioId).toBe("welcome-tour");
|
|
61
|
+
const paths = manifest.files.map((f) => f.path);
|
|
62
|
+
expect(paths).toContain("index.html");
|
|
63
|
+
expect(paths).toContain("assets/index-abc123.js");
|
|
64
|
+
expect(paths).toContain("assets/index-abc123.css");
|
|
65
|
+
expect(paths).toContain("scenario.json");
|
|
66
|
+
|
|
67
|
+
// sha256 is the real lowercase-hex digest of the bytes.
|
|
68
|
+
const js = manifest.files.find((f) => f.path === "assets/index-abc123.js")!;
|
|
69
|
+
const expected = createHash("sha256").update("console.log(1)").digest("hex");
|
|
70
|
+
expect(js.sha256).toBe(expected);
|
|
71
|
+
expect(js.contentType).toBe("text/javascript");
|
|
72
|
+
expect(js.sizeBytes).toBe(Buffer.byteLength("console.log(1)"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("never lists pack-manifest.json itself", async () => {
|
|
76
|
+
await seedCleanBundle();
|
|
77
|
+
await writeScenarioJson(dir, "welcome-tour", "0.0.1", DEFAULT_VIEWPORT);
|
|
78
|
+
const manifest = await buildPackManifest(dir, "welcome-tour");
|
|
79
|
+
await writePackManifest(dir, manifest);
|
|
80
|
+
// Re-derive after the manifest file exists on disk.
|
|
81
|
+
const manifest2 = await buildPackManifest(dir, "welcome-tour");
|
|
82
|
+
expect(manifest2.files.map((f) => f.path)).not.toContain(PACK_MANIFEST_FILE);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("includes raster images and fonts with their canonical content types", async () => {
|
|
86
|
+
await seedCleanBundle();
|
|
87
|
+
await writeScenarioJson(dir, "welcome-tour", "0.0.1", DEFAULT_VIEWPORT);
|
|
88
|
+
await writeFile(join(dir, "assets", "logo-abc.png"), "PNG-bytes", "utf-8");
|
|
89
|
+
await writeFile(join(dir, "assets", "brand-abc.woff2"), "WOFF2-bytes", "utf-8");
|
|
90
|
+
|
|
91
|
+
const manifest = await buildPackManifest(dir, "welcome-tour");
|
|
92
|
+
const byPath = new Map(manifest.files.map((f) => [f.path, f]));
|
|
93
|
+
|
|
94
|
+
expect(byPath.get("assets/logo-abc.png")?.contentType).toBe("image/png");
|
|
95
|
+
expect(byPath.get("assets/brand-abc.woff2")?.contentType).toBe("font/woff2");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects a bundle with a disallowed extension (svg active content)", async () => {
|
|
99
|
+
await seedCleanBundle();
|
|
100
|
+
await writeFile(join(dir, "assets", "icon-abc.svg"), "<svg/>", "utf-8");
|
|
101
|
+
await expect(buildPackManifest(dir, "welcome-tour")).rejects.toThrow(/allowlist/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The anti-drift anchor: every file pack emits must satisfy the exact rules the
|
|
107
|
+
* backend enforces (DeployManifestValidator + CompleteDeployUploadSessionHandler
|
|
108
|
+
* + ScenarioJsonValidator). If the backend tightens a rule, this test must be
|
|
109
|
+
* updated in lockstep with bundle-contract.ts.
|
|
110
|
+
*/
|
|
111
|
+
describe("bundle-contract conformance (pack output ↔ backend validators)", () => {
|
|
112
|
+
it("produces a manifest every field of which the backend would accept", async () => {
|
|
113
|
+
await seedCleanBundle();
|
|
114
|
+
await writeScenarioJson(dir, "welcome-tour", "0.0.1", DEFAULT_VIEWPORT);
|
|
115
|
+
const manifest = await buildPackManifest(dir, "welcome-tour");
|
|
116
|
+
|
|
117
|
+
// scenario.json is present at the root (REQUIRED_FILE).
|
|
118
|
+
expect(manifest.files.some((f) => f.path === "scenario.json")).toBe(true);
|
|
119
|
+
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
for (const file of manifest.files) {
|
|
122
|
+
// Clean relative path, bounded depth.
|
|
123
|
+
expect(file.path).toMatch(RELATIVE_PATH_PATTERN);
|
|
124
|
+
expect(file.path.split("/").length).toBeLessThanOrEqual(MAX_PATH_DEPTH);
|
|
125
|
+
// Allowlisted final extension.
|
|
126
|
+
expect(ALLOWED_EXTENSIONS as readonly string[]).toContain(finalExtension(file.path));
|
|
127
|
+
// Lowercase-hex sha256, exactly 64 chars.
|
|
128
|
+
expect(file.sha256).toMatch(SHA256_HEX_PATTERN);
|
|
129
|
+
// Positive size.
|
|
130
|
+
expect(file.sizeBytes).toBeGreaterThan(0);
|
|
131
|
+
// type/subtype content type.
|
|
132
|
+
expect(file.contentType).toMatch(CONTENT_TYPE_PATTERN);
|
|
133
|
+
// No duplicate paths.
|
|
134
|
+
expect(seen.has(file.path)).toBe(false);
|
|
135
|
+
seen.add(file.path);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseViewport, DEFAULT_VIEWPORT } from "../pack/viewport.js";
|
|
3
|
+
|
|
4
|
+
describe("parseViewport", () => {
|
|
5
|
+
it("accepts a well-formed viewport object", () => {
|
|
6
|
+
expect(parseViewport({ width: 896, height: 480 })).toEqual({ width: 896, height: 480 });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("ignores extra keys, keeping only width/height", () => {
|
|
10
|
+
expect(parseViewport({ width: 800, height: 600, depth: 1 })).toEqual({
|
|
11
|
+
width: 800,
|
|
12
|
+
height: 600,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
["null", null],
|
|
18
|
+
["a string", "896x480"],
|
|
19
|
+
["an array", [896, 480]],
|
|
20
|
+
["missing height", { width: 896 }],
|
|
21
|
+
["zero width", { width: 0, height: 480 }],
|
|
22
|
+
["negative height", { width: 896, height: -1 }],
|
|
23
|
+
["non-integer", { width: 896.5, height: 480 }],
|
|
24
|
+
["non-numeric", { width: "896", height: "480" }],
|
|
25
|
+
["NaN", { width: Number.NaN, height: 480 }],
|
|
26
|
+
])("returns null for %s", (_label, value) => {
|
|
27
|
+
expect(parseViewport(value)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("exposes sane defaults", () => {
|
|
31
|
+
expect(DEFAULT_VIEWPORT).toEqual({ width: 896, height: 480 });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8E5D"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { PACK_MANIFEST_FILE, SCENARIO_JSON_FILE } from "../pack/pack-manifest.js";
|
|
4
|
+
import { DEFAULT_VIEWPORT, parseViewport } from "../pack/viewport.js";
|
|
5
|
+
import { createBackendClients } from "../deploy/client.js";
|
|
6
|
+
import { putFile } from "../deploy/upload.js";
|
|
7
|
+
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
8
|
+
import { runDeployFlow, localViewUrl } from "../deploy/deploy-flow.js";
|
|
9
|
+
/** Default local backend (gRPC). Local dev shifts off 8080 to avoid colliding with stigmer-service. */
|
|
10
|
+
const DEFAULT_BACKEND = "http://localhost:8090";
|
|
11
|
+
/** Slug rule from ApiResourceMetadata: lowercase, hyphens, start letter, end alphanumeric. */
|
|
12
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
13
|
+
export function registerDeployCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("deploy")
|
|
16
|
+
.description("Deploy a packed bundle to Scenar Cloud and print its embed URL.\n\n" +
|
|
17
|
+
"Takes a bundle directory produced by `scenar pack` (containing\n" +
|
|
18
|
+
"pack-manifest.json). It ensures the parent scenario exists, opens a\n" +
|
|
19
|
+
"two-phase upload session, uploads every file directly to object\n" +
|
|
20
|
+
"storage via presigned URLs, completes the session, and prints the\n" +
|
|
21
|
+
"deploy's embed URL.\n\n" +
|
|
22
|
+
"Backend defaults to http://localhost:8090 (override with --backend or\n" +
|
|
23
|
+
"SCENAR_BACKEND). The owning org is required (--org or SCENAR_ORG).")
|
|
24
|
+
.argument("<bundleDir>", "path to a packed bundle directory (from scenar pack)")
|
|
25
|
+
.option("--backend <url>", "backend gRPC endpoint (default: $SCENAR_BACKEND or http://localhost:8090)")
|
|
26
|
+
.option("--org <org>", "owning organization (default: $SCENAR_ORG)")
|
|
27
|
+
.option("--slug <slug>", "scenario slug (default: the packed scenario id)")
|
|
28
|
+
.option("--name <name>", "scenario display name (default: the slug)")
|
|
29
|
+
.action(async (bundleDir, options) => {
|
|
30
|
+
const resolvedDir = resolve(bundleDir);
|
|
31
|
+
try {
|
|
32
|
+
const info = await stat(resolvedDir).catch(() => null);
|
|
33
|
+
if (!info || !info.isDirectory()) {
|
|
34
|
+
throw new Error(`${bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`);
|
|
35
|
+
}
|
|
36
|
+
const manifest = await readPackManifest(resolvedDir);
|
|
37
|
+
const backend = options.backend ?? process.env.SCENAR_BACKEND ?? DEFAULT_BACKEND;
|
|
38
|
+
const org = options.org ?? process.env.SCENAR_ORG;
|
|
39
|
+
if (!org) {
|
|
40
|
+
throw new Error("an owning org is required: pass --org <org> or set SCENAR_ORG.");
|
|
41
|
+
}
|
|
42
|
+
const slug = options.slug ?? manifest.scenarioId;
|
|
43
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
44
|
+
throw new Error(`invalid scenario slug "${slug}". Slugs are lowercase letters, digits,\n` +
|
|
45
|
+
"and hyphens, starting with a letter and ending alphanumeric. Pass --slug to override.");
|
|
46
|
+
}
|
|
47
|
+
const name = options.name ?? slug;
|
|
48
|
+
process.stderr.write(`Bundle: ${resolvedDir}\n`);
|
|
49
|
+
process.stderr.write(`Backend: ${backend}\n`);
|
|
50
|
+
process.stderr.write(`Org: ${org}\n`);
|
|
51
|
+
process.stderr.write(`Scenario: ${slug}\n\n`);
|
|
52
|
+
const clients = createBackendClients(backend);
|
|
53
|
+
const deps = makeDeps(clients, resolvedDir);
|
|
54
|
+
const { deployId, embedUrl } = await runDeployFlow(deps, { manifest, org, slug, name });
|
|
55
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Deployed ${deployId}\n`);
|
|
56
|
+
process.stdout.write(`${embedUrl}\n`);
|
|
57
|
+
const localUrl = localViewUrl(embedUrl);
|
|
58
|
+
if (localUrl !== embedUrl) {
|
|
59
|
+
process.stderr.write(` Local view: ${localUrl}\n`);
|
|
60
|
+
}
|
|
61
|
+
// Embed snippet (guidance → stderr; stdout stays the bare URL for
|
|
62
|
+
// piping). Responsive iframe at the bundle's canonical aspect ratio
|
|
63
|
+
// (DD-002/DD-004); falls back to the default viewport for bundles packed
|
|
64
|
+
// before the viewport was recorded.
|
|
65
|
+
const viewport = await readScenarioViewport(resolvedDir, name);
|
|
66
|
+
process.stderr.write("\n Embed snippet (paste into any page):\n\n");
|
|
67
|
+
process.stderr.write(`${indent(buildEmbedSnippet({ embedUrl, viewport, title: name }))}\n`);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
71
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Build the flow dependencies from real Connect clients + the bundle on disk. */
|
|
77
|
+
function makeDeps(clients, bundleDir) {
|
|
78
|
+
return {
|
|
79
|
+
async applyScenario({ org, slug, name }) {
|
|
80
|
+
// Idempotent upsert by (org, slug). The spec is a minimal valid placeholder
|
|
81
|
+
// (one step) — the embed plays from the packed JS, so the Scenario resource
|
|
82
|
+
// serves as the authorization parent and publication anchor, not the
|
|
83
|
+
// serving source. A richer spec projection is a deliberate follow-up.
|
|
84
|
+
const scenario = await clients.scenario.apply({
|
|
85
|
+
apiVersion: "scenario.scenar.ai/v1",
|
|
86
|
+
kind: "Scenario",
|
|
87
|
+
metadata: { org, slug, name },
|
|
88
|
+
spec: { steps: [{ view: "scenario", delayMs: 0 }] },
|
|
89
|
+
});
|
|
90
|
+
const id = scenario.metadata?.id;
|
|
91
|
+
if (!id) {
|
|
92
|
+
throw new Error("scenario apply returned no metadata.id");
|
|
93
|
+
}
|
|
94
|
+
return id;
|
|
95
|
+
},
|
|
96
|
+
async createSession(scenarioId, files) {
|
|
97
|
+
const response = await clients.deploy.createDeployUploadSession({ scenarioId, files });
|
|
98
|
+
return {
|
|
99
|
+
deployId: response.deployId,
|
|
100
|
+
uploadTargets: response.uploadTargets.map((target) => ({
|
|
101
|
+
relativePath: target.relativePath,
|
|
102
|
+
presignedPutUrl: target.presignedPutUrl,
|
|
103
|
+
requiredHeaders: target.requiredHeaders,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
readBundleFile(relativePath) {
|
|
108
|
+
return readFile(join(bundleDir, ...relativePath.split("/")));
|
|
109
|
+
},
|
|
110
|
+
uploadFile(target, bytes) {
|
|
111
|
+
return putFile(target.presignedPutUrl, target.requiredHeaders, bytes);
|
|
112
|
+
},
|
|
113
|
+
async completeSession(deployId) {
|
|
114
|
+
const deploy = await clients.deploy.completeDeployUploadSession({ deployId });
|
|
115
|
+
const embedUrl = deploy.status?.embedUrl;
|
|
116
|
+
if (!embedUrl) {
|
|
117
|
+
throw new Error("deploy completed but returned no embed_url");
|
|
118
|
+
}
|
|
119
|
+
return embedUrl;
|
|
120
|
+
},
|
|
121
|
+
log(message) {
|
|
122
|
+
process.stderr.write(`${message}\n`);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read the canonical viewport recorded in the bundle's scenario.json (DD-004).
|
|
128
|
+
* Falls back to {@link DEFAULT_VIEWPORT} for bundles packed before the viewport
|
|
129
|
+
* was recorded, or if the file is missing/malformed — a snippet with sensible
|
|
130
|
+
* proportions always beats no snippet. `scenarioLabel` is only for the warning.
|
|
131
|
+
*/
|
|
132
|
+
async function readScenarioViewport(bundleDir, scenarioLabel) {
|
|
133
|
+
try {
|
|
134
|
+
const raw = await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8");
|
|
135
|
+
const viewport = parseViewport(JSON.parse(raw).viewport);
|
|
136
|
+
if (viewport)
|
|
137
|
+
return viewport;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Missing/unreadable/invalid scenario.json — fall through to the default.
|
|
141
|
+
}
|
|
142
|
+
process.stderr.write(` Note: no recorded viewport for "${scenarioLabel}"; ` +
|
|
143
|
+
`snippet uses the default ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}. ` +
|
|
144
|
+
"Re-pack with the current CLI to embed at the exact aspect ratio.\n");
|
|
145
|
+
return DEFAULT_VIEWPORT;
|
|
146
|
+
}
|
|
147
|
+
/** Indent every line by two spaces (for nesting a block under a heading). */
|
|
148
|
+
function indent(block) {
|
|
149
|
+
return block.replace(/^/gm, " ");
|
|
150
|
+
}
|
|
151
|
+
async function readPackManifest(bundleDir) {
|
|
152
|
+
const manifestPath = join(bundleDir, PACK_MANIFEST_FILE);
|
|
153
|
+
let raw;
|
|
154
|
+
try {
|
|
155
|
+
raw = await readFile(manifestPath, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new Error(`no ${PACK_MANIFEST_FILE} in ${bundleDir}. Run \`scenar pack\` to produce a bundle first.`);
|
|
159
|
+
}
|
|
160
|
+
const parsed = JSON.parse(raw);
|
|
161
|
+
if (!parsed.scenarioId || !Array.isArray(parsed.files) || parsed.files.length === 0) {
|
|
162
|
+
throw new Error(`${PACK_MANIFEST_FILE} is malformed or lists no files.`);
|
|
163
|
+
}
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import type { PackManifest } from "../pack/pack-manifest.js";
|
|
5
|
+
import { PACK_MANIFEST_FILE, SCENARIO_JSON_FILE } from "../pack/pack-manifest.js";
|
|
6
|
+
import { DEFAULT_VIEWPORT, parseViewport, type Viewport } from "../pack/viewport.js";
|
|
7
|
+
import { createBackendClients } from "../deploy/client.js";
|
|
8
|
+
import { putFile } from "../deploy/upload.js";
|
|
9
|
+
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
10
|
+
import { runDeployFlow, localViewUrl, type DeployFlowDeps } from "../deploy/deploy-flow.js";
|
|
11
|
+
|
|
12
|
+
/** Default local backend (gRPC). Local dev shifts off 8080 to avoid colliding with stigmer-service. */
|
|
13
|
+
const DEFAULT_BACKEND = "http://localhost:8090";
|
|
14
|
+
|
|
15
|
+
/** Slug rule from ApiResourceMetadata: lowercase, hyphens, start letter, end alphanumeric. */
|
|
16
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
17
|
+
|
|
18
|
+
interface DeployOptions {
|
|
19
|
+
backend?: string;
|
|
20
|
+
org?: string;
|
|
21
|
+
slug?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerDeployCommand(program: Command): void {
|
|
26
|
+
program
|
|
27
|
+
.command("deploy")
|
|
28
|
+
.description(
|
|
29
|
+
"Deploy a packed bundle to Scenar Cloud and print its embed URL.\n\n" +
|
|
30
|
+
"Takes a bundle directory produced by `scenar pack` (containing\n" +
|
|
31
|
+
"pack-manifest.json). It ensures the parent scenario exists, opens a\n" +
|
|
32
|
+
"two-phase upload session, uploads every file directly to object\n" +
|
|
33
|
+
"storage via presigned URLs, completes the session, and prints the\n" +
|
|
34
|
+
"deploy's embed URL.\n\n" +
|
|
35
|
+
"Backend defaults to http://localhost:8090 (override with --backend or\n" +
|
|
36
|
+
"SCENAR_BACKEND). The owning org is required (--org or SCENAR_ORG).",
|
|
37
|
+
)
|
|
38
|
+
.argument("<bundleDir>", "path to a packed bundle directory (from scenar pack)")
|
|
39
|
+
.option("--backend <url>", "backend gRPC endpoint (default: $SCENAR_BACKEND or http://localhost:8090)")
|
|
40
|
+
.option("--org <org>", "owning organization (default: $SCENAR_ORG)")
|
|
41
|
+
.option("--slug <slug>", "scenario slug (default: the packed scenario id)")
|
|
42
|
+
.option("--name <name>", "scenario display name (default: the slug)")
|
|
43
|
+
.action(async (bundleDir: string, options: DeployOptions) => {
|
|
44
|
+
const resolvedDir = resolve(bundleDir);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const info = await stat(resolvedDir).catch(() => null);
|
|
48
|
+
if (!info || !info.isDirectory()) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${bundleDir} is not a directory. Pass a bundle produced by \`scenar pack\`.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const manifest = await readPackManifest(resolvedDir);
|
|
55
|
+
|
|
56
|
+
const backend = options.backend ?? process.env.SCENAR_BACKEND ?? DEFAULT_BACKEND;
|
|
57
|
+
const org = options.org ?? process.env.SCENAR_ORG;
|
|
58
|
+
if (!org) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"an owning org is required: pass --org <org> or set SCENAR_ORG.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const slug = options.slug ?? manifest.scenarioId;
|
|
64
|
+
if (!SLUG_PATTERN.test(slug)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`invalid scenario slug "${slug}". Slugs are lowercase letters, digits,\n` +
|
|
67
|
+
"and hyphens, starting with a letter and ending alphanumeric. Pass --slug to override.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const name = options.name ?? slug;
|
|
71
|
+
|
|
72
|
+
process.stderr.write(`Bundle: ${resolvedDir}\n`);
|
|
73
|
+
process.stderr.write(`Backend: ${backend}\n`);
|
|
74
|
+
process.stderr.write(`Org: ${org}\n`);
|
|
75
|
+
process.stderr.write(`Scenario: ${slug}\n\n`);
|
|
76
|
+
|
|
77
|
+
const clients = createBackendClients(backend);
|
|
78
|
+
const deps = makeDeps(clients, resolvedDir);
|
|
79
|
+
|
|
80
|
+
const { deployId, embedUrl } = await runDeployFlow(deps, { manifest, org, slug, name });
|
|
81
|
+
|
|
82
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Deployed ${deployId}\n`);
|
|
83
|
+
process.stdout.write(`${embedUrl}\n`);
|
|
84
|
+
|
|
85
|
+
const localUrl = localViewUrl(embedUrl);
|
|
86
|
+
if (localUrl !== embedUrl) {
|
|
87
|
+
process.stderr.write(` Local view: ${localUrl}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Embed snippet (guidance → stderr; stdout stays the bare URL for
|
|
91
|
+
// piping). Responsive iframe at the bundle's canonical aspect ratio
|
|
92
|
+
// (DD-002/DD-004); falls back to the default viewport for bundles packed
|
|
93
|
+
// before the viewport was recorded.
|
|
94
|
+
const viewport = await readScenarioViewport(resolvedDir, name);
|
|
95
|
+
process.stderr.write("\n Embed snippet (paste into any page):\n\n");
|
|
96
|
+
process.stderr.write(`${indent(buildEmbedSnippet({ embedUrl, viewport, title: name }))}\n`);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
99
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build the flow dependencies from real Connect clients + the bundle on disk. */
|
|
106
|
+
function makeDeps(
|
|
107
|
+
clients: ReturnType<typeof createBackendClients>,
|
|
108
|
+
bundleDir: string,
|
|
109
|
+
): DeployFlowDeps {
|
|
110
|
+
return {
|
|
111
|
+
async applyScenario({ org, slug, name }) {
|
|
112
|
+
// Idempotent upsert by (org, slug). The spec is a minimal valid placeholder
|
|
113
|
+
// (one step) — the embed plays from the packed JS, so the Scenario resource
|
|
114
|
+
// serves as the authorization parent and publication anchor, not the
|
|
115
|
+
// serving source. A richer spec projection is a deliberate follow-up.
|
|
116
|
+
const scenario = await clients.scenario.apply({
|
|
117
|
+
apiVersion: "scenario.scenar.ai/v1",
|
|
118
|
+
kind: "Scenario",
|
|
119
|
+
metadata: { org, slug, name },
|
|
120
|
+
spec: { steps: [{ view: "scenario", delayMs: 0 }] },
|
|
121
|
+
});
|
|
122
|
+
const id = scenario.metadata?.id;
|
|
123
|
+
if (!id) {
|
|
124
|
+
throw new Error("scenario apply returned no metadata.id");
|
|
125
|
+
}
|
|
126
|
+
return id;
|
|
127
|
+
},
|
|
128
|
+
async createSession(scenarioId, files) {
|
|
129
|
+
const response = await clients.deploy.createDeployUploadSession({ scenarioId, files });
|
|
130
|
+
return {
|
|
131
|
+
deployId: response.deployId,
|
|
132
|
+
uploadTargets: response.uploadTargets.map((target) => ({
|
|
133
|
+
relativePath: target.relativePath,
|
|
134
|
+
presignedPutUrl: target.presignedPutUrl,
|
|
135
|
+
requiredHeaders: target.requiredHeaders,
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
readBundleFile(relativePath) {
|
|
140
|
+
return readFile(join(bundleDir, ...relativePath.split("/")));
|
|
141
|
+
},
|
|
142
|
+
uploadFile(target, bytes) {
|
|
143
|
+
return putFile(target.presignedPutUrl, target.requiredHeaders, bytes);
|
|
144
|
+
},
|
|
145
|
+
async completeSession(deployId) {
|
|
146
|
+
const deploy = await clients.deploy.completeDeployUploadSession({ deployId });
|
|
147
|
+
const embedUrl = deploy.status?.embedUrl;
|
|
148
|
+
if (!embedUrl) {
|
|
149
|
+
throw new Error("deploy completed but returned no embed_url");
|
|
150
|
+
}
|
|
151
|
+
return embedUrl;
|
|
152
|
+
},
|
|
153
|
+
log(message) {
|
|
154
|
+
process.stderr.write(`${message}\n`);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Read the canonical viewport recorded in the bundle's scenario.json (DD-004).
|
|
161
|
+
* Falls back to {@link DEFAULT_VIEWPORT} for bundles packed before the viewport
|
|
162
|
+
* was recorded, or if the file is missing/malformed — a snippet with sensible
|
|
163
|
+
* proportions always beats no snippet. `scenarioLabel` is only for the warning.
|
|
164
|
+
*/
|
|
165
|
+
async function readScenarioViewport(
|
|
166
|
+
bundleDir: string,
|
|
167
|
+
scenarioLabel: string,
|
|
168
|
+
): Promise<Viewport> {
|
|
169
|
+
try {
|
|
170
|
+
const raw = await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8");
|
|
171
|
+
const viewport = parseViewport((JSON.parse(raw) as { viewport?: unknown }).viewport);
|
|
172
|
+
if (viewport) return viewport;
|
|
173
|
+
} catch {
|
|
174
|
+
// Missing/unreadable/invalid scenario.json — fall through to the default.
|
|
175
|
+
}
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
` Note: no recorded viewport for "${scenarioLabel}"; ` +
|
|
178
|
+
`snippet uses the default ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}. ` +
|
|
179
|
+
"Re-pack with the current CLI to embed at the exact aspect ratio.\n",
|
|
180
|
+
);
|
|
181
|
+
return DEFAULT_VIEWPORT;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Indent every line by two spaces (for nesting a block under a heading). */
|
|
185
|
+
function indent(block: string): string {
|
|
186
|
+
return block.replace(/^/gm, " ");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readPackManifest(bundleDir: string): Promise<PackManifest> {
|
|
190
|
+
const manifestPath = join(bundleDir, PACK_MANIFEST_FILE);
|
|
191
|
+
let raw: string;
|
|
192
|
+
try {
|
|
193
|
+
raw = await readFile(manifestPath, "utf-8");
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`no ${PACK_MANIFEST_FILE} in ${bundleDir}. Run \`scenar pack\` to produce a bundle first.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const parsed = JSON.parse(raw) as PackManifest;
|
|
200
|
+
if (!parsed.scenarioId || !Array.isArray(parsed.files) || parsed.files.length === 0) {
|
|
201
|
+
throw new Error(`${PACK_MANIFEST_FILE} is malformed or lists no files.`);
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2H1D"}
|