@@ -92,6 +92,67 @@ pub(crate) fn list(ctx: &AppContext) -> Result<()> {
9292 Ok ( ( ) )
9393}
9494
95+ /// The infrastructure a usable v3 client needs: stdlib is the foundation
96+ /// every module builds on, and the store is how a user installs anything else
97+ /// without the CLI. Not `manager`, which the store supersedes.
98+ const SYSTEM_MODULES : & [ & str ] = & [ "stdlib" , "store" ] ;
99+
100+ /// Which system modules are not present on disk. Absence is the signal: a
101+ /// disabled module keeps its directory, so this never re-seeds one the user
102+ /// turned off, and a deliberate `pkg delete` of stdlib or store leaves a
103+ /// client that cannot manage itself, so bringing it back is recovery.
104+ fn missing_system_modules ( modules_root : & Path ) -> Vec < & ' static str > {
105+ SYSTEM_MODULES . iter ( ) . copied ( ) . filter ( |id| !modules_root. join ( id) . exists ( ) ) . collect ( )
106+ }
107+
108+ /// Installs and enables any absent system module from the registry, so a fresh
109+ /// `apply` produces a client that can manage itself rather than a patched
110+ /// Spotify with no stdlib and no store.
111+ ///
112+ /// Best-effort by design, like the classmap fetch beside it: an unreachable
113+ /// vault or a failed download warns and leaves the rest of the apply to
114+ /// proceed, and a module already on disk is left alone for the store to
115+ /// update. Disk-backed on purpose, so the store's later updates (which shadow
116+ /// it through localStorage) always have a working version to fall back to.
117+ pub ( crate ) fn ensure_system_modules ( ctx : & AppContext ) {
118+ let modules_root = crate :: module:: modules_dir ( & ctx. config_root ) ;
119+ let missing = missing_system_modules ( & modules_root) ;
120+ if missing. is_empty ( ) {
121+ return ;
122+ }
123+ let vault = match fetch_vault ( DEFAULT_VAULT ) {
124+ Ok ( vault) => vault,
125+ Err ( e) => {
126+ tracing:: warn!( "cannot seed system modules ({}): {e}" , missing. join( ", " ) ) ;
127+ return ;
128+ }
129+ } ;
130+ for id in missing {
131+ if let Err ( e) = seed_system_module ( ctx, & vault, id) {
132+ tracing:: warn!( "could not seed system module {id}: {e}" ) ;
133+ }
134+ }
135+ }
136+
137+ fn seed_system_module ( ctx : & AppContext , vault : & Vault , id : & str ) -> Result < ( ) > {
138+ let module = vault. modules . get ( id) . ok_or_else ( || anyhow:: anyhow!( "not in the registry" ) ) ?;
139+ let version = resolve_version ( module) ?;
140+ let entry = module. v . get ( & version) . ok_or_else ( || anyhow:: anyhow!( "{version} is not in the registry" ) ) ?;
141+ if entry. artifacts . is_empty ( ) {
142+ anyhow:: bail!( "{id}@{version} has no artifact" ) ;
143+ }
144+ let tag = format ! ( "{id}@{version}" ) ;
145+ crate :: module:: install_from_vault (
146+ & ctx. config_root ,
147+ & tag,
148+ entry. artifacts . clone ( ) ,
149+ entry. checksum . clone ( ) ,
150+ ) ?;
151+ crate :: module:: enable_module ( & ctx. config_root , & tag) ?;
152+ tracing:: info!( "seeded system module {tag}" ) ;
153+ Ok ( ( ) )
154+ }
155+
95156/// The checksum the registry recorded for `id@version`, if it carries that
96157/// version at all.
97158///
@@ -175,4 +236,21 @@ mod tests {
175236 fn rejects_an_empty_vault_entry ( ) {
176237 assert ! ( resolve_version( & module( "" , & [ ] ) ) . is_err( ) ) ;
177238 }
239+
240+ #[ test]
241+ fn seeds_only_the_system_modules_that_are_absent ( ) {
242+ let dir = std:: env:: temp_dir ( ) . join ( format ! ( "spicetify-seed-{}" , std:: process:: id( ) ) ) ;
243+ let _ = std:: fs:: remove_dir_all ( & dir) ;
244+ std:: fs:: create_dir_all ( & dir) . expect ( "temp dir" ) ;
245+
246+ assert_eq ! ( missing_system_modules( & dir) , vec![ "stdlib" , "store" ] , "a fresh config needs both" ) ;
247+
248+ std:: fs:: create_dir_all ( dir. join ( "stdlib" ) ) . expect ( "stdlib dir" ) ;
249+ assert_eq ! ( missing_system_modules( & dir) , vec![ "store" ] , "an installed module is left alone" ) ;
250+
251+ std:: fs:: create_dir_all ( dir. join ( "store" ) ) . expect ( "store dir" ) ;
252+ assert ! ( missing_system_modules( & dir) . is_empty( ) , "nothing to seed once both exist" ) ;
253+
254+ std:: fs:: remove_dir_all ( & dir) . expect ( "cleanup" ) ;
255+ }
178256}
0 commit comments