chatup 0.2.13__tar.gz → 0.2.14__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 (89) hide show
  1. {chatup-0.2.13/src/chatup.egg-info → chatup-0.2.14}/PKG-INFO +2 -1
  2. {chatup-0.2.13 → chatup-0.2.14}/pyproject.toml +1 -0
  3. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/__init__.py +1 -1
  4. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/codex.py +108 -24
  5. {chatup-0.2.13 → chatup-0.2.14/src/chatup.egg-info}/PKG-INFO +2 -1
  6. {chatup-0.2.13 → chatup-0.2.14}/src/chatup.egg-info/requires.txt +1 -0
  7. {chatup-0.2.13 → chatup-0.2.14}/tests/test_codex_setup.py +146 -0
  8. {chatup-0.2.13 → chatup-0.2.14}/tests/test_pyproject_metadata.py +1 -0
  9. {chatup-0.2.13 → chatup-0.2.14}/tests/test_version.py +2 -2
  10. {chatup-0.2.13 → chatup-0.2.14}/LICENSE +0 -0
  11. {chatup-0.2.13 → chatup-0.2.14}/README.md +0 -0
  12. {chatup-0.2.13 → chatup-0.2.14}/setup.cfg +0 -0
  13. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/chrome_for_testing.py +0 -0
  14. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/chromedriver.py +0 -0
  15. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/cli.py +0 -0
  16. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/commands/__init__.py +0 -0
  17. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/commands/chrome_for_testing.py +0 -0
  18. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/commands/chromedriver.py +0 -0
  19. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/commands/playwright.py +0 -0
  20. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/config.py +0 -0
  21. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/const.py +0 -0
  22. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/interaction/__init__.py +0 -0
  23. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/interaction/command_schema.py +0 -0
  24. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/interaction/policy.py +0 -0
  25. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/interaction/warnings.py +0 -0
  26. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/playwright.py +0 -0
  27. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/runtime/__init__.py +0 -0
  28. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/runtime/_managed_artifact.py +0 -0
  29. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/runtime/chrome_for_testing.py +0 -0
  30. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/runtime/chromedriver.py +0 -0
  31. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/runtime/playwright.py +0 -0
  32. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/__init__.py +0 -0
  33. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/assets/hermes/install.sh +0 -0
  34. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/assets/nvm.sh +0 -0
  35. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/cc_connect.py +0 -0
  36. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/claude.py +0 -0
  37. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/cli.py +0 -0
  38. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/crs.py +0 -0
  39. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/cursor_agent.py +0 -0
  40. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/discourse.py +0 -0
  41. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/docker.py +0 -0
  42. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/elements.py +0 -0
  43. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/frp.py +0 -0
  44. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/gitea.py +0 -0
  45. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/hermes.py +0 -0
  46. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/lark_cli.py +0 -0
  47. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/mode_prompt.py +0 -0
  48. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/mysql.py +0 -0
  49. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/nginx.py +0 -0
  50. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/nodejs.py +0 -0
  51. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/opencode.py +0 -0
  52. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/service_credentials.py +0 -0
  53. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/shell_rc.py +0 -0
  54. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/twikoo.py +0 -0
  55. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/uv.py +0 -0
  56. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/__init__.py +0 -0
  57. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/cli.py +0 -0
  58. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/core.py +0 -0
  59. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/options.py +0 -0
  60. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/render.py +0 -0
  61. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  62. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  63. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  64. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  65. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  66. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  67. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/workspace.py +0 -0
  68. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/zsh.py +0 -0
  69. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/setup/zulip.py +0 -0
  70. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/utils/__init__.py +0 -0
  71. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/utils/custom_logger.py +0 -0
  72. {chatup-0.2.13 → chatup-0.2.14}/src/chatup/utils/pathing.py +0 -0
  73. {chatup-0.2.13 → chatup-0.2.14}/src/chatup.egg-info/SOURCES.txt +0 -0
  74. {chatup-0.2.13 → chatup-0.2.14}/src/chatup.egg-info/dependency_links.txt +0 -0
  75. {chatup-0.2.13 → chatup-0.2.14}/src/chatup.egg-info/entry_points.txt +0 -0
  76. {chatup-0.2.13 → chatup-0.2.14}/src/chatup.egg-info/top_level.txt +0 -0
  77. {chatup-0.2.13 → chatup-0.2.14}/tests/test_browser_backend_cli.py +0 -0
  78. {chatup-0.2.13 → chatup-0.2.14}/tests/test_chrome_for_testing.py +0 -0
  79. {chatup-0.2.13 → chatup-0.2.14}/tests/test_chromedriver.py +0 -0
  80. {chatup-0.2.13 → chatup-0.2.14}/tests/test_cli_tree_docs.py +0 -0
  81. {chatup-0.2.13 → chatup-0.2.14}/tests/test_crs_setup.py +0 -0
  82. {chatup-0.2.13 → chatup-0.2.14}/tests/test_cursor_agent_setup.py +0 -0
  83. {chatup-0.2.13 → chatup-0.2.14}/tests/test_playwright.py +0 -0
  84. {chatup-0.2.13 → chatup-0.2.14}/tests/test_playwright_cli.py +0 -0
  85. {chatup-0.2.13 → chatup-0.2.14}/tests/test_publish_workflow.py +0 -0
  86. {chatup-0.2.13 → chatup-0.2.14}/tests/test_service_installers.py +0 -0
  87. {chatup-0.2.13 → chatup-0.2.14}/tests/test_setup_cli.py +0 -0
  88. {chatup-0.2.13 → chatup-0.2.14}/tests/test_twikoo_setup.py +0 -0
  89. {chatup-0.2.13 → chatup-0.2.14}/tests/test_workspace_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.13
