hivemind-manager 0.1.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.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-manager
3
+ Version: 0.1.0
4
+ Summary: A developer CLI tool for managing Hivemind services
5
+ Author: dhruv13x
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+
9
+ # Hivemind Manager (hm) CLI
10
+
11
+ `hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
12
+
13
+ ## Installation
14
+
15
+ Install in editable mode in your virtual environment:
16
+
17
+ ```bash
18
+ pip install -e ~/workspace/tools/hivemind_manager
19
+ ```
20
+
21
+ ## Dynamic Dependencies
22
+
23
+ Declare dependencies inside your `*.hm` files using comments:
24
+
25
+ ```bash
26
+ # depends_on: infra
27
+ ```
@@ -0,0 +1,19 @@
1
+ # Hivemind Manager (hm) CLI
2
+
3
+ `hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
4
+
5
+ ## Installation
6
+
7
+ Install in editable mode in your virtual environment:
8
+
9
+ ```bash
10
+ pip install -e ~/workspace/tools/hivemind_manager
11
+ ```
12
+
13
+ ## Dynamic Dependencies
14
+
15
+ Declare dependencies inside your `*.hm` files using comments:
16
+
17
+ ```bash
18
+ # depends_on: infra
19
+ ```
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "hivemind-manager"
3
+ version = "0.1.0"
4
+ description = "A developer CLI tool for managing Hivemind services"
5
+ readme = "README.md"
6
+ authors = [{ name = "dhruv13x" }]
7
+ requires-python = ">=3.8"
8
+ dependencies = []
9
+
10
+ [project.scripts]
11
+ hm = "hm.cli:main"
12
+
13
+ [build-system]
14
+ requires = ["setuptools>=61.0"]
15
+ build-backend = "setuptools.build_meta"
16
+
17
+ [tool.setuptools.package-dir]
18
+ "" = "src"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-manager
3
+ Version: 0.1.0
4
+ Summary: A developer CLI tool for managing Hivemind services
5
+ Author: dhruv13x
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+
9
+ # Hivemind Manager (hm) CLI
10
+
11
+ `hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
12
+
13
+ ## Installation
14
+
15
+ Install in editable mode in your virtual environment:
16
+
17
+ ```bash
18
+ pip install -e ~/workspace/tools/hivemind_manager
19
+ ```
20
+
21
+ ## Dynamic Dependencies
22
+
23
+ Declare dependencies inside your `*.hm` files using comments:
24
+
25
+ ```bash
26
+ # depends_on: infra
27
+ ```
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hivemind_manager.egg-info/PKG-INFO
4
+ src/hivemind_manager.egg-info/SOURCES.txt
5
+ src/hivemind_manager.egg-info/dependency_links.txt
6
+ src/hivemind_manager.egg-info/entry_points.txt
7
+ src/hivemind_manager.egg-info/top_level.txt
8
+ src/hm/__init__.py
9
+ src/hm/cli.py
10
+ src/hm/config.py
11
+ src/hm/discovery.py
12
+ src/hm/process.py
13
+ src/hm/tailer.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hm = hm.cli:main
@@ -0,0 +1 @@
1
+ from .cli import main
@@ -0,0 +1,252 @@
1
+ import sys
2
+ import time
3
+ import subprocess
4
+ from pathlib import Path
5
+ from .config import PROJECT_ROOT, HIVEMIND_BIN
6
+ from .discovery import discover_services
7
+ from .process import (
8
+ read_pid,
9
+ is_running,
10
+ log_file,
11
+ stop_service,
12
+ run_supervised,
13
+ remove_pid
14
+ )
15
+ from .tailer import multi_tail
16
+
17
+ def start(service, follow=True, extra_args=None, started_set=None):
18
+ """
19
+ Starts a service supervisor. Checks and starts dependencies first if they are not running.
20
+ """
21
+ if extra_args is None:
22
+ extra_args = []
23
+ if started_set is None:
24
+ started_set = set()
25
+
26
+ services_meta = discover_services()
27
+ if service not in services_meta:
28
+ print(f"Unknown service: {service}")
29
+ sys.exit(1)
30
+
31
+ if service in started_set:
32
+ return
33
+ started_set.add(service)
34
+
35
+ # 1. Resolve and start dependencies
36
+ deps = services_meta[service]["dependencies"]
37
+ for dep in deps:
38
+ if dep not in services_meta:
39
+ print(f"Warning: [{service}] depends on unknown service '{dep}'")
40
+ continue
41
+
42
+ dep_pid = read_pid(dep)
43
+ if dep_pid and is_running(dep_pid):
44
+ # Dependency already running, no action needed
45
+ continue
46
+
47
+ print(f"[{service}] Dependency '{dep}' is not running. Starting '{dep}' first...")
48
+ start(dep, follow=False, started_set=started_set)
49
+ # Brief pause to allow the dependency supervisor & worker to start
50
+ time.sleep(1.5)
51
+
52
+ # 2. Stop existing service instance to prevent collision
53
+ stop_service(service)
54
+
55
+ logfile = log_file(service)
56
+ # Truncate logs
57
+ open(logfile, "w").close()
58
+
59
+ print(f"[{service}] supervisor starting...")
60
+
61
+ # Spawn supervisor pointing back to the entrypoint CLI wrapper
62
+ entrypoint_script = str(Path(sys.argv[0]).resolve())
63
+ proc = subprocess.Popen(
64
+ [sys.executable, entrypoint_script, "_run", service] + extra_args,
65
+ preexec_fn=os_setsid_safely(),
66
+ cwd=str(PROJECT_ROOT),
67
+ )
68
+
69
+ from .process import write_pid
70
+ write_pid(service, proc.pid)
71
+
72
+ print(f"[{service}] supervisor started (PID {proc.pid})")
73
+
74
+ if follow:
75
+ multi_tail([service])
76
+
77
+
78
+ def os_setsid_safely():
79
+ """
80
+ Safely returns os.setsid reference if available (Unix systems).
81
+ """
82
+ import os
83
+ return getattr(os, "setsid", None)
84
+
85
+
86
+ def ps():
87
+ """
88
+ Lists running services and unmanaged hivemind processes.
89
+ """
90
+ services_meta = discover_services()
91
+ print(f"{'SERVICE':<15} {'STATUS':<10} {'PID':<8}")
92
+ print("-" * 40)
93
+
94
+ supervisor_pids = set()
95
+
96
+ for svc in services_meta:
97
+ pid = read_pid(svc)
98
+ if pid and is_running(pid):
99
+ print(f"{svc:<15} {'running':<10} {pid:<8}")
100
+ supervisor_pids.add(str(pid))
101
+ else:
102
+ print(f"{svc:<15} {'stopped':<10} -")
103
+ remove_pid(svc)
104
+
105
+ try:
106
+ out = subprocess.check_output(["pgrep", "-af", HIVEMIND_BIN], text=True)
107
+ lines = out.strip().split("\n")
108
+
109
+ extra = []
110
+
111
+ for line in lines:
112
+ if not line.strip():
113
+ continue
114
+ parts = line.split()
115
+ pid = parts[0]
116
+
117
+ try:
118
+ ppid = subprocess.check_output(
119
+ ["ps", "-o", "ppid=", "-p", pid],
120
+ text=True
121
+ ).strip()
122
+ except subprocess.CalledProcessError:
123
+ continue
124
+
125
+ if ppid in supervisor_pids or pid in supervisor_pids:
126
+ continue
127
+
128
+ extra.append(line)
129
+
130
+ if extra:
131
+ print("\n[unmanaged hivemind processes]")
132
+ for l in extra:
133
+ print(" ", l)
134
+
135
+ except subprocess.CalledProcessError:
136
+ pass
137
+
138
+
139
+ def status(service=None):
140
+ """
141
+ Shows status of all or a specific service.
142
+ """
143
+ if service is None:
144
+ ps()
145
+ return
146
+
147
+ pid = read_pid(service)
148
+ if pid and is_running(pid):
149
+ print(f"[{service}] running (PID {pid})")
150
+ else:
151
+ print(f"[{service}] stopped")
152
+ remove_pid(service)
153
+
154
+
155
+ def up():
156
+ """
157
+ Starts all discovered services in dynamic order.
158
+ """
159
+ services_meta = discover_services()
160
+ started_set = set()
161
+ for svc in services_meta:
162
+ start(svc, follow=False, started_set=started_set)
163
+ print("\nAll services started.\n")
164
+
165
+
166
+ def down():
167
+ """
168
+ Stops all discovered services.
169
+ """
170
+ services_meta = discover_services()
171
+ for svc in services_meta:
172
+ stop_service(svc)
173
+ print("\nAll services stopped.\n")
174
+
175
+
176
+ def usage():
177
+ cmd = Path(sys.argv[0]).name
178
+ print(f"""
179
+ Usage:
180
+ {cmd} start <service> [--no-follow]
181
+ {cmd} stop <service>
182
+ {cmd} restart <service>
183
+ {cmd} status [service]
184
+ {cmd} logs <service> [service2...]
185
+ {cmd} ps
186
+ {cmd} up
187
+ {cmd} down
188
+ """)
189
+
190
+
191
+ def main():
192
+ if len(sys.argv) < 2:
193
+ usage()
194
+ sys.exit(1)
195
+
196
+ cmd = sys.argv[1]
197
+
198
+ if cmd == "_run":
199
+ if len(sys.argv) < 3:
200
+ print("Error: service name required for _run")
201
+ sys.exit(1)
202
+ service = sys.argv[2]
203
+ extra_args = sys.argv[3:]
204
+ run_supervised(service, extra_args)
205
+ return
206
+
207
+ if cmd in ("up", "down"):
208
+ if cmd == "up":
209
+ up()
210
+ else:
211
+ down()
212
+ return
213
+
214
+ if cmd == "ps":
215
+ ps()
216
+ return
217
+
218
+ if cmd == "status":
219
+ if len(sys.argv) == 2:
220
+ status(None)
221
+ else:
222
+ status(sys.argv[2])
223
+ return
224
+
225
+ if cmd == "logs":
226
+ if len(sys.argv) < 3:
227
+ usage()
228
+ return
229
+ multi_tail(sys.argv[2:])
230
+ return
231
+
232
+ if len(sys.argv) < 3:
233
+ usage()
234
+ sys.exit(1)
235
+
236
+ service = sys.argv[2]
237
+ extra_args = sys.argv[3:]
238
+
239
+ follow = True
240
+ if "--no-follow" in extra_args:
241
+ follow = False
242
+ extra_args.remove("--no-follow")
243
+
244
+ if cmd == "start":
245
+ start(service, follow, extra_args)
246
+ elif cmd == "stop":
247
+ stop_service(service)
248
+ elif cmd == "restart":
249
+ stop_service(service)
250
+ start(service, follow, extra_args)
251
+ else:
252
+ usage()
@@ -0,0 +1,33 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ def find_project_root():
6
+ """
7
+ Traverses upwards from the current working directory to find a directory
8
+ containing *.hm files. Falls back to the current directory if none are found.
9
+ """
10
+ current_dir = Path(os.getcwd()).resolve()
11
+ for parent in [current_dir] + list(current_dir.parents):
12
+ if list(parent.glob("*.hm")):
13
+ return parent
14
+ return current_dir
15
+
16
+ PROJECT_ROOT = find_project_root()
17
+ BASE_DIR = PROJECT_ROOT / "hm"
18
+ BASE_DIR.mkdir(exist_ok=True)
19
+
20
+ HIVEMIND_BIN = "hivemind"
21
+
22
+ RESTART_DELAY = 1.0
23
+ MAX_RESTART_DELAY = 10.0
24
+
25
+ COLORS = [
26
+ "\033[36m", # cyan
27
+ "\033[33m", # yellow
28
+ "\033[35m", # magenta
29
+ "\033[32m", # green
30
+ "\033[34m", # blue
31
+ "\033[31m", # red
32
+ ]
33
+ RESET = "\033[0m"
@@ -0,0 +1,32 @@
1
+ import re
2
+ from .config import PROJECT_ROOT
3
+
4
+ def discover_services():
5
+ """
6
+ Scans the project root for *.hm files and dynamically builds the services registry,
7
+ including dependencies parsed from comments inside each file (e.g., # depends_on: infra).
8
+ """
9
+ services = {}
10
+ for path in sorted(PROJECT_ROOT.glob("*.hm")):
11
+ service_name = path.stem
12
+ dependencies = []
13
+
14
+ try:
15
+ with open(path, "r") as f:
16
+ for line in f:
17
+ # Parse line like "# depends_on: infra, database"
18
+ match = re.match(r"^\s*#\s*depends_on\s*:\s*(.*)$", line, re.IGNORECASE)
19
+ if match:
20
+ deps = match.group(1).split(",")
21
+ for dep in deps:
22
+ dep = dep.strip()
23
+ if dep:
24
+ dependencies.append(dep)
25
+ except Exception:
26
+ pass
27
+
28
+ services[service_name] = {
29
+ "path": path,
30
+ "dependencies": dependencies
31
+ }
32
+ return services
@@ -0,0 +1,173 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import signal
5
+ import subprocess
6
+ from .config import BASE_DIR, PROJECT_ROOT, HIVEMIND_BIN, RESTART_DELAY, MAX_RESTART_DELAY
7
+
8
+ def pid_file(service):
9
+ return BASE_DIR / f"{service}.pid"
10
+
11
+
12
+ def log_file(service):
13
+ return BASE_DIR / f"{service}.log"
14
+
15
+
16
+ def is_running(pid):
17
+ try:
18
+ os.kill(pid, 0)
19
+ return True
20
+ except OSError:
21
+ return False
22
+
23
+
24
+ def read_pid(service):
25
+ f = pid_file(service)
26
+ if not f.exists():
27
+ return None
28
+ try:
29
+ return int(f.read_text().strip())
30
+ except:
31
+ return None
32
+
33
+
34
+ def write_pid(service, pid):
35
+ pid_file(service).write_text(str(pid))
36
+
37
+
38
+ def remove_pid(service):
39
+ pid_file(service).unlink(missing_ok=True)
40
+
41
+
42
+ def stop_service(service):
43
+ """
44
+ Terminates the specified service.
45
+ First tries to gracefully stop using the stored PID.
46
+ Then scans the system for orphaned or unmanaged processes of that service and stops them as well.
47
+ """
48
+ pid = read_pid(service)
49
+ stopped_any = False
50
+
51
+ if pid:
52
+ if is_running(pid):
53
+ print(f"[{service}] stopping (PID {pid})")
54
+ try:
55
+ os.killpg(pid, signal.SIGTERM)
56
+ stopped_any = True
57
+ except ProcessLookupError:
58
+ pass
59
+
60
+ for _ in range(20):
61
+ if not is_running(pid):
62
+ break
63
+ time.sleep(0.2)
64
+
65
+ if is_running(pid):
66
+ print(f"[{service}] force killing")
67
+ try:
68
+ os.killpg(pid, signal.SIGKILL)
69
+ except ProcessLookupError:
70
+ pass
71
+ remove_pid(service)
72
+
73
+ # Terminate any leftover orphaned/unmanaged processes running the .hm file for this service
74
+ try:
75
+ out = subprocess.check_output(["pgrep", "-af", HIVEMIND_BIN], text=True)
76
+ for line in out.strip().split("\n"):
77
+ if not line.strip():
78
+ continue
79
+ parts = line.split()
80
+ p_pid = int(parts[0])
81
+ cmdline = " ".join(parts[1:])
82
+
83
+ if f"{service}.hm" in cmdline:
84
+ print(f"[{service}] found orphaned/unmanaged process (PID {p_pid}): {cmdline}")
85
+ print(f"[{service}] terminating process group for PID {p_pid}...")
86
+ try:
87
+ pgid = os.getpgid(p_pid)
88
+ os.killpg(pgid, signal.SIGTERM)
89
+ # Wait up to 1 second for the process to exit
90
+ for _ in range(10):
91
+ try:
92
+ os.kill(p_pid, 0)
93
+ except OSError:
94
+ break
95
+ time.sleep(0.1)
96
+ # Force kill if still running
97
+ try:
98
+ os.killpg(pgid, signal.SIGKILL)
99
+ except OSError:
100
+ pass
101
+ stopped_any = True
102
+ except OSError:
103
+ try:
104
+ os.kill(p_pid, 0) # check existence
105
+ os.kill(p_pid, signal.SIGTERM)
106
+ for _ in range(10):
107
+ try:
108
+ os.kill(p_pid, 0)
109
+ except OSError:
110
+ break
111
+ time.sleep(0.1)
112
+ os.kill(p_pid, signal.SIGKILL)
113
+ stopped_any = True
114
+ except OSError:
115
+ pass
116
+ except subprocess.CalledProcessError:
117
+ pass
118
+
119
+ if not pid and not stopped_any:
120
+ print(f"[{service}] not running")
121
+
122
+
123
+ def run_supervised(service, extra_args):
124
+ """
125
+ Supervisor process loop. Monitors a single hivemind worker and restarts it if it crashes.
126
+ """
127
+ logfile = log_file(service)
128
+
129
+ stop_flag = False
130
+ child_proc = None
131
+
132
+ def handle_exit(signum, frame):
133
+ nonlocal stop_flag, child_proc
134
+ stop_flag = True
135
+ if child_proc and child_proc.poll() is None:
136
+ try:
137
+ os.killpg(child_proc.pid, signal.SIGTERM)
138
+ except ProcessLookupError:
139
+ pass
140
+
141
+ signal.signal(signal.SIGTERM, handle_exit)
142
+ signal.signal(signal.SIGINT, handle_exit)
143
+
144
+ delay = RESTART_DELAY
145
+
146
+ while not stop_flag:
147
+ with open(logfile, "ab") as f:
148
+ child_proc = subprocess.Popen(
149
+ [HIVEMIND_BIN, f"{service}.hm"] + extra_args,
150
+ stdout=f,
151
+ stderr=subprocess.STDOUT,
152
+ preexec_fn=os.setsid,
153
+ cwd=str(PROJECT_ROOT),
154
+ )
155
+
156
+ print(f"[{service}] worker started (PID {child_proc.pid})")
157
+
158
+ exit_code = child_proc.wait()
159
+
160
+ if stop_flag:
161
+ break
162
+
163
+ print(f"[{service}] worker exited (code {exit_code})")
164
+
165
+ if exit_code == 0:
166
+ break
167
+
168
+ print(f"[{service}] restarting in {delay:.1f}s...")
169
+ time.sleep(delay)
170
+
171
+ delay = min(delay * 2, MAX_RESTART_DELAY)
172
+
173
+ print(f"[{service}] supervisor exiting")
@@ -0,0 +1,51 @@
1
+ import os
2
+ import time
3
+ import threading
4
+ from .config import COLORS, RESET
5
+ from .process import log_file, read_pid, is_running
6
+
7
+ def tail_worker(service, color):
8
+ """
9
+ Tails the log file for a specific service and color-prefixes output.
10
+ Terminates when the service stops running.
11
+ """
12
+ logfile = log_file(service)
13
+
14
+ while not logfile.exists():
15
+ time.sleep(0.1)
16
+
17
+ with open(logfile, "r") as f:
18
+ f.seek(0, os.SEEK_END)
19
+
20
+ while True:
21
+ line = f.readline()
22
+ if line:
23
+ print(line, end="")
24
+ else:
25
+ # auto-stop if service dead
26
+ pid = read_pid(service)
27
+ if not pid or not is_running(pid):
28
+ print(f"{color}[{service}] stopped{RESET}")
29
+ break
30
+ time.sleep(0.2)
31
+
32
+
33
+ def multi_tail(services):
34
+ """
35
+ Spawns log tailing threads for multiple services.
36
+ """
37
+ print(f"Tailing logs: {', '.join(services)}\n")
38
+
39
+ threads = []
40
+
41
+ for i, svc in enumerate(services):
42
+ color = COLORS[i % len(COLORS)]
43
+ t = threading.Thread(target=tail_worker, args=(svc, color), daemon=True)
44
+ t.start()
45
+ threads.append(t)
46
+
47
+ try:
48
+ while True:
49
+ time.sleep(1)
50
+ except KeyboardInterrupt:
51
+ print("\nStopped log tail")