wassup 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe6fcd8e497b99430a22d16872f83d9b8a08356436c95f222bb3c9bed1b86d47
4
- data.tar.gz: db98e8f4af5997b228bd8f148b13fc0b09681dcfa5486ad8d37e8c21aae0dc7b
3
+ metadata.gz: f810171df69f9c53ab10416de45b9c539881923be7aa7036c72974faea63518c
4
+ data.tar.gz: af206eb3965efeedf1ef5d388d787baa2e8ed5890ff3786d26b0199e87168656
5
5
  SHA512:
6
- metadata.gz: 7623256788359eac541c314581b9b552257f58af44a36216df2922c80a411a9a0dd078e3ca3291b0b0943706689f06c49ec27898879bca2a2131857e960c8b21
7
- data.tar.gz: 9b4a0cbb77b12eebb1e9a1392d4d0ac6fcc9796aac72c9632bab5d9217aba1d179847220348532106ae4175131207f3f1484041b7527db1cc5e7957f038d3b89
6
+ metadata.gz: 0ef15857c9aecbcb4cd7723e4695604415ce4d86ae47aa95ca701750e1fdc54745bf241d79e39935f38dd0292fb0f6a6eff7f62bef39537cd778ea0c61c79a6c
7
+ data.tar.gz: 627382fd74f6ff0645b508400e6375df42ab5de54b37381153fdc784687f224df23d23d5d1454427281fc143c68e5105dfba547aa3de685c3dfcdaa3e53e3aa8
data/Gemfile.lock CHANGED
@@ -1,15 +1,30 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wassup (0.1.0)
4
+ wassup (0.2.1)
5
5
  curses
6
+ rest-client
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
11
  curses (1.4.2)
11
12
  diff-lcs (1.4.4)
13
+ domain_name (0.5.20190701)
14
+ unf (>= 0.0.5, < 1.0.0)
15
+ http-accept (1.7.0)
16
+ http-cookie (1.0.4)
17
+ domain_name (~> 0.5)
18
+ mime-types (3.4.1)
19
+ mime-types-data (~> 3.2015)
20
+ mime-types-data (3.2021.1115)
21
+ netrc (0.11.0)
12
22
  rake (12.3.3)
23
+ rest-client (2.1.0)
24
+ http-accept (>= 1.7.0, < 2.0)
25
+ http-cookie (>= 1.0.2, < 2.0)
26
+ mime-types (>= 1.16, < 4.0)
27
+ netrc (~> 0.8)
13
28
  rspec (3.10.0)
14
29
  rspec-core (~> 3.10.0)
15
30
  rspec-expectations (~> 3.10.0)
@@ -23,6 +38,9 @@ GEM
23
38
  diff-lcs (>= 1.2.0, < 2.0)
24
39
  rspec-support (~> 3.10.0)
25
40
  rspec-support (3.10.3)
41
+ unf (0.1.4)
42
+ unf_ext
43
+ unf_ext (0.0.8)
26
44
 
27
45
  PLATFORMS
28
46
  arm64-darwin-21
data/bin/wassup CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  require 'wassup'
4
4
 
5
+ debug = ARGV.delete("--debug")
5
6
  path = ARGV[0] || 'Supfile'
6
7
 
7
8
  unless File.exists?(path)
8
9
  raise "Missing file: #{path}"
9
10
  end
10
11
 
11
- Wassup::App.start(path: path)
12
+ if debug
13
+ Wassup::App.debug(path: path)
14
+ else
15
+ Wassup::App.start(path: path)
16
+ end
@@ -1,8 +1,5 @@
1
- require 'json'
2
- require 'rest-client'
3
-
4
1
  add_pane do |pane|
5
- pane.height = 0.5
2
+ pane.height = 0.25
6
3
  pane.width = 0.5
7
4
  pane.top = 0
8
5
  pane.left = 0
@@ -11,18 +8,188 @@ add_pane do |pane|
11
8
  pane.title = "Open PRs - fastlane/fastlane"
12
9
 
13
10
  pane.interval = 60 * 5
