llm-ide-rules 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,15 @@
1
1
  """Download command: Download LLM instruction files from GitHub repositories."""
2
2
 
3
- import logging
4
3
  import re
5
4
  import tempfile
6
5
  import zipfile
7
6
  from pathlib import Path
8
- from typing import List
9
7
 
10
8
  import requests
11
- import structlog
12
9
  import typer
13
10
  from typing_extensions import Annotated
14
11
 
15
- logger = structlog.get_logger()
12
+ from llm_ide_rules.log import log
16
13
 
17
14
  DEFAULT_REPO = "iloveitaly/llm-ide-rules"
18
15
  DEFAULT_BRANCH = "master"
@@ -20,7 +17,7 @@ DEFAULT_BRANCH = "master"
20
17
 
21
18
  def normalize_repo(repo: str) -> str:
22
19
  """Normalize repository input to user/repo format.
23
-
20
+
24
21
  Handles both formats:
25
22
  - user/repo (unchanged)
26
23
  - https://github.com/user/repo/ (extracts user/repo)
@@ -28,17 +25,18 @@ def normalize_repo(repo: str) -> str:
28
25
  # If it's already in user/repo format, return as-is
29
26
  if "/" in repo and not repo.startswith("http"):
30
27
  return repo
31
-
28
+
32
29
  # Extract user/repo from GitHub URL
33
30
  github_pattern = r"https?://github\.com/([^/]+/[^/]+)/?.*"
34
31
  match = re.match(github_pattern, repo)
35
-
32
+
36
33
  if match:
37
34
  return match.group(1)
38
-
35
+
39
36
  # If no pattern matches, assume it's already in the correct format
40
37
  return repo
41
38
 
39
+
42
40
  # Define what files/directories each instruction type includes
43
41
  INSTRUCTION_TYPES = {
44
42
  "cursor": {"directories": [".cursor"], "files": []},
@@ -47,8 +45,8 @@ INSTRUCTION_TYPES = {
47
45
  "files": [],
48
46
  "exclude_patterns": ["workflows/*"],
49
47
  },
50
- "gemini": {"directories": [], "files": ["GEMINI.md"]},
51
- "claude": {"directories": [], "files": ["CLAUDE.md"]},
48
+ "gemini": {"directories": [".gemini/commands"], "files": ["GEMINI.md"]},
49
+ "claude": {"directories": [".claude/commands"], "files": ["CLAUDE.md"]},
52
50
  "agent": {"directories": [], "files": ["AGENT.md"]},
53
51
  "agents": {"directories": [], "files": [], "recursive_files": ["AGENTS.md"]},
54
52
  }
@@ -62,13 +60,19 @@ def download_and_extract_repo(repo: str, branch: str = DEFAULT_BRANCH) -> Path:
62
60
  normalized_repo = normalize_repo(repo)
63
61
  zip_url = f"https://github.com/{normalized_repo}/archive/{branch}.zip"
64
62
 
65
- logger.info("Downloading repository", repo=repo, normalized_repo=normalized_repo, branch=branch, url=zip_url)
63
+ log.info(
64
+ "downloading repository",
65
+ repo=repo,
66
+ normalized_repo=normalized_repo,
67
+ branch=branch,
68
+ url=zip_url,
69
+ )
66
70
 
67
71
  try:
68
72
  response = requests.get(zip_url, timeout=30)
69
73
  response.raise_for_status()
70
74
  except requests.RequestException as e:
71
- logger.error("Failed to download repository", error=str(e), url=zip_url)
75
+ log.error("failed to download repository", error=str(e), url=zip_url)
72
76
  raise typer.Exit(1)
73
77
 
74
78
  # Create temporary directory and file
@@ -88,24 +92,24 @@ def download_and_extract_repo(repo: str, branch: str = DEFAULT_BRANCH) -> Path:
88
92
  # Find the extracted repository directory (should be the only directory)
89
93
  repo_dirs = [d for d in extract_dir.iterdir() if d.is_dir()]
90
94
  if not repo_dirs:
91
- logger.error("No directories found in extracted ZIP")
95
+ log.error("no directories found in extracted zip")
92
96
  raise typer.Exit(1)
93
97
 
94
98
  repo_dir = repo_dirs[0]
95
- logger.info("Repository extracted", path=str(repo_dir))
99
+ log.info("repository extracted", path=str(repo_dir))
96
100
 
97
101
  return repo_dir
98
102
 
99
103
 
100
104
  def copy_instruction_files(
101
- repo_dir: Path, instruction_types: List[str], target_dir: Path
105
+ repo_dir: Path, instruction_types: list[str], target_dir: Path
102
106
  ):
103
107
  """Copy instruction files from the repository to the target directory."""
104
108
  copied_items = []
105
109
 
106
110
  for inst_type in instruction_types:
107
111
  if inst_type not in INSTRUCTION_TYPES:
108
- logger.warning("Unknown instruction type", type=inst_type)
112
+ log.warning("unknown instruction type", type=inst_type)
109
113
  continue
110
114
 
111
115
  config = INSTRUCTION_TYPES[inst_type]
@@ -116,8 +120,8 @@ def copy_instruction_files(
116
120
  target_subdir = target_dir / dir_name
117
121
 
118
122
  if source_dir.exists():
119
- logger.info(
120
- "Copying directory",
123
+ log.info(
124
+ "copying directory",
121
125
  source=str(source_dir),
122
126
  target=str(target_subdir),
123
127
  )
@@ -137,8 +141,8 @@ def copy_instruction_files(
137
141
  target_file = target_dir / file_name
138
142
 
139
143
  if source_file.exists():
140
- logger.info(
141
- "Copying file", source=str(source_file), target=str(target_file)
144
+ log.info(
145
+ "copying file", source=str(source_file), target=str(target_file)
142
146
  )
143
147
 
144
148
  # Create parent directories if needed
@@ -158,55 +162,53 @@ def copy_instruction_files(
158
162
 
159
163
  def copy_recursive_files(
160
164
  repo_dir: Path, target_dir: Path, file_pattern: str
161
- ) -> List[str]:
165
+ ) -> list[str]:
162
166
  """Recursively copy files matching pattern, preserving directory structure.
