Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .claude/agent-memory/project-manager/MEMORY.md

This file was deleted.

12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ module github.com/ALT-F4-LLC/docket
go 1.26.0

require (
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260522183231-0898253a54a2
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260602231358-501858cf198b
github.com/charmbracelet/glamour v1.0.0
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/dustin/go-humanize v1.0.1
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.43.0
modernc.org/sqlite v1.50.1
modernc.org/sqlite v1.52.0
)

require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/alecthomas/chroma/v2 v2.25.0 // indirect
github.com/alecthomas/chroma/v2 v2.26.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
Expand All @@ -25,20 +25,20 @@ require (
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260525135217-abeec2b8bf0b // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260607010151-cd19a2bba55f // indirect
github.com/charmbracelet/x/exp/strings v0.1.0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dlclark/regexp2/v2 v2.1.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260522183231-0898253a54a2 h1:JjkN0bRj36DMemGK/roI9p1GwePny82oqDRzkvOxVY8=
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260522183231-0898253a54a2/go.mod h1:LnxjOk1cYJPm2s1mmsn26qvquhz3CgxVaxU9RR6CcDY=
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260602231358-501858cf198b h1:Blw9y7PQL5suojzBKkq1xN6ouYXKfHJu0PVcidp/vC0=
github.com/ALT-F4-LLC/vorpal/sdk/go v0.0.0-20260602231358-501858cf198b/go.mod h1:LnxjOk1cYJPm2s1mmsn26qvquhz3CgxVaxU9RR6CcDY=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.25.0 h1:DWkVlxrNpxPf+Qcfe04LBqUArxUiybK8ZQ9T7OFu68E=
github.com/alecthomas/chroma/v2 v2.25.0/go.mod h1:+95AZrRWlpW9g6qXD7S7UdHviopsGP/kCIrtJcU3QoQ=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
Expand Down Expand Up @@ -44,8 +44,8 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20260525135217-abeec2b8bf0b h1:CR084j/4jhbEi5jd5GsU0KExMWZLJac3vsZX+5+i+B4=
github.com/charmbracelet/x/exp/slice v0.0.0-20260525135217-abeec2b8bf0b/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/exp/slice v0.0.0-20260607010151-cd19a2bba55f h1:qfvRQEbGZ0bNU/IlPz4SGpyVfx9hSSPB2h8kGnE6Jpc=
github.com/charmbracelet/x/exp/slice v0.0.0-20260607010151-cd19a2bba55f/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
Expand All @@ -61,8 +61,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dlclark/regexp2/v2 v2.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY=
github.com/dlclark/regexp2/v2 v2.1.0/go.mod h1:Bz5TMy5d8fPK0ximH0Yi9KvsRHNnvXqUx9XG6a4wB+I=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
Expand Down Expand Up @@ -94,8 +94,8 @@ github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJ
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
Expand Down Expand Up @@ -189,8 +189,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
Expand Down
80 changes: 80 additions & 0 deletions internal/cli/cli_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cli

import (
"bytes"
"context"
"database/sql"
"testing"

"github.com/ALT-F4-LLC/docket/internal/db"
"github.com/ALT-F4-LLC/docket/internal/model"
"github.com/ALT-F4-LLC/docket/internal/output"
"github.com/spf13/cobra"
)

func newTestDB(t *testing.T) *sql.DB {
t.Helper()
conn, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Open(:memory:): %v", err)
}
t.Cleanup(func() { conn.Close() })
if err := db.Initialize(conn); err != nil {
t.Fatalf("Initialize: %v", err)
}
if err := db.Migrate(conn); err != nil {
t.Fatalf("Migrate: %v", err)
}
return conn
}

func cmdWithDB(conn *sql.DB) *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().Bool("json", false, "")
cmd.Flags().Bool("quiet", false, "")
cmd.Flags().Bool("watch", false, "")
cmd.SetContext(context.WithValue(context.Background(), dbKey, conn))
return cmd
}

func bufWriter(jsonMode bool) (*output.Writer, *bytes.Buffer) {
buf := &bytes.Buffer{}
w := &output.Writer{JSONMode: jsonMode, Stdout: buf, Stderr: &bytes.Buffer{}}
return w, buf
}

func createIssue(t *testing.T, conn *sql.DB, title string, status model.Status, priority model.Priority) int {
t.Helper()
id, err := db.CreateIssue(conn, &model.Issue{
Title: title,
Status: status,
Priority: priority,
Kind: model.IssueKindFeature,
}, nil, nil)
if err != nil {
t.Fatalf("CreateIssue(%q): %v", title, err)
}
return id
}

