Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c057ae1
feat: add app filesystem access commands (apps fs, apps path)
gmegidish May 11, 2026
684e431
feat: implement android pull and fix ls/pull to not require bundle-id
gmegidish May 11, 2026
570ecbb
feat: implement android push and update README with filesystem commands
gmegidish May 11, 2026
d0f9ef6
fix: use exec-out for app container pull to avoid CRLF corruption on …
gmegidish May 11, 2026
f2bae42
feat: implement android mkdir (-p) and rm (-r) for fs commands
gmegidish May 11, 2026
6e8afde
docs: add mkdir and rm examples to README
gmegidish May 11, 2026
387a115
feat: implement iOS simulator filesystem commands
gmegidish May 11, 2026
f9db748
refactor: clean code and architecture review fixes
gmegidish May 12, 2026
6f6eca8
chore: remove stray file
gmegidish May 12, 2026
fd2f81f
refactor: remove bundleID from PushFile/PullFile interface
gmegidish May 12, 2026
883006f
feat: validate local file exists before push
gmegidish May 12, 2026
e178dc4
chore: remove stray file
gmegidish May 12, 2026
489c073
feat: implement device.fs.* and device.apps.path JSON-RPC handlers
gmegidish May 12, 2026
defc32a
test: add Android fs operations to emulator test suite
gmegidish May 12, 2026
35fcbbd
refactor: extract runAsArgs helper and apply Go best practices
gmegidish May 13, 2026
6f4f4a5
fix: apply clean-code review fixes
gmegidish May 14, 2026
dfe4aa3
Merge remote-tracking branch 'origin/main' into feat-working-with-fil…
gmegidish May 16, 2026
1098f8c
fixed lint
gmegidish May 16, 2026
2c288cd
Merge remote-tracking branch 'origin/main' into feat-working-with-fil…
gmegidish May 17, 2026
b691bc6
refactor: code review fixes
gmegidish May 19, 2026
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")
}
156 changes: 156 additions & 0 deletions cli/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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
switch len(args) {
case 1:
if strings.HasPrefix(args[0], "/") {
remotePath = args[0]
} else {
bundleID = args[0]
}
case 2:
bundleID = args[0]
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)

fsMkdirCmd.Flags().BoolVarP(&fsMkdirParents, "parents", "p", false, "Create parent directories as needed")
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