@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.
Files changed (122) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +6 -3
  3. package/cli-manifest.json +1085 -73
  4. package/clis/barchart/flow.js +1 -1
  5. package/clis/barchart/greeks.js +2 -2
  6. package/clis/barchart/options.js +2 -2
  7. package/clis/barchart/quote.js +1 -1
  8. package/clis/bilibili/feed.js +202 -48
  9. package/clis/binance/asks.js +21 -0
  10. package/clis/binance/commands.test.js +70 -0
  11. package/clis/binance/depth.js +21 -0
  12. package/clis/binance/gainers.js +22 -0
  13. package/clis/binance/klines.js +21 -0
  14. package/clis/binance/losers.js +22 -0
  15. package/clis/binance/pairs.js +21 -0
  16. package/clis/binance/price.js +18 -0
  17. package/clis/binance/prices.js +19 -0
  18. package/clis/binance/ticker.js +21 -0
  19. package/clis/binance/top.js +21 -0
  20. package/clis/binance/trades.js +20 -0
  21. package/clis/boss/utils.js +2 -1
  22. package/clis/chatgpt/image.js +97 -0
  23. package/clis/chatgpt/utils.js +297 -0
  24. package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
  25. package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
  26. package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
  27. package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
  28. package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
  29. package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
  30. package/clis/discord-app/delete.js +114 -0
  31. package/clis/douban/utils.js +29 -2
  32. package/clis/douban/utils.test.js +121 -1
  33. package/clis/ke/chengjiao.js +77 -0
  34. package/clis/ke/ershoufang.js +100 -0
  35. package/clis/ke/utils.js +104 -0
  36. package/clis/ke/xiaoqu.js +77 -0
  37. package/clis/ke/zufang.js +94 -0
  38. package/clis/maimai/search-talents.js +172 -0
  39. package/clis/mubu/doc.js +40 -0
  40. package/clis/mubu/docs.js +43 -0
  41. package/clis/mubu/notes.js +244 -0
  42. package/clis/mubu/recent.js +27 -0
  43. package/clis/mubu/search.js +62 -0
  44. package/clis/mubu/utils.js +304 -0
  45. package/clis/reuters/search.js +1 -1
  46. package/clis/twitter/lists-parser.js +77 -0
  47. package/clis/twitter/lists.d.ts +5 -0
  48. package/clis/twitter/lists.js +62 -0
  49. package/clis/twitter/lists.test.js +50 -0
  50. package/clis/weibo/feed.js +18 -5
  51. package/clis/xiaohongshu/comments.js +18 -6
  52. package/clis/xiaohongshu/comments.test.js +36 -0
  53. package/clis/xiaohongshu/creator-note-detail.js +2 -0
  54. package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
  55. package/clis/xiaohongshu/creator-notes-summary.js +4 -0
  56. package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
  57. package/clis/xiaohongshu/creator-notes.js +1 -0
  58. package/clis/xiaohongshu/creator-profile.js +1 -0
  59. package/clis/xiaohongshu/creator-stats.js +1 -0
  60. package/clis/xiaohongshu/download.js +12 -0
  61. package/clis/xiaohongshu/download.test.js +30 -0
  62. package/clis/xiaohongshu/navigation.test.js +34 -0
  63. package/clis/xiaohongshu/note.js +14 -5
  64. package/clis/xiaohongshu/note.test.js +28 -0
  65. package/clis/xiaohongshu/publish.js +1 -0
  66. package/clis/xiaohongshu/search.js +1 -0
  67. package/clis/xiaohongshu/user.js +1 -0
  68. package/clis/yahoo-finance/quote.js +1 -1
  69. package/clis/zsxq/topic.js +5 -3
  70. package/clis/zsxq/topic.test.js +4 -3
  71. package/clis/zsxq/utils.js +1 -1
  72. package/dist/src/browser/base-page.d.ts +9 -0
  73. package/dist/src/browser/base-page.js +19 -0
  74. package/dist/src/browser/cdp.js +10 -2
  75. package/dist/src/browser/daemon-client.d.ts +1 -0
  76. package/dist/src/cli.js +112 -2
  77. package/dist/src/daemon.js +5 -0
  78. package/dist/src/discovery.d.ts +5 -2
  79. package/dist/src/discovery.js +7 -35
  80. package/dist/src/doctor.d.ts +1 -0
  81. package/dist/src/doctor.js +51 -2
  82. package/dist/src/electron-apps.js +1 -1
  83. package/dist/src/engine.test.js +29 -1
  84. package/dist/src/errors.d.ts +1 -0
  85. package/dist/src/errors.js +13 -0
  86. package/dist/src/execution.js +36 -9
  87. package/dist/src/execution.test.js +23 -0
  88. package/dist/src/logger.d.ts +2 -2
  89. package/dist/src/logger.js +4 -9
  90. package/dist/src/main.js +6 -5
  91. package/dist/src/registry.js +3 -4
  92. package/dist/src/types.d.ts +2 -0
  93. package/dist/src/update-check.d.ts +14 -0
  94. package/dist/src/update-check.js +48 -3
  95. package/dist/src/update-check.test.js +31 -0
  96. package/package.json +3 -3
  97. package/scripts/fetch-adapters.js +92 -34
  98. package/dist/src/clis/binance/asks.js +0 -20
  99. package/dist/src/clis/binance/commands.test.d.ts +0 -3
  100. package/dist/src/clis/binance/commands.test.js +0 -58
  101. package/dist/src/clis/binance/depth.d.ts +0 -1
  102. package/dist/src/clis/binance/depth.js +0 -20
  103. package/dist/src/clis/binance/gainers.d.ts +0 -1
  104. package/dist/src/clis/binance/gainers.js +0 -21
  105. package/dist/src/clis/binance/klines.d.ts +0 -1
  106. package/dist/src/clis/binance/klines.js +0 -20
  107. package/dist/src/clis/binance/losers.d.ts +0 -1
  108. package/dist/src/clis/binance/losers.js +0 -21
  109. package/dist/src/clis/binance/pairs.d.ts +0 -1
  110. package/dist/src/clis/binance/pairs.js +0 -20
  111. package/dist/src/clis/binance/price.d.ts +0 -1
  112. package/dist/src/clis/binance/price.js +0 -17
  113. package/dist/src/clis/binance/prices.d.ts +0 -1
  114. package/dist/src/clis/binance/prices.js +0 -18
  115. package/dist/src/clis/binance/ticker.d.ts +0 -1
  116. package/dist/src/clis/binance/ticker.js +0 -20
  117. package/dist/src/clis/binance/top.d.ts +0 -1
  118. package/dist/src/clis/binance/top.js +0 -20
  119. package/dist/src/clis/binance/trades.d.ts +0 -1
  120. package/dist/src/clis/binance/trades.js +0 -19
  121. /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
  122. /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
