@flowriki/agent 0.1.0-beta.6 → 0.1.0-beta.7
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-ZUHML4TR.js → chunk-RNHUFS2Q.js} +695 -166
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
// src/engine.ts
|
|
4
4
|
import os5 from "os";
|
|
5
|
-
import { createRequire as
|
|
5
|
+
import { createRequire as createRequire4 } from "module";
|
|
6
6
|
import { spawnSync } from "child_process";
|
|
7
7
|
|
|
8
8
|
// ../agent-core/dist/index.js
|
|
9
9
|
import { z as z4 } from "zod";
|
|
10
|
-
import
|
|
10
|
+
import fs6 from "fs";
|
|
11
11
|
import path5 from "path";
|
|
12
12
|
import crypto22 from "crypto";
|
|
13
|
-
import { createRequire as
|
|
13
|
+
import { createRequire as createRequire3 } from "module";
|
|
14
14
|
import { WebSocket, WebSocketServer } from "ws";
|
|
15
15
|
import fs23 from "fs";
|
|
16
16
|
import os2 from "os";
|
|
17
|
-
import
|
|
17
|
+
import path24 from "path";
|
|
18
18
|
|
|
19
19
|
// ../orchestrator/dist/index.js
|
|
20
|
-
import
|
|
20
|
+
import fs5 from "fs";
|
|
21
21
|
import path4 from "path";
|
|
22
22
|
|
|
23
23
|
// ../protocol/dist/chunk-ROEX7TWK.js
|
|
@@ -287,17 +287,17 @@ var FlowDocumentSchema = z.object({
|
|
|
287
287
|
revision: z.number().int().nonnegative().default(0)
|
|
288
288
|
}).strict().superRefine((flow, ctx) => {
|
|
289
289
|
const ids = /* @__PURE__ */ new Set();
|
|
290
|
-
flow.steps.forEach((
|
|
291
|
-
if (ids.has(
|
|
292
|
-
ids.add(
|
|
290
|
+
flow.steps.forEach((s2, i) => {
|
|
291
|
+
if (ids.has(s2.id)) ctx.addIssue({ code: "custom", path: ["steps", i, "id"], message: "Step IDs must be unique" });
|
|
292
|
+
ids.add(s2.id);
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
|
-
var surfaceOf = (
|
|
296
|
-
var containerOf = (
|
|
297
|
-
function targetOfStep(
|
|
298
|
-
const t = { role:
|
|
295
|
+
var surfaceOf = (s2) => JSON.stringify([s2["pageId"] ?? "", s2["frame"] ?? []]);
|
|
296
|
+
var containerOf = (s2) => JSON.stringify([targetOfStep(s2), s2["legacySelector"] ?? ""]);
|
|
297
|
+
function targetOfStep(s2) {
|
|
298
|
+
const t = { role: s2["role"] ?? "", name: s2["name"] ?? "", scope: s2["scope"] ?? "" };
|
|
299
299
|
for (const key of ["scopeRole", "frame", "pageId", "nameFrom", "locator", "nth"]) {
|
|
300
|
-
if (
|
|
300
|
+
if (s2[key] !== void 0) t[key] = s2[key];
|
|
301
301
|
}
|
|
302
302
|
return t;
|
|
303
303
|
}
|
|
@@ -305,12 +305,12 @@ function migrateScrollPrerequisites(doc) {
|
|
|
305
305
|
const steps = Array.isArray(doc.steps) ? doc.steps : [];
|
|
306
306
|
const out = [];
|
|
307
307
|
const pending = [];
|
|
308
|
-
const isScroll = (
|
|
309
|
-
const hasChecks = (
|
|
310
|
-
const carry = (
|
|
311
|
-
const pos =
|
|
312
|
-
const prerequisite = { x: Number(pos.x ?? 0), y: Number(pos.y ?? 0), container: targetOfStep(
|
|
313
|
-
if (typeof
|
|
308
|
+
const isScroll = (s2) => s2["type"] === "scroll" && s2["scroll"] !== void 0 && s2["scroll"] !== null;
|
|
309
|
+
const hasChecks = (s2) => Array.isArray(s2["proposals"]) && s2["proposals"].length > 0;
|
|
310
|
+
const carry = (s2) => {
|
|
311
|
+
const pos = s2["scroll"] ?? {};
|
|
312
|
+
const prerequisite = { x: Number(pos.x ?? 0), y: Number(pos.y ?? 0), container: targetOfStep(s2) };
|
|
313
|
+
if (typeof s2["legacySelector"] === "string" && s2["legacySelector"]) prerequisite["legacySelector"] = s2["legacySelector"];
|
|
314
314
|
return prerequisite;
|
|
315
315
|
};
|
|
316
316
|
for (const step of steps) {
|
|
@@ -784,7 +784,7 @@ function describeAssertion(a, subject) {
|
|
|
784
784
|
// ../orchestrator/dist/index.js
|
|
785
785
|
import { execFileSync } from "child_process";
|
|
786
786
|
import fs22 from "fs";
|
|
787
|
-
import
|
|
787
|
+
import path23 from "path";
|
|
788
788
|
|
|
789
789
|
// ../runner/dist/index.js
|
|
790
790
|
import { chromium, firefox, webkit } from "playwright";
|
|
@@ -807,7 +807,7 @@ function installRecorder() {
|
|
|
807
807
|
if (d.__flInstalled) return;
|
|
808
808
|
d.__flInstalled = true;
|
|
809
809
|
w.__flInstalled = true;
|
|
810
|
-
const clean = (
|
|
810
|
+
const clean = (s2) => (s2 || "").replace(/\s+/g, " ").trim();
|
|
811
811
|
const FRAME_CHAIN = (() => {
|
|
812
812
|
const chain = [];
|
|
813
813
|
try {
|
|
@@ -887,7 +887,7 @@ function installRecorder() {
|
|
|
887
887
|
return (first && first.tagName ? first : e.target) ?? null;
|
|
888
888
|
}
|
|
889
889
|
w.__flSignature = () => {
|
|
890
|
-
const texts = (sel) => Array.from(document.querySelectorAll(sel)).filter(readable).map((
|
|
890
|
+
const texts = (sel) => Array.from(document.querySelectorAll(sel)).filter(readable).map((n2) => clean(n2.textContent)).filter(Boolean);
|
|
891
891
|
return {
|
|
892
892
|
url: location.href,
|
|
893
893
|
title: document.title,
|
|
@@ -896,15 +896,15 @@ function installRecorder() {
|
|
|
896
896
|
fields: Array.from(document.querySelectorAll("input, select, textarea")).filter(readable).map(accName).filter(Boolean),
|
|
897
897
|
texts: (() => {
|
|
898
898
|
const out = [];
|
|
899
|
-
for (const
|
|
899
|
+
for (const n2 of Array.from(document.querySelectorAll("body *"))) {
|
|
900
900
|
if (out.length >= 200) break;
|
|
901
901
|
const t = clean(
|
|
902
|
-
Array.from(
|
|
902
|
+
Array.from(n2.childNodes).filter((c) => c.nodeType === 3).map((c) => c.textContent).join(" ")
|
|
903
903
|
);
|
|
904
904
|
if (!t || t.length > 120) continue;
|
|
905
|
-
if (!readable(
|
|
906
|
-
const tag = (
|
|
907
|
-
out.push({ key: `${tag}${
|
|
905
|
+
if (!readable(n2)) continue;
|
|
906
|
+
const tag = (n2.tagName || "").toLowerCase();
|
|
907
|
+
out.push({ key: `${tag}${n2.id ? "#" + n2.id : ""}|${t}`, text: t });
|
|
908
908
|
}
|
|
909
909
|
return out;
|
|
910
910
|
})()
|
|
@@ -1894,7 +1894,7 @@ function installDebugPanel() {
|
|
|
1894
1894
|
back: "Back",
|
|
1895
1895
|
forward: "Forward"
|
|
1896
1896
|
})[t] || t;
|
|
1897
|
-
const esc = (
|
|
1897
|
+
const esc = (s2) => String(s2).replace(
|
|
1898
1898
|
/[&<>"]/g,
|
|
1899
1899
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]
|
|
1900
1900
|
);
|
|
@@ -2320,7 +2320,7 @@ async function locateAll(page, step, opts = {}) {
|
|
|
2320
2320
|
if (step.scope && !plan.overridden) {
|
|
2321
2321
|
const roles = step.scopeRole ? [step.scopeRole] : [...SCOPE_ROLES];
|
|
2322
2322
|
const containers = roles.map((role) => root.getByRole(role, { name: step.scope, exact: !step.scopeRole, includeHidden: opts.includeHidden }));
|
|
2323
|
-
root = containers.reduce((a,
|
|
2323
|
+
root = containers.reduce((a, b2) => a.or(b2));
|
|
2324
2324
|
}
|
|
2325
2325
|
if (plan.viaAdjacentLabel) return root.getByText(plan.arg, { exact: true }).locator("xpath=..").getByRole(step.role || "textbox", { includeHidden: opts.includeHidden });
|
|
2326
2326
|
let loc;
|
|
@@ -2719,7 +2719,7 @@ async function startRecordingSession(opts) {
|
|
|
2719
2719
|
void ensureInstalled(p);
|
|
2720
2720
|
p.on("domcontentloaded", () => void ensureInstalled(p));
|
|
2721
2721
|
p.on("download", (download) => {
|
|
2722
|
-
const step = [...steps].reverse().find((
|
|
2722
|
+
const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(p));
|
|
2723
2723
|
if (step) {
|
|
2724
2724
|
(step.downloads ??= []).push(download.suggestedFilename());
|
|
2725
2725
|
onStep?.(step);
|
|
@@ -2727,7 +2727,7 @@ async function startRecordingSession(opts) {
|
|
|
2727
2727
|
}
|
|
2728
2728
|
});
|
|
2729
2729
|
p.on("dialog", (dialog) => {
|
|
2730
|
-
const step = [...steps].reverse().find((
|
|
2730
|
+
const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(p));
|
|
2731
2731
|
if (!step) {
|
|
2732
2732
|
void dialog.dismiss();
|
|
2733
2733
|
record({ type: "unsupported", name: "Dialog before first action", warning: "Record an explicit navigation before a page-load dialog", pageId: pageIds.get(p) }, null);
|
|
@@ -2746,7 +2746,7 @@ async function startRecordingSession(opts) {
|
|
|
2746
2746
|
pageIds.set(p, id);
|
|
2747
2747
|
watchPage(p);
|
|
2748
2748
|
void p.opener().then((opener) => {
|
|
2749
|
-
const step = [...steps].reverse().find((
|
|
2749
|
+
const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(opener));
|
|
2750
2750
|
if (step) {
|
|
2751
2751
|
step.opensPage = id;
|
|
2752
2752
|
onStep?.(step);
|
|
@@ -3154,8 +3154,8 @@ async function checkAssertion(page, kind, value, timeoutMs = 3e3, assertion) {
|
|
|
3154
3154
|
reading = { outcome: "value", value: kind === "url" ? page.url() : await page.title() };
|
|
3155
3155
|
} else if (kind === "count") {
|
|
3156
3156
|
try {
|
|
3157
|
-
const
|
|
3158
|
-
reading = { outcome: "value", value: String(
|
|
3157
|
+
const n2 = await (await locateAll(page, asStep(assertion.target), { allowRoleOnly: true })).count();
|
|
3158
|
+
reading = { outcome: "value", value: String(n2) };
|
|
3159
3159
|
} catch (err) {
|
|
3160
3160
|
reading = { outcome: "invalid", detail: firstLine2(err) };
|
|
3161
3161
|
}
|
|
@@ -3225,7 +3225,7 @@ async function applyAction(loc, step, value, ctx = {}) {
|
|
|
3225
3225
|
case "upload": {
|
|
3226
3226
|
const name = String(value ?? "");
|
|
3227
3227
|
const names = step.values ?? (name ? [name] : []);
|
|
3228
|
-
const files = names.map((
|
|
3228
|
+
const files = names.map((n2) => ctx.resolveFile?.(n2));
|
|
3229
3229
|
const file = files.every(Boolean);
|
|
3230
3230
|
if (!file) {
|
|
3231
3231
|
throw Object.assign(
|
|
@@ -3271,7 +3271,7 @@ async function replayFlow(opts) {
|
|
|
3271
3271
|
opts.environment,
|
|
3272
3272
|
headless,
|
|
3273
3273
|
slowMo,
|
|
3274
|
-
{ policy: opts.artifacts, dir: evidenceDir, hasSecrets: steps.some((
|
|
3274
|
+
{ policy: opts.artifacts, dir: evidenceDir, hasSecrets: steps.some((s2) => s2.secret) }
|
|
3275
3275
|
);
|
|
3276
3276
|
const page = await context.newPage();
|
|
3277
3277
|
const pages = new ExecutionPages(context, page);
|
|
@@ -3639,7 +3639,7 @@ async function startDebugSession(opts) {
|
|
|
3639
3639
|
onDisconnect,
|
|
3640
3640
|
onEndRequested
|
|
3641
3641
|
} = opts;
|
|
3642
|
-
const steps = opts.steps.map((
|
|
3642
|
+
const steps = opts.steps.map((s2) => ({ ...s2 }));
|
|
3643
3643
|
const breakpoints = new Set(opts.breakpoints ?? []);
|
|
3644
3644
|
const results = {};
|
|
3645
3645
|
let status2 = "idle";
|
|
@@ -3661,7 +3661,7 @@ async function startDebugSession(opts) {
|
|
|
3661
3661
|
status: status2,
|
|
3662
3662
|
cursor,
|
|
3663
3663
|
breakpoints: [...breakpoints],
|
|
3664
|
-
steps: steps.map((
|
|
3664
|
+
steps: steps.map((s2) => ({ ...s2 })),
|
|
3665
3665
|
results: { ...results },
|
|
3666
3666
|
failure,
|
|
3667
3667
|
recording,
|
|
@@ -3669,15 +3669,15 @@ async function startDebugSession(opts) {
|
|
|
3669
3669
|
mode: "debug"
|
|
3670
3670
|
});
|
|
3671
3671
|
const publish = () => {
|
|
3672
|
-
const
|
|
3673
|
-
onState?.(
|
|
3674
|
-
void paint(
|
|
3675
|
-
return
|
|
3672
|
+
const s2 = snapshot();
|
|
3673
|
+
onState?.(s2);
|
|
3674
|
+
void paint(s2);
|
|
3675
|
+
return s2;
|
|
3676
3676
|
};
|
|
3677
|
-
async function paint(
|
|
3677
|
+
async function paint(s2) {
|
|
3678
3678
|
const safe2 = {
|
|
3679
|
-
...
|
|
3680
|
-
steps:
|
|
3679
|
+
...s2,
|
|
3680
|
+
steps: s2.steps.map((st) => ({
|
|
3681
3681
|
id: st.id,
|
|
3682
3682
|
type: st.type,
|
|
3683
3683
|
name: st.name,
|
|
@@ -3720,7 +3720,7 @@ async function startDebugSession(opts) {
|
|
|
3720
3720
|
legacySelector: payload.legacySelector ?? "",
|
|
3721
3721
|
proposals: []
|
|
3722
3722
|
};
|
|
3723
|
-
const at = cursor ? steps.findIndex((
|
|
3723
|
+
const at = cursor ? steps.findIndex((s2) => s2.id === cursor) : steps.length;
|
|
3724
3724
|
const insertAt = at === -1 ? steps.length : at;
|
|
3725
3725
|
if (["fill", "select", "setRange"].includes(step.type)) {
|
|
3726
3726
|
const prev = steps[insertAt - 1];
|
|
@@ -3810,7 +3810,7 @@ async function startDebugSession(opts) {
|
|
|
3810
3810
|
break;
|
|
3811
3811
|
}
|
|
3812
3812
|
}
|
|
3813
|
-
const indexOf = (id) => id ? steps.findIndex((
|
|
3813
|
+
const indexOf = (id) => id ? steps.findIndex((s2) => s2.id === id) : -1;
|
|
3814
3814
|
function advanceFrom(i) {
|
|
3815
3815
|
let next = i + 1;
|
|
3816
3816
|
while (next < steps.length && applied.has(steps[next].id)) next += 1;
|
|
@@ -3969,7 +3969,7 @@ async function startDebugSession(opts) {
|
|
|
3969
3969
|
return publish();
|
|
3970
3970
|
},
|
|
3971
3971
|
deleteStep(stepId) {
|
|
3972
|
-
const i = steps.findIndex((
|
|
3972
|
+
const i = steps.findIndex((s2) => s2.id === stepId);
|
|
3973
3973
|
if (i === -1) return publish();
|
|
3974
3974
|
if (cursor === stepId) cursor = steps[i + 1]?.id ?? null;
|
|
3975
3975
|
if (failure?.stepId === stepId) failure = null;
|
|
@@ -3990,7 +3990,7 @@ async function startDebugSession(opts) {
|
|
|
3990
3990
|
status2 = "finished";
|
|
3991
3991
|
await browser.close().catch(() => {
|
|
3992
3992
|
});
|
|
3993
|
-
return steps.map((
|
|
3993
|
+
return steps.map((s2) => ({ ...s2 }));
|
|
3994
3994
|
},
|
|
3995
3995
|
async abort() {
|
|
3996
3996
|
closed = true;
|
|
@@ -4003,6 +4003,9 @@ async function startDebugSession(opts) {
|
|
|
4003
4003
|
}
|
|
4004
4004
|
|
|
4005
4005
|
// ../rundb/dist/index.js
|
|
4006
|
+
import fs4 from "fs";
|
|
4007
|
+
import { createRequire as createRequire2 } from "module";
|
|
4008
|
+
import path22 from "path";
|
|
4006
4009
|
import os from "os";
|
|
4007
4010
|
import path3 from "path";
|
|
4008
4011
|
var MIGRATIONS2 = [
|
|
@@ -4150,9 +4153,46 @@ var MIGRATIONS2 = [
|
|
|
4150
4153
|
`
|
|
4151
4154
|
];
|
|
4152
4155
|
var SCHEMA_VERSION = MIGRATIONS2.length;
|
|
4156
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4157
|
+
var TIME_LEN = 10;
|
|
4158
|
+
var RANDOM_LEN = 16;
|
|
4159
|
+
var lastTime = -1;
|
|
4160
|
+
var lastRandom = [];
|
|
4161
|
+
function encodeTime(ms) {
|
|
4162
|
+
let out = "";
|
|
4163
|
+
for (let i = TIME_LEN - 1; i >= 0; i--) {
|
|
4164
|
+
out = CROCKFORD[ms % 32] + out;
|
|
4165
|
+
ms = Math.floor(ms / 32);
|
|
4166
|
+
}
|
|
4167
|
+
return out;
|
|
4168
|
+
}
|
|
4169
|
+
function bumpRandom() {
|
|
4170
|
+
for (let i = RANDOM_LEN - 1; i >= 0; i--) {
|
|
4171
|
+
if (lastRandom[i] < 31) {
|
|
4172
|
+
lastRandom[i]++;
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
4175
|
+
lastRandom[i] = 0;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
function ulid(now = Date.now()) {
|
|
4179
|
+
if (now === lastTime) {
|
|
4180
|
+
bumpRandom();
|
|
4181
|
+
} else {
|
|
4182
|
+
lastTime = now;
|
|
4183
|
+
lastRandom = Array.from(crypto.getRandomValues(new Uint8Array(RANDOM_LEN)), (b2) => b2 % 32);
|
|
4184
|
+
}
|
|
4185
|
+
return encodeTime(now) + lastRandom.map((n2) => CROCKFORD[n2]).join("");
|
|
4186
|
+
}
|
|
4187
|
+
function rowId(prefix) {
|
|
4188
|
+
return `${prefix}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
4189
|
+
}
|
|
4153
4190
|
function flowrikiHome() {
|
|
4154
4191
|
return process.env.FLOWRIKI_HOME || path3.join(os.homedir(), ".flowriki");
|
|
4155
4192
|
}
|
|
4193
|
+
function defaultDbPath() {
|
|
4194
|
+
return path3.join(flowrikiHome(), "flowriki.db");
|
|
4195
|
+
}
|
|
4156
4196
|
function runArtifactsDir(runId, projectId = "default") {
|
|
4157
4197
|
return path3.join(flowrikiHome(), "projects", safe(projectId), "runs", safe(runId));
|
|
4158
4198
|
}
|
|
@@ -4167,6 +4207,469 @@ function isInside(root, candidate) {
|
|
|
4167
4207
|
const target = path3.resolve(candidate);
|
|
4168
4208
|
return target === base || target.startsWith(base + path3.sep);
|
|
4169
4209
|
}
|
|
4210
|
+
var b = (v) => v ? 1 : 0;
|
|
4211
|
+
var s = (v) => v === void 0 || v === null ? null : String(v);
|
|
4212
|
+
var n = (v) => v === void 0 || v === null ? null : Number(v);
|
|
4213
|
+
var RUN_COLUMNS = `r.*,
|
|
4214
|
+
(SELECT COALESCE(SUM(steps_run), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_run,
|
|
4215
|
+
(SELECT COALESCE(SUM(steps_total), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_total`;
|
|
4216
|
+
var SqliteStorage = class {
|
|
4217
|
+
constructor(file = defaultDbPath()) {
|
|
4218
|
+
this.file = file;
|
|
4219
|
+
}
|
|
4220
|
+
file;
|
|
4221
|
+
db = null;
|
|
4222
|
+
async init() {
|
|
4223
|
+
if (this.db) return;
|
|
4224
|
+
let DatabaseSync2;
|
|
4225
|
+
try {
|
|
4226
|
+
({ DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite"));
|
|
4227
|
+
} catch (err) {
|
|
4228
|
+
throw new Error(
|
|
4229
|
+
`run history needs node:sqlite, which this Node (${process.version}) does not provide \u2014 Node 22.13 or newer is required (${err.message})`
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
if (this.file !== ":memory:") fs4.mkdirSync(path22.dirname(this.file), { recursive: true });
|
|
4233
|
+
const db = new DatabaseSync2(this.file);
|
|
4234
|
+
if (this.file !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
|
|
4235
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
4236
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
4237
|
+
const current = Number(db.prepare("PRAGMA user_version").get().user_version ?? 0);
|
|
4238
|
+
if (current > SCHEMA_VERSION) {
|
|
4239
|
+
throw new Error(
|
|
4240
|
+
`${this.file} was written by a newer Flowriki (schema v${current}, this build understands v${SCHEMA_VERSION})`
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
for (let v = current; v < MIGRATIONS2.length; v++) {
|
|
4244
|
+
db.exec("BEGIN");
|
|
4245
|
+
try {
|
|
4246
|
+
db.exec(MIGRATIONS2[v]);
|
|
4247
|
+
db.exec(`PRAGMA user_version = ${v + 1}`);
|
|
4248
|
+
db.exec("COMMIT");
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
db.exec("ROLLBACK");
|
|
4251
|
+
throw err;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
this.db = db;
|
|
4255
|
+
}
|
|
4256
|
+
async close() {
|
|
4257
|
+
this.db?.close();
|
|
4258
|
+
this.db = null;
|
|
4259
|
+
}
|
|
4260
|
+
get handle() {
|
|
4261
|
+
if (!this.db) throw new Error("storage used before init()");
|
|
4262
|
+
return this.db;
|
|
4263
|
+
}
|
|
4264
|
+
tx(fn) {
|
|
4265
|
+
const db = this.handle;
|
|
4266
|
+
db.exec("BEGIN IMMEDIATE");
|
|
4267
|
+
try {
|
|
4268
|
+
const out = fn();
|
|
4269
|
+
db.exec("COMMIT");
|
|
4270
|
+
return out;
|
|
4271
|
+
} catch (err) {
|
|
4272
|
+
db.exec("ROLLBACK");
|
|
4273
|
+
throw err;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
// ---- runs ----------------------------------------------------------------
|
|
4277
|
+
async createRun(input) {
|
|
4278
|
+
const id = ulid();
|
|
4279
|
+
const queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4280
|
+
this.handle.prepare(
|
|
4281
|
+
`INSERT INTO run (id, project_id, kind, trigger, status, flow_id, flow_name,
|
|
4282
|
+
git_sha, git_branch, git_dirty, env, base_url, concurrency, queued_at, flow_snapshot)
|
|
4283
|
+
VALUES (?,?,?,?,'queued',?,?,?,?,?,?,?,?,?,?)`
|
|
4284
|
+
).run(
|
|
4285
|
+
id,
|
|
4286
|
+
input.projectId ?? "default",
|
|
4287
|
+
input.kind,
|
|
4288
|
+
input.trigger,
|
|
4289
|
+
input.flowId,
|
|
4290
|
+
input.flowName,
|
|
4291
|
+
input.git?.sha ?? null,
|
|
4292
|
+
input.git?.branch ?? null,
|
|
4293
|
+
b(input.git?.dirty),
|
|
4294
|
+
input.env ?? null,
|
|
4295
|
+
input.baseUrl ?? null,
|
|
4296
|
+
input.concurrency ?? null,
|
|
4297
|
+
queuedAt,
|
|
4298
|
+
input.flowSnapshot === void 0 ? null : JSON.stringify(input.flowSnapshot)
|
|
4299
|
+
);
|
|
4300
|
+
return await this.getRunSummary(id);
|
|
4301
|
+
}
|
|
4302
|
+
async startRun(runId) {
|
|
4303
|
+
this.handle.prepare("UPDATE run SET status='running', started_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), runId);
|
|
4304
|
+
}
|
|
4305
|
+
async finishRun(runId, input) {
|
|
4306
|
+
const row = this.handle.prepare("SELECT started_at, queued_at FROM run WHERE id=?").get(runId);
|
|
4307
|
+
if (!row) return;
|
|
4308
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4309
|
+
const from = Date.parse(String(row.started_at ?? row.queued_at));
|
|
4310
|
+
this.handle.prepare(
|
|
4311
|
+
`UPDATE run SET status=?, finished_at=?, duration_ms=?, error_message=?, artifacts_dir=?
|
|
4312
|
+
WHERE id=?`
|
|
4313
|
+
).run(
|
|
4314
|
+
input.status,
|
|
4315
|
+
finishedAt,
|
|
4316
|
+
Math.max(0, Date.parse(finishedAt) - from),
|
|
4317
|
+
input.errorMessage ?? null,
|
|
4318
|
+
input.artifactsDir ?? null,
|
|
4319
|
+
runId
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
// ---- tasks and steps -----------------------------------------------------
|
|
4323
|
+
async createTask(runId, input) {
|
|
4324
|
+
const id = rowId("tsk");
|
|
4325
|
+
return this.tx(() => {
|
|
4326
|
+
const ordinal = input.ordinal ?? Number(
|
|
4327
|
+
this.handle.prepare("SELECT COUNT(*) AS c FROM run_task WHERE run_id=?").get(runId).c
|
|
4328
|
+
);
|
|
4329
|
+
this.handle.prepare(
|
|
4330
|
+
`INSERT INTO run_task (id, run_id, ordinal, case_name, row_id, capability_id,
|
|
4331
|
+
capability, provider, attempt, max_attempts, status, worker_id, queued_at, started_at)
|
|
4332
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,'running',?,?,?)`
|
|
4333
|
+
).run(
|
|
4334
|
+
id,
|
|
4335
|
+
runId,
|
|
4336
|
+
ordinal,
|
|
4337
|
+
input.caseName,
|
|
4338
|
+
input.rowId ?? null,
|
|
4339
|
+
input.capability.id,
|
|
4340
|
+
JSON.stringify(input.capability),
|
|
4341
|
+
input.capability.provider ?? "local",
|
|
4342
|
+
input.attempt ?? 1,
|
|
4343
|
+
input.maxAttempts ?? 1,
|
|
4344
|
+
input.workerId ?? null,
|
|
4345
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4346
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
4347
|
+
);
|
|
4348
|
+
this.handle.prepare("UPDATE run SET tasks_total = tasks_total + 1 WHERE id=?").run(runId);
|
|
4349
|
+
return id;
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
async finishTask(taskId, input) {
|
|
4353
|
+
this.tx(() => {
|
|
4354
|
+
const row = this.handle.prepare("SELECT run_id, started_at, queued_at FROM run_task WHERE id=?").get(taskId);
|
|
4355
|
+
if (!row) return;
|
|
4356
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4357
|
+
const from = Date.parse(String(row.started_at ?? row.queued_at ?? finishedAt));
|
|
4358
|
+
this.handle.prepare(
|
|
4359
|
+
`UPDATE run_task SET status=?, detail=?, session_id=?, session_url=?,
|
|
4360
|
+
steps_run=COALESCE(?, steps_run), steps_total=COALESCE(?, steps_total),
|
|
4361
|
+
finished_at=?, duration_ms=? WHERE id=?`
|
|
4362
|
+
).run(
|
|
4363
|
+
input.status,
|
|
4364
|
+
input.detail ?? null,
|
|
4365
|
+
input.sessionId ?? null,
|
|
4366
|
+
input.sessionUrl ?? null,
|
|
4367
|
+
input.stepsRun ?? null,
|
|
4368
|
+
input.stepsTotal ?? null,
|
|
4369
|
+
finishedAt,
|
|
4370
|
+
Math.max(0, Date.parse(finishedAt) - from),
|
|
4371
|
+
taskId
|
|
4372
|
+
);
|
|
4373
|
+
const column = input.status === "passed" ? "tasks_passed" : input.status === "failed" || input.status === "errored" ? "tasks_failed" : null;
|
|
4374
|
+
if (column) {
|
|
4375
|
+
this.handle.prepare(`UPDATE run SET ${column} = ${column} + 1 WHERE id=?`).run(String(row.run_id));
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
async recordStep(taskId, input) {
|
|
4380
|
+
const id = rowId("stp");
|
|
4381
|
+
return this.tx(() => {
|
|
4382
|
+
this.handle.prepare(
|
|
4383
|
+
`INSERT INTO run_step (id, run_task_id, ordinal, step_id, action, target_role,
|
|
4384
|
+
target_name, target_scope, status, failure_kind, detail, duration_ms,
|
|
4385
|
+
page_url, page_title, value, secret)
|
|
4386
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
4387
|
+
).run(
|
|
4388
|
+
id,
|
|
4389
|
+
taskId,
|
|
4390
|
+
input.ordinal,
|
|
4391
|
+
input.stepId,
|
|
4392
|
+
input.action,
|
|
4393
|
+
input.targetRole ?? null,
|
|
4394
|
+
input.targetName ?? null,
|
|
4395
|
+
input.targetScope ?? null,
|
|
4396
|
+
input.status,
|
|
4397
|
+
input.failureKind ?? null,
|
|
4398
|
+
input.detail ?? null,
|
|
4399
|
+
input.durationMs ?? null,
|
|
4400
|
+
input.pageUrl ?? null,
|
|
4401
|
+
input.pageTitle ?? null,
|
|
4402
|
+
// Never the plaintext. A secret arrives here already withheld.
|
|
4403
|
+
input.secret ? null : input.value ?? null,
|
|
4404
|
+
b(input.secret ?? false)
|
|
4405
|
+
);
|
|
4406
|
+
const stmt = this.handle.prepare(
|
|
4407
|
+
`INSERT INTO run_check (id, run_step_id, assertion_id, kind, expected, actual, ok, soft, detail)
|
|
4408
|
+
VALUES (?,?,?,?,?,?,?,?,?)`
|
|
4409
|
+
);
|
|
4410
|
+
for (const c of input.checks ?? []) {
|
|
4411
|
+
stmt.run(
|
|
4412
|
+
rowId("chk"),
|
|
4413
|
+
id,
|
|
4414
|
+
c.assertionId ?? null,
|
|
4415
|
+
c.kind,
|
|
4416
|
+
c.expected ?? null,
|
|
4417
|
+
c.actual ?? null,
|
|
4418
|
+
b(c.ok),
|
|
4419
|
+
b(c.soft ?? false),
|
|
4420
|
+
c.detail ?? null
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
return id;
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4426
|
+
async addArtifact(runId, input) {
|
|
4427
|
+
const artifact = {
|
|
4428
|
+
id: rowId("art"),
|
|
4429
|
+
runId,
|
|
4430
|
+
runStepId: input.runStepId ?? null,
|
|
4431
|
+
kind: input.kind,
|
|
4432
|
+
relPath: input.relPath,
|
|
4433
|
+
bytes: input.bytes ?? null,
|
|
4434
|
+
masked: input.masked ?? false,
|
|
4435
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4436
|
+
};
|
|
4437
|
+
this.handle.prepare(
|
|
4438
|
+
`INSERT INTO artifact (id, run_id, run_step_id, kind, rel_path, bytes, masked, created_at)
|
|
4439
|
+
VALUES (?,?,?,?,?,?,?,?)`
|
|
4440
|
+
).run(
|
|
4441
|
+
artifact.id,
|
|
4442
|
+
artifact.runId,
|
|
4443
|
+
artifact.runStepId,
|
|
4444
|
+
artifact.kind,
|
|
4445
|
+
artifact.relPath,
|
|
4446
|
+
artifact.bytes,
|
|
4447
|
+
b(artifact.masked),
|
|
4448
|
+
artifact.createdAt
|
|
4449
|
+
);
|
|
4450
|
+
return artifact;
|
|
4451
|
+
}
|
|
4452
|
+
// ---- events --------------------------------------------------------------
|
|
4453
|
+
async appendEvent(runId, type, payload) {
|
|
4454
|
+
return this.tx(() => {
|
|
4455
|
+
const seq = Number(
|
|
4456
|
+
this.handle.prepare("SELECT COALESCE(MAX(seq), 0) AS m FROM run_event WHERE run_id=?").get(runId).m
|
|
4457
|
+
) + 1;
|
|
4458
|
+
this.handle.prepare("INSERT INTO run_event (run_id, seq, at, type, payload) VALUES (?,?,?,?,?)").run(runId, seq, (/* @__PURE__ */ new Date()).toISOString(), type, JSON.stringify(payload ?? null));
|
|
4459
|
+
return seq;
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
async listEvents(runId, since = 0) {
|
|
4463
|
+
return this.handle.prepare("SELECT seq, at, type, payload FROM run_event WHERE run_id=? AND seq>? ORDER BY seq").all(runId, since).map((r) => ({
|
|
4464
|
+
seq: Number(r.seq),
|
|
4465
|
+
at: String(r.at),
|
|
4466
|
+
type: String(r.type),
|
|
4467
|
+
payload: JSON.parse(String(r.payload))
|
|
4468
|
+
}));
|
|
4469
|
+
}
|
|
4470
|
+
// ---- reads ---------------------------------------------------------------
|
|
4471
|
+
async getRunSummary(runId) {
|
|
4472
|
+
const row = this.handle.prepare(`SELECT ${RUN_COLUMNS} FROM run r WHERE r.id=?`).get(runId);
|
|
4473
|
+
return row ? toSummary(row) : null;
|
|
4474
|
+
}
|
|
4475
|
+
async listRuns(query = {}) {
|
|
4476
|
+
const limit = Math.min(Math.max(query.limit ?? 25, 1), 200);
|
|
4477
|
+
const where = [];
|
|
4478
|
+
const params = [];
|
|
4479
|
+
const restrict = (clause, value) => {
|
|
4480
|
+
where.push(clause);
|
|
4481
|
+
params.push(value);
|
|
4482
|
+
};
|
|
4483
|
+
if (query.projectId) restrict("r.project_id=?", query.projectId);
|
|
4484
|
+
if (query.flowId) restrict("r.flow_id=?", query.flowId);
|
|
4485
|
+
if (query.status) restrict("r.status=?", query.status);
|
|
4486
|
+
if (query.branch) restrict("r.git_branch=?", query.branch);
|
|
4487
|
+
if (query.cursor) restrict("r.id<?", query.cursor);
|
|
4488
|
+
const rows = this.handle.prepare(
|
|
4489
|
+
`SELECT ${RUN_COLUMNS} FROM run r
|
|
4490
|
+
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
4491
|
+
ORDER BY r.id DESC LIMIT ?`
|
|
4492
|
+
).all(...params, limit + 1);
|
|
4493
|
+
const page = rows.slice(0, limit);
|
|
4494
|
+
return {
|
|
4495
|
+
runs: page.map(toSummary),
|
|
4496
|
+
nextCursor: rows.length > limit ? String(page[page.length - 1].id) : null
|
|
4497
|
+
};
|
|
4498
|
+
}
|
|
4499
|
+
async getRun(runId) {
|
|
4500
|
+
const run = this.handle.prepare(`SELECT ${RUN_COLUMNS} FROM run r WHERE r.id=?`).get(runId);
|
|
4501
|
+
if (!run) return null;
|
|
4502
|
+
const tasks = this.handle.prepare("SELECT * FROM run_task WHERE run_id=? ORDER BY ordinal").all(runId);
|
|
4503
|
+
const steps = this.handle.prepare(
|
|
4504
|
+
`SELECT s.* FROM run_step s JOIN run_task t ON t.id = s.run_task_id
|
|
4505
|
+
WHERE t.run_id=? ORDER BY t.ordinal, s.ordinal`
|
|
4506
|
+
).all(runId);
|
|
4507
|
+
const artifacts = this.handle.prepare("SELECT * FROM artifact WHERE run_id=? ORDER BY created_at").all(runId);
|
|
4508
|
+
const checks = this.handle.prepare(
|
|
4509
|
+
`SELECT c.* FROM run_check c
|
|
4510
|
+
JOIN run_step s ON s.id = c.run_step_id
|
|
4511
|
+
JOIN run_task t ON t.id = s.run_task_id
|
|
4512
|
+
WHERE t.run_id=?`
|
|
4513
|
+
).all(runId);
|
|
4514
|
+
const checksByStep = /* @__PURE__ */ new Map();
|
|
4515
|
+
for (const c of checks) {
|
|
4516
|
+
const list = checksByStep.get(String(c.run_step_id)) ?? [];
|
|
4517
|
+
list.push({
|
|
4518
|
+
id: String(c.id),
|
|
4519
|
+
assertionId: s(c.assertion_id),
|
|
4520
|
+
kind: String(c.kind),
|
|
4521
|
+
expected: s(c.expected),
|
|
4522
|
+
actual: s(c.actual),
|
|
4523
|
+
ok: Number(c.ok) === 1,
|
|
4524
|
+
soft: Number(c.soft) === 1,
|
|
4525
|
+
detail: s(c.detail)
|
|
4526
|
+
});
|
|
4527
|
+
checksByStep.set(String(c.run_step_id), list);
|
|
4528
|
+
}
|
|
4529
|
+
const stepsByTask = /* @__PURE__ */ new Map();
|
|
4530
|
+
for (const st of steps) {
|
|
4531
|
+
const list = stepsByTask.get(String(st.run_task_id)) ?? [];
|
|
4532
|
+
list.push({
|
|
4533
|
+
id: String(st.id),
|
|
4534
|
+
ordinal: Number(st.ordinal),
|
|
4535
|
+
stepId: String(st.step_id),
|
|
4536
|
+
action: String(st.action),
|
|
4537
|
+
targetRole: s(st.target_role),
|
|
4538
|
+
targetName: s(st.target_name),
|
|
4539
|
+
targetScope: s(st.target_scope),
|
|
4540
|
+
value: s(st.value),
|
|
4541
|
+
secret: Number(st.secret) === 1,
|
|
4542
|
+
status: String(st.status),
|
|
4543
|
+
failureKind: s(st.failure_kind),
|
|
4544
|
+
detail: s(st.detail),
|
|
4545
|
+
durationMs: n(st.duration_ms),
|
|
4546
|
+
pageUrl: s(st.page_url),
|
|
4547
|
+
pageTitle: s(st.page_title),
|
|
4548
|
+
checks: checksByStep.get(String(st.id)) ?? []
|
|
4549
|
+
});
|
|
4550
|
+
stepsByTask.set(String(st.run_task_id), list);
|
|
4551
|
+
}
|
|
4552
|
+
return {
|
|
4553
|
+
...toSummary(run),
|
|
4554
|
+
errorMessage: s(run.error_message),
|
|
4555
|
+
artifactsDir: s(run.artifacts_dir),
|
|
4556
|
+
artifacts: artifacts.map(
|
|
4557
|
+
(a) => ({
|
|
4558
|
+
id: String(a.id),
|
|
4559
|
+
runId: String(a.run_id),
|
|
4560
|
+
runStepId: s(a.run_step_id),
|
|
4561
|
+
kind: String(a.kind),
|
|
4562
|
+
relPath: String(a.rel_path),
|
|
4563
|
+
bytes: n(a.bytes),
|
|
4564
|
+
masked: Number(a.masked) === 1,
|
|
4565
|
+
createdAt: String(a.created_at)
|
|
4566
|
+
})
|
|
4567
|
+
),
|
|
4568
|
+
tasks: tasks.map(
|
|
4569
|
+
(t) => ({
|
|
4570
|
+
id: String(t.id),
|
|
4571
|
+
ordinal: Number(t.ordinal),
|
|
4572
|
+
caseName: String(t.case_name),
|
|
4573
|
+
rowId: s(t.row_id),
|
|
4574
|
+
capability: JSON.parse(String(t.capability)),
|
|
4575
|
+
provider: String(t.provider),
|
|
4576
|
+
attempt: Number(t.attempt),
|
|
4577
|
+
maxAttempts: Number(t.max_attempts),
|
|
4578
|
+
status: String(t.status),
|
|
4579
|
+
workerId: s(t.worker_id),
|
|
4580
|
+
sessionId: s(t.session_id),
|
|
4581
|
+
sessionUrl: s(t.session_url),
|
|
4582
|
+
detail: s(t.detail),
|
|
4583
|
+
stepsRun: Number(t.steps_run),
|
|
4584
|
+
stepsTotal: Number(t.steps_total),
|
|
4585
|
+
durationMs: n(t.duration_ms),
|
|
4586
|
+
steps: stepsByTask.get(String(t.id)) ?? []
|
|
4587
|
+
})
|
|
4588
|
+
)
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
async deleteRun(runId) {
|
|
4592
|
+
this.handle.prepare("DELETE FROM run WHERE id=?").run(runId);
|
|
4593
|
+
}
|
|
4594
|
+
async pruneRuns(keep) {
|
|
4595
|
+
if (keep < 0) throw new Error("keep must not be negative");
|
|
4596
|
+
return this.tx(() => {
|
|
4597
|
+
const doomed = this.handle.prepare(
|
|
4598
|
+
`SELECT id FROM (
|
|
4599
|
+
SELECT id, ROW_NUMBER() OVER (
|
|
4600
|
+
PARTITION BY COALESCE(flow_id, '') ORDER BY id DESC
|
|
4601
|
+
) AS rn FROM run
|
|
4602
|
+
) WHERE rn > ?`
|
|
4603
|
+
).all(keep).map((r) => String(r.id));
|
|
4604
|
+
const stmt = this.handle.prepare("DELETE FROM run WHERE id=?");
|
|
4605
|
+
for (const id of doomed) stmt.run(id);
|
|
4606
|
+
return doomed;
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
async runStats(days, projectId = "default") {
|
|
4610
|
+
const since = new Date(Date.now() - days * 864e5).toISOString();
|
|
4611
|
+
const row = this.handle.prepare(
|
|
4612
|
+
`SELECT COUNT(*) AS total,
|
|
4613
|
+
SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) AS passed,
|
|
4614
|
+
SUM(CASE WHEN status IN ('failed', 'errored') THEN 1 ELSE 0 END) AS failed
|
|
4615
|
+
FROM run
|
|
4616
|
+
WHERE project_id = ? AND queued_at >= ? AND finished_at IS NOT NULL
|
|
4617
|
+
-- A flakiness check repeats one flow on purpose and reports how
|
|
4618
|
+
-- often it passed. Counting a deliberate 98/100 as a failing run
|
|
4619
|
+
-- would make measuring reliability look like losing it.
|
|
4620
|
+
AND kind <> 'flake'`
|
|
4621
|
+
).get(projectId, since);
|
|
4622
|
+
return {
|
|
4623
|
+
total: Number(row.total ?? 0),
|
|
4624
|
+
passed: Number(row.passed ?? 0),
|
|
4625
|
+
failed: Number(row.failed ?? 0)
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
async stepHistory(stepId, limit = 20) {
|
|
4629
|
+
return this.handle.prepare(
|
|
4630
|
+
`SELECT r.id AS run_id, COALESCE(r.started_at, r.queued_at) AS at, s.status,
|
|
4631
|
+
s.failure_kind, s.detail, s.duration_ms, r.git_sha
|
|
4632
|
+
FROM run_step s
|
|
4633
|
+
JOIN run_task t ON t.id = s.run_task_id
|
|
4634
|
+
JOIN run r ON r.id = t.run_id
|
|
4635
|
+
WHERE s.step_id = ?
|
|
4636
|
+
ORDER BY r.id DESC LIMIT ?`
|
|
4637
|
+
).all(stepId, limit).map((r) => ({
|
|
4638
|
+
runId: String(r.run_id),
|
|
4639
|
+
at: String(r.at),
|
|
4640
|
+
status: String(r.status),
|
|
4641
|
+
failureKind: s(r.failure_kind),
|
|
4642
|
+
detail: s(r.detail),
|
|
4643
|
+
durationMs: n(r.duration_ms),
|
|
4644
|
+
gitSha: s(r.git_sha)
|
|
4645
|
+
})).reverse();
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
function toSummary(row) {
|
|
4649
|
+
return {
|
|
4650
|
+
id: String(row.id),
|
|
4651
|
+
projectId: String(row.project_id),
|
|
4652
|
+
kind: String(row.kind),
|
|
4653
|
+
trigger: String(row.trigger),
|
|
4654
|
+
status: String(row.status),
|
|
4655
|
+
flowId: s(row.flow_id),
|
|
4656
|
+
flowName: s(row.flow_name),
|
|
4657
|
+
gitSha: s(row.git_sha),
|
|
4658
|
+
gitBranch: s(row.git_branch),
|
|
4659
|
+
gitDirty: Number(row.git_dirty) === 1,
|
|
4660
|
+
env: s(row.env),
|
|
4661
|
+
concurrency: n(row.concurrency),
|
|
4662
|
+
tasksTotal: Number(row.tasks_total),
|
|
4663
|
+
tasksPassed: Number(row.tasks_passed),
|
|
4664
|
+
tasksFailed: Number(row.tasks_failed),
|
|
4665
|
+
stepsRun: Number(row.steps_run ?? 0),
|
|
4666
|
+
stepsTotal: Number(row.steps_total ?? 0),
|
|
4667
|
+
queuedAt: String(row.queued_at),
|
|
4668
|
+
startedAt: s(row.started_at),
|
|
4669
|
+
finishedAt: s(row.finished_at),
|
|
4670
|
+
durationMs: n(row.duration_ms)
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4170
4673
|
|
|
4171
4674
|
// ../orchestrator/dist/index.js
|
|
4172
4675
|
import crypto2 from "crypto";
|
|
@@ -4189,17 +4692,17 @@ var FlowConflict = class extends Error {
|
|
|
4189
4692
|
};
|
|
4190
4693
|
function writeFileAtomic(file, contents) {
|
|
4191
4694
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
4192
|
-
const handle =
|
|
4695
|
+
const handle = fs5.openSync(tmp, "w");
|
|
4193
4696
|
try {
|
|
4194
|
-
|
|
4195
|
-
|
|
4697
|
+
fs5.writeFileSync(handle, contents);
|
|
4698
|
+
fs5.fsyncSync(handle);
|
|
4196
4699
|
} finally {
|
|
4197
|
-
|
|
4700
|
+
fs5.closeSync(handle);
|
|
4198
4701
|
}
|
|
4199
4702
|
try {
|
|
4200
|
-
|
|
4703
|
+
fs5.renameSync(tmp, file);
|
|
4201
4704
|
} catch (err) {
|
|
4202
|
-
|
|
4705
|
+
fs5.rmSync(tmp, { force: true });
|
|
4203
4706
|
throw err;
|
|
4204
4707
|
}
|
|
4205
4708
|
}
|
|
@@ -4213,9 +4716,9 @@ var FlowNotRunnable = class extends Error {
|
|
|
4213
4716
|
var DEFAULT_FLAKINESS_REPEAT = 10;
|
|
4214
4717
|
var MAX_FLAKINESS_REPEAT = 500;
|
|
4215
4718
|
function clampRepeat(value) {
|
|
4216
|
-
const
|
|
4217
|
-
if (!Number.isFinite(
|
|
4218
|
-
return Math.min(
|
|
4719
|
+
const n2 = Math.floor(Number(value));
|
|
4720
|
+
if (!Number.isFinite(n2) || n2 < 2) return DEFAULT_FLAKINESS_REPEAT;
|
|
4721
|
+
return Math.min(n2, MAX_FLAKINESS_REPEAT);
|
|
4219
4722
|
}
|
|
4220
4723
|
function uniqueRef(flowSlug, name, taken) {
|
|
4221
4724
|
const base = `${flowSlug}_${slugify(name) || "secret"}`;
|
|
@@ -4223,8 +4726,8 @@ function uniqueRef(flowSlug, name, taken) {
|
|
|
4223
4726
|
taken.add(base);
|
|
4224
4727
|
return base;
|
|
4225
4728
|
}
|
|
4226
|
-
for (let
|
|
4227
|
-
const ref = `${base}_${
|
|
4729
|
+
for (let n2 = 2; ; n2++) {
|
|
4730
|
+
const ref = `${base}_${n2}`;
|
|
4228
4731
|
if (!taken.has(ref)) {
|
|
4229
4732
|
taken.add(ref);
|
|
4230
4733
|
return ref;
|
|
@@ -4246,14 +4749,14 @@ var Store = class {
|
|
|
4246
4749
|
return slug;
|
|
4247
4750
|
}
|
|
4248
4751
|
listFeatures() {
|
|
4249
|
-
if (!
|
|
4752
|
+
if (!fs5.existsSync(this.featuresDir)) return [];
|
|
4250
4753
|
const out = [];
|
|
4251
|
-
for (const entry of
|
|
4754
|
+
for (const entry of fs5.readdirSync(this.featuresDir, { withFileTypes: true })) {
|
|
4252
4755
|
if (!entry.isDirectory()) continue;
|
|
4253
4756
|
const file = path4.join(this.featuresDir, entry.name, "feature.json");
|
|
4254
|
-
if (!
|
|
4757
|
+
if (!fs5.existsSync(file)) continue;
|
|
4255
4758
|
try {
|
|
4256
|
-
out.push(FeatureSchema.parse(JSON.parse(
|
|
4759
|
+
out.push(FeatureSchema.parse(JSON.parse(fs5.readFileSync(file, "utf8"))));
|
|
4257
4760
|
} catch (err) {
|
|
4258
4761
|
this.featureProblems.push({
|
|
4259
4762
|
path: path4.relative(this.root, file),
|
|
@@ -4262,14 +4765,14 @@ var Store = class {
|
|
|
4262
4765
|
});
|
|
4263
4766
|
}
|
|
4264
4767
|
}
|
|
4265
|
-
return out.sort((a,
|
|
4768
|
+
return out.sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
|
|
4266
4769
|
}
|
|
4267
4770
|
featureProblems = [];
|
|
4268
4771
|
createFeature(name, description = "") {
|
|
4269
4772
|
const slug = this.assertSlug(slugify(name));
|
|
4270
4773
|
const dir = path4.join(this.featuresDir, slug);
|
|
4271
|
-
if (
|
|
4272
|
-
|
|
4774
|
+
if (fs5.existsSync(dir)) throw new Error(`a feature named "${name}" already exists`);
|
|
4775
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
4273
4776
|
const doc = {
|
|
4274
4777
|
schemaVersion: 1,
|
|
4275
4778
|
id: `f_${slug}`,
|
|
@@ -4278,11 +4781,11 @@ var Store = class {
|
|
|
4278
4781
|
description: description.trim(),
|
|
4279
4782
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4280
4783
|
};
|
|
4281
|
-
|
|
4784
|
+
fs5.writeFileSync(path4.join(dir, "feature.json"), JSON.stringify(doc, null, 2) + "\n");
|
|
4282
4785
|
return doc;
|
|
4283
4786
|
}
|
|
4284
4787
|
deleteFeature(slug) {
|
|
4285
|
-
|
|
4788
|
+
fs5.rmSync(path4.join(this.featuresDir, this.assertSlug(slug)), { recursive: true, force: true });
|
|
4286
4789
|
}
|
|
4287
4790
|
/** Every flow that could be read.
|
|
4288
4791
|
*
|
|
@@ -4297,22 +4800,22 @@ var Store = class {
|
|
|
4297
4800
|
this.problemsFound = [];
|
|
4298
4801
|
for (const slug of features) {
|
|
4299
4802
|
const dir = path4.join(this.featuresDir, slug);
|
|
4300
|
-
if (!
|
|
4301
|
-
for (const file of
|
|
4803
|
+
if (!fs5.existsSync(dir)) continue;
|
|
4804
|
+
for (const file of fs5.readdirSync(dir)) {
|
|
4302
4805
|
if (!file.endsWith(".flow.json")) continue;
|
|
4303
4806
|
const full = path4.join(dir, file);
|
|
4304
4807
|
try {
|
|
4305
|
-
out.push(parseFlowDocument(JSON.parse(
|
|
4808
|
+
out.push(parseFlowDocument(JSON.parse(fs5.readFileSync(full, "utf8"))));
|
|
4306
4809
|
} catch (err) {
|
|
4307
4810
|
this.problemsFound.push({
|
|
4308
4811
|
path: path4.relative(this.root, full),
|
|
4309
4812
|
error: String(err instanceof Error ? err.message : err).split("\n")[0].trim(),
|
|
4310
|
-
recoverable:
|
|
4813
|
+
recoverable: fs5.existsSync(this.recoveryPathFor(full))
|
|
4311
4814
|
});
|
|
4312
4815
|
}
|
|
4313
4816
|
}
|
|
4314
4817
|
}
|
|
4315
|
-
return out.sort((a,
|
|
4818
|
+
return out.sort((a, b2) => a.name.localeCompare(b2.name));
|
|
4316
4819
|
}
|
|
4317
4820
|
problemsFound = [];
|
|
4318
4821
|
/** Documents that could not be read on the last listing, and whether a
|
|
@@ -4337,8 +4840,8 @@ var Store = class {
|
|
|
4337
4840
|
recoverFlowFile(relativePath) {
|
|
4338
4841
|
const file = path4.join(this.root, relativePath);
|
|
4339
4842
|
const backup = this.recoveryPathFor(file);
|
|
4340
|
-
if (!
|
|
4341
|
-
|
|
4843
|
+
if (!fs5.existsSync(backup)) return false;
|
|
4844
|
+
fs5.copyFileSync(backup, file);
|
|
4342
4845
|
return true;
|
|
4343
4846
|
}
|
|
4344
4847
|
/** Write a flow, atomically, keeping the previous contents recoverable.
|
|
@@ -4356,10 +4859,10 @@ var Store = class {
|
|
|
4356
4859
|
const doc = parseFlowDocument(input);
|
|
4357
4860
|
const featureSlug = this.assertSlug(doc.featureId.replace(/^f_/, ""));
|
|
4358
4861
|
const dir = path4.join(this.featuresDir, featureSlug);
|
|
4359
|
-
|
|
4862
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
4360
4863
|
const file = path4.join(dir, `${this.assertSlug(doc.slug)}.flow.json`);
|
|
4361
4864
|
const destination = this.readFlowFile(file);
|
|
4362
|
-
if (
|
|
4865
|
+
if (fs5.existsSync(file) && (!destination || destination.id !== doc.id)) {
|
|
4363
4866
|
throw new FlowConflict("the destination contains another flow or an unreadable document \u2014 choose another name or recover it first", destination?.revision ?? 0);
|
|
4364
4867
|
}
|
|
4365
4868
|
const current = this.getFlow(doc.id) ?? destination;
|
|
@@ -4377,20 +4880,20 @@ var Store = class {
|
|
|
4377
4880
|
}
|
|
4378
4881
|
doc.revision = (current?.revision ?? doc.revision ?? 0) + 1;
|
|
4379
4882
|
const previousFile = current ? path4.join(this.root, this.relPath(current)) : file;
|
|
4380
|
-
if (
|
|
4883
|
+
if (fs5.existsSync(previousFile)) {
|
|
4381
4884
|
const backup = this.recoveryPathFor(previousFile);
|
|
4382
|
-
|
|
4383
|
-
|
|
4885
|
+
fs5.mkdirSync(path4.dirname(backup), { recursive: true });
|
|
4886
|
+
fs5.copyFileSync(previousFile, backup);
|
|
4384
4887
|
this.ensureRecoveryIgnored();
|
|
4385
4888
|
}
|
|
4386
4889
|
writeFileAtomic(file, JSON.stringify(doc, null, 2) + "\n");
|
|
4387
|
-
if (previousFile !== file)
|
|
4890
|
+
if (previousFile !== file) fs5.rmSync(previousFile);
|
|
4388
4891
|
return path4.relative(this.root, file);
|
|
4389
4892
|
}
|
|
4390
4893
|
/** Read one flow file, or null when it is missing or unreadable. */
|
|
4391
4894
|
readFlowFile(file) {
|
|
4392
4895
|
try {
|
|
4393
|
-
return parseFlowDocument(JSON.parse(
|
|
4896
|
+
return parseFlowDocument(JSON.parse(fs5.readFileSync(file, "utf8")));
|
|
4394
4897
|
} catch {
|
|
4395
4898
|
return null;
|
|
4396
4899
|
}
|
|
@@ -4427,7 +4930,7 @@ var Store = class {
|
|
|
4427
4930
|
deleteFlowDocument(doc) {
|
|
4428
4931
|
const featureSlug = this.assertSlug(doc.featureId.replace(/^f_/, ""));
|
|
4429
4932
|
this.assertSlug(doc.slug);
|
|
4430
|
-
|
|
4933
|
+
fs5.rmSync(path4.join(this.featuresDir, featureSlug, `${doc.slug}.flow.json`), { force: true });
|
|
4431
4934
|
}
|
|
4432
4935
|
relPath(doc) {
|
|
4433
4936
|
return path4.join(".flowriki", "features", doc.featureId.replace(/^f_/, ""), `${doc.slug}.flow.json`);
|
|
@@ -4460,7 +4963,7 @@ var Store = class {
|
|
|
4460
4963
|
}
|
|
4461
4964
|
readConfig() {
|
|
4462
4965
|
try {
|
|
4463
|
-
const raw = JSON.parse(
|
|
4966
|
+
const raw = JSON.parse(fs5.readFileSync(this.configPath, "utf8"));
|
|
4464
4967
|
return {
|
|
4465
4968
|
// Normalised on write, so anything here is already a URL. Read back
|
|
4466
4969
|
// leniently anyway: this file is git-tracked and routinely arrives
|
|
@@ -4489,8 +4992,8 @@ var Store = class {
|
|
|
4489
4992
|
artifacts: ArtifactPolicySchema.parse(input.artifacts ?? existing.artifacts),
|
|
4490
4993
|
flakinessRepeat: clampRepeat(input.flakinessRepeat ?? existing.flakinessRepeat)
|
|
4491
4994
|
};
|
|
4492
|
-
|
|
4493
|
-
|
|
4995
|
+
fs5.mkdirSync(path4.dirname(this.configPath), { recursive: true });
|
|
4996
|
+
fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
4494
4997
|
return config;
|
|
4495
4998
|
}
|
|
4496
4999
|
/** Where a flow actually starts.
|
|
@@ -4518,7 +5021,7 @@ var Store = class {
|
|
|
4518
5021
|
}
|
|
4519
5022
|
readSecrets() {
|
|
4520
5023
|
try {
|
|
4521
|
-
return JSON.parse(
|
|
5024
|
+
return JSON.parse(fs5.readFileSync(this.secretsPath, "utf8"));
|
|
4522
5025
|
} catch {
|
|
4523
5026
|
return {};
|
|
4524
5027
|
}
|
|
@@ -4564,8 +5067,8 @@ var Store = class {
|
|
|
4564
5067
|
changed = true;
|
|
4565
5068
|
}
|
|
4566
5069
|
if (changed) {
|
|
4567
|
-
|
|
4568
|
-
|
|
5070
|
+
fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
|
|
5071
|
+
fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
|
|
4569
5072
|
this.ensureSecretsIgnored();
|
|
4570
5073
|
}
|
|
4571
5074
|
return steps;
|
|
@@ -4585,7 +5088,7 @@ var Store = class {
|
|
|
4585
5088
|
const first = name;
|
|
4586
5089
|
if (!first || first.includes("/") || first.includes("\\") || first.includes("..")) return void 0;
|
|
4587
5090
|
const file = path4.join(this.filesDir, first);
|
|
4588
|
-
return
|
|
5091
|
+
return fs5.existsSync(file) ? file : void 0;
|
|
4589
5092
|
}
|
|
4590
5093
|
/** The secret references a flow needs that this machine does not hold.
|
|
4591
5094
|
*
|
|
@@ -4612,8 +5115,8 @@ var Store = class {
|
|
|
4612
5115
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(ref)) throw new Error(`invalid secret name: ${ref}`);
|
|
4613
5116
|
const secrets = this.readSecrets();
|
|
4614
5117
|
secrets[ref] = value;
|
|
4615
|
-
|
|
4616
|
-
|
|
5118
|
+
fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
|
|
5119
|
+
fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
|
|
4617
5120
|
this.ensureSecretsIgnored();
|
|
4618
5121
|
}
|
|
4619
5122
|
/** Move plaintext passwords out of flow documents recorded before secrets
|
|
@@ -4636,7 +5139,7 @@ var Store = class {
|
|
|
4636
5139
|
const healed = [];
|
|
4637
5140
|
for (const flow of this.listFlows()) {
|
|
4638
5141
|
let changed = false;
|
|
4639
|
-
const taken = new Set(flow.steps.flatMap((
|
|
5142
|
+
const taken = new Set(flow.steps.flatMap((s2) => s2.secretRef ? [s2.secretRef] : []));
|
|
4640
5143
|
for (const step of flow.steps) {
|
|
4641
5144
|
if (!step.secret || step.secretRef || !step.value) continue;
|
|
4642
5145
|
if (isSecretRefName(step.value)) continue;
|
|
@@ -4653,8 +5156,8 @@ var Store = class {
|
|
|
4653
5156
|
}
|
|
4654
5157
|
}
|
|
4655
5158
|
if (healed.length) {
|
|
4656
|
-
|
|
4657
|
-
|
|
5159
|
+
fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
|
|
5160
|
+
fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
|
|
4658
5161
|
this.ensureSecretsIgnored();
|
|
4659
5162
|
}
|
|
4660
5163
|
return healed;
|
|
@@ -4671,10 +5174,10 @@ var Store = class {
|
|
|
4671
5174
|
/** Add one entry to .flowriki/.gitignore if it is not already there. */
|
|
4672
5175
|
ensureIgnored(line, why) {
|
|
4673
5176
|
const ignore = path4.join(this.root, ".flowriki", ".gitignore");
|
|
4674
|
-
const existing =
|
|
5177
|
+
const existing = fs5.existsSync(ignore) ? fs5.readFileSync(ignore, "utf8") : "";
|
|
4675
5178
|
if (existing.split("\n").includes(line)) return;
|
|
4676
|
-
|
|
4677
|
-
|
|
5179
|
+
fs5.mkdirSync(path4.dirname(ignore), { recursive: true });
|
|
5180
|
+
fs5.writeFileSync(
|
|
4678
5181
|
ignore,
|
|
4679
5182
|
`${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}# ${why}
|
|
4680
5183
|
${line}
|
|
@@ -4752,12 +5255,12 @@ async function executeFlow(opts) {
|
|
|
4752
5255
|
stepsTotal: flow.steps.length
|
|
4753
5256
|
});
|
|
4754
5257
|
const runDir = storage && run ? runArtifactsDir(run.id, run.projectId) : void 0;
|
|
4755
|
-
const byId = new Map(flow.steps.map((
|
|
5258
|
+
const byId = new Map(flow.steps.map((s2) => [s2.id, s2]));
|
|
4756
5259
|
const attempts = [];
|
|
4757
5260
|
const perStep = /* @__PURE__ */ new Map();
|
|
4758
5261
|
for (let attempt = 1; attempt <= repeat; attempt++) {
|
|
4759
5262
|
if (opts.signal?.aborted) break;
|
|
4760
|
-
const evidenceDir = runDir && repeat > 1 ?
|
|
5263
|
+
const evidenceDir = runDir && repeat > 1 ? path23.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
|
|
4761
5264
|
const relativeTo = (file) => repeat > 1 ? `attempt-${String(attempt).padStart(3, "0")}/${file}` : file;
|
|
4762
5265
|
const taskId = storage && run ? await storage.createTask(run.id, {
|
|
4763
5266
|
caseName: repeat > 1 ? `${flow.name} \xB7 attempt ${attempt}` : flow.name,
|
|
@@ -4914,15 +5417,15 @@ var percentile = (sorted, p) => sorted.length === 0 ? 0 : sorted[Math.min(sorted
|
|
|
4914
5417
|
function report(attempts, perStep) {
|
|
4915
5418
|
const passed = attempts.filter((a) => a.status === "passed").length;
|
|
4916
5419
|
const failed = attempts.length - passed;
|
|
4917
|
-
const steps = [...perStep].map(([stepId,
|
|
4918
|
-
const sorted = [...
|
|
5420
|
+
const steps = [...perStep].map(([stepId, s2]) => {
|
|
5421
|
+
const sorted = [...s2.durations].sort((a, b2) => a - b2);
|
|
4919
5422
|
return {
|
|
4920
5423
|
stepId,
|
|
4921
|
-
name:
|
|
4922
|
-
type:
|
|
4923
|
-
ran:
|
|
4924
|
-
failed:
|
|
4925
|
-
reasons: [...
|
|
5424
|
+
name: s2.name,
|
|
5425
|
+
type: s2.type,
|
|
5426
|
+
ran: s2.ran,
|
|
5427
|
+
failed: s2.failed,
|
|
5428
|
+
reasons: [...s2.reasons],
|
|
4926
5429
|
durationMs: { p50: percentile(sorted, 50), p99: percentile(sorted, 99), max: sorted[sorted.length - 1] ?? 0 }
|
|
4927
5430
|
};
|
|
4928
5431
|
});
|
|
@@ -4932,14 +5435,14 @@ function report(attempts, perStep) {
|
|
|
4932
5435
|
failed,
|
|
4933
5436
|
// How much a single green result is worth. 0 = every repetition agreed.
|
|
4934
5437
|
flakeRate: attempts.length ? Math.min(passed, failed) / attempts.length * 2 : 0,
|
|
4935
|
-
unstableSteps: steps.filter((
|
|
5438
|
+
unstableSteps: steps.filter((s2) => s2.failed > 0).sort((a, b2) => b2.failed - a.failed),
|
|
4936
5439
|
steps
|
|
4937
5440
|
};
|
|
4938
5441
|
}
|
|
4939
5442
|
function sizeOf(dir, file) {
|
|
4940
5443
|
if (!dir) return null;
|
|
4941
5444
|
try {
|
|
4942
|
-
return fs22.statSync(
|
|
5445
|
+
return fs22.statSync(path23.join(dir, file)).size;
|
|
4943
5446
|
} catch {
|
|
4944
5447
|
return null;
|
|
4945
5448
|
}
|
|
@@ -4992,7 +5495,7 @@ function redactPanel(state) {
|
|
|
4992
5495
|
if (!state || typeof state !== "object") return state;
|
|
4993
5496
|
const panel = state;
|
|
4994
5497
|
if (!Array.isArray(panel.steps)) return state;
|
|
4995
|
-
return { ...panel, steps: panel.steps.map((
|
|
5498
|
+
return { ...panel, steps: panel.steps.map((s2) => redact(s2)) };
|
|
4996
5499
|
}
|
|
4997
5500
|
var START_GRACE_MS = 3e3;
|
|
4998
5501
|
var RecordingCoordinator = class {
|
|
@@ -5123,7 +5626,7 @@ var RecordingCoordinator = class {
|
|
|
5123
5626
|
* resolves and attaches assertions — so this is an upsert, not an append. */
|
|
5124
5627
|
upsert(step) {
|
|
5125
5628
|
if (!this.draft) return;
|
|
5126
|
-
const i = this.draft.steps.findIndex((
|
|
5629
|
+
const i = this.draft.steps.findIndex((s2) => s2.id === step.id);
|
|
5127
5630
|
if (i === -1) this.draft.steps.push(step);
|
|
5128
5631
|
else this.draft.steps[i] = step;
|
|
5129
5632
|
this.deps.emit({ type: "recording.step", step: redact(step) });
|
|
@@ -5290,32 +5793,32 @@ var DebugCoordinator = class {
|
|
|
5290
5793
|
async command(command, args = {}) {
|
|
5291
5794
|
if (!this.current) throw new Error("no debug session is running");
|
|
5292
5795
|
if (!COMMANDS.has(command)) throw new UnknownDebugCommand(`unknown command: ${command}`);
|
|
5293
|
-
const
|
|
5796
|
+
const s2 = this.current.session;
|
|
5294
5797
|
switch (command) {
|
|
5295
5798
|
case "play":
|
|
5296
|
-
return
|
|
5799
|
+
return s2.play();
|
|
5297
5800
|
case "pause":
|
|
5298
|
-
return
|
|
5801
|
+
return s2.pause();
|
|
5299
5802
|
case "stepOver":
|
|
5300
|
-
return
|
|
5803
|
+
return s2.stepOver();
|
|
5301
5804
|
case "retry":
|
|
5302
|
-
return
|
|
5805
|
+
return s2.retry();
|
|
5303
5806
|
case "skip":
|
|
5304
|
-
return
|
|
5807
|
+
return s2.skip();
|
|
5305
5808
|
case "runTo":
|
|
5306
|
-
return
|
|
5809
|
+
return s2.runTo(String(args.stepId));
|
|
5307
5810
|
case "toggleBreakpoint":
|
|
5308
|
-
return
|
|
5811
|
+
return s2.toggleBreakpoint(String(args.stepId));
|
|
5309
5812
|
case "deleteStep":
|
|
5310
|
-
return
|
|
5813
|
+
return s2.deleteStep(String(args.stepId));
|
|
5311
5814
|
case "updateStep":
|
|
5312
|
-
return
|
|
5815
|
+
return s2.updateStep(String(args.stepId), args.patch ?? {});
|
|
5313
5816
|
case "record":
|
|
5314
|
-
return
|
|
5817
|
+
return s2.setRecording(Boolean(args.on));
|
|
5315
5818
|
case "moveStep":
|
|
5316
|
-
return
|
|
5819
|
+
return s2.moveStep(String(args.stepId), Number(args.toIndex));
|
|
5317
5820
|
case "replay":
|
|
5318
|
-
return
|
|
5821
|
+
return s2.replay();
|
|
5319
5822
|
}
|
|
5320
5823
|
}
|
|
5321
5824
|
/** Shared by every door — the local route, the hosted command, and the
|
|
@@ -5356,7 +5859,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
5356
5859
|
const { store } = deps;
|
|
5357
5860
|
const flow = store.getFlow(opts.flowId);
|
|
5358
5861
|
if (!flow) throw new FlowNotFound("flow not found");
|
|
5359
|
-
const steps = (opts.steps ?? flow.steps).map((
|
|
5862
|
+
const steps = (opts.steps ?? flow.steps).map((s2) => RecordedStepSchema.parse(s2));
|
|
5360
5863
|
if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
|
|
5361
5864
|
const baseUrl = store.baseUrlFor(flow);
|
|
5362
5865
|
const claim = deps.lease?.acquire("run");
|
|
@@ -5717,16 +6220,16 @@ var EventJournal = class {
|
|
|
5717
6220
|
return this.state ? this.state.size() : this.events.length;
|
|
5718
6221
|
}
|
|
5719
6222
|
};
|
|
5720
|
-
var { DatabaseSync } =
|
|
6223
|
+
var { DatabaseSync } = createRequire3(import.meta.url)("node:sqlite");
|
|
5721
6224
|
var AgentState = class {
|
|
5722
6225
|
constructor(file, deviceId, commandLimit = 1e5) {
|
|
5723
6226
|
this.commandLimit = commandLimit;
|
|
5724
6227
|
if (!file || file === ":memory:") throw new Error("a persistent agent state file is required");
|
|
5725
6228
|
if (!Number.isInteger(commandLimit) || commandLimit < 1) throw new Error("invalid command capacity");
|
|
5726
|
-
|
|
5727
|
-
const fd =
|
|
5728
|
-
|
|
5729
|
-
|
|
6229
|
+
fs6.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
6230
|
+
const fd = fs6.openSync(file, "a", 384);
|
|
6231
|
+
fs6.closeSync(fd);
|
|
6232
|
+
fs6.chmodSync(file, 384);
|
|
5730
6233
|
this.db = new DatabaseSync(file);
|
|
5731
6234
|
try {
|
|
5732
6235
|
this.db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
|
|
@@ -5770,7 +6273,7 @@ var AgentState = class {
|
|
|
5770
6273
|
static fingerprint(command) {
|
|
5771
6274
|
const canonical = (value) => {
|
|
5772
6275
|
if (Array.isArray(value)) return value.map(canonical);
|
|
5773
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [
|
|
6276
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b2]) => a.localeCompare(b2)).map(([key, v]) => [key, canonical(v)]));
|
|
5774
6277
|
return value;
|
|
5775
6278
|
};
|
|
5776
6279
|
const { expiresAt: _expiry, issuedAt: _issued, ...work } = command;
|
|
@@ -5917,15 +6420,11 @@ var Agent = class {
|
|
|
5917
6420
|
);
|
|
5918
6421
|
}
|
|
5919
6422
|
const delivered = command.args["flow"];
|
|
5920
|
-
const
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
"unknown_project",
|
|
5926
|
-
"this device has no project mapped under that name"
|
|
5927
|
-
);
|
|
5928
|
-
}
|
|
6423
|
+
const project = this.opts.projects.find((p) => p.id === command.projectId) ?? {
|
|
6424
|
+
id: command.projectId,
|
|
6425
|
+
name: String(delivered?.name ?? command.projectId),
|
|
6426
|
+
root: ""
|
|
6427
|
+
};
|
|
5929
6428
|
const allowed = this.opts.authorise?.(command) ?? { ok: true };
|
|
5930
6429
|
if (!allowed.ok) return this.refuse(command.id, allowed.reason);
|
|
5931
6430
|
if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
|
|
@@ -6218,11 +6717,16 @@ var scratchRoots = /* @__PURE__ */ new Map();
|
|
|
6218
6717
|
function scratchRoot(projectId) {
|
|
6219
6718
|
let root = scratchRoots.get(projectId);
|
|
6220
6719
|
if (!root) {
|
|
6221
|
-
root = fs23.mkdtempSync(
|
|
6720
|
+
root = fs23.mkdtempSync(path24.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
|
|
6222
6721
|
scratchRoots.set(projectId, root);
|
|
6223
6722
|
}
|
|
6224
6723
|
return root;
|
|
6225
6724
|
}
|
|
6725
|
+
var NO_CHECKOUT = {
|
|
6726
|
+
ok: false,
|
|
6727
|
+
reason: "failed",
|
|
6728
|
+
detail: "this machine has no checkout of that project \u2014 clone it and start the agent with that folder"
|
|
6729
|
+
};
|
|
6226
6730
|
function buildHandlers(deps) {
|
|
6227
6731
|
const { storeFor, storage = null, onActivity, lease, recorder } = deps;
|
|
6228
6732
|
const debugging = deps.debugger;
|
|
@@ -6230,6 +6734,7 @@ function buildHandlers(deps) {
|
|
|
6230
6734
|
const cancels = /* @__PURE__ */ new Map();
|
|
6231
6735
|
return {
|
|
6232
6736
|
"project.list": async (command, project) => {
|
|
6737
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6233
6738
|
const store = storeFor(project.root);
|
|
6234
6739
|
const full = command.args["documents"] === true;
|
|
6235
6740
|
return {
|
|
@@ -6254,6 +6759,7 @@ function buildHandlers(deps) {
|
|
|
6254
6759
|
};
|
|
6255
6760
|
},
|
|
6256
6761
|
"flow.get": async (command, project) => {
|
|
6762
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6257
6763
|
const store = storeFor(project.root);
|
|
6258
6764
|
const flow = store.getFlow(String(command.args["flowId"] ?? ""));
|
|
6259
6765
|
if (!flow) return { ok: false, reason: "failed", detail: "no such flow in this project" };
|
|
@@ -6265,7 +6771,7 @@ function buildHandlers(deps) {
|
|
|
6265
6771
|
// has to be able to say "this flow cannot run until you set the
|
|
6266
6772
|
// password" without the password ever leaving this machine.
|
|
6267
6773
|
unresolvedSecrets: store.unresolvedSecrets(flow),
|
|
6268
|
-
boundSecrets: flow.steps.filter((
|
|
6774
|
+
boundSecrets: flow.steps.filter((s2) => s2.secret && s2.secretRef).map((s2) => s2.secretRef).filter((ref) => store.readSecrets()[ref] !== void 0)
|
|
6269
6775
|
}
|
|
6270
6776
|
};
|
|
6271
6777
|
},
|
|
@@ -6288,6 +6794,7 @@ function buildHandlers(deps) {
|
|
|
6288
6794
|
return { ok: true, result: { ...await storage.runStats(Number(command.args["days"] ?? 7)) } };
|
|
6289
6795
|
},
|
|
6290
6796
|
"flow.save": async (command, project) => {
|
|
6797
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6291
6798
|
if (command.flowRevision === void 0) {
|
|
6292
6799
|
return { ok: false, reason: "stale_revision", detail: "supply the revision being edited; new flows require create:true and revision 0" };
|
|
6293
6800
|
}
|
|
@@ -6317,6 +6824,7 @@ function buildHandlers(deps) {
|
|
|
6317
6824
|
// keyboard. The file stays in the machine's own git repository — the
|
|
6318
6825
|
// service never gets a copy to diverge from.
|
|
6319
6826
|
"flow.update": async (command, project) => {
|
|
6827
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6320
6828
|
if (command.flowRevision === void 0) {
|
|
6321
6829
|
return { ok: false, reason: "stale_revision", detail: "this edit needs the revision it was opened from \u2014 reload the flow" };
|
|
6322
6830
|
}
|
|
@@ -6354,6 +6862,7 @@ function buildHandlers(deps) {
|
|
|
6354
6862
|
// rather than the command, the service persists no command at all, and
|
|
6355
6863
|
// neither the result below nor any event carries the value back.
|
|
6356
6864
|
"secret.set": async (command, project) => {
|
|
6865
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6357
6866
|
const store = storeFor(project.root);
|
|
6358
6867
|
const ref = String(command.args["ref"] ?? "");
|
|
6359
6868
|
const value = command.args["value"];
|
|
@@ -6373,6 +6882,7 @@ function buildHandlers(deps) {
|
|
|
6373
6882
|
// from the CLI or the local app — which, for anybody who had only ever
|
|
6374
6883
|
// used the hosted service, was none.
|
|
6375
6884
|
"feature.create": async (command, project) => {
|
|
6885
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6376
6886
|
const store = storeFor(project.root);
|
|
6377
6887
|
const name = String(command.args["name"] ?? "").trim();
|
|
6378
6888
|
if (!name) return { ok: false, reason: "failed", detail: "a feature name is required" };
|
|
@@ -6385,6 +6895,7 @@ function buildHandlers(deps) {
|
|
|
6385
6895
|
}
|
|
6386
6896
|
},
|
|
6387
6897
|
"flow.delete": async (command, project) => {
|
|
6898
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6388
6899
|
const store = storeFor(project.root);
|
|
6389
6900
|
const flowId = String(command.args["flowId"] ?? "");
|
|
6390
6901
|
if (!store.getFlow(flowId)) return { ok: true, result: { deleted: false } };
|
|
@@ -6395,6 +6906,7 @@ function buildHandlers(deps) {
|
|
|
6395
6906
|
// command because the whole point is to check an edit nobody has saved —
|
|
6396
6907
|
// saving is gated on this passing.
|
|
6397
6908
|
"flow.verify": async (command, project, emit) => {
|
|
6909
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6398
6910
|
if (!deps.verify) {
|
|
6399
6911
|
return { ok: false, reason: "failed", detail: "this agent cannot verify steps \u2014 no browser is configured" };
|
|
6400
6912
|
}
|
|
@@ -6423,6 +6935,7 @@ function buildHandlers(deps) {
|
|
|
6423
6935
|
// The browser opens on this machine and the panel lives inside that window,
|
|
6424
6936
|
// so whoever is at the keyboard sees the same state the dashboard does.
|
|
6425
6937
|
"debug.start": async (command, project) => {
|
|
6938
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6426
6939
|
if (!debugging) {
|
|
6427
6940
|
return { ok: false, reason: "failed", detail: "this agent cannot debug \u2014 no browser is configured" };
|
|
6428
6941
|
}
|
|
@@ -6471,6 +6984,7 @@ function buildHandlers(deps) {
|
|
|
6471
6984
|
}
|
|
6472
6985
|
},
|
|
6473
6986
|
"config.save": async (command, project) => {
|
|
6987
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6474
6988
|
const store = storeFor(project.root);
|
|
6475
6989
|
try {
|
|
6476
6990
|
store.writeConfig(command.args["config"]);
|
|
@@ -6483,6 +6997,7 @@ function buildHandlers(deps) {
|
|
|
6483
6997
|
// committed, and the answer is what lets the service tell "not set up yet"
|
|
6484
6998
|
// apart from "set up, but you are in the wrong clone".
|
|
6485
6999
|
"project.status": async (_command, project) => {
|
|
7000
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6486
7001
|
if (!deps.binding) {
|
|
6487
7002
|
return { ok: false, reason: "failed", detail: "this agent cannot inspect a repository" };
|
|
6488
7003
|
}
|
|
@@ -6491,6 +7006,7 @@ function buildHandlers(deps) {
|
|
|
6491
7006
|
// Write the marker, commit it, push it. Everything here happens with the
|
|
6492
7007
|
// person's own git credentials, on their own machine.
|
|
6493
7008
|
"project.claim": async (command, project) => {
|
|
7009
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6494
7010
|
if (!deps.binding) {
|
|
6495
7011
|
return { ok: false, reason: "failed", detail: "this agent cannot write to a repository" };
|
|
6496
7012
|
}
|
|
@@ -6555,7 +7071,10 @@ function buildHandlers(deps) {
|
|
|
6555
7071
|
runId = String(event.runId ?? "");
|
|
6556
7072
|
if (runId) cancels.set(runId, cancels.get(command.id));
|
|
6557
7073
|
}
|
|
6558
|
-
emit(`run.${event.type}`,
|
|
7074
|
+
emit(`run.${event.type}`, {
|
|
7075
|
+
...event,
|
|
7076
|
+
projectId: project.id
|
|
7077
|
+
});
|
|
6559
7078
|
}
|
|
6560
7079
|
});
|
|
6561
7080
|
return {
|
|
@@ -6688,7 +7207,7 @@ function buildHandlers(deps) {
|
|
|
6688
7207
|
const artifact = run.artifacts.find((a) => a.id === artifactId);
|
|
6689
7208
|
if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
|
|
6690
7209
|
const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
|
|
6691
|
-
const file =
|
|
7210
|
+
const file = path24.join(dir, artifact.relPath);
|
|
6692
7211
|
if (!isInside(dir, file) || !fs23.existsSync(file) || !isInside(fs23.realpathSync(dir), fs23.realpathSync(file))) {
|
|
6693
7212
|
return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
|
|
6694
7213
|
}
|
|
@@ -6847,15 +7366,15 @@ function createAgentRuntime(options) {
|
|
|
6847
7366
|
}
|
|
6848
7367
|
|
|
6849
7368
|
// src/engine.ts
|
|
6850
|
-
import
|
|
7369
|
+
import fs10 from "fs";
|
|
6851
7370
|
import path7 from "path";
|
|
6852
7371
|
|
|
6853
7372
|
// src/credentials.ts
|
|
6854
7373
|
import crypto3 from "crypto";
|
|
6855
|
-
import
|
|
7374
|
+
import fs8 from "fs";
|
|
6856
7375
|
|
|
6857
7376
|
// src/paths.ts
|
|
6858
|
-
import
|
|
7377
|
+
import fs7 from "fs";
|
|
6859
7378
|
import os3 from "os";
|
|
6860
7379
|
import path6 from "path";
|
|
6861
7380
|
function dataDir(env = process.env) {
|
|
@@ -6873,10 +7392,10 @@ function dataDir(env = process.env) {
|
|
|
6873
7392
|
}
|
|
6874
7393
|
function ensureDataDir(env = process.env) {
|
|
6875
7394
|
const dir = dataDir(env);
|
|
6876
|
-
|
|
7395
|
+
fs7.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6877
7396
|
if (process.platform !== "win32") {
|
|
6878
7397
|
try {
|
|
6879
|
-
|
|
7398
|
+
fs7.chmodSync(dir, 448);
|
|
6880
7399
|
} catch {
|
|
6881
7400
|
}
|
|
6882
7401
|
}
|
|
@@ -6889,7 +7408,7 @@ var ledgerPath = (deviceId, env = process.env) => path6.join(dataDir(env), `ledg
|
|
|
6889
7408
|
// src/credentials.ts
|
|
6890
7409
|
function readFile(env = process.env) {
|
|
6891
7410
|
try {
|
|
6892
|
-
const raw = JSON.parse(
|
|
7411
|
+
const raw = JSON.parse(fs8.readFileSync(credentialsPath(env), "utf8"));
|
|
6893
7412
|
if (Array.isArray(raw.connections)) return { version: 3, connections: raw.connections };
|
|
6894
7413
|
const older = Array.isArray(raw.devices) ? raw.devices : raw.deviceId ? [raw] : [];
|
|
6895
7414
|
const connections = older.filter((d) => d.deviceId && d.secret).map((d) => ({
|
|
@@ -6919,8 +7438,8 @@ function saveConnection(connection, env = process.env) {
|
|
|
6919
7438
|
connections: [...readFile(env).connections.filter((c) => c.service !== connection.service), connection]
|
|
6920
7439
|
};
|
|
6921
7440
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
6922
|
-
|
|
6923
|
-
|
|
7441
|
+
fs8.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
7442
|
+
fs8.renameSync(temporary, file);
|
|
6924
7443
|
}
|
|
6925
7444
|
function forgetConnection(service, env = process.env) {
|
|
6926
7445
|
const before = readFile(env).connections;
|
|
@@ -6929,8 +7448,8 @@ function forgetConnection(service, env = process.env) {
|
|
|
6929
7448
|
ensureDataDir(env);
|
|
6930
7449
|
const file = credentialsPath(env);
|
|
6931
7450
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
6932
|
-
|
|
6933
|
-
|
|
7451
|
+
fs8.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
|
|
7452
|
+
fs8.renameSync(temporary, file);
|
|
6934
7453
|
return true;
|
|
6935
7454
|
}
|
|
6936
7455
|
var fingerprint = (secret) => crypto3.createHash("sha256").update(secret).digest("hex").slice(0, 12);
|
|
@@ -6957,7 +7476,7 @@ function resolveService(explicit, env = process.env) {
|
|
|
6957
7476
|
}
|
|
6958
7477
|
|
|
6959
7478
|
// src/lock.ts
|
|
6960
|
-
import
|
|
7479
|
+
import fs9 from "fs";
|
|
6961
7480
|
import os4 from "os";
|
|
6962
7481
|
function takeLock(env = process.env) {
|
|
6963
7482
|
ensureDataDir(env);
|
|
@@ -6965,20 +7484,20 @@ function takeLock(env = process.env) {
|
|
|
6965
7484
|
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os4.hostname() };
|
|
6966
7485
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
6967
7486
|
try {
|
|
6968
|
-
|
|
7487
|
+
fs9.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
|
|
6969
7488
|
return { ok: true, lock: { release: () => releaseLock(file, mine.pid) } };
|
|
6970
7489
|
} catch {
|
|
6971
7490
|
const held = readLock(file);
|
|
6972
7491
|
if (!held) {
|
|
6973
7492
|
try {
|
|
6974
|
-
|
|
7493
|
+
fs9.rmSync(file, { force: true });
|
|
6975
7494
|
} catch {
|
|
6976
7495
|
}
|
|
6977
7496
|
continue;
|
|
6978
7497
|
}
|
|
6979
7498
|
if (held.host === mine.host && !alive(held.pid)) {
|
|
6980
7499
|
try {
|
|
6981
|
-
|
|
7500
|
+
fs9.rmSync(file, { force: true });
|
|
6982
7501
|
} catch {
|
|
6983
7502
|
}
|
|
6984
7503
|
continue;
|
|
@@ -6990,7 +7509,7 @@ function takeLock(env = process.env) {
|
|
|
6990
7509
|
}
|
|
6991
7510
|
function readLock(file) {
|
|
6992
7511
|
try {
|
|
6993
|
-
const held = JSON.parse(
|
|
7512
|
+
const held = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
6994
7513
|
return typeof held.pid === "number" ? { pid: held.pid, since: held.since ?? "", host: held.host ?? "" } : null;
|
|
6995
7514
|
} catch {
|
|
6996
7515
|
return null;
|
|
@@ -7009,7 +7528,7 @@ function releaseLock(file, pid) {
|
|
|
7009
7528
|
const held = readLock(file);
|
|
7010
7529
|
if (held?.pid === pid) {
|
|
7011
7530
|
try {
|
|
7012
|
-
|
|
7531
|
+
fs9.rmSync(file, { force: true });
|
|
7013
7532
|
} catch {
|
|
7014
7533
|
}
|
|
7015
7534
|
}
|
|
@@ -7038,7 +7557,7 @@ function openBrowser(url, env = process.env) {
|
|
|
7038
7557
|
}
|
|
7039
7558
|
|
|
7040
7559
|
// src/engine.ts
|
|
7041
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
7560
|
+
var AGENT_VERSION = "0.1.0-beta.7";
|
|
7042
7561
|
async function connect(options = {}) {
|
|
7043
7562
|
let service;
|
|
7044
7563
|
try {
|
|
@@ -7122,7 +7641,7 @@ function resolveCheckouts(dirs) {
|
|
|
7122
7641
|
const skipped = [];
|
|
7123
7642
|
for (const dir of dirs) {
|
|
7124
7643
|
const root = path7.resolve(dir);
|
|
7125
|
-
if (!
|
|
7644
|
+
if (!fs10.existsSync(root)) {
|
|
7126
7645
|
skipped.push({ root, why: "no such folder" });
|
|
7127
7646
|
continue;
|
|
7128
7647
|
}
|
|
@@ -7186,11 +7705,21 @@ async function start(options = {}) {
|
|
|
7186
7705
|
if (projects.length !== (connection.checkouts ?? []).length || options.projects?.length) {
|
|
7187
7706
|
saveConnection({ ...connection, checkouts: projects.map((p) => p.root) });
|
|
7188
7707
|
}
|
|
7708
|
+
const storage = new SqliteStorage();
|
|
7709
|
+
try {
|
|
7710
|
+
await storage.init();
|
|
7711
|
+
} catch (error) {
|
|
7712
|
+
options.onProgress?.({
|
|
7713
|
+
kind: "warning",
|
|
7714
|
+
message: `run history is unavailable on this machine \u2014 ${error.message}`
|
|
7715
|
+
});
|
|
7716
|
+
}
|
|
7189
7717
|
const agent = createAgentRuntime({
|
|
7190
7718
|
deviceId: connection.deviceId,
|
|
7191
7719
|
agentVersion: AGENT_VERSION,
|
|
7192
7720
|
stateFile: ledgerPath(connection.deviceId),
|
|
7193
7721
|
projects,
|
|
7722
|
+
storage,
|
|
7194
7723
|
onEvent: (event) => options.onProgress?.({ kind: event.name, message: "", detail: event.data })
|
|
7195
7724
|
});
|
|
7196
7725
|
const link = new AgentLink({
|
|
@@ -7221,7 +7750,7 @@ async function prepareBrowsers() {
|
|
|
7221
7750
|
}
|
|
7222
7751
|
let cli;
|
|
7223
7752
|
try {
|
|
7224
|
-
cli =
|
|
7753
|
+
cli = createRequire4(import.meta.url).resolve("playwright/cli.js");
|
|
7225
7754
|
} catch {
|
|
7226
7755
|
return {
|
|
7227
7756
|
ok: false,
|
|
@@ -7259,7 +7788,7 @@ function status(serviceArg) {
|
|
|
7259
7788
|
...connection ? { deviceId: connection.deviceId, machineName: connection.machineName, connectedAt: connection.connectedAt } : {},
|
|
7260
7789
|
checkouts: [
|
|
7261
7790
|
...projects.map((p) => ({ root: p.root, project: p.name })),
|
|
7262
|
-
...skipped.map((
|
|
7791
|
+
...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
|
|
7263
7792
|
],
|
|
7264
7793
|
running
|
|
7265
7794
|
};
|
|
@@ -7307,7 +7836,7 @@ async function doctor(serviceArg) {
|
|
|
7307
7836
|
function inspectPlaywright() {
|
|
7308
7837
|
try {
|
|
7309
7838
|
const browsersPath = process.env["PLAYWRIGHT_BROWSERS_PATH"] ?? (process.platform === "win32" ? path7.join(os5.homedir(), "AppData", "Local", "ms-playwright") : process.platform === "darwin" ? path7.join(os5.homedir(), "Library", "Caches", "ms-playwright") : path7.join(os5.homedir(), ".cache", "ms-playwright"));
|
|
7310
|
-
const chromiumInstalled =
|
|
7839
|
+
const chromiumInstalled = fs10.existsSync(browsersPath) ? fs10.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
|
|
7311
7840
|
return { version: null, browsersPath, chromiumInstalled };
|
|
7312
7841
|
} catch {
|
|
7313
7842
|
return { version: null, browsersPath: null, chromiumInstalled: null };
|