brainiac 0.0.6 → 0.0.7

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +382 -11
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +15 -38
  7. data/lib/brainiac/config.rb +40 -11
  8. data/lib/brainiac/handlers/discord/api.rb +196 -0
  9. data/lib/brainiac/handlers/discord/config.rb +134 -0
  10. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  11. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  12. data/lib/brainiac/handlers/discord/message.rb +933 -0
  13. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  14. data/lib/brainiac/handlers/discord.rb +14 -1892
  15. data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
  16. data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
  17. data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
  18. data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
  19. data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
  20. data/lib/brainiac/handlers/fizzy.rb +12 -1290
  21. data/lib/brainiac/handlers/github.rb +149 -212
  22. data/lib/brainiac/handlers/shared/git.rb +203 -0
  23. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  24. data/lib/brainiac/handlers/zoho.rb +79 -76
  25. data/lib/brainiac/helpers.rb +2 -121
  26. data/lib/brainiac/planning.rb +2 -2
  27. data/lib/brainiac/plugins.rb +129 -0
  28. data/lib/brainiac/restart.rb +94 -0
  29. data/lib/brainiac/routes/api.rb +427 -0
  30. data/lib/brainiac/version.rb +1 -1
  31. data/lib/brainiac/zoho_mail_api.rb +2 -1
  32. data/lib/brainiac.rb +7 -0
  33. data/monitor/daemon.rb +4 -27
  34. data/monitor/shared.rb +247 -0
  35. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
  36. data/monitor/waybar/setup.rb +232 -0
  37. data/monitor/waybar/status.rb +51 -0
  38. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  39. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  40. data/monitor/xbar/plugin.rb +155 -0
  41. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  42. data/receiver.rb +91 -450
  43. data/templates/brainiac.json.example +9 -0
  44. data/templates/cli-providers/kiro.json.example +8 -2
  45. data/templates/plugins.json.example +3 -0
  46. metadata +30 -20
  47. data/lib/user_registry.rb +0 -159
  48. data/monitor/menubar.rb +0 -295
  49. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  50. data/monitor/setup-waybar-deployments.rb +0 -96
  51. data/monitor/setup-waybar-module.rb +0 -113
  52. data/monitor/setup-xbar-plugin.rb +0 -35
  53. data/monitor/view-logs.rb +0 -119
  54. data/monitor/waybar-config-updater.rb +0 -56
  55. data/monitor/waybar-deployments.rb +0 -239
  56. data/monitor/waybar.rb +0 -146
  57. data/monitor/xbar.3s.rb +0 -179
  58. /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
  59. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  60. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Brainiac Waybar Status Module
