Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta

### Added

- **CLI:** `skillware doctor --install` and `skillware config show` report package install health for duplicate, orphan, or editable-plus-wheel metadata, with opt-out-aware startup advice and repair scripts (#333).

- **Core:** Pluggable secret providers — `SecretProvider`, `EnvSecretProvider`, `MappingSecretProvider`, `CallableSecretProvider`, and `SkillLoader.resolve_env_vars()` inject manifest `env_vars` into `BaseSkill(config=...)` without requiring global `os.environ` mutation (#39).
- **Core:** `BaseSkill.credential()` — config-first credential lookup with `os.environ` fallback for local `.env` workflows (#39).
- **Core:** `SkillContext(secret_provider=...)` resolves credentials on each `execute()` and passes them via `config` (supports ephemeral tokens from custom providers) (#39).
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,30 @@ Registry skills are shipped inside the `skillware` wheel. Per-skill layout uses
- Hand-maintained extras (`dev`, `gemini`, `claude`, `openai`, `bedrock`, `agents`) stay above the generated block in `pyproject.toml`.
- Contributors and CI install skill runtime deps with `pip install -e ".[dev,all]"`; add `[agents]` when running SDK examples locally.

**Editable vs PyPI on the same Python**

Keep **one install mode per interpreter**. Mixing an editable install
(`pip install -e .` from a clone) with a PyPI wheel
(`pip install skillware` / `pip install -U skillware`) on the same Python can
leave orphaned or duplicate `skillware-*.dist-info` metadata. The CLI then
fails to resolve a version and prints `vNone` / `skillware None`.

- Uninstall before switching modes:
- Windows: `py -m pip uninstall skillware -y`
- Unix: `python -m pip uninstall skillware -y`
- Prefer `py -3.13 -m pip ...` on Windows to target the right interpreter.
- If `pip uninstall` fails with `uninstall-no-record-file`, remove the orphan
`skillware/` package dir and stale `skillware-*.dist-info/` dirs from
`site-packages` manually, then reinstall with
`python -m pip install --force-reinstall skillware`.
- Diagnose the current state with `skillware doctor --install`; it reports
duplicate / orphan / editable-plus-wheel conflicts and prints copy-paste fix
commands (exit 0 = healthy, 1 = conflicts).
- To repair a development checkout consistently on either platform, run
`scripts/dev_install.sh` (Unix) or `scripts/dev_install.ps1` (Windows). Both
remove orphan `skillware-*.dist-info` directories before installing
`-e ".[dev,all]"`.

### 6. `docs/skills/<skill_name>.md` (catalog page)

- Human-readable documentation linked from the [Skill Library](docs/skills/README.md).
Expand Down
23 changes: 17 additions & 6 deletions docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ After installation, the `skillware` command is available directly:
skillware
skillware list
skillware doctor
skillware doctor --install
skillware config show
skillware context show
skillware chain list
Expand Down Expand Up @@ -72,17 +73,21 @@ system PATH, or use the `py` launcher:
py -3 -m pip install skillware
py -3 -m skillware list

## Version advisory
## Version and install advisories

On CLI startup, Skillware checks the installed package version **once per process**.
If you are on an **unsupported** release (below `0.3.5`, for example `0.3.4` or `0.2.9`), a single
dim message is printed to stderr suggesting an upgrade to `>= 0.4.7`. Installs in the
`0.3.5`–`0.4.6` band stay silent (no security backports, but no startup spam). Current
supported installs (`0.4.7` and above) stay silent.
If you are on an **unsupported** release (below `0.4.6`), a single dim message is
printed to stderr suggesting an upgrade to `>= 0.5.5`. Releases from `0.4.6`
upward stay silent unless their installation metadata is corrupt.

Skillware also emits one dim advisory when it detects duplicate, orphan, or
editable-plus-wheel package metadata. Run `skillware doctor --install` for the
full report and copy-paste repair commands. `skillware config show` includes a
short install-health block with the installed version and this same pointer.

Library use (`import skillware`, `SkillLoader`) never prints this message.

To disable the check in CI or automation:
To disable both startup advisories in CI or automation:

export SKILLWARE_NO_VERSION_CHECK=1

Expand Down Expand Up @@ -286,6 +291,11 @@ Check whether skills can load in the current environment — manifest **requirem
skillware doctor --category compliance
skillware doctor --skills-root /path/to/my/skills

Check the **install health** of the `skillware` package itself (duplicate,
orphan, or editable-plus-wheel conflicts) with copy-paste fix commands:

skillware doctor --install

#### Arguments and flags

| Input | Description |
Expand All @@ -294,6 +304,7 @@ Check whether skills can load in the current environment — manifest **requirem
| `<category>/<skill_name>` | Diagnose one skill |
| `--category <name>` | Diagnose all skills in a category |
| `--skills-root <path>` | Override the skills directory for discovery and load |
| `--install` | Diagnose the local `skillware` install state and print fix commands (exit 0 = healthy, 1 = conflicts) |

**DEPS** validates manifest `requirements`. **LOAD** imports `skill.py`; skipped (`—`) when **DEPS** fails. **ENVS** checks required manifest `env_vars` via `EnvSecretProvider` (your shell, `.env`, or CI secrets — see [API keys](api_keys.md)). Skills with no `env_vars` show `—`.

Expand Down
17 changes: 17 additions & 0 deletions scripts/dev_install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Rebuild a contributor checkout without leaving stale package metadata behind.
$ErrorActionPreference = 'Stop'

# Locate incomplete metadata that pip cannot uninstall safely and remove only it.
$sitePackages = py -c "import site; print('\n'.join(site.getsitepackages()))"
foreach ($root in $sitePackages -split "`n") {
Get-ChildItem -Path $root -Filter 'skillware-*.dist-info' -Directory -ErrorAction SilentlyContinue |
Where-Object { -not ((Test-Path (Join-Path $_.FullName 'METADATA')) -and (Test-Path (Join-Path $_.FullName 'RECORD'))) } |
ForEach-Object {
Write-Host "Removing orphan metadata: $($_.FullName)"
Remove-Item -Recurse -Force $_.FullName
}
}

# Remove the old distribution before installing the full editable developer set.
py -m pip uninstall skillware -y
py -m pip install -e ".[dev,all]"
21 changes: 21 additions & 0 deletions scripts/dev_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env sh
# Rebuild a contributor checkout without leaving stale package metadata behind.
set -eu

# Delete only incomplete Skillware metadata that pip cannot uninstall safely.
python - <<'PY'
import shutil
import site
from pathlib import Path

for root in map(Path, site.getsitepackages()):
for dist_info in root.glob("skillware-*.dist-info"):
if (dist_info / "METADATA").is_file() and (dist_info / "RECORD").is_file():
continue
print(f"Removing orphan metadata: {dist_info}")
shutil.rmtree(dist_info)
PY

# Remove the old distribution before installing the full editable developer set.
python -m pip uninstall skillware -y || true
python -m pip install -e ".[dev,all]"
106 changes: 103 additions & 3 deletions skillware/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@
list_registry_skill_ids,
resolution_order_summary,
)
from skillware.version_policy import emit_upgrade_advisory, get_installed_version
from skillware.version_policy import (
detect_install_conflicts,
emit_upgrade_advisory,
get_installed_version,
is_version_check_disabled,
)


def _active_theme() -> ThemePalette:
Expand Down Expand Up @@ -116,6 +121,7 @@ def _apply_active_theme() -> None:
("skillware test --category <n>", "test all skills in a category"),
("skillware doctor [id]", "check deps and skill.py import"),
("skillware doctor --category <n>", "diagnose a category"),
("skillware doctor --install", "diagnose package install health"),
],
_DOCS_CLI_LIST,
),
Expand Down Expand Up @@ -1176,6 +1182,11 @@ def cmd_config_show(console=None) -> int:
console.print(f" theme: {config.presentation.theme}", style=MENU_STYLE)
console.print()

# Surface the package state alongside configuration before an invalid
# version reaches the rest of the CLI.
_print_install_health_summary(console)
console.print()

if not config.has_config_files:
console.print(
"No config files found — using legacy resolution "
Expand Down Expand Up @@ -1246,6 +1257,23 @@ def cmd_config_show(console=None) -> int:
return 0


def _print_install_health_summary(console: Console) -> None:
"""Print the installed version and a concise install-health summary."""
installed = get_installed_version()
conflicts = detect_install_conflicts()
version = str(installed) if installed is not None else "unknown"

console.print(Text("install health", style=TABLE_STYLE))
console.print(f" version: {version}", style=MENU_STYLE)
if conflicts:
console.print(
f" status: {len(conflicts)} conflict(s) — run skillware doctor --install",
style=ERROR_STYLE,
)
else:
console.print(" status: ok", style=ID_STYLE)


def _parse_host_vars(pairs: Optional[List[str]]) -> Dict[str, Any]:
host: Dict[str, Any] = {}
if not pairs:
Expand Down Expand Up @@ -1594,13 +1622,21 @@ def cmd_doctor(
skills_root_override: Optional[Path] = None,
skill_id: Optional[str] = None,
category: Optional[str] = None,
install: bool = False,
console=None,
) -> int:
"""Check manifest deps and skill.py import without running execute()."""
"""Check manifest deps and skill.py import without running execute().

With ``install=True``, diagnose the local install state instead and print
fix commands (exit 0 = healthy, 1 = conflicts).
"""
_apply_active_theme()
if console is None:
console = Console(stderr=True)

if install:
return _cmd_doctor_install(console)

skill_ids, error = _resolve_doctor_skill_ids(
skills_root_override=skills_root_override,
skill_id=skill_id,
Expand Down Expand Up @@ -1675,6 +1711,40 @@ def cmd_doctor(
return 1 if failures else 0


def _cmd_doctor_install(console: Console) -> int:
"""Diagnose the local install state and print a copy-paste fix report."""
conflicts = detect_install_conflicts()
installed = get_installed_version()

console.print(
Text(
f"Skillware install version: "
f"{installed if installed is not None else 'unknown'}",
)
)

if not conflicts:
console.print("Install health: ok", style=ID_STYLE)
return 0

console.print(f"Install health: {len(conflicts)} conflict(s)", style=ERROR_STYLE)
console.print()
for idx, conflict in enumerate(conflicts, start=1):
console.print(
f"{idx}. [{conflict.code}] {conflict.summary}",
markup=False,
)
console.print(" Fix (Windows):", style="dim")
for line in conflict.fix_windows.splitlines():
console.print(f" {line}", style="dim")
console.print(" Fix (Unix):", style="dim")
for line in conflict.fix_unix.splitlines():
console.print(f" {line}", style="dim")
console.print()

return 1


def _prompt_examples_skill_id(
console, input_fn=None
) -> Tuple[Optional[str], Optional[str]]:
Expand Down Expand Up @@ -1783,9 +1853,32 @@ def _package_version_str() -> str:
if installed is not None:
return str(installed)
try:
return importlib.metadata.version("skillware")
raw = importlib.metadata.version("skillware")
except importlib.metadata.PackageNotFoundError:
return "dev"
if not raw or raw in ("dev", "None"):
return "dev"
return raw


def emit_install_conflict_advisory() -> None:
"""Print one dim stderr line when install conflicts are detected; else silent."""
# Match the established advisory opt-out for CI and scripted invocation.
if is_version_check_disabled():
return
conflicts = detect_install_conflicts()
if not conflicts:
return
message = (
"skillware install conflict detected; run 'skillware doctor --install' "
"for details and fix commands."
)
try:
from rich.console import Console

Console(stderr=True).print(message, style="dim")
except ImportError:
print(message, file=sys.stderr)


def cmd_interactive(console=None, parser=None) -> None:
Expand Down Expand Up @@ -1908,6 +2001,7 @@ def cmd_interactive(console=None, parser=None) -> None:
def main() -> None:
"""CLI entry point."""
emit_upgrade_advisory()
emit_install_conflict_advisory()

parser = argparse.ArgumentParser(prog="skillware", add_help=False)

Expand Down Expand Up @@ -2027,6 +2121,11 @@ def main() -> None:
default=None,
help="Diagnose all skills in a category.",
)
doctor_parser.add_argument(
"--install",
action="store_true",
help="Diagnose the local skillware install and print fix commands.",
)

config_parser = subparsers.add_parser(
"config",
Expand Down Expand Up @@ -2296,6 +2395,7 @@ def main() -> None:
skills_root_override=args.skills_root,
skill_id=args.skill_id,
category=args.category,
install=getattr(args, "install", False),
)
)
elif args.command == "config":
Expand Down
Loading