Detailed changes
@@ -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)
+ }
+}
@@ -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
}
@@ -1,34 +0,0 @@
-// 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 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")
-}
@@ -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
}
@@ -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)
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// 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
+}
@@ -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
+}
@@ -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
+}