verikun 0.4.1

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.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSelector = parseSelector;
4
+ exports.matchElements = matchElements;
5
+ exports.resolveOne = resolveOne;
6
+ const errors_1 = require("../errors");
7
+ const format_1 = require("./format");
8
+ function parseSelector(raw, opts = {}) {
9
+ let kind = 'text';
10
+ let value = raw;
11
+ if (raw.startsWith('@')) {
12
+ kind = 'id';
13
+ value = raw.slice(1);
14
+ }
15
+ else {
16
+ const m = /^(id|text|desc|class):([\s\S]*)$/.exec(raw);
17
+ if (m) {
18
+ kind = m[1];
19
+ value = m[2];
20
+ }
21
+ }
22
+ if (!value)
23
+ throw new errors_1.CliError(`Empty selector value in '${raw}'`, 2);
24
+ return { kind, value, contains: !!opts.contains, index: opts.index, raw };
25
+ }
26
+ const norm = (s) => s.trim().toLowerCase();
27
+ const strip = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '');
28
+ /** The ordered match tiers for a selector. First tier with a hit wins. */
29
+ function tiers(sel) {
30
+ const nv = norm(sel.value);
31
+ const sv = strip(sel.value);
32
+ // exact -> partial -> normalized over a single text-like field.
33
+ const textField = (get) => [
34
+ { tier: 'exact', test: (el) => norm(get(el)) === nv },
35
+ { tier: 'partial', test: (el) => nv.length > 0 && norm(get(el)).includes(nv) },
36
+ { tier: 'normalized', test: (el) => sv.length > 0 && strip(get(el)).includes(sv) },
37
+ ];
38
+ let list;
39
+ switch (sel.kind) {
40
+ case 'id':
41
+ list = [
42
+ {
43
+ tier: 'exact',
44
+ test: (el) => norm(el.idShort) === nv || norm(el.id) === nv || norm(el.id).endsWith('/' + nv),
45
+ },
46
+ {
47
+ tier: 'partial',
48
+ test: (el) => nv.length > 0 && (norm(el.idShort).includes(nv) || norm(el.id).includes(nv)),
49
+ },
50
+ {
51
+ tier: 'normalized',
52
+ test: (el) => sv.length > 0 && (strip(el.idShort).includes(sv) || strip(el.id).includes(sv)),
53
+ },
54
+ ];
55
+ break;
56
+ case 'class':
57
+ list = [
58
+ {
59
+ tier: 'exact',
60
+ test: (el) => norm(el.type) === nv || norm(el.class) === nv || norm(el.class).endsWith('.' + nv),
61
+ },
62
+ {
63
+ tier: 'partial',
64
+ test: (el) => nv.length > 0 && (norm(el.type).includes(nv) || norm(el.class).includes(nv)),
65
+ },
66
+ ];
67
+ break;
68
+ case 'desc':
69
+ list = textField((el) => el.desc);
70
+ break;
71
+ case 'text':
72
+ default:
73
+ list = textField((el) => el.text);
74
+ break;
75
+ }
76
+ // --contains forces substring matching: drop the exact-only tier.
77
+ return sel.contains ? list.filter((t) => t.tier !== 'exact') : list;
78
+ }
79
+ function matchElements(elements, sel) {
80
+ for (const { tier, test } of tiers(sel)) {
81
+ const found = elements.filter(test);
82
+ if (found.length === 0)
83
+ continue;
84
+ if (sel.index !== undefined) {
85
+ const picked = found[sel.index];
86
+ return picked ? { matches: [picked], tier } : { matches: [], tier: null };
87
+ }
88
+ return { matches: found, tier };
89
+ }
90
+ // For text: selectors, if no text matches found, fall back to desc
91
+ if (sel.kind === 'text') {
92
+ const descSel = { ...sel, kind: 'desc' };
93
+ for (const { tier, test } of tiers(descSel)) {
94
+ const found = elements.filter(test);
95
+ if (found.length === 0)
96
+ continue;
97
+ if (sel.index !== undefined) {
98
+ const picked = found[sel.index];
99
+ return picked ? { matches: [picked], tier } : { matches: [], tier: null };
100
+ }
101
+ return { matches: found, tier };
102
+ }
103
+ }
104
+ return { matches: [], tier: null };
105
+ }
106
+ /** Resolve to exactly one element (with the tier it matched), or throw. */
107
+ function resolveOne(elements, sel) {
108
+ const { matches, tier } = matchElements(elements, sel);
109
+ if (matches.length === 0) {
110
+ throw new errors_1.SelectorNotFoundError(`No element matched selector '${sel.raw}'. Run \`verikun ui\` to inspect the current screen.`);
111
+ }
112
+ if (matches.length > 1) {
113
+ const list = matches.map((m) => ' ' + (0, format_1.formatInline)(m)).join('\n');
114
+ throw new errors_1.AmbiguousSelectorError(`Selector '${sel.raw}' matched ${matches.length} elements; refine it or add --index N:\n${list}`, matches);
115
+ }
116
+ return { element: matches[0], tier: tier };
117
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ // GENERATED by scripts/gen-version.mjs from package.json's "version" at build time
5
+ // (the `prebuild` script). Do NOT edit by hand; bump package.json instead.
6
+ exports.VERSION = '0.4.1';
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "verikun",
3
+ "version": "0.4.1",
4
+ "description": "Drive Android emulators/devices (and later iOS simulators) for AI agents: tap, type, swipe, screenshot, and inspect the UI hierarchy by semantic identifiers — like Puppeteer for native apps.",
5
+ "keywords": [
6
+ "android",
7
+ "adb",
8
+ "ios",
9
+ "simulator",
10
+ "ui-automation",
11
+ "testing",
12
+ "puppeteer",
13
+ "ai-agent"
14
+ ],
15
+ "homepage": "https://github.com/ddikman/verikun#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/ddikman/verikun/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ddikman/verikun.git"
22
+ },
23
+ "author": "David Dikman <ddikman@gmail.com>",
24
+ "bin": {
25
+ "verikun": "dist/bin/verikun.js",
26
+ "vk": "dist/bin/verikun.js"
27
+ },
28
+ "main": "dist/cli.js",
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "prebuild": "node scripts/gen-version.mjs",
34
+ "build": "tsc",
35
+ "dev": "tsc --watch",
36
+ "verikun": "node dist/bin/verikun.js",
37
+ "test": "tsc -p tsconfig.test.json && node --test --test-reporter=spec .test-build/tests/*.test.js",
38
+ "test:ci": "mkdir -p test-results && tsc -p tsconfig.test.json && node --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./scripts/github-test-summary.mjs --test-reporter-destination=test-results/summary.md .test-build/tests/*.test.js",
39
+ "test:watch": "tsc -p tsconfig.test.json && node --test --watch --test-reporter=spec .test-build/tests/*.test.js",
40
+ "prepare": "npm run build"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "license": "MIT",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.11.0",
51
+ "typescript": "^5.4.0"
52
+ }
53
+ }