linear-rb 0.1.1 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9c94b7659e207b517fcf2960b83390d2c7a4e17d6ea02e59d5079730eca3da5
4
- data.tar.gz: bbee449d73c50d2c6c623e138fd983faa46f98017ee9d8125b867d4902433452
3
+ metadata.gz: 2d86a47411e5f3e9ab46945cded06b74c305fee7552ac4770f6c93023dddb3dd
4
+ data.tar.gz: 8b8aa4987546fc77b34b7addab90d6181aa6b13b8d5fa1f7a32ce96d5c2814ef
5
5
  SHA512:
6
- metadata.gz: 1cbe8c882e24fe6acd64f086b97e45f40cbf2029191b92f0d0be00a83e5b3cc0ece68d4b11bb6ae037c943e0efb704ee6f7fe83e8ccfe7dd98198f8c91969151
7
- data.tar.gz: 69bba6defc5690f2663b1868cc14ca1e789177f8ce00d352484101b7ae3a53785d981cb3cd9447aa9df95e0238b551fb52fb4ca889802d6558805b88e8e37f40
6
+ metadata.gz: 89a95415e7aa7cc0d9f605eeb37d1444bd604fcb321cb182cb13b5f60410c35b95ff39836574462c56b4682c3a117fa7fcbeef8e0cfcad709fed91ab052b5e29
7
+ data.tar.gz: 7888ac607e72ae1480d9364bf0034acbe789e7bd6e7f4fa047b2e12cbce7dd626e0920d08c3d0aa1cc104aa92d0c2fb57632b46d0fb38506cb47b5bd471bc740
data/bin/linear CHANGED
@@ -8,26 +8,57 @@ def show_usage
8
8
  Linear CLI - Ruby wrapper for Linear GraphQL API
9
9
 
10
10
  Usage:
11
- linear issue ISSUE_ID Fetch issue details
12
- linear search QUERY [OPTIONS] Search for issues
11
+ linear issue ISSUE_ID Fetch a specific issue by ID
12
+ linear issues [OPTIONS] List and filter issues (all filters are optional)
13
13
  linear mine Show issues assigned to you
14
14
  linear teams List all teams
15
15
  linear projects List all projects
16
16
  linear comment ISSUE_ID COMMENT Add a comment to an issue
17
- linear update ISSUE_ID STATE Update issue state
18
-
19
- Search Options:
17
+ linear update ISSUE_ID [OPTIONS] Update issue (at least one option required)
18
+ --state STATE Update issue state
19
+ --title TITLE Update issue title
20
+ --description DESCRIPTION Update issue description
21
+
22
+ Issues Filters (all optional, can be combined):
23
+ --query TEXT Search by title text
24
+ --project PROJECT_ID Filter by project ID
25
+ --state STATE Filter by state name (case-insensitive)
20
26
  --team TEAM_KEY Filter by team key
21
- --state STATE Filter by state name
22
27
 
23
28
  Examples:
29
+ # View a specific issue
24
30
  linear issue ENG-123
25
- linear search "bug fix" --team=ENG --state=Backlog
31
+
32
+ # List all issues
33
+ linear issues
34
+
35
+ # Search issues by title
36
+ linear issues --query "authentication"
37
+
38
+ # Filter by state (case-insensitive)
39
+ linear issues --state backlog
40
+ linear issues --state "In Progress"
41
+
42
+ # Filter by team
43
+ linear issues --team ENG
44
+
45
+ # Combine multiple filters
46
+ linear issues --query "bug" --state Backlog --team ENG
47
+ linear issues --project abc123 --state Done
48
+
49
+ # Your assigned issues
26
50
  linear mine
51
+
52
+ # Other commands
27
53
  linear teams
28
54
  linear projects
29
- linear comment FAT-85 "This is done"
30
- linear update FAT-85 "Done"
55
+ linear comment DEV-85 "This is done"
56
+
57
+ # Update issue
58
+ linear update DEV-85 --state "Done"
59
+ linear update DEV-85 --title "New title"
60
+ linear update DEV-85 --description "Updated description"
61
+ linear update DEV-85 --state "In Progress" --title "Working on it" --description "Started implementation"
31
62
 
32
63
  Configuration:
33
64
  Set the LINEAR_API_KEY environment variable with your API key from:
@@ -53,27 +84,6 @@ when 'issue'
53
84
  exit 1
54
85
  end
55
86
 
56
- when 'search'
57
- query = ARGV.shift
58
- if query.nil? || query.empty?
59
- puts "Error: search query required"
60
- puts "Usage: linear search QUERY [--team TEAM] [--state STATE]"
61
- exit 1
62
- end
63
-
64
- options = {}
65
- OptionParser.new do |opts|
66
- opts.on("--team TEAM", "Filter by team key") { |v| options[:team] = v }
67
- opts.on("--state STATE", "Filter by state name") { |v| options[:state] = v }
68
- end.parse!
69
-
70
- begin
71
- Linear::Commands.search(query, options)
72
- rescue => e
73
- puts "Error: #{e.message}"
74
- exit 1
75
- end
76
-
77
87
  when 'mine'
