@darksol/terminal 0.2.1 → 0.3.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/README.md +263 -189
- package/package.json +11 -9
- package/src/cli.js +31 -1
- package/src/config/keys.js +53 -1
- package/src/llm/intent.js +126 -0
- package/src/setup/wizard.js +516 -0
- package/src/ui/banner.js +4 -2
- package/tests/cli.test.js +0 -72
- package/tests/config.test.js +0 -75
- package/tests/dca.test.js +0 -141
- package/tests/keystore.test.js +0 -94
- package/tests/scripts.test.js +0 -136
- package/tests/trading.test.js +0 -21
- package/tests/ui.test.js +0 -27
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { theme } from '../ui/theme.js';
|
|
3
|
+
import { showSection, showDivider } from '../ui/banner.js';
|
|
4
|
+
import { success, error, warn, info, kvDisplay } from '../ui/components.js';
|
|
5
|
+
import { getConfig, setConfig } from '../config/store.js';
|
|
6
|
+
import { addKeyDirect, hasKey, SERVICES } from '../config/keys.js';
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
// ══════════════════════════════════════════════════
|
|
12
|
+
// FIRST-RUN SETUP WIZARD
|
|
13
|
+
// ══════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this is a first run (no LLM keys configured)
|
|
17
|
+
*/
|
|
18
|
+
export function isFirstRun() {
|
|
19
|
+
const hasAnyLLM = ['openai', 'anthropic', 'openrouter', 'ollama'].some(s => hasKey(s));
|
|
20
|
+
const setupDone = getConfig('setupComplete');
|
|
21
|
+
return !hasAnyLLM && !setupDone;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run the setup wizard
|
|
26
|
+
*/
|
|
27
|
+
export async function runSetupWizard(opts = {}) {
|
|
28
|
+
const force = opts.force || false;
|
|
29
|
+
|
|
30
|
+
if (!force && !isFirstRun()) {
|
|
31
|
+
info('Setup already complete. Use --force to re-run.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('');
|
|
36
|
+
showSection('🌑 DARKSOL TERMINAL — FIRST RUN SETUP');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(theme.dim(' Welcome to DARKSOL Terminal. Let\'s get you set up.'));
|
|
39
|
+
console.log(theme.dim(' You need an LLM provider to use the AI trading assistant.'));
|
|
40
|
+
console.log(theme.dim(' Everything else works without one.'));
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
showDivider();
|
|
44
|
+
|
|
45
|
+
// Step 1: Choose LLM provider
|
|
46
|
+
const { provider } = await inquirer.prompt([{
|
|
47
|
+
type: 'list',
|
|
48
|
+
name: 'provider',
|
|
49
|
+
message: theme.gold('Choose your AI provider:'),
|
|
50
|
+
choices: [
|
|
51
|
+
{ name: '🤖 OpenAI (GPT-4o, GPT-5) — API key or OAuth', value: 'openai' },
|
|
52
|
+
{ name: '🧠 Anthropic (Claude Opus, Sonnet) — API key or OAuth', value: 'anthropic' },
|
|
53
|
+
{ name: '🔀 OpenRouter (any model, one key) — API key', value: 'openrouter' },
|
|
54
|
+
{ name: '🏠 Ollama (local models, free, private) — no key needed', value: 'ollama' },
|
|
55
|
+
{ name: '⏭️ Skip for now', value: 'skip' },
|
|
56
|
+
],
|
|
57
|
+
}]);
|
|
58
|
+
|
|
59
|
+
if (provider === 'skip') {
|
|
60
|
+
warn('Skipped LLM setup. You can set up later with: darksol setup');
|
|
61
|
+
setConfig('setupComplete', true);
|
|
62
|
+
showPostSetup();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (provider === 'ollama') {
|
|
67
|
+
await setupOllama();
|
|
68
|
+
} else {
|
|
69
|
+
await setupCloudProvider(provider);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2: Chain selection
|
|
73
|
+
console.log('');
|
|
74
|
+
const { chain } = await inquirer.prompt([{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'chain',
|
|
77
|
+
message: theme.gold('Default chain:'),
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: 'Base (recommended — low fees, fast)', value: 'base' },
|
|
80
|
+
{ name: 'Ethereum (mainnet)', value: 'ethereum' },
|
|
81
|
+
{ name: 'Arbitrum', value: 'arbitrum' },
|
|
82
|
+
{ name: 'Optimism', value: 'optimism' },
|
|
83
|
+
{ name: 'Polygon', value: 'polygon' },
|
|
84
|
+
],
|
|
85
|
+
default: 'base',
|
|
86
|
+
}]);
|
|
87
|
+
setConfig('chain', chain);
|
|
88
|
+
success(`Chain set to ${chain}`);
|
|
89
|
+
|
|
90
|
+
// Step 3: Wallet
|
|
91
|
+
console.log('');
|
|
92
|
+
const { createWallet } = await inquirer.prompt([{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'createWallet',
|
|
95
|
+
message: theme.gold('Create a wallet now?'),
|
|
96
|
+
default: true,
|
|
97
|
+
}]);
|
|
98
|
+
|
|
99
|
+
if (createWallet) {
|
|
100
|
+
const { createNewWallet } = await import('../wallet/manager.js');
|
|
101
|
+
await createNewWallet();
|
|
102
|
+
} else {
|
|
103
|
+
info('Create one later: darksol wallet create <name>');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setConfig('setupComplete', true);
|
|
107
|
+
showPostSetup();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Setup a cloud provider (OpenAI, Anthropic, OpenRouter)
|
|
112
|
+
*/
|
|
113
|
+
async function setupCloudProvider(provider) {
|
|
114
|
+
const supportsOAuth = ['openai', 'anthropic'].includes(provider);
|
|
115
|
+
const providerName = {
|
|
116
|
+
openai: 'OpenAI',
|
|
117
|
+
anthropic: 'Anthropic',
|
|
118
|
+
openrouter: 'OpenRouter',
|
|
119
|
+
}[provider];
|
|
120
|
+
|
|
121
|
+
if (supportsOAuth) {
|
|
122
|
+
const { method } = await inquirer.prompt([{
|
|
123
|
+
type: 'list',
|
|
124
|
+
name: 'method',
|
|
125
|
+
message: theme.gold(`How do you want to connect ${providerName}?`),
|
|
126
|
+
choices: [
|
|
127
|
+
{ name: `🔑 API Key — paste your ${providerName} API key`, value: 'apikey' },
|
|
128
|
+
{ name: `🌐 OAuth — sign in with your ${providerName} account`, value: 'oauth' },
|
|
129
|
+
{ name: `📋 Instructions — show me how to get a key`, value: 'help' },
|
|
130
|
+
],
|
|
131
|
+
}]);
|
|
132
|
+
|
|
133
|
+
if (method === 'apikey') {
|
|
134
|
+
await setupAPIKey(provider);
|
|
135
|
+
} else if (method === 'oauth') {
|
|
136
|
+
await startOAuth(provider);
|
|
137
|
+
} else {
|
|
138
|
+
showKeyInstructions(provider);
|
|
139
|
+
// After showing instructions, ask for key
|
|
140
|
+
await setupAPIKey(provider);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
await setupAPIKey(provider);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Setup via API key entry
|
|
149
|
+
*/
|
|
150
|
+
async function setupAPIKey(provider) {
|
|
151
|
+
const providerName = {
|
|
152
|
+
openai: 'OpenAI',
|
|
153
|
+
anthropic: 'Anthropic',
|
|
154
|
+
openrouter: 'OpenRouter',
|
|
155
|
+
}[provider];
|
|
156
|
+
|
|
157
|
+
const { key } = await inquirer.prompt([{
|
|
158
|
+
type: 'password',
|
|
159
|
+
name: 'key',
|
|
160
|
+
message: theme.gold(`${providerName} API key:`),
|
|
161
|
+
mask: '●',
|
|
162
|
+
validate: (v) => {
|
|
163
|
+
if (!v || v.length < 10) return 'Key seems too short';
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
}]);
|
|
167
|
+
|
|
168
|
+
addKeyDirect(provider, key);
|
|
169
|
+
success(`${providerName} key saved (encrypted)`);
|
|
170
|
+
|
|
171
|
+
// Set as default provider
|
|
172
|
+
setConfig('llmProvider', provider);
|
|
173
|
+
info(`Default AI provider set to ${provider}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Setup Ollama (local)
|
|
178
|
+
*/
|
|
179
|
+
async function setupOllama() {
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(theme.gold(' OLLAMA SETUP'));
|
|
182
|
+
console.log(theme.dim(' Ollama runs models locally — free, private, no API key needed.'));
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
const { host } = await inquirer.prompt([{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'host',
|
|
188
|
+
message: theme.gold('Ollama host:'),
|
|
189
|
+
default: 'http://localhost:11434',
|
|
190
|
+
}]);
|
|
191
|
+
|
|
192
|
+
setConfig('ollamaHost', host);
|
|
193
|
+
|
|
194
|
+
const { model } = await inquirer.prompt([{
|
|
195
|
+
type: 'input',
|
|
196
|
+
name: 'model',
|
|
197
|
+
message: theme.gold('Default model:'),
|
|
198
|
+
default: 'llama3',
|
|
199
|
+
}]);
|
|
200
|
+
|
|
201
|
+
setConfig('ollamaModel', model);
|
|
202
|
+
setConfig('llmProvider', 'ollama');
|
|
203
|
+
|
|
204
|
+
success(`Ollama configured: ${host} / ${model}`);
|
|
205
|
+
info('Make sure Ollama is running: ollama serve');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Show instructions for getting API keys
|
|
210
|
+
*/
|
|
211
|
+
function showKeyInstructions(provider) {
|
|
212
|
+
console.log('');
|
|
213
|
+
|
|
214
|
+
if (provider === 'openai') {
|
|
215
|
+
showSection('GET AN OPENAI API KEY');
|
|
216
|
+
console.log(theme.dim(' 1. Go to https://platform.openai.com/api-keys'));
|
|
217
|
+
console.log(theme.dim(' 2. Click "Create new secret key"'));
|
|
218
|
+
console.log(theme.dim(' 3. Copy the key (starts with sk-)'));
|
|
219
|
+
console.log(theme.dim(' 4. Paste it below'));
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(theme.dim(' 💡 If you have a ChatGPT Plus/Pro subscription,'));
|
|
222
|
+
console.log(theme.dim(' you can use OAuth instead (sign in with your account).'));
|
|
223
|
+
} else if (provider === 'anthropic') {
|
|
224
|
+
showSection('GET AN ANTHROPIC API KEY');
|
|
225
|
+
console.log(theme.dim(' 1. Go to https://console.anthropic.com/settings/keys'));
|
|
226
|
+
console.log(theme.dim(' 2. Click "Create Key"'));
|
|
227
|
+
console.log(theme.dim(' 3. Copy the key (starts with sk-ant-)'));
|
|
228
|
+
console.log(theme.dim(' 4. Paste it below'));
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(theme.dim(' 💡 If you have a Claude Pro/Team subscription,'));
|
|
231
|
+
console.log(theme.dim(' you can use OAuth instead.'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ══════════════════════════════════════════════════
|
|
238
|
+
// OAuth FLOWS
|
|
239
|
+
// ══════════════════════════════════════════════════
|
|
240
|
+
|
|
241
|
+
// OAuth configurations
|
|
242
|
+
const OAUTH_CONFIGS = {
|
|
243
|
+
openai: {
|
|
244
|
+
name: 'OpenAI',
|
|
245
|
+
authUrl: 'https://auth.openai.com/authorize',
|
|
246
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
247
|
+
// These are placeholder client IDs — users need to register their own app
|
|
248
|
+
// or use the direct API key flow
|
|
249
|
+
clientId: null,
|
|
250
|
+
scopes: ['openid', 'profile'],
|
|
251
|
+
helpUrl: 'https://platform.openai.com/docs/guides/authentication',
|
|
252
|
+
},
|
|
253
|
+
anthropic: {
|
|
254
|
+
name: 'Anthropic',
|
|
255
|
+
authUrl: 'https://console.anthropic.com/oauth/authorize',
|
|
256
|
+
tokenUrl: 'https://console.anthropic.com/oauth/token',
|
|
257
|
+
clientId: null,
|
|
258
|
+
scopes: ['api'],
|
|
259
|
+
helpUrl: 'https://docs.anthropic.com/en/docs/authentication',
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Start OAuth flow for a provider
|
|
265
|
+
*/
|
|
266
|
+
async function startOAuth(provider) {
|
|
267
|
+
const config = OAUTH_CONFIGS[provider];
|
|
268
|
+
|
|
269
|
+
// Check if provider has public OAuth available
|
|
270
|
+
// As of 2026, OpenAI and Anthropic have limited OAuth — API keys are more common
|
|
271
|
+
console.log('');
|
|
272
|
+
showSection(`${config.name} OAuth`);
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(theme.dim(' OAuth lets you sign in with your existing subscription'));
|
|
275
|
+
console.log(theme.dim(' without creating a separate API key.'));
|
|
276
|
+
console.log('');
|
|
277
|
+
|
|
278
|
+
// Check for custom client ID (user may have registered an OAuth app)
|
|
279
|
+
const storedClientId = getConfig(`oauth_${provider}_clientId`);
|
|
280
|
+
|
|
281
|
+
if (!storedClientId && !config.clientId) {
|
|
282
|
+
// No OAuth app registered — offer alternatives
|
|
283
|
+
console.log(theme.accent(' ⚠️ OAuth requires a registered application.'));
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(theme.dim(' Options:'));
|
|
286
|
+
console.log(theme.dim(` 1. Register an OAuth app at ${config.helpUrl}`));
|
|
287
|
+
console.log(theme.dim(' 2. Use an API key instead (faster, simpler)'));
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
const { oauthChoice } = await inquirer.prompt([{
|
|
291
|
+
type: 'list',
|
|
292
|
+
name: 'oauthChoice',
|
|
293
|
+
message: theme.gold('How to proceed?'),
|
|
294
|
+
choices: [
|
|
295
|
+
{ name: '🔑 Use API key instead (recommended)', value: 'apikey' },
|
|
296
|
+
{ name: '📝 Enter my OAuth client ID', value: 'clientid' },
|
|
297
|
+
{ name: '🌐 Open registration page in browser', value: 'register' },
|
|
298
|
+
],
|
|
299
|
+
}]);
|
|
300
|
+
|
|
301
|
+
if (oauthChoice === 'apikey') {
|
|
302
|
+
await setupAPIKey(provider);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (oauthChoice === 'register') {
|
|
307
|
+
try {
|
|
308
|
+
await open(config.helpUrl);
|
|
309
|
+
info(`Opened ${config.helpUrl} in your browser`);
|
|
310
|
+
} catch {
|
|
311
|
+
info(`Go to: ${config.helpUrl}`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
const { hasClientId } = await inquirer.prompt([{
|
|
315
|
+
type: 'confirm',
|
|
316
|
+
name: 'hasClientId',
|
|
317
|
+
message: theme.gold('Do you have a client ID now?'),
|
|
318
|
+
default: false,
|
|
319
|
+
}]);
|
|
320
|
+
if (!hasClientId) {
|
|
321
|
+
info('No problem — use an API key for now.');
|
|
322
|
+
await setupAPIKey(provider);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Get client ID from user
|
|
328
|
+
const { clientId } = await inquirer.prompt([{
|
|
329
|
+
type: 'input',
|
|
330
|
+
name: 'clientId',
|
|
331
|
+
message: theme.gold('OAuth Client ID:'),
|
|
332
|
+
validate: (v) => v.length > 5 || 'Client ID seems too short',
|
|
333
|
+
}]);
|
|
334
|
+
|
|
335
|
+
const { clientSecret } = await inquirer.prompt([{
|
|
336
|
+
type: 'password',
|
|
337
|
+
name: 'clientSecret',
|
|
338
|
+
message: theme.gold('OAuth Client Secret:'),
|
|
339
|
+
mask: '●',
|
|
340
|
+
}]);
|
|
341
|
+
|
|
342
|
+
setConfig(`oauth_${provider}_clientId`, clientId);
|
|
343
|
+
if (clientSecret) {
|
|
344
|
+
addKeyDirect(`${provider}_oauth_secret`, clientSecret);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await executeOAuthFlow(provider, clientId, clientSecret);
|
|
348
|
+
} else {
|
|
349
|
+
const clientId = storedClientId || config.clientId;
|
|
350
|
+
const clientSecret = getKey(`${provider}_oauth_secret`);
|
|
351
|
+
await executeOAuthFlow(provider, clientId, clientSecret);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Execute the OAuth authorization code flow
|
|
357
|
+
*/
|
|
358
|
+
async function executeOAuthFlow(provider, clientId, clientSecret) {
|
|
359
|
+
const config = OAUTH_CONFIGS[provider];
|
|
360
|
+
const port = 19876; // Local callback port
|
|
361
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
362
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
363
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
364
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
365
|
+
|
|
366
|
+
// Build auth URL
|
|
367
|
+
const params = new URLSearchParams({
|
|
368
|
+
response_type: 'code',
|
|
369
|
+
client_id: clientId,
|
|
370
|
+
redirect_uri: redirectUri,
|
|
371
|
+
scope: config.scopes.join(' '),
|
|
372
|
+
state,
|
|
373
|
+
code_challenge: codeChallenge,
|
|
374
|
+
code_challenge_method: 'S256',
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const authUrl = `${config.authUrl}?${params}`;
|
|
378
|
+
|
|
379
|
+
// Start local server to receive callback
|
|
380
|
+
return new Promise(async (resolve) => {
|
|
381
|
+
const server = createServer(async (req, res) => {
|
|
382
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
383
|
+
|
|
384
|
+
if (url.pathname === '/callback') {
|
|
385
|
+
const code = url.searchParams.get('code');
|
|
386
|
+
const returnedState = url.searchParams.get('state');
|
|
387
|
+
const err = url.searchParams.get('error');
|
|
388
|
+
|
|
389
|
+
if (err) {
|
|
390
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
391
|
+
res.end('<html><body><h2>❌ Authorization failed</h2><p>You can close this window.</p></body></html>');
|
|
392
|
+
error(`OAuth error: ${err}`);
|
|
393
|
+
server.close();
|
|
394
|
+
resolve(false);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (returnedState !== state) {
|
|
399
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
400
|
+
res.end('<html><body><h2>❌ State mismatch</h2><p>Possible CSRF. You can close this window.</p></body></html>');
|
|
401
|
+
error('OAuth state mismatch — possible security issue');
|
|
402
|
+
server.close();
|
|
403
|
+
resolve(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Exchange code for token
|
|
408
|
+
try {
|
|
409
|
+
const fetch = (await import('node-fetch')).default;
|
|
410
|
+
const tokenResp = await fetch(config.tokenUrl, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
413
|
+
body: new URLSearchParams({
|
|
414
|
+
grant_type: 'authorization_code',
|
|
415
|
+
code,
|
|
416
|
+
redirect_uri: redirectUri,
|
|
417
|
+
client_id: clientId,
|
|
418
|
+
...(clientSecret ? { client_secret: clientSecret } : {}),
|
|
419
|
+
code_verifier: codeVerifier,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const tokenData = await tokenResp.json();
|
|
424
|
+
|
|
425
|
+
if (tokenData.access_token) {
|
|
426
|
+
// Store the token as the API key
|
|
427
|
+
addKeyDirect(provider, tokenData.access_token);
|
|
428
|
+
if (tokenData.refresh_token) {
|
|
429
|
+
addKeyDirect(`${provider}_refresh`, tokenData.refresh_token);
|
|
430
|
+
}
|
|
431
|
+
setConfig('llmProvider', provider);
|
|
432
|
+
|
|
433
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
434
|
+
res.end(`<html><body style="background:#1a1a2e;color:#d4a574;font-family:monospace;text-align:center;padding:60px"><h2>✅ DARKSOL Terminal — Connected to ${config.name}</h2><p>You can close this window.</p></body></html>`);
|
|
435
|
+
|
|
436
|
+
success(`${config.name} connected via OAuth`);
|
|
437
|
+
info(`Token stored (encrypted). Provider set to ${provider}.`);
|
|
438
|
+
} else {
|
|
439
|
+
throw new Error(tokenData.error || 'No access token in response');
|
|
440
|
+
}
|
|
441
|
+
} catch (tokenErr) {
|
|
442
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
443
|
+
res.end('<html><body><h2>❌ Token exchange failed</h2><p>You can close this window.</p></body></html>');
|
|
444
|
+
error(`Token exchange failed: ${tokenErr.message}`);
|
|
445
|
+
info('Try using an API key instead: darksol keys add ' + provider);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
server.close();
|
|
449
|
+
resolve(true);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
server.listen(port, '127.0.0.1', async () => {
|
|
454
|
+
console.log('');
|
|
455
|
+
info(`Opening ${config.name} authorization page...`);
|
|
456
|
+
console.log(theme.dim(` If browser doesn't open, go to:`));
|
|
457
|
+
console.log(theme.accent(` ${authUrl}`));
|
|
458
|
+
console.log('');
|
|
459
|
+
info('Waiting for authorization...');
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await open(authUrl);
|
|
463
|
+
} catch {
|
|
464
|
+
warn('Could not open browser automatically');
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Timeout after 5 minutes
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
warn('OAuth timed out (5 minutes)');
|
|
471
|
+
server.close();
|
|
472
|
+
resolve(false);
|
|
473
|
+
}, 300000);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ══════════════════════════════════════════════════
|
|
478
|
+
// POST-SETUP & HELPERS
|
|
479
|
+
// ══════════════════════════════════════════════════
|
|
480
|
+
|
|
481
|
+
function showPostSetup() {
|
|
482
|
+
console.log('');
|
|
483
|
+
showSection('🌑 YOU\'RE READY');
|
|
484
|
+
console.log('');
|
|
485
|
+
console.log(theme.gold(' Next steps:'));
|
|
486
|
+
console.log(theme.dim(' • darksol ai chat Start the AI trading assistant'));
|
|
487
|
+
console.log(theme.dim(' • darksol market top See what\'s moving'));
|
|
488
|
+
console.log(theme.dim(' • darksol wallet create Create an encrypted wallet'));
|
|
489
|
+
console.log(theme.dim(' • darksol tips Trading tips & tricks'));
|
|
490
|
+
console.log(theme.dim(' • darksol quickstart Full getting started guide'));
|
|
491
|
+
console.log('');
|
|
492
|
+
console.log(theme.dim(' Re-run setup anytime: darksol setup --force'));
|
|
493
|
+
console.log('');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Quick check on startup — if first run, prompt setup
|
|
498
|
+
*/
|
|
499
|
+
export async function checkFirstRun() {
|
|
500
|
+
if (isFirstRun()) {
|
|
501
|
+
console.log('');
|
|
502
|
+
warn('No AI provider configured yet.');
|
|
503
|
+
const { runSetup } = await inquirer.prompt([{
|
|
504
|
+
type: 'confirm',
|
|
505
|
+
name: 'runSetup',
|
|
506
|
+
message: theme.gold('Run setup wizard?'),
|
|
507
|
+
default: true,
|
|
508
|
+
}]);
|
|
509
|
+
if (runSetup) {
|
|
510
|
+
await runSetupWizard();
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
info('Skip for now. Run later: darksol setup');
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
package/src/ui/banner.js
CHANGED
|
@@ -26,7 +26,7 @@ export function showBanner(opts = {}) {
|
|
|
26
26
|
);
|
|
27
27
|
console.log(
|
|
28
28
|
theme.dim(' ║ ') +
|
|
29
|
-
theme.subtle(' v0.
|
|
29
|
+
theme.subtle(' v0.3.0') +
|
|
30
30
|
theme.dim(' ') +
|
|
31
31
|
theme.gold('🌑') +
|
|
32
32
|
theme.dim(' ║')
|
|
@@ -44,7 +44,7 @@ export function showBanner(opts = {}) {
|
|
|
44
44
|
|
|
45
45
|
export function showMiniBanner() {
|
|
46
46
|
console.log('');
|
|
47
|
-
console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.
|
|
47
|
+
console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.3.0'));
|
|
48
48
|
console.log(theme.dim(' ─────────────────────────────'));
|
|
49
49
|
console.log('');
|
|
50
50
|
}
|
|
@@ -61,3 +61,5 @@ export function showDivider() {
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
|
|
65
|
+
|
package/tests/cli.test.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
function runCli(args, env) {
|
|
9
|
-
return spawnSync(process.execPath, ['bin/darksol.js', ...args], {
|
|
10
|
-
cwd: process.cwd(),
|
|
11
|
-
encoding: 'utf8',
|
|
12
|
-
env,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let tempRoot;
|
|
17
|
-
let env;
|
|
18
|
-
|
|
19
|
-
before(() => {
|
|
20
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-cli-'));
|
|
21
|
-
const home = join(tempRoot, 'home');
|
|
22
|
-
const appData = join(tempRoot, 'appdata');
|
|
23
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
24
|
-
mkdirSync(home, { recursive: true });
|
|
25
|
-
mkdirSync(appData, { recursive: true });
|
|
26
|
-
mkdirSync(localAppData, { recursive: true });
|
|
27
|
-
|
|
28
|
-
env = {
|
|
29
|
-
...process.env,
|
|
30
|
-
HOME: home,
|
|
31
|
-
USERPROFILE: home,
|
|
32
|
-
APPDATA: appData,
|
|
33
|
-
LOCALAPPDATA: localAppData,
|
|
34
|
-
FORCE_COLOR: '0',
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
after(() => {
|
|
39
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('CLI help output renders', (t) => {
|
|
43
|
-
const res = runCli(['--help'], env);
|
|
44
|
-
if (res.error && res.error.code === 'EPERM') {
|
|
45
|
-
t.skip('Child process spawn not permitted in this environment');
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
assert.equal(res.status, 0, res.stderr);
|
|
49
|
-
assert.match(res.stdout, /Usage:/);
|
|
50
|
-
assert.match(res.stdout, /\bwallet\b/);
|
|
51
|
-
assert.match(res.stdout, /\btrade\b/);
|
|
52
|
-
assert.match(res.stdout, /\bscript\b/);
|
|
53
|
-
assert.match(res.stdout, /\bconfig\b/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('command registration includes trade and script subcommands', (t) => {
|
|
57
|
-
const tradeHelp = runCli(['trade', '--help'], env);
|
|
58
|
-
if (tradeHelp.error && tradeHelp.error.code === 'EPERM') {
|
|
59
|
-
t.skip('Child process spawn not permitted in this environment');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
assert.equal(tradeHelp.status, 0, tradeHelp.stderr);
|
|
63
|
-
assert.match(tradeHelp.stdout, /\bswap\b/);
|
|
64
|
-
assert.match(tradeHelp.stdout, /\bsnipe\b/);
|
|
65
|
-
assert.match(tradeHelp.stdout, /\bwatch\b/);
|
|
66
|
-
|
|
67
|
-
const scriptHelp = runCli(['script', '--help'], env);
|
|
68
|
-
assert.equal(scriptHelp.status, 0, scriptHelp.stderr);
|
|
69
|
-
assert.match(scriptHelp.stdout, /\bcreate\b/);
|
|
70
|
-
assert.match(scriptHelp.stdout, /\brun\b/);
|
|
71
|
-
assert.match(scriptHelp.stdout, /\btemplates\b/);
|
|
72
|
-
});
|
package/tests/config.test.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import test, { before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
function importFresh(relativePath) {
|
|
8
|
-
const url = new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, import.meta.url);
|
|
9
|
-
return import(url);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function setTempEnv(tempRoot) {
|
|
13
|
-
const home = join(tempRoot, 'home');
|
|
14
|
-
const appData = join(tempRoot, 'appdata');
|
|
15
|
-
const localAppData = join(tempRoot, 'localappdata');
|
|
16
|
-
mkdirSync(home, { recursive: true });
|
|
17
|
-
mkdirSync(appData, { recursive: true });
|
|
18
|
-
mkdirSync(localAppData, { recursive: true });
|
|
19
|
-
|
|
20
|
-
const prev = {
|
|
21
|
-
HOME: process.env.HOME,
|
|
22
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
23
|
-
APPDATA: process.env.APPDATA,
|
|
24
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
process.env.HOME = home;
|
|
28
|
-
process.env.USERPROFILE = home;
|
|
29
|
-
process.env.APPDATA = appData;
|
|
30
|
-
process.env.LOCALAPPDATA = localAppData;
|
|
31
|
-
|
|
32
|
-
return prev;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function restoreEnv(prev) {
|
|
36
|
-
for (const key of Object.keys(prev)) {
|
|
37
|
-
if (prev[key] === undefined) delete process.env[key];
|
|
38
|
-
else process.env[key] = prev[key];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let tempRoot;
|
|
43
|
-
let prevEnv;
|
|
44
|
-
let store;
|
|
45
|
-
|
|
46
|
-
before(async () => {
|
|
47
|
-
tempRoot = mkdtempSync(join(tmpdir(), 'darksol-config-'));
|
|
48
|
-
prevEnv = setTempEnv(tempRoot);
|
|
49
|
-
store = await importFresh('../src/config/store.js');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
after(() => {
|
|
53
|
-
restoreEnv(prevEnv);
|
|
54
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('get/set config values', () => {
|
|
58
|
-
assert.equal(store.getConfig('chain'), 'base');
|
|
59
|
-
store.setConfig('chain', 'ethereum');
|
|
60
|
-
assert.equal(store.getConfig('chain'), 'ethereum');
|
|
61
|
-
const all = store.getAllConfig();
|
|
62
|
-
assert.equal(all.chain, 'ethereum');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('RPC management works for getRPC/setRPC', () => {
|
|
66
|
-
const defaultBaseRpc = store.getRPC('base');
|
|
67
|
-
assert.ok(typeof defaultBaseRpc === 'string' && defaultBaseRpc.startsWith('http'));
|
|
68
|
-
|
|
69
|
-
const custom = 'https://rpc.example.invalid';
|
|
70
|
-
store.setRPC('base', custom);
|
|
71
|
-
assert.equal(store.getRPC('base'), custom);
|
|
72
|
-
|
|
73
|
-
store.setConfig('chain', 'base');
|
|
74
|
-
assert.equal(store.getRPC(), custom);
|
|
75
|
-
});
|