ctx_cli 0.2.0 → 0.3.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 +3 -1
- data/docs/migration.md +7 -1
- data/docs/superpowers/plans/2026-10-24-configuration-source-paths.md +46 -0
- data/docs/superpowers/specs/2026-10-24-configuration-source-paths.md +27 -0
- data/lib/ctx_cli/collection.rb +14 -14
- data/lib/ctx_cli/command.rb +1 -1
- data/lib/ctx_cli/configuration.rb +4 -3
- data/lib/ctx_cli/options.rb +14 -14
- data/lib/ctx_cli/output.rb +1 -1
- data/lib/ctx_cli/review.rb +4 -4
- data/lib/ctx_cli/router.rb +1 -1
- data/lib/ctx_cli/terminal.rb +4 -4
- data/lib/ctx_cli/version.rb +1 -1
- metadata +5 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 993414ac12bd17c37f136bb85d871efc2f217c7ca81fb7c9c57faca557030686
|
|
4
|
+
data.tar.gz: d1cfd1282ef0e9ddf5cd23243bf09076d0ffc8fed4c00186f5471e5e96bbcb94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a72826b05f4e540be4999b61acbf9e89517c41949ccefd475e2e67ea039e39a49754f4ddcf179de83f164954032c0d250463db0b0ac6925d51e11665d98b6d2
|
|
7
|
+
data.tar.gz: aa88dbda8a415b0f3db1a52a9283a6711951e35c5003814f75724e1ace72ae35472d7f09beda808597c04f746d6dabe7fbd6946f4a55f241cd104c05e831c5a5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.0 (2026-10-24)
|
|
6
|
+
|
|
7
|
+
- Use JSON Pointer keys in configuration field provenance, separating literal dots, slashes, tildes, empty keys and nested mappings.
|
|
8
|
+
- Remove only actual descendant sources when a mapping is replaced; preserve array replacement and empty-mapping merge behavior.
|
|
9
|
+
- Breaking change: sources.fields and host --show-config output no longer expose dotted-path aliases; migrate field lookups to JSON Pointer.
|
|
10
|
+
|
|
11
|
+
## 0.2.1 (2026-10-23)
|
|
12
|
+
|
|
13
|
+
- Use English command help, prompts and collection/review diagnostics.
|
|
14
|
+
- Allow compatible dotenv versions without an upper version bound.
|
|
15
|
+
|
|
5
16
|
## 0.2.0 (2026-10-23)
|
|
6
17
|
|
|
7
18
|
- Rename Initialization to Initializer and Application to Router; remove the old constants and require paths in the 0.2.0 candidate.
|
data/README.md
CHANGED
|
@@ -53,6 +53,8 @@ files.environment # .env 加原进程环境;不修改 ENV
|
|
|
53
53
|
files.sources # 路径与字段来源,不包含环境值
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
`sources["fields"]` 使用 JSON Pointer 作为字段键,例如 `/parallel`、`/ssh/known_hosts`。字面键 `a.b` 对应 `/a.b`,嵌套 `a → b` 对应 `/a/b`;键内 `/` 编为 `~1`、`~` 编为 `~0`。数组作为整体字段记录来源。宿主的 `path_fields` 声明语法不变,来源索引不保留点分键别名。
|
|
57
|
+
|
|
56
58
|
默认依次搜索 `/etc/<namespace>`、`/usr/local/etc/<namespace>`、`$XDG_CONFIG_HOME/<namespace>`(否则 `~/.config/<namespace>`)、入口目录和启动目录。每层读取 `<namespace>.yml` 与 `<name>.yml`,显式文件最后。共享文件按 `common` 和工具名称分区;name 与 namespace 相同时使用普通独立文件。`directories:` 可以完整替换搜索范围。
|
|
57
59
|
|
|
58
60
|
宿主把各层公共环境值转换成领域字段后,通过 `Configuration.merge(defaults, dotenv_values, yaml, process_values, cli)` 实现 CLI > 进程环境 > YAML > .env > 默认值。数组整层替换,false/nil 保留。相对路径按来源目录解释;支持 `path_literals:`(如 `system`)和 URI。`load` 重复调用不重新读文件。
|
|
@@ -73,7 +75,7 @@ Options 协作者沿用现有入口协议:`informational?`、`validate_interac
|
|
|
73
75
|
bundle install
|
|
74
76
|
bundle exec rake
|
|
75
77
|
bundle exec rake build
|
|
76
|
-
ruby script/verify_package.rb pkg/ctx_cli-0.
|
|
78
|
+
ruby script/verify_package.rb pkg/ctx_cli-0.3.0.gem
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
默认任务包括 RSpec、RuboCop 和离线示例。安装验证在独立 GEM_HOME/GEM_PATH 下运行普通与最小 Bundler 应用,检查实际加载来源。CI 参照 ctx_parser 的固定 action 提交和候选制品校验;发布需另行明确授权。
|
data/docs/migration.md
CHANGED
|
@@ -17,4 +17,10 @@ ctx_cli 初始版本为 0.1.0。首次发布依赖顺序先 ctx_cli,再 net-co
|
|
|
17
17
|
- `CtxCli::Initialization` / `ctx_cli/initialization` 改为 `CtxCli::Initializer` / `ctx_cli/initializer`,仅负责模板安全发布。
|
|
18
18
|
- `CtxCli::Application` / `ctx_cli/application` 改为 `CtxCli::Router` / `ctx_cli/router`,仅负责全局信息和子命令分发。
|
|
19
19
|
|
|
20
|
-
旧常量和 require
|
|
20
|
+
旧常量和 require 路径不保留别名。宿主需同步修改引用,并在应用锁文件中选择包含新接口的 ctx_cli 版本。`create`、`existing?`、`run` 的结果约定不变;初始化参数询问和厂商语义不迁入这两个对象。这些名称从 0.2.0 起使用。
|
|
21
|
+
|
|
22
|
+
## 0.3.0 配置来源字段
|
|
23
|
+
|
|
24
|
+
`Configuration#sources["fields"]` 的键统一使用 [JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901):`parallel` 改为 `/parallel`,`ssh.port` 的嵌套路径改为 `/ssh/port`。字面键 `ssh.port` 则为 `/ssh.port`,两者拥有独立来源。空键为 `/`;每段中的 `~` 编为 `~0`,`/` 编为 `~1`,不作 Unicode 归一化。
|
|
25
|
+
|
|
26
|
+
来源值仍是 YAML 文件路径;数组整体替换,来源记在数组字段上。空映射合并不覆盖既有非空映射的来源。宿主透传的 `--show-config` 同步采用此结构;外部来源索引调用方须迁移键查找,不提供点分键回退或格式开关。`path_fields` 仍是宿主声明哪些字段要解析为文件路径的独立语法。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Configuration Source Paths Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Give every merged configuration field an unambiguous source identity.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Configuration emits JSON Pointer keys in sources.fields. Existing hosts forward the source map unchanged; their tests and documentation migrate together. No new abstraction or alternate format.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby, YAML, RSpec/Minitest, Bundler.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-10-24-configuration-source-paths.md`
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Ruby >= 3.4; no dependency or version-policy change.
|
|
16
|
+
- lib method comments Chinese; business literals English.
|
|
17
|
+
- Preserve WIP, no commit/push/release.
|
|
18
|
+
- Preserve merge semantics, source-relative paths and domain ownership.
|
|
19
|
+
- JSON Pointer applies to sources.fields only; no dot-path aliases.
|
|
20
|
+
|
|
21
|
+
## Review Focus
|
|
22
|
+
|
|
23
|
+
- Literal dots/slashes cannot collide with nesting.
|
|
24
|
+
- Tilde escape order preserves literal ~1 and ~0.
|
|
25
|
+
- Empty keys and Unicode keys retain exact identity without normalization.
|
|
26
|
+
- Replacing a parent removes only its actual descendant sources.
|
|
27
|
+
- Arrays and empty mappings keep their existing merge/source semantics.
|
|
28
|
+
|
|
29
|
+
### Task 1: Replace ambiguous source keys and migrate consumers
|
|
30
|
+
|
|
31
|
+
**Files:**
|
|
32
|
+
- Modify ctx_cli: `lib/ctx_cli/configuration.rb`, `spec/configuration_spec.rb`, `README.md`, `docs/migration.md`, `CHANGELOG.md`.
|
|
33
|
+
- Create ctx_cli: this plan and its spec.
|
|
34
|
+
- Modify VXLAN: `spec/cli/configuration_sources_spec.rb`.
|
|
35
|
+
- Modify Wireless: `spec/cli/options_spec.rb`.
|
|
36
|
+
- Modify net-connector: `test/config_test.rb`, `test/cli_setup_test.rb`.
|
|
37
|
+
|
|
38
|
+
**Interfaces:**
|
|
39
|
+
- Consumes: validated string-keyed YAML mappings and previous merged mapping.
|
|
40
|
+
- Produces: `Configuration#sources["fields"]` mapping from JSON Pointer to file path, deeply frozen; examples `/parallel`, `/ssh/port`, `/a.b`, `/a~1b`, `/m~0n`, `/`.
|
|
41
|
+
|
|
42
|
+
- [x] **Step 1:** Add real layered-YAML regressions for colliding literal and nested keys, parent replacement, escaped tokens, empty/Unicode keys, arrays and nil/false. Migrate existing source expectations to pointer keys.
|
|
43
|
+
- [x] **Step 2:** Run `bundle exec rspec spec/configuration_spec.rb`; expect RED from dot-path collisions or missing pointer keys.
|
|
44
|
+
- [x] **Step 3:** Change `record_fields` prefix construction to escaped slash tokens and descendant removal to slash-separated prefixes. Keep all merge and ownership logic. Update consumer source-key assertions and docs; no compatibility branch.
|
|
45
|
+
- [x] **Step 4:** Run ctx_cli default rake, consumer targeted tests and complete gates in isolated worktrees. Run the existing three-size benchmark before/after; verify same configuration/source counts. Expect green gates and no return to quadratic leaf scans.
|
|
46
|
+
- [x] **Step 5:** One independent final review. Guard-copy only listed files into actual checkouts, validate ordinary consumer gates using exact installed CLI candidate, and run plain/Bundler installed source/preview probes. Compare all package metadata/source bytes. Keep uncommitted work and evidence; mark all steps only with passing receipts.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Configuration 来源身份
|
|
2
|
+
|
|
3
|
+
## 问题与选择
|
|
4
|
+
|
|
5
|
+
Configuration 合并结果允许任意字符串键,但来源索引把层级拼为点分字符串。字面键 `a.b` 和嵌套 `a → b` 共享索引;用标量替换映射 `a` 时,前缀删除还会误删字面键的来源。这使 `--show-config` 的来源与实际配置不一致。
|
|
6
|
+
|
|
7
|
+
使用 [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901) JSON Pointer 表达 `sources["fields"]` 的键。每层以 `/` 分隔;token 内 `~` 编为 `~0`,`/` 编为 `~1`。字面点不转义,空键使用 `/`。只生成指针,不增加求值器或依赖。来源值仍为文件路径,不含配置值。
|
|
8
|
+
|
|
9
|
+
拒绝含点的合法键会人为限制宿主数据;自定义点转义会新增不必要的语法。采用标准指针统一迁移,不保留点分别名或旧格式选择。
|
|
10
|
+
|
|
11
|
+
## 不变量与职责
|
|
12
|
+
|
|
13
|
+
- 合并、来源相对路径、数组替换、false/nil/缺失、空映射合并语义保持不变。
|
|
14
|
+
- 来源索引的叶子/祖先替换必须与实际合并一致;兄弟字面键不能被前缀删除。
|
|
15
|
+
- 数组是整体替换值,在数组字段指针上记录来源,不逐元素建表;空映射作为值时也记录其字段。
|
|
16
|
+
- `path_fields` 是宿主声明路径字段的既有语法,和来源输出不同,不在此阶段改变。
|
|
17
|
+
- 库保持 Ruby >= 3.4,不依赖 Runner/Parser/connector;lib 方法说明中文、业务字面量英文。
|
|
18
|
+
- VXLAN/Wireless/connector 仍透传来源结构;其领域校验、命令、状态与资源所有权不迁移。
|
|
19
|
+
- 保留所有既有 WIP 和当前依赖政策,不提交、推送或发布。
|
|
20
|
+
|
|
21
|
+
## 范围
|
|
22
|
+
|
|
23
|
+
11 个文件:ctx_cli 的 configuration.rb、configuration_spec.rb、README、migration、Changelog、本规格及计划;VXLAN/Wireless 各一个参数来源测试;connector 的 config_test 和 cli_setup_test。没有新增配置项或生产适配器。
|
|
24
|
+
|
|
25
|
+
## 验收
|
|
26
|
+
|
|
27
|
+
回归从真实 YAML 层证明字面点与层级不同、slash/tilde/空键/Unicode 不碰撞、替换映射只删除真正后代、数组整体替换、冻结及不含秘密值。先观察旧实现失败,再修改来源键构造与后代前缀。迁移消费端断言后运行四模块 gates、实际入口和精确候选普通/Bundler 安装。使用既有 500/1000/3000 字段基准检查规模趋势,不为共享主机设硬性能阈值。一次独立审查,无双格式接口。
|
data/lib/ctx_cli/collection.rb
CHANGED
|
@@ -33,13 +33,13 @@ module CtxCli
|
|
|
33
33
|
return 0 if @options.missing?(input: @input, error: @error)
|
|
34
34
|
@options.load
|
|
35
35
|
@progress = @progress_factory.call(@options)
|
|
36
|
-
@progress.notice("
|
|
36
|
+
@progress.notice("Configuration: #{@options.config}")
|
|
37
37
|
return output(@options.public_values.merge("sources" => @options.sources)) if @options.flags[:show_config]
|
|
38
38
|
@devices, @inventory_total = @adapter.inventory(@options, progress: @progress)
|
|
39
39
|
devices = @devices
|
|
40
40
|
return output(devices) if @options.flags[:inventory_only]
|
|
41
41
|
if devices.empty?
|
|
42
|
-
@progress.notice("
|
|
42
|
+
@progress.notice("No devices matched; no connections opened")
|
|
43
43
|
@output.json({ status: :no_devices, total: 0 }, pretty: false) if @options.flags[:json]
|
|
44
44
|
return 1
|
|
45
45
|
end
|
|
@@ -47,7 +47,7 @@ module CtxCli
|
|
|
47
47
|
@store = @adapter.report(@options)
|
|
48
48
|
@batch = @batch_class.new(report: @store, progress: @progress, requested: devices)
|
|
49
49
|
outcome = @batch.run(present: ->(summary) { @output.json(summary) if @options.flags[:json] }) do
|
|
50
|
-
@progress.plan(devices: devices, total: @inventory_total, parallel: @options.values.fetch("parallel"), label: "#{@label}
|
|
50
|
+
@progress.plan(devices: devices, total: @inventory_total, parallel: @options.values.fetch("parallel"), label: "#{@label} read-only collection")
|
|
51
51
|
@progress.with_updates do
|
|
52
52
|
@adapter.execute(devices, @options) do |device|
|
|
53
53
|
@batch.collect(device) { @adapter.collect(device, @options, context: context, progress: @progress) }
|
|
@@ -57,19 +57,19 @@ module CtxCli
|
|
|
57
57
|
end
|
|
58
58
|
completion(outcome)
|
|
59
59
|
rescue Interrupt
|
|
60
|
-
diagnostic "
|
|
60
|
+
diagnostic "Collection cancelled; saved device reports retained; check report status for unsaved results"
|
|
61
61
|
130
|
|
62
62
|
rescue SystemCallError, IOError => error
|
|
63
|
-
diagnostic "#{@label}
|
|
63
|
+
diagnostic "#{@label} argument or report error: #{report_diagnostic(error)}"
|
|
64
64
|
2
|
|
65
65
|
rescue Error, Configuration::Error => error
|
|
66
|
-
diagnostic "#{@label}
|
|
66
|
+
diagnostic "#{@label} argument, dependency or report error: #{error.message}; use --help for options"
|
|
67
67
|
2
|
|
68
68
|
rescue OptionParser::ParseError, ArgumentError, LoadError => error
|
|
69
|
-
diagnostic "#{@label}
|
|
69
|
+
diagnostic "#{@label} argument, dependency or report error: #{error.class.name}; use --help for options"
|
|
70
70
|
2
|
|
71
71
|
rescue StandardError => error
|
|
72
|
-
diagnostic "#{@label}
|
|
72
|
+
diagnostic "#{@label} inventory or execution failed: #{@failure_code.call(error)} (#{error.class.name})"
|
|
73
73
|
2
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -82,18 +82,18 @@ module CtxCli
|
|
|
82
82
|
# Runner 决定批次结论;CLI 仅呈现安全诊断并映射退出码。
|
|
83
83
|
def completion(outcome)
|
|
84
84
|
if outcome.finalization_error
|
|
85
|
-
diagnostic "#{@label}
|
|
85
|
+
diagnostic "#{@label} summary report error: #{outcome.finalization_error}"
|
|
86
86
|
end
|
|
87
87
|
if outcome.recovery_failed
|
|
88
|
-
diagnostic "
|
|
88
|
+
diagnostic "Summary recovery is unconfirmed; check the exit code and report directory. An earlier summary does not confirm this run succeeded."
|
|
89
89
|
end
|
|
90
90
|
if outcome.status == :interrupted
|
|
91
|
-
diagnostic "
|
|
91
|
+
diagnostic "Collection cancelled; saved device reports retained; check report status for unsaved results"
|
|
92
92
|
elsif (failure = outcome.failure)
|
|
93
93
|
if failure[:kind] == :report
|
|
94
|
-
diagnostic "#{@label}
|
|
94
|
+
diagnostic "#{@label} argument or report error: #{failure[:detail]}"
|
|
95
95
|
else
|
|
96
|
-
diagnostic "#{@label}
|
|
96
|
+
diagnostic "#{@label} inventory or execution failed: #{failure[:error_type]}; see the batch report for collected results"
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
{ complete: 0, incomplete: 1, failed: 2, interrupted: 130 }.fetch(outcome.status)
|
|
@@ -115,7 +115,7 @@ module CtxCli
|
|
|
115
115
|
@output.json(value)
|
|
116
116
|
else
|
|
117
117
|
value.each { |device| @out.puts "#{device.fetch('host')}\t#{device.fetch('name')}\t#{device['model']}" }
|
|
118
|
-
@out.puts "
|
|
118
|
+
@out.puts "Devices: #{value.size}; no device connections opened"
|
|
119
119
|
end
|
|
120
120
|
value.is_a?(Array) && value.empty? ? 1 : 0
|
|
121
121
|
end
|
data/lib/ctx_cli/command.rb
CHANGED
|
@@ -18,7 +18,7 @@ module CtxCli
|
|
|
18
18
|
@parser.banner = @banner
|
|
19
19
|
@configure&.call(@parser, @values)
|
|
20
20
|
rest = @parser.parse(argv.dup)
|
|
21
|
-
raise ArgumentError, "
|
|
21
|
+
raise ArgumentError, "Positional arguments are not accepted" unless rest.empty?
|
|
22
22
|
@values
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -341,9 +341,10 @@ module CtxCli
|
|
|
341
341
|
self.class.merge(previous, resolved)
|
|
342
342
|
end
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
# 用 JSON Pointer 区分字面键与层级,只移除被替换映射的真正后代来源。
|
|
345
|
+
def record_fields(document, source, fields, prefix = "", previous: {})
|
|
345
346
|
document.each do |key, value|
|
|
346
|
-
name =
|
|
347
|
+
name = "#{prefix}/#{key.gsub("~", "~0").gsub("/", "~1")}"
|
|
347
348
|
before = previous.is_a?(Hash) ? previous[key] : nil
|
|
348
349
|
next if value.is_a?(Hash) && value.empty? && before.is_a?(Hash) && !before.empty?
|
|
349
350
|
|
|
@@ -353,7 +354,7 @@ module CtxCli
|
|
|
353
354
|
else
|
|
354
355
|
# 只有旧映射被整体替换才需要删除后代;新增/覆盖叶子直接赋值。
|
|
355
356
|
if before.is_a?(Hash) && !before.empty?
|
|
356
|
-
descendant_prefix = "#{name}
|
|
357
|
+
descendant_prefix = "#{name}/"
|
|
357
358
|
fields.delete_if { |field, _| field.start_with?(descendant_prefix) }
|
|
358
359
|
end
|
|
359
360
|
fields[name] = source
|
data/lib/ctx_cli/options.rb
CHANGED
|
@@ -4,34 +4,34 @@ module CtxCli
|
|
|
4
4
|
# 普通参数分组只登记语法;不加载配置、不执行操作、不拥有领域校验。
|
|
5
5
|
module Options
|
|
6
6
|
def self.initialization(parser, values)
|
|
7
|
-
parser.on("--init", "
|
|
8
|
-
parser.on("--interactive", "
|
|
9
|
-
parser.on("--non-interactive", "
|
|
7
|
+
parser.on("--init", "Create missing configuration and environment templates, then exit; preserve existing files") { values[:init] = true }
|
|
8
|
+
parser.on("--interactive", "Prompt to create missing configuration; requires an interactive terminal") { values[:interactive] = true }
|
|
9
|
+
parser.on("--non-interactive", "Disable prompts; specify --init to create files") { values[:interactive] = false }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def self.output(parser, values, quiet: :quiet)
|
|
13
|
-
parser.on("--json", "stdout
|
|
14
|
-
parser.on("--quiet", "--no-progress", "
|
|
15
|
-
parser.on("--verbose", "
|
|
13
|
+
parser.on("--json", "Write JSON to stdout; defaults to a readable summary") { values[:json] = true }
|
|
14
|
+
parser.on("--quiet", "--no-progress", "Disable progress on stderr; can be combined with --json") { values[quiet] = true }
|
|
15
|
+
parser.on("--verbose", "Show per-device results and safe command events") { values[:verbose] = true }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def self.information(parser, values)
|
|
19
|
-
parser.on("-h", "--help", "
|
|
20
|
-
parser.on("--help-all", "
|
|
21
|
-
parser.on("--examples", "
|
|
19
|
+
parser.on("-h", "--help", "Show option help") { values[:help] = true }
|
|
20
|
+
parser.on("--help-all", "Show all options, paths and environment conventions") { values[:help_all] = true }
|
|
21
|
+
parser.on("--examples", "Show common command examples without executing tasks") { values[:examples] = true }
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# 值转换留在宿主边界,允许不同入口沿用原有存储格式。
|
|
25
|
-
def self.configuration(parser, parallel_help: "
|
|
26
|
-
parser.on("--config FILE", "
|
|
27
|
-
parser.on("--env-file FILE", "
|
|
25
|
+
def self.configuration(parser, parallel_help: "Device concurrency", &store)
|
|
26
|
+
parser.on("--config FILE", "Overlay explicit YAML after automatically discovered configuration") { |value| store.call(:config, value) }
|
|
27
|
+
parser.on("--env-file FILE", "Load explicit environment files last; process environment takes precedence") { |value| store.call(:env_file, value) }
|
|
28
28
|
parser.on("-p", "--parallel N", Integer, parallel_help) { |value| store.call(:parallel, value) }
|
|
29
|
-
parser.on("--directory DIR", "
|
|
29
|
+
parser.on("--directory DIR", "Directory for per-device reports") { |value| store.call(:directory, value) }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def self.list(value)
|
|
33
33
|
values = value.split(",", -1).map(&:strip)
|
|
34
|
-
raise ArgumentError, "
|
|
34
|
+
raise ArgumentError, "Lists must not contain empty entries" if values.any?(&:empty?)
|
|
35
35
|
values
|
|
36
36
|
end
|
|
37
37
|
end
|
data/lib/ctx_cli/output.rb
CHANGED
|
@@ -14,7 +14,7 @@ module CtxCli
|
|
|
14
14
|
|
|
15
15
|
# 最后的错误流失败不能覆盖原本的失败或取消状态。
|
|
16
16
|
def diagnostic(message, exception: nil)
|
|
17
|
-
@error.puts(exception ? "#{message}
|
|
17
|
+
@error.puts(exception ? "#{message} (#{exception.class.name})" : message)
|
|
18
18
|
rescue StandardError
|
|
19
19
|
nil
|
|
20
20
|
end
|
data/lib/ctx_cli/review.rb
CHANGED
|
@@ -23,10 +23,10 @@ module CtxCli
|
|
|
23
23
|
def run(argv)
|
|
24
24
|
options = @command.parse(argv)
|
|
25
25
|
return 0 if @command.informational?
|
|
26
|
-
raise ArgumentError, "
|
|
26
|
+
raise ArgumentError, "A batch directory is required" unless options[:batch]
|
|
27
27
|
batch = File.expand_path(options[:batch])
|
|
28
28
|
paths = source_paths(File.join(batch, @directory))
|
|
29
|
-
raise ArgumentError, "
|
|
29
|
+
raise ArgumentError, "No device reports found" if paths.empty?
|
|
30
30
|
remaining = @max_total_bytes
|
|
31
31
|
sources = paths.map do |path|
|
|
32
32
|
raise ArgumentError, "review byte budget exceeded" unless remaining.positive?
|
|
@@ -38,10 +38,10 @@ module CtxCli
|
|
|
38
38
|
@output.text(@adapter.publish(sources, options, batch: batch, context: context))
|
|
39
39
|
0
|
|
40
40
|
rescue Interrupt
|
|
41
|
-
@output.diagnostic("
|
|
41
|
+
@output.diagnostic("Review cancelled; existing reports retained; no device changes executed.")
|
|
42
42
|
130
|
|
43
43
|
rescue StandardError => error
|
|
44
|
-
@output.diagnostic("
|
|
44
|
+
@output.diagnostic("Review failed (#{error.class.name}); check the input structure, directory permissions and --help; no device changes executed.")
|
|
45
45
|
2
|
|
46
46
|
end
|
|
47
47
|
|
data/lib/ctx_cli/router.rb
CHANGED
|
@@ -21,7 +21,7 @@ module CtxCli
|
|
|
21
21
|
return 0
|
|
22
22
|
end
|
|
23
23
|
return @commands.fetch(command).call(arguments) if @commands.key?(command)
|
|
24
|
-
@error.puts "
|
|
24
|
+
@error.puts "Unknown command or extra arguments; use #{@name} --help for usage"
|
|
25
25
|
2
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/ctx_cli/terminal.rb
CHANGED
|
@@ -10,7 +10,7 @@ module CtxCli
|
|
|
10
10
|
def self.tty?(*streams) = streams.all? { |stream| stream.respond_to?(:tty?) && stream.tty? }
|
|
11
11
|
def tty?(*others) = self.class.tty?(@input, @out, *others)
|
|
12
12
|
|
|
13
|
-
def require_tty!(*others, message: "
|
|
13
|
+
def require_tty!(*others, message: "An interactive terminal is required")
|
|
14
14
|
raise ArgumentError, message unless tty?(*others)
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -26,13 +26,13 @@ module CtxCli
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def password(label:)
|
|
29
|
-
raise ArgumentError, "
|
|
29
|
+
raise ArgumentError, "Password prompts require an interactive terminal; use environment variables or --stdin-credentials for automation" unless self.class.tty?(@input)
|
|
30
30
|
require "io/console"
|
|
31
|
-
@out.print "#{label}
|
|
31
|
+
@out.print "#{label}: "
|
|
32
32
|
@out.flush
|
|
33
33
|
value = @input.noecho { |terminal| terminal.gets("\n") }&.chomp("\n")
|
|
34
34
|
@out.puts
|
|
35
|
-
raise ArgumentError, "
|
|
35
|
+
raise ArgumentError, "Password must not be empty" if value.nil? || value.empty?
|
|
36
36
|
value
|
|
37
37
|
end
|
|
38
38
|
end
|
data/lib/ctx_cli/version.rb
CHANGED
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -15,20 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
19
|
-
- - "<"
|
|
20
|
-
- !ruby/object:Gem::Version
|
|
21
|
-
version: '4'
|
|
18
|
+
version: '0'
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
22
|
requirements:
|
|
26
23
|
- - ">="
|
|
27
24
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '
|
|
29
|
-
- - "<"
|
|
30
|
-
- !ruby/object:Gem::Version
|
|
31
|
-
version: '4'
|
|
25
|
+
version: '0'
|
|
32
26
|
- !ruby/object:Gem::Dependency
|
|
33
27
|
name: fileutils
|
|
34
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -123,6 +117,8 @@ files:
|
|
|
123
117
|
- LICENSE
|
|
124
118
|
- README.md
|
|
125
119
|
- docs/migration.md
|
|
120
|
+
- docs/superpowers/plans/2026-10-24-configuration-source-paths.md
|
|
121
|
+
- docs/superpowers/specs/2026-10-24-configuration-source-paths.md
|
|
126
122
|
- examples/configuration.rb
|
|
127
123
|
- examples/offline.rb
|
|
128
124
|
- lib/ctx_cli.rb
|