@t3lnet/sceneforge 1.0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2137 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
31
+
32
+ // src/index.ts
33
+ var index_exports = {};
34
+ __export(index_exports, {
35
+ DEMO_SCHEMA_VERSION: () => DEMO_SCHEMA_VERSION,
36
+ ScriptGenerator: () => ScriptGenerator,
37
+ VoiceSynthesizer: () => VoiceSynthesizer,
38
+ createClickAction: () => createClickAction,
39
+ createDragAction: () => createDragAction,
40
+ createEmptyDemo: () => createEmptyDemo,
41
+ createEmptyStep: () => createEmptyStep,
42
+ createHoverAction: () => createHoverAction,
43
+ createNavigateAction: () => createNavigateAction,
44
+ createScriptGenerator: () => createScriptGenerator,
45
+ createScrollAction: () => createScrollAction,
46
+ createScrollToAction: () => createScrollToAction,
47
+ createTypeAction: () => createTypeAction,
48
+ createUploadAction: () => createUploadAction,
49
+ createVoiceSynthesizer: () => createVoiceSynthesizer,
50
+ createWaitAction: () => createWaitAction,
51
+ createWaitForAction: () => createWaitForAction,
52
+ demoClick: () => demoClick,
53
+ demoDefinitionSchema: () => demoDefinitionSchema,
54
+ demoHover: () => demoHover,
55
+ demoType: () => demoType,
56
+ discoverDemos: () => discoverDemos,
57
+ formatValidationError: () => formatValidationError,
58
+ generateTimingManifest: () => generateTimingManifest,
59
+ highlightElement: () => highlightElement,
60
+ injectCursorOverlay: () => injectCursorOverlay,
61
+ loadDemoDefinition: () => loadDemoDefinition,
62
+ moveCursorTo: () => moveCursorTo,
63
+ parseDemoDefinition: () => parseDemoDefinition,
64
+ parseFromYAML: () => parseFromYAML,
65
+ removeCursorOverlay: () => removeCursorOverlay,
66
+ resolvePath: () => resolvePath,
67
+ resolveTarget: () => resolveTarget,
68
+ runDemo: () => runDemo,
69
+ runDemoFromFile: () => runDemoFromFile,
70
+ safeParseDemoDefinition: () => safeParseDemoDefinition,
71
+ serializeToYAML: () => serializeToYAML,
72
+ triggerClickRipple: () => triggerClickRipple,
73
+ validateDemoDefinition: () => validateDemoDefinition
74
+ });
75
+ module.exports = __toCommonJS(index_exports);
76
+
77
+ // ../shared/src/yaml-parser.ts
78
+ var import_yaml = require("yaml");
79
+
80
+ // ../shared/src/schema.ts
81
+ var import_zod = require("zod");
82
+ var DEMO_SCHEMA_VERSION = 1;
83
+ var stepTargetSchema = import_zod.z.object({
84
+ type: import_zod.z.enum(["button", "link", "input", "text", "selector"]),
85
+ text: import_zod.z.string().optional(),
86
+ selector: import_zod.z.string().optional(),
87
+ name: import_zod.z.string().optional()
88
+ }).strict().superRefine((target, ctx) => {
89
+ switch (target.type) {
90
+ case "selector":
91
+ if (!target.selector) {
92
+ ctx.addIssue({
93
+ code: import_zod.z.ZodIssueCode.custom,
94
+ message: "selector target requires selector"
95
+ });
96
+ }
97
+ break;
98
+ case "text":
99
+ if (!target.text) {
100
+ ctx.addIssue({
101
+ code: import_zod.z.ZodIssueCode.custom,
102
+ message: "text target requires text"
103
+ });
104
+ }
105
+ break;
106
+ case "button":
107
+ case "link":
108
+ case "input":
109
+ if (!target.text && !target.name && !target.selector) {
110
+ ctx.addIssue({
111
+ code: import_zod.z.ZodIssueCode.custom,
112
+ message: "target requires text, name, or selector"
113
+ });
114
+ }
115
+ break;
116
+ }
117
+ });
118
+ var waitConditionSchema = import_zod.z.object({
119
+ type: import_zod.z.enum(["text", "selector", "navigation", "idle", "selectorHidden", "textHidden"]),
120
+ value: import_zod.z.string().optional(),
121
+ timeout: import_zod.z.coerce.number().int().positive().optional()
122
+ }).strict().superRefine((condition, ctx) => {
123
+ if (["text", "selector", "selectorHidden", "textHidden"].includes(condition.type)) {
124
+ if (!condition.value) {
125
+ ctx.addIssue({
126
+ code: import_zod.z.ZodIssueCode.custom,
127
+ message: `wait condition '${condition.type}' requires value`
128
+ });
129
+ }
130
+ }
131
+ });
132
+ var dragConfigSchema = import_zod.z.object({
133
+ deltaX: import_zod.z.coerce.number(),
134
+ deltaY: import_zod.z.coerce.number(),
135
+ steps: import_zod.z.coerce.number().int().positive().optional()
136
+ }).strict();
137
+ var waitActionSchema = import_zod.z.object({
138
+ action: import_zod.z.literal("wait"),
139
+ duration: import_zod.z.coerce.number().nonnegative().optional(),
140
+ waitFor: waitConditionSchema.optional()
141
+ }).strict().superRefine((action, ctx) => {
142
+ if (action.duration === void 0 && action.waitFor === void 0) {
143
+ ctx.addIssue({
144
+ code: import_zod.z.ZodIssueCode.custom,
145
+ message: "wait action requires duration or waitFor"
146
+ });
147
+ }
148
+ });
149
+ var navigateActionSchema = import_zod.z.object({
150
+ action: import_zod.z.literal("navigate"),
151
+ path: import_zod.z.string().min(1),
152
+ waitFor: waitConditionSchema.optional()
153
+ }).strict();
154
+ var clickActionSchema = import_zod.z.object({
155
+ action: import_zod.z.literal("click"),
156
+ target: stepTargetSchema,
157
+ highlight: import_zod.z.boolean().optional(),
158
+ waitFor: waitConditionSchema.optional()
159
+ }).strict();
160
+ var typeActionSchema = import_zod.z.object({
161
+ action: import_zod.z.literal("type"),
162
+ target: stepTargetSchema,
163
+ text: import_zod.z.string(),
164
+ waitFor: waitConditionSchema.optional()
165
+ }).strict();
166
+ var uploadActionSchema = import_zod.z.object({
167
+ action: import_zod.z.literal("upload"),
168
+ file: import_zod.z.string().min(1),
169
+ target: stepTargetSchema.optional(),
170
+ waitFor: waitConditionSchema.optional()
171
+ }).strict();
172
+ var hoverActionSchema = import_zod.z.object({
173
+ action: import_zod.z.literal("hover"),
174
+ target: stepTargetSchema,
175
+ waitFor: waitConditionSchema.optional()
176
+ }).strict();
177
+ var scrollActionSchema = import_zod.z.object({
178
+ action: import_zod.z.literal("scroll"),
179
+ duration: import_zod.z.coerce.number().optional(),
180
+ waitFor: waitConditionSchema.optional()
181
+ }).strict();
182
+ var scrollToActionSchema = import_zod.z.object({
183
+ action: import_zod.z.literal("scrollTo"),
184
+ target: stepTargetSchema,
185
+ waitFor: waitConditionSchema.optional()
186
+ }).strict();
187
+ var dragActionSchema = import_zod.z.object({
188
+ action: import_zod.z.literal("drag"),
189
+ target: stepTargetSchema,
190
+ drag: dragConfigSchema,
191
+ waitFor: waitConditionSchema.optional()
192
+ }).strict();
193
+ var actionSchema = import_zod.z.union([
194
+ navigateActionSchema,
195
+ clickActionSchema,
196
+ typeActionSchema,
197
+ uploadActionSchema,
198
+ waitActionSchema,
199
+ hoverActionSchema,
200
+ scrollActionSchema,
201
+ scrollToActionSchema,
202
+ dragActionSchema
203
+ ]);
204
+ var demoStepSchema = import_zod.z.object({
205
+ id: import_zod.z.string().min(1),
206
+ script: import_zod.z.string(),
207
+ actions: import_zod.z.array(actionSchema)
208
+ }).strict();
209
+ var videoSegmentSchema = import_zod.z.object({
210
+ file: import_zod.z.string().min(1),
211
+ duration: import_zod.z.coerce.number().positive().optional(),
212
+ fade: import_zod.z.boolean().optional(),
213
+ fadeDuration: import_zod.z.coerce.number().positive().optional()
214
+ }).strict();
215
+ var musicStartPointSchema = import_zod.z.union([
216
+ import_zod.z.object({ type: import_zod.z.literal("beginning") }).strict(),
217
+ import_zod.z.object({ type: import_zod.z.literal("afterIntro") }).strict(),
218
+ import_zod.z.object({ type: import_zod.z.literal("step"), stepId: import_zod.z.string().min(1) }).strict(),
219
+ import_zod.z.object({ type: import_zod.z.literal("time"), seconds: import_zod.z.coerce.number().nonnegative() }).strict()
220
+ ]);
221
+ var musicEndPointSchema = import_zod.z.union([
222
+ import_zod.z.object({ type: import_zod.z.literal("end") }).strict(),
223
+ import_zod.z.object({ type: import_zod.z.literal("beforeOutro") }).strict(),
224
+ import_zod.z.object({ type: import_zod.z.literal("step"), stepId: import_zod.z.string().min(1) }).strict(),
225
+ import_zod.z.object({ type: import_zod.z.literal("time"), seconds: import_zod.z.coerce.number().nonnegative() }).strict()
226
+ ]);
227
+ var backgroundMusicSchema = import_zod.z.object({
228
+ file: import_zod.z.string().min(1),
229
+ volume: import_zod.z.coerce.number().min(0).max(1).optional(),
230
+ startAt: musicStartPointSchema.optional(),
231
+ endAt: musicEndPointSchema.optional(),
232
+ loop: import_zod.z.boolean().optional(),
233
+ fadeIn: import_zod.z.coerce.number().nonnegative().optional(),
234
+ fadeOut: import_zod.z.coerce.number().nonnegative().optional()
235
+ }).strict();
236
+ var mediaConfigSchema = import_zod.z.object({
237
+ intro: videoSegmentSchema.optional(),
238
+ outro: videoSegmentSchema.optional(),
239
+ backgroundMusic: backgroundMusicSchema.optional()
240
+ }).strict();
241
+ var demoDefinitionSchema = import_zod.z.object({
242
+ version: import_zod.z.coerce.number().int().positive().default(DEMO_SCHEMA_VERSION),
243
+ name: import_zod.z.string().min(1),
244
+ title: import_zod.z.string().min(1),
245
+ description: import_zod.z.string().optional(),
246
+ steps: import_zod.z.array(demoStepSchema),
247
+ media: mediaConfigSchema.optional()
248
+ }).strict();
249
+ function formatValidationError(error) {
250
+ if (!error || typeof error !== "object" || !("issues" in error)) {
251
+ return "Invalid demo definition";
252
+ }
253
+ const issues = error.issues;
254
+ return issues.map((issue) => {
255
+ const path4 = issue.path.length ? issue.path.join(".") : "root";
256
+ return `${path4}: ${issue.message}`;
257
+ }).join("; ");
258
+ }
259
+ function parseDemoDefinition(input) {
260
+ return demoDefinitionSchema.parse(input);
261
+ }
262
+ function safeParseDemoDefinition(input) {
263
+ return demoDefinitionSchema.safeParse(input);
264
+ }
265
+
266
+ // ../shared/src/yaml-parser.ts
267
+ function parseFromYAML(yamlString, options) {
268
+ const parsed = (0, import_yaml.parse)(yamlString);
269
+ if (!parsed || typeof parsed !== "object") {
270
+ throw new Error("Invalid YAML: expected an object");
271
+ }
272
+ const resolved = options?.resolveSecrets ? resolveSecrets(parsed, options.resolveSecrets) : parsed;
273
+ try {
274
+ return parseDemoDefinition(resolved);
275
+ } catch (error) {
276
+ throw new Error(formatValidationError(error));
277
+ }
278
+ }
279
+ function serializeToYAML(demo) {
280
+ const cleanDemo = {
281
+ version: demo.version ?? DEMO_SCHEMA_VERSION,
282
+ name: demo.name,
283
+ title: demo.title,
284
+ ...demo.description && { description: demo.description },
285
+ steps: demo.steps.map(serializeStep)
286
+ };
287
+ return (0, import_yaml.stringify)(cleanDemo, {
288
+ indent: 2,
289
+ lineWidth: 100,
290
+ defaultStringType: "QUOTE_DOUBLE",
291
+ defaultKeyType: "PLAIN"
292
+ });
293
+ }
294
+ function serializeStep(step) {
295
+ return {
296
+ id: step.id,
297
+ script: step.script,
298
+ actions: step.actions.map(serializeAction)
299
+ };
300
+ }
301
+ function serializeAction(action) {
302
+ const result = {
303
+ action: action.action
304
+ };
305
+ if (action.path !== void 0) {
306
+ result.path = action.path;
307
+ }
308
+ if (action.target !== void 0) {
309
+ result.target = serializeTarget(action.target);
310
+ }
311
+ if (action.text !== void 0) {
312
+ result.text = action.text;
313
+ }
314
+ if (action.file !== void 0) {
315
+ result.file = action.file;
316
+ }
317
+ if (action.duration !== void 0) {
318
+ result.duration = action.duration;
319
+ }
320
+ if (action.highlight === true) {
321
+ result.highlight = true;
322
+ }
323
+ if (action.waitFor !== void 0) {
324
+ result.waitFor = serializeWaitCondition(action.waitFor);
325
+ }
326
+ if (action.drag !== void 0) {
327
+ result.drag = {
328
+ deltaX: action.drag.deltaX,
329
+ deltaY: action.drag.deltaY,
330
+ ...action.drag.steps !== void 0 && { steps: action.drag.steps }
331
+ };
332
+ }
333
+ return result;
334
+ }
335
+ function serializeTarget(target) {
336
+ const result = {
337
+ type: target.type
338
+ };
339
+ if (target.text !== void 0) {
340
+ result.text = target.text;
341
+ }
342
+ if (target.selector !== void 0) {
343
+ result.selector = target.selector;
344
+ }
345
+ if (target.name !== void 0) {
346
+ result.name = target.name;
347
+ }
348
+ return result;
349
+ }
350
+ function serializeWaitCondition(waitFor) {
351
+ const result = {
352
+ type: waitFor.type
353
+ };
354
+ if (waitFor.value !== void 0) {
355
+ result.value = waitFor.value;
356
+ }
357
+ if (waitFor.timeout !== void 0) {
358
+ result.timeout = waitFor.timeout;
359
+ }
360
+ return result;
361
+ }
362
+ function validateDemoDefinition(definition) {
363
+ const result = safeParseDemoDefinition(definition);
364
+ if (!result.success) {
365
+ throw new Error(formatValidationError(result.error));
366
+ }
367
+ }
368
+ function createEmptyDemo() {
369
+ return {
370
+ version: DEMO_SCHEMA_VERSION,
371
+ name: "new-demo",
372
+ title: "New Demo",
373
+ description: "",
374
+ steps: []
375
+ };
376
+ }
377
+ function createEmptyStep(id) {
378
+ const stepId = id || `step-${Date.now()}`;
379
+ return {
380
+ id: stepId,
381
+ script: "",
382
+ actions: []
383
+ };
384
+ }
385
+ var SECRET_PATTERN = /\$\{(ENV|SECRET):([A-Za-z0-9_]+)\}/g;
386
+ function resolveSecrets(value, resolver, path4 = "root") {
387
+ if (typeof value === "string") {
388
+ if (!SECRET_PATTERN.test(value)) {
389
+ return value;
390
+ }
391
+ SECRET_PATTERN.lastIndex = 0;
392
+ return value.replace(SECRET_PATTERN, (_match, _type, key) => {
393
+ const resolved = resolver(key);
394
+ if (resolved === void 0) {
395
+ throw new Error(`Missing secret for ${key} at ${path4}`);
396
+ }
397
+ return resolved;
398
+ });
399
+ }
400
+ if (Array.isArray(value)) {
401
+ return value.map(
402
+ (entry, index) => resolveSecrets(entry, resolver, `${path4}[${index}]`)
403
+ );
404
+ }
405
+ if (value && typeof value === "object") {
406
+ const result = {};
407
+ for (const [key, entry] of Object.entries(value)) {
408
+ result[key] = resolveSecrets(entry, resolver, `${path4}.${key}`);
409
+ }
410
+ return result;
411
+ }
412
+ return value;
413
+ }
414
+
415
+ // ../shared/src/target-resolver.ts
416
+ function resolveTarget(target) {
417
+ if (target.selector) {
418
+ return target.selector;
419
+ }
420
+ switch (target.type) {
421
+ case "button":
422
+ if (target.text) {
423
+ return `button:has-text("${target.text}"), [role="button"]:has-text("${target.text}")`;
424
+ }
425
+ if (target.name) {
426
+ return `button[name="${target.name}"], [role="button"][name="${target.name}"]`;
427
+ }
428
+ break;
429
+ case "link":
430
+ if (target.text) {
431
+ return `a:has-text("${target.text}")`;
432
+ }
433
+ if (target.name) {
434
+ return `a[name="${target.name}"]`;
435
+ }
436
+ break;
437
+ case "input":
438
+ if (target.name) {
439
+ return `input[name="${target.name}"], textarea[name="${target.name}"]`;
440
+ }
441
+ if (target.text) {
442
+ return `label:has-text("${target.text}") + input, label:has-text("${target.text}") input`;
443
+ }
444
+ break;
445
+ case "text":
446
+ if (target.text) {
447
+ return `text="${target.text}"`;
448
+ }
449
+ break;
450
+ case "selector":
451
+ break;
452
+ }
453
+ throw new Error(`Unable to resolve target: ${JSON.stringify(target)}`);
454
+ }
455
+ function resolvePath(pathTemplate, variables) {
456
+ let result = pathTemplate;
457
+ for (const [key, value] of Object.entries(variables)) {
458
+ if (value === void 0) {
459
+ continue;
460
+ }
461
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
462
+ }
463
+ return result;
464
+ }
465
+
466
+ // ../shared/src/action-helpers.ts
467
+ function createClickAction(selector, highlight = false) {
468
+ return {
469
+ action: "click",
470
+ target: {
471
+ type: "selector",
472
+ selector
473
+ },
474
+ highlight
475
+ };
476
+ }
477
+ function createTypeAction(selector, text) {
478
+ return {
479
+ action: "type",
480
+ target: {
481
+ type: "selector",
482
+ selector
483
+ },
484
+ text
485
+ };
486
+ }
487
+ function createNavigateAction(path4) {
488
+ return {
489
+ action: "navigate",
490
+ path: path4
491
+ };
492
+ }
493
+ function createWaitAction(duration) {
494
+ return {
495
+ action: "wait",
496
+ duration
497
+ };
498
+ }
499
+ function createWaitForAction(type, value, timeout = 15e3) {
500
+ return {
501
+ action: "wait",
502
+ waitFor: {
503
+ type,
504
+ value,
505
+ timeout
506
+ }
507
+ };
508
+ }
509
+ function createUploadAction(file, selector) {
510
+ const action = {
511
+ action: "upload",
512
+ file
513
+ };
514
+ if (selector) {
515
+ action.target = {
516
+ type: "selector",
517
+ selector
518
+ };
519
+ }
520
+ return action;
521
+ }
522
+ function createHoverAction(selector) {
523
+ return {
524
+ action: "hover",
525
+ target: {
526
+ type: "selector",
527
+ selector
528
+ }
529
+ };
530
+ }
531
+ function createScrollAction(duration = 500) {
532
+ return {
533
+ action: "scroll",
534
+ duration
535
+ };
536
+ }
537
+ function createScrollToAction(selector) {
538
+ return {
539
+ action: "scrollTo",
540
+ target: {
541
+ type: "selector",
542
+ selector
543
+ }
544
+ };
545
+ }
546
+ function createDragAction(selector, deltaX, deltaY, steps = 20) {
547
+ return {
548
+ action: "drag",
549
+ target: {
550
+ type: "selector",
551
+ selector
552
+ },
553
+ drag: {
554
+ deltaX,
555
+ deltaY,
556
+ steps
557
+ }
558
+ };
559
+ }
560
+
561
+ // ../playwright/src/demo-runner.ts
562
+ var import_test = require("@playwright/test");
563
+ var fs3 = __toESM(require("fs/promises"), 1);
564
+ var path3 = __toESM(require("path"), 1);
565
+
566
+ // ../generation/src/voice-synthesis.ts
567
+ var import_child_process = require("child_process");
568
+ var import_elevenlabs = require("elevenlabs");
569
+ var fs = __toESM(require("fs/promises"), 1);
570
+ var path = __toESM(require("path"), 1);
571
+ var DEFAULT_MAX_OUTPUT_BYTES = 512 * 1024;
572
+ var DEFAULT_MAX_CONCURRENCY = 2;
573
+ var DEFAULT_RETRY_OPTIONS = {
574
+ retries: 3,
575
+ minDelayMs: 500,
576
+ maxDelayMs: 8e3
577
+ };
578
+ function sanitizeFileSegment(value, fallback = "segment", maxLength = 80) {
579
+ const raw = String(value ?? "").trim();
580
+ if (!raw) {
581
+ return fallback;
582
+ }
583
+ const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "_");
584
+ const collapsed = cleaned.replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
585
+ const safe = collapsed || fallback;
586
+ const trimmed = safe.length > maxLength ? safe.slice(0, maxLength) : safe;
587
+ if (trimmed === "." || trimmed === "..") {
588
+ return fallback;
589
+ }
590
+ return trimmed;
591
+ }
592
+ function appendWithLimit(buffer, chunk, limit) {
593
+ const next = buffer + chunk;
594
+ if (next.length <= limit) {
595
+ return next;
596
+ }
597
+ return next.slice(next.length - limit);
598
+ }
599
+ function runCommand(command, args, options = {}) {
600
+ const {
601
+ stdio = ["ignore", "pipe", "pipe"],
602
+ cwd,
603
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES
604
+ } = options;
605
+ return new Promise((resolve2, reject) => {
606
+ const child = (0, import_child_process.spawn)(command, args, { stdio, cwd });
607
+ let stdout = "";
608
+ let stderr = "";
609
+ if (child.stdout) {
610
+ child.stdout.on("data", (chunk) => {
611
+ stdout = appendWithLimit(stdout, chunk.toString(), maxOutputBytes);
612
+ });
613
+ }
614
+ if (child.stderr) {
615
+ child.stderr.on("data", (chunk) => {
616
+ stderr = appendWithLimit(stderr, chunk.toString(), maxOutputBytes);
617
+ });
618
+ }
619
+ child.on("error", (error) => {
620
+ reject(error);
621
+ });
622
+ child.on("close", (code) => {
623
+ if (code === 0) {
624
+ resolve2({ stdout, stderr });
625
+ return;
626
+ }
627
+ const error = new Error(`${command} exited with code ${code}`);
628
+ error.code = code ?? void 0;
629
+ error.stderr = stderr;
630
+ reject(error);
631
+ });
632
+ });
633
+ }
634
+ async function probeMediaDurationMs(filePath) {
635
+ try {
636
+ const { stdout } = await runCommand("ffprobe", [
637
+ "-v",
638
+ "error",
639
+ "-show_entries",
640
+ "format=duration",
641
+ "-of",
642
+ "default=noprint_wrappers=1:nokey=1",
643
+ filePath
644
+ ]);
645
+ const durationSec = parseFloat(stdout.trim());
646
+ if (Number.isFinite(durationSec)) {
647
+ return Math.round(durationSec * 1e3);
648
+ }
649
+ } catch {
650
+ return null;
651
+ }
652
+ return null;
653
+ }
654
+ function sleep(ms) {
655
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
656
+ }
657
+ function getStatusCode(error) {
658
+ if (!error || typeof error !== "object") return void 0;
659
+ const maybeError = error;
660
+ return maybeError.status ?? maybeError.statusCode ?? maybeError.response?.status;
661
+ }
662
+ function getHeaderValue(headers, name) {
663
+ if (!headers || typeof headers !== "object") {
664
+ return null;
665
+ }
666
+ const getter = headers.get;
667
+ if (typeof getter === "function") {
668
+ return getter.call(headers, name);
669
+ }
670
+ const record = headers;
671
+ const direct = record[name];
672
+ if (typeof direct === "string") {
673
+ return direct;
674
+ }
675
+ const lower = record[name.toLowerCase()];
676
+ if (typeof lower === "string") {
677
+ return lower;
678
+ }
679
+ const upper = record[name.toUpperCase()];
680
+ if (typeof upper === "string") {
681
+ return upper;
682
+ }
683
+ return null;
684
+ }
685
+ function getRetryAfterMs(error) {
686
+ if (!error || typeof error !== "object") return null;
687
+ const maybeError = error;
688
+ const headerValue = getHeaderValue(maybeError.response?.headers, "retry-after");
689
+ const retryAfter = headerValue ?? maybeError.retryAfter;
690
+ if (retryAfter === void 0 || retryAfter === null) {
691
+ return null;
692
+ }
693
+ const seconds = Number(retryAfter);
694
+ if (Number.isFinite(seconds)) {
695
+ return Math.max(0, Math.round(seconds * 1e3));
696
+ }
697
+ return null;
698
+ }
699
+ function isRetryableError(error) {
700
+ const status = getStatusCode(error);
701
+ if (status && [408, 429, 500, 502, 503, 504].includes(status)) {
702
+ return true;
703
+ }
704
+ const message = String(error?.message ?? "");
705
+ return /(rate limit|timeout|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up)/i.test(message);
706
+ }
707
+ function getRetryDelayMs(attempt, options, error) {
708
+ const minDelayMs = options.minDelayMs ?? DEFAULT_RETRY_OPTIONS.minDelayMs;
709
+ const maxDelayMs = options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs;
710
+ const baseDelay = Math.min(maxDelayMs, minDelayMs * 2 ** attempt);
711
+ const jitter = 0.5 + Math.random();
712
+ const retryAfterMs = getRetryAfterMs(error) ?? 0;
713
+ return Math.max(retryAfterMs, Math.round(baseDelay * jitter));
714
+ }
715
+ async function withRetry(fn, options = {}) {
716
+ const retries = options.retries ?? DEFAULT_RETRY_OPTIONS.retries;
717
+ let attempt = 0;
718
+ while (true) {
719
+ try {
720
+ return await fn();
721
+ } catch (error) {
722
+ if (!isRetryableError(error) || attempt >= retries) {
723
+ throw error;
724
+ }
725
+ const delayMs = getRetryDelayMs(attempt, options, error);
726
+ await sleep(delayMs);
727
+ attempt += 1;
728
+ }
729
+ }
730
+ }
731
+ var VoiceSynthesizer = class {
732
+ constructor(config) {
733
+ __publicField(this, "client");
734
+ __publicField(this, "config");
735
+ this.config = config;
736
+ this.client = new import_elevenlabs.ElevenLabsClient({
737
+ apiKey: config.apiKey
738
+ });
739
+ }
740
+ /**
741
+ * List available voices (useful for finding voice IDs)
742
+ */
743
+ async listVoices() {
744
+ const response = await this.client.voices.getAll();
745
+ return response.voices.map((voice) => ({
746
+ voiceId: voice.voice_id,
747
+ name: voice.name || "Unnamed",
748
+ category: voice.category || "unknown"
749
+ }));
750
+ }
751
+ /**
752
+ * Synthesize a single text segment to audio
753
+ */
754
+ async synthesizeSegment(text, outputPath, options) {
755
+ const audio = await this.client.textToSpeech.convert(this.config.voiceId, {
756
+ text,
757
+ model_id: this.config.modelId || "eleven_multilingual_v2",
758
+ voice_settings: {
759
+ stability: options?.stability ?? 0.5,
760
+ similarity_boost: options?.similarityBoost ?? 0.75,
761
+ style: options?.style ?? 0,
762
+ use_speaker_boost: options?.useSpeakerBoost ?? true
763
+ }
764
+ });
765
+ const chunks = [];
766
+ for await (const chunk of audio) {
767
+ chunks.push(Buffer.from(chunk));
768
+ }
769
+ const audioBuffer = Buffer.concat(chunks);
770
+ await fs.writeFile(outputPath, audioBuffer);
771
+ const probedDurationMs = await probeMediaDurationMs(outputPath);
772
+ if (probedDurationMs !== null) {
773
+ return { durationMs: probedDurationMs };
774
+ }
775
+ const fileSizeBytes = audioBuffer.length;
776
+ const estimatedDurationMs = Math.round(fileSizeBytes * 8 / 128);
777
+ console.warn(`[voice] ffprobe unavailable, using estimated duration for ${outputPath}`);
778
+ return { durationMs: estimatedDurationMs };
779
+ }
780
+ /**
781
+ * Generate a sound effect from text description
782
+ */
783
+ async generateSoundEffect(description, outputPath, options) {
784
+ const audio = await withRetry(
785
+ () => this.client.textToSoundEffects.convert({
786
+ text: description,
787
+ duration_seconds: options?.durationSeconds ?? 1,
788
+ prompt_influence: options?.promptInfluence ?? 0.3
789
+ }),
790
+ options?.retry
791
+ );
792
+ const chunks = [];
793
+ for await (const chunk of audio) {
794
+ chunks.push(Buffer.from(chunk));
795
+ }
796
+ await fs.writeFile(outputPath, Buffer.concat(chunks));
797
+ }
798
+ /**
799
+ * Synthesize all segments from a script JSON file
800
+ */
801
+ async synthesizeScript(scriptPath, outputDir, options) {
802
+ const scriptContent = await fs.readFile(scriptPath, "utf-8");
803
+ const script = JSON.parse(scriptContent);
804
+ const audioDir = path.join(outputDir, "audio", script.demoName);
805
+ await fs.mkdir(audioDir, { recursive: true });
806
+ const synthesizedSegments = new Array(script.segments.length);
807
+ const maxConcurrency = Math.max(
808
+ 1,
809
+ options?.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY
810
+ );
811
+ let progressCount = 0;
812
+ let nextIndex = 0;
813
+ const workers = Array.from({ length: Math.min(maxConcurrency, script.segments.length) }, async () => {
814
+ while (true) {
815
+ const index = nextIndex;
816
+ nextIndex += 1;
817
+ if (index >= script.segments.length) {
818
+ break;
819
+ }
820
+ const segment = script.segments[index];
821
+ const safeStepId = sanitizeFileSegment(segment.stepId, `step-${index + 1}`);
822
+ const audioFileName = `${String(index + 1).padStart(2, "0")}_${safeStepId}.mp3`;
823
+ const audioPath = path.join(audioDir, audioFileName);
824
+ progressCount += 1;
825
+ options?.onProgress?.(progressCount, script.segments.length, segment.stepId);
826
+ try {
827
+ const result = await withRetry(
828
+ () => this.synthesizeSegment(segment.text, audioPath, options?.voiceSettings),
829
+ options?.retry
830
+ );
831
+ synthesizedSegments[index] = {
832
+ stepId: segment.stepId,
833
+ audioPath,
834
+ durationMs: result.durationMs,
835
+ text: segment.text
836
+ };
837
+ console.log(`[voice] Synthesized: ${segment.stepId} (${result.durationMs.toFixed(0)}ms)`);
838
+ } catch (error) {
839
+ console.error(`[voice] Failed to synthesize ${segment.stepId}:`, error);
840
+ throw error;
841
+ }
842
+ }
843
+ });
844
+ await Promise.all(workers);
845
+ let soundEffectsPath;
846
+ if (options?.generateClickSounds) {
847
+ soundEffectsPath = path.join(audioDir, "click.mp3");
848
+ await this.generateClickSound(soundEffectsPath);
849
+ }
850
+ return {
851
+ demoName: script.demoName,
852
+ segments: synthesizedSegments,
853
+ soundEffectsPath
854
+ };
855
+ }
856
+ /**
857
+ * Generate a UI click sound effect
858
+ */
859
+ async generateClickSound(outputPath) {
860
+ await this.generateSoundEffect(
861
+ "soft UI button click, subtle, digital, clean interface sound",
862
+ outputPath,
863
+ { durationSeconds: 0.5, promptInfluence: 0.5 }
864
+ );
865
+ console.log("[voice] Generated click sound effect");
866
+ }
867
+ /**
868
+ * Generate ambient background music
869
+ */
870
+ async generateBackgroundMusic(outputPath, options) {
871
+ const styleDescriptions = {
872
+ corporate: "soft corporate background music, professional, minimal, ambient technology sounds",
873
+ tech: "modern technology background music, subtle electronic, innovative, clean",
874
+ calm: "calm ambient background music, peaceful, soft piano, gentle",
875
+ upbeat: "light upbeat background music, positive, motivational, subtle energy"
876
+ };
877
+ const description = styleDescriptions[options?.style || "tech"];
878
+ const duration = options?.durationSeconds ?? 30;
879
+ await this.generateSoundEffect(description, outputPath, {
880
+ durationSeconds: Math.min(duration, 30),
881
+ promptInfluence: 0.4
882
+ });
883
+ console.log(`[voice] Generated background music (${options?.style || "tech"} style)`);
884
+ }
885
+ };
886
+ async function generateTimingManifest(scriptPath, synthesisResult, outputPath) {
887
+ const scriptContent = await fs.readFile(scriptPath, "utf-8");
888
+ const script = JSON.parse(scriptContent);
889
+ const manifest = {
890
+ demoName: script.demoName,
891
+ segments: synthesisResult.segments.map((synth) => {
892
+ const originalSegment = script.segments.find((s) => s.stepId === synth.stepId);
893
+ return {
894
+ stepId: synth.stepId,
895
+ audioFile: synth.audioPath,
896
+ videoStartTimeMs: originalSegment?.startTimeMs ?? 0,
897
+ audioDurationMs: synth.durationMs,
898
+ text: synth.text
899
+ };
900
+ }),
901
+ soundEffects: synthesisResult.soundEffectsPath ? { clickSound: synthesisResult.soundEffectsPath } : void 0,
902
+ backgroundMusic: synthesisResult.backgroundMusicPath
903
+ };
904
+ await fs.writeFile(outputPath, JSON.stringify(manifest, null, 2));
905
+ return manifest;
906
+ }
907
+ function createVoiceSynthesizer(config) {
908
+ return new VoiceSynthesizer(config);
909
+ }
910
+
911
+ // ../generation/src/script-generator.ts
912
+ var fs2 = __toESM(require("fs/promises"), 1);
913
+ var path2 = __toESM(require("path"), 1);
914
+ var ScriptGenerator = class {
915
+ constructor(demoName, title, startTimeMs) {
916
+ __publicField(this, "segments", []);
917
+ __publicField(this, "stepBoundaries", []);
918
+ __publicField(this, "currentStepId", null);
919
+ __publicField(this, "currentStepStartMs", null);
920
+ __publicField(this, "demoName");
921
+ __publicField(this, "title");
922
+ __publicField(this, "startTime");
923
+ this.demoName = demoName;
924
+ this.title = title;
925
+ this.startTime = startTimeMs ?? Date.now();
926
+ }
927
+ /**
928
+ * Marks the start of a step (for video splitting).
929
+ */
930
+ startStep(stepId) {
931
+ if (this.currentStepId && this.currentStepStartMs !== null) {
932
+ this.stepBoundaries.push({
933
+ stepId: this.currentStepId,
934
+ stepIndex: this.stepBoundaries.length,
935
+ videoStartMs: this.currentStepStartMs,
936
+ videoEndMs: Date.now() - this.startTime
937
+ });
938
+ }
939
+ this.currentStepId = stepId;
940
+ this.currentStepStartMs = Date.now() - this.startTime;
941
+ }
942
+ /**
943
+ * Marks the end of all steps (call at demo completion).
944
+ */
945
+ finishAllSteps() {
946
+ if (this.currentStepId && this.currentStepStartMs !== null) {
947
+ this.stepBoundaries.push({
948
+ stepId: this.currentStepId,
949
+ stepIndex: this.stepBoundaries.length,
950
+ videoStartMs: this.currentStepStartMs,
951
+ videoEndMs: Date.now() - this.startTime
952
+ });
953
+ this.currentStepId = null;
954
+ this.currentStepStartMs = null;
955
+ }
956
+ }
957
+ /**
958
+ * Estimates reading duration for text.
959
+ * Based on ~150 words per minute average speaking rate.
960
+ */
961
+ estimateReadingDuration(text) {
962
+ const words = text.split(/\s+/).length;
963
+ const wpm = 150;
964
+ return Math.round(words / wpm * 60 * 1e3);
965
+ }
966
+ /**
967
+ * Adds a narration segment with automatic timing.
968
+ */
969
+ addSegment(stepId, text, options) {
970
+ const now = Date.now();
971
+ const startTimeMs = now - this.startTime;
972
+ const estimatedDurationMs = this.estimateReadingDuration(text);
973
+ this.segments.push({
974
+ stepId,
975
+ text,
976
+ startTimeMs,
977
+ endTimeMs: startTimeMs + estimatedDurationMs,
978
+ estimatedDurationMs,
979
+ pauseBeforeMs: options?.pauseBeforeMs,
980
+ pauseAfterMs: options?.pauseAfterMs
981
+ });
982
+ }
983
+ /**
984
+ * Updates the end time of the last segment.
985
+ * Call this when a step completes to capture actual duration.
986
+ */
987
+ completeLastSegment() {
988
+ if (this.segments.length > 0) {
989
+ const lastSegment = this.segments[this.segments.length - 1];
990
+ lastSegment.endTimeMs = Date.now() - this.startTime;
991
+ }
992
+ }
993
+ /**
994
+ * Generates the complete script output.
995
+ */
996
+ getOutput() {
997
+ const now = Date.now();
998
+ return {
999
+ demoName: this.demoName,
1000
+ title: this.title,
1001
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1002
+ totalDurationMs: now - this.startTime,
1003
+ segments: this.segments,
1004
+ stepBoundaries: this.stepBoundaries
1005
+ };
1006
+ }
1007
+ /**
1008
+ * Exports script as JSON.
1009
+ */
1010
+ async exportJSON(outputPath) {
1011
+ const output = this.getOutput();
1012
+ await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1013
+ await fs2.writeFile(outputPath, JSON.stringify(output, null, 2));
1014
+ }
1015
+ /**
1016
+ * Exports script as SRT subtitle format.
1017
+ */
1018
+ async exportSRT(outputPath) {
1019
+ const output = this.getOutput();
1020
+ let srt = "";
1021
+ output.segments.forEach((segment, index) => {
1022
+ const startTime = formatSRTTime(segment.startTimeMs);
1023
+ const endTime = formatSRTTime(segment.endTimeMs);
1024
+ srt += `${index + 1}
1025
+ `;
1026
+ srt += `${startTime} --> ${endTime}
1027
+ `;
1028
+ srt += `${segment.text}
1029
+
1030
+ `;
1031
+ });
1032
+ await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1033
+ await fs2.writeFile(outputPath, srt.trim());
1034
+ }
1035
+ /**
1036
+ * Exports script in a format suitable for AI voice generation services.
1037
+ * Includes SSML-like pause markers.
1038
+ */
1039
+ async exportAIVoiceScript(outputPath) {
1040
+ const output = this.getOutput();
1041
+ const voiceScript = {
1042
+ demoName: output.demoName,
1043
+ title: output.title,
1044
+ totalDuration: formatReadableTime(output.totalDurationMs),
1045
+ segments: output.segments.map((segment, index) => ({
1046
+ index: index + 1,
1047
+ stepId: segment.stepId,
1048
+ text: segment.text,
1049
+ ssmlHints: generateSSMLHints(segment),
1050
+ pauseBeforeMs: segment.pauseBeforeMs || 0,
1051
+ pauseAfterMs: segment.pauseAfterMs || 500,
1052
+ syncPointMs: segment.startTimeMs
1053
+ }))
1054
+ };
1055
+ await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1056
+ await fs2.writeFile(outputPath, JSON.stringify(voiceScript, null, 2));
1057
+ }
1058
+ /**
1059
+ * Exports script as human-readable markdown for voice actors.
1060
+ */
1061
+ async exportMarkdown(outputPath) {
1062
+ const output = this.getOutput();
1063
+ let markdown = `# ${output.title}
1064
+
1065
+ `;
1066
+ markdown += `**Demo:** ${output.demoName}
1067
+ `;
1068
+ markdown += `**Total Duration:** ${formatReadableTime(output.totalDurationMs)}
1069
+ `;
1070
+ markdown += `**Generated:** ${output.generatedAt}
1071
+
1072
+ `;
1073
+ markdown += `---
1074
+
1075
+ `;
1076
+ markdown += `## Script
1077
+
1078
+ `;
1079
+ output.segments.forEach((segment, index) => {
1080
+ markdown += `### ${index + 1}. ${segment.stepId}
1081
+
1082
+ `;
1083
+ markdown += `**Timing:** ${formatReadableTime(segment.startTimeMs)} - ${formatReadableTime(segment.endTimeMs)}
1084
+
1085
+ `;
1086
+ if (segment.pauseBeforeMs) {
1087
+ markdown += `*[Pause ${segment.pauseBeforeMs}ms before]*
1088
+
1089
+ `;
1090
+ }
1091
+ markdown += `> ${segment.text}
1092
+
1093
+ `;
1094
+ if (segment.pauseAfterMs) {
1095
+ markdown += `*[Pause ${segment.pauseAfterMs}ms after]*
1096
+
1097
+ `;
1098
+ }
1099
+ markdown += `---
1100
+
1101
+ `;
1102
+ });
1103
+ await fs2.mkdir(path2.dirname(outputPath), { recursive: true });
1104
+ await fs2.writeFile(outputPath, markdown);
1105
+ }
1106
+ };
1107
+ function formatSRTTime(ms) {
1108
+ const hours = Math.floor(ms / 36e5);
1109
+ const minutes = Math.floor(ms % 36e5 / 6e4);
1110
+ const seconds = Math.floor(ms % 6e4 / 1e3);
1111
+ const milliseconds = ms % 1e3;
1112
+ return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)},${pad(milliseconds, 3)}`;
1113
+ }
1114
+ function formatReadableTime(ms) {
1115
+ const totalSeconds = Math.floor(ms / 1e3);
1116
+ const hours = Math.floor(totalSeconds / 3600);
1117
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1118
+ const seconds = totalSeconds % 60;
1119
+ if (hours > 0) {
1120
+ return `${hours}:${pad(minutes, 2)}:${pad(seconds, 2)}`;
1121
+ }
1122
+ return `${minutes}:${pad(seconds, 2)}`;
1123
+ }
1124
+ function pad(num, size) {
1125
+ let s = num.toString();
1126
+ while (s.length < size) s = "0" + s;
1127
+ return s;
1128
+ }
1129
+ function generateSSMLHints(segment) {
1130
+ let ssml = segment.text;
1131
+ if (segment.pauseBeforeMs) {
1132
+ ssml = `<break time="${segment.pauseBeforeMs}ms"/> ${ssml}`;
1133
+ }
1134
+ if (segment.pauseAfterMs) {
1135
+ ssml = `${ssml} <break time="${segment.pauseAfterMs}ms"/>`;
1136
+ }
1137
+ return ssml;
1138
+ }
1139
+ function createScriptGenerator(demoName, title, startTimeMs) {
1140
+ return new ScriptGenerator(demoName, title, startTimeMs);
1141
+ }
1142
+
1143
+ // ../playwright/src/cursor-overlay.ts
1144
+ var CURSOR_STYLES = `
1145
+ /* Hide the real cursor when demo cursor is active */
1146
+ html.demo-cursor-active, html.demo-cursor-active * {
1147
+ cursor: none !important;
1148
+ }
1149
+
1150
+ /* Demo cursor container */
1151
+ #demo-cursor {
1152
+ position: fixed;
1153
+ pointer-events: none;
1154
+ z-index: 999999;
1155
+ will-change: left, top;
1156
+ }
1157
+
1158
+ /* Cursor arrow SVG - larger for visibility */
1159
+ #demo-cursor-arrow {
1160
+ width: 32px;
1161
+ height: 32px;
1162
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6)) drop-shadow(0 0 2px rgba(0, 0, 0, 0.4));
1163
+ }
1164
+
1165
+ /* Click ripple effect - larger and more visible */
1166
+ .demo-click-ripple {
1167
+ position: absolute;
1168
+ top: 4px;
1169
+ left: 4px;
1170
+ width: 60px;
1171
+ height: 60px;
1172
+ border-radius: 50%;
1173
+ background: radial-gradient(circle, rgba(239, 68, 68, 0.7) 0%, rgba(239, 68, 68, 0.3) 40%, rgba(239, 68, 68, 0) 70%);
1174
+ transform: translate(-50%, -50%) scale(0);
1175
+ animation: demo-ripple 0.5s ease-out forwards;
1176
+ pointer-events: none;
1177
+ }
1178
+
1179
+ @keyframes demo-ripple {
1180
+ 0% {
1181
+ transform: translate(-50%, -50%) scale(0);
1182
+ opacity: 1;
1183
+ }
1184
+ 100% {
1185
+ transform: translate(-50%, -50%) scale(2.5);
1186
+ opacity: 0;
1187
+ }
1188
+ }
1189
+
1190
+ /* Highlight ring for elements being clicked - thicker and more visible */
1191
+ .demo-highlight-ring {
1192
+ position: fixed;
1193
+ border: 3px solid rgba(239, 68, 68, 0.9);
1194
+ border-radius: 6px;
1195
+ pointer-events: none;
1196
+ z-index: 999998;
1197
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.3), inset 0 0 0 1px rgba(239, 68, 68, 0.2);
1198
+ animation: demo-highlight-pulse 0.8s ease-out forwards;
1199
+ }
1200
+
1201
+ @keyframes demo-highlight-pulse {
1202
+ 0% {
1203
+ opacity: 1;
1204
+ transform: scale(1);
1205
+ }
1206
+ 50% {
1207
+ opacity: 0.9;
1208
+ transform: scale(1.03);
1209
+ }
1210
+ 100% {
1211
+ opacity: 0;
1212
+ transform: scale(1.08);
1213
+ }
1214
+ }
1215
+
1216
+ /* Trail effect for cursor movement */
1217
+ .demo-cursor-trail {
1218
+ position: fixed;
1219
+ width: 8px;
1220
+ height: 8px;
1221
+ border-radius: 50%;
1222
+ background: rgba(239, 68, 68, 0.4);
1223
+ pointer-events: none;
1224
+ z-index: 999998;
1225
+ animation: demo-trail-fade 0.3s ease-out forwards;
1226
+ }
1227
+
1228
+ @keyframes demo-trail-fade {
1229
+ 0% {
1230
+ opacity: 0.6;
1231
+ transform: scale(1);
1232
+ }
1233
+ 100% {
1234
+ opacity: 0;
1235
+ transform: scale(0.5);
1236
+ }
1237
+ }
1238
+ `;
1239
+ var CURSOR_SVG = `
1240
+ <svg id="demo-cursor-arrow" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1241
+ <path d="M3 3L10.5 21L13 13L21 10.5L3 3Z" fill="#ef4444" stroke="white" stroke-width="2" stroke-linejoin="round"/>
1242
+ </svg>
1243
+ `;
1244
+ var NAME_HELPER_SCRIPT = `
1245
+ (() => {
1246
+ if (typeof window.__name !== "function") {
1247
+ window.__name = (target, value) => {
1248
+ try {
1249
+ Object.defineProperty(target, "name", { value, configurable: true });
1250
+ } catch {
1251
+ // Ignore failures setting function names.
1252
+ }
1253
+ return target;
1254
+ };
1255
+ }
1256
+ })();
1257
+ `;
1258
+ var nameHelperInstalled = /* @__PURE__ */ new WeakSet();
1259
+ async function ensureNameHelper(page) {
1260
+ if (!nameHelperInstalled.has(page)) {
1261
+ await page.addInitScript({ content: NAME_HELPER_SCRIPT });
1262
+ nameHelperInstalled.add(page);
1263
+ }
1264
+ await page.addScriptTag({ content: NAME_HELPER_SCRIPT });
1265
+ }
1266
+ async function injectCursorOverlay(page) {
1267
+ await ensureNameHelper(page);
1268
+ await page.addStyleTag({ content: CURSOR_STYLES });
1269
+ await page.evaluate((cursorSvg) => {
1270
+ const cursor = document.createElement("div");
1271
+ cursor.id = "demo-cursor";
1272
+ cursor.innerHTML = cursorSvg;
1273
+ cursor.style.left = "-100px";
1274
+ cursor.style.top = "-100px";
1275
+ document.body.appendChild(cursor);
1276
+ document.documentElement.classList.add("demo-cursor-active");
1277
+ window.__demoCursor = {
1278
+ setPosition: (x, y) => {
1279
+ cursor.style.left = `${x}px`;
1280
+ cursor.style.top = `${y}px`;
1281
+ },
1282
+ click: () => {
1283
+ const ripple = document.createElement("div");
1284
+ ripple.className = "demo-click-ripple";
1285
+ cursor.appendChild(ripple);
1286
+ setTimeout(() => ripple.remove(), 500);
1287
+ },
1288
+ highlight: (rect) => {
1289
+ const ring = document.createElement("div");
1290
+ ring.className = "demo-highlight-ring";
1291
+ ring.style.left = `${rect.x}px`;
1292
+ ring.style.top = `${rect.y}px`;
1293
+ ring.style.width = `${rect.width}px`;
1294
+ ring.style.height = `${rect.height}px`;
1295
+ document.body.appendChild(ring);
1296
+ setTimeout(() => ring.remove(), 800);
1297
+ },
1298
+ addTrail: (x, y) => {
1299
+ const trail = document.createElement("div");
1300
+ trail.className = "demo-cursor-trail";
1301
+ trail.style.left = `${x}px`;
1302
+ trail.style.top = `${y}px`;
1303
+ document.body.appendChild(trail);
1304
+ setTimeout(() => trail.remove(), 300);
1305
+ },
1306
+ destroy: () => {
1307
+ cursor.remove();
1308
+ document.documentElement.classList.remove("demo-cursor-active");
1309
+ document.querySelectorAll(".demo-cursor-trail, .demo-highlight-ring, .demo-click-ripple").forEach((el) => el.remove());
1310
+ }
1311
+ };
1312
+ }, CURSOR_SVG);
1313
+ }
1314
+ async function moveCursorTo(page, x, y, options) {
1315
+ const steps = options?.steps ?? 15;
1316
+ const trailEnabled = options?.trailEnabled ?? true;
1317
+ const currentPos = await page.evaluate(() => {
1318
+ const cursor = document.getElementById("demo-cursor");
1319
+ if (!cursor) return { x: 0, y: 0 };
1320
+ return {
1321
+ x: parseFloat(cursor.style.left) || 0,
1322
+ y: parseFloat(cursor.style.top) || 0
1323
+ };
1324
+ });
1325
+ if (currentPos.x < 0 || currentPos.y < 0) {
1326
+ currentPos.x = x > 100 ? x - 100 : 50;
1327
+ currentPos.y = y > 100 ? y - 100 : 50;
1328
+ await page.evaluate(
1329
+ ([startX, startY]) => {
1330
+ const api = window.__demoCursor;
1331
+ if (api) api.setPosition(startX, startY);
1332
+ },
1333
+ [currentPos.x, currentPos.y]
1334
+ );
1335
+ await page.waitForTimeout(50);
1336
+ }
1337
+ for (let i = 1; i <= steps; i++) {
1338
+ const t = i / steps;
1339
+ const easeT = 1 - Math.pow(1 - t, 3);
1340
+ const newX = currentPos.x + (x - currentPos.x) * easeT;
1341
+ const newY = currentPos.y + (y - currentPos.y) * easeT;
1342
+ await page.evaluate(
1343
+ ({ posX, posY, addTrail }) => {
1344
+ const api = window.__demoCursor;
1345
+ if (api) {
1346
+ api.setPosition(posX, posY);
1347
+ if (addTrail) {
1348
+ api.addTrail(posX + 4, posY + 4);
1349
+ }
1350
+ }
1351
+ },
1352
+ { posX: newX, posY: newY, addTrail: trailEnabled && i % 3 === 0 }
1353
+ );
1354
+ await page.waitForTimeout(20);
1355
+ }
1356
+ await page.evaluate(
1357
+ ([finalX, finalY]) => {
1358
+ const api = window.__demoCursor;
1359
+ if (api) api.setPosition(finalX, finalY);
1360
+ },
1361
+ [x, y]
1362
+ );
1363
+ await page.mouse.move(x, y);
1364
+ await page.waitForTimeout(100);
1365
+ }
1366
+ async function triggerClickRipple(page) {
1367
+ await page.evaluate(() => {
1368
+ const api = window.__demoCursor;
1369
+ if (api) {
1370
+ api.click();
1371
+ }
1372
+ });
1373
+ await page.waitForTimeout(150);
1374
+ }
1375
+ async function highlightElement(page, selector) {
1376
+ const boundingBox = await page.locator(selector).first().boundingBox();
1377
+ if (boundingBox) {
1378
+ await page.evaluate(
1379
+ (rect) => {
1380
+ const api = window.__demoCursor;
1381
+ if (api) {
1382
+ api.highlight(rect);
1383
+ }
1384
+ },
1385
+ {
1386
+ x: boundingBox.x - 6,
1387
+ y: boundingBox.y - 6,
1388
+ width: boundingBox.width + 12,
1389
+ height: boundingBox.height + 12
1390
+ }
1391
+ );
1392
+ }
1393
+ }
1394
+ async function removeCursorOverlay(page) {
1395
+ await page.evaluate(() => {
1396
+ const api = window.__demoCursor;
1397
+ if (api) {
1398
+ api.destroy();
1399
+ }
1400
+ });
1401
+ }
1402
+ async function demoClick(page, selector, options) {
1403
+ const element = page.locator(selector).first();
1404
+ const timeout = options?.timeoutMs ?? 1e4;
1405
+ await element.scrollIntoViewIfNeeded();
1406
+ await element.waitFor({ state: "visible", timeout });
1407
+ const boundingBox = await element.boundingBox();
1408
+ if (!boundingBox) {
1409
+ throw new Error(`Element not found or not visible: ${selector}`);
1410
+ }
1411
+ const centerX = boundingBox.x + boundingBox.width / 2;
1412
+ const centerY = boundingBox.y + boundingBox.height / 2;
1413
+ if (options?.delayBefore) {
1414
+ await page.waitForTimeout(options.delayBefore);
1415
+ }
1416
+ await moveCursorTo(page, centerX, centerY, { steps: 20 });
1417
+ if (options?.highlight) {
1418
+ await highlightElement(page, selector);
1419
+ await page.waitForTimeout(200);
1420
+ }
1421
+ await triggerClickRipple(page);
1422
+ await element.click({ timeout });
1423
+ await page.waitForTimeout(200);
1424
+ if (options?.delayAfter) {
1425
+ await page.waitForTimeout(options.delayAfter);
1426
+ }
1427
+ }
1428
+ async function demoHover(page, selector, options) {
1429
+ const element = page.locator(selector).first();
1430
+ const timeout = options?.timeoutMs ?? 1e4;
1431
+ await element.scrollIntoViewIfNeeded();
1432
+ await element.waitFor({ state: "visible", timeout });
1433
+ const boundingBox = await element.boundingBox();
1434
+ if (!boundingBox) {
1435
+ throw new Error(`Element not found or not visible: ${selector}`);
1436
+ }
1437
+ const centerX = boundingBox.x + boundingBox.width / 2;
1438
+ const centerY = boundingBox.y + boundingBox.height / 2;
1439
+ await moveCursorTo(page, centerX, centerY, { steps: 15 });
1440
+ if (options?.highlight) {
1441
+ await highlightElement(page, selector);
1442
+ }
1443
+ await element.hover({ timeout });
1444
+ await page.waitForTimeout(300);
1445
+ }
1446
+ async function demoType(page, selector, text, options) {
1447
+ const element = page.locator(selector).first();
1448
+ await element.scrollIntoViewIfNeeded();
1449
+ await element.waitFor({ state: "visible", timeout: 1e4 });
1450
+ const boundingBox = await element.boundingBox();
1451
+ if (!boundingBox) {
1452
+ throw new Error(`Element not found or not visible: ${selector}`);
1453
+ }
1454
+ const centerX = boundingBox.x + boundingBox.width / 2;
1455
+ const centerY = boundingBox.y + boundingBox.height / 2;
1456
+ await moveCursorTo(page, centerX, centerY);
1457
+ if (options?.highlight) {
1458
+ await highlightElement(page, selector);
1459
+ await page.waitForTimeout(150);
1460
+ }
1461
+ await triggerClickRipple(page);
1462
+ await element.click();
1463
+ await page.waitForTimeout(100);
1464
+ await element.fill(text);
1465
+ await page.waitForTimeout(200);
1466
+ }
1467
+
1468
+ // ../playwright/src/demo-runner.ts
1469
+ var DEFAULT_ACTION_TIMEOUT_MS = 15e3;
1470
+ var NAVIGATION_TIMEOUT_MS = 45e3;
1471
+ var UPLOAD_TIMEOUT_MS = 3e4;
1472
+ var ACTION_TIMEOUT_BUFFER_MS = 2e3;
1473
+ var DEFAULT_WAIT_TIMEOUT_MS = Number(process.env.DEMO_WAIT_TIMEOUT_MS ?? 15e3);
1474
+ var WAIT_RETRY_ATTEMPTS = Math.max(
1475
+ 1,
1476
+ Number(process.env.DEMO_WAIT_RETRY_ATTEMPTS ?? 2)
1477
+ );
1478
+ var WAIT_RETRY_BASE_DELAY_MS = Number(process.env.DEMO_WAIT_RETRY_DELAY_MS ?? 500);
1479
+ function sanitizeDiagnosticsSegment(value, fallback) {
1480
+ const raw = String(value ?? "").trim();
1481
+ if (!raw) {
1482
+ return fallback;
1483
+ }
1484
+ const cleaned = raw.replace(/[^a-zA-Z0-9._-]+/g, "_");
1485
+ const collapsed = cleaned.replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
1486
+ const safe = collapsed || fallback;
1487
+ const trimmed = safe.length > 80 ? safe.slice(0, 80) : safe;
1488
+ return trimmed === "." || trimmed === ".." ? fallback : trimmed;
1489
+ }
1490
+ function logEvent(level, event, data) {
1491
+ const payload = {
1492
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1493
+ level,
1494
+ event,
1495
+ ...data
1496
+ };
1497
+ const line = JSON.stringify(payload);
1498
+ if (level === "error") {
1499
+ console.error(line);
1500
+ } else if (level === "warn") {
1501
+ console.warn(line);
1502
+ } else {
1503
+ console.log(line);
1504
+ }
1505
+ }
1506
+ function getActionTimeoutMs(action) {
1507
+ if (action.action === "navigate") {
1508
+ return NAVIGATION_TIMEOUT_MS;
1509
+ }
1510
+ if (action.action === "upload") {
1511
+ return UPLOAD_TIMEOUT_MS;
1512
+ }
1513
+ if (action.action === "wait") {
1514
+ if (action.duration !== void 0) {
1515
+ return Math.max(DEFAULT_ACTION_TIMEOUT_MS, action.duration + ACTION_TIMEOUT_BUFFER_MS);
1516
+ }
1517
+ if (action.waitFor?.timeout) {
1518
+ return action.waitFor.timeout + ACTION_TIMEOUT_BUFFER_MS;
1519
+ }
1520
+ }
1521
+ if (action.waitFor?.timeout) {
1522
+ return action.waitFor.timeout + ACTION_TIMEOUT_BUFFER_MS;
1523
+ }
1524
+ return DEFAULT_ACTION_TIMEOUT_MS;
1525
+ }
1526
+ async function withActionTimeout(timeoutMs, label, fn) {
1527
+ let timeoutId = null;
1528
+ const timeoutPromise = new Promise((_, reject) => {
1529
+ timeoutId = setTimeout(() => {
1530
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
1531
+ }, timeoutMs);
1532
+ });
1533
+ try {
1534
+ return await Promise.race([fn(), timeoutPromise]);
1535
+ } finally {
1536
+ if (timeoutId) {
1537
+ clearTimeout(timeoutId);
1538
+ }
1539
+ }
1540
+ }
1541
+ async function captureDiagnostics(params) {
1542
+ const { context, stepId, stepIndex, actionIndex, action, error, runId } = params;
1543
+ const diagnosticsDir = path3.join(context.outputDir, "diagnostics");
1544
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1545
+ const stepLabel = sanitizeDiagnosticsSegment(stepId ?? "unknown-step", "unknown-step");
1546
+ const actionLabel = actionIndex !== void 0 ? `action-${actionIndex + 1}` : "action";
1547
+ const baseName = `${timestamp}-${runId}-${stepLabel}-${actionLabel}`;
1548
+ try {
1549
+ await fs3.mkdir(diagnosticsDir, { recursive: true });
1550
+ } catch {
1551
+ }
1552
+ const metadata = {
1553
+ runId,
1554
+ stepId,
1555
+ stepIndex,
1556
+ actionIndex,
1557
+ action,
1558
+ error: error instanceof Error ? error.message : String(error),
1559
+ url: context.page.url(),
1560
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1561
+ };
1562
+ try {
1563
+ const screenshotPath = path3.join(diagnosticsDir, `${baseName}.png`);
1564
+ await context.page.screenshot({ path: screenshotPath, fullPage: true });
1565
+ } catch {
1566
+ }
1567
+ try {
1568
+ const domContent = await context.page.content();
1569
+ const maxChars = 2e4;
1570
+ const snippet = domContent.length > maxChars ? `${domContent.slice(0, maxChars)}
1571
+ <!-- truncated -->` : domContent;
1572
+ const domPath = path3.join(diagnosticsDir, `${baseName}.dom.html`);
1573
+ await fs3.writeFile(domPath, snippet, "utf-8");
1574
+ } catch {
1575
+ }
1576
+ try {
1577
+ const metaPath = path3.join(diagnosticsDir, `${baseName}.json`);
1578
+ await fs3.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
1579
+ } catch {
1580
+ }
1581
+ }
1582
+ async function loadDemoDefinition(filePath, options) {
1583
+ const content = await fs3.readFile(filePath, "utf-8");
1584
+ const definition = parseFromYAML(content, options);
1585
+ validateDemoDefinition(definition);
1586
+ return definition;
1587
+ }
1588
+ function resolveFilePath(filePath, baseDir) {
1589
+ if (path3.isAbsolute(filePath)) {
1590
+ return filePath;
1591
+ }
1592
+ const root = baseDir ?? process.cwd();
1593
+ return path3.resolve(root, filePath);
1594
+ }
1595
+ function resolveNavigatePath(actionPath, context) {
1596
+ const resolvedPath = resolvePath(actionPath, { baseURL: context.baseURL });
1597
+ if (/\{[^}]+\}/.test(resolvedPath)) {
1598
+ throw new Error(
1599
+ `Navigate action contains unresolved template variables: ${resolvedPath}`
1600
+ );
1601
+ }
1602
+ return resolvedPath;
1603
+ }
1604
+ async function executeWaitCondition(page, condition, actionContext) {
1605
+ const timeout = condition.timeout || DEFAULT_WAIT_TIMEOUT_MS;
1606
+ let navigated = false;
1607
+ const getSelectorContext = async () => {
1608
+ if (!condition.value) return {};
1609
+ try {
1610
+ if (condition.type === "selector" || condition.type === "selectorHidden") {
1611
+ const count = await page.locator(condition.value).count();
1612
+ return { selectorCount: count };
1613
+ }
1614
+ if (condition.type === "text" || condition.type === "textHidden") {
1615
+ const count = await page.getByText(condition.value, { exact: false }).count();
1616
+ return { textMatchCount: count };
1617
+ }
1618
+ } catch (error) {
1619
+ return { selectorContextError: error instanceof Error ? error.message : String(error) };
1620
+ }
1621
+ return {};
1622
+ };
1623
+ for (let attempt = 0; attempt < WAIT_RETRY_ATTEMPTS; attempt += 1) {
1624
+ try {
1625
+ switch (condition.type) {
1626
+ case "text":
1627
+ if (!condition.value) {
1628
+ throw new Error("Wait for text requires 'value'");
1629
+ }
1630
+ await (0, import_test.expect)(page.getByText(condition.value, { exact: false }).first()).toBeVisible({
1631
+ timeout
1632
+ });
1633
+ break;
1634
+ case "selector":
1635
+ if (!condition.value) {
1636
+ throw new Error("Wait for selector requires 'value'");
1637
+ }
1638
+ await page.locator(condition.value).first().waitFor({ state: "visible", timeout });
1639
+ break;
1640
+ case "navigation":
1641
+ await page.waitForURL(/.*/, { waitUntil: "networkidle", timeout });
1642
+ navigated = true;
1643
+ break;
1644
+ case "idle":
1645
+ await page.waitForLoadState("networkidle", { timeout });
1646
+ break;
1647
+ case "selectorHidden":
1648
+ if (!condition.value) {
1649
+ throw new Error("Wait for selectorHidden requires 'value'");
1650
+ }
1651
+ await page.locator(condition.value).first().waitFor({ state: "hidden", timeout });
1652
+ break;
1653
+ case "textHidden":
1654
+ if (!condition.value) {
1655
+ throw new Error("Wait for textHidden requires 'value'");
1656
+ }
1657
+ await (0, import_test.expect)(page.getByText(condition.value, { exact: false }).first()).toBeHidden({
1658
+ timeout
1659
+ });
1660
+ break;
1661
+ }
1662
+ if (attempt > 0 && actionContext) {
1663
+ logEvent("info", "wait_retry_success", {
1664
+ runId: actionContext.runId,
1665
+ demoName: actionContext.demoName,
1666
+ stepId: actionContext.stepId,
1667
+ actionIndex: actionContext.actionIndex,
1668
+ waitType: condition.type,
1669
+ attempt: attempt + 1
1670
+ });
1671
+ }
1672
+ return navigated;
1673
+ } catch (error) {
1674
+ const selectorContext = await getSelectorContext();
1675
+ logEvent("warn", "wait_retry", {
1676
+ runId: actionContext?.runId,
1677
+ demoName: actionContext?.demoName,
1678
+ stepId: actionContext?.stepId,
1679
+ actionIndex: actionContext?.actionIndex,
1680
+ waitType: condition.type,
1681
+ waitValue: condition.value,
1682
+ timeout,
1683
+ attempt: attempt + 1,
1684
+ error: error instanceof Error ? error.message : String(error),
1685
+ ...selectorContext
1686
+ });
1687
+ if (attempt >= WAIT_RETRY_ATTEMPTS - 1) {
1688
+ throw error;
1689
+ }
1690
+ const delayMs = WAIT_RETRY_BASE_DELAY_MS * (attempt + 1);
1691
+ await page.waitForTimeout(delayMs);
1692
+ }
1693
+ }
1694
+ return navigated;
1695
+ }
1696
+ async function executeAction(action, context, actionContext) {
1697
+ const { page } = context;
1698
+ let navigated = false;
1699
+ const timeoutMs = getActionTimeoutMs(action);
1700
+ const startedAt = Date.now();
1701
+ logEvent("info", "action_start", {
1702
+ runId: actionContext.runId,
1703
+ demoName: actionContext.demoName,
1704
+ stepId: actionContext.stepId,
1705
+ stepIndex: actionContext.stepIndex,
1706
+ actionIndex: actionContext.actionIndex,
1707
+ actionType: action.action,
1708
+ timeoutMs
1709
+ });
1710
+ try {
1711
+ await withActionTimeout(timeoutMs, `${action.action} action`, async () => {
1712
+ switch (action.action) {
1713
+ case "navigate": {
1714
+ if (!action.path) {
1715
+ throw new Error("Navigate action missing 'path'");
1716
+ }
1717
+ const resolvedPath = resolveNavigatePath(action.path, context);
1718
+ const fullURL = resolvedPath.startsWith("http") ? resolvedPath : `${context.baseURL}${resolvedPath}`;
1719
+ await page.goto(fullURL, { waitUntil: "networkidle", timeout: timeoutMs });
1720
+ navigated = true;
1721
+ await injectCursorOverlay(page);
1722
+ break;
1723
+ }
1724
+ case "click": {
1725
+ if (!action.target) {
1726
+ throw new Error("Click action missing 'target'");
1727
+ }
1728
+ const selector = resolveTarget(action.target);
1729
+ await demoClick(page, selector, {
1730
+ highlight: action.highlight,
1731
+ delayAfter: 500,
1732
+ timeoutMs
1733
+ });
1734
+ break;
1735
+ }
1736
+ case "type": {
1737
+ if (!action.target) {
1738
+ throw new Error("Type action missing 'target'");
1739
+ }
1740
+ if (!action.text) {
1741
+ throw new Error("Type action missing 'text'");
1742
+ }
1743
+ const selector = resolveTarget(action.target);
1744
+ const element = page.locator(selector).first();
1745
+ await element.waitFor({ state: "visible", timeout: timeoutMs });
1746
+ await element.scrollIntoViewIfNeeded();
1747
+ const box = await element.boundingBox();
1748
+ if (box) {
1749
+ await moveCursorTo(page, box.x + box.width / 2, box.y + box.height / 2);
1750
+ }
1751
+ await element.click({ timeout: timeoutMs });
1752
+ await element.fill(action.text, { timeout: timeoutMs });
1753
+ break;
1754
+ }
1755
+ case "upload": {
1756
+ if (!action.file) {
1757
+ throw new Error("Upload action missing 'file'");
1758
+ }
1759
+ const filePath = resolveFilePath(action.file, context.assetBaseDir);
1760
+ const fileSelector = action.target?.selector || 'input[type="file"]';
1761
+ const fileInput = page.locator(fileSelector).first();
1762
+ await fileInput.setInputFiles(filePath, { timeout: timeoutMs });
1763
+ break;
1764
+ }
1765
+ case "wait": {
1766
+ if (action.duration) {
1767
+ await page.waitForTimeout(action.duration);
1768
+ } else if (action.waitFor) {
1769
+ navigated = await executeWaitCondition(page, action.waitFor, actionContext);
1770
+ if (navigated) {
1771
+ await injectCursorOverlay(page);
1772
+ }
1773
+ }
1774
+ break;
1775
+ }
1776
+ case "hover": {
1777
+ if (!action.target) {
1778
+ throw new Error("Hover action missing 'target'");
1779
+ }
1780
+ const selector = resolveTarget(action.target);
1781
+ const element = page.locator(selector).first();
1782
+ await element.waitFor({ state: "visible", timeout: timeoutMs });
1783
+ await element.scrollIntoViewIfNeeded();
1784
+ const box = await element.boundingBox();
1785
+ if (box) {
1786
+ await moveCursorTo(page, box.x + box.width / 2, box.y + box.height / 2);
1787
+ }
1788
+ await element.hover({ timeout: timeoutMs });
1789
+ break;
1790
+ }
1791
+ case "scroll": {
1792
+ const scrollAmount = action.duration || 500;
1793
+ await page.mouse.wheel(0, scrollAmount);
1794
+ await page.waitForTimeout(300);
1795
+ break;
1796
+ }
1797
+ case "scrollTo": {
1798
+ if (!action.target) {
1799
+ throw new Error("ScrollTo action missing 'target'");
1800
+ }
1801
+ const selector = resolveTarget(action.target);
1802
+ await page.locator(selector).first().scrollIntoViewIfNeeded({ timeout: timeoutMs });
1803
+ await page.waitForTimeout(300);
1804
+ break;
1805
+ }
1806
+ case "drag": {
1807
+ if (!action.target) {
1808
+ throw new Error("Drag action missing 'target'");
1809
+ }
1810
+ if (!action.drag) {
1811
+ throw new Error("Drag action missing 'drag' configuration");
1812
+ }
1813
+ const selector = resolveTarget(action.target);
1814
+ const element = page.locator(selector).first();
1815
+ await element.waitFor({ state: "visible", timeout: timeoutMs });
1816
+ await element.scrollIntoViewIfNeeded();
1817
+ const box = await element.boundingBox();
1818
+ if (!box) {
1819
+ throw new Error(`Drag target not found or not visible: ${selector}`);
1820
+ }
1821
+ const startX = box.x + box.width / 2;
1822
+ const startY = box.y + box.height / 2;
1823
+ const endX = startX + action.drag.deltaX;
1824
+ const endY = startY + action.drag.deltaY;
1825
+ const dragSteps = action.drag.steps || 20;
1826
+ await moveCursorTo(page, startX, startY, { steps: 15 });
1827
+ await page.waitForTimeout(100);
1828
+ await page.mouse.move(startX, startY);
1829
+ await page.mouse.down();
1830
+ await page.waitForTimeout(50);
1831
+ for (let i = 1; i <= dragSteps; i++) {
1832
+ const t = i / dragSteps;
1833
+ const easeT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
1834
+ const currentX = startX + (endX - startX) * easeT;
1835
+ const currentY = startY + (endY - startY) * easeT;
1836
+ await page.mouse.move(currentX, currentY);
1837
+ await page.evaluate(
1838
+ ([x, y, shouldAddTrail]) => {
1839
+ const api = window.__demoCursor;
1840
+ if (api) {
1841
+ api.setPosition(x, y);
1842
+ if (shouldAddTrail) {
1843
+ api.addTrail(x + 4, y + 4);
1844
+ }
1845
+ }
1846
+ },
1847
+ [currentX, currentY, i % 3 === 0]
1848
+ );
1849
+ await page.waitForTimeout(25);
1850
+ }
1851
+ await page.mouse.up();
1852
+ await page.waitForTimeout(200);
1853
+ break;
1854
+ }
1855
+ default:
1856
+ throw new Error(`Unknown action type: ${action.action}`);
1857
+ }
1858
+ if (action.waitFor && action.action !== "wait") {
1859
+ const waitNavigated = await executeWaitCondition(page, action.waitFor, actionContext);
1860
+ if (waitNavigated) {
1861
+ await injectCursorOverlay(page);
1862
+ navigated = true;
1863
+ }
1864
+ }
1865
+ await page.waitForTimeout(200);
1866
+ });
1867
+ logEvent("info", "action_complete", {
1868
+ runId: actionContext.runId,
1869
+ demoName: actionContext.demoName,
1870
+ stepId: actionContext.stepId,
1871
+ stepIndex: actionContext.stepIndex,
1872
+ actionIndex: actionContext.actionIndex,
1873
+ actionType: action.action,
1874
+ durationMs: Date.now() - startedAt,
1875
+ navigated
1876
+ });
1877
+ } catch (error) {
1878
+ logEvent("error", "action_failed", {
1879
+ runId: actionContext.runId,
1880
+ demoName: actionContext.demoName,
1881
+ stepId: actionContext.stepId,
1882
+ stepIndex: actionContext.stepIndex,
1883
+ actionIndex: actionContext.actionIndex,
1884
+ actionType: action.action,
1885
+ durationMs: Date.now() - startedAt,
1886
+ error: error instanceof Error ? error.message : String(error)
1887
+ });
1888
+ await captureDiagnostics({
1889
+ context,
1890
+ stepId: actionContext.stepId,
1891
+ stepIndex: actionContext.stepIndex,
1892
+ actionIndex: actionContext.actionIndex,
1893
+ action,
1894
+ error,
1895
+ runId: actionContext.runId
1896
+ });
1897
+ throw error;
1898
+ }
1899
+ return navigated;
1900
+ }
1901
+ async function executeStep(step, context, scriptGenerator, runId, demoName, stepIndex) {
1902
+ console.log(`[demo] Executing step: ${step.id} (${step.actions.length} actions)`);
1903
+ logEvent("info", "step_start", {
1904
+ runId,
1905
+ demoName,
1906
+ stepId: step.id,
1907
+ stepIndex,
1908
+ actionCount: step.actions.length
1909
+ });
1910
+ if (scriptGenerator) {
1911
+ scriptGenerator.addSegment(step.id, step.script);
1912
+ }
1913
+ for (let i = 0; i < step.actions.length; i++) {
1914
+ const action = step.actions[i];
1915
+ console.log(`[demo] Action ${i + 1}/${step.actions.length}: ${action.action}`);
1916
+ await executeAction(action, context, {
1917
+ stepId: step.id,
1918
+ stepIndex,
1919
+ actionIndex: i,
1920
+ runId,
1921
+ demoName
1922
+ });
1923
+ }
1924
+ logEvent("info", "step_complete", {
1925
+ runId,
1926
+ demoName,
1927
+ stepId: step.id,
1928
+ stepIndex,
1929
+ actionCount: step.actions.length
1930
+ });
1931
+ }
1932
+ function getStepStartTarget(step) {
1933
+ for (const action of step.actions) {
1934
+ if (action.action === "wait" || action.action === "navigate") {
1935
+ continue;
1936
+ }
1937
+ if (!action.target) {
1938
+ continue;
1939
+ }
1940
+ try {
1941
+ return {
1942
+ selector: resolveTarget(action.target),
1943
+ waitForState: action.action === "upload" ? "attached" : "visible"
1944
+ };
1945
+ } catch {
1946
+ return null;
1947
+ }
1948
+ }
1949
+ return null;
1950
+ }
1951
+ async function waitForStepReady(page, step) {
1952
+ const target = getStepStartTarget(step);
1953
+ if (!target) {
1954
+ return;
1955
+ }
1956
+ try {
1957
+ await page.locator(target.selector).first().waitFor({ state: target.waitForState, timeout: DEFAULT_WAIT_TIMEOUT_MS });
1958
+ } catch {
1959
+ }
1960
+ }
1961
+ function getStepEndSelector(step) {
1962
+ for (let index = step.actions.length - 1; index >= 0; index -= 1) {
1963
+ const action = step.actions[index];
1964
+ if (action.action === "wait" || action.action === "navigate") {
1965
+ continue;
1966
+ }
1967
+ if (!action.target) {
1968
+ continue;
1969
+ }
1970
+ try {
1971
+ return resolveTarget(action.target);
1972
+ } catch {
1973
+ return null;
1974
+ }
1975
+ }
1976
+ return null;
1977
+ }
1978
+ async function waitForStepExit(page, step) {
1979
+ const selector = getStepEndSelector(step);
1980
+ if (!selector) {
1981
+ return;
1982
+ }
1983
+ try {
1984
+ await page.locator(selector).first().waitFor({ state: "hidden", timeout: 1500 });
1985
+ } catch {
1986
+ }
1987
+ }
1988
+ async function runDemo(definition, context, options = {}) {
1989
+ const startTime = Date.now();
1990
+ const transitionDelayMs = 500;
1991
+ const runId = `${definition.name}-${startTime}`;
1992
+ let scriptGenerator = null;
1993
+ logEvent("info", "demo_start", {
1994
+ runId,
1995
+ demoName: definition.name,
1996
+ stepCount: definition.steps.length,
1997
+ baseURL: context.baseURL
1998
+ });
1999
+ try {
2000
+ await injectCursorOverlay(context.page);
2001
+ const hadVideoStartTime = context.videoRecordingStartTime !== void 0;
2002
+ const syncedVideoStartTimeMs = context.videoRecordingStartTime ?? Date.now();
2003
+ if (!hadVideoStartTime) {
2004
+ context.videoRecordingStartTime = syncedVideoStartTimeMs;
2005
+ }
2006
+ logEvent("info", "video_sync", {
2007
+ runId,
2008
+ demoName: definition.name,
2009
+ videoStartTimeMs: syncedVideoStartTimeMs,
2010
+ providedByCaller: hadVideoStartTime
2011
+ });
2012
+ if (options.generateScripts) {
2013
+ scriptGenerator = createScriptGenerator(
2014
+ definition.name,
2015
+ definition.title,
2016
+ syncedVideoStartTimeMs
2017
+ );
2018
+ }
2019
+ for (let stepIndex = 0; stepIndex < definition.steps.length; stepIndex += 1) {
2020
+ const step = definition.steps[stepIndex];
2021
+ if (scriptGenerator && stepIndex > 0) {
2022
+ await waitForStepReady(context.page, step);
2023
+ }
2024
+ if (scriptGenerator) {
2025
+ scriptGenerator.startStep(step.id);
2026
+ }
2027
+ await executeStep(step, context, scriptGenerator, runId, definition.name, stepIndex);
2028
+ if (scriptGenerator && stepIndex < definition.steps.length - 1) {
2029
+ await waitForStepExit(context.page, step);
2030
+ await context.page.waitForTimeout(transitionDelayMs);
2031
+ }
2032
+ }
2033
+ if (scriptGenerator) {
2034
+ scriptGenerator.finishAllSteps();
2035
+ const scriptOutputDir = options.scriptOutputDir ?? path3.join(context.outputDir, "scripts");
2036
+ const scriptBasePath = path3.join(scriptOutputDir, definition.name);
2037
+ await scriptGenerator.exportJSON(`${scriptBasePath}.json`);
2038
+ await scriptGenerator.exportSRT(`${scriptBasePath}.srt`);
2039
+ await scriptGenerator.exportMarkdown(`${scriptBasePath}.md`);
2040
+ await scriptGenerator.exportAIVoiceScript(`${scriptBasePath}.voice.json`);
2041
+ await removeCursorOverlay(context.page);
2042
+ logEvent("info", "demo_complete", {
2043
+ runId,
2044
+ demoName: definition.name,
2045
+ durationMs: Date.now() - startTime
2046
+ });
2047
+ return {
2048
+ success: true,
2049
+ demoName: definition.name,
2050
+ videoPath: context.videoPath,
2051
+ scriptPath: `${scriptBasePath}.json`,
2052
+ duration: Date.now() - startTime
2053
+ };
2054
+ }
2055
+ await removeCursorOverlay(context.page);
2056
+ logEvent("info", "demo_complete", {
2057
+ runId,
2058
+ demoName: definition.name,
2059
+ durationMs: Date.now() - startTime
2060
+ });
2061
+ return {
2062
+ success: true,
2063
+ demoName: definition.name,
2064
+ videoPath: context.videoPath,
2065
+ duration: Date.now() - startTime
2066
+ };
2067
+ } catch (error) {
2068
+ logEvent("error", "demo_failed", {
2069
+ runId,
2070
+ demoName: definition.name,
2071
+ durationMs: Date.now() - startTime,
2072
+ error: error instanceof Error ? error.message : String(error)
2073
+ });
2074
+ await captureDiagnostics({
2075
+ context,
2076
+ error,
2077
+ runId
2078
+ });
2079
+ return {
2080
+ success: false,
2081
+ demoName: definition.name,
2082
+ error: error instanceof Error ? error : new Error(String(error)),
2083
+ duration: Date.now() - startTime
2084
+ };
2085
+ }
2086
+ }
2087
+ async function runDemoFromFile(definitionPath, context, options) {
2088
+ const definition = await loadDemoDefinition(definitionPath);
2089
+ return runDemo(definition, context, options);
2090
+ }
2091
+ async function discoverDemos(demoDir) {
2092
+ const files = await fs3.readdir(demoDir);
2093
+ return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path3.join(demoDir, f));
2094
+ }
2095
+ // Annotate the CommonJS export names for ESM import in node:
2096
+ 0 && (module.exports = {
2097
+ DEMO_SCHEMA_VERSION,
2098
+ ScriptGenerator,
2099
+ VoiceSynthesizer,
2100
+ createClickAction,
2101
+ createDragAction,
2102
+ createEmptyDemo,
2103
+ createEmptyStep,
2104
+ createHoverAction,
2105
+ createNavigateAction,
2106
+ createScriptGenerator,
2107
+ createScrollAction,
2108
+ createScrollToAction,
2109
+ createTypeAction,
2110
+ createUploadAction,
2111
+ createVoiceSynthesizer,
2112
+ createWaitAction,
2113
+ createWaitForAction,
2114
+ demoClick,
2115
+ demoDefinitionSchema,
2116
+ demoHover,
2117
+ demoType,
2118
+ discoverDemos,
2119
+ formatValidationError,
2120
+ generateTimingManifest,
2121
+ highlightElement,
2122
+ injectCursorOverlay,
2123
+ loadDemoDefinition,
2124
+ moveCursorTo,
2125
+ parseDemoDefinition,
2126
+ parseFromYAML,
2127
+ removeCursorOverlay,
2128
+ resolvePath,
2129
+ resolveTarget,
2130
+ runDemo,
2131
+ runDemoFromFile,
2132
+ safeParseDemoDefinition,
2133
+ serializeToYAML,
2134
+ triggerClickRipple,
2135
+ validateDemoDefinition
2136
+ });
2137
+ //# sourceMappingURL=index.cjs.map