@flowriki/agent 0.1.0-beta.65 → 0.1.0-beta.66
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.
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
adjacentLabelPattern,
|
|
29
29
|
caseDebugPlan,
|
|
30
30
|
checkMobileAssertion,
|
|
31
|
+
commandFor,
|
|
32
|
+
commandSystemOf,
|
|
31
33
|
describeAssertion,
|
|
32
34
|
describeLocators,
|
|
33
35
|
expandForRun,
|
|
@@ -69,7 +71,7 @@ import {
|
|
|
69
71
|
usageKindOf,
|
|
70
72
|
validateActors,
|
|
71
73
|
webCloudOf
|
|
72
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-YD7F7JMA.js";
|
|
73
75
|
|
|
74
76
|
// src/engine/machine.ts
|
|
75
77
|
import fs from "fs";
|
|
@@ -77,7 +79,7 @@ import os from "os";
|
|
|
77
79
|
import path from "path";
|
|
78
80
|
import { createRequire } from "module";
|
|
79
81
|
import { spawnSync } from "child_process";
|
|
80
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
82
|
+
var AGENT_VERSION = "0.1.0-beta.66";
|
|
81
83
|
async function prepareBrowsers() {
|
|
82
84
|
if (browsersReady()) {
|
|
83
85
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -137,7 +139,7 @@ async function checkMachine() {
|
|
|
137
139
|
}
|
|
138
140
|
} catch {
|
|
139
141
|
}
|
|
140
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
142
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-CUDUSTFO.js");
|
|
141
143
|
return {
|
|
142
144
|
agentVersion: AGENT_VERSION,
|
|
143
145
|
node: process.versions.node,
|
|
@@ -236,7 +238,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
236
238
|
}
|
|
237
239
|
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
238
240
|
|
|
239
|
-
// ../contracts/dist/chunk-
|
|
241
|
+
// ../contracts/dist/chunk-VTO4SX4J.js
|
|
240
242
|
import { z } from "zod";
|
|
241
243
|
var PROTOCOL_VERSION = 1;
|
|
242
244
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -289,7 +291,10 @@ var CommandNameSchema = z.enum([
|
|
|
289
291
|
// a recording's — as a picture, for the live mirror. Reading.
|
|
290
292
|
"mobile.peek",
|
|
291
293
|
// What this computer has installed for web and phone testing. Reading.
|
|
292
|
-
"machine.check"
|
|
294
|
+
"machine.check",
|
|
295
|
+
// A helper run HERE — "Try it" on a command helper, which only the
|
|
296
|
+
// tester's computer can run. NOT baseline.
|
|
297
|
+
"helper.run"
|
|
293
298
|
]);
|
|
294
299
|
var EFFECTFUL = /* @__PURE__ */ new Set([
|
|
295
300
|
"record.start",
|
|
@@ -333,7 +338,9 @@ var EFFECTFUL = /* @__PURE__ */ new Set([
|
|
|
333
338
|
"project.claim",
|
|
334
339
|
"run.cancel",
|
|
335
340
|
// A phone session taps and types on somebody's device.
|
|
336
|
-
"mobile.session"
|
|
341
|
+
"mobile.session",
|
|
342
|
+
// Runs a command on somebody's computer.
|
|
343
|
+
"helper.run"
|
|
337
344
|
]);
|
|
338
345
|
var CommandSchema = z.object({
|
|
339
346
|
/** Unique per command. The agent stores it, so a redelivery after a
|
|
@@ -484,7 +491,7 @@ function compareAgentVersions(a, b2) {
|
|
|
484
491
|
}
|
|
485
492
|
return 0;
|
|
486
493
|
}
|
|
487
|
-
var agentUpdateFor = (
|
|
494
|
+
var agentUpdateFor = (running2, latest) => Boolean(running2 && latest && running2 !== "test" && compareAgentVersions(latest, running2) > 0);
|
|
488
495
|
|
|
489
496
|
// src/service.ts
|
|
490
497
|
var BadServiceUrl = class extends Error {
|
|
@@ -1843,7 +1850,7 @@ function installDebugPanel() {
|
|
|
1843
1850
|
pendingRender = false;
|
|
1844
1851
|
const p = root.getElementById("p");
|
|
1845
1852
|
p.className = "p" + (vertical ? " v" : "");
|
|
1846
|
-
const
|
|
1853
|
+
const running2 = state.status === "running";
|
|
1847
1854
|
const done = state.status === "finished";
|
|
1848
1855
|
const stCls = state.failure ? "failed" : state.recording ? "running" : state.status;
|
|
1849
1856
|
const stLabel = state.recording ? "recording" : state.failure ? "paused at a failure" : state.status === "idle" ? "not started" : state.status;
|
|
@@ -1936,7 +1943,7 @@ function installDebugPanel() {
|
|
|
1936
1943
|
});
|
|
1937
1944
|
return;
|
|
1938
1945
|
}
|
|
1939
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (
|
|
1946
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (running2 || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry" title="Run this step again">Retry</button>' : "") + '<button id="step" title="Run exactly one step" ' + (running2 || done ? "disabled" : "") + '>Step</button><button id="pause" title="Stop after the step in flight" ' + (running2 ? "" : "disabled") + '>Pause</button><button id="replay" title="Start over from the first step" ' + (running2 ? "disabled" : "") + '>Replay</button><span class="dv"></span>' + // While armed, this is the only way to stop it — so it says so, rather
|
|
1940
1947
|
// than reading as a status label nobody realises is a button.
|
|
1941
1948
|
(state.recording ? '<button id="rec" class="danger" title="Stop capturing"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec" title="Capture new steps at the cursor">Record</button>') + // AUTHORING A CHECK BELONGS HERE MOST OF ALL. Recording had it and
|
|
1942
1949
|
// debugging did not, which is the wrong way round: the moment worth
|
|
@@ -2474,9 +2481,9 @@ var RunActors = class {
|
|
|
2474
2481
|
const previous = this.sessions.get(id);
|
|
2475
2482
|
const round = (this.resets.get(id) ?? 0) + 1;
|
|
2476
2483
|
this.resets.set(id, round);
|
|
2477
|
-
const
|
|
2478
|
-
const session = await openRunSession({ ...this.opts, browser: this.main.browser, ...this.evidenceIn(
|
|
2479
|
-
this.homes.set(session,
|
|
2484
|
+
const home2 = `actor-${id}-${round}`;
|
|
2485
|
+
const session = await openRunSession({ ...this.opts, browser: this.main.browser, ...this.evidenceIn(home2) });
|
|
2486
|
+
this.homes.set(session, home2);
|
|
2480
2487
|
this.sessions.set(id, session);
|
|
2481
2488
|
this.current = session;
|
|
2482
2489
|
this.currentId = id;
|
|
@@ -7025,7 +7032,7 @@ function ensureRootCommit(root) {
|
|
|
7025
7032
|
}
|
|
7026
7033
|
|
|
7027
7034
|
// ../helpers/dist/index.js
|
|
7028
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
7035
|
+
import { createHash, createHmac as createHmac2 } from "crypto";
|
|
7029
7036
|
var HelperFailure = class extends Error {
|
|
7030
7037
|
constructor(message) {
|
|
7031
7038
|
super(message);
|
|
@@ -7279,6 +7286,82 @@ async function runHttp(config, ctx) {
|
|
|
7279
7286
|
}
|
|
7280
7287
|
}
|
|
7281
7288
|
}
|
|
7289
|
+
function commandFingerprint(config, system) {
|
|
7290
|
+
const line = commandFor(config, system);
|
|
7291
|
+
const written = JSON.stringify({
|
|
7292
|
+
program: line.program,
|
|
7293
|
+
args: line.args,
|
|
7294
|
+
folder: config.folder,
|
|
7295
|
+
env: config.env,
|
|
7296
|
+
secretEnv: Object.keys(config.secretEnv).sort(),
|
|
7297
|
+
mode: config.mode,
|
|
7298
|
+
readyPattern: config.readyPattern
|
|
7299
|
+
});
|
|
7300
|
+
return createHash("sha256").update(written).digest("hex").slice(0, 16);
|
|
7301
|
+
}
|
|
7302
|
+
var lastLine = (text) => text.trim().split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
7303
|
+
async function runCommand(config, ctx, name) {
|
|
7304
|
+
if (!ctx.commands) {
|
|
7305
|
+
throw new HelperFailure("this helper runs a command on a tester's computer \u2014 try it, and run tests that use it, from a computer running the Flowriki agent");
|
|
7306
|
+
}
|
|
7307
|
+
const system = ctx.system ?? commandSystemOf(process.platform);
|
|
7308
|
+
const line = commandFor(config, system);
|
|
7309
|
+
const env = {};
|
|
7310
|
+
for (const [key, value] of Object.entries(config.env)) env[key] = fill(value, ctx.inputs);
|
|
7311
|
+
const redact2 = [];
|
|
7312
|
+
for (const [key, ref] of Object.entries(config.secretEnv)) {
|
|
7313
|
+
const value = secret(ctx, ref, `${key} secret`);
|
|
7314
|
+
env[key] = value;
|
|
7315
|
+
redact2.push(value);
|
|
7316
|
+
}
|
|
7317
|
+
const outcome = await ctx.commands.run({
|
|
7318
|
+
name,
|
|
7319
|
+
fingerprint: commandFingerprint(config, system),
|
|
7320
|
+
program: fill(line.program, ctx.inputs),
|
|
7321
|
+
// EACH ARGUMENT FILLED ON ITS OWN: a value is never split into two, and
|
|
7322
|
+
// never read by a shell.
|
|
7323
|
+
args: line.args.map((arg) => fill(arg, ctx.inputs)),
|
|
7324
|
+
folder: fill(config.folder, ctx.inputs),
|
|
7325
|
+
env,
|
|
7326
|
+
redact: redact2,
|
|
7327
|
+
mode: config.mode,
|
|
7328
|
+
readyPattern: config.readyPattern,
|
|
7329
|
+
timeoutMs: config.timeoutSeconds * 1e3
|
|
7330
|
+
});
|
|
7331
|
+
const evidence = {
|
|
7332
|
+
...outcome.exitCode === null ? { state: config.mode === "background" ? "running until the test ends" : "stopped" } : { "exit code": String(outcome.exitCode) },
|
|
7333
|
+
...lastLine(outcome.stdout) ? { "last line": lastLine(outcome.stdout).slice(0, 200) } : {}
|
|
7334
|
+
};
|
|
7335
|
+
if (config.mode === "run" && outcome.exitCode !== config.expectExit) {
|
|
7336
|
+
const why = lastLine(outcome.stderr) || lastLine(outcome.stdout);
|
|
7337
|
+
throw new HelperFailure(`the command ended with exit code ${outcome.exitCode ?? "?"}${config.expectExit ? ` (expected ${config.expectExit})` : ""}${why ? `: ${why.slice(0, 300)}` : ""}`);
|
|
7338
|
+
}
|
|
7339
|
+
const text = outcome.stdout;
|
|
7340
|
+
switch (config.extract.take) {
|
|
7341
|
+
case "nothing":
|
|
7342
|
+
return { value: "", detail: config.mode === "background" ? "started, and runs until the test ends" : "it ran", evidence };
|
|
7343
|
+
case "output": {
|
|
7344
|
+
const value = text.trim();
|
|
7345
|
+
return { value: value.length > 2e3 ? lastLine(value) : value, detail: "what the command printed", evidence };
|
|
7346
|
+
}
|
|
7347
|
+
case "pattern": {
|
|
7348
|
+
const value = takeCode(text, config.extract.pattern);
|
|
7349
|
+
if (value === null) throw new HelperFailure(`the command printed nothing matching ${config.extract.pattern}`);
|
|
7350
|
+
return { value, detail: "the match in what the command printed", evidence };
|
|
7351
|
+
}
|
|
7352
|
+
case "json": {
|
|
7353
|
+
let parsed;
|
|
7354
|
+
try {
|
|
7355
|
+
parsed = JSON.parse(text);
|
|
7356
|
+
} catch {
|
|
7357
|
+
throw new HelperFailure("the command did not print JSON");
|
|
7358
|
+
}
|
|
7359
|
+
const value = atPath(parsed, config.extract.path);
|
|
7360
|
+
if (value === void 0 || value === null) throw new HelperFailure(`what the command printed has no "${config.extract.path}"`);
|
|
7361
|
+
return { value: typeof value === "string" ? value : JSON.stringify(value), detail: `"${config.extract.path}" from what the command printed`, evidence };
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7282
7365
|
async function runHelper(helper, ctx) {
|
|
7283
7366
|
const config = "config" in helper ? helper.config : helper;
|
|
7284
7367
|
switch (config.kind) {
|
|
@@ -7292,11 +7375,17 @@ async function runHelper(helper, ctx) {
|
|
|
7292
7375
|
return runSms(config, ctx);
|
|
7293
7376
|
case "http":
|
|
7294
7377
|
return runHttp(config, ctx);
|
|
7378
|
+
case "command":
|
|
7379
|
+
return runCommand(config, ctx, "name" in helper ? helper.name : "this command");
|
|
7295
7380
|
}
|
|
7296
7381
|
}
|
|
7297
7382
|
|
|
7298
7383
|
// ../execution/dist/index.js
|
|
7384
|
+
import { spawn as spawn2 } from "child_process";
|
|
7299
7385
|
import fs24 from "fs";
|
|
7386
|
+
import os5 from "os";
|
|
7387
|
+
import path24 from "path";
|
|
7388
|
+
import fs34 from "fs";
|
|
7300
7389
|
import crypto3 from "crypto";
|
|
7301
7390
|
async function cloudBrowserFor(config, secrets, name) {
|
|
7302
7391
|
const settings = config ?? {};
|
|
@@ -7470,6 +7559,7 @@ async function executeFlow(opts) {
|
|
|
7470
7559
|
writes.push(emit({ type: "run.step", runId: run?.id ?? "", flowId: flow.id, result }));
|
|
7471
7560
|
}
|
|
7472
7561
|
});
|
|
7562
|
+
store.runHelper?.close?.();
|
|
7473
7563
|
if (storage && taskId && run) {
|
|
7474
7564
|
const reached = new Set(outcome.steps.map((r) => r.stepId));
|
|
7475
7565
|
for (const step of flow.steps) {
|
|
@@ -7635,27 +7725,203 @@ function toStepRow(ordinal, result, step) {
|
|
|
7635
7725
|
checks
|
|
7636
7726
|
};
|
|
7637
7727
|
}
|
|
7728
|
+
var home = () => process.env["FLOWRIKI_HOME"] || path24.join(os5.homedir(), ".flowriki");
|
|
7729
|
+
var approvalsFile = () => path24.join(home(), "allowed-commands.json");
|
|
7730
|
+
function readApprovals() {
|
|
7731
|
+
try {
|
|
7732
|
+
const parsed = JSON.parse(fs24.readFileSync(approvalsFile(), "utf8"));
|
|
7733
|
+
return { allowed: parsed.allowed ?? {}, waiting: parsed.waiting ?? {} };
|
|
7734
|
+
} catch {
|
|
7735
|
+
return { allowed: {}, waiting: {} };
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
function writeApprovals(approvals) {
|
|
7739
|
+
fs24.mkdirSync(home(), { recursive: true, mode: 448 });
|
|
7740
|
+
const temporary = `${approvalsFile()}.${process.pid}.tmp`;
|
|
7741
|
+
fs24.writeFileSync(temporary, JSON.stringify(approvals, null, 2), { mode: 384 });
|
|
7742
|
+
fs24.renameSync(temporary, approvalsFile());
|
|
7743
|
+
}
|
|
7744
|
+
function allowCommands(which) {
|
|
7745
|
+
const approvals = readApprovals();
|
|
7746
|
+
const wanted = which.trim().toLowerCase();
|
|
7747
|
+
const chosen = Object.values(approvals.waiting).filter((c) => wanted === "all" || c.fingerprint === wanted || c.name.toLowerCase() === wanted);
|
|
7748
|
+
for (const command of chosen) {
|
|
7749
|
+
approvals.allowed[command.fingerprint] = { ...command, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7750
|
+
delete approvals.waiting[command.fingerprint];
|
|
7751
|
+
}
|
|
7752
|
+
if (chosen.length) writeApprovals(approvals);
|
|
7753
|
+
return chosen;
|
|
7754
|
+
}
|
|
7755
|
+
function revokeCommands(which) {
|
|
7756
|
+
const approvals = readApprovals();
|
|
7757
|
+
const wanted = which.trim().toLowerCase();
|
|
7758
|
+
const chosen = Object.values(approvals.allowed).filter((c) => wanted === "all" || c.fingerprint === wanted || c.name.toLowerCase() === wanted);
|
|
7759
|
+
for (const command of chosen) delete approvals.allowed[command.fingerprint];
|
|
7760
|
+
if (chosen.length) writeApprovals(approvals);
|
|
7761
|
+
return chosen;
|
|
7762
|
+
}
|
|
7763
|
+
var CommandNotAllowed = class extends HelperFailure {
|
|
7764
|
+
constructor(command) {
|
|
7765
|
+
super(`"${command.name}" has not been allowed on this computer yet. In a terminal here, run: flowriki-agent allow "${command.name}" \u2014 it shows the command before allowing it.`);
|
|
7766
|
+
this.command = command;
|
|
7767
|
+
this.name = "CommandNotAllowed";
|
|
7768
|
+
}
|
|
7769
|
+
command;
|
|
7770
|
+
code = "command_not_allowed";
|
|
7771
|
+
};
|
|
7772
|
+
var KEEP_BYTES = 64 * 1024;
|
|
7773
|
+
var BACKGROUND_LIMIT_MS = 30 * 6e4;
|
|
7774
|
+
var SETTLE_MS = 2e3;
|
|
7775
|
+
var running = /* @__PURE__ */ new Set();
|
|
7776
|
+
process.once("exit", () => {
|
|
7777
|
+
for (const child of running) {
|
|
7778
|
+
try {
|
|
7779
|
+
child.kill("SIGKILL");
|
|
7780
|
+
} catch {
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
});
|
|
7784
|
+
function stop(child) {
|
|
7785
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
7786
|
+
try {
|
|
7787
|
+
child.kill("SIGTERM");
|
|
7788
|
+
} catch {
|
|
7789
|
+
}
|
|
7790
|
+
const hard = setTimeout(() => {
|
|
7791
|
+
try {
|
|
7792
|
+
child.kill("SIGKILL");
|
|
7793
|
+
} catch {
|
|
7794
|
+
}
|
|
7795
|
+
}, 2e3);
|
|
7796
|
+
hard.unref();
|
|
7797
|
+
}
|
|
7798
|
+
function launchLine(program, args) {
|
|
7799
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(program)) {
|
|
7800
|
+
const unsafe = [program, ...args].find((part) => /[&|<>^%!"]/.test(part));
|
|
7801
|
+
if (unsafe !== void 0) throw new HelperFailure(`on Windows a .cmd or .bat cannot take "${unsafe}" \u2014 those characters mean something to cmd.exe`);
|
|
7802
|
+
return { file: process.env["ComSpec"] || "cmd.exe", args: ["/d", "/c", program, ...args] };
|
|
7803
|
+
}
|
|
7804
|
+
return { file: program, args };
|
|
7805
|
+
}
|
|
7806
|
+
function localCommandRunner(opts) {
|
|
7807
|
+
const mine = /* @__PURE__ */ new Set();
|
|
7808
|
+
const trustAll = opts.trustAll ?? process.env["FLOWRIKI_TRUST_COMMANDS"] === "1";
|
|
7809
|
+
const run = async (request) => {
|
|
7810
|
+
const record = {
|
|
7811
|
+
fingerprint: request.fingerprint,
|
|
7812
|
+
name: request.name,
|
|
7813
|
+
program: request.program,
|
|
7814
|
+
args: request.args,
|
|
7815
|
+
folder: request.folder,
|
|
7816
|
+
env: Object.keys(request.env),
|
|
7817
|
+
mode: request.mode,
|
|
7818
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7819
|
+
};
|
|
7820
|
+
if (!trustAll) {
|
|
7821
|
+
const approvals = readApprovals();
|
|
7822
|
+
if (!approvals.allowed[request.fingerprint]) {
|
|
7823
|
+
approvals.waiting[request.fingerprint] = record;
|
|
7824
|
+
try {
|
|
7825
|
+
writeApprovals(approvals);
|
|
7826
|
+
} catch {
|
|
7827
|
+
}
|
|
7828
|
+
throw new CommandNotAllowed(record);
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7831
|
+
const cwd = request.folder ? path24.resolve(opts.root, request.folder) : opts.root;
|
|
7832
|
+
if (!fs24.existsSync(cwd)) throw new HelperFailure(`the folder ${cwd} does not exist on this computer`);
|
|
7833
|
+
const blank = (text) => request.redact.reduce((out, secret2) => secret2 ? out.split(secret2).join("\u2022\u2022\u2022") : out, text);
|
|
7834
|
+
const line = launchLine(request.program, request.args);
|
|
7835
|
+
return await new Promise((resolve2, reject) => {
|
|
7836
|
+
let stdout = "", stderr = "", settled = false;
|
|
7837
|
+
let child;
|
|
7838
|
+
try {
|
|
7839
|
+
child = spawn2(line.file, line.args, { cwd, env: { ...process.env, ...request.env }, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
7840
|
+
} catch (error) {
|
|
7841
|
+
reject(new HelperFailure(`${request.program} could not be started: ${error.message}`));
|
|
7842
|
+
return;
|
|
7843
|
+
}
|
|
7844
|
+
running.add(child);
|
|
7845
|
+
mine.add(child);
|
|
7846
|
+
const forget = () => {
|
|
7847
|
+
running.delete(child);
|
|
7848
|
+
mine.delete(child);
|
|
7849
|
+
};
|
|
7850
|
+
const finish = (outcome) => {
|
|
7851
|
+
if (settled) return;
|
|
7852
|
+
settled = true;
|
|
7853
|
+
clearTimeout(timer);
|
|
7854
|
+
if (outcome instanceof Error) reject(outcome);
|
|
7855
|
+
else resolve2(outcome);
|
|
7856
|
+
};
|
|
7857
|
+
const ready = request.readyPattern ? new RegExp(request.readyPattern) : null;
|
|
7858
|
+
const take = (into) => (chunk) => {
|
|
7859
|
+
const text = chunk.toString();
|
|
7860
|
+
if (into === "out") stdout = (stdout + text).slice(-KEEP_BYTES);
|
|
7861
|
+
else stderr = (stderr + text).slice(-KEEP_BYTES);
|
|
7862
|
+
if (request.mode === "background" && ready && ready.test(into === "out" ? stdout : stderr)) {
|
|
7863
|
+
finish({ exitCode: null, stdout: blank(stdout), stderr: blank(stderr) });
|
|
7864
|
+
}
|
|
7865
|
+
};
|
|
7866
|
+
child.stdout?.on("data", take("out"));
|
|
7867
|
+
child.stderr?.on("data", take("err"));
|
|
7868
|
+
child.on("error", (error) => {
|
|
7869
|
+
forget();
|
|
7870
|
+
finish(new HelperFailure(error.code === "ENOENT" ? `${request.program} was not found on this computer \u2014 install it, or give its full path` : `${request.program} could not be started: ${error.message}`));
|
|
7871
|
+
});
|
|
7872
|
+
child.on("close", (code) => {
|
|
7873
|
+
forget();
|
|
7874
|
+
if (request.mode === "background" && !settled) {
|
|
7875
|
+
const why = (stderr || stdout).trim().split(/\r?\n/).pop() ?? "";
|
|
7876
|
+
finish(new HelperFailure(`the command stopped before it was ready (exit ${code ?? "?"})${why ? `: ${blank(why).slice(0, 300)}` : ""}`));
|
|
7877
|
+
return;
|
|
7878
|
+
}
|
|
7879
|
+
finish({ exitCode: code, stdout: blank(stdout), stderr: blank(stderr) });
|
|
7880
|
+
});
|
|
7881
|
+
const timer = request.mode === "background" && !ready ? setTimeout(() => finish({ exitCode: null, stdout: blank(stdout), stderr: blank(stderr) }), Math.min(SETTLE_MS, request.timeoutMs)) : setTimeout(() => {
|
|
7882
|
+
stop(child);
|
|
7883
|
+
finish(new HelperFailure(request.mode === "background" ? `the command did not say it was ready within ${Math.round(request.timeoutMs / 1e3)}s` : `the command did not finish within ${Math.round(request.timeoutMs / 1e3)}s, so it was stopped`));
|
|
7884
|
+
}, request.timeoutMs);
|
|
7885
|
+
if (request.mode === "background") {
|
|
7886
|
+
const limit = setTimeout(() => stop(child), BACKGROUND_LIMIT_MS);
|
|
7887
|
+
limit.unref();
|
|
7888
|
+
}
|
|
7889
|
+
});
|
|
7890
|
+
};
|
|
7891
|
+
return {
|
|
7892
|
+
run,
|
|
7893
|
+
/** Stop every background command this runner started — the test is over. */
|
|
7894
|
+
close: () => {
|
|
7895
|
+
for (const child of mine) stop(child);
|
|
7896
|
+
mine.clear();
|
|
7897
|
+
}
|
|
7898
|
+
};
|
|
7899
|
+
}
|
|
7638
7900
|
var CLOCK_SLACK_MS = 1e4;
|
|
7639
|
-
function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date()) {
|
|
7901
|
+
function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date(), root = process.cwd()) {
|
|
7640
7902
|
if (!Array.isArray(delivered) || delivered.length === 0) return void 0;
|
|
7641
7903
|
const helpers = new Map(
|
|
7642
7904
|
DeliveredHelperSchema.array().parse(delivered).map((helper) => [helper.id, helper])
|
|
7643
7905
|
);
|
|
7644
7906
|
const since = new Date(startedAt.getTime() - CLOCK_SLACK_MS);
|
|
7645
|
-
|
|
7907
|
+
const commands = [...helpers.values()].some((h) => h.config.kind === "command") ? localCommandRunner({ root }) : void 0;
|
|
7908
|
+
const run = (async (helperId, inputs) => {
|
|
7646
7909
|
const helper = helpers.get(helperId);
|
|
7647
7910
|
if (!helper) throw new Error("that helper was deleted, or was not sent with this run \u2014 choose another");
|
|
7648
7911
|
try {
|
|
7649
|
-
const { value, detail } = await runHelper(helper
|
|
7912
|
+
const { value, detail } = await runHelper(helper, {
|
|
7650
7913
|
resolveSecret: (ref) => secrets?.[ref],
|
|
7651
7914
|
inputs,
|
|
7652
|
-
since
|
|
7915
|
+
since,
|
|
7916
|
+
...commands ? { commands } : {}
|
|
7653
7917
|
});
|
|
7654
7918
|
return { value, detail: `${helper.name}: ${detail}` };
|
|
7655
7919
|
} catch (error) {
|
|
7656
7920
|
throw new Error(`${helper.name}: ${error.message}`);
|
|
7657
7921
|
}
|
|
7658
|
-
};
|
|
7922
|
+
});
|
|
7923
|
+
run.close = () => commands?.close();
|
|
7924
|
+
return run;
|
|
7659
7925
|
}
|
|
7660
7926
|
var LOCAL_CHROMIUM2 = { id: "chromium", provider: "local", engine: "chromium" };
|
|
7661
7927
|
async function summaryOf2(storage, runId) {
|
|
@@ -7829,6 +8095,7 @@ async function executeCase(opts) {
|
|
|
7829
8095
|
};
|
|
7830
8096
|
}
|
|
7831
8097
|
await cloud?.close().catch(() => void 0);
|
|
8098
|
+
helpers?.close();
|
|
7832
8099
|
if (lane) outcome = { ...outcome, artifacts: [...outcome.artifacts ?? [], ...await lane.close(outcome.status === "passed")] };
|
|
7833
8100
|
await Promise.all(writes);
|
|
7834
8101
|
if (storage && run) {
|
|
@@ -7854,7 +8121,7 @@ async function executeCase(opts) {
|
|
|
7854
8121
|
await storage.finishRun(run.id, {
|
|
7855
8122
|
status: outcome.status === "passed" ? "passed" : outcome.error === "the run was cancelled" ? "cancelled" : outcome.status === "failed" ? "failed" : "errored",
|
|
7856
8123
|
errorMessage: outcome.error,
|
|
7857
|
-
artifactsDir: runDir &&
|
|
8124
|
+
artifactsDir: runDir && fs34.existsSync(runDir) ? runDir : null
|
|
7858
8125
|
});
|
|
7859
8126
|
}
|
|
7860
8127
|
await emit({ type: "case.finished", runId: run?.id ?? "", caseId: testCase.id, outcome });
|
|
@@ -8539,10 +8806,10 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8539
8806
|
}
|
|
8540
8807
|
function deliveredRun(delivered) {
|
|
8541
8808
|
const { flow, secrets, config } = delivered;
|
|
8542
|
-
const
|
|
8809
|
+
const runHelper3 = helperRunner(delivered.helpers, secrets);
|
|
8543
8810
|
return {
|
|
8544
8811
|
resolveAuth: authenticationResolver(delivered.authentications),
|
|
8545
|
-
...
|
|
8812
|
+
...runHelper3 ? { runHelper: runHelper3 } : {},
|
|
8546
8813
|
readConfig: () => config,
|
|
8547
8814
|
// By id, and only the one that was sent. A machine serving no checkout has
|
|
8548
8815
|
// no other flow to confuse it with.
|
|
@@ -9017,10 +9284,10 @@ var Agent = class {
|
|
|
9017
9284
|
if (!allowed.ok) return this.refuse(command.id, allowed.reason);
|
|
9018
9285
|
if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
|
|
9019
9286
|
const fingerprint2 = AgentState.fingerprint(command);
|
|
9020
|
-
const
|
|
9021
|
-
if (
|
|
9022
|
-
if (
|
|
9023
|
-
return { ...await
|
|
9287
|
+
const running2 = this.inFlight.get(command.id);
|
|
9288
|
+
if (running2) {
|
|
9289
|
+
if (running2.fingerprint !== fingerprint2) return this.refuse(command.id, "command_conflict", "this command ID names different work");
|
|
9290
|
+
return { ...await running2.promise, status: "duplicate" };
|
|
9024
9291
|
}
|
|
9025
9292
|
const previous = this.previous(command.id, fingerprint2);
|
|
9026
9293
|
if (previous) return previous;
|
|
@@ -9221,18 +9488,19 @@ var AgentLink = class {
|
|
|
9221
9488
|
|
|
9222
9489
|
// ../provider-local/dist/index.js
|
|
9223
9490
|
import fs10 from "fs";
|
|
9224
|
-
import
|
|
9491
|
+
import os6 from "os";
|
|
9225
9492
|
import path10 from "path";
|
|
9226
9493
|
import fs25 from "fs";
|
|
9227
|
-
import
|
|
9494
|
+
import path25 from "path";
|
|
9228
9495
|
import { randomUUID } from "crypto";
|
|
9229
9496
|
import { execFile } from "child_process";
|
|
9230
|
-
import
|
|
9497
|
+
import fs35 from "fs";
|
|
9231
9498
|
import path34 from "path";
|
|
9232
9499
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9500
|
+
import os22 from "os";
|
|
9233
9501
|
import { spawn as nodeSpawn } from "child_process";
|
|
9234
9502
|
import fs43 from "fs";
|
|
9235
|
-
import
|
|
9503
|
+
import os32 from "os";
|
|
9236
9504
|
import path42 from "path";
|
|
9237
9505
|
var NO_CHECKOUT = {
|
|
9238
9506
|
ok: false,
|
|
@@ -9243,7 +9511,7 @@ var scratchRoots = /* @__PURE__ */ new Map();
|
|
|
9243
9511
|
function scratchRoot(projectId) {
|
|
9244
9512
|
let root = scratchRoots.get(projectId);
|
|
9245
9513
|
if (!root) {
|
|
9246
|
-
root = fs10.mkdtempSync(path10.join(
|
|
9514
|
+
root = fs10.mkdtempSync(path10.join(os6.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
|
|
9247
9515
|
scratchRoots.set(projectId, root);
|
|
9248
9516
|
}
|
|
9249
9517
|
return root;
|
|
@@ -9477,7 +9745,7 @@ function historyHandlers({ storage, artifactLimit }) {
|
|
|
9477
9745
|
const artifact = run.artifacts.find((a) => a.id === artifactId);
|
|
9478
9746
|
if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
|
|
9479
9747
|
const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
|
|
9480
|
-
const file2 =
|
|
9748
|
+
const file2 = path25.join(dir, artifact.relPath);
|
|
9481
9749
|
if (!isInside(dir, file2) || !fs25.existsSync(file2) || !isInside(fs25.realpathSync(dir), fs25.realpathSync(file2))) {
|
|
9482
9750
|
return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
|
|
9483
9751
|
}
|
|
@@ -9908,7 +10176,7 @@ function debuggingHandlers(ctx) {
|
|
|
9908
10176
|
var file = () => path34.join(flowrikiHome(), "assistant-folders.json");
|
|
9909
10177
|
function readAll() {
|
|
9910
10178
|
try {
|
|
9911
|
-
const parsed = JSON.parse(
|
|
10179
|
+
const parsed = JSON.parse(fs35.readFileSync(file(), "utf8"));
|
|
9912
10180
|
if (!parsed || typeof parsed !== "object") return {};
|
|
9913
10181
|
const out = {};
|
|
9914
10182
|
for (const [id, value] of Object.entries(parsed)) {
|
|
@@ -9921,18 +10189,18 @@ function readAll() {
|
|
|
9921
10189
|
}
|
|
9922
10190
|
}
|
|
9923
10191
|
function writeAll(all) {
|
|
9924
|
-
|
|
9925
|
-
|
|
10192
|
+
fs35.mkdirSync(path34.dirname(file()), { recursive: true, mode: 448 });
|
|
10193
|
+
fs35.writeFileSync(file(), JSON.stringify(all, null, 2), { mode: 384 });
|
|
9926
10194
|
}
|
|
9927
10195
|
function gitHead(folder) {
|
|
9928
10196
|
try {
|
|
9929
|
-
const head =
|
|
10197
|
+
const head = fs35.readFileSync(path34.join(folder, ".git", "HEAD"), "utf8").trim();
|
|
9930
10198
|
const ref = /^ref: (.+)$/.exec(head)?.[1];
|
|
9931
10199
|
if (!ref) return { commit: /^[0-9a-f]{40}$/.test(head) ? head : null, branch: null };
|
|
9932
10200
|
const branch = ref.replace(/^refs\/heads\//, "");
|
|
9933
10201
|
const loose = path34.join(folder, ".git", ref);
|
|
9934
|
-
if (
|
|
9935
|
-
const packed =
|
|
10202
|
+
if (fs35.existsSync(loose)) return { commit: fs35.readFileSync(loose, "utf8").trim(), branch };
|
|
10203
|
+
const packed = fs35.readFileSync(path34.join(folder, ".git", "packed-refs"), "utf8");
|
|
9936
10204
|
const line = packed.split("\n").find((l) => l.endsWith(` ${ref}`));
|
|
9937
10205
|
return { commit: line?.split(" ")[0] ?? null, branch };
|
|
9938
10206
|
} catch {
|
|
@@ -9942,7 +10210,7 @@ function gitHead(folder) {
|
|
|
9942
10210
|
function describeFolder(folder, name = path34.basename(folder)) {
|
|
9943
10211
|
const exists = (() => {
|
|
9944
10212
|
try {
|
|
9945
|
-
return
|
|
10213
|
+
return fs35.statSync(folder).isDirectory();
|
|
9946
10214
|
} catch {
|
|
9947
10215
|
return false;
|
|
9948
10216
|
}
|
|
@@ -10363,6 +10631,32 @@ function finished(l) {
|
|
|
10363
10631
|
};
|
|
10364
10632
|
return { flow, secrets: Object.fromEntries(l.typed) };
|
|
10365
10633
|
}
|
|
10634
|
+
function helperHandlers(_ctx) {
|
|
10635
|
+
return {
|
|
10636
|
+
"helper.run": async (command, project) => {
|
|
10637
|
+
const parsed = DeliveredHelperSchema.safeParse(command.args["helper"]);
|
|
10638
|
+
if (!parsed.success) return { ok: false, reason: "failed", detail: "that helper could not be read" };
|
|
10639
|
+
const secrets = command.args["secrets"] ?? {};
|
|
10640
|
+
const inputs = command.args["inputs"] ?? {};
|
|
10641
|
+
const commands = localCommandRunner({ root: project.root || os22.homedir() });
|
|
10642
|
+
const started = Date.now();
|
|
10643
|
+
try {
|
|
10644
|
+
const result = await runHelper(parsed.data, { resolveSecret: (ref) => secrets[ref], inputs, commands });
|
|
10645
|
+
return { ok: true, result: { ...result, tookMs: Date.now() - started } };
|
|
10646
|
+
} catch (error) {
|
|
10647
|
+
const notAllowed = error instanceof CommandNotAllowed;
|
|
10648
|
+
return {
|
|
10649
|
+
ok: false,
|
|
10650
|
+
reason: "failed",
|
|
10651
|
+
detail: error instanceof HelperFailure ? error.message : `the helper failed: ${error.message}`,
|
|
10652
|
+
...notAllowed ? { code: "command_not_allowed" } : {}
|
|
10653
|
+
};
|
|
10654
|
+
} finally {
|
|
10655
|
+
commands.close();
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
|
+
};
|
|
10659
|
+
}
|
|
10366
10660
|
function buildHandlers(deps) {
|
|
10367
10661
|
const context = {
|
|
10368
10662
|
deps,
|
|
@@ -10391,6 +10685,7 @@ function buildHandlers(deps) {
|
|
|
10391
10685
|
...recordingHandlers(context),
|
|
10392
10686
|
...assistantHandlers(context),
|
|
10393
10687
|
...mobileHandlers(context),
|
|
10688
|
+
...helperHandlers(context),
|
|
10394
10689
|
...debug
|
|
10395
10690
|
};
|
|
10396
10691
|
}
|
|
@@ -10667,7 +10962,7 @@ function eventsFromLine(askId, line) {
|
|
|
10667
10962
|
}
|
|
10668
10963
|
return out;
|
|
10669
10964
|
}
|
|
10670
|
-
function
|
|
10965
|
+
function stop2(child) {
|
|
10671
10966
|
child.kill("SIGTERM");
|
|
10672
10967
|
const force = setTimeout(() => {
|
|
10673
10968
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
@@ -10675,14 +10970,14 @@ function stop(child) {
|
|
|
10675
10970
|
force.unref?.();
|
|
10676
10971
|
}
|
|
10677
10972
|
function createClaudeCli(options) {
|
|
10678
|
-
const
|
|
10973
|
+
const spawn3 = options.spawn ?? nodeSpawn;
|
|
10679
10974
|
const binary = options.binary ?? process.env["FLOWRIKI_CLAUDE_BIN"] ?? "claude";
|
|
10680
10975
|
const maxAskMs = options.maxAskMs ?? 60 * 6e4;
|
|
10681
|
-
const scratchRoot2 = options.scratchRoot ?? path42.join(
|
|
10682
|
-
const
|
|
10976
|
+
const scratchRoot2 = options.scratchRoot ?? path42.join(os32.tmpdir(), "flowriki-claude");
|
|
10977
|
+
const running2 = /* @__PURE__ */ new Map();
|
|
10683
10978
|
const MAX_AT_ONCE = 3;
|
|
10684
10979
|
process.once("exit", () => {
|
|
10685
|
-
for (const child of
|
|
10980
|
+
for (const child of running2.values()) {
|
|
10686
10981
|
try {
|
|
10687
10982
|
child.kill("SIGTERM");
|
|
10688
10983
|
} catch {
|
|
@@ -10692,7 +10987,7 @@ function createClaudeCli(options) {
|
|
|
10692
10987
|
const cancelled = /* @__PURE__ */ new Set();
|
|
10693
10988
|
const retrying = /* @__PURE__ */ new Set();
|
|
10694
10989
|
const sessionsInUse = /* @__PURE__ */ new Set();
|
|
10695
|
-
const occupied = () =>
|
|
10990
|
+
const occupied = () => running2.size + retrying.size;
|
|
10696
10991
|
function capture(args, timeoutMs = 15e3) {
|
|
10697
10992
|
return new Promise((resolve2) => {
|
|
10698
10993
|
let out = "", err = "", settled = false;
|
|
@@ -10704,7 +10999,7 @@ function createClaudeCli(options) {
|
|
|
10704
10999
|
};
|
|
10705
11000
|
let child;
|
|
10706
11001
|
try {
|
|
10707
|
-
child =
|
|
11002
|
+
child = spawn3(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10708
11003
|
} catch {
|
|
10709
11004
|
return resolve2({ code: null, out: "", err: "", failed: true });
|
|
10710
11005
|
}
|
|
@@ -10764,10 +11059,10 @@ function createClaudeCli(options) {
|
|
|
10764
11059
|
return { installed: true, version: version.out.trim().split(/\s+/)[0] ?? null, ...parsed };
|
|
10765
11060
|
},
|
|
10766
11061
|
connect(emit) {
|
|
10767
|
-
if (
|
|
11062
|
+
if (running2.size > 0) return { started: false, detail: "The assistant is busy answering a question." };
|
|
10768
11063
|
let child;
|
|
10769
11064
|
try {
|
|
10770
|
-
child =
|
|
11065
|
+
child = spawn3(binary, ["auth", "login"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
10771
11066
|
} catch {
|
|
10772
11067
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10773
11068
|
}
|
|
@@ -10826,10 +11121,10 @@ function createClaudeCli(options) {
|
|
|
10826
11121
|
cancelled.add(askId);
|
|
10827
11122
|
return true;
|
|
10828
11123
|
}
|
|
10829
|
-
const child =
|
|
11124
|
+
const child = running2.get(askId);
|
|
10830
11125
|
if (!child) return false;
|
|
10831
11126
|
cancelled.add(askId);
|
|
10832
|
-
|
|
11127
|
+
stop2(child);
|
|
10833
11128
|
setTimeout(() => child.kill("SIGKILL"), 3e3).unref?.();
|
|
10834
11129
|
return true;
|
|
10835
11130
|
},
|
|
@@ -10842,7 +11137,7 @@ function createClaudeCli(options) {
|
|
|
10842
11137
|
function launch(ask, dir, mcpConfigPath, sessionId, attempt = 1) {
|
|
10843
11138
|
let child;
|
|
10844
11139
|
try {
|
|
10845
|
-
child =
|
|
11140
|
+
child = spawn3(
|
|
10846
11141
|
binary,
|
|
10847
11142
|
buildAskArgs({
|
|
10848
11143
|
prompt: ask.prompt,
|
|
@@ -10864,7 +11159,7 @@ function createClaudeCli(options) {
|
|
|
10864
11159
|
} catch {
|
|
10865
11160
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10866
11161
|
}
|
|
10867
|
-
|
|
11162
|
+
running2.set(ask.askId, child);
|
|
10868
11163
|
if (sessionId) sessionsInUse.add(sessionId);
|
|
10869
11164
|
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10870
11165
|
let silentFailure = false;
|
|
@@ -10906,7 +11201,7 @@ function createClaudeCli(options) {
|
|
|
10906
11201
|
ended = true;
|
|
10907
11202
|
clearTimeout(timer);
|
|
10908
11203
|
if (buffer.trim()) handleLine(buffer);
|
|
10909
|
-
|
|
11204
|
+
running2.delete(ask.askId);
|
|
10910
11205
|
if (sessionId) sessionsInUse.delete(sessionId);
|
|
10911
11206
|
const stopped = cancelled.has(ask.askId);
|
|
10912
11207
|
if (toolsMissing && !stopped) {
|
|
@@ -10941,7 +11236,7 @@ function createClaudeCli(options) {
|
|
|
10941
11236
|
cancelled.delete(ask.askId);
|
|
10942
11237
|
};
|
|
10943
11238
|
const timer = setTimeout(() => {
|
|
10944
|
-
|
|
11239
|
+
stop2(child);
|
|
10945
11240
|
end(false, `Stopped after ${Math.max(1, Math.round(maxAskMs / 6e4))} minutes. Whatever it saved so far is on the Branches page \u2014 send \u201Ccontinue\u201D to carry on.`);
|
|
10946
11241
|
}, maxAskMs);
|
|
10947
11242
|
timer.unref?.();
|
|
@@ -11157,11 +11452,11 @@ function createAgentRuntime(options) {
|
|
|
11157
11452
|
|
|
11158
11453
|
// src/lock.ts
|
|
11159
11454
|
import fs11 from "fs";
|
|
11160
|
-
import
|
|
11455
|
+
import os7 from "os";
|
|
11161
11456
|
function takeLock(env = process.env) {
|
|
11162
11457
|
ensureDataDir(env);
|
|
11163
11458
|
const file2 = lockPath(env);
|
|
11164
|
-
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host:
|
|
11459
|
+
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os7.hostname() };
|
|
11165
11460
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
11166
11461
|
try {
|
|
11167
11462
|
fs11.writeFileSync(file2, JSON.stringify(mine), { flag: "wx", mode: 384 });
|
|
@@ -11317,7 +11612,7 @@ function status(serviceArg) {
|
|
|
11317
11612
|
const service = resolveService(serviceArg);
|
|
11318
11613
|
const connection = connectionFor(service);
|
|
11319
11614
|
const lock = takeLock();
|
|
11320
|
-
const
|
|
11615
|
+
const running2 = lock.ok ? null : lock.held;
|
|
11321
11616
|
if (lock.ok) lock.lock.release();
|
|
11322
11617
|
const dirs = connection?.checkouts ?? [];
|
|
11323
11618
|
const { projects, skipped } = resolveCheckouts(dirs);
|
|
@@ -11329,7 +11624,7 @@ function status(serviceArg) {
|
|
|
11329
11624
|
...projects.map((p) => ({ root: p.root, project: p.name })),
|
|
11330
11625
|
...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
|
|
11331
11626
|
],
|
|
11332
|
-
running
|
|
11627
|
+
running: running2
|
|
11333
11628
|
};
|
|
11334
11629
|
}
|
|
11335
11630
|
async function doctor(serviceArg) {
|
|
@@ -11409,6 +11704,9 @@ export {
|
|
|
11409
11704
|
backupsFor,
|
|
11410
11705
|
withBackups,
|
|
11411
11706
|
flowrikiHome,
|
|
11707
|
+
readApprovals,
|
|
11708
|
+
allowCommands,
|
|
11709
|
+
revokeCommands,
|
|
11412
11710
|
helperRunner,
|
|
11413
11711
|
resolveCheckouts,
|
|
11414
11712
|
start,
|
|
@@ -1454,11 +1454,52 @@ var HttpHelperSchema = z5.object({
|
|
|
1454
1454
|
pattern: z5.string().max(500).default("")
|
|
1455
1455
|
}).strict().default({})
|
|
1456
1456
|
}).strict();
|
|
1457
|
+
var CommandLineSchema = z5.object({
|
|
1458
|
+
program: z5.string().trim().min(1).max(500),
|
|
1459
|
+
args: z5.array(z5.string().max(2e3)).max(50).default([])
|
|
1460
|
+
}).strict();
|
|
1461
|
+
var EnvName = z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "an environment variable name is letters, digits and _");
|
|
1462
|
+
var CommandHelperSchema = z5.object({
|
|
1463
|
+
kind: z5.literal("command"),
|
|
1464
|
+
/** What runs everywhere, unless a system has its own below. */
|
|
1465
|
+
run: CommandLineSchema,
|
|
1466
|
+
/** The same job on another system — a different program, or a .cmd on
|
|
1467
|
+
* Windows. A tester's computer uses its own when there is one. */
|
|
1468
|
+
perSystem: z5.object({
|
|
1469
|
+
macos: CommandLineSchema.optional(),
|
|
1470
|
+
windows: CommandLineSchema.optional(),
|
|
1471
|
+
linux: CommandLineSchema.optional()
|
|
1472
|
+
}).strict().default({}),
|
|
1473
|
+
/** Where it runs: a folder inside the project's checkout, or empty for the
|
|
1474
|
+
* project's folder (the agent's own folder when there is no checkout). */
|
|
1475
|
+
folder: z5.string().max(500).default(""),
|
|
1476
|
+
/** Plain environment variables, in the repository. */
|
|
1477
|
+
env: z5.record(EnvName, z5.string().max(2e3)).default({}),
|
|
1478
|
+
/** Environment variable → vault reference: how a token reaches the command
|
|
1479
|
+
* without ever being on its command line. */
|
|
1480
|
+
secretEnv: z5.record(EnvName, SecretRef).default({}),
|
|
1481
|
+
/** `run` waits for it to finish; `background` starts it, waits until it is
|
|
1482
|
+
* ready, and stops it when the test ends — a mock server, a simulator. */
|
|
1483
|
+
mode: z5.enum(["run", "background"]).default("run"),
|
|
1484
|
+
/** Background: the output line that says it is ready. Empty waits a
|
|
1485
|
+
* couple of seconds instead. */
|
|
1486
|
+
readyPattern: z5.string().max(500).default(""),
|
|
1487
|
+
timeoutSeconds: z5.number().int().min(1).max(600).default(60),
|
|
1488
|
+
/** Refused unless it ends with this exit code. */
|
|
1489
|
+
expectExit: z5.number().int().min(0).max(255).default(0),
|
|
1490
|
+
extract: z5.object({
|
|
1491
|
+
take: z5.enum(["output", "pattern", "json", "nothing"]).default("output"),
|
|
1492
|
+
pattern: z5.string().max(500).default(""),
|
|
1493
|
+
/** For json: a dot path — `device.id`. */
|
|
1494
|
+
path: z5.string().max(300).default("")
|
|
1495
|
+
}).strict().default({})
|
|
1496
|
+
}).strict();
|
|
1457
1497
|
var HelperConfigSchema = z5.discriminatedUnion("kind", [
|
|
1458
1498
|
MailboxHelperSchema,
|
|
1459
1499
|
TotpHelperSchema,
|
|
1460
1500
|
SmsHelperSchema,
|
|
1461
|
-
HttpHelperSchema
|
|
1501
|
+
HttpHelperSchema,
|
|
1502
|
+
CommandHelperSchema
|
|
1462
1503
|
]);
|
|
1463
1504
|
var HelperSchema = z5.object({
|
|
1464
1505
|
schemaVersion: z5.literal(HELPER_SCHEMA_VERSION),
|
|
@@ -1475,7 +1516,9 @@ var HelperSchema = z5.object({
|
|
|
1475
1516
|
const patterns = [
|
|
1476
1517
|
helper.config.kind === "mailbox" && helper.config.extract.take === "code" ? helper.config.extract.pattern : "",
|
|
1477
1518
|
helper.config.kind === "sms" ? helper.config.extract.pattern : "",
|
|
1478
|
-
helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : ""
|
|
1519
|
+
helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
|
|
1520
|
+
helper.config.kind === "command" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
|
|
1521
|
+
helper.config.kind === "command" ? helper.config.readyPattern : ""
|
|
1479
1522
|
];
|
|
1480
1523
|
for (const pattern of patterns) {
|
|
1481
1524
|
if (pattern && !PatternSchema.safeParse(pattern).success) {
|
|
@@ -1483,6 +1526,12 @@ var HelperSchema = z5.object({
|
|
|
1483
1526
|
}
|
|
1484
1527
|
}
|
|
1485
1528
|
});
|
|
1529
|
+
function commandFor(config, system) {
|
|
1530
|
+
return config.perSystem[system] ?? config.run;
|
|
1531
|
+
}
|
|
1532
|
+
function commandSystemOf(platform) {
|
|
1533
|
+
return platform === "win32" ? "windows" : platform === "darwin" ? "macos" : "linux";
|
|
1534
|
+
}
|
|
1486
1535
|
var DeliveredHelperSchema = z5.object({
|
|
1487
1536
|
id: z5.string().min(1),
|
|
1488
1537
|
name: z5.string(),
|
|
@@ -4452,6 +4501,8 @@ export {
|
|
|
4452
4501
|
sameIdentity,
|
|
4453
4502
|
PrerequisiteProblem,
|
|
4454
4503
|
expandForRun,
|
|
4504
|
+
commandFor,
|
|
4505
|
+
commandSystemOf,
|
|
4455
4506
|
DeliveredHelperSchema,
|
|
4456
4507
|
exploreSummary,
|
|
4457
4508
|
CODEBASE_TAG_NAME,
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AGENT_LATEST_URL,
|
|
4
4
|
AGENT_VERSION,
|
|
5
5
|
agentUpdateFor,
|
|
6
|
+
allowCommands,
|
|
6
7
|
authenticationResolver,
|
|
7
8
|
backupsFor,
|
|
8
9
|
checkAssertion,
|
|
@@ -18,15 +19,17 @@ import {
|
|
|
18
19
|
helperRunner,
|
|
19
20
|
openRunSession,
|
|
20
21
|
prepareBrowsers,
|
|
22
|
+
readApprovals,
|
|
21
23
|
replayCase,
|
|
22
24
|
replayFlow,
|
|
23
25
|
resolveCheckouts,
|
|
24
26
|
resolveService,
|
|
27
|
+
revokeCommands,
|
|
25
28
|
runtimeValue,
|
|
26
29
|
start,
|
|
27
30
|
status,
|
|
28
31
|
withBackups
|
|
29
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-WUK3X43D.js";
|
|
30
33
|
import {
|
|
31
34
|
CLOUD_PROVIDERS,
|
|
32
35
|
CLOUD_PROVIDER_NAME,
|
|
@@ -54,7 +57,7 @@ import {
|
|
|
54
57
|
slugify,
|
|
55
58
|
stepRefusedOn,
|
|
56
59
|
targetFor
|
|
57
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-YD7F7JMA.js";
|
|
58
61
|
|
|
59
62
|
// src/cli.ts
|
|
60
63
|
import { parseArgs } from "util";
|
|
@@ -631,6 +634,8 @@ var AuthoringSession = class _AuthoringSession {
|
|
|
631
634
|
mobile = null;
|
|
632
635
|
values = /* @__PURE__ */ new Map();
|
|
633
636
|
resolvers = null;
|
|
637
|
+
/** Every helper runner made in this session, so background commands stop with it. */
|
|
638
|
+
helperRunners = [];
|
|
634
639
|
lastTree = null;
|
|
635
640
|
/** The navigation as last shown, so a step repeats it only when it changed. */
|
|
636
641
|
lastNavigation = null;
|
|
@@ -677,6 +682,7 @@ var AuthoringSession = class _AuthoringSession {
|
|
|
677
682
|
draft.device?.kind === "cloud" ? { cloud: draft.device.provider } : {}
|
|
678
683
|
);
|
|
679
684
|
const runHelper = helperRunner(prepared.helpers, prepared.secrets);
|
|
685
|
+
if (runHelper) this.helperRunners.push(runHelper);
|
|
680
686
|
this.resolvers = {
|
|
681
687
|
resolveAuth: authenticationResolver(prepared.authentications),
|
|
682
688
|
resolveSecret: (ref) => this.typed.get(ref) ?? prepared.secrets[ref],
|
|
@@ -1296,6 +1302,7 @@ ${describeRows(built.rows, built.testCase)}`;
|
|
|
1296
1302
|
const steps = testCase.occurrences.flatMap((o) => o.steps);
|
|
1297
1303
|
const prepared = await this.service.prepare(projectId, steps, null);
|
|
1298
1304
|
const runHelper = helperRunner(prepared.helpers, prepared.secrets);
|
|
1305
|
+
if (runHelper) this.helperRunners.push(runHelper);
|
|
1299
1306
|
const base = catalog.baseUrl;
|
|
1300
1307
|
const phoneRows = testCase.occurrences.filter((o) => isMobile(o.platform));
|
|
1301
1308
|
if (!base && phoneRows.length < testCase.occurrences.length) throw new Error("this project has no base URL \u2014 set one in the project's settings first");
|
|
@@ -1344,6 +1351,7 @@ ${lines.join("\n")}
|
|
|
1344
1351
|
Review and publish it here: ${review}`;
|
|
1345
1352
|
}
|
|
1346
1353
|
async close() {
|
|
1354
|
+
for (const runner of this.helperRunners.splice(0)) runner.close();
|
|
1347
1355
|
if (this.reportTimer) await this.sendReport();
|
|
1348
1356
|
const run = this.run;
|
|
1349
1357
|
const mobile = this.mobile;
|
|
@@ -2638,7 +2646,7 @@ Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accou
|
|
|
2638
2646
|
{
|
|
2639
2647
|
name: "create_helper",
|
|
2640
2648
|
title: "Make a helper",
|
|
2641
|
-
description: `A helper for things outside the page, as the Helpers screen makes one. config by kind \u2014 mailbox: {kind, host, port?, secure?, username, folder?, match?: {to, from, subject}, extract?: {take: code|link|subject|body, pattern}, waitSeconds?} with secretValues {passwordRef: the mailbox password}. totp: {kind, digits?, period?, algorithm?} with secretValues {secretRef: the base32 secret}. sms: {kind, accountSid, to, match?, extract?} with secretValues {authTokenRef: the Twilio token}. http: {kind, method, url, headers?, secretHeaders?: {name: ""}, body?, expectStatus?, extract?: {take: json|pattern|status|nothing, path, pattern}} with secretValues {"header:<name>": value}. Use {{name}} in fields for inputs a step supplies. Secrets are sealed in the vault and never shown again. Making the same name again updates Claude's earlier one.`,
|
|
2649
|
+
description: `A helper for things outside the page, as the Helpers screen makes one. config by kind \u2014 mailbox: {kind, host, port?, secure?, username, folder?, match?: {to, from, subject}, extract?: {take: code|link|subject|body, pattern}, waitSeconds?} with secretValues {passwordRef: the mailbox password}. totp: {kind, digits?, period?, algorithm?} with secretValues {secretRef: the base32 secret}. sms: {kind, accountSid, to, match?, extract?} with secretValues {authTokenRef: the Twilio token}. http: {kind, method, url, headers?, secretHeaders?: {name: ""}, body?, expectStatus?, extract?: {take: json|pattern|status|nothing, path, pattern}} with secretValues {"header:<name>": value}. command (runs on the tester's computer): {kind, run: {program, args: [..]}, perSystem?: {macos|windows|linux: {program, args}}, folder?, env?, secretEnv?: {NAME: ""}, mode?: run|background, readyPattern?, timeoutSeconds?, expectExit?, extract?: {take: output|pattern|json|nothing, pattern, path}} with secretValues {"env:<NAME>": value}; no shell \u2014 one program, its arguments a list. A command runs only after the person allows it on their computer (flowriki-agent allow "<name>"): when use_helper says it is not allowed, stop and tell them that exact command \u2014 never work around it. Use {{name}} in fields for inputs a step supplies. Secrets are sealed in the vault and never shown again. Making the same name again updates Claude's earlier one.`,
|
|
2642
2650
|
inputSchema: {
|
|
2643
2651
|
type: "object",
|
|
2644
2652
|
properties: {
|
|
@@ -3044,6 +3052,7 @@ async function runMcp(opts) {
|
|
|
3044
3052
|
}
|
|
3045
3053
|
|
|
3046
3054
|
// src/cli.ts
|
|
3055
|
+
var quoteArg = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
|
|
3047
3056
|
var paint = (code) => (s) => `\x1B[${code}m${s}\x1B[0m`;
|
|
3048
3057
|
var bold = paint("1");
|
|
3049
3058
|
var dim = paint("2");
|
|
@@ -3066,6 +3075,11 @@ ${bold("USAGE")}
|
|
|
3066
3075
|
needs (Appium and its drivers)
|
|
3067
3076
|
flowriki-agent mobile doctor is this computer ready to test phones
|
|
3068
3077
|
flowriki-agent mobile devices the phones, emulators and simulators here
|
|
3078
|
+
flowriki-agent commands command helpers waiting to be allowed here,
|
|
3079
|
+
and the ones already allowed
|
|
3080
|
+
flowriki-agent allow <name> allow a command helper to run on this
|
|
3081
|
+
computer \u2014 it is shown to you first
|
|
3082
|
+
flowriki-agent revoke <name> stop allowing one
|
|
3069
3083
|
flowriki-agent mcp Flowriki as tools for Claude \u2014 started by
|
|
3070
3084
|
Claude Code or Claude Desktop, not by you:
|
|
3071
3085
|
claude mcp add flowriki -- flowriki-agent mcp
|
|
@@ -3097,6 +3111,7 @@ async function main(argv) {
|
|
|
3097
3111
|
service: { type: "string" },
|
|
3098
3112
|
json: { type: "boolean" },
|
|
3099
3113
|
"no-open": { type: "boolean" },
|
|
3114
|
+
yes: { type: "boolean", short: "y" },
|
|
3100
3115
|
help: { type: "boolean", short: "h" },
|
|
3101
3116
|
version: { type: "boolean", short: "v" }
|
|
3102
3117
|
}
|
|
@@ -3315,6 +3330,69 @@ Run \`flowriki-agent --help\`.`);
|
|
|
3315
3330
|
console.error(`unknown: mobile ${sub} \u2014 use setup, doctor or devices`);
|
|
3316
3331
|
return 2;
|
|
3317
3332
|
}
|
|
3333
|
+
// COMMAND HELPERS: written by anybody with the project, run on THIS
|
|
3334
|
+
// computer only once somebody sitting here has seen and allowed them.
|
|
3335
|
+
case "commands": {
|
|
3336
|
+
const { allowed, waiting } = readApprovals();
|
|
3337
|
+
const show = (c) => `${[c.program, ...c.args].map(quoteArg).join(" ")}${c.folder ? dim(` in ${c.folder}`) : ""}${c.mode === "background" ? dim(" (keeps running during the test)") : ""}`;
|
|
3338
|
+
if (values.json) {
|
|
3339
|
+
console.log(JSON.stringify({ allowed: Object.values(allowed), waiting: Object.values(waiting) }, null, 2));
|
|
3340
|
+
return 0;
|
|
3341
|
+
}
|
|
3342
|
+
console.log(bold("Waiting to be allowed"));
|
|
3343
|
+
if (!Object.keys(waiting).length) console.log(dim(" none"));
|
|
3344
|
+
for (const c of Object.values(waiting)) console.log(` ${yellow("\u2022")} ${c.name}
|
|
3345
|
+
${show(c)}`);
|
|
3346
|
+
console.log(bold("\nAllowed on this computer"));
|
|
3347
|
+
if (!Object.keys(allowed).length) console.log(dim(" none"));
|
|
3348
|
+
for (const c of Object.values(allowed)) console.log(` ${green("\u2713")} ${c.name}
|
|
3349
|
+
${show(c)}`);
|
|
3350
|
+
return 0;
|
|
3351
|
+
}
|
|
3352
|
+
case "allow":
|
|
3353
|
+
case "revoke": {
|
|
3354
|
+
const which = positionals.slice(1).join(" ").trim();
|
|
3355
|
+
if (!which) {
|
|
3356
|
+
console.error(`say which: flowriki-agent ${command} "<helper name>" (or all)`);
|
|
3357
|
+
return 2;
|
|
3358
|
+
}
|
|
3359
|
+
if (command === "revoke") {
|
|
3360
|
+
const gone = revokeCommands(which);
|
|
3361
|
+
console.log(gone.length ? `${green("no longer allowed:")} ${gone.map((c) => c.name).join(", ")}` : yellow(`nothing allowed matches "${which}"`));
|
|
3362
|
+
return gone.length ? 0 : 1;
|
|
3363
|
+
}
|
|
3364
|
+
const { waiting } = readApprovals();
|
|
3365
|
+
const wanted = which.toLowerCase();
|
|
3366
|
+
const matching = Object.values(waiting).filter((c) => wanted === "all" || c.fingerprint === wanted || c.name.toLowerCase() === wanted);
|
|
3367
|
+
if (!matching.length) {
|
|
3368
|
+
console.log(yellow(`nothing is waiting that matches "${which}". A command is listed here after a test or Try it asks to run it \u2014 see flowriki-agent commands.`));
|
|
3369
|
+
return 1;
|
|
3370
|
+
}
|
|
3371
|
+
for (const c of matching) {
|
|
3372
|
+
console.log(`${bold(c.name)}
|
|
3373
|
+
runs ${[c.program, ...c.args].map(quoteArg).join(" ")}
|
|
3374
|
+
in ${c.folder || "the project's folder"}${c.env.length ? `
|
|
3375
|
+
env ${c.env.join(", ")}` : ""}${c.mode === "background" ? "\n keeps running until the test ends" : ""}`);
|
|
3376
|
+
}
|
|
3377
|
+
if (!values.yes) {
|
|
3378
|
+
if (!process.stdin.isTTY) {
|
|
3379
|
+
console.error("add --yes to allow without being asked");
|
|
3380
|
+
return 2;
|
|
3381
|
+
}
|
|
3382
|
+
const { createInterface } = await import("readline/promises");
|
|
3383
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3384
|
+
const answer = (await rl.question(`
|
|
3385
|
+
Allow ${matching.length === 1 ? "this command" : `these ${matching.length} commands`} to run on this computer? [y/N] `)).trim().toLowerCase();
|
|
3386
|
+
rl.close();
|
|
3387
|
+
if (answer !== "y" && answer !== "yes") {
|
|
3388
|
+
console.log("Not allowed.");
|
|
3389
|
+
return 1;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
const done = allowCommands(which);
|
|
3393
|
+
console.log(`${green("allowed:")} ${done.map((c) => c.name).join(", ")}. Tests and Try it can run ${done.length === 1 ? "it" : "them"} here now.`);
|
|
3394
|
+
return 0;
|
|
3395
|
+
}
|
|
3318
3396
|
case "projects": {
|
|
3319
3397
|
const { projects: found, skipped } = resolveCheckouts(projects.length ? projects : [process.cwd()]);
|
|
3320
3398
|
for (const p of found) console.log(` ${p.name} ${dim(p.root)}`);
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED