unipod 0.1.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.
@@ -0,0 +1,526 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'socket'
6
+ require 'digest'
7
+ require 'thread'
8
+
9
+ module UniPod
10
+ module Server
11
+ # 处理HTTP请求的处理程序
12
+ class ServerHandler
13
+ attr_reader :cache_manager, :verbose
14
+
15
+ # 使用DownloadHandler的共享锁
16
+ def self.package_build_locks
17
+ DownloadHandler.package_build_locks
18
+ end
19
+
20
+ def self.locks_mutex
21
+ DownloadHandler.locks_mutex
22
+ end
23
+
24
+ def initialize(cache_manager, options = {})
25
+ @cache_manager = cache_manager
26
+ @verbose = options[:verbose] || false
27
+ end
28
+
29
+ # 添加CORS头信息
30
+ def add_cors_headers(res)
31
+ res['Access-Control-Allow-Origin'] = '*'
32
+ res['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
33
+ res['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, Accept, X-Requested-With'
34
+ res['Access-Control-Max-Age'] = '86400' # 24小时
35
+
36
+ # 添加npm兼容的header
37
+ res['npm-notice'] = 'UniPod Registry for Unity Package Manager'
38
+
39
+ # Unity Package Manager 专用头
40
+ res['X-Unity-Package-Manager'] = 'true'
41
+
42
+ # 添加CORS头,确保Unity Package Manager可以正确识别
43
+ res['Access-Control-Expose-Headers'] = 'Content-Type, ETag, Cache-Control, Content-Length, Accept-Ranges, X-Unity-Package-Manager'
44
+
45
+ # 添加缓存控制头
46
+ res['Cache-Control'] = 'max-age=0, no-cache, no-store, must-revalidate'
47
+ res['Pragma'] = 'no-cache'
48
+ res['Expires'] = '0'
49
+ end
50
+
51
+ # 处理搜索请求 - Unity Package Manager API
52
+ def handle_search(req, res, req_id = 0)
53
+ query = req.query.nil? ? '' : (req.query['text'] || '')
54
+ from = req.query.nil? ? 0 : (req.query['from'] || '0').to_i
55
+ size = req.query.nil? ? 20 : (req.query['size'] || '20').to_i
56
+
57
+ log_verbose "[#{req_id}] 处理搜索请求: 查询=#{query}, from=#{from}, size=#{size}"
58
+
59
+ # 动态获取主机地址,优先使用客户端请求的主机地址
60
+ host_address = if req.header['host'] && !req.header['host'].empty?
61
+ req.header['host'].first
62
+ else
63
+ "localhost:7748"
64
+ end
65
+ log_verbose "[#{req_id}] 使用主机地址: #{host_address}"
66
+
67
+ # 初始化空的搜索结果
68
+ search_results = {'objects' => [], 'total' => 0, 'time' => Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')}
69
+
70
+ # 尝试从缓存管理器获取包
71
+ if @cache_manager.respond_to?(:search_packages)
72
+ # 如果缓存管理器支持搜索功能,直接使用
73
+ log_verbose "[#{req_id}] 使用缓存管理器搜索包: #{query}"
74
+ # 修正参数数量, 确保参数顺序正确
75
+ packages_from_cache = @cache_manager.search_packages(query)
76
+
77
+ if packages_from_cache && packages_from_cache.is_a?(Hash) && packages_from_cache['objects']
78
+ search_results = packages_from_cache
79
+ end
80
+ elsif @cache_manager.respond_to?(:get_all_packages)
81
+ # 否则获取全部包并自行过滤
82
+ log_verbose "[#{req_id}] 从缓存管理器获取所有包"
83
+ all_packages = @cache_manager.get_all_packages
84
+
85
+ if all_packages && all_packages.is_a?(Hash) && all_packages['objects']
86
+ log_verbose "[#{req_id}] 成功从缓存管理器获取到 #{all_packages['objects'].size} 个包"
87
+
88
+ # 如果有查询,根据查询条件过滤包
89
+ filtered_packages = all_packages['objects']
90
+ if !query.empty?
91
+ filtered_packages = filtered_packages.select do |pkg_obj|
92
+ pkg = pkg_obj['package']
93
+ pkg['name'].to_s.downcase.include?(query.downcase) ||
94
+ (pkg['displayName'] && pkg['displayName'].to_s.downcase.include?(query.downcase)) ||
95
+ (pkg['description'] && pkg['description'].to_s.downcase.include?(query.downcase)) ||
96
+ (pkg['keywords'] && pkg['keywords'].any? { |k| k.to_s.downcase.include?(query.downcase) })
97
+ end
98
+ end
99
+
100
+ # 更新搜索结果
101
+ search_results['objects'] = filtered_packages
102
+ search_results['total'] = filtered_packages.size
103
+ search_results['time'] = Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
104
+ end
105
+ end
106
+
107
+ # 确保所有包对象都有正确的格式和字段
108
+ if search_results['objects'] && search_results['objects'].is_a?(Array)
109
+ search_results['objects'].each do |package_obj|
110
+ if package_obj.is_a?(Hash) && package_obj['package'] && package_obj['package'].is_a?(Hash)
111
+ pkg = package_obj['package']
112
+
113
+ # 确保包含所需字段
114
+ pkg['name'] ||= ''
115
+ pkg['description'] ||= ''
116
+
117
+ # 添加或更新dist-tags字段
118
+ version = pkg['version'] || '1.0.0'
119
+ pkg['dist-tags'] = {
120
+ 'latest' => version
121
+ }
122
+
123
+ # 添加versions字段
124
+ pkg['versions'] = {
125
+ version => 'latest'
126
+ }
127
+
128
+ # 确保包含时间信息
129
+ pkg['time'] = {
130
+ 'modified' => (Time.now - rand(100000)).utc.iso8601
131
+ } unless pkg['time']
132
+
133
+ # 确保包含readmeFilename
134
+ pkg['readmeFilename'] = 'README.md' unless pkg['readmeFilename']
135
+
136
+ # 确保包对象有flags和local字段
137
+ package_obj['flags'] = {} unless package_obj['flags']
138
+ package_obj['local'] = true unless package_obj.key?('local')
139
+
140
+ # 设置searchScore为客户端期望的值
141
+ package_obj['searchScore'] = 100000 if package_obj['searchScore']
142
+
143
+ # 确保tarball URL使用正确的主机地址
144
+ if pkg['dist'] && pkg['dist']['tarball']
145
+ pkg_name = pkg['name']
146
+ pkg_version = pkg['version'] || version
147
+ tarball_url = "http://#{host_address}/#{pkg_name}/-/#{pkg_name}-#{pkg_version}.tgz"
148
+ pkg['dist']['tarball'] = tarball_url
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ # 处理分页
155
+ if search_results['objects'] && search_results['objects'].is_a?(Array)
156
+ total_packages = search_results['objects'].size
157
+
158
+ # 修复分页问题:Unity Package Manager需要特殊处理
159
+ if from >= total_packages
160
+ # 返回所有包而不是空数组 - Unity需要这个行为
161
+ paginated_packages = search_results['objects']
162
+ else
163
+ # 正常分页
164
+ paginated_packages = search_results['objects'][from, size] || []
165
+ end
166
+
167
+ # 更新搜索结果
168
+ search_results['objects'] = paginated_packages
169
+ search_results['total'] = total_packages
170
+ end
171
+
172
+ # 确保time字段格式正确(HTTP日期格式)
173
+ search_results['time'] = Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
174
+
175
+ log_verbose "[#{req_id}] 返回 #{search_results['objects'].size} 个搜索结果, 总计 #{search_results['total']} 个"
176
+
177
+ # 设置响应
178
+ res.status = 200
179
+ res['Content-Type'] = 'application/json'
180
+ json_response = JSON.generate(search_results)
181
+ res.body = json_response
182
+ puts_formatted_json(json_response, req_id, "搜索响应") if @verbose
183
+ end
184
+
185
+ # 处理包信息请求 - Unity Package Manager API
186
+ def handle_package_info(package_name, res, req_id = 0, req = nil)
187
+ begin
188
+ log_verbose "[#{req_id}] 处理包信息请求: #{package_name}"
189
+
190
+ # 动态获取主机地址,优先使用客户端请求的主机地址
191
+ host_address = if req && req.header['host'] && !req.header['host'].empty?
192
+ req.header['host'].first
193
+ else
194
+ "localhost:7748"
195
+ end
196
+ log_verbose "[#{req_id}] 使用主机地址: #{host_address}"
197
+
198
+ # 初始化变量,确保它们都有默认值
199
+ package_data = nil
200
+ versions = []
201
+ latest_version = nil
202
+
203
+ # 尝试获取包信息
204
+ if @cache_manager.respond_to?(:get_package_info)
205
+ log_verbose "[#{req_id}] 从缓存管理器获取包信息: #{package_name}"
206
+ package_data = @cache_manager.get_package_info(package_name)
207
+
208
+ if package_data && package_data['version']
209
+ log_verbose "[#{req_id}] 获取到包信息: #{package_name}, 版本: #{package_data['version']}"
210
+ latest_version = package_data['version']
211
+ versions << latest_version
212
+ else
213
+ log_verbose "[#{req_id}] 没有获取到包信息或版本号"
214
+ end
215
+ end
216
+
217
+ # 如果没有获取到包信息,返回404
218
+ if versions.empty?
219
+ log_verbose "[#{req_id}] 未找到包: #{package_name}"
220
+ res.status = 404
221
+ res['Content-Type'] = 'application/json'
222
+ error_response = {
223
+ "error" => "Not Found",
224
+ "code" => "PACKAGE_NOT_FOUND",
225
+ "message" => "Package '#{package_name}' not found"
226
+ }
227
+ json_response = JSON.generate(error_response)
228
+ res.body = json_response
229
+ return
230
+ end
231
+
232
+ # 从主版本生成其他可能的版本
233
+ if versions.size == 1
234
+ version_parts = latest_version.split('.')
235
+ if version_parts.size >= 3
236
+ major, minor, patch = version_parts
237
+
238
+ # 添加可能的之前版本
239
+ patch_num = patch.to_i
240
+ if patch_num > 0
241
+ versions << "#{major}.#{minor}.#{patch_num - 1}"
242
+ end
243
+
244
+ if minor.to_i > 0
245
+ versions << "#{major}.#{minor.to_i - 1}.0"
246
+ end
247
+
248
+ # 确保版本唯一且按照版本号排序
249
+ versions = versions.uniq.sort_by { |v| Gem::Version.new(v) rescue "0" }
250
+ latest_version = versions.last
251
+
252
+ log_verbose "[#{req_id}] 生成的版本列表: #{versions.join(', ')}, 最新版本: #{latest_version}"
253
+ end
254
+ end
255
+
256
+ # 构建包信息
257
+ full_package_data = {
258
+ "name" => package_name,
259
+ "versions" => {},
260
+ "time" => {
261
+ "modified" => Time.now.utc.iso8601,
262
+ "created" => (Time.now - 86400).utc.iso8601
263
+ },
264
+ "users" => {},
265
+ "dist-tags" => {
266
+ "latest" => latest_version
267
+ },
268
+ "_rev" => "10-#{Digest::MD5.hexdigest(Time.now.to_s)[0,16]}",
269
+ "_id" => package_name,
270
+ "_attachments" => {},
271
+ "readme" => "# #{package_name}\n\n#{package_data && package_data['description'] ? package_data['description'] : '无描述'}"
272
+ }
273
+
274
+ # 为每个版本生成信息
275
+ versions.each_with_index do |version, idx|
276
+ # 创建版本的基本信息
277
+ version_info = if version == latest_version && package_data
278
+ # 使用获取到的包信息作为基础
279
+ package_data.clone
280
+ else
281
+ # 基于最新版本创建其他版本信息
282
+ {
283
+ 'name' => package_name,
284
+ 'version' => version,
285
+ 'description' => package_data && package_data['description'] ? package_data['description'] : '无描述',
286
+ 'keywords' => package_data && package_data['keywords'] ? package_data['keywords'] : [],
287
+ 'author' => package_data && package_data['author'] ? package_data['author'] : {'name' => 'Unknown', 'email' => 'unknown@example.com'}
288
+ }
289
+ end
290
+
291
+ # 确保版本号正确
292
+ version_info['version'] = version
293
+
294
+ # 添加必要的字段
295
+ version_info['_id'] = "#{package_name}@#{version}"
296
+ version_info['readmeFilename'] = 'README.md' unless version_info['readmeFilename']
297
+ version_info['gitHead'] = Digest::SHA1.hexdigest("#{package_name}-#{version}") unless version_info['gitHead']
298
+ version_info['_nodeVersion'] = '23.9.0' unless version_info['_nodeVersion']
299
+ version_info['_npmVersion'] = '10.9.2' unless version_info['_npmVersion']
300
+ version_info['contributors'] = [] unless version_info['contributors']
301
+
302
+ # 添加或更新dist信息
303
+ version_info['dist'] = {} unless version_info['dist']
304
+
305
+ # 计算shasum
306
+ shasum = get_or_calculate_shasum(package_name, version, version_info, req_id, version == latest_version)
307
+
308
+ # 生成integrity值
309
+ integrity_hash = Digest::SHA512.hexdigest("#{package_name}-#{version}-#{shasum}")
310
+ integrity_chars = integrity_hash[0...40] + rand(10000).to_s.rjust(4, '0')
311
+ integrity_b64 = integrity_chars.tr('0123456789abcdef', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
312
+
313
+ # 更新dist信息
314
+ version_info['dist']['integrity'] = "sha512-#{integrity_b64}"
315
+ version_info['dist']['shasum'] = shasum
316
+ version_info['dist']['tarball'] = "http://#{host_address}/#{package_name}/-/#{package_name}-#{version}.tgz"
317
+
318
+ # 将版本添加到versions对象
319
+ full_package_data['versions'][version] = version_info
320
+
321
+ # 添加时间信息
322
+ time_offset = idx * 600 # 每个版本间隔10分钟
323
+ full_package_data['time'][version] = (Time.now - (86400 - time_offset)).utc.iso8601
324
+ end
325
+
326
+ # 设置响应
327
+ res.status = 200
328
+ res['Content-Type'] = 'application/json'
329
+ json_response = JSON.generate(full_package_data)
330
+ res.body = json_response
331
+ log_verbose "[#{req_id}] 成功生成包信息响应: #{package_name}, 包含 #{versions.size} 个版本"
332
+
333
+ rescue => e
334
+ # 捕获所有异常
335
+ error_message = "处理包信息请求时出错: #{e.message}\n#{e.backtrace.join("\n")}"
336
+ log_verbose "[#{req_id}] 错误: #{error_message}"
337
+
338
+ res.status = 500
339
+ res['Content-Type'] = 'application/json'
340
+ error_response = {
341
+ "error" => "Internal Server Error",
342
+ "code" => "REQUEST_PROCESSING_ERROR",
343
+ "message" => e.message
344
+ }
345
+ json_response = JSON.generate(error_response)
346
+ res.body = json_response
347
+ end
348
+ end
349
+
350
+ # 处理根路由请求
351
+ def handle_root_request(res, req_id = 0)
352
+ # 尝试读取HTML模板
353
+ template_path = File.join(File.dirname(__FILE__), 'index.html')
354
+
355
+ # 设置响应状态和内容类型
356
+ res.status = 200
357
+ res['Content-Type'] = 'text/html'
358
+
359
+ if File.exist?(template_path)
360
+ # 读取index.html文件内容
361
+ html = File.read(template_path)
362
+
363
+ # 获取包列表
364
+ packages_data = []
365
+
366
+ # 尝试从缓存管理器获取包列表
367
+ if @cache_manager.respond_to?(:get_all_packages)
368
+ log_verbose "[#{req_id}] 从缓存管理器获取所有包用于首页展示"
369
+ all_packages = @cache_manager.get_all_packages
370
+
371
+ if all_packages && all_packages['objects'] && all_packages['objects'].is_a?(Array)
372
+ log_verbose "[#{req_id}] 成功从缓存管理器获取到 #{all_packages['objects'].size} 个包"
373
+
374
+ # 转换为前端需要的格式
375
+ packages_data = all_packages['objects'].map do |pkg_obj|
376
+ pkg = pkg_obj['package']
377
+ {
378
+ name: pkg['name'],
379
+ version: pkg['version'],
380
+ displayName: pkg['displayName'] || pkg['name'],
381
+ description: pkg['description'] || '',
382
+ unity: pkg['unity'] || 'unknown',
383
+ author: pkg['author'] || { name: 'Unknown', email: '' },
384
+ keywords: pkg['keywords'] || [],
385
+ category: pkg['category'] || 'Libraries'
386
+ }
387
+ end
388
+ end
389
+ end
390
+
391
+ # 如果没有从缓存获取到包,使用空数组
392
+ if packages_data.empty?
393
+ log_verbose "[#{req_id}] 未从缓存获取到包,返回空数组用于首页展示"
394
+ packages_data = []
395
+ end
396
+
397
+ # 转换为JSON并替换模板中的变量
398
+ packages_json = packages_data.to_json
399
+ html.gsub!('{{PACKAGES_JSON}}', packages_json)
400
+
401
+ # 设置响应体
402
+ res.body = html
403
+ log_verbose "[#{req_id}] 首页展示 #{packages_data.size} 个包"
404
+ else
405
+ # 如果找不到模板文件,返回空白页面
406
+ log_verbose "[#{req_id}] 未找到index.html模板文件,返回空白页面"
407
+ res.body = ''
408
+ end
409
+ end
410
+
411
+ # 处理请求错误
412
+ def handle_request_error(error, res, req_id = 0)
413
+ UI.error "[#{req_id}] 请求处理错误: #{error.message}"
414
+ UI.error error.backtrace.join("\n") if @verbose
415
+
416
+ # 返回500错误
417
+ res.status = 500
418
+ res['Content-Type'] = 'application/json'
419
+ error_json = JSON.generate({
420
+ error: "服务器内部错误",
421
+ message: error.message
422
+ })
423
+ res.body = error_json
424
+ puts_formatted_json(error_json, req_id, "错误响应") if @verbose
425
+ end
426
+
427
+ # 美化JSON并打印输出
428
+ def puts_formatted_json(json_string, req_id, desc = "JSON响应")
429
+ begin
430
+ # 解析JSON字符串为Ruby对象
431
+ json_obj = JSON.parse(json_string)
432
+ # 使用pretty_generate重新生成格式化的JSON
433
+ formatted_json = JSON.pretty_generate(json_obj)
434
+ # 打印格式化后的JSON
435
+ puts "[#{req_id}] #{desc}:"
436
+ puts formatted_json
437
+ rescue => e
438
+ puts "[#{req_id}] 无法格式化JSON: #{e.message}"
439
+ puts json_string
440
+ end
441
+ end
442
+
443
+ private
444
+
445
+ def log_verbose(message)
446
+ UI.puts message if @verbose
447
+ end
448
+
449
+ # 获取本地IP地址
450
+ def get_local_ip
451
+ begin
452
+ Socket.ip_address_list.detect{|intf| intf.ipv4_private?}&.ip_address || 'localhost'
453
+ rescue
454
+ 'localhost'
455
+ end
456
+ end
457
+
458
+ # 获取包锁
459
+ def get_package_lock(package_key)
460
+ self.class.locks_mutex.synchronize do
461
+ self.class.package_build_locks[package_key] ||= Mutex.new
462
+ end
463
+ end
464
+
465
+ # 计算或生成给定包版本的shasum
466
+ def get_or_calculate_shasum(package_name, version, version_info, req_id, is_latest_version = false)
467
+ # 锁定此包版本,防止并发生成
468
+ package_lock = get_package_lock("#{package_name}-#{version}")
469
+
470
+ package_lock.synchronize do
471
+ tarball_filename = "#{package_name}-#{version}.tgz"
472
+ tarball_path = @cache_manager.get_package_tarball(package_name, tarball_filename)
473
+
474
+ # 如果tarball文件已存在,使用实际文件计算shasum
475
+ if tarball_path && File.exist?(tarball_path)
476
+ file_content = File.open(tarball_path, 'rb') { |f| f.read }
477
+ shasum = Digest::SHA1.hexdigest(file_content)
478
+ log_verbose "[#{req_id}] 版本 #{version} 使用实际文件计算的shasum: #{shasum}"
479
+ return shasum
480
+ end
481
+
482
+ # 只有最新版本才尝试构建tarball
483
+ if is_latest_version
484
+ # 如果有Git信息,尝试构建tarball并计算shasum
485
+ if version_info && version_info['git'] && version_info['git']['url']
486
+ git_url = version_info['git']['url']
487
+ git_tag = version_info['git']['tag'] || "v#{version}"
488
+ git_branch = version_info['git']['branch'] || 'main'
489
+
490
+ log_verbose "[#{req_id}] 为最新版本#{version}添加tarball构建任务: #{git_url}, tag: #{git_tag}"
491
+
492
+ # 获取包构建队列单例实例
493
+ builder_queue = PackageBuilderQueue.instance
494
+
495
+ # 设置任务优先级
496
+ # 包信息API触发的构建优先级为0(普通)
497
+ priority = 0 # 普通优先级
498
+
499
+ # 将任务添加到队列,不需要等待结果
500
+ builder_queue.enqueue(package_name, version, git_url, git_tag, git_branch, priority) do |success, result_path|
501
+ if success
502
+ log_verbose "[#{req_id}] 异步包构建成功: #{result_path}"
503
+ else
504
+ log_verbose "[#{req_id}] 异步包构建失败: #{package_name}@#{version}"
505
+ end
506
+ end
507
+
508
+ # 不需要等待构建完成,使用默认值
509
+ log_verbose "[#{req_id}] 任务已加入队列,使用默认shasum"
510
+ else
511
+ log_verbose "[#{req_id}] 包没有Git信息,无法构建: #{package_name}@#{version}"
512
+ end
513
+ else
514
+ # 非最新版本,记录日志
515
+ log_verbose "[#{req_id}] 版本 #{version} 不是最新版本,不加入构建队列"
516
+ end
517
+
518
+ # 如果无法生成文件或计算失败,使用默认值
519
+ shasum = Digest::SHA1.hexdigest("#{package_name}-#{version}")
520
+ log_verbose "[#{req_id}] #{is_latest_version ? '使用默认' : '不生成'}shasum: #{shasum}"
521
+ return shasum
522
+ end
523
+ end
524
+ end
525
+ end
526
+ end
data/lib/unipod/ui.rb ADDED
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rainbow'
4
+ require 'tty-spinner'
5
+ require 'tty-prompt'
6
+ require 'tty-table'
7
+ require 'tty-progressbar'
8
+
9
+ module UniPod
10
+ class UI
11
+ class << self
12
+ def puts(message)
13
+ Kernel.puts(message)
14
+ end
15
+
16
+ def info(message)
17
+ puts Rainbow("ℹ️ #{message}").blue
18
+ end
19
+
20
+ def success(message)
21
+ puts Rainbow("✅ #{message}").green
22
+ end
23
+
24
+ def warning(message)
25
+ puts Rainbow("⚠️ #{message}").yellow
26
+ end
27
+
28
+ def error(message)
29
+ puts Rainbow("❌ #{message}").red
30
+ end
31
+
32
+ def debug(message)
33
+ return unless debug_mode?
34
+ puts Rainbow("🔍 #{message}").magenta
35
+ end
36
+
37
+ def title(message)
38
+ width = terminal_width
39
+ puts
40
+ puts Rainbow("━" * width).blue
41
+ puts Rainbow(message.center(width)).blue.bright
42
+ puts Rainbow("━" * width).blue
43
+ puts
44
+ end
45
+
46
+ def spinner(message, &block)
47
+ spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots)
48
+ spinner.auto_spin
49
+
50
+ begin
51
+ result = block.call
52
+ spinner.success(Rainbow('(完成)').green)
53
+ result
54
+ rescue => e
55
+ spinner.error(Rainbow("(失败: #{e.message})").red)
56
+ raise e
57
+ end
58
+ end
59
+
60
+ def progress(total, message = "处理中")
61
+ bar = TTY::ProgressBar.new("[:bar] #{message} :percent (:current/:total)",
62
+ total: total, width: terminal_width - 30)
63
+
64
+ if block_given?
65
+ (0...total).each do |i|
66
+ yield i
67
+ bar.advance
68
+ end
69
+ end
70
+
71
+ bar
72
+ end
73
+
74
+ def prompt
75
+ @prompt ||= TTY::Prompt.new
76
+ end
77
+
78
+ def select(message, choices)
79
+ prompt.select(message, choices)
80
+ end
81
+
82
+ def multi_select(message, choices)
83
+ prompt.multi_select(message, choices)
84
+ end
85
+
86
+ def ask(message, default: nil, required: false)
87
+ options = {}
88
+ options[:default] = default if default
89
+ options[:required] = required
90
+
91
+ prompt.ask(message, **options)
92
+ end
93
+
94
+ def yes?(message, default: true)
95
+ prompt.yes?(message, default: default)
96
+ end
97
+
98
+ def no?(message, default: true)
99
+ prompt.no?(message, default: default)
100
+ end
101
+
102
+ def table(headers, rows)
103
+ table = TTY::Table.new(headers, rows)
104
+ puts table.render(:unicode, padding: [0, 1])
105
+ end
106
+
107
+ def format_package(package)
108
+ name = package[:name] || package["name"]
109
+ version = package[:version] || package["version"]
110
+ description = package[:description] || package["description"]
111
+
112
+ "#{Rainbow(name).green.bright} #{Rainbow(version).yellow} - #{description}"
113
+ end
114
+
115
+ def format_package_list(packages)
116
+ packages.each do |package|
117
+ puts format_package(package)
118
+ end
119
+ end
120
+
121
+ def format_package_details(package)
122
+ rows = []
123
+
124
+ # 基本信息
125
+ rows << ["名称", Rainbow(package[:name] || package["name"]).green.bright]
126
+ rows << ["版本", Rainbow(package[:version] || package["version"]).yellow]
127
+ rows << ["描述", package[:description] || package["description"]]
128
+
129
+ # 作者信息
130
+ author = package[:author] || package["author"]
131
+ if author
132
+ author_str = author.is_a?(Hash) ? "#{author[:name]} <#{author[:email]}>" : author.to_s
133
+ rows << ["作者", author_str]
134
+ end
135
+
136
+ # 许可证
137
+ license = package[:license] || package["license"]
138
+ rows << ["许可证", license] if license
139
+
140
+ # 依赖项
141
+ dependencies = package[:dependencies] || package["dependencies"]
142
+ if dependencies && !dependencies.empty?
143
+ deps_str = dependencies.map { |k, v| "#{k} (#{v})" }.join(", ")
144
+ rows << ["依赖项", deps_str]
145
+ end
146
+
147
+ # Unity 特定信息
148
+ unity_version =
149
+ package.dig(:unity, :version) ||
150
+ package.dig("unity", "version") ||
151
+ package[:unity_version] ||
152
+ package["unity_version"]
153
+
154
+ rows << ["Unity 版本", unity_version] if unity_version
155
+
156
+ # 显示表格
157
+ table = TTY::Table.new([nil, nil], rows)
158
+ puts table.render(:unicode, padding: [0, 1], multiline: true)
159
+ end
160
+
161
+ private
162
+
163
+ def debug_mode?
164
+ ENV['UNIPOD_DEBUG'] == 'true'
165
+ end
166
+
167
+ def terminal_width
168
+ require 'io/console'
169
+ IO.console.winsize[1] rescue 80
170
+ end
171
+ end
172
+ end
173
+ end