brainiac 0.0.25 → 0.0.26

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: 0a843b412b04f2b176f13e248bcea6327eb27c2893ed516a566c9f79e3b9dbc4
4
- data.tar.gz: 0bc50a0db4b6e8d5ea67dbf3de47d8481e94a64d886a378730d4d27da5185403
3
+ metadata.gz: 963f3a3319ca2ccff44fc003072a0566d3b76d06a42ea645d81693bb48ea2ab3
4
+ data.tar.gz: 7abb9b38680549aaf3da6068b459f0faf474f5862f7f235f1dabddfcb38d5e55
5
5
  SHA512:
6
- metadata.gz: b3aa491acebfe3b08aef05735fb593c6835b3757b2502f0ca47ad765aca21d9357a0fbb52c8d0a8f0103ad5aff1ff855d2159ff056297dfa4d7ecfeeb2f4387a
7
- data.tar.gz: 54179e3366fb8246ed4221e82e70edc67ac0a24fa05212011624f3b9997f11cbdcc481c49b0141efa4c3c8cfbcbb23ffd9f20286442f2a0fb96557ccc8308c12
6
+ metadata.gz: dae8e213cb21a26f97de1406a5fe0bc1ce7dc5806e3b67079e2c0183b94888439f288243d86dc52fd9831d1ee61294a2f28fe9fe4f88dc03819c1d720a95db9b
7
+ data.tar.gz: 2e7cf53a24fd776e2eb7960c379dc49309d932412f5fe39a8a56dd90a6162dcf3a9924396a7386b67a0288c0034381343effb529e53b8bcf4b4f43b7fc454708
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brainiac (0.0.25)
4
+ brainiac (0.0.26)
5
5
  puma (~> 7.2)
6
6
  rackup (~> 2.3)
7
7
  sinatra (~> 4.1)
@@ -84,7 +84,7 @@ DEPENDENCIES
84
84
  CHECKSUMS
85
85
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
86
86
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
87
- brainiac (0.0.25)
87
+ brainiac (0.0.26)
88
88
  json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
89
89
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
90
90
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
data/bin/brainiac CHANGED
@@ -208,14 +208,7 @@ end
208
208
  def stop_server
209
209
  pid = find_server_pid
210
210
 
211
- unless pid
212
- puts "No running Brainiac server found."
213
- return false
214
- end
215
-
216
- puts "Stopping Brainiac server (PID: #{pid})..."
217
-
218
- # Also stop the monitor daemon
211
+ # Stop the monitor daemon regardless of server PID
219
212
  daemon_pid_file = "/tmp/brainiac-daemon.pid"
220
213
  if File.exist?(daemon_pid_file)
221
214
  daemon_pid = File.read(daemon_pid_file).strip.to_i
@@ -228,40 +221,61 @@ def stop_server
228
221
  File.delete(daemon_pid_file)
229
222
  end
230
223
 
231
- begin
232
- Process.kill("TERM", pid)
233
-
234
- # Wait up to 5 seconds for graceful shutdown
235
- 5.times do
236
- sleep 2
237
- next if process_running?(pid)
224
+ # Kill the known server PID
225
+ if pid
226
+ puts "Stopping Brainiac server (PID: #{pid})..."
227
+ kill_pid(pid)
228
+ end
238
229
 
239
- puts "✓ Server stopped."
240
- FileUtils.rm_f(PID_FILE)
241
- return true
242
- end
230
+ # After killing the known PID, find anything still holding port 4567.
231
+ # This catches orphaned processes from self-restarts, stale PIDs, or child processes.
232
+ lingering = find_port_holders(4567) - [pid].compact
233
+ if lingering.any?
234
+ puts "Cleaning up #{lingering.size} orphaned process(es) on port 4567..."
235
+ lingering.each { |lpid| kill_pid(lpid, quiet: true) }
236
+ end
243
237
 
244
- # Force kill if still running
245
- puts "Server didn't stop gracefully, forcing..."
246
- Process.kill("KILL", pid)
247
- sleep 1
238
+ FileUtils.rm_f(PID_FILE)
248
239
 
