Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **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
- **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

### 🎯 Platform Support
Expand Down Expand Up @@ -221,6 +222,88 @@ Example output for `apps foreground`:
}
```

### Filesystem 📂

Access files on the device or inside an app's data container. Currently supported on **Android** and **iOS Simulator**.

```bash
# Get the data container path of an app (Android)
mobilecli apps path <bundle-id> --device <device-id>

# List files at any absolute path (defaults to device root if omitted)
mobilecli fs ls --device <device-id>
mobilecli fs ls --device <device-id> /sdcard
mobilecli fs ls --device <device-id> /sdcard/Download

# List files inside an app's data container
mobilecli fs ls --device <device-id> com.example.app
mobilecli fs ls --device <device-id> com.example.app /Documents

# Pull a file from the device to local disk
mobilecli fs pull --device <device-id> /sdcard/recording.mp4 ./recording.mp4

# Pull a file from an app's private container
mobilecli fs pull --device <device-id> /data/user/0/com.example.app/files/db.sqlite ./db.sqlite

# Push a file to the device
mobilecli fs push --device <device-id> ./config.json /sdcard/config.json

# Push a file into an app's private container
mobilecli fs push --device <device-id> ./config.json /data/user/0/com.example.app/files/config.json

# Create a directory
mobilecli fs mkdir --device <device-id> /sdcard/myfolder

# Create a directory and all parent directories
mobilecli fs mkdir --device <device-id> -p /sdcard/a/b/c
mobilecli fs mkdir --device <device-id> -p /data/user/0/com.example.app/files/cache/v2

# Remove a file
mobilecli fs rm --device <device-id> /sdcard/old_file.txt

# Remove a directory recursively
mobilecli fs rm --device <device-id> -r /sdcard/myfolder
mobilecli fs rm --device <device-id> -r /data/user/0/com.example.app/files/cache
```

Example output for `apps path`:
```json
{
"status": "ok",
"data": {
"path": "/data/user/0/com.example.app"
}
}
```

Example output for `fs ls`:
```json
{
"status": "ok",
"data": [
{
"name": "files",
"path": "/data/user/0/com.example.app/files",
"size": 4096,
"modTime": "2026-05-11T19:20:00Z",
"isDir": true
},
{
"name": "shared_prefs",
"path": "/data/user/0/com.example.app/shared_prefs",
"size": 4096,
"modTime": "2026-05-11T12:49:00Z",
"isDir": true
}
]
}
```

**Notes:**
- Paths under `/data/user/` are accessed via `run-as`, so the app must be debuggable.
- Pushing to `/data/user/` stages the file through `/data/local/tmp/` then copies it into the container.
- Pulling binary files (images, databases, DEX files) is fully supported and binary-safe on all platforms.
Comment on lines +227 to +305
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify platform-support wording to avoid contradiction.

Line 227 limits support to Android/iOS Simulator, but Line 305 says binary-safe pull works on “all platforms.” Please align this phrasing (for example, “all supported platforms”) so expectations are clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 227 - 305, Update the wording in the "Access files on
the device..." section to remove the contradiction by changing the phrase
"Pulling binary files (images, databases, DEX files) is fully supported and
binary-safe on all platforms." to either "…on all supported platforms." or
explicitly "…on Android and iOS Simulator." Locate and edit the notes block (the
paragraph starting with "Pulling binary files...") in the README's file-system
section so the platform claim matches the earlier sentence that support is
currently limited to Android and iOS Simulator.


### Agent Management 🤖

On **iOS**, the on-device agent is required for touch input (taps, swipes, button presses), screen capture streaming, and UI tree inspection. These capabilities are not available through standard iOS tooling without an agent running on the device.
Expand Down
21 changes: 21 additions & 0 deletions cli/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@ var appsUninstallCmd = &cobra.Command{
},
}

var appsPathCmd = &cobra.Command{
Use: "path [bundle_id]",
Short: "Get the container path of an app on a device",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
req := commands.AppPathRequest{
DeviceID: deviceId,
BundleID: args[0],
}

response := commands.AppPathCommand(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",
Expand Down Expand Up @@ -159,6 +178,7 @@ func init() {
appsCmd.AddCommand(appsInstallCmd)
appsCmd.AddCommand(appsUninstallCmd)
appsCmd.AddCommand(appsForegroundCmd)
appsCmd.AddCommand(appsPathCmd)

appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
appsLaunchCmd.Flags().StringVar(&locale, "locale", "", "Comma-separated BCP 47 locale tags (e.g., fr-FR,en-GB)")
Expand All @@ -170,4 +190,5 @@ func init() {
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")
appsForegroundCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get foreground app from")
appsPathCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
}
158 changes: 158 additions & 0 deletions cli/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cli

import (
"fmt"
"strings"

"github.com/mobile-next/mobilecli/commands"
"github.com/spf13/cobra"
)

var fsCmd = &cobra.Command{
Use: "fs",
Short: "Access device filesystem",
Long: `Push, pull, list, and manage files on a device or in an app's container.`,
}

