mirror of
https://github.com/Canner/WrenAI.git
synced 2026-08-29 00:03:22 +08:00
Feature/cli enhancement (#81)
* feat(wren-launcher): makefile * feat(wren-launcher): detect os * fix(wren-launcher): fix panic error recovery and windows open docker * fix(wren-launcher): use exe extension * feat(wren-launcher): find available port * feat(wren-launcher): testing list process of docker API * feat(wren-launcher): check port not used and if it's used, check if wren-ui is using it
This commit is contained in:
committed by
GitHub
parent
eb8c2cb73d
commit
efc7a2f675
@@ -11,3 +11,7 @@ trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Makefile
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
@@ -67,3 +67,6 @@ yarn-error.log*
|
||||
## typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# wren-launcher
|
||||
wren-launcher/dist
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
BINARY_NAME=wren-launcher
|
||||
|
||||
build:
|
||||
env GOARCH=amd64 GOOS=darwin CGO_ENABLED=1 go build -o dist/${BINARY_NAME}-darwin main.go
|
||||
env GOARCH=amd64 GOOS=linux go build -o dist/${BINARY_NAME}-linux main.go
|
||||
env GOARCH=amd64 GOOS=windows go build -o dist/${BINARY_NAME}-windows.exe main.go
|
||||
|
||||
clean:
|
||||
go clean
|
||||
rm -rf dist
|
||||
|
||||
rebuild: clean build
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -61,23 +59,15 @@ func askForAPIKey() (string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func openbrowser(url string) error {
|
||||
var err error
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Launch() {
|
||||
// recover from panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
pterm.Error.Println("An error occurred:", r)
|
||||
fmt.Scanf("h")
|
||||
}
|
||||
}()
|
||||
|
||||
// print WrenAI header
|
||||
fmt.Println(strings.Repeat("=", 55))
|
||||
myFigure := figure.NewFigure("WrenAI", "", true)
|
||||
@@ -90,7 +80,7 @@ func Launch() {
|
||||
|
||||
if err != nil {
|
||||
pterm.Error.Println("Failed to get API key")
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// check if docker daemon is running, if not, open it and loop to check again
|
||||
@@ -116,7 +106,10 @@ func Launch() {
|
||||
|
||||
// download docker-compose file and env file template for WrenAI
|
||||
pterm.Info.Println("Downloading docker-compose file and env file")
|
||||
err = utils.PrepareDockerFiles(apiKey, projectDir)
|
||||
// find an available port
|
||||
defaultPort := 3000
|
||||
port := utils.FindAvailablePort(defaultPort)
|
||||
err = utils.PrepareDockerFiles(apiKey, port, projectDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -131,6 +124,7 @@ func Launch() {
|
||||
|
||||
// wait for 10 seconds
|
||||
pterm.Info.Println("WrenAI is starting, please wait for a moment...")
|
||||
url := fmt.Sprintf("http://localhost:%d", port)
|
||||
// wait until checking if CheckWrenAIStarted return without error
|
||||
// if timeout 2 minutes, panic
|
||||
timeoutTime := time.Now().Add(2 * time.Minute)
|
||||
@@ -140,7 +134,7 @@ func Launch() {
|
||||
}
|
||||
|
||||
// check if WrenAI is started
|
||||
err = utils.CheckWrenAIStarted()
|
||||
err = utils.CheckWrenAIStarted(url)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -149,7 +143,7 @@ func Launch() {
|
||||
|
||||
// open browser
|
||||
pterm.Info.Println("Opening browser")
|
||||
openbrowser("http://localhost:3000")
|
||||
utils.Openbrowser(url)
|
||||
|
||||
pterm.Info.Println("You can now safely close this terminal window")
|
||||
fmt.Scanf("h")
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
cmdCompose "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
@@ -23,9 +24,15 @@ const (
|
||||
DOCKER_COMPOSE_ENV_URL string = "https://gist.githubusercontent.com/wwwy3y3/5fee68a54458a07abbeb573711652292/raw/c8965ceba6eae274d2eec0595fd12b0989880ba3/.env.example"
|
||||
)
|
||||
|
||||
func replaceEnvFileContent(content string, OpenaiApiKey string) string {
|
||||
func replaceEnvFileContent(content string, OpenaiApiKey string, port int) string {
|
||||
// replace OPENAI_API_KEY
|
||||
reg := regexp.MustCompile(`OPENAI_API_KEY=sk-(.*)`)
|
||||
return reg.ReplaceAllString(content, "OPENAI_API_KEY="+OpenaiApiKey)
|
||||
str := reg.ReplaceAllString(content, "OPENAI_API_KEY="+OpenaiApiKey)
|
||||
|
||||
// replace PORT
|
||||
reg = regexp.MustCompile(`HOST_PORT=(.*)`)
|
||||
str = reg.ReplaceAllString(str, "HOST_PORT="+fmt.Sprintf("%d", port))
|
||||
return str
|
||||
}
|
||||
|
||||
func downloadFile(filepath string, url string) error {
|
||||
@@ -69,7 +76,7 @@ func CheckDockerDaemonRunning() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func PrepareDockerFiles(OpenaiApiKey string, projectDir string) error {
|
||||
func PrepareDockerFiles(openaiApiKey string, port int, projectDir string) error {
|
||||
// download docker-compose file
|
||||
composeFile := path.Join(projectDir, "docker-compose.yaml")
|
||||
pterm.Info.Println("Downloading docker-compose file to", composeFile)
|
||||
@@ -94,7 +101,7 @@ func PrepareDockerFiles(OpenaiApiKey string, projectDir string) error {
|
||||
}
|
||||
|
||||
// replace the content with regex
|
||||
newEnvFileContent := replaceEnvFileContent(string(envFileContent), OpenaiApiKey)
|
||||
newEnvFileContent := replaceEnvFileContent(string(envFileContent), openaiApiKey, port)
|
||||
newEnvFile := path.Join(projectDir, ".env")
|
||||
// write the file
|
||||
err = os.WriteFile(newEnvFile, []byte(newEnvFileContent), 0644)
|
||||
@@ -159,19 +166,64 @@ func RunDockerCompose(projectName string, projectDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func OpenDockerDaemon() error {
|
||||
// open docker daemon with command
|
||||
cmd := exec.Command("open", "-a", "Docker")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
func listProcess() ([]types.Container, error) {
|
||||
ctx := context.Background()
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
err = dockerCli.Initialize(flags.NewClientOptions())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerListOptions := container.ListOptions{
|
||||
All: true,
|
||||
}
|
||||
|
||||
containers, err := dockerCli.Client().ContainerList(ctx, containerListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func CheckWrenAIStarted() error {
|
||||
func findWrenUIContainer() (types.Container, error) {
|
||||
containers, err := listProcess()
|
||||
if err != nil {
|
||||
return types.Container{}, err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
// return if com.docker.compose.project == wrenai && com.docker.compose.service=wren-ui
|
||||
if container.Labels["com.docker.compose.project"] == "wrenai" && container.Labels["com.docker.compose.service"] == "wren-ui" {
|
||||
return container, nil
|
||||
}
|
||||
}
|
||||
|
||||
return types.Container{}, fmt.Errorf("WrenUI container not found")
|
||||
}
|
||||
|
||||
func IfPortUsedByWrenUI(port int) bool {
|
||||
container, err := findWrenUIContainer()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, containerPort := range container.Ports {
|
||||
if containerPort.PublicPort == uint16(port) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckWrenAIStarted(url string) error {
|
||||
// check response from localhost:3000
|
||||
resp, err := http.Get("http://localhost:3000")
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindWrenUIContainer(t *testing.T) {
|
||||
container, error := findWrenUIContainer()
|
||||
if error != nil {
|
||||
t.Errorf("Error: %v", error)
|
||||
}
|
||||
|
||||
t.Logf("Container ID: %s", container.ID)
|
||||
t.Logf("Container Name: %s", container.Names[0])
|
||||
for _, port := range container.Ports {
|
||||
t.Logf("Container IP: %s", port.IP)
|
||||
t.Logf("Container Port Type: %s", port.Type)
|
||||
t.Logf("Container PublicPort: %d", port.PublicPort)
|
||||
t.Logf("Container PrivatePort: %d", port.PrivatePort)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
func ifPortAvailable(port int) bool {
|
||||
// listen on port to check if it's used
|
||||
_, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func FindAvailablePort(defaultPort int) int {
|
||||
// Find an available port
|
||||
// Start from the default port and increment by 1
|
||||
// until a port is found that is not in use
|
||||
for port := defaultPort; port < defaultPort+100; port++ {
|
||||
pterm.Info.Printf("Checking if port %d is available\n", port)
|
||||
|
||||
if ifPortAvailable(port) {
|
||||
// Return the port if it's not used
|
||||
return port
|
||||
} else if IfPortUsedByWrenUI(port) {
|
||||
// Return the port if it's used, but used by wrenAI
|
||||
return port
|
||||
}
|
||||
}
|
||||
|
||||
// If no port is available, return 0
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type OS int
|
||||
|
||||
const (
|
||||
// Windows is the Windows operating system.
|
||||
Windows OS = iota
|
||||
// Darwin is the Apple operating system.
|
||||
Darwin
|
||||
// Linux is the Linux operating system.
|
||||
Linux
|
||||
// Unknown is an unknown operating system.
|
||||
Unknown
|
||||
)
|
||||
|
||||
func DetectOS() OS {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return Windows
|
||||
case "darwin":
|
||||
return Darwin
|
||||
case "linux":
|
||||
return Linux
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func Openbrowser(url string) error {
|
||||
var err error
|
||||
|
||||
switch DetectOS() {
|
||||
case Linux:
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case Windows:
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case Darwin:
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func OpenDockerDaemon() error {
|
||||
var err error
|
||||
|
||||
switch DetectOS() {
|
||||
case Linux:
|
||||
// systemctl --user start docker-desktop
|
||||
err = exec.Command("systemctl", "--user", "start", "docker-desktop").Run()
|
||||
case Windows:
|
||||
// C:\Program Files\Docker\Docker\Docker Desktop.exe
|
||||
err = exec.Command("C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe").Run()
|
||||
case Darwin:
|
||||
cmd := exec.Command("open", "-a", "Docker")
|
||||
err = cmd.Run()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user