chatup 0.2.12__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.12/src/chatup.egg-info → chatup-0.2.14}/PKG-INFO +3 -2
  2. {chatup-0.2.12 → chatup-0.2.14}/pyproject.toml +2 -1
  3. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/__init__.py +1 -1
  4. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/codex.py +166 -56
  5. {chatup-0.2.12 → chatup-0.2.14/src/chatup.egg-info}/PKG-INFO +3 -2
  6. {chatup-0.2.12 → chatup-0.2.14}/src/chatup.egg-info/SOURCES.txt +1 -0
  7. {chatup-0.2.12 → chatup-0.2.14}/src/chatup.egg-info/requires.txt +2 -1
  8. chatup-0.2.14/tests/test_codex_setup.py +297 -0
  9. {chatup-0.2.12 → chatup-0.2.14}/tests/test_pyproject_metadata.py +2 -1
  10. {chatup-0.2.12 → chatup-0.2.14}/tests/test_version.py +2 -2
  11. {chatup-0.2.12 → chatup-0.2.14}/LICENSE +0 -0
  12. {chatup-0.2.12 → chatup-0.2.14}/README.md +0 -0
  13. {chatup-0.2.12 → chatup-0.2.14}/setup.cfg +0 -0
  14. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/chrome_for_testing.py +0 -0
  15. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/chromedriver.py +0 -0
  16. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/cli.py +0 -0
  17. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/commands/__init__.py +0 -0
  18. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/commands/chrome_for_testing.py +0 -0
  19. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/commands/chromedriver.py +0 -0
  20. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/commands/playwright.py +0 -0
  21. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/config.py +0 -0
  22. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/const.py +0 -0
  23. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/interaction/__init__.py +0 -0
  24. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/interaction/command_schema.py +0 -0
  25. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/interaction/policy.py +0 -0
  26. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/interaction/warnings.py +0 -0
  27. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/playwright.py +0 -0
  28. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/runtime/__init__.py +0 -0
  29. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/runtime/_managed_artifact.py +0 -0
  30. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/runtime/chrome_for_testing.py +0 -0
  31. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/runtime/chromedriver.py +0 -0
  32. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/runtime/playwright.py +0 -0
  33. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/__init__.py +0 -0
  34. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/assets/hermes/install.sh +0 -0
  35. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/assets/nvm.sh +0 -0
  36. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/cc_connect.py +0 -0
  37. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/claude.py +0 -0
  38. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/cli.py +0 -0
  39. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/crs.py +0 -0
  40. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/cursor_agent.py +0 -0
  41. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/discourse.py +0 -0
  42. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/docker.py +0 -0
  43. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/elements.py +0 -0
  44. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/frp.py +0 -0
  45. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/gitea.py +0 -0
  46. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/hermes.py +0 -0
  47. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/lark_cli.py +0 -0
  48. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/mode_prompt.py +0 -0
  49. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/mysql.py +0 -0
  50. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/nginx.py +0 -0
  51. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/nodejs.py +0 -0
  52. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/opencode.py +0 -0
  53. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/service_credentials.py +0 -0
  54. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/shell_rc.py +0 -0
  55. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/twikoo.py +0 -0
  56. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/uv.py +0 -0
  57. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/__init__.py +0 -0
  58. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/cli.py +0 -0
  59. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/core.py +0 -0
  60. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/options.py +0 -0
  61. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/render.py +0 -0
  62. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/AGENTS.md +0 -0
  63. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/discussion/README.md +0 -0
  64. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/en/projects/README.md +0 -0
  65. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/AGENTS.md +0 -0
  66. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/discussion/README.md +0 -0
  67. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace/templates/default/zh/projects/README.md +0 -0
  68. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/workspace.py +0 -0
  69. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/zsh.py +0 -0
  70. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/setup/zulip.py +0 -0
  71. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/utils/__init__.py +0 -0
  72. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/utils/custom_logger.py +0 -0
  73. {chatup-0.2.12 → chatup-0.2.14}/src/chatup/utils/pathing.py +0 -0
  74. {chatup-0.2.12 → chatup-0.2.14}/src/chatup.egg-info/dependency_links.txt +0 -0
  75. {chatup-0.2.12 → chatup-0.2.14}/src/chatup.egg-info/entry_points.txt +0 -0
  76. {chatup-0.2.12 → chatup-0.2.14}/src/chatup.egg-info/top_level.txt +0 -0
  77. {chatup-0.2.12 → chatup-0.2.14}/tests/test_browser_backend_cli.py +0 -0
  78. {chatup-0.2.12 → chatup-0.2.14}/tests/test_chrome_for_testing.py +0 -0
  79. {chatup-0.2.12 → chatup-0.2.14}/tests/test_chromedriver.py +0 -0
  80. {chatup-0.2.12 → chatup-0.2.14}/tests/test_cli_tree_docs.py +0 -0
  81. {chatup-0.2.12 → chatup-0.2.14}/tests/test_crs_setup.py +0 -0
  82. {chatup-0.2.12 → chatup-0.2.14}/tests/test_cursor_agent_setup.py +0 -0
  83. {chatup-0.2.12 → chatup-0.2.14}/tests/test_playwright.py +0 -0
  84. {chatup-0.2.12 → chatup-0.2.14}/tests/test_playwright_cli.py +0 -0
  85. {chatup-0.2.12 → chatup-0.2.14}/tests/test_publish_workflow.py +0 -0
  86. {chatup-0.2.12 → chatup-0.2.14}/tests/test_service_installers.py +0 -0
  87. {chatup-0.2.12 → chatup-0.2.14}/tests/test_setup_cli.py +0 -0
  88. {chatup-0.2.12 → chatup-0.2.14}/tests/test_twikoo_setup.py +0 -0
  89. {chatup-0.2.12 → 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.12
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
@@ -15,7 +15,8 @@ Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
- Requires-Dist: chatenv<0.3.0,>=0.2.10
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"
@@ -12,7 +12,8 @@ license = "MIT"
12
12
  dependencies = [
13
13
  "click>=8.0,<9.0",
14
14
  "chatstyle>=0.2.0,<0.3.0",
15
- "chatenv>=0.2.10,<0.3.0",
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.12"
5
+ __version__ = "0.2.14"
@@ -1,8 +1,12 @@
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
@@ -25,9 +29,10 @@ from chatup.setup.nodejs import (
25
29
  )
26
30
  from chatup.utils.custom_logger import setup_logger
27
31
 
28
- DEFAULT_MODEL = "gpt-5.4"
32
+ DEFAULT_MODEL = "gpt-5.5"
29
33
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
30
34
  DEFAULT_AUTH_METHOD = "apikey"
35
+ _ENV_VAR_REF_RE = re.compile(r"\$(?:\{[^}]+\}|[A-Za-z_][A-Za-z0-9_]*)")
31
36
  logger = setup_logger("setup_codex")
32
37
 
33
38
 
@@ -207,6 +212,29 @@ def _write_codex_config(config_path: Path, *, model: str, base_url: str) -> list
207
212
  return changed_root + changed_provider
208
213
 
209
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
+
210
238
  def _write_codex_auth(auth_path: Path, api_key: str) -> list[str]:
211
239
  auth_data = {}
212
240
  if auth_path.exists():
@@ -218,11 +246,9 @@ def _write_codex_auth(auth_path: Path, api_key: str) -> list[str]:
218
246
  auth_data = {}
219
247
  old_value = auth_data.get("OPENAI_API_KEY")
220
248
  auth_data["OPENAI_API_KEY"] = api_key
221
- auth_path.parent.mkdir(parents=True, exist_ok=True)
222
- auth_path.write_text(
223
- 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"
224
251
  )
225
- auth_path.chmod(0o600)
226
252
  logger.info(f"Patched auth file: {auth_path}")
227
253
  return ["OPENAI_API_KEY"] if old_value != api_key else []
228
254
 
@@ -250,11 +276,36 @@ def _restore_openai_values(values: dict[str, str | None]) -> None:
250
276
  OpenAIConfig.OPENAI_API_MODEL.value = values.get("model")
251
277
 
252
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
+
253
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
+
254
300
  candidate = Path(env_ref).expanduser()
255
301
  if candidate.is_file():
256
302
  return candidate
257
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
+
258
309
  profile_path = OpenAIConfig.get_profile_env_file(CHATARCH_ENV_DIR, env_ref)
259
310
  if profile_path.exists():
260
311
  return profile_path
@@ -264,17 +315,42 @@ def _resolve_openai_env_path(env_ref: str) -> Path:
264
315
  )
265
316
 
266
317
 
318
+ def _openai_values_from_mapping(values: dict[str, str]) -> dict[str, str | None]:
319
+ return {
320
+ "openai_api_key": values.get("OPENAI_API_KEY"),
321
+ "base_url": values.get("OPENAI_API_BASE"),
322
+ "model": values.get("OPENAI_API_MODEL"),
323
+ }
324
+
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
+
267
342
  def _load_openai_values_from_env_ref(env_ref: str) -> dict[str, str | None]:
268
- current_values = _snapshot_openai_values()
269
- try:
270
- env_path = _resolve_openai_env_path(env_ref)
271
- BaseEnvConfig.load_all_with_override(
272
- CHATARCH_ENV_DIR,
273
- override_env_file=env_path,
274
- )
275
- return _snapshot_openai_values()
276
- finally:
277
- _restore_openai_values(current_values)
343
+ """Load only the explicitly selected OpenAI env file/profile.
344
+
345
+ `chatup codex -e PROFILE` is an account-selection boundary. Do not merge
346
+ the selected profile with the active/default ChatEnv profile or process
347
+ environment, otherwise a profile missing `OPENAI_API_KEY` can silently write
348
+ a different account's key into `~/.codex/auth.json`.
349
+ """
350
+ env_path = _resolve_openai_env_path(env_ref)
351
+ values = _openai_values_from_mapping(_load_dotenv_without_interpolation(env_path))
352
+ _reject_unresolved_env_references(values)
353
+ return values
278
354
 
279
355
 
280
356
  def setup_codex(
@@ -302,13 +378,19 @@ def setup_codex(
302
378
  if isinstance(model, str) and not model.strip():
303
379
  model = None
304
380
 
305
- api_key = resolve_value(
306
- api_key,
307
- env_config.get("openai_api_key"),
308
- existing_api_key,
309
- env_values.get("OPENAI_API_KEY"),
310
- typed_env_values.get("OPENAI_API_KEY"),
311
- )
381
+ if env_ref:
382
+ api_key = resolve_value(
383
+ api_key,
384
+ env_config.get("openai_api_key"),
385
+ )
386
+ else:
387
+ api_key = resolve_value(
388
+ api_key,
389
+ env_config.get("openai_api_key"),
390
+ existing_api_key,
391
+ env_values.get("OPENAI_API_KEY"),
392
+ typed_env_values.get("OPENAI_API_KEY"),
393
+ )
312
394
  missing_required = not api_key
313
395
  has_existing_config = any(
314
396
  value for key, value in existing.items() if key != "openai_api_key"
@@ -377,27 +459,43 @@ def setup_codex(
377
459
  if api_key == BACK_VALUE:
378
460
  return
379
461
 
380
- base_url = prompt_text_value(
381
- "base_url (optional)",
382
- base_url,
383
- env_config.get("base_url"),
384
- existing.get("base_url"),
385
- env_values.get("OPENAI_API_BASE"),
386
- typed_env_values.get("OPENAI_API_BASE"),
387
- fallback=DEFAULT_BASE_URL,
388
- )
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
+ )
389
479
  if base_url == BACK_VALUE:
390
480
  return
391
481
 
392
- model = prompt_text_value(
393
- "default model (optional)",
394
- model,
395
- env_config.get("model"),
396
- existing.get("model"),
397
- env_values.get("OPENAI_API_MODEL"),
398
- typed_env_values.get("OPENAI_API_MODEL"),
399
- fallback=DEFAULT_MODEL,
400
- )
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
+ )
401
499
  if model == BACK_VALUE:
402
500
  return
403
501
 
@@ -421,22 +519,34 @@ def setup_codex(
421
519
  click.echo(result.stderr.strip(), err=True)
422
520
  raise click.Abort()
423
521
 
424
- base_url = resolve_value(
425
- base_url,
426
- env_config.get("base_url"),
427
- existing.get("base_url"),
428
- env_values.get("OPENAI_API_BASE"),
429
- typed_env_values.get("OPENAI_API_BASE"),
430
- DEFAULT_BASE_URL,
431
- )
432
- model = resolve_value(
433
- model,
434
- env_config.get("model"),
435
- existing.get("model"),
436
- env_values.get("OPENAI_API_MODEL"),
437
- typed_env_values.get("OPENAI_API_MODEL"),
438
- DEFAULT_MODEL,
439
- )
522
+ if env_ref:
523
+ base_url = resolve_value(
524
+ base_url,
525
+ env_config.get("base_url"),
526
+ DEFAULT_BASE_URL,
527
+ )
528
+ model = resolve_value(
529
+ model,
530
+ env_config.get("model"),
531
+ DEFAULT_MODEL,
532
+ )
533
+ else:
534
+ base_url = resolve_value(
535
+ base_url,
536
+ env_config.get("base_url"),
537
+ existing.get("base_url"),
538
+ env_values.get("OPENAI_API_BASE"),
539
+ typed_env_values.get("OPENAI_API_BASE"),
540
+ DEFAULT_BASE_URL,
541
+ )
542
+ model = resolve_value(
543
+ model,
544
+ env_config.get("model"),
545
+ existing.get("model"),
546
+ env_values.get("OPENAI_API_MODEL"),
547
+ typed_env_values.get("OPENAI_API_MODEL"),
548
+ DEFAULT_MODEL,
549
+ )
440
550
 
441
551
  codex_dir.mkdir(parents=True, exist_ok=True)
442
552
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatup
3
- Version: 0.2.12
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
@@ -15,7 +15,8 @@ Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: click<9.0,>=8.0
17
17
  Requires-Dist: chatstyle<0.3.0,>=0.2.0
18
- Requires-Dist: chatenv<0.3.0,>=0.2.10
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"
@@ -73,6 +73,7 @@ tests/test_browser_backend_cli.py
73
73
  tests/test_chrome_for_testing.py
74
74
  tests/test_chromedriver.py
75
75
  tests/test_cli_tree_docs.py
76
+ tests/test_codex_setup.py
76
77
  tests/test_crs_setup.py
77
78
  tests/test_cursor_agent_setup.py
78
79
  tests/test_playwright.py
@@ -1,6 +1,7 @@
1
1
  click<9.0,>=8.0
2
2
  chatstyle<0.3.0,>=0.2.0
3
- chatenv<0.3.0,>=0.2.10
3
+ chatenv<0.3.0,>=0.2.11
4
+ python-dotenv<2.0,>=1.0
4
5
  colorama
5
6
 
6
7
  [dev]
@@ -0,0 +1,297 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import click
6
+ import pytest
7
+
8
+
9
+ def _write_env(path, values):
10
+ path.parent.mkdir(parents=True, exist_ok=True)
11
+ lines = [f"{key}='{value}'" for key, value in values.items()]
12
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
13
+
14
+
15
+ def test_codex_env_profile_does_not_backfill_missing_key_from_active_or_process_env(
16
+ tmp_path, monkeypatch
17
+ ):
18
+ import chatup.setup.codex as codex_setup
19
+
20
+ home = tmp_path / "home"
21
+ envs_dir = tmp_path / "chatarch" / "envs"
22
+ openai_dir = envs_dir / "OpenAI"
23
+ monkeypatch.setenv("HOME", str(home))
24
+ monkeypatch.setenv("OPENAI_API_KEY", "process-key-should-not-be-used")
25
+ monkeypatch.setenv("OPENAI_API_BASE", "https://process.example/v1")
26
+ monkeypatch.setenv("OPENAI_API_MODEL", "process-model")
27
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
28
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
29
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
30
+ monkeypatch.setattr(
31
+ codex_setup,
32
+ "should_install_global_npm_package",
33
+ lambda *args, **kwargs: False,
34
+ )
35
+
36
+ _write_env(
37
+ openai_dir / ".env",
38
+ {
39
+ "OPENAI_API_KEY": "active-key-should-not-be-used",
40
+ "OPENAI_API_BASE": "https://active.example/v1",
41
+ "OPENAI_API_MODEL": "active-model",
42
+ },
43
+ )
44
+ _write_env(
45
+ openai_dir / "apple.env",
46
+ {
47
+ "OPENAI_API_BASE": "https://crs.example/openai/v1",
48
+ "OPENAI_API_MODEL": "gpt-5.5",
49
+ },
50
+ )
51
+
52
+ with pytest.raises(click.Abort):
53
+ codex_setup.setup_codex(env_ref="apple", interactive=False)
54
+
55
+ auth_path = home / ".codex" / "auth.json"
56
+ if auth_path.exists():
57
+ auth_data = json.loads(auth_path.read_text(encoding="utf-8"))
58
+ assert auth_data.get("OPENAI_API_KEY") not in {
59
+ "active-key-should-not-be-used",
60
+ "process-key-should-not-be-used",
61
+ }
62
+
63
+
64
+ def test_codex_env_profile_writes_exact_named_profile_values(tmp_path, monkeypatch):
65
+ import chatup.setup.codex as codex_setup
66
+
67
+ home = tmp_path / "home"
68
+ envs_dir = tmp_path / "chatarch" / "envs"
69
+ openai_dir = envs_dir / "OpenAI"
70
+ monkeypatch.setenv("HOME", str(home))
71
+ monkeypatch.setenv("OPENAI_API_KEY", "process-key-should-not-be-used")
72
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
73
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
74
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
75
+ monkeypatch.setattr(
76
+ codex_setup,
77
+ "should_install_global_npm_package",
78
+ lambda *args, **kwargs: False,
79
+ )
80
+
81
+ _write_env(
82
+ openai_dir / ".env",
83
+ {
84
+ "OPENAI_API_KEY": "active-key-should-not-be-used",
85
+ "OPENAI_API_BASE": "https://active.example/v1",
86
+ "OPENAI_API_MODEL": "active-model",
87
+ },
88
+ )
89
+ _write_env(
90
+ openai_dir / "apple.env",
91
+ {
92
+ "OPENAI_API_KEY": "apple-key",
93
+ "OPENAI_API_BASE": "https://crs.example/openai/v1",
94
+ "OPENAI_API_MODEL": "gpt-5.5",
95
+ },
96
+ )
97
+
98
+ codex_setup.setup_codex(env_ref="apple", interactive=False)
99
+
100
+ auth_path = home / ".codex" / "auth.json"
101
+ auth_data = json.loads(auth_path.read_text(encoding="utf-8"))
102
+ assert auth_data == {"OPENAI_API_KEY": "apple-key"}
103
+ assert auth_path.stat().st_mode & 0o777 == 0o600
104
+
105
+ config_text = (home / ".codex" / "config.toml").read_text(encoding="utf-8")
106
+ assert 'model = "gpt-5.5"' in config_text
107
+ assert 'base_url = "https://crs.example/openai/v1"' in config_text
108
+ assert "active-key-should-not-be-used" not in config_text
109
+ assert "process-key-should-not-be-used" not in config_text
110
+
111
+
112
+ def test_codex_env_dotenv_ref_loads_active_openai_profile(tmp_path, monkeypatch):
113
+ import chatup.setup.codex as codex_setup
114
+
115
+ home = tmp_path / "home"
116
+ envs_dir = tmp_path / "chatarch" / "envs"
117
+ openai_dir = envs_dir / "OpenAI"
118
+ monkeypatch.setenv("HOME", str(home))
119
+ monkeypatch.setenv("OPENAI_API_KEY", "process-key-should-not-be-used")
120
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_DIR", envs_dir)
121
+ monkeypatch.setattr(codex_setup, "CHATARCH_ENV_FILE", envs_dir / ".env")
122
+ monkeypatch.setattr(codex_setup, "ensure_nodejs_requirement", lambda **kwargs: None)
123
+ monkeypatch.setattr(
124
+ codex_setup,
125
+ "should_install_global_npm_package",
126
+ lambda *args, **kwargs: False,
127
+ )
128
+ _write_env(
129
+ openai_dir / ".env",
130
+ {
131
+ "OPENAI_API_KEY": "active-key",
132
+ "OPENAI_API_BASE": "https://active.example/v1",
133
+ "OPENAI_API_MODEL": "gpt-5.5",
134
+ },
135
+ )
136
+
137
+ codex_setup.setup_codex(env_ref=".env", interactive=False)
138
+
139
+ auth_data = json.loads((home / ".codex" / "auth.json").read_text(encoding="utf-8"))
140
+ assert auth_data == {"OPENAI_API_KEY": "active-key"}
141
+ config_text = (home / ".codex" / "config.toml").read_text(encoding="utf-8")
142
+ assert 'model = "gpt-5.5"' in config_text
143
+ assert 'base_url = "https://active.example/v1"' in config_text
144
+ assert "process-key-should-not-be-used" not in config_text
145
+
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
+
293
+ def test_codex_default_model_matches_shared_openai_default():
294
+ from chatenv.configs import OpenAIConfig
295
+ import chatup.setup.codex as codex_setup
296
+
297
+ assert codex_setup.DEFAULT_MODEL == OpenAIConfig.OPENAI_API_MODEL.default
@@ -23,7 +23,8 @@ def test_chatup_depends_on_chatenv_and_registers_cursor_agent_config_provider():
23
23
  assert data["project"]["requires-python"] == ">=3.10"
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
- assert "chatenv>=0.2.10,<0.3.0" in data["project"]["dependencies"]
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.12"
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.12 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