llm-ide-rules 0.6.0__py3-none-any.whl → 0.8.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.
- llm_ide_rules/__init__.py +20 -1
- llm_ide_rules/agents/__init__.py +4 -0
- llm_ide_rules/agents/agents.py +124 -0
- llm_ide_rules/agents/base.py +63 -6
- llm_ide_rules/agents/claude.py +31 -4
- llm_ide_rules/agents/cursor.py +64 -14
- llm_ide_rules/agents/gemini.py +44 -4
- llm_ide_rules/agents/github.py +49 -13
- llm_ide_rules/agents/opencode.py +14 -4
- llm_ide_rules/agents/vscode.py +88 -0
- llm_ide_rules/commands/config.py +46 -0
- llm_ide_rules/commands/delete.py +111 -5
- llm_ide_rules/commands/download.py +123 -14
- llm_ide_rules/commands/explode.py +169 -170
- llm_ide_rules/commands/implode.py +25 -56
- llm_ide_rules/commands/mcp.py +1 -1
- llm_ide_rules/constants.py +1 -27
- llm_ide_rules/markdown_parser.py +108 -0
- llm_ide_rules/utils.py +118 -0
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/METADATA +4 -3
- llm_ide_rules-0.8.0.dist-info/RECORD +27 -0
- llm_ide_rules/sections.json +0 -17
- llm_ide_rules-0.6.0.dist-info/RECORD +0 -23
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/WHEEL +0 -0
- {llm_ide_rules-0.6.0.dist-info → llm_ide_rules-0.8.0.dist-info}/entry_points.txt +0 -0
llm_ide_rules/agents/github.py
CHANGED
|
@@ -28,7 +28,7 @@ class GitHubAgent(BaseAgent):
|
|
|
28
28
|
mcp_project_path = ".copilot/mcp-config.json"
|
|
29
29
|
|
|
30
30
|
def bundle_rules(
|
|
31
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
31
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
32
32
|
) -> bool:
|
|
33
33
|
"""Bundle GitHub instruction files into a single output file."""
|
|
34
34
|
rules_dir = self.rules_dir
|
|
@@ -46,7 +46,7 @@ class GitHubAgent(BaseAgent):
|
|
|
46
46
|
instr_files = list(instructions_path.glob(f"*{rule_ext}"))
|
|
47
47
|
|
|
48
48
|
ordered_instructions = get_ordered_files_github(
|
|
49
|
-
instr_files, list(section_globs.keys())
|
|
49
|
+
instr_files, list(section_globs.keys()) if section_globs else None
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
content_parts: list[str] = []
|
|
@@ -57,15 +57,21 @@ class GitHubAgent(BaseAgent):
|
|
|
57
57
|
content_parts.append("\n\n")
|
|
58
58
|
|
|
59
59
|
for instr_file in ordered_instructions:
|
|
60
|
-
|
|
61
|
-
if not
|
|
60
|
+
file_content = instr_file.read_text().strip()
|
|
61
|
+
if not file_content:
|
|
62
62
|
continue
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
apply_to_pattern = self._extract_apply_to_from_frontmatter(file_content)
|
|
65
|
+
|
|
66
|
+
content = strip_yaml_frontmatter(file_content)
|
|
65
67
|
content = strip_header(content)
|
|
66
68
|
base_stem = instr_file.stem.replace(".instructions", "")
|
|
67
|
-
header = resolve_header_from_stem(
|
|
69
|
+
header = resolve_header_from_stem(
|
|
70
|
+
base_stem, section_globs if section_globs else {}
|
|
71
|
+
)
|
|
68
72
|
content_parts.append(f"## {header}\n\n")
|
|
73
|
+
if apply_to_pattern:
|
|
74
|
+
content_parts.append(f"globs: {apply_to_pattern}\n\n")
|
|
69
75
|
content_parts.append(content)
|
|
70
76
|
content_parts.append("\n\n")
|
|
71
77
|
|
|
@@ -75,8 +81,23 @@ class GitHubAgent(BaseAgent):
|
|
|
75
81
|
output_file.write_text("".join(content_parts))
|
|
76
82
|
return True
|
|
77
83
|
|
|
84
|
+
def _extract_apply_to_from_frontmatter(self, content: str) -> str | None:
|
|
85
|
+
"""Extract applyTo pattern from YAML frontmatter."""
|
|
86
|
+
lines = content.splitlines()
|
|
87
|
+
if not lines or lines[0].strip() != "---":
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
for i in range(1, len(lines)):
|
|
91
|
+
if lines[i].strip() == "---":
|
|
92
|
+
break
|
|
93
|
+
if lines[i].startswith("applyTo:"):
|
|
94
|
+
apply_to_value = lines[i][8:].strip().strip('"').strip("'")
|
|
95
|
+
return apply_to_value if apply_to_value else None
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
78
99
|
def bundle_commands(
|
|
79
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
100
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
80
101
|
) -> bool:
|
|
81
102
|
"""Bundle GitHub prompt files into a single output file."""
|
|
82
103
|
commands_dir = self.commands_dir
|
|
@@ -101,11 +122,12 @@ class GitHubAgent(BaseAgent):
|
|
|
101
122
|
prompt_dict[base_stem] = f
|
|
102
123
|
|
|
103
124
|
ordered_prompts = []
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
if section_globs:
|
|
126
|
+
for section_name in section_globs.keys():
|
|
127
|
+
filename = header_to_filename(section_name)
|
|
128
|
+
if filename in prompt_dict:
|
|
129
|
+
ordered_prompts.append(prompt_dict[filename])
|
|
130
|
+
del prompt_dict[filename]
|
|
109
131
|
|
|
110
132
|
remaining_prompts = sorted(prompt_dict.values(), key=lambda p: p.name)
|
|
111
133
|
ordered_prompts.extend(remaining_prompts)
|
|
@@ -119,7 +141,9 @@ class GitHubAgent(BaseAgent):
|
|
|
119
141
|
content = strip_yaml_frontmatter(content)
|
|
120
142
|
content = strip_header(content)
|
|
121
143
|
base_stem = prompt_file.stem.replace(".prompt", "")
|
|
122
|
-
header = resolve_header_from_stem(
|
|
144
|
+
header = resolve_header_from_stem(
|
|
145
|
+
base_stem, section_globs if section_globs else {}
|
|
146
|
+
)
|
|
123
147
|
content_parts.append(f"## {header}\n\n")
|
|
124
148
|
content_parts.append(content)
|
|
125
149
|
content_parts.append("\n\n")
|
|
@@ -136,6 +160,7 @@ class GitHubAgent(BaseAgent):
|
|
|
136
160
|
filename: str,
|
|
137
161
|
rules_dir: Path,
|
|
138
162
|
glob_pattern: str | None = None,
|
|
163
|
+
description: str | None = None,
|
|
139
164
|
) -> None:
|
|
140
165
|
"""Write a GitHub instruction file (.instructions.md) with YAML frontmatter."""
|
|
141
166
|
extension = self.rule_extension or ".instructions.md"
|
|
@@ -167,6 +192,7 @@ applyTo: "{glob_pattern}"
|
|
|
167
192
|
)
|
|
168
193
|
|
|
169
194
|
frontmatter = f"---\nmode: 'agent'\ndescription: '{description}'\n---\n"
|
|
195
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
170
196
|
filepath.write_text(frontmatter + "".join(filtered_content))
|
|
171
197
|
|
|
172
198
|
def write_general_instructions(
|
|
@@ -205,3 +231,13 @@ applyTo: "{glob_pattern}"
|
|
|
205
231
|
args=config.get("args", []),
|
|
206
232
|
env=config.get("env"),
|
|
207
233
|
)
|
|
234
|
+
|
|
235
|
+
def configure_agents_md(self, base_dir: Path) -> bool:
|
|
236
|
+
"""Configure VS Code to use AGENTS.md."""
|
|
237
|
+
from llm_ide_rules.utils import modify_json_file
|
|
238
|
+
|
|
239
|
+
settings_path = base_dir / ".vscode" / "settings.json"
|
|
240
|
+
|
|
241
|
+
updates = {"chat.useAgentsMdFile": True, "chat.useNestedAgentsMdFiles": True}
|
|
242
|
+
|
|
243
|
+
return modify_json_file(settings_path, updates)
|
llm_ide_rules/agents/opencode.py
CHANGED
|
@@ -25,13 +25,13 @@ class OpenCodeAgent(BaseAgent):
|
|
|
25
25
|
mcp_root_key = "mcp"
|
|
26
26
|
|
|
27
27
|
def bundle_rules(
|
|
28
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
28
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
29
29
|
) -> bool:
|
|
30
30
|
"""OpenCode doesn't support rules."""
|
|
31
31
|
return False
|
|
32
32
|
|
|
33
33
|
def bundle_commands(
|
|
34
|
-
self, output_file: Path, section_globs: dict[str, str | None]
|
|
34
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
35
35
|
) -> bool:
|
|
36
36
|
"""Bundle OpenCode command files (.md) into a single output file."""
|
|
37
37
|
commands_dir = self.commands_dir
|
|
@@ -50,7 +50,9 @@ class OpenCodeAgent(BaseAgent):
|
|
|
50
50
|
if not command_files:
|
|
51
51
|
return False
|
|
52
52
|
|
|
53
|
-
ordered_commands = get_ordered_files(
|
|
53
|
+
ordered_commands = get_ordered_files(
|
|
54
|
+
command_files, list(section_globs.keys()) if section_globs else None
|
|
55
|
+
)
|
|
54
56
|
|
|
55
57
|
content_parts: list[str] = []
|
|
56
58
|
for command_file in ordered_commands:
|
|
@@ -58,7 +60,9 @@ class OpenCodeAgent(BaseAgent):
|
|
|
58
60
|
if not content:
|
|
59
61
|
continue
|
|
60
62
|
|
|
61
|
-
header = resolve_header_from_stem(
|
|
63
|
+
header = resolve_header_from_stem(
|
|
64
|
+
command_file.stem, section_globs if section_globs else {}
|
|
65
|
+
)
|
|
62
66
|
content_parts.append(f"## {header}\n\n")
|
|
63
67
|
content_parts.append(content)
|
|
64
68
|
content_parts.append("\n\n")
|
|
@@ -75,6 +79,7 @@ class OpenCodeAgent(BaseAgent):
|
|
|
75
79
|
filename: str,
|
|
76
80
|
rules_dir: Path,
|
|
77
81
|
glob_pattern: str | None = None,
|
|
82
|
+
description: str | None = None,
|
|
78
83
|
) -> None:
|
|
79
84
|
"""OpenCode doesn't support rules."""
|
|
80
85
|
pass
|
|
@@ -91,6 +96,7 @@ class OpenCodeAgent(BaseAgent):
|
|
|
91
96
|
filepath = commands_dir / f"{filename}{extension}"
|
|
92
97
|
|
|
93
98
|
trimmed = trim_content(content_lines)
|
|
99
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
94
100
|
filepath.write_text("".join(trimmed))
|
|
95
101
|
|
|
96
102
|
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
@@ -124,3 +130,7 @@ class OpenCodeAgent(BaseAgent):
|
|
|
124
130
|
args=command_array[1:] if len(command_array) > 1 else [],
|
|
125
131
|
env=config.get("environment"),
|
|
126
132
|
)
|
|
133
|
+
|
|
134
|
+
def configure_agents_md(self, base_dir: Path) -> bool:
|
|
135
|
+
"""OpenCode has native support, no configuration needed."""
|
|
136
|
+
return False
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""VS Code agent implementation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from llm_ide_rules.agents.base import BaseAgent
|
|
6
|
+
from llm_ide_rules.mcp import McpServer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VSCodeAgent(BaseAgent):
|
|
10
|
+
"""Agent for VS Code (native MCP support)."""
|
|
11
|
+
|
|
12
|
+
name = "vscode"
|
|
13
|
+
rules_dir = None # VS Code typically uses .github (handled by GitHubAgent)
|
|
14
|
+
commands_dir = None
|
|
15
|
+
rule_extension = None
|
|
16
|
+
command_extension = None
|
|
17
|
+
|
|
18
|
+
mcp_global_path = None # VS Code user settings are complex, focusing on workspace
|
|
19
|
+
mcp_project_path = ".vscode/mcp.json"
|
|
20
|
+
mcp_root_key = "servers"
|
|
21
|
+
|
|
22
|
+
def bundle_rules(
|
|
23
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""VS Code doesn't support rules directly (uses GitHub Copilot)."""
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def bundle_commands(
|
|
29
|
+
self, output_file: Path, section_globs: dict[str, str | None] | None = None
|
|
30
|
+
) -> bool:
|
|
31
|
+
"""VS Code doesn't support commands directly."""
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def write_rule(
|
|
35
|
+
self,
|
|
36
|
+
content_lines: list[str],
|
|
37
|
+
filename: str,
|
|
38
|
+
rules_dir: Path,
|
|
39
|
+
glob_pattern: str | None = None,
|
|
40
|
+
description: str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""VS Code doesn't support rules."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def write_command(
|
|
46
|
+
self,
|
|
47
|
+
content_lines: list[str],
|
|
48
|
+
filename: str,
|
|
49
|
+
commands_dir: Path,
|
|
50
|
+
section_name: str | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""VS Code doesn't support commands."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def transform_mcp_server(self, server: McpServer) -> dict:
|
|
56
|
+
"""Transform unified server to VS Code format."""
|
|
57
|
+
# VS Code uses "env" key, similar to standard MCP
|
|
58
|
+
base = {}
|
|
59
|
+
if server.env:
|
|
60
|
+
base["env"] = server.env
|
|
61
|
+
|
|
62
|
+
if server.url:
|
|
63
|
+
# VS Code supports SSE via "url" (or "type": "sse"?)
|
|
64
|
+
# Research indicates basic MCP config in VS Code is similar to Claude
|
|
65
|
+
# but usually requires "command" for stdio.
|
|
66
|
+
# However, for remote/SSE, it might just need 'url'.
|
|
67
|
+
# Let's assume standard 'url' for now based on 'mcp.json' schema compatibility.
|
|
68
|
+
return {"url": server.url, **base}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"command": server.command,
|
|
72
|
+
"args": server.args or [],
|
|
73
|
+
**base,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def reverse_transform_mcp_server(self, name: str, config: dict) -> McpServer:
|
|
77
|
+
"""Transform VS Code config back to unified format."""
|
|
78
|
+
if "url" in config:
|
|
79
|
+
return McpServer(
|
|
80
|
+
url=config["url"],
|
|
81
|
+
env=config.get("env"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return McpServer(
|
|
85
|
+
command=config["command"],
|
|
86
|
+
args=config.get("args", []),
|
|
87
|
+
env=config.get("env"),
|
|
88
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Command to configure agents to use AGENTS.md."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from llm_ide_rules.agents import get_agent, get_all_agents
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def config_main(
|
|
12
|
+
agent: Annotated[
|
|
13
|
+
str | None,
|
|
14
|
+
typer.Option(help="Specific agent to configure (cursor, github, etc.)"),
|
|
15
|
+
] = None,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Configure agents to use AGENTS.md as their context source.
|
|
19
|
+
"""
|
|
20
|
+
base_dir = Path.cwd()
|
|
21
|
+
|
|
22
|
+
agents_to_configure = []
|
|
23
|
+
if agent:
|
|
24
|
+
try:
|
|
25
|
+
agents_to_configure.append(get_agent(agent))
|
|
26
|
+
except ValueError as e:
|
|
27
|
+
typer.echo(f"Error: {e}", err=True)
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
else:
|
|
30
|
+
agents_to_configure = get_all_agents()
|
|
31
|
+
|
|
32
|
+
for agent_inst in agents_to_configure:
|
|
33
|
+
if agent_inst.name == "agents":
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
configured = agent_inst.configure_agents_md(base_dir)
|
|
38
|
+
if configured:
|
|
39
|
+
typer.echo(
|
|
40
|
+
typer.style(f"Configured {agent_inst.name}", fg=typer.colors.GREEN)
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
msg = f"Skipped {agent_inst.name} (no changes needed or not applicable)"
|
|
44
|
+
typer.echo(typer.style(msg, fg=typer.colors.YELLOW))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
typer.echo(f"Failed to configure {agent_inst.name}: {e}", err=True)
|
llm_ide_rules/commands/delete.py
CHANGED
|
@@ -7,7 +7,58 @@ import typer
|
|
|
7
7
|
from typing_extensions import Annotated
|
|
8
8
|
|
|
9
9
|
from llm_ide_rules.commands.download import INSTRUCTION_TYPES, DEFAULT_TYPES
|
|
10
|
+
from llm_ide_rules.constants import header_to_filename
|
|
10
11
|
from llm_ide_rules.log import log
|
|
12
|
+
from llm_ide_rules.markdown_parser import parse_sections
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_generated_files(target_dir: Path) -> set[Path]:
|
|
16
|
+
"""Identify files that would be generated from local instruction files."""
|
|
17
|
+
generated = set()
|
|
18
|
+
|
|
19
|
+
# Check instructions.md
|
|
20
|
+
instructions_path = target_dir / "instructions.md"
|
|
21
|
+
if instructions_path.exists():
|
|
22
|
+
try:
|
|
23
|
+
general, sections = parse_sections(instructions_path.read_text())
|
|
24
|
+
|
|
25
|
+
# If general instructions exist, these files are generated
|
|
26
|
+
if any(line.strip() for line in general):
|
|
27
|
+
generated.add(target_dir / ".cursor/rules/general.mdc")
|
|
28
|
+
generated.add(target_dir / ".github/copilot-instructions.md")
|
|
29
|
+
generated.add(target_dir / "CLAUDE.md")
|
|
30
|
+
|
|
31
|
+
# If any sections exist, root docs are definitely generated
|
|
32
|
+
if sections:
|
|
33
|
+
generated.add(target_dir / "CLAUDE.md")
|
|
34
|
+
|
|
35
|
+
# Section specific files
|
|
36
|
+
for header in sections:
|
|
37
|
+
filename = header_to_filename(header)
|
|
38
|
+
generated.add(target_dir / f".cursor/rules/{filename}.mdc")
|
|
39
|
+
generated.add(
|
|
40
|
+
target_dir / f".github/instructions/{filename}.instructions.md"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
log.warning("failed to parse instructions.md", error=str(e))
|
|
45
|
+
|
|
46
|
+
# Check commands.md
|
|
47
|
+
commands_path = target_dir / "commands.md"
|
|
48
|
+
if commands_path.exists():
|
|
49
|
+
try:
|
|
50
|
+
_, sections = parse_sections(commands_path.read_text())
|
|
51
|
+
for header in sections:
|
|
52
|
+
filename = header_to_filename(header)
|
|
53
|
+
generated.add(target_dir / f".cursor/commands/{filename}.md")
|
|
54
|
+
generated.add(target_dir / f".github/prompts/{filename}.prompt.md")
|
|
55
|
+
generated.add(target_dir / f".gemini/commands/{filename}.toml")
|
|
56
|
+
generated.add(target_dir / f".claude/commands/{filename}.md")
|
|
57
|
+
generated.add(target_dir / f".opencode/commands/{filename}.md")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
log.warning("failed to parse commands.md", error=str(e))
|
|
60
|
+
|
|
61
|
+
return {p.resolve() for p in generated}
|
|
11
62
|
|
|
12
63
|
|
|
13
64
|
def find_files_to_delete(
|
|
@@ -38,6 +89,11 @@ def find_files_to_delete(
|
|
|
38
89
|
if file_path.exists() and file_path.is_file():
|
|
39
90
|
files_to_delete.append(file_path)
|
|
40
91
|
|
|
92
|
+
for file_name in config.get("generated_files", []):
|
|
93
|
+
file_path = target_dir / file_name
|
|
94
|
+
if file_path.exists() and file_path.is_file():
|
|
95
|
+
files_to_delete.append(file_path)
|
|
96
|
+
|
|
41
97
|
for file_pattern in config.get("recursive_files", []):
|
|
42
98
|
matching_files = list(target_dir.rglob(file_pattern))
|
|
43
99
|
files_to_delete.extend([f for f in matching_files if f.is_file()])
|
|
@@ -49,12 +105,19 @@ def delete_main(
|
|
|
49
105
|
instruction_types: Annotated[
|
|
50
106
|
list[str] | None,
|
|
51
107
|
typer.Argument(
|
|
52
|
-
help="Types of instructions to delete (cursor, github, gemini, claude,
|
|
108
|
+
help="Types of instructions to delete (cursor, github, gemini, claude, opencode, agents). Deletes everything by default."
|
|
53
109
|
),
|
|
54
110
|
] = None,
|
|
55
111
|
target_dir: Annotated[
|
|
56
112
|
str, typer.Option("--target", "-t", help="Target directory to delete from")
|
|
57
113
|
] = ".",
|
|
114
|
+
everything: Annotated[
|
|
115
|
+
bool,
|
|
116
|
+
typer.Option(
|
|
117
|
+
"--everything",
|
|
118
|
+
help="Delete all instruction files, not just those generated from local sources.",
|
|
119
|
+
),
|
|
120
|
+
] = False,
|
|
58
121
|
yes: Annotated[
|
|
59
122
|
bool,
|
|
60
123
|
typer.Option(
|
|
@@ -64,17 +127,25 @@ def delete_main(
|
|
|
64
127
|
):
|
|
65
128
|
"""Remove downloaded LLM instruction files.
|
|
66
129
|
|
|
67
|
-
This command removes files and directories that were downloaded by the 'download' command
|
|
68
|
-
|
|
130
|
+
This command removes files and directories that were downloaded by the 'download' command
|
|
131
|
+
or generated by the 'explode' command.
|
|
132
|
+
|
|
133
|
+
By default, it ONLY deletes files that correspond to your local 'instructions.md' and
|
|
134
|
+
'commands.md' files. This prevents accidental deletion of manually created files.
|
|
135
|
+
Use --everything to delete all standard instruction files and directories.
|
|
69
136
|
|
|
70
137
|
Examples:
|
|
71
138
|
|
|
72
139
|
\b
|
|
73
|
-
# Delete
|
|
140
|
+
# Delete only generated files (safest, default)
|
|
74
141
|
llm_ide_rules delete
|
|
75
142
|
|
|
76
143
|
\b
|
|
77
|
-
# Delete
|
|
144
|
+
# Delete ALL instruction files (including manual ones)
|
|
145
|
+
llm_ide_rules delete --everything
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
# Delete only Cursor and Gemini files (but only if generated)
|
|
78
149
|
llm_ide_rules delete cursor gemini
|
|
79
150
|
|
|
80
151
|
\b
|
|
@@ -115,9 +186,39 @@ def delete_main(
|
|
|
115
186
|
instruction_types, target_path
|
|
116
187
|
)
|
|
117
188
|
|
|
189
|
+
skipped_files = []
|
|
190
|
+
|
|
191
|
+
if not everything:
|
|
192
|
+
log.info("filtering files to delete based on local sources")
|
|
193
|
+
generated_files = get_generated_files(target_path)
|
|
194
|
+
|
|
195
|
+
# Expand directories to files for granular filtering
|
|
196
|
+
expanded_files = []
|
|
197
|
+
for d in dirs_to_delete:
|
|
198
|
+
expanded_files.extend([f for f in d.rglob("*") if f.is_file()])
|
|
199
|
+
|
|
200
|
+
all_candidates = files_to_delete + expanded_files
|
|
201
|
+
|
|
202
|
+
# Filter: keep only files that are in the generated set
|
|
203
|
+
# We compare resolved paths to be safe
|
|
204
|
+
files_to_delete = [f for f in all_candidates if f.resolve() in generated_files]
|
|
205
|
+
|
|
206
|
+
# Identify skipped files (candidates that were NOT in generated set)
|
|
207
|
+
skipped_files = [
|
|
208
|
+
f for f in all_candidates if f.resolve() not in generated_files
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
# We are no longer deleting whole directories in safe mode
|
|
212
|
+
dirs_to_delete = []
|
|
213
|
+
|
|
118
214
|
if not dirs_to_delete and not files_to_delete:
|
|
119
215
|
log.info("no files found to delete")
|
|
120
216
|
typer.echo("No matching instruction files found to delete.")
|
|
217
|
+
if skipped_files:
|
|
218
|
+
typer.echo(
|
|
219
|
+
f"\n{len(skipped_files)} files were skipped because they don't match local instructions/commands."
|
|
220
|
+
)
|
|
221
|
+
typer.echo("Use --everything to delete them.")
|
|
121
222
|
return
|
|
122
223
|
|
|
123
224
|
typer.echo("\nThe following files and directories will be deleted:\n")
|
|
@@ -137,6 +238,11 @@ def delete_main(
|
|
|
137
238
|
total_items = len(dirs_to_delete) + len(files_to_delete)
|
|
138
239
|
typer.echo(f"\nTotal: {total_items} items")
|
|
139
240
|
|
|
241
|
+
if skipped_files:
|
|
242
|
+
typer.echo(
|
|
243
|
+
f"\n(Note: {len(skipped_files)} other files will be preserved. Use --everything to delete them)"
|
|
244
|
+
)
|
|
245
|
+
|
|
140
246
|
if not yes:
|
|
141
247
|
typer.echo()
|
|
142
248
|
confirm = typer.confirm("Are you sure you want to delete these files?")
|