5
+ # Reads from daemon socket and outputs JSON for waybar (agent sessions).
6
+
7
+ require_relative "../shared"
8
+
9
+ AGENTS = load_agent_config.freeze
10
+ INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
11
+
12
+ def escape_pango(str)
13
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
14
+ end
15
+
16
+ def generate_output
17
+ state = fetch_state
18
+
19
+ return { text: "⚠️", tooltip: "Brainiac Error: #{escape_pango(state["error"])}", class: "error" } if state["error"]
20
+
21
+ sessions = state["sessions"] || []
22
+
23
+ return { text: "💤", tooltip: "No active agent sessions", class: "idle" } if sessions.empty?
24
+
25
+ text = sessions.map { |s| AGENTS.dig(s["agent"]&.downcase, :emoji) || DEFAULT_EMOJI }.join(" ")
26
+
27
+ tooltip_lines = sessions.map do |s|
28
+ agent = s["agent"]
29
+ emoji = AGENTS.dig(agent&.downcase, :emoji) || DEFAULT_EMOJI
30
+ elapsed = format_elapsed(s["elapsed_seconds"] || 0)
31
+ context = format_context(s["card_key"])
32
+
33
+ lines = ["#{emoji} #{agent}: #{context} (#{elapsed})"]
34
+
35
+ (s["children"] || []).each do |c|
36
+ cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
37
+ cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
38
+ next if INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
39
+
40
+ lines << " └ #{escape_pango(cmd_short)} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]"
41
+ end
42
+
43
+ lines.join("\n")
44
+ end
45
+
46
+ tooltip_lines << "\n[Click to manage]"
47
+
48
+ { text: text, tooltip: tooltip_lines.join("\n"), class: "working" }
49
+ end
50
+
51
+ puts generate_output.to_json
@@ -1,76 +1,21 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Brainiac Log Viewer (Rofi version)
5
- # Shows a rofi menu to select which agent log to tail
6
- # Non-blocking, safe for waybar on-click
7
-
8
- require "json"
9
- require "net/http"
10
- require "socket"
11
-
12
- SOCKET_PATH = "/tmp/brainiac-monitor.sock"
13
- API_URL = "http://localhost:4567/api/status"
14
- CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
15
-
16
- # Load agent configuration from JSON
17
- def load_agent_config
18
- config = JSON.parse(File.read(CONFIG_PATH))
19
- agents = {}
20
- config["agents"].each do |agent|
21
- agents[agent["name"].downcase] = agent["emoji"]
22
- end
23
- default_emoji = config["default_emoji"] || "❓"
24
- [agents, default_emoji]
25
- rescue StandardError => e
26
- warn "Failed to load waybar.json: #{e.message}"
27
- [{}, "❓"]
28
- end
4
+ # Brainiac Log Viewer (Linux — rofi/fuzzel/wofi/zenity/fzf)
5
+ # Shows a menu to select which agent log to tail, with kill support.
29
6
 
30
- AGENTS, DEFAULT_EMOJI = load_agent_config
31
-
32
- def fetch_state_from_socket
33
- socket = UNIXSocket.new(SOCKET_PATH)
34
- data = socket.read
35
- socket.close
36
- JSON.parse(data)
37
- end
7
+ require_relative "../shared"
38
8
 
39
- def fetch_state_from_api
40
- uri = URI(API_URL)
41
- response = Net::HTTP.get_response(uri)
42
- return nil unless response.is_a?(Net::HTTPSuccess)
9
+ AGENTS = load_agent_config.freeze
10
+ INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
43
11
 
44
- JSON.parse(response.body)
45
- end
12
+ state = fetch_state
46
13
 
47
- def fetch_state
48
- # Prefer socket (daemon mode) — faster, no HTTP overhead
49
- fetch_state_from_socket
50
- rescue Errno::ENOENT, Errno::ECONNREFUSED
51
- # Daemon not running — fall back to direct API call
52
- data = fetch_state_from_api
53
- unless data
54
- system("notify-send", "Brainiac", "Server not reachable")
55
- exit 1
56
- end
57
- data
58
- rescue StandardError => e
59
- system("notify-send", "Brainiac Error", e.message)
14
+ if state["error"]
15
+ system("notify-send", "Brainiac Error", state["error"])
60
16
  exit 1
61
17
  end
62
18
 
63
- def format_elapsed(seconds)
64
- return "#{seconds}s" if seconds < 60
65
-
66
- minutes = seconds / 60
67
- return "#{minutes}m" if minutes < 60
68
-
69
- hours = minutes / 60
70
- "#{hours}h"
71
- end
72
-
73
- state = fetch_state
74
19
  sessions = state["sessions"] || []
75
20
 
76
21
  if sessions.empty?
@@ -78,24 +23,15 @@ if sessions.empty?
78
23
  exit 0
79
24
  end
80
25
 
81
- INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
82
-
83
26
  # Build menu entries: sessions + their child processes
84
27
  entries = []
85
28
  sessions.each do |s|
86
29
  agent = s["agent"]
87
30
  elapsed = format_elapsed(s["elapsed_seconds"])
