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