move src file to seperate direcotry

This commit is contained in:
2026-04-01 22:29:20 +02:00
parent 228cefb921
commit e42397cc7a
30 changed files with 0 additions and 0 deletions

84
src/cmd/add.go Normal file
View File

@@ -0,0 +1,84 @@
package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"github.com/spf13/cobra"
)
var cDue, cContent, cType, cStatus, cPrio, cNamespace, cAssignee string
var cTags, cRels []string
var addCmd = &cobra.Command{
Use: "add <title>", Short: "Create a new node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
input := service.AddInput{
Title: args[0],
Content: cContent,
DueDate: cDue,
}
// --tag is an alias for --rel with no target.
for _, tag := range cTags {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelType(tag), Target: ""})
}
// Shorthand flags expand to property rels or edge rels.
if cType != "" {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelType("_type::" + cType), Target: ""})
}
if cStatus != "" {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelType("_status::" + cStatus), Target: ""})
}
if cPrio != "" {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelType("_prio::" + cPrio), Target: ""})
}
if cNamespace != "" {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelInNamespace, Target: cNamespace})
}
if cAssignee != "" {
input.Rels = append(input.Rels, service.RelInput{Type: models.RelAssignee, Target: cAssignee})
}
for _, r := range cRels {
ri, err := parseRelInput(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
input.Rels = append(input.Rels, ri)
}
n, err := svc.Add(input)
if err != nil {
fmt.Fprintln(os.Stderr, "failed to create:", err)
return
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(addCmd)
f := addCmd.Flags()
f.StringVar(&cType, "type", "", "node type (issue, note, …)")
f.StringVar(&cStatus, "status", "", "initial status (open, done)")
f.StringVar(&cPrio, "prio", "", "priority (high, medium, low)")
f.StringVar(&cNamespace, "namespace", "", "namespace name or ID")
f.StringVar(&cAssignee, "assignee", "", "assignee username or ID")
f.StringVar(&cDue, "due", "", "due date")
f.StringVar(&cContent, "content", "", "node body")
f.StringArrayVar(&cTags, "tag", nil, "label tag (alias for --rel tagname)")
f.StringArrayVar(&cRels, "rel", nil, "relation (prefix::value or relname:target)")
}

56
src/cmd/alias.go Normal file
View File

@@ -0,0 +1,56 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"github.com/spf13/cobra"
)
var aliasDesc string
var aliasCmd = &cobra.Command{
Use: "alias [name] [command]", Short: "Manage aliases", Args: cobra.MaximumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
w := cmd.OutOrStdout()
if len(args) == 0 {
if aliases, err := cfg.ListAliases(); err == nil {
output.PrintAliases(w, aliases, jsonFlag)
}
return
}
if len(args) == 1 {
a, err := cfg.GetAlias(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "alias not found:", args[0])
os.Exit(1)
}
fmt.Println(a.Command)
return
}
if err := cfg.SetAlias(&service.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)
}
},
}
var aliasDelCmd = &cobra.Command{
Use: "del <name>", Short: "Delete an alias", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := cfg.DeleteAlias(args[0]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output.PrintAction(cmd.OutOrStdout(), "Alias deleted", args[0], false)
},
}
func init() {
rootCmd.AddCommand(aliasCmd)
aliasCmd.AddCommand(aliasDelCmd)
aliasCmd.Flags().StringVar(&aliasDesc, "desc", "", "description for the alias")
}

50
src/cmd/del.go Normal file
View File

@@ -0,0 +1,50 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"bufio"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var dForce bool
var delCmd = &cobra.Command{
Use: "del <id>", Short: "Delete a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
n, err := svc.GetByID(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "node not found: %s\n", args[0])
os.Exit(1)
}
if !dForce {
fmt.Printf("Delete %s '%s'? [y/N]: ", n.GetProperty("type"), n.Title)
r, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if r = strings.TrimSpace(strings.ToLower(r)); r != "y" && r != "yes" {
fmt.Println("Cancelled.")
return
}
}
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)
}
},
}
func init() {
rootCmd.AddCommand(delCmd)
delCmd.Flags().BoolVarP(&dForce, "force", "f", false, "")
}

65
src/cmd/edit.go Normal file
View File

