tng 0.3.0 → 0.3.3

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,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tempfile"
5
+ require "rbconfig"
6
+
7
+ module Tng
8
+ module UI
9
+ # Ruby wrapper for calling go-ui binary
10
+ class GoUISession
11
+ def initialize
12
+ @binary_path = find_go_ui_binary
13
+ @running = false
14
+ end
15
+
16
+ def start
17
+ @running = true
18
+ end
19
+
20
+ def stop
21
+ @running = false
22
+ end
23
+
24
+ def running?
25
+ @running
26
+ end
27
+
28
+ # Show main menu and return selected choice
29
+ # Returns: "tests", "stats", "about", "exit"
30
+ def show_menu
31
+ output_file = Tempfile.new(["go_ui_menu", ".json"])
32
+ output_path = output_file.path
33
+ output_file.close
34
+
35
+ begin
36
+ system(@binary_path, "menu", "--output", output_path)
37
+
38
+ return "exit" unless File.exist?(output_path) && File.size(output_path) > 0
39
+
40
+ choice = File.read(output_path).strip
41
+ choice.empty? ? "exit" : choice
42
+ rescue StandardError
43
+ "exit"
44
+ ensure
45
+ File.unlink(output_path) if File.exist?(output_path)
46
+ end
47
+ end
48
+
49
+ # Show test type selection menu
50
+ # Returns: "controller", "model", "service", "other", "back"
51
+ def show_test_type_menu
52
+ output_file = Tempfile.new(["go_ui_test_type", ".json"])
53
+ output_path = output_file.path
54
+ output_file.close
55
+
56
+ begin
57
+ system(@binary_path, "rails-test-menu", "--output", output_path)
58
+
59
+ return "back" unless File.exist?(output_path) && File.size(output_path) > 0
60
+
61
+ choice = File.read(output_path).strip
62
+ choice.empty? ? "back" : choice
63
+ rescue StandardError
64
+ "back"
65
+ ensure
66
+ File.unlink(output_path) if File.exist?(output_path)
67
+ end
68
+ end
69
+
70
+ # Show searchable list view for any component
71
+ # @param title [String] List title (e.g., "Select Controller")
72
+ # @param items [Array<Hash>] List of items with :name and :path keys
73
+ # @return [String] Selected item name or "back"
74
+ def show_list_view(title, items)
75
+ data_json = JSON.generate({ title: title, items: items })
76
+
77
+ output_file = Tempfile.new(["go_ui_list", ".txt"])
78
+ output_path = output_file.path
79
+ output_file.close
80
+
81
+ begin
82
+ system(@binary_path, "list-view", "--data", data_json, "--output", output_path)
83
+
84
+ selected = File.read(output_path).strip
85
+ selected.empty? ? "back" : selected
86
+ ensure
87
+ File.unlink(output_path) if File.exist?(output_path)
88
+ end
89
+ end
90
+
91
+ # Show spinner while executing block
92
+ # @param message [String] Spinner message
93
+ # @yield Block that returns Hash with :success and :message keys
94
+ # @return [Hash] Result from block
95
+ def show_spinner(message, &block)
96
+ control_file = Tempfile.new(["go_ui_spinner", ".json"])
97
+ control_path = control_file.path
98
+ control_file.close
99
+
100
+ # Start spinner in background
101
+ pid = spawn(@binary_path, "spinner", "--message", message, "--control", control_path)
102
+
103
+ begin
104
+ result = yield
105
+
106
+ # Write completion status
107
+ status = {
108
+ status: result&.dig(:success) ? "success" : "error",
109
+ message: result&.dig(:message) || "Done!"
110
+ }
111
+ File.write(control_path, JSON.generate(status))
112
+
113
+ Process.wait(pid)
114
+ result
115
+ rescue StandardError => e
116
+ status = { status: "error", message: e.message }
117
+ File.write(control_path, JSON.generate(status))
118
+ Process.wait(pid)
119
+ raise
120
+ ensure
121
+ File.unlink(control_path) if File.exist?(control_path)
122
+ end
123
+ end
124
+
125
+ # Show progress bar with steps
126
+ # @param title [String] Progress title
127
+ # @yield [ProgressUpdater] Block receives updater and returns Hash with :message and :result
128
+ # @return [Hash] Result from block
129
+ def show_progress(title, &block)
130
+ control_file = Tempfile.new(["go_ui_progress", ".json"])
131
+ control_path = control_file.path
132
+ control_file.close
133
+
134
+ # Start progress in background (inherit TTY)
135
+ pid = spawn(@binary_path, "progress", "--title", title, "--control", control_path)
136
+
137
+ begin
138
+ updater = ProgressUpdater.new(control_path)
139
+ result = yield(updater)
140
+
141
+ # Write completion status
142
+ if result&.dig(:result, :error)
143
+ # Error already written by updater.error
144
+ else
145
+ completion = { type: "complete", message: result&.dig(:message) || "Done!" }
146
+ File.write(control_path, JSON.generate(completion))
147
+ end
148
+
149
+ Process.wait(pid)
150
+ result
151
+ rescue StandardError => e
152
+ error_data = { type: "error", message: "Error: #{e.message}" }
153
+ File.write(control_path, JSON.generate(error_data))
154
+ Process.wait(pid)
155
+ raise
156
+ ensure
157
+ File.unlink(control_path) if File.exist?(control_path)
158
+ end
159
+ end
160
+
161
+ # Show 'no items found' message
162
+ # @param item_type [String] Type of items (e.g., "controllers", "models")
163
+ def show_no_items(item_type)
164
+ system(@binary_path, "no-items", "--type", item_type)
165
+ end
166
+
167
+ # Show user statistics
168
+ # @param stats_data [Hash] Statistics data
169
+ def show_stats(stats_data)
170
+ stats_json = stats_data ? JSON.generate(stats_data) : "{}"
171
+ system(@binary_path, "stats", "--data", stats_json)
172
+ rescue StandardError => e
173
+ puts "Stats error: #{e.message}"
174
+ end
175
+
176
+ # Show about screen
177
+ def show_about
178
+ system(@binary_path, "about")
179
+ rescue StandardError => e
180
+ puts "About error: #{e.message}"
181
+ end
182
+
183
+ # Show configuration error
184
+ # @param missing [Array] List of missing configuration items
185
+ def show_config_error(missing)
186
+ data_json = JSON.generate({ missing: missing })
187
+ system(@binary_path, "config-error", "--data", data_json)
188
+ rescue StandardError => e
189
+ puts "Config error: #{e.message}"
190
+ end
191
+
192
+ # Show post-generation menu
193
+ # @param file_path [String] Generated test file path
194
+ # @param run_command [String] Command to run tests
195
+ # @return [String] Selected choice ("run_tests", "copy_command", "back")
196
+ def show_post_generation_menu(file_path, run_command)
197
+ data_json = JSON.generate({ file_path: file_path, run_command: run_command })
198
+
199
+ output_file = Tempfile.new(["go_ui_post_gen", ".txt"])
200
+ output_path = output_file.path
201
+ output_file.close
202
+
203
+ begin
204
+ system(@binary_path, "post-generation-menu", "--data", data_json, "--output", output_path)
205
+
206
+ choice = File.read(output_path).strip
207
+ choice.empty? ? "back" : choice
208
+ ensure
209
+ File.unlink(output_path) if File.exist?(output_path)
210
+ end
211
+ end
212
+
213
+ # Show clipboard success message
214
+ # @param command [String] Command that was copied
215
+ def show_clipboard_success(command)
216
+ system(@binary_path, "clipboard-success", "--command", command)
217
+ end
218
+
219
+ # Show test results
220
+ # @param title [String] Results title
221
+ # @param passed [Integer] Number of passed tests
222
+ # @param failed [Integer] Number of failed tests
223
+ # @param errors [Integer] Number of errors
224
+ # @param total [Integer] Total number of tests
225
+ # @param results [Array] Optional list of individual test results
226
+ def show_test_results(title, passed, failed, errors, total, results = [])
227
+ data = {
228
+ title: title,
229
+ passed: passed,
230
+ failed: failed,
231
+ errors: errors,
232
+ total: total,
233
+ results: results
234
+ }
235
+
236
+ data_json = JSON.generate(data)
237
+ system(@binary_path, "test-results", "--data", data_json)
238
+ rescue StandardError => e
239
+ puts "Test results error: #{e.message}"
240
+ end
241
+
242
+ # Show authentication error
243
+ # @param message [String] Error message to display
244
+ def show_auth_error(message = "Authentication failed")
245
+ system(@binary_path, "auth-error", "--message", message)
246
+ rescue StandardError => e
247
+ puts "Auth error: #{e.message}"
248
+ end
249
+
250
+ # Show welcome banner
251
+ # @param version [String] Version to display
252
+ def show_banner(version)
253
+ system(@binary_path, "banner", "--version", version)
254
+ rescue StandardError => e
255
+ puts "Banner error: #{e.message}"
256
+ end
257
+
258
+ # Show goodbye message
259
+ def show_goodbye
260
+ system(@binary_path, "goodbye")
261
+ rescue StandardError => e
262
+ puts "Goodbye error: #{e.message}"
263
+ end
264
+
265
+ # Show authentication warning
266
+ # @param missing_items [String] Description of missing items
267
+ # @return [String] "continue" or "cancel"
268
+ def show_auth_warning(missing_items = "")
269
+ output_file = Tempfile.new(["go_ui_auth", ".txt"])
270
+ output_path = output_file.path
271
+ output_file.close
272
+
273
+ data = { missing_items: missing_items }
274
+ data_json = JSON.generate(data)
275
+
276
+ pid = spawn(@binary_path, "auth-warning", "--data", data_json, out: output_path)
277
+ Process.wait(pid)
278
+
279
+ result = File.read(output_path).strip
280
+ result.empty? ? "cancel" : result
281
+ rescue StandardError => e
282
+ puts "Auth warning error: #{e.message}"
283
+ "cancel"
284
+ ensure
285
+ output_file&.unlink
286
+ end
287
+
288
+ # Show missing configuration
289
+ # @param missing_items [Array<String>] List of missing config items
290
+ def show_config_missing(missing_items)
291
+ data = { missing_items: Array(missing_items) }
292
+ data_json = JSON.generate(data)
293
+ system(@binary_path, "config-missing", "--data", data_json)
294
+ rescue StandardError => e
295
+ puts "Config missing error: #{e.message}"
296
+ end
297
+
298
+ # Show post-install message
299
+ # @param version [String] Version to display
300
+ def show_post_install(version)
301
+ system(@binary_path, "post-install", "--version", version)
302
+ rescue StandardError => e
303
+ puts "Post install error: #{e.message}"
304
+ end
305
+
306
+ # Show help
307
+ # @param version [String] Version to display
308
+ def show_help(version)
309
+ system(@binary_path, "help-box", "--version", version)
310
+ rescue StandardError => e
311
+ puts "Help error: #{e.message}"
312
+ end
313
+
314
+ # Show system status
315
+ # @param status [Hash] Status data from testng
316
+ def show_system_status(status)
317
+ data = {
318
+ status: status[:status].to_s,
319
+ message: status[:message],
320
+ local_version: status[:gem_version],
321
+ current_version: status[:current_version],
322
+ user_base_url: status[:user_base_url],
323
+ server_base_url: status[:server_base_url]
324
+ }
325
+ data_json = JSON.generate(data)
326
+ system(@binary_path, "system-status", "--data", data_json)
327
+ rescue StandardError => e
328
+ puts "System status error: #{e.message}"
329
+ end
330
+
331
+ private
332
+
333
+ def find_go_ui_binary
334
+ # Detect platform
335
+ platform = RUBY_PLATFORM
336
+ arch = RbConfig::CONFIG["host_cpu"]
337
+
338
+ # Normalize architecture
339
+ arch = case arch
340
+ when /x86_64|amd64/i then "amd64"
341
+ when /arm64|aarch64/i then "arm64"
342
+ else arch
343
+ end
344
+
345
+ # Determine binary name
346
+ binary_name = if platform.include?("darwin")
347
+ "go-ui-darwin-#{arch}"
348
+ elsif platform.include?("linux")
349
+ "go-ui-linux-#{arch}"
350
+ else
351
+ raise "Unsupported platform: #{platform}"
352
+ end
353
+
354
+ # Search paths
355
+ binary_paths = [
356
+ File.expand_path("../../../binaries/#{binary_name}", __dir__),
357
+ File.expand_path("../../../../binaries/#{binary_name}", __FILE__),
358
+ File.expand_path("binaries/#{binary_name}", Dir.pwd)
359
+ ]
360
+
361
+ binary_paths.each do |path|
362
+ return path if File.exist?(path) && File.executable?(path)
363
+ end
364
+
365
+ raise "go-ui binary not found. Searched: #{binary_paths.join(", ")}"
366
+ end
367
+ end
368
+
369
+ # Helper class for updating progress bar from within a block
370
+ class ProgressUpdater
371
+ def initialize(control_file)
372
+ @control_file = control_file
373
+ @step = 0
374
+ end
375
+
376
+ # Update progress with a new step
377
+ # @param message [String] Progress message
378
+ # @param percent [Integer, nil] Optional percentage (0-100)
379
+ # @param step_increment [Boolean] Whether to increment step counter (default: true)
380
+ # @param explicit_step [Integer, nil] Optional explicit step index
381
+ def update(message, percent = nil, step_increment: true, explicit_step: nil)
382
+ step_idx = if explicit_step
383
+ explicit_step
384
+ else
385
+ step_increment ? @step : (@step > 0 ? @step - 1 : 0)
386
+ end
387
+
388
+ data = { type: "step", step: step_idx, message: message }
389
+ data[:percent] = percent if percent
390
+
391
+ File.write(@control_file, JSON.generate(data))
392
+ @step += 1 if step_increment && explicit_step.nil?
393
+ sleep(0.1) # Give UI time to update
394
+ end
395
+
396
+ # Report an error
397
+ # @param message [String] Error message
398
+ def error(message)
399
+ data = { type: "error", message: message }
400
+ File.write(@control_file, JSON.generate(data))
401
+ sleep(0.1)
402
+ end
403
+ end
404
+ end
405
+ end
406
+
data/lib/tng/utils.rb CHANGED
@@ -20,26 +20,6 @@ module Tng
20
20
  end.join("\n")
