ctx_cli 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 367e5a47fb0f653f5ef833f8efee765606717ae2c97aa6025f6116032ba1edbf
4
- data.tar.gz: b87ac1112712391f7fd1eac7fc66fd4e93e6af6dc5ceb3d08508872f0c55345c
3
+ metadata.gz: d2e8622dd69aeb8ad48fb4e9b5d0212345698f81b78f042d4dfa761bdf0c3890
4
+ data.tar.gz: b579bf0a5ad0a3d795249f4e8d0048366120f5a8f857f95a29901c78266b28f2
5
5
  SHA512:
6
- metadata.gz: 948f143970bc4d5a0af50470e6c35a1bdfe98c0eb63d6d3efa18531c7578f6a00ea3da4da2545091433416b119e715f69bd739c745ca763f08acf2432a0a51d1
7
- data.tar.gz: a3f788a3d3bd2e946c484594747c01912d28455a3b771f5acc7d8208cacca5d838103003fa67383bc3139afceb84e1f1555cd86981d22f355d239dffd8a8e691
6
+ metadata.gz: 2e18324d1c852f300873ea0bf02fadd159fd70bf4637f716590e1e0435d92d5dc1a1580e5eed4f552cc607cf2aeef5549df544a8f07f1340faff5cc87724ecc3
7
+ data.tar.gz: b61f9d0812d4bde92d8b5bf86d804bc9d9f2b8991b7f3b7c308909c74696c4ecfec333dd1ef9b14ae9da57c043cf91a35b9ee74d62eaeea6bd17398e015222c6
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0 (2026-10-23)
6
+
7
+ - Rename Initialization to Initializer and Application to Router; remove the old constants and require paths in the 0.2.0 candidate.
8
+ - Preserve caller argument strings during routing and publish configuration applicability only after every layer succeeds.
9
+ - Remove quadratic field-source scans for leaf configuration values.
10
+ - Bound offline review source counts and cumulative bytes, and treat batch directory names literally.
11
+ - Redact adapter argument and dependency errors at the collection boundary.
12
+ - Share offline review preflight/publication ordering and redacted diagnostics without depending on Runner or domain schemas.
13
+ - Bound input bytes and configuration nesting/nodes before deserialization; merge owned layers without recursive copies.
14
+ - Synchronize newly created parent directories before publishing configuration templates.
15
+
5
16
  ## 0.1.0 (2026-10-22)
6
17
 
7
18
  - Extract reusable configuration discovery, private initialization, command dispatch, option groups, terminal interaction, input/output and collection entrypoint orchestration.
data/README.md CHANGED
@@ -6,12 +6,13 @@
6
6
 
7
7
  | 对象 | 职责 |
8
8
  | --- | --- |
9
- | `Application` | 惰性子命令分发、全局帮助/版本、返回退出码 |
9
+ | `Router` | 惰性子命令分发、全局帮助/版本、返回退出码 |
10
10
  | `Command` / `Options` | 每次独立的参数快照、信息分支、公共 OptionParser 分组 |
11
11
  | `Configuration` | 多层文件发现、隔离环境、来源路径、纯数据快照 |
12
- | `Initialization` | 私有文件排他创建、同步、保留已有配置 |
12
+ | `Initializer` | 私有文件排他创建、同步、保留已有配置 |
13
13
  | `Terminal` | 真实终端检查、问答、隐藏密码、完整 token 确认 |
14
14
  | `Input` / `Output` | 文件/stdin、显式 JSON、仅含错误类型的诊断 |
15
+ | `Review` | 离线来源读取、全量预检后发布、安全诊断与退出码映射 |
15
16
  | `Collection` | 配置、预览、全批预检、调度交付及退出码映射 |
16
17
 
17
18
  Ruby >= 3.4。`require "ctx_cli"` 或按能力加载 `ctx_cli/configuration` 等路径;加载不读取用户文件、不修改 ENV、不建立网络连接。核心不依赖 ctx_parser、ctx_runner 或 net-connector。代码和工程规范参照 ctx_parser,保留 Ruby 3.4 是为了兼容现有 backup 宿主。
