xb-init 2.5.2__tar.gz → 2.6.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.
- {xb_init-2.5.2 → xb_init-2.6.2}/.gitignore +2 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/PKG-INFO +12 -20
- {xb_init-2.5.2 → xb_init-2.6.2}/README.md +11 -19
- {xb_init-2.5.2 → xb_init-2.6.2}/pyproject.toml +1 -1
- {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_cli.py +20 -44
- {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_config_form.py +13 -41
- xb_init-2.6.2/tests/test_doctor.py +167 -0
- xb_init-2.6.2/tests/test_template_render.py +239 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_update_templates.py +0 -2
- xb_init-2.6.2/xb/commands/doctor.py +318 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/init.py +18 -48
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/config.py.j2 +0 -135
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/main.py.j2 +1 -4
- xb_init-2.6.2/xb/templates/backend/managers/__init__.py.j2 +7 -0
- xb_init-2.6.2/xb/templates/backend/managers/privileged_manager.py.j2 +85 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/updater.py.j2 +21 -14
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/secrets.yaml.example.j2 +0 -3
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/package.json.j2 +7 -0
- xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged-helper.sh.j2 +58 -0
- xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged.policy.j2 +34 -0
- xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged.rules.j2 +22 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/postinst.py.j2 +26 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/postrm.py.j2 +18 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/App.vue.j2 +0 -6
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/FileManager.vue.j2 +6 -77
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/AGENTS.md.j2 +18 -19
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/README.md.j2 +25 -26
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/build.py.j2 +78 -100
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/dev.py.j2 +202 -92
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs//350/207/252/345/212/250/346/233/264/346/226/260/350/257/264/346/230/216.md +10 -9
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/config_form.py +6 -29
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/template_engine.py +27 -26
- xb_init-2.5.2/tests/test_template_render.py +0 -150
- xb_init-2.5.2/uv.lock +0 -445
- xb_init-2.5.2/xb/commands/doctor.py +0 -113
- xb_init-2.5.2/xb/templates/backend/managers/__init__.py.j2 +0 -7
- xb_init-2.5.2/xb/templates/backend/managers/sudoers_manager.py.j2 +0 -165
- xb_init-2.5.2/xb/templates/configs/secrets.yaml.j2 +0 -9
- xb_init-2.5.2/xb/templates/frontend/src/components/ConfigSetup.vue.j2 +0 -227
- {xb_init-2.5.2 → xb_init-2.6.2}/.github/workflows/ci.yml +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/docs/screenshot.png +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_upgrade.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/__init__.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/cli.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/__init__.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/build.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/dev.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/upgrade.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/version.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/__init__.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/ports.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/terminal/__init__.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/terminal/routes.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/update/__init__.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/app_version.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/backend_build.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/logger_manager.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/path_manager.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/secret_obfuscator.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/requirements.txt.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/__init__.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/feishu_client.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/config_changes.json.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/global_config.yaml.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/launcher.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/main.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/port_diagnostics.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/preload.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/prepare.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/uninstaller.nsh.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/index.html.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/package.json.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/DashboardHome.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/GitVersionBadge.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/RefreshButton.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/TerminalPanel.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/TerminalView.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/ThemeToggle.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/version/CommitHistoryPanel.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/version/UpdatePanel.vue.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/composables/useTerminalSessions.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/composables/useUpdate.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/main.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/style.css.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/vite.config.js.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/.gitignore.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/LICENSE.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/feishu-folder.png +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/feishu-permission.png +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/release-upload.png +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/update-dialog.png +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/pyproject.toml.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/scripts/example.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/scripts/feishu_upload.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/hooks/pre-commit.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/gui_version_manager.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/install_hooks.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/version_manager.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/web_version_manager.py.j2 +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/__init__.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/click_helpers.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/project.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/validators.py +0 -0
- {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: xb-init
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.2
|
|
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
|
|
@@ -41,7 +41,6 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
- 自动 `git init` 并提交首个 commit(含 lock 文件)
|
|
42
42
|
- 自动生成 `AGENTS.md`(AI 编码助手协作约定)
|
|
43
43
|
- 内置全局亮色/暗色主题切换
|
|
44
|
-
- 可选 sudo 免密配置(`--sudoers`)
|
|
45
44
|
- 可选内置终端(`--terminal`)
|
|
46
45
|
- 可选应用内自动更新(`--update`,飞书云盘发布 + 版本检查 + 下载校验 + 自动安装)
|
|
47
46
|
- 可选自定义应用图标(`--icon`)
|
|
@@ -75,15 +74,10 @@ xb --help
|
|
|
75
74
|
# 创建项目
|
|
76
75
|
xb init demo
|
|
77
76
|
|
|
78
|
-
# 带 sudo 免密配置
|
|
79
|
-
xb init demo --sudoers
|
|
80
77
|
# 带内置终端
|
|
81
78
|
xb init demo --terminal
|
|
82
79
|
|
|
83
|
-
#
|
|
84
|
-
xb init demo --terminal --sudoers
|
|
85
|
-
|
|
86
|
-
# 启用应用内自动更新(自动附带 --sudoers 免密,输入 sudo 密码即可)
|
|
80
|
+
# 启用应用内自动更新(Linux 提权走 polkit,管理员组无感更新,无密码落盘)
|
|
87
81
|
xb init demo --update
|
|
88
82
|
|
|
89
83
|
# 带自定义图标
|
|
@@ -110,16 +104,14 @@ xb dev stop
|
|
|
110
104
|
────────────────────────────────────────────────────────
|
|
111
105
|
❯ [ ] 应用显示名 : Demo
|
|
112
106
|
[ ] 端口(后端/前端) : 8000 / 8001
|
|
113
|
-
[ ] sudo 免密 ✘ (Linux 应用内更新需要)
|
|
114
107
|
[ ] 内置终端 ✘ (Web 终端组件)
|
|
115
|
-
[ ] 应用内更新 ✘ (
|
|
108
|
+
[ ] 应用内更新 ✘ (Linux 提权走 polkit,无密码落盘)
|
|
116
109
|
[ ] 应用图标 : 使用默认
|
|
117
110
|
[ ] 跳过依赖安装 ✘ (离线环境可跳过)
|
|
118
111
|
```
|
|
119
112
|
|
|
120
|
-
- `--name/--port/--
|
|
113
|
+
- `--name/--port/--terminal/--update/--icon/--skip-install` 传了的参数
|
|
121
114
|
直接预填进表单并勾上,回车即确认,老用法不受影响
|
|
122
|
-
- 勾选"应用内更新"会自动勾上"sudo 免密",且后者此时不可单独取消
|
|
123
115
|
- 文本行(显示名/端口/图标)勾选时若仍是默认值则自动清空,直接输入即可
|
|
124
116
|
- stdin 非终端时(脚本/CI 管道调用)自动跳过表单,沿用 CLI 参数
|
|
125
117
|
|
|
@@ -149,8 +141,8 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
149
141
|
> nrm 全局管理
|
|
150
142
|
> - electron 约 200MB 二进制走华为云镜像下载
|
|
151
143
|
> - uv 源同样自动优化:首次依赖同步(dev.py)与后端构建(build.py -b/-a)
|
|
152
|
-
>
|
|
153
|
-
>
|
|
144
|
+
> 前并行实测各镜像(要求最终 HTTP 200),自动切换到最快可用源(约
|
|
145
|
+
> 1-6 秒,纯标准库实现,无需额外工具)
|
|
154
146
|
> - 若安装失败或超时,会打印提示但不中断项目创建,可稍后手动重试
|
|
155
147
|
> - 执行 `git init` 并提交首个 commit(包含所有文件和 lock 文件)
|
|
156
148
|
> - 若检测到 PyPI 有新版 xb,会询问是否先升级再创建项目
|
|
@@ -160,12 +152,12 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
160
152
|
|
|
161
153
|
| 命令 | 说明 |
|
|
162
154
|
|------|------|
|
|
163
|
-
| `xb init <name> [--name 显示名] [--port 9000] [--
|
|
155
|
+
| `xb init <name> [--name 显示名] [--port 9000] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
|
|
164
156
|
| `xb dev [start\|stop\|status]` | 启动/停止/查看开发环境 |
|
|
165
157
|
| `xb build [all\|frontend\|backend\|electron]` | 构建项目 |
|
|
166
158
|
| `xb build -f / -b / -e / -a` | 构建快捷 flag |
|
|
167
159
|
| `xb version [patch\|minor\|major]` | 读取或更新版本号 |
|
|
168
|
-
| `xb doctor` |
|
|
160
|
+
| `xb doctor [--fix]` | 检查开发环境;`--fix` 自动安装 nrm/uv、交互式配置 git 身份 |
|
|
169
161
|
| `xb --upgrade` | 升级 xb 到 PyPI 最新版本 |
|
|
170
162
|
|
|
171
163
|
## 应用图标
|
|
@@ -209,8 +201,8 @@ xb init demo --icon ./my-icon.png
|
|
|
209
201
|
|
|
210
202
|
**客户端:** 启动时 / 每小时 / 点版本号徽章时检查更新;发现新版本后下载
|
|
211
203
|
(4 线程分块 + SHA256 校验 + `.part` 防半截包)→ 备份配置 → 按
|
|
212
|
-
`config_changes.json` 迁移 → Ubuntu
|
|
213
|
-
|
|
204
|
+
`config_changes.json` 迁移 → Ubuntu 走 polkit 免密安装(授权规则由 DEB
|
|
205
|
+
postinst 预部署,管理员组无感、无密码落盘),Windows 等退出后静默安装。
|
|
214
206
|
|
|
215
207
|
**飞书侧准备:** 开放平台建自建应用 → 开通 `drive:drive` 权限并发布 →
|
|
216
208
|
建云盘文件夹,完整链接填入 `update_folder_url`。开发模式(源码运行)不支持
|
|
@@ -279,8 +271,8 @@ xb/
|
|
|
279
271
|
## 环境要求
|
|
280
272
|
|
|
281
273
|
- **Python**: 3.12+
|
|
282
|
-
- **Node.js**:
|
|
283
|
-
- **npm**:
|
|
274
|
+
- **Node.js**: 20.19+(推荐 22 LTS;模板前端使用 Vite 8,Node 18 及以下会构建报错)
|
|
275
|
+
- **npm**: 10+
|
|
284
276
|
- **uv**: 已安装
|
|
285
277
|
- **OS**: Windows 10/11 或 Ubuntu/Debian
|
|
286
278
|
- **Windows 终端**: 使用 `--terminal` 时自动安装 Windows 专用 `pywinpty`
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
- 自动 `git init` 并提交首个 commit(含 lock 文件)
|
|
13
13
|
- 自动生成 `AGENTS.md`(AI 编码助手协作约定)
|
|
14
14
|
- 内置全局亮色/暗色主题切换
|
|
15
|
-
- 可选 sudo 免密配置(`--sudoers`)
|
|
16
15
|
- 可选内置终端(`--terminal`)
|
|
17
16
|
- 可选应用内自动更新(`--update`,飞书云盘发布 + 版本检查 + 下载校验 + 自动安装)
|
|
18
17
|
- 可选自定义应用图标(`--icon`)
|
|
@@ -46,15 +45,10 @@ xb --help
|
|
|
46
45
|
# 创建项目
|
|
47
46
|
xb init demo
|
|
48
47
|
|
|
49
|
-
# 带 sudo 免密配置
|
|
50
|
-
xb init demo --sudoers
|
|
51
48
|
# 带内置终端
|
|
52
49
|
xb init demo --terminal
|
|
53
50
|
|
|
54
|
-
#
|
|
55
|
-
xb init demo --terminal --sudoers
|
|
56
|
-
|
|
57
|
-
# 启用应用内自动更新(自动附带 --sudoers 免密,输入 sudo 密码即可)
|
|
51
|
+
# 启用应用内自动更新(Linux 提权走 polkit,管理员组无感更新,无密码落盘)
|
|
58
52
|
xb init demo --update
|
|
59
53
|
|
|
60
54
|
# 带自定义图标
|
|
@@ -81,16 +75,14 @@ xb dev stop
|
|
|
81
75
|
────────────────────────────────────────────────────────
|
|
82
76
|
❯ [ ] 应用显示名 : Demo
|
|
83
77
|
[ ] 端口(后端/前端) : 8000 / 8001
|
|
84
|
-
[ ] sudo 免密 ✘ (Linux 应用内更新需要)
|
|
85
78
|
[ ] 内置终端 ✘ (Web 终端组件)
|
|
86
|
-
[ ] 应用内更新 ✘ (
|
|
79
|
+
[ ] 应用内更新 ✘ (Linux 提权走 polkit,无密码落盘)
|
|
87
80
|
[ ] 应用图标 : 使用默认
|
|
88
81
|
[ ] 跳过依赖安装 ✘ (离线环境可跳过)
|
|
89
82
|
```
|
|
90
83
|
|
|
91
|
-
- `--name/--port/--
|
|
84
|
+
- `--name/--port/--terminal/--update/--icon/--skip-install` 传了的参数
|
|
92
85
|
直接预填进表单并勾上,回车即确认,老用法不受影响
|
|
93
|
-
- 勾选"应用内更新"会自动勾上"sudo 免密",且后者此时不可单独取消
|
|
94
86
|
- 文本行(显示名/端口/图标)勾选时若仍是默认值则自动清空,直接输入即可
|
|
95
87
|
- stdin 非终端时(脚本/CI 管道调用)自动跳过表单,沿用 CLI 参数
|
|
96
88
|
|
|
@@ -120,8 +112,8 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
120
112
|
> nrm 全局管理
|
|
121
113
|
> - electron 约 200MB 二进制走华为云镜像下载
|
|
122
114
|
> - uv 源同样自动优化:首次依赖同步(dev.py)与后端构建(build.py -b/-a)
|
|
123
|
-
>
|
|
124
|
-
>
|
|
115
|
+
> 前并行实测各镜像(要求最终 HTTP 200),自动切换到最快可用源(约
|
|
116
|
+
> 1-6 秒,纯标准库实现,无需额外工具)
|
|
125
117
|
> - 若安装失败或超时,会打印提示但不中断项目创建,可稍后手动重试
|
|
126
118
|
> - 执行 `git init` 并提交首个 commit(包含所有文件和 lock 文件)
|
|
127
119
|
> - 若检测到 PyPI 有新版 xb,会询问是否先升级再创建项目
|
|
@@ -131,12 +123,12 @@ Electron 二进制镜像通过 `ELECTRON_MIRROR` 环境变量注入。
|
|
|
131
123
|
|
|
132
124
|
| 命令 | 说明 |
|
|
133
125
|
|------|------|
|
|
134
|
-
| `xb init <name> [--name 显示名] [--port 9000] [--
|
|
126
|
+
| `xb init <name> [--name 显示名] [--port 9000] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
|
|
135
127
|
| `xb dev [start\|stop\|status]` | 启动/停止/查看开发环境 |
|
|
136
128
|
| `xb build [all\|frontend\|backend\|electron]` | 构建项目 |
|
|
137
129
|
| `xb build -f / -b / -e / -a` | 构建快捷 flag |
|
|
138
130
|
| `xb version [patch\|minor\|major]` | 读取或更新版本号 |
|
|
139
|
-
| `xb doctor` |
|
|
131
|
+
| `xb doctor [--fix]` | 检查开发环境;`--fix` 自动安装 nrm/uv、交互式配置 git 身份 |
|
|
140
132
|
| `xb --upgrade` | 升级 xb 到 PyPI 最新版本 |
|
|
141
133
|
|
|
142
134
|
## 应用图标
|
|
@@ -180,8 +172,8 @@ xb init demo --icon ./my-icon.png
|
|
|
180
172
|
|
|
181
173
|
**客户端:** 启动时 / 每小时 / 点版本号徽章时检查更新;发现新版本后下载
|
|
182
174
|
(4 线程分块 + SHA256 校验 + `.part` 防半截包)→ 备份配置 → 按
|
|
183
|
-
`config_changes.json` 迁移 → Ubuntu
|
|
184
|
-
|
|
175
|
+
`config_changes.json` 迁移 → Ubuntu 走 polkit 免密安装(授权规则由 DEB
|
|
176
|
+
postinst 预部署,管理员组无感、无密码落盘),Windows 等退出后静默安装。
|
|
185
177
|
|
|
186
178
|
**飞书侧准备:** 开放平台建自建应用 → 开通 `drive:drive` 权限并发布 →
|
|
187
179
|
建云盘文件夹,完整链接填入 `update_folder_url`。开发模式(源码运行)不支持
|
|
@@ -250,8 +242,8 @@ xb/
|
|
|
250
242
|
## 环境要求
|
|
251
243
|
|
|
252
244
|
- **Python**: 3.12+
|
|
253
|
-
- **Node.js**:
|
|
254
|
-
- **npm**:
|
|
245
|
+
- **Node.js**: 20.19+(推荐 22 LTS;模板前端使用 Vite 8,Node 18 及以下会构建报错)
|
|
246
|
+
- **npm**: 10+
|
|
255
247
|
- **uv**: 已安装
|
|
256
248
|
- **OS**: Windows 10/11 或 Ubuntu/Debian
|
|
257
249
|
- **Windows 终端**: 使用 `--terminal` 时自动安装 Windows 专用 `pywinpty`
|
|
@@ -15,15 +15,9 @@ def test_upgrade_option_runs_without_subcommand(monkeypatch):
|
|
|
15
15
|
assert "Missing command" not in result.output
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def _invoke_init(monkeypatch, tmp_path, args,
|
|
19
|
-
"""跑 init 命令但替换掉耗时/有副作用的步骤,捕获传给模板引擎的参数。
|
|
20
|
-
|
|
21
|
-
- 密码通过 monkeypatch rich Prompt.ask 注入而非 stdin 管道
|
|
22
|
-
- platform 默认 patch 为 linux(询问密码的行为只在 Linux 分支),
|
|
23
|
-
否则真 Windows runner 上 sys.platform=win32 会直接跳过密码询问
|
|
24
|
-
"""
|
|
18
|
+
def _invoke_init(monkeypatch, tmp_path, args, platform="linux"):
|
|
19
|
+
"""跑 init 命令但替换掉耗时/有副作用的步骤,捕获传给模板引擎的参数。"""
|
|
25
20
|
captured = {}
|
|
26
|
-
asked = []
|
|
27
21
|
monkeypatch.setattr(init_module, "get_latest_if_newer", lambda _v: None)
|
|
28
22
|
monkeypatch.setattr(init_module.shutil, "which", lambda _name: None)
|
|
29
23
|
monkeypatch.setattr(init_module, "init_git_repo", lambda *_a, **_k: True)
|
|
@@ -34,46 +28,39 @@ def _invoke_init(monkeypatch, tmp_path, args, password, platform="linux"):
|
|
|
34
28
|
lambda self, **kwargs: captured.update(kwargs),
|
|
35
29
|
)
|
|
36
30
|
|
|
37
|
-
def fake_ask(*_args, **_kwargs):
|
|
38
|
-
asked.append(True)
|
|
39
|
-
return password
|
|
40
|
-
|
|
41
|
-
monkeypatch.setattr(init_module.Prompt, "ask", fake_ask)
|
|
42
|
-
|
|
43
31
|
result = CliRunner().invoke(
|
|
44
32
|
cli.main, ["init", str(tmp_path / "demo"), *args], input="\n"
|
|
45
33
|
)
|
|
46
34
|
assert result.exit_code == 0, result.output
|
|
47
|
-
return captured, result.output
|
|
35
|
+
return captured, result.output
|
|
48
36
|
|
|
49
37
|
|
|
50
|
-
def
|
|
51
|
-
captured,
|
|
38
|
+
def test_update_passed_through(monkeypatch, tmp_path):
|
|
39
|
+
captured, output = _invoke_init(monkeypatch, tmp_path, ["--update"])
|
|
52
40
|
|
|
53
41
|
assert captured["enable_update"] is True
|
|
54
|
-
|
|
55
|
-
assert
|
|
42
|
+
# Linux 上 --update 提示走 polkit(不再有 sudoers 密码询问)
|
|
43
|
+
assert "polkit" in output
|
|
56
44
|
|
|
57
45
|
|
|
58
|
-
def
|
|
59
|
-
captured, output
|
|
46
|
+
def test_update_hint_suppressed_on_windows(monkeypatch, tmp_path):
|
|
47
|
+
captured, output = _invoke_init(
|
|
48
|
+
monkeypatch, tmp_path, ["--update"], platform="win32"
|
|
49
|
+
)
|
|
60
50
|
|
|
61
|
-
assert captured["enable_update"] is
|
|
62
|
-
assert
|
|
63
|
-
assert captured["sudo_password"] == "pw123"
|
|
64
|
-
assert "自动附带" not in output
|
|
51
|
+
assert captured["enable_update"] is True
|
|
52
|
+
assert "polkit" not in output
|
|
65
53
|
|
|
66
54
|
|
|
67
|
-
def
|
|
68
|
-
captured,
|
|
55
|
+
def test_plain_init_has_no_update(monkeypatch, tmp_path):
|
|
56
|
+
captured, _ = _invoke_init(monkeypatch, tmp_path, [])
|
|
69
57
|
|
|
70
58
|
assert captured["enable_update"] is False
|
|
71
|
-
assert captured["
|
|
72
|
-
assert captured["sudo_password"] == ""
|
|
59
|
+
assert captured["enable_terminal"] is False
|
|
73
60
|
|
|
74
61
|
|
|
75
62
|
def test_display_name_passed_through(monkeypatch, tmp_path):
|
|
76
|
-
captured, _
|
|
63
|
+
captured, _ = _invoke_init(
|
|
77
64
|
monkeypatch, tmp_path, ["--name", "我的应用"], ""
|
|
78
65
|
)
|
|
79
66
|
|
|
@@ -81,19 +68,19 @@ def test_display_name_passed_through(monkeypatch, tmp_path):
|
|
|
81
68
|
|
|
82
69
|
|
|
83
70
|
def test_display_name_defaults_to_none(monkeypatch, tmp_path):
|
|
84
|
-
captured, _
|
|
71
|
+
captured, _ = _invoke_init(monkeypatch, tmp_path, [])
|
|
85
72
|
|
|
86
73
|
assert captured["display_name"] is None
|
|
87
74
|
|
|
88
75
|
|
|
89
76
|
def test_port_passed_through(monkeypatch, tmp_path):
|
|
90
|
-
captured, _output
|
|
77
|
+
captured, _output = _invoke_init(monkeypatch, tmp_path, ["--port", "9100"])
|
|
91
78
|
|
|
92
79
|
assert captured["backend_port"] == 9100
|
|
93
80
|
|
|
94
81
|
|
|
95
82
|
def test_port_defaults_to_none(monkeypatch, tmp_path):
|
|
96
|
-
captured, _
|
|
83
|
+
captured, _ = _invoke_init(monkeypatch, tmp_path, [])
|
|
97
84
|
|
|
98
85
|
assert captured["backend_port"] is None
|
|
99
86
|
|
|
@@ -123,14 +110,3 @@ def test_pick_fastest_from_nrm_output():
|
|
|
123
110
|
def test_pick_fastest_from_nrm_output_all_failed():
|
|
124
111
|
output = " npmMirror ---- 1265 ms (Fetch error, if this is your private registry, please ignore)\n"
|
|
125
112
|
assert init_module._pick_fastest_from_nrm_output(output) == ("", None)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_windows_skips_sudo_password(monkeypatch, tmp_path):
|
|
129
|
-
captured, output, asked = _invoke_init(
|
|
130
|
-
monkeypatch, tmp_path, ["--sudoers"], "should-not-be-used", platform="win32"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
assert captured["enable_sudo"] is True
|
|
134
|
-
assert captured["sudo_password"] == ""
|
|
135
|
-
assert asked == [] # Windows 不询问密码
|
|
136
|
-
assert "跳过密码输入" in output
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
"""配置表单(ConfigForm)交互逻辑测试:不进键盘循环,直接驱动方法。
|
|
2
|
-
|
|
3
|
-
表单在 Windows 上会隐藏 sudoers 行,逻辑测试统一以 Linux 形态为准
|
|
4
|
-
(mock os.name),Windows 隐藏行为由 test_sudoers_row_hidden_on_windows 覆盖。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import unittest.mock as mock
|
|
1
|
+
"""配置表单(ConfigForm)交互逻辑测试:不进键盘循环,直接驱动方法。"""
|
|
8
2
|
|
|
9
3
|
from xb.utils.config_form import ConfigForm, run_config_form
|
|
10
4
|
|
|
11
5
|
|
|
12
6
|
def make_form(cli=None):
|
|
13
|
-
|
|
14
|
-
return ConfigForm("demo", cli)
|
|
7
|
+
return ConfigForm("demo", cli)
|
|
15
8
|
|
|
16
9
|
|
|
17
10
|
def _cursor_key(form, key):
|
|
@@ -19,28 +12,13 @@ def _cursor_key(form, key):
|
|
|
19
12
|
i for i, r in enumerate(form.rows) if r["key"] == key)
|
|
20
13
|
|
|
21
14
|
|
|
22
|
-
def
|
|
23
|
-
form = make_form()
|
|
24
|
-
_cursor_key(form, "update")
|
|
25
|
-
form.toggle()
|
|
26
|
-
assert form._row("update")["checked"] is True
|
|
27
|
-
assert form._row("sudoers")["checked"] is True
|
|
28
|
-
|
|
29
|
-
# update 勾着时 sudoers 无法单独取消
|
|
30
|
-
_cursor_key(form, "sudoers")
|
|
31
|
-
form.toggle()
|
|
32
|
-
assert form._row("sudoers")["checked"] is True
|
|
33
|
-
assert form.message # 有提示
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_sudoers_toggle_without_update():
|
|
15
|
+
def test_toggle_switches_rows():
|
|
37
16
|
form = make_form()
|
|
38
|
-
_cursor_key(form, "
|
|
17
|
+
_cursor_key(form, "terminal")
|
|
39
18
|
form.toggle()
|
|
40
|
-
assert form._row("
|
|
19
|
+
assert form._row("terminal")["checked"] is True
|
|
41
20
|
form.toggle()
|
|
42
|
-
assert form._row("
|
|
43
|
-
assert not form.message
|
|
21
|
+
assert form._row("terminal")["checked"] is False
|
|
44
22
|
|
|
45
23
|
|
|
46
24
|
def test_text_row_clears_default_on_first_check():
|
|
@@ -68,12 +46,16 @@ def test_text_row_keeps_custom_value_when_rechecked():
|
|
|
68
46
|
assert form._row("display_name")["value"] == "机器人"
|
|
69
47
|
|
|
70
48
|
|
|
49
|
+
def test_no_sudo_row_in_any_platform():
|
|
50
|
+
"""sudoers 行已随 polkit 方案彻底移除,任何平台都不应出现。"""
|
|
51
|
+
assert not make_form()._has("sudoers")
|
|
52
|
+
|
|
53
|
+
|
|
71
54
|
def test_collect_unchecked_items_are_none():
|
|
72
55
|
form = make_form()
|
|
73
56
|
result = form.collect()
|
|
74
57
|
assert result["display_name"] is None
|
|
75
58
|
assert result["port"] is None
|
|
76
|
-
assert result["sudoers"] is False
|
|
77
59
|
assert result["terminal"] is False
|
|
78
60
|
assert result["update"] is False
|
|
79
61
|
assert result["icon"] is None
|
|
@@ -88,7 +70,6 @@ def test_collect_valid_port():
|
|
|
88
70
|
form.feed_char(ch)
|
|
89
71
|
result = form.collect()
|
|
90
72
|
assert result["port"] == 9200
|
|
91
|
-
assert result["sudoers"] is False
|
|
92
73
|
|
|
93
74
|
|
|
94
75
|
def test_collect_invalid_port_falls_back_to_none():
|
|
@@ -102,14 +83,6 @@ def test_collect_invalid_port_falls_back_to_none():
|
|
|
102
83
|
assert form._row("port")["checked"] is False
|
|
103
84
|
|
|
104
85
|
|
|
105
|
-
def test_sudoers_row_hidden_on_windows():
|
|
106
|
-
with mock.patch("xb.utils.config_form.os.name", "nt"):
|
|
107
|
-
form = ConfigForm("demo")
|
|
108
|
-
assert not form._has("sudoers")
|
|
109
|
-
result = form.collect()
|
|
110
|
-
assert result["sudoers"] is False
|
|
111
|
-
|
|
112
|
-
|
|
113
86
|
def test_backspace_only_on_checked_text_row():
|
|
114
87
|
form = make_form({"display_name": "abc"})
|
|
115
88
|
_cursor_key(form, "display_name")
|
|
@@ -124,14 +97,13 @@ def test_run_config_form_skipped_when_not_tty():
|
|
|
124
97
|
result = run_config_form("demo", {
|
|
125
98
|
"display_name": None,
|
|
126
99
|
"port": 9100,
|
|
127
|
-
"sudoers": False,
|
|
128
100
|
"terminal": True,
|
|
129
101
|
"update": True,
|
|
130
102
|
"icon": None,
|
|
131
103
|
"skip_install": False,
|
|
132
104
|
})
|
|
133
|
-
# 非 tty 环境跳过表单,CLI
|
|
105
|
+
# 非 tty 环境跳过表单,CLI 值原样透传
|
|
134
106
|
assert result["port"] == 9100
|
|
135
|
-
assert result["sudoers"] is True
|
|
136
107
|
assert result["terminal"] is True
|
|
108
|
+
assert result["update"] is True
|
|
137
109
|
assert result["display_name"] is None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
import xb.commands.doctor as doctor_module
|
|
6
|
+
from xb import cli
|
|
7
|
+
from xb.commands.doctor import CheckResult, _fix_nrm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_doctor_plain_run_does_not_fix(monkeypatch):
|
|
11
|
+
called = []
|
|
12
|
+
monkeypatch.setattr(
|
|
13
|
+
doctor_module,
|
|
14
|
+
"_collect_checks",
|
|
15
|
+
lambda: [CheckResult("uv", False, "未找到", fixable=True)],
|
|
16
|
+
)
|
|
17
|
+
monkeypatch.setitem(doctor_module._FIXERS, "uv", lambda: called.append(1) or True)
|
|
18
|
+
|
|
19
|
+
result = CliRunner().invoke(cli.main, ["doctor"])
|
|
20
|
+
|
|
21
|
+
assert result.exit_code == 0
|
|
22
|
+
assert called == []
|
|
23
|
+
assert "需要处理" in result.output
|
|
24
|
+
assert "正在修复" not in result.output
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_doctor_fix_invokes_fixer_and_rechecks(monkeypatch):
|
|
28
|
+
calls = []
|
|
29
|
+
|
|
30
|
+
def fake_checks():
|
|
31
|
+
return [
|
|
32
|
+
CheckResult("uv", False, "未找到", fixable=True),
|
|
33
|
+
CheckResult("Node.js", False, "未找到"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(doctor_module, "_collect_checks", fake_checks)
|
|
37
|
+
monkeypatch.setitem(doctor_module._FIXERS, "uv", lambda: calls.append("uv") or True)
|
|
38
|
+
|
|
39
|
+
result = CliRunner().invoke(cli.main, ["doctor", "--fix"])
|
|
40
|
+
|
|
41
|
+
assert result.exit_code == 0, result.output
|
|
42
|
+
assert calls == ["uv"]
|
|
43
|
+
assert "可自动修复" in result.output
|
|
44
|
+
assert "uv 已修复" in result.output
|
|
45
|
+
assert "修复后复查" in result.output
|
|
46
|
+
# Node.js 缺失不可自动修复,修复循环不应触碰它
|
|
47
|
+
assert "Node.js 已修复" not in result.output
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_doctor_fix_reports_failure(monkeypatch):
|
|
51
|
+
monkeypatch.setattr(
|
|
52
|
+
doctor_module,
|
|
53
|
+
"_collect_checks",
|
|
54
|
+
lambda: [CheckResult("uv", False, "未找到", fixable=True)],
|
|
55
|
+
)
|
|
56
|
+
monkeypatch.setitem(doctor_module._FIXERS, "uv", lambda: False)
|
|
57
|
+
|
|
58
|
+
result = CliRunner().invoke(cli.main, ["doctor", "--fix"])
|
|
59
|
+
|
|
60
|
+
assert result.exit_code == 0
|
|
61
|
+
assert "uv 修复失败" in result.output
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_doctor_fix_nothing_fixable(monkeypatch):
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
doctor_module,
|
|
67
|
+
"_collect_checks",
|
|
68
|
+
lambda: [CheckResult("Node.js", False, "未找到")],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
result = CliRunner().invoke(cli.main, ["doctor", "--fix"])
|
|
72
|
+
|
|
73
|
+
assert result.exit_code == 0
|
|
74
|
+
assert "没有可自动修复的问题" in result.output
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_doctor_fix_git_identity(monkeypatch):
|
|
78
|
+
commands = []
|
|
79
|
+
|
|
80
|
+
def fake_run(cmd, **kwargs):
|
|
81
|
+
commands.append(cmd)
|
|
82
|
+
return subprocess.CompletedProcess(cmd, 0)
|
|
83
|
+
|
|
84
|
+
monkeypatch.setattr(
|
|
85
|
+
doctor_module,
|
|
86
|
+
"_collect_checks",
|
|
87
|
+
lambda: [CheckResult("git 身份", False, "未配置 user.name, user.email", fixable=True)],
|
|
88
|
+
)
|
|
89
|
+
monkeypatch.setattr(doctor_module.shutil, "which", lambda name: "/usr/bin/git")
|
|
90
|
+
monkeypatch.setattr(doctor_module.subprocess, "run", fake_run)
|
|
91
|
+
|
|
92
|
+
result = CliRunner().invoke(cli.main, ["doctor", "--fix"], input="张三\nzhangsan@test.com\n")
|
|
93
|
+
|
|
94
|
+
assert result.exit_code == 0, result.output
|
|
95
|
+
assert ["git", "config", "--global", "user.name", "张三"] in commands
|
|
96
|
+
assert ["git", "config", "--global", "user.email", "zhangsan@test.com"] in commands
|
|
97
|
+
assert "git 身份 已修复" in result.output
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_git_identity_check_in_collect_checks(monkeypatch):
|
|
101
|
+
monkeypatch.setattr(doctor_module.shutil, "which", lambda name: "/usr/bin/git")
|
|
102
|
+
monkeypatch.setattr(
|
|
103
|
+
doctor_module,
|
|
104
|
+
"_git_global_config",
|
|
105
|
+
lambda key: "xb-test" if key == "user.name" else None,
|
|
106
|
+
)
|
|
107
|
+
monkeypatch.setattr(doctor_module, "_command_version", lambda *a, **k: "v22.12.0")
|
|
108
|
+
|
|
109
|
+
checks = {item.name: item for item in doctor_module._collect_checks()}
|
|
110
|
+
|
|
111
|
+
assert checks["git 身份"].ok is False
|
|
112
|
+
assert "user.email" in checks["git 身份"].detail
|
|
113
|
+
assert checks["git 身份"].fixable is True
|
|
114
|
+
assert checks["uv"].fixable is True
|
|
115
|
+
assert checks["nrm (npm 源测速)"].fixable is True
|
|
116
|
+
assert checks["Node.js"].fixable is False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_version_tuple_parses_tool_output():
|
|
120
|
+
assert doctor_module._version_tuple("v22.23.2") == (22, 23, 2)
|
|
121
|
+
assert doctor_module._version_tuple("npm 10.9.8") == (10, 9, 8)
|
|
122
|
+
assert doctor_module._version_tuple("git version 2.43.0") == (2, 43, 0)
|
|
123
|
+
assert doctor_module._version_tuple("无版本号输出") is None
|
|
124
|
+
assert doctor_module._version_tuple(None) is None
|
|
125
|
+
assert doctor_module._version_tuple("") is None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_node_version_too_low_rejected(monkeypatch):
|
|
129
|
+
versions = {
|
|
130
|
+
"uv": "uv 0.12.8 (x86_64-unknown-linux-gnu)",
|
|
131
|
+
"node": "v18.19.0",
|
|
132
|
+
"npm": "9.8.1",
|
|
133
|
+
"git": "git version 2.43.0",
|
|
134
|
+
}
|
|
135
|
+
monkeypatch.setattr(doctor_module, "_command_version", lambda name, *a, **k: versions.get(name))
|
|
136
|
+
monkeypatch.setattr(doctor_module.shutil, "which", lambda name: None)
|
|
137
|
+
|
|
138
|
+
checks = {item.name: item for item in doctor_module._collect_checks()}
|
|
139
|
+
|
|
140
|
+
assert checks["Node.js"].ok is False
|
|
141
|
+
assert "版本过低" in checks["Node.js"].detail
|
|
142
|
+
assert "20.19" in checks["Node.js"].detail
|
|
143
|
+
assert checks["npm"].ok is False
|
|
144
|
+
assert checks["uv"].ok is True
|
|
145
|
+
assert checks["git"].ok is True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_node_version_met_passes(monkeypatch):
|
|
149
|
+
versions = {
|
|
150
|
+
"uv": "uv 0.12.8",
|
|
151
|
+
"node": "v20.19.0",
|
|
152
|
+
"npm": "10.9.8",
|
|
153
|
+
"git": "git version 2.43.0",
|
|
154
|
+
}
|
|
155
|
+
monkeypatch.setattr(doctor_module, "_command_version", lambda name, *a, **k: versions.get(name))
|
|
156
|
+
monkeypatch.setattr(doctor_module.shutil, "which", lambda name: None)
|
|
157
|
+
|
|
158
|
+
checks = {item.name: item for item in doctor_module._collect_checks()}
|
|
159
|
+
|
|
160
|
+
assert checks["Node.js"].ok is True
|
|
161
|
+
assert checks["npm"].ok is True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_fix_nrm_skips_without_npm(monkeypatch):
|
|
165
|
+
monkeypatch.setattr(doctor_module.shutil, "which", lambda name: None)
|
|
166
|
+
|
|
167
|
+
assert _fix_nrm() is False
|