@aarwitz/tapp 0.17.0-rc.13 → 0.17.0-rc.15
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +4 -4
- package/AGENTS.md +11 -5
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +11 -0
- package/README.md +52 -59
- package/bin/tapp.js +71 -27
- package/mcp-server/src/android-explorer.js +3 -1
- package/mcp-server/src/android-flow.js +18 -1
- package/mcp-server/src/application-model.js +76 -9
- package/mcp-server/src/ci-report.js +2 -2
- package/mcp-server/src/ci-setup.js +4 -4
- package/mcp-server/src/environment-preflight.js +43 -0
- package/mcp-server/src/html-report.js +3 -2
- package/mcp-server/src/index.js +139 -25
- package/mcp-server/src/pr-selection.js +2 -2
- package/mcp-server/src/product-operations.js +1 -1
- package/mcp-server/src/report.js +17 -4
- package/mcp-server/src/web-explorer.js +3 -2
- package/mcp-server/src/web-flow.js +17 -1
- package/package.json +4 -4
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +2 -2
- package/skills/tapp/SKILL.md +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapp",
|
|
3
|
-
"description": "The official Tapp plugin:
|
|
3
|
+
"description": "The official Tapp plugin: the agent skill and MCP tools for testing real iOS, Android, and web app surfaces.",
|
|
4
4
|
"owner": {
|
|
5
5
|
"name": "Aaron Horowitz",
|
|
6
6
|
"url": "https://github.com/aarwitz"
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"source": "npm",
|
|
13
13
|
"package": "@aarwitz/tapp"
|
|
14
14
|
},
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "Give Claude hands and eyes on iOS, Android, and web apps, with exploration, replayable flows, evidence, and deterministic CI gates.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Aaron Horowitz",
|
|
18
18
|
"url": "https://github.com/aarwitz"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://
|
|
20
|
+
"homepage": "https://runtapp.com/",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"keywords": [
|
|
23
23
|
"ios",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapp",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.17.0-rc.
|
|
3
|
+
"description": "Give Claude hands and eyes on iOS, Android, and web apps, with exploration, replayable flows, evidence, and deterministic CI gates.",
|
|
4
|
+
"version": "0.17.0-rc.15",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Aaron Horowitz",
|
|
7
7
|
"url": "https://github.com/aarwitz"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://
|
|
9
|
+
"homepage": "https://runtapp.com/",
|
|
10
10
|
"repository": "https://github.com/aarwitz/tapp",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"command": "npx",
|
|
25
25
|
"args": [
|
|
26
26
|
"-y",
|
|
27
|
-
"@aarwitz/tapp@0.17.0-rc.
|
|
27
|
+
"@aarwitz/tapp@0.17.0-rc.15",
|
|
28
28
|
"mcp"
|
|
29
29
|
],
|
|
30
30
|
"cwd": "${CLAUDE_PROJECT_DIR}"
|
package/AGENTS.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Tapp agent playbook
|
|
2
2
|
|
|
3
|
-
You (the agent) have Tapp: hands
|
|
4
|
-
|
|
3
|
+
You (the agent) have Tapp: hands and eyes on real app surfaces — iOS simulators, Android
|
|
4
|
+
emulators/devices, plus (beta) web apps in a real browser. Release judgment belongs only to Tapp's
|
|
5
|
+
repository-connected deterministic gate.
|
|
5
6
|
|
|
6
7
|
## No MCP connected? Just run the CLI
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
The core inspect, explore, replay, and gate capabilities work as plain commands — no Tapp account,
|
|
10
|
+
server, or global install. `[target]` is
|
|
9
11
|
optional: with nothing, tapp finds + builds the Xcode project in the cwd (or falls back to
|
|
10
12
|
the app already on the simulator); it also accepts a repo dir, a `path/to/App.app`, a
|
|
11
13
|
bundle id, or (web) an http(s) URL. You never need to know a bundle id up front.
|
|
@@ -57,7 +59,7 @@ installs, returns the bundle id) → `tapp_explore {appBundleId}`.
|
|
|
57
59
|
|
|
58
60
|
| The user wants… | Use | NOT |
|
|
59
61
|
|---|---|---|
|
|
60
|
-
| "Show me / screenshot a screen" | `tapp_open_app` (launch + screenshot + tree, ~15s) | `tapp_explore` (a full multi-minute
|
|
62
|
+
| "Show me / screenshot a screen" | `tapp_open_app` (launch + screenshot + tree, ~15s) | `tapp_explore` (a full multi-minute exploration) |
|
|
61
63
|
| "Tap through / drive / fill a form / log in" | `tapp_session_start` → `session_act` loop | repeated `open_app` calls (cold relaunch each time) |
|
|
62
64
|
| "Is my app broken? Find bugs" | `tapp_explore` — `appBundleId` for iOS, `androidAppId` for Android, `url` for owned web apps; returns an observation (findings + evidence), not a ship verdict — gate a merge with the CI gate (`tapp ci` CLI / the GitHub Action) + a contract | a manual session (exploration is autonomous) |
|
|
63
65
|
| "Make this flow a repeatable test" | drive it in a session, then `tapp_flow_save`; replay with `tapp_flow_run` | re-driving it by hand every time |
|
|
@@ -67,6 +69,7 @@ installs, returns the bundle id) → `tapp_explore {appBundleId}`.
|
|
|
67
69
|
|
|
68
70
|
```
|
|
69
71
|
tapp_session_start { appBundleId: "com.acme.app" } → fresh launch + initial tree
|
|
72
|
+
tapp_session_act { action: "login", email: "qa@x.com", password: "…" } → atomic fill + submit + verify
|
|
70
73
|
tapp_session_act { action: "tap", id: "Email" } → tap by a11y id OR visible label
|
|
71
74
|
tapp_session_act { action: "type", text: "qa@x.com" } → types into the focused field
|
|
72
75
|
tapp_session_act { action: "tap", id: "Sign In" }
|
|
@@ -90,7 +93,8 @@ Rules that prevent 90% of failures:
|
|
|
90
93
|
3. **`wait` after anything async** (navigation, network loads): `{action: "wait", id|text, timeoutMs}`.
|
|
91
94
|
Never assume the next screen is instantly there.
|
|
92
95
|
4. **Tap the field before typing** — `type` goes to the focused field. Tap email → type email →
|
|
93
|
-
tap password → type password.
|
|
96
|
+
tap password → type password. For sign-in, prefer the atomic `login` action: it records a
|
|
97
|
+
secret-templated replay step and avoids native secure-field refocus behavior.
|
|
94
98
|
5. Tap results: `ok` (landed), `not_hittable` (exists but disabled/covered — the harness
|
|
95
99
|
auto-dismisses keyboards and retries), `not_found` (nothing matches — re-read the tree).
|
|
96
100
|
6. One session at a time. `session_start` always begins from a fresh app launch.
|
|
@@ -134,6 +138,8 @@ without a coding agent, model, subscription, or API key. AI generation and `asse
|
|
|
134
138
|
deterministic assertions, same result every time. A failed assertion is a finding.
|
|
135
139
|
- **Generate:** `tapp_flow_generate { goal: "log in and add the first item to cart" }` —
|
|
136
140
|
grounded in the app's actually-explored screens, so it can't invent steps.
|
|
141
|
+
- **Discover the file format without MCP:** `npx -y @aarwitz/tapp@latest flow example` prints a
|
|
142
|
+
complete starter Flow; `tapp flow validate <file>` checks it without launching a target.
|
|
137
143
|
|
|
138
144
|
## Setup facts (tell the user when relevant)
|
|
139
145
|
|
|
@@ -765,6 +765,12 @@ class ExplorerTests: XCTestCase {
|
|
|
765
765
|
case "type":
|
|
766
766
|
status = sessionType(value, id: target.isEmpty ? nil : target) ? "pass" : "fail"
|
|
767
767
|
if status == "fail" { detail = "no field ‘\(target)’ to type into" }
|
|
768
|
+
case "login":
|
|
769
|
+
let email = subst((step["email"] as? String) ?? "$TEST_EMAIL")
|
|
770
|
+
let password = subst((step["password"] as? String) ?? "$TEST_PASSWORD")
|
|
771
|
+
let result = sessionLogin(email: email, password: password)
|
|
772
|
+
status = result.status == "ok" ? "pass" : "fail"
|
|
773
|
+
if status == "fail" { detail = result.detail.isEmpty ? result.status : result.detail }
|
|
768
774
|
case "swipe":
|
|
769
775
|
switch target.lowercased() { case "down": app.swipeDown(); case "left": app.swipeLeft(); case "right": app.swipeRight(); default: app.swipeUp() }
|
|
770
776
|
case "back":
|
|
@@ -1029,6 +1035,11 @@ class ExplorerTests: XCTestCase {
|
|
|
1029
1035
|
|
|
1030
1036
|
let testEmail = resolve("OCQA_TEST_EMAIL")
|
|
1031
1037
|
let testPassword = resolve("OCQA_TEST_PASSWORD")
|
|
1038
|
+
if resolve("OCQA_CREDENTIALS_EXPLICIT") == "1" {
|
|
1039
|
+
// Presence only: never print, persist, or expose credential values. Report rebuilding
|
|
1040
|
+
// needs this durable marker to distinguish "not supplied" from "supplied but unused".
|
|
1041
|
+
print("OCQA_STATE:credentials_supplied")
|
|
1042
|
+
}
|
|
1032
1043
|
|
|
1033
1044
|
// --- Explicit login replay (config-driven): a recorded type/tap/wait sequence for custom
|
|
1034
1045
|
// login UIs the heuristic preamble below can't parse. When configured it takes precedence. ---
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tapp
|
|
2
2
|
|
|
3
3
|
[](https://github.com/aarwitz/tapp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@aarwitz/tapp)
|
|
@@ -9,29 +9,24 @@
|
|
|
9
9
|
[](cursor://anysphere.cursor-deeplink/mcp/install?name=tapp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBhYXJ3aXR6L3RhcHBAbGF0ZXN0IiwibWNwIl19)
|
|
10
10
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=tapp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40aarwitz%2Ftapp%40latest%22%2C%22mcp%22%5D%7D)
|
|
11
11
|
|
|
12
|
-
**Tapp
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
application id, and run the same exploration, committed Flows, evidence, and regression gate. The
|
|
31
|
-
app does not link a Tapp SDK.
|
|
32
|
-
- **Web (beta)** — built *on* Playwright. Your agent already has browser hands; tapp adds the
|
|
33
|
-
autonomous exploration, the deterministic detectors (uncaught exceptions, failed requests,
|
|
34
|
-
dead buttons, broken links, placeholder `href="#"` links, error pages), and the same gate.
|
|
12
|
+
**Tapp gives coding agents hands and eyes on real iOS, Android, and web apps.** It can inspect and
|
|
13
|
+
drive screens, explore for technical failures, save journeys as deterministic tests, and gate
|
|
14
|
+
reviewed behavior in CI.
|
|
15
|
+
|
|
16
|
+
Exploration reports findings, coverage, evidence, and limits. Only the repository-connected gate
|
|
17
|
+
returns `pass`, `fail`, or `inconclusive`. Tapp does not turn an autonomous crawl into a subjective
|
|
18
|
+
"ship-ready" score.
|
|
19
|
+
|
|
20
|
+
Supported targets:
|
|
21
|
+
|
|
22
|
+
| Target app | Runtime |
|
|
23
|
+
|---|---|
|
|
24
|
+
| iOS | Simulator on macOS with Xcode; driven through XCUITest and accessibility |
|
|
25
|
+
| Android | Connected emulator or device with `adb`; driven through UIAutomator |
|
|
26
|
+
| Web (beta) | Owned browser app in Playwright Chromium |
|
|
27
|
+
|
|
28
|
+
Windows can host Android and web testing when their prerequisites are installed. Windows desktop
|
|
29
|
+
UI applications such as WinForms, WPF, and WinUI are not currently Tapp targets.
|
|
35
30
|
|
|
36
31
|
## Give Tapp to your coding agent
|
|
37
32
|
|
|
@@ -41,38 +36,41 @@ After setup, the whole user prompt is:
|
|
|
41
36
|
|
|
42
37
|
The official skill teaches the agent to choose the smallest useful operation, handle repositories
|
|
43
38
|
with multiple app targets, inspect visual evidence, and keep exploration observations separate from
|
|
44
|
-
|
|
39
|
+
gate decisions.
|
|
45
40
|
|
|
46
|
-
**Claude Code
|
|
41
|
+
**Claude Code, Codex, Cursor, Copilot, and other Agent Skills clients (recommended):**
|
|
47
42
|
|
|
48
43
|
```bash
|
|
49
|
-
|
|
50
|
-
claude plugin install tapp@tapp
|
|
44
|
+
npx -y skills add aarwitz/tapp --skill tapp
|
|
51
45
|
```
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
This installs the open Agent Skills workflow into the current project and lets the agent run the npm
|
|
48
|
+
CLI directly; no MCP server, plugin, account, API key, global Tapp install, or pasted prompt block is
|
|
49
|
+
required. Add `-g` for a user-wide install, or `--agent claude-code`, `--agent codex`, and similar
|
|
50
|
+
selectors to constrain the clients. Start or restart the agent from the application repository and
|
|
51
|
+
use the short prompt above.
|
|
56
52
|
|
|
57
|
-
**Claude
|
|
53
|
+
**Claude Code — optional enhanced skill and MCP tools:**
|
|
58
54
|
|
|
59
55
|
```bash
|
|
60
|
-
|
|
56
|
+
claude plugin marketplace add aarwitz/tapp
|
|
57
|
+
claude plugin install tapp@tapp
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
The plugin bundles the same `tapp` Agent Skill with the matching npm-backed MCP server. Use it when
|
|
61
|
+
you want inline screenshot tool results and a persistent interactive tap/read/type session; it is
|
|
62
|
+
not required for the core skill-to-CLI workflow.
|
|
66
63
|
|
|
67
|
-
**No agent integration:** run the npm package directly from an app repository:
|
|
64
|
+
**No agent integration:** run the npm package directly from an app repository in one line:
|
|
68
65
|
|
|
69
66
|
```bash
|
|
70
67
|
npx -y @aarwitz/tapp@latest init . --explore
|
|
71
68
|
```
|
|
72
69
|
|
|
73
70
|
**VS Code:** install [Tapp from the Marketplace](https://marketplace.visualstudio.com/items?itemName=lidi-solutions.tapp).
|
|
74
|
-
It contributes the same cross-platform Agent Skill to Copilot plus focused iOS simulator tools and
|
|
75
|
-
|
|
71
|
+
It contributes the same cross-platform Agent Skill to Copilot plus focused iOS simulator tools and
|
|
72
|
+
an auto-refreshing screenshot preview. The preview is not an embedded Simulator or video stream.
|
|
73
|
+
Android and web remain available through the skill's CLI/MCP workflow.
|
|
76
74
|
|
|
77
75
|
```
|
|
78
76
|
you: "Add a logout button to the settings screen"
|
|
@@ -96,7 +94,9 @@ npx -y @aarwitz/tapp@latest explore # explores the real app; prints findings
|
|
|
96
94
|
Claude Code can read the saved image with its file-reading tool; Codex can open it with
|
|
97
95
|
`view_image`. The agent should report what the screenshot proves, relay the exploration findings
|
|
98
96
|
as-is (an observation, not a merge decision — `tapp ci` gates that), and link the HTML evidence
|
|
99
|
-
report. No server, account,
|
|
97
|
+
report. No Tapp server, account, global install, API key, or bundle id is required for this loop.
|
|
98
|
+
The platform runtime still needs to be installed, and repository onboarding can write reviewed
|
|
99
|
+
configuration under `.tapp/`.
|
|
100
100
|
|
|
101
101
|
The product, executable, and package leaf are all Tapp: npm distributes it as
|
|
102
102
|
`@aarwitz/tapp`, while the installed command remains `tapp`.
|
|
@@ -171,18 +171,24 @@ npx -y @aarwitz/tapp@latest explore path/to/app-debug.apk --platform android --a
|
|
|
171
171
|
npx -y @aarwitz/tapp@latest open com.acme.app --platform android
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
Turn a driven journey into a deterministic test, or print the complete Flow format before writing
|
|
175
|
+
one by hand:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx -y @aarwitz/tapp@latest flow example
|
|
179
|
+
npx -y @aarwitz/tapp@latest flow validate .tapp/flows/smoke.yml
|
|
180
|
+
npx -y @aarwitz/tapp@latest flow run .tapp/flows/smoke.yml
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Flows support an atomic `login` step using `$TEST_EMAIL` / `$TEST_PASSWORD`; Tapp's session recorder
|
|
184
|
+
creates that step without writing credential values into the repository.
|
|
185
|
+
|
|
174
186
|
Optional but recommended (prebuilds the test harness so the first run is fast):
|
|
175
187
|
```bash
|
|
176
188
|
npx -y @aarwitz/tapp@latest install # ~2 min, one time
|
|
177
189
|
npx -y @aarwitz/tapp@latest doctor # verify Xcode / simulators / toolchain
|
|
178
190
|
```
|
|
179
191
|
|
|
180
|
-
### Optional browser workspace
|
|
181
|
-
|
|
182
|
-
`npx -y @aarwitz/tapp@latest app .` opens a local Release Studio for people who want visual repository
|
|
183
|
-
onboarding, release-plan review, and CI preparation. It is not required for the coding-agent
|
|
184
|
-
`open`/`explore` workflow.
|
|
185
|
-
|
|
186
192
|
### MCP hookup (optional)
|
|
187
193
|
|
|
188
194
|
The MCP server adds the two things a CLI can't do: **screenshots inline in your agent's
|
|
@@ -387,12 +393,6 @@ environment. Add
|
|
|
387
393
|
[`docs/scenarios.md`](docs/scenarios.md). Automatic
|
|
388
394
|
baselines are isolated by platform and target, so two same-platform apps are never compared.
|
|
389
395
|
|
|
390
|
-
**The hosted service is under development and is not currently offered for customer repositories.**
|
|
391
|
-
Do not upload private code or credentials to an old preview. The retained
|
|
392
|
-
cloud prototype is not the production SaaS boundary. Use the local Release Studio and the portable
|
|
393
|
-
GitHub Action in infrastructure you control until the new account, tenant authorization, private
|
|
394
|
-
evidence, and isolated-worker boundary passes security review.
|
|
395
|
-
|
|
396
396
|
## Make your repo agent-verified
|
|
397
397
|
|
|
398
398
|
Drop this into your repo's `AGENTS.md` (read by Codex, Cursor, Copilot, Devin, Zed, …) so
|
|
@@ -435,13 +435,6 @@ only an optional authoring/enrichment layer (`tapp_flow_generate`, `assert_ai`,
|
|
|
435
435
|
The first tool call builds the harness once (~2 min, cached in `~/.tapp`; rebuilt automatically
|
|
436
436
|
if you switch simulators). All captures land in `~/.tapp/captures/`.
|
|
437
437
|
|
|
438
|
-
## Desktop status
|
|
439
|
-
|
|
440
|
-
The macOS cockpit is frozen as a supported native interface and parity floor; it still reads the
|
|
441
|
-
canonical Application Model, release plan, and UI Map in Coverage. Its older import/build path is
|
|
442
|
-
not yet a thin client of the shared product-operation layer, so new product work is converging in
|
|
443
|
-
the browser without deleting or reducing the desktop experience.
|
|
444
|
-
|
|
445
438
|
## License
|
|
446
439
|
|
|
447
440
|
[MIT](./LICENSE)
|
package/bin/tapp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Tapp CLI — agent-driven app testing on real app surfaces.
|
|
3
3
|
//
|
|
4
4
|
// Zero-config verbs (the same engine the MCP tools use, exported by mcp-server/src/index.js):
|
|
5
5
|
// tapp explore <bundleId|appId|url> Autonomous exploration → findings + evidence (observation)
|
|
@@ -53,6 +53,19 @@ function bad(label, detail = "") {
|
|
|
53
53
|
console.log(` ❌ ${label}${detail ? ` — ${detail}` : ""}`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
let lastProgressLine = "";
|
|
57
|
+
function writeProgress(line) {
|
|
58
|
+
const text = String(line || "").trimEnd();
|
|
59
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${text} `);
|
|
60
|
+
else if (text && text !== lastProgressLine) console.error(text);
|
|
61
|
+
lastProgressLine = text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function finishProgress() {
|
|
65
|
+
if (process.stderr.isTTY && lastProgressLine) process.stderr.write("\n");
|
|
66
|
+
lastProgressLine = "";
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
function bootedSims() {
|
|
57
70
|
const r = run("xcrun", ["simctl", "list", "devices", "booted", "-j"]);
|
|
58
71
|
if (r.code !== 0) return [];
|
|
@@ -155,7 +168,7 @@ const engineImport = () => import(path.join(packageRoot, "mcp-server", "src", "i
|
|
|
155
168
|
|
|
156
169
|
function requireMacFor(what) {
|
|
157
170
|
if (process.platform === "darwin") return;
|
|
158
|
-
console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp explore https://localhost:3000`);
|
|
171
|
+
console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: npx -y @aarwitz/tapp@latest explore https://localhost:3000`);
|
|
159
172
|
process.exit(1);
|
|
160
173
|
}
|
|
161
174
|
|
|
@@ -247,7 +260,7 @@ function safeCommandUsage(verb) {
|
|
|
247
260
|
shot: "tapp shot [--out FILE]",
|
|
248
261
|
apps: "tapp apps",
|
|
249
262
|
build: "tapp build [repo] [--scheme NAME] [--configuration NAME]",
|
|
250
|
-
flow: "tapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
|
|
263
|
+
flow: "tapp flow example\ntapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
|
|
251
264
|
task: "tapp task validate FILE [--platform PLATFORM] [--map FILE]\ntapp task compile FILE --platform PLATFORM [--inputs JSON] [--out FILE]\ntapp task run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID] [--inputs JSON]",
|
|
252
265
|
contract: "tapp contract validate FILE [--platform PLATFORM] [--map FILE]\ntapp contract compile FILE --platform PLATFORM [--out FILE]\ntapp contract run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID]",
|
|
253
266
|
scenario: "tapp scenario validate FILE [--project-dir DIR]\ntapp scenario run FILE --platform web --url URL [--project-dir DIR]",
|
|
@@ -279,9 +292,8 @@ const safeHelpRequested = (rest.includes("--help") || rest.includes("-h"))
|
|
|
279
292
|
&& !["help", "version", "--version", "-v"].includes(command)
|
|
280
293
|
&& (command !== "ci" || rest[0] === "install");
|
|
281
294
|
if (safeHelpRequested) {
|
|
282
|
-
console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens.
|
|
283
|
-
|
|
284
|
-
rest = [];
|
|
295
|
+
console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens. Run \`tapp --help\` for the full command reference.`);
|
|
296
|
+
process.exit(0);
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
// Create TAPP_HOME only for commands that actually use it — never for help/version/--help.
|
|
@@ -350,7 +362,7 @@ switch (command) {
|
|
|
350
362
|
runExploration: engine?.runInitExploration,
|
|
351
363
|
onProgress: (progress) => {
|
|
352
364
|
const activePlatform = progress.platform || platform;
|
|
353
|
-
|
|
365
|
+
writeProgress(`🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${activePlatform === "web" ? "pages reached" : activePlatform === "ios" ? "structural states observed" : "screens reached"}`);
|
|
354
366
|
},
|
|
355
367
|
onStatus: (status) => console.error(`⏳ ${status}`),
|
|
356
368
|
outDir,
|
|
@@ -377,11 +389,11 @@ switch (command) {
|
|
|
377
389
|
}
|
|
378
390
|
}
|
|
379
391
|
if (failure) {
|
|
380
|
-
if (explore)
|
|
392
|
+
if (explore) finishProgress();
|
|
381
393
|
printEngineError({ error: `Could not initialize repository: ${failure.message || String(failure)}`, details: failure.details || {} });
|
|
382
394
|
process.exit(2);
|
|
383
395
|
}
|
|
384
|
-
if (explore)
|
|
396
|
+
if (explore) finishProgress();
|
|
385
397
|
const built = { model: result.model, plan: result.plan };
|
|
386
398
|
const written = result.written;
|
|
387
399
|
const exploration = result.exploration;
|
|
@@ -547,7 +559,7 @@ switch (command) {
|
|
|
547
559
|
const modelPlatform = typeof flags.platform === "string" ? flags.platform.toLowerCase() : "";
|
|
548
560
|
if (modelPlatform === "ios") requireMacFor("iOS testing");
|
|
549
561
|
const onProgress = (p) =>
|
|
550
|
-
|
|
562
|
+
writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} states observed`);
|
|
551
563
|
const r = await engine.runExploreTarget({
|
|
552
564
|
projectDir: process.cwd(),
|
|
553
565
|
platform: modelPlatform,
|
|
@@ -563,7 +575,7 @@ switch (command) {
|
|
|
563
575
|
onProgress,
|
|
564
576
|
onStatus: (t) => console.error(`ℹ️ ${t}`),
|
|
565
577
|
});
|
|
566
|
-
|
|
578
|
+
finishProgress();
|
|
567
579
|
if (r.error) { printEngineError(r); process.exit(1); }
|
|
568
580
|
console.log(r.text);
|
|
569
581
|
if (flags.json && typeof flags.json === "string") {
|
|
@@ -588,14 +600,14 @@ switch (command) {
|
|
|
588
600
|
process.exit(2);
|
|
589
601
|
}
|
|
590
602
|
if (platform === "web" && !/^https?:\/\//i.test(target)) {
|
|
591
|
-
console.error("❌ Web
|
|
603
|
+
console.error("❌ Web exploration needs an http(s) URL");
|
|
592
604
|
process.exit(2);
|
|
593
605
|
}
|
|
594
606
|
const bundleId = platform === "ios" ? await resolveTargetOrExit(engine, target) : null;
|
|
595
607
|
const android = platform === "android" ? androidTarget(flags, target) : null;
|
|
596
608
|
const progressMetric = platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached";
|
|
597
609
|
const onProgress = (p) =>
|
|
598
|
-
|
|
610
|
+
writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${progressMetric}`);
|
|
599
611
|
const r = platform === "web"
|
|
600
612
|
? await engine.runQaWeb({
|
|
601
613
|
url: target,
|
|
@@ -628,7 +640,7 @@ switch (command) {
|
|
|
628
640
|
surface: "cli",
|
|
629
641
|
onProgress,
|
|
630
642
|
});
|
|
631
|
-
|
|
643
|
+
finishProgress();
|
|
632
644
|
if (r.error) {
|
|
633
645
|
printEngineError(r);
|
|
634
646
|
process.exit(1);
|
|
@@ -839,8 +851,22 @@ switch (command) {
|
|
|
839
851
|
printEngineError(inst);
|
|
840
852
|
process.exit(1);
|
|
841
853
|
}
|
|
854
|
+
let modelRefresh = null;
|
|
855
|
+
try {
|
|
856
|
+
const { persistIosBuildValidation } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
|
|
857
|
+
modelRefresh = await persistIosBuildValidation({
|
|
858
|
+
projectDir: dir,
|
|
859
|
+
bundleId: inst.bundleId,
|
|
860
|
+
container: built.container,
|
|
861
|
+
scheme: built.scheme,
|
|
862
|
+
configuration: built.configuration,
|
|
863
|
+
});
|
|
864
|
+
} catch (error) {
|
|
865
|
+
console.error(`⚠️ Build succeeded, but Tapp could not refresh the existing application model: ${error.message || String(error)}`);
|
|
866
|
+
}
|
|
842
867
|
console.log(`🔨 Built ${path.basename(built.appPath)} (scheme ${built.scheme}) — installed as ${inst.bundleId}`);
|
|
843
|
-
console.log(
|
|
868
|
+
if (modelRefresh) console.log(` application model refreshed: ${modelRefresh.modelPath}`);
|
|
869
|
+
console.log(`\nNext: npx -y @aarwitz/tapp@latest explore ${inst.bundleId}`);
|
|
844
870
|
break;
|
|
845
871
|
}
|
|
846
872
|
|
|
@@ -923,8 +949,12 @@ switch (command) {
|
|
|
923
949
|
const { flags, positionals } = parseVerbArgs(rest);
|
|
924
950
|
const verb = positionals[0] || "run";
|
|
925
951
|
const flowPath = positionals[1] || (verb === "run" || verb === "validate" ? "" : verb);
|
|
952
|
+
if (verb === "example") {
|
|
953
|
+
console.log(`# Tapp Flow — deterministic, keyless replay\nname: sign-in-smoke\nplatform: web\nurl: https://example.test/login\nsteps:\n - login:\n email: $TEST_EMAIL\n password: $TEST_PASSWORD\n - wait_for: Dashboard\n - assert_screen: Dashboard\n`);
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
926
956
|
if (!["run", "validate"].includes(verb) || !flowPath) {
|
|
927
|
-
console.error("usage: tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
|
|
957
|
+
console.error("usage: tapp flow example\n tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
|
|
928
958
|
process.exit(2);
|
|
929
959
|
}
|
|
930
960
|
const absolute = path.resolve(flowPath);
|
|
@@ -1054,7 +1084,7 @@ switch (command) {
|
|
|
1054
1084
|
try {
|
|
1055
1085
|
const adopted = adoptPrCoverageProposal({ projectDir, prPlanPath, item: flags.item, releasePlanPath: typeof flags["release-plan"] === "string" ? flags["release-plan"] : undefined });
|
|
1056
1086
|
console.log(`📥 ${adopted.mode === "reconciled-existing" ? "Reconciled PR evidence into" : "Adopted"} ${adopted.item.name}${adopted.mode === "reconciled-existing" ? ` while preserving decision '${adopted.item.decision}'` : " as a pending release-plan item"}; no Task or contract was generated or trusted`);
|
|
1057
|
-
console.log(` plan: ${adopted.path}\n next: tapp plan review ${adopted.path} --approve ${adopted.item.id}`);
|
|
1087
|
+
console.log(` plan: ${adopted.path}\n next: npx -y @aarwitz/tapp@latest plan review ${adopted.path} --approve ${adopted.item.id}`);
|
|
1058
1088
|
} catch (error) { console.error(`❌ Could not adopt PR coverage proposal: ${error.message || String(error)}`); process.exit(2); }
|
|
1059
1089
|
break;
|
|
1060
1090
|
}
|
|
@@ -1214,6 +1244,13 @@ switch (command) {
|
|
|
1214
1244
|
const python = run("python3", ["--version"]);
|
|
1215
1245
|
python.code === 0 ? ok("python3", `${python.stdout} (used by Flows)`) : bad("python3", "not found — Flow replay needs python3 + pyyaml (everything else works)");
|
|
1216
1246
|
|
|
1247
|
+
const { storagePreflight } = await import(path.join(packageRoot, "mcp-server", "src", "environment-preflight.js"));
|
|
1248
|
+
const storage = storagePreflight(tappHome);
|
|
1249
|
+
if (storage.level === "blocked") { bad("Disk space", storage.message); healthy = false; }
|
|
1250
|
+
else if (storage.level === "warning") console.log(` ⚠️ Disk space — ${storage.message}`);
|
|
1251
|
+
else if (storage.level === "ok") ok("Disk space", storage.message);
|
|
1252
|
+
else console.log(` ⬜ Disk space — ${storage.message || "could not be checked"}`);
|
|
1253
|
+
|
|
1217
1254
|
console.log("\n Platforms:");
|
|
1218
1255
|
if (process.platform === "darwin") {
|
|
1219
1256
|
const xcode = run("xcode-select", ["-p"]);
|
|
@@ -1223,7 +1260,7 @@ switch (command) {
|
|
|
1223
1260
|
const booted = bootedSims();
|
|
1224
1261
|
ok("iOS", `${ver || "Xcode"}; ${booted.length ? `${booted[0].name} booted` : "no simulator booted yet"}`);
|
|
1225
1262
|
const xctestrun = harnessXctestrun();
|
|
1226
|
-
xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: tapp install)");
|
|
1263
|
+
xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: npx -y @aarwitz/tapp@latest install)");
|
|
1227
1264
|
} else {
|
|
1228
1265
|
console.log(" ⬜ iOS — unavailable (install Xcode + simulator runtime)");
|
|
1229
1266
|
}
|
|
@@ -1257,7 +1294,7 @@ switch (command) {
|
|
|
1257
1294
|
console.log(`\n Home: ${tappHome}`);
|
|
1258
1295
|
console.log(healthy
|
|
1259
1296
|
? "\nReady. Start with:\n npx -y @aarwitz/tapp@latest open [target]\n npx -y @aarwitz/tapp@latest explore [target]"
|
|
1260
|
-
: "\nFix the ❌ items above, then re-run: tapp doctor");
|
|
1297
|
+
: "\nFix the ❌ items above, then re-run: npx -y @aarwitz/tapp@latest doctor");
|
|
1261
1298
|
process.exit(healthy ? 0 : 1);
|
|
1262
1299
|
}
|
|
1263
1300
|
|
|
@@ -1332,7 +1369,13 @@ switch (command) {
|
|
|
1332
1369
|
console.log(`✅ Actor '${name}' configured — ${result.actor.session} session · ${result.actor.provisioning} provisioning`);
|
|
1333
1370
|
console.log(` ${result.path}`);
|
|
1334
1371
|
console.log(` bindings: ${Object.entries(result.actor.credentials).map(([key, binding]) => `${key}=$${binding.env}`).join(", ") || "none"}`);
|
|
1335
|
-
|
|
1372
|
+
const { refreshExistingInitArtifacts } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
|
|
1373
|
+
let refreshed = null;
|
|
1374
|
+
let refreshWarning = "";
|
|
1375
|
+
try { refreshed = await refreshExistingInitArtifacts({ projectDir }); }
|
|
1376
|
+
catch (error) { refreshWarning = error.message || String(error); }
|
|
1377
|
+
console.log(` No credential values were accepted or written.${refreshed ? ` Application model refreshed: ${refreshed.modelPath}` : " Run npx -y @aarwitz/tapp@latest init when you are ready to create the application model."}`);
|
|
1378
|
+
if (refreshWarning) console.error(`⚠️ Actor was saved, but Tapp could not refresh the existing application model: ${refreshWarning}`);
|
|
1336
1379
|
} catch (error) { console.error(`❌ Actor not configured: ${error.message || String(error)}`); process.exit(2); }
|
|
1337
1380
|
break;
|
|
1338
1381
|
}
|
|
@@ -1347,7 +1390,7 @@ switch (command) {
|
|
|
1347
1390
|
const projectDir = fs.realpathSync(path.resolve(positionals[1] || (typeof flags["project-dir"] === "string" ? flags["project-dir"] : process.cwd())));
|
|
1348
1391
|
const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
|
|
1349
1392
|
if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
|
|
1350
|
-
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore, review/generate/validate/promote the plan, then create the baseline.`);
|
|
1393
|
+
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore, review/generate/validate/promote the plan, then create the baseline.`);
|
|
1351
1394
|
process.exit(2);
|
|
1352
1395
|
}
|
|
1353
1396
|
let model;
|
|
@@ -1454,7 +1497,7 @@ switch (command) {
|
|
|
1454
1497
|
catch { console.error(`❌ Repository directory not found: ${positionals[0] || flags["project-dir"] || process.cwd()}`); process.exit(2); }
|
|
1455
1498
|
const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
|
|
1456
1499
|
if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
|
|
1457
|
-
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore first.`);
|
|
1500
|
+
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore first.`);
|
|
1458
1501
|
process.exit(2);
|
|
1459
1502
|
}
|
|
1460
1503
|
const actionRef = typeof flags["action-ref"] === "string" ? flags["action-ref"] : `aarwitz/tapp@v${pkg.version}`;
|
|
@@ -1571,17 +1614,17 @@ switch (command) {
|
|
|
1571
1614
|
}
|
|
1572
1615
|
|
|
1573
1616
|
default: {
|
|
1574
|
-
console.log(`tapp v${pkg.version} —
|
|
1617
|
+
console.log(`tapp v${pkg.version} — agent-driven app testing for iOS, Android, and web.
|
|
1575
1618
|
|
|
1576
|
-
Core —
|
|
1619
|
+
Core — inspect, explore, gate (no Tapp account or server required):
|
|
1577
1620
|
tapp explore [target] Autonomous exploration → findings + evidence (an observation, NOT a
|
|
1578
|
-
release decision — run 'tapp ci' to gate a merge)
|
|
1621
|
+
release decision — run 'npx -y @aarwitz/tapp@latest ci' to gate a merge)
|
|
1579
1622
|
(web: --watch · all: --platform ios|android|web · --actions N)
|
|
1580
1623
|
tapp contract run FILE Replay a business-level release contract — the guarantees that must hold
|
|
1581
1624
|
tapp ci ... Merge-blocking release gate — explore + suites + baseline → pass/fail/inconclusive
|
|
1582
1625
|
(see: tapp ci --help)
|
|
1583
1626
|
|
|
1584
|
-
Primitives — an agent's eyes and hands
|
|
1627
|
+
Primitives — an agent's eyes and hands:
|
|
1585
1628
|
tapp open [target] Launch the app → screen summary + screenshot saved to a file
|
|
1586
1629
|
(web: --tap TEXT · --wait-for TEXT · --out FILE)
|
|
1587
1630
|
tapp tree [target] Accessibility tree of the current screen (--json for every element)
|
|
@@ -1597,6 +1640,7 @@ Repository & release:
|
|
|
1597
1640
|
tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
|
|
1598
1641
|
|
|
1599
1642
|
Advanced — deterministic suites, lifecycle & compilers:
|
|
1643
|
+
tapp flow example Print a complete starter Flow YAML (no target or MCP required)
|
|
1600
1644
|
tapp flow run FILE Replay a committed deterministic Flow (no AI/API key)
|
|
1601
1645
|
tapp flow validate FILE Validate a Flow without launching a target
|
|
1602
1646
|
tapp task validate FILE Validate a reusable deterministic Task (+ optional UI Map grounding)
|
|
@@ -1636,7 +1680,7 @@ Setup:
|
|
|
1636
1680
|
tapp doctor Check Xcode / simulators / toolchain
|
|
1637
1681
|
tapp mcp Start the MCP server on stdio (adds inline screenshots + interactive sessions)
|
|
1638
1682
|
|
|
1639
|
-
Agent Skill (
|
|
1683
|
+
Agent Skill (recommended — so a short “Use Tapp to test this app” prompt is enough):
|
|
1640
1684
|
Any supported agent: npx -y skills add aarwitz/tapp --skill tapp
|
|
1641
1685
|
Claude skill + MCP: claude plugin marketplace add aarwitz/tapp
|
|
1642
1686
|
claude plugin install tapp@tapp
|
|
@@ -74,6 +74,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
|
|
|
74
74
|
const visited = new Map();
|
|
75
75
|
let issues = 0;
|
|
76
76
|
let actions = 0;
|
|
77
|
+
let loginTried = false;
|
|
77
78
|
const crashExitBaseline = typeof d.latestCrashExitInfo === "function" ? await d.latestCrashExitInfo() : null;
|
|
78
79
|
let snap = await d.launch({ clearData });
|
|
79
80
|
let crashReported = false;
|
|
@@ -210,6 +211,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
|
|
|
210
211
|
if (candidate) {
|
|
211
212
|
const target = controlLabel(candidate);
|
|
212
213
|
const loginSubmit = inputs.some((input) => input.secure) && isAndroidAuthSubmit(candidate);
|
|
214
|
+
if (loginSubmit) loginTried = true;
|
|
213
215
|
tried.add(`${hash}|tap|${target}`);
|
|
214
216
|
const before = hash;
|
|
215
217
|
const r = await d.tap(target, snap);
|
|
@@ -260,7 +262,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
|
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
const timedOut = Date.now() >= deadline;
|
|
263
|
-
emit("COMPLETE", { actions, states: visited.size, issues, screens: [...new Set(visited.values())].join(","), outcome: timedOut ? "timeout" : "complete", timedOut, ...(timedOut ? { timeoutSeconds: timeoutSec } : {}) });
|
|
265
|
+
emit("COMPLETE", { actions, states: visited.size, issues, screens: [...new Set(visited.values())].join(","), outcome: timedOut ? "timeout" : "complete", timedOut, credentialsProvided: !!(testEmail || testPassword), credentialsUsed: loginTried, ...(timedOut ? { timeoutSeconds: timeoutSec } : {}) });
|
|
264
266
|
onProgress({ action: actions, max: maxActions, states: visited.size });
|
|
265
267
|
return { markersPath, outDir, actions, states: visited.size, issues, timedOut, seedTargets: normalizedTargets };
|
|
266
268
|
}
|