@jackwener/opencli 1.7.1 → 1.7.3
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/README.md +5 -2
- package/README.zh-CN.md +6 -3
- package/cli-manifest.json +1085 -73
- package/clis/barchart/flow.js +1 -1
- package/clis/barchart/greeks.js +2 -2
- package/clis/barchart/options.js +2 -2
- package/clis/barchart/quote.js +1 -1
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/asks.js +21 -0
- package/clis/binance/commands.test.js +70 -0
- package/clis/binance/depth.js +21 -0
- package/clis/binance/gainers.js +22 -0
- package/clis/binance/klines.js +21 -0
- package/clis/binance/losers.js +22 -0
- package/clis/binance/pairs.js +21 -0
- package/clis/binance/price.js +18 -0
- package/clis/binance/prices.js +19 -0
- package/clis/binance/ticker.js +21 -0
- package/clis/binance/top.js +21 -0
- package/clis/binance/trades.js +20 -0
- package/clis/boss/utils.js +2 -1
- package/clis/chatgpt/image.js +97 -0
- package/clis/chatgpt/utils.js +297 -0
- package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
- package/clis/discord-app/delete.js +114 -0
- package/clis/douban/utils.js +29 -2
- package/clis/douban/utils.test.js +121 -1
- package/clis/ke/chengjiao.js +77 -0
- package/clis/ke/ershoufang.js +100 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +77 -0
- package/clis/ke/zufang.js +94 -0
- package/clis/maimai/search-talents.js +172 -0
- package/clis/mubu/doc.js +40 -0
- package/clis/mubu/docs.js +43 -0
- package/clis/mubu/notes.js +244 -0
- package/clis/mubu/recent.js +27 -0
- package/clis/mubu/search.js +62 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/reuters/search.js +1 -1
- package/clis/twitter/lists-parser.js +77 -0
- package/clis/twitter/lists.d.ts +5 -0
- package/clis/twitter/lists.js +62 -0
- package/clis/twitter/lists.test.js +50 -0
- package/clis/weibo/feed.js +18 -5
- package/clis/xiaohongshu/comments.js +18 -6
- package/clis/xiaohongshu/comments.test.js +36 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
- package/clis/xiaohongshu/creator-notes-summary.js +4 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +12 -0
- package/clis/xiaohongshu/download.test.js +30 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note.js +14 -5
- package/clis/xiaohongshu/note.test.js +28 -0
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/zsxq/topic.js +5 -3
- package/clis/zsxq/topic.test.js +4 -3
- package/clis/zsxq/utils.js +1 -1
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +19 -0
- package/dist/src/browser/cdp.js +10 -2
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/cli.js +112 -2
- package/dist/src/daemon.js +5 -0
- package/dist/src/discovery.d.ts +5 -2
- package/dist/src/discovery.js +7 -35
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +51 -2
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/engine.test.js +29 -1
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +13 -0
- package/dist/src/execution.js +36 -9
- package/dist/src/execution.test.js +23 -0
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +4 -9
- package/dist/src/main.js +6 -5
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +2 -0
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.js +31 -0
- package/package.json +3 -3
- package/scripts/fetch-adapters.js +92 -34
- package/dist/src/clis/binance/asks.js +0 -20
- package/dist/src/clis/binance/commands.test.d.ts +0 -3
- package/dist/src/clis/binance/commands.test.js +0 -58
- package/dist/src/clis/binance/depth.d.ts +0 -1
- package/dist/src/clis/binance/depth.js +0 -20
- package/dist/src/clis/binance/gainers.d.ts +0 -1
- package/dist/src/clis/binance/gainers.js +0 -21
- package/dist/src/clis/binance/klines.d.ts +0 -1
- package/dist/src/clis/binance/klines.js +0 -20
- package/dist/src/clis/binance/losers.d.ts +0 -1
- package/dist/src/clis/binance/losers.js +0 -21
- package/dist/src/clis/binance/pairs.d.ts +0 -1
- package/dist/src/clis/binance/pairs.js +0 -20
- package/dist/src/clis/binance/price.d.ts +0 -1
- package/dist/src/clis/binance/price.js +0 -17
- package/dist/src/clis/binance/prices.d.ts +0 -1
- package/dist/src/clis/binance/prices.js +0 -18
- package/dist/src/clis/binance/ticker.d.ts +0 -1
- package/dist/src/clis/binance/ticker.js +0 -20
- package/dist/src/clis/binance/top.d.ts +0 -1
- package/dist/src/clis/binance/top.js +0 -20
- package/dist/src/clis/binance/trades.d.ts +0 -1
- package/dist/src/clis/binance/trades.js +0 -19
- /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
- /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
package/dist/src/registry.js
CHANGED
|
@@ -77,10 +77,9 @@ export function registerCommand(cmd) {
|
|
|
77
77
|
const normalized = normalizeCommand(cmd);
|
|
78
78
|
const canonicalKey = fullName(normalized);
|
|
79
79
|
const existing = _registry.get(canonicalKey);
|
|
80
|
-
if (existing) {
|
|
81
|
-
for (const
|
|
82
|
-
|
|
83
|
-
_registry.delete(key);
|
|
80
|
+
if (existing?.aliases) {
|
|
81
|
+
for (const alias of existing.aliases) {
|
|
82
|
+
_registry.delete(`${existing.site}/${alias}`);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
const aliases = normalizeAliases(normalized.aliases, normalized.name);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface IPage {
|
|
|
44
44
|
settleMs?: number;
|
|
45
45
|
}): Promise<void>;
|
|
46
46
|
evaluate(js: string): Promise<any>;
|
|
47
|
+
/** Safely evaluate JS with pre-serialized arguments — prevents injection. */
|
|
48
|
+
evaluateWithArgs?(js: string, args: Record<string, unknown>): Promise<any>;
|
|
47
49
|
getCookies(opts?: {
|
|
48
50
|
domain?: string;
|
|
49
51
|
url?: string;
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* - Notice appears AFTER command output, not before (same as npm/gh/yarn)
|
|
9
9
|
* - Never delays or blocks the CLI command
|
|
10
10
|
*/
|
|
11
|
+
interface GitHubReleaseAsset {
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
interface GitHubRelease {
|
|
15
|
+
tag_name: string;
|
|
16
|
+
assets?: GitHubReleaseAsset[];
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
19
|
* Register a process exit hook that prints an update notice if a newer
|
|
13
20
|
* version was found on the last background check.
|
|
@@ -15,8 +22,15 @@
|
|
|
15
22
|
* Skipped during --get-completions to avoid polluting shell completion output.
|
|
16
23
|
*/
|
|
17
24
|
export declare function registerUpdateNoticeOnExit(): void;
|
|
25
|
+
declare function extractLatestExtensionVersionFromReleases(releases: GitHubRelease[]): string | undefined;
|
|
18
26
|
/**
|
|
19
27
|
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
20
28
|
* Fully non-blocking — never awaited.
|
|
21
29
|
*/
|
|
22
30
|
export declare function checkForUpdateBackground(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get the cached latest extension version (if available).
|
|
33
|
+
* Used by `opencli doctor` to report extension updates.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCachedLatestExtensionVersion(): string | undefined;
|
|
36
|
+
export { extractLatestExtensionVersionFromReleases as _extractLatestExtensionVersionFromReleases, };
|
package/dist/src/update-check.js
CHANGED
|
@@ -17,6 +17,7 @@ const CACHE_DIR = path.join(os.homedir(), '.opencli');
|
|
|
17
17
|
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
18
18
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
19
19
|
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@jackwener/opencli/latest';
|
|
20
|
+
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jackwener/OpenCLI/releases?per_page=20';
|
|
20
21
|
// Read cache once at module load — shared by both exported functions
|
|
21
22
|
const _cache = (() => {
|
|
22
23
|
try {
|
|
@@ -26,10 +27,13 @@ const _cache = (() => {
|
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
28
29
|
})();
|
|
29
|
-
function writeCache(latestVersion) {
|
|
30
|
+
function writeCache(latestVersion, latestExtensionVersion) {
|
|
30
31
|
try {
|
|
31
32
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
32
|
-
|
|
33
|
+
const data = { lastCheck: Date.now(), latestVersion };
|
|
34
|
+
if (latestExtensionVersion)
|
|
35
|
+
data.latestExtensionVersion = latestExtensionVersion;
|
|
36
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
33
37
|
}
|
|
34
38
|
catch {
|
|
35
39
|
// Best-effort; never fail
|
|
@@ -80,6 +84,38 @@ export function registerUpdateNoticeOnExit() {
|
|
|
80
84
|
}
|
|
81
85
|
});
|
|
82
86
|
}
|
|
87
|
+
function extractLatestExtensionVersionFromReleases(releases) {
|
|
88
|
+
for (const release of releases) {
|
|
89
|
+
for (const asset of release.assets ?? []) {
|
|
90
|
+
const assetMatch = asset.name.match(/^opencli-extension-v(.+)\.zip$/);
|
|
91
|
+
if (assetMatch)
|
|
92
|
+
return assetMatch[1];
|
|
93
|
+
}
|
|
94
|
+
const tagMatch = release.tag_name.match(/^ext-v(.+)$/);
|
|
95
|
+
if (tagMatch)
|
|
96
|
+
return tagMatch[1];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Fetch the latest extension version from GitHub Releases. */
|
|
101
|
+
async function fetchLatestExtensionVersion() {
|
|
102
|
+
try {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
105
|
+
const res = await fetch(GITHUB_RELEASES_URL, {
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
headers: { 'User-Agent': `opencli/${PKG_VERSION}`, Accept: 'application/vnd.github+json' },
|
|
108
|
+
});
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (!res.ok)
|
|
111
|
+
return undefined;
|
|
112
|
+
const releases = await res.json();
|
|
113
|
+
return extractLatestExtensionVersionFromReleases(releases);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
83
119
|
/**
|
|
84
120
|
* Kick off a background fetch to npm registry. Writes to cache for next run.
|
|
85
121
|
* Fully non-blocking — never awaited.
|
|
@@ -102,7 +138,8 @@ export function checkForUpdateBackground() {
|
|
|
102
138
|
return;
|
|
103
139
|
const data = await res.json();
|
|
104
140
|
if (typeof data.version === 'string') {
|
|
105
|
-
|
|
141
|
+
const extVersion = await fetchLatestExtensionVersion();
|
|
142
|
+
writeCache(data.version, extVersion);
|
|
106
143
|
}
|
|
107
144
|
}
|
|
108
145
|
catch {
|
|
@@ -110,3 +147,11 @@ export function checkForUpdateBackground() {
|
|
|
110
147
|
}
|
|
111
148
|
})();
|
|
112
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the cached latest extension version (if available).
|
|
152
|
+
* Used by `opencli doctor` to report extension updates.
|
|
153
|
+
*/
|
|
154
|
+
export function getCachedLatestExtensionVersion() {
|
|
155
|
+
return _cache?.latestExtensionVersion;
|
|
156
|
+
}
|
|
157
|
+
export { extractLatestExtensionVersionFromReleases as _extractLatestExtensionVersionFromReleases, };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { _extractLatestExtensionVersionFromReleases as extractLatestExtensionVersionFromReleases } from './update-check.js';
|
|
3
|
+
describe('extractLatestExtensionVersionFromReleases', () => {
|
|
4
|
+
it('reads the extension version from a versioned asset on a normal CLI release', () => {
|
|
5
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
6
|
+
{
|
|
7
|
+
tag_name: 'v1.7.3',
|
|
8
|
+
assets: [
|
|
9
|
+
{ name: 'opencli-extension.zip' },
|
|
10
|
+
{ name: 'opencli-extension-v1.0.2.zip' },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
])).toBe('1.0.2');
|
|
14
|
+
});
|
|
15
|
+
it('falls back to ext-v tags for extension-only releases', () => {
|
|
16
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
17
|
+
{
|
|
18
|
+
tag_name: 'ext-v1.1.0',
|
|
19
|
+
assets: [{ name: 'opencli-extension.zip' }],
|
|
20
|
+
},
|
|
21
|
+
])).toBe('1.1.0');
|
|
22
|
+
});
|
|
23
|
+
it('returns undefined when no extension version source exists', () => {
|
|
24
|
+
expect(extractLatestExtensionVersionFromReleases([
|
|
25
|
+
{
|
|
26
|
+
tag_name: 'v1.7.3',
|
|
27
|
+
assets: [{ name: 'opencli-extension.zip' }],
|
|
28
|
+
},
|
|
29
|
+
])).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"typecheck": "tsc --noEmit",
|
|
53
53
|
"prepare": "[ -d src ] && npm run build || true",
|
|
54
54
|
"prepublishOnly": "npm run build",
|
|
55
|
-
"test": "vitest run --project unit --project extension",
|
|
56
|
-
"test:bun": "bun vitest run --project unit --project extension",
|
|
55
|
+
"test": "vitest run --project unit --project extension --project adapter",
|
|
56
|
+
"test:bun": "bun vitest run --project unit --project extension --project adapter",
|
|
57
57
|
"test:adapter": "vitest run --project adapter",
|
|
58
58
|
"test:all": "vitest run",
|
|
59
59
|
"test:e2e": "vitest run --project e2e",
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Sparse adapter sync: keeps ~/.opencli/clis/ clean by removing stale overrides.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
6
|
+
* Strategy (hash-based, site-level granularity):
|
|
7
|
+
* - When an official site has upstream changes: DELETE the local override
|
|
8
|
+
* (do NOT copy new version — runtime falls back to package baseline)
|
|
9
|
+
* - When an official site has no changes: leave local override intact
|
|
10
|
+
* - User-created custom sites (not in package): always preserved
|
|
11
|
+
* - Skips entirely if already synced at the same version
|
|
12
|
+
*
|
|
13
|
+
* ~/.opencli/clis/ is a sparse override layer, not a full copy.
|
|
14
|
+
* Only eject-ed or user-modified sites appear here.
|
|
11
15
|
*
|
|
12
16
|
* Only runs on global install (npm install -g) or explicit OPENCLI_FETCH=1.
|
|
13
|
-
* No network calls —
|
|
17
|
+
* No network calls — reads hashes from clis/ in the installed package.
|
|
14
18
|
*
|
|
15
19
|
* This is an ESM script (package.json type: module). No TypeScript, no src/ imports.
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
|
-
import { existsSync, mkdirSync, rmSync,
|
|
19
|
-
import {
|
|
22
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
20
25
|
import { homedir } from 'node:os';
|
|
21
26
|
|
|
22
27
|
const OPENCLI_DIR = join(homedir(), '.opencli');
|
|
@@ -38,7 +43,14 @@ function getPackageVersion() {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
46
|
+
* Compute SHA-256 hash of file content.
|
|
47
|
+
*/
|
|
48
|
+
function fileHash(filePath) {
|
|
49
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read existing manifest. Returns { version, files, hashes } or null.
|
|
42
54
|
*/
|
|
43
55
|
function readManifest() {
|
|
44
56
|
try {
|
|
@@ -70,8 +82,11 @@ function walkFiles(dir, prefix = '') {
|
|
|
70
82
|
* Remove empty parent directories up to (but not including) stopAt.
|
|
71
83
|
*/
|
|
72
84
|
function pruneEmptyDirs(filePath, stopAt) {
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const boundary = resolve(stopAt);
|
|
86
|
+
let dir = resolve(dirname(filePath));
|
|
87
|
+
while (dir !== boundary) {
|
|
88
|
+
const rel = relative(boundary, dir);
|
|
89
|
+
if (!rel || rel.startsWith('..')) break;
|
|
75
90
|
try {
|
|
76
91
|
const entries = readdirSync(dir);
|
|
77
92
|
if (entries.length > 0) break;
|
|
@@ -101,30 +116,56 @@ export function fetchAdapters() {
|
|
|
101
116
|
|
|
102
117
|
const newOfficialFiles = new Set(walkFiles(BUILTIN_CLIS));
|
|
103
118
|
const oldOfficialFiles = new Set(oldManifest?.files ?? []);
|
|
119
|
+
const rawHashes = oldManifest?.hashes;
|
|
120
|
+
// Guard against corrupted manifest: if hashes is a non-object type (string, number,
|
|
121
|
+
// array), skip sync to avoid false-positive "changed" detection that deletes overrides.
|
|
122
|
+
// null/undefined are treated as empty (old manifests may lack the field).
|
|
123
|
+
if (rawHashes != null && (typeof rawHashes !== 'object' || Array.isArray(rawHashes))) {
|
|
124
|
+
log('Warning: adapter-manifest.json has corrupted hashes — skipping sync. Will fix on next run.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const oldHashes = rawHashes ?? {};
|
|
104
128
|
mkdirSync(USER_CLIS_DIR, { recursive: true });
|
|
105
129
|
|
|
106
|
-
// 1.
|
|
107
|
-
|
|
130
|
+
// 1. Compute new hashes and detect which sites have changes
|
|
131
|
+
const newHashes = {};
|
|
132
|
+
const siteFiles = new Map(); // site -> [relPath, ...]
|
|
108
133
|
for (const relPath of newOfficialFiles) {
|
|
109
134
|
const src = join(BUILTIN_CLIS, relPath);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
const srcHash = fileHash(src);
|
|
136
|
+
newHashes[relPath] = srcHash;
|
|
137
|
+
|
|
138
|
+
const site = relPath.split('/')[0];
|
|
139
|
+
if (!siteFiles.has(site)) siteFiles.set(site, []);
|
|
140
|
+
siteFiles.get(site).push(relPath);
|
|
114
141
|
}
|
|
115
142
|
|
|
116
|
-
//
|
|
117
|
-
|
|
143
|
+
// Determine which sites have any changed/new/removed files
|
|
144
|
+
const changedSites = new Set();
|
|
145
|
+
for (const [site, files] of siteFiles) {
|
|
146
|
+
for (const relPath of files) {
|
|
147
|
+
if (oldHashes[relPath] !== newHashes[relPath]) {
|
|
148
|
+
changedSites.add(site);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Also mark sites that had files removed
|
|
118
154
|
for (const relPath of oldOfficialFiles) {
|
|
119
155
|
if (!newOfficialFiles.has(relPath)) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
changedSites.add(relPath.split('/')[0]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Sparse cleanup: for changed/removed official sites, delete local overrides.
|
|
161
|
+
// Do NOT copy new versions — runtime falls back to package baseline.
|
|
162
|
+
// Only eject-ed sites live in ~/.opencli/clis/.
|
|
163
|
+
let cleared = 0;
|
|
164
|
+
for (const site of changedSites) {
|
|
165
|
+
const siteDir = join(USER_CLIS_DIR, site);
|
|
166
|
+
if (existsSync(siteDir)) {
|
|
167
|
+
rmSync(siteDir, { recursive: true, force: true });
|
|
168
|
+
cleared++;
|
|
128
169
|
}
|
|
129
170
|
}
|
|
130
171
|
|
|
@@ -145,6 +186,24 @@ export function fetchAdapters() {
|
|
|
145
186
|
}
|
|
146
187
|
if (tsCleaned > 0) log(`Cleaned up ${tsCleaned} stale .ts adapter files`);
|
|
147
188
|
|
|
189
|
+
// 3b. Clean up stale .yaml/.yml adapter files left by older versions (pre-1.7.0)
|
|
190
|
+
// Older versions shipped adapters as YAML; current versions use .js only.
|
|
191
|
+
// These cause "Ignoring YAML adapter" warnings on every run (issue #953).
|
|
192
|
+
let yamlCleaned = 0;
|
|
193
|
+
for (const relPath of walkFiles(USER_CLIS_DIR)) {
|
|
194
|
+
if (relPath.endsWith('.yaml') || relPath.endsWith('.yml')) {
|
|
195
|
+
const jsCounterpart = relPath.replace(/\.ya?ml$/, '.js');
|
|
196
|
+
if (newOfficialFiles.has(jsCounterpart)) {
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(join(USER_CLIS_DIR, relPath));
|
|
199
|
+
pruneEmptyDirs(join(USER_CLIS_DIR, relPath), USER_CLIS_DIR);
|
|
200
|
+
yamlCleaned++;
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (yamlCleaned > 0) log(`Cleaned up ${yamlCleaned} stale .yaml adapter files`);
|
|
206
|
+
|
|
148
207
|
// 4. Clean up legacy compat shim files from ~/.opencli/
|
|
149
208
|
// These were created by an older approach that placed re-export shims directly
|
|
150
209
|
// in ~/.opencli/ (e.g., registry.js, errors.js, browser/). The current approach
|
|
@@ -206,23 +265,22 @@ export function fetchAdapters() {
|
|
|
206
265
|
log(`Cleaned up${legacyCleaned > 0 ? ` ${legacyCleaned} legacy shim files` : ''}${tmpCleaned > 0 ? `${legacyCleaned > 0 ? ',' : ''} ${tmpCleaned} stale tmp files` : ''}`);
|
|
207
266
|
}
|
|
208
267
|
|
|
209
|
-
// 6. Write updated manifest
|
|
268
|
+
// 6. Write updated manifest (with per-file hashes for smart sync)
|
|
210
269
|
writeFileSync(MANIFEST_PATH, JSON.stringify({
|
|
211
270
|
version: currentVersion,
|
|
212
271
|
files: [...newOfficialFiles].sort(),
|
|
272
|
+
hashes: newHashes,
|
|
213
273
|
updatedAt: new Date().toISOString(),
|
|
214
274
|
}, null, 2));
|
|
215
275
|
|
|
216
|
-
log(`
|
|
217
|
-
(
|
|
276
|
+
log(`Synced adapters: ${cleared} local override(s) cleared` +
|
|
277
|
+
(tsCleaned > 0 ? `, ${tsCleaned} stale .ts files removed` : '') +
|
|
278
|
+
(yamlCleaned > 0 ? `, ${yamlCleaned} stale .yaml files removed` : ''));
|
|
218
279
|
}
|
|
219
280
|
|
|
220
281
|
function main() {
|
|
221
282
|
// Skip in CI
|
|
222
283
|
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return;
|
|
223
|
-
// Allow opt-out
|
|
224
|
-
if (process.env.OPENCLI_SKIP_FETCH === '1') return;
|
|
225
|
-
|
|
226
284
|
// Only run on global install, explicit trigger, or first-run fallback
|
|
227
285
|
const isGlobal = process.env.npm_config_global === 'true';
|
|
228
286
|
const isExplicit = process.env.OPENCLI_FETCH === '1';
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'asks',
|
|
5
|
-
description: 'Order book ask prices for a trading pair',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
11
|
-
{ name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
|
|
12
|
-
],
|
|
13
|
-
columns: ['rank', 'ask_price', 'ask_qty'],
|
|
14
|
-
pipeline: [
|
|
15
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
16
|
-
{ select: 'asks' },
|
|
17
|
-
{ map: { rank: '${{ index + 1 }}', ask_price: '${{ item.0 }}', ask_qty: '${{ item.1 }}' } },
|
|
18
|
-
{ limit: '${{ args.limit }}' },
|
|
19
|
-
],
|
|
20
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { getRegistry } from '@jackwener/opencli/registry';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { executePipeline } from '../../pipeline/index.js';
|
|
4
|
-
// Import all binance adapters to register them
|
|
5
|
-
import './top.js';
|
|
6
|
-
import './gainers.js';
|
|
7
|
-
import './pairs.js';
|
|
8
|
-
function loadPipeline(name) {
|
|
9
|
-
const cmd = getRegistry().get(`binance/${name}`);
|
|
10
|
-
if (!cmd?.pipeline)
|
|
11
|
-
throw new Error(`Command binance/${name} not found or has no pipeline`);
|
|
12
|
-
return cmd.pipeline;
|
|
13
|
-
}
|
|
14
|
-
function mockJsonOnce(payload) {
|
|
15
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
16
|
-
ok: true,
|
|
17
|
-
status: 200,
|
|
18
|
-
statusText: 'OK',
|
|
19
|
-
json: vi.fn().mockResolvedValue(payload),
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
vi.unstubAllGlobals();
|
|
24
|
-
vi.restoreAllMocks();
|
|
25
|
-
});
|
|
26
|
-
describe('binance adapters', () => {
|
|
27
|
-
it('sorts top pairs by numeric quote volume', async () => {
|
|
28
|
-
mockJsonOnce([
|
|
29
|
-
{ symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' },
|
|
30
|
-
{ symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' },
|
|
31
|
-
{ symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' },
|
|
32
|
-
]);
|
|
33
|
-
const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } });
|
|
34
|
-
expect(result.map((item) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']);
|
|
35
|
-
expect(result.map((item) => item.rank)).toEqual([1, 2, 3]);
|
|
36
|
-
});
|
|
37
|
-
it('sorts gainers by numeric percent change', async () => {
|
|
38
|
-
mockJsonOnce([
|
|
39
|
-
{ symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' },
|
|
40
|
-
{ symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' },
|
|
41
|
-
{ symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' },
|
|
42
|
-
]);
|
|
43
|
-
const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } });
|
|
44
|
-
expect(result.map((item) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']);
|
|
45
|
-
});
|
|
46
|
-
it('keeps only TRADING pairs', async () => {
|
|
47
|
-
mockJsonOnce({
|
|
48
|
-
symbols: [
|
|
49
|
-
{ symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' },
|
|
50
|
-
{ symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' },
|
|
51
|
-
],
|
|
52
|
-
});
|
|
53
|
-
const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } });
|
|
54
|
-
expect(result).toEqual([
|
|
55
|
-
{ symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' },
|
|
56
|
-
]);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'depth',
|
|
5
|
-
description: 'Order book bid prices for a trading pair',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
11
|
-
{ name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
|
|
12
|
-
],
|
|
13
|
-
columns: ['rank', 'bid_price', 'bid_qty'],
|
|
14
|
-
pipeline: [
|
|
15
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
|
|
16
|
-
{ select: 'bids' },
|
|
17
|
-
{ map: { rank: '${{ index + 1 }}', bid_price: '${{ item.0 }}', bid_qty: '${{ item.1 }}' } },
|
|
18
|
-
{ limit: '${{ args.limit }}' },
|
|
19
|
-
],
|
|
20
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'gainers',
|
|
5
|
-
description: 'Top gaining trading pairs by 24h price change',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
|
|
11
|
-
],
|
|
12
|
-
columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
|
|
13
|
-
pipeline: [
|
|
14
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
15
|
-
{ filter: 'item.priceChangePercent' },
|
|
16
|
-
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
|
|
17
|
-
{ sort: { by: 'sort_change', order: 'desc' } },
|
|
18
|
-
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
|
|
19
|
-
{ limit: '${{ args.limit }}' },
|
|
20
|
-
],
|
|
21
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'klines',
|
|
5
|
-
description: 'Candlestick/kline data for a trading pair',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
11
|
-
{ name: 'interval', type: 'str', default: '1d', help: 'Kline interval (1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M)' },
|
|
12
|
-
{ name: 'limit', type: 'int', default: 10, help: 'Number of klines (max 1000)' },
|
|
13
|
-
],
|
|
14
|
-
columns: ['open', 'high', 'low', 'close', 'volume'],
|
|
15
|
-
pipeline: [
|
|
16
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/klines?symbol=${{ args.symbol }}&interval=${{ args.interval }}&limit=${{ args.limit }}' } },
|
|
17
|
-
{ map: { open: '${{ item.1 }}', high: '${{ item.2 }}', low: '${{ item.3 }}', close: '${{ item.4 }}', volume: '${{ item.5 }}' } },
|
|
18
|
-
{ limit: '${{ args.limit }}' },
|
|
19
|
-
],
|
|
20
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'losers',
|
|
5
|
-
description: 'Top losing trading pairs by 24h price change',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'limit', type: 'int', default: 10, help: 'Number of trading pairs' },
|
|
11
|
-
],
|
|
12
|
-
columns: ['rank', 'symbol', 'price', 'change_24h', 'volume'],
|
|
13
|
-
pipeline: [
|
|
14
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr' } },
|
|
15
|
-
{ filter: 'item.priceChangePercent' },
|
|
16
|
-
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}', sort_change: '${{ Number(item.priceChangePercent) }}' } },
|
|
17
|
-
{ sort: { by: 'sort_change' } },
|
|
18
|
-
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change_24h: '${{ item.priceChangePercent }}', volume: '${{ item.quoteVolume }}' } },
|
|
19
|
-
{ limit: '${{ args.limit }}' },
|
|
20
|
-
],
|
|
21
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'pairs',
|
|
5
|
-
description: 'List active trading pairs on Binance',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of trading pairs' },
|
|
11
|
-
],
|
|
12
|
-
columns: ['symbol', 'base', 'quote', 'status'],
|
|
13
|
-
pipeline: [
|
|
14
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/exchangeInfo' } },
|
|
15
|
-
{ select: 'symbols' },
|
|
16
|
-
{ filter: 'item.status === \'TRADING\'' },
|
|
17
|
-
{ map: { symbol: '${{ item.symbol }}', base: '${{ item.baseAsset }}', quote: '${{ item.quoteAsset }}', status: '${{ item.status }}' } },
|
|
18
|
-
{ limit: '${{ args.limit }}' },
|
|
19
|
-
],
|
|
20
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'price',
|
|
5
|
-
description: 'Quick price check for a trading pair',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
|
|
11
|
-
],
|
|
12
|
-
columns: ['symbol', 'price', 'change', 'change_pct', 'high', 'low', 'volume', 'quote_volume', 'trades'],
|
|
13
|
-
pipeline: [
|
|
14
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/24hr?symbol=${{ args.symbol }}' } },
|
|
15
|
-
{ map: { symbol: '${{ item.symbol }}', price: '${{ item.lastPrice }}', change: '${{ item.priceChange }}', change_pct: '${{ item.priceChangePercent }}', high: '${{ item.highPrice }}', low: '${{ item.lowPrice }}', volume: '${{ item.volume }}', quote_volume: '${{ item.quoteVolume }}', trades: '${{ item.count }}' } },
|
|
16
|
-
],
|
|
17
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'binance',
|
|
4
|
-
name: 'prices',
|
|
5
|
-
description: 'Latest prices for all trading pairs',
|
|
6
|
-
domain: 'data-api.binance.vision',
|
|
7
|
-
strategy: Strategy.PUBLIC,
|
|
8
|
-
browser: false,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of prices' },
|
|
11
|
-
],
|
|
12
|
-
columns: ['rank', 'symbol', 'price'],
|
|
13
|
-
pipeline: [
|
|
14
|
-
{ fetch: { url: 'https://data-api.binance.vision/api/v3/ticker/price' } },
|
|
15
|
-
{ map: { rank: '${{ index + 1 }}', symbol: '${{ item.symbol }}', price: '${{ item.price }}' } },
|
|
16
|
-
{ limit: '${{ args.limit }}' },
|
|
17
|
-
],
|
|
18
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|