agentdir-cli 0.7.5__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.
- agentdir/__init__.py +3 -0
- agentdir/__main__.py +6 -0
- agentdir/actors.py +50 -0
- agentdir/artifacts.py +56 -0
- agentdir/audit.py +339 -0
- agentdir/capture.py +187 -0
- agentdir/cli.py +1900 -0
- agentdir/context.py +654 -0
- agentdir/control.py +729 -0
- agentdir/daemon.py +253 -0
- agentdir/doctor.py +115 -0
- agentdir/envelope.py +147 -0
- agentdir/events.py +72 -0
- agentdir/federation.py +530 -0
- agentdir/git.py +45 -0
- agentdir/gitignore.py +106 -0
- agentdir/hooks.py +215 -0
- agentdir/index.py +362 -0
- agentdir/mailbox.py +84 -0
- agentdir/memory.py +1274 -0
- agentdir/query.py +65 -0
- agentdir/redaction.py +42 -0
- agentdir/rendering.py +89 -0
- agentdir/replay.py +31 -0
- agentdir/retention.py +319 -0
- agentdir/review.py +288 -0
- agentdir/secrets.py +158 -0
- agentdir/sessions.py +171 -0
- agentdir/skills.py +685 -0
- agentdir/store.py +270 -0
- agentdir/upgrade.py +252 -0
- agentdir_cli-0.7.5.dist-info/METADATA +393 -0
- agentdir_cli-0.7.5.dist-info/RECORD +37 -0
- agentdir_cli-0.7.5.dist-info/WHEEL +5 -0
- agentdir_cli-0.7.5.dist-info/entry_points.txt +2 -0
- agentdir_cli-0.7.5.dist-info/licenses/LICENSE +21 -0
- agentdir_cli-0.7.5.dist-info/top_level.txt +1 -0
agentdir/store.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
STORE_VERSION = "0.1"
|
|
12
|
+
CONFIG_DIR = ".agentdir"
|
|
13
|
+
INDEX_FILE = "agentdir.sqlite3"
|
|
14
|
+
SCOPE_CHOICES = ("project", "user", "global", "machine")
|
|
15
|
+
|
|
16
|
+
_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@+-]{0,127}$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentDirError(Exception):
|
|
20
|
+
"""Base exception for user-facing AgentDir failures."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class RootPaths:
|
|
25
|
+
root: Path
|
|
26
|
+
meta: Path
|
|
27
|
+
sessions: Path
|
|
28
|
+
actors: Path
|
|
29
|
+
queues: Path
|
|
30
|
+
artifacts: Path
|
|
31
|
+
archives: Path
|
|
32
|
+
indexes: Path
|
|
33
|
+
state: Path
|
|
34
|
+
hooks: Path
|
|
35
|
+
integrations: Path
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def index_path(self) -> Path:
|
|
39
|
+
return self.indexes / INDEX_FILE
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_root(root: str | Path) -> Path:
|
|
43
|
+
return Path(root).expanduser().resolve()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def paths_for(root: str | Path) -> RootPaths:
|
|
47
|
+
root_path = normalize_root(root)
|
|
48
|
+
meta_path = root_path
|
|
49
|
+
legacy_meta = root_path / CONFIG_DIR
|
|
50
|
+
if not (root_path / "VERSION").exists() and (legacy_meta / "VERSION").exists():
|
|
51
|
+
meta_path = legacy_meta
|
|
52
|
+
return RootPaths(
|
|
53
|
+
root=root_path,
|
|
54
|
+
meta=meta_path,
|
|
55
|
+
sessions=root_path / "sessions",
|
|
56
|
+
actors=root_path / "actors",
|
|
57
|
+
queues=root_path / "queues",
|
|
58
|
+
artifacts=root_path / "artifacts",
|
|
59
|
+
archives=root_path / "archives",
|
|
60
|
+
indexes=root_path / "indexes",
|
|
61
|
+
state=root_path / "state",
|
|
62
|
+
hooks=root_path / "hooks",
|
|
63
|
+
integrations=root_path / "integrations",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_project_base(cwd: str | Path | None = None) -> Path:
|
|
68
|
+
start = Path(cwd or Path.cwd()).expanduser().resolve()
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
72
|
+
cwd=start,
|
|
73
|
+
text=True,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
check=False,
|
|
76
|
+
)
|
|
77
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
78
|
+
return Path(result.stdout.strip()).resolve()
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
return start
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def machine_root() -> Path:
|
|
85
|
+
override = os.environ.get("AGENTDIR_MACHINE_ROOT")
|
|
86
|
+
if override:
|
|
87
|
+
return normalize_root(override)
|
|
88
|
+
legacy = _legacy_machine_root()
|
|
89
|
+
if legacy.exists():
|
|
90
|
+
return legacy
|
|
91
|
+
candidate = _platform_site_data_path()
|
|
92
|
+
if candidate is not None:
|
|
93
|
+
return candidate
|
|
94
|
+
return legacy
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def user_root() -> Path:
|
|
98
|
+
override = os.environ.get("AGENTDIR_USER_ROOT")
|
|
99
|
+
if override:
|
|
100
|
+
return normalize_root(override)
|
|
101
|
+
legacy = Path.home().expanduser().resolve() / CONFIG_DIR
|
|
102
|
+
if legacy.exists():
|
|
103
|
+
return legacy
|
|
104
|
+
candidate = _platform_user_data_path()
|
|
105
|
+
if candidate is not None:
|
|
106
|
+
return candidate
|
|
107
|
+
return _fallback_user_data_path()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _legacy_machine_root() -> Path:
|
|
111
|
+
if platform.system() == "Darwin":
|
|
112
|
+
return Path("/Library/Application Support/AgentDir").resolve()
|
|
113
|
+
return Path("/var/lib/agentdir").resolve()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _platform_user_data_path() -> Path | None:
|
|
117
|
+
try:
|
|
118
|
+
from platformdirs import user_data_path
|
|
119
|
+
except ImportError:
|
|
120
|
+
return None
|
|
121
|
+
return Path(user_data_path("AgentDir", appauthor=False)).expanduser().resolve()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _platform_site_data_path() -> Path | None:
|
|
125
|
+
try:
|
|
126
|
+
from platformdirs import site_data_path
|
|
127
|
+
except ImportError:
|
|
128
|
+
return None
|
|
129
|
+
return Path(site_data_path("AgentDir", appauthor=False)).expanduser().resolve()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fallback_user_data_path() -> Path:
|
|
133
|
+
if platform.system() == "Darwin":
|
|
134
|
+
return (Path.home().expanduser() / "Library" / "Application Support" / "AgentDir").resolve()
|
|
135
|
+
xdg = os.environ.get("XDG_DATA_HOME")
|
|
136
|
+
if xdg:
|
|
137
|
+
return (Path(xdg).expanduser() / "AgentDir").resolve()
|
|
138
|
+
return (Path.home().expanduser() / ".local" / "share" / "AgentDir").resolve()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def root_for_scope(scope: str | None = None, cwd: str | Path | None = None) -> Path:
|
|
142
|
+
selected = scope or os.environ.get("AGENTDIR_SCOPE") or "project"
|
|
143
|
+
if selected not in SCOPE_CHOICES:
|
|
144
|
+
raise AgentDirError(
|
|
145
|
+
f"Invalid scope {selected!r}; expected one of {', '.join(SCOPE_CHOICES)}"
|
|
146
|
+
)
|
|
147
|
+
if selected == "project":
|
|
148
|
+
return find_project_base(cwd) / CONFIG_DIR
|
|
149
|
+
if selected in {"user", "global"}:
|
|
150
|
+
return user_root()
|
|
151
|
+
return machine_root()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolve_root(
|
|
155
|
+
root: str | Path | None = None,
|
|
156
|
+
scope: str | None = None,
|
|
157
|
+
cwd: str | Path | None = None,
|
|
158
|
+
) -> Path:
|
|
159
|
+
if root is not None:
|
|
160
|
+
return normalize_root(root)
|
|
161
|
+
if scope is None and os.environ.get("AGENTDIR_ROOT"):
|
|
162
|
+
return normalize_root(os.environ["AGENTDIR_ROOT"])
|
|
163
|
+
return root_for_scope(scope, cwd)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_id(value: str, label: str = "id") -> str:
|
|
167
|
+
if not _ID_RE.match(value) or "/" in value or ":" in value or value.startswith("."):
|
|
168
|
+
raise AgentDirError(
|
|
169
|
+
f"Invalid {label} {value!r}; use filesystem-safe letters, numbers, dot, underscore, at, plus, or dash"
|
|
170
|
+
)
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ensure_mailbox(path: Path) -> Path:
|
|
175
|
+
for name in ("tmp", "new", "cur"):
|
|
176
|
+
(path / name).mkdir(parents=True, exist_ok=True)
|
|
177
|
+
return path
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def is_mailbox(path: Path) -> bool:
|
|
181
|
+
return all((path / name).is_dir() for name in ("tmp", "new", "cur"))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def init_root(root: str | Path) -> RootPaths:
|
|
185
|
+
paths = paths_for(root)
|
|
186
|
+
paths.root.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
for directory in (
|
|
188
|
+
paths.meta,
|
|
189
|
+
paths.sessions,
|
|
190
|
+
paths.actors,
|
|
191
|
+
paths.queues,
|
|
192
|
+
paths.artifacts / "blobs" / "sha256",
|
|
193
|
+
paths.archives / "sessions",
|
|
194
|
+
paths.indexes,
|
|
195
|
+
paths.state,
|
|
196
|
+
paths.hooks,
|
|
197
|
+
paths.integrations,
|
|
198
|
+
):
|
|
199
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
|
|
201
|
+
version_path = paths.meta / "VERSION"
|
|
202
|
+
if version_path.exists():
|
|
203
|
+
current = version_path.read_text(encoding="utf-8").strip()
|
|
204
|
+
if current != STORE_VERSION:
|
|
205
|
+
raise AgentDirError(
|
|
206
|
+
f"Unsupported AgentDir root version {current!r}; expected {STORE_VERSION}"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
version_path.write_text(f"{STORE_VERSION}\n", encoding="utf-8")
|
|
210
|
+
|
|
211
|
+
config_path = paths.meta / "config.json"
|
|
212
|
+
if not config_path.exists():
|
|
213
|
+
config = {"version": STORE_VERSION, "index": str(paths.index_path.relative_to(paths.root))}
|
|
214
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
215
|
+
return paths
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def require_root(root: str | Path) -> RootPaths:
|
|
219
|
+
paths = paths_for(root)
|
|
220
|
+
if not (paths.meta / "VERSION").is_file():
|
|
221
|
+
raise AgentDirError(f"Not an AgentDir root: {paths.root}")
|
|
222
|
+
return paths
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def session_mailbox(root: str | Path, session_id: str, create: bool = True) -> Path:
|
|
226
|
+
paths = require_root(root)
|
|
227
|
+
validate_id(session_id, "session id")
|
|
228
|
+
mailbox = paths.sessions / session_id / "Maildir"
|
|
229
|
+
if create:
|
|
230
|
+
ensure_mailbox(mailbox)
|
|
231
|
+
return mailbox
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def actor_dir(root: str | Path, actor_id: str) -> Path:
|
|
235
|
+
paths = require_root(root)
|
|
236
|
+
validate_id(actor_id, "actor id")
|
|
237
|
+
return paths.actors / actor_id
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def actor_mailbox(root: str | Path, actor_id: str, box: str, create: bool = True) -> Path:
|
|
241
|
+
if box not in {"inbox", "outbox"}:
|
|
242
|
+
raise AgentDirError(f"Unknown actor mailbox {box!r}")
|
|
243
|
+
mailbox = actor_dir(root, actor_id) / box / "Maildir"
|
|
244
|
+
if create:
|
|
245
|
+
ensure_mailbox(mailbox)
|
|
246
|
+
return mailbox
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def queue_mailbox(root: str | Path, queue_id: str, create: bool = True) -> Path:
|
|
250
|
+
paths = require_root(root)
|
|
251
|
+
validate_id(queue_id, "queue id")
|
|
252
|
+
mailbox = paths.queues / queue_id / "Maildir"
|
|
253
|
+
if create:
|
|
254
|
+
ensure_mailbox(mailbox)
|
|
255
|
+
return mailbox
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def discover_mailboxes(root: str | Path) -> list[tuple[str, Path]]:
|
|
259
|
+
paths = require_root(root)
|
|
260
|
+
discovered: list[tuple[str, Path]] = []
|
|
261
|
+
for session in sorted(paths.sessions.glob("*/Maildir")):
|
|
262
|
+
if is_mailbox(session):
|
|
263
|
+
discovered.append(("session", session))
|
|
264
|
+
for actor_box in sorted(paths.actors.glob("*/*/Maildir")):
|
|
265
|
+
if is_mailbox(actor_box):
|
|
266
|
+
discovered.append(("actor", actor_box))
|
|
267
|
+
for queue in sorted(paths.queues.glob("*/Maildir")):
|
|
268
|
+
if is_mailbox(queue):
|
|
269
|
+
discovered.append(("queue", queue))
|
|
270
|
+
return discovered
|
agentdir/upgrade.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .store import AgentDirError
|
|
15
|
+
|
|
16
|
+
DEFAULT_REPO = "jstxn/agentdir"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class UpgradeOptions:
|
|
21
|
+
repo: str = DEFAULT_REPO
|
|
22
|
+
version: str | None = None
|
|
23
|
+
adopt: bool = True
|
|
24
|
+
install_skill: str = "user"
|
|
25
|
+
hooks: bool = True
|
|
26
|
+
dry_run: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def upgrade_agentdir(options: UpgradeOptions) -> dict[str, Any]:
|
|
30
|
+
target_version = options.version or _latest_release_tag(options.repo, dry_run=options.dry_run)
|
|
31
|
+
cwd = Path.cwd()
|
|
32
|
+
repo_root = _git_root(cwd) if options.adopt else None
|
|
33
|
+
before = _command_version(_agentdir_bin())
|
|
34
|
+
result: dict[str, Any] = {
|
|
35
|
+
"current_process_version": __version__,
|
|
36
|
+
"installed_version_before": before,
|
|
37
|
+
"target_version": target_version,
|
|
38
|
+
"repo": options.repo,
|
|
39
|
+
"dry_run": options.dry_run,
|
|
40
|
+
"install": {"planned": True, "ok": None},
|
|
41
|
+
"adopt": {
|
|
42
|
+
"planned": bool(repo_root),
|
|
43
|
+
"ok": None,
|
|
44
|
+
"root": str(repo_root) if repo_root else None,
|
|
45
|
+
"install_skill": options.install_skill,
|
|
46
|
+
"hooks": options.hooks,
|
|
47
|
+
},
|
|
48
|
+
"doctor": {"planned": bool(repo_root), "ok": None},
|
|
49
|
+
}
|
|
50
|
+
if options.dry_run:
|
|
51
|
+
result["install"]["command"] = _install_description(options.repo, target_version)
|
|
52
|
+
if repo_root:
|
|
53
|
+
result["adopt"]["command"] = _adopt_description(options.install_skill, options.hooks)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
install_output = _run_installer(options.repo, target_version)
|
|
57
|
+
result["install"].update({"ok": install_output.returncode == 0, "stderr": install_output.stderr})
|
|
58
|
+
if install_output.returncode != 0:
|
|
59
|
+
result["install"]["stdout"] = install_output.stdout
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
agentdir_bin = _agentdir_bin()
|
|
63
|
+
after = _command_version(agentdir_bin)
|
|
64
|
+
result["installed_version_after"] = after
|
|
65
|
+
result["install"]["version_ok"] = _version_matches(after, target_version)
|
|
66
|
+
if repo_root:
|
|
67
|
+
adopt_output = _run_adopt(agentdir_bin, repo_root, options.install_skill, options.hooks)
|
|
68
|
+
result["adopt"].update(
|
|
69
|
+
{
|
|
70
|
+
"ok": adopt_output.returncode == 0,
|
|
71
|
+
"stdout": adopt_output.stdout,
|
|
72
|
+
"stderr": adopt_output.stderr,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
doctor_output = subprocess.run(
|
|
76
|
+
[agentdir_bin, "doctor", "--json"],
|
|
77
|
+
cwd=repo_root,
|
|
78
|
+
text=True,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
check=False,
|
|
81
|
+
)
|
|
82
|
+
doctor_payload = _json_or_text(doctor_output.stdout)
|
|
83
|
+
result["doctor"].update(
|
|
84
|
+
{
|
|
85
|
+
"ok": doctor_output.returncode == 0,
|
|
86
|
+
"result": doctor_payload,
|
|
87
|
+
"stderr": doctor_output.stderr,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def format_upgrade_result(result: dict[str, Any]) -> str:
|
|
94
|
+
lines = [
|
|
95
|
+
f"current_process_version={result['current_process_version']}",
|
|
96
|
+
f"installed_version_before={result.get('installed_version_before') or 'unknown'}",
|
|
97
|
+
f"target_version={result['target_version']}",
|
|
98
|
+
]
|
|
99
|
+
if result["dry_run"]:
|
|
100
|
+
lines.append(f"install={result['install']['command']}")
|
|
101
|
+
if result["adopt"]["planned"]:
|
|
102
|
+
lines.append(f"adopt={result['adopt']['command']}")
|
|
103
|
+
else:
|
|
104
|
+
lines.append("adopt=skipped outside git repository")
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
lines.append(f"install_ok={str(bool(result['install']['ok'])).lower()}")
|
|
108
|
+
if not result["install"]["ok"]:
|
|
109
|
+
lines.append("upgrade failed during reinstall")
|
|
110
|
+
return "\n".join(lines)
|
|
111
|
+
lines.append(f"installed_version_after={result.get('installed_version_after') or 'unknown'}")
|
|
112
|
+
lines.append(f"install_version_ok={str(bool(result['install'].get('version_ok'))).lower()}")
|
|
113
|
+
if result["adopt"]["planned"]:
|
|
114
|
+
lines.append(f"adopt_ok={str(bool(result['adopt']['ok'])).lower()}")
|
|
115
|
+
lines.append(f"adopt_root={result['adopt']['root']}")
|
|
116
|
+
else:
|
|
117
|
+
lines.append("adopt=skipped outside git repository")
|
|
118
|
+
if result["doctor"]["planned"]:
|
|
119
|
+
lines.append(f"doctor_ok={str(bool(result['doctor']['ok'])).lower()}")
|
|
120
|
+
doctor_result = result["doctor"].get("result")
|
|
121
|
+
if isinstance(doctor_result, dict):
|
|
122
|
+
for error in doctor_result.get("errors", []):
|
|
123
|
+
lines.append(f"doctor_error={error}")
|
|
124
|
+
for warning in doctor_result.get("warnings", []):
|
|
125
|
+
lines.append(f"doctor_warning={warning}")
|
|
126
|
+
return "\n".join(lines)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def upgrade_exit_code(result: dict[str, Any]) -> int:
|
|
130
|
+
if result["dry_run"]:
|
|
131
|
+
return 0
|
|
132
|
+
if not result["install"]["ok"]:
|
|
133
|
+
return 1
|
|
134
|
+
if not result["install"].get("version_ok"):
|
|
135
|
+
return 1
|
|
136
|
+
if result["adopt"]["planned"] and not result["adopt"]["ok"]:
|
|
137
|
+
return 1
|
|
138
|
+
if result["doctor"]["planned"] and not result["doctor"]["ok"]:
|
|
139
|
+
return 1
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _latest_release_tag(repo: str, *, dry_run: bool) -> str:
|
|
144
|
+
if dry_run:
|
|
145
|
+
return "latest"
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
["gh", "api", f"repos/{repo}/releases/latest", "--jq", ".tag_name"],
|
|
148
|
+
text=True,
|
|
149
|
+
capture_output=True,
|
|
150
|
+
check=False,
|
|
151
|
+
)
|
|
152
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
153
|
+
raise AgentDirError(result.stderr.strip() or f"could not resolve latest release for {repo}")
|
|
154
|
+
return result.stdout.strip()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_installer(repo: str, version: str) -> subprocess.CompletedProcess[str]:
|
|
158
|
+
with tempfile.TemporaryDirectory(prefix="agentdir-upgrade.") as tmp:
|
|
159
|
+
install_script = Path(tmp) / "install-agentdir.sh"
|
|
160
|
+
fetch = subprocess.run(
|
|
161
|
+
[
|
|
162
|
+
"gh",
|
|
163
|
+
"api",
|
|
164
|
+
"-H",
|
|
165
|
+
"Accept: application/vnd.github.raw",
|
|
166
|
+
f"repos/{repo}/contents/scripts/install.sh?ref={version}",
|
|
167
|
+
],
|
|
168
|
+
text=True,
|
|
169
|
+
capture_output=True,
|
|
170
|
+
check=False,
|
|
171
|
+
)
|
|
172
|
+
if fetch.returncode != 0:
|
|
173
|
+
return fetch
|
|
174
|
+
install_script.write_text(fetch.stdout, encoding="utf-8")
|
|
175
|
+
install_script.chmod(0o700)
|
|
176
|
+
env = os.environ.copy()
|
|
177
|
+
env["AGENTDIR_REPO"] = repo
|
|
178
|
+
env["AGENTDIR_VERSION"] = version
|
|
179
|
+
env["AGENTDIR_WHEEL"] = ""
|
|
180
|
+
return subprocess.run(
|
|
181
|
+
["bash", str(install_script)],
|
|
182
|
+
text=True,
|
|
183
|
+
capture_output=True,
|
|
184
|
+
check=False,
|
|
185
|
+
env=env,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_adopt(
|
|
190
|
+
agentdir_bin: str,
|
|
191
|
+
repo_root: Path,
|
|
192
|
+
install_skill: str,
|
|
193
|
+
hooks: bool,
|
|
194
|
+
) -> subprocess.CompletedProcess[str]:
|
|
195
|
+
command = [agentdir_bin, "adopt", "--install-skill", install_skill]
|
|
196
|
+
if not hooks:
|
|
197
|
+
command.append("--no-hooks")
|
|
198
|
+
return subprocess.run(command, cwd=repo_root, text=True, capture_output=True, check=False)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _git_root(cwd: Path) -> Path | None:
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
204
|
+
cwd=cwd,
|
|
205
|
+
text=True,
|
|
206
|
+
capture_output=True,
|
|
207
|
+
check=False,
|
|
208
|
+
)
|
|
209
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
210
|
+
return None
|
|
211
|
+
return Path(result.stdout.strip()).resolve()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _agentdir_bin() -> str:
|
|
215
|
+
invoked = Path(sys.argv[0])
|
|
216
|
+
if invoked.name == "agentdir" and invoked.exists():
|
|
217
|
+
return str(invoked.resolve())
|
|
218
|
+
return shutil.which("agentdir") or str(Path.home() / ".local" / "bin" / "agentdir")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _command_version(command: str) -> str | None:
|
|
222
|
+
try:
|
|
223
|
+
result = subprocess.run([command, "--version"], text=True, capture_output=True, check=False)
|
|
224
|
+
except OSError:
|
|
225
|
+
return None
|
|
226
|
+
if result.returncode != 0:
|
|
227
|
+
return None
|
|
228
|
+
return result.stdout.strip()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _version_matches(version_output: str | None, target_version: str) -> bool:
|
|
232
|
+
if not version_output:
|
|
233
|
+
return False
|
|
234
|
+
return target_version.lstrip("v") in version_output
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _json_or_text(text: str) -> Any:
|
|
238
|
+
try:
|
|
239
|
+
return json.loads(text)
|
|
240
|
+
except json.JSONDecodeError:
|
|
241
|
+
return text
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _install_description(repo: str, version: str) -> str:
|
|
245
|
+
return f"install {repo}@{version}"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _adopt_description(install_skill: str, hooks: bool) -> str:
|
|
249
|
+
command = f"agentdir adopt --install-skill {install_skill}"
|
|
250
|
+
if not hooks:
|
|
251
|
+
command += " --no-hooks"
|
|
252
|
+
return command
|