devconfig-gen-singbox 2.1.0__tar.gz → 2.1.2__tar.gz

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 (95) hide show
  1. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/PKG-INFO +20 -11
  2. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/README.md +19 -10
  3. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/pyproject.toml +2 -3
  4. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/__init__.py +3 -1
  5. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/cli.py +67 -1
  6. devconfig_gen_singbox-2.1.2/src/devconfig_gen/interactive.py +377 -0
  7. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/models.py +25 -4
  8. devconfig_gen_singbox-2.1.2/src/devconfig_gen/web_ui.py +2508 -0
  9. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen_singbox.egg-info/PKG-INFO +20 -11
  10. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen_singbox.egg-info/SOURCES.txt +6 -1
  11. devconfig_gen_singbox-2.1.2/src/devconfig_gen_singbox.egg-info/entry_points.txt +2 -0
  12. devconfig_gen_singbox-2.1.2/src/singbox_ops/cli.py +207 -0
  13. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_cli_e2e.py +1 -1
  14. devconfig_gen_singbox-2.1.2/tests/test_interactive.py +238 -0
  15. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_cli.py +13 -0
  16. devconfig_gen_singbox-2.1.2/tests/test_web_ui.py +224 -0
  17. devconfig_gen_singbox-2.1.2/tests/test_web_ui_widgets.py +146 -0
  18. devconfig_gen_singbox-2.1.0/src/devconfig_gen_singbox.egg-info/entry_points.txt +0 -4
  19. devconfig_gen_singbox-2.1.0/src/singbox_ops/cli.py +0 -109
  20. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/LICENSE +0 -0
  21. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/setup.cfg +0 -0
  22. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/engine.py +0 -0
  23. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/formats.py +0 -0
  24. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/__init__.py +0 -0
  25. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/custom.py +0 -0
  26. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/env_provider.py +0 -0
  27. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/json_provider.py +0 -0
  28. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/__init__.py +0 -0
  29. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/data/rules.json +0 -0
  30. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/links.py +0 -0
  31. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/models.py +0 -0
  32. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/plugins/__init__.py +0 -0
  33. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/plugins/anytls.py +0 -0
  34. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/plugins/base.py +0 -0
  35. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/plugins/hysteria2.py +0 -0
  36. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/plugins/tuic.py +0 -0
  37. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/provider.py +0 -0
  38. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/route.py +0 -0
  39. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/providers/singbox/schema.py +0 -0
  40. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/py.typed +0 -0
  41. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/registry.py +0 -0
  42. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen/validation.py +0 -0
  43. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen_singbox.egg-info/dependency_links.txt +0 -0
  44. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen_singbox.egg-info/requires.txt +0 -0
  45. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/devconfig_gen_singbox.egg-info/top_level.txt +0 -0
  46. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/__init__.py +0 -0
  47. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/__init__.py +0 -0
  48. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/acme/__init__.py +0 -0
  49. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/acme/base.py +0 -0
  50. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/acme/cloudflare_dns01.py +0 -0
  51. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/base.py +0 -0
  52. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/dns/__init__.py +0 -0
  53. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/dns/base.py +0 -0
  54. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/dns/cloudflare.py +0 -0
  55. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/export/__init__.py +0 -0
  56. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/export/local_files.py +0 -0
  57. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/runtime/__init__.py +0 -0
  58. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/runtime/nftables.py +0 -0
  59. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/runtime/packages.py +0 -0
  60. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/runtime/systemd.py +0 -0
  61. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/runtime/warp_watchdog.py +0 -0
  62. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/secrets/__init__.py +0 -0
  63. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/secrets/base.py +0 -0
  64. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/secrets/python_secrets.py +0 -0
  65. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/secrets/singbox_subprocess.py +0 -0
  66. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/secrets/static.py +0 -0
  67. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/state/__init__.py +0 -0
  68. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/adapters/state/local_json.py +0 -0
  69. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/__init__.py +0 -0
  70. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/command.py +0 -0
  71. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/context_builder.py +0 -0
  72. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/exceptions.py +0 -0
  73. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/network.py +0 -0
  74. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/orchestrator.py +0 -0
  75. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/plan.py +0 -0
  76. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/core/protocols.py +0 -0
  77. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/templates/singbox_auto_update.py.template +0 -0
  78. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/src/singbox_ops/templates/warp_watchdog.sh.template +0 -0
  79. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_api_parity.py +0 -0
  80. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_custom_provider.py +0 -0
  81. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_devconfig_core.py +0 -0
  82. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_env_provider.py +0 -0
  83. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_formats.py +0 -0
  84. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_merge.py +0 -0
  85. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_provider_metadata.py +0 -0
  86. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_acme.py +0 -0
  87. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_context_builder.py +0 -0
  88. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_dns.py +0 -0
  89. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_orchestrator.py +0 -0
  90. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_plan.py +0 -0
  91. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_runtime.py +0 -0
  92. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_secrets.py +0 -0
  93. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_ops_state_export.py +0 -0
  94. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_singbox_provider.py +0 -0
  95. {devconfig_gen_singbox-2.1.0 → devconfig_gen_singbox-2.1.2}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devconfig_gen_singbox
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Provider-based developer configuration generation engine and CLI
5
5
  Author-email: henryliu443 <henryliu443@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -28,7 +28,7 @@ Dynamic: license-file
