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