socaity-cli 0.1.1__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,30 @@
1
+ """Socaity platform CLI.
2
+
3
+ Lightweight bundle of all Socaity CLI commands and the backend HTTP client
4
+ (httpx + socaity-schemas only). Heavy features delegate to the optional
5
+ ``socaity`` (SDK installs) and ``apipod`` (deployment tooling) packages via
6
+ the ``requires`` decorator.
7
+ """
8
+ from socaity_cli.backend_client import SocaityBackendClient
9
+ from socaity_cli.credentials import (
10
+ Credentials,
11
+ clear_credentials,
12
+ get_api_key,
13
+ load_credentials,
14
+ require_credentials,
15
+ save_credentials,
16
+ )
17
+ from socaity_cli.requires import requires
18
+ from socaity_cli.prompts import input_yes_no
19
+
20
+ __all__ = [
21
+ "SocaityBackendClient",
22
+ "Credentials",
23
+ "clear_credentials",
24
+ "get_api_key",
25
+ "load_credentials",
26
+ "require_credentials",
27
+ "save_credentials",
28
+ "requires",
29
+ "input_yes_no",
30
+ ]
@@ -0,0 +1,4 @@
1
+ from socaity_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,82 @@
1
+ """Bridge to the APIPod CLI (requires ``pip install apipod``).
2
+
3
+ Imports stay inside the functions: ``apipod`` is optional for the CLI and the
4
+ ``requires`` decorator guards each entry with a friendly install hint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from typing import List, Optional
11
+
12
+ from socaity_cli.requires import requires
13
+
14
+
15
+ def _with_project_dir(argv: List[str], project_dir: Optional[str]) -> List[str]:
16
+ if project_dir:
17
+ return ["-C", project_dir, *argv]
18
+ return argv
19
+
20
+
21
+ @requires("apipod")
22
+ def _run_apipod_argv(argv: List[str]) -> None:
23
+ import apipod.cli
24
+
25
+ previous = sys.argv
26
+ sys.argv = ["apipod", *argv]
27
+ try:
28
+ apipod.cli.main()
29
+ finally:
30
+ sys.argv = previous
31
+
32
+
33
+ def run_scan(*, project_dir: Optional[str] = None) -> None:
34
+ _run_apipod_argv(_with_project_dir(["scan"], project_dir))
35
+
36
+
37
+ def run_build(target_file: Optional[str], *, project_dir: Optional[str] = None) -> None:
38
+ argv = ["build"]
39
+ if target_file:
40
+ argv.append(target_file)
41
+ _run_apipod_argv(_with_project_dir(argv, project_dir))
42
+
43
+
44
+ def run_start(
45
+ *,
46
+ entrypoint: Optional[str],
47
+ host: Optional[str],
48
+ port: Optional[int],
49
+ project_dir: Optional[str] = None,
50
+ ) -> None:
51
+ argv = ["start"]
52
+ if entrypoint:
53
+ argv.append(entrypoint)
54
+ if host:
55
+ argv.extend(["--host", host])
56
+ if port is not None:
57
+ argv.extend(["--port", str(port)])
58
+ _run_apipod_argv(_with_project_dir(argv, project_dir))
59
+
60
+
61
+ def run_deploy(
62
+ *,
63
+ target: Optional[str] = None,
64
+ project_dir: Optional[str] = None,
65
+ resume: Optional[str] = None,
66
+ skip_build: bool = False,
67
+ push_only: bool = False,
68
+ yes: bool = False,
69
+ ) -> None:
70
+ """Wrap ``apipod deploy`` (analyze, build, push, provision)."""
71
+ argv = ["deploy"]
72
+ if target:
73
+ argv.append(target)
74
+ if resume:
75
+ argv.extend(["--resume", resume])
76
+ if skip_build:
77
+ argv.append("--skip-build")
78
+ if push_only:
79
+ argv.append("--push-only")
80
+ if yes:
81
+ argv.append("--yes")
82
+ _run_apipod_argv(_with_project_dir(argv, project_dir))
socaity_cli/auth.py ADDED
@@ -0,0 +1,197 @@
1
+ """Browser-based CLI login (device authorization flow via web UI)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import time
9
+ import webbrowser
10
+ from typing import Any, Dict, Optional
11
+ from urllib.parse import urljoin
12
+
13
+ import httpx
14
+
15
+ from socaity_cli.credentials import (
16
+ Credentials,
17
+ DEFAULT_BACKEND_URL,
18
+ DEFAULT_FRONTEND_URL,
19
+ require_credentials,
20
+ save_credentials,
21
+ )
22
+
23
+ USER_AGENT = "socaity-cli/0.1.0"
24
+
25
+
26
+ def _backend_base_url() -> str:
27
+ return os.environ.get("SOCAITY_BACKEND_URL", DEFAULT_BACKEND_URL).rstrip("/")
28
+
29
+
30
+ def _frontend_base_url(frontend_url: Optional[str] = None) -> str:
31
+ if frontend_url is not None:
32
+ value = frontend_url.strip()
33
+ return value.rstrip("/") if value else DEFAULT_FRONTEND_URL
34
+ return os.environ.get("SOCAITY_FRONTEND_URL", DEFAULT_FRONTEND_URL).rstrip("/")
35
+
36
+
37
+ def _web_api_url(base: str) -> str:
38
+ return urljoin(base + "/", "v1/")
39
+
40
+
41
+ def open_login_url(url: str) -> bool:
42
+ try:
43
+ if webbrowser.open(url):
44
+ return True
45
+ except Exception:
46
+ pass
47
+ for cmd in ("wslview", "xdg-open"):
48
+ exe = shutil.which(cmd)
49
+ if not exe:
50
+ continue
51
+ try:
52
+ subprocess.run(
53
+ [exe, url],
54
+ check=False,
55
+ timeout=8,
56
+ stdout=subprocess.DEVNULL,
57
+ stderr=subprocess.DEVNULL,
58
+ )
59
+ return True
60
+ except Exception:
61
+ continue
62
+ return False
63
+
64
+
65
+ def start_cli_login(
66
+ *,
67
+ backend_url: Optional[str] = None,
68
+ frontend_url: Optional[str] = None,
69
+ expires_in_seconds: int = 600,
70
+ ) -> Dict[str, Any]:
71
+ base = (backend_url or _backend_base_url()).rstrip("/")
72
+ payload: Dict[str, Any] = {"expires_in_seconds": expires_in_seconds}
73
+ payload["frontend_base_url"] = _frontend_base_url(frontend_url)
74
+ with httpx.Client(timeout=20.0) as client:
75
+ response = client.post(
76
+ _web_api_url(base) + "cli-auth/start",
77
+ json=payload,
78
+ headers={"User-Agent": USER_AGENT},
79
+ )
80
+ response.raise_for_status()
81
+ return response.json()
82
+
83
+
84
+ def poll_cli_login(
85
+ *,
86
+ request_id: str,
87
+ poll_code: str,
88
+ backend_url: Optional[str] = None,
89
+ ) -> Dict[str, Any]:
90
+ base = (backend_url or _backend_base_url()).rstrip("/")
91
+ with httpx.Client(timeout=20.0) as client:
92
+ response = client.post(
93
+ _web_api_url(base) + "cli-auth/poll",
94
+ json={"request_id": request_id, "poll_code": poll_code},
95
+ headers={"User-Agent": USER_AGENT},
96
+ )
97
+ response.raise_for_status()
98
+ return response.json()
99
+
100
+
101
+ def fetch_profile(*, api_key: str, backend_url: str) -> Dict[str, Any]:
102
+ base = backend_url.rstrip("/")
103
+ with httpx.Client(timeout=20.0) as client:
104
+ response = client.get(
105
+ _web_api_url(base) + "profile/profile",
106
+ headers={
107
+ "Authorization": f"Bearer {api_key}",
108
+ "User-Agent": USER_AGENT,
109
+ },
110
+ )
111
+ response.raise_for_status()
112
+ return response.json()
113
+
114
+
115
+ def run_login(
116
+ *,
117
+ no_browser: bool = False,
118
+ timeout: int = 300,
119
+ backend_url: Optional[str] = None,
120
+ frontend_url: Optional[str] = None,
121
+ ) -> Credentials:
122
+ base = (backend_url or _backend_base_url()).rstrip("/")
123
+
124
+ print("Login to Socaity")
125
+ session = start_cli_login(
126
+ backend_url=base,
127
+ frontend_url=frontend_url,
128
+ )
129
+ verification_uri = session["verification_uri"]
130
+ request_id = session["request_id"]
131
+ poll_code = session["poll_code"]
132
+ interval = max(int(session.get("interval_seconds", 2)), 1)
133
+
134
+ print(f"Open this URL to authenticate:\n{verification_uri}")
135
+
136
+ if not no_browser:
137
+ if open_login_url(verification_uri):
138
+ print("Browser opened for login.")
139
+ else:
140
+ print("Could not open a browser automatically. Open the URL manually.")
141
+
142
+ print("Waiting for login confirmation...")
143
+ deadline = time.time() + max(timeout, 10)
144
+
145
+ while time.time() < deadline:
146
+ poll_response = poll_cli_login(
147
+ request_id=request_id,
148
+ poll_code=poll_code,
149
+ backend_url=base,
150
+ )
151
+ status = poll_response.get("status")
152
+
153
+ if status == "pending":
154
+ time.sleep(interval)
155
+ continue
156
+ if status == "expired":
157
+ print("Login request expired. Run: socaity login")
158
+ raise SystemExit(1)
159
+ if status == "approved":
160
+ api_key = (poll_response.get("api_key") or "").strip()
161
+ if not api_key:
162
+ print("Login failed: missing API key in response.")
163
+ raise SystemExit(1)
164
+ creds = Credentials(
165
+ api_key=api_key,
166
+ user_id=poll_response.get("user_id"),
167
+ user_email=poll_response.get("user_email"),
168
+ backend_url=base,
169
+ )
170
+ path = save_credentials(creds)
171
+ email = creds.user_email or "user"
172
+ print(f"Logged in as {email}")
173
+ print(f"Credentials saved to {path}")
174
+ return creds
175
+
176
+ print(f"Login failed: unexpected status '{status}'")
177
+ raise SystemExit(1)
178
+
179
+ print("Login timed out. Run: socaity login")
180
+ raise SystemExit(1)
181
+
182
+
183
+ def run_whoami() -> None:
184
+ creds = require_credentials()
185
+ email = creds.user_email
186
+ user_id = creds.user_id
187
+ if not email or not user_id:
188
+ try:
189
+ profile = fetch_profile(api_key=creds.api_key, backend_url=creds.backend_url)
190
+ email = email or profile.get("email") or profile.get("user_email")
191
+ user_id = user_id or profile.get("id") or profile.get("user_id")
192
+ except Exception as exc:
193
+ print(f"Could not fetch profile: {exc}")
194
+ raise SystemExit(1) from exc
195
+ print(f"user_id: {user_id}")
196
+ print(f"email: {email}")
197
+ print(f"backend: {creds.backend_url}")
@@ -0,0 +1,23 @@
1
+ """Composed platform backend client (catalog, jobs, projects, analytics, files, sdk, deployment)."""
2
+ from socaity_cli.clients.analytics import AnalyticsClient
3
+ from socaity_cli.clients.catalog import CatalogClient
4
+ from socaity_cli.clients.deployment import DeploymentClient
5
+ from socaity_cli.clients.files import FilesClient
6
+ from socaity_cli.clients.jobs import JobsClient
7
+ from socaity_cli.clients.projects import ProjectsClient
8
+ from socaity_cli.clients.sdk_install import SdkInstallClient
9
+ from socaity_cli.http import BackendHttp
10
+
11
+
12
+ class SocaityBackendClient(
13
+ BackendHttp,
14
+ CatalogClient,
15
+ JobsClient,
16
+ ProjectsClient,
17
+ AnalyticsClient,
18
+ FilesClient,
19
+ SdkInstallClient,
20
+ DeploymentClient,
21
+ ):
22
+ """Facade over domain mixins. Prefer importing a domain client when you only need one surface."""
23
+ pass
socaity_cli/cli.py ADDED
@@ -0,0 +1,352 @@
1
+ """Unified Socaity CLI: auth, catalog, SDK installs, and APIPod deployment tooling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from socaity_cli.apipod_commands import run_build, run_deploy, run_scan, run_start
8
+ from socaity_cli.auth import run_login, run_whoami
9
+ from socaity_cli.backend_client import SocaityBackendClient
10
+ from socaity_cli.credentials import clear_credentials, require_credentials
11
+ from socaity_cli.sdk_commands import run_install, run_update
12
+
13
+
14
+ def _run_list(entity: str, category: str | None, family: str | None, limit: int) -> None:
15
+ client = SocaityBackendClient()
16
+ if entity == "services":
17
+ services = client.list_services(category=category, limit=limit)
18
+ if not services:
19
+ print("No services found.")
20
+ return
21
+ for service in services:
22
+ name = service.display_name or service.name or service.id
23
+ official = " [official]" if service.is_official else ""
24
+ desc = f" - {service.short_desc}" if service.short_desc else ""
25
+ print(f"{name}{official}{desc}")
26
+ else:
27
+ models = client.list_models(family=family, limit=limit)
28
+ if not models:
29
+ print("No models found.")
30
+ return
31
+ for model in models:
32
+ name = model.display_name or model.name
33
+ family_info = f" ({model.family})" if model.family else ""
34
+ print(f"{name}{family_info}")
35
+
36
+
37
+ def _split_csv(value: str | None) -> list[str]:
38
+ return [part.strip() for part in (value or "").split(",") if part.strip()]
39
+
40
+
41
+ def _run_jobs(
42
+ job_id: str | None,
43
+ expand: str | None,
44
+ status: str | None,
45
+ visibility: str,
46
+ limit: int,
47
+ q: str | None = None,
48
+ ) -> None:
49
+ client = SocaityBackendClient()
50
+ expand_list = _split_csv(expand)
51
+
52
+ def _print_job(job) -> None:
53
+ cost = ""
54
+ if job.billing and job.billing.cost_amount is not None:
55
+ cost = f" {job.billing.cost_amount:.4f} {job.billing.cost_currency or 'EUR'}"
56
+ name = f" '{job.data.display_name}'" if job.data and job.data.display_name else ""
57
+ print(f"{job.id} [{job.status}] {job.endpoint or '?'} {job.created_at or ''}{cost}{name}")
58
+
59
+ if job_id:
60
+ job = client.get_job(job_id, expand=expand_list)
61
+ if not job:
62
+ print(f"Job '{job_id}' not found.")
63
+ return
64
+ print(job.model_dump_json(indent=2, exclude_none=True))
65
+ return
66
+
67
+ filters = [f"status:eq:{status}"] if status else None
68
+ jobs = client.query_jobs(
69
+ q=q, filters=filters, expand=expand_list, visibility=visibility, limit=limit,
70
+ )
71
+ if not jobs:
72
+ print("No jobs found.")
73
+ return
74
+ for job in jobs:
75
+ _print_job(job)
76
+
77
+
78
+ def _run_projects(project_id: str | None, expand: str | None, limit: int) -> None:
79
+ client = SocaityBackendClient()
80
+ expand_list = _split_csv(expand)
81
+
82
+ if project_id:
83
+ project = client.get_project(project_id, expand=expand_list)
84
+ if not project:
85
+ print(f"Project '{project_id}' not found.")
86
+ return
87
+ print(project.model_dump_json(indent=2, exclude_none=True))
88
+ return
89
+
90
+ projects = client.query_projects(expand=expand_list, limit=limit)
91
+ if not projects:
92
+ print("No projects found.")
93
+ return
94
+ for project in projects:
95
+ name = project.display_name or project.id
96
+ print(f"{project.id} {name} ({project.n_jobs or 0} jobs, {project.n_files or 0} files)")
97
+
98
+
99
+ def _run_search(query: str, collections: str | None, limit: int) -> None:
100
+ client = SocaityBackendClient()
101
+ targets = [part.strip() for part in (collections or "services,models").split(",") if part.strip()]
102
+ found = False
103
+ for collection in targets:
104
+ hits = client.search(query, collection=collection, limit=limit)
105
+ if not hits:
106
+ continue
107
+ found = True
108
+ for item in hits:
109
+ if collection == "jobs":
110
+ name = getattr(getattr(item, "data", None), "display_name", None) or item.id
111
+ status = f" [{item.status}]" if getattr(item, "status", None) else ""
112
+ print(f"[jobs] {name}{status}")
113
+ continue
114
+ name = item.display_name or item.name or item.id
115
+ desc = getattr(item, "short_desc", None) or getattr(item, "description", None) or ""
116
+ print(f"[{collection}] {name}" + (f" - {desc[:80]}" if desc else ""))
117
+ if not found:
118
+ print("No results.")
119
+
120
+
121
+ def main(argv: list[str] | None = None) -> None:
122
+ parser = argparse.ArgumentParser(
123
+ prog="socaity",
124
+ description="Socaity platform CLI: authentication, catalog, SDK, and APIPod deployment",
125
+ formatter_class=argparse.RawDescriptionHelpFormatter,
126
+ epilog="""
127
+ Examples:
128
+ socaity login
129
+ socaity install black-forest-labs/flux-schnell
130
+ socaity update
131
+ socaity list services
132
+ socaity search "flux schnell"
133
+ socaity search "lighthouse" --collections jobs
134
+ socaity jobs --q lighthouse --expand data
135
+ socaity projects --expand jobs
136
+ socaity scan
137
+ socaity build main.py
138
+ socaity start --port 8123
139
+ socaity deploy serverless-runpod
140
+ socaity -C simple_test_service deploy serverless-runpod --yes
141
+ """,
142
+ )
143
+ parser.add_argument(
144
+ "-i", "--install",
145
+ metavar="SERVICE",
146
+ help="Install an AI service (requires login)",
147
+ )
148
+ parser.add_argument(
149
+ "-C",
150
+ "--project-dir",
151
+ default=None,
152
+ metavar="DIR",
153
+ help="Service project directory for apipod commands (scan/build/start/deploy).",
154
+ )
155
+
156
+ subparsers = parser.add_subparsers(dest="command")
157
+
158
+ login_parser = subparsers.add_parser("login", help="Authenticate via browser and save credentials")
159
+ login_parser.add_argument("--no-browser", action="store_true", help="Do not open a browser automatically")
160
+ login_parser.add_argument("--timeout", type=int, default=300, help="Seconds to wait for browser login")
161
+ login_parser.add_argument(
162
+ "--backend-url",
163
+ default=None,
164
+ help="API base URL (default: SOCAITY_BACKEND_URL or https://webapi.socaity.ai)",
165
+ )
166
+ login_parser.add_argument(
167
+ "--frontend-url",
168
+ default=None,
169
+ help="Web UI base URL for sign-in (default: SOCAITY_FRONTEND_URL or https://socaity.ai)",
170
+ )
171
+
172
+ subparsers.add_parser("logout", help="Remove stored credentials")
173
+ subparsers.add_parser("whoami", help="Show logged-in user")
174
+
175
+ install_parser = subparsers.add_parser("install", help="Install an AI service (requires socaity)")
176
+ install_parser.add_argument("service_name", help='Service name, ID, or "all"')
177
+
178
+ subparsers.add_parser("update", help="Sync installed services with the platform (requires socaity)")
179
+
180
+ list_parser = subparsers.add_parser("list", help="List catalog entries")
181
+ list_parser.add_argument("entity", choices=("services", "models"), help="What to list")
182
+ list_parser.add_argument("--category", default=None, help="Filter services by category id")
183
+ list_parser.add_argument("--family", default=None, help="Filter models by family")
184
+ list_parser.add_argument("--limit", type=int, default=50)
185
+
186
+ search_parser = subparsers.add_parser("search", help="Fuzzy-search services, models, or jobs")
187
+ search_parser.add_argument("query", help="Search text (typo-tolerant)")
188
+ search_parser.add_argument("--collections", default=None, help="Comma-separated: services,models,jobs")
189
+ search_parser.add_argument("--limit", type=int, default=20)
190
+
191
+ jobs_parser = subparsers.add_parser("jobs", help="List/search your jobs or inspect one (requires login)")
192
+ jobs_parser.add_argument("job_id", nargs="?", default=None, help="Job id for a detailed view")
193
+ jobs_parser.add_argument("--q", default=None, help="Fuzzy search over job prompts / display names")
194
+ jobs_parser.add_argument("--expand", default=None, help="Comma-separated: data,files,billing,social")
195
+ jobs_parser.add_argument("--status", default=None, help="Filter by status (e.g. FINISHED)")
196
+ jobs_parser.add_argument(
197
+ "--visibility",
198
+ choices=("user", "public", "user_visible"),
199
+ default="user_visible",
200
+ help="user: own jobs. public: published. user_visible: own + public (default).",
201
+ )
202
+ jobs_parser.add_argument("--limit", type=int, default=25)
203
+
204
+ projects_parser = subparsers.add_parser("projects", help="List your projects or inspect one (requires login)")
205
+ projects_parser.add_argument("project_id", nargs="?", default=None, help="Project id for a detailed view")
206
+ projects_parser.add_argument("--expand", default=None, help="Comma-separated: jobs (nested: jobs.data, ...)")
207
+ projects_parser.add_argument("--limit", type=int, default=25)
208
+
209
+ subparsers.add_parser("scan", help="Scan project and generate apipod.json (requires apipod)")
210
+
211
+ build_parser = subparsers.add_parser("build", help="Build deployment container (requires apipod)")
212
+ build_parser.add_argument("file", nargs="?", default=None, help="Entry Python file (e.g. main.py)")
213
+
214
+ start_parser = subparsers.add_parser("start", help="Start APIPod service locally (requires apipod)")
215
+ start_parser.add_argument("entrypoint", nargs="?", default=None, help="Service entrypoint file")
216
+ start_parser.add_argument("--host", default=None, help="Host to bind (default: 0.0.0.0)")
217
+ start_parser.add_argument("--port", type=int, default=None, help="Port to bind (default: 8000)")
218
+
219
+ deploy_parser = subparsers.add_parser(
220
+ "deploy",
221
+ help="Deploy to Socaity via APIPod (requires apipod)",
222
+ formatter_class=argparse.RawDescriptionHelpFormatter,
223
+ epilog=(
224
+ "Examples:\n"
225
+ " socaity deploy\n"
226
+ " socaity deploy serverless-runpod\n"
227
+ " socaity -C simple_test_service deploy serverless-runpod --yes\n"
228
+ " socaity deploy --skip-build\n"
229
+ " socaity deploy --resume DEPLOYMENT_ID"
230
+ ),
231
+ )
232
+ deploy_parser.add_argument(
233
+ "target",
234
+ nargs="?",
235
+ default="serverless",
236
+ metavar="TARGET",
237
+ help="Deployment target '{compute}-{provider}' (default: serverless).",
238
+ )
239
+ deploy_parser.add_argument(
240
+ "--resume",
241
+ default=None,
242
+ metavar="DEPLOYMENT_ID",
243
+ help="Skip analyze/draft and push into an existing deployment attempt.",
244
+ )
245
+ deploy_parser.add_argument(
246
+ "--skip-build",
247
+ action="store_true",
248
+ help="Do not rebuild the container; push the existing local image.",
249
+ )
250
+ deploy_parser.add_argument(
251
+ "--push-only",
252
+ action="store_true",
253
+ help="With --resume: only push and poll, never build or create drafts.",
254
+ )
255
+ deploy_parser.add_argument(
256
+ "-y", "--yes",
257
+ action="store_true",
258
+ help="Non-interactive: skip confirmations (CI / automated runs).",
259
+ )
260
+
261
+ args = parser.parse_args(argv)
262
+
263
+ if args.install:
264
+ require_credentials()
265
+ run_install(args.install)
266
+ return
267
+
268
+ if args.command == "login":
269
+ run_login(
270
+ no_browser=args.no_browser,
271
+ timeout=args.timeout,
272
+ backend_url=args.backend_url,
273
+ frontend_url=args.frontend_url,
274
+ )
275
+ return
276
+
277
+ if args.command == "logout":
278
+ if clear_credentials():
279
+ print("Logged out. Credentials removed.")
280
+ else:
281
+ print("Not logged in.")
282
+ return
283
+
284
+ if args.command == "whoami":
285
+ run_whoami()
286
+ return
287
+
288
+ if args.command == "install":
289
+ require_credentials()
290
+ run_install(args.service_name)
291
+ return
292
+
293
+ if args.command == "update":
294
+ require_credentials()
295
+ run_update()
296
+ return
297
+
298
+ if args.command == "list":
299
+ _run_list(args.entity, args.category, args.family, args.limit)
300
+ return
301
+
302
+ if args.command == "search":
303
+ targets = [part.strip() for part in (args.collections or "services,models").split(",") if part.strip()]
304
+ if "jobs" in targets:
305
+ require_credentials()
306
+ _run_search(args.query, args.collections, args.limit)
307
+ return
308
+
309
+ if args.command == "jobs":
310
+ if args.visibility != "public":
311
+ require_credentials()
312
+ _run_jobs(args.job_id, args.expand, args.status, args.visibility, args.limit, q=args.q)
313
+ return
314
+
315
+ if args.command == "projects":
316
+ require_credentials()
317
+ _run_projects(args.project_id, args.expand, args.limit)
318
+ return
319
+
320
+ if args.command == "scan":
321
+ run_scan(project_dir=args.project_dir)
322
+ return
323
+
324
+ if args.command == "build":
325
+ run_build(args.file, project_dir=args.project_dir)
326
+ return
327
+
328
+ if args.command == "start":
329
+ run_start(
330
+ entrypoint=args.entrypoint,
331
+ host=args.host,
332
+ port=args.port,
333
+ project_dir=args.project_dir,
334
+ )
335
+ return
336
+
337
+ if args.command == "deploy":
338
+ run_deploy(
339
+ target=args.target,
340
+ project_dir=args.project_dir,
341
+ resume=args.resume,
342
+ skip_build=args.skip_build,
343
+ push_only=args.push_only,
344
+ yes=args.yes,
345
+ )
346
+ return
347
+
348
+ parser.print_help()
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()