chatup 0.2.14__tar.gz → 0.2.16__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 (95) hide show
  1. {chatup-0.2.14/src/chatup.egg-info → chatup-0.2.16}/PKG-INFO +17 -2
  2. {chatup-0.2.14 → chatup-0.2.16}/README.md +16 -1
  3. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/__init__.py +1 -1
  4. chatup-0.2.16/src/chatup/setup/chatgpt.py +132 -0
  5. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/claude.py +4 -2
  6. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/codex.py +5 -4
  7. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/crs.py +5 -3
  8. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/cursor_agent.py +47 -11
  9. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/discourse.py +6 -2
  10. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/docker.py +19 -1
  11. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/elements.py +55 -8
  12. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/frp.py +42 -21
  13. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/gitea.py +5 -3
  14. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/hermes.py +13 -7
  15. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/lark_cli.py +6 -3
  16. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/mysql.py +94 -10
  17. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/nginx.py +14 -12
  18. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/nodejs.py +32 -4
  19. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/opencode.py +2 -1
  20. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/service_credentials.py +6 -2
  21. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/twikoo.py +13 -7
  22. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/uv.py +107 -10
  23. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/zulip.py +6 -2
  24. chatup-0.2.16/src/chatup/utils/platforming.py +82 -0
  25. {chatup-0.2.14 → chatup-0.2.16/src/chatup.egg-info}/PKG-INFO +17 -2
  26. {chatup-0.2.14 → chatup-0.2.16}/src/chatup.egg-info/SOURCES.txt +6 -0
  27. chatup-0.2.16/tests/test_chatgpt_setup.py +183 -0
  28. {chatup-0.2.14 → chatup-0.2.16}/tests/test_codex_setup.py +99 -10
  29. {chatup-0.2.14 → chatup-0.2.16}/tests/test_crs_setup.py +1 -0
  30. {chatup-0.2.14 → chatup-0.2.16}/tests/test_cursor_agent_setup.py +45 -22
  31. chatup-0.2.16/tests/test_hermes_model_defaults.py +50 -0
  32. {chatup-0.2.14 → chatup-0.2.16}/tests/test_playwright.py +4 -4
  33. {chatup-0.2.14 → chatup-0.2.16}/tests/test_service_installers.py +9 -3
  34. {chatup-0.2.14 → chatup-0.2.16}/tests/test_setup_cli.py +13 -3
  35. {chatup-0.2.14 → chatup-0.2.16}/tests/test_twikoo_setup.py +3 -2
  36. chatup-0.2.16/tests/test_uv_activation.py +259 -0
  37. {chatup-0.2.14 → chatup-0.2.16}/tests/test_version.py +2 -2
  38. chatup-0.2.16/tests/test_windows_compat.py +135 -0
  39. {chatup-0.2.14 → chatup-0.2.16}/LICENSE +0 -0
  40. {chatup-0.2.14 → chatup-0.2.16}/pyproject.toml +0 -0
  41. {chatup-0.2.14 → chatup-0.2.16}/setup.cfg +0 -0
  42. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/chrome_for_testing.py +0 -0
  43. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/chromedriver.py +0 -0
  44. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/cli.py +0 -0
  45. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/commands/__init__.py +0 -0
  46. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/commands/chrome_for_testing.py +0 -0
  47. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/commands/chromedriver.py +0 -0
  48. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/commands/playwright.py +0 -0
  49. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/config.py +0 -0
  50. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/const.py +0 -0
  51. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/interaction/__init__.py +0 -0
  52. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/interaction/command_schema.py +0 -0
  53. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/interaction/policy.py +0 -0
  54. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/interaction/warnings.py +0 -0
  55. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/playwright.py +0 -0
  56. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/runtime/__init__.py +0 -0
  57. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/runtime/_managed_artifact.py +0 -0
  58. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/runtime/chrome_for_testing.py +0 -0
  59. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/runtime/chromedriver.py +0 -0
  60. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/runtime/playwright.py +0 -0
  61. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/__init__.py +0 -0
  62. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/assets/hermes/install.sh +0 -0
  63. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/assets/nvm.sh +0 -0
  64. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/cc_connect.py +0 -0
  65. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/cli.py +0 -0
  66. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/mode_prompt.py +0 -0
  67. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/shell_rc.py +0 -0
  68. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/__init__.py +0 -0
  69. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/cli.py +0 -0
  70. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/core.py +0 -0
  71. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/options.py +0 -0
  72. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/render.py +0 -0
  73. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  74. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  75. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  76. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  77. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  78. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  79. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/workspace.py +0 -0
  80. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/setup/zsh.py +0 -0
  81. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/utils/__init__.py +0 -0
  82. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/utils/custom_logger.py +0 -0
  83. {chatup-0.2.14 → chatup-0.2.16}/src/chatup/utils/pathing.py +0 -0
  84. {chatup-0.2.14 → chatup-0.2.16}/src/chatup.egg-info/dependency_links.txt +0 -0
  85. {chatup-0.2.14 → chatup-0.2.16}/src/chatup.egg-info/entry_points.txt +0 -0
  86. {chatup-0.2.14 → chatup-0.2.16}/src/chatup.egg-info/requires.txt +0 -0
  87. {chatup-0.2.14 → chatup-0.2.16}/src/chatup.egg-info/top_level.txt +0 -0
  88. {chatup-0.2.14 → chatup-0.2.16}/tests/test_browser_backend_cli.py +0 -0
  89. {chatup-0.2.14 → chatup-0.2.16}/tests/test_chrome_for_testing.py +0 -0
  90. {chatup-0.2.14 → chatup-0.2.16}/tests/test_chromedriver.py +0 -0
  91. {chatup-0.2.14 → chatup-0.2.16}/tests/test_cli_tree_docs.py +0 -0
  92. {chatup-0.2.14 → chatup-0.2.16}/tests/test_playwright_cli.py +0 -0
  93. {chatup-0.2.14 → chatup-0.2.16}/tests/test_publish_workflow.py +0 -0
  94. {chatup-0.2.14 → chatup-0.2.16}/tests/test_pyproject_metadata.py +0 -0
  95. {chatup-0.2.14 → chatup-0.2.16}/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.14
