@machina.ai/cell-cli 1.11.0-rc1 → 1.13.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (538) hide show
  1. package/dist/package.json +16 -10
  2. package/dist/src/commands/extensions/disable.d.ts +1 -1
  3. package/dist/src/commands/extensions/disable.js +15 -7
  4. package/dist/src/commands/extensions/disable.js.map +1 -1
  5. package/dist/src/commands/extensions/enable.d.ts +1 -1
  6. package/dist/src/commands/extensions/enable.js +15 -7
  7. package/dist/src/commands/extensions/enable.js.map +1 -1
  8. package/dist/src/commands/extensions/install.js +14 -3
  9. package/dist/src/commands/extensions/install.js.map +1 -1
  10. package/dist/src/commands/extensions/install.test.js +39 -19
  11. package/dist/src/commands/extensions/install.test.js.map +1 -1
  12. package/dist/src/commands/extensions/link.js +14 -3
  13. package/dist/src/commands/extensions/link.js.map +1 -1
  14. package/dist/src/commands/extensions/list.js +13 -4
  15. package/dist/src/commands/extensions/list.js.map +1 -1
  16. package/dist/src/commands/extensions/uninstall.js +13 -2
  17. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  18. package/dist/src/commands/extensions/update.js +18 -13
  19. package/dist/src/commands/extensions/update.js.map +1 -1
  20. package/dist/src/commands/extensions/validate.d.ts +12 -0
  21. package/dist/src/commands/extensions/validate.js +83 -0
  22. package/dist/src/commands/extensions/validate.js.map +1 -0
  23. package/dist/src/commands/extensions/validate.test.js +93 -0
  24. package/dist/src/commands/extensions/validate.test.js.map +1 -0
  25. package/dist/src/commands/extensions.js +3 -0
  26. package/dist/src/commands/extensions.js.map +1 -1
  27. package/dist/src/commands/mcp/add.test.js +3 -0
  28. package/dist/src/commands/mcp/add.test.js.map +1 -1
  29. package/dist/src/commands/mcp/list.js +10 -3
  30. package/dist/src/commands/mcp/list.js.map +1 -1
  31. package/dist/src/commands/mcp/list.test.js +37 -27
  32. package/dist/src/commands/mcp/list.test.js.map +1 -1
  33. package/dist/src/config/auth.js +0 -5
  34. package/dist/src/config/auth.js.map +1 -1
  35. package/dist/src/config/config.d.ts +6 -3
  36. package/dist/src/config/config.js +65 -80
  37. package/dist/src/config/config.js.map +1 -1
  38. package/dist/src/config/config.test.js +235 -212
  39. package/dist/src/config/config.test.js.map +1 -1
  40. package/dist/src/config/extension-manager.d.ts +63 -0
  41. package/dist/src/config/extension-manager.js +450 -0
  42. package/dist/src/config/extension-manager.js.map +1 -0
  43. package/dist/src/config/extension.d.ts +4 -51
  44. package/dist/src/config/extension.js +1 -535
  45. package/dist/src/config/extension.js.map +1 -1
  46. package/dist/src/config/extension.test.js +525 -201
  47. package/dist/src/config/extension.test.js.map +1 -1
  48. package/dist/src/config/extensions/consent.d.ts +38 -0
  49. package/dist/src/config/extensions/consent.js +123 -0
  50. package/dist/src/config/extensions/consent.js.map +1 -0
  51. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  52. package/dist/src/config/extensions/extensionEnablement.js +4 -3
  53. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  54. package/dist/src/config/extensions/extensionEnablement.test.js +10 -10
  55. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  56. package/dist/src/config/extensions/extensionSettings.d.ts +15 -0
  57. package/dist/src/config/extensions/extensionSettings.js +113 -0
  58. package/dist/src/config/extensions/extensionSettings.js.map +1 -0
  59. package/dist/src/config/extensions/extensionSettings.test.d.ts +6 -0
  60. package/dist/src/config/extensions/extensionSettings.test.js +254 -0
  61. package/dist/src/config/extensions/extensionSettings.test.js.map +1 -0
  62. package/dist/src/config/extensions/github.d.ts +2 -2
  63. package/dist/src/config/extensions/github.js +5 -10
  64. package/dist/src/config/extensions/github.js.map +1 -1
  65. package/dist/src/config/extensions/github.test.js +153 -167
  66. package/dist/src/config/extensions/github.test.js.map +1 -1
  67. package/dist/src/config/extensions/github_fetch.d.ts +1 -1
  68. package/dist/src/config/extensions/github_fetch.js +13 -1
  69. package/dist/src/config/extensions/github_fetch.js.map +1 -1
  70. package/dist/src/config/extensions/github_fetch.test.d.ts +6 -0
  71. package/dist/src/config/extensions/github_fetch.test.js +169 -0
  72. package/dist/src/config/extensions/github_fetch.test.js.map +1 -0
  73. package/dist/src/config/extensions/storage.d.ts +14 -0
  74. package/dist/src/config/extensions/storage.js +32 -0
  75. package/dist/src/config/extensions/storage.js.map +1 -0
  76. package/dist/src/config/extensions/update.d.ts +4 -4
  77. package/dist/src/config/extensions/update.js +39 -39
  78. package/dist/src/config/extensions/update.js.map +1 -1
  79. package/dist/src/config/extensions/update.test.js +72 -74
  80. package/dist/src/config/extensions/update.test.js.map +1 -1
  81. package/dist/src/config/extensions/variableSchema.d.ts +0 -6
  82. package/dist/src/config/extensions/variableSchema.js.map +1 -1
  83. package/dist/src/config/extensions/variables.d.ts +4 -0
  84. package/dist/src/config/extensions/variables.js +6 -0
  85. package/dist/src/config/extensions/variables.js.map +1 -1
  86. package/dist/src/config/keyBindings.d.ts +3 -0
  87. package/dist/src/config/keyBindings.js +30 -8
  88. package/dist/src/config/keyBindings.js.map +1 -1
  89. package/dist/src/config/keyBindings.test.js +17 -0
  90. package/dist/src/config/keyBindings.test.js.map +1 -1
  91. package/dist/src/config/policies/read-only.toml +56 -0
  92. package/dist/src/config/policies/write.toml +63 -0
  93. package/dist/src/config/policies/yolo.toml +31 -0
  94. package/dist/src/config/policy-engine.integration.test.js +41 -38
  95. package/dist/src/config/policy-engine.integration.test.js.map +1 -1
  96. package/dist/src/config/policy.d.ts +2 -2
  97. package/dist/src/config/policy.js +10 -148
  98. package/dist/src/config/policy.js.map +1 -1
  99. package/dist/src/config/sandboxConfig.d.ts +1 -1
  100. package/dist/src/config/sandboxConfig.js +6 -3
  101. package/dist/src/config/sandboxConfig.js.map +1 -1
  102. package/dist/src/config/settings.d.ts +2 -1
  103. package/dist/src/config/settings.js +58 -18
  104. package/dist/src/config/settings.js.map +1 -1
  105. package/dist/src/config/settings.test.js +128 -69
  106. package/dist/src/config/settings.test.js.map +1 -1
  107. package/dist/src/config/settingsSchema.d.ts +170 -28
  108. package/dist/src/config/settingsSchema.js +418 -27
  109. package/dist/src/config/settingsSchema.js.map +1 -1
  110. package/dist/src/config/settingsSchema.test.js +42 -1
  111. package/dist/src/config/settingsSchema.test.js.map +1 -1
  112. package/dist/src/config/trustedFolders.d.ts +1 -1
  113. package/dist/src/config/trustedFolders.js +4 -2
  114. package/dist/src/config/trustedFolders.js.map +1 -1
  115. package/dist/src/core/initializer.js +2 -1
  116. package/dist/src/core/initializer.js.map +1 -1
  117. package/dist/src/gemini.d.ts +1 -1
  118. package/dist/src/gemini.js +46 -16
  119. package/dist/src/gemini.js.map +1 -1
  120. package/dist/src/gemini.test.js +88 -30
  121. package/dist/src/gemini.test.js.map +1 -1
  122. package/dist/src/generated/git-commit.d.ts +2 -2
  123. package/dist/src/generated/git-commit.js +2 -2
  124. package/dist/src/nonInteractiveCli.d.ts +9 -1
  125. package/dist/src/nonInteractiveCli.js +114 -7
  126. package/dist/src/nonInteractiveCli.js.map +1 -1
  127. package/dist/src/nonInteractiveCli.test.js +355 -112
  128. package/dist/src/nonInteractiveCli.test.js.map +1 -1
  129. package/dist/src/services/BuiltinCommandLoader.js +4 -0
  130. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  131. package/dist/src/services/BuiltinCommandLoader.test.js +22 -0
  132. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  133. package/dist/src/services/FeedbackService.js +2 -2
  134. package/dist/src/services/FeedbackService.js.map +1 -1
  135. package/dist/src/services/McpPromptLoader.js +2 -2
  136. package/dist/src/services/McpPromptLoader.js.map +1 -1
  137. package/dist/src/services/McpPromptLoader.test.js +4 -2
  138. package/dist/src/services/McpPromptLoader.test.js.map +1 -1
  139. package/dist/src/test-utils/async.d.ts +9 -0
  140. package/dist/src/test-utils/async.js +29 -0
  141. package/dist/src/test-utils/async.js.map +1 -0
  142. package/dist/src/test-utils/createExtension.d.ts +3 -1
  143. package/dist/src/test-utils/createExtension.js +3 -3
  144. package/dist/src/test-utils/createExtension.js.map +1 -1
  145. package/dist/src/test-utils/render.d.ts +16 -2
  146. package/dist/src/test-utils/render.js +66 -4
  147. package/dist/src/test-utils/render.js.map +1 -1
  148. package/dist/src/test-utils/render.test.d.ts +6 -0
  149. package/dist/src/test-utils/render.test.js +79 -0
  150. package/dist/src/test-utils/render.test.js.map +1 -0
  151. package/dist/src/ui/App.test.js +1 -1
  152. package/dist/src/ui/App.test.js.map +1 -1
  153. package/dist/src/ui/AppContainer.js +181 -65
  154. package/dist/src/ui/AppContainer.js.map +1 -1
  155. package/dist/src/ui/AppContainer.test.js +505 -147
  156. package/dist/src/ui/AppContainer.test.js.map +1 -1
  157. package/dist/src/ui/IdeIntegrationNudge.js +1 -1
  158. package/dist/src/ui/IdeIntegrationNudge.js.map +1 -1
  159. package/dist/src/ui/auth/ApiAuthDialog.d.ts +14 -0
  160. package/dist/src/ui/auth/ApiAuthDialog.js +26 -0
  161. package/dist/src/ui/auth/ApiAuthDialog.js.map +1 -0
  162. package/dist/src/ui/auth/ApiAuthDialog.test.d.ts +6 -0
  163. package/dist/src/ui/auth/ApiAuthDialog.test.js +91 -0
  164. package/dist/src/ui/auth/ApiAuthDialog.test.js.map +1 -0
  165. package/dist/src/ui/auth/AuthDialog.js +7 -3
  166. package/dist/src/ui/auth/AuthDialog.js.map +1 -1
  167. package/dist/src/ui/auth/useAuth.d.ts +2 -0
  168. package/dist/src/ui/auth/useAuth.js +31 -2
  169. package/dist/src/ui/auth/useAuth.js.map +1 -1
  170. package/dist/src/ui/colors.js +3 -0
  171. package/dist/src/ui/colors.js.map +1 -1
  172. package/dist/src/ui/commands/directoryCommand.js +1 -1
  173. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  174. package/dist/src/ui/commands/extensionsCommand.js +64 -11
  175. package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
  176. package/dist/src/ui/commands/extensionsCommand.test.js +72 -1
  177. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
  178. package/dist/src/ui/commands/mcpCommand.js +14 -14
  179. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  180. package/dist/src/ui/commands/mcpCommand.test.js +4 -0
  181. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
  182. package/dist/src/ui/commands/memoryCommand.js +1 -1
  183. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  184. package/dist/src/ui/commands/memoryCommand.test.js +3 -1
  185. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
  186. package/dist/src/ui/commands/policiesCommand.d.ts +7 -0
  187. package/dist/src/ui/commands/policiesCommand.js +59 -0
  188. package/dist/src/ui/commands/policiesCommand.js.map +1 -0
  189. package/dist/src/ui/commands/policiesCommand.test.d.ts +6 -0
  190. package/dist/src/ui/commands/policiesCommand.test.js +83 -0
  191. package/dist/src/ui/commands/policiesCommand.test.js.map +1 -0
  192. package/dist/src/ui/components/AnsiOutput.test.js +1 -1
  193. package/dist/src/ui/components/AnsiOutput.test.js.map +1 -1
  194. package/dist/src/ui/components/AsciiArt.d.ts +3 -3
  195. package/dist/src/ui/components/AsciiArt.js +3 -3
  196. package/dist/src/ui/components/Composer.js +1 -1
  197. package/dist/src/ui/components/Composer.js.map +1 -1
  198. package/dist/src/ui/components/Composer.test.js +5 -2
  199. package/dist/src/ui/components/Composer.test.js.map +1 -1
  200. package/dist/src/ui/components/ConfigInitDisplay.js +4 -6
  201. package/dist/src/ui/components/ConfigInitDisplay.js.map +1 -1
  202. package/dist/src/ui/components/ConsentPrompt.test.js +18 -8
  203. package/dist/src/ui/components/ConsentPrompt.test.js.map +1 -1
  204. package/dist/src/ui/components/ConsoleSummaryDisplay.js +1 -1
  205. package/dist/src/ui/components/ConsoleSummaryDisplay.js.map +1 -1
  206. package/dist/src/ui/components/ContextSummaryDisplay.test.js +11 -6
  207. package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -1
  208. package/dist/src/ui/components/DetailedMessagesDisplay.js +1 -1
  209. package/dist/src/ui/components/DetailedMessagesDisplay.js.map +1 -1
  210. package/dist/src/ui/components/DialogManager.js +4 -0
  211. package/dist/src/ui/components/DialogManager.js.map +1 -1
  212. package/dist/src/ui/components/FolderTrustDialog.test.js +2 -1
  213. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  214. package/dist/src/ui/components/Footer.js +4 -3
  215. package/dist/src/ui/components/Footer.js.map +1 -1
  216. package/dist/src/ui/components/Footer.test.js +83 -0
  217. package/dist/src/ui/components/Footer.test.js.map +1 -1
  218. package/dist/src/ui/components/Header.test.js +13 -5
  219. package/dist/src/ui/components/Header.test.js.map +1 -1
  220. package/dist/src/ui/components/Help.test.js +5 -4
  221. package/dist/src/ui/components/Help.test.js.map +1 -1
  222. package/dist/src/ui/components/HistoryItemDisplay.js +1 -1
  223. package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
  224. package/dist/src/ui/components/InputPrompt.js +27 -8
  225. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  226. package/dist/src/ui/components/InputPrompt.test.js +776 -727
  227. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  228. package/dist/src/ui/components/LoadingIndicator.js +2 -2
  229. package/dist/src/ui/components/LoadingIndicator.js.map +1 -1
  230. package/dist/src/ui/components/LoadingIndicator.test.js +28 -15
  231. package/dist/src/ui/components/LoadingIndicator.test.js.map +1 -1
  232. package/dist/src/ui/components/LoopDetectionConfirmation.js +1 -1
  233. package/dist/src/ui/components/LoopDetectionConfirmation.js.map +1 -1
  234. package/dist/src/ui/components/LoopDetectionConfirmation.test.js +2 -2
  235. package/dist/src/ui/components/LoopDetectionConfirmation.test.js.map +1 -1
  236. package/dist/src/ui/components/MainContent.js +15 -4
  237. package/dist/src/ui/components/MainContent.js.map +1 -1
  238. package/dist/src/ui/components/ModelDialog.js +1 -1
  239. package/dist/src/ui/components/ModelDialog.js.map +1 -1
  240. package/dist/src/ui/components/ModelDialog.test.js +23 -13
  241. package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
  242. package/dist/src/ui/components/ModelStatsDisplay.test.js +1 -1
  243. package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -1
  244. package/dist/src/ui/components/Notifications.js +38 -5
  245. package/dist/src/ui/components/Notifications.js.map +1 -1
  246. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +2 -2
  247. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  248. package/dist/src/ui/components/PrepareLabel.test.js +14 -8
  249. package/dist/src/ui/components/PrepareLabel.test.js.map +1 -1
  250. package/dist/src/ui/components/ProQuotaDialog.test.js +14 -6
  251. package/dist/src/ui/components/ProQuotaDialog.test.js.map +1 -1
  252. package/dist/src/ui/components/QueuedMessageDisplay.test.js +11 -6
  253. package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
  254. package/dist/src/ui/components/SessionSummaryDisplay.test.js +1 -1
  255. package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -1
  256. package/dist/src/ui/components/SettingsDialog.js +32 -25
  257. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  258. package/dist/src/ui/components/SettingsDialog.test.js +428 -532
  259. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  260. package/dist/src/ui/components/ShellConfirmationDialog.js +1 -1
  261. package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -1
  262. package/dist/src/ui/components/ShellConfirmationDialog.test.js +2 -2
  263. package/dist/src/ui/components/ShellConfirmationDialog.test.js.map +1 -1
  264. package/dist/src/ui/components/StatsDisplay.test.js +1 -1
  265. package/dist/src/ui/components/StatsDisplay.test.js.map +1 -1
  266. package/dist/src/ui/components/SuggestionsDisplay.js +1 -1
  267. package/dist/src/ui/components/SuggestionsDisplay.js.map +1 -1
  268. package/dist/src/ui/components/ThemeDialog.test.js +2 -2
  269. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  270. package/dist/src/ui/components/ToolStatsDisplay.test.js +1 -1
  271. package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -1
  272. package/dist/src/ui/components/messages/CompressionMessage.test.js +25 -17
  273. package/dist/src/ui/components/messages/CompressionMessage.test.js.map +1 -1
  274. package/dist/src/ui/components/messages/DiffRenderer.test.js +1 -1
  275. package/dist/src/ui/components/messages/DiffRenderer.test.js.map +1 -1
  276. package/dist/src/ui/components/messages/InfoMessage.js +1 -1
  277. package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
  278. package/dist/src/ui/components/messages/Todo.js +27 -5
  279. package/dist/src/ui/components/messages/Todo.js.map +1 -1
  280. package/dist/src/ui/components/messages/Todo.test.js +20 -8
  281. package/dist/src/ui/components/messages/Todo.test.js.map +1 -1
  282. package/dist/src/ui/components/messages/ToolConfirmationMessage.js +1 -1
  283. package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
  284. package/dist/src/ui/components/messages/ToolGroupMessage.test.js +29 -15
  285. package/dist/src/ui/components/messages/ToolGroupMessage.test.js.map +1 -1
  286. package/dist/src/ui/components/messages/WarningMessage.js +2 -2
  287. package/dist/src/ui/components/messages/WarningMessage.js.map +1 -1
  288. package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
  289. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  290. package/dist/src/ui/components/shared/MaxSizedBox.test.js +43 -22
  291. package/dist/src/ui/components/shared/MaxSizedBox.test.js.map +1 -1
  292. package/dist/src/ui/components/shared/TextInput.d.ts +15 -0
  293. package/dist/src/ui/components/shared/TextInput.js +38 -0
  294. package/dist/src/ui/components/shared/TextInput.js.map +1 -0
  295. package/dist/src/ui/components/shared/TextInput.test.d.ts +6 -0
  296. package/dist/src/ui/components/shared/TextInput.test.js +242 -0
  297. package/dist/src/ui/components/shared/TextInput.test.js.map +1 -0
  298. package/dist/src/ui/components/shared/text-buffer.d.ts +9 -2
  299. package/dist/src/ui/components/shared/text-buffer.js +51 -13
  300. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  301. package/dist/src/ui/components/shared/text-buffer.test.js +385 -202
  302. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
  303. package/dist/src/ui/components/views/ChatList.test.js +7 -4
  304. package/dist/src/ui/components/views/ChatList.test.js.map +1 -1
  305. package/dist/src/ui/components/views/ExtensionsList.d.ts +7 -1
  306. package/dist/src/ui/components/views/ExtensionsList.js +9 -11
  307. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  308. package/dist/src/ui/components/views/ExtensionsList.test.js +43 -22
  309. package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
  310. package/dist/src/ui/components/views/McpStatus.test.js +23 -12
  311. package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
  312. package/dist/src/ui/contexts/KeypressContext.d.ts +3 -2
  313. package/dist/src/ui/contexts/KeypressContext.js +610 -540
  314. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  315. package/dist/src/ui/contexts/KeypressContext.test.js +438 -718
  316. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  317. package/dist/src/ui/contexts/MouseContext.d.ts +21 -0
  318. package/dist/src/ui/contexts/MouseContext.js +89 -0
  319. package/dist/src/ui/contexts/MouseContext.js.map +1 -0
  320. package/dist/src/ui/contexts/MouseContext.test.d.ts +6 -0
  321. package/dist/src/ui/contexts/MouseContext.test.js +164 -0
  322. package/dist/src/ui/contexts/MouseContext.test.js.map +1 -0
  323. package/dist/src/ui/contexts/SessionContext.test.js +35 -17
  324. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
  325. package/dist/src/ui/contexts/UIActionsContext.d.ts +2 -0
  326. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  327. package/dist/src/ui/contexts/UIStateContext.d.ts +2 -0
  328. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  329. package/dist/src/ui/hooks/atCommandProcessor.js +31 -9
  330. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  331. package/dist/src/ui/hooks/atCommandProcessor.test.js +163 -64
  332. package/dist/src/ui/hooks/atCommandProcessor.test.js.map +1 -1
  333. package/dist/src/ui/hooks/shellCommandProcessor.test.js +64 -35
  334. package/dist/src/ui/hooks/shellCommandProcessor.test.js.map +1 -1
  335. package/dist/src/ui/hooks/slashCommandProcessor.test.js +193 -165
  336. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  337. package/dist/src/ui/hooks/useAtCompletion.test.js +16 -5
  338. package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -1
  339. package/dist/src/ui/hooks/useAutoAcceptIndicator.js +10 -0
  340. package/dist/src/ui/hooks/useAutoAcceptIndicator.js.map +1 -1
  341. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +32 -1
  342. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  343. package/dist/src/ui/hooks/useCommandCompletion.test.js +66 -64
  344. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -1
  345. package/dist/src/ui/hooks/useConsoleMessages.test.js +26 -9
  346. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -1
  347. package/dist/src/ui/hooks/useEditorSettings.test.js +40 -34
  348. package/dist/src/ui/hooks/useEditorSettings.test.js.map +1 -1
  349. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +14 -5
  350. package/dist/src/ui/hooks/useExtensionUpdates.js +18 -13
  351. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  352. package/dist/src/ui/hooks/useExtensionUpdates.test.js +49 -44
  353. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  354. package/dist/src/ui/hooks/useFlickerDetector.test.js +9 -5
  355. package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
  356. package/dist/src/ui/hooks/useFocus.test.js +25 -9
  357. package/dist/src/ui/hooks/useFocus.test.js.map +1 -1
  358. package/dist/src/ui/hooks/useFolderTrust.test.js +46 -22
  359. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
  360. package/dist/src/ui/hooks/useGeminiStream.js +56 -19
  361. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  362. package/dist/src/ui/hooks/useGeminiStream.test.js +260 -411
  363. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  364. package/dist/src/ui/hooks/useGitBranchName.js +4 -0
  365. package/dist/src/ui/hooks/useGitBranchName.js.map +1 -1
  366. package/dist/src/ui/hooks/useGitBranchName.test.js +46 -34
  367. package/dist/src/ui/hooks/useGitBranchName.test.js.map +1 -1
  368. package/dist/src/ui/hooks/useHistoryManager.test.js +2 -1
  369. package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
  370. package/dist/src/ui/hooks/useIdeTrustListener.test.js +40 -9
  371. package/dist/src/ui/hooks/useIdeTrustListener.test.js.map +1 -1
  372. package/dist/src/ui/hooks/useInputHistory.test.js +2 -1
  373. package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
  374. package/dist/src/ui/hooks/useInputHistoryStore.test.js +2 -1
  375. package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
  376. package/dist/src/ui/hooks/useKeypress.test.js +103 -114
  377. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  378. package/dist/src/ui/hooks/useLoadingIndicator.test.js +24 -6
  379. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  380. package/dist/src/ui/hooks/useMemoryMonitor.test.js +10 -5
  381. package/dist/src/ui/hooks/useMemoryMonitor.test.js.map +1 -1
  382. package/dist/src/ui/hooks/useMessageQueue.test.js +62 -45
  383. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  384. package/dist/src/ui/hooks/useModelCommand.test.js +21 -11
  385. package/dist/src/ui/hooks/useModelCommand.test.js.map +1 -1
  386. package/dist/src/ui/hooks/useMouse.d.ts +17 -0
  387. package/dist/src/ui/hooks/useMouse.js +27 -0
  388. package/dist/src/ui/hooks/useMouse.js.map +1 -0
  389. package/dist/src/ui/hooks/useMouse.test.d.ts +6 -0
  390. package/dist/src/ui/hooks/useMouse.test.js +57 -0
  391. package/dist/src/ui/hooks/useMouse.test.js.map +1 -0
  392. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +2 -2
  393. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
  394. package/dist/src/ui/hooks/usePhraseCycler.js +1 -1
  395. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  396. package/dist/src/ui/hooks/usePhraseCycler.test.js +109 -106
  397. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
  398. package/dist/src/ui/hooks/usePrivacySettings.test.js +26 -6
  399. package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
  400. package/dist/src/ui/hooks/usePromptCompletion.js +2 -2
  401. package/dist/src/ui/hooks/usePromptCompletion.js.map +1 -1
  402. package/dist/src/ui/hooks/useQuotaAndFallback.js +13 -14
  403. package/dist/src/ui/hooks/useQuotaAndFallback.js.map +1 -1
  404. package/dist/src/ui/hooks/useQuotaAndFallback.test.js +55 -48
  405. package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
  406. package/dist/src/ui/hooks/useReactToolScheduler.d.ts +8 -1
  407. package/dist/src/ui/hooks/useReactToolScheduler.js +59 -34
  408. package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
  409. package/dist/src/ui/hooks/useReactToolScheduler.test.d.ts +6 -0
  410. package/dist/src/ui/hooks/useReactToolScheduler.test.js +65 -0
  411. package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -0
  412. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js +2 -2
  413. package/dist/src/ui/hooks/useReverseSearchCompletion.test.js.map +1 -1
  414. package/dist/src/ui/hooks/useSelectionList.js +5 -4
  415. package/dist/src/ui/hooks/useSelectionList.js.map +1 -1
  416. package/dist/src/ui/hooks/useSelectionList.test.js +272 -183
  417. package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
  418. package/dist/src/ui/hooks/useShellHistory.test.js +52 -20
  419. package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
  420. package/dist/src/ui/hooks/useSlashCompletion.js +18 -7
  421. package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
  422. package/dist/src/ui/hooks/useSlashCompletion.test.js +275 -137
  423. package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
  424. package/dist/src/ui/hooks/useTimer.test.js +43 -14
  425. package/dist/src/ui/hooks/useTimer.test.js.map +1 -1
  426. package/dist/src/ui/hooks/useToolScheduler.test.js +226 -242
  427. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  428. package/dist/src/ui/hooks/vim.test.js +235 -355
  429. package/dist/src/ui/hooks/vim.test.js.map +1 -1
  430. package/dist/src/ui/keyMatchers.test.js +30 -3
  431. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  432. package/dist/src/ui/state/extensions.d.ts +1 -0
  433. package/dist/src/ui/state/extensions.js +1 -0
  434. package/dist/src/ui/state/extensions.js.map +1 -1
  435. package/dist/src/ui/themes/ansi-light.js +1 -0
  436. package/dist/src/ui/themes/ansi-light.js.map +1 -1
  437. package/dist/src/ui/themes/ansi.js +1 -0
  438. package/dist/src/ui/themes/ansi.js.map +1 -1
  439. package/dist/src/ui/themes/atom-one-dark.js +2 -0
  440. package/dist/src/ui/themes/atom-one-dark.js.map +1 -1
  441. package/dist/src/ui/themes/ayu-light.js +2 -0
  442. package/dist/src/ui/themes/ayu-light.js.map +1 -1
  443. package/dist/src/ui/themes/ayu.js +2 -0
  444. package/dist/src/ui/themes/ayu.js.map +1 -1
  445. package/dist/src/ui/themes/color-utils.d.ts +1 -0
  446. package/dist/src/ui/themes/color-utils.js +6 -0
  447. package/dist/src/ui/themes/color-utils.js.map +1 -1
  448. package/dist/src/ui/themes/color-utils.test.js +13 -1
  449. package/dist/src/ui/themes/color-utils.test.js.map +1 -1
  450. package/dist/src/ui/themes/dracula.js +2 -0
  451. package/dist/src/ui/themes/dracula.js.map +1 -1
  452. package/dist/src/ui/themes/github-dark.js +2 -0
  453. package/dist/src/ui/themes/github-dark.js.map +1 -1
  454. package/dist/src/ui/themes/github-light.js +2 -0
  455. package/dist/src/ui/themes/github-light.js.map +1 -1
  456. package/dist/src/ui/themes/googlecode.js +2 -0
  457. package/dist/src/ui/themes/googlecode.js.map +1 -1
  458. package/dist/src/ui/themes/no-color.js +3 -0
  459. package/dist/src/ui/themes/no-color.js.map +1 -1
  460. package/dist/src/ui/themes/semantic-tokens.d.ts +2 -0
  461. package/dist/src/ui/themes/semantic-tokens.js +6 -0
  462. package/dist/src/ui/themes/semantic-tokens.js.map +1 -1
  463. package/dist/src/ui/themes/shades-of-purple.js +2 -0
  464. package/dist/src/ui/themes/shades-of-purple.js.map +1 -1
  465. package/dist/src/ui/themes/theme.d.ts +3 -0
  466. package/dist/src/ui/themes/theme.js +14 -3
  467. package/dist/src/ui/themes/theme.js.map +1 -1
  468. package/dist/src/ui/themes/theme.test.js +67 -1
  469. package/dist/src/ui/themes/theme.test.js.map +1 -1
  470. package/dist/src/ui/themes/xcode.js +2 -0
  471. package/dist/src/ui/themes/xcode.js.map +1 -1
  472. package/dist/src/ui/types.d.ts +3 -1
  473. package/dist/src/ui/types.js +2 -0
  474. package/dist/src/ui/types.js.map +1 -1
  475. package/dist/src/ui/utils/CodeColorizer.js +2 -1
  476. package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
  477. package/dist/src/ui/utils/InlineMarkdownRenderer.d.ts +1 -0
  478. package/dist/src/ui/utils/InlineMarkdownRenderer.js +11 -10
  479. package/dist/src/ui/utils/InlineMarkdownRenderer.js.map +1 -1
  480. package/dist/src/ui/utils/MarkdownDisplay.js +11 -9
  481. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  482. package/dist/src/ui/utils/clipboardUtils.js +2 -2
  483. package/dist/src/ui/utils/clipboardUtils.js.map +1 -1
  484. package/dist/src/ui/utils/input.d.ts +17 -0
  485. package/dist/src/ui/utils/input.js +51 -0
  486. package/dist/src/ui/utils/input.js.map +1 -0
  487. package/dist/src/ui/utils/input.test.d.ts +6 -0
  488. package/dist/src/ui/utils/input.test.js +44 -0
  489. package/dist/src/ui/utils/input.test.js.map +1 -0
  490. package/dist/src/ui/utils/kittyProtocolDetector.js +13 -4
  491. package/dist/src/ui/utils/kittyProtocolDetector.js.map +1 -1
  492. package/dist/src/ui/utils/mouse.d.ts +31 -0
  493. package/dist/src/ui/utils/mouse.js +164 -0
  494. package/dist/src/ui/utils/mouse.js.map +1 -0
  495. package/dist/src/ui/utils/mouse.test.d.ts +6 -0
  496. package/dist/src/ui/utils/mouse.test.js +131 -0
  497. package/dist/src/ui/utils/mouse.test.js.map +1 -0
  498. package/dist/src/ui/utils/textOutput.d.ts +25 -0
  499. package/dist/src/ui/utils/textOutput.js +49 -0
  500. package/dist/src/ui/utils/textOutput.js.map +1 -0
  501. package/dist/src/ui/utils/textOutput.test.d.ts +6 -0
  502. package/dist/src/ui/utils/textOutput.test.js +79 -0
  503. package/dist/src/ui/utils/textOutput.test.js.map +1 -0
  504. package/dist/src/ui/utils/updateCheck.d.ts +7 -1
  505. package/dist/src/ui/utils/updateCheck.js +33 -29
  506. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  507. package/dist/src/ui/utils/updateCheck.test.js +24 -50
  508. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  509. package/dist/src/utils/commentJson.js +2 -2
  510. package/dist/src/utils/commentJson.js.map +1 -1
  511. package/dist/src/utils/commentJson.test.js +7 -6
  512. package/dist/src/utils/commentJson.test.js.map +1 -1
  513. package/dist/src/utils/envVarResolver.d.ts +2 -2
  514. package/dist/src/utils/envVarResolver.js +10 -7
  515. package/dist/src/utils/envVarResolver.js.map +1 -1
  516. package/dist/src/utils/events.d.ts +11 -2
  517. package/dist/src/utils/events.js +1 -0
  518. package/dist/src/utils/events.js.map +1 -1
  519. package/dist/src/utils/handleAutoUpdate.js +9 -3
  520. package/dist/src/utils/handleAutoUpdate.js.map +1 -1
  521. package/dist/src/utils/sandbox.js +16 -18
  522. package/dist/src/utils/sandbox.js.map +1 -1
  523. package/dist/src/utils/version.js +6 -2
  524. package/dist/src/utils/version.js.map +1 -1
  525. package/dist/src/zed-integration/acp.js +2 -1
  526. package/dist/src/zed-integration/acp.js.map +1 -1
  527. package/dist/src/zed-integration/schema.d.ts +4 -4
  528. package/dist/src/zed-integration/zedIntegration.d.ts +2 -2
  529. package/dist/src/zed-integration/zedIntegration.js +12 -19
  530. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  531. package/dist/tsconfig.tsbuildinfo +1 -1
  532. package/package.json +18 -14
  533. package/dist/src/config/policy.test.js +0 -360
  534. package/dist/src/config/policy.test.js.map +0 -1
  535. package/dist/src/utils/package.d.ts +0 -12
  536. package/dist/src/utils/package.js +0 -24
  537. package/dist/src/utils/package.js.map +0 -1
  538. /package/dist/src/{config/policy.test.d.ts → commands/extensions/validate.test.d.ts} +0 -0
