@jackwener/opencli 1.6.7 → 1.6.9
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 -1
- package/README.zh-CN.md +8 -3
- package/dist/clis/1688/assets.d.ts +42 -0
- package/dist/clis/1688/assets.js +204 -0
- package/dist/clis/1688/assets.test.d.ts +1 -0
- package/dist/clis/1688/assets.test.js +39 -0
- package/dist/clis/1688/download.d.ts +9 -0
- package/dist/clis/1688/download.js +76 -0
- package/dist/clis/1688/download.test.d.ts +1 -0
- package/dist/clis/1688/download.test.js +31 -0
- package/dist/clis/1688/shared.d.ts +10 -0
- package/dist/clis/1688/shared.js +43 -0
- package/dist/clis/jianyu/search.d.ts +14 -0
- package/dist/clis/jianyu/search.js +135 -0
- package/dist/clis/jianyu/search.test.d.ts +1 -0
- package/dist/clis/jianyu/search.test.js +23 -0
- package/dist/clis/linux-do/topic-content.d.ts +35 -0
- package/dist/clis/linux-do/topic-content.js +154 -0
- package/dist/clis/linux-do/topic-content.test.d.ts +1 -0
- package/dist/clis/linux-do/topic-content.test.js +59 -0
- package/dist/clis/linux-do/topic.yaml +1 -16
- package/dist/clis/quark/ls.d.ts +1 -0
- package/dist/clis/quark/ls.js +63 -0
- package/dist/clis/quark/mkdir.d.ts +1 -0
- package/dist/clis/quark/mkdir.js +36 -0
- package/dist/clis/quark/mv.d.ts +1 -0
- package/dist/clis/quark/mv.js +53 -0
- package/dist/clis/quark/rename.d.ts +1 -0
- package/dist/clis/quark/rename.js +26 -0
- package/dist/clis/quark/rm.d.ts +1 -0
- package/dist/clis/quark/rm.js +24 -0
- package/dist/clis/quark/save.d.ts +1 -0
- package/dist/clis/quark/save.js +80 -0
- package/dist/clis/quark/share-tree.d.ts +1 -0
- package/dist/clis/quark/share-tree.js +45 -0
- package/dist/clis/quark/utils.d.ts +50 -0
- package/dist/clis/quark/utils.js +146 -0
- package/dist/clis/quark/utils.test.d.ts +1 -0
- package/dist/clis/quark/utils.test.js +58 -0
- package/dist/clis/twitter/reply.js +3 -8
- package/dist/clis/twitter/reply.test.js +5 -5
- package/dist/clis/xiaohongshu/note.js +8 -3
- package/dist/clis/xiaohongshu/note.test.js +11 -0
- package/dist/clis/xueqiu/groups.yaml +23 -0
- package/dist/clis/xueqiu/kline.yaml +65 -0
- package/dist/clis/xueqiu/watchlist.yaml +9 -9
- package/dist/clis/zhihu/answer.d.ts +1 -0
- package/dist/clis/zhihu/answer.js +194 -0
- package/dist/clis/zhihu/answer.test.d.ts +1 -0
- package/dist/clis/zhihu/answer.test.js +81 -0
- package/dist/clis/zhihu/comment.d.ts +1 -0
- package/dist/clis/zhihu/comment.js +335 -0
- package/dist/clis/zhihu/comment.test.d.ts +1 -0
- package/dist/clis/zhihu/comment.test.js +54 -0
- package/dist/clis/zhihu/favorite.d.ts +1 -0
- package/dist/clis/zhihu/favorite.js +224 -0
- package/dist/clis/zhihu/favorite.test.d.ts +1 -0
- package/dist/clis/zhihu/favorite.test.js +196 -0
- package/dist/clis/zhihu/follow.d.ts +1 -0
- package/dist/clis/zhihu/follow.js +80 -0
- package/dist/clis/zhihu/follow.test.d.ts +1 -0
- package/dist/clis/zhihu/follow.test.js +45 -0
- package/dist/clis/zhihu/like.d.ts +1 -0
- package/dist/clis/zhihu/like.js +91 -0
- package/dist/clis/zhihu/like.test.d.ts +1 -0
- package/dist/clis/zhihu/like.test.js +64 -0
- package/dist/clis/zhihu/target.d.ts +24 -0
- package/dist/clis/zhihu/target.js +91 -0
- package/dist/clis/zhihu/target.test.d.ts +1 -0
- package/dist/clis/zhihu/target.test.js +77 -0
- package/dist/clis/zhihu/write-shared.d.ts +32 -0
- package/dist/clis/zhihu/write-shared.js +221 -0
- package/dist/clis/zhihu/write-shared.test.d.ts +1 -0
- package/dist/clis/zhihu/write-shared.test.js +175 -0
- package/dist/src/analysis.d.ts +2 -0
- package/dist/src/analysis.js +6 -0
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +30 -24
- package/dist/src/browser/cdp.js +96 -0
- package/dist/src/browser/daemon-client.d.ts +17 -8
- package/dist/src/browser/daemon-client.js +12 -13
- package/dist/src/browser/daemon-client.test.js +32 -25
- package/dist/src/browser/index.d.ts +2 -1
- package/dist/src/browser/index.js +1 -1
- package/dist/src/browser.test.js +2 -3
- package/dist/src/build-manifest.d.ts +3 -1
- package/dist/src/build-manifest.js +10 -7
- package/dist/src/build-manifest.test.js +8 -4
- package/dist/src/cli.d.ts +2 -1
- package/dist/src/cli.js +48 -46
- package/dist/src/clis/binance/commands.test.d.ts +1 -0
- package/dist/src/clis/binance/commands.test.js +54 -0
- package/dist/src/commanderAdapter.js +19 -6
- package/dist/src/commands/daemon.js +2 -10
- package/dist/src/diagnostic.d.ts +28 -2
- package/dist/src/diagnostic.js +263 -25
- package/dist/src/diagnostic.test.js +220 -1
- package/dist/src/discovery.js +7 -17
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +59 -31
- package/dist/src/doctor.test.js +89 -16
- package/dist/src/download/progress.js +7 -2
- package/dist/src/execution.js +1 -13
- package/dist/src/explore.d.ts +0 -2
- package/dist/src/explore.js +61 -38
- package/dist/src/extension-manifest-regression.test.js +0 -1
- package/dist/src/generate.d.ts +3 -6
- package/dist/src/generate.js +4 -8
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/plugin-scaffold.js +1 -3
- package/dist/src/plugin.d.ts +2 -1
- package/dist/src/plugin.js +25 -8
- package/dist/src/plugin.test.js +16 -1
- package/dist/src/record.d.ts +1 -2
- package/dist/src/record.js +14 -52
- package/dist/src/synthesize.d.ts +0 -2
- package/dist/src/synthesize.js +8 -4
- package/package.json +3 -3
- package/dist/cli-manifest.json +0 -17250
- package/dist/src/browser/discover.d.ts +0 -15
- package/dist/src/browser/discover.js +0 -19
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* manifest.json for instant cold-start registration (no runtime YAML parsing).
|
|
7
7
|
*
|
|
8
8
|
* Usage: npx tsx src/build-manifest.ts
|
|
9
|
-
* Output:
|
|
9
|
+
* Output: cli-manifest.json at the package root
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
@@ -14,9 +14,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
15
|
import { getErrorMessage } from './errors.js';
|
|
16
16
|
import { fullName, getRegistry } from './registry.js';
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
|
|
18
|
+
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
19
|
+
const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
|
|
20
|
+
const OUTPUT = getCliManifestPath(CLIS_DIR);
|
|
20
21
|
import { parseYamlArgs } from './yaml-schema.js';
|
|
21
22
|
import { isRecord } from './utils.js';
|
|
22
23
|
const CLI_MODULE_PATTERN = /\bcli\s*\(/;
|
|
@@ -43,7 +44,7 @@ function isCliCommandValue(value, site) {
|
|
|
43
44
|
&& typeof value.name === 'string'
|
|
44
45
|
&& Array.isArray(value.args);
|
|
45
46
|
}
|
|
46
|
-
function toManifestEntry(cmd, modulePath) {
|
|
47
|
+
function toManifestEntry(cmd, modulePath, sourceFile) {
|
|
47
48
|
return {
|
|
48
49
|
site: cmd.site,
|
|
49
50
|
name: cmd.name,
|
|
@@ -59,6 +60,7 @@ function toManifestEntry(cmd, modulePath) {
|
|
|
59
60
|
replacedBy: cmd.replacedBy,
|
|
60
61
|
type: 'ts',
|
|
61
62
|
modulePath,
|
|
63
|
+
sourceFile,
|
|
62
64
|
navigateBefore: cmd.navigateBefore,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
@@ -90,6 +92,7 @@ function scanYaml(filePath, site) {
|
|
|
90
92
|
deprecated: cliDef.deprecated,
|
|
91
93
|
replacedBy: cliDef.replacedBy,
|
|
92
94
|
type: 'yaml',
|
|
95
|
+
sourceFile: path.relative(CLIS_DIR, filePath),
|
|
93
96
|
navigateBefore: cliDef.navigateBefore,
|
|
94
97
|
};
|
|
95
98
|
}
|
|
@@ -130,7 +133,7 @@ export async function loadTsManifestEntries(filePath, site, importer = moduleHre
|
|
|
130
133
|
return true;
|
|
131
134
|
})
|
|
132
135
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
133
|
-
.map(cmd => toManifestEntry(cmd, modulePath));
|
|
136
|
+
.map(cmd => toManifestEntry(cmd, modulePath, path.relative(CLIS_DIR, filePath)));
|
|
134
137
|
}
|
|
135
138
|
catch (err) {
|
|
136
139
|
// If parsing fails, log a warning (matching scanYaml behaviour) and skip the entry.
|
|
@@ -200,7 +203,7 @@ async function main() {
|
|
|
200
203
|
// entry-point loses its executable permission, causing "Permission denied".
|
|
201
204
|
// See: https://github.com/jackwener/opencli/issues/446
|
|
202
205
|
if (process.platform !== 'win32') {
|
|
203
|
-
const projectRoot =
|
|
206
|
+
const projectRoot = PACKAGE_ROOT;
|
|
204
207
|
const pkgPath = path.resolve(projectRoot, 'package.json');
|
|
205
208
|
try {
|
|
206
209
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -87,7 +87,7 @@ describe('manifest helper rules', () => {
|
|
|
87
87
|
replacedBy: 'opencli demo new',
|
|
88
88
|
}),
|
|
89
89
|
}));
|
|
90
|
-
expect(entries).
|
|
90
|
+
expect(entries).toMatchObject([
|
|
91
91
|
{
|
|
92
92
|
site,
|
|
93
93
|
name: 'dynamic',
|
|
@@ -97,7 +97,7 @@ describe('manifest helper rules', () => {
|
|
|
97
97
|
browser: false,
|
|
98
98
|
aliases: ['metadata'],
|
|
99
99
|
args: [
|
|
100
|
-
{
|
|
100
|
+
expect.objectContaining({
|
|
101
101
|
name: 'model',
|
|
102
102
|
type: 'str',
|
|
103
103
|
required: true,
|
|
@@ -105,7 +105,7 @@ describe('manifest helper rules', () => {
|
|
|
105
105
|
help: 'Choose a model',
|
|
106
106
|
choices: ['auto', 'thinking'],
|
|
107
107
|
default: '30',
|
|
108
|
-
},
|
|
108
|
+
}),
|
|
109
109
|
],
|
|
110
110
|
type: 'ts',
|
|
111
111
|
modulePath: `${site}/${site}.js`,
|
|
@@ -114,6 +114,8 @@ describe('manifest helper rules', () => {
|
|
|
114
114
|
replacedBy: 'opencli demo new',
|
|
115
115
|
},
|
|
116
116
|
]);
|
|
117
|
+
// Verify sourceFile is included
|
|
118
|
+
expect(entries[0].sourceFile).toBeDefined();
|
|
117
119
|
getRegistry().delete(key);
|
|
118
120
|
});
|
|
119
121
|
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
@@ -133,7 +135,7 @@ describe('manifest helper rules', () => {
|
|
|
133
135
|
});
|
|
134
136
|
return {};
|
|
135
137
|
});
|
|
136
|
-
expect(entries).
|
|
138
|
+
expect(entries).toMatchObject([
|
|
137
139
|
{
|
|
138
140
|
site,
|
|
139
141
|
name: 'legacy',
|
|
@@ -147,6 +149,8 @@ describe('manifest helper rules', () => {
|
|
|
147
149
|
replacedBy: 'opencli demo new',
|
|
148
150
|
},
|
|
149
151
|
]);
|
|
152
|
+
// Verify sourceFile is included
|
|
153
|
+
expect(entries[0].sourceFile).toBeDefined();
|
|
150
154
|
getRegistry().delete(key);
|
|
151
155
|
});
|
|
152
156
|
it('keeps every command a module exports instead of guessing by site', async () => {
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Dynamic adapter commands are registered via commanderAdapter.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
+
import { findPackageRoot } from './package-paths.js';
|
|
8
9
|
export declare function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command;
|
|
9
10
|
export declare function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void;
|
|
10
11
|
export interface OperateVerifyInvocation {
|
|
@@ -13,7 +14,7 @@ export interface OperateVerifyInvocation {
|
|
|
13
14
|
cwd: string;
|
|
14
15
|
shell?: boolean;
|
|
15
16
|
}
|
|
16
|
-
export
|
|
17
|
+
export { findPackageRoot };
|
|
17
18
|
export declare function resolveOperateVerifyInvocation(opts?: {
|
|
18
19
|
projectRoot?: string;
|
|
19
20
|
platform?: NodeJS.Platform;
|
package/dist/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
+
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
12
13
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
13
14
|
import { serializeCommand, formatArgSummary } from './serialization.js';
|
|
14
15
|
import { render as renderOutput } from './output.js';
|
|
@@ -268,13 +269,17 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
268
269
|
const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=50000,F=window.fetch;window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();if(window.__opencli_net.length<M){var b=null;if(t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),method:(arguments[1]&&arguments[1].method)||'GET',status:r.status,size:t.length,ct:ct,body:b})}}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if((ct.includes('json')||ct.includes('text'))&&window.__opencli_net.length<M){var t=x.responseText,b=null;if(t&&t.length<=B)try{b=JSON.parse(t)}catch(e){b=t}window.__opencli_net.push({url:x._ou,method:x._om||'GET',status:x.status,size:t?t.length:0,ct:ct,body:b})}}catch(e){}});return S.apply(this,arguments)}})()`;
|
|
269
270
|
operate.command('open').argument('<url>').description('Open URL in automation window')
|
|
270
271
|
.action(operateAction(async (page, url) => {
|
|
272
|
+
// Start session-level capture before navigation (catches initial requests)
|
|
273
|
+
const hasSessionCapture = await page.startNetworkCapture?.().then(() => true).catch(() => false);
|
|
271
274
|
await page.goto(url);
|
|
272
275
|
await page.wait(2);
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
// Fallback: inject JS interceptor when session capture is unavailable
|
|
277
|
+
if (!hasSessionCapture) {
|
|
278
|
+
try {
|
|
279
|
+
await page.evaluate(NETWORK_INTERCEPTOR_JS);
|
|
280
|
+
}
|
|
281
|
+
catch { /* non-fatal */ }
|
|
276
282
|
}
|
|
277
|
-
catch { /* non-fatal */ }
|
|
278
283
|
console.log(`Navigated to: ${await page.getCurrentUrl?.() ?? url}`);
|
|
279
284
|
}));
|
|
280
285
|
operate.command('back').description('Go back in browser history')
|
|
@@ -456,17 +461,46 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
456
461
|
.option('--all', 'Show all requests including static resources')
|
|
457
462
|
.description('Show captured network requests (auto-captured since last open)')
|
|
458
463
|
.action(operateAction(async (page, opts) => {
|
|
459
|
-
const requests = await page.evaluate(`(function(){
|
|
460
|
-
var reqs = window.__opencli_net || [];
|
|
461
|
-
return JSON.stringify(reqs);
|
|
462
|
-
})()`);
|
|
463
464
|
let items = [];
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
if (page.readNetworkCapture) {
|
|
466
|
+
const raw = await page.readNetworkCapture();
|
|
467
|
+
// Normalize daemon/CDP capture entries to __opencli_net shape.
|
|
468
|
+
// Daemon returns: responseStatus, responseContentType, responsePreview
|
|
469
|
+
// CDP returns the same shape after PR A fix.
|
|
470
|
+
items = raw.map(e => {
|
|
471
|
+
const preview = e.responsePreview ?? null;
|
|
472
|
+
let body = null;
|
|
473
|
+
if (preview) {
|
|
474
|
+
try {
|
|
475
|
+
body = JSON.parse(preview);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
body = preview;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
url: e.url || '',
|
|
483
|
+
method: e.method || 'GET',
|
|
484
|
+
status: e.responseStatus || 0,
|
|
485
|
+
size: preview ? preview.length : 0,
|
|
486
|
+
ct: e.responseContentType || '',
|
|
487
|
+
body,
|
|
488
|
+
};
|
|
489
|
+
});
|
|
466
490
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
491
|
+
else {
|
|
492
|
+
// Fallback to JS interceptor data
|
|
493
|
+
const requests = await page.evaluate(`(function(){
|
|
494
|
+
var reqs = window.__opencli_net || [];
|
|
495
|
+
return JSON.stringify(reqs);
|
|
496
|
+
})()`);
|
|
497
|
+
try {
|
|
498
|
+
items = JSON.parse(requests);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
console.log('No network data captured. Run "operate open <url>" first.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
470
504
|
}
|
|
471
505
|
if (items.length === 0) {
|
|
472
506
|
console.log('No requests captured.');
|
|
@@ -943,39 +977,7 @@ cli({
|
|
|
943
977
|
export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
944
978
|
createProgram(BUILTIN_CLIS, USER_CLIS).parse();
|
|
945
979
|
}
|
|
946
|
-
export
|
|
947
|
-
let dir = path.dirname(startFile);
|
|
948
|
-
while (true) {
|
|
949
|
-
if (fileExists(path.join(dir, 'package.json')))
|
|
950
|
-
return dir;
|
|
951
|
-
const parent = path.dirname(dir);
|
|
952
|
-
if (parent === dir) {
|
|
953
|
-
throw new Error(`Could not find package.json above ${startFile}`);
|
|
954
|
-
}
|
|
955
|
-
dir = parent;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function getBuiltEntryCandidates(packageRoot, readFile) {
|
|
959
|
-
const candidates = [];
|
|
960
|
-
try {
|
|
961
|
-
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json')));
|
|
962
|
-
if (typeof pkg.bin === 'string') {
|
|
963
|
-
candidates.push(path.join(packageRoot, pkg.bin));
|
|
964
|
-
}
|
|
965
|
-
else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
|
|
966
|
-
candidates.push(path.join(packageRoot, pkg.bin.opencli));
|
|
967
|
-
}
|
|
968
|
-
if (typeof pkg.main === 'string') {
|
|
969
|
-
candidates.push(path.join(packageRoot, pkg.main));
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
catch {
|
|
973
|
-
// Fall through to compatibility candidates below.
|
|
974
|
-
}
|
|
975
|
-
// Compatibility fallback for partially-built trees or older layouts.
|
|
976
|
-
candidates.push(path.join(packageRoot, 'dist', 'src', 'main.js'), path.join(packageRoot, 'dist', 'main.js'));
|
|
977
|
-
return [...new Set(candidates)];
|
|
978
|
-
}
|
|
980
|
+
export { findPackageRoot };
|
|
979
981
|
export function resolveOperateVerifyInvocation(opts = {}) {
|
|
980
982
|
const platform = opts.platform ?? process.platform;
|
|
981
983
|
const fileExists = opts.fileExists ?? fs.existsSync;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { executePipeline } from '../../pipeline/index.js';
|
|
5
|
+
function loadPipeline(name) {
|
|
6
|
+
const file = new URL(`./${name}.yaml`, import.meta.url);
|
|
7
|
+
const def = yaml.load(fs.readFileSync(file, 'utf-8'));
|
|
8
|
+
return def.pipeline;
|
|
9
|
+
}
|
|
10
|
+
function mockJsonOnce(payload) {
|
|
11
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
statusText: 'OK',
|
|
15
|
+
json: vi.fn().mockResolvedValue(payload),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
describe('binance YAML adapters', () => {
|
|
23
|
+
it('sorts top pairs by numeric quote volume', async () => {
|
|
24
|
+
mockJsonOnce([
|
|
25
|
+
{ symbol: 'SMALL', lastPrice: '1', priceChangePercent: '1.2', highPrice: '1', lowPrice: '1', quoteVolume: '9.9' },
|
|
26
|
+
{ symbol: 'LARGE', lastPrice: '2', priceChangePercent: '2.3', highPrice: '2', lowPrice: '2', quoteVolume: '100.0' },
|
|
27
|
+
{ symbol: 'MID', lastPrice: '3', priceChangePercent: '3.4', highPrice: '3', lowPrice: '3', quoteVolume: '11.0' },
|
|
28
|
+
]);
|
|
29
|
+
const result = await executePipeline(null, loadPipeline('top'), { args: { limit: 3 } });
|
|
30
|
+
expect(result.map((item) => item.symbol)).toEqual(['LARGE', 'MID', 'SMALL']);
|
|
31
|
+
expect(result.map((item) => item.rank)).toEqual([1, 2, 3]);
|
|
32
|
+
});
|
|
33
|
+
it('sorts gainers by numeric percent change', async () => {
|
|
34
|
+
mockJsonOnce([
|
|
35
|
+
{ symbol: 'TEN', lastPrice: '1', priceChangePercent: '10.0', quoteVolume: '100' },
|
|
36
|
+
{ symbol: 'NINE', lastPrice: '1', priceChangePercent: '9.5', quoteVolume: '100' },
|
|
37
|
+
{ symbol: 'HUNDRED', lastPrice: '1', priceChangePercent: '100.0', quoteVolume: '100' },
|
|
38
|
+
]);
|
|
39
|
+
const result = await executePipeline(null, loadPipeline('gainers'), { args: { limit: 3 } });
|
|
40
|
+
expect(result.map((item) => item.symbol)).toEqual(['HUNDRED', 'TEN', 'NINE']);
|
|
41
|
+
});
|
|
42
|
+
it('keeps only TRADING pairs', async () => {
|
|
43
|
+
mockJsonOnce({
|
|
44
|
+
symbols: [
|
|
45
|
+
{ symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', status: 'TRADING' },
|
|
46
|
+
{ symbol: 'OLDPAIR', baseAsset: 'OLD', quoteAsset: 'USDT', status: 'BREAK' },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const result = await executePipeline(null, loadPipeline('pairs'), { args: { limit: 10 } });
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{ symbol: 'BTCUSDT', base: 'BTC', quote: 'USDT', status: 'TRADING' },
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -15,7 +15,8 @@ import { formatRegistryHelpText } from './serialization.js';
|
|
|
15
15
|
import { render as renderOutput } from './output.js';
|
|
16
16
|
import { executeCommand } from './execution.js';
|
|
17
17
|
import { CliError, EXIT_CODES, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
|
|
18
|
-
import {
|
|
18
|
+
import { getDaemonHealth } from './browser/daemon-client.js';
|
|
19
|
+
import { isDiagnosticEnabled } from './diagnostic.js';
|
|
19
20
|
export function normalizeArgValue(argType, value, name) {
|
|
20
21
|
if (argType !== 'bool' && argType !== 'boolean')
|
|
21
22
|
return value;
|
|
@@ -169,18 +170,27 @@ function renderBridgeStatus(running, extensionConnected) {
|
|
|
169
170
|
console.error(chalk.dim(' Try reloading the extension, or run: opencli doctor'));
|
|
170
171
|
}
|
|
171
172
|
}
|
|
173
|
+
/** Emit AutoFix hint for repairable adapter errors (skipped if already in diagnostic mode). */
|
|
174
|
+
function emitAutoFixHint(cmdName) {
|
|
175
|
+
if (isDiagnosticEnabled())
|
|
176
|
+
return; // Already collecting diagnostics, don't repeat
|
|
177
|
+
console.error();
|
|
178
|
+
console.error(chalk.cyan('💡 AutoFix: re-run with OPENCLI_DIAGNOSTIC=1 for repair context.'));
|
|
179
|
+
console.error(chalk.dim(` OPENCLI_DIAGNOSTIC=1 ${cmdName}`));
|
|
180
|
+
}
|
|
172
181
|
async function renderError(err, cmdName, verbose) {
|
|
173
182
|
// ── BrowserConnectError: real-time diagnosis, kind as fallback ────────
|
|
174
183
|
if (err instanceof BrowserConnectError) {
|
|
175
|
-
console.error(chalk.red(
|
|
184
|
+
console.error(chalk.red(`🔌 ${err.message}`));
|
|
185
|
+
if (err.hint)
|
|
186
|
+
console.error(chalk.yellow(`→ ${err.hint}`));
|
|
176
187
|
console.error();
|
|
177
188
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
renderBridgeStatus(status.running, status.extensionConnected);
|
|
189
|
+
const health = await getDaemonHealth({ timeout: 300 });
|
|
190
|
+
renderBridgeStatus(health.state !== 'stopped', health.state === 'ready');
|
|
181
191
|
}
|
|
182
192
|
catch (_statusErr) {
|
|
183
|
-
//
|
|
193
|
+
// getDaemonHealth itself failed — derive best-guess state from kind.
|
|
184
194
|
const running = err.kind !== 'daemon-not-running';
|
|
185
195
|
const extensionConnected = err.kind === 'command-failed';
|
|
186
196
|
renderBridgeStatus(running, extensionConnected);
|
|
@@ -208,6 +218,7 @@ async function renderError(err, cmdName, verbose) {
|
|
|
208
218
|
console.error(chalk.yellow(`→ ${err.hint ?? 'The page structure may have changed — this adapter may be outdated.'}`));
|
|
209
219
|
console.error(chalk.dim(` Debug: ${cmdName} --verbose`));
|
|
210
220
|
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
221
|
+
emitAutoFixHint(cmdName);
|
|
211
222
|
return;
|
|
212
223
|
}
|
|
213
224
|
// ── ArgumentError ─────────────────────────────────────────────────────
|
|
@@ -251,6 +262,8 @@ async function renderError(err, cmdName, verbose) {
|
|
|
251
262
|
console.error(chalk.yellow(`→ ${classified.hint}`));
|
|
252
263
|
if (classified.kind === 'not-found')
|
|
253
264
|
console.error(chalk.dim(` Report: ${ISSUES_URL}`));
|
|
265
|
+
if (classified.kind === 'not-found')
|
|
266
|
+
emitAutoFixHint(cmdName);
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
256
269
|
// ── Unknown error: show stack in verbose mode ─────────────────────────
|
|
@@ -6,15 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { fetchDaemonStatus, requestDaemonShutdown } from '../browser/daemon-client.js';
|
|
9
|
-
|
|
10
|
-
const h = Math.floor(seconds / 3600);
|
|
11
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
12
|
-
if (h > 0)
|
|
13
|
-
return `${h}h ${m}m`;
|
|
14
|
-
if (m > 0)
|
|
15
|
-
return `${m}m`;
|
|
16
|
-
return `${Math.floor(seconds)}s`;
|
|
17
|
-
}
|
|
9
|
+
import { formatDuration } from '../download/progress.js';
|
|
18
10
|
function formatTimeSince(timestampMs) {
|
|
19
11
|
const seconds = (Date.now() - timestampMs) / 1000;
|
|
20
12
|
if (seconds < 60)
|
|
@@ -32,7 +24,7 @@ export async function daemonStatus() {
|
|
|
32
24
|
return;
|
|
33
25
|
}
|
|
34
26
|
console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
|
|
35
|
-
console.log(`Uptime: ${
|
|
27
|
+
console.log(`Uptime: ${formatDuration(Math.round(status.uptime * 1000))}`);
|
|
36
28
|
console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
|
|
37
29
|
console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
|
|
38
30
|
console.log(`Memory: ${status.memoryMB} MB`);
|
package/dist/src/diagnostic.d.ts
CHANGED
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
* When OPENCLI_DIAGNOSTIC=1, failed commands emit a JSON RepairContext to stderr
|
|
5
5
|
* containing the error, adapter source, and browser state (DOM snapshot, network
|
|
6
6
|
* requests, console errors). AI Agents consume this to diagnose and fix adapters.
|
|
7
|
+
*
|
|
8
|
+
* Safety boundaries:
|
|
9
|
+
* - Sensitive headers/cookies are redacted before emission
|
|
10
|
+
* - Individual fields are capped to prevent unbounded output
|
|
11
|
+
* - Network response bodies from authenticated requests are stripped
|
|
12
|
+
* - Total output is capped to MAX_DIAGNOSTIC_BYTES
|
|
7
13
|
*/
|
|
8
14
|
import type { IPage } from './types.js';
|
|
9
15
|
import type { InternalCliCommand } from './registry.js';
|
|
16
|
+
/** Maximum bytes for the entire diagnostic JSON output. */
|
|
17
|
+
export declare const MAX_DIAGNOSTIC_BYTES: number;
|
|
10
18
|
export interface RepairContext {
|
|
11
19
|
error: {
|
|
12
20
|
code: string;
|
|
@@ -24,15 +32,33 @@ export interface RepairContext {
|
|
|
24
32
|
url: string;
|
|
25
33
|
snapshot: string;
|
|
26
34
|
networkRequests: unknown[];
|
|
35
|
+
capturedPayloads?: unknown[];
|
|
27
36
|
consoleErrors: unknown[];
|
|
28
37
|
};
|
|
29
38
|
timestamp: string;
|
|
30
39
|
}
|
|
40
|
+
/** Truncate a string to maxLen, appending a truncation marker. */
|
|
41
|
+
export declare function truncate(str: string, maxLen: number): string;
|
|
42
|
+
/** Redact sensitive query parameters from a URL. */
|
|
43
|
+
export declare function redactUrl(url: string): string;
|
|
44
|
+
/** Redact inline secrets from free-text strings (error messages, stack traces, console output, DOM). */
|
|
45
|
+
export declare function redactText(text: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the editable source file path for an adapter.
|
|
48
|
+
*
|
|
49
|
+
* Priority:
|
|
50
|
+
* 1. cmd.source (set for FS-scanned YAML/TS and manifest lazy-loaded TS)
|
|
51
|
+
* 2. cmd._modulePath (set for manifest lazy-loaded TS, points to dist/)
|
|
52
|
+
*
|
|
53
|
+
* For dist/ paths, attempt to map back to the original .ts source file.
|
|
54
|
+
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveAdapterSourcePath(cmd: InternalCliCommand): string | undefined;
|
|
31
57
|
/** Whether diagnostic mode is enabled. */
|
|
32
58
|
export declare function isDiagnosticEnabled(): boolean;
|
|
33
59
|
/** Build a RepairContext from an error, command metadata, and optional page state. */
|
|
34
60
|
export declare function buildRepairContext(err: unknown, cmd: InternalCliCommand, pageState?: RepairContext['page']): RepairContext;
|
|
35
|
-
/** Collect full diagnostic context including page state. */
|
|
61
|
+
/** Collect full diagnostic context including page state (with timeout). */
|
|
36
62
|
export declare function collectDiagnostic(err: unknown, cmd: InternalCliCommand, page: IPage | null): Promise<RepairContext>;
|
|
37
|
-
/** Emit diagnostic JSON to stderr. */
|
|
63
|
+
/** Emit diagnostic JSON to stderr, enforcing total size cap. */
|
|
38
64
|
export declare function emitDiagnostic(ctx: RepairContext): void;
|