@flowriki/agent 0.1.0-beta.6 → 0.1.0-beta.8
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-VN4KB4YX.js} +740 -172
- 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
|
|
@@ -5335,11 +5838,25 @@ var DebugCoordinator = class {
|
|
|
5335
5838
|
const steps = await session.finish();
|
|
5336
5839
|
const flow = this.deps.store.getFlow(flowId);
|
|
5337
5840
|
if (!flow) return { ok: false, error: "flow not found" };
|
|
5338
|
-
this.deps.
|
|
5841
|
+
if (this.deps.commit.to === "caller") {
|
|
5842
|
+
const { scratch } = this.deps.commit;
|
|
5843
|
+
const before = scratch.readSecrets();
|
|
5844
|
+
scratch.extractSecrets(flow.slug, steps);
|
|
5845
|
+
const after = scratch.readSecrets();
|
|
5846
|
+
const captured = {};
|
|
5847
|
+
for (const [ref, value] of Object.entries(after)) {
|
|
5848
|
+
if (before[ref] !== value) captured[ref] = value;
|
|
5849
|
+
}
|
|
5850
|
+
const updated2 = { ...flow, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5851
|
+
this.deps.emit({ type: "debug.ended", flowId, committed: true });
|
|
5852
|
+
return { ok: true, committed: true, flow: updated2, secrets: captured };
|
|
5853
|
+
}
|
|
5854
|
+
const { store } = this.deps.commit;
|
|
5855
|
+
store.extractSecrets(flow.slug, steps);
|
|
5339
5856
|
const updated = { ...flow, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5340
|
-
|
|
5857
|
+
store.saveFlow(updated);
|
|
5341
5858
|
this.deps.emit({ type: "debug.ended", flowId, committed: true });
|
|
5342
|
-
return { ok: true, committed: true, flow: { ...updated, path:
|
|
5859
|
+
return { ok: true, committed: true, flow: { ...updated, path: store.relPath(updated) } };
|
|
5343
5860
|
} finally {
|
|
5344
5861
|
this.deps.lease?.release("debug");
|
|
5345
5862
|
}
|
|
@@ -5356,7 +5873,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
5356
5873
|
const { store } = deps;
|
|
5357
5874
|
const flow = store.getFlow(opts.flowId);
|
|
5358
5875
|
if (!flow) throw new FlowNotFound("flow not found");
|
|
5359
|
-
const steps = (opts.steps ?? flow.steps).map((
|
|
5876
|
+
const steps = (opts.steps ?? flow.steps).map((s2) => RecordedStepSchema.parse(s2));
|
|
5360
5877
|
if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
|
|
5361
5878
|
const baseUrl = store.baseUrlFor(flow);
|
|
5362
5879
|
const claim = deps.lease?.acquire("run");
|
|
@@ -5717,16 +6234,16 @@ var EventJournal = class {
|
|
|
5717
6234
|
return this.state ? this.state.size() : this.events.length;
|
|
5718
6235
|
}
|
|
5719
6236
|
};
|
|
5720
|
-
var { DatabaseSync } =
|
|
6237
|
+
var { DatabaseSync } = createRequire3(import.meta.url)("node:sqlite");
|
|
5721
6238
|
var AgentState = class {
|
|
5722
6239
|
constructor(file, deviceId, commandLimit = 1e5) {
|
|
5723
6240
|
this.commandLimit = commandLimit;
|
|
5724
6241
|
if (!file || file === ":memory:") throw new Error("a persistent agent state file is required");
|
|
5725
6242
|
if (!Number.isInteger(commandLimit) || commandLimit < 1) throw new Error("invalid command capacity");
|
|
5726
|
-
|
|
5727
|
-
const fd =
|
|
5728
|
-
|
|
5729
|
-
|
|
6243
|
+
fs6.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
|
|
6244
|
+
const fd = fs6.openSync(file, "a", 384);
|
|
6245
|
+
fs6.closeSync(fd);
|
|
6246
|
+
fs6.chmodSync(file, 384);
|
|
5730
6247
|
this.db = new DatabaseSync(file);
|
|
5731
6248
|
try {
|
|
5732
6249
|
this.db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
|
|
@@ -5770,7 +6287,7 @@ var AgentState = class {
|
|
|
5770
6287
|
static fingerprint(command) {
|
|
5771
6288
|
const canonical = (value) => {
|
|
5772
6289
|
if (Array.isArray(value)) return value.map(canonical);
|
|
5773
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [
|
|
6290
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b2]) => a.localeCompare(b2)).map(([key, v]) => [key, canonical(v)]));
|
|
5774
6291
|
return value;
|
|
5775
6292
|
};
|
|
5776
6293
|
const { expiresAt: _expiry, issuedAt: _issued, ...work } = command;
|
|
@@ -5917,15 +6434,11 @@ var Agent = class {
|
|
|
5917
6434
|
);
|
|
5918
6435
|
}
|
|
5919
6436
|
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
|
-
}
|
|
6437
|
+
const project = this.opts.projects.find((p) => p.id === command.projectId) ?? {
|
|
6438
|
+
id: command.projectId,
|
|
6439
|
+
name: String(delivered?.name ?? command.projectId),
|
|
6440
|
+
root: ""
|
|
6441
|
+
};
|
|
5929
6442
|
const allowed = this.opts.authorise?.(command) ?? { ok: true };
|
|
5930
6443
|
if (!allowed.ok) return this.refuse(command.id, allowed.reason);
|
|
5931
6444
|
if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
|
|
@@ -6218,11 +6731,16 @@ var scratchRoots = /* @__PURE__ */ new Map();
|
|
|
6218
6731
|
function scratchRoot(projectId) {
|
|
6219
6732
|
let root = scratchRoots.get(projectId);
|
|
6220
6733
|
if (!root) {
|
|
6221
|
-
root = fs23.mkdtempSync(
|
|
6734
|
+
root = fs23.mkdtempSync(path24.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
|
|
6222
6735
|
scratchRoots.set(projectId, root);
|
|
6223
6736
|
}
|
|
6224
6737
|
return root;
|
|
6225
6738
|
}
|
|
6739
|
+
var NO_CHECKOUT = {
|
|
6740
|
+
ok: false,
|
|
6741
|
+
reason: "failed",
|
|
6742
|
+
detail: "this machine has no checkout of that project \u2014 clone it and start the agent with that folder"
|
|
6743
|
+
};
|
|
6226
6744
|
function buildHandlers(deps) {
|
|
6227
6745
|
const { storeFor, storage = null, onActivity, lease, recorder } = deps;
|
|
6228
6746
|
const debugging = deps.debugger;
|
|
@@ -6230,6 +6748,7 @@ function buildHandlers(deps) {
|
|
|
6230
6748
|
const cancels = /* @__PURE__ */ new Map();
|
|
6231
6749
|
return {
|
|
6232
6750
|
"project.list": async (command, project) => {
|
|
6751
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6233
6752
|
const store = storeFor(project.root);
|
|
6234
6753
|
const full = command.args["documents"] === true;
|
|
6235
6754
|
return {
|
|
@@ -6254,6 +6773,7 @@ function buildHandlers(deps) {
|
|
|
6254
6773
|
};
|
|
6255
6774
|
},
|
|
6256
6775
|
"flow.get": async (command, project) => {
|
|
6776
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6257
6777
|
const store = storeFor(project.root);
|
|
6258
6778
|
const flow = store.getFlow(String(command.args["flowId"] ?? ""));
|
|
6259
6779
|
if (!flow) return { ok: false, reason: "failed", detail: "no such flow in this project" };
|
|
@@ -6265,7 +6785,7 @@ function buildHandlers(deps) {
|
|
|
6265
6785
|
// has to be able to say "this flow cannot run until you set the
|
|
6266
6786
|
// password" without the password ever leaving this machine.
|
|
6267
6787
|
unresolvedSecrets: store.unresolvedSecrets(flow),
|
|
6268
|
-
boundSecrets: flow.steps.filter((
|
|
6788
|
+
boundSecrets: flow.steps.filter((s2) => s2.secret && s2.secretRef).map((s2) => s2.secretRef).filter((ref) => store.readSecrets()[ref] !== void 0)
|
|
6269
6789
|
}
|
|
6270
6790
|
};
|
|
6271
6791
|
},
|
|
@@ -6288,6 +6808,7 @@ function buildHandlers(deps) {
|
|
|
6288
6808
|
return { ok: true, result: { ...await storage.runStats(Number(command.args["days"] ?? 7)) } };
|
|
6289
6809
|
},
|
|
6290
6810
|
"flow.save": async (command, project) => {
|
|
6811
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6291
6812
|
if (command.flowRevision === void 0) {
|
|
6292
6813
|
return { ok: false, reason: "stale_revision", detail: "supply the revision being edited; new flows require create:true and revision 0" };
|
|
6293
6814
|
}
|
|
@@ -6317,6 +6838,7 @@ function buildHandlers(deps) {
|
|
|
6317
6838
|
// keyboard. The file stays in the machine's own git repository — the
|
|
6318
6839
|
// service never gets a copy to diverge from.
|
|
6319
6840
|
"flow.update": async (command, project) => {
|
|
6841
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6320
6842
|
if (command.flowRevision === void 0) {
|
|
6321
6843
|
return { ok: false, reason: "stale_revision", detail: "this edit needs the revision it was opened from \u2014 reload the flow" };
|
|
6322
6844
|
}
|
|
@@ -6354,6 +6876,7 @@ function buildHandlers(deps) {
|
|
|
6354
6876
|
// rather than the command, the service persists no command at all, and
|
|
6355
6877
|
// neither the result below nor any event carries the value back.
|
|
6356
6878
|
"secret.set": async (command, project) => {
|
|
6879
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6357
6880
|
const store = storeFor(project.root);
|
|
6358
6881
|
const ref = String(command.args["ref"] ?? "");
|
|
6359
6882
|
const value = command.args["value"];
|
|
@@ -6373,6 +6896,7 @@ function buildHandlers(deps) {
|
|
|
6373
6896
|
// from the CLI or the local app — which, for anybody who had only ever
|
|
6374
6897
|
// used the hosted service, was none.
|
|
6375
6898
|
"feature.create": async (command, project) => {
|
|
6899
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6376
6900
|
const store = storeFor(project.root);
|
|
6377
6901
|
const name = String(command.args["name"] ?? "").trim();
|
|
6378
6902
|
if (!name) return { ok: false, reason: "failed", detail: "a feature name is required" };
|
|
@@ -6385,6 +6909,7 @@ function buildHandlers(deps) {
|
|
|
6385
6909
|
}
|
|
6386
6910
|
},
|
|
6387
6911
|
"flow.delete": async (command, project) => {
|
|
6912
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6388
6913
|
const store = storeFor(project.root);
|
|
6389
6914
|
const flowId = String(command.args["flowId"] ?? "");
|
|
6390
6915
|
if (!store.getFlow(flowId)) return { ok: true, result: { deleted: false } };
|
|
@@ -6395,6 +6920,7 @@ function buildHandlers(deps) {
|
|
|
6395
6920
|
// command because the whole point is to check an edit nobody has saved —
|
|
6396
6921
|
// saving is gated on this passing.
|
|
6397
6922
|
"flow.verify": async (command, project, emit) => {
|
|
6923
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6398
6924
|
if (!deps.verify) {
|
|
6399
6925
|
return { ok: false, reason: "failed", detail: "this agent cannot verify steps \u2014 no browser is configured" };
|
|
6400
6926
|
}
|
|
@@ -6423,13 +6949,23 @@ function buildHandlers(deps) {
|
|
|
6423
6949
|
// The browser opens on this machine and the panel lives inside that window,
|
|
6424
6950
|
// so whoever is at the keyboard sees the same state the dashboard does.
|
|
6425
6951
|
"debug.start": async (command, project) => {
|
|
6952
|
+
const carried = command.args["flow"];
|
|
6953
|
+
if (!carried && !project.root) return NO_CHECKOUT;
|
|
6426
6954
|
if (!debugging) {
|
|
6427
6955
|
return { ok: false, reason: "failed", detail: "this agent cannot debug \u2014 no browser is configured" };
|
|
6428
6956
|
}
|
|
6429
6957
|
try {
|
|
6430
6958
|
const started = await debugging.start(String(command.args["flowId"] ?? ""), {
|
|
6431
6959
|
...command.args["breakpoints"] ? { breakpoints: command.args["breakpoints"] } : {},
|
|
6432
|
-
...command.args["watch"] === void 0 ? {} : { watch: Boolean(command.args["watch"]) }
|
|
6960
|
+
...command.args["watch"] === void 0 ? {} : { watch: Boolean(command.args["watch"]) },
|
|
6961
|
+
...carried ? {
|
|
6962
|
+
delivered: {
|
|
6963
|
+
flow: carried,
|
|
6964
|
+
secrets: command.args["secrets"] ?? {},
|
|
6965
|
+
config: command.args["config"] ?? {},
|
|
6966
|
+
scratchRoot: scratchRoot(project.id)
|
|
6967
|
+
}
|
|
6968
|
+
} : {}
|
|
6433
6969
|
});
|
|
6434
6970
|
onActivity?.({ kind: "debugging", projectId: project.id, detail: started.flowId });
|
|
6435
6971
|
return { ok: true, result: started };
|
|
@@ -6471,6 +7007,7 @@ function buildHandlers(deps) {
|
|
|
6471
7007
|
}
|
|
6472
7008
|
},
|
|
6473
7009
|
"config.save": async (command, project) => {
|
|
7010
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6474
7011
|
const store = storeFor(project.root);
|
|
6475
7012
|
try {
|
|
6476
7013
|
store.writeConfig(command.args["config"]);
|
|
@@ -6483,6 +7020,7 @@ function buildHandlers(deps) {
|
|
|
6483
7020
|
// committed, and the answer is what lets the service tell "not set up yet"
|
|
6484
7021
|
// apart from "set up, but you are in the wrong clone".
|
|
6485
7022
|
"project.status": async (_command, project) => {
|
|
7023
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6486
7024
|
if (!deps.binding) {
|
|
6487
7025
|
return { ok: false, reason: "failed", detail: "this agent cannot inspect a repository" };
|
|
6488
7026
|
}
|
|
@@ -6491,6 +7029,7 @@ function buildHandlers(deps) {
|
|
|
6491
7029
|
// Write the marker, commit it, push it. Everything here happens with the
|
|
6492
7030
|
// person's own git credentials, on their own machine.
|
|
6493
7031
|
"project.claim": async (command, project) => {
|
|
7032
|
+
if (!project.root) return NO_CHECKOUT;
|
|
6494
7033
|
if (!deps.binding) {
|
|
6495
7034
|
return { ok: false, reason: "failed", detail: "this agent cannot write to a repository" };
|
|
6496
7035
|
}
|
|
@@ -6555,7 +7094,10 @@ function buildHandlers(deps) {
|
|
|
6555
7094
|
runId = String(event.runId ?? "");
|
|
6556
7095
|
if (runId) cancels.set(runId, cancels.get(command.id));
|
|
6557
7096
|
}
|
|
6558
|
-
emit(`run.${event.type}`,
|
|
7097
|
+
emit(`run.${event.type}`, {
|
|
7098
|
+
...event,
|
|
7099
|
+
projectId: project.id
|
|
7100
|
+
});
|
|
6559
7101
|
}
|
|
6560
7102
|
});
|
|
6561
7103
|
return {
|
|
@@ -6688,7 +7230,7 @@ function buildHandlers(deps) {
|
|
|
6688
7230
|
const artifact = run.artifacts.find((a) => a.id === artifactId);
|
|
6689
7231
|
if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
|
|
6690
7232
|
const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
|
|
6691
|
-
const file =
|
|
7233
|
+
const file = path24.join(dir, artifact.relPath);
|
|
6692
7234
|
if (!isInside(dir, file) || !fs23.existsSync(file) || !isInside(fs23.realpathSync(dir), fs23.realpathSync(file))) {
|
|
6693
7235
|
return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
|
|
6694
7236
|
}
|
|
@@ -6734,8 +7276,10 @@ function createAgentRuntime(options) {
|
|
|
6734
7276
|
const debuggerFor = (projectRoot) => {
|
|
6735
7277
|
let existing = debuggers.get(projectRoot);
|
|
6736
7278
|
if (!existing) {
|
|
7279
|
+
const store = new Store(projectRoot);
|
|
6737
7280
|
existing = new DebugCoordinator({
|
|
6738
|
-
store
|
|
7281
|
+
store,
|
|
7282
|
+
commit: { to: "disk", store },
|
|
6739
7283
|
emit: (event) => {
|
|
6740
7284
|
journal?.append({ name: `debug.${event.type.replace(/^debug\./, "")}`, data: event });
|
|
6741
7285
|
}
|
|
@@ -6744,10 +7288,24 @@ function createAgentRuntime(options) {
|
|
|
6744
7288
|
}
|
|
6745
7289
|
return existing;
|
|
6746
7290
|
};
|
|
7291
|
+
const deliveredDebugger = (delivered) => new DebugCoordinator({
|
|
7292
|
+
store: deliveredRun({
|
|
7293
|
+
flow: delivered.flow,
|
|
7294
|
+
secrets: delivered.secrets,
|
|
7295
|
+
config: delivered.config
|
|
7296
|
+
}),
|
|
7297
|
+
// Handed back rather than written, and redacted on the way out through
|
|
7298
|
+
// a scratch store — somewhere for extraction to put a password typed
|
|
7299
|
+
// during the session, so it does not travel inside the document.
|
|
7300
|
+
commit: { to: "caller", scratch: new Store(delivered.scratchRoot) },
|
|
7301
|
+
emit: (event) => {
|
|
7302
|
+
journal?.append({ name: `debug.${event.type.replace(/^debug\./, "")}`, data: event });
|
|
7303
|
+
}
|
|
7304
|
+
});
|
|
6747
7305
|
const debugControl = {
|
|
6748
7306
|
start: async (flowId, opts) => {
|
|
6749
7307
|
if (debuggingIn?.active()) throw new Error("a debug session is already running on this machine");
|
|
6750
|
-
const coordinator = debuggerFor(options.projects[0].root);
|
|
7308
|
+
const coordinator = opts.delivered ? deliveredDebugger(opts.delivered) : debuggerFor(options.projects[0].root);
|
|
6751
7309
|
const started = await coordinator.start(flowId, opts);
|
|
6752
7310
|
debuggingIn = coordinator;
|
|
6753
7311
|
return started;
|
|
@@ -6847,15 +7405,15 @@ function createAgentRuntime(options) {
|
|
|
6847
7405
|
}
|
|
6848
7406
|
|
|
6849
7407
|
// src/engine.ts
|
|
6850
|
-
import
|
|
7408
|
+
import fs10 from "fs";
|
|
6851
7409
|
import path7 from "path";
|
|
6852
7410
|
|
|
6853
7411
|
// src/credentials.ts
|
|
6854
7412
|
import crypto3 from "crypto";
|
|
6855
|
-
import
|
|
7413
|
+
import fs8 from "fs";
|
|
6856
7414
|
|
|
6857
7415
|
// src/paths.ts
|
|
6858
|
-
import
|
|
7416
|
+
import fs7 from "fs";
|
|
6859
7417
|
import os3 from "os";
|
|
6860
7418
|
import path6 from "path";
|
|
6861
7419
|
function dataDir(env = process.env) {
|
|
@@ -6873,10 +7431,10 @@ function dataDir(env = process.env) {
|
|
|
6873
7431
|
}
|
|
6874
7432
|
function ensureDataDir(env = process.env) {
|
|
6875
7433
|
const dir = dataDir(env);
|
|
6876
|
-
|
|
7434
|
+
fs7.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6877
7435
|
if (process.platform !== "win32") {
|
|
6878
7436
|
try {
|
|
6879
|
-
|
|
7437
|
+
fs7.chmodSync(dir, 448);
|
|
6880
7438
|
} catch {
|
|
6881
7439
|
}
|
|
6882
7440
|
}
|
|
@@ -6889,7 +7447,7 @@ var ledgerPath = (deviceId, env = process.env) => path6.join(dataDir(env), `ledg
|
|
|
6889
7447
|
// src/credentials.ts
|
|
6890
7448
|
function readFile(env = process.env) {
|
|
6891
7449
|
try {
|
|
6892
|
-
const raw = JSON.parse(
|
|
7450
|
+
const raw = JSON.parse(fs8.readFileSync(credentialsPath(env), "utf8"));
|
|
6893
7451
|
if (Array.isArray(raw.connections)) return { version: 3, connections: raw.connections };
|
|
6894
7452
|
const older = Array.isArray(raw.devices) ? raw.devices : raw.deviceId ? [raw] : [];
|
|
6895
7453
|
const connections = older.filter((d) => d.deviceId && d.secret).map((d) => ({
|
|
@@ -6919,8 +7477,8 @@ function saveConnection(connection, env = process.env) {
|
|
|
6919
7477
|
connections: [...readFile(env).connections.filter((c) => c.service !== connection.service), connection]
|
|
6920
7478
|
};
|
|
6921
7479
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
6922
|
-
|
|
6923
|
-
|
|
7480
|
+
fs8.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
7481
|
+
fs8.renameSync(temporary, file);
|
|
6924
7482
|
}
|
|
6925
7483
|
function forgetConnection(service, env = process.env) {
|
|
6926
7484
|
const before = readFile(env).connections;
|
|
@@ -6929,8 +7487,8 @@ function forgetConnection(service, env = process.env) {
|
|
|
6929
7487
|
ensureDataDir(env);
|
|
6930
7488
|
const file = credentialsPath(env);
|
|
6931
7489
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
6932
|
-
|
|
6933
|
-
|
|
7490
|
+
fs8.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
|
|
7491
|
+
fs8.renameSync(temporary, file);
|
|
6934
7492
|
return true;
|
|
6935
7493
|
}
|
|
6936
7494
|
var fingerprint = (secret) => crypto3.createHash("sha256").update(secret).digest("hex").slice(0, 12);
|
|
@@ -6957,7 +7515,7 @@ function resolveService(explicit, env = process.env) {
|
|
|
6957
7515
|
}
|
|
6958
7516
|
|
|
6959
7517
|
// src/lock.ts
|
|
6960
|
-
import
|
|
7518
|
+
import fs9 from "fs";
|
|
6961
7519
|
import os4 from "os";
|
|
6962
7520
|
function takeLock(env = process.env) {
|
|
6963
7521
|
ensureDataDir(env);
|
|
@@ -6965,20 +7523,20 @@ function takeLock(env = process.env) {
|
|
|
6965
7523
|
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os4.hostname() };
|
|
6966
7524
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
6967
7525
|
try {
|
|
6968
|
-
|
|
7526
|
+
fs9.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
|
|
6969
7527
|
return { ok: true, lock: { release: () => releaseLock(file, mine.pid) } };
|
|
6970
7528
|
} catch {
|
|
6971
7529
|
const held = readLock(file);
|
|
6972
7530
|
if (!held) {
|
|
6973
7531
|
try {
|
|
6974
|
-
|
|
7532
|
+
fs9.rmSync(file, { force: true });
|
|
6975
7533
|
} catch {
|
|
6976
7534
|
}
|
|
6977
7535
|
continue;
|
|
6978
7536
|
}
|
|
6979
7537
|
if (held.host === mine.host && !alive(held.pid)) {
|
|
6980
7538
|
try {
|
|
6981
|
-
|
|
7539
|
+
fs9.rmSync(file, { force: true });
|
|
6982
7540
|
} catch {
|
|
6983
7541
|
}
|
|
6984
7542
|
continue;
|
|
@@ -6990,7 +7548,7 @@ function takeLock(env = process.env) {
|
|
|
6990
7548
|
}
|
|
6991
7549
|
function readLock(file) {
|
|
6992
7550
|
try {
|
|
6993
|
-
const held = JSON.parse(
|
|
7551
|
+
const held = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
6994
7552
|
return typeof held.pid === "number" ? { pid: held.pid, since: held.since ?? "", host: held.host ?? "" } : null;
|
|
6995
7553
|
} catch {
|
|
6996
7554
|
return null;
|
|
@@ -7009,7 +7567,7 @@ function releaseLock(file, pid) {
|
|
|
7009
7567
|
const held = readLock(file);
|
|
7010
7568
|
if (held?.pid === pid) {
|
|
7011
7569
|
try {
|
|
7012
|
-
|
|
7570
|
+
fs9.rmSync(file, { force: true });
|
|
7013
7571
|
} catch {
|
|
7014
7572
|
}
|
|
7015
7573
|
}
|
|
@@ -7038,7 +7596,7 @@ function openBrowser(url, env = process.env) {
|
|
|
7038
7596
|
}
|
|
7039
7597
|
|
|
7040
7598
|
// src/engine.ts
|
|
7041
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
7599
|
+
var AGENT_VERSION = "0.1.0-beta.8";
|
|
7042
7600
|
async function connect(options = {}) {
|
|
7043
7601
|
let service;
|
|
7044
7602
|
try {
|
|
@@ -7122,7 +7680,7 @@ function resolveCheckouts(dirs) {
|
|
|
7122
7680
|
const skipped = [];
|
|
7123
7681
|
for (const dir of dirs) {
|
|
7124
7682
|
const root = path7.resolve(dir);
|
|
7125
|
-
if (!
|
|
7683
|
+
if (!fs10.existsSync(root)) {
|
|
7126
7684
|
skipped.push({ root, why: "no such folder" });
|
|
7127
7685
|
continue;
|
|
7128
7686
|
}
|
|
@@ -7186,11 +7744,21 @@ async function start(options = {}) {
|
|
|
7186
7744
|
if (projects.length !== (connection.checkouts ?? []).length || options.projects?.length) {
|
|
7187
7745
|
saveConnection({ ...connection, checkouts: projects.map((p) => p.root) });
|
|
7188
7746
|
}
|
|
7747
|
+
const storage = new SqliteStorage();
|
|
7748
|
+
try {
|
|
7749
|
+
await storage.init();
|
|
7750
|
+
} catch (error) {
|
|
7751
|
+
options.onProgress?.({
|
|
7752
|
+
kind: "warning",
|
|
7753
|
+
message: `run history is unavailable on this machine \u2014 ${error.message}`
|
|
7754
|
+
});
|
|
7755
|
+
}
|
|
7189
7756
|
const agent = createAgentRuntime({
|
|
7190
7757
|
deviceId: connection.deviceId,
|
|
7191
7758
|
agentVersion: AGENT_VERSION,
|
|
7192
7759
|
stateFile: ledgerPath(connection.deviceId),
|
|
7193
7760
|
projects,
|
|
7761
|
+
storage,
|
|
7194
7762
|
onEvent: (event) => options.onProgress?.({ kind: event.name, message: "", detail: event.data })
|
|
7195
7763
|
});
|
|
7196
7764
|
const link = new AgentLink({
|
|
@@ -7221,7 +7789,7 @@ async function prepareBrowsers() {
|
|
|
7221
7789
|
}
|
|
7222
7790
|
let cli;
|
|
7223
7791
|
try {
|
|
7224
|
-
cli =
|
|
7792
|
+
cli = createRequire4(import.meta.url).resolve("playwright/cli.js");
|
|
7225
7793
|
} catch {
|
|
7226
7794
|
return {
|
|
7227
7795
|
ok: false,
|
|
@@ -7259,7 +7827,7 @@ function status(serviceArg) {
|
|
|
7259
7827
|
...connection ? { deviceId: connection.deviceId, machineName: connection.machineName, connectedAt: connection.connectedAt } : {},
|
|
7260
7828
|
checkouts: [
|
|
7261
7829
|
...projects.map((p) => ({ root: p.root, project: p.name })),
|
|
7262
|
-
...skipped.map((
|
|
7830
|
+
...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
|
|
7263
7831
|
],
|
|
7264
7832
|
running
|
|
7265
7833
|
};
|
|
@@ -7307,7 +7875,7 @@ async function doctor(serviceArg) {
|
|
|
7307
7875
|
function inspectPlaywright() {
|
|
7308
7876
|
try {
|
|
7309
7877
|
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 =
|
|
7878
|
+
const chromiumInstalled = fs10.existsSync(browsersPath) ? fs10.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
|
|
7311
7879
|
return { version: null, browsersPath, chromiumInstalled };
|
|
7312
7880
|
} catch {
|
|
7313
7881
|
return { version: null, browsersPath: null, chromiumInstalled: null };
|