mobox-cli 0.1.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.
@@ -0,0 +1,188 @@
1
+ """App management commands: apps, status, control, delete"""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+
7
+ from mobox import utils
8
+ from mobox.client import get_client
9
+
10
+
11
+ def apps():
12
+ """List all deployed applications"""
13
+ client = get_client()
14
+ result = asyncio.run(client.query("list", {"type": "apps"}))
15
+
16
+ if not result.get("success"):
17
+ utils.error(result.get("error", "Failed to list apps"))
18
+ raise typer.Exit(1)
19
+
20
+ apps_data = result.get("apps") or result.get("data", [])
21
+ if not apps_data:
22
+ utils.info("No applications found.")
23
+ return
24
+
25
+ table = utils.make_table(
26
+ "Applications",
27
+ [("Name", "cyan"), ("Status", ""), ("Created", "dim"), ("URL", "blue")],
28
+ [
29
+ [
30
+ a.get("name", "?"),
31
+ _status_style(a.get("status", "?")),
32
+ str(a.get("created_at", "?"))[:10] if a.get("created_at") else "?",
33
+ a.get("url", ""),
34
+ ]
35
+ for a in apps_data
36
+ ],
37
+ )
38
+ utils.console.print(table)
39
+
40
+
41
+ def status(app_name: str = typer.Argument(..., help="Application name")):
42
+ """Show application status"""
43
+ client = get_client()
44
+ result = asyncio.run(client.query("check", {"app_name": app_name}))
45
+
46
+ if not result.get("success"):
47
+ utils.error(result.get("error", f"Failed to get status for {app_name}"))
48
+ raise typer.Exit(1)
49
+
50
+ utils.console.print(f" App: [bold]{app_name}[/bold]")
51
+ for key in ("status", "url", "node", "port", "created_at", "image"):
52
+ val = result.get(key)
53
+ if val:
54
+ label = key.replace("_", " ").title()
55
+ utils.console.print(f" {label:9s}{val}")
56
+
57
+
58
+ def control(
59
+ app_name: str = typer.Argument(..., help="Application name"),
60
+ action: str = typer.Argument(..., help="Action: start/stop/restart"),
61
+ ):
62
+ """Start/stop/restart an application"""
63
+ if action not in ("start", "stop", "restart"):
64
+ utils.error(f"Invalid action: {action}. Use start/stop/restart")
65
+ raise typer.Exit(1)
66
+
67
+ client = get_client()
68
+ result = asyncio.run(client.control(action, {"app_name": app_name}))
69
+
70
+ if not result.get("success"):
71
+ utils.error(result.get("error", f"Failed to {action} {app_name}"))
72
+ raise typer.Exit(1)
73
+
74
+ utils.success(f"{app_name} {action} successful")
75
+
76
+
77
+ def delete(app_name: str = typer.Argument(..., help="Application name")):
78
+ """Delete an application (requires confirmation)"""
79
+ confirm = typer.confirm(
80
+ f"Delete application '{app_name}'? This cannot be undone"
81
+ )
82
+ if not confirm:
83
+ utils.info("Cancelled.")
84
+ raise typer.Exit()
85
+
86
+ client = get_client()
87
+ result = asyncio.run(client.control("delete", {"app_name": app_name}))
88
+
89
+ if not result.get("success"):
90
+ utils.error(result.get("error", f"Failed to delete {app_name}"))
91
+ raise typer.Exit(1)
92
+
93
+ utils.success(f"{app_name} deleted")
94
+
95
+
96
+ def _status_style(s: str) -> str:
97
+ colors = {"running": "green", "stopped": "red", "building": "yellow"}
98
+ color = colors.get(s, "dim")
99
+ return f"[{color}]{s}[/{color}]"
100
+
101
+
102
+ def boxes():
103
+ """List all sandbox boxes"""
104
+ client = get_client()
105
+ result = asyncio.run(client.query("list", {"type": "boxes"}))
106
+
107
+ if not result.get("success"):
108
+ utils.error(result.get("error", "Failed to list boxes"))
109
+ raise typer.Exit(1)
110
+
111
+ boxes_data = result.get("apps") or result.get("data", [])
112
+ if not boxes_data:
113
+ utils.info("No boxes found.")
114
+ return
115
+
116
+ table = utils.make_table(
117
+ "Sandbox Boxes",
118
+ [("Name", "cyan"), ("Status", ""), ("Type", "dim"), ("Created", "dim")],
119
+ [
120
+ [
121
+ b.get("name", "?"),
122
+ _status_style(b.get("status", "?")),
123
+ b.get("container_type", "?"),
124
+ str(b.get("created_at", "?"))[:10] if b.get("created_at") else "?",
125
+ ]
126
+ for b in boxes_data
127
+ ],
128
+ )
129
+ utils.console.print(table)
130
+
131
+
132
+ def images():
133
+ """List all available images"""
134
+ client = get_client()
135
+ result = asyncio.run(client.query("list", {"type": "images"}))
136
+
137
+ if not result.get("success"):
138
+ utils.error(result.get("error", "Failed to list images"))
139
+ raise typer.Exit(1)
140
+
141
+ images_data = result.get("images") or result.get("data", [])
142
+ if not images_data:
143
+ utils.info("No images found.")
144
+ return
145
+
146
+ table = utils.make_table(
147
+ "Images",
148
+ [("Name", "cyan"), ("Type", "dim"), ("Description", "")],
149
+ [
150
+ [
151
+ i.get("name", "?"),
152
+ i.get("image_type", "?"),
153
+ i.get("description", "")[:60] if i.get("description") else "",
154
+ ]
155
+ for i in images_data
156
+ ],
157
+ )
158
+ utils.console.print(table)
159
+
160
+
161
+ def templates():
162
+ """List all available templates"""
163
+ client = get_client()
164
+ result = asyncio.run(client.query("list", {"type": "templates"}))
165
+
166
+ if not result.get("success"):
167
+ utils.error(result.get("error", "Failed to list templates"))
168
+ raise typer.Exit(1)
169
+
170
+ templates_data = result.get("templates") or result.get("data", [])
171
+ if not templates_data:
172
+ utils.info("No templates found.")
173
+ return
174
+
175
+ table = utils.make_table(
176
+ "Templates",
177
+ [("Name", "cyan"), ("Type", "dim"), ("Image", "blue"), ("Description", "")],
178
+ [
179
+ [
180
+ t.get("name", "?"),
181
+ t.get("usage_type", "?"),
182
+ t.get("pull_image", "").split("/")[-1] if t.get("pull_image") else "?",
183
+ t.get("description", "")[:50] if t.get("description") else "",
184
+ ]
185
+ for t in templates_data
186
+ ],
187
+ )
188
+ utils.console.print(table)
mobox/commands/ops.py ADDED
@@ -0,0 +1,125 @@
1
+ """Operations commands: logs, bash, file"""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+
7
+ from mobox import utils
8
+ from mobox.client import get_client
9
+
10
+
11
+ def logs(
12
+ app_name: str = typer.Argument(..., help="Application name"),
13
+ tail: int = typer.Option(100, "--tail", "-n", help="Number of lines"),
14
+ ):
15
+ """View application logs"""
16
+ client = get_client()
17
+ result = asyncio.run(
18
+ client.query("logs", {"app_name": app_name, "lines": tail})
19
+ )
20
+
21
+ if not result.get("success"):
22
+ utils.error(result.get("error", f"Failed to get logs for {app_name}"))
23
+ raise typer.Exit(1)
24
+
25
+ log_text = result.get("logs") or result.get("text", "")
26
+ if log_text:
27
+ utils.console.print(log_text)
28
+ else:
29
+ utils.info("No logs available.")
30
+
31
+
32
+ def bash(
33
+ box_name: str = typer.Argument(..., help="Box name"),
34
+ cmd: str = typer.Argument(..., help="Command to execute"),
35
+ ):
36
+ """Execute command in application container"""
37
+ client = get_client()
38
+ result = asyncio.run(
39
+ client.boxbash("exec", {"box_name": box_name, "command": cmd})
40
+ )
41
+
42
+ if not result.get("success"):
43
+ utils.error(result.get("error", "Command execution failed"))
44
+ raise typer.Exit(1)
45
+
46
+ # If async task, poll for result
47
+ task_id = result.get("task_id")
48
+ if task_id:
49
+ utils.waiting(f"Task {task_id} running...")
50
+ check = asyncio.run(
51
+ client.boxbash("check", {"task_id": task_id, "wait": True})
52
+ )
53
+ if not check.get("success"):
54
+ utils.error(check.get("error", "Failed to retrieve task result"))
55
+ raise typer.Exit(1)
56
+ output = check.get("output", check.get("text", ""))
57
+ else:
58
+ output = result.get("output", result.get("text", ""))
59
+
60
+ if output:
61
+ utils.console.print(output)
62
+
63
+
64
+ def file(
65
+ box_name: str = typer.Argument(..., help="Box name"),
66
+ op: str = typer.Argument(
67
+ ..., help="Operation: read/write/edit/grep/list/down"
68
+ ),
69
+ path: str = typer.Option("", "--path", "-p", help="File path in container"),
70
+ content: str = typer.Option("", "--content", "-c", help="Content for write"),
71
+ pattern: str = typer.Option("", "--pattern", help="Pattern for grep"),
72
+ old_string: str = typer.Option("", "--old", help="Old string for edit"),
73
+ new_string: str = typer.Option("", "--new", help="New string for edit"),
74
+ upload_id: str = typer.Option("", "--upload-id", help="Upload ID for down operation"),
75
+ ):
76
+ """File operations in application container"""
77
+ valid_ops = ("read", "write", "edit", "grep", "list", "down")
78
+ if op not in valid_ops:
79
+ utils.error(f"Invalid operation: {op}. Use: {', '.join(valid_ops)}")
80
+ raise typer.Exit(1)
81
+
82
+ client = get_client()
83
+ options: dict = {"box_name": box_name}
84
+
85
+ if path:
86
+ options["path"] = path
87
+
88
+ # Validate required options per operation
89
+ if op == "write":
90
+ if not content:
91
+ utils.error("--content is required for write operation")
92
+ raise typer.Exit(1)
93
+ options["content"] = content
94
+ elif op == "grep":
95
+ if not pattern:
96
+ utils.error("--pattern is required for grep operation")
97
+ raise typer.Exit(1)
98
+ options["pattern"] = pattern
99
+ elif op == "edit":
100
+ if not old_string or not new_string:
101
+ utils.error("--old and --new are required for edit operation")
102
+ raise typer.Exit(1)
103
+ options["old_string"] = old_string
104
+ options["new_string"] = new_string
105
+ elif op == "down":
106
+ if not upload_id:
107
+ utils.error("--upload-id is required for down operation")
108
+ raise typer.Exit(1)
109
+ options["upload_id"] = upload_id
110
+
111
+ result = asyncio.run(client.boxfile(op, options))
112
+
113
+ if not result.get("success"):
114
+ utils.error(result.get("error", f"File operation '{op}' failed"))
115
+ raise typer.Exit(1)
116
+
117
+ text = result.get("content") or result.get("text") or result.get("files", "")
118
+ if text:
119
+ if isinstance(text, list):
120
+ for item in text:
121
+ utils.console.print(str(item))
122
+ else:
123
+ utils.console.print(str(text))
124
+ else:
125
+ utils.success(f"File {op} completed")
mobox/config.py ADDED
@@ -0,0 +1,82 @@
1
+ """~/.mobox/ credentials and config management"""
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ MOBOX_DIR = Path.home() / ".mobox"
10
+ CREDENTIALS_FILE = MOBOX_DIR / "credentials.json"
11
+ CONFIG_FILE = MOBOX_DIR / "config.json"
12
+
13
+ DEFAULT_SERVER = "https://hub-sh-1.aid.pub/mcp"
14
+
15
+
16
+ def _ensure_dir():
17
+ MOBOX_DIR.mkdir(parents=True, exist_ok=True)
18
+ os.chmod(MOBOX_DIR, 0o700)
19
+
20
+
21
+ def _write_secure(path: Path, data: dict):
22
+ """Write JSON with 0600 permissions (atomic create)"""
23
+ _ensure_dir()
24
+ content = json.dumps(data, indent=2, ensure_ascii=False)
25
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
26
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
27
+ f.write(content)
28
+
29
+
30
+ def _read_json(path: Path) -> Optional[dict]:
31
+ if not path.exists():
32
+ return None
33
+ try:
34
+ return json.loads(path.read_text(encoding="utf-8"))
35
+ except (json.JSONDecodeError, OSError):
36
+ return None
37
+
38
+
39
+ # --- Credentials ---
40
+
41
+
42
+ def save_credentials(mcp_key: str, namespace: str, server_url: str):
43
+ _write_secure(CREDENTIALS_FILE, {
44
+ "mcp_key": mcp_key,
45
+ "namespace": namespace,
46
+ "server_url": server_url,
47
+ "created_at": datetime.now(timezone.utc).isoformat(),
48
+ })
49
+
50
+
51
+ def load_credentials() -> Optional[dict]:
52
+ return _read_json(CREDENTIALS_FILE)
53
+
54
+
55
+ def clear_credentials():
56
+ if CREDENTIALS_FILE.exists():
57
+ CREDENTIALS_FILE.unlink()
58
+
59
+
60
+ # --- Config ---
61
+
62
+
63
+ def get_config(key: str, default=None):
64
+ cfg = _read_json(CONFIG_FILE) or {}
65
+ return cfg.get(key, default)
66
+
67
+
68
+ def set_config(key: str, value):
69
+ cfg = _read_json(CONFIG_FILE) or {}
70
+ cfg[key] = value
71
+ _write_secure(CONFIG_FILE, cfg)
72
+
73
+
74
+ def list_config() -> dict:
75
+ return _read_json(CONFIG_FILE) or {}
76
+
77
+
78
+ def get_server_url() -> str:
79
+ creds = load_credentials()
80
+ if creds and creds.get("server_url"):
81
+ return creds["server_url"]
82
+ return get_config("default_server", DEFAULT_SERVER)
File without changes