Skip to content

Commit 88e1934

Browse files
author
Neo
committed
feat: openmatrix CLI and one-command installer
Add full CLI with gateway management commands: openmatrix gateway start [-d] openmatrix gateway stop openmatrix gateway status openmatrix gateway restart openmatrix gateway logs [-f] [-n N] openmatrix setup / health / version / config One-command install via curl: curl -sSL https://raw.githubusercontent.com/ItsDardanRexhepi/0pnMatrx/main/install.sh | bash Registered 'openmatrix' as console_scripts entry point in pyproject.toml. Updated start.sh to wrap the CLI with direct fallback.
1 parent bff4afa commit 88e1934

7 files changed

Lines changed: 730 additions & 102 deletions

File tree

cli/__init__.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""openmatrix CLI — command-line interface for 0pnMatrx."""
2+
3+
import argparse
4+
import sys
5+
6+
from cli.gateway import register_gateway_commands
7+
from cli.info import register_info_commands
8+
9+
10+
def build_parser() -> argparse.ArgumentParser:
11+
parser = argparse.ArgumentParser(
12+
prog="openmatrix",
13+
description="0pnMatrx — multi-agent crypto-native platform",
14+
formatter_class=argparse.RawDescriptionHelpFormatter,
15+
epilog="""\
16+
examples:
17+
openmatrix gateway start Start the gateway server
18+
openmatrix gateway start -d Start in background (daemon)
19+
openmatrix gateway stop Stop a running gateway
20+
openmatrix gateway status Check if gateway is running
21+
openmatrix gateway restart Restart the gateway
22+
openmatrix gateway logs Tail gateway logs
23+
openmatrix setup Run interactive setup
24+
openmatrix health Quick health check
25+
openmatrix version Show version
26+
""",
27+
)
28+
29+
sub = parser.add_subparsers(dest="command", metavar="<command>")
30+
31+
register_gateway_commands(sub)
32+
register_info_commands(sub)
33+
34+
return parser
35+
36+
37+
def main(argv: list[str] | None = None) -> None:
38+
parser = build_parser()
39+
args = parser.parse_args(argv)
40+
41+
if not args.command:
42+
parser.print_help()
43+
sys.exit(0)
44+
45+
if hasattr(args, "func"):
46+
try:
47+
args.func(args)
48+
except KeyboardInterrupt:
49+
print("\nInterrupted.")
50+
sys.exit(130)
51+
else:
52+
parser.print_help()
53+
sys.exit(1)

cli/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Allow running the CLI with: python -m cli"""
2+
from cli import main
3+
4+
if __name__ == "__main__":
5+
main()

