refactor: replace explicit fields with tag-based property system

This commit is contained in:
2026-03-27 02:11:46 +01:00
parent 2d4cff717b
commit b2225cff7b
17 changed files with 485 additions and 825 deletions

View File

@@ -12,46 +12,38 @@ import (
var aliasList bool
var aliasCmd = &cobra.Command{
Use: "alias [name] [command]",
Short: "Manage aliases",
Args: cobra.MaximumNArgs(2),
Use: "alias [name] [command]", Short: "Manage aliases", Args: cobra.MaximumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if aliasList {
aliases, err := d.ListAliases()
if err != nil {
return
w := cmd.OutOrStdout()
if aliasList || len(args) == 0 {
if aliases, err := d.ListAliases(); err == nil {
output.PrintAliases(w, aliases, jsonFlag)
}
output.PrintAliases(cmd.OutOrStdout(), aliases, jsonFlag)
return
}
if len(args) == 0 {
aliases, _ := d.ListAliases()
output.PrintAliases(cmd.OutOrStdout(), aliases, jsonFlag)
return
}
if len(args) == 1 {
a, err := d.GetAlias(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, " alias not found:", args[0])
return
if a, err := d.GetAlias(args[0]); err != nil {
fmt.Fprintln(os.Stderr, "alias not found:", args[0])
} else {
fmt.Println(a.Command)
}
fmt.Println(a.Command)
return
}
if err := d.SetAlias(args[0], args[1]); err != nil {
fmt.Fprintln(os.Stderr, " failed to set alias:", err)
return
fmt.Fprintln(os.Stderr, "failed to set alias:", err)
} else {
output.PrintAction(w, "Alias set", args[0], false)
}
output.PrintSuccess(cmd.OutOrStdout(), "Alias '%s' set", args[0])
},
}
func init() {
rootCmd.AddCommand(aliasCmd)
aliasCmd.Flags().BoolVar(&aliasList, "list", false, "list all aliases")
aliasCmd.Flags().BoolVar(&aliasList, "list", false, "")
}

View File

@@ -6,63 +6,61 @@ import (
"axolotl/output"
"fmt"
"os"
"slices"
"strings"
"github.com/spf13/cobra"
)
var createType, createStatus, createPrio, createNamespace string = "issue", "open", "", ""
var createDue, createContent string
var createTags, createRels []string
var cDue, cContent, cDummy string
var cTags, cRels []string
var createCmd = &cobra.Command{
Use: "create <title>",
Short: "Create a new node",
Args: cobra.ExactArgs(1),
Use: "create <title>", Short: "Create a new node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if createType == "issue" && createStatus == "" {
createStatus = "open"
if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_type::") }) {
cTags = append(cTags, "_type::issue")
}
if slices.Contains(cTags, "_type::issue") && !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_status::") }) {
cTags = append(cTags, "_status::open")
}
if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_namespace::") }) {
cTags = append(cTags, "_namespace::" + db.GetCurrentUser())
}
rels := make(map[models.RelType][]string)
for _, r := range createRels {
relType, target, err := db.ParseRelFlag(r)
for _, r := range cRels {
rt, tgt, err := db.ParseRelFlag(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
rels[relType] = append(rels[relType], target)
rels[rt] = append(rels[rt], tgt)
}
n, err := d.CreateNode(db.CreateParams{
Title: args[0],
Content: createContent,
DueDate: createDue,
Type: createType,
Status: createStatus,
Priority: createPrio,
Namespace: createNamespace,
Tags: createTags,
Rels: rels,
})
if err != nil {
if n, err := d.CreateNode(db.CreateParams{Title: args[0], Content: cContent, DueDate: cDue, Tags: cTags, Rels: rels}); err != nil {
fmt.Fprintln(os.Stderr, "failed to create:", err)
return
} else {
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
}
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(createCmd)
createCmd.Flags().StringVar(&createType, "type", "issue", "node type (issue, note, user, namespace)")
createCmd.Flags().StringVar(&createStatus, "status", "", "status (open, done)")
createCmd.Flags().StringVar(&createPrio, "prio", "", "priority (high, medium, low)")
createCmd.Flags().StringVar(&createNamespace, "namespace", "", "namespace")
createCmd.Flags().StringVar(&createDue, "due", "", "due date")
createCmd.Flags().StringVar(&createContent, "content", "", "content")
createCmd.Flags().StringArrayVar(&createTags, "tag", nil, "tags (repeatable)")
createCmd.Flags().StringArrayVar(&createRels, "rel", nil, "relations (type:id, repeatable)")
f := createCmd.Flags()
f.StringVar(&cDummy, "type", "issue", "")
f.StringVar(&cDummy, "status", "", "")
f.StringVar(&cDummy, "prio", "", "")
f.StringVar(&cDummy, "namespace", "", "")
f.StringVar(&cDue, "due", "", "")
f.StringVar(&cContent, "content", "", "")
f.StringArrayVar(&cTags, "tag", nil, "")
f.StringArrayVar(&cRels, "rel", nil, "")
}

View File

@@ -11,43 +11,39 @@ import (
"github.com/spf13/cobra"
)
var deleteForce bool
var dForce bool
var deleteCmd = &cobra.Command{
Use: "delete <id>",
Short: "Delete a node",
Args: cobra.ExactArgs(1),
Use: "delete <id>", Short: "Delete a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
id := args[0]
n, err := d.NodeByID(id)
n, err := d.NodeByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, " node not found:", id)
fmt.Fprintln(os.Stderr, " node not found:", args[0])
return
}
if !deleteForce {
fmt.Printf(" Delete %s '%s'? [y/N]: ", n.GetType(), n.Title)
reader := bufio.NewReader(os.Stdin)
resp, _ := reader.ReadString('\n')
resp = strings.TrimSpace(strings.ToLower(resp))
if resp != "y" && resp != "yes" {
fmt.Println(" Cancelled.")
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 := d.DeleteNode(id); err != nil {
fmt.Fprintln(os.Stderr, " failed to delete:", err)
return
if err := d.DeleteNode(args[0]); err != nil {
fmt.Fprintln(os.Stderr, "failed to delete: ", err)
} else {
output.PrintAction(cmd.OutOrStdout(), "Deleted", args[0], true)
}
output.PrintDeleted(cmd.OutOrStdout(), id)
},
}
func init() {
rootCmd.AddCommand(deleteCmd)
deleteCmd.Flags().BoolVarP(&deleteForce, "force", "f", false, "skip confirmation")
deleteCmd.Flags().BoolVarP(&dForce, "force", "f", false, "")
}

View File

@@ -11,21 +11,19 @@ import (
)
var editCmd = &cobra.Command{
Use: "edit <id>",
Short: "Edit node content in $EDITOR",
Args: cobra.ExactArgs(1),
Use: "edit <id>", Short: "Edit node content in $EDITOR", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
id := args[0]
n, err := d.NodeByID(id)
n, err := d.NodeByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "node not found:", id)
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)
@@ -40,24 +38,22 @@ var editCmd = &cobra.Command{
editor = "vi"
}
c := exec.Command(editor, tmp.Name())
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
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 {
if content, err := os.ReadFile(tmp.Name()); err == nil {
if err := d.UpdateNode(args[0], db.UpdateParams{Content: string(content)}); err != nil {
fmt.Fprintln(os.Stderr, "failed to update:", err)
return
}
n, _ = d.NodeByID(args[0])
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "failed to read temp file:", err)
return
}
if err := d.UpdateNode(id, db.UpdateParams{Content: string(content)}); err != nil {
fmt.Fprintln(os.Stderr, "failed to update:", err)
return
}
n, _ = d.NodeByID(id)
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
},
}

