mact-cli 1.0.0__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.
cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """MACT Tunnel Client CLI package"""
cli/cli.py ADDED
@@ -0,0 +1,380 @@
1
+ """MACT CLI for Unit 3 - Tunnel Client
2
+
3
+ Complete implementation with:
4
+ - Developer initialization
5
+ - Room creation with automatic tunnel and hook setup
6
+ - Room joining with automatic tunnel and hook setup
7
+ - Room leaving with tunnel cleanup
8
+ - Status command to show active rooms
9
+
10
+ Features:
11
+ - Automatic frpc tunnel management
12
+ - Git post-commit hook installation
13
+ - Room membership tracking
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+ from typing import Dict, Optional
23
+ import requests
24
+
25
+ from .frpc_manager import FrpcManager, TunnelConfig
26
+ from .hook import install_post_commit
27
+ from .room_config import RoomConfig, RoomMembership
28
+
29
+ def get_config_path() -> Path:
30
+ # Resolve HOME at runtime so tests can monkeypatch HOME before import
31
+ home = os.getenv("HOME") or str(Path.home())
32
+ return Path(home) / ".mact_config.json"
33
+
34
+
35
+ DEFAULT_BACKEND = os.getenv("BACKEND_BASE_URL", "http://localhost:5000")
36
+ DEFAULT_FRP_SERVER = os.getenv("FRP_SERVER_ADDR", "127.0.0.1")
37
+ DEFAULT_FRP_PORT = int(os.getenv("FRP_SERVER_PORT", "7100"))
38
+
39
+
40
+ def load_config() -> Dict[str, str]:
41
+ cfg_path = get_config_path()
42
+ if cfg_path.exists():
43
+ try:
44
+ with cfg_path.open("r") as fh:
45
+ return json.load(fh)
46
+ except Exception:
47
+ return {}
48
+ return {}
49
+
50
+
51
+ def save_config(cfg: Dict[str, str]) -> None:
52
+ cfg_path = get_config_path()
53
+ with cfg_path.open("w") as fh:
54
+ json.dump(cfg, fh)
55
+
56
+
57
+ def cmd_init(args: argparse.Namespace) -> int:
58
+ cfg = load_config()
59
+ cfg["developer_id"] = args.name
60
+ save_config(cfg)
61
+ print(f"Initialized developer_id={args.name} (saved to {get_config_path()})")
62
+ return 0
63
+
64
+
65
+ def cmd_create(args: argparse.Namespace) -> int:
66
+ """Create a new room with automatic tunnel and hook setup."""
67
+ cfg = load_config()
68
+ developer_id = cfg.get("developer_id")
69
+ if not developer_id:
70
+ print("Error: Developer ID not set. Run 'mact init --name <your_name>' first.")
71
+ return 1
72
+
73
+ # Support both syntax styles: positional and -project flag
74
+ project_name = args.project or getattr(args, 'project_flag', None)
75
+ if not project_name:
76
+ print("Error: Project name required. Use: mact create TelegramBot -port 5000")
77
+ return 1
78
+
79
+ # Get local port (support both --local-port and -port)
80
+ local_port = getattr(args, 'port', None) or 3000
81
+
82
+ # Auto-generate subdomain if not provided
83
+ subdomain = args.subdomain
84
+ if not subdomain:
85
+ # Auto-generate: dev-{developer}-{project}
86
+ subdomain = f"dev-{developer_id}-{project_name.lower()}"
87
+
88
+ # Construct full subdomain URL for backend
89
+ # If subdomain is already a full URL, use it; otherwise construct it
90
+ if subdomain.startswith("http://") or subdomain.startswith("https://"):
91
+ subdomain_url = subdomain
92
+ else:
93
+ subdomain_url = f"http://{subdomain}.localhost:7101"
94
+
95
+ # Create room via backend
96
+ payload = {"project_name": project_name, "developer_id": developer_id, "subdomain_url": subdomain_url}
97
+ resp = requests.post(f"{DEFAULT_BACKEND}/rooms/create", json=payload, timeout=5)
98
+ if resp.status_code != 201:
99
+ print(f"Failed to create room: {resp.status_code} {resp.text}")
100
+ return 1
101
+
102
+ data = resp.json()
103
+ room_code = data['room_code']
104
+ print(f"✓ Room created: {room_code} -> {data['public_url']}")
105
+
106
+ # Save room membership
107
+ room_config = RoomConfig()
108
+ membership = RoomMembership(
109
+ room_code=room_code,
110
+ developer_id=developer_id,
111
+ subdomain_url=subdomain_url, # Use the full URL
112
+ local_port=local_port,
113
+ backend_url=DEFAULT_BACKEND
114
+ )
115
+ room_config.add_room(membership)
116
+ print(f"✓ Room membership saved")
117
+
118
+ # Start frpc tunnel if not in test mode
119
+ if not getattr(args, 'no_tunnel', False):
120
+ try:
121
+ frpc = FrpcManager()
122
+ # Extract subdomain from URL or use the subdomain variable (already computed above)
123
+ # subdomain variable contains the correct value (either user-provided or auto-generated)
124
+ tunnel_subdomain = subdomain
125
+ if "//" in subdomain:
126
+ # Full URL provided: extract subdomain part
127
+ tunnel_subdomain = subdomain.split("//")[-1].split(".")[0]
128
+
129
+ tunnel = TunnelConfig(
130
+ room_code=room_code,
131
+ developer_id=developer_id,
132
+ local_port=local_port,
133
+ remote_subdomain=tunnel_subdomain,
134
+ server_addr=DEFAULT_FRP_SERVER,
135
+ server_port=DEFAULT_FRP_PORT
136
+ )
137
+ if frpc.start_tunnel(tunnel):
138
+ print(f"✓ Tunnel started: {tunnel_subdomain} -> localhost:{local_port}")
139
+ else:
140
+ print(f"✗ Failed to start tunnel (continuing anyway)")
141
+ print(f" Tip: Check if frpc binary exists and frps server is running on port {DEFAULT_FRP_PORT}")
142
+ except Exception as e:
143
+ print(f"✗ Tunnel setup failed: {e} (continuing anyway)")
144
+ print(f" Tip: Run with DEBUG=1 for full traceback")
145
+
146
+ # Install git hook if in a git repo
147
+ if not getattr(args, 'no_hook', False):
148
+ git_dir = Path.cwd()
149
+ if (git_dir / ".git").exists():
150
+ try:
151
+ install_post_commit(git_dir, developer_id, room_code, DEFAULT_BACKEND)
152
+ print(f"✓ Git post-commit hook installed")
153
+ except Exception as e:
154
+ print(f"✗ Hook installation failed: {e}")
155
+ else:
156
+ print(f"ℹ Not a git repository; skipping hook installation")
157
+
158
+ print(f"\n✓ Room '{room_code}' is ready!")
159
+ print(f" Public URL: {data['public_url']}")
160
+ print(f" Local dev: http://localhost:{local_port}")
161
+ return 0
162
+
163
+
164
+ def cmd_join(args: argparse.Namespace) -> int:
165
+ """Join an existing room with automatic tunnel and hook setup."""
166
+ cfg = load_config()
167
+ developer_id = cfg.get("developer_id") or args.developer
168
+
169
+ if not developer_id:
170
+ print("Error: Developer ID not set. Run 'mact init --name <your_name>' first or use --developer flag.")
171
+ return 1
172
+
173
+ # Support both syntax styles: positional and -join flag
174
+ room_code = args.room or getattr(args, 'room_flag', None)
175
+ if not room_code:
176
+ print("Error: Room code required. Use: mact join XXXX-XXXX-XXXX -port 5023")
177
+ return 1
178
+
179
+ # Get local port (support both styles)
180
+ local_port = getattr(args, 'port', None) or 3000
181
+
182
+ # Auto-generate subdomain if not provided
183
+ subdomain = args.subdomain
184
+ if not subdomain:
185
+ # Auto-generate: dev-{developer}-{room}
186
+ subdomain = f"dev-{developer_id}-{room_code}"
187
+
188
+ # Construct full subdomain URL for backend
189
+ if subdomain.startswith("http://") or subdomain.startswith("https://"):
190
+ subdomain_url = subdomain
191
+ else:
192
+ subdomain_url = f"http://{subdomain}.localhost:7101"
193
+
194
+ # Join room via backend
195
+ payload = {"room_code": room_code, "developer_id": developer_id, "subdomain_url": subdomain_url}
196
+ resp = requests.post(f"{DEFAULT_BACKEND}/rooms/join", json=payload, timeout=5)
197
+ if resp.status_code != 200:
198
+ print(f"Failed to join room: {resp.status_code} {resp.text}")
199
+ return 1
200
+
201
+ print(f"✓ Joined room: {room_code}")
202
+
203
+ # Save room membership
204
+ room_config = RoomConfig()
205
+ membership = RoomMembership(
206
+ room_code=room_code,
207
+ developer_id=developer_id,
208
+ subdomain_url=subdomain_url,
209
+ local_port=local_port,
210
+ backend_url=DEFAULT_BACKEND
211
+ )
212
+ room_config.add_room(membership)
213
+ print(f"✓ Room membership saved")
214
+
215
+ # Start frpc tunnel
216
+ if not getattr(args, 'no_tunnel', False):
217
+ try:
218
+ frpc = FrpcManager()
219
+ # Extract subdomain from URL or use as-is (subdomain variable already computed above)
220
+ tunnel_subdomain = subdomain
221
+ if "//" in subdomain:
222
+ # Full URL provided: extract subdomain part
223
+ tunnel_subdomain = subdomain.split("//")[-1].split(".")[0]
224
+
225
+ tunnel = TunnelConfig(
226
+ room_code=room_code,
227
+ developer_id=developer_id,
228
+ local_port=local_port,
229
+ remote_subdomain=tunnel_subdomain,
230
+ server_addr=DEFAULT_FRP_SERVER,
231
+ server_port=DEFAULT_FRP_PORT
232
+ )
233
+ if frpc.start_tunnel(tunnel):
234
+ print(f"✓ Tunnel started: {tunnel_subdomain} -> localhost:{local_port}")
235
+ else:
236
+ print(f"✗ Failed to start tunnel (continuing anyway)")
237
+ print(f" Tip: Check if frpc binary exists and frps server is running on port {DEFAULT_FRP_PORT}")
238
+ except Exception as e:
239
+ print(f"✗ Tunnel setup failed: {e} (continuing anyway)")
240
+ print(f" Tip: Run with DEBUG=1 for full traceback")
241
+
242
+ # Install git hook
243
+ if not getattr(args, 'no_hook', False):
244
+ git_dir = Path.cwd()
245
+ if (git_dir / ".git").exists():
246
+ try:
247
+ install_post_commit(git_dir, developer_id, room_code, DEFAULT_BACKEND)
248
+ print(f"✓ Git post-commit hook installed")
249
+ except Exception as e:
250
+ print(f"✗ Hook installation failed: {e}")
251
+ else:
252
+ print(f"ℹ Not a git repository; skipping hook installation")
253
+
254
+ print(f"\n✓ Successfully joined room '{room_code}'!")
255
+ return 0
256
+
257
+
258
+ def cmd_leave(args: argparse.Namespace) -> int:
259
+ """Leave a room and stop tunnel."""
260
+ cfg = load_config()
261
+ developer_id = cfg.get("developer_id") or args.developer
262
+
263
+ if not developer_id:
264
+ print("Error: Developer ID not set. Run 'mact init --name <your_name>' first or use --developer flag.")
265
+ return 1
266
+
267
+ # Leave room via backend
268
+ payload = {"room_code": args.room, "developer_id": developer_id}
269
+ resp = requests.post(f"{DEFAULT_BACKEND}/rooms/leave", json=payload, timeout=5)
270
+ if resp.status_code != 200:
271
+ print(f"Failed to leave room: {resp.status_code} {resp.text}")
272
+ return 1
273
+
274
+ print(f"✓ Left room: {args.room}")
275
+
276
+ # Stop tunnel
277
+ try:
278
+ frpc = FrpcManager()
279
+ if frpc.stop_tunnel(args.room, developer_id):
280
+ print(f"✓ Tunnel stopped")
281
+ except Exception as e:
282
+ print(f"✗ Failed to stop tunnel: {e}")
283
+
284
+ # Remove room membership
285
+ room_config = RoomConfig()
286
+ if room_config.remove_room(args.room):
287
+ print(f"✓ Room membership removed")
288
+
289
+ return 0
290
+
291
+
292
+ def cmd_status(args: argparse.Namespace) -> int:
293
+ """Show active room memberships."""
294
+ room_config = RoomConfig()
295
+ rooms = room_config.list_rooms()
296
+
297
+ if not rooms:
298
+ print("No active room memberships.")
299
+ print("Create a room with: mact create --project <name> --subdomain <url>")
300
+ return 0
301
+
302
+ print(f"Active room memberships ({len(rooms)}):\n")
303
+ for room in rooms:
304
+ print(f" Room: {room.room_code}")
305
+ print(f" Developer: {room.developer_id}")
306
+ print(f" Subdomain: {room.subdomain_url}")
307
+ if room.local_port:
308
+ print(f" Local port: {room.local_port}")
309
+
310
+ # Check tunnel status
311
+ try:
312
+ frpc = FrpcManager()
313
+ if frpc.is_running(room.room_code, room.developer_id):
314
+ print(f" Tunnel: ✓ Running")
315
+ else:
316
+ print(f" Tunnel: ✗ Not running")
317
+ except:
318
+ print(f" Tunnel: ? Unknown")
319
+ print()
320
+
321
+ return 0
322
+
323
+
324
+ def build_parser() -> argparse.ArgumentParser:
325
+ parser = argparse.ArgumentParser(
326
+ prog="mact",
327
+ description="MACT - Mirrored Active Collaborative Tunnel CLI"
328
+ )
329
+ sub = parser.add_subparsers(dest="cmd", help="Command to run")
330
+
331
+ # init command
332
+ p_init = sub.add_parser("init", help="Initialize MACT with your developer ID")
333
+ p_init.add_argument("--name", required=True, help="Your developer ID/name")
334
+ p_init.set_defaults(func=cmd_init)
335
+
336
+ # create command
337
+ p_create = sub.add_parser("create", help="Create a new room")
338
+ p_create.add_argument("project", nargs="?", help="Project name for the room (positional)")
339
+ p_create.add_argument("-project", dest="project_flag", help="Project name (alternative)")
340
+ p_create.add_argument("-port", type=int, default=3000, help="Local port (default: 3000)")
341
+ p_create.add_argument("--subdomain", help="Your subdomain (auto-generated if not provided)")
342
+ p_create.add_argument("--no-tunnel", action="store_true", help="Skip tunnel setup")
343
+ p_create.add_argument("--no-hook", action="store_true", help="Skip git hook installation")
344
+ p_create.set_defaults(func=cmd_create)
345
+
346
+ # join command
347
+ p_join = sub.add_parser("join", help="Join an existing room")
348
+ p_join.add_argument("room", nargs="?", help="Room code to join (positional)")
349
+ p_join.add_argument("-join", dest="room_flag", help="Room code (alternative)")
350
+ p_join.add_argument("-port", type=int, default=3000, help="Local port (default: 3000)")
351
+ p_join.add_argument("--developer", help="Developer ID (uses init value if not specified)")
352
+ p_join.add_argument("--subdomain", help="Your subdomain (auto-generated if not provided)")
353
+ p_join.add_argument("--no-tunnel", action="store_true", help="Skip tunnel setup")
354
+ p_join.add_argument("--no-hook", action="store_true", help="Skip git hook installation")
355
+ p_join.set_defaults(func=cmd_join)
356
+
357
+ # leave command
358
+ p_leave = sub.add_parser("leave", help="Leave a room")
359
+ p_leave.add_argument("--room", required=True, help="Room code to leave")
360
+ p_leave.add_argument("--developer", help="Developer ID (uses init value if not specified)")
361
+ p_leave.set_defaults(func=cmd_leave)
362
+
363
+ # status command
364
+ p_status = sub.add_parser("status", help="Show active room memberships")
365
+ p_status.set_defaults(func=cmd_status)
366
+
367
+ return parser
368
+
369
+
370
+ def main(argv: Optional[list[str]] = None) -> int:
371
+ parser = build_parser()
372
+ args = parser.parse_args(argv)
373
+ if not hasattr(args, "func"):
374
+ parser.print_help()
375
+ return 1
376
+ return args.func(args)
377
+
378
+
379
+ if __name__ == "__main__":
380
+ raise SystemExit(main())
cli/frpc_manager.py ADDED
@@ -0,0 +1,132 @@
1
+ """FRPC tunnel management for MACT CLI.
2
+
3
+ This module handles starting, stopping, and managing frpc tunnel client processes
4
+ for developer rooms.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ @dataclass
18
+ class TunnelConfig:
19
+ """Configuration for a single frpc tunnel."""
20
+
21
+ room_code: str
22
+ developer_id: str
23
+ local_port: int
24
+ remote_subdomain: str
25
+ server_addr: str = "127.0.0.1"
26
+ server_port: int = 7100
27
+
28
+
29
+ class FrpcManager:
30
+ """Manages frpc tunnel client processes."""
31
+
32
+ def __init__(self, frpc_binary: Optional[str] = None):
33
+ self.frpc_binary = frpc_binary or self._find_frpc_binary()
34
+ self._processes: dict[str, subprocess.Popen] = {}
35
+ self._config_files: dict[str, Path] = {}
36
+
37
+ def _find_frpc_binary(self) -> str:
38
+ """Find frpc binary in vendored location or PATH."""
39
+ # Try vendored binary first
40
+ vendored = Path(__file__).parent.parent / "third_party" / "frp" / "frpc"
41
+ if vendored.exists() and vendored.is_file():
42
+ return str(vendored.absolute())
43
+
44
+ # Try PATH
45
+ frpc_path = shutil.which("frpc")
46
+ if frpc_path:
47
+ return frpc_path
48
+
49
+ raise RuntimeError("frpc binary not found. Please install frp or set FRPC_BIN environment variable.")
50
+
51
+ def _generate_config(self, tunnel: TunnelConfig) -> str:
52
+ """Generate frpc TOML configuration."""
53
+ return f"""# MACT frpc config for room {tunnel.room_code}
54
+ serverAddr = "{tunnel.server_addr}"
55
+ serverPort = {tunnel.server_port}
56
+
57
+ [[proxies]]
58
+ name = "{tunnel.room_code}-{tunnel.developer_id}"
59
+ type = "http"
60
+ localIP = "127.0.0.1"
61
+ localPort = {tunnel.local_port}
62
+ subdomain = "{tunnel.remote_subdomain}"
63
+ """
64
+
65
+ def start_tunnel(self, tunnel: TunnelConfig) -> bool:
66
+ """Start an frpc tunnel for the given configuration."""
67
+ key = f"{tunnel.room_code}:{tunnel.developer_id}"
68
+
69
+ # Check if already running
70
+ if key in self._processes:
71
+ proc = self._processes[key]
72
+ if proc.poll() is None:
73
+ return True # Already running
74
+
75
+ # Create temporary config file
76
+ config_content = self._generate_config(tunnel)
77
+ config_file = Path(tempfile.mkdtemp()) / f"frpc_{tunnel.room_code}.toml"
78
+ config_file.write_text(config_content)
79
+ self._config_files[key] = config_file
80
+
81
+ # Start frpc
82
+ try:
83
+ proc = subprocess.Popen(
84
+ [self.frpc_binary, "-c", str(config_file)],
85
+ stdout=subprocess.DEVNULL,
86
+ stderr=subprocess.DEVNULL,
87
+ )
88
+ self._processes[key] = proc
89
+ return True
90
+ except Exception as e:
91
+ print(f"Failed to start frpc: {e}")
92
+ return False
93
+
94
+ def stop_tunnel(self, room_code: str, developer_id: str) -> bool:
95
+ """Stop the frpc tunnel for the given room and developer."""
96
+ key = f"{room_code}:{developer_id}"
97
+
98
+ if key not in self._processes:
99
+ return True # Not running
100
+
101
+ proc = self._processes[key]
102
+ if proc.poll() is None:
103
+ proc.terminate()
104
+ try:
105
+ proc.wait(timeout=5)
106
+ except subprocess.TimeoutExpired:
107
+ proc.kill()
108
+
109
+ # Clean up
110
+ del self._processes[key]
111
+ if key in self._config_files:
112
+ try:
113
+ self._config_files[key].parent.rmdir()
114
+ except:
115
+ pass
116
+ del self._config_files[key]
117
+
118
+ return True
119
+
120
+ def is_running(self, room_code: str, developer_id: str) -> bool:
121
+ """Check if a tunnel is currently running."""
122
+ key = f"{room_code}:{developer_id}"
123
+ if key not in self._processes:
124
+ return False
125
+ proc = self._processes[key]
126
+ return proc.poll() is None
127
+
128
+ def stop_all(self) -> None:
129
+ """Stop all running tunnels."""
130
+ for key in list(self._processes.keys()):
131
+ room, dev = key.split(":")
132
+ self.stop_tunnel(room, dev)
cli/hook.py ADDED
@@ -0,0 +1,45 @@
1
+ """Utilities for installing a Git post-commit hook that calls the backend /report-commit endpoint.
2
+
3
+ This is a minimal helper used by the CLI to install a script into .git/hooks/post-commit
4
+ that will POST to the coordination backend with commit info. The actual hook content
5
+ is intentionally small and can be improved later.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ HOOK_TEMPLATE = """#!/usr/bin/env bash
13
+ # MACT post-commit hook - this script posts commit details to the coordination backend
14
+ # Usage: This file is written into .git/hooks/post-commit
15
+ BACKEND_URL="__BACKEND_URL__"
16
+ DEVELOPER_ID="__DEVELOPER_ID__"
17
+ ROOM_CODE="__ROOM_CODE__"
18
+
19
+ COMMIT_HASH=$(git rev-parse --short HEAD || true)
20
+ BRANCH=$(git rev-parse --abbrev-ref HEAD || true)
21
+ MSG=$(git log -1 --pretty=%B | tr -d '"' | tr -d "'" | head -c 200)
22
+
23
+ if [ -z "$ROOM_CODE" ]; then
24
+ echo "MACT: ROOM_CODE not set; skipping report-commit"
25
+ exit 0
26
+ fi
27
+
28
+ curl -s -X POST "$BACKEND_URL/report-commit" \\
29
+ -H "Content-Type: application/json" \\
30
+ -d "{\\"room_code\\": \\"$ROOM_CODE\\", \\"developer_id\\": \\"$DEVELOPER_ID\\", \\"commit_hash\\": \\"$COMMIT_HASH\\", \\"branch\\": \\"$BRANCH\\", \\"commit_message\\": \\"$MSG\\"}" >/dev/null
31
+
32
+ echo "✓ Commit reported to MACT (Room: $ROOM_CODE)"
33
+ """
34
+
35
+
36
+ def install_post_commit(git_dir: Path, developer_id: str, room_code: str, backend_url: str = "http://localhost:5000") -> None:
37
+ hooks_dir = git_dir / ".git" / "hooks"
38
+ hooks_dir.mkdir(parents=True, exist_ok=True)
39
+ hook_path = hooks_dir / "post-commit"
40
+ content = HOOK_TEMPLATE.replace("__BACKEND_URL__", backend_url)
41
+ content = content.replace("__DEVELOPER_ID__", developer_id)
42
+ content = content.replace("__ROOM_CODE__", room_code)
43
+ with hook_path.open("w") as fh:
44
+ fh.write(content)
45
+ hook_path.chmod(0o755)
cli/room_config.py ADDED
@@ -0,0 +1,90 @@
1
+ """Room configuration management for MACT CLI.
2
+
3
+ Tracks active room memberships and tunnel configurations.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+
12
+
13
+ @dataclass
14
+ class RoomMembership:
15
+ """Represents a developer's membership in a room."""
16
+
17
+ room_code: str
18
+ developer_id: str
19
+ subdomain_url: str
20
+ local_port: Optional[int] = None
21
+ backend_url: str = "http://localhost:5000"
22
+
23
+ def to_dict(self) -> dict:
24
+ return asdict(self)
25
+
26
+ @classmethod
27
+ def from_dict(cls, data: dict) -> "RoomMembership":
28
+ return cls(**data)
29
+
30
+
31
+ class RoomConfig:
32
+ """Manages room membership configuration."""
33
+
34
+ def __init__(self, config_path: Optional[Path] = None):
35
+ if config_path is None:
36
+ home = Path.home()
37
+ config_path = home / ".mact_rooms.json"
38
+ self.config_path = config_path
39
+ self._rooms: Dict[str, RoomMembership] = {}
40
+ self.load()
41
+
42
+ def load(self) -> None:
43
+ """Load room configurations from disk."""
44
+ if not self.config_path.exists():
45
+ self._rooms = {}
46
+ return
47
+
48
+ try:
49
+ with self.config_path.open("r") as fh:
50
+ data = json.load(fh)
51
+ self._rooms = {
52
+ room_code: RoomMembership.from_dict(room_data)
53
+ for room_code, room_data in data.items()
54
+ }
55
+ except Exception:
56
+ self._rooms = {}
57
+
58
+ def save(self) -> None:
59
+ """Save room configurations to disk."""
60
+ data = {
61
+ room_code: room.to_dict()
62
+ for room_code, room in self._rooms.items()
63
+ }
64
+ with self.config_path.open("w") as fh:
65
+ json.dump(data, fh, indent=2)
66
+
67
+ def add_room(self, room: RoomMembership) -> None:
68
+ """Add or update a room membership."""
69
+ self._rooms[room.room_code] = room
70
+ self.save()
71
+
72
+ def remove_room(self, room_code: str) -> bool:
73
+ """Remove a room membership."""
74
+ if room_code in self._rooms:
75
+ del self._rooms[room_code]
76
+ self.save()
77
+ return True
78
+ return False
79
+
80
+ def get_room(self, room_code: str) -> Optional[RoomMembership]:
81
+ """Get room membership by room code."""
82
+ return self._rooms.get(room_code)
83
+
84
+ def list_rooms(self) -> List[RoomMembership]:
85
+ """List all room memberships."""
86
+ return list(self._rooms.values())
87
+
88
+ def has_room(self, room_code: str) -> bool:
89
+ """Check if developer is a member of a room."""
90
+ return room_code in self._rooms
@@ -0,0 +1,324 @@
1
+ Metadata-Version: 2.4
2
+ Name: mact-cli
3
+ Version: 1.0.0
4
+ Summary: MACT (Mirrored Active Collaborative Tunnel) - CLI tool for collaborative development
5
+ Home-page: https://m-act.live
6
+ Author: MACT Team
7
+ Author-email: 22btcs042hy@manuu.edu.in
8
+ License: MIT
9
+ Project-URL: Homepage, https://m-act.live
10
+ Project-URL: Documentation, https://github.com/mact/mact
11
+ Project-URL: Repository, https://github.com/mact/mact
12
+ Project-URL: Issues, https://github.com/mact/mact/issues
13
+ Keywords: tunnel,development,collaboration,git,frp
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: requests>=2.25.0
27
+ Dynamic: author-email
28
+ Dynamic: home-page
29
+ Dynamic: requires-python
30
+
31
+ # MACT (Mirrored Active Collaborative Tunnel)
32
+
33
+ A collaborative development platform that provides a single, persistent public URL for project "rooms" that live-mirrors the localhost of the developer with the latest Git commit.
34
+
35
+ ## 🚀 Quick Start
36
+
37
+ ### For End Users (Install CLI)
38
+
39
+ ```bash
40
+ # Install via pip (easy!)
41
+ pip install git+https://github.com/int33k/M-ACT.git
42
+
43
+ # Initialize
44
+ mact init --name your-name
45
+
46
+ # Create your first room (from your project directory)
47
+ cd ~/your-project
48
+ mact create TelegramBot -port 3000
49
+
50
+ # 🎉 Your room is live!
51
+ # Mirror: https://telegrambot.m-act.live/
52
+ # Dashboard: https://telegrambot.m-act.live/dashboard
53
+ ```
54
+
55
+ See [.docs/QUICK_START.md](.docs/QUICK_START.md) for complete guide.
56
+
57
+ ### For Administrators (Run Server Locally)
58
+
59
+ ```bash
60
+ # 1. Clone and setup
61
+ git clone https://github.com/int33k/M-ACT.git
62
+ cd M-ACT
63
+ python -m venv .venv
64
+ source .venv/bin/activate
65
+ pip install -r requirements.txt
66
+
67
+ # 2. Start services (3 terminals)
68
+ python -m backend.app # Terminal 1: Backend (port 5000)
69
+ ./scripts/run_frp_local.sh # Terminal 2: FRP server (port 7100)
70
+ FRP_AUTOSTART=0 python -m proxy.app # Terminal 3: Proxy (port 9000)
71
+ ```
72
+
73
+ See [INSTALL.md](INSTALL.md) for detailed local development setup.
74
+ See [.docs/PRODUCTION_DEPLOYMENT_GUIDE.md](.docs/PRODUCTION_DEPLOYMENT_GUIDE.md) for production deployment.
75
+
76
+ ### For Server Administrators (Manage Production)
77
+
78
+ Once deployed to DigitalOcean, use the admin CLI to manage rooms and users:
79
+
80
+ ```bash
81
+ # SSH into your droplet
82
+ ssh root@m-act.live
83
+
84
+ # List all rooms
85
+ mact-admin rooms list
86
+
87
+ # Delete a specific room
88
+ mact-admin rooms delete old-project
89
+
90
+ # Clean up empty rooms
91
+ mact-admin rooms cleanup
92
+
93
+ # Check system health
94
+ mact-admin system health
95
+
96
+ # View logs
97
+ mact-admin system logs backend
98
+ ```
99
+
100
+ See [.docs/ADMIN_CLI_GUIDE.md](.docs/ADMIN_CLI_GUIDE.md) for complete admin reference.
101
+
102
+ ## What is MACT?
103
+
104
+ MACT eliminates deployment delays in collaborative development:
105
+
106
+ - 🏠 **Room-based collaboration** - Multiple developers share one permanent URL
107
+ - 🔄 **Git-driven switching** - Latest commit author becomes "active developer"
108
+ - 🪞 **Live mirroring** - Room URL auto-proxies to active developer's localhost
109
+ - 🌐 **Subdomain routing** - Clean URLs: `project-name.m-act.live`
110
+ - ⚡ **Zero configuration** - One CLI command sets up tunnel + git hooks
111
+ - 📊 **Real-time dashboard** - WebSocket-powered status updates
112
+
113
+ ## Architecture
114
+
115
+ ```
116
+ ┌─────────────┐
117
+ │ Browser │ ← User accesses project-name.m-act.live
118
+ └──────┬──────┘
119
+
120
+ ┌──────▼────────────────────────────────────────────┐
121
+ │ Proxy (Port 9000) │
122
+ │ • Subdomain routing │
123
+ │ • WebSocket auto-refresh │
124
+ │ • Dashboard UI │
125
+ └──────┬───────────────┬────────────────────────────┘
126
+ │ │
127
+ ┌──────▼──────┐ ┌────▼───────────────────┐
128
+ │ Backend │ │ FRP Server (Port 7100)│
129
+ │ Port 5000 │ │ • Tunnel multiplexing │
130
+ │ • Rooms │ │ • Vhost HTTP (7101) │
131
+ │ • Commits │ └────┬───────────────────┘
132
+ └─────────────┘ │
133
+ ┌────▼─────┐ ┌──────────┐
134
+ │ Dev A │ │ Dev B │
135
+ │ :3000 │ │ :3001 │
136
+ │ (Active) │ │ (Idle) │
137
+ └──────────┘ └──────────┘
138
+ ```
139
+
140
+ **Four Components:**
141
+ 1. **Backend** - Flask REST API managing rooms, participants, commits
142
+ 2. **Proxy** - Starlette/ASGI server with subdomain routing + WebSocket support
143
+ 3. **CLI** - Developer tool for room creation, joining, and tunnel management
144
+ 4. **FRP** - Fast Reverse Proxy for localhost tunneling
145
+
146
+ ## Features
147
+
148
+ ### ✅ Core Functionality
149
+ - **Subdomain-based routing** - `project-name.localhost:9000` → active developer
150
+ - **Git-driven active developer** - Latest commit author gets the spotlight
151
+ - **WebSocket auto-refresh** - Dashboard & mirror update instantly on commits
152
+ - **Automatic tunnel setup** - One CLI command handles everything
153
+ - **Real-time dashboard** - See participants, commits, active developer
154
+ - **Production security** - Input validation, auth, XSS prevention
155
+
156
+ ### 🧪 Test Coverage
157
+ **36 tests passing** across all components:
158
+ - Backend: 13 tests
159
+ - Proxy: 8 tests
160
+ - CLI: 7 tests
161
+ - FRP Manager: 5 tests
162
+ - Integration: 3 tests
163
+
164
+ ### 📦 Technology Stack
165
+ - **Backend**: Python 3.12 + Flask
166
+ - **Proxy**: Starlette/ASGI + uvicorn
167
+ - **Tunneling**: frp v0.65.0 (vendored)
168
+ - **Testing**: pytest
169
+ - **Security**: Flask-Limiter + input validation
170
+
171
+ ## CLI Usage
172
+
173
+ ```bash
174
+ # 1. Initialize your developer identity
175
+ mact init --name your-name
176
+
177
+ # 2. Create a room (from your git project directory)
178
+ # Simple syntax: mact create PROJECT_NAME -port PORT
179
+ mact create TelegramBot -port 5000
180
+
181
+ # Your room is now accessible at:
182
+ # 🪞 Mirror: https://telegrambot.m-act.live/
183
+ # 📊 Dashboard: https://telegrambot.m-act.live/dashboard
184
+
185
+ # 3. Another developer joins
186
+ mact join telegrambot -port 5001
187
+
188
+ # 4. Make commits - active developer switches automatically!
189
+ git commit -m "My feature" # You become active
190
+ # Mirror now shows YOUR localhost:5000
191
+
192
+ # 5. Check status
193
+ mact status
194
+
195
+ # 6. Leave room
196
+ mact leave --room telegrambot
197
+ ```
198
+
199
+ **Key Features:**
200
+ - ✨ **Simple syntax:** `mact create ProjectName -port 3000`
201
+ - 🚀 **Auto-subdomain:** Generates subdomain automatically
202
+ - 🔧 **Zero config:** One command does everything
203
+ - 📦 **pip install:** No manual setup needed
204
+
205
+ See [`.docs/QUICK_START.md`](.docs/QUICK_START.md) for complete guide.
206
+ See [`cli/README.md`](cli/README.md) for detailed CLI documentation.
207
+
208
+ ## API Endpoints
209
+
210
+ | Endpoint | Description |
211
+ |----------|-------------|
212
+ | `POST /rooms/create` | Create a new room |
213
+ | `POST /rooms/join` | Join an existing room |
214
+ | `POST /report-commit` | Report a Git commit (auto-called by git hook) |
215
+ | `GET /get-active-url` | Get active developer's tunnel URL |
216
+ | `GET /rooms/status` | Get room participants and state |
217
+ | `GET /rooms/<room>/commits` | Get commit history |
218
+ | `GET /health` | Health check |
219
+
220
+ **Proxy Endpoints:**
221
+ | Endpoint | Description |
222
+ |----------|-------------|
223
+ | `http://<room>.localhost:9000/` | Mirror - proxies to active developer |
224
+ | `http://<room>.localhost:9000/dashboard` | Dashboard - room status UI |
225
+ | `ws://<room>.localhost:9000/notifications` | WebSocket - real-time updates |
226
+
227
+ See [`backend/README.md`](backend/README.md) for API examples.
228
+
229
+ ## Port Configuration
230
+
231
+ | Service | Port | Purpose |
232
+ |---------|------|---------|
233
+ | Backend | 5000 | REST API |
234
+ | Proxy | 9000 | Public entry point |
235
+ | FRP Server | 7100 | Tunnel control |
236
+ | FRP VHost | 7101 | HTTP tunnels (subdomain multiplexing) |
237
+ | Developer localhost | 3000+ | Your local dev servers |
238
+
239
+ ## Testing
240
+
241
+ ```bash
242
+ # Run all tests
243
+ pytest tests/ -v
244
+
245
+ # Run specific component
246
+ pytest tests/test_backend.py -v
247
+ pytest tests/test_proxy.py -v
248
+ pytest tests/test_cli.py -v
249
+
250
+ # Run with coverage
251
+ pytest --cov=. tests/
252
+ ```
253
+
254
+ **Test Status**: 36 tests passing (13 backend + 8 proxy + 7 CLI + 5 FRP + 3 integration)
255
+
256
+ ## Production Deployment
257
+
258
+ MACT is production-ready with:
259
+ - ✅ **Systemd services** with auto-restart
260
+ - ✅ **Nginx configuration** for SSL/TLS termination
261
+ - ✅ **Security hardening** (input validation, auth, XSS prevention)
262
+ - ✅ **Deployment scripts** (setup, deploy, rollback)
263
+ - ✅ **Wildcard DNS support** for subdomain routing
264
+
265
+ ### Quick Deploy (Ubuntu 22.04)
266
+
267
+ ```bash
268
+ # 1. Setup server (as root)
269
+ cd deployment/scripts
270
+ ./setup.sh
271
+
272
+ # 2. Configure environment
273
+ cp deployment/mact-*.env.template /etc/mact/
274
+ # Edit env files with your settings
275
+
276
+ # 3. Deploy
277
+ ./deploy.sh
278
+
279
+ # 4. Configure DNS
280
+ # Add wildcard DNS: *.m-act.live → YOUR_SERVER_IP
281
+
282
+ # 5. Setup SSL (Let's Encrypt)
283
+ certbot --nginx -d m-act.live -d "*.m-act.live"
284
+ ```
285
+
286
+ See [`.docs/DEPLOYMENT.md`](.docs/DEPLOYMENT.md) for complete guide.
287
+
288
+ ### Security Features
289
+ - Input validation on all endpoints
290
+ - Bearer token authentication for admin endpoints
291
+ - XSS prevention with HTML sanitization
292
+ - Rate limiting (nginx + Flask-Limiter)
293
+ - Security headers (X-Frame-Options, CSP, etc.)
294
+
295
+ See [`.docs/SECURITY_THREAT_MODEL.md`](.docs/SECURITY_THREAT_MODEL.md) for threat analysis.
296
+
297
+ ## Documentation
298
+
299
+ ### 📚 User Guides
300
+ - **[INSTALL.md](INSTALL.md)** - Installation and setup guide
301
+ - **[cli/README.md](cli/README.md)** - CLI commands and usage
302
+ - **[backend/README.md](backend/README.md)** - API reference
303
+ - **[proxy/README.md](proxy/README.md)** - Proxy configuration
304
+
305
+ ### 🔧 Technical Documentation
306
+ - **[.docs/PROJECT_CONTEXT.md](.docs/PROJECT_CONTEXT.md)** - Architecture and design decisions
307
+ - **[.docs/DEPLOYMENT.md](.docs/DEPLOYMENT.md)** - Production deployment guide
308
+ - **[.docs/SECURITY_THREAT_MODEL.md](.docs/SECURITY_THREAT_MODEL.md)** - Security analysis
309
+ - **[.docs/WEBSOCKET_DESIGN.md](.docs/WEBSOCKET_DESIGN.md)** - WebSocket implementation
310
+ - **[.docs/E2E_TEST_REPORT.md](.docs/E2E_TEST_REPORT.md)** - End-to-end testing results
311
+ - **[ARCHITECTURE_NOTES.md](ARCHITECTURE_NOTES.md)** - URL standardization & nginx setup
312
+
313
+ ### 📖 Additional Resources
314
+ - **[FRP_AUTOMATION.md](FRP_AUTOMATION.md)** - FRP tunnel automation guide
315
+ - **[CLI_QUICKREF.md](CLI_QUICKREF.md)** - Quick CLI reference
316
+ - **[.docs/PROGRESS_LOG.md](.docs/PROGRESS_LOG.md)** - Development history
317
+
318
+ ## Contributing
319
+
320
+ Academic research project. Code follows "Build in Units" methodology with strict adherence to `PROJECT_CONTEXT.md`.
321
+
322
+ ## License
323
+
324
+ MIT License - See LICENSE file for details.
@@ -0,0 +1,10 @@
1
+ cli/__init__.py,sha256=FMFwvK7OHiX5551Bgc6ZwIrmoeraTcXt2AFYg6dnfg0,37
2
+ cli/cli.py,sha256=CtBcRIxgJOhLRB6LY5QkQc1o1ejejs9h9554VZgHROQ,14314
3
+ cli/frpc_manager.py,sha256=Yy5G_EawoXzr8Do138OfTWc1JUw4dz-ZpV7AIXi5gzk,4232
4
+ cli/hook.py,sha256=4SJVBgfyoOvcYgCGSUImSHXMASNDrsoL78aeIbZTzzM,1828
5
+ cli/room_config.py,sha256=N1cFxH0GLlV9YxCkckqAuyfTKsVtXiVWL61bAS4tXNc,2705
6
+ mact_cli-1.0.0.dist-info/METADATA,sha256=IAY24rT0CExAnF7frK0xStbo6v8fPx-UnT9KIwnHLKI,11233
7
+ mact_cli-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ mact_cli-1.0.0.dist-info/entry_points.txt,sha256=5ca2QAJyrq0JI35JzTMFo_QyO0xv2Rz5kluhDtinuOE,38
9
+ mact_cli-1.0.0.dist-info/top_level.txt,sha256=2ImG917oaVHlm0nP9oJE-Qrgs-fq_fGWgba2H1f8fpE,4
10
+ mact_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mact = cli.cli:main
@@ -0,0 +1 @@
1
+ cli