@@ -56,7 +57,7 @@ files.sources # 路径与字段来源,不包含环境值
56
57
 
57
58
  宿主把各层公共环境值转换成领域字段后,通过 `Configuration.merge(defaults, dotenv_values, yaml, process_values, cli)` 实现 CLI > 进程环境 > YAML > .env > 默认值。数组整层替换,false/nil 保留。相对路径按来源目录解释;支持 `path_literals:`(如 `system`)和 URI。`load` 重复调用不重新读文件。
58
59
 
59
- `Initialization#create(path:, contents:)` 返回 `:created` 或 `:exists`。文件先写完并同步,再排他发布并同步目录;既有普通文件保留,符号链接/目录拒绝。发布后同步失败仍抛错,不能当作成功;不把凭据自动写入模板。例子见 [examples/configuration.rb](examples/configuration.rb)。
60
+ `Initializer#create(path:, contents:)` 返回 `:created` 或 `:exists`。文件先写完并同步,再排他发布并同步目录;既有普通文件保留,符号链接/目录拒绝。发布后同步失败仍抛错,不能当作成功;不把凭据自动写入模板。例子见 [examples/configuration.rb](examples/configuration.rb)。
60
61
 
61
62
  ## 采集协议
62
63
 
@@ -72,9 +73,22 @@ Options 协作者沿用现有入口协议:`informational?`、`validate_interac
72
73
  bundle install
73
74
  bundle exec rake
74
75
  bundle exec rake build
75
- ruby script/verify_package.rb pkg/ctx_cli-0.1.0.gem
76
+ ruby script/verify_package.rb pkg/ctx_cli-0.2.0.gem
76
77
  ```
77
78
 
78
79
  默认任务包括 RSpec、RuboCop 和离线示例。安装验证在独立 GEM_HOME/GEM_PATH 下运行普通与最小 Bundler 应用,检查实际加载来源。CI 参照 ctx_parser 的固定 action 提交和候选制品校验;发布需另行明确授权。
79
80
 
80
81
  消费仓库可用 `CTX_CLI_PATH=../ctx_cli bundle exec rake` 显式联调;普通入口须先安装候选包。不隐式探测相邻目录。baseline 只在 Gemfile 中为现有 coverage 脚本引入开发依赖,没有新增 baseline 命令或核心依赖。
82
+
83
+ Input reads default to a 16 MiB byte budget (`CtxCli::Input.read(max_bytes:)`). Configuration data is limited to 128 nesting levels and 100,000 nodes; oversized input raises `ArgumentError` before recursive deserialization.
84
+
85
+ ## 离线 review 入口
86
+
87
+ `CtxCli::Review` 接收 `command:`(`CtxCli::Command`)、`directory:`(批次内证据目录名)和 `adapter:`。入口先处理帮助,再按文件名顺序读取 JSON,通过 `Input` 限制单个文件大小,最后依次调用:
88
+
89
+ - `adapter.prepare(sources, options)`:验证全部领域记录、身份及策略并返回上下文,不创建输出。每条来源为 `[path, body, document]`;领域适配器可在本次入口拥有的来源数组中附加建议。
90
+ - `adapter.publish(sources, options, batch:, context:)`:创建报告并返回完成提示。发布及持久性状态仍由领域模块和 Runner 管理。
91
+
92
+ 失败只输出异常类型,返回 2;中断返回 130;诊断流失败不覆盖原始退出码。帮助不加载证据或调用适配器。此类不读取凭据、连接设备或判断业务健康。默认单文件 16 MiB、批次原始正文总计 64 MiB、文件数 10000;宿主可通过 `max_bytes:`、`max_total_bytes:`、`max_files:` 调整。JSON 默认嵌套限制保持开启;上述限制不是进程 RSS 的精确上限。
93
+
94
+ `Collection` 对普通参数/依赖异常只显示类型;宿主需要公开操作提示时使用 `CtxCli::Collection::Error`,其正文必须由宿主构造,不得直接包装外部异常正文、凭据或设备回显。该类型继承 ArgumentError,既有库调用方仍可按参数错误捕获。
data/docs/migration.md CHANGED
@@ -11,3 +11,10 @@
11
11
  ctx_runner 继续拥有采集/报告/执行的业务状态,net-connector Inventory 继续拥有筛选;ctx_cli 不反向加载二者。普通 Config 旧入口保留为宿主政策适配,无第二份配置发现实现。
