lyceum-cli 1.0.25__py3-none-any.whl → 1.0.27__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.
Files changed (34) hide show
  1. lyceum/external/auth/login.py +18 -18
  2. lyceum/external/compute/execution/docker.py +4 -2
  3. lyceum/external/compute/execution/docker_compose.py +263 -0
  4. lyceum/external/compute/execution/notebook.py +0 -2
  5. lyceum/external/compute/execution/python.py +2 -1
  6. lyceum/external/compute/inference/batch.py +8 -10
  7. lyceum/external/vms/instances.py +301 -0
  8. lyceum/external/vms/management.py +383 -0
  9. lyceum/main.py +3 -0
  10. lyceum/shared/config.py +19 -24
  11. lyceum/shared/display.py +12 -31
  12. lyceum/shared/streaming.py +17 -45
  13. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
  14. lyceum_cli-1.0.27.dist-info/RECORD +34 -0
  15. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
  16. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
  17. lyceum/external/compute/execution/docker_config.py +0 -123
  18. lyceum/external/storage/files.py +0 -273
  19. lyceum_cli-1.0.25.dist-info/RECORD +0 -46
  20. tests/__init__.py +0 -1
  21. tests/conftest.py +0 -200
  22. tests/unit/__init__.py +0 -1
  23. tests/unit/external/__init__.py +0 -1
  24. tests/unit/external/compute/__init__.py +0 -1
  25. tests/unit/external/compute/execution/__init__.py +0 -1
  26. tests/unit/external/compute/execution/test_data.py +0 -33
  27. tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
  28. tests/unit/external/compute/execution/test_python_helpers.py +0 -406
  29. tests/unit/external/compute/execution/test_python_run.py +0 -289
  30. tests/unit/shared/__init__.py +0 -1
  31. tests/unit/shared/test_config.py +0 -341
  32. tests/unit/shared/test_streaming.py +0 -259
  33. /lyceum/external/{storage → vms}/__init__.py +0 -0
  34. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
lyceum/main.py CHANGED
@@ -11,8 +11,10 @@ from rich.console import Console
11
11
  from .external.auth.login import auth_app
12
12
  from .external.compute.execution.python import python_app
13
13
  from .external.compute.execution.docker import docker_app
14
+ from .external.compute.execution.docker_compose import compose_app
14
15
  from .external.compute.execution.workloads import workloads_app
15
16
  from .external.compute.execution.notebook import notebook_app
17
+ from .external.vms.management import vms_app
16
18
 