249
- FileUtils.rm_f(PID_FILE)
250
- puts "✓ Server stopped (forced)."
240
+ if pid || lingering.any?
241
+ puts "✓ Server stopped."
251
242
  true
252
- rescue Errno::ESRCH
253
- puts " Server already stopped."
254
- FileUtils.rm_f(PID_FILE)
255
- true
256
- rescue Errno::EPERM
257
- puts "Error: Permission denied. Try running with sudo or check process ownership."
258
- false
259
- rescue StandardError => e
260
- puts "Error stopping server: #{e.message}"
243
+ else
244
+ puts "No running Brainiac server found."
261
245
  false
262
246
  end
263
247
  end
264
248
 
249
+ # Kill a single process: TERM first, KILL if it doesn't exit within 5s.
250
+ def kill_pid(pid, quiet: false)
251
+ Process.kill("TERM", pid)
252
+
253
+ 5.times do
254
+ sleep 1
255
+ return true unless process_running?(pid)
256
+ end
257
+
258
+ puts " PID #{pid} didn't stop gracefully, forcing..." unless quiet
259
+ Process.kill("KILL", pid)
260
+ sleep 1
261
+ true
262
+ rescue Errno::ESRCH
263
+ true # Already dead
264
+ rescue Errno::EPERM
265
+ puts "Error: Permission denied to stop PID #{pid}." unless quiet
266
+ false
267
+ end
268
+
269
+ # Find all PIDs listening on a given port via lsof.
270
+ def find_port_holders(port)
271
+ output, status = Open3.capture2("lsof", "-ti", ":#{port}")
272
+ return [] unless status.success?
273
+
274
+ output.strip.split("\n").map(&:to_i).select(&:positive?)
275
+ rescue StandardError
276
+ []
277
+ end
278
+
265
279
  def update_handler_config(name, enabled)
266
280
  brainiac_config_path = File.join(BRAINIAC_DIR, "brainiac.json")
267
281
  config = if File.exist?(brainiac_config_path)
@@ -3372,16 +3386,227 @@ when "plugin"
3372
3386
  puts " brainiac install #{plugin_name} --path #{output_dir}"
3373
3387
  puts " brainiac restart"
3374
3388
 