78
88
  begin
79
89
  Linear::Commands.my_issues
@@ -98,6 +108,22 @@ when 'projects'
98
108
  exit 1
99
109
  end
100
110
 
111
+ when 'issues'
112
+ options = {}
113
+ OptionParser.new do |opts|
114
+ opts.on("--query QUERY", "Filter by title text") { |v| options[:query] = v }
115
+ opts.on("--project PROJECT", "Filter by project ID") { |v| options[:project] = v }
116
+ opts.on("--state STATE", "Filter by state name") { |v| options[:state] = v }
117
+ opts.on("--team TEAM", "Filter by team key") { |v| options[:team] = v }
118
+ end.parse!
119
+
120
+ begin
121
+ Linear::Commands.list_issues(options)
122
+ rescue => e
123
+ puts "Error: #{e.message}"
124
+ exit 1
125
+ end
126
+
101
127
  when 'comment'
102
128
  issue_id = ARGV.shift
103
129
  comment_body = ARGV.shift
@@ -117,16 +143,27 @@ when 'comment'
117
143
 
118
144
  when 'update'
119
145
  issue_id = ARGV.shift
120
- state_name = ARGV.shift
121
146
 
122
- if issue_id.nil? || issue_id.empty? || state_name.nil? || state_name.empty?
123
- puts "Error: issue ID and state name required"
124
- puts "Usage: linear update ISSUE_ID \"State Name\""
147
+ if issue_id.nil? || issue_id.empty?
148
+ puts "Error: issue ID required"
149
+ puts "Usage: linear update ISSUE_ID [--state STATE] [--title TITLE] [--description DESCRIPTION]"
125
150
  exit 1
126
151
  end
127
152
 
153
+ options = {}
154
+ OptionParser.new do |opts|
155
+ opts.on("--state STATE", "Update issue state") { |v| options[:state] = v }
156
+ opts.on("--title TITLE", "Update issue title") { |v| options[:title] = v }
157
+ opts.on("--description DESCRIPTION", "Update issue description") { |v| options[:description] = v }
158
+ end.parse!
159
+
128
160
  begin
129
- Linear::Commands.update_issue_state(issue_id, state_name)
161
+ Linear::Commands.update_issue(
162
+ issue_id,
163
+ state: options[:state],
164
+ title: options[:title],
165
+ description: options[:description]
166
+ )
130
167
  rescue => e
131
168
  puts "Error: #{e.message}"
132
169
  exit 1
@@ -13,16 +13,18 @@ module Linear
13
13
  end
14
14
  end
15
15
 
16
- def search(query, options = {}, client: Client.new)
17
- filter = { title: { contains: query } }
16
+ def list_issues(options = {}, client: Client.new)
17
+ filter = {}
18
+ filter[:title] = { contains: options[:query] } if options[:query]
19
+ filter[:project] = { id: { eq: options[:project] } } if options[:project]
20
+ filter[:state] = { name: { eqIgnoreCase: options[:state] } } if options[:state]
18
21
  filter[:team] = { key: { eq: options[:team] } } if options[:team]
19
- filter[:state] = { name: { eq: options[:state] } } if options[:state]
20
22
 
21
- result = client.query(Queries::SEARCH_ISSUES, { filter: filter })
23
+ result = client.query(Queries::LIST_ISSUES, { filter: filter })
22
24
 
23
25
  issues = result.dig("data", "issues", "nodes") || []
24
26
  if issues.empty?
25
- puts "No issues found matching: #{query}"
27
+ puts "No issues found"
26
28
  else
27
29
  display_issue_list(issues)
28
30
  end
@@ -81,8 +83,14 @@ module Linear
81
83
  end
82
84
  end
83
85
 
84
- def update_issue_state(issue_id, state_name, client: Client.new)
85
- # Get the issue details including team
86
+ def update_issue(issue_id, state: nil, title: nil, description: nil, client: Client.new)
87
+ # 1. Validate at least one change provided
88
+ if state.nil? && title.nil? && description.nil?
89
+ puts "Error: At least one of --state, --title, or --description must be provided"
90
+ return
91
+ end
92
+
93
+ # 2. Fetch issue to get internal UUID
86
94
  issue_result = client.query(Queries::ISSUE, { id: issue_id })
87
95
  issue = issue_result.dig("data", "issue")
88
96
 
@@ -91,65 +99,51 @@ module Linear
91
99
  return
92
100
  end
93
101
 
