llm-ide-rules 0.5.0__py3-none-any.whl → 0.7.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,400 +1,214 @@
1
1
  """Implode command: Bundle rule files into a single instruction file."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing_extensions import Annotated
6
- import logging
7
5
 
8
6
  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")
7
+
8
+ from llm_ide_rules.agents import get_agent
9
+ from llm_ide_rules.log import log
282
10
 
283
11
 
284
12
  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
- )
13
+ output: Annotated[
14
+ str, typer.Argument(help="Output file for rules")
15
+ ] = "instructions.md",
16
+ ) -> None:
17
+ """Bundle Cursor rules into instructions.md and commands into commands.md."""
18
+
19
+ agent = get_agent("cursor")
20
+ cwd = Path.cwd()
21
+
22
+ rules_dir = agent.rules_dir
23
+ if not rules_dir:
24
+ log.error("cursor rules directory not configured")
25
+ raise typer.Exit(1)
295
26
 
296
- # Load section globs (with optional custom config)
297
- SECTION_GLOBS = load_section_globs(config)
27
+ log.info(
28
+ "bundling cursor rules and commands",
29
+ rules_dir=rules_dir,
30
+ commands_dir=agent.commands_dir,
31
+ )
32
+
33
+ rules_path = cwd / rules_dir
34
+ if not rules_path.exists():
35
+ log.error("cursor rules directory not found", rules_dir=str(rules_path))
36
+ error_msg = f"Cursor rules directory not found: {rules_path}"
37
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
38
+ raise typer.Exit(1)
298
39
 
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)
40
+ output_path = cwd / output
41
+ rules_written = agent.bundle_rules(output_path)
42
+ if rules_written:
43
+ success_msg = f"Bundled cursor rules into {output}"
44
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
45
+ else:
46
+ output_path.unlink(missing_ok=True)
47
+ log.info("no cursor rules to bundle")
302
48
 
303
- logger.info("Bundling Cursor rules and commands", rules_dir=rules_dir, commands_dir=commands_dir, output_file=output_path, config=config)
49
+ commands_output_path = cwd / "commands.md"
50
+ commands_written = agent.bundle_commands(commands_output_path)
51
+ if commands_written:
52
+ success_msg = "Bundled cursor commands into commands.md"
53
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
54
+ else:
55
+ commands_output_path.unlink(missing_ok=True)
304
56
 
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
57
 
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}")
58
+ def github(
59
+ output: Annotated[
60
+ str, typer.Argument(help="Output file for instructions")
61
+ ] = "instructions.md",
62
+ ) -> None:
63
+ """Bundle GitHub instructions into instructions.md and prompts into commands.md."""
64
+
65
+ agent = get_agent("github")
66
+ cwd = Path.cwd()
67
+
68
+ rules_dir = agent.rules_dir
69
+ if not rules_dir:
70
+ log.error("github rules directory not configured")
71
+ raise typer.Exit(1)
312
72
 
73
+ log.info(
74
+ "bundling github instructions and prompts",
75
+ instructions_dir=rules_dir,
76
+ prompts_dir=agent.commands_dir,
77
+ )
313
78
 
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),
79
+ rules_path = cwd / rules_dir
80
+ if not rules_path.exists():
81
+ log.error(
82
+ "github instructions directory not found", instructions_dir=str(rules_path)
324
83
  )
84
+ error_msg = f"GitHub instructions directory not found: {rules_path}"
85
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
86
+ raise typer.Exit(1)
325
87
 
326
- # Load section globs (with optional custom config)
327
- SECTION_GLOBS = load_section_globs(config)
88
+ output_path = cwd / output
89
+ instructions_written = agent.bundle_rules(output_path)
90
+ if instructions_written:
91
+ success_msg = f"Bundled github instructions into {output}"
92
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
93
+ else:
94
+ output_path.unlink(missing_ok=True)
95
+ log.info("no github instructions to bundle")
328
96
 
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)
97
+ commands_output_path = cwd / "commands.md"
98
+ prompts_written = agent.bundle_commands(commands_output_path)
99
+ if prompts_written:
100
+ success_msg = "Bundled github prompts into commands.md"
101
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
102
+ else:
103
+ commands_output_path.unlink(missing_ok=True)
332
104
 
333
- logger.info("Bundling GitHub instructions and prompts", instructions_dir=instructions_dir, prompts_dir=prompts_dir, output_file=output_path, config=config)
334
105
 
335
- if not Path(instructions_dir).exists():
336
- logger.error("GitHub instructions directory not found", instructions_dir=instructions_dir)
337
- raise typer.Exit(1)
106
+ def claude(
107
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
108
+ ) -> None:
109
+ """Bundle Claude Code commands into commands.md."""
110
+
111
+ agent = get_agent("claude")
112
+ cwd = Path.cwd()
338
113
 
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}")
114
+ commands_dir = agent.commands_dir
115
+ if not commands_dir:
116
+ log.error("claude code commands directory not configured")
117
+ raise typer.Exit(1)
342
118
 
119
+ log.info(
120
+ "bundling claude code commands",
121
+ commands_dir=commands_dir,
122
+ )
343
123
 
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),
124
+ commands_path = cwd / commands_dir
125
+ if not commands_path.exists():
126
+ log.error(
127
+ "claude code commands directory not found", commands_dir=str(commands_path)
354
128
  )
129
+ error_msg = f"Claude Code commands directory not found: {commands_path}"
130
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
131
+ raise typer.Exit(1)
355
132
 
356
- # Load section globs (with optional custom config)
357
- SECTION_GLOBS = load_section_globs(config)
133
+ output_path = cwd / output
134
+ commands_written = agent.bundle_commands(output_path)
135
+ if commands_written:
136
+ success_msg = f"Bundled claude commands into {output}"
137
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
138
+ else:
139
+ output_path.unlink(missing_ok=True)
140
+ log.info("no claude commands to bundle")
358
141
 
359
- commands_dir = os.path.join(os.getcwd(), ".claude", "commands")
360
- output_path = os.path.join(os.getcwd(), output)
361
142
 
362
- logger.info("Bundling Claude Code commands", commands_dir=commands_dir, output_file=output_path, config=config)
143
+ def gemini(
144
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
145
+ ) -> None:
146
+ """Bundle Gemini CLI commands into commands.md."""
363
147
 
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)
148
+ agent = get_agent("gemini")
149
+ cwd = Path.cwd()
367
150
 
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}")
151
+ commands_dir = agent.commands_dir
152
+ if not commands_dir:
153
+ log.error("gemini cli commands directory not configured")
154
+ raise typer.Exit(1)
371
155
 
156
+ log.info(
157
+ "bundling gemini cli commands",
158
+ commands_dir=commands_dir,
159
+ )
372
160
 
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),
161
+ commands_path = cwd / commands_dir
162
+ if not commands_path.exists():
163
+ log.error(
164
+ "gemini cli commands directory not found", commands_dir=str(commands_path)
383
165
  )
166
+ error_msg = f"Gemini CLI commands directory not found: {commands_path}"
167
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
168
+ raise typer.Exit(1)
169
+
170
+ output_path = cwd / output
171
+ commands_written = agent.bundle_commands(output_path)
172
+ if commands_written:
173
+ success_msg = f"Bundled gemini commands into {output}"
174
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
175
+ else:
176
+ output_path.unlink(missing_ok=True)
177
+ log.info("no gemini commands to bundle")
384
178
 
385
- # Load section globs (with optional custom config)
386
- SECTION_GLOBS = load_section_globs(config)
387
179
 
388
- commands_dir = os.path.join(os.getcwd(), ".gemini", "commands")
389
- output_path = os.path.join(os.getcwd(), output)
180
+ def opencode(
181
+ output: Annotated[str, typer.Argument(help="Output file")] = "commands.md",
182
+ ) -> None:
183
+ """Bundle OpenCode commands into commands.md."""
390
184
 
391
- logger.info("Bundling Gemini CLI commands", commands_dir=commands_dir, output_file=output_path, config=config)
185
+ agent = get_agent("opencode")
186
+ cwd = Path.cwd()
392
187
 
393
- if not Path(commands_dir).exists():
394
- logger.error("Gemini CLI commands directory not found", commands_dir=commands_dir)
188
+ commands_dir = agent.commands_dir
189
+ if not commands_dir:
190
+ log.error("opencode commands directory not configured")
395
191
  raise typer.Exit(1)
396
192
 
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}")
193
+ log.info(
194
+ "bundling opencode commands",
195
+ commands_dir=commands_dir,
196
+ )
197
+
198
+ commands_path = cwd / commands_dir
199
+ if not commands_path.exists():
200
+ log.error(
201
+ "opencode commands directory not found", commands_dir=str(commands_path)
202
+ )
203
+ error_msg = f"OpenCode commands directory not found: {commands_path}"
204
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
205
+ raise typer.Exit(1)
400
206
 
207
+ output_path = cwd / output
208
+ commands_written = agent.bundle_commands(output_path)
209
+ if commands_written:
210
+ success_msg = f"Bundled opencode commands into {output}"
211
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
212
+ else:
213
+ output_path.unlink(missing_ok=True)
214
+ log.info("no opencode commands to bundle")