diff --git a/.gitignore b/.gitignore
index 3983192..8c6ae29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,11 @@
/src/version/_version.py
src/*.egg-info
+__pycache__
dist
build
out
+.venv
+.env
.coverage
.*.history*
*.tags.cache*
diff --git a/README.md b/README.md
index e594511..6473b80 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://pypi.org/project/cedarscript-editor/)
[](https://pypi.org/project/cedarscript-editor/)
[](https://github.com/psf/black)
-[](https://www.gnu.org/licenses/agpl-3.0)
+[](https://opensource.org/licenses/Apache-2.0)
`CEDARScript Editor (Python)` is a [CEDARScript](https://bit.ly/cedarscript) runtime
for interpreting `CEDARScript` scripts and performing code analysis and modification operations on a codebase.
@@ -42,36 +42,151 @@ pip install cedarscript-editor
## Usage
-Here's a quick example of how to use `CEDARScript` Editor:
+### Python Library
+
+Here's a quick example of how to use `CEDARScript` Editor as a Python library:
```python
-from cedarscript_editor import CEDARScriptEdior
-
-parser = CEDARScriptEdior()
-code = """
-CREATE FILE "example.py"
-UPDATE FILE "example.py"
- INSERT AT END OF FILE
- CONTENT
- print("Hello, World!")
- END CONTENT
-END UPDATE
+from cedarscript_editor import CEDARScriptEditor
+
+editor = CEDARScriptEditor("/path/to/project")
+
+# Parse and execute CEDARScript commands
+cedarscript = """```CEDARScript
+CREATE FILE "example.py" WITH
+"""
+print("Hello, World!")
+"""
+```"""
+
+# Apply commands to the codebase
+results = editor.apply_cedarscript(cedarscript)
+print(results)
+```
+
+### Command Line Interface
+
+`cedarscript-editor` also provides a CLI for executing CEDARScript commands directly from the command line.
+
+#### Installation
+
+After installing via pip, the `cedarscript` command will be available:
+
+```bash
+pip install cedarscript-editor
+```
+
+#### Basic Usage
+
+```bash
+# Execute CEDARScript directly
+cedarscript 'CREATE FILE "example.py" WITH "print(\"Hello World\")"'
+
+# Read CEDARScript from file
+cedarscript -f commands.cedar
+cedarscript --file commands.cedar
+
+# Read from STDIN
+cat commands.cedar | cedarscript
+echo 'UPDATE FILE "test.py" INSERT LINE 1 "import os"' | cedarscript
+
+# Specify base directory
+cedarscript --root /path/to/project -f commands.cedar
+
+# Quiet mode for scripting
+cedarscript --quiet -f commands.cedar
+
+# Syntax check only
+cedarscript --check -f commands.cedar
+```
+
+#### CLI Options
+
+- `-f, --file FILENAME`: Read CEDARScript commands from file
+- `--root DIRECTORY`: Base directory for file operations (default: current directory)
+- `-q, --quiet`: Minimal output (for scripting)
+- `--check`: Syntax check only - parse commands without executing
+- `COMMAND`: Direct CEDARScript command (alternative to file input)
+
+#### CEDARScript File Format
+
+CEDARScript commands must be enclosed in fenced code blocks:
+
+````markdown
+```CEDARScript
+CREATE FILE "example.py" WITH
"""
+print("Hello, World!")
+"""
+```
+````
-commands, errors = parser.parse_script(code)
+Or use the `` tag for direct command execution:
-if errors:
- for error in errors:
- print(f"Error: {error}")
-else:
- for command in commands:
- print(f"Parsed command: {command}")
+```cedarscript
+
+CREATE FILE "example.py" WITH
+"""
+print("Hello, World!")
+"""
+```
+
+#### Examples
+
+**Create a new file:**
+```bash
+cedarscript '```CEDARScript
+CREATE FILE "utils.py" WITH
+"""
+def hello():
+ print("Hello from utils!")
+"""
+```'
+```
+
+**Update an existing file:**
+```bash
+cat > update_commands.cedar << 'EOF'
+```CEDARScript
+UPDATE FILE "app.py"
+INSERT LINE 1
+ """Application module"""
+INSERT AFTER "import os"
+ import sys
+```'
+EOF
+
+cedarscript -f update_commands.cedar
+```
+
+**Multi-file operations:**
+```bash
+cat > refactor.cedar << 'EOF'
+```CEDARScript
+# Move method to top level
+UPDATE CLASS "DataProcessor"
+FROM FILE "data.py"
+MOVE METHOD "process"
+
+# Update call sites
+UPDATE FUNCTION "main"
+FROM FILE "main.py"
+REPLACE LINE 5
+ result = process(data)
+```'
+EOF
+
+cedarscript --root ./my-project -f refactor.cedar
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
+## See Also
+
+- https://github.com/oraios/serena
+
## License
This project is licensed under the MIT License.
diff --git a/pyproject.toml b/pyproject.toml
index da22f44..0542c25 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,7 @@ dependencies = [
# https://github.com/grantjenks/py-tree-sitter-languages/issues/64
"tree-sitter==0.21.3", # 0.22 breaks tree-sitter-languages
"tree-sitter-languages==1.10.2",
+ "click>=8.0.0",
]
requires-python = ">=3.11"
@@ -38,6 +39,9 @@ Documentation = "https://github.com/CEDARScript/cedarscript-editor-python#readme
Repository = "https://github.com/CEDARScript/cedarscript-editor-python.git"
"Bug Tracker" = "https://github.com/CEDARScript/cedarscript-editor-python/issues"
+[project.scripts]
+cedarscript = "cedarscript_editor.cli:main"
+
[project.optional-dependencies]
dev = [
"pytest>=7.0",
diff --git a/src/cedarscript_editor/__init__.py b/src/cedarscript_editor/__init__.py
index c7b0617..7ca7dfd 100644
--- a/src/cedarscript_editor/__init__.py
+++ b/src/cedarscript_editor/__init__.py
@@ -9,17 +9,22 @@
# TODO Move to cedarscript-ast-parser
-def find_commands(content: str):
+def find_commands(content: str, require_fenced: bool = True):
# Regex pattern to match CEDARScript blocks
pattern = r'```CEDARScript\n(.*?)```'
cedar_script_blocks = re.findall(pattern, content, re.DOTALL)
print(f'[find_cedar_commands] Script block count: {len(cedar_script_blocks)}')
- if len(cedar_script_blocks) == 0 and not content.strip().endswith(''):
- raise ValueError(
- "No CEDARScript block detected. "
- "Perhaps you forgot to enclose the block using ```CEDARScript and ``` ? "
- "Or was that intentional? If so, just write tag as the last line"
- )
+ if require_fenced:
+ if len(cedar_script_blocks) == 0 and not content.strip().endswith(''):
+ raise ValueError(
+ "No CEDARScript block detected. "
+ "Perhaps you forgot to enclose the block using ```CEDARScript and ``` ? "
+ "Or was that intentional? If so, just write tag as the last line"
+ )
+ else:
+ # New behavior - treat entire content as single block
+ cedar_script_blocks = [content]
+ print(f'[find_cedar_commands] Processing entire input as single CEDARScript block')
cedarscript_parser = CEDARScriptASTParser()
for cedar_script in cedar_script_blocks:
parsed_commands, parse_errors = cedarscript_parser.parse_script(cedar_script)
diff --git a/src/cedarscript_editor/cli.py b/src/cedarscript_editor/cli.py
new file mode 100644
index 0000000..726a2ab
--- /dev/null
+++ b/src/cedarscript_editor/cli.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+"""
+CEDARScript Editor CLI Interface
+
+Provides command-line interface for executing CEDARScript commands
+on code files using the CEDARScript Editor library.
+"""
+
+import sys
+from pathlib import Path
+from typing import Optional
+
+import click
+from cedarscript_ast_parser import CEDARScriptASTParser
+
+from .cedarscript_editor import CEDARScriptEditor, CEDARScriptEditorException
+from . import find_commands
+
+
+@click.command(help="Execute CEDARScript commands on code files")
+@click.option(
+ '--file', '-f',
+ type=click.File('r'),
+ help='Read CEDARScript commands from file'
+)
+@click.option(
+ '--root', '-r',
+ type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
+ default=Path.cwd(),
+ help='Base directory for file operations (default: current directory)'
+)
+@click.option(
+ '--quiet', '-q',
+ is_flag=True,
+ help='Minimal output (for scripting)'
+)
+@click.option(
+ '--check', '-c',
+ is_flag=True,
+ help='Syntax check only - parse commands without executing'
+)
+@click.option(
+ '--fenced', '-F',
+ is_flag=True,
+ default=False,
+ help='Require CEDARScript blocks to be fenced with ```CEDARScript (default: treat entire input as single block)'
+)
+@click.argument('command', required=False)
+def main(
+ file: Optional[click.File],
+ root: Path,
+ quiet: bool,
+ check: bool,
+ fenced: bool,
+ command: Optional[str]
+) -> None:
+ """
+ CEDARScript Editor CLI
+
+ Execute CEDARScript commands to transform code files.
+
+ Examples:
+ \b
+ # Direct command
+ cedarscript "UPDATE myfile.py SET imports.append('import os')"
+
+ # From file (both short and long forms)
+ cedarscript -f commands.cedar
+ cedarscript --file commands.cedar
+
+ # From STDIN
+ cat commands.cedar | cedarscript
+
+ # With custom base directory
+ cedarscript -r /path/to/project -f commands.cedar
+ cedarscript --root /path/to/project -f commands.cedar
+
+ # Quiet mode for scripting
+ cedarscript -q -f commands.cedar
+ cedarscript --quiet -f commands.cedar
+
+ # Syntax check only
+ cedarscript -c -f commands.cedar
+ cedarscript --check -f commands.cedar
+
+ # With fenced requirement
+ cedarscript -F -f commands.cedar
+ cedarscript --fenced -f commands.cedar
+
+ # Mixed short and long flags
+ cedarscript -r /path/to -c -F --file commands.cedar
+ """
+ try:
+ # Determine command source
+ commands = _get_commands_input(file, command, quiet)
+
+ if not commands.strip():
+ _echo_error("No CEDARScript commands provided", quiet)
+ sys.exit(1)
+
+ # Parse commands
+ if not quiet:
+ click.echo("Parsing CEDARScript commands...")
+
+ try:
+ parsed_commands = list(find_commands(commands, require_fenced=fenced))
+ except Exception as e:
+ _echo_error(f"Failed to parse CEDARScript: {e}", quiet)
+ sys.exit(1)
+
+ if not quiet:
+ click.echo(f"Parsed {len(parsed_commands)} command(s)")
+
+ # If syntax check only, exit here
+ if check:
+ if not quiet:
+ click.echo("Syntax check passed - commands are valid")
+ sys.exit(0)
+
+ # Execute commands
+ if not quiet:
+ click.echo(f"Executing commands in directory: {root}")
+
+ editor = CEDARScriptEditor(str(root))
+ results = editor.apply_commands(parsed_commands)
+
+ # Output results
+ if not quiet:
+ click.echo("\nResults:")
+ for result in results:
+ click.echo(f" {result}")
+ else:
+ # Quiet mode - just show success/failure
+ click.echo(f"Applied {len(results)} command(s)")
+
+ except CEDARScriptEditorException as e:
+ _echo_error(f"CEDARScript execution error: {e}", quiet)
+ sys.exit(2)
+ except KeyboardInterrupt:
+ _echo_error("Operation cancelled by user", quiet)
+ sys.exit(130)
+ except Exception as e:
+ _echo_error(f"Unexpected error: {e}", quiet)
+ sys.exit(3)
+
+
+def _get_commands_input(
+ file: Optional[click.File],
+ command: Optional[str],
+ quiet: bool
+) -> str:
+ """
+ Determine the source of CEDARScript commands based on provided inputs.
+
+ Priority: command argument > file option > STDIN
+ """
+ if command:
+ return command
+
+ if file:
+ return file.read()
+
+ # Check if STDIN has data (not a TTY)
+ if not sys.stdin.isatty():
+ try:
+ return sys.stdin.read()
+ except Exception as e:
+ _echo_error(f"Error reading from STDIN: {e}", quiet)
+ sys.exit(1)
+
+ return ""
+
+
+def _echo_error(message: str, quiet: bool) -> None:
+ """Echo error message with appropriate formatting."""
+ if quiet:
+ # In quiet mode, send errors to stderr but keep them brief
+ click.echo(f"Error: {message}", err=True)
+ else:
+ # In normal mode, use styled error output
+ click.echo(click.style(f"Error: {message}", fg='red'), err=True)
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file