@@ -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"\n Generated { 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
759884def 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 )
0 commit comments