21
21
  end
22
22
 
23
- def copy_to_clipboard(text)
24
- # Try to copy to clipboard
25
- if system("which pbcopy > /dev/null 2>&1")
26
- system("echo '#{text}' | pbcopy")
27
- success_msg = @pastel.green("✅ Command copied to clipboard!")
28
- elsif system("which xclip > /dev/null 2>&1")
29
- system("echo '#{text}' | xclip -selection clipboard")
30
- success_msg = @pastel.green("✅ Command copied to clipboard!")
31
- else
32
- success_msg = @pastel.yellow("📋 Copy this command:")
33
- puts center_text(success_msg)
34
- puts center_text(@pastel.bright_white(text))
35
- @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
36
- return
37
- end
38
-
39
- puts center_text(success_msg)
40
- @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
41
- end
42
-
43
23
  def load_rails_environment
44
24
  # Use bundler environment to avoid gem conflicts
45
25
  require "bundler/setup"
@@ -101,22 +81,17 @@ module Tng
101
81
  parsed_response = JSON.parse(test_content)
102
82
 
103
83
  if parsed_response["error"]
104
- puts "❌ API responded with an error: #{parsed_response["error"]}"
105
- return
84
+ return { error: :generation_failed, message: parsed_response["error"] }
106
85
  end
107
86
  # Validate required fields