func createDoc(t *testing.T, conn *sql.DB, title, typ, status string) int {
t.Helper()
id, err := db.CreateDoc(conn, &model.Doc{
Title: title,
Type: typ,
Status: status,
Body: "body",
Author: "tester",
})
if err != nil {
t.Fatalf("CreateDoc(%q): %v", title, err)
}
return id
}

func linkDocIssue(t *testing.T, conn *sql.DB, docID, issueID int) {
t.Helper()
if err := db.LinkDocIssue(conn, docID, issueID); err != nil {
t.Fatalf("LinkDocIssue(%d,%d): %v", docID, issueID, err)
}
}
13 changes: 13 additions & 0 deletions internal/cli/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cli

import "github.com/spf13/cobra"

var docCmd = &cobra.Command{
Use: "doc",
Short: "Manage documents",
Aliases: []string{"d"},
}

func init() {
rootCmd.AddCommand(docCmd)
}
137 changes: 137 additions & 0 deletions internal/cli/doc_comment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cli

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/ALT-F4-LLC/docket/internal/config"
"github.com/ALT-F4-LLC/docket/internal/db"
"github.com/ALT-F4-LLC/docket/internal/model"
"github.com/ALT-F4-LLC/docket/internal/output"
"github.com/spf13/cobra"
)

var docCommentCmd = &cobra.Command{
Use: "comment",
Short: "Manage comments",
}

var docCommentAddCmd = &cobra.Command{
Use: "add [id]",
Short: "Add a comment to a document",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
w := getWriter(cmd)
conn := getDB(cmd)

id, err := model.ParseDocID(args[0])
if err != nil {
return cmdErr(fmt.Errorf("invalid doc ID: %w", err), output.ErrValidation)
}

doc, err := db.GetDoc(conn, id)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return cmdErr(fmt.Errorf("doc %s not found", args[0]), output.ErrNotFound)
}
return cmdErr(fmt.Errorf("fetching doc: %w", err), output.ErrGeneral)
}

jsonMode, _ := cmd.Flags().GetBool("json")
body, _ := cmd.Flags().GetString("message")

if !cmd.Flags().Changed("message") {
stat, err := os.Stdin.Stat()
if err == nil && (stat.Mode()&os.ModeCharDevice) == 0 {
const maxStdinSize = 1 << 20
lr := &io.LimitedReader{R: os.Stdin, N: maxStdinSize + 1}
data, err := io.ReadAll(lr)
if err != nil {
return cmdErr(fmt.Errorf("reading comment from stdin: %w", err), output.ErrGeneral)
}
if int64(len(data)) > maxStdinSize {
return cmdErr(fmt.Errorf("comment body exceeds %d bytes", maxStdinSize), output.ErrValidation)
}
body = strings.TrimSpace(string(data))
}
}

if body == "" && jsonMode {
return cmdErr(fmt.Errorf("message is required in JSON mode"), output.ErrValidation)
}

if body == "" {
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}

tmpFile, err := os.CreateTemp("", "docket-comment-*.md")
if err != nil {
return cmdErr(fmt.Errorf("creating temp file: %w", err), output.ErrGeneral)
}
tmpPath := tmpFile.Name()
if err := tmpFile.Close(); err != nil {
os.Remove(tmpPath)
return cmdErr(fmt.Errorf("closing temp file: %w", err), output.ErrGeneral)
}
defer os.Remove(tmpPath)

editorCmd := exec.Command(editor, tmpPath)
editorCmd.Stdin = os.Stdin
editorCmd.Stdout = os.Stdout
editorCmd.Stderr = os.Stderr

if err := editorCmd.Run(); err != nil {
return cmdErr(fmt.Errorf("editor exited with error: %w", err), output.ErrGeneral)
}

content, err := os.ReadFile(tmpPath)
if err != nil {
return cmdErr(fmt.Errorf("reading temp file: %w", err), output.ErrGeneral)
}

body = strings.TrimSpace(string(content))
}

if body == "" {
w.Info("Cancelled.")
return nil
}

author := config.DefaultAuthor()

comment := model.DocComment{
DocID: id,
Body: body,
Author: author,
}

commentID, err := db.CreateDocComment(conn, &comment)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
return cmdErr(fmt.Errorf("doc %s not found", args[0]), output.ErrNotFound)
}
return cmdErr(fmt.Errorf("creating comment: %w", err), output.ErrGeneral)
}

created, err := db.GetDocComment(conn, commentID)
if err != nil {
return cmdErr(fmt.Errorf("fetching created comment: %w", err), output.ErrGeneral)
}

w.Success(created, fmt.Sprintf("Comment added to %s: %s", model.FormatDocID(id), doc.Title))

return nil
},
}

func init() {
docCommentAddCmd.Flags().StringP("message", "m", "", "Comment body")
docCommentCmd.AddCommand(docCommentAddCmd)
docCmd.AddCommand(docCommentCmd)
}
Loading