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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +18 -4
- data/docs/migration.md +7 -0
- data/examples/configuration.rb +1 -1
- data/lib/ctx_cli/collection.rb +8 -1
- data/lib/ctx_cli/configuration.rb +48 -15
- data/lib/ctx_cli/{initialization.rb → initializer.rb} +15 -2
- data/lib/ctx_cli/input.rb +8 -2
- data/lib/ctx_cli/review.rb +65 -0
- data/lib/ctx_cli/{application.rb → router.rb} +2 -2
- data/lib/ctx_cli/version.rb +1 -1
- data/lib/ctx_cli.rb +3 -2
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2e8622dd69aeb8ad48fb4e9b5d0212345698f81b78f042d4dfa761bdf0c3890
|
|
4
|
+
data.tar.gz: b579bf0a5ad0a3d795249f4e8d0048366120f5a8f857f95a29901c78266b28f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
| `
|
|
9
|
+
| `Router` | 惰性子命令分发、全局帮助/版本、返回退出码 |
|
|
10
10
|
| `Command` / `Options` | 每次独立的参数快照、信息分支、公共 OptionParser 分组 |
|
|
11
11
|
| `Configuration` | 多层文件发现、隔离环境、来源路径、纯数据快照 |
|
|
12
|
-
| `
|
|
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
|
-
`
|
|
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.
|
|
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 当前为本地候选,尚未发布。
|
data/examples/configuration.rb
CHANGED
|
@@ -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::
|
|
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 })
|
data/lib/ctx_cli/collection.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
91
|
+
common, native, native_present = shared_sections(document, path)
|
|
86
92
|
merged = merge_layer(merged, common, path, fields)
|
|
87
|
-
|
|
93
|
+
applicable ||= native_present
|
|
88
94
|
else
|
|
89
95
|
native = document
|
|
90
96
|
validate_string_keys!(native, path)
|
|
91
|
-
|
|
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
|
-
#
|
|
142
|
+
# 每层只复制一次,再合并拥有的映射;数组、false、nil 按值替换。
|
|
136
143
|
def self.merge(*hashes)
|
|
137
144
|
hashes.reduce({}) do |result, hash|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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| [
|
|
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|
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
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.
|
|
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
|
data/lib/ctx_cli/version.rb
CHANGED
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/
|
|
6
|
-
require_relative "ctx_cli/
|
|
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.
|
|
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/
|
|
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
|