diff --git a/README.md b/README.md index 9770379..417f953 100644 --- a/README.md +++ b/README.md @@ -1 +1,186 @@ -# devkit-cli \ No newline at end of file +# ๐Ÿ› ๏ธ devkit-cli + +A powerful CLI toolkit for developers built with Python + Typer + Rich. + +## โœจ Features + +- ๐Ÿ“Š **System Information** - View CPU, memory, disk, and OS info in beautiful tables +- ๐Ÿ“ **Batch File Rename** - Rename files with custom prefix, suffix, and auto-numbering +- โœ… **Task Management** - Local todo list with priorities and status tracking +- ๐Ÿ“ **Logging** - Automatic logging to `~/.devkit/logs/` +- ๐ŸŽจ **Beautiful Output** - Colorful, formatted console output using Rich + +## ๐Ÿ“ฆ Installation + +```bash +pip install -e . +``` + +## ๐Ÿš€ Quick Start + +### View Help + +```bash +devkit --help # Main help +devkit info --help # Info command help +devkit file --help # File command help +devkit todo --help # Todo command help +``` + +## ๐Ÿ“– Commands Reference + +### 1. System Information (`info`) + +View detailed system information in colorful tables. + +```bash +devkit info all # View all system information +devkit info cpu # CPU information only +devkit info memory # Memory information only +devkit info disk # Disk information only +devkit info os # Operating system information only +``` + +**Example Output:** + +``` + ๐Ÿ–ฅ๏ธ CPU +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Property โ”ƒ Value โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ Processor โ”‚ Intel64 Family 6 Model 190 Stepping 0, GenuineIntel โ”‚ +โ”‚ Physical Cores โ”‚ 4 โ”‚ +โ”‚ Logical Cores โ”‚ 4 โ”‚ +โ”‚ Max Frequency โ”‚ 1700.00 MHz โ”‚ +โ”‚ Current Frequency โ”‚ 1700.00 MHz โ”‚ +โ”‚ Usage โ”‚ 35.9% โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. File Operations (`file`) + +Batch rename files with powerful options. + +#### List Files + +```bash +devkit file list # List files in current directory +devkit file list ./photos # List files in specific directory +devkit file list . --ext .txt # Filter by extension +devkit file list . --pattern "\.py$" # Filter by regex pattern +``` + +#### Rename Files + +```bash +# Add prefix with auto-numbering +devkit file rename ./photos --prefix "vacation_" --start 1 + +# Add suffix (before extension) +devkit file rename ./docs --suffix "_final" + +# Filter by regex pattern and rename +devkit file rename . --pattern "\.txt$" --prefix "doc_" + +# Preview changes without executing +devkit file rename . --prefix "test_" --dry-run + +# Skip confirmation prompt +devkit file rename . --prefix "file_" --yes +``` + +**Options:** + +| Option | Short | Description | +|--------|-------|-------------| +| `--prefix` | `-p` | Prefix to add to filenames | +| `--suffix` | `-s` | Suffix to add (before extension) | +| `--start` | `-n` | Starting number for auto-numbering (default: 1) | +| `--width` | `-w` | Width of number padding (default: 3, e.g., 001) | +| `--pattern` | `-f` | Regex pattern to filter files | +| `--dry-run` | `-d` | Preview changes without executing | +| `--yes` | `-y` | Skip confirmation prompt | + +### 3. Task Management (`todo`) + +Manage your tasks locally with priorities. + +#### Add Tasks + +```bash +devkit todo add "Complete the project documentation" +devkit todo add "Fix critical bug" --priority high +devkit todo add "Update dependencies" --priority medium +devkit todo add "Write unit tests" --priority low +``` + +#### List Tasks + +```bash +devkit todo list # List all tasks +devkit todo list --status pending # Only pending tasks +devkit todo list --status completed # Only completed tasks +devkit todo list --priority high # Filter by priority +``` + +#### Complete/Uncomplete Tasks + +```bash +devkit todo complete 1 # Mark task as completed +devkit todo uncomplete 1 # Mark task as pending +``` + +#### Edit Tasks + +```bash +devkit todo edit 1 --task "New task description" +devkit todo edit 1 --priority high +devkit todo edit 1 --task "New text" --priority low +``` + +#### Delete Tasks + +```bash +devkit todo delete 1 # Delete task (with confirmation) +devkit todo delete 1 --yes # Delete without confirmation +``` + +#### Clear Tasks + +```bash +devkit todo clear --status completed # Clear all completed tasks +devkit todo clear --status all # Clear all tasks +devkit todo clear --status all --yes # Clear all without confirmation +``` + +**Example Output:** + +``` + ๐Ÿ“‹ Todo List +โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ ID โ”ƒ Status โ”ƒ Priority โ”ƒ Task โ”ƒ Created โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ 1 โ”‚ โœ… โ”‚ HIGH โ”‚ Complete the project โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 2 โ”‚ โฌœ โ”‚ HIGH โ”‚ Fix critical bug โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 3 โ”‚ โฌœ โ”‚ MEDIUM โ”‚ Update dependencies โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 4 โ”‚ โฌœ โ”‚ LOW โ”‚ Write unit tests โ”‚ 2026-03-20 13:45 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Progress: 1/4 completed (25%) +``` + +## ๐Ÿ“ Data Storage + +- **Logs**: `~/.devkit/logs/devkit_YYYYMMDD.log` +- **Todos**: `~/.devkit/data/todos.json` + +## ๐Ÿ“‹ Requirements + +- Python >= 3.8 +- typer >= 0.9.0 +- rich >= 13.0.0 +- psutil >= 5.9.0 + +## ๐Ÿ“œ License + +MIT License diff --git a/devkit_cli.egg-info/PKG-INFO b/devkit_cli.egg-info/PKG-INFO new file mode 100644 index 0000000..2a895ca --- /dev/null +++ b/devkit_cli.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.4 +Name: devkit-cli +Version: 1.0.0 +Summary: A powerful CLI toolkit for developers +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: typer>=0.9.0 +Requires-Dist: rich>=13.0.0 +Requires-Dist: psutil>=5.9.0 + +# devkit-cli diff --git a/devkit_cli.egg-info/SOURCES.txt b/devkit_cli.egg-info/SOURCES.txt new file mode 100644 index 0000000..68e8b22 --- /dev/null +++ b/devkit_cli.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +README.md +pyproject.toml +devkit_cli/__init__.py +devkit_cli/main.py +devkit_cli.egg-info/PKG-INFO +devkit_cli.egg-info/SOURCES.txt +devkit_cli.egg-info/dependency_links.txt +devkit_cli.egg-info/entry_points.txt +devkit_cli.egg-info/requires.txt +devkit_cli.egg-info/top_level.txt +devkit_cli/commands/__init__.py +devkit_cli/commands/file.py +devkit_cli/commands/info.py +devkit_cli/commands/todo.py +devkit_cli/utils/__init__.py +devkit_cli/utils/display.py +devkit_cli/utils/logger.py \ No newline at end of file diff --git a/devkit_cli.egg-info/dependency_links.txt b/devkit_cli.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/devkit_cli.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/devkit_cli.egg-info/entry_points.txt b/devkit_cli.egg-info/entry_points.txt new file mode 100644 index 0000000..579ad4e --- /dev/null +++ b/devkit_cli.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +devkit = devkit_cli.main:app diff --git a/devkit_cli.egg-info/requires.txt b/devkit_cli.egg-info/requires.txt new file mode 100644 index 0000000..88182e2 --- /dev/null +++ b/devkit_cli.egg-info/requires.txt @@ -0,0 +1,3 @@ +typer>=0.9.0 +rich>=13.0.0 +psutil>=5.9.0 diff --git a/devkit_cli.egg-info/top_level.txt b/devkit_cli.egg-info/top_level.txt new file mode 100644 index 0000000..59e8581 --- /dev/null +++ b/devkit_cli.egg-info/top_level.txt @@ -0,0 +1 @@ +devkit_cli diff --git a/devkit_cli/__init__.py b/devkit_cli/__init__.py new file mode 100644 index 0000000..1b339ea --- /dev/null +++ b/devkit_cli/__init__.py @@ -0,0 +1,9 @@ +""" +devkit-cli - A powerful CLI toolkit for developers + +This package provides a set of useful command-line tools for developers, +including system information viewing, batch file renaming, and task management. +""" + +__version__ = "1.0.0" +__author__ = "Developer" diff --git a/devkit_cli/__pycache__/__init__.cpython-314.pyc b/devkit_cli/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..7b3723a Binary files /dev/null and b/devkit_cli/__pycache__/__init__.cpython-314.pyc differ diff --git a/devkit_cli/__pycache__/main.cpython-314.pyc b/devkit_cli/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..59009ad Binary files /dev/null and b/devkit_cli/__pycache__/main.cpython-314.pyc differ diff --git a/devkit_cli/commands/__init__.py b/devkit_cli/commands/__init__.py new file mode 100644 index 0000000..80c50e5 --- /dev/null +++ b/devkit_cli/commands/__init__.py @@ -0,0 +1,11 @@ +""" +Commands Module + +This module contains all CLI subcommands. +""" + +from . import info +from . import file +from . import todo + +__all__ = ["info", "file", "todo"] diff --git a/devkit_cli/commands/__pycache__/__init__.cpython-314.pyc b/devkit_cli/commands/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..6782c3c Binary files /dev/null and b/devkit_cli/commands/__pycache__/__init__.cpython-314.pyc differ diff --git a/devkit_cli/commands/__pycache__/file.cpython-314.pyc b/devkit_cli/commands/__pycache__/file.cpython-314.pyc new file mode 100644 index 0000000..25a6fb2 Binary files /dev/null and b/devkit_cli/commands/__pycache__/file.cpython-314.pyc differ diff --git a/devkit_cli/commands/__pycache__/info.cpython-314.pyc b/devkit_cli/commands/__pycache__/info.cpython-314.pyc new file mode 100644 index 0000000..400dad0 Binary files /dev/null and b/devkit_cli/commands/__pycache__/info.cpython-314.pyc differ diff --git a/devkit_cli/commands/__pycache__/todo.cpython-314.pyc b/devkit_cli/commands/__pycache__/todo.cpython-314.pyc new file mode 100644 index 0000000..d8077b9 Binary files /dev/null and b/devkit_cli/commands/__pycache__/todo.cpython-314.pyc differ diff --git a/devkit_cli/commands/file.py b/devkit_cli/commands/file.py new file mode 100644 index 0000000..5995c2f --- /dev/null +++ b/devkit_cli/commands/file.py @@ -0,0 +1,341 @@ +""" +File Command Module + +This module provides batch file renaming functionality. +Supports custom prefix and automatic numbering. +""" + +import os +import re +from pathlib import Path +from typing import Optional + +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.prompt import Prompt, Confirm + +from devkit_cli.utils.logger import get_logger +from devkit_cli.utils.display import print_success, print_error, print_warning, print_info + +app = typer.Typer(help="๐Ÿ“ Batch file operations") +console = Console() +logger = get_logger() + + +def validate_directory(directory: Path) -> bool: + """ + Validate that the directory exists and is accessible. + + Args: + directory: Path to the directory + + Returns: + True if valid, False otherwise + """ + if not directory.exists(): + print_error(f"Directory does not exist: {directory}") + return False + if not directory.is_dir(): + print_error(f"Path is not a directory: {directory}") + return False + return True + + +def get_files(directory: Path, pattern: Optional[str] = None) -> list[Path]: + """ + Get list of files in directory, optionally filtered by pattern. + + Args: + directory: Path to the directory + pattern: Optional regex pattern to filter files + + Returns: + List of file paths + """ + files = [] + try: + for item in directory.iterdir(): + if item.is_file(): + if pattern: + if re.search(pattern, item.name): + files.append(item) + else: + files.append(item) + files.sort(key=lambda x: x.name.lower()) + except PermissionError as e: + logger.error(f"Permission denied: {e}") + print_error(f"Permission denied accessing directory: {directory}") + except Exception as e: + logger.error(f"Error reading directory: {e}") + print_error(f"Error reading directory: {e}") + return files + + +def generate_new_name( + original_name: str, + prefix: Optional[str] = None, + suffix: Optional[str] = None, + number: Optional[int] = None, + number_width: int = 3, + keep_extension: bool = True, +) -> str: + """ + Generate a new filename based on the provided parameters. + + Args: + original_name: Original filename + prefix: Optional prefix to add + suffix: Optional suffix to add (before extension) + number: Optional number to include + number_width: Width of number padding (default: 3, e.g., 001, 002) + keep_extension: Whether to keep the original extension + + Returns: + New filename + """ + path = Path(original_name) + stem = path.stem + ext = path.suffix if keep_extension else "" + + new_name = stem + + if number is not None: + new_name = f"{str(number).zfill(number_width)}_{new_name}" + + if prefix: + new_name = f"{prefix}{new_name}" + + if suffix: + new_name = f"{new_name}{suffix}" + + if ext: + new_name = f"{new_name}{ext}" + + return new_name + + +def preview_rename( + files: list[Path], + prefix: Optional[str] = None, + suffix: Optional[str] = None, + start_number: int = 1, + number_width: int = 3, +) -> list[tuple[Path, Path]]: + """ + Preview the renaming operation. + + Args: + files: List of files to rename + prefix: Optional prefix + suffix: Optional suffix + start_number: Starting number for auto-numbering + number_width: Width of number padding + + Returns: + List of (old_path, new_path) tuples + """ + rename_pairs = [] + for i, file_path in enumerate(files): + new_name = generate_new_name( + file_path.name, + prefix=prefix, + suffix=suffix, + number=start_number + i, + number_width=number_width, + ) + new_path = file_path.parent / new_name + rename_pairs.append((file_path, new_path)) + return rename_pairs + + +def display_preview(rename_pairs: list[tuple[Path, Path]]) -> None: + """ + Display a preview table of the renaming operation. + + Args: + rename_pairs: List of (old_path, new_path) tuples + """ + table = Table(title="๐Ÿ“‹ Rename Preview", show_header=True, header_style="bold cyan") + table.add_column("Original Name", style="yellow") + table.add_column("โ†’", style="dim", justify="center", width=3) + table.add_column("New Name", style="green") + + for old_path, new_path in rename_pairs: + table.add_row(old_path.name, "โ†’", new_path.name) + + console.print(table) + + +@app.command() +def rename( + directory: str = typer.Argument( + ".", + help="Directory containing files to rename", + ), + prefix: Optional[str] = typer.Option( + None, + "--prefix", "-p", + help="Prefix to add to filenames", + ), + suffix: Optional[str] = typer.Option( + None, + "--suffix", "-s", + help="Suffix to add to filenames (before extension)", + ), + start: int = typer.Option( + 1, + "--start", "-n", + help="Starting number for auto-numbering", + ), + width: int = typer.Option( + 3, + "--width", "-w", + help="Width of number padding (e.g., 3 gives 001, 002)", + ), + pattern: Optional[str] = typer.Option( + None, + "--pattern", "-f", + help="Regex pattern to filter files", + ), + dry_run: bool = typer.Option( + False, + "--dry-run", "-d", + help="Preview changes without executing", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Batch rename files in a directory. + + Examples: + devkit file rename ./photos --prefix "vacation_" --start 1 + devkit file rename ./docs --suffix "_final" --dry-run + devkit file rename . --pattern "\\.txt$" --prefix "doc_" + """ + dir_path = Path(directory).resolve() + + logger.info(f"Starting batch rename in: {dir_path}") + + if not validate_directory(dir_path): + raise typer.Exit(1) + + files = get_files(dir_path, pattern) + + if not files: + print_warning("No files found matching the criteria") + raise typer.Exit(0) + + print_info(f"Found {len(files)} file(s) to process") + + rename_pairs = preview_rename( + files, + prefix=prefix, + suffix=suffix, + start_number=start, + number_width=width, + ) + + console.print() + display_preview(rename_pairs) + console.print() + + if dry_run: + print_info("Dry run mode - no changes made") + raise typer.Exit(0) + + if not yes: + if not Confirm.ask("Proceed with renaming?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + success_count = 0 + error_count = 0 + + for old_path, new_path in rename_pairs: + try: + if new_path.exists(): + print_warning(f"Skipping {old_path.name} - target already exists: {new_path.name}") + error_count += 1 + continue + + old_path.rename(new_path) + logger.info(f"Renamed: {old_path.name} -> {new_path.name}") + success_count += 1 + except Exception as e: + logger.error(f"Error renaming {old_path.name}: {e}") + print_error(f"Failed to rename {old_path.name}: {e}") + error_count += 1 + + console.print() + print_success(f"Renaming complete: {success_count} succeeded, {error_count} failed") + + +@app.command() +def list( + directory: str = typer.Argument( + ".", + help="Directory to list files from", + ), + pattern: Optional[str] = typer.Option( + None, + "--pattern", "-f", + help="Regex pattern to filter files", + ), + ext: Optional[str] = typer.Option( + None, + "--ext", "-e", + help="Filter by file extension (e.g., .txt)", + ), +): + """ + List files in a directory with optional filtering. + + Examples: + devkit file list ./photos + devkit file list . --ext .txt + devkit file list . --pattern "doc_.*\\.pdf$" + """ + dir_path = Path(directory).resolve() + + logger.info(f"Listing files in: {dir_path}") + + if not validate_directory(dir_path): + raise typer.Exit(1) + + files = get_files(dir_path, pattern) + + if ext: + files = [f for f in files if f.suffix.lower() == ext.lower()] + + if not files: + print_warning("No files found matching the criteria") + raise typer.Exit(0) + + table = Table(title=f"๐Ÿ“ Files in {dir_path}", show_header=True, header_style="bold cyan") + table.add_column("#", style="dim", width=4) + table.add_column("Name", style="green") + table.add_column("Extension", style="yellow") + table.add_column("Size", style="blue", justify="right") + + for i, file_path in enumerate(files, 1): + size = file_path.stat().st_size + size_str = f"{size:,} B" if size < 1024 else f"{size / 1024:.1f} KB" + table.add_row( + str(i), + file_path.name, + file_path.suffix or "(none)", + size_str, + ) + + console.print(table) + print_info(f"Total: {len(files)} file(s)") + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/commands/info.py b/devkit_cli/commands/info.py new file mode 100644 index 0000000..4205af6 --- /dev/null +++ b/devkit_cli/commands/info.py @@ -0,0 +1,273 @@ +""" +Info Command Module + +This module provides system information viewing functionality. +Displays CPU, memory, disk, and OS information in a formatted table. +""" + +import platform +from typing import Optional + +import psutil +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from devkit_cli.utils.logger import get_logger + +app = typer.Typer(help="๐Ÿ“Š View system information") +console = Console() +logger = get_logger() + + +def get_size(bytes_value: int, suffix: str = "B") -> str: + """ + Convert bytes to human-readable format. + + Args: + bytes_value: Size in bytes + suffix: Unit suffix (default: B for bytes) + + Returns: + Human-readable size string + """ + factor = 1024 + for unit in ["", "K", "M", "G", "T", "P"]: + if bytes_value < factor: + return f"{bytes_value:.2f} {unit}{suffix}" + bytes_value /= factor + return f"{bytes_value:.2f} P{suffix}" + + +def get_cpu_info() -> dict[str, str]: + """ + Get CPU information. + + Returns: + Dictionary containing CPU details + """ + try: + cpu_freq = psutil.cpu_freq() + cpu_count_logical = psutil.cpu_count(logical=True) + cpu_count_physical = psutil.cpu_count(logical=False) + cpu_percent = psutil.cpu_percent(interval=0.5) + + return { + "Processor": platform.processor() or "Unknown", + "Physical Cores": str(cpu_count_physical) if cpu_count_physical else "N/A", + "Logical Cores": str(cpu_count_logical) if cpu_count_logical else "N/A", + "Max Frequency": f"{cpu_freq.max:.2f} MHz" if cpu_freq else "N/A", + "Current Frequency": f"{cpu_freq.current:.2f} MHz" if cpu_freq else "N/A", + "Usage": f"{cpu_percent:.1f}%", + } + except Exception as e: + logger.error(f"Error getting CPU info: {e}") + return {"Error": str(e)} + + +def get_memory_info() -> dict[str, str]: + """ + Get memory information. + + Returns: + Dictionary containing memory details + """ + try: + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + + return { + "Total RAM": get_size(mem.total), + "Available RAM": get_size(mem.available), + "Used RAM": get_size(mem.used), + "RAM Usage": f"{mem.percent}%", + "Total Swap": get_size(swap.total), + "Used Swap": get_size(swap.used), + "Swap Usage": f"{swap.percent}%", + } + except Exception as e: + logger.error(f"Error getting memory info: {e}") + return {"Error": str(e)} + + +def get_disk_info() -> list[dict[str, str]]: + """ + Get disk information for all partitions. + + Returns: + List of dictionaries containing disk details + """ + disks = [] + try: + partitions = psutil.disk_partitions() + for partition in partitions: + try: + usage = psutil.disk_usage(partition.mountpoint) + disks.append({ + "Device": partition.device, + "Mountpoint": partition.mountpoint, + "File System": partition.fstype, + "Total Size": get_size(usage.total), + "Used": get_size(usage.used), + "Free": get_size(usage.free), + "Usage": f"{usage.percent}%", + }) + except PermissionError: + continue + except Exception as e: + logger.warning(f"Error accessing partition {partition.mountpoint}: {e}") + continue + except Exception as e: + logger.error(f"Error getting disk info: {e}") + disks.append({"Error": str(e)}) + return disks + + +def get_os_info() -> dict[str, str]: + """ + Get operating system information. + + Returns: + Dictionary containing OS details + """ + try: + return { + "System": platform.system(), + "Node Name": platform.node(), + "Release": platform.release(), + "Version": platform.version(), + "Machine": platform.machine(), + "Processor": platform.processor() or "Unknown", + "Python Version": platform.python_version(), + } + except Exception as e: + logger.error(f"Error getting OS info: {e}") + return {"Error": str(e)} + + +def create_info_table(title: str, data: dict[str, str]) -> Table: + """ + Create a formatted table for displaying information. + + Args: + title: Table title + data: Dictionary of key-value pairs to display + + Returns: + Configured Table object + """ + table = Table(title=title, show_header=True, header_style="bold cyan") + table.add_column("Property", style="green", no_wrap=True) + table.add_column("Value", style="white") + + for key, value in data.items(): + table.add_row(key, value) + + return table + + +@app.command() +def cpu(): + """Display CPU information.""" + logger.info("Fetching CPU information") + cpu_data = get_cpu_info() + table = create_info_table("๐Ÿ–ฅ๏ธ CPU Information", cpu_data) + console.print(table) + + +@app.command() +def memory(): + """Display memory information.""" + logger.info("Fetching memory information") + mem_data = get_memory_info() + table = create_info_table("๐Ÿ’พ Memory Information", mem_data) + console.print(table) + + +@app.command() +def disk(): + """Display disk information.""" + logger.info("Fetching disk information") + disk_data = get_disk_info() + + if not disk_data: + console.print("[yellow]No disk information available[/yellow]") + return + + table = Table(title="๐Ÿ’ฟ Disk Information", show_header=True, header_style="bold cyan") + table.add_column("Device", style="green") + table.add_column("Mountpoint", style="blue") + table.add_column("File System", style="yellow") + table.add_column("Total", style="white") + table.add_column("Used", style="white") + table.add_column("Free", style="white") + table.add_column("Usage", style="magenta") + + for disk in disk_data: + if "Error" not in disk: + table.add_row( + disk.get("Device", "N/A"), + disk.get("Mountpoint", "N/A"), + disk.get("File System", "N/A"), + disk.get("Total Size", "N/A"), + disk.get("Used", "N/A"), + disk.get("Free", "N/A"), + disk.get("Usage", "N/A"), + ) + + console.print(table) + + +@app.command() +def os(): + """Display operating system information.""" + logger.info("Fetching OS information") + os_data = get_os_info() + table = create_info_table("๐Ÿ–ฅ๏ธ Operating System Information", os_data) + console.print(table) + + +@app.command() +def all(): + """Display all system information.""" + logger.info("Fetching all system information") + + console.print() + console.print(Panel.fit("[bold cyan]๐Ÿ“Š System Information Report[/bold cyan]", border_style="blue")) + + console.print() + cpu_data = get_cpu_info() + console.print(create_info_table("๐Ÿ–ฅ๏ธ CPU", cpu_data)) + + console.print() + mem_data = get_memory_info() + console.print(create_info_table("๐Ÿ’พ Memory", mem_data)) + + console.print() + disk_data = get_disk_info() + if disk_data: + disk_table = Table(title="๐Ÿ’ฟ Disk", show_header=True, header_style="bold cyan") + disk_table.add_column("Device", style="green") + disk_table.add_column("Mountpoint", style="blue") + disk_table.add_column("Total", style="white") + disk_table.add_column("Usage", style="magenta") + for disk in disk_data: + if "Error" not in disk: + disk_table.add_row( + disk.get("Device", "N/A"), + disk.get("Mountpoint", "N/A"), + disk.get("Total Size", "N/A"), + disk.get("Usage", "N/A"), + ) + console.print(disk_table) + + console.print() + os_data = get_os_info() + console.print(create_info_table("๐Ÿ–ฅ๏ธ Operating System", os_data)) + + console.print() + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/commands/todo.py b/devkit_cli/commands/todo.py new file mode 100644 index 0000000..697bbec --- /dev/null +++ b/devkit_cli/commands/todo.py @@ -0,0 +1,459 @@ +""" +Todo Command Module + +This module provides local task management functionality. +Supports adding, viewing, marking complete, and deleting tasks. +""" + +import json +from datetime import datetime +from pathlib import Path +from typing import Optional + +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from devkit_cli.utils.logger import get_logger +from devkit_cli.utils.display import print_success, print_error, print_warning, print_info + +app = typer.Typer(help="โœ… Task management") +console = Console() +logger = get_logger() + +DATA_DIR = Path.home() / ".devkit" / "data" +DATA_FILE = DATA_DIR / "todos.json" + + +def ensure_data_dir() -> None: + """Ensure the data directory exists.""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + + +def load_todos() -> list[dict]: + """ + Load todos from the JSON file. + + Returns: + List of todo dictionaries + """ + ensure_data_dir() + try: + if DATA_FILE.exists(): + with open(DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + logger.error(f"Error parsing todo file: {e}") + except Exception as e: + logger.error(f"Error loading todos: {e}") + return [] + + +def save_todos(todos: list[dict]) -> None: + """ + Save todos to the JSON file. + + Args: + todos: List of todo dictionaries to save + """ + ensure_data_dir() + try: + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(todos, f, indent=2, ensure_ascii=False) + logger.info(f"Saved {len(todos)} todo(s)") + except Exception as e: + logger.error(f"Error saving todos: {e}") + raise + + +def get_next_id(todos: list[dict]) -> int: + """ + Get the next available todo ID. + + Args: + todos: List of existing todos + + Returns: + Next available ID + """ + if not todos: + return 1 + return max(todo.get("id", 0) for todo in todos) + 1 + + +def display_todos_table(todos: list[dict], title: str = "๐Ÿ“‹ Todo List") -> None: + """ + Display todos in a formatted table. + + Args: + todos: List of todo dictionaries + title: Table title + """ + if not todos: + print_info("No tasks found. Add a new task with 'devkit todo add'") + return + + table = Table(title=title, show_header=True, header_style="bold cyan") + table.add_column("ID", style="dim", width=4, justify="center") + table.add_column("Status", width=8, justify="center") + table.add_column("Priority", width=8, justify="center") + table.add_column("Task", style="white") + table.add_column("Created", style="dim") + + for todo in todos: + status = "โœ…" if todo.get("completed", False) else "โฌœ" + status_style = "green" if todo.get("completed", False) else "yellow" + + priority = todo.get("priority", "medium") + priority_styles = { + "high": "bold red", + "medium": "yellow", + "low": "dim", + } + priority_style = priority_styles.get(priority, "white") + + created = todo.get("created", "") + if created: + try: + dt = datetime.fromisoformat(created) + created = dt.strftime("%Y-%m-%d %H:%M") + except ValueError: + pass + + table.add_row( + str(todo.get("id", "?")), + f"[{status_style}]{status}[/{status_style}]", + f"[{priority_style}]{priority.upper()}[/{priority_style}]", + todo.get("task", ""), + created, + ) + + console.print(table) + + completed = sum(1 for t in todos if t.get("completed", False)) + total = len(todos) + console.print() + console.print(f"[dim]Progress: {completed}/{total} completed ({completed/total*100:.0f}%)[/dim]") + + +@app.command() +def add( + task: str = typer.Argument( + ..., + help="Task description", + ), + priority: str = typer.Option( + "medium", + "--priority", "-p", + help="Task priority: high, medium, low", + ), +): + """ + Add a new task to the todo list. + + Examples: + devkit todo add "Complete the project documentation" + devkit todo add "Fix critical bug" --priority high + """ + if priority.lower() not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + + todos = load_todos() + + new_todo = { + "id": get_next_id(todos), + "task": task, + "priority": priority.lower(), + "completed": False, + "created": datetime.now().isoformat(), + "completed_at": None, + } + + todos.append(new_todo) + save_todos(todos) + + logger.info(f"Added task: {task}") + print_success(f"Task added: {task}") + console.print(f"[dim]ID: {new_todo['id']} | Priority: {priority}[/dim]") + + +@app.command() +def list( + status: Optional[str] = typer.Option( + None, + "--status", "-s", + help="Filter by status: all, pending, completed", + ), + priority: Optional[str] = typer.Option( + None, + "--priority", "-p", + help="Filter by priority: high, medium, low", + ), +): + """ + List all tasks with optional filtering. + + Examples: + devkit todo list + devkit todo list --status pending + devkit todo list --priority high + """ + todos = load_todos() + + if status: + status = status.lower() + if status == "pending": + todos = [t for t in todos if not t.get("completed", False)] + elif status == "completed": + todos = [t for t in todos if t.get("completed", False)] + elif status != "all": + print_error(f"Invalid status: {status}. Must be all, pending, or completed.") + raise typer.Exit(1) + + if priority: + priority = priority.lower() + if priority not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + todos = [t for t in todos if t.get("priority", "medium") == priority] + + logger.info(f"Listing {len(todos)} todo(s)") + display_todos_table(todos) + + +@app.command() +def complete( + task_id: int = typer.Argument( + ..., + help="Task ID to mark as complete", + ), +): + """ + Mark a task as completed. + + Examples: + devkit todo complete 1 + """ + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + if todo.get("completed", False): + print_warning(f"Task {task_id} is already completed") + raise typer.Exit(0) + + todo["completed"] = True + todo["completed_at"] = datetime.now().isoformat() + save_todos(todos) + + logger.info(f"Completed task {task_id}: {todo.get('task')}") + print_success(f"Task {task_id} marked as completed: {todo.get('task')}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def uncomplete( + task_id: int = typer.Argument( + ..., + help="Task ID to mark as incomplete", + ), +): + """ + Mark a completed task as pending. + + Examples: + devkit todo uncomplete 1 + """ + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + if not todo.get("completed", False): + print_warning(f"Task {task_id} is already pending") + raise typer.Exit(0) + + todo["completed"] = False + todo["completed_at"] = None + save_todos(todos) + + logger.info(f"Uncompleted task {task_id}: {todo.get('task')}") + print_success(f"Task {task_id} marked as pending: {todo.get('task')}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def delete( + task_id: int = typer.Argument( + ..., + help="Task ID to delete", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Delete a task from the todo list. + + Examples: + devkit todo delete 1 + devkit todo delete 1 --yes + """ + todos = load_todos() + + for i, todo in enumerate(todos): + if todo.get("id") == task_id: + task_text = todo.get("task", "") + + if not yes: + console.print(f"[yellow]Delete task:[/yellow] {task_text}") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + todos.pop(i) + save_todos(todos) + + logger.info(f"Deleted task {task_id}: {task_text}") + print_success(f"Task {task_id} deleted: {task_text}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def clear( + status: Optional[str] = typer.Option( + None, + "--status", "-s", + help="Clear tasks by status: all, completed", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Clear tasks from the todo list. + + Examples: + devkit todo clear --status completed + devkit todo clear --status all --yes + """ + todos = load_todos() + + if not todos: + print_info("No tasks to clear") + raise typer.Exit(0) + + if status is None: + status = "completed" + + status = status.lower() + + if status == "completed": + to_remove = [t for t in todos if t.get("completed", False)] + if not to_remove: + print_info("No completed tasks to clear") + raise typer.Exit(0) + + if not yes: + console.print(f"[yellow]Clear {len(to_remove)} completed task(s)?[/yellow]") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + todos = [t for t in todos if not t.get("completed", False)] + save_todos(todos) + logger.info(f"Cleared {len(to_remove)} completed task(s)") + print_success(f"Cleared {len(to_remove)} completed task(s)") + + elif status == "all": + if not yes: + console.print(f"[red]Clear ALL {len(todos)} task(s)?[/red]") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + save_todos([]) + logger.info("Cleared all tasks") + print_success("Cleared all tasks") + + else: + print_error(f"Invalid status: {status}. Must be all or completed.") + raise typer.Exit(1) + + +@app.command() +def edit( + task_id: int = typer.Argument( + ..., + help="Task ID to edit", + ), + task: Optional[str] = typer.Option( + None, + "--task", "-t", + help="New task description", + ), + priority: Optional[str] = typer.Option( + None, + "--priority", "-p", + help="New priority: high, medium, low", + ), +): + """ + Edit an existing task. + + Examples: + devkit todo edit 1 --task "Updated task description" + devkit todo edit 1 --priority high + devkit todo edit 1 --task "New text" --priority low + """ + if task is None and priority is None: + print_error("Please provide at least one option to edit (--task or --priority)") + raise typer.Exit(1) + + if priority and priority.lower() not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + old_task = todo.get("task", "") + old_priority = todo.get("priority", "medium") + + if task: + todo["task"] = task + if priority: + todo["priority"] = priority.lower() + + save_todos(todos) + + logger.info(f"Edited task {task_id}") + print_success(f"Task {task_id} updated") + console.print(f"[dim]Task: {old_task} โ†’ {todo.get('task')}[/dim]") + console.print(f"[dim]Priority: {old_priority} โ†’ {todo.get('priority')}[/dim]") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/main.py b/devkit_cli/main.py new file mode 100644 index 0000000..5dbd57c --- /dev/null +++ b/devkit_cli/main.py @@ -0,0 +1,51 @@ +""" +devkit-cli Main Entry Point + +This module defines the main CLI application and registers all subcommands. +""" + +import typer +from rich.console import Console +from rich.panel import Panel + +from devkit_cli.commands import info, file, todo + +app = typer.Typer( + name="devkit", + help="๐Ÿ› ๏ธ DevKit CLI - A powerful developer toolkit", + add_completion=False, +) +console = Console() + + +@app.callback() +def main(): + """ + ๐Ÿ› ๏ธ DevKit CLI - A powerful developer toolkit + + A collection of useful command-line tools for developers. + Use --help with any command to see more details. + """ + pass + + +app.add_typer(info.app, name="info", help="๐Ÿ“Š View system information") +app.add_typer(file.app, name="file", help="๐Ÿ“ Batch file operations") +app.add_typer(todo.app, name="todo", help="โœ… Task management") + + +@app.command() +def version(): + """Show the current version of devkit-cli.""" + from devkit_cli import __version__ + console.print( + Panel.fit( + f"[bold green]devkit-cli[/bold green] version [bold cyan]{__version__}[/bold cyan]", + title="Version", + border_style="blue", + ) + ) + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/utils/__init__.py b/devkit_cli/utils/__init__.py new file mode 100644 index 0000000..85b4905 --- /dev/null +++ b/devkit_cli/utils/__init__.py @@ -0,0 +1,17 @@ +""" +Utilities Module + +This module contains utility functions for logging and display. +""" + +from .logger import get_logger, setup_logger +from .display import print_success, print_error, print_warning, print_info + +__all__ = [ + "get_logger", + "setup_logger", + "print_success", + "print_error", + "print_warning", + "print_info", +] diff --git a/devkit_cli/utils/__pycache__/__init__.cpython-314.pyc b/devkit_cli/utils/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..e4f6e23 Binary files /dev/null and b/devkit_cli/utils/__pycache__/__init__.cpython-314.pyc differ diff --git a/devkit_cli/utils/__pycache__/display.cpython-314.pyc b/devkit_cli/utils/__pycache__/display.cpython-314.pyc new file mode 100644 index 0000000..c42c149 Binary files /dev/null and b/devkit_cli/utils/__pycache__/display.cpython-314.pyc differ diff --git a/devkit_cli/utils/__pycache__/logger.cpython-314.pyc b/devkit_cli/utils/__pycache__/logger.cpython-314.pyc new file mode 100644 index 0000000..52980e2 Binary files /dev/null and b/devkit_cli/utils/__pycache__/logger.cpython-314.pyc differ diff --git a/devkit_cli/utils/display.py b/devkit_cli/utils/display.py new file mode 100644 index 0000000..f386fba --- /dev/null +++ b/devkit_cli/utils/display.py @@ -0,0 +1,123 @@ +""" +Display Module + +This module provides beautiful console output utilities using Rich. +""" + +from typing import Any, Optional +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +console = Console() + + +def print_success(message: str, title: Optional[str] = None) -> None: + """ + Print a success message with green styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold green]โœ“[/bold green] {message}", + title=title, + border_style="green", + ) + ) + else: + console.print(f"[bold green]โœ“[/bold green] {message}") + + +def print_error(message: str, title: Optional[str] = None) -> None: + """ + Print an error message with red styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold red]โœ—[/bold red] {message}", + title=title, + border_style="red", + ) + ) + else: + console.print(f"[bold red]โœ—[/bold red] {message}") + + +def print_warning(message: str, title: Optional[str] = None) -> None: + """ + Print a warning message with yellow styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold yellow]โš [/bold yellow] {message}", + title=title, + border_style="yellow", + ) + ) + else: + console.print(f"[bold yellow]โš [/bold yellow] {message}") + + +def print_info(message: str, title: Optional[str] = None) -> None: + """ + Print an info message with blue styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold blue]โ„น[/bold blue] {message}", + title=title, + border_style="blue", + ) + ) + else: + console.print(f"[bold blue]โ„น[/bold blue] {message}") + + +def create_table( + title: str, + columns: list[str], + rows: list[list[Any]], + show_header: bool = True, +) -> Table: + """ + Create a formatted table with colored output. + + Args: + title: Table title + columns: Column headers + rows: Table data rows + show_header: Whether to show column headers + + Returns: + Configured Table object + """ + table = Table(title=title, show_header=show_header, header_style="bold cyan") + + for column in columns: + table.add_column(column, style="white") + + for row in rows: + styled_row = [str(cell) for cell in row] + table.add_row(*styled_row) + + return table diff --git a/devkit_cli/utils/logger.py b/devkit_cli/utils/logger.py new file mode 100644 index 0000000..798ed88 --- /dev/null +++ b/devkit_cli/utils/logger.py @@ -0,0 +1,95 @@ +""" +Logger Module + +This module provides logging functionality with colored output support. +""" + +import logging +import sys +from pathlib import Path +from typing import Optional +from datetime import datetime +from rich.console import Console + +console = Console() + +LOG_DIR = Path.home() / ".devkit" / "logs" +LOG_DIR.mkdir(parents=True, exist_ok=True) + + +class ColoredFormatter(logging.Formatter): + """Custom formatter with color support for different log levels.""" + + COLORS = { + "DEBUG": "dim", + "INFO": "blue", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "bold red", + } + + def format(self, record: logging.LogRecord) -> str: + log_message = super().format(record) + color = self.COLORS.get(record.levelname, "white") + return f"[{color}]{log_message}[/{color}]" + + +def setup_logger( + name: str = "devkit", + level: int = logging.INFO, + log_to_file: bool = True, +) -> logging.Logger: + """ + Setup and configure a logger instance. + + Args: + name: Logger name + level: Logging level + log_to_file: Whether to log to file + + Returns: + Configured logger instance + """ + logger = logging.getLogger(name) + logger.setLevel(level) + + if logger.handlers: + return logger + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(level) + console_formatter = ColoredFormatter( + "%(asctime)s | %(levelname)-8s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + if log_to_file: + log_file = LOG_DIR / f"devkit_{datetime.now().strftime('%Y%m%d')}.log" + file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler.setLevel(level) + file_formatter = logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + return logger + + +_logger: Optional[logging.Logger] = None + + +def get_logger() -> logging.Logger: + """ + Get the global logger instance. + + Returns: + Global logger instance + """ + global _logger + if _logger is None: + _logger = setup_logger() + return _logger diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1b23e52 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "devkit-cli" +version = "1.0.0" +description = "A powerful CLI toolkit for developers" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "typer>=0.9.0", + "rich>=13.0.0", + "psutil>=5.9.0", +] + +[project.scripts] +devkit = "devkit_cli.main:app"