chatup 0.2.21__tar.gz → 0.2.23__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 (113) hide show
  1. {chatup-0.2.21/src/chatup.egg-info → chatup-0.2.23}/PKG-INFO +8 -2
  2. {chatup-0.2.21 → chatup-0.2.23}/README.md +5 -1
  3. {chatup-0.2.21 → chatup-0.2.23}/pyproject.toml +2 -0
  4. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/__init__.py +1 -1
  5. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/playwright.py +19 -14
  6. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/claude.py +30 -6
  7. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/codex.py +66 -27
  8. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cursor_agent.py +42 -2
  9. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/elements.py +43 -2
  10. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/frp.py +10 -9
  11. chatup-0.2.23/src/chatup/setup/glance.py +240 -0
  12. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/hermes.py +43 -6
  13. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/lark_cli.py +3 -1
  14. chatup-0.2.23/src/chatup/setup/nodejs.py +1009 -0
  15. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/remotion.py +34 -16
  16. {chatup-0.2.21 → chatup-0.2.23/src/chatup.egg-info}/PKG-INFO +8 -2
  17. {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/SOURCES.txt +6 -0
  18. {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/requires.txt +2 -0
  19. {chatup-0.2.21 → chatup-0.2.23}/tests/test_codex_setup.py +68 -0
  20. chatup-0.2.23/tests/test_glance_bridge.py +6 -0
  21. chatup-0.2.23/tests/test_glance_setup.py +190 -0
  22. {chatup-0.2.21 → chatup-0.2.23}/tests/test_pyproject_metadata.py +1 -0
  23. {chatup-0.2.21 → chatup-0.2.23}/tests/test_setup_cli.py +2 -0
  24. {chatup-0.2.21 → chatup-0.2.23}/tests/test_version.py +1 -1
  25. chatup-0.2.23/tests/test_windows_adapter_review.py +52 -0
  26. chatup-0.2.23/tests/test_windows_checkpoint_review.py +75 -0
  27. chatup-0.2.23/tests/test_windows_compat.py +762 -0
  28. chatup-0.2.23/tests/test_windows_final_review.py +111 -0
  29. chatup-0.2.21/src/chatup/setup/nodejs.py +0 -466
  30. chatup-0.2.21/tests/test_windows_compat.py +0 -135
  31. {chatup-0.2.21 → chatup-0.2.23}/LICENSE +0 -0
  32. {chatup-0.2.21 → chatup-0.2.23}/setup.cfg +0 -0
  33. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/chrome_for_testing.py +0 -0
  34. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/chromedriver.py +0 -0
  35. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/cli.py +0 -0
  36. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/__init__.py +0 -0
  37. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/chrome_for_testing.py +0 -0
  38. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/chromedriver.py +0 -0
  39. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/desktop.py +0 -0
  40. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/macos.py +0 -0
  41. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/playwright.py +0 -0
  42. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/remotion.py +0 -0
  43. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/config.py +0 -0
  44. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/const.py +0 -0
  45. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/__init__.py +0 -0
  46. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/command_schema.py +0 -0
  47. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/policy.py +0 -0
  48. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/warnings.py +0 -0
  49. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/playwright.py +0 -0
  50. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/__init__.py +0 -0
  51. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/_managed_artifact.py +0 -0
  52. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/chrome_for_testing.py +0 -0
  53. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/chromedriver.py +0 -0
  54. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/__init__.py +0 -0
  55. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/hermes/install.sh +0 -0
  56. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/nvm.sh +0 -0
  57. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/.gitignore +0 -0
  58. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
  59. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package.json +0 -0
  60. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
  61. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cc_connect.py +0 -0
  62. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/chatgpt.py +0 -0
  63. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cli.py +0 -0
  64. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/crs.py +0 -0
  65. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/desktop.py +0 -0
  66. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/discourse.py +0 -0
  67. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/docker.py +0 -0
  68. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/gitea.py +0 -0
  69. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/mode_prompt.py +0 -0
  70. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/mysql.py +0 -0
  71. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/nginx.py +0 -0
  72. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/opencode.py +0 -0
  73. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/service_credentials.py +0 -0
  74. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/shell_rc.py +0 -0
  75. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/twikoo.py +0 -0
  76. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/uv.py +0 -0
  77. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/__init__.py +0 -0
  78. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/cli.py +0 -0
  79. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/core.py +0 -0
  80. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/options.py +0 -0
  81. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/render.py +0 -0
  82. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  83. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  84. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  85. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  86. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  87. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  88. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace.py +0 -0
  89. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/zsh.py +0 -0
  90. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/zulip.py +0 -0
  91. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/__init__.py +0 -0
  92. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/custom_logger.py +0 -0
  93. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/pathing.py +0 -0
  94. {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/platforming.py +0 -0
  95. {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/dependency_links.txt +0 -0
  96. {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/entry_points.txt +0 -0
  97. {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/top_level.txt +0 -0
  98. {chatup-0.2.21 → chatup-0.2.23}/tests/test_browser_backend_cli.py +0 -0
  99. {chatup-0.2.21 → chatup-0.2.23}/tests/test_chatgpt_setup.py +0 -0
  100. {chatup-0.2.21 → chatup-0.2.23}/tests/test_chrome_for_testing.py +0 -0
  101. {chatup-0.2.21 → chatup-0.2.23}/tests/test_chromedriver.py +0 -0
  102. {chatup-0.2.21 → chatup-0.2.23}/tests/test_cli_tree_docs.py +0 -0
  103. {chatup-0.2.21 → chatup-0.2.23}/tests/test_crs_setup.py +0 -0
  104. {chatup-0.2.21 → chatup-0.2.23}/tests/test_cursor_agent_setup.py +0 -0
  105. {chatup-0.2.21 → chatup-0.2.23}/tests/test_hermes_model_defaults.py +0 -0
  106. {chatup-0.2.21 → chatup-0.2.23}/tests/test_playwright.py +0 -0
  107. {chatup-0.2.21 → chatup-0.2.23}/tests/test_playwright_cli.py +0 -0
  108. {chatup-0.2.21 → chatup-0.2.23}/tests/test_publish_workflow.py +0 -0
  109. {chatup-0.2.21 → chatup-0.2.23}/tests/test_service_installers.py +0 -0
  110. {chatup-0.2.21 → chatup-0.2.23}/tests/test_twikoo_setup.py +0 -0
  111. {chatup-0.2.21 → chatup-0.2.23}/tests/test_uv_activation.py +0 -0
  112. {chatup-0.2.21 → chatup-0.2.23}/tests/test_workspace_setup.py +0 -0
  113. {chatup-0.2.21 → chatup-0.2.23}/tests/test_zsh_alias_names.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.21
3
+ Version: 0.2.23
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,7 +16,9 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle[tui]<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: ChatGlance<0.4.0,>=0.3.0
19
20
  Requires-Dist: python-dotenv<2.0,>=1.0
21
+ Requires-Dist: tomlkit<1.0,>=0.13
20
22
  Requires-Dist: colorama
21
23
  Provides-Extra: dev
22
24
  Requires-Dist: build; extra == "dev"
@@ -68,6 +70,8 @@ chatup workspace ~/Playground
68
70
 
69
71
  ```bash
70
72
  chatup gitea --force
73
+ chatup glance --dry-run
74
+ chatup glance
71
75
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
72
76
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
73
77
  chatup mysql
@@ -131,7 +135,9 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
131
135
  | 工作区脚手架 | `workspace` |
132
136
  | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
133
137
  | 视频项目 | `remotion` |
134
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
138
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
139
+
140
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
135
141
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
136
142
 
137
143
  桌面应用使用系统原生应用目录;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`。
@@ -37,6 +37,8 @@ chatup workspace ~/Playground
37
37
 
38
38
  ```bash
39
39
  chatup gitea --force
40
+ chatup glance --dry-run
41
+ chatup glance
40
42
  chatup discourse -e discourse-prod --hostname discourse.public.wzhecnu.cn
41
43
  chatup zulip -e zulip-prod --external-host zulip.public.wzhecnu.cn --port 3095
42
44
  chatup mysql
@@ -100,7 +102,9 @@ chatup chatgpt --yes # Windows 首次安装时接受 Store 协议
100
102
  | 工作区脚手架 | `workspace` |
101
103
  | 桌面应用 | `macos`(Snipaste/iTerm2/Chrome/Blender 多选)、`chrome`、`snipaste`(macOS/Windows)、`iterm`(仅 macOS)、`chatgpt`(含 Codex) |
102
104
  | 视频项目 | `remotion` |
103
- | 本地服务安装 | `gitea`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
105
+ | 本地服务安装 | `gitea`、`glance`、`discourse`、`zulip`、`mysql`、`nginx`、`crs` |
106
+
107
+ `chatup glance` 裸命令安装 `ChatArch/glance` 最新稳定维护版到有效 ChatArch home 下的 `glance`。当前仅支持 Linux amd64;严格验证归档、`SHA256SUMS`、`BUILDINFO.txt`、source SHA 和原始二进制版本,复用 ChatGlance portable API 初始化 loopback 配置。它不启动服务、不建立公网入口、不写密码;重复运行保留配置和数据,版本变更请使用 `chatglance runtime update`。
104
108
  | Agent 工具链 | `cc-connect`、`claude`、`codex`、`cursor-agent`、`opencode`、`hermes`、`lark-cli` |
105
109
 
106
110
  桌面应用使用系统原生应用目录;ChatArch 自管安装项默认目录收敛到 `~/.chatarch/...`,例如 `~/.chatarch/chrome-for-testing`、`~/.chatarch/chromedriver`、`~/.chatarch/playwright`、`~/.chatarch/chattea`、`~/.chatarch/discourse`、`~/.chatarch/zulip`、`~/.chatarch/chatdata`、`~/.chatarch/nginx` 和 `~/.chatarch/crs/local`。`cursor-agent` 额外注册 ChatEnv `CursorAgent` profile,`discourse`/`zulip` 注册管理员凭据 profile,并支持 `-e/--env` 从 env 文件或 profile 快速配置;显式选择 profile 时不会混入进程环境中的其他账号凭据。CLI 树由 ChatStyle 的注册表渲染。更完整的能力边界见 `docs/capability-map.md`。
@@ -13,7 +13,9 @@ dependencies = [
13
13
  "click>=8.0,<9.0",
14
14
  "chatstyle[tui]>=0.2.0,<0.3.0",
15
15
  "chatenv>=0.2.11,<0.3.0",
16
+ "ChatGlance>=0.3.0,<0.4.0",
16
17
  "python-dotenv>=1.0,<2.0",
18
+ "tomlkit>=0.13,<1.0",
17
19
  "colorama",
18
20
  ]
19
21
  authors = [{name = "rexwzh", email = "1073853456@qq.com"}]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.21"
5
+ __version__ = "0.2.23"
@@ -11,7 +11,12 @@ from datetime import datetime, timezone
11
11
  from pathlib import Path
12
12
  from typing import Any
13
13
 
14
- from chatup.setup.nodejs import MIN_NODEJS_MAJOR, _detect_nodejs_runtime
14
+ from chatup.setup.nodejs import (
15
+ MIN_NODEJS_MAJOR,
16
+ _detect_nodejs_runtime,
17
+ node_runtime_env,
18
+ npm_command_for_runtime,
19
+ )
15
20
 
16
21
  from ._managed_artifact import (
17
22
  ManagedArtifactError,
@@ -133,9 +138,7 @@ def _resolve_node_runtime(runtime_resolver: RuntimeResolver) -> dict[str, Any]:
133
138
 
134
139
 
135
140
  def _command_env(runtime: dict[str, Any], browsers_dir: Path) -> dict[str, str]:
136
- env = dict(os.environ)
137
- node_bin = Path(str(runtime["node_bin"]))
138
- env["PATH"] = str(node_bin.parent) + os.pathsep + env.get("PATH", "")
141
+ env = node_runtime_env(runtime)
139
142
  env["PLAYWRIGHT_BROWSERS_PATH"] = str(browsers_dir)
140
143
  env["PLAYWRIGHT_SKIP_BROWSER_GC"] = "1"
141
144
  return env
@@ -168,16 +171,18 @@ def _install_with_npm(
168
171
  env = _command_env(runtime, browsers_dir)
169
172
 
170
173
  npm_result = runner(
171
- [
172
- str(runtime["npm_bin"]),
173
- "install",
174
- "--prefix",
175
- str(package_dir),
176
- "--ignore-scripts",
177
- "--no-audit",
178
- "--no-fund",
179
- f"playwright@{version}",
180
- ],
174
+ npm_command_for_runtime(
175
+ runtime,
176
+ [
177
+ "install",
178
+ "--prefix",
179
+ str(package_dir),
180
+ "--ignore-scripts",
181
+ "--no-audit",
182
+ "--no-fund",
183
+ f"playwright@{version}",
184
+ ],
185
+ ),
181
186
  cwd=staging_dir,
182
187
  env=env,
183
188
  timeout=600,
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  from pathlib import Path
3
4
 
4
5
  from chatup.utils.platforming import chmod_private
@@ -7,6 +8,13 @@ DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
7
8
  DEFAULT_SMALL_FAST_MODEL = "claude-opus-4-6"
8
9
 
9
10
 
11
+ def resolve_claude_home() -> Path:
12
+ override = os.getenv("CLAUDE_CONFIG_DIR")
13
+ if override:
14
+ return Path(override).expanduser().resolve()
15
+ return Path.home() / ".claude"
16
+
17
+
10
18
  def _mask_secret(value):
11
19
  if not value:
12
20
  return ""
@@ -82,7 +90,7 @@ def setup_claude(
82
90
  from chatup.interaction import BACK_VALUE
83
91
 
84
92
  logger = setup_logger("setup_claude", log_level=str(log_level).upper())
85
- claude_dir = Path.home() / ".claude"
93
+ claude_dir = resolve_claude_home()
86
94
  existing = _load_existing_claude_config(claude_dir)
87
95
  existing_auth = existing.get("auth_token")
88
96
  logger.info("Start claude setup")
@@ -214,22 +222,38 @@ def setup_claude(
214
222
 
215
223
  claude_dir.mkdir(parents=True, exist_ok=True)
216
224
 
217
- settings_json = {
218
- "env": {
225
+ settings_path = claude_dir / "settings.json"
226
+ try:
227
+ settings_json = json.loads(settings_path.read_text(encoding="utf-8"))
228
+ except (OSError, ValueError, json.JSONDecodeError):
229
+ settings_json = {}
230
+ if not isinstance(settings_json, dict):
231
+ settings_json = {}
232
+ settings_env = settings_json.get("env")
233
+ if not isinstance(settings_env, dict):
234
+ settings_env = {}
235
+ settings_json["env"] = settings_env
236
+ settings_env.update(
237
+ {
219
238
  "ANTHROPIC_AUTH_TOKEN": auth_token,
220
239
  "ANTHROPIC_BASE_URL": base_url,
221
240
  "ANTHROPIC_SMALL_FAST_MODEL": small_fast_model,
222
241
  }
223
- }
224
- settings_path = claude_dir / "settings.json"
242
+ )
225
243
  settings_path.write_text(
226
244
  json.dumps(settings_json, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
227
245
  )
228
246
  chmod_private(settings_path)
229
247
  logger.info(f"Wrote settings file: {settings_path}")
230
248
 
231
- config_json = {"primaryApiKey": primary_api_key}
232
249
  config_path = claude_dir / "config.json"
250
+ try:
251
+ config_json = json.loads(config_path.read_text(encoding="utf-8"))
252
+ except (OSError, ValueError, json.JSONDecodeError):
253
+ config_json = {}
254
+ if not isinstance(config_json, dict):
255
+ config_json = {}
256
+ config_json["primaryApiKey"] = primary_api_key
233
257
  config_path.write_text(
234
258
  json.dumps(config_json, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
235
259
  )
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  import re
7
7
  import tempfile
8
8
  import click
9
+ import tomlkit
9
10
  from dotenv import dotenv_values
10
11
 
11
12
  from chatenv.configs import OpenAIConfig
@@ -32,7 +33,7 @@ from chatup.utils.platforming import chmod_private
32
33
 
33
34
  DEFAULT_MODEL = "gpt-5.6-terra"
34
35
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
35
- DEFAULT_AUTH_METHOD = "apikey"
36
+ DEFAULT_FORCED_LOGIN_METHOD = "api"
36
37
  _ENV_VAR_REF_RE = re.compile(r"\$(?:\{[^}]+\}|[A-Za-z_][A-Za-z0-9_]*)")
37
38
  logger = setup_logger("setup_codex")
38
39
 
@@ -54,9 +55,17 @@ def _mask_secret(value):
54
55
  return f"{value[:3]}{'*' * (len(value) - 7)}{value[-4:]}"
55
56
 
56
57
 
58
+ def resolve_codex_home() -> Path:
59
+ """Return the native Codex configuration root without changing user defaults."""
60
+ configured_home = os.getenv("CODEX_HOME")
61
+ if configured_home and configured_home.strip():
62
+ return Path(configured_home).expanduser()
63
+ return Path.home() / ".codex"
64
+
65
+
57
66
  def _load_existing_codex_config(codex_dir):
58
67
  existing = {
59
- "preferred_auth_method": None,
68
+ "forced_login_method": None,
60
69
  "openai_api_key": None,
61
70
  "model": None,
62
71
  "base_url": None,
@@ -96,7 +105,7 @@ def _load_existing_codex_config(codex_dir):
96
105
  value = value[1:-1]
97
106
  values[(section, key)] = value
98
107
 
99
- existing["preferred_auth_method"] = values.get((None, "preferred_auth_method"))
108
+ existing["forced_login_method"] = values.get((None, "forced_login_method"))
100
109
  existing["model"] = values.get((None, "model"))
101
110
  provider_name = values.get((None, "model_provider"))
102
111
  if provider_name:
@@ -185,32 +194,62 @@ def _upsert_toml_section(content: str, section: str | None, values: dict) -> tup
185
194
  return "\n".join(output).rstrip() + "\n", changed
186
195
 
187
196
 
197
+ def _remove_root_toml_keys(content: str, keys: set[str]) -> tuple[str, list[str]]:
198
+ """Remove only obsolete root-level keys while retaining other TOML tables."""
199
+ output: list[str] = []
200
+ removed: list[str] = []
201
+ section: str | None = None
202
+ for raw_line in content.splitlines():
203
+ stripped = raw_line.strip()
204
+ if stripped.startswith("[") and stripped.endswith("]"):
205
+ section = stripped[1:-1].strip()
206
+ elif section is None and "=" in stripped and not stripped.startswith("#"):
207
+ key = stripped.split("=", 1)[0].strip()
208
+ if key in keys:
209
+ removed.append(key)
210
+ continue
211
+ output.append(raw_line)
212
+ return "\n".join(output).rstrip() + "\n", removed
213
+
214
+
188
215
  def _write_codex_config(config_path: Path, *, model: str, base_url: str) -> list[str]:
189
216
  content = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
190
- content, changed_root = _upsert_toml_section(
191
- content,
192
- None,
193
- {
194
- "model_provider": "crs",
195
- "model": model,
196
- "preferred_auth_method": DEFAULT_AUTH_METHOD,
197
- },
198
- )
199
- content, changed_provider = _upsert_toml_section(
200
- content,
201
- "model_providers.crs",
202
- {
203
- "name": "crs",
204
- "base_url": base_url,
205
- "wire_api": "responses",
206
- "requires_openai_auth": True,
207
- },
208
- )
209
- config_path.parent.mkdir(parents=True, exist_ok=True)
210
- config_path.write_text(content, encoding="utf-8")
211
- chmod_private(config_path)
217
+ try:
218
+ document = tomlkit.parse(content)
219
+ except tomlkit.exceptions.ParseError:
220
+ raise click.ClickException("Existing Codex config.toml is invalid; it was not modified.") from None
221
+ changed = []
222
+ if "preferred_auth_method" in document:
223
+ del document["preferred_auth_method"]
224
+ changed.append("preferred_auth_method")
225
+ root_values = {
226
+ "model_provider": "crs",
227
+ "model": model,
228
+ "forced_login_method": DEFAULT_FORCED_LOGIN_METHOD,
229
+ }
230
+ for key, value in root_values.items():
231
+ if document.get(key) != value:
232
+ changed.append(key)
233
+ document[key] = value
234
+ providers = document.setdefault("model_providers", tomlkit.table())
235
+ if not isinstance(providers, (tomlkit.items.Table, tomlkit.items.InlineTable)):
236
+ raise click.ClickException("Codex model_providers must be a TOML table; config was not modified.")
237
+ provider = providers.setdefault("crs", tomlkit.table())
238
+ if not isinstance(provider, (tomlkit.items.Table, tomlkit.items.InlineTable)):
239
+ raise click.ClickException("Codex model_providers.crs must be a TOML table; config was not modified.")
240
+ values = {
241
+ "name": "crs",
242
+ "base_url": base_url,
243
+ "wire_api": "responses",
244
+ "requires_openai_auth": True,
245
+ }
246
+ for key, value in values.items():
247
+ if provider.get(key) != value:
248
+ changed.append("model_providers.crs." + key)
249
+ provider[key] = value
250
+ _write_private_text(config_path, tomlkit.dumps(document))
212
251
  logger.info(f"Patched config file: {config_path}")
213
- return changed_root + changed_provider
252
+ return changed
214
253
 
215
254
 
216
255
  def _write_private_text(path: Path, content: str) -> None:
@@ -364,7 +403,7 @@ def setup_codex(
364
403
  log_level="INFO",
365
404
  ):
366
405
  _configure_logger(log_level)
367
- codex_dir = Path.home() / ".codex"
406
+ codex_dir = resolve_codex_home()
368
407
  existing = _load_existing_codex_config(codex_dir)
369
408
  env_values, typed_env_values = split_config_sources(
370
409
  OpenAIConfig,
@@ -17,6 +17,7 @@ from chatup.utils.custom_logger import setup_logger
17
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
+ WINDOWS_INSTALL_COMMAND = "irm 'https://cursor.com/install?win32=true' | iex"
20
21
  CREDENTIAL_STORE_CHOICES = ("native", "file-wrapper")
21
22
  CURSOR_ACCESS_TOKEN_KEY = "CURSOR_ACCESS_TOKEN"
22
23
  CURSOR_REFRESH_TOKEN_KEY = "CURSOR_REFRESH_TOKEN"
@@ -167,12 +168,20 @@ def _resolve_command_or_user_bin(name: str) -> Path | None:
167
168
  candidates.append(Path(found).expanduser())
168
169
  candidates.extend(user_bin_candidates(name))
169
170
  if is_windows():
171
+ local_app_data = os.environ.get("LOCALAPPDATA")
170
172
  candidates.extend(
171
173
  [
172
174
  Path.home() / ".local" / "bin" / f"{name}.cmd",
173
175
  Path.home() / ".local" / "bin" / f"{name}.bat",
174
176
  ]
175
177
  )
178
+ if local_app_data:
179
+ candidates.extend(
180
+ [
181
+ Path(local_app_data) / "cursor-agent" / f"{name}.cmd",
182
+ Path(local_app_data) / "cursor-agent" / f"{name}.exe",
183
+ ]
184
+ )
176
185
  for candidate in candidates:
177
186
  if candidate.exists() and (is_windows() or os.access(candidate, os.X_OK)):
178
187
  return candidate
@@ -311,9 +320,40 @@ def _install_cursor_agent_if_needed(*, install_url: str = DEFAULT_INSTALL_URL) -
311
320
  if _resolve_cursor_agent_binary():
312
321
  return False
313
322
  if is_windows():
314
- raise click.ClickException(
315
- "Cursor Agent was not found on PATH. Install Cursor Agent for Windows first, then rerun ChatUp."
323
+ if install_url != DEFAULT_INSTALL_URL:
324
+ raise click.ClickException("Windows Cursor Agent installs use the official Cursor installer URL only.")
325
+ local_app_data = os.environ.get("LOCALAPPDATA")
326
+ if not local_app_data:
327
+ raise click.ClickException("LOCALAPPDATA is required for the Windows Cursor Agent installer.")
328
+ target = Path(local_app_data) / "cursor-agent"
329
+ if target.exists() or target.is_symlink():
330
+ raise click.ClickException(
331
+ f"The official Cursor installer deletes the existing directory {target}; "
332
+ "refusing to run automatically. Preserve or migrate it manually first."
333
+ )
334
+ script = subprocess.run(
335
+ [
336
+ "powershell",
337
+ "-NoProfile",
338
+ "-NonInteractive",
339
+ "-ExecutionPolicy",
340
+ "Bypass",
341
+ "-Command",
342
+ WINDOWS_INSTALL_COMMAND,
343
+ ],
344
+ text=True,
345
+ capture_output=True,
346
+ timeout=300,
316
347
  )
348
+ if script.returncode != 0:
349
+ raise click.ClickException(
350
+ f"Cursor Agent Windows installer failed (exit {script.returncode}); see the official installer logs."
351
+ )
352
+ if not _resolve_cursor_agent_binary():
353
+ raise click.ClickException(
354
+ "Cursor Agent Windows installer finished but cursor-agent was not found in PATH or %LOCALAPPDATA%\\cursor-agent."
355
+ )
356
+ return True
317
357
  script = subprocess.run(
318
358
  ["bash", "-lc", f"curl -fsSL {install_url!r} | bash"],
319
359
  text=True,
@@ -26,6 +26,7 @@ from chatup.setup.discourse import (
26
26
  setup_discourse,
27
27
  )
28
28
  from chatup.setup.frp import setup_frp
29
+ from chatup.setup.glance import DEFAULT_GLANCE_VERSION, setup_glance
29
30
  from chatup.setup.gitea import (
30
31
  DEFAULT_BASE_URL as DEFAULT_GITEA_BASE_URL,
31
32
  DEFAULT_DATABASE_BACKEND as DEFAULT_GITEA_DATABASE_BACKEND,
@@ -231,6 +232,15 @@ def gitea_setup(
231
232
  )
232
233
 
233
234
 
235
+ def glance_setup(version, runtime_home, dry_run, interactive):
236
+ return setup_glance(
237
+ version=version,
238
+ runtime_home=runtime_home,
239
+ dry_run=dry_run,
240
+ interactive=interactive,
241
+ )
242
+
243
+
234
244
  def discourse_setup(
235
245
  home,
236
246
  hostname,
@@ -612,6 +622,37 @@ SETUP_COMMAND_ELEMENTS = (
612
622
  ),
613
623
  ),
614
624
 
625
+ SetupCommandElement(
626
+ name="glance",
627
+ help="Install the verified maintained ChatArch Glance runtime without starting it.",
628
+ callback=glance_setup,
629
+ options=(
630
+ SetupOptionElement(
631
+ param_decls=("--version",),
632
+ kwargs={
633
+ "default": DEFAULT_GLANCE_VERSION,
634
+ "show_default": True,
635
+ "help": "Maintained release: latest or exact chatarch-vMAJOR.MINOR.PATCH.",
636
+ },
637
+ ),
638
+ SetupOptionElement(
639
+ param_decls=("--runtime-home",),
640
+ kwargs={
641
+ "default": None,
642
+ "type": click.Path(path_type=Path),
643
+ "help": "Runtime root; defaults to the active ChatArch home/glance.",
644
+ },
645
+ ),
646
+ SetupOptionElement(
647
+ param_decls=("--dry-run",),
648
+ kwargs={"is_flag": True, "help": "Show paths without network access or writes."},
649
+ ),
650
+ SetupOptionElement(
651
+ param_decls=("--interactive/--no-interactive", "-i/-I"),
652
+ kwargs={"default": None, "help": INTERACTIVE_OPTION_HELP},
653
+ ),
654
+ ),
655
+ ),
615
656
  SetupCommandElement(
616
657
  name="gitea",
617
658
  help="Install ChatArch Gitea and optionally write a local app.ini/user service.",
@@ -1230,7 +1271,7 @@ SETUP_COMMAND_ELEMENTS = (
1230
1271
  ),
1231
1272
  SetupCommandElement(
1232
1273
  name="nodejs",
1233
- help="Install nvm and Node.js (default LTS).",
1274
+ help="Install default LTS Node.js (nvm on POSIX; ChatArch portable ZIP on Windows).",
1234
1275
  callback=nodejs_setup,
1235
1276
  options=(
1236
1277
  LOG_LEVEL_OPTION,
@@ -1299,7 +1340,7 @@ SETUP_COMMAND_ELEMENTS = (
1299
1340
  ),
1300
1341
  SetupCommandElement(
1301
1342
  name="codex",
1302
- help="Configure Codex CLI and config files.",
1343
+ help="Configure Codex CLI and config files (uses CODEX_HOME when set).",
1303
1344
  callback=codex_setup,
1304
1345
  options=(
1305
1346
  LOG_LEVEL_OPTION,
@@ -32,10 +32,10 @@ def _configure_logger(log_level="INFO"):
32
32
 
33
33
 
34
34
  def get_system_arch():
35
- arch = platform.machine()
36
- if arch == "x86_64":
35
+ arch = platform.machine().lower()
36
+ if arch in {"x86_64", "amd64"}:
37
37
  return "amd64"
38
- elif arch == "aarch64":
38
+ elif arch in {"aarch64", "arm64"}:
39
39
  return "arm64"
40
40
  elif arch == "armv7l":
41
41
  return "arm"
@@ -139,6 +139,8 @@ def setup_frp(interactive=None, log_level="INFO"):
139
139
  work_dir.mkdir(parents=True, exist_ok=True)
140
140
 
141
141
  final_bin_path = work_dir / binary_name
142
+ if final_bin_path.exists() or final_bin_path.is_symlink():
143
+ raise click.ClickException(f"Refusing to overwrite existing FRP binary: {final_bin_path}")
142
144
 
143
145
  if install_method == "Download":
144
146
  version = ask_text("Enter FRP Version:", default=FRP_VERSION_DEFAULT)
@@ -161,8 +163,7 @@ def setup_frp(interactive=None, log_level="INFO"):
161
163
  shutil.copy2(extracted_bin, final_bin_path)
162
164
  except Exception as e:
163
165
  logger.error(f"Error downloading or extracting FRP: {e}")
164
- print(f"Error downloading/extracting: {e}")
165
- return
166
+ raise click.ClickException(f"Error downloading/extracting FRP: {e}") from e
166
167
 
167
168
  else: # Local File
168
169
  local_path = ask_path("Enter path to local FRP archive (tar.gz or zip):")
@@ -171,8 +172,7 @@ def setup_frp(interactive=None, log_level="INFO"):
171
172
 
172
173
  if not os.path.exists(local_path):
173
174
  logger.error(f"Local FRP archive not found: {local_path}")
174
- print("File not found.")
175
- return
175
+ raise click.ClickException(f"FRP archive not found: {local_path}")
176
176
 
177
177
  with tempfile.TemporaryDirectory() as tmpdir:
178
178
  try:
@@ -180,8 +180,7 @@ def setup_frp(interactive=None, log_level="INFO"):
180
180
  shutil.copy2(extracted_bin, final_bin_path)
181
181
  except Exception as e:
182
182
  logger.error(f"Error extracting FRP from local archive: {e}")
183
- print(f"Error extracting: {e}")
184
- return
183
+ raise click.ClickException(f"Error extracting FRP: {e}") from e
185
184
 
186
185
  logger.info(f"FRP binary installed at: {final_bin_path}")
187
186
  print(f"FRP binary installed at: {final_bin_path}")
@@ -255,6 +254,8 @@ auth.token = "{token}"
255
254
  """
256
255
 
257
256
  config_path = work_dir / config_name
257
+ if config_path.exists() or config_path.is_symlink():
258
+ raise click.ClickException(f"Refusing to overwrite existing FRP configuration: {config_path}")
258
259
  with open(config_path, "w") as f:
259
260
  f.write(config_content)
260
261