14
- pane.content do
15
- resp = RestClient.get "https://api.github.com/repos/fastlane/fastlane/pulls"
16
- json = JSON.parse(resp)
17
- json.map do |pr|
18
- display = "##{pr["number"]} #{pr["title"]}"
19
-
20
- # First element is displayed
21
- # Second element is passed to pane.selection
22
- [display, pr["html_url"]]
23
- end
24
- end
25
- pane.selection do |url|
11
+ pane.show_refresh = true
12
+
13
+ pane.content do |content|
14
+ prs = Helpers::GitHub.pull_requests(org: 'fastlane', repo: 'fastlane')
15
+ prs.each do |pr|
16
+ display = Helpers::GitHub::Formatter.pr(pr)
17
+ content.add_row(display, pr)
18
+ end
19
+ end
20
+ pane.selection('enter', 'Open PR in browser') do |pr|
21
+ `open #{pr['html_url']}`
22
+ end
23
+ end
24
+
25
+ add_pane do |pane|
26
+ pane.height = 0.25
27
+ pane.width = 0.5
28
+ pane.top = 0.25
29
+ pane.left = 0
30
+
31
+ pane.highlight = true
32
+ pane.title = "Open PRs - fastlane-community"
33
+
34
+ pane.interval = 60 * 5
35
+ pane.show_refresh = true
36
+
37
+ pane.content do |content|
38
+ # Uses GitHub's /search/issues API
39
+ # Docs - https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests
40
+ issues = Helpers::GitHub.issues(org: 'fastlane-community', q: 'is:pr is:open')
41
+ issues.each do |issue|
42
+ display = Helpers::GitHub::Formatter.pr(issue, show_repo: true)
43
+ content.add_row(display, issue)
44
+ end
45
+ end
46
+ pane.selection('enter', 'Open PR in browser') do |pr|
47
+ `open #{pr['html_url']}`
48
+ end
49
+ end
50
+
51
+ add_pane do |pane|
52
+ pane.height = 0.25
53
+ pane.width = 0.5
54
+ pane.top = 0.5
55
+ pane.left = 0
56
+
57
+ pane.highlight = true
58
+ pane.title = "High Interaction Issues - fastlane/fastlane"
59
+
60
+ pane.interval = 60 * 5
61
+ pane.show_refresh = true
62
+
63
+ pane.content do |content|
64
+ # Uses GitHub's /search/issues API
65
+ # Doc - https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests
66
+ issues = Helpers::GitHub.issues(org: 'fastlane', repo: 'fastlane', q: 'is:issue is:open interactions:>10')
67
+ issues.each do |issue|
68
+ display = Helpers::GitHub::Formatter.issue(issue, show_interactions: true)
69
+ content.add_row(display, issue)
70
+ end
71
+ end
72
+ pane.selection('enter', 'Open issue in browser') do |pr|
73
+ `open #{pr['html_url']}`
74
+ end
75
+ end
76
+
77
+ add_pane do |pane|
78
+ pane.height = 0.25
79
+ pane.width = 0.5
80
+ pane.top = 0
81
+ pane.left = 0.5
82
+
83
+ pane.highlight = true
84
+ pane.title = "Releases - fastlane/fastlane"
85
+
86
+ pane.interval = 60 * 5
87
+ pane.show_refresh = true
88
+
89
+ pane.content do |content|
90
+ releases = Helpers::GitHub.releases(org: 'fastlane', repo: 'fastlane')
91
+ releases.each do |release|
92
+ display = Helpers::GitHub::Formatter.release(release)
93
+ content.add_row(display, release)
94
+ end
95
+ end
96
+ pane.selection('enter', 'Open release in browser') do |pr|
97
+ `open #{pr['html_url']}`
98
+ end
99
+ end
100
+
101
+ add_pane do |pane|
102
+ pane.height = 0.5
103
+ pane.width = 0.5
104
+ pane.top = 0.25
105
+ pane.left = 0.5
106
+
107
+ pane.highlight = true
108
+ pane.title = "CircleCI - fastlane/fastlane"
109
+
110
+ pane.interval = 60 * 5
111
+ pane.show_refresh = true
112
+
113
+ pane.content do |content|
114
+ workflows = Helpers::CircleCI.workflows(vcs: 'github', org: 'fastlane', repo: 'fastlane', limit_days: 14)
115
+ workflows.each do |workflow|
116
+ display = Helpers::CircleCI::Formatter.workflow(workflow)
117
+ content.add_row(display, workflow)
118
+ end
119
+ end
120
+ pane.selection('enter', 'Open workflow in browser') do |workflow|
121
+ slug = workflow["project_slug"]
122
+ pipeline_number = workflow["pipeline_number"]
123
+ workflow_id = workflow["id"]
124
+
125
+ url = "https://app.circleci.com/pipelines/#{slug}/#{pipeline_number}/workflows/#{workflow_id}"
126
+ `open #{url}`
127
+ end
128
+ end
129
+
130
+ add_pane do |pane|
131
+ pane.height = 0.25
132
+ pane.width = 0.5
133
+ pane.top = 0.75
134
+ pane.left = 0
135
+
136
+ pane.highlight = true
137
+ pane.title = "Netlify - wassup"
138
+
139
+ pane.interval = 60 * 5
140
+ pane.show_refresh = true
141
+
142
+ pane.content do |content|
143
+ deploys = Helpers::Netlify.deploys(site_id: '91e8af7d-ea1c-4553-afb0-af7539bed063')
144
+ deploys.each do |deploy|
145
+ display = Helpers::Netlify::Formatter.deploy(deploy)
146
+ content.add_row(display, deploy)
147
+ end
148
+ end
149
+ pane.selection('enter', 'Open in Netlify') do |deploy|
150
+ url = "#{deploy['admin_url']}/deploys/#{deploy['id']}"
151
+ `open #{url}`
152
+ end
153
+ pane.selection('o', 'Open preview') do |deploy|
154
+ if deploy['state'] == 'error'
155
+ # show alert that isn't here yet
156
+ elsif deploy['review_id'].nil?
157
+ `open #{deploy['url']}`
158
+ else
159
+ `open #{deploy['deploy_ssl_url']}`
160
+ end
161
+ end
162
+ end
163
+
164
+ add_pane do |pane|
165
+ pane.height = 0.25
166
+ pane.width = 0.5
167
+ pane.top = 0.75
168
+ pane.left = 0.5
169
+
170
+ pane.highlight = true
171
+ pane.title = "Shortcut - Stories"
172
+
173
+ pane.interval = 60 * 5
174
+ pane.show_refresh = true
175
+
176
+ pane.content do |content|
177
+ # Owned stories
178
+ stories = Helpers::Shortcut.search_stories(query: "owner:joshholtz")
179
+ stories.each do |story|
180
+ display = Helpers::Shortcut::Formatter.story(story)
181
+ content.add_row(display, story, page: "Owned Stories")
182
+ end
183
+
184
+ # Ready for review stories
185
+ stories = Helpers::Shortcut.search_stories(query: "state:\"Ready For Review\" team:\"The\"")
186
+ stories.each do |story|
187
+ display = Helpers::Shortcut::Formatter.story(story)
188
+ content.add_row(display, story, page: "Ready For Review")
189
+ end
190
+ end
191
+ pane.selection('enter', 'Open in Shortcut') do |story|
192
+ url = story['app_url']
26
193
  `open #{url}`
27
194
  end
28
195
  end
@@ -53,6 +53,21 @@ add_pane do |pane|
53
53
  end
54
54
  end
55
55
 
56
+ add_pane do |pane|
57
+ pane.height = 0.25
58
+ pane.width = 0.3
59
+ pane.top = 0.75
60
+ pane.left = 0.0
61
+
62
+ pane.highlight = false
63
+ pane.title = "Always error"
64
+
65
+ pane.interval = 10
66
+ pane.content do |content|
67
+ raise "An error occured! Oh no!"
68
+ end
69
+ end
70
+
56
71
  add_pane do |pane|
