chatup 0.2.20__tar.gz → 0.2.22__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 (108) hide show
  1. {chatup-0.2.20/src/chatup.egg-info → chatup-0.2.22}/PKG-INFO +12 -6
  2. {chatup-0.2.20 → chatup-0.2.22}/README.md +10 -5
  3. {chatup-0.2.20 → chatup-0.2.22}/pyproject.toml +1 -0
  4. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/__init__.py +1 -1
  5. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/cli.py +2 -1
  6. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/desktop.py +9 -0
  7. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/_managed_artifact.py +1 -1
  8. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/desktop.py +83 -12
  9. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/elements.py +41 -0
  10. chatup-0.2.22/src/chatup/setup/glance.py +240 -0
  11. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/options.py +125 -6
  12. {chatup-0.2.20 → chatup-0.2.22/src/chatup.egg-info}/PKG-INFO +12 -6
  13. {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/SOURCES.txt +3 -0
  14. {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/requires.txt +1 -0
  15. {chatup-0.2.20 → chatup-0.2.22}/tests/test_browser_backend_cli.py +3 -0
  16. {chatup-0.2.20 → chatup-0.2.22}/tests/test_chrome_for_testing.py +18 -3
  17. {chatup-0.2.20 → chatup-0.2.22}/tests/test_chromedriver.py +2 -2
  18. {chatup-0.2.20 → chatup-0.2.22}/tests/test_cli_tree_docs.py +1 -0
  19. chatup-0.2.22/tests/test_glance_bridge.py +6 -0
  20. chatup-0.2.22/tests/test_glance_setup.py +190 -0
  21. {chatup-0.2.20 → chatup-0.2.22}/tests/test_playwright.py +1 -1
  22. {chatup-0.2.20 → chatup-0.2.22}/tests/test_pyproject_metadata.py +1 -0
  23. {chatup-0.2.20 → chatup-0.2.22}/tests/test_setup_cli.py +2 -0
  24. {chatup-0.2.20 → chatup-0.2.22}/tests/test_version.py +1 -1
  25. {chatup-0.2.20 → chatup-0.2.22}/tests/test_workspace_setup.py +126 -10
  26. {chatup-0.2.20 → chatup-0.2.22}/LICENSE +0 -0
  27. {chatup-0.2.20 → chatup-0.2.22}/setup.cfg +0 -0
  28. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/chrome_for_testing.py +0 -0
  29. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/chromedriver.py +0 -0
  30. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/__init__.py +0 -0
  31. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/chrome_for_testing.py +0 -0
  32. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/chromedriver.py +0 -0
  33. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/macos.py +0 -0
  34. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/playwright.py +0 -0
  35. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/remotion.py +0 -0
  36. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/config.py +0 -0
  37. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/const.py +0 -0
  38. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/__init__.py +0 -0
  39. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/command_schema.py +0 -0
  40. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/policy.py +0 -0
  41. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/warnings.py +0 -0
  42. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/playwright.py +0 -0
  43. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/__init__.py +0 -0
  44. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/chrome_for_testing.py +0 -0
  45. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/chromedriver.py +0 -0
  46. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/playwright.py +0 -0
  47. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/__init__.py +0 -0
  48. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/hermes/install.sh +0 -0
  49. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/nvm.sh +0 -0
  50. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/.gitignore +0 -0
  51. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
  52. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package.json +0 -0
  53. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
  54. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cc_connect.py +0 -0
  55. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/chatgpt.py +0 -0
  56. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/claude.py +0 -0
  57. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cli.py +0 -0
  58. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/codex.py +0 -0
  59. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/crs.py +0 -0
  60. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cursor_agent.py +0 -0
  61. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/discourse.py +0 -0
  62. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/docker.py +0 -0
  63. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/frp.py +0 -0
  64. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/gitea.py +0 -0
  65. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/hermes.py +0 -0
  66. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/lark_cli.py +0 -0
  67. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/mode_prompt.py +0 -0
  68. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/mysql.py +0 -0
  69. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/nginx.py +0 -0
  70. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/nodejs.py +0 -0
  71. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/opencode.py +0 -0
  72. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/remotion.py +0 -0
  73. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/service_credentials.py +0 -0
  74. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/shell_rc.py +0 -0
  75. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/twikoo.py +0 -0
  76. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/uv.py +0 -0
  77. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/__init__.py +0 -0
  78. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/cli.py +0 -0
  79. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/core.py +0 -0
  80. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/render.py +0 -0
  81. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  82. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  83. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  84. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  85. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  86. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  87. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace.py +0 -0
  88. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/zsh.py +0 -0
  89. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/zulip.py +0 -0
  90. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/__init__.py +0 -0
  91. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/custom_logger.py +0 -0
  92. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/pathing.py +0 -0
  93. {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/platforming.py +0 -0
  94. {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/dependency_links.txt +0 -0
  95. {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/entry_points.txt +0 -0
  96. {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/top_level.txt +0 -0
  97. {chatup-0.2.20 → chatup-0.2.22}/tests/test_chatgpt_setup.py +0 -0
  98. {chatup-0.2.20 → chatup-0.2.22}/tests/test_codex_setup.py +0 -0
  99. {chatup-0.2.20 → chatup-0.2.22}/tests/test_crs_setup.py +0 -0
  100. {chatup-0.2.20 → chatup-0.2.22}/tests/test_cursor_agent_setup.py +0 -0
  101. {chatup-0.2.20 → chatup-0.2.22}/tests/test_hermes_model_defaults.py +0 -0
  102. {chatup-0.2.20 → chatup-0.2.22}/tests/test_playwright_cli.py +0 -0
  103. {chatup-0.2.20 → chatup-0.2.22}/tests/test_publish_workflow.py +0 -0
  104. {chatup-0.2.20 → chatup-0.2.22}/tests/test_service_installers.py +0 -0
  105. {chatup-0.2.20 → chatup-0.2.22}/tests/test_twikoo_setup.py +0 -0
  106. {chatup-0.2.20 → chatup-0.2.22}/tests/test_uv_activation.py +0 -0
  107. {chatup-0.2.20 → chatup-0.2.22}/tests/test_windows_compat.py +0 -0
  108. {chatup-0.2.20 → chatup-0.2.22}/tests/test_zsh_alias_names.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.20
3
+ Version: 0.2.22
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle[tui]<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: ChatGlance<0.4.0,>=0.3.0
19
20
  Requires-Dist: python-dotenv<2.0,>=1.0
20
21
  Requires-Dist: colorama
21
22
  Provides-Extra: dev
@@ -59,7 +60,7 @@ chatup doctor
59
60
  chatup uv
60
61
  chatup chrome-for-testing install --channel stable -I
61
62
  chatup playwright install 1.61.1 -I
62
- chatup workspace default ~/Playground
63
+ chatup workspace ~/Playground
63
64
  ```
64
65
 
65
66
  `chatup uv` 默认更新已有 `~/.bashrc`、`~/.zshrc` 的自动激活块;使用 `chatup uv --no-activate` 可保持启动配置不变。不会创建缺失的 rc 文件,Windows 跳过此项。
@@ -68,6 +69,8 @@ chatup workspace default ~/Playground
68
69
 
69
70
  ```bash
70
71
  chatup gitea --force
72
+ chatup glance --dry-run
73
+ chatup glance
71
74
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
72
75
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
73
76
  chatup mysql
@@ -99,16 +102,17 @@ npm run studio
99
102
 
100
103
  需要已有 Node.js >=18.12 和 npm >=9。创建带锁文件的 Remotion 4.0.530 项目,检测本机 Chrome 并输出渲染命令;不覆盖已有无关目录,也不替换 Node 或下载浏览器。项目放在指定目录,已有 Blender/Chrome 可通过 `chatup macos --app blender --app chrome` 验证或安装。详见[Remotion 安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#remotion)。
101
104
 
102
- ## Chrome 与 iTerm2
105
+ ## Chrome、Snipaste 与 iTerm2
103
106
 
104
107
  ```bash
105
108
  chatup chrome --dry-run
106
109
  chatup chrome # 按系统安装普通 Google Chrome
110
+ chatup snipaste # 在 macOS 或 Windows 安装 Snipaste
107
111
  chatup iterm # 安装 iTerm2,仅限 macOS
108
112
  chatup chrome --sudo --yes # Linux:允许提权并确认包安装
109
113
  ```
110
114
 
111
- macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet `Google.Chrome`;Linux x86_64 使用官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
115
+ macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet 精确包 `Google.Chrome.EXE`(当前用户安装)和 `liule.Snipaste`,需以 `--yes` 接受首次协议,Chrome 还会验证已安装程序的 Google 签名、产品身份和版本;Linux x86_64 支持 Chrome 的官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
112
116
 
113
117
  ## ChatGPT / Codex 桌面应用
114
118
 
@@ -128,9 +132,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
128
132
  | --- | --- |
129
133
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
130
134
  | 工作区脚手架 | `workspace` |
131
- | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
135
+ | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
132
136
  | 视频项目 | `remotion` |
133
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
137
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
138
+
139
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
134
140
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
135
141
 
136
142
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -28,7 +28,7 @@ chatup doctor
28
28
  chatup uv
29
29
  chatup chrome-for-testing install --channel stable -I
30
30
  chatup playwright install 1.61.1 -I
31
- chatup workspace default ~/Playground
31
+ chatup workspace ~/Playground
32
32
  ```
33
33
 
34
34
  `chatup uv` 默认更新已有 `~/.bashrc`、`~/.zshrc` 的自动激活块;使用 `chatup uv --no-activate` 可保持启动配置不变。不会创建缺失的 rc 文件,Windows 跳过此项。
@@ -37,6 +37,8 @@ chatup workspace default ~/Playground
37
37
 
38
38
  ```bash
39
39
  chatup gitea --force
40
+ chatup glance --dry-run
41
+ chatup glance
40
42
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
41
43
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
42
44
  chatup mysql
@@ -68,16 +70,17 @@ npm run studio
68
70
 
69
71
  需要已有 Node.js >=18.12 和 npm >=9。创建带锁文件的 Remotion 4.0.530 项目,检测本机 Chrome 并输出渲染命令;不覆盖已有无关目录,也不替换 Node 或下载浏览器。项目放在指定目录,已有 Blender/Chrome 可通过 `chatup macos --app blender --app chrome` 验证或安装。详见[Remotion 安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#remotion)。
70
72
 
71
- ## Chrome 与 iTerm2
73
+ ## Chrome、Snipaste 与 iTerm2
72
74
 
73
75
  ```bash
74
76
  chatup chrome --dry-run
75
77
  chatup chrome # 按系统安装普通 Google Chrome
78
+ chatup snipaste # 在 macOS 或 Windows 安装 Snipaste
76
79
  chatup iterm # 安装 iTerm2,仅限 macOS
77
80
  chatup chrome --sudo --yes # Linux:允许提权并确认包安装
78
81
  ```
79
82
 
80
- macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet `Google.Chrome`;Linux x86_64 使用官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
83
+ macOS 直接下载官方 DMG/ZIP 并校验开发者签名,无需 Homebrew;默认装到 `/Applications`,不可写时使用 `~/Applications`。Windows 使用 WinGet 精确包 `Google.Chrome.EXE`(当前用户安装)和 `liule.Snipaste`,需以 `--yes` 接受首次协议,Chrome 还会验证已安装程序的 Google 签名、产品身份和版本;Linux x86_64 支持 Chrome 的官方 DEB/RPM 与 apt/dnf/yum/zypper。已安装时验证并复用,不改默认浏览器、终端或用户配置。自动化浏览器仍使用 `chatup chrome-for-testing`。详见[安装约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chrome-iterm)。
81
84
 
82
85
  ## ChatGPT / Codex 桌面应用
83
86
 
@@ -97,9 +100,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
97
100
  | --- | --- |
98
101
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
99
102
  | 工作区脚手架 | `workspace` |
100
- | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
103
+ | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
101
104
  | 视频项目 | `remotion` |
102
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
105
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
106
+
107
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
103
108
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
104
109
 
105
110
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "click>=8.0,<9.0",
14
14
  "chatstyle[tui]>=0.2.0,<0.3.0",
15
15
  "chatenv>=0.2.11,<0.3.0",
16
+ "ChatGlance>=0.3.0,<0.4.0",
16
17
  "python-dotenv>=1.0,<2.0",
17
18
  "colorama",
18
19
  ]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.20"
5
+ __version__ = "0.2.22"
@@ -6,7 +6,7 @@ from chatstyle import add_tree_option
6
6
  from chatup import __version__
7
7
  from chatup.commands.chrome_for_testing import cli as chrome_for_testing_cli
8
8
  from chatup.commands.chromedriver import cli as chromedriver_cli
9
- from chatup.commands.desktop import chrome_cli, iterm_cli
9
+ from chatup.commands.desktop import chrome_cli, iterm_cli, snipaste_cli
10
10
  from chatup.commands.macos import macos_cli
11
11
  from chatup.commands.playwright import cli as playwright_cli
12
12
  from chatup.commands.remotion import remotion_cli
@@ -36,6 +36,7 @@ main.add_command(chrome_for_testing_cli)
36
36
  main.add_command(chromedriver_cli)
37
37
  main.add_command(playwright_cli)
38
38
  main.add_command(chrome_cli)
39
+ main.add_command(snipaste_cli)
39
40
  main.add_command(iterm_cli)
40
41
  main.add_command(macos_cli)
41
42
  main.add_command(remotion_cli)
@@ -44,6 +44,15 @@ def chrome_cli(**kwargs) -> None:
44
44
  _install("chrome", **kwargs)
45
45
 
46
46
 
47
+ @click.command(name="snipaste")
48
+ @click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
49
+ @click.option("--yes", "-y", is_flag=True, help="Accept Windows agreements.")
50
+ @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
51
+ def snipaste_cli(**kwargs) -> None:
52
+ """Install Snipaste for macOS or Windows."""
53
+ _install("snipaste", **kwargs)
54
+
55
+
47
56
  @click.command(name="iterm")
48
57
  @click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
49
58
  @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
@@ -79,7 +79,7 @@ def safe_extract_zip(
79
79
  f"Symlink escapes {label} archive root: {member.filename}"
80
80
  )
81
81
  target.parent.mkdir(parents=True, exist_ok=True)
82
- target.symlink_to(link_value)
82
+ target.symlink_to(link_value, target_is_directory=resolved_link.is_dir())
83
83
  continue
84
84
  if member.is_dir() or stat.S_ISDIR(mode):
85
85
  target.mkdir(parents=True, exist_ok=True)
@@ -1,8 +1,10 @@
1
- """Native installers for Google Chrome and supported macOS desktop apps."""
1
+ """Native installers for Chrome, Snipaste and supported macOS desktop apps."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
5
5
  import hashlib
6
+ import base64
7
+ import json
6
8
  import os
7
9
  from pathlib import Path
8
10
  import platform
@@ -22,6 +24,8 @@ logger = logging.getLogger(__name__)
22
24
  MAC_APPS = {
23
25
  "chrome": {
24
26
  "app": "Google Chrome",
27
+ "winget_id": "Google.Chrome.EXE",
28
+ "winget_scope": "user",
25
29
  "bundle": "Google Chrome.app",
26
30
  "bundle_id": "com.google.Chrome",
27
31
  "team_id": "EQHXZ8M8AV",
@@ -38,6 +42,7 @@ MAC_APPS = {
38
42
  },
39
43
  "snipaste": {
40
44
  "app": "Snipaste",
45
+ "winget_id": "liule.Snipaste",
41
46
  "bundle": "Snipaste.app",
42
47
  "bundle_id": "com.Snipaste",
43
48
  "team_id": "NGTL73P583",
@@ -73,15 +78,15 @@ def _is_root() -> bool:
73
78
  return hasattr(os, "geteuid") and os.geteuid() == 0
74
79
 
75
80
 
76
- def plan_desktop_install(
77
- app: str, *, sudo: bool = False, yes: bool = False,
78
- ) -> dict[str, Any]:
81
+ def plan_desktop_install(app: str, *, sudo: bool = False, yes: bool = False) -> dict[str, Any]:
79
82
  """Describe the current platform's installation without writes or processes."""
80
83
  spec = MAC_APPS[app]
81
84
  system = platform.system()
82
85
  plan = {"app": spec["app"], "platform": system}
83
- if app != "chrome" and system != "Darwin":
86
+ if system != "Darwin" and app in {"iterm", "blender"}:
84
87
  raise RuntimeError(f"{spec['app']} is supported on macOS only; current platform: {system}.")
88
+ if system not in {"Darwin", "Windows"} and app == "snipaste":
89
+ raise RuntimeError(f"{spec['app']} is supported on macOS and Windows only; current platform: {system}.")
85
90
  if system == "Darwin":
86
91
  if platform.machine().lower() not in {"arm64", "aarch64", "x86_64", "amd64"}:
87
92
  raise RuntimeError(f"Unsupported macOS architecture: {platform.machine()}.")
@@ -92,13 +97,24 @@ def plan_desktop_install(
92
97
  directory = directories[0] if os.access(directories[0], os.W_OK) else directories[1]
93
98
  return {**plan, **spec, "method": "macos", "path": str(existing or directory / spec["bundle"])}
94
99
  if system == "Windows":
95
- selector = ["--id", "Google.Chrome", "--exact", "--source", "winget", "--disable-interactivity"]
100
+ package_id = spec.get("winget_id")
101
+ if not package_id:
102
+ raise RuntimeError(f"{spec['app']} is supported on macOS only; current platform: {system}.")
103
+ selector = ["--id", package_id, "--exact", "--source", "winget", "--disable-interactivity"]
96
104
  command = ["winget", "install", *selector, "--silent", "--no-upgrade"]
105
+ if spec.get("winget_scope"):
106
+ command += ["--scope", spec["winget_scope"]]
97
107
  verify = ["winget", "list", *selector]
98
108
  if yes:
99
109
  command += ["--accept-package-agreements", "--accept-source-agreements"]
100
110
  verify += ["--accept-source-agreements"]
101
- return {**plan, "method": "winget", "command": command, "verify_command": verify}
111
+ return {
112
+ **plan,
113
+ "method": "winget",
114
+ "winget_id": package_id,
115
+ "command": command,
116
+ "verify_command": verify,
117
+ }
102
118
  if system == "Linux":
103
119
  if platform.machine().lower() not in {"x86_64", "amd64"}:
104
120
  raise RuntimeError("Google Chrome for Linux requires x86_64; Google does not provide a Linux ARM package.")
@@ -274,22 +290,77 @@ def _install_linux(plan: dict[str, Any], *, sudo: bool) -> dict[str, Any]:
274
290
  return {**plan, **installed, "status": "installed", "verified": True}
275
291
 
276
292
 
293
+ def _google_signed_file(path: Path) -> dict[str, str]:
294
+ powershell = shutil.which("powershell.exe") or shutil.which("pwsh.exe")
295
+ if not powershell:
296
+ raise RuntimeError("PowerShell is required to verify Google's Authenticode signature.")
297
+ # Encode the script and quote its sole path literal, including apostrophes.
298
+ literal = str(path).replace("'", "''")
299
+ script = (
300
+ "$ErrorActionPreference = 'Stop'; "
301
+ "$ProgressPreference = 'SilentlyContinue'; "
302
+ "$env:PSModulePath = Join-Path $PSHOME 'Modules'; "
303
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); "
304
+ f"$file = Get-Item -LiteralPath '{literal}'; "
305
+ "$sig = Get-AuthenticodeSignature -LiteralPath $file.FullName; "
306
+ "$publisher = if ($sig.SignerCertificate) { "
307
+ "$sig.SignerCertificate.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) "
308
+ "} else { '' }; "
309
+ "@{status=[string]$sig.Status; publisher=$publisher; "
310
+ "product=$file.VersionInfo.ProductName; version=$file.VersionInfo.ProductVersion; "
311
+ "original=$file.VersionInfo.OriginalFilename} | ConvertTo-Json -Compress"
312
+ )
313
+ encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
314
+ result = _run([powershell, "-NoProfile", "-NonInteractive", "-OutputFormat", "Text",
315
+ "-EncodedCommand", encoded], timeout=120)
316
+ details = json.loads(result.stdout.lstrip("\ufeff"))
317
+ if not isinstance(details, dict) or details.get("status") != "Valid" or details.get("publisher") != "Google LLC":
318
+ raise RuntimeError(f"Google Authenticode signature verification failed: {path}.")
319
+ version = details.get("version", "")
320
+ if (details.get("product") != "Google Chrome"
321
+ or str(details.get("original", "")).lower() != "chrome.exe"
322
+ or not isinstance(version, str) or not re.fullmatch(r"\d+(?:\.\d+){3}", version)):
323
+ raise RuntimeError(f"Cannot verify installed Google Chrome identity/version: {path}.")
324
+ return {"binary": str(path), "path": str(path), "version": version}
325
+
326
+
327
+ def _windows_chrome() -> dict[str, str] | None:
328
+ for variable in ("LOCALAPPDATA", "PROGRAMW6432", "PROGRAMFILES", "PROGRAMFILES(X86)"):
329
+ root = os.environ.get(variable)
330
+ if root:
331
+ binary = Path(root) / "Google/Chrome/Application/chrome.exe"
332
+ if binary.exists():
333
+ return _google_signed_file(binary)
334
+ return None
335
+
336
+
277
337
  def _install_windows(plan: dict[str, Any]) -> dict[str, Any]:
338
+ is_chrome = plan["winget_id"] == "Google.Chrome.EXE"
339
+ existing = _windows_chrome() if is_chrome else None
340
+ if existing:
341
+ return {**plan, **existing, "status": "already_installed", "verified": True}
278
342
  executable = shutil.which("winget")
279
343
  if not executable:
280
- raise RuntimeError("WinGet is required to install Google Chrome: https://aka.ms/getwinget")
344
+ raise RuntimeError(f"WinGet is required to install {plan['app']}: https://aka.ms/getwinget")
281
345
 
282
346
  def installed():
283
347
  result = _run([executable, *plan["verify_command"][1:]], timeout=120, check=False)
284
- return result.returncode == 0 and re.search(r"\bGoogle\.Chrome\b", result.stdout) is not None
348
+ return result.returncode == 0 and re.search(
349
+ rf"(?<![\w.-]){re.escape(plan['winget_id'])}(?![\w.-])", result.stdout,
350
+ ) is not None
285
351
 
286
352
  if installed():
353
+ if is_chrome:
354
+ raise RuntimeError("Google Chrome package record exists, but signed chrome.exe was not found.")
287
355
  return {**plan, "status": "already_installed", "verified": True}
288
- logger.info("Installing Google Chrome with WinGet; use --yes to accept source/package agreements")
356
+ logger.info("Installing %s with WinGet; use --yes to accept source/package agreements", plan["app"])
289
357
  _run([executable, *plan["command"][1:]], capture=False)
290
358
  if not installed():
291
- raise RuntimeError("Google Chrome installation verification failed: no matching WinGet package record.")
292
- return {**plan, "status": "installed", "verified": True}
359
+ raise RuntimeError(f"{plan['app']} installation verification failed: no matching WinGet package record.")
360
+ details = _windows_chrome() if is_chrome else {}
361
+ if is_chrome and not details:
362
+ raise RuntimeError("Google Chrome installation verification failed: signed chrome.exe not found.")
363
+ return {**plan, **details, "status": "installed", "verified": True}
293
364
 
294
365
 
295
366
  def setup_desktop_app(
@@ -26,6 +26,7 @@ from chatup.setup.discourse import (
26
26
  setup_discourse,
27
27
  )
28
28
  from chatup.setup.frp import setup_frp
29
+ from chatup.setup.glance import DEFAULT_GLANCE_VERSION, setup_glance
29
30
  from chatup.setup.gitea import (
30
31
  DEFAULT_BASE_URL as DEFAULT_GITEA_BASE_URL,
31
32
  DEFAULT_DATABASE_BACKEND as DEFAULT_GITEA_DATABASE_BACKEND,
@@ -231,6 +232,15 @@ def gitea_setup(
231
232
  )
232
233
 
233
234
 
235
+ def glance_setup(version, runtime_home, dry_run, interactive):
236
+ return setup_glance(
237
+ version=version,
238
+ runtime_home=runtime_home,
239
+ dry_run=dry_run,
240
+ interactive=interactive,
241
+ )
242
+
243
+
234
244
  def discourse_setup(
235
245
  home,
236
246
  hostname,
@@ -612,6 +622,37 @@ SETUP_COMMAND_ELEMENTS = (
612
622
  ),
613
623
  ),
614
624
 
625
+ SetupCommandElement(
626
+ name="glance",
627
+ help="Install the verified maintained ChatArch Glance runtime without starting it.",
628
+ callback=glance_setup,
629
+ options=(
630
+ SetupOptionElement(
631
+ param_decls=("--version",),
632
+ kwargs={
633
+ "default": DEFAULT_GLANCE_VERSION,
634
+ "show_default": True,
635
+ "help": "Maintained release: latest or exact chatarch-vMAJOR.MINOR.PATCH.",
636
+ },
637
+ ),
638
+ SetupOptionElement(
639
+ param_decls=("--runtime-home",),
640
+ kwargs={
641
+ "default": None,
642
+ "type": click.Path(path_type=Path),
643
+ "help": "Runtime root; defaults to the active ChatArch home/glance.",
644
+ },
645
+ ),
646
+ SetupOptionElement(
647
+ param_decls=("--dry-run",),
648
+ kwargs={"is_flag": True, "help": "Show paths without network access or writes."},
649
+ ),
650
+ SetupOptionElement(
651
+ param_decls=("--interactive/--no-interactive", "-i/-I"),
652
+ kwargs={"default": None, "help": INTERACTIVE_OPTION_HELP},
653
+ ),
654
+ ),
655
+ ),
615
656
  SetupCommandElement(
616
657
  name="gitea",
617
658
  help="Install ChatArch Gitea and optionally write a local app.ini/user service.",
@@ -0,0 +1,240 @@
1
+ """Install the maintained ChatArch Glance release into a portable runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import hashlib
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import platform
11
+ import re
12
+ import shlex
13
+ import subprocess
14
+ import tempfile
15
+ import urllib.request
16
+
17
+ import click
18
+ from chatenv import get_paths
19
+ from chatglance.portable import initialize, install_verified_binary, runtime_home
20
+
21
+
22
+ DEFAULT_GLANCE_REPO = "ChatArch/glance"
23
+ DEFAULT_GLANCE_VERSION = "latest"
24
+ TAG_PATTERN = re.compile(r"chatarch-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")
25
+ SOURCE_SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class GlanceInstallation:
30
+ status: str
31
+ tag: str
32
+ version: str | None
33
+ binary: Path
34
+ config: Path
35
+ start_command: tuple[str, ...]
36
+
37
+
38
+ def normalize_release_tag(value: str) -> str:
39
+ tag = str(value).strip()
40
+ if not TAG_PATTERN.fullmatch(tag):
41
+ raise ValueError("Glance version must be an exact chatarch-vMAJOR.MINOR.PATCH tag")
42
+ return tag
43
+
44
+
45
+ def _require_supported_platform() -> None:
46
+ system = platform.system().lower()
47
+ machine = platform.machine().lower()
48
+ if system != "linux" or machine not in {"x86_64", "amd64"}:
49
+ raise ValueError("Maintained ChatArch/glance releases currently support Linux amd64 only")
50
+
51
+
52
+ def resolve_latest_release_tag(repo: str = DEFAULT_GLANCE_REPO) -> str:
53
+ url = f"https://api.github.com/repos/{repo}/releases/latest"
54
+ with urllib.request.urlopen(url, timeout=30) as response:
55
+ payload = json.loads(response.read().decode("utf-8"))
56
+ tag = payload.get("tag_name")
57
+ if not isinstance(tag, str):
58
+ raise ValueError(f"Latest {repo} release did not provide a tag")
59
+ return normalize_release_tag(tag)
60
+
61
+
62
+ def resolve_requested_tag(version: str, repo: str = DEFAULT_GLANCE_REPO) -> str:
63
+ value = str(version or DEFAULT_GLANCE_VERSION).strip()
64
+ return resolve_latest_release_tag(repo) if value == DEFAULT_GLANCE_VERSION else normalize_release_tag(value)
65
+
66
+
67
+ def release_asset_name(tag: str) -> str:
68
+ return f"glance-{normalize_release_tag(tag)}-linux-amd64.tar.gz"
69
+
70
+
71
+ def release_asset_url(repo: str, tag: str, name: str) -> str:
72
+ return f"https://github.com/{repo}/releases/download/{tag}/{name}"
73
+
74
+
75
+ def download_release_asset(url: str, destination: Path) -> None:
76
+ with urllib.request.urlopen(url, timeout=120) as response, destination.open("wb") as output:
77
+ status = getattr(response, "status", 200)
78
+ if status != 200:
79
+ raise ValueError(f"Glance release download failed with HTTP {status}")
80
+ while chunk := response.read(1024 * 1024):
81
+ output.write(chunk)
82
+
83
+
84
+ def _sha256(path: Path) -> str:
85
+ digest = hashlib.sha256()
86
+ with path.open("rb") as stream:
87
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
88
+ digest.update(chunk)
89
+ return digest.hexdigest()
90
+
91
+
92
+ def _parse_checksums(path: Path, expected_names: set[str]) -> dict[str, str]:
93
+ checksums: dict[str, str] = {}
94
+ for line in path.read_text(encoding="utf-8").splitlines():
95
+ match = re.fullmatch(r"([0-9a-f]{64}) ([A-Za-z0-9._+-]+)", line)
96
+ if not match or match.group(2) in checksums:
97
+ raise ValueError("Invalid SHA256SUMS manifest")
98
+ checksums[match.group(2)] = match.group(1)
99
+ if set(checksums) != expected_names:
100
+ raise ValueError("SHA256SUMS does not describe the exact expected release files")
101
+ return checksums
102
+
103
+
104
+ def _parse_buildinfo(path: Path) -> dict[str, str]:
105
+ result: dict[str, str] = {}
106
+ for line in path.read_text(encoding="utf-8").splitlines():
107
+ key, separator, value = line.partition("=")
108
+ if not separator or not key or key in result:
109
+ raise ValueError("Invalid BUILDINFO.txt manifest")
110
+ result[key] = value
111
+ expected = {"tag", "source_sha", "binary_version", "archive", "goos", "goarch", "cgo_enabled"}
112
+ if set(result) != expected:
113
+ raise ValueError("BUILDINFO.txt has unexpected or missing fields")
114
+ return result
115
+
116
+
117
+ def _verify_release_files(archive: Path, checksums_path: Path, buildinfo_path: Path, tag: str) -> tuple[str, str]:
118
+ checksums = _parse_checksums(checksums_path, {archive.name, buildinfo_path.name})
119
+ for path in (archive, buildinfo_path):
120
+ if _sha256(path) != checksums[path.name]:
121
+ raise ValueError(f"SHA256 mismatch for {path.name}")
122
+ info = _parse_buildinfo(buildinfo_path)
123
+ if info["tag"] != tag:
124
+ raise ValueError("BUILDINFO tag does not match requested release")
125
+ if not SOURCE_SHA_PATTERN.fullmatch(info["source_sha"]):
126
+ raise ValueError("BUILDINFO source_sha must be exactly 40 lowercase hex characters")
127
+ version = f"{tag}+{info['source_sha']}"
128
+ expected = {
129
+ "binary_version": version,
130
+ "archive": archive.name,
131
+ "goos": "linux",
132
+ "goarch": "amd64",
133
+ "cgo_enabled": "0",
134
+ }
135
+ for key, value in expected.items():
136
+ if info[key] != value:
137
+ raise ValueError(f"BUILDINFO {key} does not match the requested release")
138
+ return checksums[archive.name], version
139
+
140
+
141
+ def _read_observed_version(binary: Path) -> str:
142
+ try:
143
+ result = subprocess.run(
144
+ [str(binary), "--version"], capture_output=True, text=True, timeout=5,
145
+ check=True, env={"PATH": "/usr/bin:/bin"},
146
+ )
147
+ except (OSError, subprocess.SubprocessError, UnicodeError) as exc:
148
+ raise ValueError(f"Installed Glance version check failed ({type(exc).__name__})") from None
149
+ return result.stdout.strip()
150
+
151
+
152
+ def _existing_binary_matches(home: Path, archive_sha256: str, version: str) -> bool:
153
+ binary = home / "bin/glance"
154
+ provenance_path = home / "bin/glance.provenance.json"
155
+ if not (binary.exists() or binary.is_symlink() or provenance_path.exists() or provenance_path.is_symlink()):
156
+ return False
157
+ refusal = "Existing Glance runtime is not this exact verified release; use native `chatglance runtime update` for explicit changes"
158
+ if binary.is_symlink() or provenance_path.is_symlink() or not binary.is_file() or not provenance_path.is_file():
159
+ raise ValueError(refusal)
160
+ try:
161
+ provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
162
+ except (OSError, UnicodeError, json.JSONDecodeError):
163
+ raise ValueError(refusal) from None
164
+ tag, source_sha = version.split("+", 1)
165
+ expected = {
166
+ "source": "verified local archive",
167
+ "source_tag": tag,
168
+ "source_revision": source_sha,
169
+ "version": version,
170
+ "archive_sha256": archive_sha256,
171
+ "binary_sha256": _sha256(binary),
172
+ }
173
+ if provenance != expected or _read_observed_version(binary) != version:
174
+ raise ValueError(refusal)
175
+ return True
176
+
177
+
178
+ def install_glance(
179
+ *, version: str = DEFAULT_GLANCE_VERSION, home: str | Path | None = None,
180
+ repo: str = DEFAULT_GLANCE_REPO, dry_run: bool = False,
181
+ ) -> GlanceInstallation:
182
+ """Install or exactly reuse one verified maintained Go release, then initialize resources."""
183
+ _require_supported_platform()
184
+ root = runtime_home(home)
185
+ binary = root / "bin/glance"
186
+ config = root / "config/glance.yml"
187
+ requested = str(version or DEFAULT_GLANCE_VERSION).strip()
188
+ if dry_run:
189
+ tag = DEFAULT_GLANCE_VERSION if requested == DEFAULT_GLANCE_VERSION else normalize_release_tag(requested)
190
+ return GlanceInstallation("planned", tag, None, binary, config, ("chatglance", "runtime", "serve", "--runtime-home", str(root)))
191
+
192
+ tag = resolve_requested_tag(requested, repo)
193
+ archive_name = release_asset_name(tag)
194
+ names = (archive_name, "SHA256SUMS", "BUILDINFO.txt")
195
+ staging_parent = get_paths().home_dir / "chatup" / "staging"
196
+ staging_parent.mkdir(parents=True, mode=0o700, exist_ok=True)
197
+ if staging_parent.is_symlink() or not staging_parent.is_dir():
198
+ raise ValueError("Unsafe Glance download staging directory")
199
+ staging_parent.chmod(0o700)
200
+ with tempfile.TemporaryDirectory(prefix="glance-", dir=staging_parent) as temporary:
201
+ staging = Path(temporary)
202
+ for name in names:
203
+ download_release_asset(release_asset_url(repo, tag, name), staging / name)
204
+ archive = staging / archive_name
205
+ archive_sha256, raw_version = _verify_release_files(
206
+ archive, staging / "SHA256SUMS", staging / "BUILDINFO.txt", tag
207
+ )
208
+ reused = _existing_binary_matches(root, archive_sha256, raw_version)
209
+ if not reused:
210
+ install_verified_binary(archive, archive_sha256, root, version=raw_version)
211
+ initialize(root)
212
+ if not config.is_file() or config.is_symlink():
213
+ raise ValueError("ChatGlance initialization did not provide a safe runtime config")
214
+ return GlanceInstallation(
215
+ "reused" if reused else "installed", tag, raw_version, binary, config,
216
+ ("chatglance", "runtime", "serve", "--runtime-home", str(root)),
217
+ )
218
+
219
+
220
+ def setup_glance(
221
+ version: str = DEFAULT_GLANCE_VERSION,
222
+ runtime_home: str | Path | None = None,
223
+ dry_run: bool = False,
224
+ interactive=None,
225
+ ) -> GlanceInstallation:
226
+ """Click-facing setup wrapper; setup never starts or enables the server."""
227
+ del interactive
228
+ try:
229
+ result = install_glance(version=version, home=runtime_home, dry_run=dry_run)
230
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
231
+ raise click.ClickException(str(exc)) from exc
232
+ if result.status == "planned":
233
+ click.echo(f"Glance setup plan: {result.tag} -> {result.binary}")
234
+ else:
235
+ click.echo(f"Glance binary: {result.binary}")
236
+ click.echo(f"Glance config: {result.config}")
237
+ click.echo(f"Glance release: {result.version} ({result.status})")
238
+ click.echo("Native start command: " + shlex.join(result.start_command))
239
+ click.echo("The server was not started and no service or public endpoint was created.")
240
+ return result