verikun 0.9.0 → 0.10.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 +12 -8
- package/dist/agent/cli-provider.js +63 -1
- package/dist/agent/cost.js +6 -4
- package/dist/bin/verikun.js +0 -0
- package/dist/cli.js +34 -33
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/drivers/simctl.js +0 -156
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ vk screenshot # -> ./.verikun/screen.png
|
|
|
103
103
|
### AI
|
|
104
104
|
| Command | Description |
|
|
105
105
|
|---|---|
|
|
106
|
-
| `ai <file> [--model m] [--max-cost-usd n] [--timeout dur] [--cost-override in/out] [--effort e] [--package pkg] [--app-build id] [--server url] [--show-plan] [--recompile] [--json]` | Run a plain-English test: compile it to a deterministic plan once, replay it model-free, and self-heal failures via the model. Needs `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (per model), or no key with `--model codex-cli` (a logged-in `codex` CLI). See [AI](#ai--natural-language-tests). |
|
|
106
|
+
| `ai <file> [--model m] [--max-cost-usd n] [--timeout dur] [--cost-override in/out] [--effort e] [--package pkg] [--app-build id] [--server url] [--show-plan] [--recompile] [--json]` | Run a plain-English test: compile it to a deterministic plan once, replay it model-free, and self-heal failures via the model. Needs `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (per model), or no key with `--model codex-cli` / `cursor-cli` (a logged-in `codex` / `cursor-agent` CLI). See [AI](#ai--natural-language-tests). |
|
|
107
107
|
| `suite <dir> [--app <id>] [--name n] [--server url] [--json]` (+ all `ai` flags) | Run every `*.md` in `<dir>` as one sequential suite with an overview report and a non-zero exit on failure — the CI gate. See [Suites](#suites--run-a-directory-of-tests). |
|
|
108
108
|
|
|
109
109
|
### Remote
|
|
@@ -211,8 +211,9 @@ with no model calls on the happy path**. The model is woken only to *repair* a s
|
|
|
211
211
|
whose selector stops resolving; a green run persists the repaired plan, so the next
|
|
212
212
|
run is free again. That is what keeps a CI suite's steady-state token cost near zero.
|
|
213
213
|
Needs `ANTHROPIC_API_KEY` (Claude models) or `OPENAI_API_KEY` (OpenAI models) — or **no
|
|
214
|
-
key** with `--model codex-cli`, which
|
|
215
|
-
|
|
214
|
+
key** with `--model codex-cli` / `--model cursor-cli`, which drive an already-logged-in
|
|
215
|
+
`codex` or `cursor-agent` CLI off your ChatGPT / Cursor subscription (log in once;
|
|
216
|
+
verikun just needs the binary on PATH).
|
|
216
217
|
|
|
217
218
|
```sh
|
|
218
219
|
# onboarding.md (plain English):
|
|
@@ -243,9 +244,12 @@ a hard iteration cap and stop early if the screen stops changing.
|
|
|
243
244
|
- **`--model`** picks the model and its provider — Anthropic (`claude-haiku-4-5` ·
|
|
244
245
|
`claude-sonnet-4-6` (default) · `claude-opus-4-8` · `claude-fable-5`), OpenAI
|
|
245
246
|
(`gpt-5.4-mini` · `gpt-5.4` · `gpt-5.5`), each read from its own key
|
|
246
|
-
(`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`), or
|
|
247
|
-
|
|
248
|
-
`--max-cost-usd` /
|
|
247
|
+
(`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`), or a CLI backend — **`codex-cli`** (the
|
|
248
|
+
logged-in `codex` binary) or **`cursor-cli`** (`cursor-agent`) — which need no key at
|
|
249
|
+
all: spend is on your subscription, so their cost line is `$0` and `--max-cost-usd` /
|
|
250
|
+
`--cost-override` are no-ops. Each CLI picks its own underlying model, and runs
|
|
251
|
+
read-only in a scratch directory so it never touches your working tree.
|
|
252
|
+
**`--recompile`** ignores the cache.
|
|
249
253
|
- An `ai` run records like any other flow, so it produces the same JUnit + HTML report —
|
|
250
254
|
with the cost line and any **suggested test improvements** (workarounds the model
|
|
251
255
|
applied, which you can fold back into the prose to stabilize the test and cut tokens).
|
|
@@ -282,8 +286,8 @@ vk suite tests/ --app com.example.app --server "$VERIKUN_SERVER" # remote devi
|
|
|
282
286
|
- **`index.html`** — a summary page linking every test's `report.html`.
|
|
283
287
|
- **Exit code is the CI gate:** `1` if any test failed, `0` all green, `2` bad/empty
|
|
284
288
|
directory. All `ai` flags (`--model`, `--max-cost-usd`, `--timeout`, …) apply to
|
|
285
|
-
every test; the provider (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`, or the `codex`
|
|
286
|
-
`--model codex-cli`) is checked up front.
|
|
289
|
+
every test; the provider (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`, or the `codex` /
|
|
290
|
+
`cursor-agent` CLI for `--model codex-cli` / `cursor-cli`) is checked up front.
|
|
287
291
|
|
|
288
292
|
## Remote devices — `vk server`
|
|
289
293
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CliProvider = exports.CODEX_SPEC = void 0;
|
|
3
|
+
exports.CliProvider = exports.CURSOR_SPEC = exports.CODEX_SPEC = void 0;
|
|
4
|
+
exports.cursorResultText = cursorResultText;
|
|
4
5
|
exports.schemaInstruction = schemaInstruction;
|
|
5
6
|
exports.extractJson = extractJson;
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
@@ -69,6 +70,67 @@ exports.CODEX_SPEC = {
|
|
|
69
70
|
rawText: (stdout) => stdout, // fallback only; the message is read from --output-last-message
|
|
70
71
|
loginHint: 'run `codex login` to sign in with your ChatGPT subscription (no API key needed)',
|
|
71
72
|
};
|
|
73
|
+
/** cursor-agent (Cursor CLI): non-interactive `--print` with a JSON envelope on stdout. Unlike
|
|
74
|
+
* codex it has NO schema flag of any kind, so the schema is injected into the prompt and
|
|
75
|
+
* extractJson/parsePlan do the rest. Two flags are load-bearing beyond the obvious ones:
|
|
76
|
+
* `--trust`, without which a headless call dies at a "Workspace Trust Required" gate and exits 1
|
|
77
|
+
* before the model ever runs; and `--mode ask`, cursor's documented read-only Q&A mode, which
|
|
78
|
+
* stands in for codex's `--sandbox read-only` (cursor's own `--sandbox` only takes enabled/
|
|
79
|
+
* disabled). Plain `--print` "has access to all tools, including write and shell", so `--mode ask`
|
|
80
|
+
* plus the neutral `--workspace` are what keep this a pure transform — and we deliberately never
|
|
81
|
+
* pass --force/--yolo/--approve-mcps. */
|
|
82
|
+
exports.CURSOR_SPEC = {
|
|
83
|
+
id: 'cursor',
|
|
84
|
+
bin: 'cursor-agent',
|
|
85
|
+
schema: 'prompt', // no native schema flag — schemaInstruction() injects it into the prompt
|
|
86
|
+
usesOutputFile: false, // stdout is the only channel out; no --output-last-message equivalent
|
|
87
|
+
buildArgs(prompt, { cwd, model }) {
|
|
88
|
+
const args = [
|
|
89
|
+
'--print', // non-interactive one-shot
|
|
90
|
+
'--output-format', 'json', // a stable envelope instead of TTY-decorated text
|
|
91
|
+
'--mode', 'ask', // read-only Q&A mode: no edits, no shell
|
|
92
|
+
'--workspace', cwd, // root the agent in a neutral temp dir, not the verikun working tree
|
|
93
|
+
'--trust', // required: else headless stops at the workspace-trust prompt
|
|
94
|
+
];
|
|
95
|
+
if (model)
|
|
96
|
+
args.push('--model', model);
|
|
97
|
+
args.push(prompt); // prompt is the trailing positional
|
|
98
|
+
return args;
|
|
99
|
+
},
|
|
100
|
+
rawText: cursorResultText,
|
|
101
|
+
loginHint: 'run `cursor-agent login` to sign in with your Cursor subscription (no API key needed)',
|
|
102
|
+
};
|
|
103
|
+
/** Peel the model's final message out of cursor-agent's `--output-format json` envelope:
|
|
104
|
+
* `{type:"result", subtype:"success", is_error:false, result:"<final text>", …}`.
|
|
105
|
+
* Two things a plain `JSON.parse(s).result` would get wrong:
|
|
106
|
+
* - cursor can exit 0 while reporting failure via `is_error:true` (e.g. it hit a turn limit), which
|
|
107
|
+
* CliProvider's exit-code check cannot see. Left alone, the error prose would flow into
|
|
108
|
+
* extractJson and surface as a misleading "did not return parseable JSON", so map it to the same
|
|
109
|
+
* exit 3 a non-zero exit gets.
|
|
110
|
+
* - an envelope shape drift (or a future default of --output-format text) falls back to the raw
|
|
111
|
+
* stdout, so extractJson's tolerant scan still gets a chance instead of failing outright. */
|
|
112
|
+
function cursorResultText(stdout) {
|
|
113
|
+
let envelope;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(stdout.trim());
|
|
116
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
117
|
+
envelope = parsed;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* not JSON at all (e.g. --output-format text) — fall through to the raw stdout */
|
|
121
|
+
}
|
|
122
|
+
if (!envelope)
|
|
123
|
+
return stdout;
|
|
124
|
+
if (envelope.is_error === true) {
|
|
125
|
+
const detail = typeof envelope.result === 'string' ? tail(envelope.result) : '';
|
|
126
|
+
throw new errors_1.CliError(`\`cursor-agent\` reported an error: ${detail || '(no detail)'}`, 3);
|
|
127
|
+
}
|
|
128
|
+
// Only peel when this really IS the envelope. Parsing alone isn't enough: if cursor ever returns
|
|
129
|
+
// the plan object bare (or renames the field), treating any JSON object as an envelope would
|
|
130
|
+
// blank it to '' and report "returned an empty response" — so anything without a string
|
|
131
|
+
// `result` falls through to the raw stdout, where extractJson can still find the object.
|
|
132
|
+
return typeof envelope.result === 'string' ? envelope.result : stdout;
|
|
133
|
+
}
|
|
72
134
|
// Collision-free temp-file names within a process without needing Math.random() (which the
|
|
73
135
|
// plan-cache/version paths keep deterministic); pid + a counter is enough.
|
|
74
136
|
let tempCounter = 0;
|
package/dist/agent/cost.js
CHANGED
|
@@ -21,11 +21,13 @@ const MODELS = {
|
|
|
21
21
|
'gpt-5.4-mini': { input: 0.75, output: 4.5, provider: 'openai' },
|
|
22
22
|
'gpt-5.4': { input: 2.5, output: 15, provider: 'openai' },
|
|
23
23
|
'gpt-5.5': { input: 5, output: 30, provider: 'openai' },
|
|
24
|
-
// CLI-agent
|
|
25
|
-
// token — so price is $0 and --max-cost-usd/--cost-override are inert no-ops (the
|
|
26
|
-
// bounded by maxRepairs + --timeout instead).
|
|
27
|
-
// and
|
|
24
|
+
// CLI-agent backends: billed to the user's ChatGPT/Cursor subscription via an already-logged-in
|
|
25
|
+
// CLI, not per token — so price is $0 and --max-cost-usd/--cost-override are inert no-ops (the
|
|
26
|
+
// run is bounded by maxRepairs + --timeout instead). The `-cli` suffix reads clearly as "the
|
|
27
|
+
// CLI" and keeps these from colliding with the CLIs' own model aliases — cursor in particular
|
|
28
|
+
// offers `gpt-5.3-codex`, `gpt-5.4-high`, `claude-opus-4-8-thinking-high` and friends.
|
|
28
29
|
'codex-cli': { input: 0, output: 0, provider: 'codex' },
|
|
30
|
+
'cursor-cli': { input: 0, output: 0, provider: 'cursor' },
|
|
29
31
|
};
|
|
30
32
|
exports.MODEL_PRICES = Object.fromEntries(Object.entries(MODELS).map(([m, s]) => [m, { input: s.input, output: s.output }]));
|
|
31
33
|
exports.ALLOWED_MODELS = Object.keys(MODELS);
|
package/dist/bin/verikun.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -984,45 +984,43 @@ function readAiTest(file) {
|
|
|
984
984
|
throw new errors_1.CliError(`ai: '${file}' is empty`, 2);
|
|
985
985
|
return nl;
|
|
986
986
|
}
|
|
987
|
+
/** The CLI-agent backends, by ProviderId. Adding a CLI provider is one entry here rather than a
|
|
988
|
+
* new arm in each of the three functions below — they all ask the same question of it. */
|
|
989
|
+
const CLI_SPECS = { codex: cli_provider_1.CODEX_SPEC, cursor: cli_provider_1.CURSOR_SPEC };
|
|
987
990
|
/** Is the backend for `model` usable right now? HTTP providers need their API key in env;
|
|
988
991
|
* a CLI provider needs its binary on PATH (auth lives in the CLI's own login, not an env key). */
|
|
989
992
|
function providerAvailable(model) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
default:
|
|
996
|
-
return !!process.env.ANTHROPIC_API_KEY;
|
|
997
|
-
}
|
|
993
|
+
const id = (0, cost_1.providerFor)(model);
|
|
994
|
+
const spec = CLI_SPECS[id];
|
|
995
|
+
if (spec)
|
|
996
|
+
return (0, exec_1.commandExists)(spec.bin);
|
|
997
|
+
return id === 'openai' ? !!process.env.OPENAI_API_KEY : !!process.env.ANTHROPIC_API_KEY;
|
|
998
998
|
}
|
|
999
999
|
/** What's missing when a provider is unavailable — the tail of the preflight error message. */
|
|
1000
1000
|
function providerRequirement(model) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
return 'ANTHROPIC_API_KEY is not set';
|
|
1008
|
-
}
|
|
1001
|
+
const id = (0, cost_1.providerFor)(model);
|
|
1002
|
+
const spec = CLI_SPECS[id];
|
|
1003
|
+
// Reuse the spec's own loginHint rather than restating it, so the two can't drift apart.
|
|
1004
|
+
if (spec)
|
|
1005
|
+
return `the \`${spec.bin}\` CLI was not found on PATH — install it and ${spec.loginHint}`;
|
|
1006
|
+
return id === 'openai' ? 'OPENAI_API_KEY is not set' : 'ANTHROPIC_API_KEY is not set';
|
|
1009
1007
|
}
|
|
1010
1008
|
/** Route the model to its backend. HTTP providers read their own key; a CLI provider shells
|
|
1011
1009
|
* out to its logged-in binary. Unavailable → null (compile/repair off), the same graceful
|
|
1012
|
-
* degradation as before: a cached plan can still replay for free without any provider.
|
|
1010
|
+
* degradation as before: a cached plan can still replay for free without any provider.
|
|
1011
|
+
* CLI providers get no `model`, so the CLI picks its own default — the "I just have a
|
|
1012
|
+
* subscription" path; --effort is likewise inapplicable to them. */
|
|
1013
1013
|
function makeProvider(opts) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1014
|
+
const id = (0, cost_1.providerFor)(opts.model);
|
|
1015
|
+
const spec = CLI_SPECS[id];
|
|
1016
|
+
if (spec)
|
|
1017
|
+
return (0, exec_1.commandExists)(spec.bin) ? new cli_provider_1.CliProvider({ spec }) : null;
|
|
1018
|
+
if (id === 'openai') {
|
|
1019
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1020
|
+
return apiKey ? new openai_1.OpenAiProvider({ model: opts.model, apiKey, effort: opts.effort }) : null;
|
|
1021
|
+
}
|
|
1022
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1023
|
+
return apiKey ? new claude_1.ClaudeProvider({ model: opts.model, apiKey, effort: opts.effort }) : null;
|
|
1026
1024
|
}
|
|
1027
1025
|
/** Obtain the plan: a cache hit (free) or a compile (pays tokens; may seed from a
|
|
1028
1026
|
* prior build's plan to avoid a full recompile). The fresh compile is cached right
|
|
@@ -1587,15 +1585,18 @@ AI (run a natural-language test — compile once, replay model-free, self-heal)
|
|
|
1587
1585
|
that fails to resolve; a green run persists the
|
|
1588
1586
|
(repaired) plan so the next run is free. Needs
|
|
1589
1587
|
ANTHROPIC_API_KEY (Claude), OPENAI_API_KEY (gpt-5.x),
|
|
1590
|
-
or a logged-in agent CLI
|
|
1591
|
-
'codex login' ChatGPT
|
|
1592
|
-
|
|
1588
|
+
or a logged-in agent CLI — no API key: --model
|
|
1589
|
+
codex-cli uses your 'codex login' ChatGPT
|
|
1590
|
+
subscription, cursor-cli your 'cursor-agent login'
|
|
1591
|
+
Cursor one (cost is $0 for both, so
|
|
1592
|
+
--max-cost-usd/--cost-override are no-ops).
|
|
1593
1593
|
Progress -> stderr; the report path ->
|
|
1594
1594
|
stdout. --show-plan prints the compiled IR without
|
|
1595
1595
|
running; --recompile ignores the cache.
|
|
1596
1596
|
Models: claude-haiku-4-5 | claude-sonnet-4-6
|
|
1597
1597
|
(default) | claude-opus-4-8 | claude-fable-5 |
|
|
1598
|
-
gpt-5.4-mini | gpt-5.4 | gpt-5.5 | codex-cli
|
|
1598
|
+
gpt-5.4-mini | gpt-5.4 | gpt-5.5 | codex-cli |
|
|
1599
|
+
cursor-cli.
|
|
1599
1600
|
|
|
1600
1601
|
SUITE (run a directory of natural-language tests as one gated suite)
|
|
1601
1602
|
suite <dir> [--app <id>] [--name n] [--json] (+ all \`ai\` flags, incl. --server)
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VERSION = void 0;
|
|
4
4
|
// GENERATED by scripts/gen-version.mjs from package.json's "version" at build time
|
|
5
5
|
// (the `prebuild` script). Do NOT edit by hand; bump package.json instead.
|
|
6
|
-
exports.VERSION = '0.
|
|
6
|
+
exports.VERSION = '0.10.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verikun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Drive Android emulators/devices and iOS simulators for AI agents: tap, type, swipe, screenshot, and inspect the UI hierarchy by semantic identifiers — like Puppeteer for native apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
package/dist/drivers/simctl.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SimctlDriver = void 0;
|
|
4
|
-
const node_os_1 = require("node:os");
|
|
5
|
-
const node_path_1 = require("node:path");
|
|
6
|
-
const node_fs_1 = require("node:fs");
|
|
7
|
-
const errors_1 = require("../errors");
|
|
8
|
-
const exec_1 = require("../exec");
|
|
9
|
-
const XCRUN = 'xcrun';
|
|
10
|
-
function listPhysicalDevices() {
|
|
11
|
-
const r = (0, exec_1.runText)(XCRUN, ['devicectl', 'list', 'devices']);
|
|
12
|
-
if (r.code !== 0)
|
|
13
|
-
return [];
|
|
14
|
-
const lines = r.stdout.split('\n');
|
|
15
|
-
const headerIdx = lines.findIndex((l) => l.includes('Identifier'));
|
|
16
|
-
if (headerIdx < 0)
|
|
17
|
-
return [];
|
|
18
|
-
const header = lines[headerIdx];
|
|
19
|
-
const nameCol = header.indexOf('Name');
|
|
20
|
-
const hostCol = header.indexOf('Hostname');
|
|
21
|
-
const idCol = header.indexOf('Identifier');
|
|
22
|
-
const stateCol = header.indexOf('State');
|
|
23
|
-
const modelCol = header.indexOf('Model');
|
|
24
|
-
const devices = [];
|
|
25
|
-
for (const line of lines.slice(headerIdx + 2)) {
|
|
26
|
-
if (!line.trim() || line.startsWith('-'))
|
|
27
|
-
continue;
|
|
28
|
-
const identifier = line.slice(idCol, stateCol).trim();
|
|
29
|
-
const state = line.slice(stateCol, modelCol).trim();
|
|
30
|
-
const name = line.slice(nameCol, hostCol).trim();
|
|
31
|
-
const model = line.slice(modelCol).trim();
|
|
32
|
-
if (!identifier)
|
|
33
|
-
continue;
|
|
34
|
-
const productMatch = model.match(/\(([^)]+)\)$/);
|
|
35
|
-
devices.push({
|
|
36
|
-
serial: identifier,
|
|
37
|
-
state,
|
|
38
|
-
model: name,
|
|
39
|
-
product: productMatch?.[1],
|
|
40
|
-
platform: 'ios',
|
|
41
|
-
note: 'physical — screenshot only, tap/ui not supported',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return devices;
|
|
45
|
-
}
|
|
46
|
-
// iOS support is intentionally partial. `simctl` covers screenshots, launch,
|
|
47
|
-
// and stop. Full interaction (tap, swipe, type) and the accessibility hierarchy
|
|
48
|
-
// require WebDriverAgent (https://github.com/appium/WebDriverAgent) — the
|
|
49
|
-
// planned next step. WDA is an open-source XCTest HTTP server: build it once
|
|
50
|
-
// in Xcode, sign it with your Apple developer certificate, and run it on the
|
|
51
|
-
// target device or simulator. No Python required; works on both simulators and
|
|
52
|
-
// physical devices. The Driver seam is already in place so the command layer
|
|
53
|
-
// won't change when WDA support is added.
|
|
54
|
-
function notSupported(feature) {
|
|
55
|
-
throw new errors_1.CliError(`iOS ${feature} is not supported yet.\n` +
|
|
56
|
-
'Screenshots, launch, and stop work today via xcrun simctl.\n' +
|
|
57
|
-
'Full interaction (tap / swipe / type) and UI hierarchy inspection are planned\n' +
|
|
58
|
-
'via WebDriverAgent — build it once in Xcode and vk will drive it over HTTP.\n' +
|
|
59
|
-
'See: https://github.com/appium/WebDriverAgent', 3);
|
|
60
|
-
}
|
|
61
|
-
class SimctlDriver {
|
|
62
|
-
platform = 'ios';
|
|
63
|
-
udid;
|
|
64
|
-
constructor(device) {
|
|
65
|
-
this.udid = device || 'booted';
|
|
66
|
-
}
|
|
67
|
-
listDevices() {
|
|
68
|
-
const devices = [];
|
|
69
|
-
// Simulators via simctl
|
|
70
|
-
const { stdout } = (0, exec_1.runText)(XCRUN, ['simctl', 'list', 'devices', 'available', '--json']);
|
|
71
|
-
try {
|
|
72
|
-
const data = JSON.parse(stdout);
|
|
73
|
-
for (const [runtime, list] of Object.entries(data.devices)) {
|
|
74
|
-
for (const d of list) {
|
|
75
|
-
devices.push({
|
|
76
|
-
serial: d.udid,
|
|
77
|
-
state: d.state.toLowerCase(),
|
|
78
|
-
model: d.name,
|
|
79
|
-
product: runtime.split('.').pop(),
|
|
80
|
-
platform: 'ios',
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
/* tolerate unexpected simctl output */
|
|
87
|
-
}
|
|
88
|
-
// Physical devices via devicectl (Xcode 15+)
|
|
89
|
-
devices.push(...listPhysicalDevices());
|
|
90
|
-
return devices;
|
|
91
|
-
}
|
|
92
|
-
resolvedSerial() {
|
|
93
|
-
return this.udid;
|
|
94
|
-
}
|
|
95
|
-
screenshot() {
|
|
96
|
-
const tmp = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `verikun-ios-${process.pid}.png`);
|
|
97
|
-
const r = (0, exec_1.runText)(XCRUN, ['simctl', 'io', this.udid, 'screenshot', tmp]);
|
|
98
|
-
if (r.code !== 0)
|
|
99
|
-
throw new errors_1.CliError(`simctl screenshot failed: ${r.stderr.trim()}`, 3);
|
|
100
|
-
const buf = (0, node_fs_1.readFileSync)(tmp);
|
|
101
|
-
try {
|
|
102
|
-
(0, node_fs_1.unlinkSync)(tmp);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
/* best-effort cleanup */
|
|
106
|
-
}
|
|
107
|
-
return buf;
|
|
108
|
-
}
|
|
109
|
-
launch(appId) {
|
|
110
|
-
const r = (0, exec_1.runText)(XCRUN, ['simctl', 'launch', this.udid, appId]);
|
|
111
|
-
if (r.code !== 0)
|
|
112
|
-
throw new errors_1.CliError(`simctl launch failed: ${r.stderr.trim()}`, 3);
|
|
113
|
-
}
|
|
114
|
-
stop(appId) {
|
|
115
|
-
const r = (0, exec_1.runText)(XCRUN, ['simctl', 'terminate', this.udid, appId]);
|
|
116
|
-
if (r.code !== 0)
|
|
117
|
-
throw new errors_1.CliError(`simctl terminate failed: ${r.stderr.trim()}`, 3);
|
|
118
|
-
}
|
|
119
|
-
clearApp(appId) {
|
|
120
|
-
// simctl has no per-app data reset. The manual equivalent is to uninstall and
|
|
121
|
-
// reinstall, which also removes the app itself — so we don't do it implicitly.
|
|
122
|
-
throw new errors_1.CliError(`iOS app-data clearing is not supported yet (requested for '${appId}').\n` +
|
|
123
|
-
'simctl has no per-app data reset; uninstall + reinstall ' +
|
|
124
|
-
'(`xcrun simctl uninstall <udid> <bundleId>`) is the manual equivalent, but it removes the app too.', 3);
|
|
125
|
-
}
|
|
126
|
-
getElements() {
|
|
127
|
-
return notSupported('hierarchy inspection');
|
|
128
|
-
}
|
|
129
|
-
screenSize() {
|
|
130
|
-
return notSupported('screen size');
|
|
131
|
-
}
|
|
132
|
-
tap() {
|
|
133
|
-
notSupported('tap');
|
|
134
|
-
}
|
|
135
|
-
swipe() {
|
|
136
|
-
notSupported('swipe');
|
|
137
|
-
}
|
|
138
|
-
inputText() {
|
|
139
|
-
notSupported('text input');
|
|
140
|
-
}
|
|
141
|
-
pressKey() {
|
|
142
|
-
notSupported('key events');
|
|
143
|
-
}
|
|
144
|
-
currentApp() {
|
|
145
|
-
return notSupported('current app');
|
|
146
|
-
}
|
|
147
|
-
getLogs() {
|
|
148
|
-
// Eventually: `xcrun simctl spawn <udid> log show` (simulators) / devicectl
|
|
149
|
-
// (physical). Heavier (predicate language, large output) — deferred with WDA.
|
|
150
|
-
return notSupported('log capture');
|
|
151
|
-
}
|
|
152
|
-
deviceTime() {
|
|
153
|
-
return ''; // no log window to anchor while iOS log capture is unsupported
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
exports.SimctlDriver = SimctlDriver;
|