linear-rb 0.3.0 → 0.4.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 +43 -23
- data/lib/linear/commands/add_comment.rb +29 -0
- data/lib/linear/commands/create_issue.rb +67 -0
- data/lib/linear/commands/fetch_issue.rb +18 -0
- data/lib/linear/commands/list_issues.rb +24 -0
- data/lib/linear/commands/list_projects.rb +18 -0
- data/lib/linear/commands/list_teams.rb +16 -0
- data/lib/linear/commands/my_issues.rb +18 -0
- data/lib/linear/commands/update_issue_description.rb +30 -0
- data/lib/linear/commands/update_issue_state.rb +56 -0
- data/lib/linear/commands.rb +24 -204
- data/lib/linear/formatters.rb +66 -0
- data/lib/linear/queries/create_comment.rb +18 -0
- data/lib/linear/queries/create_issue.rb +25 -0
- data/lib/linear/queries/issue.rb +26 -0
- data/lib/linear/queries/list_issues.rb +24 -0
- data/lib/linear/queries/my_issues.rb +23 -0
- data/lib/linear/queries/projects.rb +24 -0
- data/lib/linear/queries/teams.rb +15 -0
- data/lib/linear/queries/update_issue.rb +22 -0
- data/lib/linear/queries/workflow_states.rb +17 -0
- data/lib/linear/queries.rb +10 -146
- data/lib/linear/version.rb +1 -1
- data/lib/linear.rb +1 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3e9c972a48cc50b483ca7b2b0add3aaaff8d6db08c421964e9f8cad348f8f95
|
|
4
|
+
data.tar.gz: dd874783401a2a38cc14d1febddaf2271f2ff75201797b5a84b9c67177244913
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46ef93d456c996f850892082b45bd753e51874b451d6d528d4d5a8d8b1deebcf0fa49d956baf7a6ae0bc626beb8128b2f9219c79ff1c29e30fc78a9a5cce0e27
|
|
7
|
+
data.tar.gz: 003a97d70f58e93d3ccb0a2b6152e7936c9dd2abefc277d9ab20bdc646eec8e919c7b649b2875cb18b1dedf54994fc005e02fa2182845648c068b0a068559682
|
data/bin/linear
CHANGED
|
@@ -13,11 +13,9 @@ def show_usage
|
|
|
13
13
|
linear mine Show issues assigned to you
|
|
14
14
|
linear teams List all teams
|
|
15
15
|
linear projects List all projects
|
|
16
|
+
linear create [OPTIONS] Create a new issue
|
|
16
17
|
linear comment ISSUE_ID COMMENT Add a comment to an issue
|
|
17
|
-
linear update ISSUE_ID
|
|
18
|
-
--state STATE Update issue state
|
|
19
|
-
--title TITLE Update issue title
|
|
20
|
-
--description DESCRIPTION Update issue description
|
|
18
|
+
linear update ISSUE_ID STATE Update issue state
|
|
21
19
|
|
|
22
20
|
Issues Filters (all optional, can be combined):
|
|
23
21
|
--query TEXT Search by title text
|
|
@@ -25,6 +23,14 @@ def show_usage
|
|
|
25
23
|
--state STATE Filter by state name (case-insensitive)
|
|
26
24
|
--team TEAM_KEY Filter by team key
|
|
27
25
|
|
|
26
|
+
Create Issue Options:
|
|
27
|
+
--team TEAM_KEY Team key (required)
|
|
28
|
+
--title "TITLE" Issue title (required)
|
|
29
|
+
--description "DESC" Issue description (optional)
|
|
30
|
+
--priority PRIORITY Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low (optional)
|
|
31
|
+
--state "STATE" State name (optional)
|
|
32
|
+
--project PROJECT_ID Project ID (optional)
|
|
33
|
+
|
|
28
34
|
Examples:
|
|
29
35
|
# View a specific issue
|
|
30
36
|
linear issue ENG-123
|
|
@@ -49,16 +55,16 @@ def show_usage
|
|
|
49
55
|
# Your assigned issues
|
|
50
56
|
linear mine
|
|
51
57
|
|
|
58
|
+
# Create a new issue
|
|
59
|
+
linear create --team ENG --title "Fix login bug"
|
|
60
|
+
linear create --team ENG --title "Add dark mode" --description "Users want dark mode" --priority 2
|
|
61
|
+
linear create --team ENG --title "Refactor API" --state "In Progress" --priority 3
|
|
62
|
+
|
|
52
63
|
# Other commands
|
|
53
64
|
linear teams
|
|
54
65
|
linear projects
|
|
55
|
-
linear comment
|
|
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"
|
|
66
|
+
linear comment FAT-85 "This is done"
|
|
67
|
+
linear update FAT-85 "Done"
|
|
62
68
|
|
|
63
69
|
Configuration:
|
|
64
70
|
Set the LINEAR_API_KEY environment variable with your API key from:
|
|
@@ -143,27 +149,41 @@ when 'comment'
|
|
|
143
149
|
|
|
144
150
|
when 'update'
|
|
145
151
|
issue_id = ARGV.shift
|
|
152
|
+
state_name = ARGV.shift
|
|
146
153
|
|
|
147
|
-
if issue_id.nil? || issue_id.empty?
|
|
148
|
-
puts "Error: issue ID required"
|
|
149
|
-
puts "Usage: linear update ISSUE_ID
|
|
154
|
+
if issue_id.nil? || issue_id.empty? || state_name.nil? || state_name.empty?
|
|
155
|
+
puts "Error: issue ID and state name required"
|
|
156
|
+
puts "Usage: linear update ISSUE_ID \"State Name\""
|
|
157
|
+
exit 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
begin
|
|
161
|
+
Linear::Commands.update_issue_state(issue_id, state_name)
|
|
162
|
+
rescue => e
|
|
163
|
+
puts "Error: #{e.message}"
|
|
150
164
|
exit 1
|
|
151
165
|
end
|
|
152
166
|
|
|
167
|
+
when 'create'
|
|
153
168
|
options = {}
|
|
154
169
|
OptionParser.new do |opts|
|
|
155
|
-
opts.on("--
|
|
156
|
-
opts.on("--title TITLE", "
|
|
157
|
-
opts.on("--description
|
|
170
|
+
opts.on("--team TEAM", "Team key (required)") { |v| options[:team] = v }
|
|
171
|
+
opts.on("--title TITLE", "Issue title (required)") { |v| options[:title] = v }
|
|
172
|
+
opts.on("--description DESC", "Issue description") { |v| options[:description] = v }
|
|
173
|
+
opts.on("--priority PRIORITY", "Priority (0-4)") { |v| options[:priority] = v }
|
|
174
|
+
opts.on("--state STATE", "State name") { |v| options[:state] = v }
|
|
175
|
+
opts.on("--assignee EMAIL", "Assignee email") { |v| options[:assignee] = v }
|
|
176
|
+
opts.on("--project PROJECT", "Project ID") { |v| options[:project] = v }
|
|
158
177
|
end.parse!
|
|
159
178
|
|
|
179
|
+
unless options[:team] && options[:title]
|
|
180
|
+
puts "Error: --team and --title are required"
|
|
181
|
+
puts "Usage: linear create --team TEAM --title \"Issue title\" [OPTIONS]"
|
|
182
|
+
exit 1
|
|
183
|
+
end
|
|
184
|
+
|
|
160
185
|
begin
|
|
161
|
-
Linear::Commands.
|
|
162
|
-
issue_id,
|
|
163
|
-
state: options[:state],
|
|
164
|
-
title: options[:title],
|
|
165
|
-
description: options[:description]
|
|
166
|
-
)
|
|
186
|
+
Linear::Commands.create_issue(options)
|
|
167
187
|
rescue => e
|
|
168
188
|
puts "Error: #{e.message}"
|
|
169
189
|
exit 1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module AddComment
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def add_comment(issue_id, body, client: Client.new)
|
|
7
|
+
# First get the issue to get its internal ID
|
|
8
|
+
issue_result = client.query(Queries::ISSUE, { id: issue_id })
|
|
9
|
+
issue = issue_result.dig("data", "issue")
|
|
10
|
+
|
|
11
|
+
unless issue
|
|
12
|
+
puts "Error: Issue not found: #{issue_id}"
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
result = client.query(Queries::CREATE_COMMENT, {
|
|
17
|
+
issueId: issue['id'],
|
|
18
|
+
body: body
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if result.dig("data", "commentCreate", "success")
|
|
22
|
+
puts "Comment added to #{issue_id}"
|
|
23
|
+
else
|
|
24
|
+
puts "Error: Failed to add comment"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module CreateIssue
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def create_issue(options, client: Client.new)
|
|
7
|
+
# Get team ID from team key
|
|
8
|
+
teams_result = client.query(Queries::TEAMS)
|
|
9
|
+
teams = teams_result.dig("data", "teams", "nodes") || []
|
|
10
|
+
team = teams.find { |t| t['key'].upcase == options[:team].upcase }
|
|
11
|
+
|
|
12
|
+
unless team
|
|
13
|
+
puts "Error: Team '#{options[:team]}' not found. Available teams:"
|
|
14
|
+
teams.each { |t| puts " #{t['key']} - #{t['name']}" }
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
variables = {
|
|
19
|
+
teamId: team['id'],
|
|
20
|
+
title: options[:title]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Add optional description
|
|
24
|
+
variables[:description] = options[:description] if options[:description]
|
|
25
|
+
|
|
26
|
+
# Add optional project
|
|
27
|
+
variables[:projectId] = options[:project] if options[:project]
|
|
28
|
+
|
|
29
|
+
# Handle priority (convert string to integer if needed)
|
|
30
|
+
if options[:priority]
|
|
31
|
+
variables[:priority] = options[:priority].to_i
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Handle state (need to look up state ID from name)
|
|
35
|
+
if options[:state]
|
|
36
|
+
states_result = client.query(Queries::WORKFLOW_STATES, { teamId: team['id'] })
|
|
37
|
+
states = states_result.dig("data", "team", "states", "nodes") || []
|
|
38
|
+
target_state = states.find { |s| s['name'].downcase == options[:state].downcase }
|
|
39
|
+
|
|
40
|
+
if target_state
|
|
41
|
+
variables[:stateId] = target_state['id']
|
|
42
|
+
else
|
|
43
|
+
puts "Warning: State '#{options[:state]}' not found, using default"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Handle assignee (need to look up user ID from email)
|
|
48
|
+
if options[:assignee]
|
|
49
|
+
# Would need a new USER_BY_EMAIL query
|
|
50
|
+
puts "Warning: Assignee lookup not yet implemented"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create the issue
|
|
54
|
+
result = client.query(Queries::CREATE_ISSUE, variables)
|
|
55
|
+
|
|
56
|
+
if result.dig("data", "issueCreate", "success")
|
|
57
|
+
issue = result.dig("data", "issueCreate", "issue")
|
|
58
|
+
puts "Created issue: #{issue['identifier']}"
|
|
59
|
+
puts "Title: #{issue['title']}"
|
|
60
|
+
puts "URL: #{issue['url']}"
|
|
61
|
+
else
|
|
62
|
+
puts "Error: Failed to create issue"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module FetchIssue
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def fetch_issue(issue_id, client: Client.new)
|
|
7
|
+
result = client.query(Queries::ISSUE, { id: issue_id })
|
|
8
|
+
|
|
9
|
+
issue = result.dig("data", "issue")
|
|
10
|
+
if issue
|
|
11
|
+
Formatters.display_issue(issue)
|
|
12
|
+
else
|
|
13
|
+
puts "Issue not found: #{issue_id}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module ListIssues
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def list_issues(options = {}, client: Client.new)
|
|
7
|
+
filter = {}
|
|
8
|
+
filter[:title] = { contains: options[:query] } if options[:query]
|
|
9
|
+
filter[:project] = { id: { eq: options[:project] } } if options[:project]
|
|
10
|
+
filter[:state] = { name: { eqIgnoreCase: options[:state] } } if options[:state]
|
|
11
|
+
filter[:team] = { key: { eq: options[:team] } } if options[:team]
|
|
12
|
+
|
|
13
|
+
result = client.query(Queries::LIST_ISSUES, { filter: filter })
|
|
14
|
+
|
|
15
|
+
issues = result.dig("data", "issues", "nodes") || []
|
|
16
|
+
if issues.empty?
|
|
17
|
+
puts "No issues found"
|
|
18
|
+
else
|
|
19
|
+
Formatters.display_issue_list(issues)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module ListProjects
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def list_projects(client: Client.new)
|
|
7
|
+
result = client.query(Queries::PROJECTS)
|
|
8
|
+
|
|
9
|
+
projects = result.dig("data", "projects", "nodes") || []
|
|
10
|
+
if projects.empty?
|
|
11
|
+
puts "No projects found"
|
|
12
|
+
else
|
|
13
|
+
Formatters.display_project_list(projects)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module ListTeams
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def list_teams(client: Client.new)
|
|
7
|
+
result = client.query(Queries::TEAMS)
|
|
8
|
+
|
|
9
|
+
teams = result.dig("data", "teams", "nodes") || []
|
|
10
|
+
teams.each do |team|
|
|
11
|
+
puts "#{team['key'].ljust(10)} #{team['name']}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module MyIssues
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def my_issues(client: Client.new)
|
|
7
|
+
result = client.query(Queries::MY_ISSUES)
|
|
8
|
+
|
|
9
|
+
issues = result.dig("data", "viewer", "assignedIssues", "nodes") || []
|
|
10
|
+
if issues.empty?
|
|
11
|
+
puts "No issues assigned to you"
|
|
12
|
+
else
|
|
13
|
+
Formatters.display_issue_list(issues)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module UpdateIssueDescription
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def update_issue_description(issue_id, description, client: Client.new)
|
|
7
|
+
# Get the issue to get its internal ID
|
|
8
|
+
issue_result = client.query(Queries::ISSUE, { id: issue_id })
|
|
9
|
+
issue = issue_result.dig("data", "issue")
|
|
10
|
+
|
|
11
|
+
unless issue
|
|
12
|
+
puts "Error: Issue not found: #{issue_id}"
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Update the issue description
|
|
17
|
+
result = client.query(Queries::UPDATE_ISSUE, {
|
|
18
|
+
issueId: issue['id'],
|
|
19
|
+
description: description
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
if result.dig("data", "issueUpdate", "success")
|
|
23
|
+
puts "Updated #{issue_id} description"
|
|
24
|
+
else
|
|
25
|
+
puts "Error: Failed to update issue description"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Commands
|
|
3
|
+
module UpdateIssueState
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
def update_issue_state(issue_id, state_name, client: Client.new)
|
|
7
|
+
# Get the issue details including team
|
|
8
|
+
issue_result = client.query(Queries::ISSUE, { id: issue_id })
|
|
9
|
+
issue = issue_result.dig("data", "issue")
|
|
10
|
+
|
|
11
|
+
unless issue
|
|
12
|
+
puts "Error: Issue not found: #{issue_id}"
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get team states - need to find team ID first
|
|
17
|
+
teams_result = client.query(Queries::TEAMS)
|
|
18
|
+
teams = teams_result.dig("data", "teams", "nodes") || []
|
|
19
|
+
|
|
20
|
+
# Find the team from the issue identifier prefix (e.g., "FAT" from "FAT-85")
|
|
21
|
+
team_key = issue_id.split('-').first
|
|
22
|
+
team = teams.find { |t| t['key'] == team_key }
|
|
23
|
+
|
|
24
|
+
unless team
|
|
25
|
+
puts "Error: Team not found for issue #{issue_id}"
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get workflow states for the team
|
|
30
|
+
states_result = client.query(Queries::WORKFLOW_STATES, { teamId: team['id'] })
|
|
31
|
+
states = states_result.dig("data", "team", "states", "nodes") || []
|
|
32
|
+
|
|
33
|
+
# Find the state by name (case-insensitive)
|
|
34
|
+
target_state = states.find { |s| s['name'].downcase == state_name.downcase }
|
|
35
|
+
|
|
36
|
+
unless target_state
|
|
37
|
+
puts "Error: State '#{state_name}' not found. Available states:"
|
|
38
|
+
states.each { |s| puts " - #{s['name']}" }
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Update the issue
|
|
43
|
+
result = client.query(Queries::UPDATE_ISSUE, {
|
|
44
|
+
issueId: issue['id'],
|
|
45
|
+
stateId: target_state['id']
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if result.dig("data", "issueUpdate", "success")
|
|
49
|
+
puts "Updated #{issue_id} to '#{target_state['name']}'"
|
|
50
|
+
else
|
|
51
|
+
puts "Error: Failed to update issue state"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/linear/commands.rb
CHANGED
|
@@ -1,212 +1,32 @@
|
|
|
1
|
+
require_relative "commands/fetch_issue"
|
|
2
|
+
require_relative "commands/list_issues"
|
|
3
|
+
require_relative "commands/my_issues"
|
|
4
|
+
require_relative "commands/list_teams"
|
|
5
|
+
require_relative "commands/list_projects"
|
|
6
|
+
require_relative "commands/add_comment"
|
|
7
|
+
require_relative "commands/update_issue_state"
|
|
8
|
+
require_relative "commands/update_issue_description"
|
|
9
|
+
require_relative "commands/create_issue"
|
|
10
|
+
|
|
1
11
|
module Linear
|
|
2
12
|
module Commands
|
|
3
13
|
extend self
|
|
4
14
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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]
|
|
21
|
-
filter[:team] = { key: { eq: options[:team] } } if options[:team]
|
|
22
|
-
|
|
23
|
-
result = client.query(Queries::LIST_ISSUES, { filter: filter })
|
|
24
|
-
|
|
25
|
-
issues = result.dig("data", "issues", "nodes") || []
|
|
26
|
-
if issues.empty?
|
|
27
|
-
puts "No issues found"
|
|
28
|
-
else
|
|
29
|
-
display_issue_list(issues)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def my_issues(client: Client.new)
|
|
34
|
-
result = client.query(Queries::MY_ISSUES)
|
|
35
|
-
|
|
36
|
-
issues = result.dig("data", "viewer", "assignedIssues", "nodes") || []
|
|
37
|
-
if issues.empty?
|
|
38
|
-
puts "No issues assigned to you"
|
|
39
|
-
else
|
|
40
|
-
display_issue_list(issues)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def list_teams(client: Client.new)
|
|
45
|
-
result = client.query(Queries::TEAMS)
|
|
46
|
-
|
|
47
|
-
teams = result.dig("data", "teams", "nodes") || []
|
|
48
|
-
teams.each do |team|
|
|
49
|
-
puts "#{team['key'].ljust(10)} #{team['name']}"
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def list_projects(client: Client.new)
|
|
54
|
-
result = client.query(Queries::PROJECTS)
|
|
55
|
-
|
|
56
|
-
projects = result.dig("data", "projects", "nodes") || []
|
|
57
|
-
if projects.empty?
|
|
58
|
-
puts "No projects found"
|
|
59
|
-
else
|
|
60
|
-
display_project_list(projects)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def add_comment(issue_id, body, client: Client.new)
|
|
65
|
-
# First get the issue to get its internal ID
|
|
66
|
-
issue_result = client.query(Queries::ISSUE, { id: issue_id })
|
|
67
|
-
issue = issue_result.dig("data", "issue")
|
|
68
|
-
|
|
69
|
-
unless issue
|
|
70
|
-
puts "Error: Issue not found: #{issue_id}"
|
|
71
|
-
return
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
result = client.query(Queries::CREATE_COMMENT, {
|
|
75
|
-
issueId: issue['id'],
|
|
76
|
-
body: body
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
if result.dig("data", "commentCreate", "success")
|
|
80
|
-
puts "Comment added to #{issue_id}"
|
|
81
|
-
else
|
|
82
|
-
puts "Error: Failed to add comment"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
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
|
|
94
|
-
issue_result = client.query(Queries::ISSUE, { id: issue_id })
|
|
95
|
-
issue = issue_result.dig("data", "issue")
|
|
96
|
-
|
|
97
|
-
unless issue
|
|
98
|
-
puts "Error: Issue not found: #{issue_id}"
|
|
99
|
-
return
|
|
100
|
-
end
|
|
101
|
-
|
|
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 }
|
|
110
|
-
|
|
111
|
-
unless team
|
|
112
|
-
puts "Error: Team not found for issue #{issue_id}"
|
|
113
|
-
return
|
|
114
|
-
end
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
state_id = target_state['id']
|
|
127
|
-
end
|
|
128
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
# 5. Execute mutation
|
|
136
|
-
result = client.query(Queries::UPDATE_ISSUE, params)
|
|
137
|
-
|
|
138
|
-
# 6. Display results
|
|
139
|
-
if result.dig("data", "issueUpdate", "success")
|
|
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(', ')}"
|
|
145
|
-
else
|
|
146
|
-
puts "Error: Failed to update issue"
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
private
|
|
151
|
-
|
|
152
|
-
def display_issue(issue)
|
|
153
|
-
puts "\n#{issue['identifier']}: #{issue['title']}"
|
|
154
|
-
puts "=" * 60
|
|
155
|
-
puts "Status: #{issue['state']['name']}"
|
|
156
|
-
puts "Assignee: #{issue.dig('assignee', 'name') || 'Unassigned'}"
|
|
157
|
-
puts "Priority: #{priority_label(issue['priority'])}"
|
|
158
|
-
puts "URL: #{issue['url']}"
|
|
159
|
-
puts "\nDescription:"
|
|
160
|
-
puts issue['description'] || "(no description)"
|
|
161
|
-
puts ""
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def display_issue_list(issues)
|
|
165
|
-
puts "\nFound #{issues.length} issue(s):\n\n"
|
|
166
|
-
issues.each do |issue|
|
|
167
|
-
state_badge = "[#{issue['state']['name']}]".ljust(15)
|
|
168
|
-
priority_badge = priority_label(issue['priority']).ljust(8)
|
|
169
|
-
assignee = (issue.dig('assignee', 'name') || 'Unassigned').ljust(15)
|
|
170
|
-
|
|
171
|
-
puts "#{issue['identifier'].ljust(12)} #{state_badge} #{priority_badge} #{assignee} #{issue['title']}"
|
|
172
|
-
end
|
|
173
|
-
puts ""
|
|
174
|
-
end
|
|
175
|
-
|
|
15
|
+
# Include all command modules
|
|
16
|
+
include FetchIssue
|
|
17
|
+
include ListIssues
|
|
18
|
+
include MyIssues
|
|
19
|
+
include ListTeams
|
|
20
|
+
include ListProjects
|
|
21
|
+
include AddComment
|
|
22
|
+
include UpdateIssueState
|
|
23
|
+
include UpdateIssueDescription
|
|
24
|
+
include CreateIssue
|
|
25
|
+
|
|
26
|
+
# Expose formatters for backward compatibility
|
|
176
27
|
def priority_label(priority)
|
|
177
|
-
|
|
178
|
-
when 0 then "None"
|
|
179
|
-
when 1 then "Urgent"
|
|
180
|
-
when 2 then "High"
|
|
181
|
-
when 3 then "Medium"
|
|
182
|
-
when 4 then "Low"
|
|
183
|
-
else "Unknown"
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def display_project_list(projects)
|
|
188
|
-
puts "\nFound #{projects.length} project(s):\n\n"
|
|
189
|
-
projects.each do |project|
|
|
190
|
-
state_badge = "[#{project['state']}]".ljust(15)
|
|
191
|
-
progress = project['progress'] ? "#{(project['progress'] * 100).round}%" : "0%"
|
|
192
|
-
progress_badge = progress.ljust(6)
|
|
193
|
-
lead = (project.dig('lead', 'name') || 'No lead').ljust(20)
|
|
194
|
-
|
|
195
|
-
puts "#{project['name'].ljust(30)} #{state_badge} #{progress_badge} #{lead}"
|
|
196
|
-
|
|
197
|
-
if project['description'] && !project['description'].empty?
|
|
198
|
-
# Show first line of description
|
|
199
|
-
first_line = project['description'].lines.first&.strip
|
|
200
|
-
puts " #{first_line[0..80]}#{'...' if first_line && first_line.length > 80}" if first_line
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
if project['targetDate']
|
|
204
|
-
puts " Target: #{project['targetDate']}"
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
puts " URL: #{project['url']}" if project['url']
|
|
208
|
-
puts ""
|
|
209
|
-
end
|
|
28
|
+
Formatters.priority_label(priority)
|
|
210
29
|
end
|
|
30
|
+
private :priority_label
|
|
211
31
|
end
|
|
212
32
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Formatters
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def display_issue(issue)
|
|
6
|
+
puts "\n#{issue['identifier']}: #{issue['title']}"
|
|
7
|
+
puts "=" * 60
|
|
8
|
+
puts "Status: #{issue['state']['name']}"
|
|
9
|
+
puts "Assignee: #{issue.dig('assignee', 'name') || 'Unassigned'}"
|
|
10
|
+
puts "Priority: #{priority_label(issue['priority'])}"
|
|
11
|
+
puts "URL: #{issue['url']}"
|
|
12
|
+
puts "\nDescription:"
|
|
13
|
+
puts issue['description'] || "(no description)"
|
|
14
|
+
puts ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def display_issue_list(issues)
|
|
18
|
+
puts "\nFound #{issues.length} issue(s):\n\n"
|
|
19
|
+
issues.each do |issue|
|
|
20
|
+
state_badge = "[#{issue['state']['name']}]".ljust(15)
|
|
21
|
+
priority_badge = priority_label(issue['priority']).ljust(8)
|
|
22
|
+
assignee = (issue.dig('assignee', 'name') || 'Unassigned').ljust(15)
|
|
23
|
+
|
|
24
|
+
puts "#{issue['identifier'].ljust(12)} #{state_badge} #{priority_badge} #{assignee} #{issue['title']}"
|
|
25
|
+
end
|
|
26
|
+
puts ""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def priority_label(priority)
|
|
30
|
+
case priority
|
|
31
|
+
when 0 then "None"
|
|
32
|
+
when 1 then "Urgent"
|
|
33
|
+
when 2 then "High"
|
|
34
|
+
when 3 then "Medium"
|
|
35
|
+
when 4 then "Low"
|
|
36
|
+
else "Unknown"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def display_project_list(projects)
|
|
41
|
+
puts "\nFound #{projects.length} project(s):\n\n"
|
|
42
|
+
projects.each do |project|
|
|
43
|
+
state_badge = "[#{project['state']}]".ljust(15)
|
|
44
|
+
progress = project['progress'] ? "#{(project['progress'] * 100).round}%" : "0%"
|
|
45
|
+
progress_badge = progress.ljust(6)
|
|
46
|
+
lead = (project.dig('lead', 'name') || 'No lead').ljust(20)
|
|
47
|
+
|
|
48
|
+
puts "#{project['name'].ljust(30)} #{state_badge} #{progress_badge} #{lead}"
|
|
49
|
+
puts " ID: #{project['id']}"
|
|
50
|
+
|
|
51
|
+
if project['description'] && !project['description'].empty?
|
|
52
|
+
# Show first line of description
|
|
53
|
+
first_line = project['description'].lines.first&.strip
|
|
54
|
+
puts " #{first_line[0..80]}#{'...' if first_line && first_line.length > 80}" if first_line
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if project['targetDate']
|
|
58
|
+
puts " Target: #{project['targetDate']}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
puts " URL: #{project['url']}" if project['url']
|
|
62
|
+
puts ""
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Queries
|
|
3
|
+
CREATE_ISSUE = <<~GQL
|
|
4
|
+
mutation($teamId: String!, $title: String!, $description: String, $priority: Int, $stateId: String, $assigneeId: String, $projectId: String) {
|
|
5
|
+
issueCreate(input: {
|
|
6
|
+
teamId: $teamId
|
|
7
|
+
title: $title
|
|
8
|
+
description: $description
|
|
9
|
+
priority: $priority
|
|
10
|
+
stateId: $stateId
|
|
11
|
+
assigneeId: $assigneeId
|
|
12
|
+
projectId: $projectId
|
|
13
|
+
}) {
|
|
14
|
+
success
|
|
15
|
+
issue {
|
|
16
|
+
id
|
|
17
|
+
identifier
|
|
18
|
+
title
|
|
19
|
+
url
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
GQL
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Queries
|
|
3
|
+
ISSUE = <<~GQL
|
|
4
|
+
query($id: String!) {
|
|
5
|
+
issue(id: $id) {
|
|
6
|
+
id
|
|
7
|
+
identifier
|
|
8
|
+
title
|
|
9
|
+
description
|
|
10
|
+
state {
|
|
11
|
+
name
|
|
12
|
+
type
|
|
13
|
+
}
|
|
14
|
+
assignee {
|
|
15
|
+
name
|
|
16
|
+
email
|
|
17
|
+
}
|
|
18
|
+
priority
|
|
19
|
+
createdAt
|
|
20
|
+
updatedAt
|
|
21
|
+
url
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
GQL
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Queries
|
|
3
|
+
LIST_ISSUES = <<~GQL
|
|
4
|
+
query($filter: IssueFilter!) {
|
|
5
|
+
issues(filter: $filter) {
|
|
6
|
+
nodes {
|
|
7
|
+
id
|
|
8
|
+
identifier
|
|
9
|
+
title
|
|
10
|
+
state {
|
|
11
|
+
name
|
|
12
|
+
type
|
|
13
|
+
}
|
|
14
|
+
assignee {
|
|
15
|
+
name
|
|
16
|
+
}
|
|
17
|
+
priority
|
|
18
|
+
url
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
GQL
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Linear
|
|
2
|
+
module Queries
|
|
3
|
+
UPDATE_ISSUE = <<~GQL
|
|
4
|
+
mutation($issueId: String!, $stateId: String, $description: String) {
|
|
5
|
+
issueUpdate(id: $issueId, input: {
|
|
6
|
+
stateId: $stateId
|
|
7
|
+
description: $description
|
|
8
|
+
}) {
|
|
9
|
+
success
|
|
10
|
+
issue {
|
|
11
|
+
id
|
|
12
|
+
identifier
|
|
13
|
+
state {
|
|
14
|
+
name
|
|
15
|
+
}
|
|
16
|
+
description
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
GQL
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/linear/queries.rb
CHANGED
|
@@ -1,150 +1,14 @@
|
|
|
1
|
+
require_relative "queries/issue"
|
|
2
|
+
require_relative "queries/list_issues"
|
|
3
|
+
require_relative "queries/my_issues"
|
|
4
|
+
require_relative "queries/teams"
|
|
5
|
+
require_relative "queries/projects"
|
|
6
|
+
require_relative "queries/workflow_states"
|
|
7
|
+
require_relative "queries/create_comment"
|
|
8
|
+
require_relative "queries/update_issue"
|
|
9
|
+
require_relative "queries/create_issue"
|
|
10
|
+
|
|
1
11
|
module Linear
|
|
2
12
|
module Queries
|
|
3
|
-
ISSUE = <<~GQL
|
|
4
|
-
query($id: String!) {
|
|
5
|
-
issue(id: $id) {
|
|
6
|
-
id
|
|
7
|
-
identifier
|
|
8
|
-
title
|
|
9
|
-
description
|
|
10
|
-
state {
|
|
11
|
-
name
|
|
12
|
-
type
|
|
13
|
-
}
|
|
14
|
-
assignee {
|
|
15
|
-
name
|
|
16
|
-
email
|
|
17
|
-
}
|
|
18
|
-
priority
|
|
19
|
-
createdAt
|
|
20
|
-
updatedAt
|
|
21
|
-
url
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
GQL
|
|
25
|
-
|
|
26
|
-
LIST_ISSUES = <<~GQL
|
|
27
|
-
query($filter: IssueFilter!) {
|
|
28
|
-
issues(filter: $filter) {
|
|
29
|
-
nodes {
|
|
30
|
-
id
|
|
31
|
-
identifier
|
|
32
|
-
title
|
|
33
|
-
state {
|
|
34
|
-
name
|
|
35
|
-
type
|
|
36
|
-
}
|
|
37
|
-
assignee {
|
|
38
|
-
name
|
|
39
|
-
}
|
|
40
|
-
priority
|
|
41
|
-
url
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
GQL
|
|
46
|
-
|
|
47
|
-
MY_ISSUES = <<~GQL
|
|
48
|
-
query {
|
|
49
|
-
viewer {
|
|
50
|
-
assignedIssues {
|
|
51
|
-
nodes {
|
|
52
|
-
id
|
|
53
|
-
identifier
|
|
54
|
-
title
|
|
55
|
-
state {
|
|
56
|
-
name
|
|
57
|
-
type
|
|
58
|
-
}
|
|
59
|
-
priority
|
|
60
|
-
url
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
GQL
|
|
66
|
-
|
|
67
|
-
TEAMS = <<~GQL
|
|
68
|
-
query {
|
|
69
|
-
teams {
|
|
70
|
-
nodes {
|
|
71
|
-
id
|
|
72
|
-
key
|
|
73
|
-
name
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
GQL
|
|
78
|
-
|
|
79
|
-
PROJECTS = <<~GQL
|
|
80
|
-
query {
|
|
81
|
-
projects {
|
|
82
|
-
nodes {
|
|
83
|
-
id
|
|
84
|
-
name
|
|
85
|
-
description
|
|
86
|
-
state
|
|
87
|
-
progress
|
|
88
|
-
startDate
|
|
89
|
-
targetDate
|
|
90
|
-
url
|
|
91
|
-
lead {
|
|
92
|
-
name
|
|
93
|
-
email
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
GQL
|
|
99
|
-
|
|
100
|
-
WORKFLOW_STATES = <<~GQL
|
|
101
|
-
query($teamId: String!) {
|
|
102
|
-
team(id: $teamId) {
|
|
103
|
-
states {
|
|
104
|
-
nodes {
|
|
105
|
-
id
|
|
106
|
-
name
|
|
107
|
-
type
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
GQL
|
|
113
|
-
|
|
114
|
-
CREATE_COMMENT = <<~GQL
|
|
115
|
-
mutation($issueId: String!, $body: String!) {
|
|
116
|
-
commentCreate(input: {
|
|
117
|
-
issueId: $issueId
|
|
118
|
-
body: $body
|
|
119
|
-
}) {
|
|
120
|
-
success
|
|
121
|
-
comment {
|
|
122
|
-
id
|
|
123
|
-
body
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
GQL
|
|
128
|
-
|
|
129
|
-
UPDATE_ISSUE = <<~GQL
|
|
130
|
-
mutation($issueId: String!, $stateId: String, $description: String, $title: String) {
|
|
131
|
-
issueUpdate(id: $issueId, input: {
|
|
132
|
-
stateId: $stateId
|
|
133
|
-
description: $description
|
|
134
|
-
title: $title
|
|
135
|
-
}) {
|
|
136
|
-
success
|
|
137
|
-
issue {
|
|
138
|
-
id
|
|
139
|
-
identifier
|
|
140
|
-
title
|
|
141
|
-
state {
|
|
142
|
-
name
|
|
143
|
-
}
|
|
144
|
-
description
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
GQL
|
|
149
13
|
end
|
|
150
14
|
end
|
data/lib/linear/version.rb
CHANGED
data/lib/linear.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dave Kinkead
|
|
@@ -36,7 +36,26 @@ files:
|
|
|
36
36
|
- lib/linear.rb
|
|
37
37
|
- lib/linear/client.rb
|
|
38
38
|
- lib/linear/commands.rb
|
|
39
|
+
- lib/linear/commands/add_comment.rb
|
|
40
|
+
- lib/linear/commands/create_issue.rb
|
|
41
|
+
- lib/linear/commands/fetch_issue.rb
|
|
42
|
+
- lib/linear/commands/list_issues.rb
|
|
43
|
+
- lib/linear/commands/list_projects.rb
|
|
44
|
+
- lib/linear/commands/list_teams.rb
|
|
45
|
+
- lib/linear/commands/my_issues.rb
|
|
46
|
+
- lib/linear/commands/update_issue_description.rb
|
|
47
|
+
- lib/linear/commands/update_issue_state.rb
|
|
48
|
+
- lib/linear/formatters.rb
|
|
39
49
|
- lib/linear/queries.rb
|
|
50
|
+
- lib/linear/queries/create_comment.rb
|
|
51
|
+
- lib/linear/queries/create_issue.rb
|
|
52
|
+
- lib/linear/queries/issue.rb
|
|
53
|
+
- lib/linear/queries/list_issues.rb
|
|
54
|
+
- lib/linear/queries/my_issues.rb
|
|
55
|
+
- lib/linear/queries/projects.rb
|
|
56
|
+
- lib/linear/queries/teams.rb
|
|
57
|
+
- lib/linear/queries/update_issue.rb
|
|
58
|
+
- lib/linear/queries/workflow_states.rb
|
|
40
59
|
- lib/linear/version.rb
|
|
41
60
|
- readme.md
|
|
42
61
|
homepage: https://github.com/davekinkead/linear-rb
|