12
12
 
13
13
  ctx_cli 初始版本为 0.1.0。首次发布依赖顺序先 ctx_cli,再 net-connector 与 CLI 消费模块;ctx_runner 的依赖链仍按其原有约定。提交、远程创建和发布按用户明确授权的范围执行。
14
+
15
+ ## 0.2.0 接口命名
16
+
17
+ - `CtxCli::Initialization` / `ctx_cli/initialization` 改为 `CtxCli::Initializer` / `ctx_cli/initializer`,仅负责模板安全发布。
18
+ - `CtxCli::Application` / `ctx_cli/application` 改为 `CtxCli::Router` / `ctx_cli/router`,仅负责全局信息和子命令分发。
19
+
20
+ 旧常量和 require 路径不保留别名。宿主需同步修改引用,并将 ctx_cli 依赖更新为 `~> 0.2.0`。`create`、`existing?`、`run` 的结果约定不变;初始化参数询问和厂商语义不迁入这两个对象。0.2.0 当前为本地候选,尚未发布。
@@ -6,7 +6,7 @@ require "tmpdir"
6
6
  # 自定义宿主命名空间;示例只操作临时目录,不读取真实用户配置。
7
7
  Dir.mktmpdir do |root|
8
8
  path = File.join(root, "audit.yml")
9
- publisher = CtxCli::Initialization.new
9
+ publisher = CtxCli::Initializer.new
10
10
  abort "Initialization failed" unless publisher.create(path: path, contents: "directory: reports\nparallel: 2\n") == :created
11
11
  files = CtxCli::Configuration.new(name: "audit", env: {}, directories: [ root ], path_fields: [ "directory" ]).load
12
12
  effective = CtxCli::Configuration.merge({ "parallel" => 1 }, files.data, { "parallel" => 4 })
@@ -2,10 +2,14 @@
2
2
 
3
3
  require "optparse"
4
4
  require_relative "output"
5
+ require_relative "configuration"
5
6
 
6
7
  module CtxCli
7
8
  # 终端采集入口的固定顺序;领域适配器拥有清单、预检、调度及采集策略。
8
9
  class Collection
10
+ # 宿主明确构造的公开操作提示,不得包装凭据或外部异常正文。
11
+ class Error < ArgumentError; end
12
+
9
13
  def initialize(adapter:, options:, progress:, batch:, label:, report_error:, failure_code:,
10
14
  out: $stdout, error: $stderr, input: $stdin)
11
15
  @adapter, @options_factory, @progress_factory, @batch_class = adapter, options, progress, batch
@@ -58,9 +62,12 @@ module CtxCli
58
62
  rescue SystemCallError, IOError => error
59
63
  diagnostic "#{@label} 参数或报告错误:#{report_diagnostic(error)}"
60
64
  2
61
- rescue OptionParser::ParseError, ArgumentError, LoadError => error
65
+ rescue Error, Configuration::Error => error
62
66
  diagnostic "#{@label} 参数、依赖或报告错误:#{error.message};使用 --help 查看参数"
63
67
  2
68
+ rescue OptionParser::ParseError, ArgumentError, LoadError => error
69
+ diagnostic "#{@label} 参数、依赖或报告错误:#{error.class.name};使用 --help 查看参数"
70
+ 2
64
71
  rescue StandardError => error
65
72
  diagnostic "#{@label} 清单或执行失败:#{@failure_code.call(error)}(#{error.class.name})"
66
73
  2
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "input"
4
5
 
