grokbot-kit 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/PKG-INFO +1 -1
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/pyproject.toml +1 -1
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/pyproject.toml.orig +1 -1
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/cli.py +17 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/doctor.py +9 -3
- grokbot_kit-0.3.0/src/grokbot_kit/update.py +320 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/LICENSE +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/README.md +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/__init__.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/assets/event.v1.json +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/assets/opencode/grokbot-kit.ts +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/config.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/deliver.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/emit.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/gc.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/launch.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/login.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/models.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/opencode_plugin.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/payload.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/redeliver.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/resolve.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/route.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/run.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/secret.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/setup.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/sinks.py +0 -0
- {grokbot_kit-0.2.2 → grokbot_kit-0.3.0}/src/grokbot_kit/terminal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grokbot-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Grok Bot integration kit: delegate tasks to headless coding agents (opencode, Claude Code, Codex) and wake the right Grok Bot routine when they finish or need input.
|
|
5
5
|
Author: Rodrigo Pasini
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "grokbot-kit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Grok Bot integration kit: delegate tasks to headless coding agents (opencode, Claude Code, Codex) and wake the right Grok Bot routine when they finish or need input."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "grokbot-kit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Grok Bot integration kit: delegate tasks to headless coding agents (opencode, Claude Code, Codex) and wake the right Grok Bot routine when they finish or need input."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -88,6 +88,15 @@ def build_parser() -> "argparse.ArgumentParser":
|
|
|
88
88
|
uninstall = sub.add_parser("uninstall", help="remove the opencode plugin")
|
|
89
89
|
uninstall.add_argument("target", choices=("opencode",))
|
|
90
90
|
|
|
91
|
+
update = sub.add_parser("update", help="check for a new grokbot-kit release and upgrade to it")
|
|
92
|
+
update_sub = update.add_subparsers(dest="update_command", required=True)
|
|
93
|
+
update_sub.add_parser("check", help="tell whether a new release exists").add_argument(
|
|
94
|
+
"--json", action="store_true", help="machine-readable output"
|
|
95
|
+
)
|
|
96
|
+
update_sub.add_parser("apply", help="upgrade with the tool that installed grokbot-kit and rewrite the plugin")
|
|
97
|
+
update_sub.add_parser("skip", help="do not ask again about this version").add_argument("version")
|
|
98
|
+
update_sub.add_parser("later", help="do not ask again for 24 hours")
|
|
99
|
+
|
|
91
100
|
secret = sub.add_parser("secret", help="store secrets in ~/.secrets/grokbot-kit/ (mode 0600)")
|
|
92
101
|
secret_sub = secret.add_subparsers(dest="secret_command", required=True)
|
|
93
102
|
secret_sub.add_parser("set", help="read a secret from the terminal (no echo) or stdin").add_argument("name")
|
|
@@ -145,6 +154,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
145
154
|
from grokbot_kit import opencode_plugin
|
|
146
155
|
|
|
147
156
|
return opencode_plugin.install() if args.command == "install" else opencode_plugin.uninstall()
|
|
157
|
+
if args.command == "update":
|
|
158
|
+
from grokbot_kit import update
|
|
159
|
+
|
|
160
|
+
if args.update_command == "check":
|
|
161
|
+
return update.check(args.json)
|
|
162
|
+
if args.update_command == "apply":
|
|
163
|
+
return update.apply()
|
|
164
|
+
return update.skip(args.version) if args.update_command == "skip" else update.later()
|
|
148
165
|
if args.command == "secret":
|
|
149
166
|
from grokbot_kit.secret import set_secret
|
|
150
167
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""`grokbot-kit doctor [--json]`: versões, CLIs, login, padrão, tiers, rotas e segredos (AC 100, door 21).
|
|
1
|
+
"""`grokbot-kit doctor [--json]`: versões, CLIs, login, padrão, tiers, rotas e segredos (AC 100, door 21).
|
|
2
|
+
|
|
3
|
+
`kit.update` traz o objeto da checagem de release (kit-update, AC 34-36, door 4).
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
import json
|
|
4
7
|
import os
|
|
@@ -8,7 +11,7 @@ import subprocess
|
|
|
8
11
|
import sys
|
|
9
12
|
from typing import Any
|
|
10
13
|
|
|
11
|
-
from grokbot_kit import config, launch, opencode_plugin
|
|
14
|
+
from grokbot_kit import config, launch, opencode_plugin, update
|
|
12
15
|
from grokbot_kit.launch import TOOLS
|
|
13
16
|
|
|
14
17
|
TIERS = ("trivial", "standard", "complex")
|
|
@@ -153,7 +156,7 @@ def collect() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
|
153
156
|
out = {
|
|
154
157
|
"ok": True,
|
|
155
158
|
"problems": [],
|
|
156
|
-
"kit": {"version": __version__},
|
|
159
|
+
"kit": {"version": __version__, "update": update.status()},
|
|
157
160
|
"python": {"version": platform.python_version(), "executable": sys.executable},
|
|
158
161
|
"setup": {"installed": installed, "default_cli": default},
|
|
159
162
|
"cli": {tool: _cli(tool, cfg.get("cli", {}).get(tool, {}), default) for tool in installed},
|
|
@@ -165,6 +168,9 @@ def collect() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
|
165
168
|
|
|
166
169
|
def _text(out: dict[str, Any]) -> str:
|
|
167
170
|
lines = [f"grokbot-kit {out['kit']['version']} (Python {out['python']['version']}, {out['python']['executable']})"]
|
|
171
|
+
kit_update = out["kit"]["update"]
|
|
172
|
+
if kit_update["update_available"]:
|
|
173
|
+
lines.append(f"update: grokbot-kit {kit_update['latest']} available; run: grokbot-kit update apply")
|
|
168
174
|
for tool, cli in out["cli"].items():
|
|
169
175
|
login = cli["login"]
|
|
170
176
|
state = {True: "logged in", False: "not logged in", None: "unknown"}[login["logged_in"]]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""`grokbot-kit update check|apply|skip|later`: a release nova no PyPI e o upgrade pelo instalador (kit-update).
|
|
2
|
+
|
|
3
|
+
A versão vem do PyPI (door 3) e fica em `~/.grokbot-kit/update.json` (door 2); a checagem vira o objeto da door 4,
|
|
4
|
+
que a skill e o `doctor` leem. O `apply` usa o instalador do recibo em `sys.prefix` (door 5) e regrava o plugin do
|
|
5
|
+
opencode com o executável novo.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from datetime import UTC, datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from grokbot_kit import config, launch, opencode_plugin
|
|
20
|
+
from grokbot_kit.payload import rfc3339
|
|
21
|
+
|
|
22
|
+
PYPI_URL = "https://pypi.org/pypi/grokbot-kit/json"
|
|
23
|
+
FETCH_TIMEOUT = 5
|
|
24
|
+
INSTALL_TIMEOUT = 300
|
|
25
|
+
PLUGIN_TIMEOUT = 60
|
|
26
|
+
FRESH_OK = timedelta(hours=24)
|
|
27
|
+
FRESH_ERROR = timedelta(hours=1)
|
|
28
|
+
LATER = timedelta(hours=24)
|
|
29
|
+
TASK_GRACE = timedelta(seconds=60)
|
|
30
|
+
STDERR_TAIL = 20
|
|
31
|
+
VERSION = re.compile(r"\d+\.\d+\.\d+")
|
|
32
|
+
UPGRADE_ARGV = {"uv": ["uv", "tool", "upgrade", "grokbot-kit"], "pipx": ["pipx", "upgrade", "grokbot-kit"]}
|
|
33
|
+
RECEIPTS = (("uv-receipt.toml", "uv"), ("pipx_metadata.json", "pipx"))
|
|
34
|
+
UNKNOWN_INSTALLER = "cannot tell how grokbot-kit was installed; upgrade it with the tool that installed it"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FetchError(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def state_path() -> Path:
|
|
42
|
+
return Path.home() / ".grokbot-kit" / "update.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def lock_path() -> Path:
|
|
46
|
+
return Path.home() / ".grokbot-kit" / "update.lock"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse(value: Any) -> datetime | None:
|
|
50
|
+
try:
|
|
51
|
+
return datetime.fromisoformat(value) if isinstance(value, str) else None
|
|
52
|
+
except ValueError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def newer(a: str, b: str) -> bool:
|
|
57
|
+
"""`a` > `b`, comparando os três números de `X.Y.Z`."""
|
|
58
|
+
return tuple(map(int, a.split("."))) > tuple(map(int, b.split(".")))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ask(
|
|
62
|
+
update_available: bool, latest: str | None, skipped: str | None, later_until: str | None, now: datetime
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""Perguntar ao usuário: há versão nova, ela não foi pulada e o adiamento venceu (door 4)."""
|
|
65
|
+
until = _parse(later_until)
|
|
66
|
+
return update_available and latest != skipped and (until is None or now >= until)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def installer(prefix: Path) -> str | None:
|
|
70
|
+
"""O instalador do kit pelo recibo em `sys.prefix` (door 5)."""
|
|
71
|
+
for receipt, name in RECEIPTS:
|
|
72
|
+
if (prefix / receipt).exists():
|
|
73
|
+
return name
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def task_running(task_dir: Path, now: datetime) -> bool:
|
|
78
|
+
"""Sem `finished_at` e ainda dentro de `started_at` + `timeout_seconds` + 60 s; na dúvida, parada."""
|
|
79
|
+
try:
|
|
80
|
+
meta = json.loads((task_dir / "meta.json").read_text(encoding="utf-8"))
|
|
81
|
+
if meta["finished_at"] is not None:
|
|
82
|
+
return False
|
|
83
|
+
deadline = datetime.fromisoformat(meta["started_at"]) + timedelta(seconds=meta["timeout_seconds"])
|
|
84
|
+
return bool(deadline + TASK_GRACE > now)
|
|
85
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def running_tasks(now: datetime) -> list[str]:
|
|
90
|
+
root = config.tasks_dir()
|
|
91
|
+
return [d.name for d in sorted(root.iterdir()) if d.is_dir() and task_running(d, now)] if root.is_dir() else []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fetch(url: str) -> str:
|
|
95
|
+
"""O `info.version` do PyPI (door 3); qualquer falha vira `FetchError` com o motivo."""
|
|
96
|
+
from grokbot_kit import __version__
|
|
97
|
+
|
|
98
|
+
request = urllib.request.Request(url, headers={"User-Agent": f"grokbot-kit/{__version__}"})
|
|
99
|
+
try:
|
|
100
|
+
with urllib.request.urlopen(request, timeout=FETCH_TIMEOUT) as response:
|
|
101
|
+
if response.status != 200:
|
|
102
|
+
raise FetchError(f"HTTP {response.status} from {url}")
|
|
103
|
+
data = json.loads(response.read())
|
|
104
|
+
except urllib.error.HTTPError as e:
|
|
105
|
+
raise FetchError(f"HTTP {e.code} from {url}") from None
|
|
106
|
+
except urllib.error.URLError as e:
|
|
107
|
+
raise FetchError(f"cannot reach {url}: {e.reason}") from None
|
|
108
|
+
except TimeoutError:
|
|
109
|
+
raise FetchError(f"{url} did not answer in {FETCH_TIMEOUT}s") from None
|
|
110
|
+
except OSError as e:
|
|
111
|
+
raise FetchError(f"cannot reach {url}: {e}") from None
|
|
112
|
+
except ValueError:
|
|
113
|
+
raise FetchError(f"{url} did not return JSON") from None
|
|
114
|
+
version = data.get("info", {}).get("version") if isinstance(data, dict) else None
|
|
115
|
+
if not isinstance(version, str) or not VERSION.fullmatch(version):
|
|
116
|
+
raise FetchError(f"{url} has no X.Y.Z info.version")
|
|
117
|
+
return version
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_state() -> dict[str, Any] | None:
|
|
121
|
+
try:
|
|
122
|
+
state = json.loads(state_path().read_text(encoding="utf-8"))
|
|
123
|
+
except (OSError, ValueError):
|
|
124
|
+
return None
|
|
125
|
+
return state if isinstance(state, dict) else None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def save_state(state: dict[str, Any]) -> None:
|
|
129
|
+
path = state_path()
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
keys = ("checked_at", "latest", "error", "skipped", "later_until")
|
|
132
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
133
|
+
tmp.write_text(json.dumps({k: state.get(k) for k in keys}), encoding="utf-8")
|
|
134
|
+
os.replace(tmp, path)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _fresh(state: dict[str, Any], now: datetime) -> bool:
|
|
138
|
+
checked = _parse(state.get("checked_at"))
|
|
139
|
+
return checked is not None and now - checked < (FRESH_ERROR if state.get("error") is not None else FRESH_OK)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def refresh(now: datetime, *, force: bool = False) -> dict[str, Any]:
|
|
143
|
+
"""O estado da door 2, consultando o PyPI quando venceu (AC 1, 2 e 6) ou com `force`."""
|
|
144
|
+
state = load_state() or {}
|
|
145
|
+
if force or not _fresh(state, now):
|
|
146
|
+
try:
|
|
147
|
+
latest, error = fetch(os.environ.get("GROKBOT_KIT_PYPI_URL") or PYPI_URL), None
|
|
148
|
+
except FetchError as e:
|
|
149
|
+
latest, error = None, str(e)
|
|
150
|
+
state.update(checked_at=rfc3339(now), latest=latest, error=error)
|
|
151
|
+
save_state(state)
|
|
152
|
+
return state
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def status_of(state: dict[str, Any], now: datetime) -> dict[str, Any]:
|
|
156
|
+
"""O objeto da door 4."""
|
|
157
|
+
from grokbot_kit import __version__
|
|
158
|
+
|
|
159
|
+
latest = state.get("latest")
|
|
160
|
+
available = isinstance(latest, str) and bool(VERSION.fullmatch(latest)) and newer(latest, __version__)
|
|
161
|
+
return {
|
|
162
|
+
"installed": __version__,
|
|
163
|
+
"latest": latest,
|
|
164
|
+
"update_available": available,
|
|
165
|
+
"ask": ask(available, latest, state.get("skipped"), state.get("later_until"), now),
|
|
166
|
+
"skipped": state.get("skipped"),
|
|
167
|
+
"later_until": state.get("later_until"),
|
|
168
|
+
"checked_at": state.get("checked_at"),
|
|
169
|
+
"installer": installer(Path(sys.prefix)),
|
|
170
|
+
"error": state.get("error"),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def status() -> dict[str, Any]:
|
|
175
|
+
now = datetime.now(UTC)
|
|
176
|
+
return status_of(refresh(now), now)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check(as_json: bool) -> int:
|
|
180
|
+
out = status()
|
|
181
|
+
if as_json:
|
|
182
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
183
|
+
elif out["error"] is not None:
|
|
184
|
+
print(f"update check failed: {out['error']}")
|
|
185
|
+
elif out["update_available"]:
|
|
186
|
+
print(f"grokbot-kit {out['latest']} available (installed {out['installed']}); run: grokbot-kit update apply")
|
|
187
|
+
else:
|
|
188
|
+
print(f"grokbot-kit {out['installed']} is the latest")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def skip(version: str) -> int:
|
|
193
|
+
if not VERSION.fullmatch(version):
|
|
194
|
+
print(f"grokbot-kit update skip: {version!r} is not a X.Y.Z version", file=sys.stderr)
|
|
195
|
+
return 2
|
|
196
|
+
state = refresh(datetime.now(UTC))
|
|
197
|
+
state["skipped"] = version
|
|
198
|
+
save_state(state)
|
|
199
|
+
print(f"skipping grokbot-kit {version}")
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def later() -> int:
|
|
204
|
+
now = datetime.now(UTC)
|
|
205
|
+
state = refresh(now)
|
|
206
|
+
state["later_until"] = rfc3339(now + LATER)
|
|
207
|
+
save_state(state)
|
|
208
|
+
print(f"asking again after {state['later_until']}")
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _pid_alive(pid: int) -> bool:
|
|
213
|
+
try:
|
|
214
|
+
os.kill(pid, 0)
|
|
215
|
+
except ProcessLookupError:
|
|
216
|
+
return False
|
|
217
|
+
except PermissionError:
|
|
218
|
+
return True
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _take_lock() -> bool:
|
|
223
|
+
"""O lock do `apply` (AC 26); um lock cujo pid não existe é tomado."""
|
|
224
|
+
path = lock_path()
|
|
225
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
for _ in range(2):
|
|
227
|
+
try:
|
|
228
|
+
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
229
|
+
except FileExistsError:
|
|
230
|
+
try:
|
|
231
|
+
pid = int(path.read_text(encoding="utf-8").strip())
|
|
232
|
+
except (OSError, ValueError):
|
|
233
|
+
pid = 0
|
|
234
|
+
if pid > 0 and _pid_alive(pid):
|
|
235
|
+
return False
|
|
236
|
+
path.unlink(missing_ok=True)
|
|
237
|
+
continue
|
|
238
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
239
|
+
f.write(str(os.getpid()))
|
|
240
|
+
return True
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _fail(code: int, message: str) -> int:
|
|
245
|
+
print(message, file=sys.stderr)
|
|
246
|
+
return code
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def apply() -> int:
|
|
250
|
+
if not _take_lock():
|
|
251
|
+
return _fail(2, f"another grokbot-kit update apply is running ({lock_path()})")
|
|
252
|
+
try:
|
|
253
|
+
return _apply()
|
|
254
|
+
finally:
|
|
255
|
+
lock_path().unlink(missing_ok=True)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _apply() -> int:
|
|
259
|
+
now = datetime.now(UTC)
|
|
260
|
+
state = refresh(now, force=True)
|
|
261
|
+
out = status_of(state, now)
|
|
262
|
+
if out["error"] is not None:
|
|
263
|
+
return _fail(1, f"update check failed: {out['error']}")
|
|
264
|
+
if not out["update_available"]:
|
|
265
|
+
print(f"grokbot-kit {out['installed']} is the latest")
|
|
266
|
+
return 0
|
|
267
|
+
tool = out["installer"]
|
|
268
|
+
if tool is None:
|
|
269
|
+
return _fail(2, UNKNOWN_INSTALLER)
|
|
270
|
+
running = running_tasks(now)
|
|
271
|
+
if running:
|
|
272
|
+
return _fail(2, "tasks still running, try again when they end: " + ", ".join(running))
|
|
273
|
+
argv = UPGRADE_ARGV[tool]
|
|
274
|
+
try:
|
|
275
|
+
upgrade = subprocess.run(
|
|
276
|
+
argv, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=INSTALL_TIMEOUT, check=False
|
|
277
|
+
)
|
|
278
|
+
except subprocess.TimeoutExpired:
|
|
279
|
+
return _fail(1, f"{' '.join(argv)} did not finish in {INSTALL_TIMEOUT}s")
|
|
280
|
+
except OSError as e:
|
|
281
|
+
return _fail(1, f"cannot run {argv[0]}: {e.strerror}")
|
|
282
|
+
if upgrade.returncode != 0:
|
|
283
|
+
tail = "\n".join(upgrade.stderr.splitlines()[-STDERR_TAIL:])
|
|
284
|
+
return _fail(1, f"{' '.join(argv)} exited with {upgrade.returncode}\n{tail}".rstrip())
|
|
285
|
+
exe = launch.kit_executable()
|
|
286
|
+
new = _version_of(exe) or out["latest"]
|
|
287
|
+
if opencode_plugin.plugin_path().exists() and _call([exe, "install", "opencode"]) != 0:
|
|
288
|
+
return _fail(
|
|
289
|
+
1, f"upgraded to {new}, but the opencode plugin was not rewritten; run: grokbot-kit install opencode"
|
|
290
|
+
)
|
|
291
|
+
state.update(skipped=None, later_until=None)
|
|
292
|
+
save_state(state)
|
|
293
|
+
print(f"grokbot-kit {out['installed']} -> {new}")
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _call(argv: list[str]) -> int | None:
|
|
298
|
+
try:
|
|
299
|
+
return subprocess.run(
|
|
300
|
+
argv, stdin=subprocess.DEVNULL, capture_output=True, timeout=PLUGIN_TIMEOUT, check=False
|
|
301
|
+
).returncode
|
|
302
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _version_of(exe: str) -> str | None:
|
|
307
|
+
"""A versão do executável novo (`grokbot-kit X.Y.Z`); o código já carregado ainda é o antigo."""
|
|
308
|
+
try:
|
|
309
|
+
proc = subprocess.run(
|
|
310
|
+
[exe, "--version"],
|
|
311
|
+
stdin=subprocess.DEVNULL,
|
|
312
|
+
capture_output=True,
|
|
313
|
+
text=True,
|
|
314
|
+
timeout=PLUGIN_TIMEOUT,
|
|
315
|
+
check=False,
|
|
316
|
+
)
|
|
317
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
318
|
+
return None
|
|
319
|
+
words = proc.stdout.split()
|
|
320
|
+
return words[-1] if proc.returncode == 0 and words and VERSION.fullmatch(words[-1]) else None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|