@@ -7,14 +7,16 @@ import { vi } from 'vitest';
7
7
  import * as fs from 'node:fs';
8
8
  import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
- import { createHash } from 'node:crypto';
11
- import { EXTENSIONS_CONFIG_FILENAME, ExtensionStorage, INSTALL_METADATA_FILENAME, INSTALL_WARNING_MESSAGE, disableExtension, enableExtension, installOrUpdateExtension, loadExtension, loadExtensionConfig, loadExtensions, uninstallExtension, hashValue, } from './extension.js';
12
- import { CELL_DIR, ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, } from '@google/gemini-cli-core';
13
- import { SettingScope } from './settings.js';
14
- import { isWorkspaceTrusted } from './trustedFolders.js';
10
+ import { ExtensionUninstallEvent, ExtensionDisableEvent, ExtensionEnableEvent, KeychainTokenStorage, } from '@google/gemini-cli-core';
11
+ import { loadSettings, SettingScope } from './settings.js';
12
+ import { isWorkspaceTrusted, resetTrustedFoldersForTesting, } from './trustedFolders.js';
15
13
  import { createExtension } from '../test-utils/createExtension.js';
16
14
  import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
17
15
  import { join } from 'node:path';
16
+ import { EXTENSIONS_CONFIG_FILENAME, EXTENSIONS_DIRECTORY_NAME, INSTALL_METADATA_FILENAME, } from './extensions/variables.js';
17
+ import { hashValue, ExtensionManager } from './extension-manager.js';
18
+ import { ExtensionStorage } from './extensions/storage.js';
19
+ import { INSTALL_WARNING_MESSAGE } from './extensions/consent.js';
18
20
  const mockGit = {
19
21
  clone: vi.fn(),
20
22
  getRemotes: vi.fn(),
@@ -40,11 +42,12 @@ vi.mock('simple-git', () => ({
40
42
  return mockGit;
41
43
  }),
42
44
  }));