28
28
  > **A sing-box domain implementation built on top of DevConfig-Gen.**
29
29
  > **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
30
30
 
31
- [![Version](https://img.shields.io/badge/version-2.1.0-blue.svg)](CHANGELOG.md)
31
+ [![Version](https://img.shields.io/badge/version-2.1.2-blue.svg)](CHANGELOG.md)
32
32
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
33
33
  ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)
34
34
  ![Tests](https://img.shields.io/badge/tests-136%20passing-brightgreen.svg)
@@ -62,7 +62,7 @@ devconfig_gen_singbox
62
62
  git clone https://github.com/henryliu443/devconfig_gen_singbox.git
63
63
  cd devconfig_gen_singbox
64
64
  pip install -e ".[yaml]"
65
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist
65
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist
66
66
  ```
67
67
 
68
68
  未安装时亦可直接通过源码运行:
@@ -226,9 +226,9 @@ options:
226
226
  ## 输入示例 / Input Example
227
227
 
228
228
  ```bash
229
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist
230
- devconfig-gen validate --provider singbox --input examples/singbox.yaml
231
- devconfig-gen schema --provider singbox
229
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist
230
+ devconfig_gen_singbox validate --provider singbox --input examples/singbox.yaml
231
+ devconfig_gen_singbox schema --provider singbox
232
232
  ```
233
233
 
234
234
  完整 context 见 [`examples/singbox.yaml`](examples/singbox.yaml)。
@@ -281,13 +281,14 @@ singbox-ops destroy --plan examples/singbox-deploy.yaml
281
281
  - 结构化校验与诊断(`Diagnostic` / `ValidationError`)
282
282
  - JSON/YAML 序列化(标准库 + 可选 PyYAML,内置子集解析器兜底)
283
283
  - 多源合并与 dotted-path 覆盖(`deep_merge`、`--set`)
284
- - CLI 与 Python API
284
+ - CLI、**终端向导**(`devconfig_gen_singbox init`)、**本地 Web 工作台**
285
+ (`devconfig_gen_singbox ui`)与 Python API
285
286
 
286
287
  同时也继承中立的 Provider `custom` / `json` / `env`。本仓库在其之上新增领域
287
288
  Provider `singbox`:
288
289
 
289
290
  ```bash
290
- devconfig-gen providers
291
+ devconfig_gen_singbox providers
291
292
  # custom / env / json / singbox
292
293
  ```
293
294
 
@@ -298,9 +299,15 @@ devconfig-gen providers
298
299
 
299
300
  ```bash
300
301
  # 生成 / 校验 / 查看 schema
301
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist --format yaml
302
- devconfig-gen validate --provider singbox --input examples/singbox.yaml --json
303
- devconfig-gen schema --provider singbox
302
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist --format yaml
303
+ devconfig_gen_singbox validate --provider singbox --input examples/singbox.yaml --json
304
+ devconfig_gen_singbox schema --provider singbox
305
+
306
+ # 交互式终端向导(所有 provider 通用,含 singbox)
307
+ devconfig_gen_singbox init --provider singbox --output-dir dist
308
+
309
+ # 本地 Web 工作台(零构建,标准库 http.server)
310
+ devconfig_gen_singbox ui --port 8848
304
311
  ```
305
312
 
306
313
  ```python
@@ -348,6 +355,8 @@ generate_pipeline(
348
355
  | 校验与诊断 | [`docs/validation.md`](docs/validation.md) | [validation](https://henryliu443.github.io/DevConfig-Gen/docs/validation/) |
349
356
  | Provider 开发 | [`docs/providers.md`](docs/providers.md) | [providers](https://henryliu443.github.io/DevConfig-Gen/docs/providers/) |
350
357
  | Python API | [`docs/python-api.md`](docs/python-api.md) | [python-api](https://henryliu443.github.io/DevConfig-Gen/docs/python-api/) |
358
+ | 终端向导 | [`docs/wizard.md`](docs/wizard.md) | [wizard](https://henryliu443.github.io/DevConfig-Gen/docs/wizard/) |
359
+ | Web 工作台 | [`docs/web-ui.md`](docs/web-ui.md) | [web-ui](https://henryliu443.github.io/DevConfig-Gen/docs/web-ui/) |
351
360
  | 架构总览 | [`ARCHITECTURE.md`](ARCHITECTURE.md) | — |
352
361
  | Provider 铁标准 | [`PROVIDER_STANDARD.md`](PROVIDER_STANDARD.md) | — |
353
362
 
@@ -3,7 +3,7 @@
3
3
  > **A sing-box domain implementation built on top of DevConfig-Gen.**
4
4
  > **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
5
5
 
6
- [![Version](https://img.shields.io/badge/version-2.1.0-blue.svg)](CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/version-2.1.2-blue.svg)](CHANGELOG.md)
7
7
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
8
  ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)
9
9
  ![Tests](https://img.shields.io/badge/tests-136%20passing-brightgreen.svg)
@@ -37,7 +37,7 @@ devconfig_gen_singbox
37
37
  git clone https://github.com/henryliu443/devconfig_gen_singbox.git
38
38
  cd devconfig_gen_singbox
39
39
  pip install -e ".[yaml]"
40
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist
40
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist
41
41
  ```
42
42
 
43
43
  未安装时亦可直接通过源码运行:
@@ -201,9 +201,9 @@ options:
201
201
  ## 输入示例 / Input Example
202
202
 
203
203
  ```bash
204
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist
205
- devconfig-gen validate --provider singbox --input examples/singbox.yaml
206
- devconfig-gen schema --provider singbox
204
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist
205
+ devconfig_gen_singbox validate --provider singbox --input examples/singbox.yaml
206
+ devconfig_gen_singbox schema --provider singbox
207
207
  ```
208
208
 
209
209
  完整 context 见 [`examples/singbox.yaml`](examples/singbox.yaml)。
@@ -256,13 +256,14 @@ singbox-ops destroy --plan examples/singbox-deploy.yaml
256
256
  - 结构化校验与诊断(`Diagnostic` / `ValidationError`)
257
257
  - JSON/YAML 序列化(标准库 + 可选 PyYAML,内置子集解析器兜底)
258
258
  - 多源合并与 dotted-path 覆盖(`deep_merge`、`--set`)
259
- - CLI 与 Python API
259
+ - CLI、**终端向导**(`devconfig_gen_singbox init`)、**本地 Web 工作台**
260
+ (`devconfig_gen_singbox ui`)与 Python API
260
261
 
261
262
  同时也继承中立的 Provider `custom` / `json` / `env`。本仓库在其之上新增领域
262
263
  Provider `singbox`:
263
264
 
264
265
  ```bash
265
- devconfig-gen providers
266
+ devconfig_gen_singbox providers
266
267
  # custom / env / json / singbox
267
268
  ```
268
269
 
@@ -273,9 +274,15 @@ devconfig-gen providers
273
274
 
274
275
  ```bash
275
276
  # 生成 / 校验 / 查看 schema
276
- devconfig-gen generate --provider singbox --input examples/singbox.yaml --output-dir dist --format yaml
277
- devconfig-gen validate --provider singbox --input examples/singbox.yaml --json
278
- devconfig-gen schema --provider singbox
277
+ devconfig_gen_singbox generate --provider singbox --input examples/singbox.yaml --output-dir dist --format yaml
278
+ devconfig_gen_singbox validate --provider singbox --input examples/singbox.yaml --json
279
+ devconfig_gen_singbox schema --provider singbox
280
+
281
+ # 交互式终端向导(所有 provider 通用,含 singbox)
282
+ devconfig_gen_singbox init --provider singbox --output-dir dist
283
+
284
+ # 本地 Web 工作台(零构建,标准库 http.server)
285
+ devconfig_gen_singbox ui --port 8848
279
286
  ```
280
287
 
281
288
  ```python
@@ -323,6 +330,8 @@ generate_pipeline(
323
330
  | 校验与诊断 | [`docs/validation.md`](docs/validation.md) | [validation](https://henryliu443.github.io/DevConfig-Gen/docs/validation/) |
324
331
  | Provider 开发 | [`docs/providers.md`](docs/providers.md) | [providers](https://henryliu443.github.io/DevConfig-Gen/docs/providers/) |
325
332
  | Python API | [`docs/python-api.md`](docs/python-api.md) | [python-api](https://henryliu443.github.io/DevConfig-Gen/docs/python-api/) |
333
+ | 终端向导 | [`docs/wizard.md`](docs/wizard.md) | [wizard](https://henryliu443.github.io/DevConfig-Gen/docs/wizard/) |
334
+ | Web 工作台 | [`docs/web-ui.md`](docs/web-ui.md) | [web-ui](https://henryliu443.github.io/DevConfig-Gen/docs/web-ui/) |
326
335
  | 架构总览 | [`ARCHITECTURE.md`](ARCHITECTURE.md) | — |
327
336
  | Provider 铁标准 | [`PROVIDER_STANDARD.md`](PROVIDER_STANDARD.md) | — |
328
337
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devconfig_gen_singbox"
7
- version = "2.1.0"
7
+ version = "2.1.2"
8
8
  authors = [
9
9
  { name = "henryliu443", email = "henryliu443@gmail.com" },
10
10
  ]
@@ -26,9 +26,8 @@ dev = ["build", "wheel", "PyYAML>=5.1", "cryptography>=3.4"]
26
26
  docs = ["mkdocs-material>=9.5,<10"]
27
27
 
28
28
  [project.scripts]
29
- devconfig-gen = "devconfig_gen.cli:main"
29
+ # One command, named exactly after the PyPI distribution.
30
30
  devconfig_gen_singbox = "devconfig_gen.cli:main"
31
- singbox-ops = "singbox_ops.cli:main"
32
31
 
33
32
  [tool.setuptools.packages.find]
34
33
  where = ["src"]
@@ -32,6 +32,7 @@ from .models import (
32
32
  GenerationResult,
33
33
  ProviderField,
34
34
  ProviderStep,
35
+ WebUIWidgets,
35
36
  )
36
37
  from .registry import ProviderRegistry, default_registry
37
38
  from .validation import ValidationError
@@ -47,6 +48,7 @@ __all__ = [
47
48
  "ProviderRegistry",
48
49
  "ProviderStep",
49
50
  "ValidationError",
51
+ "WebUIWidgets",
50
52
  "build_request",
51
53
  "coerce_scalar",
52
54
  "deep_merge",
@@ -64,4 +66,4 @@ __all__ = [
64
66
  "loads",
65
67
  "validate_request",
66
68
  ]
67
- __version__ = "2.1.0"
69
+ __version__ = "2.1.2"
@@ -14,7 +14,7 @@ from .registry import default_registry
14
14
 
15
15
  def build_parser() -> argparse.ArgumentParser:
16
16
  parser = argparse.ArgumentParser(
17
- prog="devconfig-gen",
17
+ prog="devconfig_gen_singbox",
18
18
  description="Generate and validate structured JSON/YAML configuration.",
19
19
  )
20
20
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
@@ -74,9 +74,51 @@ def build_parser() -> argparse.ArgumentParser:
74
74
  schema_cmd.add_argument("--provider", default="json", help="Provider name (default: json)")
75
75
  schema_cmd.set_defaults(handler=_schema)
76
76
 
77
+ init_cmd = sub.add_parser(
78
+ "init",
79
+ help="Run interactive terminal wizard to create configuration",
80
+ description="Run interactive terminal wizard to create configuration.",
81
+ )
82
+ init_cmd.add_argument("--provider", default="custom", help="Provider name (default: custom)")
83
+ init_cmd.add_argument("--input", type=Path, help="Optional existing config file to pre-fill wizard")
84
+ init_cmd.add_argument("--output-dir", default=".", type=Path, help="Directory for generated artifact")
85
+ init_cmd.add_argument("--format", choices=("json", "yaml"), help="Output format override")
86
+ init_cmd.set_defaults(handler=_init)
87
+
88
+ ui_cmd = sub.add_parser(
89
+ "ui",
90
+ help="Launch local configuration studio WebUI",
91
+ description="Launch local configuration studio WebUI.",
92
+ )
93
+ ui_cmd.add_argument("--host", default="127.0.0.1", help="Host address (default: 127.0.0.1)")
94
+ ui_cmd.add_argument("--port", default=8848, type=int, help="Port to bind (default: 8848)")
95
+ ui_cmd.add_argument("--no-browser", action="store_true", help="Do not automatically open browser")
96
+ ui_cmd.add_argument(
97
+ "--workspace",
98
+ default=None,
99
+ help="Directory the studio may export into (default: current directory)",
100
+ )
101
+ ui_cmd.set_defaults(handler=_ui)
102
+
103
+ _register_ops_subparsers(sub)
104
+
77
105
  return parser
78
106
 
79
107
 
108
+ def _register_ops_subparsers(sub) -> None:
109
+ """Graft the optional ``singbox_ops`` subcommands onto this single CLI.
110
+
111
+ ``singbox_ops`` is imported lazily, so importing ``devconfig_gen`` stays
112
+ side-effect free and the pure engine works even without the ops package.
113
+ """
114
+
115
+ try:
116
+ from singbox_ops.cli import add_subparsers
117
+ except Exception: # pragma: no cover - ops layer is optional
118
+ return
119
+ add_subparsers(sub)
120
+
121
+
80
122
  def _list_providers(args) -> int:
81
123
  for name in default_registry.names():
82
124
  print(name)
@@ -154,6 +196,30 @@ def _schema(args) -> int:
154
196
  return 0
155
197
 
156
198
 
199
+ def _init(args) -> int:
200
+ from .interactive import run_interactive_wizard
201
+
202
+ input_path = str(args.input) if args.input else None
203
+ return run_interactive_wizard(
204
+ args.provider,
205
+ input_path=input_path,
206
+ output_dir=str(args.output_dir),
207
+ output_format=args.format,
208
+ )
209
+
210
+
211
+ def _ui(args) -> int:
212
+ from .web_ui import run_web_ui
213
+
214
+ run_web_ui(
215
+ host=args.host,
216
+ port=args.port,
217
+ open_browser=not args.no_browser,
218
+ workspace_root=args.workspace,
219
+ )
220
+ return 0
221
+
222
+
157
223
  def main(argv=None) -> int:
158
224
  args = build_parser().parse_args(argv)
159
225
  return args.handler(args)
@@ -0,0 +1,377 @@
1
+ """Interactive terminal wizard for generating configuration.
2
+
3
+ Designed for headless environments (e.g. Linux SSH servers or terminal-first
4
+ workflows on macOS). Driven entirely by the provider's declarative steps and
5
+ schema metadata, so there is no duplication with the core engine or the WebUI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional, Sequence, TextIO
13
+
14
+ from . import formats
15
+ from .engine import describe_provider, diagnose_request, generate
16
+ from .models import GenerationRequest, ProviderField, ProviderStep
17
+ from .registry import ProviderRegistry, default_registry
18
+
19
+
20
+ class _EndOfInput(Exception):
21
+ """Raised when the input stream ends before a required answer is given."""
22
+
23
+ def __init__(self, field_name: str):
24
+ super().__init__(field_name)
25
+ self.field_name = field_name
26
+
27
+
28
+ def _set_dotted_path(target: Dict[str, Any], path: str, value: Any) -> None:
29
+ """Set a value in a nested dict using a dotted path like 'app.port'."""
30
+
31
+ parts = path.split(".")
32
+ current = target
33
+ for part in parts[:-1]:
34
+ if part not in current or not isinstance(current[part], dict):
35
+ current[part] = {}
36
+ current = current[part]
37
+ current[parts[-1]] = value
38
+
39
+
40
+ def _get_dotted_path(source: Any, path: str, default: Any = None) -> Any:
41
+ """Retrieve a value from a nested dict using a dotted path."""
42
+
43
+ if not isinstance(source, dict):
44
+ return default
45
+ current: Any = source
46
+ for part in path.split("."):
47
+ if not isinstance(current, dict) or part not in current:
48
+ return default
49
+ current = current[part]
50
+ return current
51
+
52
+
53
+ def _read_line(reader: TextIO, writer: TextIO, prompt: str) -> Optional[str]:
54
+ """Write ``prompt`` and read one line.
55
+
56
+ Returns the stripped line, or ``None`` when the stream is exhausted. A blank
57
+ line (``"\\n"``) is distinct from end-of-input (``""``).
58
+ """
59
+
60
+ writer.write(prompt)
61
+ writer.flush()
62
+ line = reader.readline()
63
+ if line == "":
64
+ return None
65
+ return line.strip()
66
+
67
+
68
+ def _prompt_string(
69
+ field: ProviderField,
70
+ current_value: Any,
71
+ reader: TextIO,
72
+ writer: TextIO,
73
+ ) -> Any:
74
+ effective_default = current_value if current_value is not None else field.default
75
+ default_str = f" [{effective_default}]" if effective_default is not None else ""
76
+ req_str = " (required)" if field.required and effective_default is None else ""
77
+
78
+ if field.choices:
79
+ writer.write(f"? {field.name} - Select option:{req_str}\n")
80
+ for i, choice in enumerate(field.choices, 1):
81
+ writer.write(f" {i}) {choice}\n")
82
+ lower_choices = {str(c).lower(): c for c in field.choices}
83
+ while True:
84
+ answer = _read_line(reader, writer, f" Enter choice number or name{default_str}: ")
85
+ if answer is None:
86
+ if effective_default is not None:
87
+ return effective_default
88
+ raise _EndOfInput(field.name)
89
+ if answer == "":
90
+ if effective_default is not None:
91
+ return effective_default
92
+ if field.required:
93
+ writer.write(" [!] Selection is required. Please choose an option.\n")
94
+ continue
95
+ return None
96
+ if answer.isdigit():
97
+ idx = int(answer) - 1
98
+ if 0 <= idx < len(field.choices):
99
+ return field.choices[idx]
100
+ if answer.lower() in lower_choices:
101
+ return lower_choices[answer.lower()]
102
+ writer.write(
103
+ f" [!] Invalid choice: {answer!r}. Please select 1-{len(field.choices)}.\n"
104
+ )
105
+
106
+ while True:
107
+ answer = _read_line(reader, writer, f"? {field.name}{req_str}{default_str}: ")
108
+ if answer is None:
109
+ if effective_default is not None:
110
+ return effective_default
111
+ raise _EndOfInput(field.name)
112
+ if answer == "":
113
+ if effective_default is not None:
114
+ return effective_default
115
+ if field.required:
116
+ writer.write(" [!] This field is required.\n")
117
+ continue
118
+ return None
119
+ return answer
120
+
121
+
122
+ def _prompt_integer(
123
+ field: ProviderField,
124
+ current_value: Any,
125
+ reader: TextIO,
126
+ writer: TextIO,
127
+ ) -> Any:
128
+ effective_default = current_value if current_value is not None else field.default
129
+ default_str = f" [{effective_default}]" if effective_default is not None else ""
130
+ bounds = []
131
+ if field.minimum is not None:
132
+ bounds.append(f"min {int(field.minimum)}")
133
+ if field.maximum is not None:
134
+ bounds.append(f"max {int(field.maximum)}")
135
+ bound_str = f" ({', '.join(bounds)})" if bounds else ""
136
+ req_str = " (required)" if field.required and effective_default is None else ""
137
+
138
+ while True:
139
+ answer = _read_line(reader, writer, f"? {field.name}{bound_str}{req_str}{default_str}: ")
140
+ if answer is None:
141
+ if effective_default is not None:
142
+ return effective_default
143
+ raise _EndOfInput(field.name)
144
+ if answer == "":
145
+ if effective_default is not None:
146
+ return effective_default
147
+ if field.required:
148
+ writer.write(" [!] An integer value is required.\n")
149
+ continue
150
+ return None
151
+ try:
152
+ number = int(answer)
153
+ except ValueError:
154
+ writer.write(f" [!] Expected integer, got {answer!r}.\n")
155
+ continue
156
+ if field.minimum is not None and number < field.minimum:
157
+ writer.write(f" [!] Value must be >= {int(field.minimum)}.\n")
158
+ continue
159
+ if field.maximum is not None and number > field.maximum:
160
+ writer.write(f" [!] Value must be <= {int(field.maximum)}.\n")
161
+ continue
162
+ return number
163
+
164
+
165
+ def _prompt_boolean(
166
+ field: ProviderField,
167
+ current_value: Any,
168
+ reader: TextIO,
169
+ writer: TextIO,
170
+ ) -> bool:
171
+ effective_default = bool(current_value if current_value is not None else field.default)
172
+ prompt_hint = "[Y/n]" if effective_default else "[y/N]"
173
+ while True:
174
+ answer = _read_line(reader, writer, f"? {field.name} {prompt_hint}: ")
175
+ if answer is None or answer == "":
176
+ return effective_default
177
+ lowered = answer.lower()
178
+ if lowered in ("y", "yes", "true", "1"):
179
+ return True
180
+ if lowered in ("n", "no", "false", "0"):
181
+ return False
182
+ writer.write(" [!] Please enter 'y' for yes or 'n' for no.\n")
183
+
184
+
185
+ def _prompt_mapping(
186
+ field: ProviderField,
187
+ current_value: Any,
188
+ reader: TextIO,
189
+ writer: TextIO,
190
+ ) -> Dict[str, str]:
191
+ mapping: Dict[str, str] = dict(current_value) if isinstance(current_value, dict) else {}
192
+ writer.write(
193
+ f"? {field.name} (enter key=value pairs, press enter on empty line to finish):\n"
194
+ )
195
+ if mapping:
196
+ writer.write(
197
+ " Current entries: " + ", ".join(f"{k}={v}" for k, v in mapping.items()) + "\n"
198
+ )
199
+ while True:
200
+ answer = _read_line(reader, writer, " key=val (or empty to finish): ")
201
+ if answer is None or answer == "":
202
+ break
203
+ if "=" not in answer:
204
+ writer.write(" [!] Invalid format. Must be key=value.\n")
205
+ continue
206
+ key, value = answer.split("=", 1)
207
+ mapping[key.strip()] = value.strip()
208
+ return mapping
209
+
210
+
211
+ def _prompt_document(
212
+ field: ProviderField,
213
+ current_value: Any,
214
+ reader: TextIO,
215
+ writer: TextIO,
216
+ ) -> Any:
217
+ writer.write(f"? {field.name} (load document file or enter entries):\n")
218
+ path_str = _read_line(
219
+ reader, writer, " Path to JSON/YAML file (or press enter for key=value input): "
220
+ )
221
+ if path_str:
222
+ file_path = Path(path_str).expanduser()
223
+ if file_path.is_file():
224
+ try:
225
+ loaded = formats.load_file(file_path)
226
+ writer.write(f" [✓] Loaded document from {file_path}\n")
227
+ return loaded
228
+ except Exception as exc:
229
+ writer.write(f" [!] Error reading {file_path}: {exc}\n")
230
+ else:
231
+ writer.write(f" [!] File not found: {path_str}\n")
232
+ return _prompt_mapping(field, current_value, reader, writer)
233
+
234
+
235
+ def prompt_field(
236
+ field: ProviderField,
237
+ current_value: Any,
238
+ reader: TextIO,
239
+ writer: TextIO,
240
+ ) -> Any:
241
+ """Prompt the user for a single field's value based on its type."""
242
+
243
+ if field.type == "integer":
244
+ return _prompt_integer(field, current_value, reader, writer)
245
+ if field.type == "boolean":
246
+ return _prompt_boolean(field, current_value, reader, writer)
247
+ if field.type in ("mapping", "dict"):
248
+ return _prompt_mapping(field, current_value, reader, writer)
249
+ if field.type in ("document", "tree"):
250
+ return _prompt_document(field, current_value, reader, writer)
251
+ return _prompt_string(field, current_value, reader, writer)
252
+
253
+
254
+ def run_interactive_wizard(
255
+ provider_name: str,
256
+ *,
257
+ input_path: Optional[str] = None,
258
+ context: Optional[Dict[str, Any]] = None,
259
+ output_dir: Optional[str] = None,
260
+ output_format: Optional[str] = None,
261
+ registry: Optional[ProviderRegistry] = None,
262
+ reader: Optional[TextIO] = None,
263
+ writer: Optional[TextIO] = None,
264
+ ) -> int:
265
+ """Execute the interactive wizard loop in the terminal.
266
+
267
+ ``context`` seeds the wizard with existing answers. ``input_path`` loads a
268
+ document from disk and merges it on top of ``context``. On validation
269
+ failure the wizard offers to re-run while keeping every answer pre-filled.
270
+
271
+ Returns ``0`` on successful generation and ``1`` on cancellation or error.
272
+ """
273
+
274
+ in_stream = reader or sys.stdin
275
+ out_stream = writer or sys.stdout
276
+
277
+ reg = registry or default_registry
278
+ try:
279
+ reg.get(provider_name)
280
+ except ValueError as exc:
281
+ out_stream.write(f"Error: {exc}\n")
282
+ return 1
283
+
284
+ steps: Sequence[ProviderStep] = describe_provider(provider_name, registry=reg)
285
+ if not steps:
286
+ out_stream.write(
287
+ f"Provider '{provider_name}' does not define declarative wizard steps.\n"
288
+ )
289
+ return 1
290
+
291
+ working: Dict[str, Any] = dict(context) if context else {}
292
+ if input_path:
293
+ try:
294
+ loaded = formats.load_file(input_path)
295
+ except Exception as exc:
296
+ out_stream.write(f"[warn] Could not load initial context: {exc}\n")
297
+ else:
298
+ if isinstance(loaded, dict):
299
+ working.update(loaded)
300
+ out_stream.write(f"[info] Pre-populated wizard with values from {input_path}\n")
301
+ else:
302
+ out_stream.write(
303
+ f"[warn] {input_path} does not contain a mapping; ignoring it.\n"
304
+ )
305
+
306
+ out_stream.write("========================================================\n")
307
+ out_stream.write(f" DevConfig-Gen Interactive Wizard: '{provider_name}'\n")
308
+ out_stream.write(" Answer the prompts below. Press Enter to use defaults.\n")
309
+ out_stream.write("========================================================\n")
310
+
311
+ total_steps = len(steps)
312
+ while True:
313
+ try:
314
+ for step_idx, step in enumerate(steps, 1):
315
+ out_stream.write(f"\n--- [{step_idx}/{total_steps}] {step.title} ---\n")
316
+ if step.description:
317
+ out_stream.write(f" {step.description}\n")
318
+ for field in step.fields:
319
+ existing = _get_dotted_path(working, field.name)
320
+ value = prompt_field(field, existing, in_stream, out_stream)
321
+ if value is not None:
322
+ _set_dotted_path(working, field.name, value)
323
+ except _EndOfInput as exc:
324
+ out_stream.write(
325
+ f"\n[!] Input ended before '{exc.field_name}' was provided. Aborting.\n"
326
+ )
327
+ return 1
328
+
329
+ out_stream.write("\nValidating configuration...\n")
330
+ diagnostics = diagnose_request(provider_name, context=working, registry=reg)
331
+ errors = [item for item in diagnostics if item.severity == "error"]
332
+ if not errors:
333
+ break
334
+
335
+ out_stream.write("\n[!] Validation found errors:\n")
336
+ for err in errors:
337
+ out_stream.write(f" - {err.field}: {err.message}\n")
338
+
339
+ answer = _read_line(
340
+ in_stream,
341
+ out_stream,
342
+ "Re-run the wizard to correct these? [Y/n]: ",
343
+ )
344
+ if answer is None or answer.lower() in ("n", "no"):
345
+ out_stream.write("Aborted.\n")
346
+ return 1
347
+ out_stream.write("\nRe-running wizard with your previous answers pre-filled...\n")
348
+
349
+ out_stream.write("[✓] All validations passed!\n\n")
350
+
351
+ selected_format = output_format
352
+ if not selected_format:
353
+ answer = _read_line(
354
+ in_stream,
355
+ out_stream,
356
+ "Select output format (1: YAML [default], 2: JSON): ",
357
+ )
358
+ selected_format = "json" if answer == "2" else "yaml"
359
+
360
+ target_dir = output_dir or "."
361
+ out_stream.write(f"Writing configuration to '{target_dir}'...\n")
362
+
363
+ try:
364
+ result = generate(
365
+ provider_name,
366
+ GenerationRequest(context=working, options={"format": selected_format}),
367
+ registry=reg,
368
+ output_dir=target_dir,
369
+ )
370
+ except Exception as exc:
371
+ out_stream.write(f"[!] Generation failed: {exc}\n")
372
+ return 1
373
+
374
+ for artifact in result.artifacts:
375
+ out_path = Path(target_dir) / artifact.name
376
+ out_stream.write(f"[✓] Generated artifact: {out_path.resolve()}\n")
377
+ return 0