@scenar/cli 0.3.0 → 0.4.0

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 (94) hide show
  1. package/package.json +4 -9
  2. package/src/__tests__/embed-snippet.test.ts +1 -1
  3. package/src/__tests__/install-command.test.ts +97 -0
  4. package/src/__tests__/local-url.test.ts +17 -0
  5. package/src/__tests__/run-install.test.ts +222 -0
  6. package/src/api.d.ts +3 -1
  7. package/src/api.d.ts.map +1 -1
  8. package/src/api.js +3 -1
  9. package/src/api.js.map +1 -1
  10. package/src/api.ts +10 -1
  11. package/src/commands/install.d.ts +3 -0
  12. package/src/commands/install.d.ts.map +1 -0
  13. package/src/commands/install.js +67 -0
  14. package/src/commands/install.js.map +1 -0
  15. package/src/commands/install.ts +89 -0
  16. package/src/commands/pack.js +2 -2
  17. package/src/commands/pack.js.map +1 -1
  18. package/src/commands/pack.ts +2 -2
  19. package/src/commands/serve.d.ts.map +1 -1
  20. package/src/commands/serve.js +1 -2
  21. package/src/commands/serve.js.map +1 -1
  22. package/src/commands/serve.ts +1 -2
  23. package/src/commands/try.js +1 -1
  24. package/src/commands/try.ts +1 -1
  25. package/src/embed/embed-snippet.d.ts.map +1 -0
  26. package/src/embed/embed-snippet.js.map +1 -0
  27. package/src/index.d.ts.map +1 -1
  28. package/src/index.js +3 -5
  29. package/src/index.js.map +1 -1
  30. package/src/index.ts +3 -5
  31. package/src/install/run-install.d.ts +72 -0
  32. package/src/install/run-install.d.ts.map +1 -0
  33. package/src/install/run-install.js +378 -0
  34. package/src/install/run-install.js.map +1 -0
  35. package/src/install/run-install.ts +461 -0
  36. package/src/install/scaffold.d.ts +29 -0
  37. package/src/install/scaffold.d.ts.map +1 -0
  38. package/src/install/scaffold.js +233 -0
  39. package/src/install/scaffold.js.map +1 -0
  40. package/src/install/scaffold.ts +255 -0
  41. package/src/pack/pack-manifest.d.ts +1 -1
  42. package/src/pack/pack-manifest.d.ts.map +1 -1
  43. package/src/pack/pack-manifest.ts +1 -1
  44. package/src/publish/publish-flow.d.ts +3 -3
  45. package/src/publish/publish-flow.js +3 -3
  46. package/src/publish/publish-flow.ts +3 -3
  47. package/src/publish/run-publish.js +1 -1
  48. package/src/publish/run-publish.js.map +1 -1
  49. package/src/publish/run-publish.ts +1 -1
  50. package/src/render/detect-render-export.d.ts +2 -2
  51. package/src/render/detect-render-export.js +2 -2
  52. package/src/render/detect-render-export.ts +2 -2
  53. package/src/serve/local-url.d.ts +8 -0
  54. package/src/serve/local-url.d.ts.map +1 -0
  55. package/src/serve/local-url.js +21 -0
  56. package/src/serve/local-url.js.map +1 -0
  57. package/src/serve/local-url.ts +20 -0
  58. package/src/serve/run-serve.js +2 -2
  59. package/src/serve/run-serve.js.map +1 -1
  60. package/src/serve/run-serve.ts +2 -2
  61. package/tsconfig.tsbuildinfo +1 -1
  62. package/src/__tests__/deploy-command.test.ts +0 -45
  63. package/src/__tests__/deploy-flow.test.ts +0 -125
  64. package/src/__tests__/deploy-upload.test.ts +0 -39
  65. package/src/commands/deploy.d.ts +0 -3
  66. package/src/commands/deploy.d.ts.map +0 -1
  67. package/src/commands/deploy.js +0 -150
  68. package/src/commands/deploy.js.map +0 -1
  69. package/src/commands/deploy.ts +0 -186
  70. package/src/commands/preview.d.ts +0 -3
  71. package/src/commands/preview.d.ts.map +0 -1
  72. package/src/commands/preview.js +0 -93
  73. package/src/commands/preview.js.map +0 -1
  74. package/src/commands/preview.ts +0 -138
  75. package/src/deploy/client.d.ts +0 -20
  76. package/src/deploy/client.d.ts.map +0 -1
  77. package/src/deploy/client.js +0 -22
  78. package/src/deploy/client.js.map +0 -1
  79. package/src/deploy/client.ts +0 -28
  80. package/src/deploy/deploy-flow.d.ts +0 -78
  81. package/src/deploy/deploy-flow.d.ts.map +0 -1
  82. package/src/deploy/deploy-flow.js +0 -53
  83. package/src/deploy/deploy-flow.js.map +0 -1
  84. package/src/deploy/deploy-flow.ts +0 -115
  85. package/src/deploy/embed-snippet.d.ts.map +0 -1
  86. package/src/deploy/embed-snippet.js.map +0 -1
  87. package/src/deploy/upload.d.ts +0 -10
  88. package/src/deploy/upload.d.ts.map +0 -1
  89. package/src/deploy/upload.js +0 -23
  90. package/src/deploy/upload.js.map +0 -1
  91. package/src/deploy/upload.ts +0 -29
  92. /package/src/{deploy → embed}/embed-snippet.d.ts +0 -0
  93. /package/src/{deploy → embed}/embed-snippet.js +0 -0
  94. /package/src/{deploy → embed}/embed-snippet.ts +0 -0
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { createProgram } from "../index.js";
3
-
4
- describe("scenar deploy", () => {
5
- let stderrOutput: string;
6
-
7
- beforeEach(() => {
8
- stderrOutput = "";
9
- vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
10
- stderrOutput += String(chunk);
11
- return true;
12
- });
13
- process.exitCode = undefined;
14
- });
15
-
16
- it("registers the deploy command", () => {
17
- const program = createProgram();
18
- const cmd = program.commands.find((c) => c.name() === "deploy");
19
- expect(cmd).toBeDefined();
20
- expect(cmd!.description()).toContain("embed URL");
21
- });
22
-
23
- it("accepts a bundle directory argument and exposes its options", () => {
24
- const program = createProgram();
25
- const cmd = program.commands.find((c) => c.name() === "deploy");
26
- expect(cmd!.registeredArguments[0]!.name()).toBe("bundleDir");
27
- const opts = cmd!.options.map((o) => o.long);
28
- expect(opts).toContain("--backend");
29
- expect(opts).toContain("--org");
30
- expect(opts).toContain("--slug");
31
- expect(opts).toContain("--name");
32
- });
33
-
34
- it("fails clearly when the bundle directory does not exist", async () => {
35
- const program = createProgram();
36
- program.exitOverride();
37
- try {
38
- await program.parseAsync(["node", "scenar", "deploy", "/tmp/__no_bundle__", "--org", "acme"]);
39
- } catch {
40
- // exitOverride
41
- }
42
- expect(process.exitCode).toBe(1);
43
- expect(stderrOutput).toContain("Error");
44
- });
45
- });
@@ -1,125 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import {
3
- runDeployFlow,
4
- toDeclaredFiles,
5
- localViewUrl,
6
- type DeployFlowDeps,
7
- type UploadTarget,
8
- } from "../deploy/deploy-flow.js";
9
- import type { PackManifest } from "../pack/pack-manifest.js";
10
-
11
- const MANIFEST: PackManifest = {
12
- schemaVersion: 1,
13
- scenarioId: "welcome-tour",
14
- files: [
15
- { path: "index.html", sha256: "a".repeat(64), sizeBytes: 397, contentType: "text/html" },
16
- { path: "assets/app.js", sha256: "b".repeat(64), sizeBytes: 356965, contentType: "text/javascript" },
17
- { path: "scenario.json", sha256: "c".repeat(64), sizeBytes: 91, contentType: "application/json" },
18
- ],
19
- };
20
-
21
- describe("toDeclaredFiles", () => {
22
- it("maps manifest files to declared files with bigint sizes and lowercase-hex sha256", () => {
23
- const declared = toDeclaredFiles(MANIFEST);
24
- expect(declared).toHaveLength(3);
25
- expect(declared[0]).toEqual({
26
- relativePath: "index.html",
27
- sha256: "a".repeat(64),
28
- sizeBytes: 397n,
29
- contentType: "text/html",
30
- });
31
- expect(typeof declared[1]!.sizeBytes).toBe("bigint");
32
- expect(declared[1]!.sizeBytes).toBe(356965n);
33
- });
34
- });
35
-
36
- describe("runDeployFlow", () => {
37
- it("ensures the scenario before creating the session, then uploads then completes", async () => {
38
- const calls: string[] = [];
39
- const targets: UploadTarget[] = [
40
- {
41
- relativePath: "index.html",
42
- presignedPutUrl: "https://store/idx",
43
- requiredHeaders: { "Content-Type": "text/html", "x-amz-checksum-sha256": "AAAA" },
44
- },
45
- {
46
- relativePath: "assets/app.js",
47
- presignedPutUrl: "https://store/js",
48
- requiredHeaders: { "Content-Type": "text/javascript", "x-amz-checksum-sha256": "BBBB" },
49
- },
50
- ];
51
-
52
- const uploaded: Array<{ target: UploadTarget; bytes: number }> = [];
53
-
54
- const deps: DeployFlowDeps = {
55
- applyScenario: vi.fn(async (input) => {
56
- calls.push("apply");
57
- expect(input).toEqual({ org: "acme", slug: "welcome-tour", name: "Welcome" });
58
- return "scn_123";
59
- }),
60
- createSession: vi.fn(async (scenarioId, files) => {
61
- calls.push("create");
62
- expect(scenarioId).toBe("scn_123");
63
- expect(files).toHaveLength(3);
64
- return { deployId: "dep_456", uploadTargets: targets };
65
- }),
66
- readBundleFile: vi.fn(async (rel) => new Uint8Array(rel.length)),
67
- uploadFile: vi.fn(async (target, bytes) => {
68
- calls.push(`upload:${target.relativePath}`);
69
- uploaded.push({ target, bytes: bytes.byteLength });
70
- }),
71
- completeSession: vi.fn(async (deployId) => {
72
- calls.push("complete");
73
- expect(deployId).toBe("dep_456");
74
- return "https://d-dep_456.localhost:8787/";
75
- }),
76
- log: () => {},
77
- };
78
-
79
- const result = await runDeployFlow(deps, {
80
- manifest: MANIFEST,
81
- org: "acme",
82
- slug: "welcome-tour",
83
- name: "Welcome",
84
- });
85
-
86
- expect(result).toEqual({ deployId: "dep_456", embedUrl: "https://d-dep_456.localhost:8787/" });
87
- // Ordering: ensure -> create -> upload each (in target order) -> complete.
88
- expect(calls).toEqual(["apply", "create", "upload:index.html", "upload:assets/app.js", "complete"]);
89
- // Required headers are passed through verbatim to each upload.
90
- expect(uploaded[0]!.target.requiredHeaders).toEqual(targets[0]!.requiredHeaders);
91
- expect(uploaded[1]!.target.requiredHeaders).toEqual(targets[1]!.requiredHeaders);
92
- });
93
-
94
- it("propagates a missing-embed-url failure from completion", async () => {
95
- const deps: DeployFlowDeps = {
96
- applyScenario: async () => "scn_1",
97
- createSession: async () => ({ deployId: "dep_1", uploadTargets: [] }),
98
- readBundleFile: async () => new Uint8Array(),
99
- uploadFile: async () => {},
100
- completeSession: async () => {
101
- throw new Error("deploy completed but returned no embed_url");
102
- },
103
- log: () => {},
104
- };
105
-
106
- await expect(
107
- runDeployFlow(deps, { manifest: MANIFEST, org: "acme", slug: "welcome-tour", name: "x" }),
108
- ).rejects.toThrow(/embed_url/);
109
- });
110
- });
111
-
112
- describe("localViewUrl", () => {
113
- it("downgrades https to http for *.localhost hosts (wrangler dev serves http)", () => {
114
- expect(localViewUrl("https://d-abc.localhost:8787/")).toBe("http://d-abc.localhost:8787/");
115
- expect(localViewUrl("https://localhost:8787/")).toBe("http://localhost:8787/");
116
- });
117
-
118
- it("leaves production https URLs untouched", () => {
119
- expect(localViewUrl("https://d-abc.scenar.ai/")).toBe("https://d-abc.scenar.ai/");
120
- });
121
-
122
- it("returns non-URL input unchanged", () => {
123
- expect(localViewUrl("not a url")).toBe("not a url");
124
- });
125
- });
@@ -1,39 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
2
- import { putFile } from "../deploy/upload.js";
3
-
4
- const realFetch = globalThis.fetch;
5
-
6
- afterEach(() => {
7
- globalThis.fetch = realFetch;
8
- vi.restoreAllMocks();
9
- });
10
-
11
- describe("putFile", () => {
12
- it("PUTs the raw bytes and replays required headers verbatim", async () => {
13
- const seen: { url?: string; init?: RequestInit } = {};
14
- globalThis.fetch = vi.fn(async (url: any, init: any) => {
15
- seen.url = String(url);
16
- seen.init = init;
17
- return new Response(null, { status: 200 });
18
- }) as unknown as typeof fetch;
19
-
20
- const bytes = new Uint8Array([1, 2, 3]);
21
- const headers = { "Content-Type": "text/html", "x-amz-checksum-sha256": "AAAA" };
22
- await putFile("https://store/key", headers, bytes);
23
-
24
- expect(seen.url).toBe("https://store/key");
25
- expect(seen.init!.method).toBe("PUT");
26
- expect(seen.init!.headers).toEqual(headers);
27
- expect(seen.init!.body).toBe(bytes);
28
- });
29
-
30
- it("throws with the status and body detail on a non-2xx response", async () => {
31
- globalThis.fetch = vi.fn(async () =>
32
- new Response("SignatureDoesNotMatch", { status: 403, statusText: "Forbidden" }),
33
- ) as unknown as typeof fetch;
34
-
35
- await expect(putFile("https://store/key", {}, new Uint8Array())).rejects.toThrow(
36
- /upload failed \(403 Forbidden\): SignatureDoesNotMatch/,
37
- );
38
- });
39
- });
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerDeployCommand(program: Command): void;
3
- //# sourceMappingURL=deploy.d.ts.map
@@ -1 +0,0 @@
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,CAqF5D"}
@@ -1,150 +0,0 @@
1
- import { resolve, join } from "node:path";
2
- import { readFile, stat } from "node:fs/promises";
3
- import { PACK_MANIFEST_FILE } from "../pack/pack-manifest.js";
4
- import { readBundleViewport } from "../bundle/read-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, recorded } = await readBundleViewport(resolvedDir);
66
- if (!recorded) {
67
- process.stderr.write(` Note: no recorded viewport for "${name}"; ` +
68
- `snippet uses the default ${viewport.width}x${viewport.height}. ` +
69
- "Re-pack with the current CLI to embed at the exact aspect ratio.\n");
70
- }
71
- process.stderr.write("\n Embed snippet (paste into any page):\n\n");
72
- process.stderr.write(`${indent(buildEmbedSnippet({ embedUrl, viewport, title: name }))}\n`);
73
- }
74
- catch (error) {
75
- const msg = error instanceof Error ? error.message : String(error);
76
- process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
77
- process.exitCode = 1;
78
- }
79
- });
80
- }
81
- /** Build the flow dependencies from real Connect clients + the bundle on disk. */
82
- function makeDeps(clients, bundleDir) {
83
- return {
84
- async applyScenario({ org, slug, name }) {
85
- // Idempotent upsert by (org, slug). The spec is a minimal valid placeholder
86
- // (one step) — the embed plays from the packed JS, so the Scenario resource
87
- // serves as the authorization parent and publication anchor, not the
88
- // serving source. A richer spec projection is a deliberate follow-up.
89
- const scenario = await clients.scenario.apply({
90
- apiVersion: "scenario.scenar.ai/v1",
91
- kind: "Scenario",
92
- metadata: { org, slug, name },
93
- spec: { steps: [{ view: "scenario", delayMs: 0 }] },
94
- });
95
- const id = scenario.metadata?.id;
96
- if (!id) {
97
- throw new Error("scenario apply returned no metadata.id");
98
- }
99
- return id;
100
- },
101
- async createSession(scenarioId, files) {
102
- const response = await clients.deploy.createDeployUploadSession({ scenarioId, files });
103
- return {
104
- deployId: response.deployId,
105
- uploadTargets: response.uploadTargets.map((target) => ({
106
- relativePath: target.relativePath,
107
- presignedPutUrl: target.presignedPutUrl,
108
- requiredHeaders: target.requiredHeaders,
109
- })),
110
- };
111
- },
112
- readBundleFile(relativePath) {
113
- return readFile(join(bundleDir, ...relativePath.split("/")));
114
- },
115
- uploadFile(target, bytes) {
116
- return putFile(target.presignedPutUrl, target.requiredHeaders, bytes);
117
- },
118
- async completeSession(deployId) {
119
- const deploy = await clients.deploy.completeDeployUploadSession({ deployId });
120
- const embedUrl = deploy.status?.embedUrl;
121
- if (!embedUrl) {
122
- throw new Error("deploy completed but returned no embed_url");
123
- }
124
- return embedUrl;
125
- },
126
- log(message) {
127
- process.stderr.write(`${message}\n`);
128
- },
129
- };
130
- }
131
- /** Indent every line by two spaces (for nesting a block under a heading). */
132
- function indent(block) {
133
- return block.replace(/^/gm, " ");
134
- }
135
- async function readPackManifest(bundleDir) {
136
- const manifestPath = join(bundleDir, PACK_MANIFEST_FILE);
137
- let raw;
138
- try {
139
- raw = await readFile(manifestPath, "utf-8");
140
- }
141
- catch {
142
- throw new Error(`no ${PACK_MANIFEST_FILE} in ${bundleDir}. Run \`scenar pack\` to produce a bundle first.`);
143
- }
144
- const parsed = JSON.parse(raw);
145
- if (!parsed.scenarioId || !Array.isArray(parsed.files) || parsed.files.length === 0) {
146
- throw new Error(`${PACK_MANIFEST_FILE} is malformed or lists no files.`);
147
- }
148
- return parsed;
149
- }
150
- //# sourceMappingURL=deploy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"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"}
@@ -1,186 +0,0 @@
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 } from "../pack/pack-manifest.js";
6
- import { readBundleViewport } from "../bundle/read-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, recorded } = await readBundleViewport(resolvedDir);
95
- if (!recorded) {
96
- process.stderr.write(
97
- ` Note: no recorded viewport for "${name}"; ` +
98
- `snippet uses the default ${viewport.width}x${viewport.height}. ` +
99
- "Re-pack with the current CLI to embed at the exact aspect ratio.\n",
100
- );
101
- }
102
- process.stderr.write("\n Embed snippet (paste into any page):\n\n");
103
- process.stderr.write(`${indent(buildEmbedSnippet({ embedUrl, viewport, title: name }))}\n`);
104
- } catch (error) {
105
- const msg = error instanceof Error ? error.message : String(error);
106
- process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
107
- process.exitCode = 1;
108
- }
109
- });
110
- }
111
-
112
- /** Build the flow dependencies from real Connect clients + the bundle on disk. */
113
- function makeDeps(
114
- clients: ReturnType<typeof createBackendClients>,
115
- bundleDir: string,
116
- ): DeployFlowDeps {
117
- return {
118
- async applyScenario({ org, slug, name }) {
119
- // Idempotent upsert by (org, slug). The spec is a minimal valid placeholder
120
- // (one step) — the embed plays from the packed JS, so the Scenario resource
121
- // serves as the authorization parent and publication anchor, not the
122
- // serving source. A richer spec projection is a deliberate follow-up.
123
- const scenario = await clients.scenario.apply({
124
- apiVersion: "scenario.scenar.ai/v1",
125
- kind: "Scenario",
126
- metadata: { org, slug, name },
127
- spec: { steps: [{ view: "scenario", delayMs: 0 }] },
128
- });
129
- const id = scenario.metadata?.id;
130
- if (!id) {
131
- throw new Error("scenario apply returned no metadata.id");
132
- }
133
- return id;
134
- },
135
- async createSession(scenarioId, files) {
136
- const response = await clients.deploy.createDeployUploadSession({ scenarioId, files });
137
- return {
138
- deployId: response.deployId,
139
- uploadTargets: response.uploadTargets.map((target) => ({
140
- relativePath: target.relativePath,
141
- presignedPutUrl: target.presignedPutUrl,
142
- requiredHeaders: target.requiredHeaders,
143
- })),
144
- };
145
- },
146
- readBundleFile(relativePath) {
147
- return readFile(join(bundleDir, ...relativePath.split("/")));
148
- },
149
- uploadFile(target, bytes) {
150
- return putFile(target.presignedPutUrl, target.requiredHeaders, bytes);
151
- },
152
- async completeSession(deployId) {
153
- const deploy = await clients.deploy.completeDeployUploadSession({ deployId });
154
- const embedUrl = deploy.status?.embedUrl;
155
- if (!embedUrl) {
156
- throw new Error("deploy completed but returned no embed_url");
157
- }
158
- return embedUrl;
159
- },
160
- log(message) {
161
- process.stderr.write(`${message}\n`);
162
- },
163
- };
164
- }
165
-
166
- /** Indent every line by two spaces (for nesting a block under a heading). */
167
- function indent(block: string): string {
168
- return block.replace(/^/gm, " ");
169
- }
170
-
171
- async function readPackManifest(bundleDir: string): Promise<PackManifest> {
172
- const manifestPath = join(bundleDir, PACK_MANIFEST_FILE);
173
- let raw: string;
174
- try {
175
- raw = await readFile(manifestPath, "utf-8");
176
- } catch {
177
- throw new Error(
178
- `no ${PACK_MANIFEST_FILE} in ${bundleDir}. Run \`scenar pack\` to produce a bundle first.`,
179
- );
180
- }
181
- const parsed = JSON.parse(raw) as PackManifest;
182
- if (!parsed.scenarioId || !Array.isArray(parsed.files) || parsed.files.length === 0) {
183
- throw new Error(`${PACK_MANIFEST_FILE} is malformed or lists no files.`);
184
- }
185
- return parsed;
186
- }
@@ -1,3 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerPreviewCommand(program: Command): void;
3
- //# sourceMappingURL=preview.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/commands/preview.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuB7D"}
@@ -1,93 +0,0 @@
1
- import * as path from "path";
2
- import { scanProject, generate, initMswServiceWorker } from "@scenar/preview";
3
- export function registerPreviewCommand(program) {
4
- const preview = program
5
- .command("preview")
6
- .description("Scan a React project and generate a preview view registry.");
7
- preview
8
- .command("init")
9
- .description("Scan a project and generate the .scenar/ directory.")
10
- .option("-s, --source <path>", "project to scan (default: cwd)")
11
- .option("-o, --output <path>", "output directory for generated files (default: .scenar relative to cwd)")
12
- .option("--reset-providers", "force-regenerate providers.tsx")
13
- .action(async (options) => {
14
- await runPreviewInit(options);
15
- });
16
- preview
17
- .command("sync")
18
- .description("Re-scan and update scanner-owned files (preserves user files).")
19
- .option("-s, --source <path>", "project to scan (default: cwd)")
20
- .option("-o, --output <path>", "output directory for generated files (default: .scenar relative to cwd)")
21
- .action(async (options) => {
22
- await runPreviewSync(options);
23
- });
24
- }
25
- async function runPreviewInit(options) {
26
- const sourceRoot = path.resolve(options.source ?? process.cwd());
27
- const outputDir = path.resolve(options.output ?? ".scenar");
28
- process.stdout.write(`\x1b[36m●\x1b[0m Scanning project: ${sourceRoot}\n`);
29
- const scanResult = scanProject(sourceRoot);
30
- process.stdout.write(` Found ${scanResult.discovered.length} components, ` +
31
- `skipped ${scanResult.skipped.length}\n`);
32
- process.stdout.write(` Framework: ${scanResult.framework}, ` +
33
- `entry: ${scanResult.entryPoint ?? "none detected"}\n`);
34
- if (scanResult.detectedProviders.length > 0) {
35
- process.stdout.write(` Providers: ${scanResult.detectedProviders.join(", ")}\n`);
36
- }
37
- const relOutput = path.relative(process.cwd(), outputDir);
38
- process.stdout.write(`\n\x1b[36m●\x1b[0m Generating ${relOutput}/\n`);
39
- const result = generate(scanResult, {
40
- sourceRoot,
41
- outputDir,
42
- isInit: true,
43
- resetProviders: options.resetProviders,
44
- });
45
- for (const file of result.written) {
46
- process.stdout.write(` \x1b[32m✓\x1b[0m ${file}\n`);
47
- }
48
- for (const file of result.preserved) {
49
- process.stdout.write(` \x1b[33m●\x1b[0m ${file} (preserved)\n`);
50
- }
51
- // --- MSW service worker ---
52
- const projectRoot = path.resolve(path.dirname(outputDir));
53
- const mswResult = initMswServiceWorker(projectRoot, scanResult.framework);
54
- if (mswResult.status === "created") {
55
- process.stdout.write(`\n\x1b[36m●\x1b[0m MSW service worker\n` +
56
- ` \x1b[32m✓\x1b[0m ${path.relative(process.cwd(), mswResult.path)}\n`);
57
- }
58
- else if (mswResult.status === "exists") {
59
- process.stdout.write(`\n\x1b[36m●\x1b[0m MSW service worker\n` +
60
- ` \x1b[33m●\x1b[0m ${path.relative(process.cwd(), mswResult.path)} (exists)\n`);
61
- }
62
- else if (mswResult.status === "error") {
63
- process.stdout.write(`\n\x1b[33m⚠\x1b[0m Could not generate MSW service worker: ${mswResult.error}\n` +
64
- ` Run \`npx msw init public/\` manually.\n`);
65
- }
66
- process.stdout.write(`\n\x1b[32m✓\x1b[0m Preview initialized.\n`);
67
- process.stdout.write(` Review ${relOutput}/report.md for scan details.\n`);
68
- process.stdout.write(` Add custom views in ${relOutput}/views.custom.tsx.\n`);
69
- process.stdout.write(` Customize providers in ${relOutput}/providers.tsx.\n`);
70
- }
71
- async function runPreviewSync(options) {
72
- const sourceRoot = path.resolve(options.source ?? process.cwd());
73
- const outputDir = path.resolve(options.output ?? ".scenar");
74
- process.stdout.write(`\x1b[36m●\x1b[0m Re-scanning project: ${sourceRoot}\n`);
75
- const scanResult = scanProject(sourceRoot);
76
- process.stdout.write(` Found ${scanResult.discovered.length} components, ` +
77
- `skipped ${scanResult.skipped.length}\n`);
78
- const relOutput = path.relative(process.cwd(), outputDir);
79
- process.stdout.write(`\n\x1b[36m●\x1b[0m Updating ${relOutput}/\n`);
80
- const result = generate(scanResult, {
81
- sourceRoot,
82
- outputDir,
83
- isInit: false,
84
- });
85
- for (const file of result.written) {
86
- process.stdout.write(` \x1b[32m✓\x1b[0m ${file} (updated)\n`);
87
- }
88
- for (const file of result.preserved) {
89
- process.stdout.write(` \x1b[33m●\x1b[0m ${file} (preserved)\n`);
90
- }
91
- process.stdout.write(`\n\x1b[32m✓\x1b[0m Preview synced.\n`);
92
- }
93
- //# sourceMappingURL=preview.js.map