Skip to content

Commit 4d0f76f

Browse files
committed
feat: mobilecli doctor
1 parent 6b09091 commit 4d0f76f

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-0
lines changed

cli/doctor.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mobile-next/mobilecli/commands"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var doctorCmd = &cobra.Command{
11+
Use: "doctor",
12+
Short: "Run system diagnostics",
13+
Long: `Performs system diagnostics for better troubleshooting`,
14+
RunE: func(cmd *cobra.Command, args []string) error {
15+
response := commands.DoctorCommand(GetVersion())
16+
printJson(response)
17+
if response.Status == "error" {
18+
return fmt.Errorf("%s", response.Error)
19+
}
20+
return nil
21+
},
22+
}
23+
24+
func init() {
25+
rootCmd.AddCommand(doctorCmd)
26+
}

cli/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ func Execute() error {
128128
return rootCmd.Execute()
129129
}
130130

131+
// GetVersion returns the current version of mobilecli
132+
func GetVersion() string {
133+
return version
134+
}
135+
131136
// printJson is a helper function to print JSON responses
132137
func printJson(data interface{}) {
133138
jsonData, err := json.MarshalIndent(data, "", " ")

commands/doctor.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package commands
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
)
10+
11+
type DoctorInfo struct {
12+
MobileCLIVersion string `json:"mobilecli_version"`
13+
OS string `json:"os"`
14+
OSVersion string `json:"os_version"`
15+
AndroidHome string `json:"android_home"`
16+
ADBPath string `json:"adb_path"`
17+
ADBVersion string `json:"adb_version,omitempty"`
18+
EmulatorPath string `json:"emulator_path"`
19+
XcodePath string `json:"xcode_path,omitempty"`
20+
XcodeCLIToolsPath string `json:"xcode_cli_tools_path,omitempty"`
21+
DevToolsSecurityEnabled *bool `json:"devtools_security_enabled,omitempty"`
22+
}
23+
24+
func getAndroidSdkPath() string {
25+
sdkPath := os.Getenv("ANDROID_HOME")
26+
if sdkPath != "" {
27+
if _, err := os.Stat(sdkPath); err == nil {
28+
return sdkPath
29+
}
30+
}
31+
32+
// try default Android SDK location on macOS
33+
homeDir := os.Getenv("HOME")
34+
if homeDir != "" {
35+
defaultPath := filepath.Join(homeDir, "Library", "Android", "sdk")
36+
if _, err := os.Stat(defaultPath); err == nil {
37+
return defaultPath
38+
}
39+
}
40+
41+
// try default Android SDK location on Windows
42+
if runtime.GOOS == "windows" {
43+
localAppData := os.Getenv("LOCALAPPDATA")
44+
if localAppData != "" {
45+
defaultPath := filepath.Join(localAppData, "Android", "Sdk")
46+
if _, err := os.Stat(defaultPath); err == nil {
47+
return defaultPath
48+
}
49+
}
50+
51+
// fallback to USERPROFILE on Windows
52+
userProfile := os.Getenv("USERPROFILE")
53+
if userProfile != "" {
54+
defaultPath := filepath.Join(userProfile, "AppData", "Local", "Android", "Sdk")
55+
if _, err := os.Stat(defaultPath); err == nil {
56+
return defaultPath
57+
}
58+
}
59+
}
60+
61+
return ""
62+
}
63+
64+
func getAdbPath() string {
65+
sdkPath := getAndroidSdkPath()
66+
if sdkPath != "" {
67+
adbPath := filepath.Join(sdkPath, "platform-tools", "adb")
68+
if runtime.GOOS == "windows" {
69+
adbPath += ".exe"
70+
}
71+
72+
if _, err := os.Stat(adbPath); err == nil {
73+
return adbPath
74+
}
75+
}
76+
77+
// check if adb is in PATH
78+
adbPath, err := exec.LookPath("adb")
79+
if err == nil {
80+
return adbPath
81+
}
82+
83+
return ""
84+
}
85+
86+
func getEmulatorPath() string {
87+
sdkPath := getAndroidSdkPath()
88+
if sdkPath != "" {
89+
emulatorPath := filepath.Join(sdkPath, "emulator", "emulator")
90+
if runtime.GOOS == "windows" {
91+
emulatorPath += ".exe"
92+
}
93+
if _, err := os.Stat(emulatorPath); err == nil {
94+
return emulatorPath
95+
}
96+
}
97+
98+
// check if emulator is in PATH
99+
emulatorPath, err := exec.LookPath("emulator")
100+
if err == nil {
101+
return emulatorPath
102+
}
103+
104+
return ""
105+
}
106+
107+
func getAdbVersion(adbPath string) string {
108+
if adbPath == "" {
109+
return ""
110+
}
111+
112+
cmd := exec.Command(adbPath, "version")
113+
output, err := cmd.CombinedOutput()
114+
if err != nil {
115+
return ""
116+
}
117+
118+
// parse the output to get just the version line
119+
lines := strings.Split(string(output), "\n")
120+
for _, line := range lines {
121+
if strings.Contains(line, "Android Debug Bridge version") {
122+
return strings.TrimSpace(line)
123+
}
124+
}
125+
126+
return strings.TrimSpace(string(output))
127+
}
128+
129+
func getXcodePath() string {
130+
if runtime.GOOS != "darwin" {
131+
return ""
132+
}
133+
134+
// check if Xcode.app is installed
135+
cmd := exec.Command("xcode-select", "-p")
136+
output, err := cmd.CombinedOutput()
137+
if err != nil {
138+
return ""
139+
}
140+
141+
path := strings.TrimSpace(string(output))
142+
143+
// check if this is the full Xcode.app path
144+
if strings.Contains(path, "Xcode.app") {
145+
return path
146+
}
147+
148+
return ""
149+
}
150+
151+
func getXcodeCLIToolsPath() string {
152+
if runtime.GOOS != "darwin" {
153+
return ""
154+
}
155+
156+
cmd := exec.Command("xcode-select", "-p")
157+
output, err := cmd.CombinedOutput()
158+
if err != nil {
159+
return ""
160+
}
161+
162+
path := strings.TrimSpace(string(output))
163+
164+
// verify the path exists
165+
if _, err := os.Stat(path); err == nil {
166+
return path
167+
}
168+
169+
return ""
170+
}
171+
172+
func getDevToolsSecurityEnabled() *bool {
173+
if runtime.GOOS != "darwin" {
174+
return nil
175+
}
176+
177+
cmd := exec.Command("DevToolsSecurity", "-status")
178+
output, err := cmd.CombinedOutput()
179+
180+
if err != nil {
181+
return nil
182+
}
183+
184+
outputStr := strings.TrimSpace(string(output))
185+
// the output is typically "Developer mode is currently enabled." or "Developer mode is currently disabled."
186+
enabled := strings.Contains(strings.ToLower(outputStr), "enabled")
187+
188+
return &enabled
189+
}
190+
191+
func getOSVersion() string {
192+
switch runtime.GOOS {
193+
case "darwin":
194+
cmd := exec.Command("sw_vers", "-productVersion")
195+
output, err := cmd.CombinedOutput()
196+
if err != nil {
197+
return ""
198+
}
199+
return strings.TrimSpace(string(output))
200+
case "windows":
201+
cmd := exec.Command("cmd", "/c", "ver")
202+
output, err := cmd.CombinedOutput()
203+
if err != nil {
204+
return ""
205+
}
206+
return strings.TrimSpace(string(output))
207+
case "linux":
208+
// try reading /etc/os-release
209+
data, err := os.ReadFile("/etc/os-release")
210+
if err != nil {
211+
return ""
212+
}
213+
lines := strings.Split(string(data), "\n")
214+
for _, line := range lines {
215+
if strings.HasPrefix(line, "PRETTY_NAME=") {
216+
return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
217+
}
218+
}
219+
return ""
220+
default:
221+
return ""
222+
}
223+
}
224+
225+
// DoctorCommand performs system diagnostics and returns information about the environment
226+
func DoctorCommand(version string) *CommandResponse {
227+
info := DoctorInfo{
228+
MobileCLIVersion: version,
229+
OS: runtime.GOOS,
230+
OSVersion: getOSVersion(),
231+
AndroidHome: os.Getenv("ANDROID_HOME"),
232+
ADBPath: getAdbPath(),
233+
EmulatorPath: getEmulatorPath(),
234+
}
235+
236+
// get adb version if adb is available
237+
if info.ADBPath != "" {
238+
info.ADBVersion = getAdbVersion(info.ADBPath)
239+
}
240+
241+
// only get Xcode path on darwin
242+
if runtime.GOOS == "darwin" {
243+
info.XcodePath = getXcodePath()
244+
info.XcodeCLIToolsPath = getXcodeCLIToolsPath()
245+
info.DevToolsSecurityEnabled = getDevToolsSecurityEnabled()
246+
}
247+
248+
return NewSuccessResponse(info)
249+
}

0 commit comments

Comments
 (0)