@swmansion/argent 0.19.0 → 0.19.1-next.0
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/bin/argent-android-devtools-0.1.0.apk +0 -0
- package/bin/darwin/ax-service +0 -0
- package/bin/darwin/tvos-ax-service +0 -0
- package/bin/darwin/tvos-hid-daemon +0 -0
- package/bin/tcp/ax-service +0 -0
- package/dist/cli-cmds.mjs +6 -2
- package/dist/tool-server.cjs +316 -65
- package/dylibs/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/libKeyboardPatch.dylib +0 -0
- package/dylibs/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tcp/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tcp/libKeyboardPatch.dylib +0 -0
- package/dylibs/tcp/libNativeDevtoolsIos.dylib +0 -0
- package/dylibs/tvos/libArgentInjectionBootstrap.dylib +0 -0
- package/dylibs/tvos/libKeyboardPatch.dylib +0 -0
- package/dylibs/tvos/libNativeDevtoolsIos.dylib +0 -0
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +5 -5
|
Binary file
|
package/bin/darwin/ax-service
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/tcp/ax-service
CHANGED
|
Binary file
|
package/dist/cli-cmds.mjs
CHANGED
|
@@ -9547,7 +9547,11 @@ filename (minus .yaml) names the run's report and artifacts, so it must
|
|
|
9547
9547
|
contain only letters, numbers, "_", or "-" \u2014 the same charset a name must
|
|
9548
9548
|
match. A flow that begins with a \`launch\` step runs its app from scratch; any
|
|
9549
9549
|
other flow (a fragment) runs against the device's current state \u2014 handy while
|
|
9550
|
-
authoring one.
|
|
9550
|
+
authoring one. Exception: a fragment whose first step \`run:\`s a chromium e2e
|
|
9551
|
+
flow boots that flow's app before step 1 \u2014 when that launch is unambiguously
|
|
9552
|
+
chromium (a lone \`{ chromium: ... }\` target, or --platform chromium); a
|
|
9553
|
+
multi-platform launch auto-detects a device instead. Pass --device to attach to
|
|
9554
|
+
a running instance.
|
|
9551
9555
|
|
|
9552
9556
|
A directory run prints only failing steps plus a final flow summary;
|
|
9553
9557
|
--recursive walks subdirectories too (dot-directories and node_modules are
|
|
@@ -9674,7 +9678,7 @@ function renderUnderStepLine(s, n2, text2) {
|
|
|
9674
9678
|
function renderSummary(report, opts = {}) {
|
|
9675
9679
|
const warnings = report.steps.filter((s) => s.warning).length;
|
|
9676
9680
|
const warningsNote = warnings ? `, ${warnings} warning${warnings === 1 ? "" : "s"}` : "";
|
|
9677
|
-
const where = opts.withDevice && report.device ? ` on ${report.device}` : "";
|
|
9681
|
+
const where = opts.withDevice && report.device ? ` (started on ${report.device})` : "";
|
|
9678
9682
|
const nothingCounted = report.ok && report.passed + report.failed + report.errored + report.skipped === 0;
|
|
9679
9683
|
const note = nothingCounted ? " (no test steps)" : "";
|
|
9680
9684
|
return `${report.ok ? "PASS" : "FAIL"}${where} \u2014 ${report.passed} passed, ${report.failed} failed, ${report.errored} errored, ${report.skipped} skipped${warningsNote}${note}`;
|
package/dist/tool-server.cjs
CHANGED
|
@@ -792,6 +792,12 @@ function getFailureSignal(error52) {
|
|
|
792
792
|
function getFailureSignalOrFallback(error52, fallback = FALLBACK_SIGNAL) {
|
|
793
793
|
return getFailureSignal(error52) ?? fallback;
|
|
794
794
|
}
|
|
795
|
+
function wrapFailure(error52, fallback, message) {
|
|
796
|
+
const cause = error52 instanceof Error ? error52 : new Error(String(error52));
|
|
797
|
+
return new FailureError(message ?? cause.message, getFailureSignalOrFallback(cause, fallback), {
|
|
798
|
+
cause
|
|
799
|
+
});
|
|
800
|
+
}
|
|
795
801
|
function subprocessFailureMetadata(error52, failure_command) {
|
|
796
802
|
const metadata = { failure_command };
|
|
797
803
|
const err = error52;
|
|
@@ -124390,6 +124396,7 @@ function electronGuiChildEnv(overrides = {}) {
|
|
|
124390
124396
|
|
|
124391
124397
|
// ../tool-server/src/tools/devices/boot-electron.ts
|
|
124392
124398
|
var DEFAULT_READY_TIMEOUT_MS = 3e4;
|
|
124399
|
+
var BOOT_CONFIRM_WINDOW_MS = 300;
|
|
124393
124400
|
async function pickFreePort() {
|
|
124394
124401
|
return new Promise((resolve10, reject) => {
|
|
124395
124402
|
const srv = net5.createServer();
|
|
@@ -124488,11 +124495,22 @@ function sanitizeExtraArgs(extra) {
|
|
|
124488
124495
|
return true;
|
|
124489
124496
|
});
|
|
124490
124497
|
}
|
|
124498
|
+
function signalGroup(pid, signal) {
|
|
124499
|
+
try {
|
|
124500
|
+
process.kill(-pid, signal);
|
|
124501
|
+
return true;
|
|
124502
|
+
} catch (err) {
|
|
124503
|
+
return err.code !== "ESRCH";
|
|
124504
|
+
}
|
|
124505
|
+
}
|
|
124491
124506
|
function killChildEscalating(child) {
|
|
124492
124507
|
try {
|
|
124493
124508
|
child.kill("SIGTERM");
|
|
124494
124509
|
} catch {
|
|
124495
124510
|
}
|
|
124511
|
+
if (child.pid !== void 0 && (child.exitCode !== null || child.signalCode !== null)) {
|
|
124512
|
+
signalGroup(child.pid, "SIGTERM");
|
|
124513
|
+
}
|
|
124496
124514
|
setTimeout(() => {
|
|
124497
124515
|
if (child.exitCode === null && child.signalCode === null) {
|
|
124498
124516
|
try {
|
|
@@ -124500,6 +124518,9 @@ function killChildEscalating(child) {
|
|
|
124500
124518
|
} catch {
|
|
124501
124519
|
}
|
|
124502
124520
|
}
|
|
124521
|
+
if (child.pid !== void 0 && signalGroup(child.pid, 0)) {
|
|
124522
|
+
signalGroup(child.pid, "SIGKILL");
|
|
124523
|
+
}
|
|
124503
124524
|
}, 2e3).unref();
|
|
124504
124525
|
}
|
|
124505
124526
|
var liveChildren = /* @__PURE__ */ new Map();
|
|
@@ -124512,21 +124533,33 @@ function killChromiumByPort(port, pid) {
|
|
|
124512
124533
|
}
|
|
124513
124534
|
if (pid !== void 0) killChromiumByPidFallback(pid);
|
|
124514
124535
|
}
|
|
124536
|
+
var EXIT_WAIT_TIMEOUT_MS = 5e3;
|
|
124537
|
+
var EXIT_POLL_MS = 50;
|
|
124538
|
+
async function killChromiumByPortAndWait(port, pid, timeoutMs = EXIT_WAIT_TIMEOUT_MS) {
|
|
124539
|
+
const child = liveChildren.get(port);
|
|
124540
|
+
const alive = child && child.exitCode === null && child.signalCode === null;
|
|
124541
|
+
const exited = alive ? new Promise((resolve10) => child.once("exit", () => resolve10())) : null;
|
|
124542
|
+
killChromiumByPort(port, pid);
|
|
124543
|
+
if (exited) return Promise.race([exited, sleepUnref(timeoutMs)]);
|
|
124544
|
+
if (!child && pid !== void 0) {
|
|
124545
|
+
const deadline = Date.now() + timeoutMs;
|
|
124546
|
+
while (Date.now() < deadline) {
|
|
124547
|
+
if (!signalGroup(pid, 0)) return;
|
|
124548
|
+
await sleepUnref(EXIT_POLL_MS);
|
|
124549
|
+
}
|
|
124550
|
+
}
|
|
124551
|
+
}
|
|
124552
|
+
function sleepUnref(ms) {
|
|
124553
|
+
return new Promise((resolve10) => {
|
|
124554
|
+
setTimeout(resolve10, ms).unref();
|
|
124555
|
+
});
|
|
124556
|
+
}
|
|
124515
124557
|
function killChromiumByPidFallback(pid) {
|
|
124516
|
-
if (
|
|
124558
|
+
if (!signalGroup(pid, "SIGTERM")) return;
|
|
124517
124559
|
setTimeout(() => {
|
|
124518
|
-
if (
|
|
124519
|
-
signalPid(pid, "SIGKILL");
|
|
124560
|
+
if (signalGroup(pid, 0)) signalGroup(pid, "SIGKILL");
|
|
124520
124561
|
}, 2e3).unref();
|
|
124521
124562
|
}
|
|
124522
|
-
function signalPid(pid, signal) {
|
|
124523
|
-
try {
|
|
124524
|
-
process.kill(pid, signal);
|
|
124525
|
-
return "sent";
|
|
124526
|
-
} catch (err) {
|
|
124527
|
-
return err.code === "ESRCH" ? "gone" : "sent";
|
|
124528
|
-
}
|
|
124529
|
-
}
|
|
124530
124563
|
var ANTI_THROTTLING_ARGS = [
|
|
124531
124564
|
"--disable-background-timer-throttling",
|
|
124532
124565
|
"--disable-backgrounding-occluded-windows",
|
|
@@ -124642,6 +124675,7 @@ async function bootElectronApp(options) {
|
|
|
124642
124675
|
earlyExit,
|
|
124643
124676
|
spawnError
|
|
124644
124677
|
]);
|
|
124678
|
+
await Promise.race([earlyExit, sleepUnref(BOOT_CONFIRM_WINDOW_MS)]);
|
|
124645
124679
|
} catch (err) {
|
|
124646
124680
|
detachBootListeners();
|
|
124647
124681
|
killChildEscalating(child);
|
|
@@ -148287,7 +148321,7 @@ var zodSchema60 = external_exports.object({
|
|
|
148287
148321
|
"Absolute path to the project root directory (the directory that contains or should contain `.argent/flows/`). The flow file is created at `<project_root>/.argent/flows/<name>.yaml`."
|
|
148288
148322
|
),
|
|
148289
148323
|
executionPrerequisite: external_exports.string().optional().describe(
|
|
148290
|
-
'Fragments only: the app/device state assumed on entry (e.g. "Settings app open on General page"). For a self-contained e2e flow, omit this and record a `restart-app` as the first step instead \u2014 it is captured as the flow\'s `launch` step.'
|
|
148324
|
+
'Fragments only: the app/device state assumed on entry (e.g. "Settings app open on General page"). For a self-contained e2e flow, omit this and record a `restart-app` as the first step instead \u2014 it is captured as the flow\'s `launch` step. restart-app has no chromium support, so a chromium flow records as a fragment; add the `launch: { chromium: <app path> }` line to the YAML afterward, deleting the executionPrerequisite line if you passed one \u2014 a flow that starts with a launch must not declare it.'
|
|
148291
148325
|
)
|
|
148292
148326
|
});
|
|
148293
148327
|
var fileInputs2 = [
|
|
@@ -149058,7 +149092,7 @@ function createFlowAddStepTool(registry2) {
|
|
|
149058
149092
|
completedMsg: ({ params }) => `Added ${params.command} step to recorded flow`,
|
|
149059
149093
|
failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to add ${params.command} step to recorded flow: ${failureSignal2.error_code}`
|
|
149060
149094
|
},
|
|
149061
|
-
description: `Execute a tool call and record it as a step in the active flow. Use when recording a flow with flow-start-recording and you want to run and capture each action. A coordinate \`gesture-tap\` is recorded as a portable \`tap: { selector }\` step when the tapped element has stable text/identifier (otherwise coordinates are kept with a warning); a \`restart-app\` is recorded as a \`launch\` step (record one FIRST to make the flow a self-contained e2e flow). Returns { message, toolResult, flowFile } on success. If it fails an error is returned and nothing is recorded.
|
|
149095
|
+
description: `Execute a tool call and record it as a step in the active flow. Use when recording a flow with flow-start-recording and you want to run and capture each action. A coordinate \`gesture-tap\` is recorded as a portable \`tap: { selector }\` step when the tapped element has stable text/identifier (otherwise coordinates are kept with a warning); a \`restart-app\` is recorded as a \`launch\` step (record one FIRST to make the flow a self-contained e2e flow; restart-app has no chromium support, so a chromium flow records as a fragment \u2014 add the \`launch: { chromium: <app path> }\` line to the YAML afterward, deleting the executionPrerequisite line if one was recorded: a flow that starts with a launch must not declare it). Returns { message, toolResult, flowFile } on success. If it fails an error is returned and nothing is recorded.
|
|
149062
149096
|
If a step was recorded by mistake, edit the .yaml file directly to remove it.`,
|
|
149063
149097
|
zodSchema: zodSchema61,
|
|
149064
149098
|
services: () => ({}),
|
|
@@ -151967,6 +152001,15 @@ async function runSnapshot(env, opts) {
|
|
|
151967
152001
|
const key = `${snapshotKey}.png`;
|
|
151968
152002
|
const dir = baselineDir(opts.flowsDir, opts.flowName);
|
|
151969
152003
|
const baselinePath = path34.join(dir, key);
|
|
152004
|
+
const priorApp = opts.seenKeys.get(snapshotKey);
|
|
152005
|
+
if (priorApp !== void 0 && priorApp !== opts.appIdentity) {
|
|
152006
|
+
return {
|
|
152007
|
+
status: "fail",
|
|
152008
|
+
reason: `snapshot "${opts.name}" was already captured in this run from a different app (${priorApp}) \u2014 both captures would share the baseline ${key}, nothing was compared. Give per-app snapshots distinct names`,
|
|
152009
|
+
snapshotKey
|
|
152010
|
+
};
|
|
152011
|
+
}
|
|
152012
|
+
opts.seenKeys.set(snapshotKey, opts.appIdentity);
|
|
151970
152013
|
let currentPath = shot.image.hostPath;
|
|
151971
152014
|
let cropDir;
|
|
151972
152015
|
let keepCropped = false;
|
|
@@ -152262,34 +152305,7 @@ async function treeSourceGate(registry2, device, bundleId, signal) {
|
|
|
152262
152305
|
async function runLaunch(state3, app) {
|
|
152263
152306
|
const env = deviceEnv(state3);
|
|
152264
152307
|
const { registry: registry2, device, signal } = env;
|
|
152265
|
-
if (device.platform === "chromium")
|
|
152266
|
-
if (state3.chromiumLaunched) {
|
|
152267
|
-
return {
|
|
152268
|
-
ok: false,
|
|
152269
|
-
reason: `chromium launches only the top-level flow's app, once per run \u2014 a nested launch can't boot its own instance and would run against the already-launched app. Nested chromium e2e flows aren't supported: run this flow at the top level, or drop its launch step to make it a fragment.`
|
|
152270
|
-
};
|
|
152271
|
-
}
|
|
152272
|
-
state3.chromiumLaunched = true;
|
|
152273
|
-
if (state3.chromiumBooted) {
|
|
152274
|
-
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
152275
|
-
return { ok: true };
|
|
152276
|
-
}
|
|
152277
|
-
if (!appIdForPlatform(app, "chromium")) {
|
|
152278
|
-
return { ok: false, reason: `no chromium app declared \u2014 add a chromium launch entry` };
|
|
152279
|
-
}
|
|
152280
|
-
try {
|
|
152281
|
-
const ref = chromiumCdpRef(device);
|
|
152282
|
-
const api = await registry2.resolveService(ref.urn, ref.options);
|
|
152283
|
-
await api.refreshViewport();
|
|
152284
|
-
} catch (err) {
|
|
152285
|
-
return {
|
|
152286
|
-
ok: false,
|
|
152287
|
-
reason: `could not attach to chromium instance "${device.id}": ${errMsg2(err)}`
|
|
152288
|
-
};
|
|
152289
|
-
}
|
|
152290
|
-
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
152291
|
-
return { ok: true };
|
|
152292
|
-
}
|
|
152308
|
+
if (device.platform === "chromium") return runChromiumLaunch(state3, app);
|
|
152293
152309
|
const bundleId = appIdForPlatform(app, device.platform);
|
|
152294
152310
|
if (!bundleId) {
|
|
152295
152311
|
return {
|
|
@@ -152309,6 +152325,137 @@ async function runLaunch(state3, app) {
|
|
|
152309
152325
|
if (gate) return { ok: false, reason: gate };
|
|
152310
152326
|
return { ok: true };
|
|
152311
152327
|
}
|
|
152328
|
+
async function runChromiumLaunch(state3, app) {
|
|
152329
|
+
const { registry: registry2, device, signal } = deviceEnv(state3);
|
|
152330
|
+
if (state3.chromiumLaunched) return bootChromiumForLaunch(state3, app);
|
|
152331
|
+
state3.chromiumLaunched = true;
|
|
152332
|
+
const spec = chromiumLaunchSpec(app);
|
|
152333
|
+
if (!spec) return { ok: false, reason: noChromiumAppReason(device) };
|
|
152334
|
+
const owned = ownedInstance(state3);
|
|
152335
|
+
if (owned) {
|
|
152336
|
+
const declared = await resolveAppPath(spec.path, state3.flowsDir);
|
|
152337
|
+
if (declared !== owned.appPath) {
|
|
152338
|
+
return {
|
|
152339
|
+
ok: false,
|
|
152340
|
+
reason: `launch declares "${declared}" but the instance booted for this run is "${owned.appPath}" \u2014 the flow file changed after the run started`
|
|
152341
|
+
};
|
|
152342
|
+
}
|
|
152343
|
+
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
152344
|
+
return { ok: true, reason: `booted chromium instance ${device.id}` };
|
|
152345
|
+
}
|
|
152346
|
+
try {
|
|
152347
|
+
const ref = chromiumCdpRef(device);
|
|
152348
|
+
const api = await registry2.resolveService(ref.urn, ref.options);
|
|
152349
|
+
await api.refreshViewport();
|
|
152350
|
+
} catch (err) {
|
|
152351
|
+
return {
|
|
152352
|
+
ok: false,
|
|
152353
|
+
reason: `could not attach to chromium instance "${device.id}": ${errMsg2(err)}`
|
|
152354
|
+
};
|
|
152355
|
+
}
|
|
152356
|
+
state3.attachedAppPath = await resolveAppPath(spec.path, state3.flowsDir);
|
|
152357
|
+
for (const [key, appId] of state3.snapshotApps) {
|
|
152358
|
+
if (appId === `attached:${device.id}`) state3.snapshotApps.set(key, state3.attachedAppPath);
|
|
152359
|
+
}
|
|
152360
|
+
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
152361
|
+
return { ok: true };
|
|
152362
|
+
}
|
|
152363
|
+
async function bootChromiumForLaunch(state3, app) {
|
|
152364
|
+
const { registry: registry2, device, signal } = deviceEnv(state3);
|
|
152365
|
+
const spec = chromiumLaunchSpec(app);
|
|
152366
|
+
if (!spec) return { ok: false, reason: noChromiumAppReason(device) };
|
|
152367
|
+
const appPath = await resolveAppPath(spec.path, state3.flowsDir);
|
|
152368
|
+
const prevId = device.id;
|
|
152369
|
+
const retiring = state3.owned.findIndex((o) => o.appPath === appPath);
|
|
152370
|
+
let retiredId;
|
|
152371
|
+
if (retiring !== -1) {
|
|
152372
|
+
const [prev] = state3.owned.splice(retiring, 1);
|
|
152373
|
+
retiredId = prev.deviceId;
|
|
152374
|
+
await teardownBootedChromium(registry2, prev);
|
|
152375
|
+
}
|
|
152376
|
+
let booted;
|
|
152377
|
+
try {
|
|
152378
|
+
booted = await bootChromiumForFlow(spec, state3.flowsDir, state3.viaUpload);
|
|
152379
|
+
} catch (err) {
|
|
152380
|
+
return { ok: false, reason: await chromiumBootFailureReason(state3, err) };
|
|
152381
|
+
}
|
|
152382
|
+
state3.owned.push(booted);
|
|
152383
|
+
state3.device = resolveDevice(booted.deviceId);
|
|
152384
|
+
await frontChromiumPage(registry2, state3.device);
|
|
152385
|
+
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
152386
|
+
const move = retiredId === prevId ? `retired ${prevId} (same app relaunched)` : `run moved off ${prevId}`;
|
|
152387
|
+
const alsoRetired = retiredId !== void 0 && retiredId !== prevId ? `, retired ${retiredId} (same app relaunched)` : "";
|
|
152388
|
+
return {
|
|
152389
|
+
ok: true,
|
|
152390
|
+
reason: `booted chromium instance ${booted.deviceId} \u2014 ${move}${alsoRetired}`
|
|
152391
|
+
};
|
|
152392
|
+
}
|
|
152393
|
+
var LOCK_SUSPECT_PROBE_TIMEOUT_MS = 800;
|
|
152394
|
+
function singleInstanceLockSignal(err) {
|
|
152395
|
+
const signal = getFailureSignal(err);
|
|
152396
|
+
if (signal?.error_code !== FAILURE_CODES.CHROMIUM_ELECTRON_EXITED_BEFORE_READY || signal.failure_exit_code !== 0) {
|
|
152397
|
+
return null;
|
|
152398
|
+
}
|
|
152399
|
+
return signal;
|
|
152400
|
+
}
|
|
152401
|
+
var NO_LOCK_SUSPECTS = Object.freeze({ attached: null, owned: [] });
|
|
152402
|
+
function singleInstanceLockHint(suspects) {
|
|
152403
|
+
const clauses = [];
|
|
152404
|
+
if (suspects.attached) {
|
|
152405
|
+
clauses.push(
|
|
152406
|
+
`${suspects.attached} is running and this run does not own it; if it is this same app, it holds that lock.`
|
|
152407
|
+
);
|
|
152408
|
+
}
|
|
152409
|
+
if (suspects.owned.length > 0) {
|
|
152410
|
+
const owned = suspects.owned.map((o) => `${o.deviceId} (${o.appPath})`).join(", ");
|
|
152411
|
+
clauses.push(
|
|
152412
|
+
`This run booted ${owned}, alive until run end \u2014 an app path that shares an Electron \`name\` with this one shares its lock. That holder is the runner's own, so closing it is not on offer and a rerun fails identically; launch them in separate runs, or give this launch its own \`--user-data-dir\` in \`args\`.`
|
|
152413
|
+
);
|
|
152414
|
+
}
|
|
152415
|
+
if (clauses.length === 0)
|
|
152416
|
+
clauses.push(`If a copy of this app is already running, close it and rerun.`);
|
|
152417
|
+
return `A clean exit before CDP comes up is the signature of a single-instance lock \u2014 an already-running copy of the app quits the new one at startup. ${clauses.join(" ")}`;
|
|
152418
|
+
}
|
|
152419
|
+
async function chromiumBootFailureReason(state3, err) {
|
|
152420
|
+
const base = `could not boot the chromium app: ${errMsg2(err)}`;
|
|
152421
|
+
if (!singleInstanceLockSignal(err)) return base;
|
|
152422
|
+
return `${base} ${singleInstanceLockHint(await liveLockSuspects(state3))}`;
|
|
152423
|
+
}
|
|
152424
|
+
async function liveLockSuspects(state3) {
|
|
152425
|
+
const [attached, owned] = await Promise.all([
|
|
152426
|
+
liveAttachedInstance(state3),
|
|
152427
|
+
liveOwnedInstances(state3)
|
|
152428
|
+
]);
|
|
152429
|
+
return { attached, owned };
|
|
152430
|
+
}
|
|
152431
|
+
async function liveAttachedInstance(state3) {
|
|
152432
|
+
const id = state3.attachedDeviceId;
|
|
152433
|
+
if (id === void 0) return null;
|
|
152434
|
+
const port = parseChromiumCdpPort(id);
|
|
152435
|
+
if (port === null) return null;
|
|
152436
|
+
return await answersCdp(port) ? id : null;
|
|
152437
|
+
}
|
|
152438
|
+
async function liveOwnedInstances(state3) {
|
|
152439
|
+
const alive = await Promise.all(state3.owned.map((o) => answersCdp(o.port)));
|
|
152440
|
+
return state3.owned.filter((_, i) => alive[i]);
|
|
152441
|
+
}
|
|
152442
|
+
async function answersCdp(port) {
|
|
152443
|
+
try {
|
|
152444
|
+
await ensureCdpReachable(port, AbortSignal.timeout(LOCK_SUSPECT_PROBE_TIMEOUT_MS));
|
|
152445
|
+
return true;
|
|
152446
|
+
} catch {
|
|
152447
|
+
return false;
|
|
152448
|
+
}
|
|
152449
|
+
}
|
|
152450
|
+
function ownedInstance(state3) {
|
|
152451
|
+
return state3.owned.find((o) => o.deviceId === state3.device?.id);
|
|
152452
|
+
}
|
|
152453
|
+
function snapshotAppIdentity(state3) {
|
|
152454
|
+
return ownedInstance(state3)?.appPath ?? state3.attachedAppPath ?? `attached:${deviceEnv(state3).device.id}`;
|
|
152455
|
+
}
|
|
152456
|
+
function noChromiumAppReason(device) {
|
|
152457
|
+
return `no chromium app declared \u2014 the run is on ${device.id}; add a \`chromium:\` entry to this launch`;
|
|
152458
|
+
}
|
|
152312
152459
|
function deviceEnv(state3) {
|
|
152313
152460
|
if (!state3.device) {
|
|
152314
152461
|
throw new Error("internal: a step that acts on a device ran in a flow resolved as device-free");
|
|
@@ -152385,11 +152532,22 @@ checked once with the short assert grace \u2014 for one-sided divergences like i
|
|
|
152385
152532
|
marks; a skipped block reports distinctly and failures inside an entered block are real failures.
|
|
152386
152533
|
A flow that begins with a \`launch\` step is a self-contained e2e flow; one that doesn't runs against the
|
|
152387
152534
|
device's current state. Device id is injected by the runner (flows store none) \u2014 pass \`device\` or
|
|
152388
|
-
\`platform\` to pick one, else the single booted device is used.
|
|
152389
|
-
|
|
152390
|
-
|
|
152391
|
-
\`
|
|
152392
|
-
|
|
152535
|
+
\`platform\` to pick one, else the single booted device is used. On Chromium a \`launch\` step's value is an
|
|
152536
|
+
Electron app path ({ chromium: <path> | { path, args } }) the runner boots (on the tool-server host) rather
|
|
152537
|
+
than an installed app id it relaunches. With no explicit \`device\`, a run whose leading launch is
|
|
152538
|
+
unambiguously chromium (\`platform: chromium\`, or a lone \`{ chromium: \u2026 }\` target) boots that app and
|
|
152539
|
+
starts there \u2014 following a leading \`run:\`, so a fragment that composes a chromium e2e flow boots too;
|
|
152540
|
+
otherwise the first launch attaches to an already-running instance and never kills it. Every later
|
|
152541
|
+
launch \u2014 a nested e2e flow's own, or a mid-flow relaunch \u2014 boots a fresh instance the run moves onto;
|
|
152542
|
+
an instance the run already owns for that same app is killed first (its exit awaited) so the
|
|
152543
|
+
replacement can't lose the race against its single-instance lock. Instances the runner still owns at
|
|
152544
|
+
run end are torn down then. A launch declaring no id for the run's platform is an error, not a cue to
|
|
152545
|
+
switch platforms. Every step hard-stops the flow on failure; later steps are reported as skipped.
|
|
152546
|
+
Returns a structured report ({ flow, device, executionPrerequisite, ok, aborted?, passed, failed,
|
|
152547
|
+
skipped, errored, steps }) \u2014 \`device\` is the device the run STARTED on; when launches moved it onto
|
|
152548
|
+
runner-booted instances, each names its instance in that step's reason and marks the move \u2014 \`run moved
|
|
152549
|
+
off <id>\`, or \`retired <id> (same app relaunched)\` when the instance it left was the one killed \u2014
|
|
152550
|
+
a relaunch that retired an older owned instance names both.
|
|
152393
152551
|
|
|
152394
152552
|
If a fragment has an execution prerequisite and prerequisiteAcknowledged is not set to true, the tool
|
|
152395
152553
|
returns a notice with the prerequisite instead of running.`,
|
|
@@ -152409,6 +152567,22 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
152409
152567
|
const flowsDir = path35.dirname(canonicalPath);
|
|
152410
152568
|
const flow = parseFlow(await fs49.readFile(canonicalPath, "utf8"));
|
|
152411
152569
|
if (viaUpload) assertUploadSelfContained(flow);
|
|
152570
|
+
const rootEntry = { canonical: canonicalPath, display: flowName };
|
|
152571
|
+
if (flow.executionPrerequisite && !pinnedToChromium(params.device)) {
|
|
152572
|
+
const leading = await leadingLaunch(flow, [rootEntry]);
|
|
152573
|
+
if (leading) {
|
|
152574
|
+
const pinRemedy = chromiumPinnable(leading.app, params.platform) ? ` Or pin the run to a chromium instance you have already brought to that state (--device chromium-cdp-<port>), where the leading launch only attaches.` : "";
|
|
152575
|
+
throw new FailureError(
|
|
152576
|
+
`A flow whose leading run: chain reaches a launch step must not declare executionPrerequisite \u2014 it launches its own app and controls its start state. Drop the leading launch in "${leading.flow}" to make it a fragment, or drop executionPrerequisite from "${flowName}".${pinRemedy}`,
|
|
152577
|
+
{
|
|
152578
|
+
error_code: FAILURE_CODES.FLOW_E2E_HAS_PREREQUISITE,
|
|
152579
|
+
failure_stage: "flow_run_validate",
|
|
152580
|
+
failure_area: "tool_server",
|
|
152581
|
+
error_kind: "validation"
|
|
152582
|
+
}
|
|
152583
|
+
);
|
|
152584
|
+
}
|
|
152585
|
+
}
|
|
152412
152586
|
if (flow.executionPrerequisite && !params.prerequisiteAcknowledged) {
|
|
152413
152587
|
return {
|
|
152414
152588
|
flow: flowName,
|
|
@@ -152416,7 +152590,15 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
152416
152590
|
executionPrerequisite: flow.executionPrerequisite
|
|
152417
152591
|
};
|
|
152418
152592
|
}
|
|
152419
|
-
const resolved = await resolveRunDevice(
|
|
152593
|
+
const resolved = await resolveRunDevice(
|
|
152594
|
+
registry2,
|
|
152595
|
+
ctx,
|
|
152596
|
+
flow,
|
|
152597
|
+
params,
|
|
152598
|
+
flowsDir,
|
|
152599
|
+
rootEntry,
|
|
152600
|
+
viaUpload
|
|
152601
|
+
);
|
|
152420
152602
|
const device = resolved.device;
|
|
152421
152603
|
const statusBarPinned = device !== null && await pinStatusBar(device);
|
|
152422
152604
|
if (device?.platform === "chromium") await frontChromiumPage(registry2, device);
|
|
@@ -152426,25 +152608,30 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
152426
152608
|
device,
|
|
152427
152609
|
signal,
|
|
152428
152610
|
flowsDir,
|
|
152611
|
+
viaUpload,
|
|
152429
152612
|
baselineKey: baselineKeyFor(canonicalPath, flowName),
|
|
152430
152613
|
updateBaselines: Boolean(params.updateBaselines),
|
|
152431
152614
|
reports: [],
|
|
152432
152615
|
stopped: false,
|
|
152433
152616
|
pinned: statusBarPinned,
|
|
152434
|
-
|
|
152617
|
+
owned: resolved.booted ? [resolved.booted] : [],
|
|
152435
152618
|
chromiumLaunched: false,
|
|
152619
|
+
snapshotApps: /* @__PURE__ */ new Map(),
|
|
152620
|
+
...!resolved.booted && device?.platform === "chromium" ? { attachedDeviceId: device.id } : {},
|
|
152436
152621
|
...ctx?.emitProgress ? { onStepReport: ctx.emitProgress } : {}
|
|
152437
152622
|
};
|
|
152438
152623
|
let aborted2;
|
|
152439
152624
|
try {
|
|
152440
152625
|
await execSteps(state3, flow.steps, {
|
|
152441
|
-
runStack: [
|
|
152626
|
+
runStack: [rootEntry],
|
|
152442
152627
|
depth: 0
|
|
152443
152628
|
});
|
|
152444
152629
|
} finally {
|
|
152445
152630
|
aborted2 = state3.signal?.aborted === true;
|
|
152446
152631
|
if (state3.pinned && device) await restoreStatusBar(device);
|
|
152447
|
-
|
|
152632
|
+
for (let i = state3.owned.length - 1; i >= 0; i--) {
|
|
152633
|
+
await teardownBootedChromium(registry2, state3.owned[i]);
|
|
152634
|
+
}
|
|
152448
152635
|
}
|
|
152449
152636
|
return summarize(
|
|
152450
152637
|
flowName,
|
|
@@ -152456,11 +152643,17 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
152456
152643
|
}
|
|
152457
152644
|
};
|
|
152458
152645
|
}
|
|
152459
|
-
async function resolveRunDevice(registry2, ctx, flow, params, flowDir, viaUpload) {
|
|
152646
|
+
async function resolveRunDevice(registry2, ctx, flow, params, flowDir, rootEntry, viaUpload) {
|
|
152460
152647
|
if (!params.device) {
|
|
152461
|
-
const
|
|
152648
|
+
const leading = await leadingLaunch(flow, [rootEntry]);
|
|
152649
|
+
const spec = leading && chromiumBootSpec(leading.app, params.platform);
|
|
152462
152650
|
if (spec) {
|
|
152463
|
-
|
|
152651
|
+
let booted;
|
|
152652
|
+
try {
|
|
152653
|
+
booted = await bootChromiumForFlow(spec, flowDir, viaUpload);
|
|
152654
|
+
} catch (err) {
|
|
152655
|
+
throw hoistedBootFailure(err);
|
|
152656
|
+
}
|
|
152464
152657
|
return { device: resolveDevice(booted.deviceId), booted };
|
|
152465
152658
|
}
|
|
152466
152659
|
if (!flowRequiresDevice(registry2, flow.steps)) {
|
|
@@ -152473,12 +152666,54 @@ async function resolveRunDevice(registry2, ctx, flow, params, flowDir, viaUpload
|
|
|
152473
152666
|
});
|
|
152474
152667
|
return { device, booted: null };
|
|
152475
152668
|
}
|
|
152476
|
-
function
|
|
152477
|
-
|
|
152478
|
-
|
|
152479
|
-
|
|
152480
|
-
|
|
152481
|
-
|
|
152669
|
+
function hoistedBootFailure(err) {
|
|
152670
|
+
const signal = singleInstanceLockSignal(err);
|
|
152671
|
+
if (!signal) return err;
|
|
152672
|
+
return wrapFailure(err, signal, `${errMsg2(err)} ${singleInstanceLockHint(NO_LOCK_SUSPECTS)}`);
|
|
152673
|
+
}
|
|
152674
|
+
function pinnedToChromium(device) {
|
|
152675
|
+
return device !== void 0 && resolveDevice(device).platform === "chromium";
|
|
152676
|
+
}
|
|
152677
|
+
function chromiumPinnable(app, platform) {
|
|
152678
|
+
if (typeof app === "string") return platform === "chromium";
|
|
152679
|
+
return chromiumLaunchSpec(app) !== null;
|
|
152680
|
+
}
|
|
152681
|
+
var NO_EXECUTABLE_STEP = "no-executable-step";
|
|
152682
|
+
async function leadingLaunch(flow, stack) {
|
|
152683
|
+
const found = await scanLeadingLaunch(flow, stack);
|
|
152684
|
+
return found === NO_EXECUTABLE_STEP ? null : found;
|
|
152685
|
+
}
|
|
152686
|
+
async function scanLeadingLaunch(flow, stack) {
|
|
152687
|
+
const top = stack[stack.length - 1];
|
|
152688
|
+
for (const step of flow.steps) {
|
|
152689
|
+
if (step.kind === "echo") continue;
|
|
152690
|
+
if (step.kind === "launch") return { app: step.app, flow: top.display };
|
|
152691
|
+
if (step.kind !== "run") return null;
|
|
152692
|
+
const spelled = path35.dirname(top.canonical) + path35.sep + step.flow;
|
|
152693
|
+
let nested;
|
|
152694
|
+
let canonical;
|
|
152695
|
+
try {
|
|
152696
|
+
canonical = await canonicalFlowPath(spelled);
|
|
152697
|
+
if (stack.some((entry) => entry.canonical === canonical)) return null;
|
|
152698
|
+
if (stack.length >= MAX_RUN_DEPTH) return null;
|
|
152699
|
+
const supplied = path35.posix.basename(step.flow);
|
|
152700
|
+
const spelling = await classifyOnDiskSpelling(path35.dirname(spelled), supplied);
|
|
152701
|
+
if (spelling.state === "case_folded") return null;
|
|
152702
|
+
nested = parseFlow(await fs49.readFile(canonical, "utf8"));
|
|
152703
|
+
} catch {
|
|
152704
|
+
return null;
|
|
152705
|
+
}
|
|
152706
|
+
const inner = await scanLeadingLaunch(nested, [
|
|
152707
|
+
...stack,
|
|
152708
|
+
{ canonical, display: runDisplayFor(step.flow, stack[0].display) }
|
|
152709
|
+
]);
|
|
152710
|
+
if (inner !== NO_EXECUTABLE_STEP) return inner;
|
|
152711
|
+
}
|
|
152712
|
+
return NO_EXECUTABLE_STEP;
|
|
152713
|
+
}
|
|
152714
|
+
function chromiumBootSpec(app, platform) {
|
|
152715
|
+
if (launchTargetPlatform(app, platform) !== "chromium") return null;
|
|
152716
|
+
return chromiumLaunchSpec(app);
|
|
152482
152717
|
}
|
|
152483
152718
|
function launchTargetPlatform(launch, platform) {
|
|
152484
152719
|
if (platform) return platform;
|
|
@@ -152488,6 +152723,14 @@ function launchTargetPlatform(launch, platform) {
|
|
|
152488
152723
|
}
|
|
152489
152724
|
return null;
|
|
152490
152725
|
}
|
|
152726
|
+
async function resolveAppPath(specPath, flowDir) {
|
|
152727
|
+
const lexical = path35.resolve(flowDir, specPath);
|
|
152728
|
+
try {
|
|
152729
|
+
return await fs49.realpath(lexical);
|
|
152730
|
+
} catch {
|
|
152731
|
+
return lexical;
|
|
152732
|
+
}
|
|
152733
|
+
}
|
|
152491
152734
|
async function bootChromiumForFlow(spec, flowDir, viaUpload) {
|
|
152492
152735
|
if (viaUpload && !path35.isAbsolute(spec.path)) {
|
|
152493
152736
|
throw new FailureError(
|
|
@@ -152500,9 +152743,9 @@ async function bootChromiumForFlow(spec, flowDir, viaUpload) {
|
|
|
152500
152743
|
}
|
|
152501
152744
|
);
|
|
152502
152745
|
}
|
|
152503
|
-
const appPath =
|
|
152746
|
+
const appPath = await resolveAppPath(spec.path, flowDir);
|
|
152504
152747
|
const res = await bootElectronApp({ appPath, extraArgs: spec.args });
|
|
152505
|
-
return { deviceId: res.id, port: res.port, pid: res.pid };
|
|
152748
|
+
return { deviceId: res.id, port: res.port, pid: res.pid, appPath: res.appPath };
|
|
152506
152749
|
}
|
|
152507
152750
|
async function teardownBootedChromium(registry2, booted) {
|
|
152508
152751
|
const urn = `${CHROMIUM_CDP_NAMESPACE}:${booted.deviceId}`;
|
|
@@ -152511,8 +152754,11 @@ async function teardownBootedChromium(registry2, booted) {
|
|
|
152511
152754
|
if (entry && isLiveServiceState(entry.state)) await registry2.disposeService(urn);
|
|
152512
152755
|
} catch {
|
|
152513
152756
|
}
|
|
152514
|
-
|
|
152515
|
-
|
|
152757
|
+
try {
|
|
152758
|
+
await killChromiumByPortAndWait(booted.port, booted.pid);
|
|
152759
|
+
untrackChromiumPort(booted.port);
|
|
152760
|
+
} catch {
|
|
152761
|
+
}
|
|
152516
152762
|
}
|
|
152517
152763
|
async function frontChromiumPage(registry2, device) {
|
|
152518
152764
|
try {
|
|
@@ -152614,8 +152860,11 @@ function scopeFlow(scope) {
|
|
|
152614
152860
|
return scope.runStack[scope.runStack.length - 1].display;
|
|
152615
152861
|
}
|
|
152616
152862
|
function runDisplayName(target, scope) {
|
|
152863
|
+
return runDisplayFor(target, scope.runStack[0].display);
|
|
152864
|
+
}
|
|
152865
|
+
function runDisplayFor(target, rootDisplay) {
|
|
152617
152866
|
const stem = runTargetName(target);
|
|
152618
|
-
if (stem !==
|
|
152867
|
+
if (stem !== rootDisplay) return stem;
|
|
152619
152868
|
const spelled = target.slice(0, -".yaml".length);
|
|
152620
152869
|
return spelled === stem ? `./${stem}` : spelled;
|
|
152621
152870
|
}
|
|
@@ -152879,7 +153128,9 @@ async function execLeafStep(state3, step, index, scope) {
|
|
|
152879
153128
|
name: step.name,
|
|
152880
153129
|
maxMismatch: step.maxMismatch ?? DEFAULT_MAX_MISMATCH,
|
|
152881
153130
|
updateBaselines: state3.updateBaselines,
|
|
152882
|
-
cropOn: step.cropOn
|
|
153131
|
+
cropOn: step.cropOn,
|
|
153132
|
+
appIdentity: snapshotAppIdentity(state3),
|
|
153133
|
+
seenKeys: state3.snapshotApps
|
|
152883
153134
|
});
|
|
152884
153135
|
return {
|
|
152885
153136
|
...base,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -11,10 +11,10 @@ Flows store **no device id**: the runner binds a device (the single booted one,
|
|
|
11
11
|
|
|
12
12
|
**Two flow types**
|
|
13
13
|
|
|
14
|
-
- **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:` other flows, and
|
|
15
|
-
- **fragment** — doesn't begin with a launch; runs against the device's current state. May declare an `executionPrerequisite` (a documented entry-state contract). Invoked from other flows via a `run:` step, or directly by you at any time.
|
|
14
|
+
- **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:` other flows, and may itself be a `run:` target — when nested, its `launch` runs inline, restarting the app for that sub-scenario. **On Chromium a launch is a process, not a relaunch:** the "device" is the booted app (its id is the CDP port). The runner needs a device before step 1, so it boots for the launch the run _begins_ with, following a leading `run:` — a fragment whose first step composes a chromium e2e flow boots that flow's app (pass `--platform chromium` when the launch names several platforms, or the target is ambiguous and auto-detection is used instead). That first launch then just settles the instance it was booted for; every _later_ launch — a nested e2e flow's own, or a mid-flow `launch:` of the same app — boots its own instance and the run moves onto it for the remaining steps, replacing the one the runner already owns for that app. Every instance the runner boots is torn down at run end; one you pinned with `--device` is attached to, never killed — so relaunching _that_ app mid-flow fails if it holds a single-instance lock. A launch that names no id for the run's platform is an error — a `chromium:` entry does not make a flow runnable on iOS, and the run never switches platforms mid-flight. Record one by adding a `restart-app` of the app under test as the **first** step — it is captured as the `launch` step. Not on Chromium, though: `restart-app` has no chromium support and only successful calls are recorded, so a recorded chromium flow is always a fragment — write the `launch: { chromium: <app path> }` line into the YAML yourself afterward, and delete any `executionPrerequisite` the recording declared: with its own launch the flow controls its start state, and a launch-first flow must not carry one.
|
|
15
|
+
- **fragment** — doesn't begin with a launch; runs against the device's current state. May declare an `executionPrerequisite` (a documented entry-state contract). Invoked from other flows via a `run:` step, or directly by you at any time. One exception to "current state": a fragment whose **first** step `run:`s a chromium e2e flow takes on that flow's launch — the runner boots that app before step 1 (see the e2e bullet; pass `--device` to attach to a running instance instead).
|
|
16
16
|
|
|
17
|
-
Both run via `argent flow run <flow|flow.yaml>` — a fragment simply runs against whatever is on screen (its prerequisite is printed as a reminder). A bare name is read from `.argent/flows/<name>.yaml`; anything ending in `.yaml` is a path. Only e2e flows are meaningful CI/suite entries, since only they give a deterministic verdict from a clean start.
|
|
17
|
+
Both run via `argent flow run <flow|flow.yaml>` — a fragment simply runs against whatever is on screen (its prerequisite is printed as a reminder), except the chromium leading-`run:` case above, which boots. A bare name is read from `.argent/flows/<name>.yaml`; anything ending in `.yaml` is a path. Only e2e flows are meaningful CI/suite entries, since only they give a deterministic verdict from a clean start.
|
|
18
18
|
|
|
19
19
|
### Step directives
|
|
20
20
|
|
|
@@ -118,7 +118,7 @@ Since a `tv-remote` path is positional (like a coordinate tap), gate each naviga
|
|
|
118
118
|
|
|
119
119
|
### Standalone runner
|
|
120
120
|
|
|
121
|
-
`argent flow run <flow|flow.yaml|dir> [--device <id>] [--platform ios|android|chromium|vega] [--update-baselines] [--output <dir>] [-r|--recursive] [--json]` runs a flow with no LLM in the loop and exits non-zero on any failure — suitable for CI (e2e flows; a fragment runs against the current device state, useful while authoring). The argument is either a saved flow's name, read from `.argent/flows/<name>.yaml` under the current directory, or a `.yaml` file path (relative to the current directory, or absolute) for a flow kept anywhere else. The two never collide: a name carries no separator and no extension, so an argument ending in `.yaml` is always a path and never falls back to the flows directory. Either way the filename (minus `.yaml`) names the run's report and artifacts, so it must contain only letters, numbers, `_`, or `-`. A directory path runs every flow in it sequentially, printing only failing steps plus a final `passed/failed/skipped` flow summary (`--json` prints one aggregate object); `-r`/`--recursive` walks subdirectories too, skipping dot-directories and `node_modules`. An invalid flow file fails alone and the batch continues; an infra error stops the batch, counting the remaining flows skipped. Only a path reaches a directory — a name always resolves to one `.yaml` file. `argent flow list` prints runnable paths for flows saved under `.argent/flows/` — a nested one is addressable by its path only.
|
|
121
|
+
`argent flow run <flow|flow.yaml|dir> [--device <id>] [--platform ios|android|chromium|vega] [--update-baselines] [--output <dir>] [-r|--recursive] [--json]` runs a flow with no LLM in the loop and exits non-zero on any failure — suitable for CI (e2e flows; a fragment runs against the current device state, useful while authoring — unless its first step `run:`s a chromium e2e flow, which boots that app). The argument is either a saved flow's name, read from `.argent/flows/<name>.yaml` under the current directory, or a `.yaml` file path (relative to the current directory, or absolute) for a flow kept anywhere else. The two never collide: a name carries no separator and no extension, so an argument ending in `.yaml` is always a path and never falls back to the flows directory. Either way the filename (minus `.yaml`) names the run's report and artifacts, so it must contain only letters, numbers, `_`, or `-`. A directory path runs every flow in it sequentially, printing only failing steps plus a final `passed/failed/skipped` flow summary (`--json` prints one aggregate object); `-r`/`--recursive` walks subdirectories too, skipping dot-directories and `node_modules`. An invalid flow file fails alone and the batch continues; an infra error stops the batch, counting the remaining flows skipped. Only a path reaches a directory — a name always resolves to one `.yaml` file. `argent flow list` prints runnable paths for flows saved under `.argent/flows/` — a nested one is addressable by its path only.
|
|
122
122
|
|
|
123
123
|
The standalone command uses only the auto-started local tool server. It is unavailable while `ARGENT_TOOLS_URL` or `argent link` routing is active; unset `ARGENT_TOOLS_URL` or run `argent unlink` first. This restriction applies only to the CLI: an agent may continue calling `flow-execute` with `name` and `project_root`, including through a remote tool server — but a remote call uploads the one YAML into a temp directory on the server, and both `run:` targets (whose referenced files stay behind on the client) and `__baselines__/` resolve beside that copy, so only a self-contained flow replays remotely (see _Replaying_).
|
|
124
124
|
|
|
@@ -159,7 +159,7 @@ Record an `await-ui-element` step to **gate** the next step on a screen transiti
|
|
|
159
159
|
|
|
160
160
|
## Recording
|
|
161
161
|
|
|
162
|
-
1. **Start, then launch as the first step (e2e) or set the stage yourself (fragment).** Call `flow-start-recording` with a descriptive name and the absolute `project_root`. For an **e2e** flow, record a `restart-app` of the app under test as the **first** step — it runs live (resetting the device for the rest of the recording) and is captured as the flow's `launch` step. For a **fragment**, bring the device to the entry state _before_ recording and pass an `executionPrerequisite` describing it (e.g. "App on the login screen") to `flow-start-recording` instead.
|
|
162
|
+
1. **Start, then launch as the first step (e2e) or set the stage yourself (fragment).** Call `flow-start-recording` with a descriptive name and the absolute `project_root`. For an **e2e** flow, record a `restart-app` of the app under test as the **first** step — it runs live (resetting the device for the rest of the recording) and is captured as the flow's `launch` step (`restart-app` has no chromium support, so on Chromium record the flow as a fragment against the running app and add the `launch:` line to the YAML afterward, deleting the `executionPrerequisite` line if you passed one — a launch-first flow must not declare it). For a **fragment**, bring the device to the entry state _before_ recording and pass an `executionPrerequisite` describing it (e.g. "App on the login screen") to `flow-start-recording` instead.
|
|
163
163
|
2. **Build step-by-step**: for each action, call `flow-add-step` with the tool name and args. The tool runs immediately — check the result before moving on, and gate each navigation with an `await-ui-element` step.
|
|
164
164
|
3. **Add labels**: use `flow-add-echo` between steps — echo the expected state, not just the action (see _Making flows resilient_).
|
|
165
165
|
4. **Finish**: call `flow-finish-recording`. It returns the file path where the flow was saved and a summary of all steps.
|