diff --git a/hbrtl_ext/nodertl/nodertl.go b/hbrtl_ext/nodertl/nodertl.go new file mode 100644 index 0000000..669d3c1 --- /dev/null +++ b/hbrtl_ext/nodertl/nodertl.go @@ -0,0 +1,86 @@ +// hbrtl_ext/nodertl — 순수 Go 로 Node.js 서브프로세스를 호출해 npm 생태계를 +// PRG 에서 쓰게 하는 fivenode 런타임 RTL. C/CGO/Harbour 네이티브 한 줄 없이 +// (Five = C→Go 재구현 철학 유지) Node 의 전체 라이브러리 생태계에 접근한다. +// +// Five(Go 바이너리) ──exec──▶ node -e ──require──▶ npm 모듈 +// +// PRG surface: +// NODE_EVAL(cJs [, cWorkdir [, cInput]]) -> cJson +// cJs : 실행할 JS (결과를 process.stdout 으로 출력) +// cWorkdir : require() 해석 기준 디렉터리(node_modules 위치). 비우면 +// 환경변수 SOLMADE_NODE_DIR 사용. +// cInput : 사용자 입력. JS 에 코드로 박지 말고 env FIVE_NODE_INPUT 으로 +// 전달된다(코드 인젝션 방지). JS 에서 process.env.FIVE_NODE_INPUT 로 읽음. +// 반환: {"ok":bool,"out":string,"err":string} JSON 문자열. +package nodertl + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + + "five/hbrt" +) + +func init() { + hbrt.HB_FUNC("NODE_EVAL", nodeEval) +} + +// nodeBin: PATH 에 node 가 없어도(예: launchd 의 좁은 PATH) 흔한 위치를 폴백. +func nodeBin() string { + if p, err := exec.LookPath("node"); err == nil { + return p + } + for _, p := range []string{"/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"} { + if _, err := os.Stat(p); err == nil { + return p + } + } + return "" +} + +func resJSON(ok bool, out, errs string) string { + b, _ := json.Marshal(map[string]interface{}{"ok": ok, "out": out, "err": errs}) + return string(b) +} + +func nodeEval(ctx *hbrt.HBContext) { + if ctx.PCount() < 1 || !ctx.IsChar(1) { + ctx.RetC(resJSON(false, "", "NODE_EVAL(cJs[,cWorkdir[,cInput]]) — missing js")) + return + } + js := ctx.ParC(1) + + workdir := "" + if ctx.PCount() >= 2 && ctx.IsChar(2) { + workdir = ctx.ParC(2) + } + if workdir == "" { + workdir = os.Getenv("SOLMADE_NODE_DIR") + } + + input := "" + if ctx.PCount() >= 3 && ctx.IsChar(3) { + input = ctx.ParC(3) + } + + bin := nodeBin() + if bin == "" { + ctx.RetC(resJSON(false, "", "node not found (install node; or set PATH)")) + return + } + + cmd := exec.Command(bin, "-e", js) + if workdir != "" { + cmd.Dir = workdir + } + cmd.Env = append(os.Environ(), "FIVE_NODE_INPUT="+input) + + var out, errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + runErr := cmd.Run() + + ctx.RetC(resJSON(runErr == nil, out.String(), errb.String())) +}