View File

@@ -3,23 +3,23 @@ package cmd
import (
"axolotl/db"
"axolotl/output"
"fmt"
"os"
"github.com/spf13/cobra"
)
var inboxCmd = &cobra.Command{
Use: "inbox",
Short: "Show your inbox",
Use: "inbox", Short: "Show your inbox",
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
user := db.GetCurrentUser()
nodes, err := d.ListNodes(db.ListFilter{
Inbox: user,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
if nodes, err := d.ListNodes(db.ListFilter{TagPrefixes: []string{"_inbox::" + db.GetCurrentUser()}}); err == nil {
output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
}
},
}

View File

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

View File

@@ -9,40 +9,32 @@ import (
"github.com/spf13/cobra"
)
var listType, listStatus, listPrio, listNamespace, listTag, listInbox, listAssignee string
var lDummy, lAssignee string
var lTags []string
var listCmd = &cobra.Command{
Use: "list",
Short: "List nodes",
Use: "list", Short: "List nodes",
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
nodes, err := d.ListNodes(db.ListFilter{
Type: listType,
Status: listStatus,
Priority: listPrio,
Namespace: listNamespace,
Tag: listTag,
Inbox: listInbox,
Assignee: listAssignee,
})
if err != nil {
return
if nodes, err := d.ListNodes(db.ListFilter{TagPrefixes: lTags, Assignee: lAssignee}); err == nil {
output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "err: %v", err)
}
output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().StringVar(&listType, "type", "", "filter by type")
listCmd.Flags().StringVar(&listStatus, "status", "", "filter by status")
listCmd.Flags().StringVar(&listPrio, "prio", "", "filter by priority")
listCmd.Flags().StringVar(&listNamespace, "namespace", "", "filter by namespace")
listCmd.Flags().StringVar(&listTag, "tag", "", "filter by tag")
listCmd.Flags().StringVar(&listInbox, "inbox", "", "filter by inbox user")
listCmd.Flags().StringVar(&listAssignee, "assignee", "", "filter by assignee")
f := listCmd.Flags()
f.StringVar(&lDummy, "type", "", "")
f.StringVar(&lDummy, "status", "", "")
f.StringVar(&lDummy, "prio", "", "")
f.StringVar(&lDummy, "namespace", "", "")
f.StringVar(&lAssignee, "assignee", "", "")
f.StringArrayVar(&lTags, "tag", nil, "")
}

View File

@@ -2,23 +2,46 @@ package cmd
import (
"os"
"strings"
"github.com/spf13/cobra"
)
var jsonFlag bool
var rootCmd = &cobra.Command{
Use: "ax",
Short: "The axolotl issue tracker",
}
var rootCmd = &cobra.Command{Use: "ax", Short: "The axolotl issue tracker"}
func Execute() {
rootCmd.SetArgs(transformArgs(os.Args[1:]))
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "output as JSON")
func transformArgs(args []string) []string {
aliases := map[string]string{
"--status": "_status",
"--prio": "_prio",
"--type": "_type",
"--namespace": "_namespace",
}
result := []string{}
for i := 0; i < len(args); i++ {
if idx := strings.Index(args[i], "="); idx != -1 {
if prop, ok := aliases[args[i][:idx]]; ok {
result = append(result, "--tag", prop+"::"+args[i][idx+1:])
continue
}
}
if prop, ok := aliases[args[i]]; ok && i+1 < len(args) {
result, i = append(result, "--tag", prop+"::"+args[i+1]), i+1
continue
}
result = append(result, args[i])
}
return result
}
func init() {
rootCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "")
}

