@@ -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 ("\n Get 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 ("\n Skipping 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\n export { 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"\n Created { 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\n keys/\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+
92304def 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
110323Quick start:
111324 code-recap summarize 2025 --author "Your Name"
0 commit comments