@@ -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 [key, value] of _registry.entries()) {
82
- if (value === existing && key !== canonicalKey)
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);
@@ -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, };
@@ -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
- fs.writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: Date.now(), latestVersion }), 'utf-8');
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
- writeCache(data.version);
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.1",
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
- * Copy official CLI adapters from the installed package to ~/.opencli/clis/.
4
+ * Sparse adapter sync: keeps ~/.opencli/clis/ clean by removing stale overrides.
5
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
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 — copies directly from clis/ in the installed package.
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, cpSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
19
- import { join, resolve, dirname } from 'node:path';
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
- * Read existing manifest. Returns { version, files } or null.
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
- let dir = dirname(filePath);
74
- while (dir !== stopAt && dir.startsWith(stopAt)) {
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. Copy official files (unconditionally overwrite)
107
- let copied = 0;
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 dst = join(USER_CLIS_DIR, relPath);
111
- mkdirSync(dirname(dst), { recursive: true });
112
- cpSync(src, dst, { force: true });
113
- copied++;
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
- // 2. Remove files that were official but are no longer (upstream deleted)
117
- let removed = 0;
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
- 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
- }
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(`Installed ${copied} adapter files to ${USER_CLIS_DIR}` +
217
- (removed > 0 ? `, removed ${removed} deprecated files` : ''));
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,3 +0,0 @@
1
- import './top.js';
2
- import './gainers.js';
3
- import './pairs.js';
@@ -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 {};