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.
Files changed (60) hide show
  1. appium_pilot-0.1.0/.claude/settings.local.json +8 -0
  2. appium_pilot-0.1.0/.gitignore +20 -0
  3. appium_pilot-0.1.0/LICENSE +21 -0
  4. appium_pilot-0.1.0/Makefile +28 -0
  5. appium_pilot-0.1.0/PKG-INFO +138 -0
  6. appium_pilot-0.1.0/README.md +106 -0
  7. appium_pilot-0.1.0/pyproject.toml +62 -0
  8. appium_pilot-0.1.0/src/appium_pilot/__init__.py +3 -0
  9. appium_pilot-0.1.0/src/appium_pilot/__main__.py +4 -0
  10. appium_pilot-0.1.0/src/appium_pilot/cli.py +96 -0
  11. appium_pilot-0.1.0/src/appium_pilot/commands/__init__.py +1 -0
  12. appium_pilot-0.1.0/src/appium_pilot/commands/app_cmd.py +106 -0
  13. appium_pilot-0.1.0/src/appium_pilot/commands/capture_cmd.py +48 -0
  14. appium_pilot-0.1.0/src/appium_pilot/commands/devices_cmd.py +30 -0
  15. appium_pilot-0.1.0/src/appium_pilot/commands/doctor_cmd.py +103 -0
  16. appium_pilot-0.1.0/src/appium_pilot/commands/gesture_cmd.py +89 -0
  17. appium_pilot-0.1.0/src/appium_pilot/commands/open_cmd.py +114 -0
  18. appium_pilot-0.1.0/src/appium_pilot/commands/session_cmd.py +108 -0
  19. appium_pilot-0.1.0/src/appium_pilot/commands/skills_cmd.py +149 -0
  20. appium_pilot-0.1.0/src/appium_pilot/commands/snapshot_cmd.py +35 -0
  21. appium_pilot-0.1.0/src/appium_pilot/commands/tap_cmd.py +25 -0
  22. appium_pilot-0.1.0/src/appium_pilot/commands/type_cmd.py +54 -0
  23. appium_pilot-0.1.0/src/appium_pilot/commands/video_cmd.py +63 -0
  24. appium_pilot-0.1.0/src/appium_pilot/commands/wait_cmd.py +45 -0
  25. appium_pilot-0.1.0/src/appium_pilot/config.py +44 -0
  26. appium_pilot-0.1.0/src/appium_pilot/devices.py +185 -0
  27. appium_pilot-0.1.0/src/appium_pilot/output.py +61 -0
  28. appium_pilot-0.1.0/src/appium_pilot/proc.py +43 -0
  29. appium_pilot-0.1.0/src/appium_pilot/resolve.py +44 -0
  30. appium_pilot-0.1.0/src/appium_pilot/server.py +84 -0
  31. appium_pilot-0.1.0/src/appium_pilot/session.py +109 -0
  32. appium_pilot-0.1.0/src/appium_pilot/skilldata/SKILL.md +129 -0
  33. appium_pilot-0.1.0/src/appium_pilot/snapshot.py +86 -0
  34. appium_pilot-0.1.0/src/appium_pilot/strategies/__init__.py +19 -0
  35. appium_pilot-0.1.0/src/appium_pilot/strategies/android.py +157 -0
  36. appium_pilot-0.1.0/src/appium_pilot/strategies/base.py +118 -0
  37. appium_pilot-0.1.0/src/appium_pilot/strategies/ios.py +132 -0
  38. appium_pilot-0.1.0/tests/conftest.py +16 -0
  39. appium_pilot-0.1.0/tests/e2e/apps.py +139 -0
  40. appium_pilot-0.1.0/tests/e2e/conftest.py +74 -0
  41. appium_pilot-0.1.0/tests/e2e/test_actions_e2e.py +40 -0
  42. appium_pilot-0.1.0/tests/e2e/test_gestures_e2e.py +24 -0
  43. appium_pilot-0.1.0/tests/e2e/test_inspection_e2e.py +44 -0
  44. appium_pilot-0.1.0/tests/e2e/test_install_remove_e2e.py +23 -0
  45. appium_pilot-0.1.0/tests/e2e/test_keys_orientation_e2e.py +37 -0
  46. appium_pilot-0.1.0/tests/e2e/test_lifecycle_e2e.py +20 -0
  47. appium_pilot-0.1.0/tests/e2e/test_video_e2e.py +21 -0
  48. appium_pilot-0.1.0/tests/e2e/test_waits_e2e.py +23 -0
  49. appium_pilot-0.1.0/tests/fixtures/android_source.xml +16 -0
  50. appium_pilot-0.1.0/tests/fixtures/ios_source.xml +10 -0
  51. appium_pilot-0.1.0/tests/unit/test_config.py +23 -0
  52. appium_pilot-0.1.0/tests/unit/test_locators.py +75 -0
  53. appium_pilot-0.1.0/tests/unit/test_open_helpers.py +56 -0
  54. appium_pilot-0.1.0/tests/unit/test_output.py +59 -0
  55. appium_pilot-0.1.0/tests/unit/test_parser.py +45 -0
  56. appium_pilot-0.1.0/tests/unit/test_quoting.py +25 -0
  57. appium_pilot-0.1.0/tests/unit/test_recording_options.py +15 -0
  58. appium_pilot-0.1.0/tests/unit/test_session.py +52 -0
  59. appium_pilot-0.1.0/tests/unit/test_skills.py +40 -0
  60. appium_pilot-0.1.0/tests/unit/test_snapshot.py +40 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/marcoperugini/.claude/skills/**)",
5
+ "Read(//Users/marcoperugini/.claude/skills/playwright-cli/**)"
6
+ ]
7
+ }
8
+ }
@@ -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,3 @@
1
+ """appium-pilot — agent-first, session-based CLI for driving mobile apps via Appium."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from appium_pilot.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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})")