@@ -872,20 +872,6 @@ fn hook_already_present(root: &serde_json::Value, hook_command: &str) -> bool {
872872}
873873
874874/// Default mode: hook + slim RTK.md + @RTK.md reference
875- #[ cfg( not( unix) ) ]
876- fn run_default_mode (
877- _global : bool ,
878- _patch_mode : PatchMode ,
879- _verbose : u8 ,
880- _install_opencode : bool ,
881- ) -> Result < ( ) > {
882- eprintln ! ( "[warn] Hook-based mode requires Unix (macOS/Linux)." ) ;
883- eprintln ! ( " Windows: use --claude-md mode for full injection." ) ;
884- eprintln ! ( " Falling back to --claude-md mode." ) ;
885- run_claude_md_mode ( _global, _verbose, _install_opencode)
886- }
887-
888- #[ cfg( unix) ]
889875fn run_default_mode (
890876 global : bool ,
891877 patch_mode : PatchMode ,
@@ -1127,17 +1113,6 @@ fn generate_global_filters_template(verbose: u8) -> Result<()> {
11271113}
11281114
11291115/// Hook-only mode: just the hook, no RTK.md
1130- #[ cfg( not( unix) ) ]
1131- fn run_hook_only_mode (
1132- _global : bool ,
1133- _patch_mode : PatchMode ,
1134- _verbose : u8 ,
1135- _install_opencode : bool ,
1136- ) -> Result < ( ) > {
1137- anyhow:: bail!( "Hook install requires Unix (macOS/Linux). Use WSL or --claude-md mode." )
1138- }
1139-
1140- #[ cfg( unix) ]
11411116fn run_hook_only_mode (
11421117 global : bool ,
11431118 patch_mode : PatchMode ,
@@ -1752,6 +1727,9 @@ fn resolve_home_subdir(subdir: &str) -> Result<PathBuf> {
17521727}
17531728
17541729fn resolve_claude_dir ( ) -> Result < PathBuf > {
1730+ if let Ok ( dir) = std:: env:: var ( "RTK_CLAUDE_DIR" ) {
1731+ return Ok ( PathBuf :: from ( dir) ) ;
1732+ }
17551733 resolve_home_subdir ( CLAUDE_DIR )
17561734}
17571735
@@ -3668,4 +3646,133 @@ More notes
36683646 assert_eq ! ( arr. len( ) , 1 ) ;
36693647 assert_eq ! ( arr[ 0 ] [ "command" ] . as_str( ) . unwrap( ) , CURSOR_HOOK_COMMAND ) ;
36703648 }
3649+
3650+ use std:: sync:: Mutex ;
3651+ static CLAUDE_DIR_LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
3652+
3653+ fn with_claude_dir_override < F : FnOnce ( & Path ) > ( tmp : & TempDir , f : F ) {
3654+ let _guard = CLAUDE_DIR_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
3655+ let claude_dir = tmp. path ( ) . join ( CLAUDE_DIR ) ;
3656+ fs:: create_dir_all ( & claude_dir) . unwrap ( ) ;
3657+
3658+ let orig = std:: env:: var_os ( "RTK_CLAUDE_DIR" ) ;
3659+ std:: env:: set_var ( "RTK_CLAUDE_DIR" , & claude_dir) ;
3660+ f ( & claude_dir) ;
3661+ match orig {
3662+ Some ( v) => std:: env:: set_var ( "RTK_CLAUDE_DIR" , v) ,
3663+ None => std:: env:: remove_var ( "RTK_CLAUDE_DIR" ) ,
3664+ }
3665+ }
3666+
3667+ #[ test]
3668+ fn test_global_default_mode_creates_artifacts ( ) {
3669+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3670+ with_claude_dir_override ( & tmp, |claude_dir| {
3671+ run_default_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3672+
3673+ assert ! ( claude_dir. join( RTK_MD ) . exists( ) , "RTK.md must be created" ) ;
3674+ assert ! (
3675+ claude_dir. join( CLAUDE_MD ) . exists( ) ,
3676+ "CLAUDE.md must be created"
3677+ ) ;
3678+
3679+ let settings = claude_dir. join ( SETTINGS_JSON ) ;
3680+ assert ! ( settings. exists( ) , "settings.json must be created" ) ;
3681+ let content = fs:: read_to_string ( & settings) . unwrap ( ) ;
3682+ assert ! (
3683+ content. contains( CLAUDE_HOOK_COMMAND ) ,
3684+ "settings.json must contain hook command"
3685+ ) ;
3686+ } ) ;
3687+ }
3688+
3689+ #[ test]
3690+ fn test_global_uninstall_removes_artifacts ( ) {
3691+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3692+ with_claude_dir_override ( & tmp, |claude_dir| {
3693+ run_default_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3694+ uninstall ( true , false , false , false , 0 ) . unwrap ( ) ;
3695+
3696+ assert ! ( !claude_dir. join( RTK_MD ) . exists( ) , "RTK.md must be removed" ) ;
3697+ let settings_content =
3698+ fs:: read_to_string ( claude_dir. join ( SETTINGS_JSON ) ) . unwrap_or_default ( ) ;
3699+ assert ! (
3700+ !settings_content. contains( CLAUDE_HOOK_COMMAND ) ,
3701+ "hook entry must be removed from settings.json"
3702+ ) ;
3703+ } ) ;
3704+ }
3705+
3706+ #[ test]
3707+ fn test_global_default_mode_idempotent ( ) {
3708+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3709+ with_claude_dir_override ( & tmp, |claude_dir| {
3710+ run_default_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3711+ run_default_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3712+
3713+ let settings = fs:: read_to_string ( claude_dir. join ( SETTINGS_JSON ) ) . unwrap ( ) ;
3714+ let count = settings. matches ( CLAUDE_HOOK_COMMAND ) . count ( ) ;
3715+ assert_eq ! ( count, 1 , "hook command must appear exactly once" ) ;
3716+ } ) ;
3717+ }
3718+
3719+ #[ test]
3720+ fn test_upgrade_from_claude_md_to_hook_mode ( ) {
3721+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3722+ with_claude_dir_override ( & tmp, |claude_dir| {
3723+ run_claude_md_mode ( true , 0 , false ) . unwrap ( ) ;
3724+ let claude_md_content = fs:: read_to_string ( claude_dir. join ( CLAUDE_MD ) ) . unwrap ( ) ;
3725+ assert ! (
3726+ claude_md_content. contains( "<!-- rtk-instructions" ) ,
3727+ "pre-condition: old block must exist"
3728+ ) ;
3729+
3730+ run_default_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3731+
3732+ assert ! ( claude_dir. join( RTK_MD ) . exists( ) , "RTK.md must be created" ) ;
3733+ let settings = fs:: read_to_string ( claude_dir. join ( SETTINGS_JSON ) ) . unwrap ( ) ;
3734+ assert ! (
3735+ settings. contains( CLAUDE_HOOK_COMMAND ) ,
3736+ "hook must be in settings.json after upgrade"
3737+ ) ;
3738+ } ) ;
3739+ }
3740+
3741+ #[ test]
3742+ fn test_local_init_no_hook ( ) {
3743+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3744+ let cwd = std:: env:: current_dir ( ) . unwrap ( ) ;
3745+ std:: env:: set_current_dir ( tmp. path ( ) ) . unwrap ( ) ;
3746+
3747+ let result = run_default_mode ( false , PatchMode :: Auto , 0 , false ) ;
3748+ std:: env:: set_current_dir ( & cwd) . unwrap ( ) ;
3749+
3750+ result. unwrap ( ) ;
3751+ assert ! (
3752+ tmp. path( ) . join( CLAUDE_MD ) . exists( ) ,
3753+ "local CLAUDE.md must be created"
3754+ ) ;
3755+ assert ! (
3756+ !tmp. path( ) . join( SETTINGS_JSON ) . exists( ) ,
3757+ "settings.json must not be created for local init"
3758+ ) ;
3759+ }
3760+
3761+ #[ test]
3762+ fn test_global_hook_only_mode_creates_settings ( ) {
3763+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
3764+ with_claude_dir_override ( & tmp, |claude_dir| {
3765+ run_hook_only_mode ( true , PatchMode :: Auto , 0 , false ) . unwrap ( ) ;
3766+
3767+ assert ! (
3768+ !claude_dir. join( RTK_MD ) . exists( ) ,
3769+ "RTK.md must NOT be created in hook-only mode"
3770+ ) ;
3771+ let settings = fs:: read_to_string ( claude_dir. join ( SETTINGS_JSON ) ) . unwrap ( ) ;
3772+ assert ! (
3773+ settings. contains( CLAUDE_HOOK_COMMAND ) ,
3774+ "settings.json must contain hook command"
3775+ ) ;
3776+ } ) ;
3777+ }
36713778}
0 commit comments