View File

@@ -10,21 +10,18 @@ import (
)
var showCmd = &cobra.Command{
Use: "show <id>",
Short: "Show node details",
Args: cobra.ExactArgs(1),
Use: "show <id>", Short: "Show node details", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
n, err := d.NodeByID(args[0])
if err != nil {
if n, err := d.NodeByID(args[0]); err == nil {
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "node not found:", args[0])
return
}
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
},
}

View File

@@ -6,45 +6,45 @@ import (
"axolotl/output"
"fmt"
"os"
"slices"
"strings"
"github.com/spf13/cobra"
)
var updateTitle, updateContent, updateStatus, updatePrio, updateDue string
var updateClearDue bool
var updateAddTags, updateRemoveTags, updateAddRels, updateRemoveRels []string
var (
uTitle, uContent, uDue, dummy string
uClearDue bool
uAddTags, uRmTags, uAddRels, uRmRels []string
)
var updateCmd = &cobra.Command{
Use: "update <id>",
Short: "Update a node",
Args: cobra.ExactArgs(1),
Use: "update <id>", Short: "Update a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
d, err := db.GetDB()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
id := args[0]
addRels := make(map[models.RelType][]string)
removeRels := make(map[models.RelType][]string)
for _, r := range updateAddRels {
relType, target, err := db.ParseRelFlag(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
addRels, rmRels := make(map[models.RelType][]string), make(map[models.RelType][]string)
parseRel := func(src []string, dst map[models.RelType][]string) bool {
for _, r := range src {
rt, tgt, err := db.ParseRelFlag(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return false
}
dst[rt] = append(dst[rt], tgt)
}
addRels[relType] = append(addRels[relType], target)
return true
}
for _, r := range updateRemoveRels {
relType, target, err := db.ParseRelFlag(r)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
removeRels[relType] = append(removeRels[relType], target)
if !parseRel(uAddRels, addRels) || !parseRel(uRmRels, rmRels) {
return
}
if updateStatus == "done" {
ok, blockers, err := d.CanClose(id)
if slices.Contains(uAddTags, "_status::done") {
ok, blockers, err := d.CanClose(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "failed to check blockers:", err)
return
@@ -53,42 +53,39 @@ var updateCmd = &cobra.Command{
fmt.Fprintf(os.Stderr, "cannot close: blocked by %v\n", blockers)
return
}
uRmTags = append(uRmTags, "_status::open")
} else if slices.Contains(uAddTags, "_status::open") {
uRmTags = append(uRmTags, "_status::done")
}
err = d.UpdateNode(id, db.UpdateParams{
Title: updateTitle,
Content: updateContent,
DueDate: updateDue,
ClearDue: updateClearDue,
Status: updateStatus,
Priority: updatePrio,
AddTags: updateAddTags,
RemoveTags: updateRemoveTags,
AddRels: addRels,
RemoveRels: removeRels,
})
if err != nil {
if slices.ContainsFunc(uAddTags, func(e string) bool { return strings.HasPrefix(e, "_prio") }) {
uRmTags = append(uRmTags, "_prio::low", "_prio::medium", "_prio::high")
}
uParams := db.UpdateParams{Title: uTitle, Content: uContent, DueDate: uDue, ClearDue: uClearDue,
AddTags: uAddTags, RemoveTags: uRmTags, AddRels: addRels, RemoveRels: rmRels}
if err := d.UpdateNode(args[0], uParams); err != nil {
fmt.Fprintln(os.Stderr, "failed to update:", err)
return
}
n, err := d.NodeByID(id)
if err != nil {
if n, err := d.NodeByID(args[0]); err == nil {
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "failed to fetch node:", err)
return
}
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
},
}
func init() {
rootCmd.AddCommand(updateCmd)
updateCmd.Flags().StringVar(&updateTitle, "title", "", "new title")
updateCmd.Flags().StringVar(&updateContent, "content", "", "new content")
updateCmd.Flags().StringVar(&updateStatus, "status", "", "status (open, done)")
updateCmd.Flags().StringVar(&updatePrio, "prio", "", "priority (high, medium, low)")
updateCmd.Flags().StringVar(&updateDue, "due", "", "due date")
updateCmd.Flags().BoolVar(&updateClearDue, "clear-due", false, "clear due date")
updateCmd.Flags().StringArrayVar(&updateAddTags, "tag", nil, "add tags (repeatable)")
updateCmd.Flags().StringArrayVar(&updateRemoveTags, "tag-remove", nil, "remove tags (repeatable)")
updateCmd.Flags().StringArrayVar(&updateAddRels, "rel", nil, "add relations (type:id, repeatable)")
updateCmd.Flags().StringArrayVar(&updateRemoveRels, "rel-remove", nil, "remove relations (type:id, repeatable)")
f := updateCmd.Flags()
f.StringVar(&uTitle, "title", "", "")
f.StringVar(&uContent, "content", "", "")
f.StringVar(&uDue, "due", "", "")
f.BoolVar(&uClearDue, "clear-due", false, "")
f.StringVar(&dummy, "status", "", "")
f.StringVar(&dummy, "prio", "", "")
f.StringArrayVar(&uAddTags, "tag", nil, "")
f.StringArrayVar(&uRmTags, "tag-remove", nil, "")
f.StringArrayVar(&uAddRels, "rel", nil, "")
f.StringArrayVar(&uRmRels, "rel-remove", nil, "")
}