@flowriki/agent 0.1.0-beta.23 → 0.1.0-beta.25

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.
@@ -6,7 +6,7 @@ import os from "os";
6
6
  import path from "path";
7
7
  import { createRequire } from "module";
8
8
  import { spawnSync } from "child_process";
9
- var AGENT_VERSION = "0.1.0-beta.23";
9
+ var AGENT_VERSION = "0.1.0-beta.25";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -135,7 +135,7 @@ function forgetConnection(service, env = process.env) {
135
135
  }
136
136
  var fingerprint = (secret) => crypto2.createHash("sha256").update(secret).digest("hex").slice(0, 12);
137
137
 
138
- // ../contracts/dist/chunk-BZ2CJR5T.js
138
+ // ../contracts/dist/chunk-MQTBBCDK.js
139
139
  import { z } from "zod";
140
140
  var PROTOCOL_VERSION = 1;
141
141
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -290,7 +290,10 @@ var AGENT_FEATURES = /* @__PURE__ */ new Set([
290
290
  "authentication.v1",
291
291
  /** More than one participant, each with a browser session of its own —
292
292
  * which also covers starting one over and sharing a captured output. */
293
- "actors.v1"
293
+ "actors.v1",
294
+ /** Replaying other flows' steps before a recording starts capturing, so a
295
+ * flow can be recorded from where its prerequisites leave the browser. */
296
+ "prerequisites.v1"
294
297
  ]);
