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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +508 -0
- data/LICENSE.txt +21 -0
- data/README.md +899 -0
- data/Rakefile +10 -0
- data/exe/kamal-dev +10 -0
- data/exe/plugin-kamal-dev +283 -0
- data/lib/kamal/cli/dev.rb +1192 -0
- data/lib/kamal/dev/builder.rb +332 -0
- data/lib/kamal/dev/compose_parser.rb +255 -0
- data/lib/kamal/dev/config.rb +359 -0
- data/lib/kamal/dev/devcontainer.rb +122 -0
- data/lib/kamal/dev/devcontainer_parser.rb +204 -0
- data/lib/kamal/dev/registry.rb +149 -0
- data/lib/kamal/dev/secrets_loader.rb +93 -0
- data/lib/kamal/dev/state_manager.rb +271 -0
- data/lib/kamal/dev/templates/dev-entrypoint.sh +44 -0
- data/lib/kamal/dev/templates/dev.yml +93 -0
- data/lib/kamal/dev/version.rb +7 -0
- data/lib/kamal/dev.rb +33 -0
- data/lib/kamal/providers/base.rb +121 -0
- data/lib/kamal/providers/upcloud.rb +299 -0
- data/lib/kamal-dev.rb +5 -0
- data/sig/kamal/dev.rbs +6 -0
- data/test_installer.sh +73 -0
- metadata +141 -0
data/Rakefile
ADDED
data/exe/kamal-dev
ADDED
|
@@ -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
|