@jackwener/opencli 1.4.1 → 1.5.0

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 (322) 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/discover.d.ts +4 -1
  8. package/dist/browser/discover.js +6 -2
  9. package/dist/browser/errors.d.ts +2 -2
  10. package/dist/browser/errors.js +4 -12
  11. package/dist/browser/mcp.d.ts +2 -1
  12. package/dist/build-manifest.d.ts +2 -0
  13. package/dist/build-manifest.js +39 -14
  14. package/dist/build-manifest.test.js +21 -0
  15. package/dist/capabilityRouting.d.ts +2 -0
  16. package/dist/capabilityRouting.js +2 -1
  17. package/dist/cli-manifest.json +1111 -112
  18. package/dist/cli.js +34 -3
  19. package/dist/clis/36kr/article.d.ts +1 -0
  20. package/dist/clis/36kr/article.js +62 -0
  21. package/dist/clis/36kr/hot.d.ts +3 -0
  22. package/dist/clis/36kr/hot.js +80 -0
  23. package/dist/clis/36kr/hot.test.d.ts +1 -0
  24. package/dist/clis/36kr/hot.test.js +15 -0
  25. package/dist/clis/36kr/news.d.ts +1 -0
  26. package/dist/clis/36kr/news.js +51 -0
  27. package/dist/clis/36kr/news.test.d.ts +1 -0
  28. package/dist/clis/36kr/news.test.js +85 -0
  29. package/dist/clis/36kr/search.d.ts +1 -0
  30. package/dist/clis/36kr/search.js +72 -0
  31. package/dist/clis/bilibili/comments.d.ts +5 -0
  32. package/dist/clis/bilibili/comments.js +40 -0
  33. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  34. package/dist/clis/bilibili/comments.test.js +82 -0
  35. package/dist/clis/chatgpt/ask.js +29 -14
  36. package/dist/clis/chatgpt/ax.d.ts +6 -0
  37. package/dist/clis/chatgpt/ax.js +172 -1
  38. package/dist/clis/chatgpt/model.d.ts +1 -0
  39. package/dist/clis/chatgpt/model.js +24 -0
  40. package/dist/clis/chatgpt/send.js +12 -3
  41. package/dist/clis/douban/download.d.ts +1 -0
  42. package/dist/clis/douban/download.js +67 -0
  43. package/dist/clis/douban/download.test.d.ts +1 -0
  44. package/dist/clis/douban/download.test.js +170 -0
  45. package/dist/clis/douban/photos.d.ts +1 -0
  46. package/dist/clis/douban/photos.js +34 -0
  47. package/dist/clis/douban/utils.d.ts +25 -0
  48. package/dist/clis/douban/utils.js +190 -1
  49. package/dist/clis/douban/utils.test.d.ts +1 -0
  50. package/dist/clis/douban/utils.test.js +64 -0
  51. package/dist/clis/imdb/person.d.ts +1 -0
  52. package/dist/clis/imdb/person.js +203 -0
  53. package/dist/clis/imdb/reviews.d.ts +1 -0
  54. package/dist/clis/imdb/reviews.js +88 -0
  55. package/dist/clis/imdb/search.d.ts +1 -0
  56. package/dist/clis/imdb/search.js +161 -0
  57. package/dist/clis/imdb/title.d.ts +1 -0
  58. package/dist/clis/imdb/title.js +93 -0
  59. package/dist/clis/imdb/top.d.ts +1 -0
  60. package/dist/clis/imdb/top.js +53 -0
  61. package/dist/clis/imdb/trending.d.ts +1 -0
  62. package/dist/clis/imdb/trending.js +52 -0
  63. package/dist/clis/imdb/utils.d.ts +46 -0
  64. package/dist/clis/imdb/utils.js +285 -0
  65. package/dist/clis/imdb/utils.test.d.ts +1 -0
  66. package/dist/clis/imdb/utils.test.js +88 -0
  67. package/dist/clis/jd/item.d.ts +4 -0
  68. package/dist/clis/jd/item.js +16 -15
  69. package/dist/clis/jd/item.test.js +16 -1
  70. package/dist/clis/linux-do/categories.yaml +38 -9
  71. package/dist/clis/linux-do/category.d.ts +1 -0
  72. package/dist/clis/linux-do/category.js +36 -0
  73. package/dist/clis/linux-do/feed.d.ts +45 -0
  74. package/dist/clis/linux-do/feed.js +397 -0
  75. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  76. package/dist/clis/linux-do/feed.test.js +118 -0
  77. package/dist/clis/linux-do/hot.d.ts +1 -0
  78. package/dist/clis/linux-do/hot.js +25 -0
  79. package/dist/clis/linux-do/latest.d.ts +1 -0
  80. package/dist/clis/linux-do/latest.js +18 -0
  81. package/dist/clis/linux-do/tags.yaml +41 -0
  82. package/dist/clis/linux-do/topic.yaml +41 -3
  83. package/dist/clis/linux-do/user-posts.yaml +67 -0
  84. package/dist/clis/linux-do/user-topics.yaml +54 -0
  85. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  86. package/dist/clis/paperreview/commands.test.js +243 -0
  87. package/dist/clis/paperreview/feedback.d.ts +1 -0
  88. package/dist/clis/paperreview/feedback.js +52 -0
  89. package/dist/clis/paperreview/review.d.ts +1 -0
  90. package/dist/clis/paperreview/review.js +37 -0
  91. package/dist/clis/paperreview/submit.d.ts +1 -0
  92. package/dist/clis/paperreview/submit.js +85 -0
  93. package/dist/clis/paperreview/utils.d.ts +46 -0
  94. package/dist/clis/paperreview/utils.js +197 -0
  95. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  96. package/dist/clis/paperreview/utils.test.js +49 -0
  97. package/dist/clis/producthunt/browse.d.ts +1 -0
  98. package/dist/clis/producthunt/browse.js +99 -0
  99. package/dist/clis/producthunt/hot.d.ts +1 -0
  100. package/dist/clis/producthunt/hot.js +110 -0
  101. package/dist/clis/producthunt/posts.d.ts +1 -0
  102. package/dist/clis/producthunt/posts.js +28 -0
  103. package/dist/clis/producthunt/today.d.ts +1 -0
  104. package/dist/clis/producthunt/today.js +35 -0
  105. package/dist/clis/producthunt/utils.d.ts +29 -0
  106. package/dist/clis/producthunt/utils.js +99 -0
  107. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  108. package/dist/clis/producthunt/utils.test.js +64 -0
  109. package/dist/clis/twitter/article.js +4 -28
  110. package/dist/clis/twitter/likes.d.ts +24 -0
  111. package/dist/clis/twitter/likes.js +217 -0
  112. package/dist/clis/twitter/likes.test.d.ts +1 -0
  113. package/dist/clis/twitter/likes.test.js +85 -0
  114. package/dist/clis/twitter/profile.js +4 -28
  115. package/dist/clis/twitter/search.js +2 -1
  116. package/dist/clis/twitter/search.test.js +2 -0
  117. package/dist/clis/twitter/shared.d.ts +6 -0
  118. package/dist/clis/twitter/shared.js +35 -0
  119. package/dist/clis/twitter/timeline.js +2 -13
  120. package/dist/clis/weixin/download.d.ts +17 -0
  121. package/dist/clis/weixin/download.js +88 -20
  122. package/dist/clis/weread/book.js +2 -2
  123. package/dist/clis/weread/commands.test.d.ts +3 -0
  124. package/dist/clis/weread/commands.test.js +43 -0
  125. package/dist/clis/weread/highlights.js +2 -2
  126. package/dist/clis/weread/notebooks.js +2 -2
  127. package/dist/clis/weread/notes.js +3 -3
  128. package/dist/clis/weread/shelf.js +2 -2
  129. package/dist/clis/weread/utils.d.ts +4 -4
  130. package/dist/clis/weread/utils.js +32 -14
  131. package/dist/clis/weread/utils.test.js +1 -28
  132. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  133. package/dist/clis/xiaohongshu/comments.js +74 -0
  134. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  135. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  136. package/dist/clis/xiaohongshu/publish.js +114 -18
  137. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  138. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  139. package/dist/commanderAdapter.d.ts +1 -0
  140. package/dist/commanderAdapter.js +176 -29
  141. package/dist/commanderAdapter.test.d.ts +1 -0
  142. package/dist/commanderAdapter.test.js +62 -0
  143. package/dist/daemon.js +17 -1
  144. package/dist/discovery.js +8 -14
  145. package/dist/doctor.d.ts +1 -0
  146. package/dist/doctor.js +9 -2
  147. package/dist/download/index.js +63 -51
  148. package/dist/download/index.test.js +17 -4
  149. package/dist/errors.d.ts +3 -1
  150. package/dist/errors.js +15 -32
  151. package/dist/execution.d.ts +1 -3
  152. package/dist/execution.js +21 -1
  153. package/dist/hooks.js +2 -0
  154. package/dist/main.js +5 -0
  155. package/dist/output.js +5 -1
  156. package/dist/pipeline/executor.js +3 -4
  157. package/dist/plugin-manifest.d.ts +70 -0
  158. package/dist/plugin-manifest.js +160 -0
  159. package/dist/plugin-manifest.test.d.ts +4 -0
  160. package/dist/plugin-manifest.test.js +179 -0
  161. package/dist/plugin.d.ts +38 -5
  162. package/dist/plugin.js +267 -33
  163. package/dist/plugin.test.js +220 -3
  164. package/dist/registry.d.ts +4 -0
  165. package/dist/registry.js +2 -0
  166. package/dist/runtime-detect.d.ts +21 -0
  167. package/dist/runtime-detect.js +32 -0
  168. package/dist/runtime-detect.test.d.ts +1 -0
  169. package/dist/runtime-detect.test.js +27 -0
  170. package/dist/runtime.js +1 -1
  171. package/dist/serialization.d.ts +2 -0
  172. package/dist/serialization.js +6 -0
  173. package/dist/types.d.ts +1 -0
  174. package/dist/update-check.d.ts +22 -0
  175. package/dist/update-check.js +112 -0
  176. package/dist/weixin-download.test.d.ts +1 -0
  177. package/dist/weixin-download.test.js +30 -0
  178. package/dist/weread-private-api-regression.test.d.ts +1 -0
  179. package/dist/weread-private-api-regression.test.js +122 -0
  180. package/dist/yaml-schema.d.ts +3 -0
  181. package/dist/yaml-schema.js +18 -1
  182. package/docs/.vitepress/config.mts +4 -0
  183. package/docs/adapters/browser/36kr.md +47 -0
  184. package/docs/adapters/browser/douban.md +14 -0
  185. package/docs/adapters/browser/imdb.md +47 -0
  186. package/docs/adapters/browser/jd.md +2 -2
  187. package/docs/adapters/browser/linux-do.md +181 -20
  188. package/docs/adapters/browser/paperreview.md +43 -0
  189. package/docs/adapters/browser/producthunt.md +49 -0
  190. package/docs/adapters/desktop/chatgpt.md +5 -0
  191. package/docs/adapters/index.md +6 -2
  192. package/docs/advanced/download.md +4 -0
  193. package/docs/advanced/rate-limiter-plugin.md +99 -0
  194. package/docs/guide/electron-app-cli.md +200 -0
  195. package/docs/guide/getting-started.md +1 -0
  196. package/docs/guide/plugins.md +87 -0
  197. package/docs/zh/guide/electron-app-cli.md +188 -0
  198. package/docs/zh/guide/getting-started.md +1 -0
  199. package/docs/zh/guide/plugins.md +65 -0
  200. package/extension/package.json +1 -0
  201. package/extension/scripts/package-release.mjs +179 -0
  202. package/extension/src/background.ts +2 -0
  203. package/package.json +4 -1
  204. package/scripts/postinstall.js +10 -0
  205. package/src/browser/cdp.ts +2 -1
  206. package/src/browser/discover.ts +8 -3
  207. package/src/browser/errors.ts +13 -14
  208. package/src/browser/mcp.ts +2 -1
  209. package/src/build-manifest.test.ts +23 -0
  210. package/src/build-manifest.ts +40 -15
  211. package/src/capabilityRouting.ts +2 -1
  212. package/src/cli.ts +35 -3
  213. package/src/clis/36kr/article.ts +69 -0
  214. package/src/clis/36kr/hot.test.ts +19 -0
  215. package/src/clis/36kr/hot.ts +100 -0
  216. package/src/clis/36kr/news.test.ts +90 -0
  217. package/src/clis/36kr/news.ts +54 -0
  218. package/src/clis/36kr/search.ts +78 -0
  219. package/src/clis/bilibili/comments.test.ts +102 -0
  220. package/src/clis/bilibili/comments.ts +44 -0
  221. package/src/clis/chatgpt/ask.ts +28 -14
  222. package/src/clis/chatgpt/ax.ts +180 -1
  223. package/src/clis/chatgpt/model.ts +27 -0
  224. package/src/clis/chatgpt/send.ts +16 -6
  225. package/src/clis/douban/download.test.ts +196 -0
  226. package/src/clis/douban/download.ts +78 -0
  227. package/src/clis/douban/photos.ts +36 -0
  228. package/src/clis/douban/utils.test.ts +97 -0
  229. package/src/clis/douban/utils.ts +232 -1
  230. package/src/clis/imdb/person.ts +232 -0
  231. package/src/clis/imdb/reviews.ts +111 -0
  232. package/src/clis/imdb/search.ts +179 -0
  233. package/src/clis/imdb/title.ts +121 -0
  234. package/src/clis/imdb/top.ts +67 -0
  235. package/src/clis/imdb/trending.ts +66 -0
  236. package/src/clis/imdb/utils.test.ts +117 -0
  237. package/src/clis/imdb/utils.ts +305 -0
  238. package/src/clis/jd/item.test.ts +18 -1
  239. package/src/clis/jd/item.ts +18 -15
  240. package/src/clis/linux-do/categories.yaml +38 -9
  241. package/src/clis/linux-do/category.ts +37 -0
  242. package/src/clis/linux-do/feed.test.ts +132 -0
  243. package/src/clis/linux-do/feed.ts +501 -0
  244. package/src/clis/linux-do/hot.ts +26 -0
  245. package/src/clis/linux-do/latest.ts +19 -0
  246. package/src/clis/linux-do/tags.yaml +41 -0
  247. package/src/clis/linux-do/topic.yaml +41 -3
  248. package/src/clis/linux-do/user-posts.yaml +67 -0
  249. package/src/clis/linux-do/user-topics.yaml +54 -0
  250. package/src/clis/paperreview/commands.test.ts +283 -0
  251. package/src/clis/paperreview/feedback.ts +64 -0
  252. package/src/clis/paperreview/review.ts +47 -0
  253. package/src/clis/paperreview/submit.ts +119 -0
  254. package/src/clis/paperreview/utils.test.ts +68 -0
  255. package/src/clis/paperreview/utils.ts +276 -0
  256. package/src/clis/producthunt/browse.ts +109 -0
  257. package/src/clis/producthunt/hot.ts +127 -0
  258. package/src/clis/producthunt/posts.ts +29 -0
  259. package/src/clis/producthunt/today.ts +37 -0
  260. package/src/clis/producthunt/utils.test.ts +72 -0
  261. package/src/clis/producthunt/utils.ts +122 -0
  262. package/src/clis/twitter/article.ts +5 -28
  263. package/src/clis/twitter/likes.test.ts +91 -0
  264. package/src/clis/twitter/likes.ts +256 -0
  265. package/src/clis/twitter/profile.ts +5 -28
  266. package/src/clis/twitter/search.test.ts +2 -0
  267. package/src/clis/twitter/search.ts +3 -1
  268. package/src/clis/twitter/shared.ts +45 -0
  269. package/src/clis/twitter/timeline.ts +2 -13
  270. package/src/clis/weixin/download.ts +114 -20
  271. package/src/clis/weread/book.ts +2 -2
  272. package/src/clis/weread/commands.test.ts +57 -0
  273. package/src/clis/weread/highlights.ts +2 -2
  274. package/src/clis/weread/notebooks.ts +2 -2
  275. package/src/clis/weread/notes.ts +3 -3
  276. package/src/clis/weread/shelf.ts +2 -2
  277. package/src/clis/weread/utils.test.ts +1 -32
  278. package/src/clis/weread/utils.ts +41 -16
  279. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  280. package/src/clis/xiaohongshu/comments.ts +81 -0
  281. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  282. package/src/clis/xiaohongshu/publish.ts +129 -18
  283. package/src/commanderAdapter.test.ts +78 -0
  284. package/src/commanderAdapter.ts +188 -24
  285. package/src/daemon.ts +19 -1
  286. package/src/discovery.ts +8 -15
  287. package/src/doctor.ts +13 -2
  288. package/src/download/index.test.ts +14 -4
  289. package/src/download/index.ts +67 -55
  290. package/src/errors.ts +25 -66
  291. package/src/execution.ts +28 -3
  292. package/src/hooks.ts +1 -0
  293. package/src/main.ts +6 -0
  294. package/src/output.ts +3 -1
  295. package/src/pipeline/executor.ts +4 -6
  296. package/src/plugin-manifest.test.ts +223 -0
  297. package/src/plugin-manifest.ts +206 -0
  298. package/src/plugin.test.ts +246 -2
  299. package/src/plugin.ts +338 -36
  300. package/src/registry.ts +6 -1
  301. package/src/runtime-detect.test.ts +30 -0
  302. package/src/runtime-detect.ts +36 -0
  303. package/src/runtime.ts +1 -1
  304. package/src/serialization.ts +4 -0
  305. package/src/types.ts +1 -0
  306. package/src/update-check.ts +114 -0
  307. package/src/weixin-download.test.ts +64 -0
  308. package/src/weread-private-api-regression.test.ts +150 -0
  309. package/src/yaml-schema.ts +20 -0
  310. package/tests/e2e/browser-auth.test.ts +13 -9
  311. package/tests/e2e/browser-public-extended.test.ts +1 -1
  312. package/tests/e2e/browser-public.test.ts +62 -4
  313. package/tests/e2e/helpers.ts +2 -1
  314. package/tests/e2e/public-commands.test.ts +37 -3
  315. package/tests/smoke/api-health.test.ts +1 -1
  316. package/vitest.config.ts +10 -0
  317. package/dist/clis/linux-do/category.yaml +0 -51
  318. package/dist/clis/linux-do/hot.yaml +0 -50
  319. package/dist/clis/linux-do/latest.yaml +0 -40
  320. package/src/clis/linux-do/category.yaml +0 -51
  321. package/src/clis/linux-do/hot.yaml +0 -50
  322. package/src/clis/linux-do/latest.yaml +0 -40
