xb-init 2.2.4__tar.gz → 2.5.1__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 (99) hide show
  1. xb_init-2.5.1/.github/workflows/ci.yml +30 -0
  2. {xb_init-2.2.4 → xb_init-2.5.1}/PKG-INFO +28 -5
  3. {xb_init-2.2.4 → xb_init-2.5.1}/README.md +27 -4
  4. {xb_init-2.2.4 → xb_init-2.5.1}/pyproject.toml +3 -3
  5. {xb_init-2.2.4 → xb_init-2.5.1}/tests/test_cli.py +37 -2
  6. xb_init-2.5.1/tests/test_config_form.py +133 -0
  7. xb_init-2.5.1/tests/test_template_render.py +150 -0
  8. xb_init-2.5.1/uv.lock +445 -0
  9. {xb_init-2.2.4 → xb_init-2.5.1}/xb/__init__.py +2 -1
  10. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/build.py +8 -16
  11. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/dev.py +8 -16
  12. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/doctor.py +25 -3
  13. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/init.py +64 -12
  14. xb_init-2.5.1/xb/commands/version.py +63 -0
  15. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/main.py.j2 +2 -2
  16. xb_init-2.5.1/xb/templates/configs/global_config.yaml.j2 +3 -0
  17. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/package.json.j2 +1 -1
  18. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/index.html.j2 +1 -1
  19. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/App.vue.j2 +4 -4
  20. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/DashboardHome.vue.j2 +1 -1
  21. xb_init-2.5.1/xb/templates/root/LICENSE.j2 +21 -0
  22. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/README.md.j2 +1 -1
  23. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/pyproject.toml.j2 +1 -1
  24. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/version/scripts/version_manager.py.j2 +1 -1
  25. xb_init-2.5.1/xb/utils/config_form.py +308 -0
  26. xb_init-2.5.1/xb/utils/project.py +18 -0
  27. {xb_init-2.2.4 → xb_init-2.5.1}/xb/utils/template_engine.py +35 -4
  28. xb_init-2.2.4/uv.lock +0 -445
  29. xb_init-2.2.4/xb/commands/version.py +0 -65
  30. xb_init-2.2.4/xb/templates/configs/global_config.yaml.j2 +0 -3
  31. {xb_init-2.2.4 → xb_init-2.5.1}/.gitignore +0 -0
  32. {xb_init-2.2.4 → xb_init-2.5.1}/docs/screenshot.png +0 -0
  33. {xb_init-2.2.4 → xb_init-2.5.1}/tests/test_update_templates.py +0 -0
  34. {xb_init-2.2.4 → xb_init-2.5.1}/tests/test_upgrade.py +0 -0
  35. {xb_init-2.2.4 → xb_init-2.5.1}/xb/cli.py +0 -0
  36. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/__init__.py +0 -0
  37. {xb_init-2.2.4 → xb_init-2.5.1}/xb/commands/upgrade.py +0 -0
  38. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/__init__.py.j2 +0 -0
  39. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/config.py.j2 +0 -0
  40. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/ports.py.j2 +0 -0
  41. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/terminal/__init__.py.j2 +0 -0
  42. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/terminal/routes.py.j2 +0 -0
  43. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/api/update/__init__.py.j2 +0 -0
  44. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/app_version.py.j2 +0 -0
  45. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/backend_build.py.j2 +0 -0
  46. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/managers/__init__.py.j2 +0 -0
  47. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/managers/logger_manager.py.j2 +0 -0
  48. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/managers/path_manager.py.j2 +0 -0
  49. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/managers/secret_obfuscator.py.j2 +0 -0
  50. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/managers/sudoers_manager.py.j2 +0 -0
  51. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/requirements.txt.j2 +0 -0
  52. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/services/__init__.py.j2 +0 -0
  53. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/services/feishu_client.py.j2 +0 -0
  54. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/backend/services/updater.py.j2 +0 -0
  55. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/configs/config_changes.json.j2 +0 -0
  56. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/configs/secrets.yaml.example.j2 +0 -0
  57. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/configs/secrets.yaml.j2 +0 -0
  58. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/launcher.js.j2 +0 -0
  59. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/main.js.j2 +0 -0
  60. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/port_diagnostics.js.j2 +0 -0
  61. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/preload.js.j2 +0 -0
  62. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/prepare.js.j2 +0 -0
  63. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/resources/postinst.py.j2 +0 -0
  64. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/resources/postrm.py.j2 +0 -0
  65. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/electron/resources/uninstaller.nsh.j2 +0 -0
  66. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/package.json.j2 +0 -0
  67. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/ConfigSetup.vue.j2 +0 -0
  68. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/FileManager.vue.j2 +0 -0
  69. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/GitVersionBadge.vue.j2 +0 -0
  70. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/RefreshButton.vue.j2 +0 -0
  71. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/TerminalPanel.vue.j2 +0 -0
  72. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/TerminalView.vue.j2 +0 -0
  73. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/ThemeToggle.vue.j2 +0 -0
  74. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/version/CommitHistoryPanel.vue.j2 +0 -0
  75. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/components/version/UpdatePanel.vue.j2 +0 -0
  76. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/composables/useTerminalSessions.js.j2 +0 -0
  77. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/composables/useUpdate.js.j2 +0 -0
  78. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/main.js.j2 +0 -0
  79. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/src/style.css.j2 +0 -0
  80. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/frontend/vite.config.js.j2 +0 -0
  81. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/.gitignore.j2 +0 -0
  82. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/AGENTS.md.j2 +0 -0
  83. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/build.py.j2 +0 -0
  84. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/dev.py.j2 +0 -0
  85. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/docs/images/feishu-folder.png +0 -0
  86. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/docs/images/feishu-permission.png +0 -0
  87. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/docs/images/release-upload.png +0 -0
  88. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/docs/images/update-dialog.png +0 -0
  89. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/root/docs//350/207/252/345/212/250/346/233/264/346/226/260/350/257/264/346/230/216.md" +0 -0
  90. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/scripts/example.py.j2 +0 -0
  91. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/scripts/feishu_upload.py.j2 +0 -0
  92. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/version/hooks/pre-commit.py.j2 +0 -0
  93. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/version/scripts/gui_version_manager.py.j2 +0 -0
  94. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/version/scripts/install_hooks.py.j2 +0 -0
  95. {xb_init-2.2.4 → xb_init-2.5.1}/xb/templates/version/scripts/web_version_manager.py.j2 +0 -0
  96. {xb_init-2.2.4 → xb_init-2.5.1}/xb/utils/__init__.py +0 -0
  97. {xb_init-2.2.4 → xb_init-2.5.1}/xb/utils/click_helpers.py +0 -0
  98. {xb_init-2.2.4 → xb_init-2.5.1}/xb/utils/validators.py +0 -0
  99. {xb_init-2.2.4 → xb_init-2.5.1}/xb/utils/version_check.py +0 -0
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [release_v0, main, master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Sync dependencies
24
+ run: uv sync --extra dev
25
+
26
+ - name: Lint (ruff)
27
+ run: uv run ruff check xb tests
28
+
29
+ - name: Test (pytest)
30
+ run: uv run pytest -q
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xb-init
3
- Version: 2.2.4
3
+ Version: 2.5.1
4
4
  Summary: Project management tool for UV + FastAPI + Vue3 + Electron desktop applications
5
5
  Project-URL: Homepage, https://github.com/xuebli/xb
6
6
  Project-URL: Documentation, https://github.com/xuebli/xb#readme
@@ -77,7 +77,6 @@ xb init demo
77
77
 
78
78
  # 带 sudo 免密配置
79
79
  xb init demo --sudoers
80
-
81
80
  # 带内置终端
82
81
  xb init demo --terminal
83
82
 
@@ -91,7 +90,6 @@ xb init demo --update
91
90
  xb init demo --icon ~/icons/app.png
92
91
 
93
92
  cd demo
94
-
95
93
  # 启动开发环境
96
94
  xb dev
97
95
 
@@ -102,6 +100,29 @@ xb dev status
102
100
  xb dev stop
103
101
  ```
104
102
 
103
+ ### 交互式配置表单
104
+
105
+ `xb init` 会在创建前显示一个单屏配置表单,回车即按当前值创建:
106
+
107
+ ```
108
+ xb init demo — 应用配置
109
+ ↑↓ 移动 · 空格 勾选/取消 · 选中后直接输入 · 回车 开始创建
110
+ ────────────────────────────────────────────────────────
111
+ ❯ [ ] 应用显示名 : Demo
112
+ [ ] 端口(后端/前端) : 8000 / 8001
113
+ [ ] sudo 免密 ✘ (Linux 应用内更新需要)
114
+ [ ] 内置终端 ✘ (Web 终端组件)
115
+ [ ] 应用内更新 ✘ (自动附带 sudo 免密)
116
+ [ ] 应用图标 : 使用默认
117
+ [ ] 跳过依赖安装 ✘ (离线环境可跳过)
118
+ ```
119
+
120
+ - `--name/--port/--sudoers/--terminal/--update/--icon/--skip-install` 传了的参数
121
+ 直接预填进表单并勾上,回车即确认,老用法不受影响
122
+ - 勾选"应用内更新"会自动勾上"sudo 免密",且后者此时不可单独取消
123
+ - 文本行(显示名/端口/图标)勾选时若仍是默认值则自动清空,直接输入即可
124
+ - stdin 非终端时(脚本/CI 管道调用)自动跳过表单,沿用 CLI 参数
125
+
105
126
  Windows PowerShell 也可以直接运行生成项目脚本:
106
127
 
107
128
  ```powershell
@@ -121,6 +142,7 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
121
142
 
122
143
  > **`xb init` 自动行为**:
123
144
  > - 执行 `npm install`(frontend + electron),生成 `package-lock.json`
145
+ > 离线环境可加 `--skip-install` 跳过,稍后手动安装
124
146
  > - 生成的项目用 [nrm](https://github.com/Pana/nrm) 管理 npm 源:`xb init`
125
147
  > 安装依赖前与 `build.py` 构建前都会自动 `nrm test` 测速所有内置源并切换
126
148
  > 到最快的(未安装 nrm 时自动全局安装);项目不放 `.npmrc`,npm 源完全由
@@ -138,7 +160,7 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
138
160
 
139
161
  | 命令 | 说明 |
140
162
  |------|------|
141
- | `xb init <name> [--sudoers] [--terminal] [--update] [--icon PATH]` | 初始化项目 |
163
+ | `xb init <name> [--name 显示名] [--port 9000] [--sudoers] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
142
164
  | `xb dev [start\|stop\|status]` | 启动/停止/查看开发环境 |
143
165
  | `xb build [all\|frontend\|backend\|electron]` | 构建项目 |
144
166
  | `xb build -f / -b / -e / -a` | 构建快捷 flag |
@@ -163,7 +185,7 @@ xb init demo --icon ./my-icon.png
163
185
  > **[xb/templates/root/docs/自动更新说明.md](xb/templates/root/docs/自动更新说明.md)**
164
186
  > (生成的项目会自带 `docs/自动更新说明.md` 与配图)。
165
187
 
166
- `xb init demo --update` 会让生成的项目内置与 [leo_stm32_board_test](../leo_stm32_board_test) 同源的自动更新能力,
188
+ `xb init demo --update` 会让生成的项目内置自动更新能力,
167
189
  以飞书云盘的一个文件夹作为发布"货架":
168
190
 
169
191
  **生成内容:**
@@ -201,6 +223,7 @@ demo/
201
223
  ├── pyproject.toml # Python 依赖(uv 管理)
202
224
  ├── AGENTS.md # AI 编码助手协作约定
203
225
  ├── README.md # 项目说明(含三种启动方式详解)
226
+ ├── LICENSE # MIT 许可证
204
227
  ├── .gitignore
205
228
  ├── backend/ # FastAPI 后端
206
229
  │ ├── main.py # FastAPI 入口(lifespan / CORS / SPA 兜底)
@@ -48,7 +48,6 @@ xb init demo
48
48
 
49
49
  # 带 sudo 免密配置
50
50
  xb init demo --sudoers
51
-
52
51
  # 带内置终端
53
52
  xb init demo --terminal
54
53
 
@@ -62,7 +61,6 @@ xb init demo --update
62
61
  xb init demo --icon ~/icons/app.png
63
62
 
64
63
  cd demo
65
-
66
64
  # 启动开发环境
67
65
  xb dev
68
66
 
@@ -73,6 +71,29 @@ xb dev status
73
71
  xb dev stop
74
72
  ```
75
73
 
74
+ ### 交互式配置表单
75
+
76
+ `xb init` 会在创建前显示一个单屏配置表单,回车即按当前值创建:
77
+
78
+ ```
79
+ xb init demo — 应用配置
80
+ ↑↓ 移动 · 空格 勾选/取消 · 选中后直接输入 · 回车 开始创建
81
+ ────────────────────────────────────────────────────────
82
+ ❯ [ ] 应用显示名 : Demo
83
+ [ ] 端口(后端/前端) : 8000 / 8001
84
+ [ ] sudo 免密 ✘ (Linux 应用内更新需要)
85
+ [ ] 内置终端 ✘ (Web 终端组件)
86
+ [ ] 应用内更新 ✘ (自动附带 sudo 免密)
87
+ [ ] 应用图标 : 使用默认
88
+ [ ] 跳过依赖安装 ✘ (离线环境可跳过)
89
+ ```
90
+
91
+ - `--name/--port/--sudoers/--terminal/--update/--icon/--skip-install` 传了的参数
92
+ 直接预填进表单并勾上,回车即确认,老用法不受影响
93
+ - 勾选"应用内更新"会自动勾上"sudo 免密",且后者此时不可单独取消
94
+ - 文本行(显示名/端口/图标)勾选时若仍是默认值则自动清空,直接输入即可
95
+ - stdin 非终端时(脚本/CI 管道调用)自动跳过表单,沿用 CLI 参数
96
+
76
97
  Windows PowerShell 也可以直接运行生成项目脚本:
77
98
 
78
99
  ```powershell
@@ -92,6 +113,7 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
92
113
 
93
114
  > **`xb init` 自动行为**:
94
115
  > - 执行 `npm install`(frontend + electron),生成 `package-lock.json`
116
+ > 离线环境可加 `--skip-install` 跳过,稍后手动安装
95
117
  > - 生成的项目用 [nrm](https://github.com/Pana/nrm) 管理 npm 源:`xb init`
96
118
  > 安装依赖前与 `build.py` 构建前都会自动 `nrm test` 测速所有内置源并切换
97
119
  > 到最快的(未安装 nrm 时自动全局安装);项目不放 `.npmrc`,npm 源完全由
@@ -109,7 +131,7 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
109
131
 
110
132
  | 命令 | 说明 |
111
133
  |------|------|
112
- | `xb init <name> [--sudoers] [--terminal] [--update] [--icon PATH]` | 初始化项目 |
134
+ | `xb init <name> [--name 显示名] [--port 9000] [--sudoers] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
113
135
  | `xb dev [start\|stop\|status]` | 启动/停止/查看开发环境 |
114
136
  | `xb build [all\|frontend\|backend\|electron]` | 构建项目 |
115
137
  | `xb build -f / -b / -e / -a` | 构建快捷 flag |
@@ -134,7 +156,7 @@ xb init demo --icon ./my-icon.png
134
156
  > **[xb/templates/root/docs/自动更新说明.md](xb/templates/root/docs/自动更新说明.md)**
135
157
  > (生成的项目会自带 `docs/自动更新说明.md` 与配图)。
136
158
 
137
- `xb init demo --update` 会让生成的项目内置与 [leo_stm32_board_test](../leo_stm32_board_test) 同源的自动更新能力,
159
+ `xb init demo --update` 会让生成的项目内置自动更新能力,
138
160
  以飞书云盘的一个文件夹作为发布"货架":
139
161
 
140
162
  **生成内容:**
@@ -172,6 +194,7 @@ demo/
172
194
  ├── pyproject.toml # Python 依赖(uv 管理)
173
195
  ├── AGENTS.md # AI 编码助手协作约定
174
196
  ├── README.md # 项目说明(含三种启动方式详解)
197
+ ├── LICENSE # MIT 许可证
175
198
  ├── .gitignore
176
199
  ├── backend/ # FastAPI 后端
177
200
  │ ├── main.py # FastAPI 入口(lifespan / CORS / SPA 兜底)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xb-init"
7
- version = "2.2.4"
7
+ version = "2.5.1"
8
8
  description = "Project management tool for UV + FastAPI + Vue3 + Electron desktop applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -51,11 +51,11 @@ packages = ["xb"]
51
51
 
52
52
  [tool.black]
53
53
  line-length = 100
54
- target-version = ['py310']
54
+ target-version = ['py312']
55
55
 
56
56
  [tool.ruff]
57
57
  line-length = 100
58
- target-version = "py310"
58
+ target-version = "py312"
59
59
 
60
60
  [tool.ruff.lint]
61
61
  select = ["E", "F", "I", "N", "W", "B"]
@@ -35,12 +35,11 @@ def _invoke_init(monkeypatch, tmp_path, args, password):
35
35
 
36
36
 
37
37
  def test_update_implies_sudoers(monkeypatch, tmp_path):
38
- captured, output = _invoke_init(monkeypatch, tmp_path, ["--update"], "pw123")
38
+ captured, _output = _invoke_init(monkeypatch, tmp_path, ["--update"], "pw123")
39
39
 
40
40
  assert captured["enable_update"] is True
41
41
  assert captured["enable_sudo"] is True
42
42
  assert captured["sudo_password"] == "pw123"
43
- assert "自动附带 --sudoers" in output
44
43
 
45
44
 
46
45
  def test_sudoers_alone_still_prompts(monkeypatch, tmp_path):
@@ -60,6 +59,42 @@ def test_plain_init_has_no_sudo(monkeypatch, tmp_path):
60
59
  assert captured["sudo_password"] == ""
61
60
 
62
61
 
62
+ def test_display_name_passed_through(monkeypatch, tmp_path):
63
+ captured, _ = _invoke_init(
64
+ monkeypatch, tmp_path, ["--name", "我的应用"], ""
65
+ )
66
+
67
+ assert captured["display_name"] == "我的应用"
68
+
69
+
70
+ def test_display_name_defaults_to_none(monkeypatch, tmp_path):
71
+ captured, _ = _invoke_init(monkeypatch, tmp_path, [], "")
72
+
73
+ assert captured["display_name"] is None
74
+
75
+
76
+ def test_port_passed_through(monkeypatch, tmp_path):
77
+ captured, _output = _invoke_init(monkeypatch, tmp_path, ["--port", "9100"], "")
78
+
79
+ assert captured["backend_port"] == 9100
80
+
81
+
82
+ def test_port_defaults_to_none(monkeypatch, tmp_path):
83
+ captured, _ = _invoke_init(monkeypatch, tmp_path, [], "")
84
+
85
+ assert captured["backend_port"] is None
86
+
87
+
88
+ def test_port_out_of_range_rejected(monkeypatch, tmp_path):
89
+ monkeypatch.setattr(init_module, "get_latest_if_newer", lambda _v: None)
90
+
91
+ result = CliRunner().invoke(
92
+ cli.main, ["init", str(tmp_path / "demo"), "--port", "70000"], input="\n"
93
+ )
94
+
95
+ assert result.exit_code != 0
96
+
97
+
63
98
  def test_pick_fastest_from_nrm_output():
64
99
  output = """ npm ---------- 807 ms
65
100
  yarn --------- 795 ms
@@ -0,0 +1,133 @@
1
+ """配置表单(ConfigForm)交互逻辑测试:不进键盘循环,直接驱动方法。"""
2
+
3
+ from xb.utils.config_form import ConfigForm, run_config_form
4
+
5
+
6
+ def make_form(cli=None):
7
+ form = ConfigForm("demo", cli)
8
+ return form
9
+
10
+
11
+ def _cursor_key(form, key):
12
+ form.cursor = next(
13
+ i for i, r in enumerate(form.rows) if r["key"] == key)
14
+
15
+
16
+ def test_update_checks_sudoers_and_blocks_unchecking():
17
+ form = make_form()
18
+ _cursor_key(form, "update")
19
+ form.toggle()
20
+ assert form._row("update")["checked"] is True
21
+ assert form._row("sudoers")["checked"] is True
22
+
23
+ # update 勾着时 sudoers 无法单独取消
24
+ _cursor_key(form, "sudoers")
25
+ form.toggle()
26
+ assert form._row("sudoers")["checked"] is True
27
+ assert form.message # 有提示
28
+
29
+
30
+ def test_sudoers_toggle_without_update():
31
+ form = make_form()
32
+ _cursor_key(form, "sudoers")
33
+ form.toggle()
34
+ assert form._row("sudoers")["checked"] is True
35
+ form.toggle()
36
+ assert form._row("sudoers")["checked"] is False
37
+ assert not form.message
38
+
39
+
40
+ def test_text_row_clears_default_on_first_check():
41
+ form = make_form()
42
+ _cursor_key(form, "port")
43
+ assert form._row("port")["value"] == "8000"
44
+ form.toggle()
45
+ assert form._row("port")["checked"] is True
46
+ assert form._row("port")["value"] == "" # 默认值清空等待输入
47
+
48
+ for ch in "9100":
49
+ form.feed_char(ch)
50
+ assert form._row("port")["value"] == "9100"
51
+
52
+
53
+ def test_text_row_keeps_custom_value_when_rechecked():
54
+ form = make_form({"display_name": "机器人"})
55
+ _cursor_key(form, "display_name")
56
+ # CLI 预填的值直接勾上
57
+ assert form._row("display_name")["checked"] is True
58
+ assert form._row("display_name")["value"] == "机器人"
59
+
60
+ form.toggle() # 取消
61
+ form.toggle() # 再勾上,自定义值不被清掉
62
+ assert form._row("display_name")["value"] == "机器人"
63
+
64
+
65
+ def test_collect_unchecked_items_are_none():
66
+ form = make_form()
67
+ result = form.collect()
68
+ assert result["display_name"] is None
69
+ assert result["port"] is None
70
+ assert result["sudoers"] is False
71
+ assert result["terminal"] is False
72
+ assert result["update"] is False
73
+ assert result["icon"] is None
74
+ assert result["skip_install"] is False
75
+
76
+
77
+ def test_collect_valid_port():
78
+ form = make_form()
79
+ _cursor_key(form, "port")
80
+ form.toggle()
81
+ for ch in "9200":
82
+ form.feed_char(ch)
83
+ result = form.collect()
84
+ assert result["port"] == 9200
85
+ assert result["sudoers"] is False
86
+
87
+
88
+ def test_collect_invalid_port_falls_back_to_none():
89
+ form = make_form()
90
+ _cursor_key(form, "port")
91
+ form.toggle()
92
+ for ch in "99999":
93
+ form.feed_char(ch)
94
+ result = form.collect()
95
+ assert result["port"] is None
96
+ assert form._row("port")["checked"] is False
97
+
98
+
99
+ def test_sudoers_row_hidden_on_windows():
100
+ import unittest.mock as mock
101
+
102
+ with mock.patch("xb.utils.config_form.os.name", "nt"):
103
+ form = make_form()
104
+ assert not form._has("sudoers")
105
+ result = form.collect()
106
+ assert result["sudoers"] is False
107
+
108
+
109
+ def test_backspace_only_on_checked_text_row():
110
+ form = make_form({"display_name": "abc"})
111
+ _cursor_key(form, "display_name")
112
+ form.backspace()
113
+ assert form._row("display_name")["value"] == "ab"
114
+
115
+ _cursor_key(form, "terminal")
116
+ form.backspace() # 开关行:无值可删,不报错即可
117
+
118
+
119
+ def test_run_config_form_skipped_when_not_tty():
120
+ result = run_config_form("demo", {
121
+ "display_name": None,
122
+ "port": 9100,
123
+ "sudoers": False,
124
+ "terminal": True,
125
+ "update": True,
126
+ "icon": None,
127
+ "skip_install": False,
128
+ })
129
+ # 非 tty 环境跳过表单,CLI 值原样透传且 update 联动 sudoers
130
+ assert result["port"] == 9100
131
+ assert result["sudoers"] is True
132
+ assert result["terminal"] is True
133
+ assert result["display_name"] is None
@@ -0,0 +1,150 @@
1
+ """模板渲染矩阵测试:sudoers x terminal x update 全组合。
2
+
3
+ 渲染到临时目录,校验:
4
+ - 条件文件按开关出现/缺席(防止模板改坏条件渲染)
5
+ - 所有生成的 .py 能通过 compile() 语法检查(防止模板产出非法 Python)
6
+ - secrets.yaml 权限收紧为 0600(POSIX)
7
+ """
8
+
9
+ import itertools
10
+ import json
11
+ import os
12
+
13
+ import pytest
14
+
15
+ from xb.utils.template_engine import TemplateEngine
16
+
17
+ COMBOS = list(itertools.product([False, True], repeat=3))
18
+
19
+
20
+ def _render(
21
+ tmp_path, enable_sudo, enable_terminal, enable_update, display_name=None,
22
+ backend_port=None,
23
+ ):
24
+ target = tmp_path / "demo"
25
+ TemplateEngine().render_project(
26
+ target_dir=target,
27
+ package_name="demo",
28
+ display_name=display_name,
29
+ backend_port=backend_port,
30
+ enable_sudo=enable_sudo,
31
+ enable_terminal=enable_terminal,
32
+ enable_update=enable_update,
33
+ sudo_password="pw123" if enable_sudo else "",
34
+ )
35
+ return target
36
+
37
+
38
+ @pytest.mark.parametrize(("enable_sudo", "enable_terminal", "enable_update"), COMBOS)
39
+ def test_render_project_matrix(tmp_path, enable_sudo, enable_terminal, enable_update):
40
+ target = _render(tmp_path, enable_sudo, enable_terminal, enable_update)
41
+
42
+ # 基础骨架始终存在
43
+ for rel in (
44
+ "pyproject.toml",
45
+ "dev.py",
46
+ "build.py",
47
+ "README.md",
48
+ "LICENSE",
49
+ "AGENTS.md",
50
+ "backend/main.py",
51
+ "backend/managers/path_manager.py",
52
+ "frontend/src/App.vue",
53
+ "electron/main.js",
54
+ "configs/global_config.yaml",
55
+ "version/scripts/version_manager.py",
56
+ ):
57
+ assert (target / rel).exists(), f"缺少基础文件 {rel}"
58
+
59
+ # 条件文件按开关出现/缺席
60
+ conditional = {
61
+ "backend/api/terminal/routes.py": enable_terminal,
62
+ "frontend/src/components/TerminalPanel.vue": enable_terminal,
63
+ "frontend/src/composables/useTerminalSessions.js": enable_terminal,
64
+ "backend/managers/sudoers_manager.py": enable_sudo or enable_update,
65
+ "backend/api/update/__init__.py": enable_update,
66
+ "backend/services/updater.py": enable_update,
67
+ "backend/managers/secret_obfuscator.py": enable_update,
68
+ "scripts/feishu_upload.py": enable_update,
69
+ "frontend/src/composables/useUpdate.js": enable_update,
70
+ }
71
+ for rel, should_exist in conditional.items():
72
+ assert (target / rel).exists() is should_exist, f"{rel} 存在性与开关不符"
73
+
74
+ # secrets.yaml 仅在 sudo + 提供密码时生成
75
+ secrets = target / "configs" / "secrets.yaml"
76
+ assert secrets.exists() is (enable_sudo and bool("pw123"))
77
+ if secrets.exists() and os.name != "nt":
78
+ assert secrets.stat().st_mode & 0o777 == 0o600, "secrets.yaml 应为 0600 权限"
79
+
80
+ # 生成的所有 Python 文件必须是合法语法(含带 jinja 条件的模板产物)
81
+ for py in target.rglob("*.py"):
82
+ source = py.read_text(encoding="utf-8")
83
+ compile(source, str(py), "exec")
84
+
85
+
86
+ def test_render_project_main_py_switches(tmp_path):
87
+ """main.py 的路由注册随开关变化。"""
88
+ plain = _render(tmp_path / "a", False, False, False)
89
+ main_plain = (plain / "backend" / "main.py").read_text(encoding="utf-8")
90
+ assert "terminal_router" not in main_plain
91
+ assert "update_router" not in main_plain
92
+ assert "sudoers_manager" not in main_plain
93
+
94
+ full = _render(tmp_path / "b", True, True, True)
95
+ main_full = (full / "backend" / "main.py").read_text(encoding="utf-8")
96
+ assert "terminal_router" in main_full
97
+ assert "update_router" in main_full
98
+ assert "sudoers_manager" in main_full
99
+
100
+
101
+ def test_display_name_defaults_to_capitalized_package(tmp_path):
102
+ target = _render(tmp_path, False, False, False)
103
+ assert "<title>Demo</title>" in (target / "frontend" / "index.html").read_text(
104
+ encoding="utf-8"
105
+ )
106
+
107
+
108
+ def test_ports_written_to_global_config(tmp_path):
109
+ """--port 指定后端端口时,前端端口自动 +1;不传时用默认值。"""
110
+ target = _render(tmp_path, False, False, False, backend_port=9100)
111
+ config = (target / "configs" / "global_config.yaml").read_text(encoding="utf-8")
112
+ assert "backend: 9100" in config
113
+ assert "frontend: 9101" in config
114
+
115
+ default = _render(tmp_path / "d", False, False, False)
116
+ default_config = (default / "configs" / "global_config.yaml").read_text(
117
+ encoding="utf-8"
118
+ )
119
+ assert "backend: 8000" in default_config
120
+ assert "frontend: 5173" in default_config
121
+
122
+
123
+ def test_display_name_with_quotes_and_chinese(tmp_path):
124
+ """显示名含引号/中文/空格时,各产物文件必须仍然合法。"""
125
+ display_name = '我的 "Cool" App'
126
+ target = _render(tmp_path, True, False, True, display_name=display_name)
127
+
128
+ # HTML 文案直接使用显示名
129
+ assert f"<title>{display_name}</title>" in (
130
+ target / "frontend" / "index.html"
131
+ ).read_text(encoding="utf-8")
132
+
133
+ # electron/package.json 必须是合法 JSON 且 description 正确
134
+ pkg = json.loads((target / "electron" / "package.json").read_text(encoding="utf-8"))
135
+ assert pkg["description"] == display_name
136
+ # 技术标识不被显示名污染
137
+ assert pkg["name"] == "demo"
138
+ assert pkg["build"]["productName"] == "Demo"
139
+
140
+ # Python 产物可编译且标题正确(tojson 转义后的字面量)
141
+ main_py = (target / "backend" / "main.py").read_text(encoding="utf-8")
142
+ compile(main_py, "main.py", "exec")
143
+ assert f"title={json.dumps(display_name, ensure_ascii=False)}" in main_py
144
+
145
+ # pyproject 的 TOML description 合法(TOML basic string 与 JSON 转义兼容)
146
+ pyproject = (target / "pyproject.toml").read_text(encoding="utf-8")
147
+ assert 'description = "我的 \\"Cool\\" App' in pyproject
148
+
149
+ # README 标题使用显示名
150
+ assert f"# {display_name}" in (target / "README.md").read_text(encoding="utf-8")