11package main
22
33import (
4+ "context"
45 "encoding/json"
56 "flag"
67 "fmt"
@@ -10,12 +11,16 @@ import (
1011 "os/exec"
1112 "path/filepath"
1213 "runtime"
14+ "strconv"
1315 "strings"
16+ "syscall"
1417 "time"
1518
19+ "github.com/codeclysm/extract/v3"
1620 "github.com/doITmagic/rag-code-mcp/internal/config"
1721 "github.com/doITmagic/rag-code-mcp/internal/healthcheck"
1822 "github.com/doITmagic/rag-code-mcp/internal/uninstall"
23+ "github.com/doITmagic/rag-code-mcp/internal/updater"
1924)
2025
2126var (
@@ -70,24 +75,41 @@ func main() {
7075 fail (fmt .Sprintf ("Failed to create directories: %v" , err ))
7176 }
7277
73- // 2. Determine current location
78+ // 2. Determine current location strictly
7479 execPath , _ := os .Executable ()
7580 sourceDir := filepath .Dir (execPath )
76- resourceDir := sourceDir
7781
78- // If we are running from bin/, resources might be in the parent dir
79- if _ , err := os .Stat (filepath .Join (resourceDir , "README.md" )); os .IsNotExist (err ) {
80- parentDir := filepath .Dir (resourceDir )
81- if _ , err := os .Stat (filepath .Join (parentDir , "README.md" )); err == nil {
82- resourceDir = parentDir
83- }
84- }
85-
86- // 3. Define files to install
8782 binaries := []string {"rag-code-mcp" , "rag-code-install" }
8883 resources := []string {"README.md" , "llms.txt" , "LICENSE" , "config.yaml" }
8984
90- log ("Copying files from: " + sourceDir )
85+ // Fast Check: Are files present?
86+ missingFiles := false
87+ for _ , f := range append (binaries , resources ... ) {
88+ name := f
89+ if runtime .GOOS == "windows" && (f == "rag-code-mcp" || f == "rag-code-install" ) {
90+ name += ".exe"
91+ }
92+ if _ , err := os .Stat (filepath .Join (sourceDir , name )); os .IsNotExist (err ) {
93+ missingFiles = true
94+ break
95+ }
96+ }
97+
98+ // If files are missing, we must download the latest release and use its contents as source
99+ tempDir := ""
100+ if missingFiles {
101+ log ("Required files missing in current directory. Downloading latest release..." )
102+ var err error
103+ tempDir , err = downloadAndExtractLatest ()
104+ if err != nil {
105+ fail (fmt .Sprintf ("Failed to download and extract release: %v" , err ))
106+ }
107+ defer os .RemoveAll (tempDir )
108+ sourceDir = tempDir
109+ log ("Files extracted successfully from release." )
110+ } else {
111+ log ("Copying files from: " + sourceDir )
112+ }
91113
92114 // Copy Binaries to bin/
93115 for _ , b := range binaries {
@@ -110,7 +132,7 @@ func main() {
110132
111133 // Copy Resources to bin/
112134 for _ , r := range resources {
113- src := filepath .Join (resourceDir , r )
135+ src := filepath .Join (sourceDir , r )
114136 dst := filepath .Join (binPath , r )
115137
116138 if r == "config.yaml" {
@@ -373,29 +395,67 @@ func stopRunningProcess(binPath string) {
373395 return
374396 }
375397
376- log ("Stopping existing process running: " + binPath )
398+ log ("Stopping existing process gracefully: " + binPath )
399+
400+ // Attempt Graceful Shutdown using PID file
401+ home , err := os .UserHomeDir ()
402+ if err == nil {
403+ pidPath := filepath .Join (home , ".ragcode" , "daemon.pid" )
404+ if data , err := os .ReadFile (pidPath ); err == nil {
405+ pidStr := strings .TrimSpace (string (data ))
406+ if pid , err := strconv .Atoi (pidStr ); err == nil {
407+ log (fmt .Sprintf ("Found daemon PID: %d. Sending termination signal..." , pid ))
408+
409+ // For Windows
410+ if runtime .GOOS == "windows" {
411+ _ = exec .Command ("taskkill" , "/PID" , pidStr ).Run ()
412+ time .Sleep (2 * time .Second )
413+ _ = exec .Command ("taskkill" , "/F" , "/PID" , pidStr ).Run ()
414+ return
415+ }
416+
417+ // For Unix
418+ process , err := os .FindProcess (pid )
419+ if err == nil {
420+ // Send SIGTERM
421+ _ = process .Signal (syscall .SIGTERM )
422+
423+ // Wait up to 5 seconds for it to exit gracefully
424+ for i := 0 ; i < 50 ; i ++ {
425+ if err := process .Signal (syscall .Signal (0 )); err != nil {
426+ // Process is gone
427+ break
428+ }
429+ time .Sleep (100 * time .Millisecond )
430+ }
431+
432+ // After grace period, only SIGKILL if process still appears alive
433+ if err := process .Signal (syscall .Signal (0 )); err == nil {
434+ _ = process .Signal (syscall .SIGKILL )
435+ time .Sleep (200 * time .Millisecond )
436+ }
437+ return
438+ }
439+ }
440+ }
441+ }
442+
443+ log ("PID file not found or process untrackable. Using fallback termination..." )
377444
378445 if runtime .GOOS == "windows" {
446+ _ = exec .Command ("taskkill" , "/IM" , filepath .Base (binPath )).Run ()
447+ time .Sleep (1 * time .Second )
379448 _ = exec .Command ("taskkill" , "/F" , "/IM" , filepath .Base (binPath )).Run ()
380- time .Sleep (500 * time .Millisecond )
381449 return
382450 }
383451
384- // 1. Precise kill using full path
385- _ = exec .Command ("pkill" , "-f" , binPath ).Run ()
452+ // 1. Soft kill (SIGTERM)
453+ _ = exec .Command ("pkill" , "-15" , "-f" , binPath ).Run ()
454+ time .Sleep (1 * time .Second )
386455
387- // 2. Fallback using lsof to find PIDs mapping this binary
388- if _ , err := exec .LookPath ("lsof" ); err == nil {
389- cmd := exec .Command ("lsof" , "-t" , binPath )
390- if output , err := cmd .Output (); err == nil {
391- pids := strings .Fields (string (output ))
392- for _ , pid := range pids {
393- _ = exec .Command ("kill" , "-9" , pid ).Run ()
394- }
395- }
396- }
456+ // 2. Hard kill (SIGKILL) fallback
457+ _ = exec .Command ("pkill" , "-9" , "-f" , binPath ).Run ()
397458
398- // Give it a moment to release file handles
399459 time .Sleep (500 * time .Millisecond )
400460}
401461
@@ -810,6 +870,56 @@ func updateZedConfig(displayName, path, binPath, transport string, ssePort int)
810870 return false
811871}
812872
873+ func downloadAndExtractLatest () (string , error ) {
874+ // Pass "v0.0.0" to force a network check to get the absolute latest if we're a naked installer
875+ ctx := context .Background ()
876+ info , err := updater .CheckForUpdates (ctx , "v0.0.0" , true )
877+ if err != nil {
878+ return "" , fmt .Errorf ("failed to check for updates: %w" , err )
879+ }
880+ if info == nil || info .AssetURL == "" {
881+ return "" , fmt .Errorf ("no update asset found" )
882+ }
883+
884+ tempDir , err := os .MkdirTemp ("" , "ragcode-install-*" )
885+ if err != nil {
886+ return "" , err
887+ }
888+ cleanup := true
889+ defer func () {
890+ if cleanup {
891+ os .RemoveAll (tempDir )
892+ }
893+ }()
894+
895+ archivePath := filepath .Join (tempDir , "release-archive" )
896+ log ("Downloading " + info .AssetURL )
897+
898+ // Use DownloadAndVerify for checksum-verified downloads (supply-chain security)
899+ if err := info .DownloadAndVerify (ctx , archivePath ); err != nil {
900+ return "" , fmt .Errorf ("failed to download and verify release: %w" , err )
901+ }
902+
903+ log ("Extracting archive..." )
904+ f , err := os .Open (archivePath )
905+ if err != nil {
906+ return "" , err
907+ }
908+ defer f .Close ()
909+
910+ extractDir := filepath .Join (tempDir , "extracted" )
911+ if err := os .MkdirAll (extractDir , 0755 ); err != nil {
912+ return "" , err
913+ }
914+
915+ if err := extract .Archive (ctx , f , extractDir , nil ); err != nil {
916+ return "" , err
917+ }
918+
919+ cleanup = false // success — caller is responsible for cleanup
920+ return extractDir , nil
921+ }
922+
813923// buildMCPServerEntry builds the stdio (binary) MCP server entry.
814924func buildMCPServerEntry (ideKey , binPath string ) map [string ]interface {} {
815925 entry := map [string ]interface {}{
0 commit comments