@yoqa/runner 0.3.17 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +773 -478
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16801,7 +16801,7 @@ function getCatalogDb() {
|
|
|
16801
16801
|
// package.json
|
|
16802
16802
|
var package_default = {
|
|
16803
16803
|
name: "@yoqa/runner",
|
|
16804
|
-
version: "0.3.
|
|
16804
|
+
version: "0.3.19",
|
|
16805
16805
|
description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
|
|
16806
16806
|
license: "UNLICENSED",
|
|
16807
16807
|
type: "module",
|
|
@@ -16896,11 +16896,458 @@ function abortAllMjpegProxies() {
|
|
|
16896
16896
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
16897
16897
|
import { createServer as createServer2 } from "net";
|
|
16898
16898
|
import { homedir as homedir6 } from "os";
|
|
16899
|
-
import { join as
|
|
16899
|
+
import { join as join7 } from "path";
|
|
16900
16900
|
import { remote } from "webdriverio";
|
|
16901
|
-
|
|
16901
|
+
|
|
16902
|
+
// src/domains/devices/application.ts
|
|
16903
|
+
import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
|
|
16904
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
16905
|
+
import { join as join6 } from "path";
|
|
16906
|
+
|
|
16907
|
+
// src/domains/devices/android-identity.ts
|
|
16908
|
+
function matchAndroidAppiumIdentity(deviceId, connected, knownAvds = []) {
|
|
16909
|
+
const id = deviceId.trim();
|
|
16910
|
+
if (!id)
|
|
16911
|
+
return {};
|
|
16912
|
+
const bySerial = connected.find((device) => device.serial === id);
|
|
16913
|
+
if (bySerial) {
|
|
16914
|
+
return {
|
|
16915
|
+
udid: bySerial.serial,
|
|
16916
|
+
...bySerial.avdName ? { avd: bySerial.avdName } : {}
|
|
16917
|
+
};
|
|
16918
|
+
}
|
|
16919
|
+
const byAvd = connected.find((device) => device.avdName === id);
|
|
16920
|
+
if (byAvd) {
|
|
16921
|
+
return { udid: byAvd.serial, avd: byAvd.avdName ?? id };
|
|
16922
|
+
}
|
|
16923
|
+
if (knownAvds.includes(id)) {
|
|
16924
|
+
return { avd: id };
|
|
16925
|
+
}
|
|
16926
|
+
return { udid: id };
|
|
16927
|
+
}
|
|
16928
|
+
function parseAdbEmuAvdName(stdout) {
|
|
16929
|
+
const name = stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line && line !== "OK");
|
|
16930
|
+
return name || null;
|
|
16931
|
+
}
|
|
16932
|
+
|
|
16933
|
+
// src/domains/devices/application.ts
|
|
16934
|
+
async function runCommand3(command, options) {
|
|
16935
|
+
try {
|
|
16936
|
+
const proc = Bun.spawn(command, {
|
|
16937
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
16938
|
+
stdout: "pipe",
|
|
16939
|
+
stderr: "pipe"
|
|
16940
|
+
});
|
|
16941
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
16942
|
+
new Response(proc.stdout).text(),
|
|
16943
|
+
new Response(proc.stderr).text(),
|
|
16944
|
+
proc.exited
|
|
16945
|
+
]);
|
|
16946
|
+
return { stdout, stderr, exitCode };
|
|
16947
|
+
} catch {
|
|
16948
|
+
return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
|
|
16949
|
+
}
|
|
16950
|
+
}
|
|
16951
|
+
function androidSdkRoot() {
|
|
16952
|
+
return resolveAndroidSdkRoot();
|
|
16953
|
+
}
|
|
16954
|
+
async function resolveTool(candidates) {
|
|
16955
|
+
for (const candidate of candidates) {
|
|
16956
|
+
if (!candidate)
|
|
16957
|
+
continue;
|
|
16958
|
+
const { exitCode } = await runCommand3(["test", "-x", candidate]);
|
|
16959
|
+
if (exitCode === 0)
|
|
16960
|
+
return candidate;
|
|
16961
|
+
}
|
|
16962
|
+
return null;
|
|
16963
|
+
}
|
|
16964
|
+
async function resolveAdb() {
|
|
16965
|
+
const sdk = androidSdkRoot();
|
|
16966
|
+
return resolveTool([...sdk ? [join6(sdk, "platform-tools/adb")] : [], "adb"]);
|
|
16967
|
+
}
|
|
16968
|
+
async function resolveEmulator() {
|
|
16969
|
+
const sdk = androidSdkRoot();
|
|
16970
|
+
return resolveTool([...sdk ? [join6(sdk, "emulator/emulator")] : [], "emulator"]);
|
|
16971
|
+
}
|
|
16972
|
+
function formatIosVersion(version2) {
|
|
16973
|
+
const trimmed = version2.trim();
|
|
16974
|
+
if (!trimmed)
|
|
16975
|
+
return "iOS";
|
|
16976
|
+
if (/^iOS\b/i.test(trimmed))
|
|
16977
|
+
return trimmed;
|
|
16978
|
+
return `iOS ${trimmed}`;
|
|
16979
|
+
}
|
|
16980
|
+
function formatAndroidVersion(apiOrVersion) {
|
|
16981
|
+
const trimmed = apiOrVersion.trim();
|
|
16982
|
+
if (!trimmed)
|
|
16983
|
+
return "Android";
|
|
16984
|
+
if (/^Android\b/i.test(trimmed))
|
|
16985
|
+
return trimmed;
|
|
16986
|
+
if (/^API\b/i.test(trimmed))
|
|
16987
|
+
return trimmed;
|
|
16988
|
+
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
16989
|
+
return `API ${trimmed}`;
|
|
16990
|
+
return `Android ${trimmed}`;
|
|
16991
|
+
}
|
|
16992
|
+
function titleCaseWords(value) {
|
|
16993
|
+
return value.split(/\s+/).filter(Boolean).map((word) => {
|
|
16994
|
+
if (/^[a-z]$/i.test(word))
|
|
16995
|
+
return word.toUpperCase();
|
|
16996
|
+
if (/^\d/.test(word))
|
|
16997
|
+
return word;
|
|
16998
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
16999
|
+
}).join(" ");
|
|
17000
|
+
}
|
|
17001
|
+
function runtimeVersionFromIdentifier(runtimeId) {
|
|
17002
|
+
const match = runtimeId.match(/iOS-([\d-]+)$/i);
|
|
17003
|
+
if (!match?.[1])
|
|
17004
|
+
return "iOS";
|
|
17005
|
+
return formatIosVersion(match[1].replace(/-/g, "."));
|
|
17006
|
+
}
|
|
17007
|
+
async function listIosSimulators(includeUnavailable) {
|
|
17008
|
+
const { stdout, exitCode } = await runCommand3([
|
|
17009
|
+
"xcrun",
|
|
17010
|
+
"simctl",
|
|
17011
|
+
"list",
|
|
17012
|
+
"devices",
|
|
17013
|
+
"available",
|
|
17014
|
+
"-j"
|
|
17015
|
+
]);
|
|
17016
|
+
if (exitCode !== 0)
|
|
17017
|
+
return [];
|
|
17018
|
+
let parsed;
|
|
17019
|
+
try {
|
|
17020
|
+
parsed = JSON.parse(stdout);
|
|
17021
|
+
} catch {
|
|
17022
|
+
return [];
|
|
17023
|
+
}
|
|
17024
|
+
const devices = [];
|
|
17025
|
+
for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
|
|
17026
|
+
if (!/iOS/i.test(runtimeId))
|
|
17027
|
+
continue;
|
|
17028
|
+
const osVersion = runtimeVersionFromIdentifier(runtimeId);
|
|
17029
|
+
for (const device of runtimeDevices) {
|
|
17030
|
+
if (!device.udid || !device.name)
|
|
17031
|
+
continue;
|
|
17032
|
+
if (device.isAvailable === false)
|
|
17033
|
+
continue;
|
|
17034
|
+
const state = device.state ?? "Unknown";
|
|
17035
|
+
if (!includeUnavailable && state !== "Booted")
|
|
17036
|
+
continue;
|
|
17037
|
+
devices.push({
|
|
17038
|
+
id: device.udid,
|
|
17039
|
+
name: device.name,
|
|
17040
|
+
osVersion,
|
|
17041
|
+
platform: "ios",
|
|
17042
|
+
kind: "simulator",
|
|
17043
|
+
state,
|
|
17044
|
+
model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
|
|
17045
|
+
});
|
|
17046
|
+
}
|
|
17047
|
+
}
|
|
17048
|
+
return devices.sort((a, b) => {
|
|
17049
|
+
if (a.state === "Booted" && b.state !== "Booted")
|
|
17050
|
+
return -1;
|
|
17051
|
+
if (b.state === "Booted" && a.state !== "Booted")
|
|
17052
|
+
return 1;
|
|
17053
|
+
return a.name.localeCompare(b.name);
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
17056
|
+
function isPhoneOrTablet(deviceType) {
|
|
17057
|
+
if (!deviceType)
|
|
17058
|
+
return false;
|
|
17059
|
+
return /^(iPhone|iPad)$/i.test(deviceType);
|
|
17060
|
+
}
|
|
17061
|
+
function physicalIosState(connection) {
|
|
17062
|
+
const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
|
|
17063
|
+
if (tunnel.includes("connected"))
|
|
17064
|
+
return "connected";
|
|
17065
|
+
if (tunnel.includes("unavailable"))
|
|
17066
|
+
return "unavailable";
|
|
17067
|
+
if (tunnel.includes("disconnected"))
|
|
17068
|
+
return "disconnected";
|
|
17069
|
+
if (connection?.pairingState === "paired")
|
|
17070
|
+
return "paired";
|
|
17071
|
+
return tunnel || connection?.pairingState || "unknown";
|
|
17072
|
+
}
|
|
17073
|
+
async function listIosPhysicalDevices(includeUnavailable) {
|
|
17074
|
+
const dir = await mkdtemp2(join6(tmpdir2(), "yoqa-devices-"));
|
|
17075
|
+
const jsonPath = join6(dir, "devices.json");
|
|
17076
|
+
try {
|
|
17077
|
+
const { exitCode } = await runCommand3([
|
|
17078
|
+
"xcrun",
|
|
17079
|
+
"devicectl",
|
|
17080
|
+
"list",
|
|
17081
|
+
"devices",
|
|
17082
|
+
"--json-output",
|
|
17083
|
+
jsonPath
|
|
17084
|
+
]);
|
|
17085
|
+
if (exitCode !== 0)
|
|
17086
|
+
return [];
|
|
17087
|
+
let parsed;
|
|
17088
|
+
try {
|
|
17089
|
+
parsed = JSON.parse(await readFile(jsonPath, "utf8"));
|
|
17090
|
+
} catch {
|
|
17091
|
+
return [];
|
|
17092
|
+
}
|
|
17093
|
+
const devices = [];
|
|
17094
|
+
for (const item of parsed.result?.devices ?? []) {
|
|
17095
|
+
const hardware = item.hardwareProperties;
|
|
17096
|
+
const props = item.deviceProperties;
|
|
17097
|
+
if (!hardware?.udid)
|
|
17098
|
+
continue;
|
|
17099
|
+
if (hardware.reality && hardware.reality !== "physical")
|
|
17100
|
+
continue;
|
|
17101
|
+
if (!isPhoneOrTablet(hardware.deviceType))
|
|
17102
|
+
continue;
|
|
17103
|
+
if (hardware.platform && !/^iOS$/i.test(hardware.platform))
|
|
17104
|
+
continue;
|
|
17105
|
+
const state = physicalIosState(item.connectionProperties);
|
|
17106
|
+
if (!includeUnavailable && state === "unavailable")
|
|
17107
|
+
continue;
|
|
17108
|
+
const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
|
|
17109
|
+
const owner = props?.name?.trim() || undefined;
|
|
17110
|
+
devices.push({
|
|
17111
|
+
id: hardware.udid,
|
|
17112
|
+
name: modelName,
|
|
17113
|
+
owner,
|
|
17114
|
+
osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
|
|
17115
|
+
platform: "ios",
|
|
17116
|
+
kind: "physical",
|
|
17117
|
+
state,
|
|
17118
|
+
model: hardware.productType
|
|
17119
|
+
});
|
|
17120
|
+
}
|
|
17121
|
+
return devices.sort((a, b) => a.name.localeCompare(b.name));
|
|
17122
|
+
} finally {
|
|
17123
|
+
await rm2(dir, { recursive: true, force: true });
|
|
17124
|
+
}
|
|
17125
|
+
}
|
|
17126
|
+
async function listIosDevices(options = {}) {
|
|
17127
|
+
const includeUnavailable = options.includeUnavailable ?? true;
|
|
17128
|
+
const [physical, simulators] = await Promise.all([
|
|
17129
|
+
listIosPhysicalDevices(includeUnavailable),
|
|
17130
|
+
listIosSimulators(includeUnavailable)
|
|
17131
|
+
]);
|
|
17132
|
+
return [...physical, ...simulators];
|
|
17133
|
+
}
|
|
17134
|
+
function parseAdbDevices(output) {
|
|
17135
|
+
const lines = output.split(`
|
|
17136
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
17137
|
+
const results = [];
|
|
17138
|
+
for (const line of lines) {
|
|
17139
|
+
if (line.startsWith("List of devices"))
|
|
17140
|
+
continue;
|
|
17141
|
+
const parts = line.split(/\s+/);
|
|
17142
|
+
const serial = parts[0];
|
|
17143
|
+
const state = parts[1];
|
|
17144
|
+
if (!serial || !state)
|
|
17145
|
+
continue;
|
|
17146
|
+
const meta = {};
|
|
17147
|
+
for (const part of parts.slice(2)) {
|
|
17148
|
+
const eq2 = part.indexOf(":");
|
|
17149
|
+
if (eq2 <= 0)
|
|
17150
|
+
continue;
|
|
17151
|
+
meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
|
|
17152
|
+
}
|
|
17153
|
+
results.push({
|
|
17154
|
+
serial,
|
|
17155
|
+
state,
|
|
17156
|
+
product: meta.product,
|
|
17157
|
+
model: meta.model,
|
|
17158
|
+
device: meta.device,
|
|
17159
|
+
transportId: meta.transport_id
|
|
17160
|
+
});
|
|
17161
|
+
}
|
|
17162
|
+
return results;
|
|
17163
|
+
}
|
|
17164
|
+
function humanizeAvdName(avdName) {
|
|
17165
|
+
return titleCaseWords(avdName.replace(/_/g, " "));
|
|
17166
|
+
}
|
|
17167
|
+
function humanizeModel(model) {
|
|
17168
|
+
if (!model)
|
|
17169
|
+
return;
|
|
17170
|
+
return titleCaseWords(model.replace(/_/g, " "));
|
|
17171
|
+
}
|
|
17172
|
+
async function findAvdConfigPath(avdName) {
|
|
17173
|
+
const home = process.env.HOME ?? "";
|
|
17174
|
+
const roots = [join6(home, ".android/avd"), join6(home, "Library/Android/sdk/.android/avd")];
|
|
17175
|
+
for (const root of roots) {
|
|
17176
|
+
const direct = join6(root, `${avdName}.avd`, "config.ini");
|
|
17177
|
+
try {
|
|
17178
|
+
await readFile(direct, "utf8");
|
|
17179
|
+
return direct;
|
|
17180
|
+
} catch {}
|
|
17181
|
+
try {
|
|
17182
|
+
const glob = new Bun.Glob("*.avd/config.ini");
|
|
17183
|
+
for await (const match of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
17184
|
+
const path = join6(root, match);
|
|
17185
|
+
const text2 = await readFile(path, "utf8");
|
|
17186
|
+
const idLine = text2.split(`
|
|
17187
|
+
`).find((line) => line.startsWith("AvdId="));
|
|
17188
|
+
const id = idLine?.slice("AvdId=".length).trim();
|
|
17189
|
+
if (id === avdName)
|
|
17190
|
+
return path;
|
|
17191
|
+
}
|
|
17192
|
+
} catch {}
|
|
17193
|
+
}
|
|
17194
|
+
return null;
|
|
17195
|
+
}
|
|
17196
|
+
async function readAvdConfig(avdName) {
|
|
17197
|
+
const path = await findAvdConfigPath(avdName);
|
|
17198
|
+
if (!path)
|
|
17199
|
+
return { api: null, deviceName: null };
|
|
17200
|
+
try {
|
|
17201
|
+
const text2 = await readFile(path, "utf8");
|
|
17202
|
+
const imageLine = text2.split(`
|
|
17203
|
+
`).find((line) => line.includes("system-images") && line.includes("android-"));
|
|
17204
|
+
const apiMatch = imageLine?.match(/android-([\d.]+)/);
|
|
17205
|
+
const deviceLine = text2.split(`
|
|
17206
|
+
`).find((entry) => entry.startsWith("hw.device.name="));
|
|
17207
|
+
const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
|
|
17208
|
+
return {
|
|
17209
|
+
api: apiMatch?.[1] ?? null,
|
|
17210
|
+
deviceName: humanizeModel(deviceRaw) ?? null
|
|
17211
|
+
};
|
|
17212
|
+
} catch {
|
|
17213
|
+
return { api: null, deviceName: null };
|
|
17214
|
+
}
|
|
17215
|
+
}
|
|
17216
|
+
async function adbAvdName(adb, serial) {
|
|
17217
|
+
const fromProp = await adbGetProp(adb, serial, "ro.boot.qemu.avd_name") ?? await adbGetProp(adb, serial, "ro.kernel.qemu.avd_name");
|
|
17218
|
+
if (fromProp)
|
|
17219
|
+
return fromProp;
|
|
17220
|
+
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
|
|
17221
|
+
if (exitCode !== 0)
|
|
17222
|
+
return null;
|
|
17223
|
+
return parseAdbEmuAvdName(stdout);
|
|
17224
|
+
}
|
|
17225
|
+
async function adbGetProp(adb, serial, prop) {
|
|
17226
|
+
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
|
|
17227
|
+
if (exitCode !== 0)
|
|
17228
|
+
return null;
|
|
17229
|
+
const value = stdout.trim();
|
|
17230
|
+
return value || null;
|
|
17231
|
+
}
|
|
17232
|
+
async function listAndroidAdbDevices(adb) {
|
|
17233
|
+
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
17234
|
+
if (exitCode !== 0) {
|
|
17235
|
+
return { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
17236
|
+
}
|
|
17237
|
+
const entries = parseAdbDevices(stdout);
|
|
17238
|
+
const physical = [];
|
|
17239
|
+
const runningEmulators = [];
|
|
17240
|
+
const runningAvdNames = new Set;
|
|
17241
|
+
for (const entry of entries) {
|
|
17242
|
+
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
17243
|
+
if (isEmulator) {
|
|
17244
|
+
const avdName = await adbAvdName(adb, entry.serial);
|
|
17245
|
+
if (avdName)
|
|
17246
|
+
runningAvdNames.add(avdName);
|
|
17247
|
+
const avdConfig = avdName ? await readAvdConfig(avdName) : null;
|
|
17248
|
+
const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
|
|
17249
|
+
const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
|
|
17250
|
+
runningEmulators.push({
|
|
17251
|
+
id: avdName ?? entry.serial,
|
|
17252
|
+
name: model2,
|
|
17253
|
+
osVersion: formatAndroidVersion(release2 ?? ""),
|
|
17254
|
+
platform: "android",
|
|
17255
|
+
kind: "emulator",
|
|
17256
|
+
state: entry.state === "device" ? "online" : entry.state,
|
|
17257
|
+
model: avdName ?? entry.model
|
|
17258
|
+
});
|
|
17259
|
+
continue;
|
|
17260
|
+
}
|
|
17261
|
+
const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
|
|
17262
|
+
const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
|
|
17263
|
+
physical.push({
|
|
17264
|
+
id: entry.serial,
|
|
17265
|
+
name: model,
|
|
17266
|
+
osVersion: formatAndroidVersion(release ?? ""),
|
|
17267
|
+
platform: "android",
|
|
17268
|
+
kind: "physical",
|
|
17269
|
+
state: entry.state,
|
|
17270
|
+
model: entry.model
|
|
17271
|
+
});
|
|
17272
|
+
}
|
|
17273
|
+
return { physical, runningEmulators, runningAvdNames };
|
|
17274
|
+
}
|
|
17275
|
+
async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
|
|
17276
|
+
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
17277
|
+
if (exitCode !== 0)
|
|
17278
|
+
return [];
|
|
17279
|
+
const names = stdout.split(`
|
|
17280
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
17281
|
+
const devices = [];
|
|
17282
|
+
for (const avdName of names) {
|
|
17283
|
+
const isRunning = runningAvdNames.has(avdName);
|
|
17284
|
+
if (!includeUnavailable && !isRunning)
|
|
17285
|
+
continue;
|
|
17286
|
+
if (isRunning)
|
|
17287
|
+
continue;
|
|
17288
|
+
const { api, deviceName } = await readAvdConfig(avdName);
|
|
17289
|
+
devices.push({
|
|
17290
|
+
id: avdName,
|
|
17291
|
+
name: deviceName ?? humanizeAvdName(avdName),
|
|
17292
|
+
osVersion: formatAndroidVersion(api ?? ""),
|
|
17293
|
+
platform: "android",
|
|
17294
|
+
kind: "emulator",
|
|
17295
|
+
state: "offline",
|
|
17296
|
+
model: avdName
|
|
17297
|
+
});
|
|
17298
|
+
}
|
|
17299
|
+
return devices;
|
|
17300
|
+
}
|
|
17301
|
+
async function resolveAndroidAppiumIdentity(deviceId) {
|
|
17302
|
+
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
17303
|
+
const connected = [];
|
|
17304
|
+
if (adb) {
|
|
17305
|
+
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
17306
|
+
if (exitCode === 0) {
|
|
17307
|
+
for (const entry of parseAdbDevices(stdout)) {
|
|
17308
|
+
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
17309
|
+
connected.push({
|
|
17310
|
+
serial: entry.serial,
|
|
17311
|
+
avdName: isEmulator ? await adbAvdName(adb, entry.serial) : null
|
|
17312
|
+
});
|
|
17313
|
+
}
|
|
17314
|
+
}
|
|
17315
|
+
}
|
|
17316
|
+
const knownAvds = [];
|
|
17317
|
+
if (emulatorBin) {
|
|
17318
|
+
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
17319
|
+
if (exitCode === 0) {
|
|
17320
|
+
knownAvds.push(...stdout.split(`
|
|
17321
|
+
`).map((line) => line.trim()).filter(Boolean));
|
|
17322
|
+
}
|
|
17323
|
+
}
|
|
17324
|
+
return matchAndroidAppiumIdentity(deviceId, connected, knownAvds);
|
|
17325
|
+
}
|
|
17326
|
+
async function listAndroidDevices(options = {}) {
|
|
17327
|
+
const includeUnavailable = options.includeUnavailable ?? true;
|
|
17328
|
+
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
17329
|
+
const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
17330
|
+
const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
|
|
17331
|
+
const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
|
|
17332
|
+
const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
|
|
17333
|
+
const aOnline = a.state === "online" ? 0 : 1;
|
|
17334
|
+
const bOnline = b.state === "online" ? 0 : 1;
|
|
17335
|
+
if (aOnline !== bOnline)
|
|
17336
|
+
return aOnline - bOnline;
|
|
17337
|
+
return a.name.localeCompare(b.name);
|
|
17338
|
+
});
|
|
17339
|
+
return [...physical, ...emulators];
|
|
17340
|
+
}
|
|
17341
|
+
async function listDevices(platform, options = {}) {
|
|
17342
|
+
if (platform === "ios")
|
|
17343
|
+
return listIosDevices(options);
|
|
17344
|
+
return listAndroidDevices(options);
|
|
17345
|
+
}
|
|
17346
|
+
|
|
17347
|
+
// src/domains/devices/session.ts
|
|
17348
|
+
var YOQA_ROOT4 = join7(homedir6(), ".yoqa");
|
|
16902
17349
|
var DEFAULT_MJPEG_PORT = Number(process.env.YOQA_MJPEG_PORT ?? "9100");
|
|
16903
|
-
var SCREENSHOT_DIR =
|
|
17350
|
+
var SCREENSHOT_DIR = join7(YOQA_ROOT4, "runs", "screenshots");
|
|
16904
17351
|
var MJPEG_SETTINGS_BASE = {
|
|
16905
17352
|
mjpegServerScreenshotQuality: 35,
|
|
16906
17353
|
mjpegScalingFactor: 50
|
|
@@ -17022,7 +17469,7 @@ function looksLikePhysicalIosUdid(udid) {
|
|
|
17022
17469
|
}
|
|
17023
17470
|
var PHYSICAL_IOS_SESSION_TIMEOUT_MS = 60000;
|
|
17024
17471
|
var SIMULATOR_WDA_SESSION_TIMEOUT_MS = 600000;
|
|
17025
|
-
var SIMULATOR_WDA_DERIVED_DATA =
|
|
17472
|
+
var SIMULATOR_WDA_DERIVED_DATA = join7(YOQA_ROOT4, "wda-sim");
|
|
17026
17473
|
function iosSessionCreateTimeoutMs(deviceId) {
|
|
17027
17474
|
return looksLikePhysicalIosUdid(deviceId) ? PHYSICAL_IOS_SESSION_TIMEOUT_MS : SIMULATOR_WDA_SESSION_TIMEOUT_MS;
|
|
17028
17475
|
}
|
|
@@ -17033,18 +17480,17 @@ async function buildW3cCapabilities(options, mjpegPort) {
|
|
|
17033
17480
|
const caps = {
|
|
17034
17481
|
platformName,
|
|
17035
17482
|
"appium:automationName": automationName,
|
|
17036
|
-
"appium:udid": options.deviceId,
|
|
17037
17483
|
"appium:newCommandTimeout": 3600,
|
|
17038
17484
|
"appium:mjpegServerPort": mjpegPort,
|
|
17039
17485
|
...Object.fromEntries(Object.entries(merged).map(([key, value]) => key.includes(":") ? [key, value] : [`appium:${key}`, value]))
|
|
17040
17486
|
};
|
|
17041
|
-
if (options.platform === "ios" && options.bundleId && !caps["appium:bundleId"]) {
|
|
17042
|
-
caps["appium:bundleId"] = options.bundleId;
|
|
17043
|
-
}
|
|
17044
|
-
if (options.platform === "android" && options.appPackage && !caps["appium:appPackage"]) {
|
|
17045
|
-
caps["appium:appPackage"] = options.appPackage;
|
|
17046
|
-
}
|
|
17047
17487
|
if (options.platform === "ios") {
|
|
17488
|
+
if (caps["appium:udid"] === undefined) {
|
|
17489
|
+
caps["appium:udid"] = options.deviceId;
|
|
17490
|
+
}
|
|
17491
|
+
if (options.bundleId && !caps["appium:bundleId"]) {
|
|
17492
|
+
caps["appium:bundleId"] = options.bundleId;
|
|
17493
|
+
}
|
|
17048
17494
|
const physical = looksLikePhysicalIosUdid(options.deviceId);
|
|
17049
17495
|
if (physical) {
|
|
17050
17496
|
const prep = await loadDevicePrep(options.deviceId);
|
|
@@ -17079,6 +17525,19 @@ async function buildW3cCapabilities(options, mjpegPort) {
|
|
|
17079
17525
|
caps["appium:derivedDataPath"] = SIMULATOR_WDA_DERIVED_DATA;
|
|
17080
17526
|
}
|
|
17081
17527
|
}
|
|
17528
|
+
if (options.platform === "android") {
|
|
17529
|
+
const requested = String(caps["appium:udid"] ?? options.deviceId);
|
|
17530
|
+
const identity = await resolveAndroidAppiumIdentity(requested);
|
|
17531
|
+
if (identity.udid) {
|
|
17532
|
+
caps["appium:udid"] = identity.udid;
|
|
17533
|
+
}
|
|
17534
|
+
if (identity.avd && caps["appium:avd"] === undefined) {
|
|
17535
|
+
caps["appium:avd"] = identity.avd;
|
|
17536
|
+
}
|
|
17537
|
+
if (options.appPackage && !caps["appium:appPackage"]) {
|
|
17538
|
+
caps["appium:appPackage"] = options.appPackage;
|
|
17539
|
+
}
|
|
17540
|
+
}
|
|
17082
17541
|
return caps;
|
|
17083
17542
|
}
|
|
17084
17543
|
|
|
@@ -17369,7 +17828,7 @@ async function createDeviceSession(options) {
|
|
|
17369
17828
|
const screenshot = async () => {
|
|
17370
17829
|
await mkdir3(SCREENSHOT_DIR, { recursive: true });
|
|
17371
17830
|
const frame = await captureFrame();
|
|
17372
|
-
const path =
|
|
17831
|
+
const path = join7(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
|
|
17373
17832
|
await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
|
|
17374
17833
|
return { path, base64: frame.base64 };
|
|
17375
17834
|
};
|
|
@@ -23799,7 +24258,7 @@ var probeProviderResponseSchema = exports_external.object({
|
|
|
23799
24258
|
var providerModelSchema = exports_external.object({
|
|
23800
24259
|
id: exports_external.string().min(1),
|
|
23801
24260
|
name: exports_external.string().min(1),
|
|
23802
|
-
|
|
24261
|
+
provider: exports_external.string().min(1).optional()
|
|
23803
24262
|
});
|
|
23804
24263
|
var listProviderModelsResponseSchema = exports_external.object({
|
|
23805
24264
|
models: exports_external.array(providerModelSchema),
|
|
@@ -24193,8 +24652,13 @@ async function installBuildOnDevice(options) {
|
|
|
24193
24652
|
}
|
|
24194
24653
|
throw new Error(`Cannot install iOS build at ${build.path}. Use a .app on simulator or install .ipa via ideviceinstaller.`);
|
|
24195
24654
|
}
|
|
24655
|
+
const identity = await resolveAndroidAppiumIdentity(deviceId);
|
|
24656
|
+
const serial = identity.udid;
|
|
24657
|
+
if (!serial) {
|
|
24658
|
+
throw new Error(`Android emulator ${deviceId} is not running. Boot it, then retry the install.`);
|
|
24659
|
+
}
|
|
24196
24660
|
const adb = Bun.which("adb") ?? "adb";
|
|
24197
|
-
const proc = Bun.spawn([adb, "-s",
|
|
24661
|
+
const proc = Bun.spawn([adb, "-s", serial, "install", "-r", build.path], {
|
|
24198
24662
|
stdout: "pipe",
|
|
24199
24663
|
stderr: "pipe"
|
|
24200
24664
|
});
|
|
@@ -25017,397 +25481,6 @@ function createCatalogRoutes() {
|
|
|
25017
25481
|
return app;
|
|
25018
25482
|
}
|
|
25019
25483
|
|
|
25020
|
-
// src/domains/devices/application.ts
|
|
25021
|
-
import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
|
|
25022
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
25023
|
-
import { join as join7 } from "path";
|
|
25024
|
-
async function runCommand3(command, options) {
|
|
25025
|
-
try {
|
|
25026
|
-
const proc = Bun.spawn(command, {
|
|
25027
|
-
env: options?.env ? { ...process.env, ...options.env } : process.env,
|
|
25028
|
-
stdout: "pipe",
|
|
25029
|
-
stderr: "pipe"
|
|
25030
|
-
});
|
|
25031
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
25032
|
-
new Response(proc.stdout).text(),
|
|
25033
|
-
new Response(proc.stderr).text(),
|
|
25034
|
-
proc.exited
|
|
25035
|
-
]);
|
|
25036
|
-
return { stdout, stderr, exitCode };
|
|
25037
|
-
} catch {
|
|
25038
|
-
return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
|
|
25039
|
-
}
|
|
25040
|
-
}
|
|
25041
|
-
function androidSdkRoot() {
|
|
25042
|
-
return resolveAndroidSdkRoot();
|
|
25043
|
-
}
|
|
25044
|
-
async function resolveTool(candidates) {
|
|
25045
|
-
for (const candidate of candidates) {
|
|
25046
|
-
if (!candidate)
|
|
25047
|
-
continue;
|
|
25048
|
-
const { exitCode } = await runCommand3(["test", "-x", candidate]);
|
|
25049
|
-
if (exitCode === 0)
|
|
25050
|
-
return candidate;
|
|
25051
|
-
}
|
|
25052
|
-
return null;
|
|
25053
|
-
}
|
|
25054
|
-
async function resolveAdb() {
|
|
25055
|
-
const sdk = androidSdkRoot();
|
|
25056
|
-
return resolveTool([...sdk ? [join7(sdk, "platform-tools/adb")] : [], "adb"]);
|
|
25057
|
-
}
|
|
25058
|
-
async function resolveEmulator() {
|
|
25059
|
-
const sdk = androidSdkRoot();
|
|
25060
|
-
return resolveTool([...sdk ? [join7(sdk, "emulator/emulator")] : [], "emulator"]);
|
|
25061
|
-
}
|
|
25062
|
-
function formatIosVersion(version2) {
|
|
25063
|
-
const trimmed = version2.trim();
|
|
25064
|
-
if (!trimmed)
|
|
25065
|
-
return "iOS";
|
|
25066
|
-
if (/^iOS\b/i.test(trimmed))
|
|
25067
|
-
return trimmed;
|
|
25068
|
-
return `iOS ${trimmed}`;
|
|
25069
|
-
}
|
|
25070
|
-
function formatAndroidVersion(apiOrVersion) {
|
|
25071
|
-
const trimmed = apiOrVersion.trim();
|
|
25072
|
-
if (!trimmed)
|
|
25073
|
-
return "Android";
|
|
25074
|
-
if (/^Android\b/i.test(trimmed))
|
|
25075
|
-
return trimmed;
|
|
25076
|
-
if (/^API\b/i.test(trimmed))
|
|
25077
|
-
return trimmed;
|
|
25078
|
-
if (/^\d+(\.\d+)?$/.test(trimmed))
|
|
25079
|
-
return `API ${trimmed}`;
|
|
25080
|
-
return `Android ${trimmed}`;
|
|
25081
|
-
}
|
|
25082
|
-
function titleCaseWords(value) {
|
|
25083
|
-
return value.split(/\s+/).filter(Boolean).map((word) => {
|
|
25084
|
-
if (/^[a-z]$/i.test(word))
|
|
25085
|
-
return word.toUpperCase();
|
|
25086
|
-
if (/^\d/.test(word))
|
|
25087
|
-
return word;
|
|
25088
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
25089
|
-
}).join(" ");
|
|
25090
|
-
}
|
|
25091
|
-
function runtimeVersionFromIdentifier(runtimeId) {
|
|
25092
|
-
const match2 = runtimeId.match(/iOS-([\d-]+)$/i);
|
|
25093
|
-
if (!match2?.[1])
|
|
25094
|
-
return "iOS";
|
|
25095
|
-
return formatIosVersion(match2[1].replace(/-/g, "."));
|
|
25096
|
-
}
|
|
25097
|
-
async function listIosSimulators(includeUnavailable) {
|
|
25098
|
-
const { stdout, exitCode } = await runCommand3([
|
|
25099
|
-
"xcrun",
|
|
25100
|
-
"simctl",
|
|
25101
|
-
"list",
|
|
25102
|
-
"devices",
|
|
25103
|
-
"available",
|
|
25104
|
-
"-j"
|
|
25105
|
-
]);
|
|
25106
|
-
if (exitCode !== 0)
|
|
25107
|
-
return [];
|
|
25108
|
-
let parsed;
|
|
25109
|
-
try {
|
|
25110
|
-
parsed = JSON.parse(stdout);
|
|
25111
|
-
} catch {
|
|
25112
|
-
return [];
|
|
25113
|
-
}
|
|
25114
|
-
const devices = [];
|
|
25115
|
-
for (const [runtimeId, runtimeDevices] of Object.entries(parsed.devices ?? {})) {
|
|
25116
|
-
if (!/iOS/i.test(runtimeId))
|
|
25117
|
-
continue;
|
|
25118
|
-
const osVersion = runtimeVersionFromIdentifier(runtimeId);
|
|
25119
|
-
for (const device of runtimeDevices) {
|
|
25120
|
-
if (!device.udid || !device.name)
|
|
25121
|
-
continue;
|
|
25122
|
-
if (device.isAvailable === false)
|
|
25123
|
-
continue;
|
|
25124
|
-
const state = device.state ?? "Unknown";
|
|
25125
|
-
if (!includeUnavailable && state !== "Booted")
|
|
25126
|
-
continue;
|
|
25127
|
-
devices.push({
|
|
25128
|
-
id: device.udid,
|
|
25129
|
-
name: device.name,
|
|
25130
|
-
osVersion,
|
|
25131
|
-
platform: "ios",
|
|
25132
|
-
kind: "simulator",
|
|
25133
|
-
state,
|
|
25134
|
-
model: device.deviceTypeIdentifier?.split(".").pop()?.replace(/-/g, " ")
|
|
25135
|
-
});
|
|
25136
|
-
}
|
|
25137
|
-
}
|
|
25138
|
-
return devices.sort((a, b) => {
|
|
25139
|
-
if (a.state === "Booted" && b.state !== "Booted")
|
|
25140
|
-
return -1;
|
|
25141
|
-
if (b.state === "Booted" && a.state !== "Booted")
|
|
25142
|
-
return 1;
|
|
25143
|
-
return a.name.localeCompare(b.name);
|
|
25144
|
-
});
|
|
25145
|
-
}
|
|
25146
|
-
function isPhoneOrTablet(deviceType) {
|
|
25147
|
-
if (!deviceType)
|
|
25148
|
-
return false;
|
|
25149
|
-
return /^(iPhone|iPad)$/i.test(deviceType);
|
|
25150
|
-
}
|
|
25151
|
-
function physicalIosState(connection) {
|
|
25152
|
-
const tunnel = connection?.tunnelState?.toLowerCase() ?? "";
|
|
25153
|
-
if (tunnel.includes("connected"))
|
|
25154
|
-
return "connected";
|
|
25155
|
-
if (tunnel.includes("unavailable"))
|
|
25156
|
-
return "unavailable";
|
|
25157
|
-
if (tunnel.includes("disconnected"))
|
|
25158
|
-
return "disconnected";
|
|
25159
|
-
if (connection?.pairingState === "paired")
|
|
25160
|
-
return "paired";
|
|
25161
|
-
return tunnel || connection?.pairingState || "unknown";
|
|
25162
|
-
}
|
|
25163
|
-
async function listIosPhysicalDevices(includeUnavailable) {
|
|
25164
|
-
const dir = await mkdtemp2(join7(tmpdir2(), "yoqa-devices-"));
|
|
25165
|
-
const jsonPath = join7(dir, "devices.json");
|
|
25166
|
-
try {
|
|
25167
|
-
const { exitCode } = await runCommand3([
|
|
25168
|
-
"xcrun",
|
|
25169
|
-
"devicectl",
|
|
25170
|
-
"list",
|
|
25171
|
-
"devices",
|
|
25172
|
-
"--json-output",
|
|
25173
|
-
jsonPath
|
|
25174
|
-
]);
|
|
25175
|
-
if (exitCode !== 0)
|
|
25176
|
-
return [];
|
|
25177
|
-
let parsed;
|
|
25178
|
-
try {
|
|
25179
|
-
parsed = JSON.parse(await readFile(jsonPath, "utf8"));
|
|
25180
|
-
} catch {
|
|
25181
|
-
return [];
|
|
25182
|
-
}
|
|
25183
|
-
const devices = [];
|
|
25184
|
-
for (const item of parsed.result?.devices ?? []) {
|
|
25185
|
-
const hardware = item.hardwareProperties;
|
|
25186
|
-
const props = item.deviceProperties;
|
|
25187
|
-
if (!hardware?.udid)
|
|
25188
|
-
continue;
|
|
25189
|
-
if (hardware.reality && hardware.reality !== "physical")
|
|
25190
|
-
continue;
|
|
25191
|
-
if (!isPhoneOrTablet(hardware.deviceType))
|
|
25192
|
-
continue;
|
|
25193
|
-
if (hardware.platform && !/^iOS$/i.test(hardware.platform))
|
|
25194
|
-
continue;
|
|
25195
|
-
const state = physicalIosState(item.connectionProperties);
|
|
25196
|
-
if (!includeUnavailable && state === "unavailable")
|
|
25197
|
-
continue;
|
|
25198
|
-
const modelName = hardware.marketingName ?? hardware.productType ?? "iPhone";
|
|
25199
|
-
const owner = props?.name?.trim() || undefined;
|
|
25200
|
-
devices.push({
|
|
25201
|
-
id: hardware.udid,
|
|
25202
|
-
name: modelName,
|
|
25203
|
-
owner,
|
|
25204
|
-
osVersion: formatIosVersion(props?.osVersionNumber ?? ""),
|
|
25205
|
-
platform: "ios",
|
|
25206
|
-
kind: "physical",
|
|
25207
|
-
state,
|
|
25208
|
-
model: hardware.productType
|
|
25209
|
-
});
|
|
25210
|
-
}
|
|
25211
|
-
return devices.sort((a, b) => a.name.localeCompare(b.name));
|
|
25212
|
-
} finally {
|
|
25213
|
-
await rm2(dir, { recursive: true, force: true });
|
|
25214
|
-
}
|
|
25215
|
-
}
|
|
25216
|
-
async function listIosDevices(options = {}) {
|
|
25217
|
-
const includeUnavailable = options.includeUnavailable ?? true;
|
|
25218
|
-
const [physical, simulators] = await Promise.all([
|
|
25219
|
-
listIosPhysicalDevices(includeUnavailable),
|
|
25220
|
-
listIosSimulators(includeUnavailable)
|
|
25221
|
-
]);
|
|
25222
|
-
return [...physical, ...simulators];
|
|
25223
|
-
}
|
|
25224
|
-
function parseAdbDevices(output) {
|
|
25225
|
-
const lines = output.split(`
|
|
25226
|
-
`).map((line) => line.trim()).filter(Boolean);
|
|
25227
|
-
const results = [];
|
|
25228
|
-
for (const line of lines) {
|
|
25229
|
-
if (line.startsWith("List of devices"))
|
|
25230
|
-
continue;
|
|
25231
|
-
const parts = line.split(/\s+/);
|
|
25232
|
-
const serial = parts[0];
|
|
25233
|
-
const state = parts[1];
|
|
25234
|
-
if (!serial || !state)
|
|
25235
|
-
continue;
|
|
25236
|
-
const meta = {};
|
|
25237
|
-
for (const part of parts.slice(2)) {
|
|
25238
|
-
const eq2 = part.indexOf(":");
|
|
25239
|
-
if (eq2 <= 0)
|
|
25240
|
-
continue;
|
|
25241
|
-
meta[part.slice(0, eq2)] = part.slice(eq2 + 1);
|
|
25242
|
-
}
|
|
25243
|
-
results.push({
|
|
25244
|
-
serial,
|
|
25245
|
-
state,
|
|
25246
|
-
product: meta.product,
|
|
25247
|
-
model: meta.model,
|
|
25248
|
-
device: meta.device,
|
|
25249
|
-
transportId: meta.transport_id
|
|
25250
|
-
});
|
|
25251
|
-
}
|
|
25252
|
-
return results;
|
|
25253
|
-
}
|
|
25254
|
-
function humanizeAvdName(avdName) {
|
|
25255
|
-
return titleCaseWords(avdName.replace(/_/g, " "));
|
|
25256
|
-
}
|
|
25257
|
-
function humanizeModel(model) {
|
|
25258
|
-
if (!model)
|
|
25259
|
-
return;
|
|
25260
|
-
return titleCaseWords(model.replace(/_/g, " "));
|
|
25261
|
-
}
|
|
25262
|
-
async function findAvdConfigPath(avdName) {
|
|
25263
|
-
const home = process.env.HOME ?? "";
|
|
25264
|
-
const roots = [join7(home, ".android/avd"), join7(home, "Library/Android/sdk/.android/avd")];
|
|
25265
|
-
for (const root of roots) {
|
|
25266
|
-
const direct = join7(root, `${avdName}.avd`, "config.ini");
|
|
25267
|
-
try {
|
|
25268
|
-
await readFile(direct, "utf8");
|
|
25269
|
-
return direct;
|
|
25270
|
-
} catch {}
|
|
25271
|
-
try {
|
|
25272
|
-
const glob = new Bun.Glob("*.avd/config.ini");
|
|
25273
|
-
for await (const match2 of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
25274
|
-
const path = join7(root, match2);
|
|
25275
|
-
const text2 = await readFile(path, "utf8");
|
|
25276
|
-
const idLine = text2.split(`
|
|
25277
|
-
`).find((line) => line.startsWith("AvdId="));
|
|
25278
|
-
const id = idLine?.slice("AvdId=".length).trim();
|
|
25279
|
-
if (id === avdName)
|
|
25280
|
-
return path;
|
|
25281
|
-
}
|
|
25282
|
-
} catch {}
|
|
25283
|
-
}
|
|
25284
|
-
return null;
|
|
25285
|
-
}
|
|
25286
|
-
async function readAvdConfig(avdName) {
|
|
25287
|
-
const path = await findAvdConfigPath(avdName);
|
|
25288
|
-
if (!path)
|
|
25289
|
-
return { api: null, deviceName: null };
|
|
25290
|
-
try {
|
|
25291
|
-
const text2 = await readFile(path, "utf8");
|
|
25292
|
-
const imageLine = text2.split(`
|
|
25293
|
-
`).find((line) => line.includes("system-images") && line.includes("android-"));
|
|
25294
|
-
const apiMatch = imageLine?.match(/android-([\d.]+)/);
|
|
25295
|
-
const deviceLine = text2.split(`
|
|
25296
|
-
`).find((entry) => entry.startsWith("hw.device.name="));
|
|
25297
|
-
const deviceRaw = deviceLine?.slice("hw.device.name=".length).trim();
|
|
25298
|
-
return {
|
|
25299
|
-
api: apiMatch?.[1] ?? null,
|
|
25300
|
-
deviceName: humanizeModel(deviceRaw) ?? null
|
|
25301
|
-
};
|
|
25302
|
-
} catch {
|
|
25303
|
-
return { api: null, deviceName: null };
|
|
25304
|
-
}
|
|
25305
|
-
}
|
|
25306
|
-
async function adbAvdName(adb, serial) {
|
|
25307
|
-
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "emu", "avd", "name"]);
|
|
25308
|
-
if (exitCode !== 0)
|
|
25309
|
-
return null;
|
|
25310
|
-
const name = stdout.trim().split(`
|
|
25311
|
-
`)[0]?.trim();
|
|
25312
|
-
return name || null;
|
|
25313
|
-
}
|
|
25314
|
-
async function adbGetProp(adb, serial, prop) {
|
|
25315
|
-
const { stdout, exitCode } = await runCommand3([adb, "-s", serial, "shell", "getprop", prop]);
|
|
25316
|
-
if (exitCode !== 0)
|
|
25317
|
-
return null;
|
|
25318
|
-
const value = stdout.trim();
|
|
25319
|
-
return value || null;
|
|
25320
|
-
}
|
|
25321
|
-
async function listAndroidAdbDevices(adb) {
|
|
25322
|
-
const { stdout, exitCode } = await runCommand3([adb, "devices", "-l"]);
|
|
25323
|
-
if (exitCode !== 0) {
|
|
25324
|
-
return { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
25325
|
-
}
|
|
25326
|
-
const entries = parseAdbDevices(stdout);
|
|
25327
|
-
const physical = [];
|
|
25328
|
-
const runningEmulators = [];
|
|
25329
|
-
const runningAvdNames = new Set;
|
|
25330
|
-
for (const entry of entries) {
|
|
25331
|
-
const isEmulator = entry.serial.startsWith("emulator-") || entry.product === "sdk_gphone";
|
|
25332
|
-
if (isEmulator) {
|
|
25333
|
-
const avdName = await adbAvdName(adb, entry.serial);
|
|
25334
|
-
if (avdName)
|
|
25335
|
-
runningAvdNames.add(avdName);
|
|
25336
|
-
const avdConfig = avdName ? await readAvdConfig(avdName) : null;
|
|
25337
|
-
const release2 = await adbGetProp(adb, entry.serial, "ro.build.version.release") ?? avdConfig?.api ?? null;
|
|
25338
|
-
const model2 = humanizeModel(entry.model) ?? avdConfig?.deviceName ?? (avdName ? humanizeAvdName(avdName) : entry.serial);
|
|
25339
|
-
runningEmulators.push({
|
|
25340
|
-
id: avdName ?? entry.serial,
|
|
25341
|
-
name: model2,
|
|
25342
|
-
osVersion: formatAndroidVersion(release2 ?? ""),
|
|
25343
|
-
platform: "android",
|
|
25344
|
-
kind: "emulator",
|
|
25345
|
-
state: entry.state === "device" ? "online" : entry.state,
|
|
25346
|
-
model: avdName ?? entry.model
|
|
25347
|
-
});
|
|
25348
|
-
continue;
|
|
25349
|
-
}
|
|
25350
|
-
const release = await adbGetProp(adb, entry.serial, "ro.build.version.release");
|
|
25351
|
-
const model = humanizeModel(entry.model) ?? await adbGetProp(adb, entry.serial, "ro.product.model") ?? entry.serial;
|
|
25352
|
-
physical.push({
|
|
25353
|
-
id: entry.serial,
|
|
25354
|
-
name: model,
|
|
25355
|
-
osVersion: formatAndroidVersion(release ?? ""),
|
|
25356
|
-
platform: "android",
|
|
25357
|
-
kind: "physical",
|
|
25358
|
-
state: entry.state,
|
|
25359
|
-
model: entry.model
|
|
25360
|
-
});
|
|
25361
|
-
}
|
|
25362
|
-
return { physical, runningEmulators, runningAvdNames };
|
|
25363
|
-
}
|
|
25364
|
-
async function listAndroidAvds(emulatorBin, runningAvdNames, includeUnavailable) {
|
|
25365
|
-
const { stdout, exitCode } = await runCommand3([emulatorBin, "-list-avds"]);
|
|
25366
|
-
if (exitCode !== 0)
|
|
25367
|
-
return [];
|
|
25368
|
-
const names = stdout.split(`
|
|
25369
|
-
`).map((line) => line.trim()).filter(Boolean);
|
|
25370
|
-
const devices = [];
|
|
25371
|
-
for (const avdName of names) {
|
|
25372
|
-
const isRunning = runningAvdNames.has(avdName);
|
|
25373
|
-
if (!includeUnavailable && !isRunning)
|
|
25374
|
-
continue;
|
|
25375
|
-
if (isRunning)
|
|
25376
|
-
continue;
|
|
25377
|
-
const { api, deviceName } = await readAvdConfig(avdName);
|
|
25378
|
-
devices.push({
|
|
25379
|
-
id: avdName,
|
|
25380
|
-
name: deviceName ?? humanizeAvdName(avdName),
|
|
25381
|
-
osVersion: formatAndroidVersion(api ?? ""),
|
|
25382
|
-
platform: "android",
|
|
25383
|
-
kind: "emulator",
|
|
25384
|
-
state: "offline",
|
|
25385
|
-
model: avdName
|
|
25386
|
-
});
|
|
25387
|
-
}
|
|
25388
|
-
return devices;
|
|
25389
|
-
}
|
|
25390
|
-
async function listAndroidDevices(options = {}) {
|
|
25391
|
-
const includeUnavailable = options.includeUnavailable ?? true;
|
|
25392
|
-
const [adb, emulatorBin] = await Promise.all([resolveAdb(), resolveEmulator()]);
|
|
25393
|
-
const adbResult = adb ? await listAndroidAdbDevices(adb) : { physical: [], runningEmulators: [], runningAvdNames: new Set };
|
|
25394
|
-
const offlineAvds = emulatorBin != null ? await listAndroidAvds(emulatorBin, adbResult.runningAvdNames, includeUnavailable) : [];
|
|
25395
|
-
const physical = includeUnavailable ? adbResult.physical : adbResult.physical.filter((device) => device.state === "device");
|
|
25396
|
-
const emulators = [...adbResult.runningEmulators, ...offlineAvds].sort((a, b) => {
|
|
25397
|
-
const aOnline = a.state === "online" ? 0 : 1;
|
|
25398
|
-
const bOnline = b.state === "online" ? 0 : 1;
|
|
25399
|
-
if (aOnline !== bOnline)
|
|
25400
|
-
return aOnline - bOnline;
|
|
25401
|
-
return a.name.localeCompare(b.name);
|
|
25402
|
-
});
|
|
25403
|
-
return [...physical, ...emulators];
|
|
25404
|
-
}
|
|
25405
|
-
async function listDevices(platform, options = {}) {
|
|
25406
|
-
if (platform === "ios")
|
|
25407
|
-
return listIosDevices(options);
|
|
25408
|
-
return listAndroidDevices(options);
|
|
25409
|
-
}
|
|
25410
|
-
|
|
25411
25484
|
// src/interfaces/http/devices.ts
|
|
25412
25485
|
function createDevicesRoutes() {
|
|
25413
25486
|
const app = new Hono2;
|
|
@@ -57472,6 +57545,9 @@ function formatProviderHttpError(label, status, body) {
|
|
|
57472
57545
|
if (label === "OpenCode" && (body.includes("unknown variant `image_url`") || body.includes("No endpoints found that support image input") || body.includes("expected `text`"))) {
|
|
57473
57546
|
return `This OpenCode model does not accept screenshots (text-only). In Settings \u2192 Provider, set the default model to ${EXAMPLE_OPENCODE_VISION_MODEL} (or another vision-capable model). deepseek-v4-flash-free and big-pickle are text-only on Zen.`;
|
|
57474
57547
|
}
|
|
57548
|
+
if (label === "OpenCode" && (body.includes("output_config.format") || body.includes("BedrockException") && body.includes("Extra inputs are not permitted"))) {
|
|
57549
|
+
return "LiteLLM/Bedrock rejected JSON schema mode for this model (output_config.format). Restart the Yoqa runner and retry \u2014 LiteLLM vision now asks for JSON in the prompt only.";
|
|
57550
|
+
}
|
|
57475
57551
|
if (label === "OpenCode" && status >= 500 && (body.includes("Internal server error") || body.includes('"type":"error"'))) {
|
|
57476
57552
|
return `OpenCode Zen returned an internal error for this vision request. Many free models reject screenshots. In Settings \u2192 Provider, set the default model to ${EXAMPLE_OPENCODE_VISION_MODEL} (or another vision-capable model).`;
|
|
57477
57553
|
}
|
|
@@ -57505,6 +57581,16 @@ function mapProviderError(label, error40) {
|
|
|
57505
57581
|
}
|
|
57506
57582
|
throw new AgentProviderError(String(error40));
|
|
57507
57583
|
}
|
|
57584
|
+
function repairModelJsonText(text3) {
|
|
57585
|
+
const trimmed = text3.trim();
|
|
57586
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
57587
|
+
const candidate = fenced?.[1]?.trim() ?? trimmed;
|
|
57588
|
+
const start = candidate.indexOf("{");
|
|
57589
|
+
const end = candidate.lastIndexOf("}");
|
|
57590
|
+
if (start < 0 || end <= start)
|
|
57591
|
+
return trimmed;
|
|
57592
|
+
return candidate.slice(start, end + 1);
|
|
57593
|
+
}
|
|
57508
57594
|
async function completeWithAiSdk(input) {
|
|
57509
57595
|
const run = async (prompt) => {
|
|
57510
57596
|
try {
|
|
@@ -57513,6 +57599,7 @@ async function completeWithAiSdk(input) {
|
|
|
57513
57599
|
schema: input.schema,
|
|
57514
57600
|
system: input.system,
|
|
57515
57601
|
maxOutputTokens: VISION_MAX_TOKENS,
|
|
57602
|
+
experimental_repairText: async ({ text: text3 }) => repairModelJsonText(text3),
|
|
57516
57603
|
messages: [
|
|
57517
57604
|
{
|
|
57518
57605
|
role: "user",
|
|
@@ -57529,6 +57616,11 @@ async function completeWithAiSdk(input) {
|
|
|
57529
57616
|
});
|
|
57530
57617
|
return object3;
|
|
57531
57618
|
} catch (error40) {
|
|
57619
|
+
if (NoObjectGeneratedError.isInstance(error40) && error40.text?.trim()) {
|
|
57620
|
+
try {
|
|
57621
|
+
return input.schema.parse(JSON.parse(repairModelJsonText(error40.text)));
|
|
57622
|
+
} catch {}
|
|
57623
|
+
}
|
|
57532
57624
|
mapProviderError(input.label, error40);
|
|
57533
57625
|
}
|
|
57534
57626
|
};
|
|
@@ -57571,41 +57663,58 @@ function openCodeAuthPaths() {
|
|
|
57571
57663
|
paths.push(join9(homedir8(), ".local", "share", "opencode", "auth.json"));
|
|
57572
57664
|
return paths;
|
|
57573
57665
|
}
|
|
57574
|
-
|
|
57666
|
+
function apiKeyFromOpenCodeAuthEntry(value) {
|
|
57667
|
+
if (!value || typeof value !== "object")
|
|
57668
|
+
return null;
|
|
57669
|
+
const entry = value;
|
|
57670
|
+
for (const field of ["key", "apiKey", "token"]) {
|
|
57671
|
+
const candidate = entry[field];
|
|
57672
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
57673
|
+
return candidate.trim();
|
|
57674
|
+
}
|
|
57675
|
+
return null;
|
|
57676
|
+
}
|
|
57677
|
+
async function readOpenCodeCliAuthJson() {
|
|
57575
57678
|
for (const path of openCodeAuthPaths()) {
|
|
57576
57679
|
try {
|
|
57577
57680
|
const file2 = Bun.file(path);
|
|
57578
57681
|
if (!await file2.exists())
|
|
57579
57682
|
continue;
|
|
57580
57683
|
const json3 = await file2.json();
|
|
57581
|
-
if (!json3 || typeof json3 !== "object")
|
|
57684
|
+
if (!json3 || typeof json3 !== "object" || Array.isArray(json3))
|
|
57582
57685
|
continue;
|
|
57583
|
-
|
|
57584
|
-
const opencode = record2.opencode;
|
|
57585
|
-
if (opencode && typeof opencode === "object") {
|
|
57586
|
-
const entry = opencode;
|
|
57587
|
-
for (const field of ["key", "apiKey", "token"]) {
|
|
57588
|
-
const value = entry[field];
|
|
57589
|
-
if (typeof value === "string" && value.trim())
|
|
57590
|
-
return value.trim();
|
|
57591
|
-
}
|
|
57592
|
-
}
|
|
57593
|
-
for (const value of Object.values(record2)) {
|
|
57594
|
-
if (!value || typeof value !== "object")
|
|
57595
|
-
continue;
|
|
57596
|
-
const entry = value;
|
|
57597
|
-
const key = entry.key ?? entry.apiKey ?? entry.token;
|
|
57598
|
-
if (typeof key === "string" && key.trim()) {
|
|
57599
|
-
const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
|
|
57600
|
-
if (!type || type.includes("api") || type.includes("key") || type === "opencode") {
|
|
57601
|
-
return key.trim();
|
|
57602
|
-
}
|
|
57603
|
-
}
|
|
57604
|
-
}
|
|
57686
|
+
return json3;
|
|
57605
57687
|
} catch {}
|
|
57606
57688
|
}
|
|
57607
57689
|
return null;
|
|
57608
57690
|
}
|
|
57691
|
+
async function readOpenCodeCliAuthKeyForProvider(providerId) {
|
|
57692
|
+
const record2 = await readOpenCodeCliAuthJson();
|
|
57693
|
+
if (!record2)
|
|
57694
|
+
return null;
|
|
57695
|
+
return apiKeyFromOpenCodeAuthEntry(record2[providerId]);
|
|
57696
|
+
}
|
|
57697
|
+
async function readOpenCodeCliAuthKey() {
|
|
57698
|
+
const record2 = await readOpenCodeCliAuthJson();
|
|
57699
|
+
if (!record2)
|
|
57700
|
+
return null;
|
|
57701
|
+
const zen = apiKeyFromOpenCodeAuthEntry(record2.opencode);
|
|
57702
|
+
if (zen)
|
|
57703
|
+
return zen;
|
|
57704
|
+
for (const value of Object.values(record2)) {
|
|
57705
|
+
const key = apiKeyFromOpenCodeAuthEntry(value);
|
|
57706
|
+
if (!key)
|
|
57707
|
+
continue;
|
|
57708
|
+
if (!value || typeof value !== "object")
|
|
57709
|
+
continue;
|
|
57710
|
+
const entry = value;
|
|
57711
|
+
const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
|
|
57712
|
+
if (!type || type.includes("api") || type.includes("key") || type === "opencode") {
|
|
57713
|
+
return key;
|
|
57714
|
+
}
|
|
57715
|
+
}
|
|
57716
|
+
return null;
|
|
57717
|
+
}
|
|
57609
57718
|
async function resolveOpenAiCompatibleKey(auth) {
|
|
57610
57719
|
if (auth.apiKey?.trim())
|
|
57611
57720
|
return auth.apiKey.trim();
|
|
@@ -57648,18 +57757,34 @@ function withOpenCodeRequestHooks(opts) {
|
|
|
57648
57757
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
57649
57758
|
return async (input, init) => {
|
|
57650
57759
|
const headers = new Headers(init?.headers);
|
|
57651
|
-
if (opts.authHeaders
|
|
57652
|
-
|
|
57760
|
+
if (opts.authHeaders) {
|
|
57761
|
+
for (const [key, value] of Object.entries(opts.authHeaders)) {
|
|
57762
|
+
if (value)
|
|
57763
|
+
headers.set(key, value);
|
|
57764
|
+
}
|
|
57765
|
+
}
|
|
57766
|
+
if (opts.extraHeaders) {
|
|
57767
|
+
for (const [key, value] of Object.entries(opts.extraHeaders)) {
|
|
57768
|
+
if (value)
|
|
57769
|
+
headers.set(key, value);
|
|
57770
|
+
}
|
|
57653
57771
|
}
|
|
57654
|
-
|
|
57772
|
+
const rawBody = init?.body;
|
|
57773
|
+
const shouldRewriteBody = (opts.disableThinking || Boolean(opts.stripResponseFormat)) && typeof rawBody === "string";
|
|
57774
|
+
if (!shouldRewriteBody || typeof rawBody !== "string") {
|
|
57655
57775
|
return fetchImpl(input, { ...init, headers });
|
|
57656
57776
|
}
|
|
57657
57777
|
try {
|
|
57658
|
-
const parsed = JSON.parse(
|
|
57778
|
+
const parsed = JSON.parse(rawBody);
|
|
57659
57779
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
57660
57780
|
return fetchImpl(input, { ...init, headers });
|
|
57661
57781
|
}
|
|
57662
|
-
const
|
|
57782
|
+
const parsedBody = { ...parsed };
|
|
57783
|
+
if (opts.disableThinking) {
|
|
57784
|
+
parsedBody.thinking = { type: "disabled" };
|
|
57785
|
+
}
|
|
57786
|
+
const { response_format: _responseFormat, ...withoutFormat } = parsedBody;
|
|
57787
|
+
const body = opts.stripResponseFormat ? withoutFormat : parsedBody;
|
|
57663
57788
|
return fetchImpl(input, {
|
|
57664
57789
|
...init,
|
|
57665
57790
|
headers,
|
|
@@ -96163,12 +96288,144 @@ var openaiDriver = {
|
|
|
96163
96288
|
}
|
|
96164
96289
|
};
|
|
96165
96290
|
|
|
96291
|
+
// src/domains/providers/drivers/opencode-config.ts
|
|
96292
|
+
import { homedir as homedir10 } from "os";
|
|
96293
|
+
import { join as join13 } from "path";
|
|
96294
|
+
var OPENAI_COMPATIBLE_PACKAGES = new Set([
|
|
96295
|
+
"@ai-sdk/openai-compatible",
|
|
96296
|
+
"@ai-sdk/openai",
|
|
96297
|
+
"@ai-sdk/azure"
|
|
96298
|
+
]);
|
|
96299
|
+
function asRecord3(value) {
|
|
96300
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
96301
|
+
return null;
|
|
96302
|
+
return value;
|
|
96303
|
+
}
|
|
96304
|
+
function isOpenAiCompatibleNpm(npm) {
|
|
96305
|
+
const trimmed = npm.trim();
|
|
96306
|
+
if (!trimmed)
|
|
96307
|
+
return true;
|
|
96308
|
+
if (OPENAI_COMPATIBLE_PACKAGES.has(trimmed))
|
|
96309
|
+
return true;
|
|
96310
|
+
return trimmed.includes("openai-compatible");
|
|
96311
|
+
}
|
|
96312
|
+
function interpolateOpenCodeTemplate(value, env) {
|
|
96313
|
+
return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_full, name31) => {
|
|
96314
|
+
return env[name31]?.trim() || process.env[name31]?.trim() || "";
|
|
96315
|
+
}).trim();
|
|
96316
|
+
}
|
|
96317
|
+
function openCodeConfigPaths() {
|
|
96318
|
+
const paths = [];
|
|
96319
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
96320
|
+
if (xdg)
|
|
96321
|
+
paths.push(join13(xdg, "opencode", "opencode.json"));
|
|
96322
|
+
paths.push(join13(homedir10(), ".config", "opencode", "opencode.json"));
|
|
96323
|
+
return paths;
|
|
96324
|
+
}
|
|
96325
|
+
function parseOpenCodeCompatibleProvider(config2, providerId) {
|
|
96326
|
+
const root = asRecord3(config2);
|
|
96327
|
+
const providers2 = asRecord3(root?.provider);
|
|
96328
|
+
const entry = asRecord3(providers2?.[providerId]);
|
|
96329
|
+
if (!entry)
|
|
96330
|
+
return null;
|
|
96331
|
+
const npm = typeof entry.npm === "string" ? entry.npm : "";
|
|
96332
|
+
if (!isOpenAiCompatibleNpm(npm))
|
|
96333
|
+
return null;
|
|
96334
|
+
const options = asRecord3(entry.options);
|
|
96335
|
+
const baseURLRaw = typeof options?.baseURL === "string" ? options.baseURL.trim() : "";
|
|
96336
|
+
if (!baseURLRaw)
|
|
96337
|
+
return null;
|
|
96338
|
+
const apiKeyTemplate = typeof options?.apiKey === "string" ? options.apiKey : null;
|
|
96339
|
+
const headers = {};
|
|
96340
|
+
const headerBag = asRecord3(options?.headers);
|
|
96341
|
+
if (headerBag) {
|
|
96342
|
+
for (const [key, value] of Object.entries(headerBag)) {
|
|
96343
|
+
if (typeof value === "string" && value.trim())
|
|
96344
|
+
headers[key] = value;
|
|
96345
|
+
}
|
|
96346
|
+
}
|
|
96347
|
+
return {
|
|
96348
|
+
providerId,
|
|
96349
|
+
baseURL: baseURLRaw.replace(/\/$/, ""),
|
|
96350
|
+
apiKeyTemplate,
|
|
96351
|
+
headers
|
|
96352
|
+
};
|
|
96353
|
+
}
|
|
96354
|
+
function resolveOpenCodeCompatibleAuth(input) {
|
|
96355
|
+
const parsed = parseOpenCodeCompatibleProvider(input.config, input.providerId);
|
|
96356
|
+
const override = input.overrideBaseUrl?.trim().replace(/\/$/, "") || null;
|
|
96357
|
+
if (!parsed && !(override && input.providerId === "litellm"))
|
|
96358
|
+
return null;
|
|
96359
|
+
const baseURL = override || parsed?.baseURL || null;
|
|
96360
|
+
if (!baseURL)
|
|
96361
|
+
return null;
|
|
96362
|
+
const headers = {};
|
|
96363
|
+
if (parsed) {
|
|
96364
|
+
for (const [key, value] of Object.entries(parsed.headers)) {
|
|
96365
|
+
const interpolated = interpolateOpenCodeTemplate(value, input.env);
|
|
96366
|
+
if (interpolated)
|
|
96367
|
+
headers[key] = interpolated;
|
|
96368
|
+
}
|
|
96369
|
+
}
|
|
96370
|
+
const fromTemplate = parsed?.apiKeyTemplate ? interpolateOpenCodeTemplate(parsed.apiKeyTemplate, input.env) : "";
|
|
96371
|
+
const envKeyName = `${input.providerId.replace(/-/g, "_").toUpperCase()}_API_KEY`;
|
|
96372
|
+
const apiKey = fromTemplate || input.env[envKeyName]?.trim() || process.env[envKeyName]?.trim() || input.storedKey?.trim() || null;
|
|
96373
|
+
return { baseURL, apiKey, headers };
|
|
96374
|
+
}
|
|
96375
|
+
async function loadOpenCodeUserConfig() {
|
|
96376
|
+
for (const path of openCodeConfigPaths()) {
|
|
96377
|
+
try {
|
|
96378
|
+
const file2 = Bun.file(path);
|
|
96379
|
+
if (!await file2.exists())
|
|
96380
|
+
continue;
|
|
96381
|
+
return await file2.json();
|
|
96382
|
+
} catch {}
|
|
96383
|
+
}
|
|
96384
|
+
return null;
|
|
96385
|
+
}
|
|
96386
|
+
|
|
96166
96387
|
// src/domains/providers/drivers/opencode.ts
|
|
96167
|
-
var OPENCODE_FREE_MODEL_IDS = new Set(["big-pickle"]);
|
|
96168
96388
|
var OPENCODE_DEFAULT_VISION_MODEL = "mimo-v2.5-free";
|
|
96169
|
-
|
|
96170
|
-
|
|
96171
|
-
|
|
96389
|
+
var OPENCODE_PROVIDER_LABELS = {
|
|
96390
|
+
"amazon-bedrock": "Amazon Bedrock",
|
|
96391
|
+
anthropic: "Anthropic",
|
|
96392
|
+
azure: "Azure OpenAI",
|
|
96393
|
+
"github-copilot": "GitHub Copilot",
|
|
96394
|
+
google: "Google",
|
|
96395
|
+
"google-vertex": "Google Vertex AI",
|
|
96396
|
+
groq: "Groq",
|
|
96397
|
+
litellm: "LiteLLM",
|
|
96398
|
+
ollama: "Ollama",
|
|
96399
|
+
openai: "OpenAI",
|
|
96400
|
+
opencode: "OpenCode Zen",
|
|
96401
|
+
"opencode-go": "OpenCode Go",
|
|
96402
|
+
openrouter: "OpenRouter",
|
|
96403
|
+
xai: "xAI",
|
|
96404
|
+
zenmux: "ZenMux"
|
|
96405
|
+
};
|
|
96406
|
+
function humanizeOpenCodeProvider(providerId) {
|
|
96407
|
+
const known = OPENCODE_PROVIDER_LABELS[providerId];
|
|
96408
|
+
if (known)
|
|
96409
|
+
return known;
|
|
96410
|
+
return providerId.split(/[-_]/).filter(Boolean).map((part) => {
|
|
96411
|
+
if (part.toLowerCase() === "ai")
|
|
96412
|
+
return "AI";
|
|
96413
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
96414
|
+
}).join(" ");
|
|
96415
|
+
}
|
|
96416
|
+
function parseOpenCodeModelSlug(slug) {
|
|
96417
|
+
const trimmed = slug.trim();
|
|
96418
|
+
const sep = trimmed.indexOf("/");
|
|
96419
|
+
if (sep <= 0 || sep === trimmed.length - 1) {
|
|
96420
|
+
return { providerId: "opencode", modelId: trimmed };
|
|
96421
|
+
}
|
|
96422
|
+
return {
|
|
96423
|
+
providerId: trimmed.slice(0, sep),
|
|
96424
|
+
modelId: trimmed.slice(sep + 1)
|
|
96425
|
+
};
|
|
96426
|
+
}
|
|
96427
|
+
function isOpenCodeZenModel(slug) {
|
|
96428
|
+
return parseOpenCodeModelSlug(slug).providerId === "opencode";
|
|
96172
96429
|
}
|
|
96173
96430
|
function humanizeModelId(id) {
|
|
96174
96431
|
return id.split(/[-_]/).filter(Boolean).map((part) => {
|
|
@@ -96179,11 +96436,13 @@ function humanizeModelId(id) {
|
|
|
96179
96436
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
96180
96437
|
}).join(" ");
|
|
96181
96438
|
}
|
|
96182
|
-
function toOpenCodeModelEntry(
|
|
96439
|
+
function toOpenCodeModelEntry(slug) {
|
|
96440
|
+
const trimmed = slug.trim();
|
|
96441
|
+
const { providerId, modelId } = parseOpenCodeModelSlug(trimmed);
|
|
96183
96442
|
return {
|
|
96184
|
-
id,
|
|
96185
|
-
name: humanizeModelId(
|
|
96186
|
-
|
|
96443
|
+
id: trimmed,
|
|
96444
|
+
name: humanizeModelId(modelId),
|
|
96445
|
+
provider: humanizeOpenCodeProvider(providerId)
|
|
96187
96446
|
};
|
|
96188
96447
|
}
|
|
96189
96448
|
function openCodeServerAuthHeaders(password) {
|
|
@@ -96197,18 +96456,14 @@ function resolveOpenCodeServerPassword(env) {
|
|
|
96197
96456
|
return env.OPENCODE_SERVER_PASSWORD?.trim() || env.SERVER_PASSWORD?.trim() || process.env.OPENCODE_SERVER_PASSWORD?.trim() || process.env.SERVER_PASSWORD?.trim() || null;
|
|
96198
96457
|
}
|
|
96199
96458
|
function stripOpenCodeModelSlug(slug) {
|
|
96200
|
-
|
|
96201
|
-
const sep = trimmed.indexOf("/");
|
|
96202
|
-
if (sep <= 0 || sep === trimmed.length - 1)
|
|
96203
|
-
return trimmed;
|
|
96204
|
-
return trimmed.slice(sep + 1);
|
|
96459
|
+
return parseOpenCodeModelSlug(slug).modelId;
|
|
96205
96460
|
}
|
|
96206
96461
|
function parseModelsPayload(json3) {
|
|
96207
96462
|
const models = [];
|
|
96208
96463
|
if (json3 && typeof json3 === "object" && "data" in json3 && Array.isArray(json3.data)) {
|
|
96209
96464
|
for (const item of json3.data) {
|
|
96210
96465
|
if (item && typeof item === "object" && "id" in item && typeof item.id === "string") {
|
|
96211
|
-
models.push(toOpenCodeModelEntry(
|
|
96466
|
+
models.push(toOpenCodeModelEntry(item.id));
|
|
96212
96467
|
}
|
|
96213
96468
|
}
|
|
96214
96469
|
}
|
|
@@ -96237,44 +96492,84 @@ async function listOpenCodeModelsFromCli(binaryPath) {
|
|
|
96237
96492
|
const slug = line.trim();
|
|
96238
96493
|
if (!slug || /\s/.test(slug))
|
|
96239
96494
|
continue;
|
|
96240
|
-
|
|
96241
|
-
if (!id || seen.has(id))
|
|
96495
|
+
if (seen.has(slug))
|
|
96242
96496
|
continue;
|
|
96243
|
-
seen.add(
|
|
96244
|
-
models.push(toOpenCodeModelEntry(
|
|
96497
|
+
seen.add(slug);
|
|
96498
|
+
models.push(toOpenCodeModelEntry(slug));
|
|
96245
96499
|
}
|
|
96246
96500
|
return {
|
|
96247
96501
|
models,
|
|
96248
96502
|
detail: `${models.length} models from opencode CLI`
|
|
96249
96503
|
};
|
|
96250
96504
|
}
|
|
96505
|
+
async function resolveCompatibleOpenCodeVision(providerId, auth) {
|
|
96506
|
+
const config2 = await loadOpenCodeUserConfig();
|
|
96507
|
+
const storedKey = await readOpenCodeCliAuthKeyForProvider(providerId);
|
|
96508
|
+
return resolveOpenCodeCompatibleAuth({
|
|
96509
|
+
providerId,
|
|
96510
|
+
config: config2,
|
|
96511
|
+
env: auth.env,
|
|
96512
|
+
storedKey,
|
|
96513
|
+
overrideBaseUrl: auth.baseUrl
|
|
96514
|
+
});
|
|
96515
|
+
}
|
|
96251
96516
|
var opencodeDriver = {
|
|
96252
96517
|
kind: "opencode",
|
|
96253
96518
|
label: "OpenCode",
|
|
96254
|
-
description: "OpenCode Zen (OpenAI-compatible). Paste a Zen API key for
|
|
96519
|
+
description: "OpenCode Zen or LiteLLM (OpenAI-compatible). Paste a Zen API key for Zen; LiteLLM uses opencode.json.",
|
|
96255
96520
|
defaultBinary: "opencode",
|
|
96256
96521
|
authModes: ["cli", "api_key"],
|
|
96257
|
-
envHints: [
|
|
96258
|
-
|
|
96522
|
+
envHints: [
|
|
96523
|
+
"OPENCODE_API_KEY",
|
|
96524
|
+
"OPENCODE_ZEN_API_KEY",
|
|
96525
|
+
"OPENCODE_SERVER_PASSWORD",
|
|
96526
|
+
"LITELLM_API_KEY"
|
|
96527
|
+
],
|
|
96528
|
+
loginInstructions: "Paste a Zen API key from https://opencode.ai for Zen vision. LiteLLM uses ~/.config/opencode/opencode.json plus CLI auth or LITELLM_API_KEY. Local `opencode serve` is not OpenAI-compatible.",
|
|
96259
96529
|
capabilities: { vision: true },
|
|
96260
96530
|
vision: createSdkVisionPort({
|
|
96261
96531
|
label: "OpenCode",
|
|
96262
96532
|
defaultModel: OPENCODE_DEFAULT_VISION_MODEL,
|
|
96263
96533
|
createModel: async (auth, modelId) => {
|
|
96264
|
-
|
|
96265
|
-
|
|
96266
|
-
|
|
96534
|
+
if (isOpenCodeZenModel(modelId)) {
|
|
96535
|
+
const apiKey = await resolveOpenAiCompatibleKey(auth);
|
|
96536
|
+
if (!apiKey) {
|
|
96537
|
+
throw new AgentProviderError("OpenCode vision needs a Zen API key. Local `opencode serve` returns a web UI for /v1 (not chat completions). Paste a key from https://opencode.ai in Settings \u2192 Provider, or set OPENCODE_API_KEY.");
|
|
96538
|
+
}
|
|
96539
|
+
const baseURL = auth.baseUrl?.trim().replace(/\/$/, "") || "https://opencode.ai/zen/v1";
|
|
96540
|
+
return createOpenAI({
|
|
96541
|
+
apiKey,
|
|
96542
|
+
baseURL,
|
|
96543
|
+
name: "opencode",
|
|
96544
|
+
fetch: withOpenCodeRequestHooks({
|
|
96545
|
+
disableThinking: true,
|
|
96546
|
+
authHeaders: null
|
|
96547
|
+
})
|
|
96548
|
+
}).chat(stripOpenCodeModelSlug(modelId));
|
|
96549
|
+
}
|
|
96550
|
+
const { providerId } = parseOpenCodeModelSlug(modelId);
|
|
96551
|
+
const providerLabel = humanizeOpenCodeProvider(providerId);
|
|
96552
|
+
const compatible = await resolveCompatibleOpenCodeVision(providerId, auth);
|
|
96553
|
+
if (compatible?.apiKey) {
|
|
96554
|
+
return createOpenAI({
|
|
96555
|
+
apiKey: compatible.apiKey,
|
|
96556
|
+
baseURL: compatible.baseURL,
|
|
96557
|
+
name: providerId,
|
|
96558
|
+
fetch: withOpenCodeRequestHooks({
|
|
96559
|
+
disableThinking: false,
|
|
96560
|
+
stripResponseFormat: true,
|
|
96561
|
+
authHeaders: null,
|
|
96562
|
+
extraHeaders: Object.keys(compatible.headers).length > 0 ? compatible.headers : null
|
|
96563
|
+
})
|
|
96564
|
+
}).chat(stripOpenCodeModelSlug(modelId));
|
|
96267
96565
|
}
|
|
96268
|
-
|
|
96269
|
-
|
|
96270
|
-
|
|
96271
|
-
|
|
96272
|
-
|
|
96273
|
-
|
|
96274
|
-
|
|
96275
|
-
authHeaders: null
|
|
96276
|
-
})
|
|
96277
|
-
}).chat(modelId);
|
|
96566
|
+
if (compatible && !compatible.apiKey) {
|
|
96567
|
+
throw new AgentProviderError(`${providerLabel} vision needs an API key. OpenCode stores it in auth.json after login, or set ${providerId.replace(/-/g, "_").toUpperCase()}_API_KEY in Settings \u2192 Provider.`);
|
|
96568
|
+
}
|
|
96569
|
+
if (providerId === "litellm") {
|
|
96570
|
+
throw new AgentProviderError("Yoqa vision can use LiteLLM via OpenCode. Add provider.litellm.options.baseURL in ~/.config/opencode/opencode.json, then pick litellm/\u2026 in Settings \u2192 Provider.");
|
|
96571
|
+
}
|
|
96572
|
+
throw new AgentProviderError(`Yoqa vision uses OpenCode Zen or LiteLLM. "${modelId}" is a ${providerLabel} model. Pick an OpenCode Zen or LiteLLM model in Settings \u2192 Provider.`);
|
|
96278
96573
|
}
|
|
96279
96574
|
}),
|
|
96280
96575
|
async probe(binaryPath) {
|
|
@@ -96365,6 +96660,10 @@ var opencodeDriver = {
|
|
|
96365
96660
|
async listModels(input) {
|
|
96366
96661
|
const key = resolveOpenCodeKey(input);
|
|
96367
96662
|
const serverUrl = input.serverUrl?.trim() || null;
|
|
96663
|
+
const cli = await listOpenCodeModelsFromCli(input.binaryPath);
|
|
96664
|
+
if (cli.models.length > 0) {
|
|
96665
|
+
return { models: cli.models, message: cli.detail };
|
|
96666
|
+
}
|
|
96368
96667
|
if (serverUrl) {
|
|
96369
96668
|
try {
|
|
96370
96669
|
const password = resolveOpenCodeServerPassword(input.env);
|
|
@@ -96390,14 +96689,10 @@ var opencodeDriver = {
|
|
|
96390
96689
|
label: "OpenCode"
|
|
96391
96690
|
});
|
|
96392
96691
|
return {
|
|
96393
|
-
models: (result.models ?? []).map((id) => toOpenCodeModelEntry(
|
|
96692
|
+
models: (result.models ?? []).map((id) => toOpenCodeModelEntry(id)),
|
|
96394
96693
|
message: result.ok ? `${result.models?.length ?? 0} models available` : result.message
|
|
96395
96694
|
};
|
|
96396
96695
|
}
|
|
96397
|
-
const cli = await listOpenCodeModelsFromCli(input.binaryPath);
|
|
96398
|
-
if (cli.models.length > 0) {
|
|
96399
|
-
return { models: cli.models, message: cli.detail };
|
|
96400
|
-
}
|
|
96401
96696
|
return {
|
|
96402
96697
|
models: [],
|
|
96403
96698
|
message: "Add OPENCODE_API_KEY, a server URL, or install the opencode CLI to list models"
|
|
@@ -96470,10 +96765,10 @@ import {
|
|
|
96470
96765
|
randomBytes
|
|
96471
96766
|
} from "crypto";
|
|
96472
96767
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
96473
|
-
import { homedir as
|
|
96474
|
-
import { dirname as dirname3, join as
|
|
96475
|
-
var YOQA_ROOT5 =
|
|
96476
|
-
var KEY_PATH =
|
|
96768
|
+
import { homedir as homedir11 } from "os";
|
|
96769
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
96770
|
+
var YOQA_ROOT5 = join14(homedir11(), ".yoqa");
|
|
96771
|
+
var KEY_PATH = join14(YOQA_ROOT5, ".provider-key");
|
|
96477
96772
|
var ALGORITHM = "aes-256-gcm";
|
|
96478
96773
|
var IV_LENGTH = 12;
|
|
96479
96774
|
var KEY_LENGTH = 32;
|