verikun 0.4.1 → 0.7.0
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 +145 -27
- package/dist/agent/cost.js +21 -10
- package/dist/agent/engine.js +8 -8
- package/dist/agent/openai.js +243 -0
- package/dist/agent/remote.js +166 -0
- package/dist/args.js +2 -0
- package/dist/bin/verikun.js +0 -0
- package/dist/cli.js +425 -108
- package/dist/drivers/adb.js +12 -0
- package/dist/drivers/index.js +5 -5
- package/dist/drivers/ios.js +358 -0
- package/dist/report.js +93 -3
- package/dist/rpc.js +46 -0
- package/dist/run.js +101 -1
- package/dist/server.js +349 -0
- package/dist/suite.js +152 -0
- package/dist/ui/ios-parse.js +149 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The remote execution backend: `vk ai/suite/install --server <url>` run their
|
|
3
|
+
// device work through a `vk server` sitting next to the device, over HTTP+JSON
|
|
4
|
+
// (Node's global fetch — no SDK, zero runtime deps). One validated leaf command =
|
|
5
|
+
// ONE round-trip: the server keeps the whole auto-wait/dump loop on its side.
|
|
6
|
+
//
|
|
7
|
+
// The step detail each exec produces (selector, tier, resolved element, failure
|
|
8
|
+
// evidence) comes back in the response and is handed to `onStep`, which splices it
|
|
9
|
+
// into the CALLER's local run — so a remote run archives a report identical to a
|
|
10
|
+
// local one. Recording stays a caller concern: this module never touches ./.verikun.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.pingServer = pingServer;
|
|
13
|
+
exports.createRemoteBackend = createRemoteBackend;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const errors_1 = require("../errors");
|
|
18
|
+
const rpc_1 = require("../rpc");
|
|
19
|
+
// Per-call ceilings. exec is generous: a single leaf may legitimately block for its
|
|
20
|
+
// whole auto-wait window or an explicit `wait --timeout`, plus device time.
|
|
21
|
+
const HEALTH_TIMEOUT_MS = 10_000;
|
|
22
|
+
const ELEMENTS_TIMEOUT_MS = 60_000;
|
|
23
|
+
const EXEC_TIMEOUT_MS = 10 * 60_000;
|
|
24
|
+
const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
25
|
+
const trimUrl = (url) => url.replace(/\/+$/, '');
|
|
26
|
+
function describeStatus(status, body, url) {
|
|
27
|
+
const detail = body?.error ? `: ${body.error}` : '';
|
|
28
|
+
if (status === 401) {
|
|
29
|
+
return new errors_1.CliError(`verikun server rejected the auth key (401)${detail}. Check --auth-key / VERIKUN_SERVER_AUTH_KEY.`, 3);
|
|
30
|
+
}
|
|
31
|
+
if (status === 409) {
|
|
32
|
+
return new errors_1.CliError(`verikun server device is busy (409)${detail || ' — another run holds the device; retry when it finishes'}.`, 3);
|
|
33
|
+
}
|
|
34
|
+
// The server sends the intended exit code (usage 2 / env 3) in the body; fall
|
|
35
|
+
// back on the HTTP class when it didn't.
|
|
36
|
+
const exitCode = body?.exitCode ?? (status === 400 || status === 404 || status === 413 ? 2 : 3);
|
|
37
|
+
return new errors_1.CliError(`verikun server error ${status} at ${url}${detail}`, exitCode);
|
|
38
|
+
}
|
|
39
|
+
async function readBody(res) {
|
|
40
|
+
try {
|
|
41
|
+
return (await res.json());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
class RemoteTransport {
|
|
48
|
+
opts;
|
|
49
|
+
base;
|
|
50
|
+
/** One token per backend = one logical run holding the server's device lock. */
|
|
51
|
+
runToken = (0, node_crypto_1.randomUUID)();
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.opts = opts;
|
|
54
|
+
this.base = trimUrl(opts.url);
|
|
55
|
+
}
|
|
56
|
+
headers(extra = {}) {
|
|
57
|
+
const h = { 'x-verikun-run': this.runToken, ...extra };
|
|
58
|
+
if (this.opts.authKey)
|
|
59
|
+
h.authorization = `Bearer ${this.opts.authKey}`;
|
|
60
|
+
return h;
|
|
61
|
+
}
|
|
62
|
+
async request(method, path, body, timeoutMs, extraHeaders = {}) {
|
|
63
|
+
const url = `${this.base}${path}`;
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
66
|
+
let res;
|
|
67
|
+
try {
|
|
68
|
+
res = await fetch(url, {
|
|
69
|
+
method,
|
|
70
|
+
headers: this.headers(extraHeaders),
|
|
71
|
+
body,
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const reason = e.name === 'AbortError' ? `timed out after ${Math.round(timeoutMs / 1000)}s` : e.message;
|
|
77
|
+
throw new errors_1.CliError(`cannot reach verikun server at ${url} (${reason})`, 3);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw describeStatus(res.status, await readBody(res), url);
|
|
84
|
+
const parsed = await readBody(res);
|
|
85
|
+
if (parsed === null)
|
|
86
|
+
throw new errors_1.CliError(`verikun server at ${url} returned a non-JSON response`, 3);
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
postJson(path, payload, timeoutMs) {
|
|
90
|
+
return this.request('POST', path, JSON.stringify(payload), timeoutMs, { 'content-type': 'application/json' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** GET /v1/health — the `--server` preflight. Reachability, the server's platform +
|
|
94
|
+
* serial (which become the run's context), and — when a key is supplied — an auth
|
|
95
|
+
* check, so a bad key fails fast here instead of at the first step. */
|
|
96
|
+
async function pingServer(opts) {
|
|
97
|
+
const t = new RemoteTransport(opts);
|
|
98
|
+
const health = await t.request('GET', '/v1/health', undefined, HEALTH_TIMEOUT_MS);
|
|
99
|
+
if (!health.ok || !health.platform) {
|
|
100
|
+
throw new errors_1.CliError(`'${trimUrl(opts.url)}' does not look like a verikun server (unexpected /v1/health payload).`, 3);
|
|
101
|
+
}
|
|
102
|
+
return health;
|
|
103
|
+
}
|
|
104
|
+
function decodeArtifacts(encoded) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [rel, b64] of Object.entries(encoded ?? {}))
|
|
107
|
+
out[rel] = Buffer.from(b64, 'base64');
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function createRemoteBackend(opts, health) {
|
|
111
|
+
const t = new RemoteTransport(opts);
|
|
112
|
+
const execRaw = async (req, record) => {
|
|
113
|
+
const res = await t.postJson('/v1/exec', req, EXEC_TIMEOUT_MS);
|
|
114
|
+
if (record && res.step)
|
|
115
|
+
opts.onStep?.(res.step, decodeArtifacts(res.artifacts));
|
|
116
|
+
return { code: res.code, error: res.error ? (0, rpc_1.rebuildError)(res.error) : undefined };
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
exec: (command, positionals, flags) => execRaw({ command, positionals, flags }, true),
|
|
120
|
+
async getElements() {
|
|
121
|
+
const res = await t.postJson('/v1/elements', {}, ELEMENTS_TIMEOUT_MS);
|
|
122
|
+
return res.elements;
|
|
123
|
+
},
|
|
124
|
+
async install(appPath) {
|
|
125
|
+
// v1 remote installs are single-file uploads; the extension is the only thing
|
|
126
|
+
// the client tells the server about the artifact (never a path).
|
|
127
|
+
const ext = (0, node_path_1.extname)(appPath).slice(1).toLowerCase();
|
|
128
|
+
if (ext !== 'apk' && ext !== 'ipa') {
|
|
129
|
+
throw new errors_1.CliError(`install --server accepts a single .apk or .ipa file; got '${appPath}'. (.app directories are local-only.)`, 2);
|
|
130
|
+
}
|
|
131
|
+
let buf;
|
|
132
|
+
try {
|
|
133
|
+
buf = (0, node_fs_1.readFileSync)(appPath);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
throw new errors_1.CliError(`install: cannot read '${appPath}' (${e.message})`, 2);
|
|
137
|
+
}
|
|
138
|
+
const sha256 = (0, node_crypto_1.createHash)('sha256').update(buf).digest('hex');
|
|
139
|
+
await t.request('POST', '/v1/install', buf, INSTALL_TIMEOUT_MS, {
|
|
140
|
+
'content-type': 'application/octet-stream',
|
|
141
|
+
'x-verikun-ext': ext,
|
|
142
|
+
'x-verikun-sha256': sha256,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
async reset(appId) {
|
|
146
|
+
// Between-test housekeeping (vk suite): the step is deliberately NOT spliced
|
|
147
|
+
// into any run. iOS has no per-app data reset, so degrade to a force-stop —
|
|
148
|
+
// the same honest degrade the local backend applies.
|
|
149
|
+
const command = health.platform === 'ios' ? 'stop' : 'clear';
|
|
150
|
+
const { code, error } = await execRaw({ command, positionals: [appId], flags: {} }, false);
|
|
151
|
+
if (code !== 0)
|
|
152
|
+
throw error ?? new errors_1.CliError(`reset (${command} ${appId}) failed on the server (exit ${code})`, 3);
|
|
153
|
+
},
|
|
154
|
+
async close() {
|
|
155
|
+
// Free the server's device lock so the next command (a fresh run token, e.g.
|
|
156
|
+
// `vk install` then `vk suite` in one CI job) isn't 409'd until the idle
|
|
157
|
+
// takeover. Best-effort: a dead server just means the lock ages out.
|
|
158
|
+
try {
|
|
159
|
+
await t.postJson('/v1/release', {}, HEALTH_TIMEOUT_MS);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* the idle takeover covers a lock we failed to release */
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
package/dist/args.js
CHANGED
package/dist/bin/verikun.js
CHANGED
|
File without changes
|