brainiac-fizzy 0.0.16 → 0.0.18
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/lib/brainiac/plugins/fizzy/cli.rb +465 -1
- data/lib/brainiac/plugins/fizzy/config.rb +16 -2
- data/lib/brainiac/plugins/fizzy/delegators.rb +6 -6
- data/lib/brainiac/plugins/fizzy/handlers/assignment.rb +3 -3
- data/lib/brainiac/plugins/fizzy/handlers/comments.rb +5 -5
- data/lib/brainiac/plugins/fizzy/handlers/dedup.rb +1 -1
- data/lib/brainiac/plugins/fizzy/hooks.rb +29 -0
- data/lib/brainiac/plugins/fizzy/prompts.rb +8 -4
- data/lib/brainiac/plugins/fizzy/version.rb +1 -1
- data/lib/brainiac/plugins/fizzy.rb +4 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34a03eed47ca8ae2a7de8ec8351962c5a3aa59652d7466c14221d4919866519f
|
|
4
|
+
data.tar.gz: e18090ad45a15ce328c6c1fdabc267e32c29868bd28e36f6540af7ed1ace6436
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4db395ff509a514e144c4237affb376ac43de3d2a04ed0a5e89a12f5260c7b02ad144344b3bab5e471f9a617df1af3bbcc99ed29c2151c2e3b61d99df6f5f9c4
|
|
7
|
+
data.tar.gz: d9c0cfdcec762022846ed40cc2e8713504f55c6206df41024c484ec9eade1378d60d82e3dd593ebdf76261d7cbcb8bde25f10dfb863830af2e76aca431b960c2
|
|
@@ -12,6 +12,8 @@ module Brainiac
|
|
|
12
12
|
BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
|
|
13
13
|
FIZZY_CONFIG_FILE = File.join(BRAINIAC_DIR, "fizzy.json")
|
|
14
14
|
|
|
15
|
+
PROJECTS_FILE = File.join(BRAINIAC_DIR, "projects.json")
|
|
16
|
+
|
|
15
17
|
class << self
|
|
16
18
|
def run(args)
|
|
17
19
|
command = args.shift
|
|
@@ -23,6 +25,8 @@ module Brainiac
|
|
|
23
25
|
cmd_status
|
|
24
26
|
when "setup"
|
|
25
27
|
cmd_setup
|
|
28
|
+
when "board"
|
|
29
|
+
cmd_board(args)
|
|
26
30
|
else
|
|
27
31
|
print_help
|
|
28
32
|
end
|
|
@@ -97,6 +101,463 @@ module Brainiac
|
|
|
97
101
|
puts "Webhook URL: https://<your-ngrok>.ngrok-free.app/fizzy/<board-key>"
|
|
98
102
|
end
|
|
99
103
|
|
|
104
|
+
def cmd_board(args)
|
|
105
|
+
sub = args.shift
|
|
106
|
+
case sub
|
|
107
|
+
when "setup", "add"
|
|
108
|
+
board_setup
|
|
109
|
+
when "list", "ls"
|
|
110
|
+
board_list
|
|
111
|
+
when "assign"
|
|
112
|
+
board_assign(args)
|
|
113
|
+
when "columns", "refresh"
|
|
114
|
+
board_columns(args)
|
|
115
|
+
when "webhook"
|
|
116
|
+
board_webhook(args)
|
|
117
|
+
else
|
|
118
|
+
puts "Usage: brainiac fizzy board <setup|list|assign|columns|webhook>"
|
|
119
|
+
puts ""
|
|
120
|
+
puts "Commands:"
|
|
121
|
+
puts " setup Add a new board (interactive, fetches columns from Fizzy)"
|
|
122
|
+
puts " list List configured boards and their columns"
|
|
123
|
+
puts " assign <project> <board_key> Assign a project to a board"
|
|
124
|
+
puts " columns <board_key> Refresh columns for a board from Fizzy"
|
|
125
|
+
puts " webhook <board_key> [url] Create/replace webhook and save the signing secret"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# --- board setup (split into phases) ---
|
|
130
|
+
|
|
131
|
+
def board_setup
|
|
132
|
+
puts "Fizzy Board Setup"
|
|
133
|
+
puts "================="
|
|
134
|
+
puts ""
|
|
135
|
+
|
|
136
|
+
board = board_setup_select_board
|
|
137
|
+
return unless board
|
|
138
|
+
|
|
139
|
+
board_key = board_setup_choose_key(board["name"])
|
|
140
|
+
column_map = board_setup_map_columns(board["id"], board["name"])
|
|
141
|
+
secret = board_setup_ask_secret(board_key)
|
|
142
|
+
|
|
143
|
+
config = board_setup_save(board_key, board["id"], column_map, secret)
|
|
144
|
+
|
|
145
|
+
puts ""
|
|
146
|
+
puts "✓ Board \"#{board["name"]}\" added as '#{board_key}' in #{FIZZY_CONFIG_FILE}"
|
|
147
|
+
puts " Columns: #{column_map.keys.join(", ")}" unless column_map.empty?
|
|
148
|
+
|
|
149
|
+
board_setup_offer_webhook(config, board_key, board["id"])
|
|
150
|
+
|
|
151
|
+
puts ""
|
|
152
|
+
puts "Next steps:"
|
|
153
|
+
puts " brainiac fizzy board assign <project-key> #{board_key}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def board_setup_select_board
|
|
157
|
+
boards_json = run_fizzy("board", "list", "--all", "--json")
|
|
158
|
+
unless boards_json
|
|
159
|
+
puts "Error: Could not fetch boards from Fizzy CLI."
|
|
160
|
+
puts " Ensure 'fizzy' is on your PATH and authenticated."
|
|
161
|
+
puts " Run: fizzy doctor"
|
|
162
|
+
return nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
boards = JSON.parse(boards_json)["data"] || []
|
|
166
|
+
if boards.empty?
|
|
167
|
+
puts "No boards found in your Fizzy account."
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
puts "Available boards:"
|
|
172
|
+
boards.each_with_index do |board, i|
|
|
173
|
+
puts " #{i + 1}) #{board["name"]} (#{board["id"]})"
|
|
174
|
+
end
|
|
175
|
+
puts ""
|
|
176
|
+
print "Select board number: "
|
|
177
|
+
choice = $stdin.gets&.chomp&.to_i
|
|
178
|
+
return puts("Cancelled.") unless choice&.positive? && choice <= boards.size
|
|
179
|
+
|
|
180
|
+
boards[choice - 1]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def board_setup_choose_key(board_name)
|
|
184
|
+
suggested_key = board_name.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
|
|
185
|
+
print "Board key for config [#{suggested_key}]: "
|
|
186
|
+
board_key = $stdin.gets&.chomp
|
|
187
|
+
board_key = suggested_key if board_key.nil? || board_key.empty?
|
|
188
|
+
board_key
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def board_setup_map_columns(board_id, board_name)
|
|
192
|
+
columns_json = run_fizzy("column", "list", "--board", board_id, "--json")
|
|
193
|
+
columns = columns_json ? (JSON.parse(columns_json)["data"] || []) : []
|
|
194
|
+
custom_columns = columns.reject { |c| c["pseudo"] }
|
|
195
|
+
|
|
196
|
+
puts ""
|
|
197
|
+
puts "Columns for \"#{board_name}\":"
|
|
198
|
+
if columns.empty?
|
|
199
|
+
puts " (no columns found)"
|
|
200
|
+
else
|
|
201
|
+
columns.each do |col|
|
|
202
|
+
pseudo_tag = col["pseudo"] ? " [built-in]" : ""
|
|
203
|
+
puts " • #{col["name"]} (#{col["id"]})#{pseudo_tag}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
puts ""
|
|
208
|
+
puts "Which columns should brainiac track? Enter config names for each."
|
|
209
|
+
puts "(These map to column transitions like right_now, needs_review, uat, etc.)"
|
|
210
|
+
puts "Leave blank to skip a column."
|
|
211
|
+
puts ""
|
|
212
|
+
|
|
213
|
+
prompt_column_mapping(custom_columns)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def board_setup_ask_secret(board_key)
|
|
217
|
+
puts ""
|
|
218
|
+
puts "Webhook setup:"
|
|
219
|
+
puts " Fizzy generates the signing secret when you create a webhook."
|
|
220
|
+
print "Paste the webhook signing_secret from Fizzy (or 'skip' to set later): "
|
|
221
|
+
secret = $stdin.gets&.chomp
|
|
222
|
+
if secret.nil? || secret.empty? || secret == "skip"
|
|
223
|
+
puts " ⚠ No secret set — you'll need to add it to fizzy.json manually later."
|
|
224
|
+
puts " Create the webhook in Fizzy, then paste the signing_secret into:"
|
|
225
|
+
puts " #{FIZZY_CONFIG_FILE} → boards.#{board_key}.webhook_secret"
|
|
226
|
+
nil
|
|
227
|
+
else
|
|
228
|
+
secret
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def board_setup_save(board_key, board_id, column_map, secret)
|
|
233
|
+
config = load_fizzy_config
|
|
234
|
+
config["boards"] ||= {}
|
|
235
|
+
board_entry = { "board_id" => board_id, "columns" => column_map }
|
|
236
|
+
board_entry["webhook_secret"] = secret if secret
|
|
237
|
+
config["boards"][board_key] = board_entry
|
|
238
|
+
save_fizzy_config(config)
|
|
239
|
+
config
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def board_setup_offer_webhook(config, board_key, board_id)
|
|
243
|
+
puts ""
|
|
244
|
+
print "Create webhook in Fizzy now? [Y/n]: "
|
|
245
|
+
answer = $stdin.gets&.chomp&.downcase
|
|
246
|
+
return if answer == "n"
|
|
247
|
+
|
|
248
|
+
webhook_url = prompt_webhook_url(board_key)
|
|
249
|
+
return unless webhook_url && !webhook_url.empty?
|
|
250
|
+
|
|
251
|
+
signing_secret = create_fizzy_webhook(board_id, board_key, webhook_url)
|
|
252
|
+
return unless signing_secret
|
|
253
|
+
|
|
254
|
+
config["boards"][board_key]["webhook_secret"] = signing_secret
|
|
255
|
+
save_fizzy_config(config)
|
|
256
|
+
puts "✓ Webhook created! Signing secret saved to fizzy.json."
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# --- board list ---
|
|
260
|
+
|
|
261
|
+
def board_list
|
|
262
|
+
config = load_fizzy_config
|
|
263
|
+
boards = config["boards"] || {}
|
|
264
|
+
|
|
265
|
+
if boards.empty?
|
|
266
|
+
puts "No boards configured."
|
|
267
|
+
puts "Run 'brainiac fizzy board setup' to add one."
|
|
268
|
+
return
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
projects = load_projects_config
|
|
272
|
+
|
|
273
|
+
puts "Configured boards:"
|
|
274
|
+
boards.each do |key, board_config|
|
|
275
|
+
puts ""
|
|
276
|
+
puts " #{key}:"
|
|
277
|
+
puts " Board ID: #{board_config["board_id"]}"
|
|
278
|
+
puts " Webhook: #{board_config["webhook_secret"] ? "configured" : "not set"}"
|
|
279
|
+
|
|
280
|
+
columns = board_config["columns"] || {}
|
|
281
|
+
if columns.any?
|
|
282
|
+
puts " Columns:"
|
|
283
|
+
columns.each { |name, id| puts " #{name}: #{id}" }
|
|
284
|
+
else
|
|
285
|
+
puts " Columns: (none configured)"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
assigned = projects.select { |_, cfg| cfg["fizzy_board"] == key }.keys
|
|
289
|
+
puts " Projects: #{assigned.any? ? assigned.join(", ") : "(none)"}"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# --- board assign ---
|
|
294
|
+
|
|
295
|
+
def board_assign(args)
|
|
296
|
+
project_key = args.shift
|
|
297
|
+
board_key = args.shift
|
|
298
|
+
|
|
299
|
+
unless project_key && board_key
|
|
300
|
+
puts "Usage: brainiac fizzy board assign <project-key> <board-key>"
|
|
301
|
+
puts ""
|
|
302
|
+
puts "Available boards:"
|
|
303
|
+
config = load_fizzy_config
|
|
304
|
+
(config["boards"] || {}).each_key { |k| puts " #{k}" }
|
|
305
|
+
puts ""
|
|
306
|
+
puts "Available projects:"
|
|
307
|
+
load_projects_config.each_key { |k| puts " #{k}" }
|
|
308
|
+
return
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
config = load_fizzy_config
|
|
312
|
+
unless config.dig("boards", board_key)
|
|
313
|
+
puts "Error: Board '#{board_key}' not found in fizzy.json."
|
|
314
|
+
puts "Available: #{(config["boards"] || {}).keys.join(", ")}"
|
|
315
|
+
return
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
projects = load_projects_config
|
|
319
|
+
unless projects.key?(project_key)
|
|
320
|
+
puts "Error: Project '#{project_key}' not found in projects.json."
|
|
321
|
+
puts "Available: #{projects.keys.join(", ")}"
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
projects[project_key]["fizzy_board"] = board_key
|
|
326
|
+
save_projects_config(projects)
|
|
327
|
+
|
|
328
|
+
puts "✓ Assigned project '#{project_key}' to board '#{board_key}'"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# --- board columns ---
|
|
332
|
+
|
|
333
|
+
def board_columns(args)
|
|
334
|
+
board_key = args.shift
|
|
335
|
+
unless board_key
|
|
336
|
+
puts "Usage: brainiac fizzy board columns <board-key>"
|
|
337
|
+
puts ""
|
|
338
|
+
puts "Fetches current columns from Fizzy and lets you update the config."
|
|
339
|
+
return
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
config = load_fizzy_config
|
|
343
|
+
board_config = config.dig("boards", board_key)
|
|
344
|
+
unless board_config
|
|
345
|
+
puts "Error: Board '#{board_key}' not found in fizzy.json."
|
|
346
|
+
puts "Available: #{(config["boards"] || {}).keys.join(", ")}"
|
|
347
|
+
return
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
board_id = board_config["board_id"]
|
|
351
|
+
columns_json = run_fizzy("column", "list", "--board", board_id, "--json")
|
|
352
|
+
unless columns_json
|
|
353
|
+
puts "Error: Could not fetch columns from Fizzy."
|
|
354
|
+
return
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
columns = JSON.parse(columns_json)["data"] || []
|
|
358
|
+
custom_columns = columns.reject { |c| c["pseudo"] }
|
|
359
|
+
existing_map = board_config["columns"] || {}
|
|
360
|
+
|
|
361
|
+
puts "Columns for board '#{board_key}' (#{board_id}):"
|
|
362
|
+
puts ""
|
|
363
|
+
|
|
364
|
+
columns.each do |col|
|
|
365
|
+
pseudo_tag = col["pseudo"] ? " [built-in]" : ""
|
|
366
|
+
existing_key = existing_map.key(col["id"])
|
|
367
|
+
mapped = existing_key ? " → #{existing_key}" : ""
|
|
368
|
+
puts " • #{col["name"]} (#{col["id"]})#{pseudo_tag}#{mapped}"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
puts ""
|
|
372
|
+
puts "Update column mappings? (Enter new config keys, blank to keep, '-' to remove)"
|
|
373
|
+
puts ""
|
|
374
|
+
|
|
375
|
+
new_map = prompt_column_mapping(custom_columns, existing_map)
|
|
376
|
+
|
|
377
|
+
config["boards"][board_key]["columns"] = new_map
|
|
378
|
+
save_fizzy_config(config)
|
|
379
|
+
|
|
380
|
+
puts ""
|
|
381
|
+
puts "✓ Updated columns for '#{board_key}': #{new_map.keys.join(", ")}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# --- board webhook (split into phases) ---
|
|
385
|
+
|
|
386
|
+
def board_webhook(args)
|
|
387
|
+
board_key = args.shift
|
|
388
|
+
webhook_url = args.shift
|
|
389
|
+
|
|
390
|
+
unless board_key
|
|
391
|
+
puts "Usage: brainiac fizzy board webhook <board-key> [payload-url]"
|
|
392
|
+
puts ""
|
|
393
|
+
puts "Creates a webhook in Fizzy for the board and saves the signing secret."
|
|
394
|
+
puts "If the board already has a webhook, deletes it first."
|
|
395
|
+
puts ""
|
|
396
|
+
puts "Available boards:"
|
|
397
|
+
config = load_fizzy_config
|
|
398
|
+
(config["boards"] || {}).each_key { |k| puts " #{k}" }
|
|
399
|
+
return
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
config = load_fizzy_config
|
|
403
|
+
board_config = config.dig("boards", board_key)
|
|
404
|
+
unless board_config
|
|
405
|
+
puts "Error: Board '#{board_key}' not found in fizzy.json."
|
|
406
|
+
return
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
board_id = board_config["board_id"]
|
|
410
|
+
|
|
411
|
+
return if remove_existing_webhooks(board_id) == :cancelled
|
|
412
|
+
|
|
413
|
+
webhook_url ||= prompt_webhook_url(board_key)
|
|
414
|
+
if webhook_url.nil? || webhook_url.empty?
|
|
415
|
+
puts "Error: Payload URL is required."
|
|
416
|
+
return
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
board_webhook_create_and_save(config, board_key, board_id, webhook_url)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def remove_existing_webhooks(board_id)
|
|
423
|
+
existing_json = run_fizzy("webhook", "list", "--board", board_id, "--all", "--json")
|
|
424
|
+
return :proceed unless existing_json
|
|
425
|
+
|
|
426
|
+
existing = JSON.parse(existing_json)["data"] || []
|
|
427
|
+
brainiac_webhooks = existing.select { |w| w["name"]&.start_with?("brainiac") }
|
|
428
|
+
return :proceed unless brainiac_webhooks.any?
|
|
429
|
+
|
|
430
|
+
puts "Existing brainiac webhook(s) found:"
|
|
431
|
+
brainiac_webhooks.each do |w|
|
|
432
|
+
status = w["active"] ? "active" : "inactive"
|
|
433
|
+
puts " • #{w["name"]} (#{status}) → #{w["payload_url"]}"
|
|
434
|
+
end
|
|
435
|
+
print "Delete existing and create new? [Y/n]: "
|
|
436
|
+
answer = $stdin.gets&.chomp&.downcase
|
|
437
|
+
if answer == "n"
|
|
438
|
+
puts "Cancelled."
|
|
439
|
+
return :cancelled
|
|
440
|
+
end
|
|
441
|
+
brainiac_webhooks.each do |w|
|
|
442
|
+
run_fizzy("webhook", "delete", w["id"], "--board", board_id)
|
|
443
|
+
puts " Deleted: #{w["name"]}"
|
|
444
|
+
end
|
|
445
|
+
:proceed
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def board_webhook_create_and_save(config, board_key, board_id, webhook_url)
|
|
449
|
+
signing_secret = create_fizzy_webhook(board_id, board_key, webhook_url)
|
|
450
|
+
webhook_id = @last_webhook_id
|
|
451
|
+
|
|
452
|
+
if signing_secret
|
|
453
|
+
config["boards"][board_key]["webhook_secret"] = signing_secret
|
|
454
|
+
save_fizzy_config(config)
|
|
455
|
+
puts "✓ Webhook created (ID: #{webhook_id})"
|
|
456
|
+
puts " URL: #{webhook_url}"
|
|
457
|
+
puts " Signing secret saved to fizzy.json"
|
|
458
|
+
else
|
|
459
|
+
puts "✓ Webhook created but no signing_secret in response."
|
|
460
|
+
puts " Check: fizzy webhook show #{webhook_id} --board #{board_id}"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# --- shared helpers ---
|
|
465
|
+
|
|
466
|
+
def prompt_column_mapping(columns, existing_map = {})
|
|
467
|
+
column_map = {}
|
|
468
|
+
columns.each do |col|
|
|
469
|
+
existing_key = existing_map.key(col["id"])
|
|
470
|
+
suggested = existing_key || col["name"].downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
|
|
471
|
+
print " \"#{col["name"]}\" → config key [#{suggested}]: "
|
|
472
|
+
key = $stdin.gets&.chomp
|
|
473
|
+
key = suggested if key.nil? || key.empty?
|
|
474
|
+
next if ["-", "skip"].include?(key)
|
|
475
|
+
|
|
476
|
+
column_map[key] = col["id"]
|
|
477
|
+
end
|
|
478
|
+
column_map
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def prompt_webhook_url(board_key)
|
|
482
|
+
detected_url = detect_ngrok_url
|
|
483
|
+
if detected_url
|
|
484
|
+
default_url = "#{detected_url}/fizzy/#{board_key}"
|
|
485
|
+
print "Webhook payload URL [#{default_url}]: "
|
|
486
|
+
input = $stdin.gets&.chomp
|
|
487
|
+
input.nil? || input.empty? ? default_url : input
|
|
488
|
+
else
|
|
489
|
+
print "Webhook payload URL (e.g., https://your-ngrok.app/fizzy/#{board_key}): "
|
|
490
|
+
$stdin.gets&.chomp
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def create_fizzy_webhook(board_id, board_key, webhook_url)
|
|
495
|
+
actions = "card_assigned,card_closed,card_published,card_reopened,card_triaged,comment_created"
|
|
496
|
+
webhook_json = run_fizzy("webhook", "create", "--board", board_id,
|
|
497
|
+
"--name", "brainiac-#{board_key}",
|
|
498
|
+
"--url", webhook_url,
|
|
499
|
+
"--actions", actions, "--json")
|
|
500
|
+
unless webhook_json
|
|
501
|
+
puts "⚠ Failed to create webhook. Create it manually in Fizzy."
|
|
502
|
+
return nil
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
webhook_data = JSON.parse(webhook_json)["data"]
|
|
506
|
+
@last_webhook_id = webhook_data&.dig("id")
|
|
507
|
+
webhook_data&.dig("signing_secret")
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def load_fizzy_config
|
|
511
|
+
if File.exist?(FIZZY_CONFIG_FILE)
|
|
512
|
+
JSON.parse(File.read(FIZZY_CONFIG_FILE))
|
|
513
|
+
else
|
|
514
|
+
{ "boards" => {}, "authorized_users" => [] }
|
|
515
|
+
end
|
|
516
|
+
rescue JSON::ParserError
|
|
517
|
+
{ "boards" => {}, "authorized_users" => [] }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def save_fizzy_config(config)
|
|
521
|
+
File.write(FIZZY_CONFIG_FILE, JSON.pretty_generate(config))
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def load_projects_config
|
|
525
|
+
if File.exist?(PROJECTS_FILE)
|
|
526
|
+
JSON.parse(File.read(PROJECTS_FILE))
|
|
527
|
+
else
|
|
528
|
+
{}
|
|
529
|
+
end
|
|
530
|
+
rescue JSON::ParserError
|
|
531
|
+
{}
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def save_projects_config(projects)
|
|
535
|
+
File.write(PROJECTS_FILE, JSON.pretty_generate(projects))
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def run_fizzy(*)
|
|
539
|
+
require "open3"
|
|
540
|
+
output, status = Open3.capture2("fizzy", *)
|
|
541
|
+
status.success? ? output : nil
|
|
542
|
+
rescue Errno::ENOENT
|
|
543
|
+
nil
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def detect_ngrok_url
|
|
547
|
+
require "net/http"
|
|
548
|
+
uri = URI("http://127.0.0.1:4040/api/tunnels")
|
|
549
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
550
|
+
http.open_timeout = 2
|
|
551
|
+
http.read_timeout = 2
|
|
552
|
+
response = http.get(uri.path)
|
|
553
|
+
data = JSON.parse(response.body)
|
|
554
|
+
tunnels = data["tunnels"] || []
|
|
555
|
+
https_tunnel = tunnels.find { |t| t["public_url"]&.start_with?("https://") }
|
|
556
|
+
https_tunnel&.dig("public_url")
|
|
557
|
+
rescue StandardError
|
|
558
|
+
nil
|
|
559
|
+
end
|
|
560
|
+
|
|
100
561
|
def print_help
|
|
101
562
|
puts <<~HELP
|
|
102
563
|
Usage: brainiac fizzy <command>
|
|
@@ -105,6 +566,10 @@ module Brainiac
|
|
|
105
566
|
config Show Fizzy config
|
|
106
567
|
status Check Fizzy status via server API
|
|
107
568
|
setup Show setup guide
|
|
569
|
+
board setup Add a new board (interactive, fetches columns from Fizzy)
|
|
570
|
+
board list List configured boards and their columns
|
|
571
|
+
board assign <project> <board_key> Assign a project to a board
|
|
572
|
+
board columns <board_key> Refresh columns for a board from Fizzy
|
|
108
573
|
|
|
109
574
|
Fizzy handles card assignment, comments, @mentions, cross-agent reviews,
|
|
110
575
|
duplicate detection, and planning mode via webhooks.
|
|
@@ -155,7 +620,6 @@ module Brainiac
|
|
|
155
620
|
config = JSON.parse(File.read(config_file))
|
|
156
621
|
config["authorized_users"] ||= []
|
|
157
622
|
|
|
158
|
-
# Don't add if already present
|
|
159
623
|
existing = config["authorized_users"].find { |u| u["id"] == user_id || u["name"]&.downcase == display_name.downcase }
|
|
160
624
|
if existing
|
|
161
625
|
puts " ✓ #{display_name} already in authorized_users (id: #{existing["id"]})"
|
|
@@ -92,7 +92,7 @@ module Brainiac
|
|
|
92
92
|
user && user["human"]
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
def identify_project_by_tags(tags)
|
|
95
|
+
def identify_project_by_tags(tags, board_key: nil)
|
|
96
96
|
tag_names = tags.map { |t| t.is_a?(Hash) ? t["name"] : t.to_s }.map(&:downcase)
|
|
97
97
|
|
|
98
98
|
PROJECTS.each do |key, config|
|
|
@@ -100,7 +100,21 @@ module Brainiac
|
|
|
100
100
|
return [key, config] if tag_names.intersect?(project_tags)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
# Fall back to
|
|
103
|
+
# Fall back to board's default_project if configured
|
|
104
|
+
if board_key
|
|
105
|
+
board_config = @boards[board_key]
|
|
106
|
+
if board_config && board_config["default_project"]
|
|
107
|
+
project_key = board_config["default_project"]
|
|
108
|
+
return [project_key, PROJECTS[project_key]] if PROJECTS.key?(project_key)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Fall back to any project whose fizzy_board matches this board_key
|
|
112
|
+
PROJECTS.each do |key, config|
|
|
113
|
+
return [key, config] if config["fizzy_board"] == board_key
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Fall back to global default project
|
|
104
118
|
default_key = default_project_key
|
|
105
119
|
default_key ? [default_key, PROJECTS[default_key]] : nil
|
|
106
120
|
end
|
|
@@ -72,8 +72,8 @@ def verify_signature!(request, payload_body, board_key: nil)
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
# Config delegators
|
|
75
|
-
def identify_project_by_tags(tags)
|
|
76
|
-
Brainiac::Plugins::Fizzy::Config.identify_project_by_tags(tags)
|
|
75
|
+
def identify_project_by_tags(tags, board_key: nil)
|
|
76
|
+
Brainiac::Plugins::Fizzy::Config.identify_project_by_tags(tags, board_key: board_key)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def board_config(board_key)
|
|
@@ -114,14 +114,14 @@ end
|
|
|
114
114
|
|
|
115
115
|
# Webhook dispatch — routes incoming actions to the appropriate handler.
|
|
116
116
|
# Called from within the Sinatra route block, so it must be a top-level method.
|
|
117
|
-
def dispatch_webhook_action(action, payload)
|
|
117
|
+
def dispatch_webhook_action(action, payload, board_key: nil)
|
|
118
118
|
case action
|
|
119
119
|
when "card_assigned"
|
|
120
|
-
handle_card_assigned(payload)
|
|
120
|
+
handle_card_assigned(payload, board_key: board_key)
|
|
121
121
|
when "comment_created"
|
|
122
|
-
handle_comment(payload)
|
|
122
|
+
handle_comment(payload, board_key: board_key)
|
|
123
123
|
when "card_published", "card_triaged"
|
|
124
|
-
Brainiac::Plugins::Fizzy.handle_publish_or_triage(action, payload)
|
|
124
|
+
Brainiac::Plugins::Fizzy.handle_publish_or_triage(action, payload, board_key: board_key)
|
|
125
125
|
else
|
|
126
126
|
LOG.info "[Fizzy] Ignoring unknown action: #{action}"
|
|
127
127
|
[200, { status: "ignored", action: action }.to_json]
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# When a card is assigned to a local agent, creates a worktree, builds the prompt,
|
|
6
6
|
# and dispatches the agent to begin work.
|
|
7
7
|
|
|
8
|
-
def handle_card_assigned(payload)
|
|
8
|
+
def handle_card_assigned(payload, board_key: nil)
|
|
9
9
|
eventable = payload["eventable"] || {}
|
|
10
10
|
assignees = eventable["assignees"] || []
|
|
11
11
|
|
|
@@ -23,10 +23,10 @@ def handle_card_assigned(payload)
|
|
|
23
23
|
title = eventable["title"] || "untitled"
|
|
24
24
|
tags = eventable["tags"] || []
|
|
25
25
|
|
|
26
|
-
project_result = identify_project_by_tags(tags)
|
|
26
|
+
project_result = identify_project_by_tags(tags, board_key: board_key)
|
|
27
27
|
unless project_result
|
|
28
28
|
tag_names = tags.map { |t| t.is_a?(Hash) ? t["name"] : t }.join(", ")
|
|
29
|
-
LOG.warn "No project found for card ##{card_number} with tags: #{tag_names}"
|
|
29
|
+
LOG.warn "No project found for card ##{card_number} with tags: #{tag_names} (board: #{board_key})"
|
|
30
30
|
return [200, { status: "ignored", reason: "no matching project" }.to_json]
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -21,7 +21,7 @@ CommentContext = Struct.new(
|
|
|
21
21
|
keyword_init: true
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
def handle_comment(payload)
|
|
24
|
+
def handle_comment(payload, board_key: nil)
|
|
25
25
|
eventable = payload["eventable"] || {}
|
|
26
26
|
plain_text = eventable.dig("body", "plain_text") || ""
|
|
27
27
|
card_internal_id = eventable.dig("card", "id")
|
|
@@ -44,7 +44,7 @@ def handle_comment(payload)
|
|
|
44
44
|
card_info = lookup_fizzy_card_info(card_internal_id)
|
|
45
45
|
comment_id = eventable["id"]
|
|
46
46
|
|
|
47
|
-
project_config, project_key = resolve_fizzy_project(card_info, card_internal_id, eventable)
|
|
47
|
+
project_config, project_key = resolve_fizzy_project(card_info, card_internal_id, eventable, board_key: board_key)
|
|
48
48
|
return [200, { status: "ignored", reason: "no matching project" }.to_json] unless project_config
|
|
49
49
|
|
|
50
50
|
tags = parse_inline_tags(plain_text)
|
|
@@ -285,14 +285,14 @@ def handle_cancel_command(eventable, card_internal_id)
|
|
|
285
285
|
[200, { status: "cancelled", card: card_number_for_cancel || card_internal_id, sessions_killed: killed }.to_json]
|
|
286
286
|
end
|
|
287
287
|
|
|
288
|
-
def resolve_fizzy_project(card_info, card_internal_id, eventable)
|
|
288
|
+
def resolve_fizzy_project(card_info, card_internal_id, eventable, board_key: nil)
|
|
289
289
|
if card_info
|
|
290
290
|
if card_info["project"]
|
|
291
291
|
project_key = card_info["project"]
|
|
292
292
|
project_config = PROJECTS[project_key] || DEFAULT_PROJECT
|
|
293
293
|
else
|
|
294
294
|
card_tags = eventable.dig("card", "tags") || []
|
|
295
|
-
project_result = identify_project_by_tags(card_tags)
|
|
295
|
+
project_result = identify_project_by_tags(card_tags, board_key: board_key)
|
|
296
296
|
if project_result
|
|
297
297
|
project_key, project_config = project_result
|
|
298
298
|
card_info["project"] = project_key
|
|
@@ -305,7 +305,7 @@ def resolve_fizzy_project(card_info, card_internal_id, eventable)
|
|
|
305
305
|
end
|
|
306
306
|
else
|
|
307
307
|
card_tags = eventable.dig("card", "tags") || []
|
|
308
|
-
project_result = identify_project_by_tags(card_tags)
|
|
308
|
+
project_result = identify_project_by_tags(card_tags, board_key: board_key)
|
|
309
309
|
if project_result
|
|
310
310
|
project_key, project_config = project_result
|
|
311
311
|
else
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# When a new card is created, checks for similar existing cards using
|
|
6
6
|
# trigram and semantic similarity. Posts a warning comment if duplicates found.
|
|
7
7
|
|
|
8
|
-
def handle_card_published(payload)
|
|
8
|
+
def handle_card_published(payload, board_key: nil)
|
|
9
9
|
eventable = payload["eventable"] || {}
|
|
10
10
|
card_number = eventable["number"]
|
|
11
11
|
title = eventable["title"] || ""
|
|
@@ -336,9 +336,38 @@ module Brainiac
|
|
|
336
336
|
LOG.info "[Fizzy:CardIndex] Starting background backfill..."
|
|
337
337
|
CARD_INDEX.backfill
|
|
338
338
|
end
|
|
339
|
+
|
|
340
|
+
# Auto-reactivate deactivated webhooks in background
|
|
341
|
+
Thread.new { check_and_reactivate_webhooks }
|
|
339
342
|
end
|
|
340
343
|
end
|
|
341
344
|
|
|
345
|
+
def check_and_reactivate_webhooks
|
|
346
|
+
boards = Config.boards
|
|
347
|
+
return if boards.empty?
|
|
348
|
+
|
|
349
|
+
env = Helpers.default_fizzy_env
|
|
350
|
+
boards.each do |board_key, board_config|
|
|
351
|
+
board_id = board_config["board_id"]
|
|
352
|
+
next unless board_id
|
|
353
|
+
|
|
354
|
+
output = run_cmd("fizzy", "webhook", "list", "--board", board_id, "--all", "--json", env: env)
|
|
355
|
+
webhooks = JSON.parse(output)["data"] || []
|
|
356
|
+
|
|
357
|
+
webhooks.each do |webhook|
|
|
358
|
+
next unless webhook["name"]&.start_with?("brainiac")
|
|
359
|
+
next if webhook["active"]
|
|
360
|
+
|
|
361
|
+
LOG.info "[Fizzy] Reactivating webhook '#{webhook["name"]}' on board '#{board_key}' (was inactive)"
|
|
362
|
+
run_cmd("fizzy", "webhook", "reactivate", webhook["id"], "--board", board_id, env: env)
|
|
363
|
+
end
|
|
364
|
+
rescue StandardError => e
|
|
365
|
+
LOG.warn "[Fizzy] Could not check webhooks for board '#{board_key}': #{e.message}"
|
|
366
|
+
end
|
|
367
|
+
rescue StandardError => e
|
|
368
|
+
LOG.warn "[Fizzy] Webhook reactivation check failed: #{e.message}"
|
|
369
|
+
end
|
|
370
|
+
|
|
342
371
|
# Detect CLI provider from Fizzy card tags (e.g., cli-grok tag)
|
|
343
372
|
def register_detect_cli_provider
|
|
344
373
|
Brainiac.on(:detect_cli_provider) do |ctx|
|
|
@@ -23,8 +23,12 @@ module Brainiac
|
|
|
23
23
|
### Screenshots (MANDATORY for UI changes)
|
|
24
24
|
If you touched any `.js`, `.jsx`, `.css`, or `.html` in a web app directory and `./scripts/screenshot-page.sh` exists, screenshot every affected page.
|
|
25
25
|
|
|
26
|
-
###
|
|
27
|
-
|
|
26
|
+
### Fizzy CLI Tips
|
|
27
|
+
- Use `--jq` for filtering output (built-in, never pipe to external jq): `fizzy card list --jq '[.data[] | {number, title}]'`
|
|
28
|
+
- Use `fizzy search "query"` for cross-board full-text search
|
|
29
|
+
- Use `fizzy card list --search "term"` for structured filtering within a board
|
|
30
|
+
- Use `--agent` flag for raw JSON data without envelope when parsing output
|
|
31
|
+
- Retrieve full comment text: `fizzy comment show COMMENT_ID --card CARD_NUMBER`
|
|
28
32
|
|
|
29
33
|
### Card Memory Discipline (CRITICAL for long-running cards)
|
|
30
34
|
When writing your memory file for a Fizzy card session, include:
|
|
@@ -40,8 +44,8 @@ module Brainiac
|
|
|
40
44
|
Re-fetch the card to see if anything changed while you were working:
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
|
-
fizzy card show {{CARD_NUMBER}}
|
|
44
|
-
fizzy comment list --card {{CARD_NUMBER}}
|
|
47
|
+
fizzy card show {{CARD_NUMBER}} --jq '.data | {title, description, assignees: [.assignees[].name]}'
|
|
48
|
+
fizzy comment list --card {{CARD_NUMBER}} --jq '[.data[-5:] | .[] | {creator: .creator.name, body: .body.plain_text}]'
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
Compare against the card context from the start of your session. Check for:
|
|
@@ -78,7 +78,7 @@ module Brainiac
|
|
|
78
78
|
reload_projects!
|
|
79
79
|
reload_agent_registry!
|
|
80
80
|
|
|
81
|
-
status_code, body = dispatch_webhook_action(action, payload)
|
|
81
|
+
status_code, body = dispatch_webhook_action(action, payload, board_key: board_key)
|
|
82
82
|
LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
|
|
83
83
|
halt status_code, body
|
|
84
84
|
rescue JSON::ParserError => e
|
|
@@ -175,7 +175,7 @@ module Brainiac
|
|
|
175
175
|
|
|
176
176
|
public
|
|
177
177
|
|
|
178
|
-
def handle_publish_or_triage(action, payload)
|
|
178
|
+
def handle_publish_or_triage(action, payload, board_key: nil)
|
|
179
179
|
eventable = payload["eventable"] || {}
|
|
180
180
|
card_number = eventable["number"]&.to_s
|
|
181
181
|
|
|
@@ -189,10 +189,10 @@ module Brainiac
|
|
|
189
189
|
|
|
190
190
|
if action == "card_published"
|
|
191
191
|
assignees = eventable["assignees"] || []
|
|
192
|
-
return handle_card_assigned(payload) if assignees.any? { |a| local_agent_names.include?(a["name"]) }
|
|
192
|
+
return handle_card_assigned(payload, board_key: board_key) if assignees.any? { |a| local_agent_names.include?(a["name"]) }
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
handle_card_published(payload)
|
|
195
|
+
handle_card_published(payload, board_key: board_key)
|
|
196
196
|
end
|
|
197
197
|
end
|
|
198
198
|
end
|