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.
- {chatup-0.2.21/src/chatup.egg-info → chatup-0.2.23}/PKG-INFO +8 -2
- {chatup-0.2.21 → chatup-0.2.23}/README.md +5 -1
- {chatup-0.2.21 → chatup-0.2.23}/pyproject.toml +2 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/__init__.py +1 -1
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/playwright.py +19 -14
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/claude.py +30 -6
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/codex.py +66 -27
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cursor_agent.py +42 -2
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/elements.py +43 -2
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/frp.py +10 -9
- chatup-0.2.23/src/chatup/setup/glance.py +240 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/hermes.py +43 -6
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/lark_cli.py +3 -1
- chatup-0.2.23/src/chatup/setup/nodejs.py +1009 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/remotion.py +34 -16
- {chatup-0.2.21 → chatup-0.2.23/src/chatup.egg-info}/PKG-INFO +8 -2
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/SOURCES.txt +6 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/requires.txt +2 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_codex_setup.py +68 -0
- chatup-0.2.23/tests/test_glance_bridge.py +6 -0
- chatup-0.2.23/tests/test_glance_setup.py +190 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_pyproject_metadata.py +1 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_setup_cli.py +2 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_version.py +1 -1
- chatup-0.2.23/tests/test_windows_adapter_review.py +52 -0
- chatup-0.2.23/tests/test_windows_checkpoint_review.py +75 -0
- chatup-0.2.23/tests/test_windows_compat.py +762 -0
- chatup-0.2.23/tests/test_windows_final_review.py +111 -0
- chatup-0.2.21/src/chatup/setup/nodejs.py +0 -466
- chatup-0.2.21/tests/test_windows_compat.py +0 -135
- {chatup-0.2.21 → chatup-0.2.23}/LICENSE +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/setup.cfg +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/desktop.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/macos.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/commands/remotion.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/config.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/const.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/.gitignore +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package.json +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/chatgpt.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/desktop.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_browser_backend_cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_chatgpt_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_cli_tree_docs.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_hermes_model_defaults.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_playwright.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_service_installers.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_uv_activation.py +0 -0
- {chatup-0.2.21 → chatup-0.2.23}/tests/test_workspace_setup.py +0 -0
- {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.
|
|
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"}]
|
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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["
|
|
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
|
-
|
|
191
|
-
content
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
315
|
-
"Cursor Agent
|
|
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
|
|
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
|
|
35
|
+
arch = platform.machine().lower()
|
|
36
|
+
if arch in {"x86_64", "amd64"}:
|
|
37
37
|
return "amd64"
|
|
38
|
-
elif arch
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|