@@ -11,6 +11,47 @@ pub enum SetupTarget {
1111 Both ,
1212}
1313
14+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
15+ pub struct EmbeddedAsset {
16+ pub relative_path : & ' static str ,
17+ pub bytes : & ' static [ u8 ] ,
18+ }
19+
20+ include ! ( concat!( env!( "OUT_DIR" ) , "/setup_embedded_assets.rs" ) ) ;
21+
22+ pub enum EmbeddedAssetSelectionIter {
23+ One ( std:: slice:: Iter < ' static , EmbeddedAsset > ) ,
24+ Both (
25+ std:: iter:: Chain <
26+ std:: slice:: Iter < ' static , EmbeddedAsset > ,
27+ std:: slice:: Iter < ' static , EmbeddedAsset > ,
28+ > ,
29+ ) ,
30+ }
31+
32+ impl Iterator for EmbeddedAssetSelectionIter {
33+ type Item = & ' static EmbeddedAsset ;
34+
35+ fn next ( & mut self ) -> Option < Self :: Item > {
36+ match self {
37+ Self :: One ( iter) => iter. next ( ) ,
38+ Self :: Both ( iter) => iter. next ( ) ,
39+ }
40+ }
41+ }
42+
43+ pub fn iter_embedded_assets_for_setup_target ( target : SetupTarget ) -> EmbeddedAssetSelectionIter {
44+ match target {
45+ SetupTarget :: OpenCode => EmbeddedAssetSelectionIter :: One ( OPENCODE_EMBEDDED_ASSETS . iter ( ) ) ,
46+ SetupTarget :: Claude => EmbeddedAssetSelectionIter :: One ( CLAUDE_EMBEDDED_ASSETS . iter ( ) ) ,
47+ SetupTarget :: Both => EmbeddedAssetSelectionIter :: Both (
48+ OPENCODE_EMBEDDED_ASSETS
49+ . iter ( )
50+ . chain ( CLAUDE_EMBEDDED_ASSETS . iter ( ) ) ,
51+ ) ,
52+ }
53+ }
54+
1455#[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
1556pub enum SetupMode {
1657 Interactive ,
@@ -78,9 +119,15 @@ pub fn run_placeholder_setup_for_mode(mode: SetupMode) -> Result<String> {
78119 SetupMode :: NonInteractive ( SetupTarget :: Both ) => "--both" ,
79120 } ;
80121
122+ let selected_target = match mode {
123+ SetupMode :: Interactive => SetupTarget :: Both ,
124+ SetupMode :: NonInteractive ( target) => target,
125+ } ;
126+ let embedded_asset_count = iter_embedded_assets_for_setup_target ( selected_target) . count ( ) ;
127+
81128 Ok ( format ! (
82- "TODO: '{NAME}' is planned and not implemented yet. Setup mode '{mode_label}' accepted; setup plan scaffolded with {} deferred step(s)." ,
83- plan. tasks. len( ) ,
129+ "TODO: '{NAME}' is planned and not implemented yet. Setup mode '{mode_label}' accepted; setup plan scaffolded with {} deferred step(s). Embedded asset manifest is ready with {embedded_asset_count} file(s). " ,
130+ plan. tasks. len( )
84131 ) )
85132}
86133
@@ -223,12 +270,18 @@ pub fn resolve_setup_mode(options: SetupCliOptions) -> Result<SetupMode> {
223270
224271#[ cfg( test) ]
225272mod tests {
273+ use std:: {
274+ fs,
275+ path:: { Path , PathBuf } ,
276+ } ;
277+
226278 use anyhow:: Result ;
227279
228280 use super :: {
229- parse_setup_cli_options, resolve_setup_dispatch, resolve_setup_mode,
230- run_placeholder_setup_for_mode, setup_usage_text, PlaceholderSetupService , SetupCliOptions ,
231- SetupDispatch , SetupMode , SetupRequest , SetupService , SetupTarget ,
281+ iter_embedded_assets_for_setup_target, parse_setup_cli_options, resolve_setup_dispatch,
282+ resolve_setup_mode, run_placeholder_setup_for_mode, setup_usage_text,
283+ PlaceholderSetupService , SetupCliOptions , SetupDispatch , SetupMode , SetupRequest ,
284+ SetupService , SetupTarget ,
232285 } ;
233286
234287 #[ derive( Clone , Copy , Debug ) ]
@@ -341,4 +394,133 @@ mod tests {
341394 assert_eq ! ( dispatch, SetupDispatch :: Cancelled ) ;
342395 Ok ( ( ) )
343396 }
397+
398+ #[ test]
399+ fn embedded_manifest_paths_are_sorted_and_normalized ( ) {
400+ for target in [ SetupTarget :: OpenCode , SetupTarget :: Claude ] {
401+ let assets = assets_for_target ( target) ;
402+
403+ assert ! ( !assets. is_empty( ) , "embedded asset set should not be empty" ) ;
404+
405+ let paths: Vec < & str > = assets. iter ( ) . map ( |asset| asset. relative_path ) . collect ( ) ;
406+ assert_eq ! ( paths. len( ) , assets. len( ) ) ;
407+
408+ for asset in assets {
409+ assert ! ( !asset. relative_path. is_empty( ) ) ;
410+ assert ! ( !asset. relative_path. starts_with( '/' ) ) ;
411+ assert ! ( !asset. relative_path. contains( '\\' ) ) ;
412+ assert ! ( !asset. relative_path. starts_with( "config/" ) ) ;
413+ assert ! (
414+ !asset. bytes. is_empty( ) ,
415+ "embedded files should have content bytes"
416+ ) ;
417+ }
418+
419+ let mut sorted = paths. clone ( ) ;
420+ sorted. sort_unstable ( ) ;
421+ assert_eq ! (
422+ paths, sorted,
423+ "embedded paths should be deterministic and sorted"
424+ ) ;
425+ }
426+ }
427+
428+ #[ test]
429+ fn embedded_manifest_matches_runtime_config_tree ( ) -> Result < ( ) > {
430+ let opencode_expected =
431+ collect_runtime_relative_paths ( runtime_target_root ( SetupTarget :: OpenCode ) ) ?;
432+ let claude_expected =
433+ collect_runtime_relative_paths ( runtime_target_root ( SetupTarget :: Claude ) ) ?;
434+
435+ let opencode_actual: Vec < String > = assets_for_target ( SetupTarget :: OpenCode )
436+ . iter ( )
437+ . map ( |asset| asset. relative_path . to_string ( ) )
438+ . collect ( ) ;
439+ let claude_actual: Vec < String > = assets_for_target ( SetupTarget :: Claude )
440+ . iter ( )
441+ . map ( |asset| asset. relative_path . to_string ( ) )
442+ . collect ( ) ;
443+
444+ assert_eq ! ( opencode_actual, opencode_expected) ;
445+ assert_eq ! ( claude_actual, claude_expected) ;
446+ Ok ( ( ) )
447+ }
448+
449+ #[ test]
450+ fn embedded_setup_target_iterator_scopes_assets_per_target ( ) {
451+ let opencode_count = assets_for_target ( SetupTarget :: OpenCode ) . len ( ) ;
452+ let claude_count = assets_for_target ( SetupTarget :: Claude ) . len ( ) ;
453+
454+ let iter_opencode_count =
455+ iter_embedded_assets_for_setup_target ( SetupTarget :: OpenCode ) . count ( ) ;
456+ let iter_claude_count = iter_embedded_assets_for_setup_target ( SetupTarget :: Claude ) . count ( ) ;
457+ let iter_both_count = iter_embedded_assets_for_setup_target ( SetupTarget :: Both ) . count ( ) ;
458+
459+ assert_eq ! ( iter_opencode_count, opencode_count) ;
460+ assert_eq ! ( iter_claude_count, claude_count) ;
461+ assert_eq ! ( iter_both_count, opencode_count + claude_count) ;
462+ }
463+
464+ fn runtime_target_root ( target : SetupTarget ) -> PathBuf {
465+ let target_relative = match target {
466+ SetupTarget :: OpenCode => "config/.opencode" ,
467+ SetupTarget :: Claude => "config/.claude" ,
468+ SetupTarget :: Both => unreachable ! ( "both is not a concrete filesystem root" ) ,
469+ } ;
470+
471+ PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) )
472+ . parent ( )
473+ . expect ( "cli crate should be nested under repository root" )
474+ . join ( target_relative)
475+ }
476+
477+ fn assets_for_target ( target : SetupTarget ) -> & ' static [ super :: EmbeddedAsset ] {
478+ match target {
479+ SetupTarget :: OpenCode => super :: OPENCODE_EMBEDDED_ASSETS ,
480+ SetupTarget :: Claude => super :: CLAUDE_EMBEDDED_ASSETS ,
481+ SetupTarget :: Both => unreachable ! ( "both is not a single embedded target" ) ,
482+ }
483+ }
484+
485+ fn collect_runtime_relative_paths ( root : PathBuf ) -> Result < Vec < String > > {
486+ let mut files = Vec :: new ( ) ;
487+ collect_runtime_files ( & root, & root, & mut files) ?;
488+
489+ files. sort_unstable ( ) ;
490+
491+ let stable_paths = files
492+ . into_iter ( )
493+ . map ( |path| {
494+ path. to_str ( )
495+ . expect ( "runtime config path should be UTF-8" )
496+ . replace ( '\\' , "/" )
497+ } )
498+ . collect ( ) ;
499+
500+ Ok ( stable_paths)
501+ }
502+
503+ fn collect_runtime_files (
504+ base_root : & Path ,
505+ current_dir : & Path ,
506+ output : & mut Vec < PathBuf > ,
507+ ) -> Result < ( ) > {
508+ for entry in fs:: read_dir ( current_dir) ? {
509+ let entry = entry?;
510+ let path = entry. path ( ) ;
511+
512+ if entry. file_type ( ) ?. is_dir ( ) {
513+ collect_runtime_files ( base_root, & path, output) ?;
514+ continue ;
515+ }
516+
517+ let relative = path
518+ . strip_prefix ( base_root)
519+ . expect ( "relative path should be under root" )
520+ . to_path_buf ( ) ;
521+ output. push ( relative) ;
522+ }
523+
524+ Ok ( ( ) )
525+ }
344526}
0 commit comments