# โ cli dev test, cli dev typecheck
```
-myproject/
-โโโ src/ # YOUR BUSINESS LOGIC GOES HERE
-โ โโโ myproject/ # Your Python package
-โ โโโ __init__.py
-โ โโโ core.py # Core functionality
-โ โโโ models.py # Data models
-โ โโโ utils.py # Utilities
-โโโ commands/ # CLI commands (keep these separate)
-โ โโโ subs/ # Subcommand modules
-โ โ โโโ proj.py # Example: project commands
-โ โ โโโ dev.py # Example: dev tools
-โ โ โโโ myapp.py # YOUR CLI COMMANDS GO HERE
-โ โโโ main.py # CLI entry point (router)
-โโโ tests/ # Your tests
-โโโ setup.sh # One-command setup
-โโโ pyproject.toml # Project configuration
+
+- **Auto-discovers** all your commands and options
+- **Context-aware** suggestions based on what you're typing
+- **Works everywhere** - bash, zsh, fish, even in SSH sessions
+- **Zero config** - installs automatically with `./setup.sh`
+
+### ๐ค CI/CD Ready - Non-Interactive by Design
+
+**GitHub Actions? Jenkins? GitLab CI?** We've got you covered:
+
+```yaml
+# That's it. No complex setup. It just works.
+- run: |
+ ./setup.sh -y # Non-interactive mode
+ source .venv/bin/activate
+ cli dev all # Run all checks
+ cli build --all # Build everything
+ cli test # Test everything
```
-## ๐ Table of Contents
+- **Exit codes that make sense** - 0 for success, non-zero for any failure
+- **Structured output** - Parse-friendly for your CI pipelines
+- **Quiet modes** - `--quiet` for minimal output, `--verbose` for debugging
+- **No interactive prompts** - Everything can be automated
+- **Docker-friendly** - Works perfectly in containers
-- [Getting Started](#-getting-started)
-- [Quick Start](#-quick-start)
-- [Features](#-features)
-- [Development](#-development)
-- [Architecture](#-architecture)
-- [Versioning](#-versioning)
-- [License](#-license)
+### ๐ Universal Command Interface
-## ๐ Quick Start
+**One CLI, Any Language, Any Tool:**
```bash
-# Setup (installs dependencies and pre-commit hooks)
-./setup.sh
+# Instead of remembering:
+# make build && npm run build && cargo build && go build
+# Just:
+cli build all
+
+# Instead of:
+# pytest && npm test && cargo test && go test
+# Just:
+cli test
+
+# Instead of:
+# black . && ruff check && prettier --write && cargo fmt
+# Just:
+cli dev format
+```
-# Activate the virtual environment
-source .venv/bin/activate
+Your team will thank you. Your future self will thank you.
-# Now use 'cli' directly
-cli --help
-cli proj -s # Show repository size
-cli dev -a # Run all code checks
+## ๐๏ธ Architecture
+
+
+
+```mermaid
+flowchart TB
+ subgraph YourFocus["๐ฏ YOUR FOCUS AREA"]
+ direction TB
+ A[fa:fa-brain Your Ideas]
+ B[fa:fa-code Your Core Logic]
+ C[fa:fa-flask Your Research]
+ D[fa:fa-robot Your AI Models]
+ A --> B
+ C --> B
+ D --> B
+ end
+
+ B ==>|Just Write Code| CLI[fa:fa-terminal ehAyeโข Core CLI Framework]
+
+ subgraph Infrastructure["๐ง WE HANDLE ALL THIS"]
+ direction TB
+
+ subgraph DevTools["๐ ๏ธ Development Tools"]
+ DT1[fa:fa-paint-brush Black
Auto-formatting]
+ DT2[fa:fa-search Ruff
Fast Linting]
+ DT3[fa:fa-shield MyPy
Type Safety]
+ DT4[fa:fa-check-circle Pytest
Testing Suite]
+ DT5[fa:fa-code-branch Pre-commit
Git Hooks]
+ DT6[fa:fa-terminal Shell
Completion]
+ end
+
+ subgraph BuildSys["๐ฆ Build System"]
+ BS1[fa:fa-linux Linux
Builds]
+ BS2[fa:fa-apple macOS
Builds]
+ BS3[fa:fa-windows Windows
Builds]
+ BS4[fa:fa-microchip ARM64
Support]
+ BS5[fa:fa-desktop x86_64
Support]
+ BS6[fa:fa-bug Debug
Builds]
+ end
+
+ subgraph Package["๐ Package Management"]
+ PK1[fa:fa-box Wheel
Creation]
+ PK2[fa:fa-upload PyPI
Publishing]
+ PK3[fa:fa-download Dependency
Resolution]
+ PK4[fa:fa-archive Source
Distribution]
+ PK5[fa:fa-certificate Package
Signing]
+ PK6[fa:fa-check Verification]
+ end
+
+ subgraph Release["๐ Release Automation"]
+ RL1[fa:fa-tag Version
Tagging]
+ RL2[fa:fa-github GitHub
Releases]
+ RL3[fa:fa-docker Docker
Images]
+ RL4[fa:fa-file-text Changelog
Generation]
+ RL5[fa:fa-cloud CI/CD
Pipeline]
+ RL6[fa:fa-bell Notifications]
+ end
+
+ subgraph Quality["โ
Quality Assurance"]
+ QA1[fa:fa-microscope Code
Coverage]
+ QA2[fa:fa-shield-alt Security
Scanning]
+ QA3[fa:fa-chart-line Performance
Metrics]
+ QA4[fa:fa-book Documentation
Check]
+ QA5[fa:fa-sync Integration
Tests]
+ QA6[fa:fa-globe Cross-platform
Tests]
+ end
+ end
+
+ CLI --> DevTools
+ CLI --> BuildSys
+ CLI --> Package
+ CLI --> Release
+ CLI --> Quality
+
+ subgraph Commands["๐ป CLI COMMANDS"]
+ direction LR
+ CMD1[cli dev all]
+ CMD2[cli build --target]
+ CMD3[cli package dist]
+ CMD4[cli release create]
+ CMD5[cli proj stats]
+ end
+
+ DevTools -.-> CMD1
+ BuildSys -.-> CMD2
+ Package -.-> CMD3
+ Release -.-> CMD4
+ Quality -.-> CMD5
+
+ style YourFocus fill:#C8E6C9,stroke:#2E7D32,stroke-width:4px,color:#000
+ style Infrastructure fill:#FFF3E0,stroke:#F57C00,stroke-width:2px,color:#000
+ style CLI fill:#BBDEFB,stroke:#1565C0,stroke-width:3px,color:#000
+ style DevTools fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
+ style BuildSys fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
+ style Package fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
+ style Release fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
+ style Quality fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
+ style Commands fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#000
+
+ classDef focus fill:#C8E6C9,stroke:#2E7D32,stroke-width:3px,color:#000
+ classDef framework fill:#BBDEFB,stroke:#1565C0,stroke-width:2px,color:#000
+ classDef tool fill:#FFF3E0,stroke:#F57C00,stroke-width:1px,color:#000
+ classDef command fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#000
+
+ class A,B,C,D focus
+ class CLI framework
+ class DT1,DT2,DT3,DT4,DT5,DT6,BS1,BS2,BS3,BS4,BS5,BS6,PK1,PK2,PK3,PK4,PK5,PK6,RL1,RL2,RL3,RL4,RL5,RL6,QA1,QA2,QA3,QA4,QA5,QA6 tool
+ class CMD1,CMD2,CMD3,CMD4,CMD5 command
```
+### ๐ **Perfect for AI Developers & Researchers**
+
+**Your Focus:** Research, Models, Algorithms, Data Processing
+**Our Focus:** DevOps, Testing, Building, Packaging, Distribution
+
+
+
## โจ Features
-- ๐งฉ **Modular Architecture**: Each command group in its own module
-- ๐ง **Development Tools**: Integrated linting (ruff), formatting (black), and type checking (mypy)
-- ๐ **Pre-commit Hooks**: Automatic code quality checks before commits
-- ๐ฏ **Auto-setup**: Virtual environment and dependencies managed automatically
-- ๐ **Type-Safe**: Full type annotations with strict mypy checking
-- ๐ฆ **Zero Config**: Works out of the box with sensible defaults
-- ๐ **Production Ready**: Best practices baked in from the start
+### ๐งฉ Modular Command Architecture
+Each command group lives in its own module. Add new commands by creating a file in `commands/subs/`:
-## ๐ Requirements
+```python
+# commands/subs/hello.py
+import click
+
+@click.group()
+def hello() -> None:
+ """Hello world commands"""
+ pass
+
+@hello.command()
+def world() -> None:
+ """Say hello to the world"""
+ click.echo("Hello, World! ๐")
+```
-- Python 3.9 or higher
-- Git (for pre-commit hooks)
-- Unix-like environment (Linux, macOS, WSL)
+### ๐ง Professional Development Tools
+Built-in development commands that enforce code quality:
+
+```bash
+cli dev format # Auto-format with Black
+cli dev lint # Lint with Ruff
+cli dev typecheck # Type check with MyPy
+cli dev test # Run tests with pytest
+cli dev all # Run everything at once
+```
-## ๐ ๏ธ Development
+### ๐จ Rich Command Examples
+Placeholder commands with comprehensive options to learn from:
```bash
-# Format code
-cli dev -f
+# Build commands with platform targeting
+cli build all --target linux --arch x86_64 --release
+
+# Package commands with multiple formats
+cli package build --format wheel --sign --include-deps
-# Run linter
-cli dev -l
+# Release commands with distribution support
+cli release create --version 1.0.0 --draft --notes "First release!"
+cli release publish --target pypi --skip-tests
+```
-# Type check
-cli dev -t
+### ๐ Type Safety Throughout
+Full type annotations with strict MyPy checking:
-# Run all checks
-cli dev -a
+```python
+from typing import Optional, List, Dict
+from pathlib import Path
+
+def process_files(
+ files: List[Path],
+ options: Dict[str, Any],
+ output: Optional[Path] = None
+) -> bool:
+ """Fully typed functions catch errors before runtime"""
+ ...
```
-Pre-commit hooks run automatically on `git commit`.
+### ๐ฏ Shell Completion
+Tab completion that just works:
-## ๐๏ธ Architecture
+```bash
+cli
+# Shows: build dev package proj release version
+
+cli dev
+# Shows: all format lint typecheck test precommit
+
+cli build all --
+# Shows: --target --arch --force --copy-only --debug --release
+```
+
+### ๐ Project Intelligence
+Built-in project management commands:
+
+```bash
+cli proj info # Git status, branch info, recent commits
+cli proj size # Repository size analysis
+cli proj stats # File counts, lines of code, language breakdown
+```
+
+## ๐ Command Showcase
+
+### Development Workflow
+
+```bash
+# Start your day - check project status
+$ cli proj info
+๐ Project Information
+Git branch: main
+Status: 3 modified files
+Latest commit: 2 hours ago
+
+# Make changes and check quality
+$ cli dev all
+โ
Black: All formatted
+โ
Ruff: No issues
+โ
MyPy: Type safe
+โ
Tests: 42 passed
+
+# Ready to commit - run pre-commit checks
+$ cli dev precommit --fix
+โ
All pre-commit checks passed!
+```
+
+### Extensible Placeholders
-See [commands/README.md](commands/README.md) for detailed architecture documentation.
+The template includes thoughtfully designed placeholder commands that demonstrate various CLI patterns:
-## ๐ Versioning
+#### Build System
+```bash
+cli build all --target darwin --arch arm64 --release
+cli build clean --force --cache --deps
+cli build component my-component --copy-only
+```
+
+#### Package Management
+```bash
+cli package build --format wheel --output ./dist
+cli package dist --upload-url https://pypi.org --verify
+cli package list --outdated --format json
+cli package verify package.whl --check-signature
+```
+
+#### Release Automation
+```bash
+cli release create --version 2.0.0 --tag v2.0.0 --draft
+cli release publish --target github --token $GITHUB_TOKEN
+cli release list --limit 10
+cli release delete 1.0.0-beta --keep-tag
+```
+
+## ๐๏ธ Project Structure
+
+```
+your-project/
+โโโ commands/ # CLI implementation
+โ โโโ config.py # Project configuration (customize here!)
+โ โโโ main.py # CLI entry point
+โ โโโ subs/ # Command modules
+โ โ โโโ build/ # Build commands
+โ โ โโโ dev/ # Development tools
+โ โ โโโ package/ # Package management
+โ โ โโโ proj/ # Project utilities
+โ โ โโโ release/ # Release automation
+โ โโโ utils/ # Shared utilities
+โ โโโ tests/ # Test suite
+โโโ tools/ # Development tools
+โโโ .pre-commit-config.yaml
+โโโ pyproject.toml # Project configuration
+โโโ setup.sh # One-command setup
+โโโ LICENSE # AGPL-3.0
+โโโ README.md # You are here!
+```
+
+## ๐ ๏ธ Customization Guide
+
+### 1. Make It Yours
+
+Edit `commands/config.py`:
+
+```python
+PROJECT_NAME = "MyCLI"
+PROJECT_DESCRIPTION = "My awesome CLI tool"
+```
-Version is managed in `commands/__version__.py`. To update:
+### 2. Add Your Commands
+
+Create new command groups in `commands/subs/`:
```python
-# commands/__version__.py
-__version__ = "1.0.0" # Update this
+# commands/subs/database.py
+import click
+
+@click.group()
+def database() -> None:
+ """Database management commands"""
+ pass
+
+@database.command()
+@click.option("--host", default="localhost")
+def connect(host: str) -> None:
+ """Connect to database"""
+ click.echo(f"Connecting to {host}...")
```
-Access version in your code:
+### 3. Register Commands
+
+Add to `commands/main.py`:
+
```python
-from commands import __version__
-print(f"Version: {__version__}")
+from commands.subs.database import database
+
+cli.add_command(database)
```
-The version is automatically used in:
-- `cli --version`
-- Package metadata
-- PyPI uploads (if you publish)
+## ๐ Requirements
+
+- Python 3.9 or higher
+- Git (for pre-commit hooks)
+- Unix-like environment (Linux, macOS, WSL)
+
+## ๐งช Testing
+
+The template includes a complete testing setup:
+
+```bash
+# Run all tests
+cli dev test
+
+# Run specific test file
+pytest commands/tests/test_main.py -v
+
+# Run with coverage
+pytest --cov=commands --cov-report=html
+
+# Open coverage report
+open htmlcov/index.html
+```
+
+## ๐ Pre-commit Hooks
+
+Quality checks run automatically on every commit:
+
+- **Black** - Code formatting
+- **Ruff** - Fast Python linting
+- **MyPy** - Static type checking
+
+Run manually anytime:
+
+```bash
+cli dev precommit # Check staged files
+cli dev precommit --fix # Auto-fix issues
+cli dev precommit --ci # Check all files
+```
+
+## ๐ Documentation
+
+- [CLAUDE.md](CLAUDE.md) - Development guidelines and conventions
+- [Commands Reference](#-command-showcase) - Detailed command documentation
+- [API Documentation](docs/api.md) - Python API reference (if applicable)
+
+## ๐ค Contributing
+
+We love contributions! Whether it's:
+
+- ๐ Bug reports
+- ๐ก Feature suggestions
+- ๐ Documentation improvements
+- ๐ง Code contributions
+
+Please check our [Contributing Guide](CONTRIBUTING.md) (coming soon) for details.
## ๐ License
-MIT License - see [LICENSE](LICENSE) file for details.
+This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details.
----
+The AGPL-3.0 ensures that any modifications to this CLI framework remain open source, benefiting the entire community.
+
+## ๐ Acknowledgments
+
+### Built With
+
+- [Click](https://click.palletsprojects.com/) - Command line interface creation kit
+- [Black](https://github.com/psf/black) - The uncompromising code formatter
+- [Ruff](https://github.com/astral-sh/ruff) - An extremely fast Python linter
+- [MyPy](https://mypy-lang.org/) - Static type checker for Python
+- [Rich](https://github.com/Textualize/rich) - Rich text and beautiful formatting
+
+### Special Thanks
+
+If you find ehAyeโข Core CLI helpful, we'd appreciate a mention:
+
+> This project was bootstrapped with [ehAyeโข Core CLI](https://github.com/neekware/ehAyeCoreCLI)
+
+## ๐ฆ Status
-### ๐ Attribution
+
-If your project is public and you found CoreCLI helpful, we'd appreciate a mention like:
-> This project was bootstrapped with [CoreCLI](https://github.com/neekware/CoreCLI)
+**Project Status:** ๐ข Active Development
+[](https://github.com/neekware/ehAyeCoreCLI/issues)
+[](https://github.com/neekware/ehAyeCoreCLI/pulls)
+[](https://github.com/neekware/ehAyeCoreCLI)
+
+
---
+
+
+**Ready to build something amazing?**
+
+[Get Started Now](#-quick-start) โข [Star on GitHub](https://github.com/neekware/ehAyeCoreCLI) โข [Report an Issue](https://github.com/neekware/ehAyeCoreCLI/issues)
+
+
+
+**Built with Python ๐**
+
Developed with โค๏ธ by [Val Neekman](https://github.com/un33k) @ [Neekware Inc.](https://neekware.com)
+
+
diff --git a/assets/ehAye.png b/assets/ehAye.png
new file mode 100644
index 0000000..d00e859
Binary files /dev/null and b/assets/ehAye.png differ
diff --git a/cli b/cli
deleted file mode 100755
index 3349289..0000000
--- a/cli
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/bash
-# cli - project root version (auto-setup and auto-activates venv)
-# This version is placed in project root and automatically handles environment setup
-
-# Get the project root (where this script is located)
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-PROJECT_ROOT="$SCRIPT_DIR"
-VENV_DIR="$PROJECT_ROOT/.venv"
-VENV_BIN_DIR="$VENV_DIR/bin"
-
-# Check if venv exists
-if [ ! -d "$VENV_DIR" ]; then
- echo "Virtual environment not found. Running setup..." >&2
-
- # Run setup.sh with -y flag to auto-confirm
- if [ -x "$PROJECT_ROOT/setup.sh" ]; then
- "$PROJECT_ROOT/setup.sh" -y
-
- # Check if setup succeeded
- if [ $? -ne 0 ]; then
- echo "Error: Setup failed" >&2
- exit 1
- fi
- else
- echo "Error: setup.sh not found or not executable" >&2
- exit 1
- fi
-fi
-
-# Check if Python exists in venv
-if [ ! -x "$VENV_BIN_DIR/python" ]; then
- echo "Error: Python not found in virtual environment" >&2
- echo "Running setup.sh to fix..." >&2
- "$PROJECT_ROOT/setup.sh" -y
-
- if [ ! -x "$VENV_BIN_DIR/python" ]; then
- echo "Error: Setup failed to create working environment" >&2
- exit 1
- fi
-fi
-
-# If we're not already in the virtual environment, activate it and re-run
-if [ -z "$VIRTUAL_ENV" ] || [ "$VIRTUAL_ENV" != "$VENV_DIR" ]; then
- # Source the activation script and re-execute this script with all arguments
- exec /bin/bash -c "source '$VENV_DIR/bin/activate' && exec '$0' \"\$@\"" -- "$@"
-fi
-
-# Run the CLI directly - we're now in the activated venv
-cd "$PROJECT_ROOT"
-exec python -m commands.main "$@"
\ No newline at end of file
diff --git a/commands/__init__.py b/commands/__init__.py
index e105d90..c63cec0 100644
--- a/commands/__init__.py
+++ b/commands/__init__.py
@@ -1,5 +1,10 @@
-"""Core CLI - A modular command-line interface framework"""
+"""ehAyeโข Core CLI - A modular command-line interface framework"""
-from .__version__ import __version__, __version_info__
+# Simple version for the CLI
+__version__ = "0.1.0"
+__version_info__ = tuple(int(i) for i in __version__.split("."))
-__all__ = ["__version__", "__version_info__"]
+__all__ = [
+ "__version__",
+ "__version_info__",
+]
diff --git a/commands/__main__.py b/commands/__main__.py
new file mode 100644
index 0000000..7ffabca
--- /dev/null
+++ b/commands/__main__.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+"""Module entry point for running commands as a package"""
+
+from commands.main import main
+
+if __name__ == "__main__":
+ main()
diff --git a/commands/__version__.py b/commands/__version__.py
deleted file mode 100644
index 221411c..0000000
--- a/commands/__version__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""Version information for Core CLI"""
-
-__version__ = "0.1.0"
-__version_info__ = tuple(int(i) for i in __version__.split("."))
diff --git a/commands/bin/cli-venv b/commands/bin/cli-venv
index cb020c1..71536cb 100755
--- a/commands/bin/cli-venv
+++ b/commands/bin/cli-venv
@@ -45,6 +45,36 @@ if [ "$VIRTUAL_ENV" != "$VENV_DIR" ]; then
exit 1
fi
+# Check if we're in the project root directory
+CURRENT_DIR="$(pwd)"
+if [ "$CURRENT_DIR" != "$PROJECT_ROOT" ]; then
+ echo "cli ($PROJECT_NAME)" >&2
+ echo "Error: Unauthorized execution path" >&2
+ echo "" >&2
+ echo "CLI is designed to run from the root of the project where ./setup.sh was invoked." >&2
+ echo "" >&2
+ echo "Expected directory: $PROJECT_ROOT" >&2
+ echo "Current directory: $CURRENT_DIR" >&2
+ echo "" >&2
+
+ # Check if current directory looks like a moved/renamed project
+ if [ -f "$CURRENT_DIR/setup.sh" ] && [ -d "$CURRENT_DIR/commands" ] && [ -f "$CURRENT_DIR/pyproject.toml" ]; then
+ echo "โ ๏ธ It appears this project was moved or renamed." >&2
+ echo "" >&2
+ echo "The virtual environment is still linked to the old location." >&2
+ echo "To fix this, please run:" >&2
+ echo "" >&2
+ echo " ./setup.sh" >&2
+ echo "" >&2
+ echo "This will reconfigure the CLI for the new location." >&2
+ else
+ echo "Please go to: $PROJECT_ROOT" >&2
+ echo "" >&2
+ echo "If you have copied or renamed the original project directory, please run ./setup.sh" >&2
+ echo "in the new location to reconfigure the CLI." >&2
+ fi
+ exit 1
+fi
+
# Use the venv's Python to run CLI as a module
-cd "$PROJECT_ROOT"
exec "$VENV_BIN_DIR/python" -m commands.main "$@"
\ No newline at end of file
diff --git a/commands/completion.sh b/commands/completion.sh
new file mode 100644
index 0000000..710a9b1
--- /dev/null
+++ b/commands/completion.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# Git-tracked completion wrapper for ehAyeโข Core CLI
+#
+# This stable wrapper handles shell completion hookup logic and sources
+# the auto-generated completion functions. It provides:
+# - Universal shell support (bash + zsh)
+# - Path resolution fallbacks
+# - Development reload functionality
+# - Interactive shell detection
+#
+# The wrapper sources: commands/autogen/completion.sh (auto-generated, git-ignored)
+
+# Function to force reload completion (useful for development)
+reload_cli_completion() {
+ # Setup completion based on shell type
+ if [[ -n "$ZSH_VERSION" ]]; then
+ # zsh: enable bash compatibility
+ autoload -U +X bashcompinit && bashcompinit
+ elif [[ -n "$BASH_VERSION" ]]; then
+ # bash: completion should work natively
+ true
+ fi
+
+ # Clear existing completion
+ complete -r cli 2>/dev/null
+ unset -f _cli_completion 2>/dev/null
+ unset -f _corecli_completions 2>/dev/null
+
+ # Get the directory where this script is located
+ local SCRIPT_DIR
+ if [[ -n "${BASH_SOURCE[0]}" ]]; then
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ else
+ # Fallback: assume we're in project root
+ SCRIPT_DIR="$(pwd)/commands"
+ fi
+
+ # Source the auto-generated completion script
+ local AUTOGEN_COMPLETION="$SCRIPT_DIR/autogen/completion.sh"
+
+ if [[ -f "$AUTOGEN_COMPLETION" ]]; then
+ source "$AUTOGEN_COMPLETION" 2>/dev/null || true
+ export _CORECLI_COMPLETION_LOADED="$(date)"
+ echo "โ
CLI completion reloaded ($([[ -n "$ZSH_VERSION" ]] && echo "zsh" || echo "bash"))"
+ else
+ echo "โ Completion script not found: $AUTOGEN_COMPLETION"
+ echo " Run 'cli dev completion sync' to generate it"
+ fi
+}
+
+# Only enable completion for interactive shells
+if [[ $- == *i* ]]; then
+ # Get the directory where this script is located
+ if [[ -n "${BASH_SOURCE[0]}" ]]; then
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ else
+ # Fallback: assume we're in project root
+ SCRIPT_DIR="$(pwd)/commands"
+ fi
+
+ # Source the auto-generated completion script
+ AUTOGEN_COMPLETION="$SCRIPT_DIR/autogen/completion.sh"
+
+ if [[ -f "$AUTOGEN_COMPLETION" ]]; then
+ # Setup completion based on shell type
+ if [[ -n "$ZSH_VERSION" ]]; then
+ # zsh: enable bash compatibility
+ autoload -U +X bashcompinit && bashcompinit
+ elif [[ -n "$BASH_VERSION" ]]; then
+ # bash: completion should work natively
+ true
+ fi
+
+ # Clear any existing completion first
+ complete -r cli 2>/dev/null
+ unset -f _cli_completion 2>/dev/null
+ unset -f _corecli_completions 2>/dev/null
+
+ source "$AUTOGEN_COMPLETION" 2>/dev/null || true
+ export _CORECLI_COMPLETION_LOADED="$(date)"
+ fi
+fi
diff --git a/commands/config.py b/commands/config.py
new file mode 100644
index 0000000..f8d5a56
--- /dev/null
+++ b/commands/config.py
@@ -0,0 +1,19 @@
+"""Central configuration for ehAyeโข Core CLI"""
+
+from commands import __version__
+
+# Project metadata
+PROJECT_NAME = "MyProject" # Change this to your project name
+PROJECT_DESCRIPTION = (
+ "A Python CLI application" # Change this to your project description
+)
+CLI_NAME = "ehAyeโข Core CLI"
+CLI_COMMAND = "cli"
+
+__all__ = [
+ "PROJECT_NAME",
+ "PROJECT_DESCRIPTION",
+ "CLI_NAME",
+ "CLI_COMMAND",
+ "__version__",
+]
diff --git a/commands/main.py b/commands/main.py
index 7ce906c..a2cd4c9 100644
--- a/commands/main.py
+++ b/commands/main.py
@@ -1,106 +1,61 @@
#!/usr/bin/env python3
-"""CLI - Main entry point"""
+"""ehAyeโข Core CLI - Main entry point"""
-import argparse
-from pathlib import Path
+import sys
-from . import __version__
-from .subs.dev import DevCommands
-from .subs.proj import ProjectCommands
+import click
+from commands import __version__
+from commands.config import CLI_NAME
+from commands.subs.build import build
+from commands.subs.dev import dev
+from commands.subs.package import package
+from commands.subs.proj import proj
+from commands.subs.release import release
-def main() -> None:
- """Main CLI entry point"""
- parser = argparse.ArgumentParser(
- prog="cli",
- description="Core CLI - A modular command-line interface",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- cli proj -s # Show repository size
- cli proj -i # Show project information
- cli proj --stats # Show detailed statistics
-
- cli dev -f # Format code with black
- cli dev -l # Lint code with ruff
- cli dev -a # Run all checks
- """,
- )
- parser.add_argument(
- "--version", action="version", version=f"%(prog)s {__version__}"
- )
- parser.add_argument("--debug", action="store_true", help="Enable debug output")
+@click.group()
+@click.version_option(version=__version__, prog_name="cli")
+@click.option("--debug", is_flag=True, help="Enable debug output")
+@click.pass_context
+def cli(ctx: click.Context, debug: bool) -> None:
+ """ehAyeโข Core CLI - A modular command-line interface
- # Add subcommands
- subparsers = parser.add_subparsers(dest="command", help="Available commands")
+ Examples:
+ cli proj info # Show project information
+ cli proj size # Show repository size
+ cli proj stats # Show detailed statistics
- # Add project commands
- ProjectCommands.add_subparser(subparsers)
+ cli dev format # Format code with black
+ cli dev lint # Lint code with ruff
+ cli dev all # Run all checks
+ """
+ ctx.ensure_object(dict)
+ ctx.obj["DEBUG"] = debug
- # Add dev commands
- DevCommands.add_subparser(subparsers)
- # Parse arguments
- args = parser.parse_args()
+# Add a version command that shows version info
+@cli.command()
+def version() -> None:
+ """Show version information"""
+ click.echo(f"{CLI_NAME} version: {__version__}")
- # Get project root (where cli was called from)
- project_root = Path.cwd()
- # Handle commands
- if args.command == "proj":
- proj_cmd = ProjectCommands(project_root)
+# Add command groups - sorted alphabetically for consistency
+cli.add_command(build)
+cli.add_command(dev)
+cli.add_command(package)
+cli.add_command(proj)
+cli.add_command(release)
- if args.size:
- size = proj_cmd.get_repo_size()
- print(f"Repository size: {size}")
- elif args.info:
- info = proj_cmd.get_git_info()
- if "error" in info:
- print(f"Error: {info['error']}")
- else:
- print(f"Branch: {info.get('branch', 'unknown')}")
- print(f"Total commits: {info.get('commits', 'unknown')}")
- print(
- f"Uncommitted changes: {'Yes' if info.get('has_changes') else 'No'}"
- )
- elif args.stats:
- stats = proj_cmd.get_stats()
- if "error" in stats:
- print(f"Error: {stats['error']}")
- else:
- print("Repository Statistics:")
- print(f" Total files: {stats.get('total_files', 0)}")
- print(f" Total directories: {stats.get('total_directories', 0)}")
- print(f" Total lines of code: {stats.get('total_lines', 0):,}")
- file_types = stats.get("file_types")
- if file_types and isinstance(file_types, list):
- print("\nTop file types:")
- for ext, count in file_types:
- print(f" {ext}: {count} files")
- else:
- # No flags provided, show help for proj command
- parser.parse_args(["proj", "--help"])
- elif args.command == "dev":
- dev_cmd = DevCommands(project_root)
-
- if args.all:
- dev_cmd.run_all_checks()
- elif args.format:
- dev_cmd.format_code(check_only=args.check)
- elif args.lint:
- dev_cmd.lint_code(fix=args.fix)
- elif args.type_check:
- dev_cmd.type_check()
- elif args.pre_commit:
- dev_cmd.setup_pre_commit(uninstall=args.uninstall)
- else:
- # No flags provided, show help for dev command
- parser.parse_args(["dev", "--help"])
- else:
- # No command provided, show main help
- parser.print_help()
+def main() -> None:
+ """Main entry point"""
+ try:
+ cli(prog_name="cli")
+ except Exception as e:
+ click.echo(f"Error: {e}", err=True)
+ sys.exit(1)
if __name__ == "__main__":
diff --git a/commands/subs/README.md b/commands/subs/README.md
new file mode 100644
index 0000000..e979255
--- /dev/null
+++ b/commands/subs/README.md
@@ -0,0 +1,169 @@
+# Commands Structure Pattern
+
+## Directory Organization
+
+This directory follows a clean, modular pattern for organizing CLI commands:
+
+### Pattern Rules
+
+1. **Each command group gets its own directory**
+ ```
+ commands/subs/
+ s3/ # S3 commands
+ onnx/ # ONNX commands
+ build/ # Build commands (if complex)
+ ```
+
+2. **Directory structure for command groups**
+ ```
+ s3/
+ __init__.py # Router only - imports and registers subcommands
+ upload.py # Individual command implementation
+ download.py # Individual command implementation
+ list.py # Individual command implementation
+ configure.py # Individual command implementation
+ bucket.py # Subgroup with its own commands
+ acl.py # Subgroup with its own commands
+ utils.py # Shared utilities for this command group
+ ```
+
+3. **Router pattern (__init__.py)**
+ - Contains only the Click group definition
+ - Imports all subcommands
+ - Registers them with `.add_command()`
+ - NO implementation logic
+
+ ```python
+ """S3 command router - imports all subcommands"""
+
+ import click
+
+ from commands.subs.s3.upload import upload
+ from commands.subs.s3.download import download
+ # ... other imports
+
+ @click.group()
+ def s3() -> None:
+ """S3 artifact management"""
+ pass
+
+ # Add all subcommands
+ s3.add_command(upload)
+ s3.add_command(download)
+ # ... other commands
+ ```
+
+4. **Simple commands stay as single files**
+ - If a command is simple (< 100 lines), keep it as a single file
+ - Examples: `clean.py`, `dev.py`
+
+5. **Import in main.py**
+ ```python
+ from commands.subs.s3 import s3 # Import from directory
+ from commands.subs.build import build # Import from directory
+ ```
+
+### Benefits
+
+1. **Small files** - Each command in its own file (< 500 lines rule)
+2. **Clear organization** - Easy to find commands
+3. **Modular** - Easy to add/remove commands
+4. **Shared utilities** - Each command group can have its own utils
+5. **Scalable** - Pattern works for any number of commands
+
+### Migration Guide
+
+When a single-file command grows too large:
+
+1. Create a directory with the command name
+2. Create `__init__.py` with the router pattern
+3. Move each subcommand to its own file
+4. Move shared functions to `utils.py`
+5. Update imports in `main.py`
+
+### Example: S3 Command Structure
+
+```
+s3/
+ __init__.py # Router: @click.group() def s3()
+ upload.py # @click.command() def upload()
+ download.py # @click.command() def download()
+ list.py # @click.command(name="list") def list_artifacts()
+ configure.py # @click.command() def configure()
+ bucket.py # @click.group() def bucket() with subcommands
+ acl.py # @click.group() def acl() with subcommands
+ utils.py # get_aws_cmd() and other shared functions
+```
+
+### Example: Mirror Command Structure (Nested Groups)
+
+For commands with multiple subgroups, create nested directories:
+
+```
+mirror/
+ __init__.py # Main router: @click.group() def mirror()
+ onnx/
+ __init__.py # Subgroup router: @click.group() def onnx()
+ push.py # @click.command() def push()
+ pull.py # @click.command() def pull()
+ pcaudio/
+ __init__.py # Subgroup router: @click.group() def pcaudio()
+ push.py # @click.command() def push()
+ pull.py # @click.command() def pull()
+ ort/
+ __init__.py # Subgroup router: @click.group() def ort()
+ fetch.py # @click.command() def fetch()
+```
+
+This pattern:
+- Keeps related commands together
+- Makes it easy to find specific functionality
+- Allows for shared utilities within each subgroup
+- Scales well as more commands are added
+
+This keeps each file focused and under 500 lines while maintaining a clean, predictable structure.
+
+## Mirror Commands Pattern
+
+The mirror commands follow a specific pattern for S3 and CloudFront usage:
+
+### Key Principle: Push to S3, Pull from CloudFront
+
+1. **Push commands** (`cli mirror * push`)
+ - Upload artifacts directly to S3
+ - Use AWS credentials for authentication
+ - S3 paths configured in: `config/project-config.json`
+ - Also update catalog.json with metadata
+
+2. **Pull commands** (`cli mirror * pull`)
+ - Download from CloudFront for speed and reliability
+ - CloudFront URL and paths from: `config/project-config.json`
+ - Falls back to S3 if CloudFront unavailable
+
+3. **Vendor commands** (`cli vendor *`)
+ - Download from external sites (GitHub, HuggingFace, etc.)
+ - Cache locally for reuse
+ - These are the original sources
+
+### Example Flow
+
+```bash
+# 1. First time: fetch from external vendor
+cli vendor onnx libs fetch --target darwin --arch arm64
+
+# 2. Push to S3 for team/CI access
+cli mirror onnx push --target darwin --arch arm64
+
+# 3. Future builds: pull from CloudFront (fast)
+cli mirror onnx pull --target darwin --arch arm64
+```
+
+### Configuration
+
+All S3 bucket, CloudFront URLs, and path prefixes are defined in:
+- `config/project-config.json` - See the `s3` section
+
+This pattern ensures:
+- Fast downloads via CloudFront CDN
+- Reliable uploads to S3
+- Clear separation between external vendors and our mirror
\ No newline at end of file
diff --git a/commands/subs/__init__.py b/commands/subs/__init__.py
index 2e3a2e0..dd27fe4 100644
--- a/commands/subs/__init__.py
+++ b/commands/subs/__init__.py
@@ -1 +1,4 @@
-"""Command modules for CLI"""
+"""Command modules for ehAyeโข Core CLI"""
+
+# Note: We don't import subcommands here to avoid loading everything at startup.
+# Each command is imported individually in main.py when needed.
diff --git a/commands/subs/build/__init__.py b/commands/subs/build/__init__.py
new file mode 100644
index 0000000..ddd4e77
--- /dev/null
+++ b/commands/subs/build/__init__.py
@@ -0,0 +1,113 @@
+"""Build commands - stubbed for future implementation"""
+
+from typing import Optional
+
+import click
+
+__all__ = ["build"]
+
+
+@click.group()
+def build() -> None:
+ """Build commands (placeholder for project builds)"""
+ pass
+
+
+@build.command()
+@click.option(
+ "--target",
+ type=click.Choice(["linux", "darwin", "windows"], case_sensitive=False),
+ help="Target platform (linux, darwin, windows)",
+)
+@click.option(
+ "--arch",
+ type=click.Choice(["x86_64", "arm64", "aarch64", "i386"], case_sensitive=False),
+ help="Target architecture",
+)
+@click.option("--force", is_flag=True, help="Force rebuild even if up-to-date")
+@click.option("--copy-only", is_flag=True, help="Only copy files, don't compile")
+@click.option("--debug", is_flag=True, help="Build with debug symbols")
+@click.option("--release", is_flag=True, help="Build optimized release version")
+def all(
+ target: Optional[str],
+ arch: Optional[str],
+ force: bool,
+ copy_only: bool,
+ debug: bool,
+ release: bool,
+) -> None:
+ """Build all targets"""
+ click.echo("Build all: Not yet implemented")
+
+ # Show what options were provided as reference
+ if target:
+ click.echo(f" Target platform: {target}")
+ if arch:
+ click.echo(f" Architecture: {arch}")
+ if force:
+ click.echo(" Force rebuild: enabled")
+ if copy_only:
+ click.echo(" Copy-only mode: enabled")
+ if debug:
+ click.echo(" Debug build: enabled")
+ if release:
+ click.echo(" Release build: enabled")
+
+ click.echo("\nThis is a placeholder for future build functionality")
+
+
+@build.command()
+@click.option("--force", is_flag=True, help="Force clean even if already clean")
+@click.option("--cache", is_flag=True, help="Also clean cache directories")
+@click.option("--deps", is_flag=True, help="Also clean dependencies")
+def clean(force: bool, cache: bool, deps: bool) -> None:
+ """Clean build artifacts"""
+ click.echo("Clean: Not yet implemented")
+
+ if force:
+ click.echo(" Force clean: enabled")
+ if cache:
+ click.echo(" Clean cache: enabled")
+ if deps:
+ click.echo(" Clean dependencies: enabled")
+
+ click.echo("\nThis is a placeholder for cleaning build artifacts")
+
+
+@build.command()
+@click.argument("component", required=False)
+@click.option(
+ "--target",
+ type=click.Choice(["linux", "darwin", "windows"], case_sensitive=False),
+ help="Target platform",
+)
+@click.option(
+ "--arch",
+ type=click.Choice(["x86_64", "arm64", "aarch64"], case_sensitive=False),
+ help="Target architecture",
+)
+@click.option("--force", is_flag=True, help="Force rebuild")
+@click.option("--copy-only", is_flag=True, help="Only copy files, don't compile")
+def component(
+ component: Optional[str],
+ target: Optional[str],
+ arch: Optional[str],
+ force: bool,
+ copy_only: bool,
+) -> None:
+ """Build a specific component"""
+ if component:
+ click.echo(f"Build component '{component}': Not yet implemented")
+ else:
+ click.echo("Build component: Not yet implemented (no component specified)")
+
+ if target:
+ click.echo(f" Target: {target}")
+ if arch:
+ click.echo(f" Architecture: {arch}")
+ if force:
+ click.echo(" Force rebuild: enabled")
+ if copy_only:
+ click.echo(" Copy-only mode: enabled")
+
+ click.echo("\nThis is a placeholder for component build functionality")
diff --git a/commands/subs/dev.py b/commands/subs/dev.py
deleted file mode 100644
index dcf0d21..0000000
--- a/commands/subs/dev.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""Development tools CLI commands"""
-
-import argparse
-import subprocess
-import sys
-from pathlib import Path
-from typing import Any
-
-
-class DevCommands:
- """Commands for development tools like linting and formatting"""
-
- def __init__(self, project_root: Path):
- self.project_root = project_root
-
- def run_command(self, cmd: list[str], description: str) -> tuple[bool, str]:
- """Run a command and return success status and output"""
- try:
- result = subprocess.run(
- cmd, capture_output=True, text=True, cwd=self.project_root
- )
-
- if result.returncode == 0:
- return True, result.stdout
- else:
- return False, result.stderr or result.stdout
-
- except FileNotFoundError:
- return False, f"{cmd[0]} not found. Run: pip install -e '.[dev]'"
- except Exception as e:
- return False, str(e)
-
- def format_code(self, check_only: bool = False) -> None:
- """Run black formatter on the codebase"""
- cmd = ["black", "."]
- if check_only:
- cmd.append("--check")
- print("Checking code formatting...")
- else:
- print("Formatting code...")
-
- success, output = self.run_command(cmd, "black")
-
- if check_only and not success:
- print("โ Code formatting issues found. Run: cli dev --format")
- if output:
- print(output)
- sys.exit(1)
- elif success:
- print("โ
Code formatting OK" if check_only else "โ
Code formatted")
- else:
- print(f"โ Error: {output}")
- sys.exit(1)
-
- def lint_code(self, fix: bool = False) -> None:
- """Run ruff linter on the codebase"""
- cmd = ["ruff", "check", "."]
- if fix:
- cmd.append("--fix")
- print("Linting and fixing code...")
- else:
- print("Linting code...")
-
- success, output = self.run_command(cmd, "ruff")
-
- if not success and not fix:
- print("โ Linting issues found. Run: cli dev --lint --fix")
- if output:
- print(output)
- sys.exit(1)
- elif success:
- print("โ
No linting issues found")
- else:
- print(f"โ Error: {output}")
- sys.exit(1)
-
- def type_check(self) -> None:
- """Run mypy type checker"""
- print("Type checking code...")
-
- # Run mypy on the commands package
- cmd = ["mypy", "commands"]
- success, output = self.run_command(cmd, "mypy")
-
- if success:
- print("โ
Type checking passed")
- if output:
- print(output)
- else:
- print("โ Type checking failed")
- print(output)
- sys.exit(1)
-
- def run_all_checks(self) -> None:
- """Run all checks (format check, lint, type check)"""
- print("Running all checks...\n")
-
- # Format check (don't modify)
- self.format_code(check_only=True)
- print()
-
- # Lint check
- self.lint_code(fix=False)
- print()
-
- # Type check
- self.type_check()
-
- print("\nโ
All checks passed!")
-
- def setup_pre_commit(self, uninstall: bool = False) -> None:
- """Install or uninstall pre-commit hooks"""
- if uninstall:
- print("Uninstalling pre-commit hooks...")
- cmd = ["pre-commit", "uninstall"]
- else:
- print("Installing pre-commit hooks...")
- cmd = ["pre-commit", "install"]
-
- success, output = self.run_command(cmd, "pre-commit")
-
- if success:
- action = "uninstalled" if uninstall else "installed"
- print(f"โ
Pre-commit hooks {action}")
- if not uninstall:
- print("Hooks will run automatically on git commit")
- else:
- print(f"โ Error: {output}")
- sys.exit(1)
-
- @staticmethod
- def add_subparser(
- subparsers: "argparse._SubParsersAction[Any]",
- ) -> argparse.ArgumentParser:
- """Add dev subcommands to argument parser"""
- dev_parser = subparsers.add_parser("dev", help="Development tools")
-
- # Add flags for different operations
- dev_parser.add_argument(
- "-f", "--format", action="store_true", help="Format code with black"
- )
- dev_parser.add_argument(
- "-l", "--lint", action="store_true", help="Lint code with ruff"
- )
- dev_parser.add_argument(
- "-t", "--type-check", action="store_true", help="Type check with mypy"
- )
- dev_parser.add_argument(
- "-a", "--all", action="store_true", help="Run all checks"
- )
- dev_parser.add_argument(
- "-p", "--pre-commit", action="store_true", help="Install pre-commit hooks"
- )
-
- # Modifiers
- dev_parser.add_argument(
- "--check", action="store_true", help="Check only, don't modify (for format)"
- )
- dev_parser.add_argument(
- "--fix", action="store_true", help="Fix issues automatically (for lint)"
- )
- dev_parser.add_argument(
- "--uninstall", action="store_true", help="Uninstall pre-commit hooks"
- )
-
- return dev_parser # type: ignore[no-any-return]
diff --git a/commands/subs/dev/__init__.py b/commands/subs/dev/__init__.py
new file mode 100644
index 0000000..39153f4
--- /dev/null
+++ b/commands/subs/dev/__init__.py
@@ -0,0 +1,27 @@
+"""Development commands router"""
+
+import click
+
+from commands.subs.dev.all import all
+from commands.subs.dev.completion import completion
+from commands.subs.dev.format import format
+from commands.subs.dev.lint import lint
+from commands.subs.dev.precommit import precommit
+from commands.subs.dev.test import test
+from commands.subs.dev.typecheck import typecheck
+
+
+@click.group()
+def dev() -> None:
+ """Development tools"""
+ pass
+
+
+# Add subcommands
+dev.add_command(format)
+dev.add_command(lint)
+dev.add_command(typecheck)
+dev.add_command(test)
+dev.add_command(all)
+dev.add_command(precommit)
+dev.add_command(completion)
diff --git a/commands/subs/dev/all.py b/commands/subs/dev/all.py
new file mode 100644
index 0000000..6916ec8
--- /dev/null
+++ b/commands/subs/dev/all.py
@@ -0,0 +1,37 @@
+"""Run all development checks command"""
+
+import subprocess
+import sys
+
+import click
+
+
+@click.command()
+def all() -> None:
+ """Run all checks"""
+ click.echo("Running all development checks...\n")
+
+ commands = [
+ (["black", "--check", "."], "Formatting check"),
+ (["ruff", "check", "."], "Linting"),
+ (["mypy", "commands"], "Type checking"),
+ (["python", "commands/tests/test_cmd_completion.py"], "Completion tests"),
+ (["pytest", "-v"], "Tests"),
+ ]
+
+ failed = []
+ for cmd, name in commands:
+ click.echo("\n" + "=" * 60)
+ click.echo("Running " + name + "...")
+ click.echo("=" * 60)
+
+ result = subprocess.run(cmd)
+ if result.returncode != 0:
+ failed.append(name)
+
+ if failed:
+ click.echo(f"\nโ Failed checks: {', '.join(failed)}", err=True)
+ sys.exit(1)
+ else:
+ click.echo("\nโ
All checks passed!")
+ sys.exit(0)
diff --git a/commands/subs/dev/completion.py b/commands/subs/dev/completion.py
new file mode 100644
index 0000000..9f5076f
--- /dev/null
+++ b/commands/subs/dev/completion.py
@@ -0,0 +1,96 @@
+"""Shell completion management commands"""
+
+import subprocess
+import sys
+from pathlib import Path
+
+import click
+
+
+@click.group()
+def completion() -> None:
+ """Shell completion management"""
+ pass
+
+
+@completion.command(name="test")
+def test_completion() -> None:
+ """Test shell completion functionality"""
+ click.echo("Running completion tests...")
+ result = subprocess.run(
+ ["python", "commands/tests/test_cmd_completion.py"],
+ capture_output=True,
+ text=True,
+ )
+
+ click.echo(result.stdout)
+ if result.stderr:
+ click.echo(result.stderr, err=True)
+
+ if result.returncode != 0:
+ click.echo("โ Completion tests failed!", err=True)
+ sys.exit(1)
+ else:
+ click.echo("โ
Completion tests passed!")
+ sys.exit(0)
+
+
+@completion.command()
+def sync() -> None:
+ """Sync shell completion with current CLI commands"""
+ project_root = Path(__file__).parent.parent.parent.parent
+ completion_path = project_root / "commands" / "autogen" / "completion.sh"
+
+ # Files to check for changes
+ command_files = [
+ project_root / "commands" / "main.py",
+ *list((project_root / "commands" / "subs").rglob("*.py")),
+ ]
+
+ # Check if regeneration is needed
+ if completion_path.exists():
+ completion_mtime = completion_path.stat().st_mtime
+ needs_update = any(
+ cmd_file.stat().st_mtime > completion_mtime
+ for cmd_file in command_files
+ if cmd_file.exists()
+ )
+
+ if not needs_update:
+ click.echo("โ Shell completion is already up to date")
+ return
+ else:
+ click.echo("โ ๏ธ Shell completion script not found")
+
+ click.echo("๐ Regenerating shell completion...")
+
+ try:
+ # Import here to avoid circular imports
+ sys.path.insert(0, str(project_root))
+ from commands.main import cli
+ from commands.utils.completion import (
+ generate_completion_script,
+ get_command_info,
+ )
+
+ # Generate completion script
+ cli_info = get_command_info(cli)
+ completion_script = generate_completion_script(cli_info)
+
+ # Add completion loaded marker
+ completion_script = completion_script.replace(
+ "# Auto-generated completion script for ehAyeโข Core CLI",
+ "# Auto-generated completion script for ehAyeโข Core CLI\nexport _ehaye_cli_completions_loaded=1",
+ )
+
+ # Write to file
+ completion_path.write_text(completion_script)
+
+ click.echo(f"โ
Generated {completion_path.relative_to(project_root)}")
+ click.echo(
+ "๐ก Restart your shell or run 'source .venv/bin/activate' to load new completions"
+ )
+
+ except Exception as e:
+ click.echo(f"โ Failed to generate completion: {e}", err=True)
+ sys.exit(1)
diff --git a/commands/subs/dev/format.py b/commands/subs/dev/format.py
new file mode 100644
index 0000000..9c3f2a9
--- /dev/null
+++ b/commands/subs/dev/format.py
@@ -0,0 +1,19 @@
+"""Code formatting command"""
+
+import subprocess
+import sys
+
+import click
+
+
+@click.command()
+@click.option("--check", is_flag=True, help="Check only, don't modify files")
+def format(check: bool) -> None:
+ """Format code with black"""
+ cmd = ["black", "."]
+ if check:
+ cmd.append("--check")
+
+ click.echo(f"Running: {' '.join(cmd)}")
+ result = subprocess.run(cmd)
+ sys.exit(result.returncode)
diff --git a/commands/subs/dev/lint.py b/commands/subs/dev/lint.py
new file mode 100644
index 0000000..13933cc
--- /dev/null
+++ b/commands/subs/dev/lint.py
@@ -0,0 +1,19 @@
+"""Code linting command"""
+
+import subprocess
+import sys
+
+import click
+
+
+@click.command()
+@click.option("--fix", is_flag=True, help="Fix issues automatically")
+def lint(fix: bool) -> None:
+ """Lint code with ruff"""
+ cmd = ["ruff", "check", "."]
+ if fix:
+ cmd.append("--fix")
+
+ click.echo(f"Running: {' '.join(cmd)}")
+ result = subprocess.run(cmd)
+ sys.exit(result.returncode)
diff --git a/commands/subs/dev/precommit.py b/commands/subs/dev/precommit.py
new file mode 100644
index 0000000..7d9cf49
--- /dev/null
+++ b/commands/subs/dev/precommit.py
@@ -0,0 +1,155 @@
+"""Pre-commit checks command"""
+
+import subprocess
+import sys
+from pathlib import Path
+
+import click
+
+
+@click.command()
+@click.option("--fix", is_flag=True, help="Automatically fix issues where possible")
+@click.option(
+ "--ci", is_flag=True, help="Run in CI mode (check all files, not just staged)"
+)
+def precommit(fix: bool, ci: bool) -> None:
+ """Run all pre-commit checks locally
+
+ This runs the same checks as the actual pre-commit hooks:
+ - Black formatting (always applied to ensure consistency)
+ - Ruff linting (--fix to auto-fix)
+ - MyPy type checking
+ - Rust formatting and checks
+ - Tests (when --ci flag is used)
+
+ By default, runs on staged files only. Use --ci to check ALL files like CI does.
+ Use --fix to automatically fix Ruff issues (Black always formats).
+ """
+ project_root = Path(__file__).parent.parent.parent
+
+ # Track if any changes were made
+ any_changes = False
+ any_failures = False
+
+ if ci:
+ click.echo("๐ Running CI checks (all files)...\n")
+ else:
+ click.echo("๐ Running pre-commit checks...\n")
+
+ # 1. Always format with Black first (to ensure consistent formatting)
+ click.echo("๐ Formatting with Black...")
+ black_cmd = ["black", "."]
+ result = subprocess.run(black_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ click.echo(" โ Black formatting failed")
+ if result.stdout:
+ click.echo(result.stdout)
+ if result.stderr:
+ click.echo(result.stderr)
+ any_failures = True
+ else:
+ if "reformatted" in result.stdout:
+ click.echo(" โ Black reformatted files")
+ any_changes = True
+ else:
+ click.echo(" โ Black: all files already formatted")
+
+ # 2. Ruff linting
+ click.echo("\n๐ Running Ruff linter...")
+ ruff_cmd = ["ruff", "check", "."]
+ if fix:
+ ruff_cmd.append("--fix")
+ result = subprocess.run(ruff_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ if fix and "fixed" in result.stdout:
+ click.echo(" โ Ruff fixed issues")
+ any_changes = True
+ else:
+ click.echo(
+ " โ Ruff found issues" + (" (run with --fix)" if not fix else "")
+ )
+ if result.stdout:
+ click.echo(result.stdout)
+ any_failures = True
+ else:
+ click.echo(" โ Ruff: all good")
+
+ # 3. MyPy type checking (on specific directories)
+ click.echo("\n๐ Running MyPy type checker...")
+ mypy_cmd = ["mypy", "commands", "--config-file", "pyproject.toml"]
+ result = subprocess.run(mypy_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ click.echo(" โ MyPy found type errors")
+ if result.stdout:
+ click.echo(result.stdout)
+ any_failures = True
+ else:
+ click.echo(" โ MyPy: all good")
+
+ # 4. Run tests if in CI mode
+ if ci:
+ click.echo("\n๐งช Running tests...")
+ test_cmd = ["pytest", "commands/tests/"]
+ result = subprocess.run(test_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ click.echo(" โ Tests failed")
+ if result.stdout:
+ click.echo(result.stdout)
+ if result.stderr:
+ click.echo(result.stderr)
+ any_failures = True
+ else:
+ click.echo(" โ Tests: all passed")
+
+ # 5. Check if there are Rust files changed
+ rust_files = list(project_root.glob("rust/**/*.rs"))
+ if rust_files:
+ # 4a. Rust formatting
+ click.echo("\n๐ฆ Running Rust formatter...")
+ rust_fmt_cmd = [sys.executable, "-m", "commands", "rust", "all"]
+ if fix:
+ rust_fmt_cmd.append("--fix")
+ result = subprocess.run(rust_fmt_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ click.echo(" โ Rust format failed")
+ if result.stdout:
+ click.echo(result.stdout)
+ any_failures = True
+ else:
+ if fix and "Formatted" in result.stdout:
+ click.echo(" โ Rust files formatted")
+ any_changes = True
+ else:
+ click.echo(" โ Rust format: all good")
+
+ # 4b. Rust check
+ click.echo("\n๐ฆ Running Rust check...")
+ rust_check_cmd = [sys.executable, "-m", "commands", "rust", "check"]
+ result = subprocess.run(rust_check_cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ click.echo(" โ Rust check failed")
+ if result.stdout:
+ click.echo(result.stdout)
+ any_failures = True
+ else:
+ click.echo(" โ Rust check: all good")
+
+ # Summary
+ click.echo("\n" + "=" * 60)
+ if any_failures:
+ click.echo("โ Pre-commit checks failed!")
+ if not fix:
+ click.echo(
+ "\n๐ก Tip: Run 'cli dev precommit --fix' to automatically fix issues"
+ )
+ sys.exit(1)
+ elif any_changes:
+ click.echo("โ
Pre-commit checks passed (with fixes applied)")
+ click.echo(
+ "\nโ ๏ธ Files were modified. Remember to stage changes before committing:"
+ )
+ click.echo(" git add -A")
+ click.echo(" git commit -m 'Your message'")
+ else:
+ click.echo("โ
All pre-commit checks passed!")
+ click.echo("\nYou're ready to commit! ๐")
diff --git a/commands/subs/dev/test.py b/commands/subs/dev/test.py
new file mode 100644
index 0000000..1d88848
--- /dev/null
+++ b/commands/subs/dev/test.py
@@ -0,0 +1,15 @@
+"""Testing command"""
+
+import subprocess
+import sys
+
+import click
+
+
+@click.command()
+def test() -> None:
+ """Run pytest"""
+ cmd = ["pytest", "-v"]
+ click.echo(f"Running: {' '.join(cmd)}")
+ result = subprocess.run(cmd)
+ sys.exit(result.returncode)
diff --git a/commands/subs/dev/test_cmd_dev.py b/commands/subs/dev/test_cmd_dev.py
new file mode 100644
index 0000000..922d654
--- /dev/null
+++ b/commands/subs/dev/test_cmd_dev.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""Tests for dev commands"""
+
+import subprocess
+
+import pytest
+
+
+def run_cli_command(args: str) -> tuple[int, str, str]:
+ """Run a CLI command and return exit code, stdout, stderr"""
+ cmd = f"cli {args}"
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ return result.returncode, result.stdout, result.stderr
+
+
+class TestDevCommands:
+ """Test suite for dev commands"""
+
+ def test_dev_help(self) -> None:
+ """Test that dev help works"""
+ code, stdout, stderr = run_cli_command("dev --help")
+
+ assert code == 0
+ assert "Development tools" in stdout
+ assert "format" in stdout
+ assert "lint" in stdout
+ assert "typecheck" in stdout
+ assert "test" in stdout
+ assert "precommit" in stdout
+
+ def test_dev_format_help(self) -> None:
+ """Test dev format help"""
+ code, stdout, stderr = run_cli_command("dev format --help")
+
+ assert code == 0
+ assert "Format code with black" in stdout
+ assert "--check" in stdout
+
+ def test_dev_lint_help(self) -> None:
+ """Test dev lint help"""
+ code, stdout, stderr = run_cli_command("dev lint --help")
+
+ assert code == 0
+ assert "Lint code with ruff" in stdout
+ assert "--fix" in stdout
+
+ def test_dev_typecheck_help(self) -> None:
+ """Test dev typecheck help"""
+ code, stdout, stderr = run_cli_command("dev typecheck --help")
+
+ assert code == 0
+ assert "Type check with mypy" in stdout
+
+ def test_dev_test_help(self) -> None:
+ """Test dev test help"""
+ code, stdout, stderr = run_cli_command("dev test --help")
+
+ assert code == 0
+ assert "Run pytest" in stdout
+
+ def test_dev_precommit_help(self) -> None:
+ """Test dev precommit help"""
+ code, stdout, stderr = run_cli_command("dev precommit --help")
+
+ assert code == 0
+ assert "pre-commit checks" in stdout
+ assert "--fix" in stdout
+
+ def test_dev_completion_help(self) -> None:
+ """Test dev completion help"""
+ code, stdout, stderr = run_cli_command("dev completion --help")
+
+ assert code == 0
+ assert "Shell completion management" in stdout
+
+ def test_dev_completion_test_help(self) -> None:
+ """Test dev completion test help"""
+ code, stdout, stderr = run_cli_command("dev completion test --help")
+
+ assert code == 0
+ assert "Test shell completion" in stdout
+
+ def test_dev_completion_sync_help(self) -> None:
+ """Test dev completion sync help"""
+ code, stdout, stderr = run_cli_command("dev completion sync --help")
+
+ assert code == 0
+ assert "Sync shell completion" in stdout
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/commands/subs/dev/typecheck.py b/commands/subs/dev/typecheck.py
new file mode 100644
index 0000000..0336bdb
--- /dev/null
+++ b/commands/subs/dev/typecheck.py
@@ -0,0 +1,16 @@
+"""Type checking command"""
+
+import subprocess
+import sys
+
+import click
+
+
+@click.command()
+def typecheck() -> None:
+ """Type check with mypy"""
+ # Only check commands directory (tools may not have Python files)
+ cmd = ["mypy", "commands"]
+ click.echo(f"Running: {' '.join(cmd)}")
+ result = subprocess.run(cmd)
+ sys.exit(result.returncode)
diff --git a/commands/subs/package/__init__.py b/commands/subs/package/__init__.py
new file mode 100644
index 0000000..6e625e4
--- /dev/null
+++ b/commands/subs/package/__init__.py
@@ -0,0 +1,172 @@
+"""Package commands - stubbed for future implementation"""
+
+from typing import Optional
+
+import click
+
+__all__ = ["package"]
+
+
+@click.group()
+def package() -> None:
+ """Package commands (placeholder for packaging)"""
+ pass
+
+
+@package.command()
+@click.option(
+ "--format",
+ type=click.Choice(
+ ["wheel", "sdist", "tar", "zip", "deb", "rpm"], case_sensitive=False
+ ),
+ default="wheel",
+ help="Package format",
+)
+@click.option("--output", "-o", help="Output directory for packages")
+@click.option("--name", help="Package name (defaults to project name)")
+@click.option("--version", help="Package version")
+@click.option("--include-deps", is_flag=True, help="Include dependencies in package")
+@click.option("--sign", is_flag=True, help="Sign the package")
+@click.option("--dry-run", is_flag=True, help="Show what would be packaged")
+def build(
+ format: str,
+ output: Optional[str],
+ name: Optional[str],
+ version: Optional[str],
+ include_deps: bool,
+ sign: bool,
+ dry_run: bool,
+) -> None:
+ """Build a package"""
+ click.echo("Package build: Not yet implemented")
+
+ click.echo(f" Format: {format}")
+ if output:
+ click.echo(f" Output directory: {output}")
+ if name:
+ click.echo(f" Package name: {name}")
+ if version:
+ click.echo(f" Version: {version}")
+ if include_deps:
+ click.echo(" Include dependencies: enabled")
+ if sign:
+ click.echo(" Sign package: enabled")
+ if dry_run:
+ click.echo(" Dry-run mode: enabled")
+
+ click.echo("\nThis is a placeholder for package building")
+
+
+@package.command()
+@click.option(
+ "--format",
+ type=click.Choice(["wheel", "sdist", "all"], case_sensitive=False),
+ help="Distribution format",
+)
+@click.option("--upload-url", help="Repository URL (defaults to PyPI)")
+@click.option("--username", "-u", help="Username for authentication")
+@click.option("--password", "-p", help="Password or token")
+@click.option("--skip-existing", is_flag=True, help="Skip if version already exists")
+@click.option("--verify", is_flag=True, help="Verify package after upload")
+@click.option("--dry-run", is_flag=True, help="Show what would be uploaded")
+def dist(
+ format: Optional[str],
+ upload_url: Optional[str],
+ username: Optional[str],
+ password: Optional[str],
+ skip_existing: bool,
+ verify: bool,
+ dry_run: bool,
+) -> None:
+ """Create and distribute packages"""
+ click.echo("Package dist: Not yet implemented")
+
+ if format:
+ click.echo(f" Format: {format}")
+ if upload_url:
+ click.echo(f" Upload URL: {upload_url}")
+ if username:
+ click.echo(f" Username: {username}")
+ if password:
+ click.echo(" Password: ***hidden***")
+ if skip_existing:
+ click.echo(" Skip existing: enabled")
+ if verify:
+ click.echo(" Verify after upload: enabled")
+ if dry_run:
+ click.echo(" Dry-run mode: enabled")
+
+ click.echo("\nThis is a placeholder for package distribution")
+
+
+@package.command()
+@click.option("--local", is_flag=True, help="List local packages only")
+@click.option("--remote", is_flag=True, help="List remote packages only")
+@click.option("--outdated", is_flag=True, help="Show only outdated packages")
+@click.option(
+ "--format",
+ type=click.Choice(["table", "json", "yaml"], case_sensitive=False),
+ default="table",
+ help="Output format",
+)
+def list(local: bool, remote: bool, outdated: bool, format: str) -> None:
+ """List packages"""
+ click.echo("Package list: Not yet implemented")
+
+ if local:
+ click.echo(" Showing local packages")
+ elif remote:
+ click.echo(" Showing remote packages")
+ else:
+ click.echo(" Showing all packages")
+
+ if outdated:
+ click.echo(" Filter: outdated only")
+ click.echo(f" Output format: {format}")
+
+ click.echo("\nThis is a placeholder for listing packages")
+
+
+@package.command()
+@click.argument("package_file")
+@click.option("--verbose", "-v", is_flag=True, help="Show detailed verification")
+@click.option("--check-signature", is_flag=True, help="Verify package signature")
+@click.option("--check-deps", is_flag=True, help="Verify all dependencies")
+def verify(
+ package_file: str, verbose: bool, check_signature: bool, check_deps: bool
+) -> None:
+ """Verify a package"""
+ click.echo(f"Package verify '{package_file}': Not yet implemented")
+
+ if verbose:
+ click.echo(" Verbose mode: enabled")
+ if check_signature:
+ click.echo(" Check signature: enabled")
+ if check_deps:
+ click.echo(" Check dependencies: enabled")
+
+ click.echo("\nThis is a placeholder for package verification")
+
+
+@package.command()
+@click.option("--all", is_flag=True, help="Clean all package artifacts")
+@click.option("--dist", is_flag=True, help="Clean dist directory")
+@click.option("--build", "clean_build", is_flag=True, help="Clean build directory")
+@click.option("--cache", is_flag=True, help="Clean package cache")
+@click.option("--force", is_flag=True, help="Force clean without confirmation")
+def clean(all: bool, dist: bool, clean_build: bool, cache: bool, force: bool) -> None:
+ """Clean package artifacts"""
+ click.echo("Package clean: Not yet implemented")
+
+ if all:
+ click.echo(" Clean all: enabled")
+ if dist:
+ click.echo(" Clean dist: enabled")
+ if clean_build:
+ click.echo(" Clean build: enabled")
+ if cache:
+ click.echo(" Clean cache: enabled")
+ if force:
+ click.echo(" Force clean: enabled")
+
+ click.echo("\nThis is a placeholder for cleaning package artifacts")
diff --git a/commands/subs/proj.py b/commands/subs/proj.py
deleted file mode 100644
index 7e0bd21..0000000
--- a/commands/subs/proj.py
+++ /dev/null
@@ -1,175 +0,0 @@
-"""Project-related CLI commands"""
-
-import argparse
-import os
-import subprocess
-from pathlib import Path
-from typing import TYPE_CHECKING, Any, Union
-
-if TYPE_CHECKING:
- pass
-
-
-class ProjectCommands:
- """Commands for project management and information"""
-
- def __init__(self, project_root: Path):
- self.project_root = project_root
-
- def get_repo_size(self) -> str:
- """Get the size of the repository"""
- try:
- # Use du command to get directory size
- # -s: summarize, -h: human readable
- result = subprocess.run(
- ["du", "-sh", str(self.project_root)],
- capture_output=True,
- text=True,
- check=True,
- )
-
- # Output format is "size\tpath", we want just the size
- size = result.stdout.strip().split("\t")[0]
- return size
-
- except subprocess.CalledProcessError as e:
- return f"Error getting repository size: {e}"
-
- def get_git_info(self) -> dict[str, Union[str, bool]]:
- """Get basic git repository information"""
- info: dict[str, Union[str, bool]] = {}
-
- try:
- # Get current branch
- result = subprocess.run(
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
- capture_output=True,
- text=True,
- check=True,
- cwd=self.project_root,
- )
- info["branch"] = result.stdout.strip()
-
- # Get number of commits
- result = subprocess.run(
- ["git", "rev-list", "--count", "HEAD"],
- capture_output=True,
- text=True,
- check=True,
- cwd=self.project_root,
- )
- info["commits"] = result.stdout.strip()
-
- # Check for uncommitted changes
- result = subprocess.run(
- ["git", "status", "--porcelain"],
- capture_output=True,
- text=True,
- check=True,
- cwd=self.project_root,
- )
- info["has_changes"] = bool(result.stdout.strip())
-
- except subprocess.CalledProcessError:
- info["error"] = "Not a git repository or git not available"
-
- return info
-
- def get_stats(self) -> dict[str, Union[str, int, list[tuple[str, int]]]]:
- """Get detailed repository statistics"""
- stats: dict[str, Union[str, int, list[tuple[str, int]]]] = {}
-
- try:
- # Count files by extension
- file_counts: dict[str, int] = {}
- total_files = 0
- total_lines = 0
-
- # Walk through all files
- for root, dirs, files in os.walk(self.project_root):
- # Skip hidden directories and common ignore patterns
- dirs[:] = [
- d
- for d in dirs
- if not d.startswith(".")
- and d not in ["node_modules", "__pycache__"]
- ]
-
- for file in files:
- if file.startswith("."):
- continue
-
- total_files += 1
- ext = Path(file).suffix or "no extension"
- file_counts[ext] = file_counts.get(ext, 0) + 1
-
- # Try to count lines for text files
- if ext in [
- ".py",
- ".js",
- ".ts",
- ".rs",
- ".go",
- ".c",
- ".cpp",
- ".h",
- ".java",
- ".rb",
- ".sh",
- ".md",
- ".txt",
- ]:
- try:
- file_path = os.path.join(root, file)
- with open(
- file_path, encoding="utf-8", errors="ignore"
- ) as f:
- lines = len(f.readlines())
- total_lines += lines
- except Exception:
- pass
-
- stats["total_files"] = total_files
- stats["total_lines"] = total_lines
- stats["file_types"] = sorted(
- file_counts.items(), key=lambda x: x[1], reverse=True
- )[
- :10
- ] # Top 10
-
- # Get directory count
- dir_count = sum(
- 1
- for _, dirs, _ in os.walk(self.project_root)
- for d in dirs
- if not d.startswith(".")
- )
- stats["total_directories"] = dir_count
-
- except Exception as e:
- stats["error"] = f"Error gathering statistics: {e}"
-
- return stats
-
- @staticmethod
- def add_subparser(
- subparsers: "argparse._SubParsersAction[Any]",
- ) -> argparse.ArgumentParser:
- """Add project subcommands to argument parser"""
- proj_parser = subparsers.add_parser("proj", help="Project management commands")
-
- # Add flags (not subcommands) for different operations
- proj_parser.add_argument(
- "-s", "--size", action="store_true", help="Show repository size"
- )
- proj_parser.add_argument(
- "-i", "--info", action="store_true", help="Show project information"
- )
- # Example: --super-fast has no short form because -s is taken by --size
- proj_parser.add_argument(
- "--stats",
- action="store_true",
- help="Show detailed statistics (no short form, -s taken by --size)",
- )
-
- return proj_parser # type: ignore[no-any-return]
diff --git a/commands/subs/proj/__init__.py b/commands/subs/proj/__init__.py
new file mode 100644
index 0000000..e39e44c
--- /dev/null
+++ b/commands/subs/proj/__init__.py
@@ -0,0 +1,19 @@
+"""Project management router"""
+
+import click
+
+from commands.subs.proj.info import info
+from commands.subs.proj.size import size
+from commands.subs.proj.stats import stats
+
+
+@click.group()
+def proj() -> None:
+ """Project management commands"""
+ pass
+
+
+# Add subcommands
+proj.add_command(info)
+proj.add_command(size)
+proj.add_command(stats)
diff --git a/commands/subs/proj/info.py b/commands/subs/proj/info.py
new file mode 100644
index 0000000..96188d3
--- /dev/null
+++ b/commands/subs/proj/info.py
@@ -0,0 +1,53 @@
+"""Project information command"""
+
+import subprocess
+from pathlib import Path
+
+import click
+
+
+@click.command()
+def info() -> None:
+ """Show project information"""
+ project_root = Path(__file__).parent.parent.parent.parent
+
+ click.echo(f"Project root: {project_root}")
+
+ # Get git info
+ try:
+ # Get current branch
+ result = subprocess.run(
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=True,
+ cwd=project_root,
+ )
+ branch = result.stdout.strip()
+
+ # Get number of commits
+ result = subprocess.run(
+ ["git", "rev-list", "--count", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=True,
+ cwd=project_root,
+ )
+ commits = result.stdout.strip()
+
+ # Check for uncommitted changes
+ result = subprocess.run(
+ ["git", "status", "--porcelain"],
+ capture_output=True,
+ text=True,
+ check=True,
+ cwd=project_root,
+ )
+ has_changes = bool(result.stdout.strip())
+
+ click.echo(f"Git branch: {branch}")
+ click.echo(f"Total commits: {commits}")
+ click.echo(f"Uncommitted changes: {'Yes' if has_changes else 'No'}")
+
+ except subprocess.CalledProcessError:
+ click.echo("Not a git repository or git not available", err=True)
diff --git a/commands/subs/proj/size.py b/commands/subs/proj/size.py
new file mode 100644
index 0000000..cf155e1
--- /dev/null
+++ b/commands/subs/proj/size.py
@@ -0,0 +1,29 @@
+"""Repository size command"""
+
+import subprocess
+from pathlib import Path
+
+import click
+
+
+@click.command()
+def size() -> None:
+ """Show repository size"""
+ project_root = Path(__file__).parent.parent.parent.parent
+
+ try:
+ # Use du command to get directory size
+ # -s: summarize, -h: human readable
+ result = subprocess.run(
+ ["du", "-sh", str(project_root)],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+
+ # Output format is "size\tpath", we want just the size
+ size_value = result.stdout.strip().split("\t")[0]
+ click.echo(f"Repository size: {size_value}")
+
+ except subprocess.CalledProcessError as e:
+ click.echo(f"Error getting repository size: {e}", err=True)
diff --git a/commands/subs/proj/stats.py b/commands/subs/proj/stats.py
new file mode 100644
index 0000000..278b211
--- /dev/null
+++ b/commands/subs/proj/stats.py
@@ -0,0 +1,108 @@
+"""Project statistics command"""
+
+import os
+from pathlib import Path
+
+import click
+
+
+@click.command()
+def stats() -> None:
+ """Show detailed statistics"""
+ project_root = Path(__file__).parent.parent.parent.parent
+
+ try:
+ # Count files by extension
+ file_counts: dict[str, int] = {}
+ total_files = 0
+ total_lines = 0
+
+ # Walk through all files
+ for root, dirs, files in os.walk(project_root):
+ # Skip hidden directories and common ignore patterns
+ dirs[:] = [
+ d
+ for d in dirs
+ if not d.startswith(".")
+ and d
+ not in [
+ "node_modules",
+ "__pycache__",
+ "target",
+ "dist",
+ "cache",
+ "release",
+ ".venv",
+ ]
+ ]
+
+ for file in files:
+ if file.startswith("."):
+ continue
+
+ total_files += 1
+ ext = Path(file).suffix or "no extension"
+ file_counts[ext] = file_counts.get(ext, 0) + 1
+
+ # Try to count lines for text files
+ if ext in [
+ ".py",
+ ".js",
+ ".ts",
+ ".rs",
+ ".go",
+ ".c",
+ ".cpp",
+ ".h",
+ ".java",
+ ".rb",
+ ".sh",
+ ".md",
+ ".txt",
+ ".toml",
+ ".yaml",
+ ".yml",
+ ".json",
+ ]:
+ try:
+ file_path = os.path.join(root, file)
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
+ lines = len(f.readlines())
+ total_lines += lines
+ except Exception:
+ pass
+
+ # Get directory count
+ dir_count = sum(
+ 1
+ for _, dirs, _ in os.walk(project_root)
+ for d in dirs
+ if not d.startswith(".")
+ and d
+ not in [
+ "node_modules",
+ "__pycache__",
+ "target",
+ "dist",
+ "cache",
+ "release",
+ ".venv",
+ ]
+ )
+
+ # Show results
+ click.echo(f"Total files: {total_files}")
+ click.echo(f"Total directories: {dir_count}")
+ click.echo(f"Total lines of code: {total_lines:,}")
+ click.echo("\nTop 10 file types:")
+
+ # Sort and show top 10 file types
+ sorted_types = sorted(file_counts.items(), key=lambda x: x[1], reverse=True)[
+ :10
+ ]
+
+ for ext, count in sorted_types:
+ click.echo(f" {ext}: {count}")
+
+ except Exception as e:
+ click.echo(f"Error gathering statistics: {e}", err=True)
diff --git a/commands/subs/release/__init__.py b/commands/subs/release/__init__.py
new file mode 100644
index 0000000..9c49b1d
--- /dev/null
+++ b/commands/subs/release/__init__.py
@@ -0,0 +1,147 @@
+"""Release commands - stubbed for future implementation"""
+
+from typing import Optional
+
+import click
+
+__all__ = ["release"]
+
+
+@click.group()
+def release() -> None:
+ """Release commands (placeholder for releases)"""
+ pass
+
+
+@release.command()
+@click.option("--version", help="Version number (e.g., 1.0.0)")
+@click.option(
+ "--target",
+ type=click.Choice(["linux", "darwin", "windows", "all"], case_sensitive=False),
+ default="all",
+ help="Target platform(s)",
+)
+@click.option(
+ "--arch",
+ type=click.Choice(["x86_64", "arm64", "aarch64", "all"], case_sensitive=False),
+ default="all",
+ help="Target architecture(s)",
+)
+@click.option("--tag", help="Git tag to create for this release")
+@click.option("--draft", is_flag=True, help="Create as draft release")
+@click.option("--prerelease", is_flag=True, help="Mark as pre-release")
+@click.option("--notes", help="Release notes or changelog")
+@click.option(
+ "--dry-run", is_flag=True, help="Show what would be done without doing it"
+)
+def create(
+ version: Optional[str],
+ target: str,
+ arch: str,
+ tag: Optional[str],
+ draft: bool,
+ prerelease: bool,
+ notes: Optional[str],
+ dry_run: bool,
+) -> None:
+ """Create a release"""
+ click.echo("Release create: Not yet implemented")
+
+ # Show configuration as reference
+ if version:
+ click.echo(f" Version: {version}")
+ click.echo(f" Target: {target}")
+ click.echo(f" Architecture: {arch}")
+ if tag:
+ click.echo(f" Git tag: {tag}")
+ if draft:
+ click.echo(" Draft release: enabled")
+ if prerelease:
+ click.echo(" Pre-release: enabled")
+ if notes:
+ click.echo(f" Release notes: {notes[:50]}...")
+ if dry_run:
+ click.echo(" Dry-run mode: enabled")
+
+ click.echo("\nThis is a placeholder for creating releases")
+
+
+@release.command()
+@click.option("--version", help="Version to publish")
+@click.option(
+ "--target",
+ type=click.Choice(["pypi", "github", "npm", "docker", "all"], case_sensitive=False),
+ default="pypi",
+ help="Publishing target",
+)
+@click.option("--token", help="Authentication token for publishing")
+@click.option("--skip-tests", is_flag=True, help="Skip running tests before publish")
+@click.option("--skip-build", is_flag=True, help="Skip building before publish")
+@click.option("--force", is_flag=True, help="Force publish even if version exists")
+@click.option("--dry-run", is_flag=True, help="Show what would be published")
+def publish(
+ version: Optional[str],
+ target: str,
+ token: Optional[str],
+ skip_tests: bool,
+ skip_build: bool,
+ force: bool,
+ dry_run: bool,
+) -> None:
+ """Publish a release"""
+ click.echo("Release publish: Not yet implemented")
+
+ if version:
+ click.echo(f" Version: {version}")
+ click.echo(f" Target: {target}")
+ if token:
+ click.echo(" Token: ***hidden***")
+ if skip_tests:
+ click.echo(" Skip tests: enabled")
+ if skip_build:
+ click.echo(" Skip build: enabled")
+ if force:
+ click.echo(" Force publish: enabled")
+ if dry_run:
+ click.echo(" Dry-run mode: enabled")
+
+ click.echo("\nThis is a placeholder for publishing releases")
+
+
+@release.command()
+@click.option("--remote", default="origin", help="Git remote name")
+@click.option("--branch", help="Branch to list releases from")
+@click.option("--limit", type=int, default=10, help="Number of releases to show")
+@click.option("--all", "show_all", is_flag=True, help="Show all releases")
+def list(remote: str, branch: Optional[str], limit: int, show_all: bool) -> None:
+ """List releases"""
+ click.echo("Release list: Not yet implemented")
+
+ click.echo(f" Remote: {remote}")
+ if branch:
+ click.echo(f" Branch: {branch}")
+ if show_all:
+ click.echo(" Showing all releases")
+ else:
+ click.echo(f" Limit: {limit}")
+
+ click.echo("\nThis is a placeholder for listing releases")
+
+
+@release.command()
+@click.argument("version")
+@click.option("--force", is_flag=True, help="Force deletion")
+@click.option("--keep-tag", is_flag=True, help="Keep git tag when deleting release")
+@click.option("--dry-run", is_flag=True, help="Show what would be deleted")
+def delete(version: str, force: bool, keep_tag: bool, dry_run: bool) -> None:
+ """Delete a release"""
+ click.echo(f"Release delete '{version}': Not yet implemented")
+
+ if force:
+ click.echo(" Force delete: enabled")
+ if keep_tag:
+ click.echo(" Keep git tag: enabled")
+ if dry_run:
+ click.echo(" Dry-run mode: enabled")
+
+ click.echo("\nThis is a placeholder for deleting releases")
diff --git a/commands/tests/__init__.py b/commands/tests/__init__.py
new file mode 100644
index 0000000..9be4a55
--- /dev/null
+++ b/commands/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests for CLI commands"""
diff --git a/commands/tests/test_all_commands.py b/commands/tests/test_all_commands.py
new file mode 100755
index 0000000..11d0dc4
--- /dev/null
+++ b/commands/tests/test_all_commands.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+"""Test all CLI commands to ensure they work correctly.
+
+This script tests all CLI commands (except expensive model operations).
+It can be run with actual tests or in dry-run mode.
+"""
+
+import subprocess
+import sys
+from typing import Optional
+
+
+class Colors:
+ """ANSI color codes for terminal output."""
+
+ GREEN = "\033[92m"
+ RED = "\033[91m"
+ YELLOW = "\033[93m"
+ BLUE = "\033[94m"
+ ENDC = "\033[0m"
+ BOLD = "\033[1m"
+
+
+def run_command(cmd: str, dry_run: bool = False) -> tuple[bool, str]:
+ """Run a command and return success status and output."""
+ if dry_run:
+ print(f"{Colors.BLUE}[DRY-RUN] Would execute:{Colors.ENDC} {cmd}")
+ return True, "Dry run - command not executed"
+
+ try:
+ # Ensure venv is activated
+ activate_cmd = "source .venv/bin/activate && "
+ full_cmd = activate_cmd + cmd
+
+ result = subprocess.run(
+ full_cmd, shell=True, capture_output=True, text=True, timeout=30
+ )
+
+ output = result.stdout + result.stderr
+ success = result.returncode == 0
+
+ if not success and "No such option: --dry-run" in output:
+ # Try command without --dry-run if it's not supported
+ cmd_without_dry = cmd.replace(" --dry-run", "").replace(" --dry", "")
+ if cmd_without_dry != cmd:
+ print(
+ f"{Colors.YELLOW} Command doesn't support --dry-run, running without it{Colors.ENDC}"
+ )
+ return run_command(cmd_without_dry, dry_run=False)
+
+ return success, output
+
+ except subprocess.TimeoutExpired:
+ return False, "Command timed out after 30 seconds"
+ except Exception as e:
+ return False, f"Error running command: {str(e)}"
+
+
+def check_command(
+ name: str, cmd: str, dry_run: bool = False, skip_reason: Optional[str] = None
+) -> bool:
+ """Test a single command and report results."""
+ print(f"\n{Colors.BOLD}Testing: {name}{Colors.ENDC}")
+ print(f"Command: {cmd}")
+
+ if skip_reason:
+ print(f"{Colors.YELLOW}SKIPPED:{Colors.ENDC} {skip_reason}")
+ return True
+
+ success, output = run_command(cmd, dry_run)
+
+ if success:
+ print(f"{Colors.GREEN}โ PASSED{Colors.ENDC}")
+ if "--help" not in cmd and not dry_run:
+ # Show first few lines of output for non-help commands
+ lines = output.strip().split("\n")[:3]
+ for line in lines:
+ print(f" {line}")
+ if len(output.strip().split("\n")) > 3:
+ print(" ...")
+ else:
+ print(f"{Colors.RED}โ FAILED{Colors.ENDC}")
+ print(f"Output: {output[:500]}...")
+
+ return success
+
+
+def main() -> None:
+ """Run all command tests."""
+ dry_run = "--dry-run" in sys.argv or "--dry" in sys.argv
+
+ print(f"{Colors.BOLD}{'='*60}{Colors.ENDC}")
+ print(f"{Colors.BOLD}Testing ehAyeโข Core CLI Commands{Colors.ENDC}")
+ print(f"{Colors.BOLD}{'='*60}{Colors.ENDC}")
+ print(f"Mode: {'DRY RUN' if dry_run else 'ACTUAL EXECUTION'}")
+
+ # Define all commands to test
+ # Format: (test_name, command, skip_reason)
+ commands = [
+ # Basic commands
+ ("Version", "cli version", None),
+ ("Help", "cli --help", None),
+ # Build commands
+ ("Build Help", "cli build --help", None),
+ ("Build All Help", "cli build all --help", None),
+ ("Build Clean Help", "cli build clean --help", None),
+ # Dev commands
+ ("Dev Help", "cli dev --help", None),
+ ("Dev Format Check", "cli dev format --check", None),
+ ("Dev Lint", "cli dev lint", None),
+ ("Dev Typecheck", "cli dev typecheck", None),
+ ("Dev Test", "cli dev test", None),
+ ("Dev All", "cli dev all", None),
+ # Package commands
+ ("Package Help", "cli package --help", None),
+ ("Package Build Help", "cli package build --help", None),
+ ("Package Dist Help", "cli package dist --help", None),
+ # Project commands
+ ("Proj Help", "cli proj --help", None),
+ ("Proj Info", "cli proj info", None),
+ ("Proj Size", "cli proj size", None),
+ ("Proj Stats", "cli proj stats", None),
+ # Release commands
+ ("Release Help", "cli release --help", None),
+ ("Release Create Help", "cli release create --help", None),
+ ("Release Publish Help", "cli release publish --help", None),
+ ]
+
+ # Track results
+ passed = 0
+ failed = 0
+ skipped = 0
+ failed_commands = []
+
+ # Run all tests
+ for test_name, cmd, skip_reason in commands:
+ if check_command(test_name, cmd, dry_run=False, skip_reason=skip_reason):
+ if skip_reason:
+ skipped += 1
+ else:
+ passed += 1
+ else:
+ failed += 1
+ failed_commands.append((test_name, cmd))
+
+ # Summary
+ print(f"\n{Colors.BOLD}{'='*60}{Colors.ENDC}")
+ print(f"{Colors.BOLD}Test Summary{Colors.ENDC}")
+ print(f"{Colors.BOLD}{'='*60}{Colors.ENDC}")
+ print(f"{Colors.GREEN}Passed: {passed}{Colors.ENDC}")
+ print(f"{Colors.RED}Failed: {failed}{Colors.ENDC}")
+ print(f"{Colors.YELLOW}Skipped: {skipped}{Colors.ENDC}")
+
+ if failed_commands:
+ print(f"\n{Colors.RED}Failed Commands:{Colors.ENDC}")
+ for name, cmd in failed_commands:
+ print(f" - {name}: {cmd}")
+
+ # Exit with appropriate code
+ sys.exit(0 if failed == 0 else 1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/commands/tests/test_cmd_completion.py b/commands/tests/test_cmd_completion.py
new file mode 100644
index 0000000..2eed3cf
--- /dev/null
+++ b/commands/tests/test_cmd_completion.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+"""Test completion functionality"""
+
+import sys
+from pathlib import Path
+
+# Add parent dir to path so we can import our modules
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from commands.main import cli # noqa: E402
+from commands.utils.completion import ( # noqa: E402
+ generate_completion_script,
+ get_command_info,
+)
+
+
+def test_completion_generation() -> None:
+ """Test that completion script can be generated"""
+ # Get CLI info
+ cli_info = get_command_info(cli)
+ assert cli_info is not None
+ assert "name" in cli_info
+ assert "subcommands" in cli_info
+
+ # Generate completion script
+ script = generate_completion_script(cli_info)
+ assert script is not None
+ assert len(script) > 0
+ assert "#!/bin/bash" in script
+ assert "_ehaye_cli_completions" in script
+
+
+def test_command_structure() -> None:
+ """Test that command structure is properly extracted"""
+ cli_info = get_command_info(cli)
+
+ # Check main commands exist
+ expected_commands = {"build", "dev", "package", "proj", "release"}
+ actual_commands = set(cli_info["subcommands"].keys())
+
+ assert expected_commands.issubset(
+ actual_commands
+ ), f"Missing commands: {expected_commands - actual_commands}"
+
+ # Check dev subcommands
+ dev_info = cli_info["subcommands"]["dev"]
+ dev_subcommands = set(dev_info["subcommands"].keys())
+ expected_dev = {"format", "lint", "typecheck", "test", "all", "precommit"}
+
+ assert expected_dev.issubset(
+ dev_subcommands
+ ), f"Missing dev commands: {expected_dev - dev_subcommands}"
+
+
+# For running as a standalone script
+def run_tests() -> bool:
+ """Run tests when called as a script"""
+ success = True
+ try:
+ test_completion_generation()
+ print("โ
Completion generation test passed")
+ except Exception as e:
+ print(f"โ Completion generation test failed: {e}")
+ success = False
+
+ try:
+ test_command_structure()
+ print("โ
Command structure test passed")
+ except Exception as e:
+ print(f"โ Command structure test failed: {e}")
+ success = False
+
+ return success
+
+
+if __name__ == "__main__":
+ if run_tests():
+ print("\nโ
All completion tests passed!")
+ sys.exit(0)
+ else:
+ print("\nโ Some completion tests failed")
+ sys.exit(1)
diff --git a/commands/tests/test_cmd_main.py b/commands/tests/test_cmd_main.py
new file mode 100644
index 0000000..3417591
--- /dev/null
+++ b/commands/tests/test_cmd_main.py
@@ -0,0 +1,37 @@
+"""Basic tests for ehAyeโข Core CLI"""
+
+import subprocess
+import sys
+from pathlib import Path
+
+from click.testing import CliRunner
+
+from commands.main import cli
+
+
+def test_cli_help() -> None:
+ """Test that CLI help works"""
+ runner = CliRunner()
+ result = runner.invoke(cli, ["--help"])
+ assert result.exit_code == 0
+ assert "ehAyeโข Core CLI" in result.output
+
+
+def test_cli_version() -> None:
+ """Test that CLI version command works"""
+ runner = CliRunner()
+ result = runner.invoke(cli, ["version"])
+ assert result.exit_code == 0
+ assert "CLI version:" in result.output
+
+
+def test_python_module_invocation() -> None:
+ """Test that CLI can be invoked as python module"""
+ result = subprocess.run(
+ [sys.executable, "-m", "commands", "--help"],
+ capture_output=True,
+ text=True,
+ cwd=Path(__file__).parent.parent.parent, # Go up to project root
+ )
+ assert result.returncode == 0
+ assert "ehAyeโข Core CLI" in result.stdout
diff --git a/commands/utils/__init__.py b/commands/utils/__init__.py
new file mode 100644
index 0000000..7799068
--- /dev/null
+++ b/commands/utils/__init__.py
@@ -0,0 +1,5 @@
+"""Commands utilities package"""
+
+from .platform import get_current_platform, get_current_target_arch
+
+__all__ = ["get_current_platform", "get_current_target_arch"]
diff --git a/commands/utils/completion.py b/commands/utils/completion.py
new file mode 100644
index 0000000..98678ef
--- /dev/null
+++ b/commands/utils/completion.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+"""Generate shell completion script from CLI structure"""
+
+from typing import Any
+
+import click
+
+
+def get_command_info(cmd: click.Command) -> dict[str, Any]:
+ """Extract command info including options and subcommands"""
+ info: dict[str, Any] = {
+ "name": cmd.name,
+ "help": cmd.help or "",
+ "options": [],
+ "subcommands": {},
+ }
+
+ # Get options
+ for param in cmd.params:
+ if isinstance(param, click.Option):
+ opt_info = {
+ "names": param.opts,
+ "type": (
+ param.type.name if hasattr(param.type, "name") else str(param.type)
+ ),
+ "choices": [],
+ }
+
+ # Get choices if it's a Choice type
+ if isinstance(param.type, click.Choice):
+ opt_info["choices"] = list(param.type.choices)
+
+ info["options"].append(opt_info)
+
+ # Get subcommands if it's a group
+ if isinstance(cmd, click.Group):
+ for name, subcmd in cmd.commands.items():
+ info["subcommands"][name] = get_command_info(subcmd)
+
+ return info
+
+
+def generate_completion_case(cmd_info: dict[str, Any], depth: int = 0) -> str:
+ """Generate a completion case for a command and its subcommands recursively"""
+ indent = " " * (depth + 3)
+ case_content = ""
+
+ # Handle options
+ if cmd_info["options"]:
+ case_content += f'{indent}if [[ "${{prev}}" == --* ]]; then\n'
+ case_content += f'{indent} case "${{prev}}" in\n'
+
+ for opt in cmd_info["options"]:
+ if opt["choices"]:
+ for opt_name in opt["names"]:
+ if opt_name.startswith("--"):
+ choices = " ".join(opt["choices"])
+ case_content += f"{indent} {opt_name})\n"
+ case_content += f'{indent} COMPREPLY=($(compgen -W "{choices}" -- "${{cur}}"))\n'
+ case_content += f"{indent} return 0\n"
+ case_content += f"{indent} ;;\n"
+
+ case_content += f"{indent} esac\n"
+ case_content += f"{indent}fi\n\n"
+
+ # Handle subcommands
+ if cmd_info["subcommands"]:
+ subcommands = " ".join(cmd_info["subcommands"].keys())
+
+ # Generate option list for this level
+ options = []
+ for opt in cmd_info["options"]:
+ options.extend(opt["names"])
+ options_str = " ".join(options) if options else ""
+
+ case_content += f"{indent}# Check for subcommand at this level\n"
+ case_content += f"{indent}local subcmd=''\n"
+ case_content += f"{indent}local subcommands='{subcommands}'\n"
+ case_content += f"{indent}local idx=$((cmd_idx + {depth}))\n"
+ case_content += f"{indent}for ((i=idx+1; i < ${{cword}}; i++)); do\n"
+ case_content += f'{indent} if [[ "${{words[i]}}" != -* ]] && [[ " ${{subcommands}} " == *" ${{words[i]}} "* ]]; then\n'
+ case_content += f'{indent} subcmd="${{words[i]}}"\n'
+ case_content += f"{indent} break\n"
+ case_content += f"{indent} fi\n"
+ case_content += f"{indent}done\n\n"
+
+ case_content += f'{indent}if [[ -n "${{subcmd}}" ]]; then\n'
+ case_content += f'{indent} case "${{subcmd}}" in\n'
+
+ # Recursively handle each subcommand
+ for subcmd_name, subcmd_info in cmd_info["subcommands"].items():
+ case_content += f"{indent} {subcmd_name})\n"
+ subcmd_case = generate_completion_case(subcmd_info, depth + 1)
+ # Add the subcmd case content with proper indentation
+ for line in subcmd_case.splitlines():
+ if line:
+ case_content += f"{indent} {line}\n"
+ case_content += f"{indent} ;;\n"
+
+ case_content += f"{indent} esac\n"
+ case_content += f"{indent}else\n"
+ case_content += (
+ f"{indent} # No subcommand yet, offer subcommands and options\n"
+ )
+ case_content += f'{indent} if [[ "${{cur}}" == -* ]]; then\n'
+ if options_str:
+ # Add logic to filter out already used options
+ case_content += f"{indent} # Filter out already used options\n"
+ case_content += f'{indent} local available_opts=" {options_str} "\n'
+ case_content += f'{indent} for word in "${{words[@]}}"; do\n'
+ case_content += f'{indent} if [[ "$word" == -* ]] && [[ "$word" != "${{cur}}" ]]; then\n'
+ case_content += f'{indent} available_opts="${{available_opts// $word / }}"\n'
+ case_content += f"{indent} fi\n"
+ case_content += f"{indent} done\n"
+ case_content += (
+ f"{indent} # Trim spaces and offer remaining options\n"
+ )
+ case_content += f'{indent} available_opts="${{available_opts## }}"\n'
+ case_content += f'{indent} available_opts="${{available_opts%% }}"\n'
+ case_content += f'{indent} COMPREPLY=($(compgen -W "${{available_opts}}" -- "${{cur}}"))\n'
+ else:
+ case_content += f"{indent} COMPREPLY=()\n"
+ case_content += f"{indent} else\n"
+ case_content += f'{indent} COMPREPLY=($(compgen -W "${{subcommands}}" -- "${{cur}}"))\n'
+ case_content += f"{indent} fi\n"
+ case_content += f"{indent}fi\n"
+ else:
+ # No subcommands, just complete options
+ options = []
+ for opt in cmd_info["options"]:
+ options.extend(opt["names"])
+ if options:
+ options_str = " ".join(options)
+ case_content += f'{indent}if [[ "${{cur}}" == -* ]]; then\n'
+ case_content += f"{indent} # User typed -, show matching options\n"
+ # Add logic to filter out already used options
+ case_content += f"{indent} # Filter out already used options\n"
+ case_content += f'{indent} local available_opts=" {options_str} "\n'
+ case_content += f'{indent} for word in "${{words[@]}}"; do\n'
+ case_content += f'{indent} if [[ "$word" == --* ]] && [[ "$word" != "${{cur}}" ]]; then\n'
+ case_content += (
+ f'{indent} available_opts="${{available_opts// $word / }}"\n'
+ )
+ case_content += f"{indent} fi\n"
+ case_content += f"{indent} done\n"
+ case_content += f"{indent} # Trim spaces and offer remaining options\n"
+ case_content += f'{indent} available_opts="${{available_opts## }}"\n'
+ case_content += f'{indent} available_opts="${{available_opts%% }}"\n'
+ case_content += f'{indent} COMPREPLY=($(compgen -W "${{available_opts}}" -- "${{cur}}"))\n'
+ case_content += f'{indent}elif [[ -z "${{cur}}" ]]; then\n'
+ case_content += f"{indent} # Empty current word, show filtered options\n"
+ # Add logic to filter out already used options
+ case_content += f"{indent} # Filter out already used options\n"
+ case_content += f'{indent} local available_opts=" {options_str} "\n'
+ case_content += f'{indent} for word in "${{words[@]}}"; do\n'
+ case_content += f'{indent} if [[ "$word" == --* ]] && [[ "$word" != "${{cur}}" ]]; then\n'
+ case_content += (
+ f'{indent} available_opts="${{available_opts// $word / }}"\n'
+ )
+ case_content += f"{indent} fi\n"
+ case_content += f"{indent} done\n"
+ case_content += f"{indent} # Trim spaces and offer remaining options\n"
+ case_content += f'{indent} available_opts="${{available_opts## }}"\n'
+ case_content += f'{indent} available_opts="${{available_opts%% }}"\n'
+ case_content += (
+ f'{indent} COMPREPLY=($(compgen -W "${{available_opts}}" -- ""))\n'
+ )
+ case_content += f"{indent}fi\n"
+
+ return case_content.rstrip()
+
+
+def generate_completion_script(cli_info: dict[str, Any]) -> str:
+ """Generate bash/zsh completion script from CLI info"""
+
+ # First, collect all commands and their structures
+ all_commands = list(cli_info["subcommands"].keys())
+
+ script = (
+ '''#!/bin/bash
+# Auto-generated completion script for ehAyeโข Core CLI
+export _ehaye_cli_completions_loaded=1
+
+_ehaye_cli_completions() {
+ local cur prev words cword
+ if [[ -n "$ZSH_VERSION" ]]; then
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+ words=("${COMP_WORDS[@]}")
+ cword=$COMP_CWORD
+ else
+ if type _get_comp_words_by_ref &>/dev/null; then
+ _get_comp_words_by_ref -n : cur prev words cword
+ else
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+ words=("${COMP_WORDS[@]}")
+ cword=$COMP_CWORD
+ fi
+ fi
+
+ # Main commands
+ local commands="'''
+ + " ".join(all_commands)
+ + """"
+
+ if [[ ${cword} -eq 1 ]]; then
+ COMPREPLY=($(compgen -W "${commands}" -- "${cur}"))
+ return 0
+ fi
+
+ # Find the main command
+ local cmd=""
+ local cmd_idx=1
+ for ((i=1; i < ${cword}; i++)); do
+ if [[ "${words[i]}" != -* ]]; then
+ cmd="${words[i]}"
+ cmd_idx=$i
+ break
+ fi
+ done
+
+ # Complete based on command
+ case "${cmd}" in
+"""
+ )
+
+ # Generate cases for each command
+ for cmd_name, cmd_info in cli_info["subcommands"].items():
+ script += f" {cmd_name})\n"
+ # Use the recursive function to generate the case content
+ case_content = generate_completion_case(cmd_info)
+ script += case_content
+ script += "\n ;;\n"
+
+ script += """ *)
+ if [[ "${cur}" == -* ]]; then
+ COMPREPLY=($(compgen -W "--help" -- "${cur}"))
+ fi
+ ;;
+ esac
+}
+
+# Only enable completion for interactive shells
+if [[ $- == *i* ]]; then
+ # For bash
+ if [[ -n "$BASH_VERSION" ]]; then
+ complete -F _ehaye_cli_completions cli
+ fi
+
+ # For zsh
+ if [[ -n "$ZSH_VERSION" ]]; then
+ autoload -U +X bashcompinit && bashcompinit
+ complete -F _ehaye_cli_completions cli
+ fi
+fi
+"""
+
+ return script
+
+
+# This module is meant to be imported, not run directly
+# Use it from commands.main.py in the enable_completion command
diff --git a/commands/utils/log.py b/commands/utils/log.py
new file mode 100644
index 0000000..ae9552d
--- /dev/null
+++ b/commands/utils/log.py
@@ -0,0 +1,8 @@
+def log_info(message: str) -> None:
+ """Log info message with green color"""
+ print(f"\033[0;32m[INFO]\033[0m {message}")
+
+
+def log_error(message: str) -> None:
+ """Log error message with red color"""
+ print(f"\033[0;31m[ERROR]\033[0m {message}")
diff --git a/commands/utils/platform.py b/commands/utils/platform.py
new file mode 100644
index 0000000..331dd59
--- /dev/null
+++ b/commands/utils/platform.py
@@ -0,0 +1,54 @@
+"""Platform detection utilities for commands"""
+
+import platform
+
+
+def get_current_target_arch() -> tuple[str, str]:
+ """Get current system's target and architecture
+
+ Returns:
+ Tuple of (target, arch) where:
+ - target: 'darwin', 'linux', or 'windows'
+ - arch: 'arm64' or 'amd64'
+
+ Example:
+ target, arch = get_current_target_arch()
+ # On macOS ARM64: ('darwin', 'arm64')
+ # On Linux x86_64: ('linux', 'amd64')
+ """
+ # Get system name
+ system = platform.system().lower()
+ if system == "darwin":
+ target = "darwin"
+ elif system == "linux":
+ target = "linux"
+ elif system == "windows":
+ target = "windows"
+ else:
+ # Default to linux for unknown systems
+ target = "linux"
+
+ # Get machine architecture
+ machine = platform.machine().lower()
+ if machine in ("arm64", "aarch64"):
+ arch = "arm64"
+ elif machine in ("x86_64", "amd64", "x64"):
+ arch = "amd64"
+ elif machine in ("i386", "i686", "x86"):
+ # 32-bit systems map to amd64 as closest supported
+ arch = "amd64"
+ else:
+ # Default to arm64 for unknown architectures
+ arch = "arm64"
+
+ return target, arch
+
+
+def get_current_platform() -> str:
+ """Get current platform as 'target-arch' string
+
+ Returns:
+ Platform string like 'darwin-arm64', 'linux-amd64', etc.
+ """
+ target, arch = get_current_target_arch()
+ return f"{target}-{arch}"
diff --git a/cspell.json b/cspell.json
index 4685f42..621c8fd 100644
--- a/cspell.json
+++ b/cspell.json
@@ -2,441 +2,441 @@
"version": "0.2",
"language": "en",
"words": [
- "neekware",
- "neekman",
- "corecli",
- "venv",
- "mypy",
- "ruff",
- "pyproject",
- "toml",
+ "abspath",
+ "abstractmethod",
+ "accelerate",
+ "aiofiles",
+ "aiohttp",
+ "airflow",
+ "alembic",
+ "altair",
+ "altsep",
+ "ansible",
+ "anyio",
+ "apptainer",
+ "apscheduler",
+ "argh",
+ "argon",
"argparse",
- "subparser",
- "subparsers",
- "commands",
- "pytest",
- "setuptools",
- "pycodestyle",
- "pyflakes",
- "pyupgrade",
- "isort",
- "bugbear",
- "untyped",
- "defs",
- "uncommitted",
- "repo",
- "repos",
- "pytest",
- "cov",
- "pyi",
- "isinstance",
- "readlines",
- "errno",
- "SIGINT",
- "SIGTERM",
- "returncode",
- "stdout",
- "stderr",
- "subprocess",
- "pathlib",
- "distutils",
- "sysconfig",
- "virtualenv",
- "pipx",
- "pyenv",
- "conda",
- "mamba",
- "editable",
+ "asciimatics",
+ "asks",
+ "asyncio",
+ "attrs",
"autodiscovery",
"autogenerated",
- "proj",
- "multiline",
- "dotall",
- "ripgrep",
- "heredoc",
- "fmt",
- "linting",
- "linter",
- "formatter",
- "formatters",
- "epilog",
- "prolog",
- "prog",
- "dest",
- "nargs",
- "const",
- "metavar",
- "kwargs",
- "kwarg",
- "staticmethod",
+ "basename",
+ "bcrypt",
+ "beautifulsoup",
+ "begins",
+ "behave",
+ "bemenu",
+ "betamax",
+ "bleach",
+ "bokeh",
+ "bugbear",
+ "buildah",
+ "caplog",
+ "capsys",
+ "catalyst",
+ "catboost",
+ "cattrs",
+ "cbor",
+ "celery",
+ "cement",
+ "cerberus",
+ "chameleon",
+ "charliecloud",
+ "chdir",
+ "chmod",
"classmethod",
- "abstractmethod",
+ "cleo",
+ "cliff",
+ "clize",
+ "colorama",
+ "colorlog",
+ "commands",
+ "conda",
+ "configparser",
+ "conftest",
+ "const",
+ "containerd",
+ "contextlib",
+ "contextmanager",
+ "contextvars",
+ "copytree",
+ "corecli",
+ "coreml",
+ "cov",
+ "cri-o",
+ "croniter",
+ "cryptography",
+ "cssselect",
+ "cuda",
+ "cudnn",
+ "curdir",
+ "dacite",
+ "dagster",
+ "dask",
"dataclass",
"dataclasses",
- "popen",
- "chmod",
- "chdir",
- "getcwd",
- "getenv",
- "setenv",
- "unsetenv",
- "mkdir",
- "makedirs",
- "rmdir",
- "removedirs",
- "listdir",
- "scandir",
- "stat",
- "lstat",
- "symlink",
- "readlink",
- "realpath",
- "abspath",
+ "datasets",
+ "dateutil",
+ "decouple",
+ "deepcopy",
+ "defaultdict",
+ "defpath",
+ "defs",
+ "deque",
+ "desert",
+ "dest",
+ "devnull",
+ "dialog",
+ "diffusers",
"dirname",
- "basename",
+ "distutils",
+ "django",
+ "django-environ",
+ "dmenu",
+ "docopt",
+ "doctest",
+ "dotall",
+ "dotenv",
+ "dramatiq",
+ "dvc",
+ "dynaconf",
+ "editable",
+ "ehaye",
+ "elasticsearch",
+ "eliot",
+ "endswith",
+ "environs",
+ "epilog",
+ "errno",
"expanduser",
"expandvars",
- "normpath",
- "normcase",
- "splitext",
- "splitdrive",
- "pathsep",
- "defpath",
- "altsep",
"extsep",
- "devnull",
- "curdir",
- "pardir",
- "startswith",
- "endswith",
- "rsplit",
- "lstrip",
- "rstrip",
- "zfill",
- "ljust",
- "rjust",
- "islower",
- "isupper",
+ "fabric",
+ "factory-boy",
+ "faker",
+ "fastai",
+ "fastapi",
+ "feast",
+ "fire",
+ "firecracker",
+ "flask",
+ "flax",
+ "fmt",
+ "formatter",
+ "formatters",
+ "freezegun",
+ "functools",
+ "fzf",
+ "genshi",
+ "getcwd",
+ "getenv",
+ "ghostscript",
+ "goodconf",
+ "gooey",
+ "grafana",
+ "graphviz",
+ "great-expectations",
+ "grpc",
+ "grpcio",
+ "gum",
+ "gunicorn",
+ "gvisor",
+ "haiku",
+ "hashlib",
+ "heredoc",
+ "hmac",
+ "html5lib",
+ "httplib",
+ "httpx",
+ "huey",
+ "hydra",
+ "hydra-core",
+ "hypothesis",
+ "ignite",
+ "igraph",
+ "imageio",
+ "inquirer",
+ "invoke",
+ "isalnum",
"isalpha",
"isdigit",
- "isalnum",
+ "isinstance",
+ "islower",
+ "isort",
"isspace",
"istitle",
- "splitlines",
- "startfile",
- "pprint",
- "pformat",
- "deepcopy",
- "shutil",
- "copytree",
- "rmtree",
- "contextlib",
- "contextmanager",
- "wraps",
- "functools",
+ "isupper",
"itertools",
- "defaultdict",
- "deque",
- "namedtuple",
- "sqlite",
- "postgresql",
- "mongodb",
- "redis",
- "memcached",
- "rabbitmq",
+ "jax",
+ "jinja",
+ "jsonschema",
"kafka",
- "elasticsearch",
- "logstash",
- "kibana",
- "grafana",
- "prometheus",
- "nginx",
- "gunicorn",
- "uvicorn",
- "fastapi",
- "django",
- "flask",
- "sqlalchemy",
- "alembic",
- "pydantic",
- "httpx",
- "aiohttp",
- "asyncio",
- "aiofiles",
- "anyio",
- "starlette",
- "websocket",
- "websockets",
- "grpc",
- "grpcio",
- "protobuf",
- "proto",
- "msgpack",
- "cbor",
- "ujson",
- "orjson",
- "simplejson",
- "tomlkit",
- "ruamel",
- "pyyaml",
- "configparser",
- "dotenv",
- "decouple",
- "environs",
- "pydantic_settings",
- "cryptography",
- "passlib",
- "argon",
- "bcrypt",
- "scrypt",
- "hashlib",
- "hmac",
- "uuid",
- "shortuuid",
- "nanoid",
- "ulid",
- "pendulum",
- "dateutil",
- "pytz",
- "tzdata",
- "croniter",
- "apscheduler",
- "celery",
- "dramatiq",
- "rq",
- "huey",
- "airflow",
- "prefect",
- "dagster",
- "dask",
- "numba",
- "numpy",
- "scipy",
- "pandas",
- "polars",
- "matplotlib",
- "seaborn",
- "plotly",
- "bokeh",
- "altair",
- "scikit",
- "sklearn",
- "tensorflow",
- "pytorch",
+ "kata",
+ "kdialog",
+ "kedro",
"keras",
- "xgboost",
+ "kibana",
+ "kubeflow",
+ "kwarg",
+ "kwargs",
+ "lettuce",
+ "libvirt",
"lightgbm",
- "catboost",
- "statsmodels",
- "networkx",
- "igraph",
- "pygraphviz",
- "graphviz",
- "pydot",
- "pillow",
- "opencv",
- "imageio",
- "scikit-image",
- "wand",
- "pytesseract",
- "tesseract",
- "ghostscript",
- "pypdf",
- "pdfplumber",
- "reportlab",
- "xlsxwriter",
- "openpyxl",
- "xlrd",
- "xlwt",
- "tabulate",
- "prettytable",
- "terminaltables",
- "colorama",
- "termcolor",
- "pygments",
- "jinja",
+ "linter",
+ "linting",
+ "listdir",
+ "ljust",
+ "locust",
+ "logbook",
+ "logstash",
+ "loguru",
+ "lstat",
+ "lstrip",
+ "lxc",
+ "lxd",
+ "lxml",
+ "magicmock",
+ "makedirs",
"mako",
- "chameleon",
- "genshi",
+ "mamba",
"markupsafe",
- "bleach",
- "html5lib",
- "beautifulsoup",
- "lxml",
- "cssselect",
- "pyquery",
- "scrapy",
- "selenium",
- "playwright",
+ "marshmallow",
+ "mashumaro",
+ "matplotlib",
"mechanize",
- "httplib",
- "urllib",
- "urllib3",
- "requests",
- "treq",
- "asks",
- "respx",
- "trustme",
+ "memcached",
+ "metaflow",
+ "metavar",
+ "mindspore",
"mitmproxy",
- "locust",
- "pytest-benchmark",
- "pytest-timeout",
- "pytest-mock",
- "pytest-asyncio",
- "pytest-xdist",
- "pytest-bdd",
- "behave",
- "lettuce",
- "robotframework",
- "doctest",
- "unittest",
- "nose",
- "hypothesis",
- "faker",
- "factory-boy",
- "freezegun",
- "responses",
- "vcr",
- "betamax",
+ "mkdir",
+ "mlflow",
"mock",
- "magicmock",
+ "mongodb",
"monkeypatch",
- "caplog",
- "capsys",
- "tmpdir",
- "tmp_path",
- "pytestconfig",
- "conftest",
- "tox",
- "nox",
- "invoke",
- "fabric",
- "ansible",
- "saltstack",
- "puppet",
- "terraform",
- "packer",
- "vagrant",
- "virtualbox",
- "qemu",
- "libvirt",
- "lxc",
- "lxd",
- "podman",
- "buildah",
- "skopeo",
- "cri-o",
- "containerd",
- "runc",
- "kata",
- "firecracker",
- "gvisor",
- "singularity",
- "apptainer",
- "charliecloud",
- "shifter",
- "udocker",
- "nvidia-docker",
- "rocm",
- "cuda",
- "cudnn",
- "tensorrt",
- "onnx",
- "onnxruntime",
- "torchscript",
- "tflite",
- "coreml",
- "openvino",
+ "msgpack",
+ "multiline",
+ "mypy",
+ "namedtuple",
+ "nanoid",
+ "nargs",
"ncnn",
- "tengine",
- "paddle",
- "mindspore",
- "jax",
- "flax",
- "optax",
- "haiku",
- "trax",
- "transformers",
- "tokenizers",
- "datasets",
- "accelerate",
- "diffusers",
- "timm",
- "fastai",
- "pytorch-lightning",
- "ignite",
- "catalyst",
- "hydra",
+ "neekman",
+ "neekware",
+ "networkx",
+ "nginx",
+ "normcase",
+ "normpath",
+ "nose",
+ "nox",
+ "npyscreen",
+ "numba",
+ "numpy",
+ "nvidia-docker",
"omegaconf",
- "wandb",
- "mlflow",
- "kubeflow",
- "metaflow",
- "kedro",
- "dvc",
+ "onnx",
+ "onnxruntime",
+ "opencv",
+ "openpyxl",
+ "openvino",
+ "optax",
+ "orjson",
"pachyderm",
- "feast",
- "great-expectations",
+ "packer",
+ "paddle",
+ "pandas",
"pandera",
- "cerberus",
- "marshmallow",
- "attrs",
- "cattrs",
- "dacite",
- "desert",
- "mashumaro",
- "schematics",
- "voluptuous",
- "schema",
- "jsonschema",
- "yamale",
- "strictyaml",
- "goodconf",
- "dynaconf",
- "hydra-core",
+ "pardir",
+ "passlib",
+ "pathlib",
+ "pathsep",
+ "pdfplumber",
+ "peco",
+ "pendulum",
+ "percol",
+ "pformat",
+ "pillow",
+ "pipx",
+ "plac",
+ "playwright",
+ "plotly",
+ "podman",
+ "polars",
+ "popen",
+ "postgresql",
+ "pprint",
+ "prefect",
+ "prettytable",
+ "prog",
+ "proj",
+ "prolog",
+ "prometheus",
+ "prompt-toolkit",
+ "proto",
+ "protobuf",
+ "puppet",
+ "pycodestyle",
+ "pydantic",
+ "pydantic_settings",
+ "pydot",
+ "pyenv",
+ "pyflakes",
+ "pygments",
+ "pygraphviz",
+ "pyi",
+ "pypdf",
+ "pyproject",
+ "pyquery",
+ "pytesseract",
+ "pytest",
+ "pytest-asyncio",
+ "pytest-bdd",
+ "pytest-benchmark",
+ "pytest-mock",
+ "pytest-timeout",
+ "pytest-xdist",
+ "pytestconfig",
"python-decouple",
"python-dotenv",
- "django-environ",
+ "pytorch",
+ "pytorch-lightning",
+ "pytz",
+ "pyupgrade",
+ "pyyaml",
+ "qemu",
+ "questionary",
+ "rabbitmq",
+ "readlines",
+ "readlink",
+ "realpath",
+ "redis",
+ "removedirs",
+ "repo",
+ "reportlab",
+ "repos",
+ "requests",
+ "responses",
+ "respx",
+ "returncode",
+ "rich",
+ "ripgrep",
+ "rjust",
+ "rmdir",
+ "rmtree",
+ "robotframework",
+ "rocm",
+ "rofi",
+ "rq",
+ "rsplit",
+ "rstrip",
+ "ruamel",
+ "ruff",
+ "runc",
+ "saltstack",
+ "scandir",
+ "schema",
+ "schematics",
+ "scikit",
+ "scikit-image",
+ "scipy",
+ "scrapy",
+ "scrypt",
+ "seaborn",
+ "selenium",
+ "setenv",
+ "setuptools",
+ "shifter",
+ "shortuuid",
+ "shutil",
+ "SIGINT",
+ "SIGTERM",
+ "simplejson",
+ "singularity",
+ "skim",
+ "sklearn",
+ "skopeo",
+ "splitdrive",
+ "splitext",
+ "splitlines",
+ "sqlalchemy",
+ "sqlite",
+ "starlette",
"starlette-context",
- "contextvars",
+ "startfile",
+ "startswith",
+ "stat",
+ "staticmethod",
+ "statsmodels",
+ "stderr",
+ "stdout",
+ "strictyaml",
"structlog",
- "loguru",
- "eliot",
- "logbook",
- "colorlog",
- "rich",
+ "subparser",
+ "subparsers",
+ "subprocess",
+ "symlink",
+ "sysconfig",
+ "tabulate",
+ "tengine",
+ "tensorflow",
+ "tensorrt",
+ "termcolor",
+ "terminaltables",
+ "terraform",
+ "tesseract",
"textual",
+ "tflite",
+ "timm",
+ "tmp_path",
+ "tmpdir",
+ "tokenizers",
+ "toml",
+ "tomlkit",
+ "torchscript",
+ "tox",
+ "transformers",
+ "trax",
+ "treq",
+ "trustme",
"typer",
- "cleo",
- "cliff",
- "cement",
- "docopt",
- "fire",
- "plac",
- "clize",
- "begins",
- "argh",
- "gooey",
- "npyscreen",
- "asciimatics",
+ "tzdata",
+ "udocker",
+ "ujson",
+ "ulid",
+ "uncommitted",
+ "unittest",
+ "unsetenv",
+ "untyped",
+ "urllib",
+ "urllib3",
"urwid",
- "prompt-toolkit",
- "questionary",
- "inquirer",
+ "uuid",
+ "uvicorn",
+ "vagrant",
+ "vcr",
+ "venv",
+ "virtualbox",
+ "virtualenv",
+ "voluptuous",
+ "wand",
+ "wandb",
+ "websocket",
+ "websockets",
"whiptail",
- "dialog",
- "zenity",
- "kdialog",
- "yad",
- "gum",
- "fzf",
- "peco",
- "percol",
- "skim",
- "dmenu",
- "rofi",
"wofi",
- "bemenu"
+ "wraps",
+ "xgboost",
+ "xlrd",
+ "xlsxwriter",
+ "xlwt",
+ "yad",
+ "yamale",
+ "zenity",
+ "zfill"
],
"ignorePaths": [
"**/__pycache__/**",
@@ -475,4 +475,4 @@
"softwareTerms",
"misc"
]
-}
\ No newline at end of file
+}
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
new file mode 100644
index 0000000..cc7ca7f
--- /dev/null
+++ b/docker-compose.test.yml
@@ -0,0 +1,78 @@
+version: '3.8'
+
+services:
+ test-py39:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ args:
+ PYTHON_VERSION: "3.9-slim"
+ image: ehaye-cli-test:py39
+ container_name: ehaye-cli-test-py39
+ command: |
+ bash -c "
+ echo '=== Python 3.9 Test ==='
+ python --version
+ cli --version
+ cli dev all
+ "
+ environment:
+ - PYTHONUNBUFFERED=1
+
+ test-py311:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ args:
+ PYTHON_VERSION: "3.11-slim"
+ image: ehaye-cli-test:py311
+ container_name: ehaye-cli-test-py311
+ command: |
+ bash -c "
+ echo '=== Python 3.11 Test ==='
+ python --version
+ cli --version
+ cli dev all
+ "
+ environment:
+ - PYTHONUNBUFFERED=1
+
+ test-py313:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ args:
+ PYTHON_VERSION: "3.13-slim"
+ image: ehaye-cli-test:py313
+ container_name: ehaye-cli-test-py313
+ command: |
+ bash -c "
+ echo '=== Python 3.13 Test ==='
+ python --version
+ cli --version
+ cli dev all
+ "
+ environment:
+ - PYTHONUNBUFFERED=1
+
+ # Quick test - just run the CLI
+ quick-test:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ image: ehaye-cli-test:latest
+ container_name: ehaye-cli-quick-test
+ command: |
+ bash -c "
+ echo '=== Quick CLI Test ==='
+ cli --help
+ cli proj size
+ cli proj stats
+ cli dev --help
+ cli build --help
+ cli package --help
+ cli release --help
+ echo 'โ
All commands work!'
+ "
+ environment:
+ - PYTHONUNBUFFERED=1
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 6434962..9a1cdf9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,12 +5,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "core-cli"
dynamic = ["version"]
-description = "Core CLI - A modular command-line interface framework"
+description = "ehAyeโข Core CLI - A modular command-line interface framework"
authors = [
- {name = "Your Name", email = "you@example.com"}
+ {name = "Val Neekman", email = "info@neekware.com"}
]
requires-python = ">=3.9"
-dependencies = []
+dependencies = [
+ "click>=8.0",
+]
[project.optional-dependencies]
dev = [
@@ -25,8 +27,10 @@ dev = [
[project.scripts]
cli = "commands.main:main"
-[tool.setuptools]
-packages = ["commands"]
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["commands", "commands.*"]
+exclude = ["commands.tests*"]
[tool.setuptools.dynamic]
version = {attr = "commands.__version__"}
@@ -64,4 +68,4 @@ python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
-ignore_missing_imports = true
\ No newline at end of file
+ignore_missing_imports = true
diff --git a/setup.sh b/setup.sh
index c4aaa08..ea9856a 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# Core CLI Bootstrap Script
+# ehAyeโข Core CLI Bootstrap Script
# Sets up Python virtual environment and installs the cli
set -e
@@ -47,14 +47,14 @@ ask() {
if [[ "$AUTO_YES" == true ]]; then
return 0
fi
-
+
local prompt="$1"
local default="${2:-y}"
-
+
echo -e "${BLUE}?${NC} $prompt (${default}/n): "
read -r response
response=${response:-$default}
-
+
[[ "$response" =~ ^[Yy] ]]
}
@@ -67,16 +67,16 @@ check_python() {
else
error "Python not found. Please install Python 3.9+"
fi
-
+
# Check version
PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | cut -d' ' -f2)
PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
-
+
if [ "$PYTHON_MAJOR" -lt 3 ] || [ "$PYTHON_MAJOR" -eq 3 -a "$PYTHON_MINOR" -lt 9 ]; then
error "Python 3.9+ required, found $PYTHON_VERSION"
fi
-
+
log "Found Python $PYTHON_VERSION"
}
@@ -85,84 +85,150 @@ create_venv() {
if [[ -d "$VENV_DIR" ]]; then
if ask "Virtual environment already exists. Recreate?"; then
log "Moving existing virtual environment to temp..."
-
+
# Move to temp with unique name and remove in background
local temp_dir="/tmp/corecli-venv-$$-$(date +%s)"
mv "$VENV_DIR" "$temp_dir"
-
+
# Remove in background
(rm -rf "$temp_dir" 2>/dev/null) &
-
+
log "Old virtual environment moved to temp, removal running in background"
else
log "Using existing virtual environment"
return 0
fi
fi
-
- log "Creating virtual environment in $VENV_DIR..."
+
+ log "Creating virtual environment in .venv"
sleep 1.0 && $PYTHON_CMD -m venv "$VENV_DIR"
-
+
# Verify venv was created
if [[ ! -f "$VENV_DIR/bin/activate" ]]; then
error "Virtual environment creation failed"
fi
-
+
log "Virtual environment created"
}
# Install dependencies
install_deps() {
log "Activating virtual environment and installing dependencies..."
-
+
source "$VENV_DIR/bin/activate"
- pip install --upgrade pip setuptools wheel
-
+ # Upgrade pip quietly
+ pip install --quiet --upgrade pip setuptools wheel
+
# Configure pip to use temp directory for build artifacts
export PIP_BUILD=/tmp/pip-build-$$
-
- # Install the package in editable mode with dev dependencies
+
+ # Install dependencies from tools/requirements.txt (NOT as a package)
log "Installing dependencies..."
- pip install --no-build-isolation -e ".[dev]"
-
- # Clean up any egg-info that might have been created in project root
- rm -rf "$SCRIPT_DIR"/*.egg-info
-
+
+ # Extract main package names (not sub-dependencies)
+ MAIN_PACKAGES=$(grep -E '^[^#]' "$SCRIPT_DIR/tools/requirements.txt" | cut -d'>' -f1 | cut -d'=' -f1 | xargs)
+ echo " Installing: $MAIN_PACKAGES"
+
+ # Install with minimal output
+ pip install --quiet -r "$SCRIPT_DIR/tools/requirements.txt"
+
log "Dependencies installed successfully"
-
- # Install pre-commit hooks
- log "Installing pre-commit hooks..."
- pre-commit install
- log "Pre-commit hooks installed"
+
+ # Install pre-commit hooks (only if in a git repo)
+ if [[ -d ".git" ]]; then
+ log "Installing pre-commit hooks..."
+ pre-commit install >/dev/null 2>&1 || warn "Could not install pre-commit hooks (not a git repo?)"
+ log "Pre-commit hooks installed"
+ fi
}
# Install cli
install_cli() {
log "Installing cli..."
-
- # Copy the wrapper script to venv
+
+ # Copy the wrapper script to venv only
cp "$SCRIPT_DIR/commands/bin/cli-venv" "$VENV_DIR/bin/cli"
chmod +x "$VENV_DIR/bin/cli"
+
+ log "cli installed to venv"
+
+ # Generate completion script (optional, may fail in CI)
+ log "Generating shell completion..."
- # Install project root version (auto-activates)
- cp "$SCRIPT_DIR/commands/bin/cli-project" "$SCRIPT_DIR/cli"
- chmod +x "$SCRIPT_DIR/cli"
-
- log "cli installed"
+ # Ensure autogen directory exists
+ mkdir -p "$SCRIPT_DIR/commands/autogen"
+
+ # Try to generate completion, but don't fail if it doesn't work
+ if "$VENV_DIR/bin/python" -c "
+from pathlib import Path
+import sys
+sys.path.insert(0, '$SCRIPT_DIR')
+try:
+ from commands.utils.completion import generate_completion_script, get_command_info
+ from commands.main import cli
+
+ completion_path = Path('$SCRIPT_DIR/commands/autogen/completion.sh')
+ completion_path.parent.mkdir(parents=True, exist_ok=True)
+ cli_info = get_command_info(cli)
+ completion_script = generate_completion_script(cli_info)
+
+ # Add completion loaded marker
+ completion_script = completion_script.replace(
+ '# Auto-generated completion script for ehAyeโข Core CLI',
+ '# Auto-generated completion script for ehAyeโข Core CLI\\nexport _ehaye_cli_completions_loaded=1'
+ )
+
+ completion_path.write_text(completion_script)
+ print(f'โ Generated {completion_path}')
+except Exception as e:
+ print(f'โ Could not generate completion: {e}')
+ sys.exit(1)
+" 2>/dev/null; then
+ log "Shell completion generated"
+ else
+ warn "Could not generate shell completion (this is normal in CI)"
+ fi
+
+ # Add completion sourcing to activate script
+ if [[ -f "$SCRIPT_DIR/commands/completion.sh" ]]; then
+ # Add to the end of activate script with proper safeguards
+ if ! grep -q "source.*commands/completion.sh" "$VENV_DIR/bin/activate"; then
+ echo "" >> "$VENV_DIR/bin/activate"
+ echo "# Auto-load CLI completion (only in interactive shells)" >> "$VENV_DIR/bin/activate"
+ echo "if [[ -f \"$SCRIPT_DIR/commands/completion.sh\" ]] && [[ \$- == *i* ]]; then" >> "$VENV_DIR/bin/activate"
+ echo " source \"$SCRIPT_DIR/commands/completion.sh\" 2>/dev/null || true" >> "$VENV_DIR/bin/activate"
+ echo "fi" >> "$VENV_DIR/bin/activate"
+ fi
+ log "Shell completion configured"
+ fi
}
# Main execution
main() {
- echo -e "${BLUE}๐ Core CLI Bootstrap${NC}"
- echo "Setting up Python environment for Core CLI..."
+ echo -e "${BLUE}๐ ehAyeโข Core CLI Bootstrap${NC}"
+ echo "Setting up Python environment for ehAyeโข Core CLI..."
echo
-
+
+ # Check if already in a virtual environment
+ if [ -n "$VIRTUAL_ENV" ]; then
+ # Show a friendlier path if it's our project's venv
+ VENV_DISPLAY="$VIRTUAL_ENV"
+ if [[ "$VIRTUAL_ENV" == "$SCRIPT_DIR/.venv" ]]; then
+ VENV_DISPLAY=".venv (this project)"
+ elif [[ "$VIRTUAL_ENV" == *"/.venv" ]]; then
+ # Show just the parent directory name and .venv
+ PARENT_DIR=$(basename "$(dirname "$VIRTUAL_ENV")")
+ VENV_DISPLAY="$PARENT_DIR/.venv"
+ fi
+ error "A virtual environment is currently active: $VENV_DISPLAY\n\nPlease deactivate it first by running:\n deactivate\n\nThen run ./setup.sh again."
+ fi
+
check_python
create_venv
install_deps
install_cli
-
+
echo
log "Bootstrap complete!"
echo
@@ -171,9 +237,6 @@ main() {
echo -e "${BLUE}To use the CLI:${NC}"
echo " source .venv/bin/activate"
echo " cli --help"
- echo
- echo -e "${YELLOW}๐ก Pro tip: Add this to your ~/.bashrc or ~/.zshrc for auto-activation:${NC}"
- echo " cd $(pwd) && source .venv/bin/activate"
}
-main "$@"
\ No newline at end of file
+main "$@"
diff --git a/src/.gitkeep b/src/.gitkeep
new file mode 100644
index 0000000..3b466ea
--- /dev/null
+++ b/src/.gitkeep
@@ -0,0 +1 @@
+# Auto-generated files directory
diff --git a/src/.keepme b/src/.keepme
deleted file mode 100644
index 370b9cf..0000000
--- a/src/.keepme
+++ /dev/null
@@ -1,19 +0,0 @@
-# Your Business Logic Goes Here
-
-This directory is for your core application code. Keep your CLI commands in `commands/subs/`
-and your business logic here in `src/`.
-
-Example structure:
-```
-src/
-โโโ myproject/
- โโโ __init__.py
- โโโ core.py
- โโโ models.py
- โโโ utils.py
-```
-
-Then import in your CLI commands:
-```python
-from src.myproject.core import MyBusinessLogic
-```
\ No newline at end of file
diff --git a/tools/requirements.txt b/tools/requirements.txt
new file mode 100644
index 0000000..65c7c66
--- /dev/null
+++ b/tools/requirements.txt
@@ -0,0 +1,8 @@
+# Development dependencies for ehAyeโข Core CLI
+click>=8.0
+pytest>=7.0
+pytest-cov>=3.0
+black>=23.0
+ruff>=0.1.0
+mypy>=1.0
+pre-commit>=3.0
\ No newline at end of file