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 +1 -0
- cli/cli.py +380 -0
- cli/frpc_manager.py +132 -0
- cli/hook.py +45 -0
- cli/room_config.py +90 -0
- mact_cli-1.0.0.dist-info/METADATA +324 -0
- mact_cli-1.0.0.dist-info/RECORD +10 -0
- mact_cli-1.0.0.dist-info/WHEEL +5 -0
- mact_cli-1.0.0.dist-info/entry_points.txt +2 -0
- mact_cli-1.0.0.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
cli
|