3
+ Version: 0.2.14
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: python-dotenv<2.0,>=1.0
19
20
  Requires-Dist: colorama
20
21
  Provides-Extra: dev
21
22
  Requires-Dist: build; extra == "dev"
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "click>=8.0,<9.0",
14
14
  "chatstyle>=0.2.0,<0.3.0",
15
15
  "chatenv>=0.2.11,<0.3.0",
16
+ "python-dotenv>=1.0,<2.0",
16
17
  "colorama",
17
18
  ]
18
19
  authors = [{name = "rexwzh", email = "1073853456@qq.com"}]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.13"
5
+ __version__ = "0.2.14"
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  from pathlib import Path
6
+ import re
7
+ import tempfile
5
8
  import click
9
+ from dotenv import dotenv_values
6
10
 
7
11
  from chatenv.configs import OpenAIConfig
8
12
  from chatenv.fields import BaseEnvConfig
9
13
  from chatenv.source_chain import split_config_sources
10
- from chatenv.store import EnvStore
11
14
  from chatup.const import CHATARCH_ENV_DIR, CHATARCH_ENV_FILE
12
15
  from chatup.interaction import (
13
16
  BACK_VALUE,
@@ -29,6 +32,7 @@ from chatup.utils.custom_logger import setup_logger
29
32
  DEFAULT_MODEL = "gpt-5.5"
30
33
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
31
34
  DEFAULT_AUTH_METHOD = "apikey"
35
+ _ENV_VAR_REF_RE = re.compile(r"\$(?:\{[^}]+\}|[A-Za-z_][A-Za-z0-9_]*)")
32
36
  logger = setup_logger("setup_codex")
33
37
 
34
38
 
@@ -208,6 +212,29 @@ def _write_codex_config(config_path: Path, *, model: str, base_url: str) -> list
208
212
  return changed_root + changed_provider
209
213
 
210
214
 
215
+ def _write_private_text(path: Path, content: str) -> None:
216
+ path.parent.mkdir(parents=True, exist_ok=True)
217
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
218
+ try:
219
+ os.chmod(tmp_name, 0o600)
220
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
221
+ handle.write(content)
222
+ handle.flush()
223
+ os.fsync(handle.fileno())
224
+ os.replace(tmp_name, path)
225
+ path.chmod(0o600)
226
+ except Exception:
227
+ try:
228
+ os.close(fd)
229
+ except OSError:
230
+ pass
231
+ try:
232
+ os.unlink(tmp_name)
233
+ except OSError:
234
+ pass
235
+ raise
236
+
237
+
211
238
  def _write_codex_auth(auth_path: Path, api_key: str) -> list[str]:
212
239
  auth_data = {}
213
240
  if auth_path.exists():
@@ -219,11 +246,9 @@ def _write_codex_auth(auth_path: Path, api_key: str) -> list[str]:
219
246
  auth_data = {}
220
247
  old_value = auth_data.get("OPENAI_API_KEY")
221
248
  auth_data["OPENAI_API_KEY"] = api_key
222
- auth_path.parent.mkdir(parents=True, exist_ok=True)
223
- auth_path.write_text(
224
- json.dumps(auth_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
249
+ _write_private_text(
250
+ auth_path, json.dumps(auth_data, ensure_ascii=False, indent=2) + "\n"
225
251
  )
226
- auth_path.chmod(0o600)
227
252
  logger.info(f"Patched auth file: {auth_path}")
228
253
  return ["OPENAI_API_KEY"] if old_value != api_key else []
229
254
 
@@ -251,11 +276,36 @@ def _restore_openai_values(values: dict[str, str | None]) -> None:
251
276
  OpenAIConfig.OPENAI_API_MODEL.value = values.get("model")
252
277
 
253
278
 
279
+ def _env_ref_has_path_separator(env_ref: str) -> bool:
280
+ return "/" in env_ref or "\\" in env_ref
281
+
282
+
283
+ def _invalid_profile_name(env_ref: str) -> bool:
284
+ if env_ref == ".env":
285
+ return False
286
+ profile_name = env_ref.removesuffix(".env")
287
+ return (
288
+ not profile_name
289
+ or profile_name in {".", ".."}
290
+ or ".." in profile_name
291
+ or _env_ref_has_path_separator(profile_name)
292
+ )
293
+
294
+
254
295
  def _resolve_openai_env_path(env_ref: str) -> Path:
296
+ env_ref = str(env_ref).strip()
297
+ if not env_ref:
298
+ raise click.ClickException("OpenAI profile name cannot be empty.")
299
+
255
300
  candidate = Path(env_ref).expanduser()
256
301
  if candidate.is_file():
257
302
  return candidate
258
303
 
304
+ if _invalid_profile_name(env_ref) or _env_ref_has_path_separator(env_ref):
305
+ raise click.ClickException(
306
+ "Invalid OpenAI profile name. Profile names cannot contain path separators, '.' or '..'; pass an existing .env file path for file-based config."
307
+ )
308
+
259
309
  profile_path = OpenAIConfig.get_profile_env_file(CHATARCH_ENV_DIR, env_ref)
260
310
  if profile_path.exists():
261
311
  return profile_path
@@ -273,6 +323,22 @@ def _openai_values_from_mapping(values: dict[str, str]) -> dict[str, str | None]
273
323
  }
274
324
 
275
325
 
326
+ def _load_dotenv_without_interpolation(env_path: Path) -> dict[str, str]:
327
+ return {
328
+ key: value
329
+ for key, value in dotenv_values(env_path, interpolate=False).items()
330
+ if value is not None
331
+ }
332
+
333
+
334
+ def _reject_unresolved_env_references(values: dict[str, str | None]) -> None:
335
+ for field, value in values.items():
336
+ if isinstance(value, str) and _ENV_VAR_REF_RE.search(value):
337
+ raise click.ClickException(
338
+ f"OpenAI profile field {field} contains an unresolved variable reference; selected profiles are loaded without process-environment interpolation."
339
+ )
340
+
341
+
276
342
  def _load_openai_values_from_env_ref(env_ref: str) -> dict[str, str | None]:
277
343
  """Load only the explicitly selected OpenAI env file/profile.
278
344
 
@@ -282,7 +348,9 @@ def _load_openai_values_from_env_ref(env_ref: str) -> dict[str, str | None]:
282
348
  a different account's key into `~/.codex/auth.json`.
283
349
  """
284
350
  env_path = _resolve_openai_env_path(env_ref)
285
- return _openai_values_from_mapping(EnvStore(CHATARCH_ENV_DIR).load_path(env_path))
351
+ values = _openai_values_from_mapping(_load_dotenv_without_interpolation(env_path))
352
+ _reject_unresolved_env_references(values)
353
+ return values
286
354
 
287
355
 
288
356
  def setup_codex(
@@ -391,27 +459,43 @@ def setup_codex(
391
459
  if api_key == BACK_VALUE:
392
460
  return
393
461
 
394
- base_url = prompt_text_value(
395
- "base_url (optional)",
396
- base_url,
397
- env_config.get("base_url"),
398
- existing.get("base_url"),
399
- env_values.get("OPENAI_API_BASE"),
400
- typed_env_values.get("OPENAI_API_BASE"),
401
- fallback=DEFAULT_BASE_URL,
402
- )
462
+ if env_ref:
463
+ base_url = prompt_text_value(
464
+ "base_url (optional)",
465
+ base_url,
466
+ env_config.get("base_url"),
467
+ fallback=DEFAULT_BASE_URL,
468
+ )
469
+ else:
470
+ base_url = prompt_text_value(
471
+ "base_url (optional)",
472
+ base_url,
473
+ env_config.get("base_url"),
474
+ existing.get("base_url"),
475
+ env_values.get("OPENAI_API_BASE"),
476
+ typed_env_values.get("OPENAI_API_BASE"),
477
+ fallback=DEFAULT_BASE_URL,
478
+ )
403
479
  if base_url == BACK_VALUE:
404
480
  return
405
481
 
406
- model = prompt_text_value(
407
- "default model (optional)",
408
- model,
409
- env_config.get("model"),
410
- existing.get("model"),
411
- env_values.get("OPENAI_API_MODEL"),
412
- typed_env_values.get("OPENAI_API_MODEL"),
413
- fallback=DEFAULT_MODEL,
414
- )
482
+ if env_ref:
483
+ model = prompt_text_value(
484
+ "default model (optional)",
485
+ model,
486
+ env_config.get("model"),
487
+ fallback=DEFAULT_MODEL,
488
+ )
489
+ else:
490
+ model = prompt_text_value(
491
+ "default model (optional)",
492
+ model,
493
+ env_config.get("model"),
494
+ existing.get("model"),
495
+ env_values.get("OPENAI_API_MODEL"),
496
+ typed_env_values.get("OPENAI_API_MODEL"),
497
+ fallback=DEFAULT_MODEL,
498
+ )
415
499
  if model == BACK_VALUE:
416
500
  return
417
501
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.13
3
+ Version: 0.2.14
4
4
  Summary: ChatArch setup CLI extracted from ChatTool
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
18
  Requires-Dist: chatenv<0.3.0,>=0.2.11
19
+ Requires-Dist: python-dotenv<2.0,>=1.0
19
20
  Requires-Dist: colorama
20
21
  Provides-Extra: dev
21
22
  Requires-Dist: build; extra == "dev"
@@ -1,6 +1,7 @@
1
1
  click<9.0,>=8.0
2
2
  chatstyle<0.3.0,>=0.2.0
3
3
  chatenv<0.3.0,>=0.2.11
4
+ python-dotenv<2.0,>=1.0
4
5
  colorama
5
6
 
6
7
  [dev]
@@ -144,6 +144,152 @@ def test_codex_env_dotenv_ref_loads_active_openai_profile(tmp_path, monkeypatch)
144
144
  assert "process-key-should-not-be-used" not in config_text
145
145
 
146
146
 
147
+ def test_codex_env_profile_rejects_dotenv_interpolation_backfill(tmp_path, monkeypatch):
148
+ import chatup.setup.codex as codex_setup
149
+
150
+ home = tmp_path / "home"
151
+ envs_dir = tmp_path / "chatarch" / "envs"
152
+ openai_dir = envs_dir / "OpenAI"
153
+ monkeypatch.setenv("HOME", str(home))
154
+ monkeypatch.setenv("OPENAI_API_KEY", "process-key-should-not-be-used")
155
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
156
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
157
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
158
+ monkeypatch.setattr(
159
+ codex_setup,
160
+ "should_install_global_npm_package",
161
+ lambda *args, **kwargs: False,
162
+ )
163
+ _write_env(
164
+ openai_dir / "apple.env",
165
+ {
166
+ "OPENAI_API_KEY": "${OPENAI_API_KEY}",
167
+ "OPENAI_API_BASE": "https://crs.example/openai/v1",
168
+ "OPENAI_API_MODEL": "gpt-5.5",
169
+ },
170
+ )
171
+
172
+ with pytest.raises(click.ClickException, match="unresolved variable reference"):
173
+ codex_setup.setup_codex(env_ref="apple", interactive=False)
174
+
175
+ assert not (home / ".codex" / "auth.json").exists()
176
+
177
+
178
+ def test_codex_env_profile_rejects_path_traversal_name(tmp_path, monkeypatch):
179
+ import chatup.setup.codex as codex_setup
180
+
181
+ home = tmp_path / "home"
182
+ envs_dir = tmp_path / "chatarch" / "envs"
183
+ other_dir = envs_dir / "Other"
184
+ monkeypatch.setenv("HOME", str(home))
185
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
186
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
187
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
188
+ monkeypatch.setattr(
189
+ codex_setup,
190
+ "should_install_global_npm_package",
191
+ lambda *args, **kwargs: False,
192
+ )
193
+ _write_env(
194
+ other_dir / "secret.env",
195
+ {
196
+ "OPENAI_API_KEY": "other-key-should-not-be-read",
197
+ "OPENAI_API_BASE": "https://other.example/v1",
198
+ "OPENAI_API_MODEL": "other-model",
199
+ },
200
+ )
201
+
202
+ with pytest.raises(click.ClickException, match="profile name"):
203
+ codex_setup.setup_codex(env_ref="../Other/secret", interactive=False)
204
+
205
+ assert not (home / ".codex" / "auth.json").exists()
206
+
207
+
208
+ def test_codex_env_profile_prompt_defaults_stay_inside_selected_profile(
209
+ tmp_path, monkeypatch
210
+ ):
211
+ import chatup.setup.codex as codex_setup
212
+
213
+ home = tmp_path / "home"
214
+ envs_dir = tmp_path / "chatarch" / "envs"
215
+ openai_dir = envs_dir / "OpenAI"
216
+ codex_dir = home / ".codex"
217
+ codex_dir.mkdir(parents=True)
218
+ (codex_dir / "config.toml").write_text(
219
+ 'model_provider = "crs"\n'
220
+ 'model = "existing-model"\n\n'
221
+ '[model_providers.crs]\n'
222
+ 'base_url = "https://existing.example/v1"\n',
223
+ encoding="utf-8",
224
+ )
225
+ monkeypatch.setenv("HOME", str(home))
226
+ monkeypatch.setenv("OPENAI_API_BASE", "https://process.example/v1")
227
+ monkeypatch.setenv("OPENAI_API_MODEL", "process-model")
228
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
229
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
230
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
231
+ monkeypatch.setattr(
232
+ codex_setup,
233
+ "should_install_global_npm_package",
234
+ lambda *args, **kwargs: False,
235
+ )
236
+ monkeypatch.setattr(
237
+ codex_setup,
238
+ "resolve_interactive_mode",
239
+ lambda **kwargs: (True, True, True, False, True),
240
+ )
241
+ monkeypatch.setattr(codex_setup, "abort_if_force_without_tty", lambda *a, **k: None)
242
+ monkeypatch.setattr(codex_setup, "abort_if_missing_without_tty", lambda *a, **k: None)
243
+ monkeypatch.setattr(
244
+ codex_setup,
245
+ "resolve_install_only_mode",
246
+ lambda **kwargs: (False, False),
247
+ )
248
+ monkeypatch.setattr(
249
+ codex_setup,
250
+ "prompt_sensitive_value",
251
+ lambda _label, value, _masker: value,
252
+ )
253
+
254
+ prompt_candidates = []
255
+
256
+ def fake_prompt_text_value(_label, *candidates, fallback=None):
257
+ prompt_candidates.extend(candidates)
258
+ for candidate in candidates:
259
+ if candidate:
260
+ return candidate
261
+ return fallback
262
+
263
+ monkeypatch.setattr(codex_setup, "prompt_text_value", fake_prompt_text_value)
264
+ _write_env(
265
+ openai_dir / ".env",
266
+ {
267
+ "OPENAI_API_BASE": "https://active.example/v1",
268
+ "OPENAI_API_MODEL": "active-model",
269
+ },
270
+ )
271
+ _write_env(
272
+ openai_dir / "apple.env",
273
+ {
274
+ "OPENAI_API_KEY": "apple-key",
275
+ "OPENAI_API_BASE": "https://apple.example/v1",
276
+ "OPENAI_API_MODEL": "apple-model",
277
+ },
278
+ )
279
+
280
+ codex_setup.setup_codex(env_ref="apple", interactive=True)
281
+
282
+ assert "https://existing.example/v1" not in prompt_candidates
283
+ assert "existing-model" not in prompt_candidates
284
+ assert "https://process.example/v1" not in prompt_candidates
285
+ assert "process-model" not in prompt_candidates
286
+ assert "https://active.example/v1" not in prompt_candidates
287
+ assert "active-model" not in prompt_candidates
288
+ config_text = (codex_dir / "config.toml").read_text(encoding="utf-8")
289
+ assert 'base_url = "https://apple.example/v1"' in config_text
290
+ assert 'model = "apple-model"' in config_text
291
+
292
+
147
293
  def test_codex_default_model_matches_shared_openai_default():
148
294
  from chatenv.configs import OpenAIConfig
149
295
  import chatup.setup.codex as codex_setup
@@ -24,6 +24,7 @@ def test_chatup_depends_on_chatenv_and_registers_cursor_agent_config_provider():
24
24
  assert "click>=8.0,<9.0" in data["project"]["dependencies"]
25
25
  assert "chatstyle>=0.2.0,<0.3.0" in data["project"]["dependencies"]
26
26
  assert "chatenv>=0.2.11,<0.3.0" in data["project"]["dependencies"]
27
+ assert "python-dotenv>=1.0,<2.0" in data["project"]["dependencies"]
27
28
  entry_points = data["project"]["entry-points"]["chatenv.configs"]
28
29
  assert entry_points["cursor-agent"] == "chatup.config:CursorAgentConfig"
29
30
  assert entry_points["discourse"] == "chatup.config:DiscourseAdminConfig"
@@ -5,7 +5,7 @@ from chatup.cli import main
5
5
 
6
6
 
7
7
  def test_version_present():
8
- assert __version__ == "0.2.13"
8
+ assert __version__ == "0.2.14"
9
9
 
10
10
 
11
11
  def test_cli_help():
@@ -61,4 +61,4 @@ def test_cli_doctor():
61
61
  result = CliRunner().invoke(main, ["doctor"])
62
62
 
63
63
  assert result.exit_code == 0
64
- assert "chatup 0.2.13 ok" in result.output
64
+ assert "chatup 0.2.14 ok" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes