superthread 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +492 -0
- data/exe/suth +19 -0
- data/lib/superthread/cli/accounts.rb +240 -0
- data/lib/superthread/cli/activity.rb +210 -0
- data/lib/superthread/cli/base.rb +355 -0
- data/lib/superthread/cli/boards.rb +131 -0
- data/lib/superthread/cli/cards.rb +530 -0
- data/lib/superthread/cli/checklists.rb +223 -0
- data/lib/superthread/cli/comments.rb +86 -0
- data/lib/superthread/cli/completion.rb +306 -0
- data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
- data/lib/superthread/cli/concerns/confirmable.rb +55 -0
- data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
- data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
- data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
- data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
- data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
- data/lib/superthread/cli/config.rb +129 -0
- data/lib/superthread/cli/formatter.rb +388 -0
- data/lib/superthread/cli/lists.rb +85 -0
- data/lib/superthread/cli/main.rb +121 -0
- data/lib/superthread/cli/members.rb +19 -0
- data/lib/superthread/cli/notes.rb +64 -0
- data/lib/superthread/cli/pages.rb +128 -0
- data/lib/superthread/cli/projects.rb +124 -0
- data/lib/superthread/cli/replies.rb +94 -0
- data/lib/superthread/cli/search.rb +34 -0
- data/lib/superthread/cli/setup.rb +253 -0
- data/lib/superthread/cli/spaces.rb +141 -0
- data/lib/superthread/cli/sprints.rb +32 -0
- data/lib/superthread/cli/tags.rb +86 -0
- data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
- data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
- data/lib/superthread/cli/ui.rb +263 -0
- data/lib/superthread/cli/workspaces.rb +105 -0
- data/lib/superthread/cli.rb +12 -0
- data/lib/superthread/client.rb +207 -0
- data/lib/superthread/configuration.rb +354 -0
- data/lib/superthread/connection.rb +57 -0
- data/lib/superthread/error.rb +164 -0
- data/lib/superthread/mention_formatter.rb +96 -0
- data/lib/superthread/model.rb +178 -0
- data/lib/superthread/models/board.rb +59 -0
- data/lib/superthread/models/card.rb +321 -0
- data/lib/superthread/models/checklist.rb +91 -0
- data/lib/superthread/models/checklist_item.rb +69 -0
- data/lib/superthread/models/comment.rb +71 -0
- data/lib/superthread/models/concerns/archivable.rb +32 -0
- data/lib/superthread/models/concerns/presentable.rb +113 -0
- data/lib/superthread/models/concerns/timestampable.rb +91 -0
- data/lib/superthread/models/list.rb +67 -0
- data/lib/superthread/models/member.rb +40 -0
- data/lib/superthread/models/note.rb +56 -0
- data/lib/superthread/models/page.rb +70 -0
- data/lib/superthread/models/project.rb +83 -0
- data/lib/superthread/models/space.rb +71 -0
- data/lib/superthread/models/sprint.rb +53 -0
- data/lib/superthread/models/tag.rb +52 -0
- data/lib/superthread/models/team.rb +68 -0
- data/lib/superthread/models/user.rb +76 -0
- data/lib/superthread/models.rb +12 -0
- data/lib/superthread/object.rb +285 -0
- data/lib/superthread/objects/collection.rb +179 -0
- data/lib/superthread/resources/base.rb +204 -0
- data/lib/superthread/resources/boards.rb +150 -0
- data/lib/superthread/resources/cards.rb +363 -0
- data/lib/superthread/resources/comments.rb +163 -0
- data/lib/superthread/resources/notes.rb +61 -0
- data/lib/superthread/resources/pages.rb +110 -0
- data/lib/superthread/resources/projects.rb +117 -0
- data/lib/superthread/resources/search.rb +46 -0
- data/lib/superthread/resources/spaces.rb +104 -0
- data/lib/superthread/resources/sprints.rb +37 -0
- data/lib/superthread/resources/tags.rb +52 -0
- data/lib/superthread/resources/users.rb +29 -0
- data/lib/superthread/version.rb +6 -0
- data/lib/superthread/version_checker.rb +174 -0
- data/lib/superthread.rb +30 -0
- metadata +259 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# Main entry point for the Superthread CLI.
|
|
6
|
+
# Registers all subcommands and provides top-level commands like version and setup.
|
|
7
|
+
class Main < Base
|
|
8
|
+
map %w[--version -V] => :version
|
|
9
|
+
|
|
10
|
+
desc "version", "Show version and check for updates"
|
|
11
|
+
# Displays the current version and checks for updates.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def version
|
|
15
|
+
puts "superthread #{Superthread::VERSION}"
|
|
16
|
+
|
|
17
|
+
latest = Superthread::VersionChecker.check!
|
|
18
|
+
if latest && Superthread::VersionChecker.newer?(latest)
|
|
19
|
+
say ""
|
|
20
|
+
say "Update available: #{Superthread::VERSION} \u2192 #{latest}", :yellow
|
|
21
|
+
say "Run: brew upgrade superthread", :yellow
|
|
22
|
+
end
|
|
23
|
+
rescue
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "setup", "Interactive setup wizard"
|
|
28
|
+
# Runs the interactive setup wizard to configure accounts and workspaces.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
def setup
|
|
32
|
+
Superthread::Cli::Setup.execute
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "config SUBCOMMAND", "Manage configuration"
|
|
36
|
+
subcommand "config", Superthread::Cli::Config
|
|
37
|
+
|
|
38
|
+
desc "me", "Show current user account info"
|
|
39
|
+
# Displays information about the currently authenticated user.
|
|
40
|
+
#
|
|
41
|
+
# @return [void]
|
|
42
|
+
def me
|
|
43
|
+
handle_error do
|
|
44
|
+
user = client.users.me
|
|
45
|
+
|
|
46
|
+
if json_output?
|
|
47
|
+
output_item user
|
|
48
|
+
else
|
|
49
|
+
# Show basic user info
|
|
50
|
+
output_item user, fields: %i[display_name email time_created], labels: {
|
|
51
|
+
time_created: "Time Created"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Show workspaces with roles
|
|
55
|
+
if user.teams&.any?
|
|
56
|
+
say ""
|
|
57
|
+
say "Workspaces:", :cyan
|
|
58
|
+
user.teams.each do |team|
|
|
59
|
+
say " #{team.team_name} (#{team.id}) - #{team.role}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "accounts SUBCOMMAND", "Manage accounts"
|
|
67
|
+
subcommand "accounts", Superthread::Cli::Accounts
|
|
68
|
+
|
|
69
|
+
desc "workspaces SUBCOMMAND", "List and select workspaces"
|
|
70
|
+
subcommand "workspaces", Superthread::Cli::Workspaces
|
|
71
|
+
|
|
72
|
+
desc "members SUBCOMMAND", "Workspace member commands"
|
|
73
|
+
subcommand "members", Superthread::Cli::Members
|
|
74
|
+
|
|
75
|
+
desc "cards SUBCOMMAND", "Card management commands"
|
|
76
|
+
subcommand "cards", Superthread::Cli::Cards
|
|
77
|
+
|
|
78
|
+
desc "boards SUBCOMMAND", "Board and list management commands"
|
|
79
|
+
subcommand "boards", Superthread::Cli::Boards
|
|
80
|
+
|
|
81
|
+
desc "projects SUBCOMMAND", "Roadmap project (epic) commands"
|
|
82
|
+
subcommand "projects", Superthread::Cli::Projects
|
|
83
|
+
|
|
84
|
+
desc "spaces SUBCOMMAND", "Space management commands"
|
|
85
|
+
subcommand "spaces", Superthread::Cli::Spaces
|
|
86
|
+
|
|
87
|
+
desc "comments SUBCOMMAND", "Comment management commands"
|
|
88
|
+
subcommand "comments", Superthread::Cli::Comments
|
|
89
|
+
|
|
90
|
+
desc "replies SUBCOMMAND", "Comment reply commands"
|
|
91
|
+
subcommand "replies", Superthread::Cli::Replies
|
|
92
|
+
|
|
93
|
+
desc "lists SUBCOMMAND", "Board list (column) commands"
|
|
94
|
+
subcommand "lists", Superthread::Cli::Lists
|
|
95
|
+
|
|
96
|
+
desc "checklists SUBCOMMAND", "Card checklist commands"
|
|
97
|
+
subcommand "checklists", Superthread::Cli::Checklists
|
|
98
|
+
|
|
99
|
+
desc "pages SUBCOMMAND", "Page/documentation commands"
|
|
100
|
+
subcommand "pages", Superthread::Cli::Pages
|
|
101
|
+
|
|
102
|
+
desc "notes SUBCOMMAND", "Meeting notes commands"
|
|
103
|
+
subcommand "notes", Superthread::Cli::Notes
|
|
104
|
+
|
|
105
|
+
desc "sprints SUBCOMMAND", "Sprint commands"
|
|
106
|
+
subcommand "sprints", Superthread::Cli::Sprints
|
|
107
|
+
|
|
108
|
+
desc "search QUERY", "Search across workspace"
|
|
109
|
+
subcommand "search", Superthread::Cli::Search
|
|
110
|
+
|
|
111
|
+
desc "tags SUBCOMMAND", "Tag management commands"
|
|
112
|
+
subcommand "tags", Superthread::Cli::Tags
|
|
113
|
+
|
|
114
|
+
desc "activity", "Show recent activity across workspace"
|
|
115
|
+
subcommand "activity", Superthread::Cli::Activity
|
|
116
|
+
|
|
117
|
+
desc "completion SHELL", "Generate shell completion scripts"
|
|
118
|
+
subcommand "completion", Superthread::Cli::Completion
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for workspace member operations.
|
|
6
|
+
class Members < Base
|
|
7
|
+
desc "list", "List workspace members"
|
|
8
|
+
# Lists all members of the current workspace.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def list
|
|
12
|
+
handle_error do
|
|
13
|
+
members = client.users.members(workspace_id)
|
|
14
|
+
output_list members, columns: %i[id display_name email role], headers: {id: "USER_ID"}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing Superthread notes.
|
|
6
|
+
#
|
|
7
|
+
# Notes are standalone content items that can contain transcripts
|
|
8
|
+
# and user annotations. They can be made public for sharing.
|
|
9
|
+
class Notes < Base
|
|
10
|
+
desc "list", "List all notes"
|
|
11
|
+
# Lists all notes in the current workspace.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def list
|
|
15
|
+
notes = client.notes.list(workspace_id)
|
|
16
|
+
output_list notes, columns: %i[id title time_created], headers: {id: "NOTE_ID"}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "get NOTE", "Get note details"
|
|
20
|
+
# Retrieves and displays details for a specific note.
|
|
21
|
+
#
|
|
22
|
+
# @param note_id [String] numeric ID of the note to retrieve
|
|
23
|
+
# @return [void]
|
|
24
|
+
def get(note_id)
|
|
25
|
+
handle_error do
|
|
26
|
+
note = with_not_found("Note not found: '#{note_id}'. Use 'suth notes list' to see available notes.") do
|
|
27
|
+
client.notes.find(workspace_id, note_id)
|
|
28
|
+
end
|
|
29
|
+
output_item note, fields: %i[id title content time_created time_updated], labels: {id: "Note ID"}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "create", "Create a new note"
|
|
34
|
+
option :title, type: :string, required: true, desc: "Note title"
|
|
35
|
+
option :transcript, type: :string, desc: "Transcript content"
|
|
36
|
+
option :user_notes, type: :string, desc: "User notes"
|
|
37
|
+
option :is_public, type: :boolean, desc: "Make note public"
|
|
38
|
+
# Creates a new note in the current workspace.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def create
|
|
42
|
+
note = client.notes.create(workspace_id, **symbolized_options(:title, :transcript, :user_notes, :is_public))
|
|
43
|
+
output_item note, labels: {id: "Note ID"}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "delete NOTE", "Delete a note"
|
|
47
|
+
# Deletes a note after confirmation.
|
|
48
|
+
#
|
|
49
|
+
# @param note_ref [String] note identifier (ID or name)
|
|
50
|
+
# @return [void]
|
|
51
|
+
def delete(note_ref)
|
|
52
|
+
handle_error do
|
|
53
|
+
note = with_not_found("Note not found: '#{note_ref}'. Use 'suth notes list' to see available notes.") do
|
|
54
|
+
client.notes.find(workspace_id, note_ref)
|
|
55
|
+
end
|
|
56
|
+
confirming("Delete note '#{note.title}' (#{note.id})?") do
|
|
57
|
+
client.notes.destroy(workspace_id, note.id)
|
|
58
|
+
output_success "Note '#{note.title}' deleted"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing Superthread pages (documents).
|
|
6
|
+
#
|
|
7
|
+
# Pages are rich-text documents that can be organized within spaces
|
|
8
|
+
# and nested under parent pages to form a hierarchy.
|
|
9
|
+
class Pages < Base
|
|
10
|
+
desc "list", "List all pages"
|
|
11
|
+
option :space, type: :string, aliases: "-s", desc: "Space to filter by (ID or name)"
|
|
12
|
+
option :include_archived, type: :boolean, desc: "Include archived pages"
|
|
13
|
+
option :updated_recently, type: :boolean, desc: "Filter by recently updated pages"
|
|
14
|
+
# Lists all pages in the current workspace with optional filters.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def list
|
|
18
|
+
opts = symbolized_options(:updated_recently)
|
|
19
|
+
opts[:archived] = options[:include_archived] if options[:include_archived]
|
|
20
|
+
opts[:space_id] = space_id if options[:space]
|
|
21
|
+
pages = client.pages.list(workspace_id, **opts)
|
|
22
|
+
output_list pages, columns: %i[id title], headers: {id: "PAGE_ID"}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "get PAGE", "Get page details"
|
|
26
|
+
# Retrieves and displays details for a specific page.
|
|
27
|
+
#
|
|
28
|
+
# @param page_id [String] numeric ID or URL slug of the page to retrieve
|
|
29
|
+
# @return [void]
|
|
30
|
+
def get(page_id)
|
|
31
|
+
handle_error do
|
|
32
|
+
page = with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
33
|
+
client.pages.find(workspace_id, page_id)
|
|
34
|
+
end
|
|
35
|
+
output_item page, fields: %i[id title time_created time_updated], labels: {id: "Page ID"}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "create", "Create a new page"
|
|
40
|
+
option :space, type: :string, required: true, aliases: "-s", desc: "Space to create page in (ID or name)"
|
|
41
|
+
option :title, type: :string, desc: "Page title"
|
|
42
|
+
option :content, type: :string, desc: "Page content"
|
|
43
|
+
option :parent_page, type: :string, desc: "Parent page ID"
|
|
44
|
+
option :is_public, type: :boolean, desc: "Make page public"
|
|
45
|
+
# Creates a new page in a space.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
def create
|
|
49
|
+
opts = symbolized_options(:title, :content, :is_public)
|
|
50
|
+
opts[:space_id] = space_id
|
|
51
|
+
opts[:parent_page_id] = options[:parent_page] if options[:parent_page]
|
|
52
|
+
page = client.pages.create(workspace_id, **opts)
|
|
53
|
+
output_item page, labels: {id: "Page ID"}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "update PAGE", "Update a page"
|
|
57
|
+
option :title, type: :string, desc: "New title"
|
|
58
|
+
option :is_public, type: :boolean, desc: "Public visibility"
|
|
59
|
+
option :parent_page, type: :string, desc: "Parent page ID"
|
|
60
|
+
option :archived, type: :boolean, desc: "Archive/unarchive"
|
|
61
|
+
# Updates an existing page's properties.
|
|
62
|
+
#
|
|
63
|
+
# @param page_id [String] numeric ID or URL slug of the page to retrieve
|
|
64
|
+
# @return [void]
|
|
65
|
+
def update(page_id)
|
|
66
|
+
handle_error do
|
|
67
|
+
opts = symbolized_options(:title, :is_public, :archived)
|
|
68
|
+
opts[:parent_page_id] = options[:parent_page] if options[:parent_page]
|
|
69
|
+
page = with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
70
|
+
client.pages.update(workspace_id, page_id, **opts)
|
|
71
|
+
end
|
|
72
|
+
output_item page, labels: {id: "Page ID"}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "duplicate PAGE", "Duplicate a page"
|
|
77
|
+
option :space, type: :string, required: true, aliases: "-s", desc: "Destination space (ID or name)"
|
|
78
|
+
option :title, type: :string, desc: "Title for the duplicated page"
|
|
79
|
+
option :parent_page, type: :string, desc: "Parent page ID"
|
|
80
|
+
# Creates a copy of an existing page in a specified space.
|
|
81
|
+
#
|
|
82
|
+
# @param page_id [String] numeric ID of the source page to duplicate
|
|
83
|
+
# @return [void]
|
|
84
|
+
def duplicate(page_id)
|
|
85
|
+
handle_error do
|
|
86
|
+
opts = symbolized_options(:title)
|
|
87
|
+
opts[:space_id] = space_id
|
|
88
|
+
opts[:parent_page_id] = options[:parent_page] if options[:parent_page]
|
|
89
|
+
page = with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
90
|
+
client.pages.duplicate(workspace_id, page_id, **opts)
|
|
91
|
+
end
|
|
92
|
+
output_item page, labels: {id: "Page ID"}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "archive PAGE", "Archive a page"
|
|
97
|
+
# Archives a page, hiding it from default views.
|
|
98
|
+
#
|
|
99
|
+
# @param page_id [String] numeric ID or URL slug of the page to retrieve
|
|
100
|
+
# @return [void]
|
|
101
|
+
def archive(page_id)
|
|
102
|
+
handle_error do
|
|
103
|
+
page = with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
104
|
+
client.pages.archive(workspace_id, page_id)
|
|
105
|
+
end
|
|
106
|
+
output_item page, labels: {id: "Page ID"}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
desc "delete PAGE", "Delete a page permanently"
|
|
111
|
+
# Permanently deletes a page after confirmation.
|
|
112
|
+
#
|
|
113
|
+
# @param page_ref [String] page identifier (ID or name)
|
|
114
|
+
# @return [void]
|
|
115
|
+
def delete(page_ref)
|
|
116
|
+
handle_error do
|
|
117
|
+
page = with_not_found("Page not found: '#{page_ref}'. Use 'suth pages list' to see available pages.") do
|
|
118
|
+
client.pages.find(workspace_id, page_ref)
|
|
119
|
+
end
|
|
120
|
+
confirming("Delete page '#{page.title}' (#{page.id})?") do
|
|
121
|
+
client.pages.destroy(workspace_id, page.id)
|
|
122
|
+
output_success "Page '#{page.title}' deleted"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing Superthread projects (epics/roadmap items).
|
|
6
|
+
#
|
|
7
|
+
# Projects are high-level items on the roadmap that can contain multiple
|
|
8
|
+
# cards. They have start/due dates, owners, and can be organized on boards.
|
|
9
|
+
class Projects < Base
|
|
10
|
+
# Kebab-case aliases for commands
|
|
11
|
+
map "add-card" => :add_card,
|
|
12
|
+
"remove-card" => :remove_card
|
|
13
|
+
desc "list", "List all roadmap projects"
|
|
14
|
+
# Lists all projects in the current workspace.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def list
|
|
18
|
+
projects = client.projects.list(workspace_id)
|
|
19
|
+
output_list projects, columns: %i[id title status], headers: {id: "PROJECT_ID"}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "get PROJECT", "Get project details"
|
|
23
|
+
# Retrieves and displays details for a specific project.
|
|
24
|
+
#
|
|
25
|
+
# @param project_id [String] numeric ID or short code of the project/epic
|
|
26
|
+
# @return [void]
|
|
27
|
+
def get(project_id)
|
|
28
|
+
handle_error do
|
|
29
|
+
project = with_not_found("Project not found: '#{project_id}'. Use 'suth projects list' to see available projects.") do
|
|
30
|
+
client.projects.find(workspace_id, project_id)
|
|
31
|
+
end
|
|
32
|
+
output_item project, fields: %i[id title status start_date due_date time_created time_updated],
|
|
33
|
+
labels: {id: "Project ID"}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "create", "Create a new project"
|
|
38
|
+
option :title, type: :string, required: true, desc: "Project title"
|
|
39
|
+
option :list, type: :string, required: true, aliases: "-l", desc: "Destination list (ID or name, requires --board)"
|
|
40
|
+
option :board, type: :string, aliases: "-b", desc: "Board (helps resolve list name)"
|
|
41
|
+
option :space, type: :string, aliases: "-s", desc: "Space (helps resolve board name)"
|
|
42
|
+
option :content, type: :string, desc: "Project description"
|
|
43
|
+
option :start_date, type: :numeric, desc: "Start date (Unix timestamp)"
|
|
44
|
+
option :due_date, type: :numeric, desc: "Due date (Unix timestamp)"
|
|
45
|
+
option :owner, type: :string, aliases: "-o", desc: "Owner (user ID, name, or email)"
|
|
46
|
+
option :priority, type: :numeric, desc: "Priority level"
|
|
47
|
+
# Creates a new project on a board list.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def create
|
|
51
|
+
opts = symbolized_options(:title, :content, :start_date, :due_date, :priority)
|
|
52
|
+
opts[:list_id] = resolve_list(options[:list])
|
|
53
|
+
opts[:owner_id] = resolve_user(options[:owner]) if options[:owner]
|
|
54
|
+
project = client.projects.create(workspace_id, **opts)
|
|
55
|
+
output_item project, labels: {id: "Project ID"}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "update PROJECT", "Update a project"
|
|
59
|
+
option :title, type: :string, desc: "New title"
|
|
60
|
+
option :list, type: :string, aliases: "-l", desc: "Destination list (ID or name, requires --board)"
|
|
61
|
+
option :board, type: :string, aliases: "-b", desc: "Board (helps resolve list name)"
|
|
62
|
+
option :space, type: :string, aliases: "-s", desc: "Space (helps resolve board name)"
|
|
63
|
+
option :owner, type: :string, aliases: "-o", desc: "New owner (user ID, name, or email)"
|
|
64
|
+
option :start_date, type: :numeric, desc: "Start date"
|
|
65
|
+
option :due_date, type: :numeric, desc: "Due date"
|
|
66
|
+
option :priority, type: :numeric, desc: "Priority"
|
|
67
|
+
option :archived, type: :boolean, desc: "Archive/unarchive"
|
|
68
|
+
# Updates an existing project's properties.
|
|
69
|
+
#
|
|
70
|
+
# @param project_id [String] numeric ID or short code of the project/epic
|
|
71
|
+
# @return [void]
|
|
72
|
+
def update(project_id)
|
|
73
|
+
handle_error do
|
|
74
|
+
opts = symbolized_options(:title, :start_date, :due_date, :priority, :archived)
|
|
75
|
+
opts[:list_id] = resolve_list(options[:list]) if options[:list]
|
|
76
|
+
opts[:owner_id] = resolve_user(options[:owner]) if options[:owner]
|
|
77
|
+
project = with_not_found("Project not found: '#{project_id}'. Use 'suth projects list' to see available projects.") do
|
|
78
|
+
client.projects.update(workspace_id, project_id, **opts)
|
|
79
|
+
end
|
|
80
|
+
output_item project, labels: {id: "Project ID"}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
desc "delete PROJECT", "Delete a project"
|
|
85
|
+
# Deletes a project after confirmation.
|
|
86
|
+
#
|
|
87
|
+
# @param project_ref [String] project identifier (ID or name)
|
|
88
|
+
# @return [void]
|
|
89
|
+
def delete(project_ref)
|
|
90
|
+
handle_error do
|
|
91
|
+
project = with_not_found("Project not found: '#{project_ref}'. Use 'suth projects list' to see available projects.") do
|
|
92
|
+
client.projects.find(workspace_id, project_ref)
|
|
93
|
+
end
|
|
94
|
+
confirming("Delete project '#{project.title}' (#{project.id})?") do
|
|
95
|
+
client.projects.destroy(workspace_id, project.id)
|
|
96
|
+
output_success "Project '#{project.title}' deleted"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
desc "add_card PROJECT_ID CARD_ID", "Link a card to a project"
|
|
102
|
+
# Links an existing card to a project.
|
|
103
|
+
#
|
|
104
|
+
# @param project_id [String] numeric ID or short code of the project/epic
|
|
105
|
+
# @param card_id [String] unique identifier of the card to link
|
|
106
|
+
# @return [void]
|
|
107
|
+
def add_card(project_id, card_id)
|
|
108
|
+
client.projects.add_card(workspace_id, project_id, card_id)
|
|
109
|
+
output_success "Linked card #{card_id} to project #{project_id}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc "remove_card PROJECT_ID CARD_ID", "Remove a card from a project"
|
|
113
|
+
# Removes a card's association from a project.
|
|
114
|
+
#
|
|
115
|
+
# @param project_id [String] numeric ID or short code of the project/epic
|
|
116
|
+
# @param card_id [String] unique identifier of the card to unlink
|
|
117
|
+
# @return [void]
|
|
118
|
+
def remove_card(project_id, card_id)
|
|
119
|
+
client.projects.remove_card(workspace_id, project_id, card_id)
|
|
120
|
+
output_success "Removed card #{card_id} from project #{project_id}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing comment replies.
|
|
6
|
+
#
|
|
7
|
+
# Replies are threaded responses to comments on cards and pages.
|
|
8
|
+
# This class provides commands to list, create, update, and delete replies.
|
|
9
|
+
class Replies < Base
|
|
10
|
+
desc "list", "List all replies to a comment"
|
|
11
|
+
option :comment, type: :string, required: true, desc: "Parent comment ID"
|
|
12
|
+
# List all replies to a specified comment.
|
|
13
|
+
#
|
|
14
|
+
# @return [void]
|
|
15
|
+
def list
|
|
16
|
+
handle_error do
|
|
17
|
+
replies = client.comments.replies(workspace_id, options[:comment])
|
|
18
|
+
output_list replies, columns: %i[id content user_id time_created], headers: {id: "REPLY_ID"}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "get REPLY_ID", "Get reply details"
|
|
23
|
+
option :comment, type: :string, required: true, desc: "Parent comment ID"
|
|
24
|
+
# Display detailed information about a specific reply.
|
|
25
|
+
#
|
|
26
|
+
# @param reply_id [String] the unique identifier of the reply
|
|
27
|
+
# @return [void]
|
|
28
|
+
def get(reply_id)
|
|
29
|
+
handle_error do
|
|
30
|
+
begin
|
|
31
|
+
reply = client.comments.find_reply(workspace_id, options[:comment], reply_id)
|
|
32
|
+
rescue Superthread::NotFoundError
|
|
33
|
+
raise Thor::Error, "Reply not found: '#{reply_id}' on comment '#{options[:comment]}'."
|
|
34
|
+
end
|
|
35
|
+
output_item reply, fields: %i[id content user_id card_id time_created time_updated], labels: {
|
|
36
|
+
id: "Reply ID",
|
|
37
|
+
user_id: "User ID",
|
|
38
|
+
card_id: "Card ID",
|
|
39
|
+
time_created: "Time Created",
|
|
40
|
+
time_updated: "Time Updated"
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "create", "Reply to a comment"
|
|
46
|
+
option :comment, type: :string, required: true, desc: "Parent comment ID"
|
|
47
|
+
option :content, type: :string, required: true, desc: "Reply content"
|
|
48
|
+
# Add a threaded reply to an existing comment.
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
def create
|
|
52
|
+
handle_error do
|
|
53
|
+
reply = client.comments.reply(workspace_id, options[:comment], content: options[:content])
|
|
54
|
+
output_item reply, labels: {id: "Reply ID"}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "update REPLY_ID", "Update a reply"
|
|
59
|
+
option :comment, type: :string, required: true, desc: "Parent comment ID"
|
|
60
|
+
option :content, type: :string, desc: "New content"
|
|
61
|
+
option :status, type: :string, enum: %w[resolved open orphaned], desc: "Status"
|
|
62
|
+
# Update an existing reply's content or status.
|
|
63
|
+
#
|
|
64
|
+
# @param reply_id [String] the unique identifier of the reply
|
|
65
|
+
# @return [void]
|
|
66
|
+
def update(reply_id)
|
|
67
|
+
handle_error do
|
|
68
|
+
reply = client.comments.update_reply(
|
|
69
|
+
workspace_id, options[:comment], reply_id,
|
|
70
|
+
**symbolized_options(:content, :status)
|
|
71
|
+
)
|
|
72
|
+
output_item reply, labels: {id: "Reply ID"}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "delete REPLY_ID", "Delete a reply"
|
|
77
|
+
option :comment, type: :string, required: true, desc: "Parent comment ID"
|
|
78
|
+
# Permanently delete a reply after confirmation.
|
|
79
|
+
#
|
|
80
|
+
# @param reply_id [String] the unique identifier of the reply
|
|
81
|
+
# @return [void]
|
|
82
|
+
def delete(reply_id)
|
|
83
|
+
handle_error do
|
|
84
|
+
reply = client.comments.find_reply(workspace_id, options[:comment], reply_id)
|
|
85
|
+
preview = Formatter.truncate(Formatter.strip_html(reply.content), 50)
|
|
86
|
+
confirming("Delete reply '#{preview}' (#{reply_id})?") do
|
|
87
|
+
client.comments.delete_reply(workspace_id, options[:comment], reply_id)
|
|
88
|
+
output_success "Reply #{reply_id} deleted"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for search operations.
|
|
6
|
+
class Search < Base
|
|
7
|
+
desc "query SEARCH_TERM", "Search across workspace"
|
|
8
|
+
option :field, type: :string, enum: %w[title content], desc: "Field to search in"
|
|
9
|
+
option :types, type: :string, desc: "Entity types to search (comma-separated: board,card,page,project,epic,note)"
|
|
10
|
+
option :space, type: :string, aliases: "-s", desc: "Space to filter by (ID or name)"
|
|
11
|
+
option :include_archived, type: :boolean, desc: "Include archived items"
|
|
12
|
+
option :grouped, type: :boolean, desc: "Group results by type"
|
|
13
|
+
# Searches across all entities in the workspace.
|
|
14
|
+
#
|
|
15
|
+
# @param search_term [String] the text to search for
|
|
16
|
+
# @return [void]
|
|
17
|
+
def query(search_term)
|
|
18
|
+
handle_error do
|
|
19
|
+
types = options[:types]&.split(",")&.map(&:strip)
|
|
20
|
+
results = client.search.query(
|
|
21
|
+
workspace_id,
|
|
22
|
+
query: search_term,
|
|
23
|
+
field: options[:field],
|
|
24
|
+
types: types,
|
|
25
|
+
space_id: space_id,
|
|
26
|
+
archived: options[:include_archived],
|
|
27
|
+
grouped: options[:grouped]
|
|
28
|
+
)
|
|
29
|
+
output_list results, columns: %i[result_type id title], headers: {id: "ID"}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|