pindo 5.13.7 → 5.13.10

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +247 -42
  3. data/lib/pindo/command/android/autobuild.rb +104 -31
  4. data/lib/pindo/command/android/autoresign.rb +23 -322
  5. data/lib/pindo/command/android/keystore.rb +7 -130
  6. data/lib/pindo/command/appstore/adhocbuild.rb +52 -15
  7. data/lib/pindo/command/appstore/autobuild.rb +104 -8
  8. data/lib/pindo/command/appstore/autoresign.rb +3 -5
  9. data/lib/pindo/command/ios/autobuild.rb +96 -32
  10. data/lib/pindo/command/ios/build.rb +8 -186
  11. data/lib/pindo/command/jps/media.rb +146 -0
  12. data/lib/pindo/command/jps/upload.rb +49 -21
  13. data/lib/pindo/command/jps.rb +1 -0
  14. data/lib/pindo/command/unity/autobuild.rb +141 -32
  15. data/lib/pindo/command/unity/packpush.rb +5 -8
  16. data/lib/pindo/command/utils/repoinit.rb +0 -2
  17. data/lib/pindo/command/utils/tag.rb +58 -26
  18. data/lib/pindo/command/utils.rb +0 -1
  19. data/lib/pindo/command/web/autobuild.rb +98 -34
  20. data/lib/pindo/command.rb +0 -56
  21. data/lib/pindo/config/build_info_manager.rb +7 -8
  22. data/lib/pindo/module/android/android_config_helper.rb +2 -11
  23. data/lib/pindo/module/appselect.rb +15 -41
  24. data/lib/pindo/module/appstore/itcapp_helper.rb +3 -6
  25. data/lib/pindo/module/build/build_helper.rb +28 -18
  26. data/lib/pindo/module/build/git_repo_helper.rb +284 -405
  27. data/lib/pindo/module/cert/pem_helper.rb +3 -6
  28. data/lib/pindo/module/pgyer/pgyerhelper.rb +193 -25
  29. data/lib/pindo/module/task/model/appstore/appstore_task.rb +5 -0
  30. data/lib/pindo/module/task/model/build/android_build_adhoc_task.rb +13 -187
  31. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +36 -34
  32. data/lib/pindo/module/task/model/build/android_build_gplay_task.rb +13 -187
  33. data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +9 -6
  34. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +9 -6
  35. data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +37 -32
  36. data/lib/pindo/module/task/model/build/web_build_dev_task.rb +7 -5
  37. data/lib/pindo/module/task/model/build_task.rb +8 -11
  38. data/lib/pindo/module/task/model/git/git_commit_task.rb +118 -0
  39. data/lib/pindo/module/task/model/git/git_tag_task.rb +125 -0
  40. data/lib/pindo/module/task/model/git_task.rb +75 -0
  41. data/lib/pindo/module/task/model/jps/jps_message_task.rb +178 -0
  42. data/lib/pindo/module/task/model/{jps_resign_task.rb → jps/jps_resign_task.rb} +14 -23
  43. data/lib/pindo/module/task/model/jps/jps_upload_media_task.rb +248 -0
  44. data/lib/pindo/module/task/model/{jps_upload_task.rb → jps/jps_upload_task.rb} +39 -94
  45. data/lib/pindo/module/task/model/jps_task.rb +43 -0
  46. data/lib/pindo/module/task/model/{ipa_local_resign_task.rb → resign/ipa_local_resign_task.rb} +7 -2
  47. data/lib/pindo/module/task/model/unity/unity_config_task.rb +103 -0
  48. data/lib/pindo/module/task/model/{unity_export_task.rb → unity/unity_export_task.rb} +76 -78
  49. data/lib/pindo/module/task/model/unity/unity_update_task.rb +95 -0
  50. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +156 -0
  51. data/lib/pindo/module/task/model/unity_task.rb +118 -0
  52. data/lib/pindo/module/task/pindo_task.rb +101 -1
  53. data/lib/pindo/module/task/task_manager.rb +29 -24
  54. data/lib/pindo/module/unity/nuget_helper.rb +7 -7
  55. data/lib/pindo/module/unity/unity_command_helper.rb +188 -0
  56. data/lib/pindo/module/unity/unity_env_helper.rb +208 -0
  57. data/lib/pindo/module/unity/unity_helper.rb +189 -746
  58. data/lib/pindo/module/unity/unity_proc_helper.rb +390 -0
  59. data/lib/pindo/options/core/global_options_state.rb +96 -26
  60. data/lib/pindo/options/core/option_configuration.rb +3 -0
  61. data/lib/pindo/options/core/option_item.rb +36 -0
  62. data/lib/pindo/options/groups/build_options.rb +23 -6
  63. data/lib/pindo/options/groups/git_options.rb +115 -0
  64. data/lib/pindo/options/groups/jps_options.rb +7 -0
  65. data/lib/pindo/options/groups/option_group.rb +15 -0
  66. data/lib/pindo/options/groups/unity_options.rb +49 -0
  67. data/lib/pindo/options/options.rb +2 -0
  68. data/lib/pindo/version.rb +2 -2
  69. metadata +25 -14
  70. data/lib/pindo/base/githelper.rb +0 -686
  71. data/lib/pindo/base/pindocontext.rb +0 -602
  72. data/lib/pindo/command/utils/feishu.rb +0 -134
  73. data/lib/pindo/module/build/version_helper.rb +0 -146
  74. data/lib/pindo/module/task/model/git_tag_task.rb +0 -80
