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/LICENSE +21 -0
- package/README.md +4 -3
- package/assets/cursor.png +0 -0
- package/bundled/skills/manifest.json +14 -0
- package/{skills → bundled/skills}/studioflow-cli/SKILL.md +11 -2
- package/{skills → bundled/skills}/studioflow-cli/references/handoff-spec.md +12 -3
- package/{skills → bundled/skills}/studioflow-investigate/SKILL.md +13 -3
- package/{skills → bundled/skills}/studioflow-investigate/references/artifact-spec.md +6 -0
- package/{skills → bundled/skills}/studioflow-investigate/references/clarification-state.md +5 -2
- package/{skills → bundled/skills}/studioflow-investigate/references/cli-handoff-spec.md +9 -5
- package/dist/index.js +917 -137
- package/dist/index.js.map +4 -4
- package/package.json +22 -22
- package/scripts/sync-skills.mjs +67 -3
- /package/{flows → bundled/flows}/billing.yaml +0 -0
- /package/{flows → bundled/flows}/onboarding.yaml +0 -0
- /package/{flows → bundled/flows}/onboarding_billing.yaml +0 -0
- /package/{skills → bundled/skills}/studioflow-cli/references/troubleshooting.md +0 -0
- /package/{skills → bundled/skills}/studioflow-investigate/references/question-card-spec.md +0 -0
package/dist/index.js
CHANGED
|
@@ -6,9 +6,9 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import
|
|
9
|
+
import fs13 from "node:fs/promises";
|
|
10
10
|
import path13 from "node:path";
|
|
11
|
-
import { fileURLToPath as
|
|
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
|
|
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:
|
|
248
|
-
height:
|
|
249
|
-
background-image: url("
|
|
468
|
+
width: ${asset.width}px;
|
|
469
|
+
height: ${asset.height}px;
|
|
470
|
+
background-image: url("\${cursorAsset}");
|
|
250
471
|
background-repeat: no-repeat;
|
|
251
|
-
background-size:
|
|
472
|
+
background-size: ${asset.width}px ${asset.height}px;
|
|
252
473
|
pointer-events: none;
|
|
253
474
|
z-index: 2147483647;
|
|
254
|
-
|
|
255
|
-
transform: translate3d(
|
|
256
|
-
|
|
257
|
-
transition
|
|
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: -
|
|
263
|
-
top: -
|
|
264
|
-
width:
|
|
265
|
-
height:
|
|
484
|
+
left: -7px;
|
|
485
|
+
top: -7px;
|
|
486
|
+
width: 18px;
|
|
487
|
+
height: 18px;
|
|
266
488
|
border-radius: 999px;
|
|
267
|
-
border: 2px solid rgba(
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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.
|
|
293
|
-
|
|
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
|
-
|
|
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
|
|
623
|
+
var cursorPositionScript = `
|
|
315
624
|
(() => {
|
|
316
625
|
const cursor = window.__studioflowCursor;
|
|
317
|
-
if (cursor)
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
4986
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
4639
4987
|
var __dirname = path2.dirname(__filename);
|
|
4640
4988
|
var rootDir = path2.resolve(__dirname, "..");
|
|
4641
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
5071
|
+
import fs4 from "node:fs/promises";
|
|
4714
5072
|
async function appendJsonLine(filePath, payload) {
|
|
4715
|
-
await
|
|
5073
|
+
await fs4.appendFile(filePath, `${JSON.stringify(payload)}
|
|
4716
5074
|
`, "utf8");
|
|
4717
5075
|
}
|
|
4718
5076
|
|
|
4719
5077
|
// ../../packages/artifacts/src/writer.ts
|
|
4720
|
-
import
|
|
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
|
|
5085
|
+
await fs5.mkdir(screenshotsDir, { recursive: true });
|
|
4728
5086
|
const eventsFile = path4.join(runDir, "events.jsonl");
|
|
4729
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
5217
|
+
function buildActivateQuickTimeScript(appName2) {
|
|
4850
5218
|
return `tell application "${quote(appName2)}" to activate`;
|
|
4851
5219
|
}
|
|
4852
|
-
function
|
|
4853
|
-
return `tell application "System Events" to tell process "${quote(appName2)}" to get name of every menu item of menu "
|
|
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
|
|
4856
|
-
return `tell application "System Events" to tell process "${quote(appName2)}" to click menu item "${quote(itemName)}" of menu "
|
|
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
|
|
4882
|
-
await runAppleScript(
|
|
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
|
|
5363
|
+
function int2(value, fallback) {
|
|
4888
5364
|
const parsed = Number(value);
|
|
4889
5365
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
4890
5366
|
}
|
|
4891
|
-
var
|
|
4892
|
-
var
|
|
4893
|
-
var
|
|
4894
|
-
var
|
|
4895
|
-
var
|
|
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
|
|
4900
|
-
await
|
|
4901
|
-
await
|
|
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
|
|
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
|
|
4912
|
-
await
|
|
4913
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5550
|
+
await fs7.mkdir(run2.runDir, { recursive: true });
|
|
5053
5551
|
}
|
|
5054
5552
|
}
|
|
5055
5553
|
|
|
5056
5554
|
// src/commands/config.ts
|
|
5057
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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 === "\\" &&
|
|
5976
|
+
if (char === "\\" && quote3 !== "'") {
|
|
5414
5977
|
escaped = true;
|
|
5415
5978
|
tokenStarted = true;
|
|
5416
5979
|
continue;
|
|
5417
5980
|
}
|
|
5418
|
-
if (
|
|
5419
|
-
if (char ===
|
|
5420
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
5519
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5781
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5889
|
-
await
|
|
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
|
|
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
|
|
6517
|
+
import { createHash } from "node:crypto";
|
|
6518
|
+
import fs11 from "node:fs/promises";
|
|
5902
6519
|
import path11 from "node:path";
|
|
5903
|
-
import { fileURLToPath as
|
|
5904
|
-
var __dirname2 = path11.dirname(
|
|
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
|
|
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
|
|
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
|
-
|
|
5946
|
-
|
|
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 (
|
|
5950
|
-
await
|
|
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
|
|
5953
|
-
|
|
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},
|
|
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
|
|
6021
|
-
await
|
|
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},
|
|
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(
|
|
6856
|
+
const dirname = path13.dirname(fileURLToPath4(import.meta.url));
|
|
6097
6857
|
const packageJsonPath = path13.resolve(dirname, "../package.json");
|
|
6098
|
-
const raw = await
|
|
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
|
-
|
|
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"),
|