steve-cli 0.3.2__tar.gz → 0.3.6__tar.gz
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.
- {steve_cli-0.3.2 → steve_cli-0.3.6}/PKG-INFO +1 -1
- {steve_cli-0.3.2 → steve_cli-0.3.6}/pyproject.toml +1 -1
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli/cli.py +269 -3
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/PKG-INFO +1 -1
- {steve_cli-0.3.2 → steve_cli-0.3.6}/README.md +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/setup.cfg +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli/__init__.py +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli/storage.py +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/SOURCES.txt +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/dependency_links.txt +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/entry_points.txt +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/requires.txt +0 -0
- {steve_cli-0.3.2 → steve_cli-0.3.6}/steve_cli.egg-info/top_level.txt +0 -0
|
@@ -9,8 +9,11 @@ Usage:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
+
import signal
|
|
13
|
+
import socket
|
|
12
14
|
import subprocess
|
|
13
15
|
import sys
|
|
16
|
+
import time
|
|
14
17
|
from pathlib import Path
|
|
15
18
|
from typing import Dict, List, Any, Optional
|
|
16
19
|
|
|
@@ -110,8 +113,6 @@ def jobs(ctx: click.Context, jobs_file: Optional[Path]):
|
|
|
110
113
|
if not job_names:
|
|
111
114
|
click.echo("❌ No jobs found in jobs.yaml")
|
|
112
115
|
return
|
|
113
|
-
click.echo(ctx.get_help())
|
|
114
|
-
click.echo()
|
|
115
116
|
choice = questionary.select(
|
|
116
117
|
"Select a job to run:",
|
|
117
118
|
choices=job_names,
|
|
@@ -175,6 +176,268 @@ def jobs_run(job_name: str, jobs_file: Optional[Path]):
|
|
|
175
176
|
sys.exit(run_job_command(job))
|
|
176
177
|
|
|
177
178
|
|
|
179
|
+
APPS_DIR = Path.home() / ".steve" / "apps"
|
|
180
|
+
AUTH_PROXY_INTERNAL_URL = "http://auth-proxy-service"
|
|
181
|
+
APP_NAME_PREFIX = "term-"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AppsConfig:
|
|
185
|
+
def __init__(self, apps_file: Optional[Path] = None):
|
|
186
|
+
self.apps_file = apps_file or Path.cwd() / "apps.yaml"
|
|
187
|
+
self.apps: List[Dict[str, Any]] = []
|
|
188
|
+
self._load_apps()
|
|
189
|
+
|
|
190
|
+
def _load_apps(self) -> None:
|
|
191
|
+
try:
|
|
192
|
+
if not self.apps_file.exists():
|
|
193
|
+
click.echo(f"❌ Error: apps.yaml not found at {self.apps_file}", err=True)
|
|
194
|
+
click.echo("Make sure you're in a directory with an apps.yaml file", err=True)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
with open(self.apps_file, 'r') as f:
|
|
198
|
+
data = yaml.safe_load(f)
|
|
199
|
+
|
|
200
|
+
if not data or 'apps' not in data:
|
|
201
|
+
click.echo("❌ Error: Invalid apps.yaml format. Expected 'apps' key at root", err=True)
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
self.apps = data['apps']
|
|
205
|
+
|
|
206
|
+
except yaml.YAMLError as e:
|
|
207
|
+
click.echo(f"❌ Error parsing apps.yaml: {e}", err=True)
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
click.echo(f"❌ Error reading apps.yaml: {e}", err=True)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
def get_app(self, app_name: str) -> Optional[Dict[str, Any]]:
|
|
214
|
+
for app in self.apps:
|
|
215
|
+
if app.get('name') == app_name:
|
|
216
|
+
return app
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def list_apps(self) -> List[str]:
|
|
220
|
+
return [app.get('name', 'unnamed') for app in self.apps]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _get_session_name() -> Optional[str]:
|
|
224
|
+
return os.environ.get('SESSION_NAME') or socket.gethostname()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _pid_file(app_name: str) -> Path:
|
|
228
|
+
return APPS_DIR / f"{app_name}.pid"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _url_file(app_name: str) -> Path:
|
|
232
|
+
return APPS_DIR / f"{app_name}.url"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _get_running_pid(app_name: str) -> Optional[int]:
|
|
236
|
+
pf = _pid_file(app_name)
|
|
237
|
+
if not pf.exists():
|
|
238
|
+
return None
|
|
239
|
+
try:
|
|
240
|
+
pid = int(pf.read_text().strip())
|
|
241
|
+
os.kill(pid, 0)
|
|
242
|
+
return pid
|
|
243
|
+
except (ValueError, OSError):
|
|
244
|
+
pf.unlink(missing_ok=True)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _register_app(session_name: str, app_name: str, port: int) -> Optional[Dict[str, Any]]:
|
|
249
|
+
import urllib.request
|
|
250
|
+
import json
|
|
251
|
+
url = f"{AUTH_PROXY_INTERNAL_URL}/api/internal/register-app"
|
|
252
|
+
payload = json.dumps({"sessionName": session_name, "appName": app_name, "port": port}).encode()
|
|
253
|
+
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST")
|
|
254
|
+
try:
|
|
255
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
256
|
+
return json.loads(resp.read())
|
|
257
|
+
except Exception as e:
|
|
258
|
+
click.echo(f"⚠️ Failed to register app with auth-proxy: {e}", err=True)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _deregister_app(session_name: str, app_name: str) -> None:
|
|
263
|
+
import urllib.request
|
|
264
|
+
import json
|
|
265
|
+
url = f"{AUTH_PROXY_INTERNAL_URL}/api/internal/register-app"
|
|
266
|
+
payload = json.dumps({"sessionName": session_name, "appName": app_name}).encode()
|
|
267
|
+
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="DELETE")
|
|
268
|
+
try:
|
|
269
|
+
urllib.request.urlopen(req, timeout=5)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@main.group(invoke_without_command=True)
|
|
275
|
+
@click.option('--apps-file', '-f', type=click.Path(exists=True, path_type=Path),
|
|
276
|
+
help='Path to apps.yaml file (default: ./apps.yaml)')
|
|
277
|
+
@click.pass_context
|
|
278
|
+
def apps(ctx: click.Context, apps_file: Optional[Path]):
|
|
279
|
+
"""Manage and run apps from apps.yaml inside the terminal."""
|
|
280
|
+
if ctx.invoked_subcommand is not None:
|
|
281
|
+
return
|
|
282
|
+
config = AppsConfig(apps_file)
|
|
283
|
+
app_names = config.list_apps()
|
|
284
|
+
if not app_names:
|
|
285
|
+
click.echo("❌ No apps found in apps.yaml")
|
|
286
|
+
return
|
|
287
|
+
choice = questionary.select(
|
|
288
|
+
"Select an app to start:",
|
|
289
|
+
choices=app_names,
|
|
290
|
+
).ask()
|
|
291
|
+
if choice is None:
|
|
292
|
+
sys.exit(0)
|
|
293
|
+
app_def = config.get_app(choice)
|
|
294
|
+
ctx.invoke(apps_start, app_name=choice, apps_file=apps_file)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@apps.command('ls')
|
|
298
|
+
@click.option('--apps-file', '-f', type=click.Path(exists=True, path_type=Path),
|
|
299
|
+
help='Path to apps.yaml file (default: ./apps.yaml)')
|
|
300
|
+
def apps_list(apps_file: Optional[Path]):
|
|
301
|
+
"""List all available apps."""
|
|
302
|
+
config = AppsConfig(apps_file)
|
|
303
|
+
if not config.apps:
|
|
304
|
+
click.echo("❌ No apps found in apps.yaml")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
click.echo(f"📋 Available apps in {click.style(str(config.apps_file), fg='cyan')}:")
|
|
308
|
+
click.echo()
|
|
309
|
+
|
|
310
|
+
for app_data in config.apps:
|
|
311
|
+
name = app_data.get('name', 'unnamed')
|
|
312
|
+
command = app_data.get('command', [])
|
|
313
|
+
port = app_data.get('port', '?')
|
|
314
|
+
env_vars = app_data.get('env', {})
|
|
315
|
+
pid = _get_running_pid(name)
|
|
316
|
+
status = click.style('● running', fg='green') if pid else click.style('○ stopped', fg='yellow')
|
|
317
|
+
|
|
318
|
+
click.echo(f" {click.style(name, fg='blue', bold=True)} {status}")
|
|
319
|
+
if command:
|
|
320
|
+
click.echo(f" Command: {click.style(' '.join(command), fg='cyan')}")
|
|
321
|
+
click.echo(f" Port: {click.style(str(port), fg='magenta')}")
|
|
322
|
+
if env_vars:
|
|
323
|
+
click.echo(f" Environment: {click.style(f'{len(env_vars)} variables', fg='green')}")
|
|
324
|
+
click.echo()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@apps.command('start')
|
|
328
|
+
@click.argument('app_name')
|
|
329
|
+
@click.option('--apps-file', '-f', type=click.Path(exists=True, path_type=Path),
|
|
330
|
+
help='Path to apps.yaml file (default: ./apps.yaml)')
|
|
331
|
+
def apps_start(app_name: str, apps_file: Optional[Path]):
|
|
332
|
+
"""Start an app by name and register it with the auth-proxy."""
|
|
333
|
+
config = AppsConfig(apps_file)
|
|
334
|
+
app_def = config.get_app(app_name)
|
|
335
|
+
if not app_def:
|
|
336
|
+
available = config.list_apps()
|
|
337
|
+
click.echo(f"❌ Error: App '{app_name}' not found", err=True)
|
|
338
|
+
if available:
|
|
339
|
+
click.echo(f"Available apps: {', '.join(available)}", err=True)
|
|
340
|
+
sys.exit(1)
|
|
341
|
+
|
|
342
|
+
session_name = _get_session_name()
|
|
343
|
+
if not session_name:
|
|
344
|
+
click.echo("❌ Error: SESSION_NAME environment variable not set. Are you running inside the terminal?", err=True)
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
existing_pid = _get_running_pid(app_name)
|
|
348
|
+
if existing_pid:
|
|
349
|
+
click.echo(f"⚠️ App '{app_name}' is already running (PID {existing_pid})")
|
|
350
|
+
uf = _url_file(app_name)
|
|
351
|
+
if uf.exists():
|
|
352
|
+
click.echo(f"🌐 Access at: {click.style(uf.read_text().strip(), fg='cyan')}")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
command = app_def.get('command', [])
|
|
356
|
+
port = app_def.get('port')
|
|
357
|
+
env_vars = app_def.get('env', {})
|
|
358
|
+
|
|
359
|
+
if not command:
|
|
360
|
+
click.echo(f"❌ Error: No command specified for app '{app_name}'", err=True)
|
|
361
|
+
sys.exit(1)
|
|
362
|
+
if not port:
|
|
363
|
+
click.echo(f"❌ Error: No port specified for app '{app_name}'", err=True)
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
|
|
366
|
+
env = os.environ.copy()
|
|
367
|
+
for key, value in env_vars.items():
|
|
368
|
+
env[key] = str(value)
|
|
369
|
+
|
|
370
|
+
APPS_DIR.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
log_file = APPS_DIR / f"{app_name}.log"
|
|
372
|
+
|
|
373
|
+
click.echo(f"🚀 Starting app: {click.style(app_name, fg='blue', bold=True)}")
|
|
374
|
+
click.echo(f"📝 Command: {click.style(' '.join(command), fg='cyan')}")
|
|
375
|
+
click.echo(f"🔌 Port: {click.style(str(port), fg='magenta')}")
|
|
376
|
+
|
|
377
|
+
app_cwd = config.apps_file.parent
|
|
378
|
+
with open(log_file, 'a') as lf:
|
|
379
|
+
proc = subprocess.Popen(command, env=env, cwd=app_cwd, stdout=lf, stderr=lf)
|
|
380
|
+
|
|
381
|
+
time.sleep(1)
|
|
382
|
+
if proc.poll() is not None:
|
|
383
|
+
click.echo(f"❌ App '{app_name}' exited immediately (code {proc.returncode})", err=True)
|
|
384
|
+
click.echo(f"📄 Last log output:", err=True)
|
|
385
|
+
try:
|
|
386
|
+
with open(log_file) as lf:
|
|
387
|
+
click.echo(lf.read(), err=True)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
|
|
392
|
+
_pid_file(app_name).write_text(str(proc.pid))
|
|
393
|
+
|
|
394
|
+
result = _register_app(session_name, f"{APP_NAME_PREFIX}{app_name}", port)
|
|
395
|
+
|
|
396
|
+
if result:
|
|
397
|
+
click.echo(f"✅ App started (PID {proc.pid}) and registered with auth-proxy")
|
|
398
|
+
public_url = result.get('url')
|
|
399
|
+
else:
|
|
400
|
+
click.echo(f"✅ App started (PID {proc.pid}), but registration with auth-proxy failed")
|
|
401
|
+
public_url = None
|
|
402
|
+
|
|
403
|
+
if public_url:
|
|
404
|
+
click.echo(f"🌐 Access at: {click.style(public_url, fg='cyan')}")
|
|
405
|
+
else:
|
|
406
|
+
public_url = f'http://localhost:{port}'
|
|
407
|
+
click.echo(f"🌐 Access at (local): {click.style(public_url, fg='yellow')}")
|
|
408
|
+
|
|
409
|
+
_url_file(app_name).write_text(public_url)
|
|
410
|
+
|
|
411
|
+
click.echo(f"📄 Logs: {click.style(str(log_file), fg='yellow')}")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@apps.command('stop')
|
|
415
|
+
@click.argument('app_name')
|
|
416
|
+
@click.option('--apps-file', '-f', type=click.Path(exists=True, path_type=Path),
|
|
417
|
+
help='Path to apps.yaml file (default: ./apps.yaml)')
|
|
418
|
+
def apps_stop(app_name: str, apps_file: Optional[Path]):
|
|
419
|
+
"""Stop a running app and deregister it from the auth-proxy."""
|
|
420
|
+
session_name = _get_session_name()
|
|
421
|
+
|
|
422
|
+
pid = _get_running_pid(app_name)
|
|
423
|
+
if not pid:
|
|
424
|
+
click.echo(f"⚠️ App '{app_name}' is not running")
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
os.kill(pid, signal.SIGTERM)
|
|
429
|
+
click.echo(f"🛑 Stopped app '{app_name}' (PID {pid})")
|
|
430
|
+
except OSError as e:
|
|
431
|
+
click.echo(f"❌ Failed to stop process: {e}", err=True)
|
|
432
|
+
|
|
433
|
+
_pid_file(app_name).unlink(missing_ok=True)
|
|
434
|
+
_url_file(app_name).unlink(missing_ok=True)
|
|
435
|
+
|
|
436
|
+
if session_name:
|
|
437
|
+
_deregister_app(session_name, f"{APP_NAME_PREFIX}{app_name}")
|
|
438
|
+
click.echo(f"🔌 Deregistered from auth-proxy")
|
|
439
|
+
|
|
440
|
+
|
|
178
441
|
SETUP_IGNORE = [".env.example", ".env.template"]
|
|
179
442
|
|
|
180
443
|
|
|
@@ -230,8 +493,11 @@ def setup_env():
|
|
|
230
493
|
out_file.write_text(result.stdout)
|
|
231
494
|
|
|
232
495
|
click.echo(f" ✅ Decrypted -> {click.style(out_file.name, fg='cyan')}")
|
|
233
|
-
|
|
496
|
+
cmd = f"""set -a
|
|
497
|
+
source {out_file.name}
|
|
498
|
+
set +a"""
|
|
234
499
|
|
|
500
|
+
click.echo(f" 💡 Run:\n{click.style(cmd, fg='yellow')}")
|
|
235
501
|
|
|
236
502
|
def _detect_workspaces() -> List[str]:
|
|
237
503
|
tiers = {"BRONZE", "SILVER", "GOLD"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|