chatup 0.2.16__tar.gz → 0.2.18__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 (98) hide show
  1. {chatup-0.2.16/src/chatup.egg-info → chatup-0.2.18}/PKG-INFO +14 -3
  2. {chatup-0.2.16 → chatup-0.2.18}/README.md +13 -2
  3. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/__init__.py +1 -1
  4. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/cli.py +3 -0
  5. chatup-0.2.18/src/chatup/commands/desktop.py +50 -0
  6. chatup-0.2.18/src/chatup/setup/desktop.py +279 -0
  7. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/zsh.py +1 -1
  8. {chatup-0.2.16 → chatup-0.2.18/src/chatup.egg-info}/PKG-INFO +14 -3
  9. {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/SOURCES.txt +4 -1
  10. {chatup-0.2.16 → chatup-0.2.18}/tests/test_browser_backend_cli.py +3 -1
  11. {chatup-0.2.16 → chatup-0.2.18}/tests/test_cli_tree_docs.py +2 -1
  12. {chatup-0.2.16 → chatup-0.2.18}/tests/test_version.py +1 -1
  13. chatup-0.2.18/tests/test_zsh_alias_names.py +8 -0
  14. {chatup-0.2.16 → chatup-0.2.18}/LICENSE +0 -0
  15. {chatup-0.2.16 → chatup-0.2.18}/pyproject.toml +0 -0
  16. {chatup-0.2.16 → chatup-0.2.18}/setup.cfg +0 -0
  17. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/chrome_for_testing.py +0 -0
  18. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/chromedriver.py +0 -0
  19. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/__init__.py +0 -0
  20. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/chrome_for_testing.py +0 -0
  21. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/chromedriver.py +0 -0
  22. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/commands/playwright.py +0 -0
  23. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/config.py +0 -0
  24. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/const.py +0 -0
  25. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/__init__.py +0 -0
  26. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/command_schema.py +0 -0
  27. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/policy.py +0 -0
  28. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/interaction/warnings.py +0 -0
  29. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/playwright.py +0 -0
  30. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/__init__.py +0 -0
  31. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/_managed_artifact.py +0 -0
  32. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/chrome_for_testing.py +0 -0
  33. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/chromedriver.py +0 -0
  34. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/runtime/playwright.py +0 -0
  35. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/__init__.py +0 -0
  36. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/assets/hermes/install.sh +0 -0
  37. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/assets/nvm.sh +0 -0
  38. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cc_connect.py +0 -0
  39. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/chatgpt.py +0 -0
  40. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/claude.py +0 -0
  41. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cli.py +0 -0
  42. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/codex.py +0 -0
  43. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/crs.py +0 -0
  44. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/cursor_agent.py +0 -0
  45. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/discourse.py +0 -0
  46. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/docker.py +0 -0
  47. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/elements.py +0 -0
  48. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/frp.py +0 -0
  49. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/gitea.py +0 -0
  50. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/hermes.py +0 -0
  51. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/lark_cli.py +0 -0
  52. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/mode_prompt.py +0 -0
  53. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/mysql.py +0 -0
  54. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/nginx.py +0 -0
  55. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/nodejs.py +0 -0
  56. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/opencode.py +0 -0
  57. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/service_credentials.py +0 -0
  58. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/shell_rc.py +0 -0
  59. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/twikoo.py +0 -0
  60. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/uv.py +0 -0
  61. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/__init__.py +0 -0
  62. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/cli.py +0 -0
  63. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/core.py +0 -0
  64. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/options.py +0 -0
  65. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/render.py +0 -0
  66. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  67. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  68. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  69. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  70. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  71. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  72. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/workspace.py +0 -0
  73. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/setup/zulip.py +0 -0
  74. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/__init__.py +0 -0
  75. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/custom_logger.py +0 -0
  76. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/pathing.py +0 -0
  77. {chatup-0.2.16 → chatup-0.2.18}/src/chatup/utils/platforming.py +0 -0
  78. {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/dependency_links.txt +0 -0
  79. {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/entry_points.txt +0 -0
  80. {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/requires.txt +0 -0
  81. {chatup-0.2.16 → chatup-0.2.18}/src/chatup.egg-info/top_level.txt +0 -0
  82. {chatup-0.2.16 → chatup-0.2.18}/tests/test_chatgpt_setup.py +0 -0
  83. {chatup-0.2.16 → chatup-0.2.18}/tests/test_chrome_for_testing.py +0 -0
  84. {chatup-0.2.16 → chatup-0.2.18}/tests/test_chromedriver.py +0 -0
  85. {chatup-0.2.16 → chatup-0.2.18}/tests/test_codex_setup.py +0 -0
  86. {chatup-0.2.16 → chatup-0.2.18}/tests/test_crs_setup.py +0 -0
  87. {chatup-0.2.16 → chatup-0.2.18}/tests/test_cursor_agent_setup.py +0 -0
  88. {chatup-0.2.16 → chatup-0.2.18}/tests/test_hermes_model_defaults.py +0 -0
  89. {chatup-0.2.16 → chatup-0.2.18}/tests/test_playwright.py +0 -0
  90. {chatup-0.2.16 → chatup-0.2.18}/tests/test_playwright_cli.py +0 -0
  91. {chatup-0.2.16 → chatup-0.2.18}/tests/test_publish_workflow.py +0 -0
  92. {chatup-0.2.16 → chatup-0.2.18}/tests/test_pyproject_metadata.py +0 -0
  93. {chatup-0.2.16 → chatup-0.2.18}/tests/test_service_installers.py +0 -0
  94. {chatup-0.2.16 → chatup-0.2.18}/tests/test_setup_cli.py +0 -0
  95. {chatup-0.2.16 → chatup-0.2.18}/tests/test_twikoo_setup.py +0 -0
  96. {chatup-0.2.16 → chatup-0.2.18}/tests/test_uv_activation.py +0 -0
  97. {chatup-0.2.16 → chatup-0.2.18}/tests/test_windows_compat.py +0 -0
  98. {chatup-0.2.16 → chatup-0.2.18}/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.16
3
+ Version: 0.2.18
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -76,6 +76,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
76
76
  chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
77
77
  ```
78
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
+
79
90
  ## ChatGPT / Codex 桌面应用
80
91
 
81
92
  ```bash
@@ -94,11 +105,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
94
105
  | --- | --- |
95
106
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
96
107
  | 工作区脚手架 | `workspace` |
97
- | 官方桌面应用 | `chatgpt`(含 Codex) |
108
+ | 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
98
109
  | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
99
110
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
100
111
 
101
- 桌面应用使用系统包管理器的原生目录;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`。
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`。
102
113
 
103
114
  ## 开发
104
115
 
@@ -45,6 +45,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
45
45
  chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
46
46
  ```
47
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
+
48
59
  ## ChatGPT / Codex 桌面应用
49
60
 
50
61
  ```bash
@@ -63,11 +74,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
63
74
  | --- | --- |
64
75
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
65
76
  | 工作区脚手架 | `workspace` |
66
- | 官方桌面应用 | `chatgpt`(含 Codex) |
77
+ | 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
67
78
  | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
68
79
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
69
80
 
70
- 桌面应用使用系统包管理器的原生目录;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`。
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`。
71
82
 
72
83
  ## 开发
73
84
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.16"
5
+ __version__ = "0.2.18"
@@ -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,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
@@ -120,7 +120,7 @@ addkernel (){
120
120
 
121
121
  # pip install
122
122
  alias tspip="pip install -i https://pypi.tuna.tsinghua.edu.cn/simple"
123
- alias pypi='pip install -i https://pypi.python.org/simple'
123
+ alias pypip='pip install -i https://pypi.python.org/simple'
124
124
 
125
125
  # kill process
126
126
  kport (){
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -76,6 +76,17 @@ chatup nginx proxy-pass ./gitea-local.conf --set SERVER_NAME=gitea.local.example
76
76
  chatup crs --install-dir ~/.chatarch/crs/local --port 12392 --redis-port 6379
77
77
  ```
78
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
+
79
90
  ## ChatGPT / Codex 桌面应用
80
91
 
81
92
  ```bash
@@ -94,11 +105,11 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
94
105
  | --- | --- |
95
106
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
96
107
  | 工作区脚手架 | `workspace` |
97
- | 官方桌面应用 | `chatgpt`(含 Codex) |
108
+ | 桌面应用 | `chrome`、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
98
109
  | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
99
110
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
100
111
 
101
- 桌面应用使用系统包管理器的原生目录;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`。
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`。
102
113
 
103
114
  ## 开发
104
115
 
@@ -17,6 +17,7 @@ src/chatup.egg-info/top_level.txt
17
17
  src/chatup/commands/__init__.py
18
18
  src/chatup/commands/chrome_for_testing.py
19
19
  src/chatup/commands/chromedriver.py
20
+ src/chatup/commands/desktop.py
20
21
  src/chatup/commands/playwright.py
21
22
  src/chatup/interaction/__init__.py
22
23
  src/chatup/interaction/command_schema.py
@@ -35,6 +36,7 @@ src/chatup/setup/cli.py
35
36
  src/chatup/setup/codex.py
36
37
  src/chatup/setup/crs.py
37
38
  src/chatup/setup/cursor_agent.py
39
+ src/chatup/setup/desktop.py
38
40
  src/chatup/setup/discourse.py
39
41
  src/chatup/setup/docker.py
40
42
  src/chatup/setup/elements.py
@@ -90,4 +92,5 @@ tests/test_twikoo_setup.py
90
92
  tests/test_uv_activation.py
91
93
  tests/test_version.py
92
94
  tests/test_windows_compat.py
93
- tests/test_workspace_setup.py
95
+ tests/test_workspace_setup.py
96
+ tests/test_zsh_alias_names.py
@@ -14,7 +14,9 @@ def test_browser_artifacts_have_independent_top_level_backends():
14
14
  assert "chrome-for-testing" in commands
15
15
  assert "chromedriver" in commands
16
16
  assert "playwright" in commands
17
- assert "chrome" not in commands
17
+ assert "chrome" in commands
18
+ assert not isinstance(main.commands["chrome"], click.Group)
19
+ assert "regular Google Chrome" in main.commands["chrome"].help
18
20
  assert "browser" not in commands
19
21
  assert "chromium" not in commands
20
22
 
@@ -80,5 +80,6 @@ def test_cli_tree_is_in_mkdocs_nav_and_backend_contracts_are_complete():
80
80
  text = (ROOT / "docs" / name).read_text(encoding="utf-8")
81
81
  missing = sorted(item for item in required if item not in text)
82
82
  assert not missing, f"{name} is missing backend contract terms: {missing}"
83
- assert "\nchatup chrome\n" not in text
83
+ assert "chatup chrome" in text
84
+ assert "chatup iterm" in text
84
85
  assert "from chatup.chrome import" not in text
@@ -5,7 +5,7 @@ from chatup.cli import main
5
5
 
6
6
 
7
7
  def test_version_present():
8
- assert __version__ == "0.2.16"
8
+ assert __version__ == "0.2.18"
9
9
 
10
10
 
11
11
  def test_cli_help():
@@ -0,0 +1,8 @@
1
+ from chatup.setup.zsh import render_zsh_aliases
2
+
3
+
4
+ def test_pip_install_alias_rename_preserves_both_urls():
5
+ lines = render_zsh_aliases().splitlines()
6
+ assert 'alias tspip="pip install -i https://pypi.tuna.tsinghua.edu.cn/simple"' in lines
7
+ assert "alias pypip='pip install -i https://pypi.python.org/simple'" in lines
8
+ assert not any(line.startswith("alias pypi=") for line in lines)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes