studioflow 0.1.0 → 0.1.2
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 +89 -0
- package/dist/index.js +83 -65
- package/dist/index.js.map +3 -3
- package/package.json +17 -18
- package/skills/studioflow-cli/SKILL.md +8 -5
- package/skills/studioflow-cli/references/handoff-spec.md +19 -0
- package/skills/studioflow-cli/references/troubleshooting.md +2 -2
- package/skills/studioflow-investigate/SKILL.md +3 -0
- package/skills/studioflow-investigate/references/artifact-spec.md +4 -0
- package/skills/studioflow-investigate/references/cli-handoff-spec.md +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StudioFlow contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# StudioFlow CLI
|
|
2
|
+
|
|
3
|
+
StudioFlow turns natural-language demo intent into deterministic UI run artifacts and executes them through Screen Studio.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- macOS (Screen Studio automation)
|
|
8
|
+
- Screen Studio installed
|
|
9
|
+
- Node.js 22+
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g studioflow
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup Runtime + Skills
|
|
18
|
+
|
|
19
|
+
`setup` installs Playwright Chromium, installs bundled skills for Codex and Claude, and checks permissions.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
studioflow setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Bundled skills include:
|
|
26
|
+
- `studioflow-investigate` (intent -> deterministic artifacts, including `artifacts/flow.json`)
|
|
27
|
+
- `studioflow-cli` (artifact execution workflow)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
1. Start your agent from the project root.
|
|
32
|
+
|
|
33
|
+
Codex:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd /path/to/your/project
|
|
37
|
+
codex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Claude Code:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd /path/to/your/project
|
|
44
|
+
claude
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If your launcher command differs, start your usual Codex or Claude session in this repo root.
|
|
48
|
+
|
|
49
|
+
2. Trigger `studioflow-investigate` to generate `artifacts/flow.json`.
|
|
50
|
+
|
|
51
|
+
Paste this prompt:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
Use StudioFlow skill studioflow-investigate.
|
|
55
|
+
Intent: "Record a demo for onboarding and billing."
|
|
56
|
+
Generate artifacts/flow.json for this repo.
|
|
57
|
+
If intent details are missing, ask concise follow-up questions before authoring the flow.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`studioflow-investigate` automatically collects project context artifacts before creating the flow.
|
|
61
|
+
|
|
62
|
+
3. Validate and run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
studioflow validate --flow artifacts/flow.json
|
|
66
|
+
studioflow demo --flow artifacts/flow.json --intent "onboarding and billing demo"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`run`/`demo` automatically performs Screen Studio preflight checks. Use manual prep only for troubleshooting.
|
|
70
|
+
Headed runs auto-open a maximized browser window for cleaner recording composition.
|
|
71
|
+
Runs do not auto-export by default; include `recorder_export` in flow steps only when export is explicitly needed.
|
|
72
|
+
|
|
73
|
+
## Optional: Trigger Runtime Skill In Agent
|
|
74
|
+
|
|
75
|
+
If you want the agent to drive execution too, use:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
Use StudioFlow skill studioflow-cli.
|
|
79
|
+
Validate artifacts/flow.json, run the demo, and report run artifact paths.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- Day 0 runbook: https://github.com/ydarar/studioflow/blob/main/docs/day0-runbook.md
|
|
85
|
+
- CLI reference: https://github.com/ydarar/studioflow/blob/main/docs/cli-reference.md
|
|
86
|
+
- Configuration: https://github.com/ydarar/studioflow/blob/main/docs/configuration.md
|
|
87
|
+
- Smoke checklist: https://github.com/ydarar/studioflow/blob/main/docs/testing-manual-smoke.md
|
|
88
|
+
- Open intent tests: https://github.com/ydarar/studioflow/blob/main/docs/studioflow-open-intent-tests.md
|
|
89
|
+
- Full docs map: https://github.com/ydarar/studioflow/blob/main/docs/README.md
|
package/dist/index.js
CHANGED
|
@@ -7513,29 +7513,37 @@ async function assertVisible(page, selector, timeoutMs = 5e3) {
|
|
|
7513
7513
|
function wait(ms) {
|
|
7514
7514
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7515
7515
|
}
|
|
7516
|
-
function boolEnv(name, fallback) {
|
|
7517
|
-
const raw =
|
|
7516
|
+
function boolEnv(name, fallback, env = process.env) {
|
|
7517
|
+
const raw = env[name];
|
|
7518
7518
|
if (!raw) return fallback;
|
|
7519
7519
|
const normalized = raw.trim().toLowerCase();
|
|
7520
7520
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
7521
7521
|
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
7522
7522
|
return fallback;
|
|
7523
7523
|
}
|
|
7524
|
-
function intEnv(name, fallback) {
|
|
7525
|
-
const raw =
|
|
7524
|
+
function intEnv(name, fallback, env = process.env) {
|
|
7525
|
+
const raw = env[name];
|
|
7526
7526
|
if (!raw) return fallback;
|
|
7527
7527
|
const parsed = Number(raw);
|
|
7528
7528
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
7529
7529
|
}
|
|
7530
|
-
var renderCursorOverlay = boolEnv("STUDIOFLOW_RENDER_CURSOR", true);
|
|
7531
|
-
var defaultCursorMoveMs = intEnv("STUDIOFLOW_CURSOR_MOVE_MS", 320);
|
|
7532
|
-
var defaultCursorHighlightMs = intEnv("STUDIOFLOW_CURSOR_HIGHLIGHT_MS", 120);
|
|
7533
|
-
var realisticTyping = boolEnv("STUDIOFLOW_REALISTIC_TYPING", true);
|
|
7534
|
-
var typingDelayMs = Math.max(0, intEnv("STUDIOFLOW_TYPING_DELAY_MS", 35));
|
|
7535
|
-
var pacingAdjustmentEnabled = boolEnv("STUDIOFLOW_PACING_ADJUSTMENT", true);
|
|
7536
|
-
var pacingJitterEnabled = boolEnv("STUDIOFLOW_PACING_JITTER", true);
|
|
7537
7530
|
var maxDelayMs = 6e4;
|
|
7538
|
-
|
|
7531
|
+
function resolveRuntimePacingDefaults(env = process.env) {
|
|
7532
|
+
return {
|
|
7533
|
+
renderCursorOverlay: boolEnv("STUDIOFLOW_RENDER_CURSOR", true, env),
|
|
7534
|
+
cursorMoveMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_MOVE_MS", 430, env)),
|
|
7535
|
+
cursorHighlightMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_HIGHLIGHT_MS", 170, env)),
|
|
7536
|
+
realisticTyping: boolEnv("STUDIOFLOW_REALISTIC_TYPING", true, env),
|
|
7537
|
+
typingDelayMs: Math.max(0, intEnv("STUDIOFLOW_TYPING_DELAY_MS", 55, env)),
|
|
7538
|
+
clickPulseMs: Math.max(0, intEnv("STUDIOFLOW_CLICK_PULSE_MS", 220, env)),
|
|
7539
|
+
stepPreDelayMs: Math.max(0, intEnv("STUDIOFLOW_STEP_PRE_DELAY_MS", 90, env)),
|
|
7540
|
+
stepPostDelayMs: Math.max(0, intEnv("STUDIOFLOW_STEP_POST_DELAY_MS", 130, env)),
|
|
7541
|
+
stepDwellMs: Math.max(0, intEnv("STUDIOFLOW_STEP_DWELL_MS", 180, env)),
|
|
7542
|
+
pacingAdjustmentEnabled: boolEnv("STUDIOFLOW_PACING_ADJUSTMENT", true, env),
|
|
7543
|
+
pacingJitterEnabled: boolEnv("STUDIOFLOW_PACING_JITTER", true, env)
|
|
7544
|
+
};
|
|
7545
|
+
}
|
|
7546
|
+
var cursorOverlaySetupScript = (clickPulseMs) => `
|
|
7539
7547
|
(() => {
|
|
7540
7548
|
const win = window;
|
|
7541
7549
|
if (win.__studioflowCursor) return;
|
|
@@ -7548,29 +7556,32 @@ var cursorOverlaySetupScript = `
|
|
|
7548
7556
|
position: fixed;
|
|
7549
7557
|
top: 0;
|
|
7550
7558
|
left: 0;
|
|
7551
|
-
width:
|
|
7552
|
-
height:
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
background:
|
|
7556
|
-
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.95), 0 4px 14px rgba(2, 6, 23, 0.28);
|
|
7559
|
+
width: 22px;
|
|
7560
|
+
height: 30px;
|
|
7561
|
+
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");
|
|
7562
|
+
background-repeat: no-repeat;
|
|
7563
|
+
background-size: 22px 30px;
|
|
7557
7564
|
pointer-events: none;
|
|
7558
7565
|
z-index: 2147483647;
|
|
7559
|
-
|
|
7566
|
+
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55));
|
|
7567
|
+
transform: translate3d(16px, 16px, 0);
|
|
7560
7568
|
transition-property: transform;
|
|
7561
7569
|
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
7562
7570
|
}
|
|
7563
7571
|
#studioflow-cursor::after {
|
|
7564
7572
|
content: "";
|
|
7565
7573
|
position: absolute;
|
|
7566
|
-
|
|
7574
|
+
left: -8px;
|
|
7575
|
+
top: -8px;
|
|
7576
|
+
width: 20px;
|
|
7577
|
+
height: 20px;
|
|
7567
7578
|
border-radius: 999px;
|
|
7568
7579
|
border: 2px solid rgba(59, 130, 246, 0.72);
|
|
7569
7580
|
opacity: 0;
|
|
7570
7581
|
transform: scale(0.65);
|
|
7571
7582
|
}
|
|
7572
7583
|
#studioflow-cursor.studioflow-cursor-pulse::after {
|
|
7573
|
-
animation: studioflow-cursor-pulse
|
|
7584
|
+
animation: studioflow-cursor-pulse ${Math.max(0, clickPulseMs)}ms ease-out;
|
|
7574
7585
|
}
|
|
7575
7586
|
@keyframes studioflow-cursor-pulse {
|
|
7576
7587
|
0% { opacity: 0.9; transform: scale(0.55); }
|
|
@@ -7591,7 +7602,7 @@ var cursorOverlaySetupScript = `
|
|
|
7591
7602
|
x = nextX;
|
|
7592
7603
|
y = nextY;
|
|
7593
7604
|
cursor.style.transitionDuration = \`\${Math.max(0, durationMs)}ms\`;
|
|
7594
|
-
cursor.style.transform = \`translate3d(\${x
|
|
7605
|
+
cursor.style.transform = \`translate3d(\${x}px, \${y}px, 0)\`;
|
|
7595
7606
|
};
|
|
7596
7607
|
|
|
7597
7608
|
win.__studioflowCursor = {
|
|
@@ -7631,40 +7642,40 @@ function jitterFactor(seed) {
|
|
|
7631
7642
|
const normalized = hash / 4294967295;
|
|
7632
7643
|
return normalized * 0.24 - 0.12;
|
|
7633
7644
|
}
|
|
7634
|
-
function resolvePacedDelay(rawMs, context, channel, allowJitter = true) {
|
|
7645
|
+
function resolvePacedDelay(rawMs, context, runtime, channel, allowJitter = true) {
|
|
7635
7646
|
if (!rawMs || rawMs <= 0) return 0;
|
|
7636
7647
|
let value = rawMs;
|
|
7637
|
-
const multiplier = pacingAdjustmentEnabled ? context.pacingMultiplier ?? 1 : 1;
|
|
7648
|
+
const multiplier = runtime.pacingAdjustmentEnabled ? context.pacingMultiplier ?? 1 : 1;
|
|
7638
7649
|
value *= multiplier;
|
|
7639
|
-
const shouldApplyJitter = allowJitter && pacingJitterEnabled && !context.strictPacing && Boolean(context.jitterSeed);
|
|
7650
|
+
const shouldApplyJitter = allowJitter && runtime.pacingJitterEnabled && !context.strictPacing && Boolean(context.jitterSeed);
|
|
7640
7651
|
if (shouldApplyJitter) {
|
|
7641
7652
|
value *= 1 + jitterFactor(`${context.jitterSeed}:${channel}`);
|
|
7642
7653
|
}
|
|
7643
7654
|
return Math.max(0, Math.min(maxDelayMs, Math.round(value)));
|
|
7644
7655
|
}
|
|
7645
|
-
async function ensureCursorOverlay(page) {
|
|
7646
|
-
if (!renderCursorOverlay) return;
|
|
7647
|
-
await page.evaluate(cursorOverlaySetupScript);
|
|
7656
|
+
async function ensureCursorOverlay(page, runtime) {
|
|
7657
|
+
if (!runtime.renderCursorOverlay) return;
|
|
7658
|
+
await page.evaluate(cursorOverlaySetupScript(runtime.clickPulseMs));
|
|
7648
7659
|
}
|
|
7649
7660
|
async function getTargetCenter(page, target) {
|
|
7650
7661
|
const box = await page.locator(target).first().boundingBox();
|
|
7651
7662
|
if (!box) return null;
|
|
7652
7663
|
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
7653
7664
|
}
|
|
7654
|
-
async function moveCursor(page, point, durationMs) {
|
|
7665
|
+
async function moveCursor(page, point, durationMs, runtime) {
|
|
7655
7666
|
await page.mouse.move(point.x, point.y, { steps: Math.max(8, Math.floor(durationMs / 16)) });
|
|
7656
|
-
if (renderCursorOverlay) {
|
|
7657
|
-
await ensureCursorOverlay(page);
|
|
7667
|
+
if (runtime.renderCursorOverlay) {
|
|
7668
|
+
await ensureCursorOverlay(page, runtime);
|
|
7658
7669
|
await page.evaluate(cursorMoveScript(point, durationMs));
|
|
7659
7670
|
}
|
|
7660
7671
|
}
|
|
7661
|
-
async function clickPulse(page) {
|
|
7662
|
-
if (!renderCursorOverlay) return;
|
|
7663
|
-
await ensureCursorOverlay(page);
|
|
7672
|
+
async function clickPulse(page, runtime) {
|
|
7673
|
+
if (!runtime.renderCursorOverlay) return;
|
|
7674
|
+
await ensureCursorOverlay(page, runtime);
|
|
7664
7675
|
await page.evaluate(cursorClickPulseScript);
|
|
7665
7676
|
}
|
|
7666
|
-
async function applyPreStepPacing(page, step, context) {
|
|
7667
|
-
const preDelay = resolvePacedDelay(step.preDelayMs, context, "pre");
|
|
7677
|
+
async function applyPreStepPacing(page, step, context, runtime) {
|
|
7678
|
+
const preDelay = resolvePacedDelay(step.preDelayMs ?? runtime.stepPreDelayMs, context, runtime, "pre");
|
|
7668
7679
|
if (preDelay > 0) {
|
|
7669
7680
|
await wait(preDelay);
|
|
7670
7681
|
}
|
|
@@ -7672,66 +7683,67 @@ async function applyPreStepPacing(page, step, context) {
|
|
|
7672
7683
|
if (!shouldMoveCursor || !step.target) return;
|
|
7673
7684
|
const center = await getTargetCenter(page, step.target);
|
|
7674
7685
|
if (!center) return;
|
|
7675
|
-
const moveMs = resolvePacedDelay(step.mouseMoveMs ??
|
|
7686
|
+
const moveMs = resolvePacedDelay(step.mouseMoveMs ?? runtime.cursorMoveMs, context, runtime, "move");
|
|
7676
7687
|
if (moveMs > 0) {
|
|
7677
|
-
await moveCursor(page, center, moveMs);
|
|
7688
|
+
await moveCursor(page, center, moveMs, runtime);
|
|
7678
7689
|
}
|
|
7679
|
-
const highlightMs = resolvePacedDelay(step.highlightMs ??
|
|
7690
|
+
const highlightMs = resolvePacedDelay(step.highlightMs ?? runtime.cursorHighlightMs, context, runtime, "highlight");
|
|
7680
7691
|
if (highlightMs > 0) {
|
|
7681
7692
|
await wait(highlightMs);
|
|
7682
7693
|
}
|
|
7683
7694
|
}
|
|
7684
|
-
async function applyPostStepPacing(step, context) {
|
|
7685
|
-
const postDelay = resolvePacedDelay(step.postDelayMs, context, "post");
|
|
7695
|
+
async function applyPostStepPacing(step, context, runtime) {
|
|
7696
|
+
const postDelay = resolvePacedDelay(step.postDelayMs ?? runtime.stepPostDelayMs, context, runtime, "post");
|
|
7686
7697
|
if (postDelay > 0) {
|
|
7687
7698
|
await wait(postDelay);
|
|
7688
7699
|
}
|
|
7689
|
-
const dwellDelay = resolvePacedDelay(step.dwellMs, context, "dwell");
|
|
7700
|
+
const dwellDelay = resolvePacedDelay(step.dwellMs ?? runtime.stepDwellMs, context, runtime, "dwell");
|
|
7690
7701
|
if (dwellDelay > 0) {
|
|
7691
7702
|
await wait(dwellDelay);
|
|
7692
7703
|
}
|
|
7693
7704
|
}
|
|
7694
7705
|
async function executeStep(page, step, runDir, baseUrl, context = {}) {
|
|
7695
7706
|
const timeout = step.timeoutMs ?? 6e3;
|
|
7696
|
-
|
|
7707
|
+
const runtime = resolveRuntimePacingDefaults();
|
|
7708
|
+
await applyPreStepPacing(page, step, context, runtime);
|
|
7697
7709
|
if (step.action === "goto") {
|
|
7698
7710
|
const target = step.value ?? "/";
|
|
7699
7711
|
const isAbsolute = /^https?:\/\//.test(target);
|
|
7700
7712
|
await page.goto(isAbsolute ? target : new URL(target, baseUrl).toString(), { timeout });
|
|
7701
|
-
await applyPostStepPacing(step, context);
|
|
7713
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7702
7714
|
return;
|
|
7703
7715
|
}
|
|
7704
7716
|
if (step.action === "click") {
|
|
7705
7717
|
if (!step.target) throw new Error(`Step ${step.id} missing target`);
|
|
7706
7718
|
await page.locator(step.target).first().click({ timeout });
|
|
7707
|
-
await clickPulse(page);
|
|
7708
|
-
await applyPostStepPacing(step, context);
|
|
7719
|
+
await clickPulse(page, runtime);
|
|
7720
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7709
7721
|
return;
|
|
7710
7722
|
}
|
|
7711
7723
|
if (step.action === "type") {
|
|
7712
7724
|
if (!step.target) throw new Error(`Step ${step.id} missing target`);
|
|
7713
7725
|
const locator = page.locator(step.target).first();
|
|
7714
|
-
if (realisticTyping) {
|
|
7726
|
+
if (runtime.realisticTyping) {
|
|
7715
7727
|
await locator.click({ timeout });
|
|
7716
|
-
await clickPulse(page);
|
|
7728
|
+
await clickPulse(page, runtime);
|
|
7717
7729
|
await locator.fill("", { timeout });
|
|
7718
|
-
const effectiveTypingDelay = resolvePacedDelay(typingDelayMs, context, "typing", false);
|
|
7730
|
+
const effectiveTypingDelay = resolvePacedDelay(runtime.typingDelayMs, context, runtime, "typing", false);
|
|
7719
7731
|
await page.keyboard.type(step.value ?? "", { delay: effectiveTypingDelay });
|
|
7720
7732
|
} else {
|
|
7721
7733
|
await locator.fill(step.value ?? "", { timeout });
|
|
7722
7734
|
}
|
|
7723
|
-
await applyPostStepPacing(step, context);
|
|
7735
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7724
7736
|
return;
|
|
7725
7737
|
}
|
|
7726
7738
|
if (step.action === "wait_for") {
|
|
7727
7739
|
if (step.target) {
|
|
7728
7740
|
await page.locator(step.target).first().waitFor({ state: "visible", timeout });
|
|
7729
|
-
await applyPostStepPacing(step, context);
|
|
7741
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7730
7742
|
return;
|
|
7731
7743
|
}
|
|
7732
7744
|
if (step.value) {
|
|
7733
7745
|
await page.getByText(step.value).first().waitFor({ timeout });
|
|
7734
|
-
await applyPostStepPacing(step, context);
|
|
7746
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7735
7747
|
return;
|
|
7736
7748
|
}
|
|
7737
7749
|
throw new Error(`Step ${step.id} requires target or value for wait_for`);
|
|
@@ -7739,23 +7751,23 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
|
|
|
7739
7751
|
if (step.action === "assert_text") {
|
|
7740
7752
|
if (!step.value) throw new Error(`Step ${step.id} missing value`);
|
|
7741
7753
|
await assertText(page, step.value, timeout);
|
|
7742
|
-
await applyPostStepPacing(step, context);
|
|
7754
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7743
7755
|
return;
|
|
7744
7756
|
}
|
|
7745
7757
|
if (step.action === "assert_visible") {
|
|
7746
7758
|
if (!step.target) throw new Error(`Step ${step.id} missing target`);
|
|
7747
7759
|
await assertVisible(page, step.target, timeout);
|
|
7748
|
-
await applyPostStepPacing(step, context);
|
|
7760
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7749
7761
|
return;
|
|
7750
7762
|
}
|
|
7751
7763
|
if (step.action === "screenshot") {
|
|
7752
7764
|
const name = step.value ?? `${step.id}.png`;
|
|
7753
7765
|
await page.screenshot({ path: `${runDir}/screenshots/${name}.png`, fullPage: true });
|
|
7754
|
-
await applyPostStepPacing(step, context);
|
|
7766
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7755
7767
|
return;
|
|
7756
7768
|
}
|
|
7757
7769
|
if (["recorder_start", "recorder_stop", "recorder_export"].includes(step.action)) {
|
|
7758
|
-
await applyPostStepPacing(step, context);
|
|
7770
|
+
await applyPostStepPacing(step, context, runtime);
|
|
7759
7771
|
return;
|
|
7760
7772
|
}
|
|
7761
7773
|
throw new Error(`Unsupported action: ${step.action}`);
|
|
@@ -11982,9 +11994,12 @@ import fs6 from "node:fs/promises";
|
|
|
11982
11994
|
import path4 from "node:path";
|
|
11983
11995
|
|
|
11984
11996
|
// ../../packages/artifacts/src/paths.ts
|
|
11997
|
+
import os2 from "node:os";
|
|
11985
11998
|
import path2 from "node:path";
|
|
11986
11999
|
function getRunsRoot() {
|
|
11987
|
-
const
|
|
12000
|
+
const dataDir = process.env.STUDIOFLOW_DATA_DIR ?? process.env.STUDIOFLOW_HOME;
|
|
12001
|
+
const defaultRunsDir = dataDir && dataDir.trim() ? path2.join(path2.resolve(dataDir), "runs") : path2.join(os2.homedir(), ".studioflow", "runs");
|
|
12002
|
+
const configured = process.env.STUDIOFLOW_RUNS_DIR || defaultRunsDir;
|
|
11988
12003
|
if (path2.isAbsolute(configured)) {
|
|
11989
12004
|
return configured;
|
|
11990
12005
|
}
|
|
@@ -12224,6 +12239,7 @@ async function withRetry(fn, opts = {}) {
|
|
|
12224
12239
|
async function runEngine(input) {
|
|
12225
12240
|
const run2 = await createRunContext();
|
|
12226
12241
|
let state = "INIT";
|
|
12242
|
+
const shouldExport = input.flows.some((flow) => flow.steps.some((step) => step.action === "recorder_export"));
|
|
12227
12243
|
const files = {
|
|
12228
12244
|
events: run2.eventsFile
|
|
12229
12245
|
};
|
|
@@ -12273,10 +12289,12 @@ async function runEngine(input) {
|
|
|
12273
12289
|
await emit("recorder.stop.begin");
|
|
12274
12290
|
await stopRecording();
|
|
12275
12291
|
await emit("recorder.stop.done");
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12292
|
+
if (shouldExport) {
|
|
12293
|
+
state = "EXPORT";
|
|
12294
|
+
await emit("recorder.export.begin");
|
|
12295
|
+
await exportRecording();
|
|
12296
|
+
await emit("recorder.export.done");
|
|
12297
|
+
}
|
|
12280
12298
|
state = "VERIFY_ARTIFACTS";
|
|
12281
12299
|
const planPath = path4.join(run2.runDir, "plan.json");
|
|
12282
12300
|
await writeJsonFile(planPath, {
|
|
@@ -12354,18 +12372,18 @@ function resolveFromWorkspace(inputPath) {
|
|
|
12354
12372
|
}
|
|
12355
12373
|
|
|
12356
12374
|
// src/commands/runtime-paths.ts
|
|
12357
|
-
import
|
|
12375
|
+
import os3 from "node:os";
|
|
12358
12376
|
import path6 from "node:path";
|
|
12359
12377
|
function getStudioflowDataDir() {
|
|
12360
12378
|
const configured = process.env.STUDIOFLOW_DATA_DIR ?? process.env.STUDIOFLOW_HOME;
|
|
12361
12379
|
if (configured && configured.trim()) {
|
|
12362
12380
|
return path6.resolve(configured);
|
|
12363
12381
|
}
|
|
12364
|
-
return path6.join(
|
|
12382
|
+
return path6.join(os3.homedir(), ".studioflow");
|
|
12365
12383
|
}
|
|
12366
12384
|
function resolveAgentHome(envVar, defaultDir) {
|
|
12367
12385
|
const configured = process.env[envVar];
|
|
12368
|
-
return configured && configured.trim() ? path6.resolve(configured) : path6.join(
|
|
12386
|
+
return configured && configured.trim() ? path6.resolve(configured) : path6.join(os3.homedir(), defaultDir);
|
|
12369
12387
|
}
|
|
12370
12388
|
function getCodexSkillsDir() {
|
|
12371
12389
|
return path6.join(resolveAgentHome("CODEX_HOME", ".codex"), "skills");
|
|
@@ -12511,7 +12529,7 @@ async function resolveRuntimeConfig(overrides = {}) {
|
|
|
12511
12529
|
{ value: project.config.runsDir, source: "project-config" },
|
|
12512
12530
|
{ value: user.config.runsDir, source: "user-config" }
|
|
12513
12531
|
],
|
|
12514
|
-
"
|
|
12532
|
+
path7.join(getStudioflowDataDir(), "runs")
|
|
12515
12533
|
);
|
|
12516
12534
|
validateBaseUrl(baseUrl.value);
|
|
12517
12535
|
return {
|