chatup 0.2.22__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.22/src/chatup.egg-info → chatup-0.2.23}/PKG-INFO +2 -1
- {chatup-0.2.22 → chatup-0.2.23}/pyproject.toml +1 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/__init__.py +1 -1
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/runtime/playwright.py +19 -14
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/claude.py +30 -6
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/codex.py +66 -27
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/cursor_agent.py +42 -2
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/elements.py +2 -2
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/frp.py +10 -9
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/hermes.py +43 -6
- {chatup-0.2.22 → 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.22 → chatup-0.2.23}/src/chatup/setup/remotion.py +34 -16
- {chatup-0.2.22 → chatup-0.2.23/src/chatup.egg-info}/PKG-INFO +2 -1
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup.egg-info/SOURCES.txt +3 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup.egg-info/requires.txt +1 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_codex_setup.py +68 -0
- {chatup-0.2.22 → 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.22/src/chatup/setup/nodejs.py +0 -466
- chatup-0.2.22/tests/test_windows_compat.py +0 -135
- {chatup-0.2.22 → chatup-0.2.23}/LICENSE +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/README.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/setup.cfg +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/chrome_for_testing.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/chromedriver.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/chrome_for_testing.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/chromedriver.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/desktop.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/macos.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/playwright.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/commands/remotion.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/config.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/const.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/interaction/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/interaction/command_schema.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/interaction/policy.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/interaction/warnings.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/playwright.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/runtime/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/runtime/_managed_artifact.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/runtime/chrome_for_testing.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/runtime/chromedriver.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/hermes/install.sh +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/nvm.sh +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/remotion/.gitignore +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package-lock.json +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/remotion/package.json +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/assets/remotion/src/index.jsx +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/cc_connect.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/chatgpt.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/crs.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/desktop.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/discourse.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/docker.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/gitea.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/glance.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/mode_prompt.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/mysql.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/nginx.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/opencode.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/service_credentials.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/shell_rc.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/twikoo.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/uv.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/core.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/options.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/render.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/workspace.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/zsh.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/setup/zulip.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/utils/__init__.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/utils/custom_logger.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/utils/pathing.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup/utils/platforming.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup.egg-info/dependency_links.txt +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup.egg-info/entry_points.txt +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/src/chatup.egg-info/top_level.txt +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_browser_backend_cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_chatgpt_setup.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_chrome_for_testing.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_chromedriver.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_cli_tree_docs.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_crs_setup.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_cursor_agent_setup.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_glance_bridge.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_glance_setup.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_hermes_model_defaults.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_playwright.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_playwright_cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_publish_workflow.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_pyproject_metadata.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_service_installers.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_setup_cli.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_twikoo_setup.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_uv_activation.py +0 -0
- {chatup-0.2.22 → chatup-0.2.23}/tests/test_workspace_setup.py +0 -0
- {chatup-0.2.22 → 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
|
|
@@ -18,6 +18,7 @@ Requires-Dist: chatstyle[tui]<0.3.0,>=0.2.0
|
|
|
18
18
|
Requires-Dist: chatenv<0.3.0,>=0.2.11
|
|
19
19
|
Requires-Dist: ChatGlance<0.4.0,>=0.3.0
|
|
20
20
|
Requires-Dist: python-dotenv<2.0,>=1.0
|
|
21
|
+
Requires-Dist: tomlkit<1.0,>=0.13
|
|
21
22
|
Requires-Dist: colorama
|
|
22
23
|
Provides-Extra: dev
|
|
23
24
|
Requires-Dist: build; extra == "dev"
|
|
@@ -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,
|
|
@@ -1271,7 +1271,7 @@ SETUP_COMMAND_ELEMENTS = (
|
|
|
1271
1271
|
),
|
|
1272
1272
|
SetupCommandElement(
|
|
1273
1273
|
name="nodejs",
|
|
1274
|
-
help="Install
|
|
1274
|
+
help="Install default LTS Node.js (nvm on POSIX; ChatArch portable ZIP on Windows).",
|
|
1275
1275
|
callback=nodejs_setup,
|
|
1276
1276
|
options=(
|
|
1277
1277
|
LOG_LEVEL_OPTION,
|
|
@@ -1340,7 +1340,7 @@ SETUP_COMMAND_ELEMENTS = (
|
|
|
1340
1340
|
),
|
|
1341
1341
|
SetupCommandElement(
|
|
1342
1342
|
name="codex",
|
|
1343
|
-
help="Configure Codex CLI and config files.",
|
|
1343
|
+
help="Configure Codex CLI and config files (uses CODEX_HOME when set).",
|
|
1344
1344
|
callback=codex_setup,
|
|
1345
1345
|
options=(
|
|
1346
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
|
|
|
@@ -16,13 +16,16 @@ from chatenv.source_chain import split_config_sources
|
|
|
16
16
|
from chatup.const import CHATARCH_ENV_DIR
|
|
17
17
|
from chatup.interaction import abort_if_force_without_tty, resolve_interactive_mode, resolve_value
|
|
18
18
|
from chatup.utils.custom_logger import setup_logger
|
|
19
|
-
from chatup.utils.platforming import chmod_private, is_windows
|
|
19
|
+
from chatup.utils.platforming import chmod_private, is_windows
|
|
20
20
|
|
|
21
21
|
logger = setup_logger("setup_hermes")
|
|
22
22
|
|
|
23
23
|
CHATARCH_HERMES_INSTALLER_URL = (
|
|
24
24
|
"https://raw.githubusercontent.com/ChatArch/hermes-agent/main/scripts/install.sh"
|
|
25
25
|
)
|
|
26
|
+
CHATARCH_HERMES_WINDOWS_INSTALLER_URL = (
|
|
27
|
+
"https://raw.githubusercontent.com/ChatArch/hermes-agent/main/scripts/install.ps1"
|
|
28
|
+
)
|
|
26
29
|
DEFAULT_MODEL = "gpt-5.6-terra"
|
|
27
30
|
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
|
28
31
|
DEFAULT_WEBUI_HOST = "127.0.0.1"
|
|
@@ -59,19 +62,22 @@ def _sha256(path: Path) -> str:
|
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
def _cache_installer_path() -> Path:
|
|
62
|
-
|
|
65
|
+
home = Path(os.environ.get("CHATARCH_HOME", str(Path.home() / ".chatarch"))).expanduser()
|
|
66
|
+
return home / "cache" / "chatup" / "hermes" / ("install.ps1" if is_windows() else "install.sh")
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
def _packaged_installer_path() -> Path:
|
|
66
|
-
|
|
70
|
+
name = "install.ps1" if is_windows() else "install.sh"
|
|
71
|
+
return Path(__file__).resolve().parent / "assets" / "hermes" / name
|
|
67
72
|
|
|
68
73
|
|
|
69
74
|
def _download_installer() -> Path:
|
|
70
75
|
target = _cache_installer_path()
|
|
71
76
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
72
77
|
tmp = target.with_suffix(".tmp")
|
|
78
|
+
url = CHATARCH_HERMES_WINDOWS_INSTALLER_URL if is_windows() else CHATARCH_HERMES_INSTALLER_URL
|
|
73
79
|
logger.info("Downloading ChatArch Hermes installer")
|
|
74
|
-
with urllib.request.urlopen(
|
|
80
|
+
with urllib.request.urlopen(url, timeout=60) as response:
|
|
75
81
|
tmp.write_bytes(response.read())
|
|
76
82
|
chmod_private(tmp)
|
|
77
83
|
tmp.replace(target)
|
|
@@ -96,6 +102,11 @@ def _resolve_installer(installer: str | None, update_installer: bool) -> Path:
|
|
|
96
102
|
if packaged.is_file():
|
|
97
103
|
return packaged
|
|
98
104
|
|
|
105
|
+
if is_windows():
|
|
106
|
+
# The fork's PowerShell installer is the native Windows bootstrap;
|
|
107
|
+
# cache it under CHATARCH_HOME rather than falling back to Bash/WSL.
|
|
108
|
+
return _download_installer()
|
|
109
|
+
|
|
99
110
|
raise click.ClickException(
|
|
100
111
|
"Hermes installer is unavailable. Pass --installer PATH or run with --update-installer."
|
|
101
112
|
)
|
|
@@ -329,10 +340,36 @@ def _hermes_installed(hermes_home: Path) -> bool:
|
|
|
329
340
|
|
|
330
341
|
|
|
331
342
|
def _run_installer(installer_path: Path, hermes_home: Path) -> None:
|
|
332
|
-
require_non_windows("Hermes shell installer")
|
|
333
343
|
env = os.environ.copy()
|
|
334
344
|
env["HERMES_HOME"] = str(hermes_home)
|
|
335
|
-
|
|
345
|
+
if is_windows():
|
|
346
|
+
if installer_path.suffix.lower() != ".ps1":
|
|
347
|
+
raise click.ClickException("Windows Hermes installer must be a PowerShell .ps1 script.")
|
|
348
|
+
command = [
|
|
349
|
+
"powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(installer_path),
|
|
350
|
+
"-NonInteractive", "-HermesHome", str(hermes_home), "-InstallDir", str(hermes_home / "hermes-agent"),
|
|
351
|
+
]
|
|
352
|
+
readback = subprocess.run(command + ["-ShowResolvedPaths"], env=env, text=True, capture_output=True)
|
|
353
|
+
if readback.returncode != 0:
|
|
354
|
+
raise click.ClickException(f"Hermes Windows installer path readback failed (exit {readback.returncode}).")
|
|
355
|
+
try:
|
|
356
|
+
import json as _json
|
|
357
|
+
resolved = _json.loads(readback.stdout)
|
|
358
|
+
home_resolved = resolved.get("hermes_home")
|
|
359
|
+
install_resolved = resolved.get("install_dir")
|
|
360
|
+
if not isinstance(home_resolved, str) or not isinstance(install_resolved, str):
|
|
361
|
+
raise ValueError("missing installer path fields")
|
|
362
|
+
# Drive-letter case and slash direction may differ on Windows.
|
|
363
|
+
import ntpath
|
|
364
|
+
def same_path(a, b):
|
|
365
|
+
return ntpath.normcase(ntpath.normpath(str(a))) == ntpath.normcase(ntpath.normpath(str(b)))
|
|
366
|
+
if not same_path(home_resolved, hermes_home) or not same_path(install_resolved, hermes_home / "hermes-agent"):
|
|
367
|
+
raise ValueError("resolved Hermes paths differ from requested paths")
|
|
368
|
+
except (ValueError, TypeError):
|
|
369
|
+
raise click.ClickException("Hermes Windows installer path readback did not match the requested home/install directory.") from None
|
|
370
|
+
click.echo("Hermes installer resolved paths read back successfully.")
|
|
371
|
+
else:
|
|
372
|
+
command = ["bash", str(installer_path)]
|
|
336
373
|
click.echo(f"Running Hermes installer: {_display_command(command)}")
|
|
337
374
|
result = subprocess.run(command, env=env, text=True)
|
|
338
375
|
if result.returncode != 0:
|
|
@@ -25,6 +25,7 @@ from chatup.interaction import (
|
|
|
25
25
|
from chatup.setup.nodejs import (
|
|
26
26
|
_detect_nodejs_runtime,
|
|
27
27
|
ensure_nodejs_requirement,
|
|
28
|
+
managed_npm_launcher,
|
|
28
29
|
run_npm_command,
|
|
29
30
|
should_install_global_npm_package,
|
|
30
31
|
)
|
|
@@ -229,7 +230,8 @@ def _run_lark_cli_command(
|
|
|
229
230
|
text=True,
|
|
230
231
|
)
|
|
231
232
|
|
|
232
|
-
|
|
233
|
+
launcher = managed_npm_launcher(runtime, "lark-cli")
|
|
234
|
+
command = str(launcher) if launcher is not None else (shutil.which("lark-cli") or "lark-cli")
|
|
233
235
|
return subprocess.run(
|
|
234
236
|
[command, *args],
|
|
235
237
|
input=input_text,
|