Detailed changes
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package cmd
+
+import (
+ "git.secluded.site/lune/cmd/task"
+ "github.com/spf13/cobra"
+)
+
+var addCmd = &cobra.Command{
+ Use: "add NAME",
+ Short: "Shortcut for 'task add'",
+ GroupID: "shortcuts",
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return task.AddCmd.RunE(cmd, args)
+ },
+}
+
+func init() {
+ addCmd.Flags().AddFlagSet(task.AddCmd.Flags())
+}
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var doneCmd = &cobra.Command{
+ Use: "done ID",
+ Short: "Mark a task as completed",
+ GroupID: "shortcuts",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: implement as task update --status completed
+ fmt.Fprintf(cmd.OutOrStdout(), "Marking task %s as done (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package habit provides commands for tracking Lunatask habits.
+package habit
+
+import "github.com/spf13/cobra"
+
+// Cmd is the parent command for habit operations.
+var Cmd = &cobra.Command{
+ Use: "habit",
+ Short: "Manage habits",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(TrackCmd)
+}
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package habit
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/config"
+ "github.com/spf13/cobra"
+)
+
+// TrackCmd tracks a habit activity. Exported for potential use by shortcuts.
+var TrackCmd = &cobra.Command{
+ Use: "track KEY",
+ Short: "Track a habit activity",
+ Long: `Record that a habit was performed.
+
+KEY is the habit key from your config (not the raw Lunatask ID).
+Tracks for today by default. Use --date to specify another date.`,
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeHabits,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ date, _ := cmd.Flags().GetString("date")
+ if date == "" {
+ date = "today"
+ }
+
+ // TODO: implement habit tracking
+ fmt.Fprintf(cmd.OutOrStdout(), "Tracking habit %s for %s (not yet implemented)\n", args[0], date)
+
+ return nil
+ },
+}
+
+func init() {
+ TrackCmd.Flags().StringP("date", "d", "", "Date performed (natural language, default: today)")
+}
+
+// completeHabits returns habit keys from config for shell completion.
+func completeHabits(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ cfg, err := config.Load()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ keys := make([]string, len(cfg.Habits))
+ for i, h := range cfg.Habits {
+ keys[i] = h.Key
+ }
+
+ return keys, cobra.ShellCompDirectiveNoFileComp
+}
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var initCmd = &cobra.Command{
+ Use: "init",
+ Short: "Initialize lune configuration interactively",
+ Long: `Interactively set up your lune configuration.
+
+This command will guide you through:
+ - Verifying your LUNATASK_API_KEY
+ - Adding areas, goals, notebooks, and habits from Lunatask
+ - Setting default area and notebook`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: implement interactive setup with huh
+ fmt.Fprintln(cmd.OutOrStdout(), "Interactive setup not yet implemented")
+
+ return nil
+ },
+}
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package journal
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// AddCmd creates a journal entry. Exported for use by the jrnl shortcut.
+var AddCmd = &cobra.Command{
+ Use: "add [CONTENT]",
+ 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
+ },
+}
+
+func init() {
+ AddCmd.Flags().StringP("date", "d", "", "Entry date (natural language, default: today)")
+ AddCmd.Flags().StringP("name", "n", "", "Entry title (default: weekday name)")
+}
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package journal provides commands for managing Lunatask journal entries.
+package journal
+
+import "github.com/spf13/cobra"
+
+// Cmd is the parent command for journal operations.
+var Cmd = &cobra.Command{
+ Use: "journal",
+ Short: "Manage journal entries",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(AddCmd)
+}
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package cmd
+
+import (
+ "git.secluded.site/lune/cmd/journal"
+ "github.com/spf13/cobra"
+)
+
+var jrnlCmd = &cobra.Command{
+ Use: "jrnl [CONTENT]",
+ Short: "Shortcut for 'journal add'",
+ GroupID: "shortcuts",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return journal.AddCmd.RunE(cmd, args)
+ },
+}
+
+func init() {
+ jrnlCmd.Flags().AddFlagSet(journal.AddCmd.Flags())
+}
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/config"
+ "github.com/spf13/cobra"
+)
+
+// AddCmd creates a new note. Exported for potential use by shortcuts.
+var AddCmd = &cobra.Command{
+ Use: "add [NAME]",
+ Short: "Create a new note",
+ Long: `Create a new note in Lunatask.
+
+Use "-" as content flag value to read from stdin.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: implement note creation
+ name := ""
+ if len(args) > 0 {
+ name = args[0]
+ }
+ fmt.Fprintf(cmd.OutOrStdout(), "Creating note: %s (not yet implemented)\n", name)
+
+ return nil
+ },
+}
+
+func init() {
+ AddCmd.Flags().StringP("notebook", "b", "", "Notebook key (from config)")
+ AddCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)")
+
+ _ = AddCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks)
+}
+
+// completeNotebooks returns notebook keys from config for shell completion.
+func completeNotebooks(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ cfg, err := config.Load()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ keys := make([]string, len(cfg.Notebooks))
+ for i, n := range cfg.Notebooks {
+ keys[i] = n.Key
+ }
+
+ return keys, cobra.ShellCompDirectiveNoFileComp
+}
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "git.secluded.site/lune/internal/ui"
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// DeleteCmd deletes a note. Exported for potential use by shortcuts.
+var DeleteCmd = &cobra.Command{
+ Use: "delete ID",
+ Short: "Delete a note",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ force, _ := cmd.Flags().GetBool("force")
+ if !force {
+ fmt.Fprintf(cmd.OutOrStderr(), "%s Delete note %s? [y/N] ",
+ ui.Warning.Render("Warning:"), args[0])
+
+ reader := bufio.NewReader(os.Stdin)
+ response, _ := reader.ReadString('\n')
+ response = strings.TrimSpace(strings.ToLower(response))
+
+ if response != "y" && response != "yes" {
+ fmt.Fprintln(cmd.OutOrStdout(), "Cancelled")
+
+ return nil
+ }
+ }
+
+ // TODO: implement note deletion
+ fmt.Fprintf(cmd.OutOrStdout(), "Deleting note %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
+}
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// GetCmd retrieves a note by ID. Exported for potential use by shortcuts.
+var GetCmd = &cobra.Command{
+ Use: "get ID",
+ Short: "Get a note by ID",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement note get
+ fmt.Fprintf(cmd.OutOrStdout(), "Getting note %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ GetCmd.Flags().Bool("json", false, "Output as JSON")
+}
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// ListCmd lists notes. Exported for potential use by shortcuts.
+var ListCmd = &cobra.Command{
+ Use: "list",
+ Short: "List notes",
+ Long: `List notes from Lunatask.
+
+Note: Due to end-to-end encryption, note names and content
+are not available through the API. Only metadata is shown.`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ // TODO: implement note listing
+ fmt.Fprintln(cmd.OutOrStdout(), "Note listing not yet implemented")
+
+ return nil
+ },
+}
+
+func init() {
+ ListCmd.Flags().StringP("notebook", "b", "", "Filter by notebook key")
+ ListCmd.Flags().Bool("json", false, "Output as JSON")
+
+ _ = ListCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks)
+}
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package note provides commands for managing Lunatask notes.
+package note
+
+import "github.com/spf13/cobra"
+
+// Cmd is the parent command for note operations.
+var Cmd = &cobra.Command{
+ Use: "note",
+ Short: "Manage notes",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(AddCmd)
+ Cmd.AddCommand(ListCmd)
+ Cmd.AddCommand(GetCmd)
+ Cmd.AddCommand(UpdateCmd)
+ Cmd.AddCommand(DeleteCmd)
+}
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// UpdateCmd updates a note. Exported for potential use by shortcuts.
+var UpdateCmd = &cobra.Command{
+ Use: "update ID",
+ Short: "Update a note",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement note update
+ fmt.Fprintf(cmd.OutOrStdout(), "Updating note %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ UpdateCmd.Flags().String("name", "", "New note name")
+ UpdateCmd.Flags().StringP("notebook", "b", "", "Move to notebook key")
+ UpdateCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)")
+ UpdateCmd.Flags().StringP("date", "d", "", "Note date (natural language)")
+
+ _ = UpdateCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks)
+}
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// AddCmd creates a new person. Exported for potential use by shortcuts.
+var AddCmd = &cobra.Command{
+ Use: "add FIRST LAST",
+ Short: "Add a new person",
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: implement person creation
+ fmt.Fprintf(cmd.OutOrStdout(), "Adding person: %s %s (not yet implemented)\n", args[0], args[1])
+
+ return nil
+ },
+}
+
+func init() {
+ AddCmd.Flags().StringP("relationship", "r", "", "Relationship strength")
+
+ _ = AddCmd.RegisterFlagCompletionFunc("relationship", completeRelationships)
+}
+
+// completeRelationships returns relationship strength options for shell completion.
+func completeRelationships(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{
+ "family",
+ "intimate-friends",
+ "close-friends",
+ "casual-friends",
+ "acquaintances",
+ "business-contacts",
+ "almost-strangers",
+ }, cobra.ShellCompDirectiveNoFileComp
+}
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "git.secluded.site/lune/internal/ui"
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// DeleteCmd deletes a person. Exported for potential use by shortcuts.
+var DeleteCmd = &cobra.Command{
+ Use: "delete ID",
+ Short: "Delete a person",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ force, _ := cmd.Flags().GetBool("force")
+ if !force {
+ fmt.Fprintf(cmd.OutOrStderr(), "%s Delete person %s? [y/N] ",
+ ui.Warning.Render("Warning:"), args[0])
+
+ reader := bufio.NewReader(os.Stdin)
+ response, _ := reader.ReadString('\n')
+ response = strings.TrimSpace(strings.ToLower(response))
+
+ if response != "y" && response != "yes" {
+ fmt.Fprintln(cmd.OutOrStdout(), "Cancelled")
+
+ return nil
+ }
+ }
+
+ // TODO: implement person deletion
+ fmt.Fprintf(cmd.OutOrStdout(), "Deleting person %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
+}
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// GetCmd retrieves a person by ID. Exported for potential use by shortcuts.
+var GetCmd = &cobra.Command{
+ Use: "get ID",
+ Short: "Get a person by ID",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement person get
+ fmt.Fprintf(cmd.OutOrStdout(), "Getting person %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ GetCmd.Flags().Bool("json", false, "Output as JSON")
+}
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// ListCmd lists people. Exported for potential use by shortcuts.
+var ListCmd = &cobra.Command{
+ Use: "list",
+ Short: "List people",
+ Long: `List people from Lunatask.
+
+Note: Due to end-to-end encryption, names are not available
+through the API. Only metadata is shown.`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ // TODO: implement person listing
+ fmt.Fprintln(cmd.OutOrStdout(), "Person listing not yet implemented")
+
+ return nil
+ },
+}
+
+func init() {
+ ListCmd.Flags().Bool("json", false, "Output as JSON")
+}
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package person provides commands for managing Lunatask relationships.
+package person
+
+import "github.com/spf13/cobra"
+
+// Cmd is the parent command for person operations.
+var Cmd = &cobra.Command{
+ Use: "person",
+ Short: "Manage people (relationships)",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(AddCmd)
+ Cmd.AddCommand(ListCmd)
+ Cmd.AddCommand(GetCmd)
+ Cmd.AddCommand(UpdateCmd)
+ Cmd.AddCommand(DeleteCmd)
+ Cmd.AddCommand(TimelineCmd)
+}
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// TimelineCmd adds a timeline note to a person. Exported for potential use by shortcuts.
+var TimelineCmd = &cobra.Command{
+ Use: "timeline ID",
+ Short: "Add a timeline note to a person",
+ Long: `Add a timeline note to a person's memory timeline.
+
+Use "-" as content flag value to read from stdin.`,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement timeline note creation
+ fmt.Fprintf(cmd.OutOrStdout(), "Adding timeline note to person %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ TimelineCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)")
+ TimelineCmd.Flags().StringP("date", "d", "", "Date of interaction (natural language)")
+}
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package person
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// UpdateCmd updates a person. Exported for potential use by shortcuts.
+var UpdateCmd = &cobra.Command{
+ Use: "update ID",
+ Short: "Update a person",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement person update
+ fmt.Fprintf(cmd.OutOrStdout(), "Updating person %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ UpdateCmd.Flags().String("first", "", "First name")
+ UpdateCmd.Flags().String("last", "", "Last name")
+ UpdateCmd.Flags().StringP("relationship", "r", "", "Relationship strength")
+
+ _ = UpdateCmd.RegisterFlagCompletionFunc("relationship", completeRelationships)
+}
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+
+ "git.secluded.site/lune/internal/client"
+ "git.secluded.site/lune/internal/ui"
+ "github.com/spf13/cobra"
+)
+
+var pingCmd = &cobra.Command{
+ Use: "ping",
+ Short: "Verify your API key is valid",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ c, err := client.New()
+ if err != nil {
+ if errors.Is(err, client.ErrNoAPIKey) {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("LUNATASK_API_KEY not set"))
+
+ return err
+ }
+
+ return err
+ }
+
+ resp, err := c.Ping(cmd.Context())
+ if err != nil {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Authentication failed"))
+
+ return err
+ }
+
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("✓ "+resp.Message))
+
+ return nil
+ },
+}
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package cmd provides the lune CLI commands.
+package cmd
+
+import (
+ "context"
+ "os"
+
+ "git.secluded.site/lune/cmd/habit"
+ "git.secluded.site/lune/cmd/journal"
+ "git.secluded.site/lune/cmd/note"
+ "git.secluded.site/lune/cmd/person"
+ "git.secluded.site/lune/cmd/task"
+ "github.com/charmbracelet/fang"
+ "github.com/spf13/cobra"
+)
+
+var (
+ version = "dev"
+ commit = "none"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "lune",
+ Short: "A delightful CLI for Lunatask",
+ Long: `lune is a command-line interface for Lunatask, the encrypted
+all-in-one productivity app for tasks, habits, journaling, and more.
+
+Set LUNATASK_API_KEY in your environment to authenticate.`,
+ SilenceUsage: true,
+ SilenceErrors: true,
+}
+
+func init() {
+ rootCmd.AddGroup(&cobra.Group{ID: "resources", Title: "Resources"})
+ rootCmd.AddGroup(&cobra.Group{ID: "shortcuts", Title: "Shortcuts"})
+
+ rootCmd.AddCommand(initCmd)
+ rootCmd.AddCommand(pingCmd)
+
+ rootCmd.AddCommand(task.Cmd)
+ rootCmd.AddCommand(note.Cmd)
+ rootCmd.AddCommand(person.Cmd)
+ rootCmd.AddCommand(journal.Cmd)
+ rootCmd.AddCommand(habit.Cmd)
+
+ rootCmd.AddCommand(addCmd)
+ rootCmd.AddCommand(doneCmd)
+ rootCmd.AddCommand(jrnlCmd)
+}
+
+// Execute runs the root command with Fang styling.
+func Execute(ctx context.Context) error {
+ return fang.Execute(
+ ctx,
+ rootCmd,
+ fang.WithVersion(version),
+ fang.WithCommit(commit),
+ fang.WithNotifySignal(os.Interrupt),
+ )
+}
@@ -0,0 +1,92 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package task
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/config"
+ "github.com/spf13/cobra"
+)
+
+// AddCmd creates a new task. Exported for use by the add shortcut.
+var AddCmd = &cobra.Command{
+ Use: "add NAME",
+ Short: "Create a new task",
+ Long: `Create a new task in Lunatask.
+
+The task name is required. Use flags to set additional properties.
+Use "-" as NAME to read the task name from stdin.`,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: implement task creation
+ name := args[0]
+ fmt.Fprintf(cmd.OutOrStdout(), "Creating task: %s (not yet implemented)\n", name)
+
+ return nil
+ },
+}
+
+func init() {
+ AddCmd.Flags().StringP("area", "a", "", "Area key (from config)")
+ AddCmd.Flags().StringP("goal", "g", "", "Goal key (from config)")
+ AddCmd.Flags().StringP("status", "s", "", "Status: later, next, started, waiting")
+ AddCmd.Flags().StringP("note", "n", "", "Task note (use - for stdin)")
+ AddCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2")
+ AddCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)")
+ AddCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want")
+ AddCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4")
+ AddCmd.Flags().String("schedule", "", "Schedule date (natural language)")
+
+ _ = AddCmd.RegisterFlagCompletionFunc("area", completeAreas)
+ _ = AddCmd.RegisterFlagCompletionFunc("goal", completeGoals)
+ _ = AddCmd.RegisterFlagCompletionFunc("status",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"later", "next", "started", "waiting"}, cobra.ShellCompDirectiveNoFileComp
+ })
+ _ = AddCmd.RegisterFlagCompletionFunc("motivation",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"must", "should", "want"}, cobra.ShellCompDirectiveNoFileComp
+ })
+ _ = AddCmd.RegisterFlagCompletionFunc("eisenhower",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"1", "2", "3", "4"}, cobra.ShellCompDirectiveNoFileComp
+ })
+}
+
+// completeAreas returns area keys from config for shell completion.
+func completeAreas(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ cfg, err := config.Load()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ keys := make([]string, len(cfg.Areas))
+ for i, a := range cfg.Areas {
+ keys[i] = a.Key
+ }
+
+ return keys, cobra.ShellCompDirectiveNoFileComp
+}
+
+// completeGoals returns goal keys from config for shell completion.
+// Note: This returns all goals across all areas. A smarter completion
+// would filter based on the --area flag value if set.
+func completeGoals(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ cfg, err := config.Load()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ var keys []string
+
+ for _, a := range cfg.Areas {
+ for _, g := range a.Goals {
+ keys = append(keys, g.Key)
+ }
+ }
+
+ return keys, cobra.ShellCompDirectiveNoFileComp
+}
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package task
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "git.secluded.site/lune/internal/ui"
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// DeleteCmd deletes a task. Exported for potential use by shortcuts.
+var DeleteCmd = &cobra.Command{
+ Use: "delete ID",
+ Short: "Delete a task",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ force, _ := cmd.Flags().GetBool("force")
+ if !force {
+ fmt.Fprintf(cmd.OutOrStderr(), "%s Delete task %s? [y/N] ",
+ ui.Warning.Render("Warning:"), args[0])
+
+ reader := bufio.NewReader(os.Stdin)
+ response, _ := reader.ReadString('\n')
+ response = strings.TrimSpace(strings.ToLower(response))
+
+ if response != "y" && response != "yes" {
+ fmt.Fprintln(cmd.OutOrStdout(), "Cancelled")
+
+ return nil
+ }
+ }
+
+ // TODO: implement task deletion
+ fmt.Fprintf(cmd.OutOrStdout(), "Deleting task %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
+}
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package task
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// GetCmd retrieves a task by ID. Exported for potential use by shortcuts.
+var GetCmd = &cobra.Command{
+ Use: "get ID",
+ Short: "Get a task by ID",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement task get
+ fmt.Fprintf(cmd.OutOrStdout(), "Getting task %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ GetCmd.Flags().Bool("json", false, "Output as JSON")
+}
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package task
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// ListCmd lists tasks. Exported for potential use by shortcuts.
+var ListCmd = &cobra.Command{
+ Use: "list",
+ Short: "List tasks",
+ Long: `List tasks from Lunatask.
+
+Note: Due to end-to-end encryption, task names and notes
+are not available through the API. Only metadata is shown.`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ // TODO: implement task listing
+ fmt.Fprintln(cmd.OutOrStdout(), "Task listing not yet implemented")
+
+ return nil
+ },
+}
+
+func init() {
+ ListCmd.Flags().StringP("area", "a", "", "Filter by area key")
+ ListCmd.Flags().StringP("status", "s", "", "Filter by status")
+ ListCmd.Flags().Bool("json", false, "Output as JSON")
+
+ _ = ListCmd.RegisterFlagCompletionFunc("area", completeAreas)
+ _ = ListCmd.RegisterFlagCompletionFunc("status",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"later", "next", "started", "waiting", "completed"}, cobra.ShellCompDirectiveNoFileComp
+ })
+}
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package task provides commands for managing Lunatask tasks.
+package task
+
+import "github.com/spf13/cobra"
+
+// Cmd is the parent command for task operations.
+var Cmd = &cobra.Command{
+ Use: "task",
+ Short: "Manage tasks",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(AddCmd)
+ Cmd.AddCommand(ListCmd)
+ Cmd.AddCommand(GetCmd)
+ Cmd.AddCommand(UpdateCmd)
+ Cmd.AddCommand(DeleteCmd)
+}
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package task
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/validate"
+ "github.com/spf13/cobra"
+)
+
+// UpdateCmd updates a task. Exported for potential use by shortcuts.
+var UpdateCmd = &cobra.Command{
+ Use: "update ID",
+ Short: "Update a task",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := validate.ID(args[0]); err != nil {
+ return err
+ }
+
+ // TODO: implement task update
+ fmt.Fprintf(cmd.OutOrStdout(), "Updating task %s (not yet implemented)\n", args[0])
+
+ return nil
+ },
+}
+
+func init() {
+ UpdateCmd.Flags().String("name", "", "New task name")
+ UpdateCmd.Flags().StringP("area", "a", "", "Move to area key")
+ UpdateCmd.Flags().StringP("goal", "g", "", "Move to goal key")
+ UpdateCmd.Flags().StringP("status", "s", "", "Status: later, next, started, waiting, completed")
+ UpdateCmd.Flags().StringP("note", "n", "", "Task note (use - for stdin)")
+ UpdateCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2")
+ UpdateCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)")
+ UpdateCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want")
+ UpdateCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4")
+ UpdateCmd.Flags().String("schedule", "", "Schedule date (natural language)")
+
+ _ = UpdateCmd.RegisterFlagCompletionFunc("area", completeAreas)
+ _ = UpdateCmd.RegisterFlagCompletionFunc("goal", completeGoals)
+ _ = UpdateCmd.RegisterFlagCompletionFunc("status",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"later", "next", "started", "waiting", "completed"}, cobra.ShellCompDirectiveNoFileComp
+ })
+ _ = UpdateCmd.RegisterFlagCompletionFunc("motivation",
+ func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+ return []string{"must", "should", "want"}, cobra.ShellCompDirectiveNoFileComp
+ })
+}
@@ -0,0 +1,44 @@
+module git.secluded.site/lune
+
+go 1.25.5
+
+require (
+ git.secluded.site/go-lunatask v0.1.0-rc7
+ github.com/BurntSushi/toml v1.6.0
+ github.com/charmbracelet/fang v0.4.4
+ github.com/charmbracelet/lipgloss v1.1.0
+ github.com/google/uuid v1.6.0
+ github.com/spf13/cobra v1.10.2
+)
+
+require (
+ charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/colorprofile v0.4.1 // indirect
+ github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 // indirect
+ github.com/charmbracelet/x/ansi v0.11.3 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
+ github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383 // indirect
+ github.com/charmbracelet/x/term v0.2.2 // indirect
+ github.com/charmbracelet/x/termios v0.1.1 // indirect
+ github.com/charmbracelet/x/windows v0.2.2 // indirect
+ github.com/clipperhouse/displaywidth v0.6.2 // indirect
+ github.com/clipperhouse/stringish v0.1.1 // indirect
+ github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.19 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/mango v0.2.0 // indirect
+ github.com/muesli/mango-cobra v1.3.0 // indirect
+ github.com/muesli/mango-pflag v0.2.0 // indirect
+ github.com/muesli/roff v0.1.0 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
+)
@@ -0,0 +1,90 @@
+charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
+charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
+git.secluded.site/go-lunatask v0.1.0-rc7 h1:kzwAN9h4zVTo0OBs4B23ba5mAqxus6nYU62LElUkdnw=
+git.secluded.site/go-lunatask v0.1.0-rc7/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
+github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
+github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
+github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
+github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
+github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 h1:dsDBRP9Iyco0EjVpCsAzl8VGbxk04fP3sa80ySJSAZw=
+github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9/go.mod h1:Ns3cOzzY9hEFFeGxB6VpfgRnqOJZJFhQAPfRxPqflQs=
+github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
+github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
+github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
+github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
+github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383 h1:xGojlO6kHCDB1k6DolME79LG0u90TzVd8atGhmxFRIo=
+github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
+github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
+github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
+github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
+github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
+github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
+github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
+github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
+github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
+github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k=
+github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y=
+github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
+github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package client provides a configured Lunatask API client.
+package client
+
+import (
+ "errors"
+ "os"
+ "runtime/debug"
+
+ "git.secluded.site/go-lunatask"
+)
+
+// EnvAPIKey is the environment variable name for the Lunatask API key.
+const EnvAPIKey = "LUNATASK_API_KEY" //nolint:gosec // not a credential, just env var name
+
+// ErrNoAPIKey indicates the API key environment variable is not set.
+var ErrNoAPIKey = errors.New("LUNATASK_API_KEY environment variable not set")
+
+// New creates a Lunatask client from the LUNATASK_API_KEY environment variable.
+func New() (*lunatask.Client, error) {
+ token := os.Getenv(EnvAPIKey)
+ if token == "" {
+ return nil, ErrNoAPIKey
+ }
+
+ return lunatask.NewClient(token, lunatask.UserAgent("lune/"+version())), nil
+}
+
+// version returns the module version from build info, or "dev" if unavailable.
+func version() string {
+ info, ok := debug.ReadBuildInfo()
+ if !ok || info.Main.Version == "" || info.Main.Version == "(devel)" {
+ return "dev"
+ }
+
+ return info.Main.Version
+}
@@ -0,0 +1,169 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package config handles loading and saving lune configuration from TOML.
+package config
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/BurntSushi/toml"
+)
+
+// ErrNotFound indicates the config file doesn't exist.
+var ErrNotFound = errors.New("config file not found")
+
+// Config represents the lune configuration file structure.
+type Config struct {
+ UI UIConfig `toml:"ui"`
+ Defaults Defaults `toml:"defaults"`
+ Areas []Area `toml:"areas"`
+ Notebooks []Notebook `toml:"notebooks"`
+ Habits []Habit `toml:"habits"`
+}
+
+// UIConfig holds user interface preferences.
+type UIConfig struct {
+ Color string `toml:"color"` // "always", "never", "auto"
+}
+
+// Defaults holds default selections for commands.
+type Defaults struct {
+ Area string `toml:"area"`
+ Notebook string `toml:"notebook"`
+}
+
+// Area represents a Lunatask area of life with its goals.
+type Area struct {
+ ID string `toml:"id"`
+ Name string `toml:"name"`
+ Key string `toml:"key"`
+ Goals []Goal `toml:"goals"`
+}
+
+// Goal represents a goal within an area.
+type Goal struct {
+ ID string `toml:"id"`
+ Name string `toml:"name"`
+ Key string `toml:"key"`
+}
+
+// Notebook represents a Lunatask notebook for notes.
+type Notebook struct {
+ ID string `toml:"id"`
+ Name string `toml:"name"`
+ Key string `toml:"key"`
+}
+
+// Habit represents a trackable habit.
+type Habit struct {
+ ID string `toml:"id"`
+ Name string `toml:"name"`
+ Key string `toml:"key"`
+}
+
+// Path returns the path to the config file.
+func Path() (string, error) {
+ configDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", fmt.Errorf("getting config dir: %w", err)
+ }
+
+ return filepath.Join(configDir, "lunatask", "config.toml"), nil
+}
+
+// Load reads the config file. Returns ErrNotFound if the file doesn't exist.
+func Load() (*Config, error) {
+ path, err := Path()
+ if err != nil {
+ return nil, err
+ }
+
+ var cfg Config
+ if _, err := toml.DecodeFile(path, &cfg); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, ErrNotFound
+ }
+
+ return nil, fmt.Errorf("decoding config: %w", err)
+ }
+
+ return &cfg, nil
+}
+
+// Save writes the config to disk.
+func (c *Config) Save() error {
+ path, err := Path()
+ if err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
+ return fmt.Errorf("creating config dir: %w", err)
+ }
+
+ f, err := os.Create(path) //nolint:gosec // path is from user config dir
+ if err != nil {
+ return fmt.Errorf("creating config file: %w", err)
+ }
+
+ if err := toml.NewEncoder(f).Encode(c); err != nil {
+ _ = f.Close()
+
+ return fmt.Errorf("encoding config: %w", err)
+ }
+
+ if err := f.Close(); err != nil {
+ return fmt.Errorf("closing config file: %w", err)
+ }
+
+ return nil
+}
+
+// AreaByKey finds an area by its key.
+func (c *Config) AreaByKey(key string) *Area {
+ for i := range c.Areas {
+ if c.Areas[i].Key == key {
+ return &c.Areas[i]
+ }
+ }
+
+ return nil
+}
+
+// NotebookByKey finds a notebook by its key.
+func (c *Config) NotebookByKey(key string) *Notebook {
+ for i := range c.Notebooks {
+ if c.Notebooks[i].Key == key {
+ return &c.Notebooks[i]
+ }
+ }
+
+ return nil
+}
+
+// HabitByKey finds a habit by its key.
+func (c *Config) HabitByKey(key string) *Habit {
+ for i := range c.Habits {
+ if c.Habits[i].Key == key {
+ return &c.Habits[i]
+ }
+ }
+
+ return nil
+}
+
+// GoalByKey finds a goal within this area by its key.
+func (a *Area) GoalByKey(key string) *Goal {
+ for i := range a.Goals {
+ if a.Goals[i].Key == key {
+ return &a.Goals[i]
+ }
+ }
+
+ return nil
+}
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package ui provides lipgloss styles for terminal output.
+package ui
+
+import "github.com/charmbracelet/lipgloss"
+
+// Terminal output styles using ANSI colors for broad compatibility.
+var (
+ Success = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green
+ Warning = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
+ Error = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red
+ Muted = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray
+ Bold = lipgloss.NewStyle().Bold(true)
+)
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package validate provides input validation helpers.
+package validate
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/google/uuid"
+)
+
+// ErrInvalidID indicates an ID is not a valid UUID.
+var ErrInvalidID = errors.New("invalid ID: expected UUID format")
+
+// ID validates that the given string is a valid Lunatask ID (UUID).
+func ID(id string) error {
+ if _, err := uuid.Parse(id); err != nil {
+ return fmt.Errorf("%w: %s", ErrInvalidID, id)
+ }
+
+ return nil
+}
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package main is the entry point for the lune CLI.
+package main
+
+import (
+ "context"
+ "os"
+
+ "git.secluded.site/lune/cmd"
+)
+
+func main() {
+ if err := cmd.Execute(context.Background()); err != nil {
+ os.Exit(1)
+ }
+}