diff --git a/cmd/lk/agent_utils.go b/cmd/lk/agent_utils.go index d6bca7db..4cb1296e 100644 --- a/cmd/lk/agent_utils.go +++ b/cmd/lk/agent_utils.go @@ -44,11 +44,8 @@ func detectProject(cmd *cli.Command) (string, agentfs.ProjectType, string, error return "", "", "", noAgentError() } - // TODO(node): support JS/Node agents here. DetectProjectRoot already - // recognizes Node projects; once the session daemon can spawn a Node - // agent in console mode, drop this gate and branch on projectType. - if !projectType.IsPython() { - return "", "", "", fmt.Errorf("currently only supports Python agents (detected: %s)", projectType) + if !projectType.IsPython() && !projectType.IsNode() { + return "", "", "", fmt.Errorf("only Python and Node agents are supported (detected: %s)", projectType) } if explicit != "" { diff --git a/cmd/lk/session_daemon.go b/cmd/lk/session_daemon.go index 720e8b38..f775cf15 100644 --- a/cmd/lk/session_daemon.go +++ b/cmd/lk/session_daemon.go @@ -47,8 +47,6 @@ func runSessionDaemon() { } defer server.Close() - // TODO(node): detect a node/JS agent project and build the equivalent - // `node console --connect-addr ` argv. agentProc, err := startAgent(AgentStartConfig{ Dir: os.Getenv(envSessionDir), Entrypoint: os.Getenv(envSessionEntry), diff --git a/cmd/lk/simulate_subprocess.go b/cmd/lk/simulate_subprocess.go index f15728a2..bf448f3d 100644 --- a/cmd/lk/simulate_subprocess.go +++ b/cmd/lk/simulate_subprocess.go @@ -77,6 +77,26 @@ func findPythonBinary(dir string, projectType agentfs.ProjectType) (string, []st return pythonPath, nil, nil } +// findNodeBinary locates the Node binary used to run a JS/TS agent. +func findNodeBinary() (string, error) { + nodePath, err := exec.LookPath("node") + if err != nil { + return "", fmt.Errorf("could not find Node binary; ensure node is on PATH") + } + return nodePath, nil +} + +// isTypeScriptEntry reports whether the entrypoint is TypeScript source that +// needs Node's type-stripping loader to run directly (no build step). +func isTypeScriptEntry(entry string) bool { + switch strings.ToLower(filepath.Ext(entry)) { + case ".ts", ".mts", ".cts": + return true + default: + return false + } +} + // findEntrypoint resolves the agent entrypoint file. func findEntrypoint(dir, explicit string, projectType agentfs.ProjectType) (string, error) { if explicit != "" { @@ -131,16 +151,43 @@ type AgentStartConfig struct { ForwardOutput io.Writer // if set, forward each output line to this writer } -// startAgent launches a Python agent subprocess and monitors its output. -func startAgent(cfg AgentStartConfig) (*AgentProcess, error) { +// buildAgentCommand resolves the interpreter and argv for an agent subprocess, +// branching on project type. Python: ` ` (uv prefixes +// `run python`). Node: `node [--experimental-strip-types] `, +// where the type-stripping flag lets a `.ts` entrypoint run without a build. +func buildAgentCommand(cfg AgentStartConfig) (string, []string, error) { + if cfg.ProjectType.IsNode() { + nodeBin, err := findNodeBinary() + if err != nil { + return "", nil, err + } + args := make([]string, 0, len(cfg.CLIArgs)+2) + if isTypeScriptEntry(cfg.Entrypoint) { + args = append(args, "--experimental-strip-types") + } + args = append(args, cfg.Entrypoint) + args = append(args, cfg.CLIArgs...) + return nodeBin, args, nil + } + pythonBin, prefixArgs, err := findPythonBinary(cfg.Dir, cfg.ProjectType) if err != nil { - return nil, err + return "", nil, err } - - args := append(prefixArgs, cfg.Entrypoint) + args := make([]string, 0, len(prefixArgs)+len(cfg.CLIArgs)+1) + args = append(args, prefixArgs...) + args = append(args, cfg.Entrypoint) args = append(args, cfg.CLIArgs...) - cmd := exec.Command(pythonBin, args...) + return pythonBin, args, nil +} + +// startAgent launches a Python or Node agent subprocess and monitors its output. +func startAgent(cfg AgentStartConfig) (*AgentProcess, error) { + bin, args, err := buildAgentCommand(cfg) + if err != nil { + return nil, err + } + cmd := exec.Command(bin, args...) setProcAttr(cmd) cmd.Dir = cfg.Dir if len(cfg.Env) > 0 {