Skip to content

Commit 997b669

Browse files
committed
Added init command
Signed-off-by: Nick Brook <[email protected]>
1 parent f848c78 commit 997b669

File tree

4 files changed

+238
-13
lines changed

4 files changed

+238
-13
lines changed

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Run `code-recap` from a directory containing git repositories:
7474

7575
### For Consultants (Optional)
7676

77-
If you work with multiple clients, configure `config/config.yaml` to organize reports by client:
77+
If you work with multiple clients, create a `config.yaml` to organize reports by client (see [example config](config.example/config.yaml)):
7878

7979
```yaml
8080
clients:
@@ -121,13 +121,15 @@ pip install -e .
121121

122122
### Configuration (Optional)
123123

124-
For multi-client workflows, create a config file:
124+
For multi-client workflows, create a `config.yaml`:
125125

126126
```bash
127-
mkdir -p config && cp -r config.example/* config/
128-
# Edit config/config.yaml to define your clients
127+
code-recap init # Creates template config.yaml
128+
# Edit config.yaml to define your clients
129129
```
130130

131+
Or copy the [full example](config.example/config.yaml) with all options documented.
132+
131133
---
132134

133135
## Recommended Models
@@ -275,13 +277,15 @@ code-recap git unarchive my-project --execute
275277

276278
Configuration is **optional**. For basic use, just run the scripts—no config needed.
277279

278-
For customization, copy examples from `config.example/`:
280+
For customization, create a template config:
279281

280282
```bash
281-
cp -r config.example/* config/
283+
code-recap init # Creates config.yaml with commented examples
282284
```
283285

284-
### Client Configuration (`config/config.yaml`) — Optional
286+
Or copy the [full example](config.example/config.yaml) with all options documented.
287+
288+
### Client Configuration (`config.yaml`) — Optional
285289

286290
If you're a consultant working with multiple clients, configure project-to-client mapping:
287291

@@ -314,7 +318,7 @@ clients:
314318
- First match wins (order matters in YAML)
315319
- Unmatched projects go to "Other"
316320

317-
### Exclusion Patterns (`config/excludes.yaml`)
321+
### Exclusion Patterns (`excludes.yaml`)
318322

319323
Configure files/directories to exclude from line count statistics:
320324

scripts/bump-version.sh

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,23 @@ echo "✓ Updated pyproject.toml"
7171
sed -i '' "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" src/code_recap/__init__.py
7272
echo "✓ Updated src/code_recap/__init__.py"
7373

74+
# Update uv.lock
75+
if command -v uv &> /dev/null; then
76+
uv lock --quiet
77+
echo "✓ Updated uv.lock"
78+
fi
79+
7480
# Show changes
7581
echo ""
7682
echo "Changes:"
77-
git diff --color pyproject.toml src/code_recap/__init__.py
83+
git diff --color pyproject.toml src/code_recap/__init__.py uv.lock
7884

7985
echo ""
8086
read -p "Commit and tag v$NEW_VERSION? [y/N] " -n 1 -r
8187
echo
8288

8389
if [[ $REPLY =~ ^[Yy]$ ]]; then
84-
git add pyproject.toml src/code_recap/__init__.py
90+
git add pyproject.toml src/code_recap/__init__.py uv.lock
8591
git commit -m "Bump version to $NEW_VERSION"
8692
git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION"
8793

@@ -92,8 +98,10 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
9298
echo " git push && git push origin v$NEW_VERSION"
9399
else
94100
echo ""
95-
echo "Changes staged but not committed. To finish manually:"
96-
echo " git add -A && git commit -m 'Bump version to $NEW_VERSION'"
101+
echo "Aborted. Files were modified but not committed."
102+
echo "To finish manually:"
103+
echo " git add pyproject.toml src/code_recap/__init__.py uv.lock"
104+
echo " git commit -m 'Bump version to $NEW_VERSION'"
97105
echo " git tag -a v$NEW_VERSION -m 'Release v$NEW_VERSION'"
98106
echo " git push && git push origin v$NEW_VERSION"
99107
fi

src/code_recap/cli.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ def main(argv: Optional[list[str]] = None) -> int:
7676

7777
return git_main(sub_argv)
7878

79+
elif subcommand in ("init", "config"):
80+
return init_config(sub_argv)
81+
7982
elif subcommand == "help":
8083
if sub_argv:
8184
# Show help for specific subcommand
@@ -89,6 +92,215 @@ def main(argv: Optional[list[str]] = None) -> int:
8992
return 1
9093

9194

95+
def init_config(argv: list[str]) -> int:
96+
"""Creates config files and sets up API keys.
97+
98+
Args:
99+
argv: Command-line arguments.
100+
101+
Returns:
102+
Exit code.
103+
"""
104+
import argparse
105+
import getpass
106+
import os
107+
from pathlib import Path
108+
109+
parser = argparse.ArgumentParser(
110+
prog="code-recap init",
111+
description="Initialize code-recap configuration and API keys",
112+
)
113+
parser.add_argument(
114+
"-o",
115+
"--output",
116+
default="config.yaml",
117+
help="Config file path (default: config.yaml)",
118+
)
119+
parser.add_argument(
120+
"-f",
121+
"--force",
122+
action="store_true",
123+
help="Overwrite existing files",
124+
)
125+
parser.add_argument(
126+
"--no-keys",
127+
action="store_true",
128+
help="Skip API key setup",
129+
)
130+
parser.add_argument(
131+
"--keys-only",
132+
action="store_true",
133+
help="Only set up API keys, skip config.yaml",
134+
)
135+
args = parser.parse_args(argv)
136+
137+
output_path = Path(args.output)
138+
created_files: list[str] = []
139+
140+
# Create config.yaml
141+
if not args.keys_only:
142+
if output_path.exists() and not args.force:
143+
print(f"Config file {output_path} already exists (use --force to overwrite)")
144+
else:
145+
template = '''# Code Recap Configuration
146+
# See: https://github.com/NRB-Tech/code-recap
147+
148+
# Global context for LLM summaries (optional)
149+
# global_context: |
150+
# Brief description of your work or company for context in summaries.
151+
152+
# Client configuration (optional - for consultants with multiple clients)
153+
# clients:
154+
# "Client Name":
155+
# directories:
156+
# - "project-*" # Glob patterns for repo names
157+
# - "another-repo" # Exact match
158+
# exclude:
159+
# - "*-archive" # Exclude patterns
160+
# context: |
161+
# Brief description of work for this client.
162+
#
163+
# "Another Client":
164+
# directories:
165+
# - "client2-*"
166+
167+
# Assign unmatched repos to a default client (optional)
168+
# default_client: Personal
169+
170+
# File patterns to exclude from statistics (optional)
171+
# excludes:
172+
# global:
173+
# - "*.lock"
174+
# - "package-lock.json"
175+
# - "*/node_modules/*"
176+
# - "*/build/*"
177+
# - "*.min.js"
178+
# projects:
179+
# MyProject:
180+
# - "vendor/*"
181+
182+
# HTML report branding (optional)
183+
# html_report:
184+
# company_name: "Your Company"
185+
# logo_url: "https://example.com/logo.png"
186+
# primary_color: "#2563eb"
187+
'''
188+
output_path.write_text(template)
189+
created_files.append(str(output_path))
190+
191+
# Set up API keys
192+
if not args.no_keys:
193+
print("\n" + "=" * 50)
194+
print("API Key Setup (press Enter to skip any)")
195+
print("=" * 50)
196+
print("\nGet API keys from:")
197+
print(" OpenAI: https://platform.openai.com/api-keys")
198+
print(" Gemini: https://aistudio.google.com/apikey")
199+
print(" Anthropic: https://console.anthropic.com/settings/keys")
200+
print()
201+
202+
keys_dir = Path("keys")
203+
keys: dict[str, tuple[str, str]] = {} # name -> (env_var, key)
204+
205+
# Prompt for each key
206+
for name, env_var, example in [
207+
("OpenAI", "OPENAI_API_KEY", "sk-..."),
208+
("Gemini", "GEMINI_API_KEY", "AI..."),
209+
("Anthropic", "ANTHROPIC_API_KEY", "sk-ant-..."),
210+
]:
211+
# Check if already set in environment
212+
existing = os.environ.get(env_var)
213+
if existing:
214+
print(f"{name}: Already set in environment ✓")
215+
continue
216+
217+
try:
218+
key = getpass.getpass(f"{name} API key ({example}): ").strip()
219+
if key:
220+
keys[name.lower()] = (env_var, key)
221+
except (KeyboardInterrupt, EOFError):
222+
print("\nSkipping remaining keys...")
223+
break
224+
225+
# Create key files if any keys were entered
226+
if keys:
227+
keys_dir.mkdir(exist_ok=True)
228+
229+
for name, (env_var, key) in keys.items():
230+
key_file = keys_dir / f"{name}.sh"
231+
key_file.write_text(f"#!/bin/bash\nexport {env_var}='{key}'\n")
232+
key_file.chmod(0o600) # Restrict permissions
233+
created_files.append(str(key_file))
234+
235+
# Create all.sh to source all keys
236+
all_file = keys_dir / "all.sh"
237+
all_content = "#!/bin/bash\n# Source all API keys\n"
238+
for name in keys:
239+
all_content += f'source "$(dirname "$0")/{name}.sh"\n'
240+
all_file.write_text(all_content)
241+
all_file.chmod(0o700)
242+
created_files.append(str(all_file))
243+
244+
print(f"\nCreated {len(keys)} key file(s) in {keys_dir}/")
245+
print("To load keys: source keys/all.sh")
246+
247+
# Add keys/ to .gitignore if not already
248+
gitignore = Path(".gitignore")
249+
if gitignore.exists():
250+
content = gitignore.read_text()
251+
if "keys/" not in content and "/keys" not in content:
252+
with gitignore.open("a") as f:
253+
f.write("\n# API keys\nkeys/\n")
254+
print("Added keys/ to .gitignore")
255+
256+
# Summary
257+
print("\n" + "=" * 50)
258+
if created_files:
259+
print("Created files:")
260+
for f in created_files:
261+
print(f" ✓ {f}")
262+
else:
263+
print("No files created.")
264+
265+
# Get author name for examples
266+
author = _get_git_author() or "Your Name"
267+
268+
# Next steps
269+
print("\n" + "=" * 50)
270+
print("Try these commands:")
271+
print("=" * 50)
272+
print(f"""
273+
# Generate a year-in-review summary
274+
code-recap summarize 2025 --author "{author}" --html --open
275+
276+
# Quick daily summary for time logging
277+
code-recap daily --author "{author}"
278+
279+
# Statistics only (no LLM, no API key needed)
280+
code-recap stats 2025 --author "{author}" --format markdown
281+
""")
282+
print("Documentation: https://github.com/NRB-Tech/code-recap")
283+
return 0
284+
285+
286+
def _get_git_author() -> str | None:
287+
"""Gets the git user name from config."""
288+
import subprocess
289+
290+
try:
291+
result = subprocess.run(
292+
["git", "config", "user.name"],
293+
capture_output=True,
294+
text=True,
295+
timeout=5,
296+
)
297+
if result.returncode == 0:
298+
return result.stdout.strip()
299+
except Exception:
300+
pass
301+
return None
302+
303+
92304
def print_help() -> None:
93305
"""Prints the main help message."""
94306
from code_recap import __version__
@@ -106,6 +318,7 @@ def print_help() -> None:
106318
commits List commits for a specific date
107319
deploy Deploy HTML reports (zip, Cloudflare)
108320
git, repos Repository utilities (fetch, archive)
321+
init Create a template config.yaml file
109322
110323
Quick start:
111324
code-recap summarize 2025 --author "Your Name"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)