@browserstack/mcp-server 1.0.9 → 1.0.11
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/lib/device-cache.js +52 -0
- package/dist/tools/applive-utils/start-session.js +2 -2
- package/dist/tools/live-utils/desktop-filter.js +98 -0
- package/dist/tools/live-utils/mobile-filter.js +65 -0
- package/dist/tools/live-utils/start-session.js +64 -28
- package/dist/tools/live-utils/types.js +8 -0
- package/dist/tools/live-utils/version-resolver.js +48 -0
- package/dist/tools/live.js +87 -66
- package/package.json +1 -1
- package/dist/tools/applive-utils/constants.js +0 -82
- package/dist/tools/applive-utils/device-cache.js +0 -33
- package/dist/tools/applive-utils/types.js +0 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDevicesAndBrowsers = getDevicesAndBrowsers;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "combined_cache");
|
|
11
|
+
const CACHE_FILE = path_1.default.join(CACHE_DIR, "data.json");
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
13
|
+
const URLS = {
|
|
14
|
+
live: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
|
|
15
|
+
app_live: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Fetches and caches both BrowserStack datasets (live + app_live) with a shared TTL.
|
|
19
|
+
*/
|
|
20
|
+
async function getDevicesAndBrowsers(type) {
|
|
21
|
+
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
22
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
let cache = {};
|
|
25
|
+
if (fs_1.default.existsSync(CACHE_FILE)) {
|
|
26
|
+
const stats = fs_1.default.statSync(CACHE_FILE);
|
|
27
|
+
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
28
|
+
try {
|
|
29
|
+
cache = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
30
|
+
return cache[type];
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("Error parsing cache file:", error);
|
|
34
|
+
// Continue with fetching fresh data
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const [liveRes, appLiveRes] = await Promise.all([
|
|
39
|
+
fetch(URLS.live),
|
|
40
|
+
fetch(URLS.app_live),
|
|
41
|
+
]);
|
|
42
|
+
if (!liveRes.ok || !appLiveRes.ok) {
|
|
43
|
+
throw new Error(`Failed to fetch configuration from BrowserStack : live=${liveRes.statusText}, app_live=${appLiveRes.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
const [liveData, appLiveData] = await Promise.all([
|
|
46
|
+
liveRes.json(),
|
|
47
|
+
appLiveRes.json(),
|
|
48
|
+
]);
|
|
49
|
+
cache = { live: liveData, app_live: appLiveData };
|
|
50
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
51
|
+
return cache[type];
|
|
52
|
+
}
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.startSession = startSession;
|
|
7
7
|
const child_process_1 = __importDefault(require("child_process"));
|
|
8
8
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
-
const device_cache_1 = require("
|
|
9
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
10
10
|
const fuzzy_search_1 = require("./fuzzy-search");
|
|
11
11
|
const utils_1 = require("../../lib/utils");
|
|
12
12
|
const upload_app_1 = require("./upload-app");
|
|
@@ -19,7 +19,7 @@ const upload_app_1 = require("./upload-app");
|
|
|
19
19
|
async function startSession(args) {
|
|
20
20
|
const { appPath, desiredPlatform, desiredPhone } = args;
|
|
21
21
|
let { desiredPlatformVersion } = args;
|
|
22
|
-
const data = await (0, device_cache_1.
|
|
22
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("app_live");
|
|
23
23
|
const allDevices = data.mobile.flatMap((group) => group.devices.map((dev) => ({ ...dev, os: group.os })));
|
|
24
24
|
desiredPlatformVersion = resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
25
25
|
const filteredDevices = filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterDesktop = filterDesktop;
|
|
4
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
5
|
+
const version_resolver_1 = require("./version-resolver");
|
|
6
|
+
const fuzzy_1 = require("../../lib/fuzzy");
|
|
7
|
+
async function filterDesktop(args) {
|
|
8
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("live");
|
|
9
|
+
const allEntries = getAllDesktopEntries(data);
|
|
10
|
+
// Filter OS
|
|
11
|
+
const osList = filterByOS(allEntries, args.os);
|
|
12
|
+
// Filter browser
|
|
13
|
+
const browserList = filterByBrowser(osList, args.browser, args.os);
|
|
14
|
+
// Resolve OS version
|
|
15
|
+
const uniqueOSVersions = getUniqueOSVersions(browserList);
|
|
16
|
+
const chosenOS = resolveOSVersion(args.os, args.osVersion, uniqueOSVersions);
|
|
17
|
+
// Filter entries based on chosen OS version
|
|
18
|
+
const entriesForOS = filterByOSVersion(browserList, chosenOS);
|
|
19
|
+
// Resolve browser version
|
|
20
|
+
const browserVersions = entriesForOS.map((e) => e.browser_version);
|
|
21
|
+
const chosenBrowserVersion = (0, version_resolver_1.resolveVersion)(args.browserVersion, browserVersions);
|
|
22
|
+
// Find final entry
|
|
23
|
+
const finalEntry = entriesForOS.find((e) => e.browser_version === chosenBrowserVersion);
|
|
24
|
+
if (!finalEntry) {
|
|
25
|
+
throw new Error(`No entry for browser version "${args.browserVersion}".`);
|
|
26
|
+
}
|
|
27
|
+
// Add notes if versions were adjusted
|
|
28
|
+
addNotes(finalEntry, args, chosenOS, chosenBrowserVersion);
|
|
29
|
+
return finalEntry;
|
|
30
|
+
}
|
|
31
|
+
function getAllDesktopEntries(data) {
|
|
32
|
+
return data.desktop.flatMap((plat) => plat.browsers.map((b) => ({
|
|
33
|
+
os: plat.os,
|
|
34
|
+
os_version: plat.os_version,
|
|
35
|
+
browser: b.browser,
|
|
36
|
+
browser_version: b.browser_version,
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
function filterByOS(entries, os) {
|
|
40
|
+
const filtered = entries.filter((e) => e.os === os);
|
|
41
|
+
if (!filtered.length)
|
|
42
|
+
throw new Error(`No OS entries for "${os}".`);
|
|
43
|
+
return filtered;
|
|
44
|
+
}
|
|
45
|
+
function filterByBrowser(entries, browser, os) {
|
|
46
|
+
const filtered = entries.filter((e) => e.browser === browser);
|
|
47
|
+
if (!filtered.length)
|
|
48
|
+
throw new Error(`No browser "${browser}" on ${os}.`);
|
|
49
|
+
return filtered;
|
|
50
|
+
}
|
|
51
|
+
function getUniqueOSVersions(entries) {
|
|
52
|
+
return Array.from(new Set(entries.map((e) => e.os_version)));
|
|
53
|
+
}
|
|
54
|
+
function resolveOSVersion(os, requestedVersion, availableVersions) {
|
|
55
|
+
if (os === "OS X") {
|
|
56
|
+
return resolveMacOSVersion(requestedVersion, availableVersions);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// For Windows, use semantic versioning
|
|
60
|
+
return (0, version_resolver_1.resolveVersion)(requestedVersion, availableVersions);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function resolveMacOSVersion(requested, available) {
|
|
64
|
+
if (requested === "latest") {
|
|
65
|
+
return available[available.length - 1];
|
|
66
|
+
}
|
|
67
|
+
else if (requested === "oldest") {
|
|
68
|
+
return available[0];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Try fuzzy matching
|
|
72
|
+
const fuzzy = (0, fuzzy_1.customFuzzySearch)(available.map((v) => ({ os_version: v })), ["os_version"], requested, 1);
|
|
73
|
+
const matched = fuzzy.length ? fuzzy[0].os_version : requested;
|
|
74
|
+
// Fallback if not valid
|
|
75
|
+
return available.includes(matched) ? matched : available[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function filterByOSVersion(entries, osVersion) {
|
|
79
|
+
return entries.filter((e) => e.os_version === osVersion);
|
|
80
|
+
}
|
|
81
|
+
function addNotes(entry, args, resolvedOS, resolvedBrowser) {
|
|
82
|
+
if (args.osVersion !== resolvedOS &&
|
|
83
|
+
args.osVersion !== "latest" &&
|
|
84
|
+
args.osVersion !== "oldest") {
|
|
85
|
+
entry.notes = `Note: OS version ${args.osVersion} was not found. Using "${resolvedOS}" instead.`;
|
|
86
|
+
}
|
|
87
|
+
if (args.browserVersion !== resolvedBrowser &&
|
|
88
|
+
args.browserVersion !== "latest" &&
|
|
89
|
+
args.browserVersion !== "oldest") {
|
|
90
|
+
if (!entry.notes) {
|
|
91
|
+
entry.notes = `Note: `;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
entry.notes += ` `;
|
|
95
|
+
}
|
|
96
|
+
entry.notes += `Browser version ${args.browserVersion} was not found. Using "${resolvedBrowser}" instead.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterMobile = filterMobile;
|
|
4
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
5
|
+
const version_resolver_1 = require("./version-resolver");
|
|
6
|
+
const fuzzy_1 = require("../../lib/fuzzy");
|
|
7
|
+
// Extract all mobile entries from the data
|
|
8
|
+
function getAllMobileEntries(data) {
|
|
9
|
+
return data.mobile.flatMap((grp) => grp.devices.map((d) => ({
|
|
10
|
+
os: grp.os,
|
|
11
|
+
os_version: d.os_version,
|
|
12
|
+
display_name: d.display_name,
|
|
13
|
+
notes: "",
|
|
14
|
+
})));
|
|
15
|
+
}
|
|
16
|
+
// Filter entries by OS
|
|
17
|
+
function filterByOS(entries, os) {
|
|
18
|
+
const candidates = entries.filter((d) => d.os === os);
|
|
19
|
+
if (!candidates.length)
|
|
20
|
+
throw new Error(`No mobile OS entries for "${os}".`);
|
|
21
|
+
return candidates;
|
|
22
|
+
}
|
|
23
|
+
// Find matching device with exact match validation
|
|
24
|
+
function findMatchingDevice(entries, deviceName, os) {
|
|
25
|
+
const matches = (0, fuzzy_1.customFuzzySearch)(entries, ["display_name"], deviceName, 5);
|
|
26
|
+
if (!matches.length)
|
|
27
|
+
throw new Error(`No devices matching "${deviceName}" on ${os}.`);
|
|
28
|
+
const exact = matches.find((m) => m.display_name.toLowerCase() === deviceName.toLowerCase());
|
|
29
|
+
if (!exact) {
|
|
30
|
+
const names = matches.map((m) => m.display_name).join(", ");
|
|
31
|
+
throw new Error(`Alternative Device/Device's found : ${names}. Please Select one.`);
|
|
32
|
+
}
|
|
33
|
+
const result = entries.filter((d) => d.display_name === exact.display_name);
|
|
34
|
+
if (!result.length)
|
|
35
|
+
throw new Error(`No device "${exact.display_name}" on ${os}.`);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// Find the appropriate OS version
|
|
39
|
+
function findOSVersion(entries, requestedVersion) {
|
|
40
|
+
const versions = entries.map((d) => d.os_version);
|
|
41
|
+
const chosenVersion = (0, version_resolver_1.resolveVersion)(requestedVersion, versions);
|
|
42
|
+
const result = entries.filter((d) => d.os_version === chosenVersion);
|
|
43
|
+
if (!result.length)
|
|
44
|
+
throw new Error(`No entry for OS version "${requestedVersion}".`);
|
|
45
|
+
return { entries: result, chosenVersion };
|
|
46
|
+
}
|
|
47
|
+
// Create version note if needed
|
|
48
|
+
function createVersionNote(requestedVersion, actualVersion) {
|
|
49
|
+
if (actualVersion !== requestedVersion &&
|
|
50
|
+
requestedVersion !== "latest" &&
|
|
51
|
+
requestedVersion !== "oldest") {
|
|
52
|
+
return `Note: Os version ${requestedVersion} was not found. Using ${actualVersion} instead.`;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
async function filterMobile(args) {
|
|
57
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("live");
|
|
58
|
+
const allEntries = getAllMobileEntries(data);
|
|
59
|
+
const osCandidates = filterByOS(allEntries, args.os);
|
|
60
|
+
const deviceCandidates = findMatchingDevice(osCandidates, args.device, args.os);
|
|
61
|
+
const { entries: versionCandidates, chosenVersion } = findOSVersion(deviceCandidates, args.osVersion);
|
|
62
|
+
const final = versionCandidates[0];
|
|
63
|
+
final.notes = createVersionNote(args.osVersion, chosenVersion);
|
|
64
|
+
return final;
|
|
65
|
+
}
|
|
@@ -4,35 +4,77 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.startBrowserSession = startBrowserSession;
|
|
7
|
-
const utils_1 = require("../../lib/utils");
|
|
8
7
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
8
|
const child_process_1 = __importDefault(require("child_process"));
|
|
9
|
+
const desktop_filter_1 = require("./desktop-filter");
|
|
10
|
+
const mobile_filter_1 = require("./mobile-filter");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
const local_1 = require("../../lib/local");
|
|
13
|
+
/**
|
|
14
|
+
* Prepares local tunnel setup based on URL type
|
|
15
|
+
*/
|
|
16
|
+
async function prepareLocalTunnel(url) {
|
|
17
|
+
const isLocal = (0, local_1.isLocalURL)(url);
|
|
18
|
+
if (isLocal) {
|
|
19
|
+
await (0, local_1.ensureLocalBinarySetup)();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await (0, local_1.killExistingBrowserStackLocalProcesses)();
|
|
23
|
+
}
|
|
24
|
+
return isLocal;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Entrypoint: detects platformType & delegates.
|
|
28
|
+
*/
|
|
10
29
|
async function startBrowserSession(args) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
|
|
30
|
+
const entry = args.platformType === types_1.PlatformType.DESKTOP
|
|
31
|
+
? await (0, desktop_filter_1.filterDesktop)(args)
|
|
32
|
+
: await (0, mobile_filter_1.filterMobile)(args);
|
|
33
|
+
const isLocal = await prepareLocalTunnel(args.url);
|
|
34
|
+
const url = args.platformType === types_1.PlatformType.DESKTOP
|
|
35
|
+
? buildDesktopUrl(args, entry, isLocal)
|
|
36
|
+
: buildMobileUrl(args, entry, isLocal);
|
|
37
|
+
openBrowser(url);
|
|
38
|
+
return entry.notes ? `${url}, ${entry.notes}` : url;
|
|
39
|
+
}
|
|
40
|
+
function buildDesktopUrl(args, e, isLocal) {
|
|
21
41
|
const params = new URLSearchParams({
|
|
22
|
-
os:
|
|
23
|
-
os_version:
|
|
24
|
-
browser:
|
|
25
|
-
browser_version:
|
|
42
|
+
os: e.os,
|
|
43
|
+
os_version: e.os_version,
|
|
44
|
+
browser: e.browser,
|
|
45
|
+
browser_version: e.browser_version,
|
|
46
|
+
url: args.url,
|
|
26
47
|
scale_to_fit: "true",
|
|
27
|
-
url: sanitizedArgs.url,
|
|
28
48
|
resolution: "responsive-mode",
|
|
29
49
|
speed: "1",
|
|
30
|
-
local:
|
|
50
|
+
local: isLocal ? "true" : "false",
|
|
51
|
+
start: "true",
|
|
52
|
+
});
|
|
53
|
+
return `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
54
|
+
}
|
|
55
|
+
function buildMobileUrl(args, d, isLocal) {
|
|
56
|
+
const os_map = {
|
|
57
|
+
android: "Android",
|
|
58
|
+
ios: "iOS",
|
|
59
|
+
winphone: "Winphone",
|
|
60
|
+
};
|
|
61
|
+
const os = os_map[d.os] || d.os;
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
os: os,
|
|
64
|
+
os_version: d.os_version,
|
|
65
|
+
device: d.display_name,
|
|
66
|
+
device_browser: args.browser,
|
|
67
|
+
url: args.url,
|
|
68
|
+
scale_to_fit: "true",
|
|
69
|
+
speed: "1",
|
|
70
|
+
local: isLocal ? "true" : "false",
|
|
31
71
|
start: "true",
|
|
32
72
|
});
|
|
33
|
-
|
|
73
|
+
return `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
74
|
+
}
|
|
75
|
+
// ——— Open a browser window ———
|
|
76
|
+
function openBrowser(launchUrl) {
|
|
34
77
|
try {
|
|
35
|
-
// Use platform-specific commands with proper escaping
|
|
36
78
|
const command = process.platform === "darwin"
|
|
37
79
|
? ["open", launchUrl]
|
|
38
80
|
: process.platform === "win32"
|
|
@@ -43,16 +85,10 @@ async function startBrowserSession(args) {
|
|
|
43
85
|
stdio: "ignore",
|
|
44
86
|
detached: true,
|
|
45
87
|
});
|
|
46
|
-
|
|
47
|
-
child.on("error", (error) => {
|
|
48
|
-
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
49
|
-
});
|
|
50
|
-
// Unref the child process to allow the parent to exit
|
|
88
|
+
child.on("error", (err) => logger_1.default.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
|
|
51
89
|
child.unref();
|
|
52
|
-
return launchUrl;
|
|
53
90
|
}
|
|
54
|
-
catch (
|
|
55
|
-
logger_1.default.error(`Failed to
|
|
56
|
-
return launchUrl;
|
|
91
|
+
catch (err) {
|
|
92
|
+
logger_1.default.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
|
|
57
93
|
}
|
|
58
94
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlatformType = void 0;
|
|
4
|
+
var PlatformType;
|
|
5
|
+
(function (PlatformType) {
|
|
6
|
+
PlatformType["DESKTOP"] = "desktop";
|
|
7
|
+
PlatformType["MOBILE"] = "mobile";
|
|
8
|
+
})(PlatformType || (exports.PlatformType = PlatformType = {}));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveVersion = resolveVersion;
|
|
4
|
+
/**
|
|
5
|
+
* If req === "latest" or "oldest", returns max/min numeric (or lex)
|
|
6
|
+
* Else if exact match, returns that
|
|
7
|
+
* Else picks the numerically closest (or first)
|
|
8
|
+
*/
|
|
9
|
+
function resolveVersion(requested, available) {
|
|
10
|
+
// strip duplicates & sort
|
|
11
|
+
const uniq = Array.from(new Set(available));
|
|
12
|
+
// pick min/max
|
|
13
|
+
if (requested === "latest" || requested === "oldest") {
|
|
14
|
+
// try numeric
|
|
15
|
+
const nums = uniq
|
|
16
|
+
.map((v) => ({ v, n: parseFloat(v) }))
|
|
17
|
+
.filter((x) => !isNaN(x.n))
|
|
18
|
+
.sort((a, b) => a.n - b.n);
|
|
19
|
+
if (nums.length) {
|
|
20
|
+
return requested === "latest" ? nums[nums.length - 1].v : nums[0].v;
|
|
21
|
+
}
|
|
22
|
+
// fallback lex
|
|
23
|
+
const lex = uniq.slice().sort();
|
|
24
|
+
return requested === "latest" ? lex[lex.length - 1] : lex[0];
|
|
25
|
+
}
|
|
26
|
+
// exact?
|
|
27
|
+
if (uniq.includes(requested)) {
|
|
28
|
+
return requested;
|
|
29
|
+
}
|
|
30
|
+
// try closest numeric
|
|
31
|
+
const reqNum = parseFloat(requested);
|
|
32
|
+
const nums = uniq
|
|
33
|
+
.map((v) => ({ v, n: parseFloat(v) }))
|
|
34
|
+
.filter((x) => !isNaN(x.n));
|
|
35
|
+
if (!isNaN(reqNum) && nums.length) {
|
|
36
|
+
let best = nums[0], bestDiff = Math.abs(nums[0].n - reqNum);
|
|
37
|
+
for (const x of nums) {
|
|
38
|
+
const d = Math.abs(x.n - reqNum);
|
|
39
|
+
if (d < bestDiff) {
|
|
40
|
+
best = x;
|
|
41
|
+
bestDiff = d;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return best.v;
|
|
45
|
+
}
|
|
46
|
+
// final fallback
|
|
47
|
+
return uniq[0];
|
|
48
|
+
}
|
package/dist/tools/live.js
CHANGED
|
@@ -3,90 +3,111 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.startBrowserLiveSession = startBrowserLiveSession;
|
|
7
6
|
exports.default = addBrowserLiveTools;
|
|
8
7
|
const zod_1 = require("zod");
|
|
9
|
-
const start_session_1 = require("./live-utils/start-session");
|
|
10
8
|
const logger_1 = __importDefault(require("../logger"));
|
|
11
|
-
const
|
|
9
|
+
const start_session_1 = require("./live-utils/start-session");
|
|
10
|
+
const types_1 = require("./live-utils/types");
|
|
11
|
+
// Define the schema shape
|
|
12
|
+
const LiveArgsShape = {
|
|
13
|
+
platformType: zod_1.z
|
|
14
|
+
.nativeEnum(types_1.PlatformType)
|
|
15
|
+
.describe("Must be 'desktop' or 'mobile'"),
|
|
16
|
+
desiredURL: zod_1.z.string().url().describe("The URL to test"),
|
|
17
|
+
desiredOS: zod_1.z
|
|
18
|
+
.enum(["Windows", "OS X", "android", "ios", "winphone"])
|
|
19
|
+
.describe("Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')"),
|
|
20
|
+
desiredOSVersion: zod_1.z
|
|
21
|
+
.string()
|
|
22
|
+
.describe("The OS version must be specified as a version number (e.g., '10', '14.0') or as a keyword such as 'latest' or 'oldest'. Normalize variations like 'newest' or 'most recent' to 'latest', and terms like 'earliest' or 'first' to 'oldest'. For macOS, version names (e.g., 'Sequoia') must be used instead of numeric versions."),
|
|
23
|
+
desiredBrowser: zod_1.z
|
|
24
|
+
.enum(["chrome", "firefox", "safari", "edge", "ie"])
|
|
25
|
+
.describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"),
|
|
26
|
+
desiredBrowserVersion: zod_1.z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Browser version for desktop (e.g. '133.2', 'latest'). If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
|
|
30
|
+
desiredDevice: zod_1.z.string().optional().describe("Device name for mobile"),
|
|
31
|
+
};
|
|
32
|
+
const LiveArgsSchema = zod_1.z.object(LiveArgsShape);
|
|
12
33
|
/**
|
|
13
|
-
* Launches a
|
|
34
|
+
* Launches a desktop browser session
|
|
14
35
|
*/
|
|
15
|
-
async function
|
|
16
|
-
if (!args.desiredBrowser)
|
|
17
|
-
throw new Error("You must provide a desiredBrowser
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error("You must provide a desiredOS.");
|
|
24
|
-
}
|
|
25
|
-
if (!args.desiredOSVersion) {
|
|
26
|
-
throw new Error("You must provide a desiredOSVersion.");
|
|
27
|
-
}
|
|
28
|
-
if (!args.desiredBrowserVersion) {
|
|
29
|
-
throw new Error("You must provide a desiredBrowserVersion.");
|
|
30
|
-
}
|
|
31
|
-
// Validate URL format
|
|
32
|
-
try {
|
|
33
|
-
new URL(args.desiredURL);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
logger_1.default.error("Invalid URL format: %s", error);
|
|
37
|
-
throw new Error("The provided URL is invalid.");
|
|
38
|
-
}
|
|
39
|
-
const isLocal = (0, local_1.isLocalURL)(args.desiredURL);
|
|
40
|
-
if (isLocal) {
|
|
41
|
-
await (0, local_1.ensureLocalBinarySetup)();
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
await (0, local_1.killExistingBrowserStackLocalProcesses)();
|
|
45
|
-
}
|
|
46
|
-
const launchUrl = await (0, start_session_1.startBrowserSession)({
|
|
47
|
-
browser: args.desiredBrowser,
|
|
36
|
+
async function launchDesktopSession(args) {
|
|
37
|
+
if (!args.desiredBrowser)
|
|
38
|
+
throw new Error("You must provide a desiredBrowser");
|
|
39
|
+
if (!args.desiredBrowserVersion)
|
|
40
|
+
throw new Error("You must provide a desiredBrowserVersion");
|
|
41
|
+
return (0, start_session_1.startBrowserSession)({
|
|
42
|
+
platformType: types_1.PlatformType.DESKTOP,
|
|
43
|
+
url: args.desiredURL,
|
|
48
44
|
os: args.desiredOS,
|
|
49
45
|
osVersion: args.desiredOSVersion,
|
|
50
|
-
|
|
46
|
+
browser: args.desiredBrowser,
|
|
51
47
|
browserVersion: args.desiredBrowserVersion,
|
|
52
|
-
isLocal,
|
|
53
48
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Launches a mobile browser session
|
|
52
|
+
*/
|
|
53
|
+
async function launchMobileSession(args) {
|
|
54
|
+
if (!args.desiredDevice)
|
|
55
|
+
throw new Error("You must provide a desiredDevice");
|
|
56
|
+
return (0, start_session_1.startBrowserSession)({
|
|
57
|
+
platformType: types_1.PlatformType.MOBILE,
|
|
58
|
+
browser: args.desiredBrowser,
|
|
59
|
+
url: args.desiredURL,
|
|
60
|
+
os: args.desiredOS,
|
|
61
|
+
osVersion: args.desiredOSVersion,
|
|
62
|
+
device: args.desiredDevice,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Handles the core logic for running a browser session
|
|
67
|
+
*/
|
|
68
|
+
async function runBrowserSession(rawArgs) {
|
|
69
|
+
// Validate and narrow
|
|
70
|
+
const args = LiveArgsSchema.parse(rawArgs);
|
|
71
|
+
try {
|
|
72
|
+
// Branch desktop vs mobile and delegate
|
|
73
|
+
const launchUrl = args.platformType === types_1.PlatformType.DESKTOP
|
|
74
|
+
? await launchDesktopSession(args)
|
|
75
|
+
: await launchMobileSession(args);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `✅ Session started. If it didn't open automatically, visit:\n${launchUrl}`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
logger_1.default.error("Live session failed: %s", err);
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: `❌ Failed to start session: ${err.message || err}`,
|
|
92
|
+
isError: true,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
62
98
|
}
|
|
63
99
|
function addBrowserLiveTools(server) {
|
|
64
|
-
server.tool("runBrowserLiveSession", "
|
|
65
|
-
desiredBrowser: zod_1.z
|
|
66
|
-
.enum(["Chrome", "IE", "Firefox", "Safari", "Edge"])
|
|
67
|
-
.describe("The browser to run the test on. Example: 'Chrome', 'IE', 'Safari', 'Edge'. Always ask the user for the browser they want to use, do not assume it."),
|
|
68
|
-
desiredOSVersion: zod_1.z
|
|
69
|
-
.string()
|
|
70
|
-
.describe("The OS version to run the browser on. Example: '10' for Windows, '12' for macOS, '14' for iOS"),
|
|
71
|
-
desiredOS: zod_1.z
|
|
72
|
-
.enum(["Windows", "OS X"])
|
|
73
|
-
.describe("The operating system to run the browser on. Example: 'Windows', 'OS X'"),
|
|
74
|
-
desiredURL: zod_1.z
|
|
75
|
-
.string()
|
|
76
|
-
.describe("The URL of the website to test. This can be a local URL (e.g., http://localhost:3000) or a public URL. Always ask the user for the URL, do not assume it."),
|
|
77
|
-
desiredBrowserVersion: zod_1.z
|
|
78
|
-
.string()
|
|
79
|
-
.describe("The version of the browser to use. Example: 133.0, 134.0, 87.0"),
|
|
80
|
-
}, async (args) => {
|
|
100
|
+
server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape, async (args) => {
|
|
81
101
|
try {
|
|
82
|
-
|
|
102
|
+
const result = await runBrowserSession(args);
|
|
103
|
+
return result;
|
|
83
104
|
}
|
|
84
105
|
catch (error) {
|
|
85
106
|
return {
|
|
86
107
|
content: [
|
|
87
108
|
{
|
|
88
109
|
type: "text",
|
|
89
|
-
text: `Failed to start a browser live session. Error: ${error}
|
|
110
|
+
text: `Failed to start a browser live session. Error: ${error}`,
|
|
90
111
|
isError: true,
|
|
91
112
|
},
|
|
92
113
|
],
|
package/package.json
CHANGED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_LIVE_CONFIGURATIONS = void 0;
|
|
4
|
-
exports.SUPPORTED_LIVE_CONFIGURATIONS = {
|
|
5
|
-
android: {
|
|
6
|
-
chrome: {
|
|
7
|
-
version: "100",
|
|
8
|
-
},
|
|
9
|
-
firefox: {
|
|
10
|
-
version: "100",
|
|
11
|
-
},
|
|
12
|
-
safari: {
|
|
13
|
-
version: "100",
|
|
14
|
-
},
|
|
15
|
-
"samsung browser": {
|
|
16
|
-
version: "100",
|
|
17
|
-
},
|
|
18
|
-
"internet-explorer": {
|
|
19
|
-
version: "100",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
ios: {
|
|
23
|
-
chrome: {
|
|
24
|
-
version: "100",
|
|
25
|
-
},
|
|
26
|
-
firefox: {
|
|
27
|
-
version: "100",
|
|
28
|
-
},
|
|
29
|
-
edge: {
|
|
30
|
-
version: "100",
|
|
31
|
-
},
|
|
32
|
-
safari: {
|
|
33
|
-
version: "100",
|
|
34
|
-
},
|
|
35
|
-
"samsung browser": {
|
|
36
|
-
version: "100",
|
|
37
|
-
},
|
|
38
|
-
"internet-explorer": {
|
|
39
|
-
version: "100",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
windows: {
|
|
43
|
-
chrome: {
|
|
44
|
-
version: "100",
|
|
45
|
-
},
|
|
46
|
-
firefox: {
|
|
47
|
-
version: "100",
|
|
48
|
-
},
|
|
49
|
-
edge: {
|
|
50
|
-
version: "100",
|
|
51
|
-
},
|
|
52
|
-
safari: {
|
|
53
|
-
version: "100",
|
|
54
|
-
},
|
|
55
|
-
"samsung browser": {
|
|
56
|
-
version: "100",
|
|
57
|
-
},
|
|
58
|
-
"internet-explorer": {
|
|
59
|
-
version: "100",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
macos: {
|
|
63
|
-
chrome: {
|
|
64
|
-
version: "100",
|
|
65
|
-
},
|
|
66
|
-
firefox: {
|
|
67
|
-
version: "100",
|
|
68
|
-
},
|
|
69
|
-
edge: {
|
|
70
|
-
version: "100",
|
|
71
|
-
},
|
|
72
|
-
safari: {
|
|
73
|
-
version: "100",
|
|
74
|
-
},
|
|
75
|
-
"samsung browser": {
|
|
76
|
-
version: "100",
|
|
77
|
-
},
|
|
78
|
-
"internet-explorer": {
|
|
79
|
-
version: "100",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getAppLiveData = getAppLiveData;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "app_live_cache");
|
|
11
|
-
const CACHE_FILE = path_1.default.join(CACHE_DIR, "app_live.json");
|
|
12
|
-
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
13
|
-
/**
|
|
14
|
-
* Fetches and caches the App Live devices JSON with a 1-day TTL.
|
|
15
|
-
*/
|
|
16
|
-
async function getAppLiveData() {
|
|
17
|
-
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
18
|
-
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
if (fs_1.default.existsSync(CACHE_FILE)) {
|
|
21
|
-
const stats = fs_1.default.statSync(CACHE_FILE);
|
|
22
|
-
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
23
|
-
return JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const response = await fetch("https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json");
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
throw new Error(`Failed to fetch app live list: ${response.statusText}`);
|
|
29
|
-
}
|
|
30
|
-
const data = await response.json();
|
|
31
|
-
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(data), "utf8");
|
|
32
|
-
return data;
|
|
33
|
-
}
|