5
6
  module CtxCli
6
7
  # 宿主配置发现;加载文件不读取配置或建立设备连接。
7
8
  class Configuration
9
+ # 仅用于本类构造的公开配置诊断,不携带文件正文。
10
+ class Error < ArgumentError; end
11
+
12
+ MAX_DEPTH = 128
13
+ MAX_NODES = 100_000
8
14
  attr_reader :data, :specific, :process_environment, :file_environment, :environment,
9
15
  :config_files, :environment_files, :sources
10
16
 
@@ -74,7 +80,7 @@ module CtxCli
74
80
  specific = {}
75
81
  fields = {}
76
82
  loaded = []
77
- @applicable = false
83
+ applicable = false
78
84
  paths = @directories.flat_map { |directory| [ File.join(directory, "#{@namespace}.yml"), File.join(directory, "#{@name}.yml") ] }
79
85
  paths = final_candidates(paths, @selected_config)
80
86
  paths.each do |path|
@@ -82,13 +88,13 @@ module CtxCli
82
88
 
83
89
  document = read_yaml(path)
84
90
  if @namespace != @name && File.basename(path) == "#{@namespace}.yml"
85
- common, native, applicable = shared_sections(document, path)
91
+ common, native, native_present = shared_sections(document, path)
86
92
  merged = merge_layer(merged, common, path, fields)
87
- @applicable ||= applicable
93
+ applicable ||= native_present
88
94
  else
89
95
  native = document
90
96
  validate_string_keys!(native, path)
91
- @applicable = true
97
+ applicable = true
92
98
  end
93
99
  merged = merge_layer(merged, native, path, fields)
94
100
  specific = self.class.merge(specific, resolve_paths(native, path))
@@ -98,6 +104,7 @@ module CtxCli
98
104
  @specific = self.class.freeze_tree(specific)
99
105
  @config_files = self.class.freeze_tree(loaded)
100
106
  update_sources(fields)
107
+ @applicable = applicable
101
108
  @loaded = true
102
109
  self
103
110
  end
@@ -132,27 +139,41 @@ module CtxCli
132
139
  alias_method :to_s, :inspect
133
140
  def pretty_print(printer) = printer.text(inspect)
134
141
 
135
- # 递归合并但不修改来源;数组、false、nil 按值替换。
142
+ # 每层只复制一次,再合并拥有的映射;数组、false、nil 按值替换。
136
143
  def self.merge(*hashes)
137
144
  hashes.reduce({}) do |result, hash|
138
- hash.each do |key, value|
139
- result[copy(key)] = result[key].is_a?(Hash) && value.is_a?(Hash) ? merge(result[key], value) : copy(value)
145
+ raise ArgumentError, "configuration layers must be mappings" unless hash.is_a?(Hash)
146
+ pending = [ [ result, copy(hash) ] ]
147
+ until pending.empty?
148
+ target, source = pending.pop
149
+ source.each do |key, value|
150
+ if target[key].is_a?(Hash) && value.is_a?(Hash)
151
+ pending << [ target[key], value ]
152
+ else
153
+ target[key] = value
154
+ end
155
+ end
140
156
  end
141
157
  result
142
158
  end
143
159
  end
144
160
 
145
161
  # 只复制普通数据;循环或不透明对象不能进入快照并冻结调用方资源。
146
- def self.copy(value, active = {}.compare_by_identity)
162
+ def self.copy(value) = copy_value(value, {}.compare_by_identity, 0, [ MAX_NODES ])
163
+
164
+ def self.copy_value(value, active, depth, budget)
165
+ raise ArgumentError, "configuration nesting exceeds budget" if depth > MAX_DEPTH
166
+ budget[0] -= 1
167
+ raise ArgumentError, "configuration node budget exceeded" if budget[0].negative?
147
168
  case value
148
169
  when Hash, Array
149
170
  raise ArgumentError, "cyclic configuration value" if active.key?(value)
150
171
  active[value] = true
151
172
  begin