3
+ Version: 0.2.16
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,29 @@ 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
+ ## ChatGPT / Codex 桌面应用
80
+
81
+ ```bash
82
+ chatup chatgpt --dry-run
83
+ chatup chatgpt
84
+ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
85
+ ```
86
+
87
+ 安装包含 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)。
88
+
89
+ `chatup codex`、`chatup hermes` 未配置模型时默认使用 `gpt-5.6-terra`;用户指定的模型和已有配置仍优先。
90
+
77
91
  ## 当前能力
78
92
 
79
93
  | 能力组 | 命令 |
80
94
  | --- | --- |
81
95
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
82
96
  | 工作区脚手架 | `workspace` |
97
+ | 官方桌面应用 | `chatgpt`(含 Codex) |
83
98
  | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
84
99
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
85
100
 
86
- 所有新增安装项默认目录都收敛到 `~/.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`。
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`。
87
102
 
88
103
  ## 开发
89
104
 
@@ -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,29 @@ 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
+ ## ChatGPT / Codex 桌面应用
49
+
50
+ ```bash
51
+ chatup chatgpt --dry-run
52
+ chatup chatgpt
53
+ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
54
+ ```
55
+
56
+ 安装包含 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)。
57
+
58
+ `chatup codex`、`chatup hermes` 未配置模型时默认使用 `gpt-5.6-terra`;用户指定的模型和已有配置仍优先。
59
+
46
60
  ## 当前能力
47
61
 
48
62
  | 能力组 | 命令 |
49
63
  | --- | --- |
50
64
  | 基础运行环境 | `doctor`、`uv`、`nodejs`、`docker`、`zsh`、`chrome-for-testing`、`chromedriver`、`playwright`、`frp` |
51
65
  | 工作区脚手架 | `workspace` |
66
+ | 官方桌面应用 | `chatgpt`(含 Codex) |
52
67
  | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
53
68
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
54
69
 
55
- 所有新增安装项默认目录都收敛到 `~/.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`。
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`。
56
71
 
57
72
  ## 开发
58
73
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.14"
5
+ __version__ = "0.2.16"
@@ -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}
@@ -1,6 +1,8 @@
1
1
  import json
2
2
  from pathlib import Path
3
3
 
4
+ from chatup.utils.platforming import chmod_private
5
+
4
6
  DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