17
19
  app = typer.Typer(
18
20
  name="lyceum",
@@ -26,6 +28,7 @@ console = Console()
26
28
  app.add_typer(auth_app, name="auth")
27
29
  app.add_typer(python_app, name="python")
28
30
  app.add_typer(docker_app, name="docker")
31
+ app.add_typer(compose_app, name="compose")
29
32
  app.add_typer(workloads_app, name="workloads")
30
33
  app.add_typer(notebook_app, name="notebook")
31
34
 
lyceum/shared/config.py CHANGED
@@ -1,32 +1,28 @@
1
1
  """Configuration management for Lyceum CLI"""
2
2
 
3
3
  import json
4
-
5
- # Add the generated client to the path
6
- import sys
4
+ import os
7
5
  import time
8
6
  from pathlib import Path
9
7
 
10
8
  import jwt
11
9
  import typer
12
10
  from rich.console import Console
13
-
14
11
  from supabase import create_client
15
12
 
16
- sys.path.insert(0, str(Path(__file__).parent.parent.parent / "lyceum-cloud-execution-api-client"))
17
-
18
- # Commented out - using httpx directly instead
19
- # from lyceum_cloud_execution_api_client.client import AuthenticatedClient
20
-
21
13
  console = Console()
22
14
 
23
15
  # Configuration
24
16
  CONFIG_DIR = Path.home() / ".lyceum"
25
17
  CONFIG_FILE = CONFIG_DIR / "config.json"
26
18
 
19
+ # Supabase configuration from environment variables (required)
20
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
21
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
27
22
 
28
- class Config:
29
- """Configuration management for Lyceum CLI"""
23
+
24
+ class _Config:
25
+ """Configuration management for Lyceum CLI (private - use the global 'config' instance)"""
30
26
 
31
27
  def __init__(self):
32
28
  """Initialize configuration with default values"""
@@ -92,17 +88,16 @@ class Config:
92
88
  return False
93
89
 
94
90
  try:
95
- # Use Supabase's refresh_session method with the service role key
96
- supabase_url = "https://tqcebgbexyszvqhnwnhh.supabase.co"
97
- # NOTE: This service key is scoped for refresh token operations only
98
- supabase_service_key = (
99
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
100
- "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRxY2ViZ2JleHlzenZxaG53bmhoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIs"
101
- "ImlhdCI6MTc0NzE1NDQ3MSwiZXhwIjoyMDYyNzMwNDcxfQ."
102
- "RpxhmMxFKJSJERobr28bmaZOG9Fxe-qthTYlg8iyFdc"
103
- )
104
-
105
- supabase = create_client(supabase_url, supabase_service_key)
91
+ # Check if Supabase credentials are configured
92
+ if not SUPABASE_URL or not SUPABASE_ANON_KEY:
93
+ console.print(
94
+ "[red]Error: SUPABASE_URL and SUPABASE_ANON_KEY environment variables must be set[/red]"
95
+ )
96
+ return False
97
+
98
+ # Use Supabase's refresh_session method with the public anon key
99
+ # This is safe for client-side use and properly scoped for user token refresh
100
+ supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
106
101
 
107
102
  # Use refresh_session method
108
103
  response = supabase.auth.refresh_session(refresh_token=self.refresh_token)
@@ -131,7 +126,7 @@ class Config:
131
126
  def get_client(self):
132
127
  """Get authenticated API client with automatic token refresh"""
133
128
  if not self.api_key:
134
- console.print("[red]Error: Not authenticated. Run 'lyceum login' first.[/red]")
129
+ console.print("[red]Error: Not authenticated. Run 'lyceum auth login' first.[/red]")
135
130
  raise typer.Exit(1)
136
131
 
137
132
  # Check if token is expired and try to refresh
@@ -146,4 +141,4 @@ class Config:
146
141
 
147
142
 
148
143
  # Global config instance
149
- config = Config()
144
+ config = _Config()
lyceum/shared/display.py CHANGED
@@ -2,25 +2,6 @@
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- from rich.table import Table
6
-
7
-
8
- def create_table(title: str, columns: list[tuple[str, str]]) -> Table:
9
- """
10
- Create a Rich table with the given title and columns.
11
-
12
- Args:
13
- title: Table title
14
- columns: List of (column_name, style) tuples
15
-
16
- Returns:
17
- Rich Table instance
18
- """
19
- table = Table(title=title)
20
- for col_name, style in columns:
21
- table.add_column(col_name, style=style)
22
- return table
23
-
24
5
 
25
6
  def format_timestamp(timestamp: str | int | datetime, relative: bool = False) -> str:
26
7
  """
@@ -161,33 +142,33 @@ def status_color(status: str) -> str:
161
142
  return 'white'
162
143
 
163
144
 
164
- def status_emoji(status: str) -> str:
145
+ def status_icon(status: str) -> str:
165
146
  """
166
- Get emoji for status display.
147
+ Get text icon for status display.
167
148
 
168
149
  Args:
169
150
  status: Status string
170
151
 
171
152
  Returns:
172
- Emoji character
153
+ Text-based status indicator
173
154
  """
174
155
  status_lower = status.lower()
175
156
 
176
157
  if status_lower in ['completed', 'success']:
177
- return ''
158
+ return '[OK]'
178
159
  elif status_lower in ['failed', 'error', 'failed_user', 'failed_system']:
179
- return ''
160
+ return '[FAIL]'
180
161
  elif status_lower in ['running', 'in_progress']:
181
- return '🔄'
162
+ return '[RUN]'
182
163
  elif status_lower in ['pending', 'queued']:
183
- return ''
164
+ return '[WAIT]'
184
165
  elif status_lower in ['validating']:
185
- return '🔍'
166
+ return '[VAL]'
186
167
  elif status_lower in ['finalizing']:
187
- return '📦'
168
+ return '[FIN]'
188
169
  elif status_lower in ['cancelled']:
189
- return '🛑'
170
+ return '[STOP]'
190
171
  elif status_lower in ['expired', 'timeout']:
191
- return ''
172
+ return '[TIME]'
192
173
  else:
193
- return ''
174
+ return '[--]'
@@ -60,7 +60,7 @@ def normalize_newlines(text: str) -> str:
60
60
  return re.sub(r'\n{2,}', '\n', text)
61
61
 
62
62
 
63
- def stream_execution_output(execution_id: str, streaming_url: str = None, status: StatusLine = None) -> bool:
63
+ def stream_execution_output(execution_id: str, streaming_url: str = None) -> bool:
64
64
  """Stream execution output in real-time. Returns True if successful, False if failed."""
65
65
  if not streaming_url:
66
66
  # Fallback to stream endpoint if no streaming URL provided
@@ -69,25 +69,15 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
69
69
  stream_url = streaming_url
70
70
 
71
71
  try:
72
- if status:
73
- status.update("Connecting to stream...")
74
-
75
- headers = {
76
- "Accept": "text/event-stream",
77
- "Cache-Control": "no-cache",
78
- }
79
- with httpx.stream("POST", stream_url, headers=headers, timeout=600.0) as response:
72
+ console.print("[dim]Connecting to execution stream...[/dim]")
73
+
74
+ headers = {"Authorization": f"Bearer {config.api_key}"}
75
+ with httpx.stream("GET", stream_url, headers=headers, timeout=600.0) as response:
80
76
  if response.status_code != 200:
81
- if status:
82
- status.stop()
83
- if response.status_code == 404:
84
- console.print("[yellow]Stream not found - execution may have already completed[/yellow]")
85
- else:
86
- console.print(f"[red]Stream failed: HTTP {response.status_code}[/red]")
77
+ console.print(f"[red]Stream failed: HTTP {response.status_code}[/red]")
87
78
  return False
88
79
 
89
- if status:
90
- status.update("Waiting for output...")
80
+ console.print("[dim]Streaming output...[/dim]")
91
81
 
92
82
  first_output = True
93
83
  for line in response.iter_lines():
@@ -103,10 +93,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
103
93
  output_data = data["output"]
104
94
  content = output_data.get("content", "")
105
95
  if content:
106
- # Stop status spinner on first output
107
- if first_output and status:
108
- status.stop()
109
- first_output = False
110
96
  clean_output = strip_ansi_codes(content)
111
97
  # Normalize newlines to avoid excessive blank lines
112
98
  clean_output = normalize_newlines(clean_output)
@@ -114,8 +100,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
114
100
 
115
101
  # Handle job finished event
116
102
  elif "jobFinished" in data:
117
- if status:
118
- status.stop()
119
103
  job_data = data["jobFinished"]
120
104
  job = job_data.get("job", {})
121
105
  result = job.get("result", {})
@@ -126,7 +110,6 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
126
110
  if not first_output:
127
111
  print()
128
112
 
129
- # Check for system failure (error field present)
130
113
  if error:
131
114
  console.print(f"[red]{error}[/red]")
132
115
  return False
@@ -143,46 +126,34 @@ def stream_execution_output(execution_id: str, streaming_url: str = None, status
143
126
  if event_type == "output":
144
127
  output = data.get("content", "")
145
128
  if output:
146
- if first_output and status:
147
- status.stop()
148
- first_output = False
149
129
  clean_output = strip_ansi_codes(output)
150
130
  clean_output = normalize_newlines(clean_output)
151
131
  print(clean_output, end="", flush=True)
152
132
 
153
133
  elif event_type == "completed":
154
- if status:
155
- status.stop()
156
- status_val = data.get("status", "unknown")
157
- if not first_output:
158
- print()
159
- if status_val == "completed":
134
+ status = data.get("status", "unknown")
135
+ if status == "completed":
136
+ console.print("\n[green]Execution completed successfully[/green]")
160
137
  return True
161
138
  else:
162
- console.print(f"[red]Failed: {status_val}[/red]")
139
+ console.print(f"\n[red]Execution failed: {status}[/red]")
163
140
  return False
164
141
 
165
142
  elif event_type == "error":
166
- if status:
167
- status.stop()
168
143
  error_msg = data.get("message", "Unknown error")
169
- console.print(f"[red]Error: {error_msg}[/red]")
144
+ console.print(f"\n[red]Error: {error_msg}[/red]")
170
145
  return False
171
146
 
172
147
  except json.JSONDecodeError:
173
148
  # Skip malformed JSON
174
149
  continue
175
150
 
176
- if status:
177
- status.stop()
178
- console.print("[yellow]Stream ended without completion signal[/yellow]")
151
+ console.print("\n[yellow]Stream ended without completion signal[/yellow]")
179
152
  # Fallback: poll execution status
180
153
  return check_execution_status(execution_id)
181
154
 
182
155
  except Exception as e:
183
- if status:
184
- status.stop()
185
- console.print(f"[red]Streaming error: {e}[/red]")
156
+ console.print(f"\n[red]Streaming error: {e}[/red]")
186
157
  # Fallback: poll execution status
187
158
  return check_execution_status(execution_id)
188
159
 
@@ -206,15 +177,16 @@ def check_execution_status(execution_id: str) -> bool:
206
177
  status = data.get('status', 'unknown')
207
178
 
208
179
  if status == 'completed':
180
+ console.print("[green]Execution completed successfully[/green]")
209
181
  return True
210
182
  elif status in ['failed_user', 'failed_system', 'failed']:
211
- console.print(f"[red]Failed: {status}[/red]")
183
+ console.print(f"[red]Execution failed: {status}[/red]")
212
184
  errors = data.get('errors')
213
185
  if errors:
214
186
  console.print(f"[red]Error: {errors}[/red]")
215
187
  return False
216
188
  elif status in ['timeout', 'cancelled']:
217
- console.print(f"[yellow]{status.capitalize()}[/yellow]")
189
+ console.print(f"[yellow]Execution {status}[/yellow]")
218
190
  return False
219
191
  elif status in ['running', 'pending', 'queued']:
220
192
  # Still running, continue polling
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lyceum-cli
3
- Version: 1.0.25
3
+ Version: 1.0.27
4
4
  Summary: Command-line interface for Lyceum Cloud Execution API
5
5
  Home-page: https://lyceum.technology
6
6
  Author: Lyceum Team
@@ -0,0 +1,34 @@
1
+ lyceum/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ lyceum/main.py,sha256=s4ZvNPGMeGoV9AIq68hEwfo9ioFfu2qIoRXb2upjNDk,1076
3
+ lyceum/external/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
+ lyceum/external/auth/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ lyceum/external/auth/login.py,sha256=-yJ0aEV8_vDXiT6BXzjpqZ2uDdnTnkop4qhagw2dSZA,23447
6
+ lyceum/external/compute/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ lyceum/external/compute/execution/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ lyceum/external/compute/execution/config.py,sha256=6JJgLJnDPTwevEaNdB1nEICih_qbBmws5u5_S9gj7k0,8866
9
+ lyceum/external/compute/execution/docker.py,sha256=0Y6lxJAm56Jrl0HxeNz1mX6DGs556i2iMN9_U1JQP0c,9635
10
+ lyceum/external/compute/execution/docker_compose.py,sha256=YsWPnw5nB1ZpqjU9X8o_klT78I5m46PapVwVEeWra_8,9189
11
+ lyceum/external/compute/execution/notebook.py,sha256=Gw9UhJ-UjYhpjdIYQ4IMYhVjhSkAFpOQ9aFYj1qOeww,7542
12
+ lyceum/external/compute/execution/python.py,sha256=8Y9ElWs9RdauQbhECKcBPSoT0XZeGhXZ_pkEpr3sGro,12878
13
+ lyceum/external/compute/execution/workloads.py,sha256=4fsRWbYGmsQMGPPIN1jUG8cG5NPG9yV26ANJ-DtaXqc,5844
14
+ lyceum/external/compute/inference/__init__.py,sha256=4YLoUKDEzitexynJv_Q5O0w1lty8CJ6uyRxuc1LiaBw,89
15
+ lyceum/external/compute/inference/batch.py,sha256=mgEndr02UM1j00o-iRLUpDqS5KFvyg0Htc0Gg0s3hTU,11394
16
+ lyceum/external/compute/inference/chat.py,sha256=hITj_UGLaxCJQskU-YbeaEerM5Xt_eJpEsYrTJoUpk4,8485
17
+ lyceum/external/compute/inference/models.py,sha256=BkCEdvyliezGOUulj557e-Eoif0_HKR3CxqpEhdAZaA,10339
18
+ lyceum/external/general/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
+ lyceum/external/vms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ lyceum/external/vms/instances.py,sha256=8DKpI8PbyZFzk5RT-IPgoMDjkf_-HC-2pJKuSFs-5BA,11007
21
+ lyceum/external/vms/management.py,sha256=dYEkN5Qiur-SG4G5CLOk2Rbr0HW3rK1BROSp0K6KxC8,15405
22
+ lyceum/shared/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
+ lyceum/shared/config.py,sha256=gz2AjjdOsi6WbJApIh_GYRs_GYjsORQ6h3YJiHEIOxI,5384
24
+ lyceum/shared/display.py,sha256=-VSAfoa0yivTvxRrN2RYr2Sq1x_msZqENjnkSedmbhQ,4444
25
+ lyceum/shared/imports.py,sha256=wEG4wfVTIqJ6MBWDRAN96iGmVCb9ST2aOqSjkbvajug,11768
26
+ lyceum/shared/streaming.py,sha256=x3zA8Pn9ia06t8nKJfP6hztxOVKPUC3Nk3qmAiIsl9M,8194
27
+ lyceum_cloud_execution_api_client/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
+ lyceum_cloud_execution_api_client/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
+ lyceum_cloud_execution_api_client/models/__init__.py,sha256=AMlb9R9O9aNC9hvKz_8TFpEfOolYC3VtFS5JX17kYks,4888
30
+ lyceum_cli-1.0.27.dist-info/METADATA,sha256=r__wR3k1hw1LzdIwKOVpNoOWRwYmJ1pmYhonGMC73Sc,1482
31
+ lyceum_cli-1.0.27.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
32
+ lyceum_cli-1.0.27.dist-info/entry_points.txt,sha256=Oq-9wDkxVd6MHgNiUTYwXI9SGhvR3VkD7Mvk0xhiUZo,43
33
+ lyceum_cli-1.0.27.dist-info/top_level.txt,sha256=CR7FEMloAXgLsHUR6ti3mWNcpgje27HRHSfq8doIils,41
34
+ lyceum_cli-1.0.27.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,3 +1,2 @@
1
1
  lyceum
2
2
  lyceum_cloud_execution_api_client
3
- tests
@@ -1,123 +0,0 @@
1
- """Docker registry configuration commands"""
2
-
3
- import json
4
- from pathlib import Path
5
-
6
- import typer
7
- from rich.console import Console
8
- from rich.table import Table
9
-
10
- console = Console()
11
-
12
- docker_config_app = typer.Typer(name="config", help="Docker registry configuration")
13
-
14
- CONFIG_DIR = Path.home() / ".lyceum"
15
- DOCKER_CONFIG_FILE = CONFIG_DIR / "docker-registries.json"
16
-
17
-
18
- def load_docker_config() -> dict:
19
- """Load docker registry configuration."""
20
- if DOCKER_CONFIG_FILE.exists():
21
- try:
22
- with open(DOCKER_CONFIG_FILE) as f:
23
- return json.load(f)
24
- except Exception:
25
- pass
26
- return {"registries": {}}
27
-
28
-
29
- def save_docker_config(config: dict) -> None:
30
- """Save docker registry configuration."""
31
- CONFIG_DIR.mkdir(parents=True, exist_ok=True)
32
- with open(DOCKER_CONFIG_FILE, "w") as f:
33
- json.dump(config, f, indent=2)
34
- # Set restrictive permissions since this contains credentials
35
- DOCKER_CONFIG_FILE.chmod(0o600)
36
-
37
-
38
- def get_hub_credentials(image: str) -> dict | None:
39
- """Get Docker Hub credentials if the image is from Docker Hub.
40
-
41
- Docker Hub images either have no registry prefix or use docker.io.
42
- Returns None if no credentials found or image is not from Docker Hub.
43
- """
44
- config = load_docker_config()
45
- registries = config.get("registries", {})
46
-
47
- if "hub" not in registries:
48
- return None
49
-
50
- # Check if this looks like a Docker Hub image
51
- # Docker Hub: no dots in the first part, or explicitly docker.io
52
- if "/" not in image:
53
- # Single name like "python:3.9" - it's Docker Hub
54
- return registries["hub"]
55
-
56
- parts = image.split("/")
57
- first_part = parts[0]
58
-
59
- # If first part has a dot, it's a custom registry (not Docker Hub)
60
- # Exception: docker.io is Docker Hub
61
- if "." in first_part and first_part != "docker.io":
62
- return None
63
-
64
- # Otherwise it's Docker Hub (e.g., "myuser/myimage" or "docker.io/myuser/myimage")
65
- return registries["hub"]
66
-
67
-
68
- @docker_config_app.command("hub")
69
- def configure_hub():
70
- """Configure Docker Hub credentials interactively."""
71
- console.print("\n[bold]Docker Hub Configuration[/bold]")
72
- console.print("[dim]These credentials will be used for private Docker Hub images.[/dim]\n")
73
-
74
- username = typer.prompt("Docker Hub Username")
75
- password = typer.prompt("Docker Hub Password/Token", hide_input=True)
76
-
77
- config = load_docker_config()
78
- config["registries"]["hub"] = {
79
- "username": username,
80
- "password": password,
81
- }
82
- save_docker_config(config)
83
-
84
- console.print("\n[green]Docker Hub credentials saved![/green]")
85
- console.print("[dim]Credentials stored in ~/.lyceum/docker-registries.json[/dim]")
86
-
87
-
88
- @docker_config_app.command("show")
89
- def show_config():
90
- """Show configured docker registries."""
91
- config = load_docker_config()
92
- registries = config.get("registries", {})
93
-
94
- if not registries:
95
- console.print("[dim]No docker registries configured.[/dim]")
96
- console.print("[dim]Run 'lyceum docker config hub' to configure Docker Hub.[/dim]")
97
- console.print("[dim]For AWS ECR, use 'lyceum docker run <image> --aws' to auto-detect credentials.[/dim]")
98
- return
99
-
100
- table = Table(title="Configured Docker Registries")
101
- table.add_column("Registry", style="cyan")
102
- table.add_column("Details", style="dim")
103
-
104
- if "hub" in registries:
105
- hub = registries["hub"]
106
- username = hub.get("username", "")
107
- table.add_row("Docker Hub", f"User: {username}")
108
-
109
- console.print(table)
110
- console.print("\n[dim]For AWS ECR, use 'lyceum docker run <image> --aws' to auto-detect credentials.[/dim]")
111
-
112
-
113
- @docker_config_app.command("clear")
114
- def clear_config():
115
- """Clear saved Docker Hub credentials."""
116
- config = load_docker_config()
117
-
118
- if "hub" in config.get("registries", {}):
119
- del config["registries"]["hub"]
120
- save_docker_config(config)
121
- console.print("[green]Cleared Docker Hub credentials.[/green]")
122
- else:
123
- console.print("[yellow]No Docker Hub credentials found.[/yellow]")