@hasna/testers 0.0.6 → 0.0.8
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/cli/index.js +1424 -395
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/environments.d.ts +22 -0
- package/dist/db/environments.d.ts.map +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +422 -171
- package/dist/lib/assertions.d.ts +26 -0
- package/dist/lib/assertions.d.ts.map +1 -0
- package/dist/lib/browser-lightpanda.d.ts +43 -0
- package/dist/lib/browser-lightpanda.d.ts.map +1 -0
- package/dist/lib/browser.d.ts +9 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/ci.d.ts +2 -0
- package/dist/lib/ci.d.ts.map +1 -0
- package/dist/lib/openapi-import.d.ts +8 -0
- package/dist/lib/openapi-import.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +24 -0
- package/dist/lib/recorder.d.ts.map +1 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/visual-diff.d.ts +36 -0
- package/dist/lib/visual-diff.d.ts.map +1 -0
- package/dist/mcp/index.js +341 -12
- package/dist/server/index.js +335 -12
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
|
+
var __require = import.meta.require;
|
|
13
14
|
|
|
14
15
|
// src/types/index.ts
|
|
15
16
|
function projectFromRow(row) {
|
|
@@ -49,6 +50,7 @@ function scenarioFromRow(row) {
|
|
|
49
50
|
requiresAuth: row.requires_auth === 1,
|
|
50
51
|
authConfig: row.auth_config ? JSON.parse(row.auth_config) : null,
|
|
51
52
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
53
|
+
assertions: JSON.parse(row.assertions || "[]"),
|
|
52
54
|
version: row.version,
|
|
53
55
|
createdAt: row.created_at,
|
|
54
56
|
updatedAt: row.updated_at
|
|
@@ -68,7 +70,8 @@ function runFromRow(row) {
|
|
|
68
70
|
failed: row.failed,
|
|
69
71
|
startedAt: row.started_at,
|
|
70
72
|
finishedAt: row.finished_at,
|
|
71
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
73
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
74
|
+
isBaseline: row.is_baseline === 1
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
function resultFromRow(row) {
|
|
@@ -287,6 +290,7 @@ function resetDatabase() {
|
|
|
287
290
|
database.exec("DELETE FROM flows");
|
|
288
291
|
database.exec("DELETE FROM webhooks");
|
|
289
292
|
database.exec("DELETE FROM auth_presets");
|
|
293
|
+
database.exec("DELETE FROM environments");
|
|
290
294
|
database.exec("DELETE FROM schedules");
|
|
291
295
|
database.exec("DELETE FROM runs");
|
|
292
296
|
database.exec("DELETE FROM scenarios");
|
|
@@ -480,6 +484,24 @@ var init_database = __esm(() => {
|
|
|
480
484
|
CREATE INDEX IF NOT EXISTS idx_deps_scenario ON scenario_dependencies(scenario_id);
|
|
481
485
|
CREATE INDEX IF NOT EXISTS idx_deps_depends ON scenario_dependencies(depends_on);
|
|
482
486
|
CREATE INDEX IF NOT EXISTS idx_flows_project ON flows(project_id);
|
|
487
|
+
`,
|
|
488
|
+
`
|
|
489
|
+
ALTER TABLE scenarios ADD COLUMN assertions TEXT DEFAULT '[]';
|
|
490
|
+
`,
|
|
491
|
+
`
|
|
492
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
493
|
+
id TEXT PRIMARY KEY,
|
|
494
|
+
name TEXT NOT NULL UNIQUE,
|
|
495
|
+
url TEXT NOT NULL,
|
|
496
|
+
auth_preset_name TEXT,
|
|
497
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
498
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
499
|
+
metadata TEXT DEFAULT '{}',
|
|
500
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
501
|
+
);
|
|
502
|
+
`,
|
|
503
|
+
`
|
|
504
|
+
ALTER TABLE runs ADD COLUMN is_baseline INTEGER NOT NULL DEFAULT 0;
|
|
483
505
|
`
|
|
484
506
|
];
|
|
485
507
|
});
|
|
@@ -596,6 +618,10 @@ function updateRun(id, updates) {
|
|
|
596
618
|
sets.push("metadata = ?");
|
|
597
619
|
params.push(updates.metadata);
|
|
598
620
|
}
|
|
621
|
+
if (updates.is_baseline !== undefined) {
|
|
622
|
+
sets.push("is_baseline = ?");
|
|
623
|
+
params.push(updates.is_baseline);
|
|
624
|
+
}
|
|
599
625
|
if (sets.length === 0) {
|
|
600
626
|
return existing;
|
|
601
627
|
}
|
|
@@ -767,6 +793,168 @@ var init_flows = __esm(() => {
|
|
|
767
793
|
init_types();
|
|
768
794
|
});
|
|
769
795
|
|
|
796
|
+
// src/lib/browser-lightpanda.ts
|
|
797
|
+
var exports_browser_lightpanda = {};
|
|
798
|
+
__export(exports_browser_lightpanda, {
|
|
799
|
+
startLightpandaServer: () => startLightpandaServer,
|
|
800
|
+
launchLightpanda: () => launchLightpanda,
|
|
801
|
+
isLightpandaAvailable: () => isLightpandaAvailable,
|
|
802
|
+
installLightpanda: () => installLightpanda,
|
|
803
|
+
getLightpandaPage: () => getLightpandaPage,
|
|
804
|
+
closeLightpanda: () => closeLightpanda
|
|
805
|
+
});
|
|
806
|
+
import { chromium } from "playwright";
|
|
807
|
+
import { spawn } from "child_process";
|
|
808
|
+
function isLightpandaAvailable() {
|
|
809
|
+
try {
|
|
810
|
+
const possiblePaths = [
|
|
811
|
+
`${process.env["HOME"]}/.cache/lightpanda-node/lightpanda`,
|
|
812
|
+
process.env["LIGHTPANDA_EXECUTABLE_PATH"]
|
|
813
|
+
];
|
|
814
|
+
for (const p of possiblePaths) {
|
|
815
|
+
if (p) {
|
|
816
|
+
try {
|
|
817
|
+
const { existsSync: existsSync3 } = __require("fs");
|
|
818
|
+
if (existsSync3(p))
|
|
819
|
+
return true;
|
|
820
|
+
} catch {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const { execSync } = __require("child_process");
|
|
826
|
+
execSync("lightpanda --version", { stdio: "ignore", timeout: 5000 });
|
|
827
|
+
return true;
|
|
828
|
+
} catch {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function findLightpandaBinary() {
|
|
833
|
+
const envPath = process.env["LIGHTPANDA_EXECUTABLE_PATH"];
|
|
834
|
+
if (envPath)
|
|
835
|
+
return envPath;
|
|
836
|
+
const cachePath = `${process.env["HOME"]}/.cache/lightpanda-node/lightpanda`;
|
|
837
|
+
try {
|
|
838
|
+
const { existsSync: existsSync3 } = __require("fs");
|
|
839
|
+
if (existsSync3(cachePath))
|
|
840
|
+
return cachePath;
|
|
841
|
+
} catch {}
|
|
842
|
+
return "lightpanda";
|
|
843
|
+
}
|
|
844
|
+
async function startLightpandaServer(port) {
|
|
845
|
+
const binary = findLightpandaBinary();
|
|
846
|
+
const cdpPort = port ?? 9222 + Math.floor(Math.random() * 1000);
|
|
847
|
+
return new Promise((resolve, reject) => {
|
|
848
|
+
const proc = spawn(binary, ["serve", "--port", String(cdpPort)], {
|
|
849
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
850
|
+
});
|
|
851
|
+
let resolved = false;
|
|
852
|
+
const timeout = setTimeout(() => {
|
|
853
|
+
if (!resolved) {
|
|
854
|
+
resolved = true;
|
|
855
|
+
resolve({
|
|
856
|
+
process: proc,
|
|
857
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}, 5000);
|
|
861
|
+
proc.stdout?.on("data", (data) => {
|
|
862
|
+
const output = data.toString();
|
|
863
|
+
if (output.includes("127.0.0.1") || output.includes("listening") || output.includes("DevTools")) {
|
|
864
|
+
if (!resolved) {
|
|
865
|
+
resolved = true;
|
|
866
|
+
clearTimeout(timeout);
|
|
867
|
+
resolve({
|
|
868
|
+
process: proc,
|
|
869
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
proc.stderr?.on("data", (data) => {
|
|
875
|
+
const output = data.toString();
|
|
876
|
+
if (output.includes("127.0.0.1") || output.includes("listening")) {
|
|
877
|
+
if (!resolved) {
|
|
878
|
+
resolved = true;
|
|
879
|
+
clearTimeout(timeout);
|
|
880
|
+
resolve({
|
|
881
|
+
process: proc,
|
|
882
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
proc.on("error", (err) => {
|
|
888
|
+
clearTimeout(timeout);
|
|
889
|
+
if (!resolved) {
|
|
890
|
+
resolved = true;
|
|
891
|
+
reject(new BrowserError(`Failed to start Lightpanda: ${err.message}. ` + `Install it with: bun install @lightpanda/browser`));
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
proc.on("exit", (code) => {
|
|
895
|
+
if (!resolved) {
|
|
896
|
+
resolved = true;
|
|
897
|
+
clearTimeout(timeout);
|
|
898
|
+
reject(new BrowserError(`Lightpanda exited with code ${code}. ` + `Install it with: bun install @lightpanda/browser`));
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
async function launchLightpanda(_options) {
|
|
904
|
+
try {
|
|
905
|
+
const { process: proc, wsEndpoint } = await startLightpandaServer();
|
|
906
|
+
lightpandaProcess = proc;
|
|
907
|
+
const browser = await chromium.connectOverCDP(wsEndpoint);
|
|
908
|
+
return browser;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
911
|
+
throw new BrowserError(`Failed to launch Lightpanda: ${message}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async function getLightpandaPage(browser, options) {
|
|
915
|
+
try {
|
|
916
|
+
const contexts = browser.contexts();
|
|
917
|
+
const context = contexts.length > 0 ? contexts[0] : await browser.newContext({
|
|
918
|
+
viewport: options?.viewport ?? { width: 1280, height: 720 },
|
|
919
|
+
userAgent: options?.userAgent,
|
|
920
|
+
locale: options?.locale
|
|
921
|
+
});
|
|
922
|
+
const page = await context.newPage();
|
|
923
|
+
return page;
|
|
924
|
+
} catch (error) {
|
|
925
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
926
|
+
throw new BrowserError(`Failed to create Lightpanda page: ${message}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function closeLightpanda(browser) {
|
|
930
|
+
try {
|
|
931
|
+
await browser.close();
|
|
932
|
+
} catch {}
|
|
933
|
+
if (lightpandaProcess) {
|
|
934
|
+
try {
|
|
935
|
+
lightpandaProcess.kill("SIGTERM");
|
|
936
|
+
lightpandaProcess = null;
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function installLightpanda() {
|
|
941
|
+
const { execSync } = __require("child_process");
|
|
942
|
+
try {
|
|
943
|
+
execSync("bun install @lightpanda/browser", {
|
|
944
|
+
stdio: "inherit",
|
|
945
|
+
cwd: process.env["HOME"]
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
949
|
+
throw new BrowserError(`Failed to install Lightpanda: ${message}
|
|
950
|
+
` + `Try manually: bun install @lightpanda/browser`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
var lightpandaProcess = null;
|
|
954
|
+
var init_browser_lightpanda = __esm(() => {
|
|
955
|
+
init_types();
|
|
956
|
+
});
|
|
957
|
+
|
|
770
958
|
// src/index.ts
|
|
771
959
|
init_types();
|
|
772
960
|
init_database();
|
|
@@ -792,9 +980,9 @@ function createScenario(input) {
|
|
|
792
980
|
const short_id = nextShortId(input.projectId);
|
|
793
981
|
const timestamp = now();
|
|
794
982
|
db2.query(`
|
|
795
|
-
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, version, created_at, updated_at)
|
|
796
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
797
|
-
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, timestamp, timestamp);
|
|
983
|
+
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, version, created_at, updated_at)
|
|
984
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
985
|
+
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), timestamp, timestamp);
|
|
798
986
|
return getScenario(id);
|
|
799
987
|
}
|
|
800
988
|
function getScenario(id) {
|
|
@@ -912,6 +1100,10 @@ function updateScenario(id, input, version) {
|
|
|
912
1100
|
sets.push("metadata = ?");
|
|
913
1101
|
params.push(JSON.stringify(input.metadata));
|
|
914
1102
|
}
|
|
1103
|
+
if (input.assertions !== undefined) {
|
|
1104
|
+
sets.push("assertions = ?");
|
|
1105
|
+
params.push(JSON.stringify(input.assertions));
|
|
1106
|
+
}
|
|
915
1107
|
if (sets.length === 0) {
|
|
916
1108
|
return existing;
|
|
917
1109
|
}
|
|
@@ -1308,14 +1500,22 @@ function resolveModel(nameOrId) {
|
|
|
1308
1500
|
}
|
|
1309
1501
|
// src/lib/browser.ts
|
|
1310
1502
|
init_types();
|
|
1311
|
-
import { chromium } from "playwright";
|
|
1503
|
+
import { chromium as chromium2 } from "playwright";
|
|
1312
1504
|
import { execSync } from "child_process";
|
|
1313
1505
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
1314
1506
|
async function launchBrowser(options) {
|
|
1507
|
+
const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
|
|
1508
|
+
if (engine === "lightpanda") {
|
|
1509
|
+
const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
1510
|
+
if (!isLightpandaAvailable2()) {
|
|
1511
|
+
throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
|
|
1512
|
+
}
|
|
1513
|
+
return launchLightpanda2({ viewport: options?.viewport });
|
|
1514
|
+
}
|
|
1315
1515
|
const headless = options?.headless ?? true;
|
|
1316
1516
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
1317
1517
|
try {
|
|
1318
|
-
const browser = await
|
|
1518
|
+
const browser = await chromium2.launch({
|
|
1319
1519
|
headless,
|
|
1320
1520
|
args: [
|
|
1321
1521
|
`--window-size=${viewport.width},${viewport.height}`
|
|
@@ -1328,6 +1528,11 @@ async function launchBrowser(options) {
|
|
|
1328
1528
|
}
|
|
1329
1529
|
}
|
|
1330
1530
|
async function getPage(browser, options) {
|
|
1531
|
+
const engine = options?.engine ?? "playwright";
|
|
1532
|
+
if (engine === "lightpanda") {
|
|
1533
|
+
const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
1534
|
+
return getLightpandaPage2(browser, options);
|
|
1535
|
+
}
|
|
1331
1536
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
1332
1537
|
try {
|
|
1333
1538
|
const context = await browser.newContext({
|
|
@@ -1342,7 +1547,11 @@ async function getPage(browser, options) {
|
|
|
1342
1547
|
throw new BrowserError(`Failed to create page: ${message}`);
|
|
1343
1548
|
}
|
|
1344
1549
|
}
|
|
1345
|
-
async function closeBrowser(browser) {
|
|
1550
|
+
async function closeBrowser(browser, engine) {
|
|
1551
|
+
if (engine === "lightpanda") {
|
|
1552
|
+
const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
1553
|
+
return closeLightpanda2(browser);
|
|
1554
|
+
}
|
|
1346
1555
|
try {
|
|
1347
1556
|
await browser.close();
|
|
1348
1557
|
} catch (error) {
|
|
@@ -1356,26 +1565,29 @@ class BrowserPool {
|
|
|
1356
1565
|
maxSize;
|
|
1357
1566
|
headless;
|
|
1358
1567
|
viewport;
|
|
1568
|
+
engine;
|
|
1359
1569
|
constructor(size, options) {
|
|
1360
1570
|
this.maxSize = size;
|
|
1361
1571
|
this.headless = options?.headless ?? true;
|
|
1362
1572
|
this.viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
1573
|
+
this.engine = options?.engine ?? "playwright";
|
|
1363
1574
|
}
|
|
1364
1575
|
async acquire() {
|
|
1365
1576
|
const idle = this.pool.find((entry) => !entry.inUse);
|
|
1366
1577
|
if (idle) {
|
|
1367
1578
|
idle.inUse = true;
|
|
1368
|
-
const page = await getPage(idle.browser, { viewport: this.viewport });
|
|
1579
|
+
const page = await getPage(idle.browser, { viewport: this.viewport, engine: this.engine });
|
|
1369
1580
|
return { browser: idle.browser, page };
|
|
1370
1581
|
}
|
|
1371
1582
|
if (this.pool.length < this.maxSize) {
|
|
1372
1583
|
const browser = await launchBrowser({
|
|
1373
1584
|
headless: this.headless,
|
|
1374
|
-
viewport: this.viewport
|
|
1585
|
+
viewport: this.viewport,
|
|
1586
|
+
engine: this.engine
|
|
1375
1587
|
});
|
|
1376
1588
|
const entry = { browser, inUse: true };
|
|
1377
1589
|
this.pool.push(entry);
|
|
1378
|
-
const page = await getPage(browser, { viewport: this.viewport });
|
|
1590
|
+
const page = await getPage(browser, { viewport: this.viewport, engine: this.engine });
|
|
1379
1591
|
return { browser, page };
|
|
1380
1592
|
}
|
|
1381
1593
|
return new Promise((resolve, reject) => {
|
|
@@ -1384,7 +1596,7 @@ class BrowserPool {
|
|
|
1384
1596
|
if (available) {
|
|
1385
1597
|
clearInterval(interval);
|
|
1386
1598
|
available.inUse = true;
|
|
1387
|
-
getPage(available.browser, { viewport: this.viewport }).then((page) => resolve({ browser: available.browser, page })).catch(reject);
|
|
1599
|
+
getPage(available.browser, { viewport: this.viewport, engine: this.engine }).then((page) => resolve({ browser: available.browser, page })).catch(reject);
|
|
1388
1600
|
}
|
|
1389
1601
|
}, 50);
|
|
1390
1602
|
});
|
|
@@ -1401,7 +1613,11 @@ class BrowserPool {
|
|
|
1401
1613
|
this.pool.length = 0;
|
|
1402
1614
|
}
|
|
1403
1615
|
}
|
|
1404
|
-
async function installBrowser() {
|
|
1616
|
+
async function installBrowser(engine) {
|
|
1617
|
+
if (engine === "lightpanda") {
|
|
1618
|
+
const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
1619
|
+
return installLightpanda2();
|
|
1620
|
+
}
|
|
1405
1621
|
try {
|
|
1406
1622
|
execSync("bunx playwright install chromium", {
|
|
1407
1623
|
stdio: "inherit"
|
|
@@ -1411,6 +1627,10 @@ async function installBrowser() {
|
|
|
1411
1627
|
throw new BrowserError(`Failed to install browser: ${message}`);
|
|
1412
1628
|
}
|
|
1413
1629
|
}
|
|
1630
|
+
|
|
1631
|
+
// src/index.ts
|
|
1632
|
+
init_browser_lightpanda();
|
|
1633
|
+
|
|
1414
1634
|
// src/lib/screenshotter.ts
|
|
1415
1635
|
import { mkdirSync as mkdirSync2, existsSync as existsSync3, writeFileSync } from "fs";
|
|
1416
1636
|
import { join as join3 } from "path";
|
|
@@ -2292,6 +2512,160 @@ function createClient(apiKey) {
|
|
|
2292
2512
|
}
|
|
2293
2513
|
// src/lib/runner.ts
|
|
2294
2514
|
init_runs();
|
|
2515
|
+
|
|
2516
|
+
// src/lib/webhooks.ts
|
|
2517
|
+
init_database();
|
|
2518
|
+
function fromRow(row) {
|
|
2519
|
+
return {
|
|
2520
|
+
id: row.id,
|
|
2521
|
+
url: row.url,
|
|
2522
|
+
events: JSON.parse(row.events),
|
|
2523
|
+
projectId: row.project_id,
|
|
2524
|
+
secret: row.secret,
|
|
2525
|
+
active: row.active === 1,
|
|
2526
|
+
createdAt: row.created_at
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
function createWebhook(input) {
|
|
2530
|
+
const db2 = getDatabase();
|
|
2531
|
+
const id = uuid();
|
|
2532
|
+
const events = input.events ?? ["failed"];
|
|
2533
|
+
const secret = input.secret ?? crypto.randomUUID().replace(/-/g, "");
|
|
2534
|
+
db2.query(`
|
|
2535
|
+
INSERT INTO webhooks (id, url, events, project_id, secret, active, created_at)
|
|
2536
|
+
VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
2537
|
+
`).run(id, input.url, JSON.stringify(events), input.projectId ?? null, secret, now());
|
|
2538
|
+
return getWebhook(id);
|
|
2539
|
+
}
|
|
2540
|
+
function getWebhook(id) {
|
|
2541
|
+
const db2 = getDatabase();
|
|
2542
|
+
const row = db2.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
2543
|
+
if (!row) {
|
|
2544
|
+
const rows = db2.query("SELECT * FROM webhooks WHERE id LIKE ? || '%'").all(id);
|
|
2545
|
+
if (rows.length === 1)
|
|
2546
|
+
return fromRow(rows[0]);
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
return fromRow(row);
|
|
2550
|
+
}
|
|
2551
|
+
function listWebhooks(projectId) {
|
|
2552
|
+
const db2 = getDatabase();
|
|
2553
|
+
let query = "SELECT * FROM webhooks WHERE active = 1";
|
|
2554
|
+
const params = [];
|
|
2555
|
+
if (projectId) {
|
|
2556
|
+
query += " AND (project_id = ? OR project_id IS NULL)";
|
|
2557
|
+
params.push(projectId);
|
|
2558
|
+
}
|
|
2559
|
+
query += " ORDER BY created_at DESC";
|
|
2560
|
+
const rows = db2.query(query).all(...params);
|
|
2561
|
+
return rows.map(fromRow);
|
|
2562
|
+
}
|
|
2563
|
+
function deleteWebhook(id) {
|
|
2564
|
+
const db2 = getDatabase();
|
|
2565
|
+
const webhook = getWebhook(id);
|
|
2566
|
+
if (!webhook)
|
|
2567
|
+
return false;
|
|
2568
|
+
db2.query("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
function signPayload(body, secret) {
|
|
2572
|
+
const encoder = new TextEncoder;
|
|
2573
|
+
const key = encoder.encode(secret);
|
|
2574
|
+
const data = encoder.encode(body);
|
|
2575
|
+
let hash = 0;
|
|
2576
|
+
for (let i = 0;i < data.length; i++) {
|
|
2577
|
+
hash = (hash << 5) - hash + data[i] + (key[i % key.length] ?? 0) | 0;
|
|
2578
|
+
}
|
|
2579
|
+
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
2580
|
+
}
|
|
2581
|
+
function formatSlackPayload(payload) {
|
|
2582
|
+
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
2583
|
+
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
2584
|
+
return {
|
|
2585
|
+
attachments: [
|
|
2586
|
+
{
|
|
2587
|
+
color,
|
|
2588
|
+
blocks: [
|
|
2589
|
+
{
|
|
2590
|
+
type: "section",
|
|
2591
|
+
text: {
|
|
2592
|
+
type: "mrkdwn",
|
|
2593
|
+
text: `${status} *Test Run ${payload.run.status.toUpperCase()}*
|
|
2594
|
+
` + `URL: ${payload.run.url}
|
|
2595
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
2596
|
+
Schedule: ${payload.schedule.name}` : "")
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
]
|
|
2600
|
+
}
|
|
2601
|
+
]
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
async function dispatchWebhooks(event, run, schedule) {
|
|
2605
|
+
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
2606
|
+
const payload = {
|
|
2607
|
+
event,
|
|
2608
|
+
run: {
|
|
2609
|
+
id: run.id,
|
|
2610
|
+
url: run.url,
|
|
2611
|
+
status: run.status,
|
|
2612
|
+
passed: run.passed,
|
|
2613
|
+
failed: run.failed,
|
|
2614
|
+
total: run.total
|
|
2615
|
+
},
|
|
2616
|
+
schedule,
|
|
2617
|
+
timestamp: new Date().toISOString()
|
|
2618
|
+
};
|
|
2619
|
+
for (const webhook of webhooks) {
|
|
2620
|
+
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
2621
|
+
continue;
|
|
2622
|
+
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
2623
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
2624
|
+
const headers = {
|
|
2625
|
+
"Content-Type": "application/json"
|
|
2626
|
+
};
|
|
2627
|
+
if (webhook.secret) {
|
|
2628
|
+
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
2629
|
+
}
|
|
2630
|
+
try {
|
|
2631
|
+
const response = await fetch(webhook.url, {
|
|
2632
|
+
method: "POST",
|
|
2633
|
+
headers,
|
|
2634
|
+
body
|
|
2635
|
+
});
|
|
2636
|
+
if (!response.ok) {
|
|
2637
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
2638
|
+
await fetch(webhook.url, { method: "POST", headers, body });
|
|
2639
|
+
}
|
|
2640
|
+
} catch {}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
async function testWebhook(id) {
|
|
2644
|
+
const webhook = getWebhook(id);
|
|
2645
|
+
if (!webhook)
|
|
2646
|
+
return false;
|
|
2647
|
+
const testPayload = {
|
|
2648
|
+
event: "test",
|
|
2649
|
+
run: { id: "test-run", url: "http://localhost:3000", status: "passed", passed: 3, failed: 0, total: 3 },
|
|
2650
|
+
timestamp: new Date().toISOString()
|
|
2651
|
+
};
|
|
2652
|
+
try {
|
|
2653
|
+
const body = JSON.stringify(testPayload);
|
|
2654
|
+
const response = await fetch(webhook.url, {
|
|
2655
|
+
method: "POST",
|
|
2656
|
+
headers: {
|
|
2657
|
+
"Content-Type": "application/json",
|
|
2658
|
+
...webhook.secret ? { "X-Testers-Signature": signPayload(body, webhook.secret) } : {}
|
|
2659
|
+
},
|
|
2660
|
+
body
|
|
2661
|
+
});
|
|
2662
|
+
return response.ok;
|
|
2663
|
+
} catch {
|
|
2664
|
+
return false;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/lib/runner.ts
|
|
2295
2669
|
var eventHandler = null;
|
|
2296
2670
|
function onRunEvent(handler) {
|
|
2297
2671
|
eventHandler = handler;
|
|
@@ -2300,6 +2674,20 @@ function emit(event) {
|
|
|
2300
2674
|
if (eventHandler)
|
|
2301
2675
|
eventHandler(event);
|
|
2302
2676
|
}
|
|
2677
|
+
function withTimeout(promise, ms, label) {
|
|
2678
|
+
return new Promise((resolve, reject) => {
|
|
2679
|
+
const timer = setTimeout(() => {
|
|
2680
|
+
reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
|
|
2681
|
+
}, ms);
|
|
2682
|
+
promise.then((val) => {
|
|
2683
|
+
clearTimeout(timer);
|
|
2684
|
+
resolve(val);
|
|
2685
|
+
}, (err) => {
|
|
2686
|
+
clearTimeout(timer);
|
|
2687
|
+
reject(err);
|
|
2688
|
+
});
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2303
2691
|
async function runSingleScenario(scenario, runId, options) {
|
|
2304
2692
|
const config = loadConfig();
|
|
2305
2693
|
const model = resolveModel2(options.model ?? scenario.model ?? config.defaultModel);
|
|
@@ -2317,13 +2705,14 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
2317
2705
|
let browser = null;
|
|
2318
2706
|
let page = null;
|
|
2319
2707
|
try {
|
|
2320
|
-
browser = await launchBrowser({ headless: !(options.headed ?? false) });
|
|
2708
|
+
browser = await launchBrowser({ headless: !(options.headed ?? false), engine: options.engine });
|
|
2321
2709
|
page = await getPage(browser, {
|
|
2322
2710
|
viewport: config.browser.viewport
|
|
2323
2711
|
});
|
|
2324
2712
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
2325
|
-
|
|
2326
|
-
|
|
2713
|
+
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
2714
|
+
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
2715
|
+
const agentResult = await withTimeout(runAgentLoop({
|
|
2327
2716
|
client,
|
|
2328
2717
|
page,
|
|
2329
2718
|
scenario,
|
|
@@ -2344,7 +2733,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
2344
2733
|
stepNumber: stepEvent.stepNumber
|
|
2345
2734
|
});
|
|
2346
2735
|
}
|
|
2347
|
-
});
|
|
2736
|
+
}), scenarioTimeout, scenario.name);
|
|
2348
2737
|
for (const ss of agentResult.screenshots) {
|
|
2349
2738
|
createScreenshot({
|
|
2350
2739
|
resultId: result.id,
|
|
@@ -2381,7 +2770,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
2381
2770
|
return updatedResult;
|
|
2382
2771
|
} finally {
|
|
2383
2772
|
if (browser)
|
|
2384
|
-
await closeBrowser(browser);
|
|
2773
|
+
await closeBrowser(browser, options.engine);
|
|
2385
2774
|
}
|
|
2386
2775
|
}
|
|
2387
2776
|
async function runBatch(scenarios, options) {
|
|
@@ -2476,6 +2865,8 @@ async function runBatch(scenarios, options) {
|
|
|
2476
2865
|
finished_at: new Date().toISOString()
|
|
2477
2866
|
});
|
|
2478
2867
|
emit({ type: "run:complete", runId: run.id });
|
|
2868
|
+
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
2869
|
+
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
2479
2870
|
return { run: finalRun, results };
|
|
2480
2871
|
}
|
|
2481
2872
|
async function runByFilter(options) {
|
|
@@ -2561,6 +2952,9 @@ function startRunAsync(options) {
|
|
|
2561
2952
|
finished_at: new Date().toISOString()
|
|
2562
2953
|
});
|
|
2563
2954
|
emit({ type: "run:complete", runId: run.id });
|
|
2955
|
+
const asyncRun = getRun(run.id);
|
|
2956
|
+
if (asyncRun)
|
|
2957
|
+
dispatchWebhooks(asyncRun.status === "failed" ? "failed" : "completed", asyncRun).catch(() => {});
|
|
2564
2958
|
} catch (error) {
|
|
2565
2959
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2566
2960
|
updateRun(run.id, {
|
|
@@ -2568,6 +2962,9 @@ function startRunAsync(options) {
|
|
|
2568
2962
|
finished_at: new Date().toISOString()
|
|
2569
2963
|
});
|
|
2570
2964
|
emit({ type: "run:complete", runId: run.id, error: errorMsg });
|
|
2965
|
+
const failedRun = getRun(run.id);
|
|
2966
|
+
if (failedRun)
|
|
2967
|
+
dispatchWebhooks("failed", failedRun).catch(() => {});
|
|
2571
2968
|
}
|
|
2572
2969
|
})();
|
|
2573
2970
|
return { runId: run.id, scenarioCount: scenarios.length };
|
|
@@ -4104,7 +4501,7 @@ function listTemplateNames() {
|
|
|
4104
4501
|
}
|
|
4105
4502
|
// src/db/auth-presets.ts
|
|
4106
4503
|
init_database();
|
|
4107
|
-
function
|
|
4504
|
+
function fromRow2(row) {
|
|
4108
4505
|
return {
|
|
4109
4506
|
id: row.id,
|
|
4110
4507
|
name: row.name,
|
|
@@ -4128,12 +4525,12 @@ function createAuthPreset(input) {
|
|
|
4128
4525
|
function getAuthPreset(name) {
|
|
4129
4526
|
const db2 = getDatabase();
|
|
4130
4527
|
const row = db2.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
|
|
4131
|
-
return row ?
|
|
4528
|
+
return row ? fromRow2(row) : null;
|
|
4132
4529
|
}
|
|
4133
4530
|
function listAuthPresets() {
|
|
4134
4531
|
const db2 = getDatabase();
|
|
4135
4532
|
const rows = db2.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
|
|
4136
|
-
return rows.map(
|
|
4533
|
+
return rows.map(fromRow2);
|
|
4137
4534
|
}
|
|
4138
4535
|
function deleteAuthPreset(name) {
|
|
4139
4536
|
const db2 = getDatabase();
|
|
@@ -4602,157 +4999,6 @@ async function startWatcher(options) {
|
|
|
4602
4999
|
process.on("SIGTERM", cleanup);
|
|
4603
5000
|
await new Promise(() => {});
|
|
4604
5001
|
}
|
|
4605
|
-
// src/lib/webhooks.ts
|
|
4606
|
-
init_database();
|
|
4607
|
-
function fromRow2(row) {
|
|
4608
|
-
return {
|
|
4609
|
-
id: row.id,
|
|
4610
|
-
url: row.url,
|
|
4611
|
-
events: JSON.parse(row.events),
|
|
4612
|
-
projectId: row.project_id,
|
|
4613
|
-
secret: row.secret,
|
|
4614
|
-
active: row.active === 1,
|
|
4615
|
-
createdAt: row.created_at
|
|
4616
|
-
};
|
|
4617
|
-
}
|
|
4618
|
-
function createWebhook(input) {
|
|
4619
|
-
const db2 = getDatabase();
|
|
4620
|
-
const id = uuid();
|
|
4621
|
-
const events = input.events ?? ["failed"];
|
|
4622
|
-
const secret = input.secret ?? crypto.randomUUID().replace(/-/g, "");
|
|
4623
|
-
db2.query(`
|
|
4624
|
-
INSERT INTO webhooks (id, url, events, project_id, secret, active, created_at)
|
|
4625
|
-
VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
4626
|
-
`).run(id, input.url, JSON.stringify(events), input.projectId ?? null, secret, now());
|
|
4627
|
-
return getWebhook(id);
|
|
4628
|
-
}
|
|
4629
|
-
function getWebhook(id) {
|
|
4630
|
-
const db2 = getDatabase();
|
|
4631
|
-
const row = db2.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
4632
|
-
if (!row) {
|
|
4633
|
-
const rows = db2.query("SELECT * FROM webhooks WHERE id LIKE ? || '%'").all(id);
|
|
4634
|
-
if (rows.length === 1)
|
|
4635
|
-
return fromRow2(rows[0]);
|
|
4636
|
-
return null;
|
|
4637
|
-
}
|
|
4638
|
-
return fromRow2(row);
|
|
4639
|
-
}
|
|
4640
|
-
function listWebhooks(projectId) {
|
|
4641
|
-
const db2 = getDatabase();
|
|
4642
|
-
let query = "SELECT * FROM webhooks WHERE active = 1";
|
|
4643
|
-
const params = [];
|
|
4644
|
-
if (projectId) {
|
|
4645
|
-
query += " AND (project_id = ? OR project_id IS NULL)";
|
|
4646
|
-
params.push(projectId);
|
|
4647
|
-
}
|
|
4648
|
-
query += " ORDER BY created_at DESC";
|
|
4649
|
-
const rows = db2.query(query).all(...params);
|
|
4650
|
-
return rows.map(fromRow2);
|
|
4651
|
-
}
|
|
4652
|
-
function deleteWebhook(id) {
|
|
4653
|
-
const db2 = getDatabase();
|
|
4654
|
-
const webhook = getWebhook(id);
|
|
4655
|
-
if (!webhook)
|
|
4656
|
-
return false;
|
|
4657
|
-
db2.query("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
|
|
4658
|
-
return true;
|
|
4659
|
-
}
|
|
4660
|
-
function signPayload(body, secret) {
|
|
4661
|
-
const encoder = new TextEncoder;
|
|
4662
|
-
const key = encoder.encode(secret);
|
|
4663
|
-
const data = encoder.encode(body);
|
|
4664
|
-
let hash = 0;
|
|
4665
|
-
for (let i = 0;i < data.length; i++) {
|
|
4666
|
-
hash = (hash << 5) - hash + data[i] + (key[i % key.length] ?? 0) | 0;
|
|
4667
|
-
}
|
|
4668
|
-
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
4669
|
-
}
|
|
4670
|
-
function formatSlackPayload(payload) {
|
|
4671
|
-
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
4672
|
-
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
4673
|
-
return {
|
|
4674
|
-
attachments: [
|
|
4675
|
-
{
|
|
4676
|
-
color,
|
|
4677
|
-
blocks: [
|
|
4678
|
-
{
|
|
4679
|
-
type: "section",
|
|
4680
|
-
text: {
|
|
4681
|
-
type: "mrkdwn",
|
|
4682
|
-
text: `${status} *Test Run ${payload.run.status.toUpperCase()}*
|
|
4683
|
-
` + `URL: ${payload.run.url}
|
|
4684
|
-
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
4685
|
-
Schedule: ${payload.schedule.name}` : "")
|
|
4686
|
-
}
|
|
4687
|
-
}
|
|
4688
|
-
]
|
|
4689
|
-
}
|
|
4690
|
-
]
|
|
4691
|
-
};
|
|
4692
|
-
}
|
|
4693
|
-
async function dispatchWebhooks(event, run, schedule) {
|
|
4694
|
-
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
4695
|
-
const payload = {
|
|
4696
|
-
event,
|
|
4697
|
-
run: {
|
|
4698
|
-
id: run.id,
|
|
4699
|
-
url: run.url,
|
|
4700
|
-
status: run.status,
|
|
4701
|
-
passed: run.passed,
|
|
4702
|
-
failed: run.failed,
|
|
4703
|
-
total: run.total
|
|
4704
|
-
},
|
|
4705
|
-
schedule,
|
|
4706
|
-
timestamp: new Date().toISOString()
|
|
4707
|
-
};
|
|
4708
|
-
for (const webhook of webhooks) {
|
|
4709
|
-
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
4710
|
-
continue;
|
|
4711
|
-
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
4712
|
-
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
4713
|
-
const headers = {
|
|
4714
|
-
"Content-Type": "application/json"
|
|
4715
|
-
};
|
|
4716
|
-
if (webhook.secret) {
|
|
4717
|
-
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
4718
|
-
}
|
|
4719
|
-
try {
|
|
4720
|
-
const response = await fetch(webhook.url, {
|
|
4721
|
-
method: "POST",
|
|
4722
|
-
headers,
|
|
4723
|
-
body
|
|
4724
|
-
});
|
|
4725
|
-
if (!response.ok) {
|
|
4726
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
4727
|
-
await fetch(webhook.url, { method: "POST", headers, body });
|
|
4728
|
-
}
|
|
4729
|
-
} catch {}
|
|
4730
|
-
}
|
|
4731
|
-
}
|
|
4732
|
-
async function testWebhook(id) {
|
|
4733
|
-
const webhook = getWebhook(id);
|
|
4734
|
-
if (!webhook)
|
|
4735
|
-
return false;
|
|
4736
|
-
const testPayload = {
|
|
4737
|
-
event: "test",
|
|
4738
|
-
run: { id: "test-run", url: "http://localhost:3000", status: "passed", passed: 3, failed: 0, total: 3 },
|
|
4739
|
-
timestamp: new Date().toISOString()
|
|
4740
|
-
};
|
|
4741
|
-
try {
|
|
4742
|
-
const body = JSON.stringify(testPayload);
|
|
4743
|
-
const response = await fetch(webhook.url, {
|
|
4744
|
-
method: "POST",
|
|
4745
|
-
headers: {
|
|
4746
|
-
"Content-Type": "application/json",
|
|
4747
|
-
...webhook.secret ? { "X-Testers-Signature": signPayload(body, webhook.secret) } : {}
|
|
4748
|
-
},
|
|
4749
|
-
body
|
|
4750
|
-
});
|
|
4751
|
-
return response.ok;
|
|
4752
|
-
} catch {
|
|
4753
|
-
return false;
|
|
4754
|
-
}
|
|
4755
|
-
}
|
|
4756
5002
|
export {
|
|
4757
5003
|
writeScenarioMeta,
|
|
4758
5004
|
writeRunMeta,
|
|
@@ -4806,7 +5052,10 @@ export {
|
|
|
4806
5052
|
listFlows,
|
|
4807
5053
|
listAuthPresets,
|
|
4808
5054
|
listAgents,
|
|
5055
|
+
launchLightpanda,
|
|
4809
5056
|
launchBrowser,
|
|
5057
|
+
isLightpandaAvailable,
|
|
5058
|
+
installLightpanda,
|
|
4810
5059
|
installBrowser,
|
|
4811
5060
|
initProject,
|
|
4812
5061
|
importFromTodos,
|
|
@@ -4828,6 +5077,7 @@ export {
|
|
|
4828
5077
|
getProject,
|
|
4829
5078
|
getPage,
|
|
4830
5079
|
getNextRunTime,
|
|
5080
|
+
getLightpandaPage,
|
|
4831
5081
|
getFlow,
|
|
4832
5082
|
getExitCode,
|
|
4833
5083
|
getEnabledSchedules,
|
|
@@ -4877,6 +5127,7 @@ export {
|
|
|
4877
5127
|
createClient,
|
|
4878
5128
|
createAuthPreset,
|
|
4879
5129
|
connectToTodos,
|
|
5130
|
+
closeLightpanda,
|
|
4880
5131
|
closeDatabase,
|
|
4881
5132
|
closeBrowser,
|
|
4882
5133
|
checkBudget,
|