@@ -127,7 +127,8 @@ function install({ configRoot, evolverRoot, force }) {
127127
128128 if ( ! force && isEvolverManagedPluginFile ( pluginPath ) ) {
129129 console . log ( '[opencode] Evolver plugin already installed. Use --force to overwrite.' ) ;
130- return { ok : true , skipped : true } ;
130+ printPostInstallNotice ( pluginPath ) ;
131+ return { ok : true , skipped : true , plugin_path : pluginPath } ;
131132 }
132133
133134 fs . mkdirSync ( opencodeDir , { recursive : true } ) ;
@@ -144,15 +145,147 @@ function install({ configRoot, evolverRoot, force }) {
144145 }
145146
146147 console . log ( '[opencode] Installation complete.' ) ;
147- console . log ( '[opencode] opencode auto-loads plugins from .opencode/plugins/ -- restart opencode to activate.' ) ;
148+ printPostInstallNotice ( written ) ;
148149
149150 return {
150151 ok : true ,
151152 platform : 'opencode' ,
153+ plugin_path : written ,
152154 files : [ written , agentsMdPath , ...copied ] ,
153155 } ;
154156}
155157
158+ function printPostInstallNotice ( pluginPath ) {
159+ // The opencode TUI "Plugins" tab only lists TUI plugins (registered via
160+ // tui.json with `default export { id, tui }`). Server plugins like ours,
161+ // auto-loaded from .opencode/plugins/, are functional but invisible in
162+ // that tab. This is by opencode design, not a bug. See:
163+ // https://github.com/sst/opencode -> packages/opencode/specs/tui-plugins.md
164+ // Surfacing this proactively here prevents the "is it actually loaded?"
165+ // confusion that motivated EvoMap/evolver issue #531.
166+ console . log ( '' ) ;
167+ console . log ( '[opencode] Restart opencode for the plugin to take effect.' ) ;
168+ console . log ( '[opencode] Note: this is a SERVER plugin (event hooks). It will NOT appear in' ) ;
169+ console . log ( '[opencode] the TUI "Plugins" tab -- that tab only lists TUI plugins' ) ;
170+ console . log ( '[opencode] (registered via tui.json). The plugin is still loaded and' ) ;
171+ console . log ( '[opencode] running silently in the background.' ) ;
172+ console . log ( '[opencode] To verify the plugin is loaded, run:' ) ;
173+ console . log ( '[opencode] opencode --print-logs --log-level INFO debug config 2>&1 | grep ' + JSON . stringify ( pluginPath ) ) ;
174+ console . log ( '[opencode] Or run:' ) ;
175+ console . log ( '[opencode] evolver setup-hooks --platform=opencode --verify' ) ;
176+ }
177+
178+ // Walk the install state and report what is healthy / what is missing.
179+ // Used by the `--verify` flag to give users a definitive answer to
180+ // "is the plugin actually installed and loadable?". Returns a structured
181+ // result so callers (CLI, future TUI plugin) can format it as they like.
182+ function verify ( { configRoot } ) {
183+ const opencodeDir = path . join ( configRoot , '.opencode' ) ;
184+ const hooksDir = path . join ( opencodeDir , HOOK_SCRIPTS_DIR_NAME ) ;
185+ const pluginsDir = path . join ( opencodeDir , PLUGINS_DIR_NAME ) ;
186+ const pluginPath = path . join ( pluginsDir , PLUGIN_FILE_NAME ) ;
187+ const agentsMdPath = path . join ( configRoot , 'AGENTS.md' ) ;
188+ const expectedScripts = [
189+ 'evolver-session-start.js' ,
190+ 'evolver-signal-detect.js' ,
191+ 'evolver-session-end.js' ,
192+ ] ;
193+
194+ const checks = [ ] ;
195+
196+ const pluginExists = fs . existsSync ( pluginPath ) ;
197+ checks . push ( {
198+ id : 'plugin_file_present' ,
199+ ok : pluginExists ,
200+ detail : pluginExists ? pluginPath : 'missing: ' + pluginPath ,
201+ } ) ;
202+
203+ const managed = pluginExists && isEvolverManagedPluginFile ( pluginPath ) ;
204+ checks . push ( {
205+ id : 'plugin_managed_marker' ,
206+ ok : managed ,
207+ detail : managed
208+ ? 'first line contains _evolver_managed: true'
209+ : 'plugin file is not evolver-managed (was it edited or replaced by another tool?)' ,
210+ } ) ;
211+
212+ let pluginLoadable = false ;
213+ let pluginLoadError = null ;
214+ if ( pluginExists ) {
215+ try {
216+ // Require the plugin in an isolated module cache slot to confirm it
217+ // parses and exports the expected shape. opencode does an ESM dynamic
218+ // import; CommonJS require here is a strict subset of that, so a
219+ // failure here is a guaranteed failure under opencode too.
220+ delete require . cache [ require . resolve ( pluginPath ) ] ;
221+ const mod = require ( pluginPath ) ;
222+ const fn = mod && ( mod . Evolver || mod . default ) ;
223+ pluginLoadable = typeof fn === 'function' ;
224+ if ( ! pluginLoadable ) pluginLoadError = 'no Evolver/default function export' ;
225+ } catch ( err ) {
226+ pluginLoadError = ( err && err . message ) || String ( err ) ;
227+ }
228+ }
229+ checks . push ( {
230+ id : 'plugin_loadable' ,
231+ ok : pluginLoadable ,
232+ detail : pluginLoadable
233+ ? 'require() succeeded and exports Evolver()'
234+ : 'require() failed: ' + ( pluginLoadError || 'unknown' ) ,
235+ } ) ;
236+
237+ const missingScripts = expectedScripts . filter (
238+ ( name ) => ! fs . existsSync ( path . join ( hooksDir , name ) )
239+ ) ;
240+ checks . push ( {
241+ id : 'hook_scripts_present' ,
242+ ok : missingScripts . length === 0 ,
243+ detail : missingScripts . length === 0
244+ ? 'all 3 hook scripts present in ' + hooksDir
245+ : 'missing: ' + missingScripts . join ( ', ' ) ,
246+ } ) ;
247+
248+ let agentsMdHasSection = false ;
249+ try {
250+ if ( fs . existsSync ( agentsMdPath ) ) {
251+ agentsMdHasSection = fs . readFileSync ( agentsMdPath , 'utf8' ) . includes ( EVOLVER_MARKER ) ;
252+ }
253+ } catch { /* ignore */ }
254+ checks . push ( {
255+ id : 'agents_md_section' ,
256+ ok : agentsMdHasSection ,
257+ detail : agentsMdHasSection
258+ ? 'AGENTS.md contains evolution memory section'
259+ : 'AGENTS.md is missing or has no evolution section (re-run install)' ,
260+ } ) ;
261+
262+ const allOk = checks . every ( ( c ) => c . ok ) ;
263+
264+ return {
265+ ok : allOk ,
266+ plugin_path : pluginPath ,
267+ hooks_dir : hooksDir ,
268+ config_root : configRoot ,
269+ checks,
270+ note : allOk
271+ ? 'Plugin is installed and loadable. opencode\'s TUI "Plugins" tab will not list it -- that tab only shows TUI plugins. Run `opencode --print-logs --log-level INFO debug config` and grep for the plugin path to confirm opencode loads it at startup.'
272+ : 'One or more checks failed. Re-run `evolver setup-hooks --platform=opencode --force` to repair.' ,
273+ } ;
274+ }
275+
276+ function printVerifyReport ( report ) {
277+ console . log ( '[opencode] Verify report' ) ;
278+ console . log ( '[opencode] plugin path : ' + report . plugin_path ) ;
279+ console . log ( '[opencode] hooks dir : ' + report . hooks_dir ) ;
280+ console . log ( '[opencode] config root : ' + report . config_root ) ;
281+ console . log ( '[opencode] Checks:' ) ;
282+ for ( const c of report . checks ) {
283+ console . log ( '[opencode] ' + ( c . ok ? '[OK] ' : '[FAIL]' ) + ' ' + c . id + ' -- ' + c . detail ) ;
284+ }
285+ console . log ( '[opencode] ' + ( report . ok ? 'All checks passed.' : 'Some checks failed.' ) ) ;
286+ console . log ( '[opencode] ' + report . note ) ;
287+ }
288+
156289function uninstall ( { configRoot } ) {
157290 const opencodeDir = path . join ( configRoot , '.opencode' ) ;
158291 const hooksDir = path . join ( opencodeDir , HOOK_SCRIPTS_DIR_NAME ) ;
@@ -193,6 +326,8 @@ function uninstall({ configRoot }) {
193326module . exports = {
194327 install,
195328 uninstall,
329+ verify,
330+ printVerifyReport,
196331 buildPluginSource,
197332 isEvolverManagedPluginFile,
198333 PLUGIN_FILE_NAME ,
0 commit comments