-
Notifications
You must be signed in to change notification settings - Fork 17
feat: file system support for ls, mkdir, rm, push, pull and get app path #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 15 commits
c057ae1
684e431
570ecbb
d0f9ef6
f2bae42
6e8afde
387a115
f9db748
6f6eca8
fd2f81f
883006f
e178dc4
489c073
defc32a
35fcbbd
6f4f4a5
dfe4aa3
1098f8c
2c288cd
b691bc6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 `apps 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
|
|
||
| ### 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| 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) == 0 { | ||
| // leave remotePath empty; each device picks its own default | ||
| } else 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") | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.