88
- card_key = s["card_key"]
89
- context = if card_key.start_with?("discord-")
90
- "Discord chat"
91
- elsif card_key.start_with?("card-")
92
- card_key.split("-")[1]
93
- else
94
- card_key
95
- end
96
- emoji = AGENTS[agent.downcase] || DEFAULT_EMOJI
31
+ context = format_context(s["card_key"])
32
+ emoji = AGENTS.dig(agent&.downcase, :emoji) || DEFAULT_EMOJI
97
33
  entries << { display: "#{emoji} #{agent}: #{context} (#{elapsed})", type: :log, log: s["log_file"] }
98
- entries << { display: " ⛔ Kill session: #{agent} (#{context})", type: :kill_session, card_key: card_key, agent: agent }
34
+ entries << { display: " ⛔ Kill session: #{agent} (#{context})", type: :kill_session, card_key: s["card_key"], agent: agent }
99
35
 
100
36
  (s["children"] || []).each do |c|
101
37
  cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
@@ -116,7 +52,7 @@ if entries.size == 1 && entries[0][:type] == :log
116
52
  end
117
53
 
118
54
  def find_launcher
119
- %w[rofi fuzzel wofi zenity].find { |cmd| system("which #{cmd} > /dev/null 2>&1") }
55
+ %w[rofi fuzzel wofi zenity fzf].find { |cmd| system("which #{cmd} > /dev/null 2>&1") }
120
56
  end
121
57
 
122
58
  def run_menu(launcher, entries)
@@ -147,12 +83,14 @@ def run_menu(launcher, entries)
147
83
  io.close_write
148
84
  io.read.strip
149
85
  end
86
+ when "fzf"
87
+ `echo "#{menu_text}" | fzf --prompt="Agent Sessions: "`.strip
150
88
  end
151
89
  end
152
90
 
153
91
  launcher = find_launcher
154
92
  unless launcher
155
- system("notify-send", "Brainiac", "No menu launcher found (install rofi, fuzzel, wofi, or zenity)")
93
+ system("notify-send", "Brainiac", "No menu launcher found (install rofi, fuzzel, wofi, zenity, or fzf)")
156
94
  exit 1
157
95
  end
158
96
 
@@ -165,30 +103,25 @@ unless selected_line.to_s.empty?
165
103
  when :log
166
104
  spawn("alacritty", "-e", "tail", "-f", selected[:log]) if selected[:log]
167
105
  when :kill_session
168
- card_key = selected[:card_key]
169
- uri = URI("http://localhost:4567/api/sessions/kill/#{card_key}")
106
+ uri = URI("#{SERVER_URL}/api/sessions/kill/#{selected[:card_key]}")
170
107
  response = Net::HTTP.post(uri, "", { "Content-Type" => "application/json" })
171
- if response.is_a?(Net::HTTPSuccess)
172
- system("notify-send", "Brainiac", "Killed session: #{selected[:agent]}")
173
- else
174
- system("notify-send", "Brainiac", "Failed to kill session: #{selected[:agent]}")
175
- end
108
+ msg = response.is_a?(Net::HTTPSuccess) ? "Killed session: #{selected[:agent]}" : "Failed to kill session: #{selected[:agent]}"
109
+ system("notify-send", "Brainiac", msg)
176
110
  when :kill_child
177
- pid = selected[:pid]
178
111
  begin
179
- Process.kill("TERM", pid)
112
+ Process.kill("TERM", selected[:pid])
180
113
  rescue StandardError
181
114
  nil
182
115
  end
183
116
  Thread.new do
184
117
  sleep 3
185
118
  begin
186
- Process.kill("KILL", pid)
119
+ Process.kill("KILL", selected[:pid])
187
120
  rescue StandardError
188
121
  nil
189
122
  end
190
123
  end
