Initial commit on Forgejo
Fresh repository history for elektrine/tarakan-client hosted at https://git.elektrine.com/elektrine/tarakan-client.
This commit is contained in:
commit
58b29a7f84
70 changed files with 12994 additions and 0 deletions
243
cmd/tarakan/main.go
Normal file
243
cmd/tarakan/main.go
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/atomine-elektrine/tarakan-client/internal/agent"
|
||||
"github.com/atomine-elektrine/tarakan-client/internal/api"
|
||||
"github.com/atomine-elektrine/tarakan-client/internal/app"
|
||||
repoctx "github.com/atomine-elektrine/tarakan-client/internal/context"
|
||||
"github.com/atomine-elektrine/tarakan-client/internal/headless"
|
||||
"github.com/atomine-elektrine/tarakan-client/internal/updatecheck"
|
||||
)
|
||||
|
||||
// version is the client release (override with -ldflags "-X main.version=…").
|
||||
var version = "0.2.4"
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func run(arguments []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
peeledURL, peeledToken, arguments := peelAPIFlags(arguments)
|
||||
cfg := api.LoadConfig(peeledURL, peeledToken)
|
||||
if len(arguments) > 0 && arguments[0] == "login" {
|
||||
return runLogin(arguments[1:], stdout, stderr, cfg, peeledToken)
|
||||
}
|
||||
if len(arguments) > 0 && arguments[0] == "logout" {
|
||||
if len(arguments) != 1 {
|
||||
fmt.Fprintln(stderr, "Usage: tarakan logout")
|
||||
return 2
|
||||
}
|
||||
return runLogout(stdout, stderr)
|
||||
}
|
||||
|
||||
if len(arguments) > 0 && isWorkCommand(arguments[0]) {
|
||||
return runWorkCommand(arguments[0], arguments[1:], stdin, stdout, stderr, cfg)
|
||||
}
|
||||
|
||||
flags := flag.NewFlagSet("tarakan", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
|
||||
var prompt string
|
||||
var agentName string
|
||||
var model string
|
||||
var jobID int64
|
||||
var pickup bool
|
||||
var printContext bool
|
||||
var printAgents bool
|
||||
var printVersion bool
|
||||
var urlFlag, hostFlag, tokenFlag string
|
||||
var minStars int
|
||||
var language, kind string
|
||||
flags.StringVar(&prompt, "p", "", "run one prompt in headless JSON mode")
|
||||
flags.StringVar(&prompt, "prompt", "", "run one prompt in headless JSON mode")
|
||||
flags.StringVar(&agentName, "agent", "", "review backend: kimi, claude, codex, grok, ollama, openrouter, or kimi-http")
|
||||
flags.StringVar(&model, "model", "", "override the model for HTTP backends (ollama, openrouter, kimi-http)")
|
||||
flags.Int64Var(&jobID, "job", 0, "open interactive UI, claim this job, and run the agent")
|
||||
flags.BoolVar(&pickup, "pickup", false, "open interactive UI, claim next open job from the global queue, run agent")
|
||||
flags.IntVar(&minStars, "min-stars", 0, "only pick up jobs/repos with at least this many stars")
|
||||
flags.StringVar(&language, "language", "", "only pick up jobs/repos with this primary language")
|
||||
flags.StringVar(&language, "lang", "", "alias for --language")
|
||||
flags.StringVar(&kind, "kind", "", "only pick up jobs of this kind (e.g. code_review, verify_findings)")
|
||||
flags.BoolVar(&printContext, "context", false, "print repository context as JSON")
|
||||
flags.BoolVar(&printAgents, "agents", false, "print detected review backends as JSON")
|
||||
flags.BoolVar(&printVersion, "version", false, "print version")
|
||||
addAPIFlags(flags, &urlFlag, &hostFlag, &tokenFlag)
|
||||
flags.Usage = func() {
|
||||
fmt.Fprintln(stderr, "Tarakan - public security reports from your terminal")
|
||||
fmt.Fprintln(stderr, "\nAuth (saved login; CLI flags and env override it):")
|
||||
fmt.Fprintln(stderr, " tarakan login Save a token for future commands")
|
||||
fmt.Fprintln(stderr, " tarakan logout Remove the saved token")
|
||||
fmt.Fprintln(stderr, " --url / --host Tarakan base URL")
|
||||
fmt.Fprintln(stderr, " --token One-command API token override")
|
||||
fmt.Fprintln(stderr, "\nMass path:")
|
||||
fmt.Fprintln(stderr, " tarakan login")
|
||||
fmt.Fprintln(stderr, " tarakan report --agent grok --pickup")
|
||||
fmt.Fprintln(stderr, " tarakan worker --agent codex --min-stars 1000 --language Rust")
|
||||
fmt.Fprintln(stderr, " tarakan report --agent grok --pickup --lang Elixir --min-stars 100")
|
||||
fmt.Fprintln(stderr, " tarakan --url http://localhost:4000 --token TOKEN --agent grok --pickup")
|
||||
fmt.Fprintln(stderr, " tarakan report --agent grok --job ID --yes")
|
||||
fmt.Fprintln(stderr, " tarakan register owner/name")
|
||||
fmt.Fprintln(stderr, " tarakan check REPORT_ID --verdict confirmed|disputed --notes TEXT")
|
||||
fmt.Fprintln(stderr, "\nInteractive: /url, /token, /config · Also: jobs | claim | submit | …")
|
||||
fmt.Fprintln(stderr, "\nUsage: tarakan [options]")
|
||||
flags.PrintDefaults()
|
||||
}
|
||||
|
||||
if err := flags.Parse(arguments); err != nil {
|
||||
return 2
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
fmt.Fprintf(stderr, "unexpected arguments: %v\n", flags.Args())
|
||||
return 2
|
||||
}
|
||||
if printVersion {
|
||||
fmt.Fprintln(stdout, version)
|
||||
// Best-effort: show whether a newer release exists (stderr keeps stdout machine-friendly).
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
result, err := updatecheck.Check(ctx, version)
|
||||
cancel()
|
||||
if err == nil && result.UpdateAvailable {
|
||||
fmt.Fprintln(stderr, result.Notice())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
resolvedURL, err := resolveAPIFlagURL(urlFlag, hostFlag)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 2
|
||||
}
|
||||
cfg = cfg.WithOverrides(resolvedURL, tokenFlag)
|
||||
|
||||
repository, err := repoctx.Current()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "discover repository: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
registry := agent.Detect()
|
||||
if printContext {
|
||||
return encodeJSON(stdout, stderr, repository)
|
||||
}
|
||||
if printAgents {
|
||||
return encodeJSON(stdout, stderr, registry.Providers())
|
||||
}
|
||||
|
||||
var selected agent.Provider
|
||||
if agentName != "" {
|
||||
var ok bool
|
||||
selected, ok = registry.Find(agentName)
|
||||
if !ok {
|
||||
fmt.Fprintf(stderr, "backend %q is not installed or configured\n", agentName)
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
selected, _ = registry.Default()
|
||||
}
|
||||
selected = selected.WithModel(model)
|
||||
|
||||
if prompt != "" {
|
||||
if jobID > 0 || pickup {
|
||||
fmt.Fprintln(stderr, "use either -p/--prompt or --job/--pickup, not both")
|
||||
return 2
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if selected.Name == "" {
|
||||
selected = agent.Provider{Name: "unavailable", Description: "No supported agent"}
|
||||
}
|
||||
if err := headless.Run(ctx, stdout, repository, selected, prompt); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
fmt.Fprintf(stderr, "review failed: %v\n", err)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (jobID > 0 || pickup) && selected.Name == "" {
|
||||
fmt.Fprintln(stderr, "no review backend available; install grok/codex/claude/kimi or pass --agent")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Surfaces "newer release available" before long agent runs / TUI work.
|
||||
updatecheck.MaybeNotify(stderr, version)
|
||||
|
||||
program := tea.NewProgram(app.NewSession(repository, registry, selected, app.SessionOpts{
|
||||
JobID: jobID,
|
||||
Pickup: pickup,
|
||||
APIConfig: cfg,
|
||||
Filter: api.QueueFilter{
|
||||
MinStars: minStars,
|
||||
Language: language,
|
||||
Kind: kind,
|
||||
},
|
||||
}))
|
||||
if _, err := program.Run(); err != nil {
|
||||
fmt.Fprintf(stderr, "run Tarakan: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func encodeJSON(stdout, stderr io.Writer, value any) int {
|
||||
encoder := json.NewEncoder(stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
fmt.Fprintf(stderr, "encode JSON: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runInteractiveJob opens the TUI. If jobID > 0, claims that job; if pickup,
|
||||
// claims the next open report job for this repo. Then runs the agent and waits
|
||||
// for /submit-report.
|
||||
func runInteractiveJob(agentName, model string, jobID int64, pickup bool, cfg api.Config, filter api.QueueFilter, stderr io.Writer) int {
|
||||
repository, err := repoctx.Current()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "discover repository: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
registry := agent.Detect()
|
||||
var selected agent.Provider
|
||||
if agentName != "" {
|
||||
var ok bool
|
||||
selected, ok = registry.Find(agentName)
|
||||
if !ok {
|
||||
fmt.Fprintf(stderr, "backend %q is not installed or configured\n", agentName)
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
selected, _ = registry.Default()
|
||||
}
|
||||
selected = selected.WithModel(model)
|
||||
if selected.Name == "" {
|
||||
fmt.Fprintln(stderr, "no review backend available; install grok/codex/claude/kimi or pass --agent")
|
||||
return 1
|
||||
}
|
||||
|
||||
program := tea.NewProgram(app.NewSession(repository, registry, selected, app.SessionOpts{
|
||||
JobID: jobID,
|
||||
Pickup: pickup,
|
||||
APIConfig: cfg,
|
||||
Filter: filter,
|
||||
}))
|
||||
if _, err := program.Run(); err != nil {
|
||||
fmt.Fprintf(stderr, "run Tarakan: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue