@@ -355,6 +355,8 @@ def _run_uv_sync(
355355 repos : dict ,
356356 ws_root : str ,
357357 type_filter : set [str ] | None = None ,
358+ * ,
359+ upgrade_deps : bool = False ,
358360) -> None :
359361 """Run ``uv sync`` in selected repos and projects.
360362
@@ -366,11 +368,18 @@ def _run_uv_sync(
366368 Absolute path to the workspace root.
367369 type_filter:
368370 If provided, only sync repos whose *type* is in this set.
371+ upgrade_deps:
372+ When True, actively upgrade ``streamtex`` in the lock (legacy
373+ behaviour). When False (default), pass ``--locked`` to ``uv sync``
374+ so the lock is never modified — deterministic sync to the committed
375+ state. The ``--no-sources`` fallback always drops ``--locked``
376+ because it intentionally rewrites the lock to PyPI mode.
369377 """
370378 uv = _find_uv ()
371379 console = get_console ()
372380 synced = 0
373381 skipped = 0
382+ needs_upgrade_hint = False
374383
375384 targets = _collect_sync_targets (repos , ws_root , type_filter )
376385
@@ -379,8 +388,7 @@ def _run_uv_sync(
379388 return
380389
381390 for target_name , target_path in targets :
382- # Upgrade streamtex in the lock file first (skip for the library itself)
383- if _depends_on_streamtex (target_path ):
391+ if upgrade_deps and _depends_on_streamtex (target_path ):
384392 no_src = _has_missing_local_sources (target_path )
385393 lock_cmd = [uv , "lock" , "--upgrade-package" , "streamtex" ]
386394 if no_src :
@@ -399,13 +407,17 @@ def _run_uv_sync(
399407 skipped += 1
400408 continue
401409
402- # If local editable sources are missing, fall back to PyPI
410+ no_sources = _has_missing_local_sources ( target_path )
403411 cmd = [uv , "sync" ]
404- if _has_missing_local_sources ( target_path ) :
412+ if no_sources :
405413 cmd .append ("--no-sources" )
406414 console .print (f" [cyan]{ target_name } [/cyan]: running uv sync --no-sources (editable source not found) …" )
415+ elif upgrade_deps :
416+ console .print (f" [cyan]{ target_name } [/cyan]: running uv sync (upgrade-deps) …" )
407417 else :
408- console .print (f" [cyan]{ target_name } [/cyan]: running uv sync …" )
418+ cmd .append ("--locked" )
419+ console .print (f" [cyan]{ target_name } [/cyan]: running uv sync --locked …" )
420+
409421 result = subprocess .run (
410422 cmd ,
411423 cwd = target_path ,
@@ -416,17 +428,28 @@ def _run_uv_sync(
416428 if result .returncode == 0 :
417429 console .print (f" [green]{ target_name } [/green]: ok" )
418430 synced += 1
419- # --no-sources rewrites uv.lock (local paths → PyPI).
420- # Restore the committed version so the repo stays clean.
421- if "--no-sources" in cmd :
431+ if no_sources :
422432 _restore_uv_lock_if_only_dirty (target_path )
423433 else :
424- console .print (f" [red]{ target_name } [/red]: failed" )
425- if result .stderr :
426- console .print (f" { result .stderr .strip ()} " )
434+ stderr_text = (result .stderr or "" ).strip ()
435+ if "--locked" in cmd and ("lock" in stderr_text .lower () or "out of date" in stderr_text .lower ()):
436+ console .print (
437+ f" [yellow]{ target_name } [/yellow]: lock out of date "
438+ "— re-run with `stx update --upgrade-deps`"
439+ )
440+ needs_upgrade_hint = True
441+ else :
442+ console .print (f" [red]{ target_name } [/red]: failed" )
443+ if stderr_text :
444+ console .print (f" { stderr_text } " )
427445 skipped += 1
428446
429447 console .print (f"\n [bold]Done:[/bold] { synced } synced, { skipped } skipped" )
448+ if needs_upgrade_hint :
449+ console .print (
450+ "[dim]Hint: `stx update --upgrade-deps` refreshes the lock when "
451+ "pyproject.toml has new constraints or new streamtex versions exist.[/dim]"
452+ )
430453
431454
432455# ---------------------------------------------------------------------------
@@ -838,7 +861,13 @@ def _upgrade_cli_tool(
838861@click .option ("--dry-run" , is_flag = True , help = "Show steps without executing." )
839862@click .option ("--repair" , is_flag = True , help = "Run repair checks (broken venv, missing __init__.py)." )
840863@click .option ("--force" , is_flag = True , help = "Overwrite locally modified profile files (backup in .claude/.backup/)." )
841- def update (skip_sync , skip_profiles , dry_run , repair , force ):
864+ @click .option (
865+ "--upgrade-deps" ,
866+ is_flag = True ,
867+ help = "Allow uv to refresh uv.lock (upgrade streamtex + apply pyproject changes). "
868+ "Default is --locked: deterministic sync to the committed lock state." ,
869+ )
870+ def update (skip_sync , skip_profiles , dry_run , repair , force , upgrade_deps ):
842871 """Pull repos, clone missing, sync deps, install hooks, update profiles."""
843872 ws_root , config = _require_workspace ()
844873 repos = config .get ("repos" , {})
@@ -995,10 +1024,13 @@ def _step(label: str) -> None:
9951024 _step ("Syncing dependencies …" )
9961025 if dry_run :
9971026 for target_name , target_path in _collect_sync_targets (repos , ws_root ):
998- upgrade = " (+ upgrade streamtex)" if _depends_on_streamtex (target_path ) else ""
999- console .print (f" [cyan]{ target_name } [/cyan]: would uv sync{ upgrade } " )
1027+ if upgrade_deps :
1028+ upgrade = " (+ upgrade streamtex)" if _depends_on_streamtex (target_path ) else ""
1029+ console .print (f" [cyan]{ target_name } [/cyan]: would uv sync{ upgrade } " )
1030+ else :
1031+ console .print (f" [cyan]{ target_name } [/cyan]: would uv sync --locked" )
10001032 else :
1001- _run_uv_sync (repos , ws_root )
1033+ _run_uv_sync (repos , ws_root , upgrade_deps = upgrade_deps )
10021034
10031035 # --- project migrations ---
10041036 if not skip_sync :
0 commit comments