Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **Multiple Output Formats**: Save screenshots as PNG or JPEG with quality control
- **Screencapture video streaming**: Stream mjpeg/h264 video directly from device
- **Device Control**: Reboot devices, tap screen coordinates, press hardware buttons
- **App Management**: Launch, terminate, install, uninstall, list, and get foreground apps
- **App Management**: Launch, terminate, install, uninstall, clear data, list, and get foreground apps
- **Filesystem**: Push, pull, list, mkdir, and rm files on-device or in app containers (Android, iOS Simulator)
- **Crash Reports**: List and fetch crash reports from iOS and Android devices
- **Webview Inspection**: List, navigate, query DOM, and evaluate JavaScript in embedded webviews
Expand Down Expand Up @@ -218,6 +218,10 @@ mobilecli apps install <path> --device <device-id>

# Uninstall an app
mobilecli apps uninstall <bundle-id> --device <device-id>

# Clear app data (cache, preferences, databases) without uninstalling
# Supported on Android and iOS Simulator
mobilecli apps clear <bundle-id> --device <device-id>
```

Example output for `apps foreground`:
Expand Down
22 changes: 22 additions & 0 deletions cli/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ var appsUninstallCmd = &cobra.Command{
},
}

var appsClearCmd = &cobra.Command{
Use: "clear [bundle_id]",
Short: "Clear app data on a device",
Long: `Clears all data (cache, preferences, databases) for an app without uninstalling it. Supported on Android and iOS Simulator.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
req := commands.ClearAppRequest{
DeviceID: deviceId,
BundleID: args[0],
}

response := commands.ClearAppCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var appsPathCmd = &cobra.Command{
Use: "path [bundle_id]",
Short: "Get the container path of an app on a device",
Expand Down Expand Up @@ -178,6 +198,7 @@ func init() {
appsCmd.AddCommand(appsListCmd)
appsCmd.AddCommand(appsInstallCmd)
appsCmd.AddCommand(appsUninstallCmd)
appsCmd.AddCommand(appsClearCmd)
appsCmd.AddCommand(appsForegroundCmd)
appsCmd.AddCommand(appsPathCmd)

Expand All @@ -191,6 +212,7 @@ func init() {
appsInstallCmd.Flags().StringVar(&provisioningProfile, "provisioning-profile", "", "Path to a .mobileprovision file to use for re-signing")
appsInstallCmd.Flags().StringVar(&signingIdentity, "signing-identity", "", "Signing identity name to use for re-signing")
appsUninstallCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to uninstall app from")
appsClearCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to clear app data on")
appsForegroundCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get foreground app from")
appsPathCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
}
25 changes: 25 additions & 0 deletions commands/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ func AppPathCommand(req AppPathRequest) *CommandResponse {
})
}

type ClearAppRequest struct {
DeviceID string `json:"deviceId"`
BundleID string `json:"bundleId"`
}

func ClearAppCommand(req ClearAppRequest) *CommandResponse {
if req.BundleID == "" {
return NewErrorResponse(fmt.Errorf("bundle ID is required"))
}

targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID)
if err != nil {
return NewErrorResponse(fmt.Errorf("error finding device: %w", err))
}

err = targetDevice.ClearApp(req.BundleID)
if err != nil {
return NewErrorResponse(fmt.Errorf("failed to clear app on device %s: %w", targetDevice.ID(), err))
}

return NewSuccessResponse(map[string]any{
"message": fmt.Sprintf("Cleared app '%s' on device %s", req.BundleID, targetDevice.ID()),
})
}

