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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +492 -0
  5. data/exe/suth +19 -0
  6. data/lib/superthread/cli/accounts.rb +240 -0
  7. data/lib/superthread/cli/activity.rb +210 -0
  8. data/lib/superthread/cli/base.rb +355 -0
  9. data/lib/superthread/cli/boards.rb +131 -0
  10. data/lib/superthread/cli/cards.rb +530 -0
  11. data/lib/superthread/cli/checklists.rb +223 -0
  12. data/lib/superthread/cli/comments.rb +86 -0
  13. data/lib/superthread/cli/completion.rb +306 -0
  14. data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
  15. data/lib/superthread/cli/concerns/confirmable.rb +55 -0
  16. data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
  17. data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
  18. data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
  19. data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
  20. data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
  21. data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
  22. data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
  23. data/lib/superthread/cli/config.rb +129 -0
  24. data/lib/superthread/cli/formatter.rb +388 -0
  25. data/lib/superthread/cli/lists.rb +85 -0
  26. data/lib/superthread/cli/main.rb +121 -0
  27. data/lib/superthread/cli/members.rb +19 -0
  28. data/lib/superthread/cli/notes.rb +64 -0
  29. data/lib/superthread/cli/pages.rb +128 -0
  30. data/lib/superthread/cli/projects.rb +124 -0
  31. data/lib/superthread/cli/replies.rb +94 -0
  32. data/lib/superthread/cli/search.rb +34 -0
  33. data/lib/superthread/cli/setup.rb +253 -0
  34. data/lib/superthread/cli/spaces.rb +141 -0
  35. data/lib/superthread/cli/sprints.rb +32 -0
  36. data/lib/superthread/cli/tags.rb +86 -0
  37. data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
  38. data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
  39. data/lib/superthread/cli/ui.rb +263 -0
  40. data/lib/superthread/cli/workspaces.rb +105 -0
  41. data/lib/superthread/cli.rb +12 -0
  42. data/lib/superthread/client.rb +207 -0
  43. data/lib/superthread/configuration.rb +354 -0
  44. data/lib/superthread/connection.rb +57 -0
  45. data/lib/superthread/error.rb +164 -0
  46. data/lib/superthread/mention_formatter.rb +96 -0
  47. data/lib/superthread/model.rb +178 -0
  48. data/lib/superthread/models/board.rb +59 -0
  49. data/lib/superthread/models/card.rb +321 -0
  50. data/lib/superthread/models/checklist.rb +91 -0
  51. data/lib/superthread/models/checklist_item.rb +69 -0
  52. data/lib/superthread/models/comment.rb +71 -0
  53. data/lib/superthread/models/concerns/archivable.rb +32 -0
  54. data/lib/superthread/models/concerns/presentable.rb +113 -0
  55. data/lib/superthread/models/concerns/timestampable.rb +91 -0
  56. data/lib/superthread/models/list.rb +67 -0
  57. data/lib/superthread/models/member.rb +40 -0
  58. data/lib/superthread/models/note.rb +56 -0
  59. data/lib/superthread/models/page.rb +70 -0
  60. data/lib/superthread/models/project.rb +83 -0
  61. data/lib/superthread/models/space.rb +71 -0
  62. data/lib/superthread/models/sprint.rb +53 -0
  63. data/lib/superthread/models/tag.rb +52 -0
  64. data/lib/superthread/models/team.rb +68 -0
  65. data/lib/superthread/models/user.rb +76 -0
  66. data/lib/superthread/models.rb +12 -0
  67. data/lib/superthread/object.rb +285 -0
  68. data/lib/superthread/objects/collection.rb +179 -0
  69. data/lib/superthread/resources/base.rb +204 -0
  70. data/lib/superthread/resources/boards.rb +150 -0
  71. data/lib/superthread/resources/cards.rb +363 -0
  72. data/lib/superthread/resources/comments.rb +163 -0
  73. data/lib/superthread/resources/notes.rb +61 -0
  74. data/lib/superthread/resources/pages.rb +110 -0
  75. data/lib/superthread/resources/projects.rb +117 -0
  76. data/lib/superthread/resources/search.rb +46 -0
  77. data/lib/superthread/resources/spaces.rb +104 -0
  78. data/lib/superthread/resources/sprints.rb +37 -0
  79. data/lib/superthread/resources/tags.rb +52 -0
  80. data/lib/superthread/resources/users.rb +29 -0
  81. data/lib/superthread/version.rb +6 -0
  82. data/lib/superthread/version_checker.rb +174 -0
  83. data/lib/superthread.rb +30 -0
  84. metadata +259 -0
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for page (document) operations.
6
+ #
7
+ # Provides methods for creating, updating, and managing pages
8
+ # (documents, wikis) via the Superthread API.
9
+ class Pages < Base
10
+ # Lists all pages in a workspace.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @param space_id [String, nil] optional space identifier to filter by
14
+ # @param archived [Boolean, nil] when true, includes archived pages
15
+ # @param updated_recently [Boolean, nil] when true, returns only recently updated pages
16
+ # @return [Superthread::Objects::Collection<Superthread::Models::Page>] the pages in the workspace
17
+ def list(workspace_id, space_id: nil, archived: nil, updated_recently: nil)
18
+ ws = safe_id("workspace_id", workspace_id)
19
+ params = compact_params(project_id: space_id, archived: archived, updated_recently: updated_recently)
20
+ get_collection("/#{ws}/pages", params: params,
21
+ item_class: Models::Page, items_key: :pages)
22
+ end
23
+
24
+ # Gets a specific page.
25
+ #
26
+ # @param workspace_id [String] the workspace identifier
27
+ # @param page_id [String] the page identifier
28
+ # @return [Superthread::Models::Page] the page with all attributes
29
+ def find(workspace_id, page_id)
30
+ ws = safe_id("workspace_id", workspace_id)
31
+ page = safe_id("page_id", page_id)
32
+ get_object("/#{ws}/pages/#{page}",
33
+ object_class: Models::Page, unwrap_key: :page)
34
+ end
35
+
36
+ # Creates a new page in a space.
37
+ #
38
+ # @param workspace_id [String] the workspace identifier
39
+ # @param space_id [String] the space identifier
40
+ # @param params [Hash{Symbol => Object}] page parameters
41
+ # @option params [String] :title the page title
42
+ # @option params [String] :content the page content as HTML
43
+ # @option params [Hash{Symbol => Object}] :schema the content schema for structured content
44
+ # @option params [String] :parent_page_id the parent page identifier for nesting
45
+ # @return [Superthread::Models::Page] the created page
46
+ def create(workspace_id, space_id:, **params)
47
+ ws = safe_id("workspace_id", workspace_id)
48
+ body = compact_params(project_id: space_id, **params)
49
+ post_object("/#{ws}/pages", body: body,
50
+ object_class: Models::Page, unwrap_key: :page)
51
+ end
52
+
53
+ # Updates a page's attributes.
54
+ #
55
+ # @param workspace_id [String] the workspace identifier
56
+ # @param page_id [String] the page identifier
57
+ # @param params [Hash{Symbol => Object}] the attributes to update
58
+ # @option params [String] :title the new page title
59
+ # @option params [String] :content the new page content as HTML
60
+ # @option params [Boolean] :archived whether the page is archived
61
+ # @option params [String] :parent_page_id the new parent page identifier
62
+ # @return [Superthread::Models::Page] the updated page
63
+ def update(workspace_id, page_id, **params)
64
+ ws = safe_id("workspace_id", workspace_id)
65
+ page = safe_id("page_id", page_id)
66
+ patch_object("/#{ws}/pages/#{page}", body: compact_params(**params),
67
+ object_class: Models::Page, unwrap_key: :page)
68
+ end
69
+
70
+ # Duplicates a page to a destination space.
71
+ #
72
+ # @param workspace_id [String] the workspace identifier
73
+ # @param page_id [String] the page identifier to duplicate
74
+ # @param space_id [String] the destination space identifier
75
+ # @param params [Hash{Symbol => Object}] optional duplication parameters
76
+ # @option params [String] :title the title for the duplicated page (defaults to original)
77
+ # @option params [String] :parent_page_id the parent page identifier in the destination
78
+ # @option params [Integer] :position the position index among sibling pages
79
+ # @return [Superthread::Models::Page] the duplicated page
80
+ def duplicate(workspace_id, page_id, space_id:, **params)
81
+ ws = safe_id("workspace_id", workspace_id)
82
+ page = safe_id("page_id", page_id)
83
+ body = compact_params(project_id: space_id, **params)
84
+ post_object("/#{ws}/pages/#{page}/copy", body: body,
85
+ object_class: Models::Page, unwrap_key: :page)
86
+ end
87
+
88
+ # Archives a page (soft delete).
89
+ #
90
+ # @param workspace_id [String] the workspace identifier
91
+ # @param page_id [String] the page identifier to archive
92
+ # @return [Superthread::Models::Page] the archived page
93
+ def archive(workspace_id, page_id)
94
+ update(workspace_id, page_id, archived: true)
95
+ end
96
+
97
+ # Deletes a page permanently.
98
+ #
99
+ # @param workspace_id [String] the workspace identifier
100
+ # @param page_id [String] the page identifier to delete
101
+ # @return [Superthread::Object] a response object with success: true
102
+ def destroy(workspace_id, page_id)
103
+ ws = safe_id("workspace_id", workspace_id)
104
+ page = safe_id("page_id", page_id)
105
+ http_delete("/#{ws}/pages/#{page}")
106
+ success_response
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for project (epic/roadmap item) operations.
6
+ #
7
+ # Provides methods for creating, updating, and managing projects
8
+ # (epics) and their linked cards via the Superthread API.
9
+ class Projects < Base
10
+ # Lists all roadmap projects (epics) in a workspace.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @return [Superthread::Objects::Collection<Superthread::Models::Project>] the projects in the workspace
14
+ # @note The API returns epics nested within lists (status columns).
15
+ # This method flattens them into a single collection.
16
+ def list(workspace_id)
17
+ ws = safe_id("workspace_id", workspace_id)
18
+ data = http_get("/#{ws}/epics")
19
+
20
+ # Epics are nested inside each list - flatten them
21
+ epics = (data[:lists] || []).flat_map { |list| list[:epics] || [] }
22
+
23
+ Superthread::Objects::Collection.from_response(
24
+ {epics: epics},
25
+ key: :epics,
26
+ item_class: Models::Project
27
+ )
28
+ end
29
+
30
+ # Gets a specific project.
31
+ #
32
+ # @param workspace_id [String] the workspace identifier
33
+ # @param project_id [String] the project identifier (maps to epic_id in API)
34
+ # @return [Superthread::Models::Project] the project with all attributes
35
+ def find(workspace_id, project_id)
36
+ ws = safe_id("workspace_id", workspace_id)
37
+ proj = safe_id("project_id", project_id)
38
+ get_object("/#{ws}/epics/#{proj}",
39
+ object_class: Models::Project, unwrap_key: :epic)
40
+ end
41
+
42
+ # Creates a new project (epic) on the roadmap.
43
+ #
44
+ # @param workspace_id [String] the workspace identifier
45
+ # @param title [String] the project title
46
+ # @param list_id [String] the list identifier (status column) for the project
47
+ # @param params [Hash{Symbol => Object}] optional project parameters
48
+ # @option params [String] :content the project description content
49
+ # @option params [Integer] :start_date the start date as Unix timestamp
50
+ # @option params [Integer] :due_date the due date as Unix timestamp
51
+ # @return [Superthread::Models::Project] the created project
52
+ def create(workspace_id, title:, list_id:, **params)
53
+ ws = safe_id("workspace_id", workspace_id)
54
+ body = compact_params(title: title, list_id: list_id, **params)
55
+ post_object("/#{ws}/epics", body: body,
56
+ object_class: Models::Project, unwrap_key: :epic)
57
+ end
58
+
59
+ # Updates a project's attributes.
60
+ #
61
+ # @param workspace_id [String] the workspace identifier
62
+ # @param project_id [String] the project identifier
63
+ # @param params [Hash{Symbol => Object}] the attributes to update
64
+ # @option params [String] :title the new project title
65
+ # @option params [String] :content the new project description
66
+ # @option params [String] :list_id the new list identifier (status column)
67
+ # @option params [Integer] :start_date the new start date as Unix timestamp
68
+ # @option params [Integer] :due_date the new due date as Unix timestamp
69
+ # @return [Superthread::Models::Project] the updated project
70
+ def update(workspace_id, project_id, **params)
71
+ ws = safe_id("workspace_id", workspace_id)
72
+ proj = safe_id("project_id", project_id)
73
+ patch_object("/#{ws}/epics/#{proj}", body: compact_params(**params),
74
+ object_class: Models::Project, unwrap_key: :epic)
75
+ end
76
+
77
+ # Deletes a project permanently.
78
+ #
79
+ # @param workspace_id [String] the workspace identifier
80
+ # @param project_id [String] the project identifier to delete
81
+ # @return [Superthread::Object] a response object with success: true
82
+ def destroy(workspace_id, project_id)
83
+ ws = safe_id("workspace_id", workspace_id)
84
+ proj = safe_id("project_id", project_id)
85
+ http_delete("/#{ws}/epics/#{proj}")
86
+ success_response
87
+ end
88
+
89
+ # Links a card to a project (epic).
90
+ #
91
+ # @param workspace_id [String] the workspace identifier
92
+ # @param project_id [String] the project identifier
93
+ # @param card_id [String] the card identifier to link
94
+ # @return [Superthread::Object] the link result
95
+ def add_card(workspace_id, project_id, card_id)
96
+ ws = safe_id("workspace_id", workspace_id)
97
+ proj = safe_id("project_id", project_id)
98
+ card = safe_id("card_id", card_id)
99
+ post_object("/#{ws}/epics/#{proj}/cards/#{card}")
100
+ end
101
+
102
+ # Removes a card from a project (epic).
103
+ #
104
+ # @param workspace_id [String] the workspace identifier
105
+ # @param project_id [String] the project identifier
106
+ # @param card_id [String] the card identifier to remove
107
+ # @return [Superthread::Object] a response object with success: true
108
+ def remove_card(workspace_id, project_id, card_id)
109
+ ws = safe_id("workspace_id", workspace_id)
110
+ proj = safe_id("project_id", project_id)
111
+ card = safe_id("card_id", card_id)
112
+ http_delete("/#{ws}/epics/#{proj}/cards/#{card}")
113
+ success_response
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for search operations.
6
+ #
7
+ # Provides methods for searching across workspace entities
8
+ # (cards, pages, boards, etc.) via the Superthread API.
9
+ class Search < Base
10
+ # Searches across workspace entities.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @param query [String] the search query string
14
+ # @param params [Hash{Symbol => Object}] optional search parameters
15
+ # @option params [String] :field the field to search (title, content)
16
+ # @option params [Array<String>] :types entity types to include (board, card, page, etc.)
17
+ # @option params [Array<String>] :statuses status values to filter by
18
+ # @option params [String] :space_id the space identifier to filter by
19
+ # @option params [Boolean] :archived when true, includes archived entities
20
+ # @option params [Boolean] :grouped when true, groups results by type (default: false)
21
+ # @option params [String] :cursor the pagination cursor for next page
22
+ # @return [Superthread::Objects::Collection] the search results
23
+ def query(workspace_id, query:, **params)
24
+ ws = safe_id("workspace_id", workspace_id)
25
+ # Default grouped to false so results come in a flat array
26
+ # Use || instead of fetch because CLI may pass grouped: nil explicitly
27
+ grouped = params[:grouped].nil? ? false : params[:grouped]
28
+ search_params = compact_params(
29
+ query: query,
30
+ project_id: params[:space_id],
31
+ grouped: grouped,
32
+ **params.except(:space_id, :grouped)
33
+ )
34
+ response = http_get("/#{ws}/search", params: search_params)
35
+
36
+ # Unwrap results - each item is {"card": {...}} or {"board": {...}}, etc.
37
+ results = (response[:results] || []).map do |item|
38
+ result_type, data = item.first
39
+ data.merge(result_type: result_type.to_s)
40
+ end
41
+
42
+ Objects::Collection.from_response(results)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for space operations.
6
+ #
7
+ # Provides methods for creating, updating, and managing spaces
8
+ # and their members via the Superthread API.
9
+ class Spaces < Base
10
+ # Lists all spaces in a workspace.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @return [Superthread::Objects::Collection<Superthread::Models::Space>] the spaces in the workspace
14
+ def list(workspace_id)
15
+ ws = safe_id("workspace_id", workspace_id)
16
+ get_collection("/#{ws}/projects",
17
+ item_class: Models::Space, items_key: :projects)
18
+ end
19
+
20
+ # Gets a specific space.
21
+ #
22
+ # @param workspace_id [String] the workspace identifier
23
+ # @param space_id [String] the space identifier (maps to project_id in API)
24
+ # @return [Superthread::Models::Space] the space with all attributes
25
+ def find(workspace_id, space_id)
26
+ ws = safe_id("workspace_id", workspace_id)
27
+ space = safe_id("space_id", space_id)
28
+ get_object("/#{ws}/projects/#{space}",
29
+ object_class: Models::Space, unwrap_key: :project)
30
+ end
31
+
32
+ # Creates a new space in the workspace.
33
+ #
34
+ # @param workspace_id [String] the workspace identifier
35
+ # @param title [String] the space title
36
+ # @param params [Hash{Symbol => Object}] optional space parameters
37
+ # @option params [String] :description the space description
38
+ # @option params [String] :icon the space icon identifier
39
+ # @return [Superthread::Models::Space] the created space
40
+ def create(workspace_id, title:, **params)
41
+ ws = safe_id("workspace_id", workspace_id)
42
+ body = compact_params(title: title, **params)
43
+ post_object("/#{ws}/projects", body: body,
44
+ object_class: Models::Space, unwrap_key: :project)
45
+ end
46
+
47
+ # Updates a space's attributes.
48
+ #
49
+ # @param workspace_id [String] the workspace identifier
50
+ # @param space_id [String] the space identifier
51
+ # @param params [Hash{Symbol => Object}] the attributes to update
52
+ # @option params [String] :title the new space title
53
+ # @option params [String] :description the new space description
54
+ # @option params [String] :icon the new space icon identifier
55
+ # @return [Superthread::Models::Space] the updated space
56
+ def update(workspace_id, space_id, **params)
57
+ ws = safe_id("workspace_id", workspace_id)
58
+ space = safe_id("space_id", space_id)
59
+ patch_object("/#{ws}/projects/#{space}", body: compact_params(**params),
60
+ object_class: Models::Space, unwrap_key: :project)
61
+ end
62
+
63
+ # Deletes a space permanently.
64
+ #
65
+ # @param workspace_id [String] the workspace identifier
66
+ # @param space_id [String] the space identifier to delete
67
+ # @return [Superthread::Object] a response object with success: true
68
+ def destroy(workspace_id, space_id)
69
+ ws = safe_id("workspace_id", workspace_id)
70
+ space = safe_id("space_id", space_id)
71
+ http_delete("/#{ws}/projects/#{space}")
72
+ success_response
73
+ end
74
+
75
+ # Adds a member to a space.
76
+ #
77
+ # @param workspace_id [String] the workspace identifier
78
+ # @param space_id [String] the space identifier
79
+ # @param user_id [String] the user identifier to add as a member
80
+ # @param role [String] the member role (admin, member, viewer)
81
+ # @return [Superthread::Object] the membership result
82
+ def add_member(workspace_id, space_id, user_id:, role: "member")
83
+ ws = safe_id("workspace_id", workspace_id)
84
+ space = safe_id("space_id", space_id)
85
+ body = {members: [{user_id: user_id, role: role}]}
86
+ post_object("/#{ws}/projects/#{space}/members", body: body)
87
+ end
88
+
89
+ # Removes a member from a space.
90
+ #
91
+ # @param workspace_id [String] the workspace identifier
92
+ # @param space_id [String] the space identifier
93
+ # @param user_id [String] the user identifier to remove
94
+ # @return [Superthread::Object] a response object with success: true
95
+ def remove_member(workspace_id, space_id, user_id)
96
+ ws = safe_id("workspace_id", workspace_id)
97
+ space = safe_id("space_id", space_id)
98
+ user = safe_id("user_id", user_id)
99
+ http_delete("/#{ws}/projects/#{space}/members/#{user}")
100
+ success_response
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for sprint operations.
6
+ #
7
+ # Provides methods for listing and retrieving sprints
8
+ # via the Superthread API.
9
+ class Sprints < Base
10
+ # Lists all sprints in a space.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @param space_id [String] the space identifier to list sprints from
14
+ # @return [Superthread::Objects::Collection<Superthread::Models::Sprint>] the sprints in the space
15
+ def list(workspace_id, space_id:)
16
+ ws = safe_id("workspace_id", workspace_id)
17
+ params = compact_params(project_id: space_id)
18
+ get_collection("/#{ws}/sprints", params: params,
19
+ item_class: Models::Sprint, items_key: :sprints)
20
+ end
21
+
22
+ # Gets a specific sprint with its available lists.
23
+ #
24
+ # @param workspace_id [String] the workspace identifier
25
+ # @param sprint_id [String] the sprint identifier
26
+ # @param space_id [String] the space identifier (required by the API)
27
+ # @return [Superthread::Models::Sprint] the sprint with available lists
28
+ def find(workspace_id, sprint_id, space_id:)
29
+ ws = safe_id("workspace_id", workspace_id)
30
+ sprint = safe_id("sprint_id", sprint_id)
31
+ params = compact_params(project_id: space_id)
32
+ get_object("/#{ws}/sprints/#{sprint}", params: params,
33
+ object_class: Models::Sprint, unwrap_key: :sprint)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for tag operations.
6
+ #
7
+ # Provides methods for creating, updating, and deleting tags
8
+ # via the Superthread API.
9
+ class Tags < Base
10
+ # Creates a new tag in the workspace.
11
+ #
12
+ # @param workspace_id [String] the workspace identifier
13
+ # @param name [String] the tag name
14
+ # @param color [String] the tag color as hex string (e.g., "#FF5733")
15
+ # @param space_id [String, nil] optional space identifier to scope the tag
16
+ # @return [Superthread::Models::Tag] the created tag
17
+ def create(workspace_id, name:, color:, space_id: nil)
18
+ ws = safe_id("workspace_id", workspace_id)
19
+ body = compact_params(name: name, color: color, project_id: space_id)
20
+ post_object("/#{ws}/tags", body: body,
21
+ object_class: Models::Tag, unwrap_key: :tag)
22
+ end
23
+
24
+ # Updates a tag's attributes.
25
+ #
26
+ # @param workspace_id [String] the workspace identifier
27
+ # @param tag_id [String] the tag identifier
28
+ # @param params [Hash{Symbol => Object}] the attributes to update
29
+ # @option params [String] :name the new tag name
30
+ # @option params [String] :color the new tag color as hex string
31
+ # @return [Superthread::Models::Tag] the updated tag
32
+ def update(workspace_id, tag_id, **params)
33
+ ws = safe_id("workspace_id", workspace_id)
34
+ tag = safe_id("tag_id", tag_id)
35
+ patch_object("/#{ws}/tags/#{tag}", body: compact_params(**params),
36
+ object_class: Models::Tag, unwrap_key: :tag)
37
+ end
38
+
39
+ # Deletes a tag permanently.
40
+ #
41
+ # @param workspace_id [String] the workspace identifier
42
+ # @param tag_id [String] the tag identifier to delete
43
+ # @return [Superthread::Object] a response object with success: true
44
+ def destroy(workspace_id, tag_id)
45
+ ws = safe_id("workspace_id", workspace_id)
46
+ tag = safe_id("tag_id", tag_id)
47
+ http_delete("/#{ws}/tags/#{tag}")
48
+ success_response
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ module Resources
5
+ # API resource for user operations.
6
+ #
7
+ # Provides methods for retrieving user information and
8
+ # listing workspace members via the Superthread API.
9
+ class Users < Base
10
+ # Gets the current authenticated user's account information.
11
+ #
12
+ # @return [Superthread::Models::User] the current user's profile
13
+ def me
14
+ get_object("/users/me", object_class: Models::User, unwrap_key: :user)
15
+ end
16
+
17
+ # Gets workspace members.
18
+ #
19
+ # @param workspace_id [String] the workspace identifier
20
+ # @return [Superthread::Objects::Collection<Superthread::Models::User>] the workspace members
21
+ # @note The API uses /teams/:id/members but we use workspace terminology.
22
+ def members(workspace_id)
23
+ ws = safe_id("workspace_id", workspace_id)
24
+ get_collection("/teams/#{ws}/members",
25
+ item_class: Models::User, items_key: :members)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superthread
4
+ # Current version of the Superthread gem.
5
+ VERSION = "0.7.2"
6
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Superthread
8
+ # Checks for newer versions of the Superthread CLI.
9
+ #
10
+ # Uses the GitHub Releases API to find the latest version and caches
11
+ # the result to avoid repeated network requests. Designed to never
12
+ # interrupt or slow down normal CLI usage.
13
+ #
14
+ # @example Check for updates
15
+ # Superthread::VersionChecker.check_if_stale!
16
+ # if (notice = Superthread::VersionChecker.update_notice)
17
+ # $stderr.puts notice
18
+ # end
19
+ #
20
+ # @example Force check (bypass cache TTL)
21
+ # latest = Superthread::VersionChecker.check!
22
+ module VersionChecker
23
+ # GitHub API endpoint for the latest release.
24
+ RELEASES_URL = "https://api.github.com/repos/steveclarke/superthread/releases/latest"
25
+
26
+ # How long to cache the version check result (24 hours).
27
+ CACHE_TTL = 86_400
28
+
29
+ # HTTP connect timeout in seconds.
30
+ CONNECT_TIMEOUT = 3
31
+
32
+ # HTTP read timeout in seconds.
33
+ READ_TIMEOUT = 5
34
+
35
+ module_function
36
+
37
+ # Returns a formatted update notice if a newer version is available.
38
+ #
39
+ # Reads from cache only — does not make network requests.
40
+ # Returns nil if no update is available, cache is missing, or
41
+ # update checks are disabled.
42
+ #
43
+ # @return [String, nil] update message or nil
44
+ def update_notice
45
+ return nil if disabled?
46
+
47
+ cached = read_cache
48
+ return nil unless cached
49
+
50
+ latest = cached[:latest_version]
51
+ return nil unless latest
52
+ return nil unless newer?(latest)
53
+
54
+ current = Superthread::VERSION
55
+ "\n" \
56
+ " Update available: #{current} \u2192 #{latest}\n" \
57
+ " Run: brew upgrade superthread\n"
58
+ end
59
+
60
+ # Fetches the latest version from GitHub and updates the cache.
61
+ #
62
+ # Always makes a network request regardless of cache freshness.
63
+ # Returns nil silently on any error (network, parse, etc.).
64
+ #
65
+ # @return [String, nil] the latest version string, or nil on failure
66
+ def check!
67
+ return nil if disabled?
68
+
69
+ version = fetch_latest_version
70
+ write_cache(version) if version
71
+ version
72
+ rescue
73
+ nil
74
+ end
75
+
76
+ # Fetches the latest version only if the cache is stale.
77
+ #
78
+ # If the cache is fresh (less than {CACHE_TTL} old), does nothing.
79
+ # This is the method to call on every CLI invocation — it's
80
+ # effectively free when the cache is warm.
81
+ #
82
+ # @return [String, nil] the latest version string, or nil
83
+ def check_if_stale!
84
+ return nil if disabled?
85
+
86
+ cached = read_cache
87
+ if cached && (Time.now.to_i - cached[:checked_at].to_i) < CACHE_TTL
88
+ return cached[:latest_version]
89
+ end
90
+
91
+ check!
92
+ rescue
93
+ nil
94
+ end
95
+
96
+ # Whether version checking is disabled via environment variable.
97
+ #
98
+ # @return [Boolean] true if SUPERTHREAD_NO_UPDATE_CHECK is set
99
+ def disabled?
100
+ ENV.key?("SUPERTHREAD_NO_UPDATE_CHECK")
101
+ end
102
+
103
+ # Compares a version string against the current version.
104
+ #
105
+ # @param version [String] the version to compare
106
+ # @return [Boolean] true if the given version is newer
107
+ def newer?(version)
108
+ Gem::Version.new(version) > Gem::Version.new(Superthread::VERSION)
109
+ rescue ArgumentError
110
+ false
111
+ end
112
+
113
+ # Returns the path to the version check cache file.
114
+ #
115
+ # @return [String] absolute path to version_check.json
116
+ def cache_path
117
+ File.join(
118
+ ENV.fetch("XDG_STATE_HOME", File.expand_path("~/.local/state")),
119
+ "superthread",
120
+ "version_check.json"
121
+ )
122
+ end
123
+
124
+ # Reads the cached version check result.
125
+ #
126
+ # @return [Hash{Symbol => Object}, nil] cached data or nil
127
+ def read_cache
128
+ return nil unless File.exist?(cache_path)
129
+
130
+ data = JSON.parse(File.read(cache_path), symbolize_names: true)
131
+ data if data.is_a?(Hash)
132
+ rescue JSON::ParserError, Errno::ENOENT
133
+ nil
134
+ end
135
+
136
+ # Writes a version check result to the cache file.
137
+ #
138
+ # @param version [String] the latest version string
139
+ # @return [void]
140
+ def write_cache(version)
141
+ FileUtils.mkdir_p(File.dirname(cache_path))
142
+ File.write(cache_path, JSON.generate(
143
+ latest_version: version,
144
+ checked_at: Time.now.to_i
145
+ ))
146
+ rescue Errno::EACCES, Errno::ENOSPC
147
+ nil
148
+ end
149
+
150
+ # Fetches the latest version tag from GitHub Releases API.
151
+ #
152
+ # @return [String, nil] version string (without leading "v"), or nil
153
+ def fetch_latest_version
154
+ conn = Faraday.new(url: RELEASES_URL) do |f|
155
+ f.headers["Accept"] = "application/vnd.github+json"
156
+ f.options.timeout = READ_TIMEOUT
157
+ f.options.open_timeout = CONNECT_TIMEOUT
158
+ f.adapter Faraday.default_adapter
159
+ end
160
+
161
+ response = conn.get
162
+ return nil unless response.status == 200
163
+
164
+ data = JSON.parse(response.body, symbolize_names: true)
165
+ tag = data[:tag_name]
166
+ return nil unless tag
167
+
168
+ # Strip leading "v" (e.g., "v0.5.7" -> "0.5.7")
169
+ tag.sub(/\Av/, "")
170
+ rescue Faraday::Error, JSON::ParserError
171
+ nil
172
+ end
173
+ end
174
+ end