diff --git a/seme/README.md b/seme/README.md new file mode 100644 index 00000000..d0f221f8 --- /dev/null +++ b/seme/README.md @@ -0,0 +1,113 @@ +# seme - Second-Me CLI Tool + +`seme` is a command-line tool written in Golang to manage the lifecycle of Second-Me application services. It replaces the original set of shell scripts with a more consistent interface and better error handling. + +## Installation + +### Building from Source + +```bash +git clone https://github.com/bitliu/Second-Me.git +cd Second-Me/seme +go build -o seme +``` + +You can then move the compiled binary to a directory in your system path, for example: + +```bash +sudo mv seme /usr/local/bin/ +``` + +## Usage + +`seme` provides the following commands: + +### Setting Up the Environment + +```bash +# Set up the complete environment (Python, llama.cpp, and frontend) +seme setup + +# Set up specific components only +seme setup python +seme setup llama +seme setup frontend + +# Skip confirmation steps +seme setup --skip-confirmation +``` + +### Starting Services + +```bash +# Start all services (frontend and backend) +seme start + +# Start backend service only +seme start --backend-only +``` + +### Stopping Services + +```bash +# Stop all services +seme stop +``` + +### Restarting Services + +```bash +# Restart all services +seme restart + +# Restart backend service only +seme restart --backend-only + +# Force restart (terminate all related processes) +seme restart --force +``` + +### Checking Service Status + +```bash +# Check the status of all services +seme status +``` + +## Configuration + +`seme` will search for a `.env` file in the current directory and its parent directories to load environment variables. You can also specify the location of the environment variable file using command-line arguments: + +```bash +seme --env-file /path/to/.env start +``` + +You can also specify the project root directory: + +```bash +seme --base-path /path/to/project start +``` + +## Global Options + +The following options are available for all commands: + +- `--base-path string`: Specify the project root directory +- `--env-file string`: Specify the path to the environment variable file +- `-v, --verbose`: Enable verbose output mode +- `-h, --help`: Show help information + +## Shell Completion + +`seme` supports generating shell completion scripts for different shells: + +```bash +# Generate bash completion script +seme completion bash > ~/.bash_completion.d/seme + +# Generate zsh completion script +seme completion zsh > "${fpath[1]}/_seme" + +# Generate fish completion script +seme completion fish > ~/.config/fish/completions/seme.fish +``` \ No newline at end of file diff --git a/seme/cmd/helpers.md b/seme/cmd/helpers.md new file mode 100644 index 00000000..d1200733 --- /dev/null +++ b/seme/cmd/helpers.md @@ -0,0 +1,92 @@ +# Second-Me CLI Interface Enhancement + +The Second-Me CLI interface has been enhanced with emojis and improved formatting to provide a better user experience. + +## Command Structure + +Each command now includes specific emojis and color-coded output: + +| Command | Emoji | Description | +|---------|-------|-------------| +| `seme start` | 🚀 | Start Second-Me services | +| `seme stop` | 🛑 | Stop all services | +| `seme restart` | 🔄 | Restart services | +| `seme status` | 📊 | Check services status | +| `seme setup` | ⚙️ | Set up environment | + +## Improved Logging + +Log messages have been enhanced with appropriate emojis: + +- ℹ️ [INFO] - Informational messages +- ✅ [SUCCESS] - Success messages +- ⚠️ [WARNING] - Warning messages +- ❌ [ERROR] - Error messages + +## Section Headers + +Section headers now use a consistent format with emojis: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 🔷 SECTION NAME +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## Status Command Example + +The status command now provides a more visual representation of service status: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 🔷 📊 SERVICE STATUS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Backend Service: + PID File: Running ✅ (PID: 12345, Process: python) + ▶ python server.py + +Frontend Service: + PID File: Running ✅ (PID: 12346, Process: node) + ▶ node --watch app.js + +LLM Server Status: + Port 8080: In use ✅ (PID: 12347, Process: llama-server) + ▶ ./llama-server --model models/7B.gguf + +Summary: + 🖥️ Backend: Running ✅ + 🌐 Frontend: Running ✅ + 🦙 LLM Server: Running ✅ +``` + +## Setup Command Example + +The setup command provides clear progress indicators: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 🔷 ⚙️ SETTING UP PYTHON ENVIRONMENT +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +[2023-08-10 12:34:56] ℹ️ [INFO] Using conda environment: second-me +[2023-08-10 12:34:56] ℹ️ [INFO] Creating Conda environment: second-me +[2023-08-10 12:35:12] ✅ [SUCCESS] Conda environment second-me created successfully ✓ +[2023-08-10 12:35:12] ℹ️ [INFO] Installing Python dependencies... +[2023-08-10 12:36:05] ✅ [SUCCESS] Python environment setup completed ✓ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 🔷 🦙 BUILDING LLAMA.CPP +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## Additional Emojis + +Component-specific emojis make it easier to identify what's being processed: + +- 🐍 Python environment +- 🌐 Frontend services +- 🦙 LLama.cpp and LLM services +- ✓ Check mark for completed tasks + +These enhancements create a more engaging and informative command-line experience for users of the Second-Me CLI tool. \ No newline at end of file diff --git a/seme/cmd/restart.go b/seme/cmd/restart.go new file mode 100644 index 00000000..edf000c6 --- /dev/null +++ b/seme/cmd/restart.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + force bool + backendOnlyRestart bool +) + +// restartCmd represents the restart command +var restartCmd = &cobra.Command{ + Use: "restart", + Short: "Restart Second-Me services", + Long: `Stop and restart Second-Me services, including backend and frontend.`, + Run: restartServices, +} + +func init() { + RootCmd.AddCommand(restartCmd) + restartCmd.Flags().BoolVar(&force, "force", false, "Force restart, killing all related processes") + restartCmd.Flags().BoolVar(&backendOnlyRestart, "backend-only", false, "Restart backend service only") +} + +// restartServices is the handler for the restart command +func restartServices(cmd *cobra.Command, args []string) { + logSection(restartEmoji + " RESTARTING SERVICES") + + // Execute stop command + logInfo(stopEmoji + " Stopping services...") + stopCmd.Run(cmd, args) + + // Execute start command, with backend-only flag if specified + logInfo(startEmoji + " Starting services...") + if backendOnlyRestart { + backendOnly = true + } + startCmd.Run(cmd, args) + + logSuccess("Services restarted successfully " + successEmoji) +} diff --git a/seme/cmd/root.go b/seme/cmd/root.go new file mode 100644 index 00000000..a37d2643 --- /dev/null +++ b/seme/cmd/root.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/fatih/color" + "github.com/joho/godotenv" + "github.com/spf13/cobra" +) + +var ( + // Global flags + verbose bool + envFile string + basePath string + + // Color definitions + red = color.New(color.FgRed).SprintFunc() + green = color.New(color.FgGreen).SprintFunc() + yellow = color.New(color.FgYellow).SprintFunc() + blue = color.New(color.FgBlue).SprintFunc() + cyan = color.New(color.FgCyan).SprintFunc() + magenta = color.New(color.FgMagenta).SprintFunc() + gray = color.New(color.FgHiBlack).SprintFunc() + bold = color.New(color.Bold).SprintFunc() + underline = color.New(color.Underline).SprintFunc() + + // Emoji set for different log types + infoEmoji = "ℹ️ " + successEmoji = "✅ " + warningEmoji = "⚠️ " + errorEmoji = "❌ " + sectionEmoji = "🔷 " + startEmoji = "🚀 " + stopEmoji = "🛑 " + restartEmoji = "🔄 " + statusEmoji = "📊 " + setupEmoji = "⚙️ " + pythonEmoji = "🐍 " + frontendEmoji = "🌐 " + llamaEmoji = "🦙 " + checkEmoji = "✓ " +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "seme", + Short: "Second-Me CLI Tool", + Long: `seme is a CLI tool for managing Second-Me application, +it can start, stop, restart the application and check the status.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Find project root if not explicitly set + if basePath == "" { + cwd, err := os.Getwd() + if err != nil { + fmt.Println(red("Cannot get current working directory:"), err) + os.Exit(1) + } + + // Look for .env file up the directory tree to identify project root + dir := cwd + for { + if _, err := os.Stat(filepath.Join(dir, ".env")); err == nil { + basePath = dir + break + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + // If .env file not found, use current directory + if basePath == "" { + basePath = cwd + } + } + + // Load environment variables + if envFile == "" { + envFile = filepath.Join(basePath, ".env") + } + + err := godotenv.Load(envFile) + if err != nil { + // Just show a warning as some commands may not require environment variables + fmt.Println(yellow(warningEmoji+"Warning: Cannot load environment variables file:"), envFile) + } + }, +} + +// Initialize command line flags +func init() { + RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + RootCmd.PersistentFlags().StringVar(&envFile, "env-file", "", "Path to environment variables file (default: .env)") + RootCmd.PersistentFlags().StringVar(&basePath, "base-path", "", "Project root directory path") +} + +// Get current timestamp +func getTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +// Log functions +func logInfo(message string) { + fmt.Printf("%s %s %s\n", gray("["+getTimestamp()+"]"), blue(infoEmoji+"[INFO]"), message) +} + +func logSuccess(message string) { + fmt.Printf("%s %s %s\n", gray("["+getTimestamp()+"]"), green(successEmoji+"[SUCCESS]"), message) +} + +func logWarning(message string) { + fmt.Printf("%s %s %s\n", gray("["+getTimestamp()+"]"), yellow(warningEmoji+"[WARNING]"), message) +} + +func logError(message string) { + fmt.Printf("%s %s %s\n", gray("["+getTimestamp()+"]"), red(errorEmoji+"[ERROR]"), message) +} + +func logSection(message string) { + bar := strings.Repeat("━", 80) + fmt.Printf("\n%s\n %s %s\n%s\n\n", cyan(bar), sectionEmoji, cyan(bold(message)), cyan(bar)) +} + +// Get value from environment variables, or use default value if not exists +func getEnvOrDefault(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +// Check if port is in use +func isPortInUse(port string) bool { + cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%s", port)) + err := cmd.Run() + // If command executes successfully, the port is in use + return err == nil +} + +// Get process information by PID file +func getProcessInfo(pidFile string) (int, string, string, bool) { + // In a real project, implement this function to read the PID file and get process information + // This is just a placeholder + return 0, "", "", false +} diff --git a/seme/cmd/setup.go b/seme/cmd/setup.go new file mode 100644 index 00000000..c73111e5 --- /dev/null +++ b/seme/cmd/setup.go @@ -0,0 +1,574 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var ( + skipConfirmation bool + setupComponent string +) + +// setupCmd represents the setup command +var setupCmd = &cobra.Command{ + Use: "setup [component]", + Short: "Set up Second-Me development environment", + Long: `Set up Second-Me development environment, including Python environment, llama.cpp, and frontend. +You can specify to set up a specific component only: python, llama, frontend`, + Run: runSetup, +} + +func init() { + RootCmd.AddCommand(setupCmd) + setupCmd.Flags().BoolVar(&skipConfirmation, "skip-confirmation", false, "Skip confirmation steps") +} + +// runSetup is the handler for the setup command +func runSetup(cmd *cobra.Command, args []string) { + // Parse component argument + if len(args) > 0 { + setupComponent = args[0] + validComponents := map[string]bool{ + "python": true, + "llama": true, + "frontend": true, + } + if !validComponents[setupComponent] { + logError(fmt.Sprintf("Invalid component: %s", bold(setupComponent))) + fmt.Printf("\nValid components: %s, %s, %s\n\n", + cyan(bold("python")), + cyan(bold("llama")), + cyan(bold("frontend"))) + os.Exit(1) + } + } + + // Display welcome message + displayHeader() + + // Check potential conflicts + logSection(setupEmoji + " RUNNING PRE-INSTALLATION CHECKS") + if !checkPotentialConflicts() { + logError("Basic tools check failed") + os.Exit(1) + } + + // Start installation process + logSection(setupEmoji + " STARTING INSTALLATION") + + // If a component is specified, only install that component + if setupComponent != "" { + switch setupComponent { + case "python": + setupPythonEnvironment() + case "llama": + buildLlama() + case "frontend": + buildFrontend() + } + return + } + + // Install all components + if !setupPythonEnvironment() { + os.Exit(1) + } + + if !setupNpm() { + logError("npm setup failed") + os.Exit(1) + } + + if !checkAndInstallCmake() { + logError("cmake check and installation failed") + os.Exit(1) + } + + if !buildLlama() { + os.Exit(1) + } + + if !buildFrontend() { + os.Exit(1) + } + + logSuccess("Installation complete! " + successEmoji) + fmt.Printf("\n %s %s\n\n", startEmoji, cyan(bold("Run 'seme start' to start the services"))) +} + +// Display header information +func displayHeader() { + fmt.Println("") + fmt.Println(cyan(" ███████╗███████╗ ██████╗ ██████╗ ███╗ ██╗██████╗ ███╗ ███╗███████╗")) + fmt.Println(cyan(" ██╔════╝██╔════╝██╔════╝██╔═══██╗████╗ ██║██╔══██╗ ████╗ ████║██╔════╝")) + fmt.Println(cyan(" ███████╗█████╗ ██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗██╔████╔██║█████╗ ")) + fmt.Println(cyan(" ╚════██║██╔══╝ ██║ ██║ ██║██║╚██╗██║██║ ██║╚════╝██║╚██╔╝██║██╔══╝ ")) + fmt.Println(cyan(" ███████║███████╗╚██████╗╚██████╔╝██║ ╚████║██████╔╝ ██║ ╚═╝ ██║███████╗")) + fmt.Println(cyan(" ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝╚══════╝")) + fmt.Println("") + fmt.Printf(" %s %s\n", setupEmoji, bold(magenta("Second-Me Setup Tool"))) + fmt.Println("") +} + +// Check if command exists +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +// Check potential conflicts +func checkPotentialConflicts() bool { + logInfo("Checking necessary tools...") + + // Check Homebrew installation + if commandExists("brew") { + logInfo("Homebrew is installed") + } else { + logWarning("Homebrew not installed, attempting automatic installation...") + // Here we just show a message, not actually installing + logError("Homebrew not installed, please install Homebrew first: https://brew.sh") + return false + } + + // Check Conda installation + if commandExists("conda") { + logInfo("Conda is installed") + } else { + logWarning("Conda not installed, attempting automatic installation...") + // Use Homebrew to install Conda + if !installConda() { + logError("Automatic Conda installation failed") + return false + } + } + + // Check config files and directory permissions + if !checkConfigFiles() { + logError("Configuration files check failed") + return false + } + + if !checkDirectoryPermissions() { + logError("Directory permissions check failed") + return false + } + + return true +} + +// Install Conda +func installConda() bool { + logInfo("Installing Conda using Homebrew...") + + cmd := exec.Command("brew", "install", "--cask", "miniconda") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install Conda: %v, output: %s", err, string(output))) + return false + } + + logSuccess("Conda installed successfully") + return true +} + +// Check configuration files +func checkConfigFiles() bool { + logInfo("Checking configuration files...") + // Implement configuration file checking here + // Simplified to always return true + return true +} + +// Check directory permissions +func checkDirectoryPermissions() bool { + logInfo("Checking directory permissions...") + + dirsToCheck := []string{ + filepath.Join(basePath, "logs"), + filepath.Join(basePath, "run"), + } + + for _, dir := range dirsToCheck { + // Ensure directory exists + if err := ensureDir(dir); err != nil { + logError(fmt.Sprintf("Cannot create directory %s: %v", dir, err)) + return false + } + + // Check if writable + testFile := filepath.Join(dir, ".write_test") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { + logError(fmt.Sprintf("Directory %s is not writable: %v", dir, err)) + return false + } + + // Clean up test file + os.Remove(testFile) + } + + return true +} + +// Set up Python environment +func setupPythonEnvironment() bool { + logSection("SETTING UP PYTHON ENVIRONMENT") + + condaEnv := getEnvOrDefault("CONDA_DEFAULT_ENV", "second-me") + logInfo(fmt.Sprintf("Using Conda environment: %s", condaEnv)) + + // Check if Conda environment exists + cmd := exec.Command("conda", "env", "list") + output, err := cmd.CombinedOutput() + if err != nil { + logError(fmt.Sprintf("Failed to get Conda environment list: %v", err)) + return false + } + + // Check if environment name is in the output + if !strings.Contains(string(output), condaEnv) { + logInfo(fmt.Sprintf("Creating Conda environment: %s", condaEnv)) + + // Use environment.yml file to create environment + envFile := filepath.Join(basePath, "environment.yml") + if fileExists(envFile) { + cmd = exec.Command("conda", "env", "create", "-f", envFile) + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to create Conda environment: %v, output: %s", err, string(output))) + return false + } + } else { + // If no environment.yml file, create with Python 3.10 + cmd = exec.Command("conda", "create", "-n", condaEnv, "python=3.10", "-y") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to create Conda environment: %v, output: %s", err, string(output))) + return false + } + } + + logSuccess(fmt.Sprintf("Conda environment %s created successfully", condaEnv)) + } else { + logInfo(fmt.Sprintf("Conda environment %s already exists, skipping creation", condaEnv)) + } + + // Install Python dependencies + logInfo("Installing Python dependencies...") + + // Check for requirements.txt or pyproject.toml + pytorchToml := filepath.Join(basePath, "pyproject.toml") + if fileExists(pytorchToml) { + logInfo("Found pyproject.toml, using Poetry to install dependencies...") + + // Use conda run to execute command in the specified environment + cmd = exec.Command("conda", "run", "-n", condaEnv, "pip", "install", "poetry") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install Poetry: %v, output: %s", err, string(output))) + return false + } + + cmd = exec.Command("conda", "run", "-n", condaEnv, "poetry", "install") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install dependencies with Poetry: %v, output: %s", err, string(output))) + return false + } + } else { + // If no pyproject.toml, look for requirements.txt + reqFile := filepath.Join(basePath, "requirements.txt") + if fileExists(reqFile) { + logInfo("Found requirements.txt, using pip to install dependencies...") + + cmd = exec.Command("conda", "run", "-n", condaEnv, "pip", "install", "-r", reqFile) + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install dependencies with pip: %v, output: %s", err, string(output))) + return false + } + } else { + logWarning("No pyproject.toml or requirements.txt found, skipping dependency installation") + } + } + + // Check specific dependency packages + graphragPath := filepath.Join(basePath, "dependencies/graphrag-1.2.1.dev27.tar.gz") + if fileExists(graphragPath) { + logInfo("Installing specific version of graphrag...") + + cmd = exec.Command("conda", "run", "-n", condaEnv, "pip", "install", "--force-reinstall", graphragPath) + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install graphrag: %v, output: %s", err, string(output))) + return false + } + + logSuccess("graphrag installed successfully") + } + + logSuccess("Python environment setup completed") + return true +} + +// Set up npm +func setupNpm() bool { + logInfo("Setting up npm package manager...") + + // Check if npm is already installed + if !commandExists("npm") { + logWarning("npm not found - installing Node.js and npm") + + cmd := exec.Command("brew", "install", "node") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install Node.js and npm: %v, output: %s", err, string(output))) + return false + } + + // Verify npm was installed successfully + if !commandExists("npm") { + logError("npm installation failed - command not found after installation") + return false + } + logSuccess("Successfully installed Node.js and npm") + } else { + logSuccess("npm is already installed") + } + + // Configure npm settings + logInfo("Configuring npm settings...") + + // Set npm registry + cmd := exec.Command("npm", "config", "set", "registry", "https://registry.npmjs.org/") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to set npm registry: %v, output: %s", err, string(output))) + return false + } + + // Set npm cache directory + cmd = exec.Command("npm", "config", "set", "cache", filepath.Join(os.Getenv("HOME"), ".npm")) + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to set npm cache directory: %v, output: %s", err, string(output))) + return false + } + + // Verify npm configuration + cmd = exec.Command("npm", "config", "list") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("npm configuration failed: %v, output: %s", err, string(output))) + return false + } + + logSuccess("npm setup completed") + return true +} + +// Check and install cmake +func checkAndInstallCmake() bool { + logInfo("Checking cmake installation...") + + if !commandExists("cmake") { + logWarning("cmake not installed, attempting automatic installation...") + + cmd := exec.Command("brew", "install", "cmake") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install cmake using Homebrew: %v, output: %s", err, string(output))) + return false + } + logSuccess("cmake installed successfully") + } else { + logInfo("cmake is already installed") + } + + return true +} + +// Build llama.cpp +func buildLlama() bool { + logSection("BUILDING LLAMA.CPP") + + llamaDir := filepath.Join(basePath, "llama.cpp") + llamaZip := filepath.Join(basePath, "dependencies/llama.cpp.zip") + + // Check if llama.cpp directory exists + if !fileExists(llamaDir) { + logInfo("Setting up llama.cpp...") + + if fileExists(llamaZip) { + logInfo("Using local llama.cpp archive...") + + // Extract llama.cpp archive + cmd := exec.Command("unzip", "-q", llamaZip, "-d", basePath) + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to extract local llama.cpp archive: %v, output: %s", err, string(output))) + return false + } + } else { + logError(fmt.Sprintf("Local llama.cpp archive not found at: %s", llamaZip)) + logError("Please ensure the llama.cpp.zip file exists in the dependencies directory") + return false + } + } else { + logInfo("Found existing llama.cpp directory") + } + + // Check if llama.cpp has been successfully compiled + llamaServer := filepath.Join(llamaDir, "build/bin/llama-server") + if fileExists(llamaServer) { + logInfo("Found existing llama-server build") + + // Check if executable file can be run and get version information + cmd := exec.Command(llamaServer, "--version") + if output, err := cmd.CombinedOutput(); err == nil { + outStr := string(output) + if strings.Contains(outStr, "version") { + logSuccess(fmt.Sprintf("Existing llama-server build is working properly (%s), skipping compilation", strings.TrimSpace(outStr))) + return true + } + } + logWarning("Existing build seems broken or incompatible, will recompile...") + } + + // Enter llama.cpp directory and build + oldDir, err := os.Getwd() + if err != nil { + logError(fmt.Sprintf("Failed to get current working directory: %v", err)) + return false + } + + if err := os.Chdir(llamaDir); err != nil { + logError(fmt.Sprintf("Failed to enter llama.cpp directory: %v", err)) + return false + } + + // Clean previous build + buildDir := filepath.Join(llamaDir, "build") + if fileExists(buildDir) { + logInfo("Cleaning previous build...") + if err := os.RemoveAll(buildDir); err != nil { + logError(fmt.Sprintf("Failed to clean previous build: %v", err)) + os.Chdir(oldDir) + return false + } + } + + // Create and enter build directory + logInfo("Creating build directory...") + if err := os.MkdirAll(buildDir, 0755); err != nil { + logError(fmt.Sprintf("Failed to create build directory: %v", err)) + os.Chdir(oldDir) + return false + } + + if err := os.Chdir(buildDir); err != nil { + logError(fmt.Sprintf("Failed to enter build directory: %v", err)) + os.Chdir(oldDir) + return false + } + + // Configure CMake + logInfo("Configuring CMake...") + cmd := exec.Command("cmake", "..") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("CMake configuration failed: %v, output: %s", err, string(output))) + os.Chdir(oldDir) + return false + } + + // Build project + logInfo("Building project...") + cmd = exec.Command("cmake", "--build", ".", "--config", "Release") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Build failed: %v, output: %s", err, string(output))) + os.Chdir(oldDir) + return false + } + + // Check build result + if !fileExists(filepath.Join(buildDir, "bin/llama-server")) { + logError("Build failed: llama-server executable not found") + logError("Expected at: bin/llama-server") + os.Chdir(oldDir) + return false + } + + logSuccess("Found llama-server at bin/llama-server") + os.Chdir(oldDir) + logSection("LLAMA.CPP BUILD COMPLETE") + return true +} + +// Build frontend +func buildFrontend() bool { + logSection("SETTING UP FRONTEND") + + frontendDir := filepath.Join(basePath, "lpm_frontend") + + // Enter frontend directory + oldDir, err := os.Getwd() + if err != nil { + logError(fmt.Sprintf("Failed to get current working directory: %v", err)) + return false + } + + if err := os.Chdir(frontendDir); err != nil { + logError(fmt.Sprintf("Failed to enter frontend directory: %s: %v", frontendDir, err)) + logError("Please ensure the directory exists and you have permission to access it.") + return false + } + + // Check if dependencies have been installed + nodeModules := filepath.Join(frontendDir, "node_modules") + packageLock := filepath.Join(frontendDir, "package-lock.json") + + if fileExists(nodeModules) { + logInfo("Found existing node_modules, checking for updates...") + + if fileExists(packageLock) { + logInfo("Using existing package-lock.json...") + // Run npm install even if package-lock.json exists to ensure dependencies are complete + logInfo("Running npm install to ensure dependencies are complete...") + + cmd := exec.Command("npm", "install") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install frontend dependencies with existing package-lock.json: %v, output: %s", err, string(output))) + logError("Try removing node_modules directory and package-lock.json, then run setup again") + os.Chdir(oldDir) + return false + } + } else { + logInfo("Installing dependencies...") + + cmd := exec.Command("npm", "install") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install frontend dependencies: %v, output: %s", err, string(output))) + logError("Check your npm configuration and network connection") + logError(fmt.Sprintf("You can try running 'npm install' manually in the %s directory", frontendDir)) + os.Chdir(oldDir) + return false + } + } + } else { + logInfo("Installing dependencies...") + + cmd := exec.Command("npm", "install") + if output, err := cmd.CombinedOutput(); err != nil { + logError(fmt.Sprintf("Failed to install frontend dependencies: %v, output: %s", err, string(output))) + logError("Check your npm configuration and network connection") + logError(fmt.Sprintf("You can try running 'npm install' manually in the %s directory", frontendDir)) + os.Chdir(oldDir) + return false + } + } + + // Verify installation was successful + if !fileExists(nodeModules) { + logError("node_modules directory not found after npm install") + logError("Frontend dependencies installation failed") + os.Chdir(oldDir) + return false + } + + logSuccess("Frontend dependencies installed successfully") + os.Chdir(oldDir) + logSection("FRONTEND SETUP COMPLETE") + return true +} diff --git a/seme/cmd/start.go b/seme/cmd/start.go new file mode 100644 index 00000000..51533944 --- /dev/null +++ b/seme/cmd/start.go @@ -0,0 +1,314 @@ +package cmd + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" +) + +var ( + backendOnly bool +) + +// startCmd represents the start command +var startCmd = &cobra.Command{ + Use: "start", + Short: "Start Second-Me services", + Long: `Start Second-Me backend and frontend services, with the option to start backend only.`, + Run: startServices, +} + +func init() { + RootCmd.AddCommand(startCmd) + startCmd.Flags().BoolVar(&backendOnly, "backend-only", false, "Start backend service only") +} + +// Check basic configuration and environment +func checkSetupComplete() bool { + logInfo("Checking if setup is complete...") + + // Check conda environment, simplified to check if environment directory exists + condaEnv := getEnvOrDefault("CONDA_DEFAULT_ENV", "second-me") + logInfo(fmt.Sprintf("Using conda environment: %s", bold(condaEnv))) + + // In a real project, should check if conda environment exists + // Here, simplified to check frontend dependencies directory + frontendNodeModules := filepath.Join(basePath, "lpm_frontend", "node_modules") + if !backendOnly && !fileExists(frontendNodeModules) { + logError("Frontend dependencies not installed. Please run 'make setup' first.") + return false + } + + logSuccess("Setup check passed " + checkEmoji) + return true +} + +// Check if file exists +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +// Check if ports are available +func checkPorts() bool { + logInfo("Checking port availability...") + + // Get port configuration + backendPort := getEnvOrDefault("LOCAL_APP_PORT", "8002") + frontendPort := getEnvOrDefault("LOCAL_FRONTEND_PORT", "3000") + + // Check backend port + if isPortInUse(backendPort) { + logError(fmt.Sprintf("Backend port %s is already in use!", bold(backendPort))) + return false + } + + // If not backend-only mode, check frontend port + if !backendOnly && isPortInUse(frontendPort) { + logError(fmt.Sprintf("Frontend port %s is already in use!", bold(frontendPort))) + return false + } + + logSuccess("All ports are available " + checkEmoji) + return true +} + +// Check backend health +func checkBackendHealth() bool { + port := getEnvOrDefault("LOCAL_APP_PORT", "8002") + url := fmt.Sprintf("http://127.0.0.1:%s/health", port) + + client := http.Client{ + Timeout: 3 * time.Second, + } + + if verbose { + logInfo(fmt.Sprintf("Checking backend health at %s", url)) + } + + resp, err := client.Get(url) + if err != nil { + if verbose { + logInfo(fmt.Sprintf("Health check failed: %v", err)) + } + return false + } + defer resp.Body.Close() + + if verbose { + logInfo(fmt.Sprintf("Health check response: %d", resp.StatusCode)) + } + + return resp.StatusCode == http.StatusOK +} + +// Check if frontend is ready +func checkFrontendReady() bool { + // Try to determine from frontend log if ready + // In a real project, could verify by checking port or HTTP request + frontendLogFile := filepath.Join(basePath, LogsDir, "frontend.log") + + // Check if log file exists + if !fileExists(frontendLogFile) { + return false + } + + // Look for "Local:" marker, indicating frontend service is started + output, err := runCommand(fmt.Sprintf("grep -q 'Local:' %s", frontendLogFile)) + if err != nil { + return false + } + + return output != "" +} + +// Start backend service +func startBackendService() bool { + logInfo(startEmoji + " Starting backend service...") + + // Ensure directories exist + ensureDir(filepath.Join(basePath, LogsDir)) + ensureDir(filepath.Join(basePath, RunDir)) + + // Build start command + startLocalScript := filepath.Join(basePath, ScriptsDir, "start_local.sh") + logFile := filepath.Join(basePath, LogsDir, "start.log") + pidFile := getPidFilePath("backend") + + // Check if backend service is already running + if fileExists(pidFile) { + pid, err := readPidFile(pidFile) + if err == nil && isProcessRunning(pid) { + logWarning(fmt.Sprintf("Backend service is already running with PID: %s", bold(fmt.Sprintf("%d", pid)))) + + // Check if it's responding to health checks + if checkBackendHealth() { + logSuccess("Backend service is already running and responding " + checkEmoji) + return true + } else { + logWarning("Backend service is running but not responding to health checks, will restart it") + killProcess(pid, true) + os.Remove(pidFile) + } + } else { + // PID file exists but process is not running, remove stale PID file + os.Remove(pidFile) + } + } + + // Get conda environment + condaEnv := getEnvOrDefault("CONDA_DEFAULT_ENV", "second-me") + + // Start backend service with conda environment activated + // First try to find conda.sh for proper activation + condaShPath := "/opt/homebrew/Caskroom/miniconda/base/etc/profile.d/conda.sh" + if !fileExists(condaShPath) { + // Try alternative paths + possiblePaths := []string{ + "/opt/homebrew/Caskroom/miniconda/base/etc/profile.d/conda.sh", + "/opt/miniconda3/etc/profile.d/conda.sh", + "/opt/anaconda3/etc/profile.d/conda.sh", + "/Users/$(whoami)/miniconda3/etc/profile.d/conda.sh", + "/Users/$(whoami)/anaconda3/etc/profile.d/conda.sh", + } + + for _, path := range possiblePaths { + expandedPath, err := runCommand(fmt.Sprintf("echo %s", path)) + if err == nil && fileExists(strings.TrimSpace(expandedPath)) { + condaShPath = strings.TrimSpace(expandedPath) + break + } + } + } + + // Build command with conda activation + cmdString := "" + if fileExists(condaShPath) { + cmdString = fmt.Sprintf("source %s && conda activate %s && %s", condaShPath, condaEnv, startLocalScript) + } else { + // Fallback without conda.sh + cmdString = fmt.Sprintf("conda activate %s && %s", condaEnv, startLocalScript) + } + + // Start the process + err := startBackgroundProcess(cmdString, logFile, pidFile) + if err != nil { + logError(fmt.Sprintf("Failed to start backend service: %v", err)) + return false + } + + pid, _ := readPidFile(pidFile) + logInfo(fmt.Sprintf("Backend service started in background with PID: %s", bold(fmt.Sprintf("%d", pid)))) + + // Wait for backend service to be ready + verbose = true + if !waitForService(checkBackendHealth, 60, "backend service") { + // Check backend log for errors when health check fails + backendLog := filepath.Join(basePath, LogsDir, "backend.log") + if fileExists(backendLog) { + // Get last 10 lines of log + logOutput, err := runCommand(fmt.Sprintf("tail -n 20 %s", backendLog)) + if err == nil && logOutput != "" { + logWarning("Last 20 lines of backend log:") + fmt.Println(yellow(logOutput)) + } + } + + logError("Backend service failed to become ready within 60 seconds") + logInfo("You can check the backend logs for more information:") + fmt.Printf(" %s %s\n", magenta("▶"), cyan(fmt.Sprintf("cat %s/logs/backend.log", basePath))) + fmt.Printf(" %s %s\n", magenta("▶"), cyan(fmt.Sprintf("cat %s/logs/start.log", basePath))) + return false + } + verbose = false + + logSuccess("Backend service is ready " + checkEmoji) + return true +} + +// Start frontend service +func startFrontendService() bool { + frontendDir := filepath.Join(basePath, "lpm_frontend") + + // Check if frontend directory exists + if !fileExists(frontendDir) { + logError("Frontend directory 'lpm_frontend' not found!") + return false + } + + logInfo(frontendEmoji + " Starting frontend service...") + + // Check and install dependencies + nodeModulesDir := filepath.Join(frontendDir, "node_modules") + if !fileExists(nodeModulesDir) { + logInfo("Installing frontend dependencies...") + cmd := fmt.Sprintf("cd %s && npm install", frontendDir) + if err := runCommandWithOutput(cmd); err != nil { + logError(fmt.Sprintf("Failed to install frontend dependencies: %v", err)) + return false + } + logSuccess("Frontend dependencies installed " + checkEmoji) + } + + // Start frontend service + logInfo("Starting frontend dev server...") + command := fmt.Sprintf("cd %s && npm run dev", frontendDir) + logFile := filepath.Join(basePath, LogsDir, "frontend.log") + pidFile := getPidFilePath("frontend") + + if err := startBackgroundProcess(command, logFile, pidFile); err != nil { + logError(fmt.Sprintf("Failed to start frontend service: %v", err)) + return false + } + + pid, _ := readPidFile(pidFile) + logInfo(fmt.Sprintf("Frontend service started in background with PID: %s", bold(fmt.Sprintf("%d", pid)))) + + // Wait for frontend service to be ready + if !waitForService(checkFrontendReady, 120, "frontend service") { + logError("Frontend service failed to become ready within 120 seconds") + return false + } + + logSuccess("Frontend service is ready " + checkEmoji) + + // Display frontend access URL + port := getEnvOrDefault("LOCAL_FRONTEND_PORT", "3000") + fmt.Printf("\n %s %s\n\n", frontendEmoji, cyan(underline(fmt.Sprintf("Frontend service can be accessed at http://localhost:%s", port)))) + + return true +} + +// startServices is the handler for the start command +func startServices(cmd *cobra.Command, args []string) { + logSection(startEmoji + " STARTING SERVICES") + + // Check if setup is complete + if !checkSetupComplete() { + os.Exit(1) + } + + // Check if ports are available + if !checkPorts() { + os.Exit(1) + } + + // Start backend service + if !startBackendService() { + os.Exit(1) + } + + // If not backend-only mode, start frontend service + if !backendOnly { + if !startFrontendService() { + os.Exit(1) + } + } + + logSuccess("All services started successfully " + successEmoji) +} diff --git a/seme/cmd/status.go b/seme/cmd/status.go new file mode 100644 index 00000000..54b1d486 --- /dev/null +++ b/seme/cmd/status.go @@ -0,0 +1,163 @@ +package cmd + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +// statusCmd represents the status command +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Check Second-Me services status", + Long: `Check the running status of Second-Me services, including frontend, backend, and related services.`, + Run: checkStatus, +} + +func init() { + RootCmd.AddCommand(statusCmd) +} + +// Check process status +func checkProcessStatus(pidFile string, name string) bool { + fmt.Printf(" %s: ", bold(name)) + + if !fileExists(pidFile) { + fmt.Printf("%s (No PID file)\n", red("Not running ❌")) + return false + } + + pid, err := readPidFile(pidFile) + if err != nil { + fmt.Printf("%s (Invalid PID file: %v)\n", red("Not running ❌"), err) + return false + } + + if !isProcessRunning(pid) { + fmt.Printf("%s (PID file exists but process is dead: %s)\n", red("Not running ❌"), bold(fmt.Sprintf("%d", pid))) + return false + } + + // Get process information + cmd := fmt.Sprintf("ps -p %d -o comm= 2>/dev/null", pid) + processName, _ := runCommand(cmd) + + cmd = fmt.Sprintf("ps -p %d -o args= 2>/dev/null", pid) + processCmd, _ := runCommand(cmd) + + fmt.Printf("%s (PID: %s, Process: %s)\n", green("Running ✅"), bold(fmt.Sprintf("%d", pid)), bold(processName)) + fmt.Printf(" %s %s\n", magenta("▶"), processCmd) + + return true +} + +// Check port status +func checkPortStatus(port string, description string) bool { + fmt.Printf(" %s: ", bold(fmt.Sprintf("Port %s", port))) + + // Check if port is in use + cmd := fmt.Sprintf("lsof -ti:%s 2>/dev/null", port) + output, err := runCommand(cmd) + if err != nil || output == "" { + fmt.Printf("%s\n", red("Not in use ❌")) + return false + } + + // Process may have multiple PIDs - split by newline and take the first one + pidStr := strings.Split(strings.TrimSpace(output), "\n")[0] + + // Parse PID + pid, err := strconv.Atoi(pidStr) + if err != nil { + fmt.Printf("%s (Cannot parse PID: %v)\n", red("Error ⚠️"), err) + return false + } + + // Get process information + cmd = fmt.Sprintf("ps -p %d -o comm= 2>/dev/null", pid) + processName, _ := runCommand(cmd) + + cmd = fmt.Sprintf("ps -p %d -o args= 2>/dev/null", pid) + processCmd, _ := runCommand(cmd) + + // If multiple PIDs, show a note + countPIDs := len(strings.Split(strings.TrimSpace(output), "\n")) + if countPIDs > 1 { + fmt.Printf("%s (PID: %s, Process: %s, +%d more)\n", + green("In use ✅"), + bold(fmt.Sprintf("%d", pid)), + bold(processName), + countPIDs-1) + } else { + fmt.Printf("%s (PID: %s, Process: %s)\n", + green("In use ✅"), + bold(fmt.Sprintf("%d", pid)), + bold(processName)) + } + + fmt.Printf(" %s %s\n", magenta("▶"), processCmd) + + return true +} + +// Create status summary +func printStatusSummary(backendRunning, frontendRunning, port8080Running bool) { + fmt.Printf("\n%s\n", bold("Summary:")) + + if backendRunning { + fmt.Printf(" %s Backend: %s\n", "🖥️ ", green("Running ✅")) + } else { + fmt.Printf(" %s Backend: %s\n", "🖥️ ", red("Not running ❌")) + } + + if frontendRunning { + fmt.Printf(" %s Frontend: %s\n", "🌐", green("Running ✅")) + } else { + fmt.Printf(" %s Frontend: %s\n", "🌐", red("Not running ❌")) + } + + if port8080Running { + fmt.Printf(" %s LLM Server: %s\n", "🦙", green("Running ✅")) + } else { + fmt.Printf(" %s LLM Server: %s\n", "🦙", red("Not running ❌")) + } + + // Add a note about how to start services if they're not running + if !backendRunning || !frontendRunning || !port8080Running { + fmt.Printf("\n%s To start services, run: %s\n", infoEmoji, cyan("seme start")) + } +} + +// checkStatus is the handler for the status command +func checkStatus(cmd *cobra.Command, args []string) { + logSection(statusEmoji + " SERVICE STATUS") + + // Get port configuration + backendPort := getEnvOrDefault("LOCAL_APP_PORT", "8002") + frontendPort := getEnvOrDefault("LOCAL_FRONTEND_PORT", "3000") + + // Check backend service status + fmt.Printf("%s\n", bold("Backend Service:")) + backendPidFile := getPidFilePath("backend") + backendPidStatus := checkProcessStatus(backendPidFile, "PID File") + + backendPortStatus := checkPortStatus(backendPort, "Backend Port") + + // Check frontend service status + fmt.Printf("\n%s\n", bold("Frontend Service:")) + frontendPidFile := getPidFilePath("frontend") + frontendPidStatus := checkProcessStatus(frontendPidFile, "PID File") + + frontendPortStatus := checkPortStatus(frontendPort, "Frontend Port") + + // Check port 8080 status (commonly used for llama-server) + fmt.Printf("\n%s\n", bold("LLM Server Status:")) + port8080Status := checkPortStatus("8080", "LLaMA Port") + + // Print status summary + backendRunning := backendPidStatus || backendPortStatus + frontendRunning := frontendPidStatus || frontendPortStatus + printStatusSummary(backendRunning, frontendRunning, port8080Status) +} diff --git a/seme/cmd/stop.go b/seme/cmd/stop.go new file mode 100644 index 00000000..1da4bb77 --- /dev/null +++ b/seme/cmd/stop.go @@ -0,0 +1,190 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" +) + +// stopCmd represents the stop command +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "Stop Second-Me services", + Long: `Stop all running Second-Me services, including backend, frontend, and related processes.`, + Run: stopServices, +} + +func init() { + RootCmd.AddCommand(stopCmd) +} + +// Stop a specific process +func stopProcess(pidFile string, description string) { + if fileExists(pidFile) { + pid, err := readPidFile(pidFile) + if err != nil { + logWarning(fmt.Sprintf("Failed to read PID file: %v", err)) + return + } + + if isProcessRunning(pid) { + logInfo(fmt.Sprintf("Stopping %s process (PID: %s)...", bold(description), bold(fmt.Sprintf("%d", pid)))) + if err := killProcess(pid, false); err != nil { + // 检查错误信息,判断进程是否已经结束 + if strings.Contains(err.Error(), "process already finished") { + logInfo(fmt.Sprintf("Process %d was already finished", pid)) + } else { + logWarning(fmt.Sprintf("Process still running, trying force termination...")) + if err := killProcess(pid, true); err != nil { + if strings.Contains(err.Error(), "process already finished") { + logInfo(fmt.Sprintf("Process %d was already finished", pid)) + } else { + logError(fmt.Sprintf("Cannot terminate process: %v", err)) + } + } + } + } + logSuccess(fmt.Sprintf("%s process stopped (PID: %s) %s", bold(description), bold(fmt.Sprintf("%d", pid)), stopEmoji)) + } else { + logInfo(fmt.Sprintf("%s process not running", description)) + } + + // Remove PID file + if err := os.Remove(pidFile); err != nil { + logWarning(fmt.Sprintf("Cannot remove PID file: %v", err)) + } + } +} + +// Check and close process on a specific port +func stopProcessOnPort(port string, description string) { + logInfo(fmt.Sprintf("Checking process on port %s...", bold(port))) + + // Use lsof to find process using the port + cmd := fmt.Sprintf("lsof -ti:%s", port) + output, err := runCommand(cmd) + if err != nil || output == "" { + logInfo(fmt.Sprintf("No process running on port %s", bold(port))) + return + } + + // 修改: 处理可能有多个PID的情况 + pidStrings := strings.Split(strings.TrimSpace(output), "\n") + logInfo(fmt.Sprintf("Found %d processes on port %s", len(pidStrings), bold(port))) + + for _, pidStr := range pidStrings { + // 去除每个PID字符串的空白 + pidStr = strings.TrimSpace(pidStr) + if pidStr == "" { + continue + } + + // 解析PID + pid, err := strconv.Atoi(pidStr) + if err != nil { + logWarning(fmt.Sprintf("Cannot parse process ID: %v, value: %s", err, pidStr)) + continue + } + + logInfo(fmt.Sprintf("Stopping process using port %s (PID: %s)...", bold(port), bold(fmt.Sprintf("%d", pid)))) + if err := killProcess(pid, true); err != nil { + logError(fmt.Sprintf("Cannot terminate process: %v", err)) + } else { + logSuccess(fmt.Sprintf("Process on port %s (PID: %s) terminated %s", bold(port), bold(fmt.Sprintf("%d", pid)), stopEmoji)) + } + } + + // 检查是否所有进程都已终止 + time.Sleep(500 * time.Millisecond) // 给进程一些时间终止 + + remainingOutput, _ := runCommand(cmd) + if remainingOutput != "" { + pidCount := len(strings.Split(strings.TrimSpace(remainingOutput), "\n")) + logWarning(fmt.Sprintf("Still %d processes running on port %s. You might need to manually kill them.", pidCount, bold(port))) + } else { + logSuccess(fmt.Sprintf("All processes on port %s stopped successfully %s", bold(port), stopEmoji)) + } +} + +// Stop processes matching a specific pattern +func stopProcessesByPattern(pattern string, description string) { + logInfo(fmt.Sprintf("Checking %s processes...", bold(description))) + + pids, err := findProcessByPattern(pattern) + if err != nil { + logWarning(fmt.Sprintf("Failed to find processes: %v", err)) + return + } + + if len(pids) == 0 { + logInfo(fmt.Sprintf("No %s processes found", description)) + return + } + + for _, pid := range pids { + logInfo(fmt.Sprintf("Stopping %s process (PID: %s)...", bold(description), bold(fmt.Sprintf("%d", pid)))) + if err := killProcess(pid, false); err != nil { + // 检查错误信息,判断进程是否已经结束 + if strings.Contains(err.Error(), "process already finished") { + logInfo(fmt.Sprintf("Process %d was already finished", pid)) + } else { + logWarning(fmt.Sprintf("Process still running, trying force termination...")) + if err := killProcess(pid, true); err != nil { + if strings.Contains(err.Error(), "process already finished") { + logInfo(fmt.Sprintf("Process %d was already finished", pid)) + } else { + logError(fmt.Sprintf("Cannot terminate process: %v", err)) + } + } + } + } + time.Sleep(500 * time.Millisecond) + } + + logSuccess(fmt.Sprintf("%s processes stopped %s", bold(description), stopEmoji)) +} + +// stopServices is the handler for the stop command +func stopServices(cmd *cobra.Command, args []string) { + logSection(stopEmoji + " STOPPING SERVICES") + + // Get port configuration + backendPort := getEnvOrDefault("LOCAL_APP_PORT", "8002") + frontendPort := getEnvOrDefault("LOCAL_FRONTEND_PORT", "3000") + + // Create run directory if it doesn't exist + ensureDir(filepath.Join(basePath, RunDir)) + + // Stop backend service + backendPidFile := getPidFilePath("backend") + stopProcess(backendPidFile, "backend") + + // Check backend port + stopProcessOnPort(backendPort, "backend") + + // Stop llama-server processes + stopProcessesByPattern("llama-server", "llama-server") + + // Check port 8080 (commonly used for llama-server) + stopProcessOnPort("8080", "llama-server") + + // Stop frontend service + frontendPidFile := getPidFilePath("frontend") + stopProcess(frontendPidFile, "frontend") + + // Stop all Next.js related processes + stopProcessesByPattern("next dev|next-server", "Next.js") + + // Stop all npm run dev related processes + stopProcessesByPattern("npm run dev", "npm") + + // Check frontend port + stopProcessOnPort(frontendPort, "frontend") + + logSuccess("All services stopped successfully " + successEmoji) +} diff --git a/seme/cmd/utils.go b/seme/cmd/utils.go new file mode 100644 index 00000000..9c64d329 --- /dev/null +++ b/seme/cmd/utils.go @@ -0,0 +1,242 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" +) + +// Common project paths and files +const ( + RunDir = "run" + LogsDir = "logs" + ScriptsDir = "scripts" +) + +// Get PID file path +func getPidFilePath(name string) string { + return filepath.Join(basePath, RunDir, fmt.Sprintf(".%s.pid", name)) +} + +// Read PID file +func readPidFile(pidFile string) (int, error) { + data, err := os.ReadFile(pidFile) + if err != nil { + return 0, err + } + + pidStr := strings.TrimSpace(string(data)) + pid, err := strconv.Atoi(pidStr) + if err != nil { + return 0, fmt.Errorf("PID file contains invalid PID: %s", pidStr) + } + + return pid, nil +} + +// Write PID file +func writePidFile(pidFile string, pid int) error { + // Ensure directory exists + dir := filepath.Dir(pidFile) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + return os.WriteFile(pidFile, []byte(strconv.Itoa(pid)), 0644) +} + +// Check if process exists +func isProcessRunning(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + + // On Unix systems, FindProcess always succeeds, need to send signal 0 to check if process exists + err = process.Signal(syscall.Signal(0)) + return err == nil +} + +// Terminate process +func killProcess(pid int, force bool) error { + process, err := os.FindProcess(pid) + if err != nil { + return err + } + + // 在发送终止信号前,先检查进程是否存在 + if !isProcessRunning(pid) { + return fmt.Errorf("process already finished") + } + + var sig syscall.Signal + if force { + sig = syscall.SIGKILL + } else { + sig = syscall.SIGTERM + } + + return process.Signal(sig) +} + +// Start background process +func startBackgroundProcess(command string, logFile string, pidFile string) error { + // Check if log file can be created + logDir := filepath.Dir(logFile) + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("cannot create log directory %s: %v", logDir, err) + } + + // Check if PID directory can be created + pidDir := filepath.Dir(pidFile) + if err := os.MkdirAll(pidDir, 0755); err != nil { + return fmt.Errorf("cannot create PID directory %s: %v", pidDir, err) + } + + // Open log file + file, err := os.Create(logFile) + if err != nil { + return fmt.Errorf("cannot create log file: %v", err) + } + defer file.Close() + + // Prepare command - use zsh instead of sh for better conda compatibility + cmd := exec.Command("/bin/zsh", "-c", command) + cmd.Stdout = file + cmd.Stderr = file + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, // Put the process in a new process group + } + + // Start process + if err := cmd.Start(); err != nil { + return fmt.Errorf("cannot start process: %v", err) + } + + // Write PID file + if err := writePidFile(pidFile, cmd.Process.Pid); err != nil { + // If cannot write PID, terminate process + cmd.Process.Kill() + return fmt.Errorf("cannot write PID file: %v", err) + } + + return nil +} + +// Run command and return output +func runCommand(command string) (string, error) { + cmd := exec.Command("/bin/zsh", "-c", command) + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("command execution failed: %v, output: %s", err, string(output)) + } + return string(output), nil +} + +// Run command in foreground, streaming output to console +func runCommandWithOutput(command string) error { + cmd := exec.Command("/bin/zsh", "-c", command) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("cannot get command stdout: %v", err) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return fmt.Errorf("cannot get command stderr: %v", err) + } + + if err := cmd.Start(); err != nil { + return fmt.Errorf("cannot start command: %v", err) + } + + // Handle stdout + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + }() + + // Handle stderr + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + fmt.Fprintln(os.Stderr, scanner.Text()) + } + }() + + return cmd.Wait() +} + +// Find process IDs matching pattern +func findProcessByPattern(pattern string) ([]int, error) { + cmd := exec.Command("/bin/zsh", "-c", fmt.Sprintf("pgrep -f '%s'", pattern)) + output, err := cmd.Output() + if err != nil { + // Check if non-zero exit code is due to no matching processes + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return []int{}, nil + } + return nil, err + } + + var pids []int + for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") { + if line == "" { + continue + } + pid, err := strconv.Atoi(line) + if err != nil { + return nil, fmt.Errorf("cannot parse PID: %v", err) + } + pids = append(pids, pid) + } + + return pids, nil +} + +// Check if directory exists, create if not +func ensureDir(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return os.MkdirAll(dir, 0755) + } + return nil +} + +// Wait for service health check +func waitForService(checkFunc func() bool, maxAttempts int, description string) bool { + logInfo(fmt.Sprintf("Waiting for %s to be ready...", description)) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + if checkFunc() { + return true + } + + // Always log every 5 attempts regardless of verbose setting + if verbose || attempt%5 == 0 { + logInfo(fmt.Sprintf("Attempt %d/%d: %s not ready, waiting...", attempt, maxAttempts, description)) + } + + // Gradually increase wait time between attempts (up to 3 seconds) + waitTime := time.Duration(min(3, 1+(attempt/10))) * time.Second + time.Sleep(waitTime) + } + + return false +} + +// Helper function for min since Go < 1.21 doesn't have math.Min for integers +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/seme/go.mod b/seme/go.mod new file mode 100644 index 00000000..a65d7ebc --- /dev/null +++ b/seme/go.mod @@ -0,0 +1,14 @@ +module github.com/bitliu/Second-Me/seme + +go 1.24.1 + +require ( + github.com/fatih/color v1.18.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + golang.org/x/sys v0.31.0 // indirect +) diff --git a/seme/go.sum b/seme/go.sum new file mode 100644 index 00000000..03228cfa --- /dev/null +++ b/seme/go.sum @@ -0,0 +1,21 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seme/main.go b/seme/main.go new file mode 100644 index 00000000..f72bd544 --- /dev/null +++ b/seme/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + + "github.com/bitliu/Second-Me/seme/cmd" +) + +// main is the entry point for the application +func main() { + if err := cmd.RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/seme/seme b/seme/seme new file mode 100755 index 00000000..0edf11a0 Binary files /dev/null and b/seme/seme differ