108
87
  unless parsed_response["file_content"]
109
- puts "API response missing file_content field"
110
- puts "📋 Response keys: #{parsed_response.keys.inspect}"
111
- return
88
+ return { error: :invalid_response, message: "API response missing file_content" }
112
89
  end
113
90
 
114
91
  # Handle both possible field names for file path
115
92
  file_path = parsed_response["test_file_path"] || parsed_response["file_path"] || parsed_response["file_name"] || parsed_response["file"]
116
93
  unless file_path
117
- puts "API response missing test_file_path or file_path field"
118
- puts "📋 Response keys: #{parsed_response.keys.inspect}"
119
- return
94
+ return { error: :invalid_response, message: "API response missing file path" }
120
95
  end
121
96
 
122
97
  begin
@@ -126,9 +101,7 @@ module Tng
126
101
  FileUtils.mkdir_p(File.dirname(file_path))
127
102
  File.write(file_path, parsed_response["file_content"])
128
103
  end
129
- puts "✅ Test generated successfully!"
130
104
  absolute_path = File.expand_path(file_path)
131
- puts "Please review the generated tests at \e]8;;file://#{absolute_path}\e\\#{file_path}\e]8;;\e\\"
132
105
 
133
106
  # Count tests in the generated file
134
107
  test_count = count_tests_in_file(file_path)
