package main import ( "bytes" "encoding/json" "log" "net/http" "os" "os/exec" "path/filepath" "time" "github.com/nicepkg/jarvis/transform" ) type CompileRequest struct { Language string `json:"language"` Source string `json:"source"` } type CompileResponse struct { Success bool `json:"success"` Wasm []byte `json:"wasm,omitempty"` Size int `json:"size,omitempty"` TimeMS int64 `json:"time_ms,omitempty"` Errors []string `json:"errors,omitempty"` } type Language struct { Value string `json:"value"` Label string `json:"label"` } var languages = []Language{ {Value: "typescript", Label: "TypeScript"}, {Value: "go", Label: "Go"}, } func main() { port := os.Getenv("PORT") if port == "" { port = "8090" } http.HandleFunc("/health", healthHandler) http.HandleFunc("/compile", compileHandler) http.HandleFunc("/languages", languagesHandler) http.HandleFunc("/hooks", hooksHandler) http.HandleFunc("/template", templateHandler) http.HandleFunc("/sdk", sdkHandler) http.HandleFunc("/lsp", lspHandler) log.Printf("Jarvis listening on :%s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } func healthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func languagesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(languages) } func hooksHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(hooks) } func templateHandler(w http.ResponseWriter, r *http.Request) { hook := r.URL.Query().Get("hook") language := r.URL.Query().Get("language") if hook == "" { hook = "post.published" } if language == "" { language = "typescript" } template := GetTemplate(hook, language) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "template": template, "hook": hook, "language": language, }) } func compileHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req CompileRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, "Invalid request body") return } start := time.Now() var resp CompileResponse switch req.Language { case "typescript": resp = compileTypeScript(req.Source) case "go": resp = compileGo(req.Source) default: respondError(w, "Unsupported language: "+req.Language) return } resp.TimeMS = time.Since(start).Milliseconds() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func respondError(w http.ResponseWriter, msg string) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CompileResponse{Success: false, Errors: []string{msg}}) } func compileTypeScript(source string) CompileResponse { // Transform source from clean API to Extism-compatible format transformed, err := transform.TypeScript(source) if err != nil { return CompileResponse{Success: false, Errors: []string{"Transform error: " + err.Error()}} } tmpDir, err := os.MkdirTemp("", "jarvis-ts-*") if err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } defer os.RemoveAll(tmpDir) srcFile := filepath.Join(tmpDir, "plugin.ts") outFile := filepath.Join(tmpDir, "plugin.wasm") dtsFile := filepath.Join(tmpDir, "index.d.ts") if err := os.WriteFile(srcFile, []byte(transformed), 0644); err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } // Copy the Extism d.ts file - extism-js requires interface declarations sdkDts, err := os.ReadFile("/app/sdk/extism.d.ts") if err != nil { return CompileResponse{Success: false, Errors: []string{"Failed to read SDK d.ts: " + err.Error()}} } if err := os.WriteFile(dtsFile, sdkDts, 0644); err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } cmd := exec.Command("extism-js", srcFile, "-i", dtsFile, "-o", outFile) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return CompileResponse{Success: false, Errors: []string{stderr.String()}} } wasm, err := os.ReadFile(outFile) if err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } return CompileResponse{Success: true, Wasm: wasm, Size: len(wasm)} } func compileGo(source string) CompileResponse { // Transform source from clean API to Extism-compatible format transformed, err := transform.Go(source) if err != nil { return CompileResponse{Success: false, Errors: []string{"Transform error: " + err.Error()}} } tmpDir, err := os.MkdirTemp("", "jarvis-go-*") if err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } defer os.RemoveAll(tmpDir) srcFile := filepath.Join(tmpDir, "main.go") outFile := filepath.Join(tmpDir, "plugin.wasm") if err := os.WriteFile(srcFile, []byte(transformed), 0644); err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(` module writekit-plugin go 1.22 require github.com/extism/go-pdk v1.0.6 `), 0644); err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } // Run go mod tidy to fetch dependencies and create go.sum tidyCmd := exec.Command("go", "mod", "tidy") tidyCmd.Dir = tmpDir var tidyStderr bytes.Buffer tidyCmd.Stderr = &tidyStderr if err := tidyCmd.Run(); err != nil { return CompileResponse{Success: false, Errors: []string{"Failed to tidy dependencies: " + tidyStderr.String()}} } cmd := exec.Command("tinygo", "build", "-o", outFile, "-target", "wasi", srcFile) cmd.Dir = tmpDir var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return CompileResponse{Success: false, Errors: []string{stderr.String()}} } wasm, err := os.ReadFile(outFile) if err != nil { return CompileResponse{Success: false, Errors: []string{err.Error()}} } return CompileResponse{Success: true, Wasm: wasm, Size: len(wasm)} }