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,1106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+ require 'json'
6
+ require 'pathname'
7
+ require 'yaml'
8
+ require 'digest'
9
+ require 'open-uri'
10
+ require 'unipod/config'
11
+
12
+ module UniPod
13
+ module Server
14
+ # 管理本地缓存和索引仓库
15
+ class CacheManager
16
+ DEFAULT_CACHE_DIR = File.join(Dir.home, 'Library', 'Caches', 'UniPod')
17
+ DEFAULT_INDEX_REPO_DIR = File.join(DEFAULT_CACHE_DIR, 'scoped')
18
+ DEFAULT_PACKAGES_DIR = File.join(DEFAULT_CACHE_DIR, 'packages')
19
+ DEFAULT_TARBALLS_DIR = File.join(DEFAULT_CACHE_DIR, 'tarballs')
20
+
21
+ # 增加Unity和NPM的包类型常量
22
+ PACKAGE_TYPE_UNITY = 'unity'.freeze
23
+ PACKAGE_TYPE_NPM = 'npm'.freeze
24
+
25
+ def initialize(options = {})
26
+ @cache_dir = options[:cache_dir] || detect_cache_dir
27
+ @index_repo_dir = options[:index_repo_dir] || File.join(@cache_dir, 'scoped')
28
+ @packages_dir = File.join(@cache_dir, 'packages')
29
+ @tarballs_dir = File.join(@cache_dir, 'tarballs')
30
+ @verbose = options[:verbose] || false
31
+ @index_repos = []
32
+ @cache = {} # 内存缓存
33
+ @last_update_time = {} # 最后更新时间
34
+ @index_updated = false # 索引是否已更新
35
+ @package_type_cache = {} # 包类型缓存
36
+
37
+ # 确保缓存目录存在
38
+ FileUtils.mkdir_p(@cache_dir)
39
+ FileUtils.mkdir_p(@index_repo_dir)
40
+ FileUtils.mkdir_p(@packages_dir)
41
+ FileUtils.mkdir_p(@tarballs_dir)
42
+
43
+ # 从配置中读取索引库
44
+ load_index_repos_from_config
45
+
46
+ # 初始化索引库
47
+ update_index_repos
48
+ end
49
+
50
+ # 刷新缓存的公开方法,确保在private方法列表之前定义
51
+ def refresh_cache
52
+ UI.puts "刷新包缓存..." if @verbose
53
+ @index_updated = false
54
+ @cache = {} # 清空缓存
55
+ get_all_packages # 重新加载所有包
56
+ UI.puts "包缓存已刷新,共发现#{@cache.size}个包" if @verbose
57
+ true
58
+ end
59
+
60
+ # 从Unity的manifest.json文件中读取索引库配置
61
+ def load_index_repos_from_config
62
+ # 从Config模块获取配置的索引库
63
+ config_repos = Config.index_repos || []
64
+
65
+ # 将配置的索引库添加到索引库列表
66
+ config_repos.each do |repo|
67
+ if repo[:package_index_url]
68
+ @index_repos << repo
69
+ end
70
+ end
71
+
72
+ # 如果没有配置索引库,使用默认索引库
73
+ if @index_repos.empty?
74
+ # 默认Git URL
75
+ default_git_url = 'https://gitee.com/goodunitylib/JoyUnityLibIndex.git'
76
+
77
+ # 从URL中提取仓库名称
78
+ repo_name = extract_repo_name_from_url(default_git_url)
79
+
80
+ @index_repos = [
81
+ {
82
+ name: repo_name,
83
+ url: default_git_url,
84
+ package_index_url: default_git_url
85
+ }
86
+ ]
87
+ end
88
+
89
+ UI.puts "加载了#{@index_repos.size}个索引库配置" if @verbose
90
+ end
91
+
92
+ # 从Git URL中提取仓库名称
93
+ def extract_repo_name_from_url(url)
94
+ # 处理URL格式
95
+ # 示例: https://gitee.com/goodunitylib/JoyUnityLibIndex.git -> JoyUnityLibIndex
96
+ # 示例: git@github.com:user/repo.git -> repo
97
+ # 示例: https://github.com/user/repo -> repo
98
+
99
+ if url.nil? || url.empty?
100
+ return 'default_repo'
101
+ end
102
+
103
+ # 移除尾部的.git(如果有)
104
+ url = url.sub(/\.git\z/, '')
105
+
106
+ # 处理不同格式的URL
107
+ if url.include?('/')
108
+ # 处理https://格式或git://格式
109
+ repo_name = url.split('/').last
110
+ elsif url.include?(':')
111
+ # 处理git@格式
112
+ repo_name = url.split(':').last.split('/').last
113
+ else
114
+ # 无法解析时使用默认名称
115
+ repo_name = 'default_repo'
116
+ end
117
+
118
+ # 确保名称不包含特殊字符
119
+ repo_name.gsub(/[^a-zA-Z0-9_-]/, '_')
120
+ end
121
+
122
+ # 获取包信息
123
+ def get_package_info(package_name)
124
+ # 从缓存中获取
125
+ if @cache.key?(package_name)
126
+ return @cache[package_name]
127
+ end
128
+
129
+ ensure_index_updated
130
+
131
+ UI.puts "获取包信息: #{package_name}" if @verbose
132
+
133
+ # 从索引库中查找包信息
134
+ @index_repos.each do |repo|
135
+ repo_dir = get_repo_dir(repo[:name])
136
+ specs_dir = File.join(repo_dir, 'Specs')
137
+
138
+ # 首先检查Specs目录(CocoaPods结构)
139
+ package_dir = File.join(specs_dir, package_name)
140
+ if Dir.exist?(package_dir)
141
+ # 查找最新版本
142
+ version_dirs = Dir.glob(File.join(package_dir, '*')).select { |d| File.directory?(d) }
143
+ if version_dirs.any?
144
+ # 获取最新版本
145
+ latest_version_dir = version_dirs.sort_by { |d| Gem::Version.new(File.basename(d)) rescue '0.0.0' }.last
146
+ spec_path = File.join(latest_version_dir, 'spec.json')
147
+
148
+ if File.exist?(spec_path)
149
+ begin
150
+ spec = JSON.parse(File.read(spec_path))
151
+ package_info = convert_spec_to_package(spec)
152
+ processed_info = ensure_package_structure(package_info)
153
+ @cache[package_name] = processed_info
154
+ return processed_info
155
+ rescue => e
156
+ UI.puts "解析spec.json错误: #{e.message}" if @verbose
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ # 向后兼容:检查package.json
163
+ package_path = File.join(repo_dir, package_name, 'package.json')
164
+ if File.exist?(package_path)
165
+ begin
166
+ package_info = JSON.parse(File.read(package_path))
167
+ processed_info = ensure_package_structure(package_info)
168
+ @cache[package_name] = processed_info
169
+ return processed_info
170
+ rescue => e
171
+ UI.puts "解析package.json错误: #{e.message}" if @verbose
172
+ end
173
+ end
174
+ end
175
+
176
+ nil
177
+ end
178
+
179
+ # 获取所有包的信息
180
+ def get_all_packages
181
+ UI.puts 'CacheManager: 获取所有包' if @verbose
182
+
183
+ # 检查索引是否已更新且缓存不为空
184
+ if is_index_updated? && !@cache.empty?
185
+ UI.puts "CacheManager: 返回缓存中的 #{@cache.size} 个包" if @verbose
186
+ # 确保返回格式化的结果
187
+ return format_packages_response(@cache.values)
188
+ end
189
+
190
+ packages = {}
191
+
192
+ # 获取存储库目录中的package.json文件
193
+ package_files = Dir.glob(File.join(@index_repo_dir, '**/package.json'))
194
+ spec_files = Dir.glob(File.join(@index_repo_dir, '**/spec.json'))
195
+
196
+ UI.puts "CacheManager: 找到 #{package_files.size} 个package.json文件" if @verbose
197
+ UI.puts "CacheManager: 找到 #{spec_files.size} 个spec.json文件" if @verbose
198
+
199
+ # 处理所有找到的package.json文件
200
+ package_files.each do |package_file|
201
+ begin
202
+ package_data = JSON.parse(File.read(package_file))
203
+ package_name = package_data['name']
204
+
205
+ if package_name.nil? || package_name.empty?
206
+ UI.puts "CacheManager: 忽略没有名称的包: #{package_file}" if @verbose
207
+ next
208
+ end
209
+
210
+ UI.puts "CacheManager: 处理包 #{package_name}" if @verbose
211
+ packages[package_name] = package_data
212
+
213
+ rescue JSON::ParserError => e
214
+ UI.puts "CacheManager: 解析错误 #{package_file}: #{e.message}" if @verbose
215
+ rescue => e
216
+ UI.puts "CacheManager: 处理 #{package_file} 时出错: #{e.message}" if @verbose
217
+ end
218
+ end
219
+
220
+ # 处理所有找到的spec.json文件
221
+ spec_files.each do |spec_file|
222
+ begin
223
+ spec_data = JSON.parse(File.read(spec_file))
224
+ package_name = spec_data['name']
225
+
226
+ if package_name.nil? || package_name.empty?
227
+ UI.puts "CacheManager: 忽略没有名称的规格: #{spec_file}" if @verbose
228
+ next
229
+ end
230
+
231
+ if packages.key?(package_name)
232
+ # 合并spec.json中的信息到现有包
233
+ packages[package_name] = packages[package_name].merge(spec_data)
234
+ UI.puts "CacheManager: 合并规格到包 #{package_name}" if @verbose
235
+ else
236
+ # 添加新包
237
+ packages[package_name] = spec_data
238
+ UI.puts "CacheManager: 添加规格作为新包 #{package_name}" if @verbose
239
+ end
240
+
241
+ rescue JSON::ParserError => e
242
+ UI.puts "CacheManager: 解析错误 #{spec_file}: #{e.message}" if @verbose
243
+ rescue => e
244
+ UI.puts "CacheManager: 处理 #{spec_file} 时出错: #{e.message}" if @verbose
245
+ end
246
+ end
247
+
248
+ # 如果没有找到包,添加示例包以确保功能正常运行
249
+ if packages.empty?
250
+ UI.puts "CacheManager: 警告 - 未找到有效的包,创建示例包" if @verbose
251
+ # 创建并添加示例包
252
+ example_package = create_example_package
253
+ packages[example_package['name']] = example_package
254
+ UI.puts "CacheManager: 添加了示例包: #{example_package['name']}" if @verbose
255
+ end
256
+
257
+ # 更新缓存
258
+ @cache = packages
259
+ @index_updated = true
260
+
261
+ # 使用format_packages_response格式化返回结果
262
+ formatted_result = format_packages_response(@cache.values)
263
+ UI.puts "CacheManager: 返回 #{formatted_result['objects'].size} 个包" if @verbose
264
+
265
+ return formatted_result
266
+ end
267
+
268
+ # 获取包的版本信息
269
+ def get_package_versions(package_name)
270
+ UI.puts "获取包版本信息: #{package_name}" if @verbose
271
+
272
+ package_info = get_package_info(package_name)
273
+ return nil unless package_info
274
+
275
+ versions = {}
276
+ package_info['versions'].each do |version, info|
277
+ versions[version] = ensure_package_structure(info)
278
+ end
279
+
280
+ {
281
+ 'name' => package_name,
282
+ 'versions' => versions,
283
+ 'time' => {
284
+ 'modified' => Time.now.iso8601,
285
+ 'created' => Time.now.iso8601
286
+ },
287
+ 'dist-tags' => package_info['dist-tags'] || { 'latest' => package_info['version'] }
288
+ }
289
+ end
290
+
291
+ # 获取包的压缩文件
292
+ def get_package_tarball(package_name, tarball_name)
293
+ UI.puts "获取包压缩文件: #{package_name} - #{tarball_name}" if @verbose
294
+
295
+ # 查找缓存的压缩文件
296
+ tarball_path = File.join(@tarballs_dir, package_name, tarball_name)
297
+ if File.exist?(tarball_path)
298
+ UI.puts "使用缓存的压缩文件: #{tarball_path}" if @verbose
299
+ return tarball_path
300
+ end
301
+
302
+ UI.puts "缓存中没有找到压缩文件,尝试从Git仓库构建" if @verbose
303
+
304
+ # 获取包信息
305
+ package_info = get_package_info(package_name)
306
+ unless package_info
307
+ UI.puts "未找到包信息: #{package_name}" if @verbose
308
+ return nil
309
+ end
310
+
311
+ # 检查是否有Git仓库信息
312
+ if package_info['git'] && package_info['git']['url']
313
+ git_url = package_info['git']['url']
314
+ git_tag = package_info['git']['tag'] || package_info['version']
315
+ git_branch = package_info['git']['branch'] || 'main'
316
+
317
+ UI.puts "从Git仓库构建压缩包: #{git_url} (tag: #{git_tag}, branch: #{git_branch})" if @verbose
318
+
319
+ # 调用克隆并构建包的方法
320
+ if clone_and_build_package(package_name, git_url, git_tag, git_branch)
321
+ # 检查是否成功生成压缩文件
322
+ if File.exist?(tarball_path)
323
+ UI.puts "成功构建压缩包: #{tarball_path}" if @verbose
324
+ return tarball_path
325
+ end
326
+ end
327
+ end
328
+
329
+ # 如果还是没有找到,尝试从索引库中查找现有的tgz文件
330
+ @index_repos.each do |repo|
331
+ repo_dir = get_repo_dir(repo[:name])
332
+ pkg_dir = File.join(repo_dir, package_name)
333
+
334
+ # 检查是否有对应的压缩文件
335
+ tarball_paths = Dir.glob(File.join(pkg_dir, "*.tgz"))
336
+ if !tarball_paths.empty?
337
+ # 保存到缓存
338
+ FileUtils.mkdir_p(File.dirname(tarball_path))
339
+ FileUtils.cp(tarball_paths.first, tarball_path)
340
+ return tarball_path
341
+ end
342
+ end
343
+
344
+ nil
345
+ end
346
+
347
+ # 搜索包
348
+ def search_packages(query)
349
+ # 确保索引是最新的
350
+ ensure_index_updated
351
+
352
+ UI.puts "搜索包: #{query}" if @verbose
353
+
354
+ # 获取所有包信息
355
+ all_packages = get_all_packages
356
+
357
+ # 如果没有查询,返回所有包
358
+ if query.nil? || query.empty?
359
+ return all_packages
360
+ end
361
+
362
+ # 过滤包
363
+ filtered_packages = all_packages['objects'].select do |obj|
364
+ package = obj['package']
365
+ name_matches_query?(package['name'], query) ||
366
+ (package['description'] && package['description'].downcase.include?(query.downcase)) ||
367
+ (package['keywords'] && package['keywords'].any? { |k| k.downcase.include?(query.downcase) })
368
+ end
369
+
370
+ # 为空结果时尝试更灵活的匹配
371
+ if filtered_packages.empty?
372
+ # 尝试作用域匹配
373
+ if query.include?('.')
374
+ scope = query.split('.').first
375
+ filtered_packages = all_packages['objects'].select do |obj|
376
+ package = obj['package']
377
+ package['name'].start_with?(scope)
378
+ end
379
+ end
380
+
381
+ # 尝试前缀匹配
382
+ if filtered_packages.empty?
383
+ filtered_packages = all_packages['objects'].select do |obj|
384
+ package = obj['package']
385
+ package['name'].include?(query.downcase)
386
+ end
387
+ end
388
+ end
389
+
390
+ {
391
+ 'objects' => filtered_packages,
392
+ 'total' => filtered_packages.size,
393
+ 'time' => all_packages['time']
394
+ }
395
+ end
396
+
397
+ private
398
+
399
+ # 检查包名是否匹配查询条件
400
+ def name_matches_query?(name, query)
401
+ # 标准化名称和查询
402
+ name_lower = name.downcase
403
+ query_lower = query.downcase
404
+
405
+ # 直接匹配
406
+ return true if name_lower == query_lower
407
+
408
+ # 处理作用域名称匹配
409
+ if query_lower.include?('.')
410
+ # Unity包格式: com.company.package
411
+ parts = query_lower.split('.')
412
+ return true if name_lower == parts.join('.')
413
+
414
+ # 匹配前缀
415
+ return true if name_lower.start_with?(query_lower)
416
+ end
417
+
418
+ # 部分匹配
419
+ name_lower.include?(query_lower)
420
+ end
421
+
422
+ def detect_cache_dir
423
+ # 使用 Config 类提供的缓存目录,确保所有系统统一
424
+ UniPod::Config.cache_dir
425
+ end
426
+
427
+ def init_cache_dirs
428
+ FileUtils.mkdir_p(@index_repo_dir)
429
+ FileUtils.mkdir_p(@packages_dir)
430
+ FileUtils.mkdir_p(@tarballs_dir)
431
+ end
432
+
433
+ # 获取当前缓存目录
434
+ def cache_dir
435
+ detect_cache_dir
436
+ end
437
+
438
+ # 获取索引仓库目录
439
+ def scoped_dir
440
+ File.join(cache_dir, 'scoped')
441
+ end
442
+
443
+ # 获取包仓库目录
444
+ def packages_dir
445
+ File.join(cache_dir, 'packages')
446
+ end
447
+
448
+ # 获取压缩文件目录
449
+ def tarballs_dir
450
+ File.join(cache_dir, 'tarballs')
451
+ end
452
+
453
+ def update_index_repos
454
+ # 如果最后更新时间在1小时内,跳过
455
+ if @index_updated
456
+ return true
457
+ end
458
+
459
+ UI.puts "更新索引库" if @verbose
460
+
461
+ success = true
462
+ @index_repos.each do |repo|
463
+ success = success && update_index_repo(repo)
464
+ end
465
+
466
+ if @index_repos.empty?
467
+ # 如果没有配置索引库,创建一个默认目录
468
+ UI.puts "没有配置索引库,创建默认目录结构" if @verbose
469
+
470
+ # 获取默认仓库名称
471
+ default_git_url = 'https://gitee.com/goodunitylib/JoyUnityLibIndex.git'
472
+ default_repo_name = extract_repo_name_from_url(default_git_url)
473
+
474
+ FileUtils.mkdir_p(File.join(@index_repo_dir, default_repo_name))
475
+
476
+ # 获取包信息
477
+ dirs = Dir.glob(File.join(@index_repo_dir, '**/package.json'))
478
+ dirs.each do |d|
479
+ UI.puts "发现包: #{d}" if @verbose
480
+ end
481
+ end
482
+
483
+ @index_updated = success
484
+ get_all_packages if success # 预加载所有包信息
485
+ success
486
+ end
487
+
488
+ def update_index_repo(repo)
489
+ repo_name = repo[:name]
490
+ repo_url = repo[:package_index_url] || repo[:url]
491
+ repo_dir = get_repo_dir(repo_name)
492
+
493
+ # 检查最后更新时间
494
+ last_update = @last_update_time[repo_name]
495
+ if last_update && (Time.now - last_update) < 3600 # 1小时
496
+ UI.puts "索引库 #{repo_name} 已在1小时内更新,跳过" if @verbose
497
+ return true
498
+ end
499
+
500
+ UI.puts "更新索引库: #{repo_name} - #{repo_url}" if @verbose
501
+
502
+ begin
503
+ # 如果目录不存在,克隆仓库
504
+ if !Dir.exist?(repo_dir)
505
+ UI.puts "克隆索引库: #{repo_url} 到 #{repo_dir}" if @verbose
506
+
507
+ # 使用git克隆
508
+ clone_cmd = "git clone #{repo_url} #{repo_dir}"
509
+ system(clone_cmd)
510
+
511
+ unless $?.success?
512
+ UI.error "克隆索引库失败: #{repo_url}"
513
+ return false
514
+ end
515
+ else
516
+ # 更新仓库
517
+ UI.puts "更新索引库: #{repo_dir}" if @verbose
518
+
519
+ # 切换到仓库目录
520
+ Dir.chdir(repo_dir) do
521
+ # 拉取更新
522
+ pull_cmd = "git pull"
523
+ system(pull_cmd)
524
+
525
+ unless $?.success?
526
+ UI.error "更新索引库失败: #{repo_dir}"
527
+ return false
528
+ end
529
+ end
530
+ end
531
+
532
+ # 更新最后更新时间
533
+ @last_update_time[repo_name] = Time.now
534
+
535
+ true
536
+ rescue => e
537
+ UI.error "更新索引库时出错: #{e.message}"
538
+ false
539
+ end
540
+ end
541
+
542
+ def find_package_file(package_name)
543
+ # 首先查找spec.json文件
544
+ spec_files = Dir.glob(File.join(@index_repo_dir, "*/Specs/#{package_name}/**/spec.json"))
545
+ return spec_files.first if spec_files.any?
546
+
547
+ # 如果没有找到spec.json,查找package.json
548
+ Dir.glob(File.join(@index_repo_dir, "*/#{package_name}/**/package.json")).first
549
+ end
550
+
551
+ def convert_spec_to_package(spec_info)
552
+ # 将spec.json格式转换为标准的package.json格式
553
+ package_info = spec_info.dup
554
+
555
+ # 确保有displayName字段(Unity需要)
556
+ package_info['displayName'] ||= humanize_package_name(package_info['name'])
557
+
558
+ # 确保有unity字段
559
+ package_info['unity'] ||= '2019.4'
560
+
561
+ # 添加Unity特定字段
562
+ package_info['unity'] ||= '2019.4'
563
+ package_info['type'] ||= 'library'
564
+
565
+ # 添加versions字段
566
+ package_info['versions'] = {
567
+ spec_info['version'] => package_info.dup
568
+ }
569
+
570
+ # 添加repository字段
571
+ if spec_info['git'] && spec_info['git']['url']
572
+ package_info['repository'] = {
573
+ 'type' => 'git',
574
+ 'url' => spec_info['git']['url']
575
+ }
576
+ end
577
+
578
+ # 添加dist标签
579
+ tarball_name = "#{package_info['name'].gsub('/', '-')}-#{package_info['version']}.tgz"
580
+ package_info['dist'] = {
581
+ 'tarball' => "http://localhost:7748/#{package_info['name']}/-/#{tarball_name}"
582
+ }
583
+
584
+ package_info
585
+ end
586
+
587
+ # 确保包信息具有Unity包管理器需要的结构
588
+ def ensure_package_structure(package_info)
589
+ return {} unless package_info
590
+
591
+ # 检测包类型
592
+ package_type = detect_package_type(package_info)
593
+
594
+ # 复制包信息,确保不修改原始对象
595
+ info = package_info.clone
596
+
597
+ # 添加必要的字段
598
+ info['name'] ||= 'unknown'
599
+ info['version'] ||= '1.0.0'
600
+ info['description'] ||= ''
601
+ info['author'] ||= { 'name' => 'Unknown' }
602
+ info['keywords'] ||= []
603
+ info['maintainers'] ||= [{ 'name' => info['author']['name'] || 'Unknown' }]
604
+
605
+ # 处理Unity特定字段
606
+ if package_type == PACKAGE_TYPE_UNITY
607
+ # 对于Unity包,确保有正确的格式
608
+ info['displayName'] ||= humanize_package_name(info['name'])
609
+ info['unity'] ||= '*'
610
+
611
+ # 确保dependencies格式正确
612
+ if info['dependencies']
613
+ info['dependencies'].each do |k, v|
614
+ # 确保版本格式是Unity兼容的
615
+ info['dependencies'][k] = ensure_unity_version_format(v)
616
+ end
617
+ end
618
+ end
619
+
620
+ # 处理版本信息
621
+ if !info['versions'] || info['versions'].empty?
622
+ version = info['version']
623
+ info['versions'] = {
624
+ version => {
625
+ 'name' => info['name'],
626
+ 'version' => version,
627
+ 'description' => info['description'],
628
+ 'displayName' => info['displayName'],
629
+ 'author' => info['author'],
630
+ 'dependencies' => info['dependencies'] || {},
631
+ 'keywords' => info['keywords'],
632
+ 'dist' => {
633
+ 'shasum' => '',
634
+ 'tarball' => "./#{info['name']}/#{info['name']}-#{version}.tgz"
635
+ }
636
+ }
637
+ }
638
+ end
639
+
640
+ # 确保dependencies字段在每个版本中都存在
641
+ info['versions'].each do |version, version_info|
642
+ version_info['dependencies'] ||= {}
643
+
644
+ # 处理Unity特定字段
645
+ if package_type == PACKAGE_TYPE_UNITY
646
+ version_info['displayName'] ||= info['displayName'] || humanize_package_name(info['name'])
647
+ version_info['unity'] ||= info['unity'] || '*'
648
+
649
+ # 确保有特定的Unity字段
650
+ version_info['unity'] ||= info['unity'] || '*'
651
+ version_info['unityRelease'] ||= info['unityRelease'] || ''
652
+
653
+ # 确保dependencies格式正确
654
+ if version_info['dependencies']
655
+ version_info['dependencies'].each do |k, v|
656
+ # 确保版本格式是Unity兼容的
657
+ version_info['dependencies'][k] = ensure_unity_version_format(v)
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ # 确保dist-tags字段存在
664
+ info['dist-tags'] ||= { 'latest' => info['version'] || info['versions'].keys.sort.last }
665
+
666
+ info
667
+ end
668
+
669
+ # 将包名转换为更友好的显示名称
670
+ def humanize_package_name(name)
671
+ return name unless name
672
+
673
+ # 处理Unity包名格式 (com.company.package)
674
+ if name.include?('.')
675
+ parts = name.split('.')
676
+ if parts.size > 1
677
+ # 使用最后一部分作为显示名称
678
+ display_name = parts.last
679
+ # 将camelCase转换为有空格的形式
680
+ display_name = display_name.gsub(/([A-Z])/, ' \1').strip
681
+ # 首字母大写
682
+ return display_name.capitalize
683
+ end
684
+ end
685
+
686
+ # 处理NPM包名格式 (@scope/package)
687
+ if name.include?('/')
688
+ parts = name.split('/')
689
+ if parts.size > 1
690
+ # 移除前缀@
691
+ scope = parts.first.sub(/^@/, '')
692
+ package = parts.last
693
+ # 将camelCase转换为有空格的形式
694
+ package = package.gsub(/([A-Z])/, ' \1').strip
695
+ # 首字母大写
696
+ return "#{scope.capitalize} #{package.capitalize}"
697
+ end
698
+ end
699
+
700
+ # 将camelCase或snake_case转换为有空格的形式
701
+ name = name.gsub(/([A-Z])/, ' \1').gsub('_', ' ').strip
702
+ name.capitalize
703
+ end
704
+
705
+ # 确保版本号符合Unity要求
706
+ def ensure_unity_version_format(version)
707
+ return '*' if version.nil? || version.empty?
708
+
709
+ # 处理特殊值
710
+ return version if ['*', 'latest'].include?(version)
711
+
712
+ # 如果是纯数字或常规版本号,添加^前缀
713
+ if version =~ /^\d/ && !version.start_with?('^', '~', '>', '<', '=')
714
+ return "^#{version}"
715
+ end
716
+
717
+ version
718
+ end
719
+
720
+ def clone_and_build_package(package_name, git_url, git_tag = nil, git_branch = nil)
721
+ UI.puts "克隆并构建包: #{package_name} (#{git_url})" if @verbose
722
+
723
+ # 创建包目录
724
+ package_dir = File.join(@packages_dir, package_name)
725
+ FileUtils.mkdir_p(package_dir) unless Dir.exist?(package_dir)
726
+
727
+ begin
728
+ # 检查目录是否已经是Git仓库
729
+ if File.directory?(File.join(package_dir, '.git'))
730
+ # 已存在Git仓库,更新它
731
+ UI.puts "更新现有的Git仓库: #{package_dir}" if @verbose
732
+ Dir.chdir(package_dir) do
733
+ # 获取远程URL
734
+ remote_url = `git config --get remote.origin.url`.strip
735
+
736
+ # 如果远程URL与预期不同,重新设置
737
+ if remote_url != git_url
738
+ UI.puts "重新设置远程URL: #{git_url}" if @verbose
739
+ run_git_command("git remote set-url origin #{git_url}")
740
+ end
741
+
742
+ # 获取最新更改
743
+ run_git_command("git fetch --all --quiet")
744
+
745
+ # 尝试切换到指定标签或分支
746
+ if git_tag
747
+ UI.puts "切换到标签: #{git_tag}" if @verbose
748
+ result = run_git_command("git checkout tags/#{git_tag} -f --quiet")
749
+
750
+ # 如果标签不存在,尝试直接使用作为提交ID
751
+ unless result
752
+ UI.puts "标签不存在,尝试作为提交ID: #{git_tag}" if @verbose
753
+ result = run_git_command("git checkout #{git_tag} -f --quiet")
754
+ end
755
+
756
+ # 如果仍然失败,尝试使用分支
757
+ unless result
758
+ UI.puts "使用分支: #{git_branch}" if @verbose
759
+ run_git_command("git checkout #{git_branch} -f --quiet && git pull --quiet")
760
+ end
761
+ elsif git_branch
762
+ UI.puts "切换到分支: #{git_branch}" if @verbose
763
+ run_git_command("git checkout #{git_branch} -f --quiet && git pull --quiet")
764
+ else
765
+ # 如果没有指定标签或分支,使用主分支
766
+ UI.puts "使用默认分支" if @verbose
767
+ run_git_command("git checkout main -f --quiet || git checkout master -f --quiet")
768
+ run_git_command("git pull --quiet")
769
+ end
770
+ end
771
+ else
772
+ # 不存在Git仓库,克隆它
773
+ UI.puts "克隆新的Git仓库: #{git_url} 到 #{package_dir}" if @verbose
774
+
775
+ # 先清空目录(如果非空)
776
+ FileUtils.rm_rf(package_dir)
777
+ FileUtils.mkdir_p(package_dir)
778
+
779
+ # 克隆仓库 - 使用--quiet选项减少输出
780
+ clone_cmd = "git clone --quiet #{git_url} #{package_dir}"
781
+ unless run_git_command(clone_cmd)
782
+ UI.error "克隆Git仓库失败: #{git_url}"
783
+ return false
784
+ end
785
+
786
+ # 切换到指定标签或分支
787
+ Dir.chdir(package_dir) do
788
+ if git_tag
789
+ UI.puts "切换到标签: #{git_tag}" if @verbose
790
+ result = run_git_command("git checkout tags/#{git_tag} --quiet")
791
+
792
+ # 如果标签不存在,尝试直接使用作为提交ID
793
+ unless result
794
+ UI.puts "标签不存在,尝试作为提交ID: #{git_tag}" if @verbose
795
+ result = run_git_command("git checkout #{git_tag} --quiet")
796
+ end
797
+
798
+ # 如果仍然失败,尝试使用分支
799
+ unless result
800
+ UI.puts "使用分支: #{git_branch}" if @verbose
801
+ run_git_command("git checkout #{git_branch} --quiet")
802
+ end
803
+ elsif git_branch
804
+ UI.puts "切换到分支: #{git_branch}" if @verbose
805
+ run_git_command("git checkout #{git_branch} --quiet")
806
+ end
807
+ end
808
+ end
809
+
810
+ # 构建压缩包
811
+ build_package_tarball(package_name, package_dir)
812
+
813
+ true
814
+ rescue => e
815
+ UI.error "克隆并构建包时出错: #{e.message}"
816
+ UI.error e.backtrace.join("\n") if @verbose
817
+ false
818
+ end
819
+ end
820
+
821
+ # 执行Git命令并减少输出
822
+ def run_git_command(cmd)
823
+ # 1. 将命令输出重定向到/dev/null (Unix) 或 NUL (Windows)
824
+ # 2. 使用system返回命令执行结果(成功/失败)
825
+ null_device = RUBY_PLATFORM =~ /mswin|mingw|cygwin/ ? 'NUL' : '/dev/null'
826
+
827
+ # 默认不显示详细输出,除非启用了verbose且明确标记为需要输出
828
+ if @verbose && ENV['UNIPOD_GIT_VERBOSE'] == 'true'
829
+ system(cmd)
830
+ else
831
+ # 重定向标准输出和错误输出
832
+ system("#{cmd} > #{null_device} 2>&1")
833
+ end
834
+ end
835
+
836
+ def build_package_tarball(package_name, package_dir)
837
+ UI.puts "构建包压缩文件: #{package_name}" if @verbose
838
+
839
+ # 确保包目录存在
840
+ unless Dir.exist?(package_dir)
841
+ UI.error "包目录不存在: #{package_dir}"
842
+ return false
843
+ end
844
+
845
+ # 确保package.json文件存在
846
+ package_json_path = File.join(package_dir, 'package.json')
847
+ unless File.exist?(package_json_path)
848
+ UI.error "package.json文件不存在: #{package_json_path}"
849
+ return false
850
+ end
851
+
852
+ # 读取package.json获取版本信息
853
+ begin
854
+ package_json = JSON.parse(File.read(package_json_path))
855
+ version = package_json['version']
856
+
857
+ unless version
858
+ UI.error "package.json中未找到版本信息"
859
+ return false
860
+ end
861
+
862
+ # 创建压缩文件目录
863
+ tarball_dir = File.join(@tarballs_dir, package_name)
864
+ FileUtils.mkdir_p(tarball_dir)
865
+
866
+ # 压缩文件路径
867
+ tarball_filename = "#{package_name.gsub('/', '-')}-#{version}.tgz"
868
+ tarball_path = File.join(tarball_dir, tarball_filename)
869
+
870
+ # 创建临时目录
871
+ temp_dir = File.join(@cache_dir, 'temp', "#{package_name}-#{version}")
872
+ FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
873
+ FileUtils.mkdir_p(temp_dir)
874
+
875
+ # 复制文件到临时目录(排除.git目录)
876
+ UI.puts "复制文件到临时目录: #{temp_dir}" if @verbose
877
+ Dir.glob(File.join(package_dir, '*')).each do |path|
878
+ basename = File.basename(path)
879
+ next if basename == '.git' # 排除.git目录
880
+
881
+ if File.directory?(path)
882
+ FileUtils.cp_r(path, temp_dir)
883
+ else
884
+ FileUtils.cp(path, temp_dir)
885
+ end
886
+ end
887
+
888
+ # 创建压缩文件
889
+ UI.puts "创建压缩文件: #{tarball_path}" if @verbose
890
+
891
+ Dir.chdir(temp_dir) do
892
+ system("tar -czf #{tarball_path} .")
893
+ end
894
+
895
+ # 清理临时目录
896
+ FileUtils.rm_rf(temp_dir)
897
+
898
+ # 检查压缩文件是否创建成功
899
+ if File.exist?(tarball_path)
900
+ UI.puts "压缩文件创建成功: #{tarball_path}" if @verbose
901
+ return true
902
+ else
903
+ UI.error "压缩文件创建失败"
904
+ return false
905
+ end
906
+ rescue => e
907
+ UI.error "构建压缩文件时出错: #{e.message}"
908
+ UI.error e.backtrace.join("\n") if @verbose
909
+ false
910
+ end
911
+ end
912
+
913
+ def verbose?
914
+ # 可以从配置或环境变量获取
915
+ ENV['UNIPOD_VERBOSE'] == 'true'
916
+ end
917
+
918
+ # 计算搜索得分
919
+ def calculate_search_score(name, query)
920
+ return 1.0 if query.empty?
921
+
922
+ name_downcase = name.downcase
923
+ query_downcase = query.downcase
924
+
925
+ if name_downcase == query_downcase
926
+ # 完全匹配
927
+ return 1.0
928
+ elsif name_downcase.start_with?(query_downcase)
929
+ # 前缀匹配
930
+ return 0.9
931
+ elsif name_downcase.include?(query_downcase)
932
+ # 包含匹配
933
+ return 0.7
934
+ else
935
+ # 使用Levenshtein距离计算相似度
936
+ return calculate_similarity(name_downcase, query_downcase)
937
+ end
938
+ end
939
+
940
+ # 计算字符串相似度
941
+ def calculate_similarity(str1, str2)
942
+ len1 = str1.length
943
+ len2 = str2.length
944
+
945
+ # 如果一个字符串为空,距离等于另一个字符串的长度
946
+ return 0.0 if len1 == 0 || len2 == 0
947
+
948
+ # 使用更简单的方法计算相似度
949
+ common_chars = 0
950
+ str2.each_char do |char|
951
+ if str1.include?(char)
952
+ common_chars += 1
953
+ end
954
+ end
955
+
956
+ # 计算相似度得分 (0-1之间)
957
+ similarity = common_chars.to_f / [len1, len2].max
958
+
959
+ # 归一化得分
960
+ [similarity, 0.5].max
961
+ end
962
+
963
+ # 确保索引已更新
964
+ def ensure_index_updated(force_refresh = false)
965
+ if force_refresh
966
+ UI.puts "强制刷新缓存..." if @verbose
967
+ @index_updated = false
968
+ @cache = {} # 清空缓存
969
+ end
970
+ update_index_repos unless @index_updated
971
+ end
972
+
973
+ # 获取索引库目录
974
+ def get_repo_dir(repo_name)
975
+ repo_name = repo_name.gsub(/[^a-zA-Z0-9_-]/, '_')
976
+ File.join(@index_repo_dir, repo_name)
977
+ end
978
+
979
+ # 检测包类型
980
+ def detect_package_type(package_info)
981
+ # 如果已经检测过,直接返回
982
+ if @package_type_cache.key?(package_info['name'])
983
+ return @package_type_cache[package_info['name']]
984
+ end
985
+
986
+ type = PACKAGE_TYPE_NPM
987
+
988
+ # 检查Unity特定字段
989
+ if package_info['name']&.include?('.') || package_info['unity'] || package_info['displayName']
990
+ type = PACKAGE_TYPE_UNITY
991
+ end
992
+
993
+ # 缓存结果
994
+ @package_type_cache[package_info['name']] = type
995
+ type
996
+ end
997
+
998
+ # 转换包信息为返回格式
999
+ def format_packages_response(packages)
1000
+ packages_array = packages.is_a?(Hash) ? packages.values : packages
1001
+
1002
+ objects = packages_array.map do |package|
1003
+ {
1004
+ 'package' => {
1005
+ 'name' => package['name'],
1006
+ 'scope' => package_scope(package['name']),
1007
+ 'version' => package['version'] || package['dist-tags']&.[]('latest') || '1.0.0',
1008
+ 'description' => package['description'] || '',
1009
+ 'keywords' => package['keywords'] || [],
1010
+ 'date' => Time.now.iso8601,
1011
+ 'links' => {},
1012
+ 'author' => package['author'] || { 'name' => 'Unknown' },
1013
+ 'publisher' => package['author'] || { 'name' => 'Unknown' },
1014
+ 'maintainers' => package['maintainers'] || [{ 'name' => 'Unknown' }]
1015
+ },
1016
+ 'score' => {
1017
+ 'final' => 1.0,
1018
+ 'detail' => {
1019
+ 'quality' => 1.0,
1020
+ 'popularity' => 1.0,
1021
+ 'maintenance' => 1.0
1022
+ }
1023
+ },
1024
+ 'searchScore' => 1.0
1025
+ }
1026
+ end
1027
+
1028
+ {
1029
+ 'objects' => objects,
1030
+ 'total' => objects.size,
1031
+ 'time' => Time.now.iso8601
1032
+ }
1033
+ end
1034
+
1035
+ # 获取包的作用域
1036
+ def package_scope(name)
1037
+ return nil unless name
1038
+
1039
+ # 检查Unity包格式
1040
+ if name.include?('.')
1041
+ parts = name.split('.')
1042
+ if parts.size >= 2
1043
+ return parts[0, parts.size - 1].join('.')
1044
+ end
1045
+ end
1046
+
1047
+ # 检查NPM包格式
1048
+ if name.start_with?('@')
1049
+ return name.split('/').first
1050
+ end
1051
+
1052
+ nil
1053
+ end
1054
+
1055
+ # 确保索引已更新
1056
+ def is_index_updated?
1057
+ @index_updated
1058
+ end
1059
+
1060
+ # 创建示例包
1061
+ def create_example_package
1062
+ package_name = 'com.dreamstudio.example'
1063
+ version = '1.0.0'
1064
+
1065
+ # 确保示例包的目录结构存在
1066
+ tarball_dir = File.join(@tarballs_dir, package_name)
1067
+ FileUtils.mkdir_p(tarball_dir)
1068
+
1069
+ # 创建示例包结构
1070
+ {
1071
+ 'name' => package_name,
1072
+ 'displayName' => 'Dream Studio Example Package',
1073
+ 'version' => version,
1074
+ 'unity' => '2019.4',
1075
+ 'description' => '这是一个示例包,用于测试Unity包管理器功能',
1076
+ 'keywords' => ['example', 'test', 'unity'],
1077
+ 'category' => 'Libraries',
1078
+ 'author' => {
1079
+ 'name' => 'DreamStudio',
1080
+ 'email' => 'example@dreamstudio.com',
1081
+ 'url' => 'https://www.dreamstudio.com'
1082
+ },
1083
+ 'maintainers' => [
1084
+ {
1085
+ 'name' => 'DreamStudio',
1086
+ 'email' => 'example@dreamstudio.com'
1087
+ }
1088
+ ],
1089
+ 'dependencies' => {},
1090
+ 'repository' => {
1091
+ 'type' => 'git',
1092
+ 'url' => 'https://github.com/dreamstudio/example-package.git'
1093
+ },
1094
+ 'license' => 'MIT',
1095
+ 'dist' => {
1096
+ 'tarball' => "http://127.0.0.1:7748/#{package_name}/-/#{package_name}-#{version}.tgz",
1097
+ 'type' => 'tgz'
1098
+ }
1099
+ }
1100
+ end
1101
+
1102
+ # 提供缓存目录的访问方法
1103
+ attr_reader :cache_dir
1104
+ end
1105
+ end
1106
+ end