295
298
  var AgentMessageSchema = z.discriminatedUnion("type", [
296
299
  AckSchema,
@@ -458,7 +461,7 @@ import path7 from "path";
458
461
  // ../engine-playwright/dist/index.js
459
462
  import path3 from "path";
460
463
 
461
- // ../test-model/dist/chunk-X4X4KGMA.js
464
+ // ../test-model/dist/chunk-TSFRTYLG.js
462
465
  import { z as z3 } from "zod";
463
466
  var LocatorOverrideSchema = z3.object({
464
467
  strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -759,6 +762,12 @@ var ActorDefinitionSchema = z3.object({
759
762
  name: z3.string().min(1),
760
763
  startSignedIn: z3.object({ profileId: z3.string().min(1), accountId: z3.string().optional() }).strict().optional()
761
764
  }).strict();
765
+ var FlowSignInSchema = z3.object({
766
+ profileId: z3.string().min(1),
767
+ accountId: z3.string().optional(),
768
+ from: z3.enum(["loginPage", "here"]).optional()
769
+ }).strict();
770
+ var FlowPrerequisitesSchema = z3.array(z3.object({ flowId: z3.string().min(1) }).strict());
762
771
  var CURRENT_SCHEMA_VERSION = 5;
763
772
  var FlowDocumentSchema = z3.object({
764
773
  schemaVersion: z3.literal(CURRENT_SCHEMA_VERSION),
@@ -800,6 +809,24 @@ var FlowDocumentSchema = z3.object({
800
809
  path: z3.string().optional(),
801
810
  actors: z3.array(ActorDefinitionSchema).optional(),
802
811
  defaultActorId: z3.string().optional(),
812
+ /** SIGN IN FIRST: the account this flow starts as, before anything else runs.
813
+ *
814
+ * A setting rather than step one, which is what it used to be saved as. As a
815
+ * step it could not be switched off without deleting it, and a flow reused
816
+ * as somebody else's prerequisite could not tell "my own sign-in" from a
817
+ * sign-in that is part of what it tests. `parseFlowDocument` lifts an old
818
+ * step one into this field, so both spellings read the same. A reference to
819
+ * a profile, never a copy of its steps. */
820
+ signIn: FlowSignInSchema.optional(),
821
+ /** Flows replayed, in this order, after signing in and before this flow's
822
+ * own steps — to reach the state it starts from.
823
+ *
824
+ * LINKS, NOT COPIES: re-record "Save basic info" once and every flow that
825
+ * starts after it follows. Each runs WITHOUT its own sign-in; this flow's
826
+ * is the only one. Explicit and flat — nothing here pulls in a
827
+ * prerequisite's prerequisites at run time, so what runs is exactly this
828
+ * list. The editor fills those in when a flow is added. */
829
+ prerequisites: FlowPrerequisitesSchema.optional(),
803
830
  /** Bumped on every save. An edit carries the revision it was made against,
804
831
  * so a save built on a stale copy is refused instead of overwriting work
805
832
  * somebody else — or a debug session on the same machine — has since done.
@@ -895,8 +922,19 @@ function parseFlowDocument(input) {
895
922
  version += 1;
896
923
  raw.schemaVersion = version;
897
924
  }
925
+ liftSignInFirst(raw);
898
926
  return FlowDocumentSchema.parse(raw);
899
927
  }
928
+ var SIGN_IN_FIRST_STEP_ID = "sign-in-first";
929
+ function liftSignInFirst(raw) {
930
+ const first = raw.steps?.[0];
931
+ const auth = first?.auth;
932
+ if (!first || first.type !== "signIn" || first.disabled === true || first.actorId !== void 0) return;
933
+ if (!auth || typeof auth.profileId !== "string" || !auth.profileId) return;
934
+ if (raw["signIn"] !== void 0 || Array.isArray(raw["actors"]) && raw["actors"].length > 0) return;
935
+ raw["signIn"] = auth;
936
+ raw.steps = raw.steps.slice(1);
937
+ }
900
938
 
901
939
  // ../test-model/dist/index.js
902
940
  import { z as z4 } from "zod";
@@ -1015,7 +1053,25 @@ var FlowOccurrenceSchema = z22.object({
1015
1053
  * against the source can line the two up; nothing depends on it to run. */
1016
1054
  stepIds: z22.record(z22.string(), z22.string()).default({}),
1017
1055
  copiedAt: z22.string()
1018
- })
1056
+ }),
1057
+ /** WHICH OF THIS ROW'S STEPS ARE WHAT RUNS BEFORE ITS FLOW — the flow's
1058
+ * sign-in and the prerequisites copied in with it — so the row can be shown
1059
+ * folded, the way the Flows screen shows the same flow.
1060
+ *
1061
+ * A description of steps the row already holds, and nothing more: they are
1062
+ * ordinary copies in `steps` and run in order whatever this says. Kept by
1063
+ * id, so editing or deleting one of them leaves the rest still grouped.
1064
+ * Absent on every row copied before this existed, which simply show flat.
1065
+ * Never sent to a machine — see `withoutFolding`. */
1066
+ before: z22.object({
1067
+ /** The flow's own sign-in, when the copy brought one. */
1068
+ signInStepId: z22.string().optional(),
1069
+ flows: z22.array(z22.object({
1070
+ flowId: z22.string(),
1071
+ name: z22.string(),
1072
+ stepIds: z22.array(z22.string())
1073
+ }).strict()).default([])
1074
+ }).strict().optional()
1019
1075
  }).strict();
1020
1076
  var CASE_SCHEMA_VERSION = 1;
1021
1077
  var TestCaseSchema = z22.object({
@@ -1177,6 +1233,65 @@ function sessionLanes(occurrences, defaultActorId) {
1177
1233
  }
1178
1234
  return rows;
1179
1235
  }
1236
+ function randomUUID() {
1237
+ const crypto5 = globalThis.crypto;
1238
+ if (crypto5?.randomUUID) return crypto5.randomUUID();
1239
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 14)}`;
1240
+ }
1241
+ var mintPage = (remap, pageId) => {
1242
+ if (pageId === "main") return "main";
1243
+ const existing = remap.pages.get(pageId);
1244
+ if (existing) return existing;
1245
+ const fresh = `pg_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
1246
+ remap.pages.set(pageId, fresh);
1247
+ return fresh;
1248
+ };
1249
+ function retarget(target, remap) {
1250
+ if (!target.pageId) return target;
1251
+ return { ...target, pageId: mintPage(remap, target.pageId) };
1252
+ }
1253
+ function copyStep(step, remap) {
1254
+ const copied = retarget({ ...step, id: remap.steps.get(step.id) }, remap);
1255
+ if (copied.opensPage) copied.opensPage = mintPage(remap, copied.opensPage);
1256
+ if (copied.to) copied.to = retarget({ ...copied.to }, remap);
1257
+ if (copied.scroll?.container) {
1258
+ copied.scroll = { ...copied.scroll, container: retarget({ ...copied.scroll.container }, remap) };
1259
+ }
1260
+ copied.proposals = step.proposals.map((assertion) => ({ ...assertion }));
1261
+ return copied;
1262
+ }
1263
+ function copyFlowIntoCase(flow, context) {
1264
+ const remap = { steps: /* @__PURE__ */ new Map(), pages: /* @__PURE__ */ new Map() };
1265
+ for (const step of flow.steps) {
1266
+ remap.steps.set(step.id, `st_${randomUUID().replace(/-/g, "").slice(0, 12)}`);
1267
+ }
1268
+ const steps = flow.steps.map((step) => copyStep(step, remap));
1269
+ return {
1270
+ occurrenceId: `occ_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
1271
+ name: flow.name,
1272
+ // WHO RUNS THIS COPY. The flow said which of its participants it belongs
1273
+ // to; dropping that here made every copy run as the main participant, so a
1274
+ // flow recorded as the approver silently became the buyer doing approvals.
1275
+ // The case has to be given the participant too — see `mergeParticipants`.
1276
+ ...flow.defaultActorId && flow.defaultActorId !== "default" ? { actorId: flow.defaultActorId } : {},
1277
+ ...flow.baseUrl ? { baseUrl: flow.baseUrl } : {},
1278
+ startPath: flow.startPath,
1279
+ navigateFirst: false,
1280
+ steps,
1281
+ ...flow.environment ? { environment: flow.environment } : {},
1282
+ origin: {
1283
+ flowId: flow.id,
1284
+ schemaVersion: flow.schemaVersion,
1285
+ revision: flow.revision,
1286
+ ...context.commit ? { commit: context.commit } : {},
1287
+ featureId: flow.featureId,
1288
+ featureName: context.featureName,
1289
+ flowName: flow.name,
1290
+ stepIds: Object.fromEntries(remap.steps),
1291
+ copiedAt: (context.at ?? /* @__PURE__ */ new Date()).toISOString()
1292
+ }
1293
+ };
1294
+ }
1180
1295
  function caseDebugPlan(testCase, baseUrl) {
1181
1296
  const owners = {};
1182
1297
  const steps = [];
@@ -1396,10 +1511,89 @@ function sameIdentity(a, b2) {
1396
1511
  if (a.accountId && b2.accountId) return a.accountId === b2.accountId;
1397
1512
  return true;
1398
1513
  }
1514
+ var PrerequisiteProblem = class extends Error {
1515
+ constructor(message) {
1516
+ super(message);
1517
+ this.name = "PrerequisiteProblem";
1518
+ }
1519
+ };
1520
+ function signInFirstStep(flow, id = SIGN_IN_FIRST_STEP_ID) {
1521
+ if (!flow.signIn) return null;
1522
+ return {
1523
+ id,
1524
+ type: "signIn",
1525
+ auth: flow.signIn,
1526
+ name: "Sign in first",
1527
+ role: "",
1528
+ scope: "",
1529
+ value: "",
1530
+ secret: false,
1531
+ legacySelector: "",
1532
+ proposals: []
1533
+ };
1534
+ }
1535
+ function stepsWithoutSignIn(flow) {
1536
+ const first = flow.steps[0];
1537
+ return first && first.type === "signIn" && !first.disabled ? flow.steps.slice(1) : flow.steps;
1538
+ }
1539
+ var idsOf = (flow) => (flow.prerequisites ?? []).map((p) => p.flowId);
1540
+ function prerequisiteProblems(flow, lookup) {
1541
+ const problems = [];
1542
+ const seen = /* @__PURE__ */ new Set();
1543
+ for (const id of idsOf(flow)) {
1544
+ if (id === flow.id) {
1545
+ problems.push(`"${flow.name}" cannot run before itself`);
1546
+ continue;
1547
+ }
1548
+ if (seen.has(id)) {
1549
+ problems.push(`"${lookup(id)?.name ?? id}" is listed twice before "${flow.name}"`);
1550
+ continue;
1551
+ }
1552
+ seen.add(id);
1553
+ const prerequisite = lookup(id);
1554
+ if (!prerequisite) {
1555
+ problems.push(`a flow "${flow.name}" runs after no longer exists \u2014 remove it from the list`);
1556
+ continue;
1557
+ }
1558
+ if (reaches(prerequisite, flow.id, lookup, /* @__PURE__ */ new Set())) {
1559
+ problems.push(`"${prerequisite.name}" already runs after "${flow.name}", so it cannot also run before it`);
1560
+ }
1561
+ }
1562
+ return problems;
1563
+ }
1564
+ function reaches(from, target, lookup, visited) {
1565
+ if (visited.has(from.id)) return false;
1566
+ visited.add(from.id);
1567
+ return idsOf(from).some((id) => {
1568
+ if (id === target) return true;
1569
+ const next = lookup(id);
1570
+ return next ? reaches(next, target, lookup, visited) : false;
1571
+ });
1572
+ }
1573
+ function prerequisitesOf(flow, lookup) {
1574
+ const problems = prerequisiteProblems(flow, lookup);
1575
+ if (problems.length > 0) throw new PrerequisiteProblem(problems.join("; "));
1576
+ return idsOf(flow).map((id) => lookup(id));
1577
+ }
1578
+ function stepsForHost(prerequisite) {
1579
+ return copyFlowIntoCase({ ...prerequisite, steps: stepsWithoutSignIn(prerequisite) }, { featureName: "" }).steps;
1580
+ }
1581
+ function preparationFor(flow, lookup) {
1582
+ return {
1583
+ signIn: signInFirstStep(flow),
1584
+ steps: prerequisitesOf(flow, lookup).flatMap(stepsForHost)
1585
+ };
1586
+ }
1587
+ function expandForRun(flow, lookup) {
1588
+ const { signIn: _signIn, prerequisites: _prerequisites, ...rest } = flow;
1589
+ if (!flow.signIn && idsOf(flow).length === 0) return rest;
1590
+ const { signIn, steps } = preparationFor(flow, lookup);
1591
+ return { ...rest, steps: [...signIn ? [signIn] : [], ...steps, ...flow.steps] };
1592
+ }
1399
1593
 
1400
1594
  // ../engine-playwright/dist/index.js
1401
1595
  import { createHmac, randomBytes } from "crypto";
1402
- import { errors } from "playwright";
1596
+ import { errors as errors2 } from "playwright";
1403
1597
 
1404
1598
  // ../core/dist/index.js
1405
1599
  function proposeAssertions(before, after) {
@@ -1720,7 +1914,7 @@ var STEP_PRESENTATION = {
1720
1914
  // ../engine-playwright/dist/index.js
1721
1915
  import fs4 from "fs";
1722
1916
  import path22 from "path";
1723
- import { chromium, firefox, webkit } from "playwright";
1917
+ import { chromium, errors, firefox, webkit } from "playwright";
1724
1918
 
1725
1919
  // ../recorder/dist/index.js
1726
1920
  function openShadowRoots() {
@@ -3747,38 +3941,63 @@ function resolveCapture(options = {}) {
3747
3941
  };
3748
3942
  }
3749
3943
  var forcedHeadless = () => process.env["FLOWRIKI_FORCE_HEADLESS"] === "1";
3944
+ var LAUNCH_ATTEMPT_MS = () => Number(process.env["FLOWRIKI_LAUNCH_TIMEOUT_MS"] ?? 45e3);
3945
+ var isStartupFailure = (error) => error instanceof errors.TimeoutError || /Target page, context or browser has been closed|Browser has been closed|browser has disconnected/i.test(String(error?.message ?? error));
3946
+ async function launchWithRetry(launch, setUp, { attempts = 2, pauseMs = 1e3 } = {}) {
3947
+ let last;
3948
+ for (let attempt = 1; attempt <= attempts; attempt++) {
3949
+ let browser = null;
3950
+ try {
3951
+ browser = await launch(LAUNCH_ATTEMPT_MS());
3952
+ return { browser, value: await setUp(browser) };
3953
+ } catch (error) {
3954
+ last = error;
3955
+ await browser?.close().catch(() => {
3956
+ });
3957
+ if (!isStartupFailure(error)) throw error;
3958
+ if (attempt < attempts) await new Promise((r) => setTimeout(r, pauseMs));
3959
+ }
3960
+ }
3961
+ const seconds = Math.round(LAUNCH_ATTEMPT_MS() / 1e3);
3962
+ const why = String(last?.message ?? last).split("\n")[0];
3963
+ throw new Error(
3964
+ `the browser did not start after ${attempts} attempts of ${seconds}s each (${why}) \u2014 on a Mac this is usually a visible browser that could not open a window; run it again, or run it headless`
3965
+ );
3966
+ }
3750
3967
  async function launchSession(environment = {}, headlessRequested = true, slowMo = 0, capture = {}, existingBrowser, storageState) {
3751
3968
  const headless = headlessRequested || forcedHeadless();
3752
3969
  const config = { ...DEFAULT_ENVIRONMENT, ...ExecutionEnvironmentSchema.parse(environment) };
3753
3970
  const fullWindow = config.fullWindow === true && !headless;
3754
3971
  const engine = { chromium, firefox, webkit }[config.browser];
3755
- const browser = existingBrowser ?? await engine.launch({
3972
+ const launch = (timeout) => engine.launch({
3756
3973
  headless,
3757
3974
  slowMo,
3975
+ timeout,
3758
3976
  // Chromium's flag. Firefox and WebKit have no equivalent, and `viewport:
3759
3977
  // null` still gives them a page that fills their default window — bigger
3760
3978
  // than the fixed box, if not maximised.
3761
3979
  ...fullWindow && config.browser === "chromium" ? { args: ["--start-maximized"] } : {}
3762
3980
  });
3981
+ const { viewport, locale, timezoneId, colorScheme } = config;
3982
+ const artifacts = resolveCapture(capture);
3983
+ const makeContext = (browser2) => browser2.newContext({
3984
+ viewport: fullWindow ? null : viewport,
3985
+ locale,
3986
+ timezoneId,
3987
+ colorScheme,
3988
+ ...storageState ? { storageState } : {},
3989
+ // Playwright can only be told to record at context creation, so a video
3990
+ // wanted "on failure" is recorded for every run and thrown away when the
3991
+ // run passes. There is no way to start one after the fact.
3992
+ //
3993
+ // The video still needs a fixed size even when the page has none —
3994
+ // `size` is what the file is encoded at, and Playwright scales the frame
3995
+ // to fit it. The configured viewport is the closest thing to an intended
3996
+ // aspect ratio we have.
3997
+ ...artifacts.video !== "off" && capture.dir ? { recordVideo: { dir: capture.dir, size: viewport } } : {}
3998
+ });
3999
+ const { browser, value: context } = existingBrowser ? { browser: existingBrowser, value: await makeContext(existingBrowser) } : await launchWithRetry(launch, makeContext);
3763
4000
  try {
3764
- const { viewport, locale, timezoneId, colorScheme } = config;
3765
- const artifacts = resolveCapture(capture);
3766
- const context = await browser.newContext({
3767
- viewport: fullWindow ? null : viewport,
3768
- locale,
3769
- timezoneId,
3770
- colorScheme,
3771
- ...storageState ? { storageState } : {},
3772
- // Playwright can only be told to record at context creation, so a video
3773
- // wanted "on failure" is recorded for every run and thrown away when the
3774
- // run passes. There is no way to start one after the fact.
3775
- //
3776
- // The video still needs a fixed size even when the page has none —
3777
- // `size` is what the file is encoded at, and Playwright scales the frame
3778
- // to fit it. The configured viewport is the closest thing to an intended
3779
- // aspect ratio we have.
3780
- ...artifacts.video !== "off" && capture.dir ? { recordVideo: { dir: capture.dir, size: viewport } } : {}
3781
- });
3782
4001
  if (artifacts.trace !== "off" && capture.dir) {
3783
4002
  await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
3784
4003
  }
@@ -4388,6 +4607,26 @@ var checkStep = (id, proposals) => ({
4388
4607
  legacySelector: "",
4389
4608
  proposals
4390
4609
  });
4610
+ async function replayInto(target, steps, opts) {
4611
+ const session = {
4612
+ ownsBrowser: false,
4613
+ browser: target.browser,
4614
+ context: target.context,
4615
+ page: target.page,
4616
+ pages: new ExecutionPages(target.context, target.page),
4617
+ config: target.config,
4618
+ artifacts: resolveCapture({ policy: { screenshot: "off", video: "off", trace: "off", console: false } }),
4619
+ consoleLines: [],
4620
+ runArtifacts: []
4621
+ };
4622
+ const outcome = await runStepsIn(session, steps, {
4623
+ startUrl: null,
4624
+ ...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
4625
+ ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
4626
+ ...opts.baseUrl ? { baseUrl: opts.baseUrl } : {}
4627
+ });
4628
+ return outcome.status === "passed" ? { ok: true } : { ok: false, error: outcome.error ?? "a step before recording did not complete" };
4629
+ }
4391
4630
  function opensItsOwnEntry(steps, opts) {
4392
4631
  const first = steps.find((step) => !step.disabled);
4393
4632
  if (!first || first.type !== "signIn" || !first.auth) return false;
@@ -4643,7 +4882,7 @@ async function runStepsIn(session, steps, opts) {
4643
4882
  }
4644
4883
  } catch (err) {
4645
4884
  result.status = "failed";
4646
- result.failureKind = err.kind ?? (/strict mode violation/.test(String(err)) ? "ambiguous" : err instanceof errors.TimeoutError ? "timeout" : "unknown");
4885
+ result.failureKind = err.kind ?? (/strict mode violation/.test(String(err)) ? "ambiguous" : err instanceof errors2.TimeoutError ? "timeout" : "unknown");
4647
4886
  result.detail = firstLine2(err);
4648
4887
  }
4649
4888
  const wantsShot = artifacts.screenshot === "always" || artifacts.screenshot === "on-failure" && result.status === "failed";
@@ -5441,6 +5680,25 @@ async function startRecordingSession(opts) {
5441
5680
  throw new Error(`${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
5442
5681
  }
5443
5682
  }
5683
+ const prepared = Boolean(opts.preparation?.steps.length);
5684
+ if (opts.preparation && prepared) {
5685
+ try {
5686
+ if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded" });
5687
+ const replayed = await replayInto({ browser, context, page, config }, opts.preparation.steps, {
5688
+ resolveSecret: opts.preparation.resolveSecret,
5689
+ ...opts.preparation.resolveAuth ? { resolveAuth: opts.preparation.resolveAuth } : {},
5690
+ baseUrl: url
5691
+ });
5692
+ if (!replayed.ok) throw new Error(replayed.error);
5693
+ } catch (error) {
5694
+ const landed = page.url().replace(/^https?:\/\/[^/]+/, "");
5695
+ const title = await page.title().catch(() => "");
5696
+ stopped = true;
5697
+ await browser.close().catch(() => {
5698
+ });
5699
+ throw new Error(`the flows this one starts after did not finish: ${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
5700
+ }
5701
+ }
5444
5702
  await context.addInitScript({ content: INIT_SCRIPT });
5445
5703
  const ensureInstalled = async (p) => {
5446
5704
  try {
@@ -5493,7 +5751,7 @@ async function startRecordingSession(opts) {
5493
5751
  });
5494
5752
  });
5495
5753
  try {
5496
- if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
5754
+ if (!opts.authentication && !prepared) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
5497
5755
  else await ensureInstalled(page);
5498
5756
  } catch (err) {
5499
5757
  stopped = true;
@@ -7762,8 +8020,15 @@ var LOCAL_CHROMIUM = { id: "chromium", provider: "local", engine: "chromium" };
7762
8020
  async function executeFlow(opts) {
7763
8021
  const { store, storage = null, flowId, trigger, watch = false, onEvent } = opts;
7764
8022
  const headless = watch ? false : opts.headless ?? store.readConfig().headless;
7765
- const flow = store.getFlow(flowId);
7766
- if (!flow) throw new FlowNotRunnable(`flow not found: ${flowId}`, "not_found");
8023
+ const found = store.getFlow(flowId);
8024
+ if (!found) throw new FlowNotRunnable(`flow not found: ${flowId}`, "not_found");
8025
+ let flow = found;
8026
+ try {
8027
+ flow = expandForRun(found, (id) => store.getFlow(id));
8028
+ } catch (problem) {
8029
+ if (problem instanceof PrerequisiteProblem) throw new FlowNotRunnable(problem.message, "empty");
8030
+ throw problem;
8031
+ }
7767
8032
  if (opts.expectedRevision !== void 0 && flow.revision !== opts.expectedRevision) {
7768
8033
  throw new FlowConflict("the flow changed before execution \u2014 reload it before running", flow.revision);
7769
8034
  }
@@ -8313,6 +8578,11 @@ var RecordingCoordinator = class {
8313
8578
  resolveAuth: authenticationResolver(opts.authentications),
8314
8579
  resolveSecret: (ref) => opts.secrets?.[ref]
8315
8580
  } } : {},
8581
+ ...opts.preparation?.length ? { preparation: {
8582
+ steps: opts.preparation,
8583
+ resolveAuth: authenticationResolver(opts.authentications),
8584
+ resolveSecret: (ref) => opts.secrets?.[ref]
8585
+ } } : {},
8316
8586
  // The project's saved defaults, unless this recording asks for
8317
8587
  // something else. Without the fallback the settings screen would write
8318
8588
  // a file nothing ever read.
@@ -8757,7 +9027,7 @@ function resolveCheckouts(dirs) {
8757
9027
  return { projects, skipped };
8758
9028
  }
8759
9029
 
8760
- // ../agent-core/dist/chunk-EJDGHASQ.js
9030
+ // ../agent-core/dist/chunk-4MOK25KG.js
8761
9031
  import { WebSocket, WebSocketServer } from "ws";
8762
9032
  var HEARTBEAT_MS = 3e4;
8763
9033
  function adapt(socket) {
@@ -9827,8 +10097,10 @@ function recordingHandlers({ recorder, onActivity }) {
9827
10097
  // returned on stop for the service to commit to the repository. A
9828
10098
  // machine that has the project checked out still records into it.
9829
10099
  root,
9830
- ...command.args["authentication"] ? {
9831
- authentication: AuthReferenceSchema.parse(command.args["authentication"]),
10100
+ ...command.args["authentication"] ? { authentication: AuthReferenceSchema.parse(command.args["authentication"]) } : {},
10101
+ // The flows this recording starts after, replayed before capturing.
10102
+ ...Array.isArray(command.args["preparation"]) && command.args["preparation"].length > 0 ? { preparation: RecordedStepSchema.array().parse(command.args["preparation"]) } : {},
10103
+ ...command.args["authentication"] || command.args["preparation"] ? {
9832
10104
  authentications: command.args["authentications"],
9833
10105
  secrets: command.args["secrets"] ?? {}
9834
10106
  } : {},
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-5J5D7IIT.js";
11
+ } from "./chunk-WJ643WHC.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { parseArgs } from "util";
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-5J5D7IIT.js";
12
+ } from "./chunk-WJ643WHC.js";
13
13
  export {
14
14
  AGENT_VERSION,
15
15
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.23",
3
+ "version": "0.1.0-beta.25",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",