rhythmos-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rhythmos_cli-0.1.0/.gitignore +30 -0
- rhythmos_cli-0.1.0/LICENCE +19 -0
- rhythmos_cli-0.1.0/PKG-INFO +79 -0
- rhythmos_cli-0.1.0/README.md +55 -0
- rhythmos_cli-0.1.0/examples/commands/hello.py +12 -0
- rhythmos_cli-0.1.0/pyproject.toml +47 -0
- rhythmos_cli-0.1.0/src/rhythmos/__init__.py +6 -0
- rhythmos_cli-0.1.0/src/rhythmos/__main__.py +4 -0
- rhythmos_cli-0.1.0/src/rhythmos/base_command.py +19 -0
- rhythmos_cli-0.1.0/src/rhythmos/cli.py +70 -0
- rhythmos_cli-0.1.0/src/rhythmos/discovery.py +94 -0
- rhythmos_cli-0.1.0/src/rhythmos/stubs/command.py.stub +13 -0
- rhythmos_cli-0.1.0/src/rhythmos/system_commands/__init__.py +10 -0
- rhythmos_cli-0.1.0/src/rhythmos/system_commands/list.py +21 -0
- rhythmos_cli-0.1.0/src/rhythmos/system_commands/make.py +25 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/.venv
|
|
2
|
+
/.ipynb_checkpoints
|
|
3
|
+
|
|
4
|
+
/.pytest_cache
|
|
5
|
+
/.ruff_cache
|
|
6
|
+
/.mypy_cache
|
|
7
|
+
/.tox
|
|
8
|
+
/.direnv
|
|
9
|
+
/.eggs
|
|
10
|
+
/*.egg-info
|
|
11
|
+
/src/*.egg-info
|
|
12
|
+
**/*.egg-info/
|
|
13
|
+
|
|
14
|
+
/dist
|
|
15
|
+
/build
|
|
16
|
+
|
|
17
|
+
/__pycache__
|
|
18
|
+
/*/__pycache__
|
|
19
|
+
**/__pycache__/
|
|
20
|
+
|
|
21
|
+
!/.gitignore
|
|
22
|
+
|
|
23
|
+
/.vscode
|
|
24
|
+
/tests/temp
|
|
25
|
+
|
|
26
|
+
.env
|
|
27
|
+
data/**/*.yml
|
|
28
|
+
|
|
29
|
+
.DS_Store
|
|
30
|
+
environment/*.env
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 JoanMbela
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rhythmos-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight CLI framework with project-local auto-discovered commands
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/JoanMbela/rhythmos-cli
|
|
6
|
+
Project-URL: Repository, https://gitlab.com/JoanMbela/rhythmos-cli
|
|
7
|
+
Author-email: mbela imm <mbela.immongault@globalnext.rocks>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENCE
|
|
10
|
+
Keywords: argparse,cli,commands
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Rhythmos CLI
|
|
26
|
+
|
|
27
|
+
A lightweight CLI framework. Install once, put commands in each project's `commands/` folder.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install rhythmos-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
In any project:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
rhythmos-cli make hello -d "Say hello to someone"
|
|
41
|
+
# edit commands/hello.py
|
|
42
|
+
rhythmos-cli hello Alice
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`rhythmos` is an optional shorthand for the same CLI.
|
|
46
|
+
|
|
47
|
+
`make` and `list` are built-in system commands. If you define a user command with the same name, yours wins and a warning is printed.
|
|
48
|
+
|
|
49
|
+
Commands are loaded from `./commands` (cwd), or from `RHYTHMOS_COMMANDS` if set.
|
|
50
|
+
|
|
51
|
+
## Write a command
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from rhythmos import BaseCommand
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HelloCommand(BaseCommand):
|
|
58
|
+
def get_description(self):
|
|
59
|
+
return "Say hello to someone"
|
|
60
|
+
|
|
61
|
+
def add_arguments(self):
|
|
62
|
+
self.parser.add_argument("name", help="Name of the person to greet")
|
|
63
|
+
|
|
64
|
+
def handle(self, name):
|
|
65
|
+
print(f"Hello, {name}!")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
File `commands/foo_bar.py` → `rhythmos-cli foo-bar`.
|
|
69
|
+
|
|
70
|
+
See `examples/commands/hello.py` for a ready example.
|
|
71
|
+
|
|
72
|
+
## Develop this package
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m venv .venv
|
|
76
|
+
source .venv/bin/activate
|
|
77
|
+
pip install -e .
|
|
78
|
+
RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
|
|
79
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Rhythmos CLI
|
|
2
|
+
|
|
3
|
+
A lightweight CLI framework. Install once, put commands in each project's `commands/` folder.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install rhythmos-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
In any project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rhythmos-cli make hello -d "Say hello to someone"
|
|
17
|
+
# edit commands/hello.py
|
|
18
|
+
rhythmos-cli hello Alice
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`rhythmos` is an optional shorthand for the same CLI.
|
|
22
|
+
|
|
23
|
+
`make` and `list` are built-in system commands. If you define a user command with the same name, yours wins and a warning is printed.
|
|
24
|
+
|
|
25
|
+
Commands are loaded from `./commands` (cwd), or from `RHYTHMOS_COMMANDS` if set.
|
|
26
|
+
|
|
27
|
+
## Write a command
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from rhythmos import BaseCommand
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HelloCommand(BaseCommand):
|
|
34
|
+
def get_description(self):
|
|
35
|
+
return "Say hello to someone"
|
|
36
|
+
|
|
37
|
+
def add_arguments(self):
|
|
38
|
+
self.parser.add_argument("name", help="Name of the person to greet")
|
|
39
|
+
|
|
40
|
+
def handle(self, name):
|
|
41
|
+
print(f"Hello, {name}!")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
File `commands/foo_bar.py` → `rhythmos-cli foo-bar`.
|
|
45
|
+
|
|
46
|
+
See `examples/commands/hello.py` for a ready example.
|
|
47
|
+
|
|
48
|
+
## Develop this package
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m venv .venv
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
pip install -e .
|
|
54
|
+
RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
|
|
55
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from rhythmos import BaseCommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HelloCommand(BaseCommand):
|
|
5
|
+
def get_description(self):
|
|
6
|
+
return "Say hello to someone"
|
|
7
|
+
|
|
8
|
+
def add_arguments(self):
|
|
9
|
+
self.parser.add_argument("name", help="Name of the person to greet")
|
|
10
|
+
|
|
11
|
+
def handle(self, name):
|
|
12
|
+
print(f"Hello, {name}! Welcome to your custom CLI.")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rhythmos-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight CLI framework with project-local auto-discovered commands"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "mbela imm", email = "mbela.immongault@globalnext.rocks" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["cli", "argparse", "commands"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://gitlab.com/JoanMbela/rhythmos-cli"
|
|
32
|
+
Repository = "https://gitlab.com/JoanMbela/rhythmos-cli"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
rhythmos-cli = "rhythmos.cli:main"
|
|
36
|
+
rhythmos = "rhythmos.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/rhythmos"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = [
|
|
43
|
+
"/src",
|
|
44
|
+
"/examples",
|
|
45
|
+
"/README.md",
|
|
46
|
+
"/LICENCE",
|
|
47
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseCommand:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.parser = argparse.ArgumentParser(description=self.get_description())
|
|
7
|
+
self.add_arguments()
|
|
8
|
+
|
|
9
|
+
def get_description(self):
|
|
10
|
+
return "A command for the CLI"
|
|
11
|
+
|
|
12
|
+
def add_arguments(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def handle(self, *args, **options):
|
|
16
|
+
raise NotImplementedError("You must implement the handle method.")
|
|
17
|
+
|
|
18
|
+
def run(self, namespace):
|
|
19
|
+
self.handle(**vars(namespace))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from rhythmos.base_command import BaseCommand
|
|
5
|
+
from rhythmos.discovery import commands_dir, discover_commands, invoked_as
|
|
6
|
+
from rhythmos.system_commands import SYSTEM_COMMANDS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_commands() -> dict[str, type[BaseCommand]]:
|
|
10
|
+
"""Merge system + user commands. User commands win on name clashes."""
|
|
11
|
+
user = discover_commands()
|
|
12
|
+
for name in user:
|
|
13
|
+
if name in SYSTEM_COMMANDS:
|
|
14
|
+
print(
|
|
15
|
+
f"Warning: user command '{name}' overrides system command '{name}'",
|
|
16
|
+
file=sys.stderr,
|
|
17
|
+
)
|
|
18
|
+
return {**SYSTEM_COMMANDS, **user}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> None:
|
|
22
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
23
|
+
cli = invoked_as()
|
|
24
|
+
|
|
25
|
+
parser = argparse.ArgumentParser(prog=cli, description="Rhythmos CLI")
|
|
26
|
+
subparsers = parser.add_subparsers(
|
|
27
|
+
dest="command", help="Command to execute", required=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
commands = resolve_commands()
|
|
31
|
+
command_instances = {}
|
|
32
|
+
|
|
33
|
+
for command_name, command_class in commands.items():
|
|
34
|
+
command_instance = command_class()
|
|
35
|
+
command_instances[command_name] = command_instance
|
|
36
|
+
subparsers.add_parser(
|
|
37
|
+
command_name,
|
|
38
|
+
help=command_instance.get_description(),
|
|
39
|
+
parents=[command_instance.parser],
|
|
40
|
+
conflict_handler="resolve",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
command_name = args.command
|
|
46
|
+
|
|
47
|
+
if command_name not in command_instances:
|
|
48
|
+
print(f"Command '{command_name}' not found.")
|
|
49
|
+
print(f"Looking in: {commands_dir()}")
|
|
50
|
+
print(f"Create one with: {cli} make <name>")
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
|
|
53
|
+
args_dict = vars(args)
|
|
54
|
+
args_dict.pop("command", None)
|
|
55
|
+
command_instances[command_name].run(argparse.Namespace(**args_dict))
|
|
56
|
+
|
|
57
|
+
except KeyboardInterrupt:
|
|
58
|
+
print("\nOPERATION CANCELLED.\n")
|
|
59
|
+
raise SystemExit(130) from None
|
|
60
|
+
|
|
61
|
+
except SystemExit:
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"\nERROR: {str(e)}\n")
|
|
66
|
+
raise SystemExit(1) from e
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rhythmos.base_command import BaseCommand
|
|
9
|
+
|
|
10
|
+
COMMANDS_DIR_ENV = "RHYTHMOS_COMMANDS"
|
|
11
|
+
DEFAULT_COMMANDS_DIR = "commands"
|
|
12
|
+
SKIP_STEMS = {"__init__", "base_command"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def commands_dir() -> Path:
|
|
16
|
+
override = os.environ.get(COMMANDS_DIR_ENV)
|
|
17
|
+
if override:
|
|
18
|
+
return Path(override).expanduser().resolve()
|
|
19
|
+
return (Path.cwd() / DEFAULT_COMMANDS_DIR).resolve()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def invoked_as() -> str:
|
|
23
|
+
name = Path(sys.argv[0]).name
|
|
24
|
+
return name if name else "rhythmos-cli"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_module(path: Path):
|
|
28
|
+
module_name = f"rhythmos_user_commands.{path.stem}"
|
|
29
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
30
|
+
if spec is None or spec.loader is None:
|
|
31
|
+
raise ImportError(f"Could not load {path}")
|
|
32
|
+
module = importlib.util.module_from_spec(spec)
|
|
33
|
+
sys.modules[module_name] = module
|
|
34
|
+
spec.loader.exec_module(module)
|
|
35
|
+
return module
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def discover_commands(directory: Path | None = None) -> dict[str, type[BaseCommand]]:
|
|
39
|
+
directory = directory or commands_dir()
|
|
40
|
+
found: dict[str, type[BaseCommand]] = {}
|
|
41
|
+
|
|
42
|
+
if not directory.is_dir():
|
|
43
|
+
return found
|
|
44
|
+
|
|
45
|
+
for file in sorted(directory.glob("*.py")):
|
|
46
|
+
if file.stem in SKIP_STEMS or file.stem.startswith("_"):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
module = _load_module(file)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Warning: Could not load command from {file}: {e}", file=sys.stderr)
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
for _, obj in inspect.getmembers(module):
|
|
56
|
+
if (
|
|
57
|
+
inspect.isclass(obj)
|
|
58
|
+
and issubclass(obj, BaseCommand)
|
|
59
|
+
and obj is not BaseCommand
|
|
60
|
+
):
|
|
61
|
+
found[file.stem.replace("_", "-")] = obj
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
return found
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_snake(name: str) -> str:
|
|
68
|
+
name = name.replace("-", "_")
|
|
69
|
+
name = re.sub(r"(?<!^)(?=[A-Z])", "_", name)
|
|
70
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", name).lower().strip("_")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_class_name(snake: str) -> str:
|
|
74
|
+
return "".join(part.capitalize() for part in snake.split("_") if part)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def scaffold_command(name: str, description: str = "A command for the CLI") -> Path:
|
|
78
|
+
directory = commands_dir()
|
|
79
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
snake = to_snake(name)
|
|
82
|
+
if not snake:
|
|
83
|
+
raise ValueError("Command name is empty")
|
|
84
|
+
|
|
85
|
+
target = directory / f"{snake}.py"
|
|
86
|
+
if target.exists():
|
|
87
|
+
raise FileExistsError(f"Command already exists: {target}")
|
|
88
|
+
|
|
89
|
+
stub = Path(__file__).parent / "stubs" / "command.py.stub"
|
|
90
|
+
contents = stub.read_text(encoding="utf-8")
|
|
91
|
+
contents = contents.replace("{{ClassName}}", to_class_name(snake))
|
|
92
|
+
contents = contents.replace("{{description}}", description)
|
|
93
|
+
target.write_text(contents, encoding="utf-8")
|
|
94
|
+
return target
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from rhythmos import BaseCommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class {{ClassName}}Command(BaseCommand):
|
|
5
|
+
def get_description(self):
|
|
6
|
+
return "{{description}}"
|
|
7
|
+
|
|
8
|
+
def add_arguments(self):
|
|
9
|
+
# self.parser.add_argument("name", help="...")
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def handle(self, **options):
|
|
13
|
+
raise NotImplementedError("Implement this command.")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from rhythmos.base_command import BaseCommand
|
|
2
|
+
from rhythmos.system_commands.list import ListCommand
|
|
3
|
+
from rhythmos.system_commands.make import MakeCommand
|
|
4
|
+
|
|
5
|
+
SYSTEM_COMMANDS: dict[str, type[BaseCommand]] = {
|
|
6
|
+
"make": MakeCommand,
|
|
7
|
+
"list": ListCommand,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
__all__ = ["SYSTEM_COMMANDS", "MakeCommand", "ListCommand"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from rhythmos.base_command import BaseCommand
|
|
2
|
+
from rhythmos.discovery import commands_dir, discover_commands
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ListCommand(BaseCommand):
|
|
6
|
+
"""System command: list system and user commands."""
|
|
7
|
+
|
|
8
|
+
def get_description(self):
|
|
9
|
+
return "List all available commands"
|
|
10
|
+
|
|
11
|
+
def handle(self):
|
|
12
|
+
from rhythmos.system_commands import SYSTEM_COMMANDS
|
|
13
|
+
|
|
14
|
+
user = discover_commands()
|
|
15
|
+
merged = {**SYSTEM_COMMANDS, **user}
|
|
16
|
+
print(f"Commands directory: {commands_dir()}")
|
|
17
|
+
print()
|
|
18
|
+
for name in sorted(merged):
|
|
19
|
+
origin = "user" if name in user else "system"
|
|
20
|
+
description = merged[name]().get_description()
|
|
21
|
+
print(f" {name:<16} [{origin:<6}] {description}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from rhythmos.base_command import BaseCommand
|
|
2
|
+
from rhythmos.discovery import invoked_as, scaffold_command
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MakeCommand(BaseCommand):
|
|
6
|
+
"""System command: scaffold a user command in the project."""
|
|
7
|
+
|
|
8
|
+
def get_description(self):
|
|
9
|
+
return "Create a command in this project's commands/"
|
|
10
|
+
|
|
11
|
+
def add_arguments(self):
|
|
12
|
+
self.parser.add_argument("name", help="Command name (snake or kebab case)")
|
|
13
|
+
self.parser.add_argument(
|
|
14
|
+
"-d",
|
|
15
|
+
"--description",
|
|
16
|
+
default="A command for the CLI",
|
|
17
|
+
help="Short help text for the command",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def handle(self, name, description="A command for the CLI"):
|
|
21
|
+
cli = invoked_as()
|
|
22
|
+
path = scaffold_command(name, description)
|
|
23
|
+
kebab = path.stem.replace("_", "-")
|
|
24
|
+
print(f"Created {path}")
|
|
25
|
+
print(f"Run it with: {cli} {kebab}")
|