data/lib/tng/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tng
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.3"
5
5
  end
data/lib/tng.rb CHANGED
@@ -7,25 +7,38 @@ require_relative "tng/utils"
7
7
  require_relative "tng/api/http_client"
8
8
  require_relative "tng/ui/theme"
9
9
  require_relative "tng/ui/post_install_box"
10
- require_relative "tng/ui/authentication_warning_display"
10
+ require_relative "tng/ui/go_ui_session"
11
11
  require_relative "tng/services/test_generator"
12
12
 
13
13
  require_relative "tng/railtie" if defined?(Rails)
14
14
 
15
15
  begin
16
+ require "rbconfig"
17
+
16
18
  platform = RUBY_PLATFORM
17
- binary_name = if platform.include?("darwin") # macOS
18
- "tng.bundle"
19
- elsif platform.include?("linux")
20
- "tng.so"
21
- else
22
- raise "Unsupported platform: #{platform}"
23
- end
19
+ arch = RbConfig::CONFIG["host_cpu"]
20
+
21
+ # Normalize architecture names
22
+ arch = case arch
23
+ when /x86_64|amd64/i then "x86_64"
24
+ when /arm64|aarch64/i then "arm64"
25
+ else arch
26
+ end
27
+
28
+ # Determine OS and extension
29
+ os = platform.include?("darwin") ? "darwin" : "linux"
30
+ ext = platform.include?("darwin") ? "bundle" : "so"
24
31
 