@@ -0,0 +1,65 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"os/exec"
"github.com/spf13/cobra"
)
var editCmd = &cobra.Command{
Use: "edit <id>", Short: "Edit node content in $EDITOR", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
n, err := svc.GetByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "node not found:", args[0])
return
}
tmp, err := os.CreateTemp("", "ax-*.md")
if err != nil {
fmt.Fprintln(os.Stderr, "failed to create temp file:", err)
return
}
tmp.WriteString(n.Content)
tmp.Close()
defer os.Remove(tmp.Name())
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
c := exec.Command(editor, 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)
return
}
content, err := os.ReadFile(tmp.Name())
if err != nil {
fmt.Fprintln(os.Stderr, "failed to read temp file:", err)
return
}
s := string(content)
n, err = svc.Update(args[0], service.UpdateInput{Content: &s})
if err != nil {
fmt.Fprintln(os.Stderr, "failed to update:", err)
return
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(editCmd)
}

33
src/cmd/init.go Normal file
View File

@@ -0,0 +1,33 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init [path]", Short: "Initialize a new database", Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
p := "."
if len(args) > 0 {
p = args[0]
}
dbPath := filepath.Join(p, ".ax.db")
if _, err := os.Stat(dbPath); err == nil {
fmt.Fprintln(os.Stderr, "database already exists:", dbPath)
os.Exit(1)
}
if err := service.InitNodeService(dbPath); err != nil {
fmt.Fprintln(os.Stderr, "failed to initialize:", err)
os.Exit(1)
}
output.PrintAction(cmd.OutOrStdout(), "Created", dbPath, false)
},
}
func init() { rootCmd.AddCommand(initCmd) }

80
src/cmd/list.go Normal file
View File

@@ -0,0 +1,80 @@
package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"github.com/spf13/cobra"
)
var lTags, lRels []string
var lStatus, lPrio, lType, lNamespace, lAssignee, lMention string
var listCmd = &cobra.Command{
Use: "list", Short: "List nodes",
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
var filter service.ListFilter
// --tag is an alias for a label filter with no target.
for _, tag := range lTags {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType(tag), Target: ""})
}
// Shorthand flags expand to property filters or edge filters.
if lStatus != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType("_status::" + lStatus), Target: ""})
}
if lPrio != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType("_prio::" + lPrio), Target: ""})
}
if lType != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType("_type::" + lType), Target: ""})
}
if lNamespace != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelInNamespace, Target: lNamespace})
}
if lAssignee != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelAssignee, Target: lAssignee})
}
if lMention != "" {
filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelMentions, Target: lMention})
}
for _, r := range lRels {
ri, err := parseRelInput(r)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse relation flag: %v", err)
return
}
filter.Rels = append(filter.Rels, ri)
}
if nodes, err := svc.List(filter); err == nil {
output.PrintNodes(cmd.OutOrStdout(), svc, nodes, jsonFlag)
} else {
fmt.Fprintf(os.Stderr, "err: %v\n", err)
}
},
}
func init() {
rootCmd.AddCommand(listCmd)
f := listCmd.Flags()
f.StringArrayVar(&lTags, "tag", nil, "filter by label tag")
f.StringArrayVar(&lRels, "rel", nil, "filter by relation (prefix::value or relname:target)")
f.StringVar(&lStatus, "status", "", "filter by status")
f.StringVar(&lPrio, "prio", "", "filter by priority")
f.StringVar(&lType, "type", "", "filter by type")
f.StringVar(&lNamespace, "namespace", "", "filter by namespace")
f.StringVar(&lAssignee, "assignee", "", "filter by assignee")
f.StringVar(&lMention, "mention", "", "filter by mention")
}

84
src/cmd/login.go Normal file
View File

