LastNoob commited on
Commit
dea66dd
·
1 Parent(s): a3893ea

Use root env example for fcc init

Browse files
cli/entrypoints.py CHANGED
@@ -2,6 +2,23 @@
2
 
3
  from __future__ import annotations
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  def serve() -> None:
7
  """Start the FastAPI server (registered as `free-claude-code` script)."""
@@ -25,9 +42,6 @@ def serve() -> None:
25
 
26
  def init() -> None:
27
  """Scaffold config at ~/.config/free-claude-code/.env (registered as `fcc-init`)."""
28
- import importlib.resources
29
- from pathlib import Path
30
-
31
  config_dir = Path.home() / ".config" / "free-claude-code"
32
  env_file = config_dir / ".env"
33
 
@@ -37,9 +51,7 @@ def init() -> None:
37
  return
38
 
39
  config_dir.mkdir(parents=True, exist_ok=True)
40
- template = (
41
- importlib.resources.files("config").joinpath("env.example").read_text("utf-8")
42
- )
43
  env_file.write_text(template, encoding="utf-8")
44
  print(f"Config created at {env_file}")
45
  print(
 
2
 
3
  from __future__ import annotations
4
 
5
+ from pathlib import Path
6
+
7
+
8
+ def _load_env_template() -> str:
9
+ """Load the canonical root env template from package resources or source."""
10
+ import importlib.resources
11
+
12
+ packaged = importlib.resources.files("cli").joinpath("env.example")
13
+ if packaged.is_file():
14
+ return packaged.read_text("utf-8")
15
+
16
+ source_template = Path(__file__).resolve().parents[1] / ".env.example"
17
+ if source_template.is_file():
18
+ return source_template.read_text(encoding="utf-8")
19
+
20
+ raise FileNotFoundError("Could not find bundled or source .env.example template.")
21
+
22
 
23
  def serve() -> None:
24
  """Start the FastAPI server (registered as `free-claude-code` script)."""
 
42
 
43
  def init() -> None:
44
  """Scaffold config at ~/.config/free-claude-code/.env (registered as `fcc-init`)."""
 
 
 
45
  config_dir = Path.home() / ".config" / "free-claude-code"
46
  env_file = config_dir / ".env"
47
 
 
51
  return
52
 
53
  config_dir.mkdir(parents=True, exist_ok=True)
54
+ template = _load_env_template()
 
 
55
  env_file.write_text(template, encoding="utf-8")
56
  print(f"Config created at {env_file}")
57
  print(
config/env.example DELETED
@@ -1,99 +0,0 @@
1
- # NVIDIA NIM Config
2
- NVIDIA_NIM_API_KEY=""
3
-
4
-
5
- # OpenRouter Config
6
- OPENROUTER_API_KEY=""
7
-
8
-
9
- # DeepSeek Config
10
- DEEPSEEK_API_KEY=""
11
-
12
-
13
- # LM Studio Config (local provider, no API key required)
14
- LM_STUDIO_BASE_URL="http://localhost:1234/v1"
15
-
16
-
17
- # Llama.cpp Config (local provider, no API key required)
18
- LLAMACPP_BASE_URL="http://localhost:8080/v1"
19
-
20
-
21
- # All Claude model requests are mapped to these models, plain model is fallback
22
- # Format: provider_type/model/name
23
- # Valid providers: "nvidia_nim" | "open_router" | "deepseek" | "lmstudio" | "llamacpp"
24
- MODEL_OPUS=
25
- MODEL_SONNET=
26
- MODEL_HAIKU=
27
- MODEL="nvidia_nim/z-ai/glm4.7"
28
-
29
-
30
- # Thinking output
31
- # Per-Claude-model switches for provider reasoning requests and Claude thinking blocks.
32
- # Blank per-model switches inherit ENABLE_MODEL_THINKING.
33
- ENABLE_OPUS_THINKING=
34
- ENABLE_SONNET_THINKING=
35
- ENABLE_HAIKU_THINKING=
36
- ENABLE_MODEL_THINKING=true
37
-
38
-
39
- # Provider config
40
- # Per-provider proxy support: http and socks5, example: "http://username:password@host:port"
41
- NVIDIA_NIM_PROXY=""
42
- OPENROUTER_PROXY=""
43
- LMSTUDIO_PROXY=""
44
- LLAMACPP_PROXY=""
45
-
46
- PROVIDER_RATE_LIMIT=40
47
- PROVIDER_RATE_WINDOW=60
48
- PROVIDER_MAX_CONCURRENCY=5
49
-
50
-
51
- # HTTP client timeouts (seconds) for provider API requests
52
- HTTP_READ_TIMEOUT=120
53
- HTTP_WRITE_TIMEOUT=10
54
- HTTP_CONNECT_TIMEOUT=2
55
-
56
-
57
- # Optional server API key (Anthropic-style)
58
- ANTHROPIC_AUTH_TOKEN=
59
-
60
-
61
- # Messaging Platform: "telegram" | "discord" | "none"
62
- MESSAGING_PLATFORM="discord"
63
- MESSAGING_RATE_LIMIT=1
64
- MESSAGING_RATE_WINDOW=1
65
-
66
-
67
- # Voice Note Transcription
68
- VOICE_NOTE_ENABLED=false
69
- # WHISPER_DEVICE: "cpu" | "cuda" | "nvidia_nim"
70
- # - "cpu"/"cuda": Hugging Face transformers Whisper (offline, free; install with: uv sync --extra voice_local)
71
- # - "nvidia_nim": NVIDIA NIM Whisper via Riva gRPC (requires NVIDIA_NIM_API_KEY; install with: uv sync --extra voice)
72
- WHISPER_DEVICE="nvidia_nim"
73
- # WHISPER_MODEL:
74
- # - For cpu/cuda: Hugging Face ID or short name (tiny, base, small, medium, large-v2, large-v3, large-v3-turbo)
75
- # - For nvidia_nim: NVIDIA NIM model (e.g., "nvidia/parakeet-ctc-1.1b-asr", "openai/whisper-large-v3")
76
- # - For nvidia_nim, default to "openai/whisper-large-v3" for best performance
77
- WHISPER_MODEL="openai/whisper-large-v3"
78
- HF_TOKEN=""
79
-
80
-
81
- # Telegram Config
82
- TELEGRAM_BOT_TOKEN=""
83
- ALLOWED_TELEGRAM_USER_ID=""
84
-
85
-
86
- # Discord Config
87
- DISCORD_BOT_TOKEN=""
88
- ALLOWED_DISCORD_CHANNELS=""
89
-
90
-
91
- # Agent Config
92
- CLAUDE_WORKSPACE="./agent_workspace"
93
- ALLOWED_DIR=""
94
- CLAUDE_CLI_BIN="claude"
95
- FAST_PREFIX_DETECTION=true
96
- ENABLE_NETWORK_PROBE_MOCK=true
97
- ENABLE_TITLE_GENERATION_SKIP=true
98
- ENABLE_SUGGESTION_MODE_SKIP=true
99
- ENABLE_FILEPATH_EXTRACTION_MOCK=true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -44,6 +44,9 @@ voice_local = [
44
  [tool.hatch.build.targets.wheel]
45
  packages = ["api", "cli", "config", "core", "messaging", "providers"]
46
 
 
 
 
47
  [tool.uv.sources]
48
  torch = { index = "pytorch-cu130" }
49
 
 
44
  [tool.hatch.build.targets.wheel]
45
  packages = ["api", "cli", "config", "core", "messaging", "providers"]
46
 
47
+ [tool.hatch.build.targets.wheel.force-include]
48
+ ".env.example" = "cli/env.example"
49
+
50
  [tool.uv.sources]
51
  torch = { index = "pytorch-cu130" }
52
 
tests/cli/test_entrypoints.py CHANGED
@@ -33,17 +33,26 @@ def test_init_creates_env_file(tmp_path: Path) -> None:
33
 
34
 
35
  def test_init_copies_template_content(tmp_path: Path) -> None:
36
- """init() writes the actual bundled env.example content, not an empty file."""
37
- import importlib.resources
38
-
39
- template = (
40
- importlib.resources.files("config").joinpath("env.example").read_text("utf-8")
41
  )
42
  _, env_file = _run_init(tmp_path)
43
 
44
  assert env_file.read_text("utf-8") == template
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
47
  def test_init_creates_parent_directories(tmp_path: Path) -> None:
48
  """init() creates ~/.config/free-claude-code/ even if it doesn't exist."""
49
  config_dir = tmp_path / ".config" / "free-claude-code"
 
33
 
34
 
35
  def test_init_copies_template_content(tmp_path: Path) -> None:
36
+ """init() writes the canonical root env.example content, not an empty file."""
37
+ template = (Path(__file__).resolve().parents[2] / ".env.example").read_text(
38
+ encoding="utf-8"
 
 
39
  )
40
  _, env_file = _run_init(tmp_path)
41
 
42
  assert env_file.read_text("utf-8") == template
43
 
44
 
45
+ def test_env_template_loader_uses_root_template_in_source_checkout() -> None:
46
+ """Source checkout fallback uses the root .env.example as the single source."""
47
+ from cli.entrypoints import _load_env_template
48
+
49
+ template = (Path(__file__).resolve().parents[2] / ".env.example").read_text(
50
+ encoding="utf-8"
51
+ )
52
+
53
+ assert _load_env_template() == template
54
+
55
+
56
  def test_init_creates_parent_directories(tmp_path: Path) -> None:
57
  """init() creates ~/.config/free-claude-code/ even if it doesn't exist."""
58
  config_dir = tmp_path / ".config" / "free-claude-code"
tests/contracts/test_architecture_contracts.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import re
 
4
  from pathlib import Path
5
 
6
 
@@ -14,12 +15,24 @@ def test_architecture_plan_exists() -> None:
14
  assert "Smoke Coverage Policy" in text
15
 
16
 
17
- def test_env_examples_are_in_sync() -> None:
18
  repo_root = Path(__file__).resolve().parents[2]
19
  root_example = repo_root / ".env.example"
20
- packaged_example = repo_root / "config" / "env.example"
21
 
22
- assert _env_keys(root_example) == _env_keys(packaged_example)
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
 
25
  def test_pyproject_first_party_packages_match_packaged_roots() -> None:
@@ -35,14 +48,3 @@ def test_pyproject_first_party_packages_match_packaged_roots() -> None:
35
  }
36
  expected = {"api", "cli", "config", "core", "messaging", "providers", "smoke"}
37
  assert configured == expected
38
-
39
-
40
- def _env_keys(path: Path) -> set[str]:
41
- keys: set[str] = set()
42
- for line in path.read_text(encoding="utf-8").splitlines():
43
- stripped = line.strip()
44
- if not stripped or stripped.startswith("#") or "=" not in stripped:
45
- continue
46
- key, _, _value = stripped.partition("=")
47
- keys.add(key.strip())
48
- return keys
 
1
  from __future__ import annotations
2
 
3
  import re
4
+ import tomllib
5
  from pathlib import Path
6
 
7
 
 
15
  assert "Smoke Coverage Policy" in text
16
 
17
 
18
+ def test_root_env_example_is_the_single_template_source() -> None:
19
  repo_root = Path(__file__).resolve().parents[2]
20
  root_example = repo_root / ".env.example"
21
+ duplicate_example = repo_root / "config" / "env.example"
22
 
23
+ assert root_example.is_file()
24
+ assert not duplicate_example.exists()
25
+
26
+
27
+ def test_root_env_example_is_packaged_for_fcc_init() -> None:
28
+ repo_root = Path(__file__).resolve().parents[2]
29
+ pyproject = tomllib.loads((repo_root / "pyproject.toml").read_text("utf-8"))
30
+
31
+ force_include = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"][
32
+ "force-include"
33
+ ]
34
+
35
+ assert force_include[".env.example"] == "cli/env.example"
36
 
37
 
38
  def test_pyproject_first_party_packages_match_packaged_roots() -> None:
 
48
  }
49
  expected = {"api", "cli", "config", "core", "messaging", "providers", "smoke"}
50
  assert configured == expected