brainiac-basecamp 0.0.22 โ 0.0.23
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/basecamp/cli.rb +287 -1
- data/lib/brainiac/plugins/basecamp/version.rb +1 -1
- 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: afa545126394e953d63caa1cc6f70658f9c0c1333c77abb6a704230b72d6f8cb
|
|
4
|
+
data.tar.gz: 1fe95fc5d1433efa9b0b526686da1349141de314d5de59c25073e0798d05af25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e68209ddcbc0ff9a4f4889d68354f424638323b99ed5c252c870e55f848680ebc752b58b9b24f97ad20f5e9f4ad7c6a96ad8f18d645f48c7ef3d299a064dc460
|
|
7
|
+
data.tar.gz: b94eee6029d0c89b60b4e6582af63164ac31035de1994f069c70f05afa98838443ae4380c47d21ef573d09664ff22bc3e896aab6e34f1c3b66e2225b50c94260
|
|
@@ -37,6 +37,8 @@ module Brainiac
|
|
|
37
37
|
cmd_set(args)
|
|
38
38
|
when "reset"
|
|
39
39
|
cmd_reset(args)
|
|
40
|
+
when "scrap"
|
|
41
|
+
cmd_scrap(args)
|
|
40
42
|
when "webhook", "webhooks"
|
|
41
43
|
cmd_webhook(args)
|
|
42
44
|
else
|
|
@@ -332,6 +334,183 @@ module Brainiac
|
|
|
332
334
|
exec ENV.fetch("SHELL", "/bin/bash")
|
|
333
335
|
end
|
|
334
336
|
|
|
337
|
+
def cmd_scrap(args)
|
|
338
|
+
todolist_id = args.shift
|
|
339
|
+
|
|
340
|
+
dry_run = args.delete("--dry-run") || args.delete("-n")
|
|
341
|
+
confirmed = args.delete("--confirm") || args.delete("-y")
|
|
342
|
+
keep_cards = args.delete("--keep-cards")
|
|
343
|
+
|
|
344
|
+
unless todolist_id
|
|
345
|
+
puts "Usage: brainiac basecamp scrap <todolist-id> [options]"
|
|
346
|
+
puts ""
|
|
347
|
+
puts "Scrap an epic โ full teardown of all related resources."
|
|
348
|
+
puts ""
|
|
349
|
+
puts "Options:"
|
|
350
|
+
puts " --confirm, -y Skip confirmation prompt"
|
|
351
|
+
puts " --dry-run, -n Show what would happen without doing it"
|
|
352
|
+
puts " --keep-cards Don't close the Fizzy cards (keep them for re-planning)"
|
|
353
|
+
puts ""
|
|
354
|
+
puts "This will:"
|
|
355
|
+
puts " 1. Close all Fizzy cards in the epic (unless --keep-cards)"
|
|
356
|
+
puts " 2. Close any open PRs targeting the epic branch"
|
|
357
|
+
puts " 3. Delete the epic branch from all repos"
|
|
358
|
+
puts " 4. Mark all Basecamp todos complete with a ๐๏ธ comment"
|
|
359
|
+
puts " 5. Clean up epic memory files"
|
|
360
|
+
puts " 6. Remove the epic from state"
|
|
361
|
+
puts ""
|
|
362
|
+
puts "Active epics:"
|
|
363
|
+
scrappable_statuses = %w[active cancelled]
|
|
364
|
+
load_epics.select { |e| scrappable_statuses.include?(e["status"]) }.each do |e|
|
|
365
|
+
puts " #{e['basecamp_todolist_id']} โ #{e['title']} [#{e['status']}]"
|
|
366
|
+
end
|
|
367
|
+
return
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
epics = load_epics
|
|
371
|
+
epic = epics.find { |e| e["basecamp_todolist_id"] == todolist_id.to_s }
|
|
372
|
+
|
|
373
|
+
unless epic
|
|
374
|
+
puts "โ No epic found with todolist ID #{todolist_id}"
|
|
375
|
+
puts ""
|
|
376
|
+
puts "Known epics:"
|
|
377
|
+
epics.each do |e|
|
|
378
|
+
puts " #{e['basecamp_todolist_id']} โ #{e['title']} [#{e['status']}]"
|
|
379
|
+
end
|
|
380
|
+
return
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
tasks = epic["tasks"] || []
|
|
384
|
+
epic_branches = epic["epic_branches"] || {}
|
|
385
|
+
card_numbers = tasks.filter_map { |t| t["fizzy_card"] }
|
|
386
|
+
|
|
387
|
+
# Preview what we're about to do
|
|
388
|
+
puts dry_run ? "๐ DRY RUN โ nothing will be changed" : "๐๏ธ Scrapping epic: #{epic['title']}"
|
|
389
|
+
puts ""
|
|
390
|
+
puts "Epic: #{epic['title']}"
|
|
391
|
+
puts "Status: #{epic['status']}"
|
|
392
|
+
puts "Todolist: #{epic['basecamp_todolist_id']}"
|
|
393
|
+
puts "Tasks: #{tasks.size}"
|
|
394
|
+
puts ""
|
|
395
|
+
|
|
396
|
+
actions = []
|
|
397
|
+
|
|
398
|
+
# 1. Fizzy cards to close
|
|
399
|
+
unless keep_cards
|
|
400
|
+
if card_numbers.any?
|
|
401
|
+
actions << { type: :close_cards, cards: card_numbers }
|
|
402
|
+
puts " ๐ Close #{card_numbers.size} Fizzy card(s): #{card_numbers.map { |c| "##{c}" }.join(', ')}"
|
|
403
|
+
end
|
|
404
|
+
else
|
|
405
|
+
puts " ๐ Keep Fizzy cards (--keep-cards)"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# 2. Close open PRs and delete epic branches
|
|
409
|
+
if epic_branches.any?
|
|
410
|
+
epic_branches.each do |project_key, branch_name|
|
|
411
|
+
actions << { type: :close_prs_and_delete_branch, project: project_key, branch: branch_name }
|
|
412
|
+
puts " ๐ฟ Delete branch '#{branch_name}' in #{project_key} (close open PRs first)"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# 3. Mark Basecamp todos complete
|
|
417
|
+
todo_ids = tasks.filter_map { |t| t["todo_id"] }
|
|
418
|
+
if todo_ids.any?
|
|
419
|
+
actions << { type: :complete_todos, todo_ids: todo_ids, project_id: epic["basecamp_project_id"] }
|
|
420
|
+
puts " โ Mark #{todo_ids.size} Basecamp todo(s) complete"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# 4. Clean up epic memory
|
|
424
|
+
actions << { type: :cleanup_memory, todolist_id: todolist_id }
|
|
425
|
+
puts " ๐งน Clean up epic memory"
|
|
426
|
+
|
|
427
|
+
# 5. Remove from state
|
|
428
|
+
actions << { type: :remove_from_state, epic_id: epic["id"] }
|
|
429
|
+
puts " ๐พ Remove epic from state file"
|
|
430
|
+
|
|
431
|
+
puts ""
|
|
432
|
+
|
|
433
|
+
if dry_run
|
|
434
|
+
puts "No changes made. Remove --dry-run to execute."
|
|
435
|
+
return
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Confirm unless --confirm passed
|
|
439
|
+
unless confirmed
|
|
440
|
+
print "Proceed? [y/N] "
|
|
441
|
+
answer = $stdin.gets&.strip&.downcase
|
|
442
|
+
unless %w[y yes].include?(answer)
|
|
443
|
+
puts "Aborted."
|
|
444
|
+
return
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
puts ""
|
|
449
|
+
puts "Scrapping..."
|
|
450
|
+
|
|
451
|
+
errors = []
|
|
452
|
+
|
|
453
|
+
actions.each do |action|
|
|
454
|
+
case action[:type]
|
|
455
|
+
when :close_cards
|
|
456
|
+
action[:cards].each do |card_number|
|
|
457
|
+
result = scrap_close_fizzy_card(card_number)
|
|
458
|
+
if result
|
|
459
|
+
puts " โ Closed Fizzy card ##{card_number}"
|
|
460
|
+
else
|
|
461
|
+
errors << "Failed to close Fizzy card ##{card_number}"
|
|
462
|
+
puts " โ Failed to close Fizzy card ##{card_number}"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
when :close_prs_and_delete_branch
|
|
467
|
+
project_key = action[:project]
|
|
468
|
+
branch_name = action[:branch]
|
|
469
|
+
repo_path = resolve_repo_path(project_key)
|
|
470
|
+
|
|
471
|
+
if repo_path
|
|
472
|
+
# Close any open PRs targeting or from the epic branch
|
|
473
|
+
closed_prs = scrap_close_epic_prs(repo_path, branch_name)
|
|
474
|
+
closed_prs.each { |pr| puts " โ Closed PR ##{pr} in #{project_key}" }
|
|
475
|
+
|
|
476
|
+
# Delete the remote branch
|
|
477
|
+
if scrap_delete_branch(repo_path, branch_name)
|
|
478
|
+
puts " โ Deleted branch '#{branch_name}' in #{project_key}"
|
|
479
|
+
else
|
|
480
|
+
errors << "Failed to delete branch '#{branch_name}' in #{project_key}"
|
|
481
|
+
puts " โ Failed to delete branch '#{branch_name}' in #{project_key}"
|
|
482
|
+
end
|
|
483
|
+
else
|
|
484
|
+
errors << "Could not resolve repo path for project '#{project_key}'"
|
|
485
|
+
puts " โ Could not resolve repo path for '#{project_key}'"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
when :complete_todos
|
|
489
|
+
action[:todo_ids].each do |todo_id|
|
|
490
|
+
scrap_complete_todo(todo_id, action[:project_id], epic["agent"])
|
|
491
|
+
end
|
|
492
|
+
puts " โ Marked #{action[:todo_ids].size} todo(s) complete in Basecamp"
|
|
493
|
+
|
|
494
|
+
when :cleanup_memory
|
|
495
|
+
EpicMemory.cleanup(action[:todolist_id], archive: false)
|
|
496
|
+
puts " โ Cleaned up epic memory"
|
|
497
|
+
|
|
498
|
+
when :remove_from_state
|
|
499
|
+
epics.reject! { |e| e["id"] == action[:epic_id] }
|
|
500
|
+
save_epics(epics)
|
|
501
|
+
puts " โ Removed epic from state"
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
puts ""
|
|
506
|
+
if errors.empty?
|
|
507
|
+
puts "โ
Epic scrapped successfully."
|
|
508
|
+
else
|
|
509
|
+
puts "โ ๏ธ Epic scrapped with #{errors.size} error(s):"
|
|
510
|
+
errors.each { |e| puts " - #{e}" }
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
335
514
|
def cmd_link(args)
|
|
336
515
|
fizzy_card = args.shift
|
|
337
516
|
basecamp_url = args.shift
|
|
@@ -663,6 +842,7 @@ module Brainiac
|
|
|
663
842
|
reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
|
|
664
843
|
reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
|
|
665
844
|
reset gates <card-number> Clear gate approvals and re-dispatch gates
|
|
845
|
+
scrap <todolist-id> [--confirm] Scrap an epic: close cards, delete branches, clean up
|
|
666
846
|
webhook sync Ensure webhooks have all required event types
|
|
667
847
|
|
|
668
848
|
Config file: ~/.brainiac/basecamp.json
|
|
@@ -1010,6 +1190,112 @@ module Brainiac
|
|
|
1010
1190
|
}))
|
|
1011
1191
|
end
|
|
1012
1192
|
|
|
1193
|
+
# --- Scrap helpers ---
|
|
1194
|
+
|
|
1195
|
+
# Close a Fizzy card via CLI.
|
|
1196
|
+
def scrap_close_fizzy_card(card_number)
|
|
1197
|
+
stdout, stderr, status = Open3.capture3("fizzy", "card", "close", card_number.to_s)
|
|
1198
|
+
status.success?
|
|
1199
|
+
rescue StandardError => e
|
|
1200
|
+
false
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# Close all open PRs that reference the epic branch (head or base).
|
|
1204
|
+
#
|
|
1205
|
+
# @param repo_path [String] Path to the repo
|
|
1206
|
+
# @param branch_name [String] Epic branch name
|
|
1207
|
+
# @return [Array<Integer>] Closed PR numbers
|
|
1208
|
+
def scrap_close_epic_prs(repo_path, branch_name)
|
|
1209
|
+
closed = []
|
|
1210
|
+
|
|
1211
|
+
# Find PRs where epic branch is the base (task PRs targeting epic branch)
|
|
1212
|
+
stdout, _stderr, status = Open3.capture3(
|
|
1213
|
+
"gh", "pr", "list", "--base", branch_name, "--state", "open",
|
|
1214
|
+
"--json", "number", "--jq", ".[].number",
|
|
1215
|
+
chdir: repo_path
|
|
1216
|
+
)
|
|
1217
|
+
if status.success? && !stdout.strip.empty?
|
|
1218
|
+
stdout.strip.split("\n").each do |pr_num|
|
|
1219
|
+
close_pr(repo_path, pr_num.strip.to_i, "Scrapped as part of epic teardown")
|
|
1220
|
+
closed << pr_num.strip.to_i
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
# Find PRs where epic branch is the head (final PR to main)
|
|
1225
|
+
stdout, _stderr, status = Open3.capture3(
|
|
1226
|
+
"gh", "pr", "list", "--head", branch_name, "--state", "open",
|
|
1227
|
+
"--json", "number", "--jq", ".[].number",
|
|
1228
|
+
chdir: repo_path
|
|
1229
|
+
)
|
|
1230
|
+
if status.success? && !stdout.strip.empty?
|
|
1231
|
+
stdout.strip.split("\n").each do |pr_num|
|
|
1232
|
+
close_pr(repo_path, pr_num.strip.to_i, "Scrapped as part of epic teardown")
|
|
1233
|
+
closed << pr_num.strip.to_i
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
closed
|
|
1238
|
+
rescue StandardError
|
|
1239
|
+
closed
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
# Close a single PR with a comment.
|
|
1243
|
+
def close_pr(repo_path, pr_number, comment)
|
|
1244
|
+
Open3.capture3(
|
|
1245
|
+
"gh", "pr", "close", pr_number.to_s, "--comment", "๐๏ธ #{comment}",
|
|
1246
|
+
chdir: repo_path
|
|
1247
|
+
)
|
|
1248
|
+
rescue StandardError
|
|
1249
|
+
# Best effort
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
# Delete an epic branch from the remote.
|
|
1253
|
+
def scrap_delete_branch(repo_path, branch_name)
|
|
1254
|
+
# Delete remote branch
|
|
1255
|
+
_stdout, _stderr, status = Open3.capture3(
|
|
1256
|
+
"git", "push", "origin", "--delete", branch_name,
|
|
1257
|
+
chdir: repo_path
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
# Also delete local branch if it exists
|
|
1261
|
+
Open3.capture3("git", "branch", "-D", branch_name, chdir: repo_path)
|
|
1262
|
+
|
|
1263
|
+
status.success?
|
|
1264
|
+
rescue StandardError
|
|
1265
|
+
false
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
# Mark a Basecamp todo complete with a scrap comment.
|
|
1269
|
+
def scrap_complete_todo(todo_id, project_id, agent)
|
|
1270
|
+
# Add a comment first
|
|
1271
|
+
Client.run_safe(
|
|
1272
|
+
"comments", "create", todo_id.to_s,
|
|
1273
|
+
"๐๏ธ Scrapped โ epic torn down via `brainiac basecamp scrap`",
|
|
1274
|
+
"--in", project_id.to_s, "--json",
|
|
1275
|
+
profile: agent&.downcase
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# Mark complete
|
|
1279
|
+
Client.run_safe(
|
|
1280
|
+
"todos", "complete", todo_id.to_s,
|
|
1281
|
+
"--in", project_id.to_s, "--json",
|
|
1282
|
+
profile: agent&.downcase
|
|
1283
|
+
)
|
|
1284
|
+
rescue StandardError
|
|
1285
|
+
# Best effort
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
# Resolve a project key to its repo path from projects.json.
|
|
1289
|
+
def resolve_repo_path(project_key)
|
|
1290
|
+
projects_file = File.join(BRAINIAC_DIR, "projects.json")
|
|
1291
|
+
return nil unless File.exist?(projects_file)
|
|
1292
|
+
|
|
1293
|
+
projects = JSON.parse(File.read(projects_file))
|
|
1294
|
+
projects.dig(project_key, "repo_path")
|
|
1295
|
+
rescue JSON::ParserError
|
|
1296
|
+
nil
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1013
1299
|
# --- Deploy helpers ---
|
|
1014
1300
|
|
|
1015
1301
|
def list_epics_for_deploy
|
|
@@ -1150,7 +1436,7 @@ module Brainiac
|
|
|
1150
1436
|
end
|
|
1151
1437
|
|
|
1152
1438
|
def self.completions
|
|
1153
|
-
%w[setup config status epics deploy link bot projects set reset webhook]
|
|
1439
|
+
%w[setup config status epics deploy link bot projects set reset scrap webhook]
|
|
1154
1440
|
end
|
|
1155
1441
|
end
|
|
1156
1442
|
end
|