@@ -0,0 +1,84 @@
package cmd
import (
"axolotl/service"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/spf13/cobra"
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate with the remote server via OIDC",
Run: func(cmd *cobra.Command, args []string) {
rc, ok := cfg.GetRemoteConfig()
if !ok {
fmt.Fprintln(os.Stderr, "no remote server configured; set remote.host in your config")
os.Exit(1)
}
base := fmt.Sprintf("http://%s:%d", rc.Host, rc.Port)
resp, err := http.Post(base+"/auth/start", "application/json", nil)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to contact server: %v\n", err)
os.Exit(1)
}
var start struct {
URL string `json:"url"`
SessionID string `json:"session_id"`
}
json.NewDecoder(resp.Body).Decode(&start)
resp.Body.Close()
if start.URL == "" {
fmt.Fprintln(os.Stderr, "server did not return an auth URL; is OIDC configured on the server?")
os.Exit(1)
}
fmt.Printf("Open this URL in your browser:\n\n %s\n\nWaiting for login...\n", start.URL)
deadline := time.Now().Add(5 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(2 * time.Second)
resp, err := http.Get(fmt.Sprintf("%s/auth/poll?session_id=%s", base, start.SessionID))
if err != nil {
continue
}
if resp.StatusCode == http.StatusAccepted {
resp.Body.Close()
continue
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
fmt.Fprintln(os.Stderr, "login failed")
os.Exit(1)
}
var result struct {
Token string `json:"token"`
Username string `json:"username"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if err := service.SaveSession(&service.Session{Token: result.Token}); err != nil {
fmt.Fprintf(os.Stderr, "failed to save session: %v\n", err)
os.Exit(1)
}
fmt.Printf("Logged in as %s\n", result.Username)
return
}
fmt.Fprintln(os.Stderr, "login timed out")
os.Exit(1)
},
}
func init() {
rootCmd.AddCommand(loginCmd)
}

26
src/cmd/rel.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"axolotl/models"
"axolotl/service"
"strings"
)
// 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
// - "tagname" → simple label rel with no target (alias for --tag)
func parseRelInput(s string) (service.RelInput, error) {
if strings.Contains(s, "::") {
// Property: name::value — no target node.
return service.RelInput{Type: models.RelType(s), Target: ""}, nil
}
if idx := strings.Index(s, ":"); idx >= 0 {
// Edge rel: relname:target.
return service.RelInput{Type: models.RelType(s[:idx]), Target: s[idx+1:]}, nil
}
// Simple label tag — no target node.
return service.RelInput{Type: models.RelType(s), Target: ""}, nil
}

86
src/cmd/root.go Normal file
View File

@@ -0,0 +1,86 @@
package cmd
import (
"axolotl/service"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var jsonFlag bool
var cfg service.Config
var rootCmd = &cobra.Command{Use: "ax", Short: "The axolotl issue tracker"}
func Execute() {
var err error
cfg, err = service.LoadConfigFile()
if err != nil {
fmt.Fprintln(os.Stderr, "failed to load config:", err)
os.Exit(1)
}
registerAliasCommands()
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "")
}
func registerAliasCommands() {
rootCmd.AddGroup(&cobra.Group{ID: "aliases", Title: "Aliases:"})
aliases, _ := cfg.ListAliases()
for _, a := range aliases {
rootCmd.AddCommand(&cobra.Command{
Use: a.Name,
Short: a.Description,
GroupID: "aliases",
DisableFlagParsing: true,
Run: func(ccmd *cobra.Command, args []string) {
acmd := a.Command
acmd = strings.ReplaceAll(acmd, "$me", cfg.GetUser())
parts := strings.Fields(acmd)
var expanded []string
usedArgs := make([]bool, len(args))
for _, part := range parts {
if part == "$@" {
expanded = append(expanded, args...)
for i := range usedArgs {
usedArgs[i] = true
}
continue
}
hasCatchAll := strings.Contains(part, "$@")
replaced := part
if hasCatchAll {
replaced = strings.ReplaceAll(replaced, "$@", strings.Join(args, " "))
for i := range usedArgs {
usedArgs[i] = true
}
}
for i := len(args) - 1; i >= 0; i-- {
placeholder := fmt.Sprintf("$%d", i+1)
if strings.Contains(replaced, placeholder) {
replaced = strings.ReplaceAll(replaced, placeholder, args[i])
usedArgs[i] = true
}
}
expanded = append(expanded, replaced)
}
// Forward any unconsumed args (e.g. --json flag).
for i, arg := range args {
if !usedArgs[i] {
expanded = append(expanded, arg)
}
}
rootCmd.SetArgs(expanded)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
},
})
}
}

40
src/cmd/serve.go Normal file
View File

@@ -0,0 +1,40 @@
package cmd
import (
"axolotl/serve"
"axolotl/service"
"fmt"
"net/http"
"os"
"github.com/spf13/cobra"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the JSON API server",
Run: func(cmd *cobra.Command, args []string) {
sc := cfg.GetServerConfig()
addr := fmt.Sprintf("%s:%d", sc.Host, sc.Port)
var oidcCfg *service.OIDCConfig
if oc, ok := cfg.GetOIDCConfig(); ok {
oidcCfg = &oc
}
handler, err := serve.New(service.GetNodeServiceForUser, oidcCfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "listening on %s\n", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(serveCmd)
}

32
src/cmd/show.go Normal file
View File

@@ -0,0 +1,32 @@
package cmd
import (
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"github.com/spf13/cobra"
)
var showCmd = &cobra.Command{
Use: "show <id>", Short: "Show node details", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
n, err := svc.GetByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "node not found:", args[0])
os.Exit(1)
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(showCmd)
}

113
src/cmd/update.go Normal file
View File

@@ -0,0 +1,113 @@
package cmd
import (
"axolotl/models"
"axolotl/output"
"axolotl/service"
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
uTitle, uContent, uDue string
uClearDue bool
uStatus, uPrio, uType string
uNamespace, uAssignee string
uAddTags, uRmTags, uAddRels, uRmRels []string
)
var updateCmd = &cobra.Command{
Use: "update <id>", Short: "Update a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
var input service.UpdateInput
if cmd.Flags().Changed("title") {
input.Title = &uTitle
}
if cmd.Flags().Changed("content") {
input.Content = &uContent
}
if cmd.Flags().Changed("due") {
input.DueDate = &uDue
}
if uClearDue {
empty := ""
input.DueDate = &empty
}
// --tag / --tag-remove are aliases for --rel / --rel-remove with no target.
for _, tag := range uAddTags {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelType(tag), Target: ""})
}
for _, tag := range uRmTags {
input.RemoveRels = append(input.RemoveRels, service.RelInput{Type: models.RelType(tag), Target: ""})
}
// Shorthand flags expand to property rels or edge rels.
if cmd.Flags().Changed("type") {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelType("_type::" + uType), Target: ""})
}
if cmd.Flags().Changed("status") {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelType("_status::" + uStatus), Target: ""})
}
if cmd.Flags().Changed("prio") {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelType("_prio::" + uPrio), Target: ""})
}
if cmd.Flags().Changed("namespace") {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelInNamespace, Target: uNamespace})
}
if cmd.Flags().Changed("assignee") {
input.AddRels = append(input.AddRels, service.RelInput{Type: models.RelAssignee, Target: uAssignee})
}
for _, r := range uAddRels {
ri, err := parseRelInput(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
input.AddRels = append(input.AddRels, ri)
}
for _, r := range uRmRels {
ri, err := parseRelInput(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
input.RemoveRels = append(input.RemoveRels, ri)
}
n, err := svc.Update(args[0], input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(updateCmd)
f := updateCmd.Flags()
f.StringVar(&uTitle, "title", "", "new title")
f.StringVar(&uContent, "content", "", "new content")
f.StringVar(&uDue, "due", "", "due date")
f.BoolVar(&uClearDue, "clear-due", false, "clear due date")
f.StringVar(&uStatus, "status", "", "status (open, done)")
f.StringVar(&uPrio, "prio", "", "priority (high, medium, low)")
f.StringVar(&uType, "type", "", "node type")
f.StringVar(&uNamespace, "namespace", "", "namespace name or ID")
f.StringVar(&uAssignee, "assignee", "", "assignee username or ID")
f.StringArrayVar(&uAddTags, "tag", nil, "add label tag (alias for --rel tagname)")
f.StringArrayVar(&uRmTags, "tag-remove", nil, "remove label tag")
f.StringArrayVar(&uAddRels, "rel", nil, "add relation (prefix::value or relname:target)")
f.StringArrayVar(&uRmRels, "rel-remove", nil, "remove relation (prefix::value or relname:target)")
}