@lobehub/lobehub 2.0.0-next.33 → 2.0.0-next.35

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 (148) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/model-bank/src/aiModels/google.ts +1 -1
  5. package/src/app/[variants]/(main)/chat/ChatRouter.tsx +83 -0
  6. package/src/app/[variants]/(main)/chat/_layout/ChatLayout.tsx +22 -0
  7. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +12 -7
  8. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +2 -2
  9. package/src/app/[variants]/(main)/chat/_layout/FeatureFlagsProvider.tsx +24 -0
  10. package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +3 -2
  11. package/src/app/[variants]/(main)/chat/_layout/type.ts +0 -1
  12. package/src/app/[variants]/(main)/chat/components/ConversationArea.tsx +29 -0
  13. package/src/app/[variants]/(main)/chat/components/MainChatPage.tsx +25 -0
  14. package/src/app/[variants]/(main)/chat/components/PortalPanel.tsx +28 -0
  15. package/src/app/[variants]/(main)/chat/components/SessionPanel.tsx +33 -0
  16. package/src/app/[variants]/(main)/chat/{settings/page.tsx → components/SettingsPage.tsx} +35 -3
  17. package/src/app/[variants]/(main)/chat/components/TopicSidebar.tsx +30 -0
  18. package/src/app/[variants]/(main)/chat/components/WorkspaceLayout.tsx +73 -0
  19. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/index.tsx +1 -1
  20. package/src/app/[variants]/(main)/chat/{layout.ts → layout.tsx} +0 -1
  21. package/src/app/[variants]/(main)/chat/page.tsx +12 -0
  22. package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
  23. package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
  24. package/src/features/Portal/GroupThread/Body/index.tsx +1 -1
  25. package/src/hooks/useHotkeys/chatScope.ts +1 -1
  26. package/src/locales/default/modelProvider.ts +15 -1
  27. package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
  28. package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
  29. package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
  30. package/src/store/global/initialState.ts +4 -0
  31. package/src/store/global/selectors/systemStatus.ts +6 -0
  32. package/src/app/[variants]/(main)/chat/(workspace)/layout.ts +0 -11
  33. package/src/app/[variants]/(main)/chat/(workspace)/page.tsx +0 -53
  34. package/src/app/[variants]/(main)/chat/@session/default.tsx +0 -31
  35. package/src/app/[variants]/(main)/chat/settings/layout.tsx +0 -21
  36. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/default.tsx +0 -0
  37. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatHydration/index.tsx +0 -0
  38. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/ClassicChat.tsx +0 -0
  39. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/GroupChat.tsx +0 -0
  40. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/MessageFromUrl.tsx +0 -0
  41. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/index.tsx +0 -0
  42. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/useSendMenuItems.tsx +0 -0
  43. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/MentionedUserItem.tsx +0 -0
  44. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/index.tsx +0 -0
  45. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/index.tsx +0 -0
  46. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/ActionBar.tsx +0 -0
  47. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Files/index.tsx +0 -0
  48. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/Container.tsx +0 -0
  49. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/index.tsx +0 -0
  50. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Send.tsx +0 -0
  51. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/index.tsx +0 -0
  52. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/useSend.ts +0 -0
  53. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/index.tsx +0 -0
  54. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/useSend.ts +0 -0
  55. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/OrchestratorThinking.tsx +0 -0
  56. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/Thread.tsx +0 -0
  57. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/ThreadItem.tsx +0 -0
  58. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/Content.tsx +0 -0
  59. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/AddButton.tsx +0 -0
  60. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +0 -0
  61. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/index.tsx +0 -0
  62. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +0 -0
  63. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/index.tsx +0 -0
  64. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/useTemplateMatching.ts +0 -0
  65. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/index.tsx +0 -0
  66. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/index.tsx +0 -0
  67. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatMinimap/index.tsx +0 -0
  68. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ThreadHydration.tsx +0 -0
  69. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/Toast.tsx +0 -0
  70. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/index.tsx +0 -0
  71. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentSettings/index.tsx +0 -0
  72. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentTeamSettings/index.tsx +0 -0
  73. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ChangelogModal.tsx +0 -0
  74. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/SettingButton.tsx +0 -0
  75. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ShareButton/index.tsx +0 -0
  76. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/TelemetryNotification.tsx +0 -0
  77. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/HeaderAction.tsx +0 -0
  78. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Main.tsx +0 -0
  79. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/HistoryLimitTags.tsx +0 -0
  80. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/KnowledgeTag.tsx +0 -0
  81. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/MemberCountTag.tsx +0 -0
  82. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/SearchTags.tsx +0 -0
  83. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/index.tsx +0 -0
  84. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/index.tsx +0 -0
  85. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/Portal.tsx +0 -0
  86. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/TopicPanel.tsx +0 -0
  87. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/index.tsx +0 -0
  88. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/ChatHeaderTitle.tsx +0 -0
  89. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/index.tsx +0 -0
  90. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/TopicModal.tsx +0 -0
  91. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/index.tsx +0 -0
  92. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/type.ts +0 -0
  93. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Desktop.tsx +0 -0
  94. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Mobile.tsx +0 -0
  95. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/default.tsx +0 -0
  96. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/error.tsx +0 -0
  97. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/features/Body.tsx +0 -0
  98. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/loading.tsx +0 -0
  99. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Desktop.tsx +0 -0
  100. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Mobile.tsx +0 -0
  101. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/default.tsx +0 -0
  102. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/SystemRole.tsx +0 -0
  103. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/index.tsx +0 -0
  104. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigLayout.tsx +0 -0
  105. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigSwitcher.tsx +0 -0
  106. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMember.tsx +0 -0
  107. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMemberItem.tsx +0 -0
  108. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupRole.tsx +0 -0
  109. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/index.tsx +0 -0
  110. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/style.ts +0 -0
  111. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/SkeletonList.tsx +0 -0
  112. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/Header.tsx +0 -0
  113. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/GroupItem.tsx +0 -0
  114. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/index.tsx +0 -0
  115. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/FlatMode/index.tsx +0 -0
  116. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/SearchResult/index.tsx +0 -0
  117. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/Content.tsx +0 -0
  118. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/index.tsx +0 -0
  119. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadList/index.tsx +0 -0
  120. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/DefaultContent.tsx +0 -0
  121. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +0 -0
  122. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/index.tsx +0 -0
  123. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/index.tsx +0 -0
  124. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicSearchBar/index.tsx +0 -0
  125. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/index.tsx +0 -0
  126. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionHydration.tsx +0 -0
  127. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/Actions.tsx +0 -0
  128. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/index.tsx +0 -0
  129. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/DefaultMode.tsx +0 -0
  130. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Inbox/index.tsx +0 -0
  131. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/AddButton.tsx +0 -0
  132. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/Actions.tsx +0 -0
  133. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/index.tsx +0 -0
  134. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/index.tsx +0 -0
  135. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/ListItem/index.tsx +0 -0
  136. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +0 -0
  137. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +0 -0
  138. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/CreateGroupModal.tsx +0 -0
  139. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/RenameGroupModal.tsx +0 -0
  140. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/SearchMode.tsx +0 -0
  141. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/index.tsx +0 -0
  142. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionSearchBar.tsx +0 -0
  143. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SkeletonList.tsx +0 -0
  144. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/PanelBody.tsx +0 -0
  145. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/SessionHeader.tsx +0 -0
  146. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/index.tsx +0 -0
  147. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/SessionHeader.tsx +0 -0
  148. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/index.tsx +0 -0
