@flowriki/agent 0.1.0-beta.52 → 0.1.0-beta.55
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.
|
@@ -14,7 +14,9 @@ import { spawn as spawn2 } from "child_process";
|
|
|
14
14
|
import fs3 from "fs";
|
|
15
15
|
import os3 from "os";
|
|
16
16
|
import path3 from "path";
|
|
17
|
+
import { execFile as execFile2 } from "child_process";
|
|
17
18
|
import fs4 from "fs";
|
|
19
|
+
import os4 from "os";
|
|
18
20
|
import path4 from "path";
|
|
19
21
|
|
|
20
22
|
// ../test-model/dist/chunk-I5COYCQ5.js
|
|
@@ -2568,6 +2570,10 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2568
2570
|
"appium:appPackage": app.id,
|
|
2569
2571
|
...app.activity ? { "appium:appActivity": app.activity } : {},
|
|
2570
2572
|
"appium:appWaitActivity": "*",
|
|
2573
|
+
// Its own ports: two phones on one Appium each need their own driver
|
|
2574
|
+
// server and picture stream, or the second session steals the first's.
|
|
2575
|
+
...opts.ports?.system ? { "appium:systemPort": opts.ports.system } : {},
|
|
2576
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2571
2577
|
"appium:autoGrantPermissions": false,
|
|
2572
2578
|
"appium:disableWindowAnimation": true,
|
|
2573
2579
|
"appium:uiautomator2ServerLaunchTimeout": 12e4,
|
|
@@ -2584,6 +2590,8 @@ async function localCapabilities(target, udid, opts = {}) {
|
|
|
2584
2590
|
"appium:bundleId": app.id,
|
|
2585
2591
|
"appium:wdaLaunchTimeout": 24e4,
|
|
2586
2592
|
"appium:wdaConnectionTimeout": 24e4,
|
|
2593
|
+
...opts.ports?.wda ? { "appium:wdaLocalPort": opts.ports.wda } : {},
|
|
2594
|
+
...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
|
|
2587
2595
|
...target.mobile.ios?.teamId ? { "appium:xcodeOrgId": target.mobile.ios.teamId, "appium:xcodeSigningId": "Apple Development" } : {}
|
|
2588
2596
|
};
|
|
2589
2597
|
}
|
|
@@ -2641,7 +2649,7 @@ async function cloudDevices(account, key) {
|
|
|
2641
2649
|
if (!res.ok) throw new Error(`${account.provider} answered ${res.status}.`);
|
|
2642
2650
|
return res.json();
|
|
2643
2651
|
};
|
|
2644
|
-
const platformOf = (
|
|
2652
|
+
const platformOf = (os5) => /android/i.test(os5) ? "android" : /ios/i.test(os5) ? "ios" : null;
|
|
2645
2653
|
const out = [];
|
|
2646
2654
|
if (account.provider === "browserstack") {
|
|
2647
2655
|
for (const d of await get("https://api-cloud.browserstack.com/app-automate/devices.json")) {
|
|
@@ -2655,11 +2663,11 @@ async function cloudDevices(account, key) {
|
|
|
2655
2663
|
if (p) out.push({ name: d.name, platform: p, osVersion: d.osVersion });
|
|
2656
2664
|
}
|
|
2657
2665
|
} else {
|
|
2658
|
-
for (const
|
|
2659
|
-
const listed = await get(`https://mobile-api.lambdatest.com/mobile-automation/api/v1/list?region=us&os=${
|
|
2666
|
+
for (const os5 of ["android", "ios"]) {
|
|
2667
|
+
const listed = await get(`https://mobile-api.lambdatest.com/mobile-automation/api/v1/list?region=us&os=${os5}`);
|
|
2660
2668
|
for (const [, devices] of Object.entries(listed)) {
|
|
2661
2669
|
if (!Array.isArray(devices)) continue;
|
|
2662
|
-
for (const d of devices) out.push({ name: d.deviceName, platform:
|
|
2670
|
+
for (const d of devices) out.push({ name: d.deviceName, platform: os5, osVersion: d.platformVersion });
|
|
2663
2671
|
}
|
|
2664
2672
|
}
|
|
2665
2673
|
}
|
|
@@ -3203,6 +3211,8 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
|
|
|
3203
3211
|
throw last;
|
|
3204
3212
|
}
|
|
3205
3213
|
var uploaded = /* @__PURE__ */ new Map();
|
|
3214
|
+
var OPEN = /* @__PURE__ */ new Map();
|
|
3215
|
+
var openSessionOn = (platform) => OPEN.get(platform) ?? null;
|
|
3206
3216
|
var MobileSession = class _MobileSession {
|
|
3207
3217
|
constructor(driver, platform, appId, deviceName, cloud) {
|
|
3208
3218
|
this.driver = driver;
|
|
@@ -3243,6 +3253,7 @@ var MobileSession = class _MobileSession {
|
|
|
3243
3253
|
throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
|
|
3244
3254
|
});
|
|
3245
3255
|
const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
|
|
3256
|
+
OPEN.set(target.platform, cloudSession);
|
|
3246
3257
|
await cloudSession.settle(2e4);
|
|
3247
3258
|
return cloudSession;
|
|
3248
3259
|
}
|
|
@@ -3256,12 +3267,14 @@ var MobileSession = class _MobileSession {
|
|
|
3256
3267
|
const endpoint = await appiumServer();
|
|
3257
3268
|
const buildFile = app.build ? await localBuild(app.build) : null;
|
|
3258
3269
|
say(`Opening the app on ${known.name}\u2026`);
|
|
3259
|
-
const
|
|
3270
|
+
const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
|
|
3271
|
+
const capabilities = await localCapabilities(target, udid, { buildFile, ports });
|
|
3260
3272
|
const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
|
|
3261
3273
|
const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
|
|
3262
3274
|
throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
|
|
3263
3275
|
});
|
|
3264
3276
|
const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
|
|
3277
|
+
OPEN.set(target.platform, session);
|
|
3265
3278
|
await session.terminateApp().catch(() => {
|
|
3266
3279
|
});
|
|
3267
3280
|
await session.launchApp().catch(() => {
|
|
@@ -3302,6 +3315,16 @@ var MobileSession = class _MobileSession {
|
|
|
3302
3315
|
}
|
|
3303
3316
|
return png;
|
|
3304
3317
|
}
|
|
3318
|
+
/** The screen as a picture to LOOK at — the recorder's mirror, the run
|
|
3319
|
+
* page's live phone — at most `maxPx` on its longer side. A phone's own
|
|
3320
|
+
* resolution is three to four times what a mirror draws, and every byte
|
|
3321
|
+
* of it crosses the control channel each time; on macOS `sips` shrinks it
|
|
3322
|
+
* in a few milliseconds, elsewhere the full picture is sent. Evidence
|
|
3323
|
+
* files keep the full size. */
|
|
3324
|
+
async preview(maxPx = 900) {
|
|
3325
|
+
const png = await this.screenshot();
|
|
3326
|
+
return shrink(png, maxPx);
|
|
3327
|
+
}
|
|
3305
3328
|
/** Android: the activity on screen. iOS has none worth reading. */
|
|
3306
3329
|
async currentScreenName() {
|
|
3307
3330
|
if (this.platform !== "android") return "";
|
|
@@ -3494,10 +3517,27 @@ var MobileSession = class _MobileSession {
|
|
|
3494
3517
|
return { relPath: path4.basename(file), bytes: bytes.length };
|
|
3495
3518
|
}
|
|
3496
3519
|
async close() {
|
|
3520
|
+
if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
|
|
3497
3521
|
if (this.recording) await this.stopVideo(null);
|
|
3498
3522
|
await this.driver.close();
|
|
3499
3523
|
}
|
|
3500
3524
|
};
|
|
3525
|
+
async function shrink(png, maxPx) {
|
|
3526
|
+
if (process.platform !== "darwin") return png;
|
|
3527
|
+
const dir = fs4.mkdtempSync(path4.join(os4.tmpdir(), "flowriki-shot-"));
|
|
3528
|
+
const file = path4.join(dir, "s.png");
|
|
3529
|
+
try {
|
|
3530
|
+
fs4.writeFileSync(file, png);
|
|
3531
|
+
await new Promise((resolve, reject) => {
|
|
3532
|
+
execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
|
|
3533
|
+
});
|
|
3534
|
+
return fs4.readFileSync(file);
|
|
3535
|
+
} catch {
|
|
3536
|
+
return png;
|
|
3537
|
+
} finally {
|
|
3538
|
+
fs4.rmSync(dir, { recursive: true, force: true });
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3501
3541
|
var ANDROID_KEYS = {
|
|
3502
3542
|
enter: 66,
|
|
3503
3543
|
return: 66,
|
|
@@ -4152,7 +4192,9 @@ export {
|
|
|
4152
4192
|
CheckError,
|
|
4153
4193
|
textOf,
|
|
4154
4194
|
checkMobileAssertion,
|
|
4195
|
+
openSessionOn,
|
|
4155
4196
|
MobileSession,
|
|
4197
|
+
shrink,
|
|
4156
4198
|
MOBILE_TIMEOUTS,
|
|
4157
4199
|
performMobileStep,
|
|
4158
4200
|
runMobileSteps,
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
nodeAt,
|
|
48
48
|
normalizeStartUrl,
|
|
49
49
|
openMobileSession,
|
|
50
|
+
openSessionOn,
|
|
50
51
|
parseFlowDocument,
|
|
51
52
|
performMobileStep,
|
|
52
53
|
platformsOf,
|
|
@@ -67,7 +68,7 @@ import {
|
|
|
67
68
|
textIndependent,
|
|
68
69
|
validateActors,
|
|
69
70
|
webCloudOf
|
|
70
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-3XKTIMRP.js";
|
|
71
72
|
|
|
72
73
|
// src/engine/machine.ts
|
|
73
74
|
import fs from "fs";
|
|
@@ -75,7 +76,7 @@ import os from "os";
|
|
|
75
76
|
import path from "path";
|
|
76
77
|
import { createRequire } from "module";
|
|
77
78
|
import { spawnSync } from "child_process";
|
|
78
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
79
|
+
var AGENT_VERSION = "0.1.0-beta.55";
|
|
79
80
|
async function prepareBrowsers() {
|
|
80
81
|
if (browsersReady()) {
|
|
81
82
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -135,7 +136,7 @@ async function checkMachine() {
|
|
|
135
136
|
}
|
|
136
137
|
} catch {
|
|
137
138
|
}
|
|
138
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
139
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-RTDFWPOC.js");
|
|
139
140
|
return {
|
|
140
141
|
agentVersion: AGENT_VERSION,
|
|
141
142
|
node: process.versions.node,
|
|
@@ -234,7 +235,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
234
235
|
}
|
|
235
236
|
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
236
237
|
|
|
237
|
-
// ../contracts/dist/chunk-
|
|
238
|
+
// ../contracts/dist/chunk-NTBXG5EZ.js
|
|
238
239
|
import { z } from "zod";
|
|
239
240
|
var PROTOCOL_VERSION = 1;
|
|
240
241
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -283,6 +284,9 @@ var CommandNameSchema = z.enum([
|
|
|
283
284
|
// Reading a live phone session: its screen as a picture, and what is under
|
|
284
285
|
// a point. Not effectful — a picture is far too big to keep as a result.
|
|
285
286
|
"mobile.screen",
|
|
287
|
+
// The screen of whatever phone session is open on a platform — a run's,
|
|
288
|
+
// a recording's — as a picture, for the live mirror. Reading.
|
|
289
|
+
"mobile.peek",
|
|
286
290
|
// What this computer has installed for web and phone testing. Reading.
|
|
287
291
|
"machine.check"
|
|
288
292
|
]);
|
|
@@ -3962,7 +3966,7 @@ async function replayCase(opts) {
|
|
|
3962
3966
|
let status2 = "passed";
|
|
3963
3967
|
let error = null;
|
|
3964
3968
|
const browserRows = occurrences.filter((o) => o.platform !== "android" && o.platform !== "ios");
|
|
3965
|
-
const
|
|
3969
|
+
const laneOf2 = new Map(sessionLanes(browserRows, opts.defaultActorId).map((plan, i) => [browserRows[i].occurrenceId, plan]));
|
|
3966
3970
|
try {
|
|
3967
3971
|
for (const [index, occurrence] of occurrences.entries()) {
|
|
3968
3972
|
if (status2 !== "passed" || opts.signal?.aborted) {
|
|
@@ -4014,7 +4018,7 @@ async function replayCase(opts) {
|
|
|
4014
4018
|
const navigates = navigatesFirst(occurrence);
|
|
4015
4019
|
const who = occurrence.actorId ?? opts.defaultActorId ?? "default";
|
|
4016
4020
|
const entry = resolveStartUrl(occurrence.baseUrl ?? baseUrl, occurrence.startPath ?? "/");
|
|
4017
|
-
const plan =
|
|
4021
|
+
const plan = laneOf2.get(occurrence.occurrenceId);
|
|
4018
4022
|
try {
|
|
4019
4023
|
if (plan.opens && startOf(occurrence) === "fresh") await actors.restart(who, entry);
|
|
4020
4024
|
else if (plan.opens) await actors.select(who, entry);
|
|
@@ -7990,6 +7994,105 @@ var BrowserBusy = class extends Error {
|
|
|
7990
7994
|
this.name = "BrowserBusy";
|
|
7991
7995
|
}
|
|
7992
7996
|
};
|
|
7997
|
+
var LANES = ["web", "android", "ios"];
|
|
7998
|
+
var OneLane = class {
|
|
7999
|
+
owner = null;
|
|
8000
|
+
phase = "idle";
|
|
8001
|
+
since = null;
|
|
8002
|
+
/** Take the lane, or report who has it. Never awaits — that is the point. */
|
|
8003
|
+
acquire(owner) {
|
|
8004
|
+
if (this.owner !== null) {
|
|
8005
|
+
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8006
|
+
}
|
|
8007
|
+
this.owner = owner;
|
|
8008
|
+
this.phase = "starting";
|
|
8009
|
+
this.since = Date.now();
|
|
8010
|
+
return { ok: true };
|
|
8011
|
+
}
|
|
8012
|
+
/** The holder finished starting and is now doing the thing. */
|
|
8013
|
+
activate(owner) {
|
|
8014
|
+
if (this.owner === owner) this.phase = "active";
|
|
8015
|
+
}
|
|
8016
|
+
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8017
|
+
* join the first rather than starting its own. */
|
|
8018
|
+
beginRelease(owner) {
|
|
8019
|
+
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8020
|
+
this.phase = "stopping";
|
|
8021
|
+
return true;
|
|
8022
|
+
}
|
|
8023
|
+
release(owner) {
|
|
8024
|
+
if (this.owner !== owner) return;
|
|
8025
|
+
this.owner = null;
|
|
8026
|
+
this.phase = "idle";
|
|
8027
|
+
this.since = null;
|
|
8028
|
+
}
|
|
8029
|
+
forceRelease() {
|
|
8030
|
+
this.owner = null;
|
|
8031
|
+
this.phase = "idle";
|
|
8032
|
+
this.since = null;
|
|
8033
|
+
}
|
|
8034
|
+
state() {
|
|
8035
|
+
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8036
|
+
}
|
|
8037
|
+
};
|
|
8038
|
+
var BrowserLease = class {
|
|
8039
|
+
lanes = { web: new OneLane(), android: new OneLane(), ios: new OneLane() };
|
|
8040
|
+
/** The web lane's methods, as they always were: the browser coordinators
|
|
8041
|
+
* drive one platform and need not know about lanes. */
|
|
8042
|
+
acquire(owner, lane = "web") {
|
|
8043
|
+
return this.lanes[lane].acquire(owner);
|
|
8044
|
+
}
|
|
8045
|
+
activate(owner, lane = "web") {
|
|
8046
|
+
this.lanes[lane].activate(owner);
|
|
8047
|
+
}
|
|
8048
|
+
beginRelease(owner, lane = "web") {
|
|
8049
|
+
return this.lanes[lane].beginRelease(owner);
|
|
8050
|
+
}
|
|
8051
|
+
release(owner, lane = "web") {
|
|
8052
|
+
this.lanes[lane].release(owner);
|
|
8053
|
+
}
|
|
8054
|
+
state(lane = "web") {
|
|
8055
|
+
return this.lanes[lane].state();
|
|
8056
|
+
}
|
|
8057
|
+
/** One lane, bound: what a coordinator for that platform is handed. */
|
|
8058
|
+
lane(lane) {
|
|
8059
|
+
return this.lanes[lane];
|
|
8060
|
+
}
|
|
8061
|
+
/** Take several lanes at once — all or none, so a job on two platforms
|
|
8062
|
+
* never holds one while refused the other. */
|
|
8063
|
+
acquireAll(owner, lanes) {
|
|
8064
|
+
const taken = [];
|
|
8065
|
+
for (const lane of new Set(lanes)) {
|
|
8066
|
+
const claim = this.lanes[lane].acquire(owner);
|
|
8067
|
+
if (!claim.ok) {
|
|
8068
|
+
for (const had of taken) this.lanes[had].release(owner);
|
|
8069
|
+
return { ok: false, lane, heldBy: claim.heldBy, phase: claim.phase };
|
|
8070
|
+
}
|
|
8071
|
+
taken.push(lane);
|
|
8072
|
+
}
|
|
8073
|
+
return { ok: true };
|
|
8074
|
+
}
|
|
8075
|
+
activateAll(owner, lanes) {
|
|
8076
|
+
for (const lane of new Set(lanes)) this.lanes[lane].activate(owner);
|
|
8077
|
+
}
|
|
8078
|
+
releaseAll(owner, lanes) {
|
|
8079
|
+
for (const lane of new Set(lanes)) this.lanes[lane].release(owner);
|
|
8080
|
+
}
|
|
8081
|
+
/** Drop every lane whoever holds it — shutdown, and nothing else. */
|
|
8082
|
+
forceRelease() {
|
|
8083
|
+
for (const lane of LANES) this.lanes[lane].forceRelease();
|
|
8084
|
+
}
|
|
8085
|
+
/** The sentence a refused caller is shown. Names what is happening and on
|
|
8086
|
+
* what, because "busy" leaves someone hunting for a window they cannot find. */
|
|
8087
|
+
static refusal(heldBy, phase, lane = "web") {
|
|
8088
|
+
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8089
|
+
const where = lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone";
|
|
8090
|
+
if (phase === "starting") return `${what} is starting on ${where} \u2014 wait for it to open, or stop it first`;
|
|
8091
|
+
if (phase === "stopping") return `${what} is still closing on ${where} \u2014 try again in a moment`;
|
|
8092
|
+
return `${what} is already using ${where} \u2014 stop it before starting another`;
|
|
8093
|
+
}
|
|
8094
|
+
};
|
|
8095
|
+
var laneOf = (platform) => platform === "android" || platform === "ios" ? platform : "web";
|
|
7993
8096
|
var COMMANDS = /* @__PURE__ */ new Set([
|
|
7994
8097
|
"play",
|
|
7995
8098
|
"pause",
|
|
@@ -8166,11 +8269,12 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8166
8269
|
const steps = (opts.steps ?? flow.steps).map((s2) => DraftStepSchema.parse(s2));
|
|
8167
8270
|
if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
|
|
8168
8271
|
const baseUrl = store.baseUrlFor(flow);
|
|
8169
|
-
const
|
|
8272
|
+
const lane = laneOf(flow.platform);
|
|
8273
|
+
const claim = deps.lease?.acquire("run", lane);
|
|
8170
8274
|
if (claim && !claim.ok) {
|
|
8171
|
-
throw new BrowserBusy(
|
|
8275
|
+
throw new BrowserBusy(`${lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone"} on this machine is in use (${claim.heldBy}, ${claim.phase})`);
|
|
8172
8276
|
}
|
|
8173
|
-
deps.lease?.activate("run");
|
|
8277
|
+
deps.lease?.activate("run", lane);
|
|
8174
8278
|
try {
|
|
8175
8279
|
const secrets = store.readSecrets();
|
|
8176
8280
|
if (isMobile(flow.platform)) {
|
|
@@ -8215,7 +8319,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8215
8319
|
resolveSecret: (ref) => secrets[ref]
|
|
8216
8320
|
});
|
|
8217
8321
|
} finally {
|
|
8218
|
-
deps.lease?.release("run");
|
|
8322
|
+
deps.lease?.release("run", lane);
|
|
8219
8323
|
}
|
|
8220
8324
|
}
|
|
8221
8325
|
function deliveredRun(delivered) {
|
|
@@ -8235,55 +8339,6 @@ function deliveredRun(delivered) {
|
|
|
8235
8339
|
resolveFile: () => void 0
|
|
8236
8340
|
};
|
|
8237
8341
|
}
|
|
8238
|
-
var BrowserLease = class {
|
|
8239
|
-
owner = null;
|
|
8240
|
-
phase = "idle";
|
|
8241
|
-
since = null;
|
|
8242
|
-
/** Take the lease, or report who has it. Never awaits — that is the point. */
|
|
8243
|
-
acquire(owner) {
|
|
8244
|
-
if (this.owner !== null) {
|
|
8245
|
-
return { ok: false, heldBy: this.owner, phase: this.phase };
|
|
8246
|
-
}
|
|
8247
|
-
this.owner = owner;
|
|
8248
|
-
this.phase = "starting";
|
|
8249
|
-
this.since = Date.now();
|
|
8250
|
-
return { ok: true };
|
|
8251
|
-
}
|
|
8252
|
-
/** The holder finished starting and is now doing the thing. */
|
|
8253
|
-
activate(owner) {
|
|
8254
|
-
if (this.owner === owner) this.phase = "active";
|
|
8255
|
-
}
|
|
8256
|
-
/** The holder has begun shutting down. Still not idle: a second stop must
|
|
8257
|
-
* join the first rather than starting its own. */
|
|
8258
|
-
beginRelease(owner) {
|
|
8259
|
-
if (this.owner !== owner || this.phase === "stopping") return false;
|
|
8260
|
-
this.phase = "stopping";
|
|
8261
|
-
return true;
|
|
8262
|
-
}
|
|
8263
|
-
release(owner) {
|
|
8264
|
-
if (this.owner !== owner) return;
|
|
8265
|
-
this.owner = null;
|
|
8266
|
-
this.phase = "idle";
|
|
8267
|
-
this.since = null;
|
|
8268
|
-
}
|
|
8269
|
-
/** Drop the lease whoever holds it — shutdown, and nothing else. */
|
|
8270
|
-
forceRelease() {
|
|
8271
|
-
this.owner = null;
|
|
8272
|
-
this.phase = "idle";
|
|
8273
|
-
this.since = null;
|
|
8274
|
-
}
|
|
8275
|
-
state() {
|
|
8276
|
-
return { owner: this.owner, phase: this.phase, since: this.since };
|
|
8277
|
-
}
|
|
8278
|
-
/** The sentence a refused caller is shown. Names what is happening, because
|
|
8279
|
-
* "busy" leaves someone hunting for a window they cannot find. */
|
|
8280
|
-
static refusal(heldBy, phase) {
|
|
8281
|
-
const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
|
|
8282
|
-
if (phase === "starting") return `${what} is starting \u2014 wait for it to open, or stop it first`;
|
|
8283
|
-
if (phase === "stopping") return `${what} is still closing \u2014 try again in a moment`;
|
|
8284
|
-
return `${what} is already using the browser \u2014 stop it before starting another`;
|
|
8285
|
-
}
|
|
8286
|
-
};
|
|
8287
8342
|
|
|
8288
8343
|
// src/engine/checkouts.ts
|
|
8289
8344
|
function resolveCheckouts(dirs) {
|
|
@@ -9240,11 +9295,12 @@ function executionHandlers(ctx) {
|
|
|
9240
9295
|
detail: `this flow needs ${missing.length} secret value(s) that are not on this machine (${missing.map((m) => m.ref || m.field).join(", ")}) \u2014 set them on the machine that runs it`
|
|
9241
9296
|
};
|
|
9242
9297
|
}
|
|
9243
|
-
const
|
|
9298
|
+
const lanes = [laneOf(flow.platform)];
|
|
9299
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9244
9300
|
if (claim && !claim.ok) {
|
|
9245
|
-
return { ok: false, reason: "busy", detail:
|
|
9301
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9246
9302
|
}
|
|
9247
|
-
lease?.
|
|
9303
|
+
lease?.activateAll("run", lanes);
|
|
9248
9304
|
onActivity?.({ kind: "run", projectId: project.id, detail: flow.name });
|
|
9249
9305
|
const aborter = new AbortController();
|
|
9250
9306
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9279,6 +9335,8 @@ function executionHandlers(ctx) {
|
|
|
9279
9335
|
ok: true,
|
|
9280
9336
|
result: {
|
|
9281
9337
|
runId: outcome.run?.id ?? null,
|
|
9338
|
+
// What it was called, for a service that only knew its id.
|
|
9339
|
+
flowName: flow.name,
|
|
9282
9340
|
// The WHOLE outcome, not a summary of it. A dashboard renders the
|
|
9283
9341
|
// per-step report exactly as the local app does, and a result
|
|
9284
9342
|
// carrying only counts makes that screen crash rather than degrade.
|
|
@@ -9294,7 +9352,7 @@ function executionHandlers(ctx) {
|
|
|
9294
9352
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9295
9353
|
if (runId) cancels.delete(runId);
|
|
9296
9354
|
onActivity?.(null);
|
|
9297
|
-
lease?.
|
|
9355
|
+
lease?.releaseAll("run", lanes);
|
|
9298
9356
|
}
|
|
9299
9357
|
},
|
|
9300
9358
|
/** Run a test case: several flows, one browser, in order.
|
|
@@ -9312,11 +9370,12 @@ function executionHandlers(ctx) {
|
|
|
9312
9370
|
if (testCase.occurrences.length === 0) {
|
|
9313
9371
|
return { ok: false, reason: "failed", detail: `"${testCase.name}" has no flows in it to run` };
|
|
9314
9372
|
}
|
|
9315
|
-
const
|
|
9373
|
+
const lanes = [...new Set(testCase.occurrences.map((o) => laneOf(o.platform)))];
|
|
9374
|
+
const claim = lease?.acquireAll("run", lanes);
|
|
9316
9375
|
if (claim && !claim.ok) {
|
|
9317
|
-
return { ok: false, reason: "busy", detail:
|
|
9376
|
+
return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
|
|
9318
9377
|
}
|
|
9319
|
-
lease?.
|
|
9378
|
+
lease?.activateAll("run", lanes);
|
|
9320
9379
|
onActivity?.({ kind: "run", projectId: project.id, detail: testCase.name });
|
|
9321
9380
|
const aborter = new AbortController();
|
|
9322
9381
|
cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
|
|
@@ -9355,6 +9414,8 @@ function executionHandlers(ctx) {
|
|
|
9355
9414
|
ok: true,
|
|
9356
9415
|
result: {
|
|
9357
9416
|
runId: run?.id ?? null,
|
|
9417
|
+
// What it was called, for a service that only knew its id.
|
|
9418
|
+
caseName: testCase.name,
|
|
9358
9419
|
// The whole outcome, per-flow groups included: a dashboard renders
|
|
9359
9420
|
// the report from this, and a result carrying only counts makes
|
|
9360
9421
|
// that screen crash rather than degrade.
|
|
@@ -9366,7 +9427,7 @@ function executionHandlers(ctx) {
|
|
|
9366
9427
|
if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
|
|
9367
9428
|
if (runId) cancels.delete(runId);
|
|
9368
9429
|
onActivity?.(null);
|
|
9369
|
-
lease?.
|
|
9430
|
+
lease?.releaseAll("run", lanes);
|
|
9370
9431
|
}
|
|
9371
9432
|
},
|
|
9372
9433
|
"run.cancel": async (command, project) => {
|
|
@@ -9779,12 +9840,17 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
|
|
|
9779
9840
|
resolve2({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
9780
9841
|
});
|
|
9781
9842
|
});
|
|
9782
|
-
var live =
|
|
9843
|
+
var live = /* @__PURE__ */ new Map();
|
|
9844
|
+
var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
|
|
9845
|
+
var ownerOf = (l) => l.draft ? "recording" : "debug";
|
|
9783
9846
|
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
|
|
9784
9847
|
async function screenOf(l, withImage = false) {
|
|
9785
9848
|
l.session.changed();
|
|
9786
|
-
const screen = await
|
|
9787
|
-
|
|
9849
|
+
const [screen, png, where] = await Promise.all([
|
|
9850
|
+
l.session.screen(),
|
|
9851
|
+
withImage ? l.session.preview() : Promise.resolve(null),
|
|
9852
|
+
l.session.currentScreenName()
|
|
9853
|
+
]);
|
|
9788
9854
|
return {
|
|
9789
9855
|
...png ? { image: png.toString("base64") } : {},
|
|
9790
9856
|
width: screen.size.width,
|
|
@@ -9799,14 +9865,15 @@ async function screenOf(l, withImage = false) {
|
|
|
9799
9865
|
...n2.value ? { value: n2.password ? "\u2022\u2022\u2022" : n2.value } : {},
|
|
9800
9866
|
...n2.id ? { id: n2.id } : {}
|
|
9801
9867
|
})),
|
|
9802
|
-
where
|
|
9868
|
+
where,
|
|
9803
9869
|
device: l.session.deviceName
|
|
9804
9870
|
};
|
|
9805
9871
|
}
|
|
9806
9872
|
function mobileHandlers(ctx) {
|
|
9807
9873
|
const require2 = (command) => {
|
|
9808
|
-
|
|
9809
|
-
|
|
9874
|
+
const l = liveById(command.args["sessionId"]);
|
|
9875
|
+
if (!l) throw new Error("that phone session has ended \u2014 open it again");
|
|
9876
|
+
return l;
|
|
9810
9877
|
};
|
|
9811
9878
|
return {
|
|
9812
9879
|
// Everything this computer has for testing — web and phones — for the Guide.
|
|
@@ -9817,6 +9884,21 @@ function mobileHandlers(ctx) {
|
|
|
9817
9884
|
const devices = await listDevices();
|
|
9818
9885
|
return { ok: true, result: { devices, ...command.args["doctor"] === true ? { doctor: await mobileDoctor() } : {} } };
|
|
9819
9886
|
},
|
|
9887
|
+
// THE PHONE A RUN IS DRIVING, as a picture: whatever session is open on
|
|
9888
|
+
// that platform — a run's, a recording's. Not effectful, never kept, and
|
|
9889
|
+
// nothing when no phone is open: the mirror then shows that.
|
|
9890
|
+
"mobile.peek": async (command) => {
|
|
9891
|
+
const platform = command.args["platform"];
|
|
9892
|
+
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9893
|
+
const session = live.get(platform)?.session ?? openSessionOn(platform);
|
|
9894
|
+
if (!session) return { ok: true, result: { image: null } };
|
|
9895
|
+
try {
|
|
9896
|
+
const png = await session.preview(720);
|
|
9897
|
+
return { ok: true, result: { image: png.toString("base64"), device: session.deviceName } };
|
|
9898
|
+
} catch {
|
|
9899
|
+
return { ok: true, result: { image: null } };
|
|
9900
|
+
}
|
|
9901
|
+
},
|
|
9820
9902
|
"mobile.screen": async (command) => {
|
|
9821
9903
|
try {
|
|
9822
9904
|
const l = require2(command);
|
|
@@ -9833,14 +9915,21 @@ function mobileHandlers(ctx) {
|
|
|
9833
9915
|
},
|
|
9834
9916
|
"mobile.session": async (command, project) => {
|
|
9835
9917
|
const op = String(command.args["op"] ?? "");
|
|
9918
|
+
let claimed = null;
|
|
9836
9919
|
try {
|
|
9837
9920
|
if (op === "start") {
|
|
9838
|
-
if (live) {
|
|
9839
|
-
await live.session.close().catch(() => void 0);
|
|
9840
|
-
live = null;
|
|
9841
|
-
}
|
|
9842
9921
|
const platform = command.args["platform"];
|
|
9843
9922
|
if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
|
|
9923
|
+
const prior = live.get(platform);
|
|
9924
|
+
if (prior) {
|
|
9925
|
+
live.delete(prior.platform);
|
|
9926
|
+
await prior.session.close().catch(() => void 0);
|
|
9927
|
+
ctx.lease?.release(ownerOf(prior), prior.platform);
|
|
9928
|
+
}
|
|
9929
|
+
const owner = command.args["record"] === true ? "recording" : "debug";
|
|
9930
|
+
const claim = ctx.lease?.acquire(owner, platform);
|
|
9931
|
+
if (claim && !claim.ok) return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, platform) };
|
|
9932
|
+
claimed = { owner, platform };
|
|
9844
9933
|
const config = command.args["config"] ?? {};
|
|
9845
9934
|
const secrets = command.args["secrets"] ?? {};
|
|
9846
9935
|
const device = command.args["device"] ? DeviceChoiceSchema.parse(command.args["device"]) : void 0;
|
|
@@ -9875,7 +9964,8 @@ function mobileHandlers(ctx) {
|
|
|
9875
9964
|
...auth?.profileId ? { signIn: auth } : {}
|
|
9876
9965
|
};
|
|
9877
9966
|
}
|
|
9878
|
-
live
|
|
9967
|
+
live.set(l2.platform, l2);
|
|
9968
|
+
ctx.lease?.activate(owner, l2.platform);
|
|
9879
9969
|
if (/^[a-z][\w+.-]*:\/\//i.test(startPath)) await session.openLink(startPath);
|
|
9880
9970
|
const opening = [
|
|
9881
9971
|
...l2.draft?.signIn ? [RecordedStepSchema.parse({ id: "sign-in", type: "signIn", auth: l2.draft.signIn, role: "", name: "Sign in", scope: "", value: "", secret: false, legacySelector: "", proposals: [] })] : [],
|
|
@@ -9916,18 +10006,18 @@ function mobileHandlers(ctx) {
|
|
|
9916
10006
|
if (op === "run" || step.type === "signIn" || step.type === "assert") {
|
|
9917
10007
|
const done = await runMobileSteps(l.session, [step], { values: l.values, resolveSecret: l.resolveSecret, resolveAuth: l.resolveAuth, ...l.runHelper ? { runHelper: l.runHelper } : {} });
|
|
9918
10008
|
const result = done.results[0];
|
|
9919
|
-
if (op === "run") return { ok: true, result: { result: result ?? null
|
|
10009
|
+
if (op === "run") return { ok: true, result: { result: result ?? null } };
|
|
9920
10010
|
if (done.status !== "passed") throw new Error(result?.detail ?? done.error ?? "it did not work");
|
|
9921
10011
|
} else {
|
|
9922
10012
|
await performMobileStep(l.session, step, { values: l.values, resolveSecret: l.resolveSecret, resolveAuth: l.resolveAuth, ...l.runHelper ? { runHelper: l.runHelper } : {} });
|
|
9923
10013
|
}
|
|
9924
10014
|
} catch (error) {
|
|
9925
10015
|
if (secret2) l.typed.delete(ref);
|
|
9926
|
-
return { ok: true, result: { kept: false, error: error.message.split("\n")[0]
|
|
10016
|
+
return { ok: true, result: { kept: false, error: error.message.split("\n")[0] } };
|
|
9927
10017
|
}
|
|
9928
|
-
await new Promise((r) => setTimeout(r,
|
|
10018
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
9929
10019
|
l.draft?.steps.push(step);
|
|
9930
|
-
return { ok: true, result: { kept: true, step, durationMs: Date.now() - t0, steps: l.draft?.steps ?? []
|
|
10020
|
+
return { ok: true, result: { kept: true, step, durationMs: Date.now() - t0, steps: l.draft?.steps ?? [] } };
|
|
9931
10021
|
}
|
|
9932
10022
|
if (op === "check") {
|
|
9933
10023
|
const assertion = RecordedStepSchema.shape.proposals.element.parse({ enabled: true, ...command.args["assertion"] });
|
|
@@ -9958,7 +10048,8 @@ function mobileHandlers(ctx) {
|
|
|
9958
10048
|
return { ok: true, result: { steps: l.draft.steps } };
|
|
9959
10049
|
}
|
|
9960
10050
|
if (op === "stop" || op === "discard") {
|
|
9961
|
-
live
|
|
10051
|
+
live.delete(l.platform);
|
|
10052
|
+
ctx.lease?.release(ownerOf(l), l.platform);
|
|
9962
10053
|
await l.session.close().catch(() => void 0);
|
|
9963
10054
|
if (op === "discard" || !l.draft) return { ok: true, result: { ended: true } };
|
|
9964
10055
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9982,6 +10073,7 @@ function mobileHandlers(ctx) {
|
|
|
9982
10073
|
}
|
|
9983
10074
|
return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
|
|
9984
10075
|
} catch (error) {
|
|
10076
|
+
if (claimed && !live.has(claimed.platform)) ctx.lease?.release(claimed.owner, claimed.platform);
|
|
9985
10077
|
return { ok: false, reason: "failed", detail: error.message.split("\n")[0] ?? "the phone session failed" };
|
|
9986
10078
|
}
|
|
9987
10079
|
}
|
|
@@ -10247,75 +10339,15 @@ function createClaudeCli(options) {
|
|
|
10247
10339
|
ask(ask) {
|
|
10248
10340
|
if (current) return { started: false, detail: "The assistant is already answering a question on this machine." };
|
|
10249
10341
|
fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
|
|
10250
|
-
const dir =
|
|
10342
|
+
const dir = path42.join(scratchRoot2, `project-${ask.projectId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
10343
|
+
fs43.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10251
10344
|
const mcpConfigPath = path42.join(dir, "mcp.json");
|
|
10252
10345
|
fs43.writeFileSync(
|
|
10253
10346
|
mcpConfigPath,
|
|
10254
10347
|
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args } } }),
|
|
10255
10348
|
{ mode: 384 }
|
|
10256
10349
|
);
|
|
10257
|
-
|
|
10258
|
-
try {
|
|
10259
|
-
child = spawn2(
|
|
10260
|
-
binary,
|
|
10261
|
-
buildAskArgs({
|
|
10262
|
-
prompt: ask.prompt,
|
|
10263
|
-
mcpConfigPath,
|
|
10264
|
-
projectId: ask.projectId,
|
|
10265
|
-
projectName: ask.projectName,
|
|
10266
|
-
...ask.sessionId ? { sessionId: ask.sessionId } : {},
|
|
10267
|
-
...ask.folders?.length ? { folders: ask.folders } : {},
|
|
10268
|
-
...ask.mode ? { mode: ask.mode } : {}
|
|
10269
|
-
}),
|
|
10270
|
-
// THE SCRATCH FOLDER, not the project: nothing here to read or write.
|
|
10271
|
-
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"] }
|
|
10272
|
-
);
|
|
10273
|
-
} catch {
|
|
10274
|
-
fs43.rmSync(dir, { recursive: true, force: true });
|
|
10275
|
-
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10276
|
-
}
|
|
10277
|
-
current = { askId: ask.askId, child };
|
|
10278
|
-
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10279
|
-
const handleLine = (line) => {
|
|
10280
|
-
for (const event of eventsFromLine(ask.askId, line)) {
|
|
10281
|
-
if (event.name === "assistant.done") sawDone = true;
|
|
10282
|
-
ask.emit(event.name, event.data);
|
|
10283
|
-
}
|
|
10284
|
-
};
|
|
10285
|
-
const end = (ok, detail) => {
|
|
10286
|
-
if (ended) return;
|
|
10287
|
-
ended = true;
|
|
10288
|
-
clearTimeout(timer);
|
|
10289
|
-
if (buffer.trim()) handleLine(buffer);
|
|
10290
|
-
if (!sawDone) ask.emit("assistant.done", { askId: ask.askId, ok, result: detail });
|
|
10291
|
-
current = null;
|
|
10292
|
-
fs43.rmSync(dir, { recursive: true, force: true });
|
|
10293
|
-
};
|
|
10294
|
-
const timer = setTimeout(() => {
|
|
10295
|
-
child.kill("SIGTERM");
|
|
10296
|
-
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.`);
|
|
10297
|
-
}, maxAskMs);
|
|
10298
|
-
timer.unref?.();
|
|
10299
|
-
child.stdout?.on("data", (chunk) => {
|
|
10300
|
-
buffer += chunk.toString();
|
|
10301
|
-
let at;
|
|
10302
|
-
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
10303
|
-
handleLine(buffer.slice(0, at));
|
|
10304
|
-
buffer = buffer.slice(at + 1);
|
|
10305
|
-
}
|
|
10306
|
-
});
|
|
10307
|
-
child.stderr?.on("data", (chunk) => {
|
|
10308
|
-
stderr = (stderr + chunk.toString()).slice(-2e3);
|
|
10309
|
-
});
|
|
10310
|
-
child.on("error", () => end(false, "The assistant\u2019s command-line tool could not be started."));
|
|
10311
|
-
child.on(
|
|
10312
|
-
"close",
|
|
10313
|
-
(code) => end(
|
|
10314
|
-
code === 0,
|
|
10315
|
-
cancelled === ask.askId ? "Stopped." : code === 0 ? "" : stderr.trim().split("\n").slice(-3).join(" ") || `The assistant stopped (exit ${code ?? "?"}).`
|
|
10316
|
-
)
|
|
10317
|
-
);
|
|
10318
|
-
return { started: true };
|
|
10350
|
+
return launch(ask, dir, mcpConfigPath, ask.sessionId);
|
|
10319
10351
|
},
|
|
10320
10352
|
cancel(askId) {
|
|
10321
10353
|
if (!current || current.askId !== askId) return false;
|
|
@@ -10325,8 +10357,85 @@ function createClaudeCli(options) {
|
|
|
10325
10357
|
setTimeout(() => child.kill("SIGKILL"), 3e3).unref?.();
|
|
10326
10358
|
return true;
|
|
10327
10359
|
},
|
|
10328
|
-
busy
|
|
10360
|
+
busy() {
|
|
10361
|
+
return current !== null;
|
|
10362
|
+
}
|
|
10329
10363
|
};
|
|
10364
|
+
function launch(ask, dir, mcpConfigPath, sessionId) {
|
|
10365
|
+
let child;
|
|
10366
|
+
try {
|
|
10367
|
+
child = spawn2(
|
|
10368
|
+
binary,
|
|
10369
|
+
buildAskArgs({
|
|
10370
|
+
prompt: ask.prompt,
|
|
10371
|
+
mcpConfigPath,
|
|
10372
|
+
projectId: ask.projectId,
|
|
10373
|
+
projectName: ask.projectName,
|
|
10374
|
+
...sessionId ? { sessionId } : {},
|
|
10375
|
+
...ask.folders?.length ? { folders: ask.folders } : {},
|
|
10376
|
+
...ask.mode ? { mode: ask.mode } : {},
|
|
10377
|
+
...ask.brief ? { brief: ask.brief } : {}
|
|
10378
|
+
}),
|
|
10379
|
+
// THE SCRATCH FOLDER, not the project: nothing here to read or write.
|
|
10380
|
+
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"] }
|
|
10381
|
+
);
|
|
10382
|
+
} catch {
|
|
10383
|
+
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10384
|
+
}
|
|
10385
|
+
current = { askId: ask.askId, child };
|
|
10386
|
+
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10387
|
+
let silentFailure = false;
|
|
10388
|
+
const handleLine = (line) => {
|
|
10389
|
+
for (const event of eventsFromLine(ask.askId, line)) {
|
|
10390
|
+
if (event.name === "assistant.done" && event.data["ok"] === false && !String(event.data["result"] ?? "").trim()) {
|
|
10391
|
+
silentFailure = true;
|
|
10392
|
+
continue;
|
|
10393
|
+
}
|
|
10394
|
+
if (event.name === "assistant.done") sawDone = true;
|
|
10395
|
+
ask.emit(event.name, event.data);
|
|
10396
|
+
}
|
|
10397
|
+
};
|
|
10398
|
+
const end = (ok, detail) => {
|
|
10399
|
+
if (ended) return;
|
|
10400
|
+
ended = true;
|
|
10401
|
+
clearTimeout(timer);
|
|
10402
|
+
if (buffer.trim()) handleLine(buffer);
|
|
10403
|
+
current = null;
|
|
10404
|
+
if (silentFailure && sessionId && /no conversation found/i.test(stderr) && cancelled !== ask.askId) {
|
|
10405
|
+
const again = launch(ask, dir, mcpConfigPath, void 0);
|
|
10406
|
+
if (again.started) return;
|
|
10407
|
+
}
|
|
10408
|
+
if (!sawDone) {
|
|
10409
|
+
const why = detail || stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || "The assistant stopped without saying why.";
|
|
10410
|
+
ask.emit("assistant.done", { askId: ask.askId, ok: ok && !silentFailure, result: ok && !silentFailure ? "" : why });
|
|
10411
|
+
}
|
|
10412
|
+
};
|
|
10413
|
+
const timer = setTimeout(() => {
|
|
10414
|
+
child.kill("SIGTERM");
|
|
10415
|
+
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.`);
|
|
10416
|
+
}, maxAskMs);
|
|
10417
|
+
timer.unref?.();
|
|
10418
|
+
child.stdout?.on("data", (chunk) => {
|
|
10419
|
+
buffer += chunk.toString();
|
|
10420
|
+
let at;
|
|
10421
|
+
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
10422
|
+
handleLine(buffer.slice(0, at));
|
|
10423
|
+
buffer = buffer.slice(at + 1);
|
|
10424
|
+
}
|
|
10425
|
+
});
|
|
10426
|
+
child.stderr?.on("data", (chunk) => {
|
|
10427
|
+
stderr = (stderr + chunk.toString()).slice(-2e3);
|
|
10428
|
+
});
|
|
10429
|
+
child.on("error", () => end(false, "The assistant\u2019s command-line tool could not be started."));
|
|
10430
|
+
child.on(
|
|
10431
|
+
"close",
|
|
10432
|
+
(code) => end(
|
|
10433
|
+
code === 0,
|
|
10434
|
+
cancelled === ask.askId ? "Stopped." : code === 0 ? "" : stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || `The assistant stopped (exit ${code ?? "?"}).`
|
|
10435
|
+
)
|
|
10436
|
+
);
|
|
10437
|
+
return { started: true };
|
|
10438
|
+
}
|
|
10330
10439
|
}
|
|
10331
10440
|
function createAgentRuntime(options) {
|
|
10332
10441
|
const lease = new BrowserLease();
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runtimeValue,
|
|
24
24
|
start,
|
|
25
25
|
status
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-NWJWMDVO.js";
|
|
27
27
|
import {
|
|
28
28
|
CLOUD_PROVIDERS,
|
|
29
29
|
CLOUD_PROVIDER_NAME,
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
screenLines,
|
|
50
50
|
stepRefusedOn,
|
|
51
51
|
targetFor
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-3XKTIMRP.js";
|
|
53
53
|
|
|
54
54
|
// src/cli.ts
|
|
55
55
|
import { parseArgs } from "util";
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
mobileSetup,
|
|
32
32
|
nodeAt,
|
|
33
33
|
openMobileSession,
|
|
34
|
+
openSessionOn,
|
|
34
35
|
performMobileStep,
|
|
35
36
|
readScreen,
|
|
36
37
|
replayMobileCase,
|
|
@@ -39,12 +40,13 @@ import {
|
|
|
39
40
|
run,
|
|
40
41
|
runMobileSteps,
|
|
41
42
|
screenLines,
|
|
43
|
+
shrink,
|
|
42
44
|
stopAppium,
|
|
43
45
|
targetFor,
|
|
44
46
|
textOf,
|
|
45
47
|
toolEnv,
|
|
46
48
|
typesSecrets
|
|
47
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-3XKTIMRP.js";
|
|
48
50
|
export {
|
|
49
51
|
AGENT_HOME,
|
|
50
52
|
APPIUM_DIR,
|
|
@@ -77,6 +79,7 @@ export {
|
|
|
77
79
|
mobileSetup,
|
|
78
80
|
nodeAt,
|
|
79
81
|
openMobileSession,
|
|
82
|
+
openSessionOn,
|
|
80
83
|
performMobileStep,
|
|
81
84
|
readScreen,
|
|
82
85
|
replayMobileCase,
|
|
@@ -85,6 +88,7 @@ export {
|
|
|
85
88
|
runMobileSteps,
|
|
86
89
|
run as runTool,
|
|
87
90
|
screenLines,
|
|
91
|
+
shrink,
|
|
88
92
|
stopAppium,
|
|
89
93
|
targetFor,
|
|
90
94
|
textOf,
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED