package main import ( "bytes" "context" "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "strings" "time" "git.eidonix.com/eidonix/cletus-plugins/sdk/manifest" ) type listFlag []string func (l *listFlag) String() string { return strings.Join(*l, ",") } func (l *listFlag) Set(v string) error { *l = append(*l, v); return nil } func runRun(args []string) error { fs := flag.NewFlagSet("run", flag.ContinueOnError) files := fs.String("files", "testdata", "the folder the user opened") workspace := fs.String("workspace", "", "a directory to serve as one shared folder") edit := fs.Bool("edit", false, "the user allows edits") timeout := fs.Duration("timeout", 2*time.Minute, "how long to wait for the answer") var caps listFlag fs.Var(&caps, "capability", "a Cletus feature the user has (repeatable)") if err := fs.Parse(args); err != nil { return err } rest := fs.Args() if len(rest) == 0 { return errors.New("run needs a command: tools, invoke, ui or service") } plugin, err := readPlugin(".") if err != nil { return err } wd, _ := os.Getwd() filesDir, err := filepath.Abs(*files) if err != nil { return err } if err := os.MkdirAll(filesDir, 0o755); err != nil { return err } wsDir := "" if *workspace != "" { if wsDir, err = filepath.Abs(*workspace); err != nil { return err } } bin, cleanup, err := buildWorker(wd) if err != nil { return err } defer cleanup() host := &testHost{plugin: plugin, filesDir: filesDir, workspaceDir: wsDir, edit: *edit, dataDir: filepath.Join(wd, ".cletus-dev", "data"), capabilities: map[string]bool{}, log: os.Stderr} for _, c := range caps { host.capabilities[c] = true } if err := host.start(bin); err != nil { return err } defer host.stop() ctx, cancel := context.WithTimeout(context.Background(), *timeout) defer cancel() arg := func(i int, fallback string) string { if len(rest) > i { return rest[i] } return fallback } var out json.RawMessage switch rest[0] { case "tools": err = host.call(ctx, "tools.list", nil, &out) case "invoke": if len(rest) < 2 { return errors.New("invoke needs a tool name") } err = host.call(ctx, "tools.invoke", map[string]any{"name": rest[1], "args": arg(2, "{}"), "working_dir": filesDir, "edit_mode": *edit}, &out) case "ui": if len(rest) < 2 { return errors.New("ui needs a method name") } err = host.call(ctx, "ui."+strings.TrimPrefix(rest[1], "ui."), json.RawMessage(arg(2, "[]")), &out) case "service": if len(rest) < 2 { return errors.New("service needs .") } err = host.call(ctx, "svc."+rest[1], map[string]any{"caller": "cletus-plugin", "mutating": false, "args": json.RawMessage(arg(2, "{}"))}, &out) default: return fmt.Errorf("unknown run command %q", rest[0]) } if err != nil { return err } var pretty bytes.Buffer if json.Indent(&pretty, out, "", " ") != nil { pretty.Write(out) } fmt.Println(pretty.String()) return nil } func readPlugin(dir string) (*manifest.Plugin, error) { path := filepath.Join(dir, "plugin.json") if _, err := os.Stat(path); err != nil { return nil, manifest.ErrNoManifest } return manifest.Read(path) } // prefixWriter prefixes every line written to w. type prefixWriter struct { w io.Writer prefix string } func (p prefixWriter) Write(b []byte) (int, error) { for _, line := range strings.SplitAfter(string(b), "\n") { if line != "" { fmt.Fprint(p.w, p.prefix+line) } } return len(b), nil }