@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/cli-manifest.json
CHANGED
|
@@ -9282,6 +9282,61 @@
|
|
|
9282
9282
|
"type": "ts",
|
|
9283
9283
|
"modulePath": "sinafinance/news.js"
|
|
9284
9284
|
},
|
|
9285
|
+
{
|
|
9286
|
+
"site": "sinafinance",
|
|
9287
|
+
"name": "rolling-news",
|
|
9288
|
+
"description": "新浪财经滚动新闻",
|
|
9289
|
+
"domain": "finance.sina.com.cn/roll",
|
|
9290
|
+
"strategy": "cookie",
|
|
9291
|
+
"browser": true,
|
|
9292
|
+
"args": [],
|
|
9293
|
+
"columns": [
|
|
9294
|
+
"column",
|
|
9295
|
+
"title",
|
|
9296
|
+
"date",
|
|
9297
|
+
"url"
|
|
9298
|
+
],
|
|
9299
|
+
"type": "ts",
|
|
9300
|
+
"modulePath": "sinafinance/rolling-news.js"
|
|
9301
|
+
},
|
|
9302
|
+
{
|
|
9303
|
+
"site": "sinafinance",
|
|
9304
|
+
"name": "stock",
|
|
9305
|
+
"description": "新浪财经行情(A股/港股/美股)",
|
|
9306
|
+
"domain": "suggest3.sinajs.cn,hq.sinajs.cn",
|
|
9307
|
+
"strategy": "public",
|
|
9308
|
+
"browser": false,
|
|
9309
|
+
"args": [
|
|
9310
|
+
{
|
|
9311
|
+
"name": "key",
|
|
9312
|
+
"type": "string",
|
|
9313
|
+
"required": true,
|
|
9314
|
+
"positional": true,
|
|
9315
|
+
"help": "Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)"
|
|
9316
|
+
},
|
|
9317
|
+
{
|
|
9318
|
+
"name": "market",
|
|
9319
|
+
"type": "string",
|
|
9320
|
+
"default": "auto",
|
|
9321
|
+
"required": false,
|
|
9322
|
+
"help": "Market: cn, hk, us, auto (default: auto searches cn → hk → us)"
|
|
9323
|
+
}
|
|
9324
|
+
],
|
|
9325
|
+
"columns": [
|
|
9326
|
+
"Symbol",
|
|
9327
|
+
"Name",
|
|
9328
|
+
"Price",
|
|
9329
|
+
"Change",
|
|
9330
|
+
"ChangePercent",
|
|
9331
|
+
"Open",
|
|
9332
|
+
"High",
|
|
9333
|
+
"Low",
|
|
9334
|
+
"Volume",
|
|
9335
|
+
"MarketCap"
|
|
9336
|
+
],
|
|
9337
|
+
"type": "ts",
|
|
9338
|
+
"modulePath": "sinafinance/stock.js"
|
|
9339
|
+
},
|
|
9285
9340
|
{
|
|
9286
9341
|
"site": "smzdm",
|
|
9287
9342
|
"name": "search",
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { PKG_VERSION } from './version.js';
|
|
|
14
14
|
import { printCompletionScript } from './completion.js';
|
|
15
15
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
|
|
16
16
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
17
|
-
import { getErrorMessage } from './errors.js';
|
|
17
|
+
import { EXIT_CODES, getErrorMessage } from './errors.js';
|
|
18
18
|
export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
19
19
|
const program = new Command();
|
|
20
20
|
// enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
|
|
@@ -108,7 +108,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
108
108
|
const { verifyClis, renderVerifyReport } = await import('./verify.js');
|
|
109
109
|
const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
|
|
110
110
|
console.log(renderVerifyReport(r));
|
|
111
|
-
process.exitCode = r.ok ?
|
|
111
|
+
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
112
112
|
});
|
|
113
113
|
// ── Built-in: explore / synthesize / generate / cascade ───────────────────
|
|
114
114
|
program
|
|
@@ -164,7 +164,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
164
164
|
workspace,
|
|
165
165
|
});
|
|
166
166
|
console.log(renderGenerateSummary(r));
|
|
167
|
-
process.exitCode = r.ok ?
|
|
167
|
+
process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
|
|
168
168
|
});
|
|
169
169
|
// ── Built-in: record ─────────────────────────────────────────────────────
|
|
170
170
|
program
|
|
@@ -186,7 +186,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
186
186
|
timeoutMs: parseInt(opts.timeout, 10),
|
|
187
187
|
});
|
|
188
188
|
console.log(renderRecordSummary(result));
|
|
189
|
-
process.exitCode = result.candidateCount > 0 ?
|
|
189
|
+
process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
|
|
190
190
|
});
|
|
191
191
|
program
|
|
192
192
|
.command('cascade')
|
|
@@ -251,7 +251,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
251
251
|
}
|
|
252
252
|
catch (err) {
|
|
253
253
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
254
|
-
process.exitCode =
|
|
254
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
pluginCmd
|
|
@@ -266,7 +266,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
266
266
|
}
|
|
267
267
|
catch (err) {
|
|
268
268
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
269
|
-
process.exitCode =
|
|
269
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
270
270
|
}
|
|
271
271
|
});
|
|
272
272
|
pluginCmd
|
|
@@ -277,12 +277,12 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
277
277
|
.action(async (name, opts) => {
|
|
278
278
|
if (!name && !opts.all) {
|
|
279
279
|
console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
|
|
280
|
-
process.exitCode =
|
|
280
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
if (name && opts.all) {
|
|
284
284
|
console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
|
|
285
|
-
process.exitCode =
|
|
285
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
const { updatePlugin, updateAllPlugins } = await import('./plugin.js');
|
|
@@ -309,7 +309,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
309
309
|
console.log();
|
|
310
310
|
if (hasErrors) {
|
|
311
311
|
console.error(chalk.red('Completed with some errors.'));
|
|
312
|
-
process.exitCode =
|
|
312
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
313
313
|
}
|
|
314
314
|
else {
|
|
315
315
|
console.log(chalk.green('✅ All plugins updated successfully.'));
|
|
@@ -323,7 +323,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
323
323
|
}
|
|
324
324
|
catch (err) {
|
|
325
325
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
326
|
-
process.exitCode =
|
|
326
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
327
327
|
}
|
|
328
328
|
});
|
|
329
329
|
pluginCmd
|
|
@@ -408,7 +408,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
408
408
|
}
|
|
409
409
|
catch (err) {
|
|
410
410
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
411
|
-
process.exitCode =
|
|
411
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
412
412
|
}
|
|
413
413
|
});
|
|
414
414
|
// ── External CLIs ─────────────────────────────────────────────────────────
|
|
@@ -421,7 +421,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
421
421
|
const ext = externalClis.find(e => e.name === name);
|
|
422
422
|
if (!ext) {
|
|
423
423
|
console.error(chalk.red(`External CLI '${name}' not found in registry.`));
|
|
424
|
-
process.exitCode =
|
|
424
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
425
425
|
return;
|
|
426
426
|
}
|
|
427
427
|
installExternalCli(ext);
|
|
@@ -446,7 +446,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
446
446
|
}
|
|
447
447
|
catch (err) {
|
|
448
448
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
449
|
-
process.exitCode =
|
|
449
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
452
|
for (const ext of externalClis) {
|
|
@@ -485,7 +485,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
|
|
|
485
485
|
console.error(chalk.dim(` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
|
|
486
486
|
}
|
|
487
487
|
program.outputHelp();
|
|
488
|
-
process.exitCode =
|
|
488
|
+
process.exitCode = EXIT_CODES.USAGE_ERROR;
|
|
489
489
|
});
|
|
490
490
|
program.parse();
|
|
491
491
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { CDPBridge } from '../../browser/cdp.js';
|
|
14
|
-
import { getErrorMessage } from '../../errors.js';
|
|
14
|
+
import { EXIT_CODES, getErrorMessage } from '../../errors.js';
|
|
15
15
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
16
16
|
function generateMsgId() {
|
|
17
17
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
@@ -506,7 +506,7 @@ export async function startServe(opts = {}) {
|
|
|
506
506
|
console.error('\n[serve] Shutting down...');
|
|
507
507
|
cdp?.close().catch(() => { });
|
|
508
508
|
server.close();
|
|
509
|
-
process.exit(
|
|
509
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
510
510
|
};
|
|
511
511
|
process.on('SIGTERM', shutdown);
|
|
512
512
|
process.on('SIGINT', shutdown);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sinafinance rolling news feed
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: 'sinafinance',
|
|
7
|
+
name: 'rolling-news',
|
|
8
|
+
description: '新浪财经滚动新闻',
|
|
9
|
+
domain: 'finance.sina.com.cn/roll',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
args: [],
|
|
12
|
+
columns: ['column', 'title', 'date', 'url'],
|
|
13
|
+
func: async (page, _args) => {
|
|
14
|
+
await page.goto(`https://finance.sina.com.cn/roll/#pageid=384&lid=2519`);
|
|
15
|
+
await page.wait({ selector: '.d_list_txt li', timeout: 10000 });
|
|
16
|
+
const payload = await page.evaluate(`
|
|
17
|
+
(() => {
|
|
18
|
+
const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
19
|
+
const results = [];
|
|
20
|
+
document.querySelectorAll('.d_list_txt li').forEach(el => {
|
|
21
|
+
const titleEl = el.querySelector('.c_tit a');
|
|
22
|
+
const columnEl = el.querySelector('.c_chl');
|
|
23
|
+
const dateEl = el.querySelector('.c_time');
|
|
24
|
+
const url = titleEl?.getAttribute('href') || '';
|
|
25
|
+
if (!url) return;
|
|
26
|
+
results.push({
|
|
27
|
+
title: cleanText(titleEl?.textContent || ''),
|
|
28
|
+
column: cleanText(columnEl?.textContent || ''),
|
|
29
|
+
date: cleanText(dateEl?.textContent || ''),
|
|
30
|
+
url: url,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return results;
|
|
34
|
+
})()
|
|
35
|
+
`);
|
|
36
|
+
if (!Array.isArray(payload))
|
|
37
|
+
return [];
|
|
38
|
+
return payload;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CliError } from '../../errors.js';
|
|
10
|
+
const MARKET_CN = '11';
|
|
11
|
+
const MARKET_HK = '31';
|
|
12
|
+
const MARKET_US = '41';
|
|
13
|
+
async function fetchGBK(url) {
|
|
14
|
+
const res = await fetch(url, { headers: { Referer: 'https://finance.sina.com.cn' } });
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
throw new CliError('FETCH_ERROR', `Sina API HTTP ${res.status}`, 'Check your network');
|
|
17
|
+
const buf = await res.arrayBuffer();
|
|
18
|
+
return new TextDecoder('gbk').decode(buf);
|
|
19
|
+
}
|
|
20
|
+
function parseSuggest(raw, markets) {
|
|
21
|
+
const m = raw.match(/suggestvalue="(.*)"/s);
|
|
22
|
+
if (!m)
|
|
23
|
+
return [];
|
|
24
|
+
return m[1].split(';').filter(Boolean).map(s => {
|
|
25
|
+
const p = s.split(',');
|
|
26
|
+
return { name: p[4] || p[0] || '', market: p[1] || '', symbol: p[3] || '' };
|
|
27
|
+
}).filter(e => markets.includes(e.market));
|
|
28
|
+
}
|
|
29
|
+
function hqSymbol(e) {
|
|
30
|
+
if (e.market === MARKET_HK)
|
|
31
|
+
return `hk${e.symbol}`;
|
|
32
|
+
if (e.market === MARKET_US)
|
|
33
|
+
return `gb_${e.symbol}`;
|
|
34
|
+
return e.symbol; // A股: already "sh600519" / "sz300XXX"
|
|
35
|
+
}
|
|
36
|
+
function parseHq(raw, sym) {
|
|
37
|
+
const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
|
+
const m = raw.match(new RegExp(`hq_str_${escaped}="([^"]*)"`));
|
|
39
|
+
return m ? m[1].split(',') : [];
|
|
40
|
+
}
|
|
41
|
+
function fmtMktCap(val) {
|
|
42
|
+
const n = parseFloat(val);
|
|
43
|
+
if (!n)
|
|
44
|
+
return '';
|
|
45
|
+
if (n >= 1e12)
|
|
46
|
+
return (n / 1e12).toFixed(2) + 'T';
|
|
47
|
+
if (n >= 1e9)
|
|
48
|
+
return (n / 1e9).toFixed(2) + 'B';
|
|
49
|
+
if (n >= 1e6)
|
|
50
|
+
return (n / 1e6).toFixed(2) + 'M';
|
|
51
|
+
return String(n);
|
|
52
|
+
}
|
|
53
|
+
cli({
|
|
54
|
+
site: 'sinafinance',
|
|
55
|
+
name: 'stock',
|
|
56
|
+
description: '新浪财经行情(A股/港股/美股)',
|
|
57
|
+
domain: 'suggest3.sinajs.cn,hq.sinajs.cn',
|
|
58
|
+
strategy: Strategy.PUBLIC,
|
|
59
|
+
browser: false,
|
|
60
|
+
args: [
|
|
61
|
+
{ name: 'key', type: 'string', required: true, positional: true, help: 'Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)' },
|
|
62
|
+
{ name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
|
|
63
|
+
],
|
|
64
|
+
columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
|
|
65
|
+
func: async (_page, args) => {
|
|
66
|
+
const key = String(args.key);
|
|
67
|
+
const market = String(args.market);
|
|
68
|
+
const marketMap = {
|
|
69
|
+
cn: [MARKET_CN], hk: [MARKET_HK], us: [MARKET_US],
|
|
70
|
+
auto: [MARKET_CN, MARKET_HK, MARKET_US],
|
|
71
|
+
};
|
|
72
|
+
const targetMarkets = marketMap[market];
|
|
73
|
+
if (!targetMarkets) {
|
|
74
|
+
throw new CliError('INPUT_ERROR', `Invalid market: "${market}"`, 'Expected cn, hk, us, or auto');
|
|
75
|
+
}
|
|
76
|
+
// 1. Search symbol — only request the markets we care about
|
|
77
|
+
const suggestRaw = await fetchGBK(`https://suggest3.sinajs.cn/suggest/type=${targetMarkets.join(',')}&key=${encodeURIComponent(key)}`);
|
|
78
|
+
const entries = parseSuggest(suggestRaw, targetMarkets);
|
|
79
|
+
if (!entries.length) {
|
|
80
|
+
throw new CliError('NOT_FOUND', `No stock found for "${key}"`, 'Try a different name, code, or --market');
|
|
81
|
+
}
|
|
82
|
+
// Pick best match: score by name similarity, tiebreak by market priority
|
|
83
|
+
const needle = key.toLowerCase();
|
|
84
|
+
const score = (e) => {
|
|
85
|
+
const n = e.name.toLowerCase();
|
|
86
|
+
if (n === needle)
|
|
87
|
+
return 1;
|
|
88
|
+
if (n.includes(needle))
|
|
89
|
+
return needle.length / n.length;
|
|
90
|
+
return 0;
|
|
91
|
+
};
|
|
92
|
+
const best = entries.sort((a, b) => {
|
|
93
|
+
const d = score(b) - score(a);
|
|
94
|
+
return d !== 0 ? d : targetMarkets.indexOf(a.market) - targetMarkets.indexOf(b.market);
|
|
95
|
+
})[0];
|
|
96
|
+
// 2. Fetch quote
|
|
97
|
+
const sym = hqSymbol(best);
|
|
98
|
+
const hqRaw = await fetchGBK(`https://hq.sinajs.cn/list=${sym}`);
|
|
99
|
+
const f = parseHq(hqRaw, sym);
|
|
100
|
+
if (f.length < 2 || !f[0]) {
|
|
101
|
+
throw new CliError('NOT_FOUND', `No quote data for "${key}"`, 'Market may be closed or data unavailable');
|
|
102
|
+
}
|
|
103
|
+
if (best.market === MARKET_CN) {
|
|
104
|
+
const price = parseFloat(f[3]);
|
|
105
|
+
const prev = parseFloat(f[2]);
|
|
106
|
+
const chg = (price - prev).toFixed(2);
|
|
107
|
+
const chgPct = ((price - prev) / prev * 100).toFixed(2) + '%';
|
|
108
|
+
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: '' }];
|
|
109
|
+
}
|
|
110
|
+
if (best.market === MARKET_HK) {
|
|
111
|
+
// [2]=price [4]=high [5]=low [6]=open [7]=change [8]=change% [11]=volume
|
|
112
|
+
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: '' }];
|
|
113
|
+
}
|
|
114
|
+
// MARKET_US: [1]=price [2]=change% [4]=change [6]=open [7]=today_low [8]=52wH [9]=52wL [10]=volume [12]=mktcap
|
|
115
|
+
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]) }];
|
|
116
|
+
},
|
|
117
|
+
});
|
package/dist/commanderAdapter.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fullName, getRegistry } from './registry.js';
|
|
|
14
14
|
import { formatRegistryHelpText } from './serialization.js';
|
|
15
15
|
import { render as renderOutput } from './output.js';
|
|
16
16
|
import { executeCommand } from './execution.js';
|
|
17
|
-
import { CliError, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
|
|
17
|
+
import { CliError, EXIT_CODES, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
|
|
18
18
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
19
19
|
export function normalizeArgValue(argType, value, name) {
|
|
20
20
|
if (argType !== 'bool')
|
|
@@ -28,7 +28,7 @@ export function normalizeArgValue(argType, value, name) {
|
|
|
28
28
|
return true;
|
|
29
29
|
if (normalized === 'false')
|
|
30
30
|
return false;
|
|
31
|
-
throw new
|
|
31
|
+
throw new ArgumentError(`"${name}" must be either "true" or "false".`);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Register a single CliCommand as a Commander subcommand.
|
|
@@ -106,10 +106,33 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
106
106
|
}
|
|
107
107
|
catch (err) {
|
|
108
108
|
await renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
109
|
-
process.exitCode =
|
|
109
|
+
process.exitCode = resolveExitCode(err);
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
+
// ── Exit code resolution ─────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Map any thrown value to a Unix process exit code.
|
|
116
|
+
*
|
|
117
|
+
* - CliError subclasses carry their own exitCode (set in errors.ts).
|
|
118
|
+
* - Generic Error objects are classified by message pattern so that
|
|
119
|
+
* un-typed auth / not-found errors from adapters still produce
|
|
120
|
+
* meaningful exit codes for shell scripts.
|
|
121
|
+
*/
|
|
122
|
+
function resolveExitCode(err) {
|
|
123
|
+
if (err instanceof CliError)
|
|
124
|
+
return err.exitCode;
|
|
125
|
+
// Pattern-based fallback for untyped errors thrown by third-party adapters.
|
|
126
|
+
const msg = getErrorMessage(err);
|
|
127
|
+
const kind = classifyGenericError(msg);
|
|
128
|
+
if (kind === 'auth')
|
|
129
|
+
return EXIT_CODES.NOPERM;
|
|
130
|
+
if (kind === 'not-found')
|
|
131
|
+
return EXIT_CODES.EMPTY_RESULT;
|
|
132
|
+
if (kind === 'http')
|
|
133
|
+
return EXIT_CODES.GENERIC_ERROR; // HTTP 4xx/5xx → generic; renderer shows details
|
|
134
|
+
return EXIT_CODES.GENERIC_ERROR;
|
|
135
|
+
}
|
|
113
136
|
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
114
137
|
const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
|
|
115
138
|
/** Pattern-based classifier for untyped errors thrown by adapters. */
|
package/dist/daemon.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { createServer } from 'node:http';
|
|
22
22
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
23
23
|
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
24
|
+
import { EXIT_CODES } from './errors.js';
|
|
24
25
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
25
26
|
const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
26
27
|
// ─── State ───────────────────────────────────────────────────────────
|
|
@@ -41,7 +42,7 @@ function resetIdleTimer() {
|
|
|
41
42
|
clearTimeout(idleTimer);
|
|
42
43
|
idleTimer = setTimeout(() => {
|
|
43
44
|
console.error('[daemon] Idle timeout, shutting down');
|
|
44
|
-
process.exit(
|
|
45
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
45
46
|
}, IDLE_TIMEOUT);
|
|
46
47
|
}
|
|
47
48
|
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
@@ -269,10 +270,10 @@ httpServer.listen(PORT, '127.0.0.1', () => {
|
|
|
269
270
|
httpServer.on('error', (err) => {
|
|
270
271
|
if (err.code === 'EADDRINUSE') {
|
|
271
272
|
console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
|
|
272
|
-
process.exit(
|
|
273
|
+
process.exit(EXIT_CODES.SERVICE_UNAVAIL);
|
|
273
274
|
}
|
|
274
275
|
console.error('[daemon] Server error:', err.message);
|
|
275
|
-
process.exit(
|
|
276
|
+
process.exit(EXIT_CODES.GENERIC_ERROR);
|
|
276
277
|
});
|
|
277
278
|
// Graceful shutdown
|
|
278
279
|
function shutdown() {
|
|
@@ -285,7 +286,7 @@ function shutdown() {
|
|
|
285
286
|
if (extensionWs)
|
|
286
287
|
extensionWs.close();
|
|
287
288
|
httpServer.close();
|
|
288
|
-
process.exit(
|
|
289
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
289
290
|
}
|
|
290
291
|
process.on('SIGTERM', shutdown);
|
|
291
292
|
process.on('SIGINT', shutdown);
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,13 +4,41 @@
|
|
|
4
4
|
* All errors thrown by the framework should extend CliError so that
|
|
5
5
|
* the top-level handler in commanderAdapter.ts can render consistent,
|
|
6
6
|
* helpful output with emoji-coded severity and actionable hints.
|
|
7
|
+
*
|
|
8
|
+
* ## Exit codes
|
|
9
|
+
*
|
|
10
|
+
* opencli follows Unix conventions (sysexits.h) for process exit codes:
|
|
11
|
+
*
|
|
12
|
+
* 0 Success
|
|
13
|
+
* 1 Generic / unexpected error
|
|
14
|
+
* 2 Argument / usage error (ArgumentError)
|
|
15
|
+
* 66 No input / empty result (EmptyResultError)
|
|
16
|
+
* 69 Service unavailable (BrowserConnectError, AdapterLoadError)
|
|
17
|
+
* 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
|
|
18
|
+
* 77 Permission denied / auth needed (AuthRequiredError)
|
|
19
|
+
* 78 Configuration error (ConfigError)
|
|
20
|
+
* 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
|
|
7
21
|
*/
|
|
22
|
+
export declare const EXIT_CODES: {
|
|
23
|
+
readonly SUCCESS: 0;
|
|
24
|
+
readonly GENERIC_ERROR: 1;
|
|
25
|
+
readonly USAGE_ERROR: 2;
|
|
26
|
+
readonly EMPTY_RESULT: 66;
|
|
27
|
+
readonly SERVICE_UNAVAIL: 69;
|
|
28
|
+
readonly TEMPFAIL: 75;
|
|
29
|
+
readonly NOPERM: 77;
|
|
30
|
+
readonly CONFIG_ERROR: 78;
|
|
31
|
+
readonly INTERRUPTED: 130;
|
|
32
|
+
};
|
|
33
|
+
export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
|
|
8
34
|
export declare class CliError extends Error {
|
|
9
35
|
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
|
|
10
36
|
readonly code: string;
|
|
11
37
|
/** Human-readable hint on how to fix the problem */
|
|
12
38
|
readonly hint?: string;
|
|
13
|
-
|
|
39
|
+
/** Unix process exit code — defaults to 1 (generic error) */
|
|
40
|
+
readonly exitCode: ExitCode;
|
|
41
|
+
constructor(code: string, message: string, hint?: string, exitCode?: ExitCode);
|
|
14
42
|
}
|
|
15
43
|
export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
|
|
16
44
|
export declare class BrowserConnectError extends CliError {
|
package/dist/errors.js
CHANGED
|
@@ -4,61 +4,99 @@
|
|
|
4
4
|
* All errors thrown by the framework should extend CliError so that
|
|
5
5
|
* the top-level handler in commanderAdapter.ts can render consistent,
|
|
6
6
|
* helpful output with emoji-coded severity and actionable hints.
|
|
7
|
+
*
|
|
8
|
+
* ## Exit codes
|
|
9
|
+
*
|
|
10
|
+
* opencli follows Unix conventions (sysexits.h) for process exit codes:
|
|
11
|
+
*
|
|
12
|
+
* 0 Success
|
|
13
|
+
* 1 Generic / unexpected error
|
|
14
|
+
* 2 Argument / usage error (ArgumentError)
|
|
15
|
+
* 66 No input / empty result (EmptyResultError)
|
|
16
|
+
* 69 Service unavailable (BrowserConnectError, AdapterLoadError)
|
|
17
|
+
* 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
|
|
18
|
+
* 77 Permission denied / auth needed (AuthRequiredError)
|
|
19
|
+
* 78 Configuration error (ConfigError)
|
|
20
|
+
* 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
|
|
7
21
|
*/
|
|
22
|
+
// ── Exit code table ──────────────────────────────────────────────────────────
|
|
23
|
+
export const EXIT_CODES = {
|
|
24
|
+
SUCCESS: 0,
|
|
25
|
+
GENERIC_ERROR: 1,
|
|
26
|
+
USAGE_ERROR: 2, // Bad arguments / command misuse
|
|
27
|
+
EMPTY_RESULT: 66, // No data / not found (EX_NOINPUT)
|
|
28
|
+
SERVICE_UNAVAIL: 69, // Daemon / browser unavailable (EX_UNAVAILABLE)
|
|
29
|
+
TEMPFAIL: 75, // Timeout — try again later (EX_TEMPFAIL)
|
|
30
|
+
NOPERM: 77, // Auth required / permission (EX_NOPERM)
|
|
31
|
+
CONFIG_ERROR: 78, // Missing / invalid config (EX_CONFIG)
|
|
32
|
+
INTERRUPTED: 130, // Ctrl-C / SIGINT
|
|
33
|
+
};
|
|
34
|
+
// ── Base class ───────────────────────────────────────────────────────────────
|
|
8
35
|
export class CliError extends Error {
|
|
9
36
|
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
|
|
10
37
|
code;
|
|
11
38
|
/** Human-readable hint on how to fix the problem */
|
|
12
39
|
hint;
|
|
13
|
-
|
|
40
|
+
/** Unix process exit code — defaults to 1 (generic error) */
|
|
41
|
+
exitCode;
|
|
42
|
+
constructor(code, message, hint, exitCode = EXIT_CODES.GENERIC_ERROR) {
|
|
14
43
|
super(message);
|
|
15
44
|
this.name = new.target.name;
|
|
16
45
|
this.code = code;
|
|
17
46
|
this.hint = hint;
|
|
47
|
+
this.exitCode = exitCode;
|
|
18
48
|
}
|
|
19
49
|
}
|
|
20
50
|
export class BrowserConnectError extends CliError {
|
|
21
51
|
kind;
|
|
22
52
|
constructor(message, hint, kind = 'unknown') {
|
|
23
|
-
super('BROWSER_CONNECT', message, hint);
|
|
53
|
+
super('BROWSER_CONNECT', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
|
|
24
54
|
this.kind = kind;
|
|
25
55
|
}
|
|
26
56
|
}
|
|
27
57
|
export class AdapterLoadError extends CliError {
|
|
28
|
-
constructor(message, hint) {
|
|
58
|
+
constructor(message, hint) {
|
|
59
|
+
super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
|
|
60
|
+
}
|
|
29
61
|
}
|
|
30
62
|
export class CommandExecutionError extends CliError {
|
|
31
|
-
constructor(message, hint) {
|
|
63
|
+
constructor(message, hint) {
|
|
64
|
+
super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
|
|
65
|
+
}
|
|
32
66
|
}
|
|
33
67
|
export class ConfigError extends CliError {
|
|
34
|
-
constructor(message, hint) {
|
|
68
|
+
constructor(message, hint) {
|
|
69
|
+
super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
|
|
70
|
+
}
|
|
35
71
|
}
|
|
36
72
|
export class AuthRequiredError extends CliError {
|
|
37
73
|
domain;
|
|
38
74
|
constructor(domain, message) {
|
|
39
|
-
super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}
|
|
75
|
+
super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`, EXIT_CODES.NOPERM);
|
|
40
76
|
this.domain = domain;
|
|
41
77
|
}
|
|
42
78
|
}
|
|
43
79
|
export class TimeoutError extends CliError {
|
|
44
80
|
constructor(label, seconds, hint) {
|
|
45
|
-
super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var');
|
|
81
|
+
super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var', EXIT_CODES.TEMPFAIL);
|
|
46
82
|
}
|
|
47
83
|
}
|
|
48
84
|
export class ArgumentError extends CliError {
|
|
49
|
-
constructor(message, hint) {
|
|
85
|
+
constructor(message, hint) {
|
|
86
|
+
super('ARGUMENT', message, hint, EXIT_CODES.USAGE_ERROR);
|
|
87
|
+
}
|
|
50
88
|
}
|
|
51
89
|
export class EmptyResultError extends CliError {
|
|
52
90
|
constructor(command, hint) {
|
|
53
|
-
super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
|
|
91
|
+
super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in', EXIT_CODES.EMPTY_RESULT);
|
|
54
92
|
}
|
|
55
93
|
}
|
|
56
94
|
export class SelectorError extends CliError {
|
|
57
95
|
constructor(selector, hint) {
|
|
58
|
-
super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
|
|
96
|
+
super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.', EXIT_CODES.GENERIC_ERROR);
|
|
59
97
|
}
|
|
60
98
|
}
|
|
61
|
-
// ── Utilities
|
|
99
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
62
100
|
/** Extract a human-readable message from an unknown caught value. */
|
|
63
101
|
export function getErrorMessage(error) {
|
|
64
102
|
return error instanceof Error ? error.message : String(error);
|
package/dist/external.js
CHANGED
|
@@ -6,7 +6,7 @@ import { spawnSync, execFileSync } from 'node:child_process';
|
|
|
6
6
|
import yaml from 'js-yaml';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
|
-
import { getErrorMessage } from './errors.js';
|
|
9
|
+
import { EXIT_CODES, getErrorMessage } from './errors.js';
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
function getUserRegistryPath() {
|
|
12
12
|
const home = os.homedir();
|
|
@@ -154,7 +154,7 @@ export function executeExternalCli(name, args, preloaded) {
|
|
|
154
154
|
// 2. Try to auto install
|
|
155
155
|
const success = installExternalCli(cli);
|
|
156
156
|
if (!success) {
|
|
157
|
-
process.exitCode =
|
|
157
|
+
process.exitCode = EXIT_CODES.SERVICE_UNAVAIL;
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -162,7 +162,7 @@ export function executeExternalCli(name, args, preloaded) {
|
|
|
162
162
|
const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
|
|
163
163
|
if (result.error) {
|
|
164
164
|
console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
|
|
165
|
-
process.exitCode =
|
|
165
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
if (result.status !== null) {
|
package/dist/main.js
CHANGED
|
@@ -21,6 +21,7 @@ import { runCli } from './cli.js';
|
|
|
21
21
|
import { emitHook } from './hooks.js';
|
|
22
22
|
import { installNodeNetwork } from './node-network.js';
|
|
23
23
|
import { registerUpdateNoticeOnExit, checkForUpdateBackground } from './update-check.js';
|
|
24
|
+
import { EXIT_CODES } from './errors.js';
|
|
24
25
|
installNodeNetwork();
|
|
25
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
27
|
const __dirname = path.dirname(__filename);
|
|
@@ -53,7 +54,7 @@ if (getCompIdx !== -1) {
|
|
|
53
54
|
cursor = words.length;
|
|
54
55
|
const candidates = getCompletions(words, cursor);
|
|
55
56
|
process.stdout.write(candidates.join('\n') + '\n');
|
|
56
|
-
process.exit(
|
|
57
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
57
58
|
}
|
|
58
59
|
await emitHook('onStartup', { command: '__startup__', args: {} });
|
|
59
60
|
runCli(BUILTIN_CLIS, USER_CLIS);
|