@jackwener/opencli 1.4.1 → 1.5.1

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 (369) hide show
  1. package/.github/workflows/build-extension.yml +2 -6
  2. package/.github/workflows/ci.yml +21 -1
  3. package/README.md +35 -6
  4. package/README.zh-CN.md +12 -5
  5. package/SKILL.md +2 -0
  6. package/dist/browser/cdp.d.ts +2 -1
  7. package/dist/browser/cdp.js +5 -0
  8. package/dist/browser/discover.d.ts +4 -1
  9. package/dist/browser/discover.js +6 -2
  10. package/dist/browser/errors.d.ts +2 -2
  11. package/dist/browser/errors.js +4 -12
  12. package/dist/browser/mcp.d.ts +2 -1
  13. package/dist/browser/page.d.ts +3 -0
  14. package/dist/browser/page.js +24 -1
  15. package/dist/build-manifest.d.ts +2 -0
  16. package/dist/build-manifest.js +39 -14
  17. package/dist/build-manifest.test.js +21 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +2 -1
  20. package/dist/cli-manifest.json +1567 -108
  21. package/dist/cli.js +68 -6
  22. package/dist/clis/36kr/article.d.ts +1 -0
  23. package/dist/clis/36kr/article.js +62 -0
  24. package/dist/clis/36kr/hot.d.ts +3 -0
  25. package/dist/clis/36kr/hot.js +80 -0
  26. package/dist/clis/36kr/hot.test.d.ts +1 -0
  27. package/dist/clis/36kr/hot.test.js +15 -0
  28. package/dist/clis/36kr/news.d.ts +1 -0
  29. package/dist/clis/36kr/news.js +51 -0
  30. package/dist/clis/36kr/news.test.d.ts +1 -0
  31. package/dist/clis/36kr/news.test.js +85 -0
  32. package/dist/clis/36kr/search.d.ts +1 -0
  33. package/dist/clis/36kr/search.js +72 -0
  34. package/dist/clis/bilibili/comments.d.ts +5 -0
  35. package/dist/clis/bilibili/comments.js +40 -0
  36. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  37. package/dist/clis/bilibili/comments.test.js +82 -0
  38. package/dist/clis/bluesky/feeds.yaml +29 -0
  39. package/dist/clis/bluesky/followers.yaml +33 -0
  40. package/dist/clis/bluesky/following.yaml +33 -0
  41. package/dist/clis/bluesky/profile.yaml +27 -0
  42. package/dist/clis/bluesky/search.yaml +34 -0
  43. package/dist/clis/bluesky/starter-packs.yaml +34 -0
  44. package/dist/clis/bluesky/thread.yaml +32 -0
  45. package/dist/clis/bluesky/trending.yaml +27 -0
  46. package/dist/clis/bluesky/user.yaml +34 -0
  47. package/dist/clis/chatgpt/ask.js +29 -14
  48. package/dist/clis/chatgpt/ax.d.ts +6 -0
  49. package/dist/clis/chatgpt/ax.js +172 -1
  50. package/dist/clis/chatgpt/model.d.ts +1 -0
  51. package/dist/clis/chatgpt/model.js +24 -0
  52. package/dist/clis/chatgpt/send.js +12 -3
  53. package/dist/clis/douban/download.d.ts +1 -0
  54. package/dist/clis/douban/download.js +67 -0
  55. package/dist/clis/douban/download.test.d.ts +1 -0
  56. package/dist/clis/douban/download.test.js +170 -0
  57. package/dist/clis/douban/photos.d.ts +1 -0
  58. package/dist/clis/douban/photos.js +34 -0
  59. package/dist/clis/douban/utils.d.ts +25 -0
  60. package/dist/clis/douban/utils.js +190 -1
  61. package/dist/clis/douban/utils.test.d.ts +1 -0
  62. package/dist/clis/douban/utils.test.js +64 -0
  63. package/dist/clis/imdb/person.d.ts +1 -0
  64. package/dist/clis/imdb/person.js +203 -0
  65. package/dist/clis/imdb/reviews.d.ts +1 -0
  66. package/dist/clis/imdb/reviews.js +88 -0
  67. package/dist/clis/imdb/search.d.ts +1 -0
  68. package/dist/clis/imdb/search.js +161 -0
  69. package/dist/clis/imdb/title.d.ts +1 -0
  70. package/dist/clis/imdb/title.js +93 -0
  71. package/dist/clis/imdb/top.d.ts +1 -0
  72. package/dist/clis/imdb/top.js +53 -0
  73. package/dist/clis/imdb/trending.d.ts +1 -0
  74. package/dist/clis/imdb/trending.js +52 -0
  75. package/dist/clis/imdb/utils.d.ts +46 -0
  76. package/dist/clis/imdb/utils.js +285 -0
  77. package/dist/clis/imdb/utils.test.d.ts +1 -0
  78. package/dist/clis/imdb/utils.test.js +88 -0
  79. package/dist/clis/jd/item.d.ts +4 -0
  80. package/dist/clis/jd/item.js +16 -15
  81. package/dist/clis/jd/item.test.js +16 -1
  82. package/dist/clis/linux-do/categories.yaml +38 -9
  83. package/dist/clis/linux-do/category.d.ts +1 -0
  84. package/dist/clis/linux-do/category.js +36 -0
  85. package/dist/clis/linux-do/feed.d.ts +45 -0
  86. package/dist/clis/linux-do/feed.js +397 -0
  87. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  88. package/dist/clis/linux-do/feed.test.js +118 -0
  89. package/dist/clis/linux-do/hot.d.ts +1 -0
  90. package/dist/clis/linux-do/hot.js +25 -0
  91. package/dist/clis/linux-do/latest.d.ts +1 -0
  92. package/dist/clis/linux-do/latest.js +18 -0
  93. package/dist/clis/linux-do/tags.yaml +41 -0
  94. package/dist/clis/linux-do/topic.yaml +41 -3
  95. package/dist/clis/linux-do/user-posts.yaml +67 -0
  96. package/dist/clis/linux-do/user-topics.yaml +54 -0
  97. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  98. package/dist/clis/paperreview/commands.test.js +243 -0
  99. package/dist/clis/paperreview/feedback.d.ts +1 -0
  100. package/dist/clis/paperreview/feedback.js +52 -0
  101. package/dist/clis/paperreview/review.d.ts +1 -0
  102. package/dist/clis/paperreview/review.js +37 -0
  103. package/dist/clis/paperreview/submit.d.ts +1 -0
  104. package/dist/clis/paperreview/submit.js +85 -0
  105. package/dist/clis/paperreview/utils.d.ts +46 -0
  106. package/dist/clis/paperreview/utils.js +197 -0
  107. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  108. package/dist/clis/paperreview/utils.test.js +49 -0
  109. package/dist/clis/producthunt/browse.d.ts +1 -0
  110. package/dist/clis/producthunt/browse.js +99 -0
  111. package/dist/clis/producthunt/hot.d.ts +1 -0
  112. package/dist/clis/producthunt/hot.js +110 -0
  113. package/dist/clis/producthunt/posts.d.ts +1 -0
  114. package/dist/clis/producthunt/posts.js +28 -0
  115. package/dist/clis/producthunt/today.d.ts +1 -0
  116. package/dist/clis/producthunt/today.js +35 -0
  117. package/dist/clis/producthunt/utils.d.ts +29 -0
  118. package/dist/clis/producthunt/utils.js +99 -0
  119. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  120. package/dist/clis/producthunt/utils.test.js +64 -0
  121. package/dist/clis/twitter/article.js +4 -28
  122. package/dist/clis/twitter/likes.d.ts +24 -0
  123. package/dist/clis/twitter/likes.js +217 -0
  124. package/dist/clis/twitter/likes.test.d.ts +1 -0
  125. package/dist/clis/twitter/likes.test.js +85 -0
  126. package/dist/clis/twitter/profile.js +4 -28
  127. package/dist/clis/twitter/search.js +2 -1
  128. package/dist/clis/twitter/search.test.js +2 -0
  129. package/dist/clis/twitter/shared.d.ts +6 -0
  130. package/dist/clis/twitter/shared.js +35 -0
  131. package/dist/clis/twitter/timeline.js +2 -13
  132. package/dist/clis/twitter/trending.js +29 -61
  133. package/dist/clis/v2ex/hot.yaml +17 -3
  134. package/dist/clis/weixin/download.d.ts +17 -0
  135. package/dist/clis/weixin/download.js +88 -20
  136. package/dist/clis/weread/book.js +2 -2
  137. package/dist/clis/weread/commands.test.d.ts +3 -0
  138. package/dist/clis/weread/commands.test.js +43 -0
  139. package/dist/clis/weread/highlights.js +2 -2
  140. package/dist/clis/weread/notebooks.js +2 -2
  141. package/dist/clis/weread/notes.js +3 -3
  142. package/dist/clis/weread/shelf.js +2 -2
  143. package/dist/clis/weread/utils.d.ts +4 -4
  144. package/dist/clis/weread/utils.js +32 -14
  145. package/dist/clis/weread/utils.test.js +1 -28
  146. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  147. package/dist/clis/xiaohongshu/comments.js +74 -0
  148. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  149. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  150. package/dist/clis/xiaohongshu/publish.js +179 -47
  151. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  152. package/dist/clis/xiaohongshu/publish.test.js +131 -0
  153. package/dist/clis/xiaohongshu/search.d.ts +8 -1
  154. package/dist/clis/xiaohongshu/search.js +20 -1
  155. package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
  156. package/dist/clis/xiaohongshu/search.test.js +32 -1
  157. package/dist/commanderAdapter.d.ts +1 -0
  158. package/dist/commanderAdapter.js +176 -29
  159. package/dist/commanderAdapter.test.d.ts +1 -0
  160. package/dist/commanderAdapter.test.js +62 -0
  161. package/dist/daemon.js +17 -1
  162. package/dist/discovery.js +48 -42
  163. package/dist/doctor.d.ts +2 -2
  164. package/dist/doctor.js +11 -4
  165. package/dist/download/index.js +63 -51
  166. package/dist/download/index.test.js +17 -4
  167. package/dist/engine.test.js +42 -0
  168. package/dist/errors.d.ts +4 -2
  169. package/dist/errors.js +17 -34
  170. package/dist/execution.d.ts +1 -3
  171. package/dist/execution.js +66 -8
  172. package/dist/execution.test.d.ts +1 -0
  173. package/dist/execution.test.js +40 -0
  174. package/dist/external.js +6 -1
  175. package/dist/hooks.js +2 -0
  176. package/dist/main.js +6 -0
  177. package/dist/output.js +5 -1
  178. package/dist/pipeline/executor.js +3 -4
  179. package/dist/plugin-manifest.d.ts +70 -0
  180. package/dist/plugin-manifest.js +160 -0
  181. package/dist/plugin-manifest.test.d.ts +4 -0
  182. package/dist/plugin-manifest.test.js +179 -0
  183. package/dist/plugin-scaffold.d.ts +28 -0
  184. package/dist/plugin-scaffold.js +142 -0
  185. package/dist/plugin-scaffold.test.d.ts +4 -0
  186. package/dist/plugin-scaffold.test.js +83 -0
  187. package/dist/plugin.d.ts +82 -11
  188. package/dist/plugin.js +870 -84
  189. package/dist/plugin.test.js +1032 -17
  190. package/dist/registry.d.ts +4 -0
  191. package/dist/registry.js +2 -0
  192. package/dist/runtime-detect.d.ts +21 -0
  193. package/dist/runtime-detect.js +32 -0
  194. package/dist/runtime-detect.test.d.ts +1 -0
  195. package/dist/runtime-detect.test.js +27 -0
  196. package/dist/runtime.d.ts +1 -0
  197. package/dist/runtime.js +2 -2
  198. package/dist/serialization.d.ts +2 -0
  199. package/dist/serialization.js +6 -0
  200. package/dist/types.d.ts +3 -0
  201. package/dist/update-check.d.ts +22 -0
  202. package/dist/update-check.js +112 -0
  203. package/dist/weixin-download.test.d.ts +1 -0
  204. package/dist/weixin-download.test.js +30 -0
  205. package/dist/weread-private-api-regression.test.d.ts +1 -0
  206. package/dist/weread-private-api-regression.test.js +122 -0
  207. package/dist/yaml-schema.d.ts +3 -0
  208. package/dist/yaml-schema.js +18 -1
  209. package/docs/.vitepress/config.mts +4 -0
  210. package/docs/adapters/browser/36kr.md +47 -0
  211. package/docs/adapters/browser/bluesky.md +53 -0
  212. package/docs/adapters/browser/douban.md +14 -0
  213. package/docs/adapters/browser/imdb.md +47 -0
  214. package/docs/adapters/browser/jd.md +2 -2
  215. package/docs/adapters/browser/linux-do.md +181 -20
  216. package/docs/adapters/browser/paperreview.md +43 -0
  217. package/docs/adapters/browser/producthunt.md +49 -0
  218. package/docs/adapters/desktop/chatgpt.md +5 -0
  219. package/docs/adapters/index.md +6 -2
  220. package/docs/advanced/download.md +4 -0
  221. package/docs/advanced/rate-limiter-plugin.md +99 -0
  222. package/docs/guide/electron-app-cli.md +200 -0
  223. package/docs/guide/getting-started.md +1 -0
  224. package/docs/guide/plugins.md +97 -0
  225. package/docs/zh/guide/electron-app-cli.md +188 -0
  226. package/docs/zh/guide/getting-started.md +1 -0
  227. package/docs/zh/guide/plugins.md +65 -0
  228. package/extension/package.json +1 -0
  229. package/extension/scripts/package-release.mjs +179 -0
  230. package/extension/src/background.ts +2 -0
  231. package/package.json +4 -1
  232. package/scripts/postinstall.js +10 -0
  233. package/src/browser/cdp.ts +8 -1
  234. package/src/browser/discover.ts +8 -3
  235. package/src/browser/errors.ts +13 -14
  236. package/src/browser/mcp.ts +2 -1
  237. package/src/browser/page.ts +24 -1
  238. package/src/build-manifest.test.ts +23 -0
  239. package/src/build-manifest.ts +40 -15
  240. package/src/capabilityRouting.ts +2 -1
  241. package/src/cli.ts +69 -6
  242. package/src/clis/36kr/article.ts +69 -0
  243. package/src/clis/36kr/hot.test.ts +19 -0
  244. package/src/clis/36kr/hot.ts +100 -0
  245. package/src/clis/36kr/news.test.ts +90 -0
  246. package/src/clis/36kr/news.ts +54 -0
  247. package/src/clis/36kr/search.ts +78 -0
  248. package/src/clis/bilibili/comments.test.ts +102 -0
  249. package/src/clis/bilibili/comments.ts +44 -0
  250. package/src/clis/bluesky/feeds.yaml +29 -0
  251. package/src/clis/bluesky/followers.yaml +33 -0
  252. package/src/clis/bluesky/following.yaml +33 -0
  253. package/src/clis/bluesky/profile.yaml +27 -0
  254. package/src/clis/bluesky/search.yaml +34 -0
  255. package/src/clis/bluesky/starter-packs.yaml +34 -0
  256. package/src/clis/bluesky/thread.yaml +32 -0
  257. package/src/clis/bluesky/trending.yaml +27 -0
  258. package/src/clis/bluesky/user.yaml +34 -0
  259. package/src/clis/chatgpt/ask.ts +28 -14
  260. package/src/clis/chatgpt/ax.ts +180 -1
  261. package/src/clis/chatgpt/model.ts +27 -0
  262. package/src/clis/chatgpt/send.ts +16 -6
  263. package/src/clis/douban/download.test.ts +196 -0
  264. package/src/clis/douban/download.ts +78 -0
  265. package/src/clis/douban/photos.ts +36 -0
  266. package/src/clis/douban/utils.test.ts +97 -0
  267. package/src/clis/douban/utils.ts +232 -1
  268. package/src/clis/imdb/person.ts +232 -0
  269. package/src/clis/imdb/reviews.ts +111 -0
  270. package/src/clis/imdb/search.ts +179 -0
  271. package/src/clis/imdb/title.ts +121 -0
  272. package/src/clis/imdb/top.ts +67 -0
  273. package/src/clis/imdb/trending.ts +66 -0
  274. package/src/clis/imdb/utils.test.ts +117 -0
  275. package/src/clis/imdb/utils.ts +305 -0
  276. package/src/clis/jd/item.test.ts +18 -1
  277. package/src/clis/jd/item.ts +18 -15
  278. package/src/clis/linux-do/categories.yaml +38 -9
  279. package/src/clis/linux-do/category.ts +37 -0
  280. package/src/clis/linux-do/feed.test.ts +132 -0
  281. package/src/clis/linux-do/feed.ts +501 -0
  282. package/src/clis/linux-do/hot.ts +26 -0
  283. package/src/clis/linux-do/latest.ts +19 -0
  284. package/src/clis/linux-do/tags.yaml +41 -0
  285. package/src/clis/linux-do/topic.yaml +41 -3
  286. package/src/clis/linux-do/user-posts.yaml +67 -0
  287. package/src/clis/linux-do/user-topics.yaml +54 -0
  288. package/src/clis/paperreview/commands.test.ts +283 -0
  289. package/src/clis/paperreview/feedback.ts +64 -0
  290. package/src/clis/paperreview/review.ts +47 -0
  291. package/src/clis/paperreview/submit.ts +119 -0
  292. package/src/clis/paperreview/utils.test.ts +68 -0
  293. package/src/clis/paperreview/utils.ts +276 -0
  294. package/src/clis/producthunt/browse.ts +109 -0
  295. package/src/clis/producthunt/hot.ts +127 -0
  296. package/src/clis/producthunt/posts.ts +29 -0
  297. package/src/clis/producthunt/today.ts +37 -0
  298. package/src/clis/producthunt/utils.test.ts +72 -0
  299. package/src/clis/producthunt/utils.ts +122 -0
  300. package/src/clis/twitter/article.ts +5 -28
  301. package/src/clis/twitter/likes.test.ts +91 -0
  302. package/src/clis/twitter/likes.ts +256 -0
  303. package/src/clis/twitter/profile.ts +5 -28
  304. package/src/clis/twitter/search.test.ts +2 -0
  305. package/src/clis/twitter/search.ts +3 -1
  306. package/src/clis/twitter/shared.ts +45 -0
  307. package/src/clis/twitter/timeline.ts +2 -13
  308. package/src/clis/twitter/trending.ts +29 -77
  309. package/src/clis/v2ex/hot.yaml +17 -3
  310. package/src/clis/weixin/download.ts +114 -20
  311. package/src/clis/weread/book.ts +2 -2
  312. package/src/clis/weread/commands.test.ts +57 -0
  313. package/src/clis/weread/highlights.ts +2 -2
  314. package/src/clis/weread/notebooks.ts +2 -2
  315. package/src/clis/weread/notes.ts +3 -3
  316. package/src/clis/weread/shelf.ts +2 -2
  317. package/src/clis/weread/utils.test.ts +1 -32
  318. package/src/clis/weread/utils.ts +41 -16
  319. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  320. package/src/clis/xiaohongshu/comments.ts +81 -0
  321. package/src/clis/xiaohongshu/publish.test.ts +151 -0
  322. package/src/clis/xiaohongshu/publish.ts +206 -54
  323. package/src/clis/xiaohongshu/search.test.ts +39 -1
  324. package/src/clis/xiaohongshu/search.ts +19 -1
  325. package/src/commanderAdapter.test.ts +78 -0
  326. package/src/commanderAdapter.ts +188 -24
  327. package/src/daemon.ts +19 -1
  328. package/src/discovery.ts +49 -48
  329. package/src/doctor.ts +15 -5
  330. package/src/download/index.test.ts +14 -4
  331. package/src/download/index.ts +67 -55
  332. package/src/engine.test.ts +38 -0
  333. package/src/errors.ts +26 -63
  334. package/src/execution.test.ts +47 -0
  335. package/src/execution.ts +67 -9
  336. package/src/external.ts +6 -1
  337. package/src/hooks.ts +1 -0
  338. package/src/main.ts +7 -0
  339. package/src/output.ts +3 -1
  340. package/src/pipeline/executor.ts +4 -6
  341. package/src/plugin-manifest.test.ts +223 -0
  342. package/src/plugin-manifest.ts +206 -0
  343. package/src/plugin-scaffold.test.ts +98 -0
  344. package/src/plugin-scaffold.ts +170 -0
  345. package/src/plugin.test.ts +1104 -17
  346. package/src/plugin.ts +1101 -86
  347. package/src/registry.ts +6 -1
  348. package/src/runtime-detect.test.ts +30 -0
  349. package/src/runtime-detect.ts +36 -0
  350. package/src/runtime.ts +3 -3
  351. package/src/serialization.ts +4 -0
  352. package/src/types.ts +3 -0
  353. package/src/update-check.ts +114 -0
  354. package/src/weixin-download.test.ts +64 -0
  355. package/src/weread-private-api-regression.test.ts +150 -0
  356. package/src/yaml-schema.ts +20 -0
  357. package/tests/e2e/browser-auth.test.ts +13 -9
  358. package/tests/e2e/browser-public-extended.test.ts +1 -1
  359. package/tests/e2e/browser-public.test.ts +62 -4
  360. package/tests/e2e/helpers.ts +2 -1
  361. package/tests/e2e/public-commands.test.ts +37 -3
  362. package/tests/smoke/api-health.test.ts +1 -1
  363. package/vitest.config.ts +10 -0
  364. package/dist/clis/linux-do/category.yaml +0 -51
  365. package/dist/clis/linux-do/hot.yaml +0 -50
  366. package/dist/clis/linux-do/latest.yaml +0 -40
  367. package/src/clis/linux-do/category.yaml +0 -51
  368. package/src/clis/linux-do/hot.yaml +0 -50
  369. package/src/clis/linux-do/latest.yaml +0 -40
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Plugin manifest: reads and validates opencli-plugin.json files.
3
+ *
4
+ * Supports two modes:
5
+ * 1. Single plugin: repo root IS the plugin directory.
6
+ * 2. Monorepo: repo contains multiple plugins declared in `plugins` field.
7
+ */
8
+ export interface SubPluginEntry {
9
+ /** Relative path from repo root to the sub-plugin directory. */
10
+ path: string;
11
+ version?: string;
12
+ description?: string;
13
+ /** Semver range for opencli compatibility (overrides top-level). */
14
+ opencli?: string;
15
+ /** When true, this sub-plugin is skipped during install. */
16
+ disabled?: boolean;
17
+ }
18
+ export interface PluginManifest {
19
+ /** Plugin name (single-plugin mode). */
20
+ name?: string;
21
+ /** Semantic version of the plugin (single-plugin mode). */
22
+ version?: string;
23
+ /** Semver range for opencli compatibility, e.g. ">=1.0.0". */
24
+ opencli?: string;
25
+ /** Human-readable description. */
26
+ description?: string;
27
+ /** Monorepo sub-plugins. Key = logical plugin name. */
28
+ plugins?: Record<string, SubPluginEntry>;
29
+ }
30
+ export declare const MANIFEST_FILENAME = "opencli-plugin.json";
31
+ /**
32
+ * Read and parse opencli-plugin.json from a directory.
33
+ * Returns null if the file does not exist or is unparseable.
34
+ */
35
+ export declare function readPluginManifest(dir: string): PluginManifest | null;
36
+ /** Returns true when the manifest declares a monorepo (has `plugins` field). */
37
+ export declare function isMonorepo(manifest: PluginManifest): boolean;
38
+ /**
39
+ * Get the list of enabled sub-plugins from a monorepo manifest.
40
+ * Returns entries sorted by key name.
41
+ */
42
+ export declare function getEnabledPlugins(manifest: PluginManifest): Array<{
43
+ name: string;
44
+ entry: SubPluginEntry;
45
+ }>;
46
+ /**
47
+ * Check if the current opencli version satisfies a semver range string.
48
+ *
49
+ * Supports a simplified subset of semver ranges:
50
+ * ">=1.0.0" – greater than or equal
51
+ * "<=1.5.0" – less than or equal
52
+ * ">1.0.0" – strictly greater
53
+ * "<2.0.0" – strictly less
54
+ * "^1.2.0" – compatible (>=1.2.0 and <2.0.0)
55
+ * "~1.2.0" – patch-level (>=1.2.0 and <1.3.0)
56
+ * "1.2.0" – exact match
57
+ * ">=1.0.0 <2.0.0" – multiple constraints (space-separated, all must match)
58
+ *
59
+ * Returns true if compatible, false if not, and true for empty/undefined
60
+ * ranges (no constraint = always compatible).
61
+ */
62
+ export declare function checkCompatibility(range: string | undefined): boolean;
63
+ /** Parse a version string ("1.2.3") into [major, minor, patch]. */
64
+ export declare function parseVersion(version: string): [number, number, number] | null;
65
+ /**
66
+ * Check if a version string satisfies a range expression.
67
+ * Space-separated constraints are ANDed together.
68
+ */
69
+ export declare function satisfiesRange(versionStr: string, range: string): boolean;
70
+ export { readPluginManifest as _readPluginManifest, isMonorepo as _isMonorepo, getEnabledPlugins as _getEnabledPlugins, checkCompatibility as _checkCompatibility, parseVersion as _parseVersion, satisfiesRange as _satisfiesRange, };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Plugin manifest: reads and validates opencli-plugin.json files.
3
+ *
4
+ * Supports two modes:
5
+ * 1. Single plugin: repo root IS the plugin directory.
6
+ * 2. Monorepo: repo contains multiple plugins declared in `plugins` field.
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { PKG_VERSION } from './version.js';
11
+ export const MANIFEST_FILENAME = 'opencli-plugin.json';
12
+ // ── Read / Validate ─────────────────────────────────────────────────────────
13
+ /**
14
+ * Read and parse opencli-plugin.json from a directory.
15
+ * Returns null if the file does not exist or is unparseable.
16
+ */
17
+ export function readPluginManifest(dir) {
18
+ const manifestPath = path.join(dir, MANIFEST_FILENAME);
19
+ try {
20
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
21
+ const parsed = JSON.parse(raw);
22
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
23
+ return null;
24
+ }
25
+ return parsed;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Returns true when the manifest declares a monorepo (has `plugins` field). */
32
+ export function isMonorepo(manifest) {
33
+ return (manifest.plugins !== undefined &&
34
+ manifest.plugins !== null &&
35
+ typeof manifest.plugins === 'object' &&
36
+ Object.keys(manifest.plugins).length > 0);
37
+ }
38
+ /**
39
+ * Get the list of enabled sub-plugins from a monorepo manifest.
40
+ * Returns entries sorted by key name.
41
+ */
42
+ export function getEnabledPlugins(manifest) {
43
+ if (!manifest.plugins)
44
+ return [];
45
+ return Object.entries(manifest.plugins)
46
+ .filter(([, entry]) => !entry.disabled)
47
+ .map(([name, entry]) => ({ name, entry }))
48
+ .sort((a, b) => a.name.localeCompare(b.name));
49
+ }
50
+ // ── Version compatibility ───────────────────────────────────────────────────
51
+ /**
52
+ * Check if the current opencli version satisfies a semver range string.
53
+ *
54
+ * Supports a simplified subset of semver ranges:
55
+ * ">=1.0.0" – greater than or equal
56
+ * "<=1.5.0" – less than or equal
57
+ * ">1.0.0" – strictly greater
58
+ * "<2.0.0" – strictly less
59
+ * "^1.2.0" – compatible (>=1.2.0 and <2.0.0)
60
+ * "~1.2.0" – patch-level (>=1.2.0 and <1.3.0)
61
+ * "1.2.0" – exact match
62
+ * ">=1.0.0 <2.0.0" – multiple constraints (space-separated, all must match)
63
+ *
64
+ * Returns true if compatible, false if not, and true for empty/undefined
65
+ * ranges (no constraint = always compatible).
66
+ */
67
+ export function checkCompatibility(range) {
68
+ if (!range || range.trim() === '')
69
+ return true;
70
+ return satisfiesRange(PKG_VERSION, range);
71
+ }
72
+ /** Parse a version string ("1.2.3") into [major, minor, patch]. */
73
+ export function parseVersion(version) {
74
+ const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
75
+ if (!match)
76
+ return null;
77
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
78
+ }
79
+ /** Compare two version tuples: -1 if a<b, 0 if equal, 1 if a>b. */
80
+ function compareVersions(a, b) {
81
+ for (let i = 0; i < 3; i++) {
82
+ if (a[i] < b[i])
83
+ return -1;
84
+ if (a[i] > b[i])
85
+ return 1;
86
+ }
87
+ return 0;
88
+ }
89
+ /** Check if a version satisfies a single constraint like ">=1.2.0". */
90
+ function satisfiesSingleConstraint(version, constraint) {
91
+ const trimmed = constraint.trim();
92
+ if (!trimmed)
93
+ return true;
94
+ // ^1.2.0 → >=1.2.0 <2.0.0
95
+ if (trimmed.startsWith('^')) {
96
+ const target = parseVersion(trimmed.slice(1));
97
+ if (!target)
98
+ return true;
99
+ const upper = [target[0] + 1, 0, 0];
100
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
101
+ }
102
+ // ~1.2.0 → >=1.2.0 <1.3.0
103
+ if (trimmed.startsWith('~')) {
104
+ const target = parseVersion(trimmed.slice(1));
105
+ if (!target)
106
+ return true;
107
+ const upper = [target[0], target[1] + 1, 0];
108
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
109
+ }
110
+ // >=, <=, >, <, =
111
+ if (trimmed.startsWith('>=')) {
112
+ const target = parseVersion(trimmed.slice(2));
113
+ if (!target)
114
+ return true;
115
+ return compareVersions(version, target) >= 0;
116
+ }
117
+ if (trimmed.startsWith('<=')) {
118
+ const target = parseVersion(trimmed.slice(2));
119
+ if (!target)
120
+ return true;
121
+ return compareVersions(version, target) <= 0;
122
+ }
123
+ if (trimmed.startsWith('>')) {
124
+ const target = parseVersion(trimmed.slice(1));
125
+ if (!target)
126
+ return true;
127
+ return compareVersions(version, target) > 0;
128
+ }
129
+ if (trimmed.startsWith('<')) {
130
+ const target = parseVersion(trimmed.slice(1));
131
+ if (!target)
132
+ return true;
133
+ return compareVersions(version, target) < 0;
134
+ }
135
+ if (trimmed.startsWith('=')) {
136
+ const target = parseVersion(trimmed.slice(1));
137
+ if (!target)
138
+ return true;
139
+ return compareVersions(version, target) === 0;
140
+ }
141
+ // Exact match
142
+ const target = parseVersion(trimmed);
143
+ if (!target)
144
+ return true;
145
+ return compareVersions(version, target) === 0;
146
+ }
147
+ /**
148
+ * Check if a version string satisfies a range expression.
149
+ * Space-separated constraints are ANDed together.
150
+ */
151
+ export function satisfiesRange(versionStr, range) {
152
+ const version = parseVersion(versionStr);
153
+ if (!version)
154
+ return true; // Can't parse our own version → assume ok
155
+ // Split on whitespace for multi-constraint ranges (e.g. ">=1.0.0 <2.0.0")
156
+ const constraints = range.trim().split(/\s+/);
157
+ return constraints.every((c) => satisfiesSingleConstraint(version, c));
158
+ }
159
+ // ── Exports for testing ─────────────────────────────────────────────────────
160
+ export { readPluginManifest as _readPluginManifest, isMonorepo as _isMonorepo, getEnabledPlugins as _getEnabledPlugins, checkCompatibility as _checkCompatibility, parseVersion as _parseVersion, satisfiesRange as _satisfiesRange, };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for plugin manifest: reading, validating, and compatibility checks.
3
+ */
4
+ export {};
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Tests for plugin manifest: reading, validating, and compatibility checks.
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import { _readPluginManifest as readPluginManifest, _isMonorepo as isMonorepo, _getEnabledPlugins as getEnabledPlugins, _parseVersion as parseVersion, _satisfiesRange as satisfiesRange, MANIFEST_FILENAME, } from './plugin-manifest.js';
9
+ // ── readPluginManifest ──────────────────────────────────────────────────────
10
+ describe('readPluginManifest', () => {
11
+ let tmpDir;
12
+ beforeEach(() => {
13
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-test-'));
14
+ });
15
+ afterEach(() => {
16
+ fs.rmSync(tmpDir, { recursive: true, force: true });
17
+ });
18
+ it('returns null when no manifest file exists', () => {
19
+ expect(readPluginManifest(tmpDir)).toBeNull();
20
+ });
21
+ it('returns null for malformed JSON', () => {
22
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), 'not json {{{');
23
+ expect(readPluginManifest(tmpDir)).toBeNull();
24
+ });
25
+ it('returns null for non-object JSON (array)', () => {
26
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), '["a","b"]');
27
+ expect(readPluginManifest(tmpDir)).toBeNull();
28
+ });
29
+ it('returns null for non-object JSON (string)', () => {
30
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), '"hello"');
31
+ expect(readPluginManifest(tmpDir)).toBeNull();
32
+ });
33
+ it('reads a single-plugin manifest', () => {
34
+ const manifest = {
35
+ name: 'polymarket',
36
+ version: '1.2.0',
37
+ opencli: '>=1.0.0',
38
+ description: 'Prediction market analysis',
39
+ };
40
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), JSON.stringify(manifest));
41
+ const result = readPluginManifest(tmpDir);
42
+ expect(result).toEqual(manifest);
43
+ });
44
+ it('reads a monorepo manifest', () => {
45
+ const manifest = {
46
+ version: '1.0.0',
47
+ opencli: '>=0.9.0',
48
+ description: 'My plugin collection',
49
+ plugins: {
50
+ polymarket: {
51
+ path: 'packages/polymarket',
52
+ description: 'Prediction market',
53
+ version: '1.2.0',
54
+ },
55
+ defi: {
56
+ path: 'packages/defi',
57
+ description: 'DeFi data',
58
+ version: '0.8.0',
59
+ disabled: true,
60
+ },
61
+ },
62
+ };
63
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), JSON.stringify(manifest));
64
+ const result = readPluginManifest(tmpDir);
65
+ expect(result).toEqual(manifest);
66
+ expect(result.plugins.polymarket.path).toBe('packages/polymarket');
67
+ expect(result.plugins.defi.disabled).toBe(true);
68
+ });
69
+ });
70
+ // ── isMonorepo ──────────────────────────────────────────────────────────────
71
+ describe('isMonorepo', () => {
72
+ it('returns false for single-plugin manifest', () => {
73
+ expect(isMonorepo({ name: 'test', version: '1.0.0' })).toBe(false);
74
+ });
75
+ it('returns false for empty plugins object', () => {
76
+ expect(isMonorepo({ plugins: {} })).toBe(false);
77
+ });
78
+ it('returns true for manifest with plugins', () => {
79
+ expect(isMonorepo({
80
+ plugins: {
81
+ foo: { path: 'packages/foo' },
82
+ },
83
+ })).toBe(true);
84
+ });
85
+ });
86
+ // ── getEnabledPlugins ───────────────────────────────────────────────────────
87
+ describe('getEnabledPlugins', () => {
88
+ it('returns empty array for no plugins', () => {
89
+ expect(getEnabledPlugins({ name: 'test' })).toEqual([]);
90
+ });
91
+ it('filters out disabled plugins', () => {
92
+ const manifest = {
93
+ plugins: {
94
+ foo: { path: 'packages/foo' },
95
+ bar: { path: 'packages/bar', disabled: true },
96
+ baz: { path: 'packages/baz' },
97
+ },
98
+ };
99
+ const result = getEnabledPlugins(manifest);
100
+ expect(result).toHaveLength(2);
101
+ expect(result.map((r) => r.name)).toEqual(['baz', 'foo']); // sorted
102
+ });
103
+ it('returns all when none disabled', () => {
104
+ const manifest = {
105
+ plugins: {
106
+ charlie: { path: 'packages/charlie' },
107
+ alpha: { path: 'packages/alpha' },
108
+ },
109
+ };
110
+ const result = getEnabledPlugins(manifest);
111
+ expect(result).toHaveLength(2);
112
+ expect(result[0].name).toBe('alpha');
113
+ expect(result[1].name).toBe('charlie');
114
+ });
115
+ });
116
+ // ── parseVersion ────────────────────────────────────────────────────────────
117
+ describe('parseVersion', () => {
118
+ it('parses standard versions', () => {
119
+ expect(parseVersion('1.2.3')).toEqual([1, 2, 3]);
120
+ expect(parseVersion('0.0.0')).toEqual([0, 0, 0]);
121
+ expect(parseVersion('10.20.30')).toEqual([10, 20, 30]);
122
+ });
123
+ it('parses versions with prerelease suffix', () => {
124
+ expect(parseVersion('1.2.3-beta.1')).toEqual([1, 2, 3]);
125
+ });
126
+ it('returns null for invalid versions', () => {
127
+ expect(parseVersion('abc')).toBeNull();
128
+ expect(parseVersion('')).toBeNull();
129
+ expect(parseVersion('1.2')).toBeNull();
130
+ });
131
+ });
132
+ // ── satisfiesRange ──────────────────────────────────────────────────────────
133
+ describe('satisfiesRange', () => {
134
+ it('handles >= constraint', () => {
135
+ expect(satisfiesRange('1.4.1', '>=1.0.0')).toBe(true);
136
+ expect(satisfiesRange('1.0.0', '>=1.0.0')).toBe(true);
137
+ expect(satisfiesRange('0.9.9', '>=1.0.0')).toBe(false);
138
+ });
139
+ it('handles <= constraint', () => {
140
+ expect(satisfiesRange('1.0.0', '<=1.0.0')).toBe(true);
141
+ expect(satisfiesRange('0.9.0', '<=1.0.0')).toBe(true);
142
+ expect(satisfiesRange('1.0.1', '<=1.0.0')).toBe(false);
143
+ });
144
+ it('handles > constraint', () => {
145
+ expect(satisfiesRange('1.0.1', '>1.0.0')).toBe(true);
146
+ expect(satisfiesRange('1.0.0', '>1.0.0')).toBe(false);
147
+ });
148
+ it('handles < constraint', () => {
149
+ expect(satisfiesRange('0.9.9', '<1.0.0')).toBe(true);
150
+ expect(satisfiesRange('1.0.0', '<1.0.0')).toBe(false);
151
+ });
152
+ it('handles ^ (caret) constraint', () => {
153
+ expect(satisfiesRange('1.2.0', '^1.2.0')).toBe(true);
154
+ expect(satisfiesRange('1.9.9', '^1.2.0')).toBe(true);
155
+ expect(satisfiesRange('2.0.0', '^1.2.0')).toBe(false);
156
+ expect(satisfiesRange('1.1.0', '^1.2.0')).toBe(false);
157
+ });
158
+ it('handles ~ (tilde) constraint', () => {
159
+ expect(satisfiesRange('1.2.0', '~1.2.0')).toBe(true);
160
+ expect(satisfiesRange('1.2.9', '~1.2.0')).toBe(true);
161
+ expect(satisfiesRange('1.3.0', '~1.2.0')).toBe(false);
162
+ });
163
+ it('handles exact match', () => {
164
+ expect(satisfiesRange('1.2.3', '1.2.3')).toBe(true);
165
+ expect(satisfiesRange('1.2.4', '1.2.3')).toBe(false);
166
+ });
167
+ it('handles compound range (AND)', () => {
168
+ expect(satisfiesRange('1.5.0', '>=1.0.0 <2.0.0')).toBe(true);
169
+ expect(satisfiesRange('2.0.0', '>=1.0.0 <2.0.0')).toBe(false);
170
+ expect(satisfiesRange('0.9.0', '>=1.0.0 <2.0.0')).toBe(false);
171
+ });
172
+ it('returns true for empty range', () => {
173
+ expect(satisfiesRange('1.0.0', '')).toBe(true);
174
+ expect(satisfiesRange('1.0.0', ' ')).toBe(true);
175
+ });
176
+ it('returns true for unparseable version', () => {
177
+ expect(satisfiesRange('dev', '>=1.0.0')).toBe(true);
178
+ });
179
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Plugin scaffold: generates a ready-to-develop plugin directory.
3
+ *
4
+ * Usage: opencli plugin create <name> [--dir <path>]
5
+ *
6
+ * Creates:
7
+ * <name>/
8
+ * opencli-plugin.json — manifest with name, version, description
9
+ * package.json — ESM package with opencli peer dependency
10
+ * hello.yaml — sample YAML command
11
+ * greet.ts — sample TS command using the current registry API
12
+ * README.md — basic documentation
13
+ */
14
+ export interface ScaffoldOptions {
15
+ /** Directory to create the plugin in. Defaults to `./<name>` */
16
+ dir?: string;
17
+ /** Plugin description */
18
+ description?: string;
19
+ }
20
+ export interface ScaffoldResult {
21
+ name: string;
22
+ dir: string;
23
+ files: string[];
24
+ }
25
+ /**
26
+ * Create a new plugin scaffold directory.
27
+ */
28
+ export declare function createPluginScaffold(name: string, opts?: ScaffoldOptions): ScaffoldResult;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Plugin scaffold: generates a ready-to-develop plugin directory.
3
+ *
4
+ * Usage: opencli plugin create <name> [--dir <path>]
5
+ *
6
+ * Creates:
7
+ * <name>/
8
+ * opencli-plugin.json — manifest with name, version, description
9
+ * package.json — ESM package with opencli peer dependency
10
+ * hello.yaml — sample YAML command
11
+ * greet.ts — sample TS command using the current registry API
12
+ * README.md — basic documentation
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { PKG_VERSION } from './version.js';
17
+ /**
18
+ * Create a new plugin scaffold directory.
19
+ */
20
+ export function createPluginScaffold(name, opts = {}) {
21
+ // Validate name
22
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
23
+ throw new Error(`Invalid plugin name "${name}". ` +
24
+ `Plugin names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens.`);
25
+ }
26
+ const targetDir = opts.dir
27
+ ? path.resolve(opts.dir)
28
+ : path.resolve(name);
29
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
30
+ throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
31
+ }
32
+ fs.mkdirSync(targetDir, { recursive: true });
33
+ const files = [];
34
+ // opencli-plugin.json
35
+ const manifest = {
36
+ name,
37
+ version: '0.1.0',
38
+ description: opts.description ?? `An opencli plugin: ${name}`,
39
+ opencli: `>=${PKG_VERSION}`,
40
+ };
41
+ writeFile(targetDir, 'opencli-plugin.json', JSON.stringify(manifest, null, 2) + '\n');
42
+ files.push('opencli-plugin.json');
43
+ // package.json
44
+ const pkg = {
45
+ name: `opencli-plugin-${name}`,
46
+ version: '0.1.0',
47
+ type: 'module',
48
+ description: opts.description ?? `An opencli plugin: ${name}`,
49
+ peerDependencies: {
50
+ '@jackwener/opencli': `>=${PKG_VERSION}`,
51
+ },
52
+ };
53
+ writeFile(targetDir, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
54
+ files.push('package.json');
55
+ // hello.yaml — sample YAML command
56
+ const yamlContent = `# Sample YAML command for ${name}
57
+ # See: https://github.com/jackwener/opencli#yaml-commands
58
+
59
+ site: ${name}
60
+ name: hello
61
+ description: "A sample YAML command"
62
+ strategy: public
63
+ browser: false
64
+
65
+ domain: https://httpbin.org
66
+
67
+ pipeline:
68
+ - fetch:
69
+ url: "https://httpbin.org/get?greeting=hello"
70
+ method: GET
71
+ - extract:
72
+ type: json
73
+ selector: "$.args"
74
+ `;
75
+ writeFile(targetDir, 'hello.yaml', yamlContent);
76
+ files.push('hello.yaml');
77
+ // greet.ts — sample TS command using registry API
78
+ const tsContent = `/**
79
+ * Sample TypeScript command for ${name}.
80
+ * Demonstrates the programmatic cli() registration API.
81
+ */
82
+
83
+ import { cli, Strategy } from '@jackwener/opencli/registry';
84
+
85
+ cli({
86
+ site: '${name}',
87
+ name: 'greet',
88
+ description: 'Greet someone by name',
89
+ strategy: Strategy.PUBLIC,
90
+ browser: false,
91
+ args: [
92
+ { name: 'name', positional: true, required: true, help: 'Name to greet' },
93
+ ],
94
+ columns: ['greeting'],
95
+ func: async (_page, kwargs) => [{ greeting: \`Hello, \${String(kwargs.name ?? 'World')}!\` }],
96
+ });
97
+ `;
98
+ writeFile(targetDir, 'greet.ts', tsContent);
99
+ files.push('greet.ts');
100
+ // README.md
101
+ const readme = `# opencli-plugin-${name}
102
+
103
+ ${opts.description ?? `An opencli plugin: ${name}`}
104
+
105
+ ## Install
106
+
107
+ \`\`\`bash
108
+ # From local development directory
109
+ opencli plugin install file://${targetDir}
110
+
111
+ # From GitHub (after publishing)
112
+ opencli plugin install github:<user>/opencli-plugin-${name}
113
+ \`\`\`
114
+
115
+ ## Commands
116
+
117
+ | Command | Type | Description |
118
+ |---------|------|-------------|
119
+ | \`${name}/hello\` | YAML | Sample YAML command |
120
+ | \`${name}/greet\` | TypeScript | Sample TS command |
121
+
122
+ ## Development
123
+
124
+ \`\`\`bash
125
+ # Install locally for development (symlinked, changes reflect immediately)
126
+ opencli plugin install file://${targetDir}
127
+
128
+ # Verify commands are registered
129
+ opencli list | grep ${name}
130
+
131
+ # Run a command
132
+ opencli ${name} hello
133
+ opencli ${name} greet --name World
134
+ \`\`\`
135
+ `;
136
+ writeFile(targetDir, 'README.md', readme);
137
+ files.push('README.md');
138
+ return { name, dir: targetDir, files };
139
+ }
140
+ function writeFile(dir, name, content) {
141
+ fs.writeFileSync(path.join(dir, name), content);
142
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for plugin scaffold: create new plugin directories.
3
+ */
4
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for plugin scaffold: create new plugin directories.
3
+ */
4
+ import { describe, it, expect, afterEach } from 'vitest';
5
+ import * as fs from 'node:fs';
6
+ import * as os from 'node:os';
7
+ import * as path from 'node:path';
8
+ import { createPluginScaffold } from './plugin-scaffold.js';
9
+ describe('createPluginScaffold', () => {
10
+ const createdDirs = [];
11
+ afterEach(() => {
12
+ for (const dir of createdDirs) {
13
+ try {
14
+ fs.rmSync(dir, { recursive: true, force: true });
15
+ }
16
+ catch { }
17
+ }
18
+ createdDirs.length = 0;
19
+ });
20
+ it('creates all expected files', () => {
21
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
22
+ createdDirs.push(dir);
23
+ const result = createPluginScaffold('my-test', { dir });
24
+ expect(result.name).toBe('my-test');
25
+ expect(result.dir).toBe(dir);
26
+ expect(result.files).toContain('opencli-plugin.json');
27
+ expect(result.files).toContain('package.json');
28
+ expect(result.files).toContain('hello.yaml');
29
+ expect(result.files).toContain('greet.ts');
30
+ expect(result.files).toContain('README.md');
31
+ // All files exist
32
+ for (const f of result.files) {
33
+ expect(fs.existsSync(path.join(dir, f))).toBe(true);
34
+ }
35
+ });
36
+ it('generates valid opencli-plugin.json', () => {
37
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
38
+ createdDirs.push(dir);
39
+ createPluginScaffold('test-manifest', { dir, description: 'Test desc' });
40
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'opencli-plugin.json'), 'utf-8'));
41
+ expect(manifest.name).toBe('test-manifest');
42
+ expect(manifest.version).toBe('0.1.0');
43
+ expect(manifest.description).toBe('Test desc');
44
+ expect(manifest.opencli).toMatch(/^>=/);
45
+ });
46
+ it('generates ESM package.json', () => {
47
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
48
+ createdDirs.push(dir);
49
+ createPluginScaffold('test-pkg', { dir });
50
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
51
+ expect(pkg.type).toBe('module');
52
+ expect(pkg.peerDependencies?.['@jackwener/opencli']).toBeDefined();
53
+ });
54
+ it('generates a TS sample that matches the current plugin API', () => {
55
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
56
+ createdDirs.push(dir);
57
+ createPluginScaffold('test-ts', { dir });
58
+ const tsSample = fs.readFileSync(path.join(dir, 'greet.ts'), 'utf-8');
59
+ expect(tsSample).toContain(`import { cli, Strategy } from '@jackwener/opencli/registry';`);
60
+ expect(tsSample).toContain(`strategy: Strategy.PUBLIC`);
61
+ expect(tsSample).toContain(`help: 'Name to greet'`);
62
+ expect(tsSample).toContain(`func: async (_page, kwargs)`);
63
+ expect(tsSample).not.toContain('async run(');
64
+ });
65
+ it('documents a supported local install flow', () => {
66
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
67
+ createdDirs.push(dir);
68
+ createPluginScaffold('test-readme', { dir });
69
+ const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
70
+ expect(readme).toContain(`opencli plugin install file://${dir}`);
71
+ });
72
+ it('rejects invalid names', () => {
73
+ expect(() => createPluginScaffold('Bad_Name')).toThrow('Invalid plugin name');
74
+ expect(() => createPluginScaffold('123start')).toThrow('Invalid plugin name');
75
+ });
76
+ it('rejects non-empty directory', () => {
77
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
78
+ createdDirs.push(dir);
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ fs.writeFileSync(path.join(dir, 'existing.txt'), 'x');
81
+ expect(() => createPluginScaffold('test', { dir })).toThrow('not empty');
82
+ });
83
+ });