type UninstallAppRequest struct {
DeviceID string `json:"deviceId"`
PackageName string `json:"packageName"`
Expand Down
11 changes: 11 additions & 0 deletions devices/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,17 @@ func (d *AndroidDevice) InstallApp(path string) error {
return fmt.Errorf("installation failed: %s", string(output))
}

func (d *AndroidDevice) ClearApp(bundleID string) error {
output, err := d.runAdbCommand("shell", "pm", "clear", bundleID)
if err != nil {
return fmt.Errorf("failed to clear app %s: %w\nOutput: %s", bundleID, err, string(output))
}
if !strings.Contains(string(output), "Success") {
return fmt.Errorf("failed to clear app %s: %s", bundleID, strings.TrimSpace(string(output)))
}
return nil
}

func (d *AndroidDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
appInfo := &InstalledAppInfo{
PackageName: packageName,
Expand Down
1 change: 1 addition & 0 deletions devices/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ type ControllableDevice interface {
GetForegroundApp() (*ForegroundAppInfo, error)
InstallApp(path string) error
UninstallApp(packageName string) (*InstalledAppInfo, error)
ClearApp(bundleID string) error
Info() (*FullDeviceInfo, error)
StartScreenCapture(config ScreenCaptureConfig) error
DumpSource() ([]ScreenElement, error)
Expand Down
4 changes: 4 additions & 0 deletions devices/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,10 @@ func (d *IOSDevice) InstallApp(path string) error {
return nil
}

func (d *IOSDevice) ClearApp(bundleID string) error {
return fmt.Errorf("clearing app data is not supported on real iOS devices")
}

func (d *IOSDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
log.SetLevel(log.WarnLevel)

Expand Down
4 changes: 4 additions & 0 deletions devices/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,10 @@ func (r *RemoteDevice) UninstallApp(packageName string) (*InstalledAppInfo, erro
return nil, fmt.Errorf("uninstall app is not supported on remote devices")
}

func (r *RemoteDevice) ClearApp(bundleID string) error {
return r.fireRPC("device.apps.clear", params{"bundleId": bundleID})
}

// ScreenRecordCallbacks provides optional progress callbacks for screen recording
type ScreenRecordCallbacks struct {
OnRecordingEnded func()
Expand Down
33 changes: 33 additions & 0 deletions devices/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,39 @@ func (s SimulatorDevice) InstallApp(path string) error {
return InstallApp(s.UDID, path)
}

func (s SimulatorDevice) ClearApp(bundleID string) error {
output, err := runSimctl("get_app_container", s.UDID, bundleID, "data")
if err != nil {
return fmt.Errorf("failed to get data container for %s: %w", bundleID, err)
}

containerPath := filepath.Clean(strings.TrimSpace(string(output)))
if containerPath == "" {
return fmt.Errorf("no data container found for %s", bundleID)
}

// sanity check before recursive deletion: path must be inside this simulator's app-data containers
expectedSubpath := filepath.Join("CoreSimulator", "Devices", s.UDID, "data", "Containers", "Data", "Application") + string(os.PathSeparator)
if !filepath.IsAbs(containerPath) || !strings.Contains(containerPath, expectedSubpath) {
return fmt.Errorf("refusing to clear unexpected container path: %s", containerPath)
}

_ = s.TerminateApp(bundleID)

entries, err := os.ReadDir(containerPath)
if err != nil {
return fmt.Errorf("failed to read data container: %w", err)
}

for _, entry := range entries {
if err := os.RemoveAll(filepath.Join(containerPath, entry.Name())); err != nil {
return fmt.Errorf("failed to remove %s: %w", entry.Name(), err)
}
}

return nil
}

func (s SimulatorDevice) UninstallApp(packageName string) (*InstalledAppInfo, error) {
installedApps, err := s.ListInstalledApps()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions server/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func GetMethodRegistry() map[string]HandlerFunc {
"device.apps.foreground": handleAppsForeground,
"device.apps.install": handleAppsInstall,
"device.apps.uninstall": handleAppsUninstall,
"device.apps.clear": handleAppsClear,
"device.screenrecord": handleScreenRecord,
"device.screenrecord.stop": handleScreenRecordStop,
"device.crashes.list": handleCrashesList,
Expand Down
32 changes: 32 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,11 @@ type AppsUninstallParams struct {
BundleID string `json:"bundleId"`
}

type AppsClearParams struct {
DeviceID string `json:"deviceId"`
BundleID string `json:"bundleId"`
}

type ScreenRecordParams struct {
DeviceID string `json:"deviceId"`
Output string `json:"output"`
Expand Down Expand Up @@ -1121,6 +1126,33 @@ func handleAppsInstall(params json.RawMessage) (any, error) {
return response.Data, nil
}

func handleAppsClear(params json.RawMessage) (any, error) {
if len(params) == 0 {
return nil, fmt.Errorf("'params' is required with fields: deviceId, bundleId")
}

var p AppsClearParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid parameters: %w. Expected fields: deviceId, bundleId", err)
}

if p.BundleID == "" {
return nil, fmt.Errorf("'bundleId' is required")
}

req := commands.ClearAppRequest{
DeviceID: p.DeviceID,
BundleID: p.BundleID,
}

response := commands.ClearAppCommand(req)
if response.Status == "error" {
return nil, fmt.Errorf("%s", response.Error)
}

return response.Data, nil
}

func handleAppsUninstall(params json.RawMessage) (any, error) {
if len(params) == 0 {
return nil, fmt.Errorf("'params' is required with fields: deviceId, bundleId")
Expand Down
Loading