diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000000000000000000000000000000000000..39834c872d78321117976376eaefec899597a933 --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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()) +} diff --git a/cmd/done.go b/cmd/done.go new file mode 100644 index 0000000000000000000000000000000000000000..ab147eab96789df2d2731db53a234d7a9ab78e4f --- /dev/null +++ b/cmd/done.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 + }, +} diff --git a/cmd/habit/habit.go b/cmd/habit/habit.go new file mode 100644 index 0000000000000000000000000000000000000000..c7fd4595240351ba1aaa9d6325007d83470a4fec --- /dev/null +++ b/cmd/habit/habit.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/habit/track.go b/cmd/habit/track.go new file mode 100644 index 0000000000000000000000000000000000000000..b193937497bb9ad4d5f10191f9e2c61d1d4cbe52 --- /dev/null +++ b/cmd/habit/track.go @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000000000000000000000000000000000000..5e48966512b1e7936e134b046078052e81c56a0b --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 + }, +} diff --git a/cmd/journal/add.go b/cmd/journal/add.go new file mode 100644 index 0000000000000000000000000000000000000000..9be6ea2ff8e5de8c91ef6d6f9a6a68239b8ec87b --- /dev/null +++ b/cmd/journal/add.go @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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)") +} diff --git a/cmd/journal/journal.go b/cmd/journal/journal.go new file mode 100644 index 0000000000000000000000000000000000000000..e2eb4d692cc5eb82a320309a7c7a875bede9ac03 --- /dev/null +++ b/cmd/journal/journal.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/jrnl.go b/cmd/jrnl.go new file mode 100644 index 0000000000000000000000000000000000000000..2cea2dbf4fba707aa74dd2aa2aefdb37f71fca86 --- /dev/null +++ b/cmd/jrnl.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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()) +} diff --git a/cmd/note/add.go b/cmd/note/add.go new file mode 100644 index 0000000000000000000000000000000000000000..d714491fa13eaea45b675b999e63ec49a1e416c6 --- /dev/null +++ b/cmd/note/add.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/cmd/note/delete.go b/cmd/note/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..e6594d2de124e3bae5f0527dc43a574a3e637385 --- /dev/null +++ b/cmd/note/delete.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/note/get.go b/cmd/note/get.go new file mode 100644 index 0000000000000000000000000000000000000000..e7390a87fb1634bf8d2a137c755af0af92701178 --- /dev/null +++ b/cmd/note/get.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/note/list.go b/cmd/note/list.go new file mode 100644 index 0000000000000000000000000000000000000000..a3abc0738503ba5c0a4f355b1dc964730048741b --- /dev/null +++ b/cmd/note/list.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/note/note.go b/cmd/note/note.go new file mode 100644 index 0000000000000000000000000000000000000000..a3bc6ca4023718450f908a9525a9df93d4c21d0d --- /dev/null +++ b/cmd/note/note.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/note/update.go b/cmd/note/update.go new file mode 100644 index 0000000000000000000000000000000000000000..e42da14aaa1f9126ba68a8c23d104068d74f2803 --- /dev/null +++ b/cmd/note/update.go @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/person/add.go b/cmd/person/add.go new file mode 100644 index 0000000000000000000000000000000000000000..b8aa8086787c5a48dfd5bcb0ca5c8b419c36a7bf --- /dev/null +++ b/cmd/person/add.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/cmd/person/delete.go b/cmd/person/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..103602e0228614071c6ab7b201081d47883a04eb --- /dev/null +++ b/cmd/person/delete.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/person/get.go b/cmd/person/get.go new file mode 100644 index 0000000000000000000000000000000000000000..24de858e95ddf8d8e1ba805566758569a856b3cd --- /dev/null +++ b/cmd/person/get.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/person/list.go b/cmd/person/list.go new file mode 100644 index 0000000000000000000000000000000000000000..1bb4d0bcb9ebfc73a50c7286504be548234916c7 --- /dev/null +++ b/cmd/person/list.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/person/person.go b/cmd/person/person.go new file mode 100644 index 0000000000000000000000000000000000000000..caced528e54bacad73dd9d4e50ffd88c6434463a --- /dev/null +++ b/cmd/person/person.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/person/timeline.go b/cmd/person/timeline.go new file mode 100644 index 0000000000000000000000000000000000000000..0b6d0e92497734489bc1236de56fff8e6454102d --- /dev/null +++ b/cmd/person/timeline.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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)") +} diff --git a/cmd/person/update.go b/cmd/person/update.go new file mode 100644 index 0000000000000000000000000000000000000000..bd3ed4672a9ada18b157cc0f62ceb9bc3dff9e9a --- /dev/null +++ b/cmd/person/update.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/ping.go b/cmd/ping.go new file mode 100644 index 0000000000000000000000000000000000000000..f762eb416645271cdde12273ac90990187f88381 --- /dev/null +++ b/cmd/ping.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000000000000000000000000000000000000..b82ccf579e8420748d4561b52d15d03f91a90682 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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), + ) +} diff --git a/cmd/task/add.go b/cmd/task/add.go new file mode 100644 index 0000000000000000000000000000000000000000..d0ed8c89b400d2946c64d6c462b5e59ed5a12ccb --- /dev/null +++ b/cmd/task/add.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/cmd/task/delete.go b/cmd/task/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..9bce05f3336cd93c446f1cfc4e7cc418493372fb --- /dev/null +++ b/cmd/task/delete.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/task/get.go b/cmd/task/get.go new file mode 100644 index 0000000000000000000000000000000000000000..376017d4657e2159f00756d69ec312250b77fc32 --- /dev/null +++ b/cmd/task/get.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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") +} diff --git a/cmd/task/list.go b/cmd/task/list.go new file mode 100644 index 0000000000000000000000000000000000000000..b1fee5b82c3d88078451cf139d6b3f17288dfe1b --- /dev/null +++ b/cmd/task/list.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 + }) +} diff --git a/cmd/task/task.go b/cmd/task/task.go new file mode 100644 index 0000000000000000000000000000000000000000..ca8deae2e1abe6d187d5f2e7e9dc2070a5757bfe --- /dev/null +++ b/cmd/task/task.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +} diff --git a/cmd/task/update.go b/cmd/task/update.go new file mode 100644 index 0000000000000000000000000000000000000000..36862d017d33dd7e9d14cc02562d758fca2cf236 --- /dev/null +++ b/cmd/task/update.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..cc5706879ef5a38f004b561d7dd467c431284499 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..3832e570abec44607885cf375ea6933b56094e5e --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000000000000000000000000000000000000..41b1e9a69087b5368b71359107a02ca883f060b8 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..1ed4e475bc7a8ecd3cfb8c0b45a97498e5cae282 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/internal/ui/styles.go b/internal/ui/styles.go new file mode 100644 index 0000000000000000000000000000000000000000..2c616e2a77251e727c6a3b60a13b97cbed8d0ee2 --- /dev/null +++ b/internal/ui/styles.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) +) diff --git a/internal/validate/validate.go b/internal/validate/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..0cd3d4de3ce4aa78c52b26be5995939387ba0bb5 --- /dev/null +++ b/internal/validate/validate.go @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..ef157be97882f220d3b038707dc80971b5d3d20e --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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) + } +}