brainiac-basecamp 0.0.21 → 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 +308 -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
|
|
@@ -284,6 +286,21 @@ module Brainiac
|
|
|
284
286
|
puts " Worktree: #{worktree_path}"
|
|
285
287
|
puts ""
|
|
286
288
|
|
|
289
|
+
# Pull latest changes before deploying
|
|
290
|
+
puts "Pulling latest changes..."
|
|
291
|
+
pull_output, pull_status = Open3.capture2e("git", "pull", "--ff-only", chdir: worktree_path)
|
|
292
|
+
if pull_status.success?
|
|
293
|
+
if pull_output.include?("Already up to date")
|
|
294
|
+
puts "Already up to date."
|
|
295
|
+
else
|
|
296
|
+
puts pull_output.lines.first(5).join
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
puts "⚠️ Git pull failed (continuing anyway):"
|
|
300
|
+
puts pull_output.lines.first(3).join
|
|
301
|
+
end
|
|
302
|
+
puts ""
|
|
303
|
+
|
|
287
304
|
# Run deploy
|
|
288
305
|
success = run_deploy(worktree_path, deploy_env)
|
|
289
306
|
|
|
@@ -309,6 +326,189 @@ module Brainiac
|
|
|
309
326
|
end
|
|
310
327
|
|
|
311
328
|
save_epics(epics)
|
|
329
|
+
|
|
330
|
+
# Change into the worktree directory
|
|
331
|
+
puts ""
|
|
332
|
+
puts "Entering worktree..."
|
|
333
|
+
Dir.chdir(worktree_path)
|
|
334
|
+
exec ENV.fetch("SHELL", "/bin/bash")
|
|
335
|
+
end
|
|
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
|
|
312
512
|
end
|
|
313
513
|
|
|
314
514
|
def cmd_link(args)
|
|
@@ -642,6 +842,7 @@ module Brainiac
|
|
|
642
842
|
reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
|
|
643
843
|
reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
|
|
644
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
|
|
645
846
|
webhook sync Ensure webhooks have all required event types
|
|
646
847
|
|
|
647
848
|
Config file: ~/.brainiac/basecamp.json
|
|
@@ -989,6 +1190,112 @@ module Brainiac
|
|
|
989
1190
|
}))
|
|
990
1191
|
end
|
|
991
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
|
+
|
|
992
1299
|
# --- Deploy helpers ---
|
|
993
1300
|
|
|
994
1301
|
def list_epics_for_deploy
|
|
@@ -1129,7 +1436,7 @@ module Brainiac
|
|
|
1129
1436
|
end
|
|
1130
1437
|
|
|
1131
1438
|
def self.completions
|
|
1132
|
-
%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]
|
|
1133
1440
|
end
|
|
1134
1441
|
end
|
|
1135
1442
|
end
|