@oya-ai/cli 1.0.107 → 1.0.111
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/README.md +42 -15
- package/dist/index.js +928 -429
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,26 +1,127 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/args.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var CliError = class extends Error {
|
|
8
|
+
/** usage, invalid_json, unreachable, no_api_key, no_browser, partial_failure, and the like. */
|
|
9
|
+
code;
|
|
10
|
+
/** A second line with the next step, when there is one. */
|
|
11
|
+
hint;
|
|
12
|
+
/** Already printed by the command (a partial failure's own lines), so not printed again. */
|
|
13
|
+
shown;
|
|
14
|
+
/** Records the message, code, hint and whether it was shown. */
|
|
15
|
+
constructor(message, code, { hint: hint2, shown: shown2 = false } = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "CliError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.hint = hint2;
|
|
20
|
+
this.shown = shown2;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var usage = (message, hint2) => new CliError(message, "usage", { hint: hint2 });
|
|
24
|
+
var invalidJson = (message, hint2) => new CliError(message, "invalid_json", { hint: hint2 });
|
|
25
|
+
|
|
26
|
+
// src/args.ts
|
|
27
|
+
var SWITCHES = /* @__PURE__ */ new Set([
|
|
28
|
+
"all",
|
|
29
|
+
"debug",
|
|
30
|
+
"dry-run",
|
|
31
|
+
"force",
|
|
32
|
+
"governed",
|
|
33
|
+
"help",
|
|
34
|
+
"json",
|
|
35
|
+
"live",
|
|
36
|
+
"preview",
|
|
37
|
+
"replace",
|
|
38
|
+
"version"
|
|
39
|
+
]);
|
|
40
|
+
var VALUE_FLAGS = {
|
|
41
|
+
after: "--after 120",
|
|
42
|
+
"budget-usd": "--budget-usd 5",
|
|
43
|
+
config: "--config oya-install.json",
|
|
44
|
+
format: "--format playwright",
|
|
45
|
+
geo: "--geo us",
|
|
46
|
+
id: "--id oya-k3f9",
|
|
47
|
+
"idempotency-key": "--idempotency-key checkout-42",
|
|
48
|
+
key: "--key oya_...",
|
|
49
|
+
label: "--label ci",
|
|
50
|
+
locale: "--locale en-US",
|
|
51
|
+
max: "--max 3",
|
|
52
|
+
name: "--name shopper",
|
|
53
|
+
out: "--out cookies.json",
|
|
54
|
+
persona: "--persona auto",
|
|
55
|
+
platform: "--platform MacIntel",
|
|
56
|
+
policy: `--policy '{"allowedHosts":["example.com"]}'`,
|
|
57
|
+
priority: "--priority normal",
|
|
58
|
+
provider: "--provider cdp",
|
|
59
|
+
"queue-ms": "--queue-ms 30000",
|
|
60
|
+
role: "--role operator",
|
|
61
|
+
tz: "--tz Europe/Berlin",
|
|
62
|
+
url: "--url http://127.0.0.1:3100",
|
|
63
|
+
"ws-url": "--ws-url ws://127.0.0.1:9222"
|
|
64
|
+
};
|
|
65
|
+
function parse(argv2) {
|
|
66
|
+
const [command = "help", ...rest] = argv2;
|
|
67
|
+
const args = [];
|
|
68
|
+
const flags = {};
|
|
8
69
|
for (let i = 0; i < rest.length; i++) {
|
|
9
|
-
if (!rest[i].startsWith("--"))
|
|
10
|
-
else if (takeFlag(rest[i].slice("--".length), rest[i + 1],
|
|
70
|
+
if (!rest[i].startsWith("--")) args.push(rest[i]);
|
|
71
|
+
else if (takeFlag(rest[i].slice("--".length), rest[i + 1], flags)) i++;
|
|
72
|
+
}
|
|
73
|
+
return { command, args, flags };
|
|
74
|
+
}
|
|
75
|
+
function takeFlag(token2, next, flags) {
|
|
76
|
+
const [name, inline] = splitOnce(token2);
|
|
77
|
+
if (SWITCHES.has(name) && inline !== void 0) throw usage(`--${name} takes no value.`);
|
|
78
|
+
flags[name] = SWITCHES.has(name) || valueOf(name, inline, next);
|
|
79
|
+
return !SWITCHES.has(name) && inline === void 0;
|
|
80
|
+
}
|
|
81
|
+
function valueOf(name, inline, next) {
|
|
82
|
+
if (!Object.hasOwn(VALUE_FLAGS, name)) throw usage(`Unknown flag --${name}. Run oya help for the flags.`);
|
|
83
|
+
if (inline !== void 0) return inline;
|
|
84
|
+
if (next === void 0 || next.startsWith("--"))
|
|
85
|
+
throw usage(`--${name} needs a value, such as ${VALUE_FLAGS[name]}.`);
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
function splitOnce(token2) {
|
|
89
|
+
const at = token2.indexOf("=");
|
|
90
|
+
return at < 0 ? [token2, void 0] : [token2.slice(0, at), token2.slice(at + 1)];
|
|
91
|
+
}
|
|
92
|
+
var flagStr = (flags, name) => typeof flags[name] === "string" ? flags[name] : void 0;
|
|
93
|
+
function flagNum(flags, name) {
|
|
94
|
+
const raw = flagStr(flags, name);
|
|
95
|
+
if (raw === void 0) return void 0;
|
|
96
|
+
if (raw.trim() === "" || !Number.isFinite(Number(raw))) throw usage(`--${name} must be a number, not "${raw}".`);
|
|
97
|
+
return Number(raw);
|
|
98
|
+
}
|
|
99
|
+
function flagCount(flags, name, { allowNone = false } = {}) {
|
|
100
|
+
const raw = flagStr(flags, name);
|
|
101
|
+
if (raw === void 0) return void 0;
|
|
102
|
+
if (allowNone && raw === "none") return null;
|
|
103
|
+
if (/^[1-9]\d*$/.test(raw)) return Number(raw);
|
|
104
|
+
throw usage(`--${name} must be a whole number of 1 or more${allowNone ? ", or none" : ""}, not "${raw}".`);
|
|
105
|
+
}
|
|
106
|
+
function parseJson(text, subject, hint2, verb = "is") {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(text);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
throw invalidJson(`${subject} ${verb} not valid JSON: ${err.message}.`, hint2);
|
|
11
111
|
}
|
|
12
|
-
return { command: command2, args: args2, flags: flags2 };
|
|
13
112
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
113
|
+
function readJsonFile(file) {
|
|
114
|
+
let text;
|
|
115
|
+
try {
|
|
116
|
+
text = readFileSync(file, "utf8");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw usage(`${file} could not be read: ${err.code ?? err.message}.`);
|
|
119
|
+
}
|
|
120
|
+
return parseJson(text, file);
|
|
18
121
|
}
|
|
19
|
-
var flagStr = (flags2, name) => typeof flags2[name] === "string" ? flags2[name] : void 0;
|
|
20
|
-
var flagNum = (flags2, name) => flagStr(flags2, name) === void 0 ? void 0 : Number(flagStr(flags2, name));
|
|
21
122
|
|
|
22
123
|
// src/install.ts
|
|
23
|
-
import {
|
|
124
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
24
125
|
import { join as join5 } from "path";
|
|
25
126
|
|
|
26
127
|
// src/prompt/style.ts
|
|
@@ -78,9 +179,9 @@ function steps(total) {
|
|
|
78
179
|
}
|
|
79
180
|
function step(title, hint2) {
|
|
80
181
|
counter.number += 1;
|
|
81
|
-
const
|
|
182
|
+
const shown2 = counter.total ? style.grey(`${counter.number}/${counter.total}`) : "";
|
|
82
183
|
stdout2.write(`
|
|
83
|
-
${style.cyan("\u25C6")} ${style.bold(title)} ${
|
|
184
|
+
${style.cyan("\u25C6")} ${style.bold(title)} ${shown2}
|
|
84
185
|
`);
|
|
85
186
|
if (hint2) stdout2.write(`${style.grey(" " + hint2)}
|
|
86
187
|
`);
|
|
@@ -132,6 +233,11 @@ var masked = false;
|
|
|
132
233
|
var pending = [];
|
|
133
234
|
var waiter = null;
|
|
134
235
|
var ended = false;
|
|
236
|
+
var linesRead = 0;
|
|
237
|
+
function readLine(line) {
|
|
238
|
+
linesRead++;
|
|
239
|
+
deliver(line);
|
|
240
|
+
}
|
|
135
241
|
function deliver(line) {
|
|
136
242
|
if (waiter) {
|
|
137
243
|
const w = waiter;
|
|
@@ -153,7 +259,7 @@ function maskEcho(line) {
|
|
|
153
259
|
function iface() {
|
|
154
260
|
if (rl) return rl;
|
|
155
261
|
rl = createInterface({ input: stdin, output: stdout3, terminal: !!stdin.isTTY });
|
|
156
|
-
rl.on("line",
|
|
262
|
+
rl.on("line", readLine);
|
|
157
263
|
rl.on("close", close);
|
|
158
264
|
maskEcho(rl);
|
|
159
265
|
return rl;
|
|
@@ -170,6 +276,9 @@ function nextLine() {
|
|
|
170
276
|
function setMasked(on) {
|
|
171
277
|
masked = on;
|
|
172
278
|
}
|
|
279
|
+
function answered() {
|
|
280
|
+
return !!stdin.isTTY || linesRead > 0;
|
|
281
|
+
}
|
|
173
282
|
function inputExhausted() {
|
|
174
283
|
return ended && !pending.length;
|
|
175
284
|
}
|
|
@@ -401,27 +510,27 @@ var Status = {
|
|
|
401
510
|
};
|
|
402
511
|
|
|
403
512
|
// src/install/shell.ts
|
|
404
|
-
function settle(cmd,
|
|
405
|
-
return (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${
|
|
513
|
+
function settle(cmd, args, resolve, reject) {
|
|
514
|
+
return (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
|
|
406
515
|
}
|
|
407
|
-
function run(cmd,
|
|
516
|
+
function run(cmd, args, opts = { cwd: process.cwd() }) {
|
|
408
517
|
return new Promise((resolve, reject) => {
|
|
409
|
-
const child = spawn(cmd,
|
|
518
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, stdio: opts.quiet ? "ignore" : "inherit" });
|
|
410
519
|
child.on("error", reject);
|
|
411
|
-
child.on("close", settle(cmd,
|
|
520
|
+
child.on("close", settle(cmd, args, resolve, reject));
|
|
412
521
|
});
|
|
413
522
|
}
|
|
414
|
-
function pipeTo(cmd,
|
|
523
|
+
function pipeTo(cmd, args, stdin4, cwd) {
|
|
415
524
|
return new Promise((resolve, reject) => {
|
|
416
|
-
const child = spawn(cmd,
|
|
525
|
+
const child = spawn(cmd, args, { cwd, stdio: ["pipe", "inherit", "inherit"] });
|
|
417
526
|
child.on("error", reject);
|
|
418
|
-
child.on("close", settle(cmd,
|
|
527
|
+
child.on("close", settle(cmd, args, resolve, reject));
|
|
419
528
|
child.stdin.end(stdin4);
|
|
420
529
|
});
|
|
421
530
|
}
|
|
422
|
-
async function capture(cmd,
|
|
531
|
+
async function capture(cmd, args, cwd = process.cwd()) {
|
|
423
532
|
return new Promise((resolve) => {
|
|
424
|
-
const child = spawn(cmd,
|
|
533
|
+
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
425
534
|
const out2 = [];
|
|
426
535
|
child.stdout.on("data", (d) => out2.push(String(d)));
|
|
427
536
|
child.on("error", () => resolve(null));
|
|
@@ -429,8 +538,8 @@ async function capture(cmd, args2, cwd = process.cwd()) {
|
|
|
429
538
|
});
|
|
430
539
|
}
|
|
431
540
|
var has = async (cmd) => await capture(cmd, ["--version"]) !== null;
|
|
432
|
-
function atLeast(
|
|
433
|
-
const m = /(\d+)\.(\d+)/.exec(
|
|
541
|
+
function atLeast(version2, major, minor) {
|
|
542
|
+
const m = /(\d+)\.(\d+)/.exec(version2 || "");
|
|
434
543
|
if (!m) return false;
|
|
435
544
|
return Number(m[1]) > major || Number(m[1]) === major && Number(m[2]) >= minor;
|
|
436
545
|
}
|
|
@@ -459,14 +568,19 @@ function findRepoRoot(from = process.cwd()) {
|
|
|
459
568
|
}
|
|
460
569
|
return null;
|
|
461
570
|
}
|
|
462
|
-
|
|
571
|
+
var REPO_URL = "https://github.com/OyadotAI/oya-browser.git";
|
|
572
|
+
async function locateRepo(dryRun = false) {
|
|
463
573
|
const found = findRepoRoot();
|
|
464
574
|
if (found) return found;
|
|
465
575
|
console.log("\nThis does not look like an oya-browser checkout.");
|
|
466
576
|
const where = await ask("Clone it to:", join(process.cwd(), "oya-browser"));
|
|
577
|
+
if (dryRun) return wouldClone(where);
|
|
467
578
|
if (existsSync(where)) throw new Error(`${where} already exists`);
|
|
468
|
-
|
|
469
|
-
|
|
579
|
+
await run("git", ["clone", "--depth", "1", REPO_URL, where], { cwd: process.cwd() });
|
|
580
|
+
return where;
|
|
581
|
+
}
|
|
582
|
+
function wouldClone(where) {
|
|
583
|
+
console.log(`--dry-run: would clone ${REPO_URL} to ${where}`);
|
|
470
584
|
return where;
|
|
471
585
|
}
|
|
472
586
|
|
|
@@ -759,11 +873,11 @@ function composeCheck(compose) {
|
|
|
759
873
|
return { ok, label: "docker compose \u2265 2.24", detail: compose || "not found", fatal: true };
|
|
760
874
|
}
|
|
761
875
|
async function dockerChecks() {
|
|
762
|
-
const
|
|
876
|
+
const version2 = await capture("docker", ["--version"]);
|
|
763
877
|
const daemon = await capture("docker", ["info", "--format", "{{.ID}}"]);
|
|
764
878
|
const compose = await capture("docker", ["compose", "version", "--short"]);
|
|
765
879
|
return [
|
|
766
|
-
{ ok: !!
|
|
880
|
+
{ ok: !!version2, label: "docker", detail: version2 || "not found", fatal: true },
|
|
767
881
|
{ ok: !!daemon, label: "docker daemon reachable", detail: daemon ? "ok" : "not running", fatal: true },
|
|
768
882
|
composeCheck(compose)
|
|
769
883
|
];
|
|
@@ -773,10 +887,10 @@ function nodeCheck() {
|
|
|
773
887
|
return { ok, label: "node \u2265 22.13 (node:sqlite)", detail: process.versions.node, fatal: true };
|
|
774
888
|
}
|
|
775
889
|
async function kubeChecks() {
|
|
776
|
-
const
|
|
890
|
+
const version2 = await capture("kubectl", ["version", "--client=true", "-o", "json"]);
|
|
777
891
|
const cluster = await capture("kubectl", ["get", "namespace", "kube-system", "-o", "name"]);
|
|
778
892
|
return [
|
|
779
|
-
{ ok: !!
|
|
893
|
+
{ ok: !!version2, label: "kubectl", detail: version2 ? "ok" : "not found", fatal: true },
|
|
780
894
|
{ ok: !!cluster, label: "cluster reachable", detail: cluster || "kubectl cannot reach a cluster", fatal: true }
|
|
781
895
|
];
|
|
782
896
|
}
|
|
@@ -811,7 +925,7 @@ async function preflight(root, a) {
|
|
|
811
925
|
}
|
|
812
926
|
|
|
813
927
|
// src/install/env-file.ts
|
|
814
|
-
import { existsSync as existsSync3, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
928
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
|
|
815
929
|
var SECRET_KEYS = /* @__PURE__ */ new Set([
|
|
816
930
|
"OYA_PROFILE_SECRET",
|
|
817
931
|
"OYA_OPERATOR_TOKEN",
|
|
@@ -864,7 +978,7 @@ var SECTIONS = [
|
|
|
864
978
|
function readEnv(path) {
|
|
865
979
|
if (!existsSync3(path)) return {};
|
|
866
980
|
const out2 = {};
|
|
867
|
-
for (const line of
|
|
981
|
+
for (const line of readFileSync2(path, "utf8").split("\n")) {
|
|
868
982
|
const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
|
|
869
983
|
if (m) out2[m[1]] = m[2];
|
|
870
984
|
}
|
|
@@ -1191,9 +1305,9 @@ function printDone(publicUrl, apiKey) {
|
|
|
1191
1305
|
|
|
1192
1306
|
// src/install.ts
|
|
1193
1307
|
var ANSWERS_FILE = "oya-install.json";
|
|
1194
|
-
async function gather(configPath2) {
|
|
1195
|
-
const root = await locateRepo();
|
|
1196
|
-
const preset = configPath2 ?
|
|
1308
|
+
async function gather(configPath2, dryRun) {
|
|
1309
|
+
const root = await locateRepo(dryRun);
|
|
1310
|
+
const preset = configPath2 ? readJsonFile(configPath2) : {};
|
|
1197
1311
|
banner("Oya Browser, self-host install", root);
|
|
1198
1312
|
if (configPath2) note(`replaying ${configPath2}`);
|
|
1199
1313
|
steps(configPath2 ? 0 : INTERVIEW_STEPS);
|
|
@@ -1219,8 +1333,8 @@ async function guardKek(existing, answers, root, dryRun) {
|
|
|
1219
1333
|
const reused = await resolveKekConflict(volume, root);
|
|
1220
1334
|
if (reused) existing.OYA_PROFILE_SECRET = reused;
|
|
1221
1335
|
}
|
|
1222
|
-
async function plan(
|
|
1223
|
-
const interviewed = await gather(typeof
|
|
1336
|
+
async function plan(flags, dryRun) {
|
|
1337
|
+
const interviewed = await gather(typeof flags.config === "string" ? flags.config : null, dryRun);
|
|
1224
1338
|
const { root, answers, secrets } = interviewed;
|
|
1225
1339
|
await checkMachine(root, answers, dryRun);
|
|
1226
1340
|
warnLocalDatabase(answers, secrets.DATABASE_URL);
|
|
@@ -1252,9 +1366,9 @@ async function apply2(p) {
|
|
|
1252
1366
|
await waitReady(p.answers.publicUrl);
|
|
1253
1367
|
printDone(p.answers.publicUrl, p.env.apiKey);
|
|
1254
1368
|
}
|
|
1255
|
-
async function cmdInstall(
|
|
1256
|
-
const dryRun =
|
|
1257
|
-
const p = await plan(
|
|
1369
|
+
async function cmdInstall(flags) {
|
|
1370
|
+
const dryRun = flags["dry-run"] === true;
|
|
1371
|
+
const p = await plan(flags, dryRun);
|
|
1258
1372
|
printPlan(p.answers, p.envPath);
|
|
1259
1373
|
if (dryRun) return printDryRun(p.env.values);
|
|
1260
1374
|
await apply2(p);
|
|
@@ -1267,9 +1381,21 @@ import { join as join6, dirname as dirname2 } from "path";
|
|
|
1267
1381
|
|
|
1268
1382
|
// src/constants.ts
|
|
1269
1383
|
var ARGV_SKIP = 2;
|
|
1384
|
+
var KEY_TAIL = 4;
|
|
1385
|
+
var MIN_KEY_FOR_TAIL = 12;
|
|
1386
|
+
var LOGIN_TIMEOUT_MS = 6e4;
|
|
1387
|
+
var ExitCode = {
|
|
1388
|
+
/** Done. */
|
|
1389
|
+
DONE: 0,
|
|
1390
|
+
/** The request failed: the server or the browser refused it, or no answer came. */
|
|
1391
|
+
FAILED: 1,
|
|
1392
|
+
/** The command could not be run as typed; nothing was sent. */
|
|
1393
|
+
USAGE: 2
|
|
1394
|
+
};
|
|
1270
1395
|
var JSON_INDENT2 = 2;
|
|
1271
1396
|
var DEFAULT_BASE_URL = "https://oyabrowser.com";
|
|
1272
1397
|
var CONFIG_FILE_MODE = 384;
|
|
1398
|
+
var PRIVATE_FILE_MODE = 384;
|
|
1273
1399
|
var LsColumns = {
|
|
1274
1400
|
/** Provider name. */
|
|
1275
1401
|
PROVIDER: 14,
|
|
@@ -1290,6 +1416,10 @@ var PERSONA_NAME_WIDTH = 20;
|
|
|
1290
1416
|
var Status2 = {
|
|
1291
1417
|
/** The API key was rejected. */
|
|
1292
1418
|
UNAUTHORIZED: 401,
|
|
1419
|
+
/** A role refusal, or the auth layer's answer for an unknown key. */
|
|
1420
|
+
FORBIDDEN: 403,
|
|
1421
|
+
/** An endpoint another browser already holds. */
|
|
1422
|
+
CONFLICT: 409,
|
|
1293
1423
|
/** A quota or a persona concurrency cap. */
|
|
1294
1424
|
TOO_MANY_REQUESTS: 429
|
|
1295
1425
|
};
|
|
@@ -1310,65 +1440,65 @@ function save(values) {
|
|
|
1310
1440
|
chmodSync2(FILE, CONFIG_FILE_MODE);
|
|
1311
1441
|
}
|
|
1312
1442
|
var configPath = FILE;
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
return {
|
|
1316
|
-
|
|
1317
|
-
baseUrl: (process.env.OYA_BASE_URL || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "")
|
|
1318
|
-
};
|
|
1443
|
+
function first(flag, env2, saved, names) {
|
|
1444
|
+
if (flag) return { value: flag, from: names[0] };
|
|
1445
|
+
if (env2) return { value: env2, from: names[1] };
|
|
1446
|
+
return saved ? { value: saved, from: names[2] } : null;
|
|
1319
1447
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
if (!minted.ok || !minted.body.key) throw new Error(minted.body.error || "Could not create an API key");
|
|
1334
|
-
console.log(' Created a new API key labelled "CLI".');
|
|
1335
|
-
return minted.body.key;
|
|
1336
|
-
}
|
|
1337
|
-
function chooseMethod() {
|
|
1338
|
-
return choose("How do you want to authenticate?", [
|
|
1339
|
-
{ id: "paste", label: "Paste an API key", note: "from the dashboard, or a self-hosted key" },
|
|
1340
|
-
{ id: "password", label: "Sign in with email and password", note: "mints a new key for this machine" }
|
|
1341
|
-
]);
|
|
1342
|
-
}
|
|
1343
|
-
async function obtainKey(flags2, baseUrl) {
|
|
1344
|
-
const given = flagStr(flags2, "key") || "";
|
|
1345
|
-
if (given) return given;
|
|
1346
|
-
const how = await chooseMethod();
|
|
1347
|
-
if (how === "paste") return askSecret("API key:");
|
|
1348
|
-
return mintKey(baseUrl);
|
|
1349
|
-
}
|
|
1350
|
-
async function verifyKey(baseUrl, apiKey) {
|
|
1351
|
-
const check2 = await fetch(`${baseUrl}/api/config`, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
1352
|
-
if (!check2.ok) throw new Error(`That key was rejected by ${baseUrl} (${check2.status})`);
|
|
1353
|
-
}
|
|
1354
|
-
async function loginUrl(flags2) {
|
|
1355
|
-
const current = resolved().baseUrl;
|
|
1356
|
-
const url = flagStr(flags2, "url") || (flagStr(flags2, "key") ? current : await ask("Control plane URL:", current));
|
|
1357
|
-
return url.replace(/\/+$/, "");
|
|
1358
|
-
}
|
|
1359
|
-
async function cmdLogin(flags2) {
|
|
1360
|
-
const baseUrl = await loginUrl(flags2);
|
|
1361
|
-
const apiKey = await obtainKey(flags2, baseUrl);
|
|
1362
|
-
if (!apiKey) throw new Error("No API key given");
|
|
1363
|
-
await verifyKey(baseUrl, apiKey);
|
|
1364
|
-
save({ apiKey, baseUrl });
|
|
1365
|
-
console.log(`
|
|
1366
|
-
\u2705 Signed in to ${baseUrl}. Saved to ${configPath}`);
|
|
1367
|
-
console.log(" Scripts using @oya-ai/browser read this file too, so `new Oya()` needs no environment.");
|
|
1368
|
-
console.log(" Next: `oya init` to pick your model and browser provider.");
|
|
1448
|
+
function origin({ apiKey, baseUrl } = {}) {
|
|
1449
|
+
const saved = load();
|
|
1450
|
+
const key = first(apiKey, process.env.OYA_API_KEY, saved.apiKey, KEY_SOURCES);
|
|
1451
|
+
const url = first(baseUrl, process.env.OYA_BASE_URL, saved.baseUrl, URL_SOURCES);
|
|
1452
|
+
const address = (url?.value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
1453
|
+
const found = { apiKey: key?.value ?? "", apiKeyFrom: key?.from ?? "none", baseUrl: address };
|
|
1454
|
+
return { ...found, baseUrlFrom: url?.from ?? "default", savedKey: !!saved.apiKey };
|
|
1455
|
+
}
|
|
1456
|
+
var KEY_SOURCES = ["--key", "OYA_API_KEY", "saved"];
|
|
1457
|
+
var URL_SOURCES = ["--url", "OYA_BASE_URL", "saved"];
|
|
1458
|
+
function resolved() {
|
|
1459
|
+
const { apiKey, baseUrl } = origin();
|
|
1460
|
+
return { apiKey, baseUrl };
|
|
1369
1461
|
}
|
|
1370
1462
|
|
|
1371
1463
|
// ../sdk/dist/index.js
|
|
1464
|
+
var DEFAULT_BASE_URL2 = "https://oyabrowser.com";
|
|
1465
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
1466
|
+
var START_TIMEOUT_MS = 12e4;
|
|
1467
|
+
var READY_TIMEOUT_MS2 = 12e4;
|
|
1468
|
+
var READY_POLL_MS2 = 2e3;
|
|
1469
|
+
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
1470
|
+
var CHALLENGE_TIMEOUT_MS = 18e4;
|
|
1471
|
+
var AGENT_TIMEOUT_MS = 6e5;
|
|
1472
|
+
var PLAYBOOK_TIMEOUT_MS = 12e4;
|
|
1473
|
+
var STOP_TIMEOUT_MS = 6e4;
|
|
1474
|
+
var WAIT_FOR_DEFAULT_MS = 3e4;
|
|
1475
|
+
var WAIT_FOR_GRACE_MS = 5e3;
|
|
1476
|
+
var AIMED_SCROLL_AMOUNT = 500;
|
|
1477
|
+
var RUN_POLL_MS = 2e3;
|
|
1478
|
+
var MAX_RUN_POLL_ERRORS = 5;
|
|
1479
|
+
var BYTES_PER_MB = 1048576;
|
|
1480
|
+
var MAX_FILE_MB = 10;
|
|
1481
|
+
var MS_PER_SECOND = 1e3;
|
|
1482
|
+
var LATIN1_MAX = 255;
|
|
1483
|
+
var SPACE = 32;
|
|
1484
|
+
var TAB = 9;
|
|
1485
|
+
var DEL = 127;
|
|
1486
|
+
var HEX = 16;
|
|
1487
|
+
var CODE_POINT_DIGITS = 4;
|
|
1488
|
+
var Status3 = {
|
|
1489
|
+
/** The caller passed something unusable, such as a non-numeric element id. */
|
|
1490
|
+
BAD_REQUEST: 400,
|
|
1491
|
+
/** The server does not know that session. */
|
|
1492
|
+
NOT_FOUND: 404,
|
|
1493
|
+
/** The browser is in a state that needs attention first. */
|
|
1494
|
+
CONFLICT: 409,
|
|
1495
|
+
/** A browser command ran and failed. */
|
|
1496
|
+
UNPROCESSABLE: 422,
|
|
1497
|
+
/** A run failed without saying why. */
|
|
1498
|
+
SERVER_ERROR: 500,
|
|
1499
|
+
/** A browser did not come up in time. */
|
|
1500
|
+
GATEWAY_TIMEOUT: 504
|
|
1501
|
+
};
|
|
1372
1502
|
var OyaError = class extends Error {
|
|
1373
1503
|
/** The HTTP status, or the SDK's own status for errors it raises itself. */
|
|
1374
1504
|
status;
|
|
@@ -1382,6 +1512,10 @@ var OyaError = class extends Error {
|
|
|
1382
1512
|
this.body = body;
|
|
1383
1513
|
}
|
|
1384
1514
|
};
|
|
1515
|
+
function refusal(message, field, suggestion) {
|
|
1516
|
+
const body = { error: message, code: "invalid_request", field, ...suggestion ? { suggestion } : {} };
|
|
1517
|
+
return new OyaError(message, Status3.BAD_REQUEST, body);
|
|
1518
|
+
}
|
|
1385
1519
|
var node = () => globalThis.process;
|
|
1386
1520
|
function builtin(name) {
|
|
1387
1521
|
const get = node()?.getBuiltinModule;
|
|
@@ -1402,54 +1536,25 @@ function savedConfig() {
|
|
|
1402
1536
|
return {};
|
|
1403
1537
|
}
|
|
1404
1538
|
}
|
|
1405
|
-
var
|
|
1406
|
-
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
1407
|
-
var START_TIMEOUT_MS = 12e4;
|
|
1408
|
-
var READY_TIMEOUT_MS2 = 12e4;
|
|
1409
|
-
var READY_POLL_MS2 = 2e3;
|
|
1410
|
-
var NAVIGATE_TIMEOUT_MS = 12e4;
|
|
1411
|
-
var CHALLENGE_TIMEOUT_MS = 18e4;
|
|
1412
|
-
var AGENT_TIMEOUT_MS = 6e5;
|
|
1413
|
-
var PLAYBOOK_TIMEOUT_MS = 12e4;
|
|
1414
|
-
var STOP_TIMEOUT_MS = 6e4;
|
|
1415
|
-
var WAIT_FOR_DEFAULT_MS = 3e4;
|
|
1416
|
-
var WAIT_FOR_GRACE_MS = 5e3;
|
|
1417
|
-
var AIMED_SCROLL_AMOUNT = 500;
|
|
1418
|
-
var RUN_POLL_MS = 2e3;
|
|
1419
|
-
var MAX_RUN_POLL_ERRORS = 5;
|
|
1420
|
-
var BYTES_PER_MB = 1048576;
|
|
1421
|
-
var MAX_FILE_MB = 10;
|
|
1422
|
-
var MS_PER_SECOND = 1e3;
|
|
1423
|
-
var Status3 = {
|
|
1424
|
-
/** The caller passed something unusable, such as a non-numeric element id. */
|
|
1425
|
-
BAD_REQUEST: 400,
|
|
1426
|
-
/** The server does not know that session. */
|
|
1427
|
-
NOT_FOUND: 404,
|
|
1428
|
-
/** The browser is in a state that needs attention first. */
|
|
1429
|
-
CONFLICT: 409,
|
|
1430
|
-
/** A browser command ran and failed. */
|
|
1431
|
-
UNPROCESSABLE: 422,
|
|
1432
|
-
/** A run failed without saying why. */
|
|
1433
|
-
SERVER_ERROR: 500,
|
|
1434
|
-
/** A browser did not come up in time. */
|
|
1435
|
-
GATEWAY_TIMEOUT: 504
|
|
1436
|
-
};
|
|
1539
|
+
var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
|
|
1437
1540
|
var Http = class {
|
|
1438
|
-
/** Stores where to call, with which key, how long to wait
|
|
1439
|
-
constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
|
|
1541
|
+
/** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
|
|
1542
|
+
constructor(baseUrl, apiKey, timeoutMs, fetchImpl, origin3 = OPTIONS_ORIGIN) {
|
|
1440
1543
|
this.baseUrl = baseUrl;
|
|
1441
1544
|
this.apiKey = apiKey;
|
|
1442
1545
|
this.timeoutMs = timeoutMs;
|
|
1443
1546
|
this.fetchImpl = fetchImpl;
|
|
1547
|
+
this.origin = origin3;
|
|
1444
1548
|
}
|
|
1445
1549
|
baseUrl;
|
|
1446
1550
|
apiKey;
|
|
1447
1551
|
timeoutMs;
|
|
1448
1552
|
fetchImpl;
|
|
1553
|
+
origin;
|
|
1449
1554
|
/** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
|
|
1450
1555
|
async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
|
|
1451
1556
|
const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
|
|
1452
|
-
return readAnswer2(res, `${method} ${path}`, this
|
|
1557
|
+
return readAnswer2(res, `${method} ${path}`, this);
|
|
1453
1558
|
}
|
|
1454
1559
|
/** The fetch options for one call, with this client's key on them. */
|
|
1455
1560
|
init(method, body, timeoutMs, headers) {
|
|
@@ -1464,23 +1569,34 @@ var Http = class {
|
|
|
1464
1569
|
try {
|
|
1465
1570
|
return await this.fetchImpl(url, init);
|
|
1466
1571
|
} catch (err) {
|
|
1467
|
-
throw
|
|
1572
|
+
throw noAnswer(this, timeoutMs, err);
|
|
1468
1573
|
}
|
|
1469
1574
|
}
|
|
1470
1575
|
};
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
return
|
|
1576
|
+
var timedOut = (err) => err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
|
|
1577
|
+
function causeOf(err) {
|
|
1578
|
+
const cause = err?.cause;
|
|
1579
|
+
return cause?.code ?? cause?.errors?.[0]?.code;
|
|
1580
|
+
}
|
|
1581
|
+
function noAnswer(http, timeoutMs, err) {
|
|
1582
|
+
const code = timedOut(err) ? "timeout" : "unreachable";
|
|
1583
|
+
const body = { error: String(err?.message), code, ...causeOf(err) ? { cause: causeOf(err) } : {} };
|
|
1584
|
+
return new OyaError(unreachable(http, timeoutMs, code), 0, body);
|
|
1585
|
+
}
|
|
1586
|
+
function unreachable({ baseUrl, origin: origin3 }, timeoutMs, code) {
|
|
1587
|
+
if (code === "timeout")
|
|
1588
|
+
return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
|
|
1589
|
+
if (origin3.baseUrlFrom === "default") return `Could not reach ${baseUrl}. Check your connection.`;
|
|
1590
|
+
return `Could not reach ${baseUrl}. Is the server running, and is ${origin3.baseUrlFrom} right?`;
|
|
1475
1591
|
}
|
|
1476
1592
|
function requestInit(method, body, timeoutMs, headers) {
|
|
1477
1593
|
const json = body === void 0 ? {} : { "Content-Type": "application/json" };
|
|
1478
1594
|
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
1479
1595
|
return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
|
|
1480
1596
|
}
|
|
1481
|
-
async function readAnswer2(res, call2,
|
|
1597
|
+
async function readAnswer2(res, call2, http) {
|
|
1482
1598
|
const payload = parseBody(await res.text());
|
|
1483
|
-
if (!res.ok) throw failure(call2, res.status, payload,
|
|
1599
|
+
if (!res.ok) throw failure(call2, res.status, payload, http);
|
|
1484
1600
|
return payload;
|
|
1485
1601
|
}
|
|
1486
1602
|
function parseBody(text) {
|
|
@@ -1490,22 +1606,74 @@ function parseBody(text) {
|
|
|
1490
1606
|
return text;
|
|
1491
1607
|
}
|
|
1492
1608
|
}
|
|
1493
|
-
function failure(call2, status, payload,
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1609
|
+
function failure(call2, status, payload, http) {
|
|
1610
|
+
const message = payload?.error || `${call2} failed (${status})`;
|
|
1611
|
+
return new OyaError(message === "Invalid API key" ? invalidKey(http) : message, status, payload);
|
|
1612
|
+
}
|
|
1613
|
+
function invalidKey({ baseUrl, origin: origin3 }) {
|
|
1614
|
+
const overrides = origin3.apiKeyFrom === "OYA_API_KEY" && origin3.savedKey;
|
|
1615
|
+
const rest = overrides ? ", which overrides the one saved in ~/.oya/config.json" : "";
|
|
1616
|
+
return `Invalid API key for ${baseUrl}. The key came from ${origin3.apiKeyFrom}${rest}.`;
|
|
1499
1617
|
}
|
|
1500
1618
|
var env = (name) => globalThis.process?.env?.[name];
|
|
1619
|
+
function firstOf(option, name, saved, optionName) {
|
|
1620
|
+
if (option) return { value: option, from: `the ${optionName} option` };
|
|
1621
|
+
if (env(name)) return { value: env(name), from: name };
|
|
1622
|
+
return { value: saved, from: "~/.oya/config.json" };
|
|
1623
|
+
}
|
|
1624
|
+
function addressOf(options, saved) {
|
|
1625
|
+
const found = firstOf(options.baseUrl, "OYA_BASE_URL", saved, "baseUrl");
|
|
1626
|
+
return found.value ? found : { value: DEFAULT_BASE_URL2, from: "default" };
|
|
1627
|
+
}
|
|
1628
|
+
function keyOf(options, saved) {
|
|
1629
|
+
const found = firstOf(options.apiKey, "OYA_API_KEY", saved, "apiKey");
|
|
1630
|
+
if (!found.value) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
|
|
1631
|
+
return { ...found, value: headerSafe(found.value, "apiKey", "Copy the key again.") };
|
|
1632
|
+
}
|
|
1501
1633
|
function createHttp(options) {
|
|
1502
1634
|
const saved = savedConfig();
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || saved.baseUrl || DEFAULT_BASE_URL2).replace(/\/+$/, "");
|
|
1635
|
+
const key = keyOf(options, saved.apiKey);
|
|
1636
|
+
const address = addressOf(options, saved.baseUrl);
|
|
1506
1637
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
1507
1638
|
if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
|
|
1508
|
-
|
|
1639
|
+
const origin3 = { apiKeyFrom: key.from, baseUrlFrom: address.from, savedKey: !!saved.apiKey };
|
|
1640
|
+
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1641
|
+
return new Http(webAddress(address), key.value, timeout, fetchImpl.bind(globalThis), origin3);
|
|
1642
|
+
}
|
|
1643
|
+
var LOCAL_HOST = /^(localhost|127\.)/i;
|
|
1644
|
+
function webAddress({ value, from }) {
|
|
1645
|
+
const url = value.trim().replace(/\/+$/, "");
|
|
1646
|
+
if (/^https?:\/\//i.test(url)) return url;
|
|
1647
|
+
const name = from.startsWith("the ") ? "baseUrl" : from === "OYA_BASE_URL" ? from : `baseUrl in ${from}`;
|
|
1648
|
+
const suggestion = suggestionFor(url);
|
|
1649
|
+
const tryIt = suggestion ? ` Try ${suggestion}.` : "";
|
|
1650
|
+
throw refusal(`${name} must start with http:// or https://, not "${url}".${tryIt}`, "baseUrl", suggestion);
|
|
1651
|
+
}
|
|
1652
|
+
function suggestionFor(url) {
|
|
1653
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^[^:/]+:\d+(\/|$)/.test(url)) return void 0;
|
|
1654
|
+
return `${LOCAL_HOST.test(url) ? "http" : "https"}://${url}`;
|
|
1655
|
+
}
|
|
1656
|
+
function notInAHeader(ch) {
|
|
1657
|
+
const code = ch.codePointAt(0);
|
|
1658
|
+
return code < SPACE && code !== TAB || code === DEL || code > LATIN1_MAX;
|
|
1659
|
+
}
|
|
1660
|
+
function headerSafe(value, field, ending) {
|
|
1661
|
+
const trimmed = value.trim();
|
|
1662
|
+
const chars = Array.from(trimmed);
|
|
1663
|
+
const at = chars.findIndex(notInAHeader);
|
|
1664
|
+
if (at < 0) return trimmed;
|
|
1665
|
+
const leading = Array.from(value).length - Array.from(value.trimStart()).length;
|
|
1666
|
+
const where = `position ${leading + at + 1} (${codePoint(chars[at])})`;
|
|
1667
|
+
throw refusal(`${field} has a character HTTP headers cannot carry at ${where}. ${ending}`, field);
|
|
1668
|
+
}
|
|
1669
|
+
var codePoint = (ch) => `U+${ch.codePointAt(0).toString(HEX).toUpperCase().padStart(CODE_POINT_DIGITS, "0")}`;
|
|
1670
|
+
var shown = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
1671
|
+
function segment(value, field = "id") {
|
|
1672
|
+
if (typeof value !== "string" || !value)
|
|
1673
|
+
throw refusal(`${field} must be a non-empty string, not ${shown(value)}`, field);
|
|
1674
|
+
if (value.includes("/") || value === "." || value === "..")
|
|
1675
|
+
throw refusal(`${field} must be a single path segment (no "/" and not "." or ".."), not ${shown(value)}`, field);
|
|
1676
|
+
return encodeURIComponent(value);
|
|
1509
1677
|
}
|
|
1510
1678
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1511
1679
|
async function call(fn) {
|
|
@@ -1738,7 +1906,7 @@ var Browser = class {
|
|
|
1738
1906
|
* Play `'<name>:draft'` to try a draft before promoting it.
|
|
1739
1907
|
*/
|
|
1740
1908
|
async play(name, data = {}, { autoHeal = true } = {}) {
|
|
1741
|
-
const path = `/api/browsers/${this.id}/playbooks/${
|
|
1909
|
+
const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
|
|
1742
1910
|
return agentAnswer(
|
|
1743
1911
|
await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
|
|
1744
1912
|
);
|
|
@@ -1789,7 +1957,7 @@ var Browser = class {
|
|
|
1789
1957
|
}
|
|
1790
1958
|
/** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
|
|
1791
1959
|
async revokeShare(id) {
|
|
1792
|
-
await this.http.request("DELETE", `/api/control/credentials/${
|
|
1960
|
+
await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
|
|
1793
1961
|
}
|
|
1794
1962
|
/** Counters, health and the last 50 things this browser did. */
|
|
1795
1963
|
status() {
|
|
@@ -1843,8 +2011,9 @@ function startBody(options) {
|
|
|
1843
2011
|
const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
|
|
1844
2012
|
return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
|
|
1845
2013
|
}
|
|
2014
|
+
var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
|
|
1846
2015
|
async function start(http, wait, options) {
|
|
1847
|
-
const headers = { "Idempotency-Key": options
|
|
2016
|
+
const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
|
|
1848
2017
|
const path = "/api/browsers/start";
|
|
1849
2018
|
const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
|
|
1850
2019
|
if (started.status === "starting") {
|
|
@@ -1853,7 +2022,7 @@ async function start(http, wait, options) {
|
|
|
1853
2022
|
}
|
|
1854
2023
|
return new Browser(http(), started, options.captcha === "auto");
|
|
1855
2024
|
}
|
|
1856
|
-
var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${
|
|
2025
|
+
var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
|
|
1857
2026
|
async function reattach(http, id) {
|
|
1858
2027
|
const found = await fetchBrowser(http, id);
|
|
1859
2028
|
return new Browser(http(), asStarted(found), false);
|
|
@@ -1865,67 +2034,77 @@ function asStarted(found) {
|
|
|
1865
2034
|
var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
|
|
1866
2035
|
var browserApi = (http, wait) => ({
|
|
1867
2036
|
/** Start a browser and wait until it can take commands. */
|
|
1868
|
-
start: (options = {}) => start(http, wait, options),
|
|
2037
|
+
start: async (options = {}) => start(http, wait, options),
|
|
1869
2038
|
/** Reattach to a browser that is already running. */
|
|
1870
|
-
get: (id) => reattach(http, id),
|
|
2039
|
+
get: async (id) => reattach(http, id),
|
|
1871
2040
|
/** Every browser on this key. */
|
|
1872
|
-
list: () => http().request("GET", "/api/browsers"),
|
|
2041
|
+
list: async () => http().request("GET", "/api/browsers"),
|
|
1873
2042
|
/** Stop some (`ids`) or every browser on this key. Each reports separately. */
|
|
1874
|
-
stop: (ids) => stopBrowsers(http, ids),
|
|
2043
|
+
stop: async (ids) => stopBrowsers(http, ids),
|
|
1875
2044
|
/** Stop every browser on this key; returns how many stopped. */
|
|
1876
2045
|
stopAll: async () => (await stopBrowsers(http, "all")).stopped
|
|
1877
2046
|
});
|
|
1878
|
-
var session = (id, action = "") => `/api/control/sessions/${
|
|
1879
|
-
var item = (kind, id) => `/api/control/${kind}/${
|
|
2047
|
+
var session = (id, action = "") => `/api/control/sessions/${segment(id)}${action}`;
|
|
2048
|
+
var item = (kind, id) => `/api/control/${kind}/${segment(id)}`;
|
|
1880
2049
|
var sessionCalls = (http) => ({
|
|
1881
2050
|
/** Settings, sessions and recent events at a glance. */
|
|
1882
|
-
overview: () => http().request("GET", "/api/control"),
|
|
2051
|
+
overview: async () => http().request("GET", "/api/control"),
|
|
1883
2052
|
/** Every session, including disconnected and cleanup-pending ones. */
|
|
1884
|
-
sessions: () => http().request("GET", "/api/control/sessions"),
|
|
2053
|
+
sessions: async () => http().request("GET", "/api/control/sessions"),
|
|
1885
2054
|
/** One session. */
|
|
1886
|
-
session: (id) => http().request("GET", session(id)),
|
|
2055
|
+
session: async (id) => http().request("GET", session(id)),
|
|
1887
2056
|
/** Update limits, rate cards and retention. */
|
|
1888
|
-
settings: (changes) => http().request("PATCH", "/api/control/project", changes)
|
|
2057
|
+
settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
|
|
1889
2058
|
});
|
|
1890
2059
|
var lifecycleCalls = (http) => ({
|
|
1891
|
-
/**
|
|
1892
|
-
cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
|
|
2060
|
+
/** Stop a session in any state; answers its final state. */
|
|
2061
|
+
cancel: async (id) => http().request("POST", session(id, "/cancel"), {}),
|
|
1893
2062
|
/** Stop a session; `force` stops despite a profile-save error, or reconciles. */
|
|
1894
|
-
stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
|
|
2063
|
+
stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
|
|
1895
2064
|
/** Acquire or release human control, or acknowledge the agent's resume. */
|
|
1896
|
-
takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
|
|
2065
|
+
takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
|
|
1897
2066
|
/** Send one input as the human holding the control lease. */
|
|
1898
|
-
input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
|
|
2067
|
+
input: async (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
|
|
1899
2068
|
});
|
|
2069
|
+
function recoverBody(options) {
|
|
2070
|
+
const { replace = false, wsUrl } = typeof options === "boolean" ? { replace: options } : options ?? {};
|
|
2071
|
+
if (wsUrl && !replace)
|
|
2072
|
+
throw refusal("wsUrl is only used with replace: true; recovering in place keeps the endpoint it had.", "wsUrl");
|
|
2073
|
+
return wsUrl ? { replace, wsUrl } : { replace };
|
|
2074
|
+
}
|
|
1900
2075
|
var recoveryCalls = (http) => ({
|
|
1901
|
-
/**
|
|
1902
|
-
|
|
2076
|
+
/**
|
|
2077
|
+
* Explicitly recover a session, or replace it with a fresh one. A cdp
|
|
2078
|
+
* session's replacement needs the Chrome it runs on: `{ replace: true, wsUrl }`.
|
|
2079
|
+
* `recover(id, true)` still means replace.
|
|
2080
|
+
*/
|
|
2081
|
+
recover: async (id, options = {}) => http().request("POST", session(id, "/recover"), recoverBody(options)),
|
|
1903
2082
|
/** A single-use ticket for the live stream. */
|
|
1904
|
-
ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
|
|
2083
|
+
ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
|
|
1905
2084
|
/** Durable lifecycle events after a cursor. */
|
|
1906
|
-
events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
|
|
2085
|
+
events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
|
|
1907
2086
|
});
|
|
1908
2087
|
var credentialCalls = (http) => ({
|
|
1909
2088
|
/** Mint a service credential. Its token is returned this once. */
|
|
1910
|
-
createCredential: (options) => http().request("POST", "/api/control/credentials", options),
|
|
2089
|
+
createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
|
|
1911
2090
|
/** Revoke a service credential. */
|
|
1912
|
-
revokeCredential: (id) => http().request("DELETE", item("credentials", id))
|
|
2091
|
+
revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
|
|
1913
2092
|
});
|
|
1914
2093
|
var memberCalls = (http) => ({
|
|
1915
2094
|
/** The owner and every member. */
|
|
1916
|
-
members: () => http().request("GET", "/api/control/members"),
|
|
2095
|
+
members: async () => http().request("GET", "/api/control/members"),
|
|
1917
2096
|
/** An invitation code for a new member. */
|
|
1918
|
-
inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
|
|
2097
|
+
inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
|
|
1919
2098
|
/** Remove a member. */
|
|
1920
|
-
removeMember: (userId) => http().request("DELETE", item("members", userId))
|
|
2099
|
+
removeMember: async (userId) => http().request("DELETE", item("members", userId))
|
|
1921
2100
|
});
|
|
1922
2101
|
var webhookCalls = (http) => ({
|
|
1923
2102
|
/** Register a webhook; `types` limits which events it receives. */
|
|
1924
|
-
createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
|
|
2103
|
+
createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
|
|
1925
2104
|
/** Disable a webhook. */
|
|
1926
|
-
removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
|
|
2105
|
+
removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
|
|
1927
2106
|
/** Send a delivery again. */
|
|
1928
|
-
replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
|
|
2107
|
+
replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
|
|
1929
2108
|
});
|
|
1930
2109
|
var controlApi = (http) => ({
|
|
1931
2110
|
...sessionCalls(http),
|
|
@@ -1935,7 +2114,7 @@ var controlApi = (http) => ({
|
|
|
1935
2114
|
...memberCalls(http),
|
|
1936
2115
|
...webhookCalls(http)
|
|
1937
2116
|
});
|
|
1938
|
-
var playbook = (name) => `/api/playbooks/${
|
|
2117
|
+
var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
|
|
1939
2118
|
var playbookApi = (http) => ({
|
|
1940
2119
|
/** Every saved playbook, with any draft waiting on it. */
|
|
1941
2120
|
list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
|
|
@@ -1944,16 +2123,16 @@ var playbookApi = (http) => ({
|
|
|
1944
2123
|
await http().request("DELETE", playbook(name));
|
|
1945
2124
|
},
|
|
1946
2125
|
/** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
|
|
1947
|
-
promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
|
|
2126
|
+
promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
|
|
1948
2127
|
});
|
|
1949
2128
|
var proxyApi = (http) => ({
|
|
1950
2129
|
/** Every proxy this key can use, shared ones included. */
|
|
1951
2130
|
list: async () => (await http().request("GET", "/api/proxies")).proxies,
|
|
1952
2131
|
/** Add a proxy. Its credentials are never read back. */
|
|
1953
|
-
create: (proxy) => http().request("POST", "/api/proxies", proxy),
|
|
2132
|
+
create: async (proxy) => http().request("POST", "/api/proxies", proxy),
|
|
1954
2133
|
/** Remove one of this key's proxies. */
|
|
1955
2134
|
remove: async (id) => {
|
|
1956
|
-
await http().request("DELETE", `/api/proxies/${
|
|
2135
|
+
await http().request("DELETE", `/api/proxies/${segment(id)}`);
|
|
1957
2136
|
},
|
|
1958
2137
|
/** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
|
|
1959
2138
|
check: async () => (await http().request("POST", "/api/proxies/check", {})).results
|
|
@@ -1962,35 +2141,38 @@ var identityCalls = (http) => ({
|
|
|
1962
2141
|
/** Every persona on this key. */
|
|
1963
2142
|
list: async () => (await http().request("GET", "/api/personas")).personas,
|
|
1964
2143
|
/** One persona. */
|
|
1965
|
-
get: (id) => http().request("GET", `/api/personas/${id}`),
|
|
2144
|
+
get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
|
|
1966
2145
|
/**
|
|
1967
2146
|
* Create an identity. The device, platform, timezone, locale, is chosen
|
|
1968
2147
|
* here and fixed for its life; `preview()` shows what a choice produces.
|
|
1969
2148
|
*/
|
|
1970
|
-
create: (options = {}) => http().request("POST", "/api/personas", options),
|
|
2149
|
+
create: async (options = {}) => http().request("POST", "/api/personas", options),
|
|
1971
2150
|
/** Name, concurrency cap and proxy hint. Never the device, clone for that. */
|
|
1972
|
-
update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
|
|
2151
|
+
update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
|
|
1973
2152
|
/** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
|
|
1974
|
-
clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
|
|
2153
|
+
clone: async (id, options = {}) => http().request("POST", `/api/personas/${segment(id)}/clone`, options)
|
|
1975
2154
|
});
|
|
1976
2155
|
var deviceCalls = (http) => ({
|
|
1977
2156
|
/** The fingerprint these choices would produce. Persists nothing. */
|
|
1978
2157
|
preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
|
|
1979
2158
|
/** Platforms, and the timezones and locales each may coherently claim. */
|
|
1980
|
-
options: () => http().request("GET", "/api/personas/options"),
|
|
2159
|
+
options: async () => http().request("GET", "/api/personas/options"),
|
|
1981
2160
|
/** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
|
|
1982
|
-
pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
|
|
2161
|
+
pinProxy: async (id, proxyId) => http().request("PUT", `/api/personas/${segment(id)}/proxy`, { proxyId }),
|
|
1983
2162
|
/** Delete a persona. */
|
|
1984
2163
|
remove: async (id) => {
|
|
1985
|
-
await http().request("DELETE", `/api/personas/${id}`);
|
|
2164
|
+
await http().request("DELETE", `/api/personas/${segment(id)}`);
|
|
1986
2165
|
}
|
|
1987
2166
|
});
|
|
1988
2167
|
var mfaCalls = (http) => ({
|
|
1989
2168
|
/** Store the second factor for this identity. Sealed at rest, never read back. */
|
|
1990
|
-
setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
|
|
2169
|
+
setMfa: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/mfa`, config),
|
|
1991
2170
|
/** Remove the persona-wide factor, or the one filed against `domain`. */
|
|
1992
2171
|
clearMfa: async (id, domain) => {
|
|
1993
|
-
await http().request(
|
|
2172
|
+
await http().request(
|
|
2173
|
+
"DELETE",
|
|
2174
|
+
`/api/personas/${segment(id)}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`
|
|
2175
|
+
);
|
|
1994
2176
|
}
|
|
1995
2177
|
});
|
|
1996
2178
|
var loginCalls = (http) => ({
|
|
@@ -2001,19 +2183,36 @@ var loginCalls = (http) => ({
|
|
|
2001
2183
|
* better path. This is for portals that expire a session server-side
|
|
2002
2184
|
* between runs, where an unattended run has nothing else to recover with.
|
|
2003
2185
|
*/
|
|
2004
|
-
setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
|
|
2186
|
+
setCredentials: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/credentials`, config),
|
|
2005
2187
|
/** Which sites this identity can sign in to. Usernames only. */
|
|
2006
|
-
credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
|
|
2188
|
+
credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
|
|
2007
2189
|
/** Remove the login stored for one site. */
|
|
2008
2190
|
clearCredentials: async (id, domain) => {
|
|
2009
|
-
await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
2191
|
+
await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
|
|
2195
|
+
var jarCalls = (http) => ({
|
|
2196
|
+
/** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
|
|
2197
|
+
cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
|
|
2198
|
+
/** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
|
|
2199
|
+
importCookies: async (id, list2) => http().request("PUT", jarPath(id), { cookies: list2 })
|
|
2200
|
+
});
|
|
2201
|
+
var cookieCalls = (http) => ({
|
|
2202
|
+
...jarCalls(http),
|
|
2203
|
+
/** Copy every login of persona `from` into persona `to`, which keeps its own device. */
|
|
2204
|
+
copyCookies: async (from, to) => jarCalls(http).importCookies(to, await jarCalls(http).cookies(from)),
|
|
2205
|
+
/** Forget every cookie in the jar: signs the persona out everywhere. */
|
|
2206
|
+
clearCookies: async (id) => {
|
|
2207
|
+
await http().request("DELETE", jarPath(id));
|
|
2010
2208
|
}
|
|
2011
2209
|
});
|
|
2012
2210
|
var personaApi = (http) => ({
|
|
2013
2211
|
...identityCalls(http),
|
|
2014
2212
|
...deviceCalls(http),
|
|
2015
2213
|
...mfaCalls(http),
|
|
2016
|
-
...loginCalls(http)
|
|
2214
|
+
...loginCalls(http),
|
|
2215
|
+
...cookieCalls(http)
|
|
2017
2216
|
});
|
|
2018
2217
|
var configApi = (http) => ({
|
|
2019
2218
|
/** This key's settings. Secrets read back masked. */
|
|
@@ -2097,32 +2296,101 @@ var Oya = class {
|
|
|
2097
2296
|
};
|
|
2098
2297
|
|
|
2099
2298
|
// src/context.ts
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const apiKey =
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
console.error("No API key. Run `oya login`, or pass --key / set OYA_API_KEY.");
|
|
2106
|
-
process.exit(1);
|
|
2107
|
-
}
|
|
2299
|
+
var origin2 = (flags) => origin({ apiKey: flagStr(flags, "key"), baseUrl: flagStr(flags, "url") });
|
|
2300
|
+
function client(flags) {
|
|
2301
|
+
const { apiKey, baseUrl } = origin2(flags);
|
|
2302
|
+
if (!apiKey)
|
|
2303
|
+
throw new CliError("No API key.", "no_api_key", { hint: "Run oya login, or pass --key, or set OYA_API_KEY." });
|
|
2108
2304
|
return new Oya({ apiKey, baseUrl });
|
|
2109
2305
|
}
|
|
2110
2306
|
var printJson = (value) => console.log(JSON.stringify(value, null, JSON_INDENT2));
|
|
2111
|
-
var out = (
|
|
2112
|
-
if (
|
|
2307
|
+
var out = (flags, value, human) => {
|
|
2308
|
+
if (flags.json) printJson(value);
|
|
2113
2309
|
else human();
|
|
2114
2310
|
};
|
|
2115
|
-
async function targetBrowser(oya,
|
|
2116
|
-
const id = flagStr(
|
|
2311
|
+
async function targetBrowser(oya, flags) {
|
|
2312
|
+
const id = flagStr(flags, "id");
|
|
2117
2313
|
if (id) return oya.browser.get(id);
|
|
2118
2314
|
const all = await oya.browser.list();
|
|
2119
|
-
if (!all.length) {
|
|
2120
|
-
console.error("No browsers running. Start one with `oya start`.");
|
|
2121
|
-
process.exit(1);
|
|
2122
|
-
}
|
|
2315
|
+
if (!all.length) throw new CliError("No browsers running.", "no_browser", { hint: "Start one with oya start." });
|
|
2123
2316
|
return oya.browser.get(all[all.length - 1].id);
|
|
2124
2317
|
}
|
|
2125
2318
|
|
|
2319
|
+
// src/commands/login.ts
|
|
2320
|
+
async function postJson(url, body, token2) {
|
|
2321
|
+
const headers = { ...token2 ? { Authorization: `Bearer ${token2}` } : {}, "Content-Type": "application/json" };
|
|
2322
|
+
const signal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
2323
|
+
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), signal }).catch(() => {
|
|
2324
|
+
throw new CliError(`Could not reach ${new URL(url).origin}.`, "unreachable", {
|
|
2325
|
+
hint: "Check the address, then run oya login again."
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2328
|
+
return { ok: res.ok, body: jsonOr(await res.text(), `${res.status} from ${url}`) };
|
|
2329
|
+
}
|
|
2330
|
+
function jsonOr(text, answer) {
|
|
2331
|
+
try {
|
|
2332
|
+
return JSON.parse(text);
|
|
2333
|
+
} catch {
|
|
2334
|
+
return { error: `Not a JSON answer: ${answer}` };
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
async function mintKey(baseUrl) {
|
|
2338
|
+
const email = await ask("Email:");
|
|
2339
|
+
const password = await askSecret("Password:");
|
|
2340
|
+
const session2 = await postJson(`${baseUrl}/api/auth/login`, { email, password });
|
|
2341
|
+
if (!session2.ok || !session2.body.access_token) throw new Error(session2.body.error || "Sign-in failed");
|
|
2342
|
+
const minted = await postJson(`${baseUrl}/api/auth/keys`, { label: "CLI" }, session2.body.access_token);
|
|
2343
|
+
if (!minted.ok || !minted.body.key) throw new Error(minted.body.error || "Could not create an API key");
|
|
2344
|
+
console.log(' Created a new API key labelled "CLI".');
|
|
2345
|
+
return minted.body.key;
|
|
2346
|
+
}
|
|
2347
|
+
function chooseMethod() {
|
|
2348
|
+
return choose("How do you want to authenticate?", [
|
|
2349
|
+
{ id: "paste", label: "Paste an API key", note: "from the dashboard, or a self-hosted key" },
|
|
2350
|
+
{ id: "password", label: "Sign in with email and password", note: "mints a new key for this machine" }
|
|
2351
|
+
]);
|
|
2352
|
+
}
|
|
2353
|
+
async function obtainKey(flags, baseUrl) {
|
|
2354
|
+
const given = flagStr(flags, "key") || "";
|
|
2355
|
+
if (given) return given;
|
|
2356
|
+
const how = await chooseMethod();
|
|
2357
|
+
if (how === "paste") return askSecret("API key:");
|
|
2358
|
+
return mintKey(baseUrl);
|
|
2359
|
+
}
|
|
2360
|
+
async function verifyKey(baseUrl, apiKey) {
|
|
2361
|
+
await new Oya({ apiKey, baseUrl }).config.get().catch((err) => {
|
|
2362
|
+
throw namedFailure(err, baseUrl);
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
function namedFailure(err, baseUrl) {
|
|
2366
|
+
const status = err.status;
|
|
2367
|
+
const again = "then run oya login again.";
|
|
2368
|
+
if (status === 0)
|
|
2369
|
+
return new CliError(`Could not reach ${baseUrl}.`, "unreachable", { hint: `Check the address, ${again}` });
|
|
2370
|
+
if (status !== Status2.UNAUTHORIZED && status !== Status2.FORBIDDEN) return err;
|
|
2371
|
+
return new CliError(`${baseUrl} rejected that key.`, "invalid_key", { hint: `Check the key, ${again}` });
|
|
2372
|
+
}
|
|
2373
|
+
async function loginUrl(flags) {
|
|
2374
|
+
const current = resolved().baseUrl;
|
|
2375
|
+
const url = flagStr(flags, "url") || (flagStr(flags, "key") ? current : await ask("Control plane URL:", current));
|
|
2376
|
+
return url.replace(/\/+$/, "");
|
|
2377
|
+
}
|
|
2378
|
+
async function cmdLogin(flags) {
|
|
2379
|
+
if (flags.json && !flagStr(flags, "key")) throw usage("oya login --json never prompts, so it needs --key <key>.");
|
|
2380
|
+
const baseUrl = await loginUrl(flags);
|
|
2381
|
+
const apiKey = await obtainKey(flags, baseUrl);
|
|
2382
|
+
if (!apiKey) throw usage("No API key given.");
|
|
2383
|
+
await verifyKey(baseUrl, apiKey);
|
|
2384
|
+
save({ apiKey, baseUrl });
|
|
2385
|
+
out(flags, { ok: true, baseUrl, configPath }, () => signedIn(baseUrl));
|
|
2386
|
+
}
|
|
2387
|
+
function signedIn(baseUrl) {
|
|
2388
|
+
console.log(`
|
|
2389
|
+
\u2705 Signed in to ${baseUrl}. Saved to ${configPath}`);
|
|
2390
|
+
console.log(" Scripts using @oya-ai/browser read this file too, so `new Oya()` needs no environment.");
|
|
2391
|
+
console.log(" Next: `oya init` to pick your model and browser provider.");
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2126
2394
|
// src/commands/init.ts
|
|
2127
2395
|
var PRESETS = {
|
|
2128
2396
|
anthropic: { name: "Anthropic", model: "claude-sonnet-5" },
|
|
@@ -2149,20 +2417,29 @@ async function askModelDetails(llm, updates) {
|
|
|
2149
2417
|
const model = await ask("Default model:", PRESETS[llm].model);
|
|
2150
2418
|
if (model) updates.chat_model = model;
|
|
2151
2419
|
}
|
|
2420
|
+
var needsNote = (p) => p.needs.some((f) => f.endsWith("_url")) ? "needs its ws:// address" : "needs an API key";
|
|
2421
|
+
function fieldLabel(p, field) {
|
|
2422
|
+
if (field.endsWith("_url")) return "Chrome DevTools address, such as ws://127.0.0.1:9222:";
|
|
2423
|
+
if (field.endsWith("_api_key")) return `${p.label} API key:`;
|
|
2424
|
+
return `${p.label} ${field.replace(/^[a-z]+_/, "").replace(/_/g, " ")}:`;
|
|
2425
|
+
}
|
|
2152
2426
|
function providerOptions(current) {
|
|
2153
|
-
const note2 = (p) => p.needs.length ? p.configured ? "configured" :
|
|
2427
|
+
const note2 = (p) => p.needs.length ? p.configured ? "configured" : needsNote(p) : void 0;
|
|
2154
2428
|
return current.providers.map((p) => ({ id: p.id, label: p.label, note: note2(p) }));
|
|
2155
2429
|
}
|
|
2156
2430
|
async function askProvider(current, updates) {
|
|
2157
2431
|
console.log("\n\u2500\u2500 2. Where your browsers run \u2500\u2500");
|
|
2158
2432
|
const provider = await choose("Browser provider:", providerOptions(current));
|
|
2159
2433
|
updates.browser_provider = provider;
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
if (value) updates[field] = value;
|
|
2163
|
-
}
|
|
2434
|
+
const info = current.providers.find((p) => p.id === provider);
|
|
2435
|
+
for (const field of info?.needs || []) await askField(info, field, updates);
|
|
2164
2436
|
return provider;
|
|
2165
2437
|
}
|
|
2438
|
+
async function askField(info, field, updates) {
|
|
2439
|
+
const label = fieldLabel(info, field);
|
|
2440
|
+
const value = field.endsWith("_url") ? await ask(label) : await askSecret(label);
|
|
2441
|
+
if (value) updates[field] = value;
|
|
2442
|
+
}
|
|
2166
2443
|
var CAPTCHA_OPTIONS2 = [
|
|
2167
2444
|
{ id: "", label: "No", note: "providers that solve natively still will" },
|
|
2168
2445
|
{ id: "capsolver", label: "Yes, via CapSolver" }
|
|
@@ -2182,18 +2459,23 @@ function signInHint(provider) {
|
|
|
2182
2459
|
console.log(" arrive already logged in, as the same identity, from the same fingerprint.");
|
|
2183
2460
|
console.log(` Download the desktop browser: ${resolved().baseUrl}/downloads`);
|
|
2184
2461
|
}
|
|
2462
|
+
var NO_ANSWERS = "oya init had no input to read, so its defaults were never chosen. Nothing was saved. Run it in a terminal, or pipe the answers in.";
|
|
2463
|
+
async function saveAnswers(oya, updates) {
|
|
2464
|
+
if (!answered()) throw new InputError(NO_ANSWERS);
|
|
2465
|
+
updates.onboarded = "true";
|
|
2466
|
+
await oya.config.set(updates);
|
|
2467
|
+
}
|
|
2185
2468
|
async function interview2(oya) {
|
|
2186
2469
|
const current = await oya.config.get();
|
|
2187
2470
|
const updates = {};
|
|
2188
2471
|
await askModel(current, updates);
|
|
2189
2472
|
const provider = await askProvider(current, updates);
|
|
2190
2473
|
await askCaptcha(updates);
|
|
2191
|
-
|
|
2192
|
-
await oya.config.set(updates);
|
|
2474
|
+
await saveAnswers(oya, updates);
|
|
2193
2475
|
return provider;
|
|
2194
2476
|
}
|
|
2195
|
-
async function cmdInit(
|
|
2196
|
-
const provider = await interview2(client(
|
|
2477
|
+
async function cmdInit(flags) {
|
|
2478
|
+
const provider = await interview2(client(flags));
|
|
2197
2479
|
console.log("\n\u2705 Saved against your API key.");
|
|
2198
2480
|
signInHint(provider);
|
|
2199
2481
|
console.log("\n Then: oya start && oya goto https://example.com");
|
|
@@ -2201,47 +2483,48 @@ async function cmdInit(flags2) {
|
|
|
2201
2483
|
|
|
2202
2484
|
// src/commands/browsers.ts
|
|
2203
2485
|
import { spawn as spawn3 } from "child_process";
|
|
2204
|
-
function identityOptions(
|
|
2486
|
+
function identityOptions(flags) {
|
|
2205
2487
|
return {
|
|
2206
|
-
persona: flagStr(
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
idempotencyKey: flagStr(flags2, "idempotency-key")
|
|
2488
|
+
persona: flagStr(flags, "persona") || "default",
|
|
2489
|
+
provider: flagStr(flags, "provider"),
|
|
2490
|
+
wsUrl: flagStr(flags, "ws-url"),
|
|
2491
|
+
name: flagStr(flags, "name"),
|
|
2492
|
+
idempotencyKey: flagStr(flags, "idempotency-key")
|
|
2212
2493
|
};
|
|
2213
2494
|
}
|
|
2214
|
-
function governanceOptions(
|
|
2495
|
+
function governanceOptions(flags) {
|
|
2215
2496
|
return {
|
|
2216
|
-
queueMs: flagNum(
|
|
2217
|
-
budgetUsd: flagNum(
|
|
2218
|
-
governed:
|
|
2219
|
-
priority: flagStr(
|
|
2220
|
-
policy: flagStr(
|
|
2497
|
+
queueMs: flagNum(flags, "queue-ms"),
|
|
2498
|
+
budgetUsd: flagNum(flags, "budget-usd"),
|
|
2499
|
+
governed: flags.governed === true,
|
|
2500
|
+
priority: flagStr(flags, "priority"),
|
|
2501
|
+
policy: flagStr(flags, "policy") ? policyOf(flagStr(flags, "policy")) : void 0
|
|
2221
2502
|
};
|
|
2222
2503
|
}
|
|
2223
|
-
var
|
|
2224
|
-
|
|
2225
|
-
|
|
2504
|
+
var policyOf = (text) => parseJson(text, "--policy", `Quote it: --policy '{"allowedHosts":["example.com"]}'.`);
|
|
2505
|
+
var startOptions = (flags) => ({ ...identityOptions(flags), ...governanceOptions(flags) });
|
|
2506
|
+
async function cmdStart(flags) {
|
|
2507
|
+
const browser = await client(flags).browser.start(startOptions(flags));
|
|
2226
2508
|
const { id, provider, persona, cdpUrl } = browser;
|
|
2227
|
-
out(
|
|
2509
|
+
out(flags, { id, provider, persona, cdpUrl }, () => {
|
|
2228
2510
|
console.log(`\u2705 ${id}`);
|
|
2229
2511
|
console.log(` provider: ${provider} persona: ${persona}`);
|
|
2230
2512
|
if (cdpUrl) console.log(` cdp: ${cdpUrl}`);
|
|
2231
2513
|
});
|
|
2232
2514
|
}
|
|
2233
|
-
async function cmdGoto(
|
|
2234
|
-
const url =
|
|
2235
|
-
if (!url) throw
|
|
2236
|
-
const browser = await targetBrowser(client(
|
|
2515
|
+
async function cmdGoto(args, flags) {
|
|
2516
|
+
const url = args[0];
|
|
2517
|
+
if (!url) throw usage("oya goto needs an address: oya goto <url>.");
|
|
2518
|
+
const browser = await targetBrowser(client(flags), flags);
|
|
2237
2519
|
await browser.goto(url);
|
|
2238
|
-
console.log(`\u2705 ${browser.id} \u2192 ${url}`);
|
|
2520
|
+
out(flags, { id: browser.id, url }, () => console.log(`\u2705 ${browser.id} \u2192 ${url}`));
|
|
2239
2521
|
}
|
|
2240
|
-
async function cmdAsk(
|
|
2241
|
-
const prompt =
|
|
2242
|
-
if (!prompt) throw
|
|
2243
|
-
const browser = await targetBrowser(client(
|
|
2244
|
-
|
|
2522
|
+
async function cmdAsk(args, flags) {
|
|
2523
|
+
const prompt = args.join(" ");
|
|
2524
|
+
if (!prompt) throw usage('oya ask needs a prompt: oya ask "find the pricing page".');
|
|
2525
|
+
const browser = await targetBrowser(client(flags), flags);
|
|
2526
|
+
const answer = await browser.ask(prompt);
|
|
2527
|
+
out(flags, { id: browser.id, answer }, () => console.log(answer));
|
|
2245
2528
|
}
|
|
2246
2529
|
var DOT = { ok: "\u25CF", stale: "\u25D0", errors: "\u2717", dead: "\u25CB" };
|
|
2247
2530
|
function lsRow(b) {
|
|
@@ -2250,9 +2533,9 @@ function lsRow(b) {
|
|
|
2250
2533
|
const url = b.currentUrl.replace(/^https?:\/\//, "").slice(0, LsColumns.URL);
|
|
2251
2534
|
return `${DOT[b.health] || "\xB7"} ${b.id} ${provider} ${persona} ${b.name.padEnd(LsColumns.NAME)} ${b.commands}\xB7${b.errors} ${url}`;
|
|
2252
2535
|
}
|
|
2253
|
-
async function cmdLs(
|
|
2254
|
-
const all = await client(
|
|
2255
|
-
out(
|
|
2536
|
+
async function cmdLs(flags) {
|
|
2537
|
+
const all = await client(flags).browser.list();
|
|
2538
|
+
out(flags, all, () => {
|
|
2256
2539
|
if (!all.length) return console.log("No browsers running.");
|
|
2257
2540
|
for (const b of all) console.log(lsRow(b));
|
|
2258
2541
|
console.log(`
|
|
@@ -2264,13 +2547,19 @@ function rmLine(x) {
|
|
|
2264
2547
|
const note2 = x.sandboxRemoved === false ? ", sandbox NOT removed, check Oya Cloud" : removed;
|
|
2265
2548
|
return `${x.ok ? "\u2705" : "\u2717"} ${x.id}${note2}${x.error ? ` ${x.error}` : ""}`;
|
|
2266
2549
|
}
|
|
2267
|
-
async function cmdRm(
|
|
2268
|
-
const oya = client(
|
|
2269
|
-
if (!
|
|
2270
|
-
const r = await oya.browser.stop(
|
|
2271
|
-
|
|
2272
|
-
|
|
2550
|
+
async function cmdRm(args, flags) {
|
|
2551
|
+
const oya = client(flags);
|
|
2552
|
+
if (!flags.all && !args.length) throw usage("oya rm needs a browser id, or --all: oya rm <id>... | oya rm --all.");
|
|
2553
|
+
const r = await oya.browser.stop(flags.all ? "all" : args);
|
|
2554
|
+
out(flags, r, () => printStops(r.results));
|
|
2555
|
+
const failed = r.results.filter((x) => !x.ok).length;
|
|
2556
|
+
if (failed) throw stopsFailed(r.results.length - failed, r.results.length, !!flags.json);
|
|
2557
|
+
}
|
|
2558
|
+
function printStops(results) {
|
|
2559
|
+
for (const x of results) (x.ok ? console.log : console.error)(rmLine(x));
|
|
2560
|
+
console.log(`stopped ${results.filter((x) => x.ok).length} of ${results.length}`);
|
|
2273
2561
|
}
|
|
2562
|
+
var stopsFailed = (stopped, of, json) => new CliError(`stopped ${stopped} of ${of}`, "partial_failure", { shown: !json });
|
|
2274
2563
|
function activityLine(a) {
|
|
2275
2564
|
const error = a.error ? `, ${a.error}` : "";
|
|
2276
2565
|
return ` ${a.ok ? "\xB7" : "\u2717"} ${a.action.padEnd(StatusColumns.ACTION)} ${a.summary}${error} ${a.ms}ms`;
|
|
@@ -2284,21 +2573,21 @@ function printStatus(s) {
|
|
|
2284
2573
|
console.log(" recent:");
|
|
2285
2574
|
for (const a of s.activity.slice(0, StatusColumns.RECENT)) console.log(activityLine(a));
|
|
2286
2575
|
}
|
|
2287
|
-
async function cmdStatus(
|
|
2288
|
-
const browser = await targetBrowser(client(
|
|
2576
|
+
async function cmdStatus(flags) {
|
|
2577
|
+
const browser = await targetBrowser(client(flags), flags);
|
|
2289
2578
|
const s = await browser.status();
|
|
2290
|
-
out(
|
|
2579
|
+
out(flags, s, () => printStatus(s));
|
|
2291
2580
|
}
|
|
2292
2581
|
function opener(url) {
|
|
2293
2582
|
if (process.platform === "darwin") return ["open", [url]];
|
|
2294
2583
|
if (process.platform === "win32") return ["cmd", ["/c", "start", "", url]];
|
|
2295
2584
|
return ["xdg-open", [url]];
|
|
2296
2585
|
}
|
|
2297
|
-
async function cmdOpen(
|
|
2298
|
-
const browser = await targetBrowser(client(
|
|
2299
|
-
const [
|
|
2300
|
-
spawn3(
|
|
2301
|
-
console.log(`Opening ${browser.id}`);
|
|
2586
|
+
async function cmdOpen(flags) {
|
|
2587
|
+
const browser = await targetBrowser(client(flags), flags);
|
|
2588
|
+
const [command, args] = opener(browser.liveViewUrl());
|
|
2589
|
+
spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
2590
|
+
out(flags, { ok: true, id: browser.id }, () => console.log(`Opening the live view of ${browser.id}`));
|
|
2302
2591
|
}
|
|
2303
2592
|
|
|
2304
2593
|
// src/commands/personas.ts
|
|
@@ -2312,61 +2601,74 @@ var PLATFORM_ALIAS = {
|
|
|
2312
2601
|
linux: "Linux x86_64",
|
|
2313
2602
|
"linux x86_64": "Linux x86_64"
|
|
2314
2603
|
};
|
|
2315
|
-
function prefsFrom(
|
|
2316
|
-
const platformFlag = flagStr(
|
|
2604
|
+
function prefsFrom(flags) {
|
|
2605
|
+
const platformFlag = flagStr(flags, "platform");
|
|
2317
2606
|
return {
|
|
2318
2607
|
...platformFlag ? { platform: PLATFORM_ALIAS[platformFlag.toLowerCase()] || platformFlag } : {},
|
|
2319
|
-
...flagStr(
|
|
2320
|
-
...flagStr(
|
|
2608
|
+
...flagStr(flags, "tz") ? { timezone: flagStr(flags, "tz") } : {},
|
|
2609
|
+
...flagStr(flags, "locale") ? { locale: flagStr(flags, "locale") } : {}
|
|
2321
2610
|
};
|
|
2322
2611
|
}
|
|
2323
|
-
function createOptions(
|
|
2612
|
+
function createOptions(flags, rest, prefs) {
|
|
2324
2613
|
return {
|
|
2325
|
-
name: flagStr(
|
|
2614
|
+
name: flagStr(flags, "name") || rest[0],
|
|
2326
2615
|
prefs,
|
|
2327
|
-
proxy: flagStr(
|
|
2328
|
-
maxConcurrent:
|
|
2616
|
+
proxy: flagStr(flags, "geo") ? { geo: flagStr(flags, "geo") } : void 0,
|
|
2617
|
+
maxConcurrent: flagCount(flags, "max") ?? void 0
|
|
2329
2618
|
};
|
|
2330
2619
|
}
|
|
2331
|
-
var create = async (oya, rest,
|
|
2332
|
-
const prefs = prefsFrom(
|
|
2333
|
-
if (
|
|
2334
|
-
const created = await oya.personas.create(createOptions(
|
|
2620
|
+
var create = async (oya, rest, flags) => {
|
|
2621
|
+
const prefs = prefsFrom(flags);
|
|
2622
|
+
if (flags.preview) return preview(oya, prefs, flags);
|
|
2623
|
+
const created = await oya.personas.create(createOptions(flags, rest, prefs));
|
|
2335
2624
|
const fp = created.fingerprint;
|
|
2336
|
-
out(
|
|
2625
|
+
out(flags, created, () => console.log(`\u2705 ${created.id} ${created.name} ${fp.platform} \xB7 ${fp.timezone}`));
|
|
2337
2626
|
};
|
|
2338
|
-
async function preview(oya, prefs,
|
|
2627
|
+
async function preview(oya, prefs, flags) {
|
|
2339
2628
|
const fp = await oya.personas.preview(prefs);
|
|
2340
|
-
out(
|
|
2629
|
+
out(flags, fp, () => console.log(`${fp.platform} \xB7 ${fp.timezone} \xB7 ${fp.locale} \xB7 ${fp.screen} \xB7 ${fp.webgl}`));
|
|
2341
2630
|
}
|
|
2342
|
-
function editChanges(
|
|
2343
|
-
const max =
|
|
2631
|
+
function editChanges(flags) {
|
|
2632
|
+
const max = flagCount(flags, "max", { allowNone: true });
|
|
2344
2633
|
return {
|
|
2345
|
-
...flagStr(
|
|
2346
|
-
...max
|
|
2347
|
-
...flagStr(
|
|
2634
|
+
...flagStr(flags, "name") ? { name: flagStr(flags, "name") } : {},
|
|
2635
|
+
...max !== void 0 ? { maxConcurrent: max } : {},
|
|
2636
|
+
...flagStr(flags, "geo") ? { proxy: { geo: flagStr(flags, "geo") } } : {}
|
|
2348
2637
|
};
|
|
2349
2638
|
}
|
|
2350
|
-
var edit = async (oya, rest,
|
|
2351
|
-
if (!rest[0]) throw
|
|
2352
|
-
const updated = await oya.personas.update(rest[0], editChanges(
|
|
2353
|
-
out(
|
|
2639
|
+
var edit = async (oya, rest, flags) => {
|
|
2640
|
+
if (!rest[0]) throw usage("oya personas edit needs a persona id: oya personas edit <id> --name <n>.");
|
|
2641
|
+
const updated = await oya.personas.update(rest[0], editChanges(flags));
|
|
2642
|
+
out(flags, updated, () => console.log(`\u2705 ${updated.id} ${updated.name} cap ${updated.maxConcurrent ?? "\u221E"}`));
|
|
2354
2643
|
};
|
|
2355
|
-
var clone = async (oya, rest,
|
|
2356
|
-
if (!rest[0]) throw
|
|
2357
|
-
const c = await oya.personas.clone(rest[0], { name: flagStr(
|
|
2644
|
+
var clone = async (oya, rest, flags) => {
|
|
2645
|
+
if (!rest[0]) throw usage("oya personas clone needs a persona id: oya personas clone <id>.");
|
|
2646
|
+
const c = await oya.personas.clone(rest[0], { name: flagStr(flags, "name") });
|
|
2358
2647
|
const fp = c.fingerprint;
|
|
2359
2648
|
out(
|
|
2360
|
-
|
|
2649
|
+
flags,
|
|
2361
2650
|
c,
|
|
2362
2651
|
() => console.log(`\u2705 ${c.id} ${c.name} ${fp.platform} \xB7 ${fp.timezone} (new identity, same kind of device)`)
|
|
2363
2652
|
);
|
|
2364
2653
|
};
|
|
2365
|
-
var remove = async (oya, rest) => {
|
|
2366
|
-
if (!rest
|
|
2367
|
-
|
|
2368
|
-
|
|
2654
|
+
var remove = async (oya, rest, flags) => {
|
|
2655
|
+
if (!rest.length) throw usage("oya personas rm needs a persona id: oya personas rm <id>...");
|
|
2656
|
+
const removed = [];
|
|
2657
|
+
for (const id of rest) await removeOne(oya, id, flags, removed);
|
|
2658
|
+
out(flags, { ok: removed.length === rest.length, id: removed }, () => {
|
|
2659
|
+
});
|
|
2660
|
+
if (removed.length < rest.length) throw removalsFailed(removed.length, rest.length, !!flags.json);
|
|
2369
2661
|
};
|
|
2662
|
+
async function removeOne(oya, id, flags, removed) {
|
|
2663
|
+
try {
|
|
2664
|
+
await oya.personas.remove(id);
|
|
2665
|
+
removed.push(id);
|
|
2666
|
+
if (!flags.json) console.log(`\u2705 removed ${id}`);
|
|
2667
|
+
} catch (err) {
|
|
2668
|
+
if (!flags.json) console.error(`\u2717 ${id} ${err.message}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
var removalsFailed = (removed, of, json) => new CliError(`removed ${removed} of ${of}`, "partial_failure", { shown: !json });
|
|
2370
2672
|
function personaLine(p) {
|
|
2371
2673
|
const cap = p.maxConcurrent === null ? "\u221E" : String(p.maxConcurrent);
|
|
2372
2674
|
const where = p.exit ? ` via ${p.exit.label}` : p.proxy?.geo ? ` geo ${p.proxy.geo}` : "";
|
|
@@ -2374,58 +2676,117 @@ function personaLine(p) {
|
|
|
2374
2676
|
const device = `${p.fingerprint.platform} \xB7 ${p.fingerprint.timezone}`;
|
|
2375
2677
|
return `${p.id} ${p.name.padEnd(PERSONA_NAME_WIDTH)} ${p.activeBrowsers}/${cap} running ${device}${extras}`;
|
|
2376
2678
|
}
|
|
2377
|
-
var list = async (oya, _rest,
|
|
2679
|
+
var list = async (oya, _rest, flags) => {
|
|
2378
2680
|
const all = await oya.personas.list();
|
|
2379
|
-
out(
|
|
2681
|
+
out(flags, all, () => {
|
|
2380
2682
|
if (!all.length) return console.log("No personas yet, `oya personas new`.");
|
|
2381
2683
|
for (const p of all) console.log(personaLine(p));
|
|
2382
2684
|
});
|
|
2383
2685
|
};
|
|
2384
2686
|
var SUBCOMMANDS = { new: create, create, edit, clone, rm: remove, delete: remove };
|
|
2385
|
-
async function cmdPersonas(
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
await
|
|
2687
|
+
async function cmdPersonas(args, flags) {
|
|
2688
|
+
const [sub, ...rest] = args;
|
|
2689
|
+
if (sub !== void 0 && !Object.hasOwn(SUBCOMMANDS, sub))
|
|
2690
|
+
throw usage(`Unknown personas subcommand "${sub}". Use new, edit, clone or rm.`);
|
|
2691
|
+
await (sub === void 0 ? list : SUBCOMMANDS[sub])(client(flags), rest, flags);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/commands/cookies.ts
|
|
2695
|
+
import { chmodSync as chmodSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2696
|
+
var FORMATS = ["json", "playwright"];
|
|
2697
|
+
function formatOf(flags) {
|
|
2698
|
+
const format = flagStr(flags, "format") || "json";
|
|
2699
|
+
if (FORMATS.includes(format)) return format;
|
|
2700
|
+
throw usage(`Unknown format "${format}". Use --format json or --format playwright.`);
|
|
2701
|
+
}
|
|
2702
|
+
function writeJar(file, cookies) {
|
|
2703
|
+
writeFileSync5(file, JSON.stringify(cookies, null, JSON_INDENT2) + "\n", { mode: PRIVATE_FILE_MODE });
|
|
2704
|
+
chmodSync3(file, PRIVATE_FILE_MODE);
|
|
2705
|
+
}
|
|
2706
|
+
var exportJar = async (oya, rest, flags) => {
|
|
2707
|
+
if (!rest[0]) throw usage("oya cookies export needs a persona id: oya cookies export <persona> [--out <file>].");
|
|
2708
|
+
const cookies = await oya.personas.cookies(rest[0], formatOf(flags));
|
|
2709
|
+
if (!Array.isArray(cookies)) throw new Error("The server sent no cookie list, so nothing was written.");
|
|
2710
|
+
const file = flagStr(flags, "out");
|
|
2711
|
+
if (!file) return console.log(JSON.stringify(cookies, null, JSON_INDENT2));
|
|
2712
|
+
writeJar(file, cookies);
|
|
2713
|
+
const written = `\u2705 ${cookies.length} cookie${cookies.length === 1 ? "" : "s"} written to ${file}`;
|
|
2714
|
+
out(flags, { ok: true, id: rest[0], file, count: cookies.length }, () => console.log(written));
|
|
2715
|
+
};
|
|
2716
|
+
function cookiesIn(file) {
|
|
2717
|
+
const parsed = readJsonFile(file);
|
|
2718
|
+
const list2 = Array.isArray(parsed) ? parsed : parsed?.cookies;
|
|
2719
|
+
if (!Array.isArray(list2)) throw usage(`${file} holds no list of cookies.`);
|
|
2720
|
+
return list2;
|
|
2721
|
+
}
|
|
2722
|
+
var importJar = async (oya, rest, flags) => {
|
|
2723
|
+
if (!rest[0] || !rest[1])
|
|
2724
|
+
throw usage("oya cookies import needs a persona and a file: oya cookies import <persona> <file>.");
|
|
2725
|
+
const done = await oya.personas.importCookies(rest[0], cookiesIn(rest[1]));
|
|
2726
|
+
out(
|
|
2727
|
+
flags,
|
|
2728
|
+
done,
|
|
2729
|
+
() => console.log(`\u2705 ${done.imported} imported, ${done.skipped} skipped, ${done.total} in the jar`)
|
|
2730
|
+
);
|
|
2731
|
+
};
|
|
2732
|
+
var copy = async (oya, rest, flags) => {
|
|
2733
|
+
if (!rest[0] || !rest[1]) throw usage("oya cookies copy needs two persona ids: oya cookies copy <from> <to>.");
|
|
2734
|
+
const done = await oya.personas.copyCookies(rest[0], rest[1]);
|
|
2735
|
+
out(flags, done, () => console.log(`\u2705 ${done.imported} copied to ${rest[1]}, ${done.total} in its jar`));
|
|
2736
|
+
};
|
|
2737
|
+
var clear = async (oya, rest, flags) => {
|
|
2738
|
+
if (!rest[0]) throw usage("oya cookies clear needs a persona id: oya cookies clear <persona>.");
|
|
2739
|
+
const done = await oya.personas.clearCookies(rest[0]);
|
|
2740
|
+
out(flags, done ?? { ok: true, id: rest[0] }, () => console.log(`\u2705 cleared ${rest[0]}`));
|
|
2741
|
+
};
|
|
2742
|
+
var SUBCOMMANDS2 = { export: exportJar, import: importJar, copy, clear };
|
|
2743
|
+
async function cmdCookies(args, flags) {
|
|
2744
|
+
const [sub, ...rest] = args;
|
|
2745
|
+
if (sub === void 0 || !Object.hasOwn(SUBCOMMANDS2, sub))
|
|
2746
|
+
throw usage(`Unknown cookies subcommand "${sub ?? ""}". Use export, import, copy or clear.`);
|
|
2747
|
+
await SUBCOMMANDS2[sub](client(flags), rest, flags);
|
|
2390
2748
|
}
|
|
2391
2749
|
|
|
2392
2750
|
// src/commands/settings.ts
|
|
2393
|
-
function parsePairs(
|
|
2751
|
+
function parsePairs(args) {
|
|
2394
2752
|
const updates = {};
|
|
2395
|
-
for (const pair of
|
|
2753
|
+
for (const pair of args) {
|
|
2396
2754
|
const index = pair.indexOf("=");
|
|
2397
|
-
if (index < 1) throw
|
|
2755
|
+
if (index < 1) throw usage(`oya config takes settings as key=value, not "${pair}".`);
|
|
2398
2756
|
updates[pair.slice(0, index)] = pair.slice(index + 1);
|
|
2399
2757
|
}
|
|
2400
2758
|
return updates;
|
|
2401
2759
|
}
|
|
2402
|
-
async function cmdConfig(
|
|
2403
|
-
const oya = client(
|
|
2404
|
-
if (!
|
|
2760
|
+
async function cmdConfig(args, flags) {
|
|
2761
|
+
const oya = client(flags);
|
|
2762
|
+
if (!args.length) {
|
|
2405
2763
|
const current = await oya.config.get();
|
|
2406
|
-
return out(
|
|
2764
|
+
return out(flags, current, () => printJson(current));
|
|
2407
2765
|
}
|
|
2408
|
-
const updates = parsePairs(
|
|
2409
|
-
await oya.config.set(updates);
|
|
2410
|
-
console.log(`\u2705 updated ${Object.keys(updates).join(", ")}`);
|
|
2766
|
+
const updates = parsePairs(args);
|
|
2767
|
+
const saved = await oya.config.set(updates);
|
|
2768
|
+
out(flags, saved, () => console.log(`\u2705 updated ${Object.keys(updates).join(", ")}`));
|
|
2411
2769
|
}
|
|
2412
|
-
async function cmdUsage(
|
|
2413
|
-
printJson(await client(
|
|
2770
|
+
async function cmdUsage(flags) {
|
|
2771
|
+
printJson(await client(flags).usage());
|
|
2414
2772
|
}
|
|
2415
|
-
async function cmdWhoami() {
|
|
2416
|
-
|
|
2773
|
+
async function cmdWhoami(flags) {
|
|
2774
|
+
const { apiKey, apiKeyFrom, baseUrl, baseUrlFrom, savedKey } = origin2(flags);
|
|
2775
|
+
const tail = apiKey.length >= MIN_KEY_FOR_TAIL ? { apiKeyEnds: apiKey.slice(-KEY_TAIL) } : {};
|
|
2776
|
+
const ignored = savedKey && apiKeyFrom !== "saved" ? { savedKeyIgnored: true } : {};
|
|
2777
|
+
printJson({ baseUrl, baseUrlFrom, apiKeyFrom, ...tail, ...ignored, configPath });
|
|
2417
2778
|
}
|
|
2418
2779
|
|
|
2419
2780
|
// src/commands/stealth.ts
|
|
2420
2781
|
import { spawn as spawn4 } from "child_process";
|
|
2421
2782
|
var HARNESS = "tests/integration/stealth.test.js";
|
|
2422
|
-
var spawnHarness = (
|
|
2783
|
+
var spawnHarness = (flags) => spawn4("node", [HARNESS, ...flags.live ? ["--live"] : []], {
|
|
2423
2784
|
cwd: process.env.OYA_SERVER_DIR || "server",
|
|
2424
2785
|
stdio: "inherit"
|
|
2425
2786
|
});
|
|
2426
|
-
function cmdStealthTest(
|
|
2787
|
+
function cmdStealthTest(flags) {
|
|
2427
2788
|
return new Promise((resolve, reject) => {
|
|
2428
|
-
const child = spawnHarness(
|
|
2789
|
+
const child = spawnHarness(flags);
|
|
2429
2790
|
const missing = "Could not find the stealth harness. Run it from a checkout, or set OYA_SERVER_DIR.";
|
|
2430
2791
|
child.on("error", () => reject(new Error(missing)));
|
|
2431
2792
|
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`stealth test exited ${code}`)));
|
|
@@ -2434,163 +2795,301 @@ function cmdStealthTest(flags2) {
|
|
|
2434
2795
|
|
|
2435
2796
|
// src/commands/control.ts
|
|
2436
2797
|
var ROLES = ["viewer", "operator", "administrator"];
|
|
2437
|
-
function required(
|
|
2438
|
-
if (!
|
|
2439
|
-
return
|
|
2798
|
+
function required(args, i, command, what) {
|
|
2799
|
+
if (!args[i]) throw usage(`oya ${command} needs ${what}: oya ${command} <${what.split(" ").pop()}>.`);
|
|
2800
|
+
return args[i];
|
|
2440
2801
|
}
|
|
2441
|
-
function roleFlag(
|
|
2442
|
-
const role = flagStr(
|
|
2443
|
-
if (!ROLES.includes(role)) throw
|
|
2802
|
+
function roleFlag(flags) {
|
|
2803
|
+
const role = flagStr(flags, "role") || "operator";
|
|
2804
|
+
if (!ROLES.includes(role)) throw usage(`--role must be viewer, operator or administrator, not "${role}".`);
|
|
2444
2805
|
return role;
|
|
2445
2806
|
}
|
|
2446
|
-
function subcommand(table, sub, call2,
|
|
2447
|
-
if (!Object.hasOwn(table, sub))
|
|
2807
|
+
function subcommand(table, sub, call2, command) {
|
|
2808
|
+
if (!Object.hasOwn(table, sub))
|
|
2809
|
+
throw usage(`Unknown ${command} subcommand "${sub}". Use ${orList(Object.keys(table))}.`);
|
|
2448
2810
|
return table[sub](call2);
|
|
2449
2811
|
}
|
|
2812
|
+
var orList = (names) => names.join(", ").replace(/, ([^,]*)$/, " or $1");
|
|
2450
2813
|
var members = (call2) => {
|
|
2451
2814
|
if (!call2.args[0]) return call2.c.members();
|
|
2452
2815
|
const table = {
|
|
2453
|
-
remove: ({ c, args
|
|
2454
|
-
invite: ({ c, flags
|
|
2816
|
+
remove: ({ c, args }) => c.removeMember(required(args, 1, "members remove", "a user id")),
|
|
2817
|
+
invite: ({ c, flags }) => c.inviteMember(roleFlag(flags))
|
|
2455
2818
|
};
|
|
2456
|
-
return subcommand(table, call2.args[0], call2, "
|
|
2819
|
+
return subcommand(table, call2.args[0], call2, "members");
|
|
2457
2820
|
};
|
|
2458
2821
|
var credential = (call2) => {
|
|
2459
2822
|
const table = {
|
|
2460
|
-
revoke: ({ c, args
|
|
2461
|
-
new: ({ c, flags
|
|
2823
|
+
revoke: ({ c, args }) => c.revokeCredential(required(args, 1, "credential revoke", "a credential id")),
|
|
2824
|
+
new: ({ c, flags }) => c.createCredential({ role: roleFlag(flags), label: flagStr(flags, "label") })
|
|
2462
2825
|
};
|
|
2463
|
-
return subcommand(table, required(call2.args
|
|
2826
|
+
return subcommand(table, required(call2.args, 0, "credential", "new or revoke"), call2, "credential");
|
|
2464
2827
|
};
|
|
2465
2828
|
var webhook = (call2) => {
|
|
2466
2829
|
const table = {
|
|
2467
|
-
new: ({ c, args
|
|
2468
|
-
remove: ({ c, args
|
|
2469
|
-
replay: ({ c, args
|
|
2830
|
+
new: ({ c, args }) => c.createWebhook(required(args, 1, "webhook new", "an https url")),
|
|
2831
|
+
remove: ({ c, args }) => c.removeWebhook(required(args, 1, "webhook remove", "a webhook id")),
|
|
2832
|
+
replay: ({ c, args }) => c.replayDelivery(required(args, 1, "webhook replay", "a delivery id"))
|
|
2470
2833
|
};
|
|
2471
|
-
return subcommand(table, required(call2.args
|
|
2834
|
+
return subcommand(table, required(call2.args, 0, "webhook", "new, remove or replay"), call2, "webhook");
|
|
2472
2835
|
};
|
|
2473
|
-
|
|
2474
|
-
|
|
2836
|
+
function recoverOptions(flags) {
|
|
2837
|
+
const wsUrl = flagStr(flags, "ws-url");
|
|
2838
|
+
if (wsUrl && flags.replace !== true)
|
|
2839
|
+
throw usage("--ws-url is only used with --replace; recovering in place keeps the endpoint it had.");
|
|
2840
|
+
return { replace: flags.replace === true, ...wsUrl ? { wsUrl } : {} };
|
|
2841
|
+
}
|
|
2842
|
+
var projectSettings = (text) => parseJson(
|
|
2843
|
+
text,
|
|
2844
|
+
"The settings for oya project",
|
|
2845
|
+
`Quote them: oya project '{"retentionDays":7}'.`,
|
|
2846
|
+
"are"
|
|
2847
|
+
);
|
|
2848
|
+
var takeover = ({ c, command, args }) => c.takeover(
|
|
2849
|
+
required(args, 0, command, "a session id"),
|
|
2850
|
+
command === "takeover" ? "acquire" : command
|
|
2851
|
+
);
|
|
2852
|
+
var CONTROL_COMMANDS = {
|
|
2475
2853
|
control: ({ c }) => c.overview(),
|
|
2476
|
-
sessions: ({ c, args
|
|
2477
|
-
cancel: ({ c, args
|
|
2478
|
-
recover: ({ c, args
|
|
2854
|
+
sessions: ({ c, args }) => args[0] ? c.session(args[0]) : c.sessions(),
|
|
2855
|
+
cancel: ({ c, args }) => c.cancel(required(args, 0, "cancel", "a session id")),
|
|
2856
|
+
recover: ({ c, args, flags }) => c.recover(required(args, 0, "recover", "a session id"), recoverOptions(flags)),
|
|
2479
2857
|
members,
|
|
2480
|
-
stop: ({ c, args
|
|
2858
|
+
stop: ({ c, args, flags }) => c.stop(required(args, 0, "stop", "a session id"), flags.force === true),
|
|
2481
2859
|
takeover,
|
|
2482
2860
|
release: takeover,
|
|
2483
2861
|
resume: takeover,
|
|
2484
|
-
events: ({ c, flags
|
|
2485
|
-
project: ({ c, args
|
|
2862
|
+
events: ({ c, flags }) => c.events(flagNum(flags, "after") ?? 0),
|
|
2863
|
+
project: ({ c, args }) => c.settings(projectSettings(required(args, 0, "project", "settings as JSON"))),
|
|
2486
2864
|
credential,
|
|
2487
2865
|
webhook
|
|
2488
2866
|
};
|
|
2489
|
-
function isControlCommand(
|
|
2490
|
-
if (
|
|
2491
|
-
return Object.hasOwn(
|
|
2867
|
+
function isControlCommand(command, flags) {
|
|
2868
|
+
if (command === "stop") return flags.force === true;
|
|
2869
|
+
return Object.hasOwn(CONTROL_COMMANDS, command);
|
|
2492
2870
|
}
|
|
2493
|
-
async function runControl(
|
|
2494
|
-
const c = client(
|
|
2495
|
-
printJson(await
|
|
2871
|
+
async function runControl(command, args, flags) {
|
|
2872
|
+
const c = client(flags).control;
|
|
2873
|
+
printJson(await CONTROL_COMMANDS[command]({ c, command, args, flags }));
|
|
2496
2874
|
}
|
|
2497
2875
|
|
|
2498
2876
|
// src/help.ts
|
|
2499
2877
|
var HELP = `oya, thousands of browsers, one API
|
|
2500
2878
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
oya
|
|
2505
|
-
oya
|
|
2506
|
-
oya
|
|
2507
|
-
oya
|
|
2508
|
-
oya ls List running browsers
|
|
2509
|
-
oya rm <id> | --all Stop browsers
|
|
2510
|
-
oya personas Identities: fingerprint + cookies + proxy
|
|
2511
|
-
oya personas new [name] --platform Win32|MacIntel|Linux --tz <zone> --locale <l> --max <n>
|
|
2512
|
-
oya personas edit <id> --name <n> --max <n> --geo <cc>
|
|
2513
|
-
oya personas clone|rm <id> A new device of the same kind \xB7 delete
|
|
2514
|
-
oya status [--id <id>] Health, counters and what it has been doing
|
|
2515
|
-
oya open [--id <id>] Open the live view in your browser
|
|
2516
|
-
oya config [key=value ...] Show or change this key's settings
|
|
2517
|
-
oya control Durable project overview
|
|
2518
|
-
oya sessions [id] All sessions, including pending cleanup
|
|
2519
|
-
oya stop <id> --force Stop despite a profile-save error, or reconcile
|
|
2520
|
-
oya takeover <id> Acquire human control
|
|
2521
|
-
oya release <id> Release human control, leaving the agent paused
|
|
2522
|
-
oya resume <id> Acknowledge agent resume
|
|
2523
|
-
oya events [--after <cursor>] Read durable lifecycle events
|
|
2524
|
-
oya project <settings-json> Update limits, rate cards and retention
|
|
2525
|
-
oya start --governed --provider oya-selfhosted [--queue-ms 30000]
|
|
2526
|
-
[--budget-usd 5] [--policy JSON] [--idempotency-key ID]
|
|
2527
|
-
oya cancel <id> Cancel queued or provisioning work
|
|
2528
|
-
oya recover <id> [--replace] Explicitly recover or replace a session
|
|
2529
|
-
oya members [invite|remove] List members, invite, or remove a user
|
|
2530
|
-
oya credential new [--role viewer|operator|administrator]
|
|
2531
|
-
oya credential revoke <id> Revoke a service credential
|
|
2532
|
-
oya webhook new <https-url> Register a signed event webhook
|
|
2533
|
-
oya webhook remove <id> Disable a webhook
|
|
2534
|
-
oya webhook replay <id> Replay a delivery
|
|
2535
|
-
oya usage What this key has spent
|
|
2536
|
-
oya stealth-test [--live] Score this deployment against bot detectors
|
|
2879
|
+
Usage: oya <command> [arguments] [options]
|
|
2880
|
+
|
|
2881
|
+
Set up
|
|
2882
|
+
oya install [--dry-run] [--config <file>] Stand up a self-hosted control plane
|
|
2883
|
+
oya login [--key <key>] [--url <url>] Save an API key for this machine
|
|
2884
|
+
oya init Set your model, browser provider and sign-ins
|
|
2885
|
+
oya whoami Which control plane and key are in use, and from where
|
|
2537
2886
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2887
|
+
Browsers
|
|
2888
|
+
oya start Start a browser and print its id
|
|
2889
|
+
[--persona <id|auto>] [--name <name>] [--idempotency-key <key>]
|
|
2890
|
+
[--provider cdp --ws-url ws://127.0.0.1:9222] on a Chrome you run
|
|
2891
|
+
[--governed] [--queue-ms 30000] [--budget-usd 5] [--priority low|normal|high] [--policy '<json>']
|
|
2892
|
+
oya goto <url> [--id <id>] Navigate (defaults to the newest browser)
|
|
2893
|
+
oya ask "<prompt>" [--id <id>] Drive it in plain language
|
|
2894
|
+
oya ls | list List running browsers
|
|
2895
|
+
oya status [--id <id>] Health, counters and what it has been doing
|
|
2896
|
+
oya open [--id <id>] Open the live view in your browser
|
|
2897
|
+
oya rm | stop <id>... | --all Stop browsers
|
|
2898
|
+
|
|
2899
|
+
Personas and cookies
|
|
2900
|
+
oya personas List identities: fingerprint + cookies + proxy
|
|
2901
|
+
oya personas new | create [name] [--name <n>] [--platform Win32|MacIntel|Linux] [--tz <zone>]
|
|
2902
|
+
[--locale <l>] [--max <n>] [--geo <cc>] [--preview]
|
|
2903
|
+
oya personas edit <id> [--name <n>] [--max <n|none>] [--geo <cc>]
|
|
2904
|
+
oya personas clone <id> [--name <n>] A new device of the same kind
|
|
2905
|
+
oya personas rm | delete <id>... Delete personas
|
|
2906
|
+
oya cookies export <persona> Logins to stdout; [--out <file>] [--format json|playwright]
|
|
2907
|
+
oya cookies import <persona> <file> Logins from a JSON file
|
|
2908
|
+
oya cookies copy <from> <to> One persona's logins into another
|
|
2909
|
+
oya cookies clear <persona> Delete a persona's logins
|
|
2910
|
+
|
|
2911
|
+
Account
|
|
2912
|
+
oya config [key=value ...] Show or change this key's settings
|
|
2913
|
+
oya usage What this key has spent
|
|
2914
|
+
oya stealth-test [--live] Score this deployment against bot detectors
|
|
2915
|
+
|
|
2916
|
+
Control plane (always prints JSON)
|
|
2917
|
+
oya control Durable project overview
|
|
2918
|
+
oya sessions [<id>] All sessions, including pending cleanup
|
|
2919
|
+
oya cancel <id> Stop a session in any state; prints its final state
|
|
2920
|
+
oya stop <id> --force Stop despite a profile-save error, or reconcile
|
|
2921
|
+
oya recover <id> [--replace [--ws-url ws://host:9222]]
|
|
2922
|
+
Recover in place, or replace it (a cdp session needs --ws-url)
|
|
2923
|
+
oya takeover <id> Take human control
|
|
2924
|
+
oya release <id> Hand control back, leaving the agent paused
|
|
2925
|
+
oya resume <id> Let the agent carry on
|
|
2926
|
+
oya events [--after <cursor>] Read durable lifecycle events
|
|
2927
|
+
oya project '<settings-json>' Update limits, rate cards and retention
|
|
2928
|
+
oya members List the owner and members
|
|
2929
|
+
oya members invite [--role <role>] An invitation code; role viewer|operator|administrator
|
|
2930
|
+
oya members remove <user> Remove a member
|
|
2931
|
+
oya credential new [--role <role>] [--label <text>] Mint a service credential (shown once)
|
|
2932
|
+
oya credential revoke <id> Revoke a service credential
|
|
2933
|
+
oya webhook new <https-url> Register a signed event webhook
|
|
2934
|
+
oya webhook remove <id> Disable a webhook
|
|
2935
|
+
oya webhook replay <id> Send a delivery again
|
|
2936
|
+
|
|
2937
|
+
Options
|
|
2938
|
+
--json One JSON document on stdout; errors as JSON on stderr
|
|
2939
|
+
--url <url> Control plane for this call (else OYA_BASE_URL, else the one oya login saved)
|
|
2940
|
+
--key <key> API key for this call (else OYA_API_KEY, else the one oya login saved)
|
|
2941
|
+
--debug On failure, also print the status, the response body and the stack
|
|
2942
|
+
--version Print the version
|
|
2943
|
+
--help, -h This text; the command is not run
|
|
2944
|
+
|
|
2945
|
+
Exit codes: 0 done \xB7 1 failed, or no answer \xB7 2 usage error, nothing was sent
|
|
2946
|
+
Config: ${configPath}
|
|
2540
2947
|
`;
|
|
2541
2948
|
|
|
2542
2949
|
// src/commands/index.ts
|
|
2950
|
+
import { createRequire } from "module";
|
|
2543
2951
|
var help = async () => console.log(HELP);
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2952
|
+
function packageVersion() {
|
|
2953
|
+
const require2 = createRequire(import.meta.url);
|
|
2954
|
+
for (const path of ["../package.json", "../../package.json"]) {
|
|
2955
|
+
try {
|
|
2956
|
+
return require2(path).version;
|
|
2957
|
+
} catch {
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
return "unknown";
|
|
2961
|
+
}
|
|
2962
|
+
var VERSION = packageVersion();
|
|
2963
|
+
var version = async (_args, flags) => out(flags, { version: VERSION }, () => console.log(`oya ${VERSION}`));
|
|
2964
|
+
var noJson = (command, run2, instead) => async (args, flags) => {
|
|
2965
|
+
if (flags.json) throw usage(`oya ${command} has no --json output. ${instead}`);
|
|
2966
|
+
await run2(args, flags);
|
|
2967
|
+
};
|
|
2968
|
+
var COMMANDS = {
|
|
2969
|
+
install: noJson("install", (_args, flags) => cmdInstall(flags), "For an unattended install, use --config <file>."),
|
|
2970
|
+
login: (_args, flags) => cmdLogin(flags),
|
|
2971
|
+
init: noJson("init", (_args, flags) => cmdInit(flags), "Use oya config key=value --json."),
|
|
2972
|
+
start: (_args, flags) => cmdStart(flags),
|
|
2549
2973
|
goto: cmdGoto,
|
|
2550
2974
|
ask: cmdAsk,
|
|
2551
|
-
ls: (_args,
|
|
2552
|
-
list: (_args,
|
|
2553
|
-
status: (_args,
|
|
2975
|
+
ls: (_args, flags) => cmdLs(flags),
|
|
2976
|
+
list: (_args, flags) => cmdLs(flags),
|
|
2977
|
+
status: (_args, flags) => cmdStatus(flags),
|
|
2554
2978
|
rm: cmdRm,
|
|
2555
2979
|
stop: cmdRm,
|
|
2556
2980
|
personas: cmdPersonas,
|
|
2557
|
-
|
|
2981
|
+
cookies: cmdCookies,
|
|
2982
|
+
open: (_args, flags) => cmdOpen(flags),
|
|
2558
2983
|
config: cmdConfig,
|
|
2559
|
-
usage: (_args,
|
|
2560
|
-
"stealth-test": (_args,
|
|
2561
|
-
whoami: () => cmdWhoami(),
|
|
2984
|
+
usage: (_args, flags) => cmdUsage(flags),
|
|
2985
|
+
"stealth-test": noJson("stealth-test", (_args, flags) => cmdStealthTest(flags), "Read its report from the terminal."),
|
|
2986
|
+
whoami: (_args, flags) => cmdWhoami(flags),
|
|
2987
|
+
version,
|
|
2562
2988
|
help,
|
|
2563
2989
|
"--help": help,
|
|
2564
2990
|
"-h": help
|
|
2565
2991
|
};
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2992
|
+
var unknown = (command) => usage(`Unknown command "${command}". Run oya help for the list.`);
|
|
2993
|
+
var wantsHelp = (args, flags) => Object.hasOwn(flags, "help") || args.includes("-h");
|
|
2994
|
+
async function dispatch(command, args, flags) {
|
|
2995
|
+
if (wantsHelp(args, flags)) return help(args, flags);
|
|
2996
|
+
if (flags.version || command === "--version") return version(args, flags);
|
|
2997
|
+
if (isControlCommand(command, flags)) return runControl(command, args, flags);
|
|
2998
|
+
if (!Object.hasOwn(COMMANDS, command)) throw unknown(command);
|
|
2999
|
+
await COMMANDS[command](args, flags);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// src/failure.ts
|
|
3003
|
+
var USAGE_CODES = /* @__PURE__ */ new Set(["usage", "invalid_json"]);
|
|
3004
|
+
function fail2(err, argv2) {
|
|
3005
|
+
const failure2 = describe(err, origin(sourceFlags(argv2)));
|
|
3006
|
+
process.exitCode = USAGE_CODES.has(failure2.code) ? ExitCode.USAGE : ExitCode.FAILED;
|
|
3007
|
+
if (argv2.includes("--json")) printJson2(failure2, err, debugging(argv2));
|
|
3008
|
+
else printPlain(failure2, err, debugging(argv2));
|
|
3009
|
+
}
|
|
3010
|
+
var debugging = (argv2) => argv2.includes("--debug") || process.env.OYA_DEBUG === "1";
|
|
3011
|
+
function sourceFlags(argv2) {
|
|
3012
|
+
const valueOf2 = (name) => {
|
|
3013
|
+
const at = argv2.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
3014
|
+
if (at < 0) return void 0;
|
|
3015
|
+
return argv2[at].includes("=") ? argv2[at].split("=").slice(1).join("=") : argv2[at + 1];
|
|
3016
|
+
};
|
|
3017
|
+
return { apiKey: valueOf2("key"), baseUrl: valueOf2("url") };
|
|
3018
|
+
}
|
|
3019
|
+
function describe(err, origin3) {
|
|
3020
|
+
if (err instanceof CliError)
|
|
3021
|
+
return { message: err.message, code: err.code, status: 0, hint: err.hint, shown: err.shown };
|
|
3022
|
+
const e = err;
|
|
3023
|
+
if (typeof e?.status !== "number")
|
|
3024
|
+
return { message: String(err?.message ?? err), code: "internal", status: 0 };
|
|
3025
|
+
return fromServer(e, origin3);
|
|
3026
|
+
}
|
|
3027
|
+
function fromServer(e, origin3) {
|
|
3028
|
+
const body = e.body ?? {};
|
|
3029
|
+
const code = typeof body.code === "string" ? body.code : e.status === 0 ? "unreachable" : "http_error";
|
|
3030
|
+
const base = { code, status: e.status, browserId: typeof body.browserId === "string" ? body.browserId : void 0 };
|
|
3031
|
+
if (e.status === 0 && code === "unreachable") return { ...base, ...unreachable2(origin3, body.cause) };
|
|
3032
|
+
if (isInvalidKey(e, body)) return { ...base, ...invalidKey2(origin3) };
|
|
3033
|
+
return { ...base, message: renamed(e.message, body, origin3), hint: hintFor(e.status, code, base.browserId) };
|
|
3034
|
+
}
|
|
3035
|
+
function renamed(message, body, origin3) {
|
|
3036
|
+
if (body.field !== "baseUrl" || !message.startsWith("baseUrl ")) return message;
|
|
3037
|
+
const source = origin3.baseUrlFrom === "saved" ? "The URL saved by oya login" : origin3.baseUrlFrom;
|
|
3038
|
+
return `${source}${message.slice("baseUrl".length)}`;
|
|
3039
|
+
}
|
|
3040
|
+
var isInvalidKey = (e, body) => e.status === Status2.UNAUTHORIZED || e.status === Status2.FORBIDDEN && body.error === "Invalid API key";
|
|
3041
|
+
var CAUSES = { ECONNREFUSED: "nothing is listening there", ENOTFOUND: "no such host" };
|
|
3042
|
+
var URL_HINTS = {
|
|
3043
|
+
"--url": "The URL came from --url. Start the server there, or pass the right --url.",
|
|
3044
|
+
OYA_BASE_URL: "The URL came from OYA_BASE_URL. Start the server there, or export the right URL.",
|
|
3045
|
+
saved: "The URL was saved by oya login. Start the server there, or run oya login --url <url>.",
|
|
3046
|
+
default: "That is the hosted default. Check your connection, or pass --url for a self-hosted server."
|
|
3047
|
+
};
|
|
3048
|
+
function unreachable2(origin3, cause) {
|
|
3049
|
+
const why = typeof cause === "string" ? Object.hasOwn(CAUSES, cause) ? CAUSES[cause] : cause : "no answer";
|
|
3050
|
+
return { message: `Could not reach ${origin3.baseUrl}: ${why}.`, hint: URL_HINTS[origin3.baseUrlFrom] };
|
|
3051
|
+
}
|
|
3052
|
+
var KEY_NAMES = {
|
|
3053
|
+
"--key": "--key",
|
|
3054
|
+
OYA_API_KEY: "OYA_API_KEY",
|
|
3055
|
+
saved: "oya login",
|
|
3056
|
+
none: "nowhere"
|
|
3057
|
+
};
|
|
3058
|
+
function invalidKey2(origin3) {
|
|
3059
|
+
const message = `Invalid API key for ${origin3.baseUrl} (the key came from ${KEY_NAMES[origin3.apiKeyFrom]}).`;
|
|
3060
|
+
const overrides = origin3.apiKeyFrom === "OYA_API_KEY" && origin3.savedKey;
|
|
3061
|
+
const hint2 = overrides ? "OYA_API_KEY overrides the key saved by oya login. Unset it, or export the right key." : "Run oya login to save a new key.";
|
|
3062
|
+
return { message, hint: hint2 };
|
|
3063
|
+
}
|
|
3064
|
+
function hintFor(status, code, holder) {
|
|
3065
|
+
if (status === Status2.TOO_MANY_REQUESTS)
|
|
3066
|
+
return "A quota or a persona concurrency cap. oya personas shows what is running.";
|
|
3067
|
+
if (holder) return `oya stop ${holder} frees it; oya goto <url> --id ${holder} drives it.`;
|
|
3068
|
+
if (code === "replacement_endpoint_required")
|
|
3069
|
+
return "Add --ws-url ws://host:9222, the Chrome the replacement runs on.";
|
|
3070
|
+
return void 0;
|
|
2571
3071
|
}
|
|
2572
|
-
|
|
2573
|
-
if (
|
|
2574
|
-
if (!
|
|
2575
|
-
|
|
3072
|
+
function printPlain(failure2, err, debug) {
|
|
3073
|
+
if (!failure2.shown) console.error(`\u2717 ${failure2.message}`);
|
|
3074
|
+
if (failure2.hint && !failure2.shown) console.error(` ${failure2.hint}`);
|
|
3075
|
+
if (!debug) return;
|
|
3076
|
+
console.error(` status ${failure2.status} \xB7 code ${failure2.code}`);
|
|
3077
|
+
if (err?.body !== void 0) console.error(JSON.stringify(err.body));
|
|
3078
|
+
if (err?.stack) console.error(err.stack);
|
|
3079
|
+
}
|
|
3080
|
+
function printJson2(failure2, err, debug) {
|
|
3081
|
+
const { message, code, status, browserId } = failure2;
|
|
3082
|
+
const stack = debug ? { stack: err?.stack } : {};
|
|
3083
|
+
console.error(JSON.stringify({ error: message, code, status, ...browserId ? { browserId } : {}, ...stack }));
|
|
2576
3084
|
}
|
|
2577
3085
|
|
|
2578
3086
|
// src/index.ts
|
|
2579
|
-
|
|
2580
|
-
const error = err;
|
|
2581
|
-
console.error(`
|
|
2582
|
-
\u2717 ${error.message}`);
|
|
2583
|
-
if (error.status === Status2.UNAUTHORIZED) console.error(" The API key was rejected. Run `oya login`.");
|
|
2584
|
-
if (error.status === Status2.TOO_MANY_REQUESTS) {
|
|
2585
|
-
console.error(" A quota or a persona concurrency cap. `oya personas` shows what is running.");
|
|
2586
|
-
}
|
|
2587
|
-
process.exit(1);
|
|
2588
|
-
}
|
|
2589
|
-
var { command, args, flags } = parse(process.argv.slice(ARGV_SKIP));
|
|
3087
|
+
var argv = process.argv.slice(ARGV_SKIP);
|
|
2590
3088
|
try {
|
|
3089
|
+
const { command, args, flags } = parse(argv);
|
|
2591
3090
|
await dispatch(command, args, flags);
|
|
2592
3091
|
} catch (err) {
|
|
2593
|
-
fail2(err);
|
|
3092
|
+
fail2(err, argv);
|
|
2594
3093
|
} finally {
|
|
2595
3094
|
closePrompts();
|
|
2596
3095
|
}
|