45
+ const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
43
46
  vi.mock('os', async (importOriginal) => {
44
47
  const mockedOs = await importOriginal();
45
48
  return {
46
49
  ...mockedOs,
47
- homedir: vi.fn(),
50
+ homedir: mockHomedir,
48
51
  };
49
52
  });
50
53
  vi.mock('./trustedFolders.js', async (importOriginal) => {
@@ -72,6 +75,13 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
72
75
  ExtensionInstallEvent: vi.fn(),
73
76
  ExtensionUninstallEvent: vi.fn(),
74
77
  ExtensionDisableEvent: vi.fn(),
78
+ KeychainTokenStorage: vi.fn().mockImplementation(() => ({
79
+ getSecret: vi.fn(),
80
+ setSecret: vi.fn(),
81
+ deleteSecret: vi.fn(),
82
+ listSecrets: vi.fn(),
83
+ isAvailable: vi.fn().mockResolvedValue(true),
84
+ })),
75
85
  };
76
86
  });
77
87
  vi.mock('child_process', async (importOriginal) => {
@@ -81,15 +91,43 @@ vi.mock('child_process', async (importOriginal) => {
81
91
  execSync: vi.fn(),
82
92
  };
83
93
  });
84
- const EXTENSIONS_DIRECTORY_NAME = path.join(CELL_DIR, 'extensions');
85
94
  describe('extension tests', () => {
86
95
  let tempHomeDir;
87
96
  let tempWorkspaceDir;
88
97
  let userExtensionsDir;
98
+ let extensionManager;
99
+ let mockRequestConsent;
100
+ let mockPromptForSettings;
101
+ let mockKeychainStorage;
102
+ let keychainData;
89
103
  beforeEach(() => {
104
+ vi.clearAllMocks();
105
+ keychainData = {};
106
+ mockKeychainStorage = {
107
+ getSecret: vi
108
+ .fn()
109
+ .mockImplementation(async (key) => keychainData[key] || null),
110
+ setSecret: vi
111
+ .fn()
112
+ .mockImplementation(async (key, value) => {
113
+ keychainData[key] = value;
114
+ }),
115
+ deleteSecret: vi.fn().mockImplementation(async (key) => {
116
+ delete keychainData[key];
117
+ }),
118
+ listSecrets: vi
119
+ .fn()
120
+ .mockImplementation(async () => Object.keys(keychainData)),
121
+ isAvailable: vi.fn().mockResolvedValue(true),
122
+ };
123
+ KeychainTokenStorage.mockImplementation(() => mockKeychainStorage);
90
124
  tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cell-cli-test-home-'));
91
125
  tempWorkspaceDir = fs.mkdtempSync(path.join(tempHomeDir, 'gemini-cli-test-workspace-'));
92
126
  userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
127
+ mockRequestConsent = vi.fn();
128
+ mockRequestConsent.mockResolvedValue(true);
129
+ mockPromptForSettings = vi.fn();
130
+ mockPromptForSettings.mockResolvedValue('');
93
131
  fs.mkdirSync(userExtensionsDir, { recursive: true });
94
132
  vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
95
133
  vi.mocked(isWorkspaceTrusted).mockReturnValue({
@@ -97,6 +135,13 @@ describe('extension tests', () => {
97
135
  source: undefined,
98
136
  });
99
137
  vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
138
+ extensionManager = new ExtensionManager({
139
+ workspaceDir: tempWorkspaceDir,
140
+ requestConsent: mockRequestConsent,
141
+ requestSetting: mockPromptForSettings,
142
+ settings: loadSettings(tempWorkspaceDir).merged,
143
+ });
144
+ resetTrustedFoldersForTesting();
100
145
  });
101
146
  afterEach(() => {
102
147
  fs.rmSync(tempHomeDir, { recursive: true, force: true });
@@ -104,7 +149,7 @@ describe('extension tests', () => {
104
149
  vi.restoreAllMocks();
105
150
  });
106
151
  describe('loadExtensions', () => {
107
- it('should include extension path in loaded extension', () => {
152
+ it('should include extension path in loaded extension', async () => {
108
153
  const extensionDir = path.join(userExtensionsDir, 'test-extension');
109
154
  fs.mkdirSync(extensionDir, { recursive: true });
110
155
  createExtension({
@@ -112,12 +157,12 @@ describe('extension tests', () => {
112
157
  name: 'test-extension',
113
158
  version: '1.0.0',
114
159
  });
115
- const extensions = loadExtensions(new ExtensionEnablementManager());
160
+ const extensions = await extensionManager.loadExtensions();
116
161
  expect(extensions).toHaveLength(1);
117
162
  expect(extensions[0].path).toBe(extensionDir);
118
163
  expect(extensions[0].name).toBe('test-extension');
119
164
  });
120
- it('should load context file path when GEMINI.md is present', () => {
165
+ it('should load context file path when GEMINI.md is present', async () => {
121
166
  createExtension({
122
167
  extensionsDir: userExtensionsDir,
123
168
  name: 'ext1',
@@ -129,7 +174,7 @@ describe('extension tests', () => {
129
174
  name: 'ext2',
130
175
  version: '2.0.0',
131
176
  });
132
- const extensions = loadExtensions(new ExtensionEnablementManager());
177
+ const extensions = await extensionManager.loadExtensions();
133
178
  expect(extensions).toHaveLength(2);
134
179
  const ext1 = extensions.find((e) => e.name === 'ext1');
135
180
  const ext2 = extensions.find((e) => e.name === 'ext2');
@@ -138,7 +183,7 @@ describe('extension tests', () => {
138
183
  ]);
139
184
  expect(ext2?.contextFiles).toEqual([]);
140
185
  });
141
- it('should load context file path from the extension config', () => {
186
+ it('should load context file path from the extension config', async () => {
142
187
  createExtension({
143
188
  extensionsDir: userExtensionsDir,
144
189
  name: 'ext1',
@@ -146,14 +191,14 @@ describe('extension tests', () => {
146
191
  addContextFile: false,
147
192
  contextFileName: 'my-context-file.md',
148
193
  });
149
- const extensions = loadExtensions(new ExtensionEnablementManager());
194
+ const extensions = await extensionManager.loadExtensions();
150
195
  expect(extensions).toHaveLength(1);
151
196
  const ext1 = extensions.find((e) => e.name === 'ext1');
152
197
  expect(ext1?.contextFiles).toEqual([
153
198
  path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
154
199
  ]);
155
200
  });
156
- it('should annotate disabled extensions', () => {
201
+ it('should annotate disabled extensions', async () => {
157
202
  createExtension({
158
203
  extensionsDir: userExtensionsDir,
159
204
  name: 'disabled-extension',
@@ -164,16 +209,16 @@ describe('extension tests', () => {
164
209
  name: 'enabled-extension',
165
210
  version: '2.0.0',
166
211
  });
167
- const manager = new ExtensionEnablementManager();
168
- disableExtension('disabled-extension', SettingScope.User, manager, tempWorkspaceDir);
169
- const extensions = loadExtensions(manager);
212
+ await extensionManager.loadExtensions();
213
+ await extensionManager.disableExtension('disabled-extension', SettingScope.User);
214
+ const extensions = extensionManager.getExtensions();
170
215
  expect(extensions).toHaveLength(2);
171
216
  expect(extensions[0].name).toBe('disabled-extension');
172
217
  expect(extensions[0].isActive).toBe(false);
173
218
  expect(extensions[1].name).toBe('enabled-extension');
174
219
  expect(extensions[1].isActive).toBe(true);
175
220
  });
176
- it('should hydrate variables', () => {
221
+ it('should hydrate variables', async () => {
177
222
  createExtension({
178
223
  extensionsDir: userExtensionsDir,
179
224
  name: 'test-extension',
@@ -186,7 +231,7 @@ describe('extension tests', () => {
186
231
  },
187
232
  },
188
233
  });
189
- const extensions = loadExtensions(new ExtensionEnablementManager());
234
+ const extensions = await extensionManager.loadExtensions();
190
235
  expect(extensions).toHaveLength(1);
191
236
  const expectedCwd = path.join(userExtensionsDir, 'test-extension', 'server');
192
237
  expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
@@ -199,12 +244,13 @@ describe('extension tests', () => {
199
244
  contextFileName: 'context.md',
200
245
  });
201
246
  fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
202
- const extensionName = await installOrUpdateExtension({
247
+ await extensionManager.loadExtensions();
248
+ const extension = await extensionManager.installOrUpdateExtension({
203
249
  source: sourceExtDir,
204
250
  type: 'link',
205
- }, async (_) => true);
206
- expect(extensionName).toEqual('my-linked-extension');
207
- const extensions = loadExtensions(new ExtensionEnablementManager());
251
+ });
252
+ expect(extension.name).toEqual('my-linked-extension');
253
+ const extensions = extensionManager.getExtensions();
208
254
  expect(extensions).toHaveLength(1);
209
255
  const linkedExt = extensions[0];
210
256
  expect(linkedExt.name).toBe('my-linked-extension');
@@ -217,7 +263,32 @@ describe('extension tests', () => {
217
263
  path.join(sourceExtDir, 'context.md'),
218
264
  ]);
219
265
  });
220
- it('should resolve environment variables in extension configuration', () => {
266
+ it('should hydrate ${extensionPath} correctly for linked extensions', async () => {
267
+ const sourceExtDir = createExtension({
268
+ extensionsDir: tempWorkspaceDir,
269
+ name: 'my-linked-extension-with-path',
270
+ version: '1.0.0',
271
+ mcpServers: {
272
+ 'test-server': {
273
+ command: 'node',
274
+ args: ['${extensionPath}${/}server${/}index.js'],
275
+ cwd: '${extensionPath}${/}server',
276
+ },
277
+ },
278
+ });
279
+ await extensionManager.loadExtensions();
280
+ await extensionManager.installOrUpdateExtension({
281
+ source: sourceExtDir,
282
+ type: 'link',
283
+ });
284
+ const extensions = extensionManager.getExtensions();
285
+ expect(extensions).toHaveLength(1);
286
+ expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(path.join(sourceExtDir, 'server'));
287
+ expect(extensions[0].mcpServers?.['test-server'].args).toEqual([
288
+ path.join(sourceExtDir, 'server', 'index.js'),
289
+ ]);
290
+ });
291
+ it('should resolve environment variables in extension configuration', async () => {
221
292
  process.env['TEST_API_KEY'] = 'test-api-key-123';
222
293
  process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb';
223
294
  try {
@@ -243,7 +314,7 @@ describe('extension tests', () => {
243
314
  },
244
315
  };
245
316
  fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
246
- const extensions = loadExtensions(new ExtensionEnablementManager());
317
+ const extensions = await extensionManager.loadExtensions();
247
318
  expect(extensions).toHaveLength(1);
248
319
  const extension = extensions[0];
249
320
  expect(extension.name).toBe('test-extension');
@@ -260,7 +331,40 @@ describe('extension tests', () => {
260
331
  delete process.env['TEST_DB_URL'];
261
332
  }
262
333
  });
263
- it('should handle missing environment variables gracefully', () => {
334
+ it('should resolve environment variables from an extension .env file', async () => {
335
+ const extDir = createExtension({
336
+ extensionsDir: userExtensionsDir,
337
+ name: 'test-extension',
338
+ version: '1.0.0',
339
+ mcpServers: {
340
+ 'test-server': {
341
+ command: 'node',
342
+ args: ['server.js'],
343
+ env: {
344
+ API_KEY: '$MY_API_KEY',
345
+ STATIC_VALUE: 'no-substitution',
346
+ },
347
+ },
348
+ },
349
+ settings: [
350
+ {
351
+ name: 'My API Key',
352
+ description: 'API key for testing.',
353
+ envVar: 'MY_API_KEY',
354
+ },
355
+ ],
356
+ });
357
+ const envFilePath = path.join(extDir, '.env');
358
+ fs.writeFileSync(envFilePath, 'MY_API_KEY=test-key-from-file\n');
359
+ const extensions = await extensionManager.loadExtensions();
360
+ expect(extensions).toHaveLength(1);
361
+ const extension = extensions[0];
362
+ const serverConfig = extension.mcpServers['test-server'];
363
+ expect(serverConfig.env).toBeDefined();
364
+ expect(serverConfig.env['API_KEY']).toBe('test-key-from-file');
365
+ expect(serverConfig.env['STATIC_VALUE']).toBe('no-substitution');
366
+ });
367
+ it('should handle missing environment variables gracefully', async () => {
264
368
  const userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
265
369
  fs.mkdirSync(userExtensionsDir, { recursive: true });
266
370
  const extDir = path.join(userExtensionsDir, 'test-extension');
@@ -280,7 +384,7 @@ describe('extension tests', () => {
280
384
  },
281
385
  };
282
386
  fs.writeFileSync(path.join(extDir, EXTENSIONS_CONFIG_FILENAME), JSON.stringify(extensionConfig));
283
- const extensions = loadExtensions(new ExtensionEnablementManager());
387
+ const extensions = await extensionManager.loadExtensions();
284
388
  expect(extensions).toHaveLength(1);
285
389
  const extension = extensions[0];
286
390
  const serverConfig = extension.mcpServers['test-server'];
@@ -288,7 +392,7 @@ describe('extension tests', () => {
288
392
  expect(serverConfig.env['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR');
289
393
  expect(serverConfig.env['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
290
394
  });
291
- it('should skip extensions with invalid JSON and log a warning', () => {
395
+ it('should skip extensions with invalid JSON and log a warning', async () => {
292
396
  const consoleSpy = vi
293
397
  .spyOn(console, 'error')
294
398
  .mockImplementation(() => { });
@@ -303,14 +407,13 @@ describe('extension tests', () => {
303
407
  fs.mkdirSync(badExtDir);
304
408
  const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
305
409
  fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
306
- const extensions = loadExtensions(new ExtensionEnablementManager());
410
+ const extensions = await extensionManager.loadExtensions();
307
411
  expect(extensions).toHaveLength(1);
308
412
  expect(extensions[0].name).toBe('good-ext');
309
- expect(consoleSpy).toHaveBeenCalledOnce();
310
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
413
+ expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`));
311
414
  consoleSpy.mockRestore();
312
415
  });
313
- it('should skip extensions with missing name and log a warning', () => {
416
+ it('should skip extensions with missing name and log a warning', async () => {
314
417
  const consoleSpy = vi
315
418
  .spyOn(console, 'error')
316
419
  .mockImplementation(() => { });
@@ -325,14 +428,13 @@ describe('extension tests', () => {
325
428
  fs.mkdirSync(badExtDir);
326
429
  const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
327
430
  fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
328
- const extensions = loadExtensions(new ExtensionEnablementManager());
431
+ const extensions = await extensionManager.loadExtensions();
329
432
  expect(extensions).toHaveLength(1);
330
433
  expect(extensions[0].name).toBe('good-ext');
331
- expect(consoleSpy).toHaveBeenCalledOnce();
332
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
434
+ expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining(`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`));
333
435
  consoleSpy.mockRestore();
334
436
  });
335
- it('should filter trust out of mcp servers', () => {
437
+ it('should filter trust out of mcp servers', async () => {
336
438
  createExtension({
337
439
  extensionsDir: userExtensionsDir,
338
440
  name: 'test-extension',
@@ -345,31 +447,28 @@ describe('extension tests', () => {
345
447
  },
346
448
  },
347
449
  });
348
- const extensions = loadExtensions(new ExtensionEnablementManager());
450
+ const extensions = await extensionManager.loadExtensions();
349
451
  expect(extensions).toHaveLength(1);
350
452
  expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
351
453
  });
352
- it('should throw an error for invalid extension names', () => {
454
+ it('should throw an error for invalid extension names', async () => {
353
455
  const consoleSpy = vi
354
456
  .spyOn(console, 'error')
355
457
  .mockImplementation(() => { });
356
- const badExtDir = createExtension({
458
+ createExtension({
357
459
  extensionsDir: userExtensionsDir,
358
460
  name: 'bad_name',
359
461
  version: '1.0.0',
360
462
  });
361
- const extension = loadExtension({
362
- extensionDir: badExtDir,
363
- workspaceDir: tempWorkspaceDir,
364
- extensionEnablementManager: new ExtensionEnablementManager(),
365
- });
366
- expect(extension).toBeNull();
463
+ const extensions = await extensionManager.loadExtensions();
464
+ const extension = extensions.find((e) => e.name === 'bad_name');
465
+ expect(extension).toBeUndefined();
367
466
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid extension name: "bad_name"'));
368
467
  consoleSpy.mockRestore();
369
468
  });
370
469
  describe('id generation', () => {
371
- it('should generate id from source for non-github git urls', () => {
372
- const extensionDir = createExtension({
470
+ it('should generate id from source for non-github git urls', async () => {
471
+ createExtension({
373
472
  extensionsDir: userExtensionsDir,
374
473
  name: 'my-ext',
375
474
  version: '1.0.0',
@@ -378,18 +477,12 @@ describe('extension tests', () => {
378
477
  source: 'http://somehost.com/foo/bar',
379
478
  },
380
479
  });
381
- const extension = loadExtension({
382
- extensionDir,
383
- workspaceDir: tempWorkspaceDir,
384
- extensionEnablementManager: new ExtensionEnablementManager(),
385
- });
386
- const expectedHash = createHash('sha256')
387
- .update('http://somehost.com/foo/bar')
388
- .digest('hex');
389
- expect(extension?.id).toBe(expectedHash);
480
+ const extensions = await extensionManager.loadExtensions();
481
+ const extension = extensions.find((e) => e.name === 'my-ext');
482
+ expect(extension?.id).toBe(hashValue('http://somehost.com/foo/bar'));
390
483
  });
391
- it('should generate id from owner/repo for github http urls', () => {
392
- const extensionDir = createExtension({
484
+ it('should generate id from owner/repo for github http urls', async () => {
485
+ createExtension({
393
486
  extensionsDir: userExtensionsDir,
394
487
  name: 'my-ext',
395
488
  version: '1.0.0',
@@ -398,18 +491,12 @@ describe('extension tests', () => {
398
491
  source: 'http://github.com/foo/bar',
399
492
  },
400
493
  });
401
- const extension = loadExtension({
402
- extensionDir,
403
- workspaceDir: tempWorkspaceDir,
404
- extensionEnablementManager: new ExtensionEnablementManager(),
405
- });
406
- const expectedHash = createHash('sha256')
407
- .update('https://github.com/foo/bar')
408
- .digest('hex');
409
- expect(extension?.id).toBe(expectedHash);
494
+ const extensions = await extensionManager.loadExtensions();
495
+ const extension = extensions.find((e) => e.name === 'my-ext');
496
+ expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
410
497
  });
411
- it('should generate id from owner/repo for github ssh urls', () => {
412
- const extensionDir = createExtension({
498
+ it('should generate id from owner/repo for github ssh urls', async () => {
499
+ createExtension({
413
500
  extensionsDir: userExtensionsDir,
414
501
  name: 'my-ext',
415
502
  version: '1.0.0',
@@ -418,18 +505,12 @@ describe('extension tests', () => {
418
505
  source: 'git@github.com:foo/bar',
419
506
  },
420
507
  });
421
- const extension = loadExtension({
422
- extensionDir,
423
- workspaceDir: tempWorkspaceDir,
424
- extensionEnablementManager: new ExtensionEnablementManager(),
425
- });
426
- const expectedHash = createHash('sha256')
427
- .update('https://github.com/foo/bar')
428
- .digest('hex');
429
- expect(extension?.id).toBe(expectedHash);
508
+ const extensions = await extensionManager.loadExtensions();
509
+ const extension = extensions.find((e) => e.name === 'my-ext');
510
+ expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
430
511
  });
431
- it('should generate id from source for github-release extension', () => {
432
- const extensionDir = createExtension({
512
+ it('should generate id from source for github-release extension', async () => {
513
+ createExtension({
433
514
  extensionsDir: userExtensionsDir,
434
515
  name: 'my-ext',
435
516
  version: '1.0.0',
@@ -438,18 +519,12 @@ describe('extension tests', () => {
438
519
  source: 'https://github.com/foo/bar',
439
520
  },
440
521
  });
441
- const extension = loadExtension({
442
- extensionDir,
443
- workspaceDir: tempWorkspaceDir,
444
- extensionEnablementManager: new ExtensionEnablementManager(),
445
- });
446
- const expectedHash = createHash('sha256')
447
- .update('https://github.com/foo/bar')
448
- .digest('hex');
449
- expect(extension?.id).toBe(expectedHash);
522
+ const extensions = await extensionManager.loadExtensions();
523
+ const extension = extensions.find((e) => e.name === 'my-ext');
524
+ expect(extension?.id).toBe(hashValue('https://github.com/foo/bar'));
450
525
  });
451
- it('should generate id from the original source for local extension', () => {
452
- const extensionDir = createExtension({
526
+ it('should generate id from the original source for local extension', async () => {
527
+ createExtension({
453
528
  extensionsDir: userExtensionsDir,
454
529
  name: 'local-ext-name',
455
530
  version: '1.0.0',
@@ -458,15 +533,9 @@ describe('extension tests', () => {
458
533
  source: '/some/path',
459
534
  },
460
535
  });
461
- const extension = loadExtension({
462
- extensionDir,
463
- workspaceDir: tempWorkspaceDir,
464
- extensionEnablementManager: new ExtensionEnablementManager(),
465
- });
466
- const expectedHash = createHash('sha256')
467
- .update('/some/path')
468
- .digest('hex');
469
- expect(extension?.id).toBe(expectedHash);
536
+ const extensions = await extensionManager.loadExtensions();
537
+ const extension = extensions.find((e) => e.name === 'local-ext-name');
538
+ expect(extension?.id).toBe(hashValue('/some/path'));
470
539
  });
471
540
  it('should generate id from the original source for linked extensions', async () => {
472
541
  const extDevelopmentDir = path.join(tempHomeDir, 'local_extensions');
@@ -475,35 +544,25 @@ describe('extension tests', () => {
475
544
  name: 'link-ext-name',
476
545
  version: '1.0.0',
477
546
  });
478
- const extensionName = await installOrUpdateExtension({
547
+ await extensionManager.loadExtensions();
548
+ await extensionManager.installOrUpdateExtension({
479
549
  type: 'link',
480
550
  source: actualExtensionDir,
481
- }, async () => true, tempWorkspaceDir);
482
- const extension = loadExtension({
483
- extensionDir: new ExtensionStorage(extensionName).getExtensionDir(),
484
- workspaceDir: tempWorkspaceDir,
485
- extensionEnablementManager: new ExtensionEnablementManager(),
486
551
  });
487
- const expectedHash = createHash('sha256')
488
- .update(actualExtensionDir)
489
- .digest('hex');
490
- expect(extension?.id).toBe(expectedHash);
552
+ const extension = extensionManager
553
+ .getExtensions()
554
+ .find((e) => e.name === 'link-ext-name');
555
+ expect(extension?.id).toBe(hashValue(actualExtensionDir));
491
556
  });
492
- it('should generate id from name for extension with no install metadata', () => {
493
- const extensionDir = createExtension({
557
+ it('should generate id from name for extension with no install metadata', async () => {
558
+ createExtension({
494
559
  extensionsDir: userExtensionsDir,
495
560
  name: 'no-meta-name',
496
561
  version: '1.0.0',
497
562
  });
498
- const extension = loadExtension({
499
- extensionDir,
500
- workspaceDir: tempWorkspaceDir,
501
- extensionEnablementManager: new ExtensionEnablementManager(),
502
- });
503
- const expectedHash = createHash('sha256')
504
- .update('no-meta-name')
505
- .digest('hex');
506
- expect(extension?.id).toBe(expectedHash);
563
+ const extensions = await extensionManager.loadExtensions();
564
+ const extension = extensions.find((e) => e.name === 'no-meta-name');
565
+ expect(extension?.id).toBe(hashValue('no-meta-name'));
507
566
  });
508
567
  });
509
568
  });
@@ -516,7 +575,11 @@ describe('extension tests', () => {
516
575
  });
517
576
  const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
518
577
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
519
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
578
+ await extensionManager.loadExtensions();
579
+ await extensionManager.installOrUpdateExtension({
580
+ source: sourceExtDir,
581
+ type: 'local',
582
+ });
520
583
  expect(fs.existsSync(targetExtDir)).toBe(true);
521
584
  expect(fs.existsSync(metadataPath)).toBe(true);
522
585
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -532,14 +595,24 @@ describe('extension tests', () => {
532
595
  name: 'my-local-extension',
533
596
  version: '1.0.0',
534
597
  });
535
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
536
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow('Extension "my-local-extension" is already installed. Please uninstall it first.');
598
+ await extensionManager.loadExtensions();
599
+ await extensionManager.installOrUpdateExtension({
600
+ source: sourceExtDir,
601
+ type: 'local',
602
+ });
603
+ await expect(extensionManager.installOrUpdateExtension({
604
+ source: sourceExtDir,
605
+ type: 'local',
606
+ })).rejects.toThrow('Extension "my-local-extension" is already installed. Please uninstall it first.');
537
607
  });
538
608
  it('should throw an error and cleanup if gemini-extension.json is missing', async () => {
539
609
  const sourceExtDir = path.join(tempHomeDir, 'bad-extension');
540
610
  fs.mkdirSync(sourceExtDir, { recursive: true });
541
611
  const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
542
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(`Configuration file not found at ${configPath}`);
612
+ await expect(extensionManager.installOrUpdateExtension({
613
+ source: sourceExtDir,
614
+ type: 'local',
615
+ })).rejects.toThrow(`Configuration file not found at ${configPath}`);
543
616
  const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
544
617
  expect(fs.existsSync(targetExtDir)).toBe(false);
545
618
  });
@@ -548,7 +621,10 @@ describe('extension tests', () => {
548
621
  fs.mkdirSync(sourceExtDir, { recursive: true });
549
622
  const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
550
623
  fs.writeFileSync(configPath, '{ "name": "bad-json", "version": "1.0.0"'); // Malformed JSON
551
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(new RegExp(`^Failed to load extension config from ${configPath.replace(/\\/g, '\\\\')}`));
624
+ await expect(extensionManager.installOrUpdateExtension({
625
+ source: sourceExtDir,
626
+ type: 'local',
627
+ })).rejects.toThrow(new RegExp(`^Failed to load extension config from ${configPath.replace(/\\/g, '\\\\')}`));
552
628
  });
553
629
  it('should throw an error for missing name in gemini-extension.json', async () => {
554
630
  const sourceExtDir = createExtension({
@@ -559,7 +635,10 @@ describe('extension tests', () => {
559
635
  const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
560
636
  // Overwrite with invalid config
561
637
  fs.writeFileSync(configPath, JSON.stringify({ version: '1.0.0' }));
562
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow(`Invalid configuration in ${configPath}: missing "name"`);
638
+ await expect(extensionManager.installOrUpdateExtension({
639
+ source: sourceExtDir,
640
+ type: 'local',
641
+ })).rejects.toThrow(`Invalid configuration in ${configPath}: missing "name"`);
563
642
  });
564
643
  it('should install an extension from a git URL', async () => {
565
644
  const gitUrl = 'https://somehost.com/somerepo.git';
@@ -578,7 +657,11 @@ describe('extension tests', () => {
578
657
  failureReason: 'no release data',
579
658
  type: 'github-release',
580
659
  });
581
- await installOrUpdateExtension({ source: gitUrl, type: 'git' }, async (_) => true);
660
+ await extensionManager.loadExtensions();
661
+ await extensionManager.installOrUpdateExtension({
662
+ source: gitUrl,
663
+ type: 'git',
664
+ });
582
665
  expect(fs.existsSync(targetExtDir)).toBe(true);
583
666
  expect(fs.existsSync(metadataPath)).toBe(true);
584
667
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -596,7 +679,11 @@ describe('extension tests', () => {
596
679
  const targetExtDir = path.join(userExtensionsDir, 'my-linked-extension');
597
680
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
598
681
  const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
599
- await installOrUpdateExtension({ source: sourceExtDir, type: 'link' }, async (_) => true);
682
+ await extensionManager.loadExtensions();
683
+ await extensionManager.installOrUpdateExtension({
684
+ source: sourceExtDir,
685
+ type: 'link',
686
+ });
600
687
  expect(fs.existsSync(targetExtDir)).toBe(true);
601
688
  expect(fs.existsSync(metadataPath)).toBe(true);
602
689
  expect(fs.existsSync(configPath)).toBe(false);
@@ -607,6 +694,65 @@ describe('extension tests', () => {
607
694
  });
608
695
  fs.rmSync(targetExtDir, { recursive: true, force: true });
609
696
  });
697
+ it('should prompt for trust if workspace is not trusted', async () => {
698
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
699
+ isTrusted: false,
700
+ source: undefined,
701
+ });
702
+ const sourceExtDir = createExtension({
703
+ extensionsDir: tempHomeDir,
704
+ name: 'my-local-extension',
705
+ version: '1.0.0',
706
+ });
707
+ await extensionManager.loadExtensions();
708
+ await extensionManager.installOrUpdateExtension({
709
+ source: sourceExtDir,
710
+ type: 'local',
711
+ });
712
+ expect(mockRequestConsent).toHaveBeenCalledWith(`The current workspace at "${tempWorkspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`);
713
+ });
714
+ it('should not install if user denies trust', async () => {
715
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
716
+ isTrusted: false,
717
+ source: undefined,
718
+ });
719
+ mockRequestConsent.mockImplementation(async (message) => {
720
+ if (message.includes('is not trusted. Do you want to trust this workspace to install extensions?')) {
721
+ return false;
722
+ }
723
+ return true;
724
+ });
725
+ const sourceExtDir = createExtension({
726
+ extensionsDir: tempHomeDir,
727
+ name: 'my-local-extension',
728
+ version: '1.0.0',
729
+ });
730
+ await extensionManager.loadExtensions();
731
+ await expect(extensionManager.installOrUpdateExtension({
732
+ source: sourceExtDir,
733
+ type: 'local',
734
+ })).rejects.toThrow(`Could not install extension because the current workspace at ${tempWorkspaceDir} is not trusted.`);
735
+ });
736
+ it('should add the workspace to trusted folders if user consents', async () => {
737
+ const trustedFoldersPath = path.join(tempHomeDir, '.cell-cli', 'trustedFolders.json');
738
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
739
+ isTrusted: false,
740
+ source: undefined,
741
+ });
742
+ const sourceExtDir = createExtension({
743
+ extensionsDir: tempHomeDir,
744
+ name: 'my-local-extension',
745
+ version: '1.0.0',
746
+ });
747
+ await extensionManager.loadExtensions();
748
+ await extensionManager.installOrUpdateExtension({
749
+ source: sourceExtDir,
750
+ type: 'local',
751
+ });
752
+ expect(fs.existsSync(trustedFoldersPath)).toBe(true);
753
+ const trustedFolders = JSON.parse(fs.readFileSync(trustedFoldersPath, 'utf-8'));
754
+ expect(trustedFolders[tempWorkspaceDir]).toBe('TRUST_FOLDER');
755
+ });
610
756
  describe.each([true, false])('with previous extension config: %s', (isUpdate) => {
611
757
  let sourceExtDir;
612
758
  beforeEach(async () => {
@@ -615,14 +761,18 @@ describe('extension tests', () => {
615
761
  name: 'my-local-extension',
616
762
  version: '1.1.0',
617
763
  });
764
+ await extensionManager.loadExtensions();
618
765
  if (isUpdate) {
619
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true);
766
+ await extensionManager.installOrUpdateExtension({
767
+ source: sourceExtDir,
768
+ type: 'local',
769
+ });
620
770
  }
621
771
  // Clears out any calls to mocks from the above function calls.
622
772
  vi.clearAllMocks();
623
773
  });
624
774
  it(`should log an ${isUpdate ? 'update' : 'install'} event to clearcut on success`, async () => {
625
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true, undefined, isUpdate
775
+ await extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, isUpdate
626
776
  ? {
627
777
  name: 'my-local-extension',
628
778
  version: '1.0.0',
@@ -640,7 +790,7 @@ describe('extension tests', () => {
640
790
  it(`should ${isUpdate ? 'not ' : ''} alter the extension enablement configuration`, async () => {
641
791
  const enablementManager = new ExtensionEnablementManager();
642
792
  enablementManager.enable('my-local-extension', true, '/some/scope');
643
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true, undefined, isUpdate
793
+ await extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, isUpdate
644
794
  ? {
645
795
  name: 'my-local-extension',
646
796
  version: '1.0.0',
@@ -673,9 +823,13 @@ describe('extension tests', () => {
673
823
  },
674
824
  },
675
825
  });
676
- const mockRequestConsent = vi.fn();
677
- mockRequestConsent.mockResolvedValue(true);
678
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, mockRequestConsent)).resolves.toBe('my-local-extension');
826
+ await extensionManager.loadExtensions();
827
+ await expect(extensionManager.installOrUpdateExtension({
828
+ source: sourceExtDir,
829
+ type: 'local',
830
+ })).resolves.toMatchObject({
831
+ name: 'my-local-extension',
832
+ });
679
833
  expect(mockRequestConsent).toHaveBeenCalledWith(`Installing extension "my-local-extension".
680
834
  ${INSTALL_WARNING_MESSAGE}
681
835
  This extension will run the following MCP servers:
@@ -694,7 +848,11 @@ This extension will run the following MCP servers:
694
848
  },
695
849
  },
696
850
  });
697
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => true)).resolves.toBe('my-local-extension');
851
+ await extensionManager.loadExtensions();
852
+ await expect(extensionManager.installOrUpdateExtension({
853
+ source: sourceExtDir,
854
+ type: 'local',
855
+ })).resolves.toMatchObject({ name: 'my-local-extension' });
698
856
  });
699
857
  it('should cancel installation if user declines prompt for local extension with mcp servers', async () => {
700
858
  const sourceExtDir = createExtension({
@@ -708,7 +866,12 @@ This extension will run the following MCP servers:
708
866
  },
709
867
  },
710
868
  });
711
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => false)).rejects.toThrow('Installation cancelled for "my-local-extension".');
869
+ mockRequestConsent.mockResolvedValue(false);
870
+ await extensionManager.loadExtensions();
871
+ await expect(extensionManager.installOrUpdateExtension({
872
+ source: sourceExtDir,
873
+ type: 'local',
874
+ })).rejects.toThrow('Installation cancelled for "my-local-extension".');
712
875
  });
713
876
  it('should save the autoUpdate flag to the install metadata', async () => {
714
877
  const sourceExtDir = createExtension({
@@ -718,11 +881,12 @@ This extension will run the following MCP servers:
718
881
  });
719
882
  const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
720
883
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
721
- await installOrUpdateExtension({
884
+ await extensionManager.loadExtensions();
885
+ await extensionManager.installOrUpdateExtension({
722
886
  source: sourceExtDir,
723
887
  type: 'local',
724
888
  autoUpdate: true,
725
- }, async (_) => true);
889
+ });
726
890
  expect(fs.existsSync(targetExtDir)).toBe(true);
727
891
  expect(fs.existsSync(metadataPath)).toBe(true);
728
892
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -745,18 +909,156 @@ This extension will run the following MCP servers:
745
909
  },
746
910
  },
747
911
  });
748
- const mockRequestConsent = vi.fn();
749
- // Install it and force consent first.
750
- await installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async () => true);
912
+ await extensionManager.loadExtensions();
913
+ // Install it with hard coded consent first.
914
+ await extensionManager.installOrUpdateExtension({
915
+ source: sourceExtDir,
916
+ type: 'local',
917
+ });
918
+ expect(mockRequestConsent).toHaveBeenCalledOnce();
751
919
  // Now update it without changing anything.
752
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, mockRequestConsent, process.cwd(),
920
+ await expect(extensionManager.installOrUpdateExtension({ source: sourceExtDir, type: 'local' },
753
921
  // Provide its own existing config as the previous config.
754
- await loadExtensionConfig({
755
- extensionDir: sourceExtDir,
756
- workspaceDir: process.cwd(),
757
- extensionEnablementManager: new ExtensionEnablementManager(),
758
- }))).resolves.toBe('my-local-extension');
759
- expect(mockRequestConsent).not.toHaveBeenCalled();
922
+ await extensionManager.loadExtensionConfig(sourceExtDir))).resolves.toMatchObject({ name: 'my-local-extension' });
923
+ // Still only called once
924
+ expect(mockRequestConsent).toHaveBeenCalledOnce();
925
+ });
926
+ it('should prompt for settings if promptForSettings', async () => {
927
+ const sourceExtDir = createExtension({
928
+ extensionsDir: tempHomeDir,
929
+ name: 'my-local-extension',
930
+ version: '1.0.0',
931
+ settings: [
932
+ {
933
+ name: 'API Key',
934
+ description: 'Your API key for the service.',
935
+ envVar: 'MY_API_KEY',
936
+ },
937
+ ],
938
+ });
939
+ await extensionManager.loadExtensions();
940
+ await extensionManager.installOrUpdateExtension({
941
+ source: sourceExtDir,
942
+ type: 'local',
943
+ });
944
+ expect(mockPromptForSettings).toHaveBeenCalled();
945
+ });
946
+ it('should not prompt for settings if promptForSettings is false', async () => {
947
+ const sourceExtDir = createExtension({
948
+ extensionsDir: tempHomeDir,
949
+ name: 'my-local-extension',
950
+ version: '1.0.0',
951
+ settings: [
952
+ {
953
+ name: 'API Key',
954
+ description: 'Your API key for the service.',
955
+ envVar: 'MY_API_KEY',
956
+ },
957
+ ],
958
+ });
959
+ extensionManager = new ExtensionManager({
960
+ workspaceDir: tempWorkspaceDir,
961
+ requestConsent: mockRequestConsent,
962
+ requestSetting: null,
963
+ settings: loadSettings(tempWorkspaceDir).merged,
964
+ });
965
+ await extensionManager.loadExtensions();
966
+ await extensionManager.installOrUpdateExtension({
967
+ source: sourceExtDir,
968
+ type: 'local',
969
+ });
970
+ });
971
+ it('should only prompt for new settings on update, and preserve old settings', async () => {
972
+ // 1. Create and install the "old" version of the extension.
973
+ const oldSourceExtDir = createExtension({
974
+ extensionsDir: tempHomeDir, // Create it in a temp location first
975
+ name: 'my-local-extension',
976
+ version: '1.0.0',
977
+ settings: [
978
+ {
979
+ name: 'API Key',
980
+ description: 'Your API key for the service.',
981
+ envVar: 'MY_API_KEY',
982
+ },
983
+ ],
984
+ });
985
+ mockPromptForSettings.mockResolvedValueOnce('old-api-key');
986
+ await extensionManager.loadExtensions();
987
+ // Install it so it exists in the userExtensionsDir
988
+ await extensionManager.installOrUpdateExtension({
989
+ source: oldSourceExtDir,
990
+ type: 'local',
991
+ });
992
+ const envPath = new ExtensionStorage('my-local-extension').getEnvFilePath();
993
+ expect(fs.existsSync(envPath)).toBe(true);
994
+ let envContent = fs.readFileSync(envPath, 'utf-8');
995
+ expect(envContent).toContain('MY_API_KEY=old-api-key');
996
+ expect(mockPromptForSettings).toHaveBeenCalledTimes(1);
997
+ // 2. Create the "new" version of the extension in a new source directory.
998
+ const newSourceExtDir = createExtension({
999
+ extensionsDir: path.join(tempHomeDir, 'new-source'), // Another temp location
1000
+ name: 'my-local-extension', // Same name
1001
+ version: '1.1.0', // New version
1002
+ settings: [
1003
+ {
1004
+ name: 'API Key',
1005
+ description: 'Your API key for the service.',
1006
+ envVar: 'MY_API_KEY',
1007
+ },
1008
+ {
1009
+ name: 'New Setting',
1010
+ description: 'A new setting.',
1011
+ envVar: 'NEW_SETTING',
1012
+ },
1013
+ ],
1014
+ });
1015
+ const previousExtensionConfig = extensionManager.loadExtensionConfig(path.join(userExtensionsDir, 'my-local-extension'));
1016
+ mockPromptForSettings.mockResolvedValueOnce('new-setting-value');
1017
+ // 3. Call installOrUpdateExtension to perform the update.
1018
+ await extensionManager.installOrUpdateExtension({ source: newSourceExtDir, type: 'local' }, previousExtensionConfig);
1019
+ expect(mockPromptForSettings).toHaveBeenCalledTimes(2);
1020
+ expect(mockPromptForSettings).toHaveBeenCalledWith(expect.objectContaining({ name: 'New Setting' }));
1021
+ expect(fs.existsSync(envPath)).toBe(true);
1022
+ envContent = fs.readFileSync(envPath, 'utf-8');
1023
+ expect(envContent).toContain('MY_API_KEY=old-api-key');
1024
+ expect(envContent).toContain('NEW_SETTING=new-setting-value');
1025
+ });
1026
+ it('should fail auto-update if settings have changed', async () => {
1027
+ // 1. Install initial version with autoUpdate: true
1028
+ const oldSourceExtDir = createExtension({
1029
+ extensionsDir: tempHomeDir,
1030
+ name: 'my-auto-update-ext',
1031
+ version: '1.0.0',
1032
+ settings: [
1033
+ {
1034
+ name: 'OLD_SETTING',
1035
+ envVar: 'OLD_SETTING',
1036
+ description: 'An old setting',
1037
+ },
1038
+ ],
1039
+ });
1040
+ await extensionManager.loadExtensions();
1041
+ await extensionManager.installOrUpdateExtension({
1042
+ source: oldSourceExtDir,
1043
+ type: 'local',
1044
+ autoUpdate: true,
1045
+ });
1046
+ // 2. Create new version with different settings
1047
+ const newSourceExtDir = createExtension({
1048
+ extensionsDir: tempHomeDir,
1049
+ name: 'my-auto-update-ext',
1050
+ version: '1.1.0',
1051
+ settings: [
1052
+ {
1053
+ name: 'NEW_SETTING',
1054
+ envVar: 'NEW_SETTING',
1055
+ description: 'A new setting',
1056
+ },
1057
+ ],
1058
+ });
1059
+ const previousExtensionConfig = extensionManager.loadExtensionConfig(path.join(userExtensionsDir, 'my-auto-update-ext'));
1060
+ // 3. Attempt to update and assert it fails
1061
+ await expect(extensionManager.installOrUpdateExtension({ source: newSourceExtDir, type: 'local', autoUpdate: true }, previousExtensionConfig)).rejects.toThrow('Extension "my-auto-update-ext" has settings changes and cannot be auto-updated. Please update manually.');
760
1062
  });
761
1063
  it('should throw an error for invalid extension names', async () => {
762
1064
  const sourceExtDir = createExtension({
@@ -764,7 +1066,10 @@ This extension will run the following MCP servers:
764
1066
  name: 'bad_name',
765
1067
  version: '1.0.0',
766
1068
  });
767
- await expect(installOrUpdateExtension({ source: sourceExtDir, type: 'local' }, async (_) => true)).rejects.toThrow('Invalid extension name: "bad_name"');
1069
+ await expect(extensionManager.installOrUpdateExtension({
1070
+ source: sourceExtDir,
1071
+ type: 'local',
1072
+ })).rejects.toThrow('Invalid extension name: "bad_name"');
768
1073
  });
769
1074
  describe('installing from github', () => {
770
1075
  const gitUrl = 'https://github.com/google/gemini-test-extension.git';
@@ -797,7 +1102,11 @@ This extension will run the following MCP servers:
797
1102
  version: '1.0.0',
798
1103
  });
799
1104
  vi.spyOn(ExtensionStorage, 'createTmpDir').mockResolvedValue(join(tempDir, extensionName));
800
- await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, async () => true);
1105
+ await extensionManager.loadExtensions();
1106
+ await extensionManager.installOrUpdateExtension({
1107
+ source: gitUrl,
1108
+ type: 'github-release',
1109
+ });
801
1110
  expect(fs.existsSync(targetExtDir)).toBe(true);
802
1111
  const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
803
1112
  expect(fs.existsSync(metadataPath)).toBe(true);
@@ -815,13 +1124,12 @@ This extension will run the following MCP servers:
815
1124
  errorMessage: 'download failed',
816
1125
  type: 'github-release',
817
1126
  });
818
- const requestConsent = vi.fn().mockResolvedValue(true);
819
- await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, // Use github-release to force consent
820
- requestConsent);
1127
+ await extensionManager.loadExtensions();
1128
+ await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
821
1129
  // It gets called once to ask for a git clone, and once to consent to
822
1130
  // the actual extension features.
823
- expect(requestConsent).toHaveBeenCalledTimes(2);
824
- expect(requestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
1131
+ expect(mockRequestConsent).toHaveBeenCalledTimes(2);
1132
+ expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
825
1133
  expect(mockGit.clone).toHaveBeenCalled();
826
1134
  const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
827
1135
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -833,9 +1141,13 @@ This extension will run the following MCP servers:
833
1141
  errorMessage: 'download failed',
834
1142
  type: 'github-release',
835
1143
  });
836
- const requestConsent = vi.fn().mockResolvedValue(false);
837
- await expect(installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, requestConsent)).rejects.toThrow(`Failed to install extension ${gitUrl}: download failed`);
838
- expect(requestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
1144
+ mockRequestConsent.mockResolvedValue(false);
1145
+ await extensionManager.loadExtensions();
1146
+ await expect(extensionManager.installOrUpdateExtension({
1147
+ source: gitUrl,
1148
+ type: 'github-release',
1149
+ })).rejects.toThrow(`Failed to install extension ${gitUrl}: download failed`);
1150
+ expect(mockRequestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
839
1151
  expect(mockGit.clone).not.toHaveBeenCalled();
840
1152
  });
841
1153
  it('should fallback to git clone without consent if no release data is found on first install', async () => {
@@ -844,11 +1156,14 @@ This extension will run the following MCP servers:
844
1156
  failureReason: 'no release data',
845
1157
  type: 'github-release',
846
1158
  });
847
- const requestConsent = vi.fn().mockResolvedValue(true);
848
- await installOrUpdateExtension({ source: gitUrl, type: 'git' }, requestConsent);
1159
+ await extensionManager.loadExtensions();
1160
+ await extensionManager.installOrUpdateExtension({
1161
+ source: gitUrl,
1162
+ type: 'git',
1163
+ });
849
1164
  // We should not see the request to use git clone, this is a repo that
850
1165
  // has no github releases so it is the only install method.
851
- expect(requestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Installing extension "gemini-test-extension"'));
1166
+ expect(mockRequestConsent).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Installing extension "gemini-test-extension"'));
852
1167
  expect(mockGit.clone).toHaveBeenCalled();
853
1168
  const metadataPath = path.join(userExtensionsDir, extensionName, INSTALL_METADATA_FILENAME);
854
1169
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -861,10 +1176,9 @@ This extension will run the following MCP servers:
861
1176
  errorMessage: 'No release data found',
862
1177
  type: 'github-release',
863
1178
  });
864
- const requestConsent = vi.fn().mockResolvedValue(true);
865
- await installOrUpdateExtension({ source: gitUrl, type: 'github-release' }, // Note the type
866
- requestConsent);
867
- expect(requestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
1179
+ await extensionManager.loadExtensions();
1180
+ await extensionManager.installOrUpdateExtension({ source: gitUrl, type: 'github-release' });
1181
+ expect(mockRequestConsent).toHaveBeenCalledWith(expect.stringContaining('Would you like to attempt to install via "git clone" instead?'));
868
1182
  expect(mockGit.clone).toHaveBeenCalled();
869
1183
  });
870
1184
  });
@@ -876,7 +1190,8 @@ This extension will run the following MCP servers:
876
1190
  name: 'my-local-extension',
877
1191
  version: '1.0.0',
878
1192
  });
879
- await uninstallExtension('my-local-extension', false);
1193
+ await extensionManager.loadExtensions();
1194
+ await extensionManager.uninstallExtension('my-local-extension', false);
880
1195
  expect(fs.existsSync(sourceExtDir)).toBe(false);
881
1196
  });
882
1197
  it('should uninstall an extension by name and retain existing extensions', async () => {
@@ -890,13 +1205,15 @@ This extension will run the following MCP servers:
890
1205
  name: 'other-extension',
891
1206
  version: '1.0.0',
892
1207
  });
893
- await uninstallExtension('my-local-extension', false);
1208
+ await extensionManager.loadExtensions();
1209
+ await extensionManager.uninstallExtension('my-local-extension', false);
894
1210
  expect(fs.existsSync(sourceExtDir)).toBe(false);
895
- expect(loadExtensions(new ExtensionEnablementManager())).toHaveLength(1);
1211
+ expect(extensionManager.getExtensions()).toHaveLength(1);
896
1212
  expect(fs.existsSync(otherExtDir)).toBe(true);
897
1213
  });
898
1214
  it('should throw an error if the extension does not exist', async () => {
899
- await expect(uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
1215
+ await extensionManager.loadExtensions();
1216
+ await expect(extensionManager.uninstallExtension('nonexistent-extension', false)).rejects.toThrow('Extension not found.');
900
1217
  });
901
1218
  describe.each([true, false])('with isUpdate: %s', (isUpdate) => {
902
1219
  it(`should ${isUpdate ? 'not ' : ''}log uninstall event`, async () => {
@@ -909,7 +1226,8 @@ This extension will run the following MCP servers:
909
1226
  type: 'local',
910
1227
  },
911
1228
  });
912
- await uninstallExtension('my-local-extension', isUpdate);
1229
+ await extensionManager.loadExtensions();
1230
+ await extensionManager.uninstallExtension('my-local-extension', isUpdate);
913
1231
  if (isUpdate) {
914
1232
  expect(mockLogExtensionUninstall).not.toHaveBeenCalled();
915
1233
  expect(ExtensionUninstallEvent).not.toHaveBeenCalled();
@@ -927,7 +1245,8 @@ This extension will run the following MCP servers:
927
1245
  });
928
1246
  const enablementManager = new ExtensionEnablementManager();
929
1247
  enablementManager.enable('test-extension', true, '/some/scope');
930
- await uninstallExtension('test-extension', isUpdate);
1248
+ await extensionManager.loadExtensions();
1249
+ await extensionManager.uninstallExtension('test-extension', isUpdate);
931
1250
  const config = enablementManager.readConfig()['test-extension'];
932
1251
  if (isUpdate) {
933
1252
  expect(config).not.toBeUndefined();
@@ -949,7 +1268,8 @@ This extension will run the following MCP servers:
949
1268
  type: 'git',
950
1269
  },
951
1270
  });
952
- await uninstallExtension(gitUrl, false);
1271
+ await extensionManager.loadExtensions();
1272
+ await extensionManager.uninstallExtension(gitUrl, false);
953
1273
  expect(fs.existsSync(sourceExtDir)).toBe(false);
954
1274
  expect(mockLogExtensionUninstall).toHaveBeenCalled();
955
1275
  expect(ExtensionUninstallEvent).toHaveBeenCalledWith(hashValue('gemini-sql-extension'), hashValue('https://github.com/google/gemini-sql-extension'), 'success');
@@ -961,29 +1281,32 @@ This extension will run the following MCP servers:
961
1281
  version: '1.0.0',
962
1282
  // No installMetadata provided
963
1283
  });
964
- await expect(uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
1284
+ await extensionManager.loadExtensions();
1285
+ await expect(extensionManager.uninstallExtension('https://github.com/google/no-metadata-extension', false)).rejects.toThrow('Extension not found.');
965
1286
  });
966
1287
  });
967
1288
  describe('disableExtension', () => {
968
- it('should disable an extension at the user scope', () => {
1289
+ it('should disable an extension at the user scope', async () => {
969
1290
  createExtension({
970
1291
  extensionsDir: userExtensionsDir,
971
1292
  name: 'my-extension',
972
1293
  version: '1.0.0',
973
1294
  });
974
- disableExtension('my-extension', SettingScope.User, new ExtensionEnablementManager());
1295
+ await extensionManager.loadExtensions();
1296
+ await extensionManager.disableExtension('my-extension', SettingScope.User);
975
1297
  expect(isEnabled({
976
1298
  name: 'my-extension',
977
1299
  enabledForPath: tempWorkspaceDir,
978
1300
  })).toBe(false);
979
1301
  });
980
- it('should disable an extension at the workspace scope', () => {
1302
+ it('should disable an extension at the workspace scope', async () => {
981
1303
  createExtension({
982
1304
  extensionsDir: userExtensionsDir,
983
1305
  name: 'my-extension',
984
1306
  version: '1.0.0',
985
1307
  });
986
- disableExtension('my-extension', SettingScope.Workspace, new ExtensionEnablementManager(), tempWorkspaceDir);
1308
+ await extensionManager.loadExtensions();
1309
+ await extensionManager.disableExtension('my-extension', SettingScope.Workspace);
987
1310
  expect(isEnabled({
988
1311
  name: 'my-extension',
989
1312
  enabledForPath: tempHomeDir,
@@ -993,23 +1316,24 @@ This extension will run the following MCP servers:
993
1316
  enabledForPath: tempWorkspaceDir,
994
1317
  })).toBe(false);
995
1318
  });
996
- it('should handle disabling the same extension twice', () => {
1319
+ it('should handle disabling the same extension twice', async () => {
997
1320
  createExtension({
998
1321
  extensionsDir: userExtensionsDir,
999
1322
  name: 'my-extension',
1000
1323
  version: '1.0.0',
1001
1324
  });
1002
- disableExtension('my-extension', SettingScope.User, new ExtensionEnablementManager());
1003
- disableExtension('my-extension', SettingScope.User, new ExtensionEnablementManager());
1325
+ await extensionManager.loadExtensions();
1326
+ await extensionManager.disableExtension('my-extension', SettingScope.User);
1327
+ await extensionManager.disableExtension('my-extension', SettingScope.User);
1004
1328
  expect(isEnabled({
1005
1329
  name: 'my-extension',
1006
1330
  enabledForPath: tempWorkspaceDir,
1007
1331
  })).toBe(false);
1008
1332
  });
1009
- it('should throw an error if you request system scope', () => {
1010
- expect(() => disableExtension('my-extension', SettingScope.System, new ExtensionEnablementManager())).toThrow('System and SystemDefaults scopes are not supported.');
1333
+ it('should throw an error if you request system scope', async () => {
1334
+ await expect(async () => await extensionManager.disableExtension('my-extension', SettingScope.System)).rejects.toThrow('System and SystemDefaults scopes are not supported.');
1011
1335
  });
1012
- it('should log a disable event', () => {
1336
+ it('should log a disable event', async () => {
1013
1337
  createExtension({
1014
1338
  extensionsDir: userExtensionsDir,
1015
1339
  name: 'ext1',
@@ -1019,7 +1343,8 @@ This extension will run the following MCP servers:
1019
1343
  type: 'local',
1020
1344
  },
1021
1345
  });
1022
- disableExtension('ext1', SettingScope.Workspace, new ExtensionEnablementManager());
1346
+ await extensionManager.loadExtensions();
1347
+ await extensionManager.disableExtension('ext1', SettingScope.Workspace);
1023
1348
  expect(mockLogExtensionDisable).toHaveBeenCalled();
1024
1349
  expect(ExtensionDisableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
1025
1350
  });
@@ -1029,41 +1354,40 @@ This extension will run the following MCP servers:
1029
1354
  vi.restoreAllMocks();
1030
1355
  });
1031
1356
  const getActiveExtensions = () => {
1032
- const manager = new ExtensionEnablementManager();
1033
- const extensions = loadExtensions(manager);
1357
+ const extensions = extensionManager.getExtensions();
1034
1358
  return extensions.filter((e) => e.isActive);
1035
1359
  };
1036
- it('should enable an extension at the user scope', () => {
1360
+ it('should enable an extension at the user scope', async () => {
1037
1361
  createExtension({
1038
1362
  extensionsDir: userExtensionsDir,
1039
1363
  name: 'ext1',
1040
1364
  version: '1.0.0',
1041
1365
  });
1042
- const extensionEnablementManager = new ExtensionEnablementManager();
1043
- disableExtension('ext1', SettingScope.User, extensionEnablementManager);
1366
+ await extensionManager.loadExtensions();
1367
+ await extensionManager.disableExtension('ext1', SettingScope.User);
1044
1368
  let activeExtensions = getActiveExtensions();
1045
1369
  expect(activeExtensions).toHaveLength(0);
1046
- enableExtension('ext1', SettingScope.User, extensionEnablementManager);
1047
- activeExtensions = getActiveExtensions();
1370
+ await extensionManager.enableExtension('ext1', SettingScope.User);
1371
+ activeExtensions = await getActiveExtensions();
1048
1372
  expect(activeExtensions).toHaveLength(1);
1049
1373
  expect(activeExtensions[0].name).toBe('ext1');
1050
1374
  });
1051
- it('should enable an extension at the workspace scope', () => {
1375
+ it('should enable an extension at the workspace scope', async () => {
1052
1376
  createExtension({
1053
1377
  extensionsDir: userExtensionsDir,
1054
1378
  name: 'ext1',
1055
1379
  version: '1.0.0',
1056
1380
  });
1057
- const extensionEnablementManager = new ExtensionEnablementManager();
1058
- disableExtension('ext1', SettingScope.Workspace, extensionEnablementManager);
1381
+ await extensionManager.loadExtensions();
1382
+ await extensionManager.disableExtension('ext1', SettingScope.Workspace);
1059
1383
  let activeExtensions = getActiveExtensions();
1060
1384
  expect(activeExtensions).toHaveLength(0);
1061
- enableExtension('ext1', SettingScope.Workspace, extensionEnablementManager);
1062
- activeExtensions = getActiveExtensions();
1385
+ await extensionManager.enableExtension('ext1', SettingScope.Workspace);
1386
+ activeExtensions = await getActiveExtensions();
1063
1387
  expect(activeExtensions).toHaveLength(1);
1064
1388
  expect(activeExtensions[0].name).toBe('ext1');
1065
1389
  });
1066
- it('should log an enable event', () => {
1390
+ it('should log an enable event', async () => {
1067
1391
  createExtension({
1068
1392
  extensionsDir: userExtensionsDir,
1069
1393
  name: 'ext1',
@@ -1073,9 +1397,9 @@ This extension will run the following MCP servers:
1073
1397
  type: 'local',
1074
1398
  },
1075
1399
  });
1076
- const extensionEnablementManager = new ExtensionEnablementManager();
1077
- disableExtension('ext1', SettingScope.Workspace, extensionEnablementManager);
1078
- enableExtension('ext1', SettingScope.Workspace, extensionEnablementManager);
1400
+ await extensionManager.loadExtensions();
1401
+ await extensionManager.disableExtension('ext1', SettingScope.Workspace);
1402
+ await extensionManager.enableExtension('ext1', SettingScope.Workspace);
1079
1403
  expect(mockLogExtensionEnable).toHaveBeenCalled();
1080
1404
  expect(ExtensionEnableEvent).toHaveBeenCalledWith(hashValue('ext1'), hashValue(userExtensionsDir), SettingScope.Workspace);
1081
1405
  });