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

1"""Project initialization command implementation.""" 

2 

3import shutil 

4from pathlib import Path 

5 

6from rich.console import Console 

7from rich.progress import Progress, SpinnerColumn, TextColumn 

8from rich.prompt import Confirm 

9 

10from golf.cli.branding import ( 

11 create_success_message, 

12 create_info_panel, 

13 STATUS_ICONS, 

14 GOLF_ORANGE, 

15) 

16 

17from golf.core.telemetry import ( 

18 track_command, 

19 track_event, 

20 set_telemetry_enabled, 

21 load_telemetry_preference, 

22) 

23 

24console = Console() 

25 

26 

27def initialize_project( 

28 project_name: str, 

29 output_dir: Path, 

30) -> None: 

31 """Initialize a new GolfMCP project. 

32 

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" 

40 

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 

52 

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) 

64 

65 # Find template directory within the installed package 

66 import golf 

67 

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 

72 

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 

82 

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) 

92 

93 # Copy directory structure 

94 _copy_template(template_dir, output_dir, project_name) 

95 

96 # Ask for telemetry consent 

97 _prompt_for_telemetry_consent() 

98 

99 # Show success message 

100 console.print() 

101 create_success_message("Project initialized successfully!", console) 

102 

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) 

106 

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) 

113 

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) 

116 

117 # Re-raise to maintain existing behavior 

118 raise 

119 

120 

121def _copy_template(source_dir: Path, target_dir: Path, project_name: str) -> None: 

122 """Copy template files to the target directory, with variable substitution. 

123 

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) 

133 

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 

139 

140 # Compute relative path 

141 rel_path = source_path.relative_to(source_dir) 

142 target_path = target_dir / rel_path 

143 

144 # Create parent directories if needed 

145 target_path.parent.mkdir(parents=True, exist_ok=True) 

146 

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() 

151 

152 # Replace template variables 

153 content = content.replace("{{project_name}}", project_name) 

154 content = content.replace("{{project_name_lowercase}}", project_name.lower()) 

155 

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) 

161 

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") 

198 

199 

200def _prompt_for_telemetry_consent() -> None: 

201 """Prompt user for telemetry consent and save their preference.""" 

202 import os 

203 

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 

208 

209 # Skip if telemetry is explicitly disabled in environment 

210 if os.environ.get("GOLF_TELEMETRY", "").lower() in ("0", "false", "no", "off"): 

211 return 

212 

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 

217 

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() 

237 

238 enable_telemetry = Confirm.ask("[bold]Enable anonymous usage analytics?[/bold]", default=False) 

239 

240 set_telemetry_enabled(enable_telemetry, persist=True) 

241 

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() 

247 

248 

249def _is_text_file(path: Path) -> bool: 

250 """Check if a file is a text file that needs variable substitution. 

251 

252 Args: 

253 path: Path to check 

254 

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 } 

275 

276 # Check if the file has a text extension 

277 if path.suffix in text_extensions: 

278 return True 

279 

280 # Check specific filenames without extensions 

281 if path.name in {".gitignore", "README", "LICENSE"}: 

282 return True 

283 

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