32
+ # Try loading in order of preference
25
33
  binary_paths = [
26
- File.expand_path("../binaries/#{binary_name}", __dir__), # From gem root
27
- File.expand_path("../../binaries/#{binary_name}", __FILE__), # Alternative path
28
- File.expand_path("binaries/#{binary_name}", __dir__) # Direct from lib
34
+ # Development: simple name (rake dev output)
35
+ File.expand_path("tng/tng.#{ext}", __dir__),
36
+ File.expand_path("../binaries/tng.#{ext}", __dir__),
37
+ # Production: arch-specific names
38
+ File.expand_path("../binaries/tng-#{os}-#{arch}.#{ext}", __dir__),
39
+ # Fallback: current dir
40
+ File.expand_path("binaries/tng.#{ext}", Dir.pwd),
41
+ File.expand_path("binaries/tng-#{os}-#{arch}.#{ext}", Dir.pwd)
29
42
  ]
30
43
 
31
44
  loaded = false
data/tng.gemspec CHANGED
@@ -12,13 +12,13 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "TNG (Test Next Generation) is a Rails gem that automatically generates comprehensive test files by analyzing your Ruby code using static analysis and AI. It supports models, controllers, and services with intelligent test case generation."
13
13
  spec.homepage = "https://tng.sh/"
14
14
  spec.required_ruby_version = ">= 3.1.0"
15
- spec.required_rubygems_version = ">= 3.3.11"
15
+ spec.required_rubygems_version = ">= 3.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
  spec.metadata["homepage_uri"] = spec.homepage
19
- spec.metadata["source_code_uri"] = "https://github.com/tng-sh/tng-rails"
19
+ spec.metadata["source_code_uri"] = "https://github.com/tng-sh/tng-rails-public"
20
20
  spec.license = "Nonstandard"
21
- spec.metadata["license_uri"] = "https://github.com/tng-sh/tng-rails/blob/master/LICENSE.md"
21
+ spec.metadata["license_uri"] = "https://github.com/tng-sh/tng-rails-public/blob/main/LICENSE.md"
22
22
 
23
23
  # Package pre-compiled binaries and exclude the Rust source code.
24
24
  spec.files = Dir.glob("lib/**/*.rb") +
@@ -31,20 +31,15 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ["lib"]
32
32
  spec.extensions = []
33
33
 
34
+ spec.add_dependency "activesupport", ">= 6.0", "< 9.0"
34
35
  spec.add_dependency "httpx", "~> 1.5"
35
36
  spec.add_dependency "openssl", "~> 3.3"
36
37
  spec.add_dependency "pastel", "~> 0.8.0"
37
38
  spec.add_dependency "prism", "~> 1.4.0"
38
39
  spec.add_dependency "rb_sys", "~> 0.9.91"
39
40
  spec.add_dependency "tty-box", "~> 0.7"
40
- spec.add_dependency "tty-file", "~> 0.10"
41
41
  spec.add_dependency "tty-option", "~> 0.3"
42
- spec.add_dependency "tty-progressbar", "~> 0.18.3"
43
- spec.add_dependency "tty-prompt", "~> 0.23"
44
- spec.add_dependency "tty-reader", "~> 0.9"
45
42
  spec.add_dependency "tty-screen", "~> 0.8"
46
- spec.add_dependency "tty-spinner", "~> 0.9.3"
47
- spec.add_dependency "tty-table", "~> 0.12"
48
43
 
49
44
  spec.post_install_message = begin
50
45
  require_relative "lib/tng/ui/post_install_box"