diff --git a/cmd/person/add.go b/cmd/person/add.go index a6e02fc19f7aa243b7d633f05a83b767b85a90d2..f25edff380ddc7ca4301cbd1045945ba466aec24 100644 --- a/cmd/person/add.go +++ b/cmd/person/add.go @@ -7,7 +7,11 @@ package person import ( "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/ui" + "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -15,17 +19,69 @@ import ( 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]) + Long: `Add a new person to the Lunatask relationship tracker. - return nil - }, +Names are required. Use flags to set additional properties.`, + Args: cobra.ExactArgs(2), + RunE: runAdd, } func init() { AddCmd.Flags().StringP("relationship", "r", "", "Relationship strength") + AddCmd.Flags().String("source", "", "Source identifier") + AddCmd.Flags().String("source-id", "", "Source ID") _ = AddCmd.RegisterFlagCompletionFunc("relationship", completion.Relationships) } + +func runAdd(cmd *cobra.Command, args []string) error { + firstName := args[0] + lastName := args[1] + + apiClient, err := client.New() + if err != nil { + return err + } + + builder := apiClient.NewPerson(firstName, lastName) + + if err := applyRelationship(cmd, builder); err != nil { + return err + } + + applySource(cmd, builder) + + person, err := builder.Create(cmd.Context()) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created person: "+person.ID)) + + return nil +} + +func applyRelationship(cmd *cobra.Command, builder *lunatask.PersonBuilder) error { + rel, _ := cmd.Flags().GetString("relationship") + if rel == "" { + return nil + } + + strength, err := validate.RelationshipStrength(rel) + if err != nil { + return err + } + + builder.WithRelationshipStrength(strength) + + return nil +} + +func applySource(cmd *cobra.Command, builder *lunatask.PersonBuilder) { + source, _ := cmd.Flags().GetString("source") + sourceID, _ := cmd.Flags().GetString("source-id") + + if source != "" && sourceID != "" { + builder.FromSource(source, sourceID) + } +} diff --git a/cmd/person/delete.go b/cmd/person/delete.go index ca56f3381541f9eb53f8e2a6b2ab3cf694a1e236..b57bb858a8e09530316a75f9e76b58fd6fa5be8c 100644 --- a/cmd/person/delete.go +++ b/cmd/person/delete.go @@ -7,6 +7,7 @@ package person import ( "fmt" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" @@ -16,29 +17,43 @@ import ( var DeleteCmd = &cobra.Command{ Use: "delete ID", Short: "Delete a person", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } + Long: `Delete a person from Lunatask. + +Accepts a UUID or lunatask:// deep link.`, + Args: cobra.ExactArgs(1), + RunE: runDelete, +} + +func init() { + DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt") +} + +func runDelete(cmd *cobra.Command, args []string) error { + id, err := validate.Reference(args[0]) + if err != nil { + return err + } - force, _ := cmd.Flags().GetBool("force") - if !force { - if !ui.Confirm(fmt.Sprintf("Delete person %s?", id)) { - fmt.Fprintln(cmd.OutOrStdout(), "Cancelled") + force, _ := cmd.Flags().GetBool("force") + if !force { + if !ui.Confirm(fmt.Sprintf("Delete person %s?", id)) { + fmt.Fprintln(cmd.OutOrStdout(), "Cancelled") - return nil - } + return nil } + } - // TODO: implement person deletion - fmt.Fprintf(cmd.OutOrStdout(), "Deleting person %s (not yet implemented)\n", id) + apiClient, err := client.New() + if err != nil { + return err + } - return nil - }, -} + person, err := apiClient.DeletePerson(cmd.Context(), id) + if err != nil { + return err + } -func init() { - DeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt") + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Deleted person: "+person.ID)) + + return nil } diff --git a/cmd/person/get.go b/cmd/person/get.go deleted file mode 100644 index 08432af76b8ec0fe2d4991b053bb2e1f0c6f4da5..0000000000000000000000000000000000000000 --- a/cmd/person/get.go +++ /dev/null @@ -1,34 +0,0 @@ -// 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 or reference", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } - - // TODO: implement person get - fmt.Fprintf(cmd.OutOrStdout(), "Getting person %s (not yet implemented)\n", id) - - return nil - }, -} - -func init() { - GetCmd.Flags().Bool("json", false, "Output as JSON") -} diff --git a/cmd/person/list.go b/cmd/person/list.go index 1bb4d0bcb9ebfc73a50c7286504be548234916c7..74d8246e974c9902ea2b2271b4194479e87bd56b 100644 --- a/cmd/person/list.go +++ b/cmd/person/list.go @@ -5,8 +5,15 @@ package person import ( + "encoding/json" "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" + "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/ui" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" "github.com/spf13/cobra" ) @@ -18,14 +25,128 @@ var ListCmd = &cobra.Command{ 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 - }, + RunE: runList, } func init() { + ListCmd.Flags().StringP("relationship", "r", "", "Filter by relationship strength") + ListCmd.Flags().String("source", "", "Filter by source") + ListCmd.Flags().String("source-id", "", "Filter by source ID") ListCmd.Flags().Bool("json", false, "Output as JSON") + + _ = ListCmd.RegisterFlagCompletionFunc("relationship", completion.Relationships) +} + +func runList(cmd *cobra.Command, _ []string) error { + apiClient, err := client.New() + if err != nil { + return err + } + + opts := buildListOptions(cmd) + + people, err := apiClient.ListPeople(cmd.Context(), opts) + if err != nil { + return err + } + + relFilter, _ := cmd.Flags().GetString("relationship") + if relFilter != "" { + people = filterByRelationship(people, relFilter) + } + + if len(people) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No people found") + + return nil + } + + if mustGetBoolFlag(cmd, "json") { + return outputJSON(cmd, people) + } + + return outputTable(cmd, people) +} + +func buildListOptions(cmd *cobra.Command) *lunatask.ListPeopleOptions { + source, _ := cmd.Flags().GetString("source") + sourceID, _ := cmd.Flags().GetString("source-id") + + if source == "" && sourceID == "" { + return nil + } + + opts := &lunatask.ListPeopleOptions{} + if source != "" { + opts.Source = &source + } + + if sourceID != "" { + opts.SourceID = &sourceID + } + + return opts +} + +func filterByRelationship(people []lunatask.Person, rel string) []lunatask.Person { + filtered := make([]lunatask.Person, 0, len(people)) + + for _, person := range people { + if person.RelationshipStrength != nil && string(*person.RelationshipStrength) == rel { + filtered = append(filtered, person) + } + } + + return filtered +} + +func mustGetBoolFlag(cmd *cobra.Command, name string) bool { + f := cmd.Flags().Lookup(name) + if f == nil { + panic("flag not defined: " + name) + } + + return f.Value.String() == "true" +} + +func outputJSON(cmd *cobra.Command, people []lunatask.Person) error { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + + if err := enc.Encode(people); err != nil { + return fmt.Errorf("encoding JSON: %w", err) + } + + return nil +} + +func outputTable(cmd *cobra.Command, people []lunatask.Person) error { + rows := make([][]string, 0, len(people)) + + for _, person := range people { + rel := "-" + if person.RelationshipStrength != nil { + rel = string(*person.RelationshipStrength) + } + + created := ui.FormatDate(person.CreatedAt) + + rows = append(rows, []string{person.ID, rel, created}) + } + + tbl := table.New(). + Headers("ID", "RELATIONSHIP", "CREATED"). + Rows(rows...). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return ui.Bold + } + + return lipgloss.NewStyle() + }). + Border(lipgloss.HiddenBorder()) + + fmt.Fprintln(cmd.OutOrStdout(), tbl.Render()) + + return nil } diff --git a/cmd/person/person.go b/cmd/person/person.go index caced528e54bacad73dd9d4e50ffd88c6434463a..79297a2bd3732e1e779d8bac613f28e24dd6b8cb 100644 --- a/cmd/person/person.go +++ b/cmd/person/person.go @@ -17,7 +17,7 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(AddCmd) Cmd.AddCommand(ListCmd) - Cmd.AddCommand(GetCmd) + Cmd.AddCommand(ShowCmd) Cmd.AddCommand(UpdateCmd) Cmd.AddCommand(DeleteCmd) Cmd.AddCommand(TimelineCmd) diff --git a/cmd/person/show.go b/cmd/person/show.go new file mode 100644 index 0000000000000000000000000000000000000000..b7174901298990966b49b80d5ceaf4d757aef2bf --- /dev/null +++ b/cmd/person/show.go @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package person + +import ( + "encoding/json" + "fmt" + + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" + "git.secluded.site/lune/internal/deeplink" + "git.secluded.site/lune/internal/ui" + "git.secluded.site/lune/internal/validate" + "github.com/spf13/cobra" +) + +// ShowCmd displays a person by ID. Exported for potential use by shortcuts. +var ShowCmd = &cobra.Command{ + Use: "show ID", + Short: "Show person details", + Long: `Show detailed information for a person. + +Accepts a UUID or lunatask:// deep link. + +Note: Due to end-to-end encryption, names are not available +through the API. Only metadata is shown.`, + Args: cobra.ExactArgs(1), + RunE: runShow, +} + +func init() { + ShowCmd.Flags().Bool("json", false, "Output as JSON") +} + +func runShow(cmd *cobra.Command, args []string) error { + id, err := validate.Reference(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + person, err := apiClient.GetPerson(cmd.Context(), id) + if err != nil { + return err + } + + if mustGetBoolFlag(cmd, "json") { + return outputPersonJSON(cmd, person) + } + + return printPersonDetails(cmd, person) +} + +func outputPersonJSON(cmd *cobra.Command, person *lunatask.Person) error { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + + if err := enc.Encode(person); err != nil { + return fmt.Errorf("encoding JSON: %w", err) + } + + return nil +} + +func printPersonDetails(cmd *cobra.Command, person *lunatask.Person) error { + link, _ := deeplink.Build(deeplink.Person, person.ID) + + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Person")) + fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", person.ID) + fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", link) + + if person.RelationshipStrength != nil { + fmt.Fprintf(cmd.OutOrStdout(), " Relationship: %s\n", *person.RelationshipStrength) + } + + if len(person.Sources) > 0 { + fmt.Fprintf(cmd.OutOrStdout(), " Sources:\n") + + for _, src := range person.Sources { + fmt.Fprintf(cmd.OutOrStdout(), " - %s: %s\n", src.Source, src.SourceID) + } + } + + fmt.Fprintf(cmd.OutOrStdout(), " Created: %s\n", ui.FormatDate(person.CreatedAt)) + fmt.Fprintf(cmd.OutOrStdout(), " Updated: %s\n", ui.FormatDate(person.UpdatedAt)) + + return nil +} diff --git a/cmd/person/timeline.go b/cmd/person/timeline.go index 80383e50f5caa17575501977f3eeccd7b8b8f2d7..60c46934e6605aee894c4f25ac60deeb413c64f0 100644 --- a/cmd/person/timeline.go +++ b/cmd/person/timeline.go @@ -6,7 +6,13 @@ package person import ( "fmt" + "os" + "strings" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" + "git.secluded.site/lune/internal/dateutil" + "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -19,20 +25,86 @@ var TimelineCmd = &cobra.Command{ Use "-" as content flag value to read from stdin.`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } - - // TODO: implement timeline note creation - fmt.Fprintf(cmd.OutOrStdout(), "Adding timeline note to person %s (not yet implemented)\n", id) - - return nil - }, + RunE: runTimeline, } func init() { TimelineCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)") TimelineCmd.Flags().StringP("date", "d", "", "Date of interaction (natural language)") } + +func runTimeline(cmd *cobra.Command, args []string) error { + personID, err := validate.Reference(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + builder := apiClient.NewTimelineNote(personID) + + if err := applyTimelineContent(cmd, builder); err != nil { + return err + } + + if err := applyTimelineDate(cmd, builder); err != nil { + return err + } + + note, err := builder.Create(cmd.Context()) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created timeline note: "+note.ID)) + + return nil +} + +func applyTimelineContent(cmd *cobra.Command, builder *lunatask.TimelineNoteBuilder) error { + content, _ := cmd.Flags().GetString("content") + if content == "" { + return nil + } + + resolved, err := resolveContent(content) + if err != nil { + return err + } + + builder.WithContent(resolved) + + return nil +} + +func resolveContent(content string) (string, error) { + if content != "-" { + return content, nil + } + + data, err := os.ReadFile("/dev/stdin") + if err != nil { + return "", fmt.Errorf("reading stdin: %w", err) + } + + return strings.TrimSpace(string(data)), nil +} + +func applyTimelineDate(cmd *cobra.Command, builder *lunatask.TimelineNoteBuilder) error { + dateStr, _ := cmd.Flags().GetString("date") + if dateStr == "" { + return nil + } + + date, err := dateutil.Parse(dateStr) + if err != nil { + return err + } + + builder.OnDate(date) + + return nil +} diff --git a/cmd/person/update.go b/cmd/person/update.go index de91ccff4414be14fe2f7a6514eef67e30b950d7..e1a6418c378e1db331be02ade15c29c887c514f8 100644 --- a/cmd/person/update.go +++ b/cmd/person/update.go @@ -7,7 +7,10 @@ package person import ( "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -16,18 +19,12 @@ import ( var UpdateCmd = &cobra.Command{ Use: "update ID", Short: "Update a person", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } + Long: `Update an existing person in Lunatask. - // TODO: implement person update - fmt.Fprintf(cmd.OutOrStdout(), "Updating person %s (not yet implemented)\n", id) - - return nil - }, +Accepts a UUID or lunatask:// deep link. +Only specified flags are modified; other fields remain unchanged.`, + Args: cobra.ExactArgs(1), + RunE: runUpdate, } func init() { @@ -37,3 +34,58 @@ func init() { _ = UpdateCmd.RegisterFlagCompletionFunc("relationship", completion.Relationships) } + +func runUpdate(cmd *cobra.Command, args []string) error { + id, err := validate.Reference(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + builder := apiClient.NewPersonUpdate(id) + + applyUpdateNames(cmd, builder) + + if err := applyUpdateRelationship(cmd, builder); err != nil { + return err + } + + person, err := builder.Update(cmd.Context()) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Updated person: "+person.ID)) + + return nil +} + +func applyUpdateNames(cmd *cobra.Command, builder *lunatask.PersonUpdateBuilder) { + if first, _ := cmd.Flags().GetString("first"); first != "" { + builder.FirstName(first) + } + + if last, _ := cmd.Flags().GetString("last"); last != "" { + builder.LastName(last) + } +} + +func applyUpdateRelationship(cmd *cobra.Command, builder *lunatask.PersonUpdateBuilder) error { + rel, _ := cmd.Flags().GetString("relationship") + if rel == "" { + return nil + } + + strength, err := validate.RelationshipStrength(rel) + if err != nil { + return err + } + + builder.WithRelationshipStrength(strength) + + return nil +}