|
10 | 10 | lpb --stop Stop the container |
11 | 11 | lpb --remove Stop + remove container + state dirs |
12 | 12 | lpb --logs Stream container logs |
13 | | - lpb --update Pull latest image(s) |
14 | | - lpb --config Show config file location |
15 | | - lpb --help Show usage |
16 | 13 | lpb --tag dev|main|latest Select image pipeline (dev/main/latest/<custom>) |
| 14 | + lpb --dev / lpb --main Shorthand for --tag dev / --tag main |
| 15 | + lpb --update Pull latest image(s) (+ self-update launcher) |
17 | 16 |
|
18 | 17 | Positional command aliases (no -- needed): |
19 | 18 | lpb logs → lpb --logs |
|
29 | 28 | lpb --tag main Use latest stable image (0.0.x-lpb-cli) |
30 | 29 | lpb --tag latest Same as main |
31 | 30 | lpb --tag 0.0.9-lpb-dev Pin to specific version image |
| 31 | + lpb --dev / lpb --main Shorthand for --tag dev / --tag main |
32 | 32 | LPB_IMAGE_TAG=0.0.9-lpb-dev Or set env var for persistent override |
33 | 33 |
|
| 34 | +Self-update (lpb --update): |
| 35 | + Updates the installed lpb/lpb.py from the pipeline branch matching the |
| 36 | + tag (dev tags → dev branch, stable tags → main branch), then pulls images. |
| 37 | +
|
34 | 38 | Pi passthrough (after "--"): |
35 | 39 | lpb /myproject -- -p "summarize this repo" |
36 | 40 | lpb /myproject -- --session abc123 |
@@ -310,64 +314,68 @@ class Config: |
310 | 314 | def err(msg: str, hint: str = "") -> None: |
311 | 315 | """Print an error (red) to stderr; optionally with a yellow hint line.""" |
312 | 316 | logger.error("%sError: %s%s", _ERR, msg, _RSV) |
313 | | - print(f"{_ERR}Error: {msg}{_RSV}", file=sys.stderr) |
314 | 317 | if hint: |
315 | 318 | logger.warning(" %s%s%s", _WRN, hint, _RSV) |
316 | | - print(f" {_WRN}{hint}{_RSV}", file=sys.stderr) |
317 | 319 |
|
318 | 320 |
|
319 | 321 | def warn(msg: str) -> None: |
320 | | - """Print a warning (yellow) to stderr.""" |
| 322 | + """Print a warning (yellow) to stderr (single line via logger).""" |
321 | 323 | logger.warning("%sWarning: %s%s", _WRN, msg, _RSV) |
322 | | - print(f"{_WRN}Warning: {msg}{_RSV}", file=sys.stderr) |
323 | 324 |
|
324 | 325 |
|
325 | 326 | # ── Output helpers (stdout) ─────────────────────────────────────────────────── |
326 | 327 |
|
| 328 | +# Engine file path — resolved from __file__, NOT sys.argv[0]: the bash wrapper |
| 329 | +# execs `python3 lpb.py`, so argv[0] is the engine and the wrapper is its |
| 330 | +# sibling "lpb" in the same directory. (Tests may override this.) |
| 331 | +LPB_ENGINE_PATH = Path(__file__).resolve() |
| 332 | + |
| 333 | + |
| 334 | +def _fetch_file(url: str, dest: Path, staging: Path) -> None: |
| 335 | + """Download url and atomically replace dest if the content changed.""" |
| 336 | + with urllib.request.urlopen(url, timeout=10) as resp: |
| 337 | + new_data = resp.read() |
| 338 | + with open(dest, "rb") as f: |
| 339 | + old_data = f.read() |
| 340 | + if new_data == old_data: |
| 341 | + return |
| 342 | + info(f"Updating {dest.name}...") |
| 343 | + with open(staging, "wb") as f: |
| 344 | + f.write(new_data) |
| 345 | + staging.rename(dest) |
| 346 | + dest.chmod(0o755) |
| 347 | + |
| 348 | + |
| 349 | +# ── Self-update (lpb --update) ─────────────────────────────────────────────────── |
| 350 | + |
327 | 351 | def self_update() -> None: |
328 | | - """Update lpb itself from the GitHub repo if a newer version is available.""" |
329 | | - lpb_path = Path(sys.argv[0]).resolve() |
330 | | - lpb_dir = lpb_path.parent |
331 | | - new_path = lpb_dir / "lpb.new" |
332 | | - if not lpb_path.is_file(): |
| 352 | + """Update lpb (wrapper) + lpb.py (engine) from the GitHub repo. |
| 353 | +
|
| 354 | + Source branch follows the pipeline tag: --tag dev (or a versioned |
| 355 | + *-dev tag) pulls from the dev branch, everything else from main — so a |
| 356 | + launcher installed from main can be updated from dev (and vice versa) |
| 357 | + simply by choosing the tag. Network/IO failures never break startup. |
| 358 | + """ |
| 359 | + engine_path = LPB_ENGINE_PATH |
| 360 | + if not engine_path.is_file(): |
333 | 361 | return |
334 | | - # Select branch based on cfg.image_tag (set by --tag) |
335 | | - branch = "dev" if cfg.image_tag == "dev" else "main" |
336 | | - script_url = f"https://raw.githubusercontent.com/lpb-stack/devstack/{branch}/scripts/lpb" |
337 | | - py_url = script_url.replace("/lpb", "/lpb.py") |
| 362 | + base_dir = engine_path.parent |
| 363 | + wrapper_path = base_dir / "lpb" |
| 364 | + tag = (cfg.image_tag or "").strip().lower() |
| 365 | + branch = "dev" if (tag == "dev" or tag.endswith("-dev")) else "main" |
| 366 | + base_url = f"https://raw.githubusercontent.com/lpb-stack/devstack/{branch}/scripts/" |
| 367 | + staging = base_dir / "lpb.py.new" |
338 | 368 | try: |
339 | | - with urllib.request.urlopen(script_url, timeout=10) as resp: |
340 | | - new_wrapper = resp.read() |
341 | | - with open(lpb_path, "rb") as f: |
342 | | - old_wrapper = f.read() |
343 | | - if new_wrapper != old_wrapper: |
344 | | - info(f"Updating lpb launcher...") |
345 | | - with open(new_path, "wb") as f: |
346 | | - f.write(new_wrapper) |
347 | | - new_path.rename(lpb_path) |
348 | | - lpb_path.chmod(0o755) |
349 | | - with urllib.request.urlopen(py_url, timeout=10) as resp: |
350 | | - new_py = resp.read() |
351 | | - py_path = lpb_dir / "lpb.py" |
352 | | - if py_path.is_file(): |
353 | | - with open(py_path, "rb") as f: |
354 | | - old_py = f.read() |
355 | | - if new_py != old_py: |
356 | | - info(f"Updating lpb.py...") |
357 | | - with open(new_path, "wb") as f: |
358 | | - f.write(new_py) |
359 | | - new_path.rename(py_path) |
360 | | - elif not py_path.is_file(): |
361 | | - info(f"Installing missing lpb.py...") |
362 | | - with open(new_path, "wb") as f: |
363 | | - f.write(new_py) |
364 | | - new_path.rename(py_path) |
365 | | - py_path.chmod(0o755) |
366 | | - for _f in lpb_dir.iterdir(): |
367 | | - if _f.name.endswith(".new"): |
368 | | - _f.unlink() |
369 | | - except Exception as exc: # network/IO failures should never break startup |
| 369 | + # Engine first (this running script itself) |
| 370 | + _fetch_file(base_url + "lpb.py", engine_path, staging) |
| 371 | + # Wrapper (optional — only present in install.sh installs) |
| 372 | + if wrapper_path.is_file(): |
| 373 | + _fetch_file(base_url + "lpb", wrapper_path, staging) |
| 374 | + except Exception as exc: |
370 | 375 | warn(f"self-update skipped: {exc}") |
| 376 | + finally: |
| 377 | + if staging.exists(): |
| 378 | + staging.unlink() |
371 | 379 |
|
372 | 380 | def info(msg): |
373 | 381 | """Print an informational message to stdout (no color).""" |
@@ -739,9 +747,10 @@ def apply_overrides(project_dir: str | None = None, project_name: str | None = N |
739 | 747 | " lpb --stop Stop the container\n" |
740 | 748 | " lpb --remove Stop + remove container + state dirs\n" |
741 | 749 | " lpb --logs Stream container logs\n" |
742 | | - " lpb --update Pull latest image(s)\n" |
| 750 | + " lpb --update Pull latest image(s) (+ self-update launcher)\n" |
743 | 751 | " lpb --config Show config file location\n" |
744 | | - " lpb --help Show this help\n\n" |
| 752 | + " lpb --help Show this help\n" |
| 753 | + " lpb --tag dev|main|latest Select image pipeline (or --dev / --main)\n\n" |
745 | 754 | "Pi passthrough (after \"--\"):\n" |
746 | 755 | ' lpb /myproject -- -p "summarize" # Non-interactive, process & exit\n' |
747 | 756 | ' lpb /myproject -- --session abc123 # Resume specific session\n' |
@@ -785,6 +794,10 @@ def _build_parser() -> argparse.ArgumentParser: |
785 | 794 | parser.add_argument("--web", action="store_true") |
786 | 795 | parser.add_argument("--tag", default=None, |
787 | 796 | help="Select image pipeline (dev|main|latest|<custom>)") |
| 797 | + parser.add_argument("--dev", action="store_true", |
| 798 | + help="Shorthand for --tag dev (dev pipeline)") |
| 799 | + parser.add_argument("--main", action="store_true", |
| 800 | + help="Shorthand for --tag main (stable pipeline)") |
788 | 801 | parser.add_argument("--version", nargs="?", const="_show", |
789 | 802 | help="Pin to version tag (e.g. 0.0.9-lpb) or show stack version") |
790 | 803 | parser.add_argument("--stop", "-s", action="store_true") |
@@ -844,8 +857,14 @@ def parse_cli(args: list[str]) -> None: |
844 | 857 | cfg.shell_mode = True |
845 | 858 | if known.web: |
846 | 859 | cfg.web_mode = True |
| 860 | + # Pipeline short flags: --dev / --main (only used when --tag/--version absent; |
| 861 | + # explicit --tag or --version always win) |
| 862 | + if known.dev: |
| 863 | + cfg.image_tag = "dev" |
| 864 | + if known.main: |
| 865 | + cfg.image_tag = "main" |
847 | 866 | if known.tag is not None: |
848 | | - cfg.image_tag = known.tag |
| 867 | + cfg.image_tag, _cli_overrides["image_tag"] = known.tag, True |
849 | 868 | if known.version: |
850 | 869 | if known.version == "_show" or known.version == "": |
851 | 870 | # --version without value → show version and exit |
|
0 commit comments