@jackwener/opencli 1.5.6 → 1.5.7

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 (334) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/idle-manager.d.ts +19 -0
  176. package/dist/idle-manager.js +54 -0
  177. package/dist/launcher.d.ts +36 -0
  178. package/dist/launcher.js +152 -0
  179. package/dist/launcher.test.d.ts +1 -0
  180. package/dist/launcher.test.js +57 -0
  181. package/dist/main.js +3 -3
  182. package/dist/registry.d.ts +1 -0
  183. package/dist/registry.js +31 -3
  184. package/dist/registry.test.js +13 -0
  185. package/dist/runtime.d.ts +5 -3
  186. package/dist/runtime.js +12 -5
  187. package/dist/serialization.d.ts +1 -0
  188. package/dist/serialization.js +3 -0
  189. package/dist/serialization.test.js +17 -1
  190. package/dist/tui.d.ts +7 -0
  191. package/dist/tui.js +52 -0
  192. package/dist/tui.test.d.ts +1 -0
  193. package/dist/tui.test.js +19 -0
  194. package/dist/weixin-download.test.js +14 -0
  195. package/docs/.vitepress/config.mts +1 -0
  196. package/docs/adapters/browser/notebooklm.md +69 -0
  197. package/docs/adapters/browser/xiaohongshu.md +19 -10
  198. package/docs/adapters/index.md +67 -66
  199. package/docs/guide/browser-bridge.md +12 -0
  200. package/docs/guide/troubleshooting.md +9 -4
  201. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  202. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  203. package/docs/zh/guide/browser-bridge.md +12 -0
  204. package/extension/dist/background.js +794 -513
  205. package/extension/src/background.test.ts +202 -2
  206. package/extension/src/background.ts +174 -10
  207. package/extension/src/cdp.ts +12 -0
  208. package/extension/src/protocol.ts +7 -5
  209. package/package.json +1 -1
  210. package/src/browser/cdp.ts +24 -17
  211. package/src/browser/daemon-client.ts +7 -1
  212. package/src/browser/dom-helpers.test.ts +15 -1
  213. package/src/browser/dom-helpers.ts +1 -0
  214. package/src/browser/mcp.ts +18 -13
  215. package/src/browser/page.test.ts +58 -0
  216. package/src/browser/page.ts +18 -2
  217. package/src/browser/stealth.test.ts +153 -0
  218. package/src/browser/stealth.ts +198 -0
  219. package/src/browser.test.ts +1 -1
  220. package/src/build-manifest.test.ts +2 -0
  221. package/src/build-manifest.ts +6 -1
  222. package/src/cli.ts +21 -3
  223. package/src/clis/antigravity/SKILL.md +3 -12
  224. package/src/clis/antigravity/serve.ts +5 -10
  225. package/src/clis/bilibili/subtitle.test.ts +60 -0
  226. package/src/clis/bilibili/subtitle.ts +4 -0
  227. package/src/clis/chatwise/ask.ts +0 -2
  228. package/src/clis/chatwise/export.ts +0 -2
  229. package/src/clis/chatwise/history.ts +0 -2
  230. package/src/clis/chatwise/model.ts +0 -2
  231. package/src/clis/chatwise/new.ts +1 -2
  232. package/src/clis/chatwise/read.ts +0 -2
  233. package/src/clis/chatwise/screenshot.ts +1 -2
  234. package/src/clis/chatwise/send.ts +0 -2
  235. package/src/clis/chatwise/status.ts +1 -2
  236. package/src/clis/ctrip/search.test.ts +73 -0
  237. package/src/clis/ctrip/search.ts +97 -47
  238. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  239. package/src/clis/douyin/_shared/sts2.ts +11 -3
  240. package/src/clis/douyin/activities.test.ts +41 -1
  241. package/src/clis/douyin/activities.ts +12 -3
  242. package/src/clis/douyin/collections.test.ts +35 -2
  243. package/src/clis/douyin/collections.ts +1 -1
  244. package/src/clis/douyin/draft.test.ts +444 -2
  245. package/src/clis/douyin/draft.ts +382 -218
  246. package/src/clis/douyin/hashtag.test.ts +42 -2
  247. package/src/clis/douyin/hashtag.ts +11 -3
  248. package/src/clis/douyin/profile.test.ts +43 -1
  249. package/src/clis/douyin/profile.ts +9 -2
  250. package/src/clis/douyin/videos.test.ts +52 -2
  251. package/src/clis/douyin/videos.ts +49 -15
  252. package/src/clis/facebook/search.test.ts +70 -0
  253. package/src/clis/facebook/search.yaml +4 -3
  254. package/src/clis/instagram/download.test.ts +159 -0
  255. package/src/clis/instagram/download.ts +286 -0
  256. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  257. package/src/clis/notebooklm/bind-current.ts +36 -0
  258. package/src/clis/notebooklm/binding.test.ts +53 -0
  259. package/src/clis/notebooklm/compat.test.ts +19 -0
  260. package/src/clis/notebooklm/current.ts +38 -0
  261. package/src/clis/notebooklm/get.ts +53 -0
  262. package/src/clis/notebooklm/history.test.ts +70 -0
  263. package/src/clis/notebooklm/history.ts +36 -0
  264. package/src/clis/notebooklm/list.ts +40 -0
  265. package/src/clis/notebooklm/note-list.test.ts +64 -0
  266. package/src/clis/notebooklm/note-list.ts +42 -0
  267. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  268. package/src/clis/notebooklm/notes-get.ts +67 -0
  269. package/src/clis/notebooklm/rpc.test.ts +126 -0
  270. package/src/clis/notebooklm/rpc.ts +286 -0
  271. package/src/clis/notebooklm/shared.ts +98 -0
  272. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  273. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  274. package/src/clis/notebooklm/source-get.test.ts +100 -0
  275. package/src/clis/notebooklm/source-get.ts +60 -0
  276. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  277. package/src/clis/notebooklm/source-guide.ts +69 -0
  278. package/src/clis/notebooklm/source-list.ts +45 -0
  279. package/src/clis/notebooklm/status.ts +34 -0
  280. package/src/clis/notebooklm/summary.test.ts +94 -0
  281. package/src/clis/notebooklm/summary.ts +45 -0
  282. package/src/clis/notebooklm/utils.test.ts +446 -0
  283. package/src/clis/notebooklm/utils.ts +893 -0
  284. package/src/clis/substack/utils.test.ts +54 -0
  285. package/src/clis/substack/utils.ts +10 -2
  286. package/src/clis/v2ex/hot.yaml +4 -1
  287. package/src/clis/v2ex/latest.yaml +4 -1
  288. package/src/clis/v2ex/topic.yaml +6 -1
  289. package/src/clis/weixin/download.ts +95 -6
  290. package/src/clis/weread/book.ts +142 -2
  291. package/src/clis/weread/commands.test.ts +314 -154
  292. package/src/clis/weread/utils.ts +33 -4
  293. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  294. package/src/clis/xiaohongshu/comments.ts +76 -17
  295. package/src/clis/xiaohongshu/download.test.ts +96 -0
  296. package/src/clis/xiaohongshu/download.ts +83 -22
  297. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  298. package/src/clis/xiaohongshu/note.test.ts +164 -0
  299. package/src/clis/xiaohongshu/note.ts +86 -0
  300. package/src/clis/xiaohongshu/search.test.ts +11 -4
  301. package/src/clis/xiaohongshu/search.ts +13 -0
  302. package/src/clis/youtube/search.ts +57 -17
  303. package/src/clis/zhihu/question.test.ts +71 -0
  304. package/src/clis/zhihu/question.ts +27 -15
  305. package/src/commanderAdapter.test.ts +30 -0
  306. package/src/commanderAdapter.ts +7 -0
  307. package/src/commands/daemon.test.ts +238 -0
  308. package/src/commands/daemon.ts +135 -0
  309. package/src/completion.ts +2 -1
  310. package/src/constants.ts +3 -0
  311. package/src/daemon.test.ts +88 -0
  312. package/src/daemon.ts +26 -14
  313. package/src/discovery.ts +52 -2
  314. package/src/electron-apps.test.ts +50 -0
  315. package/src/electron-apps.ts +89 -0
  316. package/src/engine.test.ts +45 -9
  317. package/src/execution.ts +24 -19
  318. package/src/idle-manager.ts +60 -0
  319. package/src/launcher.test.ts +67 -0
  320. package/src/launcher.ts +185 -0
  321. package/src/main.ts +3 -2
  322. package/src/registry.test.ts +15 -0
  323. package/src/registry.ts +32 -3
  324. package/src/runtime.ts +13 -7
  325. package/src/serialization.test.ts +19 -1
  326. package/src/serialization.ts +2 -0
  327. package/src/tui.test.ts +23 -0
  328. package/src/tui.ts +65 -0
  329. package/src/weixin-download.test.ts +27 -0
  330. package/tests/e2e/browser-public-extended.test.ts +6 -2
  331. package/chatwise-opencli.ps1 +0 -82
  332. package/dist/clis/chatwise/shared.d.ts +0 -2
  333. package/dist/clis/chatwise/shared.js +0 -6
  334. package/src/clis/chatwise/shared.ts +0 -8
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getElectronApp, isElectronApp, loadApps } from './electron-apps.js';
3
+
4
+ describe('electron-apps registry', () => {
5
+ it('returns builtin app entry for cursor', () => {
6
+ const app = getElectronApp('cursor');
7
+ expect(app).toBeDefined();
8
+ expect(app!.port).toBe(9226);
9
+ expect(app!.processName).toBe('Cursor');
10
+ });
11
+
12
+ it('returns builtin app entry for codex', () => {
13
+ const app = getElectronApp('codex');
14
+ expect(app).toBeDefined();
15
+ expect(app!.port).toBe(9222);
16
+ });
17
+
18
+ it('returns undefined for non-Electron sites', () => {
19
+ expect(getElectronApp('bilibili')).toBeUndefined();
20
+ expect(getElectronApp('hackernews')).toBeUndefined();
21
+ });
22
+
23
+ it('isElectronApp returns true for registered apps', () => {
24
+ expect(isElectronApp('cursor')).toBe(true);
25
+ expect(isElectronApp('codex')).toBe(true);
26
+ expect(isElectronApp('chatwise')).toBe(true);
27
+ });
28
+
29
+ it('isElectronApp returns false for non-Electron sites', () => {
30
+ expect(isElectronApp('bilibili')).toBe(false);
31
+ expect(isElectronApp('unknown-app')).toBe(false);
32
+ });
33
+
34
+ it('loadApps merges user config additively', () => {
35
+ const apps = loadApps({
36
+ myapp: { port: 9234, processName: 'MyApp' },
37
+ });
38
+ expect(apps.myapp).toBeDefined();
39
+ expect(apps.myapp.port).toBe(9234);
40
+ // Builtins still present
41
+ expect(apps.cursor).toBeDefined();
42
+ });
43
+
44
+ it('loadApps does not override builtin entries', () => {
45
+ const apps = loadApps({
46
+ cursor: { port: 9999, processName: 'FakeCursor' },
47
+ });
48
+ expect(apps.cursor.port).toBe(9226); // Builtin wins
49
+ });
50
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Electron app registry — maps site names to launch metadata.
3
+ *
4
+ * Builtin apps are defined here. User-defined apps are loaded
5
+ * from ~/.opencli/apps.yaml (additive only, does not override builtins).
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import yaml from 'js-yaml';
12
+
13
+ export interface ElectronAppEntry {
14
+ /** CDP debug port (unique per app) */
15
+ port: number;
16
+ /** macOS process name for detection via pgrep */
17
+ processName: string;
18
+ /** macOS bundle ID for path discovery */
19
+ bundleId?: string;
20
+ /** Human-readable name for prompts */
21
+ displayName?: string;
22
+ /** Additional launch args beyond --remote-debugging-port */
23
+ extraArgs?: string[];
24
+ }
25
+
26
+ export const builtinApps: Record<string, ElectronAppEntry> = {
27
+ cursor: { port: 9226, processName: 'Cursor', bundleId: 'com.todesktop.runtime.Cursor', displayName: 'Cursor' },
28
+ codex: { port: 9222, processName: 'Codex', bundleId: 'com.openai.codex', displayName: 'Codex' },
29
+ chatwise: { port: 9228, processName: 'ChatWise', bundleId: 'com.chatwise.app', displayName: 'ChatWise' },
30
+ notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' },
31
+ 'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' },
32
+ 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' },
33
+ antigravity: { port: 9234, processName: 'Antigravity', bundleId: 'dev.antigravity.app', displayName: 'Antigravity' },
34
+ chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
35
+ };
36
+
37
+ /** Merge builtin + user-defined apps. User entries are additive only. */
38
+ export function loadApps(
39
+ userApps?: Record<string, Omit<ElectronAppEntry, 'displayName'> & { displayName?: string }>,
40
+ ): Record<string, ElectronAppEntry> {
41
+ const merged = { ...builtinApps };
42
+ if (userApps) {
43
+ for (const [name, entry] of Object.entries(userApps)) {
44
+ if (!(name in merged)) {
45
+ merged[name] = entry as ElectronAppEntry;
46
+ }
47
+ }
48
+ }
49
+ return merged;
50
+ }
51
+
52
+ let _apps: Record<string, ElectronAppEntry> | null = null;
53
+
54
+ function ensureLoaded(): Record<string, ElectronAppEntry> {
55
+ if (_apps) return _apps;
56
+
57
+ let userApps: Record<string, ElectronAppEntry> | undefined;
58
+ try {
59
+ const yamlPath = path.join(os.homedir(), '.opencli', 'apps.yaml');
60
+ if (fs.existsSync(yamlPath)) {
61
+ const content = fs.readFileSync(yamlPath, 'utf-8');
62
+ const parsed = yaml.load(content) as { apps?: Record<string, ElectronAppEntry> };
63
+ userApps = parsed?.apps;
64
+ }
65
+ } catch {
66
+ // Silently ignore malformed user config
67
+ }
68
+
69
+ _apps = loadApps(userApps);
70
+ return _apps;
71
+ }
72
+
73
+ export function getElectronApp(site: string): ElectronAppEntry | undefined {
74
+ return ensureLoaded()[site];
75
+ }
76
+
77
+ export function isElectronApp(site: string): boolean {
78
+ return site in ensureLoaded();
79
+ }
80
+
81
+ /** Get all registered apps (builtin + user-defined). */
82
+ export function getAllElectronApps(): Record<string, ElectronAppEntry> {
83
+ return ensureLoaded();
84
+ }
85
+
86
+ /** Reset loaded apps (for testing). */
87
+ export function _resetRegistry(): void {
88
+ _apps = null;
89
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { discoverClis, discoverPlugins, PLUGINS_DIR } from './discovery.js';
2
+ import { discoverClis, discoverPlugins, ensureUserCliCompatShims, PLUGINS_DIR } from './discovery.js';
3
3
  import { executeCommand } from './execution.js';
4
4
  import { getRegistry, cli, Strategy } from './registry.js';
5
5
  import { clearAllHooks, onAfterExecute } from './hooks.js';
@@ -78,6 +78,39 @@ cli({
78
78
  await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
79
79
  }
80
80
  });
81
+
82
+ it('loads legacy user TS CLI modules via compatibility shims', async () => {
83
+ const tempOpencliRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-user-clis-'));
84
+ const userClisDir = path.join(tempOpencliRoot, 'clis');
85
+ const siteDir = path.join(userClisDir, 'legacy-site');
86
+ const commandPath = path.join(siteDir, 'hello.ts');
87
+
88
+ try {
89
+ await ensureUserCliCompatShims(tempOpencliRoot);
90
+ await fs.promises.mkdir(siteDir, { recursive: true });
91
+ await fs.promises.writeFile(commandPath, `
92
+ import { cli, Strategy } from '../../registry';
93
+ import { CommandExecutionError } from '../../errors';
94
+
95
+ cli({
96
+ site: 'legacy-site',
97
+ name: 'hello',
98
+ description: 'hello command',
99
+ strategy: Strategy.PUBLIC,
100
+ browser: false,
101
+ func: async () => [{ ok: true, errorName: new CommandExecutionError('boom').name }],
102
+ });
103
+ `);
104
+
105
+ await discoverClis(userClisDir);
106
+
107
+ const cmd = getRegistry().get('legacy-site/hello');
108
+ expect(cmd).toBeDefined();
109
+ await expect(executeCommand(cmd!, {})).resolves.toEqual([{ ok: true, errorName: 'CommandExecutionError' }]);
110
+ } finally {
111
+ await fs.promises.rm(tempOpencliRoot, { recursive: true, force: true });
112
+ }
113
+ });
81
114
  });
