diff --git a/README.md b/README.md index 6ccea5b..c0b40e1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Claude Agent Skills are great - but they only work in Claude. What about Cursor, | 50+ skills, "which one was for PR reviews?" | Search by keyword - finds it in milliseconds | [Scale →](#scale-progressive-disclosure) | | Long debugging session, context running low | Skills load on-demand - not all upfront | [Scale →](#scale-progressive-disclosure) | | Found an awesome skill on GitHub | `skillport add ` - ready to use in seconds | [CLI →](#manage-cli) | -| Don't want to set up MCP | CLI works standalone - `init`, `add`, `sync` to AGENTS.md | [CLI Mode →](#cli-mode) | +| Don't want to set up MCP | CLI works standalone - `init`, `add`, `doc` to AGENTS.md | [CLI Mode →](#cli-mode) |
@@ -77,7 +77,7 @@ uv tool install skillport # or: pip install skillport ``` -Enables `add`, `remove`, `lint`, `search`, `show`, and `sync` (export to AGENTS.md for non-MCP agents). +Enables `add`, `remove`, `lint`, `search`, `show`, and `doc` (generate AGENTS.md for non-MCP agents). ### 2. Add Skills @@ -183,7 +183,7 @@ The agent will: **For:** Coding agents with shell commands (Cursor, Windsurf, Cline, Copilot, Codex, etc.) -Skills sync to AGENTS.md and load via `skillport show`. No MCP configuration needed. +Skills are documented in AGENTS.md and load via `skillport show`. No MCP configuration needed. ```bash # 1. Install @@ -192,7 +192,7 @@ uv tool install skillport # 2. Initialize your project (in your project directory) skillport init # → Select skills directory and instruction files interactively -# → Creates .skillportrc, syncs skills to AGENTS.md +# → Creates .skillportrc, generates skills to AGENTS.md # 3. Add skills (uses skills_dir from .skillportrc) skillport add hello-world @@ -227,8 +227,8 @@ Tools for progressive skill loading: ```bash skillport init # Initialize project (.skillportrc, AGENTS.md) -skillport sync # Update AGENTS.md when skills change -skillport sync --all # Update all instruction files in .skillportrc +skillport doc # Update AGENTS.md when skills change +skillport doc --all # Update all instruction files in .skillportrc ``` **Skill Management:** diff --git a/guide/cli.md b/guide/cli.md index b8d5e95..2c9460b 100644 --- a/guide/cli.md +++ b/guide/cli.md @@ -14,7 +14,7 @@ SkillPort provides a command-line interface for managing [Agent Skills](https:// - [remove](#skillport-remove) - Remove skills - [lint](#skillport-lint) - Validate skills - [serve](#skillport-serve) - Start MCP server - - [sync](#skillport-sync) - Sync to AGENTS.md + - [doc](#skillport-doc) - Generate AGENTS.md - [Exit Codes](#exit-codes) - [Configuration](#configuration) @@ -44,7 +44,7 @@ Precedence: CLI flag > environment variable (`SKILLPORT_SKILLS_DIR` / `SKILLPORT ### skillport init -Initialize SkillPort for a project. Creates configuration and syncs skills to instruction files. +Initialize SkillPort for a project. Creates configuration and generates skills to instruction files. ```bash skillport init [options] @@ -122,7 +122,7 @@ instructions: - GEMINI.md ``` -The `instructions` list is used by `skillport sync --all` to update all files at once. +The `instructions` list is used by `skillport doc --all` to update all files at once. --- @@ -525,12 +525,12 @@ skillport serve --- -### skillport sync +### skillport doc -Sync installed skills to instruction files (AGENTS.md, etc.). +Generate skill documentation for instruction files (AGENTS.md, etc.). ```bash -skillport sync [options] +skillport doc [options] ``` #### Options @@ -558,32 +558,32 @@ skillport sync [options] #### Examples ```bash -# Sync all skills to ./AGENTS.md -skillport sync +# Generate skill docs to ./AGENTS.md +skillport doc # Update all instruction files from .skillportrc -skillport sync --all +skillport doc --all -# Sync to specific file -skillport sync -o .claude/AGENTS.md +# Generate to specific file +skillport doc -o .claude/AGENTS.md # Force overwrite without confirmation -skillport sync -f +skillport doc -f # Filter by category -skillport sync --category development,testing +skillport doc --category development,testing # Filter by skill IDs -skillport sync --skills pdf,code-review +skillport doc --skills pdf,code-review # Use markdown format (no XML tags) -skillport sync --format markdown +skillport doc --format markdown # Generate for MCP-enabled agents -skillport sync --mode mcp +skillport doc --mode mcp # Replace entire file instead of appending -skillport sync --replace +skillport doc --replace ``` #### Output Format diff --git a/src/skillport/interfaces/cli/app.py b/src/skillport/interfaces/cli/app.py index e3c1774..a0aeb1f 100644 --- a/src/skillport/interfaces/cli/app.py +++ b/src/skillport/interfaces/cli/app.py @@ -8,7 +8,7 @@ - remove: Uninstall skills - lint: Validate skill definitions - serve: Start MCP server -- sync: Sync skills to AGENTS.md for non-MCP agents +- doc: Generate skill documentation for AGENTS.md """ from pathlib import Path @@ -26,7 +26,7 @@ from .commands.list import list_cmd from .commands.lint import lint from .commands.serve import serve -from .commands.sync import sync +from .commands.doc import doc from .commands.init import init from .theme import VERSION, console from .auto_index import should_auto_reindex @@ -182,14 +182,14 @@ def main( )(serve) app.command( - "sync", - help="Sync skills to AGENTS.md for non-MCP agents.\n\n" + "doc", + help="Generate skill documentation for AGENTS.md.\n\n" "[bold]Examples:[/bold]\n\n" - " skillport sync\n\n" - " skillport sync --all\n\n" - " skillport sync -o .claude/AGENTS.md\n\n" - " skillport sync --category development,testing", -)(sync) + " skillport doc\n\n" + " skillport doc --all\n\n" + " skillport doc -o .claude/AGENTS.md\n\n" + " skillport doc --category development,testing", +)(doc) def run(): diff --git a/src/skillport/interfaces/cli/commands/sync.py b/src/skillport/interfaces/cli/commands/doc.py similarity index 90% rename from src/skillport/interfaces/cli/commands/sync.py rename to src/skillport/interfaces/cli/commands/doc.py index 8bd7666..fef5d19 100644 --- a/src/skillport/interfaces/cli/commands/sync.py +++ b/src/skillport/interfaces/cli/commands/doc.py @@ -1,6 +1,6 @@ -"""Sync installed skills to AGENTS.md for non-MCP agents. +"""Generate skill documentation for AGENTS.md. -Implements SPEC2-CLI Section 3.2: sync コマンド. +Implements SPEC2-CLI Section 3.2: doc コマンド. Generates a skills block that can be embedded in AGENTS.md files. """ @@ -37,7 +37,7 @@ def _truncate_description(desc: str, max_len: int = 50) -> str: ### Workflow -1. **Find a skill** - Check the table below for a skill matching your task +1. **Find a skill** - Check the list below for a skill matching your task 2. **Get instructions** - Run `skillport show ` to load full instructions 3. **Follow the instructions** - Execute the steps using your available tools @@ -91,31 +91,22 @@ def generate_skills_block( """ lines = [MARKER_START] - if format == "xml": - lines.append("") - lines.append("") - # Instructions first (most important for agents) instructions = MCP_INSTRUCTIONS if mode == "mcp" else CLI_INSTRUCTIONS lines.append(instructions) lines.append("") - # Skills table - lines.append("### Available Skills") - lines.append("") - lines.append("| ID | Description | Category |") - lines.append("|----|-------------|----------|") + # Skills list wrapped in tag (xml format only) + if format == "xml": + lines.append("") for skill in skills: skill_id = skill.id - # Clean description (normalize whitespace, escape pipes) + # Clean description (normalize whitespace) desc = " ".join(skill.description.split()) - desc = desc.replace("|", "\\|") - cat = skill.category or "-" - lines.append(f"| {skill_id} | {desc} | {cat} |") + lines.append(f"- `{skill_id}`: {desc}") if format == "xml": - lines.append("") lines.append("") lines.append(MARKER_END) @@ -162,7 +153,7 @@ def update_agents_md( return True -def sync( +def doc( ctx: typer.Context, output: Path = typer.Option( Path("./AGENTS.md"), @@ -170,7 +161,7 @@ def sync( "-o", help="Output file path", ), - sync_all: bool = typer.Option( + doc_all: bool = typer.Option( False, "--all", "-a", @@ -209,7 +200,7 @@ def sync( help="Overwrite without confirmation", ), ): - """Sync skills to AGENTS.md for non-MCP agents.""" + """Generate skill documentation for AGENTS.md.""" # Validate format if format not in ("xml", "markdown"): console.print(f"[error]Invalid format: {format}. Use 'xml' or 'markdown'.[/error]") @@ -254,7 +245,7 @@ def sync( block = generate_skills_block(skills, format=format, mode=mode) # Determine output files - if sync_all: + if doc_all: # Use instruction files from project config if not project_config.instructions: console.print( @@ -282,4 +273,4 @@ def sync( # Update file update_agents_md(out_path, block, append=append) - console.print(f"[success]Synced {len(skills)} skill(s) to {out_path}[/success]") + console.print(f"[success]Generated {len(skills)} skill(s) to {out_path}[/success]") diff --git a/src/skillport/interfaces/cli/commands/init.py b/src/skillport/interfaces/cli/commands/init.py index 05b3c2e..0981fcd 100644 --- a/src/skillport/interfaces/cli/commands/init.py +++ b/src/skillport/interfaces/cli/commands/init.py @@ -12,7 +12,7 @@ from skillport.modules.indexing import build_index from skillport.modules.skills import list_skills from ..context import get_config -from .sync import generate_skills_block, update_agents_md +from .doc import generate_skills_block, update_agents_md from ..theme import console, print_banner # Default choices for interactive mode diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d6e1b95 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +"""Shared pytest fixtures for SkillPort tests.""" + +from pathlib import Path + +import pytest + +from skillport.shared.config import Config + + +@pytest.fixture +def test_config(tmp_path: Path) -> Config: + """Create a Config that uses temporary directories. + + This ensures tests don't pollute ~/.skillport/ with indexes or metadata. + """ + return Config( + skills_dir=tmp_path / "skills", + db_path=tmp_path / "index" / "skills.lancedb", + ) diff --git a/tests/integration/test_cli_commands.py b/tests/integration/test_cli_commands.py index c4497d2..e56e8a6 100644 --- a/tests/integration/test_cli_commands.py +++ b/tests/integration/test_cli_commands.py @@ -4,7 +4,6 @@ """ import json -import shutil from dataclasses import dataclass from pathlib import Path @@ -407,7 +406,14 @@ def test_add_respects_cli_overrides(self, skills_env: SkillsEnv, tmp_path: Path) assert not (skills_env.skills_dir / "hello-world" / "SKILL.md").exists() def test_add_derives_db_and_meta_from_skills_dir(self, tmp_path: Path, monkeypatch): - """When only --skills-dir is指定, db/meta are自動配置される.""" + """When only --skills-dir is given, db/meta paths are derived correctly. + + Note: We override SKILLPORT_HOME to tmp_path to avoid polluting ~/.skillport/. + """ + # Override SKILLPORT_HOME to use tmp_path instead of ~/.skillport + import skillport.shared.config as config_mod + + monkeypatch.setattr(config_mod, "SKILLPORT_HOME", tmp_path / ".skillport") monkeypatch.delenv("SKILLPORT_DB_PATH", raising=False) monkeypatch.delenv("SKILLPORT_SKILLS_DIR", raising=False) monkeypatch.setenv("SKILLPORT_EMBEDDING_PROVIDER", "none") @@ -428,15 +434,15 @@ def test_add_derives_db_and_meta_from_skills_dir(self, tmp_path: Path, monkeypat # Skills placed under the custom skills_dir assert (custom_skills / "hello-world" / "SKILL.md").exists() - # db/meta should be derived via Config(slug) under default base (~/.skillport/indexes//) + # db/meta should be derived via Config(slug) under patched SKILLPORT_HOME cfg = Config(skills_dir=custom_skills) expected_db = cfg.db_path expected_meta = cfg.meta_dir assert expected_db.exists() # meta_dir path should be derived alongside db_path assert expected_meta == expected_db.parent / "meta" - # cleanup derived index/meta to keep test isolated - shutil.rmtree(expected_db.parent, ignore_errors=True) + # Verify paths are under tmp_path, not ~/.skillport + assert str(tmp_path) in str(expected_db) class TestRemoveCommand: @@ -640,16 +646,16 @@ def test_add_local_then_search_finds_skill(self, skills_env: SkillsEnv, tmp_path assert "searchable-skill" in skill_ids -class TestSyncCommand: - """skillport sync tests.""" +class TestDocCommand: + """skillport doc tests.""" - def test_sync_creates_agents_md(self, skills_env: SkillsEnv, tmp_path: Path): - """sync creates AGENTS.md file.""" + def test_doc_creates_agents_md(self, skills_env: SkillsEnv, tmp_path: Path): + """doc creates AGENTS.md file.""" _create_skill(skills_env.skills_dir, "test-skill", "Test description") _rebuild_index(skills_env) output = tmp_path / "AGENTS.md" - result = runner.invoke(app, ["sync", "-o", str(output), "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--force"]) assert result.exit_code == 0 assert output.exists() @@ -658,34 +664,34 @@ def test_sync_creates_agents_md(self, skills_env: SkillsEnv, tmp_path: Path): assert "" in content assert "" in content - def test_sync_xml_format(self, skills_env: SkillsEnv, tmp_path: Path): - """sync --format xml includes tag.""" + def test_doc_xml_format(self, skills_env: SkillsEnv, tmp_path: Path): + """doc --format xml includes tag.""" _create_skill(skills_env.skills_dir, "test-skill") _rebuild_index(skills_env) output = tmp_path / "AGENTS.md" - result = runner.invoke(app, ["sync", "-o", str(output), "--format", "xml", "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--format", "xml", "--force"]) assert result.exit_code == 0 content = output.read_text() assert "" in content assert "" in content - def test_sync_markdown_format(self, skills_env: SkillsEnv, tmp_path: Path): - """sync --format markdown does not include XML tags.""" + def test_doc_markdown_format(self, skills_env: SkillsEnv, tmp_path: Path): + """doc --format markdown does not include XML tags.""" _create_skill(skills_env.skills_dir, "test-skill") _rebuild_index(skills_env) output = tmp_path / "AGENTS.md" - result = runner.invoke(app, ["sync", "-o", str(output), "--format", "markdown", "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--format", "markdown", "--force"]) assert result.exit_code == 0 content = output.read_text() assert "" not in content assert "## SkillPort Skills" in content - def test_sync_with_skills_filter(self, skills_env: SkillsEnv, tmp_path: Path): - """sync --skills filters to specific skills.""" + def test_doc_with_skills_filter(self, skills_env: SkillsEnv, tmp_path: Path): + """doc --skills filters to specific skills.""" _create_skill(skills_env.skills_dir, "skill-a") _create_skill(skills_env.skills_dir, "skill-b") _create_skill(skills_env.skills_dir, "skill-c") @@ -693,7 +699,7 @@ def test_sync_with_skills_filter(self, skills_env: SkillsEnv, tmp_path: Path): output = tmp_path / "AGENTS.md" result = runner.invoke( - app, ["sync", "-o", str(output), "--skills", "skill-a,skill-c", "--force"] + app, ["doc", "-o", str(output), "--skills", "skill-a,skill-c", "--force"] ) assert result.exit_code == 0 @@ -702,8 +708,8 @@ def test_sync_with_skills_filter(self, skills_env: SkillsEnv, tmp_path: Path): assert "skill-c" in content assert "skill-b" not in content - def test_sync_with_category_filter(self, skills_env: SkillsEnv, tmp_path: Path): - """sync --category filters by category.""" + def test_doc_with_category_filter(self, skills_env: SkillsEnv, tmp_path: Path): + """doc --category filters by category.""" # Create skills with different categories skill_a = skills_env.skills_dir / "skill-a" skill_a.mkdir() @@ -720,7 +726,7 @@ def test_sync_with_category_filter(self, skills_env: SkillsEnv, tmp_path: Path): output = tmp_path / "AGENTS.md" result = runner.invoke( - app, ["sync", "-o", str(output), "--category", "dev", "--force"] + app, ["doc", "-o", str(output), "--category", "dev", "--force"] ) assert result.exit_code == 0 @@ -728,33 +734,33 @@ def test_sync_with_category_filter(self, skills_env: SkillsEnv, tmp_path: Path): assert "skill-a" in content assert "skill-b" not in content - def test_sync_no_skills_exits_1(self, skills_env: SkillsEnv, tmp_path: Path): - """sync with no matching skills exits with code 1.""" + def test_doc_no_skills_exits_1(self, skills_env: SkillsEnv, tmp_path: Path): + """doc with no matching skills exits with code 1.""" _rebuild_index(skills_env) # Empty skills output = tmp_path / "AGENTS.md" - result = runner.invoke(app, ["sync", "-o", str(output), "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--force"]) assert result.exit_code == 1 assert "no skills" in result.stdout.lower() - def test_sync_appends_to_existing(self, skills_env: SkillsEnv, tmp_path: Path): - """sync appends to existing file without markers.""" + def test_doc_appends_to_existing(self, skills_env: SkillsEnv, tmp_path: Path): + """doc appends to existing file without markers.""" _create_skill(skills_env.skills_dir, "test-skill") _rebuild_index(skills_env) output = tmp_path / "AGENTS.md" output.write_text("# Existing Content\n\nSome existing text.\n") - result = runner.invoke(app, ["sync", "-o", str(output), "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--force"]) assert result.exit_code == 0 content = output.read_text() assert "# Existing Content" in content assert "test-skill" in content - def test_sync_replaces_existing_block(self, skills_env: SkillsEnv, tmp_path: Path): - """sync replaces existing SkillPort block.""" + def test_doc_replaces_existing_block(self, skills_env: SkillsEnv, tmp_path: Path): + """doc replaces existing SkillPort block.""" _create_skill(skills_env.skills_dir, "new-skill") _rebuild_index(skills_env) @@ -765,7 +771,7 @@ def test_sync_replaces_existing_block(self, skills_env: SkillsEnv, tmp_path: Pat "# Footer\n" ) - result = runner.invoke(app, ["sync", "-o", str(output), "--force"]) + result = runner.invoke(app, ["doc", "-o", str(output), "--force"]) assert result.exit_code == 0 content = output.read_text() @@ -774,14 +780,14 @@ def test_sync_replaces_existing_block(self, skills_env: SkillsEnv, tmp_path: Pat assert "new-skill" in content assert "old content" not in content - def test_sync_invalid_format_exits_1(self, skills_env: SkillsEnv, tmp_path: Path): - """sync --format invalid exits with code 1.""" + def test_doc_invalid_format_exits_1(self, skills_env: SkillsEnv, tmp_path: Path): + """doc --format invalid exits with code 1.""" _create_skill(skills_env.skills_dir, "test-skill") _rebuild_index(skills_env) output = tmp_path / "AGENTS.md" result = runner.invoke( - app, ["sync", "-o", str(output), "--format", "invalid", "--force"] + app, ["doc", "-o", str(output), "--format", "invalid", "--force"] ) assert result.exit_code == 1 diff --git a/tests/unit/test_add_skill_cleanup.py b/tests/unit/test_add_skill_cleanup.py index e139c9a..5bd05da 100644 --- a/tests/unit/test_add_skill_cleanup.py +++ b/tests/unit/test_add_skill_cleanup.py @@ -20,7 +20,10 @@ def test_prefetched_dir_cleanup_after_rename(tmp_path: Path): download_dir.mkdir() prefetched = _write_skill(download_dir, "temp-repo", "renamed-skill") - cfg = Config(skills_dir=tmp_path / "skills") + cfg = Config( + skills_dir=tmp_path / "skills", + db_path=tmp_path / "index" / "skills.lancedb", + ) result = add_skill( "https://github.com/example/repo", diff --git a/tests/unit/test_sync.py b/tests/unit/test_doc.py similarity index 86% rename from tests/unit/test_sync.py rename to tests/unit/test_doc.py index 7e626cc..d80ad00 100644 --- a/tests/unit/test_sync.py +++ b/tests/unit/test_doc.py @@ -1,9 +1,9 @@ -"""Unit tests for sync command (SPEC3 Section 3).""" +"""Unit tests for doc command (SPEC3 Section 3).""" from pathlib import Path -from skillport.interfaces.cli.commands.sync import ( +from skillport.interfaces.cli.commands.doc import ( generate_skills_block, update_agents_md, _truncate_description, @@ -65,15 +65,13 @@ def test_has_available_skills_tag(self): assert "" in result assert "" in result - def test_contains_skill_table(self): - """Output contains markdown table with skill info.""" + def test_contains_skill_list(self): + """Output contains skill list with id and description.""" skills = [ SkillSummary(id="my-skill", name="my-skill", description="My description", category="dev"), ] result = generate_skills_block(skills, format="xml") - assert "| ID | Description | Category |" in result - assert "| my-skill |" in result - assert "| dev |" in result + assert "- `my-skill`: My description" in result def test_has_workflow_instructions(self): """Output has workflow instructions for agents.""" @@ -86,15 +84,15 @@ def test_has_workflow_instructions(self): # Should explain what skills are assert "expert knowledge" in result.lower() or "instructions" in result.lower() - def test_instructions_come_before_table(self): - """Workflow instructions appear before skills table.""" + def test_instructions_come_before_skills_list(self): + """Workflow instructions appear before skills list.""" skills = [ SkillSummary(id="test", name="test", description="Test skill", category="test"), ] result = generate_skills_block(skills, format="xml") workflow_pos = result.find("### Workflow") - table_pos = result.find("### Available Skills") - assert workflow_pos < table_pos, "Workflow should come before skills table" + skills_pos = result.find("") + assert workflow_pos < skills_pos, "Workflow should come before skills list" def test_has_tips_section(self): """Output has tips for agents.""" @@ -106,22 +104,26 @@ def test_has_tips_section(self): assert "{path}" in result def test_multiple_skills(self): - """Multiple skills appear in table.""" + """Multiple skills appear in list.""" skills = [ SkillSummary(id="skill-a", name="skill-a", description="First", category=""), SkillSummary(id="skill-b", name="skill-b", description="Second", category=""), ] result = generate_skills_block(skills, format="xml") - assert "| skill-a |" in result - assert "| skill-b |" in result + assert "- `skill-a`: First" in result + assert "- `skill-b`: Second" in result - def test_empty_category_shows_dash(self): - """Empty category displays as '-'.""" + def test_skills_inside_available_skills_tag(self): + """Skills list is wrapped inside tag.""" skills = [ SkillSummary(id="test", name="test", description="Test", category=""), ] result = generate_skills_block(skills, format="xml") - assert "| - |" in result + # Find positions + start_tag = result.find("") + end_tag = result.find("") + skill_line = result.find("- `test`: Test") + assert start_tag < skill_line < end_tag, "Skill should be inside tag" def test_long_description_not_truncated(self): """Long descriptions are NOT truncated in AGENTS.md.""" @@ -196,6 +198,14 @@ def test_has_header(self): result = generate_skills_block(skills, format="markdown") assert "## SkillPort Skills" in result + def test_has_skill_list(self): + """Markdown format has skill list.""" + skills = [ + SkillSummary(id="test", name="test", description="Test skill", category="test"), + ] + result = generate_skills_block(skills, format="markdown") + assert "- `test`: Test skill" in result + class TestUpdateAgentsMd: """AGENTS.md file update tests.""" @@ -287,11 +297,11 @@ def test_creates_parent_directories(self, tmp_path: Path): assert MARKER_START in content -class TestGenerateSkillsBlockPipeEscape: - """Pipe character escaping in descriptions.""" +class TestGenerateSkillsBlockSpecialChars: + """Special character handling in descriptions.""" - def test_escapes_pipe_in_description(self): - """Pipe characters are escaped in description.""" + def test_pipe_in_description_preserved(self): + """Pipe characters are preserved in list format.""" skills = [ SkillSummary( id="test", @@ -301,18 +311,8 @@ def test_escapes_pipe_in_description(self): ), ] result = generate_skills_block(skills, format="xml") - # Pipe should be escaped as \| - assert "\\|" in result - # Should not break table structure (unescaped pipe) - lines = result.split("\n") - table_lines = [line for line in lines if line.startswith("|")] - for line in table_lines: - # Each table row should have exactly 4 pipes (5 cells for | Name | Desc | Cat |) - if "---" not in line: # Skip separator line - # Count unescaped pipes (not preceded by backslash) - import re - unescaped = len(re.findall(r'(? None: + """Set up test skills directory with hello-world skill.""" + os.makedirs(skills_dir, exist_ok=True) + + # Create hello-world skill from built-in content + hello_world_dir = os.path.join(skills_dir, "hello-world") + os.makedirs(hello_world_dir, exist_ok=True) + + skill_content = BUILTIN_SKILLS.get("hello-world", "") + with open(os.path.join(hello_world_dir, "SKILL.md"), "w") as f: + f.write(skill_content) + async def run_test(): - # Use temp dirs for DB/meta to avoid polluting ~/.skillport + # Use temp dirs for everything to be self-contained with tempfile.TemporaryDirectory() as tmpdir: - server_env["SKILLPORT_DB_PATH"] = os.path.join(tmpdir, "skills.lancedb") + skills_dir = os.path.join(tmpdir, "skills") + db_path = os.path.join(tmpdir, "skills.lancedb") + + # Set up test skills + setup_test_skills(skills_dir) + + # Configure environment for the server + server_env = os.environ.copy() + server_env["SKILLPORT_SKILLS_DIR"] = skills_dir + server_env["SKILLPORT_DB_PATH"] = db_path + server_env["SKILLPORT_EMBEDDING_PROVIDER"] = "none" + server_env["SKILLPORT_LOG_LEVEL"] = "ERROR" # Reduce noise # Define server parameters (stdio = Local mode) # For Remote mode, use: skillport serve --http @@ -61,18 +82,17 @@ async def run_test(): print("\n--- Testing load_skill ---") try: load_result = await session.call_tool("load_skill", arguments={"skill_id": "hello-world"}) - print(f"Load Result: {load_result.content[0].text[:50]}...") # Show first 50 chars + print(f"Load Result: {load_result.content[0].text[:100]}...") except Exception as e: print(f"❌ load_skill failed: {e}") + return # 5. read_skill_file - only available in HTTP mode print("\n--- read_skill_file ---") print("ℹ️ Not available in Local mode (use --http for Remote mode)") - # 6. Test run_skill_command - SKIPPED (disabled by default in Phase 5) - print("\n--- Skipping run_skill_command (disabled by default) ---") - print("\n✅ Verification Complete!") + if __name__ == "__main__": asyncio.run(run_test())