Skip to content

Commit afc88fc

Browse files
committed
support: drop .sh wrappers, rename Python files to CLI names, fix streaming output
- Rename install_browser.py → install-browser.py - Rename install_openspec.py → install-openspec.py - Rename browser_state_cleanup.py → browser-state-cleanup.py - Rename lpb_config.py → lpb-config.py - Delete wrapper scripts: install-browser.sh, validate.sh, install-openspec.sh, browser-state-cleanup.sh, build.sh, lpb-config - Update Dockerfile COPY lines and symlinks to point to .py files directly (no .sh extension on symlinks or targets) - Update test imports to use importlib.import_module() for hyphenated names - Clean up docstring references to old .sh names - Fix install_agent_browser() to stream subprocess output instead of capturing it — agent-browser install --with-deps progress is now visible - Fix install_openspec() to stream npm install -g output — npm progress is now visible during installation - lpb-config moved to /opt/pi-support/ for sys.path resolution + symlink from ~/.local/bin/
1 parent 9069204 commit afc88fc

13 files changed

Lines changed: 82 additions & 102 deletions

Dockerfile

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ FROM base AS cli
163163

164164
# ── Support utilities (moved from config/support/ to devstack/support/) ──
165165
# Copied to /opt/pi-support/ — used by start.sh at runtime
166-
COPY --chmod=755 support/browser-state-cleanup.sh /opt/pi-support/browser-state-cleanup.sh
167-
COPY --chmod=755 support/browser_state_cleanup.py /opt/pi-support/browser_state_cleanup.py
166+
COPY --chmod=755 support/browser-state-cleanup.py /opt/pi-support/browser-state-cleanup.py
168167
COPY support/browser-validate.ts /opt/pi-support/browser-validate.ts
169168
COPY --chmod=755 support/_lib.sh /opt/pi-support/_lib.sh
170169
COPY support/session-uuid.ts /opt/pi-support/session-uuid.ts
@@ -178,12 +177,9 @@ COPY support/schemas/ /opt/pi-support/schemas/
178177
COPY scripts/localpibox/ /opt/pi-support/localpibox/
179178

180179
# ── Devstack deployment scripts ──
181-
COPY --chmod=755 support/install-browser.sh /opt/devstack/install-browser.sh
182-
COPY --chmod=755 support/install_browser.py /opt/devstack/install_browser.py
183-
COPY --chmod=755 support/validate.sh /opt/devstack/validate.sh
180+
COPY --chmod=755 support/install-browser.py /opt/devstack/install-browser.py
184181
COPY --chmod=755 support/validate.py /opt/devstack/validate.py
185-
COPY --chmod=755 support/install-openspec.sh /opt/pi-support/install-openspec.sh
186-
COPY --chmod=755 support/install_openspec.py /opt/pi-support/install_openspec.py
182+
COPY --chmod=755 support/install-openspec.py /opt/pi-support/install-openspec.py
187183
COPY --chmod=755 support/start.sh /opt/devstack/start.sh
188184
COPY lpb.conf.env /opt/devstack/lpb.conf.env
189185
COPY lpb.stack.env /opt/devstack/lpb.stack.env
@@ -195,14 +191,16 @@ COPY --chmod=755 support/entrypoint-cli.sh /opt/devstack/entrypoint-cli.sh
195191
# lpb) rather than at container first-run — ~/.local/bin is not a mount, so
196192
# a first-run-only ln would vanish on the next rebuild.
197193
RUN mkdir -p /home/lpb/.local/bin \
198-
&& ln -sf /opt/devstack/install-browser.sh /home/lpb/.local/bin/install-browser.sh \
199-
&& ln -sf /opt/devstack/validate.sh /home/lpb/.local/bin/validate.sh \
200-
&& ln -sf /opt/pi-support/install-openspec.sh /home/lpb/.local/bin/install-openspec.sh \
201-
&& ln -sf /opt/pi-support/browser-state-cleanup.sh /home/lpb/.local/bin/browser-state-cleanup.sh
194+
&& ln -sf /opt/devstack/install-browser.py /home/lpb/.local/bin/install-browser \
195+
&& ln -sf /opt/devstack/validate.py /home/lpb/.local/bin/validate \
196+
&& ln -sf /opt/pi-support/install-openspec.py /home/lpb/.local/bin/install-openspec \
197+
&& ln -sf /opt/pi-support/browser-state-cleanup.py /home/lpb/.local/bin/browser-state-cleanup \
198+
&& ln -sf /opt/pi-support/lpb-config.py /home/lpb/.local/bin/lpb-config
202199

