@jackwener/opencli 0.7.10 → 0.8.0
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/.github/workflows/pkg-pr-new.yml +30 -0
- package/CDP.md +103 -0
- package/CDP.zh-CN.md +103 -0
- package/README.md +5 -0
- package/README.zh-CN.md +5 -0
- package/dist/browser/discover.d.ts +30 -0
- package/dist/browser/discover.js +128 -14
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +13 -0
- package/dist/browser/index.d.ts +6 -1
- package/dist/browser/index.js +6 -1
- package/dist/browser/mcp.js +14 -8
- package/dist/browser/page.js +11 -2
- package/dist/browser.test.js +135 -1
- package/dist/cli-manifest.json +163 -0
- package/dist/clis/barchart/flow.d.ts +1 -0
- package/dist/clis/barchart/flow.js +115 -0
- package/dist/clis/barchart/greeks.d.ts +1 -0
- package/dist/clis/barchart/greeks.js +119 -0
- package/dist/clis/barchart/options.d.ts +1 -0
- package/dist/clis/barchart/options.js +106 -0
- package/dist/clis/barchart/quote.d.ts +1 -0
- package/dist/clis/barchart/quote.js +133 -0
- package/dist/doctor.js +8 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +59 -1
- package/dist/main.js +2 -15
- package/dist/pipeline/executor.js +2 -24
- package/dist/pipeline/registry.d.ts +19 -0
- package/dist/pipeline/registry.js +41 -0
- package/package.json +1 -1
- package/src/browser/discover.ts +149 -14
- package/src/browser/errors.ts +17 -1
- package/src/browser/index.ts +6 -1
- package/src/browser/mcp.ts +14 -7
- package/src/browser/page.ts +21 -2
- package/src/browser.test.ts +140 -1
- package/src/clis/barchart/flow.ts +120 -0
- package/src/clis/barchart/greeks.ts +123 -0
- package/src/clis/barchart/options.ts +110 -0
- package/src/clis/barchart/quote.ts +137 -0
- package/src/doctor.ts +9 -0
- package/src/engine.ts +58 -1
- package/src/main.ts +6 -11
- package/src/pipeline/executor.ts +2 -28
- package/src/pipeline/registry.ts +60 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart options greeks overview — IV, delta, gamma, theta, vega, rho
|
|
3
|
+
* for near-the-money options on a given symbol.
|
|
4
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'barchart',
|
|
10
|
+
name: 'greeks',
|
|
11
|
+
description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
|
|
12
|
+
domain: 'www.barchart.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
16
|
+
{ name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
|
|
17
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' },
|
|
18
|
+
],
|
|
19
|
+
columns: [
|
|
20
|
+
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
|
|
21
|
+
'volume', 'openInterest', 'expiration',
|
|
22
|
+
],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
25
|
+
const expiration = kwargs.expiration ?? '';
|
|
26
|
+
const limit = kwargs.limit ?? 10;
|
|
27
|
+
|
|
28
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
29
|
+
await page.wait(4);
|
|
30
|
+
|
|
31
|
+
const data = await page.evaluate(`
|
|
32
|
+
(async () => {
|
|
33
|
+
const sym = '${symbol}';
|
|
34
|
+
const expDate = '${expiration}';
|
|
35
|
+
const limit = ${limit};
|
|
36
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
37
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const fields = [
|
|
41
|
+
'strikePrice','lastPrice','volume','openInterest',
|
|
42
|
+
'volatility','delta','gamma','theta','vega','rho',
|
|
43
|
+
'expirationDate','optionType','percentFromLast',
|
|
44
|
+
].join(',');
|
|
45
|
+
|
|
46
|
+
let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
|
|
47
|
+
+ '&fields=' + fields + '&raw=1';
|
|
48
|
+
if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
|
|
49
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
50
|
+
if (resp.ok) {
|
|
51
|
+
const d = await resp.json();
|
|
52
|
+
let items = d?.data || [];
|
|
53
|
+
|
|
54
|
+
if (!expDate) {
|
|
55
|
+
const expirations = items
|
|
56
|
+
.map(i => (i.raw || i).expirationDate || null)
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.sort((a, b) => {
|
|
59
|
+
const aTime = Date.parse(a);
|
|
60
|
+
const bTime = Date.parse(b);
|
|
61
|
+
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
|
|
62
|
+
if (Number.isNaN(aTime)) return 1;
|
|
63
|
+
if (Number.isNaN(bTime)) return -1;
|
|
64
|
+
return aTime - bTime;
|
|
65
|
+
});
|
|
66
|
+
const nearestExpiration = expirations[0];
|
|
67
|
+
if (nearestExpiration) {
|
|
68
|
+
items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Separate calls and puts, sort by distance from current price
|
|
73
|
+
const calls = items
|
|
74
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
|
|
75
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
76
|
+
.slice(0, limit);
|
|
77
|
+
const puts = items
|
|
78
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
|
|
79
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
80
|
+
.slice(0, limit);
|
|
81
|
+
|
|
82
|
+
return [...calls, ...puts].map(i => {
|
|
83
|
+
const r = i.raw || i;
|
|
84
|
+
return {
|
|
85
|
+
type: r.optionType,
|
|
86
|
+
strike: r.strikePrice,
|
|
87
|
+
last: r.lastPrice,
|
|
88
|
+
iv: r.volatility,
|
|
89
|
+
delta: r.delta,
|
|
90
|
+
gamma: r.gamma,
|
|
91
|
+
theta: r.theta,
|
|
92
|
+
vega: r.vega,
|
|
93
|
+
rho: r.rho,
|
|
94
|
+
volume: r.volume,
|
|
95
|
+
openInterest: r.openInterest,
|
|
96
|
+
expiration: r.expirationDate,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch(e) {}
|
|
101
|
+
|
|
102
|
+
return [];
|
|
103
|
+
})()
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
if (!data || !Array.isArray(data)) return [];
|
|
107
|
+
|
|
108
|
+
return data.map(r => ({
|
|
109
|
+
type: r.type || '',
|
|
110
|
+
strike: r.strike,
|
|
111
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
112
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
113
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
114
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
115
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
116
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
117
|
+
rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
|
|
118
|
+
volume: r.volume,
|
|
119
|
+
openInterest: r.openInterest,
|
|
120
|
+
expiration: r.expiration ?? null,
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart options chain — strike, bid/ask, volume, OI, greeks, IV.
|
|
3
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'barchart',
|
|
9
|
+
name: 'options',
|
|
10
|
+
description: 'Barchart options chain with greeks, IV, volume, and open interest',
|
|
11
|
+
domain: 'www.barchart.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
15
|
+
{ name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' },
|
|
17
|
+
],
|
|
18
|
+
columns: [
|
|
19
|
+
'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest',
|
|
20
|
+
'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration',
|
|
21
|
+
],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
24
|
+
const optType = kwargs.type || 'Call';
|
|
25
|
+
const limit = kwargs.limit ?? 20;
|
|
26
|
+
|
|
27
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
28
|
+
await page.wait(4);
|
|
29
|
+
|
|
30
|
+
const data = await page.evaluate(`
|
|
31
|
+
(async () => {
|
|
32
|
+
const sym = '${symbol}';
|
|
33
|
+
const type = '${optType}';
|
|
34
|
+
const limit = ${limit};
|
|
35
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
36
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
37
|
+
|
|
38
|
+
// API: options chain with greeks
|
|
39
|
+
try {
|
|
40
|
+
const fields = [
|
|
41
|
+
'strikePrice','bidPrice','askPrice','lastPrice','priceChange',
|
|
42
|
+
'volume','openInterest','volatility',
|
|
43
|
+
'delta','gamma','theta','vega',
|
|
44
|
+
'expirationDate','optionType','percentFromLast',
|
|
45
|
+
].join(',');
|
|
46
|
+
|
|
47
|
+
const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
|
|
48
|
+
+ '&fields=' + fields + '&raw=1';
|
|
49
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
50
|
+
if (resp.ok) {
|
|
51
|
+
const d = await resp.json();
|
|
52
|
+
let items = d?.data || [];
|
|
53
|
+
|
|
54
|
+
// Filter by type
|
|
55
|
+
items = items.filter(i => {
|
|
56
|
+
const t = (i.raw || i).optionType || '';
|
|
57
|
+
return t.toLowerCase() === type.toLowerCase();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Sort by closeness to current price
|
|
61
|
+
items.sort((a, b) => {
|
|
62
|
+
const aD = Math.abs((a.raw || a).percentFromLast || 999);
|
|
63
|
+
const bD = Math.abs((b.raw || b).percentFromLast || 999);
|
|
64
|
+
return aD - bD;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return items.slice(0, limit).map(i => {
|
|
68
|
+
const r = i.raw || i;
|
|
69
|
+
return {
|
|
70
|
+
strike: r.strikePrice,
|
|
71
|
+
bid: r.bidPrice,
|
|
72
|
+
ask: r.askPrice,
|
|
73
|
+
last: r.lastPrice,
|
|
74
|
+
change: r.priceChange,
|
|
75
|
+
volume: r.volume,
|
|
76
|
+
openInterest: r.openInterest,
|
|
77
|
+
iv: r.volatility,
|
|
78
|
+
delta: r.delta,
|
|
79
|
+
gamma: r.gamma,
|
|
80
|
+
theta: r.theta,
|
|
81
|
+
vega: r.vega,
|
|
82
|
+
expiration: r.expirationDate,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
} catch(e) {}
|
|
87
|
+
|
|
88
|
+
return [];
|
|
89
|
+
})()
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
if (!data || !Array.isArray(data)) return [];
|
|
93
|
+
|
|
94
|
+
return data.map(r => ({
|
|
95
|
+
strike: r.strike,
|
|
96
|
+
bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null,
|
|
97
|
+
ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null,
|
|
98
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
99
|
+
change: r.change != null ? Number(Number(r.change).toFixed(2)) : null,
|
|
100
|
+
volume: r.volume,
|
|
101
|
+
openInterest: r.openInterest,
|
|
102
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
103
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
104
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
105
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
106
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
107
|
+
expiration: r.expiration ?? null,
|
|
108
|
+
}));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics.
|
|
3
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'barchart',
|
|
9
|
+
name: 'quote',
|
|
10
|
+
description: 'Barchart stock quote with price, volume, and key metrics',
|
|
11
|
+
domain: 'www.barchart.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
|
|
15
|
+
],
|
|
16
|
+
columns: [
|
|
17
|
+
'symbol', 'name', 'price', 'change', 'changePct',
|
|
18
|
+
'open', 'high', 'low', 'prevClose', 'volume',
|
|
19
|
+
'avgVolume', 'marketCap', 'peRatio', 'eps',
|
|
20
|
+
],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
23
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/overview`);
|
|
24
|
+
await page.wait(4);
|
|
25
|
+
|
|
26
|
+
const data = await page.evaluate(`
|
|
27
|
+
(async () => {
|
|
28
|
+
const sym = '${symbol}';
|
|
29
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
30
|
+
|
|
31
|
+
// Strategy 1: internal proxy API with CSRF token
|
|
32
|
+
try {
|
|
33
|
+
const fields = [
|
|
34
|
+
'symbol','symbolName','lastPrice','priceChange','percentChange',
|
|
35
|
+
'highPrice','lowPrice','openPrice','previousPrice','volume','averageVolume',
|
|
36
|
+
'marketCap','peRatio','earningsPerShare','tradeTime',
|
|
37
|
+
].join(',');
|
|
38
|
+
const url = '/proxies/core-api/v1/quotes/get?symbol=' + encodeURIComponent(sym) + '&fields=' + fields;
|
|
39
|
+
const resp = await fetch(url, {
|
|
40
|
+
credentials: 'include',
|
|
41
|
+
headers: { 'X-CSRF-TOKEN': csrf },
|
|
42
|
+
});
|
|
43
|
+
if (resp.ok) {
|
|
44
|
+
const d = await resp.json();
|
|
45
|
+
const row = d?.data?.[0] || null;
|
|
46
|
+
if (row) {
|
|
47
|
+
return { source: 'api', row };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch(e) {}
|
|
51
|
+
|
|
52
|
+
// Strategy 2: parse from DOM
|
|
53
|
+
try {
|
|
54
|
+
const priceEl = document.querySelector('span.last-change');
|
|
55
|
+
const price = priceEl ? priceEl.textContent.trim() : null;
|
|
56
|
+
|
|
57
|
+
// Change values are sibling spans inside .pricechangerow > .last-change
|
|
58
|
+
const changeParent = priceEl?.parentElement;
|
|
59
|
+
const changeSpans = changeParent ? changeParent.querySelectorAll('span') : [];
|
|
60
|
+
let change = null;
|
|
61
|
+
let changePct = null;
|
|
62
|
+
for (const s of changeSpans) {
|
|
63
|
+
const t = s.textContent.trim();
|
|
64
|
+
if (s === priceEl) continue;
|
|
65
|
+
if (t.includes('%')) changePct = t.replace(/[()]/g, '');
|
|
66
|
+
else if (t.match(/^[+-]?[\\d.]+$/)) change = t;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Financial data rows
|
|
70
|
+
const rows = document.querySelectorAll('.financial-data-row');
|
|
71
|
+
const fdata = {};
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const spans = row.querySelectorAll('span');
|
|
74
|
+
if (spans.length >= 2) {
|
|
75
|
+
const label = spans[0].textContent.trim();
|
|
76
|
+
const valSpan = row.querySelector('span.right span:not(.ng-hide)');
|
|
77
|
+
fdata[label] = valSpan ? valSpan.textContent.trim() : '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Day high/low from row chart
|
|
82
|
+
const dayLow = document.querySelector('.bc-quote-row-chart .small-6:first-child .inline:not(.ng-hide)');
|
|
83
|
+
const dayHigh = document.querySelector('.bc-quote-row-chart .text-right .inline:not(.ng-hide)');
|
|
84
|
+
const openEl = document.querySelector('.mark span');
|
|
85
|
+
const openText = openEl ? openEl.textContent.trim().replace('Open ', '') : null;
|
|
86
|
+
|
|
87
|
+
const name = document.querySelector('h1 span.symbol');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
source: 'dom',
|
|
91
|
+
row: {
|
|
92
|
+
symbol: sym,
|
|
93
|
+
symbolName: name ? name.textContent.trim() : sym,
|
|
94
|
+
lastPrice: price,
|
|
95
|
+
priceChange: change,
|
|
96
|
+
percentChange: changePct,
|
|
97
|
+
open: openText,
|
|
98
|
+
highPrice: dayHigh ? dayHigh.textContent.trim() : null,
|
|
99
|
+
lowPrice: dayLow ? dayLow.textContent.trim() : null,
|
|
100
|
+
previousClose: fdata['Previous Close'] || null,
|
|
101
|
+
volume: fdata['Volume'] || null,
|
|
102
|
+
averageVolume: fdata['Average Volume'] || null,
|
|
103
|
+
marketCap: null,
|
|
104
|
+
peRatio: null,
|
|
105
|
+
earningsPerShare: null,
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
} catch(e) {
|
|
109
|
+
return { error: 'Could not fetch quote for ' + sym + ': ' + e.message };
|
|
110
|
+
}
|
|
111
|
+
})()
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
if (!data || data.error) return [];
|
|
115
|
+
|
|
116
|
+
const r = data.row || {};
|
|
117
|
+
// API returns formatted strings like "+1.41" and "+0.56%"; use raw if available
|
|
118
|
+
const raw = r.raw || {};
|
|
119
|
+
|
|
120
|
+
return [{
|
|
121
|
+
symbol: r.symbol || symbol,
|
|
122
|
+
name: r.symbolName || r.name || symbol,
|
|
123
|
+
price: r.lastPrice ?? null,
|
|
124
|
+
change: r.priceChange ?? null,
|
|
125
|
+
changePct: r.percentChange ?? null,
|
|
126
|
+
open: r.openPrice ?? r.open ?? null,
|
|
127
|
+
high: r.highPrice ?? null,
|
|
128
|
+
low: r.lowPrice ?? null,
|
|
129
|
+
prevClose: r.previousPrice ?? r.previousClose ?? null,
|
|
130
|
+
volume: r.volume ?? null,
|
|
131
|
+
avgVolume: r.averageVolume ?? null,
|
|
132
|
+
marketCap: r.marketCap ?? null,
|
|
133
|
+
peRatio: r.peRatio ?? null,
|
|
134
|
+
eps: r.earningsPerShare ?? null,
|
|
135
|
+
}];
|
|
136
|
+
},
|
|
137
|
+
});
|
package/src/doctor.ts
CHANGED
|
@@ -591,6 +591,15 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
591
591
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
592
592
|
const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
|
|
593
593
|
|
|
594
|
+
// CDP endpoint mode (for remote/server environments)
|
|
595
|
+
const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
596
|
+
if (cdpEndpoint) {
|
|
597
|
+
lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`));
|
|
598
|
+
lines.push(chalk.dim(' → Remote Chrome mode: extension token not required'));
|
|
599
|
+
lines.push('');
|
|
600
|
+
return lines.join('\n');
|
|
601
|
+
}
|
|
602
|
+
|
|
594
603
|
const installStatus: ReportStatus = report.extensionInstalled ? 'OK' : 'MISSING';
|
|
595
604
|
const installDetail = report.extensionInstalled
|
|
596
605
|
? `Extension installed (${report.extensionBrowsers.join(', ')})`
|
package/src/engine.ts
CHANGED
|
@@ -164,15 +164,72 @@ function registerYamlCli(filePath: string, defaultSite: string): void {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Validates and coerces arguments based on the command's Arg definitions.
|
|
169
|
+
*/
|
|
170
|
+
function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: Record<string, any>): Record<string, any> {
|
|
171
|
+
const result: Record<string, any> = { ...kwargs };
|
|
172
|
+
|
|
173
|
+
for (const argDef of cmdArgs) {
|
|
174
|
+
const val = result[argDef.name];
|
|
175
|
+
|
|
176
|
+
// 1. Check required
|
|
177
|
+
if (argDef.required && (val === undefined || val === null || val === '')) {
|
|
178
|
+
throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (val !== undefined && val !== null) {
|
|
182
|
+
// 2. Type coercion
|
|
183
|
+
if (argDef.type === 'int' || argDef.type === 'number') {
|
|
184
|
+
const num = Number(val);
|
|
185
|
+
if (Number.isNaN(num)) {
|
|
186
|
+
throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
|
|
187
|
+
}
|
|
188
|
+
result[argDef.name] = num;
|
|
189
|
+
} else if (argDef.type === 'boolean' || argDef.type === 'bool') {
|
|
190
|
+
if (typeof val === 'string') {
|
|
191
|
+
const lower = val.toLowerCase();
|
|
192
|
+
if (lower === 'true' || lower === '1') result[argDef.name] = true;
|
|
193
|
+
else if (lower === 'false' || lower === '0') result[argDef.name] = false;
|
|
194
|
+
else throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
|
|
195
|
+
} else {
|
|
196
|
+
result[argDef.name] = Boolean(val);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 3. Choices validation
|
|
201
|
+
const coercedVal = result[argDef.name];
|
|
202
|
+
if (argDef.choices && argDef.choices.length > 0) {
|
|
203
|
+
// Only stringent check for string/number types against choices array
|
|
204
|
+
if (!argDef.choices.map(String).includes(String(coercedVal))) {
|
|
205
|
+
throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (argDef.default !== undefined) {
|
|
209
|
+
// Set default if value is missing
|
|
210
|
+
result[argDef.name] = argDef.default;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
167
216
|
/**
|
|
168
217
|
* Execute a CLI command. Handles lazy-loading of TS modules.
|
|
169
218
|
*/
|
|
170
219
|
export async function executeCommand(
|
|
171
220
|
cmd: CliCommand,
|
|
172
221
|
page: IPage | null,
|
|
173
|
-
|
|
222
|
+
rawKwargs: Record<string, any>,
|
|
174
223
|
debug: boolean = false,
|
|
175
224
|
): Promise<any> {
|
|
225
|
+
let kwargs: Record<string, any>;
|
|
226
|
+
try {
|
|
227
|
+
kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
|
|
228
|
+
} catch (err: any) {
|
|
229
|
+
// Re-throw validation errors clearly
|
|
230
|
+
throw new Error(`[Argument Validation Error]\n${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
176
233
|
// Lazy-load TS module on first execution
|
|
177
234
|
const internal = cmd as InternalCliCommand;
|
|
178
235
|
if (internal._lazy && internal._modulePath) {
|
package/src/main.ts
CHANGED
|
@@ -189,19 +189,21 @@ for (const [, cmd] of registry) {
|
|
|
189
189
|
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
190
190
|
const startTime = Date.now();
|
|
191
191
|
const kwargs: Record<string, any> = {};
|
|
192
|
+
|
|
192
193
|
// Collect positional args
|
|
193
194
|
for (let i = 0; i < positionalArgs.length; i++) {
|
|
194
195
|
const arg = positionalArgs[i];
|
|
195
196
|
const v = actionArgs[i];
|
|
196
|
-
if (v !== undefined) kwargs[arg.name] =
|
|
197
|
-
else if (arg.default != null) kwargs[arg.name] = arg.default;
|
|
197
|
+
if (v !== undefined) kwargs[arg.name] = v;
|
|
198
198
|
}
|
|
199
|
+
|
|
199
200
|
// Collect named options
|
|
200
201
|
for (const arg of cmd.args) {
|
|
201
202
|
if (arg.positional) continue;
|
|
202
|
-
const v = actionOpts[arg.name];
|
|
203
|
-
|
|
203
|
+
const v = actionOpts[arg.name];
|
|
204
|
+
if (v !== undefined) kwargs[arg.name] = v;
|
|
204
205
|
}
|
|
206
|
+
|
|
205
207
|
try {
|
|
206
208
|
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
207
209
|
let result: any;
|
|
@@ -226,11 +228,4 @@ for (const [, cmd] of registry) {
|
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
function coerce(v: any, t: string): any {
|
|
230
|
-
if (t === 'bool') return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase());
|
|
231
|
-
if (t === 'int') return parseInt(String(v), 10);
|
|
232
|
-
if (t === 'float') return parseFloat(String(v));
|
|
233
|
-
return String(v);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
231
|
program.parse();
|
package/src/pipeline/executor.ts
CHANGED
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import type { IPage } from '../types.js';
|
|
7
|
-
import {
|
|
8
|
-
import { stepFetch } from './steps/fetch.js';
|
|
9
|
-
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
10
|
-
import { stepIntercept } from './steps/intercept.js';
|
|
11
|
-
import { stepTap } from './steps/tap.js';
|
|
7
|
+
import { getStep, type StepHandler } from './registry.js';
|
|
12
8
|
import { log } from '../logger.js';
|
|
13
9
|
|
|
14
10
|
export interface PipelineContext {
|
|
@@ -16,28 +12,6 @@ export interface PipelineContext {
|
|
|
16
12
|
debug?: boolean;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
/** Step handler: all steps conform to (page, params, data, args) => Promise<any> */
|
|
20
|
-
type StepHandler = (page: IPage | null, params: any, data: any, args: Record<string, any>) => Promise<any>;
|
|
21
|
-
|
|
22
|
-
/** Registry of all available step handlers */
|
|
23
|
-
const STEP_HANDLERS: Record<string, StepHandler> = {
|
|
24
|
-
navigate: stepNavigate,
|
|
25
|
-
fetch: stepFetch,
|
|
26
|
-
select: stepSelect,
|
|
27
|
-
evaluate: stepEvaluate,
|
|
28
|
-
snapshot: stepSnapshot,
|
|
29
|
-
click: stepClick,
|
|
30
|
-
type: stepType,
|
|
31
|
-
wait: stepWait,
|
|
32
|
-
press: stepPress,
|
|
33
|
-
map: stepMap,
|
|
34
|
-
filter: stepFilter,
|
|
35
|
-
sort: stepSort,
|
|
36
|
-
limit: stepLimit,
|
|
37
|
-
intercept: stepIntercept,
|
|
38
|
-
tap: stepTap,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
15
|
export async function executePipeline(
|
|
42
16
|
page: IPage | null,
|
|
43
17
|
pipeline: any[],
|
|
@@ -54,7 +28,7 @@ export async function executePipeline(
|
|
|
54
28
|
for (const [op, params] of Object.entries(step)) {
|
|
55
29
|
if (debug) debugStepStart(i + 1, total, op, params);
|
|
56
30
|
|
|
57
|
-
const handler =
|
|
31
|
+
const handler = getStep(op);
|
|
58
32
|
if (handler) {
|
|
59
33
|
data = await handler(page, params, data, args);
|
|
60
34
|
} else {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic registry for pipeline steps.
|
|
3
|
+
* Allows core and third-party plugins to register custom YAML operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IPage } from '../types.js';
|
|
7
|
+
|
|
8
|
+
// Import core steps
|
|
9
|
+
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
|
|
10
|
+
import { stepFetch } from './steps/fetch.js';
|
|
11
|
+
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
12
|
+
import { stepIntercept } from './steps/intercept.js';
|
|
13
|
+
import { stepTap } from './steps/tap.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Step handler: all pipeline steps conform to this generic interface.
|
|
17
|
+
* TData is the type of the `data` state flowing into the step.
|
|
18
|
+
* TResult is the expected return type.
|
|
19
|
+
*/
|
|
20
|
+
export type StepHandler<TData = any, TResult = any> = (
|
|
21
|
+
page: IPage | null,
|
|
22
|
+
params: any,
|
|
23
|
+
data: TData,
|
|
24
|
+
args: Record<string, any>
|
|
25
|
+
) => Promise<TResult>;
|
|
26
|
+
|
|
27
|
+
const _stepRegistry = new Map<string, StepHandler>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a registered step handler by name.
|
|
31
|
+
*/
|
|
32
|
+
export function getStep(name: string): StepHandler | undefined {
|
|
33
|
+
return _stepRegistry.get(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a new custom step handler for the YAML pipeline.
|
|
38
|
+
*/
|
|
39
|
+
export function registerStep(name: string, handler: StepHandler): void {
|
|
40
|
+
_stepRegistry.set(name, handler);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// -------------------------------------------------------------
|
|
44
|
+
// Auto-Register Core Steps
|
|
45
|
+
// -------------------------------------------------------------
|
|
46
|
+
registerStep('navigate', stepNavigate);
|
|
47
|
+
registerStep('fetch', stepFetch);
|
|
48
|
+
registerStep('select', stepSelect);
|
|
49
|
+
registerStep('evaluate', stepEvaluate);
|
|
50
|
+
registerStep('snapshot', stepSnapshot);
|
|
51
|
+
registerStep('click', stepClick);
|
|
52
|
+
registerStep('type', stepType);
|
|
53
|
+
registerStep('wait', stepWait);
|
|
54
|
+
registerStep('press', stepPress);
|
|
55
|
+
registerStep('map', stepMap);
|
|
56
|
+
registerStep('filter', stepFilter);
|
|
57
|
+
registerStep('sort', stepSort);
|
|
58
|
+
registerStep('limit', stepLimit);
|
|
59
|
+
registerStep('intercept', stepIntercept);
|
|
60
|
+
registerStep('tap', stepTap);
|
package/vitest.config.ts
CHANGED
|
@@ -7,6 +7,10 @@ export default defineConfig({
|
|
|
7
7
|
test: {
|
|
8
8
|
name: 'unit',
|
|
9
9
|
include: ['src/**/*.test.ts'],
|
|
10
|
+
// Run unit tests before e2e tests to avoid project-level contention in CI.
|
|
11
|
+
sequence: {
|
|
12
|
+
groupOrder: 0,
|
|
13
|
+
},
|
|
10
14
|
},
|
|
11
15
|
},
|
|
12
16
|
{
|
|
@@ -14,6 +18,9 @@ export default defineConfig({
|
|
|
14
18
|
name: 'e2e',
|
|
15
19
|
include: ['tests/**/*.test.ts'],
|
|
16
20
|
maxWorkers: 2,
|
|
21
|
+
sequence: {
|
|
22
|
+
groupOrder: 1,
|
|
23
|
+
},
|
|
17
24
|
},
|
|
18
25
|
},
|
|
19
26
|
],
|