229 lines
6.1 KiB
Go
229 lines
6.1 KiB
Go
|
|
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)}
|
||
|
|
}
|