203200
# ── Shell PATH helper ───────────────────────────────────────────────────────
204-
COPY --chmod=755 support/lpb-config /home/lpb/.local/bin/lpb-config
205-
COPY --chmod=755 support/lpb_config.py /home/lpb/.local/bin/lpb_config.py
201+
# lpb-config.py needs to live under /opt/pi-support/ for its sys.path resolution.
202+
# A symlink at ~/.local/bin/lpb-config gives users the clean CLI name.
203+
COPY --chmod=755 support/lpb-config.py /opt/pi-support/lpb-config.py
206204

207205
# ── Root operations: ownership + gitconfig + shell PATH ─────────────────────
208206
USER root

scripts/test_localpibox.py

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
- localpibox.run — subprocess helpers, tool discovery, container detection
88
- localpibox.cli — prompts, common flags, fatal-error helper
99
- support/build.py — env loading, build-arg mapping, docker command
10-
- browser_state_cleanup — session pruning (age + count)
11-
- backup_repos.py — gh repo listing + mirror cloning
10+
- browser-state-cleanup — session pruning (age + count)
11+
- backup-repos.py — gh repo listing + mirror cloning
1212
1313
Runs with plain Python (no third-party deps), mirroring scripts/test_lpb.py.
1414
"""
1515

1616
from __future__ import annotations
1717

18+
import importlib # noqa: E402
1819
import datetime
1920
import io
2021
import os
@@ -39,12 +40,12 @@
3940
from localpibox import run as run_mod # noqa: E402
4041
from localpibox import cli as cli_mod # noqa: E402
4142
import build # noqa: E402
42-
import browser_state_cleanup as bsc # noqa: E402
43-
import backup_repos # noqa: E402
44-
import lpb_config # noqa: E402
45-
import validate # noqa: E402
46-
import install_browser # noqa: E402
47-
import install_openspec # noqa: E402
43+
bsc = importlib.import_module('browser-state-cleanup')
44+
backup_repos = importlib.import_module('backup_repos')
45+
lc = importlib.import_module('lpb-config')
46+
validate = importlib.import_module('validate')
47+
ib = importlib.import_module('install-browser')
48+
iospec = importlib.import_module('install-openspec')
4849

4950

5051
# ═══════════════════════════════════════════════════════════════════════════
@@ -495,7 +496,7 @@ def test_lpb_config_migrate_legacy_layout(tmpdir):
495496
(pi_root / ".initialized").touch()
496497
(pi_root / "agent").mkdir()
497498
cons = _quiet_console()
498-
lpb_config.migrate_legacy_layout(pi_root, tmpdir / "agent_dir", cons)
499+
lc.migrate_legacy_layout(pi_root, tmpdir / "agent_dir", cons)
499500
assert (tmpdir / "agent_dir" / "auth.json").exists()
500501
assert (pi_root / ".initialized").exists() # marker preserved
501502
assert (pi_root / "agent").exists() # subdir preserved
@@ -509,86 +510,86 @@ def test_lpb_config_migrate_noop_when_already_migrated(tmpdir):
509510
agent.mkdir()
510511
(agent / ".git").mkdir()
511512
cons = _quiet_console()
512-
lpb_config.migrate_legacy_layout(pi_root, agent, cons) # must not raise
513+
lc.migrate_legacy_layout(pi_root, agent, cons) # must not raise
513514
assert True
514515

515516

516517
def test_lpb_config_update_clones(tmpdir):
517518
remote = _setup_git_remote(tmpdir)
518519
agent = tmpdir / "agent"
519520
cons = _quiet_console()
520-
code = lpb_config.cmd_update(agent, str(remote), "main", cons)
521+
code = lc.cmd_update(agent, str(remote), "main", cons)
521522
assert code == 0
522523
assert (agent / "f").exists()
523524

524525

525526
def test_lpb_config_update_fast_forward(tmpdir):
526527
remote = _setup_git_remote(tmpdir)
527528
agent = tmpdir / "agent"
528-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
529+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
529530
_push_commit(tmpdir / "work", "two", "two")
530531
cons = _quiet_console()
531-
assert lpb_config.cmd_update(agent, str(remote), "main", cons) == 0
532+
assert lc.cmd_update(agent, str(remote), "main", cons) == 0
532533
assert (agent / "f").read_text() == "two"
533534

534535

535536
def test_lpb_config_update_refuses_when_dirty(tmpdir):
536537
remote = _setup_git_remote(tmpdir)
537538
agent = tmpdir / "agent"
538-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
539+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
539540
_push_commit(tmpdir / "work", "two", "two")
540541
(agent / "local").write_text("keep") # untracked = dirty
541542
cons = _quiet_console()
542-
assert lpb_config.cmd_update(agent, str(remote), "main", cons) == 1
543+
assert lc.cmd_update(agent, str(remote), "main", cons) == 1
543544
assert (agent / "local").exists()
544545

545546

546547
def test_lpb_config_reset_force(tmpdir):
547548
remote = _setup_git_remote(tmpdir)
548549
agent = tmpdir / "agent"
549-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
550+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
550551
_push_commit(tmpdir / "work", "two", "two")
551552
(agent / "junk").write_text("x")
552553
cons = _quiet_console()
553-
assert lpb_config.cmd_reset(agent, str(remote), "main", cons, force=True) == 0
554+
assert lc.cmd_reset(agent, str(remote), "main", cons, force=True) == 0
554555
assert not (agent / "junk").exists()
555556
assert (agent / "f").read_text() == "two"
556557

557558

558559
def test_lpb_config_reset_abort_without_confirmation(tmpdir):
559560
remote = _setup_git_remote(tmpdir)
560561
agent = tmpdir / "agent"
561-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
562+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
562563
(agent / "junk").write_text("x")
563564
cons = _quiet_console()
564-
assert lpb_config.cmd_reset(agent, str(remote), "main", cons, force=False, inp=io.StringIO("n\n")) == 0
565+
assert lc.cmd_reset(agent, str(remote), "main", cons, force=False, inp=io.StringIO("n\n")) == 0
565566
assert (agent / "junk").exists()
566567

567568

568569
def test_lpb_config_status_states(tmpdir):
569570
remote = _setup_git_remote(tmpdir)
570571
agent = tmpdir / "agent"
571-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
572+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
572573
out, err = io.StringIO(), io.StringIO()
573574
cons = log_mod.Console(color=False, out=out, err=err)
574-
assert lpb_config.cmd_status(agent, str(remote), "main", cons) == 0
575+
assert lc.cmd_status(agent, str(remote), "main", cons) == 0
575576
text = out.getvalue() + err.getvalue()
576577
assert "Current:" in text and "Remote:" in text and "clean" in text
577578
# no repo yet
578579
out2, err2 = io.StringIO(), io.StringIO()
579580
cons2 = log_mod.Console(color=False, out=out2, err=err2)
580-
assert lpb_config.cmd_status(tmpdir / "nope", str(remote), "main", cons2) == 0
581+
assert lc.cmd_status(tmpdir / "nope", str(remote), "main", cons2) == 0
581582
assert "No config repo" in (out2.getvalue() + err2.getvalue())
582583

583584

584585
def test_lpb_config_merge_uptodate(tmpdir):
585586
remote = _setup_git_remote(tmpdir)
586587
agent = tmpdir / "agent"
587-
lpb_config.cmd_update(agent, str(remote), "main", _quiet_console())
588+
lc.cmd_update(agent, str(remote), "main", _quiet_console())
588589
cons = _quiet_console()
589-
assert lpb_config.cmd_merge(agent, str(remote), "main", cons) == 0
590+
assert lc.cmd_merge(agent, str(remote), "main", cons) == 0
590591
# merge when no repo -> error
591-
assert lpb_config.cmd_merge(tmpdir / "nope", str(remote), "main", _quiet_console()) == 1
592+
assert lc.cmd_merge(tmpdir / "nope", str(remote), "main", _quiet_console()) == 1
592593

593594

594595
# ═══════════════════════════════════════════════════════════════════════════
@@ -690,8 +691,8 @@ def test_validate_pi_cli_missing(tmpdir):
690691

691692
def test_install_browser_fetch_version():
692693
payload = b'{"channels":{"Stable":{"version":"130.0.6723.58"}}}'
693-
with mock.patch.object(install_browser.urllib.request, "urlopen", return_value=io.BytesIO(payload)):
694-
assert install_browser.fetch_stable_chrome_version() == "130.0.6723.58"
694+
with mock.patch.object(ib.urllib.request, "urlopen", return_value=io.BytesIO(payload)):
695+
assert ib.fetch_stable_chrome_version() == "130.0.6723.58"
695696

696697

697698
def test_install_browser_skips_existing_chrome(tmpdir):
@@ -701,7 +702,7 @@ def test_install_browser_skips_existing_chrome(tmpdir):
701702
mock.patch.object(install_browser, "fetch_stable_chrome_version", return_value=version):
702703
(tmpdir / f"chrome-{version}" / "chrome-linux64").mkdir(parents=True)
703704
(tmpdir / f"chrome-{version}" / "chrome-linux64" / "chrome").touch()
704-
assert install_browser.install_chrome(cons) == 0
705+
assert ib.install_chrome(cons) == 0
705706
assert "already installed" in cons.err.getvalue()
706707

707708

@@ -710,14 +711,14 @@ def test_install_browser_verify_no_chrome(tmpdir):
710711
with mock.patch.object(install_browser, "CHROME_BASE", tmpdir), \
711712
mock.patch.object(install_browser, "SYSTEM_CHROME", tmpdir / "nope"), \
712713
mock.patch.object(install_browser, "which", return_value=None):
713-
assert install_browser.verify_installation(cons) == 1
714+
assert ib.verify_installation(cons) == 1
714715
assert "Chrome binary not found" in cons.err.getvalue()
715716

716717

717718
def test_install_browser_agent_install_missing_binary(tmpdir):
718719
cons = _quiet_console()
719720
with mock.patch.object(install_browser, "which", return_value=None):
720-
assert install_browser.install_agent_browser(cons) == 1
721+
assert ib.install_agent_browser(cons) == 1
721722
assert "not found" in cons.err.getvalue()
722723

723724

@@ -731,7 +732,7 @@ def fake_run(args, timeout=600, cwd=None):
731732

732733
with mock.patch.object(install_browser, "which", return_value="/bin/agent-browser"), \
733734
mock.patch.object(install_browser, "run_cmd", side_effect=fake_run):
734-
assert install_browser.install_agent_browser(cons) == 0
735+
assert ib.install_agent_browser(cons) == 0
735736
assert calls == [
736737
["agent-browser", "install"],
737738
["agent-browser", "install", "--with-deps"],
@@ -746,16 +747,16 @@ def test_openspec_skips_when_installed(tmpdir):
746747
cons = _quiet_console()
747748
with mock.patch.object(install_openspec, "which", return_value="/bin/openspec"), \
748749
mock.patch.object(install_openspec, "run_cmd", return_value=("1.2.3", "", 0)):
749-
assert install_openspec.install_openspec(cons) == 0
750+
assert iospec.install_openspec(cons) == 0
750751
assert "already installed" in cons.out.getvalue()
751752

752753

753754
def test_openspec_install_retries_then_fails(tmpdir):
754755
cons = _quiet_console()
755756
with mock.patch.object(install_openspec, "which", return_value=None), \
756757
mock.patch.object(install_openspec, "run_cmd", return_value=("", "npm err", 1)), \
757-
mock.patch.object(install_openspec.time, "sleep", return_value=None):
758-
assert install_openspec.install_openspec(cons) == 1
758+
mock.patch.object(iospec.time, "sleep", return_value=None):
759+
assert iospec.install_openspec(cons) == 1
759760
assert "3 attempts" in cons.err.getvalue()
760761

761762

@@ -764,7 +765,7 @@ def test_openspec_init_new(tmpdir):
764765
target.mkdir()
765766
cons = _quiet_console()
766767
with mock.patch.object(install_openspec, "run_cmd", return_value=("", "", 0)) as m:
767-
assert install_openspec.init_openspec(target, cons) == 0
768+
assert iospec.init_openspec(target, cons) == 0
768769
assert m.call_args.args[0] == ["openspec", "init", "--tools", "pi"]
769770

770771

@@ -773,7 +774,7 @@ def test_openspec_init_existing_runs_update(tmpdir):
773774
(target / "openspec").mkdir(parents=True)
774775
cons = _quiet_console()
775776
with mock.patch.object(install_openspec, "run_cmd", return_value=("", "", 0)) as m:
776-
assert install_openspec.init_openspec(target, cons) == 0
777+
assert iospec.init_openspec(target, cons) == 0
777778
assert m.call_args.args[0] == ["openspec", "update"]
778779

779780

@@ -786,20 +787,20 @@ def test_openspec_verify(tmpdir):
786787
(target / ".pi" / "prompts" / "opsx-apply.md").write_text("x")
787788
(target / ".pi" / "skills" / "openspec-foo").mkdir(parents=True)
788789
cons = _quiet_console()
789-
assert install_openspec.verify_installation(target, cons) == 0
790+
assert iospec.verify_installation(target, cons) == 0
790791
assert "2 command files" in cons.out.getvalue()
791792

792793

793794
def test_openspec_verify_missing_openspec(tmpdir):
794795
target = tmpdir / "proj"
795796
target.mkdir()
796797
cons = _quiet_console()
797-
assert install_openspec.verify_installation(target, cons) == 1
798+
assert iospec.verify_installation(target, cons) == 1
798799
assert "missing" in cons.err.getvalue()
799800

800801

801802
def test_openspec_resolve_target(tmpdir):
802-
assert install_openspec.resolve_target_dir(str(tmpdir)) == tmpdir.resolve()
803+
assert iospec.resolve_target_dir(str(tmpdir)) == tmpdir.resolve()
803804

804805

805806
# ═══════════════════════════════════════════════════════════════════════════
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
"""browser-state-cleanup — housekeeping for agent-browser session state.
33
4-
Python port of support/browser-state-cleanup.sh.
4+
Python port of support/browser-state-cleanup.
55
66
Removes session dirs older than ``--max-age-days`` OR when the count exceeds
77
``--max-count``, keeping the most recent ones. Optionally kills orphaned

