appium-pilot 0.1.0__tar.gz
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.
- appium_pilot-0.1.0/.claude/settings.local.json +8 -0
- appium_pilot-0.1.0/.gitignore +20 -0
- appium_pilot-0.1.0/LICENSE +21 -0
- appium_pilot-0.1.0/Makefile +28 -0
- appium_pilot-0.1.0/PKG-INFO +138 -0
- appium_pilot-0.1.0/README.md +106 -0
- appium_pilot-0.1.0/pyproject.toml +62 -0
- appium_pilot-0.1.0/src/appium_pilot/__init__.py +3 -0
- appium_pilot-0.1.0/src/appium_pilot/__main__.py +4 -0
- appium_pilot-0.1.0/src/appium_pilot/cli.py +96 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/__init__.py +1 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/app_cmd.py +106 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/capture_cmd.py +48 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/devices_cmd.py +30 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/doctor_cmd.py +103 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/gesture_cmd.py +89 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/open_cmd.py +114 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/session_cmd.py +108 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/skills_cmd.py +149 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/snapshot_cmd.py +35 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/tap_cmd.py +25 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/type_cmd.py +54 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/video_cmd.py +63 -0
- appium_pilot-0.1.0/src/appium_pilot/commands/wait_cmd.py +45 -0
- appium_pilot-0.1.0/src/appium_pilot/config.py +44 -0
- appium_pilot-0.1.0/src/appium_pilot/devices.py +185 -0
- appium_pilot-0.1.0/src/appium_pilot/output.py +61 -0
- appium_pilot-0.1.0/src/appium_pilot/proc.py +43 -0
- appium_pilot-0.1.0/src/appium_pilot/resolve.py +44 -0
- appium_pilot-0.1.0/src/appium_pilot/server.py +84 -0
- appium_pilot-0.1.0/src/appium_pilot/session.py +109 -0
- appium_pilot-0.1.0/src/appium_pilot/skilldata/SKILL.md +129 -0
- appium_pilot-0.1.0/src/appium_pilot/snapshot.py +86 -0
- appium_pilot-0.1.0/src/appium_pilot/strategies/__init__.py +19 -0
- appium_pilot-0.1.0/src/appium_pilot/strategies/android.py +157 -0
- appium_pilot-0.1.0/src/appium_pilot/strategies/base.py +118 -0
- appium_pilot-0.1.0/src/appium_pilot/strategies/ios.py +132 -0
- appium_pilot-0.1.0/tests/conftest.py +16 -0
- appium_pilot-0.1.0/tests/e2e/apps.py +139 -0
- appium_pilot-0.1.0/tests/e2e/conftest.py +74 -0
- appium_pilot-0.1.0/tests/e2e/test_actions_e2e.py +40 -0
- appium_pilot-0.1.0/tests/e2e/test_gestures_e2e.py +24 -0
- appium_pilot-0.1.0/tests/e2e/test_inspection_e2e.py +44 -0
- appium_pilot-0.1.0/tests/e2e/test_install_remove_e2e.py +23 -0
- appium_pilot-0.1.0/tests/e2e/test_keys_orientation_e2e.py +37 -0
- appium_pilot-0.1.0/tests/e2e/test_lifecycle_e2e.py +20 -0
- appium_pilot-0.1.0/tests/e2e/test_video_e2e.py +21 -0
- appium_pilot-0.1.0/tests/e2e/test_waits_e2e.py +23 -0
- appium_pilot-0.1.0/tests/fixtures/android_source.xml +16 -0
- appium_pilot-0.1.0/tests/fixtures/ios_source.xml +10 -0
- appium_pilot-0.1.0/tests/unit/test_config.py +23 -0
- appium_pilot-0.1.0/tests/unit/test_locators.py +75 -0
- appium_pilot-0.1.0/tests/unit/test_open_helpers.py +56 -0
- appium_pilot-0.1.0/tests/unit/test_output.py +59 -0
- appium_pilot-0.1.0/tests/unit/test_parser.py +45 -0
- appium_pilot-0.1.0/tests/unit/test_quoting.py +25 -0
- appium_pilot-0.1.0/tests/unit/test_recording_options.py +15 -0
- appium_pilot-0.1.0/tests/unit/test_session.py +52 -0
- appium_pilot-0.1.0/tests/unit/test_skills.py +40 -0
- appium_pilot-0.1.0/tests/unit/test_snapshot.py +40 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
*.pyc
|
|
7
|
+
|
|
8
|
+
# appium-pilot artifact output (when run from this dir)
|
|
9
|
+
/appium-pilot/
|
|
10
|
+
|
|
11
|
+
# test artifacts
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
tests/_apps/
|
|
14
|
+
|
|
15
|
+
# editor
|
|
16
|
+
.vscode/
|
|
17
|
+
.idea/
|
|
18
|
+
|
|
19
|
+
# internal notes (not tracked)
|
|
20
|
+
/notes/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marco Perugini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
VENV ?= .venv
|
|
2
|
+
ifeq ($(OS),Windows_NT)
|
|
3
|
+
BIN := $(VENV)/Scripts
|
|
4
|
+
else
|
|
5
|
+
BIN := $(VENV)/bin
|
|
6
|
+
endif
|
|
7
|
+
PY := $(BIN)/python
|
|
8
|
+
PYTEST := $(BIN)/pytest
|
|
9
|
+
|
|
10
|
+
.PHONY: install test test-e2e test-e2e-ios test-e2e-android lint
|
|
11
|
+
|
|
12
|
+
install: ## install the package + dev deps into the venv
|
|
13
|
+
$(PY) -m pip install -e ".[dev]"
|
|
14
|
+
|
|
15
|
+
test: ## fast unit tests, no device (the after-every-change default)
|
|
16
|
+
$(PYTEST)
|
|
17
|
+
|
|
18
|
+
test-e2e-android: ## device-backed E2E against an Android emulator
|
|
19
|
+
$(PYTEST) -m e2e --platform=android
|
|
20
|
+
|
|
21
|
+
test-e2e-ios: ## device-backed E2E against an iOS simulator
|
|
22
|
+
$(PYTEST) -m e2e --platform=ios
|
|
23
|
+
|
|
24
|
+
test-e2e: ## device-backed E2E on both platforms
|
|
25
|
+
$(PYTEST) -m e2e --platform=both
|
|
26
|
+
|
|
27
|
+
lint: ## ruff lint (config in pyproject)
|
|
28
|
+
$(BIN)/ruff check src tests
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appium-pilot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-first, session-based CLI for driving mobile apps via Appium
|
|
5
|
+
Project-URL: Homepage, https://github.com/theperu/appium-pilot
|
|
6
|
+
Project-URL: Repository, https://github.com/theperu/appium-pilot
|
|
7
|
+
Project-URL: Issues, https://github.com/theperu/appium-pilot/issues
|
|
8
|
+
Author: Marco Perugini
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,android,appium,automation,cli,ios,llm,mobile,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: appium-python-client>=4.0.0
|
|
27
|
+
Requires-Dist: selenium>=4.20.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# appium-pilot
|
|
34
|
+
|
|
35
|
+
An agent-first, session-based CLI for driving native mobile apps via Appium.
|
|
36
|
+
One verb per invocation, state persisted across calls, output tuned for an LLM
|
|
37
|
+
agent: `snapshot` the screen to get element refs, then act on them by ref.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install . # or: pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You also need Node + the Appium server and drivers (appium-pilot does not install
|
|
46
|
+
them — run `appium-pilot doctor` to see what's missing):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm i -g appium
|
|
50
|
+
appium driver install uiautomator2 # Android
|
|
51
|
+
appium driver install xcuitest # iOS
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Launch an installed Android app (auto-picks a booted emulator)
|
|
58
|
+
appium-pilot open --platform android --app-package com.example.app
|
|
59
|
+
|
|
60
|
+
# Or an iOS Simulator app bundle
|
|
61
|
+
appium-pilot open --platform ios --app /path/to/MyApp.app
|
|
62
|
+
|
|
63
|
+
# Inspect the screen — returns filtered XML with e1..eN refs
|
|
64
|
+
appium-pilot snapshot
|
|
65
|
+
|
|
66
|
+
# Act on a ref from the latest snapshot
|
|
67
|
+
appium-pilot tap e7
|
|
68
|
+
|
|
69
|
+
# Multiple parallel sessions
|
|
70
|
+
appium-pilot -s=checkout open --platform ios --app /path/to/MyApp.app
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Session state lives under `~/.appium-pilot/` (so any invocation can reattach
|
|
74
|
+
regardless of cwd). Artifacts you want to find — screenshots and videos — are
|
|
75
|
+
written to `./appium-pilot/` in the current directory (override with
|
|
76
|
+
`APPIUM_PILOT_OUTPUT`).
|
|
77
|
+
|
|
78
|
+
## Use from AI coding agents
|
|
79
|
+
|
|
80
|
+
A skill ships inside the package so agents discover the commands and drive the
|
|
81
|
+
CLI themselves. Install it into whichever tool you use — no manual symlink:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
appium-pilot skills install # Claude Code (~/.claude/skills/), the default
|
|
85
|
+
appium-pilot skills install --tool cursor # Cursor (.cursor/rules/)
|
|
86
|
+
appium-pilot skills install --tool copilot # GitHub Copilot (.github/copilot-instructions.md)
|
|
87
|
+
appium-pilot skills install --tool agents # AGENTS.md (the cross-tool standard)
|
|
88
|
+
appium-pilot skills install --tool all # all of the above
|
|
89
|
+
appium-pilot skills uninstall --tool all # remove
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`claude` installs user-level (once, for every project); the others write into the
|
|
93
|
+
current project. The skill source lives at `src/appium_pilot/skilldata/SKILL.md`;
|
|
94
|
+
re-run `skills install` after editing it.
|
|
95
|
+
|
|
96
|
+
## Platform support
|
|
97
|
+
|
|
98
|
+
- **macOS** — full: iOS Simulator + Android Emulator.
|
|
99
|
+
- **Windows / Linux** — Android only (iOS needs a Mac). External tools are
|
|
100
|
+
resolved through `shutil.which` (honoring `PATHEXT`, so `appium.cmd`/`adb.exe`
|
|
101
|
+
work) and background processes detach via OS-appropriate flags. On Windows,
|
|
102
|
+
`make` is uncommon — run the underlying commands directly
|
|
103
|
+
(`.venv\Scripts\pytest`, `python -m appium_pilot ...`). The Windows path is
|
|
104
|
+
implemented but not yet exercised on a Windows host.
|
|
105
|
+
|
|
106
|
+
## Testing
|
|
107
|
+
|
|
108
|
+
Two tiers (see `tests/`):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
make install # pip install -e ".[dev]"
|
|
112
|
+
make test # fast unit tests, no device — run after every change
|
|
113
|
+
make test-e2e-android # device-backed E2E (auto-boots an emulator)
|
|
114
|
+
make test-e2e-ios # device-backed E2E (auto-boots a simulator)
|
|
115
|
+
make test-e2e # both platforms
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **Unit** (`tests/unit/`, ~0.2s, no device): locks in the snapshot filtering,
|
|
119
|
+
per-platform locators, quoting, output/JSON contract, parsing and config logic.
|
|
120
|
+
`pytest` runs only these by default.
|
|
121
|
+
- **E2E** (`tests/e2e/`, marked `e2e`): drives the real CLI against official
|
|
122
|
+
Appium sample apps — ApiDemos (Android) and TestApp (iOS, built on demand) —
|
|
123
|
+
covering every command on both platforms. Apps are cached under `tests/_apps/`;
|
|
124
|
+
tests skip cleanly if a device/toolchain is unavailable.
|
|
125
|
+
|
|
126
|
+
## Status
|
|
127
|
+
|
|
128
|
+
All v1 commands implemented and smoke-tested end-to-end on an iOS Simulator
|
|
129
|
+
(open → snapshot → tap → re-snapshot → screenshot → stale-ref handling → close):
|
|
130
|
+
|
|
131
|
+
`open` · `close` · `list` · `close-all` · `kill-all` · `snapshot [--raw]` ·
|
|
132
|
+
`source` · `screenshot [ref]` · `devices` · `doctor` · `tap` · `type` · `clear` ·
|
|
133
|
+
`swipe` · `scroll` · `press` · `hide-keyboard` · `orientation` · `wait` ·
|
|
134
|
+
`video-start`/`video-stop` ·
|
|
135
|
+
`launch`/`activate`/`terminate`/`background`/`install`/`remove`/`reset`.
|
|
136
|
+
|
|
137
|
+
Targets iOS Simulator + Android Emulator. Deferred to v2: hybrid webview,
|
|
138
|
+
geolocation, network conditions, deep links, real devices, cloud farms.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# appium-pilot
|
|
2
|
+
|
|
3
|
+
An agent-first, session-based CLI for driving native mobile apps via Appium.
|
|
4
|
+
One verb per invocation, state persisted across calls, output tuned for an LLM
|
|
5
|
+
agent: `snapshot` the screen to get element refs, then act on them by ref.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install . # or: pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You also need Node + the Appium server and drivers (appium-pilot does not install
|
|
14
|
+
them — run `appium-pilot doctor` to see what's missing):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i -g appium
|
|
18
|
+
appium driver install uiautomator2 # Android
|
|
19
|
+
appium driver install xcuitest # iOS
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Launch an installed Android app (auto-picks a booted emulator)
|
|
26
|
+
appium-pilot open --platform android --app-package com.example.app
|
|
27
|
+
|
|
28
|
+
# Or an iOS Simulator app bundle
|
|
29
|
+
appium-pilot open --platform ios --app /path/to/MyApp.app
|
|
30
|
+
|
|
31
|
+
# Inspect the screen — returns filtered XML with e1..eN refs
|
|
32
|
+
appium-pilot snapshot
|
|
33
|
+
|
|
34
|
+
# Act on a ref from the latest snapshot
|
|
35
|
+
appium-pilot tap e7
|
|
36
|
+
|
|
37
|
+
# Multiple parallel sessions
|
|
38
|
+
appium-pilot -s=checkout open --platform ios --app /path/to/MyApp.app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Session state lives under `~/.appium-pilot/` (so any invocation can reattach
|
|
42
|
+
regardless of cwd). Artifacts you want to find — screenshots and videos — are
|
|
43
|
+
written to `./appium-pilot/` in the current directory (override with
|
|
44
|
+
`APPIUM_PILOT_OUTPUT`).
|
|
45
|
+
|
|
46
|
+
## Use from AI coding agents
|
|
47
|
+
|
|
48
|
+
A skill ships inside the package so agents discover the commands and drive the
|
|
49
|
+
CLI themselves. Install it into whichever tool you use — no manual symlink:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
appium-pilot skills install # Claude Code (~/.claude/skills/), the default
|
|
53
|
+
appium-pilot skills install --tool cursor # Cursor (.cursor/rules/)
|
|
54
|
+
appium-pilot skills install --tool copilot # GitHub Copilot (.github/copilot-instructions.md)
|
|
55
|
+
appium-pilot skills install --tool agents # AGENTS.md (the cross-tool standard)
|
|
56
|
+
appium-pilot skills install --tool all # all of the above
|
|
57
|
+
appium-pilot skills uninstall --tool all # remove
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`claude` installs user-level (once, for every project); the others write into the
|
|
61
|
+
current project. The skill source lives at `src/appium_pilot/skilldata/SKILL.md`;
|
|
62
|
+
re-run `skills install` after editing it.
|
|
63
|
+
|
|
64
|
+
## Platform support
|
|
65
|
+
|
|
66
|
+
- **macOS** — full: iOS Simulator + Android Emulator.
|
|
67
|
+
- **Windows / Linux** — Android only (iOS needs a Mac). External tools are
|
|
68
|
+
resolved through `shutil.which` (honoring `PATHEXT`, so `appium.cmd`/`adb.exe`
|
|
69
|
+
work) and background processes detach via OS-appropriate flags. On Windows,
|
|
70
|
+
`make` is uncommon — run the underlying commands directly
|
|
71
|
+
(`.venv\Scripts\pytest`, `python -m appium_pilot ...`). The Windows path is
|
|
72
|
+
implemented but not yet exercised on a Windows host.
|
|
73
|
+
|
|
74
|
+
## Testing
|
|
75
|
+
|
|
76
|
+
Two tiers (see `tests/`):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
make install # pip install -e ".[dev]"
|
|
80
|
+
make test # fast unit tests, no device — run after every change
|
|
81
|
+
make test-e2e-android # device-backed E2E (auto-boots an emulator)
|
|
82
|
+
make test-e2e-ios # device-backed E2E (auto-boots a simulator)
|
|
83
|
+
make test-e2e # both platforms
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Unit** (`tests/unit/`, ~0.2s, no device): locks in the snapshot filtering,
|
|
87
|
+
per-platform locators, quoting, output/JSON contract, parsing and config logic.
|
|
88
|
+
`pytest` runs only these by default.
|
|
89
|
+
- **E2E** (`tests/e2e/`, marked `e2e`): drives the real CLI against official
|
|
90
|
+
Appium sample apps — ApiDemos (Android) and TestApp (iOS, built on demand) —
|
|
91
|
+
covering every command on both platforms. Apps are cached under `tests/_apps/`;
|
|
92
|
+
tests skip cleanly if a device/toolchain is unavailable.
|
|
93
|
+
|
|
94
|
+
## Status
|
|
95
|
+
|
|
96
|
+
All v1 commands implemented and smoke-tested end-to-end on an iOS Simulator
|
|
97
|
+
(open → snapshot → tap → re-snapshot → screenshot → stale-ref handling → close):
|
|
98
|
+
|
|
99
|
+
`open` · `close` · `list` · `close-all` · `kill-all` · `snapshot [--raw]` ·
|
|
100
|
+
`source` · `screenshot [ref]` · `devices` · `doctor` · `tap` · `type` · `clear` ·
|
|
101
|
+
`swipe` · `scroll` · `press` · `hide-keyboard` · `orientation` · `wait` ·
|
|
102
|
+
`video-start`/`video-stop` ·
|
|
103
|
+
`launch`/`activate`/`terminate`/`background`/`install`/`remove`/`reset`.
|
|
104
|
+
|
|
105
|
+
Targets iOS Simulator + Android Emulator. Deferred to v2: hybrid webview,
|
|
106
|
+
geolocation, network conditions, deep links, real devices, cloud farms.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "appium-pilot"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agent-first, session-based CLI for driving mobile apps via Appium"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Marco Perugini" }]
|
|
13
|
+
keywords = ["appium", "mobile", "automation", "cli", "android", "ios", "testing", "agent", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"Appium-Python-Client>=4.0.0",
|
|
31
|
+
"selenium>=4.20.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/theperu/appium-pilot"
|
|
36
|
+
Repository = "https://github.com/theperu/appium-pilot"
|
|
37
|
+
Issues = "https://github.com/theperu/appium-pilot/issues"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["pytest>=8", "ruff"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
appium-pilot = "appium_pilot.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
addopts = "-ra -m 'not e2e'"
|
|
48
|
+
markers = [
|
|
49
|
+
"e2e: device-backed end-to-end test (needs a simulator/emulator)",
|
|
50
|
+
"ios: iOS-specific test",
|
|
51
|
+
"android: Android-specific test",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/appium_pilot"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
58
|
+
"src/appium_pilot/skilldata/SKILL.md" = "appium_pilot/skilldata/SKILL.md"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 110
|
|
62
|
+
target-version = "py310"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""appium-pilot front-end: global flags, subcommand dispatch, error mapping.
|
|
2
|
+
|
|
3
|
+
Supports `-s=<name>` session selection (default "default") placed anywhere
|
|
4
|
+
before the subcommand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from selenium.common.exceptions import WebDriverException
|
|
14
|
+
|
|
15
|
+
from appium_pilot import __version__
|
|
16
|
+
from appium_pilot.commands import (
|
|
17
|
+
app_cmd,
|
|
18
|
+
capture_cmd,
|
|
19
|
+
devices_cmd,
|
|
20
|
+
doctor_cmd,
|
|
21
|
+
gesture_cmd,
|
|
22
|
+
open_cmd,
|
|
23
|
+
session_cmd,
|
|
24
|
+
skills_cmd,
|
|
25
|
+
snapshot_cmd,
|
|
26
|
+
tap_cmd,
|
|
27
|
+
type_cmd,
|
|
28
|
+
video_cmd,
|
|
29
|
+
wait_cmd,
|
|
30
|
+
)
|
|
31
|
+
from appium_pilot.output import CommandError, fail, set_json_mode
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="appium-pilot",
|
|
37
|
+
description="Agent-first, session-based CLI for driving mobile apps via Appium.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-s", "--session", default="default", help="session name (default: default)")
|
|
40
|
+
parser.add_argument("--json", action="store_true", help="emit structured JSON output")
|
|
41
|
+
parser.add_argument("--version", action="version", version=f"appium-pilot {__version__}")
|
|
42
|
+
|
|
43
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
44
|
+
for module in (
|
|
45
|
+
open_cmd,
|
|
46
|
+
snapshot_cmd,
|
|
47
|
+
capture_cmd,
|
|
48
|
+
tap_cmd,
|
|
49
|
+
type_cmd,
|
|
50
|
+
gesture_cmd,
|
|
51
|
+
wait_cmd,
|
|
52
|
+
video_cmd,
|
|
53
|
+
app_cmd,
|
|
54
|
+
devices_cmd,
|
|
55
|
+
session_cmd,
|
|
56
|
+
skills_cmd,
|
|
57
|
+
doctor_cmd,
|
|
58
|
+
):
|
|
59
|
+
module.add_parser(sub)
|
|
60
|
+
return parser
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalize_session_flag(argv: list[str]) -> list[str]:
|
|
64
|
+
"""Allow `-s=name` in addition to argparse's `-s name`."""
|
|
65
|
+
out: list[str] = []
|
|
66
|
+
for arg in argv:
|
|
67
|
+
if arg.startswith("-s=") or arg.startswith("--session="):
|
|
68
|
+
out.append("--session")
|
|
69
|
+
out.append(arg.split("=", 1)[1])
|
|
70
|
+
else:
|
|
71
|
+
out.append(arg)
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main(argv: list[str] | None = None) -> None:
|
|
76
|
+
argv = _normalize_session_flag(list(argv if argv is not None else sys.argv[1:]))
|
|
77
|
+
args = build_parser().parse_args(argv)
|
|
78
|
+
set_json_mode(args.json)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
args.func(args)
|
|
82
|
+
except CommandError as exc:
|
|
83
|
+
fail(str(exc), code=exc.code, **exc.data)
|
|
84
|
+
except WebDriverException as exc:
|
|
85
|
+
# Driver-level failures (e.g. an action the app/platform rejects) become a
|
|
86
|
+
# clean one-line error, not a traceback. Set APPIUM_PILOT_DEBUG=1 to see it.
|
|
87
|
+
if os.environ.get("APPIUM_PILOT_DEBUG"):
|
|
88
|
+
raise
|
|
89
|
+
msg = (getattr(exc, "msg", None) or str(exc)).strip().splitlines()[0]
|
|
90
|
+
fail(f"driver error: {msg}")
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
fail("interrupted", code=130)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command handlers."""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""App lifecycle (`launch`/`terminate`/`activate`/`background`/`install`/`remove`/`reset`)
|
|
2
|
+
and `orientation`."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from appium_pilot.output import CommandError, emit
|
|
10
|
+
from appium_pilot.session import Session
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
14
|
+
for name, help_text in [
|
|
15
|
+
("launch", "activate (foreground) the app under test"),
|
|
16
|
+
("activate", "activate (foreground) the app under test"),
|
|
17
|
+
("terminate", "terminate the app under test"),
|
|
18
|
+
]:
|
|
19
|
+
p = sub.add_parser(name, help=help_text)
|
|
20
|
+
p.add_argument("app_id", nargs="?", help="app id (default: the session's app)")
|
|
21
|
+
p.set_defaults(func=_make_lifecycle(name))
|
|
22
|
+
|
|
23
|
+
bg = sub.add_parser("background", help="background the app for N seconds (-1 = indefinitely)")
|
|
24
|
+
bg.add_argument("seconds", nargs="?", type=int, default=-1)
|
|
25
|
+
bg.set_defaults(func=run_background)
|
|
26
|
+
|
|
27
|
+
inst = sub.add_parser("install", help="install an app artifact on the device")
|
|
28
|
+
inst.add_argument("path", help="path to .apk/.app/.ipa")
|
|
29
|
+
inst.set_defaults(func=run_install)
|
|
30
|
+
|
|
31
|
+
rm = sub.add_parser("remove", help="uninstall an app from the device")
|
|
32
|
+
rm.add_argument("app_id", nargs="?", help="app id (default: the session's app)")
|
|
33
|
+
rm.set_defaults(func=run_remove)
|
|
34
|
+
|
|
35
|
+
rs = sub.add_parser("reset", help="terminate then re-activate the app under test")
|
|
36
|
+
rs.set_defaults(func=run_reset)
|
|
37
|
+
|
|
38
|
+
o = sub.add_parser("orientation", help="get or set screen orientation")
|
|
39
|
+
o.add_argument("value", nargs="?", choices=["portrait", "landscape"],
|
|
40
|
+
help="omit to read the current orientation")
|
|
41
|
+
o.set_defaults(func=run_orientation)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _app_id(session: Session, explicit: str | None) -> str:
|
|
45
|
+
app_id = explicit or session.app_id
|
|
46
|
+
if not app_id:
|
|
47
|
+
raise CommandError("no app id known for this session; pass one explicitly")
|
|
48
|
+
return app_id
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _make_lifecycle(action: str):
|
|
52
|
+
def run(args) -> None:
|
|
53
|
+
session = Session.load(args.session)
|
|
54
|
+
driver = session.attach()
|
|
55
|
+
app_id = _app_id(session, args.app_id)
|
|
56
|
+
if action in ("launch", "activate"):
|
|
57
|
+
driver.activate_app(app_id)
|
|
58
|
+
emit(f"activated {app_id}", app=app_id)
|
|
59
|
+
elif action == "terminate":
|
|
60
|
+
driver.terminate_app(app_id)
|
|
61
|
+
emit(f"terminated {app_id}", app=app_id)
|
|
62
|
+
|
|
63
|
+
return run
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_background(args) -> None:
|
|
67
|
+
session = Session.load(args.session)
|
|
68
|
+
driver = session.attach()
|
|
69
|
+
driver.background_app(args.seconds)
|
|
70
|
+
emit(f"backgrounded app for {args.seconds}s")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_install(args) -> None:
|
|
74
|
+
session = Session.load(args.session)
|
|
75
|
+
driver = session.attach()
|
|
76
|
+
path = str(Path(args.path).expanduser().resolve())
|
|
77
|
+
driver.install_app(path)
|
|
78
|
+
emit(f"installed {path}", path=path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_remove(args) -> None:
|
|
82
|
+
session = Session.load(args.session)
|
|
83
|
+
driver = session.attach()
|
|
84
|
+
app_id = _app_id(session, args.app_id)
|
|
85
|
+
driver.remove_app(app_id)
|
|
86
|
+
emit(f"removed {app_id}", app=app_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_reset(args) -> None:
|
|
90
|
+
session = Session.load(args.session)
|
|
91
|
+
driver = session.attach()
|
|
92
|
+
app_id = _app_id(session, None)
|
|
93
|
+
driver.terminate_app(app_id)
|
|
94
|
+
driver.activate_app(app_id)
|
|
95
|
+
emit(f"reset {app_id}", app=app_id)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_orientation(args) -> None:
|
|
99
|
+
session = Session.load(args.session)
|
|
100
|
+
driver = session.attach()
|
|
101
|
+
if args.value:
|
|
102
|
+
driver.orientation = args.value.upper()
|
|
103
|
+
emit(f"orientation set to {args.value}", orientation=args.value.upper())
|
|
104
|
+
else:
|
|
105
|
+
current = driver.orientation
|
|
106
|
+
emit(current, orientation=current)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""`screenshot` and `source` — pull pixels / raw page source off the device."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from appium_pilot import config
|
|
9
|
+
from appium_pilot.output import emit, raw
|
|
10
|
+
from appium_pilot.resolve import find_ref
|
|
11
|
+
from appium_pilot.session import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
15
|
+
sh = sub.add_parser("screenshot", help="save a PNG of the screen (or an element) and print its path")
|
|
16
|
+
sh.add_argument("ref", nargs="?", help="optional element ref to screenshot instead of the screen")
|
|
17
|
+
sh.add_argument("-o", "--out", help="output path (default: session screenshots dir)")
|
|
18
|
+
sh.set_defaults(func=run_screenshot)
|
|
19
|
+
|
|
20
|
+
src = sub.add_parser("source", help="print the full raw page source")
|
|
21
|
+
src.set_defaults(func=run_source)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_screenshot(args) -> None:
|
|
25
|
+
session = Session.load(args.session)
|
|
26
|
+
driver = session.attach()
|
|
27
|
+
|
|
28
|
+
if args.out:
|
|
29
|
+
path = args.out
|
|
30
|
+
else:
|
|
31
|
+
shots = config.screenshots_dir()
|
|
32
|
+
shots.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
path = str(shots / f"shot-{time.strftime('%Y%m%d-%H%M%S')}.png")
|
|
34
|
+
|
|
35
|
+
if args.ref:
|
|
36
|
+
locator = session.locator_for(args.ref)
|
|
37
|
+
element = find_ref(driver, locator, args.ref)
|
|
38
|
+
element.screenshot(path)
|
|
39
|
+
else:
|
|
40
|
+
driver.get_screenshot_as_file(path)
|
|
41
|
+
|
|
42
|
+
emit(path, path=path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_source(args) -> None:
|
|
46
|
+
session = Session.load(args.session)
|
|
47
|
+
driver = session.attach()
|
|
48
|
+
raw(driver.page_source)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""`devices` — list available simulators/emulators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from appium_pilot import devices
|
|
8
|
+
from appium_pilot.output import emit, json_mode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
12
|
+
p = sub.add_parser("devices", help="list available iOS simulators / Android emulators")
|
|
13
|
+
p.set_defaults(func=run)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(args) -> None:
|
|
17
|
+
found = devices.list_all()
|
|
18
|
+
payload = [
|
|
19
|
+
{"platform": d.platform, "udid": d.udid, "name": d.name, "booted": d.booted}
|
|
20
|
+
for d in found
|
|
21
|
+
]
|
|
22
|
+
if json_mode():
|
|
23
|
+
emit(f"{len(found)} devices", devices=payload)
|
|
24
|
+
return
|
|
25
|
+
if not found:
|
|
26
|
+
emit("no devices found (no booted simulators/emulators or AVDs)")
|
|
27
|
+
return
|
|
28
|
+
for d in found:
|
|
29
|
+
state = "booted" if d.booted else "available"
|
|
30
|
+
print(f"{d.platform:8} {state:10} {d.name} ({d.udid})")
|