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/core/config.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Configuration file loading and merging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
_DEFAULT_HUB_URL = (
|
|
14
|
+
"https://raw.githubusercontent.com/twisker/iphub/main"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SecurityMode(Enum):
|
|
19
|
+
"""Security enforcement level."""
|
|
20
|
+
|
|
21
|
+
PERMISSIVE = "permissive"
|
|
22
|
+
DEFAULT = "default"
|
|
23
|
+
CAUTIOUS = "cautious"
|
|
24
|
+
STRICT = "strict"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class IpManConfig:
|
|
29
|
+
"""Immutable configuration object."""
|
|
30
|
+
|
|
31
|
+
security_mode: SecurityMode = SecurityMode.DEFAULT
|
|
32
|
+
log_enabled: bool = True
|
|
33
|
+
log_path: Path = field(
|
|
34
|
+
default_factory=lambda: Path.home() / ".ipman" / "security.log",
|
|
35
|
+
)
|
|
36
|
+
hub_url: str = _DEFAULT_HUB_URL
|
|
37
|
+
agent_default: str = "auto"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_config(
|
|
41
|
+
*,
|
|
42
|
+
config_dir: Path | None = None,
|
|
43
|
+
) -> IpManConfig:
|
|
44
|
+
"""Load configuration with priority: env vars > file > defaults.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config_dir: Directory containing config.yaml.
|
|
48
|
+
Defaults to ``~/.ipman``.
|
|
49
|
+
"""
|
|
50
|
+
if config_dir is None:
|
|
51
|
+
config_dir = Path.home() / ".ipman"
|
|
52
|
+
|
|
53
|
+
# --- Load file ---
|
|
54
|
+
data: dict[str, Any] = {}
|
|
55
|
+
config_file = config_dir / "config.yaml"
|
|
56
|
+
if config_file.exists():
|
|
57
|
+
raw = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
|
58
|
+
if isinstance(raw, dict):
|
|
59
|
+
data = raw
|
|
60
|
+
|
|
61
|
+
security = data.get("security", {}) or {}
|
|
62
|
+
hub = data.get("hub", {}) or {}
|
|
63
|
+
agent = data.get("agent", {}) or {}
|
|
64
|
+
|
|
65
|
+
# --- Security mode ---
|
|
66
|
+
mode_str = security.get("mode", "default")
|
|
67
|
+
try:
|
|
68
|
+
mode = SecurityMode(mode_str)
|
|
69
|
+
except ValueError:
|
|
70
|
+
mode = SecurityMode.DEFAULT
|
|
71
|
+
|
|
72
|
+
# --- Log ---
|
|
73
|
+
log_enabled = security.get("log_enabled", True)
|
|
74
|
+
log_path_str = security.get("log_path")
|
|
75
|
+
log_path = Path(log_path_str) if log_path_str else config_dir / "security.log"
|
|
76
|
+
|
|
77
|
+
# --- Hub ---
|
|
78
|
+
hub_url = hub.get("url", _DEFAULT_HUB_URL)
|
|
79
|
+
|
|
80
|
+
# --- Agent ---
|
|
81
|
+
agent_default = agent.get("default", "auto")
|
|
82
|
+
|
|
83
|
+
# --- Environment variable overrides ---
|
|
84
|
+
env_hub = os.environ.get("IPMAN_HUB_URL")
|
|
85
|
+
if env_hub:
|
|
86
|
+
hub_url = env_hub
|
|
87
|
+
|
|
88
|
+
env_mode = os.environ.get("IPMAN_SECURITY_MODE")
|
|
89
|
+
if env_mode:
|
|
90
|
+
try:
|
|
91
|
+
mode = SecurityMode(env_mode)
|
|
92
|
+
except ValueError:
|
|
93
|
+
pass # ignore invalid env value
|
|
94
|
+
|
|
95
|
+
return IpManConfig(
|
|
96
|
+
security_mode=mode,
|
|
97
|
+
log_enabled=log_enabled,
|
|
98
|
+
log_path=log_path,
|
|
99
|
+
hub_url=hub_url,
|
|
100
|
+
agent_default=agent_default,
|
|
101
|
+
)
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Virtual environment management for IpMan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import shutil
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from ipman.agents.base import AgentAdapter
|
|
14
|
+
from ipman.utils.symlink import create_symlink, is_symlink, remove_symlink
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Scope(enum.Enum):
|
|
18
|
+
"""Environment scope."""
|
|
19
|
+
|
|
20
|
+
PROJECT = "project"
|
|
21
|
+
USER = "user"
|
|
22
|
+
MACHINE = "machine"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Path helpers
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def get_ipman_home() -> Path:
|
|
30
|
+
"""Return the global IpMan home directory (~/.ipman)."""
|
|
31
|
+
return Path.home() / ".ipman"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_project_ipman_dir(project_path: Path) -> Path:
|
|
35
|
+
"""Return the .ipman directory for a project."""
|
|
36
|
+
return project_path / ".ipman"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_envs_root(scope: Scope, project_path: Path | None = None) -> Path:
|
|
40
|
+
"""Return the envs/ root directory for the given scope."""
|
|
41
|
+
if scope == Scope.PROJECT:
|
|
42
|
+
if project_path is None:
|
|
43
|
+
msg = "project_path is required for project scope"
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
return get_project_ipman_dir(project_path) / "envs"
|
|
46
|
+
if scope == Scope.USER:
|
|
47
|
+
return get_ipman_home() / "envs"
|
|
48
|
+
# MACHINE scope
|
|
49
|
+
if _is_windows():
|
|
50
|
+
return Path("C:/ProgramData/ipman/envs")
|
|
51
|
+
return Path("/opt/ipman/envs")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_env_path(name: str, scope: Scope, project_path: Path | None = None) -> Path:
|
|
55
|
+
"""Return the full path to a named environment."""
|
|
56
|
+
return get_envs_root(scope, project_path) / name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# CRUD operations
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def create_env(
|
|
64
|
+
name: str,
|
|
65
|
+
adapter: AgentAdapter,
|
|
66
|
+
scope: Scope = Scope.PROJECT,
|
|
67
|
+
project_path: Path | None = None,
|
|
68
|
+
inherit: bool = False,
|
|
69
|
+
) -> Path:
|
|
70
|
+
"""Create a new virtual environment.
|
|
71
|
+
|
|
72
|
+
Returns the path to the created environment directory.
|
|
73
|
+
"""
|
|
74
|
+
if project_path is None:
|
|
75
|
+
project_path = Path.cwd()
|
|
76
|
+
|
|
77
|
+
env_path = get_env_path(name, scope, project_path)
|
|
78
|
+
|
|
79
|
+
if env_path.exists():
|
|
80
|
+
msg = f"Environment '{name}' already exists at {env_path}"
|
|
81
|
+
raise FileExistsError(msg)
|
|
82
|
+
|
|
83
|
+
# Create the environment directory
|
|
84
|
+
env_path.mkdir(parents=True)
|
|
85
|
+
|
|
86
|
+
# Initialize agent-specific structure
|
|
87
|
+
adapter.init_env_dir(env_path)
|
|
88
|
+
|
|
89
|
+
# Optionally inherit existing skills from current agent config
|
|
90
|
+
if inherit:
|
|
91
|
+
_inherit_existing(adapter, env_path, project_path)
|
|
92
|
+
|
|
93
|
+
# Write ipman.yaml project config (for project scope)
|
|
94
|
+
if scope == Scope.PROJECT:
|
|
95
|
+
_ensure_project_config(project_path, adapter)
|
|
96
|
+
|
|
97
|
+
# Write env metadata
|
|
98
|
+
_write_env_metadata(env_path, name, scope, adapter)
|
|
99
|
+
|
|
100
|
+
return env_path
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def delete_env(
|
|
104
|
+
name: str,
|
|
105
|
+
scope: Scope = Scope.PROJECT,
|
|
106
|
+
project_path: Path | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Delete a virtual environment."""
|
|
109
|
+
if project_path is None:
|
|
110
|
+
project_path = Path.cwd()
|
|
111
|
+
|
|
112
|
+
env_path = get_env_path(name, scope, project_path)
|
|
113
|
+
|
|
114
|
+
if not env_path.exists():
|
|
115
|
+
msg = f"Environment '{name}' does not exist"
|
|
116
|
+
raise FileNotFoundError(msg)
|
|
117
|
+
|
|
118
|
+
# Check if this env is currently active — deactivate first
|
|
119
|
+
config = _read_project_config(project_path)
|
|
120
|
+
if config and config.get("active_env") == name:
|
|
121
|
+
deactivate_env(project_path=project_path)
|
|
122
|
+
|
|
123
|
+
shutil.rmtree(env_path)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def list_envs(
|
|
127
|
+
scope: Scope = Scope.PROJECT,
|
|
128
|
+
project_path: Path | None = None,
|
|
129
|
+
) -> list[dict[str, Any]]:
|
|
130
|
+
"""List all environments for the given scope.
|
|
131
|
+
|
|
132
|
+
Returns a list of dicts with env metadata.
|
|
133
|
+
"""
|
|
134
|
+
if project_path is None:
|
|
135
|
+
project_path = Path.cwd()
|
|
136
|
+
|
|
137
|
+
envs_root = get_envs_root(scope, project_path)
|
|
138
|
+
|
|
139
|
+
if not envs_root.exists():
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
result = []
|
|
143
|
+
config = _read_project_config(project_path)
|
|
144
|
+
active_name = config.get("active_env") if config else None
|
|
145
|
+
|
|
146
|
+
for env_dir in sorted(envs_root.iterdir()):
|
|
147
|
+
if not env_dir.is_dir():
|
|
148
|
+
continue
|
|
149
|
+
meta_file = env_dir / "env.yaml"
|
|
150
|
+
if meta_file.exists():
|
|
151
|
+
meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
|
|
152
|
+
else:
|
|
153
|
+
meta = {"name": env_dir.name}
|
|
154
|
+
meta["active"] = meta.get("name", env_dir.name) == active_name
|
|
155
|
+
meta["path"] = str(env_dir)
|
|
156
|
+
result.append(meta)
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Activate / Deactivate
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def activate_env(
|
|
166
|
+
name: str,
|
|
167
|
+
scope: Scope = Scope.PROJECT,
|
|
168
|
+
project_path: Path | None = None,
|
|
169
|
+
) -> Path:
|
|
170
|
+
"""Activate a virtual environment by symlinking the agent config dir.
|
|
171
|
+
|
|
172
|
+
Returns the path to the activated environment.
|
|
173
|
+
"""
|
|
174
|
+
if project_path is None:
|
|
175
|
+
project_path = Path.cwd()
|
|
176
|
+
|
|
177
|
+
env_path = get_env_path(name, scope, project_path)
|
|
178
|
+
if not env_path.exists():
|
|
179
|
+
msg = f"Environment '{name}' does not exist"
|
|
180
|
+
raise FileNotFoundError(msg)
|
|
181
|
+
|
|
182
|
+
config = _read_project_config(project_path)
|
|
183
|
+
if not config:
|
|
184
|
+
msg = "No ipman.yaml found. Run 'ipman create' first."
|
|
185
|
+
raise FileNotFoundError(msg)
|
|
186
|
+
|
|
187
|
+
agent_config_dir_name = config.get("agent_config_dir", ".claude")
|
|
188
|
+
agent_config_path = project_path / agent_config_dir_name
|
|
189
|
+
backup_path = project_path / f"{agent_config_dir_name}.bak"
|
|
190
|
+
|
|
191
|
+
# If already a symlink (another env active), remove it
|
|
192
|
+
if is_symlink(agent_config_path):
|
|
193
|
+
remove_symlink(agent_config_path)
|
|
194
|
+
elif agent_config_path.exists():
|
|
195
|
+
# Real directory — back it up
|
|
196
|
+
if backup_path.exists():
|
|
197
|
+
msg = (
|
|
198
|
+
f"Backup already exists at {backup_path}. "
|
|
199
|
+
"Please resolve manually before activating."
|
|
200
|
+
)
|
|
201
|
+
raise FileExistsError(msg)
|
|
202
|
+
agent_config_path.rename(backup_path)
|
|
203
|
+
|
|
204
|
+
# Create the symlink
|
|
205
|
+
create_symlink(target=env_path, link=agent_config_path)
|
|
206
|
+
|
|
207
|
+
# Update project config
|
|
208
|
+
_update_project_config(project_path, active_env=name)
|
|
209
|
+
|
|
210
|
+
return env_path
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def deactivate_env(
|
|
214
|
+
project_path: Path | None = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Deactivate the current virtual environment."""
|
|
217
|
+
if project_path is None:
|
|
218
|
+
project_path = Path.cwd()
|
|
219
|
+
|
|
220
|
+
config = _read_project_config(project_path)
|
|
221
|
+
if not config or not config.get("active_env"):
|
|
222
|
+
msg = "No environment is currently active"
|
|
223
|
+
raise RuntimeError(msg)
|
|
224
|
+
|
|
225
|
+
agent_config_dir_name = config.get("agent_config_dir", ".claude")
|
|
226
|
+
agent_config_path = project_path / agent_config_dir_name
|
|
227
|
+
backup_path = project_path / f"{agent_config_dir_name}.bak"
|
|
228
|
+
|
|
229
|
+
# Remove the symlink
|
|
230
|
+
if is_symlink(agent_config_path):
|
|
231
|
+
remove_symlink(agent_config_path)
|
|
232
|
+
|
|
233
|
+
# Restore backup if it exists
|
|
234
|
+
if backup_path.exists():
|
|
235
|
+
backup_path.rename(agent_config_path)
|
|
236
|
+
|
|
237
|
+
# Clear active env in config
|
|
238
|
+
_update_project_config(project_path, active_env=None)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Shell integration (activate script generation)
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def build_prompt_tag(
|
|
246
|
+
project_path: Path | None = None,
|
|
247
|
+
) -> str:
|
|
248
|
+
"""Build the compact prompt tag showing active envs across all scopes.
|
|
249
|
+
|
|
250
|
+
Format: [ip:<machine><user><project_name>]
|
|
251
|
+
- machine active: * inactive: omitted
|
|
252
|
+
- user active: - inactive: omitted
|
|
253
|
+
- project active: env full name inactive: omitted
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
[ip:*-myenv] = all three layers active
|
|
257
|
+
[ip:myenv] = only project
|
|
258
|
+
[ip:*myenv] = machine + project
|
|
259
|
+
[ip:*-] = machine + user, no project
|
|
260
|
+
"""
|
|
261
|
+
if project_path is None:
|
|
262
|
+
project_path = Path.cwd()
|
|
263
|
+
|
|
264
|
+
parts: list[str] = []
|
|
265
|
+
|
|
266
|
+
# Machine layer
|
|
267
|
+
machine_envs = list_envs(Scope.MACHINE, project_path)
|
|
268
|
+
machine_active = any(e.get("active") for e in machine_envs)
|
|
269
|
+
if machine_active:
|
|
270
|
+
parts.append("*")
|
|
271
|
+
|
|
272
|
+
# User layer
|
|
273
|
+
user_envs = list_envs(Scope.USER, project_path)
|
|
274
|
+
user_active = any(e.get("active") for e in user_envs)
|
|
275
|
+
if user_active:
|
|
276
|
+
parts.append("-")
|
|
277
|
+
|
|
278
|
+
# Project layer
|
|
279
|
+
config = _read_project_config(project_path)
|
|
280
|
+
project_env = config.get("active_env") if config else None
|
|
281
|
+
if project_env:
|
|
282
|
+
parts.append(project_env)
|
|
283
|
+
|
|
284
|
+
if not parts:
|
|
285
|
+
return ""
|
|
286
|
+
|
|
287
|
+
return f"[ip:{''.join(parts)}]"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_env_status(
|
|
291
|
+
project_path: Path | None = None,
|
|
292
|
+
) -> list[dict[str, Any]]:
|
|
293
|
+
"""Get detailed status of active environments across all scopes.
|
|
294
|
+
|
|
295
|
+
Returns a list of dicts: {scope, name, agent, path}.
|
|
296
|
+
"""
|
|
297
|
+
if project_path is None:
|
|
298
|
+
project_path = Path.cwd()
|
|
299
|
+
|
|
300
|
+
result: list[dict[str, Any]] = []
|
|
301
|
+
|
|
302
|
+
for scope in Scope:
|
|
303
|
+
try:
|
|
304
|
+
envs = list_envs(scope, project_path)
|
|
305
|
+
except (ValueError, OSError):
|
|
306
|
+
continue
|
|
307
|
+
for env in envs:
|
|
308
|
+
if env.get("active"):
|
|
309
|
+
result.append({
|
|
310
|
+
"scope": scope.value,
|
|
311
|
+
"name": env.get("name", "unknown"),
|
|
312
|
+
"agent": env.get("agent", "unknown"),
|
|
313
|
+
"path": env.get("path", ""),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def generate_activate_script(
|
|
320
|
+
env_name: str,
|
|
321
|
+
shell: str = "bash",
|
|
322
|
+
prompt_tag: str = "",
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Generate a shell script snippet for activation."""
|
|
325
|
+
if not prompt_tag:
|
|
326
|
+
prompt_tag = f"[ip:{env_name}]"
|
|
327
|
+
if shell in ("bash", "zsh"):
|
|
328
|
+
return _bash_activate_script(env_name, prompt_tag)
|
|
329
|
+
if shell == "fish":
|
|
330
|
+
return _fish_activate_script(env_name, prompt_tag)
|
|
331
|
+
if shell in ("powershell", "pwsh"):
|
|
332
|
+
return _powershell_activate_script(env_name, prompt_tag)
|
|
333
|
+
return _bash_activate_script(env_name, prompt_tag)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def generate_deactivate_script(shell: str = "bash") -> str:
|
|
337
|
+
"""Generate a shell script snippet for deactivation."""
|
|
338
|
+
if shell in ("bash", "zsh"):
|
|
339
|
+
return _bash_deactivate_script()
|
|
340
|
+
if shell == "fish":
|
|
341
|
+
return _fish_deactivate_script()
|
|
342
|
+
if shell in ("powershell", "pwsh"):
|
|
343
|
+
return _powershell_deactivate_script()
|
|
344
|
+
return _bash_deactivate_script()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# Internal helpers
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
def _is_windows() -> bool:
|
|
352
|
+
import sys
|
|
353
|
+
return sys.platform == "win32"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _write_env_metadata(
|
|
357
|
+
env_path: Path, name: str, scope: Scope, adapter: AgentAdapter
|
|
358
|
+
) -> None:
|
|
359
|
+
meta = {
|
|
360
|
+
"name": name,
|
|
361
|
+
"scope": scope.value,
|
|
362
|
+
"agent": adapter.name,
|
|
363
|
+
"created": datetime.now(tz=timezone.utc).isoformat(),
|
|
364
|
+
}
|
|
365
|
+
meta_file = env_path / "env.yaml"
|
|
366
|
+
meta_file.write_text(yaml.dump(meta, default_flow_style=False), encoding="utf-8")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _ensure_project_config(project_path: Path, adapter: AgentAdapter) -> None:
|
|
370
|
+
"""Create or update .ipman/ipman.yaml."""
|
|
371
|
+
ipman_dir = get_project_ipman_dir(project_path)
|
|
372
|
+
ipman_dir.mkdir(parents=True, exist_ok=True)
|
|
373
|
+
config_file = ipman_dir / "ipman.yaml"
|
|
374
|
+
if config_file.exists():
|
|
375
|
+
return
|
|
376
|
+
config = {
|
|
377
|
+
"agent": adapter.name,
|
|
378
|
+
"agent_config_dir": adapter.config_dir_name,
|
|
379
|
+
"active_env": None,
|
|
380
|
+
}
|
|
381
|
+
content = yaml.dump(config, default_flow_style=False)
|
|
382
|
+
config_file.write_text(content, encoding="utf-8")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _read_project_config(project_path: Path) -> dict[str, Any] | None:
|
|
386
|
+
config_file = get_project_ipman_dir(project_path) / "ipman.yaml"
|
|
387
|
+
if not config_file.exists():
|
|
388
|
+
return None
|
|
389
|
+
return yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _update_project_config(project_path: Path, active_env: str | None) -> None:
|
|
393
|
+
config_file = get_project_ipman_dir(project_path) / "ipman.yaml"
|
|
394
|
+
config = yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
395
|
+
config["active_env"] = active_env
|
|
396
|
+
content = yaml.dump(config, default_flow_style=False)
|
|
397
|
+
config_file.write_text(content, encoding="utf-8")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _inherit_existing(
|
|
401
|
+
adapter: AgentAdapter, env_path: Path, project_path: Path
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Copy existing agent config into the new environment."""
|
|
404
|
+
source = project_path / adapter.config_dir_name
|
|
405
|
+
if not source.exists() or is_symlink(source):
|
|
406
|
+
return
|
|
407
|
+
# Copy contents (not the directory itself) into env_path
|
|
408
|
+
for item in source.iterdir():
|
|
409
|
+
dest = env_path / item.name
|
|
410
|
+
if item.is_dir():
|
|
411
|
+
shutil.copytree(item, dest, dirs_exist_ok=True)
|
|
412
|
+
else:
|
|
413
|
+
shutil.copy2(item, dest)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _bash_activate_script(env_name: str, prompt_tag: str) -> str:
|
|
417
|
+
return f"""\
|
|
418
|
+
export IPMAN_ENV="{env_name}"
|
|
419
|
+
export _IPMAN_OLD_PS1="$PS1"
|
|
420
|
+
PS1="{prompt_tag} $PS1"
|
|
421
|
+
export PS1
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _fish_activate_script(env_name: str, prompt_tag: str) -> str:
|
|
426
|
+
return f"""\
|
|
427
|
+
set -gx IPMAN_ENV "{env_name}"
|
|
428
|
+
set -gx _IPMAN_OLD_PROMPT (functions fish_prompt)
|
|
429
|
+
function fish_prompt
|
|
430
|
+
echo -n "{prompt_tag} "
|
|
431
|
+
eval $_IPMAN_OLD_PROMPT
|
|
432
|
+
end
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _powershell_activate_script(env_name: str, prompt_tag: str) -> str:
|
|
437
|
+
return f"""\
|
|
438
|
+
$env:IPMAN_ENV = "{env_name}"
|
|
439
|
+
$_ipman_old_prompt = $function:prompt
|
|
440
|
+
function prompt {{ "{prompt_tag} " + (& $_ipman_old_prompt) }}
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _bash_deactivate_script() -> str:
|
|
445
|
+
return """\
|
|
446
|
+
if [ -n "$_IPMAN_OLD_PS1" ]; then
|
|
447
|
+
PS1="$_IPMAN_OLD_PS1"
|
|
448
|
+
export PS1
|
|
449
|
+
unset _IPMAN_OLD_PS1
|
|
450
|
+
fi
|
|
451
|
+
unset IPMAN_ENV
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _fish_deactivate_script() -> str:
|
|
456
|
+
return """\
|
|
457
|
+
if set -q _IPMAN_OLD_PROMPT
|
|
458
|
+
eval "function fish_prompt; $_IPMAN_OLD_PROMPT; end"
|
|
459
|
+
set -e _IPMAN_OLD_PROMPT
|
|
460
|
+
end
|
|
461
|
+
set -e IPMAN_ENV
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _powershell_deactivate_script() -> str:
|
|
466
|
+
return """\
|
|
467
|
+
if ($null -ne $_ipman_old_prompt) {
|
|
468
|
+
$function:prompt = $_ipman_old_prompt
|
|
469
|
+
Remove-Variable _ipman_old_prompt
|
|
470
|
+
}
|
|
471
|
+
Remove-Item Env:IPMAN_ENV -ErrorAction SilentlyContinue
|
|
472
|
+
"""
|