@@ -0,0 +1,390 @@
1
+ module Pindo
2
+ module Unity
3
+ # Unity 进程管理助手
4
+ # 负责 Unity 进程的检测、管理和清理
5
+ # 所有方法均为类方法,无需实例化
6
+ class UnityProcHelper
7
+
8
+ # 构建前检查 Unity 进程(交互式)
9
+ # @param unity_exe_full_path [String] Unity 可执行文件路径
10
+ # @param project_path [String] Unity 项目路径
11
+ def self.check_unity_processes(unity_exe_full_path: nil, project_path: nil)
12
+ # 如果没有提供路径参数,直接跳过Unity进程检查
13
+ if unity_exe_full_path.nil? && project_path.nil?
14
+ return
15
+ end
16
+
17
+ # 检查是否有Unity进程在运行
18
+ unity_processes = `ps aux | grep -i unity | grep -v grep`.strip
19
+
20
+ if !unity_processes.empty?
21
+ # 解析Unity进程信息,传入Unity路径和项目路径进行精确匹配
22
+ unity_pids = parse_unity_processes(unity_processes, unity_exe_full_path: unity_exe_full_path, project_path: project_path)
23
+
24
+ # 过滤掉僵尸进程和无效进程
25
+ unity_pids = filter_valid_unity_processes(unity_pids)
26
+
27
+ if unity_pids.any?
28
+ puts "⚠️ 检测到与当前项目相关的 Unity 进程正在运行:"
29
+ # 只显示真正的Unity进程,不显示 pindo 相关进程
30
+ unity_pids.each_with_index do |process_info, index|
31
+ puts " #{index + 1}. PID: #{process_info[:pid]}, 命令: #{process_info[:command]}"
32
+ end
33
+ puts ""
34
+
35
+ # 询问用户是否要自动关闭Unity进程
36
+ loop do
37
+ puts "批处理模式需要关闭这些 Unity 进程以避免冲突"
38
+ puts " [y] 是, 自动关闭所有 Unity 进程"
39
+ puts " [s] 跳过, 我已经手动关闭 Unity 进程,继续构建(可能导致冲突)"
40
+ puts " [e] 退出编译"
41
+ print "请输入选择 (y/s/e): "
42
+
43
+ input = STDIN.gets
44
+ choice = input ? input.chomp.strip.downcase : ""
45
+
46
+ case choice
47
+ when 'y', 'yes', '1'
48
+ puts "\n正在关闭 Unity 相关进程..."
49
+ success_count = 0
50
+ unity_pids.each do |process_info|
51
+ if kill_unity_process(process_info[:pid])
52
+ puts "✅ 成功关闭进程 #{process_info[:pid]}"
53
+ success_count += 1
54
+ else
55
+ puts "❌ 关闭进程 #{process_info[:pid]} 失败"
56
+ end
57
+ end
58
+
59
+ if success_count > 0
60
+ puts "\n✅ 已关闭 #{success_count} 个 Unity 相关进程"
61
+ puts "等待3秒后继续编译..."
62
+ sleep(3)
63
+ else
64
+ puts "\n❌ 无法关闭 Unity 相关进程,请手动关闭后重试"
65
+ raise "Unity进程冲突:无法自动关闭 Unity 相关进程,请手动关闭后重试"
66
+ end
67
+ break # 退出循环
68
+
69
+ when 's', 'skip', '2'
70
+ puts "\n✅ 跳过检查,继续编译..."
71
+ puts "假设Unity Editor已经手动关闭,如果仍在运行可能导致编译失败"
72
+ sleep(1)
73
+ break # 退出循环
74
+
75
+ when 'e', 'exit', '3'
76
+ puts "\n⚠️ 用户选择退出编译"
77
+ puts "退出Unity编译流程"
78
+ exit 0
79
+
80
+ else
81
+ puts "\n⚠️ 无效选择: '#{choice}'"
82
+ puts "请输入 y (是), s (跳过), 或 e (退出)\n\n"
83
+ # 继续循环,让用户重新输入
84
+ end
85
+ end
86
+ else
87
+ # 没有检测到与当前项目相关的Unity进程,静默继续
88
+ end
89
+ end
90
+ end
91
+
92
+ # 构建完成后清理 Unity 进程残留(自动)
93
+ # @param unity_exe_full_path [String] Unity 可执行文件路径
94
+ # @param project_path [String] Unity 项目路径
95
+ def self.cleanup_unity_processes_after_build(unity_exe_full_path: nil, project_path: nil)
96
+ begin
97
+ # 等待一小段时间让 Unity 进程自然退出
98
+ sleep(2)
99
+
100
+ # 如果没有提供路径参数,不清理进程
101
+ if unity_exe_full_path.nil? && project_path.nil?
102
+ return
103
+ end
104
+
105
+ # 检查是否还有 Unity 进程在运行
106
+ unity_processes = `ps aux | grep -i unity | grep -v grep`.strip
107
+
108
+ if !unity_processes.empty?
109
+ # 解析进程信息,传入Unity路径和项目路径进行精确匹配
110
+ unity_pids = parse_unity_processes(unity_processes, unity_exe_full_path: unity_exe_full_path, project_path: project_path)
111
+
112
+ if unity_pids.any?
113
+ puts "\e[33m检测到构建后仍有项目相关的 Unity 进程残留,正在清理...\e[0m"
114
+ # 自动清理残留的 Unity 进程
115
+ cleaned_count = 0
116
+ unity_pids.each do |process_info|
117
+ if kill_unity_process(process_info[:pid])
118
+ cleaned_count += 1
119
+ end
120
+ end
121
+
122
+ if cleaned_count > 0
123
+ puts "\e[32m✅ 已清理 #{cleaned_count} 个 Unity 相关进程残留\e[0m"
124
+ end
125
+ end
126
+ end
127
+ rescue => e
128
+ # 清理失败不影响主流程
129
+ # 静默处理,不输出错误信息
130
+ end
131
+ end
132
+
133
+ # 解析 Unity 进程信息
134
+ # @param processes_output [String] ps 命令输出
135
+ # @param unity_exe_full_path [String] Unity 可执行文件路径
136
+ # @param project_path [String] Unity 项目路径
137
+ # @return [Array<Hash>] 进程信息数组
138
+ def self.parse_unity_processes(processes_output, unity_exe_full_path: nil, project_path: nil)
139
+ processes = []
140
+
141
+ processes_output.lines.each do |line|
142
+ # 解析 ps aux 输出格式
143
+ parts = line.strip.split(/\s+/)
144
+ if parts.length >= 11
145
+ pid = parts[1]
146
+ command = parts[10..-1].join(' ')
147
+
148
+ # 过滤掉grep进程本身和pindo进程
149
+ unless command.include?('grep') || command.include?('pindo')
150
+ # 精确匹配Unity进程
151
+ is_relevant_unity_process = false
152
+ match_reasons = []
153
+
154
+ # 1. 必须是Unity Editor主进程(排除VS Code等其他进程)
155
+ is_unity_editor = command.match?(/Unity\.app.*\/Unity/i) || command.match?(/Unity\.exe/i)
156
+
157
+ if is_unity_editor
158
+ # 2. 检查是否是打开了指定项目的Unity进程
159
+ # Unity 使用 -projectpath 参数指定项目路径
160
+ if project_path
161
+ # 标准化路径以确保匹配
162
+ normalized_project_path = File.expand_path(project_path)
163
+ # 精确匹配项目路径(必须完全匹配,不能只是包含)
164
+ if command.match?(/-projectpath\s+#{Regexp.escape(normalized_project_path)}(\s|$)/i)
165
+ # 只有当项目路径完全匹配时才认为是相关进程
166
+ is_relevant_unity_process = true
167
+ match_reasons << "Unity Editor打开了当前项目"
168
+
169
+ # 如果还指定了Unity路径,也要验证
170
+ if unity_exe_full_path
171
+ if command.include?(unity_exe_full_path)
172
+ match_reasons << "使用指定的Unity版本"
173
+ else
174
+ # Unity路径不匹配,不是我们要的进程
175
+ is_relevant_unity_process = false
176
+ match_reasons.clear
177
+ end
178
+ end
179
+ end
180
+ elsif unity_exe_full_path && command.include?(unity_exe_full_path)
181
+ # 只提供了Unity路径,没有项目路径时才匹配
182
+ # 这种情况下匹配所有使用该Unity版本的进程
183
+ is_relevant_unity_process = true
184
+ match_reasons << "使用指定的Unity版本"
185
+ end
186
+ end
187
+
188
+ # 3. 如果没有提供路径参数,不匹配任何进程(由上层函数处理)
189
+
190
+ if is_relevant_unity_process
191
+ processes << { pid: pid, command: command }
192
+ end
193
+ end
194
+ end
195
+ end
196
+ processes
197
+ end
198
+
199
+ # 过滤掉僵尸进程和无效进程
200
+ # @param processes [Array<Hash>] 进程信息数组
201
+ # @return [Array<Hash>] 有效进程数组
202
+ def self.filter_valid_unity_processes(processes)
203
+ valid_processes = []
204
+
205
+ processes.each do |process_info|
206
+ # PID 可能是字符串,确保正确处理
207
+ pid = process_info[:pid]
208
+ pid_int = pid.to_i
209
+
210
+ # 检查进程是否真的存在且活跃
211
+ if process_exists_and_active?(pid_int)
212
+ valid_processes << process_info
213
+ end
214
+ end
215
+
216
+ valid_processes
217
+ end
218
+
219
+ # 检查进程是否真的存在且活跃
220
+ # @param pid [Integer] 进程 ID
221
+ # @return [Boolean] true 如果进程存在且活跃
222
+ def self.process_exists_and_active?(pid)
223
+ begin
224
+ # 使用更简单可靠的方式检查进程
225
+ # 尝试发送信号0来检查进程是否存在
226
+ Process.kill(0, pid)
227
+
228
+ # 如果需要检查进程状态,使用不同的命令格式
229
+ # -o pid=,stat= 去掉标题行
230
+ process_info = `ps -p #{pid} -o pid=,stat= 2>/dev/null`.strip
231
+
232
+ if process_info.empty?
233
+ return false
234
+ end
235
+
236
+ # 解析进程状态
237
+ parts = process_info.split(/\s+/)
238
+ if parts.length >= 2
239
+ stat = parts[1]
240
+ # Z = 僵尸进程, T = 停止进程
241
+ # 只过滤僵尸进程,不过滤停止进程(T可能是正常的暂停状态)
242
+ if stat.include?('Z')
243
+ puts " 进程 #{pid} 是僵尸进程,过滤掉"
244
+ return false
245
+ end
246
+ end
247
+
248
+ true
249
+ rescue Errno::ESRCH
250
+ # 进程不存在
251
+ false
252
+ rescue Errno::EPERM
253
+ # 权限不足,但进程存在
254
+ true
255
+ rescue => e
256
+ puts "检查进程 #{pid} 时出错: #{e.message}"
257
+ # 如果出错,假设进程存在(保守处理)
258
+ true
259
+ end
260
+ end
261
+
262
+ # 关闭 Unity 进程
263
+ # @param pid [String, Integer] 进程 ID
264
+ # @return [Boolean] true 如果成功关闭
265
+ def self.kill_unity_process(pid)
266
+ begin
267
+ pid_int = pid.to_i
268
+
269
+ # 安全检查:确保不是当前进程
270
+ if pid_int == Process.pid
271
+ puts "⚠️ 跳过当前进程 #{pid}"
272
+ return true
273
+ end
274
+
275
+ # 检查进程是否存在
276
+ unless process_exists?(pid_int)
277
+ puts "进程 #{pid} 已不存在"
278
+ return true
279
+ end
280
+
281
+ # 获取进程信息进行额外验证
282
+ process_info = get_process_info(pid_int)
283
+ if process_info && !process_info.include?('Unity')
284
+ puts "⚠️ 跳过非Unity进程 #{pid}: #{process_info}"
285
+ return true
286
+ end
287
+
288
+ puts "正在关闭Unity进程 #{pid}..."
289
+
290
+ # 先尝试优雅关闭
291
+ begin
292
+ Process.kill('TERM', pid_int)
293
+ puts "已发送TERM信号给进程 #{pid}"
294
+ rescue Errno::EPERM
295
+ puts "❌ 没有权限关闭进程 #{pid},尝试使用sudo"
296
+ return kill_unity_process_with_sudo(pid)
297
+ rescue Errno::ESRCH
298
+ puts "进程 #{pid} 已不存在"
299
+ return true
300
+ end
301
+
302
+ # 等待进程优雅退出
303
+ sleep(3)
304
+
305
+ # 检查进程是否还存在
306
+ if process_exists?(pid_int)
307
+ puts "进程 #{pid} 未响应TERM信号,尝试强制关闭..."
308
+ begin
309
+ Process.kill('KILL', pid_int)
310
+ puts "已发送KILL信号给进程 #{pid}"
311
+ sleep(2)
312
+ rescue Errno::EPERM
313
+ puts "❌ 没有权限强制关闭进程 #{pid}"
314
+ return false
315
+ rescue Errno::ESRCH
316
+ puts "进程 #{pid} 已不存在"
317
+ return true
318
+ end
319
+ end
320
+
321
+ # 最终检查
322
+ if process_exists?(pid_int)
323
+ puts "❌ 无法关闭进程 #{pid}"
324
+ false
325
+ else
326
+ puts "✅ 成功关闭进程 #{pid}"
327
+ true
328
+ end
329
+
330
+ rescue => e
331
+ puts "❌ 关闭进程 #{pid} 时出错: #{e.message}"
332
+ puts "错误类型: #{e.class}"
333
+ false
334
+ end
335
+ end
336
+
337
+ # 使用 sudo 关闭进程
338
+ # @param pid [String, Integer] 进程 ID
339
+ # @return [Boolean] true 如果成功关闭
340
+ def self.kill_unity_process_with_sudo(pid)
341
+ begin
342
+ puts "尝试使用sudo关闭进程 #{pid}..."
343
+ result = system("sudo kill -TERM #{pid}")
344
+ if result
345
+ sleep(3)
346
+ if process_exists?(pid.to_i)
347
+ puts "TERM信号无效,尝试KILL信号..."
348
+ system("sudo kill -KILL #{pid}")
349
+ sleep(2)
350
+ end
351
+ !process_exists?(pid.to_i)
352
+ else
353
+ puts "❌ sudo命令执行失败"
354
+ false
355
+ end
356
+ rescue => e
357
+ puts "❌ sudo关闭进程时出错: #{e.message}"
358
+ false
359
+ end
360
+ end
361
+
362
+ # 获取进程信息
363
+ # @param pid [Integer] 进程 ID
364
+ # @return [String, nil] 进程命令名称
365
+ def self.get_process_info(pid)
366
+ begin
367
+ `ps -p #{pid} -o comm=`.strip
368
+ rescue => e
369
+ nil
370
+ end
371
+ end
372
+
373
+ # 检查进程是否存在
374
+ # @param pid [Integer] 进程 ID
375
+ # @return [Boolean] true 如果进程存在
376
+ def self.process_exists?(pid)
377
+ begin
378
+ Process.kill(0, pid)
379
+ true
380
+ rescue Errno::ESRCH
381
+ false
382
+ rescue Errno::EPERM
383
+ # 权限不足,但进程存在
384
+ true
385
+ end
386
+ end
387
+
388
+ end
389
+ end
390
+ end
@@ -11,6 +11,14 @@ module Pindo
11
11
  class GlobalOptionsState
12
12
  include Singleton
13
13
 
14
+ # 所有已知的 OptionGroup 模块(用于查找参数显示名称)
15
+ OPTION_GROUPS = [
16
+ -> { Pindo::Options::BuildOptions },
17
+ -> { Pindo::Options::JPSOptions },
18
+ -> { Pindo::Options::UnityOptions },
19
+ -> { Pindo::Options::GitOptions }
20
+ ].freeze
21
+
14
22
  def initialize
15
23
  # 运行时状态(内存)
16
24
  @current_command = nil # 当前命令名称
@@ -40,10 +48,23 @@ module Pindo
40
48
  end
41
49
 
42
50
  # 加载缓存的参数值(带用户确认)
51
+ # 环境变量 PINDO_OPTIONS_CACHE 控制缓存行为:
52
+ # - 1/true/force: 强制使用缓存,不询问用户
53
+ # - 0/false/disable: 禁用缓存,不询问用户
54
+ # - 其他/不设置: 默认行为,询问用户
43
55
  # @return [Hash] 缓存的参数值
44
56
  def load_cached_values
45
57
  return {} unless @current_directory && @current_command
46
58
 
59
+ # 检查缓存控制环境变量
60
+ cache_mode = ENV['PINDO_OPTIONS_CACHE']&.downcase
61
+
62
+ # 禁用缓存模式: 0, false, disable
63
+ if %w[0 false disable].include?(cache_mode)
64
+ log_verbose("PINDO_OPTIONS_CACHE=#{cache_mode},跳过缓存")
65
+ return {}
66
+ end
67
+
47
68
  cached_params = @cache_data.dig(@current_directory.to_sym, @current_command.to_sym)
48
69
 
49
70
  # 没有缓存数据,直接返回空Hash
@@ -52,17 +73,14 @@ module Pindo
52
73
  return {}
53
74
  end
54
75
 
55
- # 检查环境变量是否强制使用缓存
56
- force_build = ENV['PINDO_FORCE_BUILD']
57
-
58
- if force_build && !force_build.empty?
59
- # 自动使用缓存
60
- puts "\n检测到 PINDO_FORCE_BUILD 环境变量,自动使用缓存的参数"
76
+ # 强制使用缓存模式: 1, true, force
77
+ if %w[1 true force].include?(cache_mode)
78
+ puts "\n自动使用缓存的参数 (PINDO_OPTIONS_CACHE=#{cache_mode})"
61
79
  log_verbose("加载缓存参数: #{cached_params.inspect}")
62
80
  return cached_params
63
81
  end
64
82
 
65
- # 显示缓存的参数
83
+ # 默认模式:显示缓存的参数并询问用户
66
84
  display_cached_params(cached_params)
67
85
 
68
86
  # 询问用户是否使用缓存
@@ -82,6 +100,20 @@ module Pindo
82
100
  end
83
101
  end
84
102
 
103
+ # 参数显示顺序(优先级从高到低)
104
+ PARAM_DISPLAY_ORDER = [
105
+ # 1. 核心标识参数
106
+ :bundleid, :bundle_id, :bundle_name,
107
+ # 2. 构建配置
108
+ :build_type, :scheme,
109
+ # 3. JPS 相关
110
+ :proj, :upload, :send, :desc,
111
+ # 4. Unity 相关
112
+ :skipconfig, :skiplib, :skipyoo,
113
+ # 5. Git 相关
114
+ :ver_inc, :tag_type, :tag_pre, :release_branch
115
+ ].freeze
116
+
85
117
  # 显示缓存的参数
86
118
  def display_cached_params(cached_params)
87
119
  # 根据命令名显示友好的描述
@@ -99,10 +131,18 @@ module Pindo
99
131
  puts "\n检测到之前的参数 (#{group_desc}):"
100
132
  puts "────────────────────────────────────────"
101
133
 
102
- cached_params.each do |key, value|
134
+ # 按照预定义顺序排序参数
135
+ sorted_keys = cached_params.keys.sort_by do |key|
136
+ order_index = PARAM_DISPLAY_ORDER.index(key.to_sym)
137
+ order_index || PARAM_DISPLAY_ORDER.size # 未定义的参数排在最后
138
+ end
139
+
140
+ sorted_keys.each do |key|
141
+ value = cached_params[key]
103
142
  # 跳过内部字段
104
143
  next if key.to_s.start_with?('__')
105
144
  next if value.nil?
145
+ next unless is_cacheable?(key) # 跳过不可缓存的参数
106
146
 
107
147
  # 格式化显示参数
108
148
  key_name = format_param_name(key)
@@ -112,24 +152,28 @@ module Pindo
112
152
  puts "────────────────────────────────────────"
113
153
  end
114
154
 
115
- # 格式化参数名称
155
+ # 格式化参数名称(从 OptionItem 定义中查找显示名称)
156
+ # @param key [Symbol, String] 参数键名
157
+ # @return [String] 显示名称
116
158
  def format_param_name(key)
117
- case key.to_s
118
- when 'bundleid', 'bundle_id'
119
- 'Bundle ID'
120
- when 'build_type'
121
- '构建类型'
122
- when 'upload'
123
- '上传JPS'
124
- when 'send'
125
- '发送通知'
126
- when 'proj', 'project_name'
127
- '项目名称'
128
- when 'scheme'
129
- 'Scheme'
130
- else
131
- key.to_s
159
+ key_sym = key.to_sym
160
+
161
+ # 遍历所有已知的 OptionGroup,查找匹配的 OptionItem
162
+ OPTION_GROUPS.each do |group_proc|
163
+ begin
164
+ group = group_proc.call
165
+ if group.respond_to?(:all_options)
166
+ option_item = group.all_options[key_sym]
167
+ return option_item.display_name if option_item
168
+ end
169
+ rescue NameError
170
+ # 模块尚未加载,跳过
171
+ next
172
+ end
132
173
  end
174
+
175
+ # 如果没有找到匹配的 OptionItem,返回 key 本身
176
+ key.to_s
133
177
  end
134
178
 
135
179
  # 清除当前命令的缓存
@@ -235,10 +279,12 @@ module Pindo
235
279
  # 确保缓存数据结构存在
236
280
  @cache_data[@current_directory.to_sym] ||= {}
237
281
 
238
- # 提取当前参数值(排除 nil 值)
282
+ # 提取当前参数值(排除 nil 值和不可缓存的参数)
239
283
  current_params = {}
240
284
  @current_options.instance_variable_get(:@values).each do |key, value|
241
- current_params[key] = value unless value.nil?
285
+ next if value.nil?
286
+ next unless is_cacheable?(key) # 过滤不可缓存的参数
287
+ current_params[key] = value
242
288
  end
243
289
 
244
290
  # 保存到缓存
@@ -248,6 +294,30 @@ module Pindo
248
294
  save_cache_to_file_immediate
249
295
  end
250
296
 
297
+ # 判断参数是否可缓存
298
+ # @param key [Symbol, String] 参数键名
299
+ # @return [Boolean] 是否可缓存
300
+ def is_cacheable?(key)
301
+ key_sym = key.to_sym
302
+
303
+ # 遍历所有已知的 OptionGroup,查找匹配的 OptionItem
304
+ OPTION_GROUPS.each do |group_proc|
305
+ begin
306
+ group = group_proc.call
307
+ if group.respond_to?(:all_options)
308
+ option_item = group.all_options[key_sym]
309
+ return option_item.cacheable? if option_item
310
+ end
311
+ rescue NameError
312
+ # 模块尚未加载,跳过
313
+ next
314
+ end
315
+ end
316
+
317
+ # 如果没有找到匹配的 OptionItem,默认可缓存
318
+ true
319
+ end
320
+
251
321
  # 立即保存缓存数据到文件(内部方法)
252
322
  def save_cache_to_file_immediate
253
323
  begin
@@ -133,6 +133,9 @@ module Pindo
133
133
  # 应用 value_block(交互式获取值)
134
134
  # 优先级:命令行参数 > 缓存(已在 raw_values) > value_block > 默认值
135
135
  def apply_value_blocks
136
+ # 检测是否是 help 请求,跳过交互式选择
137
+ return if ARGV.include?('--help') || ARGV.include?('-h')
138
+
136
139
  @available_options.each do |item|
137
140
  # 只有当参数没有值时,才调用 value_block
138
141
  next if @values.key?(item.key) && !@values[item.key].nil?
@@ -5,12 +5,14 @@ module Pindo
5
5
 
6
6
  # 核心属性
7
7
  attr_accessor :key # Symbol: 参数键名
8
+ attr_accessor :name # String: 显示名称(用于缓存确认等场景)
8
9
  attr_accessor :description # String: 参数描述
9
10
  attr_accessor :type # Class: 数据类型 (String/Integer/Boolean)
10
11
  attr_accessor :env_name # String: 环境变量名
11
12
  attr_accessor :aliases # Array<Symbol>: 参数别名
12
13
  attr_accessor :default_value # Any: 默认值
13
14
  attr_accessor :optional # Boolean: 是否可选(默认为 true)
15
+ attr_accessor :cacheable # Boolean: 是否存入缓存(默认为 true)
14
16
  attr_accessor :verify_block # Proc: 自定义验证逻辑
15
17
  attr_accessor :value_block # Proc: 获取参数值的 block(交互式输入)
16
18
  attr_accessor :example # String: 使用示例
@@ -25,12 +27,14 @@ module Pindo
25
27
  raise ArgumentError, "key must be a Symbol" unless key.is_a?(Symbol)
26
28
 
27
29
  @key = key
30
+ @name = options[:name] # 显示名称,如果未设置则使用 key
28
31
  @description = options[:description] || options[:desc] || ""
29
32
  @type = options[:type] || String
30
33
  @env_name = options[:env_name]
31
34
  @aliases = options[:aliases] || []
32
35
  @default_value = options[:default_value] || options[:default]
33
36
  @optional = options.fetch(:optional, true)
37
+ @cacheable = options.fetch(:cacheable, false) # 默认不存入缓存
34
38
  @verify_block = options[:verify_block]
35
39
  @value_block = options[:value_block]
36
40
  @example = options[:example]
@@ -38,11 +42,21 @@ module Pindo
38
42
  validate_type!
39
43
  end
40
44
 
45
+ # 获取显示名称(优先使用 name,否则使用 key)
46
+ def display_name
47
+ @name || @key.to_s
48
+ end
49
+
41
50
  # 判断是否是 Boolean 类型
42
51
  def boolean?
43
52
  @type == Boolean || @type == :boolean
44
53
  end
45
54
 
55
+ # 判断是否需要存入缓存
56
+ def cacheable?
57
+ @cacheable
58
+ end
59
+
46
60
  # 从环境变量读取值
47
61
  # @return [String, nil] 环境变量的值
48
62
  def fetch_env_value
@@ -98,6 +112,28 @@ module Pindo
98
112
  [option_string, description_text]
99
113
  end
100
114
 
115
+ # 复制当前 OptionItem 并覆盖指定属性
116
+ # @param overrides [Hash] 要覆盖的属性
117
+ # @return [OptionItem] 新的 OptionItem 实例
118
+ # @example
119
+ # UnityOptions.select(:skipconfig).first.with(default_value: true)
120
+ def with(**overrides)
121
+ OptionItem.new(
122
+ key: overrides[:key] || @key,
123
+ name: overrides[:name] || @name,
124
+ description: overrides[:description] || @description,
125
+ type: overrides[:type] || @type,
126
+ env_name: overrides[:env_name] || @env_name,
127
+ aliases: overrides[:aliases] || @aliases,
128
+ default_value: overrides.key?(:default_value) ? overrides[:default_value] : @default_value,
129
+ optional: overrides.key?(:optional) ? overrides[:optional] : @optional,
130
+ cacheable: overrides.key?(:cacheable) ? overrides[:cacheable] : @cacheable,
131
+ verify_block: overrides[:verify_block] || @verify_block,
132
+ value_block: overrides[:value_block] || @value_block,
133
+ example: overrides[:example] || @example
134
+ )
135
+ end
136
+
101
137
  private
102
138
 
103
139
  # 验证类型是否合法