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.
- {chatup-0.2.20/src/chatup.egg-info → chatup-0.2.22}/PKG-INFO +12 -6
- {chatup-0.2.20 → chatup-0.2.22}/README.md +10 -5
- {chatup-0.2.20 → chatup-0.2.22}/pyproject.toml +1 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/__init__.py +1 -1
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/cli.py +2 -1
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/desktop.py +9 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/_managed_artifact.py +1 -1
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/desktop.py +83 -12
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/elements.py +41 -0
- chatup-0.2.22/src/chatup/setup/glance.py +240 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/options.py +125 -6
- {chatup-0.2.20 → chatup-0.2.22/src/chatup.egg-info}/PKG-INFO +12 -6
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/SOURCES.txt +3 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/requires.txt +1 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_browser_backend_cli.py +3 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_chrome_for_testing.py +18 -3
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_chromedriver.py +2 -2
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_cli_tree_docs.py +1 -0
- chatup-0.2.22/tests/test_glance_bridge.py +6 -0
- chatup-0.2.22/tests/test_glance_setup.py +190 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_playwright.py +1 -1
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_pyproject_metadata.py +1 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_setup_cli.py +2 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_version.py +1 -1
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_workspace_setup.py +126 -10
- {chatup-0.2.20 → chatup-0.2.22}/LICENSE +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/setup.cfg +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/macos.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/commands/remotion.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/config.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/const.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/playwright.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/.gitignore +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/package.json +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/chatgpt.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/codex.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/hermes.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/remotion.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_chatgpt_setup.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_codex_setup.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_hermes_model_defaults.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_service_installers.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_uv_activation.py +0 -0
- {chatup-0.2.20 → chatup-0.2.22}/tests/test_windows_compat.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`。
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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("
|
|
292
|
-
|
|
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
|