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.
- mobox/__init__.py +1 -0
- mobox/auth.py +254 -0
- mobox/client.py +98 -0
- mobox/commands/__init__.py +0 -0
- mobox/commands/deploy.py +149 -0
- mobox/commands/local.py +82 -0
- mobox/commands/manage.py +188 -0
- mobox/commands/ops.py +125 -0
- mobox/config.py +82 -0
- mobox/local/__init__.py +0 -0
- mobox/local/analyze_volumes.py +483 -0
- mobox/local/config_utils.py +632 -0
- mobox/local/detect_framework.py +1406 -0
- mobox/local/detect_project.py +388 -0
- mobox/local/pack_project.py +543 -0
- mobox/main.py +109 -0
- mobox/utils.py +52 -0
- mobox_cli-0.1.0.dist-info/METADATA +9 -0
- mobox_cli-0.1.0.dist-info/RECORD +21 -0
- mobox_cli-0.1.0.dist-info/WHEEL +4 -0
- mobox_cli-0.1.0.dist-info/entry_points.txt +2 -0
mobox/commands/manage.py
ADDED
|
@@ -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)
|
mobox/local/__init__.py
ADDED
|
File without changes
|