152
173
  if value.is_a?(Hash)
153
- value.to_h { |key, entry| [ copy(key, active), copy(entry, active) ] }
174
+ value.to_h { |key, entry| [ copy_value(key, active, depth + 1, budget), copy_value(entry, active, depth + 1, budget) ] }
154
175
  else
155
- value.map { |entry| copy(entry, active) }
176
+ value.map { |entry| copy_value(entry, active, depth + 1, budget) }
156
177
  end
157
178
  ensure
158
179
  active.delete(value)
@@ -162,6 +183,7 @@ module CtxCli
162
183
  else raise ArgumentError, "configuration accepts plain data only"
163
184
  end
164
185
  end
186
+ private_class_method :copy_value
165
187
 
166
188
  def self.freeze_tree(value)
167
189
  case value
@@ -196,7 +218,7 @@ module CtxCli
196
218
 
197
219
  def readable_file?(path, explicit:, allow_missing:)
198
220
  unless File.exist?(path)
199
- raise ArgumentError, "configuration file does not exist: #{path}" if explicit && !allow_missing
221
+ raise Error, "configuration file does not exist: #{path}" if explicit && !allow_missing
200
222
 
201
223
  return false
202
224
  end
@@ -206,7 +228,7 @@ module CtxCli
206
228
  end
207
229
 
208
230
  def read_yaml(path)
209
- contents = File.read(path)
231
+ contents = Input.read(path: path)
210
232
  validate_yaml_structure!(contents, path)
211
233
  document = YAML.safe_load(contents, permitted_classes: [], permitted_symbols: [], aliases: false)
212
234
  document = {} if document.nil?
@@ -223,7 +245,14 @@ module CtxCli
223
245
  stream = YAML.parse_stream(contents)
224
246
  raise ArgumentError, "configuration must contain a single YAML document: #{path}" if stream.children.size > 1
225
247
 
226
- stream.each do |node|
248
+ pending = [ [ stream, 0 ] ]
249
+ remaining = MAX_NODES
250
+ until pending.empty?
251
+ node, depth = pending.pop
252
+ raise ArgumentError, "configuration nesting exceeds budget" if depth > MAX_DEPTH
253
+ remaining -= 1
254
+ raise ArgumentError, "configuration node budget exceeded" if remaining.negative?
255
+ node.children&.reverse_each { |child| pending << [ child, depth + 1 ] }
227
256
  next unless node.is_a?(Psych::Nodes::Mapping)
228
257
 
229
258
  keys = {}
@@ -322,7 +351,11 @@ module CtxCli
322
351
  fields.delete(name)
323
352
  record_fields(value, source, fields, name, previous: before)
324
353
  else
325
- fields.delete_if { |field, _| field == name || field.start_with?("#{name}.") }
354
+ # 只有旧映射被整体替换才需要删除后代;新增/覆盖叶子直接赋值。
355
+ if before.is_a?(Hash) && !before.empty?
356
+ descendant_prefix = "#{name}."
357
+ fields.delete_if { |field, _| field.start_with?(descendant_prefix) }
358
+ end
326
359
  fields[name] = source
327
360
  end
328
361
  end
@@ -365,7 +398,7 @@ module CtxCli
365
398
  end
366
399
  private :expand_newlines
367
400
  end
368
- contents = File.read(path)
401
+ contents = Input.read(path: path)
369
402
  remainder = contents.gsub(Dotenv::Parser::LINE, "")
370
403
  unless remainder.lines("\n").all? { |line| line.strip.empty? || line.lstrip.start_with?("#") }
371
404
  raise ArgumentError, "invalid environment syntax"
@@ -5,7 +5,7 @@ require "tempfile"
5
5
 
6
6
  module CtxCli
7
7
  # 只发布已经过宿主校验的模板,不收集凭据、不覆盖已有目录项。
8
- class Initialization
8
+ class Initializer
9
9
  def initialize(prefix: ".ctx-cli-init-")
10
10
  @prefix = prefix
11
11
  end
