chatup 0.2.15__tar.gz → 0.2.17__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.15/src/chatup.egg-info → chatup-0.2.17}/PKG-INFO +28 -2
- {chatup-0.2.15 → chatup-0.2.17}/README.md +27 -1
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/__init__.py +1 -1
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/cli.py +3 -0
- chatup-0.2.17/src/chatup/commands/desktop.py +50 -0
- chatup-0.2.17/src/chatup/setup/chatgpt.py +132 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/codex.py +1 -1
- chatup-0.2.17/src/chatup/setup/desktop.py +279 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/elements.py +53 -6
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/hermes.py +1 -1
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/uv.py +89 -1
- {chatup-0.2.15 → chatup-0.2.17/src/chatup.egg-info}/PKG-INFO +28 -2
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup.egg-info/SOURCES.txt +6 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_browser_backend_cli.py +3 -1
- chatup-0.2.17/tests/test_chatgpt_setup.py +183 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_cli_tree_docs.py +2 -1
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_codex_setup.py +85 -3
- chatup-0.2.17/tests/test_hermes_model_defaults.py +50 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_setup_cli.py +6 -0
- chatup-0.2.17/tests/test_uv_activation.py +259 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_version.py +1 -1
- {chatup-0.2.15 → chatup-0.2.17}/LICENSE +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/pyproject.toml +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/setup.cfg +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/config.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/const.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/playwright.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/runtime/playwright.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/claude.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/cursor_agent.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/frp.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/lark_cli.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/nodejs.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup.egg-info/requires.txt +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_playwright.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_pyproject_metadata.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_service_installers.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_windows_compat.py +0 -0
- {chatup-0.2.15 → chatup-0.2.17}/tests/test_workspace_setup.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.17
|
|
4
4
|
Summary: ChatArch setup CLI extracted from ChatTool
|
|
5
5
|
Author-email: rexwzh <1073853456@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,6 +62,8 @@ chatup playwright install 1.61.1 -I
|
|
|
62
62
|
chatup workspace default ~/Playground
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
`chatup uv` 默认更新已有 `~/.bashrc`、`~/.zshrc` 的自动激活块;使用 `chatup uv --no-activate` 可保持启动配置不变。不会创建缺失的 rc 文件,Windows 跳过此项。
|
|
66
|
+
|
|
65
67
|
常见安装命令:
|
|
66
68
|
|
|
67
69
|
```bash
|
|
@@ -74,16 +76,40 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
|
|
|
74
76
|
chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
## Chrome 与 iTerm2
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
chatup chrome --dry-run
|
|
83
|
+
chatup chrome # 按系统安装普通 Google Chrome
|
|
84
|
+
chatup iterm # 安装 iTerm2,仅限 macOS
|
|
85
|
+
chatup chrome --sudo --yes # Linux:允许提权并确认包安装
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
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)。
|
|
89
|
+
|
|
90
|
+
## ChatGPT / Codex 桌面应用
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
chatup chatgpt --dry-run
|
|
94
|
+
chatup chatgpt
|
|
95
|
+
chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
安装包含 Codex 的新版官方 ChatGPT 桌面应用。macOS 需要 Homebrew,Windows 需要 WinGet;Linux preview 暂按[官方说明](https://learn.chatgpt.com/docs/linux/linux-app)手动安装。现有 `chatup codex` 仍安装/配置 Codex CLI。不会自动启动、登录或升级已有应用;[完整约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chatgpt-desktop)。
|
|
99
|
+
|
|
100
|
+
`chatup codex`、`chatup hermes` 未配置模型时默认使用 `gpt-5.6-terra`;用户指定的模型和已有配置仍优先。
|
|
101
|
+
|
|
77
102
|
## 当前能力
|
|
78
103
|
|
|
79
104
|
| 能力组 | 命令 |
|
|
80
105
|
| --- | --- |
|
|
81
106
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
82
107
|
| 工作区脚手架 | `workspace` |
|
|
108
|
+
| 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
|
|
83
109
|
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
84
110
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
85
111
|
|
|
86
|
-
|
|
112
|
+
桌面应用使用系统原生应用目录;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`。
|
|
87
113
|
|
|
88
114
|
## 开发
|
|
89
115
|
|
|
@@ -31,6 +31,8 @@ chatup playwright install 1.61.1 -I
|
|
|
31
31
|
chatup workspace default ~/Playground
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
`chatup uv` 默认更新已有 `~/.bashrc`、`~/.zshrc` 的自动激活块;使用 `chatup uv --no-activate` 可保持启动配置不变。不会创建缺失的 rc 文件,Windows 跳过此项。
|
|
35
|
+
|
|
34
36
|
常见安装命令:
|
|
35
37
|
|
|
36
38
|
```bash
|
|
@@ -43,16 +45,40 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
|
|
|
43
45
|
chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
## Chrome 与 iTerm2
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
chatup chrome --dry-run
|
|
52
|
+
chatup chrome # 按系统安装普通 Google Chrome
|
|
53
|
+
chatup iterm # 安装 iTerm2,仅限 macOS
|
|
54
|
+
chatup chrome --sudo --yes # Linux:允许提权并确认包安装
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
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)。
|
|
58
|
+
|
|
59
|
+
## ChatGPT / Codex 桌面应用
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
chatup chatgpt --dry-run
|
|
63
|
+
chatup chatgpt
|
|
64
|
+
chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
安装包含 Codex 的新版官方 ChatGPT 桌面应用。macOS 需要 Homebrew,Windows 需要 WinGet;Linux preview 暂按[官方说明](https://learn.chatgpt.com/docs/linux/linux-app)手动安装。现有 `chatup codex` 仍安装/配置 Codex CLI。不会自动启动、登录或升级已有应用;[完整约定](https://arch.gh.wzhecnu.cn/ChatUp/commands/#chatgpt-desktop)。
|
|
68
|
+
|
|
69
|
+
`chatup codex`、`chatup hermes` 未配置模型时默认使用 `gpt-5.6-terra`;用户指定的模型和已有配置仍优先。
|
|
70
|
+
|
|
46
71
|
## 当前能力
|
|
47
72
|
|
|
48
73
|
| 能力组 | 命令 |
|
|
49
74
|
| --- | --- |
|
|
50
75
|
| 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
|
|
51
76
|
| 工作区脚手架 | `workspace` |
|
|
77
|
+
| 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
|
|
52
78
|
| 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
|
|
53
79
|
| Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
|
|
54
80
|
|
|
55
|
-
|
|
81
|
+
桌面应用使用系统原生应用目录;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`。
|
|
56
82
|
|
|
57
83
|
## 开发
|
|
58
84
|
|
|
@@ -6,6 +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
10
|
from chatup.commands.playwright import cli as playwright_cli
|
|
10
11
|
from chatup.setup.cli import register_setup_commands
|
|
11
12
|
|
|
@@ -32,6 +33,8 @@ register_setup_commands(main)
|
|
|
32
33
|
main.add_command(chrome_for_testing_cli)
|
|
33
34
|
main.add_command(chromedriver_cli)
|
|
34
35
|
main.add_command(playwright_cli)
|
|
36
|
+
main.add_command(chrome_cli)
|
|
37
|
+
main.add_command(iterm_cli)
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI entry points for native desktop app installation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _install(app: str, **kwargs) -> None:
|
|
10
|
+
from chatup.setup.desktop import setup_desktop_app
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
result = setup_desktop_app(app, **kwargs)
|
|
14
|
+
except RuntimeError as exc:
|
|
15
|
+
raise click.ClickException(str(exc)) from exc
|
|
16
|
+
if result["status"] == "planned":
|
|
17
|
+
click.echo(f"{result['app']} install plan ({result['platform']}):")
|
|
18
|
+
if "url" in result:
|
|
19
|
+
click.echo(f"Download: {result['url']}")
|
|
20
|
+
if "path" in result:
|
|
21
|
+
click.echo(f"Install to: {result['path']} (verify publisher signature)")
|
|
22
|
+
if "command" in result:
|
|
23
|
+
click.echo(shlex.join(result["command"]))
|
|
24
|
+
click.echo("Dry run: no download or installation performed.")
|
|
25
|
+
return
|
|
26
|
+
status = "Already installed" if result["status"] == "already_installed" else "Installed"
|
|
27
|
+
click.echo(f"{status}: {result['app']} {result.get('version', '')}".strip())
|
|
28
|
+
if "path" in result:
|
|
29
|
+
click.echo(result["path"])
|
|
30
|
+
elif "binary" in result:
|
|
31
|
+
click.echo(result["binary"])
|
|
32
|
+
click.echo("Installation verified.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command(name="chrome")
|
|
36
|
+
@click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
|
|
37
|
+
@click.option("--sudo", is_flag=True, help="Allow sudo for Linux system package installation.")
|
|
38
|
+
@click.option("--yes", "-y", is_flag=True, help="Accept Windows agreements or Linux package-manager prompts.")
|
|
39
|
+
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
|
|
40
|
+
def chrome_cli(**kwargs) -> None:
|
|
41
|
+
"""Install regular Google Chrome for macOS, Windows or Linux."""
|
|
42
|
+
_install("chrome", **kwargs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command(name="iterm")
|
|
46
|
+
@click.option("--dry-run", is_flag=True, help="Show the installation plan without downloading or installing.")
|
|
47
|
+
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), default="INFO")
|
|
48
|
+
def iterm_cli(**kwargs) -> None:
|
|
49
|
+
"""Install iTerm2 from the official download (macOS only)."""
|
|
50
|
+
_install("iterm", **kwargs)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Install the official ChatGPT desktop app (the successor to Codex App).
|
|
2
|
+
|
|
3
|
+
The OS package manager owns downloads, signatures, application paths and state.
|
|
4
|
+
This module never launches the app or changes Codex CLI configuration/auth.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
CHATGPT_CASK = "homebrew/cask/chatgpt"
|
|
14
|
+
CHATGPT_STORE_ID = "9PLM9XGG6VKS"
|
|
15
|
+
DOWNLOAD_URL = "https://chatgpt.com/download/"
|
|
16
|
+
LINUX_GUIDE_URL = "https://learn.chatgpt.com/docs/linux/linux-app"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def plan_chatgpt_install(*, yes: bool = False) -> dict[str, Any]:
|
|
20
|
+
"""Return a side-effect-free install plan for the current OS.
|
|
21
|
+
|
|
22
|
+
``yes`` explicitly accepts Microsoft Store source/package agreements.
|
|
23
|
+
Homebrew/WinGet resolve the current release and enforce OS/CPU requirements.
|
|
24
|
+
Linux preview installation is deliberately not automated here.
|
|
25
|
+
"""
|
|
26
|
+
system = platform.system()
|
|
27
|
+
if system == "Darwin":
|
|
28
|
+
manager = "brew"
|
|
29
|
+
package = CHATGPT_CASK
|
|
30
|
+
command = [manager, "install", "--cask", package]
|
|
31
|
+
verify_command = [manager, "list", "--cask", "--versions", package]
|
|
32
|
+
manager_url = "https://brew.sh/"
|
|
33
|
+
elif system == "Windows":
|
|
34
|
+
manager = "winget"
|
|
35
|
+
package = CHATGPT_STORE_ID
|
|
36
|
+
selector = ["--id", package, "--exact", "--source", "msstore", "--disable-interactivity"]
|
|
37
|
+
command = [manager, "install", *selector, "--no-upgrade"]
|
|
38
|
+
verify_command = [manager, "list", *selector]
|
|
39
|
+
if yes:
|
|
40
|
+
command += ["--accept-package-agreements", "--accept-source-agreements"]
|
|
41
|
+
verify_command += ["--accept-source-agreements"]
|
|
42
|
+
manager_url = "https://aka.ms/getwinget"
|
|
43
|
+
else:
|
|
44
|
+
guide = LINUX_GUIDE_URL if system == "Linux" else DOWNLOAD_URL
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"ChatUp automates ChatGPT desktop installation on macOS and Windows only; "
|
|
47
|
+
f"current platform: {system}. Official downloads/instructions: {guide}"
|
|
48
|
+
)
|
|
49
|
+
return {
|
|
50
|
+
"app": "ChatGPT (includes Codex)",
|
|
51
|
+
"platform": system,
|
|
52
|
+
"manager": manager,
|
|
53
|
+
"manager_url": manager_url,
|
|
54
|
+
"package": package,
|
|
55
|
+
"command": command,
|
|
56
|
+
"verify_command": verify_command,
|
|
57
|
+
"download_url": DOWNLOAD_URL,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_installed(plan: dict[str, Any], executable: str) -> bool:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
[executable, *plan["verify_command"][1:]],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
encoding="utf-8",
|
|
67
|
+
errors="replace",
|
|
68
|
+
check=False,
|
|
69
|
+
timeout=60,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
72
|
+
return False
|
|
73
|
+
if plan["manager"] == "brew":
|
|
74
|
+
for line in result.stdout.splitlines():
|
|
75
|
+
parts = line.split()
|
|
76
|
+
if len(parts) < 2 or parts[0] != "chatgpt":
|
|
77
|
+
continue
|
|
78
|
+
if any(version.startswith("1.") for version in parts[1:]):
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"An older ChatGPT Classic cask is registered as chatgpt. "
|
|
81
|
+
"Migrate it manually with Homebrew before installing the new "
|
|
82
|
+
f"ChatGPT/Codex app: {DOWNLOAD_URL}"
|
|
83
|
+
)
|
|
84
|
+
return True
|
|
85
|
+
return False
|
|
86
|
+
# WinGet's exact Store selector exits zero only when a matching package exists.
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def setup_chatgpt(*, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
|
|
91
|
+
"""Install if absent, then verify registration with the package manager.
|
|
92
|
+
|
|
93
|
+
Returns the plan plus ``status`` (planned/already_installed/installed) and
|
|
94
|
+
``verified``. Dry-run does not require or invoke a package manager. Existing
|
|
95
|
+
installations are not upgraded, overwritten or migrated; authentication and
|
|
96
|
+
launching remain user actions. Raises RuntimeError on failed/unknown results.
|
|
97
|
+
"""
|
|
98
|
+
plan = plan_chatgpt_install(yes=yes)
|
|
99
|
+
result = {**plan, "status": "planned", "verified": False}
|
|
100
|
+
if dry_run:
|
|
101
|
+
return result
|
|
102
|
+
executable = shutil.which(plan["manager"])
|
|
103
|
+
if not executable:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"Required package manager {plan['manager']} is not on PATH. "
|
|
106
|
+
f"Install it first: {plan['manager_url']} or download manually: {DOWNLOAD_URL}"
|
|
107
|
+
)
|
|
108
|
+
try:
|
|
109
|
+
if _is_installed(plan, executable):
|
|
110
|
+
return {**result, "status": "already_installed", "verified": True}
|
|
111
|
+
installed = subprocess.run(
|
|
112
|
+
[executable, *plan["command"][1:]], check=False, timeout=1800,
|
|
113
|
+
)
|
|
114
|
+
if installed.returncode != 0:
|
|
115
|
+
hint = " Use --yes to accept Store agreements." if plan["manager"] == "winget" and not yes else ""
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"ChatGPT installer failed (exit {installed.returncode}). "
|
|
118
|
+
f"Check the package manager output above.{hint}"
|
|
119
|
+
)
|
|
120
|
+
if not _is_installed(plan, executable):
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"ChatGPT installation verification failed. Check the package manager "
|
|
123
|
+
"state before retrying; installation success is not confirmed."
|
|
124
|
+
)
|
|
125
|
+
except subprocess.TimeoutExpired as exc:
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
"ChatGPT package manager timed out; the installation state is unknown. "
|
|
128
|
+
"Check the package manager before retrying."
|
|
129
|
+
) from exc
|
|
130
|
+
except OSError as exc:
|
|
131
|
+
raise RuntimeError(f"Cannot execute {plan['manager']}: {exc}") from exc
|
|
132
|
+
return {**result, "status": "installed", "verified": True}
|
|
@@ -30,7 +30,7 @@ from chatup.setup.nodejs import (
|
|
|
30
30
|
from chatup.utils.custom_logger import setup_logger
|
|
31
31
|
from chatup.utils.platforming import chmod_private
|
|
32
32
|
|
|
33
|
-
DEFAULT_MODEL = "gpt-5.
|
|
33
|
+
DEFAULT_MODEL = "gpt-5.6-terra"
|
|
34
34
|
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
|
35
35
|
DEFAULT_AUTH_METHOD = "apikey"
|
|
36
36
|
_ENV_VAR_REF_RE = re.compile(r"\$(?:\{[^}]+\}|[A-Za-z_][A-Za-z0-9_]*)")
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Native installers for regular Google Chrome and the macOS iTerm2 app."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import platform
|
|
8
|
+
import plistlib
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import Any
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
from chatup.runtime._managed_artifact import safe_extract_zip
|
|
17
|
+
from chatup.utils.custom_logger import setup_logger
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAC_APPS = {
|
|
22
|
+
"chrome": {
|
|
23
|
+
"app": "Google Chrome",
|
|
24
|
+
"bundle": "Google Chrome.app",
|
|
25
|
+
"bundle_id": "com.google.Chrome",
|
|
26
|
+
"team_id": "EQHXZ8M8AV",
|
|
27
|
+
"url": "https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg",
|
|
28
|
+
"format": "dmg",
|
|
29
|
+
},
|
|
30
|
+
"iterm": {
|
|
31
|
+
"app": "iTerm2",
|
|
32
|
+
"bundle": "iTerm.app",
|
|
33
|
+
"bundle_id": "com.googlecode.iterm2",
|
|
34
|
+
"team_id": "H7V7XYVQ7D",
|
|
35
|
+
"url": "https://iterm2.com/downloads/stable/latest",
|
|
36
|
+
"format": "zip",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
LINUX_PACKAGES = {
|
|
40
|
+
"apt-get": ("deb", ["install"]),
|
|
41
|
+
"dnf": ("rpm", ["install"]),
|
|
42
|
+
"yum": ("rpm", ["install"]),
|
|
43
|
+
"zypper": ("rpm", ["install"]),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _mac_app_dirs() -> tuple[Path, Path]:
|
|
48
|
+
return Path("/Applications"), Path.home() / "Applications"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_root() -> bool:
|
|
52
|
+
return hasattr(os, "geteuid") and os.geteuid() == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plan_desktop_install(
|
|
56
|
+
app: str, *, sudo: bool = False, yes: bool = False,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Describe the current platform's installation without writes or processes."""
|
|
59
|
+
spec = MAC_APPS[app]
|
|
60
|
+
system = platform.system()
|
|
61
|
+
plan = {"app": spec["app"], "platform": system}
|
|
62
|
+
if app == "iterm" and system != "Darwin":
|
|
63
|
+
raise RuntimeError(f"iTerm2 is supported on macOS only; current platform: {system}.")
|
|
64
|
+
if system == "Darwin":
|
|
65
|
+
if platform.machine().lower() not in {"arm64", "aarch64", "x86_64", "amd64"}:
|
|
66
|
+
raise RuntimeError(f"Unsupported macOS architecture: {platform.machine()}.")
|
|
67
|
+
directories = _mac_app_dirs()
|
|
68
|
+
existing = next((d / spec["bundle"] for d in directories if (d / spec["bundle"]).exists()), None)
|
|
69
|
+
directory = directories[0] if os.access(directories[0], os.W_OK) else directories[1]
|
|
70
|
+
return {**plan, **spec, "method": "macos", "path": str(existing or directory / spec["bundle"])}
|
|
71
|
+
if system == "Windows":
|
|
72
|
+
selector = ["--id", "Google.Chrome", "--exact", "--source", "winget", "--disable-interactivity"]
|
|
73
|
+
command = ["winget", "install", *selector, "--silent", "--no-upgrade"]
|
|
74
|
+
verify = ["winget", "list", *selector]
|
|
75
|
+
if yes:
|
|
76
|
+
command += ["--accept-package-agreements", "--accept-source-agreements"]
|
|
77
|
+
verify += ["--accept-source-agreements"]
|
|
78
|
+
return {**plan, "method": "winget", "command": command, "verify_command": verify}
|
|
79
|
+
if system == "Linux":
|
|
80
|
+
if platform.machine().lower() not in {"x86_64", "amd64"}:
|
|
81
|
+
raise RuntimeError("Google Chrome for Linux requires x86_64; Google does not provide a Linux ARM package.")
|
|
82
|
+
manager = next((name for name in LINUX_PACKAGES if shutil.which(name)), None)
|
|
83
|
+
if not manager:
|
|
84
|
+
raise RuntimeError("Google Chrome requires apt-get, dnf, yum or zypper on Linux. See https://www.google.com/chrome/.")
|
|
85
|
+
extension, action = LINUX_PACKAGES[manager]
|
|
86
|
+
filename = f"google-chrome-stable_current_{'amd64' if extension == 'deb' else 'x86_64'}.{extension}"
|
|
87
|
+
command = [manager]
|
|
88
|
+
if manager == "zypper" and yes:
|
|
89
|
+
command += ["--non-interactive"]
|
|
90
|
+
command += action
|
|
91
|
+
if manager != "zypper" and yes:
|
|
92
|
+
command += ["-y"]
|
|
93
|
+
command += ["{package}"]
|
|
94
|
+
if sudo and not _is_root():
|
|
95
|
+
command.insert(0, "sudo")
|
|
96
|
+
return {**plan, "method": "linux", "manager": manager, "command": command,
|
|
97
|
+
"url": f"https://dl.google.com/linux/direct/{filename}", "filename": filename}
|
|
98
|
+
raise RuntimeError(f"Google Chrome installation is not supported on {system}.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run(command: list[str], *, capture: bool = True, timeout: int = 1800, check: bool = True):
|
|
102
|
+
logger.debug("Running: %s", command)
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
command, capture_output=capture, text=True, encoding="utf-8", errors="replace",
|
|
105
|
+
timeout=timeout, check=False,
|
|
106
|
+
)
|
|
107
|
+
if check and result.returncode:
|
|
108
|
+
detail = (result.stderr or result.stdout or "See installer output.").strip()
|
|
109
|
+
raise RuntimeError(f"{Path(command[0]).name} failed (exit {result.returncode}): {detail[-2000:]}")
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _download(url: str, destination: Path) -> None:
|
|
114
|
+
logger.info("Downloading %s", url)
|
|
115
|
+
request = urllib.request.Request(url, headers={"User-Agent": "ChatUp desktop installer"})
|
|
116
|
+
with urllib.request.urlopen(request, timeout=120) as response, destination.open("wb") as output:
|
|
117
|
+
if not response.url.startswith("https://"):
|
|
118
|
+
raise RuntimeError("Refusing a download redirected away from HTTPS.")
|
|
119
|
+
total = 0
|
|
120
|
+
expected = int(response.headers.get("Content-Length", "0"))
|
|
121
|
+
limit = 1024 * 1024 * 1024
|
|
122
|
+
if expected > limit:
|
|
123
|
+
raise RuntimeError("Desktop installer exceeds the 1 GiB download limit.")
|
|
124
|
+
for chunk in iter(lambda: response.read(1024 * 1024), b""):
|
|
125
|
+
total += len(chunk)
|
|
126
|
+
if total > limit:
|
|
127
|
+
raise RuntimeError("Desktop installer exceeds the 1 GiB download limit.")
|
|
128
|
+
output.write(chunk)
|
|
129
|
+
if not total or (expected and total != expected):
|
|
130
|
+
raise RuntimeError("Desktop installer download is incomplete; retry the command.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cache_dir() -> Path:
|
|
134
|
+
home = Path(os.environ.get("CHATARCH_HOME", str(Path.home() / ".chatarch"))).expanduser()
|
|
135
|
+
cache = home / "cache" / "desktop"
|
|
136
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
return cache
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _mac_bundle(plan: dict[str, Any], path: Path) -> dict[str, str]:
|
|
141
|
+
try:
|
|
142
|
+
with (path / "Contents" / "Info.plist").open("rb") as stream:
|
|
143
|
+
info = plistlib.load(stream)
|
|
144
|
+
executable_name = info.get("CFBundleExecutable", "")
|
|
145
|
+
binary = path / "Contents" / "MacOS" / executable_name
|
|
146
|
+
version = info.get("CFBundleShortVersionString", "")
|
|
147
|
+
if (info.get("CFBundleIdentifier") != plan["bundle_id"] or not version
|
|
148
|
+
or not executable_name or Path(executable_name).name != executable_name
|
|
149
|
+
or not binary.is_file() or not os.access(binary, os.X_OK)):
|
|
150
|
+
raise ValueError("bundle identity, version or executable is invalid")
|
|
151
|
+
minimum = info.get("LSMinimumSystemVersion", "")
|
|
152
|
+
current = platform.mac_ver()[0]
|
|
153
|
+
if minimum and current:
|
|
154
|
+
def version_tuple(value):
|
|
155
|
+
return tuple(int(part) for part in value.split(".")) + (0,) * (3 - len(value.split(".")))
|
|
156
|
+
if version_tuple(current) < version_tuple(minimum):
|
|
157
|
+
raise ValueError(f"requires macOS {minimum} or later; current: {current}")
|
|
158
|
+
except (OSError, ValueError, plistlib.InvalidFileException) as exc:
|
|
159
|
+
raise RuntimeError(f"Cannot verify {plan['app']} at {path}: {exc}. Existing apps are never overwritten.") from exc
|
|
160
|
+
requirement = f'=anchor apple generic and certificate leaf[subject.OU] = "{plan["team_id"]}"'
|
|
161
|
+
_run(["/usr/bin/codesign", "--verify", "--deep", "--strict", "-R", requirement, str(path)], timeout=120)
|
|
162
|
+
return {"path": str(path), "binary": str(binary), "version": str(version)}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _install_macos(plan: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
target = Path(plan["path"])
|
|
167
|
+
if target.exists() or target.is_symlink():
|
|
168
|
+
logger.info("Checking existing %s", target)
|
|
169
|
+
return {**plan, **_mac_bundle(plan, target), "status": "already_installed", "verified": True}
|
|
170
|
+
work = Path(tempfile.mkdtemp(prefix="install-", dir=_cache_dir()))
|
|
171
|
+
mount = work / "volume"
|
|
172
|
+
mounted = False
|
|
173
|
+
try:
|
|
174
|
+
archive = work / f"download.{plan['format']}"
|
|
175
|
+
_download(plan["url"], archive)
|
|
176
|
+
if plan["format"] == "dmg":
|
|
177
|
+
mount.mkdir()
|
|
178
|
+
_run(["/usr/bin/hdiutil", "attach", str(archive), "-mountpoint", str(mount),
|
|
179
|
+
"-nobrowse", "-readonly", "-noautoopen"], timeout=120)
|
|
180
|
+
mounted = True
|
|
181
|
+
source = mount / plan["bundle"]
|
|
182
|
+
else:
|
|
183
|
+
source = work / "unpacked" / plan["bundle"]
|
|
184
|
+
safe_extract_zip(archive, source.parent, label=plan["app"])
|
|
185
|
+
logger.info("Verifying %s publisher signature and app bundle", plan["app"])
|
|
186
|
+
_mac_bundle(plan, source)
|
|
187
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
# Stage on the target filesystem so the final app appears in one rename.
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix=".chatup-", dir=target.parent) as staging:
|
|
190
|
+
staged = Path(staging) / plan["bundle"]
|
|
191
|
+
logger.info("Installing %s", target)
|
|
192
|
+
_run(["/usr/bin/ditto", str(source), str(staged)])
|
|
193
|
+
_mac_bundle(plan, staged)
|
|
194
|
+
if target.exists() or target.is_symlink():
|
|
195
|
+
raise RuntimeError(f"App appeared during installation; refusing to overwrite {target}.")
|
|
196
|
+
staged.rename(target)
|
|
197
|
+
details = _mac_bundle(plan, target)
|
|
198
|
+
return {**plan, **details, "status": "installed", "verified": True}
|
|
199
|
+
finally:
|
|
200
|
+
if mounted:
|
|
201
|
+
detached = _run(["/usr/bin/hdiutil", "detach", str(mount)], timeout=120, check=False)
|
|
202
|
+
if detached.returncode:
|
|
203
|
+
_run(["/usr/bin/hdiutil", "detach", "-force", str(mount)], timeout=120, check=False)
|
|
204
|
+
# Never recursively clean up a still-mounted disk image.
|
|
205
|
+
if not os.path.ismount(mount):
|
|
206
|
+
shutil.rmtree(work)
|
|
207
|
+
else:
|
|
208
|
+
logger.warning("Disk image still mounted at %s; installer cache retained at %s", mount, work)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _linux_chrome() -> dict[str, str] | None:
|
|
212
|
+
binary = shutil.which("google-chrome-stable") or shutil.which("google-chrome")
|
|
213
|
+
if not binary:
|
|
214
|
+
return None
|
|
215
|
+
result = _run([binary, "--version"], timeout=30, check=False)
|
|
216
|
+
match = re.match(r"Google Chrome (\d+(?:\.\d+)+)", result.stdout.strip())
|
|
217
|
+
if result.returncode or not match:
|
|
218
|
+
raise RuntimeError(f"Cannot verify regular Google Chrome at {binary}.")
|
|
219
|
+
return {"binary": binary, "version": match.group(1)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _install_linux(plan: dict[str, Any], *, sudo: bool) -> dict[str, Any]:
|
|
223
|
+
existing = _linux_chrome()
|
|
224
|
+
if existing:
|
|
225
|
+
return {**plan, **existing, "status": "already_installed", "verified": True}
|
|
226
|
+
if not _is_root() and not sudo:
|
|
227
|
+
raise RuntimeError("Installing Google Chrome on Linux requires root. Re-run: chatup chrome --sudo (optionally --yes).")
|
|
228
|
+
if not _is_root() and not shutil.which("sudo"):
|
|
229
|
+
raise RuntimeError("sudo is unavailable; run chatup chrome as root.")
|
|
230
|
+
with tempfile.TemporaryDirectory(prefix="chrome-", dir=_cache_dir()) as staging:
|
|
231
|
+
package = Path(staging) / plan["filename"]
|
|
232
|
+
_download(plan["url"], package)
|
|
233
|
+
command = [str(package) if arg == "{package}" else arg for arg in plan["command"]]
|
|
234
|
+
logger.info("Installing Google Chrome with %s", plan["manager"])
|
|
235
|
+
_run(command, capture=False)
|
|
236
|
+
installed = _linux_chrome()
|
|
237
|
+
if not installed:
|
|
238
|
+
raise RuntimeError("Google Chrome installation verification failed: browser executable not found.")
|
|
239
|
+
return {**plan, **installed, "status": "installed", "verified": True}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _install_windows(plan: dict[str, Any]) -> dict[str, Any]:
|
|
243
|
+
executable = shutil.which("winget")
|
|
244
|
+
if not executable:
|
|
245
|
+
raise RuntimeError("WinGet is required to install Google Chrome: https://aka.ms/getwinget")
|
|
246
|
+
|
|
247
|
+
def installed():
|
|
248
|
+
result = _run([executable, *plan["verify_command"][1:]], timeout=120, check=False)
|
|
249
|
+
return result.returncode == 0 and re.search(r"\bGoogle\.Chrome\b", result.stdout) is not None
|
|
250
|
+
|
|
251
|
+
if installed():
|
|
252
|
+
return {**plan, "status": "already_installed", "verified": True}
|
|
253
|
+
logger.info("Installing Google Chrome with WinGet; use --yes to accept source/package agreements")
|
|
254
|
+
_run([executable, *plan["command"][1:]], capture=False)
|
|
255
|
+
if not installed():
|
|
256
|
+
raise RuntimeError("Google Chrome installation verification failed: no matching WinGet package record.")
|
|
257
|
+
return {**plan, "status": "installed", "verified": True}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def setup_desktop_app(
|
|
261
|
+
app: str, *, dry_run: bool = False, sudo: bool = False, yes: bool = False,
|
|
262
|
+
log_level: str = "INFO",
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Install a native app if absent and verify it; never launch or upgrade it."""
|
|
265
|
+
global logger
|
|
266
|
+
logger = setup_logger(__name__, log_level=log_level)
|
|
267
|
+
try:
|
|
268
|
+
plan = plan_desktop_install(app, sudo=sudo, yes=yes)
|
|
269
|
+
if dry_run:
|
|
270
|
+
return {**plan, "status": "planned", "verified": False}
|
|
271
|
+
logger.info("Checking %s installation on %s", plan["app"], plan["platform"])
|
|
272
|
+
if plan["method"] == "macos":
|
|
273
|
+
return _install_macos(plan)
|
|
274
|
+
if plan["method"] == "linux":
|
|
275
|
+
return _install_linux(plan, sudo=sudo)
|
|
276
|
+
return _install_windows(plan)
|
|
277
|
+
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
278
|
+
logger.error("%s installation failed: %s", MAC_APPS[app]["app"], exc)
|
|
279
|
+
raise RuntimeError(f"{MAC_APPS[app]['app']} installation failed: {exc}") from exc
|