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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.3.2
3
+ Version: 0.3.6
4
4
  Summary: A simple CLI tool to run jobs from jobs.yaml with proper environment setup
5
5
  Author: Frank
6
6
  License: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "steve-cli"
8
- version = "0.3.2"
8
+ version = "0.3.6"
9
9
  description = "A simple CLI tool to run jobs from jobs.yaml with proper environment setup"
10
10
  readme = "README.md"
11
11
  license = {text = "MIT"}
@@ -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
- click.echo(f" 💡 Run: {click.style(f'source {out_file.name}', fg='yellow')}")
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"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.3.2
3
+ Version: 0.3.6
4
4
  Summary: A simple CLI tool to run jobs from jobs.yaml with proper environment setup
5
5
  Author: Frank
6
6
  License: MIT
File without changes
File without changes