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 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 drives an already-logged-in `codex` CLI off your
215
- ChatGPT subscription (`codex login` once; verikun just needs the binary on PATH).
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 the CLI backend **`codex-cli`** (no key —
247
- the logged-in `codex` binary; spend is on your subscription, so its cost line is `$0` and
248
- `--max-cost-usd` / `--cost-override` are no-ops); **`--recompile`** ignores the cache.
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` CLI for
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;
@@ -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 backend: billed to the user's ChatGPT subscription via the `codex` CLI, not per
25
- // token — so price is $0 and --max-cost-usd/--cost-override are inert no-ops (the run is
26
- // bounded by maxRepairs + --timeout instead). Named `codex-cli` to read clearly as "the CLI"
27
- // and to avoid colliding with cursor's own `gpt-5.x-codex` model aliases.
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);
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
- switch ((0, cost_1.providerFor)(model)) {
991
- case 'openai':
992
- return !!process.env.OPENAI_API_KEY;
993
- case 'codex':
994
- return (0, exec_1.commandExists)(cli_provider_1.CODEX_SPEC.bin);
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
- switch ((0, cost_1.providerFor)(model)) {
1002
- case 'openai':
1003
- return 'OPENAI_API_KEY is not set';
1004
- case 'codex':
1005
- return `the \`${cli_provider_1.CODEX_SPEC.bin}\` CLI was not found on PATH — install it and run \`codex login\` (ChatGPT subscription, no API key)`;
1006
- default:
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
- switch ((0, cost_1.providerFor)(opts.model)) {
1015
- case 'openai': {
1016
- const apiKey = process.env.OPENAI_API_KEY;
1017
- return apiKey ? new openai_1.OpenAiProvider({ model: opts.model, apiKey, effort: opts.effort }) : null;
1018
- }
1019
- case 'codex':
1020
- return (0, exec_1.commandExists)(cli_provider_1.CODEX_SPEC.bin) ? new cli_provider_1.CliProvider({ spec: cli_provider_1.CODEX_SPEC }) : null;
1021
- default: {
1022
- const apiKey = process.env.ANTHROPIC_API_KEY;
1023
- return apiKey ? new claude_1.ClaudeProvider({ model: opts.model, apiKey, effort: opts.effort }) : null;
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 (--model codex-cli uses your
1591
- 'codex login' ChatGPT subscription — no API key; cost
1592
- is $0 so --max-cost-usd/--cost-override are no-ops).
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.9.0';
6
+ exports.VERSION = '0.10.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verikun",
3
- "version": "0.9.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",
@@ -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;