@@ -23,7 +23,7 @@ module CtxCli
23
23
  def create(path:, contents:)
24
24
  raise ArgumentError, "config path must be a nonempty String" unless path.is_a?(String) && !path.empty?
25
25
  return :exists if existing?(path)
26
- FileUtils.mkdir_p(File.dirname(File.expand_path(path)), mode: 0o700)
26
+ prepare_directory(File.dirname(File.expand_path(path)))
27
27
  directory = File.realpath(File.dirname(File.expand_path(path)))
28
28
  destination = File.join(directory, File.basename(path))
29
29
  file = parent = primary = nil
@@ -53,6 +53,19 @@ module CtxCli
53
53
 
54
54
  private
55
55
 
56
+ # 先确认新目录在各父级的名字已同步,再发布其中的配置文件。
57
+ def prepare_directory(path)
58
+ missing, parent = [], path
59
+ until File.directory?(parent)
60
+ missing << parent
61
+ parent = File.dirname(parent)
62
+ end
63
+ FileUtils.mkdir_p(path, mode: 0o700)
64
+ missing.reverse_each do |created|
65
+ File.open(File.realpath(File.dirname(created)), File::RDONLY | File::NOFOLLOW) { |directory| directory.fsync }
66
+ end
67
+ end
68
+
56
69
  # 全部句柄都尝试关闭,保留原始故障;不把清理失败变为成功。
57
70
  def close_files(file, parent, primary)
58
71
  Thread.handle_interrupt(Exception => :never) do
data/lib/ctx_cli/input.rb CHANGED
@@ -3,8 +3,14 @@
3
3
  module CtxCli
4
4
  # 只关闭自行打开的文件,stdin 的读取和关闭权仍由宿主控制。
5
5
  module Input
6
- def self.read(path: nil, input: $stdin)
7
- path ? File.read(path) : input.read
6
+ MAX_BYTES = 16 * 1024 * 1024
7
+
8
+ def self.read(path: nil, input: $stdin, max_bytes: MAX_BYTES)
9
+ raise ArgumentError, "input byte budget must be positive" unless max_bytes.is_a?(Integer) && max_bytes.positive?
10
+ return File.open(path, "r") { |file| read(input: file, max_bytes: max_bytes) } if path
11
+ contents = input.read(max_bytes + 1) || +""
12
+ raise ArgumentError, "input exceeds byte budget" if contents.bytesize > max_bytes
13
+ contents.force_encoding(input.external_encoding || Encoding.default_external)
8
14
  end
9
15
  end