57
72
  pane.height = 0.25
58
73
  pane.width = 0.35
@@ -20,6 +20,9 @@ add_pane do |pane|
20
20
  pane.highlight = false
21
21
 
22
22
  pane.title = "Stats: fastlane-community"
23
+ pane.description = [
24
+ "Highlevel stats from fastlane-community about PR count"
25
+ ]
23
26
 
24
27
  pane.interval = 2
25
28
  pane.show_refresh = false
@@ -62,6 +65,9 @@ add_pane do |pane|
62
65
  pane.highlight = false
63
66
 
64
67
  pane.title = "Stats: fastlane/fastlane"
68
+ pane.description = [
69
+ "Highlevel stats from fastlane/fastlane about PR count"
70
+ ]
65
71
 
66
72
  pane.interval = 2
67
73
  pane.show_refresh = false
@@ -113,52 +119,16 @@ add_pane do |pane|
113
119
 
114
120
  pane.interval = 60 * 5
115
121
  pane.show_refresh = true
122
+
116
123
  pane.content do |builder|
117
- resp = RestClient::Request.execute(
118
- method: :get,
119
- url: "https://circleci.com/api/v2/project/github/fastlane/fastlane/pipeline",
120
- headers: { "Circle-Token": ENV["WASSUP_CIRCLE_CI_API_TOKEN"] }
121
- )
122
- json = JSON.parse(resp)
123
- json["items"].select do |item|
124
- date = Time.parse(item["updated_at"])
125
- days = (Time.now - date).to_i / (24 * 60 * 60)
126
- days < 14
127
- end.map do |item|
128
- id = item["id"]
129
- number = item["number"]
130
- message = (item["vcs"]["commit"] || {})["subject"]
131
- login = item["trigger"]["actor"]["login"]
132
-
133
- resp = RestClient::Request.execute(
134
- method: :get,
135
- url: "https://circleci.com/api/v2/pipeline/#{id}/workflow",
136
- headers: { "Circle-Token": ENV["WASSUP_CIRCLE_CI_API_TOKEN"] }
137
- )
138
- json = JSON.parse(resp)
139
- workflow = json["items"].first
140
-
141
- next if workflow.nil?
142
-
143
- status = workflow["status"]
144
-
145
- if status == "failed"
146
- status = "[fg=red]#{status}[fg=white]"
147
- elsif status == "success"
148
- status = "[fg=green]#{status}[fg=white]"
149
- else
150
- status = "[fg=yellow]#{status}[fg=white]"
151
- end
152
-
153
- display = "#{number} (#{status}) by #{login} - #{message}"
154
- object = [item, workflow]
155
-
156
- builder.add_row(display, object)
124
+ workflows = Helpers::CircleCI.workflows(vcs: 'github', org: 'fastlane', repo: 'fastlane', limit_days: 14)
125
+ workflows.each do |workflow|
126
+ display = Helpers::CircleCI::Formatter.workflow(workflow)
127
+ builder.add_row(display, workflow)
157
128
  end
158
129
  end
159
- pane.selection('enter', 'Opens up CirceCI workflow') do |data|
160
- workflow = data[1]
161
130
 
131
+ pane.selection('enter', 'Opens up CirceCI workflow') do |workflow|
162
132
  slug = workflow["project_slug"]
163
133
  pipeline_number = workflow["pipeline_number"]
164
134
  workflow_id = workflow["id"]
@@ -166,8 +136,9 @@ add_pane do |pane|
166
136
  url = "https://app.circleci.com/pipelines/#{slug}/#{pipeline_number}/workflows/#{workflow_id}"
167
137
  `open #{url}`
168
138
  end
169
- pane.selection('o', 'Opens up version control review URL') do |data|
170
- pipeline = data[0]
139
+
140
+ pane.selection('o', 'Opens up version control review URL') do |workflow|
141
+ pipeline = workflow["pipeline"]
171
142
  url = pipeline["vcs"]["review_url"]
172
143
  `open #{url}`
173
144
  end
@@ -185,49 +156,26 @@ add_pane do |pane|
185
156
  pane.highlight = true
186
157
 
187
158
  pane.title = "Open PRs - fastlane-community"
159
+ pane.description = [
160
+ "Open PRs from all the fastlane-community repos"
161
+ ]
188
162
 
189
163
  pane.interval = 60 * 5
190
164
  pane.show_refresh = true
191
165
  pane.content do |builder|
192
166
  fastlane_community_prs = []
193
167
 
194
- resp = RestClient::Request.execute(
195
- method: :get,
196
- url: "https://api.github.com/orgs/fastlane-community/repos",
197
- user: ENV["WASSUP_GITHUB_USERNAME"],
198
- password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
199
- )
200
- json = JSON.parse(resp)
201
- json.map do |repo|
202
- name = repo["name"]
203
- full_name = repo["full_name"]
204
-
205
- resp = RestClient::Request.execute(
206
- method: :get,
207
- url: "https://api.github.com/repos/#{full_name}/pulls",
208
- user: ENV["WASSUP_GITHUB_USERNAME"],
209
- password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
210
- )
211
- json = JSON.parse(resp)
212
- prs = json.map do |pr|
213
- fastlane_community_prs << pr
214
-
215
- number = pr["number"]
216
- title = pr["title"]
217
- created_at = pr["created_at"]
218
-
219
- number_formatted = '%5.5s' % "##{number}"
220
-
221
- date = Time.parse(created_at)
222
- days = (Time.now - date).to_i / (24 * 60 * 60)
223
- days_formatted = '%3.3s' % days.to_s
224
-
225
- display = "[fg=yellow]#{number_formatted}[fg=cyan] #{days_formatted}d ago[fg=white] #{title}"
226
- builder.add_row(display, pr, page: name)
227
- end
168
+ prs = Wassup::Helpers::GitHub.pull_requests(org: 'fastlane-community')
169
+ prs.each do |pr|
170
+ fastlane_community_prs << pr
171
+
172
+ repo_name = pr["base"]["repo"]["name"]
173
+
174
+ display = Helpers::GitHub::Formatter.pr(pr)
175
+ builder.add_row(display, pr, page: repo_name)
228
176
  end
