@t3lnet/sceneforge 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/cli/cli.js +80 -0
- package/cli/commands/add-audio-to-steps.js +328 -0
- package/cli/commands/concat-final-videos.js +480 -0
- package/cli/commands/doctor.js +102 -0
- package/cli/commands/generate-voiceover.js +304 -0
- package/cli/commands/pipeline.js +314 -0
- package/cli/commands/record-demo.js +305 -0
- package/cli/commands/setup.js +218 -0
- package/cli/commands/split-video.js +236 -0
- package/cli/utils/args.js +15 -0
- package/cli/utils/media.js +81 -0
- package/cli/utils/paths.js +93 -0
- package/cli/utils/sanitize.js +19 -0
- package/package.json +6 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { chromium } from "@playwright/test";
|
|
4
|
+
import {
|
|
5
|
+
loadDemoDefinition,
|
|
6
|
+
runDemo,
|
|
7
|
+
} from "@t3lnet/sceneforge";
|
|
8
|
+
import { config as loadEnv } from "dotenv";
|
|
9
|
+
import { getFlagValue, hasFlag } from "../utils/args.js";
|
|
10
|
+
import {
|
|
11
|
+
ensureDir,
|
|
12
|
+
getOutputPaths,
|
|
13
|
+
resolveEnvFile,
|
|
14
|
+
resolveRoot,
|
|
15
|
+
toAbsolute,
|
|
16
|
+
} from "../utils/paths.js";
|
|
17
|
+
import { getMediaDuration } from "../utils/media.js";
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
Run a YAML demo definition with Playwright and generate scripts
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
sceneforge record [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--definition <path> Path to the YAML demo definition
|
|
28
|
+
--demo <name> Demo name (resolved in --definitions-dir)
|
|
29
|
+
--definitions-dir <p> Directory for demo YAML files (default: examples)
|
|
30
|
+
--base-url <url> Base URL for the demo (required)
|
|
31
|
+
--start-path <path> Optional path/URL to open before running actions
|
|
32
|
+
--asset-root <path> Base directory for relative upload files
|
|
33
|
+
--env-file <path> Env file for secrets (defaults to .env if present)
|
|
34
|
+
--locale <locale> Locale for requests (default: en-US)
|
|
35
|
+
--root <path> Project root (defaults to cwd)
|
|
36
|
+
--output-dir <path> Output directory (defaults to output or e2e/output)
|
|
37
|
+
--storage-state <path> Playwright storage state JSON
|
|
38
|
+
--viewport <WxH> Viewport size, e.g. 1440x900 (default)
|
|
39
|
+
--width <px> Viewport width (overrides --viewport)
|
|
40
|
+
--height <px> Viewport height (overrides --viewport)
|
|
41
|
+
--headed Run browser headed
|
|
42
|
+
--slowmo <ms> Slow down Playwright actions
|
|
43
|
+
--no-video Skip video recording
|
|
44
|
+
--help, -h Show this help message
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
sceneforge record --definition demo-definitions/create-quote.yaml --base-url http://localhost:5173
|
|
48
|
+
sceneforge record --demo create-quote --definitions-dir examples --base-url http://localhost:5173
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseViewport(args) {
|
|
53
|
+
const viewportValue = getFlagValue(args, "--viewport");
|
|
54
|
+
const widthValue = getFlagValue(args, "--width");
|
|
55
|
+
const heightValue = getFlagValue(args, "--height");
|
|
56
|
+
|
|
57
|
+
const defaultViewport = { width: 1440, height: 900 };
|
|
58
|
+
|
|
59
|
+
if (widthValue || heightValue) {
|
|
60
|
+
const width = widthValue ? Number(widthValue) : defaultViewport.width;
|
|
61
|
+
const height = heightValue ? Number(heightValue) : defaultViewport.height;
|
|
62
|
+
return { width, height };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!viewportValue) {
|
|
66
|
+
return defaultViewport;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const match = viewportValue.match(/^(\d+)x(\d+)$/i);
|
|
70
|
+
if (!match) {
|
|
71
|
+
return defaultViewport;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveStartUrl(startPath, baseUrl) {
|
|
78
|
+
if (!startPath) return null;
|
|
79
|
+
const interpolated = startPath
|
|
80
|
+
.replace("{baseURL}", baseUrl);
|
|
81
|
+
|
|
82
|
+
if (interpolated.startsWith("http://") || interpolated.startsWith("https://")) {
|
|
83
|
+
return interpolated;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (interpolated.startsWith("/")) {
|
|
87
|
+
return `${baseUrl}${interpolated}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${baseUrl}/${interpolated}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function alignScriptToVideo(scriptPath, videoPath, recordingStartTimeMs) {
|
|
94
|
+
if (!scriptPath || !videoPath) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const [scriptContent, videoDurationSec] = await Promise.all([
|
|
100
|
+
fs.readFile(scriptPath, "utf-8"),
|
|
101
|
+
getMediaDuration(videoPath),
|
|
102
|
+
]);
|
|
103
|
+
const script = JSON.parse(scriptContent);
|
|
104
|
+
const videoDurationMs = Math.round(videoDurationSec * 1000);
|
|
105
|
+
const scriptDurationMs = Number(script.totalDurationMs ?? 0);
|
|
106
|
+
const safeScriptDurationMs = Number.isFinite(scriptDurationMs) ? scriptDurationMs : 0;
|
|
107
|
+
const alignmentOffsetMs = Math.max(0, safeScriptDurationMs - videoDurationMs);
|
|
108
|
+
|
|
109
|
+
const clampTime = (value) => {
|
|
110
|
+
const adjusted = Number(value ?? 0) - alignmentOffsetMs;
|
|
111
|
+
return Math.max(0, Math.round(adjusted));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const alignedSegments = Array.isArray(script.segments)
|
|
115
|
+
? script.segments.map((segment) => ({
|
|
116
|
+
...segment,
|
|
117
|
+
startTimeMs: clampTime(segment.startTimeMs),
|
|
118
|
+
endTimeMs: clampTime(segment.endTimeMs),
|
|
119
|
+
}))
|
|
120
|
+
: script.segments;
|
|
121
|
+
|
|
122
|
+
const alignedBoundaries = Array.isArray(script.stepBoundaries)
|
|
123
|
+
? script.stepBoundaries.map((boundary) => ({
|
|
124
|
+
...boundary,
|
|
125
|
+
videoStartMs: clampTime(boundary.videoStartMs),
|
|
126
|
+
videoEndMs: clampTime(boundary.videoEndMs),
|
|
127
|
+
}))
|
|
128
|
+
: script.stepBoundaries;
|
|
129
|
+
|
|
130
|
+
const updated = {
|
|
131
|
+
...script,
|
|
132
|
+
totalDurationMs: videoDurationMs,
|
|
133
|
+
segments: alignedSegments,
|
|
134
|
+
stepBoundaries: alignedBoundaries,
|
|
135
|
+
videoMetadata: {
|
|
136
|
+
videoPath,
|
|
137
|
+
durationMs: videoDurationMs,
|
|
138
|
+
alignmentOffsetMs,
|
|
139
|
+
recordingStartTimeMs,
|
|
140
|
+
alignedAt: new Date().toISOString(),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await fs.writeFile(scriptPath, JSON.stringify(updated, null, 2));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn("[record] Failed to align script timings:", error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function resolveDefinitionPath(rootDir, demo, definitionsDir) {
|
|
151
|
+
const dir = toAbsolute(rootDir, definitionsDir);
|
|
152
|
+
const yamlPath = path.join(dir, `${demo}.yaml`);
|
|
153
|
+
const ymlPath = path.join(dir, `${demo}.yml`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await fs.access(yamlPath);
|
|
157
|
+
return yamlPath;
|
|
158
|
+
} catch {
|
|
159
|
+
// continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(ymlPath);
|
|
164
|
+
return ymlPath;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function moveVideo(sourcePath, targetPath) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.rename(sourcePath, targetPath);
|
|
173
|
+
} catch {
|
|
174
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
175
|
+
await fs.rm(sourcePath, { force: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runRecordDemoCommand(argv) {
|
|
180
|
+
const args = argv ?? process.argv.slice(2);
|
|
181
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
182
|
+
|
|
183
|
+
if (help) {
|
|
184
|
+
printHelp();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const root = getFlagValue(args, "--root");
|
|
189
|
+
const outputDirOverride = getFlagValue(args, "--output-dir");
|
|
190
|
+
const baseUrl = getFlagValue(args, "--base-url");
|
|
191
|
+
const definitionArg = getFlagValue(args, "--definition");
|
|
192
|
+
const demo = getFlagValue(args, "--demo");
|
|
193
|
+
const definitionsDir = getFlagValue(args, "--definitions-dir") ?? "examples";
|
|
194
|
+
const storageState = getFlagValue(args, "--storage-state");
|
|
195
|
+
const startPath = getFlagValue(args, "--start-path") || getFlagValue(args, "--start-url");
|
|
196
|
+
const assetRoot = getFlagValue(args, "--asset-root");
|
|
197
|
+
const envFile = getFlagValue(args, "--env-file");
|
|
198
|
+
const localeFlag = getFlagValue(args, "--locale");
|
|
199
|
+
const headed = hasFlag(args, "--headed");
|
|
200
|
+
const slowMo = getFlagValue(args, "--slowmo");
|
|
201
|
+
const noVideo = hasFlag(args, "--no-video");
|
|
202
|
+
|
|
203
|
+
if (!baseUrl) {
|
|
204
|
+
console.error("[error] --base-url is required");
|
|
205
|
+
printHelp();
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rootDir = resolveRoot(root);
|
|
210
|
+
const outputPaths = await getOutputPaths(rootDir, outputDirOverride);
|
|
211
|
+
const resolvedEnvFile = await resolveEnvFile(rootDir, envFile);
|
|
212
|
+
if (resolvedEnvFile) {
|
|
213
|
+
loadEnv({ path: resolvedEnvFile });
|
|
214
|
+
}
|
|
215
|
+
const locale = localeFlag ?? process.env.DEMO_LOCALE ?? "en-US";
|
|
216
|
+
|
|
217
|
+
const definitionPath = definitionArg
|
|
218
|
+
? toAbsolute(rootDir, definitionArg)
|
|
219
|
+
: demo
|
|
220
|
+
? await resolveDefinitionPath(rootDir, demo, definitionsDir)
|
|
221
|
+
: null;
|
|
222
|
+
|
|
223
|
+
if (!definitionPath) {
|
|
224
|
+
console.error("[error] Demo definition not found. Provide --definition or --demo.");
|
|
225
|
+
printHelp();
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const definition = await loadDemoDefinition(definitionPath, {
|
|
230
|
+
resolveSecrets: (key) => process.env[key],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await ensureDir(outputPaths.outputDir);
|
|
234
|
+
await ensureDir(outputPaths.videosDir);
|
|
235
|
+
|
|
236
|
+
const viewport = parseViewport(args);
|
|
237
|
+
|
|
238
|
+
const recordDir = path.join(outputPaths.videosDir, ".recordings", definition.name);
|
|
239
|
+
if (!noVideo) {
|
|
240
|
+
await ensureDir(recordDir);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const browser = await chromium.launch({
|
|
244
|
+
headless: !headed,
|
|
245
|
+
slowMo: slowMo ? Number(slowMo) : undefined,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const context = await browser.newContext({
|
|
249
|
+
viewport,
|
|
250
|
+
recordVideo: noVideo ? undefined : { dir: recordDir, size: viewport },
|
|
251
|
+
storageState: storageState ? toAbsolute(rootDir, storageState) : undefined,
|
|
252
|
+
locale: locale || undefined,
|
|
253
|
+
extraHTTPHeaders: locale
|
|
254
|
+
? {
|
|
255
|
+
"Accept-Language": `${locale},en;q=0.9`,
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const page = await context.newPage();
|
|
261
|
+
const video = page.video();
|
|
262
|
+
const videoRecordingStartTime = Date.now();
|
|
263
|
+
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
264
|
+
|
|
265
|
+
if (startUrl) {
|
|
266
|
+
await page.goto(startUrl, { waitUntil: "networkidle" });
|
|
267
|
+
}
|
|
268
|
+
const result = await runDemo(
|
|
269
|
+
definition,
|
|
270
|
+
{
|
|
271
|
+
page,
|
|
272
|
+
baseURL: baseUrl,
|
|
273
|
+
outputDir: outputPaths.outputDir,
|
|
274
|
+
assetBaseDir: assetRoot ? toAbsolute(rootDir, assetRoot) : undefined,
|
|
275
|
+
videoRecordingStartTime,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
generateScripts: true,
|
|
279
|
+
scriptOutputDir: path.join(outputPaths.outputDir, "scripts"),
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await context.close();
|
|
284
|
+
await browser.close();
|
|
285
|
+
|
|
286
|
+
if (video) {
|
|
287
|
+
const recordedPath = await video.path();
|
|
288
|
+
const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
|
|
289
|
+
await moveVideo(recordedPath, finalVideoPath);
|
|
290
|
+
console.log(`[record] Video saved: ${finalVideoPath}`);
|
|
291
|
+
if (result.scriptPath) {
|
|
292
|
+
await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (result.success) {
|
|
297
|
+
console.log(`[record] ✓ Completed ${definition.name}`);
|
|
298
|
+
if (result.scriptPath) {
|
|
299
|
+
console.log(`[record] Script: ${result.scriptPath}`);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
console.error(`[record] ✗ Failed: ${result.error?.message ?? "Unknown error"}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { chromium } from "@playwright/test";
|
|
4
|
+
import { loadDemoDefinition, runDemo } from "@t3lnet/sceneforge";
|
|
5
|
+
import { config as loadEnv } from "dotenv";
|
|
6
|
+
import { getFlagValue, hasFlag } from "../utils/args.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureDir,
|
|
9
|
+
getOutputPaths,
|
|
10
|
+
resolveEnvFile,
|
|
11
|
+
resolveRoot,
|
|
12
|
+
toAbsolute,
|
|
13
|
+
} from "../utils/paths.js";
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`
|
|
17
|
+
Run a setup YAML to capture and cache login storage state
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
sceneforge setup [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--definition <path> Path to the YAML setup definition
|
|
24
|
+
--demo <name> Demo name (resolved in --definitions-dir)
|
|
25
|
+
--definitions-dir <p> Directory for YAML files (default: examples)
|
|
26
|
+
--base-url <url> Base URL for the app (required)
|
|
27
|
+
--start-path <path> Optional path/URL to open before running actions
|
|
28
|
+
--asset-root <path> Base directory for relative upload files
|
|
29
|
+
--root <path> Project root (defaults to cwd)
|
|
30
|
+
--output-dir <path> Output directory (defaults to output or e2e/output)
|
|
31
|
+
--storage-state <path> Where to save storage state JSON (defaults to output/storage/<name>.json)
|
|
32
|
+
--env-file <path> Env file for secrets (defaults to .env if present)
|
|
33
|
+
--locale <locale> Locale for requests (default: en-US)
|
|
34
|
+
--viewport <WxH> Viewport size, e.g. 1440x900 (default)
|
|
35
|
+
--width <px> Viewport width (overrides --viewport)
|
|
36
|
+
--height <px> Viewport height (overrides --viewport)
|
|
37
|
+
--headed Run browser headed (recommended for login)
|
|
38
|
+
--slowmo <ms> Slow down Playwright actions
|
|
39
|
+
--help, -h Show this help message
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
sceneforge setup --definition examples/setup-login.yaml --base-url http://localhost:5173 --start-path /app --headed
|
|
43
|
+
sceneforge setup --demo setup-login --definitions-dir examples --base-url http://localhost:5173 --storage-state output/storage/login.json
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseViewport(args) {
|
|
48
|
+
const viewportValue = getFlagValue(args, "--viewport");
|
|
49
|
+
const widthValue = getFlagValue(args, "--width");
|
|
50
|
+
const heightValue = getFlagValue(args, "--height");
|
|
51
|
+
|
|
52
|
+
const defaultViewport = { width: 1440, height: 900 };
|
|
53
|
+
|
|
54
|
+
if (widthValue || heightValue) {
|
|
55
|
+
const width = widthValue ? Number(widthValue) : defaultViewport.width;
|
|
56
|
+
const height = heightValue ? Number(heightValue) : defaultViewport.height;
|
|
57
|
+
return { width, height };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!viewportValue) {
|
|
61
|
+
return defaultViewport;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const match = viewportValue.match(/^(\d+)x(\d+)$/i);
|
|
65
|
+
if (!match) {
|
|
66
|
+
return defaultViewport;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveStartUrl(startPath, baseUrl) {
|
|
73
|
+
if (!startPath) return null;
|
|
74
|
+
const interpolated = startPath.replace("{baseURL}", baseUrl);
|
|
75
|
+
|
|
76
|
+
if (interpolated.startsWith("http://") || interpolated.startsWith("https://")) {
|
|
77
|
+
return interpolated;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (interpolated.startsWith("/")) {
|
|
81
|
+
return `${baseUrl}${interpolated}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return `${baseUrl}/${interpolated}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resolveDefinitionPath(rootDir, demo, definitionsDir) {
|
|
88
|
+
const dir = toAbsolute(rootDir, definitionsDir);
|
|
89
|
+
const yamlPath = path.join(dir, `${demo}.yaml`);
|
|
90
|
+
const ymlPath = path.join(dir, `${demo}.yml`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(yamlPath);
|
|
94
|
+
return yamlPath;
|
|
95
|
+
} catch {
|
|
96
|
+
// continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(ymlPath);
|
|
101
|
+
return ymlPath;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runSetupCommand(argv) {
|
|
108
|
+
const args = argv ?? process.argv.slice(2);
|
|
109
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
110
|
+
|
|
111
|
+
if (help) {
|
|
112
|
+
printHelp();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const root = getFlagValue(args, "--root");
|
|
117
|
+
const outputDirOverride = getFlagValue(args, "--output-dir");
|
|
118
|
+
const baseUrl = getFlagValue(args, "--base-url");
|
|
119
|
+
const definitionArg = getFlagValue(args, "--definition");
|
|
120
|
+
const demo = getFlagValue(args, "--demo");
|
|
121
|
+
const definitionsDir = getFlagValue(args, "--definitions-dir") ?? "examples";
|
|
122
|
+
const startPath = getFlagValue(args, "--start-path") || getFlagValue(args, "--start-url");
|
|
123
|
+
const storageStateArg = getFlagValue(args, "--storage-state");
|
|
124
|
+
const envFile = getFlagValue(args, "--env-file");
|
|
125
|
+
const localeFlag = getFlagValue(args, "--locale");
|
|
126
|
+
const assetRoot = getFlagValue(args, "--asset-root");
|
|
127
|
+
const headed = hasFlag(args, "--headed");
|
|
128
|
+
const slowMo = getFlagValue(args, "--slowmo");
|
|
129
|
+
|
|
130
|
+
if (!baseUrl) {
|
|
131
|
+
console.error("[error] --base-url is required");
|
|
132
|
+
printHelp();
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rootDir = resolveRoot(root);
|
|
137
|
+
const outputPaths = await getOutputPaths(rootDir, outputDirOverride);
|
|
138
|
+
const resolvedEnvFile = await resolveEnvFile(rootDir, envFile);
|
|
139
|
+
if (resolvedEnvFile) {
|
|
140
|
+
loadEnv({ path: resolvedEnvFile });
|
|
141
|
+
}
|
|
142
|
+
const locale = localeFlag ?? process.env.DEMO_LOCALE ?? "en-US";
|
|
143
|
+
|
|
144
|
+
const definitionPath = definitionArg
|
|
145
|
+
? toAbsolute(rootDir, definitionArg)
|
|
146
|
+
: demo
|
|
147
|
+
? await resolveDefinitionPath(rootDir, demo, definitionsDir)
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
if (!definitionPath) {
|
|
151
|
+
console.error("[error] Setup definition not found. Provide --definition or --demo.");
|
|
152
|
+
printHelp();
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const definition = await loadDemoDefinition(definitionPath, {
|
|
157
|
+
resolveSecrets: (key) => process.env[key],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const storageStatePath = storageStateArg
|
|
161
|
+
? toAbsolute(rootDir, storageStateArg)
|
|
162
|
+
: path.join(outputPaths.outputDir, "storage", `${definition.name}.json`);
|
|
163
|
+
|
|
164
|
+
await ensureDir(path.dirname(storageStatePath));
|
|
165
|
+
await ensureDir(outputPaths.outputDir);
|
|
166
|
+
|
|
167
|
+
const viewport = parseViewport(args);
|
|
168
|
+
|
|
169
|
+
const browser = await chromium.launch({
|
|
170
|
+
headless: !headed,
|
|
171
|
+
slowMo: slowMo ? Number(slowMo) : undefined,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
let context;
|
|
175
|
+
try {
|
|
176
|
+
context = await browser.newContext({
|
|
177
|
+
viewport,
|
|
178
|
+
locale: locale || undefined,
|
|
179
|
+
extraHTTPHeaders: locale
|
|
180
|
+
? {
|
|
181
|
+
"Accept-Language": `${locale},en;q=0.9`,
|
|
182
|
+
}
|
|
183
|
+
: undefined,
|
|
184
|
+
});
|
|
185
|
+
const page = await context.newPage();
|
|
186
|
+
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
187
|
+
|
|
188
|
+
if (startUrl) {
|
|
189
|
+
await page.goto(startUrl, { waitUntil: "networkidle" });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await runDemo(
|
|
193
|
+
definition,
|
|
194
|
+
{
|
|
195
|
+
page,
|
|
196
|
+
baseURL: baseUrl,
|
|
197
|
+
outputDir: outputPaths.outputDir,
|
|
198
|
+
assetBaseDir: assetRoot ? toAbsolute(rootDir, assetRoot) : undefined,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
generateScripts: false,
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (result.success) {
|
|
206
|
+
await context.storageState({ path: storageStatePath });
|
|
207
|
+
console.log(`[setup] ✓ Saved storage state: ${storageStatePath}`);
|
|
208
|
+
} else {
|
|
209
|
+
console.error(`[setup] ✗ Failed: ${result.error?.message ?? "Unknown error"}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
if (context) {
|
|
214
|
+
await context.close();
|
|
215
|
+
}
|
|
216
|
+
await browser.close();
|
|
217
|
+
}
|
|
218
|
+
}
|