brainiac-fizzy 0.0.16 → 0.0.17
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 666eda5c356c3b8cc120591eb71c40090b3162adace36563b5c3e1e406aa4154
|
|
4
|
+
data.tar.gz: 51bf4ff7967fd7bcceeedc9ee292a5a5a4a98c4b12d411926780c1722ae125c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8180e6607b5411e10bad40abb263f22fd18c901df1ba0890a7131e8c8fa801836f88cbacda745b2edcafc50144ab5b96d7b613e06d3758df599cac5a28b1a3cd
|
|
7
|
+
data.tar.gz: 5ffdf9eec9082d78ee43c130fef1ef0c1b1e94a2533991acb2f43f9ab84d700fe397444862fa83d92bb5d7398a9402ebb06e2eb55e2d227546d287b73af5f3b8
|
|
@@ -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"]})"
|
|
@@ -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:
|