229
177
  end
230
- pane.selection do |data|
178
+ pane.selection('enter', 'Open PR in web browser') do |data|
231
179
  url = data["html_url"]
232
180
  `open #{url}`
233
181
  end
@@ -246,35 +194,24 @@ add_pane do |pane|
246
194
  pane.highlight = true
247
195
 
248
196
  pane.title = "Open PRs - fastlane/fastlane"
197
+ pane.description = [
198
+ "Open PRs from all the fastlane/fastlane repo"
199
+ ]
249
200
 
250
201
  pane.interval = 60 * 5
251
202
  pane.show_refresh = true
252
203
  pane.content do |builder|
253
204
  fastlane_prs = []
254
205
 
255
- resp = RestClient::Request.execute(
256
- method: :get,
257
- url: "https://api.github.com/repos/fastlane/fastlane/pulls?per_page=100",
258
- user: ENV["WASSUP_GITHUB_USERNAME"],
259
- password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
260
- )
261
- json = JSON.parse(resp)
262
- json.map do |pr|
206
+ prs = Wassup::Helpers::GitHub.pull_requests(org: 'fastlane', repo: 'fastlane')
207
+ prs.each do |pr|
263
208
  fastlane_prs << pr
264
209
 
265
- number = pr["number"]
266
- title = pr["title"]
267
- created_at = pr["created_at"]
268
-
269
- date = Time.parse(created_at)
270
- days = (Time.now - date).to_i / (24 * 60 * 60)
271
- days_formatted = '%3.3s' % days.to_s
272
-
273
- display = "[fg=yellow]##{number}[fg=cyan] #{days_formatted}d ago[fg=white] #{title}"
210
+ display = Helpers::GitHub::Formatter.pr(pr)
274
211
  builder.add_row(display, pr)
275
212
  end
276
213
  end
277
- pane.selection do |data|
214
+ pane.selection('enter', 'Open PR in browser') do |data|
278
215
  url = data["html_url"]
279
216
  `open #{url}`
280
217
  end
@@ -0,0 +1,17 @@
1
+ add_pane do |pane|
2
+ pane.height = 0.25
3
+ pane.width = 0.25
4
+ pane.top = 0
5
+ pane.left = 0
6
+
7
+ pane.highlight = false
8
+ pane.title = "The Title"
9
+
10
+ pane.interval = 1
11
+ pane.content do |content|
12
+ date = `date`
13
+ content.add_row(date)
14
+ content.add_row(date, page: "page 2")
15
+ end
16
+ end
17
+
data/lib/wassup/app.rb CHANGED
@@ -20,6 +20,44 @@ module Wassup
20
20
  app = App.new(path: path)
21
21
  end
22
22
 
23
+ def self.debug(path:)
24
+ app = App.new(path: path, debug: true)
25
+
26
+ app.panes.each do |k, pane|
27
+ puts "#{k} - #{pane.title}"
28
+ end
29
+
30
+ puts ""
31
+ puts "Choose a pane to run:"
32
+
33
+ selection = $stdin.gets.chomp.to_s
34
+
35
+ pane = app.panes[selection]
36
+ if pane.nil?
37
+ puts "That was not a valid option"
38
+ else
39
+ puts "Going to run: \"#{pane.title}\""
40
+
41
+ builder = Wassup::PaneBuilder::ContentBuilder.new(pane.contents)
42
+ pane.content_block.call(builder)
43
+
44
+ builder.contents.each_with_index do |content|
45
+ puts "#########################"
46
+ puts "# #{content.title || (idx == 0 ? "Default" : "<No Title>")}"
47
+ puts "#########################"
48
+
49
+ content.data.each do |data|
50
+ puts data.display
51
+ .split(/\[.*?\]/).join('') # Removes colors but make this an option probably
52
+ end
53
+
54
+ puts ""
55
+ puts ""
56
+ puts ""
57
+ end
58
+ end
59
+ end
60
+
23
61
  def add_pane
24
62
  pane_builder = Wassup::PaneBuilder.new
25
63
  yield(pane_builder)
@@ -38,22 +76,46 @@ module Wassup
38
76
  show_refresh: pane_builder.show_refresh,
39
77
  content_block: pane_builder.content_block,
40
78
  selection_blocks: pane_builder.selection_blocks,
41
- selection_blocks_description: pane_builder.selection_blocks_description
79
+ selection_blocks_description: pane_builder.selection_blocks_description,
80
+ debug: debug
42
81
  )
43
82
  pane.focus_handler = @focus_handler
44
83
  @panes[number.to_s] = pane
45
84
  end
46
85
 
47
- def initialize(path:)
86
+ attr_accessor :panes
87
+ attr_accessor :debug
88
+
89
+ def initialize(path:, debug: false)
48
90
  @hidden_pane = nil
49
91
  @help_pane = nil
50
92
  @focused_pane = nil
51
93
  @panes = {}
94
+ @debug = debug
95
+
96
+ if debug
97
+ self.start_debug(path)
98
+ else
99
+ self.start_curses(path)
100
+ end
101
+ end
52
102
 
103
+ def start_debug(path)
104
+ begin
105
+ eval(File.new(path).read)
106
+ rescue => err
107
+ puts err
108
+ puts err.backtrace
109
+ end
110
+ end
111
+
112
+ def start_curses(path)
53
113
  @redraw_panes = false
54
114
 
55
115
  # TODO: this could maybe get replaced with selection_blocks now
56
116
  @focus_handler = Proc.new do |input|
117
+ is_help_open = !@help_pane.nil?
118
+
57
119
  if input == "q"
58
120
  exit
59
121
  elsif input == "?"
@@ -61,6 +123,8 @@ module Wassup
61
123
  next true
62
124
  end
63
125
 
126
+ next true if is_help_open
127
+
64
128
  if (pane = @panes[input.to_s])
65
129
  @focused_pane.focused = false
66
130
 