163
-
167
+
164
168
  Only copies files to locations where the target directory already exists.
165
169
  Warns and skips files where target directories don't exist.
166
-
170
+
167
171
  Args:
168
172
  repo_dir: Source repository directory
169
173
  target_dir: Target directory to copy to
170
174
  file_pattern: File pattern to search for (e.g., "AGENTS.md")
171
-
175
+
172
176
  Returns:
173
177
  List of copied file paths relative to target_dir
174
178
  """
175
179
  copied_items = []
176
-
180
+
177
181
  # Find all matching files recursively
178
182
  matching_files = list(repo_dir.rglob(file_pattern))
179
-
183
+
180
184
  for source_file in matching_files:
181
185
  # Calculate relative path from repo root
182
186
  relative_path = source_file.relative_to(repo_dir)
183
187
  target_file = target_dir / relative_path
184
-
188
+
185
189
  # Check if target directory already exists
186
190
  target_parent = target_file.parent
187
191
  if not target_parent.exists():
188
- logger.warning(
189
- "Target directory does not exist, skipping file copy",
192
+ log.warning(
193
+ "target directory does not exist, skipping file copy",
190
194
  target_directory=str(target_parent),
191
- file=str(relative_path)
195
+ file=str(relative_path),
192
196
  )
193
197
  continue
194
-
195
- logger.info(
196
- "Copying recursive file",
197
- source=str(source_file),
198
- target=str(target_file)
198
+
199
+ log.info(
200
+ "copying recursive file", source=str(source_file), target=str(target_file)
199
201
  )
200
-
202
+
201
203
  # Copy file (parent directory already exists)
202
204
  target_file.write_bytes(source_file.read_bytes())
203
205
  copied_items.append(str(relative_path))
204
-
206
+
205
207
  return copied_items
206
208
 
207
209
 
208
210
  def copy_directory_contents(
209
- source_dir: Path, target_dir: Path, exclude_patterns: List[str]
211
+ source_dir: Path, target_dir: Path, exclude_patterns: list[str]
210
212
  ):
211
213
  """Recursively copy directory contents, excluding specified patterns."""
212
214
  for item in source_dir.rglob("*"):
@@ -216,6 +218,7 @@ def copy_directory_contents(
216
218
 
217
219
  # Check if file matches any exclude pattern
218
220
  should_exclude = False
221
+ pattern = ""
219
222
  for pattern in exclude_patterns:
220
223
  if pattern.endswith("/*"):
221
224
  # Pattern like "workflows/*" - exclude if path starts with "workflows/"
@@ -228,7 +231,7 @@ def copy_directory_contents(
228
231
  break
229
232
 
230
233
  if should_exclude:
231
- logger.debug("Excluding file", file=relative_str, pattern=pattern)
234
+ log.debug("excluding file", file=relative_str, pattern=pattern)
232
235
  continue
233
236
 
234
237
  target_file = target_dir / relative_path
@@ -238,7 +241,7 @@ def copy_directory_contents(
238
241
 
239
242
  def download_main(
240
243
  instruction_types: Annotated[
241
- List[str],
244
+ list[str] | None,
242
245
  typer.Argument(
243
246
  help="Types of instructions to download (cursor, github, gemini, claude, agent, agents). Downloads everything by default."
244
247
  ),
@@ -252,9 +255,6 @@ def download_main(
252
255
  target_dir: Annotated[
253
256
  str, typer.Option("--target", "-t", help="Target directory to download to")
254
257
  ] = ".",
255
- verbose: Annotated[
256
- bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
257
- ] = False,
258
258
  ):
259
259
  """Download LLM instruction files from GitHub repositories.
