44 "context"
55 "fmt"
66 "io"
7+ "os"
8+ "time"
79
810 "github.com/CryptoLabInc/rune-cli/internal/bootstrap"
911)
@@ -15,5 +17,59 @@ func runMCPServer(ctx context.Context, args []string, stderr io.Writer) int {
1517 return 1
1618 }
1719
20+ // Fresh `claude plugin install rune` try to spawn MCP server via
21+ // "${CLAUDE_PLUGIN_ROOT}/bin/rune mcp-server" which does not exist yet.
22+ // Self-install rune-mcp itself in this case.
23+ if _ , statErr := os .Stat (paths .RuneMCPBinary ); statErr != nil {
24+ fmt .Fprintln (stderr , "rune: rune-mcp not installed yet; fetching before launch..." )
25+ manifest := manifestURL
26+ if env := os .Getenv ("RUNE_MANIFEST" ); env != "" {
27+ manifest = env
28+ }
29+
30+ // Concurrency-safe install
31+ _ , instErr := bootstrap .Install (ctx , bootstrap.InstallOptions {
32+ ManifestURL : manifest ,
33+ Target : []string {bootstrap .StepRuneMCP },
34+ Log : func (format string , a ... any ) {
35+ fmt .Fprintf (stderr , format + "\n " , a ... )
36+ },
37+ })
38+ if instErr != nil {
39+ // Sessions which do not invoke install wait here
40+ if ! waitForFile (ctx , paths .RuneMCPBinary , bootstrap .InstallLockTimeout ) {
41+ fmt .Fprintf (stderr , "rune: failed to install rune-mcp: %v\n " , instErr )
42+ return 1
43+ }
44+
45+ fmt .Fprintln (stderr , "rune: rune-mcp installed by a concurrent session" )
46+ }
47+ }
48+
1849 return execInstalledBinary (ctx , paths .RuneBin , "rune-mcp" , args , nil , stderr )
1950}
51+
52+ func waitForFile (ctx context.Context , path string , timeout time.Duration ) bool {
53+ if _ , err := os .Stat (path ); err == nil {
54+ return true
55+ }
56+
57+ deadline := time .NewTimer (timeout )
58+ defer deadline .Stop ()
59+
60+ tick := time .NewTicker (300 * time .Millisecond )
61+ defer tick .Stop ()
62+
63+ for {
64+ select {
65+ case <- ctx .Done ():
66+ return false
67+ case <- deadline .C :
68+ return false
69+ case <- tick .C :
70+ if _ , err := os .Stat (path ); err == nil {
71+ return true
72+ }
73+ }
74+ }
75+ }
0 commit comments