3389
+ when "list", "ls"
3390
+ # Delegate to the same logic as `brainiac plugins`
3391
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
3392
+ plugins_config = if File.exist?(plugins_file)
3393
+ JSON.parse(File.read(plugins_file))
3394
+ else
3395
+ { "plugins" => [] }
3396
+ end
3397
+
3398
+ plugins = plugins_config["plugins"] || []
3399
+ if plugins.empty?
3400
+ puts "No plugins installed."
3401
+ puts ""
3402
+ puts "Install plugins with: brainiac install <name>"
3403
+ puts " Example: brainiac install whatsapp"
3404
+ puts " Example: brainiac install fizzy --path ~/Code/brainiac-fizzy"
3405
+ else
3406
+ puts "Installed plugins:"
3407
+ plugins.each do |p|
3408
+ entry = p.is_a?(Hash) ? p : { "name" => p.to_s }
3409
+ name = entry["name"]
3410
+ gem_name = entry["gem"] || "brainiac-#{name}"
3411
+ local_path = entry["path"]
3412
+ installed_at = entry["installed_at"] ? " (#{entry["installed_at"][0..9]})" : ""
3413
+
3414
+ if local_path
3415
+ if Dir.exist?(local_path)
3416
+ puts " ✓ #{name} → #{local_path}#{installed_at}"
3417
+ else
3418
+ puts " ✗ #{name} → #{local_path} (path missing)#{installed_at}"
3419
+ end
3420
+ else
3421
+ loadable = begin
3422
+ spec = Gem::Specification.find_by_name(gem_name)
3423
+ "✓ #{name} (#{gem_name} #{spec.version})"
3424
+ rescue Gem::MissingSpecError
3425
+ "✗ #{name} (#{gem_name} — gem not found)"
3426
+ end
3427
+ puts " #{loadable}#{installed_at}"
3428
+ end
3429
+ end
3430
+ end
3431
+
3432
+ when "switch"
3433
+ plugin_name = ARGV.shift
3434
+ branch_name = ARGV.shift
3435
+
3436
+ unless plugin_name && branch_name
3437
+ puts "Usage: brainiac plugin switch <name> <branch>"
3438
+ puts ""
3439
+ puts "Switches a local plugin to a different branch (via git worktrees)."
3440
+ puts "Useful for testing plugin changes from a PR before releasing."
3441
+ puts ""
3442
+ puts "Examples:"
3443
+ puts " brainiac plugin switch fizzy fix-fallback-for-followups-20260813"
3444
+ puts " brainiac plugin switch fizzy main # switch back to main"
3445
+ puts ""
3446
+ puts "After switching, restart the server: brainiac restart"
3447
+ exit 1
3448
+ end
3449
+
3450
+ plugin_name = plugin_name.sub(/^brainiac-/, "")
3451
+
3452
+ plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
3453
+ unless File.exist?(plugins_file)
3454
+ puts "Error: No plugins installed (plugins.json not found)."
3455
+ exit 1
3456
+ end
3457
+
3458
+ plugins_config = JSON.parse(File.read(plugins_file))
3459
+ plugin_idx = (plugins_config["plugins"] || []).index { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == plugin_name }
3460
+
3461
+ unless plugin_idx
3462
+ puts "Error: Plugin '#{plugin_name}' is not installed."
3463
+ exit 1
3464
+ end
3465
+
3466
+ entry = plugins_config["plugins"][plugin_idx]
3467
+ unless entry.is_a?(Hash) && entry["path"]
3468
+ puts "Error: Plugin '#{plugin_name}' is not a local-path plugin."
3469
+ puts " Only plugins installed with --path support branch switching."
3470
+ exit 1
3471
+ end
3472
+
3473
+ current_path = entry["path"]
3474
+ gem_name = "brainiac-#{plugin_name}"
3475
+
3476
+ # Determine the base repo path (strip worktree suffix like --branch-name)
3477
+ base_path = current_path.sub(/--[^\/]+$/, "")
3478
+ unless Dir.exist?(base_path)
3479
+ puts "Error: Base repo not found at #{base_path}"
3480
+ exit 1
3481
+ end
3482
+
3483
+ # Get the default branch of the repo
3484
+ default_branch, _, db_status = Open3.capture3("git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short", chdir: base_path)
3485
+ default_branch = default_branch.strip.sub("origin/", "") if db_status.success?
3486
+ default_branch = "main" unless db_status.success? && !default_branch.empty?
3487
+
3488
+ # If switching to the default branch, just point back to the base path
3489
+ if branch_name == default_branch
3490
+ if current_path == base_path
3491
+ puts "Plugin '#{plugin_name}' is already on #{default_branch}."
3492
+ exit 0
3493
+ end
3494
+
3495
+ plugins_config["plugins"][plugin_idx]["path"] = base_path
3496
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
3497
+ puts "✓ Switched plugin '#{plugin_name}' back to #{default_branch}"
3498
+ puts " Path: #{base_path}"
3499
+ puts " Restart the server to apply: brainiac restart"
3500
+ exit 0
3501
+ end
3502
+
3503
+ # Fetch latest to make sure the branch is available
3504
+ puts "Fetching latest from origin..."
3505
+ _out, _err, fetch_status = Open3.capture3("git", "fetch", "origin", chdir: base_path)
3506
+ unless fetch_status.success?
3507
+ puts "Warning: git fetch failed — continuing with local state."
3508
+ end
3509
+
3510
+ # Check if a worktree already exists for this branch
3511
+ worktree_list, _, wt_status = Open3.capture3("git", "worktree", "list", "--porcelain", chdir: base_path)
3512
+ target_worktree = nil
3513
+
3514
+ if wt_status.success?
3515
+ # Parse porcelain output to find worktrees on the target branch
3516
+ worktrees = worktree_list.split("\n\n").map do |block|
3517
+ wt = {}
3518
+ block.each_line do |line|
3519
+ case line
3520
+ when /^worktree (.+)/
3521
+ wt[:path] = Regexp.last_match(1).strip
3522
+ when /^branch refs\/heads\/(.+)/
3523
+ wt[:branch] = Regexp.last_match(1).strip
3524
+ end
3525
+ end
3526
+ wt
3527
+ end.select { |wt| wt[:path] && wt[:branch] }
3528
+
3529
+ target_worktree = worktrees.find { |wt| wt[:branch] == branch_name }
3530
+ end
3531
+
3532
+ if target_worktree
3533
+ # Worktree already exists — just update the path
3534
+ new_path = target_worktree[:path]
3535
+ if current_path == new_path
3536
+ puts "Plugin '#{plugin_name}' is already on branch '#{branch_name}'."
3537
+ exit 0
3538
+ end
3539
+
3540
+ plugins_config["plugins"][plugin_idx]["path"] = new_path
3541
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
3542
+ puts "✓ Switched plugin '#{plugin_name}' to branch '#{branch_name}'"
3543
+ puts " Path: #{new_path}"
3544
+ puts " Restart the server to apply: brainiac restart"
3545
+ else
3546
+ # Check if branch exists remotely or locally
3547
+ branch_exists = false
3548
+ check_cmd, _, check_status = Open3.capture3("git", "rev-parse", "--verify", "origin/#{branch_name}", chdir: base_path)
3549
+ branch_exists = check_status.success?
3550
+
3551
+ unless branch_exists
3552
+ # Try local branch
3553
+ _, _, local_status = Open3.capture3("git", "rev-parse", "--verify", branch_name, chdir: base_path)
3554
+ branch_exists = local_status.success?
3555
+ end
3556
+
3557
+ unless branch_exists
3558
+ puts "Error: Branch '#{branch_name}' not found (checked local and origin)."
3559
+ puts ""
3560
+ puts "Available remote branches:"
3561
+ branches_out, = Open3.capture3("git", "branch", "-r", "--list", "origin/*", chdir: base_path)
3562
+ branches_out.each_line do |line|
3563
+ branch = line.strip.sub("origin/", "")
3564
+ next if branch.include?("HEAD")
3565
+
3566
+ puts " #{branch}"
3567
+ end
3568
+ exit 1
3569
+ end
3570
+
3571
+ # Create a new worktree
3572
+ worktree_dir = "#{base_path}--#{branch_name}"
3573
+ puts "Creating worktree at #{worktree_dir}..."
3574
+
3575
+ # If the branch exists on origin but not locally, track it
3576
+ _, _, local_check = Open3.capture3("git", "rev-parse", "--verify", branch_name, chdir: base_path)
3577
+ if !local_check.success? && check_status.success?
3578
+ # Remote branch exists but not local — create tracking branch in worktree
3579
+ _, stderr, wt_status = Open3.capture3("git", "worktree", "add", "--track", "-b", branch_name, worktree_dir, "origin/#{branch_name}", chdir: base_path)
3580
+ else
3581
+ # Local branch exists — use it directly
3582
+ _, stderr, wt_status = Open3.capture3("git", "worktree", "add", worktree_dir, branch_name, chdir: base_path)
3583
+ end
3584
+
3585
+ unless wt_status.success?
3586
+ puts "Error: Failed to create worktree:"
3587
+ puts " #{stderr.strip}"
3588
+ exit 1
3589
+ end
3590
+
3591
+ plugins_config["plugins"][plugin_idx]["path"] = worktree_dir
3592
+ File.write(plugins_file, JSON.pretty_generate(plugins_config))
3593
+ puts "✓ Switched plugin '#{plugin_name}' to branch '#{branch_name}'"
3594
+ puts " Worktree: #{worktree_dir}"
3595
+ puts " Restart the server to apply: brainiac restart"
3596
+ end
3597
+
3375
3598
  else