260
260
 
@@ -279,12 +279,6 @@ def download_main(
279
279
  # Download to a specific directory
280
280
  llm_ide_rules download --target ./my-project
281
281
  """
282
- if verbose:
283
- logging.basicConfig(level=logging.DEBUG)
284
- structlog.configure(
285
- wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
286
- )
287
-
288
282
  # Use default types if none specified
289
283
  if not instruction_types:
290
284
  instruction_types = DEFAULT_TYPES
@@ -292,17 +286,19 @@ def download_main(
292
286
  # Validate instruction types
293
287
  invalid_types = [t for t in instruction_types if t not in INSTRUCTION_TYPES]
294
288
  if invalid_types:
295
- logger.error(
296
- "Invalid instruction types",
289
+ log.error(
290
+ "invalid instruction types",
297
291
  invalid_types=invalid_types,
298
292
  valid_types=list(INSTRUCTION_TYPES.keys()),
299
293
  )
294
+ error_msg = f"Invalid instruction types: {', '.join(invalid_types)}"
295
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
300
296
  raise typer.Exit(1)
301
297
 
302
298
  target_path = Path(target_dir).resolve()
303
299
 
304
- logger.info(
305
- "Starting download",
300
+ log.info(
301
+ "starting download",
306
302
  repo=repo,
307
303
  branch=branch,
308
304
  instruction_types=instruction_types,
@@ -317,12 +313,12 @@ def download_main(
317
313
  copied_items = copy_instruction_files(repo_dir, instruction_types, target_path)
318
314
 
319
315
  if copied_items:
320
- logger.info("Download completed successfully", copied_items=copied_items)
321
- typer.echo(f"Downloaded {len(copied_items)} items to {target_path}:")
316
+ success_msg = f"Downloaded {len(copied_items)} items to {target_path}:"
317
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
322
318
  for item in copied_items:
323
319
  typer.echo(f" - {item}")
324
320
  else:
325
- logger.warning("No files were copied")
321
+ log.warning("no files were copied")
326
322
  typer.echo("No matching instruction files found in the repository.")
327
323
 
328
324
  finally: