@nexstone/rift-cli 0.1.1
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/LICENSE +201 -0
- package/bin/run.js +22 -0
- package/dist/commands/algo.d.ts +32 -0
- package/dist/commands/algo.js +719 -0
- package/dist/commands/audit.d.ts +13 -0
- package/dist/commands/audit.js +37 -0
- package/dist/commands/auth-status.d.ts +14 -0
- package/dist/commands/auth-status.js +118 -0
- package/dist/commands/auth.d.ts +14 -0
- package/dist/commands/auth.js +275 -0
- package/dist/commands/backtest.d.ts +26 -0
- package/dist/commands/backtest.js +283 -0
- package/dist/commands/collect/start.d.ts +11 -0
- package/dist/commands/collect/start.js +78 -0
- package/dist/commands/collect/status.d.ts +6 -0
- package/dist/commands/collect/status.js +60 -0
- package/dist/commands/compare.d.ts +16 -0
- package/dist/commands/compare.js +130 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +143 -0
- package/dist/commands/cost.d.ts +20 -0
- package/dist/commands/cost.js +104 -0
- package/dist/commands/cross-asset.d.ts +14 -0
- package/dist/commands/cross-asset.js +39 -0
- package/dist/commands/data/fetch.d.ts +15 -0
- package/dist/commands/data/fetch.js +82 -0
- package/dist/commands/data/list.d.ts +6 -0
- package/dist/commands/data/list.js +28 -0
- package/dist/commands/data-inventory.d.ts +9 -0
- package/dist/commands/data-inventory.js +24 -0
- package/dist/commands/deposit.d.ts +10 -0
- package/dist/commands/deposit.js +222 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +87 -0
- package/dist/commands/funding-browser.d.ts +12 -0
- package/dist/commands/funding-browser.js +33 -0
- package/dist/commands/guide.d.ts +6 -0
- package/dist/commands/guide.js +15 -0
- package/dist/commands/home.d.ts +23 -0
- package/dist/commands/home.js +210 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +122 -0
- package/dist/commands/install.d.ts +9 -0
- package/dist/commands/install.js +89 -0
- package/dist/commands/interactive.d.ts +17 -0
- package/dist/commands/interactive.js +179 -0
- package/dist/commands/lessons.d.ts +12 -0
- package/dist/commands/lessons.js +33 -0
- package/dist/commands/montecarlo.d.ts +19 -0
- package/dist/commands/montecarlo.js +168 -0
- package/dist/commands/more.d.ts +11 -0
- package/dist/commands/more.js +227 -0
- package/dist/commands/new.d.ts +14 -0
- package/dist/commands/new.js +306 -0
- package/dist/commands/pairs.d.ts +22 -0
- package/dist/commands/pairs.js +147 -0
- package/dist/commands/perp/close.d.ts +12 -0
- package/dist/commands/perp/close.js +57 -0
- package/dist/commands/perp/long.d.ts +14 -0
- package/dist/commands/perp/long.js +38 -0
- package/dist/commands/perp/short.d.ts +14 -0
- package/dist/commands/perp/short.js +27 -0
- package/dist/commands/perp/status.d.ts +9 -0
- package/dist/commands/perp/status.js +26 -0
- package/dist/commands/portfolio/alerts.d.ts +6 -0
- package/dist/commands/portfolio/alerts.js +47 -0
- package/dist/commands/portfolio/backtest.d.ts +12 -0
- package/dist/commands/portfolio/backtest.js +178 -0
- package/dist/commands/portfolio/create.d.ts +7 -0
- package/dist/commands/portfolio/create.js +195 -0
- package/dist/commands/portfolio/start.d.ts +9 -0
- package/dist/commands/portfolio/start.js +64 -0
- package/dist/commands/portfolio/status.d.ts +6 -0
- package/dist/commands/portfolio/status.js +128 -0
- package/dist/commands/portfolio/stop.d.ts +6 -0
- package/dist/commands/portfolio/stop.js +81 -0
- package/dist/commands/portfolio-backtest.d.ts +13 -0
- package/dist/commands/portfolio-backtest.js +37 -0
- package/dist/commands/portfolio-matrix.d.ts +12 -0
- package/dist/commands/portfolio-matrix.js +30 -0
- package/dist/commands/quick-test.d.ts +17 -0
- package/dist/commands/quick-test.js +45 -0
- package/dist/commands/research.d.ts +57 -0
- package/dist/commands/research.js +1976 -0
- package/dist/commands/scout.d.ts +14 -0
- package/dist/commands/scout.js +184 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.js +1176 -0
- package/dist/commands/setup/proxy.d.ts +10 -0
- package/dist/commands/setup/proxy.js +267 -0
- package/dist/commands/spot/buy.d.ts +14 -0
- package/dist/commands/spot/buy.js +38 -0
- package/dist/commands/spot/sell.d.ts +14 -0
- package/dist/commands/spot/sell.js +39 -0
- package/dist/commands/strategies/list.d.ts +6 -0
- package/dist/commands/strategies/list.js +34 -0
- package/dist/commands/sweep.d.ts +19 -0
- package/dist/commands/sweep.js +137 -0
- package/dist/commands/sync.d.ts +17 -0
- package/dist/commands/sync.js +54 -0
- package/dist/commands/test-trade.d.ts +6 -0
- package/dist/commands/test-trade.js +97 -0
- package/dist/commands/trade.d.ts +26 -0
- package/dist/commands/trade.js +274 -0
- package/dist/commands/transfer.d.ts +13 -0
- package/dist/commands/transfer.js +65 -0
- package/dist/commands/verify.d.ts +16 -0
- package/dist/commands/verify.js +38 -0
- package/dist/commands/walkforward.d.ts +20 -0
- package/dist/commands/walkforward.js +191 -0
- package/dist/commands/withdraw.d.ts +12 -0
- package/dist/commands/withdraw.js +55 -0
- package/dist/commands/workbench-create.d.ts +13 -0
- package/dist/commands/workbench-create.js +39 -0
- package/dist/lib/account-mode.d.ts +44 -0
- package/dist/lib/account-mode.js +96 -0
- package/dist/lib/analyzer.d.ts +4 -0
- package/dist/lib/analyzer.js +62 -0
- package/dist/lib/base-command.d.ts +35 -0
- package/dist/lib/base-command.js +49 -0
- package/dist/lib/credentials.d.ts +46 -0
- package/dist/lib/credentials.js +137 -0
- package/dist/lib/engine-passthrough.d.ts +28 -0
- package/dist/lib/engine-passthrough.js +60 -0
- package/dist/lib/fees.d.ts +52 -0
- package/dist/lib/fees.js +97 -0
- package/dist/lib/python-bridge.d.ts +24 -0
- package/dist/lib/python-bridge.js +182 -0
- package/dist/lib/setup-status.d.ts +32 -0
- package/dist/lib/setup-status.js +121 -0
- package/dist/lib/status-footer.d.ts +35 -0
- package/dist/lib/status-footer.js +101 -0
- package/dist/lib/tui.d.ts +130 -0
- package/dist/lib/tui.js +300 -0
- package/dist/lib/walletconnect.d.ts +70 -0
- package/dist/lib/walletconnect.js +407 -0
- package/package.json +49 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class SetupProxy extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
run(): Promise<void>;
|
|
6
|
+
private optionVpnRunning;
|
|
7
|
+
private optionPasteProxy;
|
|
8
|
+
private optionGuidedSetup;
|
|
9
|
+
private optionClearProxy;
|
|
10
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { runEngine } from '../../lib/python-bridge.js';
|
|
5
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
6
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
7
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
8
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
9
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
10
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
11
|
+
function ask(question) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
rl.question(question, answer => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function checkApi(proxy) {
|
|
21
|
+
return new Promise(resolve => {
|
|
22
|
+
const args = proxy ? ['--proxy', proxy] : [];
|
|
23
|
+
let result = null;
|
|
24
|
+
runEngine('check-api', args, (msg) => {
|
|
25
|
+
if (msg.type === 'result') {
|
|
26
|
+
result = msg;
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.then(() => {
|
|
30
|
+
if (result?.status === 'ok') {
|
|
31
|
+
resolve({ ok: true, latency: result.latency_ms, pairs: result.pairs });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
resolve({ ok: false, error: result?.error || 'Unknown error' });
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
resolve({ ok: false, error: err.message.split('\n')[0] });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function saveProxy(proxyUrl) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
runEngine('set-proxy', [proxyUrl], () => { })
|
|
45
|
+
.then(resolve)
|
|
46
|
+
.catch(reject);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function clearProxy() {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
runEngine('clear-proxy', [], () => { })
|
|
52
|
+
.then(resolve)
|
|
53
|
+
.catch(reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function detectVpnClients() {
|
|
57
|
+
const clients = [
|
|
58
|
+
{
|
|
59
|
+
name: 'WireGuard',
|
|
60
|
+
cmd: 'which wg',
|
|
61
|
+
hint: 'WireGuard detected. Make sure your tunnel is active: wg show',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Tailscale',
|
|
65
|
+
cmd: 'which tailscale',
|
|
66
|
+
hint: 'Tailscale detected. Use an exit node in a supported region: tailscale up --exit-node=<node>',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Mullvad',
|
|
70
|
+
cmd: 'which mullvad',
|
|
71
|
+
hint: 'Mullvad detected. Connect to a non-US server: mullvad connect',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'NordVPN',
|
|
75
|
+
cmd: 'which nordvpn',
|
|
76
|
+
hint: 'NordVPN detected. Connect to a non-US server: nordvpn connect',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'ProtonVPN',
|
|
80
|
+
cmd: 'which protonvpn-cli',
|
|
81
|
+
hint: 'ProtonVPN detected. Connect to a non-US server: protonvpn-cli connect',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'OpenVPN',
|
|
85
|
+
cmd: 'which openvpn',
|
|
86
|
+
hint: 'OpenVPN detected. Connect with your config: sudo openvpn --config your.ovpn',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
return clients.map(c => {
|
|
90
|
+
let detected = false;
|
|
91
|
+
try {
|
|
92
|
+
execSync(c.cmd, { stdio: 'ignore' });
|
|
93
|
+
detected = true;
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
return { name: c.name, detected, hint: c.hint };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export default class SetupProxy extends GatedCommand {
|
|
100
|
+
static description = 'Set up proxy for Hyperliquid API access';
|
|
101
|
+
static examples = [
|
|
102
|
+
'$ rift setup proxy',
|
|
103
|
+
];
|
|
104
|
+
async run() {
|
|
105
|
+
this.log('');
|
|
106
|
+
this.log(` ${bold('RIFT Proxy Setup')}`);
|
|
107
|
+
this.log(` ${dim('─'.repeat(45))}`);
|
|
108
|
+
this.log('');
|
|
109
|
+
// Step 1: Check direct connection
|
|
110
|
+
this.log(` ${dim('Checking Hyperliquid API access...')}`);
|
|
111
|
+
const direct = await checkApi();
|
|
112
|
+
if (direct.ok) {
|
|
113
|
+
this.log(` ${green('✔')} Connected directly ${dim(`(${direct.latency}ms, ${direct.pairs} pairs)`)}`);
|
|
114
|
+
this.log('');
|
|
115
|
+
this.log(` ${dim('No proxy needed — your connection works.')}`);
|
|
116
|
+
const keepGoing = await ask(`\n ${dim('Set up a proxy anyway?')} ${dim('(y/N)')}: `);
|
|
117
|
+
if (keepGoing.toLowerCase() !== 'y') {
|
|
118
|
+
this.log('');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.log(` ${red('✘')} Connection blocked`);
|
|
124
|
+
this.log(` ${dim(direct.error || 'Could not reach Hyperliquid API')}`);
|
|
125
|
+
this.log('');
|
|
126
|
+
this.log(` ${dim("This is likely a geographic restriction. Let's fix it.")}`);
|
|
127
|
+
}
|
|
128
|
+
this.log('');
|
|
129
|
+
// Step 2: Options
|
|
130
|
+
this.log(` ${bold('How would you like to connect?')}`);
|
|
131
|
+
this.log('');
|
|
132
|
+
this.log(` ${cyan('1')} I already have a VPN running ${dim('— just test the connection')}`);
|
|
133
|
+
this.log(` ${cyan('2')} I have a proxy/SOCKS5 address ${dim('— paste it in')}`);
|
|
134
|
+
this.log(` ${cyan('3')} Help me set one up ${dim("— I'll walk you through it")}`);
|
|
135
|
+
this.log(` ${cyan('4')} Remove existing proxy config ${dim('— go back to direct connection')}`);
|
|
136
|
+
this.log('');
|
|
137
|
+
const choice = await ask(` ${cyan('>')} `);
|
|
138
|
+
switch (choice) {
|
|
139
|
+
case '1':
|
|
140
|
+
await this.optionVpnRunning();
|
|
141
|
+
break;
|
|
142
|
+
case '2':
|
|
143
|
+
await this.optionPasteProxy();
|
|
144
|
+
break;
|
|
145
|
+
case '3':
|
|
146
|
+
await this.optionGuidedSetup();
|
|
147
|
+
break;
|
|
148
|
+
case '4':
|
|
149
|
+
await this.optionClearProxy();
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
this.log(dim(' Invalid selection.'));
|
|
153
|
+
}
|
|
154
|
+
this.log('');
|
|
155
|
+
}
|
|
156
|
+
async optionVpnRunning() {
|
|
157
|
+
this.log('');
|
|
158
|
+
this.log(` ${dim('Testing connection with your VPN...')}`);
|
|
159
|
+
const result = await checkApi();
|
|
160
|
+
if (result.ok) {
|
|
161
|
+
this.log(` ${green('✔')} Connected! ${dim(`(${result.latency}ms, ${result.pairs} pairs)`)}`);
|
|
162
|
+
this.log('');
|
|
163
|
+
this.log(` ${dim('Your VPN is routing traffic correctly. No proxy config needed.')}`);
|
|
164
|
+
this.log(` ${dim('Just keep your VPN on when using RIFT.')}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.log(` ${red('✘')} Still blocked. ${dim(result.error || '')}`);
|
|
168
|
+
this.log('');
|
|
169
|
+
this.log(` ${dim('Your VPN may not be routing all traffic, or the exit node')}`);
|
|
170
|
+
this.log(` ${dim('is in a restricted region. Try a different server location,')}`);
|
|
171
|
+
this.log(` ${dim('or use option 2 to configure a SOCKS5 proxy.')}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async optionPasteProxy() {
|
|
175
|
+
this.log('');
|
|
176
|
+
this.log(` ${dim('Paste your proxy URL. Common formats:')}`);
|
|
177
|
+
this.log(` ${dim('socks5://127.0.0.1:1080')}`);
|
|
178
|
+
this.log(` ${dim('socks5://user:pass@host:port')}`);
|
|
179
|
+
this.log(` ${dim('http://host:port')}`);
|
|
180
|
+
this.log(` ${dim('https://host:port')}`);
|
|
181
|
+
this.log('');
|
|
182
|
+
const proxy = await ask(` ${cyan('Proxy URL')}: `);
|
|
183
|
+
if (!proxy) {
|
|
184
|
+
this.log(dim(' No proxy entered.'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.log('');
|
|
188
|
+
this.log(` ${dim('Testing connection via')} ${proxy} ${dim('...')}`);
|
|
189
|
+
const result = await checkApi(proxy);
|
|
190
|
+
if (result.ok) {
|
|
191
|
+
this.log(` ${green('✔')} Connected! ${dim(`(${result.latency}ms, ${result.pairs} pairs)`)}`);
|
|
192
|
+
this.log('');
|
|
193
|
+
await saveProxy(proxy);
|
|
194
|
+
this.log(` ${green('✔')} Proxy saved to ${dim('~/.rift/config.json')}`);
|
|
195
|
+
this.log(` ${dim('All RIFT API calls will now use this proxy.')}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
this.log(` ${red('✘')} Failed to connect via proxy`);
|
|
199
|
+
this.log(` ${dim(result.error || 'Unknown error')}`);
|
|
200
|
+
this.log('');
|
|
201
|
+
this.log(` ${dim('Check that the proxy is running and the URL is correct.')}`);
|
|
202
|
+
const saveAnyway = await ask(`\n ${dim('Save this proxy anyway?')} ${dim('(y/N)')}: `);
|
|
203
|
+
if (saveAnyway.toLowerCase() === 'y') {
|
|
204
|
+
await saveProxy(proxy);
|
|
205
|
+
this.log(` ${yellow('!')} Proxy saved (untested)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async optionGuidedSetup() {
|
|
210
|
+
this.log('');
|
|
211
|
+
// Detect installed VPN clients
|
|
212
|
+
const clients = detectVpnClients();
|
|
213
|
+
const installed = clients.filter(c => c.detected);
|
|
214
|
+
if (installed.length > 0) {
|
|
215
|
+
this.log(` ${bold('Detected VPN clients on your system:')}`);
|
|
216
|
+
this.log('');
|
|
217
|
+
for (const client of installed) {
|
|
218
|
+
this.log(` ${green('✔')} ${bold(client.name)}`);
|
|
219
|
+
this.log(` ${dim(client.hint)}`);
|
|
220
|
+
this.log('');
|
|
221
|
+
}
|
|
222
|
+
this.log(` ${dim('Connect using one of the above, then re-run:')}`);
|
|
223
|
+
this.log(` ${cyan('rift setup proxy')}`);
|
|
224
|
+
this.log(` ${dim('and select option 1 to test your connection.')}`);
|
|
225
|
+
this.log('');
|
|
226
|
+
this.log(` ${dim('─'.repeat(45))}`);
|
|
227
|
+
this.log('');
|
|
228
|
+
}
|
|
229
|
+
// SOCKS5 provider guides
|
|
230
|
+
this.log(` ${bold('Quick setup options:')}`);
|
|
231
|
+
this.log('');
|
|
232
|
+
this.log(` ${cyan('A')} ${bold('Mullvad SOCKS5')} ${dim('— easiest, $5/mo, no account needed')}`);
|
|
233
|
+
this.log(` ${dim('1. Go to mullvad.net and get an account number')}`);
|
|
234
|
+
this.log(` ${dim('2. Download & install Mullvad')}`);
|
|
235
|
+
this.log(` ${dim('3. Connect to any non-US server')}`);
|
|
236
|
+
this.log(` ${dim('4. SOCKS5 proxy is at: socks5://10.64.0.1:1080')}`);
|
|
237
|
+
this.log(` ${dim('5. Come back and run: rift setup proxy → option 2')}`);
|
|
238
|
+
this.log('');
|
|
239
|
+
this.log(` ${cyan('B')} ${bold('SSH tunnel')} ${dim('— if you have a VPS ($5/mo)')}`);
|
|
240
|
+
this.log(` ${dim('Run this in a separate terminal:')}`);
|
|
241
|
+
this.log(` ${cyan('ssh -D 1080 -N user@your-server-ip')}`);
|
|
242
|
+
this.log(` ${dim('Then come back and enter: socks5://127.0.0.1:1080')}`);
|
|
243
|
+
this.log('');
|
|
244
|
+
this.log(` ${cyan('C')} ${bold('NordVPN SOCKS5')} ${dim('— if you have NordVPN')}`);
|
|
245
|
+
this.log(` ${dim('1. Get SOCKS5 credentials from nordvpn.com/servers/socks')}`);
|
|
246
|
+
this.log(` ${dim('2. Use: socks5://user:pass@amsterdam.nl.socks.nordhold.net:1080')}`);
|
|
247
|
+
this.log('');
|
|
248
|
+
this.log(` ${cyan('D')} ${bold('ProtonVPN SOCKS5')} ${dim('— if you have ProtonVPN Plus')}`);
|
|
249
|
+
this.log(` ${dim('1. Enable SOCKS5 in ProtonVPN settings')}`);
|
|
250
|
+
this.log(` ${dim('2. Use: socks5://127.0.0.1:1080')}`);
|
|
251
|
+
this.log('');
|
|
252
|
+
this.log(` ${dim('─'.repeat(45))}`);
|
|
253
|
+
this.log('');
|
|
254
|
+
const ready = await ask(` ${dim('Ready to enter a proxy URL?')} ${dim('(y/N)')}: `);
|
|
255
|
+
if (ready.toLowerCase() === 'y') {
|
|
256
|
+
await this.optionPasteProxy();
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.log(` ${dim('No problem. Run')} ${cyan('rift setup proxy')} ${dim('when you\'re ready.')}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async optionClearProxy() {
|
|
263
|
+
await clearProxy();
|
|
264
|
+
this.log('');
|
|
265
|
+
this.log(` ${green('✔')} Proxy config removed. RIFT will connect directly.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class SpotBuy extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
coin: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
amount: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
size: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../../lib/engine-passthrough.js';
|
|
4
|
+
export default class SpotBuy extends GatedCommand {
|
|
5
|
+
static description = 'Buy a token on the Hyperliquid spot market. Simple purchase, no leverage. 1% builder fee on sell side only.';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift spot buy HYPE --amount 25 # spend 25 USDC buying HYPE',
|
|
8
|
+
'$ rift spot buy BTC --amount 50 # 50 USDC of BTC (auto-resolved to UBTC spot)',
|
|
9
|
+
'$ rift spot buy UETH --size 0.01 # buy a fixed 0.01 UETH',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
coin: Args.string({
|
|
13
|
+
description: 'Token to buy (e.g. HYPE, BTC, ETH — names auto-resolve to HL spot tokens like UBTC, UETH)',
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
amount: Flags.string({ description: 'USDC amount to spend (one of --amount or --size required)', default: '' }),
|
|
19
|
+
size: Flags.string({ description: 'Token amount to buy (alternative to --amount)', default: '' }),
|
|
20
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(SpotBuy);
|
|
24
|
+
const engineArgs = [args.coin];
|
|
25
|
+
if (flags.amount)
|
|
26
|
+
engineArgs.push('--amount', flags.amount);
|
|
27
|
+
if (flags.size)
|
|
28
|
+
engineArgs.push('--size', flags.size);
|
|
29
|
+
await passthroughToEngine({
|
|
30
|
+
command: 'buy',
|
|
31
|
+
args: engineArgs,
|
|
32
|
+
log: (m) => this.log(m),
|
|
33
|
+
error: (m) => this.error(m),
|
|
34
|
+
exit: (c) => this.exit(c),
|
|
35
|
+
jsonOnly: flags.json,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class SpotSell extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
coin: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
amount: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
pct: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../../lib/engine-passthrough.js';
|
|
4
|
+
export default class SpotSell extends GatedCommand {
|
|
5
|
+
static description = 'Sell a token from spot holdings back to USDC. 1% builder fee applies.';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift spot sell HYPE # sell all HYPE holdings',
|
|
8
|
+
'$ rift spot sell HYPE --pct 50 # sell half',
|
|
9
|
+
'$ rift spot sell HYPE --amount 5 # sell a fixed 5 HYPE',
|
|
10
|
+
'$ rift spot sell BTC # sell all UBTC (BTC alias)',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
coin: Args.string({
|
|
14
|
+
description: 'Token to sell (e.g. HYPE, BTC, ETH — names auto-resolve to HL spot tokens)',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static flags = {
|
|
19
|
+
amount: Flags.string({ description: 'Token amount to sell (0/omit = all)', default: '' }),
|
|
20
|
+
pct: Flags.string({ description: 'Percentage to sell (e.g. 50 = half)', default: '' }),
|
|
21
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(SpotSell);
|
|
25
|
+
const engineArgs = [args.coin];
|
|
26
|
+
if (flags.amount)
|
|
27
|
+
engineArgs.push('--amount', flags.amount);
|
|
28
|
+
if (flags.pct)
|
|
29
|
+
engineArgs.push('--pct', flags.pct);
|
|
30
|
+
await passthroughToEngine({
|
|
31
|
+
command: 'sell',
|
|
32
|
+
args: engineArgs,
|
|
33
|
+
log: (m) => this.log(m),
|
|
34
|
+
error: (m) => this.error(m),
|
|
35
|
+
exit: (c) => this.exit(c),
|
|
36
|
+
jsonOnly: flags.json,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { GatedCommand } from '../../lib/base-command.js';
|
|
2
|
+
import { runEngine } from '../../lib/python-bridge.js';
|
|
3
|
+
export default class StrategiesList extends GatedCommand {
|
|
4
|
+
static description = 'List available trading strategies';
|
|
5
|
+
static examples = [
|
|
6
|
+
'$ rift strategies list',
|
|
7
|
+
];
|
|
8
|
+
async run() {
|
|
9
|
+
await runEngine('strategies', [], (msg) => {
|
|
10
|
+
if (msg.type === 'result') {
|
|
11
|
+
const strategies = msg.strategies;
|
|
12
|
+
if (!strategies || strategies.length === 0) {
|
|
13
|
+
this.log('No strategies found. Create one with: rift new <name>');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.log('');
|
|
17
|
+
this.log(' Available strategies:');
|
|
18
|
+
this.log('');
|
|
19
|
+
for (const s of strategies) {
|
|
20
|
+
this.log(` ${s.name}`);
|
|
21
|
+
if (s.doc)
|
|
22
|
+
this.log(` ${s.doc.trim()}`);
|
|
23
|
+
if (Object.keys(s.config).length > 0) {
|
|
24
|
+
this.log(' Config:');
|
|
25
|
+
for (const [key, val] of Object.entries(s.config)) {
|
|
26
|
+
this.log(` ${key}: ${val}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.log('');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class Sweep extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
strategy: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
pair: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
equity: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
leverage: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
top: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
rank: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
private renderResult;
|
|
19
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Flags, Args } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
4
|
+
import { runEngine } from '../lib/python-bridge.js';
|
|
5
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
6
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
7
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
8
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
9
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
10
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
11
|
+
function colorNum(val, suffix = '') {
|
|
12
|
+
const str = `${val}${suffix}`;
|
|
13
|
+
if (val > 0)
|
|
14
|
+
return green(str);
|
|
15
|
+
if (val < 0)
|
|
16
|
+
return red(str);
|
|
17
|
+
return yellow(str);
|
|
18
|
+
}
|
|
19
|
+
export default class Sweep extends GatedCommand {
|
|
20
|
+
static description = 'Run a parameter sweep to find optimal strategy settings';
|
|
21
|
+
static examples = [
|
|
22
|
+
'$ rift sweep trend_follow --pair BTC --tf 4h',
|
|
23
|
+
'$ rift sweep my_strategy --config strategies/my_strategy/sweep.yaml',
|
|
24
|
+
'$ rift sweep trend_follow --pair BTC --tf 4h --rank sharpe --top 5',
|
|
25
|
+
];
|
|
26
|
+
static args = {
|
|
27
|
+
strategy: Args.string({ description: 'Strategy name', required: true }),
|
|
28
|
+
};
|
|
29
|
+
static flags = {
|
|
30
|
+
pair: Flags.string({ description: 'Trading pair', default: 'BTC-PERP' }),
|
|
31
|
+
tf: Flags.string({ description: 'Timeframe', default: '1h' }),
|
|
32
|
+
config: Flags.string({ description: 'Path to sweep.yaml config file' }),
|
|
33
|
+
equity: Flags.integer({ description: 'Starting equity', default: 10000 }),
|
|
34
|
+
leverage: Flags.integer({ description: 'Leverage multiplier', default: 1 }),
|
|
35
|
+
top: Flags.integer({ description: 'Number of top results to show', default: 10 }),
|
|
36
|
+
rank: Flags.string({ description: 'Rank by: sharpe, return, or profit_factor', default: 'sharpe', options: ['sharpe', 'return', 'profit_factor'] }),
|
|
37
|
+
};
|
|
38
|
+
async run() {
|
|
39
|
+
const { args, flags } = await this.parse(Sweep);
|
|
40
|
+
this.log('');
|
|
41
|
+
this.log(` ${bold('Parameter Sweep')}`);
|
|
42
|
+
this.log(` ${dim(`${args.strategy} on ${flags.pair} ${flags.tf} — ranked by ${flags.rank}`)}`);
|
|
43
|
+
this.log('');
|
|
44
|
+
const engineArgs = [
|
|
45
|
+
args.strategy,
|
|
46
|
+
'--pair', flags.pair,
|
|
47
|
+
'--tf', flags.tf,
|
|
48
|
+
'--equity', String(flags.equity),
|
|
49
|
+
'--leverage', String(flags.leverage),
|
|
50
|
+
'--top', String(flags.top),
|
|
51
|
+
'--rank', flags.rank,
|
|
52
|
+
];
|
|
53
|
+
// Anchor relative config paths to the user's cwd — the engine
|
|
54
|
+
// subprocess runs with cwd=engine/, so a bare "strategies/.../sweep.yaml"
|
|
55
|
+
// would otherwise resolve to engine/strategies/.../sweep.yaml.
|
|
56
|
+
if (flags.config)
|
|
57
|
+
engineArgs.push('--config', path.resolve(flags.config));
|
|
58
|
+
await runEngine('sweep', engineArgs, (msg) => {
|
|
59
|
+
if (msg.type === 'progress' && msg.msg) {
|
|
60
|
+
// Compact progress display — just combo count + ETA
|
|
61
|
+
const msgStr = String(msg.msg);
|
|
62
|
+
const match = msgStr.match(/Combo (\d+)\/(\d+)(.*?ETA \S+)?/);
|
|
63
|
+
const display = match
|
|
64
|
+
? `Combo ${match[1]}/${match[2]}${match[3] ? ' — ' + match[3].trim() : ''}`
|
|
65
|
+
: msgStr;
|
|
66
|
+
process.stdout.write(`\x1b[2K\r ${dim(display)}`);
|
|
67
|
+
}
|
|
68
|
+
else if (msg.type === 'result') {
|
|
69
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
70
|
+
this.renderResult(msg);
|
|
71
|
+
}
|
|
72
|
+
else if (msg.type === 'error') {
|
|
73
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
74
|
+
this.error(msg.msg);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
renderResult(msg) {
|
|
79
|
+
const total = msg.total_combos;
|
|
80
|
+
const completed = msg.completed;
|
|
81
|
+
const rankBy = msg.rank_by;
|
|
82
|
+
const topEntries = msg.top;
|
|
83
|
+
this.log(` ${dim(`Tested ${completed}/${total} parameter combinations`)}`);
|
|
84
|
+
this.log('');
|
|
85
|
+
if (!topEntries || topEntries.length === 0) {
|
|
86
|
+
this.log(` ${yellow('No results.')}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Get all param keys for the header
|
|
90
|
+
const paramKeys = Object.keys(topEntries[0].params);
|
|
91
|
+
// Build table header
|
|
92
|
+
const rankLabel = rankBy === 'return' ? 'Return' : rankBy === 'profit_factor' ? 'PF' : 'Sharpe';
|
|
93
|
+
this.log(dim(' ── Top Results ──'));
|
|
94
|
+
this.log('');
|
|
95
|
+
// Header
|
|
96
|
+
let header = ` ${dim('│')} ${'#'.padEnd(4)}`;
|
|
97
|
+
for (const key of paramKeys) {
|
|
98
|
+
header += `${key.padEnd(14)}`;
|
|
99
|
+
}
|
|
100
|
+
header += `${'Return'.padEnd(12)}${'Sharpe'.padEnd(12)}${'Win%'.padEnd(10)}${'Trades'.padEnd(8)}${'MaxDD'.padEnd(12)} ${dim('│')}`;
|
|
101
|
+
const hrLen = header.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
102
|
+
const hr = ` ${dim('─'.repeat(hrLen - 2))}`;
|
|
103
|
+
this.log(hr);
|
|
104
|
+
this.log(header);
|
|
105
|
+
this.log(hr);
|
|
106
|
+
for (let i = 0; i < topEntries.length; i++) {
|
|
107
|
+
const e = topEntries[i];
|
|
108
|
+
const m = e.metrics;
|
|
109
|
+
let row = ` ${dim('│')} ${bold(String(i + 1).padEnd(4))}`;
|
|
110
|
+
for (const key of paramKeys) {
|
|
111
|
+
row += `${String(e.params[key]).padEnd(14)}`;
|
|
112
|
+
}
|
|
113
|
+
const retStr = `${m.total_return_pct}%`;
|
|
114
|
+
const sharpeStr = `${m.sharpe_ratio}`;
|
|
115
|
+
const winStr = `${m.win_rate}%`;
|
|
116
|
+
const tradesStr = `${m.num_trades}`;
|
|
117
|
+
const ddStr = `${m.max_drawdown_pct}%`;
|
|
118
|
+
row += `${colorNum(m.total_return_pct, '%')}${' '.repeat(Math.max(1, 12 - retStr.length))}`;
|
|
119
|
+
row += `${colorNum(m.sharpe_ratio)}${' '.repeat(Math.max(1, 12 - sharpeStr.length))}`;
|
|
120
|
+
row += `${winStr.padEnd(10)}`;
|
|
121
|
+
row += `${tradesStr.padEnd(8)}`;
|
|
122
|
+
row += `${colorNum(m.max_drawdown_pct, '%')}${' '.repeat(Math.max(1, 12 - ddStr.length))}`;
|
|
123
|
+
row += dim('│');
|
|
124
|
+
this.log(row);
|
|
125
|
+
}
|
|
126
|
+
this.log(hr);
|
|
127
|
+
// Show best config
|
|
128
|
+
const best = topEntries[0];
|
|
129
|
+
this.log('');
|
|
130
|
+
this.log(` ${green('★')} Best by ${rankLabel}:`);
|
|
131
|
+
for (const [key, val] of Object.entries(best.params)) {
|
|
132
|
+
this.log(` ${key}: ${bold(String(val))}`);
|
|
133
|
+
}
|
|
134
|
+
this.log(` → ${colorNum(best.metrics.total_return_pct, '% return')}, Sharpe ${colorNum(best.metrics.sharpe_ratio)}`);
|
|
135
|
+
this.log('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
2
|
+
export default class Sync extends GatedCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
coins: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
tf: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
start: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
end: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'no-funding': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'aws-key': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'aws-secret': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { GatedCommand } from '../lib/base-command.js';
|
|
3
|
+
import { passthroughToEngine } from '../lib/engine-passthrough.js';
|
|
4
|
+
export default class Sync extends GatedCommand {
|
|
5
|
+
static description = 'Sync historical OHLCV + funding data from the Hyperliquid S3 archive. Requires AWS credentials (free tier; ~$2 for the full download).';
|
|
6
|
+
static examples = [
|
|
7
|
+
'$ rift sync --coins BTC --tf 1h,4h',
|
|
8
|
+
'$ rift sync --coins BTC,ETH,SOL --tf 5m,15m,1h,4h --start 2024-01-01',
|
|
9
|
+
'$ rift sync --aws-key AKIA... --aws-secret ... --coins BTC --tf 1h',
|
|
10
|
+
];
|
|
11
|
+
static flags = {
|
|
12
|
+
coins: Flags.string({
|
|
13
|
+
description: 'Comma-separated coins (empty = auto-detect from registered strategies)',
|
|
14
|
+
default: '',
|
|
15
|
+
}),
|
|
16
|
+
tf: Flags.string({
|
|
17
|
+
description: 'Comma-separated timeframes to build',
|
|
18
|
+
default: '5m,15m,1h,4h',
|
|
19
|
+
}),
|
|
20
|
+
start: Flags.string({ description: 'Start date YYYY-MM-DD', default: '2023-09-01' }),
|
|
21
|
+
end: Flags.string({ description: 'End date YYYY-MM-DD (default: today)', default: '' }),
|
|
22
|
+
'no-funding': Flags.boolean({ description: 'Skip funding rate sync', default: false }),
|
|
23
|
+
full: Flags.boolean({ description: 'Full sync (ignore incremental cache)', default: false }),
|
|
24
|
+
'aws-key': Flags.string({ description: 'AWS Access Key ID (for non-interactive setup)', default: '' }),
|
|
25
|
+
'aws-secret': Flags.string({ description: 'AWS Secret Access Key', default: '' }),
|
|
26
|
+
json: Flags.boolean({ description: 'Emit raw JSON only', default: false }),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { flags } = await this.parse(Sync);
|
|
30
|
+
const args = [];
|
|
31
|
+
if (flags.coins)
|
|
32
|
+
args.push('--coins', flags.coins);
|
|
33
|
+
args.push('--tf', flags.tf);
|
|
34
|
+
args.push('--start', flags.start);
|
|
35
|
+
if (flags.end)
|
|
36
|
+
args.push('--end', flags.end);
|
|
37
|
+
if (flags['no-funding'])
|
|
38
|
+
args.push('--no-funding');
|
|
39
|
+
if (flags.full)
|
|
40
|
+
args.push('--full');
|
|
41
|
+
if (flags['aws-key'])
|
|
42
|
+
args.push('--aws-key', flags['aws-key']);
|
|
43
|
+
if (flags['aws-secret'])
|
|
44
|
+
args.push('--aws-secret', flags['aws-secret']);
|
|
45
|
+
await passthroughToEngine({
|
|
46
|
+
command: 'sync',
|
|
47
|
+
args,
|
|
48
|
+
log: (m) => this.log(m),
|
|
49
|
+
error: (m) => this.error(m),
|
|
50
|
+
exit: (c) => this.exit(c),
|
|
51
|
+
jsonOnly: flags.json,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|