10
16
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "input"
5
+ require_relative "output"
6
+
7
+ module CtxCli
8
+ # 离线入口先读取并预检全部来源,再允许领域适配器创建报告。
9
+ class Review
10
+ MAX_FILES = 10_000
11
+ MAX_TOTAL_BYTES = 64 * 1024 * 1024
12
+
13
+ def initialize(command:, directory:, adapter:, out: $stdout, error: $stderr,
14
+ max_files: MAX_FILES, max_bytes: Input::MAX_BYTES, max_total_bytes: MAX_TOTAL_BYTES)
15
+ unless [ max_files, max_bytes, max_total_bytes ].all? { |limit| limit.is_a?(Integer) && limit.positive? }
16
+ raise ArgumentError, "review budgets must be positive integers"
17
+ end
18
+ @max_files, @max_bytes, @max_total_bytes = max_files, max_bytes, max_total_bytes
19
+ @command, @directory, @adapter = command, directory, adapter
20
+ @output = Output.new(out: out, error: error)
21
+ end
22
+
23
+ def run(argv)
24
+ options = @command.parse(argv)
25
+ return 0 if @command.informational?
26
+ raise ArgumentError, "需要批次目录" unless options[:batch]
27
+ batch = File.expand_path(options[:batch])
28
+ paths = source_paths(File.join(batch, @directory))
29
+ raise ArgumentError, "无设备报告" if paths.empty?
30
+ remaining = @max_total_bytes
31
+ sources = paths.map do |path|
32
+ raise ArgumentError, "review byte budget exceeded" unless remaining.positive?
33
+ body = Input.read(path: path, max_bytes: [ @max_bytes, remaining ].min)
34
+ remaining -= body.bytesize
35
+ [ path, body, JSON.parse(body) ]
36
+ end
37
+ context = @adapter.prepare(sources, options)
38
+ @output.text(@adapter.publish(sources, options, batch: batch, context: context))
39
+ 0
40
+ rescue Interrupt
41
+ @output.diagnostic("建议生成已取消;已有报告保留,未执行设备变更。")
42
+ 130
43
+ rescue StandardError => error
44
+ @output.diagnostic("建议生成失败(#{error.class.name});请检查输入结构、目录权限和 --help;未执行设备变更。")
45
+ 2
46
+ end
47
+
48
+ def inspect = "#<#{self.class.name}>"
49
+ alias_method :to_s, :inspect
50
+ def pretty_print(printer) = printer.text(inspect)
51
+
52
+ private
53
+
54
+ # 目录名按字面处理;边枚举边限制数量,避免先创建无限制的路径数组。
55
+ def source_paths(directory)
56
+ paths = []
57
+ Dir.each_child(directory) do |name|
58
+ next if name.start_with?(".") || !name.end_with?(".json")
59
+ raise ArgumentError, "review file budget exceeded" if paths.size >= @max_files
60
+ paths << File.join(directory, name)
61
+ end
62
+ paths.sort
63
+ end
64
+ end
65
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CtxCli
4
4
  # 命令工厂按需执行;帮助和版本不加载领域实现或配置。
5
- class Application
5
+ class Router
6
6
  def initialize(name:, version:, usage:, commands:, out: $stdout, error: $stderr)
7
7
  @name, @version, @usage, @commands = name, version, usage, commands.dup.freeze
8
8
  @out, @error = out, error
@@ -10,7 +10,7 @@ module CtxCli
10
10
 
11
11
  # 退出行为归调用方;保留 argv 并只返回命令退出码。
12
12
  def run(argv)
13
- arguments = argv.dup
13
+ arguments = argv.map { |argument| String.new(argument) }
14
14
  command = arguments.shift
15
15
  if [ nil, "--help", "--help-all", "-h" ].include?(command) && arguments.empty?
16
16
  @out.puts @usage
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CtxCli
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/ctx_cli.rb CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  require_relative "ctx_cli/version"
4
4
  require_relative "ctx_cli/configuration"
5
- require_relative "ctx_cli/initialization"
6
- require_relative "ctx_cli/application"
5
+ require_relative "ctx_cli/initializer"
6
+ require_relative "ctx_cli/router"
7
7
  require_relative "ctx_cli/command"
8
8
  require_relative "ctx_cli/options"
9
9
  require_relative "ctx_cli/terminal"
10
10
  require_relative "ctx_cli/input"
11
11
  require_relative "ctx_cli/output"
12
12
  require_relative "ctx_cli/collection"
13
+ require_relative "ctx_cli/review"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ctx_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -126,14 +126,15 @@ files:
126
126
  - examples/configuration.rb
127
127
  - examples/offline.rb
128
128
  - lib/ctx_cli.rb
129
- - lib/ctx_cli/application.rb
130
129
  - lib/ctx_cli/collection.rb
131
130
  - lib/ctx_cli/command.rb
132
131
  - lib/ctx_cli/configuration.rb
133
- - lib/ctx_cli/initialization.rb
132
+ - lib/ctx_cli/initializer.rb
134
133
  - lib/ctx_cli/input.rb
135
134
  - lib/ctx_cli/options.rb
136
135
  - lib/ctx_cli/output.rb
136
+ - lib/ctx_cli/review.rb
137
+ - lib/ctx_cli/router.rb
137
138
  - lib/ctx_cli/terminal.rb
138
139
  - lib/ctx_cli/version.rb
139
140
  homepage: https://github.com/gatework/ctx_cli