cli/gateway.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
"""openmatrix gateway — start, stop, status, restart, logs."""
2+
3+
import json
4+
import os
5+
import signal
6+
import subprocess
7+
import sys
8+
import time
9+
from pathlib import Path
10+
11+
# ── Paths ────────────────────────────────────────────────────────────────────
12+
13+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
14+
PID_FILE = PROJECT_ROOT / "data" / "gateway.pid"
15+
LOG_FILE = PROJECT_ROOT / "data" / "gateway.log"
16+
CONFIG_FILE = PROJECT_ROOT / "openmatrix.config.json"
17+
18+
# ── Colors ───────────────────────────────────────────────────────────────────
19+
20+
BOLD = "\033[1m"
21+
CYAN = "\033[36m"
22+
GREEN = "\033[32m"
23+
RED = "\033[31m"
24+
YELLOW = "\033[33m"
25+
DIM = "\033[2m"
26+
NC = "\033[0m"
27+
28+
29+
def _info(msg: str) -> None:
30+
print(f"{GREEN}[openmatrix]{NC} {msg}")
31+
32+
33+
def _warn(msg: str) -> None:
34+
print(f"{YELLOW}[openmatrix]{NC} {msg}")
35+
36+
37+
def _error(msg: str) -> None:
38+
print(f"{RED}[openmatrix]{NC} {msg}", file=sys.stderr)
39+
40+
41+
# ── PID management ───────────────────────────────────────────────────────────
42+
43+
def _read_pid() -> int | None:
44+
if not PID_FILE.exists():
45+
return None
46+
try:
47+
pid = int(PID_FILE.read_text().strip())
48+
# Check if process is actually alive
49+
os.kill(pid, 0)
50+
return pid
51+
except (ValueError, ProcessLookupError, PermissionError):
52+
PID_FILE.unlink(missing_ok=True)
53+
return None
54+
55+
56+
def _write_pid(pid: int) -> None:
57+
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
58+
PID_FILE.write_text(str(pid))
59+
60+
61+
def _remove_pid() -> None:
62+
PID_FILE.unlink(missing_ok=True)
63+
64+
65+
# ── Config ───────────────────────────────────────────────────────────────────
66+
67+
def _load_config() -> dict:
68+
if not CONFIG_FILE.exists():
69+
return {}
70+
return json.loads(CONFIG_FILE.read_text())
71+
72+
73+
def _get_host_port() -> tuple[str, int]:
74+
config = _load_config()
75+
gw = config.get("gateway", {})
76+
return gw.get("host", "0.0.0.0"), gw.get("port", 18790)
77+
78+
79+
# ── Commands ─────────────────────────────────────────────────────────────────
80+
81+
def cmd_start(args) -> None:
82+
"""Start the gateway server."""
83+
existing = _read_pid()
84+
if existing:
85+
_error(f"Gateway is already running (PID {existing}).")
86+
_info(f"Use {CYAN}openmatrix gateway restart{NC} to restart it.")
87+
sys.exit(1)
88+
89+
if not CONFIG_FILE.exists():
90+
_warn("No configuration found.")
91+
_info(f"Run {CYAN}openmatrix setup{NC} first, or create openmatrix.config.json")
92+
sys.exit(1)
93+
94+
host, port = _get_host_port()
95+
96+
# Activate venv if present
97+
venv_python = PROJECT_ROOT / ".venv" / "bin" / "python3"
98+
python = str(venv_python) if venv_python.exists() else sys.executable
99+
100+
if args.daemon:
101+
# Background mode
102+
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
103+
log_fd = open(LOG_FILE, "a")
104+
105+
proc = subprocess.Popen(
106+
[python, "-m", "gateway.server"],
107+
cwd=str(PROJECT_ROOT),
108+
stdout=log_fd,
109+
stderr=log_fd,
110+
start_new_session=True,
111+
)
112+
_write_pid(proc.pid)
113+
114+
# Wait briefly to confirm it started
115+
time.sleep(1.5)
116+
if proc.poll() is not None:
117+
_error("Gateway failed to start. Check logs:")
118+
_info(f" {CYAN}openmatrix gateway logs{NC}")
119+
_remove_pid()
120+
sys.exit(1)
121+
122+
print()
123+
print(f" {CYAN}{BOLD}0pnMatrx Gateway{NC}")
124+
print(f" {DIM}{'─' * 40}{NC}")
125+
_info(f"Running in background (PID {proc.pid})")
126+
_info(f"Listening on {BOLD}http://{host}:{port}{NC}")
127+
_info(f"Logs: {LOG_FILE}")
128+
print()
129+
_info(f"Stop with: {CYAN}openmatrix gateway stop{NC}")
130+
_info(f"View logs: {CYAN}openmatrix gateway logs{NC}")
131+
_info(f"Check status: {CYAN}openmatrix gateway status{NC}")
132+
print()
133+
else:
134+
# Foreground mode
135+
print()
136+
print(f" {CYAN}{BOLD}┌──────────────────────────────────┐{NC}")
137+
print(f" {CYAN}{BOLD}│ 0pnMatrx Gateway — Live │{NC}")
138+
print(f" {CYAN}{BOLD}└──────────────────────────────────┘{NC}")
139+
print()
140+
_info(f"Listening on {BOLD}http://{host}:{port}{NC}")
141+
_info("Press Ctrl+C to stop")
142+
print()
143+
144+
try:
145+
proc = subprocess.run(
146+
[python, "-m", "gateway.server"],
147+
cwd=str(PROJECT_ROOT),
148+
)
149+
sys.exit(proc.returncode)
150+
except KeyboardInterrupt:
151+
print()
152+
_info("Gateway stopped.")
153+
154+
155+
def cmd_stop(args) -> None:
156+
"""Stop the gateway server."""
157+
pid = _read_pid()
158+
if not pid:
159+
_warn("Gateway is not running.")
160+
return
161+
162+
_info(f"Stopping gateway (PID {pid})...")
163+
164+
try:
165+
os.kill(pid, signal.SIGTERM)
166+
167+
# Wait for graceful shutdown (up to 10 seconds)
168+
for _ in range(20):
169+
try:
170+
os.kill(pid, 0)
171+
time.sleep(0.5)
172+
except ProcessLookupError:
173+
break
174+
else:
175+
# Force kill if still running
176+
_warn("Graceful shutdown timed out. Sending SIGKILL...")
177+
try:
178+
os.kill(pid, signal.SIGKILL)
179+
except ProcessLookupError:
180+
pass
181+
except ProcessLookupError:
182+
pass
183+
184+
_remove_pid()
185+
_info("Gateway stopped.")
186+
187+
188+
def cmd_status(args) -> None:
189+
"""Check gateway status."""
190+
pid = _read_pid()
191+
host, port = _get_host_port()
192+
193+
if not pid:
194+
print(f" {RED}{NC} Gateway is {RED}not running{NC}")
195+
print()
196+
_info(f"Start with: {CYAN}openmatrix gateway start{NC}")
197+
return
198+
199+
print(f" {GREEN}{NC} Gateway is {GREEN}running{NC}")
200+
print(f" {DIM}PID:{NC} {pid}")
201+
print(f" {DIM}URL:{NC} http://{host}:{port}")
202+
203+
if LOG_FILE.exists():
204+
size = LOG_FILE.stat().st_size
205+
if size > 1024 * 1024:
206+
print(f" {DIM}Logs:{NC} {size / 1024 / 1024:.1f} MB")
207+
else:
208+
print(f" {DIM}Logs:{NC} {size / 1024:.1f} KB")
209+
210+
# Try a health check
211+
try:
212+
import urllib.request
213+
req = urllib.request.urlopen(f"http://localhost:{port}/health", timeout=3)
214+
if req.status == 200:
215+
data = json.loads(req.read())
216+
uptime = data.get("uptime_seconds", 0)
217+
hours = int(uptime // 3600)
218+
mins = int((uptime % 3600) // 60)
219+
print(f" {DIM}Up:{NC} {hours}h {mins}m")
220+
print(f" {GREEN}{NC} Health: {GREEN}OK{NC}")
221+
else:
222+
print(f" {YELLOW}{NC} Health: responded with {req.status}")
223+
except Exception:
224+
print(f" {YELLOW}{NC} Health: could not reach /health endpoint")
225+
print()
226+
227+
228+
def cmd_restart(args) -> None:
229+
"""Restart the gateway server."""
230+
pid = _read_pid()
231+
if pid:
232+
_info("Stopping current instance...")
233+
cmd_stop(args)
234+
time.sleep(1)
235+
236+
args.daemon = True
237+
cmd_start(args)
238+
239+
240+
def cmd_logs(args) -> None:
241+
"""Tail gateway logs."""
242+
if not LOG_FILE.exists():
243+
_warn("No log file found.")
244+
_info("Gateway may not have been started in daemon mode.")
245+
return
246+
247+
lines = args.lines or 50
248+
follow = args.follow
249+
250+
if follow:
251+
_info(f"Tailing {LOG_FILE} (Ctrl+C to stop)...")
252+
try:
253+
proc = subprocess.run(
254+
["tail", f"-n{lines}", "-f", str(LOG_FILE)],
255+
)
256+
except KeyboardInterrupt:
257+
print()
258+
else:
259+
_info(f"Last {lines} lines from {LOG_FILE}:")
260+
print()
261+
subprocess.run(["tail", f"-n{lines}", str(LOG_FILE)])
262+
263+
264+
# ── Register ─────────────────────────────────────────────────────────────────
265+
266+
def register_gateway_commands(subparsers) -> None:
267+
gw = subparsers.add_parser(
268+
"gateway",
269+
help="Manage the gateway server",
270+
description="Start, stop, and manage the 0pnMatrx gateway server.",
271+
)
272+
gw_sub = gw.add_subparsers(dest="gateway_command", metavar="<action>")
273+
274+
# start
275+
start = gw_sub.add_parser("start", help="Start the gateway server")
276+
start.add_argument(
277+
"-d", "--daemon", action="store_true",
278+
help="Run in background (daemon mode)",
279+
)
280+
start.add_argument(
281+
"-p", "--port", type=int, default=None,
282+
help="Override gateway port",
283+
)
284+
start.set_defaults(func=cmd_start)
285+
286+
# stop
287+
stop = gw_sub.add_parser("stop", help="Stop the gateway server")
288+
stop.set_defaults(func=cmd_stop)
289+
290+
# status
291+
status = gw_sub.add_parser("status", help="Check gateway status")
292+
status.set_defaults(func=cmd_status)
293+
294+
# restart
295+
restart = gw_sub.add_parser("restart", help="Restart the gateway")
296+
restart.set_defaults(func=cmd_restart)
297+
298+
# logs
299+
logs = gw_sub.add_parser("logs", help="View gateway logs")
300+
logs.add_argument(
301+
"-n", "--lines", type=int, default=50,
302+
help="Number of lines to show (default: 50)",
303+
)
304+
logs.add_argument(
305+
"-f", "--follow", action="store_true",
306+
help="Follow log output in real time",
307+
)
308+
logs.set_defaults(func=cmd_logs)
309+
310+
# If no subcommand given, show gateway help
311+
gw.set_defaults(func=lambda a: gw.print_help())

0 commit comments

Comments
 (0)