@jackwener/opencli 1.3.3 → 1.4.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.
- package/.github/pull_request_template.md +3 -1
- package/.github/workflows/build-extension.yml +7 -1
- package/.github/workflows/ci.yml +29 -3
- package/.github/workflows/docs.yml +1 -1
- package/.github/workflows/e2e-headed.yml +20 -0
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/security.yml +0 -3
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +6 -3
- package/README.md +30 -3
- package/README.zh-CN.md +30 -3
- package/SKILL.md +7 -1
- package/TESTING.md +1 -0
- package/chatwise-opencli.ps1 +82 -0
- package/dist/analysis.d.ts +38 -0
- package/dist/analysis.js +166 -0
- package/dist/browser/cdp.d.ts +0 -4
- package/dist/browser/cdp.js +53 -41
- package/dist/browser/cdp.test.d.ts +1 -0
- package/dist/browser/cdp.test.js +52 -0
- package/dist/browser/dom-snapshot.d.ts +2 -2
- package/dist/browser/dom-snapshot.js +54 -1
- package/dist/browser/dom-snapshot.test.js +36 -0
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/mcp.d.ts +0 -2
- package/dist/browser/mcp.js +2 -3
- package/dist/browser/page.d.ts +4 -3
- package/dist/browser/page.js +34 -37
- package/dist/browser/stealth.d.ts +0 -2
- package/dist/browser/stealth.js +24 -9
- package/dist/browser.test.js +2 -2
- package/dist/build-manifest.js +15 -9
- package/dist/build-manifest.test.js +12 -0
- package/dist/cascade.js +4 -2
- package/dist/cli-manifest.json +639 -258
- package/dist/cli.js +57 -29
- package/dist/clis/_shared/desktop-commands.d.ts +22 -0
- package/dist/clis/_shared/desktop-commands.js +108 -0
- package/dist/clis/antigravity/serve.js +5 -2
- package/dist/clis/arxiv/search.js +1 -1
- package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
- package/dist/clis/bilibili/dynamic.test.js +68 -0
- package/dist/clis/bilibili/favorite.js +4 -2
- package/dist/clis/bilibili/following.js +3 -2
- package/dist/clis/bilibili/subtitle.js +8 -7
- package/dist/clis/bilibili/utils.js +2 -2
- package/dist/clis/boss/batchgreet.js +1 -1
- package/dist/clis/boss/chatlist.js +1 -1
- package/dist/clis/boss/chatmsg.js +1 -1
- package/dist/clis/boss/detail.js +1 -1
- package/dist/clis/boss/exchange.js +1 -1
- package/dist/clis/boss/greet.js +1 -1
- package/dist/clis/boss/invite.js +1 -1
- package/dist/clis/boss/joblist.js +1 -1
- package/dist/clis/boss/mark.js +4 -3
- package/dist/clis/boss/recommend.js +1 -1
- package/dist/clis/boss/resume.js +1 -1
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +5 -4
- package/dist/clis/boss/stats.js +1 -1
- package/dist/clis/chatgpt/ask.js +4 -0
- package/dist/clis/chatgpt/new.js +5 -1
- package/dist/clis/chatgpt/read.js +5 -1
- package/dist/clis/chatgpt/send.js +2 -1
- package/dist/clis/chatgpt/status.js +5 -1
- package/dist/clis/chatwise/ask.js +8 -2
- package/dist/clis/chatwise/export.js +2 -0
- package/dist/clis/chatwise/history.js +2 -0
- package/dist/clis/chatwise/model.js +8 -3
- package/dist/clis/chatwise/new.js +3 -18
- package/dist/clis/chatwise/read.js +2 -0
- package/dist/clis/chatwise/screenshot.js +3 -27
- package/dist/clis/chatwise/send.js +8 -2
- package/dist/clis/chatwise/shared.d.ts +2 -0
- package/dist/clis/chatwise/shared.js +6 -0
- package/dist/clis/chatwise/status.js +3 -22
- package/dist/clis/codex/ask.js +6 -2
- package/dist/clis/codex/dump.js +2 -25
- package/dist/clis/codex/new.js +2 -25
- package/dist/clis/codex/screenshot.js +2 -27
- package/dist/clis/codex/send.js +6 -4
- package/dist/clis/codex/status.js +2 -22
- package/dist/clis/cursor/ask.js +2 -1
- package/dist/clis/cursor/composer.js +2 -1
- package/dist/clis/cursor/dump.js +2 -25
- package/dist/clis/cursor/new.js +2 -18
- package/dist/clis/cursor/read.js +2 -1
- package/dist/clis/cursor/screenshot.js +1 -30
- package/dist/clis/cursor/send.js +2 -1
- package/dist/clis/cursor/status.js +2 -21
- package/dist/clis/dictionary/examples.yaml +25 -0
- package/dist/clis/dictionary/search.yaml +27 -0
- package/dist/clis/dictionary/synonyms.yaml +25 -0
- package/dist/clis/douban/book-hot.js +1 -1
- package/dist/clis/douban/movie-hot.js +1 -1
- package/dist/clis/douban/search.js +1 -1
- package/dist/clis/douban/utils.d.ts +4 -1
- package/dist/clis/douban/utils.js +156 -1
- package/dist/clis/doubao/ask.js +1 -1
- package/dist/clis/doubao/new.js +1 -1
- package/dist/clis/doubao/read.js +1 -1
- package/dist/clis/doubao/send.js +1 -1
- package/dist/clis/doubao/status.js +1 -1
- package/dist/clis/doubao-app/ask.js +1 -1
- package/dist/clis/doubao-app/new.js +1 -1
- package/dist/clis/doubao-app/read.js +1 -1
- package/dist/clis/doubao-app/send.js +1 -1
- package/dist/clis/grok/ask.d.ts +4 -0
- package/dist/clis/grok/ask.js +28 -10
- package/dist/clis/grok/ask.test.js +18 -0
- package/dist/clis/jd/item.d.ts +1 -0
- package/dist/clis/jd/item.js +96 -0
- package/dist/clis/jd/item.test.d.ts +1 -0
- package/dist/clis/jd/item.test.js +28 -0
- package/dist/clis/jike/feed.js +1 -1
- package/dist/clis/jike/search.js +1 -1
- package/dist/clis/linkedin/search.js +5 -4
- package/dist/clis/linkedin/timeline.d.ts +21 -0
- package/dist/clis/linkedin/timeline.js +503 -0
- package/dist/clis/linkedin/timeline.test.d.ts +1 -0
- package/dist/clis/linkedin/timeline.test.js +81 -0
- package/dist/clis/medium/feed.js +1 -1
- package/dist/clis/medium/search.js +1 -1
- package/dist/clis/medium/user.js +1 -1
- package/dist/clis/medium/{shared.js → utils.js} +2 -1
- package/dist/clis/pixiv/detail.yaml +49 -0
- package/dist/clis/pixiv/download.d.ts +7 -0
- package/dist/clis/pixiv/download.js +78 -0
- package/dist/clis/pixiv/download.test.d.ts +1 -0
- package/dist/clis/pixiv/download.test.js +87 -0
- package/dist/clis/pixiv/illusts.d.ts +8 -0
- package/dist/clis/pixiv/illusts.js +65 -0
- package/dist/clis/pixiv/illusts.test.d.ts +1 -0
- package/dist/clis/pixiv/illusts.test.js +99 -0
- package/dist/clis/pixiv/ranking.yaml +53 -0
- package/dist/clis/pixiv/search.d.ts +6 -0
- package/dist/clis/pixiv/search.js +43 -0
- package/dist/clis/pixiv/search.test.d.ts +1 -0
- package/dist/clis/pixiv/search.test.js +83 -0
- package/dist/clis/pixiv/test-utils.d.ts +12 -0
- package/dist/clis/pixiv/test-utils.js +23 -0
- package/dist/clis/pixiv/user.yaml +46 -0
- package/dist/clis/pixiv/utils.d.ts +27 -0
- package/dist/clis/pixiv/utils.js +49 -0
- package/dist/clis/reddit/comment.js +2 -1
- package/dist/clis/reddit/read.js +4 -3
- package/dist/clis/reddit/read.test.d.ts +1 -0
- package/dist/clis/reddit/read.test.js +28 -0
- package/dist/clis/reddit/save.js +2 -1
- package/dist/clis/reddit/saved.js +7 -3
- package/dist/clis/reddit/subscribe.js +2 -1
- package/dist/clis/reddit/upvote.js +2 -1
- package/dist/clis/reddit/upvoted.js +7 -3
- package/dist/clis/sinablog/article.js +1 -1
- package/dist/clis/sinablog/hot.js +1 -1
- package/dist/clis/sinablog/user.js +1 -1
- package/dist/clis/substack/feed.js +1 -1
- package/dist/clis/substack/publication.js +1 -1
- package/dist/clis/substack/search.js +3 -2
- package/dist/clis/substack/{shared.js → utils.js} +3 -2
- package/dist/clis/tiktok/search.yaml +2 -1
- package/dist/clis/twitter/accept.js +2 -1
- package/dist/clis/twitter/article.js +4 -1
- package/dist/clis/twitter/block.js +2 -1
- package/dist/clis/twitter/bookmark.js +2 -1
- package/dist/clis/twitter/bookmarks.js +3 -2
- package/dist/clis/twitter/delete.js +2 -1
- package/dist/clis/twitter/follow.js +2 -1
- package/dist/clis/twitter/followers.js +3 -2
- package/dist/clis/twitter/following.js +3 -2
- package/dist/clis/twitter/hide-reply.js +2 -1
- package/dist/clis/twitter/like.js +2 -1
- package/dist/clis/twitter/notifications.js +2 -1
- package/dist/clis/twitter/post.js +2 -1
- package/dist/clis/twitter/profile.js +5 -2
- package/dist/clis/twitter/reply-dm.js +2 -1
- package/dist/clis/twitter/reply.js +2 -1
- package/dist/clis/twitter/search.js +30 -13
- package/dist/clis/twitter/search.test.d.ts +1 -0
- package/dist/clis/twitter/search.test.js +104 -0
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/timeline.js +3 -2
- package/dist/clis/twitter/trending.js +3 -2
- package/dist/clis/twitter/unblock.js +2 -1
- package/dist/clis/twitter/unbookmark.js +2 -1
- package/dist/clis/twitter/unfollow.js +2 -1
- package/dist/clis/v2ex/daily.js +3 -2
- package/dist/clis/v2ex/me.js +3 -2
- package/dist/clis/v2ex/notifications.js +4 -4
- package/dist/clis/web/read.d.ts +16 -0
- package/dist/clis/web/read.js +202 -0
- package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
- package/dist/clis/xueqiu/danjuan-utils.js +126 -0
- package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
- package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
- package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
- package/dist/clis/xueqiu/fund-holdings.js +28 -0
- package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
- package/dist/clis/xueqiu/fund-snapshot.js +25 -0
- package/dist/clis/youtube/transcript.js +5 -4
- package/dist/clis/youtube/video.js +3 -2
- package/dist/daemon.js +7 -3
- package/dist/discovery.js +11 -10
- package/dist/doctor.js +2 -1
- package/dist/download/index.d.ts +4 -12
- package/dist/download/index.js +33 -12
- package/dist/download/index.test.js +79 -2
- package/dist/download/media-download.js +4 -2
- package/dist/engine.test.js +76 -4
- package/dist/execution.d.ts +1 -9
- package/dist/execution.js +56 -46
- package/dist/explore.js +12 -111
- package/dist/external-clis.yaml +0 -8
- package/dist/external.js +7 -5
- package/dist/external.test.js +4 -0
- package/dist/generate.d.ts +0 -9
- package/dist/generate.js +4 -20
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +56 -0
- package/dist/hooks.test.d.ts +4 -0
- package/dist/hooks.test.js +92 -0
- package/dist/interceptor.js +70 -23
- package/dist/main.js +2 -0
- package/dist/output.js +12 -6
- package/dist/pipeline/executor.js +1 -1
- package/dist/pipeline/steps/browser.js +1 -3
- package/dist/pipeline/steps/download.js +42 -26
- package/dist/pipeline/steps/download.test.d.ts +1 -0
- package/dist/pipeline/steps/download.test.js +101 -0
- package/dist/pipeline/steps/fetch.js +40 -22
- package/dist/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/pipeline/steps/fetch.test.js +123 -0
- package/dist/pipeline/steps/transform.js +2 -6
- package/dist/pipeline/template.js +66 -52
- package/dist/pipeline/template.test.js +28 -0
- package/dist/pipeline/transform.test.js +18 -0
- package/dist/plugin.d.ts +40 -1
- package/dist/plugin.js +214 -17
- package/dist/plugin.test.d.ts +1 -1
- package/dist/plugin.test.js +219 -3
- package/dist/record.js +6 -98
- package/dist/registry-api.d.ts +2 -0
- package/dist/registry-api.js +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +1 -2
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +14 -4
- package/dist/snapshotFormatter.d.ts +7 -14
- package/dist/snapshotFormatter.js +38 -78
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +29 -0
- package/dist/validate.js +3 -5
- package/dist/yaml-schema.d.ts +26 -0
- package/dist/yaml-schema.js +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/dictionary.md +27 -0
- package/docs/adapters/browser/jd.md +27 -0
- package/docs/adapters/browser/linkedin.md +6 -0
- package/docs/adapters/browser/pixiv.md +92 -0
- package/docs/adapters/browser/web.md +30 -0
- package/docs/adapters/browser/xueqiu.md +27 -9
- package/docs/adapters/index.md +3 -1
- package/docs/comparison.md +125 -0
- package/docs/developer/contributing.md +21 -2
- package/docs/developer/testing.md +14 -8
- package/docs/developer/ts-adapter.md +18 -0
- package/docs/developer/yaml-adapter.md +16 -0
- package/docs/guide/plugins.md +10 -0
- package/docs/zh/guide/plugins.md +10 -0
- package/extension/dist/background.js +519 -444
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.test.ts +46 -1
- package/extension/src/background.ts +108 -33
- package/extension/src/cdp.ts +9 -9
- package/package.json +3 -2
- package/scripts/check-doc-coverage.sh +2 -0
- package/src/analysis.ts +170 -0
- package/src/browser/cdp.test.ts +66 -0
- package/src/browser/cdp.ts +59 -44
- package/src/browser/dom-snapshot.test.ts +42 -0
- package/src/browser/dom-snapshot.ts +56 -3
- package/src/browser/index.ts +2 -2
- package/src/browser/mcp.ts +2 -4
- package/src/browser/page.ts +34 -37
- package/src/browser/stealth.ts +24 -10
- package/src/browser.test.ts +2 -2
- package/src/build-manifest.test.ts +14 -0
- package/src/build-manifest.ts +13 -31
- package/src/cascade.ts +5 -3
- package/src/cli.ts +66 -34
- package/src/clis/_shared/desktop-commands.ts +121 -0
- package/src/clis/antigravity/serve.ts +6 -3
- package/src/clis/arxiv/search.ts +1 -1
- package/src/clis/bilibili/dynamic.test.ts +79 -0
- package/src/clis/bilibili/favorite.ts +5 -2
- package/src/clis/bilibili/following.ts +3 -2
- package/src/clis/bilibili/subtitle.ts +8 -7
- package/src/clis/bilibili/utils.ts +2 -2
- package/src/clis/boss/batchgreet.ts +1 -1
- package/src/clis/boss/chatlist.ts +1 -1
- package/src/clis/boss/chatmsg.ts +1 -1
- package/src/clis/boss/detail.ts +1 -1
- package/src/clis/boss/exchange.ts +1 -1
- package/src/clis/boss/greet.ts +1 -1
- package/src/clis/boss/invite.ts +1 -1
- package/src/clis/boss/joblist.ts +1 -1
- package/src/clis/boss/mark.ts +4 -3
- package/src/clis/boss/recommend.ts +1 -1
- package/src/clis/boss/resume.ts +1 -1
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +5 -4
- package/src/clis/boss/stats.ts +1 -1
- package/src/clis/chatgpt/ask.ts +5 -0
- package/src/clis/chatgpt/new.ts +7 -2
- package/src/clis/chatgpt/read.ts +7 -2
- package/src/clis/chatgpt/send.ts +3 -2
- package/src/clis/chatgpt/status.ts +6 -1
- package/src/clis/chatwise/ask.ts +7 -2
- package/src/clis/chatwise/export.ts +2 -0
- package/src/clis/chatwise/history.ts +2 -0
- package/src/clis/chatwise/model.ts +7 -3
- package/src/clis/chatwise/new.ts +3 -20
- package/src/clis/chatwise/read.ts +2 -0
- package/src/clis/chatwise/screenshot.ts +3 -32
- package/src/clis/chatwise/send.ts +7 -2
- package/src/clis/chatwise/shared.ts +8 -0
- package/src/clis/chatwise/status.ts +3 -24
- package/src/clis/codex/ask.ts +5 -2
- package/src/clis/codex/dump.ts +2 -27
- package/src/clis/codex/new.ts +2 -28
- package/src/clis/codex/screenshot.ts +2 -32
- package/src/clis/codex/send.ts +5 -4
- package/src/clis/codex/status.ts +2 -24
- package/src/clis/cursor/ask.ts +2 -1
- package/src/clis/cursor/composer.ts +2 -1
- package/src/clis/cursor/dump.ts +2 -27
- package/src/clis/cursor/new.ts +2 -20
- package/src/clis/cursor/read.ts +2 -1
- package/src/clis/cursor/screenshot.ts +1 -36
- package/src/clis/cursor/send.ts +2 -1
- package/src/clis/cursor/status.ts +2 -22
- package/src/clis/dictionary/examples.yaml +25 -0
- package/src/clis/dictionary/search.yaml +27 -0
- package/src/clis/dictionary/synonyms.yaml +25 -0
- package/src/clis/douban/book-hot.ts +1 -1
- package/src/clis/douban/movie-hot.ts +1 -1
- package/src/clis/douban/search.ts +1 -1
- package/src/clis/douban/utils.ts +165 -1
- package/src/clis/doubao/ask.ts +1 -1
- package/src/clis/doubao/new.ts +1 -1
- package/src/clis/doubao/read.ts +1 -1
- package/src/clis/doubao/send.ts +1 -1
- package/src/clis/doubao/status.ts +1 -1
- package/src/clis/doubao-app/ask.ts +1 -1
- package/src/clis/doubao-app/new.ts +1 -1
- package/src/clis/doubao-app/read.ts +1 -1
- package/src/clis/doubao-app/send.ts +1 -1
- package/src/clis/grok/ask.test.ts +25 -0
- package/src/clis/grok/ask.ts +25 -12
- package/src/clis/jd/item.test.ts +35 -0
- package/src/clis/jd/item.ts +101 -0
- package/src/clis/jike/feed.ts +1 -1
- package/src/clis/jike/search.ts +1 -1
- package/src/clis/linkedin/search.ts +5 -4
- package/src/clis/linkedin/timeline.test.ts +99 -0
- package/src/clis/linkedin/timeline.ts +532 -0
- package/src/clis/medium/feed.ts +1 -1
- package/src/clis/medium/search.ts +1 -1
- package/src/clis/medium/user.ts +1 -1
- package/src/clis/medium/{shared.ts → utils.ts} +2 -1
- package/src/clis/pixiv/detail.yaml +49 -0
- package/src/clis/pixiv/download.test.ts +114 -0
- package/src/clis/pixiv/download.ts +91 -0
- package/src/clis/pixiv/illusts.test.ts +115 -0
- package/src/clis/pixiv/illusts.ts +78 -0
- package/src/clis/pixiv/ranking.yaml +53 -0
- package/src/clis/pixiv/search.test.ts +97 -0
- package/src/clis/pixiv/search.ts +53 -0
- package/src/clis/pixiv/test-utils.ts +29 -0
- package/src/clis/pixiv/user.yaml +46 -0
- package/src/clis/pixiv/utils.ts +62 -0
- package/src/clis/reddit/comment.ts +2 -1
- package/src/clis/reddit/read.test.ts +34 -0
- package/src/clis/reddit/read.ts +4 -3
- package/src/clis/reddit/save.ts +2 -1
- package/src/clis/reddit/saved.ts +6 -2
- package/src/clis/reddit/subscribe.ts +2 -1
- package/src/clis/reddit/upvote.ts +2 -1
- package/src/clis/reddit/upvoted.ts +6 -2
- package/src/clis/sinablog/article.ts +1 -1
- package/src/clis/sinablog/hot.ts +1 -1
- package/src/clis/sinablog/user.ts +1 -1
- package/src/clis/substack/feed.ts +1 -1
- package/src/clis/substack/publication.ts +1 -1
- package/src/clis/substack/search.ts +3 -2
- package/src/clis/substack/{shared.ts → utils.ts} +3 -2
- package/src/clis/tiktok/search.yaml +2 -1
- package/src/clis/twitter/accept.ts +2 -1
- package/src/clis/twitter/article.ts +3 -1
- package/src/clis/twitter/block.ts +2 -1
- package/src/clis/twitter/bookmark.ts +2 -1
- package/src/clis/twitter/bookmarks.ts +3 -2
- package/src/clis/twitter/delete.ts +2 -1
- package/src/clis/twitter/follow.ts +2 -1
- package/src/clis/twitter/followers.ts +3 -2
- package/src/clis/twitter/following.ts +3 -2
- package/src/clis/twitter/hide-reply.ts +2 -1
- package/src/clis/twitter/like.ts +2 -1
- package/src/clis/twitter/notifications.ts +2 -1
- package/src/clis/twitter/post.ts +2 -1
- package/src/clis/twitter/profile.ts +4 -2
- package/src/clis/twitter/reply-dm.ts +2 -1
- package/src/clis/twitter/reply.ts +2 -1
- package/src/clis/twitter/search.test.ts +113 -0
- package/src/clis/twitter/search.ts +38 -14
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/timeline.ts +3 -2
- package/src/clis/twitter/trending.ts +3 -2
- package/src/clis/twitter/unblock.ts +2 -1
- package/src/clis/twitter/unbookmark.ts +2 -1
- package/src/clis/twitter/unfollow.ts +2 -1
- package/src/clis/v2ex/daily.ts +3 -2
- package/src/clis/v2ex/me.ts +3 -2
- package/src/clis/v2ex/notifications.ts +3 -4
- package/src/clis/web/read.ts +210 -0
- package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
- package/src/clis/xueqiu/danjuan-utils.ts +176 -0
- package/src/clis/xueqiu/fund-holdings.ts +32 -0
- package/src/clis/xueqiu/fund-snapshot.ts +27 -0
- package/src/clis/youtube/transcript.ts +5 -4
- package/src/clis/youtube/video.ts +3 -2
- package/src/daemon.ts +5 -4
- package/src/discovery.ts +12 -34
- package/src/doctor.ts +3 -2
- package/src/download/index.test.ts +93 -2
- package/src/download/index.ts +44 -23
- package/src/download/media-download.ts +5 -3
- package/src/engine.test.ts +84 -3
- package/src/execution.ts +62 -46
- package/src/explore.ts +21 -90
- package/src/external-clis.yaml +0 -8
- package/src/external.test.ts +9 -0
- package/src/external.ts +12 -10
- package/src/generate.ts +4 -41
- package/src/hooks.test.ts +126 -0
- package/src/hooks.ts +90 -0
- package/src/interceptor.ts +73 -23
- package/src/main.ts +2 -0
- package/src/output.ts +14 -6
- package/src/pipeline/executor.ts +1 -1
- package/src/pipeline/steps/browser.ts +1 -3
- package/src/pipeline/steps/download.test.ts +136 -0
- package/src/pipeline/steps/download.ts +47 -34
- package/src/pipeline/steps/fetch.test.ts +179 -0
- package/src/pipeline/steps/fetch.ts +39 -23
- package/src/pipeline/steps/transform.ts +2 -6
- package/src/pipeline/template.test.ts +28 -0
- package/src/pipeline/template.ts +67 -79
- package/src/pipeline/transform.test.ts +20 -0
- package/src/plugin.test.ts +251 -3
- package/src/plugin.ts +265 -21
- package/src/record.ts +12 -84
- package/src/registry-api.ts +2 -0
- package/src/registry.ts +7 -4
- package/src/runtime.ts +14 -4
- package/src/snapshotFormatter.ts +43 -121
- package/src/utils.ts +39 -0
- package/src/validate.ts +3 -5
- package/src/yaml-schema.ts +28 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
- package/tests/e2e/plugin-management.test.ts +137 -0
- package/tests/e2e/public-commands.test.ts +34 -1
- package/vitest.config.ts +19 -1
- package/.github/workflows/pkg-pr-new.yml +0 -30
- package/dist/clis/douban/shared.d.ts +0 -4
- package/dist/clis/douban/shared.js +0 -155
- package/src/clis/douban/shared.ts +0 -165
- /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/boss/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao/{common.js → utils.js} +0 -0
- /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
- /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/jike/{shared.js → utils.js} +0 -0
- /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
- /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
- /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
- /package/src/clis/boss/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
- /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
- /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
- /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
package/dist/plugin.d.ts
CHANGED
|
@@ -4,12 +4,37 @@
|
|
|
4
4
|
* Plugins live in ~/.opencli/plugins/<name>/.
|
|
5
5
|
* Install source format: "github:user/repo"
|
|
6
6
|
*/
|
|
7
|
+
/** Path to the lock file that tracks installed plugin versions. */
|
|
8
|
+
export declare function getLockFilePath(): string;
|
|
9
|
+
export declare const LOCK_FILE: string;
|
|
10
|
+
export interface LockEntry {
|
|
11
|
+
source: string;
|
|
12
|
+
commitHash: string;
|
|
13
|
+
installedAt: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
}
|
|
7
16
|
export interface PluginInfo {
|
|
8
17
|
name: string;
|
|
9
18
|
path: string;
|
|
10
19
|
commands: string[];
|
|
11
20
|
source?: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
installedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ValidationResult {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
errors: string[];
|
|
12
27
|
}
|
|
28
|
+
export declare function readLockFile(): Record<string, LockEntry>;
|
|
29
|
+
export declare function writeLockFile(lock: Record<string, LockEntry>): void;
|
|
30
|
+
/** Get the HEAD commit hash of a git repo directory. */
|
|
31
|
+
export declare function getCommitHash(dir: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Validate that a downloaded plugin directory is a structurally valid plugin.
|
|
34
|
+
* Checks for at least one command file (.yaml, .yml, .ts, .js) and a valid
|
|
35
|
+
* package.json if it contains .ts files.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validatePluginStructure(pluginDir: string): ValidationResult;
|
|
13
38
|
/**
|
|
14
39
|
* Install a plugin from a source.
|
|
15
40
|
* Currently supports "github:user/repo" format (git clone wrapper).
|
|
@@ -23,6 +48,16 @@ export declare function uninstallPlugin(name: string): void;
|
|
|
23
48
|
* Update a plugin by name (git pull + re-install lifecycle).
|
|
24
49
|
*/
|
|
25
50
|
export declare function updatePlugin(name: string): void;
|
|
51
|
+
export interface UpdateResult {
|
|
52
|
+
name: string;
|
|
53
|
+
success: boolean;
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Update all installed plugins.
|
|
58
|
+
* Continues even if individual plugin updates fail.
|
|
59
|
+
*/
|
|
60
|
+
export declare function updateAllPlugins(): UpdateResult[];
|
|
26
61
|
/**
|
|
27
62
|
* List all installed plugins.
|
|
28
63
|
*/
|
|
@@ -32,4 +67,8 @@ declare function parseSource(source: string): {
|
|
|
32
67
|
cloneUrl: string;
|
|
33
68
|
name: string;
|
|
34
69
|
} | null;
|
|
35
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the path to the esbuild CLI executable with fallback strategies.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveEsbuildBin(): string | null;
|
|
74
|
+
export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, parseSource as _parseSource, readLockFile as _readLockFile, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, };
|
package/dist/plugin.js
CHANGED
|
@@ -5,10 +5,88 @@
|
|
|
5
5
|
* Install source format: "github:user/repo"
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
8
9
|
import * as path from 'node:path';
|
|
9
10
|
import { execSync, execFileSync } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
10
12
|
import { PLUGINS_DIR } from './discovery.js';
|
|
13
|
+
import { getErrorMessage } from './errors.js';
|
|
11
14
|
import { log } from './logger.js';
|
|
15
|
+
const isWindows = process.platform === 'win32';
|
|
16
|
+
/** Get home directory, respecting HOME environment variable for test isolation. */
|
|
17
|
+
function getHomeDir() {
|
|
18
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
19
|
+
}
|
|
20
|
+
/** Path to the lock file that tracks installed plugin versions. */
|
|
21
|
+
export function getLockFilePath() {
|
|
22
|
+
return path.join(getHomeDir(), '.opencli', 'plugins.lock.json');
|
|
23
|
+
}
|
|
24
|
+
// Legacy const for backward compatibility (computed at load time)
|
|
25
|
+
export const LOCK_FILE = path.join(os.homedir(), '.opencli', 'plugins.lock.json');
|
|
26
|
+
// ── Lock file helpers ───────────────────────────────────────────────────────
|
|
27
|
+
export function readLockFile() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(getLockFilePath(), 'utf-8');
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeLockFile(lock) {
|
|
37
|
+
const lockPath = getLockFilePath();
|
|
38
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
39
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
40
|
+
}
|
|
41
|
+
/** Get the HEAD commit hash of a git repo directory. */
|
|
42
|
+
export function getCommitHash(dir) {
|
|
43
|
+
try {
|
|
44
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
45
|
+
cwd: dir,
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
}).trim();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate that a downloaded plugin directory is a structurally valid plugin.
|
|
56
|
+
* Checks for at least one command file (.yaml, .yml, .ts, .js) and a valid
|
|
57
|
+
* package.json if it contains .ts files.
|
|
58
|
+
*/
|
|
59
|
+
export function validatePluginStructure(pluginDir) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
if (!fs.existsSync(pluginDir)) {
|
|
62
|
+
return { valid: false, errors: ['Plugin directory does not exist'] };
|
|
63
|
+
}
|
|
64
|
+
const files = fs.readdirSync(pluginDir);
|
|
65
|
+
const hasYaml = files.some(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
66
|
+
const hasTs = files.some(f => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
|
|
67
|
+
const hasJs = files.some(f => f.endsWith('.js') && !f.endsWith('.d.js'));
|
|
68
|
+
if (!hasYaml && !hasTs && !hasJs) {
|
|
69
|
+
errors.push(`No command files found in plugin directory. A plugin must contain at least one .yaml, .ts, or .js command file.`);
|
|
70
|
+
}
|
|
71
|
+
if (hasTs) {
|
|
72
|
+
const pkgJsonPath = path.join(pluginDir, 'package.json');
|
|
73
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
74
|
+
errors.push(`Plugin contains .ts files but no package.json. A package.json with "type": "module" and "@jackwener/opencli" peer dependency is required for TS plugins.`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
try {
|
|
78
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
79
|
+
if (pkg.type !== 'module') {
|
|
80
|
+
errors.push(`Plugin package.json must have "type": "module" for TypeScript plugins.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
errors.push(`Plugin package.json is malformed or invalid JSON.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { valid: errors.length === 0, errors };
|
|
89
|
+
}
|
|
12
90
|
/**
|
|
13
91
|
* Shared post-install lifecycle: npm install → host symlink → TS transpile.
|
|
14
92
|
* Called by both installPlugin() and updatePlugin().
|
|
@@ -22,10 +100,11 @@ function postInstallLifecycle(pluginDir) {
|
|
|
22
100
|
cwd: pluginDir,
|
|
23
101
|
encoding: 'utf-8',
|
|
24
102
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
...(isWindows && { shell: true }),
|
|
25
104
|
});
|
|
26
105
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`[plugin] npm install failed in ${pluginDir}: ${err instanceof Error ? err.message : err}`);
|
|
29
108
|
}
|
|
30
109
|
// Symlink host opencli so TS plugins resolve '@jackwener/opencli/registry'
|
|
31
110
|
// against the running host, not a stale npm-published version.
|
|
@@ -59,9 +138,25 @@ export function installPlugin(source) {
|
|
|
59
138
|
});
|
|
60
139
|
}
|
|
61
140
|
catch (err) {
|
|
62
|
-
throw new Error(`Failed to clone plugin: ${err
|
|
141
|
+
throw new Error(`Failed to clone plugin: ${getErrorMessage(err)}`);
|
|
142
|
+
}
|
|
143
|
+
const validation = validatePluginStructure(targetDir);
|
|
144
|
+
if (!validation.valid) {
|
|
145
|
+
// If validation fails, clean up the cloned directory and abort
|
|
146
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
147
|
+
throw new Error(`Invalid plugin structure:\n- ${validation.errors.join('\n- ')}`);
|
|
63
148
|
}
|
|
64
149
|
postInstallLifecycle(targetDir);
|
|
150
|
+
const commitHash = getCommitHash(targetDir);
|
|
151
|
+
if (commitHash) {
|
|
152
|
+
const lock = readLockFile();
|
|
153
|
+
lock[name] = {
|
|
154
|
+
source: cloneUrl,
|
|
155
|
+
commitHash,
|
|
156
|
+
installedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
writeLockFile(lock);
|
|
159
|
+
}
|
|
65
160
|
return name;
|
|
66
161
|
}
|
|
67
162
|
/**
|
|
@@ -73,6 +168,11 @@ export function uninstallPlugin(name) {
|
|
|
73
168
|
throw new Error(`Plugin "${name}" is not installed.`);
|
|
74
169
|
}
|
|
75
170
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
171
|
+
const lock = readLockFile();
|
|
172
|
+
if (lock[name]) {
|
|
173
|
+
delete lock[name];
|
|
174
|
+
writeLockFile(lock);
|
|
175
|
+
}
|
|
76
176
|
}
|
|
77
177
|
/**
|
|
78
178
|
* Update a plugin by name (git pull + re-install lifecycle).
|
|
@@ -90,9 +190,44 @@ export function updatePlugin(name) {
|
|
|
90
190
|
});
|
|
91
191
|
}
|
|
92
192
|
catch (err) {
|
|
93
|
-
throw new Error(`Failed to update plugin: ${err
|
|
193
|
+
throw new Error(`Failed to update plugin: ${getErrorMessage(err)}`);
|
|
194
|
+
}
|
|
195
|
+
const validation = validatePluginStructure(targetDir);
|
|
196
|
+
if (!validation.valid) {
|
|
197
|
+
log.warn(`Plugin "${name}" updated, but structure is now invalid:\n- ${validation.errors.join('\n- ')}`);
|
|
94
198
|
}
|
|
95
199
|
postInstallLifecycle(targetDir);
|
|
200
|
+
const commitHash = getCommitHash(targetDir);
|
|
201
|
+
if (commitHash) {
|
|
202
|
+
const lock = readLockFile();
|
|
203
|
+
const existing = lock[name];
|
|
204
|
+
lock[name] = {
|
|
205
|
+
source: existing?.source ?? getPluginSource(targetDir) ?? '',
|
|
206
|
+
commitHash,
|
|
207
|
+
installedAt: existing?.installedAt ?? new Date().toISOString(),
|
|
208
|
+
updatedAt: new Date().toISOString(),
|
|
209
|
+
};
|
|
210
|
+
writeLockFile(lock);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Update all installed plugins.
|
|
215
|
+
* Continues even if individual plugin updates fail.
|
|
216
|
+
*/
|
|
217
|
+
export function updateAllPlugins() {
|
|
218
|
+
return listPlugins().map((plugin) => {
|
|
219
|
+
try {
|
|
220
|
+
updatePlugin(plugin.name);
|
|
221
|
+
return { name: plugin.name, success: true };
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
return {
|
|
225
|
+
name: plugin.name,
|
|
226
|
+
success: false,
|
|
227
|
+
error: getErrorMessage(err),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
96
231
|
}
|
|
97
232
|
/**
|
|
98
233
|
* List all installed plugins.
|
|
@@ -101,6 +236,7 @@ export function listPlugins() {
|
|
|
101
236
|
if (!fs.existsSync(PLUGINS_DIR))
|
|
102
237
|
return [];
|
|
103
238
|
const entries = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
|
239
|
+
const lock = readLockFile();
|
|
104
240
|
const plugins = [];
|
|
105
241
|
for (const entry of entries) {
|
|
106
242
|
if (!entry.isDirectory())
|
|
@@ -108,11 +244,14 @@ export function listPlugins() {
|
|
|
108
244
|
const pluginDir = path.join(PLUGINS_DIR, entry.name);
|
|
109
245
|
const commands = scanPluginCommands(pluginDir);
|
|
110
246
|
const source = getPluginSource(pluginDir);
|
|
247
|
+
const lockEntry = lock[entry.name];
|
|
111
248
|
plugins.push({
|
|
112
249
|
name: entry.name,
|
|
113
250
|
path: pluginDir,
|
|
114
251
|
commands,
|
|
115
252
|
source,
|
|
253
|
+
version: lockEntry?.commitHash?.slice(0, 7),
|
|
254
|
+
installedAt: lockEntry?.installedAt,
|
|
116
255
|
});
|
|
117
256
|
}
|
|
118
257
|
return plugins;
|
|
@@ -135,7 +274,7 @@ function scanPluginCommands(dir) {
|
|
|
135
274
|
/** Get git remote origin URL */
|
|
136
275
|
function getPluginSource(dir) {
|
|
137
276
|
try {
|
|
138
|
-
return
|
|
277
|
+
return execFileSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
139
278
|
cwd: dir,
|
|
140
279
|
encoding: 'utf-8',
|
|
141
280
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -179,7 +318,7 @@ function linkHostOpencli(pluginDir) {
|
|
|
179
318
|
// Determine the host opencli package root from this module's location.
|
|
180
319
|
// Both dev (tsx src/plugin.ts) and prod (node dist/plugin.js) are one level
|
|
181
320
|
// deep, so path.dirname + '..' always gives us the package root.
|
|
182
|
-
const thisFile =
|
|
321
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
183
322
|
const hostRoot = path.resolve(path.dirname(thisFile), '..');
|
|
184
323
|
const targetLink = path.join(pluginDir, 'node_modules', '@jackwener', 'opencli');
|
|
185
324
|
// Remove existing (npm-installed copy or stale symlink)
|
|
@@ -188,13 +327,73 @@ function linkHostOpencli(pluginDir) {
|
|
|
188
327
|
}
|
|
189
328
|
// Ensure parent directory exists
|
|
190
329
|
fs.mkdirSync(path.dirname(targetLink), { recursive: true });
|
|
191
|
-
//
|
|
192
|
-
|
|
330
|
+
// Use 'junction' on Windows (doesn't require admin privileges),
|
|
331
|
+
// 'dir' symlink on other platforms.
|
|
332
|
+
const linkType = isWindows ? 'junction' : 'dir';
|
|
333
|
+
fs.symlinkSync(hostRoot, targetLink, linkType);
|
|
193
334
|
log.debug(`Linked host opencli into plugin: ${targetLink} → ${hostRoot}`);
|
|
194
335
|
}
|
|
195
336
|
catch (err) {
|
|
196
|
-
log.warn(`Failed to link host opencli into plugin: ${err
|
|
337
|
+
log.warn(`Failed to link host opencli into plugin: ${getErrorMessage(err)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve the path to the esbuild CLI executable with fallback strategies.
|
|
342
|
+
*/
|
|
343
|
+
export function resolveEsbuildBin() {
|
|
344
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
345
|
+
const hostRoot = path.resolve(path.dirname(thisFile), '..');
|
|
346
|
+
// Strategy 1 (Windows): prefer the .cmd wrapper which is executable via shell
|
|
347
|
+
if (isWindows) {
|
|
348
|
+
const cmdPath = path.join(hostRoot, 'node_modules', '.bin', 'esbuild.cmd');
|
|
349
|
+
if (fs.existsSync(cmdPath)) {
|
|
350
|
+
return cmdPath;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Strategy 2: resolve esbuild binary via import.meta.resolve
|
|
354
|
+
// (On Unix, shebang scripts are directly executable; on Windows they are not,
|
|
355
|
+
// so this strategy is skipped on Windows in favour of the .cmd wrapper above.)
|
|
356
|
+
if (!isWindows) {
|
|
357
|
+
try {
|
|
358
|
+
const pkgUrl = import.meta.resolve('esbuild/package.json');
|
|
359
|
+
if (pkgUrl.startsWith('file://')) {
|
|
360
|
+
const pkgPath = fileURLToPath(pkgUrl);
|
|
361
|
+
const pkgRaw = fs.readFileSync(pkgPath, 'utf8');
|
|
362
|
+
const pkg = JSON.parse(pkgRaw);
|
|
363
|
+
if (pkg.bin && typeof pkg.bin === 'object' && pkg.bin.esbuild) {
|
|
364
|
+
const binPath = path.resolve(path.dirname(pkgPath), pkg.bin.esbuild);
|
|
365
|
+
if (fs.existsSync(binPath))
|
|
366
|
+
return binPath;
|
|
367
|
+
}
|
|
368
|
+
else if (typeof pkg.bin === 'string') {
|
|
369
|
+
const binPath = path.resolve(path.dirname(pkgPath), pkg.bin);
|
|
370
|
+
if (fs.existsSync(binPath))
|
|
371
|
+
return binPath;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// ignore package resolution failures
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Strategy 3: fallback to node_modules/.bin/esbuild (Unix)
|
|
380
|
+
const binFallback = path.join(hostRoot, 'node_modules', '.bin', 'esbuild');
|
|
381
|
+
if (fs.existsSync(binFallback)) {
|
|
382
|
+
return binFallback;
|
|
197
383
|
}
|
|
384
|
+
// Strategy 4: global esbuild in PATH
|
|
385
|
+
try {
|
|
386
|
+
const lookupCmd = isWindows ? 'where esbuild' : 'which esbuild';
|
|
387
|
+
// `where` on Windows may return multiple lines; take only the first match.
|
|
388
|
+
const globalBin = execSync(lookupCmd, { encoding: 'utf-8', stdio: 'pipe' }).trim().split('\n')[0].trim();
|
|
389
|
+
if (globalBin && fs.existsSync(globalBin)) {
|
|
390
|
+
return globalBin;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// ignore PATH lookup failures
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
198
397
|
}
|
|
199
398
|
/**
|
|
200
399
|
* Transpile TS plugin files to JS so they work in production mode.
|
|
@@ -202,12 +401,9 @@ function linkHostOpencli(pluginDir) {
|
|
|
202
401
|
*/
|
|
203
402
|
function transpilePluginTs(pluginDir) {
|
|
204
403
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const esbuildBin = path.join(hostRoot, 'node_modules', '.bin', 'esbuild');
|
|
209
|
-
if (!fs.existsSync(esbuildBin)) {
|
|
210
|
-
log.debug('esbuild not found in host node_modules, skipping TS transpilation');
|
|
404
|
+
const esbuildBin = resolveEsbuildBin();
|
|
405
|
+
if (!esbuildBin) {
|
|
406
|
+
log.debug('esbuild not found in host node_modules, via resolve, or in PATH, skipping TS transpilation');
|
|
211
407
|
return;
|
|
212
408
|
}
|
|
213
409
|
const files = fs.readdirSync(pluginDir);
|
|
@@ -223,11 +419,12 @@ function transpilePluginTs(pluginDir) {
|
|
|
223
419
|
cwd: pluginDir,
|
|
224
420
|
encoding: 'utf-8',
|
|
225
421
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
422
|
+
...(isWindows && { shell: true }),
|
|
226
423
|
});
|
|
227
424
|
log.debug(`Transpiled plugin file: ${tsFile} → ${jsFile}`);
|
|
228
425
|
}
|
|
229
426
|
catch (err) {
|
|
230
|
-
log.warn(`Failed to transpile ${tsFile}: ${err
|
|
427
|
+
log.warn(`Failed to transpile ${tsFile}: ${getErrorMessage(err)}`);
|
|
231
428
|
}
|
|
232
429
|
}
|
|
233
430
|
}
|
|
@@ -235,4 +432,4 @@ function transpilePluginTs(pluginDir) {
|
|
|
235
432
|
// Non-fatal: skip transpilation if anything goes wrong
|
|
236
433
|
}
|
|
237
434
|
}
|
|
238
|
-
export { parseSource as _parseSource };
|
|
435
|
+
export { resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, parseSource as _parseSource, readLockFile as _readLockFile, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, };
|
package/dist/plugin.test.d.ts
CHANGED
package/dist/plugin.test.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for plugin management: install, uninstall, list.
|
|
2
|
+
* Tests for plugin management: install, uninstall, list, and lock file support.
|
|
3
3
|
*/
|
|
4
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
|
+
import * as os from 'node:os';
|
|
6
7
|
import * as path from 'node:path';
|
|
7
8
|
import { PLUGINS_DIR } from './discovery.js';
|
|
8
|
-
import
|
|
9
|
+
import * as pluginModule from './plugin.js';
|
|
10
|
+
const { LOCK_FILE, _getCommitHash, listPlugins, _readLockFile, _resolveEsbuildBin, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, } = pluginModule;
|
|
9
11
|
describe('parseSource', () => {
|
|
10
12
|
it('parses github:user/repo format', () => {
|
|
11
13
|
const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
|
|
@@ -34,6 +36,131 @@ describe('parseSource', () => {
|
|
|
34
36
|
expect(_parseSource('npm:some-package')).toBeNull();
|
|
35
37
|
});
|
|
36
38
|
});
|
|
39
|
+
describe('validatePluginStructure', () => {
|
|
40
|
+
const testDir = path.join(PLUGINS_DIR, '__test-validate__');
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
try {
|
|
46
|
+
fs.rmSync(testDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
});
|
|
50
|
+
it('returns invalid for non-existent directory', () => {
|
|
51
|
+
const res = _validatePluginStructure(path.join(PLUGINS_DIR, '__does_not_exist__'));
|
|
52
|
+
expect(res.valid).toBe(false);
|
|
53
|
+
expect(res.errors[0]).toContain('does not exist');
|
|
54
|
+
});
|
|
55
|
+
it('returns invalid for empty directory', () => {
|
|
56
|
+
const res = _validatePluginStructure(testDir);
|
|
57
|
+
expect(res.valid).toBe(false);
|
|
58
|
+
expect(res.errors[0]).toContain('No command files found');
|
|
59
|
+
});
|
|
60
|
+
it('returns valid for YAML plugin', () => {
|
|
61
|
+
fs.writeFileSync(path.join(testDir, 'cmd.yaml'), 'site: test');
|
|
62
|
+
const res = _validatePluginStructure(testDir);
|
|
63
|
+
expect(res.valid).toBe(true);
|
|
64
|
+
expect(res.errors).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
it('returns valid for JS plugin', () => {
|
|
67
|
+
fs.writeFileSync(path.join(testDir, 'cmd.js'), 'console.log("hi");');
|
|
68
|
+
const res = _validatePluginStructure(testDir);
|
|
69
|
+
expect(res.valid).toBe(true);
|
|
70
|
+
expect(res.errors).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
it('returns invalid for TS plugin without package.json', () => {
|
|
73
|
+
fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
|
|
74
|
+
const res = _validatePluginStructure(testDir);
|
|
75
|
+
expect(res.valid).toBe(false);
|
|
76
|
+
expect(res.errors[0]).toContain('contains .ts files but no package.json');
|
|
77
|
+
});
|
|
78
|
+
it('returns invalid for TS plugin with missing type: module', () => {
|
|
79
|
+
fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
|
|
80
|
+
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
81
|
+
const res = _validatePluginStructure(testDir);
|
|
82
|
+
expect(res.valid).toBe(false);
|
|
83
|
+
expect(res.errors[0]).toContain('must have "type": "module"');
|
|
84
|
+
});
|
|
85
|
+
it('returns valid for TS plugin with correct package.json', () => {
|
|
86
|
+
fs.writeFileSync(path.join(testDir, 'cmd.ts'), 'console.log("hi");');
|
|
87
|
+
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
88
|
+
const res = _validatePluginStructure(testDir);
|
|
89
|
+
expect(res.valid).toBe(true);
|
|
90
|
+
expect(res.errors).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('lock file', () => {
|
|
94
|
+
const backupPath = `${LOCK_FILE}.test-backup`;
|
|
95
|
+
let hadOriginal = false;
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
hadOriginal = fs.existsSync(LOCK_FILE);
|
|
98
|
+
if (hadOriginal) {
|
|
99
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
100
|
+
fs.copyFileSync(LOCK_FILE, backupPath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
if (hadOriginal) {
|
|
105
|
+
fs.copyFileSync(backupPath, LOCK_FILE);
|
|
106
|
+
fs.unlinkSync(backupPath);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(LOCK_FILE);
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
});
|
|
114
|
+
it('reads empty lock when file does not exist', () => {
|
|
115
|
+
try {
|
|
116
|
+
fs.unlinkSync(LOCK_FILE);
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
expect(_readLockFile()).toEqual({});
|
|
120
|
+
});
|
|
121
|
+
it('round-trips lock entries', () => {
|
|
122
|
+
const entries = {
|
|
123
|
+
'test-plugin': {
|
|
124
|
+
source: 'https://github.com/user/repo.git',
|
|
125
|
+
commitHash: 'abc1234567890def',
|
|
126
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
127
|
+
},
|
|
128
|
+
'another-plugin': {
|
|
129
|
+
source: 'https://github.com/user/another.git',
|
|
130
|
+
commitHash: 'def4567890123abc',
|
|
131
|
+
installedAt: '2025-02-01T00:00:00.000Z',
|
|
132
|
+
updatedAt: '2025-03-01T00:00:00.000Z',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
_writeLockFile(entries);
|
|
136
|
+
expect(_readLockFile()).toEqual(entries);
|
|
137
|
+
});
|
|
138
|
+
it('handles malformed lock file gracefully', () => {
|
|
139
|
+
fs.mkdirSync(path.dirname(LOCK_FILE), { recursive: true });
|
|
140
|
+
fs.writeFileSync(LOCK_FILE, 'not valid json');
|
|
141
|
+
expect(_readLockFile()).toEqual({});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('getCommitHash', () => {
|
|
145
|
+
it('returns a hash for a git repo', () => {
|
|
146
|
+
const hash = _getCommitHash(process.cwd());
|
|
147
|
+
expect(hash).toBeDefined();
|
|
148
|
+
expect(hash).toMatch(/^[0-9a-f]{40}$/);
|
|
149
|
+
});
|
|
150
|
+
it('returns undefined for non-git directory', () => {
|
|
151
|
+
expect(_getCommitHash(os.tmpdir())).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('resolveEsbuildBin', () => {
|
|
155
|
+
it('resolves a usable esbuild executable path', () => {
|
|
156
|
+
const binPath = _resolveEsbuildBin();
|
|
157
|
+
expect(binPath).not.toBeNull();
|
|
158
|
+
expect(typeof binPath).toBe('string');
|
|
159
|
+
expect(fs.existsSync(binPath)).toBe(true);
|
|
160
|
+
// On Windows the resolved path ends with 'esbuild.cmd', on Unix 'esbuild'
|
|
161
|
+
expect(binPath).toMatch(/esbuild(\.cmd)?$/);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
37
164
|
describe('listPlugins', () => {
|
|
38
165
|
const testDir = path.join(PLUGINS_DIR, '__test-list-plugin__');
|
|
39
166
|
afterEach(() => {
|
|
@@ -50,6 +177,24 @@ describe('listPlugins', () => {
|
|
|
50
177
|
expect(found).toBeDefined();
|
|
51
178
|
expect(found.commands).toContain('hello');
|
|
52
179
|
});
|
|
180
|
+
it('includes version metadata from the lock file', () => {
|
|
181
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
182
|
+
fs.writeFileSync(path.join(testDir, 'hello.yaml'), 'site: test\nname: hello\n');
|
|
183
|
+
const lock = _readLockFile();
|
|
184
|
+
lock['__test-list-plugin__'] = {
|
|
185
|
+
source: 'https://github.com/user/repo.git',
|
|
186
|
+
commitHash: 'abcdef1234567890abcdef1234567890abcdef12',
|
|
187
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
188
|
+
};
|
|
189
|
+
_writeLockFile(lock);
|
|
190
|
+
const plugins = listPlugins();
|
|
191
|
+
const found = plugins.find(p => p.name === '__test-list-plugin__');
|
|
192
|
+
expect(found).toBeDefined();
|
|
193
|
+
expect(found.version).toBe('abcdef1');
|
|
194
|
+
expect(found.installedAt).toBe('2025-01-01T00:00:00.000Z');
|
|
195
|
+
delete lock['__test-list-plugin__'];
|
|
196
|
+
_writeLockFile(lock);
|
|
197
|
+
});
|
|
53
198
|
it('returns empty array when no plugins dir', () => {
|
|
54
199
|
// listPlugins should handle missing dir gracefully
|
|
55
200
|
const plugins = listPlugins();
|
|
@@ -70,6 +215,19 @@ describe('uninstallPlugin', () => {
|
|
|
70
215
|
uninstallPlugin('__test-uninstall__');
|
|
71
216
|
expect(fs.existsSync(testDir)).toBe(false);
|
|
72
217
|
});
|
|
218
|
+
it('removes lock entry on uninstall', () => {
|
|
219
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
220
|
+
fs.writeFileSync(path.join(testDir, 'test.yaml'), 'site: test');
|
|
221
|
+
const lock = _readLockFile();
|
|
222
|
+
lock['__test-uninstall__'] = {
|
|
223
|
+
source: 'https://github.com/user/repo.git',
|
|
224
|
+
commitHash: 'abc123',
|
|
225
|
+
installedAt: '2025-01-01T00:00:00.000Z',
|
|
226
|
+
};
|
|
227
|
+
_writeLockFile(lock);
|
|
228
|
+
uninstallPlugin('__test-uninstall__');
|
|
229
|
+
expect(_readLockFile()['__test-uninstall__']).toBeUndefined();
|
|
230
|
+
});
|
|
73
231
|
it('throws for non-existent plugin', () => {
|
|
74
232
|
expect(() => uninstallPlugin('__nonexistent__')).toThrow('not installed');
|
|
75
233
|
});
|
|
@@ -79,3 +237,61 @@ describe('updatePlugin', () => {
|
|
|
79
237
|
expect(() => updatePlugin('__nonexistent__')).toThrow('not installed');
|
|
80
238
|
});
|
|
81
239
|
});
|
|
240
|
+
vi.mock('node:child_process', () => {
|
|
241
|
+
return {
|
|
242
|
+
execFileSync: vi.fn((_cmd, args, opts) => {
|
|
243
|
+
if (Array.isArray(args) && args[0] === 'rev-parse' && args[1] === 'HEAD') {
|
|
244
|
+
if (opts?.cwd === os.tmpdir()) {
|
|
245
|
+
throw new Error('not a git repository');
|
|
246
|
+
}
|
|
247
|
+
return '1234567890abcdef1234567890abcdef12345678\n';
|
|
248
|
+
}
|
|
249
|
+
if (opts && opts.cwd && String(opts.cwd).endsWith('plugin-b')) {
|
|
250
|
+
throw new Error('Network error');
|
|
251
|
+
}
|
|
252
|
+
return '';
|
|
253
|
+
}),
|
|
254
|
+
execSync: vi.fn(() => ''),
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
describe('updateAllPlugins', () => {
|
|
258
|
+
const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
|
|
259
|
+
const testDirB = path.join(PLUGINS_DIR, 'plugin-b');
|
|
260
|
+
const testDirC = path.join(PLUGINS_DIR, 'plugin-c');
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
fs.mkdirSync(testDirA, { recursive: true });
|
|
263
|
+
fs.mkdirSync(testDirB, { recursive: true });
|
|
264
|
+
fs.mkdirSync(testDirC, { recursive: true });
|
|
265
|
+
fs.writeFileSync(path.join(testDirA, 'cmd.yaml'), 'site: a');
|
|
266
|
+
fs.writeFileSync(path.join(testDirB, 'cmd.yaml'), 'site: b');
|
|
267
|
+
fs.writeFileSync(path.join(testDirC, 'cmd.yaml'), 'site: c');
|
|
268
|
+
});
|
|
269
|
+
afterEach(() => {
|
|
270
|
+
try {
|
|
271
|
+
fs.rmSync(testDirA, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
catch { }
|
|
274
|
+
try {
|
|
275
|
+
fs.rmSync(testDirB, { recursive: true });
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
try {
|
|
279
|
+
fs.rmSync(testDirC, { recursive: true });
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
282
|
+
vi.clearAllMocks();
|
|
283
|
+
});
|
|
284
|
+
it('collects successes and failures without throwing', () => {
|
|
285
|
+
const results = _updateAllPlugins();
|
|
286
|
+
const resA = results.find(r => r.name === 'plugin-a');
|
|
287
|
+
const resB = results.find(r => r.name === 'plugin-b');
|
|
288
|
+
const resC = results.find(r => r.name === 'plugin-c');
|
|
289
|
+
expect(resA).toBeDefined();
|
|
290
|
+
expect(resA.success).toBe(true);
|
|
291
|
+
expect(resB).toBeDefined();
|
|
292
|
+
expect(resB.success).toBe(false);
|
|
293
|
+
expect(resB.error).toContain('Network error');
|
|
294
|
+
expect(resC).toBeDefined();
|
|
295
|
+
expect(resC.success).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|