package/dist/discovery.js CHANGED
@@ -19,6 +19,7 @@ import { log } from './logger.js';
19
19
  export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
20
20
  /** Matches files that register commands via cli() or lifecycle hooks */
21
21
  const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
22
+ import { parseYamlArgs } from './yaml-schema.js';
22
23
  function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
23
24
  if (!rawStrategy)
24
25
  return fallback;
@@ -71,6 +72,8 @@ async function loadFromManifest(manifestPath, clisDir) {
71
72
  pipeline: entry.pipeline,
72
73
  timeoutSeconds: entry.timeout,
73
74
  source: `manifest:${entry.site}/${entry.name}`,
75
+ deprecated: entry.deprecated,
76
+ replacedBy: entry.replacedBy,
74
77
  navigateBefore: entry.navigateBefore,
75
78
  };
76
79
  registerCommand(cmd);
@@ -91,6 +94,8 @@ async function loadFromManifest(manifestPath, clisDir) {
91
94
  columns: entry.columns,
92
95
  timeoutSeconds: entry.timeout,
93
96
  source: modulePath,
97
+ deprecated: entry.deprecated,
98
+ replacedBy: entry.replacedBy,
94
99
  navigateBefore: entry.navigateBefore,
95
100
  _lazy: true,
96
101
  _modulePath: modulePath,
@@ -153,20 +158,7 @@ async function registerYamlCli(filePath, defaultSite) {
153
158
  const strategyStr = cliDef.strategy ?? (cliDef.browser === false ? 'public' : 'cookie');
154
159
  const strategy = parseStrategy(strategyStr);
155
160
  const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
156
- const args = [];
157
- if (cliDef.args && typeof cliDef.args === 'object') {
158
- for (const [argName, argDef] of Object.entries(cliDef.args)) {
159
- args.push({
160
- name: argName,
161
- type: argDef?.type ?? 'str',
162
- default: argDef?.default,
163
- required: argDef?.required ?? false,
164
- positional: argDef?.positional ?? false,
165
- help: argDef?.description ?? argDef?.help ?? '',
166
- choices: argDef?.choices,
167
- });
168
- }
169
- }
161
+ const args = parseYamlArgs(cliDef.args);
170
162
  const cmd = {
171
163
  site,
172
164
  name,
@@ -179,6 +171,8 @@ async function registerYamlCli(filePath, defaultSite) {
179
171
  pipeline: cliDef.pipeline,
180
172
  timeoutSeconds: cliDef.timeout,
181
173
  source: filePath,
174
+ deprecated: cliDef.deprecated,
175
+ replacedBy: cliDef.replacedBy,
182
176
  navigateBefore: cliDef.navigateBefore,
183
177
  };
184
178
  registerCommand(cmd);
package/dist/doctor.d.ts CHANGED
@@ -20,6 +20,7 @@ export type DoctorReport = {
20
20
  cliVersion?: string;
21
21
  daemonRunning: boolean;
22
22
  extensionConnected: boolean;
23
+ extensionVersion?: string;
23
24
  connectivity?: ConnectivityResult;
24
25
  sessions?: Array<{
25
26
  workspace: string;
package/dist/doctor.js CHANGED
@@ -10,6 +10,7 @@ import { checkDaemonStatus } from './browser/discover.js';
10
10
  import { BrowserBridge } from './browser/index.js';
11
11
  import { listSessions } from './browser/daemon-client.js';
12
12
  import { getErrorMessage } from './errors.js';
13
+ import { getRuntimeLabel } from './runtime-detect.js';
13
14
  /**
14
15
  * Test connectivity by attempting a real browser command.
15
16
  */
@@ -64,23 +65,29 @@ export async function runBrowserDoctor(opts = {}) {
64
65
  if (connectivity && !connectivity.ok) {
65
66
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
66
67
  }
68
+ if (status.extensionVersion && opts.cliVersion && status.extensionVersion !== opts.cliVersion) {
69
+ issues.push(`Extension version mismatch: extension v${status.extensionVersion} ≠ CLI v${opts.cliVersion}\n` +
70
+ ' Download the latest extension from: https://github.com/jackwener/opencli/releases');
71
+ }
67
72
  return {
68
73
  cliVersion: opts.cliVersion,
69
74
  daemonRunning: status.running,
70
75
  extensionConnected: status.extensionConnected,
76
+ extensionVersion: status.extensionVersion,
71
77
  connectivity,
72
78
  sessions,
73
79
  issues,
74
80
  };
75
81
  }
76
82
  export function renderBrowserDoctorReport(report) {
77
- const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
83
+ const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`) + chalk.dim(` (${getRuntimeLabel()})`), ''];
78
84
  // Daemon status
79
85
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
80
86
  lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
81
87
  // Extension status
82
88
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
83
- lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
89
+ const extVersion = report.extensionVersion ? chalk.dim(` (v${report.extensionVersion})`) : '';
90
+ lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}${extVersion}`);
84
91
  // Connectivity
85
92
  if (report.connectivity) {
86
93
  const connIcon = report.connectivity.ok ? chalk.green('[OK]') : chalk.red('[FAIL]');
@@ -7,6 +7,8 @@ import * as path from 'node:path';
7
7
  import * as https from 'node:https';
8
8
  import * as http from 'node:http';
9
9
  import * as os from 'node:os';
10
+ import { Transform } from 'node:stream';
11
+ import { pipeline } from 'node:stream/promises';
10
12
  import { URL } from 'node:url';
11
13
  import { isBinaryInstalled } from '../external.js';
12
14
  import { getErrorMessage } from '../errors.js';
@@ -68,65 +70,75 @@ export async function httpDownload(url, destPath, options = {}, redirectCount =
68
70
  if (cookies) {
69
71
  requestHeaders['Cookie'] = cookies;
70
72
  }
71
- // Ensure directory exists
72
- const dir = path.dirname(destPath);
73
- fs.mkdirSync(dir, { recursive: true });
74
73
  const tempPath = `${destPath}.tmp`;
75
- const file = fs.createWriteStream(tempPath);
76
- const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
77
- // Handle redirects
78
- if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
79
- file.close();
80
- if (fs.existsSync(tempPath))
81
- fs.unlinkSync(tempPath);
82
- if (redirectCount >= maxRedirects) {
83
- resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
84
- return;
85
- }
86
- const redirectUrl = resolveRedirectUrl(url, response.headers.location);
87
- const originalHost = new URL(url).hostname;
88
- const redirectHost = new URL(redirectUrl).hostname;
89
- // Do not forward cookies when a redirect crosses host boundaries.
90
- const redirectOptions = originalHost === redirectHost
91
- ? options
92
- : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
93
- httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1).then(resolve);
74
+ let settled = false;
75
+ const finish = (result) => {
76
+ if (settled)
94
77
  return;
78
+ settled = true;
79
+ resolve(result);
80
+ };
81
+ const cleanupTempFile = async () => {
82
+ try {
83
+ await fs.promises.rm(tempPath, { force: true });
95
84
  }
96
- if (response.statusCode !== 200) {
97
- file.close();
98
- if (fs.existsSync(tempPath))
99
- fs.unlinkSync(tempPath);
100
- resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
101
- return;
85
+ catch {
86
+ // Ignore cleanup errors so the original failure is preserved.
102
87
  }
103
- const totalSize = parseInt(response.headers['content-length'] || '0', 10);
104
- let received = 0;
105
- response.on('data', (chunk) => {
106
- received += chunk.length;
107
- if (onProgress)
108
- onProgress(received, totalSize);
109
- });
110
- response.pipe(file);
111
- file.on('finish', () => {
112
- file.close();
113
- // Rename temp file to final destination
114
- fs.renameSync(tempPath, destPath);
115
- resolve({ success: true, size: received });
116
- });
88
+ };
89
+ const request = protocol.get(url, { headers: requestHeaders, timeout }, (response) => {
90
+ void (async () => {
91
+ // Handle redirects before creating any file handles.
92
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
93
+ response.resume();
94
+ if (redirectCount >= maxRedirects) {
95
+ finish({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
96
+ return;
97
+ }
98
+ const redirectUrl = resolveRedirectUrl(url, response.headers.location);
99
+ const originalHost = new URL(url).hostname;
100
+ const redirectHost = new URL(redirectUrl).hostname;
101
+ const redirectOptions = originalHost === redirectHost
102
+ ? options
103
+ : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
104
+ finish(await httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1));
105
+ return;
106
+ }
107
+ if (response.statusCode !== 200) {
108
+ response.resume();
109
+ finish({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
110
+ return;
111
+ }
112
+ const totalSize = parseInt(response.headers['content-length'] || '0', 10);
113
+ let received = 0;
114
+ const progressStream = new Transform({
115
+ transform(chunk, _encoding, callback) {
116
+ received += chunk.length;
117
+ if (onProgress)
118
+ onProgress(received, totalSize);
119
+ callback(null, chunk);
120
+ },
121
+ });
122
+ try {
123
+ await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
124
+ await pipeline(response, progressStream, fs.createWriteStream(tempPath));
125
+ await fs.promises.rename(tempPath, destPath);
126
+ finish({ success: true, size: received });
127
+ }
128
+ catch (err) {
129
+ await cleanupTempFile();
130
+ finish({ success: false, size: 0, error: getErrorMessage(err) });
131
+ }
132
+ })();
117
133
  });
118
134
  request.on('error', (err) => {
119
- file.close();
120
- if (fs.existsSync(tempPath))
121
- fs.unlinkSync(tempPath);
122
- resolve({ success: false, size: 0, error: err.message });
135
+ void (async () => {
136
+ await cleanupTempFile();
137
+ finish({ success: false, size: 0, error: err.message });
138
+ })();
123
139
  });
124
140
  request.on('timeout', () => {
125
- request.destroy();
126
- file.close();
127
- if (fs.existsSync(tempPath))
128
- fs.unlinkSync(tempPath);
129
- resolve({ success: false, size: 0, error: 'Timeout' });
141
+ request.destroy(new Error('Timeout'));
130
142
  });
131
143
  });
132
144
  }
@@ -5,11 +5,19 @@ import * as path from 'node:path';
5
5
  import { afterEach, describe, expect, it } from 'vitest';
6
6
  import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
7
7
  const servers = [];
8
+ const tempDirs = [];
8
9
  afterEach(async () => {
9
10
  await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
10
11
  server.close((err) => (err ? reject(err) : resolve()));
11
12
  })));
12
13
  servers.length = 0;
14
+ for (const dir of tempDirs) {
15
+ try {
16
+ fs.rmSync(dir, { recursive: true, force: true });
17
+ }
18
+ catch { /* ignore */ }
19
+ }
20
+ tempDirs.length = 0;
13
21
  });
14
22
  async function startServer(handler, hostname = '127.0.0.1') {
15
23
  const server = http.createServer(handler);
@@ -21,7 +29,9 @@ async function startServer(handler, hostname = '127.0.0.1') {
21
29
  }
22
30
  return `http://${hostname}:${address.port}`;
23
31
  }
24
- describe('download helpers', () => {
32
+ // Windows Defender can briefly lock newly-written .tmp files, causing EPERM.
33
+ // Retry once to handle this flakiness.
34
+ describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => {
25
35
  it('resolves relative redirects against the original URL', () => {
26
36
  expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
27
37
  expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
@@ -38,7 +48,8 @@ describe('download helpers', () => {
38
48
  res.setHeader('Location', '/loop');
39
49
  res.end();
40
50
  });
41
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
51
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
52
+ tempDirs.push(tempDir);
42
53
  const destPath = path.join(tempDir, 'file.txt');
43
54
  const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
44
55
  expect(result).toEqual({
@@ -60,7 +71,8 @@ describe('download helpers', () => {
60
71
  res.setHeader('Location', targetUrl);
61
72
  res.end();
62
73
  });
63
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
74
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
75
+ tempDirs.push(tempDir);
64
76
  const destPath = path.join(tempDir, 'redirect.txt');
65
77
  const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
66
78
  expect(result).toEqual({ success: true, size: 2 });
@@ -79,7 +91,8 @@ describe('download helpers', () => {
79
91
  res.setHeader('Location', targetUrl);
80
92
  res.end();
81
93
  });
82
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
94
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-'));
95
+ tempDirs.push(tempDir);
83
96
  const destPath = path.join(tempDir, 'redirect-header.txt');
84
97
  const result = await httpDownload(`${redirectUrl}/start`, destPath, {
85
98
  headers: { Cookie: 'sid=header-cookie' },
package/dist/errors.d.ts CHANGED
@@ -12,8 +12,10 @@ export declare class CliError extends Error {
12
12
  readonly hint?: string;
13
13
  constructor(code: string, message: string, hint?: string);
14
14
  }
15
+ export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
15
16
  export declare class BrowserConnectError extends CliError {
16
- constructor(message: string, hint?: string);
17
+ readonly kind: BrowserConnectKind;
18
+ constructor(message: string, hint?: string, kind?: BrowserConnectKind);
17
19
  }
18
20
  export declare class AdapterLoadError extends CliError {
19
21
  constructor(message: string, hint?: string);
package/dist/errors.js CHANGED
@@ -12,74 +12,50 @@ export class CliError extends Error {
12
12
  hint;
13
13
  constructor(code, message, hint) {
14
14
  super(message);
15
- this.name = 'CliError';
15
+ this.name = new.target.name;
16
16
  this.code = code;
17
17
  this.hint = hint;
18
18
  }
19
19
  }
20
- // ── Browser / Connection ────────────────────────────────────────────────────
21
20
  export class BrowserConnectError extends CliError {
22
- constructor(message, hint) {
21
+ kind;
22
+ constructor(message, hint, kind = 'unknown') {
23
23
  super('BROWSER_CONNECT', message, hint);
24
- this.name = 'BrowserConnectError';
24
+ this.kind = kind;
25
25
  }
26
26
  }
27
- // ── Adapter loading ─────────────────────────────────────────────────────────
28
27
  export class AdapterLoadError extends CliError {
29
- constructor(message, hint) {
30
- super('ADAPTER_LOAD', message, hint);
31
- this.name = 'AdapterLoadError';
32
- }
28
+ constructor(message, hint) { super('ADAPTER_LOAD', message, hint); }
33
29
  }
34
- // ── Command execution ───────────────────────────────────────────────────────
35
30
  export class CommandExecutionError extends CliError {
36
- constructor(message, hint) {
37
- super('COMMAND_EXEC', message, hint);
38
- this.name = 'CommandExecutionError';
39
- }
31
+ constructor(message, hint) { super('COMMAND_EXEC', message, hint); }
40
32
  }
41
- // ── Configuration ───────────────────────────────────────────────────────────
42
33
  export class ConfigError extends CliError {
43
- constructor(message, hint) {
44
- super('CONFIG', message, hint);
45
- this.name = 'ConfigError';
46
- }
34
+ constructor(message, hint) { super('CONFIG', message, hint); }
47
35
  }
48
- // ── Authentication / Login ──────────────────────────────────────────────────
49
36
  export class AuthRequiredError extends CliError {
50
37
  domain;
51
38
  constructor(domain, message) {
52
39
  super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`);
53
- this.name = 'AuthRequiredError';
54
40
  this.domain = domain;
55
41
  }
56
42
  }
57
- // ── Timeout ─────────────────────────────────────────────────────────────────
58
43
  export class TimeoutError extends CliError {
59
44
  constructor(label, seconds) {
60
45
  super('TIMEOUT', `${label} timed out after ${seconds}s`, 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var');
61
- this.name = 'TimeoutError';
62
46
  }
63
47
  }
64
- // ── Argument validation ─────────────────────────────────────────────────────
65
48
  export class ArgumentError extends CliError {
66
- constructor(message, hint) {
67
- super('ARGUMENT', message, hint);
68
- this.name = 'ArgumentError';
69
- }
49
+ constructor(message, hint) { super('ARGUMENT', message, hint); }
70
50
  }
71
- // ── Empty result ────────────────────────────────────────────────────────────
72
51
  export class EmptyResultError extends CliError {
73
52
  constructor(command, hint) {
74
53
  super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
75
- this.name = 'EmptyResultError';
76
54
  }
77
55
  }
78
- // ── Selector / DOM ──────────────────────────────────────────────────────────
79
56
  export class SelectorError extends CliError {
80
57
  constructor(selector, hint) {
81
58
  super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
82
- this.name = 'SelectorError';
83
59
  }
84
60
  }
85
61
  // ── Utilities ───────────────────────────────────────────────────────────
@@ -95,4 +71,11 @@ export const ERROR_ICONS = {
95
71
  ARGUMENT: '❌',
96
72
  EMPTY_RESULT: '📭',
97
73
  SELECTOR: '🔍',
74
+ COMMAND_EXEC: '💥',
75
+ ADAPTER_LOAD: '📦',
76
+ NETWORK: '🌐',
77
+ API_ERROR: '🚫',
78
+ RATE_LIMITED: '⏳',
79
+ PAGE_CHANGED: '🔄',
80
+ CONFIG: '⚙️ ',
98
81
  };
@@ -9,8 +9,6 @@
9
9
  * 5. Lazy-loading of TS modules from manifest
10
10
  * 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
11
11
  */
12
- import { type CliCommand, type Arg } from './registry.js';
13
- type CommandArgs = Record<string, unknown>;
12
+ import { type CliCommand, type Arg, type CommandArgs } from './registry.js';
14
13
  export declare function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): CommandArgs;
15
14
  export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs, debug?: boolean): Promise<unknown>;
16
- export {};
package/dist/execution.js CHANGED
@@ -12,10 +12,13 @@
12
12
  import { Strategy, getRegistry, fullName } from './registry.js';
13
13
  import { pathToFileURL } from 'node:url';
14
14
  import { executePipeline } from './pipeline/index.js';
15
- import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
15
+ import { AdapterLoadError, ArgumentError, BrowserConnectError, CommandExecutionError, getErrorMessage } from './errors.js';
16
16
  import { shouldUseBrowserSession } from './capabilityRouting.js';
17
17
  import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
18
18
  import { emitHook } from './hooks.js';
19
+ import { checkDaemonStatus } from './browser/discover.js';
20
+ import { PKG_VERSION } from './version.js';
21
+ import chalk from 'chalk';
19
22
  const _loadedModules = new Set();
20
23
  export function coerceAndValidateArgs(cmdArgs, kwargs) {
21
24
  const result = { ...kwargs };
@@ -126,6 +129,23 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
126
129
  let result;
127
130
  try {
128
131
  if (shouldUseBrowserSession(cmd)) {
132
+ // ── Fail-fast: only when daemon is UP but extension is not connected ──
133
+ // If daemon is not running, let browserSession() handle auto-start as usual.
134
+ // We only short-circuit when the daemon confirms the extension is missing —
135
+ // that's a clear setup gap, not a transient startup state.
136
+ // Use a short timeout: localhost responds in <50ms when running.
137
+ // 300ms avoids a full 2s wait on cold-start (daemon not yet running).
138
+ const status = await checkDaemonStatus({ timeout: 300 });
139
+ if (status.running && !status.extensionConnected) {
140
+ throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
141
+ ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
142
+ ' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
143
+ ' Then run: opencli doctor');
144
+ }
145
+ // ── Version mismatch: warn but don't block ──
146
+ if (status.extensionVersion && status.extensionVersion !== PKG_VERSION) {
147
+ process.stderr.write(chalk.yellow(`⚠ Extension v${status.extensionVersion} ≠ CLI v${PKG_VERSION} — consider updating the extension.\n`));
148
+ }
129
149
  ensureRequiredEnv(cmd);
130
150
  const BrowserFactory = getBrowserFactory();
131
151
  result = await browserSession(BrowserFactory, async (page) => {
package/dist/hooks.js CHANGED
@@ -15,6 +15,8 @@ const _hooks = globalThis.__opencli_hooks__ ??= new Map();
15
15
  // ── Registration API (used by plugins) ─────────────────────────────────────
16
16
  function addHook(name, fn) {
17
17
  const list = _hooks.get(name) ?? [];
18
+ if (list.includes(fn))
19
+ return;
18
20
  list.push(fn);
19
21
  _hooks.set(name, list);
20
22
  }
package/dist/main.js CHANGED
@@ -19,12 +19,17 @@ import { discoverClis, discoverPlugins } from './discovery.js';
19
19
  import { getCompletions } from './completion.js';
20
20
  import { runCli } from './cli.js';
21
21
  import { emitHook } from './hooks.js';
22
+ import { registerUpdateNoticeOnExit, checkForUpdateBackground } from './update-check.js';
22
23
  const __filename = fileURLToPath(import.meta.url);
23
24
  const __dirname = path.dirname(__filename);
24
25
  const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
25
26
  const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
26
27
  await discoverClis(BUILTIN_CLIS, USER_CLIS);
27
28
  await discoverPlugins();
29
+ // Register exit hook: notice appears after command output (same as npm/gh/yarn)
30
+ registerUpdateNoticeOnExit();
31
+ // Kick off background fetch for next run (non-blocking)
32
+ checkForUpdateBackground();
28
33
  // ── Fast-path: handle --get-completions before commander parses ─────────
29
34
  // Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
30
35
  const getCompIdx = process.argv.indexOf('--get-completions');
package/dist/output.js CHANGED
@@ -5,7 +5,11 @@ import chalk from 'chalk';
5
5
  import Table from 'cli-table3';
6
6
  import yaml from 'js-yaml';
7
7
  function normalizeRows(data) {
8
- return Array.isArray(data) ? data : [data];
8
+ if (Array.isArray(data))
9
+ return data;
10
+ if (data && typeof data === 'object')
11
+ return [data];
12
+ return [{ value: data }];
9
13
  }
10
14
  function resolveColumns(rows, opts) {
11
15
  return opts.columns ?? Object.keys(rows[0] ?? {});
@@ -4,8 +4,7 @@
4
4
  import { getStep } from './registry.js';
5
5
  import { log } from '../logger.js';
6
6
  import { ConfigError } from '../errors.js';
7
- /** Steps that interact with the browser and may fail transiently */
8
- const BROWSER_STEPS = new Set(['navigate', 'evaluate', 'click', 'type', 'press', 'wait', 'snapshot']);
7
+ import { BROWSER_ONLY_STEPS } from '../capabilityRouting.js';
9
8
  export async function executePipeline(page, pipeline, ctx = {}) {
10
9
  const args = ctx.args ?? {};
11
10
  const debug = ctx.debug ?? false;
@@ -33,7 +32,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
33
32
  }
34
33
  catch (err) {
35
34
  // Attempt cleanup: close automation window on pipeline failure
36
- if (page && typeof page.closeWindow === 'function') {
35
+ if (page?.closeWindow) {
37
36
  try {
38
37
  await page.closeWindow();
39
38
  }
@@ -44,7 +43,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
44
43
  return data;
45
44
  }
46
45
  async function executeStepWithRetry(handler, page, params, data, args, op, configRetries) {
47
- const maxRetries = configRetries ?? (BROWSER_STEPS.has(op) ? 2 : 0);
46
+ const maxRetries = configRetries ?? (BROWSER_ONLY_STEPS.has(op) ? 2 : 0);
48
47
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49
48
  try {
50
49
  return await handler(page, params, data, args);
@@ -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, };