diff --git a/README.md b/README.md index 224d704..766b89d 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ Or manually by adding this repo's `skills/` directory to your Agent skill search # Evaluate code (daemon starts automatically) julia-client -e 'println("hello")' -# Pkg operations (disable timeout) -julia-client --timeout 0 -e 'using Pkg; Pkg.add("Example")' - # Explicit project environment julia-client --project /path/to/project -e 'using MyPackage' @@ -42,7 +39,6 @@ echo 'println("hello")' | julia-client # Session management julia-client sessions # list active sessions -julia-client restart # restart current session julia-client stop # shut down the daemon ``` @@ -56,4 +52,4 @@ A single `julia-client` binary serves as both client and daemon: ## Alternatives - [julia-mcp](https://github.com/aplavin/julia-mcp?tab=readme-ov-file) is very similar but uses MCP server instead -- [DaemonicCabal.jl](https://github.com/tecosaur/DaemonicCabal.jl) only runs on Linux \ No newline at end of file +- [DaemonicCabal.jl](https://github.com/tecosaur/DaemonicCabal.jl) only runs on Linux diff --git a/go/client_test.go b/go/client_test.go index 5fae7f7..d46bb0b 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -4,67 +4,21 @@ import ( "encoding/json" "net" "os" + "os/exec" "path/filepath" "sync" "testing" "time" ) -// ---- detectEnv / resolveProject ---- - -func TestDetectEnv_FindsProjectToml(t *testing.T) { - root := t.TempDir() - sub := filepath.Join(root, "a", "b") - if err := os.MkdirAll(sub, 0755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, "Project.toml"), []byte{}, 0644); err != nil { - t.Fatal(err) - } - - got := detectEnv(sub) - if got != root { - t.Errorf("detectEnv(%q) = %q, want %q", sub, got, root) - } -} - -func TestDetectEnv_NoneFound(t *testing.T) { - dir := t.TempDir() - // Make sure there's no Project.toml anywhere up the tree - // (TempDir is under /tmp which never has one) - got := detectEnv(dir) - if got != "" { - t.Errorf("detectEnv(%q) = %q, want empty", dir, got) - } -} - -func TestResolveProject_Empty(t *testing.T) { - // When empty, result is either a detected env or "". - // Just ensure it doesn't panic and returns a valid absolute path or "". - got := resolveProject("") - if got != "" { - if !filepath.IsAbs(got) { - t.Errorf("resolveProject(\"\") = %q, want absolute path or empty", got) - } - } -} - -func TestResolveProject_Relative(t *testing.T) { - root := t.TempDir() - sub := filepath.Join(root, "proj") - os.Mkdir(sub, 0755) - - orig, _ := os.Getwd() - os.Chdir(root) - defer os.Chdir(orig) - - got := resolveProject("proj") - // Resolve symlinks on both sides (macOS /var → /private/var) - gotR, _ := filepath.EvalSymlinks(got) - subR, _ := filepath.EvalSymlinks(sub) - if gotR != subR { - t.Errorf("resolveProject(\"proj\") = %q, want %q", got, sub) +// TestMain allows the test binary to act as the CLI when TEST_CLI=1, +// enabling subprocess-based end-to-end tests of main(). +func TestMain(m *testing.M) { + if os.Getenv("TEST_CLI") == "1" { + main() + return } + os.Exit(m.Run()) } // ---- pkgPattern ---- @@ -142,82 +96,80 @@ func TestHandleRequest_Stop(t *testing.T) { } } -// ---- daemon socket integration (no Julia) ---- - -func TestDaemonPingOverSocket(t *testing.T) { - socketPath := filepath.Join(t.TempDir(), "test.sock") +// ---- helpers ---- - var wg sync.WaitGroup +// startTestDaemon launches serveDaemon in a goroutine and returns a stop func and the socket path. +// The returned WaitGroup is done when the daemon exits. +func startTestDaemon(t *testing.T) (socketPath string, stop func(), wg *sync.WaitGroup) { + t.Helper() + socketPath = filepath.Join(t.TempDir(), "test.sock") + wg = &sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() serveDaemon(socketPath, time.Hour) }() + waitForSocket(t, socketPath) + stop = func() { + conn, _ := net.Dial("unix", socketPath) + if conn != nil { + json.NewEncoder(conn).Encode(map[string]any{"action": "stop"}) + conn.Close() + } + wg.Wait() + } + return +} - // Wait for socket to appear +func waitForSocket(t *testing.T, socketPath string) { + t.Helper() deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(socketPath); err == nil { - break + return } time.Sleep(20 * time.Millisecond) } + t.Fatal("daemon socket did not appear in time") +} +func sendRequest(t *testing.T, socketPath string, payload map[string]any) map[string]any { + t.Helper() conn, err := net.Dial("unix", socketPath) if err != nil { t.Fatalf("dial: %v", err) } defer conn.Close() - - if err := json.NewEncoder(conn).Encode(map[string]any{"action": "ping"}); err != nil { - t.Fatal(err) - } + json.NewEncoder(conn).Encode(payload) var resp map[string]any - if err := json.NewDecoder(conn).Decode(&resp); err != nil { - t.Fatal(err) - } + json.NewDecoder(conn).Decode(&resp) + return resp +} + +// ---- daemon socket integration (no Julia) ---- + +func TestDaemonPingOverSocket(t *testing.T) { + socketPath, stop, _ := startTestDaemon(t) + defer stop() + + resp := sendRequest(t, socketPath, map[string]any{"action": "ping"}) if resp["output"] != "pong" { t.Errorf("ping over socket = %v, want pong", resp["output"]) } - - // Stop the daemon so the goroutine exits - conn2, _ := net.Dial("unix", socketPath) - json.NewEncoder(conn2).Encode(map[string]any{"action": "stop"}) - conn2.Close() - wg.Wait() } // ---- Julia integration ---- func TestEvalBasic(t *testing.T) { - socketPath := filepath.Join(t.TempDir(), "test.sock") - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - serveDaemon(socketPath, time.Hour) - }() - - // Wait for socket - deadline := time.Now().Add(5 * time.Second) - for time.Now().Before(deadline) { - if _, err := os.Stat(socketPath); err == nil { - break - } - time.Sleep(20 * time.Millisecond) - } + socketPath, stop, _ := startTestDaemon(t) + defer stop() + cwd, _ := os.Getwd() send := func(payload map[string]any) map[string]any { - conn, err := net.Dial("unix", socketPath) - if err != nil { - t.Fatalf("dial: %v", err) + if _, ok := payload["cwd"]; !ok { + payload["cwd"] = cwd } - defer conn.Close() - json.NewEncoder(conn).Encode(payload) - var resp map[string]any - json.NewDecoder(conn).Decode(&resp) - return resp + return sendRequest(t, socketPath, payload) } // Eval basic expression @@ -238,12 +190,11 @@ func TestEvalBasic(t *testing.T) { t.Errorf("state not persisted: x = %q, want %q", out2, "42\n") } - // Restart clears state - send(map[string]any{"action": "restart"}) - resp3 := send(map[string]any{"action": "eval", "code": "println(isdefined(Main, :x))"}) + // Fresh eval clears state before running code. + resp3 := send(map[string]any{"action": "eval", "code": "println(isdefined(Main, :x))", "fresh": true}) out3, _ := resp3["output"].(string) if out3 != "false\n" { - t.Errorf("after restart x should be undefined, got %q", out3) + t.Errorf("after fresh eval x should be undefined, got %q", out3) } // println adds trailing newline; print does not @@ -255,10 +206,44 @@ func TestEvalBasic(t *testing.T) { if out5, _ := resp5["output"].(string); out5 != "with-nl\n" { t.Errorf("println output = %q, want %q", out5, "with-nl\n") } +} + +// TestScriptFile exercises the full main() routing: julia-client script.jl +// The test binary re-invokes itself as the CLI via the TestMain/TEST_CLI mechanism. +func TestScriptFile(t *testing.T) { + socketPath, stop, _ := startTestDaemon(t) + defer stop() + + cmd := exec.Command(os.Args[0], "--socket", socketPath, "testdata/compute.jl") + cmd.Env = append(os.Environ(), "TEST_CLI=1") + out, err := cmd.Output() + if err != nil { + stderr := "" + if e, ok := err.(*exec.ExitError); ok { + stderr = string(e.Stderr) + } + t.Fatalf("script run failed: %v\n%s", err, stderr) + } + if got := string(out); got != "42\n" { + t.Errorf("script output = %q, want %q", got, "42\n") + } +} - // Stop daemon - conn, _ := net.Dial("unix", socketPath) - json.NewEncoder(conn).Encode(map[string]any{"action": "stop"}) - conn.Close() - wg.Wait() +func TestPrintResult(t *testing.T) { + socketPath, stop, _ := startTestDaemon(t) + defer stop() + + cwd, _ := os.Getwd() + resp := sendRequest(t, socketPath, map[string]any{ + "action": "eval", + "code": "1 + 1", + "cwd": cwd, + "print_result": true, + }) + if resp["error"] != nil { + t.Fatalf("print_result error: %v", resp["error"]) + } + if out, _ := resp["output"].(string); out != "2\n" { + t.Errorf("print_result output = %q, want %q", out, "2\n") + } } diff --git a/go/daemon.go b/go/daemon.go index e2c44e8..ee8180d 100644 --- a/go/daemon.go +++ b/go/daemon.go @@ -32,8 +32,11 @@ func handleRequest(state *daemonState, req map[string]any) map[string]any { switch action { case "eval": code, _ := req["code"].(string) - envPath, _ := req["env_path"].(string) + cwd, _ := req["cwd"].(string) + project, _ := req["project"].(string) + session, _ := req["session"].(string) juliaCmd, _ := req["julia_cmd"].(string) + fresh, _ := req["fresh"].(bool) var timeoutSecs float64 if t, ok := req["timeout"]; ok { @@ -48,24 +51,22 @@ func handleRequest(state *daemonState, req map[string]any) map[string]any { } printResult, _ := req["print_result"].(bool) - sess, err := state.manager.getOrCreate(envPath, juliaCmd) + if fresh { + state.manager.restart(session, project, cwd) + } + sess, err := state.manager.getOrCreate(cwd, project, session, juliaCmd) if err != nil { return errResp(err.Error()) } output, err := sess.execute(code, timeoutSecs, printResult) if err != nil { if !sess.isAlive() { - state.manager.remove(envPath) + state.manager.remove(session, project, cwd) } return errResp(err.Error()) } return map[string]any{"output": output, "error": nil} - case "restart": - envPath, _ := req["env_path"].(string) - state.manager.restart(envPath) - return map[string]any{"output": "Session restarted.", "error": nil} - case "sessions": sessions := state.manager.list() if len(sessions) == 0 { @@ -77,11 +78,7 @@ func handleRequest(state *daemonState, req map[string]any) map[string]any { if !s.alive { status = "dead" } - label := s.envPath - if s.isTemp { - label += " (temp)" - } - line := fmt.Sprintf(" %s: %s", label, status) + line := fmt.Sprintf(" %s: %s", s.project, status) if s.juliaCmd != "" { line += " julia_cmd=" + s.juliaCmd } diff --git a/go/main.go b/go/main.go index e60cbc7..28f40b3 100644 --- a/go/main.go +++ b/go/main.go @@ -16,41 +16,6 @@ import ( var defaultSocket = filepath.Join(os.Getenv("HOME"), ".local", "share", "julia-client", "julia-daemon.sock") -func detectEnv(start string) string { - if start == "" { - var err error - start, err = os.Getwd() - if err != nil { - return "" - } - } - dir, err := filepath.Abs(start) - if err != nil { - return "" - } - for { - if _, err := os.Stat(filepath.Join(dir, "Project.toml")); err == nil { - return dir - } - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - return "" -} - -// resolveProject returns an absolute project path: auto-detected if project is empty, -// absolutized if the caller supplied a (possibly relative) path. -func resolveProject(project string) string { - if project == "" { - return detectEnv("") - } - abs, _ := filepath.Abs(project) - return abs -} - func startDaemon(socketPath string) { // Re-exec ourselves with the daemon subcommand — no external dependency. self, err := os.Executable() @@ -123,7 +88,16 @@ func run(socketPath string, payload map[string]any, startIfNeeded bool) { } } -func cmdEval(socketPath, code, project string, timeout float64, juliaCmd string, printResult bool) { +func mustGetwd() string { + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, "error: cannot determine working directory:", err) + os.Exit(1) + } + return cwd +} + +func cmdEval(socketPath, code, project, session string, timeout float64, juliaCmd string, printResult, fresh bool) { if code == "-" { b, err := io.ReadAll(os.Stdin) if err != nil { @@ -132,9 +106,13 @@ func cmdEval(socketPath, code, project string, timeout float64, juliaCmd string, } code = string(b) } - payload := map[string]any{"action": "eval", "code": code} - if p := resolveProject(project); p != "" { - payload["env_path"] = p + projectArg := project + if project != "@." { + projectArg, _ = filepath.Abs(project) + } + payload := map[string]any{"action": "eval", "code": code, "cwd": mustGetwd(), "project": projectArg} + if session != "" { + payload["session"] = session } if timeout != -1 { payload["timeout"] = timeout @@ -145,6 +123,9 @@ func cmdEval(socketPath, code, project string, timeout float64, juliaCmd string, if printResult { payload["print_result"] = true } + if fresh { + payload["fresh"] = true + } run(socketPath, payload, true) } @@ -152,14 +133,6 @@ func cmdSessions(socketPath string) { run(socketPath, map[string]any{"action": "sessions"}, false) } -func cmdRestart(socketPath, project string) { - payload := map[string]any{"action": "restart"} - if p := resolveProject(project); p != "" { - payload["env_path"] = p - } - run(socketPath, payload, false) -} - func cmdStop(socketPath string) { run(socketPath, map[string]any{"action": "stop"}, false) } @@ -178,20 +151,25 @@ func usage() { fmt.Fprintf(os.Stderr, `julia-client: Julia REPL client Usage: - julia-client [flags] [-e CODE] + julia-client [flags] [file] [-e CODE] julia-client [--socket PATH] [options] Eval flags: -e, --eval CODE Evaluate Julia code (omit or use - to read stdin) -E, --print CODE Evaluate Julia code and display the result - --project PATH Julia project directory (auto-detected from $PWD) + --project PATH Julia project directory (passed as --project to Julia) + --session LABEL Named session to create or reuse across directories + --fresh Clear the targeted session before evaluating --timeout SECS Timeout in seconds (0 = no timeout, default: 60) --julia-cmd CMD Custom Julia binary, e.g. "julia +1.11" +Session routing (priority order): + --session LABEL Shared by label, regardless of directory + --project PATH Keyed by project path + (default) Keyed by current working directory; Julia uses --project=@. + Commands: sessions List active Julia sessions - restart Restart a Julia session, clearing all state - --project PATH Project directory stop Stop the daemon daemon Run the daemon in the foreground (normally auto-started) --idle-timeout SECS Shut down after idle (default: 1800) @@ -208,7 +186,9 @@ func main() { evalLong := flag.String("eval", "", "Evaluate Julia code") printShort := flag.String("E", "", "Evaluate and display result") printLong := flag.String("print", "", "Evaluate and display result") - projectFlag := flag.String("project", "", "Julia project directory") + projectFlag := flag.String("project", "@.", "Julia project directory") + sessionFlag := flag.String("session", "", "Named session label") + freshFlag := flag.Bool("fresh", false, "Clear the targeted session before evaluating") timeoutFlag := flag.Float64("timeout", -1, "Timeout in seconds") juliaCmdFlag := flag.String("julia-cmd", "", "Custom Julia binary") flag.Usage = usage @@ -216,14 +196,14 @@ func main() { // -E / --print: evaluate and display result if code := first(*printShort, *printLong); code != "" { - cmdEval(*socketFlag, code, *projectFlag, *timeoutFlag, *juliaCmdFlag, true) + cmdEval(*socketFlag, code, *projectFlag, *sessionFlag, *timeoutFlag, *juliaCmdFlag, true, *freshFlag) return } // -e / --eval: evaluate mode code := first(*evalShort, *evalLong) if code != "" { - cmdEval(*socketFlag, code, *projectFlag, *timeoutFlag, *juliaCmdFlag, false) + cmdEval(*socketFlag, code, *projectFlag, *sessionFlag, *timeoutFlag, *juliaCmdFlag, false, *freshFlag) return } @@ -235,7 +215,7 @@ func main() { if err != nil || fi.Mode()&os.ModeCharDevice != 0 { usage() } - cmdEval(*socketFlag, "-", *projectFlag, *timeoutFlag, *juliaCmdFlag, false) + cmdEval(*socketFlag, "-", *projectFlag, *sessionFlag, *timeoutFlag, *juliaCmdFlag, false, *freshFlag) return } @@ -243,12 +223,6 @@ func main() { case "sessions": cmdSessions(*socketFlag) - case "restart": - fs := flag.NewFlagSet("restart", flag.ExitOnError) - project := fs.String("project", "", "Project directory") - fs.Parse(args[1:]) - cmdRestart(*socketFlag, *project) - case "stop": cmdStop(*socketFlag) @@ -262,6 +236,15 @@ func main() { } default: + if filepath.Ext(args[0]) == ".jl" { + b, err := os.ReadFile(args[0]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + cmdEval(*socketFlag, string(b), *projectFlag, *sessionFlag, *timeoutFlag, *juliaCmdFlag, false, *freshFlag) + return + } fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) usage() } diff --git a/go/session.go b/go/session.go index 051e6e3..81d2ad5 100644 --- a/go/session.go +++ b/go/session.go @@ -20,16 +20,13 @@ import ( const ( defaultEvalTimeout = 60.0 startupTimeout = 120.0 - tempSessionKey = "__temp__" ) // JuliaSession manages a single persistent Julia subprocess. type JuliaSession struct { - envDir string - sentinel string - isTemp bool - isTest bool - juliaCmd string + projectVal string // pre-computed --project= arg (also used for display) + sentinel string + juliaCmd string proc *exec.Cmd stdin io.WriteCloser @@ -46,25 +43,17 @@ func newSentinel() string { return fmt.Sprintf("__JULIA_CLIENT_%s__", hex.EncodeToString(b)) } -func newJuliaSession(envDir, sentinel string, isTemp, isTest bool, juliaCmd string, logFile *os.File) *JuliaSession { +func newJuliaSession(projectVal, sentinel, juliaCmd string, logFile *os.File) *JuliaSession { return &JuliaSession{ - envDir: envDir, - sentinel: sentinel, - isTemp: isTemp, - isTest: isTest, - juliaCmd: juliaCmd, - logFile: logFile, + projectVal: projectVal, + sentinel: sentinel, + juliaCmd: juliaCmd, + logFile: logFile, } } -func (s *JuliaSession) projectPath() string { - if s.isTest { - return filepath.Dir(s.envDir) - } - return s.envDir -} -func (s *JuliaSession) start() error { +func (s *JuliaSession) start(workDir string) error { exe := "julia" var channelArgs, extraFlags []string @@ -90,10 +79,10 @@ func (s *JuliaSession) start() error { args := append(channelArgs, "-i", "--threads=auto") args = append(args, extraFlags...) - args = append(args, fmt.Sprintf("--project=%s", s.projectPath())) + args = append(args, fmt.Sprintf("--project=%s", s.projectVal)) cmd := exec.Command(exe, args...) - cmd.Dir = s.envDir + cmd.Dir = workDir stdin, err := cmd.StdinPipe() if err != nil { @@ -127,12 +116,6 @@ func (s *JuliaSession) start() error { if _, err := s.executeRaw("using InteractiveUtils", startupTimeout); err != nil { return fmt.Errorf("failed to load InteractiveUtils: %w", err) } - // TestEnv activation for test/ directories - if s.isTest { - if _, err := s.executeRaw("using TestEnv; TestEnv.activate()", 0); err != nil { - return err - } - } return nil } @@ -249,9 +232,6 @@ func (s *JuliaSession) kill() { if s.logFile != nil { s.logFile.Close() } - if s.isTemp { - os.RemoveAll(s.envDir) - } } // SessionManager tracks multiple named Julia sessions. @@ -270,25 +250,30 @@ func newSessionManager() *SessionManager { } } -func (m *SessionManager) key(envPath string) string { - if envPath == "" { - return tempSessionKey +// key returns the session map key. +// Priority: explicit session label > explicit project path > cwd. +func (m *SessionManager) key(session, project, cwd string) string { + if session != "" { + return "~" + session } - abs, _ := filepath.Abs(envPath) - return abs + if project != "" && project != "@." { + abs, _ := filepath.Abs(project) + return abs + } + return cwd } func (m *SessionManager) openLogFile(key string) *os.File { - safe := strings.NewReplacer("/", "_", "\\", "_").Replace(strings.Trim(key, "/")) + safe := strings.NewReplacer("/", "_", "\\", "_").Replace(strings.Trim(key, "/~")) if safe == "" { - safe = "temp" + safe = "default" } f, _ := os.OpenFile(filepath.Join(m.logDir, safe+".log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) return f } -func (m *SessionManager) getOrCreate(envPath, juliaCmd string) (*JuliaSession, error) { - key := m.key(envPath) +func (m *SessionManager) getOrCreate(cwd, project, session, juliaCmd string) (*JuliaSession, error) { + key := m.key(session, project, cwd) // Fast path: return existing live session without singleflight overhead. m.mu.Lock() @@ -313,23 +298,12 @@ func (m *SessionManager) getOrCreate(envPath, juliaCmd string) (*JuliaSession, e m.mu.Unlock() } - isTemp := envPath == "" - var envDir string - var isTest bool - if isTemp { - var err error - envDir, err = os.MkdirTemp("", "julia-client-") - if err != nil { - return nil, err - } - } else { - abs, _ := filepath.Abs(envPath) - envDir = abs - isTest = filepath.Base(envDir) == "test" + projectVal := project + if projectVal == "" { + projectVal = "@." } - - sess = newJuliaSession(envDir, newSentinel(), isTemp, isTest, juliaCmd, m.openLogFile(key)) - if err := sess.start(); err != nil { + sess = newJuliaSession(projectVal, newSentinel(), juliaCmd, m.openLogFile(key)) + if err := sess.start(cwd); err != nil { return nil, err } @@ -344,15 +318,15 @@ func (m *SessionManager) getOrCreate(envPath, juliaCmd string) (*JuliaSession, e return v.(*JuliaSession), nil } -func (m *SessionManager) remove(envPath string) { - key := m.key(envPath) +func (m *SessionManager) remove(session, project, cwd string) { + key := m.key(session, project, cwd) m.mu.Lock() delete(m.sessions, key) m.mu.Unlock() } -func (m *SessionManager) restart(envPath string) { - key := m.key(envPath) +func (m *SessionManager) restart(session, project, cwd string) { + key := m.key(session, project, cwd) m.mu.Lock() sess := m.sessions[key] delete(m.sessions, key) @@ -363,9 +337,8 @@ func (m *SessionManager) restart(envPath string) { } type sessionInfo struct { - envPath string + project string alive bool - isTemp bool juliaCmd string logFile string } @@ -376,9 +349,8 @@ func (m *SessionManager) list() []sessionInfo { result := make([]sessionInfo, 0, len(m.sessions)) for _, sess := range m.sessions { info := sessionInfo{ - envPath: sess.envDir, + project: sess.projectVal, alive: sess.isAlive(), - isTemp: sess.isTemp, juliaCmd: sess.juliaCmd, } if sess.logFile != nil { diff --git a/go/testdata/compute.jl b/go/testdata/compute.jl new file mode 100644 index 0000000..47d84c7 --- /dev/null +++ b/go/testdata/compute.jl @@ -0,0 +1 @@ +println(6 * 7) diff --git a/skills/julia-client/SKILL.md b/skills/julia-client/SKILL.md index e502a7e..78471bc 100644 --- a/skills/julia-client/SKILL.md +++ b/skills/julia-client/SKILL.md @@ -6,12 +6,12 @@ description: "Run Julia code with session state persistence, project env auto-de ## Running code ```bash -julia-client -e 'x=1' # Evaluate +julia-client -e 'const x=1' # Evaluate julia-client -E 'x' # Evaluate and display +julia-client --fresh -E 'x=2' # Run with clean session state -# Long-running tasks (pkg install, compile, heavy compute): set longer timeout or disable -julia-client --timeout 300 -e 'include("heavy_script.jl")' -julia-client --timeout 0 -e 'using Pkg; Pkg.add("Example")' +# Long-running tasks (pkg install, compile, heavy compute): set longer timeout or disable (0) +julia-client --timeout 300 heavy_script.jl ``` ## Tips @@ -22,6 +22,5 @@ julia-client --timeout 0 -e 'using Pkg; Pkg.add("Example")' ```bash julia-client sessions # list active sessions -julia-client restart # restart session (slow, loses state; use if "Julia session has died") julia-client stop # shut down the daemon ```