@jackwener/opencli 1.5.4 → 1.5.5
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/dist/cli-manifest.json +55 -0
- package/dist/cli.js +14 -14
- package/dist/clis/antigravity/serve.js +2 -2
- package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
- package/dist/clis/sinafinance/rolling-news.js +40 -0
- package/dist/clis/sinafinance/stock.d.ts +8 -0
- package/dist/clis/sinafinance/stock.js +117 -0
- package/dist/commanderAdapter.js +26 -3
- package/dist/daemon.js +5 -4
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +49 -11
- package/dist/external.js +3 -3
- package/dist/main.js +2 -1
- package/dist/tui.js +2 -1
- package/docs/adapters/browser/sinafinance.md +56 -6
- package/extension/dist/background.js +1 -2
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.ts +2 -1
- package/package.json +1 -1
- package/src/cli.ts +14 -14
- package/src/clis/antigravity/serve.ts +2 -2
- package/src/clis/sinafinance/rolling-news.ts +42 -0
- package/src/clis/sinafinance/stock.ts +127 -0
- package/src/commanderAdapter.ts +25 -2
- package/src/daemon.ts +5 -4
- package/src/errors.ts +71 -10
- package/src/external.ts +3 -3
- package/src/main.ts +2 -1
- package/src/tui.ts +2 -1
package/dist/tui.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses raw stdin mode + ANSI escape codes for interactive prompts.
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { EXIT_CODES } from './errors.js';
|
|
7
8
|
/**
|
|
8
9
|
* Interactive multi-select checkbox prompt.
|
|
9
10
|
*
|
|
@@ -130,7 +131,7 @@ export async function checkboxPrompt(items, opts = {}) {
|
|
|
130
131
|
// Ctrl+C — exit process
|
|
131
132
|
if (key === '\x03') {
|
|
132
133
|
cleanup();
|
|
133
|
-
process.exit(
|
|
134
|
+
process.exit(EXIT_CODES.INTERRUPTED);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
stdin.on('data', onData);
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# 新浪财经 (Sina Finance)
|
|
2
2
|
|
|
3
|
-
**Mode**: 🌐 Public · **Domain**: `finance.sina.com.cn`
|
|
3
|
+
**Mode**: 🌐 Public / 🔐 Browser · **Domain**: `finance.sina.com.cn`
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
7
|
-
| Command | Description |
|
|
8
|
-
|
|
9
|
-
| `opencli sinafinance news` | 新浪财经 7×24 小时实时快讯 |
|
|
7
|
+
| Command | Description | Mode |
|
|
8
|
+
|---------|-------------|------|
|
|
9
|
+
| `opencli sinafinance news` | 新浪财经 7×24 小时实时快讯 | 🌐 Public |
|
|
10
|
+
| `opencli sinafinance rolling-news` | 新浪财经滚动新闻 | 🔐 Browser |
|
|
11
|
+
| `opencli sinafinance stock` | 新浪财经行情(A股/港股/美股) | 🌐 Public |
|
|
10
12
|
|
|
11
13
|
## Usage Examples
|
|
12
14
|
|
|
15
|
+
### news - 7×24 实时快讯
|
|
16
|
+
|
|
13
17
|
```bash
|
|
14
18
|
# Latest financial news
|
|
15
19
|
opencli sinafinance news --limit 20
|
|
@@ -23,13 +27,59 @@ opencli sinafinance news --type 6 # 国际
|
|
|
23
27
|
opencli sinafinance news -f json
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
###
|
|
30
|
+
### rolling-news - 滚动新闻
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Rolling news feed
|
|
34
|
+
opencli sinafinance rolling-news
|
|
35
|
+
|
|
36
|
+
# JSON output
|
|
37
|
+
opencli sinafinance rolling-news -f json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### stock - 股票行情
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Search and view A-share stock
|
|
44
|
+
opencli sinafinance stock 贵州茅台 --market cn
|
|
45
|
+
|
|
46
|
+
# Search and view HK stock
|
|
47
|
+
opencli sinafinance stock 腾讯控股 --market hk
|
|
48
|
+
|
|
49
|
+
# Search and view US stock
|
|
50
|
+
opencli sinafinance stock aapl --market us
|
|
51
|
+
|
|
52
|
+
# Auto-detect market (searches cn, hk, us in order)
|
|
53
|
+
opencli sinafinance stock 招商证券
|
|
54
|
+
|
|
55
|
+
# JSON output
|
|
56
|
+
opencli sinafinance stock 贵州茅台 -f json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
### news
|
|
27
62
|
|
|
28
63
|
| Option | Description |
|
|
29
64
|
|--------|-------------|
|
|
30
65
|
| `--limit` | Max results, up to 50 (default: 20) |
|
|
31
66
|
| `--type` | News type: `0`=全部, `1`=A股, `2`=宏观, `3`=公司, `4`=数据, `5`=市场, `6`=国际, `7`=观点, `8`=央行, `9`=其它 |
|
|
32
67
|
|
|
68
|
+
### stock
|
|
69
|
+
|
|
70
|
+
| Option | Description |
|
|
71
|
+
|--------|-------------|
|
|
72
|
+
| `--market` | Market: `cn`, `hk`, `us`, `auto` (default: auto). When `auto`, searches in cn, hk, us order |
|
|
73
|
+
|
|
33
74
|
## Prerequisites
|
|
34
75
|
|
|
35
|
-
- No browser required — uses public API
|
|
76
|
+
- `news` & `stock`: No browser required — uses public API
|
|
77
|
+
- `rolling-news`: Chrome running and **logged into** `finance.sina.com.cn`
|
|
78
|
+
- For `rolling-news`: [Browser Bridge extension](/guide/browser-bridge) installed
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- `news` and `stock` use public APIs — no browser or login needed
|
|
83
|
+
- `stock` supports Chinese names, Chinese codes, and ticker symbols; auto-detects market
|
|
84
|
+
- Market priority for auto-detection: cn (A股) → hk (港股) → us (美股)
|
|
85
|
+
- US stock `High`/`Low` columns show 52-week range; A股/港股 show today's range
|
package/extension/manifest.json
CHANGED
package/extension/package.json
CHANGED
|
@@ -160,13 +160,14 @@ async function getAutomationWindow(workspace: string): Promise<number> {
|
|
|
160
160
|
|
|
161
161
|
// Create a new window with a data: URI that New Tab Override extensions cannot intercept.
|
|
162
162
|
// Using about:blank would be hijacked by extensions like "New Tab Override".
|
|
163
|
+
// Note: Do NOT set `state` parameter here. Chrome 146+ rejects 'normal' as an invalid
|
|
164
|
+
// state value for windows.create(). The window defaults to 'normal' state anyway.
|
|
163
165
|
const win = await chrome.windows.create({
|
|
164
166
|
url: BLANK_PAGE,
|
|
165
167
|
focused: false,
|
|
166
168
|
width: 1280,
|
|
167
169
|
height: 900,
|
|
168
170
|
type: 'normal',
|
|
169
|
-
state: 'normal',
|
|
170
171
|
});
|
|
171
172
|
const session: AutomationSession = {
|
|
172
173
|
windowId: win.id!,
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { PKG_VERSION } from './version.js';
|
|
|
15
15
|
import { printCompletionScript } from './completion.js';
|
|
16
16
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
17
17
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
18
|
-
import { getErrorMessage } from './errors.js';
|
|
18
|
+
import { EXIT_CODES, getErrorMessage } from './errors.js';
|
|
19
19
|
|
|
20
20
|
export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
21
21
|
const program = new Command();
|
|
@@ -120,7 +120,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
120
120
|
const { verifyClis, renderVerifyReport } = await import('./verify.js');
|
|
121
121
|
const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
|
|
122
122
|
console.log(renderVerifyReport(r));
|
|
123
|
-
process.exitCode = r.ok ?
|
|
123
|
+
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// ── Built-in: explore / synthesize / generate / cascade ───────────────────
|
|
@@ -180,7 +180,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
180
180
|
workspace,
|
|
181
181
|
});
|
|
182
182
|
console.log(renderGenerateSummary(r));
|
|
183
|
-
process.exitCode = r.ok ?
|
|
183
|
+
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
// ── Built-in: record ─────────────────────────────────────────────────────
|
|
@@ -204,7 +204,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
204
204
|
timeoutMs: parseInt(opts.timeout, 10),
|
|
205
205
|
});
|
|
206
206
|
console.log(renderRecordSummary(result));
|
|
207
|
-
process.exitCode = result.candidateCount > 0 ?
|
|
207
|
+
process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
program
|
|
@@ -272,7 +272,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
272
272
|
}
|
|
273
273
|
} catch (err) {
|
|
274
274
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
275
|
-
process.exitCode =
|
|
275
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
|
|
@@ -287,7 +287,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
287
287
|
console.log(chalk.green(`✅ Plugin "${name}" uninstalled.`));
|
|
288
288
|
} catch (err) {
|
|
289
289
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
290
|
-
process.exitCode =
|
|
290
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
291
291
|
}
|
|
292
292
|
});
|
|
293
293
|
|
|
@@ -299,12 +299,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
299
299
|
.action(async (name: string | undefined, opts: { all?: boolean }) => {
|
|
300
300
|
if (!name && !opts.all) {
|
|
301
301
|
console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
|
|
302
|
-
process.exitCode =
|
|
302
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
305
|
if (name && opts.all) {
|
|
306
306
|
console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
|
|
307
|
-
process.exitCode =
|
|
307
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
|
|
@@ -335,7 +335,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
335
335
|
console.log();
|
|
336
336
|
if (hasErrors) {
|
|
337
337
|
console.error(chalk.red('Completed with some errors.'));
|
|
338
|
-
process.exitCode =
|
|
338
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
339
339
|
} else {
|
|
340
340
|
console.log(chalk.green('✅ All plugins updated successfully.'));
|
|
341
341
|
}
|
|
@@ -348,7 +348,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
348
348
|
console.log(chalk.green(`✅ Plugin "${name}" updated successfully.`));
|
|
349
349
|
} catch (err) {
|
|
350
350
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
351
|
-
process.exitCode =
|
|
351
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
352
352
|
}
|
|
353
353
|
});
|
|
354
354
|
|
|
@@ -438,7 +438,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
438
438
|
console.log(chalk.dim(` opencli ${name} hello`));
|
|
439
439
|
} catch (err) {
|
|
440
440
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
441
|
-
process.exitCode =
|
|
441
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
442
442
|
}
|
|
443
443
|
});
|
|
444
444
|
|
|
@@ -454,7 +454,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
454
454
|
const ext = externalClis.find(e => e.name === name);
|
|
455
455
|
if (!ext) {
|
|
456
456
|
console.error(chalk.red(`External CLI '${name}' not found in registry.`));
|
|
457
|
-
process.exitCode =
|
|
457
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
458
458
|
return;
|
|
459
459
|
}
|
|
460
460
|
installExternalCli(ext);
|
|
@@ -480,7 +480,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
480
480
|
executeExternalCli(name, args, externalClis);
|
|
481
481
|
} catch (err) {
|
|
482
482
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
483
|
-
process.exitCode =
|
|
483
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -525,7 +525,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
525
525
|
console.error(chalk.dim(` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
|
|
526
526
|
}
|
|
527
527
|
program.outputHelp();
|
|
528
|
-
process.exitCode =
|
|
528
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
529
529
|
});
|
|
530
530
|
|
|
531
531
|
program.parse();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
14
14
|
import { CDPBridge } from '../../browser/cdp.js';
|
|
15
15
|
import type { IPage } from '../../types.js';
|
|
16
|
-
import { getErrorMessage } from '../../errors.js';
|
|
16
|
+
import { EXIT_CODES, getErrorMessage } from '../../errors.js';
|
|
17
17
|
|
|
18
18
|
// ─── Types ───────────────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -594,7 +594,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
|
|
594
594
|
console.error('\n[serve] Shutting down...');
|
|
595
595
|
cdp?.close().catch(() => {});
|
|
596
596
|
server.close();
|
|
597
|
-
process.exit(
|
|
597
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
598
598
|
};
|
|
599
599
|
process.on('SIGTERM', shutdown);
|
|
600
600
|
process.on('SIGINT', shutdown);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinafinance rolling news feed
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'sinafinance',
|
|
9
|
+
name: 'rolling-news',
|
|
10
|
+
description: '新浪财经滚动新闻',
|
|
11
|
+
domain: 'finance.sina.com.cn/roll',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [],
|
|
14
|
+
columns: ['column', 'title', 'date', 'url'],
|
|
15
|
+
func: async (page, _args) => {
|
|
16
|
+
await page.goto(`https://finance.sina.com.cn/roll/#pageid=384&lid=2519`);
|
|
17
|
+
await page.wait({ selector: '.d_list_txt li', timeout: 10000 });
|
|
18
|
+
|
|
19
|
+
const payload = await page.evaluate(`
|
|
20
|
+
(() => {
|
|
21
|
+
const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
22
|
+
const results = [];
|
|
23
|
+
document.querySelectorAll('.d_list_txt li').forEach(el => {
|
|
24
|
+
const titleEl = el.querySelector('.c_tit a');
|
|
25
|
+
const columnEl = el.querySelector('.c_chl');
|
|
26
|
+
const dateEl = el.querySelector('.c_time');
|
|
27
|
+
const url = titleEl?.getAttribute('href') || '';
|
|
28
|
+
if (!url) return;
|
|
29
|
+
results.push({
|
|
30
|
+
title: cleanText(titleEl?.textContent || ''),
|
|
31
|
+
column: cleanText(columnEl?.textContent || ''),
|
|
32
|
+
date: cleanText(dateEl?.textContent || ''),
|
|
33
|
+
url: url,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return results;
|
|
37
|
+
})()
|
|
38
|
+
`);
|
|
39
|
+
if (!Array.isArray(payload)) return [];
|
|
40
|
+
return payload;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinafinance stock quote — A股 / 港股 / 美股
|
|
3
|
+
*
|
|
4
|
+
* Uses two public Sina APIs (no browser required):
|
|
5
|
+
* suggest3.sinajs.cn — symbol search
|
|
6
|
+
* hq.sinajs.cn — real-time quote
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cli, Strategy } from '../../registry.js';
|
|
10
|
+
import { CliError } from '../../errors.js';
|
|
11
|
+
|
|
12
|
+
const MARKET_CN = '11';
|
|
13
|
+
const MARKET_HK = '31';
|
|
14
|
+
const MARKET_US = '41';
|
|
15
|
+
|
|
16
|
+
async function fetchGBK(url: string): Promise<string> {
|
|
17
|
+
const res = await fetch(url, { headers: { Referer: 'https://finance.sina.com.cn' } });
|
|
18
|
+
if (!res.ok) throw new CliError('FETCH_ERROR', `Sina API HTTP ${res.status}`, 'Check your network');
|
|
19
|
+
const buf = await res.arrayBuffer();
|
|
20
|
+
return new TextDecoder('gbk').decode(buf);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SuggestEntry { name: string; market: string; symbol: string; }
|
|
24
|
+
|
|
25
|
+
function parseSuggest(raw: string, markets: string[]): SuggestEntry[] {
|
|
26
|
+
const m = raw.match(/suggestvalue="(.*)"/s);
|
|
27
|
+
if (!m) return [];
|
|
28
|
+
return m[1].split(';').filter(Boolean).map(s => {
|
|
29
|
+
const p = s.split(',');
|
|
30
|
+
return { name: p[4] || p[0] || '', market: p[1] || '', symbol: p[3] || '' };
|
|
31
|
+
}).filter(e => markets.includes(e.market));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hqSymbol(e: SuggestEntry): string {
|
|
35
|
+
if (e.market === MARKET_HK) return `hk${e.symbol}`;
|
|
36
|
+
if (e.market === MARKET_US) return `gb_${e.symbol}`;
|
|
37
|
+
return e.symbol; // A股: already "sh600519" / "sz300XXX"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseHq(raw: string, sym: string): string[] {
|
|
41
|
+
const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
const m = raw.match(new RegExp(`hq_str_${escaped}="([^"]*)"`));
|
|
43
|
+
return m ? m[1].split(',') : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fmtMktCap(val: string): string {
|
|
47
|
+
const n = parseFloat(val);
|
|
48
|
+
if (!n) return '';
|
|
49
|
+
if (n >= 1e12) return (n / 1e12).toFixed(2) + 'T';
|
|
50
|
+
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
|
51
|
+
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
|
52
|
+
return String(n);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cli({
|
|
56
|
+
site: 'sinafinance',
|
|
57
|
+
name: 'stock',
|
|
58
|
+
description: '新浪财经行情(A股/港股/美股)',
|
|
59
|
+
domain: 'suggest3.sinajs.cn,hq.sinajs.cn',
|
|
60
|
+
strategy: Strategy.PUBLIC,
|
|
61
|
+
browser: false,
|
|
62
|
+
args: [
|
|
63
|
+
{ name: 'key', type: 'string', required: true, positional: true, help: 'Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)' },
|
|
64
|
+
{ name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
|
|
65
|
+
],
|
|
66
|
+
columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
|
|
67
|
+
func: async (_page, args) => {
|
|
68
|
+
const key = String(args.key);
|
|
69
|
+
const market = String(args.market);
|
|
70
|
+
|
|
71
|
+
const marketMap: Record<string, string[]> = {
|
|
72
|
+
cn: [MARKET_CN], hk: [MARKET_HK], us: [MARKET_US],
|
|
73
|
+
auto: [MARKET_CN, MARKET_HK, MARKET_US],
|
|
74
|
+
};
|
|
75
|
+
const targetMarkets = marketMap[market];
|
|
76
|
+
if (!targetMarkets) {
|
|
77
|
+
throw new CliError('INPUT_ERROR', `Invalid market: "${market}"`, 'Expected cn, hk, us, or auto');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 1. Search symbol — only request the markets we care about
|
|
81
|
+
const suggestRaw = await fetchGBK(
|
|
82
|
+
`https://suggest3.sinajs.cn/suggest/type=${targetMarkets.join(',')}&key=${encodeURIComponent(key)}`
|
|
83
|
+
);
|
|
84
|
+
const entries = parseSuggest(suggestRaw, targetMarkets);
|
|
85
|
+
if (!entries.length) {
|
|
86
|
+
throw new CliError('NOT_FOUND', `No stock found for "${key}"`, 'Try a different name, code, or --market');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pick best match: score by name similarity, tiebreak by market priority
|
|
90
|
+
const needle = key.toLowerCase();
|
|
91
|
+
const score = (e: SuggestEntry): number => {
|
|
92
|
+
const n = e.name.toLowerCase();
|
|
93
|
+
if (n === needle) return 1;
|
|
94
|
+
if (n.includes(needle)) return needle.length / n.length;
|
|
95
|
+
return 0;
|
|
96
|
+
};
|
|
97
|
+
const best = entries.sort((a, b) => {
|
|
98
|
+
const d = score(b) - score(a);
|
|
99
|
+
return d !== 0 ? d : targetMarkets.indexOf(a.market) - targetMarkets.indexOf(b.market);
|
|
100
|
+
})[0];
|
|
101
|
+
|
|
102
|
+
// 2. Fetch quote
|
|
103
|
+
const sym = hqSymbol(best);
|
|
104
|
+
const hqRaw = await fetchGBK(`https://hq.sinajs.cn/list=${sym}`);
|
|
105
|
+
const f = parseHq(hqRaw, sym);
|
|
106
|
+
|
|
107
|
+
if (f.length < 2 || !f[0]) {
|
|
108
|
+
throw new CliError('NOT_FOUND', `No quote data for "${key}"`, 'Market may be closed or data unavailable');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (best.market === MARKET_CN) {
|
|
112
|
+
const price = parseFloat(f[3]);
|
|
113
|
+
const prev = parseFloat(f[2]);
|
|
114
|
+
const chg = (price - prev).toFixed(2);
|
|
115
|
+
const chgPct = ((price - prev) / prev * 100).toFixed(2) + '%';
|
|
116
|
+
return [{ Symbol: sym.toUpperCase(), Name: f[0], Price: f[3], Change: chg, ChangePercent: chgPct, Open: f[1], High: f[4], Low: f[5], Volume: f[8], MarketCap: '' }];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (best.market === MARKET_HK) {
|
|
120
|
+
// [2]=price [4]=high [5]=low [6]=open [7]=change [8]=change% [11]=volume
|
|
121
|
+
return [{ Symbol: best.symbol, Name: f[1], Price: f[2], Change: f[7], ChangePercent: f[8] + '%', Open: f[6], High: f[4], Low: f[5], Volume: f[11], MarketCap: '' }];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARKET_US: [1]=price [2]=change% [4]=change [6]=open [7]=today_low [8]=52wH [9]=52wL [10]=volume [12]=mktcap
|
|
125
|
+
return [{ Symbol: best.symbol.toUpperCase(), Name: f[0], Price: f[1], Change: f[4], ChangePercent: f[2] + '%', Open: f[6], High: f[8], Low: f[9], Volume: f[10], MarketCap: fmtMktCap(f[12]) }];
|
|
126
|
+
},
|
|
127
|
+
});
|
package/src/commanderAdapter.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { render as renderOutput } from './output.js';
|
|
|
18
18
|
import { executeCommand } from './execution.js';
|
|
19
19
|
import {
|
|
20
20
|
CliError,
|
|
21
|
+
EXIT_CODES,
|
|
21
22
|
ERROR_ICONS,
|
|
22
23
|
getErrorMessage,
|
|
23
24
|
BrowserConnectError,
|
|
@@ -40,7 +41,7 @@ export function normalizeArgValue(argType: string | undefined, value: unknown, n
|
|
|
40
41
|
if (normalized === 'true') return true;
|
|
41
42
|
if (normalized === 'false') return false;
|
|
42
43
|
|
|
43
|
-
throw new
|
|
44
|
+
throw new ArgumentError(`"${name}" must be either "true" or "false".`);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -117,11 +118,33 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
|
|
|
117
118
|
});
|
|
118
119
|
} catch (err) {
|
|
119
120
|
await renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
120
|
-
process.exitCode =
|
|
121
|
+
process.exitCode = resolveExitCode(err);
|
|
121
122
|
}
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// ── Exit code resolution ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Map any thrown value to a Unix process exit code.
|
|
130
|
+
*
|
|
131
|
+
* - CliError subclasses carry their own exitCode (set in errors.ts).
|
|
132
|
+
* - Generic Error objects are classified by message pattern so that
|
|
133
|
+
* un-typed auth / not-found errors from adapters still produce
|
|
134
|
+
* meaningful exit codes for shell scripts.
|
|
135
|
+
*/
|
|
136
|
+
function resolveExitCode(err: unknown): number {
|
|
137
|
+
if (err instanceof CliError) return err.exitCode;
|
|
138
|
+
|
|
139
|
+
// Pattern-based fallback for untyped errors thrown by third-party adapters.
|
|
140
|
+
const msg = getErrorMessage(err);
|
|
141
|
+
const kind = classifyGenericError(msg);
|
|
142
|
+
if (kind === 'auth') return EXIT_CODES.NOPERM;
|
|
143
|
+
if (kind === 'not-found') return EXIT_CODES.EMPTY_RESULT;
|
|
144
|
+
if (kind === 'http') return EXIT_CODES.GENERIC_ERROR; // HTTP 4xx/5xx → generic; renderer shows details
|
|
145
|
+
return EXIT_CODES.GENERIC_ERROR;
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
126
149
|
|
|
127
150
|
const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
|
package/src/daemon.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
23
23
|
import { WebSocketServer, WebSocket, type RawData } from 'ws';
|
|
24
24
|
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
25
|
+
import { EXIT_CODES } from './errors.js';
|
|
25
26
|
|
|
26
27
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
27
28
|
const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
@@ -53,7 +54,7 @@ function resetIdleTimer(): void {
|
|
|
53
54
|
if (idleTimer) clearTimeout(idleTimer);
|
|
54
55
|
idleTimer = setTimeout(() => {
|
|
55
56
|
console.error('[daemon] Idle timeout, shutting down');
|
|
56
|
-
process.exit(
|
|
57
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
57
58
|
}, IDLE_TIMEOUT);
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -303,10 +304,10 @@ httpServer.listen(PORT, '127.0.0.1', () => {
|
|
|
303
304
|
httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
|
304
305
|
if (err.code === 'EADDRINUSE') {
|
|
305
306
|
console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
|
|
306
|
-
process.exit(
|
|
307
|
+
process.exit(EXIT_CODES.SERVICE_UNAVAIL);
|
|
307
308
|
}
|
|
308
309
|
console.error('[daemon] Server error:', err.message);
|
|
309
|
-
process.exit(
|
|
310
|
+
process.exit(EXIT_CODES.GENERIC_ERROR);
|
|
310
311
|
});
|
|
311
312
|
|
|
312
313
|
// Graceful shutdown
|
|
@@ -319,7 +320,7 @@ function shutdown(): void {
|
|
|
319
320
|
pending.clear();
|
|
320
321
|
if (extensionWs) extensionWs.close();
|
|
321
322
|
httpServer.close();
|
|
322
|
-
process.exit(
|
|
323
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
process.on('SIGTERM', shutdown);
|