@@ -4,14 +4,22 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
44import { describe , expect , it } from "@effect/vitest" ;
55import * as Effect from "effect/Effect" ;
66import * as Path from "effect/Path" ;
7+ import * as FileSystem from "effect/FileSystem" ;
8+ import * as Schema from "effect/Schema" ;
9+ import { ClaudeSettings , ProviderInstanceId } from "@t3tools/contracts" ;
10+ import { symlinksSupported } from "@t3tools/shared/testing/symlinks" ;
711
812import {
913 makeClaudeCapabilitiesCacheKey ,
1014 makeClaudeContinuationGroupKey ,
1115 makeClaudeEnvironment ,
16+ resolveClaudeRuntime ,
17+ materializeClaudeHome ,
1218 resolveClaudeHomePath ,
1319} from "./ClaudeHome.ts" ;
1420
21+ const decodeClaudeSettings = Schema . decodeSync ( ClaudeSettings ) ;
22+
1523it . layer ( NodeServices . layer ) ( "ClaudeHome" , ( it ) => {
1624 describe ( "Claude home resolution" , ( ) => {
1725 it . effect ( "resolves configured Claude HOME and stamps continuation/cache keys with it" , ( ) =>
@@ -47,6 +55,105 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => {
4755 } ) ,
4856 ) ;
4957
58+ for ( const shareProfile of [ true , false ] ) {
59+ it . effect . skipIf ( ! symlinksSupported ) (
60+ `keeps private accounts separate with shareProfile=${ shareProfile } ` ,
61+ ( ) =>
62+ Effect . gen ( function * ( ) {
63+ const fs = yield * FileSystem . FileSystem ;
64+ const path = yield * Path . Path ;
65+ const root = yield * fs . makeTempDirectoryScoped ( { prefix : "t3-claude-home-" } ) ;
66+ const shared = path . join ( root , "shared" ) ;
67+ const runtime = yield * resolveClaudeRuntime ( {
68+ instanceId : ProviderInstanceId . make ( "private" ) ,
69+ stateDir : root ,
70+ config : decodeClaudeSettings ( {
71+ setupMode : "cli" ,
72+ homePath : "/ignored" ,
73+ shareProfile,
74+ } ) ,
75+ environment : {
76+ CLAUDE_CONFIG_DIR : shared ,
77+ ANTHROPIC_API_KEY : "host" ,
78+ CLAUDE_SECURESTORAGE_CONFIG_DIR : "" ,
79+ } ,
80+ } ) ;
81+ const accountDir = path . join ( root , "providers" , "claude" , "private" , "cli" ) ;
82+ expect ( runtime . config . homePath ) . toBe ( accountDir ) ;
83+ expect ( runtime . environment ) . toEqual ( {
84+ CLAUDE_CONFIG_DIR : accountDir ,
85+ CLAUDE_SECURESTORAGE_CONFIG_DIR : accountDir ,
86+ } ) ;
87+ expect ( yield * makeClaudeContinuationGroupKey ( runtime . config , runtime . environment ) ) . toBe (
88+ `claude:home:${ accountDir } ` ,
89+ ) ;
90+ // Skills bind to an account; plugins can sync before the account is bound.
91+ const buckets = {
92+ skills : "11111111-1111-1111-1111-111111111111_22222222-2222-2222-2222-222222222222" ,
93+ plugins : "11111111-1111-1111-1111-111111111111_unbound" ,
94+ } as const ;
95+ for ( const name of [ "plugins" , "skills" ] as const ) {
96+ const bucket = buckets [ name ] ;
97+ yield * fs . makeDirectory ( path . join ( accountDir , name , "synced" , bucket ) , {
98+ recursive : true ,
99+ } ) ;
100+ yield * fs . writeFileString (
101+ path . join ( accountDir , name , "synced" , bucket , "skill.md" ) ,
102+ "skill" ,
103+ ) ;
104+ yield * fs . writeFileString (
105+ path . join ( accountDir , name , "synced" , `.bucket-${ bucket } ` ) ,
106+ "marker" ,
107+ ) ;
108+ }
109+ // Only a profile directory that exists gets a link; agents stays absent.
110+ for ( const name of [ "commands" , "output-styles" , "rules" , "hooks" ] ) {
111+ yield * fs . makeDirectory ( path . join ( shared , name ) , { recursive : true } ) ;
112+ }
113+ yield * materializeClaudeHome ( runtime ) ;
114+ if ( shareProfile ) {
115+ expect ( yield * fs . exists ( path . join ( accountDir , "agents" ) ) ) . toBe ( false ) ;
116+ expect ( yield * fs . exists ( path . join ( shared , "agents" ) ) ) . toBe ( false ) ;
117+ for ( const name of [
118+ "plugins" ,
119+ "skills" ,
120+ "commands" ,
121+ "output-styles" ,
122+ "rules" ,
123+ "hooks" ,
124+ "settings.json" ,
125+ "CLAUDE.md" ,
126+ "keybindings.json" ,
127+ ] ) {
128+ expect ( yield * fs . readLink ( path . join ( accountDir , name ) ) ) . toBe (
129+ path . join ( shared , name ) ,
130+ ) ;
131+ }
132+ expect ( yield * fs . exists ( path . join ( shared , "settings.json" ) ) ) . toBe ( false ) ;
133+ for ( const name of [ "plugins" , "skills" ] as const ) {
134+ const bucket = buckets [ name ] ;
135+ expect (
136+ yield * fs . readFileString ( path . join ( shared , name , "synced" , bucket , "skill.md" ) ) ,
137+ ) . toBe ( "skill" ) ;
138+ expect (
139+ yield * fs . readFileString ( path . join ( shared , name , "synced" , `.bucket-${ bucket } ` ) ) ,
140+ ) . toBe ( "marker" ) ;
141+ }
142+ if ( runtime . mode !== "cli" ) throw new Error ( "Expected CLI runtime" ) ;
143+ yield * materializeClaudeHome ( {
144+ ...runtime ,
145+ overlay : { ...runtime . overlay , active : false } ,
146+ } ) ;
147+ expect ( yield * fs . readDirectory ( accountDir ) ) . toEqual ( [ ] ) ;
148+ }
149+ expect ( yield * fs . exists ( path . join ( accountDir , "projects" ) ) ) . toBe ( false ) ;
150+ expect ( yield * fs . readDirectory ( accountDir ) ) . toEqual (
151+ shareProfile ? [ ] : [ "plugins" , "skills" ] ,
152+ ) ;
153+ } ) ,
154+ ) ;
155+ }
156+
50157 it . effect ( "separates capability probes by cwd" , ( ) =>
51158 Effect . gen ( function * ( ) {
52159 const config = { binaryPath : "claude" , homePath : "" } ;
0 commit comments