@@ -113,6 +177,21 @@ module Wassup
113
177
  end
114
178
  end
115
179
 
180
+ def row_help
181
+ {
182
+ "j" => "moves row highlight down",
183
+ "k" => "moves row highlight up",
184
+ "enter" => "perform selection on highlighted row"
185
+ }
186
+ end
187
+
188
+ def page_help
189
+ {
190
+ "h" => "previous page in pane",
191
+ "l" => "next page in pane"
192
+ }
193
+ end
194
+
116
195
  def toggle_help
117
196
  if @help_pane.nil?
118
197
  if @focused_pane == @hidden_pane
@@ -121,12 +200,13 @@ module Wassup
121
200
  "Welcome to Wassup!",
122
201
  "",
123
202
  "Press any number key to focus a pane",
124
- "j - moves row highlight down",
125
- "k - moves row highlight up",
126
- "<Enter> - perform selection on highlighted row",
203
+ "",
204
+ row_help.map { |k,v| "#{k} - #{v}"},
205
+ "",
206
+ page_help.map { |k,v| "#{k} - #{v}"},
127
207
  "",
128
208
  "? - opens help for focused pane"
129
- ]
209
+ ].flatten
130
210
 
131
211
  items.each do |item|
132
212
  content.add_row(item)
@@ -134,12 +214,27 @@ module Wassup
134
214
  end
135
215
  else
136
216
  content_block = Proc.new do |content|
217
+ hash = {}
218
+
219
+ hash = hash.merge(row_help)
220
+ hash = hash.merge(@focused_pane.selection_blocks_description)
221
+
222
+ row_help.map { |k,v| "#{k} - #{v}"}
223
+
224
+ copy_error = @focused_pane.caught_error.nil? ? [] : [
225
+ "c - copy stacktrace to clipboard",
226
+ ""
227
+ ]
228
+
137
229
  items = [
138
230
  @focused_pane.description,
139
231
  "",
140
- @focused_pane.selection_blocks_description.map do |k,v|
232
+ hash.map do |k,v|
141
233
  "#{k} - #{v}"
142
- end
234
+ end,
235
+ "",
236
+ copy_error,
237
+ page_help.map { |k,v| "#{k} - #{v}"},
143
238
  ].flatten.compact
144
239
 
145
240
  items.each do |item|
data/lib/wassup/color.rb CHANGED
@@ -17,6 +17,8 @@ module Wassup
17
17
  NORMAL = 20
18
18
  HIGHLIGHT = 21
19
19
 
20
+ GRAY = 22
21
+
20
22
  BORDER = 20
21
23
  BORDER_FOCUS = 7
22
24
 
@@ -44,6 +46,7 @@ module Wassup
44
46
  Curses.init_pair(Pair::RED, Curses::COLOR_RED, 0)
45
47
  Curses.init_pair(Pair::WHITE, Pair::WHITE, 0)
46
48
  Curses.init_pair(Pair::YELLOW, Curses::COLOR_YELLOW, 0)
49
+ Curses.init_pair(Pair::GRAY, Curses::COLOR_WHITE, 0)
47
50
  end
48
51
 
49
52
  def initialize(string_name)
@@ -64,6 +67,8 @@ module Wassup
64
67
  Pair::WHITE
65
68
  when "yellow"
66
69
  Pair::YELLOW
70
+ when "gray"
71
+ Pair::GRAY
67
72
  else
68
73
  if string_name.to_i.to_s == string_name
69
74
  string_name.to_i
