llm-ide-rules 0.4.0__tar.gz → 0.5.0__tar.gz
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-0.4.0 → llm_ide_rules-0.5.0}/PKG-INFO +1 -1
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/pyproject.toml +1 -1
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/__init__.py +6 -4
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/commands/explode.py +101 -9
- llm_ide_rules-0.5.0/src/llm_ide_rules/commands/implode.py +400 -0
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/sections.json +10 -1
- llm_ide_rules-0.4.0/src/llm_ide_rules/commands/implode.py +0 -218
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/README.md +0 -0
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/__main__.py +0 -0
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/commands/delete.py +0 -0
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/commands/download.py +0 -0
- {llm_ide_rules-0.4.0 → llm_ide_rules-0.5.0}/src/llm_ide_rules/constants.py +0 -0
|
@@ -4,11 +4,11 @@ import typer
|
|
|
4
4
|
from typing_extensions import Annotated
|
|
5
5
|
|
|
6
6
|
from llm_ide_rules.commands.explode import explode_main
|
|
7
|
-
from llm_ide_rules.commands.implode import cursor, github
|
|
7
|
+
from llm_ide_rules.commands.implode import cursor, github, claude, gemini
|
|
8
8
|
from llm_ide_rules.commands.download import download_main
|
|
9
9
|
from llm_ide_rules.commands.delete import delete_main
|
|
10
10
|
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.5.0"
|
|
12
12
|
|
|
13
13
|
app = typer.Typer(
|
|
14
14
|
name="llm_ide_rules",
|
|
@@ -23,8 +23,10 @@ app.command("delete", help="Remove downloaded LLM instruction files")(delete_mai
|
|
|
23
23
|
|
|
24
24
|
# Create implode sub-typer
|
|
25
25
|
implode_app = typer.Typer(help="Bundle rule files into a single instruction file")
|
|
26
|
-
implode_app.command("cursor", help="Bundle Cursor rules into a single file")(cursor)
|
|
27
|
-
implode_app.command("github", help="Bundle GitHub/Copilot instructions into a single file")(github)
|
|
26
|
+
implode_app.command("cursor", help="Bundle Cursor rules and commands into a single file")(cursor)
|
|
27
|
+
implode_app.command("github", help="Bundle GitHub/Copilot instructions and prompts into a single file")(github)
|
|
28
|
+
implode_app.command("claude", help="Bundle Claude Code commands into a single file")(claude)
|
|
29
|
+
implode_app.command("gemini", help="Bundle Gemini CLI commands into a single file")(gemini)
|
|
28
30
|
app.add_typer(implode_app, name="implode")
|
|
29
31
|
|
|
30
32
|
def main():
|
|
@@ -192,8 +192,87 @@ description: '{description}'
|
|
|
192
192
|
f.write(line)
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
def
|
|
196
|
-
"""
|
|
195
|
+
def write_cursor_command(content_lines, filename, commands_dir, section_name=None):
|
|
196
|
+
"""Write a Cursor command file (plain markdown, no frontmatter)."""
|
|
197
|
+
filepath = os.path.join(commands_dir, filename + ".md")
|
|
198
|
+
|
|
199
|
+
trimmed = trim_content(content_lines)
|
|
200
|
+
|
|
201
|
+
# Strip the header from content (first line starting with ##)
|
|
202
|
+
filtered_content = []
|
|
203
|
+
found_header = False
|
|
204
|
+
for line in trimmed:
|
|
205
|
+
if not found_header and line.startswith("## "):
|
|
206
|
+
found_header = True
|
|
207
|
+
continue
|
|
208
|
+
filtered_content.append(line)
|
|
209
|
+
|
|
210
|
+
# Trim again after removing header
|
|
211
|
+
filtered_content = trim_content(filtered_content)
|
|
212
|
+
|
|
213
|
+
with open(filepath, "w") as f:
|
|
214
|
+
for line in filtered_content:
|
|
215
|
+
f.write(line)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def write_claude_command(content_lines, filename, commands_dir, section_name=None):
|
|
219
|
+
"""Write a Claude Code command file (plain markdown, no frontmatter)."""
|
|
220
|
+
filepath = os.path.join(commands_dir, filename + ".md")
|
|
221
|
+
|
|
222
|
+
trimmed = trim_content(content_lines)
|
|
223
|
+
|
|
224
|
+
# Strip the header from content (first line starting with ##)
|
|
225
|
+
filtered_content = []
|
|
226
|
+
found_header = False
|
|
227
|
+
for line in trimmed:
|
|
228
|
+
if not found_header and line.startswith("## "):
|
|
229
|
+
found_header = True
|
|
230
|
+
continue
|
|
231
|
+
filtered_content.append(line)
|
|
232
|
+
|
|
233
|
+
# Trim again after removing header
|
|
234
|
+
filtered_content = trim_content(filtered_content)
|
|
235
|
+
|
|
236
|
+
with open(filepath, "w") as f:
|
|
237
|
+
for line in filtered_content:
|
|
238
|
+
f.write(line)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def write_gemini_command(content_lines, filename, commands_dir, section_name=None):
|
|
242
|
+
"""Write a Gemini CLI command file (TOML format)."""
|
|
243
|
+
filepath = os.path.join(commands_dir, filename + ".toml")
|
|
244
|
+
|
|
245
|
+
description, filtered_content = extract_description_and_filter_content(
|
|
246
|
+
content_lines, ""
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Strip the header from content (first line starting with ##)
|
|
250
|
+
final_content = []
|
|
251
|
+
found_header = False
|
|
252
|
+
for line in filtered_content:
|
|
253
|
+
if not found_header and line.startswith("## "):
|
|
254
|
+
found_header = True
|
|
255
|
+
continue
|
|
256
|
+
final_content.append(line)
|
|
257
|
+
|
|
258
|
+
# Trim and convert to string
|
|
259
|
+
final_content = trim_content(final_content)
|
|
260
|
+
content_str = "".join(final_content).strip()
|
|
261
|
+
|
|
262
|
+
with open(filepath, "w") as f:
|
|
263
|
+
f.write(f'name = "{filename}"\n')
|
|
264
|
+
if description:
|
|
265
|
+
f.write(f'description = "{description}"\n')
|
|
266
|
+
else:
|
|
267
|
+
f.write(f'description = "{section_name or filename}"\n')
|
|
268
|
+
f.write('\n[command]\n')
|
|
269
|
+
f.write('shell = """\n')
|
|
270
|
+
f.write(content_str)
|
|
271
|
+
f.write('\n"""\n')
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def process_unmapped_section(lines, section_name, cursor_commands_dir, github_prompts_dir, claude_commands_dir, gemini_commands_dir):
|
|
275
|
+
"""Process an unmapped section as a manually applied rule (command)."""
|
|
197
276
|
section_content = extract_section(lines, f"## {section_name}")
|
|
198
277
|
if any(line.strip() for line in section_content):
|
|
199
278
|
filename = header_to_filename(section_name)
|
|
@@ -203,9 +282,11 @@ def process_unmapped_section(lines, section_name, rules_dir, github_prompts_dir)
|
|
|
203
282
|
section_content, section_name
|
|
204
283
|
)
|
|
205
284
|
|
|
206
|
-
# Create
|
|
207
|
-
|
|
285
|
+
# Create command files (same as None case in SECTION_GLOBS)
|
|
286
|
+
write_cursor_command(section_content, filename, cursor_commands_dir, section_name)
|
|
208
287
|
write_github_prompt(section_content, filename, github_prompts_dir, section_name)
|
|
288
|
+
write_claude_command(section_content, filename, claude_commands_dir, section_name)
|
|
289
|
+
write_gemini_command(section_content, filename, gemini_commands_dir, section_name)
|
|
209
290
|
return True
|
|
210
291
|
return False
|
|
211
292
|
|
|
@@ -235,12 +316,18 @@ def explode_main(
|
|
|
235
316
|
|
|
236
317
|
# Work in current directory ($PWD)
|
|
237
318
|
rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
|
|
319
|
+
cursor_commands_dir = os.path.join(os.getcwd(), ".cursor", "commands")
|
|
238
320
|
copilot_dir = os.path.join(os.getcwd(), ".github", "instructions")
|
|
239
321
|
github_prompts_dir = os.path.join(os.getcwd(), ".github", "prompts")
|
|
322
|
+
claude_commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
|
|
323
|
+
gemini_commands_dir = os.path.join(os.getcwd(), ".gemini", "commands")
|
|
240
324
|
|
|
241
325
|
os.makedirs(rules_dir, exist_ok=True)
|
|
326
|
+
os.makedirs(cursor_commands_dir, exist_ok=True)
|
|
242
327
|
os.makedirs(copilot_dir, exist_ok=True)
|
|
243
328
|
os.makedirs(github_prompts_dir, exist_ok=True)
|
|
329
|
+
os.makedirs(claude_commands_dir, exist_ok=True)
|
|
330
|
+
os.makedirs(gemini_commands_dir, exist_ok=True)
|
|
244
331
|
|
|
245
332
|
input_path = os.path.join(os.getcwd(), input_file)
|
|
246
333
|
|
|
@@ -295,18 +382,20 @@ alwaysApply: true
|
|
|
295
382
|
section_content,
|
|
296
383
|
)
|
|
297
384
|
else:
|
|
298
|
-
# It's a
|
|
299
|
-
|
|
385
|
+
# It's a command - create command files using the original section name for header
|
|
386
|
+
write_cursor_command(section_content, filename, cursor_commands_dir, section_name)
|
|
300
387
|
write_github_prompt(
|
|
301
388
|
section_content, filename, github_prompts_dir, section_name
|
|
302
389
|
)
|
|
390
|
+
write_claude_command(section_content, filename, claude_commands_dir, section_name)
|
|
391
|
+
write_gemini_command(section_content, filename, gemini_commands_dir, section_name)
|
|
303
392
|
|
|
304
393
|
# Check for sections in mapping that don't exist in the file
|
|
305
394
|
for section_name in SECTION_GLOBS:
|
|
306
395
|
if section_name not in found_sections:
|
|
307
396
|
logger.warning("Section not found in file", section=section_name)
|
|
308
397
|
|
|
309
|
-
# Process unmapped sections as manually applied rules (
|
|
398
|
+
# Process unmapped sections as manually applied rules (commands)
|
|
310
399
|
processed_unmapped = set()
|
|
311
400
|
for line in lines:
|
|
312
401
|
if line.startswith("## "):
|
|
@@ -321,16 +410,19 @@ alwaysApply: true
|
|
|
321
410
|
and section_name not in processed_unmapped
|
|
322
411
|
):
|
|
323
412
|
if process_unmapped_section(
|
|
324
|
-
lines, section_name,
|
|
413
|
+
lines, section_name, cursor_commands_dir, github_prompts_dir, claude_commands_dir, gemini_commands_dir
|
|
325
414
|
):
|
|
326
415
|
processed_unmapped.add(section_name)
|
|
327
416
|
|
|
328
417
|
logger.info(
|
|
329
418
|
"Explode operation completed",
|
|
330
419
|
cursor_rules=rules_dir,
|
|
420
|
+
cursor_commands=cursor_commands_dir,
|
|
331
421
|
copilot_instructions=copilot_dir,
|
|
332
422
|
github_prompts=github_prompts_dir,
|
|
423
|
+
claude_commands=claude_commands_dir,
|
|
424
|
+
gemini_commands=gemini_commands_dir,
|
|
333
425
|
)
|
|
334
426
|
typer.echo(
|
|
335
|
-
"Created
|
|
427
|
+
"Created rules and commands in .cursor/, .claude/, .github/, and .gemini/ directories"
|
|
336
428
|
)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Implode command: Bundle rule files into a single instruction file."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from llm_ide_rules.constants import load_section_globs, header_to_filename, filename_to_header
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_ordered_files(file_list, section_globs_keys):
|
|
17
|
+
"""Order files based on SECTION_GLOBS key order, with unmapped files at the end."""
|
|
18
|
+
file_dict = {f.stem: f for f in file_list}
|
|
19
|
+
ordered_files = []
|
|
20
|
+
|
|
21
|
+
# Add files in SECTION_GLOBS order
|
|
22
|
+
for section_name in section_globs_keys:
|
|
23
|
+
filename = header_to_filename(section_name)
|
|
24
|
+
if filename in file_dict:
|
|
25
|
+
ordered_files.append(file_dict[filename])
|
|
26
|
+
del file_dict[filename]
|
|
27
|
+
|
|
28
|
+
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
29
|
+
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
30
|
+
ordered_files.extend(remaining_files)
|
|
31
|
+
|
|
32
|
+
return ordered_files
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_ordered_files_github(file_list, section_globs_keys):
|
|
36
|
+
"""Order GitHub instruction files based on SECTION_GLOBS key order, with unmapped files at the end.
|
|
37
|
+
Handles .instructions suffix by stripping it for ordering purposes."""
|
|
38
|
+
# Create dict mapping base filename (without .instructions) to the actual file
|
|
39
|
+
file_dict = {}
|
|
40
|
+
for f in file_list:
|
|
41
|
+
base_stem = f.stem.replace(".instructions", "")
|
|
42
|
+
file_dict[base_stem] = f
|
|
43
|
+
|
|
44
|
+
ordered_files = []
|
|
45
|
+
|
|
46
|
+
# Add files in SECTION_GLOBS order
|
|
47
|
+
for section_name in section_globs_keys:
|
|
48
|
+
filename = header_to_filename(section_name)
|
|
49
|
+
if filename in file_dict:
|
|
50
|
+
ordered_files.append(file_dict[filename])
|
|
51
|
+
del file_dict[filename]
|
|
52
|
+
|
|
53
|
+
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
54
|
+
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
55
|
+
ordered_files.extend(remaining_files)
|
|
56
|
+
|
|
57
|
+
return ordered_files
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bundle_cursor_rules(rules_dir, commands_dir, output_file, section_globs):
|
|
61
|
+
"""Bundle Cursor rule and command files into a single file."""
|
|
62
|
+
rule_files = list(Path(rules_dir).glob("*.mdc"))
|
|
63
|
+
command_files = list(Path(commands_dir).glob("*.md"))
|
|
64
|
+
|
|
65
|
+
general = [f for f in rule_files if f.stem == "general"]
|
|
66
|
+
others = [f for f in rule_files if f.stem != "general"]
|
|
67
|
+
|
|
68
|
+
# Order the non-general files based on section_globs
|
|
69
|
+
ordered_others = get_ordered_files(others, section_globs.keys())
|
|
70
|
+
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
71
|
+
ordered = general + ordered_others + ordered_commands
|
|
72
|
+
|
|
73
|
+
def resolve_header_from_stem(stem):
|
|
74
|
+
"""Return the canonical header for a given filename stem.
|
|
75
|
+
|
|
76
|
+
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
77
|
+
Fallback to title-casing the filename when not found in section_globs.
|
|
78
|
+
"""
|
|
79
|
+
for section_name in section_globs.keys():
|
|
80
|
+
if header_to_filename(section_name) == stem:
|
|
81
|
+
return section_name
|
|
82
|
+
return filename_to_header(stem)
|
|
83
|
+
|
|
84
|
+
with open(output_file, "w") as out:
|
|
85
|
+
for rule_file in ordered:
|
|
86
|
+
with open(rule_file, "r") as f:
|
|
87
|
+
content = f.read().strip()
|
|
88
|
+
if not content:
|
|
89
|
+
continue
|
|
90
|
+
content = strip_yaml_frontmatter(content)
|
|
91
|
+
content = strip_header(content)
|
|
92
|
+
# Use canonical header names from SECTION_GLOBS when available
|
|
93
|
+
header = resolve_header_from_stem(rule_file.stem)
|
|
94
|
+
if rule_file.stem != "general":
|
|
95
|
+
out.write(f"## {header}\n\n")
|
|
96
|
+
out.write(content)
|
|
97
|
+
out.write("\n\n")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def strip_yaml_frontmatter(text):
|
|
101
|
+
"""Strip YAML frontmatter from text."""
|
|
102
|
+
lines = text.splitlines()
|
|
103
|
+
if lines and lines[0].strip() == "---":
|
|
104
|
+
# Find the next '---' after the first
|
|
105
|
+
for i in range(1, len(lines)):
|
|
106
|
+
if lines[i].strip() == "---":
|
|
107
|
+
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
108
|
+
return text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def strip_header(text):
|
|
112
|
+
"""Remove the first markdown header (## Header) from text if present."""
|
|
113
|
+
lines = text.splitlines()
|
|
114
|
+
if lines and lines[0].startswith("## "):
|
|
115
|
+
# Remove the header line and any immediately following empty lines
|
|
116
|
+
remaining_lines = lines[1:]
|
|
117
|
+
while remaining_lines and not remaining_lines[0].strip():
|
|
118
|
+
remaining_lines = remaining_lines[1:]
|
|
119
|
+
return "\n".join(remaining_lines)
|
|
120
|
+
return text
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def bundle_github_instructions(instructions_dir, prompts_dir, output_file, section_globs):
|
|
124
|
+
"""Bundle GitHub instruction and prompt files into a single file."""
|
|
125
|
+
copilot_general = Path(os.getcwd()) / ".github" / "copilot-instructions.md"
|
|
126
|
+
instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
|
|
127
|
+
prompt_files = list(Path(prompts_dir).glob("*.prompt.md"))
|
|
128
|
+
|
|
129
|
+
# Order the instruction files based on section_globs
|
|
130
|
+
# We need to create a modified version that strips .instructions from stems for ordering
|
|
131
|
+
ordered_instructions = get_ordered_files_github(instr_files, section_globs.keys())
|
|
132
|
+
|
|
133
|
+
# For prompts, we need to handle .prompt suffix similarly
|
|
134
|
+
ordered_prompts = []
|
|
135
|
+
prompt_dict = {}
|
|
136
|
+
for f in prompt_files:
|
|
137
|
+
base_stem = f.stem.replace(".prompt", "")
|
|
138
|
+
prompt_dict[base_stem] = f
|
|
139
|
+
|
|
140
|
+
for section_name in section_globs.keys():
|
|
141
|
+
filename = header_to_filename(section_name)
|
|
142
|
+
if filename in prompt_dict:
|
|
143
|
+
ordered_prompts.append(prompt_dict[filename])
|
|
144
|
+
del prompt_dict[filename]
|
|
145
|
+
|
|
146
|
+
# Add remaining prompts alphabetically
|
|
147
|
+
remaining_prompts = sorted(prompt_dict.values(), key=lambda p: p.name)
|
|
148
|
+
ordered_prompts.extend(remaining_prompts)
|
|
149
|
+
|
|
150
|
+
def resolve_header_from_stem(stem):
|
|
151
|
+
"""Return the canonical header for a given filename stem.
|
|
152
|
+
|
|
153
|
+
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
154
|
+
Fallback to title-casing the filename when not found in section_globs.
|
|
155
|
+
"""
|
|
156
|
+
for section_name in section_globs.keys():
|
|
157
|
+
if header_to_filename(section_name) == stem:
|
|
158
|
+
return section_name
|
|
159
|
+
return filename_to_header(stem)
|
|
160
|
+
|
|
161
|
+
with open(output_file, "w") as out:
|
|
162
|
+
# Write general copilot instructions if present
|
|
163
|
+
if copilot_general.exists():
|
|
164
|
+
content = copilot_general.read_text().strip()
|
|
165
|
+
if content:
|
|
166
|
+
out.write(content)
|
|
167
|
+
out.write("\n\n")
|
|
168
|
+
for instr_file in ordered_instructions:
|
|
169
|
+
with open(instr_file, "r") as f:
|
|
170
|
+
content = f.read().strip()
|
|
171
|
+
if not content:
|
|
172
|
+
continue
|
|
173
|
+
content = strip_yaml_frontmatter(content)
|
|
174
|
+
content = strip_header(content)
|
|
175
|
+
# Use canonical header names from SECTION_GLOBS when available
|
|
176
|
+
base_stem = instr_file.stem.replace(".instructions", "")
|
|
177
|
+
header = resolve_header_from_stem(base_stem)
|
|
178
|
+
out.write(f"## {header}\n\n")
|
|
179
|
+
out.write(content)
|
|
180
|
+
out.write("\n\n")
|
|
181
|
+
for prompt_file in ordered_prompts:
|
|
182
|
+
with open(prompt_file, "r") as f:
|
|
183
|
+
content = f.read().strip()
|
|
184
|
+
if not content:
|
|
185
|
+
continue
|
|
186
|
+
content = strip_yaml_frontmatter(content)
|
|
187
|
+
content = strip_header(content)
|
|
188
|
+
# Use canonical header names from SECTION_GLOBS when available
|
|
189
|
+
base_stem = prompt_file.stem.replace(".prompt", "")
|
|
190
|
+
header = resolve_header_from_stem(base_stem)
|
|
191
|
+
out.write(f"## {header}\n\n")
|
|
192
|
+
out.write(content)
|
|
193
|
+
out.write("\n\n")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def bundle_claude_commands(commands_dir, output_file, section_globs):
|
|
197
|
+
"""Bundle Claude Code command files into a single file."""
|
|
198
|
+
command_files = list(Path(commands_dir).glob("*.md"))
|
|
199
|
+
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
200
|
+
|
|
201
|
+
def resolve_header_from_stem(stem):
|
|
202
|
+
"""Return the canonical header for a given filename stem.
|
|
203
|
+
|
|
204
|
+
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
205
|
+
Fallback to title-casing the filename when not found in section_globs.
|
|
206
|
+
"""
|
|
207
|
+
for section_name in section_globs.keys():
|
|
208
|
+
if header_to_filename(section_name) == stem:
|
|
209
|
+
return section_name
|
|
210
|
+
return filename_to_header(stem)
|
|
211
|
+
|
|
212
|
+
with open(output_file, "w") as out:
|
|
213
|
+
for command_file in ordered_commands:
|
|
214
|
+
with open(command_file, "r") as f:
|
|
215
|
+
content = f.read().strip()
|
|
216
|
+
if not content:
|
|
217
|
+
continue
|
|
218
|
+
# Claude commands don't have frontmatter, just content
|
|
219
|
+
header = resolve_header_from_stem(command_file.stem)
|
|
220
|
+
out.write(f"## {header}\n\n")
|
|
221
|
+
out.write(content)
|
|
222
|
+
out.write("\n\n")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def strip_toml_metadata(text):
|
|
226
|
+
"""Extract content from TOML command.shell block."""
|
|
227
|
+
lines = text.splitlines()
|
|
228
|
+
in_shell_block = False
|
|
229
|
+
content_lines = []
|
|
230
|
+
|
|
231
|
+
for line in lines:
|
|
232
|
+
if line.strip() == '[command]':
|
|
233
|
+
in_shell_block = True
|
|
234
|
+
continue
|
|
235
|
+
if in_shell_block:
|
|
236
|
+
if line.strip().startswith('shell = """'):
|
|
237
|
+
# Start of shell content
|
|
238
|
+
# Check if content is on same line
|
|
239
|
+
after_start = line.split('"""', 1)[1] if '"""' in line else ""
|
|
240
|
+
if after_start.strip():
|
|
241
|
+
content_lines.append(after_start)
|
|
242
|
+
continue
|
|
243
|
+
if line.strip() == '"""' or line.strip().endswith('"""'):
|
|
244
|
+
# End of shell content
|
|
245
|
+
if line.strip() != '"""':
|
|
246
|
+
# Content on same line as closing
|
|
247
|
+
content_lines.append(line.rsplit('"""', 1)[0])
|
|
248
|
+
break
|
|
249
|
+
content_lines.append(line)
|
|
250
|
+
|
|
251
|
+
return "\n".join(content_lines).strip()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def bundle_gemini_commands(commands_dir, output_file, section_globs):
|
|
255
|
+
"""Bundle Gemini CLI command files into a single file."""
|
|
256
|
+
command_files = list(Path(commands_dir).glob("*.toml"))
|
|
257
|
+
ordered_commands = get_ordered_files(command_files, section_globs.keys())
|
|
258
|
+
|
|
259
|
+
def resolve_header_from_stem(stem):
|
|
260
|
+
"""Return the canonical header for a given filename stem.
|
|
261
|
+
|
|
262
|
+
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
263
|
+
Fallback to title-casing the filename when not found in section_globs.
|
|
264
|
+
"""
|
|
265
|
+
for section_name in section_globs.keys():
|
|
266
|
+
if header_to_filename(section_name) == stem:
|
|
267
|
+
return section_name
|
|
268
|
+
return filename_to_header(stem)
|
|
269
|
+
|
|
270
|
+
with open(output_file, "w") as out:
|
|
271
|
+
for command_file in ordered_commands:
|
|
272
|
+
with open(command_file, "r") as f:
|
|
273
|
+
content = f.read().strip()
|
|
274
|
+
if not content:
|
|
275
|
+
continue
|
|
276
|
+
# Extract content from TOML shell block
|
|
277
|
+
content = strip_toml_metadata(content)
|
|
278
|
+
header = resolve_header_from_stem(command_file.stem)
|
|
279
|
+
out.write(f"## {header}\n\n")
|
|
280
|
+
out.write(content)
|
|
281
|
+
out.write("\n\n")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def cursor(
|
|
285
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
286
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
287
|
+
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
288
|
+
):
|
|
289
|
+
"""Bundle Cursor rules and commands into a single file."""
|
|
290
|
+
if verbose:
|
|
291
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
292
|
+
structlog.configure(
|
|
293
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Load section globs (with optional custom config)
|
|
297
|
+
SECTION_GLOBS = load_section_globs(config)
|
|
298
|
+
|
|
299
|
+
rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
|
|
300
|
+
commands_dir = os.path.join(os.getcwd(), ".cursor", "commands")
|
|
301
|
+
output_path = os.path.join(os.getcwd(), output)
|
|
302
|
+
|
|
303
|
+
logger.info("Bundling Cursor rules and commands", rules_dir=rules_dir, commands_dir=commands_dir, output_file=output_path, config=config)
|
|
304
|
+
|
|
305
|
+
if not Path(rules_dir).exists():
|
|
306
|
+
logger.error("Cursor rules directory not found", rules_dir=rules_dir)
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
|
|
309
|
+
bundle_cursor_rules(rules_dir, commands_dir, output_path, SECTION_GLOBS)
|
|
310
|
+
logger.info("Cursor rules and commands bundled successfully", output_file=output_path)
|
|
311
|
+
typer.echo(f"Bundled cursor rules and commands into {output}")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def github(
|
|
315
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
316
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
317
|
+
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
318
|
+
):
|
|
319
|
+
"""Bundle GitHub/Copilot instructions and prompts into a single file."""
|
|
320
|
+
if verbose:
|
|
321
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
322
|
+
structlog.configure(
|
|
323
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Load section globs (with optional custom config)
|
|
327
|
+
SECTION_GLOBS = load_section_globs(config)
|
|
328
|
+
|
|
329
|
+
instructions_dir = os.path.join(os.getcwd(), ".github", "instructions")
|
|
330
|
+
prompts_dir = os.path.join(os.getcwd(), ".github", "prompts")
|
|
331
|
+
output_path = os.path.join(os.getcwd(), output)
|
|
332
|
+
|
|
333
|
+
logger.info("Bundling GitHub instructions and prompts", instructions_dir=instructions_dir, prompts_dir=prompts_dir, output_file=output_path, config=config)
|
|
334
|
+
|
|
335
|
+
if not Path(instructions_dir).exists():
|
|
336
|
+
logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
|
|
337
|
+
raise typer.Exit(1)
|
|
338
|
+
|
|
339
|
+
bundle_github_instructions(instructions_dir, prompts_dir, output_path, SECTION_GLOBS)
|
|
340
|
+
logger.info("GitHub instructions and prompts bundled successfully", output_file=output_path)
|
|
341
|
+
typer.echo(f"Bundled github instructions and prompts into {output}")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def claude(
|
|
345
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
346
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
347
|
+
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
348
|
+
):
|
|
349
|
+
"""Bundle Claude Code commands into a single file."""
|
|
350
|
+
if verbose:
|
|
351
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
352
|
+
structlog.configure(
|
|
353
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Load section globs (with optional custom config)
|
|
357
|
+
SECTION_GLOBS = load_section_globs(config)
|
|
358
|
+
|
|
359
|
+
commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
|
|
360
|
+
output_path = os.path.join(os.getcwd(), output)
|
|
361
|
+
|
|
362
|
+
logger.info("Bundling Claude Code commands", commands_dir=commands_dir, output_file=output_path, config=config)
|
|
363
|
+
|
|
364
|
+
if not Path(commands_dir).exists():
|
|
365
|
+
logger.error("Claude Code commands directory not found", commands_dir=commands_dir)
|
|
366
|
+
raise typer.Exit(1)
|
|
367
|
+
|
|
368
|
+
bundle_claude_commands(commands_dir, output_path, SECTION_GLOBS)
|
|
369
|
+
logger.info("Claude Code commands bundled successfully", output_file=output_path)
|
|
370
|
+
typer.echo(f"Bundled claude commands into {output}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def gemini(
|
|
374
|
+
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
375
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
376
|
+
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
377
|
+
):
|
|
378
|
+
"""Bundle Gemini CLI commands into a single file."""
|
|
379
|
+
if verbose:
|
|
380
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
381
|
+
structlog.configure(
|
|
382
|
+
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Load section globs (with optional custom config)
|
|
386
|
+
SECTION_GLOBS = load_section_globs(config)
|
|
387
|
+
|
|
388
|
+
commands_dir = os.path.join(os.getcwd(), ".gemini", "commands")
|
|
389
|
+
output_path = os.path.join(os.getcwd(), output)
|
|
390
|
+
|
|
391
|
+
logger.info("Bundling Gemini CLI commands", commands_dir=commands_dir, output_file=output_path, config=config)
|
|
392
|
+
|
|
393
|
+
if not Path(commands_dir).exists():
|
|
394
|
+
logger.error("Gemini CLI commands directory not found", commands_dir=commands_dir)
|
|
395
|
+
raise typer.Exit(1)
|
|
396
|
+
|
|
397
|
+
bundle_gemini_commands(commands_dir, output_path, SECTION_GLOBS)
|
|
398
|
+
logger.info("Gemini CLI commands bundled successfully", output_file=output_path)
|
|
399
|
+
typer.echo(f"Bundled gemini commands into {output}")
|
|
400
|
+
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
"Shell": "**/*.sh",
|
|
14
14
|
"TypeScript": "**/*.ts,**/*.tsx",
|
|
15
15
|
"TypeScript DocString": null,
|
|
16
|
-
"Secrets": null
|
|
16
|
+
"Secrets": null,
|
|
17
|
+
"Dev In Browser": null,
|
|
18
|
+
"Fastapi Stripe": null,
|
|
19
|
+
"Fix Tests": null,
|
|
20
|
+
"Implement Fastapi Routes": null,
|
|
21
|
+
"Plan Only": null,
|
|
22
|
+
"Python Command": null,
|
|
23
|
+
"Refactor On Instructions": null,
|
|
24
|
+
"Standalone Python Scripts": null,
|
|
25
|
+
"Stripe Backend": null
|
|
17
26
|
}
|
|
18
27
|
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""Implode command: Bundle rule files into a single instruction file."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing_extensions import Annotated
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
import structlog
|
|
10
|
-
|
|
11
|
-
from llm_ide_rules.constants import load_section_globs, header_to_filename, filename_to_header
|
|
12
|
-
|
|
13
|
-
logger = structlog.get_logger()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_ordered_files(file_list, section_globs_keys):
|
|
17
|
-
"""Order files based on SECTION_GLOBS key order, with unmapped files at the end."""
|
|
18
|
-
file_dict = {f.stem: f for f in file_list}
|
|
19
|
-
ordered_files = []
|
|
20
|
-
|
|
21
|
-
# Add files in SECTION_GLOBS order
|
|
22
|
-
for section_name in section_globs_keys:
|
|
23
|
-
filename = header_to_filename(section_name)
|
|
24
|
-
if filename in file_dict:
|
|
25
|
-
ordered_files.append(file_dict[filename])
|
|
26
|
-
del file_dict[filename]
|
|
27
|
-
|
|
28
|
-
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
29
|
-
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
30
|
-
ordered_files.extend(remaining_files)
|
|
31
|
-
|
|
32
|
-
return ordered_files
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_ordered_files_github(file_list, section_globs_keys):
|
|
36
|
-
"""Order GitHub instruction files based on SECTION_GLOBS key order, with unmapped files at the end.
|
|
37
|
-
Handles .instructions suffix by stripping it for ordering purposes."""
|
|
38
|
-
# Create dict mapping base filename (without .instructions) to the actual file
|
|
39
|
-
file_dict = {}
|
|
40
|
-
for f in file_list:
|
|
41
|
-
base_stem = f.stem.replace(".instructions", "")
|
|
42
|
-
file_dict[base_stem] = f
|
|
43
|
-
|
|
44
|
-
ordered_files = []
|
|
45
|
-
|
|
46
|
-
# Add files in SECTION_GLOBS order
|
|
47
|
-
for section_name in section_globs_keys:
|
|
48
|
-
filename = header_to_filename(section_name)
|
|
49
|
-
if filename in file_dict:
|
|
50
|
-
ordered_files.append(file_dict[filename])
|
|
51
|
-
del file_dict[filename]
|
|
52
|
-
|
|
53
|
-
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
|
|
54
|
-
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
|
|
55
|
-
ordered_files.extend(remaining_files)
|
|
56
|
-
|
|
57
|
-
return ordered_files
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def bundle_cursor_rules(rules_dir, output_file, section_globs):
|
|
61
|
-
"""Bundle Cursor rule files into a single file."""
|
|
62
|
-
rule_files = list(Path(rules_dir).glob("*.mdc"))
|
|
63
|
-
general = [f for f in rule_files if f.stem == "general"]
|
|
64
|
-
others = [f for f in rule_files if f.stem != "general"]
|
|
65
|
-
|
|
66
|
-
# Order the non-general files based on section_globs
|
|
67
|
-
ordered_others = get_ordered_files(others, section_globs.keys())
|
|
68
|
-
ordered = general + ordered_others
|
|
69
|
-
|
|
70
|
-
def resolve_header_from_stem(stem):
|
|
71
|
-
"""Return the canonical header for a given filename stem.
|
|
72
|
-
|
|
73
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
74
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
75
|
-
"""
|
|
76
|
-
for section_name in section_globs.keys():
|
|
77
|
-
if header_to_filename(section_name) == stem:
|
|
78
|
-
return section_name
|
|
79
|
-
return filename_to_header(stem)
|
|
80
|
-
|
|
81
|
-
with open(output_file, "w") as out:
|
|
82
|
-
for rule_file in ordered:
|
|
83
|
-
with open(rule_file, "r") as f:
|
|
84
|
-
content = f.read().strip()
|
|
85
|
-
if not content:
|
|
86
|
-
continue
|
|
87
|
-
content = strip_yaml_frontmatter(content)
|
|
88
|
-
content = strip_header(content)
|
|
89
|
-
# Use canonical header names from SECTION_GLOBS when available
|
|
90
|
-
header = resolve_header_from_stem(rule_file.stem)
|
|
91
|
-
if rule_file.stem != "general":
|
|
92
|
-
out.write(f"## {header}\n\n")
|
|
93
|
-
out.write(content)
|
|
94
|
-
out.write("\n\n")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def strip_yaml_frontmatter(text):
|
|
98
|
-
"""Strip YAML frontmatter from text."""
|
|
99
|
-
lines = text.splitlines()
|
|
100
|
-
if lines and lines[0].strip() == "---":
|
|
101
|
-
# Find the next '---' after the first
|
|
102
|
-
for i in range(1, len(lines)):
|
|
103
|
-
if lines[i].strip() == "---":
|
|
104
|
-
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
105
|
-
return text
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def strip_header(text):
|
|
109
|
-
"""Remove the first markdown header (## Header) from text if present."""
|
|
110
|
-
lines = text.splitlines()
|
|
111
|
-
if lines and lines[0].startswith("## "):
|
|
112
|
-
# Remove the header line and any immediately following empty lines
|
|
113
|
-
remaining_lines = lines[1:]
|
|
114
|
-
while remaining_lines and not remaining_lines[0].strip():
|
|
115
|
-
remaining_lines = remaining_lines[1:]
|
|
116
|
-
return "\n".join(remaining_lines)
|
|
117
|
-
return text
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def bundle_github_instructions(instructions_dir, output_file, section_globs):
|
|
121
|
-
"""Bundle GitHub instruction files into a single file."""
|
|
122
|
-
copilot_general = Path(os.getcwd()) / ".github" / "copilot-instructions.md"
|
|
123
|
-
instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
|
|
124
|
-
|
|
125
|
-
# Order the instruction files based on section_globs
|
|
126
|
-
# We need to create a modified version that strips .instructions from stems for ordering
|
|
127
|
-
ordered_files = get_ordered_files_github(instr_files, section_globs.keys())
|
|
128
|
-
|
|
129
|
-
def resolve_header_from_stem(stem):
|
|
130
|
-
"""Return the canonical header for a given filename stem.
|
|
131
|
-
|
|
132
|
-
Prefer exact header names from section_globs (preserves acronyms like FastAPI, TypeScript).
|
|
133
|
-
Fallback to title-casing the filename when not found in section_globs.
|
|
134
|
-
"""
|
|
135
|
-
for section_name in section_globs.keys():
|
|
136
|
-
if header_to_filename(section_name) == stem:
|
|
137
|
-
return section_name
|
|
138
|
-
return filename_to_header(stem)
|
|
139
|
-
|
|
140
|
-
with open(output_file, "w") as out:
|
|
141
|
-
# Write general copilot instructions if present
|
|
142
|
-
if copilot_general.exists():
|
|
143
|
-
content = copilot_general.read_text().strip()
|
|
144
|
-
if content:
|
|
145
|
-
out.write(content)
|
|
146
|
-
out.write("\n\n")
|
|
147
|
-
for instr_file in ordered_files:
|
|
148
|
-
with open(instr_file, "r") as f:
|
|
149
|
-
content = f.read().strip()
|
|
150
|
-
if not content:
|
|
151
|
-
continue
|
|
152
|
-
content = strip_yaml_frontmatter(content)
|
|
153
|
-
content = strip_header(content)
|
|
154
|
-
# Use canonical header names from SECTION_GLOBS when available
|
|
155
|
-
base_stem = instr_file.stem.replace(".instructions", "")
|
|
156
|
-
header = resolve_header_from_stem(base_stem)
|
|
157
|
-
out.write(f"## {header}\n\n")
|
|
158
|
-
out.write(content)
|
|
159
|
-
out.write("\n\n")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def cursor(
|
|
163
|
-
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
164
|
-
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
165
|
-
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
166
|
-
):
|
|
167
|
-
"""Bundle Cursor rules into a single file."""
|
|
168
|
-
if verbose:
|
|
169
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
170
|
-
structlog.configure(
|
|
171
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Load section globs (with optional custom config)
|
|
175
|
-
SECTION_GLOBS = load_section_globs(config)
|
|
176
|
-
|
|
177
|
-
rules_dir = os.path.join(os.getcwd(), ".cursor", "rules")
|
|
178
|
-
output_path = os.path.join(os.getcwd(), output)
|
|
179
|
-
|
|
180
|
-
logger.info("Bundling Cursor rules", rules_dir=rules_dir, output_file=output_path, config=config)
|
|
181
|
-
|
|
182
|
-
if not Path(rules_dir).exists():
|
|
183
|
-
logger.error("Cursor rules directory not found", rules_dir=rules_dir)
|
|
184
|
-
raise typer.Exit(1)
|
|
185
|
-
|
|
186
|
-
bundle_cursor_rules(rules_dir, output_path, SECTION_GLOBS)
|
|
187
|
-
logger.info("Cursor rules bundled successfully", output_file=output_path)
|
|
188
|
-
typer.echo(f"Bundled cursor rules into {output}")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def github(
|
|
192
|
-
output: Annotated[str, typer.Argument(help="Output file")] = "instructions.md",
|
|
193
|
-
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
194
|
-
config: Annotated[str, typer.Option("--config", "-c", help="Custom configuration file path")] = None,
|
|
195
|
-
):
|
|
196
|
-
"""Bundle GitHub/Copilot instructions into a single file."""
|
|
197
|
-
if verbose:
|
|
198
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
199
|
-
structlog.configure(
|
|
200
|
-
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
# Load section globs (with optional custom config)
|
|
204
|
-
SECTION_GLOBS = load_section_globs(config)
|
|
205
|
-
|
|
206
|
-
instructions_dir = os.path.join(os.getcwd(), ".github", "instructions")
|
|
207
|
-
output_path = os.path.join(os.getcwd(), output)
|
|
208
|
-
|
|
209
|
-
logger.info("Bundling GitHub instructions", instructions_dir=instructions_dir, output_file=output_path, config=config)
|
|
210
|
-
|
|
211
|
-
if not Path(instructions_dir).exists():
|
|
212
|
-
logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
|
|
213
|
-
raise typer.Exit(1)
|
|
214
|
-
|
|
215
|
-
bundle_github_instructions(instructions_dir, output_path, SECTION_GLOBS)
|
|
216
|
-
logger.info("GitHub instructions bundled successfully", output_file=output_path)
|
|
217
|
-
typer.echo(f"Bundled github instructions into {output}")
|
|
218
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|