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 +4 -4
- data/bin/linear +72 -35
- data/lib/linear/commands.rb +50 -56
- data/lib/linear/queries.rb +4 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d86a47411e5f3e9ab46945cded06b74c305fee7552ac4770f6c93023dddb3dd
|
|
4
|
+
data.tar.gz: 8b8aa4987546fc77b34b7addab90d6181aa6b13b8d5fa1f7a32ce96d5c2814ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
12
|
-
linear
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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?
|
|
123
|
-
puts "Error: issue ID
|
|
124
|
-
puts "Usage: linear update ISSUE_ID
|
|
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.
|
|
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
|
data/lib/linear/commands.rb
CHANGED
|
@@ -13,16 +13,18 @@ module Linear
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
filter = {
|
|
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::
|
|
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
|
|
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
|
|
85
|
-
#
|
|
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
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
states.
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
146
|
+
puts "Error: Failed to update issue"
|
|
153
147
|
end
|
|
154
148
|
end
|
|
155
149
|
|
data/lib/linear/queries.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Linear
|
|
|
23
23
|
}
|
|
24
24
|
GQL
|
|
25
25
|
|
|
26
|
-
|
|
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
|
}
|