@@ -0,0 +1,75 @@
1
+ module Wassup
2
+ module Helpers
3
+ module CircleCI
4
+ def self.workflows(vcs:, org:, repo:, limit_days: nil)
5
+ require 'json'
6
+ require 'rest-client'
7
+
8
+ resp = RestClient::Request.execute(
9
+ method: :get,
10
+ url: "https://circleci.com/api/v2/project/#{vcs}/#{org}/#{repo}/pipeline",
11
+ headers: { "Circle-Token": ENV["WASSUP_CIRCLE_CI_API_TOKEN"] }
12
+ )
13
+ json = JSON.parse(resp)
14
+
15
+ return json["items"].select do |item|
16
+ if !limit_days.nil?
17
+ date = Time.parse(item["updated_at"])
18
+ days = (Time.now - date).to_i / (24 * 60 * 60)
19
+ days < limit_days
20
+ else
21
+ true
22
+ end
23
+ end.map do |pipeline|
24
+ id = pipeline["id"]
25
+
26
+ resp = RestClient::Request.execute(
27
+ method: :get,
28
+ url: "https://circleci.com/api/v2/pipeline/#{id}/workflow",
29
+ headers: { "Circle-Token": ENV["WASSUP_CIRCLE_CI_API_TOKEN"] }
30
+ )
31
+ json = JSON.parse(resp)
32
+ workflow = json["items"].first
33
+
34
+ if workflow
35
+ workflow["pipeline"] = pipeline
36
+ end
37
+
38
+ workflow
39
+ end.compact
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module Wassup
46
+ module Helpers
47
+ module CircleCI
48
+ module Formatter
49
+ def self.workflow(workflow)
50
+ pipeline = workflow["pipeline"]
51
+ number = pipeline["number"]
52
+ message = (pipeline["vcs"]["commit"] || {})["subject"]
53
+ login = pipeline["trigger"]["actor"]["login"]
54
+
55
+ status = workflow["status"]
56
+ status_formatted = '%-8.8s' % status
57
+
58
+ number_formatted = '%-7.7s' % "##{number}"
59
+
60
+ if status == "failed"
61
+ status_formatted = "[fg=red]#{status_formatted}[fg=white]"
62
+ elsif status == "success"
63
+ status_formatted = "[fg=green]#{status_formatted}[fg=white]"
64
+ else
65
+ status_formatted = "[fg=yellow]#{status_formatted}[fg=white]"
66
+ end
67
+
68
+ display = "[fg=yellow]#{number_formatted} [fg=while]#{status_formatted} [fg=white]#{login} [fg=gray]#{message}"
69
+
70
+ return display
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,137 @@
1
+ module Wassup
2
+ module Helpers
3
+ module GitHub
4
+ require 'json'
5
+ require 'rest-client'
6
+
7
+ # https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests
8
+ def self.issues(org:, repo:nil, q: nil)
9
+ q_parts = []
10
+
11
+ if repo.nil?
12
+ q_parts << "org:#{org}"
13
+ else
14
+ q_parts << "repo:#{org}/#{repo}"
15
+ end
16
+
17
+ if q
18
+ q_parts << q
19
+ end
20
+
21
+ q = q_parts.join(' ')
22
+
23
+ items = []
24
+
25
+ resp = RestClient::Request.execute(
26
+ method: :get,
27
+ url: "https://api.github.com/search/issues?q=#{q}",
28
+ user: ENV["WASSUP_GITHUB_USERNAME"],
29
+ password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"],
30
+ headers: { "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json" },
31
+ )
32
+ partial_items = JSON.parse(resp)["items"]
33
+ items += partial_items
34
+
35
+ return items
36
+ end
37
+
38
+ def self.repos(org:)
39
+ resp = RestClient::Request.execute(
40
+ method: :get,
41
+ url: "https://api.github.com/orgs/#{org}/repos",
42
+ user: ENV["WASSUP_GITHUB_USERNAME"],
43
+ password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
44
+ )
45
+ return JSON.parse(resp)
46
+ end
47
+
48
+ Repo = Struct.new(:org, :repo)
49
+ def self.pull_requests(org:, repo: nil)
50
+ repos = []
51
+ if repo.nil?
52
+ repos += self.repos(org: org).map do |repo|
53
+ Repo.new(org, repo["name"])
54
+ end
55
+ else
56
+ repos << Repo.new(org, repo)
57
+ end
58
+
59
+ return repos.map do |repo|
60
+ resp = RestClient::Request.execute(
61
+ method: :get,
62
+ url: "https://api.github.com/repos/#{repo.org}/#{repo.repo}/pulls?per_page=100",
63
+ user: ENV["WASSUP_GITHUB_USERNAME"],
64
+ password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
65
+ )
66
+
67
+ JSON.parse(resp)
68
+ end.flatten(1)
69
+ end
70
+
71
+ def self.releases(org:, repo:)
72
+ resp = RestClient::Request.execute(
73
+ method: :get,
74
+ url: "https://api.github.com/repos/#{org}/#{repo}/releases",
75
+ user: ENV["WASSUP_GITHUB_USERNAME"],
76
+ password: ENV["WASSUP_GITHUB_ACCESS_TOKEN"]
77
+ )
78
+
79
+ return JSON.parse(resp)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ module Wassup
86
+ module Helpers
87
+ module GitHub
88
+ module Formatter
89
+ def self.issue(issue, show_repo: false, show_interactions: false)
90
+ self.pr(issue, show_repo: show_repo, show_interactions: show_interactions)
91
+ end
92
+
93
+ def self.pr(pr, show_repo: false, show_interactions: false)
94
+ number = pr["number"]
95
+ title = pr["title"]
96
+ created_at = pr["created_at"]
97
+
98
+ repo_name = ""
99
+ if show_repo
100
+ repo_url_parts = pr["repository_url"].split("/")
101
+ repo_name = "[fg=gray]#{repo_url_parts.last} "
102
+ end
103
+
104
+ interactions = ""
105
+ if show_interactions
106
+ interaction_count = pr["comments"] + pr["reactions"]["total_count"]
107
+ interactions = "[fg=red]#{interaction_count} "
108
+ end
109
+
110
+ number_formatted = '%-7.7s' % "##{number}"
111
+
112
+ date = Time.parse(created_at)
113
+ days = (Time.now - date).to_i / (24 * 60 * 60)
114
+ days_formatted = '%3.3s' % days.to_s
115
+
116
+ display = "[fg=yellow]#{number_formatted}[fg=cyan] #{days_formatted}d ago #{interactions}#{repo_name}[fg=white]#{title}"
117
+
118
+ return display
119
+ end
120
+
121
+ def self.release(release)
122
+ tag_name = release["tag_name"]
123
+ name = release["name"]
124
+ published_at = release["published_at"]
125
+
126
+ date = Time.parse(published_at)
127
+ days = (Time.now - date).to_i / (24 * 60 * 60)
128
+ days_formatted = '%3.3s' % days.to_s
129
+
130
+ display = "[fg=yellow]#{tag_name} [fg=cyan]#{days_formatted} ago [fg=gray]#{name}"
131
+
132
+ return display
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,70 @@
1
+ module Wassup
2
+ module Helpers
3
+ module Netlify
4
+ require 'json'
5
+ require 'rest-client'
6
+
7
+ def self.deploys(site_id:)
8
+ resp = RestClient::Request.execute(
9
+ method: :get,
10
+ url: "https://api.netlify.com/api/v1/sites/#{site_id}/deploys",
11
+ headers: { "Authorization": "Bearer #{ENV['WASSUP_NETLIFY_TOKEN']}", "User-Agent": "Wassup" }
12
+ )
13
+ return JSON.parse(resp)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+
20
+ module Wassup
21
+ module Helpers
22
+ module Netlify
23
+ module Formatter
24
+ def self.deploy(deploy)
25
+ review_id = deploy["review_id"]
26
+ context = deploy["context"]
27
+ state = deploy["state"]
28
+ error_message = deploy["error_message"]
29
+ branch = deploy["branch"]
30
+ commit_ref = deploy["commit_ref"] || "HEAD"
31
+ url = deploy["deploy_url"]
32
+
33
+ if error_message.to_s.downcase.include?("canceled")
34
+ state = "cancelled"
35
+ end
36
+
37
+ color = "green"
38
+ if state == "building"
39
+ color = "yellow"
40
+ elsif state == "enqueued"
41
+ color = "magenta"
42
+ elsif state == "cancelled"
43
+ color = "gray"
44
+ elsif state == "error"
45
+ color = "red"
46
+ end
47
+
48
+ display_context = context.split('-').map(&:capitalize).join(' ')
49
+ display_context = "[fg=#{color}]#{display_context}"
50
+
51
+ if !review_id.nil?
52
+ display_context += " - ##{review_id}"
53
+ end
54
+
55
+ display_context += "[fg=gray]"
56
+ display_context += " (#{state})"
57
+ display_context += "[fg=white]"
58
+
59
+ if !branch.nil? && !commit_ref.nil?
60
+ display_context += "[fg=cyan]: #{branch}@#{commit_ref[0...7]}"
61
+ end
62
+
63
+ display = "#{display_context}"
64
+
65
+ return display
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,114 @@
1
+ module Wassup
2
+ module Helpers
3
+ module Shortcut
4
+ require 'json'
5
+ require 'rest-client'
6
+
7
+ def self.members
8
+ endpoint = "https://api.app.shortcut.com"
9
+ resp = RestClient::Request.execute(
10
+ method: :get,
11
+ url: "#{endpoint}/api/v3/members",
12
+ headers: { "Shortcut-Token": ENV['WASSUP_SHORTCUT_TOKEN'], "Content-Type": "application/json" }
13
+ )
14
+ return JSON.parse(resp)
15
+ end
16
+
17
+ def self.workflows
18
+ endpoint = "https://api.app.shortcut.com"
19
+ resp = RestClient::Request.execute(
20
+ method: :get,
21
+ url: "#{endpoint}/api/v3/workflows",
22
+ headers: { "Shortcut-Token": ENV['WASSUP_SHORTCUT_TOKEN'], "Content-Type": "application/json" }
23
+ )
24
+ return JSON.parse(resp)
25
+ end
26
+
27
+ def self.search_stories(query:, page_size: 25)
28
+ endpoint = "https://api.app.shortcut.com"
29
+
30
+ members = self.members
31
+ workflows = self.workflows
32
+
33
+ stories = []
34
+
35
+ resp = RestClient::Request.execute(
36
+ method: :get,
37
+ url: "#{endpoint}/api/v3/search/stories",
38
+ payload: { page_size: page_size, query: query },
39
+ headers: { "Shortcut-Token": ENV['WASSUP_SHORTCUT_TOKEN'], "Content-Type": "application/json" }
40
+ )
41
+ json = JSON.parse(resp)
42
+ stories += json["data"]
43
+
44
+ next_url = json["next"]
45
+ while !next_url.nil?
46
+ resp = RestClient::Request.execute(
47
+ method: :get,
48
+ url: "#{endpoint}#{next_url}",
49
+ headers: { "Shortcut-Token": ENV['WASSUP_SHORTCUT_TOKEN'], "Content-Type": "application/json" }
50
+ )
51
+ json = JSON.parse(resp)
52
+ stories += json["data"]
53
+ next_url = json["next"]
54
+ end
55
+
56
+ stories = stories.map do |story|
57
+ story["followers"] = story["follower_ids"].map do |owner_id|
58
+ members.find do |member|
59
+ member["id"] == owner_id
60
+ end
61
+ end
62
+
63
+ story["owners"] = story["owner_ids"].map do |owner_id|
64
+ members.find do |member|
65
+ member["id"] == owner_id
66
+ end
67
+ end
68
+
69
+ if (workflow_id = story["workflow_id"]) && (workflow_state_id = story["workflow_state_id"])
70
+ workflow = workflows.find do |workflow|
71
+ workflow["id"] == workflow_id
72
+ end
73
+
74
+ story["workflow"] = workflow
75
+ if workflow
76
+ story["workflow_state"] = workflow["states"].find do |workflow_state|
77
+ workflow_state["id"] == workflow_state_id
78
+ end
79
+ end
80
+ end
81
+
82
+ story
83
+ end
84
+
85
+ return stories
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+
92
+ module Wassup
93
+ module Helpers
94
+ module Shortcut
95
+ module Formatter
96
+ def self.story(story)
97
+ id = story["id"]
98
+ name = story["name"]
99
+ state = story["workflow_state"]["name"]
100
+
101
+ mention_name = (story["owners"] || {}).map do |member|
102
+ member["profile"]["mention_name"]
103
+ end.join(", ")
104
+
105
+ id_formatted = '%-7.7s' % "##{id}"
106
+
107
+ display = "[fg=yellow]#{id_formatted} [fg=cyan]#{state} [fg=white]#{mention_name} [fg=gray]#{name}"
108
+
109
+ return display
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
data/lib/wassup/pane.rb CHANGED
@@ -31,6 +31,8 @@ module Wassup
31
31
  attr_accessor :selection_blocks