94
- # Get team states - need to find team ID first
95
- teams_result = client.query(Queries::TEAMS)
96
- teams = teams_result.dig("data", "teams", "nodes") || []
97
-
98
- # Find the team from the issue identifier prefix (e.g., "FAT" from "FAT-85")
99
- team_key = issue_id.split('-').first
100
- team = teams.find { |t| t['key'] == team_key }
101
-
102
- unless team
103
- puts "Error: Team not found for issue #{issue_id}"
104
- return
105
- end
102
+ # 3. If state provided, resolve state ID (existing logic)
103
+ state_id = nil
104
+ target_state = nil
105
+ if state
106
+ team_key = issue_id.split('-').first
107
+ teams_result = client.query(Queries::TEAMS)
108
+ teams = teams_result.dig("data", "teams", "nodes") || []
109
+ team = teams.find { |t| t['key'] == team_key }
106
110
 
107
- # Get workflow states for the team
108
- states_result = client.query(Queries::WORKFLOW_STATES, { teamId: team['id'] })
109
- states = states_result.dig("data", "team", "states", "nodes") || []
110
-
111
- # Find the state by name (case-insensitive)
112
- target_state = states.find { |s| s['name'].downcase == state_name.downcase }
111
+ unless team
112
+ puts "Error: Team not found for issue #{issue_id}"
113
+ return
114
+ end
113
115
 
114
- unless target_state
115
- puts "Error: State '#{state_name}' not found. Available states:"
116
- states.each { |s| puts " - #{s['name']}" }
117
- return
118
- end
116
+ states_result = client.query(Queries::WORKFLOW_STATES, { teamId: team['id'] })
117
+ states = states_result.dig("data", "team", "states", "nodes") || []
118
+ target_state = states.find { |s| s['name'].downcase == state.downcase }
119
119
 
120
- # Update the issue
121
- result = client.query(Queries::UPDATE_ISSUE, {
122
- issueId: issue['id'],
123
- stateId: target_state['id']
124
- })
120
+ unless target_state
121
+ puts "Error: State '#{state}' not found. Available states:"
122
+ states.each { |s| puts " - #{s['name']}" }
123
+ return
124
+ end
125
125
 
126
- if result.dig("data", "issueUpdate", "success")
127
- puts "Updated #{issue_id} to '#{target_state['name']}'"
128
- else
129
- puts "Error: Failed to update issue state"
126
+ state_id = target_state['id']
130
127
  end
131
- end
132
128
 
133
- def update_issue_description(issue_id, description, client: Client.new)
134
- # Get the issue to get its internal ID
135
- issue_result = client.query(Queries::ISSUE, { id: issue_id })
136
- issue = issue_result.dig("data", "issue")
129
+ # 4. Build mutation parameters
130
+ params = { issueId: issue['id'] }
131
+ params[:stateId] = state_id if state_id
132
+ params[:title] = title if title
133
+ params[:description] = description if description
137
134
 
138
- unless issue
139
- puts "Error: Issue not found: #{issue_id}"
140
- return
141
- end
142
-
143
- # Update the issue description
144
- result = client.query(Queries::UPDATE_ISSUE, {
145
- issueId: issue['id'],
146
- description: description
147
- })
135
+ # 5. Execute mutation
136
+ result = client.query(Queries::UPDATE_ISSUE, params)
148
137
 
138
+ # 6. Display results
149
139
  if result.dig("data", "issueUpdate", "success")
150
- puts "Updated #{issue_id} description"
140
+ changes = []
141
+ changes << "state to '#{target_state['name']}'" if state
142
+ changes << "title" if title
143
+ changes << "description" if description
144
+ puts "Updated #{issue_id}: #{changes.join(', ')}"
151
145
  else
152
- puts "Error: Failed to update issue description"
146
+ puts "Error: Failed to update issue"
153
147
  end
154
148
  end
155
149
 
@@ -23,7 +23,7 @@ module Linear
23
23
  }
24
24
  GQL
25
25
 
26
- SEARCH_ISSUES = <<~GQL
26
+ LIST_ISSUES = <<~GQL
27
27
  query($filter: IssueFilter!) {
28
28
  issues(filter: $filter) {
29
29
  nodes {
@@ -127,15 +127,17 @@ module Linear
127
127
  GQL
128
128
 
129
129
  UPDATE_ISSUE = <<~GQL
130
- mutation($issueId: String!, $stateId: String, $description: String) {
130
+ mutation($issueId: String!, $stateId: String, $description: String, $title: String) {
131
131
  issueUpdate(id: $issueId, input: {
132
132
  stateId: $stateId
133
133
  description: $description
134
+ title: $title
134
135
  }) {
135
136
  success
136
137
  issue {
137
138
  id
138
139
  identifier
140
+ title
139
141
  state {
140
142
  name
141
143
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linear-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dave Kinkead