@flowriki/agent 0.1.0-beta.17 → 0.1.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NTARVVKX.js → chunk-K6L6NDH6.js} +287 -98
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
9
|
+
var AGENT_VERSION = "0.1.0-beta.19";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -458,7 +458,7 @@ import path7 from "path";
|
|
|
458
458
|
// ../engine-playwright/dist/index.js
|
|
459
459
|
import path3 from "path";
|
|
460
460
|
|
|
461
|
-
// ../test-model/dist/chunk-
|
|
461
|
+
// ../test-model/dist/chunk-ZRUZU4TI.js
|
|
462
462
|
import { z as z3 } from "zod";
|
|
463
463
|
var LocatorOverrideSchema = z3.object({
|
|
464
464
|
strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
|
|
@@ -758,6 +758,20 @@ var FlowDocumentSchema = z3.object({
|
|
|
758
758
|
updatedAt: z3.string(),
|
|
759
759
|
steps: z3.array(RecordedStepSchema),
|
|
760
760
|
environment: ExecutionEnvironmentSchema.optional(),
|
|
761
|
+
/** WHICH DEPLOYMENT THIS WAS RECORDED AGAINST, by name — "staging",
|
|
762
|
+
* "production", whatever the project calls them.
|
|
763
|
+
*
|
|
764
|
+
* A record of fact, not a constraint: the flow can still be run against any
|
|
765
|
+
* environment, which is the point of naming them. What it answers is the
|
|
766
|
+
* question somebody asks of a test that fails only in one place — "where
|
|
767
|
+
* was this made?" — which was previously unanswerable, because the address
|
|
768
|
+
* it used belonged to a setting that had since moved on.
|
|
769
|
+
*
|
|
770
|
+
* Deliberately NOT the address. An origin in a git-tracked file is the
|
|
771
|
+
* mistake documented on `baseUrl` above; a name survives the deployment
|
|
772
|
+
* moving. Absent on everything recorded before this, and on a project that
|
|
773
|
+
* has never named its environments. */
|
|
774
|
+
recordedIn: z3.string().min(1).optional(),
|
|
761
775
|
path: z3.string().optional(),
|
|
762
776
|
actors: z3.array(ActorDefinitionSchema).optional(),
|
|
763
777
|
defaultActorId: z3.string().optional(),
|
|
@@ -1099,8 +1113,22 @@ var AuthNavigationSchema = z32.discriminatedUnion("kind", [
|
|
|
1099
1113
|
z32.object({ kind: z32.literal("open"), path: z32.string().min(1) })
|
|
1100
1114
|
]);
|
|
1101
1115
|
var AuthFieldSchema = z32.object({
|
|
1102
|
-
/** The step in the recorded procedure this value is typed into.
|
|
1116
|
+
/** The step in the recorded procedure this value is typed into. Where a
|
|
1117
|
+
* recording types into the same box more than once, this is the FIRST of
|
|
1118
|
+
* them — the one an account's values are keyed by. */
|
|
1103
1119
|
stepId: z32.string().min(1),
|
|
1120
|
+
/** EVERY step that types into this box, when there is more than one.
|
|
1121
|
+
*
|
|
1122
|
+
* A login typed twice — a first attempt with a typo, a page that re-rendered
|
|
1123
|
+
* and was filled again — records two steps for one field, because keystrokes
|
|
1124
|
+
* coalesce only with the step immediately before them. Asking an account for
|
|
1125
|
+
* "Username" twice is the visible half of that; the dangerous half is that
|
|
1126
|
+
* substituting only the first leaves the second typing whatever was
|
|
1127
|
+
* recorded, so a run signs in as the person who made the recording.
|
|
1128
|
+
*
|
|
1129
|
+
* Empty means the one step in `stepId`, so every profile written before this
|
|
1130
|
+
* reads correctly without being migrated. */
|
|
1131
|
+
stepIds: z32.array(z32.string().min(1)).default([]),
|
|
1104
1132
|
/** What it is called where somebody fills it in — the field's own name on
|
|
1105
1133
|
* the page, so an account form reads "Username" rather than "st_9f2a". */
|
|
1106
1134
|
label: z32.string().min(1),
|
|
@@ -1134,17 +1162,25 @@ var AuthProfileSchema = z32.object({
|
|
|
1134
1162
|
updatedAt: z32.string(),
|
|
1135
1163
|
revision: z32.number().int().nonnegative().default(0)
|
|
1136
1164
|
}).strict();
|
|
1137
|
-
var ACCOUNT_BINDING_SCHEMA_VERSION =
|
|
1165
|
+
var ACCOUNT_BINDING_SCHEMA_VERSION = 3;
|
|
1138
1166
|
var AccountBindingSchema = z32.object({
|
|
1139
1167
|
schemaVersion: z32.literal(ACCOUNT_BINDING_SCHEMA_VERSION),
|
|
1140
1168
|
id: z32.string().min(1),
|
|
1141
1169
|
slug: z32.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
1142
1170
|
name: z32.string().min(1),
|
|
1143
1171
|
profileId: z32.string().min(1),
|
|
1144
|
-
/** Which
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
|
|
1172
|
+
/** Which environments of the APPLICATION UNDER TEST these credentials work
|
|
1173
|
+
* in — never Flowriki's own dev or staging.
|
|
1174
|
+
*
|
|
1175
|
+
* EMPTY MEANS EVERY ONE, which is what a project with a single deployment
|
|
1176
|
+
* wants and what most accounts will always say.
|
|
1177
|
+
*
|
|
1178
|
+
* A LIST because the same person usually exists in more than one place. One
|
|
1179
|
+
* environment per account was the shape until version 3, and it forced a
|
|
1180
|
+
* duplicate account — same name, same password, same everything — for each
|
|
1181
|
+
* deployment the identity existed in, or a blank "works anywhere" that also
|
|
1182
|
+
* claimed environments it had never been created in. Neither was true. */
|
|
1183
|
+
environments: z32.array(z32.string().min(1)).default([]),
|
|
1148
1184
|
/** Step id → what this identity types there. Ordinary values: a username is
|
|
1149
1185
|
* not a credential, and keeping it here is what lets somebody read an
|
|
1150
1186
|
* account and know who it is. */
|
|
@@ -2125,6 +2161,20 @@ function installRecorder() {
|
|
|
2125
2161
|
},
|
|
2126
2162
|
true
|
|
2127
2163
|
);
|
|
2164
|
+
const SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|one[-_\s]?time[-_\s]?(code|pass)|security[-_\s]?(code|answer)|cvv|cvc|\bpin\b/i;
|
|
2165
|
+
const looksSecret = (el, type, name) => {
|
|
2166
|
+
if (type === "password") return true;
|
|
2167
|
+
const auto = String(el.autocomplete || el.getAttribute?.("autocomplete") || "").toLowerCase();
|
|
2168
|
+
if (/current-password|new-password|one-time-code/.test(auto)) return true;
|
|
2169
|
+
try {
|
|
2170
|
+
const masked = getComputedStyle(el).webkitTextSecurity;
|
|
2171
|
+
if (masked && masked !== "none") return true;
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
return SECRET_WORDS.test(
|
|
2175
|
+
[name, el.name, el.id, el.getAttribute?.("aria-label"), el.placeholder].filter(Boolean).join(" ")
|
|
2176
|
+
);
|
|
2177
|
+
};
|
|
2128
2178
|
const editedValues = /* @__PURE__ */ new WeakMap();
|
|
2129
2179
|
const onEdit = (e) => {
|
|
2130
2180
|
if (e.isComposing) return;
|
|
@@ -2155,15 +2205,16 @@ function installRecorder() {
|
|
|
2155
2205
|
const signature = JSON.stringify(values ?? value);
|
|
2156
2206
|
if (editedValues.get(el) === signature) return;
|
|
2157
2207
|
editedValues.set(el, signature);
|
|
2208
|
+
const identity = identify(el);
|
|
2158
2209
|
send({
|
|
2159
2210
|
type: type === "range" ? "setRange" : tag === "select" ? "select" : "fill",
|
|
2160
2211
|
role: roleOf(el),
|
|
2161
|
-
...
|
|
2162
|
-
...scoping(el, roleOf(el),
|
|
2212
|
+
...identity,
|
|
2213
|
+
...scoping(el, roleOf(el), identity.name),
|
|
2163
2214
|
legacySelector: cssPath(el),
|
|
2164
2215
|
value,
|
|
2165
2216
|
...values ? { values } : {},
|
|
2166
|
-
secret: type
|
|
2217
|
+
secret: looksSecret(el, type, identity.name)
|
|
2167
2218
|
});
|
|
2168
2219
|
};
|
|
2169
2220
|
document.addEventListener("input", onEdit, true);
|
|
@@ -2218,6 +2269,29 @@ function installDebugPanel() {
|
|
|
2218
2269
|
const head = first.filter((id) => !seen.has(id) && (seen.add(id), checkById(id)));
|
|
2219
2270
|
return [...head.map((id) => checkById(id)), ...CHECKS2.filter((c) => !seen.has(c.id))];
|
|
2220
2271
|
}
|
|
2272
|
+
const GROUP_LABELS = {
|
|
2273
|
+
state: "State",
|
|
2274
|
+
text: "Text",
|
|
2275
|
+
value: "Value",
|
|
2276
|
+
attributes: "Attributes",
|
|
2277
|
+
count: "How many",
|
|
2278
|
+
page: "The page"
|
|
2279
|
+
};
|
|
2280
|
+
function optionsHtml(options, selected) {
|
|
2281
|
+
const option = (c) => `<option value="${c.id}"${c.id === selected ? " selected" : ""}>${esc(c.label)}</option>`;
|
|
2282
|
+
const suggested = options.slice(0, 3);
|
|
2283
|
+
const rest = options.slice(3);
|
|
2284
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
2285
|
+
for (const c of rest) {
|
|
2286
|
+
const key = c.group ?? "state";
|
|
2287
|
+
const list = byGroup.get(key) ?? [];
|
|
2288
|
+
list.push(c);
|
|
2289
|
+
byGroup.set(key, list);
|
|
2290
|
+
}
|
|
2291
|
+
return (suggested.length ? `<optgroup label="Most likely">${suggested.map(option).join("")}</optgroup>` : "") + [...byGroup.entries()].map(
|
|
2292
|
+
([group, list]) => `<optgroup label="${esc(GROUP_LABELS[group] ?? group)}">${list.map(option).join("")}</optgroup>`
|
|
2293
|
+
).join("");
|
|
2294
|
+
}
|
|
2221
2295
|
function prefill(c, r) {
|
|
2222
2296
|
if (c.kind === "url") return location.pathname + location.search;
|
|
2223
2297
|
if (c.kind === "title") return document.title;
|
|
@@ -2241,9 +2315,7 @@ function installDebugPanel() {
|
|
|
2241
2315
|
const current = checkById(checkId) ?? options[0];
|
|
2242
2316
|
const target = picked?.target;
|
|
2243
2317
|
const subject = target ? `${esc(String(target.role || "element"))}${target.name ? " \xB7 <b>" + esc(String(target.name)) + "</b>" : ""}` : "the page";
|
|
2244
|
-
return `<div class="as"><div class="row"><span class="tgt">${subject}</span><span class="sp"></span>` + (picked ? '<button id="arepick">Pick another</button>' : "") + '</div><div class="row"><select id="acheck">' + options.
|
|
2245
|
-
(c) => `<option value="${c.id}"${c.id === current.id ? " selected" : ""}>${esc(c.label)}</option>`
|
|
2246
|
-
).join("") + "</select>" + (current.attr ? `<input type="text" id="aattr" placeholder="attribute" value="${esc(defaultAttr(r))}">` : "") + (current.input ? `<input type="text" id="aval" placeholder="expected" value="${esc(prefill(current, r))}">` : "") + `</div><div class="row"><label class="lb"><input type="checkbox" id="asoft"${softCheck ? " checked" : ""}>Keep going if this fails</label><span class="sp"></span><button id="aclose">Cancel</button><button id="aadd" class="primary"${trouble && !checkById(checkId)?.page ? " disabled" : ""}>Add check</button></div>` + (trouble ? `<div class="err">${esc(trouble)}</div>` : "") + (pickError ? `<div class="err">${esc(pickError)}</div>` : "") + "</div>";
|
|
2318
|
+
return `<div class="as"><div class="row"><span class="tgt">${subject}</span><span class="sp"></span>` + (picked ? '<button id="arepick">Pick another</button>' : "") + '</div><div class="row"><select id="acheck">' + optionsHtml(options, current.id) + "</select>" + (current.attr ? `<input type="text" id="aattr" placeholder="attribute" value="${esc(defaultAttr(r))}">` : "") + (current.input ? `<input type="text" id="aval" placeholder="expected" value="${esc(prefill(current, r))}">` : "") + `</div><div class="row"><label class="lb"><input type="checkbox" id="asoft"${softCheck ? " checked" : ""}>Keep going if this fails</label><span class="sp"></span><button id="aclose">Cancel</button><button id="aadd" class="primary"${trouble && !checkById(checkId)?.page ? " disabled" : ""}>Add check</button></div>` + (trouble ? `<div class="err">${esc(trouble)}</div>` : "") + (pickError ? `<div class="err">${esc(pickError)}</div>` : "") + "</div>";
|
|
2247
2319
|
}
|
|
2248
2320
|
function bindAssert() {
|
|
2249
2321
|
const el = (id) => root.getElementById(id);
|
|
@@ -2366,6 +2438,13 @@ function installDebugPanel() {
|
|
|
2366
2438
|
unresolvable: picked?.unresolvable ?? false
|
|
2367
2439
|
}),
|
|
2368
2440
|
checkOptions: () => Array.from(root.querySelectorAll("#acheck option")).map((o) => o.value),
|
|
2441
|
+
/** The dropdown's headings, in order. Thirty-one options in one layer
|
|
2442
|
+
* is a list nobody reads, and whether they are grouped is invisible to
|
|
2443
|
+
* a test that only looks at the values. */
|
|
2444
|
+
checkGroups: () => Array.from(root.querySelectorAll("#acheck optgroup")).map((g) => g.label),
|
|
2445
|
+
/** Options sitting directly under the select rather than inside a
|
|
2446
|
+
* group — the flat list this was, and the thing that must be empty. */
|
|
2447
|
+
looseCheckOptions: () => Array.from(root.getElementById("acheck")?.children ?? []).filter((c) => c.tagName === "OPTION").length,
|
|
2369
2448
|
set: (id, value) => {
|
|
2370
2449
|
const el = root.getElementById(id);
|
|
2371
2450
|
if (!el) return false;
|
|
@@ -2413,12 +2492,23 @@ function installDebugPanel() {
|
|
|
2413
2492
|
};
|
|
2414
2493
|
const STYLE = [
|
|
2415
2494
|
"<style>",
|
|
2416
|
-
":host{all:initial
|
|
2495
|
+
":host{all:initial;",
|
|
2496
|
+
"--surface:oklch(20% 0.018 285);--sunken:oklch(13% 0.013 285);",
|
|
2497
|
+
"--raised:oklch(24% 0.018 285);",
|
|
2498
|
+
"--border:oklch(29% 0.018 285);--border-strong:oklch(38% 0.022 285);",
|
|
2499
|
+
"--ink:oklch(97% 0.003 285);--muted:oklch(72% 0.012 285);--subtle:oklch(56% 0.014 285);",
|
|
2500
|
+
"--accent:oklch(70% 0.19 295);--accent-soft:oklch(29% 0.075 295);",
|
|
2501
|
+
"--accent-ink:oklch(82% 0.14 295);--accent-fg:oklch(16% 0.02 285);",
|
|
2502
|
+
"--pass:var(--pass);--pass-soft:var(--pass-soft);",
|
|
2503
|
+
"--fail:var(--fail);--fail-soft:oklch(27% 0.06 22);--fail-fg:var(--fail-fg);",
|
|
2504
|
+
"--warn:var(--warn);--warn-soft:var(--warn-soft);",
|
|
2505
|
+
"--skip:var(--skip);--skip-soft:var(--skip-soft);",
|
|
2506
|
+
"--live:oklch(72% 0.15 250)}",
|
|
2417
2507
|
".p{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);",
|
|
2418
|
-
"max-width:min(96vw,1100px);background:
|
|
2419
|
-
"border:1px solid
|
|
2508
|
+
"max-width:min(96vw,1100px);background:var(--surface);",
|
|
2509
|
+
"border:1px solid var(--border);border-radius:12px;padding:10px;",
|
|
2420
2510
|
"box-shadow:0 12px 48px oklch(0% 0 0 / 0.6);",
|
|
2421
|
-
"color:
|
|
2511
|
+
"color:var(--ink);",
|
|
2422
2512
|
"font:500 12px/1.3 'Inter var',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif}",
|
|
2423
2513
|
".p.v{top:16px;bottom:auto;left:16px;transform:none;width:300px}",
|
|
2424
2514
|
// No margin-bottom here: the gap belongs to the steps list, which is not
|
|
@@ -2428,7 +2518,7 @@ function installDebugPanel() {
|
|
|
2428
2518
|
"cursor:move;user-select:none}",
|
|
2429
2519
|
".grip{display:inline-flex;flex-direction:column;gap:2px;padding:4px 2px;opacity:.45}",
|
|
2430
2520
|
".grip i{display:block;width:12px;height:2px;border-radius:1px;",
|
|
2431
|
-
"background:
|
|
2521
|
+
"background:var(--muted)}",
|
|
2432
2522
|
".p.moved{left:auto;right:auto;bottom:auto;transform:none}",
|
|
2433
2523
|
// Collapsed: a small pill, not the full panel with nothing in it. The
|
|
2434
2524
|
// point of minimising is to get out of the way.
|
|
@@ -2441,50 +2531,55 @@ function installDebugPanel() {
|
|
|
2441
2531
|
// Discard falls off the edge.
|
|
2442
2532
|
"button{white-space:nowrap}",
|
|
2443
2533
|
".sp{flex:1}",
|
|
2534
|
+
// WHAT BELONGS WITH WHAT. Eight controls in an unbroken row is a row you
|
|
2535
|
+
// read left to right every time; a hairline between the groups — what the
|
|
2536
|
+
// run is doing, what you are adding to it, and how you leave — means the
|
|
2537
|
+
// one you want is found by position instead.
|
|
2538
|
+
".dv{width:1px;align-self:stretch;margin:2px 2px;background:var(--border);flex:0 0 auto}",
|
|
2444
2539
|
"button{font:inherit;border:0;border-radius:6px;cursor:pointer;padding:6px 10px;",
|
|
2445
|
-
"background:
|
|
2446
|
-
"border:1px solid
|
|
2447
|
-
"button:hover{background:
|
|
2448
|
-
"button.primary{background:
|
|
2449
|
-
"button.danger{background:
|
|
2540
|
+
"background:var(--raised);color:var(--ink);",
|
|
2541
|
+
"border:1px solid var(--border);transition:background .15s}",
|
|
2542
|
+
"button:hover{background:var(--sunken)}",
|
|
2543
|
+
"button.primary{background:var(--accent);color:var(--accent-fg);border-color:transparent}",
|
|
2544
|
+
"button.danger{background:var(--fail);color:var(--fail-fg);border-color:transparent}",
|
|
2450
2545
|
"button:disabled{opacity:.4;cursor:default}",
|
|
2451
2546
|
// The assertion form. A card of its own rather than controls jammed into
|
|
2452
2547
|
// the bar: it is a different job from driving the recording, and it is
|
|
2453
2548
|
// only on screen while that job is being done.
|
|
2454
2549
|
".as{margin-top:8px;padding:9px;border-radius:8px;",
|
|
2455
|
-
"background:
|
|
2550
|
+
"background:var(--raised);border:1px solid var(--border);",
|
|
2456
2551
|
"display:flex;flex-direction:column;gap:7px}",
|
|
2457
2552
|
".as .row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}",
|
|
2458
|
-
".as .tgt{font-size:11px;color:
|
|
2459
|
-
".as .tgt b{color:
|
|
2553
|
+
".as .tgt{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2554
|
+
".as .tgt b{color:var(--ink);font-weight:600}",
|
|
2460
2555
|
".as select,.as input[type=text]{font:inherit;padding:5px 7px;border-radius:6px;",
|
|
2461
|
-
"background:
|
|
2462
|
-
"border:1px solid
|
|
2556
|
+
"background:var(--sunken);color:var(--ink);",
|
|
2557
|
+
"border:1px solid var(--border);min-width:0}",
|
|
2463
2558
|
".as select{flex:1 1 150px}",
|
|
2464
2559
|
".as input[type=text]{flex:1 1 140px}",
|
|
2465
2560
|
".as .lb{display:inline-flex;align-items:center;gap:5px;font-size:11px;",
|
|
2466
|
-
"color:
|
|
2467
|
-
".as .err{color:
|
|
2468
|
-
".as .hint{font-size:11px;color:
|
|
2561
|
+
"color:var(--muted);cursor:pointer}",
|
|
2562
|
+
".as .err{color:var(--fail);font-size:11px}",
|
|
2563
|
+
".as .hint{font-size:11px;color:var(--muted)}",
|
|
2469
2564
|
".steps{display:flex;gap:4px;align-items:stretch;overflow:auto;padding:2px 0;margin-top:8px}",
|
|
2470
2565
|
".steps::-webkit-scrollbar{height:8px;width:8px}",
|
|
2471
|
-
".steps::-webkit-scrollbar-thumb{background:
|
|
2566
|
+
".steps::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:99px}",
|
|
2472
2567
|
".p.v .steps{flex-direction:column;max-height:70vh}",
|
|
2473
2568
|
// The step card, matching the Flows page: ordinal + type pill, then the
|
|
2474
2569
|
// accessible name, its scope, and its value.
|
|
2475
2570
|
".c{position:relative;display:flex;flex-direction:column;gap:5px;width:176px;flex:0 0 auto;",
|
|
2476
|
-
"background:
|
|
2571
|
+
"background:var(--raised);border:1px solid var(--border);",
|
|
2477
2572
|
"border-radius:8px;padding:9px}",
|
|
2478
2573
|
".p.v .c{width:auto}",
|
|
2479
|
-
".c.cur{border-color:
|
|
2480
|
-
".c.passed{border-color:
|
|
2481
|
-
".c.failed{border-color:
|
|
2482
|
-
".c.skipped{border-color:
|
|
2574
|
+
".c.cur{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}",
|
|
2575
|
+
".c.passed{border-color:color-mix(in oklch, var(--pass) 50%, transparent);background:color-mix(in oklch, var(--pass-soft) 55%, transparent)}",
|
|
2576
|
+
".c.failed{border-color:color-mix(in oklch, var(--fail) 60%, transparent);background:color-mix(in oklch, var(--fail-soft) 60%, transparent)}",
|
|
2577
|
+
".c.skipped{border-color:color-mix(in oklch, var(--skip) 50%, transparent);background:color-mix(in oklch, var(--skip-soft) 60%, transparent)}",
|
|
2483
2578
|
// Recorded in THIS session — the steps about to be committed. A colour of
|
|
2484
2579
|
// their own, because "new" is not a run outcome and reusing pass green or
|
|
2485
2580
|
// skip sky would say something untrue about whether they ran.
|
|
2486
|
-
".c.new{border-color:
|
|
2487
|
-
".c.new .o::after{content:' new';color:
|
|
2581
|
+
".c.new{border-color:color-mix(in oklch, var(--accent) 70%, transparent);background:color-mix(in oklch, var(--accent-soft) 80%, transparent)}",
|
|
2582
|
+
".c.new .o::after{content:' new';color:var(--accent-ink);font-size:9px}",
|
|
2488
2583
|
".c{cursor:grab}.c:active{cursor:grabbing}",
|
|
2489
2584
|
// Real sorting, not a drop hint. The card you grab is the card that moves,
|
|
2490
2585
|
// and the others slide to their new places as you go — so the list always
|
|
@@ -2493,34 +2588,34 @@ function installDebugPanel() {
|
|
|
2493
2588
|
"will-change:transform}",
|
|
2494
2589
|
".c.drag{transition:none;z-index:50;cursor:grabbing;",
|
|
2495
2590
|
"box-shadow:0 14px 36px oklch(0% 0 0 / .55);",
|
|
2496
|
-
"border-color:
|
|
2591
|
+
"border-color:var(--accent)}",
|
|
2497
2592
|
".h{display:flex;align-items:center;gap:6px}",
|
|
2498
2593
|
".o{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2499
|
-
"font-variant-numeric:tabular-nums;color:
|
|
2594
|
+
"font-variant-numeric:tabular-nums;color:var(--subtle)}",
|
|
2500
2595
|
".t{display:inline-flex;align-items:center;border-radius:999px;padding:2px 7px;",
|
|
2501
2596
|
"font-size:10px;font-weight:500}",
|
|
2502
|
-
".t.fill,.t.select{background:
|
|
2503
|
-
".t.click,.t.press{background:
|
|
2504
|
-
".t.check{background:
|
|
2505
|
-
".t.back,.t.forward{background:
|
|
2597
|
+
".t.fill,.t.select{background:var(--accent-soft);color:var(--accent-ink)}",
|
|
2598
|
+
".t.click,.t.press{background:var(--pass-soft);color:var(--pass)}",
|
|
2599
|
+
".t.check{background:var(--warn-soft);color:var(--warn)}",
|
|
2600
|
+
".t.back,.t.forward{background:var(--skip-soft);color:var(--skip)}",
|
|
2506
2601
|
".nm{font:500 12px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2507
|
-
"color:
|
|
2508
|
-
".sc{font-size:10px;color:
|
|
2602
|
+
"color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2603
|
+
".sc{font-size:10px;color:var(--subtle);overflow:hidden;",
|
|
2509
2604
|
"text-overflow:ellipsis;white-space:nowrap}",
|
|
2510
2605
|
".vl{font:500 10px ui-monospace,SFMono-Regular,Menlo,monospace;",
|
|
2511
|
-
"color:
|
|
2606
|
+
"color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2512
2607
|
".bp{position:absolute;top:7px;right:24px;width:10px;height:10px;border-radius:50%;",
|
|
2513
|
-
"border:1px solid
|
|
2514
|
-
".bp:hover{border-color:
|
|
2515
|
-
".bp.on{background:
|
|
2608
|
+
"border:1px solid var(--subtle);background:transparent;padding:0;cursor:pointer}",
|
|
2609
|
+
".bp:hover{border-color:var(--fail)}",
|
|
2610
|
+
".bp.on{background:var(--fail);border-color:var(--fail)}",
|
|
2516
2611
|
".x{position:absolute;top:4px;right:4px;background:transparent;border:0;padding:2px 4px;",
|
|
2517
|
-
"color:
|
|
2518
|
-
".x:hover{background:transparent;color:
|
|
2612
|
+
"color:var(--subtle);font-size:13px;line-height:1;cursor:pointer}",
|
|
2613
|
+
".x:hover{background:transparent;color:var(--fail)}",
|
|
2519
2614
|
".err{margin-top:8px;padding:7px 9px;border-radius:8px;",
|
|
2520
|
-
"background:
|
|
2521
|
-
"color:
|
|
2615
|
+
"background:color-mix(in oklch, var(--fail-soft) 70%, transparent);border:1px solid color-mix(in oklch, var(--fail) 45%, transparent);",
|
|
2616
|
+
"color:var(--fail);white-space:normal;max-width:640px}",
|
|
2522
2617
|
".rec{display:inline-flex;align-items:center;gap:6px}",
|
|
2523
|
-
".dot{width:7px;height:7px;border-radius:50%;background:
|
|
2618
|
+
".dot{width:7px;height:7px;border-radius:50%;background:var(--fail);",
|
|
2524
2619
|
"animation:b 1.6s infinite}",
|
|
2525
2620
|
"@keyframes b{0%,100%{opacity:1}50%{opacity:.25}}",
|
|
2526
2621
|
"</style>"
|
|
@@ -2591,16 +2686,13 @@ function installDebugPanel() {
|
|
|
2591
2686
|
}).join("");
|
|
2592
2687
|
const ran = Object.keys(state.results).length;
|
|
2593
2688
|
if (state.mode === "record") {
|
|
2594
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + (state.recording ? "running" : "paused") + '"><span class="d"></span><span>' + (state.recording ? "recording" : "paused") + '</span><span class="prog"> · ' + state.steps.length + (state.steps.length === 1 ? " step" : " steps") +
|
|
2595
|
-
// to record should not fill the flow with the steps you took to get
|
|
2596
|
-
// there.
|
|
2597
|
-
(state.recording ? '<button id="rec">Pause capture</button>' : '<button id="rec" class="primary">Resume</button>') + // Authoring a check is a first-class part of recording, not something
|
|
2689
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + (state.recording ? "running" : "paused") + '"><span class="d"></span><span>' + (state.recording ? "recording" : "paused") + '</span><span class="prog"> · ' + state.steps.length + (state.steps.length === 1 ? " step" : " steps") + '</span></span><span class="dv"></span>' + (state.recording ? '<button id="rec" title="Stop capturing while you navigate">Pause capture</button>' : '<button id="rec" class="primary" title="Capture again from here">Resume</button>') + // Authoring a check is a first-class part of recording, not something
|
|
2598
2690
|
// to go back to the Flowriki tab for — the state worth asserting on is
|
|
2599
2691
|
// frequently the one that disappears the moment you look away.
|
|
2600
2692
|
// Hidden rather than disabled while the form is open: the form has an
|
|
2601
2693
|
// Add check button of its own, and two on screen at once reads as a
|
|
2602
2694
|
// mistake even when only one of them works.
|
|
2603
|
-
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button>' + // Throwing a recording away has to be possible from HERE, in the page.
|
|
2695
|
+
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button><span class="dv"></span>' + // Throwing a recording away has to be possible from HERE, in the page.
|
|
2604
2696
|
// Without it a mis-recording had to be saved and then deleted, which
|
|
2605
2697
|
// means a wrong flow briefly exists and a wrong secret is briefly on
|
|
2606
2698
|
// disk.
|
|
@@ -2656,9 +2748,14 @@ function installDebugPanel() {
|
|
|
2656
2748
|
});
|
|
2657
2749
|
return;
|
|
2658
2750
|
}
|
|
2659
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><button id="play" class="primary" ' + (running || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry">Retry</button>' : "") + '<button id="step" ' + (running || done ? "disabled" : "") + '>Step</button><button id="pause" ' + (running ? "" : "disabled") + '>Pause</button><button id="replay" ' + (running ? "disabled" : "") +
|
|
2751
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (running || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry" title="Run this step again">Retry</button>' : "") + '<button id="step" title="Run exactly one step" ' + (running || done ? "disabled" : "") + '>Step</button><button id="pause" title="Stop after the step in flight" ' + (running ? "" : "disabled") + '>Pause</button><button id="replay" title="Start over from the first step" ' + (running ? "disabled" : "") + '>Replay</button><span class="dv"></span>' + // While armed, this is the only way to stop it — so it says so, rather
|
|
2660
2752
|
// than reading as a status label nobody realises is a button.
|
|
2661
|
-
(state.recording ? '<button id="rec" class="danger"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec"
|
|
2753
|
+
(state.recording ? '<button id="rec" class="danger" title="Stop capturing"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec" title="Capture new steps at the cursor">Record</button>') + // AUTHORING A CHECK BELONGS HERE MOST OF ALL. Recording had it and
|
|
2754
|
+
// debugging did not, which is the wrong way round: the moment worth
|
|
2755
|
+
// adding a check is while looking at a failure, when the state that
|
|
2756
|
+
// explains it is on the screen — and going back to the flow editor to
|
|
2757
|
+
// add one means describing from memory a page that has since moved on.
|
|
2758
|
+
(assertMode === "off" ? '<button id="assert" title="Check something on the page">Add check</button>' : "") + '<span class="sp"></span><button id="orient" title="Switch layout">' + (vertical ? "↔" : "↕") + '</button><button id="hide" title="Collapse">–</button><span class="dv"></span><button id="finish" class="primary" title="Keep the edits and close">Save & close</button><button id="discard" class="danger" title="Close without keeping the edits">Discard</button></div>' + assertHtml() + // Only when there is something to show. An empty .steps container is
|
|
2662
2759
|
// still 4px of padding, which left the bar with 10px above it and 14px
|
|
2663
2760
|
// below — visibly off-centre before anything has been captured.
|
|
2664
2761
|
(cards ? '<div class="steps">' + cards + "</div>" : "") + (state.failure ? '<div class="err">' + esc(state.failure.error) + "</div>" : "");
|
|
@@ -2672,6 +2769,14 @@ function installDebugPanel() {
|
|
|
2672
2769
|
on("pause", () => cmd("pause"));
|
|
2673
2770
|
on("replay", () => cmd("replay"));
|
|
2674
2771
|
on("rec", () => cmd("record", { on: !state.recording }));
|
|
2772
|
+
on("assert", () => {
|
|
2773
|
+
assertMode = "picking";
|
|
2774
|
+
picked = null;
|
|
2775
|
+
pickError = null;
|
|
2776
|
+
beginPick();
|
|
2777
|
+
render();
|
|
2778
|
+
});
|
|
2779
|
+
bindAssert();
|
|
2675
2780
|
on("finish", () => cmd("end", { commit: true }));
|
|
2676
2781
|
on("discard", () => cmd("end", { commit: false }));
|
|
2677
2782
|
on("orient", () => {
|
|
@@ -4480,6 +4585,36 @@ async function replayCase(opts) {
|
|
|
4480
4585
|
warnings
|
|
4481
4586
|
};
|
|
4482
4587
|
}
|
|
4588
|
+
async function describePick(page, payload) {
|
|
4589
|
+
try {
|
|
4590
|
+
const all = await locateAll(page, {
|
|
4591
|
+
id: "pick",
|
|
4592
|
+
type: "click",
|
|
4593
|
+
value: "",
|
|
4594
|
+
secret: false,
|
|
4595
|
+
legacySelector: "",
|
|
4596
|
+
proposals: [],
|
|
4597
|
+
scope: "",
|
|
4598
|
+
...payload.target
|
|
4599
|
+
});
|
|
4600
|
+
const handles = await all.elementHandles();
|
|
4601
|
+
if (handles.length > 1) {
|
|
4602
|
+
for (const [index, handle] of handles.entries()) {
|
|
4603
|
+
const isIt = await handle.evaluate((el) => el === window.__flPickedEl).catch(() => false);
|
|
4604
|
+
if (isIt) {
|
|
4605
|
+
payload.target["nth"] = index;
|
|
4606
|
+
break;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
if (payload.target["nth"] === void 0) payload.ambiguous = handles.length;
|
|
4610
|
+
} else if (handles.length === 0) {
|
|
4611
|
+
payload.unresolvable = true;
|
|
4612
|
+
}
|
|
4613
|
+
await Promise.all(handles.map((h) => h.dispose().catch(() => {
|
|
4614
|
+
})));
|
|
4615
|
+
} catch {
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4483
4618
|
var OUTCOME_WINDOW_MS = 2500;
|
|
4484
4619
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4485
4620
|
var COMMITS = /* @__PURE__ */ new Set([
|
|
@@ -4731,37 +4866,7 @@ async function startRecordingSession(opts) {
|
|
|
4731
4866
|
const chain = src?.frame ? frameChains.get(src.frame) : void 0;
|
|
4732
4867
|
if (chain?.length) payload.target.frame = chain;
|
|
4733
4868
|
if (src.page) payload.target.pageId = pageIds.get(src.page) ?? "main";
|
|
4734
|
-
|
|
4735
|
-
const on = src.page ?? page;
|
|
4736
|
-
const all = await locateAll(on, {
|
|
4737
|
-
id: "pick",
|
|
4738
|
-
type: "click",
|
|
4739
|
-
value: "",
|
|
4740
|
-
secret: false,
|
|
4741
|
-
legacySelector: "",
|
|
4742
|
-
proposals: [],
|
|
4743
|
-
scope: "",
|
|
4744
|
-
...payload.target
|
|
4745
|
-
});
|
|
4746
|
-
const handles = await all.elementHandles();
|
|
4747
|
-
if (handles.length > 1) {
|
|
4748
|
-
for (const [i, handle] of handles.entries()) {
|
|
4749
|
-
const isIt = await handle.evaluate((el) => el === window.__flPickedEl).catch(() => false);
|
|
4750
|
-
if (isIt) {
|
|
4751
|
-
payload.target.nth = i;
|
|
4752
|
-
break;
|
|
4753
|
-
}
|
|
4754
|
-
}
|
|
4755
|
-
if (payload.target.nth === void 0) {
|
|
4756
|
-
payload.ambiguous = handles.length;
|
|
4757
|
-
}
|
|
4758
|
-
} else if (handles.length === 0) {
|
|
4759
|
-
payload.unresolvable = true;
|
|
4760
|
-
}
|
|
4761
|
-
await Promise.all(handles.map((h) => h.dispose().catch(() => {
|
|
4762
|
-
})));
|
|
4763
|
-
} catch {
|
|
4764
|
-
}
|
|
4869
|
+
await describePick(src.page ?? page, payload);
|
|
4765
4870
|
void session.setPicking(false);
|
|
4766
4871
|
void toPanel({ picked: payload });
|
|
4767
4872
|
});
|
|
@@ -5142,6 +5247,13 @@ async function startDebugSession(opts) {
|
|
|
5142
5247
|
);
|
|
5143
5248
|
}
|
|
5144
5249
|
const targetIds = /* @__PURE__ */ new Map();
|
|
5250
|
+
const toPanel = async (msg) => {
|
|
5251
|
+
await context.pages()[0]?.evaluate(
|
|
5252
|
+
(v) => window.__flPanelMessage?.(v),
|
|
5253
|
+
msg
|
|
5254
|
+
).catch(() => {
|
|
5255
|
+
});
|
|
5256
|
+
};
|
|
5145
5257
|
async function installContext(context2) {
|
|
5146
5258
|
await context2.exposeBinding("__flRecord", (_src, payload) => {
|
|
5147
5259
|
if (!recording || !payload || payload.__sig !== void 0 || actors && _src.page.context() !== actors.current.context) return;
|
|
@@ -5190,6 +5302,17 @@ async function startDebugSession(opts) {
|
|
|
5190
5302
|
};
|
|
5191
5303
|
publish();
|
|
5192
5304
|
});
|
|
5305
|
+
await context2.exposeBinding("__flPicked", async (src, payload) => {
|
|
5306
|
+
if (payload?.cancelled) {
|
|
5307
|
+
void session.setPicking(false);
|
|
5308
|
+
void toPanel({ picked: null });
|
|
5309
|
+
return;
|
|
5310
|
+
}
|
|
5311
|
+
if (!payload?.target) return;
|
|
5312
|
+
await describePick(src.page ?? page, payload);
|
|
5313
|
+
void session.setPicking(false);
|
|
5314
|
+
void toPanel({ picked: payload });
|
|
5315
|
+
});
|
|
5193
5316
|
await context2.exposeBinding("__flCmd", (_src, msg) => {
|
|
5194
5317
|
void handleCommand(msg).catch((error) => {
|
|
5195
5318
|
failure = { stepId: cursor ?? steps[0]?.id ?? "", error: firstLine2(error) };
|
|
@@ -5197,9 +5320,12 @@ async function startDebugSession(opts) {
|
|
|
5197
5320
|
void paint(snapshot());
|
|
5198
5321
|
});
|
|
5199
5322
|
});
|
|
5200
|
-
await context2.addInitScript({
|
|
5323
|
+
await context2.addInitScript({
|
|
5324
|
+
content: `${accNameScript2()}
|
|
5325
|
+
;window.__flChecks=${JSON.stringify(CHECKS)};
|
|
5201
5326
|
;(${installRecorder.toString()})();
|
|
5202
|
-
;(${installDebugPanel.toString()})();`
|
|
5327
|
+
;(${installDebugPanel.toString()})();`
|
|
5328
|
+
});
|
|
5203
5329
|
context2.on("page", (p) => p.on("domcontentloaded", () => void paint(snapshot())));
|
|
5204
5330
|
}
|
|
5205
5331
|
await installContext(context);
|
|
@@ -5260,6 +5386,18 @@ async function startDebugSession(opts) {
|
|
|
5260
5386
|
case "record":
|
|
5261
5387
|
await session.setRecording(Boolean(msg.on));
|
|
5262
5388
|
break;
|
|
5389
|
+
case "pick":
|
|
5390
|
+
await session.setPicking(msg.on !== false);
|
|
5391
|
+
break;
|
|
5392
|
+
case "assert": {
|
|
5393
|
+
await session.setPicking(false);
|
|
5394
|
+
try {
|
|
5395
|
+
session.addAssertion(msg.assertion);
|
|
5396
|
+
} catch (err) {
|
|
5397
|
+
void toPanel({ pickError: firstLine2(err) });
|
|
5398
|
+
}
|
|
5399
|
+
break;
|
|
5400
|
+
}
|
|
5263
5401
|
case "moveStep":
|
|
5264
5402
|
session.moveStep(String(msg.stepId), Number(msg.toIndex));
|
|
5265
5403
|
break;
|
|
@@ -5451,6 +5589,42 @@ async function startDebugSession(opts) {
|
|
|
5451
5589
|
await setMuted(!on);
|
|
5452
5590
|
return publish();
|
|
5453
5591
|
},
|
|
5592
|
+
async setPicking(on) {
|
|
5593
|
+
if (on) await setMuted(true);
|
|
5594
|
+
await Promise.all(
|
|
5595
|
+
(actors?.all().flatMap((s2) => s2.context.pages()) ?? context.pages()).map(
|
|
5596
|
+
(p) => p.evaluate((v) => window.__flSetPicking?.(v), on).catch(() => {
|
|
5597
|
+
})
|
|
5598
|
+
)
|
|
5599
|
+
);
|
|
5600
|
+
if (!on) await setMuted(!recording);
|
|
5601
|
+
},
|
|
5602
|
+
addAssertion(assertion) {
|
|
5603
|
+
const parsed = RecordedAssertionSchema.parse({ enabled: true, ...assertion });
|
|
5604
|
+
const step = {
|
|
5605
|
+
id: `st_${crypto.randomUUID().slice(0, 8)}`,
|
|
5606
|
+
type: "assert",
|
|
5607
|
+
// Mirrored from the target so the step list can name what is being
|
|
5608
|
+
// checked without reaching inside the assertion to find out.
|
|
5609
|
+
role: parsed.target?.role ?? "",
|
|
5610
|
+
name: parsed.target?.name ?? "",
|
|
5611
|
+
...parsed.target?.nameFrom ? { nameFrom: parsed.target.nameFrom } : {},
|
|
5612
|
+
scope: parsed.target?.scope ?? "",
|
|
5613
|
+
...parsed.target?.scopeRole ? { scopeRole: parsed.target.scopeRole } : {},
|
|
5614
|
+
...parsed.target?.frame?.length ? { frame: parsed.target.frame } : {},
|
|
5615
|
+
...parsed.target?.locator ? { locator: parsed.target.locator } : {},
|
|
5616
|
+
value: "",
|
|
5617
|
+
secret: false,
|
|
5618
|
+
legacySelector: "",
|
|
5619
|
+
proposals: [parsed]
|
|
5620
|
+
};
|
|
5621
|
+
RecordedStepSchema.parse(step);
|
|
5622
|
+
const at = cursor ? steps.findIndex((s2) => s2.id === cursor) : steps.length;
|
|
5623
|
+
const insertAt = at === -1 ? steps.length : at;
|
|
5624
|
+
steps.splice(insertAt, 0, step);
|
|
5625
|
+
recordedInSession.add(step.id);
|
|
5626
|
+
return publish();
|
|
5627
|
+
},
|
|
5454
5628
|
async finish() {
|
|
5455
5629
|
closed = true;
|
|
5456
5630
|
status2 = "finished";
|
|
@@ -6310,7 +6484,7 @@ function toSummary(row) {
|
|
|
6310
6484
|
};
|
|
6311
6485
|
}
|
|
6312
6486
|
|
|
6313
|
-
// ../repository-store/dist/chunk-
|
|
6487
|
+
// ../repository-store/dist/chunk-A4JZPKZT.js
|
|
6314
6488
|
var FLOWRIKI_DIR = ".flowriki";
|
|
6315
6489
|
var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
|
|
6316
6490
|
var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
|
|
@@ -6351,6 +6525,7 @@ function parseConfig(raw) {
|
|
|
6351
6525
|
return {
|
|
6352
6526
|
baseUrl: typeof source["baseUrl"] === "string" ? source["baseUrl"] : "",
|
|
6353
6527
|
...typeof source["applicationEnvironment"] === "string" ? { applicationEnvironment: source["applicationEnvironment"] } : {},
|
|
6528
|
+
...Array.isArray(source["environments"]) ? { environments: readEnvironments(source["environments"]) } : {},
|
|
6354
6529
|
reuseAuthentication: source["reuseAuthentication"] === true,
|
|
6355
6530
|
headless: source["headless"] === true,
|
|
6356
6531
|
environment: ExecutionEnvironmentSchema.parse(source["environment"] ?? {}),
|
|
@@ -6358,6 +6533,20 @@ function parseConfig(raw) {
|
|
|
6358
6533
|
flakinessRepeat: clampRepeat(source["flakinessRepeat"])
|
|
6359
6534
|
};
|
|
6360
6535
|
}
|
|
6536
|
+
function readEnvironments(raw) {
|
|
6537
|
+
const out = [];
|
|
6538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6539
|
+
for (const entry of raw) {
|
|
6540
|
+
if (!entry || typeof entry !== "object") continue;
|
|
6541
|
+
const { name, baseUrl } = entry;
|
|
6542
|
+
if (typeof name !== "string" || !name.trim()) continue;
|
|
6543
|
+
const key = name.trim().toLowerCase();
|
|
6544
|
+
if (seen.has(key)) continue;
|
|
6545
|
+
seen.add(key);
|
|
6546
|
+
out.push({ name: name.trim(), baseUrl: typeof baseUrl === "string" ? baseUrl : "" });
|
|
6547
|
+
}
|
|
6548
|
+
return out;
|
|
6549
|
+
}
|
|
6361
6550
|
var FlowNotFound = class extends Error {
|
|
6362
6551
|
constructor(message) {
|
|
6363
6552
|
super(message);
|
package/dist/cli.js
CHANGED
package/dist/engine.js
CHANGED
package/package.json
CHANGED