32
32
  attr_accessor :selection_blocks_description
33
33
 
34
+ attr_accessor :caught_error
35
+
34
36
  attr_accessor :content_thread
35
37
  attr_accessor :show_refresh
36
38
 
@@ -62,14 +64,17 @@ module Wassup
62
64
  end
63
65
  end
64
66
 
65
- def initialize(height, width, top, left, title: nil, description: nil, highlight: true, focus_number: nil, interval:, show_refresh:, content_block:, selection_blocks:, selection_blocks_description:)
66
- self.win_height = Curses.lines * height
67
- self.win_width = Curses.cols * width
68
- self.win_top = Curses.lines * top
69
- self.win_left = Curses.cols * left
67
+ def initialize(height, width, top, left, title: nil, description: nil, highlight: true, focus_number: nil, interval:, show_refresh:, content_block:, selection_blocks:, selection_blocks_description:, debug: false)
70
68
 
71
- self.win = Curses::Window.new(self.win_height, self.win_width, self.win_top, self.win_left)
72
- self.setup_subwin()
69
+ if !debug
70
+ self.win_height = Curses.lines * height
71
+ self.win_width = Curses.cols * width
72
+ self.win_top = Curses.lines * top
73
+ self.win_left = Curses.cols * left
74
+
75
+ self.win = Curses::Window.new(self.win_height, self.win_width, self.win_top, self.win_left)
76
+ self.setup_subwin()
77
+ end
73
78
 