@@ -0,0 +1,374 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { NpmInstallationChecker } from './NpmInstallationChecker';
4
+
5
+ // Hoist the mock to ensure it's available in the factory
6
+ const { mockExecPromise } = vi.hoisted(() => {
7
+ return {
8
+ mockExecPromise: vi.fn(),
9
+ };
10
+ });
11
+
12
+ // Mock node:child_process
13
+ vi.mock('node:child_process');
14
+
15
+ // Mock node:util to return our hoisted mock when promisify is called
16
+ vi.mock('node:util', () => ({
17
+ default: {
18
+ promisify: () => mockExecPromise,
19
+ },
20
+ promisify: () => mockExecPromise,
21
+ }));
22
+
23
+ describe('NpmInstallationChecker', () => {
24
+ let checker: NpmInstallationChecker;
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ checker = new NpmInstallationChecker();
29
+ });
30
+
31
+ describe('checkPackageInstalled', () => {
32
+ describe('validation', () => {
33
+ it('should return error when packageName is not provided', async () => {
34
+ const result = await checker.checkPackageInstalled({});
35
+
36
+ expect(result).toEqual({
37
+ error: 'Package name not provided',
38
+ installed: false,
39
+ packageName: '',
40
+ });
41
+ expect(mockExecPromise).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('should return error when packageName is undefined', async () => {
45
+ const result = await checker.checkPackageInstalled({ packageName: undefined });
46
+
47
+ expect(result).toEqual({
48
+ error: 'Package name not provided',
49
+ installed: false,
50
+ packageName: '',
51
+ });
52
+ });
53
+
54
+ it('should return error when packageName is empty string', async () => {
55
+ const result = await checker.checkPackageInstalled({ packageName: '' });
56
+
57
+ expect(result).toEqual({
58
+ error: 'Package name not provided',
59
+ installed: false,
60
+ packageName: '',
61
+ });
62
+ });
63
+ });
64
+
65
+ describe('global package detection', () => {
66
+ it('should detect globally installed package', async () => {
67
+ mockExecPromise.mockResolvedValueOnce({
68
+ stdout: '/usr/local/lib\n└── typescript@5.0.4\n',
69
+ stderr: '',
70
+ });
71
+
72
+ const result = await checker.checkPackageInstalled({ packageName: 'typescript' });
73
+
74
+ expect(mockExecPromise).toHaveBeenCalledWith('npm list -g typescript --depth=0');
75
+ expect(result).toEqual({
76
+ installed: true,
77
+ packageName: 'typescript',
78
+ });
79
+ });
80
+
81
+ it('should detect globally installed package with @scope', async () => {
82
+ mockExecPromise.mockResolvedValueOnce({
83
+ stdout: '/usr/local/lib\n└── @angular/cli@16.0.0\n',
84
+ stderr: '',
85
+ });
86
+
87
+ const result = await checker.checkPackageInstalled({ packageName: '@angular/cli' });
88
+
89
+ expect(mockExecPromise).toHaveBeenCalledWith('npm list -g @angular/cli --depth=0');
90
+ expect(result.installed).toBe(true);
91
+ });
92
+
93
+ it('should detect package with different version format', async () => {
94
+ mockExecPromise.mockResolvedValueOnce({
95
+ stdout: '/usr/local/lib\n└── eslint@8.41.0 (deduped)\n',
96
+ stderr: '',
97
+ });
98
+
99
+ const result = await checker.checkPackageInstalled({ packageName: 'eslint' });
100
+
101
+ expect(result.installed).toBe(true);
102
+ });
103
+
104
+ it('should handle npm list output with multiple packages', async () => {
105
+ mockExecPromise.mockResolvedValueOnce({
106
+ stdout: '/usr/local/lib\n├── package1@1.0.0\n├── react@18.2.0\n└── package2@2.0.0\n',
107
+ stderr: '',
108
+ });
109
+
110
+ const result = await checker.checkPackageInstalled({ packageName: 'react' });
111
+
112
+ expect(result.installed).toBe(true);
113
+ });
114
+ });
115
+
116
+ describe('npm list empty detection', () => {
117
+ it('should fallback to npx when npm list returns (empty)', async () => {
118
+ mockExecPromise
119
+ .mockResolvedValueOnce({
120
+ stdout: '/usr/local/lib\n(empty)\n',
121
+ stderr: '',
122
+ })
123
+ .mockResolvedValueOnce({
124
+ stdout: '1.0.0\n',
125
+ stderr: '',
126
+ });
127
+
128
+ const result = await checker.checkPackageInstalled({ packageName: 'create-react-app' });
129
+
130
+ expect(mockExecPromise).toHaveBeenNthCalledWith(
131
+ 1,
132
+ 'npm list -g create-react-app --depth=0',
133
+ );
134
+ expect(mockExecPromise).toHaveBeenNthCalledWith(2, 'npx -y create-react-app --version');
135
+ expect(result).toEqual({
136
+ installed: true,
137
+ packageName: 'create-react-app',
138
+ });
139
+ });
140
+
141
+ it('should fallback to npx when package not in global list', async () => {
142
+ mockExecPromise
143
+ .mockResolvedValueOnce({
144
+ stdout: '/usr/local/lib\n└── other-package@1.0.0\n',
145
+ stderr: '',
146
+ })
147
+ .mockResolvedValueOnce({
148
+ stdout: '2.3.1\n',
149
+ stderr: '',
150
+ });
151
+
152
+ const result = await checker.checkPackageInstalled({ packageName: 'cowsay' });
153
+
154
+ expect(mockExecPromise).toHaveBeenNthCalledWith(2, 'npx -y cowsay --version');
155
+ expect(result.installed).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('npx fallback mechanism', () => {
160
+ it('should use npx -y flag to auto-install if needed', async () => {
161
+ mockExecPromise
162
+ .mockResolvedValueOnce({
163
+ stdout: '(empty)\n',
164
+ stderr: '',
165
+ })
166
+ .mockResolvedValueOnce({
167
+ stdout: '5.1.0\n',
168
+ stderr: '',
169
+ });
170
+
171
+ await checker.checkPackageInstalled({ packageName: 'prettier' });
172
+
173
+ expect(mockExecPromise).toHaveBeenCalledWith('npx -y prettier --version');
174
+ });
175
+
176
+ it('should succeed if npx can execute package', async () => {
177
+ mockExecPromise
178
+ .mockResolvedValueOnce({
179
+ stdout: '(empty)\n',
180
+ stderr: '',
181
+ })
182
+ .mockResolvedValueOnce({
183
+ stdout: '3.2.1\n',
184
+ stderr: '',
185
+ });
186
+
187
+ const result = await checker.checkPackageInstalled({ packageName: 'http-server' });
188
+
189
+ expect(result.installed).toBe(true);
190
+ expect(result.packageName).toBe('http-server');
191
+ });
192
+
193
+ it('should handle npx with @scope packages', async () => {
194
+ mockExecPromise
195
+ .mockResolvedValueOnce({
196
+ stdout: '(empty)\n',
197
+ stderr: '',
198
+ })
199
+ .mockResolvedValueOnce({
200
+ stdout: '7.0.0\n',
201
+ stderr: '',
202
+ });
203
+
204
+ await checker.checkPackageInstalled({ packageName: '@vue/cli' });
205
+
206
+ expect(mockExecPromise).toHaveBeenNthCalledWith(2, 'npx -y @vue/cli --version');
207
+ });
208
+ });
209
+
210
+ describe('package not found scenarios', () => {
211
+ it('should return not installed when npm list fails and npx fails', async () => {
212
+ mockExecPromise
213
+ .mockResolvedValueOnce({
214
+ stdout: '(empty)\n',
215
+ stderr: '',
216
+ })
217
+ .mockRejectedValueOnce(new Error('command not found'));
218
+
219
+ const result = await checker.checkPackageInstalled({ packageName: 'nonexistent-pkg' });
220
+
221
+ expect(result).toEqual({
222
+ error: 'command not found',
223
+ installed: false,
224
+ packageName: 'nonexistent-pkg',
225
+ });
226
+ });
227
+
228
+ it('should return not installed when both checks fail', async () => {
229
+ mockExecPromise.mockRejectedValue(new Error('Network error'));
230
+
231
+ const result = await checker.checkPackageInstalled({ packageName: 'some-package' });
232
+
233
+ expect(result.installed).toBe(false);
234
+ expect(result.error).toBe('Network error');
235
+ });
236
+ });
237
+
238
+ describe('error handling', () => {
239
+ it('should handle npm not installed error', async () => {
240
+ mockExecPromise.mockRejectedValueOnce(new Error('npm: command not found'));
241
+
242
+ const result = await checker.checkPackageInstalled({ packageName: 'lodash' });
243
+
244
+ expect(result).toEqual({
245
+ error: 'npm: command not found',
246
+ installed: false,
247
+ packageName: 'lodash',
248
+ });
249
+ });
250
+
251
+ it('should handle permission errors', async () => {
252
+ mockExecPromise.mockRejectedValueOnce(new Error('EACCES: permission denied'));
253
+
254
+ const result = await checker.checkPackageInstalled({ packageName: 'webpack' });
255
+
256
+ expect(result.installed).toBe(false);
257
+ expect(result.error).toContain('EACCES');
258
+ });
259
+
260
+ it('should handle non-Error exceptions', async () => {
261
+ mockExecPromise.mockRejectedValueOnce('string error');
262
+
263
+ const result = await checker.checkPackageInstalled({ packageName: 'babel' });
264
+
265
+ expect(result).toEqual({
266
+ error: 'Unknown error',
267
+ installed: false,
268
+ packageName: 'babel',
269
+ });
270
+ });
271
+
272
+ it('should handle npm registry timeout', async () => {
273
+ mockExecPromise.mockRejectedValueOnce(new Error('ETIMEDOUT: connection timeout'));
274
+
275
+ const result = await checker.checkPackageInstalled({ packageName: 'axios' });
276
+
277
+ expect(result.installed).toBe(false);
278
+ expect(result.error).toBe('ETIMEDOUT: connection timeout');
279
+ });
280
+ });
281
+
282
+ describe('edge cases', () => {
283
+ it('should handle package names with hyphens', async () => {
284
+ mockExecPromise.mockResolvedValueOnce({
285
+ stdout: '/usr/local/lib\n└── create-next-app@13.4.0\n',
286
+ stderr: '',
287
+ });
288
+
289
+ const result = await checker.checkPackageInstalled({ packageName: 'create-next-app' });
290
+
291
+ expect(result.installed).toBe(true);
292
+ });
293
+
294
+ it('should handle package names with dots', async () => {
295
+ mockExecPromise.mockResolvedValueOnce({
296
+ stdout: '/usr/local/lib\n└── package.name@1.0.0\n',
297
+ stderr: '',
298
+ });
299
+
300
+ const result = await checker.checkPackageInstalled({ packageName: 'package.name' });
301
+
302
+ expect(result.installed).toBe(true);
303
+ });
304
+
305
+ it('should handle npm list with warnings in stderr', async () => {
306
+ mockExecPromise.mockResolvedValueOnce({
307
+ stdout: '/usr/local/lib\n└── typescript@5.0.4\n',
308
+ stderr: 'npm WARN deprecated package@1.0.0\n',
309
+ });
310
+
311
+ const result = await checker.checkPackageInstalled({ packageName: 'typescript' });
312
+
313
+ expect(result.installed).toBe(true);
314
+ });
315
+
316
+ it('should handle npm list with extra whitespace', async () => {
317
+ mockExecPromise.mockResolvedValueOnce({
318
+ stdout: ' /usr/local/lib \n └── jest@29.5.0 \n',
319
+ stderr: '',
320
+ });
321
+
322
+ const result = await checker.checkPackageInstalled({ packageName: 'jest' });
323
+
324
+ expect(result.installed).toBe(true);
325
+ });
326
+
327
+ it('should handle case-sensitive package names', async () => {
328
+ mockExecPromise.mockResolvedValueOnce({
329
+ stdout: '/usr/local/lib\n└── MyPackage@1.0.0\n',
330
+ stderr: '',
331
+ });
332
+
333
+ const result = await checker.checkPackageInstalled({ packageName: 'MyPackage' });
334
+
335
+ expect(result.installed).toBe(true);
336
+ });
337
+
338
+ it('should handle npm list output with symlink info', async () => {
339
+ mockExecPromise.mockResolvedValueOnce({
340
+ stdout: '/usr/local/lib\n└── react@18.2.0 -> /custom/path/react\n',
341
+ stderr: '',
342
+ });
343
+
344
+ const result = await checker.checkPackageInstalled({ packageName: 'react' });
345
+
346
+ expect(result.installed).toBe(true);
347
+ });
348
+
349
+ it('should handle npm list with peer dependency warnings', async () => {
350
+ mockExecPromise.mockResolvedValueOnce({
351
+ stdout: '/usr/local/lib\n└── UNMET PEER DEPENDENCY eslint@8.0.0\n└── webpack@5.88.0\n',
352
+ stderr: '',
353
+ });
354
+
355
+ const result = await checker.checkPackageInstalled({ packageName: 'webpack' });
356
+
357
+ expect(result.installed).toBe(true);
358
+ });
359
+
360
+ it('should match substring package names in global list', async () => {
361
+ mockExecPromise.mockResolvedValueOnce({
362
+ stdout: '/usr/local/lib\n└── react-native@0.72.0\n',
363
+ stderr: '',
364
+ });
365
+
366
+ const result = await checker.checkPackageInstalled({ packageName: 'react' });
367
+
368
+ // Note: The implementation uses includes(), so 'react' will match 'react-native'
369
+ // This is intentional behavior - grep would also match substring
370
+ expect(result.installed).toBe(true);
371
+ });
372
+ });
373
+ });
374
+ });