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.
Files changed (104) hide show
  1. {xb_init-2.5.2 → xb_init-2.6.2}/.gitignore +2 -0
  2. {xb_init-2.5.2 → xb_init-2.6.2}/PKG-INFO +12 -20
  3. {xb_init-2.5.2 → xb_init-2.6.2}/README.md +11 -19
  4. {xb_init-2.5.2 → xb_init-2.6.2}/pyproject.toml +1 -1
  5. {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_cli.py +20 -44
  6. {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_config_form.py +13 -41
  7. xb_init-2.6.2/tests/test_doctor.py +167 -0
  8. xb_init-2.6.2/tests/test_template_render.py +239 -0
  9. {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_update_templates.py +0 -2
  10. xb_init-2.6.2/xb/commands/doctor.py +318 -0
  11. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/init.py +18 -48
  12. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/config.py.j2 +0 -135
  13. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/main.py.j2 +1 -4
  14. xb_init-2.6.2/xb/templates/backend/managers/__init__.py.j2 +7 -0
  15. xb_init-2.6.2/xb/templates/backend/managers/privileged_manager.py.j2 +85 -0
  16. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/updater.py.j2 +21 -14
  17. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/secrets.yaml.example.j2 +0 -3
  18. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/package.json.j2 +7 -0
  19. xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged-helper.sh.j2 +58 -0
  20. xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged.policy.j2 +34 -0
  21. xb_init-2.6.2/xb/templates/electron/resources/polkit/privileged.rules.j2 +22 -0
  22. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/postinst.py.j2 +26 -0
  23. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/postrm.py.j2 +18 -0
  24. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/App.vue.j2 +0 -6
  25. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/FileManager.vue.j2 +6 -77
  26. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/AGENTS.md.j2 +18 -19
  27. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/README.md.j2 +25 -26
  28. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/build.py.j2 +78 -100
  29. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/dev.py.j2 +202 -92
  30. {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
  31. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/config_form.py +6 -29
  32. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/template_engine.py +27 -26
  33. xb_init-2.5.2/tests/test_template_render.py +0 -150
  34. xb_init-2.5.2/uv.lock +0 -445
  35. xb_init-2.5.2/xb/commands/doctor.py +0 -113
  36. xb_init-2.5.2/xb/templates/backend/managers/__init__.py.j2 +0 -7
  37. xb_init-2.5.2/xb/templates/backend/managers/sudoers_manager.py.j2 +0 -165
  38. xb_init-2.5.2/xb/templates/configs/secrets.yaml.j2 +0 -9
  39. xb_init-2.5.2/xb/templates/frontend/src/components/ConfigSetup.vue.j2 +0 -227
  40. {xb_init-2.5.2 → xb_init-2.6.2}/.github/workflows/ci.yml +0 -0
  41. {xb_init-2.5.2 → xb_init-2.6.2}/docs/screenshot.png +0 -0
  42. {xb_init-2.5.2 → xb_init-2.6.2}/tests/test_upgrade.py +0 -0
  43. {xb_init-2.5.2 → xb_init-2.6.2}/xb/__init__.py +0 -0
  44. {xb_init-2.5.2 → xb_init-2.6.2}/xb/cli.py +0 -0
  45. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/__init__.py +0 -0
  46. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/build.py +0 -0
  47. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/dev.py +0 -0
  48. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/upgrade.py +0 -0
  49. {xb_init-2.5.2 → xb_init-2.6.2}/xb/commands/version.py +0 -0
  50. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/__init__.py.j2 +0 -0
  51. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/ports.py.j2 +0 -0
  52. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/terminal/__init__.py.j2 +0 -0
  53. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/terminal/routes.py.j2 +0 -0
  54. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/api/update/__init__.py.j2 +0 -0
  55. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/app_version.py.j2 +0 -0
  56. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/backend_build.py.j2 +0 -0
  57. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/logger_manager.py.j2 +0 -0
  58. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/path_manager.py.j2 +0 -0
  59. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/managers/secret_obfuscator.py.j2 +0 -0
  60. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/requirements.txt.j2 +0 -0
  61. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/__init__.py.j2 +0 -0
  62. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/backend/services/feishu_client.py.j2 +0 -0
  63. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/config_changes.json.j2 +0 -0
  64. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/configs/global_config.yaml.j2 +0 -0
  65. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/launcher.js.j2 +0 -0
  66. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/main.js.j2 +0 -0
  67. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/port_diagnostics.js.j2 +0 -0
  68. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/preload.js.j2 +0 -0
  69. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/prepare.js.j2 +0 -0
  70. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/electron/resources/uninstaller.nsh.j2 +0 -0
  71. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/index.html.j2 +0 -0
  72. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/package.json.j2 +0 -0
  73. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/DashboardHome.vue.j2 +0 -0
  74. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/GitVersionBadge.vue.j2 +0 -0
  75. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/RefreshButton.vue.j2 +0 -0
  76. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/TerminalPanel.vue.j2 +0 -0
  77. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/TerminalView.vue.j2 +0 -0
  78. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/ThemeToggle.vue.j2 +0 -0
  79. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/version/CommitHistoryPanel.vue.j2 +0 -0
  80. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/components/version/UpdatePanel.vue.j2 +0 -0
  81. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/composables/useTerminalSessions.js.j2 +0 -0
  82. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/composables/useUpdate.js.j2 +0 -0
  83. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/main.js.j2 +0 -0
  84. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/src/style.css.j2 +0 -0
  85. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/frontend/vite.config.js.j2 +0 -0
  86. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/.gitignore.j2 +0 -0
  87. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/LICENSE.j2 +0 -0
  88. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/feishu-folder.png +0 -0
  89. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/feishu-permission.png +0 -0
  90. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/release-upload.png +0 -0
  91. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/docs/images/update-dialog.png +0 -0
  92. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/root/pyproject.toml.j2 +0 -0
  93. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/scripts/example.py.j2 +0 -0
  94. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/scripts/feishu_upload.py.j2 +0 -0
  95. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/hooks/pre-commit.py.j2 +0 -0
  96. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/gui_version_manager.py.j2 +0 -0
  97. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/install_hooks.py.j2 +0 -0
  98. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/version_manager.py.j2 +0 -0
  99. {xb_init-2.5.2 → xb_init-2.6.2}/xb/templates/version/scripts/web_version_manager.py.j2 +0 -0
  100. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/__init__.py +0 -0
  101. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/click_helpers.py +0 -0
  102. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/project.py +0 -0
  103. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/validators.py +0 -0
  104. {xb_init-2.5.2 → xb_init-2.6.2}/xb/utils/version_check.py +0 -0
@@ -36,3 +36,5 @@ htmlcov/
36
36
  *.log
37
37
  .env
38
38
  .env.local
39
+
40
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xb-init
3
- Version: 2.5.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
- # 同时启用终端和 sudo 免密配置
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
- [ ] 应用内更新 ✘ (自动附带 sudo 免密)
108
+ [ ] 应用内更新 ✘ (Linux 提权走 polkit,无密码落盘)
116
109
  [ ] 应用图标 : 使用默认
117
110
  [ ] 跳过依赖安装 ✘ (离线环境可跳过)
118
111
  ```
119
112
 
120
- - `--name/--port/--sudoers/--terminal/--update/--icon/--skip-install` 传了的参数
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
- > 前用 [chsrc](https://github.com/RubyMetric/chsrc) 测速切换最快镜像
153
- > (`chsrc set uv first`,未安装 chsrc 时自动安装)
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] [--sudoers] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
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 趁应用存活时免密 `sudo dpkg -i`
213
- (`--update` 默认附带 `--sudoers` 免密),Windows 等退出后静默安装。
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**: 16+
283
- - **npm**: 8+
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
- # 同时启用终端和 sudo 免密配置
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
- [ ] 应用内更新 ✘ (自动附带 sudo 免密)
79
+ [ ] 应用内更新 ✘ (Linux 提权走 polkit,无密码落盘)
87
80
  [ ] 应用图标 : 使用默认
88
81
  [ ] 跳过依赖安装 ✘ (离线环境可跳过)
89
82
  ```
90
83
 
91
- - `--name/--port/--sudoers/--terminal/--update/--icon/--skip-install` 传了的参数
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
- > 前用 [chsrc](https://github.com/RubyMetric/chsrc) 测速切换最快镜像
124
- > (`chsrc set uv first`,未安装 chsrc 时自动安装)
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] [--sudoers] [--terminal] [--update] [--icon PATH] [--skip-install]` | 初始化项目 |
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 趁应用存活时免密 `sudo dpkg -i`
184
- (`--update` 默认附带 `--sudoers` 免密),Windows 等退出后静默安装。
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**: 16+
254
- - **npm**: 8+
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`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xb-init"
7
- version = "2.5.2"
7
+ version = "2.6.2"
8
8
  description = "Project management tool for UV + FastAPI + Vue3 + Electron desktop applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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, password, platform="linux"):
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, asked
35
+ return captured, result.output
48
36
 
49
37
 
50
- def test_update_implies_sudoers(monkeypatch, tmp_path):
51
- captured, _output, _asked = _invoke_init(monkeypatch, tmp_path, ["--update"], "pw123")
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
- assert captured["enable_sudo"] is True
55
- assert captured["sudo_password"] == "pw123"
42
+ # Linux 上 --update 提示走 polkit(不再有 sudoers 密码询问)
43
+ assert "polkit" in output
56
44
 
57
45
 
58
- def test_sudoers_alone_still_prompts(monkeypatch, tmp_path):
59
- captured, output, asked = _invoke_init(monkeypatch, tmp_path, ["--sudoers"], "pw123")
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 False
62
- assert captured["enable_sudo"] is True
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 test_plain_init_has_no_sudo(monkeypatch, tmp_path):
68
- captured, output, asked = _invoke_init(monkeypatch, tmp_path, [], "")
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["enable_sudo"] is False
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, _, _asked = _invoke_init(
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, _, _asked = _invoke_init(monkeypatch, tmp_path, [], "")
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, _asked = _invoke_init(monkeypatch, tmp_path, ["--port", "9100"], "")
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, _, _asked = _invoke_init(monkeypatch, tmp_path, [], "")
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
- with mock.patch("xb.utils.config_form.os.name", "posix"):
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 test_update_checks_sudoers_and_blocks_unchecking():
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, "sudoers")
17
+ _cursor_key(form, "terminal")
39
18
  form.toggle()
40
- assert form._row("sudoers")["checked"] is True
19
+ assert form._row("terminal")["checked"] is True
41
20
  form.toggle()
42
- assert form._row("sudoers")["checked"] is False
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 值原样透传且 update 联动 sudoers
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