yatfa 1.0.65

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.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # YATFA Agent Runner (Yet Another Tool For Agents)
5
+ # Usage: npx yatfa-agent [worker|planner|reviewer|researcher]
6
+ # npx yatfa-agent attach [agent-type]
7
+ # npx yatfa-agent setup
8
+ #
9
+ # Requirements:
10
+ # - Docker
11
+ # - Ruby
12
+ # - Dockerfile.sandbox in project root
13
+ # - yatfa.env.rb in project root (gitignored)
14
+
15
+ require_relative "../lib/yatfa_agent/config"
16
+ require_relative "../lib/yatfa_agent/docker"
17
+ require_relative "../lib/yatfa_agent/agent"
18
+ require_relative "../lib/yatfa_agent/setup"
19
+ require_relative "../agents"
20
+
21
+ def show_usage
22
+ puts "YATFA Agent Runner (Yet Another Tool For Agents)"
23
+ puts ""
24
+ puts "Usage:"
25
+ puts " npx yatfa-agent setup # Interactive setup wizard"
26
+ puts " npx yatfa-agent [worker|planner|reviewer|researcher] # Run agent"
27
+ puts " npx yatfa-agent attach [agent-type] # Attach to running agent"
28
+ puts ""
29
+ puts "Setup:"
30
+ puts " Run 'npx yatfa-agent setup' for interactive configuration"
31
+ puts ""
32
+ puts "Manual setup:"
33
+ puts " 1. Create Dockerfile.sandbox (see https://github.com/RoM4iK/yatfa-public/blob/main/README.md)"
34
+ puts " 2. Create yatfa.env.rb (see https://github.com/RoM4iK/yatfa-public/blob/main/README.md)"
35
+ puts " 3. echo 'yatfa.env.rb' >> .gitignore"
36
+ puts " 4. npx yatfa-agent worker"
37
+ exit 1
38
+ end
39
+
40
+ # Main
41
+ show_usage if ARGV.empty?
42
+
43
+ command = ARGV[0].downcase
44
+
45
+ # Handle setup command
46
+ if command == "setup"
47
+ wizard = YatfaAgent::Setup::SetupWizard.new
48
+ wizard.run
49
+ exit 0
50
+ end
51
+
52
+ # Handle attach command
53
+ if command == "attach"
54
+ agent_type = ARGV[1]&.downcase
55
+ abort "Usage: npx yatfa-agent attach [agent-type]" unless agent_type
56
+ config = YatfaAgent::Config.load
57
+ YatfaAgent::Config.fetch_repositories!(config)
58
+ YatfaAgent::Agent.attach(agent_type, config, AGENT_CONFIGS)
59
+ else
60
+ # Handle run commands (worker, planner, reviewer, researcher)
61
+ config = YatfaAgent::Config.load
62
+ # Fetch repositories from Rails API before building
63
+ YatfaAgent::Config.fetch_repositories!(config)
64
+ YatfaAgent::Docker.build_image(config)
65
+ # For "run" command, use second argument as agent type
66
+ # For "attach" command, handled separately above
67
+ agent_type_to_run = (command == "run") ? ARGV[1] : command
68
+ YatfaAgent::Agent.run(agent_type_to_run, config, AGENT_CONFIGS)
69
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+ require 'fileutils'
3
+
4
+ module YatfaAgent
5
+ module Agent
6
+ def self.run(agent_type, config, agent_configs)
7
+ unless agent_configs.keys.include?(agent_type)
8
+ puts "โŒ Unknown agent type: #{agent_type}"
9
+ puts " Available: #{agent_configs.keys.join(', ')}"
10
+ exit 1
11
+ end
12
+
13
+ agent_def = agent_configs[agent_type]
14
+ agent_config = config.dig("agents", agent_type) || {}
15
+ container_name = agent_config["container_name"] || agent_def[:name]
16
+
17
+ puts "๐Ÿš€ Starting #{agent_type} agent..."
18
+
19
+ # Stop existing container if running
20
+ system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
21
+
22
+ # Write banner to a persistent temp file (not auto-deleted)
23
+ banner_path = "/tmp/yatfa-agent-banner-#{agent_type}.txt"
24
+ FileUtils.rm_rf(banner_path)
25
+
26
+ # Append repository context to banner
27
+ banner_content = agent_def[:banner].dup
28
+ if config["repositories"]&.any?
29
+ sorted_repos = config["repositories"].sort_by { |r| r["position"] || 0 }
30
+ repo_info = sorted_repos.map do |repo|
31
+ name = repo["name"]
32
+ path = repo["path_in_sandbox"] || "/workspace/#{name}"
33
+ " - **#{name}**: #{path}"
34
+ end.join("\n")
35
+
36
+ banner_content += "\n\n## REPOSITORY STRUCTURE\n\n"
37
+ banner_content += "This project has #{sorted_repos.count} repositories:\n\n"
38
+ banner_content += "#{repo_info}\n\n"
39
+ end
40
+
41
+ File.write(banner_path, banner_content)
42
+
43
+ docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
44
+
45
+ success = system(*docker_cmd)
46
+
47
+ if success
48
+ puts "โœ… Agent started in background"
49
+ puts ""
50
+ puts " Attach: npx yatfa-agent attach #{agent_type}"
51
+ puts " Logs: docker logs -f #{container_name}"
52
+ puts " Stop: docker stop #{container_name}"
53
+ else
54
+ puts "โŒ Failed to start agent"
55
+ exit 1
56
+ end
57
+ end
58
+
59
+ def self.attach(agent_type, config, agent_configs)
60
+ unless agent_configs.keys.include?(agent_type)
61
+ puts "โŒ Unknown agent type: #{agent_type}"
62
+ exit 1
63
+ end
64
+
65
+ agent_def = agent_configs[agent_type]
66
+ agent_config = config.dig("agents", agent_type) || {}
67
+ container_name = agent_config["container_name"] || agent_def[:name]
68
+
69
+ running = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
70
+
71
+ if running.empty?
72
+ puts "โš ๏ธ #{agent_type} agent is not running. Auto-starting..."
73
+ Docker.build_image(config)
74
+ run(agent_type, config, agent_configs)
75
+ sleep 3
76
+ end
77
+
78
+ puts "๐Ÿ“Ž Attaching to #{agent_type} agent..."
79
+
80
+ user = detect_agent_user(container_name)
81
+ puts " User: #{user}"
82
+
83
+ # Wait for tmux session to be ready
84
+ 10.times do
85
+ if system("docker", "exec", "-u", user, container_name, "tmux", "has-session", "-t", "agent", err: File::NULL, out: File::NULL)
86
+ break
87
+ end
88
+ sleep 1
89
+ end
90
+
91
+ # Attach to agent session which has the status bar
92
+ # Must run as agent user since tmux server runs under that user
93
+ exec("docker", "exec", "-it", "-u", user, container_name, "tmux", "attach", "-t", "agent")
94
+ end
95
+
96
+ private
97
+
98
+ def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
99
+ docker_cmd = [
100
+ "docker", "run", "-d",
101
+ "--name", container_name,
102
+ "--network", "host",
103
+ "--restart", "unless-stopped",
104
+ "--tmpfs", "/rails/tmp",
105
+ "--tmpfs", "/rails/log"
106
+ ]
107
+
108
+ # Inject custom environment variables from config
109
+ # Merge global env with agent-specific env (agent-specific takes precedence)
110
+ merged_env = {}
111
+ merged_env.merge!(config["env"]) if config["env"]
112
+ merged_env.merge!(agent_config["env"]) if agent_config["env"]
113
+
114
+ if merged_env.any?
115
+ merged_env.each do |k, v|
116
+ docker_cmd += ["-e", "#{k}=#{v}"]
117
+ end
118
+ global_count = config["env"]&.size || 0
119
+ agent_count = agent_config["env"]&.size || 0
120
+ if agent_count > 0
121
+ puts "๐ŸŒฟ Injected #{global_count} global + #{agent_count} agent-specific env vars"
122
+ else
123
+ puts "๐ŸŒฟ Injected #{global_count} custom env vars from config"
124
+ end
125
+ end
126
+
127
+ docker_cmd += [
128
+ "-v", "#{banner_path}:/etc/yatfa/system-prompt.txt:ro",
129
+ "-e", "YATFA_VERSION=main",
130
+ "-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
131
+ "-e", "AGENT_TYPE=#{agent_type}",
132
+ "-e", "YATFA_URL=#{YatfaAgent::Config.yatfa_url(config)}",
133
+ "-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"
134
+ ]
135
+
136
+ add_github_auth!(docker_cmd, config)
137
+ add_git_config!(docker_cmd, config)
138
+ add_repository_mounts!(docker_cmd, config)
139
+ add_development_mounts!(docker_cmd)
140
+
141
+ local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
142
+
143
+ if File.exist?(local_setup_script)
144
+ docker_cmd += [Config.image_name(config), "ruby", "/tmp/setup-agent.rb"]
145
+ else
146
+ docker_cmd += [Config.image_name(config)]
147
+ end
148
+
149
+ docker_cmd
150
+ end
151
+
152
+ def self.add_github_auth!(docker_cmd, config)
153
+ github = config["github"] || {}
154
+
155
+ # New method: GitHub authentication is handled by the backend
156
+ # Agents get tokens via YATFA_API_KEY using the /github/token endpoint
157
+ # No GitHub credentials need to be passed into the container
158
+
159
+ # Deprecation warning for old GitHub App config
160
+ if github["method"] == "app" || github["app_client_id"] || github["app_installation_id"]
161
+ puts "โš ๏ธ Warning: GitHub App configuration in yatfa.env.rb is deprecated"
162
+ puts " GitHub credentials are now managed centrally in the Rails backend"
163
+ puts " You can remove the 'github' section from yatfa.env.rb"
164
+ puts " The backend will use its own GitHub App credentials to generate tokens"
165
+ end
166
+
167
+ # Optional: Support GH_TOKEN fallback for development/testing
168
+ if github["token"]
169
+ docker_cmd.concat(["-e", "GH_TOKEN=#{github['token']}"])
170
+ puts "๐Ÿ”‘ Using GitHub token authentication (fallback mode)"
171
+ else
172
+ puts "โœ… GitHub authentication will be handled by backend (YATFA_API_KEY)"
173
+ end
174
+ end
175
+
176
+ def self.add_git_config!(docker_cmd, config)
177
+ return unless (git_config = config["git"])
178
+
179
+ docker_cmd.concat(["-e", "GIT_USER_NAME=#{git_config['user_name']}"]) if git_config["user_name"]
180
+ docker_cmd.concat(["-e", "GIT_USER_EMAIL=#{git_config['user_email']}"]) if git_config["user_email"]
181
+ end
182
+
183
+ def self.add_repository_mounts!(docker_cmd, config)
184
+ return unless config["repositories"].is_a?(Array) && config["repositories"].any?
185
+
186
+ puts "๐Ÿ“š Configuring #{config['repositories'].count} repositories..."
187
+
188
+ # Build REPOSITORIES_CONTEXT for sessionstart hook (format: "name:path,name:path")
189
+ sorted_repos = config["repositories"].sort_by { |r| r["position"] || 0 }
190
+ repos_context = sorted_repos.map do |repo|
191
+ repo_name = repo["name"]
192
+ sandbox_path = repo["path_in_sandbox"] || "/workspace/#{repo_name}"
193
+ "#{repo_name}:#{sandbox_path}"
194
+ end.join(",")
195
+
196
+ docker_cmd.concat(["-e", "REPOSITORIES_CONTEXT=#{repos_context}"])
197
+
198
+ # Pass repository config via environment variables for container to clone
199
+ repo_config_json = sorted_repos.to_json
200
+ docker_cmd.concat(["-e", "REPOSITORIES_CONFIG=#{repo_config_json}"])
201
+
202
+ sorted_repos.each do |repo|
203
+ repo_name = repo["name"]
204
+ sandbox_path = repo["path_in_sandbox"] || "/workspace/#{repo_name}"
205
+ puts " - #{repo_name}: #{sandbox_path} (will be cloned in container)"
206
+ end
207
+ end
208
+
209
+ def self.add_development_mounts!(docker_cmd)
210
+ # Check for local setup-agent.rb (for development)
211
+ local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
212
+
213
+ # Check for local agent-bridge binaries (for development)
214
+ arch = `uname -m`.strip
215
+ linux_arch = (arch == "x86_64") ? "amd64" : "arm64"
216
+
217
+ # Try multiple possible paths for the binary (built Go binaries in dist/)
218
+ possible_paths = [
219
+ File.join(Dir.pwd, "overrides", "agent-bridge-linux-#{linux_arch}"), # YATFA_SANDBOX/overrides
220
+ File.join(Dir.pwd, "yatfa-public", "dist", "agent-bridge-linux-#{linux_arch}"), # Running from metafolder
221
+ File.join(Dir.pwd, "dist", "agent-bridge-linux-#{linux_arch}"), # Running from yatfa-public
222
+ ]
223
+
224
+ linux_bridge = nil
225
+ possible_paths.each do |path|
226
+ if File.exist?(path)
227
+ linux_bridge = path
228
+ break
229
+ end
230
+ end
231
+
232
+ local_bridge_default = File.join(Dir.pwd, "bin", "agent-bridge")
233
+ local_tmux = File.join(File.dirname(__FILE__), "..", "..", "bin", "agent-bridge-tmux")
234
+
235
+ if File.exist?(local_setup_script)
236
+ puts "๐Ÿ”ง Using local setup-agent.rb for development"
237
+ docker_cmd.concat(["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"])
238
+ end
239
+
240
+ if linux_bridge && File.exist?(linux_bridge)
241
+ puts "๐Ÿ”ง Using local linux binary: #{linux_bridge}"
242
+ docker_cmd.concat(["-v", "#{File.expand_path(linux_bridge)}:/tmp/agent-bridge:ro"])
243
+ elsif File.exist?(local_bridge_default)
244
+ # Check if it's a binary or script
245
+ is_script = File.read(local_bridge_default, 4) == "#!/b"
246
+ if is_script
247
+ puts "โš ๏ธ bin/agent-bridge is a host wrapper script. Please run 'bin/build-bridge' to generate linux binaries."
248
+ else
249
+ puts "๐Ÿ”ง Using local agent-bridge binary"
250
+ docker_cmd.concat(["-v", "#{local_bridge_default}:/tmp/agent-bridge:ro"])
251
+ end
252
+ end
253
+
254
+ if File.exist?(local_tmux)
255
+ docker_cmd.concat(["-v", "#{File.expand_path(local_tmux)}:/tmp/agent-bridge-tmux:ro"])
256
+ end
257
+ end
258
+
259
+ def self.detect_agent_user(container_name)
260
+ # Determine the user to attach as
261
+ # Robust method: find the user running the agent process (tmux or bridge)
262
+ user = `docker exec #{container_name} ps aux | grep "[a]gent-bridge-tmux" | awk '{print $1}' | head -n 1`.strip
263
+
264
+ if user.empty?
265
+ user = `docker exec #{container_name} ps aux | grep "[t]mux new-session" | awk '{print $1}' | head -n 1`.strip
266
+ end
267
+
268
+ if user.empty?
269
+ # Fallback to previous heuristic
270
+ detected_user = `docker exec #{container_name} whoami 2>/dev/null`.strip
271
+ if detected_user == "root" || detected_user.empty?
272
+ uid = Process.uid
273
+ mapped_user = `docker exec #{container_name} getent passwd #{uid} | cut -d: -f1`.strip
274
+ user = mapped_user unless mapped_user.empty?
275
+ else
276
+ user = detected_user
277
+ end
278
+ end
279
+
280
+ if user.empty?
281
+ # Final Fallback
282
+ user = "rails"
283
+ puts "โš ๏ธ Could not detect agent user, defaulting to '#{user}'"
284
+ end
285
+
286
+ user
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module YatfaAgent
6
+ module Config
7
+ def self.load
8
+ rb_config_file = File.join(Dir.pwd, "yatfa.env.rb")
9
+
10
+ unless File.exist?(rb_config_file)
11
+ puts "โŒ Error: yatfa.env.rb not found in current directory"
12
+ puts ""
13
+ puts "Create yatfa.env.rb:"
14
+ puts " {"
15
+ puts " yatfa_url: 'https://yatfa.example.com', # Required"
16
+ puts " # OR legacy name:"
17
+ puts " rails_api_url: 'https://yatfa.example.com/api/v1',"
18
+ puts " # ..."
19
+ puts " # Paste your stripped .env content here:"
20
+ puts " dot_env: <<~ENV"
21
+ puts " STRIPE_KEY=sk_test_..."
22
+ puts " OPENAI_KEY=sk-..."
23
+ puts " ENV"
24
+ puts " }"
25
+ puts " echo 'yatfa.env.rb' >> .gitignore"
26
+ puts " exit 1"
27
+ end
28
+
29
+ puts "โš™๏ธ Loading configuration from yatfa.env.rb"
30
+ config = eval(File.read(rb_config_file), binding, rb_config_file)
31
+
32
+ # Convert symbols to strings for easier handling before JSON normalization
33
+ config = config.transform_keys(&:to_s)
34
+
35
+ # Parse dot_env heredoc if present
36
+ if (dotenv = config["dot_env"])
37
+ config["env"] ||= {}
38
+ # Ensure env is string-keyed
39
+ config["env"] = config["env"].transform_keys(&:to_s)
40
+
41
+ dotenv.each_line do |line|
42
+ line = line.strip
43
+ next if line.empty? || line.start_with?('#')
44
+ k, v = line.split('=', 2)
45
+ next unless k && v
46
+ # Remove surrounding quotes and trailing comments (simple)
47
+ v = v.strip.gsub(/^['"]|['"]$/, '')
48
+ config["env"][k.strip] = v
49
+ end
50
+
51
+ config.delete("dot_env")
52
+ puts "๐ŸŒฟ Parsed dot_env into #{config['env'].size} environment variables"
53
+ end
54
+
55
+ # Normalize per-agent env hashes
56
+ if config["agents"].is_a?(Hash)
57
+ config["agents"].each do |agent_key, agent_config|
58
+ agent_key = agent_key.to_s
59
+ if agent_config.is_a?(Hash) && agent_config["env"].is_a?(Hash)
60
+ agent_config["env"] = agent_config["env"].transform_keys(&:to_s)
61
+ elsif agent_config.is_a?(Hash) && agent_config[:env].is_a?(Hash)
62
+ agent_config["env"] = agent_config[:env].transform_keys(&:to_s)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Normalize symbols to strings for consistency via JSON round-trip
68
+ JSON.parse(JSON.generate(config))
69
+ end
70
+
71
+ def self.yatfa_url(config)
72
+ # Support both new name and legacy name for backwards compatibility
73
+ config["yatfa_url"] || config["yafta_url"] || config["rails_api_url"]
74
+ end
75
+
76
+ def self.api_url(config)
77
+ # Derive API URL from yatfa_url
78
+ yatfa_base = yatfa_url(config)
79
+
80
+ # If yatfa_url already includes /api/v1, return as-is
81
+ return yatfa_base if yatfa_base.end_with?("/api/v1") || yatfa_base.end_with?("/api/")
82
+
83
+ # Otherwise, assume it's the base URL and append /api/v1
84
+ "#{yatfa_base}/api/v1"
85
+ end
86
+
87
+ def self.ws_url(config)
88
+ # Derive WebSocket URL from yatfa_url by replacing protocol and adding /cable endpoint
89
+ api_base = yatfa_url(config)
90
+ "#{api_base.sub(/^https:\/\//, "wss://").sub(/^http:\/\//, "ws://")}/cable"
91
+ end
92
+
93
+ def self.fetch_repositories!(config)
94
+ # Support both old and new config keys
95
+ api_base = api_url(config)
96
+
97
+ return unless api_base
98
+
99
+ # Use to first available agent's MCP API key
100
+ mcp_api_key = nil
101
+ if config["agents"].is_a?(Hash)
102
+ config["agents"].each do |_, agent_config|
103
+ if agent_config.is_a?(Hash) && agent_config["mcp_api_key"]
104
+ mcp_api_key = agent_config["mcp_api_key"]
105
+ break
106
+ end
107
+ end
108
+ end
109
+
110
+ return unless mcp_api_key
111
+
112
+ # First, discover which project we belong to via /whoami
113
+ require "net/http"
114
+ require "json"
115
+ require "uri"
116
+
117
+ whoami_uri = URI("#{api_base}/whoami")
118
+ whoami_response = Net::HTTP.get_response(whoami_uri, { "X-API-Key" => mcp_api_key, "Accept" => "application/json" })
119
+
120
+ if whoami_response.code != "200"
121
+ puts "โš ๏ธ Unable to discover project from /whoami (HTTP #{whoami_response.code})"
122
+ return
123
+ end
124
+
125
+ agent_info = JSON.parse(whoami_response.body)
126
+ project_id = agent_info["project_id"]
127
+ project_name = agent_info["name"] rescue "unknown"
128
+
129
+ puts "๐Ÿ”‘ Authenticated as #{agent_info["name"]} (project #{project_id})"
130
+
131
+ # Then fetch repositories from Rails API (scoped to our project)
132
+ uri = URI("#{api_base}/repositories")
133
+ begin
134
+ response = Net::HTTP.get_response(uri, { "X-API-Key" => mcp_api_key, "Accept" => "application/json" })
135
+
136
+ if response.code == "200"
137
+ repositories = JSON.parse(response.body)
138
+
139
+ if repositories.any?
140
+ config["repositories"] = repositories
141
+
142
+ # Build REPOSITORIES_CONTEXT string: "name:path,name:path"
143
+ repo_context = repositories.map { |r| "#{r['name']}:#{r['path_in_sandbox']}" }.join(",")
144
+ config["env"] ||= {}
145
+ config["env"]["REPOSITORIES_CONTEXT"] = repo_context
146
+
147
+ puts "๐Ÿ“š Loaded #{repositories.count} repositories for project #{project_id} (#{project_name})"
148
+ repositories.each do |repo|
149
+ puts " - #{repo['name']}: #{repo['path_in_sandbox']}"
150
+ end
151
+ end
152
+ else
153
+ puts "โš ๏ธ Unable to fetch repositories (HTTP #{response.code})"
154
+ end
155
+ rescue StandardError => e
156
+ puts "โš ๏ธ Failed to fetch repositories: #{e.message}"
157
+ end
158
+ end
159
+
160
+ def self.image_name(config)
161
+ "yatfa-sandbox"
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module YatfaAgent
6
+ module Docker
7
+ def self.check_dockerfile!
8
+ unless File.exist?("Dockerfile.sandbox")
9
+ puts "โŒ Error: Dockerfile.sandbox not found"
10
+ puts ""
11
+ puts "Please create Dockerfile.sandbox by copying your existing Dockerfile"
12
+ puts "and adding the required agent dependencies."
13
+ puts ""
14
+ puts "See https://github.com/RoM4iK/yatfa-public/blob/main/README.md for instructions."
15
+ exit 1
16
+ end
17
+ end
18
+
19
+ def self.build_image(config)
20
+ check_dockerfile!
21
+
22
+ user_id = `id -u`.strip
23
+ group_id = `id -g`.strip
24
+
25
+ puts "๐Ÿ—๏ธ Building Docker image..."
26
+
27
+ # Handle .dockerignore.sandbox
28
+ dockerignore_sandbox = ".dockerignore.sandbox"
29
+ dockerignore_original = ".dockerignore"
30
+ dockerignore_backup = ".dockerignore.bak"
31
+
32
+ has_sandbox_ignore = File.exist?(dockerignore_sandbox)
33
+ has_original_ignore = File.exist?(dockerignore_original)
34
+
35
+ if has_sandbox_ignore
36
+ puts "๐Ÿ“ฆ Swapping .dockerignore with .dockerignore.sandbox..."
37
+ if has_original_ignore
38
+ FileUtils.mv(dockerignore_original, dockerignore_backup)
39
+ end
40
+ FileUtils.cp(dockerignore_sandbox, dockerignore_original)
41
+ end
42
+
43
+ success = false
44
+ begin
45
+ success = system(
46
+ "docker", "build",
47
+ "--build-arg", "USER_ID=#{user_id}",
48
+ "--build-arg", "GROUP_ID=#{group_id}",
49
+ "-t", Config.image_name(config),
50
+ "-f", "Dockerfile.sandbox",
51
+ "."
52
+ )
53
+ ensure
54
+ if has_sandbox_ignore
55
+ # Restore original state
56
+ FileUtils.rm(dockerignore_original) if File.exist?(dockerignore_original)
57
+ if has_original_ignore
58
+ FileUtils.mv(dockerignore_backup, dockerignore_original)
59
+ end
60
+ puts "๐Ÿงน Restored original .dockerignore"
61
+ end
62
+ end
63
+
64
+ unless success
65
+ puts "โŒ Failed to build Docker image"
66
+ exit 1
67
+ end
68
+
69
+ puts "โœ… Docker image built"
70
+ end
71
+ end
72
+ end