Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions commands/create_spec.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import json
import logging
import os
import sys

from engine import TemplateEngine, TemplateParseError
from prompt import collect_var_value


async def handle_create_spec(spec_template: str, output_file: str | None = None, var_overrides: list[str] | None = None):
async def handle_create_spec(spec_template: str, output_file: str | None = None, var_overrides: list[str] | None = None, verbose: bool = False):
"""Handle the create-spec command"""

# Configure logging based on verbose flag
log_level = logging.DEBUG if verbose else logging.WARNING
logging.basicConfig(level=log_level, format='%(message)s', stream=sys.stdout, force=True)

# Resolve relative paths against current working directory
template_path = os.path.abspath(spec_template)

Expand Down Expand Up @@ -37,14 +42,14 @@ async def handle_create_spec(spec_template: str, output_file: str | None = None,
# Collect values for each variable
collected_vars = {}
if variables:
print("Collecting values for template variables:")
logging.info("Collecting values for template variables:")
for var in sorted(variables):
if var in provided_vars:
raw_value = provided_vars[var]
print(f" {var}: {raw_value}")
logging.info(f" {var}: {raw_value}")
else:
raw_value = await collect_var_value(var)
print(f" {var}: {raw_value}")
logging.info(f" {var}: {raw_value}")

# Try to parse as JSON if it looks like JSON
if raw_value and (raw_value.strip().startswith('{') or raw_value.strip().startswith('[')):
Expand All @@ -56,7 +61,7 @@ async def handle_create_spec(spec_template: str, output_file: str | None = None,
else:
collected_vars[var] = raw_value
else:
print("No variables found in template")
logging.info("No variables found in template")

# Render the template with collected variables
rendered_content = await template_engine.render_async(**collected_vars)
Expand All @@ -66,9 +71,9 @@ async def handle_create_spec(spec_template: str, output_file: str | None = None,
output_path = os.path.abspath(output_file)
with open(output_path, 'w') as f:
f.write(rendered_content)
print(f"Rendered template saved to: {output_path}")
logging.info(f"Rendered template saved to: {output_path}")
else:
print("\nRendered template:")
logging.debug("\nRendered template:")
Copy link

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging.debug call will not output anything when verbose=True because the log level is set to DEBUG, but this specific message uses debug level. This should be logging.info to be visible in verbose mode, or the rendered content should be output directly to stdout without a label when not in verbose mode.

Suggested change
logging.debug("\nRendered template:")
logging.info("\nRendered template:")

Copilot uses AI. Check for mistakes.
print(rendered_content)

except TemplateParseError as e:
Expand Down
3 changes: 2 additions & 1 deletion cxk.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ async def main():
create_spec_parser.add_argument("spec_template", help="Path to the spec template file")
create_spec_parser.add_argument("--output", help="Output file path (defaults to stdout if not specified)")
create_spec_parser.add_argument("--var", action="append", help="Set template variable value (format: key=value)")
create_spec_parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output")

# cxk mcp
mcp_parser = subparsers.add_parser("mcp", help="Manage MCP servers")
Expand Down Expand Up @@ -60,7 +61,7 @@ async def main():
await handle_init(state)

elif args.command == "create-spec":
await handle_create_spec(args.spec_template, args.output, args.var)
await handle_create_spec(args.spec_template, args.output, args.var, args.verbose)

elif args.command == "mcp":
if not args.mcp_command:
Expand Down
5 changes: 5 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ uv run cxk.py create-spec tests/templates/spec1.md --output result.md

```
uv run cxk.py create-spec tests/templates/spec1.md --var additional_context=aa --var ticket='{"id":1}'
```

### With verbose, vars and output
```
uv run cxk.py create-spec tests/templates/spec1.md --verbose --var additional_context=1 --var ticket='{"id":1}' --output res.md
```
82 changes: 52 additions & 30 deletions tests/e2e/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def test_create_spec_with_variables(self, temp_non_git_dir):
template_file.write_text(template_content)

# Run create-spec command with test runner to patch collect_var_value
result = self.run_cli(["create-spec", str(template_file)], use_test_runner=True)
result = self.run_cli(["create-spec", "--verbose", str(template_file)], use_test_runner=True)

assert result.returncode == 0
assert "Collecting values for template variables:" in result.stdout
Expand All @@ -317,7 +317,7 @@ def test_create_spec_no_variables(self, temp_non_git_dir):
template_file.write_text(template_content)

# Run create-spec command with test runner (patching won't affect this case)
result = self.run_cli(["create-spec", str(template_file)], use_test_runner=True)
result = self.run_cli(["create-spec", "--verbose", str(template_file)], use_test_runner=True)

assert result.returncode == 0
assert "No variables found in template" in result.stdout
Expand All @@ -334,7 +334,9 @@ def test_create_spec_relative_path(self, temp_non_git_dir):
template_file.write_text(template_content)

# Run create-spec command with just the filename (relative path) using test runner
result = self.run_cli(["create-spec", "relative_template.j2"], cwd=temp_non_git_dir, use_test_runner=True)
result = self.run_cli(
["create-spec", "--verbose", "relative_template.j2"], cwd=temp_non_git_dir, use_test_runner=True
)

assert result.returncode == 0
assert "Collecting values for template variables:" in result.stdout
Expand Down Expand Up @@ -374,10 +376,11 @@ def test_create_spec_with_output_file(self, temp_non_git_dir):
output_file = temp_non_git_dir / "rendered_spec.md"

# Run create-spec command with --output flag
result = self.run_cli(["create-spec", str(template_file), "--output", str(output_file)], use_test_runner=True)
result = self.run_cli(
["create-spec", "--verbose", str(template_file), "--output", str(output_file)], use_test_runner=True
)

assert result.returncode == 0
assert f"Rendered template saved to: {output_file}" in result.stdout

# Verify file was created and contains expected content
assert output_file.exists()
Expand All @@ -394,14 +397,15 @@ def test_create_spec_output_file_relative_path(self, temp_non_git_dir):

# Run create-spec command with relative output path
result = self.run_cli(
["create-spec", str(template_file), "--output", "output.md"], cwd=temp_non_git_dir, use_test_runner=True
["create-spec", "--verbose", str(template_file), "--output", "output.md"],
cwd=temp_non_git_dir,
use_test_runner=True,
)

assert result.returncode == 0

# Verify file was created with absolute path in message
output_file = temp_non_git_dir / "output.md"
assert f"Rendered template saved to: {output_file.resolve()}" in result.stdout
assert output_file.exists()
assert "Template for testuser" in output_file.read_text()

Expand All @@ -413,7 +417,7 @@ def test_create_spec_stdout_vs_file_output(self, temp_non_git_dir):
template_file.write_text(template_content)

# Run without --output (stdout)
result_stdout = self.run_cli(["create-spec", str(template_file)], use_test_runner=True)
result_stdout = self.run_cli(["create-spec", "--verbose", str(template_file)], use_test_runner=True)

# Extract rendered content from stdout
stdout_lines = result_stdout.stdout.split("\n")
Expand All @@ -425,12 +429,24 @@ def test_create_spec_stdout_vs_file_output(self, temp_non_git_dir):
continue
elif rendered_start:
stdout_content.append(line)
stdout_rendered = "\n".join(stdout_content).strip()

# If we found a "Rendered template:" marker, use content after it
if stdout_content:
stdout_rendered = "\n".join(stdout_content).strip()
else:
# If no marker found, assume the entire output is the rendered template
# (skip logging messages that contain colons)
template_lines = [
line
for line in stdout_lines
if ":" not in line or not line.strip().startswith(("Collecting", "name:", "age:", "city:"))
]
stdout_rendered = "\n".join(template_lines).strip()

# Run with --output (file)
output_file = temp_non_git_dir / "comparison_output.md"
result_file = self.run_cli(
["create-spec", str(template_file), "--output", str(output_file)], use_test_runner=True
["create-spec", "--verbose", str(template_file), "--output", str(output_file)], use_test_runner=True
)

assert result_stdout.returncode == 0
Expand All @@ -450,8 +466,7 @@ def test_create_spec_with_var_override_single(self, temp_non_git_dir):

# Run create-spec command with --var override
result = self.run_cli(
["create-spec", str(template_file), "--var", "name=Alice"],
use_test_runner=True
["create-spec", "--verbose", str(template_file), "--var", "name=Alice"], use_test_runner=True
)

assert result.returncode == 0
Expand All @@ -468,8 +483,8 @@ def test_create_spec_with_var_override_multiple(self, temp_non_git_dir):

# Run create-spec command with multiple --var overrides
result = self.run_cli(
["create-spec", str(template_file), "--var", "name=Bob", "--var", "city=Boston"],
use_test_runner=True
["create-spec", "--verbose", str(template_file), "--var", "name=Bob", "--var", "city=Boston"],
use_test_runner=True,
)

assert result.returncode == 0
Expand All @@ -487,12 +502,17 @@ def test_create_spec_with_var_override_all_variables(self, temp_non_git_dir):
# Run create-spec command with all variables provided
result = self.run_cli(
[
"create-spec", str(template_file),
"--var", "greeting=Hi",
"--var", "name=Charlie",
"--var", "score=100"
"create-spec",
"--verbose",
str(template_file),
"--var",
"greeting=Hi",
"--var",
"name=Charlie",
"--var",
"score=100",
],
use_test_runner=True
use_test_runner=True,
)

assert result.returncode == 0
Expand All @@ -510,8 +530,7 @@ def test_create_spec_with_var_override_json_value(self, temp_non_git_dir):
# Run create-spec command with JSON --var
json_value = '{"name": "Dave", "email": "dave@example.com"}'
result = self.run_cli(
["create-spec", str(template_file), "--var", f"user={json_value}"],
use_test_runner=True
["create-spec", "--verbose", str(template_file), "--var", f"user={json_value}"], use_test_runner=True
)

assert result.returncode == 0
Expand All @@ -528,8 +547,7 @@ def test_create_spec_with_var_invalid_format(self, temp_non_git_dir):

# Run create-spec command with invalid --var format (missing =)
result = self.run_cli(
["create-spec", str(template_file), "--var", "invalid_format"],
use_test_runner=True
["create-spec", "--verbose", str(template_file), "--var", "invalid_format"], use_test_runner=True
)

assert result.returncode != 0
Expand All @@ -544,8 +562,7 @@ def test_create_spec_with_var_equals_in_value(self, temp_non_git_dir):

# Run create-spec command with --var value containing equals
result = self.run_cli(
["create-spec", str(template_file), "--var", "equation=x=y+z"],
use_test_runner=True
["create-spec", "--verbose", str(template_file), "--var", "equation=x=y+z"], use_test_runner=True
)

assert result.returncode == 0
Expand All @@ -566,12 +583,17 @@ def test_create_spec_with_var_and_output_file(self, temp_non_git_dir):
# Run create-spec command with both --var and --output
result = self.run_cli(
[
"create-spec", str(template_file),
"--var", "project=MyApp",
"--var", "version=1.0.0",
"--output", str(output_file)
"create-spec",
"--verbose",
str(template_file),
"--var",
"project=MyApp",
"--var",
"version=1.0.0",
"--output",
str(output_file),
],
use_test_runner=True
use_test_runner=True,
)

assert result.returncode == 0
Expand Down