feat: file system support for ls, mkdir, rm, push, pull and get app path#228
Conversation
Adds ControllableDevice interface methods for app container filesystem operations (push, pull, ls, mkdir, rm, get-container-path) with working Android implementation for ls and path, and stubs for iOS, simulator, and remote devices.
- PullFile uses exec-out cat for /sdcard paths (binary-safe) and run-as cat for /data/user/ paths - ls and pull now accept a bare absolute path without a bundle-id - Fix ls to follow symlinks by retrying with trailing slash - Fix ls parser to handle absolute path names in single-file listings - Add runAdbCommandStdout helper for stdout-only adb output
- PushFile for /sdcard uses adb push directly - PushFile for /data/user/ stages via /data/local/tmp then run-as cp - push/ls CLI commands now accept bare absolute paths without bundle-id - Document apps path, apps fs ls/pull/push in README with examples
Both commands work on /sdcard and /data/user/ paths (via run-as). bundle-id is optional when passing an absolute path directly.
- All fs ops use native Go os package (simulator container is a local Mac dir) - validatePath ensures all paths stay within ~/Library/Developer/CoreSimulator/Devices/<UDID>/ - Default ls path is <device>/data; Android default remains / - Fix directory size to always be 0 (metadata block size is meaningless)
- Fix #1: use UUID for adb tmp filenames (was rand.Int(), collision-prone) - Fix #2: replace manual mutex unlock with defer-free pattern in ListFiles - Fix #3: extract androidPackageName() to eliminate 4 duplicate blocks - Fix #6: extract iosBrowseAllApps() to avoid double BrowseAllApps scan - Fix #8: promote 'apps fs' to top-level 'fs' command (filesystem ops are not app-specific); 'apps path' stays under apps - Fix #9: rename parseLsOutput/parseLsLine -> androidParseLs* - Fix #11: remove 'on app' clause from push success message - Fix #12: use %w consistently in commands/fs.go - Update README and root.go examples for new 'fs' command path
Path contains all routing info needed on every platform.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a Filesystem capability and an apps path command. CLI: new top-level fs command with subcommands push, pull, ls, mkdir, rm, plus apps path [bundle_id]. Server: registers device.fs.* and device.apps.path JSON‑RPC methods and implements base64 file transfer with a 1 MB limit. Commands: new Fs* and AppPath commands. Devices: Android implements full FS ops and container path resolution; Simulator implements full FS ops; iOS implements container path and listing (push/pull/mkdir/rm stubbed); Remote device methods stubbed. Tests and README updated for filesystem usage. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
cli/root.go (1)
124-142: ⚡ Quick winAdd an
apps pathexample in root help for discoverability.Since app-container workflows are shown here, adding one
mobilecli apps path <bundle-id> --device <device-id>example would make the new path capability easier to discover from--help.🤖 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 `@cli/root.go` around lines 124 - 142, Add a short example for the new apps path subcommand to the CLI root help text: locate the help/examples string that contains the FILESYSTEM block in root.go (the same block showing fs ls/pull/push/mkdir/rm) and insert a line demonstrating usage of the apps path command, e.g. "mobilecli apps path <bundle-id> --device <device-id>", so the apps path capability is discoverable from --help; update the help string near the FILESYSTEM examples (the root command help definition) to include this new example.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@cli/apps.go`:
- Around line 181-184: Remove the stray blank line between the
appsCmd.AddCommand(appsPathCmd) call and the
appsLaunchCmd.Flags().StringVar(...) call to satisfy gofmt; open the function
containing appsCmd, locate the block where appsCmd.AddCommand(appsPathCmd) and
appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to
launch app on") appear and delete the extra empty line so the two statements are
adjacent.
In `@devices/ios_fs.go`:
- Around line 108-158: The ListFiles method in IOSDevice never sets
FileEntry.ModTime, causing inconsistency with Android/Simulator; update
IOSDevice.ListFiles to populate ModTime from the AFC file info returned by
client.Stat (and the single-file stat path) — when you call
client.Stat(fullPath) or client.Stat(remotePath) extract the modification time
(e.g., info.ModTime or the appropriate timestamp field on the returned info) and
assign it to FileEntry.ModTime for both the single-file branch and the directory
listing loop so ModTime is consistently populated.
In `@README.md`:
- Line 279: Update the example heading text that currently reads "Example output
for `apps fs ls`" to the correct command label "Example output for `fs ls`" so
the README example matches the actual command; locate and edit the heading
string in the README where the example output for the file-system listing is
shown and replace `apps fs ls` with `fs ls`.
- Around line 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.
---
Nitpick comments:
In `@cli/root.go`:
- Around line 124-142: Add a short example for the new apps path subcommand to
the CLI root help text: locate the help/examples string that contains the
FILESYSTEM block in root.go (the same block showing fs ls/pull/push/mkdir/rm)
and insert a line demonstrating usage of the apps path command, e.g. "mobilecli
apps path <bundle-id> --device <device-id>", so the apps path capability is
discoverable from --help; update the help string near the FILESYSTEM examples
(the root command help definition) to include this new example.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 18213652-dcd5-4af8-8273-195639792576
📒 Files selected for processing (12)
README.mdcli/apps.gocli/fs.gocli/root.gocommands/apps.gocommands/fs.godevices/android.godevices/android_fs.godevices/common.godevices/ios_fs.godevices/remote_fs.godevices/simulator_fs.go
push/pull use base64 content in request/response, 1 MB limit. Temp files are used internally and cleaned up after each call.
Tests cover /sdcard/Download and app container (com.mobilenext.mobilewright_demo): mkdir -p, push, ls, pull (verify contents), rm -r
- Add runAsArgs() to eliminate duplicated /data/user/ dispatch logic across PushFile, PullFile, ListFiles, Mkdir, and Rm - Defer mutex unlock in ios_fs.go ListFiles via anonymous function - Move defer os.Remove before write in handleFsPush to eliminate duplicate manual cleanup call - Use 1 << 20 for fsSizeLimit constant (idiomatic bit-shift)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with 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.
Inline comments:
In `@server/server.go`:
- Around line 1589-1595: The code currently reads the entire file with
os.ReadFile(tmpPath) before checking fsSizeLimit which can spike memory; change
the flow in the function handling the pulled file so you stat the file first
(use os.Stat on tmpPath), compare fi.Size() to fsSizeLimit and return an error
if too large, and only then read the file into memory; reference tmpPath,
fsSizeLimit and the existing os.ReadFile call so you replace the pre-check with
a size check using os.Stat (or open the file and use io.CopyN/LimitReader)
before loading.
- Around line 1615-1621: The code currently calls
base64.StdEncoding.DecodeString(p.Content) before checking size, so huge base64
input can allocate large memory; instead compute the decoded length using
base64.StdEncoding.DecodedLen(len(p.Content)) and if that decoded length exceeds
fsSizeLimit return the existing "file too large" error before calling
DecodeString; update the block around p.Content, fsSizeLimit and DecodeString to
perform this pre-check and only decode when the decoded size is within limits.
In `@test/emulator.ts`:
- Around line 264-303: mobilecliJson and the related mobilecli call are
constructing shell strings and are vulnerable to injection and breaking on
spaces; replace execSync string-based calls with execFileSync and pass the
mobilecli binary plus each argument as an argv array (e.g., split the command
into explicit args for subcommands like 'apps','path', packageName, '--device',
deviceId or 'fs','ls', remotePath, etc.), preserve options (encoding, timeout,
stdio, env) and keep JSON.parse(result) in mobilecliJson; update fsPush, fsPull,
fsMkdir, fsRm, getAppPath and fsList callers to build and pass proper argv
arrays to mobilecli/ mobilecliJson so all dynamic values (deviceId, remotePath,
packageName, localPath) are passed as arguments rather than interpolated into a
shell string.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 52a136ee-a514-46ae-b554-120c4e8b2167
📒 Files selected for processing (5)
devices/android_fs.godevices/ios_fs.goserver/dispatch.goserver/server.gotest/emulator.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- devices/ios_fs.go
- devices/android_fs.go
- Use %w instead of %v in AppPathCommand to preserve error chain
- Rename targetDevice to device for consistency with other commands
- Avoid mutating runAsArgs slice in PullFile; build pullArgs explicitly
- Fix stale README example label ('apps fs ls' → 'fs ls')
- Remove empty if-branch with noisy comment in fsLsCmd arg parsing
- Rename getAppPath test helper to getAppContainerPath (returns container
path, not APK path; avoids confusion with GetAppPath)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/emulator.ts (1)
264-303:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReplace shell-string execution with argv-based execution in CLI helpers.
mobilecliJsonand the FS/app helper callers still construct shell strings from dynamic values. This is injection-prone and also brittle for paths containing spaces/quotes.Suggested fix (execFileSync + argument arrays)
-import { execSync } from 'child_process'; +import { execFileSync, execSync } from 'child_process'; -function mobilecli(args: string): void { +function mobilecli(args: string[]): void { const mobilecliBinary = path.join(__dirname, '..', 'mobilecli'); - const command = `${mobilecliBinary} ${args}`; try { - const result = execSync(command, { + execFileSync(mobilecliBinary, args, { encoding: 'utf8', timeout: 180000, stdio: ['pipe', 'pipe', 'pipe'], env: { + ...process.env, ANDROID_HOME: process.env.ANDROID_HOME || "", } }); } catch (error: any) { - console.log(`Command failed: ${command}`); + console.log(`Command failed: ${mobilecliBinary} ${args.map((a) => JSON.stringify(a)).join(' ')}`); if (error.stderr) { console.log(`Error stderr: ${error.stderr}`); } @@ -function mobilecliJson(args: string): any { +function mobilecliJson(args: string[]): any { const mobilecliBinary = path.join(__dirname, '..', 'mobilecli'); - const result = execSync(`${mobilecliBinary} ${args}`, { + const result = execFileSync(mobilecliBinary, args, { encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'], - env: { ANDROID_HOME: process.env.ANDROID_HOME || '' }, + env: { ...process.env, ANDROID_HOME: process.env.ANDROID_HOME || '' }, }); return JSON.parse(result); } function getAppContainerPath(deviceId: string, packageName: string): string { - const response = mobilecliJson(`apps path ${packageName} --device ${deviceId}`); + const response = mobilecliJson(['apps', 'path', packageName, '--device', deviceId]); expect(response.status).to.equal('ok'); return response.data.path; } function fsList(deviceId: string, remotePath: string): any[] { - const response = mobilecliJson(`fs ls --device ${deviceId} "${remotePath}"`); + const response = mobilecliJson(['fs', 'ls', '--device', deviceId, remotePath]); expect(response.status).to.equal('ok'); return response.data; } function fsPush(deviceId: string, localPath: string, remotePath: string): void { - mobilecli(`fs push --device ${deviceId} "${localPath}" "${remotePath}"`); + mobilecli(['fs', 'push', '--device', deviceId, localPath, remotePath]); } function fsPull(deviceId: string, remotePath: string, localPath: string): void { - mobilecli(`fs pull --device ${deviceId} "${remotePath}" "${localPath}"`); + mobilecli(['fs', 'pull', '--device', deviceId, remotePath, localPath]); } function fsMkdir(deviceId: string, remotePath: string, parents: boolean): void { - const flag = parents ? '-p ' : ''; - mobilecli(`fs mkdir --device ${deviceId} ${flag}"${remotePath}"`); + const args = ['fs', 'mkdir', '--device', deviceId]; + if (parents) args.push('-p'); + args.push(remotePath); + mobilecli(args); } function fsRm(deviceId: string, remotePath: string, recursive: boolean): void { - const flag = recursive ? '-r ' : ''; - mobilecli(`fs rm --device ${deviceId} ${flag}"${remotePath}"`); + const args = ['fs', 'rm', '--device', deviceId]; + if (recursive) args.push('-r'); + args.push(remotePath); + mobilecli(args); }#!/bin/bash # Verify remaining string-based shell execution patterns in this file. rg -nP --type=ts 'execSync\(\s*`|execSync\(\s*["'\''].*\$\{.*["'\'']\s*\)' test/emulator.ts rg -nP --type=ts 'mobilecli(Json)?\(\s*`' test/emulator.ts # Expected after fix: no matches for dynamic/interpolated shell commands.🤖 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 `@test/emulator.ts` around lines 264 - 303, mobilecliJson and the helper callers (mobilecli, getAppContainerPath, fsList, fsPush, fsPull, fsMkdir, fsRm) currently build shell-interpolated command strings which is injection-prone; change mobilecliJson to use execFileSync (or spawnSync) with an argv array (no shell=true) and update all callers to pass arguments as separate array entries (e.g., ['apps','path', packageName, '--device', deviceId] or ['fs','push','--device', deviceId, localPath, remotePath]) instead of string interpolation, ensuring paths with spaces/quotes are passed as single argv elements and retaining existing timeout/encoding/env options.
🧹 Nitpick comments (1)
test/emulator.ts (1)
105-111: ⚡ Quick winEnsure temp files are always cleaned up on failing test paths.
Cleanup currently runs only on success. If
fsPush,fsPull, or assertions fail, temp files remain and can accumulate across CI retries.Suggested fix (use try/finally)
const localFile = writeTempFile('hello from mobilecli'); -fsPush(deviceId, localFile, remoteFile); -fs.unlinkSync(localFile); +try { + fsPush(deviceId, localFile, remoteFile); +} finally { + if (fs.existsSync(localFile)) fs.unlinkSync(localFile); +} const localDest = path.join(os.tmpdir(), `mobilecli-pull-${Date.now()}.txt`); -fsPull(deviceId, remoteFile, localDest); -const contents = fs.readFileSync(localDest, 'utf8'); -expect(contents.trim()).to.equal('hello from mobilecli'); -fs.unlinkSync(localDest); +try { + fsPull(deviceId, remoteFile, localDest); + const contents = fs.readFileSync(localDest, 'utf8'); + expect(contents.trim()).to.equal('hello from mobilecli'); +} finally { + if (fs.existsSync(localDest)) fs.unlinkSync(localDest); +}Also applies to: 121-129, 172-178, 188-196
🤖 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 `@test/emulator.ts` around lines 105 - 111, The temp files created by writeTempFile (localFile) must be removed even if fsPush/fsPull or assertions throw; wrap the test body that uses localFile in a try/finally so fs.unlinkSync(localFile) runs in finally (guarded by fs.existsSync(localFile) if needed) and move fs.unlinkSync out of the success path; apply the same try/finally pattern to the other tests that create localFile and call fsPush/fsPull (the blocks around fsPush/fsPull and the tests referenced at 121-129, 172-178, 188-196) so temp files are always cleaned up on failure.
🤖 Prompt for all review comments with 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.
Duplicate comments:
In `@test/emulator.ts`:
- Around line 264-303: mobilecliJson and the helper callers (mobilecli,
getAppContainerPath, fsList, fsPush, fsPull, fsMkdir, fsRm) currently build
shell-interpolated command strings which is injection-prone; change
mobilecliJson to use execFileSync (or spawnSync) with an argv array (no
shell=true) and update all callers to pass arguments as separate array entries
(e.g., ['apps','path', packageName, '--device', deviceId] or
['fs','push','--device', deviceId, localPath, remotePath]) instead of string
interpolation, ensuring paths with spaces/quotes are passed as single argv
elements and retaining existing timeout/encoding/env options.
---
Nitpick comments:
In `@test/emulator.ts`:
- Around line 105-111: The temp files created by writeTempFile (localFile) must
be removed even if fsPush/fsPull or assertions throw; wrap the test body that
uses localFile in a try/finally so fs.unlinkSync(localFile) runs in finally
(guarded by fs.existsSync(localFile) if needed) and move fs.unlinkSync out of
the success path; apply the same try/finally pattern to the other tests that
create localFile and call fsPush/fsPull (the blocks around fsPush/fsPull and the
tests referenced at 121-129, 172-178, 188-196) so temp files are always cleaned
up on failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6ca1cf66-30b7-42e8-bc7a-8c3e75af3dba
📒 Files selected for processing (5)
README.mdcli/fs.gocommands/apps.godevices/android_fs.gotest/emulator.ts
✅ Files skipped from review due to trivial changes (1)
- README.md
🚧 Files skipped from review as they are similar to previous changes (3)
- commands/apps.go
- cli/fs.go
- devices/android_fs.go
…lator tests resolves conflict in test/emulator.ts by keeping main's simplified device structure and adding filesystem operation tests from the feature branch. replaces shell-string execSync calls with execFileSync + argv arrays to eliminate injection risk from paths with spaces or special characters.
No description provided.