diff --git a/README.md b/README.md index c4051d9..0630caa 100644 --- a/README.md +++ b/README.md @@ -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 - **Crash Reports**: List and fetch crash reports from iOS and Android devices ### 🎯 Platform Support @@ -207,6 +207,10 @@ mobilecli apps install --device # Uninstall an app mobilecli apps uninstall --device + +# Clear app data (cache, preferences, databases) without uninstalling +# Supported on Android and iOS Simulator +mobilecli apps clear --device ``` Example output for `apps foreground`: diff --git a/cli/apps.go b/cli/apps.go index 6acd95f..7e1d10f 100644 --- a/cli/apps.go +++ b/cli/apps.go @@ -132,6 +132,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 appsForegroundCmd = &cobra.Command{ Use: "foreground", Short: "Get the currently foreground app on a device", @@ -158,6 +178,7 @@ func init() { appsCmd.AddCommand(appsListCmd) appsCmd.AddCommand(appsInstallCmd) appsCmd.AddCommand(appsUninstallCmd) + appsCmd.AddCommand(appsClearCmd) appsCmd.AddCommand(appsForegroundCmd) appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on") @@ -169,5 +190,6 @@ 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") } diff --git a/commands/apps.go b/commands/apps.go index 2d0203c..c360f7b 100644 --- a/commands/apps.go +++ b/commands/apps.go @@ -155,6 +155,31 @@ func InstallAppCommand(req InstallAppRequest) *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"` diff --git a/devices/android.go b/devices/android.go index a10a5aa..b38f38e 100644 --- a/devices/android.go +++ b/devices/android.go @@ -1273,6 +1273,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, diff --git a/devices/common.go b/devices/common.go index 494705d..ee32e23 100644 --- a/devices/common.go +++ b/devices/common.go @@ -120,6 +120,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) diff --git a/devices/ios.go b/devices/ios.go index d6deda1..de08c5d 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -1100,6 +1100,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) diff --git a/devices/remote.go b/devices/remote.go index fcd2956..bfcd14f 100644 --- a/devices/remote.go +++ b/devices/remote.go @@ -367,6 +367,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() diff --git a/devices/simulator.go b/devices/simulator.go index 74a2b5e..d645e1f 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -830,6 +830,33 @@ 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 := strings.TrimSpace(string(output)) + if containerPath == "" { + return fmt.Errorf("no data container found for %s", bundleID) + } + + _ = 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 { diff --git a/server/dispatch.go b/server/dispatch.go index 8404435..a4fe8fa 100644 --- a/server/dispatch.go +++ b/server/dispatch.go @@ -35,6 +35,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, diff --git a/server/server.go b/server/server.go index 0216489..b6a7c64 100644 --- a/server/server.go +++ b/server/server.go @@ -634,6 +634,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"` @@ -989,6 +994,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")