@@ -106,6 +106,8 @@ pub struct ApplyDiff {
106106 pub in_sync : Vec < String > ,
107107 /// Packages not found in metadata
108108 pub not_found : Vec < String > ,
109+ /// Pending version updates for packages.toml (package_name, version)
110+ pub pending_version_updates : Vec < ( String , String ) > ,
109111}
110112
111113/// Main entry point for the apply command
@@ -131,12 +133,27 @@ pub async fn apply_packages(
131133
132134 display_diff ( & diff, prune) ;
133135
134- if diff. to_install . is_empty ( ) && diff. to_update . is_empty ( ) && diff. to_remove . is_empty ( ) {
136+ let has_package_changes =
137+ !diff. to_install . is_empty ( ) || !diff. to_update . is_empty ( ) || !diff. to_remove . is_empty ( ) ;
138+ let has_toml_updates = !diff. pending_version_updates . is_empty ( ) ;
139+
140+ if !has_package_changes && !has_toml_updates {
135141 info ! ( "\n All packages are in sync!" ) ;
136142 return Ok ( ( ) ) ;
137143 }
138144
139145 if dry_run {
146+ if has_toml_updates {
147+ info ! ( "\n Would update packages.toml:" ) ;
148+ for ( pkg_name, version) in & diff. pending_version_updates {
149+ info ! (
150+ " {} {} -> {}" ,
151+ Colored ( Blue , pkg_name) ,
152+ Colored ( Yellow , "version" ) ,
153+ Colored ( Green , version)
154+ ) ;
155+ }
156+ }
140157 info ! ( "\n {} Dry run - no changes made" , icon_or( "" , "[DRY RUN]" ) ) ;
141158 return Ok ( ( ) ) ;
142159 }
@@ -171,82 +188,186 @@ async fn compute_diff(
171188 // Track declared package
172189 declared_keys. insert ( ( pkg. name . clone ( ) , pkg. pkg_id . clone ( ) , pkg. repo . clone ( ) ) ) ;
173190
174- // Handle GitHub/GitLab release sources
175- if pkg . github . is_some ( ) || pkg. gitlab . is_some ( ) {
176- let source = match ReleaseSource :: from_resolved ( pkg ) {
177- Some ( s ) => s ,
178- None => {
179- diff . not_found . push ( format ! (
180- "{} (missing asset_pattern for github/ gitlab source)" ,
181- pkg . name
182- ) ) ;
183- continue ;
184- }
191+ let is_github_or_gitlab = pkg . github . is_some ( ) || pkg . gitlab . is_some ( ) ;
192+ if is_github_or_gitlab || pkg. url . is_some ( ) {
193+ let local_pkg_id = if is_github_or_gitlab {
194+ pkg . pkg_id . clone ( ) . or_else ( || {
195+ pkg . github
196+ . as_ref ( )
197+ . or ( pkg . gitlab . as_ref ( ) )
198+ . map ( |repo| repo . replace ( '/' , "." ) )
199+ } )
200+ } else {
201+ pkg . pkg_id . clone ( )
185202 } ;
186203
187- // If version is specified, fetch that specific tag; otherwise fetch latest
188- let release = match source. resolve_version ( pkg. version . as_deref ( ) ) {
189- Ok ( r) => r,
190- Err ( e) => {
191- warn ! ( "Failed to resolve release for {}: {}" , pkg. name, e) ;
192- diff. not_found . push ( format ! ( "{} ({})" , pkg. name, e) ) ;
193- continue ;
204+ let installed: Option < InstalledPackage > = diesel_db
205+ . with_conn ( |conn| {
206+ CoreRepository :: list_filtered (
207+ conn,
208+ Some ( "local" ) ,
209+ Some ( & pkg. name ) ,
210+ local_pkg_id. as_deref ( ) ,
211+ None ,
212+ Some ( true ) ,
213+ None ,
214+ Some ( 1 ) ,
215+ None ,
216+ )
217+ } ) ?
218+ . into_iter ( )
219+ . next ( )
220+ . map ( Into :: into) ;
221+
222+ if let Some ( ref cmd) = pkg. version_command {
223+ if let Some ( ref declared) = pkg. version {
224+ let normalized = declared. strip_prefix ( 'v' ) . unwrap_or ( declared) ;
225+ if let Some ( ref existing) = installed {
226+ if existing. version == normalized {
227+ diff. in_sync . push ( format ! ( "{} (local)" , pkg. name) ) ;
228+ continue ;
229+ }
230+ }
194231 }
195- } ;
196232
197- // Use version_command if specified, otherwise use release version
198- let version = if let Some ( ref cmd) = pkg. version_command {
199- match run_version_command ( cmd) {
200- Ok ( v) => v,
233+ let result = match run_version_command ( cmd) {
234+ Ok ( r) => r,
201235 Err ( e) => {
202236 warn ! ( "Failed to run version_command for {}: {}" , pkg. name, e) ;
203- release. version . clone ( )
237+ diff. not_found
238+ . push ( format ! ( "{} (version_command failed: {})" , pkg. name, e) ) ;
239+ continue ;
240+ }
241+ } ;
242+
243+ let version = result
244+ . version
245+ . strip_prefix ( 'v' )
246+ . unwrap_or ( & result. version )
247+ . to_string ( ) ;
248+
249+ if let Some ( ref existing) = installed {
250+ if existing. version == version {
251+ let declared = pkg
252+ . version
253+ . as_ref ( )
254+ . map ( |s| s. strip_prefix ( 'v' ) . unwrap_or ( s) ) ;
255+ if declared != Some ( version. as_str ( ) ) {
256+ // Queue version update for after user confirmation
257+ diff. pending_version_updates
258+ . push ( ( pkg. name . clone ( ) , version. clone ( ) ) ) ;
259+ }
260+ diff. in_sync . push ( format ! ( "{} (local)" , pkg. name) ) ;
261+ continue ;
204262 }
205263 }
206- } else {
207- release. version . clone ( )
208- } ;
209264
210- let version = version. strip_prefix ( 'v' ) . unwrap_or ( & version) . to_string ( ) ;
265+ let mut url_pkg = UrlPackage :: from_remote (
266+ & result. download_url ,
267+ Some ( & pkg. name ) ,
268+ Some ( & version) ,
269+ pkg. pkg_type . as_deref ( ) ,
270+ local_pkg_id. as_deref ( ) ,
271+ ) ?;
272+ url_pkg. size = result. size ;
211273
212- let derived_pkg_id = pkg. pkg_id . clone ( ) . or_else ( || {
213- pkg. github
214- . as_ref ( )
215- . or ( pkg. gitlab . as_ref ( ) )
216- . map ( |repo| repo. replace ( '/' , "." ) )
217- } ) ;
274+ match check_url_package_status ( & url_pkg, pkg, "local" , & diesel_db) ? {
275+ UrlPackageStatus :: ToInstall ( target) => {
276+ diff. to_install . push ( ( pkg. clone ( ) , target) )
277+ }
278+ UrlPackageStatus :: ToUpdate ( target) => {
279+ diff. to_update . push ( ( pkg. clone ( ) , target) )
280+ }
281+ UrlPackageStatus :: InSync ( label) => diff. in_sync . push ( label) ,
282+ }
283+ continue ;
284+ }
285+
286+ if is_github_or_gitlab {
287+ if let Some ( ref declared) = pkg. version {
288+ let normalized = declared. strip_prefix ( 'v' ) . unwrap_or ( declared) ;
289+ if let Some ( ref existing) = installed {
290+ if existing. version == normalized {
291+ diff. in_sync . push ( format ! ( "{} (local)" , pkg. name) ) ;
292+ continue ;
293+ }
294+ }
295+ }
296+
297+ let source = match ReleaseSource :: from_resolved ( pkg) {
298+ Some ( s) => s,
299+ None => {
300+ diff. not_found . push ( format ! (
301+ "{} (missing asset_pattern for github/gitlab source)" ,
302+ pkg. name
303+ ) ) ;
304+ continue ;
305+ }
306+ } ;
307+ let release = match source. resolve_version ( pkg. version . as_deref ( ) ) {
308+ Ok ( r) => r,
309+ Err ( e) => {
310+ warn ! ( "Failed to resolve release for {}: {}" , pkg. name, e) ;
311+ diff. not_found . push ( format ! ( "{} ({})" , pkg. name, e) ) ;
312+ continue ;
313+ }
314+ } ;
315+ let version = release
316+ . version
317+ . strip_prefix ( 'v' )
318+ . unwrap_or ( & release. version )
319+ . to_string ( ) ;
320+
321+ let url_pkg = UrlPackage :: from_remote (
322+ & release. download_url ,
323+ Some ( & pkg. name ) ,
324+ Some ( & version) ,
325+ pkg. pkg_type . as_deref ( ) ,
326+ local_pkg_id. as_deref ( ) ,
327+ ) ?;
218328
219- let url_pkg = UrlPackage :: from_remote (
220- & release. download_url ,
221- Some ( & pkg. name ) ,
222- Some ( & version) ,
223- pkg. pkg_type . as_deref ( ) ,
224- derived_pkg_id. as_deref ( ) ,
225- ) ?;
226-
227- match check_url_package_status ( & url_pkg, pkg, "local" , & diesel_db) ? {
228- UrlPackageStatus :: ToInstall ( target) => diff. to_install . push ( ( pkg. clone ( ) , target) ) ,
229- UrlPackageStatus :: ToUpdate ( target) => diff. to_update . push ( ( pkg. clone ( ) , target) ) ,
230- UrlPackageStatus :: InSync ( label) => diff. in_sync . push ( label) ,
329+ match check_url_package_status ( & url_pkg, pkg, "local" , & diesel_db) ? {
330+ UrlPackageStatus :: ToInstall ( target) => {
331+ diff. to_install . push ( ( pkg. clone ( ) , target) )
332+ }
333+ UrlPackageStatus :: ToUpdate ( target) => {
334+ diff. to_update . push ( ( pkg. clone ( ) , target) )
335+ }
336+ UrlPackageStatus :: InSync ( label) => diff. in_sync . push ( label) ,
337+ }
338+ continue ;
231339 }
232- continue ;
233- }
234340
235- if let Some ( ref url) = pkg. url {
236- let url_pkg = UrlPackage :: from_remote (
237- url,
238- Some ( & pkg. name ) ,
239- pkg. version . as_deref ( ) ,
240- pkg. pkg_type . as_deref ( ) ,
241- pkg. pkg_id . as_deref ( ) ,
242- ) ?;
243-
244- match check_url_package_status ( & url_pkg, pkg, "local" , & diesel_db) ? {
245- UrlPackageStatus :: ToInstall ( target) => diff. to_install . push ( ( pkg. clone ( ) , target) ) ,
246- UrlPackageStatus :: ToUpdate ( target) => diff. to_update . push ( ( pkg. clone ( ) , target) ) ,
247- UrlPackageStatus :: InSync ( label) => diff. in_sync . push ( label) ,
341+ if let Some ( ref url) = pkg. url {
342+ if let Some ( ref declared) = pkg. version {
343+ let normalized = declared. strip_prefix ( 'v' ) . unwrap_or ( declared) ;
344+ if let Some ( ref existing) = installed {
345+ if existing. version == normalized {
346+ diff. in_sync . push ( format ! ( "{} (local)" , pkg. name) ) ;
347+ continue ;
348+ }
349+ }
350+ }
351+
352+ let url_pkg = UrlPackage :: from_remote (
353+ url,
354+ Some ( & pkg. name ) ,
355+ pkg. version . as_deref ( ) ,
356+ pkg. pkg_type . as_deref ( ) ,
357+ pkg. pkg_id . as_deref ( ) ,
358+ ) ?;
359+
360+ match check_url_package_status ( & url_pkg, pkg, "local" , & diesel_db) ? {
361+ UrlPackageStatus :: ToInstall ( target) => {
362+ diff. to_install . push ( ( pkg. clone ( ) , target) )
363+ }
364+ UrlPackageStatus :: ToUpdate ( target) => {
365+ diff. to_update . push ( ( pkg. clone ( ) , target) )
366+ }
367+ UrlPackageStatus :: InSync ( label) => diff. in_sync . push ( label) ,
368+ }
369+ continue ;
248370 }
249- continue ;
250371 }
251372
252373 // Find package in metadata
@@ -586,11 +707,25 @@ async fn execute_apply(state: &AppState, diff: ApplyDiff, no_verify: bool) -> So
586707
587708 let mut version_updates: Vec < ( String , String ) > = Vec :: new ( ) ;
588709
710+ // Apply pending version updates for in-sync packages
711+ for ( pkg_name, version) in & diff. pending_version_updates {
712+ if let Err ( e) = PackagesConfig :: update_package ( pkg_name, None , Some ( version) , None ) {
713+ warn ! (
714+ "Failed to update version for '{}' in packages.toml: {}" ,
715+ pkg_name, e
716+ ) ;
717+ }
718+ }
719+
589720 if !diff. to_install . is_empty ( ) {
590721 info ! ( "\n Installing {} package(s)..." , diff. to_install. len( ) ) ;
591722
592723 for ( pkg, target) in & diff. to_install {
593- if ( pkg. github . is_some ( ) || pkg. gitlab . is_some ( ) ) && pkg. version . is_none ( ) {
724+ let declared_version = pkg
725+ . version
726+ . as_ref ( )
727+ . map ( |v| v. strip_prefix ( 'v' ) . unwrap_or ( v) ) ;
728+ if declared_version != Some ( target. package . version . as_str ( ) ) {
594729 version_updates. push ( ( pkg. name . clone ( ) , target. package . version . clone ( ) ) ) ;
595730 }
596731 }
@@ -633,6 +768,17 @@ async fn execute_apply(state: &AppState, diff: ApplyDiff, no_verify: bool) -> So
633768 if !diff. to_update . is_empty ( ) {
634769 info ! ( "\n Updating {} package(s)..." , diff. to_update. len( ) ) ;
635770
771+ let mut update_version_updates: Vec < ( String , String ) > = Vec :: new ( ) ;
772+ for ( pkg, target) in & diff. to_update {
773+ let declared_version = pkg
774+ . version
775+ . as_ref ( )
776+ . map ( |v| v. strip_prefix ( 'v' ) . unwrap_or ( v) ) ;
777+ if declared_version != Some ( target. package . version . as_str ( ) ) {
778+ update_version_updates. push ( ( pkg. name . clone ( ) , target. package . version . clone ( ) ) ) ;
779+ }
780+ }
781+
636782 let targets: Vec < InstallTarget > = diff
637783 . to_update
638784 . into_iter ( )
@@ -654,6 +800,18 @@ async fn execute_apply(state: &AppState, diff: ApplyDiff, no_verify: bool) -> So
654800 perform_update ( ctx. clone ( ) , targets, diesel_db. clone ( ) , false ) . await ?;
655801 updated_count = ctx. installed_count . load ( Ordering :: Relaxed ) as usize ;
656802 failed_count += ctx. failed . load ( Ordering :: Relaxed ) as usize ;
803+
804+ if updated_count > 0 {
805+ for ( pkg_name, version) in & update_version_updates {
806+ if let Err ( e) = PackagesConfig :: update_package ( pkg_name, None , Some ( version) , None )
807+ {
808+ warn ! (
809+ "Failed to update version for '{}' in packages.toml: {}" ,
810+ pkg_name, e
811+ ) ;
812+ }
813+ }
814+ }
657815 }
658816
659817 if !diff. to_remove . is_empty ( ) {
0 commit comments