vibe-remote 2.1.6__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.
- config/__init__.py +37 -0
- config/paths.py +56 -0
- config/v2_compat.py +74 -0
- config/v2_config.py +206 -0
- config/v2_sessions.py +73 -0
- config/v2_settings.py +115 -0
- core/__init__.py +0 -0
- core/controller.py +736 -0
- core/handlers/__init__.py +13 -0
- core/handlers/command_handlers.py +342 -0
- core/handlers/message_handler.py +365 -0
- core/handlers/session_handler.py +233 -0
- core/handlers/settings_handler.py +362 -0
- modules/__init__.py +0 -0
- modules/agent_router.py +58 -0
- modules/agents/__init__.py +38 -0
- modules/agents/base.py +91 -0
- modules/agents/claude_agent.py +344 -0
- modules/agents/codex_agent.py +368 -0
- modules/agents/opencode_agent.py +2155 -0
- modules/agents/service.py +41 -0
- modules/agents/subagent_router.py +136 -0
- modules/claude_client.py +154 -0
- modules/im/__init__.py +63 -0
- modules/im/base.py +323 -0
- modules/im/factory.py +60 -0
- modules/im/formatters/__init__.py +4 -0
- modules/im/formatters/base_formatter.py +639 -0
- modules/im/formatters/slack_formatter.py +127 -0
- modules/im/slack.py +2091 -0
- modules/session_manager.py +138 -0
- modules/settings_manager.py +587 -0
- vibe/__init__.py +6 -0
- vibe/__main__.py +12 -0
- vibe/_version.py +34 -0
- vibe/api.py +412 -0
- vibe/cli.py +637 -0
- vibe/runtime.py +213 -0
- vibe/service_main.py +101 -0
- vibe/templates/slack_manifest.json +65 -0
- vibe/ui/dist/assets/index-8g3mNwMK.js +35 -0
- vibe/ui/dist/assets/index-M55aMB5R.css +1 -0
- vibe/ui/dist/assets/logo-BzryTZ7u.png +0 -0
- vibe/ui/dist/index.html +17 -0
- vibe/ui/dist/logo.png +0 -0
- vibe/ui/dist/vite.svg +1 -0
- vibe/ui_server.py +346 -0
- vibe_remote-2.1.6.dist-info/METADATA +295 -0
- vibe_remote-2.1.6.dist-info/RECORD +52 -0
- vibe_remote-2.1.6.dist-info/WHEEL +4 -0
- vibe_remote-2.1.6.dist-info/entry_points.txt +2 -0
- vibe_remote-2.1.6.dist-info/licenses/LICENSE +21 -0
vibe/cli.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from config import paths
|
|
13
|
+
from config.v2_config import (
|
|
14
|
+
AgentsConfig,
|
|
15
|
+
ClaudeConfig,
|
|
16
|
+
CodexConfig,
|
|
17
|
+
OpenCodeConfig,
|
|
18
|
+
RuntimeConfig,
|
|
19
|
+
SlackConfig,
|
|
20
|
+
V2Config,
|
|
21
|
+
)
|
|
22
|
+
from vibe import __version__, runtime
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _write_json(path, payload):
|
|
26
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _read_json(path):
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return None
|
|
32
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pid_alive(pid):
|
|
36
|
+
try:
|
|
37
|
+
os.kill(pid, 0)
|
|
38
|
+
return True
|
|
39
|
+
except OSError:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _open_browser(url):
|
|
44
|
+
try:
|
|
45
|
+
subprocess.Popen(["open", url])
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _default_config():
|
|
51
|
+
return V2Config(
|
|
52
|
+
mode="self_host",
|
|
53
|
+
version="v2",
|
|
54
|
+
slack=SlackConfig(bot_token="", app_token=""),
|
|
55
|
+
runtime=RuntimeConfig(default_cwd=str(Path.cwd())),
|
|
56
|
+
agents=AgentsConfig(
|
|
57
|
+
default_backend="opencode",
|
|
58
|
+
opencode=OpenCodeConfig(enabled=True, cli_path="opencode"),
|
|
59
|
+
claude=ClaudeConfig(enabled=True, cli_path="claude"),
|
|
60
|
+
codex=CodexConfig(enabled=False, cli_path="codex"),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ensure_config():
|
|
66
|
+
config_path = paths.get_config_path()
|
|
67
|
+
if not config_path.exists():
|
|
68
|
+
default = _default_config()
|
|
69
|
+
default.save(config_path)
|
|
70
|
+
return V2Config.load(config_path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _write_status(state, detail=None):
|
|
74
|
+
payload = {
|
|
75
|
+
"state": state,
|
|
76
|
+
"detail": detail,
|
|
77
|
+
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
78
|
+
}
|
|
79
|
+
_write_json(paths.get_runtime_status_path(), payload)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _spawn_background(
|
|
83
|
+
args,
|
|
84
|
+
pid_path,
|
|
85
|
+
stdout_name: str = "service_stdout.log",
|
|
86
|
+
stderr_name: str = "service_stderr.log",
|
|
87
|
+
):
|
|
88
|
+
stdout_path = paths.get_runtime_dir() / stdout_name
|
|
89
|
+
stderr_path = paths.get_runtime_dir() / stderr_name
|
|
90
|
+
stdout_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
stdout = stdout_path.open("ab")
|
|
92
|
+
stderr = stderr_path.open("ab")
|
|
93
|
+
process = subprocess.Popen(
|
|
94
|
+
args,
|
|
95
|
+
stdout=stdout,
|
|
96
|
+
stderr=stderr,
|
|
97
|
+
start_new_session=True,
|
|
98
|
+
)
|
|
99
|
+
stdout.close()
|
|
100
|
+
stderr.close()
|
|
101
|
+
pid_path.write_text(str(process.pid), encoding="utf-8")
|
|
102
|
+
return process.pid
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _stop_process(pid_path):
|
|
106
|
+
if not pid_path.exists():
|
|
107
|
+
return False
|
|
108
|
+
pid = int(pid_path.read_text(encoding="utf-8").strip())
|
|
109
|
+
if not _pid_alive(pid):
|
|
110
|
+
pid_path.unlink(missing_ok=True)
|
|
111
|
+
return False
|
|
112
|
+
os.kill(pid, signal.SIGTERM)
|
|
113
|
+
pid_path.unlink(missing_ok=True)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _render_status():
|
|
118
|
+
status = _read_json(paths.get_runtime_status_path()) or {}
|
|
119
|
+
pid_path = paths.get_runtime_pid_path()
|
|
120
|
+
pid = pid_path.read_text(encoding="utf-8").strip() if pid_path.exists() else None
|
|
121
|
+
running = bool(pid and pid.isdigit() and _pid_alive(int(pid)))
|
|
122
|
+
status["running"] = running
|
|
123
|
+
status["pid"] = int(pid) if pid and pid.isdigit() else None
|
|
124
|
+
return json.dumps(status, indent=2)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _doctor():
|
|
128
|
+
"""Run diagnostic checks and return results in UI-compatible format.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
{
|
|
132
|
+
"groups": [{"name": "...", "items": [{"status": "pass|warn|fail", "message": "...", "action": "..."}]}],
|
|
133
|
+
"summary": {"pass": 0, "warn": 0, "fail": 0},
|
|
134
|
+
"ok": bool
|
|
135
|
+
}
|
|
136
|
+
"""
|
|
137
|
+
groups = []
|
|
138
|
+
summary = {"pass": 0, "warn": 0, "fail": 0}
|
|
139
|
+
|
|
140
|
+
# Configuration Group
|
|
141
|
+
config_items = []
|
|
142
|
+
config_path = paths.get_config_path()
|
|
143
|
+
|
|
144
|
+
if config_path.exists():
|
|
145
|
+
config_items.append({
|
|
146
|
+
"status": "pass",
|
|
147
|
+
"message": f"Configuration file found: {config_path}",
|
|
148
|
+
})
|
|
149
|
+
summary["pass"] += 1
|
|
150
|
+
else:
|
|
151
|
+
config_items.append({
|
|
152
|
+
"status": "fail",
|
|
153
|
+
"message": "Configuration file not found",
|
|
154
|
+
"action": "Run 'vibe' to create initial configuration",
|
|
155
|
+
})
|
|
156
|
+
summary["fail"] += 1
|
|
157
|
+
|
|
158
|
+
config = None
|
|
159
|
+
try:
|
|
160
|
+
config = V2Config.load(config_path)
|
|
161
|
+
config_items.append({
|
|
162
|
+
"status": "pass",
|
|
163
|
+
"message": "Configuration loaded successfully",
|
|
164
|
+
})
|
|
165
|
+
summary["pass"] += 1
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
config_items.append({
|
|
168
|
+
"status": "fail",
|
|
169
|
+
"message": f"Failed to load configuration: {exc}",
|
|
170
|
+
"action": "Check config.json syntax or delete and reconfigure",
|
|
171
|
+
})
|
|
172
|
+
summary["fail"] += 1
|
|
173
|
+
|
|
174
|
+
groups.append({"name": "Configuration", "items": config_items})
|
|
175
|
+
|
|
176
|
+
# Slack Group
|
|
177
|
+
slack_items = []
|
|
178
|
+
if config:
|
|
179
|
+
try:
|
|
180
|
+
config.slack.validate()
|
|
181
|
+
slack_items.append({
|
|
182
|
+
"status": "pass",
|
|
183
|
+
"message": "Slack token format is valid",
|
|
184
|
+
})
|
|
185
|
+
summary["pass"] += 1
|
|
186
|
+
|
|
187
|
+
# Check if tokens are actually set
|
|
188
|
+
if config.slack.bot_token:
|
|
189
|
+
slack_items.append({
|
|
190
|
+
"status": "pass",
|
|
191
|
+
"message": "Bot token is configured",
|
|
192
|
+
})
|
|
193
|
+
summary["pass"] += 1
|
|
194
|
+
else:
|
|
195
|
+
slack_items.append({
|
|
196
|
+
"status": "warn",
|
|
197
|
+
"message": "Bot token is not configured",
|
|
198
|
+
"action": "Add your Slack bot token in the setup wizard",
|
|
199
|
+
})
|
|
200
|
+
summary["warn"] += 1
|
|
201
|
+
|
|
202
|
+
if config.slack.app_token:
|
|
203
|
+
slack_items.append({
|
|
204
|
+
"status": "pass",
|
|
205
|
+
"message": "App token is configured (Socket Mode)",
|
|
206
|
+
})
|
|
207
|
+
summary["pass"] += 1
|
|
208
|
+
else:
|
|
209
|
+
slack_items.append({
|
|
210
|
+
"status": "warn",
|
|
211
|
+
"message": "App token is not configured",
|
|
212
|
+
"action": "Add your Slack app token for Socket Mode",
|
|
213
|
+
})
|
|
214
|
+
summary["warn"] += 1
|
|
215
|
+
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
slack_items.append({
|
|
218
|
+
"status": "fail",
|
|
219
|
+
"message": f"Slack token validation failed: {exc}",
|
|
220
|
+
"action": "Check your Slack tokens in the setup wizard",
|
|
221
|
+
})
|
|
222
|
+
summary["fail"] += 1
|
|
223
|
+
else:
|
|
224
|
+
slack_items.append({
|
|
225
|
+
"status": "fail",
|
|
226
|
+
"message": "Cannot check Slack: configuration not loaded",
|
|
227
|
+
})
|
|
228
|
+
summary["fail"] += 1
|
|
229
|
+
|
|
230
|
+
groups.append({"name": "Slack", "items": slack_items})
|
|
231
|
+
|
|
232
|
+
# Agent Backends Group
|
|
233
|
+
agent_items = []
|
|
234
|
+
if config:
|
|
235
|
+
# OpenCode
|
|
236
|
+
if config.agents.opencode.enabled:
|
|
237
|
+
cli_path = config.agents.opencode.cli_path
|
|
238
|
+
import shutil
|
|
239
|
+
found_path = shutil.which(cli_path) if cli_path else None
|
|
240
|
+
if found_path:
|
|
241
|
+
agent_items.append({
|
|
242
|
+
"status": "pass",
|
|
243
|
+
"message": f"OpenCode CLI found: {found_path}",
|
|
244
|
+
})
|
|
245
|
+
summary["pass"] += 1
|
|
246
|
+
else:
|
|
247
|
+
agent_items.append({
|
|
248
|
+
"status": "warn",
|
|
249
|
+
"message": f"OpenCode CLI not found: {cli_path}",
|
|
250
|
+
"action": "Install OpenCode or update CLI path",
|
|
251
|
+
})
|
|
252
|
+
summary["warn"] += 1
|
|
253
|
+
else:
|
|
254
|
+
agent_items.append({
|
|
255
|
+
"status": "pass",
|
|
256
|
+
"message": "OpenCode: disabled",
|
|
257
|
+
})
|
|
258
|
+
summary["pass"] += 1
|
|
259
|
+
|
|
260
|
+
# Claude
|
|
261
|
+
if config.agents.claude.enabled:
|
|
262
|
+
cli_path = config.agents.claude.cli_path
|
|
263
|
+
import shutil
|
|
264
|
+
# Check preferred location first
|
|
265
|
+
preferred = Path.home() / ".claude" / "local" / "claude"
|
|
266
|
+
if preferred.exists() and os.access(preferred, os.X_OK):
|
|
267
|
+
found_path = str(preferred)
|
|
268
|
+
else:
|
|
269
|
+
found_path = shutil.which(cli_path) if cli_path else None
|
|
270
|
+
|
|
271
|
+
if found_path:
|
|
272
|
+
agent_items.append({
|
|
273
|
+
"status": "pass",
|
|
274
|
+
"message": f"Claude CLI found: {found_path}",
|
|
275
|
+
})
|
|
276
|
+
summary["pass"] += 1
|
|
277
|
+
else:
|
|
278
|
+
agent_items.append({
|
|
279
|
+
"status": "warn",
|
|
280
|
+
"message": f"Claude CLI not found: {cli_path}",
|
|
281
|
+
"action": "Install Claude Code or update CLI path",
|
|
282
|
+
})
|
|
283
|
+
summary["warn"] += 1
|
|
284
|
+
else:
|
|
285
|
+
agent_items.append({
|
|
286
|
+
"status": "pass",
|
|
287
|
+
"message": "Claude: disabled",
|
|
288
|
+
})
|
|
289
|
+
summary["pass"] += 1
|
|
290
|
+
|
|
291
|
+
# Codex
|
|
292
|
+
if config.agents.codex.enabled:
|
|
293
|
+
cli_path = config.agents.codex.cli_path
|
|
294
|
+
import shutil
|
|
295
|
+
found_path = shutil.which(cli_path) if cli_path else None
|
|
296
|
+
if found_path:
|
|
297
|
+
agent_items.append({
|
|
298
|
+
"status": "pass",
|
|
299
|
+
"message": f"Codex CLI found: {found_path}",
|
|
300
|
+
})
|
|
301
|
+
summary["pass"] += 1
|
|
302
|
+
else:
|
|
303
|
+
agent_items.append({
|
|
304
|
+
"status": "warn",
|
|
305
|
+
"message": f"Codex CLI not found: {cli_path}",
|
|
306
|
+
"action": "Install Codex or update CLI path",
|
|
307
|
+
})
|
|
308
|
+
summary["warn"] += 1
|
|
309
|
+
else:
|
|
310
|
+
agent_items.append({
|
|
311
|
+
"status": "pass",
|
|
312
|
+
"message": "Codex: disabled",
|
|
313
|
+
})
|
|
314
|
+
summary["pass"] += 1
|
|
315
|
+
|
|
316
|
+
# Default backend check
|
|
317
|
+
default_backend = config.agents.default_backend
|
|
318
|
+
agent_items.append({
|
|
319
|
+
"status": "pass",
|
|
320
|
+
"message": f"Default backend: {default_backend}",
|
|
321
|
+
})
|
|
322
|
+
summary["pass"] += 1
|
|
323
|
+
else:
|
|
324
|
+
agent_items.append({
|
|
325
|
+
"status": "fail",
|
|
326
|
+
"message": "Cannot check agents: configuration not loaded",
|
|
327
|
+
})
|
|
328
|
+
summary["fail"] += 1
|
|
329
|
+
|
|
330
|
+
groups.append({"name": "Agent Backends", "items": agent_items})
|
|
331
|
+
|
|
332
|
+
# Runtime Group
|
|
333
|
+
runtime_items = []
|
|
334
|
+
if config:
|
|
335
|
+
cwd = config.runtime.default_cwd
|
|
336
|
+
if cwd and os.path.isdir(cwd):
|
|
337
|
+
runtime_items.append({
|
|
338
|
+
"status": "pass",
|
|
339
|
+
"message": f"Working directory: {cwd}",
|
|
340
|
+
})
|
|
341
|
+
summary["pass"] += 1
|
|
342
|
+
else:
|
|
343
|
+
runtime_items.append({
|
|
344
|
+
"status": "warn",
|
|
345
|
+
"message": f"Working directory does not exist: {cwd}",
|
|
346
|
+
"action": "Update default_cwd in settings",
|
|
347
|
+
})
|
|
348
|
+
summary["warn"] += 1
|
|
349
|
+
|
|
350
|
+
runtime_items.append({
|
|
351
|
+
"status": "pass",
|
|
352
|
+
"message": f"Log level: {config.runtime.log_level}",
|
|
353
|
+
})
|
|
354
|
+
summary["pass"] += 1
|
|
355
|
+
|
|
356
|
+
# Check log file
|
|
357
|
+
log_path = paths.get_logs_dir() / "vibe_remote.log"
|
|
358
|
+
if log_path.exists():
|
|
359
|
+
runtime_items.append({
|
|
360
|
+
"status": "pass",
|
|
361
|
+
"message": f"Log file: {log_path}",
|
|
362
|
+
})
|
|
363
|
+
summary["pass"] += 1
|
|
364
|
+
else:
|
|
365
|
+
runtime_items.append({
|
|
366
|
+
"status": "pass",
|
|
367
|
+
"message": "Log file will be created on first run",
|
|
368
|
+
})
|
|
369
|
+
summary["pass"] += 1
|
|
370
|
+
|
|
371
|
+
groups.append({"name": "Runtime", "items": runtime_items})
|
|
372
|
+
|
|
373
|
+
# Calculate overall status
|
|
374
|
+
ok = summary["fail"] == 0
|
|
375
|
+
|
|
376
|
+
result = {
|
|
377
|
+
"groups": groups,
|
|
378
|
+
"summary": summary,
|
|
379
|
+
"ok": ok,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_write_json(paths.get_runtime_doctor_path(), result)
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def cmd_vibe():
|
|
388
|
+
paths.ensure_data_dirs()
|
|
389
|
+
config = _ensure_config()
|
|
390
|
+
|
|
391
|
+
# Always restart both processes
|
|
392
|
+
runtime.stop_service()
|
|
393
|
+
runtime.stop_ui()
|
|
394
|
+
|
|
395
|
+
if not config.slack.bot_token:
|
|
396
|
+
_write_status("setup", "missing Slack bot token")
|
|
397
|
+
else:
|
|
398
|
+
_write_status("starting")
|
|
399
|
+
|
|
400
|
+
service_pid = runtime.start_service()
|
|
401
|
+
ui_pid = runtime.start_ui(config.ui.setup_host, config.ui.setup_port)
|
|
402
|
+
runtime.write_status("running", "pid={}".format(service_pid), service_pid, ui_pid)
|
|
403
|
+
|
|
404
|
+
ui_url = "http://{}:{}".format(config.ui.setup_host, config.ui.setup_port)
|
|
405
|
+
if config.ui.open_browser:
|
|
406
|
+
_open_browser(ui_url)
|
|
407
|
+
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _stop_opencode_server():
|
|
413
|
+
"""Terminate the OpenCode server if running."""
|
|
414
|
+
pid_file = paths.get_logs_dir() / "opencode_server.json"
|
|
415
|
+
if not pid_file.exists():
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
info = json.loads(pid_file.read_text(encoding="utf-8"))
|
|
420
|
+
except Exception:
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
pid = info.get("pid") if isinstance(info, dict) else None
|
|
424
|
+
if not isinstance(pid, int) or not _pid_alive(pid):
|
|
425
|
+
pid_file.unlink(missing_ok=True)
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
# Verify it's actually an opencode serve process
|
|
429
|
+
try:
|
|
430
|
+
import subprocess
|
|
431
|
+
result = subprocess.run(
|
|
432
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
433
|
+
capture_output=True,
|
|
434
|
+
text=True,
|
|
435
|
+
)
|
|
436
|
+
cmd = result.stdout.strip()
|
|
437
|
+
if "opencode" not in cmd or "serve" not in cmd:
|
|
438
|
+
return False
|
|
439
|
+
except Exception:
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
os.kill(pid, signal.SIGTERM)
|
|
444
|
+
pid_file.unlink(missing_ok=True)
|
|
445
|
+
return True
|
|
446
|
+
except Exception:
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def cmd_stop():
|
|
451
|
+
runtime.stop_service()
|
|
452
|
+
runtime.stop_ui()
|
|
453
|
+
|
|
454
|
+
# Also terminate OpenCode server on full stop
|
|
455
|
+
if _stop_opencode_server():
|
|
456
|
+
print("OpenCode server stopped")
|
|
457
|
+
|
|
458
|
+
_write_status("stopped")
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def cmd_status():
|
|
463
|
+
print(_render_status())
|
|
464
|
+
return 0
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def cmd_doctor():
|
|
468
|
+
result = _doctor()
|
|
469
|
+
|
|
470
|
+
# Terminal-friendly output
|
|
471
|
+
print("\n Vibe Remote Diagnostics")
|
|
472
|
+
print(" " + "=" * 40)
|
|
473
|
+
|
|
474
|
+
for group in result.get("groups", []):
|
|
475
|
+
print(f"\n {group['name']}")
|
|
476
|
+
print(" " + "-" * 30)
|
|
477
|
+
for item in group.get("items", []):
|
|
478
|
+
status = item["status"]
|
|
479
|
+
if status == "pass":
|
|
480
|
+
icon = "\033[32m✓\033[0m" # Green checkmark
|
|
481
|
+
elif status == "warn":
|
|
482
|
+
icon = "\033[33m!\033[0m" # Yellow warning
|
|
483
|
+
else:
|
|
484
|
+
icon = "\033[31m✗\033[0m" # Red X
|
|
485
|
+
|
|
486
|
+
print(f" {icon} {item['message']}")
|
|
487
|
+
if item.get("action"):
|
|
488
|
+
print(f" → {item['action']}")
|
|
489
|
+
|
|
490
|
+
summary = result.get("summary", {})
|
|
491
|
+
print("\n " + "-" * 30)
|
|
492
|
+
print(f" \033[32m{summary.get('pass', 0)} passed\033[0m "
|
|
493
|
+
f"\033[33m{summary.get('warn', 0)} warnings\033[0m "
|
|
494
|
+
f"\033[31m{summary.get('fail', 0)} failed\033[0m")
|
|
495
|
+
print()
|
|
496
|
+
|
|
497
|
+
return 0 if result["ok"] else 1
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def cmd_version():
|
|
501
|
+
"""Show current version."""
|
|
502
|
+
print(f"vibe-remote {__version__}")
|
|
503
|
+
return 0
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def get_latest_version() -> dict:
|
|
507
|
+
"""Fetch latest version info from PyPI.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
{"current": str, "latest": str, "has_update": bool, "error": str|None}
|
|
511
|
+
"""
|
|
512
|
+
current = __version__
|
|
513
|
+
result = {"current": current, "latest": None, "has_update": False, "error": None}
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
url = "https://pypi.org/pypi/vibe-remote/json"
|
|
517
|
+
req = urllib.request.Request(url, headers={"User-Agent": "vibe-remote"})
|
|
518
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
519
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
520
|
+
latest = data.get("info", {}).get("version", "")
|
|
521
|
+
result["latest"] = latest
|
|
522
|
+
|
|
523
|
+
# Simple version comparison (works for semver)
|
|
524
|
+
if latest and latest != current:
|
|
525
|
+
# Compare version tuples
|
|
526
|
+
try:
|
|
527
|
+
current_parts = [int(x) for x in current.split(".")[:3] if x.isdigit()]
|
|
528
|
+
latest_parts = [int(x) for x in latest.split(".")[:3] if x.isdigit()]
|
|
529
|
+
result["has_update"] = latest_parts > current_parts
|
|
530
|
+
except (ValueError, AttributeError):
|
|
531
|
+
# If version format is unusual, just check if different
|
|
532
|
+
result["has_update"] = latest != current
|
|
533
|
+
except Exception as e:
|
|
534
|
+
result["error"] = str(e)
|
|
535
|
+
|
|
536
|
+
return result
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def cmd_check_update():
|
|
540
|
+
"""Check for available updates."""
|
|
541
|
+
print(f"Current version: {__version__}")
|
|
542
|
+
print("Checking for updates...")
|
|
543
|
+
|
|
544
|
+
info = get_latest_version()
|
|
545
|
+
|
|
546
|
+
if info["error"]:
|
|
547
|
+
print(f"\033[33mFailed to check for updates: {info['error']}\033[0m")
|
|
548
|
+
return 1
|
|
549
|
+
|
|
550
|
+
if info["has_update"]:
|
|
551
|
+
print(f"\033[32mNew version available: {info['latest']}\033[0m")
|
|
552
|
+
print(f"\nRun '\033[1mvibe upgrade\033[0m' to update.")
|
|
553
|
+
else:
|
|
554
|
+
print("\033[32mYou are using the latest version.\033[0m")
|
|
555
|
+
|
|
556
|
+
return 0
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def cmd_upgrade():
|
|
560
|
+
"""Upgrade vibe-remote to the latest version."""
|
|
561
|
+
print(f"Current version: {__version__}")
|
|
562
|
+
print("Checking for updates...")
|
|
563
|
+
|
|
564
|
+
info = get_latest_version()
|
|
565
|
+
|
|
566
|
+
if info["error"]:
|
|
567
|
+
print(f"\033[33mFailed to check for updates: {info['error']}\033[0m")
|
|
568
|
+
print("Attempting upgrade anyway...")
|
|
569
|
+
elif not info["has_update"]:
|
|
570
|
+
print("\033[32mYou are already using the latest version.\033[0m")
|
|
571
|
+
return 0
|
|
572
|
+
else:
|
|
573
|
+
print(f"New version available: {info['latest']}")
|
|
574
|
+
|
|
575
|
+
print("\nUpgrading...")
|
|
576
|
+
|
|
577
|
+
# Determine upgrade method based on how vibe was installed
|
|
578
|
+
# Check if running from uv tool environment
|
|
579
|
+
exe_path = sys.executable
|
|
580
|
+
is_uv_tool = ".local/share/uv/tools/" in exe_path or "/uv/tools/" in exe_path
|
|
581
|
+
|
|
582
|
+
uv_path = shutil.which("uv")
|
|
583
|
+
|
|
584
|
+
if is_uv_tool and uv_path:
|
|
585
|
+
# Installed via uv tool, upgrade with uv
|
|
586
|
+
cmd = [uv_path, "tool", "upgrade", "vibe-remote"]
|
|
587
|
+
print(f"Using uv: {' '.join(cmd)}")
|
|
588
|
+
else:
|
|
589
|
+
# Installed via pip or other method, use current Python's pip
|
|
590
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "vibe-remote"]
|
|
591
|
+
print(f"Using pip: {' '.join(cmd)}")
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
595
|
+
if result.returncode == 0:
|
|
596
|
+
print("\033[32mUpgrade successful!\033[0m")
|
|
597
|
+
print("Please restart vibe to use the new version:")
|
|
598
|
+
print(" vibe stop && vibe")
|
|
599
|
+
return 0
|
|
600
|
+
else:
|
|
601
|
+
print(f"\033[31mUpgrade failed:\033[0m\n{result.stderr}")
|
|
602
|
+
return 1
|
|
603
|
+
except Exception as e:
|
|
604
|
+
print(f"\033[31mUpgrade failed: {e}\033[0m")
|
|
605
|
+
return 1
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def build_parser():
|
|
609
|
+
parser = argparse.ArgumentParser(prog="vibe")
|
|
610
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
611
|
+
|
|
612
|
+
subparsers.add_parser("stop", help="Stop all services")
|
|
613
|
+
subparsers.add_parser("status", help="Show service status")
|
|
614
|
+
subparsers.add_parser("doctor", help="Run diagnostics")
|
|
615
|
+
subparsers.add_parser("version", help="Show version")
|
|
616
|
+
subparsers.add_parser("check-update", help="Check for updates")
|
|
617
|
+
subparsers.add_parser("upgrade", help="Upgrade to latest version")
|
|
618
|
+
return parser
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def main():
|
|
622
|
+
parser = build_parser()
|
|
623
|
+
args = parser.parse_args()
|
|
624
|
+
|
|
625
|
+
if args.command == "stop":
|
|
626
|
+
sys.exit(cmd_stop())
|
|
627
|
+
if args.command == "status":
|
|
628
|
+
sys.exit(cmd_status())
|
|
629
|
+
if args.command == "doctor":
|
|
630
|
+
sys.exit(cmd_doctor())
|
|
631
|
+
if args.command == "version":
|
|
632
|
+
sys.exit(cmd_version())
|
|
633
|
+
if args.command == "check-update":
|
|
634
|
+
sys.exit(cmd_check_update())
|
|
635
|
+
if args.command == "upgrade":
|
|
636
|
+
sys.exit(cmd_upgrade())
|
|
637
|
+
sys.exit(cmd_vibe())
|