@houwert/conductor 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ Claude learns every available command, how to coordinate across devices, and how
66
66
 
67
67
  | Capability | Commands |
68
68
  |---|---|
69
- | App lifecycle | `launch-app`, `stop-app`, `foreground-app`, `copy-app` |
69
+ | App lifecycle | `launch-app`, `stop-app`, `clear-state`, `uninstall-app`, `install-app`, `foreground-app`, `copy-app` |
70
70
  | Interaction | `tap`, `type`, `scroll`, `scroll-until-visible`, `swipe`, `press-key`, `erase-text`, `hide-keyboard` |
71
71
  | Inspection | `inspect`, `focused`, `screenshot`, `list-apps` |
72
72
  | Assertions | `assert-visible`, `assert-not-visible` |
@@ -34,11 +34,14 @@ DEVICE MANAGEMENT
34
34
  session --list List all device sessions
35
35
 
36
36
  APP CONTROL
37
+ install-app <path> Install .app / .ipa / .apk onto device
37
38
  launch-app <appId> [--device <id>] Launch app and save to session
38
39
  --clear-state Wipe app data/state before launching
39
40
  --clear-keychain Wipe keychain before launching
40
41
  --argument key=value Set launch argument (repeatable)
41
42
  stop-app [<appId>] Stop app (uses session appId if omitted)
43
+ clear-state [<appId>] Clear app data/state (uses session appId if omitted)
44
+ uninstall-app <appId> Uninstall app from device
42
45
 
43
46
  INTERACTIONS