191
- system("notify-send", "Brainiac", "Killed #{selected[:cmd]} (PID #{pid})")
124
+ system("notify-send", "Brainiac", "Killed #{selected[:cmd]} (PID #{selected[:pid]})")
192
125
  end
193
126
  end
194
127
  end
@@ -59,8 +59,9 @@ unless worktree
59
59
  end
60
60
 
61
61
  unless worktree
62
- system("osascript", "-e",
63
- "display dialog \"No worktree found for card ##{card_number}\" buttons {\"OK\"} default button \"OK\" with title \"Deploy Failed\" with icon stop")
62
+ dialog = "display dialog \"No worktree found for card ##{card_number}\" " \
63
+ "buttons {\"OK\"} default button \"OK\" with title \"Deploy Failed\" with icon stop"
64
+ system("osascript", "-e", dialog)
64
65
  exit
65
66
  end
66
67
 
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Brainiac macOS Menu Bar Plugin (xbar/SwiftBar)
5
+ # Outputs xbar-format text: sessions, recent, deployments.
6
+
7
+ require "shellwords"
8
+ require "time"
9
+ require_relative "../shared"
10
+
11
+ SELF_PATH = File.realpath(__FILE__)
12
+ AGENTS = load_agent_config.freeze
13
+ CONFIG = load_monitor_config.freeze
14
+ FIZZY_ACCOUNT_ID = CONFIG["fizzy_account_id"]
15
+ DISCORD_GUILD_ID = CONFIG["discord_guild_id"]
16
+
17
+ LOG_VIEWER_PATH = File.join(File.dirname(SELF_PATH), "view_logs.rb")
18
+ DEPLOY_SCRIPT_PATH = File.join(File.dirname(SELF_PATH), "deploy_env.rb")
19
+ OPEN_SCRIPT = File.join(File.dirname(SELF_PATH), "open_action.sh")
20
+
21
+ def log_action(log_file)
22
+ return "" unless log_file
23
+
24
+ " | shell=#{LOG_VIEWER_PATH} param1=#{log_file} terminal=false refresh=false"
25
+ end
26
+
27
+ def full_log_action(log_file)
28
+ return "" unless log_file
29
+
30
+ " | shell=#{OPEN_SCRIPT} param1=#{log_file.shellescape} terminal=false refresh=false"
31
+ end
32
+
33
+ def prompt_url(card_key)
34
+ return nil unless card_key
35
+
36
+ if card_key.start_with?("card-")
37
+ card_num = card_key.split("-")[1]
38
+ "https://app.fizzy.do/#{FIZZY_ACCOUNT_ID}/cards/#{card_num}" if FIZZY_ACCOUNT_ID && card_num
39
+ elsif card_key.start_with?("discord-") && DISCORD_GUILD_ID
40
+ parts = card_key.split("-")
41
+ channel_id = parts[-2]
42
+ message_id = parts[-1]
43
+ "https://discord.com/channels/#{DISCORD_GUILD_ID}/#{channel_id}/#{message_id}" if channel_id && message_id
44
+ end
45
+ end
46
+
47
+ def prompt_action(card_key)
48
+ url = prompt_url(card_key)
49
+ url ? " | shell=#{OPEN_SCRIPT} param1=#{url} terminal=false refresh=false" : ""
50
+ end
51
+
52
+ def worktree_path(log_file, card_key)
53
+ return nil unless log_file && card_key&.start_with?("card-")
54
+
55
+ dir = File.dirname(log_file, 2)
56
+ dir if File.directory?(dir) && dir != "/"
57
+ end
58
+
59
+ def worktree_action(log_file, card_key)
60
+ path = worktree_path(log_file, card_key)
61
+ path ? " | shell=#{OPEN_SCRIPT} param1=#{path.shellescape} terminal=false refresh=false" : ""
62
+ end
63
+
64
+ def render_session_submenu(session)
65
+ lines = tail_log(session["log_file"]).map { |line| "-- #{format_log_line(line)} | font=#{LOG_FONT} size=#{LOG_SIZE}" }
66
+ lines << "-- ---" if session["log_file"]
67
+ lines << "-- Tail Log#{log_action(session["log_file"])}" if session["log_file"]
68
+ lines << "-- View Full Log#{full_log_action(session["log_file"])}" if session["log_file"]
69
+ lines << "-- Open Prompt#{prompt_action(session["card_key"])}" unless prompt_url(session["card_key"]).nil?
70
+ wt = worktree_path(session["log_file"], session["card_key"])
71
+ lines << "-- Open Worktree#{worktree_action(session["log_file"], session["card_key"])}" if wt
72
+ lines
73
+ end
74
+
75
+ def generate_output
76
+ state = fetch_state
77
+ deployments = fetch_deployments
78
+
79
+ return ["⚠️", "---", state["error"], "---", "Refresh | refresh=true"].join("\n") if state["error"] && !deployments
80
+
81
+ sessions = state["sessions"] || []
82
+ recent = state["recent"] || []
83
+ lines = []
84
+
85
+ # Title line
86
+ parts = []
87
+ parts << sessions.map { |s| AGENTS.dig(s["agent"]&.downcase, :emoji) || DEFAULT_EMOJI }.join(" ") if sessions.any?
88
+ parts << deployments.map { |d| deploy_dot_emoji(d) }.join if deployments&.any?
89
+ lines << (parts.any? ? parts.join(" ") : "💤")
90
+ lines << "---"
91
+
92
+ # Active sessions
93
+ if sessions.any?
94
+ lines << "Active | size=12"
95
+ sessions.each do |s|
96
+ agent_key = (s["agent"] || "").downcase
97
+ emoji = AGENTS.dig(agent_key, :emoji) || DEFAULT_EMOJI
98
+ color = AGENTS.dig(agent_key, :color)
99
+ color_str = color ? " color=#{hex_color(color)}" : ""
100
+ context = format_context(s["card_key"])
101
+ elapsed = format_elapsed(s["elapsed_seconds"] || 0)
102
+ lines << "#{emoji} #{s["agent"]}: #{context} (#{elapsed}) |#{color_str}"
103
+ lines.concat(render_session_submenu(s))
104
+ end
105
+ else
106
+ lines << "No active sessions | size=12"
107
+ end
108
+
109
+ # Recent
110
+ if recent.any?
111
+ lines << "---"
112
+ lines << "Recent | size=12"
113
+ recent.each do |s|
114
+ emoji = AGENTS.dig((s["agent"] || "").downcase, :emoji) || DEFAULT_EMOJI
115
+ context = format_context(s["card_key"])
116
+ ago = time_ago(s["finished_at"]) || "?"
117
+ lines << "#{emoji} #{s["agent"]}: #{context} — #{ago}"
118
+ lines.concat(render_session_submenu(s))
119
+ end
120
+ end
121
+
122
+ # Deployments
123
+ if deployments&.any?
124
+ lines << "---"
125
+ lines << "Deployments | size=12"
126
+ deployments.each do |d|
127
+ label = d["label"] || d["env"]
128
+ env = d["env"]
129
+ dot = deploy_dot_emoji(d)
130
+ if d["status"] == "occupied"
131
+ card = d["card_number"] ? "##{d["card_number"]}" : d["branch"] || "unknown"
132
+ ago = time_ago(d["deployed_at"])
133
+ status_label = case d["last_deploy_status"]
134
+ when "deploying" then " — deploying…"
135
+ when "failed" then " — FAILED"
136
+ else ""
137
+ end
138
+ line = "#{dot} #{label}: #{card}#{status_label}#{" (#{ago})" if ago}"
139
+ lines << (d["url"] ? "#{line} | href=#{d["url"]}" : line)
140
+ else
141
+ ago = time_ago(d["cleared_at"])
142
+ last = d["last_card"] ? " (was ##{d["last_card"]})" : ""
143
+ lines << "#{dot} #{label}: Available#{" #{ago}" if ago}#{last}"
144
+ end
145
+ lines << "-- Deploy to #{label} | shell=#{DEPLOY_SCRIPT_PATH} param1=#{env} terminal=false refresh=true"
146
+ lines << "-- Open #{label} | shell=#{OPEN_SCRIPT} param1=#{d["url"]} terminal=false refresh=false" if d["status"] == "occupied" && d["url"]
147
+ end
148
+ end
149
+
150
+ lines << "---"
151
+ lines << "Refresh | refresh=true"
152
+ lines.join("\n")
153
+ end
154
+
155
+ puts generate_output
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # One-time setup script to install Brainiac menubar plugin into xbar or SwiftBar
5
- # Run this once — the plugin will then auto-refresh on its configured interval
4
+ # One-time setup: installs Brainiac menu bar plugin into xbar or SwiftBar.
5
+ #
6
+ # Detects which app is installed and symlinks the plugin.
7
+ # Run this once — the plugin auto-refreshes on its configured interval.
8
+
9
+ require "fileutils"
6
10
 
7
11
  PLUGIN_APPS = [
8
12
  {
@@ -18,7 +22,7 @@ PLUGIN_APPS = [
18
22
  ].freeze
19
23
 
20
24
  SYMLINK_NAME = "brainiac.2s.rb"
21
- SOURCE_PATH = File.join(File.dirname(File.expand_path(__FILE__)), "menubar.rb")
25
+ SOURCE_PATH = File.join(File.dirname(File.expand_path(__FILE__)), "plugin.rb")
22
26
 
23
27
  def detect_plugin_app
24
28
  PLUGIN_APPS.each do |app|
@@ -27,33 +31,8 @@ def detect_plugin_app
27
31
  nil
28
32
  end
29
33
 
30
- def install_plugin(plugin_dir, source_path)
31
- FileUtils.mkdir_p(plugin_dir)
32
- link_path = File.join(plugin_dir, SYMLINK_NAME)
33
-
34
- # Remove existing symlink/file if present
35
- File.delete(link_path) if File.exist?(link_path) || File.symlink?(link_path)
36
-
37
- File.symlink(source_path, link_path)
38
- rescue StandardError => e
39
- warn "✗ Failed to create symlink: #{e.message}"
40
- warn " Source: #{source_path}"
41
- warn " Target: #{link_path}"
42
- exit 1
43
- end
44
-
45
- def verify_executable!(path) # rubocop:disable Naming/PredicateMethod
46
- unless File.executable?(path)
47
- File.chmod(0o755, path)
48
- warn " Fixed executable permission on #{path}"
49
- end
50
- File.executable?(path)
51
- end
52
-
53
34
  # --- Main ---
54
35
 
55
- require "fileutils"
56
-
57
36
  app = detect_plugin_app
58
37
 
59
38
  unless app
@@ -69,10 +48,15 @@ unless app
69
48
  end
70
49
 
71
50
  puts "Detected #{app[:name]}"
72
- install_plugin(app[:plugin_dir], SOURCE_PATH)
73
- verify_executable!(SOURCE_PATH)
74
51
 
52
+ FileUtils.mkdir_p(app[:plugin_dir])
75
53
  link_path = File.join(app[:plugin_dir], SYMLINK_NAME)
54
+
55
+ File.delete(link_path) if File.exist?(link_path) || File.symlink?(link_path)
56
+ File.symlink(SOURCE_PATH, link_path)
57
+ File.chmod(0o755, SOURCE_PATH)
58
+
76
59
  puts "✓ Installed Brainiac plugin into #{app[:name]}"
77
60
  puts " Symlink: #{link_path} → #{SOURCE_PATH}"
78
61
  puts " Refresh interval: 2s"
62
+ puts " Restart #{app[:name]} to activate"