82
115
 
83
116
  describe('discoverPlugins', () => {
@@ -266,22 +299,25 @@ describe('executeCommand', () => {
266
299
  expect(typeof seen[0].finishedAt).toBe('number');
267
300
  });
268
301
 
269
- it('fails fast for chatwise commands when OPENCLI_CDP_ENDPOINT is missing', async () => {
302
+ it('uses launcher for registered Electron apps (chatwise)', async () => {
303
+ // Mock the launcher to return a fake endpoint (avoids real HTTP/process calls)
304
+ const launcher = await import('./launcher.js');
305
+ const spy = vi.spyOn(launcher, 'resolveElectronEndpoint')
306
+ .mockResolvedValue('http://127.0.0.1:9228');
307
+
270
308
  const cmd = cli({
271
309
  site: 'chatwise',
272
310
  name: 'status',
273
311
  description: 'chatwise status',
274
312
  browser: true,
275
313
  strategy: Strategy.PUBLIC,
276
- requiredEnv: [
277
- {
278
- name: 'OPENCLI_CDP_ENDPOINT',
279
- help: 'Set OPENCLI_CDP_ENDPOINT before running chatwise commands.',
280
- },
281
- ],
282
314
  func: async () => [{ ok: true }],
283
315
  });
284
316
 
285
- await expect(executeCommand(cmd, {})).rejects.toThrow('requires environment variable OPENCLI_CDP_ENDPOINT');
317
+ // CDPBridge.connect() will fail (no actual CDP server), but the launcher
318
+ // should have been called with 'chatwise'.
319
+ await expect(executeCommand(cmd, {})).rejects.toThrow();
320
+ expect(spy).toHaveBeenCalledWith('chatwise');
321
+ spy.mockRestore();
286
322
  });
287
323
  });
package/src/execution.ts CHANGED
@@ -20,6 +20,8 @@ import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMM
20
20
  import { emitHook, type HookContext } from './hooks.js';
21
21
  import { checkDaemonStatus } from './browser/discover.js';
22
22
  import { log } from './logger.js';
23
+ import { isElectronApp } from './electron-apps.js';
24
+ import { resolveElectronEndpoint } from './launcher.js';
23
25
 
24
26
  const _loadedModules = new Set<string>();
25
27
 
@@ -169,24 +171,29 @@ export async function executeCommand(
169
171
  let result: unknown;
170
172
  try {
171
173
  if (shouldUseBrowserSession(cmd)) {
172
- // ── Fail-fast: only when daemon is UP but extension is not connected ──
173
- // If daemon is not running, let browserSession() handle auto-start as usual.
174
- // We only short-circuit when the daemon confirms the extension is missing —
175
- // that's a clear setup gap, not a transient startup state.
176
- // Use a short timeout: localhost responds in <50ms when running.
177
- // 300ms avoids a full 2s wait on cold-start (daemon not yet running).
178
- const status = await checkDaemonStatus({ timeout: 300 });
179
- if (status.running && !status.extensionConnected) {
180
- throw new BrowserConnectError(
181
- 'Browser Bridge extension not connected',
182
- 'Install the Browser Bridge:\n' +
183
- ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
184
- ' 2. chrome://extensions Developer Mode → Load unpacked\n' +
185
- ' Then run: opencli doctor',
186
- );
174
+ const electron = isElectronApp(cmd.site);
175
+ let cdpEndpoint: string | undefined;
176
+
177
+ if (electron) {
178
+ // Electron apps: auto-detect, prompt restart if needed, launch with CDP
179
+ cdpEndpoint = await resolveElectronEndpoint(cmd.site);
180
+ } else {
181
+ // Browser Bridge: fail-fast when daemon is up but extension is missing.
182
+ // 300ms timeout avoids a full 2s wait on cold-start.
183
+ const status = await checkDaemonStatus({ timeout: 300 });
184
+ if (status.running && !status.extensionConnected) {
185
+ throw new BrowserConnectError(
186
+ 'Browser Bridge extension not connected',
187
+ 'Install the Browser Bridge:\n' +
188
+ ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
189
+ ' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
190
+ ' Then run: opencli doctor',
191
+ );
192
+ }
187
193
  }
194
+
188
195
  ensureRequiredEnv(cmd);
189
- const BrowserFactory = getBrowserFactory();
196
+ const BrowserFactory = getBrowserFactory(cmd.site);
190
197
  result = await browserSession(BrowserFactory, async (page) => {
191
198
  const preNavUrl = resolvePreNav(cmd);
192
199
  if (preNavUrl) {
@@ -195,8 +202,6 @@ export async function executeCommand(
195
202
  if (debug) log.debug('[pre-nav] Already on target domain, skipping navigation');
196
203
  } else {
197
204
  try {
198
- // goto() already includes smart DOM-settle detection (waitForDomStable).
199
- // No additional fixed sleep needed.
200
205
  await page.goto(preNavUrl);
201
206
  } catch (err) {
202
207
  if (debug) log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
@@ -207,7 +212,7 @@ export async function executeCommand(
207
212
  timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
208
213
  label: fullName(cmd),
209
214
  });
210
- }, { workspace: `site:${cmd.site}` });
215
+ }, { workspace: `site:${cmd.site}`, cdpEndpoint });
211
216
  } else {
212
217
  // Non-browser commands: apply timeout only when explicitly configured.
213
218
  const timeout = cmd.timeoutSeconds;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Manages daemon idle timeout with dual-condition logic:
3
+ * exits only when BOTH CLI is idle AND Extension is disconnected.
4
+ */
5
+ export class IdleManager {
6
+ private _timer: ReturnType<typeof setTimeout> | null = null;
7
+ private _lastCliRequestTime = Date.now();
8
+ private _extensionConnected = false;
9
+ private _timeoutMs: number;
10
+ private _onExit: () => void;
11
+
12
+ constructor(timeoutMs: number, onExit: () => void) {
13
+ this._timeoutMs = timeoutMs;
14
+ this._onExit = onExit;
15
+ }
16
+
17
+ get lastCliRequestTime(): number {
18
+ return this._lastCliRequestTime;
19
+ }
20
+
21
+ /** Call when an HTTP request arrives from CLI */
22
+ onCliRequest(): void {
23
+ this._lastCliRequestTime = Date.now();
24
+ this._resetTimer();
25
+ }
26
+
27
+ /** Call when Extension WebSocket connects or disconnects */
28
+ setExtensionConnected(connected: boolean): void {
29
+ this._extensionConnected = connected;
30
+ if (connected) {
31
+ this._clearTimer();
32
+ } else {
33
+ this._resetTimer();
34
+ }
35
+ }
36
+
37
+ private _clearTimer(): void {
38
+ if (this._timer) {
39
+ clearTimeout(this._timer);
40
+ this._timer = null;
41
+ }
42
+ }
43
+
44
+ private _resetTimer(): void {
45
+ this._clearTimer();
46
+
47
+ if (this._timeoutMs <= 0) return;
48
+ if (this._extensionConnected) return;
49
+
50
+ const elapsed = Date.now() - this._lastCliRequestTime;
51
+ if (elapsed >= this._timeoutMs) {
52
+ this._onExit();
53
+ return;
54
+ }
55
+
56
+ this._timer = setTimeout(() => {
57
+ this._onExit();
58
+ }, this._timeoutMs - elapsed);
59
+ }
60
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { probeCDP, detectProcess, discoverAppPath } from './launcher.js';
3
+
4
+ vi.mock('node:child_process', () => ({
5
+ execFileSync: vi.fn(),
6
+ spawn: vi.fn(() => ({
7
+ unref: vi.fn(),
8
+ pid: 12345,
9
+ on: vi.fn(),
10
+ })),
11
+ }));
12
+
13
+ const cp = vi.mocked(await import('node:child_process'));
14
+
15
+ describe('probeCDP', () => {
16
+ it('returns false when CDP endpoint is unreachable', async () => {
17
+ const result = await probeCDP(59999, 500);
18
+ expect(result).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe('detectProcess', () => {
23
+ beforeEach(() => {
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it('returns false when pgrep finds no process', () => {
28
+ cp.execFileSync.mockImplementation(() => {
29
+ const err = new Error('exit 1') as Error & { status: number };
30
+ err.status = 1;
31
+ throw err;
32
+ });
33
+ const result = detectProcess('NonExistentApp');
34
+ expect(result).toBe(false);
35
+ });
36
+
37
+ it('returns true when pgrep finds a process', () => {
38
+ cp.execFileSync.mockReturnValue('12345\n');
39
+ const result = detectProcess('Cursor');
40
+ expect(result).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe('discoverAppPath', () => {
45
+ beforeEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ it.skipIf(process.platform !== 'darwin')('returns path when osascript succeeds', () => {
50
+ cp.execFileSync.mockReturnValue('/Applications/Cursor.app/\n');
51
+ const result = discoverAppPath('Cursor');
52
+ expect(result).toBe('/Applications/Cursor.app');
53
+ });
54
+
55
+ it.skipIf(process.platform !== 'darwin')('returns null when osascript fails', () => {
56
+ cp.execFileSync.mockImplementation(() => {
57
+ throw new Error('app not found');
58
+ });
59
+ const result = discoverAppPath('NonExistent');
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ it.skipIf(process.platform === 'darwin')('returns null on non-darwin platform', () => {
64
+ const result = discoverAppPath('Cursor');
65
+ expect(result).toBeNull();
66
+ });
67
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Electron app launcher — auto-detect, confirm, launch, and connect.
3
+ *
4
+ * Flow:
5
+ * 1. Probe CDP port → already running with debug? connect directly
6
+ * 2. Detect process → running without CDP? prompt to restart
7
+ * 3. Discover app path → not installed? error
8
+ * 4. Launch with --remote-debugging-port
9
+ * 5. Poll /json until ready
10
+ */
11
+
12
+ import { execFileSync, spawn } from 'node:child_process';
13
+ import { request as httpRequest } from 'node:http';
14
+ import type { ElectronAppEntry } from './electron-apps.js';
15
+ import { getElectronApp } from './electron-apps.js';
16
+ import { confirmPrompt } from './tui.js';
17
+ import { CommandExecutionError } from './errors.js';
18
+ import { log } from './logger.js';
19
+
20
+ const POLL_INTERVAL_MS = 500;
21
+ const POLL_TIMEOUT_MS = 15_000;
22
+ const PROBE_TIMEOUT_MS = 2_000;
23
+ const KILL_GRACE_MS = 3_000;
24
+
25
+ /**
26
+ * Probe whether a CDP endpoint is listening on the given port.
27
+ * Returns true if http://127.0.0.1:{port}/json responds successfully.
28
+ */
29
+ export function probeCDP(port: number, timeoutMs: number = PROBE_TIMEOUT_MS): Promise<boolean> {
30
+ return new Promise((resolve) => {
31
+ const req = httpRequest(
32
+ { hostname: '127.0.0.1', port, path: '/json', method: 'GET', timeout: timeoutMs },
33
+ (res) => {
34
+ res.resume();
35
+ resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300);
36
+ },
37
+ );
38
+ req.on('error', () => resolve(false));
39
+ req.on('timeout', () => { req.destroy(); resolve(false); });
40
+ req.end();
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Check if a process with the given name is running.
46
+ * Uses pgrep on macOS/Linux.
47
+ */
48
+ export function detectProcess(processName: string): boolean {
49
+ try {
50
+ execFileSync('pgrep', ['-x', processName], { encoding: 'utf-8', stdio: 'pipe' });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Kill a process by name. Sends SIGTERM first, then SIGKILL after grace period.
59
+ */
60
+ export function killProcess(processName: string): void {
61
+ try {
62
+ execFileSync('pkill', ['-x', processName], { stdio: 'pipe' });
63
+ } catch {
64
+ // Process may have already exited
65
+ }
66
+
67
+ const deadline = Date.now() + KILL_GRACE_MS;
68
+ while (Date.now() < deadline) {
69
+ if (!detectProcess(processName)) return;
70
+ execFileSync('sleep', ['0.2'], { stdio: 'pipe' });
71
+ }
72
+
73
+ try {
74
+ execFileSync('pkill', ['-9', '-x', processName], { stdio: 'pipe' });
75
+ } catch {
76
+ // Ignore
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Discover the app installation path on macOS.
82
+ * Uses osascript to resolve the app name to a POSIX path.
83
+ * Returns null if the app is not installed.
84
+ */
85
+ export function discoverAppPath(displayName: string): string | null {
86
+ if (process.platform !== 'darwin') {
87
+ return null;
88
+ }
89
+
90
+ try {
91
+ const result = execFileSync('osascript', [
92
+ '-e', `POSIX path of (path to application "${displayName}")`,
93
+ ], { encoding: 'utf-8', stdio: 'pipe', timeout: 5_000 });
94
+ return result.trim().replace(/\/$/, '');
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function resolveExecutable(appPath: string, processName: string): string {
101
+ return `${appPath}/Contents/MacOS/${processName}`;
102
+ }
103
+
104
+ async function pollForReady(port: number): Promise<void> {
105
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
106
+ while (Date.now() < deadline) {
107
+ if (await probeCDP(port, 1_000)) return;
108
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
109
+ }
110
+ throw new CommandExecutionError(
111
+ `App launched but CDP not available on port ${port} after ${POLL_TIMEOUT_MS / 1000}s`,
112
+ 'The app may be slow to start. Try running the command again.',
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Main entry point: resolve an Electron app to a CDP endpoint URL.
118
+ *
119
+ * Returns the endpoint URL: http://127.0.0.1:{port}
120
+ */
121
+ export async function resolveElectronEndpoint(site: string): Promise<string> {
122
+ const app = getElectronApp(site);
123
+ if (!app) {
124
+ throw new CommandExecutionError(
125
+ `No Electron app registered for site "${site}"`,
126
+ 'Register the app in ~/.opencli/apps.yaml or check the site name.',
127
+ );
128
+ }
129
+
130
+ const { port, processName, displayName } = app;
131
+ const label = displayName ?? processName;
132
+ const endpoint = `http://127.0.0.1:${port}`;
133
+
134
+ // Step 1: Already running with CDP?
135
+ log.debug(`[launcher] Probing CDP on port ${port}...`);
136
+ if (await probeCDP(port)) {
137
+ log.debug(`[launcher] CDP already available on port ${port}`);
138
+ return endpoint;
139
+ }
140
+
141
+ // Step 2: Running without CDP?
142
+ const isRunning = detectProcess(processName);
143
+ if (isRunning) {
144
+ log.debug(`[launcher] ${label} is running but CDP not available`);
145
+ const confirmed = await confirmPrompt(
146
+ `${label} is running but CDP is not enabled. Restart with debug port?`,
147
+ true,
148
+ );
149
+ if (!confirmed) {
150
+ throw new CommandExecutionError(
151
+ `${label} needs to be restarted with CDP enabled.`,
152
+ `Manually restart: kill the app and relaunch with --remote-debugging-port=${port}`,
153
+ );
154
+ }
155
+ process.stderr.write(` Restarting ${label}...\n`);
156
+ killProcess(processName);
157
+ }
158
+
159
+ // Step 3: Discover path
160
+ const appPath = discoverAppPath(label);
161
+ if (!appPath) {
162
+ throw new CommandExecutionError(
163
+ `Could not find ${label} on this machine.`,
164
+ `Install ${label} or register a custom path in ~/.opencli/apps.yaml`,
165
+ );
166
+ }
167
+
168
+ // Step 4: Launch
169
+ const executable = resolveExecutable(appPath, processName);
170
+ const args = [`--remote-debugging-port=${port}`, ...(app.extraArgs ?? [])];
171
+ log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
172
+
173
+ const child = spawn(executable, args, {
174
+ detached: true,
175
+ stdio: 'ignore',
176
+ });
177
+ child.unref();
178
+
179
+ // Step 5: Poll for readiness
180
+ process.stderr.write(` Waiting for ${label} on port ${port}...\n`);
181
+ await pollForReady(port);
182
+ process.stderr.write(` Connected to ${label} on port ${port}.\n`);
183
+
184
+ return endpoint;
185
+ }
package/src/main.ts CHANGED
@@ -16,7 +16,7 @@ if (process.platform !== 'win32') {
16
16
  import * as os from 'node:os';
17
17
  import * as path from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
- import { discoverClis, discoverPlugins } from './discovery.js';
19
+ import { discoverClis, discoverPlugins, ensureUserCliCompatShims, USER_CLIS_DIR } from './discovery.js';
20
20
  import { getCompletions } from './completion.js';
21
21
  import { runCli } from './cli.js';
22
22
  import { emitHook } from './hooks.js';
@@ -29,9 +29,10 @@ installNodeNetwork();
29
29
  const __filename = fileURLToPath(import.meta.url);
30
30
  const __dirname = path.dirname(__filename);
31
31
  const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
32
- const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
32
+ const USER_CLIS = USER_CLIS_DIR;
33
33
 
34
34
  // Sequential: plugins must run after built-in discovery so they can override built-in commands.
35
+ await ensureUserCliCompatShims();
35
36
  await discoverClis(BUILTIN_CLIS, USER_CLIS);
36
37
  await discoverPlugins();
37
38
 
@@ -60,6 +60,21 @@ describe('cli() registration', () => {
60
60
  const reg = getRegistry();
61
61
  expect(reg.get('test-registry/overwrite')?.description).toBe('v2');
62
62
  });
63
+
64
+ it('registers aliases as alternate registry keys for the same command', () => {
65
+ const cmd = cli({
66
+ site: 'test-registry',
67
+ name: 'canonical',
68
+ description: 'test aliases',
69
+ aliases: ['compat', 'legacy-name'],
70
+ });
71
+
72
+ const registry = getRegistry();
73
+ expect(cmd.aliases).toEqual(['compat', 'legacy-name']);
74
+ expect(registry.get('test-registry/canonical')).toBe(cmd);
75
+ expect(registry.get('test-registry/compat')).toBe(cmd);
76
+ expect(registry.get('test-registry/legacy-name')).toBe(cmd);
77
+ });
63
78
  });
64
79
 
65
80
  describe('fullName', () => {