dotfiles-tui 0.0.5
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 +7 -0
- data/.env.example +4 -0
- data/.gitattributes +1 -0
- data/.github/workflows/publish-gem.yml +36 -0
- data/.gitignore +316 -0
- data/.shellcheckrc +1 -0
- data/.stow-local-ignore +23 -0
- data/.vscode/settings.json +2 -0
- data/Configs/aerospace/.config/aerospace/aerospace.toml +265 -0
- data/Configs/alacritty/.config/alacritty/alacritty.toml +31 -0
- data/Configs/conda/.condarc +1 -0
- data/Configs/continue/.continue/.continueignore +0 -0
- data/Configs/continue/.continue/.continuerc.json +3 -0
- data/Configs/continue/.continue/config.json +48 -0
- data/Configs/docker/.docker/completion/completion.zsh +3 -0
- data/Configs/editorconfig/.editorconfig +16 -0
- data/Configs/fzf/.fzf/completion/completion.zsh +8 -0
- data/Configs/git/.config/gitconfig/core +83 -0
- data/Configs/git/.config/gitconfig/home +20 -0
- data/Configs/git/.config/gitconfig/template.txt +7 -0
- data/Configs/git/.config/gitconfig/work +23 -0
- data/Configs/git/.gitconfig +25 -0
- data/Configs/gpg/.gnupg/.#lk0x000000015060b4e0.macBook.pro.24088 +2 -0
- data/Configs/gpg/.gnupg/gpg-agent.conf +3 -0
- data/Configs/gpg/.gnupg/openpgp-revocs.d/DE999E2ACEAE3E8EAD660459688CB4D444FB1024.rev +28 -0
- data/Configs/gpg/.gnupg/private-keys-v1.d/0FF407C984AC03CCC60D924C5B315977DF45D5D0.key +7 -0
- data/Configs/gpg/.gnupg/private-keys-v1.d/2B711D7C4A8BE25322C9966B73E6842F7E64CD38.key +7 -0
- data/Configs/gpg/.gnupg/pubring.kbx +0 -0
- data/Configs/gpg/.gnupg/trustdb.gpg +0 -0
- data/Configs/kitty/.config/kitty/current-theme.conf +80 -0
- data/Configs/kitty/.config/kitty/kitty.conf +93 -0
- data/Configs/nvim/.config/nvim/.stylua.toml +6 -0
- data/Configs/nvim/.config/nvim/README.md +9 -0
- data/Configs/nvim/.config/nvim/init.lua +37 -0
- data/Configs/nvim/.config/nvim/lazy-lock.json +29 -0
- data/Configs/nvim/.config/nvim/lua/chadrc.lua +17 -0
- data/Configs/nvim/.config/nvim/lua/configs/conform.lua +15 -0
- data/Configs/nvim/.config/nvim/lua/configs/lazy.lua +47 -0
- data/Configs/nvim/.config/nvim/lua/configs/lspconfig.lua +24 -0
- data/Configs/nvim/.config/nvim/lua/configs/null_ls.lua +27 -0
- data/Configs/nvim/.config/nvim/lua/mappings.lua +10 -0
- data/Configs/nvim/.config/nvim/lua/options.lua +6 -0
- data/Configs/nvim/.config/nvim/lua/plugins/init.lua +25 -0
- data/Configs/oh-my-posh/.config/oh-my-posh/theme/config.json +71 -0
- data/Configs/pip/.config/pip/pip.conf +3 -0
- data/Configs/python/.pythonrc +25 -0
- data/Configs/starship/.config/starship/starship.toml +134 -0
- data/Configs/starship/.config/starship/themes/frappe.toml +27 -0
- data/Configs/starship/.config/starship/themes/latte.toml +27 -0
- data/Configs/starship/.config/starship/themes/macchiato.toml +27 -0
- data/Configs/starship/.config/starship/themes/mocha.toml +27 -0
- data/Configs/tmux/.tmux.conf +21 -0
- data/Configs/wezterm/.config/wezterm/core/colors.lua +10 -0
- data/Configs/wezterm/.config/wezterm/core/font.lua +5 -0
- data/Configs/wezterm/.config/wezterm/core/helper.lua +22 -0
- data/Configs/wezterm/.config/wezterm/core/init.lua +20 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/init.lua +16 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/macos.lua +106 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/windows.lua +99 -0
- data/Configs/wezterm/.config/wezterm/core/launch.lua +41 -0
- data/Configs/wezterm/.config/wezterm/core/maximized.lua +5 -0
- data/Configs/wezterm/.config/wezterm/core/mousebindings.lua +23 -0
- data/Configs/wezterm/.config/wezterm/core/tab_title.lua +26 -0
- data/Configs/wezterm/.config/wezterm/core/window.lua +8 -0
- data/Configs/wezterm/.config/wezterm/wezterm.lua +83 -0
- data/Configs/zellij/.config/zellij/config.kdl +410 -0
- data/Configs/zellij/.config/zellij/layouts/default.kdl +159 -0
- data/Configs/zellij/.config/zellij/plugins/room.wasm +0 -0
- data/Configs/zellij/.config/zellij/plugins/zjstatus.wasm +0 -0
- data/Configs/zsh/.hushlogin +0 -0
- data/Configs/zsh/.localrc +42 -0
- data/Configs/zsh/.profile +3 -0
- data/Configs/zsh/.shellcheckrc +1 -0
- data/Configs/zsh/.zprofile +3 -0
- data/Configs/zsh/.zsh/autoload/backup_restore +84 -0
- data/Configs/zsh/.zsh/autoload/cat +8 -0
- data/Configs/zsh/.zsh/autoload/change_wallpaper +1 -0
- data/Configs/zsh/.zsh/autoload/clean_dstore +6 -0
- data/Configs/zsh/.zsh/autoload/clean_pycache +6 -0
- data/Configs/zsh/.zsh/autoload/convert_mkv_to_mp4 +14 -0
- data/Configs/zsh/.zsh/autoload/create_macos_installer +26 -0
- data/Configs/zsh/.zsh/autoload/download +71 -0
- data/Configs/zsh/.zsh/autoload/fail +2 -0
- data/Configs/zsh/.zsh/autoload/flush_dns +6 -0
- data/Configs/zsh/.zsh/autoload/info +1 -0
- data/Configs/zsh/.zsh/autoload/ls +8 -0
- data/Configs/zsh/.zsh/autoload/reset_beyond_compare +15 -0
- data/Configs/zsh/.zsh/autoload/reset_final_cut_pro +5 -0
- data/Configs/zsh/.zsh/autoload/reset_launch_pad +3 -0
- data/Configs/zsh/.zsh/autoload/reset_open_list +3 -0
- data/Configs/zsh/.zsh/autoload/speedup_terminal +6 -0
- data/Configs/zsh/.zsh/autoload/start_aria2_server +1 -0
- data/Configs/zsh/.zsh/autoload/success +1 -0
- data/Configs/zsh/.zsh/autoload/update_system +19 -0
- data/Configs/zsh/.zsh/autoload/user +1 -0
- data/Configs/zsh/.zsh/autoload/vim +9 -0
- data/Configs/zsh/.zsh/autoload/warn +1 -0
- data/Configs/zsh/.zsh/completion/aliases.zsh +3 -0
- data/Configs/zsh/.zsh/completion/completion.zsh +33 -0
- data/Configs/zsh/.zsh/completion/config.zsh +118 -0
- data/Configs/zsh/.zsh/completion/exports.zsh +108 -0
- data/Configs/zsh/.zsh/completion/fpath.zsh +8 -0
- data/Configs/zsh/.zsh_history +283 -0
- data/Configs/zsh/.zshenv +2 -0
- data/Configs/zsh/.zshrc +54 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +87 -0
- data/Hooks/adguard/pre.rb +7 -0
- data/Hooks/aerospace/pre.rb +8 -0
- data/Hooks/aide/post.rb +7 -0
- data/Hooks/aide/pre.rb +7 -0
- data/Hooks/alacritty/pre.rb +7 -0
- data/Hooks/alfred/pre.rb +7 -0
- data/Hooks/atuin/post.rb +27 -0
- data/Hooks/atuin/pre.rb +7 -0
- data/Hooks/bartender/pre.rb +7 -0
- data/Hooks/bun/pre.rb +7 -0
- data/Hooks/carapace/pre.rb +7 -0
- data/Hooks/cargo/pre.rb +7 -0
- data/Hooks/choosy/pre.rb +7 -0
- data/Hooks/controld/pre.rb +7 -0
- data/Hooks/core/common.rb +36 -0
- data/Hooks/core/final.rb +23 -0
- data/Hooks/core/lib/configurator.rb +593 -0
- data/Hooks/core/lib/display.rb +120 -0
- data/Hooks/core/lib/hook_config.rb +96 -0
- data/Hooks/core/lib/hook_context.rb +124 -0
- data/Hooks/core/lib/hooks.rb +45 -0
- data/Hooks/core/lib/logger.rb +23 -0
- data/Hooks/core/lib/menu.rb +70 -0
- data/Hooks/core/lib/spinner.rb +50 -0
- data/Hooks/core/lib/system.rb +78 -0
- data/Hooks/core/lib/tweaks.rb +59 -0
- data/Hooks/core/library.rb +3 -0
- data/Hooks/core/pre.rb +17 -0
- data/Hooks/ctrld/pre.rb +7 -0
- data/Hooks/deskpad/pre.rb +7 -0
- data/Hooks/fonts/pre.rb +60 -0
- data/Hooks/ghostty/pre.rb +10 -0
- data/Hooks/git/post.rb +20 -0
- data/Hooks/git/pre.rb +7 -0
- data/Hooks/gpg/post.rb +16 -0
- data/Hooks/gpg/pre.rb +7 -0
- data/Hooks/ice-hidemenubar/pre.rb +7 -0
- data/Hooks/iterm/IC Green PPL.itermcolors +344 -0
- data/Hooks/iterm/chalkboard.webp +0 -0
- data/Hooks/iterm/com.googlecode.iterm2.plist +2371 -0
- data/Hooks/iterm/post.rb +7 -0
- data/Hooks/iterm/pre.rb +7 -0
- data/Hooks/jujutsu/post.rb +13 -0
- data/Hooks/jujutsu/pre.rb +7 -0
- data/Hooks/keka/pre.rb +7 -0
- data/Hooks/kitty/pre.rb +7 -0
- data/Hooks/lazyvim/pre.rb +7 -0
- data/Hooks/lima/pre.rb +7 -0
- data/Hooks/little-snitch/pre.rb +7 -0
- data/Hooks/microsoft-edge/pre.rb +7 -0
- data/Hooks/mos/pre.rb +7 -0
- data/Hooks/nvchad/pre.rb +7 -0
- data/Hooks/oh-my-posh/config.json +58 -0
- data/Hooks/oh-my-posh/post.rb +7 -0
- data/Hooks/oh-my-posh/pre.rb +7 -0
- data/Hooks/pearcleaner/pre.rb +7 -0
- data/Hooks/pycharm/pre.rb +7 -0
- data/Hooks/raindropio/pre.rb +7 -0
- data/Hooks/rectangle/RectangleConfig.json +258 -0
- data/Hooks/rectangle/com.knollsoft.Rectangle.plist +0 -0
- data/Hooks/rectangle/post.rb +22 -0
- data/Hooks/rectangle/pre.rb +7 -0
- data/Hooks/slack/pre.rb +7 -0
- data/Hooks/soundsource/pre.rb +7 -0
- data/Hooks/ssh/post.rb +8 -0
- data/Hooks/starship/pre.rb +7 -0
- data/Hooks/sublime_text/post.rb +25 -0
- data/Hooks/sublime_text/pre.rb +7 -0
- data/Hooks/swiftformat-for-xcode/pre.rb +7 -0
- data/Hooks/syncthing/pre.rb +7 -0
- data/Hooks/synology/pre.rb +7 -0
- data/Hooks/tailscale/pre.rb +7 -0
- data/Hooks/tmux/post.rb +7 -0
- data/Hooks/tmux/pre.rb +7 -0
- data/Hooks/topnotch/pre.rb +7 -0
- data/Hooks/transmission/pre.rb +7 -0
- data/Hooks/vscode/extensions.txt +16 -0
- data/Hooks/vscode/keybindings.json +26 -0
- data/Hooks/vscode/post.rb +29 -0
- data/Hooks/vscode/pre.rb +9 -0
- data/Hooks/vscode/settings.json +139 -0
- data/Hooks/vscode/style.css +29 -0
- data/Hooks/wezterm/pre.rb +7 -0
- data/Hooks/wins/pre.rb +7 -0
- data/Hooks/zed/pre.rb +7 -0
- data/Hooks/zellij/post.rb +7 -0
- data/Hooks/zellij/pre.rb +7 -0
- data/Hooks/zoxide/pre.rb +7 -0
- data/Hooks/zsh/pre.rb +19 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/bin/dotfiles-tui +6 -0
- data/bootstrap.rb +186 -0
- data/dotfiles-tui.gemspec +47 -0
- data/lib/dotfiles_tui/version.rb +5 -0
- data/lib/dotfiles_tui.rb +15 -0
- metadata +275 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
require_relative '../common'
|
|
7
|
+
require_relative 'hooks'
|
|
8
|
+
require_relative 'system'
|
|
9
|
+
|
|
10
|
+
module Bootstrap
|
|
11
|
+
class Configurator
|
|
12
|
+
FORMULAE = %w[
|
|
13
|
+
aria2 bat bash bottom cmake coreutils fd ripgrep
|
|
14
|
+
fzf gcc git jj git-lfs gnutls go gnupg
|
|
15
|
+
pinentry-mac gpg2 mas mcfly neovim node
|
|
16
|
+
oh-my-posh openjdk openssh openssl procs lsd
|
|
17
|
+
readline rsync ruby shellcheck shfmt
|
|
18
|
+
ssh-copy-id tlrc watch wget zsh zsh-completions
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
FORMULAE_DISABLED = %w[rust].freeze
|
|
22
|
+
|
|
23
|
+
MAS_APPS = [
|
|
24
|
+
{ name: '1Blocker', id: 1_365_531_024, enabled: true },
|
|
25
|
+
{ name: 'Bitwarden', id: 1_352_778_147, enabled: true },
|
|
26
|
+
{ name: 'Auto HD FPS for YouTube', id: 1_546_729_687, enabled: true },
|
|
27
|
+
{ name: 'Numbers', id: 409_203_825, enabled: true },
|
|
28
|
+
{ name: 'Pages', id: 409_201_541, enabled: true },
|
|
29
|
+
{ name: 'The Unarchiver', id: 425_424_353, enabled: true },
|
|
30
|
+
{ name: 'Save to Raindrop.io', id: 1_549_370_672, enabled: true },
|
|
31
|
+
{ name: 'Amphetamine', id: 937_984_704, enabled: true },
|
|
32
|
+
{ name: 'Velja', id: 1_607_635_845, enabled: true },
|
|
33
|
+
{ name: 'CotEditor', id: 1_024_640_650, enabled: true },
|
|
34
|
+
{ name: 'Super Agent for Safari', id: 1_568_262_835, enabled: true },
|
|
35
|
+
{ name: 'Sequel Ace', id: 1_518_036_000, enabled: false },
|
|
36
|
+
{ name: 'WireGuard', id: 1_451_685_025, enabled: false },
|
|
37
|
+
{ name: 'Poolsuite FM', id: 1_514_817_810, enabled: false },
|
|
38
|
+
{ name: 'AdGuard for Safari', id: 1_440_147_259, enabled: false },
|
|
39
|
+
{ name: 'Xcode', id: 497_799_835, enabled: true }
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
PRIORITY_HOOKS = %w[core cargo mos zsh starship lunarvim].freeze
|
|
43
|
+
|
|
44
|
+
attr_reader :script_dir, :hooks_dir, :configs_dir, :core_dir, :dry_run
|
|
45
|
+
attr_accessor :quiet
|
|
46
|
+
|
|
47
|
+
def initialize(dry_run: false)
|
|
48
|
+
@script_dir = ENV.fetch('SCRIPT_DIR', File.expand_path('../../..', __dir__))
|
|
49
|
+
@hooks_dir = ENV.fetch('HOOKS_DIR', File.join(@script_dir, 'Hooks'))
|
|
50
|
+
@configs_dir = ENV.fetch('CONFIGS_DIR', File.join(@script_dir, 'Configs'))
|
|
51
|
+
@core_dir = ENV.fetch('CORE_DIR', File.join(@hooks_dir, 'core'))
|
|
52
|
+
@prereq_marker = File.join(Dir.tmpdir, 'has_pre_requisite_ran')
|
|
53
|
+
@user = ENV.fetch('USER', nil)
|
|
54
|
+
@dry_run = dry_run
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pre_requisite
|
|
58
|
+
if prerequisites_ran?
|
|
59
|
+
Bootstrap::Display.info('Pre-requisite check completed.')
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
ensure_sudo!
|
|
64
|
+
add_to_sudoers
|
|
65
|
+
install_cargo_toolchain
|
|
66
|
+
ensure_homebrew_installed
|
|
67
|
+
ensure_stow_installed
|
|
68
|
+
setup_env_file
|
|
69
|
+
run_priority_hooks
|
|
70
|
+
mark_prerequisites_done
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def install_brew_formulas
|
|
74
|
+
apply_brew_environment
|
|
75
|
+
Bootstrap::Display.header('Installing formulae')
|
|
76
|
+
|
|
77
|
+
Bootstrap::Spinner.spin('Installing formulae...') do |spinner|
|
|
78
|
+
FORMULAE.each do |formula|
|
|
79
|
+
install_formula(formula, spinner)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
Bootstrap::Display.persist('All formulae installed', :success)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def install_brew_casks
|
|
86
|
+
apply_brew_environment
|
|
87
|
+
Bootstrap::Display.header('Installing casks')
|
|
88
|
+
|
|
89
|
+
Bootstrap::Spinner.spin('Installing casks...') do |spinner|
|
|
90
|
+
Dir.children(@hooks_dir).sort.each do |dir_name|
|
|
91
|
+
next if dir_name.start_with?('.')
|
|
92
|
+
next if PRIORITY_HOOKS.include?(dir_name)
|
|
93
|
+
|
|
94
|
+
install_hook(dir_name, spinner)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
Bootstrap::Display.persist('All casks installed', :success)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def install_mas_apps
|
|
101
|
+
mas = find_binary('mas', ['/opt/homebrew/bin/mas'])
|
|
102
|
+
raise 'mas CLI is not installed. Exiting.' if mas.nil?
|
|
103
|
+
|
|
104
|
+
Bootstrap::Display.header('Installing Mac App Store applications')
|
|
105
|
+
Bootstrap::Display.info('Please login to the App Store if prompted.')
|
|
106
|
+
|
|
107
|
+
MAS_APPS.each do |app|
|
|
108
|
+
next unless app[:enabled]
|
|
109
|
+
|
|
110
|
+
Bootstrap::Display.info("Installing #{app[:name]}")
|
|
111
|
+
Bootstrap::System.run(%(#{mas} install #{app[:id]}), allow_failure: true, dry_run: @dry_run)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def link_configs
|
|
116
|
+
validate_directory(@configs_dir, 'Configs')
|
|
117
|
+
validate_directory(@hooks_dir, 'Hooks')
|
|
118
|
+
apply_brew_environment
|
|
119
|
+
|
|
120
|
+
Bootstrap::Spinner.spin('Linking Configs...') do |spinner|
|
|
121
|
+
Dir.children(@configs_dir).sort.each do |dir|
|
|
122
|
+
next if dir.start_with?('.')
|
|
123
|
+
|
|
124
|
+
dir_path = File.join(@configs_dir, dir)
|
|
125
|
+
next unless File.directory?(dir_path)
|
|
126
|
+
|
|
127
|
+
if package_linked?(dir)
|
|
128
|
+
Bootstrap::Logger.log("Skipping #{dir} (already linked)")
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
spinner.update("Linking #{dir}...")
|
|
133
|
+
self.quiet = true
|
|
134
|
+
|
|
135
|
+
run_hook(dir, :pre)
|
|
136
|
+
Bootstrap::Logger.log("Linking #{dir}")
|
|
137
|
+
Bootstrap::System.run(%(stow --adopt --target="#{Dir.home}" --dir="#{@configs_dir}" "#{dir}"), dry_run: @dry_run, quiet: true)
|
|
138
|
+
run_hook(dir, :post)
|
|
139
|
+
|
|
140
|
+
self.quiet = false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
Bootstrap::Display.persist('All configs linked', :success)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def unlink_configs
|
|
147
|
+
validate_directory(@configs_dir, 'Configs')
|
|
148
|
+
|
|
149
|
+
Bootstrap::Spinner.spin('Unlinking Configs...') do |spinner|
|
|
150
|
+
Dir.children(@configs_dir).sort.each do |dir|
|
|
151
|
+
next if dir.start_with?('.')
|
|
152
|
+
|
|
153
|
+
dir_path = File.join(@configs_dir, dir)
|
|
154
|
+
next unless File.directory?(dir_path)
|
|
155
|
+
|
|
156
|
+
spinner.update("Unlinking #{dir}...")
|
|
157
|
+
Bootstrap::Logger.log("Unlinking #{dir}")
|
|
158
|
+
Bootstrap::System.run(%(stow --target="#{Dir.home}" --dir="#{@configs_dir}" --delete "#{dir}"), allow_failure: true, dry_run: @dry_run, quiet: true)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
Bootstrap::Display.persist('All configs unlinked', :success)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def setup_uv
|
|
165
|
+
return Bootstrap::Display.info('uv already installed') if Dir.exist?(File.join(Dir.home, '.local', 'bin', 'uv'))
|
|
166
|
+
|
|
167
|
+
Bootstrap::Display.header('Installing Astral uv')
|
|
168
|
+
Bootstrap::System.run('curl -LsSf https://astral.sh/uv/install.sh | bash -s', dry_run: @dry_run)
|
|
169
|
+
Bootstrap::System.run("#{File.join(Dir.home, '.local', 'bin', 'uv')} python install && uv venv ~/.venv && source ~/.venv/bin/activate", allow_failure: true, dry_run: @dry_run)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def setup_env_file
|
|
173
|
+
env_source = File.join(@script_dir, '.env')
|
|
174
|
+
env_dest = File.join(Dir.home, '.env')
|
|
175
|
+
|
|
176
|
+
return unless File.exist?(env_source)
|
|
177
|
+
|
|
178
|
+
if File.exist?(env_dest) && !File.symlink?(env_dest)
|
|
179
|
+
Bootstrap::Display.warn("~/.env exists and is not a symlink. Skipping link.")
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check if it points to the right place
|
|
184
|
+
if File.symlink?(env_dest) && File.readlink(env_dest) == env_source
|
|
185
|
+
Bootstrap::Display.info("~/.env already linked.")
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
Bootstrap::Display.header('Linking .env')
|
|
190
|
+
Bootstrap::System.run("ln -sf #{env_source} #{env_dest}", dry_run: @dry_run)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def inject_secrets(secrets_path)
|
|
194
|
+
return if secrets_path.nil? || secrets_path.empty?
|
|
195
|
+
|
|
196
|
+
unless File.directory?(secrets_path)
|
|
197
|
+
Bootstrap::Display.warn("Secrets directory not found at #{secrets_path}. Skipping injection.")
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
Bootstrap::Display.header("Injecting secrets from #{secrets_path}")
|
|
202
|
+
|
|
203
|
+
# Inject .env
|
|
204
|
+
secrets_env = File.join(secrets_path, '.env')
|
|
205
|
+
if File.exist?(secrets_env)
|
|
206
|
+
Bootstrap::Display.info("Copying .env from #{secrets_env}...")
|
|
207
|
+
Bootstrap::System.run("cp #{secrets_env} #{@script_dir}/.env", dry_run: @dry_run)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Inject Configs
|
|
211
|
+
secrets_configs = File.join(secrets_path, 'Configs')
|
|
212
|
+
if File.directory?(secrets_configs)
|
|
213
|
+
Bootstrap::Display.info("Copying Configs from #{secrets_configs}...")
|
|
214
|
+
# We use cp_r with remove_destination: true to overwrite existing files/symlinks
|
|
215
|
+
# But we need to be careful. We want to merge directories, not replace them entirely if possible.
|
|
216
|
+
# FileUtils.cp_r merges directories.
|
|
217
|
+
Bootstrap::System.run("cp -R #{secrets_configs}/. #{@configs_dir}/", dry_run: @dry_run)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Inject Hooks
|
|
221
|
+
secrets_hooks = File.join(secrets_path, 'Hooks')
|
|
222
|
+
if File.directory?(secrets_hooks)
|
|
223
|
+
Bootstrap::Display.info("Copying Hooks from #{secrets_hooks}...")
|
|
224
|
+
# Use shell globbing to include hidden files
|
|
225
|
+
# cp -R source/. dest/ usually works, but let's be explicit
|
|
226
|
+
Bootstrap::System.run("cp -R #{secrets_hooks}/. #{@hooks_dir}/", dry_run: @dry_run)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def setup_conda
|
|
231
|
+
Bootstrap::Display.header('Configuring Conda environment')
|
|
232
|
+
Bootstrap::System.run('conda init && source ~/.zshrc', dry_run: @dry_run)
|
|
233
|
+
Bootstrap::System.run('conda create -n default python=3.9.4 --yes', dry_run: @dry_run)
|
|
234
|
+
Bootstrap::System.run('conda config --set changeps1 False', dry_run: @dry_run)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def setup_rye
|
|
238
|
+
path = File.join(Dir.home, '.rye', 'env')
|
|
239
|
+
if Dir.exist?(path)
|
|
240
|
+
Bootstrap::Display.info('rye already installed')
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
Bootstrap::Display.header('Installing Astral rye')
|
|
245
|
+
Bootstrap::System.run('curl -sSf https://rye.astral.sh/get | RYE_INSTALL_OPTION="--yes" bash', dry_run: @dry_run)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def install_rosetta
|
|
249
|
+
Bootstrap::System.run('/usr/sbin/softwareupdate --install-rosetta --agree-to-license', allow_failure: true, dry_run: @dry_run)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def restart_launchpad
|
|
253
|
+
Bootstrap::System.run('defaults write com.apple.dock ResetLaunchPad -bool true && sudo killall Dock', allow_failure: true, dry_run: @dry_run)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def change_default_shell
|
|
257
|
+
zsh_path = find_binary('zsh')
|
|
258
|
+
return Bootstrap::Display.warn('Zsh not found, skipping default shell change.') if zsh_path.nil?
|
|
259
|
+
|
|
260
|
+
Bootstrap::System.run(%(echo "#{zsh_path}" | sudo tee -a /etc/shells), allow_failure: true, dry_run: @dry_run)
|
|
261
|
+
Bootstrap::System.run("chsh -s #{zsh_path}", allow_failure: true, dry_run: @dry_run)
|
|
262
|
+
Bootstrap::System.run("sudo chsh -s #{zsh_path}", allow_failure: true, dry_run: @dry_run)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def fix_insecure_dir_problems
|
|
266
|
+
commands = [
|
|
267
|
+
'[[ -e ~/.zcompdump* ]] && rm -f ~/.zcompdump*',
|
|
268
|
+
'[[ -n $(command -v compinit) ]] && compinit',
|
|
269
|
+
'[[ -n $(command -v compaudit) ]] && compaudit | xargs -n1 -r chmod g-w,o-w',
|
|
270
|
+
'[[ -d /usr/local/share ]] && chmod go-w /usr/local/share || printf ""'
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
commands.each { |cmd| Bootstrap::System.run(cmd, allow_failure: true, dry_run: @dry_run) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def tweak_macOS_configuration
|
|
277
|
+
commands = [
|
|
278
|
+
'brew analytics off',
|
|
279
|
+
'sudo spctl --master-disable',
|
|
280
|
+
'defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false',
|
|
281
|
+
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user',
|
|
282
|
+
'defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false',
|
|
283
|
+
'defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false',
|
|
284
|
+
'defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool true',
|
|
285
|
+
'defaults write NSGlobalDomain AppleShowAllExtensions -bool true',
|
|
286
|
+
'defaults write NSGlobalDomain KeyRepeat -int 0',
|
|
287
|
+
'defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false',
|
|
288
|
+
'defaults write com.apple.finder ShowStatusBar -bool true',
|
|
289
|
+
'defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false',
|
|
290
|
+
'defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true',
|
|
291
|
+
'defaults write com.apple.frameworks.diskimages skip-verify -bool true',
|
|
292
|
+
'defaults write com.apple.frameworks.diskimages skip-verify-locked -bool true',
|
|
293
|
+
'defaults write com.apple.frameworks.diskimages skip-verify-remote -bool true',
|
|
294
|
+
'defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false',
|
|
295
|
+
'defaults write com.apple.terminal StringEncodings -array 4',
|
|
296
|
+
'defaults write com.apple.Terminal "Default Window Settings" -string "Pro"',
|
|
297
|
+
'defaults write com.apple.Terminal "Startup Window Settings" -string "Pro"',
|
|
298
|
+
'chflags nohidden ~/Library',
|
|
299
|
+
'sudo chflags nohidden /Volumes',
|
|
300
|
+
'defaults write NSGlobalDomain KeyRepeat -int 2',
|
|
301
|
+
'defaults write NSGlobalDomain InitialKeyRepeat -int 15',
|
|
302
|
+
'defaults write -g WebAutomaticTextReplacementEnabled -bool true',
|
|
303
|
+
'defaults write NSGlobalDomain AppleShowScrollBars -string "Always"',
|
|
304
|
+
'defaults write com.apple.dock mru-spaces -bool false',
|
|
305
|
+
'defaults write com.apple.dock autohide-time-modifier -int 0',
|
|
306
|
+
'defaults write com.apple.Terminal AppleShowScrollBars -string WhenScrolling',
|
|
307
|
+
'defaults write com.apple.driver.AppleBluetoothMultitouch.mouse MouseHorizontalScroll -bool NO',
|
|
308
|
+
'sudo defaults write /Library/Preferences/com.apple.loginwindow SHOWOTHERUSERS_MANAGED -bool FALSE',
|
|
309
|
+
'sudo launchctl start com.apple.locate || true',
|
|
310
|
+
'defaults write com.apple.QuickTimePlayerX MGPlayMovieOnOpen 1',
|
|
311
|
+
'defaults write -g NSShowAppCentricOpenPanelInsteadOfUntitledFile -bool false',
|
|
312
|
+
'defaults write com.apple.TextEdit "RichText" -bool "false"',
|
|
313
|
+
'defaults write com.apple.CrashReporter DialogType none',
|
|
314
|
+
'defaults write com.apple.LaunchServices LSQuarantine -bool NO',
|
|
315
|
+
'defaults write com.apple.finder "ShowPathbar" -bool "true"',
|
|
316
|
+
'defaults write com.apple.finder "_FXSortFoldersFirst" -bool "true"',
|
|
317
|
+
'defaults write com.apple.finder "_FXSortFoldersFirstOnDesktop" -bool "true"',
|
|
318
|
+
'defaults write com.apple.finder "FXDefaultSearchScope" -string "SCcf"',
|
|
319
|
+
'defaults write com.apple.finder "FXRemoveOldTrashItems" -bool "true"',
|
|
320
|
+
'defaults write com.apple.finder "FXEnableExtensionChangeWarning" -bool "false"',
|
|
321
|
+
'defaults write NSGlobalDomain "NSTableViewDefaultSizeMode" -int "3"',
|
|
322
|
+
'defaults write com.apple.finder "ShowHardDrivesOnDesktop" -bool "false"',
|
|
323
|
+
'defaults write com.apple.finder "ShowExternalHardDrivesOnDesktop" -bool "false"',
|
|
324
|
+
'defaults write com.apple.finder "ShowRemovableMediaOnDesktop" -bool "false"',
|
|
325
|
+
'defaults write com.apple.finder "ShowMountedServersOnDesktop" -bool "true"',
|
|
326
|
+
'defaults write com.apple.menuextra.clock "FlashDateSeparators" -bool "true"',
|
|
327
|
+
'defaults write com.apple.dock "expose-group-apps" -bool "true"',
|
|
328
|
+
'defaults write com.apple.spaces "spans-displays" -bool "false"',
|
|
329
|
+
%(defaults write com.apple.iphonesimulator "ScreenShotSaveLocation" -string "#{File.join(Dir.home, 'Pictures', 'Screenshots')}"),
|
|
330
|
+
%(defaults write com.apple.screencapture "location" -string "#{File.join(Dir.home, 'Pictures', 'Screenshots')}"),
|
|
331
|
+
'defaults write com.apple.TimeMachine "DoNotOfferNewDisksForBackup" -bool "true"',
|
|
332
|
+
'defaults write com.apple.dock "enable-spring-load-actions-on-all-items" -bool "true"',
|
|
333
|
+
'defaults write com.apple.LaunchServices "LSQuarantine" -bool "false"',
|
|
334
|
+
'defaults write com.apple.Terminal "FocusFollowsMouse" -bool "true"',
|
|
335
|
+
'defaults write com.apple.dock "tilesize" -int "48"',
|
|
336
|
+
'defaults write com.apple.dock "autohide" -bool "true"',
|
|
337
|
+
'defaults write com.apple.dock autohide-delay -float 0',
|
|
338
|
+
'defaults write com.apple.dock autohide-time-modifier -float 0.15',
|
|
339
|
+
'killall Dock',
|
|
340
|
+
'defaults write com.apple.dock "show-recents" -bool "false"',
|
|
341
|
+
'defaults write com.apple.dock "mineffect" -string "suck"',
|
|
342
|
+
'defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false',
|
|
343
|
+
'defaults write -g com.apple.trackpad.scaling 1',
|
|
344
|
+
'defaults write -g com.apple.mouse.scaling 2.0',
|
|
345
|
+
'defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true',
|
|
346
|
+
'defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool true',
|
|
347
|
+
'defaults write com.apple.dock tilesize -int 45',
|
|
348
|
+
'defaults write com.apple.messageshelper.MessageController SOInputLineSettings -dict-add "automaticQuoteSubstitutionEnabled" -bool false',
|
|
349
|
+
'defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false',
|
|
350
|
+
'defaults write com.apple.mail DisableInlineAttachmentViewing -bool true',
|
|
351
|
+
'defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false',
|
|
352
|
+
'launchctl unload -w /System/Library/LaunchAgents/com.apple.rcd.plist'
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
commands.each do |command|
|
|
356
|
+
Bootstrap::System.run(command, allow_failure: true, dry_run: @dry_run)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
alias tweak_macos_configuration tweak_macOS_configuration
|
|
361
|
+
|
|
362
|
+
def add_to_sudoers
|
|
363
|
+
return if @user.nil? || @user.empty?
|
|
364
|
+
|
|
365
|
+
Bootstrap::Display.header("Ensuring sudoers entry for #{@user}")
|
|
366
|
+
|
|
367
|
+
script = <<~SHELL
|
|
368
|
+
set -e
|
|
369
|
+
sed -i '' -e 's/#includedir/@includedir/' /private/etc/sudoers
|
|
370
|
+
sudoers_path="/private/etc/sudoers.d/sudoers"
|
|
371
|
+
[ -f "$sudoers_path" ] || touch "$sudoers_path"
|
|
372
|
+
entry="#{@user} ALL=(ALL) NOPASSWD:ALL"
|
|
373
|
+
if ! grep -q "$entry" "$sudoers_path"; then
|
|
374
|
+
echo "$entry" >> "$sudoers_path"
|
|
375
|
+
fi
|
|
376
|
+
SHELL
|
|
377
|
+
|
|
378
|
+
Bootstrap::System.run(['sudo', 'bash', '-c', script], dry_run: @dry_run)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def set_system_hostname(hostname: 'macBook', domain: 'pro')
|
|
382
|
+
fqdn = [hostname, domain].compact.join('.').strip
|
|
383
|
+
Bootstrap::Display.header("Setting system hostname to #{fqdn}")
|
|
384
|
+
Bootstrap::System.run("sudo scutil --set ComputerName #{fqdn}", allow_failure: true, dry_run: @dry_run)
|
|
385
|
+
Bootstrap::System.run("sudo scutil --set LocalHostName #{hostname}", allow_failure: true, dry_run: @dry_run)
|
|
386
|
+
Bootstrap::System.run("sudo scutil --set HostName #{fqdn}", allow_failure: true, dry_run: @dry_run)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def formula_installed?(formula)
|
|
390
|
+
installed_formulae.include?(formula.split('/').last)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def cask_installed?(cask)
|
|
394
|
+
installed_casks.include?(cask.split('/').last)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def package_linked?(dir)
|
|
398
|
+
# Check if all top-level files in the config dir are symlinked in home
|
|
399
|
+
config_path = File.join(@configs_dir, dir)
|
|
400
|
+
return false unless File.directory?(config_path)
|
|
401
|
+
|
|
402
|
+
Dir.children(config_path).all? do |child|
|
|
403
|
+
next true if child == '.DS_Store'
|
|
404
|
+
|
|
405
|
+
source = File.join(config_path, child)
|
|
406
|
+
target = File.join(Dir.home, child)
|
|
407
|
+
|
|
408
|
+
# If it's a directory in source, stow usually symlinks the contents unless --adopt is used differently
|
|
409
|
+
# But standard stow symlinks the directory itself if it doesn't exist, or contents if it does.
|
|
410
|
+
# Simplest check: does target exist and is it a symlink pointing to source?
|
|
411
|
+
# Stow is complex, but let's check basic symlink existence.
|
|
412
|
+
|
|
413
|
+
if File.symlink?(target)
|
|
414
|
+
# Check if it points to the right place
|
|
415
|
+
begin
|
|
416
|
+
File.readlink(target) == source || File.readlink(target).include?(source)
|
|
417
|
+
rescue StandardError
|
|
418
|
+
false
|
|
419
|
+
end
|
|
420
|
+
else
|
|
421
|
+
# If target exists and is not a symlink, it's definitely not linked (conflict or adopted)
|
|
422
|
+
# If target doesn't exist, it's not linked
|
|
423
|
+
false
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
private
|
|
429
|
+
|
|
430
|
+
def installed_formulae
|
|
431
|
+
@installed_formulae ||= begin
|
|
432
|
+
return [] if @dry_run # In dry-run, we might assume nothing is installed or check system?
|
|
433
|
+
# Actually, checking system is fine in dry-run to simulate correctly.
|
|
434
|
+
`#{brew_path} list --formula -1`.split("\n")
|
|
435
|
+
rescue StandardError
|
|
436
|
+
[]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def installed_casks
|
|
441
|
+
@installed_casks ||= begin
|
|
442
|
+
return [] if @dry_run
|
|
443
|
+
`#{brew_path} list --cask -1`.split("\n")
|
|
444
|
+
rescue StandardError
|
|
445
|
+
[]
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def prerequisites_ran?
|
|
450
|
+
File.exist?(@prereq_marker) && File.read(@prereq_marker).strip == 'true'
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def mark_prerequisites_done
|
|
454
|
+
File.write(@prereq_marker, 'true')
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def ensure_sudo!
|
|
458
|
+
result = system('sudo -n true')
|
|
459
|
+
return if result
|
|
460
|
+
|
|
461
|
+
Bootstrap::Display.info('Password may be required for installation:')
|
|
462
|
+
raise 'Failed to obtain sudo privileges. Exiting.' unless system('sudo -v')
|
|
463
|
+
|
|
464
|
+
Bootstrap::Display.info('Sudo access granted.')
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def install_cargo_toolchain
|
|
468
|
+
cargo_path = File.join(Dir.home, '.cargo', 'bin', 'cargo')
|
|
469
|
+
return Bootstrap::Display.info('Cargo already installed.') if File.exist?(cargo_path)
|
|
470
|
+
|
|
471
|
+
Bootstrap::Display.header('Installing Rust toolchain')
|
|
472
|
+
Bootstrap::System.run("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --quiet -y --profile default", dry_run: @dry_run)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def ensure_homebrew_installed
|
|
476
|
+
return Bootstrap::Display.info('Homebrew already installed.') if find_binary('brew')
|
|
477
|
+
|
|
478
|
+
Bootstrap::Display.header('Installing Homebrew')
|
|
479
|
+
install_command = %(printf '\\r' | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)")
|
|
480
|
+
Bootstrap::System.run(install_command, dry_run: @dry_run)
|
|
481
|
+
append_to_file(File.join(Dir.home, '.zprofile'), 'eval "$(/opt/homebrew/bin/brew shellenv)"')
|
|
482
|
+
@brew_path = '/opt/homebrew/bin/brew' if File.exist?('/opt/homebrew/bin/brew')
|
|
483
|
+
apply_brew_environment
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def ensure_stow_installed
|
|
487
|
+
stow = find_binary('stow')
|
|
488
|
+
return Bootstrap::Display.info('GNU Stow already installed.') if stow
|
|
489
|
+
|
|
490
|
+
Bootstrap::Display.header('Installing GNU Stow')
|
|
491
|
+
apply_brew_environment
|
|
492
|
+
Bootstrap::System.run("#{brew_path} install --quiet --formula stow", dry_run: @dry_run)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def run_priority_hooks
|
|
496
|
+
PRIORITY_HOOKS.each do |dir_name|
|
|
497
|
+
install_hook(dir_name) if File.directory?(File.join(@hooks_dir, dir_name))
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def install_hook(dir_name, spinner = nil)
|
|
502
|
+
self.quiet = !!spinner
|
|
503
|
+
if spinner
|
|
504
|
+
spinner.update("Running hooks for #{dir_name}...")
|
|
505
|
+
end
|
|
506
|
+
run_hook(dir_name, :pre)
|
|
507
|
+
run_hook(dir_name, :post)
|
|
508
|
+
ensure
|
|
509
|
+
self.quiet = false
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def run_hook(dir_name, stage)
|
|
513
|
+
script = File.join(@hooks_dir, dir_name, "#{stage}.rb")
|
|
514
|
+
return unless File.exist?(script)
|
|
515
|
+
|
|
516
|
+
Bootstrap::Hooks.with_configurator(self) do
|
|
517
|
+
load(script)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def install_formula(formula, spinner = nil)
|
|
522
|
+
if FORMULAE_DISABLED.include?(formula)
|
|
523
|
+
Bootstrap::Logger.log("Skipping #{formula} (disabled)")
|
|
524
|
+
return
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
if formula_installed?(formula)
|
|
528
|
+
Bootstrap::Logger.log("Skipping #{formula} (already installed)")
|
|
529
|
+
return
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
path = brew_path
|
|
533
|
+
raise 'Homebrew is not available.' if path.nil?
|
|
534
|
+
|
|
535
|
+
if spinner
|
|
536
|
+
spinner.update("Installing #{formula}...")
|
|
537
|
+
else
|
|
538
|
+
Bootstrap::Display.info("Installing #{formula}")
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
Bootstrap::Logger.log("Installing #{formula}")
|
|
542
|
+
# Use quiet: true if spinner is present to avoid conflict
|
|
543
|
+
Bootstrap::System.run("#{path} install --quiet --formula #{formula}", allow_failure: true, dry_run: @dry_run, quiet: !!spinner)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def apply_brew_environment
|
|
547
|
+
brew = brew_path
|
|
548
|
+
return if brew.nil?
|
|
549
|
+
|
|
550
|
+
command = %(eval "$("#{brew}" shellenv)" && env)
|
|
551
|
+
escaped = command.gsub("'", %q(\\\'))
|
|
552
|
+
output = `bash -lc '#{escaped}'`
|
|
553
|
+
|
|
554
|
+
output.each_line do |line|
|
|
555
|
+
key, value = line.strip.split('=', 2)
|
|
556
|
+
next if key.nil? || value.nil?
|
|
557
|
+
|
|
558
|
+
ENV[key] = value
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def brew_path
|
|
563
|
+
return @brew_path if defined?(@brew_path) && @brew_path
|
|
564
|
+
|
|
565
|
+
candidate = find_binary('brew', ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'])
|
|
566
|
+
@brew_path = candidate
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def find_binary(name, fallbacks = [])
|
|
570
|
+
path = `command -v #{name}`.strip
|
|
571
|
+
return path unless path.nil? || path.empty?
|
|
572
|
+
|
|
573
|
+
fallbacks.each do |candidate|
|
|
574
|
+
return candidate if File.exist?(candidate)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def validate_directory(directory, name)
|
|
581
|
+
raise "Error: #{name} directory not found." unless File.directory?(directory)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def append_to_file(path, content)
|
|
585
|
+
return if @dry_run
|
|
586
|
+
FileUtils.touch(path)
|
|
587
|
+
lines = File.read(path).split("\n")
|
|
588
|
+
return if lines.include?(content)
|
|
589
|
+
|
|
590
|
+
File.open(path, 'a') { |file| file.puts(content) }
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|