44
47
  tap <element> Tap element by text
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.clearState = clearState;
5
+ exports.HELP = ` clear-state [<appId>] Clear app data/state`;
6
+ const runner_js_1 = require("../runner.js");
7
+ const session_js_1 = require("../session.js");
8
+ const output_js_1 = require("../output.js");
9
+ async function clearState(appId, opts = {}, sessionName = 'default') {
10
+ const session = await (0, session_js_1.getSession)(sessionName);
11
+ const resolvedAppId = appId ?? session.appId;
12
+ if (!resolvedAppId) {
13
+ (0, output_js_1.printError)('clear-state: no appId provided and no active session. Run launch-app first.', opts);
14
+ return 1;
15
+ }
16
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
17
+ await driver.clearAppState(resolvedAppId);
18
+ }, sessionName);
19
+ if (result.success) {
20
+ (0, output_js_1.printSuccess)(`clear-state "${resolvedAppId}" — done`, opts);
21
+ return 0;
22
+ }
23
+ else {
24
+ (0, output_js_1.printError)(`clear-state "${resolvedAppId}" — failed\n${result.stderr}`, opts);
25
+ return 1;
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.installApp = installApp;
5
+ exports.HELP = ` install-app <path> Install .app / .ipa / .apk onto device`;
6
+ const runner_js_1 = require("../runner.js");
7
+ const session_js_1 = require("../session.js");
8
+ const output_js_1 = require("../output.js");
9
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
10
+ async function resolveDeviceId(sessionName) {
11
+ if (sessionName !== 'default')
12
+ return sessionName;
13
+ const session = await (0, session_js_1.getSession)(sessionName);
14
+ return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
15
+ }
16
+ async function installApp(appPath, opts = {}, sessionName = 'default') {
17
+ if (!appPath) {
18
+ (0, output_js_1.printError)('install-app requires <path> to a .app, .ipa, or .apk', opts);
19
+ return 1;
20
+ }
21
+ const deviceId = await resolveDeviceId(sessionName);
22
+ if (!deviceId) {
23
+ (0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
24
+ return 1;
25
+ }
26
+ const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
27
+ if (platform === 'ios' || platform === 'tvos') {
28
+ const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', deviceId, appPath]);
29
+ if (!result.success) {
30
+ (0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
31
+ return 1;
32
+ }
33
+ }
34
+ else {
35
+ const result = await (0, runner_js_1.spawnCommand)('adb', [
36
+ '-s',
37
+ deviceId,
38
+ 'install',
39
+ '-r',
40
+ '-t',
41
+ '-g',
42
+ appPath,
43
+ ]);
44
+ if (!result.success) {
45
+ (0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
46
+ return 1;
47
+ }
48
+ }
49
+ (0, output_js_1.printSuccess)(`install-app "${appPath}" — done`, opts);
50
+ return 0;
51
+ }
@@ -21,7 +21,7 @@ async function listApps(opts = {}, sessionName = 'default') {
21
21
  }
22
22
  const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
23
23
  let appIds;
24
- if (platform === 'ios') {
24
+ if (platform === 'ios' || platform === 'tvos') {
25
25
  const result = await (0, runner_js_1.spawnCommand)('bash', [
26
26
  '-c',
27
27
  `xcrun simctl listapps ${deviceId} | plutil -convert json - -o -`,
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.uninstallApp = uninstallApp;
5
+ exports.HELP = ` uninstall-app <appId> Uninstall app from device`;
6
+ const runner_js_1 = require("../runner.js");
7
+ const output_js_1 = require("../output.js");
8
+ const ios_js_1 = require("../drivers/ios.js");
9
+ const android_js_1 = require("../drivers/android.js");
10
+ async function uninstallApp(appId, opts = {}, sessionName = 'default') {
11
+ if (!appId) {
12
+ (0, output_js_1.printError)('uninstall-app requires <appId>', opts);
13
+ return 1;
14
+ }
15
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
16
+ if (driver instanceof ios_js_1.IOSDriver) {
17
+ await driver.uninstallApp(appId);
18
+ }
19
+ else if (driver instanceof android_js_1.AndroidDriver) {
20
+ await driver.uninstallApp(appId);
21
+ }
22
+ }, sessionName);
23
+ if (result.success) {
24
+ (0, output_js_1.printSuccess)(`uninstall-app "${appId}" — done`, opts);
25
+ return 0;
26
+ }
27
+ else {
28
+ (0, output_js_1.printError)(`uninstall-app "${appId}" — failed\n${result.stderr}`, opts);
29
+ return 1;
30
+ }
31
+ }
@@ -208,6 +208,10 @@ class AndroidDriver {
208
208
  async clearAppState(packageName) {
209
209
  await this.adb(['shell', 'pm', 'clear', packageName]);
210
210
  }
211
+ async uninstallApp(packageName) {
212
+ await this.adb(['shell', 'am', 'force-stop', packageName]).catch(() => { });
213
+ await this.adb(['uninstall', packageName]);
214
+ }
211
215
  async clearKeychain() {
212
216
  // No-op on Android — keychain is an iOS concept
213
217
  }
@@ -987,6 +987,21 @@ async function executeCommandBody(key, val, driver, opts) {
987
987
  await driver.clearKeychain();
988
988
  break;
989
989
  }
990
+ case 'uninstallApp': {
991
+ const appId = val == null || val === ''
992
+ ? (opts.appId ??
993
+ (() => {
994
+ throw new Error('uninstallApp: no appId in command or flow header');
995
+ })())
996
+ : resolveAppId(val, 'uninstallApp');
997
+ if (driver instanceof ios_js_1.IOSDriver) {
998
+ await driver.uninstallApp(appId);
999
+ }
1000
+ else {
1001
+ await driver.uninstallApp(appId);
1002
+ }
1003
+ break;
1004
+ }
990
1005
  // ── Keys ───────────────────────────────────────────────────────────────
991
1006
  case 'pressKey': {
992
1007
  const keyName = val.toUpperCase();
@@ -168,6 +168,11 @@ class IOSDriver {
168
168
  await promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
169
169
  }
170
170
  }
171
+ async uninstallApp(bundleId) {
172
+ const deviceId = this.requireDeviceId();
173
+ await this.simctl(['terminate', deviceId, bundleId]).catch(() => { });
174
+ await this.simctl(['uninstall', deviceId, bundleId]);
175
+ }
171
176
  async clearKeychain() {
172
177
  const deviceId = this.requireDeviceId();
173
178
  await this.simctl(['keychain', deviceId, 'reset']);
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ const verbose_js_1 = require("./verbose.js");
9
9
  const list_devices_js_1 = require("./commands/list-devices.js");
10
10
  const launch_app_js_1 = require("./commands/launch-app.js");
11
11
  const stop_app_js_1 = require("./commands/stop-app.js");
12
+ const clear_state_js_1 = require("./commands/clear-state.js");
13
+ const uninstall_app_js_1 = require("./commands/uninstall-app.js");
12
14
  const tap_js_1 = require("./commands/tap.js");
13
15
  const type_js_1 = require("./commands/type.js");
14
16
  const back_js_1 = require("./commands/back.js");
@@ -30,6 +32,7 @@ const run_parallel_js_1 = require("./commands/run-parallel.js");
30
32
  const foreground_app_js_1 = require("./commands/foreground-app.js");
31
33
  const list_apps_js_1 = require("./commands/list-apps.js");
32
34
  const copy_app_js_1 = require("./commands/copy-app.js");
35
+ const install_app_js_1 = require("./commands/install-app.js");
33
36
  const erase_text_js_1 = require("./commands/erase-text.js");
34
37
  const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
35
38
  const open_link_js_1 = require("./commands/open-link.js");
@@ -46,8 +49,11 @@ const COMMAND_HELP = {
46
49
  'foreground-app': foreground_app_js_1.HELP,
47
50
  'list-apps': list_apps_js_1.HELP,
48
51
  'copy-app': copy_app_js_1.HELP,
52
+ 'install-app': install_app_js_1.HELP,
49
53
  'launch-app': launch_app_js_1.HELP,
50
54
  'stop-app': stop_app_js_1.HELP,
55
+ 'clear-state': clear_state_js_1.HELP,
56
+ 'uninstall-app': uninstall_app_js_1.HELP,
51
57
  tap: tap_js_1.HELP,
52
58
  type: type_js_1.HELP,
53
59
  'erase-text': erase_text_js_1.HELP,
@@ -219,6 +225,11 @@ async function main() {
219
225
  exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
220
226
  break;
221
227
  }
228
+ case 'install-app': {
229
+ const appPath = rest[0] ?? '';
230
+ exitCode = await (0, install_app_js_1.installApp)(appPath, opts, sessionName);
231
+ break;
232
+ }
222
233
  case 'launch-app': {
223
234
  const appId = rest[0] ?? '';
224
235
  // --argument key=value (repeatable) → Record<string, string>
@@ -240,6 +251,16 @@ async function main() {
240
251
  exitCode = await (0, stop_app_js_1.stopApp)(appId, opts, sessionName);
241
252
  break;
242
253
  }
254
+ case 'clear-state': {
255
+ const appId = rest[0];
256
+ exitCode = await (0, clear_state_js_1.clearState)(appId, opts, sessionName);
257
+ break;
258
+ }
259
+ case 'uninstall-app': {
260
+ const appId = rest[0] ?? '';
261
+ exitCode = await (0, uninstall_app_js_1.uninstallApp)(appId, opts, sessionName);
262
+ break;
263
+ }
243
264
  case 'tap': {
244
265
  const element = rest.join(' ');
245
266
  exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,6 +90,17 @@ Both flags are required. The command reads the `.app` bundle path from the sourc
90
90
 
91
91
  ---
92
92
 
93
+ ### `install-app <path>`
94
+
95
+ Install an app from a local `.app` bundle, `.ipa`, or `.apk` file onto the device.
96
+
97
+ ```bash
98
+ conductor install-app ./build/MyApp.app
99
+ conductor install-app ./build/app-debug.apk --device emulator-5554
100
+ ```
101
+
102
+ ---
103
+
93
104
  ### `launch-app <appId>`
94
105
 
95
106
  Launch an app by bundle ID and save it to the session.
@@ -124,6 +135,28 @@ conductor stop-app com.example.myapp
124
135
 
125
136
  ---
126
137
 
138
+ ### `clear-state [<appId>]`
139
+
140
+ Clear app data/state without relaunching. Uses the session `appId` if not specified. On iOS this terminates the app, preserves the .app bundle, uninstalls, and reinstalls (clearing all user data). On Android this runs `pm clear`.
141
+
142
+ ```bash
143
+ conductor clear-state
144
+ conductor clear-state com.example.myapp
145
+ ```
146
+
147
+ ---
148
+
149
+ ### `uninstall-app <appId>`
150
+
151
+ Uninstall an app from the device. The app ID is required.
152
+
153
+ ```bash
154
+ conductor uninstall-app com.example.myapp
155
+ conductor uninstall-app com.example.myapp --device emulator-5554
156
+ ```
157
+
158
+ ---
159
+
127
160
  ### `tap <element>`
128
161
 
129
162
  Tap a UI element by its text label or accessibility ID. **Try the most obvious text label or ID first** — e.g. `"Sign In"`, `"Submit"`, `"btn_login"`. Only run `inspect` to look up the exact identifier if your first attempt fails. **For icon-only buttons (no visible text label), always run `inspect` first** to find the accessibility ID before tapping.
@@ -26,6 +26,8 @@ appId: com.example.myapp
26
26
  appId: com.other.app
27
27
  - clearState # clear app data
28
28
  - clearKeychain # clear keychain (iOS: full keychain; Android: account credentials)
29
+ - uninstallApp # uninstall app from device
30
+ - uninstallApp: com.other.app # uninstall a specific app
29
31
  ```
30
32
 
31
33
  ### Tapping