From ed084c123229ae029df017af55ef02d8556b1d3f Mon Sep 17 00:00:00 2001 From: rizzoMartin Date: Tue, 26 May 2026 18:56:14 +0200 Subject: [PATCH 1/5] Modify cli.py with the design recommendations and fix exit with enter --- skillware/cli.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/skillware/cli.py b/skillware/cli.py index 7ebe896..9e7dce0 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -10,7 +10,7 @@ ID_STYLE = "#B5EAD7" # mint - skill ID column BORDER_STYLE = "#C7CEEA" # lavender - table border -SPLASH_STYLE = "#C7CEEA" # lavender - swillware splash color +SPLASH_STYLE = "#C7CEEA" # lavender - sKillware splash color MENU_STYLE = "#FFDAC1" # peach - menu category @@ -41,7 +41,7 @@ def _short_description(data: Dict[str, Any], max_len: int = 80) -> str: """Return short_description if present, else first sentence of description truncated.""" short = data.get("short_description", "").strip() if short: - return short[:max_len] + ("..." if len(short) > max_len else "") + return short[:max_len] + ("…" if len(short) > max_len else "") desc = data.get("description", "").strip() @@ -53,7 +53,7 @@ def _short_description(data: Dict[str, Any], max_len: int = 80) -> str: desc = desc[: idx + 1] break - return desc[:max_len] + ("..." if len(desc) > max_len else "") + return desc[:max_len] + ("…" if len(desc) > max_len else "") def _discover_skills( @@ -130,15 +130,15 @@ def cmd_list( return table = Table( - box=box.SIMPLE_HEAVY, border_style=BORDER_STYLE, header_style=TABLE_STYLE + box=box.SIMPLE_HEAVY, border_style=BORDER_STYLE, header_style=TABLE_STYLE, expand=True ) - table.add_column("ID", style=ID_STYLE, no_wrap=True) - table.add_column("VERSION", style="dim") - table.add_column("CATEGORY", style=CATEGORY_STYLE) - table.add_column("ISSUER", style="dim") - table.add_column("DESCRIPTION") - table.add_column("REQUIREMENTS", style="dim") + table.add_column("ID", style=ID_STYLE, no_wrap=True, ratio=2) + table.add_column("VERSION", style="dim", no_wrap=True, ratio=1) + table.add_column("CATEGORY", style=CATEGORY_STYLE, no_wrap=True, ratio=1) + table.add_column("ISSUER", style="dim", no_wrap=True, ratio=1) + table.add_column("DESCRIPTION", ratio=3) + table.add_column("REQUIREMENTS", style="dim", ratio=2) for skill in skills: table.add_row( @@ -152,6 +152,11 @@ def cmd_list( console.print(table) +def _print_menu(console, menu) -> None: + for num, name, desc in menu: + console.print(f" [{num}] {name:<20}— {desc}", style=MENU_STYLE) + + console.print() def cmd_interactive(console=None, parser=None) -> None: """Launch ASCII splash screen and interactive menu.""" @@ -184,23 +189,20 @@ def cmd_interactive(console=None, parser=None) -> None: console.print(Text(splash, style=SPLASH_STYLE)) console.print( Text( - f" Skill Management Framework - v{version}\n", + f" Skill Management Framework — v{version}", style=f"dim {SPLASH_STYLE}", ) ) + console.print(Text(" https://skillware.site · https://github.com/arpahls/skillware\n", style=f"dim {SPLASH_STYLE}")) + menu = [ ("1", "list", "discover and display all locally installed skills"), - ("2", "paths", "show and repair skill directory resolution paths"), - ("3", "test", "run test_skill.py for one or all skills"), + ("2", "paths (soon, #81)", "show and repair skill directory resolution paths"), + ("3", "test (soon, #83)", "run test_skill.py for one or all skills"), ("4", "help", "usage guide for any command"), ] - for num, name, desc in menu: - console.print(f" [{num}] {name:<10}— {desc}", style=MENU_STYLE) - - console.print() - commands = { "1": "list", "list": "list", @@ -212,6 +214,8 @@ def cmd_interactive(console=None, parser=None) -> None: "help": "help", } + _print_menu(console, menu) + while True: try: choice = input(" > ").strip().lower() @@ -219,14 +223,14 @@ def cmd_interactive(console=None, parser=None) -> None: console.print("\n Bye.", style="dim") return - if choice in ("q", ""): + if choice == "q": console.print(" Bye.", style="dim") return command = commands.get(choice) if command == "list": - cmd_list() + cmd_list(console=console) elif command in ("paths", "test"): console.print( f" '{command}' is not yet implemented. Coming in a future release.", @@ -243,6 +247,7 @@ def cmd_interactive(console=None, parser=None) -> None: console.print(f" Unknown command: '{choice}'", style="dim #FF9AA2") console.print() + _print_menu(console, menu) def main() -> None: From eff6d86fbbde241f20657186aad4d31a9e05df12 Mon Sep 17 00:00:00 2001 From: rizzoMartin Date: Tue, 26 May 2026 19:15:34 +0200 Subject: [PATCH 2/5] Modify documentation to mention short_description and skillware use --- README.md | 2 +- docs/contributing/ai_native_workflow.md | 1 + templates/python_skill/README.md | 2 +- templates/python_skill/manifest.yaml | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e35855e..449ca6b 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ pip install "skillware[cli]" skillware list ``` -This prints a table of all locally available skills and confirms the install and path resolution are working. +This prints a table of all locally available skills and confirms the install and path resolution are working. Running `skillware` with no arguments opens the interactive menu. ### 3. Configuration diff --git a/docs/contributing/ai_native_workflow.md b/docs/contributing/ai_native_workflow.md index 0b434e0..eadf704 100644 --- a/docs/contributing/ai_native_workflow.md +++ b/docs/contributing/ai_native_workflow.md @@ -253,6 +253,7 @@ Complete the checklist that matches your issue during Stage 5. - [ ] `skills///` exists with full bundle - [ ] `manifest.yaml`: `name`, `version`, `description`, `parameters`, `constitution`, real `issuer` +- [ ] Optional: `short_description` field (~80 chars) for a concise one-line summary in `skillware list` - [ ] `skill.py`: deterministic, JSON-serializable returns, safe error handling - [ ] `instructions.md`: when to use, how to interpret output, limitations - [ ] `card.json`: `issuer` matches manifest diff --git a/templates/python_skill/README.md b/templates/python_skill/README.md index 2cf7ec1..0134f07 100644 --- a/templates/python_skill/README.md +++ b/templates/python_skill/README.md @@ -6,7 +6,7 @@ Starter bundle under `skills///`. Copy this template from 1. **Rename** the folder to match your skill ID (e.g. `skills/finance/my_skill`). 2. **Packaging**: Add empty `__init__.py` files in `skills//` (new categories only) and in your skill folder so PyPI wheels include the full bundle. No `pyproject.toml` changes per skill. -3. **`manifest.yaml`**: Set real `name`, `version`, `description`, `parameters`, `constitution`, and `issuer` (`name` + `email` required; `github` / `org` optional). +3. **`manifest.yaml`**: Set real `name`, `version`, `description`, `short_description`, `parameters`, `constitution`, and `issuer` (`name` + `email` required; `github` / `org` optional). 4. **`skill.py`**: Implement deterministic logic; no LLM-generated code in the skill body. 5. **`instructions.md`**: Tell the agent when and how to use the tool. 6. **`card.json`**: Mirror `issuer` from the manifest; customize UI fields. diff --git a/templates/python_skill/manifest.yaml b/templates/python_skill/manifest.yaml index 47e4605..056c485 100644 --- a/templates/python_skill/manifest.yaml +++ b/templates/python_skill/manifest.yaml @@ -1,6 +1,7 @@ name: "my-awesome-skill" version: "0.1.0" description: "A short description of what this skill does." +short_description: "One-line summary for skillware list (~80 chars)." issuer: name: Your Name email: you@example.com From 8606a41853b93a486a9090b45e5a2e96871f25f7 Mon Sep 17 00:00:00 2001 From: rizzoMartin Date: Tue, 26 May 2026 19:16:20 +0200 Subject: [PATCH 3/5] Add test to assert input 1 and delete test to exit with '' --- tests/test_cli.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f692ebc..d71788a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -155,11 +155,11 @@ def test_short_description_uses_short_description_field(): def test_short_description_truncates_at_80_chars(): - """short_description longer than 80 chars should be truncated with ...""" + """short_description longer than 80 chars should be truncated with …""" data = {"short_description": "A" * 90} result = _short_description(data) - assert len(result) == 83 # 80 + "..." - assert result.endswith("...") + assert len(result) == 81 # 80 + "…" + assert result.endswith("…") def test_short_description_falls_back_to_first_sentence(): @@ -184,24 +184,37 @@ def test_cmd_interactive_exits_on_q(monkeypatch): assert "Bye" in buf.getvalue() -def test_cmd_interactive_exits_on_empty(monkeypatch): - """Pressing enter without input should exit cleanly.""" +def test_cmd_interactive_unknown_command(monkeypatch): + """Unknown command should print error then exit on q.""" import io from rich.console import Console - monkeypatch.setattr("builtins.input", lambda _: "") + responses = iter(["unknown_cmd", "q"]) + monkeypatch.setattr("builtins.input", lambda _: next(responses)) buf = io.StringIO() cmd_interactive(console=Console(file=buf, force_terminal=False)) - assert "Bye" in buf.getvalue() - + assert "Unknown command" in buf.getvalue() -def test_cmd_interactive_unknown_command(monkeypatch): - """Unknown command should print error then exit on q.""" +def test_cmd_interactive_list_dispatch(tmp_path, monkeypatch): + """Entering 1 or list should dispatch to cmd_list.""" import io from rich.console import Console - responses = iter(["unknown_cmd", "q"]) + skill_dir = tmp_path / "office" / "test_skill" + skill_dir.mkdir(parents=True) + (skill_dir / "skill.py").touch() + (skill_dir / "manifest.yaml").write_text( + "name: test_skill\nversion: 0.1.0\ndescription: Test.\n" + "short_description: Test skill.\n" + ) + + responses = iter(["1", "q"]) monkeypatch.setattr("builtins.input", lambda _: next(responses)) + monkeypatch.chdir(tmp_path) + buf = io.StringIO() - cmd_interactive(console=Console(file=buf, force_terminal=False)) - assert "Unknown command" in buf.getvalue() + console = Console(file=buf, force_terminal=False) + cmd_interactive(console=console) + + output = buf.getvalue() + assert "test_skill" in output or "No skills found" in output From 8e5ea77bc9213856f9f82a51506df1f4ef3242a5 Mon Sep 17 00:00:00 2001 From: rizzoMartin Date: Tue, 26 May 2026 19:17:15 +0200 Subject: [PATCH 4/5] Formatting and linting --- skillware/cli.py | 14 ++++++++++++-- tests/test_cli.py | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/skillware/cli.py b/skillware/cli.py index 9e7dce0..c2e6488 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -130,7 +130,10 @@ def cmd_list( return table = Table( - box=box.SIMPLE_HEAVY, border_style=BORDER_STYLE, header_style=TABLE_STYLE, expand=True + box=box.SIMPLE_HEAVY, + border_style=BORDER_STYLE, + header_style=TABLE_STYLE, + expand=True, ) table.add_column("ID", style=ID_STYLE, no_wrap=True, ratio=2) @@ -152,12 +155,14 @@ def cmd_list( console.print(table) + def _print_menu(console, menu) -> None: for num, name, desc in menu: console.print(f" [{num}] {name:<20}— {desc}", style=MENU_STYLE) console.print() + def cmd_interactive(console=None, parser=None) -> None: """Launch ASCII splash screen and interactive menu.""" try: @@ -194,7 +199,12 @@ def cmd_interactive(console=None, parser=None) -> None: ) ) - console.print(Text(" https://skillware.site · https://github.com/arpahls/skillware\n", style=f"dim {SPLASH_STYLE}")) + console.print( + Text( + " https://skillware.site · https://github.com/arpahls/skillware\n", + style=f"dim {SPLASH_STYLE}", + ) + ) menu = [ ("1", "list", "discover and display all locally installed skills"), diff --git a/tests/test_cli.py b/tests/test_cli.py index d71788a..ba6cb49 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -195,6 +195,7 @@ def test_cmd_interactive_unknown_command(monkeypatch): cmd_interactive(console=Console(file=buf, force_terminal=False)) assert "Unknown command" in buf.getvalue() + def test_cmd_interactive_list_dispatch(tmp_path, monkeypatch): """Entering 1 or list should dispatch to cmd_list.""" import io From 0ba95b6b17ac298bd1056ed2ab15bb4b4a3ce00e Mon Sep 17 00:00:00 2001 From: rizzoMartin Date: Wed, 27 May 2026 18:29:03 +0200 Subject: [PATCH 5/5] Update cli.md, fix typo, fix test --- docs/usage/cli.md | 15 +++++++++++++-- skillware/cli.py | 2 +- tests/test_cli.py | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/usage/cli.md b/docs/usage/cli.md index fbae64e..76d1151 100644 --- a/docs/usage/cli.md +++ b/docs/usage/cli.md @@ -18,8 +18,19 @@ interactive numbered menu: skillware -The menu accepts both number input (`1`) and command name (`list`). Press `q` -or Enter to exit cleanly. +The splash displays the current version and links to the project site and +repository. The menu accepts both number input (`1`) and command name (`list`). +After each command completes, the menu re-prints automatically. Press `q` or +`Ctrl+C` to exit. + +Available commands: + +| Input | Command | Status | +| :--- | :--- | :--- | +| `1` / `list` | List all locally installed skills | Available | +| `2` / `paths` | Show and repair skill directory resolution paths | Coming in #81 | +| `3` / `test` | Run test_skill.py for one or all skills | Coming in #83 | +| `4` / `help` | Print usage information | Available | ## Commands diff --git a/skillware/cli.py b/skillware/cli.py index c2e6488..9d36295 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -10,7 +10,7 @@ ID_STYLE = "#B5EAD7" # mint - skill ID column BORDER_STYLE = "#C7CEEA" # lavender - table border -SPLASH_STYLE = "#C7CEEA" # lavender - sKillware splash color +SPLASH_STYLE = "#C7CEEA" # lavender - skillware splash color MENU_STYLE = "#FFDAC1" # peach - menu category diff --git a/tests/test_cli.py b/tests/test_cli.py index ba6cb49..dc1af46 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -218,4 +218,4 @@ def test_cmd_interactive_list_dispatch(tmp_path, monkeypatch): cmd_interactive(console=console) output = buf.getvalue() - assert "test_skill" in output or "No skills found" in output + assert "test_skill" in output