@huyuan-ai/cli 1.3.0 → 1.3.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/UPGRADE.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  本文档说明各版本相对上一版本的重要变更,以及从旧版升级时的注意点。
4
4
 
5
+ ## 1.3.1
6
+
7
+ ### 修复
8
+
9
+ - **全局安装后 `huyuan-opencli` 首次运行**:补齐随包发布的 `vendor/opencli/scripts/` 与 `vendor/opencli/package.json`,使 OpenCLI 首次运行时的 `fetch-adapters` 能正确解析路径并读取版本(不再出现 `Cannot find module .../fetch-adapters.js`)。
10
+
5
11
  ## 1.3.0
6
12
 
7
13
  ### 新增
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huyuan-ai/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -29,6 +29,8 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "vendor/opencli/dist",
32
+ "vendor/opencli/package.json",
33
+ "vendor/opencli/scripts",
32
34
  "README.md",
33
35
  "UPGRADE.md"
34
36
  ],
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@jackwener/opencli",
3
+ "version": "1.6.7",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Make any website or Electron App your CLI. AI-powered.",
8
+ "engines": {
9
+ "node": ">=20.0.0"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/src/main.js",
13
+ "bin": {
14
+ "opencli": "dist/src/main.js"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/src/main.js",
18
+ "./registry": "./dist/src/registry-api.js",
19
+ "./errors": "./dist/src/errors.js",
20
+ "./types": "./dist/src/types.js",
21
+ "./utils": "./dist/src/utils.js",
22
+ "./logger": "./dist/src/logger.js",
23
+ "./launcher": "./dist/src/launcher.js",
24
+ "./browser/cdp": "./dist/src/browser/cdp.js",
25
+ "./browser/page": "./dist/src/browser/page.js",
26
+ "./browser/utils": "./dist/src/browser/utils.js",
27
+ "./download": "./dist/src/download/index.js",
28
+ "./download/article-download": "./dist/src/download/article-download.js",
29
+ "./download/media-download": "./dist/src/download/media-download.js",
30
+ "./download/progress": "./dist/src/download/progress.js",
31
+ "./pipeline": "./dist/src/pipeline/index.js"
32
+ },
33
+ "files": [
34
+ "dist/src/",
35
+ "dist/clis/",
36
+ "dist/cli-manifest.json",
37
+ "scripts/",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "scripts": {
42
+ "dev": "tsx src/main.ts",
43
+ "dev:bun": "bun src/main.ts",
44
+ "build": "npm run clean-dist && tsc && npm run clean-yaml && npm run copy-yaml && npm run build-manifest",
45
+ "build-manifest": "node dist/src/build-manifest.js",
46
+ "clean-dist": "node scripts/clean-dist.cjs",
47
+ "clean-yaml": "node scripts/clean-yaml.cjs",
48
+ "copy-yaml": "node scripts/copy-yaml.cjs",
49
+ "start": "node dist/src/main.js",
50
+ "start:bun": "bun dist/src/main.js",
51
+ "postinstall": "node scripts/postinstall.js || true; node scripts/fetch-adapters.js || true",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "tsc --noEmit",
54
+ "prepare": "[ -d src ] && npm run build || true",
55
+ "prepublishOnly": "npm run build",
56
+ "test": "vitest run --project unit",
57
+ "test:bun": "bun vitest run --project unit",
58
+ "test:adapter": "vitest run --project adapter",
59
+ "test:all": "vitest run",
60
+ "test:e2e": "vitest run --project e2e",
61
+ "docs:dev": "vitepress dev docs",
62
+ "docs:build": "vitepress build docs",
63
+ "docs:preview": "vitepress preview docs"
64
+ },
65
+ "keywords": [
66
+ "cli",
67
+ "browser",
68
+ "web",
69
+ "ai"
70
+ ],
71
+ "author": "jackwener",
72
+ "license": "Apache-2.0",
73
+ "repository": {
74
+ "type": "git",
75
+ "url": "git+https://github.com/jackwener/opencli.git"
76
+ },
77
+ "dependencies": {
78
+ "chalk": "^5.3.0",
79
+ "cli-table3": "^0.6.5",
80
+ "commander": "^14.0.3",
81
+ "js-yaml": "^4.1.0",
82
+ "turndown": "^7.2.2",
83
+ "undici": "^7.24.6",
84
+ "ws": "^8.18.0"
85
+ },
86
+ "devDependencies": {
87
+ "@types/js-yaml": "^4.0.9",
88
+ "@types/node": "^22.13.10",
89
+ "@types/turndown": "^5.0.6",
90
+ "@types/ws": "^8.5.13",
91
+ "tsx": "^4.19.3",
92
+ "typescript": "^6.0.2",
93
+ "vitepress": "^1.6.4",
94
+ "vitest": "^4.1.0"
95
+ }
96
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # check-doc-coverage.sh — Verify every adapter in clis/ has a doc page.
3
+ #
4
+ # Exit codes:
5
+ # 0 — all adapters have docs
6
+ # 1 — at least one adapter is missing documentation
7
+ #
8
+ # Usage:
9
+ # bash scripts/check-doc-coverage.sh # report only
10
+ # bash scripts/check-doc-coverage.sh --strict # exit 1 on missing docs
11
+
12
+ set -euo pipefail
13
+
14
+ STRICT=false
15
+ if [[ "${1:-}" == "--strict" ]]; then
16
+ STRICT=true
17
+ fi
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
21
+
22
+ SRC_DIR="$ROOT_DIR/clis"
23
+ DOCS_DIR="$ROOT_DIR/docs/adapters"
24
+
25
+ missing=()
26
+ covered=0
27
+ total=0
28
+
29
+ for adapter_dir in "$SRC_DIR"/*/; do
30
+ adapter_name="$(basename "$adapter_dir")"
31
+ # Skip internal directories (e.g., _shared)
32
+ [[ "$adapter_name" == _* ]] && continue
33
+ total=$((total + 1))
34
+
35
+ # Check if doc exists in browser/ or desktop/ subdirectories
36
+ if [[ -f "$DOCS_DIR/browser/$adapter_name.md" ]] || \
37
+ [[ -f "$DOCS_DIR/desktop/$adapter_name.md" ]]; then
38
+ covered=$((covered + 1))
39
+ else
40
+ # Handle directory name mismatches (e.g., discord-app -> discord)
41
+ alt_name="${adapter_name%-app}"
42
+ if [[ "$alt_name" != "$adapter_name" ]] && \
43
+ { [[ -f "$DOCS_DIR/browser/$alt_name.md" ]] || \
44
+ [[ -f "$DOCS_DIR/desktop/$alt_name.md" ]]; }; then
45
+ covered=$((covered + 1))
46
+ else
47
+ missing+=("$adapter_name")
48
+ fi
49
+ fi
50
+ done
51
+
52
+ echo "📊 Doc Coverage: $covered/$total adapters documented"
53
+ echo ""
54
+
55
+ if [[ ${#missing[@]} -gt 0 ]]; then
56
+ echo "⚠️ Missing docs for ${#missing[@]} adapter(s):"
57
+ for name in "${missing[@]}"; do
58
+ echo " - $name → create docs/adapters/browser/$name.md or docs/adapters/desktop/$name.md"
59
+ done
60
+ echo ""
61
+ if $STRICT; then
62
+ echo "❌ Doc check failed (--strict mode)."
63
+ exit 1
64
+ else
65
+ echo "💡 Run with --strict to fail CI on missing docs."
66
+ exit 0
67
+ fi
68
+ else
69
+ echo "✅ All adapters have documentation."
70
+ exit 0
71
+ fi
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Remove dist/ before a fresh build so deleted adapters do not leave stale
3
+ * compiled files behind in dist/clis/.
4
+ */
5
+ const { existsSync, rmSync } = require('fs');
6
+
7
+ if (existsSync('dist')) {
8
+ rmSync('dist', { recursive: true, force: true });
9
+ }
10
+
11
+ if (existsSync('tsconfig.tsbuildinfo')) {
12
+ rmSync('tsconfig.tsbuildinfo', { force: true });
13
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Clean YAML files from dist/clis/ before copying fresh ones.
3
+ */
4
+ const { readdirSync, rmSync, existsSync, statSync } = require('fs');
5
+ const path = require('path');
6
+
7
+ function walk(dir) {
8
+ if (!existsSync(dir)) return;
9
+ for (const f of readdirSync(dir)) {
10
+ const fp = path.join(dir, f);
11
+ if (statSync(fp).isDirectory()) {
12
+ walk(fp);
13
+ } else if (/\.ya?ml$/.test(f)) {
14
+ rmSync(fp);
15
+ }
16
+ }
17
+ }
18
+
19
+ walk('dist/clis');
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copy YAML files from clis/ to dist/clis/.
3
+ */
4
+ const { readdirSync, copyFileSync, mkdirSync, existsSync, statSync } = require('fs');
5
+ const path = require('path');
6
+
7
+ function walk(src, dst) {
8
+ if (!existsSync(src)) return;
9
+ for (const f of readdirSync(src)) {
10
+ const sp = path.join(src, f);
11
+ const dp = path.join(dst, f);
12
+ if (statSync(sp).isDirectory()) {
13
+ walk(sp, dp);
14
+ } else if (/\.ya?ml$/.test(f)) {
15
+ mkdirSync(path.dirname(dp), { recursive: true });
16
+ copyFileSync(sp, dp);
17
+ }
18
+ }
19
+ }
20
+
21
+ walk('clis', 'dist/clis');
22
+
23
+ // Copy external CLI registry to dist/
24
+ const extSrc = 'src/external-clis.yaml';
25
+ if (existsSync(extSrc)) {
26
+ mkdirSync('dist/src', { recursive: true });
27
+ copyFileSync(extSrc, 'dist/src/external-clis.yaml');
28
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Copy official CLI adapters from the installed package to ~/.opencli/clis/.
5
+ *
6
+ * Update strategy (file-level granularity via adapter-manifest.json):
7
+ * - Official files (in new manifest) are unconditionally overwritten
8
+ * - Removed official files (in old manifest but not new) are cleaned up
9
+ * - User-created files (never in any manifest) are preserved
10
+ * - Skips if already installed at the same version
11
+ *
12
+ * Only runs on global install (npm install -g) or explicit OPENCLI_FETCH=1.
13
+ * No network calls — copies directly from dist/clis/ in the installed package.
14
+ *
15
+ * This is an ESM script (package.json type: module). No TypeScript, no src/ imports.
16
+ */
17
+
18
+ import { existsSync, mkdirSync, rmSync, cpSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
19
+ import { join, resolve, dirname } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+
22
+ const OPENCLI_DIR = join(homedir(), '.opencli');
23
+ const USER_CLIS_DIR = join(OPENCLI_DIR, 'clis');
24
+ const MANIFEST_PATH = join(OPENCLI_DIR, 'adapter-manifest.json');
25
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
26
+ const BUILTIN_CLIS = join(PACKAGE_ROOT, 'dist', 'clis');
27
+
28
+ function log(msg) {
29
+ console.log(`[opencli] ${msg}`);
30
+ }
31
+
32
+ function getPackageVersion() {
33
+ try {
34
+ return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8')).version;
35
+ } catch {
36
+ return 'unknown';
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Read existing manifest. Returns { version, files } or null.
42
+ */
43
+ function readManifest() {
44
+ try {
45
+ return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Collect all relative file paths under a directory.
53
+ */
54
+ function walkFiles(dir, prefix = '') {
55
+ const results = [];
56
+ if (!existsSync(dir)) return results;
57
+ for (const entry of readdirSync(dir)) {
58
+ const full = join(dir, entry);
59
+ const rel = prefix ? `${prefix}/${entry}` : entry;
60
+ if (statSync(full).isDirectory()) {
61
+ results.push(...walkFiles(full, rel));
62
+ } else {
63
+ results.push(rel);
64
+ }
65
+ }
66
+ return results;
67
+ }
68
+
69
+ /**
70
+ * Remove empty parent directories up to (but not including) stopAt.
71
+ */
72
+ function pruneEmptyDirs(filePath, stopAt) {
73
+ let dir = dirname(filePath);
74
+ while (dir !== stopAt && dir.startsWith(stopAt)) {
75
+ try {
76
+ const entries = readdirSync(dir);
77
+ if (entries.length > 0) break;
78
+ rmSync(dir);
79
+ dir = dirname(dir);
80
+ } catch {
81
+ break;
82
+ }
83
+ }
84
+ }
85
+
86
+ export function fetchAdapters() {
87
+ const currentVersion = getPackageVersion();
88
+ const oldManifest = readManifest();
89
+
90
+ // Skip if already installed at the same version (unless forced via OPENCLI_FETCH=1)
91
+ const isForced = process.env.OPENCLI_FETCH === '1';
92
+ if (!isForced && currentVersion !== 'unknown' && oldManifest?.version === currentVersion) {
93
+ log(`Adapters already up to date (v${currentVersion})`);
94
+ return;
95
+ }
96
+
97
+ if (!existsSync(BUILTIN_CLIS)) {
98
+ log('Warning: dist/clis/ not found in package — skipping adapter copy');
99
+ return;
100
+ }
101
+
102
+ const newOfficialFiles = new Set(walkFiles(BUILTIN_CLIS));
103
+ const oldOfficialFiles = new Set(oldManifest?.files ?? []);
104
+ mkdirSync(USER_CLIS_DIR, { recursive: true });
105
+
106
+ // 1. Copy official files (unconditionally overwrite)
107
+ let copied = 0;
108
+ for (const relPath of newOfficialFiles) {
109
+ const src = join(BUILTIN_CLIS, relPath);
110
+ const dst = join(USER_CLIS_DIR, relPath);
111
+ mkdirSync(dirname(dst), { recursive: true });
112
+ cpSync(src, dst, { force: true });
113
+ copied++;
114
+ }
115
+
116
+ // 2. Remove files that were official but are no longer (upstream deleted)
117
+ let removed = 0;
118
+ for (const relPath of oldOfficialFiles) {
119
+ if (!newOfficialFiles.has(relPath)) {
120
+ const dst = join(USER_CLIS_DIR, relPath);
121
+ try {
122
+ unlinkSync(dst);
123
+ pruneEmptyDirs(dst, USER_CLIS_DIR);
124
+ removed++;
125
+ } catch {
126
+ // File may not exist locally
127
+ }
128
+ }
129
+ }
130
+
131
+ // 3. Write updated manifest
132
+ writeFileSync(MANIFEST_PATH, JSON.stringify({
133
+ version: currentVersion,
134
+ files: [...newOfficialFiles].sort(),
135
+ updatedAt: new Date().toISOString(),
136
+ }, null, 2));
137
+
138
+ log(`Installed ${copied} adapter files to ${USER_CLIS_DIR}` +
139
+ (removed > 0 ? `, removed ${removed} deprecated files` : ''));
140
+ }
141
+
142
+ function main() {
143
+ // Skip in CI
144
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return;
145
+ // Allow opt-out
146
+ if (process.env.OPENCLI_SKIP_FETCH === '1') return;
147
+
148
+ // Only run on global install, explicit trigger, or first-run fallback
149
+ const isGlobal = process.env.npm_config_global === 'true';
150
+ const isExplicit = process.env.OPENCLI_FETCH === '1';
151
+ const isFirstRun = process.env._OPENCLI_FIRST_RUN === '1';
152
+ if (!isGlobal && !isExplicit && !isFirstRun) return;
153
+
154
+ fetchAdapters();
155
+ }
156
+
157
+ main();
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * postinstall script — install shell completion files and print setup instructions.
5
+ *
6
+ * Detects the user's default shell and writes the completion script to the
7
+ * standard completion directory. For zsh and bash, the script prints manual
8
+ * instructions instead of modifying rc files (~/.zshrc, ~/.bashrc) — this
9
+ * avoids breaking multi-line shell commands and other fragile rc structures.
10
+ * Fish completions work automatically without rc changes.
11
+ *
12
+ * Supported shells: bash, zsh, fish.
13
+ *
14
+ * This script is intentionally plain Node.js (no TypeScript, no imports from
15
+ * the main source tree) so that it can run without a build step.
16
+ */
17
+
18
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+
22
+
23
+ // ── Completion script content ──────────────────────────────────────────────
24
+
25
+ const BASH_COMPLETION = `# Bash completion for opencli (auto-installed)
26
+ _opencli_completions() {
27
+ local cur words cword
28
+ _get_comp_words_by_ref -n : cur words cword
29
+
30
+ local completions
31
+ completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
32
+
33
+ COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
34
+ __ltrim_colon_completions "$cur"
35
+ }
36
+ complete -F _opencli_completions opencli
37
+ `;
38
+
39
+ const ZSH_COMPLETION = `#compdef opencli
40
+ # Zsh completion for opencli (auto-installed)
41
+ _opencli() {
42
+ local -a completions
43
+ local cword=$((CURRENT - 1))
44
+ completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
45
+ compadd -a completions
46
+ }
47
+ _opencli
48
+ `;
49
+
50
+ const FISH_COMPLETION = `# Fish completion for opencli (auto-installed)
51
+ complete -c opencli -f -a '(
52
+ set -l tokens (commandline -cop)
53
+ set -l cursor (count (commandline -cop))
54
+ opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
55
+ )'
56
+ `;
57
+
58
+ // ── Helpers ────────────────────────────────────────────────────────────────
59
+
60
+ function detectShell() {
61
+ const shell = process.env.SHELL || '';
62
+ if (shell.includes('zsh')) return 'zsh';
63
+ if (shell.includes('bash')) return 'bash';
64
+ if (shell.includes('fish')) return 'fish';
65
+ return null;
66
+ }
67
+
68
+ function ensureDir(dir) {
69
+ if (!existsSync(dir)) {
70
+ mkdirSync(dir, { recursive: true });
71
+ }
72
+ }
73
+
74
+ // ── Main ───────────────────────────────────────────────────────────────────
75
+
76
+ function main() {
77
+ // Skip in CI environments
78
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
79
+ return;
80
+ }
81
+
82
+ // Only install completion for global installs and npm link
83
+ const isGlobal = process.env.npm_config_global === 'true';
84
+ if (!isGlobal) {
85
+ return;
86
+ }
87
+
88
+ const shell = detectShell();
89
+ if (!shell) {
90
+ // Cannot determine shell; silently skip
91
+ return;
92
+ }
93
+
94
+ const home = homedir();
95
+
96
+ try {
97
+ switch (shell) {
98
+ case 'zsh': {
99
+ const completionsDir = join(home, '.zsh', 'completions');
100
+ const completionFile = join(completionsDir, '_opencli');
101
+ ensureDir(completionsDir);
102
+ writeFileSync(completionFile, ZSH_COMPLETION, 'utf8');
103
+
104
+ console.log(`✓ Zsh completion installed to ${completionFile}`);
105
+ console.log('');
106
+ console.log(' \x1b[1mTo enable, add these lines to your ~/.zshrc:\x1b[0m');
107
+ console.log(` fpath=(${completionsDir} $fpath)`);
108
+ console.log(' autoload -Uz compinit && compinit');
109
+ console.log('');
110
+ console.log(' If you already have compinit (oh-my-zsh, zinit, etc.), just add the fpath line \x1b[1mbefore\x1b[0m it.');
111
+ console.log(' Then restart your shell or run: \x1b[36mexec zsh\x1b[0m');
112
+ break;
113
+ }
114
+ case 'bash': {
115
+ const userCompDir = join(home, '.bash_completion.d');
116
+ const completionFile = join(userCompDir, 'opencli');
117
+ ensureDir(userCompDir);
118
+ writeFileSync(completionFile, BASH_COMPLETION, 'utf8');
119
+
120
+ console.log(`✓ Bash completion installed to ${completionFile}`);
121
+ console.log('');
122
+ console.log(' \x1b[1mTo enable, add this line to your ~/.bashrc:\x1b[0m');
123
+ console.log(` [ -f "${completionFile}" ] && source "${completionFile}"`);
124
+ console.log('');
125
+ console.log(' Then restart your shell or run: \x1b[36msource ~/.bashrc\x1b[0m');
126
+ break;
127
+ }
128
+ case 'fish': {
129
+ const completionsDir = join(home, '.config', 'fish', 'completions');
130
+ const completionFile = join(completionsDir, 'opencli.fish');
131
+ ensureDir(completionsDir);
132
+ writeFileSync(completionFile, FISH_COMPLETION, 'utf8');
133
+
134
+ console.log(`✓ Fish completion installed to ${completionFile}`);
135
+ console.log(` Restart your shell to activate.`);
136
+ break;
137
+ }
138
+ }
139
+ } catch (err) {
140
+ // Completion install is best-effort; never fail the package install
141
+ if (process.env.OPENCLI_VERBOSE) {
142
+ console.error(`Warning: Could not install shell completion: ${err.message}`);
143
+ }
144
+ }
145
+
146
+ // ── Spotify credentials template ────────────────────────────────────
147
+ const opencliDir = join(home, '.opencli');
148
+ const spotifyEnvFile = join(opencliDir, 'spotify.env');
149
+ ensureDir(opencliDir);
150
+ if (!existsSync(spotifyEnvFile)) {
151
+ writeFileSync(spotifyEnvFile,
152
+ `# Spotify credentials — get them at https://developer.spotify.com/dashboard\n` +
153
+ `# Add http://127.0.0.1:8888/callback as a Redirect URI in your Spotify app\n` +
154
+ `SPOTIFY_CLIENT_ID=your_spotify_client_id_here\n` +
155
+ `SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here\n`,
156
+ 'utf8'
157
+ );
158
+ console.log(`✓ Spotify credentials template created at ${spotifyEnvFile}`);
159
+ console.log(` Edit the file and add your Client ID and Secret, then run: opencli spotify auth`);
160
+ }
161
+
162
+ // ── Browser Bridge setup hint ───────────────────────────────────────
163
+ console.log('');
164
+ console.log(' \x1b[1mNext step — Browser Bridge setup\x1b[0m');
165
+ console.log(' Browser commands (bilibili, zhihu, twitter...) require the extension:');
166
+ console.log(' 1. Download: https://github.com/jackwener/opencli/releases');
167
+ console.log(' 2. In Chrome or Chromium, open chrome://extensions → enable Developer Mode → Load unpacked');
168
+ console.log('');
169
+ console.log(' Then run \x1b[36mopencli doctor\x1b[0m to verify.');
170
+ console.log('');
171
+ }
172
+
173
+ main();
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+
7
+ const site = process.argv[2]?.trim();
8
+
9
+ if (!site) {
10
+ console.error('Usage: npm run test:site -- <site>');
11
+ process.exit(1);
12
+ }
13
+
14
+ const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
15
+ const srcDir = path.join(repoRoot, 'src');
16
+
17
+ function runStep(label, command, args) {
18
+ console.log(`\n==> ${label}`);
19
+ const result = spawnSync(command, args, {
20
+ cwd: repoRoot,
21
+ stdio: 'inherit',
22
+ env: process.env,
23
+ });
24
+
25
+ if (result.status !== 0) {
26
+ process.exit(result.status ?? 1);
27
+ }
28
+ }
29
+
30
+ function walk(dir) {
31
+ const files = [];
32
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
33
+ const fullPath = path.join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ files.push(...walk(fullPath));
36
+ } else {
37
+ files.push(fullPath);
38
+ }
39
+ }
40
+ return files;
41
+ }
42
+
43
+ function toPosix(filePath) {
44
+ return filePath.split(path.sep).join('/');
45
+ }
46
+
47
+ function findSiteTests() {
48
+ return walk(srcDir)
49
+ .filter(filePath => filePath.endsWith('.test.ts'))
50
+ .filter(filePath => {
51
+ const normalized = toPosix(path.relative(repoRoot, filePath));
52
+ return normalized.includes(`/clis/${site}/`) || normalized.includes(`/${site}.test.ts`);
53
+ })
54
+ .sort();
55
+ }
56
+
57
+ runStep('Typecheck', 'npm', ['run', 'typecheck']);
58
+ runStep('Targeted verify', 'npx', ['tsx', 'src/main.ts', 'verify', site]);
59
+
60
+ const testFiles = findSiteTests();
61
+ if (testFiles.length === 0) {
62
+ console.log(`\nNo site-specific vitest files found for "${site}". Skipping full vitest run.`);
63
+ process.exit(0);
64
+ }
65
+
66
+ runStep(
67
+ `Site tests (${site})`,
68
+ 'npx',
69
+ ['vitest', 'run', ...testFiles.map(filePath => path.relative(repoRoot, filePath))],
70
+ );