@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,223 @@
1
+ /**
2
+ * Tests for plugin manifest: reading, validating, and compatibility checks.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import {
10
+ _readPluginManifest as readPluginManifest,
11
+ _isMonorepo as isMonorepo,
12
+ _getEnabledPlugins as getEnabledPlugins,
13
+ _parseVersion as parseVersion,
14
+ _satisfiesRange as satisfiesRange,
15
+ MANIFEST_FILENAME,
16
+ type PluginManifest,
17
+ } from './plugin-manifest.js';
18
+
19
+ // ── readPluginManifest ──────────────────────────────────────────────────────
20
+
21
+ describe('readPluginManifest', () => {
22
+ let tmpDir: string;
23
+
24
+ beforeEach(() => {
25
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-test-'));
26
+ });
27
+
28
+ afterEach(() => {
29
+ fs.rmSync(tmpDir, { recursive: true, force: true });
30
+ });
31
+
32
+ it('returns null when no manifest file exists', () => {
33
+ expect(readPluginManifest(tmpDir)).toBeNull();
34
+ });
35
+
36
+ it('returns null for malformed JSON', () => {
37
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), 'not json {{{');
38
+ expect(readPluginManifest(tmpDir)).toBeNull();
39
+ });
40
+
41
+ it('returns null for non-object JSON (array)', () => {
42
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), '["a","b"]');
43
+ expect(readPluginManifest(tmpDir)).toBeNull();
44
+ });
45
+
46
+ it('returns null for non-object JSON (string)', () => {
47
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), '"hello"');
48
+ expect(readPluginManifest(tmpDir)).toBeNull();
49
+ });
50
+
51
+ it('reads a single-plugin manifest', () => {
52
+ const manifest: PluginManifest = {
53
+ name: 'polymarket',
54
+ version: '1.2.0',
55
+ opencli: '>=1.0.0',
56
+ description: 'Prediction market analysis',
57
+ };
58
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), JSON.stringify(manifest));
59
+ const result = readPluginManifest(tmpDir);
60
+ expect(result).toEqual(manifest);
61
+ });
62
+
63
+ it('reads a monorepo manifest', () => {
64
+ const manifest: PluginManifest = {
65
+ version: '1.0.0',
66
+ opencli: '>=0.9.0',
67
+ description: 'My plugin collection',
68
+ plugins: {
69
+ polymarket: {
70
+ path: 'packages/polymarket',
71
+ description: 'Prediction market',
72
+ version: '1.2.0',
73
+ },
74
+ defi: {
75
+ path: 'packages/defi',
76
+ description: 'DeFi data',
77
+ version: '0.8.0',
78
+ disabled: true,
79
+ },
80
+ },
81
+ };
82
+ fs.writeFileSync(path.join(tmpDir, MANIFEST_FILENAME), JSON.stringify(manifest));
83
+ const result = readPluginManifest(tmpDir);
84
+ expect(result).toEqual(manifest);
85
+ expect(result!.plugins!.polymarket.path).toBe('packages/polymarket');
86
+ expect(result!.plugins!.defi.disabled).toBe(true);
87
+ });
88
+ });
89
+
90
+ // ── isMonorepo ──────────────────────────────────────────────────────────────
91
+
92
+ describe('isMonorepo', () => {
93
+ it('returns false for single-plugin manifest', () => {
94
+ expect(isMonorepo({ name: 'test', version: '1.0.0' })).toBe(false);
95
+ });
96
+
97
+ it('returns false for empty plugins object', () => {
98
+ expect(isMonorepo({ plugins: {} })).toBe(false);
99
+ });
100
+
101
+ it('returns true for manifest with plugins', () => {
102
+ expect(
103
+ isMonorepo({
104
+ plugins: {
105
+ foo: { path: 'packages/foo' },
106
+ },
107
+ }),
108
+ ).toBe(true);
109
+ });
110
+ });
111
+
112
+ // ── getEnabledPlugins ───────────────────────────────────────────────────────
113
+
114
+ describe('getEnabledPlugins', () => {
115
+ it('returns empty array for no plugins', () => {
116
+ expect(getEnabledPlugins({ name: 'test' })).toEqual([]);
117
+ });
118
+
119
+ it('filters out disabled plugins', () => {
120
+ const manifest: PluginManifest = {
121
+ plugins: {
122
+ foo: { path: 'packages/foo' },
123
+ bar: { path: 'packages/bar', disabled: true },
124
+ baz: { path: 'packages/baz' },
125
+ },
126
+ };
127
+ const result = getEnabledPlugins(manifest);
128
+ expect(result).toHaveLength(2);
129
+ expect(result.map((r) => r.name)).toEqual(['baz', 'foo']); // sorted
130
+ });
131
+
132
+ it('returns all when none disabled', () => {
133
+ const manifest: PluginManifest = {
134
+ plugins: {
135
+ charlie: { path: 'packages/charlie' },
136
+ alpha: { path: 'packages/alpha' },
137
+ },
138
+ };
139
+ const result = getEnabledPlugins(manifest);
140
+ expect(result).toHaveLength(2);
141
+ expect(result[0].name).toBe('alpha');
142
+ expect(result[1].name).toBe('charlie');
143
+ });
144
+ });
145
+
146
+ // ── parseVersion ────────────────────────────────────────────────────────────
147
+
148
+ describe('parseVersion', () => {
149
+ it('parses standard versions', () => {
150
+ expect(parseVersion('1.2.3')).toEqual([1, 2, 3]);
151
+ expect(parseVersion('0.0.0')).toEqual([0, 0, 0]);
152
+ expect(parseVersion('10.20.30')).toEqual([10, 20, 30]);
153
+ });
154
+
155
+ it('parses versions with prerelease suffix', () => {
156
+ expect(parseVersion('1.2.3-beta.1')).toEqual([1, 2, 3]);
157
+ });
158
+
159
+ it('returns null for invalid versions', () => {
160
+ expect(parseVersion('abc')).toBeNull();
161
+ expect(parseVersion('')).toBeNull();
162
+ expect(parseVersion('1.2')).toBeNull();
163
+ });
164
+ });
165
+
166
+ // ── satisfiesRange ──────────────────────────────────────────────────────────
167
+
168
+ describe('satisfiesRange', () => {
169
+ it('handles >= constraint', () => {
170
+ expect(satisfiesRange('1.4.1', '>=1.0.0')).toBe(true);
171
+ expect(satisfiesRange('1.0.0', '>=1.0.0')).toBe(true);
172
+ expect(satisfiesRange('0.9.9', '>=1.0.0')).toBe(false);
173
+ });
174
+
175
+ it('handles <= constraint', () => {
176
+ expect(satisfiesRange('1.0.0', '<=1.0.0')).toBe(true);
177
+ expect(satisfiesRange('0.9.0', '<=1.0.0')).toBe(true);
178
+ expect(satisfiesRange('1.0.1', '<=1.0.0')).toBe(false);
179
+ });
180
+
181
+ it('handles > constraint', () => {
182
+ expect(satisfiesRange('1.0.1', '>1.0.0')).toBe(true);
183
+ expect(satisfiesRange('1.0.0', '>1.0.0')).toBe(false);
184
+ });
185
+
186
+ it('handles < constraint', () => {
187
+ expect(satisfiesRange('0.9.9', '<1.0.0')).toBe(true);
188
+ expect(satisfiesRange('1.0.0', '<1.0.0')).toBe(false);
189
+ });
190
+
191
+ it('handles ^ (caret) constraint', () => {
192
+ expect(satisfiesRange('1.2.0', '^1.2.0')).toBe(true);
193
+ expect(satisfiesRange('1.9.9', '^1.2.0')).toBe(true);
194
+ expect(satisfiesRange('2.0.0', '^1.2.0')).toBe(false);
195
+ expect(satisfiesRange('1.1.0', '^1.2.0')).toBe(false);
196
+ });
197
+
198
+ it('handles ~ (tilde) constraint', () => {
199
+ expect(satisfiesRange('1.2.0', '~1.2.0')).toBe(true);
200
+ expect(satisfiesRange('1.2.9', '~1.2.0')).toBe(true);
201
+ expect(satisfiesRange('1.3.0', '~1.2.0')).toBe(false);
202
+ });
203
+
204
+ it('handles exact match', () => {
205
+ expect(satisfiesRange('1.2.3', '1.2.3')).toBe(true);
206
+ expect(satisfiesRange('1.2.4', '1.2.3')).toBe(false);
207
+ });
208
+
209
+ it('handles compound range (AND)', () => {
210
+ expect(satisfiesRange('1.5.0', '>=1.0.0 <2.0.0')).toBe(true);
211
+ expect(satisfiesRange('2.0.0', '>=1.0.0 <2.0.0')).toBe(false);
212
+ expect(satisfiesRange('0.9.0', '>=1.0.0 <2.0.0')).toBe(false);
213
+ });
214
+
215
+ it('returns true for empty range', () => {
216
+ expect(satisfiesRange('1.0.0', '')).toBe(true);
217
+ expect(satisfiesRange('1.0.0', ' ')).toBe(true);
218
+ });
219
+
220
+ it('returns true for unparseable version', () => {
221
+ expect(satisfiesRange('dev', '>=1.0.0')).toBe(true);
222
+ });
223
+ });
@@ -0,0 +1,206 @@
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
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import { PKG_VERSION } from './version.js';
12
+
13
+ // ── Types ───────────────────────────────────────────────────────────────────
14
+
15
+ export interface SubPluginEntry {
16
+ /** Relative path from repo root to the sub-plugin directory. */
17
+ path: string;
18
+ version?: string;
19
+ description?: string;
20
+ /** Semver range for opencli compatibility (overrides top-level). */
21
+ opencli?: string;
22
+ /** When true, this sub-plugin is skipped during install. */
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export interface PluginManifest {
27
+ /** Plugin name (single-plugin mode). */
28
+ name?: string;
29
+ /** Semantic version of the plugin (single-plugin mode). */
30
+ version?: string;
31
+ /** Semver range for opencli compatibility, e.g. ">=1.0.0". */
32
+ opencli?: string;
33
+ /** Human-readable description. */
34
+ description?: string;
35
+ /** Monorepo sub-plugins. Key = logical plugin name. */
36
+ plugins?: Record<string, SubPluginEntry>;
37
+ }
38
+
39
+ export const MANIFEST_FILENAME = 'opencli-plugin.json';
40
+
41
+ // ── Read / Validate ─────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Read and parse opencli-plugin.json from a directory.
45
+ * Returns null if the file does not exist or is unparseable.
46
+ */
47
+ export function readPluginManifest(dir: string): PluginManifest | null {
48
+ const manifestPath = path.join(dir, MANIFEST_FILENAME);
49
+ try {
50
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
51
+ const parsed = JSON.parse(raw);
52
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
53
+ return null;
54
+ }
55
+ return parsed as PluginManifest;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** Returns true when the manifest declares a monorepo (has `plugins` field). */
62
+ export function isMonorepo(manifest: PluginManifest): boolean {
63
+ return (
64
+ manifest.plugins !== undefined &&
65
+ manifest.plugins !== null &&
66
+ typeof manifest.plugins === 'object' &&
67
+ Object.keys(manifest.plugins).length > 0
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Get the list of enabled sub-plugins from a monorepo manifest.
73
+ * Returns entries sorted by key name.
74
+ */
75
+ export function getEnabledPlugins(
76
+ manifest: PluginManifest,
77
+ ): Array<{ name: string; entry: SubPluginEntry }> {
78
+ if (!manifest.plugins) return [];
79
+ return Object.entries(manifest.plugins)
80
+ .filter(([, entry]) => !entry.disabled)
81
+ .map(([name, entry]) => ({ name, entry }))
82
+ .sort((a, b) => a.name.localeCompare(b.name));
83
+ }
84
+
85
+ // ── Version compatibility ───────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Check if the current opencli version satisfies a semver range string.
89
+ *
90
+ * Supports a simplified subset of semver ranges:
91
+ * ">=1.0.0" – greater than or equal
92
+ * "<=1.5.0" – less than or equal
93
+ * ">1.0.0" – strictly greater
94
+ * "<2.0.0" – strictly less
95
+ * "^1.2.0" – compatible (>=1.2.0 and <2.0.0)
96
+ * "~1.2.0" – patch-level (>=1.2.0 and <1.3.0)
97
+ * "1.2.0" – exact match
98
+ * ">=1.0.0 <2.0.0" – multiple constraints (space-separated, all must match)
99
+ *
100
+ * Returns true if compatible, false if not, and true for empty/undefined
101
+ * ranges (no constraint = always compatible).
102
+ */
103
+ export function checkCompatibility(range: string | undefined): boolean {
104
+ if (!range || range.trim() === '') return true;
105
+ return satisfiesRange(PKG_VERSION, range);
106
+ }
107
+
108
+ /** Parse a version string ("1.2.3") into [major, minor, patch]. */
109
+ export function parseVersion(version: string): [number, number, number] | null {
110
+ const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
111
+ if (!match) return null;
112
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
113
+ }
114
+
115
+ /** Compare two version tuples: -1 if a<b, 0 if equal, 1 if a>b. */
116
+ function compareVersions(
117
+ a: [number, number, number],
118
+ b: [number, number, number],
119
+ ): -1 | 0 | 1 {
120
+ for (let i = 0; i < 3; i++) {
121
+ if (a[i] < b[i]) return -1;
122
+ if (a[i] > b[i]) return 1;
123
+ }
124
+ return 0;
125
+ }
126
+
127
+ /** Check if a version satisfies a single constraint like ">=1.2.0". */
128
+ function satisfiesSingleConstraint(
129
+ version: [number, number, number],
130
+ constraint: string,
131
+ ): boolean {
132
+ const trimmed = constraint.trim();
133
+ if (!trimmed) return true;
134
+
135
+ // ^1.2.0 → >=1.2.0 <2.0.0
136
+ if (trimmed.startsWith('^')) {
137
+ const target = parseVersion(trimmed.slice(1));
138
+ if (!target) return true;
139
+ const upper: [number, number, number] = [target[0] + 1, 0, 0];
140
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
141
+ }
142
+
143
+ // ~1.2.0 → >=1.2.0 <1.3.0
144
+ if (trimmed.startsWith('~')) {
145
+ const target = parseVersion(trimmed.slice(1));
146
+ if (!target) return true;
147
+ const upper: [number, number, number] = [target[0], target[1] + 1, 0];
148
+ return compareVersions(version, target) >= 0 && compareVersions(version, upper) < 0;
149
+ }
150
+
151
+ // >=, <=, >, <, =
152
+ if (trimmed.startsWith('>=')) {
153
+ const target = parseVersion(trimmed.slice(2));
154
+ if (!target) return true;
155
+ return compareVersions(version, target) >= 0;
156
+ }
157
+ if (trimmed.startsWith('<=')) {
158
+ const target = parseVersion(trimmed.slice(2));
159
+ if (!target) return true;
160
+ return compareVersions(version, target) <= 0;
161
+ }
162
+ if (trimmed.startsWith('>')) {
163
+ const target = parseVersion(trimmed.slice(1));
164
+ if (!target) return true;
165
+ return compareVersions(version, target) > 0;
166
+ }
167
+ if (trimmed.startsWith('<')) {
168
+ const target = parseVersion(trimmed.slice(1));
169
+ if (!target) return true;
170
+ return compareVersions(version, target) < 0;
171
+ }
172
+ if (trimmed.startsWith('=')) {
173
+ const target = parseVersion(trimmed.slice(1));
174
+ if (!target) return true;
175
+ return compareVersions(version, target) === 0;
176
+ }
177
+
178
+ // Exact match
179
+ const target = parseVersion(trimmed);
180
+ if (!target) return true;
181
+ return compareVersions(version, target) === 0;
182
+ }
183
+
184
+ /**
185
+ * Check if a version string satisfies a range expression.
186
+ * Space-separated constraints are ANDed together.
187
+ */
188
+ export function satisfiesRange(versionStr: string, range: string): boolean {
189
+ const version = parseVersion(versionStr);
190
+ if (!version) return true; // Can't parse our own version → assume ok
191
+
192
+ // Split on whitespace for multi-constraint ranges (e.g. ">=1.0.0 <2.0.0")
193
+ const constraints = range.trim().split(/\s+/);
194
+ return constraints.every((c) => satisfiesSingleConstraint(version, c));
195
+ }
196
+
197
+ // ── Exports for testing ─────────────────────────────────────────────────────
198
+
199
+ export {
200
+ readPluginManifest as _readPluginManifest,
201
+ isMonorepo as _isMonorepo,
202
+ getEnabledPlugins as _getEnabledPlugins,
203
+ checkCompatibility as _checkCompatibility,
204
+ parseVersion as _parseVersion,
205
+ satisfiesRange as _satisfiesRange,
206
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tests for plugin scaffold: create new plugin directories.
3
+ */
4
+
5
+ import { describe, it, expect, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
9
+ import { createPluginScaffold } from './plugin-scaffold.js';
10
+
11
+ describe('createPluginScaffold', () => {
12
+ const createdDirs: string[] = [];
13
+
14
+ afterEach(() => {
15
+ for (const dir of createdDirs) {
16
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
17
+ }
18
+ createdDirs.length = 0;
19
+ });
20
+
21
+ it('creates all expected files', () => {
22
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
23
+ createdDirs.push(dir);
24
+
25
+ const result = createPluginScaffold('my-test', { dir });
26
+ expect(result.name).toBe('my-test');
27
+ expect(result.dir).toBe(dir);
28
+ expect(result.files).toContain('opencli-plugin.json');
29
+ expect(result.files).toContain('package.json');
30
+ expect(result.files).toContain('hello.yaml');
31
+ expect(result.files).toContain('greet.ts');
32
+ expect(result.files).toContain('README.md');
33
+
34
+ // All files exist
35
+ for (const f of result.files) {
36
+ expect(fs.existsSync(path.join(dir, f))).toBe(true);
37
+ }
38
+ });
39
+
40
+ it('generates valid opencli-plugin.json', () => {
41
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
42
+ createdDirs.push(dir);
43
+
44
+ createPluginScaffold('test-manifest', { dir, description: 'Test desc' });
45
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'opencli-plugin.json'), 'utf-8'));
46
+ expect(manifest.name).toBe('test-manifest');
47
+ expect(manifest.version).toBe('0.1.0');
48
+ expect(manifest.description).toBe('Test desc');
49
+ expect(manifest.opencli).toMatch(/^>=/);
50
+ });
51
+
52
+ it('generates ESM package.json', () => {
53
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
54
+ createdDirs.push(dir);
55
+
56
+ createPluginScaffold('test-pkg', { dir });
57
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
58
+ expect(pkg.type).toBe('module');
59
+ expect(pkg.peerDependencies?.['@jackwener/opencli']).toBeDefined();
60
+ });
61
+
62
+ it('generates a TS sample that matches the current plugin API', () => {
63
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
64
+ createdDirs.push(dir);
65
+
66
+ createPluginScaffold('test-ts', { dir });
67
+ const tsSample = fs.readFileSync(path.join(dir, 'greet.ts'), 'utf-8');
68
+
69
+ expect(tsSample).toContain(`import { cli, Strategy } from '@jackwener/opencli/registry';`);
70
+ expect(tsSample).toContain(`strategy: Strategy.PUBLIC`);
71
+ expect(tsSample).toContain(`help: 'Name to greet'`);
72
+ expect(tsSample).toContain(`func: async (_page, kwargs)`);
73
+ expect(tsSample).not.toContain('async run(');
74
+ });
75
+
76
+ it('documents a supported local install flow', () => {
77
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
78
+ createdDirs.push(dir);
79
+
80
+ createPluginScaffold('test-readme', { dir });
81
+ const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
82
+
83
+ expect(readme).toContain(`opencli plugin install file://${dir}`);
84
+ });
85
+
86
+ it('rejects invalid names', () => {
87
+ expect(() => createPluginScaffold('Bad_Name')).toThrow('Invalid plugin name');
88
+ expect(() => createPluginScaffold('123start')).toThrow('Invalid plugin name');
89
+ });
90
+
91
+ it('rejects non-empty directory', () => {
92
+ const dir = path.join(os.tmpdir(), `opencli-scaffold-${Date.now()}`);
93
+ createdDirs.push(dir);
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ fs.writeFileSync(path.join(dir, 'existing.txt'), 'x');
96
+ expect(() => createPluginScaffold('test', { dir })).toThrow('not empty');
97
+ });
98
+ });
@@ -0,0 +1,170 @@
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
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { PKG_VERSION } from './version.js';
18
+
19
+ export interface ScaffoldOptions {
20
+ /** Directory to create the plugin in. Defaults to `./<name>` */
21
+ dir?: string;
22
+ /** Plugin description */
23
+ description?: string;
24
+ }
25
+
26
+ export interface ScaffoldResult {
27
+ name: string;
28
+ dir: string;
29
+ files: string[];
30
+ }
31
+
32
+ /**
33
+ * Create a new plugin scaffold directory.
34
+ */
35
+ export function createPluginScaffold(name: string, opts: ScaffoldOptions = {}): ScaffoldResult {
36
+ // Validate name
37
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
38
+ throw new Error(
39
+ `Invalid plugin name "${name}". ` +
40
+ `Plugin names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens.`
41
+ );
42
+ }
43
+
44
+ const targetDir = opts.dir
45
+ ? path.resolve(opts.dir)
46
+ : path.resolve(name);
47
+
48
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
49
+ throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
50
+ }
51
+
52
+ fs.mkdirSync(targetDir, { recursive: true });
53
+
54
+ const files: string[] = [];
55
+
56
+ // opencli-plugin.json
57
+ const manifest = {
58
+ name,
59
+ version: '0.1.0',
60
+ description: opts.description ?? `An opencli plugin: ${name}`,
61
+ opencli: `>=${PKG_VERSION}`,
62
+ };
63
+ writeFile(targetDir, 'opencli-plugin.json', JSON.stringify(manifest, null, 2) + '\n');
64
+ files.push('opencli-plugin.json');
65
+
66
+ // package.json
67
+ const pkg = {
68
+ name: `opencli-plugin-${name}`,
69
+ version: '0.1.0',
70
+ type: 'module',
71
+ description: opts.description ?? `An opencli plugin: ${name}`,
72
+ peerDependencies: {
73
+ '@jackwener/opencli': `>=${PKG_VERSION}`,
74
+ },
75
+ };
76
+ writeFile(targetDir, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
77
+ files.push('package.json');
78
+
79
+ // hello.yaml — sample YAML command
80
+ const yamlContent = `# Sample YAML command for ${name}
81
+ # See: https://github.com/jackwener/opencli#yaml-commands
82
+
83
+ site: ${name}
84
+ name: hello
85
+ description: "A sample YAML command"
86
+ strategy: public
87
+ browser: false
88
+
89
+ domain: https://httpbin.org
90
+
91
+ pipeline:
92
+ - fetch:
93
+ url: "https://httpbin.org/get?greeting=hello"
94
+ method: GET
95
+ - extract:
96
+ type: json
97
+ selector: "$.args"
98
+ `;
99
+ writeFile(targetDir, 'hello.yaml', yamlContent);
100
+ files.push('hello.yaml');
101
+
102
+ // greet.ts — sample TS command using registry API
103
+ const tsContent = `/**
104
+ * Sample TypeScript command for ${name}.
105
+ * Demonstrates the programmatic cli() registration API.
106
+ */
107
+
108
+ import { cli, Strategy } from '@jackwener/opencli/registry';
109
+
110
+ cli({
111
+ site: '${name}',
112
+ name: 'greet',
113
+ description: 'Greet someone by name',
114
+ strategy: Strategy.PUBLIC,
115
+ browser: false,
116
+ args: [
117
+ { name: 'name', positional: true, required: true, help: 'Name to greet' },
118
+ ],
119
+ columns: ['greeting'],
120
+ func: async (_page, kwargs) => [{ greeting: \`Hello, \${String(kwargs.name ?? 'World')}!\` }],
121
+ });
122
+ `;
123
+ writeFile(targetDir, 'greet.ts', tsContent);
124
+ files.push('greet.ts');
125
+
126
+ // README.md
127
+ const readme = `# opencli-plugin-${name}
128
+
129
+ ${opts.description ?? `An opencli plugin: ${name}`}
130
+
131
+ ## Install
132
+
133
+ \`\`\`bash
134
+ # From local development directory
135
+ opencli plugin install file://${targetDir}
136
+
137
+ # From GitHub (after publishing)
138
+ opencli plugin install github:<user>/opencli-plugin-${name}
139
+ \`\`\`
140
+
141
+ ## Commands
142
+
143
+ | Command | Type | Description |
144
+ |---------|------|-------------|
145
+ | \`${name}/hello\` | YAML | Sample YAML command |
146
+ | \`${name}/greet\` | TypeScript | Sample TS command |
147
+
148
+ ## Development
149
+
150
+ \`\`\`bash
151
+ # Install locally for development (symlinked, changes reflect immediately)
152
+ opencli plugin install file://${targetDir}
153
+
154
+ # Verify commands are registered
155
+ opencli list | grep ${name}
156
+
157
+ # Run a command
158
+ opencli ${name} hello
159
+ opencli ${name} greet --name World
160
+ \`\`\`
161
+ `;
162
+ writeFile(targetDir, 'README.md', readme);
163
+ files.push('README.md');
164
+
165
+ return { name, dir: targetDir, files };
166
+ }
167
+
168
+ function writeFile(dir: string, name: string, content: string): void {
169
+ fs.writeFileSync(path.join(dir, name), content);
170
+ }