claude-task-master 0.2.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 +7 -0
- data/CLAUDE.md +192 -0
- data/README.md +231 -0
- data/bin/claude-task-master +6 -0
- data/lib/claude_task_master/claude.rb +305 -0
- data/lib/claude_task_master/cli.rb +537 -0
- data/lib/claude_task_master/github.rb +345 -0
- data/lib/claude_task_master/loop.rb +250 -0
- data/lib/claude_task_master/pr_comment.rb +170 -0
- data/lib/claude_task_master/state.rb +183 -0
- data/lib/claude_task_master/version.rb +5 -0
- data/lib/claude_task_master.rb +17 -0
- metadata +141 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'pastel'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module ClaudeTaskMaster
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc 'start GOAL', 'Start working on a new goal'
|
|
14
|
+
long_desc <<~DESC
|
|
15
|
+
Start claude-task-master with a new goal. Claude will:
|
|
16
|
+
|
|
17
|
+
1. Analyze the codebase
|
|
18
|
+
2. Create a plan in .claude-task-master/plan.md
|
|
19
|
+
3. Ask for success criteria
|
|
20
|
+
4. Work autonomously until done
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
claude-task-master start "build a REST API with user auth"
|
|
24
|
+
claude-task-master start "fix all TypeScript errors"
|
|
25
|
+
claude-task-master start "add dark mode to the UI"
|
|
26
|
+
DESC
|
|
27
|
+
option :criteria, type: :string, aliases: '-c', desc: 'Success criteria (will prompt if not provided)'
|
|
28
|
+
option :model, type: :string, default: 'sonnet', desc: 'Claude model to use (sonnet, opus, haiku)'
|
|
29
|
+
option :no_merge, type: :boolean, default: false, desc: 'Do not auto-merge PRs (require manual merge)'
|
|
30
|
+
option :max_sessions, type: :numeric, aliases: '-m', desc: 'Maximum number of sessions before stopping'
|
|
31
|
+
option :pause_on_pr, type: :boolean, default: false, desc: 'Pause after creating each PR for review'
|
|
32
|
+
option :verbose, type: :boolean, aliases: '-v', default: false, desc: 'Show verbose output'
|
|
33
|
+
def start(goal)
|
|
34
|
+
check_prerequisites!
|
|
35
|
+
|
|
36
|
+
criteria = options[:criteria] || prompt_for_criteria
|
|
37
|
+
|
|
38
|
+
state = State.new
|
|
39
|
+
loop_opts = {
|
|
40
|
+
no_merge: options[:no_merge],
|
|
41
|
+
max_sessions: options[:max_sessions],
|
|
42
|
+
pause_on_pr: options[:pause_on_pr],
|
|
43
|
+
verbose: options[:verbose]
|
|
44
|
+
}
|
|
45
|
+
loop = Loop.new(state:, model: options[:model], **loop_opts)
|
|
46
|
+
loop.run(goal:, criteria:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc 'resume', 'Resume previous work'
|
|
50
|
+
long_desc <<~DESC
|
|
51
|
+
Resume working from where you left off. State is loaded from
|
|
52
|
+
.claude-task-master/ directory.
|
|
53
|
+
|
|
54
|
+
Use this after:
|
|
55
|
+
- Pressing Ctrl+C to pause
|
|
56
|
+
- Restarting your terminal
|
|
57
|
+
- Coming back the next day
|
|
58
|
+
DESC
|
|
59
|
+
option :model, type: :string, default: 'sonnet', desc: 'Claude model to use'
|
|
60
|
+
option :no_merge, type: :boolean, default: false, desc: 'Do not auto-merge PRs'
|
|
61
|
+
option :max_sessions, type: :numeric, aliases: '-m', desc: 'Maximum sessions before stopping'
|
|
62
|
+
option :pause_on_pr, type: :boolean, default: false, desc: 'Pause after creating each PR'
|
|
63
|
+
option :verbose, type: :boolean, aliases: '-v', default: false, desc: 'Show verbose output'
|
|
64
|
+
def resume
|
|
65
|
+
check_prerequisites!
|
|
66
|
+
|
|
67
|
+
state = State.new
|
|
68
|
+
unless state.exists?
|
|
69
|
+
pastel = Pastel.new
|
|
70
|
+
puts pastel.red("No existing state found in .claude-task-master/")
|
|
71
|
+
puts "Run 'claude-task-master start \"your goal\"' to begin."
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
loop_opts = {
|
|
76
|
+
no_merge: options[:no_merge],
|
|
77
|
+
max_sessions: options[:max_sessions],
|
|
78
|
+
pause_on_pr: options[:pause_on_pr],
|
|
79
|
+
verbose: options[:verbose]
|
|
80
|
+
}
|
|
81
|
+
loop = Loop.new(state:, model: options[:model], **loop_opts)
|
|
82
|
+
loop.resume
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc 'status', 'Show current status'
|
|
86
|
+
def status
|
|
87
|
+
state = State.new
|
|
88
|
+
pastel = Pastel.new
|
|
89
|
+
|
|
90
|
+
unless state.exists?
|
|
91
|
+
puts pastel.yellow("No active task. Run 'claude-task-master start \"goal\"' to begin.")
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
current = state.load_state
|
|
96
|
+
|
|
97
|
+
puts pastel.cyan.bold("claude-task-master status")
|
|
98
|
+
puts
|
|
99
|
+
puts "Goal: #{state.goal}"
|
|
100
|
+
puts "Criteria: #{state.criteria}"
|
|
101
|
+
puts
|
|
102
|
+
puts "Status: #{status_color(current[:status], pastel)}"
|
|
103
|
+
puts "Current task: #{current[:current_task] || 'none'}"
|
|
104
|
+
puts "PR: #{current[:pr_number] ? "##{current[:pr_number]}" : 'none'}"
|
|
105
|
+
puts "Sessions: #{current[:session_count]}"
|
|
106
|
+
puts "Started: #{current[:started_at]}"
|
|
107
|
+
puts "Updated: #{current[:updated_at]}"
|
|
108
|
+
puts
|
|
109
|
+
puts "State dir: #{state.dir}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'plan', 'Show the current plan'
|
|
113
|
+
def plan
|
|
114
|
+
state = State.new
|
|
115
|
+
pastel = Pastel.new
|
|
116
|
+
|
|
117
|
+
unless state.exists?
|
|
118
|
+
puts pastel.yellow("No active task.")
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
plan_content = state.plan
|
|
123
|
+
if plan_content
|
|
124
|
+
puts plan_content
|
|
125
|
+
else
|
|
126
|
+
puts pastel.yellow("No plan yet. Run 'claude-task-master resume' to generate one.")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
desc 'logs', 'Show session logs'
|
|
131
|
+
option :session, type: :numeric, aliases: '-s', desc: 'Specific session number'
|
|
132
|
+
option :last, type: :numeric, aliases: '-l', default: 1, desc: 'Show last N sessions'
|
|
133
|
+
def logs
|
|
134
|
+
state = State.new
|
|
135
|
+
pastel = Pastel.new
|
|
136
|
+
|
|
137
|
+
unless state.exists?
|
|
138
|
+
puts pastel.yellow("No active task.")
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
logs_dir = File.join(state.dir, 'logs')
|
|
143
|
+
log_files = Dir.glob(File.join(logs_dir, 'session-*.md')).sort
|
|
144
|
+
|
|
145
|
+
if options[:session]
|
|
146
|
+
filename = format('session-%03d.md', options[:session])
|
|
147
|
+
path = File.join(logs_dir, filename)
|
|
148
|
+
if File.exist?(path)
|
|
149
|
+
puts File.read(path)
|
|
150
|
+
else
|
|
151
|
+
puts pastel.red("Session #{options[:session]} not found.")
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
log_files.last(options[:last]).each do |path|
|
|
155
|
+
puts pastel.cyan("=== #{File.basename(path)} ===")
|
|
156
|
+
puts File.read(path)
|
|
157
|
+
puts
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
desc 'version', 'Show version'
|
|
163
|
+
def version
|
|
164
|
+
puts "claude-task-master #{VERSION}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
desc 'doctor', 'Check prerequisites and system health'
|
|
168
|
+
def doctor
|
|
169
|
+
pastel = Pastel.new
|
|
170
|
+
all_good = true
|
|
171
|
+
|
|
172
|
+
puts pastel.cyan.bold("claude-task-master doctor")
|
|
173
|
+
puts
|
|
174
|
+
|
|
175
|
+
# Check Claude CLI
|
|
176
|
+
print "Claude CLI: "
|
|
177
|
+
if Claude.available?
|
|
178
|
+
version = Claude.version
|
|
179
|
+
puts pastel.green("#{version}")
|
|
180
|
+
else
|
|
181
|
+
puts pastel.red("NOT FOUND")
|
|
182
|
+
puts pastel.dim(" Install: npm install -g @anthropic-ai/claude-code")
|
|
183
|
+
all_good = false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check gh CLI
|
|
187
|
+
print "GitHub CLI: "
|
|
188
|
+
gh_version = `gh --version 2>&1`.split("\n").first rescue nil
|
|
189
|
+
if gh_version
|
|
190
|
+
puts pastel.green(gh_version)
|
|
191
|
+
else
|
|
192
|
+
puts pastel.red("NOT FOUND")
|
|
193
|
+
puts pastel.dim(" Install: https://cli.github.com/")
|
|
194
|
+
all_good = false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Check gh auth
|
|
198
|
+
print "GitHub Auth: "
|
|
199
|
+
if GitHub.available?
|
|
200
|
+
puts pastel.green("authenticated")
|
|
201
|
+
else
|
|
202
|
+
puts pastel.yellow("NOT AUTHENTICATED")
|
|
203
|
+
puts pastel.dim(" Run: gh auth login")
|
|
204
|
+
all_good = false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Check git
|
|
208
|
+
print "Git: "
|
|
209
|
+
git_version = `git --version 2>&1`.strip rescue nil
|
|
210
|
+
if git_version
|
|
211
|
+
puts pastel.green(git_version)
|
|
212
|
+
else
|
|
213
|
+
puts pastel.red("NOT FOUND")
|
|
214
|
+
all_good = false
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check Ruby version
|
|
218
|
+
print "Ruby: "
|
|
219
|
+
if RUBY_VERSION >= '3.1.0'
|
|
220
|
+
puts pastel.green("#{RUBY_VERSION}")
|
|
221
|
+
else
|
|
222
|
+
puts pastel.yellow("#{RUBY_VERSION} (recommend 3.1+)")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Check current repo
|
|
226
|
+
print "Git repo: "
|
|
227
|
+
if system('git rev-parse --git-dir > /dev/null 2>&1')
|
|
228
|
+
repo = GitHub.current_repo
|
|
229
|
+
puts pastel.green(repo || 'local repo')
|
|
230
|
+
else
|
|
231
|
+
puts pastel.yellow("not in a git repository")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Check for CLAUDE.md
|
|
235
|
+
print "CLAUDE.md: "
|
|
236
|
+
if File.exist?('CLAUDE.md')
|
|
237
|
+
puts pastel.green("present")
|
|
238
|
+
else
|
|
239
|
+
puts pastel.dim("not found (optional)")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Check for existing state
|
|
243
|
+
print "State dir: "
|
|
244
|
+
state = State.new
|
|
245
|
+
if state.exists?
|
|
246
|
+
current = state.load_state
|
|
247
|
+
puts pastel.cyan("exists (status: #{current[:status]})")
|
|
248
|
+
else
|
|
249
|
+
puts pastel.dim("none")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
puts
|
|
253
|
+
if all_good
|
|
254
|
+
puts pastel.green.bold("All prerequisites met!")
|
|
255
|
+
else
|
|
256
|
+
puts pastel.red.bold("Some prerequisites missing. Fix them before running.")
|
|
257
|
+
exit 1
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
desc 'clean', 'Remove state directory and start fresh'
|
|
262
|
+
option :force, type: :boolean, aliases: '-f', default: false, desc: 'Skip confirmation'
|
|
263
|
+
def clean
|
|
264
|
+
state = State.new
|
|
265
|
+
pastel = Pastel.new
|
|
266
|
+
|
|
267
|
+
unless state.exists?
|
|
268
|
+
puts pastel.yellow("No state directory to clean.")
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
unless options[:force]
|
|
273
|
+
puts pastel.yellow("This will delete .claude-task-master/ and all session data.")
|
|
274
|
+
print "Are you sure? (y/N) "
|
|
275
|
+
response = $stdin.gets.chomp.downcase
|
|
276
|
+
unless response == 'y' || response == 'yes'
|
|
277
|
+
puts "Aborted."
|
|
278
|
+
return
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
FileUtils.rm_rf(state.dir)
|
|
283
|
+
puts pastel.green("Cleaned up .claude-task-master/")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
desc 'context', 'Show or edit the context file'
|
|
287
|
+
def context
|
|
288
|
+
state = State.new
|
|
289
|
+
pastel = Pastel.new
|
|
290
|
+
|
|
291
|
+
unless state.exists?
|
|
292
|
+
puts pastel.yellow("No active task.")
|
|
293
|
+
return
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
context_path = File.join(state.dir, 'context.md')
|
|
297
|
+
if File.exist?(context_path)
|
|
298
|
+
puts File.read(context_path)
|
|
299
|
+
else
|
|
300
|
+
puts pastel.dim("No context file yet.")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
desc 'progress', 'Show the progress log'
|
|
305
|
+
def progress
|
|
306
|
+
state = State.new
|
|
307
|
+
pastel = Pastel.new
|
|
308
|
+
|
|
309
|
+
unless state.exists?
|
|
310
|
+
puts pastel.yellow("No active task.")
|
|
311
|
+
return
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
progress_path = File.join(state.dir, 'progress.md')
|
|
315
|
+
if File.exist?(progress_path)
|
|
316
|
+
puts File.read(progress_path)
|
|
317
|
+
else
|
|
318
|
+
puts pastel.dim("No progress log yet.")
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
desc 'comments [PR_NUMBER]', 'Show PR review comments'
|
|
323
|
+
option :actionable, type: :boolean, aliases: '-a', default: false, desc: 'Show only actionable comments'
|
|
324
|
+
option :unresolved, type: :boolean, aliases: '-u', default: false, desc: 'Show only unresolved threads'
|
|
325
|
+
def comments(pr_number = nil)
|
|
326
|
+
state = State.new
|
|
327
|
+
pastel = Pastel.new
|
|
328
|
+
|
|
329
|
+
# Get PR number from state if not provided
|
|
330
|
+
if pr_number.nil? && state.exists?
|
|
331
|
+
current = state.load_state
|
|
332
|
+
pr_number = current[:pr_number]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
if pr_number.nil?
|
|
336
|
+
puts pastel.yellow("No PR number provided and no active PR in state.")
|
|
337
|
+
puts "Usage: claude-task-master comments 123"
|
|
338
|
+
return
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
puts pastel.cyan.bold("PR ##{pr_number} Comments")
|
|
342
|
+
puts
|
|
343
|
+
|
|
344
|
+
if options[:unresolved]
|
|
345
|
+
threads = GitHub.unresolved_threads(pr_number.to_i)
|
|
346
|
+
if threads.empty?
|
|
347
|
+
puts pastel.green("No unresolved threads!")
|
|
348
|
+
else
|
|
349
|
+
puts pastel.yellow("#{threads.size} unresolved thread(s):")
|
|
350
|
+
threads.each do |thread|
|
|
351
|
+
puts
|
|
352
|
+
puts pastel.dim(" Author: #{thread[:author]}")
|
|
353
|
+
puts pastel.dim(" File: #{thread[:file_path]}:#{thread[:line]}")
|
|
354
|
+
puts " #{thread[:body]&.lines&.first&.strip}"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
else
|
|
358
|
+
all_comments = GitHub.pr_comments(pr_number.to_i)
|
|
359
|
+
comments = options[:actionable] ? all_comments.select(&:actionable?) : all_comments
|
|
360
|
+
|
|
361
|
+
if comments.empty?
|
|
362
|
+
puts pastel.green(options[:actionable] ? "No actionable comments!" : "No comments!")
|
|
363
|
+
else
|
|
364
|
+
comments.each do |comment|
|
|
365
|
+
puts severity_badge(comment.severity, pastel)
|
|
366
|
+
puts pastel.dim(" #{comment.file_path}:#{comment.line_range}")
|
|
367
|
+
puts pastel.dim(" Author: #{comment.author}")
|
|
368
|
+
puts " #{comment.summary || comment.body&.lines&.first&.strip}"
|
|
369
|
+
puts pastel.dim(" #{comment.html_url}")
|
|
370
|
+
puts
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
rescue StandardError => e
|
|
375
|
+
puts pastel.red("Error fetching comments: #{e.message}")
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
desc 'pr [SUBCOMMAND]', 'PR management commands'
|
|
379
|
+
option :number, type: :numeric, aliases: '-n', desc: 'PR number (uses current if not specified)'
|
|
380
|
+
def pr(subcommand = 'status')
|
|
381
|
+
state = State.new
|
|
382
|
+
pastel = Pastel.new
|
|
383
|
+
|
|
384
|
+
pr_number = options[:number]
|
|
385
|
+
if pr_number.nil? && state.exists?
|
|
386
|
+
current = state.load_state
|
|
387
|
+
pr_number = current[:pr_number]
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
if pr_number.nil?
|
|
391
|
+
puts pastel.yellow("No PR number provided and no active PR in state.")
|
|
392
|
+
return
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
case subcommand
|
|
396
|
+
when 'status', 'info'
|
|
397
|
+
show_pr_info(pr_number, pastel)
|
|
398
|
+
when 'checks', 'ci'
|
|
399
|
+
show_pr_checks(pr_number, pastel)
|
|
400
|
+
when 'merge'
|
|
401
|
+
merge_current_pr(pr_number, pastel)
|
|
402
|
+
else
|
|
403
|
+
puts pastel.yellow("Unknown subcommand: #{subcommand}")
|
|
404
|
+
puts "Available: status, checks, merge"
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Default command (no args = resume or show help)
|
|
409
|
+
default_task :default_action
|
|
410
|
+
|
|
411
|
+
desc 'default_action', 'Default action', hide: true
|
|
412
|
+
def default_action
|
|
413
|
+
state = State.new
|
|
414
|
+
if state.exists?
|
|
415
|
+
invoke :resume
|
|
416
|
+
else
|
|
417
|
+
invoke :help
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
private
|
|
422
|
+
|
|
423
|
+
def check_prerequisites!
|
|
424
|
+
pastel = Pastel.new
|
|
425
|
+
|
|
426
|
+
unless Claude.available?
|
|
427
|
+
puts pastel.red("Claude CLI not found. Install it first:")
|
|
428
|
+
puts " npm install -g @anthropic-ai/claude-code"
|
|
429
|
+
exit 1
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
unless GitHub.available?
|
|
433
|
+
puts pastel.yellow("Warning: gh CLI not authenticated. PR features won't work.")
|
|
434
|
+
puts " Run: gh auth login"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def prompt_for_criteria
|
|
439
|
+
pastel = Pastel.new
|
|
440
|
+
puts pastel.cyan("What are your success criteria?")
|
|
441
|
+
puts pastel.dim("(e.g., 'tests pass, deploys to staging, no sentry errors for 10min')")
|
|
442
|
+
print "> "
|
|
443
|
+
$stdin.gets.chomp
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def status_color(status, pastel)
|
|
447
|
+
case status
|
|
448
|
+
when 'success'
|
|
449
|
+
pastel.green(status)
|
|
450
|
+
when 'blocked'
|
|
451
|
+
pastel.red(status)
|
|
452
|
+
when 'ready', 'working'
|
|
453
|
+
pastel.cyan(status)
|
|
454
|
+
when 'planning'
|
|
455
|
+
pastel.yellow(status)
|
|
456
|
+
else
|
|
457
|
+
status
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def severity_badge(severity, pastel)
|
|
462
|
+
case severity
|
|
463
|
+
when 'critical'
|
|
464
|
+
pastel.red.bold("[CRITICAL]")
|
|
465
|
+
when 'warning'
|
|
466
|
+
pastel.yellow.bold("[WARNING]")
|
|
467
|
+
when 'major'
|
|
468
|
+
pastel.magenta.bold("[MAJOR]")
|
|
469
|
+
when 'trivial'
|
|
470
|
+
pastel.dim("[trivial]")
|
|
471
|
+
when 'nitpick'
|
|
472
|
+
pastel.dim("[nitpick]")
|
|
473
|
+
when 'refactor'
|
|
474
|
+
pastel.blue("[refactor]")
|
|
475
|
+
when 'suggestion'
|
|
476
|
+
pastel.cyan("[suggestion]")
|
|
477
|
+
else
|
|
478
|
+
pastel.dim("[info]")
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def show_pr_info(pr_number, pastel)
|
|
483
|
+
info = GitHub.pr_info(pr_number)
|
|
484
|
+
|
|
485
|
+
if info.nil?
|
|
486
|
+
puts pastel.red("Could not fetch PR ##{pr_number}")
|
|
487
|
+
return
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
puts pastel.cyan.bold("PR ##{info[:number]}: #{info[:title]}")
|
|
491
|
+
puts pastel.dim("State: ") + status_color(info[:state], pastel)
|
|
492
|
+
puts pastel.dim("Branch: #{info[:head_ref]} -> #{info[:base_ref]}")
|
|
493
|
+
puts pastel.dim("Mergeable: ") + (info[:mergeable] ? pastel.green("yes") : pastel.red("no"))
|
|
494
|
+
puts pastel.dim("URL: #{info[:url]}")
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def show_pr_checks(pr_number, pastel)
|
|
498
|
+
status = GitHub.pr_status(pr_number)
|
|
499
|
+
|
|
500
|
+
puts pastel.cyan.bold("CI Status: ") + ci_status_color(status[:status], pastel)
|
|
501
|
+
puts
|
|
502
|
+
|
|
503
|
+
status[:checks].each do |check|
|
|
504
|
+
icon = case check[:conclusion] || check[:bucket]
|
|
505
|
+
when 'success', 'pass' then pastel.green('✓')
|
|
506
|
+
when 'failure', 'fail' then pastel.red('✗')
|
|
507
|
+
when 'pending' then pastel.yellow('○')
|
|
508
|
+
else pastel.dim('?')
|
|
509
|
+
end
|
|
510
|
+
puts " #{icon} #{check[:name]}"
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def merge_current_pr(pr_number, pastel)
|
|
515
|
+
puts pastel.yellow("Merging PR ##{pr_number}...")
|
|
516
|
+
|
|
517
|
+
if GitHub.merge_pr(pr_number)
|
|
518
|
+
puts pastel.green("PR ##{pr_number} merged successfully!")
|
|
519
|
+
else
|
|
520
|
+
puts pastel.red("Failed to merge PR ##{pr_number}")
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def ci_status_color(status, pastel)
|
|
525
|
+
case status
|
|
526
|
+
when :passing
|
|
527
|
+
pastel.green('PASSING')
|
|
528
|
+
when :failing
|
|
529
|
+
pastel.red('FAILING')
|
|
530
|
+
when :pending
|
|
531
|
+
pastel.yellow('PENDING')
|
|
532
|
+
else
|
|
533
|
+
pastel.dim('UNKNOWN')
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|