feat(journal): implement add command

Amolith created

Create journal entries with optional date and title. Content supports
Markdown formatting for rich text. Use "-" to read content from stdin
for multi-line input.

Assisted-by: Claude Opus 4 via Crush

Change summary

cmd/journal/add.go | 86 ++++++++++++++++++++++++++++++++++++-----------
1 file changed, 65 insertions(+), 21 deletions(-)

Detailed changes

cmd/journal/add.go 🔗

@@ -6,7 +6,12 @@ package journal
 
 import (
 	"fmt"
+	"os"
+	"strings"
 
+	"git.secluded.site/lune/internal/client"
+	"git.secluded.site/lune/internal/dateutil"
+	"git.secluded.site/lune/internal/ui"
 	"github.com/spf13/cobra"
 )
 
@@ -16,29 +21,68 @@ var AddCmd = &cobra.Command{
 	Short: "Create a journal entry",
 	Long: `Create a journal entry in Lunatask.
 
-Creates an entry for today by default. Use --date to specify another date.
-Use "-" as CONTENT to read from stdin.`,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		date, _ := cmd.Flags().GetString("date")
-		if date == "" {
-			date = "today"
-		}
-
-		// TODO: implement journal entry creation
-		content := ""
-		if len(args) > 0 {
-			content = args[0]
-		}
-		fmt.Fprintf(cmd.OutOrStdout(), "Creating journal entry for %s (not yet implemented)\n", date)
-		if content != "" {
-			fmt.Fprintf(cmd.OutOrStdout(), "Content: %s\n", content)
-		}
-
-		return nil
-	},
+Creates an entry for today by default. Use --date to specify another date
+using natural language (e.g., "yesterday", "last friday", "2024-01-15").
+
+Content supports Markdown formatting for rich text entries.
+Use "-" as CONTENT to read from stdin (useful for piping or multi-line input).`,
+	RunE: runAdd,
 }
 
 func init() {
 	AddCmd.Flags().StringP("date", "d", "", "Entry date (natural language, default: today)")
-	AddCmd.Flags().StringP("name", "n", "", "Entry title (default: weekday name)")
+	AddCmd.Flags().StringP("name", "n", "", "Entry title (omit to use weekday name)")
+}
+
+func runAdd(cmd *cobra.Command, args []string) error {
+	date, err := dateutil.Parse(cmd.Flag("date").Value.String())
+	if err != nil {
+		return err
+	}
+
+	content, err := resolveContent(args)
+	if err != nil {
+		return err
+	}
+
+	apiClient, err := client.New()
+	if err != nil {
+		return err
+	}
+
+	builder := apiClient.NewJournalEntry(date)
+
+	if content != "" {
+		builder.WithContent(content)
+	}
+
+	if name := cmd.Flag("name").Value.String(); name != "" {
+		builder.WithName(name)
+	}
+
+	entry, err := builder.Create(cmd.Context())
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created journal entry: "+entry.ID))
+
+	return nil
+}
+
+func resolveContent(args []string) (string, error) {
+	if len(args) == 0 {
+		return "", nil
+	}
+
+	if args[0] != "-" {
+		return strings.Join(args, " "), nil
+	}
+
+	data, err := os.ReadFile("/dev/stdin")
+	if err != nil {
+		return "", fmt.Errorf("reading stdin: %w", err)
+	}
+
+	return strings.TrimSpace(string(data)), nil
 }