A terminal client for Tangled.
0

Configure Feed

Select the types of activity you want to include in your feed.

cli/issue: add write commands

Aly Raffauf (Jul 15, 2026, 12:13 PM EDT) cce6d60b 7ad41a81

+203
+64
internal/cli/issue_comment.go
··· 1 + package cli 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/alyraffauf/tg/tangled" 7 + "github.com/spf13/cobra" 8 + ) 9 + 10 + var ( 11 + issueCommentBody string 12 + issueCommentBodyFile string 13 + issueCommentRepo string 14 + ) 15 + 16 + var issueCommentCmd = &cobra.Command{ 17 + Use: "comment <rkey>", 18 + Short: "Add a comment to an issue", 19 + Args: cobra.ExactArgs(1), 20 + RunE: func(cmd *cobra.Command, args []string) error { 21 + body, err := commandBody(issueCommentBody, issueCommentBodyFile) 22 + if err != nil { 23 + return err 24 + } 25 + if body == "" { 26 + return fmt.Errorf("set --body or --body-file") 27 + } 28 + ctx := cmd.Context() 29 + targetArgs := []string{} 30 + if issueCommentRepo != "" { 31 + targetArgs = []string{issueCommentRepo} 32 + } 33 + handle, name, err := resolveTarget(ctx, targetArgs) 34 + if err != nil { 35 + return err 36 + } 37 + repoDid, err := findRepoDid(ctx, handle, name) 38 + if err != nil { 39 + return err 40 + } 41 + issues, err := client.ListIssues(ctx, repoDid, tangled.ListOpts{Limit: defaultListLimit}) 42 + if err != nil { 43 + return fmt.Errorf("list issues for %s/%s: %w", handle, name, err) 44 + } 45 + issue, err := findByRKey(issues.Items, args[0], "issue") 46 + if err != nil { 47 + return err 48 + } 49 + 50 + result, err := createIssueComment(ctx, issue.URI, body) 51 + if err != nil { 52 + return err 53 + } 54 + return output(result, func(result createdRecordResult) { 55 + fmt.Printf("Added comment %s\n", result.URI) 56 + }) 57 + }, 58 + } 59 + 60 + func init() { 61 + issueCommentCmd.Flags().StringVarP(&issueCommentBody, "body", "b", "", "Comment body") 62 + issueCommentCmd.Flags().StringVarP(&issueCommentBodyFile, "body-file", "F", "", "Read comment body from file") 63 + issueCommentCmd.Flags().StringVarP(&issueCommentRepo, "repo", "R", "", "Target repository as handle/repo") 64 + }
+74
internal/cli/issue_create.go
··· 1 + package cli 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/alyraffauf/tg/atproto" 8 + "github.com/alyraffauf/tg/tangled" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/spf13/cobra" 11 + ) 12 + 13 + var ( 14 + issueCreateBody string 15 + issueCreateBodyFile string 16 + issueCreateRepo string 17 + ) 18 + 19 + var issueCreateCmd = &cobra.Command{ 20 + Use: "create <title>", 21 + Short: "Create an issue on a Tangled repository", 22 + Args: cobra.ExactArgs(1), 23 + RunE: func(cmd *cobra.Command, args []string) error { 24 + ctx := cmd.Context() 25 + body, err := commandBody(issueCreateBody, issueCreateBodyFile) 26 + if err != nil { 27 + return err 28 + } 29 + atClient, did, err := authenticatedATProto(ctx) 30 + if err != nil { 31 + return err 32 + } 33 + 34 + targetArgs := []string{} 35 + if issueCreateRepo != "" { 36 + targetArgs = []string{issueCreateRepo} 37 + } 38 + handle, name, err := resolveTarget(ctx, targetArgs) 39 + if err != nil { 40 + return err 41 + } 42 + repoDid, err := findRepoDid(ctx, handle, name) 43 + if err != nil { 44 + return err 45 + } 46 + 47 + rkey := string(syntax.NewTIDNow(0)) 48 + uri, _, err := atClient.PutRecord(ctx, atproto.PutRecordInput{ 49 + Repo: did, 50 + Collection: "sh.tangled.repo.issue", 51 + Rkey: rkey, 52 + Record: tangled.IssueRecord{ 53 + Type: "sh.tangled.repo.issue", 54 + Repo: repoDid, 55 + Title: args[0], 56 + Body: body, 57 + CreatedAt: time.Now().UTC().Format(time.RFC3339), 58 + }, 59 + }) 60 + if err != nil { 61 + return fmt.Errorf("create issue: %w", err) 62 + } 63 + 64 + return output(createdRecordResult{Rkey: rkey, URI: uri}, func(result createdRecordResult) { 65 + fmt.Printf("Created issue %s\n", result.URI) 66 + }) 67 + }, 68 + } 69 + 70 + func init() { 71 + issueCreateCmd.Flags().StringVarP(&issueCreateBody, "body", "b", "", "Issue body") 72 + issueCreateCmd.Flags().StringVarP(&issueCreateBodyFile, "body-file", "F", "", "Read issue body from file") 73 + issueCreateCmd.Flags().StringVarP(&issueCreateRepo, "repo", "R", "", "Target repository as handle/repo") 74 + }
+65
internal/cli/issue_state.go
··· 1 + package cli 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/spf13/cobra" 7 + ) 8 + 9 + var ( 10 + issueStateRepo string 11 + issueEditTitle string 12 + issueEditBody string 13 + ) 14 + 15 + var issueCloseCmd = newIssueStateCmd("close", "closed") 16 + var issueReopenCmd = newIssueStateCmd("reopen", "open") 17 + 18 + var issueEditCmd = &cobra.Command{ 19 + Use: "edit <rkey>", 20 + Short: "Edit an issue", 21 + Args: cobra.ExactArgs(1), 22 + RunE: func(cmd *cobra.Command, args []string) error { 23 + setTitle := cmd.Flags().Changed("title") 24 + setBody := cmd.Flags().Changed("body") 25 + if !setTitle && !setBody { 26 + return fmt.Errorf("set --title or --body") 27 + } 28 + atClient, did, err := authenticatedATProto(cmd.Context()) 29 + if err != nil { 30 + return err 31 + } 32 + return editRecord(cmd.Context(), atClient, did, issueCollection, args[0], issueEditTitle, issueEditBody, setTitle, setBody) 33 + }, 34 + } 35 + 36 + func newIssueStateCmd(use, state string) *cobra.Command { 37 + return &cobra.Command{ 38 + Use: use + " <rkey>", 39 + Short: use + " an issue", 40 + Args: cobra.ExactArgs(1), 41 + RunE: func(cmd *cobra.Command, args []string) error { 42 + atClient, did, err := authenticatedATProto(cmd.Context()) 43 + if err != nil { 44 + return err 45 + } 46 + target, _, err := targetRecord(cmd.Context(), issueStateRepo, issueCollection, args[0]) 47 + if err != nil { 48 + return err 49 + } 50 + if err := putState(cmd.Context(), atClient, did, args[0], issueCollection, target, state); err != nil { 51 + return fmt.Errorf("%s issue: %w", use, err) 52 + } 53 + return output(stateResult{Rkey: args[0], State: state}, func(result stateResult) { 54 + fmt.Printf("Issue %s %s\n", result.Rkey, result.State) 55 + }) 56 + }, 57 + } 58 + } 59 + 60 + func init() { 61 + issueCloseCmd.Flags().StringVarP(&issueStateRepo, "repo", "R", "", "Target repository as handle/repo") 62 + issueReopenCmd.Flags().StringVarP(&issueStateRepo, "repo", "R", "", "Target repository as handle/repo") 63 + issueEditCmd.Flags().StringVarP(&issueEditTitle, "title", "t", "", "New title") 64 + issueEditCmd.Flags().StringVarP(&issueEditBody, "body", "b", "", "New body") 65 + }