refactor: consolidate packages - move output to cmd, config/session to store, rename Store to GraphStore

This commit is contained in:
2026-04-02 00:18:33 +02:00
parent 2bcc310c6d
commit 03a896d23f
25 changed files with 190 additions and 239 deletions
+1 -2
View File
@@ -2,7 +2,6 @@ package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
@@ -65,7 +64,7 @@ var addCmd = &cobra.Command{
return
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
+5 -6
View File
@@ -1,8 +1,7 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"axolotl/store"
"fmt"
"os"
@@ -17,7 +16,7 @@ var aliasCmd = &cobra.Command{
w := cmd.OutOrStdout()
if len(args) == 0 {
if aliases, err := cfg.ListAliases(); err == nil {
output.PrintAliases(w, aliases, jsonFlag)
PrintAliases(w, aliases, jsonFlag)
}
return
}
@@ -30,10 +29,10 @@ var aliasCmd = &cobra.Command{
fmt.Println(a.Command)
return
}
if err := cfg.SetAlias(&service.Alias{Name: args[0], Command: args[1], Description: aliasDesc}); err != nil {
if err := cfg.SetAlias(&store.Alias{Name: args[0], Command: args[1], Description: aliasDesc}); err != nil {
fmt.Fprintln(os.Stderr, "failed to set alias:", err)
} else {
output.PrintAction(w, "Alias set", args[0], false)
PrintAction(w, "Alias set", args[0], false)
}
},
}
@@ -45,7 +44,7 @@ var aliasDelCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output.PrintAction(cmd.OutOrStdout(), "Alias deleted", args[0], false)
PrintAction(cmd.OutOrStdout(), "Alias deleted", args[0], false)
},
}
+1 -2
View File
@@ -1,7 +1,6 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"bufio"
"fmt"
@@ -39,7 +38,7 @@ var delCmd = &cobra.Command{
if err := svc.Delete(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "failed to delete: %v", err)
} else {
output.PrintAction(cmd.OutOrStdout(), "Deleted", args[0], true)
PrintAction(cmd.OutOrStdout(), "Deleted", args[0], true)
}
},
}
+2 -7
View File
@@ -1,7 +1,6 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
@@ -34,11 +33,7 @@ var editCmd = &cobra.Command{
tmp.Close()
defer os.Remove(tmp.Name())
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
c := exec.Command(editor, tmp.Name())
c := exec.Command(cfg.GetEditor(), tmp.Name())
c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := c.Run(); err != nil {
fmt.Fprintln(os.Stderr, "editor failed:", err)
@@ -56,7 +51,7 @@ var editCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, "failed to update:", err)
return
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
+8 -5
View File
@@ -1,8 +1,8 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"axolotl/store"
"fmt"
"os"
"path/filepath"
@@ -13,11 +13,14 @@ import (
var initCmd = &cobra.Command{
Use: "init [path]", Short: "Initialize a new database", Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
p := "."
dataRoot, err := store.FindDataRoot(".local", "share")
if len(args) > 0 {
p = args[0]
dataRoot = args[0]
} else if err != nil {
fmt.Fprintln(os.Stderr, "failed to find data dir:", err)
os.Exit(1)
}
dbPath := filepath.Join(p, ".ax.db")
dbPath := filepath.Join(dataRoot, "ax.db")
if _, err := os.Stat(dbPath); err == nil {
fmt.Fprintln(os.Stderr, "database already exists:", dbPath)
os.Exit(1)
@@ -26,7 +29,7 @@ var initCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, "failed to initialize:", err)
os.Exit(1)
}
output.PrintAction(cmd.OutOrStdout(), "Created", dbPath, false)
PrintAction(cmd.OutOrStdout(), "Created", dbPath, false)
},
}
+1 -2
View File
@@ -2,7 +2,6 @@ package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
@@ -59,7 +58,7 @@ var listCmd = &cobra.Command{
}
if nodes, err := svc.List(filter); err == nil {
output.PrintNodes(cmd.OutOrStdout(), svc, nodes, jsonFlag)
PrintNodes(cmd.OutOrStdout(), svc, nodes, jsonFlag)
} else {
fmt.Fprintf(os.Stderr, "err: %v\n", err)
}
+8 -2
View File
@@ -1,7 +1,7 @@
package cmd
import (
"axolotl/service"
"axolotl/store"
"encoding/json"
"fmt"
"net/http"
@@ -66,7 +66,13 @@ var loginCmd = &cobra.Command{
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if err := service.SaveSession(&service.Session{Token: result.Token}); err != nil {
session, err := store.LoadSession()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to save session: %v\n", err)
os.Exit(1)
}
session.Token = result.Token
if err := session.Save(); err != nil {
fmt.Fprintf(os.Stderr, "failed to save session: %v\n", err)
os.Exit(1)
}
+210
View File
@@ -0,0 +1,210 @@
package cmd
import (
"axolotl/models"
"axolotl/service"
"axolotl/store"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"github.com/fatih/color"
)
type RenderMap map[string]struct {
s string
l string
c *color.Color
}
var (
cPrimary = color.New(color.FgCyan)
cSecond = color.New(color.FgMagenta)
cDim = color.New(color.FgHiBlack)
cText = color.New(color.FgWhite)
cTitle = color.New(color.FgWhite, color.Bold)
cGood = color.New(color.FgGreen)
cWarn = color.New(color.FgYellow)
cBad = color.New(color.FgRed)
typeRM = RenderMap{
"issue": {" ", "\uf188 issue", cSecond},
"note": {"\uf15c", "\uf15c note", cPrimary},
"user": {"\uf007", "\uf007 user", cGood},
"namespace": {"\uf07b", "\uf07b namespace", cWarn},
"": {" ", "n/a", cDim},
}
statusRM = RenderMap{
"open": {"●", "● open", cPrimary},
"done": {"○", "○ done", cDim},
"": {"—", "n/a", cDim},
}
prioRM = RenderMap{
"high": {"\uf0e7", "high", cBad},
"medium": {"\uf0e7", "medium", cWarn},
"low": {" ", "low", cDim},
"": {" ", "n/a", cDim},
}
relIcons = map[string]string{"blocks": "\uf068", "subtask": "\uf0da", "related": "\uf0c1", "assignee": "\uf007", "in_namespace": "\uf07b"}
prioRanks = map[string]int{"high": 3, "medium": 2, "low": 1}
statusRanks = map[string]int{"open": 2, "": 1, "done": 0}
)
const (
iconCalendar = "\uf133"
iconCheck = "\uf00c"
iconCross = "\uf00d"
iconNamespace = "\uf07b"
)
func PrintNodes(w io.Writer, svc service.NodeService, nodes []*models.Node, jsonOut bool) error {
if jsonOut {
return json.NewEncoder(w).Encode(nodes)
}
if len(nodes) == 0 {
fmt.Fprintln(w, cDim.Sprint("No results."))
return nil
}
fmt.Fprintln(w)
sort.Slice(nodes, func(i, j int) bool {
si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status")
if si != sj {
return statusRanks[si] > statusRanks[sj]
}
return prioRanks[nodes[i].GetProperty("prio")] > prioRanks[nodes[j].GetProperty("prio")]
})
for _, n := range nodes {
n_rels := n.Relations
ns_rel_node_ids := n_rels[string(models.RelInNamespace)]
ns_rel_node_titles := make([]string, 0, len(ns_rel_node_ids))
for _, id := range ns_rel_node_ids {
ns_rel_node, err := svc.GetByID(id)
if err != nil {
ns_rel_node_titles = append(ns_rel_node_titles, id)
continue
}
ns_rel_node_titles = append(ns_rel_node_titles, ns_rel_node.Title)
}
fmt.Fprintf(w, " %s %s %s %s %s %s",
cDim.Sprint(n.ID),
render(prioRM, n.GetProperty("prio"), true),
render(statusRM, n.GetProperty("status"), true),
render(typeRM, n.GetProperty("type"), true),
cTitle.Sprint(truncate(n.Title, 80)),
cDim.Sprint("["+strings.Join(ns_rel_node_titles, ",")+"]"),
)
tags := n.GetDisplayTags()
if len(tags) > 0 {
fmt.Fprintf(w, " %s", cPrimary.Sprint("#"+strings.Join(tags, " #")))
}
fmt.Fprintln(w)
}
fmt.Fprintln(w)
return nil
}
func PrintNode(w io.Writer, svc service.NodeService, n *models.Node, jsonOut bool) error {
if jsonOut {
return json.NewEncoder(w).Encode(n)
}
fmt.Fprintln(w)
fmt.Fprintf(w, " %s %s %s\n", render(typeRM, n.GetProperty("type"), false), cDim.Sprint(n.ID), cTitle.Sprint(n.Title))
fmt.Fprintln(w, cDim.Sprint(" ───────────────────────────────"))
fmt.Fprintf(w, " Status: %s\n", render(statusRM, n.GetProperty("status"), false))
fmt.Fprintf(w, " Priority: %s\n", render(prioRM, n.GetProperty("prio"), false))
if n.DueDate != "" {
fmt.Fprintf(w, " Due: %s %s\n", iconCalendar, n.DueDate)
}
fmt.Fprintf(w, " Created: %s\n", cDim.Sprint(n.CreatedAt))
fmt.Fprintf(w, " Updated: %s\n", cDim.Sprint(n.UpdatedAt))
if tags := n.GetDisplayTags(); len(tags) > 0 {
fmt.Fprintf(w, "\n tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • ")))
}
n_rels := n.Relations
for relType := range n_rels {
rel_node_ids := n_rels[string(relType)]
if len(rel_node_ids) > 0 {
fmt.Fprintf(w, "\n %s\n", string(relType))
}
for _, id := range rel_node_ids {
rel_node, err := svc.GetByID(id)
if err != nil {
fmt.Fprintf(w, " %s %s\n", relIcons[relType], cDim.Sprint(id))
continue
}
fmt.Fprintf(w, " %s %s\n", relIcons[relType], rel_node.Title)
}
}
if n.Content != "" {
fmt.Fprintln(w, "\n"+cPrimary.Sprint(" Content:"))
for i, line := range strings.Split(n.Content, "\n") {
if i > 5 {
fmt.Fprintf(w, "%s ...\n", cDim.Sprint(" │ "))
break
}
fmt.Fprintf(w, "%s%s\n", cDim.Sprint(" │ "), cText.Sprint(line))
}
}
fmt.Fprintln(w)
return nil
}
func PrintAliases(w io.Writer, aliases []*store.Alias, jsonOut bool) error {
if jsonOut {
return json.NewEncoder(w).Encode(aliases)
}
if len(aliases) == 0 {
fmt.Fprintln(w, cDim.Sprint("No aliases defined."))
return nil
}
fmt.Fprintln(w)
for _, a := range aliases {
fmt.Fprintf(w, " %s %s\n", cPrimary.Sprint(a.Name), cDim.Sprint(a.Command))
if a.Description != "" {
fmt.Fprintf(w, " %s\n", cDim.Sprint(a.Description))
}
}
fmt.Fprintln(w)
return nil
}
func PrintAction(w io.Writer, action, detail string, isError bool) {
if isError {
fmt.Fprintln(w, cBad.Sprint(iconCross+" "+action+" ")+cDim.Sprint(detail))
return
}
icon := iconCheck
if action == "Created" {
icon = iconNamespace
}
fmt.Fprintln(w, cGood.Sprint(icon+" "+action+" ")+cDim.Sprint(detail))
}
func render(rm RenderMap, key string, short bool) string {
v, ok := rm[key]
if !ok {
v, ok = rm[""]
if !ok {
return ""
}
}
if short {
return v.c.Sprint(v.s)
}
return v.c.Sprint(v.l)
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-1] + "…"
}
+5 -5
View File
@@ -3,6 +3,7 @@ package cmd
import (
"axolotl/models"
"axolotl/service"
"axolotl/store"
"fmt"
"os"
"strings"
@@ -11,17 +12,17 @@ import (
)
var jsonFlag bool
var cfg service.Config
var cfg *store.Config
var rootCmd = &cobra.Command{Use: "ax", Short: "The axolotl issue tracker"}
func Execute() {
var err error
cfg, err = service.LoadConfigFile()
cfg, err = store.LoadConfigFile()
if err != nil {
fmt.Fprintln(os.Stderr, "failed to load config:", err)
os.Exit(1)
}
registerAliasCommands()
RegisterAliasCommands()
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
@@ -31,7 +32,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "")
}
func registerAliasCommands() {
func RegisterAliasCommands() {
rootCmd.AddGroup(&cobra.Group{ID: "aliases", Title: "Aliases:"})
aliases, _ := cfg.ListAliases()
for _, a := range aliases {
@@ -87,7 +88,6 @@ func registerAliasCommands() {
}
// parseRelInput parses a rel string into a RelInput.
//
// Formats:
// - "prefix::value" → property rel with no target (tag)
// - "relname:target" → edge rel with a target node
+3 -2
View File
@@ -3,6 +3,7 @@ package cmd
import (
"axolotl/serve"
"axolotl/service"
"axolotl/store"
"fmt"
"net/http"
"os"
@@ -17,9 +18,9 @@ var serveCmd = &cobra.Command{
sc := cfg.GetServerConfig()
addr := fmt.Sprintf("%s:%d", sc.Host, sc.Port)
var oidcCfg *service.OIDCConfig
var oidcCfg *store.OIDCConfig
if oc, ok := cfg.GetOIDCConfig(); ok {
oidcCfg = &oc
oidcCfg = oc
}
handler, err := serve.New(service.GetNodeServiceForUser, oidcCfg)
+1 -2
View File
@@ -1,7 +1,6 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
@@ -23,7 +22,7 @@ var showCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, "node not found:", args[0])
os.Exit(1)
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
+5 -6
View File
@@ -2,7 +2,6 @@ package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
@@ -11,10 +10,10 @@ import (
)
var (
uTitle, uContent, uDue string
uClearDue bool
uStatus, uPrio, uType string
uNamespace, uAssignee string
uTitle, uContent, uDue string
uClearDue bool
uStatus, uPrio, uType string
uNamespace, uAssignee string
uAddTags, uRmTags, uAddRels, uRmRels []string
)
@@ -90,7 +89,7 @@ var updateCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}