3376
3599
  puts "Usage: brainiac plugin <command>"
3377
3600
  puts ""
3378
3601
  puts "Commands:"
3602
+ puts " list List installed plugins"
3379
3603
  puts " new <name> Generate a new plugin gem skeleton"
3604
+ puts " switch <name> <branch> Switch a local plugin to a different branch"
3380
3605
  puts ""
3381
3606
  puts "Managing installed plugins:"
3382
3607
  puts " brainiac install <name> Install a plugin"
3383
3608
  puts " brainiac uninstall <name> Remove a plugin"
3384
- puts " brainiac plugins List installed plugins"
3609
+ puts " brainiac plugins List installed plugins (alias)"
3385
3610
  end
3386
3611
 
3387
3612
  when "screenshot"
@@ -3613,6 +3838,7 @@ when "help", "--help", "-h", nil
3613
3838
 
3614
3839
  Plugin Commands:
3615
3840
  plugin new <name> Generate a new plugin gem skeleton
3841
+ plugin switch <name> <branch> Switch a local plugin to a branch (for testing)
3616
3842
  install <name> Install a plugin (gem install brainiac-<name>)
3617
3843
  uninstall <name> Remove a plugin
3618
3844
  update [name] Update brainiac + all plugins, or a specific plugin
@@ -63,7 +63,18 @@ _brainiac() {
63
63
  plugin)