74
79
  self.focused = false
75
80
  self.focus_number = focus_number
@@ -84,8 +89,10 @@ module Wassup
84
89
 
85
90
  self.selected_view_index = 0
86
91
 
87
- self.win.refresh
88
- self.subwin.refresh
92
+ if !debug
93
+ self.win.refresh
94
+ self.subwin.refresh
95
+ end
89
96
 
90
97
  self.title = title
91
98
  self.description = description
@@ -176,7 +183,7 @@ module Wassup
176
183
  def redraw
177
184
  self.update_box
178
185
  self.update_title
179
- self.refresh_content(nil)
186
+ self.load_current_view()
180
187
  end
181
188
 
182
189
  def refresh(force: false)
@@ -198,15 +205,27 @@ module Wassup
198
205
  elsif thread.status == false
199
206
  rtn = thread.value
200
207
  if rtn.is_a?(Ope)
201
- content = Wassup::Pane::Content.new
202
-
208
+ self.caught_error = rtn.error
209
+ content = Wassup::Pane::Content.new("Overview")
203
210
  content.add_row("[fg=red]Error during refersh[fg=white]")
204
211
  content.add_row("[fg=red]at #{Time.now}[fg=while]")
205
212
  content.add_row("")
206
213
  content.add_row("[fg=yellow]Will try again next interval[fg=white]")
207
214
 
208
- self.refresh_content([content])
215
+ content_directions = Wassup::Pane::Content.new("Directions")
216
+ content_directions.add_row("1. Press 'c' to copy the stacktrace")
217
+ content_directions.add_row("2. Debug pane content block with:")
218
+ content_directions.add_row(" $: wassup --debug")
219
+ content_directions.add_row("3. Stacktrace viewable in next page")
220
+
221
+ content_stacktrace = Wassup::Pane::Content.new("Stacktrace")
222
+ rtn.error.backtrace.each do |line|
223
+ content_stacktrace.add_row(line)
224
+ end
225
+
226
+ self.refresh_content([content, content_directions, content_stacktrace])
209
227
  elsif rtn.is_a?(Wassup::PaneBuilder::ContentBuilder)
228
+ self.caught_error = nil
210
229
  self.refresh_content(rtn.contents)
211
230
  end
212
231
 
@@ -235,7 +254,7 @@ module Wassup
235
254
  end
236
255
 
237
256
  def refresh_content(contents)
238
- self.contents = contents unless contents.nil?
257
+ self.contents = contents
239
258
 
240
259
  self.load_current_view()
241
260
  self.last_refreshed = Time.now
@@ -257,6 +276,13 @@ module Wassup
257
276
  self.update_title()
258
277
  end
259
278
 
279
+ def highlight
280
+ if self.caught_error
281
+ return true
282
+ end
283
+ return @highlight
284
+ end
285
+
260
286
  attr_accessor :refresh_char_count
261
287
  def refresh_char
262
288
  return "" unless self.show_refresh
@@ -535,6 +561,15 @@ module Wassup
535
561
  self.scroll_right
536
562
  elsif input == "r"
537
563
  self.refresh(force: true)
564
+ elsif input == "c"
565
+ if !self.caught_error.nil?
566
+ text = self.caught_error.backtrace.join("\n")
567
+ if RUBY_PLATFORM.downcase =~ /win32/
568
+ IO.popen('clip', 'w') { |pipe| pipe.puts text }
569
+ else
570
+ IO.popen('pbcopy', 'w') { |pipe| pipe.puts text }
571
+ end
572
+ end
538
573
  else
539
574
  selection_block = self.selection_blocks[input]
540
575
  if !selection_block.nil? && !self.highlighted_line.nil?
@@ -85,11 +85,11 @@ module Wassup
85
85
 
86
86
  description_input = input
87
87
  if input.to_s == "10"
88
- input = "<Enter>"
88
+ description_input = "enter"
89
89
  end
90
90
 
91
91
  self.selection_blocks[input] = block
92
- self.selection_blocks_description[input] = description
92
+ self.selection_blocks_description[description_input] = description
93
93
  end
94
94
  end
95
95
  end
@@ -1,3 +1,3 @@
1
1
  module Wassup
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/wassup.rb CHANGED
@@ -4,6 +4,11 @@ require "wassup/color"
4
4
  require "wassup/pane"
5
5
  require "wassup/pane_builder"
6
6
 
7
+ require "wassup/helpers/circleci"
8
+ require "wassup/helpers/github"
9
+ require "wassup/helpers/netlify"
10
+ require "wassup/helpers/shortcut"
11
+
7
12
  module Wassup
8
13
  class Error < StandardError; end
9
14
  # Your code goes here...
data/wassup.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.metadata["source_code_uri"] = "https://github.com/joshdholtz/wassup"
17
17
 
18
18
  spec.add_runtime_dependency 'curses'
19
+ spec.add_runtime_dependency 'rest-client'
19
20
 
20
21
  # Specify which files should be added to the gem when it is released.
21
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wassup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Holtz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-21 00:00:00.000000000 Z
11
+ date: 2021-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: curses
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rest-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: A scriptable terminal dashboard
28
42
  email:
29
43
  - me@joshholtz.com
@@ -83,10 +97,15 @@ files:
83
97
  - examples/josh-fastlane/README.md
84
98
  - examples/josh-fastlane/Supfile
85
99
  - examples/josh-fastlane/demo.png
100
+ - examples/simple/Supfile
86
101
  - examples/starter/Supfile
87
102
  - lib/wassup.rb
88
103
  - lib/wassup/app.rb
89
104
  - lib/wassup/color.rb
105
+ - lib/wassup/helpers/circleci.rb
106
+ - lib/wassup/helpers/github.rb
107
+ - lib/wassup/helpers/netlify.rb
108
+ - lib/wassup/helpers/shortcut.rb
90
109
  - lib/wassup/pane.rb
91
110
  - lib/wassup/pane_builder.rb
92
111
  - lib/wassup/version.rb