Skip to content

Commit 46f1996

Browse files
committed
feat: settings.json template + version-aware extension sync
Template-driven settings.json generation: - start.sh: generate settings.json from template on first boot with LPB_VERSION pins, no model/provider (user configures later) lpb-config enhancements: - workspace sync --extensions: sync pins to LPB_VERSION - validate: check pins against LPB_VERSION (not GitHub tags) - workspace ensure --fix: generate settings.json from template - _read_settings/_write_settings/_get_pinned_versions helpers - LPB_EXTENSION_REPOS constant for consistent repo list Pipeline-aware: dev uses 0.0.X-lpb-dev, main uses 0.0.X-lpb
1 parent efde33c commit 46f1996

2 files changed

Lines changed: 171 additions & 25 deletions

File tree

support/lpb-config.py

Lines changed: 155 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,134 @@ def cmd_workspace_ensure(pipeline: str, *, fix: bool = False, cons: Console) ->
751751
cons.info("")
752752
cons.info("Run 'lpb-config workspace ensure --fix' to auto-fix.")
753753

754+
# ── Generate settings.json from template if missing ────────────────
755+
if fix:
756+
agent_dir = Path(DEFAULT_AGENT_DIR)
757+
template = agent_dir / "settings.json.template"
758+
settings_file = agent_dir / "settings.json"
759+
if template.is_file() and not settings_file.is_file():
760+
version = get_version()
761+
if pipeline == "main":
762+
version = version.replace("-dev", "")
763+
content = template.read_text()
764+
content = content.replace("__LPB_VERSION__", version)
765+
settings_file.write_text(content)
766+
cons.info(f"\nGenerated {settings_file} from template (version: {version})")
767+
754768
return 0 if all_aligned else 1
755769

756770

771+
# ─── Settings.json helpers ──────────────────────────────────────────────
772+
773+
LPB_EXTENSION_REPOS = ["lemonade-pi-plugin", "lpb-memory", "pi-subagents"]
774+
775+
776+
def _read_settings(agent_dir: str | Path) -> dict | None:
777+
"""Read settings.json from agent dir."""
778+
path = Path(agent_dir) / "settings.json"
779+
if not path.is_file():
780+
return None
781+
try:
782+
with open(path) as f:
783+
return json.load(f)
784+
except (json.JSONDecodeError, OSError):
785+
return None
786+
787+
788+
def _write_settings(agent_dir: str | Path, settings: dict) -> None:
789+
"""Write settings.json to agent dir."""
790+
path = Path(agent_dir) / "settings.json"
791+
with open(path, "w") as f:
792+
json.dump(settings, f, indent=2)
793+
f.write("\n")
794+
795+
796+
def _get_pinned_versions(settings: dict) -> dict[str, str]:
797+
"""Extract version pins for LPB extension repos from settings."""
798+
pins: dict[str, str] = {}
799+
for pkg in settings.get("packages", []):
800+
if not isinstance(pkg, str):
801+
continue
802+
for name in LPB_EXTENSION_REPOS:
803+
marker = f"localpibox/{name}@"
804+
if marker in pkg:
805+
pins[name] = pkg.split("@")[-1]
806+
return pins
807+
808+
809+
def _update_pinned_versions(settings: dict, target_version: str) -> list[tuple[str, str, str]]:
810+
"""Update LPB extension pins to target_version. Returns list of changes."""
811+
packages = settings.get("packages", [])
812+
changes: list[tuple[str, str, str]] = []
813+
for name in LPB_EXTENSION_REPOS:
814+
marker = f"git:github.com/localpibox/{name}@"
815+
for i, pkg in enumerate(packages):
816+
if isinstance(pkg, str) and pkg.startswith(marker):
817+
old_tag = pkg.split("@")[-1]
818+
if old_tag != target_version:
819+
new_pkg = f"{marker}{target_version}"
820+
packages[i] = new_pkg
821+
changes.append((name, old_tag, target_version))
822+
settings["packages"] = packages
823+
return changes
824+
825+
826+
# ─── Workspace: sync extensions ───────────────────────────────────────────
827+
828+
def cmd_workspace_sync_extensions(pipeline: str, cons: Console) -> int:
829+
"""Sync settings.json extension pins to match current LPB_VERSION."""
830+
agent_dir = Path(DEFAULT_AGENT_DIR)
831+
settings = _read_settings(agent_dir)
832+
833+
if settings is None:
834+
cons.error(f"settings.json not found: {agent_dir}")
835+
cons.info(" Run 'lpb-config workspace ensure' to generate from template.")
836+
return 1
837+
838+
# Determine target version
839+
version = get_version()
840+
# For main pipeline, strip -dev from version
841+
if pipeline == "main":
842+
target_version = version.replace("-dev", "")
843+
else:
844+
target_version = version
845+
846+
current_pins = _get_pinned_versions(settings)
847+
848+
cons.info(f"Pipeline: {pipeline}")
849+
cons.info(f"LPB_VERSION: {version}")
850+
cons.info(f"Target pins: {target_version}")
851+
cons.info("")
852+
853+
# Check mismatches
854+
mismatches = []
855+
for name in LPB_EXTENSION_REPOS:
856+
cur = current_pins.get(name, "(unpinned)")
857+
if cur != target_version:
858+
mismatches.append((name, cur, target_version))
859+
860+
if not mismatches:
861+
cons.info("All extension pins already match LPB_VERSION.")
862+
return 0
863+
864+
cons.warn(f"Version mismatch ({len(mismatches)} extension(s)):")
865+
for name, cur, target in mismatches:
866+
cons.warn(f" {name}: {cur}{target}")
867+
868+
cons.info("")
869+
if confirm("Update settings.json extension pins?"):
870+
changes = _update_pinned_versions(settings, target_version)
871+
_write_settings(agent_dir, settings)
872+
for name, old, new in changes:
873+
cons.info(f" {name}: {old}{new}")
874+
cons.info("")
875+
cons.done("Extension pins updated. Run 'pi update --extensions' to apply.")
876+
else:
877+
cons.info("Skipped. Run 'lpb-config workspace sync --extensions' when ready.")
878+
879+
return 0 if not mismatches else 1
880+
881+
757882
# ─── Validate command ─────────────────────────────────────────────────────
758883