64
64
  case $cword in
65
65
  2)
66
- COMPREPLY=($(compgen -W "new" -- "$cur"))
66
+ COMPREPLY=($(compgen -W "new switch list" -- "$cur"))
67
+ ;;
68
+ 3)
69
+ # For "plugin switch", suggest installed plugin names
70
+ if [[ "${COMP_WORDS[2]}" == "switch" && -f "$brainiac_dir/plugins.json" ]]; then
71
+ local installed
72
+ installed=$(ruby -rjson -e '
73
+ config = JSON.parse(File.read(ARGV[0]))
74
+ (config["plugins"] || []).select { |p| p.is_a?(Hash) && p["path"] }.each { |p| puts p["name"] }
75
+ ' "$brainiac_dir/plugins.json" 2>/dev/null)
76
+ COMPREPLY=($(compgen -W "$installed" -- "$cur"))
77
+ fi
67
78
  ;;
68
79
  esac
69
80
  ;;
@@ -4,6 +4,31 @@
4
4
  #
5
5
  # All git-related utilities live here. Handlers call these instead of
6
6
  # reimplementing git worktree/branch logic.
7
+ #
8
+ # NOTE: Methods that need to be accessible from Sinatra route handlers
9
+ # are wrapped in GitHelpers module and registered with Sinatra.
10
+
11
+ # Module containing git helpers that need to be accessible from Sinatra routes.
12
+ # These are registered as Sinatra helpers in receiver.rb so plugins can call them.
13
+ module GitHelpers
14
+ def resolve_base_branch(repo_path:, card_number: nil, project_key: nil)
15
+ results = Brainiac.emit(:resolve_base_branch,
16
+ repo_path: repo_path, card_number: card_number, project_key: project_key)
17
+ custom = results.compact.first
18
+ if custom
19
+ LOG.info "Using custom base branch '#{custom}' (from plugin hook)"
20
+ custom
21
+ else
22
+ "origin/#{get_default_branch(repo_path)}"
23
+ end
24
+ end
25
+
26
+ def resolve_pr_target(repo_path:, card_number: nil, project_key: nil)
27
+ results = Brainiac.emit(:resolve_pr_target,
28
+ repo_path: repo_path, card_number: card_number, project_key: project_key)
29
+ results.compact.first
30
+ end
31
+ end
7
32
 
8
33
  # Debounced repo git fetch — avoids fetching the same repo multiple times within a short window.
9
34
  REPO_LAST_FETCH = {}
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Brainiac
4
4
  # @return [String] the current gem version
5
- VERSION = "0.0.25"
5
+ VERSION = "0.0.26"
6
6
  end
data/receiver.rb CHANGED
@@ -114,8 +114,13 @@ else
114
114
  LOG.info "[Intent] Disabled (enable in brainiac.json → intent.enabled: true)"
115
115
  end
116
116
 
117
- # --- Dashboard authentication ---
117
+ # --- Sinatra helpers ---
118
118
 
119
+ # Register GitHelpers so plugins can call resolve_pr_target, resolve_base_branch
120
+ # from within Sinatra route handlers
121
+ helpers GitHelpers
122
+
123
+ # Dashboard authentication helpers
119
124
  helpers do
120
125
  def authenticate_dashboard!
121
126
  return unless DASHBOARD_TOKEN # No token configured = no auth (local-only mode)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainiac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.25
4
+ version: 0.0.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Davis