@@ -6,11 +6,13 @@ use chrono::Utc;
66use serde:: { Deserialize , Serialize } ;
77use std:: collections:: HashMap ;
88use std:: fs;
9- use std:: path:: Path ;
9+ use std:: path:: { Path , PathBuf } ;
1010use uuid:: Uuid ;
1111
12+ use crate :: models:: snapshot:: TriggerSource ;
1213use crate :: models:: { PackageManager , Project , WorkspacePackage } ;
1314use crate :: repositories:: ProjectRepository ;
15+ use crate :: services:: snapshot:: { SnapshotCaptureService , SnapshotStorage } ;
1416use crate :: DatabaseState ;
1517
1618/// Response for scan_project command
@@ -308,14 +310,97 @@ pub async fn scan_project(
308310 } )
309311}
310312
313+ /// Get the snapshot storage base path
314+ fn get_storage_base_path ( ) -> Result < PathBuf , String > {
315+ dirs:: data_dir ( )
316+ . map ( |p| p. join ( "com.packageflow.app" ) . join ( "time-machine" ) )
317+ . ok_or_else ( || "Failed to get data directory" . to_string ( ) )
318+ }
319+
311320/// Save a project to SQLite database
321+ /// Also captures an initial snapshot if the project has a lockfile
312322#[ tauri:: command]
313323pub async fn save_project (
314324 db : tauri:: State < ' _ , DatabaseState > ,
315325 project : Project ,
316326) -> Result < ( ) , String > {
317327 let repo = ProjectRepository :: new ( db. 0 . as_ref ( ) . clone ( ) ) ;
318- repo. save ( & project)
328+ repo. save ( & project) ?;
329+
330+ // Capture initial snapshot in background
331+ let project_path = project. path . clone ( ) ;
332+ let db_clone = db. 0 . as_ref ( ) . clone ( ) ;
333+
334+ // Spawn a background task to capture initial snapshot
335+ tokio:: spawn ( async move {
336+ if let Err ( e) = capture_initial_snapshot ( & project_path, db_clone) . await {
337+ log:: warn!(
338+ "[project] Failed to capture initial snapshot for {}: {}" ,
339+ project_path,
340+ e
341+ ) ;
342+ }
343+ } ) ;
344+
345+ Ok ( ( ) )
346+ }
347+
348+ /// Capture initial snapshot for a newly added project
349+ async fn capture_initial_snapshot (
350+ project_path : & str ,
351+ db : crate :: utils:: database:: Database ,
352+ ) -> Result < ( ) , String > {
353+ let path = Path :: new ( project_path) ;
354+
355+ // Check if project has a lockfile
356+ let has_lockfile = path. join ( "pnpm-lock.yaml" ) . exists ( )
357+ || path. join ( "package-lock.json" ) . exists ( )
358+ || path. join ( "yarn.lock" ) . exists ( )
359+ || path. join ( "bun.lockb" ) . exists ( ) ;
360+
361+ if !has_lockfile {
362+ log:: info!(
363+ "[project] No lockfile found for {}, skipping initial snapshot" ,
364+ project_path
365+ ) ;
366+ return Ok ( ( ) ) ;
367+ }
368+
369+ let base_path = get_storage_base_path ( ) ?;
370+ let project_path_owned = project_path. to_string ( ) ;
371+
372+ // Run in blocking task since snapshot capture involves file I/O
373+ tokio:: task:: spawn_blocking ( move || {
374+ let storage = SnapshotStorage :: new ( base_path) ;
375+ let service = SnapshotCaptureService :: new ( storage, db) ;
376+
377+ let request = crate :: models:: snapshot:: CreateSnapshotRequest {
378+ project_path : project_path_owned. clone ( ) ,
379+ trigger_source : TriggerSource :: Manual , // Initial snapshot
380+ } ;
381+
382+ match service. capture_snapshot ( & request) {
383+ Ok ( snapshot) => {
384+ log:: info!(
385+ "[project] Captured initial snapshot {} for project {} ({} dependencies)" ,
386+ snapshot. id,
387+ project_path_owned,
388+ snapshot. total_dependencies
389+ ) ;
390+ Ok ( ( ) )
391+ }
392+ Err ( e) => {
393+ log:: error!(
394+ "[project] Failed to capture initial snapshot for {}: {}" ,
395+ project_path_owned,
396+ e
397+ ) ;
398+ Err ( e)
399+ }
400+ }
401+ } )
402+ . await
403+ . map_err ( |e| format ! ( "Task join error: {}" , e) ) ?
319404}
320405
321406/// Remove a project from SQLite database
0 commit comments