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