Coverage for /Users/antonigmitruk/golf/src/golf/commands/init.py: 0%
142 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-16 18:46 +0200
« prev ^ index » next coverage.py v7.6.12, created at 2025-08-16 18:46 +0200
1"""Project initialization command implementation."""
3import shutil
4from pathlib import Path
6from rich.console import Console
7from rich.progress import Progress, SpinnerColumn, TextColumn
8from rich.prompt import Confirm
10from golf.cli.branding import (
11 create_success_message,
12 create_info_panel,
13 STATUS_ICONS,
14 GOLF_ORANGE,
15)
17from golf.core.telemetry import (
18 track_command,
19 track_event,
20 set_telemetry_enabled,
21 load_telemetry_preference,
22)
24console = Console()
27def initialize_project(
28 project_name: str,
29 output_dir: Path,
30) -> None:
31 """Initialize a new GolfMCP project.
33 Args:
34 project_name: Name of the project
35 output_dir: Directory where the project will be created
36 """
37 try:
38 # Use the basic template by default
39 template = "basic"
41 # Check if directory exists
42 if output_dir.exists():
43 if not output_dir.is_dir():
44 console.print(f"[bold red]Error:[/bold red] '{output_dir}' exists but is not a directory.")
45 track_command(
46 "init",
47 success=False,
48 error_type="NotADirectory",
49 error_message="Target exists but is not a directory",
50 )
51 return
53 # Check if directory is empty
54 if any(output_dir.iterdir()) and not Confirm.ask(
55 f"Directory '{output_dir}' is not empty. Continue anyway?",
56 default=False,
57 ):
58 console.print("Initialization cancelled.")
59 track_event("cli_init_cancelled", {"success": False})
60 return
61 else:
62 # Create the directory
63 output_dir.mkdir(parents=True)
65 # Find template directory within the installed package
66 import golf
68 package_init_file = Path(golf.__file__)
69 # The 'examples' directory is now inside the 'golf' package directory
70 # e.g. golf/examples/basic, so go up one from __init__.py to get to 'golf'
71 template_dir = package_init_file.parent / "examples" / template
73 if not template_dir.exists():
74 console.print(f"[bold red]Error:[/bold red] Could not find template '{template}'")
75 track_command(
76 "init",
77 success=False,
78 error_type="TemplateNotFound",
79 error_message=f"Template directory not found: {template}",
80 )
81 return
83 # Copy template files
84 with Progress(
85 SpinnerColumn(),
86 TextColumn(
87 f"[bold {GOLF_ORANGE}]{STATUS_ICONS['building']} Creating project structure...[/bold {GOLF_ORANGE}]"
88 ),
89 transient=True,
90 ) as progress:
91 progress.add_task("copying", total=None)
93 # Copy directory structure
94 _copy_template(template_dir, output_dir, project_name)
96 # Ask for telemetry consent
97 _prompt_for_telemetry_consent()
99 # Show success message
100 console.print()
101 create_success_message("Project initialized successfully!", console)
103 # Show next steps
104 next_steps = f"cd {output_dir.name}\ngolf build dev\ngolf run"
105 create_info_panel("Next Steps", next_steps, console)
107 # Track successful initialization
108 track_event("cli_init_success", {"success": True, "template": template})
109 except Exception as e:
110 # Capture error details for telemetry
111 error_type = type(e).__name__
112 error_message = str(e)
114 console.print(f"[bold red]Error during initialization:[/bold red] {error_message}")
115 track_command("init", success=False, error_type=error_type, error_message=error_message)
117 # Re-raise to maintain existing behavior
118 raise
121def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None:
122 """Copy template files to the target directory, with variable substitution.
124 Args:
125 source_dir: Source template directory
126 target_dir: Target project directory
127 project_name: Name of the project (for substitutions)
128 """
129 # Create standard directory structure
130 (target_dir / "tools").mkdir(exist_ok=True)
131 (target_dir / "resources").mkdir(exist_ok=True)
132 (target_dir / "prompts").mkdir(exist_ok=True)
134 # Copy all files from the template
135 for source_path in source_dir.glob("**/*"):
136 # Skip if directory (we'll create directories as needed)
137 if source_path.is_dir():
138 continue
140 # Compute relative path
141 rel_path = source_path.relative_to(source_dir)
142 target_path = target_dir / rel_path
144 # Create parent directories if needed
145 target_path.parent.mkdir(parents=True, exist_ok=True)
147 # Copy and substitute content for text files
148 if _is_text_file(source_path):
149 with open(source_path, encoding="utf-8") as f:
150 content = f.read()
152 # Replace template variables
153 content = content.replace("{{project_name}}", project_name)
154 content = content.replace("{{project_name_lowercase}}", project_name.lower())
156 with open(target_path, "w", encoding="utf-8") as f:
157 f.write(content)
158 else:
159 # Binary file, just copy
160 shutil.copy2(source_path, target_path)
162 # Create a .gitignore if it doesn't exist
163 gitignore_file = target_dir / ".gitignore"
164 if not gitignore_file.exists():
165 with open(gitignore_file, "w", encoding="utf-8") as f:
166 f.write("# Python\n")
167 f.write("__pycache__/\n")
168 f.write("*.py[cod]\n")
169 f.write("*$py.class\n")
170 f.write("*.so\n")
171 f.write(".Python\n")
172 f.write("env/\n")
173 f.write("build/\n")
174 f.write("develop-eggs/\n")
175 f.write("dist/\n")
176 f.write("downloads/\n")
177 f.write("eggs/\n")
178 f.write(".eggs/\n")
179 f.write("lib/\n")
180 f.write("lib64/\n")
181 f.write("parts/\n")
182 f.write("sdist/\n")
183 f.write("var/\n")
184 f.write("*.egg-info/\n")
185 f.write(".installed.cfg\n")
186 f.write("*.egg\n\n")
187 f.write("# Environment\n")
188 f.write(".env\n")
189 f.write(".venv\n")
190 f.write("env/\n")
191 f.write("venv/\n")
192 f.write("ENV/\n")
193 f.write("env.bak/\n")
194 f.write("venv.bak/\n\n")
195 f.write("# GolfMCP\n")
196 f.write(".golf/\n")
197 f.write("dist/\n")
200def _prompt_for_telemetry_consent() -> None:
201 """Prompt user for telemetry consent and save their preference."""
202 import os
204 # Skip prompt in test mode, when telemetry is explicitly disabled, or if
205 # preference already exists
206 if os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on"):
207 return
209 # Skip if telemetry is explicitly disabled in environment
210 if os.environ.get("GOLF_TELEMETRY", "").lower() in ("0", "false", "no", "off"):
211 return
213 # Check if user already has a saved preference
214 existing_preference = load_telemetry_preference()
215 if existing_preference is not None:
216 return # User already made a choice
218 console.print()
219 console.rule("[bold blue]Anonymous usage analytics[/bold blue]", style="blue")
220 console.print()
221 console.print("Golf can collect [bold]anonymous usage analytics[/bold] to help improve the tool.")
222 console.print()
223 console.print("[dim]What we collect:[/dim]")
224 console.print(" • Command usage (init, build, run)")
225 console.print(" • Error types (to fix bugs)")
226 console.print(" • Golf version and Python version")
227 console.print(" • Operating system type")
228 console.print()
229 console.print("[dim]What we DON'T collect:[/dim]")
230 console.print(" • Your code or project content")
231 console.print(" • File paths or project names")
232 console.print(" • Personal information")
233 console.print(" • IP addresses")
234 console.print()
235 console.print("You can change this anytime by setting GOLF_TELEMETRY=0 in your environment.")
236 console.print()
238 enable_telemetry = Confirm.ask("[bold]Enable anonymous usage analytics?[/bold]", default=False)
240 set_telemetry_enabled(enable_telemetry, persist=True)
242 if enable_telemetry:
243 console.print("[green]✓[/green] Anonymous analytics enabled")
244 else:
245 console.print("[yellow]○[/yellow] Anonymous analytics disabled")
246 console.print()
249def _is_text_file(path: Path) -> bool:
250 """Check if a file is a text file that needs variable substitution.
252 Args:
253 path: Path to check
255 Returns:
256 True if the file is a text file
257 """
258 # List of known text file extensions
259 text_extensions = {
260 ".py",
261 ".md",
262 ".txt",
263 ".html",
264 ".css",
265 ".js",
266 ".json",
267 ".yml",
268 ".yaml",
269 ".toml",
270 ".ini",
271 ".cfg",
272 ".env",
273 ".example",
274 }
276 # Check if the file has a text extension
277 if path.suffix in text_extensions:
278 return True
280 # Check specific filenames without extensions
281 if path.name in {".gitignore", "README", "LICENSE"}:
282 return True
284 # Try to detect if it's a text file by reading a bit of it
285 try:
286 with open(path, encoding="utf-8") as f:
287 f.read(1024)
288 return True
289 except UnicodeDecodeError:
290 return False