5
7
  DEFAULT_SMALL_FAST_MODEL = "claude-opus-4-6"
6
8
 
@@ -223,7 +225,7 @@ def setup_claude(
223
225
  settings_path.write_text(
224
226
  json.dumps(settings_json, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
225
227
  )
226
- settings_path.chmod(0o600)
228
+ chmod_private(settings_path)
227
229
  logger.info(f"Wrote settings file: {settings_path}")
228
230
 
229
231
  config_json = {"primaryApiKey": primary_api_key}
@@ -231,7 +233,7 @@ def setup_claude(
231
233
  config_path.write_text(
232
234
  json.dumps(config_json, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
233
235
  )
234
- config_path.chmod(0o600)
236
+ chmod_private(config_path)
235
237
  logger.info(f"Wrote config file: {config_path}")
236
238
 
237
239
  click.echo("Claude Code setup completed.")
@@ -28,8 +28,9 @@ from chatup.setup.nodejs import (
28
28
  should_install_global_npm_package,
29
29
  )
30
30
  from chatup.utils.custom_logger import setup_logger
31
+ from chatup.utils.platforming import chmod_private
31
32
 
32
- DEFAULT_MODEL = "gpt-5.5"
33
+ DEFAULT_MODEL = "gpt-5.6-terra"
33
34
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
34
35
  DEFAULT_AUTH_METHOD = "apikey"
35
36
  _ENV_VAR_REF_RE = re.compile(r"\$(?:\{[^}]+\}|[A-Za-z_][A-Za-z0-9_]*)")
@@ -207,7 +208,7 @@ def _write_codex_config(config_path: Path, *, model: str, base_url: str) -> list
207
208
  )
208
209
  config_path.parent.mkdir(parents=True, exist_ok=True)
209
210
  config_path.write_text(content, encoding="utf-8")
210
- config_path.chmod(0o600)
211
+ chmod_private(config_path)
211
212
  logger.info(f"Patched config file: {config_path}")
212
213
  return changed_root + changed_provider
213
214
 
@@ -216,13 +217,13 @@ def _write_private_text(path: Path, content: str) -> None:
216
217
  path.parent.mkdir(parents=True, exist_ok=True)
217
218
  fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
218
219
  try:
219
- os.chmod(tmp_name, 0o600)
220
+ chmod_private(Path(tmp_name))
220
221
  with os.fdopen(fd, "w", encoding="utf-8") as handle:
221
222
  handle.write(content)
222
223
  handle.flush()
223
224
  os.fsync(handle.fileno())
224
225
  os.replace(tmp_name, path)
225
- path.chmod(0o600)
226
+ chmod_private(path)
226
227
  except Exception:
227
228
  try:
228
229
  os.close(fd)
@@ -12,6 +12,7 @@ import click
12
12
  from chatup.interaction import abort_if_force_without_tty, resolve_interactive_mode
13
13
  from chatup.setup.nodejs import ensure_nodejs_requirement
14
14
  from chatup.utils.custom_logger import setup_logger
15
+ from chatup.utils.platforming import chmod_private, require_non_windows
15
16
 
16
17
  logger = setup_logger("setup_crs")
17
18
 
@@ -146,7 +147,7 @@ def write_secret_file(path: Path) -> dict[str, str]:
146
147
  old_umask = os.umask(0o077)
147
148
  try:
148
149
  path.write_text("".join(f"{key}={value}\n" for key, value in values.items()), encoding="utf-8")
149
- path.chmod(0o600)
150
+ chmod_private(path)
150
151
  finally:
151
152
  os.umask(old_umask)
152
153
  return values
@@ -178,7 +179,7 @@ def write_env_file(crs_root: Path, *, port: int, redis_port: int, secrets_values
178
179
  ),
179
180
  encoding="utf-8",
180
181
  )
181
- env_path.chmod(0o600)
182
+ chmod_private(env_path)
182
183
  return env_path
183
184
 
184
185
 
@@ -234,6 +235,7 @@ def setup_crs(
234
235
 
235
236
  if smoke and not start:
236
237
  raise click.ClickException("--smoke requires --start; pass --no-smoke when using --no-start.")
238
+ require_non_windows("chatup crs")
237
239
 
238
240
  node_runtime = ensure_nodejs_requirement(
239
241
  interactive=interactive,
@@ -302,7 +304,7 @@ def setup_crs(
302
304
  stdout=log_file,
303
305
  stderr=subprocess.STDOUT,
304
306
  )
305
- setup_log.chmod(0o600)
307
+ chmod_private(setup_log)
306
308
 
307
309
  run_command(npm_command(node_runtime, "run", "install:web"), cwd=crs_root, env=npm_env)
308
310
  run_command(npm_command(node_runtime, "run", "build:web"), cwd=crs_root, env=npm_env)
@@ -5,7 +5,6 @@ import os
5
5
  from pathlib import Path
6
6
  import shutil
7
7
  import shlex
8
- import stat
9
8
  import subprocess
10
9
  from typing import Any
11
10
 
@@ -15,6 +14,7 @@ from chatenv.store import EnvStore
15
14
 
16
15
  from chatup.config import CursorAgentConfig
17
16
  from chatup.utils.custom_logger import setup_logger
17
+ from chatup.utils.platforming import chmod_executable, chmod_private, is_windows, user_bin_candidates
18
18
 
19
19
  DEFAULT_INSTALL_URL = "https://cursor.com/install"
20
20
  CREDENTIAL_STORE_CHOICES = ("native", "file-wrapper")
@@ -44,10 +44,7 @@ def _cursor_agent_state_path() -> Path:
44
44
 
45
45
 
46
46
  def _chmod_private(path: Path) -> None:
47
- try:
48
- path.chmod(0o600)
49
- except PermissionError:
50
- logger.warning(f"Could not chmod private file: {path}")
47
+ chmod_private(path)
51
48
 
52
49
 
53
50
  def _read_json_file(path: Path) -> dict[str, Any]:
@@ -168,9 +165,16 @@ def _resolve_command_or_user_bin(name: str) -> Path | None:
168
165
  found = shutil.which(name)
169
166
  if found:
170
167
  candidates.append(Path(found).expanduser())
171
- candidates.append(Path.home() / ".local" / "bin" / name)
168
+ candidates.extend(user_bin_candidates(name))
169
+ if is_windows():
170
+ candidates.extend(
171
+ [
172
+ Path.home() / ".local" / "bin" / f"{name}.cmd",
173
+ Path.home() / ".local" / "bin" / f"{name}.bat",
174
+ ]
175
+ )
172
176
  for candidate in candidates:
173
- if candidate.exists() and os.access(candidate, os.X_OK):
177
+ if candidate.exists() and (is_windows() or os.access(candidate, os.X_OK)):
174
178
  return candidate
175
179
  return None
176
180
 
@@ -190,14 +194,19 @@ def _managed_wrapper_official_target(entrypoint: Path) -> Path | None:
190
194
  if WRAPPER_MARKER not in text:
191
195
  return None
192
196
  for line in text.splitlines():
193
- if line.startswith("CHATUP_CURSOR_AGENT_OFFICIAL="):
194
- value = line.split("=", 1)[1]
197
+ stripped = line.strip()
198
+ if stripped.startswith("CHATUP_CURSOR_AGENT_OFFICIAL="):
199
+ value = stripped.split("=", 1)[1]
195
200
  try:
196
201
  parts = shlex.split(value)
197
202
  except ValueError:
198
203
  return None
199
204
  if parts:
200
205
  return Path(parts[0])
206
+ if stripped.lower().startswith('set "chatup_cursor_agent_official=') and stripped.endswith('"'):
207
+ value = stripped.split("=", 1)[1][:-1]
208
+ if value:
209
+ return Path(value)
201
210
  return None
202
211
 
203
212
 
@@ -231,7 +240,24 @@ def _write_file_credential_wrapper(entrypoint: Path, official_binary: Path, auth
231
240
  if existing_target and existing_target == entrypoint:
232
241
  raise click.ClickException(f"Refusing to wrap Cursor Agent entrypoint that resolves to itself: {entrypoint}")
233
242
  official = official_binary.expanduser()
234
- script = f'''#!/usr/bin/env bash
243
+ if is_windows():
244
+ script = f'''@echo off
245
+ rem {WRAPPER_MARKER}.
246
+ rem This wrapper does not store token values; it reads Cursor auth.json at runtime.
247
+ set "CHATUP_CURSOR_AGENT_OFFICIAL={official}"
248
+ if "%CURSOR_AGENT_AUTH_JSON%"=="" (
249
+ set "CHATUP_CURSOR_AGENT_AUTH_JSON={auth_path}"
250
+ ) else (
251
+ set "CHATUP_CURSOR_AGENT_AUTH_JSON=%CURSOR_AGENT_AUTH_JSON%"
252
+ )
253
+ if "%AGENT_CLI_CREDENTIAL_STORE%"=="" set "AGENT_CLI_CREDENTIAL_STORE=file"
254
+ if "%CURSOR_AUTH_TOKEN%"=="" if exist "%CHATUP_CURSOR_AGENT_AUTH_JSON%" (
255
+ for /f "usebackq delims=" %%T in (`python -c "import json,sys; print(json.load(open(sys.argv[1], encoding='utf-8')).get('accessToken',''), end='')" "%CHATUP_CURSOR_AGENT_AUTH_JSON%"`) do set "CURSOR_AUTH_TOKEN=%%T"
256
+ )
257
+ "%CHATUP_CURSOR_AGENT_OFFICIAL%" %*
258
+ '''
259
+ else:
260
+ script = f'''#!/usr/bin/env bash
235
261
  set -euo pipefail
236
262
  # {WRAPPER_MARKER}.
237
263
  # This wrapper does not store token values; it reads Cursor auth.json at runtime.
@@ -256,13 +282,19 @@ fi
256
282
  exec -a "$0" "$CHATUP_CURSOR_AGENT_OFFICIAL" "$@"
257
283
  '''
258
284
  entrypoint.write_text(script, encoding="utf-8")
259
- entrypoint.chmod(0o755)
285
+ chmod_executable(entrypoint)
260
286
  return True
261
287
 
262
288
 
263
289
  def _write_file_credential_wrappers(binary: str) -> list[str]:
264
290
  official = _resolve_official_cursor_agent_binary(binary)
265
291
  written: list[str] = []
292
+ if is_windows():
293
+ for name in ("cursor-agent", "agent"):
294
+ entrypoint = official.parent / f"{name}.cmd"
295
+ if _write_file_credential_wrapper(entrypoint, official, _cursor_auth_path()):
296
+ written.append(str(entrypoint))
297
+ return written
266
298
  for name in ("cursor-agent", "agent"):
267
299
  entrypoint = _entrypoint_for(name)
268
300
  if not entrypoint:
@@ -278,6 +310,10 @@ def _write_file_credential_wrappers(binary: str) -> list[str]:
278
310
  def _install_cursor_agent_if_needed(*, install_url: str = DEFAULT_INSTALL_URL) -> bool:
279
311
  if _resolve_cursor_agent_binary():
280
312
  return False
313
+ if is_windows():
314
+ raise click.ClickException(
315
+ "Cursor Agent was not found on PATH. Install Cursor Agent for Windows first, then rerun ChatUp."
316
+ )
281
317
  script = subprocess.run(
282
318
  ["bash", "-lc", f"curl -fsSL {install_url!r} | bash"],
283
319
  text=True,
@@ -105,7 +105,9 @@ def ensure_directories(home: Path) -> dict[str, Path]:
105
105
  }
106
106
  for path in paths.values():
107
107
  path.mkdir(parents=True, exist_ok=True)
108
- paths["secrets"].chmod(0o700)
108
+ from chatup.utils.platforming import chmod_private_dir
109
+
110
+ chmod_private_dir(paths["secrets"])
109
111
  return paths
110
112
 
111
113
 
@@ -162,7 +164,9 @@ def setup_discourse(
162
164
  render_app_yml(hostname=hostname, port=port, shared_dir=paths["shared"], with_ai=with_ai),
163
165
  encoding="utf-8",
164
166
  )
165
- app_yml.chmod(0o640)
167
+ from chatup.utils.platforming import chmod_private
168
+
169
+ chmod_private(app_yml)
166
170
 
167
171
  if clone:
168
172
  clone_or_update_discourse_docker(paths["docker"], repo)
@@ -14,6 +14,7 @@ from chatup.interaction import (
14
14
  resolve_interactive_mode,
15
15
  )
16
16
  from chatup.utils.custom_logger import setup_logger
17
+ from chatup.utils.platforming import is_windows
17
18
 
18
19
  logger = setup_logger("setup_docker")
19
20
  DOCKER_COMPOSE_VERSION = "v2.22.0"
@@ -26,7 +27,10 @@ def _configure_logger(log_level="INFO"):
26
27
 
27
28
 
28
29
  def _run(command: list[str]) -> subprocess.CompletedProcess:
29
- return subprocess.run(command, capture_output=True, text=True)
30
+ try:
31
+ return subprocess.run(command, capture_output=True, text=True)
32
+ except FileNotFoundError as exc:
33
+ return subprocess.CompletedProcess(command, 127, "", str(exc))
30
34
 
31
35
 
32
36
  def _ensure_success(result: subprocess.CompletedProcess, step_name: str) -> None:
@@ -103,6 +107,20 @@ def setup_docker(interactive=None, use_sudo=False, log_level="INFO"):
103
107
  )
104
108
  abort_if_force_without_tty(force_interactive, can_prompt, usage)
105
109
 
110
+ if is_windows():
111
+ docker_version = _docker_version()
112
+ if docker_version:
113
+ click.echo(f"Docker already installed: {docker_version}")
114
+ else:
115
+ click.echo("Docker CLI not found. Install Docker Desktop for Windows and ensure `docker` is on PATH.")
116
+ compose_version = _docker_compose_version()
117
+ if compose_version:
118
+ click.echo(f"Docker Compose already installed: {compose_version}")
119
+ else:
120
+ click.echo("Docker Compose not found. Docker Desktop usually provides `docker compose`.")
121
+ click.echo("Docker group/systemd checks are skipped on Windows.")
122
+ return {"docker": docker_version, "compose": compose_version, "platform": "windows"}
123
+
106
124
  logger.info("Start docker setup")
107
125
  user_name = _get_user_name()
108
126
 
@@ -6,7 +6,8 @@ import click
6
6
  from chatstyle import INTERACTIVE_OPTION_HELP
7
7
 
8
8
  from chatup.setup.claude import setup_claude
9
- from chatup.setup.codex import setup_codex
9
+ from chatup.setup.codex import DEFAULT_MODEL as DEFAULT_CODEX_MODEL, setup_codex
10
+ from chatup.setup.chatgpt import setup_chatgpt
10
11
  from chatup.setup.cursor_agent import CREDENTIAL_STORE_CHOICES, setup_cursor_agent
11
12
  from chatup.setup.cc_connect import setup_cc_connect
12
13
  import chatup.setup.crs as crs_module
@@ -37,7 +38,7 @@ from chatup.setup.gitea import (
37
38
  DEFAULT_WORK_DIR as DEFAULT_GITEA_WORK_DIR,
38
39
  setup_gitea,
39
40
  )
40
- from chatup.setup.hermes import setup_hermes
41
+ from chatup.setup.hermes import DEFAULT_MODEL as DEFAULT_HERMES_MODEL, setup_hermes
41
42
  from chatup.setup.lark_cli import setup_lark_cli
42
43
  from chatup.setup.mysql import (
43
44
  DEFAULT_MYSQL_BIND_ADDRESS,
@@ -62,7 +63,7 @@ from chatup.setup.twikoo import (
62
63
  DEFAULT_TWIKOO_VERSION,
63
64
  setup_twikoo,
64
65
  )
65
- from chatup.setup.uv import DEFAULT_PYTHON_VERSION, DEFAULT_VENV_PATH, setup_uv
66
+ from chatup.setup.uv import DEFAULT_PYTHON_VERSION, setup_uv
66
67
  from chatup.setup.workspace import setup_workspace
67
68
  from chatup.setup.zsh import setup_zsh
68
69
  from chatup.setup.zulip import (
@@ -102,8 +103,14 @@ def nodejs_setup(interactive, log_level):
102
103
  setup_nodejs(interactive=interactive, log_level=log_level)
103
104
 
104
105
 
105
- def uv_setup(venv, python_version, force, log_level):
106
- setup_uv(venv=venv, python_version=python_version, force=force, log_level=log_level)
106
+ def uv_setup(venv, python_version, force, log_level, shell_activate):
107
+ setup_uv(
108
+ venv=venv,
109
+ python_version=python_version,
110
+ force=force,
111
+ log_level=log_level,
112
+ shell_activate=shell_activate,
113
+ )
107
114
 
108
115
 
109
116
  def docker_setup(sudo, interactive, log_level):
@@ -120,6 +127,20 @@ def zsh_setup(omz, aliases, login_shell, interactive, log_level):
120
127
  )
121
128
 
122
129
 
130
+ def chatgpt_setup(dry_run, yes):
131
+ try:
132
+ result = setup_chatgpt(dry_run=dry_run, yes=yes)
133
+ except RuntimeError as exc:
134
+ raise click.ClickException(str(exc)) from exc
135
+ click.echo(result["app"])
136
+ if result["status"] == "planned":
137
+ click.echo("预览安装命令(未执行):" + " ".join(result["command"]))
138
+ elif result["status"] == "already_installed":
139
+ click.echo("已安装,并已通过包管理器回读确认;未升级或启动应用。")
140
+ else:
141
+ click.echo("安装完成,已通过包管理器回读确认。请手动打开 ChatGPT 并登录。")
142
+
143
+
123
144
  def codex_setup(api_key, base_url, model, env, interactive, install_only, log_level):
124
145
  setup_codex(
125
146
  api_key=api_key,
@@ -1231,7 +1252,7 @@ SETUP_COMMAND_ELEMENTS = (
1231
1252
  SetupOptionElement(
1232
1253
  param_decls=("--venv", "--venv-path"),
1233
1254
  kwargs={
1234
- "default": str(DEFAULT_VENV_PATH),
1255
+ "default": "~/.chatarch/venv",
1235
1256
  "show_default": True,
1236
1257
  "help": "Target ChatArch Python virtual environment path.",
1237
1258
  },
@@ -1251,6 +1272,29 @@ SETUP_COMMAND_ELEMENTS = (
1251
1272
  "help": "Clear and recreate the target environment if it already exists.",
1252
1273
  },
1253
1274
  ),
1275
+ SetupOptionElement(
1276
+ param_decls=("--activate/--no-activate", "shell_activate"),
1277
+ kwargs={
1278
+ "default": True,
1279
+ "show_default": True,
1280
+ "help": "Update auto-activation in existing ~/.bashrc and ~/.zshrc (POSIX only).",
1281
+ },
1282
+ ),
1283
+ ),
1284
+ ),
1285
+ SetupCommandElement(
1286
+ name="chatgpt",
1287
+ help="Install the official ChatGPT desktop app (includes Codex), not the Codex CLI.",
1288
+ callback=chatgpt_setup,
1289
+ options=(
1290
+ SetupOptionElement(
1291
+ param_decls=("--dry-run",),
1292
+ kwargs={"is_flag": True, "help": "Show the install plan without executing or downloading anything."},
1293
+ ),
1294
+ SetupOptionElement(
1295
+ param_decls=("--yes", "-y"),
1296
+ kwargs={"is_flag": True, "help": "Accept Microsoft Store source/package agreements on Windows."},
1297
+ ),
1254
1298
  ),
1255
1299
  ),
1256
1300
  SetupCommandElement(
@@ -1282,7 +1326,10 @@ SETUP_COMMAND_ELEMENTS = (
1282
1326
  ),
1283
1327
  SetupOptionElement(
1284
1328
  param_decls=("--model",),
1285
- kwargs={"default": None, "help": "Optional default model name."},
1329
+ kwargs={
1330
+ "default": None,
1331
+ "help": f"Optional model override; fallback: {DEFAULT_CODEX_MODEL}.",
1332
+ },
1286
1333
  ),
1287
1334
  SetupOptionElement(
1288
1335
  param_decls=("-e", "--env"),
@@ -1545,7 +1592,7 @@ SETUP_COMMAND_ELEMENTS = (
1545
1592
  ),
1546
1593
  SetupOptionElement(
1547
1594
  param_decls=("--model",),
1548
- kwargs={"default": None, "help": "Hermes default model."},
1595
+ kwargs={"default": None, "help": f"Hermes model override; fallback: {DEFAULT_HERMES_MODEL}."},
1549
1596
  ),
1550
1597
  SetupOptionElement(
1551
1598
  param_decls=("--with-webui-env",),