1+ import { createHash } from 'node:crypto' ;
12import { readFile , realpath } from 'node:fs/promises' ;
2- import { isAbsolute , join , resolve } from 'node:path' ;
3-
4- import {
5- PLUGIN_STATE_ROOT_ENV_ANCHOR ,
6- userDataStateRoot ,
7- } from '@agent-bundle/runtime' ;
3+ import { basename , isAbsolute , join , resolve } from 'node:path' ;
84
95import { isErrno } from '../core/errors.ts' ;
6+ import { pluginStateRootEnvAnchor } from '../core/types.ts' ;
107import { webPluginDataRoot } from '../web-host/launch.ts' ;
118import type { InstallHost } from './install.ts' ;
129
@@ -33,6 +30,24 @@ const manifestCandidates = (host: InstallHost): readonly string[] => {
3330const isRecord = ( value : unknown ) : value is Record < string , unknown > =>
3431 value !== null && typeof value === 'object' && ! Array . isArray ( value ) ;
3532
33+ const safePluginSegment = / ^ [ a - z A - Z 0 - 9 ] (?: [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) ? $ / u;
34+
35+ // The CLI cannot load the optional React runtime. Uninstall tests pin this spelling against
36+ // `userDataStateRoot` from @agent-bundle/runtime for real installed roots.
37+ const installedUserDataStateRoot = (
38+ canonicalRoot : string ,
39+ environment : Readonly < NodeJS . ProcessEnv > ,
40+ home : string ,
41+ ) : string => {
42+ const xdgStateHome = environment . XDG_STATE_HOME ?? '' ;
43+ const stateHome = isAbsolute ( xdgStateHome )
44+ ? join ( xdgStateHome , 'agent-bundle' )
45+ : join ( home , '.agent-bundle' , 'state' ) ;
46+ const digest = createHash ( 'sha256' ) . update ( canonicalRoot ) . digest ( 'hex' ) . slice ( 0 , 16 ) ;
47+ const name = basename ( canonicalRoot ) ;
48+ return join ( stateHome , safePluginSegment . test ( name ) ? `${ name } -${ digest } ` : `plugin-${ digest } ` ) ;
49+ } ;
50+
3651const declaredStateRoot = async ( pluginRoot : string , host : InstallHost ) : Promise < string | undefined > => {
3752 for ( const relativePath of manifestCandidates ( host ) ) {
3853 let document : unknown ;
@@ -45,7 +60,7 @@ const declaredStateRoot = async (pluginRoot: string, host: InstallHost): Promise
4560 if ( ! isRecord ( document ) || ! isRecord ( document [ 'mcpServers' ] ) ) continue ;
4661 for ( const server of Object . values ( document [ 'mcpServers' ] ) ) {
4762 if ( ! isRecord ( server ) || ! isRecord ( server [ 'env' ] ) ) continue ;
48- const declared = server [ 'env' ] [ PLUGIN_STATE_ROOT_ENV_ANCHOR ] ;
63+ const declared = server [ 'env' ] [ pluginStateRootEnvAnchor ] ;
4964 if ( typeof declared !== 'string' || declared . trim ( ) === '' ) continue ;
5065 const expanded = declared
5166 . replaceAll ( '${CLAUDE_PLUGIN_ROOT}' , pluginRoot )
@@ -69,13 +84,13 @@ export const resolveInstalledStateRoot = async (
6984 throw error ;
7085 } ) ;
7186 const fromManifest = await declaredStateRoot ( canonicalRoot , host ) ;
72- const inherited = environment [ PLUGIN_STATE_ROOT_ENV_ANCHOR ] ?? '' ;
87+ const inherited = environment [ pluginStateRootEnvAnchor ] ?? '' ;
7388 const expandedInherited = inherited . trim ( ) === '' || / \$ \{ [ ^ } ] * \} / u. test ( inherited )
7489 ? undefined
7590 : isAbsolute ( inherited ) ? resolve ( inherited ) : resolve ( canonicalRoot , inherited ) ;
7691 const declared = fromManifest ?? expandedInherited ;
7792 return Object . freeze ( declared === undefined
78- ? { root : userDataStateRoot ( canonicalRoot , environment , home ) , source : 'derived' as const }
93+ ? { root : installedUserDataStateRoot ( canonicalRoot , environment , home ) , source : 'derived' as const }
7994 : { root : declared , source : 'native' as const } ) ;
8095} ;
8196
0 commit comments