kamal-dev 0.3.0

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.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
data/exe/kamal-dev ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Development binstub for kamal-dev
4
+ # Loads the extension and runs kamal CLI
5
+
6
+ require "bundler/setup"
7
+ require_relative "../lib/kamal/dev"
8
+
9
+ # Now run kamal with the dev extension loaded
10
+ Kamal::Cli::Main.start(ARGV)
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ class KamalDevInstaller
7
+ PATCH_MARKER = 'require "kamal-dev"'
8
+ BINSTUB_PATH = "bin/kamal"
9
+
10
+ def initialize
11
+ @patched = false
12
+ @created_binstub = false
13
+ @mode = nil
14
+ end
15
+
16
+ def run
17
+ print_header
18
+
19
+ unless in_project_directory?
20
+ error "Not in a project directory. Please run this from your project root (must have a Gemfile)."
21
+ exit 1
22
+ end
23
+
24
+ # Ask user which installation method they prefer
25
+ choose_installation_mode
26
+
27
+ case @mode
28
+ when :gem
29
+ patch_gem_executable
30
+ when :binstub
31
+ ensure_bin_directory
32
+ ensure_kamal_binstub
33
+ patch_binstub
34
+ end
35
+
36
+ print_success
37
+ end
38
+
39
+ private
40
+
41
+ def print_header
42
+ puts "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
43
+ puts "🔧 Kamal Dev Installer"
44
+ puts "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
45
+ puts
46
+ end
47
+
48
+ def choose_installation_mode
49
+ puts "Choose installation method:"
50
+ puts
51
+ puts " 1. Patch gem-installed kamal executable (global, works with 'kamal dev')"
52
+ puts " 2. Create project binstub bin/kamal (local, use 'bin/kamal dev')"
53
+ puts
54
+ print "Enter choice [1/2] (default: 1): "
55
+
56
+ choice = $stdin.gets.chomp
57
+ choice = "1" if choice.empty?
58
+
59
+ case choice
60
+ when "1"
61
+ @mode = :gem
62
+ when "2"
63
+ @mode = :binstub
64
+ else
65
+ error "Invalid choice. Please enter 1 or 2."
66
+ exit 1
67
+ end
68
+
69
+ puts
70
+ end
71
+
72
+ def patch_gem_executable
73
+ gem_executable = find_gem_executable
74
+
75
+ unless gem_executable
76
+ error "Could not find kamal executable. Make sure 'kamal' gem is installed."
77
+ exit 1
78
+ end
79
+
80
+ puts "📍 Found kamal executable: #{gem_executable}"
81
+ puts
82
+
83
+ if already_patched?(File.read(gem_executable))
84
+ puts "✓ Kamal executable already patched (kamal-dev is loaded)"
85
+ return
86
+ end
87
+
88
+ # Create backup
89
+ backup_path = "#{gem_executable}.backup"
90
+ if File.exist?(backup_path)
91
+ puts "✓ Backup already exists: #{backup_path}"
92
+ else
93
+ puts "💾 Creating backup: #{backup_path}"
94
+ FileUtils.cp(gem_executable, backup_path)
95
+ end
96
+
97
+ # Patch the executable
98
+ puts "🔧 Patching kamal executable..."
99
+ content = File.read(gem_executable)
100
+ patched_content = insert_require(content)
101
+
102
+ if patched_content.nil?
103
+ error "Could not find a safe insertion point in kamal executable. Please patch manually."
104
+ exit 1
105
+ end
106
+
107
+ File.write(gem_executable, patched_content)
108
+ @patched = true
109
+ puts "✓ Patched kamal executable"
110
+ end
111
+
112
+ def find_gem_executable
113
+ # Try to find via bundle
114
+ kamal_path = `bundle exec which kamal 2>/dev/null`.strip
115
+ return kamal_path unless kamal_path.empty?
116
+
117
+ # Try via gem
118
+ kamal_path = `which kamal 2>/dev/null`.strip
119
+ return kamal_path unless kamal_path.empty?
120
+
121
+ # Try via gem environment
122
+ gem_bin_dir = `gem environment gemdir 2>/dev/null`.strip
123
+ return nil if gem_bin_dir.empty?
124
+
125
+ possible_path = File.join(gem_bin_dir.sub("/gems", "/bin"), "kamal")
126
+ return possible_path if File.exist?(possible_path)
127
+
128
+ nil
129
+ end
130
+
131
+ def in_project_directory?
132
+ File.exist?("Gemfile")
133
+ end
134
+
135
+ def ensure_bin_directory
136
+ return if Dir.exist?("bin")
137
+
138
+ puts "📁 Creating bin directory..."
139
+ FileUtils.mkdir_p("bin")
140
+ end
141
+
142
+ def ensure_kamal_binstub
143
+ return if File.exist?(BINSTUB_PATH)
144
+
145
+ puts "📝 Generating kamal binstub..."
146
+ unless system("bundle binstubs kamal --force", out: File::NULL, err: File::NULL)
147
+ error "Failed to generate kamal binstub. Make sure 'kamal' gem is in your Gemfile."
148
+ exit 1
149
+ end
150
+
151
+ @created_binstub = true
152
+
153
+ unless File.exist?(BINSTUB_PATH)
154
+ error "Binstub generation succeeded but #{BINSTUB_PATH} not found. Please check your bundler configuration."
155
+ exit 1
156
+ end
157
+
158
+ puts "✓ Created #{BINSTUB_PATH}"
159
+ end
160
+
161
+ def patch_binstub
162
+ content = File.read(BINSTUB_PATH)
163
+
164
+ if already_patched?(content)
165
+ puts "✓ #{BINSTUB_PATH} already patched (kamal-dev is loaded)"
166
+ return
167
+ end
168
+
169
+ puts "🔧 Patching #{BINSTUB_PATH}..."
170
+
171
+ patched_content = insert_require(content)
172
+
173
+ if patched_content.nil?
174
+ error "Could not find a safe insertion point in #{BINSTUB_PATH}. Please add '#{PATCH_MARKER}' manually after bundler setup."
175
+ exit 1
176
+ end
177
+
178
+ # Write patched content
179
+ File.write(BINSTUB_PATH, patched_content)
180
+
181
+ # Preserve executable permissions
182
+ File.chmod(0o755, BINSTUB_PATH)
183
+
184
+ @patched = true
185
+ puts "✓ Patched #{BINSTUB_PATH}"
186
+ end
187
+
188
+ def already_patched?(content)
189
+ content.include?(PATCH_MARKER) || content.include?("require 'kamal-dev'")
190
+ end
191
+
192
+ def insert_require(content)
193
+ lines = content.lines
194
+
195
+ # Strategy 1: Insert after "require 'bundler/setup'" or similar bundler requires
196
+ bundler_setup_index = lines.find_index { |line| line =~ /require ['"]bundler\/setup['"]/ }
197
+
198
+ if bundler_setup_index
199
+ # Insert after bundler setup and any following blank lines
200
+ insert_index = bundler_setup_index + 1
201
+ insert_index += 1 while insert_index < lines.size && lines[insert_index].strip.empty?
202
+
203
+ lines.insert(insert_index, "\n# Load kamal-dev extension to enable 'kamal dev' commands\n#{PATCH_MARKER}\n")
204
+ return lines.join
205
+ end
206
+
207
+ # Strategy 2: Insert before "load Gem.bin_path" or "Gem.activate_bin_path"
208
+ load_index = lines.find_index { |line| line =~ /(load Gem\.bin_path|Gem\.activate_bin_path)/ }
209
+
210
+ if load_index
211
+ # Insert before the load line, with a blank line separator
212
+ lines.insert(load_index, "\n# Load kamal-dev extension to enable 'kamal dev' commands\n#{PATCH_MARKER}\n\n")
213
+ return lines.join
214
+ end
215
+
216
+ # Strategy 3: Insert after shebang and frozen_string_literal
217
+ # This is the fallback for binstubs we don't recognize
218
+ insert_index = 0
219
+ insert_index += 1 if lines[0]&.start_with?("#!")
220
+ insert_index += 1 if lines[insert_index]&.include?("frozen_string_literal")
221
+ insert_index += 1 while insert_index < lines.size && lines[insert_index].strip.empty?
222
+
223
+ if insert_index < lines.size
224
+ lines.insert(insert_index, "\n# Load kamal-dev extension to enable 'kamal dev' commands\n#{PATCH_MARKER}\n\n")
225
+ return lines.join
226
+ end
227
+
228
+ # Could not find a safe insertion point
229
+ nil
230
+ end
231
+
232
+ def print_success
233
+ puts
234
+ puts "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
235
+ puts "✅ Installation complete!"
236
+ puts "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
237
+ puts
238
+
239
+ if @created_binstub || @patched
240
+ puts "You can now use the 'kamal dev' commands:"
241
+ puts
242
+
243
+ case @mode
244
+ when :gem
245
+ puts " kamal dev deploy --count 3"
246
+ puts " kamal dev list"
247
+ puts " kamal dev stop --all"
248
+ puts " kamal dev remove --all"
249
+ puts
250
+ puts "Or with bundle exec:"
251
+ puts " bundle exec kamal dev deploy --count 3"
252
+ when :binstub
253
+ puts " bin/kamal dev deploy --count 3"
254
+ puts " bin/kamal dev list"
255
+ puts " bin/kamal dev stop --all"
256
+ puts " bin/kamal dev remove --all"
257
+ puts
258
+ puts "For more information, run: bin/kamal dev help"
259
+ end
260
+ else
261
+ puts "No changes were needed - you're already set up!"
262
+ puts
263
+
264
+ case @mode
265
+ when :gem
266
+ puts "Use: kamal dev <command>"
267
+ when :binstub
268
+ puts "Use: bin/kamal dev <command>"
269
+ end
270
+ end
271
+
272
+ puts
273
+ end
274
+
275
+ def error(message)
276
+ puts
277
+ puts "❌ Error: #{message}"
278
+ puts
279
+ end
280
+ end
281
+
282
+ # Run installer
283
+ KamalDevInstaller.new.run