/ src / commands / reload-plugins / reload-plugins.ts
reload-plugins.ts
 1  import { feature } from 'bun:bundle'
 2  import { getIsRemoteMode } from '../../bootstrap/state.js'
 3  import { redownloadUserSettings } from '../../services/settingsSync/index.js'
 4  import type { LocalCommandCall } from '../../types/command.js'
 5  import { isEnvTruthy } from '../../utils/envUtils.js'
 6  import { refreshActivePlugins } from '../../utils/plugins/refresh.js'
 7  import { settingsChangeDetector } from '../../utils/settings/changeDetector.js'
 8  import { plural } from '../../utils/stringUtils.js'
 9  
10  export const call: LocalCommandCall = async (_args, context) => {
11    // CCR: re-pull user settings before the cache sweep so enabledPlugins /
12    // extraKnownMarketplaces pushed from the user's local CLI (settingsSync)
13    // take effect. Non-CCR headless (e.g. vscode SDK subprocess) shares disk
14    // with whoever writes settings — the file watcher delivers changes, no
15    // re-pull needed there.
16    //
17    // Managed settings intentionally NOT re-fetched: it already polls hourly
18    // (POLLING_INTERVAL_MS), and policy enforcement is eventually-consistent
19    // by design (stale-cache fallback on fetch failure). Interactive
20    // /reload-plugins has never re-fetched it either.
21    //
22    // No retries: user-initiated command, one attempt + fail-open. The user
23    // can re-run /reload-plugins to retry. Startup path keeps its retries.
24    if (
25      feature('DOWNLOAD_USER_SETTINGS') &&
26      (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) || getIsRemoteMode())
27    ) {
28      const applied = await redownloadUserSettings()
29      // applyRemoteEntriesToLocal uses markInternalWrite to suppress the
30      // file watcher (correct for startup, nothing listening yet); fire
31      // notifyChange here so mid-session applySettingsChange runs.
32      if (applied) {
33        settingsChangeDetector.notifyChange('userSettings')
34      }
35    }
36  
37    const r = await refreshActivePlugins(context.setAppState)
38  
39    const parts = [
40      n(r.enabled_count, 'plugin'),
41      n(r.command_count, 'skill'),
42      n(r.agent_count, 'agent'),
43      n(r.hook_count, 'hook'),
44      // "plugin MCP/LSP" disambiguates from user-config/built-in servers,
45      // which /reload-plugins doesn't touch. Commands/hooks are plugin-only;
46      // agent_count is total agents (incl. built-ins). (gh-31321)
47      n(r.mcp_count, 'plugin MCP server'),
48      n(r.lsp_count, 'plugin LSP server'),
49    ]
50    let msg = `Reloaded: ${parts.join(' · ')}`
51  
52    if (r.error_count > 0) {
53      msg += `\n${n(r.error_count, 'error')} during load. Run /doctor for details.`
54    }
55  
56    return { type: 'text', value: msg }
57  }
58  
59  function n(count: number, noun: string): string {
60    return `${count} ${plural(count, noun)}`
61  }