var fsPushCmd = &cobra.Command{
Use: "push <local-path> <remote-path>",
Short: "Push a file to the device or into an app's container",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
req := commands.FsPushRequest{
DeviceID: deviceId,
LocalPath: args[0],
RemotePath: args[1],
}
response := commands.FsPushCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var fsPullCmd = &cobra.Command{
Use: "pull <remote-path> <local-path>",
Short: "Pull a file from the device or from an app's container",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
req := commands.FsPullRequest{
DeviceID: deviceId,
RemotePath: args[0],
LocalPath: args[1],
}
response := commands.FsPullCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var fsLsCmd = &cobra.Command{
Use: "ls [bundle-id] [remote-path]",
Short: "List files on the device or in an app's container",
Args: cobra.RangeArgs(0, 2),
RunE: func(cmd *cobra.Command, args []string) error {
var bundleID, remotePath string
if len(args) == 1 && strings.HasPrefix(args[0], "/") {
remotePath = args[0]
} else {
bundleID = args[0]
if len(args) == 2 {
remotePath = args[1]
}
}
req := commands.FsListRequest{
DeviceID: deviceId,
BundleID: bundleID,
RemotePath: remotePath,
}
response := commands.FsListCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var (
fsMkdirParents bool
fsRmRecursive bool
)

var fsMkdirCmd = &cobra.Command{
Use: "mkdir [bundle-id] <remote-path>",
Short: "Create a directory on the device or in an app's container",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
var bundleID, remotePath string
if len(args) == 1 {
remotePath = args[0]
} else {
bundleID = args[0]
remotePath = args[1]
}
req := commands.FsMkdirRequest{
DeviceID: deviceId,
BundleID: bundleID,
RemotePath: remotePath,
Parents: fsMkdirParents,
}
response := commands.FsMkdirCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var fsRmCmd = &cobra.Command{
Use: "rm [bundle-id] <remote-path>",
Short: "Remove a file or directory on the device or in an app's container",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
var bundleID, remotePath string
if len(args) == 1 {
remotePath = args[0]
} else {
bundleID = args[0]
remotePath = args[1]
}
req := commands.FsRmRequest{
DeviceID: deviceId,
BundleID: bundleID,
RemotePath: remotePath,
Recursive: fsRmRecursive,
}
response := commands.FsRmCommand(req)
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

func init() {
rootCmd.AddCommand(fsCmd)

fsCmd.AddCommand(fsPushCmd)
fsCmd.AddCommand(fsPullCmd)
fsCmd.AddCommand(fsLsCmd)
fsCmd.AddCommand(fsMkdirCmd)
fsCmd.AddCommand(fsRmCmd)

fsPushCmd.Flags().StringVar(&deviceId, "device", "", "ID of the target device")
fsPullCmd.Flags().StringVar(&deviceId, "device", "", "ID of the target device")
fsLsCmd.Flags().StringVar(&deviceId, "device", "", "ID of the target device")
fsMkdirCmd.Flags().StringVar(&deviceId, "device", "", "ID of the target device")
fsMkdirCmd.Flags().BoolVarP(&fsMkdirParents, "parents", "p", false, "Create parent directories as needed")
fsRmCmd.Flags().StringVar(&deviceId, "device", "", "ID of the target device")
fsRmCmd.Flags().BoolVarP(&fsRmRecursive, "recursive", "r", false, "Remove directories and their contents recursively")
}
19 changes: 19 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,25 @@ REMOTE DEVICES:
# Release an allocated remote device
mobilecli remote release --device <device-id>

FILESYSTEM:
# List files on the device
mobilecli fs ls --device <device-id> /sdcard

# List files in an app's container
mobilecli fs ls --device <device-id> com.example.app /Documents

# Pull a file from the device
mobilecli fs pull --device <device-id> /sdcard/file.txt ./file.txt

# Push a file to the device
mobilecli fs push --device <device-id> ./file.txt /sdcard/file.txt

# Create a directory
mobilecli fs mkdir --device <device-id> -p /sdcard/a/b/c

# Remove a file or directory
mobilecli fs rm --device <device-id> -r /sdcard/myfolder

UTILITIES:
# Open a URL or deep link
mobilecli url --device <device-id> https://example.com
Expand Down
25 changes: 25 additions & 0 deletions commands/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ func InstallAppCommand(req InstallAppRequest) *CommandResponse {
})
}

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

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

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

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

return NewSuccessResponse(map[string]any{
"path": path,
})
}

type UninstallAppRequest struct {
DeviceID string `json:"deviceId"`
PackageName string `json:"packageName"`
Expand Down
Loading
Loading