@jackwener/opencli 1.5.3 → 1.5.4
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/.agents/skills/cross-project-adapter-migration/SKILL.md +3 -3
- package/README.md +213 -18
- package/dist/build-manifest.d.ts +2 -3
- package/dist/build-manifest.js +75 -170
- package/dist/build-manifest.test.js +113 -88
- package/dist/cli-manifest.json +1199 -1106
- package/dist/daemon.js +14 -3
- package/dist/external-clis.yaml +16 -0
- package/dist/serialization.js +6 -1
- package/dist/serialization.test.d.ts +1 -0
- package/dist/serialization.test.js +23 -0
- package/extension/dist/background.js +11 -4
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +19 -5
- package/extension/src/protocol.ts +2 -1
- package/package.json +1 -1
- package/src/build-manifest.test.ts +117 -88
- package/src/build-manifest.ts +81 -180
- package/src/daemon.ts +16 -4
- package/src/external-clis.yaml +16 -0
- package/src/serialization.test.ts +26 -0
- package/src/serialization.ts +6 -1
|
@@ -16,8 +16,8 @@ description: "Cross-project CLI command migration workflow for opencli. Use when
|
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
|
-
- 熟悉 [CLI-EXPLORER.md](
|
|
20
|
-
- 熟悉 [SKILL.md](
|
|
19
|
+
- 熟悉 [CLI-EXPLORER.md](../../../CLI-EXPLORER.md)(adapter 开发决策树)
|
|
20
|
+
- 熟悉 [SKILL.md](../../../SKILL.md)(命令参考 & 模板)
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -82,7 +82,7 @@ opencli list | grep <site> # 确认已注册命令
|
|
|
82
82
|
## Phase 3: 批量实现
|
|
83
83
|
|
|
84
84
|
> [!IMPORTANT]
|
|
85
|
-
> 实现前必须查阅 [CLI-EXPLORER.md](
|
|
85
|
+
> 实现前必须查阅 [CLI-EXPLORER.md](../../../CLI-EXPLORER.md) 确认策略选择。
|
|
86
86
|
|
|
87
87
|
### 3.1 选择实现方式
|
|
88
88
|
|
package/README.md
CHANGED
|
@@ -8,38 +8,233 @@
|
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
A CLI tool that turns **any website**, **Electron app**, or **local CLI tool** into a command-line interface — powered by browser session reuse and AI-native discovery.
|
|
11
|
+
A CLI tool that turns **any website**, **Electron app**, or **local CLI tool** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, Antigravity, `gh`, `docker`, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**Built for AI Agents** — Configure an instruction in your `AGENT.md` or `.cursorrules` to run `opencli list` via Bash. The AI will automatically discover and invoke all available tools.
|
|
14
|
+
|
|
15
|
+
**CLI Hub** — Register any local CLI (`opencli register mycli`) so AI agents can discover and call it alongside built-in commands. Auto-installs missing tools via your package manager (e.g. if `gh` isn't installed, `opencli gh ...` runs `brew install gh` first then re-executes seamlessly).
|
|
16
|
+
|
|
17
|
+
**CLI for Electron Apps** — Turn any Electron application into a CLI tool. Recombine, script, and extend apps like Antigravity Ultra from the terminal. AI agents can now control other AI apps natively.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Highlights
|
|
22
|
+
|
|
23
|
+
- **CLI All Electron** — CLI-ify apps like Antigravity Ultra! Now AI can control itself natively.
|
|
14
24
|
- **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser.
|
|
15
|
-
- **
|
|
25
|
+
- **Anti-detection built-in** — Patches `navigator.webdriver`, stubs `window.chrome`, fakes plugin lists, cleans ChromeDriver/Playwright globals, and strips CDP frames from Error stack traces. Extensive anti-fingerprinting and risk-control evasion measures baked in at every layer.
|
|
16
26
|
- **AI Agent ready** — `explore` discovers APIs, `synthesize` generates adapters, `cascade` finds auth strategies.
|
|
17
|
-
- **
|
|
27
|
+
- **External CLI Hub** — Discover, auto-install, and passthrough commands to any external CLI (gh, obsidian, docker, etc). Zero setup.
|
|
28
|
+
- **Self-healing setup** — `opencli doctor` diagnoses and auto-starts the daemon, extension, and live browser connectivity.
|
|
29
|
+
- **Dynamic Loader** — Simply drop `.ts` or `.yaml` adapters into the `clis/` folder for auto-registration.
|
|
30
|
+
- **Dual-Engine Architecture** — Supports both YAML declarative data pipelines and robust browser runtime TypeScript injections.
|
|
31
|
+
|
|
32
|
+
## Why opencli?
|
|
33
|
+
|
|
34
|
+
There are many great browser automation tools. Here's when opencli is the right choice:
|
|
35
|
+
|
|
36
|
+
| Your need | Best tool | Why |
|
|
37
|
+
|-----------|-----------|-----|
|
|
38
|
+
| Scheduled data extraction from specific sites | **opencli** | Pre-built adapters, deterministic JSON, zero LLM cost |
|
|
39
|
+
| AI agent needs reliable site operations | **opencli** | Hundreds of commands, structured output, fast deterministic response |
|
|
40
|
+
| Explore an unknown website ad-hoc | Browser-Use, Stagehand | LLM-driven general browsing for one-off tasks |
|
|
41
|
+
| Large-scale web crawling | Crawl4AI, Scrapy | Purpose-built for throughput and scale |
|
|
42
|
+
| Control desktop Electron apps from terminal | **opencli** | CDP + AppleScript — the only CLI tool that does this |
|
|
43
|
+
|
|
44
|
+
**What makes opencli different:**
|
|
45
|
+
|
|
46
|
+
- **Zero LLM cost** — No tokens consumed at runtime. Run 10,000 times and pay nothing.
|
|
47
|
+
- **Deterministic** — Same command, same output schema, every time. Pipeable, scriptable, CI-friendly.
|
|
48
|
+
- **Broad coverage** — 50+ sites across global and Chinese platforms (Bilibili, Zhihu, Xiaohongshu, Reddit, HackerNews, and more), plus desktop Electron apps via CDP.
|
|
49
|
+
|
|
50
|
+
> For a detailed comparison with Browser-Use, Crawl4AI, Firecrawl, and others, see the [Comparison Guide](./docs/comparison.md).
|
|
51
|
+
|
|
52
|
+
---
|
|
18
53
|
|
|
19
54
|
## Quick Start
|
|
20
55
|
|
|
56
|
+
### 1. Install Browser Bridge Extension
|
|
57
|
+
|
|
58
|
+
> OpenCLI connects to your browser through a lightweight **Browser Bridge** Chrome Extension + micro-daemon (zero config, auto-start).
|
|
59
|
+
|
|
60
|
+
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip`.
|
|
61
|
+
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
|
|
62
|
+
3. Click **Load unpacked** and select the unzipped folder.
|
|
63
|
+
|
|
64
|
+
### 2. Install OpenCLI
|
|
65
|
+
|
|
66
|
+
**Install via npm (recommended)**
|
|
67
|
+
|
|
21
68
|
```bash
|
|
22
69
|
npm install -g @jackwener/opencli
|
|
23
|
-
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Verify & Try
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
opencli doctor # Check extension + daemon connectivity
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Try it out:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
24
81
|
opencli list # See all commands
|
|
25
82
|
opencli hackernews top --limit 5 # Public API, no browser needed
|
|
26
|
-
opencli bilibili hot
|
|
83
|
+
opencli bilibili hot --limit 5 # Browser command (requires Extension)
|
|
27
84
|
```
|
|
28
85
|
|
|
29
|
-
|
|
86
|
+
### Update
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install -g @jackwener/opencli@latest
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### For Developers
|
|
95
|
+
|
|
96
|
+
**Install from source**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && npm run build && npm link
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Load Source Browser Bridge Extension**
|
|
103
|
+
|
|
104
|
+
1. Open `chrome://extensions` and enable **Developer mode** (top-right toggle).
|
|
105
|
+
2. Click **Load unpacked** and select the `extension/` directory from this repository.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Prerequisites
|
|
110
|
+
|
|
111
|
+
- **Node.js**: >= 20.0.0 (or **Bun** >= 1.0)
|
|
112
|
+
- **Chrome** running **and logged into the target site** (e.g. bilibili.com, zhihu.com, xiaohongshu.com).
|
|
113
|
+
|
|
114
|
+
> **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first.
|
|
115
|
+
|
|
116
|
+
## Built-in Commands
|
|
117
|
+
|
|
118
|
+
| Site | Commands |
|
|
119
|
+
|------|----------|
|
|
120
|
+
| **xiaohongshu** | `search` `feed` `user` `download` `publish` `comments` `notifications` `creator-notes` `creator-notes-summary` `creator-note-detail` `creator-profile` `creator-stats` |
|
|
121
|
+
| **bilibili** | `hot` `search` `history` `feed` `ranking` `download` `comments` `dynamic` `favorite` `following` `me` `subtitle` `user-videos` |
|
|
122
|
+
| **twitter** | `trending` `search` `timeline` `bookmarks` `post` `download` `profile` `article` `like` `likes` `notifications` `reply` `reply-dm` `thread` `follow` `unfollow` `followers` `following` `block` `unblock` `bookmark` `unbookmark` `delete` `hide-reply` `accept` |
|
|
123
|
+
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `user` `user-posts` `user-comments` `read` `save` `saved` `subscribe` `upvote` `upvoted` `comment` |
|
|
124
|
+
|
|
125
|
+
65+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
|
|
126
|
+
|
|
127
|
+
## CLI Hub
|
|
128
|
+
|
|
129
|
+
OpenCLI acts as a universal hub for your existing command-line tools — unified discovery, pure passthrough execution, and auto-install (if a tool isn't installed, OpenCLI runs `brew install <tool>` automatically before re-running the command).
|
|
130
|
+
|
|
131
|
+
| External CLI | Description | Example |
|
|
132
|
+
|--------------|-------------|---------|
|
|
133
|
+
| **gh** | GitHub CLI | `opencli gh pr list --limit 5` |
|
|
134
|
+
| **obsidian** | Obsidian vault management | `opencli obsidian search query="AI"` |
|
|
135
|
+
| **docker** | Docker | `opencli docker ps` |
|
|
136
|
+
| **gws** | Google Workspace CLI | `opencli gws docs list` |
|
|
137
|
+
| **lark-cli** | Lark/Feishu — messages, docs, calendar, tasks, 200+ commands | `opencli lark-cli calendar +agenda` |
|
|
138
|
+
| **vercel** | Vercel — deploy projects, manage domains, env vars, logs | `opencli vercel deploy --prod` |
|
|
139
|
+
|
|
140
|
+
**Register your own** — add any local CLI so AI agents can discover it via `opencli list`:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
opencli register mycli
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Desktop App Adapters
|
|
147
|
+
|
|
148
|
+
Control Electron desktop apps directly from the terminal. Each adapter has its own detailed documentation:
|
|
149
|
+
|
|
150
|
+
| App | Description | Doc |
|
|
151
|
+
|-----|-------------|-----|
|
|
152
|
+
| **Cursor** | Control Cursor IDE — Composer, chat, code extraction | [Doc](./docs/adapters/desktop/cursor.md) |
|
|
153
|
+
| **Codex** | Drive OpenAI Codex CLI agent headlessly | [Doc](./docs/adapters/desktop/codex.md) |
|
|
154
|
+
| **Antigravity** | Control Antigravity Ultra from terminal | [Doc](./docs/adapters/desktop/antigravity.md) |
|
|
155
|
+
| **ChatGPT** | Automate ChatGPT macOS desktop app | [Doc](./docs/adapters/desktop/chatgpt.md) |
|
|
156
|
+
| **ChatWise** | Multi-LLM client (GPT-4, Claude, Gemini) | [Doc](./docs/adapters/desktop/chatwise.md) |
|
|
157
|
+
| **Notion** | Search, read, write Notion pages | [Doc](./docs/adapters/desktop/notion.md) |
|
|
158
|
+
| **Discord** | Discord Desktop — messages, channels, servers | [Doc](./docs/adapters/desktop/discord.md) |
|
|
159
|
+
| **Doubao** | Control Doubao AI desktop app via CDP | [Doc](./docs/adapters/desktop/doubao-app.md) |
|
|
160
|
+
|
|
161
|
+
To add a new Electron app, start with [docs/guide/electron-app-cli.md](./docs/guide/electron-app-cli.md).
|
|
162
|
+
|
|
163
|
+
## Download Support
|
|
164
|
+
|
|
165
|
+
OpenCLI supports downloading images, videos, and articles from supported platforms.
|
|
166
|
+
|
|
167
|
+
| Platform | Content Types | Notes |
|
|
168
|
+
|----------|---------------|-------|
|
|
169
|
+
| **xiaohongshu** | Images, Videos | Downloads all media from a note |
|
|
170
|
+
| **bilibili** | Videos | Requires `yt-dlp` installed |
|
|
171
|
+
| **twitter** | Images, Videos | From user media tab or single tweet |
|
|
172
|
+
| **douban** | Images | Poster / still image lists |
|
|
173
|
+
| **pixiv** | Images | Original-quality illustrations, multi-page |
|
|
174
|
+
| **zhihu** | Articles (Markdown) | Exports with optional image download |
|
|
175
|
+
| **weixin** | Articles (Markdown) | WeChat Official Account articles |
|
|
176
|
+
|
|
177
|
+
For video downloads, install `yt-dlp` first: `brew install yt-dlp`
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
opencli xiaohongshu download abc123 --output ./xhs
|
|
181
|
+
opencli bilibili download BV1xxx --output ./bilibili
|
|
182
|
+
opencli twitter download elonmusk --limit 20 --output ./twitter
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Output Formats
|
|
186
|
+
|
|
187
|
+
All built-in commands support `--format` / `-f` with `table` (default), `json`, `yaml`, `md`, and `csv`.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
opencli bilibili hot -f json # Pipe to jq or LLMs
|
|
191
|
+
opencli bilibili hot -f csv # Spreadsheet-friendly
|
|
192
|
+
opencli bilibili hot -v # Verbose: show pipeline debug steps
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Plugins
|
|
196
|
+
|
|
197
|
+
Extend OpenCLI with community-contributed adapters:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
opencli plugin install github:user/opencli-plugin-my-tool
|
|
201
|
+
opencli plugin list
|
|
202
|
+
opencli plugin update --all
|
|
203
|
+
opencli plugin uninstall my-tool
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Plugin | Type | Description |
|
|
207
|
+
|--------|------|-------------|
|
|
208
|
+
| [opencli-plugin-github-trending](https://github.com/ByteYue/opencli-plugin-github-trending) | YAML | GitHub Trending repositories |
|
|
209
|
+
| [opencli-plugin-hot-digest](https://github.com/ByteYue/opencli-plugin-hot-digest) | TS | Multi-platform trending aggregator |
|
|
210
|
+
| [opencli-plugin-juejin](https://github.com/Astro-Han/opencli-plugin-juejin) | YAML | 稀土掘金 (Juejin) hot articles |
|
|
211
|
+
|
|
212
|
+
See [Plugins Guide](./docs/guide/plugins.md) for creating your own plugin.
|
|
213
|
+
|
|
214
|
+
## For AI Agents (Developer Guide)
|
|
215
|
+
|
|
216
|
+
> **Quick mode**: To generate a single command for a specific page URL, see [CLI-ONESHOT.md](./CLI-ONESHOT.md) — just a URL + one-line goal, 4 steps done.
|
|
217
|
+
|
|
218
|
+
> **Full mode**: Before writing any adapter code, read [CLI-EXPLORER.md](./CLI-EXPLORER.md). It contains the complete browser exploration workflow, the 5-tier authentication strategy decision tree, and debugging guide.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
opencli explore https://example.com --site mysite # Discover APIs + capabilities
|
|
222
|
+
opencli synthesize mysite # Generate YAML adapters
|
|
223
|
+
opencli generate https://example.com --goal "hot" # One-shot: explore → synthesize → register
|
|
224
|
+
opencli cascade https://api.example.com/data # Auto-probe: PUBLIC → COOKIE → HEADER
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Testing
|
|
228
|
+
|
|
229
|
+
See **[TESTING.md](./TESTING.md)** for how to run and write tests.
|
|
30
230
|
|
|
31
|
-
##
|
|
231
|
+
## Troubleshooting
|
|
32
232
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| Desktop App Adapters | [docs/adapters/desktop](./docs/adapters/desktop) |
|
|
39
|
-
| Plugins | [docs/guide/plugins.md](./docs/guide/plugins.md) |
|
|
40
|
-
| Electron App Guide | [docs/guide/electron-app-cli.md](./docs/guide/electron-app-cli.md) |
|
|
41
|
-
| Troubleshooting | [docs/guide/troubleshooting.md](./docs/guide/troubleshooting.md) |
|
|
42
|
-
| Comparison with other tools | [docs/comparison.md](./docs/comparison.md) |
|
|
233
|
+
- **"Extension not connected"** — Ensure the Browser Bridge extension is installed and **enabled** in `chrome://extensions`.
|
|
234
|
+
- **"attach failed: Cannot access a chrome-extension:// URL"** — Another extension may be interfering. Try disabling other extensions temporarily.
|
|
235
|
+
- **Empty data or 'Unauthorized' error** — Your Chrome login session may have expired. Navigate to the target site and log in again.
|
|
236
|
+
- **Node API errors** — Ensure Node.js >= 20. Some dependencies require modern Node APIs.
|
|
237
|
+
- **Daemon issues** — Check status: `curl localhost:19825/status` · View logs: `curl localhost:19825/logs`
|
|
43
238
|
|
|
44
239
|
## Star History
|
|
45
240
|
|
package/dist/build-manifest.d.ts
CHANGED
|
@@ -36,11 +36,10 @@ export interface ManifestEntry {
|
|
|
36
36
|
/** Pre-navigation control — see CliCommand.navigateBefore */
|
|
37
37
|
navigateBefore?: boolean | string;
|
|
38
38
|
}
|
|
39
|
-
export declare function
|
|
40
|
-
export declare function scanTs(filePath: string, site: string): ManifestEntry | null;
|
|
39
|
+
export declare function loadTsManifestEntries(filePath: string, site: string, importer?: (moduleHref: string) => Promise<unknown>): Promise<ManifestEntry[]>;
|
|
41
40
|
/**
|
|
42
41
|
* When both YAML and TS adapters exist for the same site/name,
|
|
43
42
|
* prefer the TS version (it self-registers and typically has richer logic).
|
|
44
43
|
*/
|
|
45
44
|
export declare function shouldReplaceManifestEntry(current: ManifestEntry, next: ManifestEntry): boolean;
|
|
46
|
-
export declare function buildManifest(): ManifestEntry[]
|
|
45
|
+
export declare function buildManifest(): Promise<ManifestEntry[]>;
|
package/dist/build-manifest.js
CHANGED
|
@@ -13,109 +13,52 @@ import * as path from 'node:path';
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
15
|
import { getErrorMessage } from './errors.js';
|
|
16
|
+
import { fullName, getRegistry } from './registry.js';
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const CLIS_DIR = path.resolve(__dirname, 'clis');
|
|
18
19
|
const OUTPUT = path.resolve(__dirname, '..', 'dist', 'cli-manifest.json');
|
|
19
20
|
import { parseYamlArgs } from './yaml-schema.js';
|
|
20
21
|
import { isRecord } from './utils.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (ch === '\\') {
|
|
33
|
-
escaped = true;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (ch === quote)
|
|
37
|
-
quote = null;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
41
|
-
quote = ch;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (ch === openChar) {
|
|
45
|
-
depth++;
|
|
46
|
-
}
|
|
47
|
-
else if (ch === closeChar) {
|
|
48
|
-
depth--;
|
|
49
|
-
if (depth === 0) {
|
|
50
|
-
return source.slice(startIndex + 1, i);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
22
|
+
const CLI_MODULE_PATTERN = /\bcli\s*\(/;
|
|
23
|
+
function toManifestArgs(args) {
|
|
24
|
+
return args.map(arg => ({
|
|
25
|
+
name: arg.name,
|
|
26
|
+
type: arg.type ?? 'str',
|
|
27
|
+
default: arg.default,
|
|
28
|
+
required: !!arg.required,
|
|
29
|
+
positional: arg.positional || undefined,
|
|
30
|
+
help: arg.help ?? '',
|
|
31
|
+
choices: arg.choices,
|
|
32
|
+
}));
|
|
55
33
|
}
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
return null;
|
|
60
|
-
const bracketIndex = source.indexOf('[', argsMatch.index);
|
|
61
|
-
if (bracketIndex === -1)
|
|
62
|
-
return null;
|
|
63
|
-
return extractBalancedBlock(source, bracketIndex, '[', ']');
|
|
34
|
+
function toTsModulePath(filePath, site) {
|
|
35
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
36
|
+
return `${site}/${baseName}.js`;
|
|
64
37
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
.map(s => s.trim().replace(/^['"`]|['"`]$/g, ''))
|
|
72
|
-
.filter(Boolean);
|
|
73
|
-
return values.length > 0 ? values : undefined;
|
|
38
|
+
function isCliCommandValue(value, site) {
|
|
39
|
+
return isRecord(value)
|
|
40
|
+
&& typeof value.site === 'string'
|
|
41
|
+
&& value.site === site
|
|
42
|
+
&& typeof value.name === 'string'
|
|
43
|
+
&& Array.isArray(value.args);
|
|
74
44
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (defaultMatch) {
|
|
93
|
-
const raw = defaultMatch[1].trim();
|
|
94
|
-
if (raw === 'true')
|
|
95
|
-
defaultVal = true;
|
|
96
|
-
else if (raw === 'false')
|
|
97
|
-
defaultVal = false;
|
|
98
|
-
else if (/^\d+$/.test(raw))
|
|
99
|
-
defaultVal = parseInt(raw, 10);
|
|
100
|
-
else if (/^\d+\.\d+$/.test(raw))
|
|
101
|
-
defaultVal = parseFloat(raw);
|
|
102
|
-
else
|
|
103
|
-
defaultVal = raw.replace(/^['"`]|['"`]$/g, '');
|
|
104
|
-
}
|
|
105
|
-
args.push({
|
|
106
|
-
name: nameMatch[1],
|
|
107
|
-
type: typeMatch?.[1] ?? 'str',
|
|
108
|
-
default: defaultVal,
|
|
109
|
-
required: requiredMatch?.[1] === 'true',
|
|
110
|
-
positional: positionalMatch?.[1] === 'true' || undefined,
|
|
111
|
-
help: helpMatch?.[1] ?? '',
|
|
112
|
-
choices: parseInlineChoices(body),
|
|
113
|
-
});
|
|
114
|
-
cursor = objectStart + body.length;
|
|
115
|
-
if (cursor <= objectStart)
|
|
116
|
-
break; // safety: prevent infinite loop
|
|
117
|
-
}
|
|
118
|
-
return args;
|
|
45
|
+
function toManifestEntry(cmd, modulePath) {
|
|
46
|
+
return {
|
|
47
|
+
site: cmd.site,
|
|
48
|
+
name: cmd.name,
|
|
49
|
+
description: cmd.description ?? '',
|
|
50
|
+
domain: cmd.domain,
|
|
51
|
+
strategy: (cmd.strategy ?? 'public').toString().toLowerCase(),
|
|
52
|
+
browser: cmd.browser ?? true,
|
|
53
|
+
args: toManifestArgs(cmd.args),
|
|
54
|
+
columns: cmd.columns,
|
|
55
|
+
timeout: cmd.timeoutSeconds,
|
|
56
|
+
deprecated: cmd.deprecated,
|
|
57
|
+
replacedBy: cmd.replacedBy,
|
|
58
|
+
type: 'ts',
|
|
59
|
+
modulePath,
|
|
60
|
+
navigateBefore: cmd.navigateBefore,
|
|
61
|
+
};
|
|
119
62
|
}
|
|
120
63
|
function scanYaml(filePath, site) {
|
|
121
64
|
try {
|
|
@@ -150,82 +93,44 @@ function scanYaml(filePath, site) {
|
|
|
150
93
|
return null;
|
|
151
94
|
}
|
|
152
95
|
}
|
|
153
|
-
export function
|
|
154
|
-
// TS adapters self-register via cli() at import time.
|
|
155
|
-
// We statically parse the source to extract metadata for the manifest stub.
|
|
156
|
-
const baseName = path.basename(filePath, path.extname(filePath));
|
|
157
|
-
const relativePath = `${site}/${baseName}.js`;
|
|
96
|
+
export async function loadTsManifestEntries(filePath, site, importer = moduleHref => import(moduleHref)) {
|
|
158
97
|
try {
|
|
159
98
|
const src = fs.readFileSync(filePath, 'utf-8');
|
|
160
99
|
// Helper/test modules should not appear as CLI commands in the manifest.
|
|
161
|
-
if (
|
|
162
|
-
return
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
entry.browser = entry.strategy !== 'public';
|
|
191
|
-
// Extract columns
|
|
192
|
-
const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
|
|
193
|
-
if (colMatch) {
|
|
194
|
-
entry.columns = colMatch[1].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean);
|
|
195
|
-
}
|
|
196
|
-
// Extract args array items: { name: '...', ... }
|
|
197
|
-
const argsBlock = extractTsArgsBlock(src);
|
|
198
|
-
if (argsBlock) {
|
|
199
|
-
entry.args = parseTsArgsBlock(argsBlock);
|
|
200
|
-
}
|
|
201
|
-
// Extract navigateBefore: false / true / 'https://...'
|
|
202
|
-
const navBoolMatch = src.match(/navigateBefore\s*:\s*(true|false)/);
|
|
203
|
-
if (navBoolMatch) {
|
|
204
|
-
entry.navigateBefore = navBoolMatch[1] === 'true';
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
const navStringMatch = src.match(/navigateBefore\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
208
|
-
if (navStringMatch)
|
|
209
|
-
entry.navigateBefore = navStringMatch[1];
|
|
210
|
-
}
|
|
211
|
-
const deprecatedBoolMatch = src.match(/deprecated\s*:\s*(true|false)/);
|
|
212
|
-
if (deprecatedBoolMatch) {
|
|
213
|
-
entry.deprecated = deprecatedBoolMatch[1] === 'true';
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
const deprecatedStringMatch = src.match(/deprecated\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
217
|
-
if (deprecatedStringMatch)
|
|
218
|
-
entry.deprecated = deprecatedStringMatch[1];
|
|
219
|
-
}
|
|
220
|
-
const replacedByMatch = src.match(/replacedBy\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
221
|
-
if (replacedByMatch)
|
|
222
|
-
entry.replacedBy = replacedByMatch[1];
|
|
223
|
-
return entry;
|
|
100
|
+
if (!CLI_MODULE_PATTERN.test(src))
|
|
101
|
+
return [];
|
|
102
|
+
const modulePath = toTsModulePath(filePath, site);
|
|
103
|
+
const registry = getRegistry();
|
|
104
|
+
const before = new Map(registry.entries());
|
|
105
|
+
const mod = await importer(pathToFileURL(filePath).href);
|
|
106
|
+
const exportedCommands = Object.values(isRecord(mod) ? mod : {})
|
|
107
|
+
.filter(value => isCliCommandValue(value, site));
|
|
108
|
+
const runtimeCommands = exportedCommands.length > 0
|
|
109
|
+
? exportedCommands
|
|
110
|
+
: [...registry.entries()]
|
|
111
|
+
.filter(([key, cmd]) => {
|
|
112
|
+
if (cmd.site !== site)
|
|
113
|
+
return false;
|
|
114
|
+
const previous = before.get(key);
|
|
115
|
+
return !previous || previous !== cmd;
|
|
116
|
+
})
|
|
117
|
+
.map(([, cmd]) => cmd);
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
return runtimeCommands
|
|
120
|
+
.filter((cmd) => {
|
|
121
|
+
const key = fullName(cmd);
|
|
122
|
+
if (seen.has(key))
|
|
123
|
+
return false;
|
|
124
|
+
seen.add(key);
|
|
125
|
+
return true;
|
|
126
|
+
})
|
|
127
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
128
|
+
.map(cmd => toManifestEntry(cmd, modulePath));
|
|
224
129
|
}
|
|
225
130
|
catch (err) {
|
|
226
131
|
// If parsing fails, log a warning (matching scanYaml behaviour) and skip the entry.
|
|
227
132
|
process.stderr.write(`Warning: failed to scan ${filePath}: ${getErrorMessage(err)}\n`);
|
|
228
|
-
return
|
|
133
|
+
return [];
|
|
229
134
|
}
|
|
230
135
|
}
|
|
231
136
|
/**
|
|
@@ -237,7 +142,7 @@ export function shouldReplaceManifestEntry(current, next) {
|
|
|
237
142
|
return false;
|
|
238
143
|
return current.type === 'yaml' && next.type === 'ts';
|
|
239
144
|
}
|
|
240
|
-
export function buildManifest() {
|
|
145
|
+
export async function buildManifest() {
|
|
241
146
|
const manifest = new Map();
|
|
242
147
|
if (fs.existsSync(CLIS_DIR)) {
|
|
243
148
|
for (const site of fs.readdirSync(CLIS_DIR)) {
|
|
@@ -261,8 +166,8 @@ export function buildManifest() {
|
|
|
261
166
|
}
|
|
262
167
|
else if ((file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts') && file !== 'index.ts') ||
|
|
263
168
|
(file.endsWith('.js') && !file.endsWith('.d.js') && !file.endsWith('.test.js') && file !== 'index.js')) {
|
|
264
|
-
const
|
|
265
|
-
|
|
169
|
+
const entries = await loadTsManifestEntries(filePath, site);
|
|
170
|
+
for (const entry of entries) {
|
|
266
171
|
const key = `${entry.site}/${entry.name}`;
|
|
267
172
|
const existing = manifest.get(key);
|
|
268
173
|
if (!existing || shouldReplaceManifestEntry(existing, entry)) {
|
|
@@ -278,8 +183,8 @@ export function buildManifest() {
|
|
|
278
183
|
}
|
|
279
184
|
return [...manifest.values()];
|
|
280
185
|
}
|
|
281
|
-
function main() {
|
|
282
|
-
const manifest = buildManifest();
|
|
186
|
+
async function main() {
|
|
187
|
+
const manifest = await buildManifest();
|
|
283
188
|
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
284
189
|
fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2));
|
|
285
190
|
const yamlCount = manifest.filter(e => e.type === 'yaml').length;
|
|
@@ -311,5 +216,5 @@ function main() {
|
|
|
311
216
|
}
|
|
312
217
|
const entrypoint = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
|
|
313
218
|
if (entrypoint === import.meta.url) {
|
|
314
|
-
main();
|
|
219
|
+
void main();
|
|
315
220
|
}
|