759884
def cmd_validate(pipeline: str, cons: Console) -> int:
@@ -909,36 +1034,36 @@ def check(label: str, condition: bool, detail: str = "", fix: str = "") -> None:
9091034
cons.info("")
9101035
cons.info(" Extension pins:")
9111036

912-
settings_path = config_path / "settings.json"
913-
if settings_path.is_file():
914-
with open(settings_path) as f:
915-
settings = json.load(f)
1037+
# Determine target version for this pipeline
1038+
target_version = version
1039+
if pipeline == "main":
1040+
target_version = version.replace("-dev", "")
9161041

917-
packages = settings.get("packages", [])
918-
lpb_repos = ["lemonade-pi-plugin", "lpb-memory", "pi-subagents"]
919-
920-
for pkg_name in lpb_repos:
921-
pinned_tag = None
922-
for pkg in packages:
923-
if isinstance(pkg, str) and f"localpibox/{pkg_name}@" in pkg:
924-
pinned_tag = pkg.split("@")[-1]
925-
break
1042+
settings_path = config_path / "settings.json"
1043+
settings = _read_settings(config_path)
1044+
if settings:
1045+
current_pins = _get_pinned_versions(settings)
9261046

1047+
for pkg_name in LPB_EXTENSION_REPOS:
1048+
pinned_tag = current_pins.get(pkg_name)
9271049
if pinned_tag:
928-
# Check if the tag matches or is newer than current version
929-
tag_matches_version = pinned_tag == version or version.startswith(pinned_tag.split("-lpb")[0] + "-lpb")
930-
# Also accept if tag is a valid version format
931-
is_version_tag = bool(re.match(r"^\d+\.\d+\.\d+-lpb", pinned_tag))
932-
check(
933-
f" {pkg_name} pinned",
934-
True,
935-
f"@{pinned_tag}",
936-
f"lpb-config align" if not tag_matches_version else "",
937-
)
1050+
if pinned_tag == target_version:
1051+
check(
1052+
f" {pkg_name} pinned",
1053+
True,
1054+
f"@{pinned_tag} (matches VERSION)",
1055+
)
1056+
else:
1057+
check(
1058+
f" {pkg_name} pinned",
1059+
False,
1060+
f"@{pinned_tag} (expected: {target_version})",
1061+
"lpb-config workspace sync --extensions",
1062+
)
9381063
else:
9391064
check(f" {pkg_name} pinned", False,
9401065
"not found in settings.json",
941-
"lpb-config align")
1066+
"lpb-config workspace sync --extensions")
9421067
else:
9431068
check("settings.json exists", False,
9441069
f"{settings_path} not found",
@@ -1041,7 +1166,9 @@ def main(argv: list[str] | None = None) -> int:
10411166
p_ws = sub.add_parser("workspace", help="Manage workspace repos")
10421167
ws_sub = p_ws.add_subparsers(dest="workspace_command")
10431168
_add_subparser(ws_sub, "status", "Show workspace repo branches + alignment")
1044-
_add_subparser(ws_sub, "sync", "Create symlinks + git pull current branches")
1169+
p_ws_sync = _add_subparser(ws_sub, "sync", "Create symlinks + git pull current branches")
1170+
p_ws_sync.add_argument("--extensions", action="store_true",
1171+
help="sync settings.json extension pins to LPB_VERSION")
10451172
p_ws_ensure = _add_subparser(ws_sub, "ensure", "Switch repos to correct branches for pipeline")
10461173
p_ws_ensure.add_argument("--fix", action="store_true", help="auto-fix misaligned repos")
10471174

@@ -1079,6 +1206,9 @@ def main(argv: list[str] | None = None) -> int:
10791206
if args.workspace_command == "status":
10801207
return cmd_workspace_status(pipeline, cons)
10811208
if args.workspace_command == "sync":
1209+
sync_ext = getattr(args, "extensions", False)
1210+
if sync_ext:
1211+
return cmd_workspace_sync_extensions(pipeline, cons)
10821212
return cmd_workspace_sync(pipeline, cons)
10831213
if args.workspace_command == "ensure":
10841214
fix = getattr(args, "fix", False)

support/start.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,22 @@ if [[ "$FIRST_RUN" = "true" ]]; then
350350
printf 'allow-scripts=better-sqlite3\nallow-scripts=agent-browser\nallow-scripts=esbuild\nallow-scripts=protobufjs\nallow-scripts=@google/genai\n' > "${AGENT_DIR}/git/.npmrc" 2>/dev/null || true
351351
printf 'allow-scripts=better-sqlite3\nallow-scripts=agent-browser\nallow-scripts=esbuild\nallow-scripts=protobufjs\nallow-scripts=@google/genai\n' > "${HOME_DIR}/.npmrc" 2>/dev/null || true
352352

353+
# ── Generate settings.json from template (first boot only) ───────
354+
# Template is in the config repo; generated file is persisted on host volume.
355+
# No model/provider — user will configure via /login after first boot.
356+
_lpb_version="${LPB_VERSION:-0.0.0-lpb}"
357+
_settings_template="${AGENT_DIR}/settings.json.template"
358+
_settings_file="${AGENT_DIR}/settings.json"
359+
if [[ -f "${_settings_template}" && ! -f "${_settings_file}" ]]; then
360+
info "Generating settings.json from template..."
361+
# Replace __LPB_VERSION__ placeholder with actual version
362+
sed "s/__LPB_VERSION__/${_lpb_version}/g" "${_settings_template}" > "${_settings_file}"
363+
info " settings.json generated (version: ${_lpb_version})"
364+
info " No model configured — run '/login lemonade' to set your model"
365+
elif [[ ! -f "${_settings_template}" ]]; then
366+
warn "settings.json.template not found — Pi will use defaults"
367+
fi
368+
353369
# ── Config repo: clone/fetch into ~/.pi/agent/ (runs every boot — see §4a)
354370
touch "${HOME_DIR}/.pi/.initialized"
355371

0 commit comments

Comments
 (0)