support/browser-state-cleanup.sh

Lines changed: 0 additions & 6 deletions
This file was deleted.

support/build.sh

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#!/usr/bin/env python3
22
"""install-browser — Install Chrome + agent-browser (with system deps).
33
4-
Python port of support/install-browser.sh.
4+
Python port of support/install-browser.
55
66
Downloads Chrome-for-Testing and runs ``agent-browser install --with-deps``,
77
which uses Playwright's dependency resolver to install only the exact
88
libraries needed for the downloaded Chrome version.
99
1010
Usage:
1111
Inside container (as root or with sudo):
12-
bash /opt/devstack/install-browser.sh
12+
/opt/devstack/install-browser
1313
"""
1414

1515
from __future__ import annotations
@@ -31,6 +31,7 @@
3131
from localpibox.cli import add_common_args, console_from_args, install_sigpipe_handler # noqa: E402
3232
from localpibox.log import Console # noqa: E402
3333
from localpibox.run import is_container, run_cmd, which # noqa: E402
34+
import subprocess # noqa: E402
3435

3536
CHROME_BASE = Path("/home/lpb/.agent-browser/browsers")
3637
SYSTEM_CHROME = Path("/opt/google/chrome/chrome")
@@ -100,15 +101,27 @@ def install_agent_browser(cons: Console) -> int:
100101
cons.error("agent-browser binary not found")
101102
return 1
102103

103-
out, err, code = run_cmd(["agent-browser", "install"], timeout=600)
104-
if code:
105-
cons.error(f"agent-browser install failed ({err.strip() or out.strip()})")
104+
def _run_agent(*args: str) -> subprocess.CompletedProcess[str]:
105+
"""Run agent-browser command, streaming stdout/stderr to the user."""
106+
r = subprocess.run(
107+
["agent-browser"] + list(args),
108+
stdout=sys.stdout,
109+
stderr=sys.stderr,
110+
timeout=600,
111+
check=False,
112+
)
113+
return r
114+
115+
result_install = _run_agent("install")
116+
if result_install.returncode != 0:
117+
cons.error("agent-browser install failed")
106118
return 1
107-
cons.info("agent-browser installed successfully")
108119

109-
out, err, code = run_cmd(["agent-browser", "install", "--with-deps"], timeout=600)
110-
if code:
120+
result_deps = _run_agent("install", "--with-deps")
121+
if result_deps.returncode != 0:
111122
cons.warn("Some system dependencies may be missing; Chrome may fail at runtime")
123+
124+
cons.info("agent-browser installed successfully")
112125
return 0
113126

114127

support/install-browser.sh

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)