studioflow 0.1.4 → 0.2.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.js CHANGED
@@ -6,9 +6,9 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/index.ts
9
- import fs12 from "node:fs/promises";
9
+ import fs13 from "node:fs/promises";
10
10
  import path13 from "node:path";
11
- import { fileURLToPath as fileURLToPath3 } from "node:url";
11
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
12
12
 
13
13
  // ../../node_modules/.pnpm/kleur@4.1.5/node_modules/kleur/index.mjs
14
14
  var FORCE_COLOR;
@@ -187,7 +187,9 @@ async function startBrowser(baseUrl, opts = {}) {
187
187
  }
188
188
 
189
189
  // ../../packages/adapters-playwright/src/actions.ts
190
+ import fs2 from "node:fs";
190
191
  import path from "node:path";
192
+ import { fileURLToPath } from "node:url";
191
193
 
192
194
  // ../../packages/adapters-playwright/src/assertions.ts
193
195
  async function assertText(page, text, timeoutMs = 5e3) {
@@ -216,9 +218,105 @@ function intEnv(name, fallback, env = process.env) {
216
218
  return Number.isFinite(parsed) ? parsed : fallback;
217
219
  }
218
220
  var maxDelayMs = 6e4;
221
+ var moduleDir = path.dirname(fileURLToPath(import.meta.url));
222
+ var macosCursorSvgDataUri = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2226%22%20height%3D%2236%22%20viewBox%3D%220%200%2026%2036%22%3E%3Cpath%20d%3D%22M2%201.5v26.2l6.7-6.4%204.4%2012.8%204.2-1.6-4.4-12.8h10.8L2%201.5z%22%20fill%3D%22%23111%22%2F%3E%3Cpath%20d%3D%22M4%204.5v18.1l5.1-4.9a1%201%200%200%201%201.6.4l3.7%2010.9%201.4-.5-3.7-10.9a1%201%200%200%201%20.9-1.3h7.8L4%204.5z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E";
223
+ var genericCursorSvgDataUri = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2230%22%20viewBox%3D%220%200%2022%2030%22%3E%3Cpath%20d%3D%22M1.5%201.5V22.2L7.4%2017.4L11%2028.5L14.4%2027L10.8%2015.9H19.6L1.5%201.5Z%22%20fill%3D%22white%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E";
224
+ var macosFallbackCursorAsset = {
225
+ dataUri: macosCursorSvgDataUri,
226
+ width: 26,
227
+ height: 36,
228
+ hotspotX: 2,
229
+ hotspotY: 1
230
+ };
231
+ var genericCursorAsset = {
232
+ dataUri: genericCursorSvgDataUri,
233
+ width: 22,
234
+ height: 30,
235
+ hotspotX: 2,
236
+ hotspotY: 1
237
+ };
238
+ var cursorAssetCache = /* @__PURE__ */ new Map();
239
+ function cursorThemeEnv(env = process.env) {
240
+ const raw = env.STUDIOFLOW_CURSOR_THEME?.trim().toLowerCase();
241
+ if (raw === "generic") return "generic";
242
+ return "macos";
243
+ }
244
+ function parsePngDimensions(buffer) {
245
+ if (buffer.length < 24) return null;
246
+ const pngHeader = [137, 80, 78, 71, 13, 10, 26, 10];
247
+ for (let index = 0; index < pngHeader.length; index += 1) {
248
+ if (buffer[index] !== pngHeader[index]) {
249
+ return null;
250
+ }
251
+ }
252
+ const width = buffer.readUInt32BE(16);
253
+ const height = buffer.readUInt32BE(20);
254
+ if (width <= 0 || height <= 0) return null;
255
+ return { width, height };
256
+ }
257
+ function resolveCursorPngPath(env = process.env) {
258
+ const projectRoot = env.INIT_CWD ?? process.cwd();
259
+ const explicitPath = env.STUDIOFLOW_CURSOR_PNG_PATH?.trim();
260
+ const candidates = [
261
+ explicitPath ? path.isAbsolute(explicitPath) ? explicitPath : path.resolve(projectRoot, explicitPath) : "",
262
+ path.resolve(projectRoot, "apps/cli/assets/cursor.png"),
263
+ path.resolve(projectRoot, "apps/cli/cursor.png"),
264
+ path.resolve(projectRoot, "assets/cursor.png"),
265
+ path.resolve(projectRoot, "cursor.png"),
266
+ path.resolve(moduleDir, "../../../apps/cli/assets/cursor.png"),
267
+ path.resolve(moduleDir, "../../../apps/cli/cursor.png"),
268
+ path.resolve(moduleDir, "../assets/cursor.png"),
269
+ path.resolve(moduleDir, "../cursor.png")
270
+ ];
271
+ const visited = /* @__PURE__ */ new Set();
272
+ for (const candidate of candidates) {
273
+ if (!candidate || visited.has(candidate)) continue;
274
+ visited.add(candidate);
275
+ if (fs2.existsSync(candidate)) {
276
+ return candidate;
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+ function loadCursorPngAsset(filePath, env = process.env) {
282
+ const cached = cursorAssetCache.get(filePath);
283
+ if (cached) return cached;
284
+ try {
285
+ const image = fs2.readFileSync(filePath);
286
+ const dimensions = parsePngDimensions(image);
287
+ const maxEdge = Math.max(20, intEnv("STUDIOFLOW_CURSOR_MAX_EDGE_PX", 34, env));
288
+ const sourceWidth = dimensions?.width ?? 26;
289
+ const sourceHeight = dimensions?.height ?? 36;
290
+ const scale = maxEdge / Math.max(sourceWidth, sourceHeight);
291
+ const width = Math.max(18, Math.round(sourceWidth * scale));
292
+ const height = Math.max(18, Math.round(sourceHeight * scale));
293
+ const asset = {
294
+ dataUri: `data:image/png;base64,${image.toString("base64")}`,
295
+ width,
296
+ height,
297
+ hotspotX: Math.max(1, Math.round(width * 0.08)),
298
+ hotspotY: Math.max(1, Math.round(height * 0.08))
299
+ };
300
+ cursorAssetCache.set(filePath, asset);
301
+ return asset;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+ function resolveCursorOverlayAsset(theme, env = process.env) {
307
+ if (theme === "generic") {
308
+ return genericCursorAsset;
309
+ }
310
+ const cursorPngPath = resolveCursorPngPath(env);
311
+ if (!cursorPngPath) {
312
+ return macosFallbackCursorAsset;
313
+ }
314
+ return loadCursorPngAsset(cursorPngPath, env) ?? macosFallbackCursorAsset;
315
+ }
219
316
  function resolveRuntimePacingDefaults(env = process.env) {
220
317
  return {
221
318
  renderCursorOverlay: boolEnv("STUDIOFLOW_RENDER_CURSOR", true, env),
319
+ cursorTheme: cursorThemeEnv(env),
222
320
  cursorMoveMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_MOVE_MS", 430, env)),
223
321
  cursorHighlightMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_HIGHLIGHT_MS", 170, env)),
224
322
  realisticTyping: boolEnv("STUDIOFLOW_REALISTIC_TYPING", true, env),
@@ -231,9 +329,132 @@ function resolveRuntimePacingDefaults(env = process.env) {
231
329
  pacingJitterEnabled: boolEnv("STUDIOFLOW_PACING_JITTER", true, env)
232
330
  };
233
331
  }
234
- var cursorOverlaySetupScript = (clickPulseMs) => `
332
+ var defaultScrollPlan = {
333
+ required: false,
334
+ alignment: "center",
335
+ maxAttempts: 2
336
+ };
337
+ async function collectTargetSnapshot(page, target) {
338
+ const locator = page.locator(target).first();
339
+ try {
340
+ if (await locator.count() === 0) {
341
+ return {
342
+ exists: false,
343
+ visible: false,
344
+ inViewport: false,
345
+ clippedByOverflow: false,
346
+ scrollableAncestors: 0
347
+ };
348
+ }
349
+ return locator.evaluate((element) => {
350
+ const rect = element.getBoundingClientRect();
351
+ const style = window.getComputedStyle(element);
352
+ const viewport = {
353
+ width: window.innerWidth,
354
+ height: window.innerHeight
355
+ };
356
+ const visible = style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0 && rect.width > 0 && rect.height > 0;
357
+ const inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < viewport.height && rect.left < viewport.width;
358
+ let clippedByOverflow = false;
359
+ let scrollableAncestors = 0;
360
+ let parent = element.parentElement;
361
+ while (parent) {
362
+ const parentStyle = window.getComputedStyle(parent);
363
+ const scrollableY = /(auto|scroll|overlay)/.test(parentStyle.overflowY) && parent.scrollHeight > parent.clientHeight + 1;
364
+ const scrollableX = /(auto|scroll|overlay)/.test(parentStyle.overflowX) && parent.scrollWidth > parent.clientWidth + 1;
365
+ if (scrollableY || scrollableX) {
366
+ scrollableAncestors += 1;
367
+ const parentRect = parent.getBoundingClientRect();
368
+ if (rect.top < parentRect.top || rect.bottom > parentRect.bottom || rect.left < parentRect.left || rect.right > parentRect.right) {
369
+ clippedByOverflow = true;
370
+ }
371
+ }
372
+ parent = parent.parentElement;
373
+ }
374
+ return {
375
+ exists: true,
376
+ visible,
377
+ inViewport,
378
+ clippedByOverflow,
379
+ scrollableAncestors,
380
+ rect: {
381
+ top: Math.round(rect.top),
382
+ left: Math.round(rect.left),
383
+ width: Math.round(rect.width),
384
+ height: Math.round(rect.height)
385
+ },
386
+ viewport
387
+ };
388
+ });
389
+ } catch {
390
+ return {
391
+ exists: false,
392
+ visible: false,
393
+ inViewport: false,
394
+ clippedByOverflow: false,
395
+ scrollableAncestors: 0
396
+ };
397
+ }
398
+ }
399
+ async function scrollTargetIntoView(page, target, timeoutMs, runtime) {
400
+ const locator = page.locator(target).first();
401
+ const snapshot = await collectTargetSnapshot(page, target);
402
+ if (!snapshot.exists) return;
403
+ const effective = defaultScrollPlan;
404
+ const needsScroll = snapshot.visible && (!snapshot.inViewport || snapshot.clippedByOverflow);
405
+ if (!effective.required && !needsScroll) return;
406
+ for (let attempt = 0; attempt < effective.maxAttempts; attempt += 1) {
407
+ try {
408
+ await locator.evaluate(
409
+ (element, alignment) => {
410
+ const block = alignment === "start" || alignment === "center" || alignment === "end" ? alignment : "nearest";
411
+ let parent = element.parentElement;
412
+ while (parent) {
413
+ const style = window.getComputedStyle(parent);
414
+ const scrollableY = /(auto|scroll|overlay)/.test(style.overflowY) && parent.scrollHeight > parent.clientHeight + 1;
415
+ const scrollableX = /(auto|scroll|overlay)/.test(style.overflowX) && parent.scrollWidth > parent.clientWidth + 1;
416
+ if (scrollableY || scrollableX) {
417
+ const targetRect = element.getBoundingClientRect();
418
+ const parentRect = parent.getBoundingClientRect();
419
+ if (scrollableY) {
420
+ const offsetTop = targetRect.top - parentRect.top + parent.scrollTop;
421
+ if (block === "start") {
422
+ parent.scrollTop = offsetTop - 8;
423
+ } else if (block === "end") {
424
+ parent.scrollTop = offsetTop - parent.clientHeight + targetRect.height + 8;
425
+ } else if (block === "center") {
426
+ parent.scrollTop = offsetTop - parent.clientHeight / 2 + targetRect.height / 2;
427
+ } else if (targetRect.top < parentRect.top || targetRect.bottom > parentRect.bottom) {
428
+ parent.scrollTop = offsetTop - parent.clientHeight / 2 + targetRect.height / 2;
429
+ }
430
+ }
431
+ if (scrollableX) {
432
+ const offsetLeft = targetRect.left - parentRect.left + parent.scrollLeft;
433
+ parent.scrollLeft = offsetLeft - parent.clientWidth / 2 + targetRect.width / 2;
434
+ }
435
+ }
436
+ parent = parent.parentElement;
437
+ }
438
+ element.scrollIntoView({ behavior: "instant", block, inline: "nearest" });
439
+ },
440
+ effective.alignment
441
+ );
442
+ } catch {
443
+ return;
444
+ }
445
+ const settleMs = Math.min(timeoutMs, Math.max(40, Math.round(runtime.stepPreDelayMs * 0.5)));
446
+ if (settleMs > 0) {
447
+ await wait(settleMs);
448
+ }
449
+ const after = await collectTargetSnapshot(page, target);
450
+ if (!after.exists) return;
451
+ if (after.visible && after.inViewport && !after.clippedByOverflow) return;
452
+ }
453
+ }
454
+ var cursorOverlaySetupScript = (clickPulseMs, theme, asset) => `
235
455
  (() => {
236
456
  const win = window;
457
+ const cursorAsset = "${asset.dataUri}";
237
458
  if (win.__studioflowCursor) return;
238
459
 
239
460
  if (!document.getElementById("studioflow-cursor-style")) {
@@ -244,32 +465,33 @@ var cursorOverlaySetupScript = (clickPulseMs) => `
244
465
  position: fixed;
245
466
  top: 0;
246
467
  left: 0;
247
- width: 22px;
248
- height: 30px;
249
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='30' viewBox='0 0 22 30'%3E%3Cpath d='M1.5 1.5V22.2L7.4 17.4L11 28.5L14.4 27L10.8 15.9H19.6L1.5 1.5Z' fill='white' stroke='%23000' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E");
468
+ width: ${asset.width}px;
469
+ height: ${asset.height}px;
470
+ background-image: url("\${cursorAsset}");
250
471
  background-repeat: no-repeat;
251
- background-size: 22px 30px;
472
+ background-size: ${asset.width}px ${asset.height}px;
252
473
  pointer-events: none;
253
474
  z-index: 2147483647;
254
- filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55));
255
- transform: translate3d(16px, 16px, 0);
256
- transition-property: transform;
257
- transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
475
+ opacity: 0;
476
+ transform: translate3d(-9999px, -9999px, 0);
477
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.48));
478
+ transition: opacity 120ms ease;
479
+ will-change: transform;
258
480
  }
259
481
  #studioflow-cursor::after {
260
482
  content: "";
261
483
  position: absolute;
262
- left: -8px;
263
- top: -8px;
264
- width: 20px;
265
- height: 20px;
484
+ left: -7px;
485
+ top: -7px;
486
+ width: 18px;
487
+ height: 18px;
266
488
  border-radius: 999px;
267
- border: 2px solid rgba(59, 130, 246, 0.72);
489
+ border: 2px solid rgba(37, 99, 235, 0.72);
268
490
  opacity: 0;
269
491
  transform: scale(0.65);
270
492
  }
271
493
  #studioflow-cursor.studioflow-cursor-pulse::after {
272
- animation: studioflow-cursor-pulse ${Math.max(0, clickPulseMs)}ms ease-out;
494
+ animation: studioflow-cursor-pulse var(--studioflow-click-pulse-ms, ${Math.max(0, clickPulseMs)}ms) ease-out;
273
495
  }
274
496
  @keyframes studioflow-cursor-pulse {
275
497
  0% { opacity: 0.9; transform: scale(0.55); }
@@ -282,20 +504,94 @@ var cursorOverlaySetupScript = (clickPulseMs) => `
282
504
  const cursor = document.createElement("div");
283
505
  cursor.id = "studioflow-cursor";
284
506
  cursor.setAttribute("aria-hidden", "true");
507
+ cursor.dataset.theme = "${theme}";
285
508
  document.body.appendChild(cursor);
286
509
 
287
- let x = 16;
288
- let y = 16;
289
- const setPosition = (nextX, nextY, durationMs) => {
510
+ const hotSpotX = ${asset.hotspotX};
511
+ const hotSpotY = ${asset.hotspotY};
512
+ let x = 0;
513
+ let y = 0;
514
+ let initialized = false;
515
+ let raf = 0;
516
+ let activeResolve = null;
517
+
518
+ const resolveActiveAnimation = () => {
519
+ if (activeResolve) {
520
+ const done = activeResolve;
521
+ activeResolve = null;
522
+ done();
523
+ }
524
+ };
525
+
526
+ const stopAnimation = () => {
527
+ if (raf) {
528
+ cancelAnimationFrame(raf);
529
+ raf = 0;
530
+ }
531
+ resolveActiveAnimation();
532
+ };
533
+
534
+ const setPosition = (nextX, nextY) => {
290
535
  x = nextX;
291
536
  y = nextY;
292
- cursor.style.transitionDuration = \`\${Math.max(0, durationMs)}ms\`;
293
- cursor.style.transform = \`translate3d(\${x}px, \${y}px, 0)\`;
537
+ cursor.style.transform = \`translate3d(\${Math.round(x - hotSpotX)}px, \${Math.round(y - hotSpotY)}px, 0)\`;
538
+ };
539
+
540
+ const easeInOut = (value) => {
541
+ if (value <= 0) return 0;
542
+ if (value >= 1) return 1;
543
+ return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
544
+ };
545
+
546
+ const animateTo = (nextX, nextY, durationMs) => {
547
+ const startX = x;
548
+ const startY = y;
549
+ const ms = Math.max(0, Math.round(durationMs));
550
+
551
+ if (ms === 0 || (Math.abs(startX - nextX) < 0.5 && Math.abs(startY - nextY) < 0.5)) {
552
+ setPosition(nextX, nextY);
553
+ return Promise.resolve();
554
+ }
555
+
556
+ return new Promise((resolve) => {
557
+ activeResolve = resolve;
558
+ const start = performance.now();
559
+ const tick = (now) => {
560
+ const progress = Math.min(1, (now - start) / ms);
561
+ const eased = easeInOut(progress);
562
+ const currentX = startX + (nextX - startX) * eased;
563
+ const currentY = startY + (nextY - startY) * eased;
564
+ setPosition(currentX, currentY);
565
+ if (progress >= 1) {
566
+ raf = 0;
567
+ resolveActiveAnimation();
568
+ return;
569
+ }
570
+ raf = requestAnimationFrame(tick);
571
+ };
572
+ raf = requestAnimationFrame(tick);
573
+ });
294
574
  };
295
575
 
296
576
  win.__studioflowCursor = {
297
577
  moveTo(nextX, nextY, durationMs) {
298
- setPosition(nextX, nextY, durationMs);
578
+ const targetX = Number(nextX) || 0;
579
+ const targetY = Number(nextY) || 0;
580
+ const transitionMs = Math.max(0, Number(durationMs) || 0);
581
+ stopAnimation();
582
+
583
+ if (!initialized) {
584
+ initialized = true;
585
+ cursor.style.opacity = "1";
586
+ setPosition(targetX - 14, targetY - 10);
587
+ return animateTo(targetX, targetY, Math.min(280, Math.max(120, transitionMs)));
588
+ }
589
+
590
+ return animateTo(targetX, targetY, transitionMs);
591
+ },
592
+ getPosition() {
593
+ if (!initialized) return null;
594
+ return { x, y };
299
595
  },
300
596
  clickPulse() {
301
597
  cursor.classList.remove("studioflow-cursor-pulse");
@@ -308,15 +604,32 @@ var cursorOverlaySetupScript = (clickPulseMs) => `
308
604
  var cursorMoveScript = (point, durationMs) => `
309
605
  (() => {
310
606
  const cursor = window.__studioflowCursor;
311
- if (cursor) cursor.moveTo(${point.x}, ${point.y}, ${durationMs});
607
+ if (cursor) return cursor.moveTo(${point.x}, ${point.y}, ${durationMs});
608
+ return undefined;
609
+ })();
610
+ `;
611
+ var cursorClickPulseScript = (pulseMs) => `
612
+ (() => {
613
+ const cursor = window.__studioflowCursor;
614
+ if (!cursor) return;
615
+ const pulse = Math.max(0, Math.round(${pulseMs}));
616
+ const cursorNode = document.getElementById("studioflow-cursor");
617
+ if (cursorNode) {
618
+ cursorNode.style.setProperty("--studioflow-click-pulse-ms", pulse + "ms");
619
+ }
620
+ cursor.clickPulse();
312
621
  })();
313
622
  `;
314
- var cursorClickPulseScript = `
623
+ var cursorPositionScript = `
315
624
  (() => {
316
625
  const cursor = window.__studioflowCursor;
317
- if (cursor) cursor.clickPulse();
626
+ if (!cursor) return null;
627
+ return cursor.getPosition();
318
628
  })();
319
629
  `;
630
+ var cursorExistsScript = `
631
+ (() => Boolean(window.__studioflowCursor))();
632
+ `;
320
633
  function hashString(input) {
321
634
  let hash = 2166136261;
322
635
  for (let i = 0; i < input.length; i += 1) {
@@ -350,7 +663,29 @@ function sanitizeScreenshotName(raw) {
350
663
  }
351
664
  async function ensureCursorOverlay(page, runtime) {
352
665
  if (!runtime.renderCursorOverlay) return;
353
- await page.evaluate(cursorOverlaySetupScript(runtime.clickPulseMs));
666
+ const cursorExists = await page.evaluate(cursorExistsScript);
667
+ if (cursorExists) return;
668
+ const cursorAsset = resolveCursorOverlayAsset(runtime.cursorTheme);
669
+ await page.evaluate(cursorOverlaySetupScript(runtime.clickPulseMs, runtime.cursorTheme, cursorAsset));
670
+ }
671
+ function distanceBetweenPoints(start, end) {
672
+ return Math.hypot(end.x - start.x, end.y - start.y);
673
+ }
674
+ function resolveCursorTravelDuration(baseMs, start, end) {
675
+ if (baseMs <= 0) return 0;
676
+ if (!start) {
677
+ return Math.max(120, Math.round(baseMs * 0.7));
678
+ }
679
+ const distanceMs = distanceBetweenPoints(start, end) * 1.1;
680
+ const blended = baseMs * 0.45 + distanceMs * 0.55;
681
+ const minMs = Math.max(110, Math.round(baseMs * 0.35));
682
+ const maxMs = Math.min(maxDelayMs, Math.max(850, Math.round(baseMs * 2.2)));
683
+ return Math.max(minMs, Math.min(maxMs, Math.round(blended)));
684
+ }
685
+ async function getCursorPosition(page, runtime) {
686
+ if (!runtime.renderCursorOverlay) return null;
687
+ await ensureCursorOverlay(page, runtime);
688
+ return page.evaluate(cursorPositionScript);
354
689
  }
355
690
  async function getTargetCenter(page, target) {
356
691
  const box = await page.locator(target).first().boundingBox();
@@ -362,23 +697,30 @@ async function moveCursor(page, point, durationMs, runtime) {
362
697
  if (runtime.renderCursorOverlay) {
363
698
  await ensureCursorOverlay(page, runtime);
364
699
  await page.evaluate(cursorMoveScript(point, durationMs));
700
+ return;
701
+ }
702
+ if (durationMs > 0) {
703
+ await wait(durationMs);
365
704
  }
366
705
  }
367
- async function clickPulse(page, runtime) {
706
+ async function clickPulse(page, runtime, pulseMs) {
368
707
  if (!runtime.renderCursorOverlay) return;
369
708
  await ensureCursorOverlay(page, runtime);
370
- await page.evaluate(cursorClickPulseScript);
709
+ await page.evaluate(cursorClickPulseScript(pulseMs));
371
710
  }
372
- async function applyPreStepPacing(page, step, context, runtime) {
711
+ async function applyPreStepPacing(page, step, timeout, context, runtime) {
373
712
  const preDelay = resolvePacedDelay(step.preDelayMs ?? runtime.stepPreDelayMs, context, runtime, "pre");
374
713
  if (preDelay > 0) {
375
714
  await wait(preDelay);
376
715
  }
377
716
  const shouldMoveCursor = ["click", "type"].includes(step.action) && Boolean(step.target);
378
717
  if (!shouldMoveCursor || !step.target) return;
718
+ await scrollTargetIntoView(page, step.target, timeout, runtime);
379
719
  const center = await getTargetCenter(page, step.target);
380
720
  if (!center) return;
381
- const moveMs = resolvePacedDelay(step.mouseMoveMs ?? runtime.cursorMoveMs, context, runtime, "move");
721
+ const baseMoveMs = resolvePacedDelay(step.mouseMoveMs ?? runtime.cursorMoveMs, context, runtime, "move");
722
+ const currentCursorPosition = await getCursorPosition(page, runtime);
723
+ const moveMs = resolveCursorTravelDuration(baseMoveMs, currentCursorPosition, center);
382
724
  if (moveMs > 0) {
383
725
  await moveCursor(page, center, moveMs, runtime);
384
726
  }
@@ -400,7 +742,7 @@ async function applyPostStepPacing(step, context, runtime) {
400
742
  async function executeStep(page, step, runDir, baseUrl, context = {}) {
401
743
  const timeout = step.timeoutMs ?? 6e3;
402
744
  const runtime = resolveRuntimePacingDefaults();
403
- await applyPreStepPacing(page, step, context, runtime);
745
+ await applyPreStepPacing(page, step, timeout, context, runtime);
404
746
  if (step.action === "goto") {
405
747
  const target = step.value ?? "/";
406
748
  const isAbsolute = /^https?:\/\//.test(target);
@@ -410,17 +752,21 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
410
752
  }
411
753
  if (step.action === "click") {
412
754
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
755
+ await scrollTargetIntoView(page, step.target, timeout, runtime);
413
756
  await page.locator(step.target).first().click({ timeout });
414
- await clickPulse(page, runtime);
757
+ const pulseMs = resolvePacedDelay(runtime.clickPulseMs, context, runtime, "click", false);
758
+ await clickPulse(page, runtime, pulseMs);
415
759
  await applyPostStepPacing(step, context, runtime);
416
760
  return;
417
761
  }
418
762
  if (step.action === "type") {
419
763
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
764
+ await scrollTargetIntoView(page, step.target, timeout, runtime);
420
765
  const locator = page.locator(step.target).first();
421
766
  if (runtime.realisticTyping) {
422
767
  await locator.click({ timeout });
423
- await clickPulse(page, runtime);
768
+ const pulseMs = resolvePacedDelay(runtime.clickPulseMs, context, runtime, "click", false);
769
+ await clickPulse(page, runtime, pulseMs);
424
770
  await locator.fill("", { timeout });
425
771
  const effectiveTypingDelay = resolvePacedDelay(runtime.typingDelayMs, context, runtime, "typing", false);
426
772
  await page.keyboard.type(step.value ?? "", { delay: effectiveTypingDelay });
@@ -432,6 +778,7 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
432
778
  }
433
779
  if (step.action === "wait_for") {
434
780
  if (step.target) {
781
+ await scrollTargetIntoView(page, step.target, timeout, runtime);
435
782
  await page.locator(step.target).first().waitFor({ state: "visible", timeout });
436
783
  await applyPostStepPacing(step, context, runtime);
437
784
  return;
@@ -451,6 +798,7 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
451
798
  }
452
799
  if (step.action === "assert_visible") {
453
800
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
801
+ await scrollTargetIntoView(page, step.target, timeout, runtime);
454
802
  await assertVisible(page, step.target, timeout);
455
803
  await applyPostStepPacing(step, context, runtime);
456
804
  return;
@@ -470,10 +818,10 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
470
818
  }
471
819
 
472
820
  // ../../packages/flow-registry/src/index.ts
473
- import fs2 from "node:fs/promises";
821
+ import fs3 from "node:fs/promises";
474
822
  import os from "node:os";
475
823
  import path2 from "node:path";
476
- import { fileURLToPath } from "node:url";
824
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
477
825
  import YAML from "yaml";
478
826
 
479
827
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -4635,10 +4983,20 @@ var runArtifactIndexSchema = external_exports.object({
4635
4983
  });
4636
4984
 
4637
4985
  // ../../packages/flow-registry/src/index.ts
4638
- var __filename = fileURLToPath(import.meta.url);
4986
+ var __filename = fileURLToPath2(import.meta.url);
4639
4987
  var __dirname = path2.dirname(__filename);
4640
4988
  var rootDir = path2.resolve(__dirname, "..");
4641
- var builtInFlowsDir = path2.join(rootDir, "flows");
4989
+ function builtInFlowsDirs() {
4990
+ const configured = process.env.STUDIOFLOW_FLOWS_SOURCE?.trim();
4991
+ const candidates = [
4992
+ configured ? path2.resolve(configured) : "",
4993
+ path2.join(rootDir, "bundled", "flows"),
4994
+ path2.join(rootDir, "flows"),
4995
+ path2.resolve(rootDir, "../../packages/flow-registry/flows"),
4996
+ path2.resolve(process.cwd(), "packages/flow-registry/flows")
4997
+ ].filter((value) => Boolean(value));
4998
+ return Array.from(new Set(candidates));
4999
+ }
4642
5000
  function dataRoot() {
4643
5001
  const configured = process.env.STUDIOFLOW_DATA_DIR ?? process.env.STUDIOFLOW_HOME;
4644
5002
  if (configured && configured.trim()) {
@@ -4651,7 +5009,7 @@ function userFlowsDir() {
4651
5009
  }
4652
5010
  async function listFlowFiles(dir) {
4653
5011
  try {
4654
- const files = await fs2.readdir(dir);
5012
+ const files = await fs3.readdir(dir);
4655
5013
  return files.filter((f) => [".yaml", ".yml", ".json"].includes(path2.extname(f).toLowerCase()));
4656
5014
  } catch (error) {
4657
5015
  const code = error.code;
@@ -4668,12 +5026,12 @@ async function loadFlowFromFile(filePath) {
4668
5026
  if (![".yaml", ".yml", ".json"].includes(ext)) {
4669
5027
  throw new Error(`Unsupported flow file format: ${filePath}`);
4670
5028
  }
4671
- const raw = await fs2.readFile(filePath, "utf8");
5029
+ const raw = await fs3.readFile(filePath, "utf8");
4672
5030
  return parseFlow(raw, ext);
4673
5031
  }
4674
5032
  async function loadFlows() {
4675
5033
  const merged = /* @__PURE__ */ new Map();
4676
- const dirs = [builtInFlowsDir, userFlowsDir()];
5034
+ const dirs = [...builtInFlowsDirs(), userFlowsDir()];
4677
5035
  for (const dir of dirs) {
4678
5036
  const files = await listFlowFiles(dir);
4679
5037
  for (const file of files) {
@@ -4686,7 +5044,7 @@ async function loadFlows() {
4686
5044
  }
4687
5045
 
4688
5046
  // ../../packages/orchestrator/src/engine.ts
4689
- import fs6 from "node:fs/promises";
5047
+ import fs7 from "node:fs/promises";
4690
5048
  import path5 from "node:path";
4691
5049
 
4692
5050
  // ../../packages/artifacts/src/paths.ts
@@ -4710,23 +5068,23 @@ function getRunDir(runId) {
4710
5068
  }
4711
5069
 
4712
5070
  // ../../packages/artifacts/src/logger.ts
4713
- import fs3 from "node:fs/promises";
5071
+ import fs4 from "node:fs/promises";
4714
5072
  async function appendJsonLine(filePath, payload) {
4715
- await fs3.appendFile(filePath, `${JSON.stringify(payload)}
5073
+ await fs4.appendFile(filePath, `${JSON.stringify(payload)}
4716
5074
  `, "utf8");
4717
5075
  }
4718
5076
 
4719
5077
  // ../../packages/artifacts/src/writer.ts
4720
- import fs4 from "node:fs/promises";
5078
+ import fs5 from "node:fs/promises";
4721
5079
  import path4 from "node:path";
4722
5080
  async function createRunContext() {
4723
5081
  const now = /* @__PURE__ */ new Date();
4724
5082
  const runId = now.toISOString().replace(/[:.]/g, "-");
4725
5083
  const runDir = getRunDir(runId);
4726
5084
  const screenshotsDir = path4.join(runDir, "screenshots");
4727
- await fs4.mkdir(screenshotsDir, { recursive: true });
5085
+ await fs5.mkdir(screenshotsDir, { recursive: true });
4728
5086
  const eventsFile = path4.join(runDir, "events.jsonl");
4729
- await fs4.writeFile(eventsFile, "", "utf8");
5087
+ await fs5.writeFile(eventsFile, "", "utf8");
4730
5088
  return {
4731
5089
  runId,
4732
5090
  runDir,
@@ -4735,7 +5093,7 @@ async function createRunContext() {
4735
5093
  };
4736
5094
  }
4737
5095
  async function writeJsonFile(filePath, payload) {
4738
- await fs4.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
5096
+ await fs5.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
4739
5097
  }
4740
5098
 
4741
5099
  // ../../packages/adapters-desktop/src/osascript.ts
@@ -4749,14 +5107,14 @@ async function runAppleScript(script) {
4749
5107
 
4750
5108
  // ../../packages/adapters-desktop/src/permissions.ts
4751
5109
  import { execFile as execFile2 } from "node:child_process";
4752
- import fs5 from "node:fs/promises";
5110
+ import fs6 from "node:fs/promises";
4753
5111
  import { promisify as promisify2 } from "node:util";
4754
5112
  var execFileAsync2 = promisify2(execFile2);
4755
5113
  async function checkPermissions() {
4756
5114
  const notes = [];
4757
5115
  let screenStudioInstalled = false;
4758
5116
  try {
4759
- await fs5.access("/Applications/Screen Studio.app");
5117
+ await fs6.access("/Applications/Screen Studio.app");
4760
5118
  screenStudioInstalled = true;
4761
5119
  } catch {
4762
5120
  notes.push("Screen Studio app not found at /Applications/Screen Studio.app");
@@ -4833,27 +5191,46 @@ async function ensureAutomationPermissions() {
4833
5191
  );
4834
5192
  }
4835
5193
 
4836
- // ../../packages/adapters-screenstudio/src/menu-controls.ts
5194
+ // ../../packages/adapters-desktop/src/quicktime.ts
5195
+ var defaultQuickTimeAppName = process.env.QUICKTIME_APP_NAME ?? "QuickTime Player";
5196
+ function int(value, fallback) {
5197
+ const parsed = Number(value);
5198
+ return Number.isFinite(parsed) ? parsed : fallback;
5199
+ }
5200
+ var preConfirmDelay = int(process.env.QUICKTIME_PRE_CONFIRM_DELAY_MS, 700);
5201
+ var postStartDelay = int(process.env.QUICKTIME_POST_START_DELAY_MS, 1200);
5202
+ var postStopDelay = int(process.env.QUICKTIME_POST_STOP_DELAY_MS, 900);
5203
+ var exportDialogConfirmDelay = int(process.env.QUICKTIME_EXPORT_DIALOG_CONFIRM_DELAY_MS, 900);
5204
+ var exportDelay = int(process.env.QUICKTIME_EXPORT_DELAY_MS, 2500);
4837
5205
  function quote(value) {
4838
5206
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4839
5207
  }
4840
5208
  function normalizeMenuItem(raw) {
4841
5209
  return raw.trim();
4842
5210
  }
4843
- function parseMenuItems(stdout) {
5211
+ function parseQuickTimeMenuItems(stdout) {
4844
5212
  if (!stdout.trim()) {
4845
5213
  return [];
4846
5214
  }
4847
5215
  return stdout.split(",").map(normalizeMenuItem).filter((item) => item.length > 0 && item !== "missing value");
4848
5216
  }
4849
- function buildActivateScreenStudioScript(appName2) {
5217
+ function buildActivateQuickTimeScript(appName2) {
4850
5218
  return `tell application "${quote(appName2)}" to activate`;
4851
5219
  }
4852
- function buildListRecordMenuItemsScript(appName2) {
4853
- return `tell application "System Events" to tell process "${quote(appName2)}" to get name of every menu item of menu "Record" of menu bar 1`;
5220
+ function buildListQuickTimeFileMenuItemsScript(appName2) {
5221
+ return `tell application "System Events" to tell process "${quote(appName2)}" to get name of every menu item of menu "File" of menu bar 1`;
4854
5222
  }
4855
- function buildClickRecordMenuItemScript(appName2, itemName) {
4856
- return `tell application "System Events" to tell process "${quote(appName2)}" to click menu item "${quote(itemName)}" of menu "Record" of menu bar 1`;
5223
+ function buildClickQuickTimeFileMenuItemScript(appName2, itemName) {
5224
+ return `tell application "System Events" to tell process "${quote(appName2)}" to click menu item "${quote(itemName)}" of menu "File" of menu bar 1`;
5225
+ }
5226
+ function buildQuickTimeStartShortcutScript() {
5227
+ return 'tell application "System Events" to keystroke "n" using {command down, control down}';
5228
+ }
5229
+ function buildQuickTimeStopShortcutScript() {
5230
+ return 'tell application "System Events" to key code 53 using {command down, control down}';
5231
+ }
5232
+ function buildQuickTimeSaveShortcutScript() {
5233
+ return 'tell application "System Events" to keystroke "s" using {command down}';
4857
5234
  }
4858
5235
  function buildPressReturnScript() {
4859
5236
  return 'tell application "System Events" to key code 36';
@@ -4861,6 +5238,105 @@ function buildPressReturnScript() {
4861
5238
  async function wait2(ms) {
4862
5239
  await new Promise((resolve) => setTimeout(resolve, ms));
4863
5240
  }
5241
+ function findMenuItem(items, partial) {
5242
+ const needle = partial.toLowerCase();
5243
+ return items.find((item) => item.toLowerCase().includes(needle));
5244
+ }
5245
+ async function activateQuickTime(appName2 = defaultQuickTimeAppName) {
5246
+ await runAppleScript(buildActivateQuickTimeScript(appName2));
5247
+ }
5248
+ async function listQuickTimeFileMenuItems(appName2 = defaultQuickTimeAppName) {
5249
+ const { stdout } = await runAppleScript(buildListQuickTimeFileMenuItemsScript(appName2));
5250
+ return parseQuickTimeMenuItems(stdout);
5251
+ }
5252
+ async function clickQuickTimeFileMenuItem(appName2, itemName) {
5253
+ const items = await listQuickTimeFileMenuItems(appName2);
5254
+ if (!items.includes(itemName)) {
5255
+ const available = items.length > 0 ? items.join(", ") : "none";
5256
+ throw new Error(
5257
+ `QuickTime File menu item "${itemName}" not available. Available items: ${available}`
5258
+ );
5259
+ }
5260
+ await runAppleScript(buildClickQuickTimeFileMenuItemScript(appName2, itemName));
5261
+ }
5262
+ async function pressQuickTimeStartShortcut() {
5263
+ await runAppleScript(buildQuickTimeStartShortcutScript());
5264
+ }
5265
+ async function pressQuickTimeStopShortcut() {
5266
+ await runAppleScript(buildQuickTimeStopShortcutScript());
5267
+ }
5268
+ async function pressQuickTimeSaveShortcut() {
5269
+ await runAppleScript(buildQuickTimeSaveShortcutScript());
5270
+ }
5271
+ async function pressReturn() {
5272
+ await runAppleScript(buildPressReturnScript());
5273
+ }
5274
+ async function startQuickTimeRecording(appName2 = defaultQuickTimeAppName) {
5275
+ await activateQuickTime(appName2);
5276
+ const items = await listQuickTimeFileMenuItems(appName2);
5277
+ const newScreenRecordingItem = findMenuItem(items, "new screen recording");
5278
+ if (newScreenRecordingItem) {
5279
+ await clickQuickTimeFileMenuItem(appName2, newScreenRecordingItem);
5280
+ } else {
5281
+ await pressQuickTimeStartShortcut();
5282
+ }
5283
+ await wait2(preConfirmDelay);
5284
+ await pressReturn();
5285
+ await wait2(postStartDelay);
5286
+ }
5287
+ async function stopQuickTimeRecording(appName2 = defaultQuickTimeAppName) {
5288
+ await activateQuickTime(appName2);
5289
+ const items = await listQuickTimeFileMenuItems(appName2);
5290
+ const stopScreenRecordingItem = findMenuItem(items, "stop screen recording");
5291
+ if (stopScreenRecordingItem) {
5292
+ await clickQuickTimeFileMenuItem(appName2, stopScreenRecordingItem);
5293
+ } else {
5294
+ await pressQuickTimeStopShortcut();
5295
+ }
5296
+ await wait2(postStopDelay);
5297
+ }
5298
+ async function exportQuickTimeRecording(appName2 = defaultQuickTimeAppName) {
5299
+ await activateQuickTime(appName2);
5300
+ const items = await listQuickTimeFileMenuItems(appName2);
5301
+ const saveItem = findMenuItem(items, "save");
5302
+ if (saveItem) {
5303
+ await clickQuickTimeFileMenuItem(appName2, saveItem);
5304
+ } else {
5305
+ await pressQuickTimeSaveShortcut();
5306
+ }
5307
+ await wait2(exportDialogConfirmDelay);
5308
+ await pressReturn();
5309
+ await wait2(exportDelay);
5310
+ }
5311
+
5312
+ // ../../packages/adapters-screenstudio/src/menu-controls.ts
5313
+ function quote2(value) {
5314
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5315
+ }
5316
+ function normalizeMenuItem2(raw) {
5317
+ return raw.trim();
5318
+ }
5319
+ function parseMenuItems(stdout) {
5320
+ if (!stdout.trim()) {
5321
+ return [];
5322
+ }
5323
+ return stdout.split(",").map(normalizeMenuItem2).filter((item) => item.length > 0 && item !== "missing value");
5324
+ }
5325
+ function buildActivateScreenStudioScript(appName2) {
5326
+ return `tell application "${quote2(appName2)}" to activate`;
5327
+ }
5328
+ function buildListRecordMenuItemsScript(appName2) {
5329
+ return `tell application "System Events" to tell process "${quote2(appName2)}" to get name of every menu item of menu "Record" of menu bar 1`;
5330
+ }
5331
+ function buildClickRecordMenuItemScript(appName2, itemName) {
5332
+ return `tell application "System Events" to tell process "${quote2(appName2)}" to click menu item "${quote2(itemName)}" of menu "Record" of menu bar 1`;
5333
+ }
5334
+ function buildPressReturnScript2() {
5335
+ return 'tell application "System Events" to key code 36';
5336
+ }
5337
+ async function wait3(ms) {
5338
+ await new Promise((resolve) => setTimeout(resolve, ms));
5339
+ }
4864
5340
  async function activateScreenStudio(appName2) {
4865
5341
  await runAppleScript(buildActivateScreenStudioScript(appName2));
4866
5342
  }
@@ -4878,39 +5354,39 @@ async function clickRecordMenuItem(appName2, itemName) {
4878
5354
  }
4879
5355
  await runAppleScript(buildClickRecordMenuItemScript(appName2, itemName));
4880
5356
  }
4881
- async function pressReturn() {
4882
- await runAppleScript(buildPressReturnScript());
5357
+ async function pressReturn2() {
5358
+ await runAppleScript(buildPressReturnScript2());
4883
5359
  }
4884
5360
 
4885
5361
  // ../../packages/adapters-screenstudio/src/recorder.ts
4886
5362
  var appName = process.env.SCREENSTUDIO_APP_NAME ?? "Screen Studio";
4887
- function int(value, fallback) {
5363
+ function int2(value, fallback) {
4888
5364
  const parsed = Number(value);
4889
5365
  return Number.isFinite(parsed) ? parsed : fallback;
4890
5366
  }
4891
- var preConfirmDelay = int(process.env.SCREENSTUDIO_PRE_CONFIRM_DELAY_MS, 800);
4892
- var postStartDelay = int(process.env.SCREENSTUDIO_POST_START_DELAY_MS, 1200);
4893
- var postStopDelay = int(process.env.SCREENSTUDIO_POST_STOP_DELAY_MS, 800);
4894
- var exportDialogConfirmDelay = int(process.env.SCREENSTUDIO_EXPORT_DIALOG_CONFIRM_DELAY_MS, 800);
4895
- var exportDelay = int(process.env.SCREENSTUDIO_EXPORT_DELAY_MS, 2500);
5367
+ var preConfirmDelay2 = int2(process.env.SCREENSTUDIO_PRE_CONFIRM_DELAY_MS, 800);
5368
+ var postStartDelay2 = int2(process.env.SCREENSTUDIO_POST_START_DELAY_MS, 1200);
5369
+ var postStopDelay2 = int2(process.env.SCREENSTUDIO_POST_STOP_DELAY_MS, 800);
5370
+ var exportDialogConfirmDelay2 = int2(process.env.SCREENSTUDIO_EXPORT_DIALOG_CONFIRM_DELAY_MS, 800);
5371
+ var exportDelay2 = int2(process.env.SCREENSTUDIO_EXPORT_DELAY_MS, 2500);
4896
5372
  async function startRecording() {
4897
5373
  await activateScreenStudio(appName);
4898
5374
  await clickRecordMenuItem(appName, "Record display");
4899
- await wait2(preConfirmDelay);
4900
- await pressReturn();
4901
- await wait2(postStartDelay);
5375
+ await wait3(preConfirmDelay2);
5376
+ await pressReturn2();
5377
+ await wait3(postStartDelay2);
4902
5378
  }
4903
5379
  async function stopRecording() {
4904
5380
  await activateScreenStudio(appName);
4905
5381
  await clickRecordMenuItem(appName, "Stop recording");
4906
- await wait2(postStopDelay);
5382
+ await wait3(postStopDelay2);
4907
5383
  }
4908
5384
  async function exportRecording() {
4909
5385
  await activateScreenStudio(appName);
4910
5386
  await clickRecordMenuItem(appName, "Export and save to file");
4911
- await wait2(exportDialogConfirmDelay);
4912
- await pressReturn();
4913
- await wait2(exportDelay);
5387
+ await wait3(exportDialogConfirmDelay2);
5388
+ await pressReturn2();
5389
+ await wait3(exportDelay2);
4914
5390
  }
4915
5391
 
4916
5392
  // ../../packages/orchestrator/src/retry-policy.ts
@@ -4932,9 +5408,31 @@ async function withRetry(fn, opts = {}) {
4932
5408
  }
4933
5409
 
4934
5410
  // ../../packages/orchestrator/src/engine.ts
5411
+ async function startRecorder(recorder) {
5412
+ if (recorder === "screenstudio") {
5413
+ await startRecording();
5414
+ return;
5415
+ }
5416
+ await startQuickTimeRecording();
5417
+ }
5418
+ async function stopRecorder(recorder) {
5419
+ if (recorder === "screenstudio") {
5420
+ await stopRecording();
5421
+ return;
5422
+ }
5423
+ await stopQuickTimeRecording();
5424
+ }
5425
+ async function exportRecorder(recorder) {
5426
+ if (recorder === "screenstudio") {
5427
+ await exportRecording();
5428
+ return;
5429
+ }
5430
+ await exportQuickTimeRecording();
5431
+ }
4935
5432
  async function runEngine(input) {
4936
5433
  const run2 = await createRunContext();
4937
5434
  let state = "INIT";
5435
+ const recorder = input.recorder ?? "screenstudio";
4938
5436
  const shouldExport = input.flows.some((flow) => flow.steps.some((step) => step.action === "recorder_export"));
4939
5437
  const files = {
4940
5438
  events: run2.eventsFile
@@ -4956,7 +5454,7 @@ async function runEngine(input) {
4956
5454
  await emit("start_app.done");
4957
5455
  state = "START_RECORDER";
4958
5456
  await emit("recorder.start.begin");
4959
- await startRecording();
5457
+ await startRecorder(recorder);
4960
5458
  await page.bringToFront();
4961
5459
  await emit("recorder.start.done");
4962
5460
  state = "RUN_FLOW";
@@ -4983,12 +5481,12 @@ async function runEngine(input) {
4983
5481
  }
4984
5482
  state = "STOP_RECORDER";
4985
5483
  await emit("recorder.stop.begin");
4986
- await stopRecording();
5484
+ await stopRecorder(recorder);
4987
5485
  await emit("recorder.stop.done");
4988
5486
  if (shouldExport) {
4989
5487
  state = "EXPORT";
4990
5488
  await emit("recorder.export.begin");
4991
- await exportRecording();
5489
+ await exportRecorder(recorder);
4992
5490
  await emit("recorder.export.done");
4993
5491
  }
4994
5492
  state = "VERIFY_ARTIFACTS";
@@ -5049,12 +5547,12 @@ async function runEngine(input) {
5049
5547
  if (closeApp) {
5050
5548
  await closeApp();
5051
5549
  }
5052
- await fs6.mkdir(run2.runDir, { recursive: true });
5550
+ await fs7.mkdir(run2.runDir, { recursive: true });
5053
5551
  }
5054
5552
  }
5055
5553
 
5056
5554
  // src/commands/config.ts
5057
- import fs7 from "node:fs/promises";
5555
+ import fs8 from "node:fs/promises";
5058
5556
  import path8 from "node:path";
5059
5557
 
5060
5558
  // src/commands/path-utils.ts
@@ -5109,6 +5607,13 @@ function optionalBoolean(value, key, label) {
5109
5607
  }
5110
5608
  return value;
5111
5609
  }
5610
+ function optionalRecorder(value, key, label) {
5611
+ if (value === void 0) return void 0;
5612
+ if (value === "quicktime" || value === "screenstudio") {
5613
+ return value;
5614
+ }
5615
+ throw new Error(`${label} field "${key}" must be "quicktime" or "screenstudio".`);
5616
+ }
5112
5617
  function parseRuntimeConfigFile(value, label) {
5113
5618
  const obj = ensureObject(value, label);
5114
5619
  return {
@@ -5116,13 +5621,14 @@ function parseRuntimeConfigFile(value, label) {
5116
5621
  startCommand: optionalString(obj.startCommand, "startCommand", label),
5117
5622
  healthPath: optionalString(obj.healthPath, "healthPath", label),
5118
5623
  headless: optionalBoolean(obj.headless, "headless", label),
5624
+ recorder: optionalRecorder(obj.recorder, "recorder", label),
5119
5625
  bootstrapReport: optionalString(obj.bootstrapReport, "bootstrapReport", label),
5120
5626
  runsDir: optionalString(obj.runsDir, "runsDir", label)
5121
5627
  };
5122
5628
  }
5123
5629
  async function readConfigFile(filePath, label) {
5124
5630
  try {
5125
- const raw = await fs7.readFile(filePath, "utf8");
5631
+ const raw = await fs8.readFile(filePath, "utf8");
5126
5632
  const parsed = JSON.parse(raw);
5127
5633
  return {
5128
5634
  exists: true,
@@ -5179,7 +5685,7 @@ async function resolveRuntimeConfig(overrides = {}) {
5179
5685
  const bootstrapReportPath = resolveFromWorkspace(bootstrapPathField.value);
5180
5686
  let bootstrap = null;
5181
5687
  try {
5182
- const raw = await fs7.readFile(bootstrapReportPath, "utf8");
5688
+ const raw = await fs8.readFile(bootstrapReportPath, "utf8");
5183
5689
  const parsed = bootstrapReportSchema.parse(JSON.parse(raw));
5184
5690
  bootstrap = { startCommand: parsed.startCommand, healthPath: parsed.healthPath };
5185
5691
  } catch (error) {
@@ -5219,6 +5725,14 @@ async function resolveRuntimeConfig(overrides = {}) {
5219
5725
  ],
5220
5726
  false
5221
5727
  );
5728
+ const recorder = chooseString(
5729
+ [
5730
+ { value: overrides.recorder, source: "flag" },
5731
+ { value: project.config.recorder, source: "project-config" },
5732
+ { value: user.config.recorder, source: "user-config" }
5733
+ ],
5734
+ "quicktime"
5735
+ );
5222
5736
  const runsDir = chooseString(
5223
5737
  [
5224
5738
  { value: overrides.runsDir, source: "flag" },
@@ -5234,6 +5748,7 @@ async function resolveRuntimeConfig(overrides = {}) {
5234
5748
  startCommand: startCommand.value,
5235
5749
  healthPath: healthPath.value,
5236
5750
  headless: headless.value,
5751
+ recorder: recorder.value,
5237
5752
  runsDir: runsDir.value
5238
5753
  },
5239
5754
  sources: {
@@ -5241,6 +5756,7 @@ async function resolveRuntimeConfig(overrides = {}) {
5241
5756
  startCommand: startCommand.source,
5242
5757
  healthPath: healthPath.source,
5243
5758
  headless: headless.source,
5759
+ recorder: recorder.source,
5244
5760
  runsDir: runsDir.source
5245
5761
  },
5246
5762
  files: {
@@ -5266,6 +5782,7 @@ async function configShowCommand(opts = {}) {
5266
5782
  );
5267
5783
  console.log(`- healthPath: ${resolved.values.healthPath} (${resolved.sources.healthPath})`);
5268
5784
  console.log(`- headless: ${String(resolved.values.headless)} (${resolved.sources.headless})`);
5785
+ console.log(`- recorder: ${resolved.values.recorder} (${resolved.sources.recorder})`);
5269
5786
  console.log(`- runsDir: ${resolved.values.runsDir} (${resolved.sources.runsDir})`);
5270
5787
  console.log(`- project config: ${resolved.files.projectConfigPath} (${resolved.files.projectConfigExists ? "found" : "missing"})`);
5271
5788
  console.log(`- user config: ${resolved.files.userConfigPath} (${resolved.files.userConfigExists ? "found" : "missing"})`);
@@ -5303,6 +5820,7 @@ async function configCheckCommand(opts = {}) {
5303
5820
  console.log(`- baseUrl: ${resolved.values.baseUrl}`);
5304
5821
  console.log(`- healthPath: ${resolved.values.healthPath}`);
5305
5822
  console.log(`- headless: ${String(resolved.values.headless)}`);
5823
+ console.log(`- recorder: ${resolved.values.recorder}`);
5306
5824
  console.log(`- runsDir: ${resolved.values.runsDir}`);
5307
5825
  if (warnings.length > 0) {
5308
5826
  console.log(kleur_default.yellow("Warnings:"));
@@ -5392,7 +5910,52 @@ async function screenstudioPrepCommand(appName2 = defaultScreenStudioAppName) {
5392
5910
  await runScreenStudioPreflight({ appName: appName2, ensurePermissions: true, quiet: false });
5393
5911
  }
5394
5912
 
5913
+ // src/commands/quicktime-prep.ts
5914
+ var defaultQuickTimeAppName2 = process.env.QUICKTIME_APP_NAME ?? "QuickTime Player";
5915
+ function hasScreenRecordingEntry(items) {
5916
+ return items.some((item) => item.toLowerCase().includes("new screen recording"));
5917
+ }
5918
+ async function runQuickTimePreflight(opts = {}) {
5919
+ const appName2 = opts.appName ?? defaultQuickTimeAppName2;
5920
+ const ensurePermissions = opts.ensurePermissions ?? true;
5921
+ if (ensurePermissions) {
5922
+ await ensureAutomationPermissions();
5923
+ }
5924
+ await activateQuickTime(appName2);
5925
+ const items = await listQuickTimeFileMenuItems(appName2);
5926
+ if (items.length === 0) {
5927
+ throw new Error(
5928
+ `QuickTime File menu is empty for app "${appName2}". Ensure QuickTime Player is running and menu automation is allowed.`
5929
+ );
5930
+ }
5931
+ if (!hasScreenRecordingEntry(items)) {
5932
+ throw new Error(
5933
+ `QuickTime File menu does not expose "New Screen Recording" for "${appName2}". Found: ${items.join(", ")}`
5934
+ );
5935
+ }
5936
+ const result = {
5937
+ appName: appName2,
5938
+ fileMenuItems: items
5939
+ };
5940
+ if (!opts.quiet) {
5941
+ console.log(kleur_default.green("QuickTime prep passed."));
5942
+ console.log(`- App: ${appName2}`);
5943
+ console.log(`- File menu items: ${items.join(", ")}`);
5944
+ }
5945
+ return result;
5946
+ }
5947
+ async function quicktimePrepCommand(appName2 = defaultQuickTimeAppName2) {
5948
+ await runQuickTimePreflight({ appName: appName2, ensurePermissions: true, quiet: false });
5949
+ }
5950
+
5395
5951
  // src/commands/run.ts
5952
+ var explicitExportIntentMatchers = [
5953
+ /\bexport(?:ed|ing)?\b/i,
5954
+ /\bdownload(?:ed|ing)?\b/i,
5955
+ /\bsave(?:\s+(?:the|to|as|a|an))*\s+(?:video|recording|file)\b/i,
5956
+ /\bshareable\s+link\b/i,
5957
+ /\bcopy\s+to\s+clipboard\b/i
5958
+ ];
5396
5959
  function isWhitespace(char) {
5397
5960
  return /\s/.test(char);
5398
5961
  }
@@ -5400,7 +5963,7 @@ function parseStartCommand(raw) {
5400
5963
  const tokens = [];
5401
5964
  let current = "";
5402
5965
  let tokenStarted = false;
5403
- let quote2 = null;
5966
+ let quote3 = null;
5404
5967
  let escaped = false;
5405
5968
  for (let index = 0; index < raw.length; index += 1) {
5406
5969
  const char = raw[index];
@@ -5410,14 +5973,14 @@ function parseStartCommand(raw) {
5410
5973
  escaped = false;
5411
5974
  continue;
5412
5975
  }
5413
- if (char === "\\" && quote2 !== "'") {
5976
+ if (char === "\\" && quote3 !== "'") {
5414
5977
  escaped = true;
5415
5978
  tokenStarted = true;
5416
5979
  continue;
5417
5980
  }
5418
- if (quote2) {
5419
- if (char === quote2) {
5420
- quote2 = null;
5981
+ if (quote3) {
5982
+ if (char === quote3) {
5983
+ quote3 = null;
5421
5984
  } else {
5422
5985
  current += char;
5423
5986
  }
@@ -5425,7 +5988,7 @@ function parseStartCommand(raw) {
5425
5988
  continue;
5426
5989
  }
5427
5990
  if (char === '"' || char === "'") {
5428
- quote2 = char;
5991
+ quote3 = char;
5429
5992
  tokenStarted = true;
5430
5993
  continue;
5431
5994
  }
@@ -5443,7 +6006,7 @@ function parseStartCommand(raw) {
5443
6006
  if (escaped) {
5444
6007
  throw new Error("Start command ends with an escape character.");
5445
6008
  }
5446
- if (quote2) {
6009
+ if (quote3) {
5447
6010
  throw new Error("Start command contains an unterminated quote.");
5448
6011
  }
5449
6012
  if (tokenStarted) {
@@ -5455,6 +6018,32 @@ function parseStartCommand(raw) {
5455
6018
  const [command, ...args] = tokens;
5456
6019
  return { command, args };
5457
6020
  }
6021
+ function flowRequestsExport(flows) {
6022
+ return flows.some((flow) => flow.steps.some((step) => step.action === "recorder_export"));
6023
+ }
6024
+ function parseBooleanFromEnv(name, env = process.env) {
6025
+ const raw = env[name];
6026
+ if (!raw) return void 0;
6027
+ const normalized = raw.trim().toLowerCase();
6028
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
6029
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
6030
+ throw new Error(`Invalid ${name} value: ${raw}. Expected true or false.`);
6031
+ }
6032
+ function intentAllowsExport(intentLabel) {
6033
+ const normalized = intentLabel.trim();
6034
+ if (!normalized) return false;
6035
+ return explicitExportIntentMatchers.some((matcher) => matcher.test(normalized));
6036
+ }
6037
+ function resolveExportPermission(intentLabel, opts = {}) {
6038
+ if (opts.allowExport !== void 0) {
6039
+ return opts.allowExport;
6040
+ }
6041
+ const envOverride = parseBooleanFromEnv("STUDIOFLOW_ALLOW_EXPORT");
6042
+ if (envOverride !== void 0) {
6043
+ return envOverride;
6044
+ }
6045
+ return intentAllowsExport(intentLabel);
6046
+ }
5458
6047
  async function waitForHealth(healthUrl, timeoutMs = 45e3) {
5459
6048
  const start = Date.now();
5460
6049
  let lastError = null;
@@ -5505,7 +6094,23 @@ async function startAppLifecycle(baseUrl, startCommand, healthPath) {
5505
6094
  function formatDuration(start) {
5506
6095
  return `${((Date.now() - start) / 1e3).toFixed(1)}s`;
5507
6096
  }
5508
- async function runWithFlows(intentLabel, flowIds, flows, runtime) {
6097
+ async function runRecorderPreflight(recorder) {
6098
+ if (recorder === "screenstudio") {
6099
+ const prep2 = await runScreenStudioPreflight({ ensurePermissions: false, quiet: true });
6100
+ return {
6101
+ label: "Screen Studio",
6102
+ itemsLabel: "Record menu",
6103
+ menuItems: prep2.recordMenuItems
6104
+ };
6105
+ }
6106
+ const prep = await runQuickTimePreflight({ ensurePermissions: false, quiet: true });
6107
+ return {
6108
+ label: "QuickTime",
6109
+ itemsLabel: "File menu",
6110
+ menuItems: prep.fileMenuItems
6111
+ };
6112
+ }
6113
+ async function runWithFlows(intentLabel, flowIds, flows, runtime, opts = {}) {
5509
6114
  const chromium2 = await ensureChromiumInstalled({ autoInstall: true });
5510
6115
  if (!chromium2.installed) {
5511
6116
  throw new Error("Playwright Chromium is not installed. Run `studioflow setup` and retry.");
@@ -5515,11 +6120,12 @@ async function runWithFlows(intentLabel, flowIds, flows, runtime) {
5515
6120
  }
5516
6121
  await ensureAutomationPermissions();
5517
6122
  try {
5518
- const prep = await runScreenStudioPreflight({ ensurePermissions: false, quiet: true });
5519
- console.log(`Screen Studio preflight: ready (${prep.recordMenuItems.join(", ")})`);
6123
+ const prep = await runRecorderPreflight(runtime.values.recorder);
6124
+ console.log(`${prep.label} preflight: ready (${prep.itemsLabel}: ${prep.menuItems.join(", ")})`);
5520
6125
  } catch (error) {
5521
6126
  const message = error instanceof Error ? error.message : String(error);
5522
- throw new Error(`Screen Studio preflight failed: ${message}. Run \`studioflow screenstudio-prep\` for diagnostics.`);
6127
+ const diagnosticsCommand = runtime.values.recorder === "screenstudio" ? "studioflow screenstudio-prep" : "studioflow quicktime-prep";
6128
+ throw new Error(`${runtime.values.recorder} preflight failed: ${message}. Run \`${diagnosticsCommand}\` for diagnostics.`);
5523
6129
  }
5524
6130
  for (const flow of flows) {
5525
6131
  const validation = validateFlowDefinition(flow);
@@ -5527,6 +6133,11 @@ async function runWithFlows(intentLabel, flowIds, flows, runtime) {
5527
6133
  throw new Error(`Flow ${flow.id} failed validation: ${validation.errors.join("; ")}`);
5528
6134
  }
5529
6135
  }
6136
+ if (flowRequestsExport(flows) && !resolveExportPermission(intentLabel, opts)) {
6137
+ throw new Error(
6138
+ "Flow includes recorder_export, but the run intent does not explicitly request export. Remove recorder_export, include export language in --intent, or pass --allow-export true."
6139
+ );
6140
+ }
5530
6141
  const started = Date.now();
5531
6142
  console.log(kleur_default.bold("StudioFlow Run"));
5532
6143
  console.log(`Intent: ${intentLabel}`);
@@ -5535,6 +6146,7 @@ async function runWithFlows(intentLabel, flowIds, flows, runtime) {
5535
6146
  console.log(`Start command: ${runtime.values.startCommand ?? "(not configured)"}`);
5536
6147
  console.log(`Health path: ${runtime.values.healthPath}`);
5537
6148
  console.log(`Headless: ${String(runtime.values.headless)}`);
6149
+ console.log(`Recorder: ${runtime.values.recorder}`);
5538
6150
  console.log(`Runs dir: ${runtime.values.runsDir}`);
5539
6151
  const previousRunsDir = process.env.STUDIOFLOW_RUNS_DIR;
5540
6152
  process.env.STUDIOFLOW_RUNS_DIR = runtime.values.runsDir;
@@ -5544,6 +6156,7 @@ async function runWithFlows(intentLabel, flowIds, flows, runtime) {
5544
6156
  flows,
5545
6157
  baseUrl: runtime.values.baseUrl,
5546
6158
  headless: runtime.values.headless,
6159
+ recorder: runtime.values.recorder,
5547
6160
  startApp: async () => startAppLifecycle(runtime.values.baseUrl, runtime.values.startCommand, runtime.values.healthPath)
5548
6161
  });
5549
6162
  console.log(kleur_default.green("Run completed successfully."));
@@ -5565,14 +6178,14 @@ async function runWithFlows(intentLabel, flowIds, flows, runtime) {
5565
6178
  }
5566
6179
  }
5567
6180
  }
5568
- async function runFlowFileCommand(flowPath, sourceIntent = "artifact flow", overrides) {
6181
+ async function runFlowFileCommand(flowPath, sourceIntent = "artifact flow", overrides, opts = {}) {
5569
6182
  if (!flowPath) {
5570
6183
  throw new Error("Usage: studioflow run --flow <path/to/flow.json|yaml>");
5571
6184
  }
5572
6185
  const resolvedPath = resolveFromWorkspace(flowPath);
5573
6186
  const runtime = await resolveRuntimeConfig(overrides);
5574
6187
  const flow = await loadFlowFromFile(resolvedPath);
5575
- await runWithFlows(sourceIntent, [flow.id], [flow], runtime);
6188
+ await runWithFlows(sourceIntent, [flow.id], [flow], runtime, opts);
5576
6189
  }
5577
6190
 
5578
6191
  // src/commands/list-flows.ts
@@ -5589,20 +6202,23 @@ async function doctorCommand() {
5589
6202
  const permissions = await checkPermissions();
5590
6203
  const checks = [
5591
6204
  {
5592
- name: "Screen Studio installed",
5593
- ok: permissions.screenStudioInstalled
6205
+ name: "Screen Studio installed (optional)",
6206
+ ok: permissions.screenStudioInstalled,
6207
+ required: false
5594
6208
  },
5595
6209
  {
5596
6210
  name: "AppleScript available",
5597
- ok: permissions.canRunAppleScript
6211
+ ok: permissions.canRunAppleScript,
6212
+ required: true
5598
6213
  },
5599
6214
  {
5600
6215
  name: "Keystroke automation allowed",
5601
- ok: permissions.canSendKeystrokes
6216
+ ok: permissions.canSendKeystrokes,
6217
+ required: true
5602
6218
  }
5603
6219
  ];
5604
6220
  for (const check of checks) {
5605
- const mark = check.ok ? kleur_default.green("PASS") : kleur_default.red("FAIL");
6221
+ const mark = check.ok ? kleur_default.green("PASS") : check.required ? kleur_default.red("FAIL") : kleur_default.yellow("WARN");
5606
6222
  console.log(`${mark} ${check.name}`);
5607
6223
  }
5608
6224
  if (permissions.notes.length > 0) {
@@ -5611,7 +6227,7 @@ async function doctorCommand() {
5611
6227
  console.log(`- ${note}`);
5612
6228
  }
5613
6229
  }
5614
- const failed = checks.filter((c) => !c.ok);
6230
+ const failed = checks.filter((c) => c.required && !c.ok);
5615
6231
  if (failed.length > 0) {
5616
6232
  await triggerPermissionPrompts();
5617
6233
  await openPermissionSettings();
@@ -5623,10 +6239,10 @@ async function doctorCommand() {
5623
6239
  }
5624
6240
 
5625
6241
  // src/commands/discover.ts
5626
- import fs8 from "node:fs/promises";
6242
+ import fs9 from "node:fs/promises";
5627
6243
  import path9 from "node:path";
5628
6244
  async function walk(dir, acc = []) {
5629
- const entries = await fs8.readdir(dir, { withFileTypes: true });
6245
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
5630
6246
  for (const entry of entries) {
5631
6247
  if (["node_modules", ".next", ".git", "dist", ".runs"].includes(entry.name)) continue;
5632
6248
  const fullPath = path9.join(dir, entry.name);
@@ -5673,7 +6289,7 @@ async function detectPackageManager(root) {
5673
6289
  ];
5674
6290
  for (const lock of lockfiles) {
5675
6291
  try {
5676
- await fs8.access(path9.join(root, lock.file));
6292
+ await fs9.access(path9.join(root, lock.file));
5677
6293
  return lock.name;
5678
6294
  } catch {
5679
6295
  }
@@ -5725,7 +6341,7 @@ async function discoverCommand(outDir = "artifacts") {
5725
6341
  const root = workspaceRoot();
5726
6342
  const files = await walk(root);
5727
6343
  const pkgPath = path9.join(root, "package.json");
5728
- const pkg = JSON.parse(await fs8.readFile(pkgPath, "utf8"));
6344
+ const pkg = JSON.parse(await fs9.readFile(pkgPath, "utf8"));
5729
6345
  const packageManager = await detectPackageManager(root);
5730
6346
  const projectType = detectProjectType(pkg, files);
5731
6347
  const frameworkHints = [
@@ -5760,7 +6376,7 @@ async function discoverCommand(outDir = "artifacts") {
5760
6376
  }));
5761
6377
  const inferredEdges = [];
5762
6378
  for (const routeFile of routeFilesWithPaths) {
5763
- const raw = await fs8.readFile(routeFile.filePath, "utf8");
6379
+ const raw = await fs9.readFile(routeFile.filePath, "utf8");
5764
6380
  inferredEdges.push(...inferEdgesFromSource(routeFile.route, routeFile.filePath, raw, routeSet));
5765
6381
  }
5766
6382
  const deduped = Array.from(
@@ -5774,11 +6390,11 @@ async function discoverCommand(outDir = "artifacts") {
5774
6390
  structureReportSchema.parse(structureReport);
5775
6391
  navigationGraphSchema.parse(graph);
5776
6392
  const outputDir = resolveFromWorkspace(outDir);
5777
- await fs8.mkdir(outputDir, { recursive: true });
6393
+ await fs9.mkdir(outputDir, { recursive: true });
5778
6394
  const structurePath = path9.join(outputDir, "structure-report.json");
5779
6395
  const graphPath = path9.join(outputDir, "navigation-graph.json");
5780
- await fs8.writeFile(structurePath, JSON.stringify(structureReport, null, 2), "utf8");
5781
- await fs8.writeFile(graphPath, JSON.stringify(graph, null, 2), "utf8");
6396
+ await fs9.writeFile(structurePath, JSON.stringify(structureReport, null, 2), "utf8");
6397
+ await fs9.writeFile(graphPath, JSON.stringify(graph, null, 2), "utf8");
5782
6398
  console.log(kleur_default.green("Discovery complete."));
5783
6399
  console.log(`- Structure report: ${structurePath}`);
5784
6400
  console.log(`- Navigation graph: ${graphPath}`);
@@ -5806,7 +6422,7 @@ async function validateCommand(flowPath) {
5806
6422
  }
5807
6423
 
5808
6424
  // src/commands/bootstrap.ts
5809
- import fs9 from "node:fs/promises";
6425
+ import fs10 from "node:fs/promises";
5810
6426
  import path10 from "node:path";
5811
6427
  async function detectPackageManager2(root) {
5812
6428
  const lockfiles = [
@@ -5816,7 +6432,7 @@ async function detectPackageManager2(root) {
5816
6432
  ];
5817
6433
  for (const lock of lockfiles) {
5818
6434
  try {
5819
- await fs9.access(path10.join(root, lock.file));
6435
+ await fs10.access(path10.join(root, lock.file));
5820
6436
  return lock.manager;
5821
6437
  } catch {
5822
6438
  }
@@ -5824,7 +6440,7 @@ async function detectPackageManager2(root) {
5824
6440
  return "unknown";
5825
6441
  }
5826
6442
  async function walk2(dir, acc = []) {
5827
- const entries = await fs9.readdir(dir, { withFileTypes: true });
6443
+ const entries = await fs10.readdir(dir, { withFileTypes: true });
5828
6444
  for (const entry of entries) {
5829
6445
  if (["node_modules", ".next", ".git", "dist", ".runs"].includes(entry.name)) continue;
5830
6446
  const fullPath = path10.join(dir, entry.name);
@@ -5864,7 +6480,7 @@ function defaultHealthPath(projectType) {
5864
6480
  async function bootstrapCommand(outPath = "artifacts/bootstrap.json") {
5865
6481
  const root = workspaceRoot();
5866
6482
  const pkgPath = path10.join(root, "package.json");
5867
- const pkg = JSON.parse(await fs9.readFile(pkgPath, "utf8"));
6483
+ const pkg = JSON.parse(await fs10.readFile(pkgPath, "utf8"));
5868
6484
  const packageManager = await detectPackageManager2(root);
5869
6485
  const files = await walk2(root);
5870
6486
  const projectType = detectProjectType2(pkg, files);
@@ -5885,8 +6501,8 @@ async function bootstrapCommand(outPath = "artifacts/bootstrap.json") {
5885
6501
  };
5886
6502
  const validated = bootstrapReportSchema.parse(report);
5887
6503
  const resolved = resolveFromWorkspace(outPath);
5888
- await fs9.mkdir(path10.dirname(resolved), { recursive: true });
5889
- await fs9.writeFile(resolved, JSON.stringify(validated, null, 2), "utf8");
6504
+ await fs10.mkdir(path10.dirname(resolved), { recursive: true });
6505
+ await fs10.writeFile(resolved, JSON.stringify(validated, null, 2), "utf8");
5890
6506
  console.log(kleur_default.green("Bootstrap report generated."));
5891
6507
  console.log(`- Output: ${resolved}`);
5892
6508
  console.log(`- Start command: ${validated.startCommand}`);
@@ -5894,26 +6510,142 @@ async function bootstrapCommand(outPath = "artifacts/bootstrap.json") {
5894
6510
  }
5895
6511
 
5896
6512
  // src/commands/setup.ts
5897
- import fs11 from "node:fs/promises";
6513
+ import fs12 from "node:fs/promises";
5898
6514
  import path12 from "node:path";
5899
6515
 
5900
6516
  // src/commands/install-skills.ts
5901
- import fs10 from "node:fs/promises";
6517
+ import { createHash } from "node:crypto";
6518
+ import fs11 from "node:fs/promises";
5902
6519
  import path11 from "node:path";
5903
- import { fileURLToPath as fileURLToPath2 } from "node:url";
5904
- var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
6520
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
6521
+ var __dirname2 = path11.dirname(fileURLToPath3(import.meta.url));
5905
6522
  var bundledSkillNames = ["studioflow-cli", "studioflow-investigate"];
6523
+ var skillManifestFile = "manifest.json";
6524
+ var skillMetadataFile = ".studioflow-skill.json";
6525
+ var packageName = "studioflow";
5906
6526
  async function pathExists(target) {
5907
6527
  try {
5908
- await fs10.access(target);
6528
+ await fs11.access(target);
5909
6529
  return true;
5910
6530
  } catch {
5911
6531
  return false;
5912
6532
  }
5913
6533
  }
6534
+ function toPosixPath(value) {
6535
+ return value.split(path11.sep).join("/");
6536
+ }
6537
+ async function listFilesRecursively(rootDir2) {
6538
+ const entries = await fs11.readdir(rootDir2, { withFileTypes: true });
6539
+ const files = [];
6540
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
6541
+ const fullPath = path11.join(rootDir2, entry.name);
6542
+ if (entry.isDirectory()) {
6543
+ files.push(...await listFilesRecursively(fullPath));
6544
+ continue;
6545
+ }
6546
+ if (entry.isFile()) {
6547
+ files.push(fullPath);
6548
+ }
6549
+ }
6550
+ return files;
6551
+ }
6552
+ async function hashDirectoryContents(rootDir2) {
6553
+ const hasher = createHash("sha256");
6554
+ const files = await listFilesRecursively(rootDir2);
6555
+ for (const filePath of files) {
6556
+ const relativePath = toPosixPath(path11.relative(rootDir2, filePath));
6557
+ hasher.update(relativePath);
6558
+ hasher.update("\n");
6559
+ hasher.update(await fs11.readFile(filePath));
6560
+ hasher.update("\n");
6561
+ }
6562
+ return hasher.digest("hex");
6563
+ }
6564
+ async function resolveCliPackageVersion(sourceDir) {
6565
+ const candidates = [
6566
+ path11.resolve(sourceDir, "../package.json"),
6567
+ path11.resolve(__dirname2, "../../package.json"),
6568
+ path11.resolve(process.cwd(), "apps/cli/package.json")
6569
+ ];
6570
+ for (const candidate of candidates) {
6571
+ try {
6572
+ const raw = await fs11.readFile(candidate, "utf8");
6573
+ const parsed = JSON.parse(raw);
6574
+ if (parsed.name === packageName && typeof parsed.version === "string") {
6575
+ return parsed.version;
6576
+ }
6577
+ } catch {
6578
+ }
6579
+ }
6580
+ return "0.0.0";
6581
+ }
6582
+ function isValidManifest(value) {
6583
+ if (!value || typeof value !== "object") return false;
6584
+ const manifest = value;
6585
+ if (manifest.schemaVersion !== 1 || manifest.packageName !== packageName || typeof manifest.packageVersion !== "string" || !manifest.skills || typeof manifest.skills !== "object") {
6586
+ return false;
6587
+ }
6588
+ return bundledSkillNames.every((name) => {
6589
+ const entry = manifest.skills[name];
6590
+ return Boolean(entry && typeof entry.hash === "string" && entry.hash.length > 0);
6591
+ });
6592
+ }
6593
+ async function resolveBundledSkillsManifest(sourceDir) {
6594
+ const manifestPath = path11.join(sourceDir, skillManifestFile);
6595
+ if (await pathExists(manifestPath)) {
6596
+ try {
6597
+ const raw = await fs11.readFile(manifestPath, "utf8");
6598
+ const parsed = JSON.parse(raw);
6599
+ if (isValidManifest(parsed)) {
6600
+ return parsed;
6601
+ }
6602
+ } catch {
6603
+ }
6604
+ }
6605
+ const skills = {};
6606
+ for (const skillName of bundledSkillNames) {
6607
+ const skillPath = path11.join(sourceDir, skillName);
6608
+ skills[skillName] = {
6609
+ hash: await hashDirectoryContents(skillPath)
6610
+ };
6611
+ }
6612
+ return {
6613
+ schemaVersion: 1,
6614
+ packageName,
6615
+ packageVersion: await resolveCliPackageVersion(sourceDir),
6616
+ skills
6617
+ };
6618
+ }
6619
+ async function readInstalledSkillMetadata(skillDir) {
6620
+ const metadataPath = path11.join(skillDir, skillMetadataFile);
6621
+ if (!await pathExists(metadataPath)) {
6622
+ return null;
6623
+ }
6624
+ try {
6625
+ const raw = await fs11.readFile(metadataPath, "utf8");
6626
+ const parsed = JSON.parse(raw);
6627
+ if (typeof parsed.packageName === "string" && typeof parsed.cliVersion === "string" && typeof parsed.skillHash === "string" && typeof parsed.installedAt === "string") {
6628
+ return parsed;
6629
+ }
6630
+ } catch {
6631
+ }
6632
+ return null;
6633
+ }
6634
+ async function writeInstalledSkillMetadata(skillDir, expectedVersion, expectedHash) {
6635
+ const metadata = {
6636
+ packageName,
6637
+ cliVersion: expectedVersion,
6638
+ skillHash: expectedHash,
6639
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
6640
+ };
6641
+ await fs11.writeFile(path11.join(skillDir, skillMetadataFile), `${JSON.stringify(metadata, null, 2)}
6642
+ `);
6643
+ }
5914
6644
  async function resolveBundledSkillsDir() {
5915
6645
  const candidates = [
5916
6646
  process.env.STUDIOFLOW_SKILLS_SOURCE,
6647
+ path11.resolve(__dirname2, "../bundled/skills"),
6648
+ path11.resolve(__dirname2, "../../bundled/skills"),
5917
6649
  path11.resolve(__dirname2, "../skills"),
5918
6650
  path11.resolve(__dirname2, "../../skills"),
5919
6651
  path11.resolve(__dirname2, "../../../../skills"),
@@ -5934,28 +6666,48 @@ async function resolveBundledSkillsDir() {
5934
6666
  `Could not locate bundled skills. Checked: ${candidates.join(", ")}. Set STUDIOFLOW_SKILLS_SOURCE if needed.`
5935
6667
  );
5936
6668
  }
5937
- async function syncSkillsToTarget(sourceDir, targetDir, force) {
5938
- await fs10.mkdir(targetDir, { recursive: true });
6669
+ async function syncSkillsToTarget(sourceDir, targetDir, force, manifest) {
6670
+ await fs11.mkdir(targetDir, { recursive: true });
5939
6671
  const installed = [];
6672
+ const updated = [];
5940
6673
  const skipped = [];
5941
6674
  for (const skillName of bundledSkillNames) {
5942
6675
  const source = path11.join(sourceDir, skillName);
5943
6676
  const target = path11.join(targetDir, skillName);
5944
6677
  const exists = await pathExists(target);
5945
- if (exists && !force) {
5946
- skipped.push(skillName);
6678
+ const expectedHash = manifest.skills[skillName]?.hash;
6679
+ if (!expectedHash) {
6680
+ throw new Error(`Missing skill hash in manifest for ${skillName}.`);
6681
+ }
6682
+ if (!exists) {
6683
+ await fs11.cp(source, target, { recursive: true });
6684
+ await writeInstalledSkillMetadata(target, manifest.packageVersion, expectedHash);
6685
+ installed.push(skillName);
5947
6686
  continue;
5948
6687
  }
5949
- if (exists && force) {
5950
- await fs10.rm(target, { recursive: true, force: true });
6688
+ if (force) {
6689
+ await fs11.rm(target, { recursive: true, force: true });
6690
+ await fs11.cp(source, target, { recursive: true });
6691
+ await writeInstalledSkillMetadata(target, manifest.packageVersion, expectedHash);
6692
+ installed.push(skillName);
6693
+ continue;
5951
6694
  }
5952
- await fs10.cp(source, target, { recursive: true });
5953
- installed.push(skillName);
6695
+ const currentMetadata = await readInstalledSkillMetadata(target);
6696
+ const matchesInstalledVersion = currentMetadata?.packageName === manifest.packageName && currentMetadata.cliVersion === manifest.packageVersion && currentMetadata.skillHash === expectedHash;
6697
+ if (matchesInstalledVersion) {
6698
+ skipped.push(skillName);
6699
+ continue;
6700
+ }
6701
+ await fs11.rm(target, { recursive: true, force: true });
6702
+ await fs11.cp(source, target, { recursive: true });
6703
+ await writeInstalledSkillMetadata(target, manifest.packageVersion, expectedHash);
6704
+ updated.push(skillName);
5954
6705
  }
5955
- return { installed, skipped };
6706
+ return { installed, updated, skipped };
5956
6707
  }
5957
6708
  async function installBundledSkills(opts = {}) {
5958
6709
  const sourceDir = await resolveBundledSkillsDir();
6710
+ const manifest = await resolveBundledSkillsManifest(sourceDir);
5959
6711
  const force = opts.force ?? false;
5960
6712
  const targets = [];
5961
6713
  if (opts.targetDir && (opts.agent || opts.codexTargetDir || opts.claudeTargetDir)) {
@@ -5980,11 +6732,12 @@ async function installBundledSkills(opts = {}) {
5980
6732
  }
5981
6733
  const results = [];
5982
6734
  for (const target of targets) {
5983
- const synced = await syncSkillsToTarget(sourceDir, target.targetDir, force);
6735
+ const synced = await syncSkillsToTarget(sourceDir, target.targetDir, force, manifest);
5984
6736
  results.push({
5985
6737
  targetId: target.targetId,
5986
6738
  targetDir: target.targetDir,
5987
6739
  installed: synced.installed,
6740
+ updated: synced.updated,
5988
6741
  skipped: synced.skipped
5989
6742
  });
5990
6743
  }
@@ -6001,8 +6754,11 @@ async function installSkillsCommand(opts = {}) {
6001
6754
  if (target.installed.length > 0) {
6002
6755
  console.log(`- Installed (${label}): ${target.installed.join(", ")}`);
6003
6756
  }
6757
+ if (target.updated.length > 0) {
6758
+ console.log(`- Updated (${label}): ${target.updated.join(", ")}`);
6759
+ }
6004
6760
  if (target.skipped.length > 0) {
6005
- console.log(kleur_default.yellow(`- Skipped (${label}, already present): ${target.skipped.join(", ")}`));
6761
+ console.log(kleur_default.yellow(`- Skipped (${label}, up to date): ${target.skipped.join(", ")}`));
6006
6762
  }
6007
6763
  }
6008
6764
  if (result.targets.some((target) => target.skipped.length > 0)) {
@@ -6017,8 +6773,8 @@ function setupStatePath() {
6017
6773
  }
6018
6774
  async function writeSetupState(payload) {
6019
6775
  const statePath = setupStatePath();
6020
- await fs11.mkdir(path12.dirname(statePath), { recursive: true });
6021
- await fs11.writeFile(statePath, JSON.stringify(payload, null, 2), "utf8");
6776
+ await fs12.mkdir(path12.dirname(statePath), { recursive: true });
6777
+ await fs12.writeFile(statePath, JSON.stringify(payload, null, 2), "utf8");
6022
6778
  }
6023
6779
  async function setupCommand(opts = {}) {
6024
6780
  console.log(kleur_default.bold("StudioFlow setup"));
@@ -6049,14 +6805,17 @@ async function setupCommand(opts = {}) {
6049
6805
  if (target.installed.length > 0) {
6050
6806
  console.log(`- Installed (${label}): ${target.installed.join(", ")}`);
6051
6807
  }
6808
+ if (target.updated.length > 0) {
6809
+ console.log(`- Updated (${label}): ${target.updated.join(", ")}`);
6810
+ }
6052
6811
  if (target.skipped.length > 0) {
6053
- console.log(kleur_default.yellow(`- Skipped (${label}, already present): ${target.skipped.join(", ")}`));
6812
+ console.log(kleur_default.yellow(`- Skipped (${label}, up to date): ${target.skipped.join(", ")}`));
6054
6813
  }
6055
6814
  }
6056
6815
  }
6057
6816
  const permissions = await checkPermissions();
6058
6817
  const permissionChecks = [
6059
- { label: "Screen Studio installed", ok: permissions.screenStudioInstalled },
6818
+ { label: "Screen Studio installed (optional)", ok: permissions.screenStudioInstalled },
6060
6819
  { label: "AppleScript available", ok: permissions.canRunAppleScript },
6061
6820
  { label: "Keystroke automation allowed", ok: permissions.canSendKeystrokes }
6062
6821
  ];
@@ -6081,6 +6840,7 @@ async function setupCommand(opts = {}) {
6081
6840
  targetId: target.targetId,
6082
6841
  targetDir: target.targetDir,
6083
6842
  installed: target.installed,
6843
+ updated: target.updated,
6084
6844
  skipped: target.skipped
6085
6845
  }))
6086
6846
  } : { skipped: true }
@@ -6093,9 +6853,9 @@ async function setupCommand(opts = {}) {
6093
6853
  var cachedVersion = null;
6094
6854
  async function cliVersion() {
6095
6855
  if (cachedVersion) return cachedVersion;
6096
- const dirname = path13.dirname(fileURLToPath3(import.meta.url));
6856
+ const dirname = path13.dirname(fileURLToPath4(import.meta.url));
6097
6857
  const packageJsonPath = path13.resolve(dirname, "../package.json");
6098
- const raw = await fs12.readFile(packageJsonPath, "utf8");
6858
+ const raw = await fs13.readFile(packageJsonPath, "utf8");
6099
6859
  const parsed = JSON.parse(raw);
6100
6860
  cachedVersion = parsed.version ?? "0.0.0";
6101
6861
  return cachedVersion;
@@ -6122,6 +6882,13 @@ function readFlagValue(args, flag) {
6122
6882
  }
6123
6883
  return value;
6124
6884
  }
6885
+ function parseRecorder(raw, flag) {
6886
+ if (raw === void 0) return void 0;
6887
+ if (raw === "quicktime" || raw === "screenstudio") {
6888
+ return raw;
6889
+ }
6890
+ throw new Error(`Invalid ${flag} value: ${raw}. Expected quicktime or screenstudio.`);
6891
+ }
6125
6892
  function parseRuntimeConfigOverrides(args) {
6126
6893
  const headlessRaw = readFlagValue(args, "--headless");
6127
6894
  return {
@@ -6130,9 +6897,16 @@ function parseRuntimeConfigOverrides(args) {
6130
6897
  healthPath: readFlagValue(args, "--health-path"),
6131
6898
  bootstrapReportPath: readFlagValue(args, "--bootstrap-report"),
6132
6899
  runsDir: readFlagValue(args, "--runs-dir"),
6900
+ recorder: parseRecorder(readFlagValue(args, "--recorder"), "--recorder"),
6133
6901
  headless: headlessRaw ? parseBoolean(headlessRaw, "--headless") : void 0
6134
6902
  };
6135
6903
  }
6904
+ function parseRunCommandOptions(args) {
6905
+ const allowExportRaw = readFlagValue(args, "--allow-export");
6906
+ return {
6907
+ allowExport: allowExportRaw ? parseBoolean(allowExportRaw, "--allow-export") : void 0
6908
+ };
6909
+ }
6136
6910
  function parseSkillsAgent(raw, flag) {
6137
6911
  if (raw === void 0) return void 0;
6138
6912
  if (raw === "codex" || raw === "claude" || raw === "all") {
@@ -6152,12 +6926,13 @@ async function main() {
6152
6926
  const flowPath = readFlag(normalizedArgs, "--flow");
6153
6927
  if (!flowPath) {
6154
6928
  throw new Error(
6155
- 'Usage: studioflow run --flow <path/to/flow.json|yaml> [--intent "<label>"] [--base-url <url>] [--start-command "<command>"] [--health-path <path>] [--headless <true|false>] [--bootstrap-report <path>] [--runs-dir <path>]'
6929
+ 'Usage: studioflow run --flow <path/to/flow.json|yaml> [--intent "<label>"] [--allow-export <true|false>] [--base-url <url>] [--start-command "<command>"] [--health-path <path>] [--headless <true|false>] [--recorder <quicktime|screenstudio>] [--bootstrap-report <path>] [--runs-dir <path>]'
6156
6930
  );
6157
6931
  }
6158
6932
  const sourceIntent = readFlag(normalizedArgs, "--intent") ?? "artifact flow";
6159
6933
  const runtimeOverrides = parseRuntimeConfigOverrides(normalizedArgs);
6160
- await runFlowFileCommand(flowPath, sourceIntent, runtimeOverrides);
6934
+ const runOptions = parseRunCommandOptions(normalizedArgs);
6935
+ await runFlowFileCommand(flowPath, sourceIntent, runtimeOverrides, runOptions);
6161
6936
  return;
6162
6937
  }
6163
6938
  if (command === "config") {
@@ -6174,7 +6949,7 @@ async function main() {
6174
6949
  return;
6175
6950
  }
6176
6951
  throw new Error(
6177
- "Usage: studioflow config <show|check> [--json] [--base-url <url>] [--start-command <command>] [--health-path <path>] [--headless <true|false>] [--bootstrap-report <path>] [--runs-dir <path>]"
6952
+ "Usage: studioflow config <show|check> [--json] [--base-url <url>] [--start-command <command>] [--health-path <path>] [--headless <true|false>] [--recorder <quicktime|screenstudio>] [--bootstrap-report <path>] [--runs-dir <path>]"
6178
6953
  );
6179
6954
  }
6180
6955
  if (command === "discover") {
@@ -6192,6 +6967,11 @@ async function main() {
6192
6967
  await screenstudioPrepCommand(appName2);
6193
6968
  return;
6194
6969
  }
6970
+ if (command === "quicktime-prep") {
6971
+ const appName2 = readFlag(normalizedArgs, "--app-name");
6972
+ await quicktimePrepCommand(appName2);
6973
+ return;
6974
+ }
6195
6975
  if (command === "setup") {
6196
6976
  await setupCommand({
6197
6977
  skipSkills: hasFlag(normalizedArgs, "--skip-skills"),