ipman-cli 0.1.73__py3-none-any.whl
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.
- ipman/__init__.py +5 -0
- ipman/agents/__init__.py +0 -0
- ipman/agents/base.py +85 -0
- ipman/agents/claude_code.py +75 -0
- ipman/agents/openclaw.py +74 -0
- ipman/agents/registry.py +45 -0
- ipman/cli/__init__.py +0 -0
- ipman/cli/_common.py +21 -0
- ipman/cli/env.py +271 -0
- ipman/cli/hub.py +237 -0
- ipman/cli/main.py +37 -0
- ipman/cli/pack.py +67 -0
- ipman/cli/skill.py +299 -0
- ipman/core/__init__.py +0 -0
- ipman/core/config.py +101 -0
- ipman/core/environment.py +472 -0
- ipman/core/package.py +188 -0
- ipman/core/resolver.py +160 -0
- ipman/core/security.py +84 -0
- ipman/core/vetter.py +193 -0
- ipman/hub/__init__.py +0 -0
- ipman/hub/client.py +132 -0
- ipman/hub/publisher.py +274 -0
- ipman/hub/stats.py +52 -0
- ipman/utils/__init__.py +0 -0
- ipman/utils/i18n.py +113 -0
- ipman/utils/symlink.py +84 -0
- ipman_cli-0.1.73.dist-info/METADATA +147 -0
- ipman_cli-0.1.73.dist-info/RECORD +32 -0
- ipman_cli-0.1.73.dist-info/WHEEL +4 -0
- ipman_cli-0.1.73.dist-info/entry_points.txt +2 -0
- ipman_cli-0.1.73.dist-info/licenses/LICENSE +201 -0
ipman/__init__.py
ADDED
ipman/agents/__init__.py
ADDED
|
File without changes
|
ipman/agents/base.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Base class for Agent tool adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SkillInfo:
|
|
13
|
+
"""Structured info about an installed skill."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
version: str = ""
|
|
17
|
+
enabled: bool = True
|
|
18
|
+
source: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentAdapter(ABC):
|
|
22
|
+
"""Base adapter for an Agent tool (e.g. Claude Code, OpenClaw).
|
|
23
|
+
|
|
24
|
+
Each adapter tells IpMan:
|
|
25
|
+
- What the agent's config directory is called
|
|
26
|
+
- How to detect if the agent is installed
|
|
27
|
+
- How to initialize a fresh environment directory for the agent
|
|
28
|
+
- How to install/uninstall/list skills via agent CLI commands
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def name(self) -> str:
|
|
34
|
+
"""Agent identifier, e.g. 'claude-code'."""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def display_name(self) -> str:
|
|
39
|
+
"""Human-readable name, e.g. 'Claude Code'."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def config_dir_name(self) -> str:
|
|
44
|
+
"""Name of the agent's config directory, e.g. '.claude'."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def is_installed(self) -> bool:
|
|
48
|
+
"""Check if this agent tool is installed on the system."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def init_env_dir(self, env_path: Path) -> None:
|
|
52
|
+
"""Initialize a fresh environment directory."""
|
|
53
|
+
|
|
54
|
+
def detect_in_project(self, project_path: Path) -> bool:
|
|
55
|
+
"""Check if this agent is used in the given project directory."""
|
|
56
|
+
return (project_path / self.config_dir_name).exists()
|
|
57
|
+
|
|
58
|
+
# --- Skill CLI delegation (Sprint 2) ---
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def install_skill(
|
|
62
|
+
self, name: str, **kwargs: str | None,
|
|
63
|
+
) -> subprocess.CompletedProcess[str]:
|
|
64
|
+
"""Install a skill via agent's native CLI command."""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def uninstall_skill(
|
|
68
|
+
self, name: str,
|
|
69
|
+
) -> subprocess.CompletedProcess[str]:
|
|
70
|
+
"""Uninstall a skill via agent's native CLI command."""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def list_skills(self) -> list[SkillInfo]:
|
|
74
|
+
"""List installed skills via agent's native CLI command."""
|
|
75
|
+
|
|
76
|
+
def _run_cli(
|
|
77
|
+
self, args: list[str],
|
|
78
|
+
) -> subprocess.CompletedProcess[str]:
|
|
79
|
+
"""Run a CLI command and capture output."""
|
|
80
|
+
return subprocess.run(
|
|
81
|
+
args,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
check=False,
|
|
85
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Claude Code agent adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ipman.agents.base import AgentAdapter, SkillInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClaudeCodeAdapter(AgentAdapter):
|
|
14
|
+
"""Adapter for Claude Code.
|
|
15
|
+
|
|
16
|
+
Skill operations delegate to `claude plugin` CLI commands.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "claude-code"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def display_name(self) -> str:
|
|
25
|
+
return "Claude Code"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def config_dir_name(self) -> str:
|
|
29
|
+
return ".claude"
|
|
30
|
+
|
|
31
|
+
def is_installed(self) -> bool:
|
|
32
|
+
return shutil.which("claude") is not None
|
|
33
|
+
|
|
34
|
+
def init_env_dir(self, env_path: Path) -> None:
|
|
35
|
+
"""Create Claude Code environment structure."""
|
|
36
|
+
skills_dir = env_path / "skills"
|
|
37
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
def install_skill(
|
|
40
|
+
self, name: str, **kwargs: str | None,
|
|
41
|
+
) -> subprocess.CompletedProcess[str]:
|
|
42
|
+
"""Install a plugin via ``claude plugin install``."""
|
|
43
|
+
args = ["claude", "plugin", "install", name]
|
|
44
|
+
scope = kwargs.get("scope")
|
|
45
|
+
if scope:
|
|
46
|
+
args.extend(["-s", scope])
|
|
47
|
+
return self._run_cli(args)
|
|
48
|
+
|
|
49
|
+
def uninstall_skill(
|
|
50
|
+
self, name: str,
|
|
51
|
+
) -> subprocess.CompletedProcess[str]:
|
|
52
|
+
"""Uninstall a plugin via ``claude plugin uninstall``."""
|
|
53
|
+
return self._run_cli(
|
|
54
|
+
["claude", "plugin", "uninstall", name],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def list_skills(self) -> list[SkillInfo]:
|
|
58
|
+
"""List installed plugins via ``claude plugin list --json``."""
|
|
59
|
+
result = self._run_cli(
|
|
60
|
+
["claude", "plugin", "list", "--json"],
|
|
61
|
+
)
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
return []
|
|
64
|
+
try:
|
|
65
|
+
plugins = json.loads(result.stdout)
|
|
66
|
+
except (json.JSONDecodeError, TypeError):
|
|
67
|
+
return []
|
|
68
|
+
return [
|
|
69
|
+
SkillInfo(
|
|
70
|
+
name=p.get("name", ""),
|
|
71
|
+
version=p.get("version", ""),
|
|
72
|
+
enabled=p.get("enabled", True),
|
|
73
|
+
)
|
|
74
|
+
for p in plugins
|
|
75
|
+
]
|
ipman/agents/openclaw.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""OpenClaw agent adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ipman.agents.base import AgentAdapter, SkillInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenClawAdapter(AgentAdapter):
|
|
14
|
+
"""Adapter for OpenClaw.
|
|
15
|
+
|
|
16
|
+
Skill operations delegate to ``clawhub`` CLI commands.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return "openclaw"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def display_name(self) -> str:
|
|
25
|
+
return "OpenClaw"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def config_dir_name(self) -> str:
|
|
29
|
+
return ".openclaw"
|
|
30
|
+
|
|
31
|
+
def is_installed(self) -> bool:
|
|
32
|
+
return shutil.which("openclaw") is not None
|
|
33
|
+
|
|
34
|
+
def init_env_dir(self, env_path: Path) -> None:
|
|
35
|
+
"""Create OpenClaw environment structure."""
|
|
36
|
+
skills_dir = env_path / "skills"
|
|
37
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
def install_skill(
|
|
40
|
+
self, name: str, **kwargs: str | None,
|
|
41
|
+
) -> subprocess.CompletedProcess[str]:
|
|
42
|
+
"""Install a skill via ``clawhub install``."""
|
|
43
|
+
args = ["clawhub", "install", name]
|
|
44
|
+
hub = kwargs.get("hub")
|
|
45
|
+
if hub:
|
|
46
|
+
args.extend(["--hub", hub])
|
|
47
|
+
return self._run_cli(args)
|
|
48
|
+
|
|
49
|
+
def uninstall_skill(
|
|
50
|
+
self, name: str,
|
|
51
|
+
) -> subprocess.CompletedProcess[str]:
|
|
52
|
+
"""Uninstall a skill via ``clawhub uninstall``."""
|
|
53
|
+
return self._run_cli(
|
|
54
|
+
["clawhub", "uninstall", name],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def list_skills(self) -> list[SkillInfo]:
|
|
58
|
+
"""List installed skills via ``clawhub list --json``."""
|
|
59
|
+
result = self._run_cli(
|
|
60
|
+
["clawhub", "list", "--json"],
|
|
61
|
+
)
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
return []
|
|
64
|
+
try:
|
|
65
|
+
skills = json.loads(result.stdout)
|
|
66
|
+
except (json.JSONDecodeError, TypeError):
|
|
67
|
+
return []
|
|
68
|
+
return [
|
|
69
|
+
SkillInfo(
|
|
70
|
+
name=s.get("name", ""),
|
|
71
|
+
version=s.get("version", ""),
|
|
72
|
+
)
|
|
73
|
+
for s in skills
|
|
74
|
+
]
|
ipman/agents/registry.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Agent adapter registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ipman.agents.base import AgentAdapter
|
|
8
|
+
from ipman.agents.claude_code import ClaudeCodeAdapter
|
|
9
|
+
from ipman.agents.openclaw import OpenClawAdapter
|
|
10
|
+
|
|
11
|
+
# All known adapters, in detection priority order
|
|
12
|
+
_ADAPTERS: list[AgentAdapter] = [
|
|
13
|
+
ClaudeCodeAdapter(),
|
|
14
|
+
OpenClawAdapter(),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_adapter(name: str) -> AgentAdapter:
|
|
19
|
+
"""Get an adapter by agent name."""
|
|
20
|
+
for adapter in _ADAPTERS:
|
|
21
|
+
if adapter.name == name:
|
|
22
|
+
return adapter
|
|
23
|
+
known = ", ".join(a.name for a in _ADAPTERS)
|
|
24
|
+
msg = f"Unknown agent: '{name}'. Known agents: {known}"
|
|
25
|
+
raise ValueError(msg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_agent(project_path: Path | None = None) -> AgentAdapter | None:
|
|
29
|
+
"""Auto-detect the agent used in a project directory."""
|
|
30
|
+
if project_path is None:
|
|
31
|
+
project_path = Path.cwd()
|
|
32
|
+
for adapter in _ADAPTERS:
|
|
33
|
+
if adapter.detect_in_project(project_path):
|
|
34
|
+
return adapter
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_installed_agents() -> list[AgentAdapter]:
|
|
39
|
+
"""Detect all agent tools installed on the system."""
|
|
40
|
+
return [a for a in _ADAPTERS if a.is_installed()]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def list_known_agents() -> list[str]:
|
|
44
|
+
"""Return names of all known agents."""
|
|
45
|
+
return [a.name for a in _ADAPTERS]
|
ipman/cli/__init__.py
ADDED
|
File without changes
|
ipman/cli/_common.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared CLI utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ipman.agents.base import AgentAdapter
|
|
8
|
+
from ipman.agents.registry import detect_agent, get_adapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_agent(agent_name: str | None) -> AgentAdapter:
|
|
12
|
+
"""Resolve adapter from --agent flag or auto-detection."""
|
|
13
|
+
if agent_name:
|
|
14
|
+
return get_adapter(agent_name)
|
|
15
|
+
detected = detect_agent()
|
|
16
|
+
if detected is None:
|
|
17
|
+
raise click.ClickException(
|
|
18
|
+
"No agent detected. Use --agent to specify one "
|
|
19
|
+
"(e.g. --agent claude-code)."
|
|
20
|
+
)
|
|
21
|
+
return detected
|
ipman/cli/env.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""CLI commands for virtual environment management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ipman.agents.registry import detect_agent, get_adapter, list_known_agents
|
|
11
|
+
from ipman.core.environment import (
|
|
12
|
+
Scope,
|
|
13
|
+
activate_env,
|
|
14
|
+
build_prompt_tag,
|
|
15
|
+
create_env,
|
|
16
|
+
deactivate_env,
|
|
17
|
+
delete_env,
|
|
18
|
+
generate_activate_script,
|
|
19
|
+
generate_deactivate_script,
|
|
20
|
+
get_env_status,
|
|
21
|
+
list_envs,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_scope(project: bool, user: bool, machine: bool) -> Scope:
|
|
26
|
+
if user:
|
|
27
|
+
return Scope.USER
|
|
28
|
+
if machine:
|
|
29
|
+
return Scope.MACHINE
|
|
30
|
+
return Scope.PROJECT
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_adapter(agent: str | None, project_path: Path) -> object:
|
|
34
|
+
"""Resolve the agent adapter from --agent flag or auto-detection."""
|
|
35
|
+
if agent:
|
|
36
|
+
return get_adapter(agent)
|
|
37
|
+
detected = detect_agent(project_path)
|
|
38
|
+
if detected:
|
|
39
|
+
return detected
|
|
40
|
+
# Default to claude-code
|
|
41
|
+
return get_adapter("claude-code")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _detect_shell() -> str:
|
|
45
|
+
shell_path = os.environ.get("SHELL", "")
|
|
46
|
+
if "fish" in shell_path:
|
|
47
|
+
return "fish"
|
|
48
|
+
if "zsh" in shell_path or "bash" in shell_path:
|
|
49
|
+
return "bash"
|
|
50
|
+
# Windows
|
|
51
|
+
if os.environ.get("PSMODULEPATH"):
|
|
52
|
+
return "powershell"
|
|
53
|
+
return "bash"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@click.group()
|
|
57
|
+
def env() -> None:
|
|
58
|
+
"""Manage virtual skill environments."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@env.command()
|
|
62
|
+
@click.argument("name")
|
|
63
|
+
@click.option("--project", "scope_project", is_flag=True, default=True,
|
|
64
|
+
help="Create in current project (default).")
|
|
65
|
+
@click.option("--user", "scope_user", is_flag=True, default=False,
|
|
66
|
+
help="Create for current user.")
|
|
67
|
+
@click.option("--machine", "scope_machine", is_flag=True, default=False,
|
|
68
|
+
help="Create for entire machine.")
|
|
69
|
+
@click.option("--agent", default=None,
|
|
70
|
+
help=f"Agent tool to target. Known: {', '.join(list_known_agents())}")
|
|
71
|
+
@click.option("--inherit", is_flag=True, default=False,
|
|
72
|
+
help="Inherit existing skills from current agent config.")
|
|
73
|
+
def create(
|
|
74
|
+
name: str,
|
|
75
|
+
scope_project: bool,
|
|
76
|
+
scope_user: bool,
|
|
77
|
+
scope_machine: bool,
|
|
78
|
+
agent: str | None,
|
|
79
|
+
inherit: bool,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Create a new virtual skill environment."""
|
|
82
|
+
project_path = Path.cwd()
|
|
83
|
+
scope = _resolve_scope(scope_project, scope_user, scope_machine)
|
|
84
|
+
adapter = _resolve_adapter(agent, project_path)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
env_path = create_env(
|
|
88
|
+
name=name,
|
|
89
|
+
adapter=adapter, # type: ignore[arg-type]
|
|
90
|
+
scope=scope,
|
|
91
|
+
project_path=project_path,
|
|
92
|
+
inherit=inherit,
|
|
93
|
+
)
|
|
94
|
+
click.secho(
|
|
95
|
+
f"Created environment '{name}' at {env_path}",
|
|
96
|
+
fg="green",
|
|
97
|
+
)
|
|
98
|
+
click.echo(f" Agent: {adapter.display_name}") # type: ignore[attr-defined]
|
|
99
|
+
click.echo(f" Scope: {scope.value}")
|
|
100
|
+
if inherit:
|
|
101
|
+
click.echo(" Inherited existing skills.")
|
|
102
|
+
click.echo(f"\nActivate with: ipman env activate {name}")
|
|
103
|
+
except FileExistsError as e:
|
|
104
|
+
click.secho(str(e), fg="red", err=True)
|
|
105
|
+
raise SystemExit(1) from None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@env.command()
|
|
109
|
+
@click.argument("name")
|
|
110
|
+
@click.option("--project", "scope_project", is_flag=True, default=True)
|
|
111
|
+
@click.option("--user", "scope_user", is_flag=True, default=False)
|
|
112
|
+
@click.option("--machine", "scope_machine", is_flag=True, default=False)
|
|
113
|
+
def activate(
|
|
114
|
+
name: str,
|
|
115
|
+
scope_project: bool,
|
|
116
|
+
scope_user: bool,
|
|
117
|
+
scope_machine: bool,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Activate a virtual skill environment.
|
|
120
|
+
|
|
121
|
+
To update your shell prompt, use:
|
|
122
|
+
|
|
123
|
+
eval "$(ipman env activate myenv)"
|
|
124
|
+
"""
|
|
125
|
+
project_path = Path.cwd()
|
|
126
|
+
scope = _resolve_scope(scope_project, scope_user, scope_machine)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
activate_env(
|
|
130
|
+
name=name,
|
|
131
|
+
scope=scope,
|
|
132
|
+
project_path=project_path,
|
|
133
|
+
)
|
|
134
|
+
prompt_tag = build_prompt_tag(project_path)
|
|
135
|
+
shell = _detect_shell()
|
|
136
|
+
script = generate_activate_script(name, shell, prompt_tag)
|
|
137
|
+
# If stdout is a terminal, print human-friendly message
|
|
138
|
+
# If piped (eval), print only the script
|
|
139
|
+
if os.isatty(1):
|
|
140
|
+
click.secho(f"Activated '{name}'.", fg="green")
|
|
141
|
+
click.echo(f" Prompt tag: {prompt_tag}")
|
|
142
|
+
click.echo(
|
|
143
|
+
"\nTo update your shell prompt, run:\n"
|
|
144
|
+
f' eval "$(ipman env activate {name})"'
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
click.echo(script, nl=False)
|
|
148
|
+
except (FileNotFoundError, FileExistsError) as e:
|
|
149
|
+
click.secho(str(e), fg="red", err=True)
|
|
150
|
+
raise SystemExit(1) from None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@env.command()
|
|
154
|
+
def deactivate() -> None:
|
|
155
|
+
"""Deactivate the current virtual skill environment.
|
|
156
|
+
|
|
157
|
+
To update your shell prompt, use:
|
|
158
|
+
|
|
159
|
+
eval "$(ipman env deactivate)"
|
|
160
|
+
"""
|
|
161
|
+
project_path = Path.cwd()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
deactivate_env(project_path=project_path)
|
|
165
|
+
shell = _detect_shell()
|
|
166
|
+
script = generate_deactivate_script(shell)
|
|
167
|
+
if os.isatty(1):
|
|
168
|
+
click.secho("Environment deactivated.", fg="green")
|
|
169
|
+
click.echo(
|
|
170
|
+
"\nTo restore your shell prompt, run:\n"
|
|
171
|
+
' eval "$(ipman env deactivate)"'
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
click.echo(script, nl=False)
|
|
175
|
+
except RuntimeError as e:
|
|
176
|
+
click.secho(str(e), fg="red", err=True)
|
|
177
|
+
raise SystemExit(1) from None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@env.command("delete")
|
|
181
|
+
@click.argument("name")
|
|
182
|
+
@click.option("--project", "scope_project", is_flag=True, default=True)
|
|
183
|
+
@click.option("--user", "scope_user", is_flag=True, default=False)
|
|
184
|
+
@click.option("--machine", "scope_machine", is_flag=True, default=False)
|
|
185
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation.")
|
|
186
|
+
def delete_cmd(
|
|
187
|
+
name: str,
|
|
188
|
+
scope_project: bool,
|
|
189
|
+
scope_user: bool,
|
|
190
|
+
scope_machine: bool,
|
|
191
|
+
yes: bool,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Delete a virtual skill environment."""
|
|
194
|
+
if not yes:
|
|
195
|
+
click.confirm(
|
|
196
|
+
f"Delete environment '{name}'? This cannot be undone.",
|
|
197
|
+
abort=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
project_path = Path.cwd()
|
|
201
|
+
scope = _resolve_scope(scope_project, scope_user, scope_machine)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
delete_env(name=name, scope=scope, project_path=project_path)
|
|
205
|
+
click.secho(f"Deleted environment '{name}'.", fg="green")
|
|
206
|
+
except FileNotFoundError as e:
|
|
207
|
+
click.secho(str(e), fg="red", err=True)
|
|
208
|
+
raise SystemExit(1) from None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@env.command("list")
|
|
212
|
+
@click.option("--project", "scope_project", is_flag=True, default=True)
|
|
213
|
+
@click.option("--user", "scope_user", is_flag=True, default=False)
|
|
214
|
+
@click.option("--machine", "scope_machine", is_flag=True, default=False)
|
|
215
|
+
def list_cmd(
|
|
216
|
+
scope_project: bool,
|
|
217
|
+
scope_user: bool,
|
|
218
|
+
scope_machine: bool,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""List all virtual skill environments."""
|
|
221
|
+
project_path = Path.cwd()
|
|
222
|
+
scope = _resolve_scope(scope_project, scope_user, scope_machine)
|
|
223
|
+
|
|
224
|
+
envs = list_envs(scope=scope, project_path=project_path)
|
|
225
|
+
|
|
226
|
+
if not envs:
|
|
227
|
+
click.echo(f"No environments found ({scope.value} scope).")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
for e in envs:
|
|
231
|
+
marker = click.style(" *", fg="cyan") if e.get("active") else ""
|
|
232
|
+
name = e.get("name", "unknown")
|
|
233
|
+
agent = e.get("agent", "unknown")
|
|
234
|
+
created = e.get("created", "")
|
|
235
|
+
click.echo(
|
|
236
|
+
f" {name}{marker} (agent: {agent}, created: {created})"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
active_count = sum(1 for e in envs if e.get("active"))
|
|
240
|
+
if active_count:
|
|
241
|
+
click.echo("\n * = currently active")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@env.command("status")
|
|
245
|
+
def status_cmd() -> None:
|
|
246
|
+
"""Show detailed active environment status across all scopes."""
|
|
247
|
+
project_path = Path.cwd()
|
|
248
|
+
prompt_tag = build_prompt_tag(project_path)
|
|
249
|
+
status = get_env_status(project_path)
|
|
250
|
+
|
|
251
|
+
if not status:
|
|
252
|
+
click.echo("No active environments.")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
click.echo(f"Prompt: {prompt_tag}\n")
|
|
256
|
+
|
|
257
|
+
scope_labels = {
|
|
258
|
+
"machine": ("*", "Machine"),
|
|
259
|
+
"user": ("-", "User"),
|
|
260
|
+
"project": ("", "Project"),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for entry in status:
|
|
264
|
+
scope = entry["scope"]
|
|
265
|
+
symbol, label = scope_labels.get(scope, ("", scope))
|
|
266
|
+
prefix = f" {symbol} " if symbol else " "
|
|
267
|
+
click.secho(f"{prefix}{label}", fg="cyan", nl=False)
|
|
268
|
+
click.echo(
|
|
269
|
+
f": {entry['name']} "
|
|
270
|
+
f"(agent: {entry['agent']}, path: {entry['path']})"
|
|
271
|
+
)
|